【vite】你不知道的小妙招,確定不看一下嗎?
分析 version:2.3.7。本文將整理 vite 靜態(tài)資源的幾種處理方式,應(yīng)用案例和源碼分析相結(jié)合,帶你 10mins 通關(guān)該模塊知識(shí)~
一.處理的五種方式
(1) 使用根絕對(duì)路徑引入 public 中的資源
- <img alt="Vue logo" src="/wy-logo.png" />
敲重點(diǎn)!
- publicDir 放靜態(tài)資源的目錄,默認(rèn)為 public
- 引入 public 中的資源永遠(yuǎn)應(yīng)該使用根絕對(duì)路徑 —— 舉個(gè)🌰,public/wy-logo.png 應(yīng)該在源碼中被引用為 /wy-logo.png。在開發(fā)時(shí)能直接通過(guò)${yourHost}/wy-logo.png根路徑訪問(wèn)到。
- public 中的資源不應(yīng)該被 JavaScript 文件引用。
接下來(lái)我們來(lái)康康代碼處理:當(dāng)我們直接/wy-logo.png 訪問(wèn)資源:
以下是 public 靜態(tài)資源中間件處理入口 - vite/src/node/server/index.ts:
- if (config.publicDir) {
- middlewares.use(servePublicMiddleware(config.publicDir))
- }
這時(shí)候大家就有疑問(wèn)了,怎樣才會(huì)走到 isImportRequest,以及為什么這么干?別急,下面我們慢慢嘮~
(2)通用 import 靜態(tài)資源, 返回[解析后]的公共路徑
首先,啥子是通用靜態(tài)資源嘞~~
是 vite 支持的默認(rèn)資源類型:
KNOWN_ASSET_TYPES = ["png", "jpe?g", "gif", "svg", "ico", "webp", "avif", "mp4", "webm", "ogg", "mp3", "wav", "flac", "aac", "woff2?", "eot", "ttf", "otf", "wasm"]
是你自定義的放到 assetsInclude 配置中的文件
其次,我們來(lái)康康,靜態(tài)資源的導(dǎo)入
- <template>
- <img alt="Vue logo" :src="starImg" /> // 解析后的公共路徑作為 src 來(lái)請(qǐng)求資源
- </template>
- <script>
- import starImg from '../assets/star.png' // 導(dǎo)入靜態(tài)資源 - 圖片
- export default defineComponent({
- data () {
- return {
- starImg
- }
- }
- })
- </script>
這是我們的輸入和輸出:我們可以看到每個(gè) import 都會(huì)被處理成 xxx?import 請(qǐng)求,返回解析后的代碼,得到一個(gè)公共可訪問(wèn) url
然后,我們依舊根據(jù) wy-logo 圖片來(lái)對(duì)比分析~ 之前是直接靜態(tài)文件訪問(wèn),提前返回資源,不會(huì)被解析成 import 依賴 反之如果作為 js 文件 import 引入,則不會(huì)當(dāng)成正常靜態(tài)資源,都得優(yōu)先處理成通用&import js 文件,:
- import logo from '../../public/wy-logo.png'
- console.log(logo)
關(guān)鍵代碼:
- // 從初始執(zhí)行 cli 處啟動(dòng) createServer - vite/src/node/server/index.ts
- // 會(huì)調(diào)用 resolveConfig()獲取 config, 而該方法里會(huì)調(diào)用 resolvePlugins(),
- // 其中有個(gè) plugin 處理是: importAnalysisPlugin(config)
- // 所在文件如下:
- import { importAnalysisPlugin } from './importAnalysis'
- // 在 importAnalysic.js 里有個(gè)關(guān)鍵方法:
- async transform(source, importer, ssr) {
- // 用`?import`標(biāo)識(shí)非 js/css 的 import 依賴
- url = markExplicitImport(url)
- }

