前端模块化

1,848 阅读3分钟

前端模块化

为什么?

  • 现代Web网站正不断向APP演进
  • 代码复杂性不断增加
  • 项目越来越大
  • 代码部署需要通过分包不断优化
  • 代码重用需求
  • ...

目前Web端主流的模块化标准

  • CommonJS
  • AMD
  • CMD
  • UMD
  • ES6

CommonJS

背景

CommonJS的制定者最初的计划是做出一份针对于Javascript语言而不依赖于任何浏览器环境的模块化标准,然后通过标准来倒逼浏览器的制造者去修改浏览器实现以支持该标准,但是制定者忽略了浏览器无法改变的一些因素,如网络因素以及单线程阻塞的问题,而且浏览器没有办法提供CommonJS所需要的四个变量module.exports、exports、global和require,所以目前CommonJS主要用在Nodejs中。

使用

  • 每个文件就是一个模块,每个模块都有独立的作用域,内部所有变量、函数对其它模块都是不可见的
  • 可以通过module.exports或者exports对模块进行导出, module代表当前模块
  • 可以在另一个文件中通过require进行导入
  • 每个模块可以多次加载但是只会在第一次加载时运行,然后会被缓存供后续加载时使用
  • 按照代码出现顺序同步加载
导出
module.exports.TestModule = function() {
    console.log('exports');
}

或者

exports.TestModule = function() {
    console.log('exports');
}

上面两种方式结果是一样的,至于module.exports和exports的区别,可以简单理解为exports是module.exports的引用,如果在exports调用之前调用了exports=...,那么就无法再通过exports来导出模块内容,除非通过exports=module.exports重新设置exports的指向。

导入
const mymodule = require('./MyModule'); //如果没有后缀,会自动按照.js、.json和.node的次序进行补齐查找
加载过程
  • 优先从缓存中加载
  • 如果缓存中没有,检查是否是核心模块,如果是直接加载
  • 如果不是核心模块,检查是否是文件模块,解析路径,根据解析出的路径定位文件,然后执行并加载
  • 如果以上都不是,沿当前路径向上逐级递归,直到根目录的node_modules目录

循环依赖处理

CommonJS每个文件都是一个模块,第一次加载的时候会去执行该模块,当遇到循环依赖时,加载的时候会只加载已经执行并导出的部分。参考

AMD(Asynchronous Module Definition)/RequireJS

背景

AMD的初衷是为了解决浏览器端目前存在的以下问题:

  • 大量出现的<script>标签以及全局作用域污染
  • 需要手工控制加载顺序
  • CommonJS存在的一些问题,如一个文件一个模块、浏览器不友好等等

使用

基本语法
define(id?, dependencies?, factory);

id用来标识一个模块,是可选的参数,如果不选的话会在引用的时候自动生成,dependencies是一个数组,也是可选的,可以用路径也可以用id,至于factory,其实是一个回调函数,会在所有依赖都加载完成后自动执行。

如果依赖的是“require”、“exports”或者“module”的话,则会根据CommonJS文档解析为自由变量,如果没有任何依赖,则默认会传入["require", "exports", "module"],但是这也要取决于工厂函数的参数数量。

定义方式
  • 使用require、exports定义一个id为“alpha”的模块
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
   exports.verb = function() {
       return beta.verb();
       //Or:
       return require("beta").verb();
   }
});
  • 匿名模块
define(["alpha"], function (alpha) {
   return {
     verb: function(){
       return alpha.verb() + 2;
     }
   };
});
  • 没有依赖的模块
define({
 add: function(x, y){
   return x + y;
 }
});
  • 通过包裹CommonJS定义的模块
define(function (require, exports, module) {
 var a = require('a'),
     b = require('b');

 exports.action = function () {};
});

这种定义会被AMD加载器通过Function.prototype.toString()(并非所有的浏览器都支持)转换为下面的形式:

define(['require', a', 'b'], function (require) {
 var a = require('a'),
     b = require('b');

 exports.action = function () {};
});

目前在用的项目

  • jQuery1.7
  • Dojo1.7
  • EmbedJS
  • ...

