WEB 前端模块化都有什么?

栏目: JavaScript · 发布时间: 7年前

内容简介:说到前端模块化,你第一时间能想到的是什么?Webpack?ES6 Module?还有吗?我们一起来看一下下图。相信大伙儿对上图的单词都不陌生,可能用过、看过或者是只是听过。那你能不能用一张图梳理清楚上述所有词汇之间的关系呢?我们日常编写代码的时候,又和他们之间的谁谁谁有关系呢?为了更贴合我们的日常开发场景(前后端分离),我们尝试先从不同平台的维度区分,作为本文的切入点。

说到前端模块化,你第一时间能想到的是什么?Webpack?ES6 Module?还有吗?我们一起来看一下下图。

WEB 前端模块化都有什么?

相信大伙儿对上图的单词都不陌生,可能用过、看过或者是只是听过。那你能不能用一张图梳理清楚上述所有词汇之间的关系呢?我们日常编写代码的时候,又和他们之间的谁谁谁有关系呢?

一、千丝万缕

为了更贴合我们的日常开发场景(前后端分离),我们尝试先从不同平台的维度区分,作为本文的切入点。

1. 根据平台划分

平台 规范 特性
浏览器 AMD、CMD 存在网络瓶颈,使用异步加载
非浏览器 CommonJS 直接操作 IO,同步加载

可以看到我们非常暴力的以是不是浏览器作为划分标准。仔细分析一下,他们之间最大的差异在于其特性上,是否存在瓶颈。 例如说网络性能瓶颈,每个模块的请求都需要发起一次网络请求,并等待资源下载完成后再进行下一步操作,那整个用户体验是非常糟糕的。 根据该场景,我们简化一下,以同步加载和异步加载两个维度进行区分。

特性 规范
同步加载 CommonJS
异步加载 AMD、CMD

2. AMD、CMD 两大规范

先忽略 CommonJS,我们先介绍下,曾经一度盛行的 AMD、CMD 两大规范。

规范 约束条件 代表作
AMD 依赖前置 requirejs
CMD 就近依赖 seajs

AMD、CMD 提供了封装模块的方法,实现语法上相近,甚至于 requirejs 在后期也默默支持了 CMD 的写法。我们用一个例子,来讲清楚这两个规范之间最大的差异:依赖前置和就近依赖。

AMD:

// hello.js
define(function() {
    console.log('hello init');
    return {
        getMessage: function() {
            return 'hello';
        }
    };
});
// world.js
define(function() {
    console.log('world init');
});

// main
define(['./hello.js', './world.js'], function(hello) {
    return {
        sayHello: function() {
            console.log(hello.getMessage());
        }
    };
});

// 输出
// hello init
// world init
复制代码

CMD:

// hello.js
define(function(require, exports) {
    console.log('hello init');
    exports.getMessage = function() {
        return 'hello';
    };
});

// world.js
define(function(require, exports) {
    console.log('world init');
    exports.getMessage = function() {
        return 'world';
    };
});

// main
define(function(require) {
    var message;
    if (true) {
        message = require('./hello').getMessage();
    } else {
        message = require('./world').getMessage();
    }
});

// 输出
// hello init
复制代码

结论: CMD 的输出结果中,没有打印"world init"。但是, 需要注意的是,CMD 没有打印"world init"并是不 world.js 文件没有加载。AMD 与 CMD 都是在页面初始化时加载完成所有模块,唯一的区别就是就近依赖是当模块被 require 时才会触发执行。

requirejs 和 seajs 的具体实现在这里就不展开阐述了,有兴趣的同学可以到官网了解一波,毕竟现在使用 requirejs 和 seajs 的应该很少了吧。

3. CommonJS

回到 CommonJS,写过 NodeJS 的同学对它肯定不会陌生。CommonJS 定义了,一个文件就是一个模块。在 node.js 的实现中,也给每个文件赋予了一个 module 对象,这个对象包括了描述当前模块的所有信息,我们尝试打印 module 对象。

// index.js
console.log(module);

// 输出
{
    id: '/Users/x/Documents/code/demo/index.js',
    exports: {},
    parent: { module }, // 调用该模块的模块,可以根据该属性查找调用链
    filename: '/Users/x/Documents/code/demo/index.js',
    loaded: false,
    children: [...],
    paths: [...]
}
复制代码

也就是说,在 CommonJS 里面,模块是用对象来表示。我们通过“循环加载”的例子进行来加深了解。

// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';

//b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';

//main
console.log('index.js', require('./a.js').x);

// 输出
b.js  a1
a.js  b2
index.js  a2
复制代码

我们的理论依据是模块对象,根据该依据我们进行如下分析。

1、 a.js准备加载,在内存中生成module对象moduleA
2、 a.js执行exports.x = 'a1'; 在moduleA的exports属性中添加x
3、 a.js执行console.log('a.js', require('./b.js').x); 检测到require关键字,开始加载b.js,a.js执行暂停
4、 b.js准备加载,在内存中生成module对象moduleB
5、 b.js执行exports.x = 'b1'; 在moduleB的exports属性中添加x
6、 b.js执行console.log('b.js', require('./a.js').x); 检测到require关键字,开始加载a.js,b.js执行暂停
7、 检测到内存中存在a.js的module对象moduleA,于是可以将第6步看成console.log('b.js', moduleA.x); 在第二步中moduleA.x赋值为a1,于是输出b.js, a1
8、 b.js继续执行,exports.x = 'b2',改写moduleBexports的x属性
9、 b.js执行完成,回到a.js,此时同理可以将第3步看成console.log('a.js', modulerB.x); 输出了a.js, b2
10、 a.js继续执行,改写exports.x = 'a2'
11、 输出index.js a2
复制代码

至此,“CommonJS 的模块,是一个对象。”这个概念大伙儿应该能理解吧?

回到这个例子,例子里面还出现了一个保留字 exports。其实 exports 是指向 module.exports 的一个引用。举个例子可以说明他们两个之间的关系。

const myFuns = { a: 1 };
let moduleExports = myFuns;
let myExports = moduleExports;

// moduleExports 重新指向
moduleExports = { b: 2 };
console.log(myExports);
// 输出 {a : 1}

// 也就是说在module.exports被重新复制时,exports与它的关系就gg了。解决方法就是重新指向
myExports = modulerExports;
console.log(myExports);
// 输出 { b: 2 }
复制代码

4. ES6 module

对 ES6 有所了解的同志们应该都清楚,web 前端模块化在 ES6 之前,并不是语言规范,不像是其他语言 javaphp 等存在命名空间或者包的概念。上文提及的 AMD、CMD、CommonJS 规范,都是为了基于规范实现的模块化,并非 JavaScript 语法上的支持。 我们先简单的看一个 ES6 模块化写法的例子:

// a.js
export const a = 1;

// b.js
export const b = 2;

// main
import { a } from './a.js';
import { b } from './b.js';
console.log(a, b);
//输出 1 2
复制代码

emmmm,没错,export 保留字看起来是不是和 CommonJS 的 exports 有点像?我们尝试 下从保留字对比 ES6 和 CommonJS。

保留字 CommonJS ES6
require 支持 支持
export / import 不支持 支持
exports / module.exports 支持 不支持

好吧,除了 require 两个都可以用之外,其他实际上还是有明显差别的。那么问题来了,既然 require 两个都可以用,那这两个在 require 使用上,有差异吗?

我们先对比下 ES6 module 和 CommonJS 之间的差异。

模块输出 加载方式
CommonJS 值拷贝 对象
ES6 引用(符号链接) 静态解析

又多了几个新颖的词汇,我们先通过例子来介绍一下值拷贝和引用的区别。

// 值拷贝 vs 引用

// CommonJS
let a = 1;
exports.a = a;
exports.add = () => {
    a++;
};

const { add, a } = require('./a.js');
add();
console.log(a); // 1

// ES6
export const a = 1;
export const add = () => {
    a++;
};

import { a, add } from './a.js';
add();
console.log(a); // 2
// 显而易见CommonJS和ES6之间,值拷贝和引用的区别吧。
复制代码

静态解析 ,什么是的静态解析呢?区别于 CommonJS 的模块实现,ES6 的模块并不是一个对象,而只是代码集合。也就是说,ES6 不需要和 CommonJS 一样,需要把整个文件加载进去,形成一个对象之后,才能知道自己有什么,而是在编写代码的过程中,代码是什么,它就是什么。

PS:

  1. 目前各个浏览器、node.js 端对 ES6 的模块化支持实际上并不友好,更多实践同志们有兴趣可以自己搞一波。
  2. 在 ES6 中使用 require 字样,静态解析的能力将会丢失!

5. UMD

模块化规范中还有一个 UMD 也不得不提及一下。什么是 UMD 呢?

UMD = AMD + CommonJS
复制代码

没错,UMD 就是这么简单。常用的场景就是当你封装的模块需要适配不同平台(浏览器、node.js),例如你写了一个基于 Date 对象二次封装的,对于时间的处理 工具 类,你想推广给负责前端页面开发的 A 同学和后台 Node.js 开发的 B 同学使用,你是不是就需要考虑你封装的模块,既能适配 Node.js 的 CommonJS 协议,也能适配前端同学使用的 AMD 协议?

二、工具时代

1. webpack

webpack 兴起之后,什么 AMD、CMD、CommonJS、UMD,似乎都变得不重要了。因为 webpack 的模块化能力真的强。

webpack 在定义模块上,可以支持 CommonJS、AMD 和 ES6 的模块声明方式,换句话说,就是你的模块如果是使用 CommonJS、AMD 或 ES6 的语法写的,webpack 都支持!我们看下例子:

//say-amd.js
define(function() {
    'use strict';
    return {
        sayHello: () => {
            console.log('say hello by AMD');
        }
    };
});
//say-commonjs.js
exports.sayHello = () => {
    console.log('say hello by commonjs');
};
//say-es6.js
export const sayHello = () => {
    console.log('say hello in es6');
};

//main
import { sayHello as sayInAMD } from './say-amd';
import { sayHello as sayInCommonJS } from './say-commonjs';
import { sayHello as sayInES6 } from './say-es6';

sayInAMD();
sayInCommonJS();
sayInES6();
复制代码

不仅如此,webpack 识别了你的模块之后,可以将其打包成 UMD、AMD 等等规范的模块重新输出。例如上文提及到的你需要把 Date 模块封装成 UMD 格式。只需要在 webpack 的 output 中添加 libraryTarget: 'UMD'即可。

2. more...

总结

回到开始我们提出的问题,我们尝试使用一张图汇总上文提及到的一溜模块化相关词汇。

WEB 前端模块化都有什么?
@Author: _Jay

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Math Adventures with Python

Math Adventures with Python

Peter Farrell / No Starch Press / 2018-11-13 / GBP 24.99

Learn math by getting creative with code! Use the Python programming language to transform learning high school-level math topics like algebra, geometry, trigonometry, and calculus! In Math Adventu......一起来看看 《Math Adventures with Python》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具