前言
本文尝试将webpack更换为esbuild进行开发+生产环境构建react项目; 体验快到飞起的感觉.
背景
本项目是webpack5构建, 基于react18开发, redux4作为状态管理, 使用react-router5路由管理, postscc处理sass; 整合eslint代码风格统一.
动手
参照esbuild官网文档进行逐步替换:
不得不说esbuild的文档写的真的很清楚👍
-
npm安装esbuild
npm i esbuild
-
配置文件
esbuild提供了命令行+API两种方式进行构建, 对于web项目显而易见更适合配置文件使用API的方式;
可参照官方文档进行初步配置
在build文件夹下新建esbuild.dev.js文件, 引入构建开发服务API:
const { serve } = require('esbuild');配置服务所在端口以及文件目录
serve( { port: defPort + 1024, servedir: PackageUtils.getBuildPath(process.env.PACKAGE_ENV), }, { // 后续配置若未说明, 则均配置在此处 } )- 配置打包入口: 接受数组, 由于本应用位SPA, 所以为单入口, 多入口暂未探索, 欢迎大家一起探索研究
entryPoints: [path.resolve(PackageUtils.Config.entryPath, './index.js')],- 打包产物输出目录
outdir: PackageUtils.getBuildPath(process.env.PACKAGE_ENV),- 一些其它需要的
bundle: true, // 捆绑依赖至文件内 platform: 'browser', // 目标环境, web项目自然是浏览器 metafile: true, // mate文件, json对象, 一般用不到 sourcemap: false, // minify: true, // 开启压缩, 会压缩js+css splitting: true, // 分片, 提取公共代码进行缓存, 避免多次加载 chunkNames: '[ext]/[name]-[hash]', // chunk命名规则 assetNames: 'assets/[name]-[hash]', // 静态文件命名规则 charset: 'utf8', // 文件编码格式 drop: !isDevelopment ? [('debugger', 'console')] : [], // 打包时需去除的代码 treeShaking: true, // 大名鼎鼎的摇树, 必须开启, web项目同时需开启bundle- 重点配置
format: 'esm', // web项目优先esm, 要引领技术革新; 可选`iife`、`cjs`、`esm` target: ['es2016'], // esbuld最低支持es6, 所以干就完了- TS ts相关配置可参考官方文档, 本项目暂未整合
- 高级配置
inject: [path.resolve(PackageUtils.Config.scriptPath, './inject.js')],inject顾名思义, 提供全局注入服务, 可以对等为webpack的ProvidePlugin, 但是配置麻烦了一点, 或者是亿点点define: {},这个与
webpack的DefinePlugin一致loader: { '.html': 'text', '.js': 'jsx', '.jsx': 'jsx', '.scss': 'css', '.sass': 'css', '.less': 'css', '.css': 'css', '.png': 'dataurl', '.jpg': 'dataurl', '.jpeg': 'dataurl', },可以对等为webpack的loader, 但不开放配置, 可以根据文件后缀名称进行配置, 支持的加载器类型为
'js' | 'jsx' | 'ts' | 'tsx' | 'css' | 'json' | 'text' | 'base64' | 'file' | 'dataurl' | 'binary' | 'copy' | 'default'plugins: []插件这里就不展开了, 后面重点讲
-
脚本配置 不解释, 和webpack不能说完全相同, 简直可以说是一模一样
"scripts": { "dev": "cross-env PACKAGE_ENV=development node ./build/esbuild.dev.js", "test": "cross-env PACKAGE_ENV=test node ./build/esbuild.js", "pre": "cross-env PACKAGE_ENV=pre node ./build/esbuild.js", "build": "cross-env PACKAGE_ENV=production node ./build/esbuild.js" },到了这里, 按捺不住就
npm run dev了一下, 果然不出意外的红了, 不用急, 该开始重头戏插件了, 虽然esbuild天然支持jsx, 但是一些基于webpack的插件能力做的处理是需要我们再次处理的 -
插件
git上已有整理, 基本覆盖大部分web场景, 比如css、lit-css、less、sass、stylus、postcss、svg、markdown、glob-path、alias、cache、clean、copy-file等等; 但是, 大部分的插件的兼容性还有待验证, 个人实测部分插件均有这样那样的问题, 最后无奈自己上了🤦♂️
本项目针对sass、less使用postcss封装插件处理, 但sass文件内@import多方尝试无果后单独编写import插件处理; 引用esbuild-plugin-path-alias进行js路径别名处理; jsx文件样式scop插件; 还有静态文件的copy.
插件如下:
- js路径别名
const PathAliasPlugin = require('esbuild-plugin-path-alias') plugins: [ PathAliasPlugin({ '@': path.resolve(__dirname, '../'), }), ]- css@import处理
const CssImportPlugin = require('./esbuild-plugin-import').default plugins: [ CssImportPlugin(), ]- js import 路径通配符支持处理
const ImportGlobPlugin = require('./esbuild-plugin-import-glob').default plugins: [ ImportGlobPlugin({ resolveDir: path.resolve(__dirname, '../'), }), ]- jsx样式分包处理
const JsxStylePlugin = require('./esbuild-plugin-jsx-style').default plugins: [ JsxStylePlugin(), ]- postcss处理 这里参照webpack的css-loader进行处理, 以支持scop
const PostCssPlugin = require('./esbuild-plugin-postcss') plugins: [ PostCssPlugin.default({ rootDir: path.resolve(PackageUtils.Config.rootPath), plugins: [ require('postcss-modules-local-by-default')({ mode: 'local' }), require('postcss-modules-extract-imports'), require('postcss-modules-scope')({ generateScopedName: (name, path) => PackageUtils.getScopedName(name, path), }), autoprefixer({ overrideBrowserslist: ['>=5%'], }), ], writeToFile: true, sassOptions: { loadPaths: [path.resolve(PackageUtils.Config.entryPath, './style')], }, lessOptions: { javascriptEnabled: true, rootpath: path.resolve(PackageUtils.Config.rootPath, './node_modules'), }, }), ]- 静态文件copy
const PostCssPlugin = require('./esbuild-plugin-postcss') plugins: [ { name: 'copy-static-files', setup(build) { try { fs.rmSync(path.resolve(PackageUtils.getBuildPath(process.env.PACKAGE_ENV)), { recursive: true, }) } catch (e) {} build.onStart(() => { fs.mkdirSync(path.resolve(PackageUtils.getBuildPath(process.env.PACKAGE_ENV)), { recursive: true, }) fs.writeFileSync( path.resolve(PackageUtils.getBuildPath(process.env.PACKAGE_ENV), './index.html'), fs.readFileSync(path.resolve(PackageUtils.Config.entryPath, './index.html'), 'utf-8'), 'utf8' ) fs.writeFileSync( path.resolve(PackageUtils.getBuildPath(process.env.PACKAGE_ENV), './favicon.ico'), fs.readFileSync(path.resolve(PackageUtils.Config.entryPath, './favicon.ico'), 'utf-8'), 'utf8' ) }) }, }, ] -
dev-service
到这里, esbuild替代webpack基本完成, 接下来再修改下本地service配置, 以支持proxy, 参照官方文档进行一点点🤏修改
serve({...}, {...}).then(result => { const { port, host } = result const proxy = httpProxy.createProxyServer({ changeOrigin: true, ignorePath: false, }) http.createServer((req, res) => { if (req.url.indexOf('/api/') !== -1) { proxy.web(req, res, { target: HOST[process.env.PACKAGE_ENV], secure: false, changeOrigin: true, }) } else { proxy.web(req, res, { target: `http://${host}:${port}/`, }) } }).listen(defPort, function () { console.info(`server is running at http://127.0.0.1:${defPort}`) }) }) .catch(res => { console.log(res) process.exit(1) })
OK, 万事俱备, 起飞
- 成果
接口使用mock模拟,
页面路由切换时, 可以在控制台看到返回的数据也是类似于esm格式的js文件, 根据页面代码会有chunk和split的相关处理
开发构建速度与vite不相上下, (毕竟vite的开发环境构建也是esbuld实现的哈); 至此, 初步实现esbuild替代webpack进行开发环境构建web项目, 相比webpack丰富的插件, esbuild还是相对羸弱, 但是速度这块碾压webpack几条街, 还是值得提前学习下的.