乐趣区

关于构建工具:vuecli-迁移-vite2-实践小结

两周前(202.02.17),vite2.0 公布了,作为应用了浏览器原生 ESM 为下一代前端工具,vite 2.0 相较于 1.0 更加成熟。在此之前笔者就开始关注这类「新型」的前端工具。这次趁着 vite 2.0 公布,也胜利将一个基于 vue-cli(-service) + vue2 的已有我的项目进行了迁徙。

迁徙工作比较顺利,花了不到半天工夫。但整个迁徙过程中也遇到了一些小问题,这里汇总一下,也不便遇到相似问题的敌人一起交换和参考。

我的项目背景

在介绍具体迁徙工作前,先简略介绍下我的项目状况。目前该我的项目上线不到一年,不太有构建相干的历史遗留债权。我的项目蕴含 1897 个模块文件(包含 node\_modules 中模块),应用了 vue2 + vuex + typescript 的技术栈,构建工具应用的是 vue-cli(webpack)。算是一套比拟规范的 vue 技术栈。因为是外部零碎,我的项目对兼容性的要求较低,用户根本都应用较新的 Chrome 浏览器(少部分应用 Safari)。

迁徙工作

上面具体来说下迁徙中都做了哪些解决。

1、配置文件

首先须要装置 vite 并创立 vite 的配置文件。

npm i -D vite

vue-cli-service 中应用 vue.config.js 作为配置文件;而 vite 则默认会须要创立一个 vite.config.ts 来作为配置文件。根底的配置文件很简略:

import {defineConfig} from 'vite';

export default defineConfig({
  plugins: [// ...],
})

创立该配置文件,之前的 vue.config.js 就不再应用了。

2、入口与 HTML 文件

在 vite 中也须要指定入口文件。但和 webpack 不同,在 vite 中不是指定 js/ts 作为入口,而是指定理论的 HTML 文件作为入口。

在 webpack 中,用户通过将 entry 设置为入口 js(例如 src/app.js)来指定 js 打包的入口文件,辅以 HtmlWebpackPlugin 将生成的 js 文件门路注入到 HTML 中。而 vite 间接应用 HTML 文件,它会解析 HTML 中的 script 标签来找到入口的 js 文件。

因而,咱们在入口 HTML 中退出对 js/ts 文件的 script 标签援用:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>

<body>
  <noscript>
    We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
  </noscript>
  <div id="app"></div>
+ <script type="module" src="/src/main.ts"></script>
</body>

</html>

留神下面 <script type="module" src="/src/main.ts"></script> 这一行,它应用浏览器原生的 ESM 来加载该脚本,/src/main.ts 就是入口 js 的源码地位。在 vite dev 模式启动时,它其实启动了一个相似动态服务器的 server,将 serve 源码目录,因而不须要像 webpack 那样的简单模块打包过程。模块的依赖加载将齐全依靠于浏览器中对 import 语法的解决,因而能够看到很长一串的脚本加载瀑布流:

这里还须要留神 project root 的设置。在默认是 process.cwd(),而 index.html 也会在 project root 下进行寻找。为了不便我将 ./public/index.html 移到了 ./index.html

3、应用 vue 插件

vite 2.0 提供了对 vue 我的项目的良好反对,但其自身并不和 vue 进行较强耦合,因而通过插件的模式来反对对 vue 技术栈的我的项目进行构建。vite 2.0 官网目前(2021.2.28)举荐的 vue 插件会和 vue3 的 SFC 一起应用更好。因而这里应用了一个专门用来反对 vue2 的插件 vite-plugin-vue2,反对 JSX,同时目前最新版本也是反对 vite2 的。

应用上也很简略:

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

export default defineConfig({
  plugins: [+   createVuePlugin(),
  ],
});

4、解决 typescript 门路映射

应用 vite 构建 ts 我的项目时,如果应用了 typescript 门路映射的性能,就须要进行非凡解决,否则会呈现模块无奈解析(找不到)的谬误:

这里须要应用 vite-tsconfig-paths 这个插件来做门路映射的解析替换。其原理较为简单,大抵就是 vite 插件的 resolveId 钩子阶段,利用 tsconfig-paths 这个库来将门路映射解析为理论映射返回。有趣味的能够看下该插件的实现,比拟简短。

具体应用形式如下:

import {defineConfig} from 'vite';
import {createVuePlugin} from 'vite-plugin-vue2';
+ import tsconfigPaths from 'vite-tsconfig-paths';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [createVuePlugin(),
+   tsconfigPaths(),],
});

