# 模块化

最近几年 Web 应用发展的速度实在是太快了,很多在后端完成工作都慢慢的跑到了前端,渐渐的 Web 应用变得越来越复杂与庞大。单单靠HTML 开发 Web 的方式已经无法满足和适应当前的 Web 应用的发展。陆续便有一些新的思想和框架涌现出来了。下面将从前端模块化三大框架新语言三个角度来讲讲 Webpack 由来的背景

模块化是指将一个复杂的系统分解为多个模块以方便编码。

因为之前写代码,开发网页的时候需要通过命名空间来组织代码,比如 JQuery 会讲它的所有 Api 都挂在 window.$ 下,在加载完 JQuery 之后,其他模块 再通过 window.$ 去使用 JQuery。这样便会出现很多问题:

  • 命名空间产生冲突,两个库可能会使用同一个名称

  • 无法合理的管理项目的依赖和版本

  • 无法方便的控制依赖的加载顺序

当项目变得越来越大的时候,这种方式会变得难以维护,所以在这里需要用模块化的思想来组织代码。目前流行的 前端模块化规范有 CommonJsAMDCMDES6

# CommonJS

CommonJs 是一种被广泛使用的 JavaScript 模块化规范,其核心思想是通过 require 方法来同步加载依赖的其他模块,通过 module.exports 导出需要暴露的接口。它的流行得益于 Node.js 采用了这种方式,后来这种方式就被引入到了 网页开发之中。

采用 CommonJs 导入及导出的代码如下:

// 定义模块 demo.js
var num1 = 0;
function add(a, b) {
  return a + b;
}

// 暴露向外提供的函数或者变量

module.exports = {
  add: add,
  num1: num1,
}

// 导入自定义的模块时,参数包含路径,可省略.js
const moduleA = require('./demo');
moduleA.add(2, 1); // 3

// 在 node 中,引用其核心模块,则不需要带路径
var fs = require('fs');
var data = fs.readFileSync('./demo.js');

特点

commonJS用同步的方式加载模块,在服务端,模块文件都存在本地磁盘,读取很快,但在浏览器端因为网络等原因,最好的方式应该需要进行异步加载;因为是同步加载,所以只有加载完成,才能执行后面的操作;在服务器端,模块的加载是运行时直接可以运行的;在浏览器端,模块需要提前编译打包处理。

机制

CommonJS 模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。下面是相关例子:

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}

module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代码输出内部变量 counter和改写这个变量的内部方法 incCounter

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

上面的代码说明,lib.js 模块加载完了之后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}

module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

上面的代码中,输出的 counter 属性是一个取值器函数。现在在执行 main.js,就可以正确的读出 变量 counter 的值了。

$ node main.js
3
4

# AMD

AMD 也是一种 JavaScript 模块化规范,与 CommonJS 最大的不同在于,他采用了异步的方式去加载依赖的模块。它主要解决针对浏览器环境的模块化问题,因为浏览器环境有可能会因为网络的原因,需要从服务器加载数据,那么这里就需要采用非同步的模式。最具代表性的实现是 require.js

采用 AMD 导入及导出的代码如下(使用require.js):

// 定义没有依赖的模块
define(function(){
  return 模块
})

// 定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
  return 模块
})

// 引入使用模块
require(['module1', 'module2'], function(m1, m2){
  // 使用m1、m2
})

特点

可以直接在浏览器中运行,可以异步加载依赖,并可以同时加载多个依赖,避免页面失去响应;定义模块的方法很清晰,不会污染全局环境,能够清晰的显示依赖关系。但是Javascript 运行环境没有原生支持 AMD,需要先导入实现了 AMD 的库后才能正常使用。

# CMD

CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。它整合了 CommonJSAMD规范的特点最具代表性的是 Sea.js

采用 CMD 导入及导出的代码如下:

导入导出代码:

//定义没有依赖的模块
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
})

// 引入使用模块:
define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

# ES6 模块化

ES6 模块化是国际标准化组织 ECMA 提出的 JavaScript 模块化 规范,它在语言层面上实现了模块化。浏览器厂商和 Node.js 都说要原生支持该规范。他将取代 CommonJsAmd 规范,成为浏览器和服务器通用的模块解决方案。

采用ES6 模块化导入及导出的代码如下:

// 导入
import { readFile } from 'fs';
import React from 'react';

// 导出
export function hello() {};
export default {
  // ...
}

缺点:

  • 代码无法直接运行在 大部分的 Javascript 运行环境之下,必须通过一些工具转化为浏览器能运行的 ES5

# ES6 与 CommonJs的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

    CommonJs 在上文中已经进行分析过,ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

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

    因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

针对第一条,还是用介绍CommonJs的代码:

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

这里面便解释了 第一条,es6的模块输出机制,输出的counter是会变得。

# 相关链接

# 示例代码

示例代码可以看这里: