- 早期 立即调用函数闭包、类、对象实现
- Node有自己的模块系统
- ES6提出依托import、export的静态模块系统(不能实现模块的按需加载)
- 虽然浏览器很早就支持动态导入,但是直到ES2020,JavaScript才新增对动态模块支持
静态模块:各个模块之间的依赖关系图所涉及的所有imports和exports都是在执行之前即编译的时候resolve。事实上也有lazy loading或按需加载,即在运行时才进行模块加载。
模块化的核心是管理依赖
动态依赖:有的模块系统要求开发者在模块开始列出所有依赖,而有些模块系统允许开发者在程序结构中动态添加依赖。动态依赖可以支持更复杂的依赖关系,但是代价是增加了对模块进行静态分析的难度。
静态分析:模块发送到浏览器会被静态分析,分析工具会检查代码结构并在不执行代码的情况下推断其行为对静态分析,友好的模块系统可以让模块打包系统更容易将代码处理为更少的文件。
循环依赖 要构建一个没有循环依赖的JavaScript应用几乎不可能。在包含循环依赖的应用程序,模块加载顺序可能会出人意料,不过只要恰当地封装模块,使它们没有副作用,加载顺序就应该不会影响应用程序的运行。
模块化已经得到浏览器的普遍支持,但是开发者仍倾向于用代码打包工具,首次访问网站时候,相比加载多个小型模块,加载一个代码包用户体验更佳
模块概念
一开始通过命名空间组织代码,例如jQuery库将其API都放在window.$
- 命名空间冲突
- 无法合理管理项目的依赖和版本
- 无法方便控制依赖的加载顺序
模块标识符 把一个模块的返回值赋给一个变量,就是为模块创建了一个命名空间。
CommonJS同步(顺序)
SeaJS(模块加载器)推广的 ,概述了同步声明依赖的模块定义,主要用于服务器端实现模块化代码组织。
- requir()指定依赖 ,在Node.js模块标识符也有可能指向包含index.js的文件夹
- exports对象定义自己的公共API
var moduleB=require('./B');
module.exports={
stuff:moduleB.doStuff();
}
优点:代码可以复用于Node.js环境下并运行,例如做同构应用 缺点:无法直接运行在浏览器下,必须通过工具【e.g:Browserify浏览器端的前端打包工具】转换成标准
Node.js采用了轻微修改版本的CommonJS(Node.js主要在服务器环境下使用,不需要考虑网络延迟问题,可以一次性把模块全部加载到内存) 很多npm的第三方包也是采用该规范。
AMD异步(乱序)
require.js(模块加载器)推广的标准
异步模块定义,以浏览器为目标执行环境,需要考虑网络延迟问题,允许加载器库控制何时获取模块。一般是让模块声明自己的依赖,加载完成后立即执行依赖他们的模块。
实际JavaScript运行环境中没有原生支持AMD,需要先导入实现AMD的库后使用
//id为moduleA的模块定义
define('moduleA',['moduleB'],function(module){
return{
stuff:moduleB.doStuff();
};
});
//导入和使用
require(['module'],function(module){});
ADM也支持CommonJS风格定义模块,但AMD加载器会把他们识别成为原生AMD
UMD统一
统一上述两种生态系统,可以创建两种系统都可以使用的模块代码,本质上,UMD定义的模块会在启动的时候选择一种
ECMAScript6模块规范(ES6的一大创举)
JavaScript运行环境(浏览器、node)表示要原生支持模块语法,但是目前还是无法直接运行在大部分环境下,必须通过工具转换成标准的ES5
全方位简化了之前出现的模块加载器,原生浏览器支持意味着不需要加载器及其他预处理。浏览器可以从顶级模块加载整个依赖图
- 默认在严格模式下执行
- 不共享全局命名空间 ,每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域。
- 模块顶级this是undefined(常规脚本是window)
- 模块中var声明不会添加到window对象
- 异步加载和执行的
- 可以定义公共接口,其他模块基于接口观察和交互
除了打包工具,在浏览器中以原生的方式实现模块化: 带有type="module"属性的< script >元素成为模块脚本,视作模块图中的入口模块,一个页面的入口模块数目没有限制。添加defer async属性优化加载性能
- 模块的安全限制:模块脚本相比普通脚本,存在跨域加载的限制。只能从包含模块的HTML文档所在的域加载模块,除非服务器添加了适当的CORS头允许跨源加载。普通的script可以从互联网上的所有服务器加载JavaScript文件,而互联网广告、分析、追踪代码都依赖于此
- 也是因为这样,在使用模块时候,必须要启动一个静态web来测试,不能用file:URL测试。
export 命令可以出现在模块的任何位置,但必需处于模块顶层。 import 命令会提升到整个模块的头部,首先执行。
/*-----export [test.js]-----*/
let myName = "Tom";
export { myName as exportName }
export { myName, myAge, myfn, myClass }
import { myName as name1 } from "./test1.js";
export default
// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}
import {a, b} from './a.js'
import XXX from './b.js'
import.meta.url
import.meta维护当前执行模块的元数据 import.meta.url是加载模块时使用的URL,主要应用是 引用和模块位于同一文件目录下的资源
ES2020动态加载模块
主要意义:代码分割
import、export指令导入都是静态的,静态模块导入需要等先加载所有模块再执行程序,不适于web应用中,因为web应用中所有的代码都需要通过网络获取。
虽然浏览器支持动态加载,但是一直到ES2020引入import(),JavaScript才开始支持动态加载。
传给import()一个模块标识符,会返回一个期约对象,动态导入完成就会产生一个模块对象。
//静态
import { myName as name1 } from "./test1.js";
//动态
import("./test1.js").then(stats=>{
let a=stats.mean(data)
});
import.meta.url维护着加载模块时使用的目录
react与动态加载
如果使用 Create React App,Next.js ,都支持动态模块特性而无需进行配置。
如果你自己配置 Webpack,你可能要阅读下 Webpack 关于代码分割的指南。你的 Webpack 配置应该类似于此。
当使用 Babel 时,你要确保 Babel 能够解析动态 import 语法而不是将其进行转换。对于这一要求你需要 @babel/plugin-syntax-dynamic-import 插件。
Node中的模块
在node中,每个文件都是一个拥有独立命名空间的模块,使用require和exports导入出。 module.exports对象
导入node的内置的系统模块
const fs=require(“fs”);//导入内置的系统文件
使用后缀名cjs、mjs区分模块风格
加载性能Tree Shaking
Tree Shaking 的概念很早就提出了,但当真正作用到 Javascript 中,是在 ES6 模块规范被提出之后,因为只有模块是通过 static 方式引用时(在打包的最初就必须知道哪些需要才能Shaking),Tree Shaking 才会起作用。Tree Shaking 在 Webpack 中得以实现,Tree Shaking 通常是和打包工具配合使用,例如 Webpack,只需在配置文件中设置mode即可。
在 ES6 模块规范之前,我们使用require()语法的 CommonJS 模块规范。这些模块是 dynamic 动态加载的,这意味着我们可以根据代码中的条件导入新模块。CommonJS 模块的这种 dynamic 性质意味着无法应用 Tree Shaking,因为在实际运行代码之前无法确定需要哪些模块。
反问? 这样一来不是所有的都需要静态加载了吗?是不是不够灵活? webpack的plugins可以对构建的各个生命周期进行操作,那么我们通过plugins引入对编译常量的判断,就可以根据编译时常量来条件导入新模块。编译常量的值取决于运行环境的配置。
if(condition){
require('./moduleA');
}