{"action":"created","issue":{"url":"https://api.github.com/repos/fouber/blog/issues/4","labels_url":"https://api.github.com/repos/fouber/blog/issues/4/labels{/name}","comments_url":"https://api.github.com/repos/fouber/blog/issues/4/comments","events_url":"https://api.github.com/repos/fouber/blog/issues/4/events","html_url":"https://github.com/fouber/blog/issues/4","id":35723976,"number":4,"title":"前端工程与模块化框架","user":{"login":"fouber","id":536297,"avatar_url":"https://avatars.githubusercontent.com/u/536297?v=3","gravatar_id":"","url":"https://api.github.com/users/fouber","html_url":"https://github.com/fouber","followers_url":"https://api.github.com/users/fouber/followers","following_url":"https://api.github.com/users/fouber/following{/other_user}","gists_url":"https://api.github.com/users/fouber/gists{/gist_id}","starred_url":"https://api.github.com/users/fouber/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fouber/subscriptions","organizations_url":"https://api.github.com/users/fouber/orgs","repos_url":"https://api.github.com/users/fouber/repos","events_url":"https://api.github.com/users/fouber/events{/privacy}","received_events_url":"https://api.github.com/users/fouber/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/fouber/blog/labels/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B","name":"前端工程","color":"eb6420"}],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":140,"created_at":"2014-06-14T08:39:52Z","updated_at":"2015-05-21T05:49:29Z","closed_at":null,"body":"本文最先发表在 [DIV.IO](http://div.io)   - 高质量前端社区，欢迎大家围观\r\n\r\n--------------------------\r\n\r\n一直酝酿着写一篇关于模块化框架的文章，因为模块化框架是前端工程中的 ``最为核心的部分`` 。本来又想长篇大论的写一篇完整且严肃的paper，但看了 [@糖饼](https://github.com/aui) 在 [DIV.IO](http://div.io/) 的一篇文章 《[再谈 SeaJS 与 RequireJS 的差异](http://div.io/topic/430)》觉得可以借着这篇继续谈一下，加上最近spm3发布，在seajs的官网上又引来了一场 [口水战](https://github.com/seajs/seajs/issues/454) ，我并不想参与到这场论战中，各有所爱的事情不好评论什么，但我想从工程的角度来阐述一下已知的模块化框架相关的问题，并给出一些新的思路，~~其实也不新啦，都实践了2多年了~~。\r\n\r\n> 前端模块化框架肩负着 ``模块管理``、``资源加载`` 两项重要的功能，这两项功能与工具、性能、业务、部署等工程环节都有着非常紧密的联系。因此，模块化框架的设计应该最高优先级考虑工程需要。\r\n\r\n基于 [@糖饼](https://github.com/aui) 的文章 《[再谈 SeaJS 与 RequireJS 的差异](http://div.io/topic/430)》，我这里还要补充一些模块化框架在工程方面的缺点：\r\n\r\n1. requirejs和seajs二者在加载上都有缺陷，就是模块的依赖要等到模块加载完成后，通过静态分析（seajs）或者deps参数（requirejs）来获取，这就为 ``合并请求`` 和 ``按需加载`` 带来了实现上的矛盾：\r\n    * 要么放弃按需加载，把所有js合成一个文件，从而满足请求合并（两个框架的官方demo都有这样的例子）；\r\n    * 要么放弃请求合并，请求独立的模块文件，从而满足按需加载。\r\n2. AMD规范在执行callback的时候，要初始化所有依赖的模块，而CMD只有执行到require的时候才初始化模块。所以用AMD实现某种if-else逻辑分支加载不同的模块的时候，就会比较麻烦了。考虑这种情况：\r\n\r\n    ```javascript\r\n    //AMD for SPA\r\n    require(['page/index', 'page/detail'], function(index, detail){\r\n        //在执行回调之前，index和detail模块的factory均执行过了\r\n        switch(location.hash){\r\n            case '#index':\r\n                index();\r\n            break;\r\n            case '#detail':\r\n                detail();\r\n            break;\r\n        }\r\n    });\r\n    ```\r\n    \r\n    在执行回调之前，已经同时执行了index和detail模块的factory，而CMD只有执行到require才会调用对应模块的factory。这种差别带来的不仅仅是性能上的差异，也可能为开发增加一点小麻烦，比如不方便实现换肤功能，factory注意不要直接操作dom等。当然，我们可以多层嵌套require来解决这个问题，但又会引起模块请求串行的问题。\r\n\r\n-------------------------\r\n> 结论：以纯前端方式实现模块化框架 **不能** 同时满足 ``按需加载``，``请求合并`` 和 ``依赖管理`` 三个需求。\r\n\r\n导致这个问题的根本原因是 ``纯前端方式只能在运行时分析依赖关系``。\r\n\r\n## 解决模块化管理的新思路\r\n\r\n由于根本问题出在 ``运行时分析依赖``，因此新思路的策略很简单：不在运行时分析依赖。这就要借助 ``构建工具`` 做线下分析了，其基本原理就是：\r\n\r\n> 利用构建工具在线下进行 ``模块依赖分析``，然后把依赖关系数据写入到构建结果中，并调用模块化框架的 ``依赖关系声明接口`` ，实现模块管理、请求合并以及按需加载等功能。\r\n\r\n举个例子，假设我们有一个这样的工程：\r\n\r\n```\r\nproject\r\n  ├ lib\r\n  │  └ xmd.js    #模块化框架\r\n  ├ mods         #模块目录\r\n  │  ├ a.js\r\n  │  ├ b.js\r\n  │  ├ c.js\r\n  │  ├ d.js\r\n  │  └ e.js\r\n  └ index.html   #入口页面\r\n```\r\n\r\n工程中，``index.html`` 的源码内容为：\r\n\r\n```html\r\n<!doctype html>\r\n...\r\n<script src=\"lib/xmd.js\"></script>   <!-- 模块化框架 -->\r\n<script>\r\n    //等待构建工具生成数据替换 `__FRAMEWORK_CONFIG__' 变量\r\n    require.config(__FRAMEWORK_CONFIG__);\r\n</script>\r\n<script>\r\n    //用户代码，异步加载模块\r\n    require.async(['a', 'e'], function(a, e){\r\n        //do something with a and e.\r\n    });\r\n</script>\r\n...\r\n```\r\n\r\n工程中，``mods/a.js`` 的源码内容为（采用类似CMD的书写规范）：\r\n\r\n```javascript\r\ndefine('a', function(require, exports, module){\r\n    console.log('a.init');\r\n    var b = require('b');\r\n    var c = require('c');\r\n    exports.run = function(){\r\n        //do something with b and c.\r\n        console.log('a.run');\r\n    };\r\n});\r\n```\r\n\r\n## 具体实现过程\r\n\r\n1. 用工具在下线对工程文件进行扫描，得到依赖关系表：\r\n\r\n    ```json\r\n    {\r\n        \"a\" : [ \"b\", \"c\" ],\r\n        \"b\" : [ \"d\" ]\r\n    }\r\n    ```\r\n\r\n2. 工具把依赖表构建到页面或者脚本中，并调用模块化框架的配置接口，``index.html``的构建结果为：\r\n    \r\n    ```html\r\n    <!doctype html>\r\n    ...\r\n    <script src=\"lib/xmd.js\"></script>   <!-- 模块化框架 -->\r\n    <script>\r\n        //构建工具生成的依赖数据\r\n        require.config({\r\n            \"deps\" : {\r\n                \"a\" : [ \"b\", \"c\" ],\r\n                \"b\" : [ \"d\" ]\r\n            }\r\n        });\r\n    </script>\r\n    <script>\r\n        //用户代码，异步加载模块\r\n        require.async(['a', 'e'], function(a, e){\r\n            //do something with a and e.\r\n        });\r\n    </script>\r\n    ```\r\n\r\n3. 模块化框架根据依赖表加载资源，比如上述例子，入口需要加载a、e两个模块，查表得知完整依赖关系，配合combo服务，可以发起一个合并后的请求：\r\n\r\n    http://www.example.com/??d.js,b.js,c.js,a.js,e.js\r\n\r\n## 先来看一下这种方案的优点\r\n\r\n1. 采用类似CMD的书写规范（同步require函数声明依赖），可以在执行到require语句的时候才调用模块的factory。\r\n1. 虽然采用CMD书写规范，但放弃了运行时分析依赖，改成工具输出依赖表，因此 ``依赖分析完成后可以压缩掉require关键字``\r\n1. 框架并没有严格依赖工具，它只是约定了一种数据结构。不使用工具，人工维护 ``require.config({...})`` 相关的数据也是可以的。对于小项目，文件全部合并的情况，更加不需要deps表了，只要在入口的require.async调用之前加载所有模块化的文件，依赖关系无需额外维护\r\n1. 构建工具设计非常简单，而且可靠。工作就是扫描模块文件目录，得到依赖表，JSON序列化之后插入到构建代码中\r\n1. 由于框架预先知道所有模块的依赖关系，因此可以借助combo服务实现``请求合并``，而不用等到一级模块加载完成才能知道后续的依赖关系。\r\n1. 如果构建工具可以自动包装define函数，那么整个系统开发起来会感觉跟nodejs非常接近，比较舒服。\r\n\r\n## 再来讨论一下这种方案的缺点：\r\n\r\n由于采用require函数作为依赖标记，因此如果需要变量方式require，需要额外声明，这个时候可以实现兼容AMD规范写法，比如\r\n\r\n```javascript\r\ndefine('a', ['b', 'c'], function(require, exports, module){\r\n    console.log('a.init');\r\n    var name = isIE ? 'b' : 'c';\r\n    var mod = require(name);\r\n    exports.run = function(){\r\n        //do something with mod.\r\n        console.log('a.run');\r\n    };\r\n})\r\n```\r\n\r\n只要工具把define函数中的 ``deps`` 参数，或者factory内的require都作为依赖声明标记来识别，这样工程性就比较完备了。\r\n    \r\n但不管怎样， ``线下分析始终依靠了字面量信息``，所以开发上可能会有一定的局限性，但总的来说瑕不掩瑜。\r\n\r\n> 希望本文能为前端模块化框架的作者带来一些新的思路。没有必要争论规范，工程问题才是最根本的问题。\r\n"},"comment":{"url":"https://api.github.com/repos/fouber/blog/issues/comments/104146733","html_url":"https://github.com/fouber/blog/issues/4#issuecomment-104146733","issue_url":"https://api.github.com/repos/fouber/blog/issues/4","id":104146733,"user":{"login":"wych1987","id":1901015,"avatar_url":"https://avatars.githubusercontent.com/u/1901015?v=3","gravatar_id":"","url":"https://api.github.com/users/wych1987","html_url":"https://github.com/wych1987","followers_url":"https://api.github.com/users/wych1987/followers","following_url":"https://api.github.com/users/wych1987/following{/other_user}","gists_url":"https://api.github.com/users/wych1987/gists{/gist_id}","starred_url":"https://api.github.com/users/wych1987/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wych1987/subscriptions","organizations_url":"https://api.github.com/users/wych1987/orgs","repos_url":"https://api.github.com/users/wych1987/repos","events_url":"https://api.github.com/users/wych1987/events{/privacy}","received_events_url":"https://api.github.com/users/wych1987/received_events","type":"User","site_admin":false},"created_at":"2015-05-21T05:49:28Z","updated_at":"2015-05-21T05:49:28Z","body":"他就是这样让我爱上他的。。。楼上你要小心了。。。"}}