用 vite 2 平滑升级 vue 2 + webpack 项目实战

简介: 用 vite 2 平滑升级 vue 2 + webpack 项目实战

目录

  • Vite vs. Webpack
  • 完整迁移实战

Vite vs. Webpack

指标对比

经过实际运行,在同一项目中、采用几乎相同的设置,结果如下:

指标 \ 工具 Vite Vite(legecy) Vue-cli + Webpack
npm run debug 至页面可用 (ms) 2405 4351 21418
npm run build 时间 (ms) 19727 82277 61000
打包后的 JS 文件数量 22 45 46
平均 JS 文件体积 (kb) 175 174 88
总 JS 文件体积 (kb) 3864 7832 4080

开发环节区别

image.png

webpack:

  • 先转译打包,然后启动 dev server
  • 热更新时,把改动过模块的相关依赖模块全部编译一次

image.png

vite:

  • 对于不会变动的第三方依赖,采用编译速度更快的go编写的esbuild预构建
  • 对于 js/jsx/css 等源码,转译为原生 ES Module(ESM)
  • 利用了现代浏览器支持 ESM,会自动向依赖的 Module 发出请求的特性
  • 直接启动 dev server (不需要打包),对请求的模块按需实时编译
  • 热更新时,仅让浏览器重新请求改动过的模块


ESM 及更早的 Javascript 模块化历史这里就不展开谈了,感兴趣的同学可以参阅这篇文章;总之太阳底下无新事,目前由 webpack 或 vite 做的这些架设本地服务、静态资源打包、动态更新的工作,起码追溯到十多年前陆续都有各种解决方案了

构建环节

  • 考虑到加载和缓存等,在生产环境中发布未打包的 ESM 仍然效率低下
  • vite 利用成熟的 Rollup,完成  tree-shaking、懒加载和 chunk 分割等

源码浅析

运行 vite 命令后:


-> start() // packages/vite/bin/vite.js
-> 利用 cac 工具构建可以处理 dev/build/preview 等命令的 cli 实例
-> cli.parse() // packages/vite/src/node/cli.ts

1. vite (dev 模式)


-> createServer() // packages/vite/src/node/server/index.ts
  - resolveHttpServer() // 基于 http 原生模块创建服务
  - createWebSocketServer() //  WebSocket 发送类似下面这样的热更新消息
  - chokidar.watch(path.resolve(root), ...) // 监听源码变化
