前端模块化

105 阅读5分钟

什么是模块?

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

为什么要使用模块化

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

实现模块化方式

IIFE

立即调用的函数表达式

  • 在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题
(function(globalVariable){
   globalVariable.test = function() {}
   // ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)

AMD

  • 异步加载模块
  • 是 RequireJS 在推广过程中对模块定义的规范化产出
  • 用于浏览器端。
  • 优点
    • 在浏览器环境中异步加载模块;并行加载多个模块;
  • 缺点
    • 开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;
// AMD
define(['./a', './b'], function(a, b) {
  // 加载模块完毕可以使用
  a.do()
  b.do()
})

CMD(目前很少使用)

  • 异步加载模块,模块使用时才会加载执行(玉伯开发,已经没有维护了)
  • 是 SeaJS 在推广过程中对模块定义的规范化产出
  • 用于浏览器端。
  • 优点
    • 依赖就近,延迟执行 可以很容易在 Node.js 中运行;
  • 缺点
    • 依赖 SPM 打包,模块的加载逻辑偏重;
// CMD
define(function(require, exports, module) {
  // 加载模块
  // 可以把 require 写在函数体的任意地方实现延迟加载
  var a = require('./a')
  a.doSomething()
})

CommonJS

  • 同步加载模块
  • 用于服务端(Node.js)。
// a.js
module.exports = {
    a: 1
}
// or 
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

ES Module

  • ES Module 是原生实现的模块化方案
  • ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
// 单个导出
export function init() {}
export const myName = 'hjw'
// 导出一组变量
function init() {}
const myName = ''
export { init, myName };
// 引入
import { init, myName } from './export.js'
// as 关键字重命名
import { myName as newName } from './a.js'

注意

  • 原生浏览器不支持 require/exports,可使用支持 CommonJS 模块规范的 webpack 等打包工具,它们会将 require/exports 转换成能在浏览器使用的代码。
  • import/export 在浏览器中无法直接使用,我们需要在引入模块的

总结

  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块与 CommonJS 模块 差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    • CommonJS 就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
    • 因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
  • CommonJS 模块的require命令是同步加载模块,ES6 模块的import命令是异步加载,有独立的模块依赖的解析阶段。

Node.js 的模块加载方法

概述

  • JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。
  • CommonJS 模块是 Node.js 专用的,从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
  • Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。
  • Node.js 要求 CommonJS 模块采用.cjs后缀文件名。如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。
  • 总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。

package.json 的 main 字段

  • package.json文件有两个字段可以指定模块的入口文件:main和exports。比较简单的模块,可以只使用main字段,指定模块加载的入口文件。
{
  "type": "module",
  "main": "./src/index.js"
}

package.json 的 exports 字段

exports字段的优先级高于main字段。它有多种用法。

  • 子目录别名
  • main 的别名
    • exports字段的别名如果是.,就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段的值。
{
  "exports": {
    ".": "./main.js"
  }
}

// 等同于
{
  "exports": "./main.js"
}
  • 由于exports字段只有支持 ES6 的 Node.js 才认识,所以可以用来兼容旧版本的 Node.js。
{
  "main": "./main-legacy.cjs",
  "exports": {
    ".": "./main-modern.cjs"
  }
}
  • 上面代码中,老版本的 Node.js (不支持 ES6 模块)的入口文件是main-legacy.cjs,新版本的 Node.js 的入口文件是main-modern.cjs。
  • 条件加载

利用.这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。

{
  "type": "module",
  "exports": {
    ".": {
      "require": "./main.cjs",
      "default": "./main.js"
    }
  }
}

拓展-框架输出产物

来源:Vuejs设计与实现

  • 直接在HTML页面中通过script标签引入
    • 需要输出 IIFE 格式的资源
    • 或者输出 UMD 格式的资源(输出内容包含 amd,cjs 和 iife ,现在用的比较少)
      • 兼容性好,但是包体积特别大
      • 现在慢慢用的比较少
  • 通过原生 ESM引入,即通过 script type="module"标签引入
    • 需要输出 esm 格式的资源
  • 在 Node.js 中引入,场景:服务端渲染
    • 需要输出 cjs 格式的资源
const config = {
  input: 'input.js',
  output: {
    file: 'output.js',
    format: 'iife' // 指定模式 or esm/cjs
  }
}

参考资料