Appearance
webpack
webpack 介绍
- webpack 是一个 JavaScript 应用程序的静态模块打包工具
入口(entry)
- 入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的
输出(output)
- output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件
loader
- webpack 只能理解 JavaScript 和 JSON 文件
- loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中
- 多个 loader,回从右往左执行
插件(plugin)
- loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量
commonjs 模块 简易实现
js
(() => {
let modules = {
"./src/title.js": (module) => {
// commonjs模块
module.exports = "title";
},
};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports, require);
return module.exports;
}
(() => {
let title = require("./src/title.js");
console.log(title);
})();
})();common.js 加载 common.js 模块
js
(() => {
let modules = {
"./src/title.js": (module, exports) => {
// commonjs模块
exports.name = "title_hl";
exports.age = "title_18";
},
};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports, require);
return module.exports;
}
(() => {
let title = require("./src/title.js");
console.log(title);
})();
})();common.js 加载 es6 模块
js
(() => {
let modules = {
"./src/title.js": (module, exports) => {
// es6 模块导出
require.r(exports);
require.d(exports, {
default: () => DEFAULT_EXPORTS, //值是一个getter
age: () => age,
});
// 默认导出
const DEFAULT_EXPORTS = "title";
// 命名导出
const age = 18;
},
};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports);
return module.exports;
}
// 判断对象是否具有某个属性
require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
// 给exports添加属性
require.r = (exports) => {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
Object.defineProperty(exports, "__esModule", { value: true });
};
// 绑定属性
require.d = (exports, definition) => {
for (let key in definition) {
// 如果definition有,exports没有,则绑定
if (require.o(definition, key) && !require.o(exports, key)) {
// getter
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
(() => {
let title = require("./src/title.js");
console.log(title);
})();
})();es6 模块加载 es6 模块
js
(() => {
let modules = {
"./src/title.js": (module, exports) => {
// es6模块的导出
require.r(exports);
require.d(exports, {
default: () => DEFAULT_EXPORTS, //值是一个getter
age: () => age,
});
// 默认导出
const DEFAULT_EXPORTS = "title";
// 命名导出
let age = 18;
// es module 获取的是最终的值
// 导出的值的引用
setTimeout(() => {
age = 20;
}, 1000);
},
};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports);
return module.exports;
}
// 判断对象是否具有某个属性
require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
// 给exports添加属性
require.r = (exports) => {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
Object.defineProperty(exports, "__esModule", { value: true });
};
// 绑定属性
require.d = (exports, definition) => {
for (let key in definition) {
// 如果definition有,exports没有,则绑定
if (require.o(definition, key) && !require.o(exports, key)) {
// getter
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
let exports = {};
(() => {
//只要打包前的模块是一个es module,那么就会调用require.r方法进行处理
require.r(exports);
let title = require("./src/title.js");
console.log(title);
})();
})();es6 模块加载 common.js 模块
js
(() => {
let modules = {
"./src/title.js": (module, exports) => {
// commonjs模块
module.exports = {
name: "title_name",
age: "title_age",
};
},
};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports);
return module.exports;
}
// 判断对象是否具有某个属性
require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
// 给exports添加属性
require.r = (exports) => {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
Object.defineProperty(exports, "__esModule", { value: true });
};
// 绑定属性
require.d = (exports, definition) => {
for (let key in definition) {
// 如果definition有,exports没有,则绑定
if (require.o(definition, key) && !require.o(exports, key)) {
// getter
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
require.n = (module) => {
var getter =
module && module.__esModule ? () => module["default"] : () => module;
return getter;
};
let exports = {};
(() => {
//只要打包前的模块是一个es module,那么就会调用require.r方法进行处理
require.r(exports);
let title = require("./src/title.js");
var title_default = require.n(title);
console.log(title_default());
console.log(title.age);
})();
})();模块异步加载
- 1、通过 chunkId 去查找对应的代码块文件其实就是 hello.main.js
- 2、通过动态创建
script标签加载 chunk 文件(JSONP 方式) - 3、加载完成后,会调用 webpackJsonpCallback 函数,将 chunk 文件中的模块添加到 modules 中,并且通过 installedChunks 记录 chunk 文件加载状态
js
[动态import()] → [生成chunkId] → [查找chunk文件] → [JSONP加载]
↑ ↓
[Promise返回] ← [触发回调] ← [执行webpackJsonpCallback] ← [chunk执行]js
let modules = {};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports);
return module.exports;
}
// 判断对象是否具有某个属性
require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
// 给exports添加属性
require.r = (exports) => {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
Object.defineProperty(exports, "__esModule", { value: true });
};
// 绑定属性
require.d = (exports, definition) => {
for (let key in definition) {
// 如果definition有,exports没有,则绑定
if (require.o(definition, key) && !require.o(exports, key)) {
// getter
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
require.f = {};
//返回此文件对应的访问路径
require.p = "";
//返回此代码块对应的文件名
require.u = function (chunkId) {
return chunkId + ".main.js";
};
//存放加载的代码块的状态
//key是代码块的名字
//0表示已经加载完成了
var installedChunks = {
main: 0,
//'hello': [resolve, reject, promise]
};
require.e = (chunkId) => {
let promises = [];
require.f.j(chunkId, promises);
return Promise.all(promises);
};
/**
* 通过JSONP异步加载一个chunkId对应的代码块文件,其实就是hello.main.js
* 会返回一个Promise
* @param {*} chunkId 代码块ID
* @param {*} promises promise数组
*/
require.f.j = function (chunkId, promises) {
//当前的代码块的数据
let installedChunkData;
//创建一个promise
const promise = new Promise((resolve, reject) => {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
promises.push(promise);
const url = require.p + require.u(chunkId);
require.l(url);
};
// 加载远程的模块
require.l = function (url) {
let script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
};
var chunkLoadingGlobal = (window["webpack5"] = []);
chunkLoadingGlobal.push = webpackJsonpCallback;
/**
*
* @param {*} chunkIds 代码块ID数组
* @param {*} moreModules 额外的模块定义
*/
function webpackJsonpCallback([chunkIds, moreModules]) {
const resolves = [];
for (let i = 0; i < chunkIds.length; i++) {
const chunkId = chunkIds[i];
resolves.push(installedChunks[chunkId][0]);
installedChunks[chunkId] = 0; //表示此代码块已经下载完毕
}
//合并模块定义到modules去
for (const moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
//依次取出resolve方法并执行 => promise resolve
while (resolves.length) {
resolves.shift()();
}
}
/**
* require.e异步加载hello代码块文件 hello.main.js
* promise成功后会把 hello.main.js里面的代码定义合并到require.m对象上,也就是modules上
* 调用require方法加载./src/hello.js模块,获取 模块的导出对象,进行打印
*/
require
.e("hello")
.then(require.bind(require, "./src/hello.js"))
.then((result) => {
console.log(result);
});webpack 编译流程
- 1、初始化参数:从配置文件和 Shell 语句中读取参数并且和 webpack
参数进行合并,得出最终的配置对象 - 2、用上一步得到的参数
初始化 Compiler对象 - 3、加载所有的
plugin配置的插件,调用插件的apply函数并且传递 Compiler 对象 - 4、执行 Compiler 对象的 run 方法开始执行编译
- 5、执行 Complication 对象的 build, 根据配置中的 entry 找出入口文件
- 6、从入口文件出发,调用所有配置的
Loader对模块进行编译 - 7、再找出该模块依赖的模块,再
递归直到所有入口依赖的文件都经过了本步骤的处理 - 8、根据入口和
模块之间的依赖关系,组装成一个个包含多个模块的 Chunk - 9、再把每个
Chunk 转换成一个单独的文件加入到输出列表 - 10、在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
module、chunk、bundlle
- module:各个源码文件,webpack 中一切皆模块
- chunk:多模块合并成的
- bundlle:最终的输出文件
Vite 为什么比 Webpack 启动速度快?
Vite
- 1、Vite 利用浏览器原生 ES Module 的支持,省略了打包这个步骤,直接启动开发服务器,当代码被请求时,根据需要动态编译模块
- 2、Vite 利用 esbuild 预编译依赖,esbuild 使用 Go 编写,比 js 编写的打包器预编译速度更快
Webpack
- 1、Webpack 一切皆为模块,启动后会从 entry 开始递归解析 entry 依赖的所有模块,在编译的过程中会根据配置的 Loader 对模块进行编译,编译完后再找出该模块依赖的模块,然后递归编译,所以 Webpack 启动会先编译 entry 依赖的所有模块,所以启动时间会慢
CommonJS 和 ES Module
| 特性 | CommonJS | ES Module |
|---|---|---|
| 导入语法 | require() | import |
| 导出语法 | module.exports / exports | export default / export |
| 加载方式 | 同步加载 | 异步加载 |
| 编译时机 | 运行时加载 | 编译时静态解析 |
| Node.js 支持 | 所有版本默认支持 | v12+ 支持,需 .mjs 扩展名或 "type": "module" |
关键差异说明:
加载机制
- CommonJS 在运行时同步加载模块(阻塞执行)
- ES Module 在编译阶段构建依赖关系图,异步加载
解析方式
javascript// CommonJS (动态) const module = condition ? "A" : "B"; const lib = require(module); // 运行时确定 // ES Module (静态) import lib from "./module.js"; // 编译时解析 - 路径必须是字面量!作用域
- CommonJS 导出值的拷贝(基础类型复制,引用类型共享地址)
- ES Module 导出值的值的引用(所有类型均为动态引用)
循环引用处理
- CommonJS:可能获得未初始化的中间状态
- ES Module:通过 "live binding" 机制保证最终一致性
浏览器兼容
- CommonJS 不能直接在浏览器运行(需打包工具转换)
- ES Module 被所有现代浏览器原生支持 (
<script type="module">)
Webpack 分包与拆包
Webpack 分包,也叫代码分割,是指将代码拆分成多个 bundle,实现按需加载或并行加载的技术。
它的核心价值主要有三点:
- 减少首屏加载时间:只加载当前页面需要的代码,显著提升首屏速度
- 提高缓存利用率:将不常变的第三方库单独打包,利用浏览器缓存
- 优化用户体验:按需加载减少初始下载量,非阻塞加载提升交互体验
在实际项目中,我们通常会对路由级别、大型组件和第三方库进行分包,实现最优的加载性能。
有哪些方式可以实现代码分割?
Webpack 提供了三种主要的代码分割方式:
第一种是入口起点:
javascriptmodule.exports = { entry: { app: "./app.js", admin: "./admin.js" }, };这种方式简单直接,但缺点是重复代码无法自动去重。
第二种是动态导入(最常用):
javascript// 使用 import() 语法 const loadModule = () => import("./module"); // React 项目中使用 const LazyComponent = React.lazy(() => import("./LazyComponent"));这是目前最推荐的方式,可以实现真正的按需加载。
第三种是 SplitChunksPlugin: 这是 Webpack 4 之后内置的插件,功能最强大,可以智能提取公共模块。
在实际项目中,我们通常会组合使用动态导入和 SplitChunksPlugin,达到最佳效果。
SplitChunksPlugin 的配置和使用
SplitChunksPlugin 是 Webpack 内置的智能分包插件,我通常这样配置:
javascriptmodule.exports = { optimization: { splitChunks: { chunks: "all", // 对所有模块进行分割 minSize: 20000, // 超过20KB才单独打包 cacheGroups: { // 第三方库 vendor: { test: /[\\/]node_modules[\\/]/, name: "vendors", priority: 10, }, // 公共模块 common: { name: "common", minChunks: 2, // 被2个以上chunk引用 priority: 5, }, }, }, }, };关键配置说明:
chunks: 'all'是最佳实践,会对所有类型模块进行分割minSize避免生成过小文件,减少请求数cacheGroups是核心,可以定义不同的提取策略在实际项目中,我还会进一步细分第三方库,比如把 React、Vue 这些基础框架单独打包,因为它们更新频率低,可以充分利用缓存。
你在项目中如何进行代码分割?有什么最佳实践?
在我的项目中,我采用分层分包策略:
第一层:路由级分割
javascript// 每个路由单独打包 const Home = lazy(() => import(/* webpackChunkName: "home" */ "./Home")); const About = lazy(() => import(/* webpackChunkName: "about" */ "./About"));第二层:组件级分割 对于超过 50KB 的大型组件或复杂功能模块,我会单独分包。
第三层:第三方库优化
javascriptcacheGroups: { react: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/ }, utils: { test: /[\\/]node_modules[\\/](lodash|moment)[\\/]/ } }
代码分割可能会带来什么问题?如何避免?
代码分割确实可能带来一些问题,我主要关注以下几点:
问题一:请求过多 过度分割会导致 HTTP 请求数增加,反而影响性能。 解决方案:合理设置
minSize(通常 20-30KB),避免生成过小 chunk。问题二:加载顺序问题 动态加载的模块可能存在依赖关系问题。 解决方案:使用 Webpack 的魔法注释确保依赖关系:
javascriptimport(/* webpackPreload: true */ "./critical-dep");