likes
comments
collection
share

浅谈前端模块化

作者站长头像
站长
· 阅读数 22

简介

技术的诞生是为了解决某个问题,模块化也是。 随着前端的发展,web技术日趋成熟,js功能越来越多,代码量也越来越大。之前一个项目通常各个页面共用一个js,但是随着js逐渐拆分,项目中引入的js越来越多. 在js模块化诞生之前,开发者面临很多问题:

  • 全局变量污染:各个文件的变量都是挂载到window对象上,污染全局变量。
  • 变量重名:不同文件中的变量如果重名,后面的会覆盖前面的,造成程序运行错误。
  • 文件依赖顺序:多个文件之间存在依赖关系,需要保证一定加载顺序问题严重。

简单来说,模块化是将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起,块的内部数据与实现是私有的, 只是向外部暴露一些接口(属性、方法)与外部其它模块通信

我们再来看看模块化有哪些优点:

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

下面我们来看看前端模块化的发展历程

浅谈前端模块化

在说这些模块化之前,我们先来看看最早期的模块化是怎么实现的

IIFE

早期的模块化主要是利用立即执行函数闭包的特性来实现的。

// module1.js

// 这样我们就可以通过 myModule.getName() 来获取 name,并且实现 name 属性的私有化,即外部调用不到
const myModule = (function () {
  const name = "闭包模块";

  const getName = () => {
    return name;
  };
  
  // 只暴露需要给外面使用的
  return { getName };
})();

console.log(myModule.name); // undefined
console.log(myModule.getName()); // 闭包模块

那假如我们这个模块需要依赖其他模块呢?假如我们这里的模块2依赖上面的模块。

我们可以给函数传递参数

// module2.js

const myModule2 = (function (myModule) {
  const name = "闭包模块2";

  const getName = () => {
    return name;
  };

  const getFullName = () => {
    return name + myModule.getName();
  };
  
  // 只暴露需要给外面使用的
  return { getName, getFullName };
})(myModule);

console.log(myModule2.name); // undefined
console.log(myModule2.getName()); // 闭包模块2
console.log(myModule2.getFullName()); // 闭包模块2闭包模块

通过这种传参的形式,我们就可以在 myModule2 模块中使用其他模块,从而解决了很多问题,这也是现代模块化规范的思想来源。

这样的模块化有什么问题呢?那就是文件越来越多,依赖越来越不明显。

比如我们上面的例子,module2依赖module1,所以我们引入js的时候一定要先引入module1再引入module2

// index.html文件 <!-- 引入的js必须有一定顺序 --> 
<script type="text/javascript" src="./module1.js"></script>
<script type="text/javascript" src="./module2.js"></script>
// ...

所以当我们模块越来越多的时候,文件也会越来越多,并且依赖也会越来越混乱。这种方案在目前基本没人使用。

下面笔者说说真正成文的几种模块化方案

CommonJS规范

说到CommonJS,大家就会不约而同的想到node.js。在node.js中,默认就支持模块系统,并且遵循CommonJS规范。

CommonJs 就是模块化的社区标准,而 Nodejs 就是 CommonJs 模块化规范的实现

CommonJS的语法是怎么样的?都有哪些特点呢?下面我们来看看

CommonJS规范中一个js文件就是一个模块,在模块内部定义的变量和函数默认只能在该模块内部访问,如果外部需要访问需要显示导出。

语法

使用module.exports、exports导出,require导入。

我们首先定义一个模块,并导出一个变量

// cjs2.js

module.exports = {
  name: "我是cjs2.js 我通过module.exports导出",
};

我们在另外一个模块引入

// cjs1.js

const module2 = require("./cjs2");

console.log(module1.name); // 我是cjs2.js 我通过module.exports导出

执行看下效果

浅谈前端模块化

除了使用module.exports导出,我们还可以使用exports导出

// cjs2.js

exports.name = "我是cjs2.js 我通过exports导出"

我们再次执行看下效果

浅谈前端模块化

都正确输出了。

对于只需要导入不需要使用的模块我们该怎么处理呢?

其实我们直接require就可以了。在引入@babel/polyfill的时候我们经常这样使用。

require('@babel/polyfill')

module.exports和exports

既然module.exportsexports都能导出,那module.exportsexports到底有什么区别呢?他们各自又有什么特点呢?

第一 module.exportsexports两者不能共存,当同时存在的时候module.exports会把exports覆盖。

// cjs2.js

