前端模块化开发规范浅谈

模块化的开发方式使得代码更加灵活,提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数,而不会对全局对象造成污染。 目前流行的模块化主要有 AMD,CMD,CommonJS,ES6 模块这

目录
  1. AMD
  2. CMD
  3. CommonJS
  4. ES6

模块化的开发方式使得代码更加灵活,提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数,而不会对全局对象造成污染。

目前流行的模块化主要有 AMD,CMD,CommonJS,ES6 模块这几种。

AMD#

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

// 模块的定义
// 不依赖其他模块
// math.js
define(function () {
  function add(a, b) {
    return a + b;
  }
  // 通过return提供接口
  return {
    add: add,
  };
});
 
// 依赖其他的模块
// foo.js
define(["math"], function (math) {
  function fn(a, b) {
    return math.add(a, b);
  }
  // 通过return提供接口
  return {
    fn: fn,
  };
});
 
// 模块的引入
// bar.js
require(["foo"], function (foo) {
  function bar(a, b) {
    return foo.fn(a, b);
  }
  console.log(bar(1, 2)); // 3
});
<!--在html中引入,需要导入requireJs库-->
<script
  data-main="./bar"
  src="https://cdn.bootcss.com/require.js/2.3.5/require.js"
></script>

AMD 模块规范主要用于浏览器端,因为需要从服务器请求模块,同步的方法影响用户体验,只能是异步加载,每个模块如果需要依赖前置模块,只有等前置模块加载完成后,才去执行回调函数。但是不影响程序后面语句的执行。

CMD#

CMD 是sea.js在推广过程中对模块定义的规范化中产出

// 模块的定义和引入
// math.js
define(function (require, exports, module) {
  // 通过require引入依赖
  const module = require("module");
  const add = function (a, b) {
    return a + b;
  };
  // 通过exports对外提供接口
  exports.add = add;
 
  // 或者通过module.export提供整个接口
  module.export = {};
});
 
// 模块的引入
// foo.js
define(function (require, exports, module) {
  const add = require("math").add;
  console.log(add(1, 2)); // 3
});

CMD 没有把依赖写在 define 里,而是写在回调函数中,当需要依赖时,才通过 require 去引入。回调函数包含 3 个参数,分别是 require,exports,module。其中 require 是唯一可以引入依赖的,另外 2 个都是对外提供接口的。

AMD 和 CMD 的区别:

  • AMD 推崇依赖前置,提前执行。把依赖的引入放在 define 中,需要先加载完依赖,再执行回调函数。
  • 而 CMD 推崇就近依赖,延迟执行。把依赖的引入放在回调的 factory 函数中,当要用到依赖时才会就近加载。

CommonJS#

CommonJS 是 Node 服务器的模块规范化。

// 模块的定义
// math.js
const add = function (a, b) {
  return a + b;
};
 
const number = 1;
 
module.exports = {
  fn: add,
  num: number,
};
 
// 模块的引入
const math = require("./math");
 
console.log(math.fn(1, 2)); // 3
console.log(math.num); // 1

CommonJS 模块化主要是用于 Node 服务器端或者是 Node 运行环境中。因为在服务器端,依赖都保存在本地硬盘中,所以读取速度非常快,可以同步加载。

在第一次用 require 引入模块时,会加载并执行整个脚本,然后在内存中生成脚本的 exports 对象。后面再调用这个模块都是从这个 exports 对象中去取值,即使后来模块修改了值,也不会更改内存中对象的值,除非手动清除掉缓存。

现在写前端代码一般都需要借助构建工具或者 MVVM 框架,这些工具和框架都是在 Node 环境下运行的,所以 CommonJS 规范被广泛的使用。可以说使用 Node 环境开发,就会用到 CommonJS 模块。

ES6#

ES6 实现了模块功能,有属于自己的模块规范。

// 模块的定义
// 输出变量
export const number = 1;
 
// 输出函数
export function add(a, b) {
  return a + b;
}
 
// 也可以集中输出
const name = "hale";
function foo() {
  console.log("hale");
}
export { name, foo };
 
// 默认输出
export default {
  add(a, b) {
    return a + b;
  },
};
// 模块的引入
 
// 引入fs库(第三方库)
import fs from "fs";
 
// 引入模块的默认输出
import Add from "./add.js";
 
// 引用模块输出的某个函数和变量
import { add, number } from "./add.js";
 
// 引入整个模块, 用as作为别名
import * as math from "./add.js";
 
Add(1, 2); // 3
add(1, 2); // 3
math.add(1, 2); // 3
math.number; // 1
 
// 也可以直接引入,这样引入就直接加载这个模块
import "index.css";

ES6 模块化功能主要通过import引入和export输出来实现的,另外 ES6 模块是动态取值的,不会缓存运行的结果。

ES6 模块化功能不仅支持客户端,还支持服务器(node 服务器)。

不过在客户端加载需要注意一些事项,比如自动采用严格模式,js 后缀名不能省略,代码是在模块作用域下运行的等等,另外在服务器上上加载比较麻烦,因为 Node 服务器有自己的 CommonJS 模块化规范,和 ES6 的有些不兼容,

目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。

ES6 模块和 CommonJS 模块的差异:

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

    • CommonJS 模块输出的是一个值的拷贝,一旦输出值后,模块的变化将不会影响到这个值。
    • ES6 模块输出的值是值的引用。当脚本运行时,才通过 import 生成一个引用去模块中取值,如果模块的原始值发现变化了,import 取得的值也会发生变化。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

    • CommonJS 模块是加载整个模块,然后生成一个对象 exports,再从这个对象上面读取方法,这种加载称为“运行时加载。
    • ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入。import 可以指定加载某个输出的方法,其他的方法不加载,这种加载称为“编译时加载”或者“静态加载”。