JS 模块化发展历史

2023/03/21 JavaScript 共 2451 字,约 8 分钟

背景

即使没有模块化,业务逻辑代码也是能正常运行的。模块化的目的是提高项目代码的可维护性,以至于代码不至于最后变成屎山代码,不可维护,模块化并不会直接面向业务逻辑场景

将代码拆分成独立的块,然后再把这些块连接起来可以通过模块模式来实现。这种模式背后的思想很简单:把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。不同的实现和特性让这些基本的概念变得有点复杂,但这个基本的思想是所有JavaScript模块系统的基础

在ES Module模块化标准出现以前,当我们需要在项目中添加多个依赖时,往往是通过大量的由上到下并列排布的 script 标签来实现的。

在 script 的执行就会阻塞文档对象模型(Document Object Model,即DOM对象)的解析,加载的脚本文件越多,页面完成初始化的时间就会越长,所以我们经常会看到 script 标签被放在 body 标签之后,这可让网站首屏的内容信息先完成解析渲染,再为页面增加交互,因为交互和逻辑能力的增加对用户而言在视觉上几乎是无感知的。尽管多个 script 标签看似将不同的代码块隔离到了不同的文件中,但这层代码就像窗户纸一样一捅就破,每一个由 script 标签引入的脚本文件实际上都是直接暴露在同一个全局作用域之下的,这就意味着如果参与合作的开发者在自己的脚本代码中使用了其他某个文件使用过的标识符,那么只有最后一个被引入的脚本中的定义会生效,而先引入的脚本中的定义全都会被覆盖掉,这就是著名的全局污染问题。

尽管HTML标准为 script 标签的async和defer这两个异步加载的属性使得加载脚本时可以不阻塞主线程,但浏览器在实现上并不是完全遵循标准的,每个浏览器在实现层面都会以自己的方式对加载和执行的过程进行优化

发展

为了满足代码隔离的基本需求,业内出现了以立即执行函数(Immediately Invoked Function Expression,IIFE)为模块包装的第一代模块化解决方案或者使用闭包(函数作用域)

再到后来有了下面的四种模块化规范

// 在AMD规范下引用模块
require(['axios'],function(axios){})

// 在CMD规范下引用模块
define(function(require){
   const axios = require('axios');
})

// 在CommonJS规范下引用模块
const axios = require('axios');

// 在ES Module规范下引用模块
import axios from 'axios'

现在一般用ES Module 规范比较多,偶尔也会用CommonJS规范

CommonJS规范(同步

它是一个JavaScript语言的服务端运行环境,Node.js对CommonJS模块化规范提供了原生支持,这就意味着使用JavaScript进行服务端开发时,不需要借助任何外部类库,就可以实现模块化管理。遗憾的是,要想让浏览器识别CommonJS模块,通常还需要依赖于构建工具注入的模块加载器代码来实现。

开发者不能同时使用这两种导出方式,因为exports和module.exports会指向内存中的同一个地址,且最终导出的模块会以module.exports为准

CommonJS中的require函数是同步执行的,它将根据Node.js原生提供的寻址策略来寻找模块的定义文件,找到后就会立即执行,require函数可以在代码中的任何地方调用,引用到某个模块时才会去执行相关的代码,这就意味着想要知道一个模块对外到底会导出哪些内容,需要等到运行时才行

CommonJS规范下却可以像使用普通函数一样为require函数传入动态路径

CommonJS模块输出的是一个值的复制,不可被修改,有缓存

Node.js也兼容了CommonJS和ES Module规范(从Node.js v13开始,package.json中声明了“type”:“module”的包都会采用ES Module标准进行解析

ES Module规范(异步

浏览器模块化规范

它同样支持具名模块导出和默认模块导出这两种形式,这两种模块的导出方式可以共存但不能混用,在使用import关键字引用模块时使用的语法也不同

import和export语句只能在顶层作用域中使用,加载器并不会直接运行脚本,它会先对代码进行静态类型检查,构建出完整的“依赖图谱”,获取并解析这些模块,然后才会从“依赖图”的末端开始执行模块代码,具名模块和默认模块互不干扰。

代码在依赖分析阶段并未运行,变量也还没有被赋值,所以无法使用动态路径来寻找模块

ES6模块输出的是值的引用,可以被修改

额外知识点 tree-shaking

性能优化技术——“tree-shaking”(摇树优化),需要使用ES Module规范中的语法来管理模块而不能使用CommonJS规范中的语法;因为CommonJS规范无法确定在实际运行前需要或者不需要某些模块,所以CommonJS不适合tree-shaking机制。ES6的import语法可以完美使用tree shaking,因为可以在代码不运行的情况下就能分析出不需要的代码。

tree shaking的原理是什么? 静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码

什么是 .cjs 和 .mjs ?

.cjs 代表使用 CommonJS 模块 ,require() 是同步加载的

.mjs 代表使用 ES 模块,ES 模块的 import() 是异步加载的

你 .cjs 的时候,那么就代表使用 CommonJs 模块规范,不能使用 ES 模块的 import 命令,.mjs 同理。

当你写了.js 就会去 package.json 文件中寻找你的 type 字段来当规范,如果没有 type 字段,默认为 CommonJs 规范

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


在技术的历史长河中,虽然我们素未谋面,却已相识已久,很微妙也很知足。互联网让世界变得更小,你我之间更近。

在逝去的青葱岁月中,虽然我们未曾相遇,却共同经历着一样的情愫。谁的青春不曾迷茫或焦虑亦是无奈,谁不曾年少过

在未来的日子里,让我们共享好的文章,共同学习进步。有不错的文章记得分享给我,我不会写好的文章,所以我只能做一个搬运工

我叫 sunseekers(张敏) ,千千万万个张敏与你同在,18年电子商务专业毕业,毕业后在前端搬砖

如果喜欢我的话,恰巧我也喜欢你的话,让我们手拉手,肩并肩共同前行,相互学习,互相鼓励

文档信息

Search

    Table of Contents