带你解读 webpack 打包原理及vite的优势
ESMoudle
模块化规范有很多:AMD/CMD(浏览器), commonJS(服务端)
浏览器对 ES Module 标准的原生支持,改变了这种情况。目前大多数浏览器已经支持通过 <script type="module">
的方式和 import 的方式加载标准的 ES 模块
模块只会执行一次并且默认为defer也支持async
传统的<script>
如果引入的JS文件地址是一样的,则JS会执行多次。但是,对于type="module"
的<script>
元素,即使模块地址一模一样,也只会执行一次。例如:
<!-- 1.mjs只会执行一次 -->
<script type="module" src="1.mjs"></script>
<script type="module" src="1.mjs"></script>
<script type="module">import "./1.mjs";</script>
<!-- 下面传统JS引入会执行2次 -->
<script src="2.js"></script>
<script src="2.js"></script>
打包工具出现的原因及作用
-
前端模块化成为趋势:前端应用日益复杂开发模块化写法可以更灵活易于管理
-
浏览器 对ES Modules 有兼容问题,
-
其次模块化开发模式下,随着代码自然增长会有越来越多模块,模块越多浏览器要发起的请求数也就越多
所以打包工具诞生了,例如webpack 需要webpack把我们写好的模块代码进行处理,让浏览器识别我们写的模块代码。最后可以打包输出一个js文件,以一种串联的方式管理好了这些模块。
webpack 如何实现模块化打包的
例子:
//文件1:
exports.bar = function () {
return 1;
}
//文件index.js:
const bar = require('./bar');function foo() {
return bar.bar();
}
一、commonJS 方式
1.webpack通过模拟module,exports,require变量,将我们的模块代码打包成一个IIFE(立即执行函数),,函数参数是我们写的各个模块被包装之后组成的数组,浏览器执行这个立即执行函数就可以运行我们的模块代码。
**
(function (modules) {
/* 省略函数内容 */
return __webpack_require__(__webpack_require__.s = 0);
//启动入口模块
})
([function (module, exports, __webpack_require__) {
/* 模块1代码 */
},function (module, exports, __webpack_require__) {
/* 模块2的代码 */
}]);
**
2.立即执行函数的入参
webpack是怎样把我们写的模块包装成函数的呢?
require入口模块时,入口模块会收到收到三个参数,下面是入口模块代码:
参数说明:
module:当前缓存的模块,包含当前模块的信息和exports
exports: module.exports的引用
__webpack_require__ :require的实现
webpack函数入参如下所示:
[(function(module, exports, __webpack_require__) {
var bar = __webpack_require__(1);bar.bar();
}),
(function(module, exports, __webpack_require__) {
exports.bar = function () {return 1;}}
)]
3.立即执行函数的函数体内容:
_webpack_require__方法 require的实现
-
定义了
__webpack_require__
这个函数,函数参数为模块的id。这个函数用来实现模块的require。 -
__webpack_require__
函数首先会检查是否缓存了已加载的模块,如果有则直接返回缓存模块的exports
。 -
如果没有缓存,也就是第一次加载,则首先初始化模块,并将模块进行缓存。
-
一个动态绑定,改变this指向module.exports,将模块函数的调用对象绑定为module.exports
-
调用完成后,模块标记为已加载。
-
返回模块
exports
的内容。{ // 1、模块缓存对象 var installedModules = {}; // 2、webpack实现的require function __webpack_require__(moduleId) { // 3、判断是否已缓存模块 if(installedModules[moduleId]) {return installedModules[moduleId].exports;} // 4、缓存模块 var module = installedModules[moduleId] = {i: moduleId,l: false,exports: {}}; // 5、调用模块函数 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 6、标记模块为已加载 module.l = true; // 7、返回module.exports return module.exports; }
二、ES Moudle 方式
文件1
export default function bar () {return 1;};
export function foo () {return 2;}
文件index.js
import bar, {foo} from './m';
bar();
foo();
同commonJs 方式一样webpack生成的代码是一个IIFE,这个IIFE完成一系列初始化工作后,就会通过__webpack_require__(0)
启动入口模块。
1.代码模块的封装函数不同
步骤:
-
不能识别原生expoerts,所以需要改写原来的方法并模拟exports。并且export default和export都被转换成了类似于commonjs的
exports.xxx
-
index模块首先通过
Object.defineProperty
在__webpack_exports__
上添加属性__esModule
,值为true
,表明这是一个es模块。所有引入的模块属性都会用Object()包装成对象,这是为了保证像Boolean、String、Number这些基本数据类型转换成相应的类型对象。 -
_webpack_require__(1); 将下一个模块引入就是文件1 [ (function(module, __webpack_exports__, __webpack_require__) { Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1); Object(__WEBPACK_IMPORTED_MODULE_0__m__["a"])(); // 方法bar()Object(__WEBPACK_IMPORTED_MODULE_0__m__["b"])(); // 方法 foo()} ), ( function(module, __webpack_exports__, __webpack_require__){ __webpack_exports__["a"] = bar; __webpack_exports__["b"] = foo; function bar () {return 1;}; function foo () {return 2;}} ) ]
结论:那么知道webpack工作的原理也就能得出结论:代码量和打包时间一定是成正比
既然已经有了 Webpack,尤大再整一个 Vite 到底有啥用呢?
webpack 无法避免的问题:
- 本地开发环境webpack也是需要先打包,然后服务器运行的是打包后的文件,所以代码量很大的项目就会有启服务很慢的现象,
- 热更新:Webpack 的热更新会以当前修改的文件为入口重新 build 打包,所有涉及到的依赖也都会被重新加载一次。虽然webpack 也采用的是局部热更新并且是有缓存机制的,但是还是需要重新打包所以很大的代码项目是真的有卡顿的现象(亲身经历,例如集成很多子平台的大型项目)
具有了快速冷启动、按需编译、模块热更新的 Vite
**Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。 **
- 依赖预构建:依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。参考文章:zhuanlan.zhihu.com/p/379164359
这个过程有两个目的:
-
CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块
-
Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。
-
快速冷启动:只启动一台静态页面的服务器,对文件代码不打包,服务器会根据客户端的请求加载不同的模块处理(利用的是浏览器对esMoudle的原生支持),所以节省了webpack 那一套打包转化封装的逻辑。所以大型项目不会再出现热更新卡顿,起服务慢的情况(理论上,尚未找到合适项目实践)
与其它非打包解决方案比较
-
**按需编译、模块热更新:**采用立即编译当前修改文件的办法。同时 vite 还会使用缓存机制( http 缓存 => vite 内置缓存 )是基于缓存的热更新。
文件缓存:Vite 会将预构建的依赖缓存到
node_modules/.vite
。它根据几个源来决定是否需要重新运行预构建步骤:package.json
中的dependencies
列表, package-lock等浏览器缓存:解析后的依赖请求会以 HTTP 头
max-age=31536000,immutable
强缓存,以提高在开发时的页面重载性能。一旦被缓存,这些请求将永远不会再到达开发服务器
快速开始
Vite 的原理
接下来开始说一下 Vite 实现的核心——拦截浏览器对模块的请求并返回处理后的结果。
浏览器发起的第一个请求自然是请求 localhost:3000/
,这个请求发送到 Vite 后端之后经过静态资源服务器的处理,会进而请求到 index.html
,此时 Vite 就开始对这个请求做拦截和处理了。
内联元素:
如下图2 你会惊奇的发现也并非读取我们的源码,而是构建后的产物。会把node-modules 里的这些引用重新构建成node-modules的绝对路径这个跟webpack一样,但是源码部分并没有像webpack 做函数转换封装。而是直接用了源码
链接引入:没有在 script
标签内部直接写 import
,而是用 src
的形式引用,那么就会在浏览器发起对 main.jsx
请求的时候进行处理。只是会替换import node-modules 中的路径
Vue :
这样就把原本一个 .vue
的文件拆成了2个请求(分别对应 template和script
、style
) ,浏览器会先收到包含 script和template
逻辑的 App.vue
的响应,然后解析到 style
的路径后,会再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。
支持使用插件:
需要将它添加到项目的 devDependencies
并在 vite.config.js
配置文件中的 plugins
数组中引入它。
强制插件排序:默认在vite 之后调用,可以强制改变顺序调整到vite之前
Loader:
静态资源引用,直接使用不必再使用loader 转换
imgUrl
在开发时会是 /img.png
,在生产构建后会是 /assets/img.2d8efhg.png
。
行为类似于 Webpack 的 file-loader
。区别在于导入既可以使用绝对公共路径(基于开发期间的项目根路径),也可以使用相对路径。
单页面&多页面
单页面:
当需要将应用部署到生产环境时,只需运行 vite build
命令。默认情况下,它使用 <root>/index.html
作为其构建入口点,
多页面:
为什么生产环境仍需打包,为啥不直接将 entry.js 文件使用 标签引用
-
尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)
-
从 entry.js 到所有依赖的模块代码,全部采用 ES Module 方案实现,我们的依赖管理是采用 npm 的,而 npm 包大部分是采用 CommonJS 标准而未兼容 ES 标准的。
未来可期
无论vite 可以走多远,但是这种本地无需打包利用浏览器去解析当前请求的模块,还有热更新时候只编译也不再打包的概念都是我所期待的。永远摆脱了代码越多打包越慢的噩梦!!!
转载自:https://juejin.cn/post/7031421642513317918