-> handleHMRUpdate() // 处理热更新 packages/vite/src/node/server/hmr.ts
  -  updateModules()  
    ``````
      ws.send({
        type: 'update',
        updates
      })
    
    [浏览器中 ws://localhost:8080/my-report/]
    {
      "type": "update",
      "updates": [
        {
          "type": "js-update",
          "timestamp": 1646797458716,
          "path": "/src/app.vue",
          "acceptedPath": "/src/app.vue?vue&type=template&lang.js"
        }
      ]
    }
    ``````

浏览器中响应 hmr 的部分:


-> handleMessage() // packages/vite/src/client/client.ts
  ``````
     if (update.type === 'js-update') {
          queueUpdate(fetchUpdate(update))
        } else {
  ``````
-> fetchUpdate() 
  ``````
  // 利用了浏览器的动态引入 https://github.com/tc39/proposal-dynamic-import
  // 可见请求如 http://.../src/app.vue?import&t=1646797458716&vue&type=template&lang.js
    const newMod = await import(
      /* @vite-ignore */
      base +
        path.slice(1) +
        `?import&t=${timestamp}${query ? `&${query}` : ''}`
    )
  ``````

2. vite build


-> build() // packages/vite/src/node/cli.ts
-> doBuild() // packages/vite/src/node/build.ts
  - resolveConfig() // 处理 vite.config.js 和 cli 参数等配置
  - prepareOutDir() // 清空打包目录等
  - rollup.rollup()['write']() // 用 rollup 完成实际打包和写入工作

迁移实践

业务背景和迁移原则

迁移背景:

  • 现有项目的 webpack 开发调试和打包速度已经较慢
  • 查看后台统计数据,项目的浏览器覆盖情况可以支持抛掉历史包袱
  • 项目具有代表性,已经包含了 TS/JSX/FC 等写法的组件和模块
  • 需要渐进迈向 vue3 技术栈

升级原则:

  • 对原有开发打包流程无痛、交付产出物结构基本不变
  • 保证线上产品安全,设置观察期并 兼容 webpack 流程 而非直接替换
  • 覆盖后台访问记录中的主流浏览器并周知测试产品等研发环节

主要涉及文件:

  • /index.html -- 新的入口,原有 src/index.html 暂时保留
  • /vite.config.js -- vite 工具的配置文件

vite版本:

  • vite v2.8.2

node 版本:

  • node v14.19.0
  • 实践表明 v14 可以兼顾新的 vite 和既有 webpack 两套流程
  • 如果涉及 jenkins 等部署环节,可能需要关心相关 node 软件包的升级

package.json

依赖


"devDependencies": {
  "vite": "^2.8.2",
  "vite-plugin-vue2": "^1.9.3",
  "vite-plugin-html": "^3.0.4",
  "vite-plugin-time-reporter": "^1.0.0",
  "sass": "^1.49.7",
  "rollup-plugin-copy": "^3.4.0",
  "@vue/compiler-sfc": "^3.2.31",
},

npm scripts


"debug": "vite --mode dev",
"build": "vite build --mode production",
"preview": "vite preview --port 8082",

之前的 webpack 命令加前缀(如:"webpack:build"),继续可用

node-sass

升级版本,同时满足了 webpack/vite 的打包要求


-    "node-sass": "^4.9.2",
+    "node-sass": "^6.0.0",
-    "sass-loader": "^7.0.3",
+    "sass-loader": "^10.0.0"

index.html


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="shortcut icon" href="/src/assets/imgs/report.ico" />
    <link rel="stylesheet" href="<%- htmlWebpackPlugin.options.navCss %>" />
    <title><%- htmlWebpackPlugin.options.title %></title>
    <script
      type="text/javascript"
      src="<%- htmlWebpackPlugin.options.navJs %>"
    ></script>
  </head>
  <body>
    <div id="nav"></div>
    <div id="app"></div>
    <script type="module" src="/src/index.js"></script>
  </body>
</html>
  • 位于根目录,vite 默认的入口
  • 加入 type="module" 的入口文件 script 元素
  • <%= => 语法变为 <%- ->

基础配置

复用并完善了之前的打包和开发配置文件:


// build/config.js
module.exports = {
    title: '报表',
    // 打包文件夹名称
    base: 'my-report',
    // 调试配置
    debug: {
        pubDir: 'dist',
        assetsDir: 'assets',
        host: 'localhost',
        port: 8080,
        navCss: '/src/assets/common2.0/scss/nav-common.css',
        navJs: '/src/assets/common2.0/js/nav-common.js',
        proxy: {
            target: 'http://api.foo.com'
        }
    },
    // 生产配置
    prod: {
        navJs: '/public/v3/js/nav-common.js',
        navCss: '/public/v3/css/nav-common.css',
    }
};


vite.config.js 基本结构


import {createVuePlugin} from 'vite-plugin-vue2';
export default ({mode}) => {
    const isProduction = mode === 'production';
    return defineConfig({
        base: `/${config.base}/`,
    logLevel: 'info',
    // 插件,兼容 rollup
        plugins: [
      // vue2 和 jsx
        createVuePlugin({
            jsx: true,
            jsxOptions: {
                compositionAPI: true
            }
        }),
      // 打包统计
          timeReporter()
        ],
    // devServer 设置
    server: {},
    // 依赖解析规则等
    resolve: {
      alias: {}
    },
    // 打包目录、素材目录、rollup原生选项等
    build: {}
    });
};

resolve 的迁移

之前 webpack 中的配置:


resolve: {
    extensions: ['.ts', '.tsx', '.vue', '.js', '.jsx', '.json', '.css', '.scss'],
    alias: {
        '@': path.resolve(__dirname, '../src'),
        assets: path.resolve(__dirname, '../src/assets'),
        vue$: path.resolve(__dirname, '../node_modules', 'vue/dist/vue.esm.js')
    },
    symlinks: false
},

vite 中的写法:


resolve: {
    extensions: ['.ts', '.tsx', '.vue', '.js', '.jsx', '.json', '.css', '.scss'],
    alias: [
        {
            find: '@',
            replacement: path.resolve(__dirname, 'src')
        },
        {
            find: 'assets',
            replacement: path.resolve(__dirname, 'src', 'assets')
        },
        {
            find: 'vue$',
            replacement: path.resolve(__dirname, 'node_modules', 'vue/dist/vue.esm.js')
        },
        {
            find: '~@foo/src/styles/common/publicVar',
            replacement: 'node_modules/@foo/src/styles/common/_publicVar.scss'
        },
        {
            find: '~@foo/src/styles/mixins/all',
            replacement: 'node_modules/@foo/src/styles/mixins/_all.scss'
        }
    ]
},

以上最后两项配置属于之前引用的错误路径,vite 无法跳过,并将引起打包失败;需要修正引用或在此特殊处理

build 的迁移

之前 webpack 中的配置:


context: path.resolve(__dirname, '../'),
mode: isProduction ? 'production' : 'development',
entry: {
    index: './src/index.js'
},
output: {
    path: path.resolve(__dirname, '../dist', config.base),
    publicPath,
    filename: isProduction ? 'assets/js/[name].[contenthash:8].js' : 'assets/js/[name].[hash:8].js',
    chunkFilename: isProduction
        ? 'assets/js/[name].[contenthash:8].chunk.js'
        : 'assets/js/[name].[hash:8].chunk.js'
},
performance: {
    maxEntrypointSize: 2000000,
    maxAssetSize: 1000000
}

vite 中的写法:


build: {
    outDir: `${pubDir}/${config.base}`,
    assetsDir,
    rollupOptions: {
    },
    chunkSizeWarningLimit: 1000000,
    cssCodeSplit: true
}


直接拷贝的素材

  • 业务中有一部分动态路径的素材图引用 <img :src="path">,path 可能为 assets/imgs/noData.png 这样的相对路径
  • webpack 中用 'copy-webpack-plugin' 插件拷贝图片到发布目录下,调试过程中是可以访问到的
  • vite 用拷贝插件 'rollup-plugin-copy' 同样可以拷贝成功,但调试进程中访问不了 dist 目录


import copy from 'rollup-plugin-copy';
...
// 打包时才拷贝
plugins: [
  isProduction
    ? copy({
          targets: [
              {
                  src: path.resolve(__dirname, 'src/assets/imgs'),
                  dest: `${pubDir}/${config.base}/${assetsDir}`
              }
          ],
          hook: 'writeBundle'
      })
    : void 0,
],
// 调试过程中特殊转写
server: {
    proxy: {
        '/my-report/assets/imgs/': {
            target: `http://${host}:${port}/`,
            rewrite: path => path.replace('assets', 'src/assets')
        }
    },
}

特殊的外部引用

  • vite 需要用 'vite-plugin-html' 插件来达成和兼容与 'html-webpack-plugin' 一样的 html 注入效果
  • 形如 '/public/v3/css/nav-common.css' 这样的特殊引用,不符合 vite 内部的保留策略,会被删除原 <link> 标签并转换成 js import,这将造成页面无法正常访问
  • 结合自定义插件实现打包过程中的 hack 和打包结束后的恢复


import {createHtmlPlugin} from 'vite-plugin-html';
...
const indexReplaceHolder = '//fakePrefix';
...
plugins: [
    createHtmlPlugin({
        template: 'index.html',
        minify: true,
        inject: {
            data: {
                htmlWebpackPlugin: {
                    options: {
                        title: config.title,
                        navCss: isProduction ? indexReplaceHolder + config.prod.navCss : config.debug.navCss,
                        navJs: isProduction ? indexReplaceHolder + config.prod.navJs : config.debug.navJs
                    }
                }
            }
        }
    }),
    (function() {
        let viteConfig;
        return {
            name: 'vite-plugin-fix-index',
            configResolved(resolvedConfig) {
                viteConfig = resolvedConfig;
            },
            transformIndexHtml(code) {
                if (viteConfig.command === 'build' && isProduction) {
                    const re = new RegExp(indexReplaceHolder, 'g');
                    code = code.replace(re, '');
                }
                return code;
            }
        };
    })(),
],

传统浏览器兼容

  • vite 用 @vitejs/plugin-legacy 插件为打包后的文件提供传统浏览器兼容性支持
  • legacy 对 build 速度影响较大,酌情采用


plugins: [
  legacy({
    targets: ['> 1%', 'last 2 versions', 'not ie <= 10']
  }),
]

legecy后全局 css 失效

环境变量

  • process.env 的写法在 vite 中改为了 import.meta,并且使用上有差异


// src/utils/env.js
export const getEnvMode = () => {
    try {
        // eslint-disable-next-line
        if (typeof process !== 'undefined' && process.env) {
            // eslint-disable-next-line
            return process.env.NODE_ENV;
        }
        // eslint-disable-next-line
        if (import.meta && import.meta.env) {
            return import.meta.env.MODE;
        }
    } catch (e) {
        console.log(e);
    }
};
// package.json
"devDependencies": {
  "@open-wc/webpack-import-meta-loader": "^0.4.7",  
}


// webpack -> module -> rules
{
  test: /\.jsx?$/,
  -loader: 'babel-loader',
  +loaders: ['babel-loader', {loader: require.resolve('@open-wc/webpack-import-meta-loader')}],
  include: [path.resolve(__dirname, '../src')]
}


// jest.config.js -> collectCoverageFrom
[  '!<rootDir>/src/utils/env.js']


// __tests__/setup.js 
jest.mock('../src/utils/env.js', () => {
    return {
        getEnvMode: () => 'production'
    };
});

require.ensure

  • 暂时没有很好的兼容写法,应尽量避免

new Set()

  • 如果使用了 Map/Set 等 ES6 的类型且没有使用 polyfill,应该注意其行为
  • 比如 Set 的值可能在 webpack/babel 的转写中会自动变为数组,而新的流程中需要手动用 Array.from() 处理

总结

  • webpack 工作流基本可以被 vite 完整复刻,适应线上平滑升级
  • 基于浏览器访问记录评估,大部分项目可以享受 vite 极速打包福利
  • 对于需要兼容 IE 11 等特殊情况的,需要充分测试后,考虑用 legecy 模式迁移
  • 需要注意生产环境rollup打包与开发环境的代码会不一致,最好用 preview 验证

参考资料



相关文章
|
8天前
|
前端开发 JavaScript 开发者
工程化(webpack+vite)
在现代前端开发中,工程化是提高开发效率和项目质量的关键。UniApp 结合 Webpack 和 Vite,提供强大的工程化支持。Webpack 功能强大,支持复杂项目的构建;Vite 则利用现代浏览器的 ESM 特性,提供快速的开发体验。开发者可根据项目需求选择合适的工具,显著提升开发效率和项目质量。
|
21天前
|
前端开发 JavaScript 开发者
工程化(webpack+vite)
工程化(webpack+vite)
|
2月前
|
前端开发 JavaScript 开发者
工程化(webpack+vite)
工程化(webpack+vite)
|
4月前
|
JavaScript Windows
安装node.js与webpack创建vue2项目
安装node.js与webpack创建vue2项目
35 1
|
6月前
|
前端开发 JavaScript 开发者
如何在Vite和Webpack之间选择合适的构建工具?
【4月更文挑战第14天】选择Vite或Webpack取决于项目需求、团队熟悉度和场景。Vite适合快速开发,小到中型项目,Vue.js技术栈,有较简单的配置和快速冷启动。而Webpack在大型项目中占优,提供深度优化,丰富的插件生态系统,适合复杂构建需求和React项目。考虑因素还包括学习曲线和社区支持,最佳工具应满足项目当前及未来需求。
69 2
|
6月前
|
前端开发 JavaScript 开发者
vite和webpack区别
【4月更文挑战第14天】Vite与Webpack都是前端构建工具,各有特点。Vite凭借原冷启动和模块热更新,适合现代前端项目,尤其是Vue、React等。它的配置简单,但社区支持较小。相比之下,Webpack拥有强大的插件系统和广泛社区支持,能适应各种项目需求,但配置复杂,启动慢。开发者应根据项目需求选择合适的工具。
155 2
|
6月前
|
前端开发 JavaScript Go
webpack -vite(Rollup )-Gulp (一)
webpack -vite(Rollup )-Gulp (一)
109 0
|
6月前
|
JavaScript 开发者
Vite和Webpack的区别是什么
Vite和Webpack的区别是什么
|
6月前
|
前端开发 JavaScript 容器
前端vw自适应解决方案,适用pc端以及移动端,适用webpack以及vite,适用vue以及react
前端vw自适应解决方案,适用pc端以及移动端,适用webpack以及vite,适用vue以及react
400 0
|
2月前
|
JavaScript
webpack打包TS
webpack打包TS
132 60