一 什么是模块化开发
模块化是最重要的前端开发范式之一。前端应用日益复杂,项目代码膨胀,我们需要更好的方式来组织和管理代码。模块化就是一种主流的代码组织方式,它通过把复杂代码划分为易于单独维护的模块,来提高开发效率,降低维护成本。
二 模块化开发的演进过程
1. Stage1 - 文件划分的方式
index.html
index.js
index.css
detail.html
detail.js
detail.css
特点:会污染全局作用域、易产生命名冲突、无法管理模块间的依赖关系、完全依靠约定不适用于大型项目。
2. Stage2 - 命名空间的方式
detail.js 中所有内容写在一个对象(如 detail)内,访问该对象内的成员通过 detail.xxx 的方式。
特点:相比文件划分的方式,可减少部分命名冲突。
3. Stage3 - IIFE 立即执行函数提供私有空间
detail.js 中所有内容写入一个立即执行函数,把其中需要对外暴露的成员挂载到全局对象。
特点:可以实现私有成员。
4. Stage4 - 模块化规范的出现(模块化标准 + 模块化加载器)
4.1 CommonJS 规范
node环境的主流规范,由nodeJS 提出的同步加载模块规范,目前主要应用于node环境。
特点:一个文件就是一个模块、每个模块有单独的作用域、通过 require 函数载入模块 、通过 module.exports 导出成员。
// CommonJS 规范示例
// module.js
function add(a, b) {
return a + b;
}
const foo = 'foo'
// 导出写法一
module.exports = {
add, foo
}
// 导出写法二
exports.add = add
exports.foo = foo
// index.js
const tool = require('./module.js')
console.log(tool.add(1, 2)) // 3
console.log(tool.foo) // foo
// 执行 node index.js
4.2 AMD (Asynchronous Module Definition)规范
为浏览器设计的异步加载模块规范,以 require.js 为代表,目前已经不再流行。
特点:AMD中使用require 其内部就创建一个script标签 载入并执行。目前绝大多数第三方库都支持 AMD 规范。
// AMD 规范示例
// module1.js
// 用 define 定义模块及其依赖
define('module1', ['jquery', './module2'], function($, module2) {
// 用 return 导出成员
return {
start : function(){
$('body').animate({ margin: '200px' })
module2()
}
}
})
// index.js
// 用 require 载入模块
require(['./module1'], function(module1) {
module1.start()
})
4.3 CMD 规范
淘宝推出的异步加载模块规范,以 sea.js 为代码,目前已经不再流行。
特点:写法上类似 CommonJS 规范。
// CMD 规范示例
// 用 define 定义模块
define(function(require, exports, module){
// 通过require 引入依赖
var $ = require('jquery')
// 通过 module.exports 或 exports.xxx = xxx 对外暴露成员
module.exports = function() {
console.log('module 2~')
$('body').append('<p>module2</p>')
}
})
4.4 ES Modules 规范
浏览器环境的主流规范,模块化的标准规范,浏览器中的模块化规范统一于ES Modules,是语言层面支持的模块化规范,目前最为流行,许多浏览器的最新版本已经直接支持 ES Modules 规范了。
特点:
a. 自动采用严格模式,忽略 'use strict'(this 为 undefined)
b. 每个 ESM 模块都有单独的私有作用域
c. ESM 通过 CORS 的方式去请求外部JS模块,需要外部JS模块支持跨域
d. ESM 的 script 标签会延迟执行脚本(相当于给script标签添加了 defer 属性)
// ES Modules 规范示例
// index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ES Modules 规范</title>
<!-- 添加 type=module 属性,就可以以 ES Module是标准执行其中js代码了 -->
<script type="module" src="./index.js"></script>
</head>
<body>
<h1>一个标题</h1>
<p>一个段落。</p>
</body>
</html>
// index.js
// 导入成员
import { time, bar } from './tool.js'
console.log(time())
console.log(bar)
// tool.js
function time(){
return new Date().toLocaleString()
}
const bar = 'bar'
// 导出成员
export { time, bar }
三 详解 ES Modules 模块化规范
1. ES Modules 导入和导出
1.1 注意事项:
a. 导出成员 export { time, bar } - export {} 是固定语法,不是对象字面量语法,表示导出成员 time, bar。
b. 导出默认成员 export default { name, age } - export default xxx 只导出一个成员,此处 { name, age }是作为一个对象导出。
c. 导入成员 import { time, bar } - import {} 是固定语法,不是解构,表示导入成员 time, bar。
d. 导入的是数据的内存地址,并非拷贝一份的数据,无论基本类型还是引用类型。
e. 导入的成员是只读的,不可修改。
1.2 导入模块的三种方式:
import { name } from './module.js' // 相对路径 路径需完整
import { name } from '/test/module.js' // 绝对路径 第一个/表示根目录
import { name } from 'http://xxxx/xxxx/module.js' // 完整url 可导入CDN资源
1.3 执行模块而不导入成员
import {} from './module.js
import './module.js'
1.4 导入全部成员
import * as mod from './module.js'
1.5 动态的导入(加载)模块
import('./module.js').then(function(module) {
console.log(module)
})
1.6 同时导出 导入默认成员和命名成员
// tool.js 同时导出默认成员 和 命名成员
function time(){
return new Date().toLocaleString()
}
const bar = 'bar'
const foo = {
id: 123,
name: 'foo123'
}
export { time, bar }
export default foo
// index.js 同时导入默认成员 和 命名成员
import { time, bar, default as foo } from './tool.js' // 写法一
import foo, { time, bar } from './tool.js' // 写法二
console.log(time())
console.log(bar)
console.log(foo)
1.7 直接导出 导入的成员
export { time, foo } from './tool.js'
2. ES Modules 浏览器环境 polyfill
由于ES Modules规范提出于2014年,故IE和一些老旧浏览器仍不支持,可以使用如下三个polyfill,让它们支持 ES Modules规范,但这种做法不可以用于生产环境(因为效率低)。
babel-browser-build.js
browser-es-module-loader.
polyfill.min.js
<!-- nomodule 表示对于不支持 ES Modules 的浏览器才执行 -->
<script nomodule src="https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
3. ES Modules 在 node 中的应用
node环境的主流模块化规范虽然是CommonJS,但是在 node8.5 版本之后,node环境也开始逐步的支持 ES Modules 规范了(以实验特性执行)。
使用方式:
方式一:
a. 修改 xxx.js 文件后缀名为 mjs
b. 执行文件时使用 node --experimental-modules xxx.mjs
方式二:
a. 在 package.json 文件中设置 type:"module",则默认所有js文件以 ESM 模块规范执行
b. 如果项目中存在CommonJS规范的文件,则修改其后缀为 .cjs 则其可按 CommonJS的方式执行
注意事项:
a. ES Modules 规范的文件中,可以导入 CommonJS 模块
b. CommonJS 规范的文件中,不能导入 ES Modules 模块
c. CommonJS 始终只会导出一个默认成员
4. node中 ES Modules 与 CommonJS 的全局变量
a. CommonJS 中可正常使用如下全局变量
// CommonJS 中可正常使用如下全局变量
// 加载模块函数
console.log('require', require)
// 模块对象
console.log('module', module)
// 导出对象别名
console.log('exports', exports)
// 当前文件的绝对路径
console.log('__filename', __filename)
// 当前文件的绝对目录
console.log('__dirname', __dirname)
b. ES Modules 中则没有上述 CommoJS 中的全局变量,用如下方式得到 __filename 和 __dirname
// ESM 中 没有 CommonJS 中的那些模块全局成员
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import['meta'].url)
console.log('__filename', __filename)
const __dirname = dirname(__filename)
console.log('__dirname', __dirname)
console.log('import: ', import.meta.url)
老版本node(版本 < 8.5) 中,可用 Babel 来兼容 ES Modules:
步骤:
yarn add @babel/node @babel/core @babel/preset-env --dev
yarn babel-node index.js --presets=@babel/preset-env
或者在根目录下 添加 .babelrc 文件:
内容:
{
"presets": ["@babel/preset-env"]
}
内容或者:
{
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
本文 完。