天天看点

Javascript模块化详解解释成 ES6 模块

前端的发展日新月异,前端工程的复杂度也不可同日而语。原始的开发方式,随着项目复杂度提高,代码量越来越多,所需加载的文件也越来越多,这个时候就需要考虑如下几个问题:

命名问题:所有文件的方法都挂载到window/global上,会污染全局环境,并且需要考虑命名冲突问题

依赖问题:script是顺序加载的,如果各个文件文件有依赖,就得考虑js文件的加载顺序

网络问题:如果js文件过多,所需请求次数就会增多,增加加载时间

Javascript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。

本文主要介绍Javascript模块化的4种规范: CommonJS、AMD、UMD、ESM。

CommonJS

CommonJS是一个更偏向于服务器端的规范。NodeJS采用了这个规范。CommonJS的一个模块就是一个脚本文件。require命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象。

{

id: '...',

exports: { ... },

loaded: true,

...

}

id是模块名,exports是该模块导出的接口,loaded表示模块是否加载完毕。

以后需要用到这个模块时,就会到exports属性上取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存中取值。

// utile.js

const util = {

name:'Clearlove'

sayHello:function () {

return 'Hello I am Clearlove';

// exports 是指向module.exports的一个快捷方式

module.exports = util

// 或者

exports.name = util.name;

exports.sayHello = util.sayHello;

const selfUtil = require('./util');

selfUtil.name;

selfUtil.sayHello();

CommonJS是同步导入模块

CommonJS导入时,它会给你一个导入对象的副本

CommonJS模块不能直接在浏览器中运行,需要进行转换、打包

由于CommonJS是同步加载模块,这对于服务器端不是一个问题,因为所有的模块都放在本地硬盘。等待模块时间就是硬盘读取文件时间,很小。但是,对于浏览器而言,它需要从服务器加载模块,涉及到网速,代理等原因,一旦等待时间过长,浏览器处于”假死”状态。

所以在浏览器端,不适合于CommonJS规范。所以在浏览器端又出现了一个规范—-AMD。

AMD

AMD(Asynchronous Module Definition - 异步加载模块定义)规范,一个单独的文件就是一个模块。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。

这里异步指的是不堵塞浏览器其他任务(www.taobaoosx.com,dom构建,css渲染等),而加载内部是同步的(加载完模块后立即执行回调)。

AMD也采用require命令加载模块,但是不同于CommonJS,它要求两个参数:

require([module], callback);

第一个参数[module],是一个数组,里面的成员是要加载的模块,callback是加载完成后的回调函数,回调函数中参数对应数组中的成员(模块)。

AMD的标准中,引入模块需要用到方法require,由于window对象上没定义require方法, 这里就不得不提到一个库,那就是RequireJS。

官网介绍RequireJS是一个js文件和模块的加载器,提供了加载和定义模块的api,当在页面中引入了RequireJS之后,我们便能够在全局调用define和require。

define(id?, dependencies?, factory);

id:模块的名字,如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字

dependencies:模块的依赖,已被模块定义的模块标识的数组字面量。依赖参数是可选的,如果忽略此参数,它应该默认为 ["require", "exports", "module"]。然而,如果工厂方法的长度属性小于3,加载器会选择以函数的长度属性指定的参数个数调用工厂方法。

factory:模块的工厂函数,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。

// 定义一个moduleA.js

define(function(){

const name = "module A";

return {

getName(){

return name

});

// 定义一个moduleB.js

define(["moduleA"], function(moduleA){

showFirstModuleName(){

console.log(moduleA.getName());

// 实现main.js

require(["moduleB"], function(moduleB){

moduleB.showFirstModuleName();

<html>

<!-- 此处省略head -->

<body>

<!--引入requirejs并且在这里指定入口文件的地址-->

<script data-main="js/main.js" src="js/require.js"></script>

</body>

</html>

要通过script引入requirejs,然后需要为标签加一个属性data-main来指定入口文件。

前面介绍用define来定义一个模块的时候,直接传“模块名”似乎就能找到对应的文件,这一块是在哪实现的呢?其实在使用RequireJS之前还需要为它做一个配置:

// main.js

require.config({

paths: {

// key为模块名称, value为模块的路径

"moduleA": "./moduleA",

"moduleB": "./moduleB"

这个配置中的属性paths只写模块名就能找到对应路径,不过这里有一项要注意的是,路径后面不能跟.js文件后缀名,更多的配置项请参考RequireJS官网。

UMD

UMD 代表通用模块定义(Universal Module Definition)。所谓的通用,就是兼容了CmmonJS和AMD规范,这意味着无论是在CmmonJS规范的项目中,还是AMD规范的项目中,都可以直接引用UMD规范的模块使用。

原理其实就是在模块中去判断全局是否存在exports和define,如果存在exports,那么以CommonJS的方式暴露模块,如果存在define那么以AMD的方式暴露模块:

(function (root, factory) {

if (typeof define === "function" && define.amd) {

define(["jquery", "underscore"], factory);

} else if (typeof exports === "object") {

module.exports = factory(require("jquery"), require("underscore"));

} else {

root.Requester = factory(root.$, root.);

}(this, function ($, ) {

// this is where I defined my module implementation

const Requester = { // ... };

return Requester;

}));

这种模式,通常会在webpack打包的时候用到。output.libraryTarget将模块以哪种规范的文件输出。

ESM

在ECMAScript 2015版本出来之后,确定了一种新的模块加载方式,我们称之为ES6 Module。它和前几种方式有区别和相同点:

它因为是标准,所以未来很多浏览器会支持,可以很方便的在浏览器中使用

它同时兼容在node环境下运行

模块的导入导出,通过import和export来确定

可以和CommonJS模块混合使用

CommonJS输出的是一个值的拷贝。ES6模块输出的是值的引用,加载的时候会做静态优化

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

ES6模块功能主要由两个命令构成:import和export。import命令用于输入其他模块提供的功能。export命令用于规范模块的对外接口。

export的几种用法:

// 输出变量

export const name = 'Clearlove';

export const year = '2021';

// 输出一个对象(推荐)

const name = 'Clearlove';

const year = '2021';

export { name, year}

// 输出函数或类

export function add(a, b) {

return a + b;

// export default 命令

export default function() {

console.log('foo')

import导入模块:

// 正常命令

import { name, year } from './module.js';

// 如果遇到export default命令导出的模块

import ed from './export-default.js';

模块编辑好之后,它有两种形式加载:

浏览器加载

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

外链js文件:

<script type="module" src="index.js"></script>

内嵌在网页中

<script type="module">

import utils from './utils.js';

// other code

</script>

对于加载外部模块,需要注意:

代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见

模块脚本自动采用严格模式,不管有没有声明use strict

模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口

模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的

同一个模块如果加载多次,将只执行一次

Node加载

Node要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定use strict。

如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为

"type": "module"

一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。

$ node my-app.js

如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。

总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。

注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import。

Node的import命令只支持异步加载本地模块(file:协议),不支持加载远程模块。

总结

由于 ESM 具有简单的语法,异步特性和可摇树性,因此它是最好的模块化方案

UMD 随处可见,通常在 ESM 不起作用的情况下用作备用

CommonJS 是同步的,适合后端

AMD 是异步的,适合前端

继续阅读