5、替换 CommonJS

vite 应用 ESM 作为模块化计划,因而不反对应用 require 形式来导入模块。否则在运行时会报 Uncaught ReferenceError: require is not defined 的谬误(浏览器并不反对 CJS,天然没有 require 办法注入)。

此外,也可能会遇到 ESM 和 CJS 的兼容问题。当然这并不是 vite 构建所导致的问题,但须要留神这一点。简略来说就是 ESM 有 default 这个概念,而 CJS 没有。任何导出的变量在 CJS 看来都是 module.exports 这个对象上的属性,ESM 的 default 导出也只是 cjs 上的 module.exports.default 属性而已。例如在 typescript 中咱们会通过 esModuleInterop 配置来让 tsc 增加一些兼容代码帮忙解析导入的模块,webpack 中也有相似操作。

例如之前的代码:

module.exports = {
    SSO_LOGIN_URL: 'https://xxx.yyy.com',
    SSO_LOGOUT_URL: 'https://xxx.yyy.com/cas/logout',
}
const config = require('./config');

在导出和导入上都须要批改为 ESM,例如:

export default {
    SSO_LOGIN_URL: 'https://xxx.yyy.com',
    SSO_LOGOUT_URL: 'https://xxx.yyy.com/cas/logout',
}
import config from './config';

6、环境变量的应用形式

应用 vue-cli(webpack)时咱们常常会利用环境变量来做运行时的代码判断,例如:

const REPORTER_HOST = process.env.REPORTER_TYPE === 'mock'
  ? 'http://mock-report.xxx.com'
  : 'http://report.xxx.com';

vite 依然反对环境变量的应用,但不再提供 process.env 这样的拜访形式。而是须要通过 import.meta.env 来拜访环境变量:

-const REPORTER_HOST = process.env.REPORTER_TYPE === 'mock'
+const REPORTER_HOST = import.meta.env.REPORTER_TYPE === 'mock'
  ? 'http://mock-report.xxx.com'
  : 'http://report.xxx.com';

与 webpack 相似,vite 也内置了一些环境变量,能够间接应用。

7、import.meta.env types

如果在 typescript 中通过 import.meta.env 来拜访环境变量,可能会有一个 ts 谬误提醒: 类型“ImportMeta”上不存在属性“env”

这是因为在目前版本下(v4.2.2)import.meta 的定义还是一个空的 interface:

interface ImportMeta {}

但咱们能够通过 interface 的 merge 能力,在我的项目中进一步定义 ImportMeta 的类型来拓展对 import.meta.env 的类型反对。例如之前通过 vue-cli 生成的 ts 我的项目在 src 目录下会生成 vue-shims.d.ts 文件,能够在这里拓展 env 类型的反对:

declare global {
  interface ImportMeta {env: Record<string, unknown>;}
}

这样就不会报错了。

8、webpack require context

在 webpack 中咱们能够通过 require.context 办法「动静」解析模块。比拟罕用的一个做法就是指定某个目录,通过正则匹配等形式加载某些模块,这样在后续减少新的模块后,能够起到「动静主动导入」的成果。

例如在我的项目中,咱们动静匹配 modules 文件夹下的 route.ts 文件,在全局的 vue-router 中设置 router 配置:

const routes = require.context('./modules', true, /([\w\d-]+)\/routes\.ts/)
    .keys()
    .map(id => context(id))
    .map(mod => mod.__esModule ? mod.default : mod)
    .reduce((pre, list) => [...pre, ...list], []);

export default new VueRouter({routes});

文件构造如下:

src/modules
├── admin
│   ├── pages
│   └── routes.ts
├── alert
│   ├── components
│   ├── pages
│   ├── routes.ts
│   ├── store.ts
│   └── utils
├── environment
│   ├── store
│   ├── types
│   └── utils
└── service
    ├── assets
    ├── pages
    ├── routes.ts
    ├── store
    └── types

require context 是 webpack 提供的特有的模块办法,并不是语言规范,所以在 vite 中不再能应用 require context。但如果齐全改为开发者手动 import 模块,一来是对已有代码改变容易产生模块导入的脱漏;二来是放弃了这种「灵便」的机制,对后续的开发模式也会有肯定扭转。但好在 vite2.0 提供了 glob 模式的模块导入。该性能能够实现上述指标。当然,会须要做肯定的代码改变:

const routesModules = import.meta.globEager<{default: unknown[]}>('./modules/**/routes.ts');
const routes = Object
  .keys(routesModules)
  .reduce<any[]>((pre, k) => [...pre, ...routesMod[k].default], []);

export default new VueRouter({routes});

次要就是将 require.context 改为 import.meta.globEager,同时适配返回值类型。当然,为了反对 types,能够为 ImportMeta 接口增加一些类型:

declare global {
  interface ImportMeta {
    env: Record<string, unknown>;
+   globEager<T = unknown>(globPath: string): Record<string, T>;
  }
}

此外再提一下,import.meta.globEager 会在构建时做动态剖析将代码替换为动态 import 语句。如果心愿能反对 dynamic import,请应用 import.meta.glob 办法。

9、API 代理

vite2.0 本地开发时(DEV 模式)依然提供了一个 HTTP server,同时也反对通过 proxy 项设置代理。其背地和 webpack 一样也是应用了 http-proxy,因而针对 vue-cli 的 proxy 设置能够迁徙到 vite 中:

import {defineConfig} from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import {createVuePlugin} from 'vite-plugin-vue2';
+ import proxy from './src/tangram/proxy-table';

export default defineConfig({
  plugins: [tsconfigPaths(),
    createVuePlugin(),],
+ server: {
+   proxy,
+ }
});

10、HTML 内容插入

在基于 vue-cli 中咱们能够利用 webpack 的 HtmlWebpackPlugin 来实现 HTML 中值的替换,例如 <%= htmlWebpackPlugin.options.title %> 这种模式来将该处模板变量在编译时,替换为理论的 title 值。要实现这样的性能也非常简单,例如 vite-plugin-html。这个插件基于 ejs 来实现模板变量注入,通过 transformIndexHtml 钩子,接管原始 HTML 字符串,而后通过 ejs 渲染注入的变量后返回。

上面是迁徙后,应用 vite-plugin-html 的配置形式:

import {defineConfig} from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import {createVuePlugin} from 'vite-plugin-vue2';
+ import {injectHtml} from 'vite-plugin-html';

export default defineConfig({
  plugins: [tsconfigPaths(),
    createVuePlugin(),
+   injectHtml({
+     injectData: {
+       title: '用户管理系统',
+     },
    }),
  ],
  server: {proxy,},
});

对应的需要批改一下 HTML 的模板变量写法:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
- <title><%= htmlWebpackPlugin.options.title %></title>
+ <title><%= title %></title>
</head>

<body>
  <noscript>
-   We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
+   We're sorry but <%= title %> doesn't work properly without JavaScript enabled.
  </noscript>
  <div id="app"></div>
  <script type="module" src="/src/main.ts"></script>
</body>

</html>

11、兼容性解决

在我的项目背景介绍上有提到该我的项目对兼容性要求很低,所以这块在迁徙中理论并未波及。

当然,如果对兼容性有要求的我的项目,能够应用 @vitejs/plugin-legacy 插件。该插件会打包出两套代码,一套面向旧式浏览器,另一套则蕴含各类 polyfill 和语法兼容来面向老式浏览器。同时在 HTML 中应用 module/nomodule 技术来实现新 / 老浏览器中的「条件加载」。

总结

该我的项目蕴含 1897 个模块文件(包含 node\_modules 中模块),迁徙前后的构建(无缓存)耗时如下:

vue-cli vite 2
dev 模式 \~8s \~400ms
prod 模式 \~42s \~36s

能够看到,在 DEV 模式下 vite2 构建效率的晋升非常明显,这也是因为其在 DEV 模式下只做一些轻量级模块文件解决,不会做较重的打包工作,而在生产模式下,因为依然须要应用 esbuild 和 rollup 做构建,所以在该我的项目中效率晋升并不显著。

以上就是笔者在做 vue-cli 迁徙 vite 2.0 时,遇到的一些问题。都是一些比拟小的点,整体迁徙上并未遇到太大的妨碍,用了不到半天工夫就迁徙了。当然,这也有赖于近年来 JavaScript、HTML 等标准化工作使得咱们写的支流代码也可能具备肯定的统一性。这也是这些前端工具让咱们「面向未来」编程带来的一大长处。心愿这篇文章可能给,筹备尝试迁徙到 vite 2.0 的各位朋友一些参考。

退出移动版