前言
前端日新月异,我们要不断学习新的知识文化才能跟上时代的步伐。
介绍
webpack现在是前端打包常用的工具。 今天分别使用 webpack.4x 和 webpack.5x 进行打包代码,对比看一下 webpack4、5 产出代码的区别。(以下简称 webpack4 、webpack5) webpack 官网
// 基本安装
mkdir webpack-demo && cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
首先来看只有一个 index.js 文件的 demo . 在 src 下新建一个入口文件 index.js .
先来看webpack4
// index.js
console.log("我是 webpack 4")
经过webpack打包之后变成了一个main.js文件,简单整理一下
// 打包后生成的的 main.js
(function(modules) {
// 模块的缓存
var installedModules = {};
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
// 构建 commonjs 模块标准
var module = installedModules[moduleId] = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
// 执行的入口函数
return __webpack_require__("./src/index.js");
})({
"./src/index.js": function(module, exports) {
console.log("我是 webpack 4")
}
})
可将上面代码,复制到浏览器验证一下 是所有的模块,每个文件对应一个模块,格式是-文件名:方法 最外层是一个立即执行函数,入参是所有的 modules(模块) list。传入的 modules 参数是一个的对象。 格式是 -> 文件名:方法。 key 是 index.js 文件的相对路径,value 是一个匿名函数,函数体里面就是咱们写在 index.js 里的代码。(这就是 webpack 加载模块的方式)
接下来,看一下这个函数怎么执行的:
首先定义了一个 installedModules 用来缓存模块,在函数 __webpack_require__ 执行时,会通过 moduleId 先判断是有此模块的缓存。
- moduleId:就是我们最外层立即执行函数的key
如果存在此模块直接返回缓存,
return installedModules[moduleId].exports;若不存在则声明一个 module 用来接收模块并进行缓存
var module = installedModules[moduleId] = {
exports: {}
};
等同于
installedModules["./src/index.js"] = module.exports = {}
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
这里是绑定this指向,并把参数传递给module函数,主要是用来收集 module 中所有的 export xxx 。但是我们这里没有用到 import xxx ,所以这里只是执行了 "./src/index.js" 的函数。
再来看一下 webpack 5
// index.js
console.log("我是 webpack 5")
经过webpack打包之后变成了一个main.js文件,简单整理一下
// 打包后生成的的 main.js
(() => {
console.log("我是 webpack 5")
})();
webpack 5的版本使用箭头函数,只用了一行代码
下面再增加一个同步文件 sync.js ,看看打包后有什么区别
先来看webpack 4
// sync.js
const data = '同步文件'
export default data
index.js 也做了修改
// index.js
import data from './sync.js'
console.log("我是", data)
console.log("我是 webpack 4")
整理一下打包后生成的 main.js
(function(modules) {
// 模块的缓存
var installedModules = {};
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
// 执行的入口函数
return __webpack_require__("./src/index.js");
})({
"./src/index.js": function(module, __webpack_exports__, __webpack_require__){
"use strict";
// import => 替换成 __webpack_require__
var _sync_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync.js");
console.log(_sync_js__WEBPACK_IMPORTED_MODULE_0__["default"])
console.log("我是 webpack 4")
},
"./src/sync.js": function(module, __webpack_exports__, __webpack_require__) {
// 1、__webpack_exports__ = module.exports = {}
// 2、__webpack_require__ 加载模块 转换 import
"use strict";
const data = '同步文件'
/* harmony default export */
// module.exports.default = data
__webpack_exports__["default"] = data;
}
})
先来看入口文件,key为 './src/index.js' 的函数, __webpack_exports__ 这里没导出所以没用到。
加载模块:__webpack_require__ 就是我们用import xxx的转换
导出模块:exports 转换成 __webpack_exports__
__webpack_require__("./src/sync.js") 加载我们的同步文件
key 为 "./src/sync.js" 的函数的第2个参数其实就是 __webpack_exports__ = module.exports = {}
而咱们导出使用的是 export default xxx , __webpack_exports__["default"] = data 所以这里有个default,相当于 module.exports.default = data
再来看一下webpack 5 增加一个同步文件
// sync.js
const data = '同步文件'
export default data
index.js 也做了修改
// index.js
import data from './sync.js'
console.log("我是", data)
console.log("我是 webpack 5")
// 整理一下打包后生成的 main.js
(() => {
"use strict";
var __webpack_modules__ = {
"./src/index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
var _sync_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync.js")
console.log(_sync_js__WEBPACK_IMPORTED_MODULE_0__.default)
console.log("我是 webpack 5")
}),
"./src/sync.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.d(__webpack_exports__, {
// default 变成函数,方便执行一些特殊的属性(方法)
"default": () => __WEBPACK_DEFAULT_EXPORT__});
const data = '同步文件'
const __WEBPACK_DEFAULT_EXPORT__ = (data);
})
};
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
if(__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
// 箭头函数 不需要再绑定this
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
// polyfill
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
})();
/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
// startup
// Load entry module
__webpack_require__("./src/index.js");
})()
首先很明显的可以看到 webpack5 不再使用传参的形式来引入文件,而是用__webpack_modules__对象来进行存储。
"default": () => __WEBPACK_DEFAULT_EXPORT__} 变成了一个函数
把每一个 polyfill 都变成闭包。__webpack_require__.d,就是去做定义。
同步文件 webpack没有太多的变化,接下来咱们看一下异步文件。
webpac 4 异步引入
// index.js 先用异步的方式引入 sync.js
import("./sync.js").then( (_) => {
console.log(_)
})
console.log("我是 webpack 4")
此时进行打包,打包后的 dist目录:
dist
|-- 0.js // 也就是咱们的 sync.js
|-- main.js
// 0.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/async.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
const data = '同步文件'
__webpack_exports__["default"] = (data);
//# sourceURL=webpack:///./src/async.js?");
})
}]);
先来说一下 webpack4 的一个问题,那就是会串id,造成缓存失效。
webpack 中每个模块有一个唯一的 id,是从 0 开始递增的。
这时咱们修改一下index.js,增加一个async.js
// async.js
const data = '我是异步数据'
export default data
// index.js 把async.js也用引入进来
import("./sync.js").then( (_) => {
console.log(_)
})
import("./async.js").then( (_) => {
console.log(_)
})
console.log("我是 webpack 4")
再次打包之后会发现
// 0.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/async.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
const data = '我是异步数据'
__webpack_exports__["default"] = (data);
})
}]);
// 1.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/async.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
const data = '同步文件'
__webpack_exports__["default"] = (data);
})
}]);
这个时候 0.js 不再是之前的 sync.js 了
// 只有一个文件的时候
sync.js => 0.js
// 引入sync.js
async.js => 0.js
sync.js => 1.js
这样就会导致缓存在客户端的文件失效。解决方法可以加入一行注释来固定chunkId 修改一下 index.js
// index.js
import(/* webpackChunkName: "sync" */ "./sync.js").then( (_) => {
console.log(_)
})
import(/* webpackChunkName: "async" */ "./async.js").then( (_) => {
console.log(_)
})
console.log("我是 webpack 4")
此时dist目录:
dist
|-- async.js
|-- main.js
|-- sync.js
这样异步文件都需要加上这行注释(相信你在vue项目里一定见过),无形之中增加了维护成本。(也可以使用插件,但是总会出现更新不及时、不更新问题),这个问题在 webpack5 中得到了很好的解决。
再来看一下 webpack5
将 async.js 同样放入到 src 下面, 然后修改index.js
// index.js
import("./sync.js").then( (_) => {
console.log(_)
})
import("./async.js").then( (_) => {
console.log(_)
})
console.log("我是 webpack 5")
webpack5下的dist目录
dist
|-- main.js
|-- src_async_js.js
|-- src_sync_js.js
默认就做了区分,用目录做前缀,用文件类型做后缀。 当然你打成开发环境也是一样的
dist // 目录: 数字是md5
|-- 67.js // sync.js
|-- 853.js // async.js
|-- main.js
'deterministic'这个是webpack5新增的,详情可参考官网
webpack5可配置chunkId
webpack5可配置moduleId
接下里咱们再来了解一下异步的加载方式,回到 webpack4 打包后的async.js(如果不加配置的话,默认打出来的是0.js)
// async.js === 0.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/async.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
const data = '我是异步数据'
__webpack_exports__["default"] = (data);
})
}]);
同等于
window["webpackJsonp"].push([
['async'],
{ /* 上面分析过的函数体 */ }
])
// main.js
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data);
while(resolves.length) {
resolves.shift()();
}
};
// The module cache
var installedModules = {};
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
"main": 0
};
在 webpackJsonpCallback 中会将 async.js 中的 chunks 和 modules 保存到全局的 modules 变量中,并用哨兵变量 installedChunks 来记录异步文件加载的个数。
__webpack_require__.e = function requireEnsure(chunkId) { ... }
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
__webpack_require__.e 是通过加载 script 标签来引入异步文件的。
流程
入口文件 -> 加载模块 -> 处理模块(处理浏览器兼容) 取第二项(第一项是名字)-> 加缓存 -> 放到 main.js 最后
webpack5打包后的src_async_js.js
(self["webpackChunkwebpack5"] = self["webpackChunkwebpack5"] || []).push([["src_async_js"],{
"./src/async.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"default": () => __WEBPACK_DEFAULT_EXPORT__
});
const data = '我是异步数据'
const __WEBPACK_DEFAULT_EXPORT__ = (data);
})
}]);
其实没有太多变化,用了新的 api
self.self === self
下面来看webpack 5 的 topLevelAwait
在 src 下新建 data.js 、demo.js 如下
// data.js
const data = '我是狮子 '
export default data
// demo.js
let output = ''
// top-level-await 的写法
const dynamic = import('./data') // await import('./data') 这样写也可以
output = (await dynamic).default + ' 🦁 ' + Math.random() * 100
// 之前的写法
/* async function main () {
const dynamic = await import('./data')
output = dynamic.default + '🦁'
}
main() */
export { output }
top-level-await 的写法,需要配置 webpack.config.js
// webpack.config.js
module.exports = {
experiments: {
// importAsync: true,
// importAwait: true,
topLevelAwait: true
},
}
总结,对于异步文件引用 webpack5 和 webpack4 不同点在于:
window上挂载的用于存放 webpack 打包的 json 变量名
webpack4: webpackJsonp
webpack5: webpackJsonpwebpack5,为了防止和我们自定义的冲突,加了 webpack5 后缀
在 webpack 4 、5 进行打包的时候,能明显感觉到 5.x 的版本比 4.x 打包速度有了很大的提升。 主要原因是 Webpack4 的缓存是在运行时的,所以缓存只存在于内存中,在热更新的时候代码更新很快,但是在进行打包的时候 Webpack 的运行程序关闭了,缓存就丢失了。这就导致我们打包时无缓存可用。 而 webpack5 使用的是持久化缓存,在本地开发时使用 MemoryCachePlugin ,而在打包时使用 IdleFileCachePlugin。 参考:Webpack5 内置缓存方案探索
IdleFileCachePlugin:持久化到本地磁盘
MemoryCachePlugin:持久化到内存