共计 6545 个字符,预计需要花费 17 分钟才能阅读完成。
简介: Webpack 将各个资源打包整合在一起造成 bundle,当资源越来越多时,打包的过程也将越来越慢。如果咱们不须要打包呢?让浏览器间接加载对应的资源,是否就能够实现质的晋升?本文分享基于浏览器的 ESModule 能力实现 Bundless 本地开发的相干思路、核心技术点和 Vite 的相干实现,以及在阿里供应链 POS 场景下的落地实际。
一 引言
Webpack 最后是为了解决前端模块化以及应用 Node.Js 生态的问题而呈现,在过来的 8 年工夫里,Webpack 的能力越来越弱小。
但因为多了打包构建这一层,随着我的项目的增长,打包构建速度越来越慢,每次启动都要期待几十秒甚至几分钟,而后启动一轮构建优化,随着我的项目的进一步增大,构建速度又会升高,陷入一直优化的循环。
在我的项目达到肯定的规模时,基于 Bundle 的构建优化的收益变得越来越无限,无奈实现质的晋升。咱们从另一个角度思考,webpack 之所以慢,次要的起因还是在于他将各个资源打包整合在一起造成 bundle,如果咱们不须要 bundle 打包的过程,间接让浏览器去加载对应的资源,咱们将有可能能够跳出这个循环,实现质的晋升。
在 Bundleless 的架构下,咱们不再须要构建一个残缺的 bundle,同时在批改文件时,浏览器也只须要从新加载单个文件即可。因为没有了构建这一层咱们将可能实现以下的指标:
- 极快的本地启动速度,只须要启动本地服务。
- 极快的代码编译速度,每次只须要解决单个文件。
- 我的项目开发构建的工夫复杂度始终为 O(1),使得我的项目可能持续保持高效的构建。
- 更加简略的调试体验,不再强依赖 sourcemaps 即可实现稳固的单文件的 debug。
基于以上的可能性 Bundleless 将从新定义前端的本地开发,让咱们从新找回前端在 10 年前批改单个文件之后,只须要刷新即可即时失效的体验,同时叠加上前端的 HotModuleReplace 相干技术,咱们能够把刷新也省去,最终实现保留即失效。
实现 Bundleless 一个很重要的根底能力是模块的动静加载能力,这一次要的思路会有两个:
- System.js 之类的 ES 模块加载器,益处是具备较高的兼容性。
- 间接利用 Web 规范的 ESModule,面向未来,同时整体架构也更加简略。
在本地开发过程中兼容性的影响不是特地大,同时 ESModule 曾经笼罩了超过 90% 的浏览器,咱们齐全能够利用 ESModule 的能力让浏览器自主加载须要的模块,从而更加低成本同时面向未来实现 Bundleless。
社区中在近一两年也呈现了很多基于 ESModule 的开发工具,如 Vite、Snowpack、es-dev-server 等。本文将次要分享基于浏览器的 ESModule 能力实现 Bundless 本地开发的相干思路、核心技术点以及 Vite 的相干实现和在供应链 POS 场景下的落地实际。
二 从资源加载看 Bundle 和 Bundleless 的不同
上面以大家最相熟的 create-react-app 默认我的项目为例,从理论的页面渲染资源的加载过程比照 Bundle 和 Bundleless 的区别。
基于 Webpack 的 bundle 开发模式
下面的图具体的模块加载机制能够简化为下图:
在我的项目启动和有文件变动时从新进行打包,这使得我的项目的启动和二次构建都须要做较多的事件,相应的耗时也会增长。
基于 ESModule Bundleless 模式
从上图能够看到,曾经不再有一个构建好的 bundle、chunk 之类的文件,而是间接加载本地对应的文件。
从上图能够看到,在 Bundleless 的机制下,我的项目的启动只须要启动一个服务器承接浏览器的申请即可,同时在文件变更时,也只须要额定解决变更的文件即可,其余文件可间接在缓存中读取。
比照总结
Bundleless 模式能够充分利用浏览器自主加载的个性,跳过打包的过程,使得咱们能在我的项目启动时获取到极快的启动速度,在本地更新时只须要从新编译单个文件。上面将分享如何基于浏览器 ESModule 的能力实现 Bundleless 的开发。
三 如何实现 Bundleless
如何应用 ESModule 模块加载
实现 Bundleless 的第一步是要让浏览器自主加载对应的模块。
应用 type=”module” 开启 ESModule
<div id="root"></div>
<script type="module">
// 间接在 script 标签中应用 type="module" 即可应用 ESModule 的形式
import React from 'https://cdn.pika.dev/react'
import ReactDOM from 'https://cdn.pika.dev/react-dom'
ReactDOM.render('Hello World', document.getElementById('root'))
</script>
利用 import-maps 反对 bare import
分享一个在 chrome 中曾经实现了的 import-maps 的规范,能够让咱们间接用 import React from ‘react’ 这样的写法,将来咱们能够利用此能力实现线上的 Bundleless 部署。
<div id="root"></div>
<!-- 开启 chrome://flags/#enable-experimental-productivity-features -->
<script type="importmap">
{
"imports": {
"react": "https://cdn.pika.dev/react",
"react-dom": "https://cdn.pika.dev/react-dom"
}
}
</script>
<script type="module">
// 反对 bare import
import React from 'react'
import ReactDOM from 'react-dom'
ReactDOM.render('Hello World!', document.getElementById('root'))
</script>
以上咱们介绍了浏览器中原生的 ESModule 是如何应用的。面向本地开发的场景,咱们只须要启动一个本地的 devServer 承载浏览器的申请映射到对应的本地文件,同时动静地将我的项目中 import 的资源门路指向咱们的本地地址,即可让浏览器间接加载本地的文件,比方能够应用上面的写法,将入口 JS 文件间接指向本地的门路,而后 devServer 再拦挡相应的申请返回对应的文件。
<div id="root"></div>
<!-- 间接指向本地门路 -->
<script type="module" src="/src/main.jsx"></script>
如何加载非 JS 的文件资源
通过 ESModule 咱们借助浏览器的能力实现了 JS 的自主加载,但理论的我的项目代码中咱们不仅仅会 import JS 文件,也会有上面的写法:
// main.jsx
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css' // import css 文件
import App from './App' // import jsx 文件
// 应用 JSX 语法
ReactDOM.render(<App />, document.getElementById('root'))
而浏览器在解决文件时是根据 Content-Type 的,不关怀具体的文件类型,所以咱们须要在浏览器发动申请时,将对应的资源转化为 ESModule 格局,同时设置对应的 Content-Type 为 JS,返回给浏览器执行,浏览器就会依照 JS 的语法进行解析解决,整体的流程可见下图:
以下是 Vite 的相干实现,在申请返回的过程中,对不同的文件进行动静解决:
如何实现 HotModuleReplace
HotModuleReplace 可能在咱们批改代码后,不须要刷新页面,间接在以后场景下失效,联合 Bundleless 极快的失效速度,咱们可能实现简直没有提早的保留即失效的体验。对于 React,在 Webpack 场景下目前只能通过应用 react-hot-loader 来实现,但这一块受限于具体的实现,有一些场景会存在 bug,作者也倡议迁徙到 React 团队实现的 react-refresh,而这一块在 Webpack 中还没有相应的实现。在 Bundleless 场景下,因为咱们的每个组件都是独立加载的,所以要集成 react-refresh,咱们只须要在浏览器申请返回时在文件的顶部和底部加上相应的脚本即可实现集成。
要残缺的实现 HotModuleReplace 会比下面画得更加简单,还须要有一套依赖剖析机制来判断当一个文件产生变更之后要替换哪些文件以及是否须要 reload。在 Bundleless 的场景下,因为不再须要打包为一个残缺的 bundle,同时咱们也能更加灵便地对单个文件进行批改,这一块相干的实现会更加容易。
以下是在 Vite 中的相干实现:
如何优化大量申请导致页面加载慢
Bundleless 的模式不再打包,晋升了启动的速度,但对于一些有较多内部依赖或者本身文件数量较多的模块,须要发动大量申请能力获取到全副的资源,这个会升高开发过程中页面加载的工夫。比方上面是间接在浏览器中 import lodash-es 会并收回大量的申请:
在这一块上咱们能够做相应的优化,将内部的依赖提前打包成单个文件来缩小在开发过程中因为内部依赖过多而发动过多的网络申请。
在 Vite 的启动流程中有一个 vite optimize 的过程会主动将 package.json 中的 depenencies 借助 Rollup 打包成 ES6 Module。
提前打包带来的益处除了可能晋升页面的加载速度,借助 @rollup/plugin-commonjs 咱们可能将 commonjs 的内部依赖打包为 ESModule 的模式引入,进一步扩充 Bundleless 的适用范围。
四 在供应链 POS 场景着落地实际
咱们团队负责的供应链 POS 业务次要可分为面向建材家居的家装行业和线下小店的批发行业,在技术架构上采纳了各个域 bundle 独立开发,而后最终借助底层的 sdk 合并为一个大的 SPA 的模式。因为我的项目的复杂性,在日常开发过程中,有以下的一些痛点:
- 我的项目的启动和耗时绝对较长。
- 改变后二次编译工夫长。
- 短少稳固的 HMR 能力,开发过程中须要反复造场景。
- debug 依赖 sourcemaps 能力,有时会呈现不稳固的状况。
基于以上的问题,借助 Vite 的相干实现,咱们对本地开发环境进行了 Bundleless 的尝试和落地,在试验的一些我的项目中对于本地的开发体验有了很大的晋升。
在启动以及批改失效的速度上带来极大的晋升
目前已实现单 bundle 维度的开发,打包构建速度:
Webpack
.gif”)
Vite Bundleless
从下面的能够看出,在启动单个 bundle 时,Webpack 须要 10s 左右的工夫,而基于 Bundleless 的 Vite 只须要 1s 左右,晋升 10 倍。
整体的页面加载工夫在 4s 左右,依然比 Webpack 的打包构建工夫要短,同时从下面的视频中也能够看到 HMR 的速度达到了毫秒级的响应,实现了根本无感的保留即失效。
不依赖 sourcemap 调试单个文件
落地过程中遇到的问题和解决
在理论落地过程中,遇到的问题次要是相干模块不合乎 ESModule 标准以及一些写法上的标准化:
- 局部模块没有 ESModule 的打包。
- less 依赖 node_modules 的写法的标准。
- jsx 文件后缀标准。
- babel-runtime 的解决。
局部模块没有 ESModule 的打包
对于没有 ESModule 打包输入或者输入的谬误的包,依据不同的类型应用不同的策略:
- 外部的包:通过降级脚手架,公布带有 ESModule 的包的新版本。
- 内部依赖:通过 issue、pull request 等模式,推动了 number-precision 等模块的降级。
- 同时有一些因为历史起因无奈打出 ESModule 的包能够借助 @rollup/plugin-commonjs 打包为 ESModule。
less 依赖 node_modules 的写法的标准
@import '~@ali/pos-style-mixin/style/lst.less';
// ~ 只在 webpack 中 less-loader 的反对,在原生的 less 中不反对
// 对立迁徙为上面的模式
@import '@ali/pos-style-mixin/style/lst.less';
// 同时在原先的 webpack 构建中的 less-laoder 中配置 lessOptions,用于最初的打包
/*
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true,
paths: [path.resolve(cwd, 'node_modules')],
}
}
}
*/
JSX 文件后缀标准
Vite 在运行的过程中会根据文件不同的后缀名进行对应的编译解决,而在 Webpack 模式下咱们通常会将 JSX、JS 等文件都丢给 babel-loader 进行解决,这使得有一些本来是 JSX 的文件没有写 JSX 后缀。Vite 只会对 /.(tsx?|jsx)$/ 的文件进行 esbuild 编译,对于纯 JS 会间接跳过 esbuild 的过程。对于这种状况咱们是逐渐将谬误的原先没有写 JSX 的文件迁徙为 JSX 文件。
babel-runtime 的解决
在应用了 babel-plugin-transform-runtime 之后,打包的输入后果会是上面这样:
下面所援用的 @babel/runtime/helpers/extends 是 commonjs 的格局无奈间接应用,针对这个状况,有两种解法:
1)针对外部本人打包的模块,能够在进行 es6 打包时增加 useModules 配置,这样打包进去的代码就会是间接援用 @babel/runtime/helpers/esm/extends
:
2)针对从新打包老本较高的模块,能够通过 Vite 的插件机制进行转换,将 @babel/runtime/helpers 在运行时替换为 @babel/runtime/helpers/esm 能够通过 alias 配置实现:
以上是在 Vite 开发环境的迁徙过程中遇到的一些问题和解决的分享,这一块的更大范畴的落地还在进行中。Bundleless 的落地不仅仅是为了适配 Vite 的开发模式,同时也是面向未来标准各个模块代码的过程,将咱们的模块进行规范的 ESModule 化,在有新的工具和思维呈现时能够用更低成本进行落地。
五 间接应用 Bundleless 进行部署的可行性
受限于网络申请和浏览器的解析速度,对于较大型的利用,bundle 在加载速度上还是可能带来较大的收益。V8 在 2018 年也给出了相干性能上的倡议:在本地开发和小型的 Web 利用中应用。在明天的场景下,随着浏览器和网络性能的一直晋升,联合 ServiceWorker 之类的缓存能力,网络加载的影响和越来越小,对于一些不须要思考兼容性问题的场景能够进行外部的尝试,间接部署通过 ESModule 加载的代码。
六 总结
本文次要分享了 Bundleless 架构下,如何晋升前端的研发效率、实现思路以及在具体业务场景着落地实际。Bundleless 实质上是将原先 Webpack 中模块依赖解析的工作交给浏览器去执行,使得在开发过程中代码的转换变少,极大地晋升了开发过程中的构建速度,同时也能够更好地利用浏览器的相干开发工具。
站在以后的背景下,Web 各个领域 JavaScript/CSS/HTML 相干的规范都已成熟,同时浏览器内核也趋于对立,前端工程化的外围重点已逐渐迁徙到研发提效上,而 Bundleless 的模式可能带来长效的启动和 HMR 的速度,是将来的一大发展趋势。随着浏览器内核和 Web 规范的一直对立,前端的代码能够不再打包间接运行将成为可能,这将进一步提高整体的研发效率。
最初非常感谢 ESModule、Vite、Snowpack 等规范和工具的呈现,让前端的开发体验往前跨了一大步。