wujie的子应用-el-pagination的插槽失效

760 阅读5分钟

问题背景

分页组件是我们项目中高频使用到的组件,最近在一次 wujie 子应用的项目中开发时,需要自定义分页组件的一些内容,发现 el-paginationlayout 属性中的 slot 属性无法生效了,但是同样的写法不在子应用中是生效的。

示例代码如下:

vue2 + webpack 的子项目中,添加 el-pagination 的测试代码

<el-pagination
  background
  layout="prev, pager, slot, next"
  :total="1000">
  <div>默认插槽</div> // 对应layout中的slot,插槽内容展示“默认插槽”文案
</el-pagination>

问题现象:

运行项目后,选中 vue2子应用 项目,默认插槽的文案并没有展示出来

image.png

相关依赖包版本:

  • vue: 2.6.14
  • wujie: 1.0.18
  • wujie-vue2: 1.0.18

为了避免项目中的其他因素引起这个问题,直接从官网的安装教程中下载demo模板进行测试

前置知识

  • Slot元素 占位符元素,会直接渲染标签中的内容
  • Web Component wujie 的子应用样式是通过 Shadow DOM 进行样式隔离的,子应用的所有的 DOM 结构都会插入到一个自定义的 wujie-app 的元素中

问题分析

因为分页组件同样的代码在 wujie 子应用的vue2项目中没展示成功,但在 vue2应用 中是可以正常展示出来,所以我们可以通过chrome的开发者工具定位 子应用-vue2vue2应用 的默认插槽的元素是如何展示的

  • 主、子应用对比图如下

    • vue2应用

      image.png

      image.png

    • vue2-子应用

      image.png

      image.png

    通过主、子应用的对比图,不难看出有几点区别及问题:

    • 为什么 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文件,又会在浏览器中怎么展示?

我们可以从以下代码中得到验证:

  1. 编写一个简单的vue组件(Test.vue)
    <div>
      <slot>132213213213</slot>
    </div>
    
  2. 在子应用或者主应用中引入该组件
    <Test>默认插槽</Test>
    
  3. 结果 image.png

从上面的结果可以看出,如果 slot 标签实在 template 模板中进行编写的话,会通过 _t 函数的执行替换掉 slot 标签,直接渲染插槽的内容

image.png

具体可以通过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 标签,从而不渲染插槽内容

参考链接

image.png

问题三 | 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, "");
  }

最后看到效果如下:

image.png

解决方案

为了方便项目的后续维护,将 el-pagination 源码拷贝到项目中,并将组件中的默认插槽中的slot标签替换成任一的元素标签即可

- slot: <slot>{ this.$slots.default ? this.$slots.default : '' }</slot>,
+ slot: <span>{ this.$slots.default ? this.$slots.default : '' }</span>,

image.png

image.png

思考扩展

如果我只是把原生的 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>
    // ... 省略

image.png 这时候发现 el-pagination 的默认插槽显示出来了,但是修改后的 Pagination 组件没有显示出来

从图中可以看到,这里还是渲染出来的是一个 slot 标签,看起来与问题二中我们得出了“子应用无法渲染slot插槽里的内容”的结论有冲突。

但其实对于 Shadow Dom 而言只有第一个没有名字的 slot 标签才是默认插槽,其他的没有名字的 slot 标签都会当做普通标签进行处理,所以这里的 Paginationslot 标签就当做默认插槽,而 el-paginationslot 就当做普通的 slot 标签处理了,如下图:

image.png

总结

  • 不经过vue编译的 slot 标签本身在浏览器端是一个占位符,只会显示包裹的内容
  • 如果 slot 标签在 wujie 的子应用且不是一个具名插槽,就会被 Shadow Dom 的顶级节点所替换掉
  • 对于 Shadow Dom 而言只有第一个没有名字的 slot 标签才是默认插槽,其他的没有名字 slot 会当做普通标签处理