原文:https://github.com/kuitos/kuitos.github.io/issues/31
全部文章:https://github.com/kuitos/kuitos.github.io/issues/
用过angular1.x(后面提到的angular均指代的angular1.x框架)的同学应该都知道,angular自身的模块系统是不具备按需加载的能力的,笔者也赞同angular的模块系统是真正称得上设计上的败笔的观点的。2015年被黑的最惨的前端主流框架莫过于angular了,但实际上angular真正设计上的硬伤只有两个:鸡肋的模块系统以及相比其他MVVM框架略显丑陋的脏值检测机制。关于其他各种所谓致命缺陷的立论其实都是站不住脚的,这些观点的提出我可以归结于使用者对angular的不熟悉,不服的同学欢迎来辩?
angular模块系统的问题
扯远了,说回正题。由于angular自身模块系统的限制,module不支持运行时添加依赖,也就是我们在定义入口模块时必须声明所有依赖项。当我们面临多项目整合的场景时(往往这类场景有按需加载的需求),这个就很恶心了,我们总不能在入口页写好所有可能会嵌入系统的项目的依赖项吧,而且要确保入口模块能找到所有依赖项对应的模块,相应的js还必须在入口处就加载好。。 更多关于angular模块化的问题,具体可以参见民工叔的这篇文章Angular的模块机制
市面上angular实现按需加载的通常方案
目前市面上流行的解决方案大概是这样的:基于requirejs等模块加载器,我们子模块的代码包裹在requirejs的模块定义语法下(define),然后在具体需要的时候在require回调里invoke我们子模块的controller或service等,可以参见这个seed项目angular-requirejs-seed
但是这种方式也有一些明显的问题:
requirejs配合angular实现的那一套按需加载的方案实在是太挫了,真的是有碍观瞻啊!?它是一套完全侵入式的方式,我个人是无法接受的。而且我认为在中小型规模的系统中,基于angular框架,我们自己需要写的代码量其实不会太大,即使在首页全部引入,在经过简单的合并压缩再配合gzip,文件体积完全在可控范围内,按需加载在这样的场景下价值有限。这也是我一直拒绝在angular体系中引入requirejs的原因。
如果我们采用angular的纯module的方式开发,那么我们自然会有包含各种controller、service、directive的不同模块,类似
angular.module('directives',[]).directive('grid',function(){})
的写法,而这些子模块必须在入口模块定义时声明其为依赖项,像这样angular.module('app',['directives'])
即便你采用requirejs做按需加载。我们不采用子单元纯module的方式开发,而是将所有的子单元都挂载在入口模块上,子模块写法类似
angular.module('app').directive('grid',function(){})
,这种做法副作用会相对少点,但是如果碰到多个项目在各个系统之间作嵌入时,很难做到不用修改代码即可完成嵌入,除非你能确保所有的系统入口模块命名一样。
基于ui-router的解决方案
刚好最近公司在做整个系统的去iframe化(没错之前各个产品嵌入主系统的做法是通过iframe。。不要笑!!?),因为各个产品之间的切换是通过tab完成的,tab的切换又是通过ui-router控制去定位到各个产品的入口html,所以基于ui-router,我的思路是这样的:
首先要理清ui-router的工作方式:tab切换时触发ui-router的路由,ui-router会通过配置好的路由规则找寻相应的模板配置(这里假设我们路由配置的都是templateUrl的方式),得到url后会去发起ajax请求拿模板,拿到模板再会填充到ui-view内容区,最后做compile、link处理(省去其他细节),这时候ui-view区域显示的就是编译好的模板内容了。
基于此,我们可以在模板做编译之前,分析并拿到模板中的script标签,然后通过简单的脚步加载器将模板中定义的js加载到浏览器内存里,在所有的js资源加载完毕之后再去调用编译流程,一切OK!这里要顺带解释一个事情,因为ui-router里采用element.html(tpl)的方式将模板填充到ui-view中的,所以模板中的script标签并不会被浏览器按正常方式解析,而link、style标签不会受到影响(出于安全考虑?具体原因没查到知道的同学请不吝指教)。
但是我们要做的当然不能是直接去找到ui-router这一块的代码然后修改源码,这种做法是有违开闭原则的也是我一直批判的方式,不到万不得已绝不要去修改第三方插件的源码!ui-router处理路由模板的主逻辑在uiView指令里,然后angular里面又提供了强大的decorator机制。开码!
angular
.module('ui.router.requirePolyfill', ['ng', 'ui.router', 'oc.lazyLoad'])
.decorator('uiViewDirective', DecoratorConstructor);
/**
* 装饰uiView指令,给其加入按需加载的能力
*/
DecoratorConstructor.$inject = ['$delegate', '$log', '$q', '$compile', '$controller', '$interpolate', '$state', '$ocLazyLoad'];
function DecoratorConstructor($delegate, $log, $q, $compile, $controller, $interpolate, $state, $ocLazyLoad) {
// 移除原始指令逻辑
$delegate.pop();
// 在原始ui-router的模版加载逻辑中加入脚本请求代码,实现按需加载需求
$delegate.push({
restrict: 'ECA',
priority: -400,
compile : function (tElement) {
var initial = tElement.html();
return function (scope, $element, attrs) {
var current = $state.$current,
name = getUiViewName(scope, attrs, $element, $interpolate),
locals = current && current.locals[name];
if (!locals) {
return;
}
$element.data('$uiView', {name: name, state: locals.$$state});
var template = locals.$template ? locals.$template : initial,
processResult = processTpl(template);
var compileTemplate = function () {
$element.html(processResult.tpl);
var link = $compile($element.contents());
if (locals.$$controller) {
locals.$scope = scope;
locals.$element = $element;
var controller = $controller(locals.$$controller, locals);
if (locals.$$controllerAs) {
scope[locals.$$controllerAs] = controller;
}
$element.data('$ngControllerController', controller);
$element.children().data('$ngControllerController', controller);
}
link(scope);
};
// 主要实现
// 模版中不含脚本则直接编译,否则在获取完脚本之后再做编译
if (processResult.scripts.length) {
loadScripts(processResult.scripts).then(compileTemplate);
} else {
compileTemplate();
}
};
}
});
return $delegate;
最早期我自己实现了一个简单的script-loader用来做基本的动态脚本加载,但是后来发现一个问题:angular框架下我们单单的只是加载脚本是没用的,我们必须把脚本定义的module注入到主app的module下才有意义。尽管在下仔细读过大部分angular的核心部件代码,但是动态注册模块这个事情难度还是很大的,改造工作一度停滞不前。。直到我发现了这个库ocLazyLoad,这之后事情就好办了。
附上完整的实现代码:ui-router-require-polyfill,文档。这里面为了解决脚本加载的时序问题,我在loadScript方法里加入了提取script seq属性的机制用于确定脚本顺序,同时为了解决gulp脚本合并时的问题,个人简单改造了下gulp-usemin插件,改造后的插件在这里,要做发布的脚本合并时请配合使用这个改造过的插件。
写在最后
这一套方案目前是我能想到的最接近完美的方案,最主要的是它是非侵入式而且基本不需要对原有angular体系下的代码做任何改造,即可实现按需加载&模块移植的需求的方式。如果有同学有改进建议或者更好的方案,欢迎一起探讨。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。