web前端 - 模块化(commonjs, es6)

119 阅读4分钟

一、模块化概述

1.1 为什么js代码需要模块化?

<script>
let NAME_1 = 'name1';
let NAME_2 = 'name2';
let NAME_3 = 'name3';

function factory1() {
  return { name: NAME_1 }
}

function factory2() {
  return { name: NAME_2 }
}

function factory3() {
  return {
    o1: factory1(),
    o2: factory2(),
    name: NAME_3
  }
}
</script>

当我们的代码复杂度变高时,如果还是如上述代码这样将所有的功能定义在全局

1.那全局作用域容易被污染 2.同时代码会非常难以维护

这个时候我们尝试将代码拆分成不同的文件,同一个功能下的代码单独维护,如下:

<script src="./1.js"></script>
<script src="./2.js"></script>
<script src="./3.js"></script>

对于问题1,这样的做法不能解决,全局污染的问题仍旧存在。 对于问题2,能一定程度上改善,增加代码的可维护性。

同时会产生新的问题:因为不同文件的js代码之间存在依赖关系,所以需要注意js的加载顺序。

典型的例子就是当我们的代码使用jquery时,我们需要在引入我们的代码前引入jquery

<script src="jquery.js"></script>
<script src="mycode.js"></script>

1.2 如何实现模块化?

var m1 = (function m1() {
  let name = 'name1'
  module.factory = function () {
    return { name }
  }
  return module
})()

var m2 = (function m2() {
  let name = 'name2'
  module.factory = function () {
    return { name }
  }
  return module
})()

通常,我们可以使用闭包和命名空间的方式实现一个模块

1.3 有哪些模块化方案?

  • commonjs: nodejs实现的模块规范,用于服务端
  • es6 Module: es6提出的模块规范,是一种静态的模块依赖描述,用于浏览器端/服务端
  • amd/cmd: require.js实现的模块规范,基本只用于浏览器端

以下是这几种规范简单差异:

标题提出者平台动态/静态同步/异步
commonjsnodejs服务端完全runtime的,动态的依赖关系同步
es6 modulees6浏览器/服务端静态,需要在编译阶段确定依赖关系通常是异步
amdrequire.js浏览器端静态,需要编译异步

二、模块化规范

2.1 amd - 异步模块加载规范

require.js提出的一种规范,通过define函数定义一个模块,通过require函数引入一个模块,并以异步回调的方式执行后续的代码。

defined('module', function())
require(['module'], function(module) { 
  console.log(module.name); 
  module.sayHello(); 
});

2.2 commonjs和es6 module的区别

commonjs是nodejs提出的模块加载规范,实现了服务端js的模块化;es6 module是es6提出的模块规范。

两者主要有以下区别:

2.2.1 commonjs是runtime的,es6是静态依赖关系

同时require一定是同步的,而es6 module的import在编译后往往是异步的

const module = require('module.js');
编译的时候不会对依赖关系做处理,代码运行时,读取module.js文件,在内存中运行并获取一个导出的值(可以是数字,对象,函数)。

import module from './module.js';
编译阶段会做建立静态的依赖关系,当运行代码时,会将module.js的代码提前定义执行到内存中,module只是一个引用。


// 所有模块的代码都存入__webpack_modules__ ,以唯一的id作为索引
var __webpack_modules__ = ([])

// 编译后的代码使用__webpack_modules__ 方法 + id访问模块
// 例如 import { module } from './module.js' -> module = __webpack_require__(1);
var __webpack_require__(id) = ...

2.2.2 commonjs的require读取返回一个值,es6 import是获取一个值的引用。

当你对引用模块的内部变量做修改时,其他引用该模块的地方也会受影响。使用require引入一个模块,会在运行阶段读取并记加载一个值到内存中,对同一个模块可以多次加载。

require函数
// 这里两次require后得到相同的引用,是因为require本身有缓存机制
const module1 = require('./1.js');
const module2 = require('./1.js');
console.log(module1 === module2) // true

// 手动清除缓存后,require读取两次文件,执行两次,导出2个结果值到内存中
const module1 = require('./1.js');
delete require.cache[require.resolve('./1.js')];
const module2 = require('./1.js');
console.log(module1 === module2) // false

使用es6 import引入一个模块,引入的地方会获得一个引用,指向这个模块对应的值,且是全局唯一的。

import { module } from './module.js';

// 其他地方也会受影响, ./module.js这个模块只有一个唯一的id,指向是唯一的
module.value = 1

2.2.3 commonjs不要求在编译阶段确定模块路径,es6模块需要在编译阶段确定模块路径


// path是动态的,只有在runtime阶段才会确定
// require可以正常运行
const path = (await getConfig()).modulePath;
require(path)


// path是动态的,只有在runtime阶段才会确定
// es6 import通常会在编译阶段报错,因为无法找到对应的文件打包到结果中
const path = (await getConfig()).modulePath;
import(path)

三、总结

规范提出者平台静态or动态同步or异步返回
amdrequire.js浏览器端runtime异步的加载后的返回值
common.jsnodejs服务端runtime同步的加载后的返回值
es6es6规范浏览器端 & 服务端静态依赖关系,依赖编译通常是异步的模块的引用