【译】构建angular.js应用的最佳实践
发布在AngularJS乱炖2014年8月15日view:10175
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

翻译原文链接:github 原文链接: Best Practices for Building Angular.js Apps 翻译:jnotnull


Burke Holland之前发了一篇文章解释了Angular如何构建应用,并且比较了browserify和require.js在Angular中的各自优缺点。

关于这两个工具的比较,其实我已经在好多项目中也涉及到了,我也找到了好多方法来使用他们。我正在写一本如何通过MEAN来构建Angular应用的书,因此对这个课题研究的也比较多。我想我已经找到了一个非常具体的框架。它比Burke Holland提议的简单的多。

特别说明的是,如果我在一个使用他的框架的项目中,我也会很满足的。它还是很不错的选择的。

然而在我们开始之前,Angular中的模块概念还是有点扑朔迷离,让我们花点时间来澄清一下。

JS中的模块是什么

JS本身没有加载模块的能力。模块的含义就是不同的人做不同的事情。在本文中,我们使用如下定义:

模块允许我们按照不同的逻辑把代码切割成不同的部分。在JS中,它还可以阻止global中的变量冲突问题。

JS开发新手可能会认为我们对模块有点小题大做。我想澄清一件问题:模块不是为了懒加载时候需要加载的模块。Require.js确实有这个功能,但是这个不是它重要的原因。模块之所以重要是因为语言没有提供这个功能,但是JS确实很需要这个功能。

模块可能由不同的框架组成。它可以是Angular,也可能是lodash,抑或是你项目中别人分享的代码,一些你从网上找到的礼物,或者是你代码中分离出来的特性。

JS没有模块,因此我们必须通过一些途径去解决没有模块带来的问题(如果你理解了JS模块那你可以忽略接下来的章节)

.noConflict()

让我们来描述下问题。假如你想在你的项目中使用jQuery,那jQuery会定义一个全局变量$。而恰好在你的代码中已经存在了这个变量,那么就会导致冲突。多年来,我们都靠.noConfict()方法来解决这个问题。确实.noConfict()允许我们去改变你引用库的名称。

如果你有这个问题,你可以这样使用:

<scrip>var $ = 'myobject that jquery will conflict with'
</scrip>
<scrip src='//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js'></scrip>
<scrip>
// $ is jQuery since we added that script tag
var jq = jQuery.noConflict();
// $ is back to 'myobject that jquery will conflict with'
// jq is now jQuery</scrip>

这已经成为大多数JS中最常见的解决方案,但却不是最优的解决方案。这种解决方案没有很好的划分你的代码,它强制你在使用之前定义了一些东西,就想上面的代码,要求导入的代码都要去实现一个.noConfict()方法。

如果你感觉有点疑惑,那接着向下读。在想知道解决方案之前,去了解问题是非常重要的。

没人会喜欢使用.noConfict(),因此他们寻找了一些其他方法去解决问题。我们在这里提供了四种方案:

Require.js (AMD标准实现) Browserify (CommonJS标准实现) Angular dependency injection(Angular的依赖注入) ES6 modules(ES6的模块)

每个方案都有它的优点和缺点,他们每个都有所不同。你甚至一起使用他们。我将讲述它们每一个都提供了什么,如何配合Angular,我建议选择哪一个。

样例

让我们开始一个简单的Angular应用。

这里是获得Github用户李彪的简单实现。

代码在这里,但是它是一个完整版本了。认真读完别剧透!

所有的代码可以在一个文件中:

var app = angular.module('app', [])

app.factory('GithubSvc', function ($http) {
  return {
    fetchStories: function () {
      return $http.get('https://api.github.com/users')
    }
  }
})

app.controller('GithubCtrl', function ($scope, GithubSvc) {
  GithubSvc.fetchStories().success(function (users) {
    $scope.users = users
  })
})

首先我们定义了一个app对象,也就是我们的模块。然后我们定义了一个能提供github上用户离别的服务GithubSvc。

后面我们定义了一个控制器用来通过调用service去获得用户数组并加载到$scope中。

把代码切割刀不同文件中

这里的代码都在一个文件中,这太糟糕了。这对于一个app来说绝对不可以的。也许我是一个倔老头,但是当我第一次看Angular和它的样例都解释是这样做的时候,我想说的是合理的去分割代码真的很有必要。

我想我的代码应该有下面这样的结构: src/module.js src/github/github.svc.js src/github/github.ctrl.js

注:如果应用变得很大,我们还需要分解gitbub模块 一个可行的办法就是按照功能去分解。 src/module.js src/services/github.svc.js src/controllers/github.ctrl.js

这两种划分我都能能接受。一个大的应用可能倾向于前者,而小的引用则倾向于后者。

