首先说说需求:以前我们有一个界面用来编辑一个带明细的数据。其效果大致如下:
但是现在由于功能拓展,明细需要新增一个模式,跟原来的处理方式差距很大,所以明细的编辑部分需要全部重新开发。但是表头部分的处理时完全一致的,并且,进入页面需要请求接口锁定数据(并确定允许修改),退出时需要调接口解除锁定,这块的逻辑也是一样的。因此最理想的办法当然是把表头和基本逻辑分离出来,然后根据需要加载不同的明细组件。但是呢,由于明细这边有调用不少表头的数据,分割成两个组件会比较麻烦,于是想将主表和锁定的逻辑写到base.vue中,然后两种明细各自使用extends函数继承这部分内容。
但是想法很美好,现实却很糟糕我们想象只需要把明细做一个插槽塞给父组件就能把两个模板都显示出来,就像这样:
base.vue:
- <template>
- <aside>我是表头</aside>
- <main><slot name="body"></slot><main>
- </template>
具体控件:
- <template slot="body">
- <div>我是明细</div>
- </template>
显示效果:
- <template>
- <aside>我是表头</aside>
- <main><div>我是明细</div><main>
- </template>
可是事与愿望违,子组件的模板还是直接把父组件的模板覆盖了。尽管锁定逻辑正常执行,但是主表并不会显示出来。查阅大量资料,发现使用extends继承的都没法好好解决这个问题,而另一种,根本就不是继承,而是把父组件引入进来使用,这样二者之间的变量是不能直接相互访问的(主要是这里需要写入数据)。
不过呢,网上还是有些有价值的数据,比如有个地方就提到在render
函数中使用this.extends.render.apply(this, arguments)
来渲染父模板。但是呢,我测试的时候发现父模板还是没有渲染出来。同时我有个疑问,如果我一个.vue文件中既有template模板又有render函数会发生什么?还有如果我只有template模板,没有定义render函数为啥不报错?带着这些疑问,我把这些对象使用console.log
打印出来看了下。
然后我就发现问题所在了,无论我怎么写,结果中都是只有render函数没有template模板。对于这个现象的推测,我认为是在打包时如果有template,则根据这个template生成render函数,无论原先是否有render函数,都将被这个生成的render函数替换掉。去掉我子组件的template,OK,父组件果然渲染出来了,那么接下来就是想办法把子组件的模板也显示出来。可是直接写template会把render覆盖掉,难道我们只能在render中自己一个一个创建?或者用jsx?
可是即使是jsx,也不能直接把原先的template拿来使用,因此,这里还需要再想想办法。于是我想,既然我们能渲染父组件的模板,我们是不是也可以渲染子组件的模板,然后把它塞到父组件的槽里面?我们先打个断点跟踪进去看看槽是怎么被显示出来的:
首先这是render中渲染槽函数的位置:
断点步入进去看看:
这下简单了,我这里简单分析下这段:
- function renderSlot (
- name,
- fallback,
- props,
- bindObject
- ) {
- var scopedSlotFn = this.$scopedSlots[name];//尝试根据槽的名称从$scopedSlots中获取槽的渲染函数
- var nodes;
- if (scopedSlotFn) { // scoped slot 如果有渲染函数
- props = props || {};//检查参数
- if (bindObject) {//检查绑点对象
- if (!isObject(bindObject)) {
- warn(
- 'slot v-bind without argument expects an Object',
- this
- );
- }
- props = extend(extend({}, bindObject), props);
- }
- nodes = scopedSlotFn(props) || fallback;//调用渲染函数
- } else {//如果没找到则直接从$slots中取
- nodes = this.$slots[name] || fallback;
- }
- var target = props && props.slot;
- //返回结果
- if (target) {
- return this.$createElement('template', { slot: target }, nodes)
- } else {
- return nodes
- }
- }
好了,到这里我们应该可以知道这个过程了,如果我们是渲染函数,丢进this.$scopedSlots中,如果是已经执行过的则丢进this.$slots中。
虽然我们拿到的是两个渲染函数,但是为了防止出现问题,我决定还是自己调用render.apply然后扔到this.$slots中比较靠谱。这里中间层是不可避免的。考虑到其他地方会需要使用,这里定义一个函数来生成这个中间层:
- export default function(child,slot){
- if(!"extends" in child){
- return child;
- }
- return {
- extends:child,
- render(){
- //由于vue不能支持模板继承,这里使用一个中间组件,分别渲染父组件和子组件模板,并将子组件以槽的方式注入父组件中
- this.$slots[slot]=child.render.apply(this, arguments);
- var superRendered = child.extends.render.apply(this, arguments);
- return superRendered;
- }
- };
- }
用的时候直接使用如下代码引入
- import baseList from "./components/list";
- import templateExtend from "../common/template-extend.js";
- var List=templateExtend(baseList,"detail");
然后使用List这个对象就能同时渲染出父子组件的模板了。好了至此问题不太完美的解决了,不过不管怎么说,还是解决了。不知道何年何月,Vue自身会不会支持这种样子的模板继承呢?