前端模块化发展

模块化就是将一个复杂的系统分解成多个独立的模块的代码组织方式。

前端模块化发展之路: IIFE(自执行函数)>>AMD(RequireJS实现)>>CMD(SeaJS实现)>>CommonJS(NodeJs)>>ES6 Modules(模块化直接成为了Javascript语言规范中的一部分)。

一、前端模块化发展简介

1.CommonJS(require / module.exports / exports)

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志”Javascript模块化编程”正式诞生。nodeJs中的模块,一律为CommonJS 格式。

1.1 语法风格

1
2
3
4
5
6
//Math.js
module.exports = {
'add': function(a, b) {
return a + b;
}
}
1
2
3
4
//main.js
const Math = require('./Math');
console.log(Math.add(2, 3));
console.log('done');

front-module


1.2 同步加载

1.3 动态加载

1
2
3
4
//main.js
const Math = require('./Ma' + 'th');//动态拼接
console.log(Math.add(2, 3));
console.log('done');

1.4 浏览器不支持CommonJS规范。

浏览器不兼容CommonJS的根本原因,在于缺少四个Node.js环境的变量。

  • module
  • exports
  • require
  • global

可以使用工具进行转换,例如:Browserify


2.AMD(require/define)

CommonJS是主要为了JS在后端的表现制定的,它是不适合前端的。

AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。

RequireJS实现了AMD规范。下面以RequireJS为例,了解一下AMD规范。

requireJS原理

require.js 的核心原理是通过动态创建 script 脚本来异步引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。

2.1 语法风格

1
2
3
4
5
6
7
8
//Math.js
define([], function(){
return {
'add': function(a, b) {
return a + b;
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
//main.js
require.config({
paths : {
"math" : "Math"
}
});
require(['math'], function (math) {
console.log(math.add(2, 3));
});
console.log('done');
//done
//5

2.2 异步加载

2.3 动态加载

2.4 依赖前置,提前执行


3.CMD

CMD是SeaJS 在推广过程中对模块定义的规范化产出。

3.1 语法风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// CMD
define(function(require, exports, module) {
var a = require('./a');
a.doSomething();
//...
var b = require('./b'); // 依赖可以就近书写
b.doSomething();
// ...
require.async('./c',function(c){ //支持异步加载
c.doSomething();
});
})

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething();
//...
b.doSomething();
//...
})

3.2 AMD和CMD的区别

1) 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible(尽可能的懒加载,也称为延迟加载,即在需要的时候才加载)。

2) CMD 推崇依赖就近,AMD 推崇依赖前置。虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。

3.3 推荐链接

与 RequireJS 的异同

SeaJS官方文档

es6(import/export)

require与import的区别

  • require支持 动态导入,import不支持,正在提案 (babel 下可支持)
  • require是 同步 导入,import属于 异步 导入
  • require是 值拷贝,导出值变化不会影响导入值;import指向内存地址,导入值会随导出值而变化

二、Module

ES6中Module的特点

  • 浏览器,服务器通用
  • 静态加载

1. 基本语法

1.1 export

一个模块就是一个独立的文件。export关键字用来输出该变量。可以输出变量,函数或类。

1
2
3
4
// test.js
export var firstName = 'cheng';
export var lastName = 'zhang';
export var age = 18;
1
2
3
4
5
6
// test.js
var firstName = 'cheng';
var lastName = 'zhang';
var age = 18;

export { firstName, lastName, age };

可以使用as为输出变量重命名。

1
2
3
4
5
6
7
8
9
var firstName = 'cheng';
var lastName = 'zhang';
var age = 18;

export {
firstName as name,
lastName as lastname,
age
};

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

1
2
3
4
5
6
// 报错
export 1;

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

export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

1
2
export var foo = 'bar';
setTimeout(() => foo = 'baz', 1000);

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。

1.2 import

其他 JS 文件通过import命令加载模块。大括号里面的变量名,必须与被导入模块(test.js)对外接口的名称相同。

1
2
3
4
5
6
7
// main.js
import { firstName, lastName, age } from './test.js';

function showName() {
console.log(firstName + ' ' + lastName);
}
//cheng zhang

import命令输入的变量都是只读的,因为它的本质是输入接口。

1
2
3
import {a} from './xxx.js'

a = {}; // Assignment to constant variable.

如果a是一个对象,改写a的属性是允许的。和const一样。

1
2
3
import {a} from './xxx.js'

a.foo = 'hello'; // 合法操作

可以用星号(*)指定一个对象,进行整体加载。

1
2
3
4
5
6
7
8
9
10
11
// main.js
import * as test from './test.js';

function showName() {
console.log(test.firstName + ' ' + test.lastName);
}
//cheng zhang

test.lastName = 'yun';
//Cannot assign to read only property 'lastName' of object '[object Module]'
//如果是对象,可以修改对象的属性。

import命令具有提升效果,会提升到整个模块的头部,首先执行。

1
2
3
foo();

import { foo } from 'my_module';

由于import是静态执行,所以不能使用表达式和变量

1
2
// 报错
import { 'f' + 'oo' } from 'my_module';

