问题背景
分页组件是我们项目中高频使用到的组件,最近在一次 wujie 子应用的项目中开发时,需要自定义分页组件的一些内容,发现 el-pagination 的 layout 属性中的 slot 属性无法生效了,但是同样的写法不在子应用中是生效的。
示例代码如下:
在 vue2 + webpack 的子项目中,添加 el-pagination 的测试代码
<el-pagination
background
layout="prev, pager, slot, next"
:total="1000">
<div>默认插槽</div> // 对应layout中的slot,插槽内容展示“默认插槽”文案
</el-pagination>
问题现象:
运行项目后,选中 vue2子应用 项目,默认插槽的文案并没有展示出来
相关依赖包版本:
vue:2.6.14wujie:1.0.18wujie-vue2:1.0.18
为了避免项目中的其他因素引起这个问题,直接从官网的安装教程中下载demo模板进行测试
前置知识
- Slot元素 占位符元素,会直接渲染标签中的内容
- Web Component
wujie的子应用样式是通过Shadow DOM进行样式隔离的,子应用的所有的DOM结构都会插入到一个自定义的wujie-app的元素中
问题分析
因为分页组件同样的代码在 wujie 子应用的vue2项目中没展示成功,但在 vue2应用 中是可以正常展示出来,所以我们可以通过chrome的开发者工具定位 子应用-vue2 和 vue2应用 的默认插槽的元素是如何展示的
-
主、子应用对比图如下
-
vue2应用
-
vue2-子应用
通过主、子应用的对比图,不难看出有几点区别及问题:
-
为什么
el-pagination的插槽在chrome对应的是slot标签进行包裹? -
主应用的
slot插槽能够渲染出占位符内的标签元素,但子应用却不行,并且会多出一个外部节点的style标签 -
style标签从哪里来的,是怎么插入到wujie-app下的
-
问题一 | 分页组件渲染slot标签
这个问题我们可以直接从 el-pagination 的源码中得到答案
render(h) {
const layout = this.layout;
let template = <div class={['el-pagination', {
'is-background': this.background,
'el-pagination--small': this.small
}] }></div>;
const TEMPLATE_MAP = {
// ...
slot: <slot>{ this.$slots.default ? this.$slots.default : '' }</slot>, // 插槽
};
const components = layout.split(',').map((item) => item.trim());
template.children = template.children || [];
components.forEach(compo => {
// ... 省略代码
if (!haveRightWrapper) {
template.children.push(TEMPLATE_MAP[compo]);
}
});
if (haveRightWrapper) {
template.children.unshift(rightWrapper);
}
return template;
},
从源码中不难看出分页组件是通过 render 函数进行渲染的,并且组件的 slot 插槽是通过 slot标签 进行包裹并渲染默认插槽的内容。这里又因为组件是通过 render 函数渲染的,没有走 vue 模板的编译流程,所以在浏览器中就会显示出 slot 标签。
那如果我们直接编写一个 带默认插槽的 .vue文件,又会在浏览器中怎么展示?
我们可以从以下代码中得到验证:
- 编写一个简单的vue组件(Test.vue)
<div> <slot>132213213213</slot> </div> - 在子应用或者主应用中引入该组件
<Test>默认插槽</Test> - 结果
从上面的结果可以看出,如果 slot 标签实在 template 模板中进行编写的话,会通过 _t 函数的执行替换掉 slot 标签,直接渲染插槽的内容
具体可以通过vue2-compiler 自行试验 slot 的编译后的效果
问题二 | 子应用无法渲染slot插槽里的内容
从问题1,我们可以知道在主应用中分页组件的插槽内容会被 由 slot 标签包裹起来,slot 标签又因为是一个占位符,直接会渲染内部的元素,所以在主应用中能够成功渲染出来。
但 wujie 的子应用的 dom 是由 wujie-app 这个 Shadow Dom 包裹起来的,
el-pagination 源码的 slot 标签,因为不走 vue 模板的编译流程,会渲染成 slot 标签并且不带 name 具名属性(这里的 slot 标签其实是 Shadow DOM 的默认插槽),刚好符合下图的条件,所以 slot 标签的内容会被替换掉成 wujie-app 元素下的顶级子节点 style 标签,从而不渲染插槽内容
问题三 | style标签从哪里来
这个问题,我们可以从 wujie 的源码中得到答案,因为子应用是通过 Shadow Dom 实现的,所以我们可以搜索 wujie 中与之相关的代码
public patchCssRules(): void {
if (this.degrade) return;
if (this.shadowRoot.host.hasAttribute(WUJIE_DATA_ATTACH_CSS_FLAG)) return;
const [hostStyleSheetElement, fontStyleSheetElement] = getPatchStyleElements(
Array.from(this.iframe.contentDocument.querySelectorAll("style")).map(
(styleSheetElement) => styleSheetElement.sheet
)
);
if (hostStyleSheetElement) { // 提取普通样式到head标签中
this.shadowRoot.head.appendChild(hostStyleSheetElement);
this.styleSheetElements.push(hostStyleSheetElement);
}
if (fontStyleSheetElement) { // 提取字体样式到根标签下
this.shadowRoot.host.appendChild(fontStyleSheetElement);
}
(hostStyleSheetElement || fontStyleSheetElement) &&
this.shadowRoot.host.setAttribute(WUJIE_DATA_ATTACH_CSS_FLAG, "");
}
最后看到效果如下:
解决方案
为了方便项目的后续维护,将 el-pagination 源码拷贝到项目中,并将组件中的默认插槽中的slot标签替换成任一的元素标签即可
- slot: <slot>{ this.$slots.default ? this.$slots.default : '' }</slot>,
+ slot: <span>{ this.$slots.default ? this.$slots.default : '' }</span>,
思考扩展
如果我只是把原生的 el-pagination 拷贝到本地生成一个 Pagination 的组件,不将 slot 标签修改为 span 标签,并将渲染位置进行一个调换,Pagination 组件放在前面,会发生什么?
<div class="item">
<span class="item_title">修改过后的el-pagination组件:</span>
<Pagination
background
layout="prev, pager, slot, next"
:total="1000">
默认插槽
</Pagination>
</div>
<div class="item">
<span class="item_title">默认的el-pagination组件:</span>
<el-pagination
background
layout="prev, pager, slot, next"
:total="1000">
<div>默认插槽</div>
</el-pagination>
</div>
// ... 省略
这时候发现
el-pagination 的默认插槽显示出来了,但是修改后的 Pagination 组件没有显示出来
从图中可以看到,这里还是渲染出来的是一个
slot标签,看起来与问题二中我们得出了“子应用无法渲染slot插槽里的内容”的结论有冲突。
但其实对于 Shadow Dom 而言只有第一个没有名字的 slot 标签才是默认插槽,其他的没有名字的 slot 标签都会当做普通标签进行处理,所以这里的 Pagination 的 slot 标签就当做默认插槽,而 el-pagination 的 slot 就当做普通的 slot 标签处理了,如下图:
总结
- 不经过vue编译的
slot标签本身在浏览器端是一个占位符,只会显示包裹的内容 - 如果
slot标签在wujie的子应用且不是一个具名插槽,就会被Shadow Dom的顶级节点所替换掉 - 对于
Shadow Dom而言只有第一个没有名字的slot标签才是默认插槽,其他的没有名字slot会当做普通标签处理