工具 – Rspack
前端打包工具 bundlers
Webpack 曾经是最受欢迎的前端打包工具,没有之一。
但是呢,它在 2020-10-10 发布 v5.0 版本后就进入了 maintenance mode。
其原因可能是因为在同年,esbuild 诞生了。
Webpack 是用 JavaScript 写的,esbuild 是用 Go 写的。
这两个语言在性能上完全不是一个档次,所以就有了下面这张图
esbuild 比 Webpack 快了 100 倍🤭。
受到 esbuild 的启发 -- 为什么非要用 JS 来写前端工具呢?难道就因为它跟 "前端" 有关?
Webpack 团队决定另起炉灶,用 Rust 语言重写 Webpack,并改名为 Turbopack (turbo 是极速的代名词,显然他们是想哪里跌倒哪里爬😏)。
令人惋惜的是,Turbopack 并没有要承接 Webpack 的意思,而是选择加入 Next.js 阵营去发大财🙄。
后续:在 2022-10-25,Turbopack 正式发布 v1.0 版本,并作为 Next.js v13 的一部分。
那被遗弃的 Webpack 用户怎么办呢?
虽然 esbuild 很快,但它其实不是一个完整的 Webpack 替代方案,作者视乎也没有想承接 Webpack 用户的意思。
于是,Vite 诞生了 (在 esbuild 发布后不久)。
Vite 是尤雨溪 (Vue 作者) 创作的,它在 esbuild 的基础上做了许多加工。
一来是因为 Vue 本身需要打包工具,二来是想承接部分 Webpack 用户。
后续:2024 年 6 月,尤雨溪成立了 VoidZero Inc. 公司,其主要负责推动 Vite 及其相关的前端工具和生态系统的发展。
为什么我说 Vite 只承接了部分 Webpack 用户,而不是全部呢?
因为 Vite 依然是以 Vue 为主,像 Angular 这样的框架就和 Vue 类似,所以 Angular 能顺利的从 Webpack 过渡到 Vite。
但是 Webpack 不仅仅被用于像 Vue 和 Angular 这样的项目,有些传统的企业网站也在使用 Webpack,这群人没有被安置 (其中就包括了我...💔)
庆幸的是,在 2022 年,字节跳动里的一群人创作了 Rspack,它就是为了承接 Webpack 而诞生的。
Rspack 是用 Rust 语言写的,速度自然不在话下,最难能可贵的地方是他们在 Webpack to Rspack migration上做了很多的贡献,这一点必须给一个大大的赞👍。
本篇就是要介绍 Rspack。
但,先声明一点,我虽然是从 Webpack 迁移到 Rspack,但我不需要 100% 无改动的迁移方案 (虽然 Rspack 几乎可以做到)。
我采用的是比较 modern 的配置写法 (毕竟 Webpack 的配置写法还停留在 2020 年),简单说就是需求迁移,但是写法没有迁移。
近两年前端 bundlers 使用率
2023 - 2024 年,npm trends
2023 年,Webpack 还一枝独秀,2024 年就彻底被 esbuild 赶超了。
Vite 底层使用 esbuild,所以 Vite 的下载量也反应到了 esbuild 里。
rollup 和 Webpack 是同期竞品,当年 rollup 是最早推出 tree shaking 概念的,也可以算是前端 bundler 的功臣之一。
Vite 在 bundle production 时,使用的是 rollup,因为 esbuild 只着重在 develop 阶段而已。
rollup 是用 JS 写的,并不快,目前 Vite 正在用 Rust 语言重写一个叫 rolldown 的,顾名思义就是想取代 rollup,到时 Vite bundle develop 和 product 都会非常快。
Turbopack 虽然是前 Webpack 团队创立的,但它限缩在 Next.js,所以下载量平平。
Rspack v1.0 是 2024-08-28 发布的,目前下载量还不多,但相信往后 Webpack 的用户都会慢慢迁移过去。
Rsbuild vs Rspack
Rsbuild 建构在 Rspack 之上,它俩的关系类似于 Vite 和 esbuild 的关系。
如果我们想 100% 无改动的从 webpack 迁移,那必须使用底层的 Rspack。
但如果我们可以配合做代码上的小修改,那强烈建议使用上层的 Rsbuild。
本篇只会讲解 Rsbuild,不会谈论更底层的 Rspack。
Rsbuild Get Started
command
yarn create rsbuild
它会问几个问题
1. 项目名字
2. 是否搭配前端框架 (比如 React, Vue, Svelte, Solid 都支持 Rspack 哦,当然除了 Angular),Vanilla 就是不采用任何框架,单纯三件套 (HTML, CSS, JS)
3. 使用 TypeScript 还是 JavaScript
4. 是否搭配 Eslint, Prettier 这类辅助工具。
接着进入 folder > install > run
cd rsbuild
yarn install
yarn dev
这样就跑起来了
IP address & Https
我个人的习惯是用 IP address 和 self-signed certificate 进行开发,这样手机可以直接访问,很方便。
先安装 Node.js 类型,因为我们会用到一些 node 模块。
yarn add @types/node --dev
接着放入 self-signed certificate 文件 (制作方式可以参考:Git Bash OpenSSL – Generate Self Signed Certificate)
接着 rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'; import fs from 'fs'; export default defineConfig({ server: { host: '192.168.0.152', port: 4200, https: { cert: fs.readFileSync('192.168.0.152.crt'), key: fs.readFileSync('192.168.0.152.key'), }, }, });
执行 command
yarn dev
效果
Sass / Scss
把原本的 index.css 换成 index.scss
还有 index.ts 里的 import 也换成 scss
执行 command
yarn dev
效果
报错了,需要配上 sass plugin 才行。
安装 sass plugin
yarn add @rsbuild/plugin-sass --dev
接着到 rsbuild.config.ts 里添加 sass plugin
import { defineConfig } from '@rsbuild/core'; import { pluginSass } from '@rsbuild/plugin-sass'; export default defineConfig({ plugins: [pluginSass()], });
搞定!
PostCSS の postcss-preset-env
安装 postcss plugin
yarn add postcss --dev
yarn add postcss-preset-env --dev
postcss 不安装也可以,不会报错,但会有 unmet peer dependency warning。
添加 browserslist 到 package.json
到 rsbuild.config.ts 添加 postcss plugin
import { defineConfig } from '@rsbuild/core'; import postcssPresetEnv from 'postcss-preset-env'; export default defineConfig({ tools: { postcss: (_config, { addPlugins }) => { addPlugins(postcssPresetEnv({ stage: 1 })); }, }, });
接着在 index.scss 里写一个测试
:root { --primary-color: red; } @media (width >= 1024px) { h1 { background-color: var(--primary-color); } }
然后执行 command
yarn build
效果
min-width 的转译不是 PostCSS 的功劳,它之所以会转译,是因为 Rspack built-in 使用了 Lightning CSS (它也是用 Rust 写的,所以也非常快)。
CSS Variables 的转译则是 PostCSS 的功劳。
Stylelint
Rspack 没有 built-in 的 stylelint plugin。
虽然 Rspack 团队用 Rust 重写了许多 Webpack plugin,但也仅限热门 plugin 而已。
那如果我们依赖冷门的 plugin 怎么办呢?(e.g. stylelint plugin)
答案是直接用回 Webpack plugin 就可以了。
是的,Rspack 兼容 Webpack plugin,只是性能会扣一点分而已,毕竟这些 plugin 是 JS 写的,不是 Rust。
安装
yarn add stylelint --dev yarn add stylelint-config-standard --dev yarn add stylelint-config-standard-scss --dev yarn add prettier --dev yarn add stylelint-prettier --dev yarn add stylelint-webpack-plugin --dev
接着 .stylelintrc.json
{ "extends": [ "stylelint-config-standard-scss", "stylelint-prettier/recommended" ], "rules": { "prettier/prettier": [ true, { "singleQuote": true, "printWidth": 100, "endOfLine": "auto" } ] } }
还有 .stylelintignore
node_modules/** dist/**
dist 是 Rspack 默认 bundle output folder。
然后 rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'; import StylelintPlugin from 'stylelint-webpack-plugin'; export default defineConfig({ tools: { rspack: { plugins: [new StylelintPlugin()], }, }, });
效果
ESLint
虽然在创建 Rsbuild 项目时,它有提问要不要搭配 ESLint。
但即使选了搭配 ESLint,它也只是在 VS Code extension 的情况下才会做检查,在 build 的时候是不会检查的。
如果我们希望它在 build 时也做检查,那就需要添加 eslint plugin,就像上一 part 的 stylelint plugin 那样。
安装
yarn add @rsbuild/plugin-eslint --dev
这个 ESLint plugin 是 Rspack 团队用 Rust 重写的,我们不需要像 stylelint plugin 那样采用 Webpack 的版本。
接着添加 eslint plugin 到 rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'; import { pluginEslint } from '@rsbuild/plugin-eslint'; export default defineConfig({ plugins: [pluginEslint({ eslintPluginOptions: { configType: "flat" }})], });
效果
Multiple Pages
By default Rspack 是单页面,但它也支持多页面,就如同 Webpack multiple entries + html-webpack-plugin 一样。
我们做 2 个 pages
home.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Home Page</title> </head> <body> <h1>Home Page</h1> </body> </html>
不需要引入 styles 和 scripts,Rspack 会替我们插入。
home.scss
* { margin: 0; padding: 0; box-sizing: border-box; } h1 { font-size: 64px; color: red; }
home.ts
import './home.scss';
rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'; import { pluginSass } from '@rsbuild/plugin-sass'; export default defineConfig({ source: { entry: { home: "./src/home/home.ts", // 如果没有 ts, entry 放 home.scss 也可以 about: "./src/about/about.ts", } }, html: { template({ entryName }) { const templates: Record<string, string> = { home: './src/home/home.html', about: './src/about/about.html', }; return templates[entryName]; }, // 默认是 'flat',区别是: // 'flat' = /dist/about.html // 'nested' = /dist/about/index.html outputStructure: 'nested' }, plugins: [pluginSass()], });
执行 command
yarn build
bundle 出来的结构长这样
html 长这样
script 虽然在 head 里,但它是 defer。
Custom inject
如果我们想自己控制 styles 和 scripts 插入的位置也可以。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Home Page</title> <%= htmlPlugin.tags.headTags %> </head> <body> <h1>Home Page</h1> <%= htmlPlugin.tags.bodyTags %> </body> </html>
注:它不是 styles 和 scripts 哦,而是 head 和 body tags。
其中 head tags 里默认还包括了 <title>,<meta name="viewport">,<meta charset="UTF-8"> 哦。
rsbuild.config.ts
export default defineConfig({ html: { inject: false, title: '', // 不让 Rspack 负责 title meta: { viewport: false, // 不让 Rspack 负责 meta viewport charset: false, // 不让 Rspack 负责 meta charset }, tags: [ // 默认 script 是插入到 head,我们这里 set 成 false 意思是让它插入到 body tags => tags.forEach(tag => tag.tag === 'script' && (tag.head = false)), ], }, });
在 html 这个部分,需要配置 inject: false,还有把 script 的 tag 改成 body。
效果
Assets
有一个 public folder
里面的文件在 build 的时候会被原封不动地搬到 dist folder 里。
我们可以在 HTML 里,直接写绝对路径去访问这些文件。
<img src="/yangmi.jpg">
这个方式虽然简单,但文件没有自动 hash,不利于缓存,所以这个方式只适合那些不需要搞缓存的文件。
而那些需要搞缓存的文件,比如图片,需要采用另一种方式。
创建一个 assets forlder,把图片从 public 搬过去
接着把 HTML <img> 改成这样
<img src="<%= require('../../assets/yangmi.jpg') %>">
使用相对路径。
然后 yarn build
assets folder 不会被 build 出来 (只有 public folder 会)。
图片 yangmi.jpg 被 clone + hash rename 后,放进了 static folder。
index.html 的 img[src] 连上了被 hash 后的图片。
CSS background-image url 也是相同的写法
效果
提醒:by default,CSS 只能写第二种方式 (相对路径有 hash 的那种)。
如果我们写第一种方式 (绝对路径没有 hash 的那种),它会直接报错。
这是 built-in css-loader 引起的,它默认会把 url 当相对路径来解析。
我们可以透过 rsbuild.config.ts 配置把这个规则改掉。
export default defineConfig({ tools: { cssLoader: { url: { filter: url => url !== '/yangmi.jpg', // 如果路径是 '/yangmi.jpg' 那就 filter 掉不处理 }, }, }, });
效果
不再报错,因为这个 URL 被 filter 掉了,css-loader 不会处理它。
Output Configuration
ASP.NET Core + Rspack
这里借由 ASP.NET Core 企业网站 folder 结构来讲解 Rspack custom output configuration。
下图是我用 ASP.NET Core + Rsbuild 开发企业网站的 folder 结构
它有几个特点:
-
case style 混乱
有些 folder 是 follow 后端的 PascalCase,有些是 follow 前端的 kebab-case。
但这无所谓,Rspack 基本上不区分大小写
-
所有 js, css bundle 后要放进 wwwroot folder 里,而不是 dist folder。
-
wwwroot 里头不仅存放 bundle 后的 js, css,它还需要放其它网站文件,比如 uplaoded-files。
因此,bundle 过程中不可以直接把 wwwroot 清空哦。
-
/Web/Home/Home.cshtml bundle 后要生成 /Web/Home/Index.cshtml。
注:是放回同一个路径 folder 里哦,并不是放到 wwwroot folder。
完整 rsbuild.config.ts

import { defineConfig } from '@rsbuild/core'; import { pluginSass } from '@rsbuild/plugin-sass'; import postcssPresetEnv from 'postcss-preset-env'; import StylelintPlugin from 'stylelint-webpack-plugin'; import { pluginEslint } from '@rsbuild/plugin-eslint'; import { fileURLToPath } from 'url'; import pathHelper from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = pathHelper.dirname(__filename); export default defineConfig({ source: { entry: { 'home': './Web/Home/home.ts', 'about': './Web/About/about.ts', 'layout': './Web/Layout/layout.ts', } }, html: { template({ entryName }) { const templates: Record<string, string> = { 'home': './Web/Home/Home.cshtml', 'about': './Web/About/About.cshtml', 'layout': './Web/Layout/Layout.cshtml', }; return templates[entryName]; }, inject: false, title: '', meta: { viewport: false, charset: false, }, tags: [ tags => tags.forEach(tag => tag.tag === 'script' && (tag.head = false)), ], }, output: { distPath: { root: pathHelper.resolve(__dirname, './wwwroot/assets'), html: pathHelper.resolve(__dirname, './Web'), }, assetPrefix: '/assets', filename: { html: '[name]/Index.cshtml', js: '[name].[contenthash].js', css: '[name].[contenthash].css', }, }, plugins: [pluginSass(), pluginEslint({ eslintPluginOptions: { configType: 'flat' }})], tools: { postcss: (_config, { addPlugins }) => { addPlugins(postcssPresetEnv({ stage: 1 })); }, rspack: { plugins: [new StylelintPlugin()], }, }, });
我们一段段来看
entry
source: { entry: { 'home': './Web/Home/home.ts', 'about': './Web/About/about.ts', 'layout': './Web/Layout/layout.ts', } }
一个页面 (.cshtml) 一个 enry (.ts or .scss)
这里需要注意的是,但凡是一个 page 就一定要有 entry。
Rsbuild 不能像 html-webpack-plugin 那样,把 html 自己独立作为 entry point。
使用底层的 Rspack 才可以做到,但本篇只讲上层的 Rsbuild,所以一定要有 entry 就对了 (我的做法是无论如何都写一个 .scss 给 .cshtml,但是在 .cshtml 里不 inject 这个 head tags)。
html

html: { template({ entryName }) { const templates: Record<string, string> = { 'home': './Web/Home/Home.cshtml', 'about': './Web/About/About.cshtml', 'layout': './Web/Layout/Layout.cshtml', }; return templates[entryName]; }, inject: false, title: '', meta: { viewport: false, charset: false, }, tags: [ tags => tags.forEach(tag => tag.tag === 'script' && (tag.head = false)), ], },
这部分没有什么特别的,配置上一 part 都讲解过了。
output
output: { distPath: { // 表示所有 bundle 物 (css, js 等等) 都输出到 /wwwroot/assets 而不是原本的 dist // 为什么需要 assets 这个 folder,不可以单单 wwwroot 呢? // 因为 Rspack 在 bundle 时会先清空 folder,wwwroot 里面还有其它网站文件,不能清空。 // 所以这里开多一个 assets folder // 最终路径大概是这样 /wwwroot/assets/static/js/home.hash.js root: pathHelper.resolve(__dirname, './wwwroot/assets'), // for html 则是输出到 ./Web 这个 folder 里,而不是 dist 或 wwwroot html: pathHelper.resolve(__dirname, './Web'), }, // 在 html 里,本来的 link URL 是 href="/static/css/home.hash.css" // 但由于我们上面加了一个 assets folder // 所以这里也需要补上,让它变成 href="/assets/static/css/home.hash.css" // 注:wwwroot 会被 ASP.NET Core 程序隐藏起来,所以 link URL 肯定不会是 starts with wwwroot assetPrefix: '/assets', filename: { // 本来是输出到 /Web/home.html, 这里把它改成 /Web/home/Index.cshtml html: '[name]/Index.cshtml', // 默认是 [name].[contenthash:8].js 会 substring(8),我不喜欢,所以做了小调整 js: '[name].[contenthash].js', css: '[name].[contenthash].css', }, },
效果
ASP.NET Core 有些 folder 需要 ignoer stylelint
node_modules/** wwwroot/** bin/** obj/**
entry name and output path
entry name 其实就是 output path 来的
entry: { 'home': './Web/Home/home.ts', }
最终 ouput 的地方是 "wwwroot/assets/static/js/home.hash.js"
wwwroot/assets 来自 output.distPath.root 配置
static 是 Rspack 默认的
js 是因为它是一个 ts file
home.hash.js,这个 home 就是 entry name 'home'。
如果我把 entry name 改成 'home/xyz'
那出来的结果会变成
wwwroot/assets/static/js/home/xyz.hash.js
Web/Home/xyz/Index.cshtml
结论:entry name 可以用来做 folder 层级结构。
inlineStyles
顾名思义,就是不要 .css file,而是采用 <style> 元素输出 styles。
output: { inlineStyles: ({ name, _size }) => { return name.includes('home'); } }
可以依据 size 或者 entry name 来判断是否要使用 inline styles。
效果
Environment
env & envMode
不同的 environment 采用不同的配置是一个常见的需求,Rspack 完全支持。
rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'; export default defineConfig(({ env, envMode }) => { console.log([env, envMode]); return { output: { // 只有在 environment 是 'production' 时才使用 inlineStyles ...(env === 'product' && { inlineStyles: ({ name }) => { return name.includes('home'); }, }), }, }; });
defineConfig 的参数可以是 RsbuildConfig 对象,也可以是一个函数,其返回 RsbuildConfig 对象。
当这个函数被调用时,它会获得当前的 environment 信息。
当 yarn dev (或者说 rsbuild dev) 时,env 会是 'development',当 yarn build 时,env 会是 'production'。
envMode 则支持自定义输入,比如
rsbuild build --env-mode my-production
那 envMode 就是 'my-production'。
envMode 还有一种玩法是这样
rsbuild build --env-mode development
然后
import { defineConfig } from '@rsbuild/core'; export default defineConfig(({ envMode }) => {return { mode: envMode === 'development' ? 'development' : 'production', }; });
这样它就会 build by development mode。
Multi-environment builds
Rspack 有一个概念是叫 "多环境打包"。
这里的 environment 不是指 'development' 或 'production'。
而是指 'browser' 或 'node'。
'browser' 就是 build for web browser visit。
'node' 则是 build for SSR。
但我比较另类,我利用它来做分 "页" 打包。
import { defineConfig } from "@rsbuild/core"; export default defineConfig({ environments: { website: { source: { entry: { home: "./Web/Home/home.ts", product: "./Web/Product/product.ts", }, }, html: { template({ entryName }) { const templates = { home: "./Web/Home/Home.cshtml", product: "./Web/Product/Product.cshtml", }; return templates[entryName]; }, }, }, landingPage: { source: { entry: { adsMonthly: "./LandingPage/AdsMonthly/ads-monthly.ts", adsWeekly: "./LandingPage/AdsWeekly/ads-weekly.ts", }, }, html: { template({ entryName }) { const templates = { about: "./LandingPage/AdsMonthly/AdsMonthly.cshtml", contact: "./LandingPage/AdsWeekly/AdsWeekly.cshtml", }; return templates[entryName]; }, }, }, }, });
执行 command
rsbuild build #build all enviroment (website and landingPage) rsbuild build --enviroment website #build only website rsbuild build --enviroment website --enviroment landingPage #build only website and landingPage rsbuild build --enviroment website,landingPage # 和上一句等价,只是 shorthand 写法而已 (注:v1.2 好像有 Bug)
提醒:"website,landingPage" 中间不能有空格哦,像 "website, landingPage" 这样是不对的。
为什么 Website 和 LandingPage 分开打包呢?
因为 LandingPage 比较多样化,不一定与主网站的设计风格吻合。
因此把它们分开独立发展有助于管理。
或许我的用法违背了它的初衷,但我用得还挺舒服的😊
Browser Compatibility
downlevel transpile TypeScript
熟悉 TypeScript 的朋友应该都知道 tsconfig.json 里有一个叫 target 的属性。
把它设置成 es5,那在 transpile TypeScript to JavaScript 时,它会采用 es5 标准,也就是用 function 代替关键字 class。(因为 class 关键字是 es6 才引入的)
如果设置成 es6 (a.k.a es2015),那就会使用 class 关键字。
Rspack 在 transpile TypeScript 时,不会依据 tsconfig.json 的 target。
它会依据 package.json 的 browserslist 属性,这个更厉害。
举例
index.ts
class Person { name = 'Derrick'; } console.log(new Person());
browserslist
"browserslist": [ "IE 11" ],
注:IE 11 不支持 es6
index.js
重点是没有 "class" 这个关键字。
我们换一下 browserslist
"browserslist": [ "Chrome > 50" ],
index.js
使用了 class 关键字,因为 Chrome version 50 以上都支持 es6 语法。
auto polyfill
除了依据 browserlist 做 downlevel transpile 外,Rspack 还能自动添加 polyfill。
到 rsbuild.config.ts 开启 auto polyfill
export default defineConfig({ output: { polyfill: 'usage' // 只有 entry 需要才会引入 polyfill.js // polyfill: 'entry' // 每个 entry 都会引入 polyfill.js // polyfill: 'off' // 关闭 polyfill 机制 (也是 Rspack 默认设置) }, });
index.ts
console.log([].toReversed());
toReversed 是 es2023 才支持的 array 方法。
bundle 后会多出一个 polyfill.js
里面就是 toReversed 的 polyfill 代码
index.html
html 会导入 polyfill.js
Rsdoctor
顾名思义,Rsdoctor 是用来诊断 Rsbuild 的医生。
每当我们 build 好了以后,透过 Rsdoctor 对项目进行诊断,确保 bundle size, syntax 等等是否完全符合预期。
安装
yarn add @rsdoctor/rspack-plugin --dev
rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'; import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'; export default defineConfig({ tools: { rspack(_config, { appendPlugins }) { if (process.env.RSDOCTOR) { appendPlugins( new RsdoctorRspackPlugin(), ); } }, }, });
不需要每次都跑诊断,只有当觉得有问题时才跑。
上面做了一个 conditional run,只有当 process.env.RSDOCTOR = true 时才会诊断。
执行 command
# 这个是 Windows PowerShell 版 $env:RSDOCTOR="true"; yarn build # 这个是 Windows cmd 版本 set RSDOCTOR=true && yarn build
或者透过 package.json command 是这样
"scripts": { // for yarn 1.x "doctor": "set RSDOCTOR=true && rsbuild build", // for yarn >= 2 "doctor": "cross-env RSDOCTOR=true rsbuild build" },
效果
这样看不清楚,我们去访问它给的链接 -- http://192.168.0.152:5600/index.html
它就是一个诊断报告,读过医学系的同学应该不陌生。
大体上没有问题 (因为代码都是 default generate 的),但出现了一个 warning。
我推测这个问题出自 browserslist 和 plugin-check-syntax。
插件文档有特别指出,目前还无法从 browserslist 表达式反推出超过 es2018 的设置。
假如我们把 package.json 的 browserslist 设置成 "IE 11"
"browserslist": ["IE 11"]
上面的 warning 就消失了,因为 "IE 11" 的话,bundle 出来的是 es5。
那如果我们改成
"browserslist": ["iOS >= 15"]
那 bundle 出来就可能会超过 es2018,这样就会出现 warning 了。
注:plugin-check-syntax 是可以单独使用的,不一定要透过 Rsdoctor,有兴趣的朋友可以去玩玩。
那 clear warning 可以这样做
import { defineConfig } from '@rsbuild/core'; import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'; import { ECMAVersion } from '@rsdoctor/utils/ruleUtils'; export default defineConfig({ tools: { rspack : { plugins: [new RsdoctorRspackPlugin({ linter: { rules: { // highestVersion 只能 set 成 es5, es6, es7+ "ecma-version-check": ['Warn', { highestVersion: ECMAVersion.ES7P, ignore: [] }] } } })] } }, });
这样配置就可以了。
另一个方法是直接把它把它关掉
"ecma-version-check": 'off'
毕竟,Rsbuild 不太可能会 build 错啊,你用 Rsdoctor 去检查 Rsbuild,感觉就不太对,Rsdoctor 的角色应该是检查我们有没有配置不妥当,或者写错东西这类的。
Bundle Code Splitting
Rsbuild 有 built-in 的 bundle code splitting 策略,如果我们不太在意的话,用 default 就好。
如果我们在意的话,那可以思考几个点:
- request count 不要太多
split 的越细,file 越多,http request 的数量就越多。
HTTP 1.1 时代,request 多肯定不能接受。
如今 HTTP 2.0 / 3.0 时代,request 多还 ok,不要太多就可以了。
-
cache 要多多
如果所有代码 bundle 到一个 file 里,那只要一点点的改动,整个 file 的缓存就失效了。
split 多个 file 就可以缓解这个问题。比如说,我们 split 了几个 file,当 upgrade jquery.js 后,jquery.js 缓存失效,但其它的 dayjs.js,home.js,rxjs.js 缓存依然可用。
-
shared by multiple page 也可以利用到 cache
cache 不仅仅是说 user repeat 访问。
multiple page 之间也可以利用 cache。
假设我们 bundle all in one file
home.js for home page
about.js for about page
但是这 2 个 page 共同都使用了 jquery.js
那我们就应该把 jquery.js split 出来
user 访问 home 就下载 jquery.js 和 home.js (excluded jquery 比较小了)
user 再访问 about 就只需要再下载 about.js (excluded jquery),因为 jquery 可以用缓存。
下面我做个简单的示范 (注:有想深入了解的朋友请自行查看官网的这篇、这篇、这篇)。
执行 command
$env:RSDOCTOR="true"; yarn build
访问 doctor 页面,右上角有一个 Bundle Size
进入后下面有一个 Bundle Analysis,选择要查看的页面 (e.g. home)
595.31 KB 是 .css 和 .js 的总和,before gzip。
我们可以挨个查看每一个 .js 的打包来源。
比如上面这个 1145.js 它的具体内容是 swiper.js 这个 js library。
by default, 这个 bundle code splitting 策略会把某些 library 单独拆分 (e.g. core-js, axios),还有把不同 page 但重复使用的代码 (e.g. shared.js) 也单独拆分 (除非体积非常非常小)。
如果我们觉得一些 files 太小,不值得拆分的话,我们可以做一点调整
export default defineConfig({ performance: { chunkSplit: { strategy: 'split-by-experience', // default strategy override: { minSize: 50 * 1024, // for shared.js 要超过 50kb 才独立拆分 }, }, }, });
效果
比之前的少了好几个 files,因为共享代码的部分需要 > 50 kb 才会被拆分。
如果想独立拆分各个 library,那可以挨个声明
performance: { chunkSplit: { strategy: 'split-by-experience', override: { minSize: 50 * 1024, cacheGroups: { rxjs: { test: /[\\/]node_modules[\\/]rxjs[\\/]/, name: 'rxjs', }, swiper: { test: /[\\/]node_modules[\\/]swiper[\\/]/, name: 'swiper', }, material: { test: /[\\/]node_modules[\\/]@material[\\/]/, name: 'swiper', }, libphonenumber: { test: /[\\/]node_modules[\\/]libphonenumber-js[\\/]/, name: 'swiper', }, // internal modules srcModule: { test: /[\\/]src[\\/]module[\\/]/, name: 'src-module', }, sharedStoogesComponent: { test: /[\\/]Web[\\/]Shared[\\/]Component[\\/]Stooges[\\/]/, name: 'shared-stooges-component', }, }, }, }, },
效果
提醒:
总体的 size 从 595.31 KB 提升到了 622.46 KB。
我给个例子说明
原本 home page 依赖 rxjs 一部分的代码。
about page 也依赖 rxjs 一部分的代码。
但是它俩依赖 rxjs 的部分,不完全相同。
在没有统一打包 rxjs 代码前,home 和 about 加载关于 rxjs 代码的部分是不同的。
但统一打包以后,所有 (home 和 about) 依赖的 rxjs 都放入了 rxjs.js file 里,这时 home 就必须加载这个完整的 rxjs.js,里头也包括了 about 才需要的 rxjs 代码。
这也导致了 home page 加载变多了。
总结
本文简要介绍了一些 Rsbuild 常见的配置,作为我从 Webpack 迁移到 Rspack 的学习笔记。希望未来有机会深入再详细探讨。
Migration from Webpack to Rspack 遇到的坑
这里记入我在 migration from Webpack to Rspack 遇到的问题。
当 @use 遇上 index.ts
我在使用 MDC – Text field 时
遇到了这个 error
询问 ChatGPT 后,它给了我一个提示
赶紧去翻一下 MDC 源码
果然,它真的有两个 index。
看来 Rspack 在处理 @use 时,真的会尝试 (甚至是优先) 选择 index.ts。
我不确定 right way 是如何,但下面这个 workaround 可以解决这种冲突,勉强用着先吧,毕竟 MDC 已经废弃了,等我有空找个替代吧。
改成这样
@use '@material/floating-label/mdc-floating-label';
@use '@material/notched-outline/mdc-notched-outline';
@use '@material/textfield/index' as textfield;
@include textfield.core-styles;
或者直接这样
@use '@material/textfield/mdc-text-field';
support Yarn PnP
Rspack 支持 yarn 4.0,但目前还不支持 PnP。
估计下一版 v1.2.0 就会支持了,因为相关 Github Issue – feat(core): add Yarn PnP support 上个星期已经 completed 了。
Html as a entry point? without js,css
Webpack 的 html-webpack-plugin 有独立的 entry point。
Rsbuild 视乎 (不是 100% 确认,懒得深入研究了) 没有
或许我们可以采用更底层的 HtmlRspackPlugin 来实现,但感觉不太顺风水。
我目前的做法是硬硬写一个 ts 或 scss 作为 entry point,只是 html 不要 inject,虽然有瑕疵但勉强能用。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体
2023-01-11 JavaScript – Sort