module.exports = {
  name: "我是cjs2.js 我通过module.exports导出",
};

exports.name = "我是cjs2.js 我通过exports导出";

我们执行看下效果

浅谈前端模块化

第二 exports导出不能直接赋值

// cjs2.js

exports = {
  name: "我是cjs2.js 我通过exports导出",
};

我们执行看下效果

浅谈前端模块化

看到,导出来的是undefined,为什么不能这样导出呢?

exports实际上是module.exports的引用,exports = module.exports,我们直接 exports = {} 修改 exports ,等于将exports重新赋值了,所以后续的导出肯定不会影响到module.exports

所以我们上面如果想使用exports导出name只能通过如下方式

exports.name = "我是cjs2.js 我通过exports导出";

第三 为什么有了module.exports还要exports

因为exports相较module.exports语法更简洁。

exports.name = 'randy'

// 相较

module.exports.name = 'randy'

第四 exports相较module.exports劣势是什么?

如果我们不想在 commonjs 中导出对象,而是只导出一个类或者一个函数再或者其他属性的情况,那么 就只能使用 module.exports 了。

如上我们知道 exports 会被初始化成一个对象,也就是我们只能在对象上绑定属性,但是我们可以通过 module.exports 自定义导出对象外的其他类型元素。

特点

接下来我们分析下CommonJS的模块化都有哪些特点

  1. 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果(运行时加载)。

我们来看个例子

// cjs2.js

let count = 1;

const incCount = () => {
  count++;
};

let user = { age: 24 };

const incAge = () => {
  user.age++;
};


module.exports = {
  count,
  incCount,
  user,
  incAge,
};

我们导入使用下

// cjs1.js

const module1 = require("./cjs2");

console.log(module1.count); // 1
module1.incCount(); 
console.log(module1.count); // 1

console.log(module1.user); // { age: 24 }
module1.incAge();
console.log(module1.user); // { age: 25 }

上面的例子,当我们第一次导入的时候,数据就会被拷贝过来。所以当数据是基本数据类型的时候,模块内部数据后续的修改就不会再影响到外部导入的模块了,当数据是引用数据类型的时候还是会影响到外部导入的模块。

注意这里说的拷贝是浅拷贝,通过上面的例子也能看出来。

  1. 以同步方式加载模块,并且模块加载的顺序,按照其在代码中出现的顺序,不会将require提升到顶部。

我们来看个例子

// cjs3.js
console.log("我是cjs3模块");

module.exports = function say() {
  console.log("cjs3");
};

cjs2.js我们引入cjs3.js

// cjs2.js
require("./cjs3.js");
console.log("我是cjs2模块");

module.exports = function say() {
  console.log("cjs2");
};

入口js文件引入cjs2.jscjs3.js

// cjs1.js

console.log("cjs1开始执行");
require("./cjs2.js");
require("./cjs3.js");
console.log("cjs1执行完毕");

我们执行cjs1.js看效果

浅谈前端模块化

可以看到require完全是运行时同步加载的。

  1. require可以放在块级作用域或条件语句中。
if (true) {
  const module1 = require("./cjs2");
  console.log(module1.count);
}

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。这在服务端node.js中使用是没什么问题的。但是如果是浏览器端,模块都依赖于网络请求,再使用同步的方式肯定是不太适用的,所以就有了后面的AMD、CMD异步加载模块的方式。

如何实现模块的?

前面说了CommonJS规范,一个js文件就是一个模块,模块内部定义的变量和函数在模块外部是访问不到的,这个是怎么实现的呢?

这就得说到模块包装函数了。看了下面的函数你就瞬间明白了

(function(exports, require, module, __filename, __dirname) {
  // 实际代码
})

其实我们每个文件的代码都会被包裹在一个立即执行函数里面。并且该函数会有exports, require, module, __filename, __dirname这五个参数。这也是为什么我们能直接使用这五个变量的原因。

  1. exports前面说了,就是module.exports的引用,用来导出模块
  2. require前面说了,用来导入模块
  3. module 前面说了,用来导出模块
  4. __filename 输出当前文件的绝对路径
  5. __dirname 输出当前文件的文件夹绝对路径,也就是不包括当前文件名。

require查找规则

既然require用来导入模块,那么对于它的查找规则你们都清楚了吗?

require的查找分两种,一种是带路径,一种是不带

我们先来说说带路径的情况

require("./serve")