但是,如果我们不使用browserify或者require.js加载器,那我们必须通过添加script标签去加载每个文件,这可能很快增长到上百个文件。

因为有性能原因,我们不能有太多的script标签,浏览器是链式加载的,但是在一个时间点只能加载一个。糟糕的速度可能会让我们的用户流失。

因此我们的目标就是:

我们需要在开发的时候拥有多个文件,但是在浏览器运行时候可以按需加载(不是通过script标签去一个一个加载)

这就是为啥我们研究诸如require.js和browserify。Angular允许你按照逻辑去分割代码,而不是按照文件。我将给个更加简单的方法,但是首先让我们比较下当前可用的模块加载器。

Require.js-太复杂

Require.js在推动JS模块化上功不可没。它允许你把需要的JS文件定义为依赖,同时可以在浏览器端按需加载。

它完成两项任务:加载模块和控制加载的顺序。

不幸的是,这要求你的代码必须用一种特殊的方式去写,而这是有陡峭的学习曲线的,我们可能对循环引用控制的不好。而这个就可能发生在Angular的顶层模块系统中。

Burke Holland列出了在Angular中使用require.js的场景,因此我建议你再认真读一遍,最好能找出一个你不用require.js的理由。

RequireJS和AngularJS一起使用就像是在禁闭岛上度假。水面上看起来一切非常平静,但是水下确实暗流涌动-Burke Holland

require.js的按需加载模块的能力是另一个我不想让它和Angular一起使用的原因。这个可能是一些人想要的,但是我绝对不会再任何项目中这样做。

我要强调一点人们经常犯的错误:模块系统不仅仅是按照需要加载模块。是的,require.js确实这么做的,但是这不是require.js有用的原因。模块是用于按照逻辑来分解代码。

不管怎么说,这是一个糟糕的方案,因此我不会告诉你该怎么做。我为什么要说呢,因为总有人问我如何集成require.js和Angular。

Browserify-一个更好的模块加载器

require.js用于在浏览器端加载模块,而browserify则运行于服务端。你不能把browserify文件运行于浏览器端。

它使用了类似于Node.js的模块加载方式,我们看下代码:

var moduleA = require('my-module')
var moduleB = require('your-module')

moduleA.doSomething(moduleB)

这看起来非常完美,格式很容易月度。你就简单的定义一个变量,然后”require()”你的模块。导出模块也很容易写。

在Node中,这确实非常棒。但是它不能运行于浏览器端,因为它是同步的。在同步模式下,如果需要某个资源,http会去下载,而这整过过程浏览器都必须在等待。

在Node中非常好用是因为这些文件都在本地文件系统中,因此加载一个模块非常的快。

对于browserify,你可以这样写,它会合并所有的文件打包成一个文件供浏览器使用。同样,Burke的文章也说明了这一点。

另外,如果我刚才关于browserify的一些东东有些懵懂的话,不要担心,后面我说的一些建议肯定会比这个简单。

如果在非Angular项目中使用这个,确实非常棒。但是对于Angular,我们可以更加简单。

Angular依赖注入-解决我们的大部分问题

回到之前我们的例子,我想说下:

是创建service在先还是创建controller在先都无所谓。Angular会自己管理它们的依赖关系。这就为我们通过mocking service提供了方便。确实很棒,这是我推崇的第一个特性。

另外强调一点,对于这个方法,我们必须首先定义模块,然后才能使用app对象。这是Angular中为一个一个注意声明顺序的地方,但是非常重要。

我想要做的就是简单的把所有文件串联到一个文件,然后加载这个文件到HTML中。因为app对象必须首先被声明,因此我们只需要确定它是在其他东东之前就可以了。

Gulp合并

我将使用Gulp来完成这项工作。不要担心使用新的工具,我会用一个非常简单的方式使用它,你也会非常方便的把它集成到Grunt、Make或者其他任何编译工具中。你只需要它来和并文件。

我已经在所有流行的编译系统中做了尝试,Gulp远远超出我的期望。用它来编译CSS和JS,就是个福音。

你可能会想,我只是替换一个编译工具为另一个工具,你可能是对的。但是Gulp不仅仅如此。你还能够合并Gulp和其他工具,比如minification, CoffeeScript precompilation,sourcemaps, rev hash appending等等。诚然这些工作browserify也能做,但是一旦你学会如何使用Gulp来做的话,你能够用它来做一切东东。你只需要学习一点点东东。

你可以使用它来处理png,编译你的sass,开启一个开发环境Node服务器,或者运行任何你在node中写的代码。非常容易学习,它提供了一个接口给其他开发者。它提供给我们一个平台方便以后扩展。

