ps:其实一直都想写关于模块化的东西,刚刚好刷到一个题目是需要讲模块化的,就借着这个机会了解了解。
可从 IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、
<script type="module">
这几个角度考虑。
原文地址:个人博客
IIFE(立即调用函数表达式)
是一个在定义时就会立即执行的 JavaScript 函数。
;(function() {
statements
})()
这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
CommonJS
-
特点
-
同步加载,模块加载会阻塞接下来代码的执行,需要等到模块加载完成才能继续执行
-
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
-
-
环境
Node.js 是 commonJS 规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:
module
、exports
、require
、global
。实际使用时,用module.exports
定义当前模块对外输出的接口(不推荐直接用 exports),用 require 加载模块。 -
语法
在 CommonJS 中,有一个全局性方法 require(),用于加载模块。假定有一个数学模块 math.js,就可以像下面这样加载。
require 命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象。即使再次执行 require 命令,也不会再次执行该模块,而是到缓存中取值。
var math = require('math')
调用模块里面的方法
var math = require('math') math.add(2, 3) // 5
定义和暴露模块:
注意暴露出去的是值的拷贝也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
// 定义模块math.js var basicNum = 0 function add(a, b) { return a + b } module.exports = { //在这里写上需要向外暴露的函数、变量 add: add, basicNum: basicNum }
AMD
AMD 是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。
-
特点
- 它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
首先我们说为什么是异步模式:
var math = require('math') math.add(2, 3)
第二行 math.add(2, 3),在第一行 require('math')之后运行,因此必须等 math.js 加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。
这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。
-
环境
浏览器环境
-
语法
AMD 也采用 require()语句加载模块,但是不同于 CommonJS,它要求两个参数:
require([module], callback)
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数 callback,则是加载成功之后的回调函数。如果将前面的代码改写成 AMD 形式,就是下面这样:
require(['math'], function(math) { math.add(2, 3) })
math.add()与 math 模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD 比较适合浏览器环境。
定义和暴露模块:
// 无模块依赖 define(function() { var add = function(x, y) { return x + y } return { add: add } }) // 有模块依赖 define([module1, module2], function(module1, module2) { // ... return module })
-
应用
目前,主要有两个 Javascript 库实现了 AMD 规范:require.js 和 curl.js。
CMD
-
特点
- CMD 是在 AMD 基础上改进的一种规范,CMD 推崇依赖就近,延迟执行此规范其实是在 sea.js 推广过程中产生的。可以把你的依赖写进代码的任意一行。
-
环境
浏览器环境
-
语法
define(factory)
factory
为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory 方法在执行时,默认会传入三个参数:require、exports 和 module。// CMD define(function(require, exports, module) { var a = require('./a') a.doSomething() var b = require('./b') b.doSomething() }) /** sea.js **/ // 定义模块 math.js define(function(require, exports, module) { var $ = require('jquery.js') var add = function(a, b) { return a + b } exports.add = add }) // 加载模块 seajs.use(['math.js'], function(math) { var sum = math.add(1, 2) })
UMD
-
特点
兼容 AMD 和 commonJS 规范的同时,还兼容全局引用的方式
-
环境
浏览器和服务器环境
-
语法
无导入导出规范,只有如下的一个常规写法:
;(function(root, factory) { if (typeof define === 'function' && define.amd) { //AMD define(['jquery'], factory) } else if (typeof exports === 'object') { //Node, CommonJS之类的 module.exports = factory(require('jquery')) } else { //浏览器全局变量(root 即 window) root.returnExports = factory(root.jQuery) } })(this, function($) { //方法 function myFunc() {} //暴露公共方法 return myFunc })
webpack(require.ensure)
-
特点
- webpack2.x 代码分割。webpack 在编译时,会静态地解析代码中的 require.ensure(),同时将模块添加到一个分开的 chunk 当中。这个新的 chunk 会被 webpack 通过 jsonp 来按需加载。
-
环境
webpack2.x
-
语法
require.ensure(dependencies: String[], callback: function(require), chunkName: String) // 依赖 dependencies // 这是一个字符串数组,通过这个参数,在所有的回调函数的代码被执行前,我们可以将所有需要用到的模块进行声明。 // 回调 callback // 当所有的依赖都加载完成后,webpack会执行这个回调函数。require 对象的一个实现会作为一个参数传递给这个回调函数。因此,我们可以进一步 require() 依赖和其它模块提供下一步的执行。 // chunk名称 chunkName // chunkName 是提供给这个特定的 require.ensure() 的 chunk 的名称。通过提供 require.ensure() 不同执行点相同的名称,我们可以保证所有的依赖都会一起放进相同的 文件束(bundle)。
ES Module
-
特点
-
编译时期加载,ES6 模块是编译时输出接口。
-
输出不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
ES6 模块输出的是值的引用。JS 引擎对脚本静态分析的时候,遇到模块加载命令
import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import
有点像 Unix 系统的“符号连接”,原始值变了,import
加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
-
-
环境
浏览器或者服务器环境
-
语法
// CommonJS模块 let { stat, exists, readFile } = require('fs') // 等同于 let _fs = require('fs') let stat = _fs.stat let exists = _fs.exists let readfile = _fs.readfile
上面代码的实质是整体加载
fs
模块(即加载fs
的所有方法),生成一个对象(_fs
),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,再通过import
命令输入。// ES6模块 import { stat, exists, readFile } from 'fs'
上面代码的实质是从
fs
模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。除了静态加载带来的各种好处,ES6 模块还有以下好处。
- 不再需要 UMD 模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
- 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者 navigator 对象的属性。
- 不再需要对象作为命名空间(比如 Math 对象),未来这些功能可以通过模块提供。
定义和暴露:
模块功能主要由两个命令构成:
export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。// profile.js export var firstName = 'Michael' export var lastName = 'Jackson' export var year = 1958
上面代码是
profile.js
文件,保存了用户信息。ES6 将其视为一个模块,里面用export
命令对外部输出了三个变量。使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块。
// main.js import { firstName, lastName, year } from './profile.js' function setName(element) { element.textContent = firstName + ' ' + lastName }
上面代码的
import
命令,用于加载profile.js
文件,并从中输入变量。import
命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js
)对外接口的名称相同。 -
应用
ES6 的最新语法支持规范
<script type="module">
-
特点
- 异步加载,浏览器对于带有
type="module"的<script>
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。
- 异步加载,浏览器对于带有
-
语法
<script type="module" src="./foo.js"></script> <!-- 等同于 --> <script type="module" src="./foo.js" defer></script>
如果网页有多个
<script type="module">
,它们会按照在页面出现的顺序依次执行。<script>
标签的async
属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。<script type="module" src="./foo.js" async></script>
一旦使用了
async
属性,<script type="module">
就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。举例来说,jQuery 就支持模块加载。
<script type="module"> import $ from "./jquery/src/jquery.js"; $('#message').text('Hi from jQuery!'); </script>
对于外部的模块脚本(上例是
foo.js
),有几点需要注意。- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明
use strict
。 - 模块之中,可以使用
import
命令加载其他模块(.js 后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export
命令输出对外接口。 - 模块之中,顶层的
this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层使用this
关键字,是无意义的。 - 同一个模块如果加载多次,将只执行一次。
参考文章
- 欢迎斧正,拍砖~
- 你的代码里有你读过的书和走过的路
- 经历过才会有更好的成长
博客地址 欢迎到访
GitHub 我不要Star✨(疯狂暗示)