公司项目基于 Vue2 + Webpack4 + TypeScript3.5 技术栈,不支持新语法与新特性 🆕 由于项目放于 monorepo 仓库中,并且仓库中所有项目共用一套基础设施,升级的话影响面较广,因此干脆将项目迁出,直接升级成 Vite,运行速度确实是 🆙 🆙 🆙
What!这也算技巧?没错,虽然朴实无华,但是不可或缺 🥷
如下所示,直接将不符合要求的代码重构成 Vite 支持的写法。
// Before ☑️
const config = {
logo: require('@/assets/logo.png'),
// After ✅
import logo from '@/assets/logo.png';
const config = { logo };
开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。详见原文
由于 Vite 只支持 ES Module 包,对于 CommonJS 或 UMD 包必须经过转换才可使用,而 Vite 的智能导入分析可能存在缺漏,因此需要手动配置让依赖被预构建处理。
export default defineConfig({
optimizeDeps: {
include: ['pica', 'tinycolor2'],
并不是有了预构建就万事大吉 🙅
NPM 包内容五花八门,你可能会遇到包含 .vue、.html 等文件的包,这类非 .js、.ts 的文件无法被预构建处理,将导致报错。
error:
No loader is configured for ".vue" files
此时就需要将其排除在预构建以外。
export default defineConfig({
optimizeDeps: {
exclude: ['xxx'],
CommonJS 插件
被排除在预构建外的 CommonJS 包怎么用? 🤔
为了解决这一问题,我对搜索到的数个 CommonJS 插件进行简单地试用,最终基于成熟、可靠、全面等几方面的考虑,采用 @rollup/plugin-commonjs 。
import commonjs from '@rollup/plugin-commonjs';
export default defineConfig({
plugins: [
commonjs({
transformMixedEsModules: true,
include: ['path/to/xxx.js'],
❗ 某些情况下,在 Vite 中使用 @rollup/plugin-commonjs 会遇到一些问题,见下方源码:
// "rollup/plugins/blob/master/packages/commonjs/src/index.js"
!dynamicRequireModuleSet.has(normalizePathSlashes(id)) &&
(!hasCjsKeywords(code, ignoreGlobal) || (isEsModule && !options.transformMixedEsModules))
return { meta: { commonjs: { isCommonJS: false } } };
当条件满足时,将返回一个没有code
属性的对象,code
可存放转换后的代码字符串,在下方使用:
// "vite/src/node/server/pluginContainer.ts"
for (const plugin of plugins) {
let result = await plugin.transform.call(ctx, code, id, ssr)
if (isObject(result)) {
code = result.code || ''
} else {
code = result
Vite 从插件接收到的返回值对象拿不到code
的值,将认为转换后的代码是空的,由此产生问题。
1️⃣ 解决方案:对返回值做一下简单处理
const cjsPlugin = commonjs({/* ... */});
const { transform } = cjsPlugin;
cjsPlugin.transform = function (code, id) {
const result = transform.call(this, code, id);
return result && !result.code && result.meta?.commonjs?.isCommonJS === false ? null : result;
此外,我还发现另一个问题:插件通过path.extname(id)
的方式来获取文件扩展名,当 Vite 传递的 id 是带查询参数的 URL (如:path/to/xxx.js?v=123456)时,将不能正常工作。
2️⃣ 解决方案:对 id 进行转换
cjsPlugin.transform = function (code, id) {
if (/\.js\?v=[^\?]+$/.test(id)) {
id = id.replace(/\?v=[^\?]+$/, ''); // 去除 ?v=xxx
const result = transform.call(this, code, id);
return result && !result.code && result.meta?.commonjs?.isCommonJS === false ? null : result;
查询参数可能是 ?v=xxx 或 ?t=xxx 或其它,真遇到了需自行变通处理;
不一定会遇到上述的两个问题,我目前采用 transformMixedEsModules 加 include 的配置运行正常;
💡 再附赠一个小技巧:默认情况下 rollup 插件在开发 (serve) 和生产 (build) 模式中都会被调用,可通过设置 apply
属性进行控制。详见
cjsPlugin.apply = 'serve';
自定义插件
当不方便直接修改代码或者通过配置和插件解决问题时,可以考虑编写插件来处理。
以我遇到的情况为例,有个依赖包导入了大量的 .html 文件,在 webpack 中可以用raw-loader
进行加载,在 Vite 中可通过添加?raw
后缀处理。然而,对依赖包不方便直接修改源码,并且文件数量较多,最好的方法是通过插件来解析:
import { createFilter, dataToEsm } from '@rollup/pluginutils';
function createHtmlPlugin() {
const filter = createFilter('**/*.html');
return {
name: 'vite-plugin-html-loader',
transform(code, id) {
if (filter(id)) return dataToEsm(code);
更多内容可查阅 Vite 插件 API 。
resolve.alias
除了用来定义@/
等路径,还有其它妙用。
比如 NPM 包component-classes
中一段代码如下:
try {
var index = require('indexof');
} catch (err) {
var index = require('component-indexof');
实际上只有component-indexof
包是存在的,因此会产生indexof
不存在的错误。处理起来也很简单:
export default defineConfig({
resolve: {
alias: [{
find: /^indexof$/,
replacement: 'component-indexof',
再比如,常用工具库lodash
不支持 Tree Shaking,可以改用 ESM 版本lodash-es
:
export default defineConfig({
resolve: {
alias: [{
find: /^lodash$/,
replacement: 'lodash-es',
改造依赖包
依赖包质量参差不齐,必要时需动手改造代码。
以webworkify
为 🌰 ,其代码如下:
var bundleFn = arguments[3];
var sources = arguments[4];
var cache = arguments[5];
module.exports = function (fn, options) {/* ... */};
代码开头的arguments
让人摸不着头脑 🧠 。如果在 webpack 中,代码将被包裹在函数体内,一切正常。而 Vite 的处理可能不会包裹函数,因而报错。
处置:将arguments[x]
改为undefined
// "shims/webworkify.mjs"
var bundleFn = undefined;
var sources = undefined;
var cache = undefined;
export default function (fn, options) {/* ... */};
改造后的代码存至“/shims/webworkify.mjs”,再通过别名映射过去:
export default defineConfig({
resolve: {
alias: [{
find: /^webworkify$/,
replacement: path.resolve(__dirname, 'shims/webworkify.mjs'),
Uncaught ReferenceError: require is not defined
参照章节“技巧”的处理方法:
优先通过修改源代码来解决;
其次通过预构建或 CommonJS 插件处理;
Uncaught SyntaxError: The requested module 'xxx' does not provide an export named 'yyy'
error: No loader is configured for ".vue" files
被预构建的包导入了非 .js、.ts 的文件(如:.vue),导致报错。
处置:排除在预构建之外,通过 CommonJS 插件或其它插件处理。
Uncaught ReferenceError: global is not defined
代码中使用了 Node.js 环境的全局对象,简单做下兼容处理:
window.global = window;
若是依赖于buffer
、stream
之类的内置模块,可通过安装兼容包来解决:
npm i buffer stream
[vite] Internal server error: Failed to resolve import "./Xxx" from "yyy.js". Does the file exist?
导入的文件省略了扩展名,且默认不被支持。将正确的扩展名添加至扩展名列表:
export default defineConfig({
resolve: {
extensions: ['.vue', /* ... */],
[plugin: vite:dep-scan] Failed to resolve entry for package "xxx"
依赖包未在 package.json 正确配置main
、module
等字段,导致 Vite 无法找到包的入口。
处置:设置别名将其映射到正确的文件上
export default defineConfig({
resolve: {
alias: [{
find: /^vue-react-dnd$/,
replacement: 'vue-react-dnd/dist/vue-react-dnd.es.js',
ReferenceError: React is not defined
未启用 JSX 转换。
// For Vue 2
export default defineConfig({
plugins: [
createVuePlugin({ jsx: true }),
error: Unexpected "<"
JSX 代码解析出错。
在 .vue 组件中,需设置<script lang="jsx">
或<script lang="tsx">
;
在 .js 文件中,需修改文件名为 .jsx
在 .ts 文件中,需修改文件名为 .tsx
[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available
Vue 组件使用了template
选项,需要编译器进行解析,而默认情况vue
包是不带编译器的。
将代码重构成 .vue 组件的写法;
使用render
选项替代template
选项;
设置别名加载含编译器的版本;
// For Vue 2
export default defineConfig({
resolve: {
alias: [{
find: /^vue$/,
replacement: 'vue/dist/vue',
net::ERR_ABORTED 408 (Request Timeout)
Vite 检测到对依赖包的请求,且该依赖尚未被 Vite 处理过时,可能会触发预构建,导致请求超时以及页面重载。
要么多刷新几次等它完成预构建,要么将依赖加入optimizeDeps.include
提前处理。