【vite】你不知道的小妙招,确定不看一下吗?

时间:2022-06-25 06:14:20

【vite】你不知道的小妙招,确定不看一下吗?

分析 version:2.3.7。本文将整理 vite 静态资源的几种处理方式,应用案例和源码分析相结合,带你 10mins 通关该模块知识~

一.处理的五种方式

 

(1) 使用根绝对路径引入 public 中的资源

  1. <img alt="Vue logo" src="/wy-logo.png" /> 

【vite】你不知道的小妙招,确定不看一下吗?

敲重点!

  • publicDir 放静态资源的目录,默认为 public
  • 引入 public 中的资源永远应该使用根绝对路径 —— 举个,public/wy-logo.png 应该在源码中被引用为 /wy-logo.png。在开发时能直接通过${yourHost}/wy-logo.png根路径访问到。
  • public 中的资源不应该被 JavaScript 文件引用。

接下来我们来康康代码处理:当我们直接/wy-logo.png 访问资源:

【vite】你不知道的小妙招,确定不看一下吗?

以下是 public 静态资源中间件处理入口 - vite/src/node/server/index.ts:

  1. if (config.publicDir) { 
  2.     middlewares.use(servePublicMiddleware(config.publicDir)) 
  3.  } 

这时候大家就有疑问了,怎样才会走到 isImportRequest,以及为什么这么干?别急,下面我们慢慢唠~

(2)通用 import 静态资源, 返回[解析后]的公共路径

首先,啥子是通用静态资源嘞~~

是 vite 支持的默认资源类型:

KNOWN_ASSET_TYPES = ["png", "jpe?g", "gif", "svg", "ico", "webp", "avif", "mp4", "webm", "ogg", "mp3", "wav", "flac", "aac", "woff2?", "eot", "ttf", "otf", "wasm"]

是你自定义的放到 assetsInclude 配置中的文件

其次,我们来康康,静态资源的导入

  1. <template> 
  2.     <img alt="Vue logo" :src="starImg" /> // 解析后的公共路径作为 src 来请求资源 
  3. </template> 
  4. <script> 
  5.   import starImg from '../assets/star.png' // 导入静态资源 - 图片 
  6.   export default defineComponent({ 
  7.     data () { 
  8.       return { 
  9.         starImg 
  10.       } 
  11.     } 
  12.   }) 
  13. </script> 

这是我们的输入和输出:我们可以看到每个 import 都会被处理成 xxx?import 请求,返回解析后的代码,得到一个公共可访问 url

【vite】你不知道的小妙招,确定不看一下吗?

然后,我们依旧根据 wy-logo 图片来对比分析~ 之前是直接静态文件访问,提前返回资源,不会被解析成 import 依赖 反之如果作为 js 文件 import 引入,则不会当成正常静态资源,都得优先处理成通用&import js 文件,:

  1. import logo from '../../public/wy-logo.png' 
  2. console.log(logo) 

