{"action":"created","issue":{"url":"https://api.github.com/repos/kuitos/kuitos.github.io/issues/31","labels_url":"https://api.github.com/repos/kuitos/kuitos.github.io/issues/31/labels{/name}","comments_url":"https://api.github.com/repos/kuitos/kuitos.github.io/issues/31/comments","events_url":"https://api.github.com/repos/kuitos/kuitos.github.io/issues/31/events","html_url":"https://github.com/kuitos/kuitos.github.io/issues/31","id":122973796,"number":31,"title":"基于ui-router的非侵入式angular按需加载方案","user":{"login":"kuitos","id":5206843,"avatar_url":"https://avatars.githubusercontent.com/u/5206843?v=3","gravatar_id":"","url":"https://api.github.com/users/kuitos","html_url":"https://github.com/kuitos","followers_url":"https://api.github.com/users/kuitos/followers","following_url":"https://api.github.com/users/kuitos/following{/other_user}","gists_url":"https://api.github.com/users/kuitos/gists{/gist_id}","starred_url":"https://api.github.com/users/kuitos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kuitos/subscriptions","organizations_url":"https://api.github.com/users/kuitos/orgs","repos_url":"https://api.github.com/users/kuitos/repos","events_url":"https://api.github.com/users/kuitos/events{/privacy}","received_events_url":"https://api.github.com/users/kuitos/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/kuitos/kuitos.github.io/labels/Angular","name":"Angular","color":"207de5"},{"url":"https://api.github.com/repos/kuitos/kuitos.github.io/labels/javascript","name":"javascript","color":"fbca04"},{"url":"https://api.github.com/repos/kuitos/kuitos.github.io/labels/%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84","name":"前端架构","color":"fad8c7"}],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":13,"created_at":"2015-12-18T15:58:35Z","updated_at":"2015-12-24T06:05:22Z","closed_at":null,"body":"## 基于ui-router的非侵入式angular按需加载方案\r\n\r\n用过angular1.x(后面提到的angular均指代的angular1.x框架)的同学应该都知道，angular自身的模块系统是不具备按需加载的能力的，笔者也赞同angular的模块系统是真正称得上设计上的败笔的观点的。2015年被黑的最惨的前端主流框架莫过于angular了，但实际上angular真正设计上的硬伤只有两个：鸡肋的模块系统以及相比其他MVVM框架略显丑陋的脏值检测机制。关于其他各种所谓致命缺陷的立论其实都是站不住脚的，这些观点的提出我可以归结于使用者对angular的不熟悉，不服的同学欢迎来辩😂  \r\n\r\n#### angular模块系统的问题\r\n扯远了，说回正题。由于angular自身模块系统的限制，module不支持运行时添加依赖，也就是我们在定义入口模块时必须声明所有依赖项。当我们面临多项目整合的场景时(往往这类场景有按需加载的需求)，这个就很恶心了，我们总不能在入口页写好所有可能会嵌入系统的项目的依赖项吧，而且要确保入口模块能找到所有依赖项对应的模块，相应的js还必须在入口处就加载好。。  更多关于angular模块化的问题，具体可以参见民工叔的这篇文章[Angular的模块机制](https://github.com/xufei/blog/issues/17)\r\n\r\n#### 市面上angular实现按需加载的通常方案\r\n目前市面上流行的解决方案大概是这样的：基于requirejs等模块加载器，我们子模块的代码包裹在requirejs的模块定义语法下(define)，然后在具体需要的时候在require回调里invoke我们子模块的controller或service等，可以参见这个seed项目[angular-requirejs-seed](https://github.com/tnajdek/angular-requirejs-seed)\r\n\r\n但是这种方式也有一些明显的问题：  \r\n\r\n1. requirejs配合angular实现的那一套按需加载的方案实在是太挫了，真的是有碍观瞻啊！😂它是一套完全侵入式的方式，我个人是无法接受的。而且我认为在中小型规模的系统中，基于angular框架，我们自己需要写的代码量其实不会太大，即使在首页全部引入，在经过简单的合并压缩再配合gzip，文件体积完全在可控范围内，按需加载在这样的场景下价值有限。这也是我一直拒绝在angular体系中引入requirejs的原因。\r\n\r\n2. 如果我们采用angular的纯module的方式开发，那么我们自然会有包含各种controller、service、directive的不同模块，类似`angular.module('directives',[]).directive('grid',function(){})`\r\n的写法，而这些子模块必须在入口模块定义时声明其为依赖项，像这样`angular.module('app',['directives'])`即便你采用requirejs做按需加载。  \r\n\r\n3. 我们不采用子单元纯module的方式开发，而是将所有的子单元都挂载在入口模块上，子模块写法类似`angular.module('app').directive('grid',function(){})`，这种做法副作用会相对少点，但是如果碰到多个项目在各个系统之间作嵌入时，很难做到不用修改代码即可完成嵌入，除非你能确保所有的系统入口模块命名一样。\r\n\r\n\r\n#### 基于ui-router的解决方案\r\n刚好最近公司在做整个系统的去iframe化(没错之前各个产品嵌入主系统的做法是通过iframe。。不要笑！！😂)，因为各个产品之间的切换是通过tab完成的，tab的切换又是通过ui-router控制去定位到各个产品的入口html，所以基于ui-router，我的思路是这样的：\r\n\r\n1. 首先要理清ui-router的工作方式：tab切换时触发ui-router的路由，ui-router会通过配置好的路由规则找寻相应的模板配置(这里假设我们路由配置的都是templateUrl的方式)，得到url后会去发起ajax请求拿模板，拿到模板再会填充到ui-view内容区，最后做compile、link处理(省去其他细节)，这时候ui-view区域显示的就是编译好的模板内容了。\r\n2. 基于此，我们可以在模板做编译之前，分析并拿到模板中的script标签，然后通过简单的脚步加载器将模板中定义的js加载到浏览器内存里，在所有的js资源加载完毕之后再去调用编译流程，一切OK！这里要顺带解释一个事情，因为ui-router里采用element.html(tpl)的方式将模板填充到ui-view中的，所以模板中的script标签并不会被浏览器按正常方式解析，而link、style标签不会受到影响(出于安全考虑？具体原因没查到知道的同学请不吝指教)。\r\n\r\n但是我们要做的当然不能是直接去找到ui-router这一块的代码然后修改源码，这种做法是有违开闭原则的也是我一直批判的方式，不到万不得已绝不要去修改第三方插件的源码！ui-router处理路由模板的主逻辑在uiView指令里，然后angular里面又提供了强大的decorator机制。开码！\r\n\r\n```js\r\nangular\r\n    .module('ui.router.requirePolyfill', ['ng', 'ui.router', 'oc.lazyLoad'])\r\n    .decorator('uiViewDirective', DecoratorConstructor);\r\n\r\n  /**\r\n   * 装饰uiView指令,给其加入按需加载的能力\r\n   */\r\n  DecoratorConstructor.$inject = ['$delegate', '$log', '$q', '$compile', '$controller', '$interpolate', '$state', '$ocLazyLoad'];\r\n  function DecoratorConstructor($delegate, $log, $q, $compile, $controller, $interpolate, $state, $ocLazyLoad) {\r\n\r\n    // 移除原始指令逻辑\r\n    $delegate.pop();\r\n    // 在原始ui-router的模版加载逻辑中加入脚本请求代码,实现按需加载需求\r\n    $delegate.push({\r\n\r\n      restrict: 'ECA',\r\n      priority: -400,\r\n      compile : function (tElement) {\r\n        var initial = tElement.html();\r\n        return function (scope, $element, attrs) {\r\n\r\n          var current = $state.$current,\r\n            name = getUiViewName(scope, attrs, $element, $interpolate),\r\n            locals = current && current.locals[name];\r\n\r\n          if (!locals) {\r\n            return;\r\n          }\r\n\r\n          $element.data('$uiView', {name: name, state: locals.$$state});\r\n\r\n          var template = locals.$template ? locals.$template : initial,\r\n            processResult = processTpl(template);\r\n\r\n          var compileTemplate = function () {\r\n            $element.html(processResult.tpl);\r\n\r\n            var link = $compile($element.contents());\r\n\r\n            if (locals.$$controller) {\r\n              locals.$scope = scope;\r\n              locals.$element = $element;\r\n              var controller = $controller(locals.$$controller, locals);\r\n              if (locals.$$controllerAs) {\r\n                scope[locals.$$controllerAs] = controller;\r\n              }\r\n              $element.data('$ngControllerController', controller);\r\n              $element.children().data('$ngControllerController', controller);\r\n            }\r\n\r\n            link(scope);\r\n          };\r\n\r\n\t\t  // 主要实现\r\n          // 模版中不含脚本则直接编译,否则在获取完脚本之后再做编译\r\n          if (processResult.scripts.length) {\r\n            loadScripts(processResult.scripts).then(compileTemplate);\r\n          } else {\r\n            compileTemplate();\r\n          }\r\n\r\n        };\r\n      }\r\n\r\n    });\r\n\r\n    return $delegate;\r\n```\r\n\r\n最早期我自己实现了一个简单的[script-loader](https://github.com/kuitos/script-loader)用来做基本的动态脚本加载，但是后来发现一个问题：angular框架下我们单单的只是加载脚本是没用的，我们必须把脚本定义的module注入到主app的module下才有意义。尽管在下仔细读过大部分angular的核心部件代码，但是动态注册模块这个事情难度还是很大的，改造工作一度停滞不前。。直到我发现了这个库[ocLazyLoad](https://github.com/ocombe/ocLazyLoad)，这之后事情就好办了。  \r\n附上完整的实现代码：[ui-router-require-polyfill](https://github.com/kuitos/angular-utils/blob/master/polyfills%2Fui-router-require-polyfill.js)，[文档](https://github.com/kuitos/angular-utils/tree/master/polyfills)。这里面为了解决脚本加载的时序问题，我在loadScript方法里加入了提取script seq属性的机制用于确定脚本顺序，同时为了解决gulp脚本合并时的问题，个人简单改造了下gulp-usemin插件，改造后的插件在[这里](https://github.com/kuitos/gulp-usemin)，要做发布的脚本合并时请配合使用这个改造过的插件。\r\n\r\n#### 写在最后\r\n这一套方案目前是我能想到的最接近完美的方案，最主要的是它是**非侵入式**而且基本不需要对原有angular体系下的代码做任何改造，即可实现**按需加载&模块移植**的需求的方式。如果有同学有改进建议或者更好的方案，欢迎一起探讨。"},"comment":{"url":"https://api.github.com/repos/kuitos/kuitos.github.io/issues/comments/167047622","html_url":"https://github.com/kuitos/kuitos.github.io/issues/31#issuecomment-167047622","issue_url":"https://api.github.com/repos/kuitos/kuitos.github.io/issues/31","id":167047622,"user":{"login":"xufei","id":2725159,"avatar_url":"https://avatars.githubusercontent.com/u/2725159?v=3","gravatar_id":"","url":"https://api.github.com/users/xufei","html_url":"https://github.com/xufei","followers_url":"https://api.github.com/users/xufei/followers","following_url":"https://api.github.com/users/xufei/following{/other_user}","gists_url":"https://api.github.com/users/xufei/gists{/gist_id}","starred_url":"https://api.github.com/users/xufei/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/xufei/subscriptions","organizations_url":"https://api.github.com/users/xufei/orgs","repos_url":"https://api.github.com/users/xufei/repos","events_url":"https://api.github.com/users/xufei/events{/privacy}","received_events_url":"https://api.github.com/users/xufei/received_events","type":"User","site_admin":false},"created_at":"2015-12-24T06:05:22Z","updated_at":"2015-12-24T06:05:22Z","body":"@kuitos 嘿嘿，构建的时候放进去，我就是这么想的，不过动静也挺大的。\r\n\r\n2年前我还在之前公司，当时规划了一个平台，js模块的部分类似npm，每个业务模块只放实现，也就是controller，service去掉外壳的部分，然后在这个平台上集中存放依赖关系，构建的时候生成外壳，就是这个module配置之类的，不过也好繁琐。"}}