51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

揭开JS模块化神秘的面纱

500.jpg

近几年,前端的发展太快了,所以每天必须给你自己充电,跟上发展的步伐;每天一小步,成功一大步。

我们知道,一个庞大的项目,如果没有做到模块化,那么后期维护和开发,会消耗更多的资源,尤其是在前端三大框架(Angular、react、vue)、Webpack盛行之时,如果你还停留在之前的老土开发模式,恭喜您快要被OUT了。所以说,作为Web前端工程师,这项技能必须掌握。

记得刚开始接触js模块化的时候,听上去是很高大上,其实不然,当你真正了解其精髓,发现也就那么回事,哈哈。还不了解JS模块化的,还不赶紧进来了解和学习。

什么是模块化

模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。

说的简单点就是模块就是实现特定功能的一组方法,模块化将使代码更好的管理、维护和使用。

模块化面临什么问题

从平时项目的尝试中,可以归纳出js模块化需要解决那些问题:

  1. 如何安全的包装一个模块的代码?(不污染模块外的任何代码)
  2. 如何唯一标识一个模块?
  3. 如何优雅的把模块的API暴漏出去?(不能增加全局变量)
  4. 如何方便的使用所依赖的模块?

要对一个东西进行深入的剖析,有必要带着目的去看。模块化所要解决的问题可以用一句话概括:

在没有全局污染的情况下,更好的组织项目代码

举一个简单的栗子,我们现在有如下的代码:

function doSomething () {
 const a = 10;
 const b = 11;
 const add = function (a + b) {
  return a + b
 }
 add (a + b)
}

在现实的应用场景中,doSomething 可能需要做很多很多的事情,add 函数可能也更为复杂,并且可以复用,那么我们希望可以将 add 函数独立到一个单独的文件中,于是:

// doSomething.js 文件
const add = require('add.js');
const a = 10;
const b = 11;
add(a+ b);
// add.js 文件
function add (a, b) {
 return a + b;
}
module.exports = add;

这样做的目的显而易见,更好的组织项目代码,注意到两个文件中的 require 和 module.exports,从现在的上帝视角来看,这出自 CommonJS 规范(后文会有一个章节来专门讲规范)中的关键字,分别代表导入和导出,抛开规范而言,这其实是我们模块化之路上需要解决的问题。另外,虽然 add 模块需要得到复用,但是我们并不希望在引入 add 的时候造成全局污染。
围绕着这些问题,js模块化开始了一段艰苦而曲折的征途。接下来一起探讨下:

简单封装:Namespace模式

命名空间模式解决了上面的两个问题:一是全局变量污染的问题,二是可能的名字冲突问题。虽然JavaScript 没有特别支持命名空间, 但命名空间模式在JavaScript中并不难实现,可以把模块写成一个对象,所有的模块成员都放到这个对象里面

上面的函数foo()和bar(),都封装在MYAPP对象里。使用的时候直接调用这个对象的属性即可 。

这虽然减少了全局变量的数量,但是它本质上是对象一点不安全,外部很容易改变内部的状态。

IIFE模式:立即执行函数写法

因为函数的局部作用域,使用"立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。

这样function内部的变量就对全局隐藏了,达到是封装的目的。但是这样还是有缺陷的,Module这个变量还是暴漏到全局了,随着模块的增多,全局变量还是会越来越多。

使用了IIFE模式后好像已经有点模块化那意思了,但是如果模块很多,我们一个模块需要另一个模块就需要改改了。

上述做法就是我们模块化的基础,目前,通行的JavaScript模块规范主要有两种:CommonJS 和 AMD。当然还有 玉伯的CMD 以及正在逐步普及的ES6 module,还有统一的通用规范UMD(本文不会讲到)

各种模块化规范。

在ES6之前 JavaScript 并没有内置的标准模块系统,于是社区的大牛们就制定了一些模块加载方案。

CommonJS

CommonJS 是nodejs也就是服务器端广泛使用的模块化机制。 该规范的主要内容是:模块必须通过module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。

commonJS用同步的方式加载模块,在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但这在浏览器端问题多多,限于网络原因,更合理的方案是使用异步加载。

AMD和require.js

AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思。它是一个在浏览器端模块化开发的规范。由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎 RequireJS,实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出。

AMD也采用 语句加载模块,但是不同于CommonJS,它要求两个参数:

我们可以来看看前面的例子用 AMD 规范的写法。

默认情况下,require.js假定这加载的模块与main.js在同一个目录,然后自动加载。我们可以使用require.config()方法对模块的加载行为进行自定义。

requireJS主要解决两个问题

js加载的时候浏览器会停止页面渲染,实现js文件的异步,加载避免网页失去响应;

管理模块之间的依赖性,便于代码的编写和维护;

CMD和sea.js

CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载时机上有所不同:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。

Sea.js 的初衷是为了让 CommonJS Modules/1.1 的模块能运行在浏览器端,但由于浏览器和服务器的实质差异,实际上这个梦无法完全达成,也没有必要去达成。

在 SeaJS 的世界里,一个文件就是一个模块。所有模块都遵循 CMD规范,我们可以像在 Node环境中一样来书写模块代码,我们还是来实现这个加法运算模块:

ES6 Module

值得庆祝的是 ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块功能主要由两个命令构成: 和 。 命令用于规定模块的对外接口, 命令用于输入其他模块提供的功能。

总结

此文只是大概提到了一些JS模块化的常用方法,细节方面还需自己摸索,没有去提供具体的实例分析(因为我比较懒),如果您还想继续深入的学习,可以加入前端群:295431592,跟大牛们一起学习。请持续关注"Web前端之家",后续会分享一些具体的实例教程。

赞(1)
未经允许不得转载:工具盒子 » 揭开JS模块化神秘的面纱