简短摘要:得益于现代浏览器内置了模块处理系统(ESM), 业界新兴开发工具 Snowpack、WMR、Vite 等直接将模块解析加载过程直接交给浏览器, Dev Server 能够秒级启动。和传统开发工具编译时解析加载模块最终打包到 JS Bundle 中相比,本地开发体验提升明显
业界主流的开发工具还是以 Webpack 为主,随着项目体积增大,开发阶段一次性将源代码和第三方依赖编译处理打包到一起的耗时会显著增加。在我们团队内部的 monorepo 仓库中,应用项目开发时,为了开发阶段调试方便,通常也会对一些公共库源码一起打包编译,成千上万个模块导致首次 dev server 启动耗时在几分钟甚至十几分钟,严重影响了开发效率与体验。
Webpack 打包慢的问题相信也是大家都会遇到的问题。随手一搜,各种 Webpack 配置优化以及最佳实践之类的文档数不胜数。大部分也都是遵循 Webpack[1]Build Performance Guide[2]。
粗略一看上面的一些优化方式很多, 但是在我们的场景中很多都是不够通用,比如 thread-loader 结合 babel-loader 的方式在业务项目中经常会遇到报错的情况,原因是业务项目通常会使用 babel-plugin-import 针对业务内部的组件库按需加载组件以及样式,配置大致如下:
[
"babel-plugin-import",
{
"libraryName": "custom-ui-components",
"style": (name: string, file: Object) => {
return `${name}/style/2x`;
}
}
]
我们知道 JavaScript 中线程间共享的数据必须序列化,上述style
函数在序列化时会直接报错。
esbuild 作为一个 bundle 工具性能很不错,但是针对应用生产环境打包还存在一些问题,如降级到 ES5,Code Split 、 CSS 处理等。社区内有提供替换 babel-loader 的 esbuild-loader, 通过 loader 的方式在 Webpack JS 运行时中编译单个文件的方式在速度上也不如单纯用 esbuild 一把梭快。对浏览器兼容性有要求的项目,平滑使用也比较困难。
减少处理的模块数量方面,针对 MPA,简单的做法是在 dev 时显式指定需要开发的页面,但是比较局限不够灵活, Webpack 5 提供的实验性特性 Lazy Compilation 在开发阶段能够做到真正的按需编译提升 dev server 启动速度:
首次 dev server 启动时, 会代理 Webpack 入口以及 dynamic import 导出的模块,打开浏览器页面后,代理模块在运行时通过 Server-Send-Events 与 Lazy Compilation Backend Server 通信决定需要真正编译处理的资源。但是作为实验功能,目前还不是很稳定,笔者在使用时,遇到过初次打开页面白屏必须手动刷新的问题。而且后续对需要编译的入口通过 babel-loader 或者 ts-loader 重新编译打包时,仍然会有慢的问题存在。
Webpack 4 中通过插件lazy-compile-webpack-plugin[3]也可以实现类似的效果。
更多的优化方式这里不一一列举, 总结下来,现有的一些 Webpack 打包优化方式或多或少都不够通用,或者存在一些问题。
沿着减少 Webpack 处理模块数量的思路,node_modules 下的第三方依赖如果能够从打包环节剔除,仅对业务代码打包,对构建速度肯定会有明显的提升。
比较常见的方法是将常用的第三方库在 Webpack 构建时配置 external, Html 中直接通过 script 标签引入 UMD 产物, 这种方式有以下问题:
既然 UMD 产物不太符合我们的场景,那么直接替换依赖为对应的 ESM 产物呢?业界也有类似的思路:
1.Rollup 社区有rollup-plugin-cdn支持代码中从 unpkg 引入依赖的 ESM 产物:
import hyper from 'https://unpkg.com/hyperhtml@latest/esm/index.js';
hyper(document.body)`
<h1>Hello ESM</h1>`;
2 .Pika(Skypack 前身)提供了\@pika/cdn-webpack-plugin支持生产环境构建时将 package.json 中的依赖替换为 Pika CDN 上对应的链接,同时 html 中通过script type=module
加载打包后的 js 产物, 以 React 为例在最终 JS Chunk 中大致如下:
import __mun2tz2a_default, * as __mun2tz2a_all from "https://cdn.skypack.dev/react";
window["https://cdn.skypack.dev/react"] = Object.assign((__mun2tz2a_default || {}), __mun2tz2a_all);
(window["webpackJsonptask_activity"] = window["webpackJsonptask_activity"] || []).push([["vendors"],{
/***/ "50ab":
/*!****************************************!*\
!*** ./node_modules/__pika__/react.js ***!
\****************************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = window["https://cdn.skypack.dev/react"];
/***/ }),
}])
可以看到利用浏览器对 ESM 的原生支持,直接从 Skypack CDN import 导入 React ,之后以全局变量的方式挂到 window 变量上。其他模块中导入 React ,最终会是 window 上存在的变量。
考虑到我们主要是想提升 dev server 启动时构建的速度,并且很多依赖都是公司内部包,笔者在\@pika/cdn-webpack-plugin[4]的基础上,结合团队内部的 CJS 转 ESM 服务,在 dev 环节将用到的第三方依赖改成从线上 import 导入。最终在实际业务项目中测试效果并不理想,主要有以下一些问题:
在 Webpack 生态基础上将第三方依赖以 ESM 形式直接加载看起来也不太能满足我们的场景,那么如何更好地提升 dev server 启动速度呢?
基于 ESM 的现代 unbundled 开发工具在社区如雨后春笋不断发展,es-dev-server、Snowpack 、Wmr 、Vite 等工具直接直接抛弃 Webpack,通过内部的 Dev Server 接收请求后实时对 JS、TS、CSS 等资源实时编译处理。
图片来自 Snowpack 官网: https://www.snowpack.dev/concepts/how-snowpack-works
和 Webpack 等打包工具相比,这类的 unbundled 开发工具有如下优点:
这种新兴 Unbundled Development 模式看着能够满足我们对 dev server 启动速度的需求,下面的问题就是针对使用我们内部应用开发工具的业务项目来说如何平滑的接入这些工具。
由于我们的应用开发工具提供了一套现代 Web 项目开发范式,从应用入口和各种资源的处理使用,以及服务端 API 的一体化调用上都有内部一些标准。直接在项目中使用上述工具会有很多问题,免不了对上面的工具进行二次开发,这就要求这些工具提供足够灵活的扩展自定义方式,这里就业界用的比较多的 Snowpack 和 Vite 2.0 简单对比如下: (点击查看大图)通过上面的简单对比,以及文档丰富程度上来看, Vite 2.0 扩展起来会更加灵活方便。因此在给我们的应用开发工具提供 Unbundled Development 模式前期,也是果断选择了 Vite 2.0 作为底层, 但是最终实现版本里面我们选择借鉴 Vite、Wmr 等工具自主开发实现 dev server 部分,主要有以下几个角度的考虑:
.module.css
的形式,也支持关闭文件后缀的约定。这些有的可以通过配置实现,但是大部分还是要接管 Vite 内部的资源的编译处理过程,如果通过 Vite 提供的插件可以完成,这样对 Vite 利用不是很充分,也显得比较冗余。综上,实现 Unbundled Dev Server 对我们来说更容易维护以及后续添加一些能力更方便。
为了更好的理解 Unbudled 开发工具的细节,我们从下面几个方面分别介绍:
上文我们提到,业务项目中使用到的依赖很多只提供了 CJS 产物, 首先,我们需要将第三方依赖转换成 ESM 格式。业界常用的工具主要是 Rollup 和 esbuild 等构建工具。
基本思路是分析项目源码中使用到的依赖, 这些依赖作为构建工具的入口整体打包,好处是整体将依赖打包得到 common chunks,浏览器中打开页面加载第三方依赖的请求数量会少很多。缺点是添加依赖或者删除一些依赖改动了 package.json 或者 lock 文件时, 需要重新对依赖编译打包,在一些比较大的中后台项目中,依赖预处理耗时还是存在的。
这里我们目前采用的方案是,沿用之前内部已有的 CJS 转 ESM 服务,直接下载线上依赖已经转换好的的 ESM 产物。后续针对下载的 ESM 文件,用 esbuild 做一次 bundle 减少浏览器中运行的请求数量。
借助 CJS 转ESM 服务和直接本地编译转换相比,有以下好处:
同时我们针对内部模块比较多的依赖,如 antd,在线上 CJS 转 ESM 时,会将内部模块打包到单个产物中,这样能减少成百上千的网络请求。以 React 为例, 直接从云端下载的 ESM 产物内容截图如下:
可以看到依赖项 object-assign 会额外带有版本号信息。在递归下载第三方依赖 ESM 文件后,能够得到如下的 json 文件,存储某个版本依赖实际 ESM 文件的路径, 如下:
{
"react?16.14.0": "/Library/Caches/__web_modules__/react@16.14.0.js",
"react-dom?16.14.0": "/Library/Caches/__web_modules__/react-dom@16.14.0.js",
"object-assign?4.1.1": "/Library/Caches/__web_modules__/object-assign@4.1.1.js",
}
版本号是直接在 node_modules 中解析对应依赖位置获取的,也就是说,项目下存在某个依赖多个版本也是支持的。
之后通过 esbuild 打包时借助 onResolve hook 从上面的 json 文件中匹配具体的 ESM 文件路径, 示例代码如下:
const bundleResult = await require('esbuild').build({
entryPoints: ['react', 'react-dom'],
bundle: true,
splitting: true,
chunkNames: 'chunks/[name]-[hash]',
metafile: true,
outdir: webModulesDir,
format: 'esm',
treeShaking: 'ignore-annotations',
plugins: [
{
name: 'resolve-deps-plugin',
setup(build) {
build.onResolve({filter: /^/}, async args => {
const { kind, path } = args;
if (['import-statement', 'entry-point', 'dynamic-import'].includes(kind)) {
if (kind === 'entrypoint') {
// 针对 bundle 入口, 直接本地 resolve 得到版本号
// 在上面的 json 文件中拿到具体路径返回
} else if (path.startsWith('/esm/bv')) {
// 针对依赖的依赖,比如 object-assign^4.1.1
// 通过 semver 在文件中匹配获取实际的 ESM 文件路径返回
}
}
})
}
}
]
})
到这里,依赖预处理已经完成了,最终通过bundleResult.metafile
能够得到我们最终的 import-map.json 后续在请求模块时用来解析模块依赖路径到转换后的 esm 文件, 目录结构和内容大致如下:
针对 monorepo 中某些 package 并不发布,在应用中直接使用源码统一构建的场景, 在分析使用到的依赖时,也会收集这些 package 用到的依赖,统一预处理成 ESM 格式。某些 package build 后提供产物在应用中使用时,会根据当前 package 最新代码, 本地实时编译转换成 ESM,这里本地编译转换和云端会复用底层代码,效果上也类似。
整体 Server 的实现部分借鉴了 WMR 和 Vite 的 插件系统设计, Plugin 作为 Rollup Plugin 的超集,通过 Plugin Container 提供统一的插件接口, 和 Snowpack、es-dev-server 在 Server 中间件中处理请求、文件转换不同,WMR 、Vite 的插件体系将文件转换以及 Server 中间件分离开来,概念上比较清晰, 也比较易于维护。WMR、Vite 这种插件系统也有利于 dev 和 build 时复用文件转换相关的逻辑。
从浏览器发出请求到 Server 返回对应资源的流程如下图所示:
在 resolveId hook 中根据 url 解析出具体文件路径。load hook 主要加载文件内容。transform hook 是编译转换各种类型资源文件的核心。下面对几种资源文件在 Server 内部的处理展开描述:
我们知道 JSX或 TSX 不能直接在浏览器中运行,这里因为 dev 环节对浏览器兼容性没有要求以及追求更快的实时编译速度,直接使用 esbuild transform 即可:
const result = await esbuild.transform(code, {
loader: 'tsx',
sourcefile: importer,
sourcemap: true,
target: 'chrome63'
})
在业务项目中实践时,遇到的一些问题举例如下:
import React from 'react'
。基础的语法转换完成后,接下来就是 Bare Import 的处理问题,我们的业务代码中直接通过包名导入依赖的方式经过打包工具处理能够正常运行, 如下:
import React from 'react'
但是在浏览器中直接运行会直接报错,import-maps[6]提案可以解决这个问题,但是只有最新版本 Chrome 支持。这里我们采用的方案和业界的做法一致,编译完成后改写 import 语句, 以 React 为例, 最终返回的内容如下:
import React from "/node_modules/.web_modules/react.js";
node_modules/.web_modules
目录就是我们在依赖预处理时生成的第三方依赖 ESM 文件目录。
浏览器中直接 import 导入资源,要求返回的类型是application/javascript
,因此这些文件在对应的插件中最终都会被处理成 JavaScript。
CSS 默认会用 PostCSS 处理之后直接创建 style 标签插入 head 节点就能生效,以import ./App.css
为例,返回的内容大致如下:
// src/App.css
const code = "body {\n margin: 0;\n}\n"
const filename = "/Users/songzhenwei/Documents/test/full-demo/src/App.css"
const styleEl = document.createElement('style');
const codeEl = document.createTextNode(code);
styleEl.type = 'text/css';
styleEl.appendChild(codeEl);
document.head.appendChild(styleEl);
这里只是简单展示返回文件的内容,真正可用还需要考虑热更新时 Style 标签的删除、CSS 文件中通过 [url function](https://developer.mozilla.org/en-US/docs/Web/CSS/url()) 加载字体图片等场景。
JSON 文件比较简单,这里我们只需要读取文件 default 导出即可:
export default { "name": "example" }
业务代码中使用图片等资源时, 我们会在 import 语句后面添加?assets
query 表示为资源请求需要编译处理, 如下:
import logo from './logo.png';
// => rewrite to
import logo from '/src/logo.png?assets'
/src/logo.png?assets
直接返回对应资源的实际 url 即可:
export default '/src/logo.png'
对于图片资源,实际业务场景中我们还需要支持根据文件尺寸决定是否通过 base64 编码内联、svgr 等特性。
到这里通过不同的插件完成了一些文件类型的编译转换,页面已经可以在浏览器中正确渲染。
在 Webpack 等打包工具里面,热更新相关代码通常写在入口文件内如下:
// src/index.jsx
import App from './App';
module.hot.accept('./App', () => {
renderApp();
})
App 根组件中引用到的文件修改时,会触发入口文件中注册的 accept 回调函数重新渲染 App 组件。
ESM 场景下的 HMR API, 业界也有一些规范:Snowpack 联合 Vue、Preact 提出了ESM-HMR Spec[7],类似module.hot
, unbundled 开发工具需要提供import.meta.hot
对象,比如常用的accept
函数使用如下:
import.meta.hot.accept();
import.meta.hot.accept(['./dep1', './dep2'], ()=> {})
这部分实现参考了 Snowpack 和 Vite , 文件更新时,通过内部建立好的依赖关系,上溯至 accept 该文件或自身的文件节点,重新在浏览器请求该文件, 如下图:
修改dep-b.js
向上遍历依赖树时,找到 accept 的文件节点App.tsx
, 同时会依赖路径上文件节点的编译缓存失效,之后通过 HMR client api 重新请求App.tsx
,为了保证返回的内容是最新的,重新请求时会加上时间戳。
和 Webpack 等打包工具热更新相比,Unbundled Development 开发工具热更新只会重新编译加载依赖路径上的文件, 因此速度也会更快。同时也能结合 React Fast Refresh 做组件级别的热更新。
上面通过一些小点,介绍了我们内部解决 Webpack 打包慢的一些探索以及最终实现 Unbundled Development 模式的一些做法, 最终实现的版本和我们应用开发的标准范式一一对齐。在业务项目中使用 Unbundled Development 模式后开发体验有很大提升:
生产环境现阶段还是通过 Webpack 打包出 JS Bundle ,在一些基础编译能力以及使用方式上尽最大努力抹平 Unbundled Development 模式和生产环境 Webpack 打包的差异。
通过实际业务项目接入 Unbundled Development 模式实践,我们这边也针对 CJS 转 ESM 积累了一定的解决方案。通过云端统一处理的方式,后续也能发挥出更大的作用。一些新的方案如免依赖安装也在持续探索中,最后,我们也希望能对 Unbundled Development 生态添砖加瓦,最后反哺生态。
[1]Webpack:https://webpack.js.org/guides/build-performance/
[2]Build Performance Guide:https://webpack.js.org/guides/build-performance/
[3]lazy-compile-webpack-plugin:https://github.com/liximomo/lazy-compile-webpack-plugin
[4]rollup-plugin-cdn:https://github.com/WebReflection/rollup-plugin-cdn
[5]@pika/cdn-webpack-plugin:https://www.npmjs.com/package/@pika/cdn-webpack-plugin
[6]@pika/cdn-webpack-plugin:https://www.npmjs.com/package/@pika/cdn-webpack-plugin
[7]babel-plugin-macros:https://github.com/kentcdodds/babel-plugin-macros
[8]import-maps:https://github.com/WICG/import-maps#the-basic-idea
[9]ESM-HMR Spec:https://github.com/snowpackjs/esm-hmr
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/NH46ffu79ks57AGKd1WKMw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。