vue3中使用auto-import与cdn插件冲突问题
背景
在开发vue3项目的时候,我们经常会用到unplugin-auto-import/vite
来简化项目的import,同时为了提高加载的效率,我们会把固定的资源放在cdn加速访问。
在实际项目常用的是使用unplugin-auto-import/vite
做自动导入,vite-plugin-cdn-import
做cdn的引入
vite-plugin-cdn-import
主要做两件事,
- 在html把你插入一个script,地址是你配置的cdn的url。
- 在通过别名映射,把编译时会把源码中的vue改成cdn的 window.Vue
- 把cdn的资源排除在rollup打包信息里
问题
然后在实际应用中,我们在dev开发模式下,可以不import也能正式使用ref和onMounted,但是打包后就会无法正常使用
经过寻找,在vite-plugin-cdn-import 的 issues github.com/MMF-FE/vite… 找到这个解决方案,主要说的是插件执行顺序问题。
为了搞清楚这个问题,我们调试一下vite的配置
重现问题
新建一个vite项目并安装
pnpm i unplugin-auto-import vite-plugin-cdn-import -D
vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import cdnImport from "vite-plugin-cdn-import";
const config = {
base: `/`,
plugins: [
vue(),
AutoImport({
imports: ["vue"],
}),
cdnImport({
modules: [
{
name: "vue",
var: "Vue",
path: "https://unpkg.com/vue@3.5.10/dist/vue.global.prod.js",
},
],
})
]
};
export default defineConfig(config)
src/components/HelloWorld.vue 文件
<script setup lang="ts">
// import { ref ,onMounted } from 'vue'
defineProps<{ msg: string }>()
onMounted(() => {
count.value = 1000
console.log('onMounted called')
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
</div>
</template>
验证开发模式
pnpm dev
一切正常:自动把count 设置成1000 ,控制台输出onMounted called
验证打包模式
pnpm build
npx serve -s dist
打包自动在 index.html 输出
<script src="https://unpkg.com/vue@3.5.10/dist/vue.global.prod.js" crossorigin="anonymous"></script>
并且文件大小只有12k 没有vue原文件信息
运行结果
但是运行结果不理想,count 没有设置成1000 ,控制台也没有输出onMounted called
打印配置
从issue上看说是plugin的enforce属性影响了。那我们来打印看看,我们在返回config的时候console一下
这是虽然看到plugins 是3个对象但是 第三个居然是数组,也就是vite插件支持对象或数组,没关系,我们提取出来再打印
cdnImport({
modules: [
{
name: "vue",
var: "Vue",
path: "https://unpkg.com/vue@3.5.10/dist/vue.global.prod.js",
},
],
})[0]
再次运行打印
其实不用打印,使用插件一样可以显示enforce的信息
pnpm i -D vite-plugin-inspect
加入inspect到plugins
/* eslint-disable */
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import externalGlobals from 'rollup-plugin-external-globals'
import cdnImport from "vite-plugin-cdn-import";
import inspect from 'vite-plugin-inspect'
const config = {
base: `/`,
plugins: [
vue(),
AutoImport({
imports: ["vue"],
}),
cdnImport({
modules: [
{
name: "vue",
var: "Vue",
path: "https://unpkg.com/vue@3.5.10/dist/vue.global.prod.js",
},
],
})[0]
, // 注意这里 他返回的是数组(vite数组一个都支持)所以要取第一个才好打印,需要访问第一个元素,
inspect({}),
]
};
console.log(config)
export default defineConfig(config)
运行dev后,会多一个 http://localhost:5173/__inspect/
分析问题
好家伙 unplugin-auto-import 是post ,但vite-plugin-cdn-import 居然是 pre 。那会发生什么事情?
我们重新梳理一下 正常直觉我们都以为 plugin会按数组顺序依次执行
然而根据vite的规则,会在执行时先按enforce来排序, pre优先,post 最后,不设置则在中间,
最终执行会变成如下图
修复问题
根据issue的指引,我们只需要确保auto-import先执行就行了,所以调整cdn-import 的enforce为 post,当两个都是post的时候vite就会按数组顺序执行
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import externalGlobals from 'rollup-plugin-external-globals'
import cdnImport from "vite-plugin-cdn-import";
import inspect from 'vite-plugin-inspect'
const config = {
base: `/`,
plugins: [
vue(),
AutoImport({
imports: ["vue"],
}),
{
...cdnImport({
modules: [
{
name: "vue",
var: "Vue",
path: "https://unpkg.com/vue@3.5.10/dist/vue.global.prod.js",
},
],
})[0],
enforce: 'post' // 覆盖原来的enforce值
},
inspect({}),
]
};
console.log(config)
export default defineConfig(config)
我们通过解构重新给 cdnImport 设置 enforce: 'post'
结果报了另一一个错误,看来还是有兼容bug,不折腾找找别的方案。
换个库试试吧
其实cdn-import 无非就是插html点js 和 把代码中的vue改成映射后window.Vue,并且把cdn资源排除在roll打包信息里。 那我们用其他库也能实现,在上面issue有另外一个大神给出其他方案
在这位大侠的源码 确实找打了解决方案 ,就是用rollup-plugin-external-globals
来处理cdn 的命名,并且自己在html页面输出对应的script的标签
vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import externalGlobals from 'rollup-plugin-external-globals'
const config = {
base: `/`,
plugins: [
vue(),
{
...AutoImport({
imports: ["vue"],
})
},
{
...externalGlobals({
vue: 'Vue',
}),
enforce: 'post', // 注意这里也要加上post ,不然上面AutoImport 又会后执行
},
],
build: {
rollupOptions: {
external: ['vue'], // 告诉 Rollup 不要将 'vue' 打包进输出文件
plugins: [
externalGlobals({
vue: 'Vue', // import vue → window.Vue
})
],
}
}
};
console.log(config)
export default defineConfig(config)
注意 externalGlobals 也是要加 post 不然上面AutoImport 又会后执行
告诉rollup 不要把vue打进来
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
在html 加入cdn,当然也可以改成动态配置,输出变量循环
最终运行
nice
并且资源请求也符合预期,项目js 1.6k,vue.js 是另外下载
进一步分析
我们改造一下配置,让所有build的代码正常输出,通过对比两次差异
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import externalGlobals from 'rollup-plugin-external-globals'
import cdnImport from "vite-plugin-cdn-import";
import inspect from 'vite-plugin-inspect'
const config = {
base: `/`,
plugins: [
vue(),
AutoImport({
imports: ["vue"],
}),
{
...cdnImport({
modules: [
{
name: "vue",
var: "Vue",
path: "https://unpkg.com/vue@3.5.10/dist/vue.global.prod.js",
},
],
}),
enforce: 'post'
}, // 注意这里 他返回的是数组(vite数组一个都支持)所以要取第一个才好打印,需要访问第一个元素,
inspect({}),
],
build: {
minify: false, // 禁用代码压缩混淆
sourcemap: true, // 生成 sourcemap 便于调试
rollupOptions: {
treeshake: false, // 禁用 tree shaking
output: {
compact: false, // 不压缩代码
},
}
}
};
console.log(config)
export default defineConfig(config)
左边是有问题的, 右边是正常的
在生成的代码里,我们看到主要差异就是,
左边 错误的
居然还保留了import { onMounted, ref } from "vue";
并且使用onMounted 和 ref 是直接使用,所以证明了auto-import后置执行了,
右边 正确的
使用vue的库是通过 Vue.onMounted访问,并且没有 import 的代码。
扩展
其实还有一种解决方案,如果你的项目域名已经是cdn指向的前提下。可以使用vite的rollupOptions 的manualChunks,单独把vue资源报分离出来。由于vue的版本如果没有变化,每次hash值是一样的,这样生成的vue.js文件每次都是一样的,也能达到cdn加速效果。但是就不太适合分发给其他项目。
当然本地也要先安装vue
pnpm i vue -D
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import cdnImport from "vite-plugin-cdn-import";
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
// AutoImport({
// imports: ["vue"],
// }),
cdnImport({
modules: [
{
name: "vue",
var: "Vue",
path: "https://unpkg.com/vue@3.5.10/dist/vue.global.prod.js",
},
],
}),
],
base: `/`,
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("node_modules")) {
// 将大的库单独打包
if (id.includes("element-plus")) return "vendor-element";
// 其他第三方库按类别分组
if (id.includes("vue")) return "vendor-vue";
console.log(id)
return "vendor";
}
},
},
},
},
});