使用esbuild代替webpack构建react项目

2,021 阅读4分钟

前言


本文尝试将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顾名思义, 提供全局注入服务, 可以对等为webpackProvidePlugin, 但是配置麻烦了一点, 或者是亿点点

    define: {},
    

    这个与webpackDefinePlugin一致

    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, 万事俱备, 起飞

  • 成果

截屏2022-08-07 10.25.44.png 接口使用mock模拟, 页面路由切换时, 可以在控制台看到返回的数据也是类似于esm格式的js文件, 根据页面代码会有chunk和split的相关处理

demo在这里

开发构建速度与vite不相上下, (毕竟vite的开发环境构建也是esbuld实现的哈); 至此, 初步实现esbuild替代webpack进行开发环境构建web项目, 相比webpack丰富的插件, esbuild还是相对羸弱, 但是速度这块碾压webpack几条街, 还是值得提前学习下的.