基于 Vue2.0 从 webpack 4.* 切换到 vite 2.9.5

基于 Vue2.0 从 webpack 4.* 切换到 vite 2.9.5

vite已经出了一段时间了,鉴于其启动及热更新速度的大幅提升,也逐渐被大家所熟知,具体原理可以参见vite的官方文档,这里就不多说了。当前项目组是基于webpack 4.*构建的,脚手架采用vue-cli那一套,随着项目维护人员几经易手,再加上项目代码量快速增加,启动速度已经非常慢,同时热更新的实时性也没有保障。借助着项目框架改造升级的机会,打算将工程化切换到vite上,毕竟是新东西,很多参考和样例都不足,跌跌撞撞,花了两个星期时间逐步将工程改造完毕,基于此将改造中遇到的问题,记录并分享出来,供大家参考。

当前项目的技术栈:vue2.0 + elementUI

运行环境:node (v14.16.1)

一、安装 Vite 环境

1. 安装依赖

直接通过 npm install vite安装即可,接下来要兼容vue 2.0的使用场景,还需要安装以下几个依赖:

  1. vite-plugin-vue2 (vue 2.*的兼容依赖)
  2. @vue/compiler-sfc (单文件组件兼容依赖)
  3. @vue/composition-api (组合api的兼容依赖)

备注:如果项目中使用了 css 预编译处理,比如我们用的是 scss,还需要安装 sass 依赖,vite 支持 sass 的开箱即用,所以没有之前烦人的 node-sass 安装的难题。

2. 配置 vite.config.js

和 vue-cli 需要配置 vue.config.js 一样,vite 也需要添加一个配置文件 vite.config.js,只不过前者用的是 cjs规范, 后者用的是esm 规范,vite.config.js 的基础配置如下:

import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2';

export default function({ mode }) {
  return defineConfig({
  	plugins: [createVuePlugin()]
  });
};

是不是很简单,没有了那么多复杂的加载器(loader)配置,到目前为止,我们就基本拥有了最基础的 vite 运行环境了。

3.配置启动脚本

接下来我们在 package.json 文件中配置启动脚本:

"scripts": {
  "devv": "vite",
  "buildv": "vite build",
  "pre": "vite preview"
},

接下来就可以把工程抱起来看看了,但是事与愿违,工程没有跑起来,反而出了一大推错误,下面就详细记录我遇到的问题,分享给大家:

二、遇到的问题

1. index.html 的位置问题

这个问题 vite 官网已经解释的很详细,就是说 index.html 现在跟着源码走(放在与 vite.config.js 同级),不在放在 public 文件夹下面了,如果你不介意,直接把 public 文件夹下的 index.html 拖出来就好了。

但是我不想动代码结构,还想放到public下面有没有办法?有,安装插件即可,已经有现成的插件了:vite-plugin-html,也可以自己写个插件,这里我以 vite-plugin-html 为例,详细配置可以参考插件的官方文档:

import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2';
import { createHtmlPlugin } from 'vite-plugin-html';

export default function({ mode }) {
  return defineConfig({
  	plugins: [
        //……
        createHtmlPlugin({
            minify: true,
         	entry: '/public/index.html',
         	inject: {
            	data: {
               		title: 'demo',
               		injectScript: `<script src="./inject.js"></script>`
            	},
         	},
      	})
      	//……
  	]
  });
};

由于 vite 是基于 esm 规范加载的,所以在 index.html 中需要标记入口文件的类型为 module:

<script src="src/main.js" type="module" charset="utf-8"></script>
2. 路径别名(alias)

vite 中别名设置的匹配规则有变化,比如你有如下配置:

 {
    resolve: {
      alias: {
        '@': path.join(__dirname, 'src'),
        '~': path.join(__dirname, 'node_modules'), // 这个通常给 css import 用的
      }
    }
}

他们会被 @/xxxx~/xxxx 命中,但是不会被 @xxxx~xxxx 命中,有些引入css的代码如: @import ~normalize.css/normalize.css,这种是无法命中的,需要将配置改写如下(精确匹配):

{
	resolve: {
        extensions: ['.js', '.vue', '.json', '.jsx', '.tsx', '.ts'],
        alias: [
            { find: /* @/ *//^@(?=\/)/, replacement: path.join(__dirname, 'src') },
            { find: /* ~/ *//^~(?=\/)/, replacement: path.join(__dirname, 'node_modules') },
            { find: /* ~@/ *//^~@(?=\/)/, replacement: path.join(__dirname, 'src') },
            { find: /* ~ *//^~(?![\/|@])/, replacement: path.join(__dirname, 'node_modules/') },
        ]
    }
} 
3. 深度选择器:/deep/ —-> ::v-deep

这个问题是在升级 sass 或者 dart-sass(node-sass已停止更新,且安装过程容易出错)后出现的,/deep/ 是 vue2.0 的语法,在vite中需要使用 vue 3.0 的语法: ::v-deep

需要全局替换深度选择器。

4. 加载 svg

在 vue-cli 中 svg 的加载需要引入对应的 svg 加载器(如:svg-sprite-loader);在 vite 中,vite-plugin-svg-icons插件可以帮你干同样的事情,首先在 vite.config.js 中引入和配置插件:

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';

