前端模块化

一、模块化的发展

js文件

最早的时候,通过文件划分的形式实现模块化,将功能状态数据各自单独放到不同的 JS 文件中。 每个文件作为独立模块,引入到页面,一个script标签对应一个模块,然后调用模块化的成员。

js 复制代码
<script src="module1.js"></script>
<script src="module2.js"></script>

弊端:模块与模块之间没有依赖关系,维护困难,无私有空间。项目变大时,上述问题更明显。

解决:命名空间

命名空间

每个模块暴露全局对象,模块的内容挂载到对象中。

js 复制代码
window.moduleA = {  
  method1: function() {  
    console.log('moduleA#method1') 
  } 
}

弊端:这种方式未解决模块与模块之间没有依赖关系的问题。

立即执行函数

使用立即执行函数为模块提供私有空间,通过参数的形式作为依赖声明。

js 复制代码
// module-a.js
(function ($) {
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
$('body').animate({ margin: '200px' })
}
window.moduleA = {
method1: method1
}
})(jQuery)

评价:上述方式用script标签在页面引入模块,模块加载不受代码控制。时间一久维护起来麻烦。

解决:页面用引入一个JS入口文件,其余用到的模块通过代码控制。按需加载。以及制定模块化的规范。如今流行的模块规范有ES Modules,以及在ES6之前CommonJS、AMD浏览器和服务器通用的模块解决方案。

二、CommonJS、AMD、ES Modules

CommonJS

主要用于服务器端开发 (如 Node.js)。CommonJS规范通过 module.exports 导出模块,通过 require 函数加载模块。CommonJS 模块是同步 加载的,它是为了解决 JavaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。

CommonJS模块规范主要分为引用、定义、标识模块三部分:

js 复制代码
module.exports = function( value ){
    return value * 2;
}
/**
 * 模块引用
 *
 * 使用require()方法来引入一个模块;
 * 这里引入 模块:moduleA,并复制给变量multiplyBy2;
 * 
 */

var multiplyBy2 = require('./moduleA');
var result = multiplyBy2(4);

/**
 * 模块标识
 * 
 * 指传给require()的参数 可以是小驼峰命名的模块名或是路径
 *
 * require("模块名"):当前目录下的node_modules目录中的模块
 * require("路径"):指定目录的指定模块
 *
 */ 
 
/**
 * 模块定义
 * 
 * module对象:module对象指的模块自身
 * export属性:module对象的属性,为外部提供接口
 *
 */ 
module.exports = function( value ){
    return value * 2;
} 

特点:

  • CommonJS是同步加载模块,模块加载的顺序,按照其在代码中出现的顺序。
  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。

注意:浏览器不兼容CommonJS的根本原因,在于缺少四个Node.js环境的变量。module、exports、require、global。所以只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。

js 复制代码
var module = { exports: {} }; 
(function(module, exports) { 
exports.multiply = function (n) 
{ return n * 1000 }; }(module, module.exports)) 

var f = module.exports.multiply; f(5) // 5000

上面代码向一个立即执行函数提供 module 和 exports 两个外部变量,模块就放在这个立即执行函数里面。模块的输出值放在 module.exports 之中,这样就实现了模块的加载。

AMD

AMD(异步模块定义)是为浏览器环境设计的,因为 CommonJS 模块系统是同步加载的,当前浏览器环境还没有准备好同步加载模块的条件。

AMD 定义了一套 JavaScript 模块依赖异步加载标准,来解决同步加载的问题。它主要用于浏览器环境 ,在加载依赖模块时使用异步 方式。目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。AMD使用define函数定义模块,使用 require 函数加载模块。

使用require.js的第一步,是先去官方网站下载最新版本。下载后,假定把它放在js子目录下面,就可以加载了。 <script src="js/require.js"></script>

加载这个文件,也可能造成网页失去响应。解决办法有两个,一个是把它放在网页底部加载,另一个是写成下面这样:
<script src="js/require.js" defer async="true" ></script>

