目的
分析webpack打包后的文件js文件原理 ,并手写实现
前提条件
- 了解 CommonJS
- 了解 webpack 的作用以及基本使用方法
- 了解 javascript 基本知识
准备工作
css
webpack
src
a.js
index.js
package.json
创建webpack文件夹以及src / a.js / index.js
在webpack路径下执行 npm init
生成package.json文件
执行 npm i -D webpack@4 webpack-cli@3
安装webpack4版本(4版本分析打包后代码比较直观 ,看懂4版本后再安装5版本分析)
配置package.json自定义脚本
json
"scripts": {
"build": "webpack --mode=production",
"build:dev": "webpack --mode=development"
}
执行 npm run build:dev
, 会生成dist文件夹 ,里面有webpack编译好的文件main.js
先不管生成的 main.js , 在dist文件夹中手动创建一个文件 my-main.js 。此时文件目录如下:
css
webpack
node_modules
dist
main.js
my-main.js
src
a.js
index.js
package.json
a.js 文件内容
js
console.log('This is file a');
module.exports = 'file a content'
index.js文件内容
js
const a = require('./a')
console.log('This is file index.js')
console.log(a)
正文
思考:如果需要手动合并工程中的代码模块 ,应该怎么做 ?
第一回合
可以直接将模块中内容复制过来 ,并且将向对应的模块导入语句替换为模块导出的值 ,并且将模块导出语句删除
js
console.log('This is file a');
// module.exports = 'file a content' // 删除
console.log('This is file index.js')
// const a = require('./a') // 替换为 ↓
const a = "file a content"
console.log(a)
第二回合
工程复杂以后,会有很多模块 ,这样会有很多重名变量 ,并且会对全局变量造成污染
可以将每个模块的内容放在一个Object中 ,key为模块路径 ,value为function ,将对应模块内容放入function中
将这个Object作为入参 ,传入一个立即执行函数中 ,在立即执行函数中执行入口模块 ,这样就不会对全局变量造成污染了
js
(function (modules) {
// 执行入口模块index.js文件
})(
{
"./src/a.js": function () {
console.log('This is file a');
module.exports = 'file a content'
},
"./src/index.js": function () {
console.log('This is file index.js')
const a = require('./a')
console.log(a)
}
}
)
此时可以看到 ,手动合并初具规模 ,但是在浏览器环境并不支持CommonJS社区规范 ,所以需要手动构造一个模仿CommonJS的导入导出方法提供给Object中的模块使用
js
(function (modules) {
// 构造CommonJS 导入API
function require(moduleId) { // moduleId 为模块路径
const func = modules[moduleId]; // 得到对应模块内容
const module = { // 模拟构造CommonJS 导出API
exports: {}
}
func(module, module.exports, require) // 运行对应模块
const res = module.exports; // 此时module的值被对应模块执行后已经发生改变,得到模块导出结果
return res
};
// 执行入口模块
require('./src/index.js') // require函数相当于是运行一个模块,得到模块的导出结果
})(
{
"./src/a.js": function (module, exports) {
console.log('This is file a');
module.exports = 'file a content'
},
"./src/index.js": function (module, exports, require) {
console.log('This is file index.js')
// const a = require('./a') // 替换为 ↓
const a = require('./src/a.js')
console.log(a)
}
}
)
第三回合
如果给立即执行函数传入的Object有很多个模块的话 ,每次调用都执行一次都会很浪费资源 ,所以要将已经获取到的模块内容缓存起来 ,用的时候如果有缓存则直接返回 ,这样就是一个基本比较完美的解决方案了
js
(function (modules) {
// 缓存模块导入结果
const moduleExportsCache = {}
// 构造CommonJS 导入API
function require(moduleId) { // moduleId 为模块路径
if(moduleExportsCache[moduleId]) {
// 如果有缓存,直接返回缓存结果
return moduleExportsCache[moduleId]
}
const func = modules[moduleId]; // 得到对应模块内容
const module = { // 模拟构造CommonJS 导出API
exports: {}
}
func(module, module.exports, require) // 运行对应模块
const res = module.exports; // 得到模块导出结果
moduleExportsCache[moduleId] = res // 缓存导出模块
return res
};
// 执行入口模块
require('./src/index.js') // require函数相当于是运行一个模块,得到模块的导出结果
})(
{
"./src/a.js": function (module, exports) {
console.log('This is file a');
module.exports = 'file a content'
},
"./src/index.js": function (module, exports, require) {
console.log('This is file index.js')
// const a = require('./a') // 替换为 ↓
const a = require('./src/a.js')
console.log(a)
}
}
)
webpack打包后的文件分析
打开之前编译好的main.js文件 , 删掉注释以及一些兼容性配置以后 ,剩余的代码就是下面这样
可以看到和上面写的代码非常相似 ,不过webpack为了不和CommonJS规范冲突 ,将变量名改为了__webpack_require_ ,还有就是入参中的模块内容使用eval()执行 , 将每个eval里面的内容拆出来以后 ,得到以下代码
js
(function (modules) {
})
({
"./src/a.js":
(function (module, exports) {
console.log('This is file a');
module.exports = 'file a content';
// eval("console.log('This is file a');\r\nmodule.exports = 'file a content'\n\n//# sourceURL=webpack:///./src/a.js?");
}),
"./src/index.js":
(function (module, exports, __webpack_require__) {
const a = __webpack_require__("./src/a.js");
console.log('This is file index.js');
nconsole.log(a);
// eval("const a = __webpack_require__(/*! ./a */ \"./src/a.js\")\r\nconsole.log('This is file index.js')\r\nconsole.log(a)\r\n\n\n//# sourceURL=webpack:///./src/index.js?");
})
});
思考:为什么使用eval()执行模块中的内容 ?
- 了解eval()原理
- 将下面两段代码在浏览器环境中运行并根据报错点击到对应源码位置查看区别
js
eval("var b = null;\nb.fun()")
eval("var b = null;\nb.fun();\n//# sourceURL=./src/eval.js")
结尾
执行 npm run build
,生成main.js文件后 ,查看生产环境编译后的代码 ,可以看到webpack将变量名字变为简写 ,并且将代码压缩为一行且没有任何注释
尝试安装其他第三方库,例如:jquery , 在入口文件或其他模块导入,执行打包命令后观察编译后代码 ,同样webpack将jquery模块的内容放在立即执行函数的入参中 ,并使用eval执行