关于前端:一文带你了解-JS-Module-的始末

1次阅读

共计 15329 个字符,预计需要花费 39 分钟才能阅读完成。

写在后面

模块化开发是咱们日常工作耳濡目染中用到的基本技能,倒退至今十分地简洁不便,但开发者们(指我本人)却很少能清晰透彻地说出它的倒退背景,倒退过程以及各个标准之间的区别。故笔者决定一探乾坤,深入浅出学习一下什么是前端模块化。
通过本文,笔者心愿各位可能播种到:

  • 前端模块化倒退的大抵历史背景 🌟
  • 各个标准之间的根本个性和区别 🌟🌟
  • 着重深刻 ESM 和 CommonJs 的异同、优缺点 🌟🌟🌟
  • 深耕 CommonJS 和 ESM 的个性 🌟🌟🌟🌟

本文的重点会以大家熟知的 CommonJSESM 动手,深入浅出,联合示例 Demo 和一些小故事,心愿给大家可能带到不一样的体验。

一、前端模块化背景

某个技术的起源简直都是为了解决一些辣手的问题,模块化也不例外。上面以一个简略的例子来给大家讲个故事,通过故事给大家讲一讲大抵的发展史。故事并未涵盖所有工夫线上产生的事件,家喻户晓在前端模块化的长河里 AMD 和 CMD 始终打的不可开交,这里笔者筛选以 CMD 为干线向大家阐释。

本故事的攥写参考了局部 Sea.js 开源大佬发表在《程序员》杂志 2013 年 3 月刊的文章 (侵删)

在线链接:前端模块化开发的价值,本文举荐大家仔细阅读,包含评论区。

故事开始!在很久之前 (可能就是 2012 年之前),JS 模块化概念并未诞生的年代,前端开发们面临诸多问题:Web 技术虽说日益成熟、JS 能实现的性能也愈发地多,但与此同时代码量也是越来越大。那个年代往往会呈现一个我的项目各个页面专用同一个 JS 的状况,为了解决这个状况,JS 文件呈现了按性能拆分 ….
缓缓地,我的项目代码变成了如下:

...
...
<script src="util/wxbridge.js"></script>
<script src="util/login.js"></script>
<script src="util/base.js"></script>
<script src="util/auth.js"></script>
<script src="util/logout.js"></script>
<script src="util/pay.js"></script>
...

拆分进去的代码相似于如下:

