模块化开发
模块化演变过程
- 模块化是一种最主流的代码组织方式,它通过把我们的复杂代码按照功能的不同划分为不同的模块,单独维护的方式去提高开发效率降低维护成本
- 模块化只是思想
历史阶段
阶段一
- 最开始是把数据分成模块,按照文件划分方式
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<!-- module-a.js -->
<script>
var name = 'module-a'
function method1() {
console.log(name + '#method1')
}
</script>
缺点:
- 没有独立私有空间,会污染全局作用域
- 模块多会产生命名冲突问题
- 无法管理模块依赖关系
阶段二
- 约定每个模块暴露一个全局对象,按命名空间方式
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<!-- module-a.js -->
<script>
var moduleA = {
name: 'module-a',
method1: function () {
console.log(this.name + '#method1')
},
}
</script>
缺点:
- 仍然没有独立私有空间,可以在外部被访问修改
- 无法管理模块依赖关系
阶段三
- 使用立即执行函数方式为模块提供私有空间
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<!-- module-a.js -->
<script>
;(function () {
var name = 'module-a'
function method1() {
console.log(name + '#method1')
}
window.moduleA = {
method1: method1,
}
})()
</script>
模块化历程
上面几种都通过 script 标签引用每一个用到的模块,意味着模块加载不受代码控制。比如:代码中引用一个模块,但在 HTML 中却忘记引用这个模块,就会出现问题
CommonJS 规范
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过
module.exports
导出成员 - 通过
require
函数载入模块
CommonJS 是以同步模式加载模块。Node 执行机制是在启动时加载模块,执行过程中是不需要加载的,如果浏览器使用就会导致效率低下(每一次页面加载都会导致大量同步模式请求出现)
AMD(Asynchronous Module Definition):异步模块定义规范。提供了 require
和 default
关键字实现模块化的操作
- 参数1:模块名字
- 参数2:模块依赖项(数组)
- 参数3:函数参数与依赖项一一对应
- AMD 使用起来相对复杂
- 模块 JS 文件请求频繁
淘宝的 Sea.js + CMD
- 整合了
CommonJS
和AMD
规范的特点专门实现浏览器异步模板化的加载
CommonJS
- CommonJS 规范期初是为了弥补 JS 语言模块化缺陷
- CommonJS 是语言层面的规范,当前主要用于 Node.js
- CommonJS 规定模块化分为引入、定义、标识符三个部分
- Moudle 在任意模块中可直接使用包含模块信息
- Require 接收标识符,加载目标模块
- Exports 与 module.exports 都能导出模块数据
- CommonJS 规范定义模块的加载是同步完成
导出 module 属性
- 任意 JS 文件就是一个模块,可以直接使用 module 属性
- id:返回模块标识符,一般是一个绝对路径
- filename:返回文件模板的绝对路径
- loaded:返回布尔值,表示模块是否完成加载
- parent:返回对象存放调用当前模块的模块
- children:返回数组,存放当前模块调用的其它模块
- exports:返回当前模块需要暴露的内容
- paths:返回数组,存放不同目录下的 node_modules 位置
导入 require 属性
- 基本功能是读入并且执行一个模板文件
- resolve:返回模板文件绝对路径
- extensions:依据不同后缀名执行解析操作
- main:返回主模板对象
缓存优化原则
- 提高模块加载速度
- 当前模块不存在,则经历一次完整加载流程
- 模块加载完成后,使用路径作为索引进行缓存
模块化标准规范
ECMAScript2015(ES6)
- 最开始的
ES modules
没有被现代浏览器支持,随着 webpack 的出现才慢慢解决
ES Modules
主要学习的点
- 学习它作为规范或标准约定了哪些特性和语法
- 通过工具或方案解决它在运行环境兼容性所带来的问
使用 serve
启动
serve .
ES Modules 有哪些特性:
- 自动采用严格模式,忽略
'use strict'
- 每个 ESM 模块都是单独的私有作用域
- ESM 是通过 CORS 去请求外部 JS 模块的
- ESM 的 script 标签会延迟执行脚本
<!-- 1. ESM 自动采用严格模式,忽略 'use strict' -->
<script type="module">
console.log(this) // undefined
</script>
<!-- 2. 每个 ES Module 都是运行在单独的私有作用域中 -->
<script type="module">
var foo = 100
console.log(foo) // 100
</script>
<script type="module">
console.log(foo) // Uncaught ReferenceError: foo is not defined
</script>
<!-- 3. ESM 是通过 CORS 的方式请求外部 JS 模块的 -->
<script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
<!-- 4. ESM 的 script 标签会延迟执行脚本 -->
<script defer src="demo.js"></script>
<p>需要显示的内容</p>
导入导出
使用 browser-sync
启动
browser-sync . --files **/*.js
使用 export default
导出的是对象字面量
// module.js
var name = 'foo module'
function hello () {
console.log('hello')
}
class Person {}
export { name, hello as fooHello, Person }
// app.js
import { name, fooHello, Person } from './module.js'
console.log(name, fooHello, Person)
使用 export
导出
- 不是字面量对象,而是固定语法
- 导出的是这个成员的引用地址(外部拿到的成员会受当前内部模块修改的影响)
- 外面导入的成员是只读的
// module.js
var name = 'jack'
var age = 18
setTimeout(function () {
name = 'ben'
}, 1000)
// app.js
import { name, age } from './module.js'
console.log(name, age) // jack 18
setTimeout(function () {
console.log(name, age) // ben 18
}, 1500)
// name = 'tom' // Uncaught TypeError: Assignment to constant variable.
使用 import
导入
- 导入的过程不是解构,而是固定语法(函数后面要跟花括号的语法)
// module.js
export default { name, age }
// app.js
import { name, age } from './module.js' // 报错
import Person from './module.js' // 正确方式
const { name, age } = Person
console.log(name, age)
import
导入注意点
import
引用相对路径需要使用./xx
或/xx
不能省略./
import './module.js'
导入不需要外界控制的子功能模块一个模块需要导入成员特别多,可以使用
* as xx
导入所有成员动态加载模块
不仅想导入
export
成员,还想导入module.export
成员可以使用
default as xxx
或在花括号之前加上导入默认成员
import {} from './module.js'
import './module.js'
import * as mod from './module.js'
console.log(mod)
import('./module.js').then(function (module) {
console.log(module)
})
import { name, age, default as title } from './module.js'
import title, { name, age } from './module.js
console.log(name, age, title)
导出导入成员
- 以后需要模块直接导入
index.js
即可
// avatar.js
export var Avatar = 'Avatar Component'
// button.js
var Button = 'Button Component'
export default Button
// index.js
export { default as Button } from './button.js'
export { Avatar } from './avatar.js'
Polyfill
不加 nomodule
在支持 ES6 Module 的浏览器会执行两次
- 浏览器本身执行一次 ES Module
- ES Module polyfill 执行一次 ES Module
- 可以使用
nomodule
属性,让脚本在不支持 ES Module 浏览器中工作
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
<script type="module">
import { foo } from './module.js'
console.log(foo)
</script>
ES Modules in Node
Node 8.5 版本过后,内部就以实验特性支持 ES Module
- 将文件的扩展名由
.js
改为.mjs
- 启动时需要额外添加
--experimental-modules
参数
node --experimental-modules index.mjs
import { foo, bar } from './module.mjs'
console.log(foo, bar)
// 此时我们也可以通过 esm 加载内置模块了
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')
// 也可以直接提取模块内的成员,内置模块兼容了 ESM 的提取成员方式
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')
// 对于第三方的 NPM 模块也可以通过 esm 加载
import _ from 'lodash'
_.camelCase('ES Module')
CommonJS 与 ES Module
ES Module 中可以导入 CommonJS 模块
// commonjs.js
import mod from './commonjs.js'
console.log(mod) // { foo: 'commonjs exports value' }
// es-module.mjs
module.exports = {
foo: 'commonjs exports value',
}
CommonJS 始终只会导出一个默认成员
注意: import 不是解构导出对象
在 Node12 版本中,将文件名修改为
.cjs
就可以使用 CommonJS 规范了
// commonjs.js
import { foo } from './commonjs.js'
console.log(foo) // Node10:SyntaxError: Unexpected token
console.log(foo) // Node12:throw new ERR_REQUIRE_ESM(filename);
console.log(foo) // Node14:SyntaxError: The requested module './commonjs.js' is expected to be of type CommonJS, which does not support named exports. CommonJS modules can be imported by importing the default export.
console.log(foo) // Node16:commonjs exports value
// es-module.mjs
exports.foo = 'commonjs exports value'
CommonJS 不能导入 ES Module 模块
// commonjs.js
export const foo = 'es module export value'
// es-module.mjs
const mod = require('./es-module.mjs')
console.log(mod) // es-module.mjs not supported.
nodemon 执行
nodemon --experimental-module esm.mjs
CommonJS 与 ES Module 差异
cjs.js
- ES Module 中没有 CommonJS 中哪些模块全局成员
// 加载模块函数
console.log(require)
// 模块对象
console.log(module)
// 导出对象别名
console.log(exports)
// 当前文件的绝对路径
console.log(__filename)
// 当前文件所在目录
console.log(__dirname)
esm.mjs
// 通过 url 模块的 fileURLToPath 方法转换为路径
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)
Node 新版本支持
Node12 新建 index.mjs
node --experimental-modules index.mjs
可以在 package.json
中新增 "type": "module"
字段,之后就可以直接执行 index.js
node --experimental-modules index.js
Babel 兼容
yarn @babel/node @babel/core @babel/preset-env --dev
preset-env 是一个插件的集合
yarn babel-node index.js --presets=@babel/preset-env
安装 @babel/plugin-transform-modules-commonjs
yarn add @babel/plugin-transform-modules-commonjs --dev
新建 .babelrc
{
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}