玩转dumi:3D示例展示的骚操作

大家好,我是老纪。

之前写了两篇关于静态站点的文章(《静态站点全文搜索实现原理之dumi篇》和《静态站点全文搜索实现原理之Rspress篇》),花了不少力气写的,在各个平台上都没什么水花,可能关注这块的同学比较少。比如在掘金上阅读量是这样的:

pnpm的介绍显然更受欢迎些:

这篇修改第三方npm包的文章居然是爆款(对我而言),每天都有人点赞收藏,以至于我不得不把掘金APP的点赞收藏提示关了:

在知乎上这篇《错位之谜:网站简体中文何以变繁?》的热度更高些:

闲言少叙,言归正传。

本文简单记录下某文档网站使用dumi后,呈现3D示例的一个骚操作。三言两语难以说尽,各位看官且听我慢慢道来。

技术选型前后

早在两年前,曾经有个团队问过我关于ES(Elastic Search)搜索的事情;又过了一段时间,又有同事让我调研Galacean(蚂蚁的一个团队)怎么实现的代码与效果展示,我研究后发现是用Markdown实现的,于是写了一篇《Markdown自定义标签》,可能标题不够水,所以一点水花都没有。

去年年底,在我的团队已经被裁的剩下大猫小猫三两只时,接到一个任务,优化某文档网站的搜索功能。我这时才意识到上面两件事其实是同一件。

当前的文档网站,内容以Markdown为主,不存在动态的内容,只是API和示例部分都是用iframe嵌套。需要我们优化的搜索功能,主要是想将API、示例代码都可以搜索并定位跳转。

于是有了三种方案:

  1. 现有的功能+ES优化
  2. 现有的功能+algolia等第三方搜索服务
  3. dumi纯前端方案

正常来说,我们应该选择第一种,只需要考虑将API、示例的文本存储到ES数据库,但涉及到定位跳转的话,还得考虑存储的粒度、是否需要行数、段数等元数据。

而第二种方案,则是花钱买服务,algolia等第三方搜索服务比较成熟了,只需要通过简单的配置,搜索服务会定时扫描整个网站,前端调用相关的API就可以了。

第三种方案,是我在网上看到dumi2的发布信息后心动了。一直以为全文搜索是后端的活儿,当数据到了某个体量后,前端不得卡死?为了能够搜索全文,前端必然需要拿到所有文本,在网络请求里会不会太大?

但自己尝试后,发现对于我们的文档级别,还是可以接受的。dumi2使用了Web Worker来优化搜索,避免卡顿;文本虽然大,但开启br压缩后,4M可以压缩到600K,这点儿流量在今天看来,真是毛毛雨了。

再复习下dumi的宣传优势,吸引我的主要是这几点:

  1. 全文搜索。省掉一个后端服务
  2. UI比我们的要好看些,自带响应式布局、暗黑模式、多语言等
  3. 可以自定义组件来扩展Markdown

看了下现有网站的功能,替换成本不高,大部分Markdown文件移植过来就行了。需要处理的细节集中在以下几点:

  1. 首页需要定制,写个React组件.dumi/pages/index.tsx轻松搞定。
  2. API部分,原先是使用jsdoc导出的HTML,将之修改为Markdown,再写个脚本替换下链接。
  3. 示例部分,是3D与代码的结合。正常来说也是一个React组件就很容易搞定,但并没有那么简单。这也正是撰写本文的由来。

示例部分的坑

如果不要求全文搜索搜索到示例中的代码块,那么,写个React组件,左侧是个iframe,右侧是个代码块,是再简单不过了。

但是,如果考虑代码块的全文搜索,就有些令人头疼了。

因为,dumi中的搜索,只包含了静态的Markdown部分,而React组件是动态的,这点儿是做不到的。

这也是使用框架的弊端,必须戴着脚链跳舞。

关于dumi的搜索原理,简单来说,是将与路由相关的文档内容作为资源加入到Webpack的工作流中,为此,dumi提供了专门的Markdown loader来解析处理定制的复杂规则。在项目启动后,内存中已经有了完整的文档元数据对象,使用JavaScript进行搜索就顺理成章了。但这个搜索过程可能会很消耗CPU,造成页面卡顿,所以dumi又使用了Web Worker,将这一环节异步处理,保障了主线程的流畅运行。详见拙作《静态站点全文搜索实现原理之dumi篇》。

应该怎么办呢?有两种思路:

  1. 魔改dumi源码的Hook useSiteSearch,暴露一个可以注入搜索源的函数,在入口文件里找个时机调用。缺点是需要深入研究dumi的搜索机制,怎么将示例的代码块分段、分行、分词,都是麻烦。
  2. 写一个JavaScript脚本,将示例的代码块批量生成多个Markdown文件,上面部分是React组件,用来呈现3D效果,下面部分是JavaScript源码。

从实现成本上讲,我更倾向于第二种。

但第二种,又有个缺点,它只能是上下布局,大致是这样的:

markdown 复制代码
## Thingjs Example

```jsx
/**
 * inline: true
 */
import { ThingjsExample } from 'thingjs-docs';

export default () => (
  <ThingjsExample
    scriptContent="const app = new THING.App();

    app.camera.position = [7, 7, 7];"
  />
);
```

```ts
const app = new THING.App();
app.camera.position = [7, 7, 7];
```

呈现出来的效果是这样的:

在某些场景中,上下布局是合理的,但作为示例,显然是左右更佳些。

方案一:吸星大法

当我以为走进了死胡同时,突发奇想,是不是可以将示例组件回归到最初的设想,包含左右两部分布局,左侧是3D,右侧是代码块,但下方用来辅助搜索的源码代码块,可以考虑添加一个元属性,将之隐藏掉。

