Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

支持渲染 React, Vue 组件 #1188

Open
HerbertHe opened this issue Mar 3, 2022 · 24 comments
Open

支持渲染 React, Vue 组件 #1188

HerbertHe opened this issue Mar 3, 2022 · 24 comments

Comments

@HerbertHe
Copy link
Contributor

HerbertHe commented Mar 3, 2022

你在什么场景下需要该功能?

越来越多的 Markdown 渲染器支持 React 和 Vue 组件的渲染,虽然渲染的实现方式都不太一样。在如今版本的 Vditor 中,可以类似于 abcjs、mermaid 支持的方式来支持 Vue 和 React 的单组件渲染,引入对应的 CDN。

描述最优的解决方案

  • 使用代码块的语法:
```react:component
const Component = () => <div>Hello World!</div>

export default Component
```
  • 为了兼容 XML 的语法特性,有另一种可以考虑的语法结构,并支持外部组件导入,给 Lute 提交过这个 issue:

88250/lute#164

可以通过如下的方式,便于集成外部组件引入

<Component />
// Component.tsx

const Component = () => <div>Hello World!</div>

export default Component

描述候选解决方案

还有更多的解决方案,依赖于对 https://github.com/vitejs/vite 进行集成,采用的思路于 VuePress 等类似。更适用于项目工程开发集成,需要处理 Vite 运行时和 Vditor 运行时的兼容。不过可以省略上面 CDN 的引入,并且可以同时兼容 Vite 生态几乎所有的适用插件。

其他信息

@Vanessa219
Copy link
Owner

这个应该可以嵌入第三方的代码块功能就好了吧

@HerbertHe
Copy link
Contributor Author

按照vditor的渲染,光写自定义渲染器可能做不到😂就是我之前在链滴提的要抽离出来的部分特性。

@Vanessa219
Copy link
Owner

第三方的不是给一个 iframe 就可以使用了?

@HerbertHe
Copy link
Contributor Author

不,是支持原生组件开发渲染😂而不是渲染代码展示效果

@Vanessa219
Copy link
Owner

是不是类似于 vditor-react 这种新建一个包装的项目?

@HerbertHe
Copy link
Contributor Author

不是,可以参考dumi的写组件demo

https://d.umijs.org/zh-CN/guide/basic

@Vanessa219
Copy link
Owner

Vanessa219 commented Mar 10, 2022

还是不太懂 🤦‍♀️
这是一个带文档的框架?

@HerbertHe
Copy link
Contributor Author

emmmm 是可以直接把组件渲染成真实 HTML 的一项功能,在 slidev、vitepress、vuepress 等里面都有

<!-- Markdown 内容 -->
这是段落文本

```react:component
const Component = () => <div>Hello React Component</div>

export default Component
```

渲染的结果会是这样:

<!-- Markdown 内容 -->
<p>这是段落文本</p>

<pre>
  <div>Hello React Component</div>
</pre>

@Vanessa219
Copy link
Owner

感觉和我第一次的回复的东西是一样的。 https://b3log.org/vditor/demo/react.html 这样使用会更加安全。如果需要展示结果的话可以直接使用 html

@HerbertHe
Copy link
Contributor Author

😂不一样,说白了组件就是封装了HTML,对于文档工具的方向还是很有意义的。而codepen是一个沙盒,不是HTML注入

@Vanessa219
Copy link
Owner

是不太一样。这个是否可以考虑使用你前面说的插件,因为内置的话可能不安全。

@HerbertHe
Copy link
Contributor Author

😂这个应该需要注入react和vue,会直接过滤掉XSS注入的风险

@Vanessa219
Copy link
Owner

只要可以注入就可以执行 js 吧?

@HerbertHe
Copy link
Contributor Author

React和Vue都做了XSS过滤,本质上是先转化为虚拟DOM,再转化成HTML

@Vanessa219
Copy link
Owner