关键代码:

  1. // 从初始执行 cli 处启动 createServer - vite/src/node/server/index.ts 
  2. // 会调用 resolveConfig()获取 config, 而该方法里会调用 resolvePlugins(), 
  3. // 其中有个 plugin 处理是: importAnalysisPlugin(config) 
  4. // 所在文件如下: 
  5. import { importAnalysisPlugin } from './importAnalysis' 
  6.  
  7. // 在 importAnalysic.js 里有个关键方法: 
  8. async transform(source, importer, ssr) { 
  9.   // 用`?import`标识非 js/css 的 import 依赖 
  10.   url = markExplicitImport(url) 
【vite】你不知道的小妙招,确定不看一下吗?

可以看到 public 下的静态资源直接请求会直接返回,反之 import 静态资源的话 - 处理成/public/wy-logo.png?importwy-logo.png?import。需要后续通过返回解析后的 url 再去访问资源。es-module-lexer 解析处理:

【vite】你不知道的小妙招,确定不看一下吗?

这时候大家就理解了之前的疑问,isImportRequest 需要区分是否是直接的静态资源请求,如果是 import xxx 来引入的,都统一处理成依赖,给到你最终需要的一个 URL => 【公共静态资源访问路径】。这就是为什么 public 中的资源不建议被 JavaScript 文件引用,因为 publicDir 资源文件的定义就是直接可以请求,没有必要解析获取 url 后再请求!!!

(3)非通用静态资源?可显式 URL

引入 ?url通用静态资源可以直接处理获取 url,那要是想要处理其他资源,怎么显式导入为一个 URL 来用 (⊙_⊙)? 答案是用?url后缀 ../data/name.js:

  1. export const nameList = ['Tim''John''Bob''Catherine'
  2. console.log(`名称列表 = `, nameList.join('  ')) 

components/name.vue

  1. import nameListUrl from '../data/name.js?url' 
  2. console.log(nameListUrl) // 解析成'/src/data/name.js' 
【vite】你不知道的小妙招,确定不看一下吗?

源码解析: 同理,也是在resolvePlugins()里面有对 asset 处理的assetPlugin

  1. const urlRE = /(\?|&)url(?:&|$)/ 
  2. async load(id) { 
  3.  ... 
  4.   // 如果没有被配置到静态资源 assetsInclude 并且 没有?url 后缀的,直接返回 
  5.   if (!config.assetsInclude(cleanUrl(id)) && !urlRE.test(id)) { 
  6.     return 
  7.   } 
  8.  
  9.   id = id.replace(urlRE, '$1').replace(/[\?&]$/, ''
  10.   const url = await fileToUrl(id, config, this) // 获取公共可访问路径 
  11.   return `export default ${JSON.stringify(url)}` // 返回解析后的代码 

(4)将资源引入为字符串 ?raw

同一个例子,我们要获取 name.js 的数据:1.场景 1 是获取执行 name.js 后输出的数据 2.场景 2 是仅仅要获取 name.js 的文本,比如我们可以做 template 的字符串,那就需要使用到?raw后缀了。

  1. import nameString from '../data/name.js?raw' 
  2. console.log(nameString) // 解析出 export default "xxxxx" 文本 
【vite】你不知道的小妙招,确定不看一下吗?

上源码。。。。。依旧是assetPlugin

  1. const rawRE = /(\?|&)raw(?:&|$)/ 
  2. async load(id) { 
  3.  ... 
  4.   // raw requests, read from disk 
  5.   if (rawRE.test(id)) { 
  6.     // publicDir 存在同名静态文件,优先返回 
  7.     const file = checkPublicFile(id, config) || cleanUrl(id) 
  8.     // ?raw 作为 query, 读取对应的文件并且返回其字符串 
  9.     return `export default ${JSON.stringify( 
  10.       await fsp.readFile(file, 'utf-8'
  11.     )}` 
  12.   } 
  13.   ... 

(5)导入脚本作为 Worker ?worker

脚本可以通过 ?worker 或 ?sharedworker 后缀导入为 web worker。上案例:/data/name-worker.js

  1. export const nameList = ['Tim''John''Bob''Catherine'
  2. addEventListener('message', (e) => { 
  3.   console.log('主线程: ', e.data) 
  4.   postMessage({ 
  5.     word: `Hi,我是 worker~~ 老大,这是你要的名单:${nameList.join(' ')}`, 
  6.     nameList 
  7.   }) 
  8.   close() // 关闭 worker 
  9. }, false

/components/name.vue

  1. import NameWorker from '../data/name-worker.js?worker' 
  2. export default defineComponent({ 
  3.   mounted () { 
  4.     const worker = new NameWorker() 
  5.     worker.postMessage('Hi, 我是主线程~') // 主线程向 Worker 发消息 
  6.     worker.onmessage = (e) => { // 接收子线程发回来的消息 
  7.       if (e.data) { 
  8.         console.log('Worder: ' + e.data.word) 
  9.         this.workerNameList = e.data.nameList 
  10.         worker.terminate() // Worker 完成任务以后,主线程就可以把它关掉 
  11.       } 
  12.     } 
  13.     worker.onerror = (e) => { 
  14.       console.log([ 
  15.         'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message 
  16.       ].join('')) 
  17.     } 
  18.   } 
  19. }) 

这里,就是给 name-worker.js 封装了一层,提供了 WorkerWrapper 函数帮你新建了一个 worker 对象

【vite】你不知道的小妙招,确定不看一下吗?

debug 图如下:

【vite】你不知道的小妙招,确定不看一下吗?

源码。。。。【啊啊啊!!!源码可真多,我可太暴躁了】

resolvePlugins()里 执行webWorkerPlugindev 开发环境下:

  1. async transform(_, id) { 
  2.     const query = parseWorkerRequest(id) 
  3.     let url: string 
  4.     url = await fileToUrl(cleanUrl(id), config, this) // 原始 url 
  5.     url = injectQuery(url, WorkerFileId) // 加上&worker_file 的 query 标识 
  6.  
  7.     const workerConstructor = 
  8.       query.sharedworker != null ? 'SharedWorker' : 'Worker' 
  9.     const workerOptions = { type: 'module' } 
  10.  
  11.     return `export default function WorkerWrapper() { // 输出新建 worker 对象的 template 
  12.       return new ${workerConstructor}(${JSON.stringify( 
  13.       url 
  14.     )}, ${JSON.stringify(workerOptions, null, 2)}) 
  15.     }` 

build 生产下:inline 模式和非 inline 模式

  1. if (query.inline != null) { 
  2.   // 打包文件作为入口去支持 import 导入 worker 或者行内写入 
  3.   const rollup = require('rollup'as typeof Rollup 
  4.   const bundle = await rollup.rollup({ 
  5.     input: cleanUrl(id), 
  6.     plugins: config.plugins as Plugin[] 
  7.   }) 
  8.   try { 
  9.     // 在生产构建中将会分离出 chunk,worker 脚本将作为单独的块发出 
  10.     const { output } = await bundle.generate({ 
  11.       format: 'es'
  12.       sourcemap: config.build.sourcemap 
  13.     }) 
  14.   
  15.     return `const blob = new Blob([atob(\"${Buffer.from(output[0].code).toString('base64')}\")], { type: 'text/javascript;charset=utf-8' }); 
  16.     export default function WorkerWrapper() { 
  17.       const objURL = (window.URL || window.webkitURL).createObjectURL(blob); 
  18.       try { 
  19.         return new Worker(objURL); 
  20.       } finally { 
  21.         (window.URL || window.webkitURL).revokeObjectURL(objURL); 
  22.       } 
  23.     }` 
  24.   } finally { 
  25.     await bundle.close() 
  26.   } 
  27. else { 
  28.   // 作为分开的 chunk 处理`?worker&inline`,内联为 base64 字符串 - 要求 inline 的 worker 
  29.   url = `__VITE_ASSET__${this.emitFile({ 
  30.     type: 'chunk'
  31.     id: cleanUrl(id) 
  32.   })}__` 
  33.   ....// 同开发返回的 template 

咦~,那这个/src/data/name-worker.js?worker_file 又通过?worker_file后缀给我们处理啥了?webWorkerPlugin:

  1. const WorkerFileId = 'worker_file' 
  2. async transform(_, id) { 
  3.     const query = parseWorkerRequest(id) 
  4.     if (query && query[WorkerFileId] != null) { 
  5.       return { 
  6.         // 其实只是作为 执行导入之前生成的 worker.js 文件 的标识....... 
  7.         code: `import '${ENV_PUBLIC_PATH}'\n` + _ 
  8.       } 
  9.     } 

害!五个静态处理方式总算是讲完了~

二. 总结

 

vite 的静态处理关键点就是 :

(1)通过特殊 query(?:worker|sharedworker|raw|url)来区分不同类型静态资源,进行特殊的 transform 处理。

(2)publicDir 限定直接访问的静态资源 本文通过列举处理点,逐一提供案例+debug 截图+源码分析的方式,让大家理解静态处理的使用和底层原理。

原文链接:https://mp.weixin.qq.com/s/J_HBCBATgVEexbhzKvb2wQ