Angular和Browserify之间的微妙关系太过烦琐
这篇文章的内容似乎有些过时了。一段时间后,Angular增强了对CommonJS的支持,与之前相比require的兼容性也有所改善。然而,由于我自己已经离开了Angular,所以无法完全跟上最新进展。
如果无意中踏入这篇文章的人,强烈建议参考@armorik83所述的详细内容“AngularJS现代实践”中的CommonJS + Browserify风格部分,适合喜欢这种风格的人。
AngularJS和Browserify的关系非常微妙。可以说类似于信仰不同的情侣关系(比喻而已)。对于习惯了CommonJS的人来说,Angular的DI机制实在有点碍事。但是又不能不使用,因为它是Angular的核心部分,所以他们两个为了距离的问题而感到困扰。当然,也可以选择与React或Ampersand.js结合,但那是另外一回事了。我想我们应该考虑如何妥协,或者是否应该寻找另一个伴侣。
在中国本土上重新表达这句话会是:与时代背景相关的
AngularJS的初始版本相当古老,可以追溯到2009年。按照顺序,Backbone.js为2010年,Ember.js为2011年。因此,最初确实需要独立实现模块加载器。但是,现在已经是2015年了,如果能使用AMD或者CommonJS就好了…!不可避免地会这样思考。实际上,未来的Angular 2.0版本将分离DI容器,更容易与其他框架集成。(我也很关注2.0版本中的web components的处理方式。)
2011Ember.js初版SproutCore 2 から2013React初版Facebookが支援2014HTML5 正式勧告
2015ES6 勧告予定
????Web Components
模块加载器的宗派
当你接触Node本身或最近的Node工具时,会感觉到像是默认使用了CommonJS,但其在客户端的普及其实进展不大。直到Browserify重新开始流行起来,为客户端打开了大门,之前几乎只有AMD这一选择,所以也无可奈何了。而2014年,webpack和duo等不是Browserify的其他支持CommonJS的工具成为了话题。
AMD: RequireJS, webpack, Dojo, …
CommonJS: Node.js, Browserify, webpack, duo, …
UMD: AMDとCommonJSの両方に対応する書き方
ECMAScript 6: 次期バージョンの JavaScript (2015年6月勧告予定)
独自系: AngularJS ※Angular用語的には「依存性の注入(DI)」
requirejs.config({ backbone: 'path/to/file' })
define(['backbone'], function (Backbone) {/* */})
CommonJSvar Backbone = require('backbone')
module.export = function() {/* */}
ES6 (参考)import { hello } from 'somemodule'
export function hello {/* */}
「Angular的DI是通过require实现的」
「DI = 依赖注入」是令Angular初学者头疼的问题之一。
-
- 「他言語でいうところのDIとなんか違うような…?」
-
- 「やっていることは、モジュール連携の作法に見える」
- 「独特でよくわからん」
听起来好像听到了很多不同的声音。但是如果过于执着于”DI”这个词,会变得很难理解。相反,如果对CommonJS很熟悉的话,应该简单地认为它是”require”的替代品,这样会更容易理解。
Angular和模块加载器
现在,让我们开始具体论述。HTML中充斥着script标签,只有避免全球污染是人们所希望的。由于TodoMVC的HTML如下所示,因此实际使用应该慎重考虑。
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers/todoCtrl.js"></script>
<script src="js/services/todoStorage.js"></script>
<script src="js/directives/todoFocus.js"></script>
<script src="js/directives/todoEscape.js"></script>
我希望能够把这个东西整理得这样清爽!
<script src="app.js"></script>
那么,让我们思考几种模式吧。
Angular + gulp-concat 可以进行重述为:Angular加上gulp-concat。
开场第一个就是浮混gulp-concat。
简单而又不过分努力的平衡的好人,可以说是解决方案。但是,总感觉不太满意…><
一応解释一下,我们将控制器和服务分别写在不同的文件中,然后简单地将这些文件合并在一起。下面是一个简化的示例。(由于会变得复杂,我们将假设Angular已经在全局中定义)
# ./app.coffee
app = angular.module 'app', []
遺憾的是,這個app是全球性的。接下來是控制器和
# ./controller/todoCtrl.coffee
app.controller 'todoCtrl', ($scope, todoStorage) ->
$scope.todos = todoStorage.get()
# 処理が続く
这是一个服务。
# src/service/todoStorage.coffee
app.factory 'todoStorage', ->
STORAGE_ID = 'todos-angularjs-perf'
ret =
get: -> JSON.parse localStorage.getItem STORAGE_ID
put: (todos) -> localStorage.setItem STORAGE_ID, JSON.stringify todos
這是一個用於結合(concat)的gulpfile。最好加上source map。
# gulpfile.coffee
gulp = require 'gulp'
coffee = require 'gulp-coffee'
concat = require 'gulp-concat'
uglify = require 'gulp-uglify'
annotate = require 'gulp-ng-annotate'
gulp.task 'angular', ->
gulp.src [
'src/app.coffee'
'src/*/*.coffee'
]
.pipe coffee bare: true # CoffeeScriptのコンパイル
.pipe concat 'app.js' # 結合
.pipe annotate() # アノテーションをつけて`uglify`に備える
.pipe uglify() # ミニファイ
.pipe gulp.dest 'dist/'
Angular和Browserify(相对较弱)
第二,接下来我们会渐渐接近和Browserify相似的软件。
几乎一样地进行了连接,但是我们是使用require从app.coffee文件中加载其他文件。需要注意的是,由于AngularJS不是CommonJS,所以很难像angular = require ‘angular’这样去做。
# src/app.coffee
angular.module 'app', []
require './controller/todoCtrl'
require './service/todoStorage'
与之前不同的是,无法直接从读取的文件中引用到app对象。Angular是一个全局变量,因此需要通过module函数来获取引用。顺便提一下,module函数可以根据传入的两个参数来生成模块,也可以根据传入的一个参数来获取模块。
# src/controller/todoCtrl.coffee
app = angular.module 'app'
app.controller 'todoCtrl', ($scope, todoStorage) ->
$scope.todos = todoStorage.get()
# 処理が続く
从这个角度看,它似乎引入了不必要的复杂性,但通过require来加载所需的npm或bower模块是很有吸引力的。而且,在app.factory之外编写变量也不会污染全局,这让人感到轻松。
# src/service/todoStorage.coffee
app = angular.module 'app'
STORAGE_ID = 'todos-angularjs-perf'
app.factory 'todoStorage', ->
get: -> JSON.parse localStorage.getItem STORAGE_ID
put: (todos) -> localStorage.setItem STORAGE_ID, JSON.stringify todos
以下是用于 Browserify 的 gulpfile 的示例。我在这里使用了 Browserify 的 Transform,但你也可以像之前的例子中一样使用 gulp-ng-annotate。顺便提一下,如果你使用 bower 作为模块管理工具,还需要将 debowerify 添加到 Transform 中。
gulp = require 'gulp'
browserify = require 'browserify'
coffeeify = require 'coffeeify'
annotatify = require 'browserify-ngannotate'
source = require 'vinyl-source-stream'
streamify = require 'gulp-streamify'
uglify = require 'gulp-uglify'
gulp.task 'angular', ->
browserify
entries: ['src/app.coffee']
extensions: ['.coffee']
.transform coffeeify # CoffeeScriptのコンパイル
.transform annotatify # アノテーションをつけて`uglify`に備える
.bundle()
.pipe source 'app.js'
.pipe streamify uglify() # ミニファイ
.pipe gulp.dest 'dist/'
Angular加上Browserify (中文)
而且,我会进一步接近一点,总的来说,我会采用类似CommonJS的操作方式。
# src/app.coffee
angular
.module 'app', []
.controller 'todoCtrl', require './controller/todoCtrl'
.factory 'todoStorage', require './service/todoStorage'
在(弱)的例子中,我没有故意使用module.export。
# src/controller/todoCtrl.coffee
module.exports = [
'$scope', 'todoStorage'
($scope, todoStorage) ->
$scope.todos = todoStorage.get()
# 処理が続く
]
为了适应Angular模块的形式,将其包装成函数并返回也很重要。虽然使用了Angular的魔法$scope和todoStorage让人感到疑惑,但依然会忍受这一点。
# src/service/todoStorage.coffee
STORAGE_ID = 'todos-angularjs-perf'
module.exports = ->
get: -> JSON.parse localStorage.getItem STORAGE_ID
put: (todos) -> localStorage.setItem STORAGE_ID, JSON.stringify todos
gulpfile的内容与之前并没有太大变化。然而,如上所述,如果使用.controller或.factory函数等类似的形式进行require,将无法进行自动注解。因此,在压缩方面会有一定的劣势,但我们已将配置设置为不启用mangle选项。(修正:如果以数组形式传递,则是可行的)
gulp = require 'gulp'
browserify = require 'browserify'
coffeeify = require 'coffeeify'
source = require 'vinyl-source-stream'
streamify = require 'gulp-streamify'
uglify = require 'gulp-uglify'
gulp.task 'angular', ->
browserify
entries: ['src/app.coffee']
extensions: ['.coffee']
.transform coffeeify # CoffeeScriptのコンパイル
.bundle()
.pipe source 'app.js'
.pipe streamify uglify mangle: false # ミニファイ (mangleなし)
.pipe gulp.dest 'dist/'
角度+瀏覽器化 (強大)
因为我们两个已经习惯了,所以让我们坦诚地交换真心话吧。
在先前的例子中,我們將 Angular 的服務先作為 Angular 模組載入,然後再使用它們,但有些感覺徒勞。考慮到可能會在 HTML 模板中調用,主要還是使用指令和過濾器,所以我們會將服務徹底從 Angular 中分離出來。(不過,對於核心服務和插件作為提供的情況,我們只能遵循 Angular 的語法,無可避免會感到兩種標準的差異)。
# src/app.coffee
angular
.module 'app', []
.controller 'todoCtrl', require './controller/todoCtrl'
噢…终于变得更像普通的JavaScript了。
# src/controller/todoCtrl.coffee
todoStorage = require '../service/todoStorage'
module.exports = [
'$scope'
($scope) ->
$scope.todos = todoStorage.get()
# 処理が続く
]
在这个例子中,因为没有必要遵循Angular的规范,所以我们返回了一个对象(也就是说,取消了函数的封装)。
# src/service/todoStorage.coffee
STORAGE_ID = 'todos-angularjs-perf'
module.exports =
get: -> JSON.parse localStorage.getItem STORAGE_ID
put: (todos) -> localStorage.setItem STORAGE_ID, JSON.stringify todos
※由于gulpfile与示例中相同,故省略不列举。
Angular + RequireJS: Angular与RequireJS的结合
有另一个角色出场了,抱歉,我已经力竭了。因为angularAMD很容易理解,所以我将把解释交给你。

总结
到目前为止,我们看到了五种模式。
-
- Angular + gulp-concat
-
- Angular + Browserify (弱)
-
- Angular + Browserify (中)
-
- Angular + Browserify (強)
- Angular + RequireJS
就个人而言,我觉得这两个选项都不太合适。但是如果要选择的话,我倾向于选择(强)或者放弃Browserify,只使用gulp-concat。
实际上,我相信从Angular源码中单独进行require操作,使用”Angular + Browserify (烈)”这种粗略的方法也是可能的,但是由于这是一条十分曲折的道路,我在进行这项尝试时有所顾虑,因此导致了现在这篇文章的产生。请不要见怪。
请参考
-
- Best Practices for Building Angular.js Apps
- Requiring vs Browserifying Angular