可以看到 public 下的靜態(tài)資源直接請(qǐng)求會(huì)直接返回,反之 import 靜態(tài)資源的話 - 處理成/public/wy-logo.png?importwy-logo.png?import。需要后續(xù)通過(guò)返回解析后的 url 再去訪問(wèn)資源。👇🏻es-module-lexer 解析處理:
這時(shí)候大家就理解了之前的疑問(wèn),isImportRequest 需要區(qū)分是否是直接的靜態(tài)資源請(qǐng)求,如果是 import xxx 來(lái)引入的,都統(tǒng)一處理成依賴,給到你最終需要的一個(gè) URL => 【公共靜態(tài)資源訪問(wèn)路徑】。這就是為什么 public 中的資源不建議被 JavaScript 文件引用,因?yàn)?publicDir 資源文件的定義就是直接可以請(qǐng)求,沒(méi)有必要解析獲取 url 后再請(qǐng)求!!!
(3)非通用靜態(tài)資源?可顯式 URL
引入 ?url通用靜態(tài)資源可以直接處理獲取 url,那要是想要處理其他資源,怎么顯式導(dǎo)入為一個(gè) URL 來(lái)用 (⊙_⊙)? 答案是用?url后綴 ../data/name.js:
- export const nameList = ['Tim', 'John', 'Bob', 'Catherine']
- console.log(`名稱列表 = `, nameList.join(' '))
components/name.vue
- import nameListUrl from '../data/name.js?url'
- console.log(nameListUrl) // 解析成'/src/data/name.js'

源碼解析: 同理,也是在resolvePlugins()里面有對(duì) asset 處理的assetPlugin
- const urlRE = /(\?|&)url(?:&|$)/
- async load(id) {
- ...
- // 如果沒(méi)有被配置到靜態(tài)資源 assetsInclude 并且 沒(méi)有?url 后綴的,直接返回
- if (!config.assetsInclude(cleanUrl(id)) && !urlRE.test(id)) {
- return
- }
- id = id.replace(urlRE, '$1').replace(/[\?&]$/, '')
- const url = await fileToUrl(id, config, this) // 獲取公共可訪問(wèn)路徑
- return `export default ${JSON.stringify(url)}` // 返回解析后的代碼
- }
(4)將資源引入為字符串 ?raw
同一個(gè)例子,我們要獲取 name.js 的數(shù)據(jù):1.場(chǎng)景 1 是獲取執(zhí)行 name.js 后輸出的數(shù)據(jù) 2.場(chǎng)景 2 是僅僅要獲取 name.js 的文本,比如我們可以做 template 的字符串,那就需要使用到?raw后綴了。
- import nameString from '../data/name.js?raw'
- console.log(nameString) // 解析出 export default "xxxxx" 文本