import语句会执行所加载的模块,因此可以有下面的写法。仅仅执行lodash模块,但是不输入任何值。

1
import 'lodash';

即使加载多次,也只会执行一次。也就是说,import语句是 Singleton 模式

1.3 单例模式解读

1
2
3
4
5
6
//counter.js
export let counter = 1;
export function addCounter(){
counter++;
}
addCounter();
1
2
3
4
5
6
7
8
//main.js
import {counter, addCounter} from './counter';
console.log('main:' + counter);
addCounter();
console.log('main:' + counter);

//main:2
//main:3
1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html>
<head>
<title>module test</title>
<script type="module" src='main.js'></script>
<script type="module" src='main2.js'></script>
</head>
</html>
1
2
3
4
5
6
7
8
//main2.js
import {counter, addCounter} from './counter';
console.log('main2:' + counter);
addCounter();
console.log('main2:' + counter);

//main2:3
//main2:4

1.4 export default

使用export default可以不用关注输出模块中的变量名。

1
2
3
4
// export-default.js
export default function () {
console.log('foo');
}
1
2
3
// import-default.js
import customName from './export-default';
customName(); // 'foo'

import命令可以为该匿名函数指定任意名字。import命令后面,不使用大括号。

1
2
3
4
5
6
// export-default.js
function foo() {
console.log('foo');
}

export default foo;

export default的本质,就是输出一个叫做default的变量或方法。

imort something from ...的本质,就是import {default as something} from ...

1
2
3
4
5
6
7
8
9
// 正确
export var a = 1;

// 正确
var a = 1;
export default a;

// 错误
export default var a = 1;
1
2
3
4
5
// 正确
export default 42;

// 报错
export 42;

所以export default是比较常用的方法:

1
2
3
4
5
6
// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();

1.5 import&export混合使用

1
2
3
4
5
export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

1
2
3
4
5
6
7
8
// 接口改名
export { fooName as newName } from 'my_module';

//具名接口改为默认接口
export { foo as default } from './someModule';

//接口也可以改名为具名接口
export { default as es6 } from './someModule';

1.6 模块的继承

1
2
3
4
//calculator.js
export function add(a, b) {
return a + b;
}
1
2
3
4
5
//calculatorPlus.js
export * from './calculator.js';
export function multiply(a, b) {
return a * b;
}
1
2
3
4
5
//main.js
import * as cal from './calculatorPlus.js';

cal.add(2, 3);//5
cal.multiply(2, 3);//6

1.7 import()

import()提案是为了解决import动态加载,和不能写在代码块中的问题。

1
2
3
4
5
import(a + '.js')
.then(...);

import(f())
.then(...);
1
2
3
4
5
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}

2. Module补充

2.1 浏览器加载

html中加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。

1
2
3
4
5
<script type="module" src="./myModule.js"></script>
<!--等同于-->
<script type="module" src="./myModule.js" defer></script>

<script type="module" src="./myModule.js" async></script>
  • defer:要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行。保证执行顺序。

  • async:一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。不能保证执行顺序。

对于外部的模块脚本,要注意:

  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的
  • 同一个模块如果加载多次,将只执行一次。

2.2 循环加载

1
2
3
4
5
// a.js
import {bar} from './b';
console.log('a.js');
console.log(bar);
export let foo = 'foo';
1
2
3
4
5
// b.js
import {foo} from './a';
console.log('b.js');
console.log(foo);
export let bar = 'bar';
1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html>
<head>
<title>module test</title>
<script type="module" src='a.js'></script>
</head>
</html>
<!--Cannot access 'foo' before initialization-->

1
2
3
4
5
6
// a.js
import {bar} from './b';
console.log('a.js');
console.log(bar());
function foo() { return 'foo' }
export {foo};
1
2
3
4
5
6
// b.js
import {foo} from './a';
console.log('b.js');
console.log(foo());
function bar() { return 'bar' }
export {bar};
1
2
3
4
//b.mjs
//foo
//a.mjs
//bar

因为函数具有提升作用。

2.3 ES6模块和CommonJS模块的差异

  1. importexport是关键字,require不是。

  2. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。(详情见上方【单例模式解读】)

  3. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

       因为 CommonJS 加载的是一个对象(即`module.exports`属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //counter.js
    let counter = 2;
    function addCounter(){
    counter++;
    }
    addCounter();
    module.exports = {
    counter,
    addCounter: addCunter
    }
    1
    2
    3
    4
    5
    6
    //main.js
    var counter = require('./addCounter.js');

    console.log('main:' + counter.counter);//3
    counter.addCounter();
    console.log('main:' + counter.counter);//3
  4. this指向不同。ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异。
本文结束感谢您的阅读

本文标题:前端模块化发展

文章作者:陈宇(cosyer)

发布时间:2019年06月13日 - 23:06

最后更新:2020年07月30日 - 22:07

原始链接:http://mydearest.cn/2019/%E5%89%8D%E7%AB%AF%E6%A8%A1%E5%9D%97%E5%8C%96%E5%8F%91%E5%B1%95.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!