抽离第三方依赖-使用cdn
目标
- 打包后使用 cdn
- 本地开发使用按需加载
使用 CDN 的好处
- 使用 CDN 的网络,加速第三方包的加载
- 更高的缓存利用率
- 通过定义 script 标签的 derfer 属性,让第三方包能够并行下载
为什么使用 cdn 能更好的利用缓存?
什么时候会使用缓存的资源,可以简单的判断,当你的文件名没变,文件内容也没变时,会使用缓存的内容。乍一听好像是否使用 cdn 也没区别,但实际却大不一样,这和我们开发和打包有关。假设你所有的 js 都打在一个包,那你每改一次,打包后的 js 文件名都会改动,那么此时客户端就无法使用原本的缓存内容,你可能会想实际我们不可能只打成 1 个 js 文件,我们是打出多个 js 文件,并且还有优化,我们会将第三方依赖分组单独打包,目的就是为了能最大限度的使用缓存。分组单独打包第三方依赖确实是一个很好的做法。但也有情况会导致单独打出第三方依赖包产生变动。如:假设你的系统基于 element-plus 开发,一开始只有一个列表页,此时按照按需引入和 tree-shark 的模式,element-plus 的东西只会包含 el-table 相关的东西,然后你将 element-plus 的依赖放入 ui-framwork 的 chunk 中,发布之后很好,用户第一次访问 ui-framework 被下载下来,然后缓存住,后续的访问因为有缓存,因此都无需再次下载 ui-framework 的内容了。过了一段时间你们又开发新功能,此时加入了详情页,详情页中还用了 element-plus 中表格以外的其他组件,当你们再次发版的时候,ui-framework 的内容已经更新了,因此客户端需要重新下载 ui-framework 的内容。
而使用 CDN 是什么情况呢?
通过 CDN 使用 element-plus,就相当于将 element-plus 全量引入。全量引入啥意思,就相当于你手动打包时,将 element-plus 全部打入 ui-framework 中,后续无论你是增加功能还是减少功能,ui-framework 的内容都不会再变化。那么只要缓存还没失效,无论你发多少次版,用户都能一直使用这个缓存
注意点
- 某个包使用了 cdn 之后,他依赖的包以及依赖他的包都必须使用 cdn
- 正式项目不要用免费 CDN,如果项目没有这个预算,那就不要使用 CDN 分离第三方依赖
可能会遇到的问题
Uncaught TypeError: Failed to resolve module specifier "vue". Relative references must start with either "/", "./", or "../".
有其他第三方组件引用到 vue 了,需要将该组件一并通过 cdn 的方式引入就可以解决。
根本原因:
非 cdn 的组件引入方式和 cdn 组件引入方式不一样,两者不兼容,所以会出现这种问题
如何排查到底是哪个地方导致的?
只能一步一步注释代码,缩小问题范围
打包时 unplugin-vue-components 组件未生效
打包时 unplugin-vue-components 组件未生效,导致自动引入的组件未被编译
根本原因:
因为 unplugin-auto-import 的 enforce 为 post ,会最后才执行,导致通过 AutoImport 的注入的代码没有被 CDN 插件转换
重复打包
已经使用 cdn 了,但使用了 cdn 的依赖,还是打包进来了
根本原因:
项目的 vite 配置逻辑编写的有问题,本地开发将按需自动引入的东西配置进来,打包时去掉按需自动引入的配置
pinia 改为 cdn 之后,缺失依赖
引入pinia之前,需要先引入vue-demi
最终配置方式
通过rollup-plugin-external-globals插件,排除 cdn 组件
示例:
vite.config.ts
js
import externalGlobals from 'rollup-plugin-external-globals'
const externalGlobalsObj = {
vue: 'Vue',
'vue-router': 'VueRouter',
'vue-i18n': 'VueI18n',
'vue-demi': 'VueDemi',
pinia: 'Pinia',
'element-plus': 'ElementPlus',
'@pzy915/backend-layout': 'BackendLayout',
mitt: 'mitt',
'@pzy915/router-tabs': 'RouterTabs',
'@pzy915/pdf-preview': 'PdfPreview',
'@pzy915/print-dom': 'PrintDom',
'@pzy915/watermark': 'Watermark',
'vue-request': 'VueRequest',
}
export default {
plugins: [
// 设置enforce和apply修改externalGlobals组件的执行时机为最后,解决unplugin-vue-components自动引入的组件未编译问题
{
...externalGlobals(externalGlobalsObj),
enforce: 'post',
apply: 'build',
},
],
build: {
rollupOptions: {
// 让vite不对这些包进行打包
external: Object.keys(externalGlobalsObj),
},
},
}
main.ts
ts
if (import.meta.env.MODE === 'production') {
// 解决ElementPlus打包之后不生效问题
// @ts-ignore
app.use(window.ElementPlus)
}
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate" />
<meta http-equiv="pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1, maximum-scale=1, user-scalable=no"
/>
<meta name="version" content="<{ VITE_APP_VERSION }>" />
<title><{VITE_PROJECT_NAME}></title>
<link
vite-if="<{ VITE_ENABLE_CDN }> === true"
href="<{ VITE_CDN }>/element-plus@2.2.28/dist/index.css"
rel="stylesheet"
/>
<link
vite-if="<{ VITE_ENABLE_CDN }> === true"
href="<{ VITE_CDN }>/@pzy915/backend-layout@0.0.1/dist/style.css"
rel="stylesheet"
/>
<link
vite-if="<{ VITE_ENABLE_CDN }> === true"
href="<{ VITE_CDN }>/@pzy915/router-tabs@0.0.6/dist/style.css"
rel="stylesheet"
/>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/vue@3.2.45/dist/vue.global.prod.js"
defer
></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/vue-router@4.1.6/dist/vue-router.global.prod.js"
defer
></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/vue-i18n@9.2.2/dist/vue-i18n.global.prod.js"
defer
></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/vue-demi@0.13.11/lib/index.iife.js"
defer
></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/pinia@2.0.28/dist/pinia.iife.prod.js"
defer
></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/element-plus@2.2.28/dist/index.full.min.js"
defer
></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/@pzy915/backend-layout@0.0.1/dist/backend-layout.umd.cjs"
defer
></script>
<script vite-if="<{ VITE_ENABLE_CDN }> === true" src="<{ VITE_CDN }>/mitt@3.0.0/dist/mitt.umd.js" defer></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/@pzy915/router-tabs@0.0.6/dist/router-tabs.umd.cjs"
defer
></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/@pzy915/pdf-preview@0.0.2/dist/pdf-preview.umd.cjs"
defer
></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/@pzy915/print-dom@0.0.1/dist/print-dom.umd.cjs"
defer
></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/@pzy915/watermark@0.0.4/dist/watermark.umd.cjs"
defer
></script>
<script
vite-if="<{ VITE_ENABLE_CDN }> === true"
src="<{ VITE_CDN }>/vue-request@1.2.4/dist/vue-request.min.js"
defer
></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
相关阅读
一直依赖的疑惑解答
为什么网上一说优化首页白屏,解决方案中必然有使用 CDN 加载第三方依赖?
大部分的理由都是:
vendor.js过大,所以将部分第三方组件不打入vendor.js中,而采用 CDN 加载
乍一听没毛病,可仔细一想好像如果这个理论是对的,那我不使用 CDN,我进行拆包,我将第三方组件进行分组拆包。同样可以减小vendor.js的大小,难道这种方式不行吗?
后来我又想 CDN 的特点就是会综合考虑距离你实际物理距离最近且延迟最低的节点让你访问,从而达到迅速访问目标资源的目的。那也就是说那些文章默认的网络情况是 CDN 块,而源站点访问资源慢,所以这样可以减少白屏时间,这样一想好像也有点道理,可他们并未描述网络环境呀,另外,我在本地测试,CDN 的速度不可能快过我访问自己本机的资源,模拟慢速网络?那最多也就是将访问本地资源的速度和访问 CDN 资源的速度,拉到一个起跑线。这么一想,一操作,一看结果,好像没有达到优化首页白屏时间的效果?
SPA 项目的特点就是,在首次进入的时候,会将当前页要用到的资源,以及打包后的第三方组件资源,全部加载完毕之后,才会开始渲染首屏内容。这就是首屏会有白屏的原因。不管你是否采用 CDN,首次渲染都会等这些资源加载完毕之后,才会渲染。
到这,好像加 CDN,加了个寂寞??
加 CDN 还是有意义的
- CDN 资源使用 script 脚本引入,加
derfer属性之后,可以并行下载. 而单个大vendor.js只能串行下载 - 大多数情况 CDN 速度要快于源网站
- 上面两点就能有效降低首次的总的资源载入时间,另外,只要 CDN 资源没变动,第一次访问之后,还能最大程度的使用缓存
后面我又想,打包成多页,是不是能减少白屏时间?
以后台管理系统为例,将一个项目打包成两部分,一个是登录,一个是后台管理。
对于第三方依赖多且大的项目,首次访问登录页确实块了,因为登录页用到的东西很少。打出来的包自然也小。但是,转折来了,你登录页是一个页面,后台是另一个页面,那你就变成进入两次首页了。登录页是快了,但你管理后台进入的速度,几乎没变。SPA 的优势也就没了。
关于缓存的疑惑
既然第一次访问之后,本地都有缓存了,为什么断网之后,刷新页面又访问不了了呢?不是有缓存吗?
这就是典型的没有真正理解缓存的意思,这里的有缓存确实没错,但没指出这是协商缓存,协商缓存啥意思?个人通俗的理解就是,缓存有没有效,需要协商,和谁协商?当然是和服务提供方协商,现在断网了,你还能和服务提供商协商吗?不能吧,所以,浏览器只能暂时认定这是无效缓存。那自然还是没法使用。这时候又有个疑问出来了,都有缓存了,那还走一次网络去协商,那缓存的意义到底在哪里?依然有意义,有缓存了,且缓存未失效,此时服务器并不会再次返回资源内容,客户端可以节省下载资源的时间。
参考资料
vite3 + vue3 + pinia 配置 CDN 后打包部署后出现 Failed to resolve module specifier “vue“ 报错处理
ElementPlus: 与 vue-cli 配置的的 external 同时使用时 element-plus 仍然会被打包到最终生成的文件中,导致文件体积过大