写点什么

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

作者:CRMEB
  • 2022 年 3 月 10 日
  • 本文字数:6152 字

    阅读完需:约 20 分钟

目录

  • Vite vs. Webpack

  • 完整迁移实战

Vite vs. Webpack

指标对比

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


开发环节区别



webpack:

  • 先转译打包,然后启动 dev server

  • 热更新时,把改动过模块的相关依赖模块全部编译一次



vite:

  • 对于不会变动的第三方依赖,采用编译速度更快的 go 编写的 esbuild 预构建

  • 对于 js/jsx/css 等源码,转译为原生 ES Module(ESM)

  • 利用了现代浏览器支持 ESM,会自动向依赖的 Module 发出请求的特性

  • 直接启动 dev server (不需要打包),对请求的模块按需实时编译

  • 热更新时,仅让浏览器重新请求改动过的模块



目前由 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 验证

最后

如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163 相互学习,我们会有专业的技术答疑解惑

如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点 star:http://github.crmeb.net/u/defu不胜感激 !

PHP 学习手册:https://doc.crmeb.com

技术交流论坛:https://q.crmeb.com

用户头像

CRMEB

关注

还未添加个人签名 2021.11.02 加入

CRMEB就是客户关系管理+营销电商系统实现公众号端、微信小程序端、H5端、APP、PC端用户账号同步,能够快速积累客户、会员数据分析、智能转化客户、有效提高销售、会员维护、网络营销的一款企业应用

评论

发布
暂无评论
用 vite 2 平滑升级 vue 2 + webpack 项目实战_CRMEB_InfoQ写作平台