是哦,太久没用都忘记了,是要开启个啥 tpl 才可以注入,但是在生命周期里面可以直接写各种脚本,这些是可以被执行的吧。

@HerbertHe
Copy link
Contributor Author

是哦,太久没用都忘记了,是要开启个啥 tpl 才可以注入,但是在生命周期里面可以直接写各种脚本,这些是可以被执行的吧。

那就是应该可以被执行的,确实有这个问题。应该可以再加一个开启特性的开关,这样避免三方网站被用户注入

@Vanessa219
Copy link
Owner

用插件或者开关都可以。前面你说的重构可以考虑用插件或许更好一点。

@HerbertHe
Copy link
Contributor Author

用插件或者开关都可以。前面你说的重构可以考虑用插件或许更好一点。

是的,渲染 markdown 的话,插件化是最好的解决方案。

如果要重构部分代码的话,得先从样式表重构开始。针对于渲染 vditor-reset 的部分可以抽象为 drawer,这样插件化的能力可以直接加载注入很多开放的功能。

现在插件的探索,在链滴里面写过一些思路,更深层的特性实现可能需要与 lute 进行沟通了

为了适配进一步插件化的话,Vditor 的 类型定义 得独立为一个包发布了,这样社区才能复用此部分类型定义。采用 monorepo 还是独立仓库管理,这个得看实际情况了。

emmmm 或许建一个 office team 仓库来维护 Vditor 相关的包,我感觉是非常值得的,就跟现在主流的开源项目管理一样。

@HerbertHe
Copy link
Contributor Author

插件的定义在这个项目中 https://github.com/HerbertHe/vditor-plugin

对于 Lute 支持的 js 自定义 renderers 情况,重写了部分的类型定义

@Vanessa219
Copy link
Owner

稍后拜读下

@dingshaohua-com
Copy link

dingshaohua-com commented Dec 2, 2023

/间隔近两年,我遇到了这样的需求, 无意搜索到楼主的issue,感觉我懂你的意思。
我自己写出来了,基于markdown-it、html-react-parser

封装的渲染富文本的组件: RenderRtf.tsx

import { useState, useEffect, useRef } from "react";
import parse from "html-react-parser";
import ReactDOM from "react-dom/client";
import Hello from "./Hello";

// 这里注册富文本中包含的自定义组件(也可以外部注册)
const regCompPublic = {
  Hello,
};

// 渲染富文本
const RenderRtf = (props) => {
  // 组件名字处理
  const regCompTemp = { ...regCompPublic, ...props["regComp"] };
  const regComp = {};
  for (const key in regCompTemp) {
    const keyStr = key.toLowerCase();
    regComp[keyStr] = regCompTemp[key];
  }

  // 子实例的根节点
  const [root, setRoot] = useState<any>();
  // 子实例的根节点的挂载点
  const wrappNode = useRef(null);
  // 子实例渲染
  const rootRender = () => {
    root.render(
      parse(props.content, {
        replace(domNode: any) {
          if (Object.keys(regComp).indexOf(domNode.name) > -1) {
            const Cmp = regComp[domNode.name];
            return <Cmp {...domNode.attribs} />;
          } else {
            return domNode;
          }
        },
      })
    );
  };

   // 一旦有子实例了,就立刻渲染,传入的内容更改 也需要重新渲染
  useEffect(() => {
    root?rootRender():console.warn("没有实例对象,此次实例无法重新render")
  }, [root, props]);

  // 挂载第二(子)实例
  useEffect(() => {
    if (wrappNode.current) {
      const instanceRoot = ReactDOM.createRoot(wrappNode.current);
      setRoot(instanceRoot);
    }
  }, [wrappNode]);

  return (
    <>
      <div className="wrapp" ref={wrappNode}></div>
    </>
  );
};

export default RenderRtf;

页面中使用: App.tsx

import { useState } from "react";
import RenderRtf from "./components/RenderRtf";
import MarkdownIt from "markdown-it";