我宁愿只要使用gulp watch就可以监视我所有的开发模式下的静态断言,而不用运行一个独立的node服务器,一个独立的sass监视器,还有其他需要管理文件的工具。

首先我将安装Gulp和gulp-concat

$ npm install --global gulp $ npm install --save-dev gulp gulp-concat

要说的是你需要在你的应用中存在一个package.json文件并且安装了Node。下面是Node应用的小招:

$ echo ’{}’ > package.json

然后我们进入gulpfile.js文件做些改动:

var gulp = require('gulp')
var concat = require('gulp-concat')

gulp.task('js', function () {
  gulp.src(['src/**/module.js', 'src/**/*.js'])
    .pipe(concat('app.js'))
    .pipe(gulp.dest('.'))
})

这是个任务非常简单,他把src下的所有js文件合并到appljs文件中。因为参数是一个数组,所以名称为module.js的文件会被优先合并。不要太担心这段代码,当我把它压缩后我就会删除它。

如果你想一个人在家玩,你可以运行gulp js去编译即可。

关于Gulp的更多内容,你可以阅读我的文章,它会介绍如何使用它构建一个项目。

Icky全局变量

你还可以做的更好。你知道你如何创建了一个app变量么?它是一个全局变量。也许只有一个app全局变量不会有啥问题,但是如果我们拥有太多的模块,那很可能会产生冲突的问题。

幸运的是Angular能够非常容易的解决这些问题。angular.module()既是一个getter也是一个setter,如果你使用两个参数来调用:

angular.module('app', ['ngRoute'])

这个是一个setter。你创建了一个依赖ngRoute的模块app。(在这里我不会使用ngRoute,我只是想告诉大家这个看起来很像一个依赖的模块)

调用这个setter将获得一个模块对象,不幸的是你只能调用它一次。更令人失望的是,调用失败产生的错误信息可能会迷惑新手。

如果我们调用angular.module()只用一个参数:

angular.module('app')

这个设置方法同时也返回了一个模块对象,但是我们可以我们我可以无限制的访问。基于这个原因,我们重写下我们的代码: 之前的代码

app.factory('GithubSvc', function ($http) {
  return {
    fetchStories: function () {
      return $http.get('https://api.github.com/users')
    }
  }
})

重写为

angular.module('app')
.factory('GithubSvc', function ($http) {
  return {
    fetchStories: function () {
      return $http.get('https://api.github.com/users')
    }
  }
})

这里微妙的不同对于JS入门者来说可能没有什么。但是后者更加符合大众需求。对于大型项目来说我们要禁止使用全局变量。

对于深究者:我知道这里仍有一个全局对象angular,但是再避免它也没有啥意义了。

这里我们拥有了一个非常好的方法去构建原始文件。但是仍有基本才能构建一个运行良好的环境。比如,如果每次都不得不运行gulp js会变的非常痛苦。

Gulp Watch

这非常简单

var gulp = require('gulp')
var concat = require('gulp-concat')

gulp.task('js', function () {
  gulp.src(['src/**/module.js', 'src/**/*.js'])
    .pipe(concat('app.js'))
    .pipe(gulp.dest('.'))
})

gulp.task('watch', ['js'], function () {
  gulp.watch('src/**/*.js', ['js'])
})

这里定义了一个gulp watch任务,如果js文件发生变化,那就能被他检视到。

Minification

下面我们来谈下压缩。在Gulp中,我们能够从文件中创建一个流(gulp.src),然后把它们传递到其他工具中(minification, concatenation等等),然后输出到gulp.dest管道中。如果你知道unix管道的话,那你就会知道这其实是相同的道理。

总之我们只需要把minification添加到管道中,首先我们安装gulp-uglify去压缩:

$ npm install -D gulp-uglify

var gulp = require('gulp')
var concat = require('gulp-concat')
var uglify = require('gulp-uglify')

gulp.task('js', function () {
  gulp.src(['src/**/module.js', 'src/**/*.js'])
    .pipe(concat('app.js'))
    .pipe(uglify())
    .pipe(gulp.dest('.'))
})

但是我们有一个问题!它弄脏了Angrular需要依赖的函数参数。这个代码不会工作了,如果你对这个问题不了解,那接着读。

我们能够使用guly数组语法,也可以看下我们的ng-gulp-annotate。

NPM install: $ npm install -D gulp-ng-annotate

下面是最新的代码:


var gulp = require('gulp')
var concat = require('gulp-concat')
var uglify = require('gulp-uglify')
var ngAnnotate = require('gulp-ng-annotate')