对于上面的导入它会依次进行如下判断

  1. 首先会查找当前目录下的serve.js
  2. 没找到再查找当前目录下的serve.json
  3. 再没找到再查找当前目录下的serve目录,并查看入口文件(package.json -> main字段),如果有定义则执行main字段指定的入口文件。
  4. 再没查到再查找当前目录下的serve目录,执行该目录下的index.js文件。

我们再来说说不带路径

require("serve")

对于不带路径的情况它会当做安装的第三方模块进行查找,也就是查找node_modules目录下是否有该文件。

它会一层一层往上查找node_modules文件夹,直到根目录

浅谈前端模块化

当然它在node_modules中查找模块也是遵循带路径的查找规则(即先serve.jsserve.jsonserve目录)

AMD

AMD是异步加载模块的一个比较流行的方案。主要用于浏览器端,其中最具代表性的就是require.js,我们来测试下。

语法

使用define定义模块并使用return导出,使用require导入模块。

首先我们引入下require.js

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AMD</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
  </head>
  <body>
    <script src="./amd.js"></script>
  </body>
</html>

然后创建我们的模块文件

// amd1.js

console.log("adm1开始执行啦");

// 异步加载,使用require导入模块
require(["./amd2.js"], function (m) {
  // 模块加载完才会进入,也就是依赖前置
  console.log("amd1.js");
  console.log(m);
});

console.log("adm1执行结束啦");

模块二,导出模块

// amd2.js

// 使用define定义模块
define(function () {
  console.log("amd2.js");
  return {
    name: "randy",
  };
});

我们到浏览器运行看下效果

浅谈前端模块化

果然是异步加载的,并且子模块会优先执行。

特点

异步加载,依赖前置,也就是依赖的模块全部加载完毕才会运行当前模块。

原理

require.js 的核心原理是通过动态创建 script 脚本来异步引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。

上面的例子,我们可以看到,他给我们自动创建了两个<script>

浅谈前端模块化

CMD

CMD也是异步加载模块的一个比较流行的方案。主要用于浏览器端,其中最具代表性的就是sea.js,我们来测试下。

语法

使用define方法利用require参数导入模块,使用exports、module.exports导出模块,使用seajs.use使用模块。

首先我们引入下sea.js

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>CMD</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/seajs/3.0.3/sea.js"></script>
  </head>
  <body>
    <script src="./cmd.js"></script>
  </body>
</html>

然后创建我们的模块文件

// cmd1.js

console.log("cmd1开始执行啦");

// 使用模块
seajs.use(["./cmd2.js"], function (math) {
  console.log("seajs");
  const sum = math.add();
  console.log(sum);
});

console.log("cmd1执行结束啦");

模块二,导出add方法

// cmd2.js

define(function (require, exports, module) {
  console.log("cmd2.js");
  // ... 这里可能会有很多其他代码
  
  // 需要用到的时候再去加载 cmd3.js
  const num = require("./cmd3.js");
  const add = () => {
    return num.a + num.b;
  };
  exports.add = add;
});

模块三,导出a、b变量

// cmd3.js

define(function (require, exports, module) {
  console.log("cmd3.js");
  
  // 还可以使用module.exports导出
  module.exports.a = 10;
  module.exports.b = 15;
});

我们到浏览器运行看下效果

浅谈前端模块化

可以看到,模块是异步加载的,并且子模块在需要用到的时候才会去加载。

特点

异步加载,依赖后置,也就是当需要用到某模块依赖的时候才会使用require去加载,而不是事先定义好。

ESM

ESM是在es6实现的。ESMJavaScript 官方推出的标准化模块系统。

语法

使用export、export default导出数据,import 导入数据。

我们首先定义一个模块,并导出一个变量

// esm2.js

export const name = '我是esm2.js 我通过export导出'

我们在另外一个模块引入

// esm1.js

import {name} from './esm2.js'

console.log(name); // 我是esm2.js 我通过export导出

因为现在很多浏览器都已经直接支持ESM了,所以我们直接在index.html中引入,然后启动服务查看一下效果。

注意script标签需要定义type="module"

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>esm</title>
  </head>
  <body>
    <script type="module" src="./esm1.js"></script>
  </body>
</html>

浅谈前端模块化

除了使用export导出,我们还可以使用export default导出。

// esm2.js

const name = '我是esm2.js 我通过export default导出'
export default name

我们在另外一个模块引入

// esm1.js

import name from './esm2.js'

console.log(name); // 我是esm2.js 我通过export default导出

浅谈前端模块化

对于只需要导入不需要使用的模块我们该怎么处理呢?

