publicPath是什么?动态publicPath又是什么
publicPath
publicPath是作为一共公共基础路径配置项在使用,它决定了资源文件的基础路径,如下
module.exports = {
//...
output: {
// One of the below
publicPath: 'auto', // It automatically determines the public path from either `import.meta.url`, `document.currentScript`, `<script />` or `self.location`.
publicPath: 'https://cdn.example.com/assets/', // CDN(总是 HTTPS 协议)
publicPath: '//cdn.example.com/assets/', // CDN(协议相同)
publicPath: '/assets/', // 相对于服务(server-relative)
publicPath: 'assets/', // 相对于 HTML 页面
publicPath: '../assets/', // 相对于 HTML 页面
publicPath: '', // 相对于 HTML 页面(目录相同)
},
};
由于该配置项只支持string,在编译时候会和assetsDir及资源文件名拼接成一个字符串地址
动态publicPath
动态publicPath那就是打包后还能通过赋值方式改变的publicPath,webpack内置了__webpack_public_path__
这个动态变量用于处理动态资源加载的问题(vue-cli可以通过webpack-dynamic-public-path插件)
为什么要做动态publicPath这么一个vite插件
vite目前暂是没有支持类似__webpack_public_path__配置项,社区插件库中的插件仅处理了preload相关部分的功能,类似图片等资源未处理,无法满足生产使用,所以就做一个。
怎么做一个vite插件
在vite中,用base配置项作为公共基础路径
找问题点
首先准备一个demo,主要路由动态加载(css/js)以及类似img这种src加载的资源文件。编译后的index.hash.js中与资源文件加载相关代码如下
// 图片资源
var b = '/assets/logo.03d6d6da.png'
// 创建虚拟dom
const N = t('img', { alt: 'Vue logo', src: b }, null, -1)
// 路由
const U = [
{
path: '/',
component: () => _(() => import('./Home.3eb23e3e.js'), ['assets/Home.3eb23e3e.js', 'assets/vendor.49a43a24.js'])
},
{
path: '/about',
component: () =>
_(
() => import('./About.e7930246.js'),
['assets/About.e7930246.js', 'assets/About.cf1872ae.css', 'assets/vendor.49a43a24.js']
)
},
{ path: '/basic', component: Q }
]
// preload函数
const w = 'modulepreload',
y = {},
x = '/', // base
u = function (s, a) {
return !a || a.length === 0
? s()
: Promise.all(
a.map(o => {
if (((o = `${x}${o}`), o in y)) return
y[o] = !0
const e = o.endsWith('.css'),
n = e ? '[rel="stylesheet"]' : ''
if (document.querySelector(`link[href="${o}"]${n}`)) return
const l = document.createElement('link')
if (
((l.rel = e ? 'stylesheet' : w),
e || ((l.as = 'script'), (l.crossOrigin = '')),
(l.href = o),
document.head.appendChild(l),
e)
)
return new Promise((k, V) => {
l.addEventListener('load', k), l.addEventListener('error', V)
})
})
).then(() => s())
}
先看下编译完成后的代码,编译后的地址全都是相对路径,看下这些代码都做了什么
图片资源
赋值后是直接被使用,渲染出来的时候还是相对路径
preload函数
s:imports
a: preloads(预加载资源)
判断是否有预加载资源,没有这直接执行imports;如果有则加载所有预加载资源,加载完成后执行imports,通过代码发现预加载的时候o = ${x}${o}
得到的也是一个相对路径。
都是相对路径,如果做这个动态publicPath,在前面加个变量就行了
确定方案
// 图片资源
var b = '/assets/logo.03d6d6da.png'
// change
var b = __dyanmic_base__ + '/assets/logo.03d6d6da.png'
// preload函数
u = function (s, a) {
return !a || a.length === 0
? s()
: Promise.all(
a.map(o => {
if (((o = `${x}${o}`), o in y)) return
y[o] = !0
const e = o.endsWith('.css'),
n = e ? '[rel="stylesheet"]' : ''
if (document.querySelector(`link[href="${o}"]${n}`)) return
const l = document.createElement('link')
if (
((l.rel = e ? 'stylesheet' : w),
e || ((l.as = 'script'), (l.crossOrigin = '')),
(l.href = o),
document.head.appendChild(l),
e)
)
return new Promise((k, V) => {
l.addEventListener('load', k), l.addEventListener('error', V)
})
})
).then(() => s())
}
// change
u = function (s, a) {
return !a || a.length === 0
? s()
: Promise.all(
a.map(o => {
if (((o = `${__dyanmic_base__}${x}${o}`), o in y)) return
y[o] = !0
const e = o.endsWith('.css'),
n = e ? '[rel="stylesheet"]' : ''
if (document.querySelector(`link[href="${o}"]${n}`)) return
const l = document.createElement('link')
if (
((l.rel = e ? 'stylesheet' : w),
e || ((l.as = 'script'), (l.crossOrigin = '')),
(l.href = o),
document.head.appendChild(l),
e)
)
return new Promise((k, V) => {
l.addEventListener('load', k), l.addEventListener('error', V)
})
})
).then(() => s())
}
那要怎么实现上面的方案?
先找到preload函数
function preload(baseModule: () => Promise<{}>, deps?: string[]) {
// @ts-ignore
if (!__VITE_IS_MODERN__ || !deps || deps.length === 0) {
return baseModule()
}
return Promise.all(
deps.map((dep) => {
// @ts-ignore
dep = `${base}${dep}`
// @ts-ignore
if (dep in seen) return
// @ts-ignore
seen[dep] = true
const isCss = dep.endsWith('.css')
const cssSelector = isCss ? '[rel="stylesheet"]' : ''
// @ts-ignore check if the file is already preloaded by SSR markup
if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) {
return
}
// @ts-ignore
const link = document.createElement('link')
// @ts-ignore
link.rel = isCss ? 'stylesheet' : scriptRel
if (!isCss) {
link.as = 'script'
link.crossOrigin = ''
}
link.href = dep
// @ts-ignore
document.head.appendChild(link)
if (isCss) {
return new Promise((res, rej) => {
link.addEventListener('load', res)
link.addEventListener('error', rej)
})
}
})
).then(() => baseModule())
}
再看应用的地方
// 截图不太清晰用复制了片段出来
const preloadHelperId = 'vite/preload-helper'
...
export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
const ssr = !!config.build.ssr
const insertPreload = !(ssr || !!config.build.lib)
const scriptRel = config.build.polyfillModulePreload
? `'modulepreload'`
: `(${detectScriptRel.toString()})()`
const preloadCode = `const scriptRel = ${scriptRel};const seen = {};const base = '${preloadBaseMarker}';export const ${preloadMethod} = ${preload.toString()}`
return {
name: 'vite:build-import-analysis',
resolveId(id) {
if (id === preloadHelperId) {
return id
}
},
load(id) {
if (id === preloadHelperId) {
return preloadCode.replace(preloadBaseMarker, config.base)
}
},
...
}
可以看到preload函数在buildImportAnalysisPlugin初始化时候进行了处理,针对preloadCode的操作都是在id === preloadHelperId时候才执行的
vite文档--插件API说明
以及rollup.js--plugin
找到我们需要用到的三个钩子,configResolved、transform、generatebundle
这三个钩子作用如下:
configResolved: 负责从编译配置中获取base和assetsDir
transform:负责转换preload
generatebundle:负责处理图片资源文件这种
综上实现如下
import type { Plugin } from 'vite'
import type { Options } from '../index'
export function dynamicBase(options?: Options): Plugin {
const { publicPath = 'window.__dynamic_base__' } = options || {}
const preloadHelperId = 'vite/preload-helper'
let assetsDir = 'assets'
let base = '/'
return {
name: 'vite-plugin-dynamic-base',
enforce: 'post', // 滞后执行
apply: 'build', // 仅编译时候执行
configResolved(resolvedConfig) { // 获取配置,用与后面配置assetsMaker使用
assetsDir = resolvedConfig.build.assetsDir
base = resolvedConfig.base
},
transform(code, id) { // 替换preload中的base
if (id === preloadHelperId) {
code = code.replace(/(\${base})/g, `\${${publicPath}}$1`)
return {
code
}
}
},
generateBundle({ format }, bundle) { // 处理图片等资源
if (format !== 'es') {
return
}
const assetsMarker = `${base}${assetsDir}/`
const assetsMarkerRE = new RegExp(`("${assetsMarker}*.*.*")`, 'g')
for (const file in bundle) {
const chunk = bundle[file]
if (chunk.type === 'chunk' && chunk.code.indexOf(assetsMarker) > -1) {
chunk.code = chunk.code.replace(assetsMarkerRE, `${publicPath}+$1`)
}
}
}
}
}
测试
vite.config.ts
import { dynamicBase } from 'vite-plugin-dynamic-base'
export default defineConfig({
plugins: [
dynamicBase({ /* options */ }),
],
})
编译结果
// 图片资源
var b = window.__dynamic_base__ + '/assets/logo.03d6d6da.png'
// preload函数
const S = 'modulepreload',
y = {},
x = '/',
u = function (s, a) {
return !a || a.length === 0
? s()
: Promise.all(
a.map(o => {
if (((o = `${window.__dynamic_base__}${x}${o}`), o in y)) return
y[o] = !0
const e = o.endsWith('.css'),
n = e ? '[rel="stylesheet"]' : ''
if (document.querySelector(`link[href="${o}"]${n}`)) return
const l = document.createElement('link')
if (
((l.rel = e ? 'stylesheet' : S),
e || ((l.as = 'script'), (l.crossOrigin = '')),
(l.href = o),
document.head.appendChild(l),
e)
)
return new Promise((k, V) => {
l.addEventListener('load', k), l.addEventListener('error', V)
})
})
).then(() => s())
}
插件和demo
目前已添加至 awesome-vite 社区插件列表