// ……
plugins: [
     	// ……
        createSvgIconsPlugin({
            // 指定需要缓存的图标文件夹
            iconDirs: [path.resolve(process.cwd(), 'src/icons/svg')],
            // 指定symbolId格式
            symbolId: 'icon-[name]',
        })
        // ……
];
// ……

还需要一步,即在项目初始化时,提前加载 svg 图标,这里在 ./src/main.js 中全局注册 svg 图标:

import 'virtual:svg-icons-register'; // 全局加载svg到缓存
5. env文件的读取(.env、.dev.env、.prod.env)

在 webpack 中,我们使用 process.env来读取.env文件中的变量,在 vite中,我们在不同的场景可以用不同的方式读取:

  • 在 vite.config.js 文件中,需要用 loadEnv方法来读取:
import { defineConfig, loadEnv } from 'vite';

export default function({ mode }) {
  return defineConfig({
  	server: {
         proxy: {
            '^/(abc|bcd)': {
                target: loadEnv(mode, process.cwd()).VITE_proxy,
                changOrigin: true,
                secure: false,
            }
        }
    } 
  });
};
  • 在工程代码中,我们可以用 import.meta.env 来读取(如 ./src/main.js):
if(import.meta.env.VITE_Mode === "test") {
	// TODO
}

注意,vite 的本地化变量需要以 VITE_开头,否则上述方式均读取不到,应该可以修改这个前缀,具体参考 vite 的官方文档。

6. require(../../image/sss.png) 加载图片的问题

vite 采用 esm 规范对模块进行加载,不支持 require 的方式对图片或其他资源进行加载,不过已经有很多插件对这一部分进行了兼容。vite 提供了通过原生 API 解决此问题的方式:

export default {
    data() {
        return {
            bgBlank: new URL(`../../assets/bg-blank.png`, import.meta.url).href,
            configParams: {}
        };
    }
}

注意:

  • new URL() 中的路径必须是相对路径,且不支持alias别名路径。(vite未作兼容)
  • 如果系统中使用new URL() 的地方比较多,你想抽象成一个通用函数,那么你最好不要这么做,因为抽象成函数后,在 development 模式下没有问题,但在 production 模式下 vite 将无法根据 new URL 关键字去解析资源路径,因为资源路径是通过参数传递的,真正执行时发现仍然是个相对路径字符串,在dist目录下是找不到该路径的, 所以会报错,这个应该 vite 打包时没有兼容好,希望后续能兼容这种情况。

另外,还有一种替代方式加载预加载图片,即将 require 改成 import 进行图片引入,一般通过 import 引入的图片均缓存在文件头部变量中:

import fitmgr from '../assets/cloud/fitmgr.png';
7.require.context、require 动态加载文件

和 require 加载图片一样,vite 也不支持 require.context 动态读取并加载本地文件,vite 提供了两个方法来做这件事:

1)import.meta.glob 异步读取文件,读取的文件在打包时以单独的 chunk 来加载;

2)import.meta.globEager 同步读取文件,打包时会将读取的内容打包到一个整体 chunk 中;

注意:这两个方法都支持alias别名,但不支持传参和模板字符串,也就是说给出的路径必须是可访问的全路径(相对或者绝对)

两者都可以用来读取本地目录,在根据目录中的文件名来处理相关逻辑时非常有用,如:

动态组织路由

采用 import.meta.glob异步加载,这样每个路由对应的组件均能独立打包成一个chunk。

const vueModules = import.meta.glob("@/**/*.vue");
/**
 * 将组件字符串路径转换为异步组件
 * @param {string} component 组件的字符串路径
 * @returns {promise}
 */
function lazyLoading(component) {
    component = component.trim();
    return vueModules[`../${component}.vue`];
}

routesData.map(item => {
	item.meta.routeId = item.menuId;
    // 根据名字动态查查读取的内容
	item.component = lazyLoading(item.component);
});

动态加载 svg 图片

只是为了获取文件夹下 svg 文件的文件名,采用 import.meta.globEager同步方式读取。

// 组件定义
<template>
    <div class="svg-icon">
        <!-- svg -->
        <svg v-else-if="svgNames.includes(svgiconClass)" :class="className" aria-hidden="true" v-on="$listeners">
            <use :xlink:href="`#${svgiconClass}`" />
        </svg>
    </div>
</template>

<script>
const svgNames = Object.keys(import.meta.globEager('@/icons/svg/*.svg')).reduce((r, c) => [...r, `icon-${c.match(/\/([^/]*)\.svg/)[1]}`], []);

export default {
    data() {
        return {
            svgNames: svgNames
        };
    },
    props: ['iconClass', 'className']
    computed: {
        svgiconClass() {
            return `icon-${this.iconClass}`;
        }
    }
};
</script>

// 组件使用
<svg-icon icon-class="avatar" class="user-avatar" />

注意:这两个方法没有 require.context 那么灵活,更倾向于全局缓存,局部按需引用。

8.commonjs 依赖库的加载