其实我们直接import就可以了。

import('@babel/polyfill')

对于导出和导入我们还可以取别名

// esm2.js

const name = 'randy'
export {
  // 导出取别名  
  name as _name
}

我们在另外一个模块引入

// esm1.js

import {_name} from './esm2.js'

console.log(_name); // randy

我们在另外一个模块引入的时候还可以使用导入别名

// esm1.js

// 导入取别名
import {_name as NAME} from './esm2.js'

console.log(NAME); // randy

export default和export

既然exportexport default都能导出,那它们之间的区别是什么呢?

第一 export default 在一个文件模块中只能有一个。导出的数据能直接被使用。

// esm2.js

const name = 'randy'
export default name

我们在另外一个模块引入

// esm1.js

import name from './esm2.js'

// name就是导出的数据,能直接被使用
console.log(name); // randy

第二 export可以有多个,并且导出的数据会在对象上。

// esm2.js

export const name = 'randy'
export const age = 24

我们在另外一个模块引入

// esm1.js

// 需要从对象中结构出来
import {name, age} from './esm2.js'

console.log(name); // randy
console.log(age); // 24

第三 不同于上面说的CommonJSESMexportexport default可以混合使用。只需要确保只有一个export default即可。

// esm2.js

export const name = 'randy'
export const age = 24

const info = 'export和export default可以混用哦'
export default info

我们在另外一个模块引入

// esm1.js

// 需要从对象中结构出来
import info, {name, age} from './esm2.js'

console.log(name); // randy
console.log(age); // 24
console.log(info); // export和export default可以混用哦

特点

接下来我们看看ESM模块化都有哪些特点

  1. ESM运行机制与commonjs运行机制不一样。js引擎对脚本静态分析的时候,遇到模块加载指令后会生成一个只读引用(编译时加载)。等到脚本真正执行的时候。才会通过引用模块中获取值,在引用到执行的过程中,模块中的值发生变化,导入的这里也会跟着发生变化。也就是说ESM是动态引入的,并不会缓存值。

我们来看这个例子

// esm2.js

export let count = 1;

export const incCount = () => {
  count++;
};

我们导入使用下

// esm1.js

import { count, incCount } from "./esm2.js";

console.log(count); // 1
incCount();
console.log(count); // 2

可以看到,我们直接修改模块内部的变量,在我们的导入模块也是会有影响的。所以可以看出来ESM导入模块导入的不是值拷贝,而是引用。

  1. ESM标准模块加载器是异步的,读取到脚本后不会立即执行,而是先进入编译阶段进行模块解析检查模块上调用了import和export的地方,并以此类推的把依赖模块一个个异步、并行地进行下载。在此阶段ESM加载器不会执行任何依赖模块代码,只检查语法错误、确定模块依赖关系、确定模块输出和输入的变量。最后ESM会进入执行阶段,按照顺序执行各模块脚本。所以import 会自动提升到代码的顶部。

我们来看个例子

// esm3.js

console.log("我是esm3模块");
export default function say() {
  console.log("esm3 say");
}

esm2.js我们引入esm3.js

// esm2.js

import "./esm3.js";
console.log("我是esm2模块");

export default function say() {
  console.log("esm2 say");
}

入口js文件引入esm2.jsesm3.js

// esm1.js

console.log("esm1开始执行");
import say2 from "./esm2.js";
say2();
import say3 from "./esm3.js";
say3();
console.log("esm1执行完毕");

我们执行esm1.js看效果

浅谈前端模块化

可以看到import语句被提升了,并且esm3.js只被加载了一次

  1. ESM 默认使用严格模式(use strict),因此在ESM模块中的this不再指向全局window对象(global object),而是undefined
console.log(this) // undefined

浅谈前端模块化

如果要访问全局对象,需要使用globalThis

console.log(globalThis);

浅谈前端模块化

  1. 使用var定义的变量不会挂载在window

我们来看例子

var name1 = "randy";
console.log(name1); // randy
console.log(window.name1); // undefined

上面代码执行结果如下

浅谈前端模块化

  1. import不可以放在块级作用域或条件语句中。如果要动态导入,需要使用到import()方法
// esm2.js

export default function say() {
  console.log("esm2");
}

export const name = "randy";

我们使用动态导入

if (true) {
  import("./esm2.js").then((res) => {
    console.log(res);
  });
}

查看下输出结果

浅谈前端模块化

关于<script type="module">

