尽管大家都对angular2和ES6而激动不已,但是并不代表我们不能用ES6开发angular1.x项目。今天我们就讲一下怎么用ES6开发angular1.x项目。
Webpack是一个模块加载器,大体来说它可以根据依赖关系生成模块的静态文件。举个例子:
有一个模块my_module.js
import foo from './foo'; foo.bar();如果我们用Webpack来打包my_module.js,就可以获取它的依赖(foo.js),最后生成一个包含两者的静态文件。
所以Webpack的作用就是:你告诉它入口文件,它来处理剩下的事情。对于angular应用来说这个入口就是main模块。以这个main模块为起点获取所有依赖的文件最后生成一个包含了所有应用功能的bundle.js文件。有了这个我们再也不用在页面里添加数不尽的script标签了。
Webpack并不是Gulp的替代品,但是它可以在不使用Gulp的情况下完成所有的事情,它们从原理上也是不同的。Gulp的原理是:获取对应目录中的所有js文件,先生成一个sourcemap文件,再合并所有这些文件(如果是线上环境还需要运行ng-annotate,uglify然后才完成sourcemap)。而Webpack的原理则在loaders,如果需要支持sass,我们可以这样做:
loaders: [{ test: /\.scss$/, loader: 'style!css!sass' }];这是如何工作呢?首先它会匹配项目中的所有.scss文件再把它们发送给loader。如果存在不只一个loader就使用!将它们分隔开,loader会从最后一个开始执行。
所有的.scss文件发送到sass loader后会被转化为css再发送给css loader。css loader读取它们生成css代码然后传递给style-loader,最后Webpack用它生成link标签的内容。
利用Webpack的功能我们就可以这么干,将scss引入到我们的应用中:
import './styles.scss'可以看出来Webpack的原理是从一个入口文件利用loaders将所有的依赖文件加载上来并合并。而ES6也需要专门的加载器babel-loader:
loaders: [ // SASS one omitted { test: /\.js$/, loader: 'babel', exclude: /node_modules/ } ];当需要加载一个javascript文件的时候它就会被babel编译。值得注意的是我们可以利用node_modules来大大提高效率(从node_modules加载比用相对路径快多了)。
有了Webpack它就可以帮我们做所有事情,我们只需要关注代码就好了。
我创建了一个workflow,你可以直接克隆下来使用,不用担心工具不熟了:
$ git clone https://github.com/Foxandxss/angular-webpack-workflow my_app $ cd my_app $ npm install $ npm run devnpm run dev 会开启一个带有livereload功能的webpack-dev-server,就可以访问http://localhost:8080来查看应用了。
从现在开始你的应用结构文件命名都可以根据你的需要来,下面是我使用ES6开发angular1.x项目的代码。
打开入口文件src/app.js输入代码:
angular.module('app',[]);像之前介绍的Webpack应该将app.js这个文件和所有依赖合并生成结果文件,可是行得通吗?当然不行。因为angular在Webpack生成build.js的时候并没有加载,最终文件没包涵angular当然跑不通。那我们怎样告诉Webpack去加载Angular呢?
require('angular'); angular.module('app', []);用require我们可以告诉Webpack打包最终的build.js的时候加载angular。有了Angular我们就可以用angular对象创建app模块。
一切都很好,但是还不够好。require模块之后最好能定义它,Angular被require进来之后虽然会创建一个全局的angular变量,但是这并不清晰。。。
这样是不是更好呢?
const angular = require('angular'); angular.module('app', []);我认为是的,Angular会输入一个angular对象而我们可以通过变量angular来使用它,创建modules以及components等等。你还可以使用ES6语法来加载模块:
import angular from 'angular'; angular.module('app', []);个人来说我更喜欢ES6语法,但是前面那么写也是很好的。
跟Angular没有关系,但是bootstrap是最常用的css框架,我们把它引入进来。怎么做呢?
$ npm install --save bootstrap然后import进来
import 'bootstrap/dist/css/bootstrap.css'; import angular from 'angular'; angular.module('app', []);It works!
为什么要在这里加载bootstrap呢?这个不是创建app模块的启动文件么?当然,但是我们可以把bootstrap当作全局的主css,可以说:我们将bootstrap.css全局引入。虽然在其他内部依赖文件也可以引入,虽然那样引入也可以作用到全局,但是从语义上来说我们在这里定义更加合适。
用ES6创建config函数跟用ES5并没什么不同,就是export这个函数让其他文件可以引用。
src/app.config.js
routing.$inject = ['$urlRouterProvider', '$locationProvider']; export default function routing($urlRouterProvider, $locationProvider) { $locationProvider.html5Mode(true); $urlRouterProvider.otherwise('/'); }我们在config函数中定义了html5Mode(这个是处理路由#时使用,不懂请google),并且定义了默认路由/。注意这里我们使用了export default输出routing函数。
注意:其实这里并不需要像我这样使用$inject,Webpack有一个ng-annotate的loader,可以让Webpack帮我们做注释。个人来说我更喜欢$inject语法。在我的workflow中并没有安装这个loader,不过这是小事一桩。
现在config函数已经写好了,我们可以在用的地方import进来了:
import 'bootstrap/dist/css/bootstrap.css'; import angular from 'angular'; import routing from './app.config'; angular.module('app', []) .config(routing);现在注册已经完成,路由也已经定义清楚。我们还用到了ui-router,这里需要安装一下:
$ npm install --save angular-ui-router然后就可以import进来作为app的依赖。但是import之后如何使用呢?angular引入之后会返回一个对象,但是ui-router并不会。angular有自己的模块系统,它需要的是我们想要加载的模块的名字,比如:
angular.module('app', ['ui.router']);但是我们又需要将ui-router包含进bundle.js中,因此我们需要一个一石二鸟的办法:
import 'bootstrap/dist/css/bootstrap.css'; import angular from 'angular'; import uirouter from 'angular-ui-router'; import routing from './app.config'; angular.module('app', [uirouter]) .config(routing);习惯上额外的模块只要输出模块的name就可以了,所以uirouter就是字符串'ui.router'。
接下来增加一个例子功能,我们需要创建一个新的module,一个controller,一个template和路由cofig。先从controller开始吧:
export default class HomeController { constructor() { this.name = 'World'; } changeName() { this.name = 'angular-tips'; } }在ES6语法中一个controller就是一个class,并且使用controllerAs语法我们可以将所有变量和方法都放到this里面。我们先初始化一个name变量并且创建一个按钮可以改变name变量。export之后就可以在别的文件中引用这个controller了,接下来再创建temaplate:
<div class="jumbotron"> <h1>Hello, {{home.name}}</h1> </div> <button class="btn btn-primary" ng-click="home.changeName()">Change</button>routing config:
routes.$inject = ['$stateProvider']; export default function routes($stateProvider) { $stateProvider .state('home', { url: '/', template: require('./home.html'), controller: 'HomeController', controllerAs: 'home' }); }看到我们是怎么加载template的吗?有了Webpack我们就可以将html文件直接require进来,Webpack会返回template的字符串。接下来就是module的代码啦:
src/features/home/index.js
import angular from 'angular'; import uirouter from 'angular-ui-router'; import routing from './home.routes'; import HomeController from './home.controller'; export default angular.module('app.home', [uirouter]) .config(routing) .controller('HomeController', HomeController) .name;注意这个文件的名字index.js,使用这个名字我们就可以直接import文件夹了。像之前提到的这里return了模块的name。
因为这个模块中我们使用到了ui-router,因此把它import进来了。实际上在app模块中我们已经import了它,这里引不引入关系不大,但是再次import可以让依赖关系更加清晰,而且如果其他应用中需要用到它,我们只需要简单的复制粘贴就搞定了,不用担心依赖出错。Angular也是同样的道理,Webpack不会做重复的合并动作。
再回到app模块,我们把这个新的模块添加到依赖:
// other imports omitted import routing from './app.config'; import home from './features/home'; angular.module('app', [uirouter, home]) .config(routing);最后把ui-view添加到index.html:
src/index.html
<!doctype html> <html ng-app="app" lang="en"> <head> <meta charset="UTF-8"> <title>Angular App</title> <base href="/"> </head> <body> <div class="container <ui-view></ui-view> </div> </body> </html>注意:如果修改index.html,我们需要重跑一下webpack。
试一试,it works!
接下来我想把跳转按钮的文字剧中,首先给这个按钮添加一个id。
src/features/home/home.html
<div id="home-header" class="jumbotron"> <h1>Hello, {{home.name}}</h1> </div> <button class="btn btn-primary" ng-click="home.changeName()">Change</button>再创建一个css文件来完成文字剧中的功能:
src/features/home/home.css
#home-header { text-align: center; }跟之前bootstrap.css一样,我们吧home.css引入进来,home模块中引入是不错的方式。
src/features/home/index.js
import './home.css'; import angular from 'angular'; import uirouter from 'angular-ui-router'; // Rest omitted搞定~
使用了ES6,我们不再需要factories了,只要services就足够了,因为class可以完美的匹配到service:
src/services/randomNames.service.js
import angular from 'angular'; class RandomNames { constructor() { this.names = ['John', 'Elisa', 'Mark', 'Annie']; } getName() { const totalNames = this.names.length; const rand = Math.floor(Math.random() * totalNames); return this.names[rand]; } } export default angular.module('services.random-names', []) .service('randomNames', RandomNames) .name;这样我们有了一个可以返回一个随机name的service,在我的代码中,我选择了奖service和module放到一个文件中最后export模块的name。
这个service本身没什么复杂的,就是有几个函数的class,要在home模块中使用它,我们先import进来吧:
src/features/home/index.js
// Rest of imports omitted import HomeController from './home.controller'; import randomNames from '../../services/randomNames.service'; export default angular.module('app.home', [uirouter, randomNames]) .config(routing) .controller('HomeController', HomeController) .name;没啥新鲜的,我们调用它的方法试试:
src/features/home/home.controller.js
export default class HomeController { constructor(randomNames) { this.random = randomNames; this.name = 'World'; } changeName() { this.name = 'angular-tips'; } randomName() { this.name = this.random.getName(); } } HomeController.$inject = ['randomNames'];我们在构造函数上将service注入,并将其付值给一个本地变量,然后就可以使用啦~
稍微改动一下模版就可以看到新功能了:
src/features/home/home.html
<div id="home-header" class="jumbotron"> <h1>Hello, {{home.name}}</h1> </div> <button class="btn btn-primary" ng-click="home.changeName()">Change</button> <button class="btn btn-danger" ng-click="home.randomName()">Random</button>不幸的是direvices并不像services那样可以直接使用class来编写,所以我还是倾向于继续使用function:
src/directives/greeting.directive.js
import angular from 'angular'; function greeting() { return { restrict: 'E', scope: { name: '=' }, template: '<h1>Hello, {{name}}</div>' } } export default angular.module('directives.greeting', []) .directive('greeting', greeting) .name;不必再纠结是class还是function,先将它import进来试试吧:
src/features/home/index.js
// Rest of imports omitted import HomeController from './home.controller'; import randomNames from '../../services/randomNames.service'; import greeting from '../../directives/greeting.directive'; export default angular.module('app.home', [uirouter, randomNames, greeting]) .config(routing) .controller('HomeController', HomeController) .name;最后改下template:
src/features/home/home.html
<div id="home-header" class="jumbotron"> <greeting name="home.name"></greeting> </div> <button class="btn btn-primary" ng-click="home.changeName()">Change</button> <button class="btn btn-danger" ng-click="home.randomName()">Random</button>还是上面的代码,改造成ES6.
src/directives/greeting.directive.js
import angular from 'angular'; class greeting{ constructor(){ this.restrict: 'E', this.scope: { name: '=' }, template: '<h1>Hello, {{name}}</div>' } } export default angular.module('directives.greeting', []) .directive('greeting', () => new greeting()) .name;只要这样改造一下就可以用ES6完成directive的编写啦~
Webpack用于测试简直太棒了,但是杯具的是Angular在这方面支持不够给力。通常的情况下每个测试用例都有不同的入口,这可以让我们对每个模块独立测试而不用加载整个应用。但是。。。Angualr并没有这方面的考虑,我们还是只能用老的方式。
在项目里面有一个karma.conf.js文件,这是自动化测试工具karma,Webpack的插件是可以支持使用它的。另外我们再加载一个名为tests.webpack.js的文件,它包含了项目的所有测试用例。有点儿激进但是我们目前只能做这么多。
src/tests.webpack.js
import 'angular'; import 'angular-mocks/angular-mocks'; var testsContext = require.context(".", true, /.test$/); testsContext.keys().forEach(testsContext);便于测试我们还要加入angular和angular-mocks两个模块。
开启karma:
$ npm run test:live添加一个简单的测试用例:
src/features/home/home.controller.test.js
import home from './index'; describe('Controller: Home', function() { let $controller; beforeEach(angular.mock.module(home)); beforeEach(angular.mock.inject(function(_$controller_) { $controller = _$controller_; })); it('name is initialized to World', function() { let ctrl = $controller('HomeController'); expect(ctrl.name).toBe('World'); }); });将想要测试的模块import进来,加载测试用例我们就可以完成测试啦~
注意:在我的workflow中,测试文件必须添加.test前缀才可以。
在build文件夹中你可以看到免费附送的测试报告:)
打包发布难吗?当然不。
$ npm run build运行上面的命令就可以生成打包目录/dist,将server指向/dist/index.html就可以在浏览器中访问我们的项目啦,自带缓存清除功能,赞!
如果第三方库不支持Webpack怎么办??比如说没有export结果的库,我们可以这样载入它:
import 'thelibrary';这样打包后就会包含“thelibrary”这个库啦。
所以工具开发者们,为了你们的工具库能更好的支持Webpack,请在你们的代码中加那么一两行吧。
Webpack可以很好的支持ES6和Angular 1.x,它可以很方便配置然后帮我们做大多数的事情。
Webpack并不是唯一的选择,下次在给大家介绍JSPM。
非常感谢我的朋友Cesar Andreu帮我创建了原始workflow以及现在这个。
如果你想试一试这个例子,可以从这里下载。
原文地址:http://www.angular-tips.com/blog/2015/06/using-angular-1-dot-x-with-es6-and-webpack/
祁幽小贵 译
相关资源:es6 angular1.X webpack 实现按路由功能打包项目的示例
