两周前(202.02.17)vite2.0 发布。 vite 2.0作为使用浏览器原生ESM的下一代前端工具,比1.0更加成熟。在此之前,笔者就开始关注这类“新”的前端工具。这次基于 vue-cli(-service) + vue2 的现有项目在利用 vita 2.0 版本的同时成功迁移。
搬家比较顺利,只用了不到半天的时间。然而,在整个迁移过程中也出现了一些小问题。这里总结一下,方便面临类似问题的朋友沟通和参考。
项目背景
在介绍具体的迁移工作之前,我先简单介绍一下项目的情况。目前,该项目上线还不到一年,几乎没有与建设相关的历史债务。该项目包含1897个模块文件(包括node_modules中的模块),使用vue2 + vuex + typescript技术栈,构建工具使用vue-cli(webpack)。是一个比较标准的vue技术栈。该项目作为内部系统,兼容性要求不高,用户基本使用较新的Chrome浏览器(少数使用Safari)。
迁移工作
以下是搬家过程中所做的详细说明。
1。配置文件
首先需要安装vita并创建vita配置文件。
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 文件
您还需要为vita指定一个输入文件。但与 webpack 不同的是,vito 没有指定 js/ts 作为入口点,而是指定实际的 HTML 文件作为入口点。
在webpack中,用户通过将input设置为js input(例如src/app.js
)来指定打包js的输入文件,并使用HtmlWebpackPlugin将生成的js文件路径插入到HTML中。 Vite直接使用HTML文件,会解析HTML中的script标签来找到输入的js文件。
因此,我们在 HTML 输入中添加对 script 标签的引用到 js/ts 文件:
<!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源码的位置。以vita dev模式启动时,实际上启动了一个类似静态服务器的服务器,为源码目录提供服务,因此不需要像webpack那样复杂的模块打包过程。加载模块依赖项将完全取决于浏览器对 import
语法的处理,因此您可以看到一长串脚本加载瀑布:
这里还需要注意项目根设置。默认是process.cwd()
,也会在项目根目录下搜索index.html。为了方便起见,我将 ./public/index.html
移至 ./index.html
。
3。使用vue插件
vite 2.0 对 Vue 项目提供了良好的支持,但与 Vue 耦合性不强,因此支持通过插件的方式构建 Vue 技术栈项目。目前vite官网推荐的vue插件2.0(2021.2.28)与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路径映射处理
使用vita构建ts项目时,如果使用了typescript路径映射功能,需要特殊处理,否则会出现模块无法解析(找不到)的错误:
这里需要使用 vite-tsconfig-paths 插件来解析并替换路径映射。原理比较简单,大致就是vita插件的resolveId hook阶段,利用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,当然不需要方法注入)。
此外,您还可能会遇到ESM和CJS的兼容性问题。当然,这不是vita结构引起的问题,但是应该注意。简单来说,ESM 有默认概念,而 CJS 没有。每个导出的变量都被 CJS 视为 module.exports 对象上的一个属性,而默认的 ESM 导出只是 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,例如:
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
种
补充:vite提供了它需要的类型定义,可以直接使用vite/client导入,不需要你自己用下面的方法添加。
在打字稿中通过import.meta.env
访问环境变量可能会导致错误消息ts:类型“ImportMeta”上不存在属性“env”
。
这是因为在当前版本(v4.2.2)中import.meta
的定义仍然是一个空接口:
interface ImportMeta {
}
或者,我们可以通过接口池功能在项目中进一步定义ImportMeta类型,以扩展对import.meta.env
的类型支持。例如,之前通过 vue-cli 创建的 ts 项目会在 src 目录下创建一个文件 vue-shims.d.ts
,您可以在这里扩展 env 类型支持:
declare global {
interface ImportMeta {
env: Record<string, unknown>;
}
}
这样你就不会收到错误。
8。 webpack 需要上下文
在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 提供的独特模块方法,不是语言标准,所以 require context 不能再在 vita 中使用。但如果完全改成开发者手动导入模块,首先改变现有代码往往会放弃模块导入;其次,放弃这种“灵活”机制,后期的发展模式也会发生一定程度的改变。但幸运的是 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
。当然,你可以在ImportMeta接口中添加一些类型来支持类型:
declare global {
interface ImportMeta {
env: Record<string, unknown>;
+ globEager<T = unknown>(globPath: string): Record<string, T>;
}
}
此外,import.meta.globEager
将在构建时执行静态分析,以用静态导入语句替换代码。要支持动态导入,请使用import.meta.glob
方法。
9。代理 API
vite2.0 仍然提供本地开发(DEV 模式)的 HTTP 服务器,并且还支持通过 proxy 元素设置代理。后面和webpack一样,使用了http-proxy,所以vue-cli的代理设置可以迁移到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 %>
在编译时将模板变量替换为实际地址值。实现像 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插件。该插件将打包两套代码,一套适用于新浏览器,另一套包含旧浏览器的各种填充和语法兼容性。同时HTML中使用了module/nomodule技术,在新/旧浏览器中进行“条件加载”。
总结
项目包含1897个模块文件(包括node_modules中的模块),迁移前后(不带缓存)的构建时间如下:
vue-cli | 邀请2 | |
---|---|---|
开发者模式 | ~8s | ~400ms |
制作方法 | ~42秒 | ~36s |
可以看到DEV模式下vite2构建的性能得到了很大的提升。这也是因为在 DEV 模式下它只进行轻型模块文件处理,而不会在生产模式下进行繁重的打包工作。在这种情况下,由于仍然需要构建esbuild和rollup,因此该项目的性能提升并不明显。
以上是作者在迁移vue-cli到vite 2.0时遇到的一些问题。这些都是比较小的点,整体迁移并没有遇到太大的阻碍。整个搬家过程只用了不到半天的时间。当然,这也有赖于近年来JavaScript、HTML等标准化工作,让我们编写的通用代码有一定程度的统一性。这也是这些前端工具给我们“面向未来”编程带来的一大优势。希望这篇文章能给准备转vita 2.0的大家一些参考。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。