比如dumi的文档里关于Markdown的增强有一个高亮的示例,就扩展了代码块的能力:

我可以添加一个规则,比如:

markdown 复制代码
```ts | hide
xxx
```

在渲染时,将相关DOM元素隐藏掉。

这思路是不是很可行?只有两点需要处理:

  1. 找到dumi源码(搜索关键字pure),看能不能抄作业(其实是SourceCode组件)
  2. 右侧的代码块,也最好复用dumi现成的组件

方案二:乾坤大挪移

当我磨刀霍霍,准备开干时,另一个更骚操作的方案涌现在我脑海里:何不来一招乾坤大挪移?

简言之:

  1. 示例组件仍是左右布局,左侧3D不变,但右侧是空的
  2. 在组件渲染时,将下方的代码块移动到右侧的div

感觉这个思路更对我的胃口啊!

难点是:怎么能知道下方的DOM元素呢?

呃,其实一点也不难。因为在我们这个场景里,这两个元素必然是相临的关系:

所以,代码也很容易写出来了:

tsx 复制代码
const exampleRef = useRef<HTMLIFrameElement>(null);
const codeRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  const parent = exampleRef.current?.parentElement!;
  const index = [...parent.childNodes].findIndex((node) =>
    node === exampleRef.current
  );
  const next = parent.childNodes[index + 1];
  codeRef.current!.appendChild(next);
}, []);

return (
    <div className="examples" ref={exampleRef}>
      <iframe ref={iframeRef} src={url} onLoad={onload}>
          <p>Your browser does not support iframes.</p>
      </iframe>
      <div className="examples-code" ref={codeRef}></div>
    </div>
);

效果如下:

哈!

呃,现实中并没有这么光鲜啦。事实上我是走了弯路滴。

首先,我给代码块添加了一个id属性:

tsx 复制代码
import React, { type FC } from 'react';
import Code from "dumi/theme-default/builtins/SourceCode";
import { type Language } from 'prism-react-renderer';

interface SourceCodeProps {
  id?: string;
  children: string;
  lang: Language;
  highlightLines?: number[];
}

const SourceCode: FC<SourceCodeProps> = (props) => {
  const { id } = props;
  if (id) {
    return <div id={id}>
      <Code {...props} />
    </div>
  }
  return <Code {...props} />;
};

export default SourceCode;

其次,写脚本注入了以下模板:

markdown 复制代码
```jsx
/**
 * inline: false
 */
import { ThingjsExample } from 'thingjs-docs';
export default () => {
  const content = `<%- replacedCode -%>`;
  return <ThingjsExample scriptContent={content} scriptId="<%= scriptId %>" />;
};
```

```ts | #<%= scriptId %>
<%- code -%>
```

最终生成了这样的文件:

markdown 复制代码
```jsx
/**
 * inline: false
 */
import { ThingjsExample } from 'thingjs-docs';
export default () => {
  const content = `
const app = new THING.App();

await app.load("./assets/scenes/campus/campus.gltf");
`;
  return <ThingjsExample scriptContent={content} scriptId="THINGJS_SCRIPT_52" />;
};
```

```ts | #THINGJS_SCRIPT_52
const app = new THING.App();

await app.load("./assets/scenes/campus/campus.gltf");
```

这样,下方的代码块就有了id了。

再在jsx中将它移动到右侧代码块:

tsx 复制代码
useEffect(() => {
  if (!scriptId) {
    return;
  }
  const script = document.getElementById(scriptId) as HTMLDivElement;
  if (!script) {
    console.warn(`script with id ${scriptId} not found`);
    return;
  }
  codeRef.current!.appendChild(script);
}, []);

搞复杂了!这圈子绕的,有些远了啊!

总结

本文详细介绍了我在使用 dumi 展现3D示例过程中遇到的挑战和解决方案。

我们的文档网站以Markdown为主,没有动态内容,API和示例部分使用iframe嵌套,需要优化搜索功能。这时,我们有三种方案处理:

  1. 现有功能 +Elastic Search(ES)优化。
  2. 现有功能 +algolia等第三方搜索服务。
  3. 使用 dumi 的纯前端方案。

最终选择了第三种方案,因为 dumi 有以下优点:

  1. 全文搜索,省去了后端服务。
  2. UI 设计更美观,支持响应式布局、暗黑模式、多语言等
  3. 可扩展 Markdown 的自定义组件。

但在实际调研过程中,遇到了示例部分的挑战。

  1. 示例部分需要实现左右布局,但还要兼顾代码块的全文搜索。
  2. dumi搜索仅包含静态 Markdown 部分,动态 React 组件无法被搜索。

这时衍生了以下解决方案:

  1. 魔改 dumi 源码 Hook,想办法将代码块动态注入搜索源。缺点是需要注入研究dumi搜索机制,较为复杂。
  2. 将代码块批量生成Markdown文件,上部分是React组件,呈现3D效果,下部分是代码块。缺点是只有上下布局。
  3. 在第二个方案基础上,上部分组件同时呈现3D和代码,添加元属性隐藏掉下方的代码块,仅供搜索使用。
  4. 将示例组件的代码块乾坤大挪移,上部分仍是左右布局,但右侧是空的div,在组件挂载后,将下方的代码块移动到右侧。这种方法简单而有效,它既复用了dumi的现有组件,同时保证了搜索功能的完整性。

虽然最终的方案看起来平平无奇,但不失为一种有趣的思路。希望能对大家有所启发!

相关推荐
前端爆冲3 分钟前
项目中无用export的检测方案
前端
热爱编程的小曾31 分钟前
sqli-labs靶场 less 8
前端·数据库·less
gongzemin42 分钟前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
Apifox1 小时前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
树上有只程序猿1 小时前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼2 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下2 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox2 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞2 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行2 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox