最近两年前端圈子犹如春秋战国:群雄并起,中原未定,就连各路大神也纷纷感叹最近两年技术选型难作。javascript
在模块化开发的问题上,一方面以AMD/CMD为表明的规范在过去几年间极大地提高了前端生产力。另外一方面,随着ES六、Web Components的临近,开发者们面临着承前启后的巨大挑战。css
对前端而言,模块化并不像后端语言那样简单,它涉及到不少工程问题与历史包袱,让人很难保持清晰的思路。在此记录下本身对于模块化的一些学习与认识,但愿能进一步理清思路。html
要谈模块化,首先要知道模块化的意义,开门见山地说:前端
不管任何语言任何项目,上述两个方面的意义均可以认为是模块化的核心价值。java
在前端项目中常用到的如jQuery、underscore.js等库,其实就能够看做是公共模块,他们对经常使用的、工具性的代码提供了抽象。node
我曾接触过一个老旧的项目,在N个js文件的顶部都写了一份对Function.prototype.bind的polyfill,看得出来出自不一样人之手:由于实现方式不同。毫无疑问,这样的代码彻底不具有可维护性,若是你不提个公共文件出来,就只能在用这几个js以前沐浴熏香,祈祷他们不要出bug。webpack
把公共代码封装成模块促进了代码的复用,但在不少时候,为了知足高内聚低耦合,咱们也须要将并不具有复用价值的代码抽离成相互独立的模块,以此来提高可维护性。有不少关于函数最好不要超过XX行,文件不要超过XXX行这样的“经验”,其实就是在督促人们多作有意义的代码拆分。git
总结一下:程序员
深刻到前端组件上,@民工精髓V 的 2015前端组件化框架之路 有很是精彩的讲解,这里就不赘述了。github
前端的模块化,因为如下几个方面的难点,始终和工程化的命题牢牢耦合:
下面一条条细说
与后端项目代码便是一切
不一样,前端因为面向展示层,在资源上呈现高度的复杂性,一个典型的前端组件可能包含html、css、js、img、swf 等资源中的一种或多种。最多见的状况就是js与css并存
这给资源的管理、聚合带来了极大的难度。
假设咱们有tooltip
组件,用伪代码描述理想的状况:
include tooltip
而事实上的状况大概是这个样子的
<link rel="stylesheet" href="./path/to/tooltip.css"/> <!-- 省略 --> <script type="text/javascript" src="./path/to/tooltip.js"></script>
简直像是洪荒时代的作法……很不幸的事实是,这在目前的前端业内其实很广泛。
固然也有难以忍受这种情况的前辈,作出了不少尝试和努力。最多见的思路是把资源依赖都交由js打理,由js做为模块的入口:
//login.js require("./login.css"); //usage require("./path/to/login.js");
至于怎么解析这种依赖并转化成浏览器能识别的输出,方法各类各样。但共同点不变:以js为入口,把资源的依赖关系管理并聚合起来。
其实这个办法不错,对资源的静态分析处理加上js在浏览器端的配合,理应能走出一条康庄大道来。
可是,咱们遇到了要说的第二个问题:
不管是W3C的标准,仍是事实上的标准,有标准总好过没标准。
前面说了,对资源作一遍预分析和处理,再辅以浏览器端js写的各类loader
,理应能很好的应对前端资源的复杂性。
但随之而来的大坑就是标准问题,前述的例子login是一个业务模块,业务模块有个好处,就是代码限定在本项目内,怎么写,用什么标准均可以掌控。但若是咱们有个公共模块tooltip,仍是按前述的方式引入:
//usage require("tooltip");
若是是nodejs,我天然知道应该去node_modules
下面找/tooltip/package.json
。
但在前端模块化上,连一个普遍承认的包管理器都没有,已知的有components规范、有spm的sea_modules、有bower的bower_components,还有不少跑在浏览器端的框架和库在脸上写着nodejs的npm上托管。
公共模块放哪儿?怎么放?你们各玩一套。
这里又引出了另外的问题:对资源作一遍预分析和处理
这在目前是属于工程化层面的问题,为了解析方便,现有的工程化方案每每只适配有限的包管理体系,例如基于F.I.S的Scrat以components做为组件规范。
spm则比较特别,它做为seajs的包管理体系,为本身量身定作了一套工程化的方案。
可是,不管是工程化适应包体系仍是包体系衍生工程化,相互之间这种不优雅而又不得已的强相关总归是提升了各自的使用门槛和将来风险。
js//tooltip.js //组件本身声明对css的依赖 require("../css/tooltip.css");
在这样背景下,上面这种更优雅的代码是不可能出如今公共模块中的,由于没有相关的规范,没法应对不一样工程化方案的不一样解读,好比有的可能须要下面这样写:
js//tooltip.js require("css!../css/tooltip.css");
对于公共模块来讲,为了保证通用性,最好的办法是:随便大家吧,老子不玩了。
恩,换个文明点的说法,就是回归原始状态,js归js,css归css,想引用?本身伸手来拿,因而在spm、webpack中,代码是这样的
//引入模块入口 require("tooltip") //引用模块下的其它资源 require("tooltip/css/tooltip.css")
这么作固然很差,依赖模块内部的路径明显不符合开闭原则
。若是哪天模块目录结构或者文件名变动,等待你的只能是报错。但这倒是目前为止不得已而为之的办法,别无其它选择。
为何模块的依赖不能在执行期解析,非要在工程化层面作?除了对多种资源的处理以外,浏览器环境的异步性也是很是重要的因素。
先简化问题:不考虑多种资源,仅考虑javascript。当我执行
//my_module.js define(function(require){ require("moduleA"); //do something });
时,若是是Node.js(固然Node.js不会有define这一段),只要去磁盘上读文件就行了,整个过程是同步的,可是在浏览器端,moduleA的代码可能还在服务器上,须要先去下载,再回来执行代码,整个过程变成了异步。
以require.js为例,看看发生了什么:
require(xxx)
,发现依赖项seajs大同小异,主要的差异在于执行依赖的时机。
上述过程,归纳就是:加载--分析--加载--分析--执行。在成功加载文件以前,它的依赖是没法肯定的。但这样不只用户会付出大量的等待时间,并且咱们发现,在运行时分析依赖是一种浪费,由于依赖关系在开发期就已经肯定了
。
生而与重复劳动为敌的程序员,是绝对不能容忍千千万万台用户电脑浪费资源干这种事的。
为了协调异步环境下模块开发与性能间的矛盾,咱们必须在工程阶段就具有依赖分析的能力,把具有依赖关系的资源进行打包。就算不打包,也但愿像 F.I.S 那样有个记录依赖关系的map.json,能够照单抓药,一次性地把须要的依赖项加载下来。
所以,在开发期解析模块依赖、合并代码、甚至重写资源路径都得在工程化的层面完成。工程化和模块化变成了容易耦合且不得不耦合的两个话题。
这也使得咱们在制定模块化或工程方案时,必须两头兼顾,全面考量。抛开模块谈工程、抛开工程谈模块,都是耍流氓。
to be continue……