最近公司用webpack和angular2来开发手机app页面,发现ES6的写法非常方便和清晰,于是我想到在angular1中也可以用ES6语法写,webpack来进行打包编辑。于是我查阅了相关资料之后终于实现了ES6语法来写angular1。
Webpack
关于webpack的配置,网上有许多的教程,在github上也有相当多的项目例子可以借鉴,我这就不详细说webpack的配置了。如果你是新手,我推荐webpack的官网,如果你对英文不感冒你可以查看webpack的中文文档学习相关知识。当然如果你实在对webpack的配置烦恼的话,我推荐GitHub上AngularClass组织的angular-starter项目,这是webpack配置angualr2的例子,它里面的配置非常详细,对每个插件的使用有相关的说明,对学习webpack的配置很有帮助,本项目也是参照该例子,主要的目录结构如下。
angular-webpack/
├──config/ * 配置文件目录
│ ├──helpers.js * helper 方法用来获取目录以及一些相关配置参数
│ ├──webpack.common.js * webpack 通用的配置信息
│ ├──webpack.dev.js * webpack 配置开发环境文件
│ ├──webpack.prod.js * webpack 配置生产环境文件
│ └──webpack.test.js * webpack 配置测试环境文件
├──src/ * web文件的主目录
│ ├──app.module.js * angularjs 主module
│ │
│ ├──index.html * 项目的主入口
│ │
│ ├──app/ * 文件主目录
│ │ ├──config/ * angularjs的config文件
│ │ │
│ │ ├──router/ * angularjs的路由配置
│ │ │
│ │ └──views/ * angularjs的页面
│ │
│ └──style/ * 项目的样式文件
│
├──package.json * npm 配置文件
└──webpack.config.js * webpack 主配置文件
angularjs目录结构
对一个项目来说,目录组织是非常重要的,它能让你一目了然的了解项目信息。在angularjs的项目中,有不同的目录搭建,对于有些人喜欢把module、controller、directive、service等文件分别放在module、controller、directive、service目录下统一管理。html页面统一放在HTML目录下它的目录结构看起来是这样的:
app/
├──controller/
│ ├──*.controller.js
│ ├──*.controller.js
│ ├──*.controller.js
│ │....
├──directive/
│ ├──*.directive.js
│ ├──*.directive.js
│ │....
├──html/
│ ├──*.html
│ ├──*.html
│ │....
├──module/
│ ├──*.module.js
│ ├──*.module.js
│ │....
│
├──service/
│ ├──*.service.js
│ ├──*.service.js
│ │....
这一种目录结构相对来说不是很好,尤其是对模块化打包来说结构很不清楚,不利于模块化,页面一多很难管理,另一种目录结构是把每一个相关的页面controller、service、module、html放到对应的页面目录下,它的结构是这样的:
app/
├──login/
│ ├──login.module.js
│ ├──login.controller.js
│ ├──login.directive.js
│ ├──login.router.js
│ ├──login.html
│ ├──login.css
│
├──home/
│ ├──home.module.js
│ ├──home.controller.js
│ ├──home.directive.js
│ ├──home.router.js
│ ├──home.html
│ ├──home.css
├──other/
│ ├──other.module.js
│ ├──other.controller.js
│ ├──other.directive.js
│ ├──other.router.js
│ ├──other.html
│ ├──other.css
这样的目录结构有利于模块的移植。
angualrjs 文件构建
在ES6中有了新方法:Class来声明类,class写法看起来更简洁,方便。在angular1中把function方法定义的controller,service等修改为Class声明,具体各修改方式如下:
module
ES6关于angualr的module写法没有大的变化,注意ES6中需要对angular模块导入之后才能使用angular,其他模块的注入也是一样的。
//--------ES6之前
angular.module('webapp',['oc.lazyLoad',]);
//--------ES6
import angular from 'angular'; //---- 或者 var angular = require('angular');
require('oclazyload');
angular.module('webapp',['oc.lazyLoad',]);
controller
<div ng-controller='loginController as vm'>
<input type="tel" placeholder="手机号" ng-model="vm.username">
<input type="password" placeholder="密码" ng-model="vm.userPassword">
<input type="button" value="登录" ng-click="vm.login()">
</div>
login.controller.js
//ES5 controller的一般写法
LoginController.$inject = ['$state', 'loginService'];
ngModule.controller('loginController', LoginController);
function LoginController($state, loginService) {
var vm = this;
vm.login = login;
function login() {
if (!vm.username) {
alert('请输入手机号');
return;
}
if (!vm.userPassword) {
alert('请输入密码');
return;
}
loginService.login({
username: vm.username,
userPassword: vm.userPassword
}).then(function(res){
$state.go('page');
}, function(res){
alert('登录失败');
})
}
}
//------------------ES6-----------------------
class LoginController {
constructor($state, loginService) {
this.loginService = loginService; //把依赖注入的方法放到this中,变为this的属性,使用this."property"来调用该变量
//与之前比最大的不同是依赖注入不在是该函数内的“全局变量”
this.$state = $state;
}
login() {
if (!this.username) {
alert('请输入手机号');
return;
}
if (!this.userPassword) {
alert('请输入密码');
return;
}
this.loginService.login({
username: this.username,
userPassword: this.userPassword
}).then((res) => {
this.$state.go('page');
}, (res) => {
alert('登录失败');
})
}
}
LoginController.$inject = ['$state', 'loginService'];
ngModule.controller('loginController', LoginController);
注意在function声明中LoginController.$inject = [‘$state’, ‘loginService’]不管是放在function前面还是后面都不会有影响,但是在class声明中LoginController.$inject = [‘$state’, ‘loginService’]只能放在class后面定义,主要原因是function会在作用域中进行提升而class不会。
service
angular中service的写法与controller相似,可以说没有大区别
login.service.js
//ES5 service写法
LoginService.$inject = ['$http'];
function LoginService($http){
var self=this;
self.login=login;
function login(params) {
/*return $http({
url: '****!/!***!/login',
method: 'GET',
params: params
})*/
/*用ES6 Promise方法模仿$http,也可用angular中的$q实现*/
return new Promise(function (resolve, reject) {
if (params) {
resolve('success');
} else {
reject('fail');
}
});
}
}
//------------------ES6-----------------------
class LoginService {
constructor($http) {
this.$http = $http;
}
login(params) {
/*return this.$http({
url: '****!/!***!/login',
method: 'GET',
params: params
})*/
/*用ES6 Promise方法模仿$http,也可用angular中的$q实现*/
return new Promise((resolve, reject) => {
if (params) {
resolve('success');
} else {
reject('fail');
}
});
}
}
LoginService.$inject = ['$http'];
ngModule.service('loginService', LoginService);
provider
//------------------ES6-----------------------
class LoginProvider {
constructor($http) {
this.$http = $http;
}
$get(){
let that=this;
login(params) {
return that.$http({ //注意this的指向问题
url: '****!/!***!/login',
method: 'GET',
params: params
})
}
return {
login:login;
}
}
}
LoginProvider.$inject = ['$http']
ngModule.provider('LoginProvider', LoginProvider);
注意,在provider中需要定义$get函数
factory
关于factory的写法稍微与前面的不同,很多人想当然的会这和service一样写factory
class LoginFactory {
constructor($http) {
this.$http = $http;
}
login(params) {
...
...
...
}
}
LoginFactory.$inject = ['$http'];
ngModule.factory('LoginFactory', LoginFactory); //TypeError: Cannot call a class as a function
class的定义是相似的,但是在module引入factory时如果按上面的写法写就会报TypeError: Cannot call a class as a function错误
可能有人会很奇怪,为什么controller,service,provider可以,但是到了factory就会错误。这个和factory的机制有关。编译器转ES6语法到ES5后class会变成这样:
var Loginfactory= function () {
function Loginfactory($http) {
_classCallCheck(this, Loginfactory);
this.$http= $http;
}
_createClass(Loginfactory, [{
key: 'login',
value: function login() {
}
}]);
return Loginfactory;
}();
关键是_classCallCheck方法,用来判断this是否为Loginfactory的实例,但是在factory中,angular内部更改了factory的返回对象,使this不在是factory的实例,所以会报错。我们需要手动new factory()方法,应该改成这样:
//LoginFactory.$inject = ['$http'];不需要了
ngModule.factory('LoginFactory', ['$http',($http)=>new LoginFactory($http)]);
注:controller,service,provider也可以按factory的注入方法写
可是这么写如果有好多依赖要注入,会使ngModule.factory(‘LoginFactory’, [‘$http’….,($http,…)=>new LoginFactory($http,…)])看起来非常长而且不易阅读,怎样才能像controller一样以LoginFactory.$inject = [‘$http’]注入,可以先获取Class上的$inject的注入数组,在由该数组和Class的实例组成[‘a’,’b’,function(a,b){}]样式,具体实现如下:
function constructorFn(configFn) {
let args = configFn.$inject || [];
let factoryFunction = (...args) => new configFn(...args);
/**
* 主要检测controller、directive的注入
* service、factory、provider、config在程序运行时只运行一次
* controller、directive每次页面载入时会重新注入,需要把上一个注入的function从数组中移除
* */
if(typeof args[args.length-1]==='function'){
args.splice(-1,1);
}
/**
* args.push(factoryFunction)类似于['a',function(a){}]
* */
return args.push(factoryFunction) && args; //return args;
}
LoginFactory.$inject = ['$http'];
ngModule.factory('LoginFactory', constructorFn(LoginFactory));
在写factory时我还发现一个小问题,在写angular的Http拦截器时候会出现意料之外的情况
export class HttpInterceptor {
constructor($q, $location) {
this.$q = $q;
this.$location = $location;
}
request(config){
console.log(this.$q); //error
//this对象不是HttpInterceptor的实例而是undefined
return config;
}
}
HttpInterceptor.$inject = ['$q', '$location'];
在调用request、requestError等方法时,this指向丢失了,应该是angular在调用时,this对象被修改了。如果你在request、requestError等方法中没有用到this对象,可以这么写。如果用到this对象,则需要变换一下:
export class HttpInterceptor {
constructor($q, $location) {
this.$q = $q;
this.$location = $location;
this.request=(config)=>{
console.log(this.$q); //success
return config;
};
}
}
HttpInterceptor.$inject = ['$q', '$location'];
注意,this指向更改可能出现的情况不止在这里,需要小心
directive
directive写法稍有不同,由于directive需要返回一个包含restrict、template 、scope 等属性的对象,所以需要在constructor中定义这些熟悉,还有directive会有link,compile等函数方法
//vm.title
<my-page page-title='vm.title'></my-page>
//------------------ES6-----------------------
class PageDirective {
constructor($timeout) {
this.restrict = "EA";
this.template = `{{vm.name}} Hello {{vm.pageTitle}}`;
this.controller = PageDController;
this.controllerAs = "vm";
this.bindToController = true;
this.scope = {
pageTitle:'='
};
this.$timeout=$timeout;
}
link(scope,element,attr){
this.$timeout(()=>scope.vm.name='',1000);
}
}
PageDirective.$inject = ['$timeout'];
class PageDController{
constructor(){
this.name='pageDirective'
}
}
ngModule.directive('myPage', constructorFn(PageDirective));
oclazyload 按需加载路由
webpack打包时会把HTML和js代码都放到一个js文件中,使这个文件非常大,导致打开页面加载会很慢,影响用户体验。所以我们需要把HTML以及部分js代码抽出放到块文件中。幸运的是我们可以用oclazyload配合webpack进行anglar路由的按需加载,oclazyload的官方文档有详细的说明和例子,我这边列了一个简单的例子:
路由配置
login.router.js
import angular from 'angular'
//import LoginModule from './login.module'
{
state: 'login',
url: '/login',
controller: 'loginController',
controllerAs: 'vm',
/*template: require('./login.html'),*/
templateProvider: ['$q', ($q)=> { //templateProvider来动态的加载template
let defer = $q.defer(); //require.ensure:webpack的方法在需要的时候才下载依赖的模块,
require.ensure([''], () => { //只有require函数调用后再执行下载的模块
let template = require('./login.html'); //require调用后加载login页面
defer.resolve(template);
});
return defer.promise;
}],
title: '登录',
resolve: {
deps: ['$q', '$ocLazyLoad', ($q, $ocLazyLoad)=> {
let defer = $q.defer();
require.ensure([], ()=> {
/**
*let module = LoginModule(angular);
*注意import导入LoginModule方法与require('./login.module')直接引用LoginModule方法是有区别的,
*import导入LoginModule方法不能分离js
*/
let module = require('./login.module').LoginModule(angular); //动态加载Module
$ocLazyLoad.load({
name: 'login' //name就是你module的名称
});
defer.resolve(module);
});
return defer.promise;
}],
}
login.module.js
import {loginControllerFunc} from "./login.controller";
import {loginServiceFunc} from "./login.service";
export function LoginModule(Angular) {
const loginModule = Angular.module('login', []); //login名称就是$ocLazyLoad.load中的name;
loginControllerFunc(loginModule); //注入模块controller
loginServiceFunc(loginModule); //注入模块service
}
注:$ocLazyLoad动态加载的module不需要在其他模块中引入,如angular.module(‘webapp’,[‘login’]),这是错误的
到这里用ES6构建anguar1项目就基本告一段落了。如果有更好的ES6写法,欢迎各位评说。
文章有不对的地方,望大神们指出。
项目地址请戳这里