因为前面我们演示是使用的<script type="module">来运行我们的esm模块化代码,对于<script type="module">这里我们再来说明一下。

当我们的script标签加上了type="module"就可以在script里面直接使用ESM模块了。也就是可以使用 import、export 啦。

需要注意 script标签加上了 type="module" 默认会被 defer 修饰。也就是会被异步加载执行。

Vite抛弃了以往以bundle为主的开发服务器,进而选择了一种nobundle的资源处理模式。其实它的底层就是利用了type="module"使浏览器能直接运行ESM模块化代码,让我们在开发阶段运行速度有一个质的飞升。

如果我们不使用这种方式,直接将ESM模块化代码在浏览器运行时会报错的。

浅谈前端模块化

所以目前使用ESM模块化的代码都会使用webpack等构建工具进行编译转化,以让浏览器能直接运行。

接下来我们来看看ESM模块到底需要怎么编译,以及编译后代码会变成什么样?

编译ESM模块

聪明的同学肯定会想到编译es肯定是babel,还真没错,对于ESM模块的编译我们需要用到babel,那使用babel就直接能把ESM模块编译成浏览器可执行的代码吗?

使用babel

假设有下面的代码

index2.js用来导出

// index2.js
const name = "randy";

export default name;

index1.js用来导入

// index1.js

import name from "./index2";

console.log(name);

我们使用babel编译一下,看输出什么

// 编译后的index2.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = void 0;
var name = "randy";
var _default = name;
exports["default"] = _default;

可以看到它定义了一个exports对象,并将__esModule属性设置为true。然后将我们的默认导出设置到default属性上。

如果还有其它导出属性的话也会放在exports上。

// 编译后的index1.js

"use strict";

var _index = _interopRequireDefault(require("./index8"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

console.log(_index["default"]); // 这里会输出randy

这边导入的时候它会使用require来进行导入,并且会使用_interopRequireDefault方法,这个方法就用到了前面的__esModule,它会进行判断是否有__esModule,有就直接返回,没有就包裹在对象的default属性中返回。

从这里就可以看出,为什么我们在写前端代码的时候既可以使用CommonJS规范还可以使用ESM规范了。因为使用babel编译后的代码都会变成CommonJS规范。

如果我们将上面的index1.js修改成require导入,编译后会变成什么样呢?

// index1.js

const name = require("./index2");

console.log(name);

我们来编译一下,编译结果如下,可以看到他并没有像上面一样使用辅助函数_interopRequireDefault,而是直接使用的require

"use strict";

var name = require("./index8");

console.log(name); // 这里会输出{default: "randy"}

这也就可以解释为什么有的地方使用 require 去引用一个模块时需要加上 default了。比如require('xx').default。因为通过require导入模块不会被处理,会被直接使用。

这个场景在写 webpack 代码分割逻辑时经常会遇到。

require.ensure([], (require) => {
   callback(null, [
     require('./src/pages/login').default,
   ]);
 });

通过babel编译后的代码能直接在浏览器运行吗?

当然是不行的,因为我们发现编译后的代码是CommonJS规范,还是不能直接在浏览器运行的。那怎么办呢?

那就还需要一个支持 CommonJS 模块语句的构建系统,比如webpackBrowserify。这里我们选择大家所熟知的webpack来演示。

使用webpack

我们安装webpack并配置babel loader再来编译一下我们上面的代码。

编译结果如下

(function () {
  "use strict";
  var __webpack_modules__ = {
    "./jssrc/es3.js": function (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) {
      __webpack_require__.r(__webpack_exports__);
      // index2.js
      var name = "randy";
      __webpack_exports__["default"] = name;
    },
  };

  // The module cache
  var __webpack_module_cache__ = {};
  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // Create a new module (and put it into the cache)
    var module = (__webpack_module_cache__[moduleId] = {
      // no module.id needed
      // no module.loaded needed
      exports: {},
    });
    // Execute the module function
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    // Return the exports of the module
    return module.exports;
  }

  /* webpack/runtime/make namespace object */
  !(function () {
    // define __esModule on exports
    __webpack_require__.r = function (exports) {
      if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, {
          value: "Module",
        });
      }
      Object.defineProperty(exports, "__esModule", { value: true });
    };
  })();

  var __webpack_exports__ = {};
  !(function () {
    __webpack_require__.r(__webpack_exports__);
    /* harmony import */ var _es3__WEBPACK_IMPORTED_MODULE_0__ =
      __webpack_require__(/*! ./es3 */ "./jssrc/es3.js");

    console.log(_es3__WEBPACK_IMPORTED_MODULE_0__["default"]);
  })();
})();

