《尝试通过AngularJS模块按需加载搭建大型应用(上)》 说到目前angular应用的通用构建方式,一种是全量预加载,将所有可能用到的模块在用户首次访问时加载;第二种是按需加载业务逻辑,根据路由加载对应的controller和view,两种处理方式互换优缺点。对比后得出最理想的方式应该是“模块按需加载”,即按需预加载业务功模块代码。本文具体聊聊如何实现。
上文说过了模块的加载方式,再来看看常见的模块划分依据,基本可以分为两类。
angular中有很多组件(service、controller、filter等等。angular组件可以理解是一段js代码,一段带有样式的html,或者两者兼而有之),其中一种划分就是把不同类别的组件划分到一个模块。
//主模块
angular.module('app', []);
//模块下所有的service
angular.module('app.service', []);
...
//模块下所有的controller
angular.module('app.controller', []);
当然也有更简单更粗暴的划分方式,因为controller包含了业务逻辑,基本无复用可能性,而其他angular组件基本上可以被复用,统统放到一个模块,即变成:
//主模块
angular.module('app', []);
//service、filter、directive等
angular.module('app.library', []);
//controller
angular.module('app.controller', []);
本质上都一样,都是按照组件划分。这种划分其实实际意义并不大,只是看起来显得解构略微清晰一点,可移植性略微提升了一丢丢,因为把包含业务逻辑的controller分离了出来。
这是比较推荐的一种划分方法。示例如下:
//主模块
angular.module('app', []);
//登陆注册
angular.module('login', []);
//首页
angular.module('home', []);
...
好处就是A模块的需要引用B模块组件的时候,只需要引入B模块即可,可移植性高。
同时从理论上来说各业务模块的耦合性可以大大降低。本文所提的就是这种划分模式。
那是不是我们把不同模块放到不同的js文件,然后按需引用就可以了呢?
是也不是,目标是要达到这个效果,但是有个棘手的问题——路由。
上文提到的两种方式都有一个前提,就是 加载其它模块之前路由已经配置完成 。
就以示例来说,如果我们在app模块配置login模块的路由,就会报错找不到controller。原因很简单,controller都在login模块下。那么是不是先把login模块引入就行了呢?恭喜你,进入了上文说的第一种全量预加载方式。
所以最佳的方式是 每个模块单独管理自己模块的路由,而不是由一个模块管理。
如果路由跟着业务模块进行配置,那么问题来了:
带着问题接着往下看~
先简单介绍一下懒加载文件的依赖模块。上文说过第三方插件和框架的兼容性,依赖加载还是考虑比较成熟的第三方angular模块 oc.lazyLoad 。主要作用就是按照模块名懒加载该模块依赖的文件,包括js和其它文件。
可以首先在主模块app中配置模块信息。
angular.module('app', ['oc.lazyLoad']).config(function('$ocLazyLoadProvider'){
//配置了两个模块
// ngAnimate是第三方模块,依赖angular-animate.min.js
// home是业务模块, 依赖home.js、home.css、home-html.js文件
$ocLazyLoadProvider.config({
modules: [{
name: 'ngAnimate',
files: ['lib/angular-animate/angular-animate.min.js']
},{
name: 'home',
files: ['home/script/home.js', 'home/style/home.css', 'home/view/home-html.js']
}]
});
});
当然这只是为了举例,真正写代码的时候不会这么写,配置数据肯定要分离出来,不要和业务逻辑混在一起。
要加载模块的时候也非常简单,利用 $ocLazyLoad
服务的 load
函数。
//返回一个promise对象
$ocLazyLoad.load(moduleName).then(function() {
//加载完成之后
});
因为我们将路由都封装在各个业务模块之中,但是页面首次加载的时候只有app模块,所以这里我们需要引用ui.router处理一下。
// 在$rootScope上监听'$locationChangeSuccess'事件
// 当用户在浏览器中输入URL地址时触发
$rootScope.$on('$locationChangeSuccess', function () {
if(!$location.path()) $location.path('/');
var mod = $location.path().split('/')[1]||'home';
// 路由路径按照 "模块/页面" 的方式配置,有两个好处:
// 1. 避免不同模块的路径冲突
// 2. 可以通过路径判断模块
$ocLazyLoad.load(mod).then(function () {
$urlRouter.sync();
});
});
$urlRouter.listen();
当然,直接这么写不会生效,我们需要阻塞ui.router默认的监听事件,在app模块的config中调用一个函数
$urlRouterProvider.deferIntercept();
这么一来当用户直接输入地址或者从收藏夹访问页面的时候可以先加载业务模块,然后由业务模块的路由来处理页面加载。这样第一个问题就解决了。还有内部跳转的情况。
通常ui.router用得最多的跳转方式有两种,一种是指令 ui-sref
,另一种是函数 $state.go
。虽然前者写在视图上后者写在controller中,但研究源码后发现都是调用了 $state.transitionTo
。
现在想要从home模块直接跳到login模块是会报错的,因为login模块没有加载找不到对应的路由。那么需要做的就是在路由跳转之前加载对应的模块,这里需要对 $state.transitionTo
稍微改造一下。
不得不用到angular中的黑科技 decorator
。 decorator
在常用开发中用得很少,它最大的作用就是修改第三方模块的服务,而且是在 不修改源码 的情况下。直接看代码和注释:
//处理模块间跳转
$provide.decorator('$state', function($delegate, $ocLazyLoad) {
var state = {};
// angular对象还是有些实用的方法的,深拷贝对象算是一个
// 这里做深拷贝不做浅拷贝是避免循环嵌套调用内存溢出
angular.copy($delegate, state);
$delegate.transitionTo = function (to) {
// 跳转的时候有两种情况,一种是传入self对象,另一种是直接把state的id传进来
if (to.self) {
// 当to为对象时,读取self.url属性获取路径,因为路径命名遵循"模块/页面"的方式,所以可以轻松判读取模块名
var mod = to.self.url.replace('main.', '').replace(///(.*)//.*/, '$1');
if (!mod || '/' === mod) {
mod = 'home';
}
//模块加载完成后再调用默认的路由跳转函数
$ocLazyLoad.load(mod).then(function (){
state.transitionTo.apply(null, arguments);
});
} else {
var id = to.replace('main.', '').replace(/(([a-z]*)[A-Z]{1})?.*/, '$2');
$ocLazyLoad.load(mnModule[id].name).then(function() {state.transitionTo.apply(null, arguments);});
}
}
return $delegate;
});
按照上述方式组织代码之后,业务模块一般会包含3个合并后的文件,一个js业务逻辑,一个css样式,一个模板文件。模块间相互引用,路由跳转传参,嵌套路由,多重路由等常用功能都可以正常使用。框架代码只是一部分,还需要构建工具配合~
源码目录结构大致如下:
编译后的代码:
博客: http://yalishizhude.github.io
作者:亚里士朱德