玩转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的现有组件,同时保证了搜索功能的完整性。

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

相关推荐
天下无贼!1 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr1 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林1 小时前
npm发布插件超级简单版
前端·npm·node.js
罔闻_spider1 小时前
爬虫----webpack
前端·爬虫·webpack
吱吱鼠叔2 小时前
MATLAB数据文件读写:1.格式化读写文件
前端·数据库·matlab
爱喝水的小鼠2 小时前
Vue3(一) Vite创建Vue3工程,选项式API与组合式API;setup的使用;Vue中的响应式ref,reactive
前端·javascript·vue.js
盏灯2 小时前
前端开发,场景题:讲一下如何实现 ✍电子签名、🎨你画我猜?
前端
WeiShuai2 小时前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
Wandra2 小时前
很全但是超级易懂的border-radius讲解,让你快速回忆和上手
前端
ice___Cpu2 小时前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端