谈一下前端模块化

@kvkens 2018-12-12 03:33:07发表于 iuap-design/blog

谈一下前端模块化

1. 感受

目前的项目开发都是npm作为前端包管理,使用的也是ES6的语法去开发,然后使用webpack去执行构建,这样的机制会让前端更佳正规,可管理型。

在我的前端学习生涯中,看过不少书,其中比较印象深刻的是“犀牛书”

现在回过头来想想,也许选择以《JavaScript权威指南》一书来作为入门有些不好,因为这本书毕竟是很早之前的,书中所讲的思想、标准也基本都只是 ES5 及那时代的相关技术。

这也就导致了,在书中看到的很多例子,虽然觉得所用到的思想很奇妙,比如临时命名空间之类的处理,但其实,有些技术到现在已经有了更为强大的技术替代了。

就像这篇要讲的模块化,目前,以我看到的各资料后,所收获的知识是,大概有四种较为常用且热门的模块化技术,也许还有更新的技术,也许还有我不知道的技术,无所谓,慢慢来,这篇的内容已经够我消化了。

目前四种模块化技术:
  1. CommonJS规范&node.js
  2. AMD规范&Require.js
  3. CMD规范&Sea.js
  4. ES6标准(常用)

2. 模块化历程

1. 全局变量、全局函数(1999年)

这时候的多个 js 脚本文件之间,直接通过全局变量或全局函数的方式进行通信,这种方式叫做:直接定义依赖。

虽然做的好一些的会对这些 js 文件做一些目录规划,将资源归类整理,但仍无法解决全局命名空间被大量污染,极其容易导致变量冲突问题。

2. 对象作为命名空间(2002年)

为了解决遍地的全局变量问题,这时候提出一种命名空间模式的思路,即将本要定义成全局变量、全局函数的这些全都作为一个对象的属性存在,这个对象的角色充当命名空间,不同模块的 JS 文件中通过访问这个对象的属性来进行通信。

3. 立即执行的函数作为临时命名空间 + 闭包(2003年)

虽然提出利用一个对象来作为命名空间的思路,一定程度解决了大量的全局变量的问题,但仍旧存在很多局限,比如没有模块的隐藏性,所以针对这些问题,这时候又新提出一种思路:利用立即执行的函数来作为临时命名空间,这样就可以避免污染全局命名空间,同时,结合闭包特性,来实现隐藏细节,只对外暴露指定接口。

虽然这种思路,解决了很多问题,但仍旧有一些局限,比如,缺乏管理者,什么意思,就是说,在前端里,开发人员得手动将不同 JS 脚本文件按照它们之间的依赖关系,以被依赖在前面的顺序来手动书写 <script> 加载这些文件。

也就是不同 <script> 的前后顺序实际上表示着它们之间的依赖关系,一旦文件过多,将会很难维护,这是上述方案都存在的问题。

4. 动态创建 <script> 工具(2009年)

针对上述问题,也就衍生出了一些加载 js 文件的工具,先看个例子:

$LAB.script("greeting.js").wait()
    .script("x.js")
    .script("y.js").wait()
    .script("run.js");

LAB.js 这类加载工具的原理实际上是动态的创建 <script>,达到作为不同 JS 脚本文件的管理者作用,来决定 JS 文件的加载和执行顺序。

虽然我没用过这种工具,但我觉得它的局限还是有很多,其实就是将开发人员本来需要手动书写在 HTML 文档里的 <script> 代码换成写在 JS 文件中,不同 JS 文件之间的依赖关系仍旧需要按照前后顺序手动维护。

5. CommonJS规范&node.js(2009年)

中间跳过了一些过程,比如 YUI 的沙箱模式等,因为不熟,想了解更详细的可以去自行搜索。

当 CommonJS 规范出来时,模块化算是进入了真正的革命,因为在此之前的探索,都是基于语言层面的优化,也就是利用函数特性、对象特性等来在运行期间模拟出模块的作用,而从这个时候起,模块化的探索就大量的使用了预编译。

CommonJS 模块化规范有如下特点:

  1. 所有代码都运行在模块作用域,不会污染全局作用域。
  2. 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  3. 模块加载的顺序,按照其在代码中出现的顺序。