上源碼。。。。。依舊是assetPlugin
- const rawRE = /(\?|&)raw(?:&|$)/
- async load(id) {
- ...
- // raw requests, read from disk
- if (rawRE.test(id)) {
- // publicDir 存在同名靜態(tài)文件,優(yōu)先返回
- const file = checkPublicFile(id, config) || cleanUrl(id)
- // ?raw 作為 query, 讀取對(duì)應(yīng)的文件并且返回其字符串
- return `export default ${JSON.stringify(
- await fsp.readFile(file, 'utf-8')
- )}`
- }
- ...
- }
(5)導(dǎo)入腳本作為 Worker ?worker
腳本可以通過(guò) ?worker 或 ?sharedworker 后綴導(dǎo)入為 web worker。上案例:/data/name-worker.js
- export const nameList = ['Tim', 'John', 'Bob', 'Catherine']
- addEventListener('message', (e) => {
- console.log('主線程: ', e.data)
- postMessage({
- word: `Hi,我是 worker~~ 老大,這是你要的名單:${nameList.join(' ')}`,
- nameList
- })
- close() // 關(guān)閉 worker
- }, false)
/components/name.vue
- import NameWorker from '../data/name-worker.js?worker'
- export default defineComponent({
- mounted () {
- const worker = new NameWorker()
- worker.postMessage('Hi, 我是主線程~') // 主線程向 Worker 發(fā)消息
- worker.onmessage = (e) => { // 接收子線程發(fā)回來(lái)的消息
- if (e.data) {
- console.log('Worder: ' + e.data.word)
- this.workerNameList = e.data.nameList
- worker.terminate() // Worker 完成任務(wù)以后,主線程就可以把它關(guān)掉
- }
- }
- worker.onerror = (e) => {
- console.log([
- 'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
- ].join(''))
- }
- }
- })
這里,就是給 name-worker.js 封裝了一層,提供了 WorkerWrapper 函數(shù)幫你新建了一個(gè) worker 對(duì)象
debug 圖如下:
源碼。。。?!景““?!!源碼可真多,我可太暴躁了】
resolvePlugins()里 執(zhí)行webWorkerPlugindev 開發(fā)環(huán)境下:
- async transform(_, id) {
- const query = parseWorkerRequest(id)
- let url: string
- url = await fileToUrl(cleanUrl(id), config, this) // 原始 url
- url = injectQuery(url, WorkerFileId) // 加上&worker_file 的 query 標(biāo)識(shí)
- const workerConstructor =
- query.sharedworker != null ? 'SharedWorker' : 'Worker'
- const workerOptions = { type: 'module' }
- return `export default function WorkerWrapper() { // 輸出新建 worker 對(duì)象的 template
- return new ${workerConstructor}(${JSON.stringify(
- url
- )}, ${JSON.stringify(workerOptions, null, 2)})
- }`
- }
build 生產(chǎn)下:inline 模式和非 inline 模式
- if (query.inline != null) {
- // 打包文件作為入口去支持 import 導(dǎo)入 worker 或者行內(nèi)寫入
- const rollup = require('rollup') as typeof Rollup
- const bundle = await rollup.rollup({
- input: cleanUrl(id),
- plugins: config.plugins as Plugin[]
- })
- try {
- // 在生產(chǎn)構(gòu)建中將會(huì)分離出 chunk,worker 腳本將作為單獨(dú)的塊發(fā)出
- const { output } = await bundle.generate({
- format: 'es',
- sourcemap: config.build.sourcemap
- })
- return `const blob = new Blob([atob(\"${Buffer.from(output[0].code).toString('base64')}\")], { type: 'text/javascript;charset=utf-8' });
- export default function WorkerWrapper() {
- const objURL = (window.URL || window.webkitURL).createObjectURL(blob);
- try {
- return new Worker(objURL);
- } finally {
- (window.URL || window.webkitURL).revokeObjectURL(objURL);
- }
- }`
- } finally {
- await bundle.close()
- }
- } else {
- // 作為分開的 chunk 處理`?worker&inline`,內(nèi)聯(lián)為 base64 字符串 - 要求 inline 的 worker
- url = `__VITE_ASSET__${this.emitFile({
- type: 'chunk',
- id: cleanUrl(id)
- })}__`
- ....// 同開發(fā)返回的 template
- }
咦~,那這個(gè)/src/data/name-worker.js?worker_file 又通過(guò)?worker_file后綴給我們處理啥了?webWorkerPlugin:
- const WorkerFileId = 'worker_file'
- async transform(_, id) {
- const query = parseWorkerRequest(id)
- if (query && query[WorkerFileId] != null) {
- return {
- // 其實(shí)只是作為 執(zhí)行導(dǎo)入之前生成的 worker.js 文件 的標(biāo)識(shí).......
- code: `import '${ENV_PUBLIC_PATH}'\n` + _
- }
- }
- }
害!五個(gè)靜態(tài)處理方式總算是講完了~
二. 總結(jié)
vite 的靜態(tài)處理關(guān)鍵點(diǎn)就是 :
(1)通過(guò)特殊 query(?:worker|sharedworker|raw|url)來(lái)區(qū)分不同類型靜態(tài)資源,進(jìn)行特殊的 transform 處理。
(2)publicDir 限定直接訪問(wèn)的靜態(tài)資源 本文通過(guò)列舉處理點(diǎn),逐一提供案例+debug 截圖+源碼分析的方式,讓大家理解靜態(tài)處理的使用和底層原理。