前言:本文讲述前端代码从脚本全局函数时代到现代化模块引入时代的发展历程,再介绍构建工具对代码予以指定条件下的打包构建,最后利用vite打包工具打包一个可供多个环境使用的SDK的例子。
一、模块化演变历程
1.1 脚本全局函数
在HTML页面所引入的脚本中定义函数,在浏览器执行脚本则可以使用脚本中所定义的全局函数
<body>
<script>
const globalFun = () => {
console.warn('this is globalFun');
};
</script>
</body>
缺点: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
1.2 namespace模式
通过命名空间来解决上述命名冲突的问题
<body>
<script>
const globalNameSpaceTest = {
fun: () => {
console.warn('this is globalNameSpaceTest');
},
};
</script>
</body>
缺点:
- 外部可以更改内部状态
- 所有模块代码都被暴露在外
1.3 IIFE (立即调用的函数表达式)
在JavaScript语言中,有一个立即执行函数,当脚本加载成功则执行所定义的函数。
<body>
<script>
(function () {
console.warn('IIFE code...');
console.info(this);
globalThis.IIFETestMoudle = {
fun: () => {
console.warn('some logic...');
},
};
})();
</script>
</body>
通过这种形式创建的模块可以自己独立的私有作用域
缺点:
- 模块之间的依赖依旧不明确,依旧是挂载到全局宿主环境上
1.4 Commonjs
CommonJS是Node.js环境下的模块化规范
两种导出方式
第一种导出方式:
const fun = () => {
console.warn("cjs fun...");
};
module.exports = fun;
const moduleTest = require("./module-test");
console.log(moduleTest);
第二种方式:
const fun = () => {
console.warn("cjs fun...");
};
module.exports.fun = fun;
上述两种方式都是对module.exports
赋值或是添加属性的形式来进行模块导出,那么对于在nodejs
当中,module
是什么呢?由截图可以看出当前文件模块可以依赖的nodejs
模块及其一些其他信息,其中:
exports
:当前模块导出的部分loaded
:当前模块是否加载完毕paths
:模块查找路径
特点
- 模块的加载是运行时同步加载的,所以在导入时会阻塞执行
- 模块的加载实际上对是所引入对象的一种深拷贝
模块加载机制
- 模块定义:在 CommonJS 规范中,每个文件被视为一个独立的模块。模块内部的变量都是局部变量,不会泄露到全局作用域。一个模块可以通过 require 函数来加载另一个模块,并通过 module.exports 或 exports 对象来导出功能。
- 模块加载:当一个模块需要使用另一个模块时,它会使用 require 函数来加载目标模块。require 函数接受一个模块标识符(通常是文件的路径或者模块的名称)作为参数,并返回该模块导出的对象。
- 模块解析:当调用 require 函数时,Node.js 会按照以下步骤解析和加载模块:
- 路径分析:首先检查模块标识符是否是内置模块(如 fs、http 等)。如果不是,Node.js 会解析相对或绝对路径。
- 文件定位:Node.js 会根据路径查找 .js、.json 或 .node 文件。如果路径没有文件扩展名,Node.js 会尝试加载支持的文件类型。
- 目录分析和包处理:如果模块标识符是一个目录,Node.js 将查找该目录下的 package.json 文件,解析它并查找 main 属性指定的文件。如果没有 package.json 或 main 属性,Node.js 将尝试加载目录下的 index.js。
- 模块缓存:每个模块在第一次加载后都会被缓存。这意味着无论 require 函数被调用多少次,模块都只会被执行一次,之后每次调用 require 都会返回相同的导出对象。这可以提高模块加载的效率并避免重复执行。
如果真有这种情况,可以手动清除缓存
1.5 ESM
ES模块(ECMAScript Modules
),通常简称为ESM
,是JavaScript
的官方标准模块系统。自ES6
(ECMAScript 2015
)开始,JavaScript
语言本身就内置了对模块的支持。
导入导出
const sum = (a, b) => a + b;
export { sum };
<body>
<script type="module">
import { sum } from "./module-es.js";
console.warn(sum(2, 3));
</script>
</body>
模块加载机制
- 静态结构:ES模块的一个关键特点是它们具有静态结构。这意味着import和export语句必须位于模块的顶层作用域,且不能动态生成或条件性地执行。这种静态结构使得模块的依赖关系在代码运行之前就已经确定,允许JavaScript引擎优化模块加载、解析和编译。
- 加载:浏览器环境,如果是结构化项目,可以直接引入,如果是script脚本引入需要添加type字段如上述例子;如果在nodejs环境,也需要在package.json文件中设置type字段,才能识别es模块的加载语法
- ES模块的加载是异步的。当模块被import时,它并不会立即执行,而是首先完成加载和解析过程,然后按照需要的顺序执行。这种方式适用于浏览器环境,因为它允许非阻塞的并行加载。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
那么怎么才能实现动态加载呢?
动态引入
- 条件加载:可以根据运行时条件来决定是否导入某个模块,这对于减少首次加载时间和优化性能非常有用。
- 代码分割和懒加载:结合现代前端构建工具(如Webpack、Rollup或Parcel),import()可以用于实现代码分割,按需加载模块,从而提高应用的启动速度和响应性。
- 与其他API和模块化功能集成:import()函数可以和其他Web API结合使用,如Service Workers、Web Workers、caches等。
<body>
<script type="module">
const loadModule = async () => {
try {
const module = await import("./module-es.js");
console.warn(module.sum(2, 3));
} catch (error) {
console.error("Module loading failed: ", error);
}
};
setTimeout(() => loadModule(), 3000);
</script>
</body>
1.6 ESM 与 CommonJS 的差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码, import 时采用静态命令的形式。即在 import 时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
二、打包构建
2.1 构建工具
实现各种环境下都能使用模块化代码,需要使用打包工具来对一套代码进行打包,打包工具主流有:
- webpack:适合应用类打包,可以进行详细配置和复杂构建过程的大型项目
- vite:适合工具类以及SDK打包,更加适合现代化web应用
本文暂不详述webpack和vite的差异,只针对模块化构建目标做一个概括,所以选用vite来构建一个在所有环境可使用的一个SDK。
UMD:并不是一种全新的模块系统,而是整合了无模块化、AMD、CommonJS三种模块规范,其可以在任何环境下工作
2.2 利用Vite构建一个SDK
目标构建一个node环境和浏览器环境都可以使用的sdk
src/inde.js
const sum = (a, b) => a + b;
export default sum;
vite.config.js
import { defineConfig } from "vite";
import path from "path";
export default defineConfig({
build: {
lib: {
entry: path.resolve(__dirname, "src/index.js"),
formats: ["es", "cjs", "umd"],
name: "sdk",
fileName: (format) => `sdk.${format}.js`,
},
},
});
在vite中构建多环境依赖只需要上述少量代码即可配置,配置了输出sdk的全局命名以及文件命名等配置,更多配置:
上述代码打包后的结果:
2.3 在不同环境使用SDK
浏览器环境使用:
<body>
<script src="./dist/sdk.umd.js"></script>
<script>
console.info(sdk);
console.warn(sdk(1, 2));
</script>
</body>
node
环境使用:
const sum = require("./dist/sdk.cjs.js");
console.warn(sum(1, 2));
node
环境(根目录package.json文件中指定type
字段为module
)使用:
import sum from "./dist/sdk.es.js";
console.warn(sum(1, 2));
评论区