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的处理方式。)

年関係するできごと備考2009AngularJS初版Googleが支援2010Knockout.js, Backbone.js初版
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)」

種類呼び出し定義AMDrequirejs.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很容易理解,所以我将把解释交给你。

angularAMD: The Simple Way to Integrate AngularJS and RequireJS.png

总结

到目前为止,我们看到了五种模式。

    • 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
bannerAds