const md = MarkdownIt({
  html: true,
  linkify: true,
  typographer: true,
});
function App() {
  const [ipt, setIpt] = useState("你好 <Hello/>");

  const onInput = (e: any) => {
    setIpt(e.target.value);
  };

  return (
    <>
      <textarea value={ipt} onInput={onInput}/>
      <RenderRtf content={md.render(ipt)} />
    </>
  );
}

export default App;

基于此,vue我也实现了
封装的渲染富文本的组件: render-rtf.vue

<script setup lang="ts">
import { onMounted, ref, watch } from "vue";
import Hello from "@/components/HelloWorld.vue";
import * as Vue from "vue/dist/vue.esm-bundler.js";

const props = defineProps(["content"]);
let app: any = null;
const renRender = () => {
  if (!showRtf.value) {
    console.warn("没有子实例对象的挂载真实node,此次实例无法重新render");
    return false;
  }
  // 不同于react,vue对外没有暴漏render函数,所以无法手动调用,只能更新整个实例来渲染
  if (app) {
    app.unmount();
  }
  showRtf.value.innerHTML = props.content;
  app = Vue.createApp({
    components: {  //这里组测富文本中包含的vue组件
      Hello,
    },
  });
  app.mount(showRtf.value);
};
onMounted(() => {
  renRender();
});
watch(
  () => props.content,
  (newVal) => {
    renRender();
  }
);

const showRtf = ref();
</script>

<template>
  <div ref="showRtf"></div>
</template>

这里是使用 App.vue

<script setup lang="ts">
import {createApp} from 'vue';
import { ref, watch, computed } from "vue";
import MarkdownIt from "markdown-it";
import RenderRtf from '@/components/render-rtf.vue';

const md = MarkdownIt({
  html: true,
  linkify: true,
  typographer: true,
});
const ipt = ref("哈哈<Hello/>");
</script>

<template>
  <textarea v-model="ipt"></textarea>
  <render-rtf :content="md.render(ipt)"/>
</template>

其本质原理,都是多实例。
markdown解析后的内容,再让子实例去render一遍即可。

@HerbertHe
Copy link
Contributor Author

@dingshaohua-cn 哈哈哈我自己都快忘了当时提的是啥 issue 了,你这个实现方法我看懂了,就跟 vuepress 他们对组件渲染的做法是一样的,本质上还是将 markdown 转化成 react/vue 的组件进行渲染,等于是外面再套了一层 react/vue。实际上我当时提这个 issue 的想法,还不是在外面包 react/vue,而是在里面包 react/vue 的渲染,也是二次渲染。但是原理不太一样,是从直接实现 vditor 支持 react、vue、svelte 等所有的框架的组件针对特定区块进行渲染。

其过程应该是 markdown -> vditor 捕获 -> renderer 特定区块 -> 组合生成 html

具体实现应该参考:https://cn.vuejs.org/guide/quick-start.html#using-vue-from-cdn

通过 iife 直接捕获渲染,跟 vditor 渲染 mermaid 是一个道理。

@HerbertHe
Copy link
Contributor Author

所以后来我考虑去直接设计支持 vditor plugin 了,支持直接重写渲染器。看了 vditor 的源码,无异于重写整个项目,所以放弃了,转而通过 markdown-it 去写 markmax,重写了 markdown-it 的 renderers ,并且通过转化 millionjs 的 AST 可以实现 diff。不过生活所迫,烂尾了哈哈哈哈哈哈

@HerbertHe
Copy link
Contributor Author

vditor在渲染频率上还是有问题的,编辑器定时器调低了 SV 渲染会卡顿的,后来写 markmax 就是希望通过 vdom 来解决这个问题。直接干掉定时器的设定,进行局部更新,当时直接 vdom 就可以 diff 进行局部渲染,不用重绘所有的节点,毕竟markdown 渲染这也是一个痛点所在。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants