背景
目前在研究低代码引擎 lowcode engine,要基于这个开源项目定制化更符合公司业务需求的低代码平台(支持vue3)。有一个功能是,将低代码平台的产物组装成一个个独立的项目,因此需再开发一个系统,用来展示低代码平台构建的页面。
首先,在低代码平台构建页面,如下图所示(官方体验demo):
然后,低代码平台生成的页面,保存后,可以变成一个个完整的项目,然后再开发一个系统A,用来展示这些项目,这样我们就能把低代码平台的产物变成一个个普通的后台管理系统。
在构建这个系统A时,我使用的构建工具是 Vite,然而在将lowcode engine的产物集成到A系统时,由于产物中依赖的第三方库,比如ant-design-vue,需要通过CDN引入。
这就带来一个问题:ant-design-vue依赖Vue,导致Vue也需要通过CDN的方式引入。所以不论是在本地调试,还是最后构建上线,都需要把Vue及依赖Vue相关的库以CDN的方式加载进来。
接下来,先回顾分析过程,最后再给出Vite使用CDN加载资源的具体方案。
分析过程回放
首先,我们明确一些前提条件:
- 低代码平台产物依赖的UI组件(ant-design-vue),要求以CDN的方式引入。低代码平台提供了vue-renderer插件(打包成UMD的格式),完整实现了渲染逻辑。
- 系统A,用来集成低代码平台产物,使用Vue + Vite。
在系统A中,我们使用Vue的方式,就是正常import进来:
js
import { createApp } from 'vue'
...
假设,我们在开发调试时,Vue从本地加载,不引入CDN,会有什么表现?
- 提示找不到defineComponent ,因为我们使用了UI组件库ant-design-vue,它通过CDN加载,需要依赖全局Vue(以CDN方式加载)
- 看看vue-renderer插件引入vue的部分是如何处理的?通过下图,可以看到,Vue最终指向本地安装的包:
- 系统A中引入Vue部分的代码处理,同样读取本地Vue


因为ant-design-vue组件加载时,没有全局引入Vue,现在页面肯定没法正常渲染。那我们先来解决这个问题.
之所以找不到defineComponent,就是因为Vue没有全局加载,我们直接在index.html中加一个script标签,引入vue
html
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.15"></script>
再运行项目,果然没有错误提示了,但页面依然没有正常加载,为什么呢?
根本原因:UI组件无法正常渲染。
现在有一个冲突:UI组件依赖全局Vue,但用来渲染这些UI组件的vue-renderer插件,依赖的却是本地Vue。不论UI组件还是vue-renderer插件,它们依赖Vue,在初始化页面时,必然也会初始化一些数据,现在因为各自依赖的Vue不同,导致UI组件和vue-renderer插件没法建立正常的数据关系,自然页面也无法正常渲染。
那怎么解决这个问题呢?很简单,就是让他们依赖同一个Vue。
因为低代码平台实现机制的原因,我们没法改变UI组件依赖Vue的方式,所以只能让系统A以及vue-renderer插件也同样依赖全局Vue,而不是本地Vue。
这就涉及到我们开篇说的问题,在Vite + Vue的项目中,如何以CDN的方式来加载Vue?
Vite如何使用CDN加载资源?
一般,我们在项目中需要通过CDN的方式引入资源,目的是为了做优化,比如减少打包体积等。这种情况,通常不需要考虑开发环境。
但我们这次,在开发环境也需要通过CDN加载资源,所以在方案上要同时兼顾开发和生产环境。
Vite 想要支持CDN,并不是直接在index.html配置一个script标签就可以了,为什么呢?
我们先了解一下Vite的原理,它由两部分构成:
1)dev server:利用浏览器的ESM能力来提供源文件,具有丰富的内置功能,以及高效的HRM。
2)生产构建:生产环境利用Rollup来构建代码,并提供指令来优化构建过程。
从它的组成部分可以看出,在开发环境和生产环境,Vite行为有比较大差异,开发环境,依赖浏览器自带的能力,生产环境则通过Rollup打包。为什么开发环境和生产环境不保持一致呢?具体原因Vite官方也有说明,详见《为什么生产环境还需打包》。
这意味着,我们在开发和生产环境要使用CDN,但方案上会有些差异。
生产环境
在生产环境要想使用CDN有两步,以Vue为例:
1)引入CDN资源
2)构建时,需要改写项目中import Vue的地方
第1点比较简单,只需要在index.html中引入Vue资源就可以。
html
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.15"></script>
第2点需要依靠一些插件来处理(rollup-plugin-external-globals)
修改vite.config.js配置文件
javascript
export default defineConfig({
...
build: {
rollupOptions: {
external: ['vue'],
plugins: [
externalGlobals({
vue: 'Vue',
}),
],
},
}
...
})
为什么在index.html中引入Vue了,在Build时,还需要使用插件?
我们先看看rollup-plugin-external-globals的作用是什么:将外部导入转换为全局变量,例如 Rollup 的 output.globals 选项。
截取构建产物的部分代码,可以看到依赖Vue地方,都变成了全局变量Vue.方法名:

正因为Vite是基于浏览器原生ES imports 的开发服务器,默认情况下,都是利用浏览器去解析imports,所以会读取import node_modules里的依赖包。
要想改变依赖指向,除了在index.html中引入资源,还需要额外的插件,修改代码中import资源的地方,将原来指向node_modules的依赖,改成全局变量。
生产环境如何配置CDN,更多详细内容,可以参考这篇文章
开发环境
在生产环境,我们是配置vite.config.js中的build属性,这个属性应用于生产环境构建,对开发环境并不起作用,那开发环境应该怎么做呢?
根据生产环境的构建思路,在开发环境是不是也有类似的插件,可以修改import资源的指向呢?
结果还真有,vite-plugin-external就可以做这个事情。
修改vite.config.js文件:
javascript
import vue from '@vitejs/plugin-vue'
import createExternal from 'vite-plugin-external'
export default defineConfig({
...
plugins: [
vue(),
createExternal({
externals: {
vue: 'Vue',
},
}),
]
...
})
我们分析一下这个插件做了什么:
1)vue-renderer插件引入vue的部分是如何处理的:


我们可以看到,引入Vue的地方,已经被vite-plugin-external插件处理成引用全局Vue。
2)系统A中引入vue的部分如何处理:



最终Vue和vue-renderer一样,都指向了全局变量。
其实这个方案同样也可以用于生产环境,rollup-plugin-external-globals是Rollup插件,Vite只有生产构建时才使用Rollup,所以也限制了这个插件的使用范围。
vite-plugin-external则是为Vite开发,可以同时作用于开发和生产环境。
不过经过测试,发现这套方案想要在生产环境使用,需要将构建产物的format指定为iife:
javascript
import vue from '@vitejs/plugin-vue'
import createExternal from 'vite-plugin-external'
export default defineConfig({
...
plugins: [
vue(),
createExternal({
externals: {
vue: 'Vue',
},
}),
],
build: {
rollupOptions: {
output: {
format: 'iife'
}
},
},
...
})
总结
通过以上方式,最终UI组件、vue-renderer插件、系统A,它们依赖的Vue都变成了全局变量Vue,它们之间可以通过Vue,正常建立关联,最后页面也正常渲染出来。
总结一下,在Vite中怎么通过CDN加载资源,第一,引入CDN资源,可直接在index.html中添加script标签,也可以根据需要写一个脚本动态引进来,或者使用Vite相关插件。第二,修改import资源的指向,这一步一般都是通过Vite插件来实现。
其中,关键就在于第二步如何实现。粗略看了一下vite-plugin-external源码,实现起来也比较简单,后面有时间分享一下Vite插件如何写,以及vite-plugin-external的实现原理。