模块化开发与规范化标准:模块化概述-笔记

189 阅读6分钟

模块化演变过程

第一阶段:

约定每个JS文件就是一个独立模块,通过script标签引入。在这种模式中,所有模块都在全局空间去工作,没有自己的私有空间。这有很多缺陷:

  • 污染全局作用域,所有模块都能在外部被任意访问或修改。
  • 容易产生命名冲突问题。
  • 无法很好管理模块之间依赖关系。

第二阶段:

通过命名空间方式,将每个模块定义在一个独立的全局对象上,可以减少命名冲突的可能,但它仍然没有私有空间。

第三阶段:

IIFE,使用立即执行函数方式为模块提供私有空间,将需要暴露给外部的函数挂载到window对象上。

第四阶段:

各个模块化标准开始出现,最开始出现的是运行于Node环境的Common.JS,但它因为自身问题无法在浏览器推展开来:

  1. Common.JS是同步的。浏览器要在JS文件,需要从远程服务器获取,网路传输效率远远低于Node环境读取本地文件效率。采用Common.JS规范则会阻塞代码的执行,极大降低运行性能。
  2. Common.JS属于社区标准,非官方标准,而它读取了JS需要放入到一个运行环境中去执行,需要浏览器厂商的支持,可是各个浏览器厂商不愿意去更改底层代码去支持,所以也难以推行。

因为Common.JS的种种问题原因,所以后来出现了AMD规范、CMD规范,以及最后官方推出了统一浏览器端的ES Modules规范。

AMD的实现原理核心是通过创建script标签定义data-main的方式去拉取js文件,当文件下载完成后,执行定义的回调函数。这个可以去具体看看源码

<script data-main="./js/index.js" src="./js/require.js"></script>

所以它存在一些缺陷:

  • AMD使用起来相对复杂,需要define和require。
  • 模块JS文件请求频繁。

而ES Modules规范是随着WebPack等一系列打包工具的流行,才开始普及。

ES Modules

基本特性

  1. ESM 自动采用严格模式,忽略 'use strict'
  2. 每个 ES Module 都是运行在单独的私有作用域中。
  3. ESM 是通过 CORS 的方式请求外部 JS 模块的。
  4. ESMScript 标签会延迟执行脚本。

要注意ES6 模块导入导出语法并不是解构,而是一个固定语法。

CommonJS 相比,CommonJS 输出的是一个值的拷贝,所以在 CommonJS 中:

  • 对于基本数据类型导入属于复制,即会被模块缓存。同时在另一个模块可以对该模块输出的变量重新赋值。 
  • 对于复杂数据类型导入属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对一个模块的值做修改时会影响另一个模块。 
  • 当使用require命令加载某个模块时,就会运行整个模块的代码。
  • CommonJS 导入模块时,可以自动补全文件路径的扩展名。而ES modules不可以。

ES6 模块输出的是值的引用 ,ES6 模块中的导入属于【动态只读引用】:

  • 对于只读来说,即不允许修改导入变量的值,import 的变量是只读的,当模块遇到 import 命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。不像 CommonJS 可以修改读取的变量。
  • 对于动态来说,由于 ES6 输出的是引用,当该引用的原始值发生变化时,import 加载的值也会发生变化。不论是基本数据类型还是复杂数据类型 。

其它小点:

  1. 引用模块时的点斜线是不能省略的,这两者都会认为是加载第三方模块,例如:

import A from "Module.js"

  1. import 可以直接引用网络资源模块。 例如:

import { name } from 'http://localhost:3000/04-import/module.js'

  1. 可以仅仅导入模块,而不引用。对于导入子功能模块很有用:

import {} from './module.js'

import './module.js'

  1. 通过星号导入模块所有文件。例如:

import * as mod from './module.js'

  1. 通过全局的import方法,实现和CommonJS一样的动态导入机制。它返回的是个Promise。
  1. 具名成员和默认成员可以同时导入,例如:

import abc, { name, age } from './module.js'

  1. export import的书写方式,将零散的模块组织到一起进行输出:

export { foo, bar } from './module.js'

export { default as Button } from './button.js' // 对于默认导出情况

export { Avatar } from './avatar.js' // 对于具名导出情况

ES Modules的Polyfill

通过以下cdn来兼容不支持ES Modules的模块,原理就是将babel和es module loader下载下来,loader将浏览器不识别的ES Modules交给babel去转换。对于需要import进来的文件通过ajax的方式去请求,再使用babel进行转换。从而支持ES Modules。

<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标签添加了 nomodule 属性,那么它只会在不支持ES Modules的浏览器下去工作。

ES Modules in Node.js

支持情况

在 node v8.5 将 ES Modules作为实验特性引入了,使用方法如下所示:

  1. 将文件的扩展名由 .js 改为 .mjs;
  2. 启动时需要额外添加`--experimental-modules`参数;
// 此时我们也可以通过 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')

// 不支持,因为第三方模块都是导出默认成员
// import { camelCase } from 'lodash'
// console.log(camelCase('ES Module'))

在新的 Node v12 之后的版本中,可以通过 package.json 中添加 type 字段为 module,将默认模块系统修改为 ES Module,此时就不需要修改文件扩展名为 .mjs 了。

{
  "type": "module"
}

如果需要在 type=module 的情况下继续使用 CommonJS,需要将文件扩展名修改为 .cjs。

对于早期的 Node.js 版本,可以使用 Babel 实现 ES Module 的兼容。babel工作原理图示:

{
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/node": "^7.6.1",
    "@babel/plugin-transform-modules-commonjs": "^7.6.0"
  }
}

babel的核心模块并不会去转换代码,具体的转换需要我们去配置插件,它是基于插件机制的,每个特性的转换都有对应的插件。而presetenv是一个插件的集合,包含了最新JS标准的所有新特性:

// .babelrc

{
  "presets": ["@babel/preset-env"],
  // 具体将其它模块标准转换为commonjs的插件
  "plugins": [
    "@babel/plugin-transform-modules-commonjs"
  ]
}

与CommonJS交互

在nodejs环境中:

  1. 在ES Module 文件中可以导入 CommonJS 模块,因为CommonJS 模块始终只会导出一个默认成员,所以不能通过import语法去直接提取成员。
  2. 不能在 CommonJS 模块中通过 require 载入 ES Module,不支持ES Module模块。
  3. 注意 import 不是解构导出对象。

与CommonJS差异

在 ESM 中没有模块全局成员,例如这些变量和方法都无法使用了:

// 加载模块函数
console.log(require)

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

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

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

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

解决方法如下:

// require, module, exports 自然是通过 import 和 export 代替

// __filename 和 __dirname 通过 import 对象的 meta 属性获取
// const currentUrl = import.meta.url
// console.log(currentUrl)

// 通过 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源码中,它也是将模块代码进行了简单的字符串包装: