前端模块化

248 阅读7分钟

模块化的作用主要是用来抽离公共代码;隔离作用域,处理全局污染,避免变量冲突;处理依赖管理混乱等。

模块模式

将代码拆分成独立的块,然后再把这些块连接起来可以通过模块模式来实现。背后的思想是把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。

  • 模块标识符是所有模块系统通用的概念。模块系统本质上是键/值实体,每个模块都有个可用于引用它的标识符。将模块标识符解析为实际模块的过程要根据模块系统对标识符的实现。
  • 模块系统的核心是管理依赖。指定依赖的模块与周围的环境会达成一种契约。本地模块向模块系统声明一组外部模块(依赖)。模块系统检视这些依赖,进而保证这些外部模块能够被加载并在本地模块运行时初始化所有依赖。每个模块都会与某个唯一的标识符关联,该标识符可用于检索模块。
  • 模块加载。加载模块的概念派生自依赖契约。本地模块期望在执行被指定为外部模块的依赖时,依赖已准备好并已初始化。如果浏览器没有收到依赖模块的代码,则必须发送请求并等待网络返回。收到模块代码之后,浏览器必须确定刚收到的模块是否也有依赖。然后递归地评估并加载所有依赖,直到所有依赖模块都加载完成。只有整个依赖图都加载完成,才可以执行入口模块。
  • 入口。相互依赖的模块必须指定一个模块作为入口(entrypoint),这也是代码执行的起点。模块加载是“阻塞的”,前置操作必须完成才能执行后续操作。每个模块在自己的代码到达浏览器之后完成加载,此时其依赖已经加载并初始化。
  • 异步依赖。js可以异步执行,所以可以让js通知模块系统在必要时加载新模块,并在模块加载完成后提供回调。
  • 动态依赖。动态依赖可以支持更复杂的依赖关系,但代价是增加了对模块进行静态分析的难度。
  • 静态分析。模块中包含的发送到浏览器的JavaScript代码经常会被静态分析,分析工具会检查代码结构并在不实际执行代码的情况下推断其行为。
  • 循环依赖 。包括CommonJS、AMD和ES6在内的所有模块系统都支持循环依赖。只要恰当地封装模块,使它们没有副作用,加载顺序就应该不会影响应用程序的运行。

模块的特性

  • 为创建一个内部作用域调用一个包装函数。包装函数的返回值至少包含一个对内部函数的引用,创建涵盖整个包装函数内部作用域的闭包。

IIFE(Immediately Invoked Function Expression)

使用自执行函数来编写模块化。
特点:在一个单独的函数作用域中执行代码,避免变量冲突。

AMD(Asynchronous Module Definition)

  • 代表:浏览器端requireJS
  • 特点:依赖必须提前声明好;模块加载异步指定回调函数。
  • 官网: RequireJS

语法

//引入
require(['module1', 'module2'], function(m1, m2){
使用m1/m2
})
// 定义和暴露模块
 //无依赖
 define(function(){
    return 模块
  }
  //有依赖
  define(['module1', 'module2'], function(m1, m2){
     return 模块
})

AMD规范是非同步加载模块,允许指定回调函数。AMD推荐的风格通过返回一个对象作为模块对象。

Commonjs

  • 代表:服务端nodejs(Node.js使用了轻微修改版本的CommonJS);浏览器端:webpack、browserfy
  • 机制:在node中引入模块需要进行路径分析、文件定位、编译执行。 Commonjs是服务端模块的规范,Node.js采用了这个规范。CommonJS的风格通过对module。exports或exports的属性赋值来达到暴露模块对象的目的。CommonJS 一般用在服务端或者Node用来同步加载模块,它对于模块的依赖发生在代码运行阶段(只有加载完成,才能执行后面的操作),不适合在浏览器端做异步加载。

语法

var math=require('math')

image.png

语法

exports.add = function add () {/* 方法 */}
 //或
 module.exports.add = function add () {/* 方法 */}

CMD

  • 代表:浏览器端seaJS
  • 特点:支持动态引入依赖文件。
  • 官网:Sea.js - A Module Loader for the Web (seajs.github.io) CMD整合了common.js 和 amd的特点,模块使用时再声明,模块的加载是异步的,模块使用时才会加载执行。在 CMD 规范中,一个模块就是一个文件。

语法

define 是一个全局函数,用来定义模块。define 接受 factory 参数,factory 可以是一个函数,也可以是一个对象或字符串。 factory 为对象、字符串时,表示模块的接口就是该对象、字符串。factory 为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory 方法在执行时,默认会传入三个参数:requireexports 和 module

//同步
var fs = require('fs');
//异步
require.async('./module3', function (m3) {
})


//定义和暴露模块


无依赖

define(function(require, exports, module){
exports.xxx = value
module.exports = value
})

有依赖

define(function(require, exports, module){\
//引入依赖模块(同步)\
var module2 = require('./module2')\
//引入依赖模块(异步)\
require.async('./module3', function (m3) {\
})\
//暴露模块\
exports.xxx = value\
})

通用模块定义(UMD, Universal Module Definition)

为了统一CommonJS和AMD生态系统,规范应运而生。UMD可用于创建这两个系统都可以使用的模块代码。本质上,UMD定义的模块会在启动时检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式(IIFE)中。

ES Modules

import会在JavaScript引擎静态分析、编译时就引入模块代码,因此也不适合异步加载。

语法

<script type="module" src="./module.js"></script>

优势

  • 死代码检测和排除。可以用静态分析工具检测出哪些模块没有被调用过。通过静态分析可以在打包时去掉未曾使用过的模块,以减小打包资源体积。
  • 模块变量类型检查。JavaScript属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。
  • 编译器优化。在CommonJS等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module支持直接导入变量,减少了引用层级,程序效率更高。

CommonJS和ESModule的区别

CommonJS模块引用后是一个值的拷贝,而ESModule引用后是一个值的引用(动态映射,并且这个映射是只读的。)。

  • CommonJS 模块输出的是值的拷贝,一旦输出之后,无论模块内部怎么变化,都无法影响之前的引用。
  • ESModule 是引擎会在遇到import后生成一个引用链接,在脚本真正执行时才会根据这个引用链接去模块里面取值,模块内部的原始值变了import加载的模块也会变。

CommonJS运行时加载,ESModule编译阶段引用。

  • CommonJS在引入时是加载整个模块,生成一个对象,然后再从这个生成的对象上读取方法和属性。
  • ESModule 通过export暴露出要输出的代码块,在import时使用静态命令的方法引用指定的输出代码块,并在import语句处执行这个要输出的代码,而不是直接加载整个模块。

参考资料