不同的模块间的依赖关系通过 require 来控制,而每个模块需要对外暴露的接口通过 exports 来决定。

由于 CommonJS 规范本身就只是为了服务端的 node.js 而考虑的,node.js 实现了 CommonJS 规范,所以运行在 node.js 环境中的 js 代码可以使用 requireexports 这两个命令,但在前端里,浏览器的 js 执行引擎并不认识 require 这些命令,所以需要进行一次转换工作,后续介绍。

6. AMD规范&Require.js(2009年)

CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS 规范比较适用。

但是,如果是浏览器环境,这种同步加载文件的模式就会导致浏览器陷入卡死状态,因为网络原因是不可控的。所以,针对浏览器环境的模块化,新提出了一种规范:AMD(Asynchronous Modules Definition)异步模块定义。

也就是说,对于 Node.js,对于服务端而言,模块化规范就是按照 CommonJS 规范即可。但对于浏览器,对于前端而言,CommonJS 不适用,需要看看 AMD 规范。

AMD 规范中定义:

  • 定义一个模块时通过 define 命令,通过 return 声明模块对外暴露的接口
  • 依赖其他模块时,通过 require 命令
7. CMD规范&Sea.js

CMD(Common Module Definition)也是专门针对浏览器、针对前端而提出的一种模块化规范。它跟 AMD 规范都是用途都是一样,用途解决前端里的模块化技术。

但两种规范各有各的优缺点,各有各的适用场景和局限性吧,我还没使用过这两种规范,所以无从比较,但网上关于这两种规范比较的文章倒是不少。

CMD 规范中定义:

总之,虽然两种规范都是用于解决前端里的模块化技术,但实现的本质上还是有些不同,后续介绍。

对于 CMD 规范的具体实现是 Sea.js,前端里如果想使用 CMD 规范的模块化技术,需要借助 Sea.js。

8. ES6标准(2015年)

2015 年发布的 ES6 标准规范中,新增了 Module 特性,也就是官方在 ES6 中,在语言本身层面上,添加了模块的机制支持,让 JavaScript 语言本身终于可以支持模块特性了。

在 ES6 之前的各种方案,包括 CommonJS,AMD,CMD,本质上其实都是利用函数具有的本地变量的特性进行封装从而模拟出模块机制。也就是说,这些方案都是需要在运行期间,才会去动态的创建出某个模块,并暴露模块的相关接口。这种方案其实也存在一些局限性。

而 ES6 新增了模块的机制后,在代码的解析阶段,就能够确定模块以及模块对外的接口,而不用等到运行期。这种本质上的区别,在借助开发工具写代码阶段的影响很明显就是,终于可以让开发工具智能的提示依赖的模块都对外提供了哪些接口。

但 ES6 由于是新增的特性,在支持方面目前好像还不是很理想,并不是所有环境都支持 ES6 的模块机制好像,所以会看到某些大佬的文章里会介绍一些诸如:Babel、Browserify。

Babel 用于将 ES6 转换为 ES5 代码,好让不支持 ES6 特性的环境可以运行 ES5 代码。

Browserify 用于将编译打包 js,处理 require 这些浏览器并不认识的命令。

3. ES6标准

ES6 中新增的模块特性,在其他同学日志中已经稍微介绍了点,这里也不具体展开介绍了,需要的话自行查阅。

这里就简单说下,在前端浏览器中使用 ES6 模块特性的步骤:

  1. 定义模块,通过指定 <script type="module"> 方式
  2. 依赖其他模块使用 import,模块对外暴露接口时使用 export;

需要注意的一点是,当 JS 文件内出现 import 或者 export 时,这份 JS 文件必须声明为模块文件,即在 HTML 文档中通过指定 <script> 标签的 type 为 module,这样 import 或 export 才能够正常运行。

也就是说,使用其他模块的功能时,当前的 JS 文件也必须是模块。

另外,有一点,ES6 的模块新特性,所有作为模块的文件都需要开发人员手动去 HTML 文档中声明并加载,这是与其他方案不同的一点,ES6 中 import 只负责导入指定模块的接口而已,声明模块和加载模块都需要借助 <script> 实现。

到这里只是我个人的一些见解和一些常识,深入的话需要自行去摸索哈,至于如何兼容浏览器的ES6,就需要各位查看以往的博客了