前言
前端模块化的概念在技术论坛是个老生常谈的话题。什么CommonJS、AMD、CMD、UMD、ES6模块化等都能扯一点,但大多囫囵吞枣。
本文围绕前端模块化的目的、历史时间线、应用三方面来叙述。
文中案例均测试执行过。如果文字看累了💕,代码直接复制粘贴,更容易理解。
模块化的目的
前端开发初期,Javascript只承担简单的交互动画、表单校验等功能实现。随着Ajax技术得以广泛使用,Web进入2.0时代,浏览器也因此承载了更多的内容与逻辑实现。
随着工程越来越庞大,越来越复杂,项目需要一个团队进行分工协作、进度管理等,开发人员不得不采取模块化的方法去管控项目开发、迭代。从而避免命名冲突、代码冗余、代码高耦合、低内聚等问题。
模块化历史时间线
无模块化规范
原始写法
将不同函数归置在一起,就算是一个模块。
缺陷:污染全局变量,存在命名冲突,模块成员之间无明显依赖关系。
function m1() {
//...
}
function m2() {
//...
}
对象/命名空间 写法
利用对象包裹模块,降低命名冲突的风险,有一定的模块封装和隔离。
缺陷:没有解决命名冲突的根本问题,比如模块名称相同🤦♂️;暴露私有内部属性和方法,且可被改写。
var m1 = new Object({
a: 0,
b: function() {
//...
}
})
m1.a = 1 // 内部a属性被改写
立即调用函数表达式(IIFE)
利用立即执行函数,创建私有的命名空间,解决暴露内部属性和方法的问题。
var m1 = (function() {
let a = 0 //函数作用域下,a变量私有化
let b = function() { console.log('a': a) }
return {
b: b
}
})()
m1.b() // 0
引入依赖
显式地传入模块依赖的全局变量。既保证模块的独立性,且使模块之间的依赖关系变的明显,依赖模块 (Jquery) 需要先于当前模块引入。这种写法奠定了模块化规范的基础。
<script type="text/javascript" src="jquery.js"></script>
<script>
//m1.js
var m1 = (function(window,$) {
let a = 0
let b = function() {
$('body').text(a)
}
return {
b: b
}
})(window,jQuery)
m1.b() // 页面上显示0
</script>
模块化规范
CommonJs [09- 10]
CommonJs规范下,每个js文件都被视为独立的模块,拥有私有的变量和方法,不会污染全局环境;模块可加载多次,但仅在首次加载时运行,后续加载均读取缓存文件,需清理缓存后可再次读取文件;模块加载按照代码顺序同步执行;导出的值为拷贝;
NodeJs即基于CommonJs规范下编写。
案例
//helloworld.js
module.exports.helloworld = function () {
console.log('hello world')
}
//main.js
let helloworld = require('./hello.js').helloworld
helloworld() // node main.js 输出 hello wolrd
CommonJs 在服务端取得了不错的实践,但在浏览器端却无法得以推广,主要因为如下原因。
- 没有
module、exports、require、global变量。 - 在服务端
require一个模块,只会有磁盘I/O,所以同步加载机制没什么问题;但若是浏览器加载,一是会产生开销更大的网络I/O,存在严重阻塞问题;二是异步,会产生时序上的错误。
目前,borwserify可转换CommonJS格式,使其运行在浏览器端。但无法根本解决上述所说的阻塞问题。
AMD [10]
为了制订新的适用浏览器的标准规范,衍生出三大流派(保守派、激进派、中间派),其中的激进派思想的产物即AMD规范。AMD规范是异步加载模块,允许指定回调函数。
require.Js是该规范下的产物。官网直通车
define(id?: String, dependencies?: String[], factory: Function|Object)
id 是模块名字,?代表可选参数。
dependencies 指定依赖的模块列表,是个数组,也是可选的参数。每个依赖的模块的输出将作为参数传入factory。如果没有指定dependencies,默认值是['require', 'exports', 'module']。
factory是最后一个参数,包裹了模块的具体实现。
案例
//定义模块
//helloworld.js
define(function () {
let msg = 'hello world'
let helloworld = function () {
console.log(msg)
}
return { helloworld }
})
//main.js
(function () {
require.config({
baseUrl: '', //加载模块根路径
paths: {
helloworld: './helloworld' //基于根路径下的相对路径
}
})
require(['helloworld'], function (helloworld) {
helloworld.helloworld()
})
})()
<!DOCTYPE html>
<html>
<head>
<title>AMD Demo</title>
</head>
<body>
<!-- require.js可从官网直接下载 -->
<script data-main="./main.js" src="./require.js"></script>
<!-- 输出hello world -->
</body>
</html>
当依赖大量模块时,AMD规范存在性能问题,需等待所有依赖模块加载完毕才可执行模块内的程序。
CMD[11]
CMD 规范与AMD规范相似,且保留CommonJS中的延迟加载、就近声明特点。
sea.js基于当前规范编写,由玉伯提出。官网直通车
案例
//helloworld.js
define(function (require, exports, module) {
let msg = 'hello world'
function helloworld() {
console.log(msg)
}
exports.helloworld = helloworld
})
//main.js
define(function (require, exports, module) {
let helloworld = require('./hello').helloworld //就近原则
helloworld()
})
<!DOCTYPE html>
<html>
<head>
<title>CMD Demo</title>
</head>
<body>
<!-- 引入sea.js -->
<script type="text/javascript" src="./sea.js"></script>
<script>
seajs.use('./main')
</script>
</body>
</html>
UMD[14]
UMD规范主要通过if-elseif-else达到通用的目的。
- 先判断是否存在
exports方法,如果存在,采用 CommonJS 方式加载模块。 - 后判断是否存在
define方法,如果存在,采用 AMD 方式加载模块。 - 均没有,直接挂在window上
ES6[15-17]
喜大普奔!!!庆幸生存在ES6模块化的时代。
ES6模块化思想:尽量静态化。使得在编译阶段就能确定模块的依赖关系、输入和输出的变量。这种差异主要因为ES6模块对外接口是一种静态定义而非对象。
案例
// helloworld.js
let helloWorld = function() {
console.log('hello world')
}
export { helloWorld }
// main.js
import { helloWorld } from './helloworld.js'
helloWorld() //node main.js hello world
案例需借助babel-cli和babel-preset-es2015将ES6语法转为ES5语法才可执行。
步骤
npm init初始化生成package.json。npm i babel-cli -g和npm install babel-preset-es2015 --save-dev。- 生成
.babelrc文件。
{ "presets": ["es2015"] }
- 新建文件夹
src,移动helloworld.js以及main.js文件至src文件夹。 - 在根目录下执行
babel src -d lib lib文件夹下执行node main.js输出hello world
ES6模块化有如下特点
- ES6模块输出的是值的引用,而非拷贝。
- ES6模块是编译时输出接口,
import命令会被JS引擎静态分析,优先于其他模块内容执行,export模块命令会有变量提升的效果。因此,import和export的位置不影响程序的输出。
参考
Javascript模块化编程(一)
深入了解JavaScript模块化编程
《编程时间简史系列》JavaScript 模块化的历史进程
深入浅出javaScript模块化