为什么须要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.jsconst 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.jsdefine(function(){  const name = "module A";  return {    getName(){      return name    }  }});// 定义一个moduleB.jsdefine(["moduleA"], function(moduleA){  return {    showFirstModuleName(){      console.log(moduleA.getName());    }  }});// 实现main.jsrequire(["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.jsrequire.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 是异步的,适宜前端