前端模块化

模块化的演进过程

早起模块化完全依靠约定,项目一旦庞大将会维护成本极高

1.基于文件的划分模块的问题

  • 污染全局作用域
  • 命名冲突问题
  • 无法管理模块依赖关系

2.命名空间方式

3.立即执行函数的形式提供私有空间

模块化规范的出现

目前实现模块化的规范主要有:

  • CommonJS
  • CMD
  • AMD
  • ES6模块(ESmodules)

模块化的好处:

  1. 可维护性。 因为模块是独立的,一个设计良好的模块会让外面的代码对自己的依赖越少越好,这样自己就可以独立去更新和改进。

  2. 命名空间。 在 JavaScript 里面,如果一个变量在最顶级的函数之外声明,它就直接变成全局可用。因此,常常不小心出现命名冲突的情况。使用模块化开发来封装变量,可以避免污染全局环境。

  3. 重用代码。 我们有时候会喜欢从之前写过的项目中拷贝代码到新的项目,这没有问题,但是更好的方法是,通过模块引用的方式,来避免重复的代码库。我们可以在更新了模块之后,让引用了该模块的所有项目都同步更新,还能指定版本号,避免 API 变更带来的麻烦。

CommonJS规范

commonjs是以同步模式加载模块

  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 通过module.exports导出成员
  • 通过require函数载入模块

AMD规范

异步模式加载模块,一种前端模块化的临时方案

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

CMD规范

CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同

模块化标准规范

模块的化的最佳实践 ESmodules 和 CommonJS

ESmodule是前端最规范的模块化标准,但需要配合构建工具使用(ES6才推出的新标准)

ESmodules特性

  • 自动采用严格模式、忽略 'use strict'
  • 每个ESM模块都是单独的私有作用域
  • ESM是通过CORS去请求外部JS模块
  • ESM的SCRIPT标签会延迟执行脚本

ESmodules导入导出的注意事项

  • 导出的成员并不是字面量对象,只是写法类似
  • 导入的语法也类似解构语法,但是并不是解构的意思,只是类似。
  • ESMODULES导出的过程中,并不是导出成员,而是把成员的值给你,你并不能修改esmodule内的导出值
  • 导入的成员是只读的成员

ES Modules导入用法

import用法

// import { name } from './module'
// import { name } from './module.js'
// console.log(name)

// import { lowercase } from './utils'
// import { lowercase } from './utils/index.js'
// console.log(lowercase('HHH'))

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

// --------------

// import {} from './module.js'
// import './module.js'

// ---------------

// import * as mod from './module.js'
// mod为导出全部 并定位 mod
// console.log(mod)

// ---------------

// var modulePath = './module.js'
// import { name } from modulePath
// console.log(name)

// if (true) {
//   import { name } from './module.js'
// }

// import('./module.js').then(function (module) {
//   console.log(module)
// })

// ----------------

// import { name, age, default as title } from './module.js'
//abc为 export default默认导出
// {name age }为 export
import abc, { name, age } from './module.js'
console.log(name, age, abc)

ES modules in Browser

Polyfill兼容方案 浏览器运行环境给我们带来兼容问题

cdn形式兼容方案

借助 <script nomodule></scirpt>来判断不识别es-module的判断

https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/ 生产环境不建议使用,运算比较大

ES Module in Node.js 支持情况

// 第一,将文件的扩展名由 .js 改为 .mjs;
// 第二,启动时需要额外添加 `--experimental-modules` 参数;

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')

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

ES modules in Node.js 与 Commonjs交互

  • ES modules 中可以导入Commonjs模块
  • CommonJS中不能导入 ES Modules模块
  • CommonJS始终只会导出一个默认成员
  • 注意import不是解构导出对象

ES Mdules in node.js 与 Commonjs的差异

它们有两个重大差异:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

下面重点解释第一个差异,我们还是举上面那个CommonJS模块的加载机制例子:

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

ES Module node.js -Babel 兼容方案

对于早期的 Node.js 版本,可以使用 Babel 实现 ES Module 的兼容

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

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