gulp.task('js', function () {
  gulp.src(['src/**/module.js', 'src/**/*.js'])
    .pipe(concat('app.js'))
    .pipe(ngAnnotate())
    .pipe(uglify())
    .pipe(gulp.dest('.'))
})

我希望你认真看我传递的值。我如何通过使用转换Gulp插件格式的方法去快速解决构建中我遇到的各种问题。

Sourcemaps

每个人都喜欢调试。我们已经构建了一个JS文件,当前也该讨论这个话题了。如果你想在chrome中使用console.log或者运行debugger,它不会展示你想要的信息。

下面就是Gulp任务要做的东东(install gulp-sourcemaps)

var gulp = require('gulp')
var concat = require('gulp-concat')
var sourcemaps = require('gulp-sourcemaps')
var uglify = require('gulp-uglify')
var ngAnnotate = require('gulp-ng-annotate')

gulp.task('js', function () {
  gulp.src(['src/**/module.js', 'src/**/*.js'])
    .pipe(sourcemaps.init())
      .pipe(concat('app.js'))
      .pipe(ngAnnotate())
      .pipe(uglify())
    .pipe(sourcemaps.write())
    .pipe(gulp.dest('.'))
})

为啥合并是更好呢

合并文件因为简单所以更好。Angular 已经提供了代码加载给我们,我们只需要管理下文件。到目前我们知道的模块setter和getters,我们没有啥可以担心的。

同样任何新的文件都可以被探知。没有动作表明我们需要browserify,我们也不需要require.js来管理依赖。

这就使得我们少操心一件事了,也不用学习多余的东西了。

我们构建了什么

这是我们最终的代码。这是一个相当完美的开局。

一个框架 一个开发服务器 一个压缩工具 一个代码映射 一个样式 没有全局变量 没有过多的script标签 没有复杂的构建步骤

我列出这些无关Gulp,但是你可能听说:我实在太喜欢这个东东了。正如我之前提到的,你可以实现一个类似的合并工具。

如果感兴趣,我可以轻松的扩展去增加测试/CSS/模板等等。我已经实现了代码

第三方代码

对于第三方代码:如果它在CDN上,那就使用它。如果用户已经从网上加载了代码,因为有缓存,浏览器会重用它。

If it’s something not available on a CDN, I would still probably use a new script tag but load it off the same server as the app code. Bower is good for keeping these sorts of things in check. 不过不是在CDN上,我仍然建议你使用一个新的script标签,但是建议下载到自己代码的服务器上。Bower对于管理文件分类非常棒。

如果你有很多第三方代码,你应该合并和压缩它们,但是我可能会在我们的代码中分开存放,防止有一个巨型文件。

ES6 Modules — 真正的解决方案

JS下一代版本会内置模块系统。它们的目标是让CommonJS (browserify) 和 AMD (require.js)的粉丝们都能用上。这个版本是一个出路,你将不会再去依赖没有shim的库。当它真正来到来的时候,这篇文章就不再需要了。

Angular 2.0

值得一说的是,Angular2.0将会使用ES6的模块机制,到那个时候我们就彻底解放了。当前如果你使用Angular,你还需要不同的选择。 Angular 2.0还是值得期待的,它将带来一系列有用的包机制。

Angular 2.0会使用一个独立的库di.js来处理这些。它非常简单,它只是基于ES6模块上的轻量级的层。我们可以在项目中很容易的使用它。但是在这之前你不得这样来使用JS模块来处理这些问题。

同志们,我为JS在进步赶到高兴,但是我们不得不学习更多的东西。

PS 我有这些例子的异步实现,你有兴趣么?

评论
发表评论
2年前
添加了一枚【评注】:应用吧
4年前

@让周飞 错了,是按需加载

4年前

怎么解决依赖加载呢

4年前

@p2227 感谢 已经修正

4年前

angular拼错了

4年前

@前端乱炖 感谢分享

4年前

我们团队是使用访问时编译。

对于css,我们用less写,本地和测试都跑了一个静态资源处理环境,访问某个css的时候会去寻找对应的less然后编译返回给浏览器。

对于js,开发时不打包不压缩,发布到cdn的时候再通过一个管道工具自动合并打包压缩上线。

4年前

@ariesjia 如果速度跟不上的话,那确实是一种灾难,严重影响开发效率。其实最理想的方案是开发模式下还是分散的,只要归档时候触发构建就可以了。

4年前

我们之前项目也是采用grunt watch 合并的方式来的,稍微还是有一点不爽,每次保存完代码,切到浏览器满心期望看到效果时候,合并代码还在处理中。

WRITTEN BY
jnotnull
专注于前端开发
TA的新浪微博
PUBLISHED IN
AngularJS乱炖

有关AngularJS优秀实践的见闻

我的收藏