这个问题是最棘手的,由于 vite 是基于 esm 规范打包的,所有的文件及依赖分析都是基于 ESModule 的方式进行的,如果我们的依赖或者代码中出现了 commonjs 规范的内容,则会报错。

处理方式有两种:

1)升级对应的依赖包、库,现在很多依赖包、库的新版本均支持 esm 规范,引入即可,注意升级后的使用方式可能有变化,如 vue-echarts 依赖,v6.* 之前都是 commonjs,v6.* 之后是 esm ;另外,对于业务代码中编写的 commonjs 代码,手动改造成 esm 规范代码;

2)引入兼容插件, @originjs/vite-plugin-commonjs ,该插件兼容处理 commonjs 模块,我试了下,并不是 100% 解决commonjs 模块依赖问题,具体用法可以查看插件官方文档。

3)迁移 commonjs 依赖包、库为外部引入,即下载对应依赖包的 cdn 版本,放到工程的静态文件夹(public)下进行管理,然后在 index.html 中前置引入即可,在生产模式下,需要配置 cdn 包的全局引入变量,详见下例。

index.html 中添加库的引入(以 @jiaminghi/data-view为例,当前最新版本仍然是 commonjs 版本):

<script src="util/datav.min.vue.js"></script>

vite.config.js 文件 external 块中配置全局引入变量:

external: ["CryptoJS", "jsonlint"]

在业务代码中,就可以直接用 CryptoJSjsonlint 变量来调用库中的方法了,并且能顺利通过 vite 的依赖分析。

9.css 中:export 语法的兼容

在 scss 变量文件中,有时我们既要被其他 .scss 引入,又想在 js 代码中导入该变量集合,于是我们在 scss 变量文件中这么定义(如:common.variable.scss):

/* theme color */
$--color-primary: #409eff;
$--color-success: #1CB02C;
$--color-warning: #F28202;
$--color-danger: #F55036;

:export {
  primary: $--color-primary;
  success:$--color-success;
  warning:$--color-warning;
  danger:$--color-danger;
  dialog_header:$--dialog-header-dark;
}

在 webpack 的环境下,上面这样跑起来没有问题,但在 vite 下就不行了,从官网中得知 vite 支持 css module,需要将上述文件名修改为:

common.variable.scss   --------->    common.variable.module.scss

没错,就是添加了 .module 进行标识,vite 会对其采用 css module 的方式进行读取。

还有个问题,一般来说 scss 变量在公共文件中引入后,全局可用,如以下方式:

index.scss 文件中引入变量文件和其他 scss 文件

// index.scss 文件中引入变量
@import "./ariables.module.scss";
@import "./btn.scss";

按理说,在 "./btn.scss" 文件中应该能使用 "./ariables.module.scss" 中定义的变量,但是在 vite 下会报错,即找不到对应的变量。

具体原因未知,直接上处理方式,vite 支持全局配置引入 scss 变量,vite.config.js 中配置如下:

css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/ariables.module.scss";`
      }
    }
}

然而事实并没有那么顺利,上述配置也会读取"./ariables.module.scss"文件并在其头部引入自身,变成了循环加载:

 [vite] Internal server error: This file is already being loaded.

感觉preprocessorOptions应该不会这么差劲,应该有什么办法可以排除加载变量文件本身才对,此处没有深究,接下来对additionalData节点进行改造,因为他支持函数:

css: {
    preprocessorOptions: {
        scss: {
            additionalData: (content, loaderContext) => {
                // 如果是变量文件本身,则直接返回内容
                if (loaderContext.endsWith('variables.module.scss')) return content;
                // 其他 scss 文件才拼接变量文件加载前缀
                return `@import 'src/styles/variables.module.scss'; ${content}`
            }
        }
    }
}

至此,对于 css 相关的调试,一切都安静了。

10.jsx语法的兼容

在 vue 的语法中,我们一般都是通过 template 进行页面 html 的定义和封装,但是有时候我们需要自定义渲染,不错,就是 render 函数,如下:

<script lang="jsx">
    export default {
        render() {
            return <div class={['code-preview-wrapper', this.className]}>
                <style lang='scss'>{this.scopedStyle}</style>
                <span id={this.uid} />
                {errorDom}
            </div>;
        },
    }
</script>

在 vite 环境中,需要显式定义 script 模块的解析模式 lang="jsx",同时在 vite.config.js 中添加对应的配置:

plugins: [
    createVuePlugin({
        jsx: true, // jsx语法兼容处理
    })
 ]
11.vue.extend语法的兼容处理

使用过 vue.extend 函数分装组件的同学都知道,这个函数的功能很强大,但是我翻遍了 vue3.0 官网、vite官网,都没有提及此 API 的升级兼容方案,实际情况是,通过此 API 封装的组件在 vite 模式下,无法正确的渲染。

解决办法,修改此类组件,用 sfc 的方式实现一遍,这个确实有点坑,希望后续能有更好的办法解决。

12.其他问题

其他的一些细节问题,不展开讲,归总如下:

1) vue 组件模板语法块中定义的 lang="html"需要去掉;

 <template lang="html"></template>

2) import path from 'path'; 对 path 的引用,需要改写为 import path from 'path-browserify';