CMD(Common Module Definition)/Seajs

与AMD的依赖前置不同,CMD推崇依赖就近,CMD拥有详细中文参考文档,不再赘述。

UMD模式(Universal Module Definition)

UMD是为了解决同时需要多种包加载器(如AMD、CommonJS)的场景,比如打通前后端,UMD包含两部分:

  • 一个及时执行函数来检查所支持的模块加载器,该函数接受两个参数,第一个参数是全局作用域的引用,第二个参数是我们模块定义函数
  • 模块定义函数,会作为及时执行函数的第二个参数,该函数根据依赖数量接受任意数量的参数。

例子(支持AMD和CommonJS)

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['exports', 'b'], factory);
    } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
        // CommonJS
        factory(exports, require('b'));
    } else {
        // Browser globals
        factory((root.commonJsStrict = {}), root.b);
    }
}(this, function (exports, b) {
    exports.action = function () {};
}));

所以UMD理论上可以支持很多种模块加载器

ES6 module

背景

从语言标准层面实现模块功能,可以取代AMD和CommonJS作为前后端通用的解决方案。

设计思想

CommonJS和AMD都是在运行时确定依赖关系,即运行时加载,CommonJS加载的是拷贝,而ES6 module则是在编译时就确定依赖关系,所有加载的其实都是引用,这样做的好处是可以执行静态分析和类型检查。

导出

方式一
export var first = 'test';
export function func() {
    return true;
}
方式二
var first = 'test';
var second = 'test';
function func() {
    return true;
}
export {first, second, func};
as关键字
var first = 'test';
export {first as second};

as关键字可以重命名暴露出的变量或方法,经过重命名后同一变量可以多次暴露出去。

export default

export default会导出默认输出,即用户不需要知道模块中输出的名字,在导入的时候为其指定任意名字。

// 导出
export default function () {
  console.log('foo');
}

// 导入
import customName from './export-default';

// 错误
export default var a = 1; //export default导出的是一个叫default的变量,所以后面不能跟声明语句

导入默认模块时不需要大括号,导出默认的变量或方法可以有名字,但是对外无效。export default只能使用一次。

export要求暴露出的必须是接口,接口与模块内部变量一一对应,而不能直接输出值,这个可以结合我们前面说的所有导入都是引用来理解。

//报错
export 1;

// 报错
var m = 1;
export m;

// 报错
function f() {}
export f;

// 正确
export var m = 1;

// 正确
var m = 1;
export {m};

// 正确
var n = 1;
export {n as m};

// 正确
export function f() {};

// 正确
function f() {}
export {f};

导入

正常导入
import {firstName, lastName, year} from './profile';

导入模块位置可以是相对路径也可以是绝对路径,.js可以省略,如果不带路径只是模块名,则需要通过配置文件告诉引擎查找的位置。

as关键字
import { lastName as surname } from './profile';

import 命令会被提升到模块头部,所以写的位置不是那么重要,但是不能使用表达式和变量来进行导入。

加载整个模块(无输出)
import 'lodash'; //仅仅是加载而已,无法使用
加载整个模块(有输出)
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

import * 会忽略default输出

导入导出复合用法

先导入后导出
export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, boo};
整体先导入再输出以及default
// 整体输出
export * from 'my_module';

// 导出default,正如前面所说,export default 其实导出的是default变量
export { default } from 'foo';

// 具名接口改default
export { es6 as default } from './someModule';
模块的继承
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}

export * 会忽略default。

要点

  • ES6模块默认是严格模式
  • 一个文件一个模块
  • es6的import可以在模块中的任何位置,但是一定要在顶层,如果在块级作用域会报错
  • export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系
  • export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时值

浏览器使用ES6模块

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

对于type="module"的<script>,浏览器会执行异步加载,不会阻塞浏览器。

小结

标准/特性适用场景同步/异步模块依赖关系确定时间
CommonJS服务端同步运行时
AMD浏览器端异步运行时
CMD浏览器端同步或异步运行时
UMD服务端/浏览器端同步或异步运行时
ES6 Module服务端/浏览器端同步或异步编译时