我们简化一下,总体结构如下

(function() {
  // __webpack_modules__ 是一个模块路径和获取模块值函数的对象。
  
  // __webpack_require__ 是一个通过模块路径在__webpack_modules__ 查找并模块值的方法
  
  // 通过立即执行函数定义__webpack_require__.r方法,这个方法类似前面的babel,给对象定义__esModule属性,表示是ESM模块编译出来的结果
  
  // 通过立即执行函数,调用__webpack_require__方法传递模块路径获取到我们需要的模块值,进行输出
})()

简化后的代码一眼就看出来是我们最开始说的IIFE模块化方案,也就是立即执行函数,这种代码我们的浏览器是一定能够直接运行的。

所以,ESM模块化代码如果要通过编译然后在浏览器执行一定会经过babelwebpack的处理,将他变成立即执行函数,然后我们的浏览器就能正常执行了。

既然模块化方案有这么多,有没有一种方案能统一呢?诶,还真有,那就是UMD方案。

UMD

UMD(Universal Module Definition),即通用模块定义,从名字就可以看出来,这东西是做大一统的。

它其实就是对AMD、CMD、CommonJS的兼容,使得各个版本都能正常运行。

我们在使用webpack构建代码的时候通过配置library就能输出UMD格式的代码。

module.exports = {
  //...
  output: {
    library: {
      name: 'MyLibrary',
      type: 'umd',
    },
  },
};

核心代码

其实它也很简单,也是一个立即执行函数

((root, factory) => {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(factory);
  } else if (typeof define === 'function' && define.cmd){
    // CMD
    define(function(require, exports, module) {
      module.exports = factory()
    })
  } else if (typeof exports === 'object') {
    // CommonJS
    module.exports = factory();
  } else {
    // 都不是直接挂载在全局对象上
    root.umdModule = factory();
  }
})(this, () => {
  // return 需要定义的模块
  return {
    name: 'randy'
  }
});

可以看到,defineAMD/CMD 语法,而 exports 只在 CommonJS 中存在,你会发现它在定义模块的时候会检测当前使用环境和模块的定义方式,如果匹配就使用其规范语法,全部不匹配则挂载再全局对象上,我们看到传入的是一个 this ,它在浏览器中指的就是 window ,在服务端环境中指的就是 global ,使用这样的方式就能兼容各种模块了。

总结

看到这,小伙伴们是否对前端模块化又有了一个新的认知呢?

就目前来说,其实使用比较广泛的只有CommonJSESM规范。笔者觉得这两个规范我们需要重点掌握。对于AMDCMD可能只能在老项目里面才能看得到,了解即可。

CommonJSESM模块化区别:

  1. 语法层面,CommonJs是通过module.exportsexports导出,require导入。而ESM则是exportexport default导出,import导入。

  2. CommonJS模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存(浅拷贝)结果(运行时加载)。ESM模块是js引擎对脚本静态分析的时候,遇到模块加载指令后会生成一个只读引用(编译时加载)。等到脚本真正执行的时候。才会通过引用模块中获取值,在引用到执行的过程中,模块中的值发生变化,导入的这里也会跟着发生变化。也就是说ESM是动态引入的,并不会缓存值。

  3. CommonJS以同步方式加载模块,并且模块加载的顺序,按照其在代码中出现的顺序,不会将require提升到顶部。ESM标准模块加载器是异步的,读取到脚本后不会立即执行,而是先进入编译阶段进行模块解析检查模块上调用了import和export的地方,并以此类推的把依赖模块一个个异步、并行地进行下载。在此阶段ESM加载器不会执行任何依赖模块代码,只检查语法错误、确定模块依赖关系、确定模块输出和输入的变量。最后ESM会进入执行阶段,按照顺序执行各模块脚本。所以import 会自动提升到代码的顶部。

  4. CommonJS的导入require可以放在块级作用域或条件语句中。而ESM模块的import不可以放在块级作用域或条件语句中。如果要动态导入,需要使用到import()方法。

  5. ESM 默认使用严格模式(use strict),因此在ESM模块中的this不再指向全局window对象(global object),而是undefined。如果要访问全局对象,需要使用globalThis

  6. ESM模块中使用var定义的变量不会挂载在window上。

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!

转载自:https://juejin.cn/post/7210216031536431162
评论
请登录