Vue3 功能拆解⑦ assets url 转换规则
文章目录
本文从源码角度讲解了 vue3 中是如何对 assets url 进行转换的,比如
<img src="@img/vue/test.png">
在编译之后是怎么样? 这篇文章将详尽的接晓。
本文涉及的源码包: compiler-sfc, compiler-core。
assets url 在模板中的使用方式:
|
|
下面会从源码角度取分析各种情况最后被解析的结果。
该解析过成在 SFC 模板解析模块 compiler-sfc 触发中,但是最终解析的是 compiler-core 模块。
相关函数: packages/compiler-sfc/src/templateTransofrmAssetUrl.ts 中的 transformAssetUrl,这个函数并非直接在哪里调用,而是做为选项,转换器传递给了 compiler-core ,在 transform 介段处理,具体代码简要流程。
compiler-sfc:src/compileTemplate.ts
|
|
省略一些无关紧要的代码,这里重点关注 transformAssetUrl
和 transformSrcset
两
个,尤其是前者。
上面是 compiler-sfc 阶段的大致逻辑,接下来执行两个 transformXxx
的地方发生在
compiler-core:src/transform.ts(更详尽的分析在这里 。)
transform() -> traverseNode() 从 root 节点开始递归处理 ast,来自 ast.ts 解析后的 AST 结构。
traverseNode() 函数分三个阶段实现
收集 node transform 函数,并会提前处理一些节点
根据节点类型 NodeTypes,做相应的分支处理,比如: children
最后一个 while 反方向执行收集到的 node transform 完成转换
这些步骤不展开讲了,更详细的还是这篇文章: Vue3 源码头脑风暴之 3 ☞compiler-core - transform + codegen
再回头看 transformAssetUrl 内的条件:
|
|
条件1: 首先是 ELEMENT 类型节点且有 props 的情况下这个函数彩绘被收集进当前组件 的 transform 队列中。
条件2: 必需是指定类型的标签,这里有默认的标签列表
|
|
满足条件后会针对每个 prop 进行单独处理:
过滤掉不满足处理条件的
非 ATTRIBUTE 类型,可能是指令
检查标签属性名是否在
options.tags
对应的 tag 的范围值内, 比如:<img>
是 src,<video>
是 src 或 poster 等等…已经是
http(s)://
打头的完整链接data:xxx
开头的 url ,比如: base64 之后的 url最后一个条件就是过滤掉非相对路径的情况(相对路径: .,~,@ 三个字符开头的路径 被视为相对路径, 比如:
"./path/to", "~/path/to", "@dir/path/to"
)
相对路径转换,包括新增的
options.base
选项(db786b1, #2477)const url = parseUrl(attr.value.content)
parseUrl 转换,首先将
~img/vue/test.png
转成img/vue/test.png
然后交给url
解析出 URL 对象:{path, hash, href, host, ...}
如: parseUrl 实现接下来是没有 options.base 或者非相对路径的情况处理,如:
~/img/vue/test.png
或@img/vue/test.png
的处理转变成 import imgUrl from '…./…./x.png' 的引入语法。
const exp = getImportsExpressionExp(url.path, url.hash, attr.loc, context)
这个函数所完成的工作:
从 context.imports 中查找是否已经存在
创建 import exp 对象最后会径由 codegen 阶段生成 import … from … 代码(SIMPLE_EXPRESSION)
缓存到 context.imports.push({ exp, path })
hash 和 path 同时存在的情况
对 url 值进行提升处理 context.hoist(…) 比如下面测试中的:
<use href="~@svg/file.svg#fragment"></use>
编译后:
const _hoisted_1 = _imports_2 + '#fragment'
const _hoisted_8 = /*#__PURE__*/_createVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */)
首先是
<use>
元素本身进行了提升,因为是普通标签,没有动态属性或指令,也没 有动态的 children 所以是静态节点给提升,同时因为 href 值有 hash 有 path 所以该值也做了提升处理,当做静态来处理。
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
const url = process.env.VNEXT_PKG_SFC +'/dist/compiler-sfc.cjs.js' const sfc = require(url.replace('stb-', '')) const { compileTemplate: compile } = sfc const source = ` <img src="/vue/logo.png" /> <img src="./vue/logo.png" /> <img src="@vue/logo.png" /> <img src="~vue/logo.png"/> <img src="https://www.cheng92.com/img/vue/logo.png"/> <img src="data:image/png;base64,i"/> <use href="~@svg/file.svg#fragment"></use> ` const opt = {} const run = () => compile({ source, transformAssetUrls: opt }) let result = run() console.log('\n>>> 没有 options.base \n', result.code); opt.base = 'https://www.cheng92.com/img' result = compile({ source, transformAssetUrls: opt }) console.log('\n>>> 有 options.base \n', result.code); return 0
>>> 没有 options.base import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue" import _imports_0 from './vue/logo.png' import _imports_1 from '@vue/logo.png' import _imports_2 from 'vue/logo.png' import _imports_3 from '@svg/file.svg' const _hoisted_1 = _imports_3 + '#fragment' const _hoisted_2 = /*#__PURE__*/_createVNode("img", { src: "/vue/logo.png" }, null, -1 /* HOISTED */) const _hoisted_3 = /*#__PURE__*/_createVNode("img", { src: _imports_0 }, null, -1 /* HOISTED */) const _hoisted_4 = /*#__PURE__*/_createVNode("img", { src: _imports_1 }, null, -1 /* HOISTED */) const _hoisted_5 = /*#__PURE__*/_createVNode("img", { src: _imports_2 }, null, -1 /* HOISTED */) const _hoisted_6 = /*#__PURE__*/_createVNode("img", { src: "https://www.cheng92.com/img/vue/logo.png" }, null, -1 /* HOISTED */) const _hoisted_7 = /*#__PURE__*/_createVNode("img", { src: "data:image/png;base64,i" }, null, -1 /* HOISTED */) const _hoisted_8 = /*#__PURE__*/_createVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */) export function render(_ctx, _cache) { return (_openBlock(), _createBlock(_Fragment, null, [ _hoisted_2, _hoisted_3, _hoisted_4, _hoisted_5, _hoisted_6, _hoisted_7, _hoisted_8 ], 64 /* STABLE_FRAGMENT */)) } >>> 有 options.base import { createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue" import _imports_0 from '@vue/logo.png' import _imports_1 from 'vue/logo.png' import _imports_2 from '@svg/file.svg' const _hoisted_1 = _imports_2 + '#fragment' const _hoisted_2 = /*#__PURE__*/_createVNode("img", { src: "/vue/logo.png" }, null, -1 /* HOISTED */) const _hoisted_3 = /*#__PURE__*/_createVNode("img", { src: "https://www.cheng92.com/img/vue/logo.png" }, null, -1 /* HOISTED */) const _hoisted_4 = /*#__PURE__*/_createVNode("img", { src: _imports_0 }, null, -1 /* HOISTED */) const _hoisted_5 = /*#__PURE__*/_createVNode("img", { src: _imports_1 }, null, -1 /* HOISTED */) const _hoisted_6 = /*#__PURE__*/_createVNode("img", { src: "https://www.cheng92.com/img/vue/logo.png" }, null, -1 /* HOISTED */) const _hoisted_7 = /*#__PURE__*/_createVNode("img", { src: "data:image/png;base64,i" }, null, -1 /* HOISTED */) const _hoisted_8 = /*#__PURE__*/_createVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */) export function render(_ctx, _cache) { return (_openBlock(), _createBlock(_Fragment, null, [ _hoisted_2, _hoisted_3, _hoisted_4, _hoisted_5, _hoisted_6, _hoisted_7, _hoisted_8 ], 64 /* STABLE_FRAGMENT */)) } 0
小结:
base 选项传递给 compileTemplate 是以
{ transformAssetUrls: { base: '...' }}
属性没有 base 情况,
./path/to
=>import ... from './path/to'
当做相对路径处 理有 base 情况,
./path/to
=>src: 'https://www.cheng92.com/path/to
会将 base 解析后与解析后的 src 进行拼接,没有import
~
语法情况,~/path/to
=>import ... from 'path/to'
@
语法情况,@path/to
=>import ... from '@path/to'
~@
有 path 又有 hash 的情况, url 值会进行提升,如:
<use href="~@svg/file.svg#fragment"></use>