Vue可否在render函数操作VNode,怎么操作?

我尝试做一个高亮关键字的公共模块;
这个模块会接收一个模板(传入模板的结构是不固定的)和一个关键字,然后把模板中存在关键字用class="high-light"包裹起来。
如传入模板 <div>Hello World</div> 和关键字 e,应该返回一个模板 <div>H<span class="high-light>e<span>llo World</div>
我用下面的方法失败了;
我这个思路是可以达到效果的吗,我需要怎么做?

import component from './index.vue';
export default {
  render: function (h, context) {
    // component 
    // <div>
    //   <p>我爱罗 2020-08-24 17:17:26</p>
    //   <p>大家好,我是...</p>
    // </div>
    const element = h(component, {
      props: context.props
    });

    // 假设现在的关键字是 "我"
    const key = '我';
    // 下面的写法是无效的;
    const list = [...element.children];
    while (list.length) {
      const element = list[0];
      if (!element.children && element.text.indexOf(key) >= 0) {
        // 这里会返回三个元素,如:
        // [
        //   <span>大家好,</span>, 
        //   <span class="high-light">我<span>,
        //   <span>是...<span>
        // ]
        element.children = fn(element.text);
      } else {
        element.children?.forEach((child) => {
          list.push(child);
        });
      }
      list.shift();
    }

    // <div>
    //   <p>
    //     <span class="high-light">我<span>,
    //     <span>爱罗 2020-08-24 17:17:26<span>
    //   </p>
    //   <p>
    //     <span>大家好,</span>, 
    //     <span class="high-light">我<span>,
    //     <span>是...<span>
    //   </p>
    // </div>
    // 这是我想要的输出
    return element;
  }
};

最后,我在 Vue 的的源码中找到了 VNode 相关的代码。但是并未提供导出,所以就自己实现(抄)了一个 VNode 类。
codesandbox

const keyword = "食";
export default Vue.component("high-light", {
  functional: true,
  render: function (h, context) {
    const component = h(MyTemplate, {
      tag: "component"
    });

    const list = component.children?.map((element) => ({ element }));
    while (list?.length && keyword) {
      let item = list[0].element;
      if (!item.children) {
        list[0].parent.children = replacer(item.text, keyword).map((item) => {
          if (item === keyword) {
            return h("span", { class: "high-light" }, item);
          } else {
            return new VNode(undefined, undefined, undefined, String(item));
          }
        });
      } else {
        item.children.forEach((_item) => {
          list.push({ parent: item, element: _item });
        });
      }
      list.shift();
    }

    return component;
  }
});
阅读 5.6k
6 个回答

处理的都是时间?
用dayjs之类的时间处理库.

  function splitTime(text) {
    const time = dayjs(text)
    if(!time.isValid()) return text
    return [time.format('YYYY-MM-DD'), time.format('HH:mm:ss')].map(i => `<span>${i}</span>`).join('')
  }
你这种情况我觉得用指令比较合适
    directives: {
      highlight: {
        bind(el) {
          const text = el.innerHTML
          el.dataset.text = text
          el.innerHTML = splitTime(text)
        },
        update(el) {
          const text = el.dataset.text
          el.innerHTML = splitTime(text)
        }
      }
    },

codepen在线示例


一样很好改.

export default {
    directives: {
      highlight: {
        bind(el, {value}) {
          value.test = value.test || /.^/
          value.handler = value.handler || (el => el.innerHTML = el.innerHTML.replace(value.test, (a) => `<span>${a}</span>`))
          el.classList.add("v-highlight");
          value.handler(el)
        },
      }
    },
}

这样可以用v-highlight="{test: /我/g}"满足你的需求.
如果还需要更彻底的控制,还可以v-highlight="{handler: el => el.innerHTML='test'}"自定义处理器

写法问题,不应该是这种写法吗?
[h('span', text), h('span', text)]
你用的JS 里面的拼接字段吧,
vue里面用 render,可以去官网查一下 api

你定义的list, 可以理解成新的element.children吗? 但是最终处理完的list并没有覆盖原先的element.children...

首先你要定义好你这个模块的输入和输出是什么?如果输入是 HTML 字符串, 那其实还相对好处理,无非小范围直接操作 DOM。如果输入是另一个 component 对象,才需要你说的处理 vnode。

有个问题,为什么不在那些 component 的 template 里手动用标签包起来而需要用一个公共 component 来遍历并替换 vnode 呢?好奇是什么样的内容需要做这种关键字高亮。文本的最终来源是在前端代码中定义的,还是来自于后端的数据?

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题