阅读 688
Vite 接入技巧与常见问题

Vite 接入技巧与常见问题

公司项目基于 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"
if (
  !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;
};
复制代码

PS:

  1. 查询参数可能是 ?v=xxx 或 ?t=xxx 或其它,真遇到了需自行变通处理;
  2. 不一定会遇到上述的两个问题,我目前采用 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'),
    }],
  },
});
复制代码

FAQ

Uncaught ReferenceError: require is not defined

参照章节“技巧”的处理方法:

  1. 优先通过修改源代码来解决;
  2. 其次通过预构建或 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;
复制代码

若是依赖于bufferstream之类的内置模块,可通过安装兼容包来解决:

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 正确配置mainmodule等字段,导致 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提前处理。

文章分类
前端
文章标签