项目中如果有大量第三方域的静态资源,采用<link rel="dns-prefetch" href="xxx">
的方法进行DNS预解析可以提高网站加载速度。
但是现实是网站往往采用Vue,React等框架开发,团队协作,手动添加可能无法全面覆盖,代码插入是更好的选择;本文基于vite插件开发,自动化完成提取和插入。
一. 使用到的Vite/Rollup钩子
configResolved
:解析完配置后,可以获取配置对象,排除网站本身域名base。generateBundle
:构建bundle完成时触发,可以获取到构建完后的JS,CSS代码transformIndexHtml
:转换入口HTML用到的钩子,可以获取到HTML字符串,插入Tag
二. 获取源码
Vite构建完成后的代码可能加载第三方域的代码无非就是分布在JS,CSS,HTML文件中。
其中构建完成后的JS,CSS代码可以在generateBundle
中获取,generateBundle
提供一个bundle对象,包含每个文件的文件名和源码字符串;遍历对象获取元素file
,文件名可以通过file.fileName
获取。
文件源码存储的位置可能因为输出的类型不同而不同,可以通过file.type
判断,chunk
类型存储在file.code
中,source
类型在file.source
中
js
if (/\.(js|css|html)$/.test(file.fileName)) {
const content =
file.type === 'chunk' ? file.code : file.source?.toString()
getDomains(content) // 获取域名
}
HTML字符串可以直接通过transformIndexHtml
获取
三. 获取bundle中的域名
可以采用正则匹配的方法获取域名,获取到的域名存储在Set中保证唯一性。获取完成后注意要排除网站域名和一些框架,w3c相关的域名如react.dev,www.w3c.org
js
// 匹配域名的正则
const DOMAIN_REGEX = /https?:\/\/([^/\s"'`]+)/gi
// 排除的域名
const EXCLUDED_DOMAINS = ['react.dev', 'www.w3.org']
while ((match = DOMAIN_REGEX.exec(content))) {
const domain = match[1] // match[0]是以http(s)开头
if (
domain &&
!domain.includes(config.base) &&
!EXCLUDED_DOMAINS.includes(domain) &&
!domain.startsWith('localhost')
) {
domains.add(domain)
}
}
四. 插入到HTML入口文件
transformIndexHtml
支持返回{ html. tags }
,首先从Set集合中取出域名字符串生成HtmlTagDescriptor
对象,我们要插入的<link rel="dns-prefetch" href="xxx">
放在tags中就行。
记得在HTML中也要提取域名
js
transformIndexHtml(html) {
getDomains(html)
const prefetchLinks: HtmlTagDescriptor[] = Array.from(domains).map(
(domain) => ({
tag: 'link',
attrs: {
rel: 'dns-prefetch',
href: `//${domain}`
},
injectTo: 'head'
})
)
return {
html,
tags: prefetchLinks
}
}
完整实现
如果有特殊需求可以添加options,增加插件灵活性
js
// vite-plugin-dns-prefetch.ts
import type { Plugin, ResolvedConfig, HtmlTagDescriptor } from 'vite'
const DOMAIN_REGEX = /https?:\/\/([^/\s"'`]+)/gi
const EXCLUDED_DOMAINS = ['react.dev', 'www.w3.org']
export default function viteDnsPrefetchPlugin(): Plugin {
let config: ResolvedConfig
const domains = new Set<string>()
return {
name: 'vite:dns-prefetch',
apply: 'build',
configResolved(resolved) {
config = resolved
},
generateBundle(_, bundle) {
console.log('bundle', bundle)
for (const file of Object.values(bundle)) {
if (/\.(js|css|html)$/.test(file.fileName)) {
const content =
file.type === 'chunk' ? file.code : file.source?.toString()
getDomains(content)
}
}
},
transformIndexHtml(html) {
getDomains(html)
const prefetchLinks: HtmlTagDescriptor[] = Array.from(domains).map(
(domain) => ({
tag: 'link',
attrs: {
rel: 'dns-prefetch',
href: `//${domain}`
},
injectTo: 'head'
})
)
return {
html,
tags: prefetchLinks
}
}
}
function getDomains(content: string) {
if (content) {
let match
while ((match = DOMAIN_REGEX.exec(content))) {
const domain = match[1]
if (
domain &&
!domain.includes(config.base) &&
!EXCLUDED_DOMAINS.includes(domain) &&
!domain.startsWith('localhost')
) {
domains.add(domain)
}
}
}
}
}
// vite中使用
import { defineConfig } from 'vite'
import prefetchDnsPlugin from './vite-plugin-prefetch-dns'
export default defineConfig({
plugins: [
prefetchDnsPlugin()
]
})