乐趣区

关于javascript:Javascript模块化详解

为什么须要 Javascipt 模块化?

前端的倒退突飞猛进,前端工程的复杂度也不可同日而语。原始的开发方式,随着我的项目复杂度进步,代码量越来越多,所需加载的文件也越来越多,这个时候就须要思考如下几个问题:

  1. 命名问题:所有文件的办法都挂载到 window/global 上,会净化全局环境,并且须要思考命名抵触问题
  2. 依赖问题:script是程序加载的,如果各个文件文件有依赖,就得思考 js 文件的加载程序
  3. 网络问题:如果 js 文件过多,所需申请次数就会增多,减少加载工夫

Javascript模块化编程,曾经成为一个迫切的需要。现实状况下,开发者只须要实现外围的业务逻辑,其余都能够加载他人曾经写好的模块。

本文次要介绍 Javascript 模块化的 4 种标准: CommonJSAMDUMDESM

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 – 异步加载模块定义)标准,一个独自的文件就是一个模块。它采纳异步形式加载模块,模块的加载不影响它前面语句的运行。

这里异步指的是不梗塞浏览器其余工作(dom构建,css渲染等),而加载外部是同步的(加载完模块后立刻执行回调)

AMD也采纳 require 命令加载模块,然而不同于CommonJS,它要求两个参数:

require([module], callback);

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

AMD的规范中,引入模块须要用到办法 require,因为window 对象上没定义 require 办法,这里就不得不提到一个库,那就是 RequireJS。

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

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){
  return {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"
  }
});

require(["moduleB"], function(moduleB){moduleB.showFirstModuleName();
});

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

UMD

UMD 代表通用模块定义(Universal Module Definition)。所谓的通用,就是兼容了 CmmonJSAMD标准,这意味着无论是在 CmmonJS 标准的我的项目中,还是 AMD 标准的我的项目中,都能够间接援用 UMD 标准的模块应用。

原理其实就是在模块中去判断全局是否存在 exportsdefine,如果存在 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 环境下运行
  • 模块的导入导出,通过 importexport来确定
  • 能够和 CommonJS 模块混合应用
  • CommonJS输入的是一个 值的拷贝 。ES6 模块输入的是 值的援用, 加载的时候会做动态优化
  • CommonJS模块是 运行时加载 确定输入接口,ES6 模块是 编译时 确定输入接口

ES6 模块性能次要由两个命令形成:importexportimport 命令用于输出其余模块提供的性能。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 模块。

# 解释成 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

Nodeimport 命令只反对异步加载本地模块 (file: 协定),不反对加载近程模块。

总结

  • 因为 ESM 具备简略的语法,异步个性和可摇树性,因而它是最好的模块化计划
  • UMD 随处可见,通常在 ESM 不起作用的状况下用作备用
  • CommonJS 是同步的,适宜后端
  • AMD 是异步的,适宜前端
退出移动版