前端模块化开发的价值和规范
Posted 爱上小小苏
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端模块化开发的价值和规范相关的知识,希望对你有一定的参考价值。
随着互联网的飞速发展,前端开发越来越复杂。本文将从实际项目中遇到的问题出发,讲述模块化能解决哪些问题,以及实现规范。
恼人的命名冲突
我们从一个简单的习惯出发。我做项目时,常常会将一些通用的、底层的功能抽象出来,独立成一个个函数,比如
function each(arr) { // 实现代码}function log(str) { // 实现代码}
并像模像样地把这些函数统一放在 util.js 里。需要用到时,引入该文件就行。这一切工作得很好,同事也很感激我提供了这么便利的工具包。直到团队越来越大,开始有人抱怨。
小杨:我想定义一个 each 方法遍历对象,但页头的 util.js 里已经定义了一个,我的只能叫 eachObject 了,好无奈。
小高:我自定义了一个 log 方法,为什么小明写的代码就出问题了呢?谁来帮帮我。
抱怨越来越多。团队经过一番激烈的讨论,决定参照 Java 的方式,引入命名空间来解决。于是 util.js 里的代码变成了
var org = {};org.CoolSite = {};org.CoolSite.Utils = {};org.CoolSite.Utils.each = function (arr) { // 实现代码};org.CoolSite.Utils.log = function (str) { // 实现代码};
不要认为上面的代码是为了写这篇文章而故意捏造的。将命名空间的概念在前端中发扬光大,首推 Yahoo! 的 YUI2 项目。下面是一段真实代码,来自 Yahoo! 的一个开源项目。
if (org.cometd.Utils.isString(response)) { return org.cometd.JSON.fromJSON(response); }if (org.cometd.Utils.isArray(response)) { return response; }
通过命名空间,的确能极大缓解冲突。但每每看到上面的代码,都忍不住充满同情。为了调用一个简单的方法,需要记住如此长的命名空间,这增加了记忆负担,同时剥夺了不少编码的乐趣。作为前端业界的标杆,YUI 团队下定决心解决这一问题。在 YUI3 项目中,引入了一种新的命名空间机制。
YUI().use('node', function (Y) { // Node 模块已加载好 // 下面可以通过 Y 来调用 var foo = Y.one('#foo'); });
YUI3 通过沙箱机制,很好的解决了命名空间过长的问题。然而,也带来了新问题。
YUI().use('a', 'b', function (Y) { Y.foo(); // foo 方法究竟是模块 a 还是 b 提供的? // 如果模块 a 和 b 都提供 foo 方法,如何避免冲突?});
看似简单的命名冲突,实际解决起来并不简单。如何更优雅地解决?我们按下暂且不表,先来看另一个常见问题。
烦琐的文件依赖
继续上面的故事。基于 util.js,我开始开发 UI 层通用组件,这样项目组同事就不用重复造轮子了。其中有一个最被大家喜欢的组件是 dialog.js,使用方式很简单。
<script src="util.js"></script> <script src="dialog.js"></script> <script> org.CoolSite.Dialog.init({ /* 传入配置 */ });</script>
可是无论我怎么写文档,以及多么郑重地发邮件宣告,时不时总会有同事来询问为什么 dialog.js 有问题。通过一番排查,发现导致错误的原因经常是
<script src="dialog.js"></script> <script> org.CoolSite.Dialog.init({ /* 传入配置 */ });</script>
在 dialog.js 前没有引入 util.js,因此 dialog.js 无法正常工作。同样不要以为我上面的故事是虚构的,在我待过的公司里,至今依旧有类似的脚本报错,特别是在各种快速制作的营销页面中。上面的文件依赖还在可控范围内。当项目越来越复杂,众多文件之间的依赖经常会让人抓狂。下面这些问题,我相信每天都在真实地发生着。
通用组更新了前端基础类库,却很难推动全站升级。
业务组想用某个新的通用组件,但发现无法简单通过几行代码搞定。
一个老产品要上新功能,最后评估只能基于老的类库继续开发。
公司整合业务,某两个产品线要合并。结果发现前端代码冲突。
……
以上很多问题都是因为文件依赖没有很好的管理起来。在前端页面里,大部分脚本的依赖目前依旧是通过人肉的方式保证。当团队比较小时,这不会有什么问题。当团队越来越大,公司业务越来越复杂后,依赖问题如果不解决,就会成为大问题。文件的依赖,目前在绝大部分类库框架里,比如国外的 YUI3 框架、国内的 KISSY 等类库,目前是通过配置的方式来解决。
YUI.add('my-module', function (Y) { // ...}, '0.0.1', { requires: ['node', 'event'] });
上面的代码,通过 requires
等方式来指定当前模块的依赖。这很大程度上可以解决依赖问题,但不够优雅。当模块很多,依赖很复杂时,烦琐的配置会带来不少隐患。命名冲突和文件依赖,是前端开发过程中的两个经典问题。
下面介绍一下CommonJS/AMD/CMD/ES6规范
一、CommonJS
Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。
commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。
二、AMD和require.js
AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这里介绍用require.js实现AMD规范的模块化:用require.config()指定引用路径等,用define()定义模块,用require()加载模块。
首先我们需要引入require.js文件和一个入口文件main.js。main.js中配置require.config()并规定项目中用到的基础模块。引用模块的时候,我们将模块名放在[]中作为reqiure()的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]中作为define()的第一参数。
三、CMD和sea.js
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js(SeaJS的作者是前淘宝UED,现支付宝前端工程师玉伯)推广过程中产生的。
四、ES6 Module
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。
ES6的模块不是对象,import命令会被 javascript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。
以上是关于前端模块化开发的价值和规范的主要内容,如果未能解决你的问题,请参考以下文章