本文章内容涉及前端模块化发展简史、CommonJS、AMD、CMD规范,以及现在的ESModule,了解这些浏览器和服务器通用的模块化方案的原理,以及后续更文的npm包管理、webpack打包编译工具,我们将能够以全新视角一举窥探前端工程化的关键技术。
前端模块化的定义
模块化,其实就是将程序按规则拆分为可按需导入、可复用的模块,通过模块依赖关系可构建成完整的程序。体现的是高内聚低耦合的设计原则。
前端模块化的简史
1. 全局function模式:将不同功能封装成不同的全局函数
// 即function声明都是挂在window下 (浏览器环境)
// 封装的全局函数可能散落在多个js文件、<script>标签
funtion dateFn(){
return new Date()
}
function timeFn(date){
return date.getTime()
}
function demoFn(){
return {}
}
const date = dateFn();
const time = timeFn(date)
缺陷:容易引发全局命名空间冲突,而且模块成员之间看不出直接关系
2. 全局namespace模式:通过统一规范namespace
// 减少全局变量 解决命名冲突
window.__Module__ = {
data: 100,
dateFn(){
return new Date()
},
timeFn(date){
return date.getTime()
},
demoFn(){
return {}
}
}
const module = window.__Module__
consr date = module.dateFn()
// 100
console.log(module.data)
module.data = 200
// 200
console.log(module.data)
缺陷:由于外部业务代码可以直接修改模块内部数据状态,引发数据安全风险
3. 立即执行函数IIFE模式:通过IIFE产生独立词法作用域
// 独立作用域 避免被外部直接访问模块内部
(function(window){
let data = 100
funtion dateFn(){
return new Date()
}
function timeFn(date){
return date.getTime()
}
function setData(val){
data = val
}
window.__Module__ = {
data,
dateFn,
timeFn,
setData,
}
})(window)
const module = window.__Module__
consr date = module.dateFn()
// 100
console.log(module.data)
module.setData(200)
// 200
console.log(module.data)
module.data = 300
// 200
console.log(module.data)
缺陷:无法解决模块间相互依赖的问题
4.立即执行函数IIFE模式增强:自定义依赖
假定上面的模块为moduleA.js 定义下面的moduleB.js依赖它
(function(window, moduleA){
function getData(){
return moduleA.data
}
window.__Modules__ = {
moduleA,
getData,
}
})(window, window.__Module__)
const module = window.__Modules__
module.moduleA.setData(250)
缺陷:依赖为参数传递,强耦合,可维护性低
前端模块化的规范
1. CommonJS模块规范简介
在Node.js模块系统中,每个文件都被视为独立的模块。拥有自己的模块作用域,不会污染全局作用域。
模块在服务器端运行时同步加载方式,在浏览器端提前编译打包处理方式。
模块可多次加载,第一次加载时会运行模块,模块输出结果会被缓存,再次加载时,会从缓存结果中直接读取模块输出结果。
模块加载的顺序,按照其在代码中出现的顺序。
通过require加载模块,exports或module.exports导出模块。
模块导出的变量值是值拷贝,类似立即执行函数IIFE方案中的内部变量。
// mod.js
var val = 250
function setVal(v) {
val = v
}
module.exports = {
val,
setVal,
}
//biz.js
const mod = require('./mod')
mod.setVal(120)
2. CommonJS模块加载机制
CommonJS模块导出的变量值是值拷贝,即使业务引入该模块变量值,通过模块暴露的接口修改内部变量值也不会影响业务的变量值。
//biz.js
const mod = require('./mod')
// 250
console.log(mod.val)
mod.setVal(110)
// 250
console.log(mod.val)
3. AMD与CMD模块规范简介
AMD规范采用非同步加载模块,允许指定回调函数(针对commonjs同步而诞生的规范)。
Node主要用于服务器端,模块文件通常都位于本地硬盘,加载速度比较快,所以适用CommonJS的这种同步加载方案。
但是浏览器环境下,模块需要网络请求从服务端加载模块,所以适用于异步加载,一般采用AMD规范。
require.js是AMD方案的一个具体实现库。
//定义模块
define(function(){
return {}
})
//定义模块 依赖其它模块
define(['module1', 'module2'], function(m1, m2){
return {}
})
//引入使用模块
require(['module1', 'module2'], function(m1, m2){
//使用m1 m2
})
CMD模块方案整合了CommonJS和AMD的优点,异步加载,使用时才会加载模块执行。
Sea.js是CMD方案的一个实现库。
// 定义模块
define(function(require, exports, module){
exports.val = value
module.exports = value
})
// 定义模块
define(function(require, exports, module){
// 引入依赖模块(同步)
var m2 = require('./m2')
// 引入依赖模块(异步)
require.async('./m3', function (m3) {
console.log(m3)
})
// 导出模块
exports.val = value
})
// 引用模块
define(function (require) {
var m1 = require('./m1')
var m4 = require('./m4')
m1.xx()
m4.xx()
})
AMD与CMD也仅是社区过渡方案,如今node环境下推行社区规范CommonJS,浏览器环境下推行ECMA规范ESModule。
4. ESModule模块规范简介
ESModule模块方案,在编译时通过静态代码分析就可确定模块的依赖关系、输入输出,同时还支持动态加载方式。通过export、export default、import、import()关键字声明和引用。
// 第1种方式 mod.js
export const val = 250;
export const getVal = () => {
return val
}
// 第2种方式 mod.js
const val = 250;
const getVal = () => {
return val
}
export { val, getVal }
// 第1、2种方式 模块引用
import { val, getVal } from './mod.js'
console.log(val)
// 第3种方式 mod.js
const request = (url) => {
return fetch(url)
}
export default request
// 第3种方式 模块引用 可自定导入名称
import req from './mod.js'
req('/api/v1').then(res => {})
// 异步加载
import('/js/mod.js').then(mod => {
console.log(mod)
})
// 动态加载 即运行时通过资源路径按需异步加载
fetch('/api/js/mod/v1').then(res => {
if (res.data.url) {
import(res.data.url).then(mod => {
console.log(mod)
})
}
})
5. npm、webpack简介
npm是一个js软件包中心仓,开发者可分享、引用、管理npm包。简单的说,npm包导出暴露的接口是符合模块化规范标准,这使得我们可引用这些npm包,安装添加至自己的项目依赖(package.json),在业务代码import、require使用它们,从而提高开发效率。通过工程化包的封装,实现代码的通用、复用能力。
webpack是一个用于现代JavaScript应用程序的静态模块打包工具。它处理应用代码时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将项目中所需的每一个模块组合成一个或多个bundles。它可通过loader和plugins拓展处理模块的能力,比如可对模块源代码进行读写、转换等一系列操作。