async属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上。

加载require.js以后,下一步就要加载我们自己的代码了。假定我们自己的代码文件是main.js,也放在js目录下面。那么,只需要写成下面这样就行了:
<script src="js/require.js" data-main="js/main"></script>

data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。 main.js有点像c语言的main()函数。主模块依赖于其他模块,这时就要使用AMD规范定义的的require()函数。

js 复制代码
// main.js
  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // some code here
  });

require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块,上例就是['moduleA', 'moduleB', 'moduleC'],即主模块依赖这三个模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。

require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

假定主模块依赖jquery、underscore和backbone这三个模块,main.js就可以这样写:

js 复制代码
require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){
    // some code here
  });

require.js会先加载jQuery、underscore和backbone,然后再运行回调函数。主模块的代码就写在回调函数中。

ES Modules

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

js 复制代码
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。 浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。
<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。

ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。

js 复制代码
<script type="module">
  import utils from "./utils.js";
    
  // other code
</script>

特点:

  • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
    利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。
js 复制代码
import utils from 'https://example.com/js/utils.js';
const x = 1;
console.log(x === window.x); //false
console.log(this === undefined); // true
  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 同一个模块如果加载多次,将只执行一次。
  • 由于 ES6 输入的模块变量,只是一个"符号连接",所以这个变量是只读的,对它进行重新赋值会报错。
js 复制代码
// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError
  • export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
js 复制代码
// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();

// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';

现在执行main.js,输出的是1

css 复制代码
$ babel-node main.js
1

三、常见面试题

ES6 模块与 CommonJS 模块的差异

- 三个重大差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

解释:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    CommonJS 模块输出的是值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。
js 复制代码
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: 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是一个原始类型的值,会被缓存。

js 复制代码
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};
//结果
$ node main.js
3
4

ES6 的import有点像 Unix 系统的"符号连接",原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

js 复制代码
// 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
  1. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
    CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

其他区别:

  • ESModule在编译期间会将所有import提升到顶部,CommonJs不会提升require。
  • CommonJS中顶层的this指向这个模块本身,而ESModule中顶层this指向undefined。
  • 两者导入导出语法不同,CommonJS通过module.exports(或exports导出)require导入;ESModule则是export导出,import导入。
  • CommonJS 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
js 复制代码
//a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。b.js执行到第二行,就会去加载a.js,这时,就发生了"循环加载"。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。上述代码结果是

js 复制代码
$ node main.js
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true

ES6 处理"循环加载"与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

js 复制代码
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

结果:

js 复制代码
$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined

首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。

解决这个问题的方法,就是让b.mjs运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决。

js 复制代码
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
//结果
$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar

参考:
前端模块化:CommonJS,AMD,CMD,ES6
CommonJS和AMD规范
js模块化编程之彻底弄懂CommonJS和AMD/CMD!
ECMAScript 6入门

相关推荐
萧大侠jdeps12 分钟前
Vue 3 与 Tauri 集成开发跨端APP
前端·javascript·vue.js·tauri
JYeontu1 小时前
实现一个动态脱敏指令,输入时候显示真实数据,展示的时候进行脱敏
前端·javascript·vue.js
发呆的薇薇°1 小时前
react里使用Day.js显示时间
前端·javascript·react.js
嘤嘤嘤1 小时前
基于大模型技术构建的 GitHub Assistant
前端·github
KeepCatch1 小时前
CSS 动画与过渡效果
前端
跑跑快跑1 小时前
React vite + less
前端·react.js·less
web136885658711 小时前
ctfshow_web入门_命令执行_web29-web39
前端
GISer_Jing1 小时前
前端面试题合集(一)——HTML/CSS/Javascript/ES6
前端·javascript·html
清岚_lxn1 小时前
es6 字符串每隔几个中间插入一个逗号
前端·javascript·算法
胡西风_foxww1 小时前
【ES6复习笔记】Map(14)
前端·笔记·es6·map