在写大型Javascript应用程序的时候,我们往往把不同的模块代码放到不同的文件中。这样提高了代码的可维护性,但是,把这些Javascript文件有序并且一个不落地放到<script> tag中去就比较烦人了,特别是Javascript文件很多的时候。在大型的AngularJS的大型应用中当然也存在这种问题。幸运的是,我们有工具来管理Javascript文件的依赖加载问题。
本篇文章中,我们会看到在AngularJS中使用RequireJS来解决依赖加载的问题,并且使用Grunt构建工具把模块化的Javascript文件打包合并。
RequireJS简介
RequireJS是一个Javascript库,它的功能是让Javascript模块的Lazy Loading。这里的模块是指一个以RequireJS规范编写的Javascript文件。RequireJS支持AMD规范,它提供了一些简单的API来创建模块和声明依赖。(简单来说,require()用于声明依赖,define()用于创建模块。)
首先,RequireJS需要一个配置文件,其配置文件也是一个Javascript文件:
require.config({ map:{ // Maps }, paths:{ // Aliases and paths of modules }, shim:{ // Modules and their dependent modules } });
定义一个模块,我们使用 define() 函数:
define([ // Dependencies ], function( // Dependency objects ){ function myModule() { // Can use the dependency objects received above } return myModule; });
define函数的第一个参数用来声明有哪些依赖的模块。Dependencies和Dependency objects的名字一一对应。通常来说,我们在模块的最后返回一个对象,作为这个模块提供的接口暴露出来。当然,这并不是强制性地要求要返回一个对象。
AngularJS的依赖注入和RequireJS的依赖管理
一个AngularJS开发人员会常问的问题是:AngularJS的依赖注入和RequireJS的依赖管理有什么区别。首先要区分开的是:RequireJS的依赖管理,它管理的是模块(即JS文件);AngularJS的依赖注入则管理的是Javascript对象。
当RequireJS在在家一个模块的时候,它会检查这个模块有哪些依赖项,然后先加载这些依赖项。这些加载后的模块会被缓存,当以后有别的模块也依赖它的时候变可以直接使用缓存的模块对象了。而AngularJS中得依赖注入则是通过injector来实现的,injector带有一个依赖模块(这里模块是指service、controller等,不是JS文件)的名字的数组和对应的模块对象。每当一个模块创建后,injector维护的这个[{模块名:模块对象}]数组便会加入一个元素来保存新的对应关系。当我们在controller或者service中声明了依赖的时候,就会根据名字去injector维护的数组中查找对应的对象。
在AngularJS中使用RequireJS
示例代码下载
这个简单例子包含两个页面。它的外部依赖关系如下:
- RequireJS
- jQuery
- AngularJS
- Angular Route
- Angular Resource
- Angular UI ngGrid
我们把这些文件按上述顺序直接放到HTML的<script>中。我们还有5个JS项目文件。下面我们来具体看看这几个文件。
把AngularJS组件定义为RequireJS模块
一个AngularJS Module会包括以下三部分:
- 函数定义
- 依赖注入
- 注册到Angular的Module中
我们先看看前面两个部分是怎么回事。
首先看config.js。它不依赖任何模块,最后返回config对象。其中用到$inject来实现AngularJS的依赖注入。实际上,AngularJS提供了三种方式来进行依赖注入,这里用的是第二种。
- Using the inline array annotation (preferred)
- Using the
$inject
property annotation - Implicitly from the function parameter names (has caveats)
define([],function(){ 'use strict'; function config($routeProvider) { $routeProvider.when('/home', {templateUrl: 'templates/home.html', controller: 'ideasHomeController'}) .when('/details/:id',{templateUrl:'templates/ideaDetails.html', controller:'ideaDetailsController'}) .otherwise({redirectTo: '/home'}); } config.$inject=['$routeProvider']; return config; });
类似地,我们定义controller:
define([], function() { 'use strict'; function ideasHomeController($scope, ideasDataSvc) { $scope.ideaName = "Todo List"; $scope.gridOptions = { data: 'ideas', columnDefs: [ {field: 'name', displayName: 'Name'}, {field: 'technologies', displayName: 'Technologies'}, {field: 'platform', displayName: 'Platforms'}, {field: 'status', displayName: 'Status'}, {field: 'devsNeeded', displayName: 'Vacancies'}, {field: 'id', displayName: 'View Details', cellTemplate: 'View Details'} ], enableColumnResize: true }; ideasDataSvc.allIdeas().then(function(result){ $scope.ideas=result; console.log($scope.ideas); }); } ideasHomeController.$inject=['$scope','ideasDataSvc']; return ideasHomeController; });
我们也可以用另一种方式来写这个ideasHomeController.js文件。首先在define的依赖项中声明依赖ideasModule.js。然后把controller注册到ideasApp Module中去。
define(["app/ideasModule"], function(ideasModule) { 'use strict'; ideasModule.controller('ideasHomeController', ['$scope', 'ideasDataSvc', function($scope, ideasDataSvc){ $scope.ideaName = "Todo List"; $scope.gridOptions = { data: 'ideas', columnDefs: [ {field: 'name', displayName: 'Name'}, {field: 'technologies', displayName: 'Technologies'}, {field: 'platform', displayName: 'Platforms'}, {field: 'status', displayName: 'Status'}, {field: 'devsNeeded', displayName: 'Vacancies'}, {field: 'id', displayName: 'View Details', cellTemplate: 'View Details'} ], enableColumnResize: true }; ideasDataSvc.allIdeas().then(function(result){ $scope.ideas=result; console.log($scope.ideas); }); }]); });
需要注意的是,用这种方式,我们得在ideasModule.js中define的依赖项中把’app/ideasHomeController’去掉,不然就是循环依赖了。我们可以把’app/ideasHomeController’这一项放到main.js中来解决这个问题。
ideasModule.js中定义了名为ideasApp的Angular Module。它要用到其他JS文件中的对象,所以define中声明了依赖项,使用的是相对路径。
define(['app/config', 'app/ideasDataSvc', 'app/ideasHomeController', 'app/ideaDetailsController'], function(config, ideasDataSvc, ideasHomeController, ideaDetailsController){ 'use strict'; var app = angular.module('ideasApp', ['ngRoute','ngResource','ngGrid']); app.config(config); app.factory('ideasDataSvc',ideasDataSvc); app.controller('ideasHomeController', ideasHomeController); app.controller('ideaDetailsController',ideaDetailsController); });
在这里不能使用ng-app指令来 bootstrap Angular应用。因为使用RequireJS后,那些JS文件是异步加载的。我们可以使用angular.bootstrap()来手动加载。
require(['app/ideasModule'], function() { 'use strict'; angular.bootstrap(document, ['ideasApp']); } );
配置Grunt的RequireJS Task
使用Grunt的grunt-contrib-requirejs插件,配置如下:
requirejs: { options: { paths: { 'appFiles': './app' }, removeCombined: true, out: './app/requirejs/appIdeas-combined.js', optimize: 'none', name: 'main' }, dev:{ options:{ optimize:'none' } }, release:{ options:{ optimize:'uglify' } } }
requirejs Task有两个Target,一个是dev,一个是release,分别生成unminified和minified的组合而成的单个JS文件。能生成单个整合的文件,是因为RequireJS提供了一个打包压缩工具r.js来对模块进行合并压缩。
总结
RequireJS让我们更优雅地在Javascript中进行模块化编程!
Leave a Reply