两周前(202.02.17),vite2.0 发布了,做为使用了浏览器原生 ESM 为下一代前端工具,vite 2.0 相较于 1.0 更加成熟。在此以前笔者就开始关注这类「新型」的前端工具。此次趁着 vite 2.0 发布,也成功将一个基于 vue-cli(-service) + vue2 的已有项目进行了迁移。javascript
迁移工做比较顺利,花了不到半天时间。但整个迁移过程当中也遇到了一些小问题,这里汇总一下,也方便遇到相似问题的朋友一块儿交流和参考。html
在介绍具体迁移工做前,先简单介绍下项目状况。目前该项目上线不到一年,不太有构建相关的历史遗留债务。项目包含 1897 个模块文件(包括 node_modules 中模块),使用了 vue2 + vuex + typescript 的技术栈,构建工具使用的是 vue-cli(webpack)。算是一套比较标准的 vue 技术栈。因为是内部系统,项目对兼容性的要求较低,用户基本都使用较新的 Chrome 浏览器(少部分使用 Safari)。前端
下面具体来讲下迁移中都作了哪些处理。vue
首先须要安装 vite 并建立 vite 的配置文件。java
npm i -D vite
复制代码
vue-cli-service 中使用 vue.config.js
做为配置文件;而 vite 则默认会须要建立一个 vite.config.ts
来做为配置文件。基础的配置文件很简单:node
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
// ...
],
})
复制代码
建立该配置文件,以前的 vue.config.js 就再也不使用了。webpack
在 vite 中也须要指定入口文件。但和 webpack 不一样,在 vite 中不是指定 js/ts 做为入口,而是指定实际的 HTML 文件做为入口。git
在 webpack 中,用户经过将 entry 设置为入口 js(例如 src/app.js
)来指定 js 打包的入口文件,辅以 HtmlWebpackPlugin 将生成的 js 文件路径注入到 HTML 中。而 vite 直接使用 HTML 文件,它会解析 HTML 中的 script 标签来找到入口的 js 文件。github
所以,咱们在入口 HTML 中加入对 js/ts 文件的 script 标签引用:web
<!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
。
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(),
],
});
复制代码
使用 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(),
],
});
复制代码
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';
复制代码
使用 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 也内置了一些环境变量,能够直接使用。
import.meta.env
types补充:vite 提供了它所须要的 types 定义,能够直接应用 vite/client 来引入,能够不须要经过如下方式来本身添加。
若是在 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>;
}
}
复制代码
这样就不会报错了。
在 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
方法。
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,
+ }
});
复制代码
在基于 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>
复制代码
在项目背景介绍上有提到该项目对兼容性要求很低,因此这块在迁移中实际并未涉及。
固然,若是对兼容性有要求的项目,可使用 @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 的各位朋友一些参考。