function mapList(list) {// 具体实现}

function canBuyIt(goodId) {// 具体实现}

看似拆分很细,但却有诸多的致命问题:

  • 全局变量净化:各个文件的变量都是挂载到 window 对象上,净化全局变量;
  • 变量可能重名:不同文件中的变量如果重名,后一份会笼罩后面的,造成谬误;
  • 文件依赖程序:多个文件之间存在依赖关系,须要保障肯定加载程序问题重大 ……

拿上述 util 工具函数文件举例!大家按标准模像样地把这些函数对立放在 util.js 里,须要用到时,间接引入该文件就好,十分不便。随着团队我的项目越来越大,问题随之越来越多:

空山:我想定义 mapList 办法遍历商品列表,然而曾经有了,很烦,我的只能叫 mapGoodsList 了。
空河:我自定义了一个 canBuyIt 办法,为什么应用的时候,空山的代码出问题了呢?
满山:我明明都用了空山的办法,为什么后果还是不对呢?

通过团队强烈探讨,决定参照 Java 的形式,用 命名空间 来解决,于是乎代码变成了如下:

// 这是新的 Utils.js 文件

var userObj = {};
userObj.Auth = {};
userObj.Auth.Utils = {};

userObj.Auth.Utils.mapGoodsList = function (list) {// 实现};

userObj.Auth.Utils.canBuyIt = function (goodId) {// 实现};

当初通过命名空间的形式极大地解决了一部分抵触,然而认真看下面的代码,如果开发人员想要调用某一个简略的办法,就须要他有弱小的记忆力,集体累赘变得很重。(这里值得提一嘴的是,Yahoo 的前端团队 YUI 采纳了命名空间的解决形式,同时也通过利用沙箱机制很好的解决了命名空间过长的问题,有趣味的同学能够自行理解)

书接上回。大家当初能够基于 util.js 开发各自的 UI 层通用组件了。举一个大佬写的 dialog.js 组件

<script src="util.js"></script>
<script src="dialog.js"></script>
<script>
  org.CoolSite.Dialog.init({/* 传入配置 */});
</script>

可是无论大佬怎么写文档,以及如许郑重地发邮件宣告,时不时总会有共事询问为什么 dialog.js 有问题。通过一番排查,发现导致谬误的起因常常是在 dialog.js 前没有引入 util.js。这样的问题和依赖仍然还在可控范畴内,然而当我的项目越来越简单,泛滥文件之间的依赖常常会让人抓狂。上面这些问题,在过后每天都在实在地产生着:

  1. 通用组更新了前端根底类库,却很难推动全站降级。
  2. 业务组想用某个新的通用组件,但发现无奈简略通过几行代码搞定。
  3. 一个老产品要上新性能,最初评估只能基于老的类库持续开发。
  4. 公司整合业务,某两个产品线要合并。后果发现前端代码抵触。
  5. ……

以上很多问题都是因为 文件依赖 没有很好的治理起来。在前端页面里,大部分脚本的依赖目前仍旧是通过人肉的形式保障。当团队比拟小时,这不会有什么问题。当团队越来越大,公司业务越来越简单后,依赖问题如果不解决,就会成为大问题。文件的依赖,目前在绝大部分类库框架里,比方国外的 YUI3 框架、国内的 KISSY 等类库,目前是通过配置的形式来解决。抛一个例子,不深究。

YUI.add('my-module', function (Y) {// ...}, '0.0.1', {requires: ['node', 'event']
});

下面的代码,通过 requires 等形式来指定以后模块的依赖。这很大水平上能够解决依赖问题,但不够优雅。当模块很多,依赖很简单时,繁缛的配置会带来不少隐患。解决命名抵触和文件依赖,是前端开发过程中的两个经典问题,大佬们心愿通过模块化开发来解决这些问题,所以 Sea.js 营运而生,再往后,CMD 标准也就瓜熟蒂落地造成了。(精确说来是因为先有了优良的 Sea.js,才在后续更替过程逐步造成了咱们起初人所学习到的 CMD 标准。)

故事讲到这里要告一段落了,是时候给大伙来个评书总结了。JS 在设计上其实并没有 模块 的概念,为了让 JS 变成一个功能强大的语言,业界大佬们各显神通,定了一个名为 CommonJS 的标准,实现了一个名为模块 的货色。但惋惜过后环境下大多浏览器并不反对,只能用于 node.js,于是 CommonJS 开始决裂,变异了一个名为 AMD 标准的模块,能够用于浏览器端。因为 AMD 与 CommonJS 标准相去甚远,于是 AMD 自立门户,并且推出了 require.js 这个框架,用于实现并推广 AMD 标准。此时,CommonJS 的拥护者认为,浏览端也能够实现 CommonJS 的标准,于是稍作改变,推出了 sea.js 这个框架并造成了 CMD 标准。
正在 AMD 与 CMD 打得火热的时候,ECMAScript6 给 JS 自身定了一个模块加载的性能,弯道超车:“你们俩别争了,JS 模块有原生的语法了”。
再起初,正因为 AMD 与 CommonJS 如此不同,且用于不同的环境,为了可能兼容两个平台,UMD 就应运而生了,不过它仅仅是一个 polyfill,以兼容两个平台而已,严格意义上来说不能成为一种标准规范。

至此,大抵历史背景已讲述结束,上文呈现的各大标准名词,接下来会跟大家见面。

二、模块化标准介绍

大抵理解背景之后,接下来认真地跟各位探讨一下各大标准。
开始之前,想阐明一下,针对于 AMD 和 CMD,笔者不打算带各位做源码级别的深究,笔者心愿大家只是做一个理解或回顾,随后将重心放至第三、四章的 CommonJSEMS 中。

老大哥 CommonJS

介绍

2009 年,美国程序员 Ryan_Dahl 发明了 node.js 我的项目,将 JS 用于服务器端编程。这标记《JS 模块化编程》正式诞生。不同于纯前端的服务器端,是肯定要有模块的概念的,它与操作系统或其余应用程序有着各种各样的互动,否则编程会大受限制,甚至根本无法编程。
Node.js 后端编程中最重要的思维之一就是“模块”,正是这个思维,让 JavaScript 的大规模工程成为可能。也是基于此,随后在浏览器端,require.js 和 sea.js 之类的工具包也呈现了;在 ES module 被齐全实现之前,CommonJs 统治了之前时代模块化编程的大半江山,它的呈现也补救了过后 JS 对于模块化没有统一标准的缺点。

简略举例 🌰

在 CommonJS 中, 模块通常应用 module.exportsexports,有一个全局性办法 require(),用于加载模块,如下:(module.exports 和 exports 后文有做论述,此处暂且不表)

// 导出  a.js
module.exports = function sumIt(a,b){return a + b}

// 引入  main.js
const sumIt = require('./a.js');
console.log('sumIt===', sumIt(1,2));

AMD 自立门户

简介

AMD — Asynchronous Module Definition(异步模块定义)。它诞生于 Dojo 在应用 XHR+eval 时的实践经验,其支持者心愿将来的解决方案都能够免受因为过来计划的缺点所带来的麻烦。因为 CommonJS 奠定了服务器模块标准,大家便开始思考客户端模块,而且想两者能够兼容,让一个模块能够同时在服务器和浏览器运行。
然而 CommonJS 是同步加载模块,服务器所有模块都寄存在本地,硬盘读取工夫很快,但对于浏览器来说,等待时间则取决于网速的快慢,如果工夫过长,浏览器可能会处于“假死”。例如刚刚 main.js 的代码,当咱们调用 sumIt(1,2) 的时候, 浏览器须要期待 a.js 加载完能力进行计算,所以浏览器端的模块化应用同步加载是有缺点的,需用异步加载取代之,这也就是 AMD 标准诞生的背景。
AMD 采纳异步形式加载模块,让模块的加载不影响它前面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载实现之后,这个回调函数才会运行。

AMD 标准详览看这里
AMD 模块的设计模式请看这里

简略举例 🌰

define(id?, dependencies?, factory)
// id: 字符串,模块名称(可选)// dependencies: 示意须要加载的依赖模块(可选)// factory: 工厂办法,返回一个模块函数,也可了解为加载胜利后的回调函数
// 引入依赖,回调函数通过形参传入依赖
define(['Module1',‘Module2’], function (Module1, Module2) {function testIt () {
      /// 业务代码
      Module1.test();}
  return testIt
});
require([module],callback())
define(function (require, exports, module) {var yourModule = require("./yourModule");
    yourModule.test();
    exports.yourKey = function () {//...}
});

不难发现,AMD 的长处是适宜在浏览器环境中异步加载模块。能够并行加载多个模块。
而毛病是进步了开发成本,并且不能按需加载,而是必须提前加载所有的依赖。

CMD — 简略纯正

简介

Common Module Definition 背景有讲,不多赘述,Sea.js 在推广中对模块定义的规范化产出,推崇依赖就近,提早执行

简略举例 🌰

//AMD
define(['./a','./b'], function (a, b) {
    // 依赖一开始就写好
    a.xxx();
    b.xxx();});

//CMD
define(id?, function (requie, exports, module) {
    // 依赖能够就近书写
    var a = require('./a');
    a.xxx();

    // 软依赖
    if (status) {var b = requie('./b');
        b.xxx();}
});

// require 是一个办法, 用来获取其余模块提供的接口

// exports 是一个对象, 用来向外提供模块接口

// module  是一个对象, 下面存储了与以后模块相关联的一些属性和办法

CMD 标准看这里

AMD 和 CMD 比照

  1. 对于依赖的模块 AMD 是 提前执行,CMD 是 提早执行。不过 Require.js 从 2.0 开始,也改成能够提早执行(依据写法不同,解决形式不通过)。
  2. AMD 推崇 依赖前置(在定义模块的时候就要申明其依赖的模块),CMD 推崇 依赖就近(只有在用到某个模块的时候再去 require —— 按需加载)。
  3. AMD 的 api 默认是一个当多个用,CMD 严格的辨别推崇职责繁多。例如:AMD 里 require 分全局的和部分的。CMD 外面没有全局的 require, 提供 seajs.use() 来实现模块零碎的加载启动。CMD 里每个 API 都更简略纯正。援用一下玉伯 2012 年的自评:

简谈下 — UMD

网络上对于 UMD (Universal Module Definition) 通用模块标准的说法形形色色,这里笔者不做任何评论,只做一个通用型认知的总结: UMD 像一种 polyfill,兼容反对多个模块标准。
参考援用:点这里能够看一下娜娜对于 UMD 的解释
UMD 理念、标准等官网材料:https://github.com/umdjs/umd
看一个简略的例子:

output: {path: path.join(__dirname),
    filename: 'index.js',
    libraryTarget: "umd",// 此处是心愿打包的插件类型
    library: "Swiper",
}

看一下打包之后:

!function(root,callback){
"object"==typeof exports&&"object"==typeof module?// 判断是不是 nodejs 环境
    module.exports=callback(require("react"),require("prop-types"))
    :
    "function"==typeof define&&define.amd?// 判断是不是 requirejs 的 AMD 环境
        define("Swiper",["react","prop-types"],callback)
        :"object"==typeof exports?// 相当于连贯到 module.exports.Swiper
            exports.Swiper=callback(require("react"),require("prop-types"))
            :
            root.Swiper=callback(root.React,root.PropTypes)// 全局变量
}(window,callback)

新大哥 ESM

应用 Javascript 中一个规范模块零碎的计划。
在此之前的期间,社区在经验了 AMD 和 CMD 洗礼后提出了一种想法:既然都是 JS 标准,Node.js 模块能被浏览器环境下的 JS 代码随便援用吗?能!本着这个想法,ES6 (ECMAScript 6th Edition, 起初被命名为 ECMAScript 2015) 于 2015 年 6 月 17 日 横空出世,次要被人熟知的其中一个个性就是 es6 module, 下文简称为 ESM。具体深耕内容请详见第四章,在此介绍章节不过多赘述。

import React from 'react';
import {a, b} from './myPath';
......
export default {
  function1,
  const1,
  a,
  b
}
  1. 在很多古代浏览器能够应用
  2. 它兼具两方面的长处:具备 CJS 的简略语法和 AMD 的异步
  3. 得益于 ES6 的动态模块构造,能够进行  Tree Shaking
  4. ESM 容许像 Rollup 这样的打包器删除不必要的代码,缩小代码包能够取得更快的加载
  5. 能够在 HTML 中调用,如下

    <script type="module">
      ...
      import {test} from 'your-path';
      test();
      ...
    <script/>

三、CommonJS 的深耕

CJS 的简略应用

先看一个简略的 Demo:

let str = 'a 文件导出'
module.exports = function logIt  (){return str}
const logIt = require('./a.js')
module.exports = function say(){
    return {name: logIt(),
        sex: 1
    }
}

以上便是 CJS 最简略的实现,那么当初咱们要带着问题了:

  1. module.exports,exports 的本质区别是什么?🤔🤔🤔
  2. require 的加载设计是怎么的?🤔🤔🤔
  3. CJS 的优缺点和与 ESM 的异同是什么?🤔🤔🤔

CJS 的实现原理

每个模块文件上存在 module,exports,require 三个变量(在 nodejs 中还存在 __filename 和 __dirname 变量),然而这几个变量是没有被定义的,然而咱们能够在 Commonjs 标准下每一个 JS 模块上间接应用它们。

  • module 记录以后模块信息。
  • require 引入模块的办法。
  • exports 以后模块导出的属性
  • __dirname 在 node 中示意被执行 js 文件的绝对路径
  • __filename 在 node 中示意被执行 js 文件的文件名

在编译过程中,Commonjs 会对 JS 的代码块进行包装, 以上述的 b.js 为 🌰,包装之后如下:

(function(exports,require,module,__filename,__dirname){const logIt = require('./a.js')
    module.exports = function say(){
        return {name: logIt(),
            sex: 1
        }
    }
})

如何执行包装的呢?让咱们来看看包装函数的实质:

function wrapper (script) {return '(function (exports, require, module, __filename, __dirname) {' + 
        script +
     '\n})'
}

// 而后是包装函数的执行
const modulefunction = wrapper(`
  const logIt = require('./a.js')
    module.exports = function say(){
        return {name: logIt(),
            sex: 1
        }
    }
`)

script 为咱们在 js 模块中写的内容,最初返回的就是如上包装之后的函数。当然这个函数暂且是一个字符串。在模块加载的时候,会通过 runInThisContext (能够了解成 eval) 执行 modulefunction,传入 require,exports,module 等参数。最终咱们写的 node.js 文件就执行了。(实在的 runInThisContext 函数执行思路和上述统一,但实现细节不一样)

 runInThisContext(modulefunction)(module.exports, require, module, __filename, __dirname)

实现详情请参照官网文档:runInThisContext 的官网文档和示例
到此,整个模块执行的原理大抵梳理结束。🎉🎉

require 的文件加载流程

先以 node.js 为例,看一个简略的代码片段

const fs = require('fs');  
const say = require('./b.js');
const moment = require('moment');

先对文件模块做一个简略的分类:

  • fs 为 nodejs 底层的外围模块,其余常见的还有 path、http 模块等;
  • b.js 为咱们编写的文件模块;
  • ./ 和 ../ 作为 相对路径 的文件模块,/ 作为 绝对路径 的文件模块。
  • moment 为自定义模块,其余常见的还有 crypto-js 等;像此类非门路模式也非核心的模块,将作为自定义模块。

当 require 办法执行的时候,接管的惟一参数作为一个 标识符
CJS 下对不同的标识符解决流程不同,然而目标都是找到对应的模块。

require 标识符加载准则

此章节借鉴了 @我不是外星人 的优良文章中的局部内容(侵删)
在线链接:《深入浅出 Commonjs 和 Es Module》
笔者在伟人的肩膀上做了一些 Curd 润色,供大家享受 😄

  • 缓存加载:曾经被加载过一次的模块,会被记录放入缓存中;
  • 外围模块:优先级仅次于 缓存加载,在 Node 源码编译中,已被编译成二进制代码,所以加载外围模块速度最快;
  • 门路模块:已 ./,../ 和 / 开始的标识符,会被当作文件模块解决。require() 办法会将门路转换成实在门路,并以实在门路作为索引,将编译后的后果放入缓存,不便二次加载。
  • 自定义块:在当前目录下的 node_modules 目录查找。如果没有,在父级目录的 node_modules 查找 …… 直到根目录下的 node_modules 目录为止。在查找过程中,会找 package.json 下 main 属性指向的文件,如果没有 package.json,在 node 环境下会以此查找 index.js,index.json,index.node。
  • 从 Node.js 12+ 起,加载第三方模块时,exports 字段优先级比 main 字段要高

require 模块引入与解决

CommonJS 模块同步加载并执行模块文件,CommonJS 模块在执行阶段剖析模块依赖

const logIt = require('./b');
console.log('我是 a 文件');
exports.say = function(){const message = logIt();
    console.log(message);
}
const say = require('./a');
const obj = {
   name:'b 文件的 object 的 name',
   author:'b 文件的 object 的 author'
}
console.log('我是 b 文件');
module.exports = function(){return obj}
const a = require('./a');
const b = require('./b');

console.log('我是 main 文件');

运行一下:

🤔️🤔️🤔️ 问题:

  • main.js 和 a.js 模块都援用了 b.js 模块,然而 b.js 模块为什么只执行了一次?
  • a.js 模块 和 b.js 模块相互援用,然而为什么没有循环援用报错?

咱们先引入一个上文并未提及的概念:Module 和 module
module:在 Node 中每一个 js 文件都是一个 module,module 上保留了 exports 等信息之外,
还有一个 loaded(boolean 类型)示意该模块是否曾经被加载过。
Module:以 nodejs 为例,整个零碎运行之后,会用 Module 缓存每一个模块加载的信息。

而后,在答复上述思考问题之前,一起来看一下阮一峰老师对于 require 的源码解读:

 // id 为门路标识符
function require(id) {
   /* 查找  Module 上有没有曾经加载的 js  对象 */
   const  cachedModule = Module._cache[id]
   
   /* 如果曾经加载了那么间接取走缓存的 exports 对象  */
  if(cachedModule){return cachedModule.exports}
 
  /* 创立以后模块的 module  */
  const module = {exports: {} ,loaded: false , ...}

  /* 将 module 缓存到  Module 的缓存属性中,门路标识符作为 id */  
  Module._cache[id] = module
  /* 加载文件 */
  runInThisContext(wrapper('module.exports ="123"'))
  (module.exports, require, module, __filename, __dirname)
  /* 加载实现 *//
  module.loaded = true 
  /* 返回值 */
  return module.exports
}

代码还是非常容易了解的,解读总结如下:

require 会接管一个参数(文件标识符),而后剖析定位文件(上一大节曾经讲到),接下来从 Module 上查找有没有缓存,如果有缓存,那么间接返回缓存的内容。

如果没有缓存,会创立一个 module 对象,缓存到 Module 上,而后执行文件;加载完文件,将 loaded 属性设置为 true,而后返回 module.exports 对象。

模块导出其实跟 a = b 赋值一样:根本类型导出的是值,援用类型导出的是援用地址。(exports 和 module.exports 持有雷同援用,后文会专门解读)

require 防止循环援用

咱们先来剖析刚刚的例子,上面先用一幅图来示意 a.js 的加载流程:

了解了这幅流程图后,再来看残缺的流程图就不再吃力了:

此时咱们须要留神一点:
当咱们第一次执行 b.js 模块的时候,a.js 还没有导出 say 办法,所以此时在 b.js 同步上下文中,是获取不到 say 的,那么如果想要获取 say,方法有两个:

异步加载

const say = require('./a');
const obj = {
   name:'b 文件的 object 的 name',
   author:'b 文件的 object 的 author'
}
console.log('我是 b 文件');

setTimeout(()=>{console.log('异步打印 a 模块' , say)
},0)

module.exports = function(){return obj}

动静加载

console.log('我是 a 文件');
exports.say = function(){const logIt = require('./b');
    const message = logIt();
    console.log(message);
}
const a = require('./a');
a.say();

由此咱们可见:
require 实质上就是一个函数,那么函数能够在任意上下文中执行,自在地加载其余模块的属性办法。

require 防止反复加载

正如上述所言,加载之后的文件的 module 会被缓存到 Module 上,比方一个模块曾经 require 引入了 a 模块,如果另外一个模块再次援用 a,那么会间接读取缓存值 module,所以无需再次执行模块。

对应 demo 片段中,首先 main.js 援用了 a.js,a.js 中 require 了 b.js。此时 b.js 的 module 放入缓存 Module 中,接下来 main.js 再次援用 b.js,那么间接走的缓存逻辑,所以 b.js 只会执行一次,也就是在 a.js 引入的时候,由此就防止了反复加载。

🤔🤔🤔🤔🤔🤔 这里给大家抛一个思考问题:

// a.js
const b = require('./b');
console.log('我是 a 文件',b);
const tets =  Object.getPrototypeOf(b);
tets.aaa = 'new aaa test';

// b.js
console.log('我是 b 文件');
module.exports = {str: 'bbbb'}

// main.js
require('./a');
const b = require('./b');
console.log('b===', b);
console.log('proto===', Object.getPrototypeOf(b));

🤔🤔🤔 看完这个事例,你有什么启发吗?是不是和第三方侵入式的工具库很像呢?

exports 和 module.exports

module.exports 和 exports 在一开始都是一个空对象 {},但实际上,这两个对象该当是指向同一块内存的。在不去扭转它们指向的内存地址的状况下,module.exports 和 exports 简直是等价的。

require 引入的对象实质上其实是 module.exports。那么这就产生了一个问题,当 module.exports 和 exports 指向的不是同一块内存时,exports 的内容就会生效。

module.exports = {money: '20 块 😭'};exports.money = '一伯万!!!😊';

这时候,require 实在失去的是 {money: ’20 块 😭’}。当他们二者 同时存在 的时候,会产生笼罩的状况,所以咱们通常最好抉择 exports 和 module.exports 两者之一。

  • 思考问题 1: 上述例子应用 exports = {money: ‘200’} 这种模式赋值对象能够吗?

答:不能够。通过上述解说都晓得 exports,module 和 require 作为形参的形式传入到 js 模块中。咱们间接 exports = {} 批改 exports,等于从新赋值了形参,然而不会在援用原来的形参。举个例子:

function change(myName){
    return myName.name = {name: '老板'}
}

let myName = {name: '小打工人'}

fix(myName);
console.log(myName);
  • 简略来说 module.exports 是给 module 外面的 exports 属性赋值,值能够是任何类型;
  • exports 是个对象,用它来裸露模块信息必须给它增加对应的属性;
  • 须要留神的是:module.exports 当导出一些函数等非对象属性的时候,也有一些危险,就比方循环援用的状况下。对象会保留雷同的内存地址,就算一些属性是后绑定的,也能通过异步模式拜访到。

四、ES Module 的深耕

导入和导出

// 导出 a.js
const name = 'jiawen'; 
const game = 'lol';
const logIt = function (){console.log('log it !!!')
}
export default { 
  name, 
  author,
  logIt
}


// 引入 main.js
import {name , author , logIt} from './a.js'

// 对于引入默认导出的模块,能够自定义名称。import allInfo from './a.js'

对于 ESM 标准中混合导出形式,日常应用,这里不再做举例。

提一下“重署名导入和重定向导出”:

import {name as newName , say,  game as newGame} from '/a.js';
console.log(newName , newGame , say);
export * from 'module'; // 1
export {name, author, ..., say} from 'module'; // 2
export {name as newName ,  game as newGame , ..., say} from '/a.js'; // 3 

只运行,不关怀导入:

import '/a.js' 

动静导入:

import asyncComponent from 'dt-common/src/utils/asyncLoad';

let lazy = (async, name) => {
    return asyncComponent(() => async.then((module: any) => module.default), {name}
    )
}

const ApiManage = lazy(import('./views/dataService/apiManage'), 'apiManage');
  • 动静导入 import(‘xxx’) 返回一个 Promise. 应用时可能须要在 webpack 中做相应的配置解决。

ESM 的动态语法

  • ES6 module 的引入和导出是动态的,import 会主动晋升到代码的顶层。动态的语法意味着能够在编译时确定导入和导出,更加疾速的查找依赖,能够应用 lint 工具对模块依赖进行查看,能够对导入导出加上类型信息进行动态的类型查看。
  • import , export 不能放在块级作用域或条件语句中。(谬误示范就不再举例了)
  • import 的导入名不能为字符串或在判断语句中,不能够用模版字符串拼接的形式。

ESM 的执行个性

  • 应用 import 导入的模块运行在 严格模式
  • 应用 import 导入的变量是 只读 的。(能够了解默认为 const 装璜,无奈被赋值)
  • 应用 import 导入的变量是与原变量 绑定 / 援用 的,能够了解为 import 导入的变量无论是否为根本类型都是援用传递,请看上面的例子:

    // js 中 根底类型是值传递
    let a = 1;
    let b = a;
    b = 2;
    console.log(a, b) // 1 2
    
    // js 中 援用类型是援用传递
    let a = {name: 'xxx'};
    let b = obj
    b.name = 'bbb'
    console.log(a.name, b.name) // bbb  bbb
    // a.js
    export let a = 1
    export function add(){a++}
    
    // main.js
    import {a, add} from './a.js';
    console.log(a); //1
    add();
    console.log(a); //2

ESM 的 import ()

刚刚曾经举过 import () 在 TagEngine 里理论利用的例子,其外围在于返回一个 Promise 对象,在返回的 Promise 的 then 胜利回调中,能够获取模块的加载胜利信息。上面举一些 import () 的社区罕用:

  • Vue 中的懒加载:

    [
    ...
     {
          path: 'home',
          name: '首页',
          component: ()=> import('./home') ,
     },
    ...
    ]
  • React 中的懒加载

    const LazyComponent = React.lazy(() => import('./text'));
    class index extends React.Component {render() {
          return (
              <React.Suspense
                  fallback={
                      <div className="icon">
                          <SyncOutlinespin />
                      </div>
                  }
              >
                  <LazyComponent />
              </React.Suspense>
          );
      }
    }
    

    import() 这种加载成果,能够很轻松的实现代码宰割, 防止一次性加载大量 js 文件,造成首次加载白
    屏工夫过长的状况。

ESM 的循环援用

// f1.js
import {f2} from './f2'
console.log(f2);
export let f1 = 'f1'

// f2.js
import {f1} from './f1'
console.log(f1);
export let f2 = 'f2'

// main.js
import {f1} from './f1'
console.log(bar)

此时会报错 f1 未定义,咱们能够采纳函数申明,因为函数申明会提醒到文件顶部,所以就能够间接在 f2.js 调用还没执行结束的 f1.js 的 f1 办法,但请不要在函数内应用内部变量 !!!!

// f1.js
import {f2} from './f2'
console.log(f2());
export function f1(){return 'f1'}

// f2.js
import {f1} from './f1'
console.log(f1());
export function f2(){return 'f2'}

// main.js
import {f1} from './f1'
console.log(f1)

Tree Shaking 和 DCE

DCE:dead code elimination。简称 DCE。死代码打消

Tree Shaking 在 Webpack 中的实现是用来尽可能的删除一些被 import 了但其实没有被应用的代码。

export let num = 1;
export const fn1 = ()=>{num ++}
export const fn2 = ()=>{num --}
import {fn1} from './a'
fn1();
  • 如上 a.js 中裸露两个办法,fn1 和 fn2,然而在 main.js 中,只用到了 fn1,那么构建打包的时候,fn2 将作为没有援用的办法,不被打包进来。
  • tree shaking 和“死代码剔除”是有本质区别的,“做一个🍰蛋糕,死代码剔除是扔一个鸡蛋进去,做好蛋糕后把鸡蛋壳拿进去;tree shaking 是先查看并尽可能地剔除没有用到的局部,比方鸡蛋壳,再去做蛋糕。”这二者还是有一些本质区别的。

五、ESM 与 CJS 的小结

CommonJS — 小结

  • CommonJS 模块由 JS 运行时实现。
  • CommonJs 是单个值导出,实质上导出的就是 exports 属性。
  • CommonJS 是能够动静加载的,对每一个加载都存在缓存,能够无效的解决循环援用问题。
  • CommonJS 模块同步加载并执行模块文件。

Es Module — 小结

  • ES6 Module 动态的,代码产生在编译时,不能放在块级作用域内,但能够动静导入。
  • ES6 Module 的值是动静绑定的,能够通过导出办法批改,能够间接拜访批改后果。
  • ES6 Module 能够导出多个属性和办法,能够单个导入导出,混合导入导出。
  • ES6 模块提前加载并执行模块文件,导入模块在严格模式下。
  • ES6 Module 的个性能够很容易实现 Tree Shaking 和 Code Splitting。

六、待探索的问题

此章节探讨 ESM 与 CMJ 的互转,👏欢送各位补充斧正!!!👏

特地鸣谢参考文章,排名不分先后:
https://github.com/amdjs/amdjs-api/wiki/AMD
https://github.com/seajs/seajs/issues/242
https://github.com/umdjs/umd
https://juejin.cn/post/6994224541312483336#heading-20
https://segmentfault.com/a/1190000017878394

🎉🎉🎉 完结 🎉🎉🎉

正文完
 0