Skip to content

模块化开发

模块化演变过程

  • 模块化是一种最主流的代码组织方式,它通过把我们的复杂代码按照功能的不同划分为不同的模块,单独维护的方式去提高开发效率降低维护成本
  • 模块化只是思想

历史阶段

阶段一

  • 最开始是把数据分成模块,按照文件划分方式
html
<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>

缺点:

  • 没有独立私有空间,会污染全局作用域
  • 模块多会产生命名冲突问题
  • 无法管理模块依赖关系

阶段二

  • 约定每个模块暴露一个全局对象,按命名空间方式
html
<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>

缺点:

  • 仍然没有独立私有空间,可以在外部被访问修改
  • 无法管理模块依赖关系

阶段三

  • 使用立即执行函数方式为模块提供私有空间
html
<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):异步模块定义规范。提供了 requiredefault 关键字实现模块化的操作

  • 参数1:模块名字
  • 参数2:模块依赖项(数组)
  • 参数3:函数参数与依赖项一一对应

模块化AMD

模块化AMDrequire

  • AMD 使用起来相对复杂
  • 模块 JS 文件请求频繁

淘宝的 Sea.js + CMD

  • 整合了CommonJSAMD 规范的特点专门实现浏览器异步模板化的加载

sea+cmd

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 的出现才慢慢解决

Common+ES

ES Modules

主要学习的点

  1. 学习它作为规范或标准约定了哪些特性和语法
  2. 通过工具或方案解决它在运行环境兼容性所带来的问

使用 serve 启动

bash
serve .

ES Modules 有哪些特性:

  • 自动采用严格模式,忽略 'use strict'
  • 每个 ESM 模块都是单独的私有作用域
  • ESM 是通过 CORS 去请求外部 JS 模块的
  • ESM 的 script 标签会延迟执行脚本
html
<!-- 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 启动

bash
browser-sync . --files **/*.js

使用 export default 导出的是对象字面量

js
// 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 导出

  1. 不是字面量对象,而是固定语法
  2. 导出的是这个成员的引用地址(外部拿到的成员会受当前内部模块修改的影响)
  3. 外面导入的成员是只读的
js
// 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 导入

  • 导入的过程不是解构,而是固定语法(函数后面要跟花括号的语法)
js
// 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 或在花括号之前加上导入默认成员

js
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 即可
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

es-module-loader

browser-es-module-loader

不加 nomodule 在支持 ES6 Module 的浏览器会执行两次

  • 浏览器本身执行一次 ES Module
  • ES Module polyfill 执行一次 ES Module
  • 可以使用 nomodule 属性,让脚本在不支持 ES Module 浏览器中工作
html
<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

  1. 将文件的扩展名由 .js 改为 .mjs
  2. 启动时需要额外添加 --experimental-modules 参数
bash
node --experimental-modules index.mjs
js
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 模块

js
// 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 规范了

js
// 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 模块

js
// 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 执行

bash
nodemon --experimental-module esm.mjs

CommonJS 与 ES Module 差异

cjs.js

  • ES Module 中没有 CommonJS 中哪些模块全局成员
js
// 加载模块函数
console.log(require)

// 模块对象
console.log(module)

// 导出对象别名
console.log(exports)

// 当前文件的绝对路径
console.log(__filename)

// 当前文件所在目录
console.log(__dirname)

esm.mjs

js
// 通过 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

bash
node --experimental-modules index.mjs

可以在 package.json 中新增 "type": "module" 字段,之后就可以直接执行 index.js

bash
node --experimental-modules index.js

Babel 兼容

bash
yarn @babel/node @babel/core @babel/preset-env --dev

preset-env 是一个插件的集合

bash
yarn babel-node index.js --presets=@babel/preset-env

安装 @babel/plugin-transform-modules-commonjs

bash
yarn add @babel/plugin-transform-modules-commonjs --dev

新建 .babelrc

json
{
  "plugins": [
    "@babel/plugin-transform-modules-commonjs"
  ]
}

常备不懈,才能有备无患