框架切换,捆绑器切换,自定义侧边栏和更好的用户体验。以及我如何在 Astro Starlight 上自定义文档。

Gueleton 帮助开发者从现有页面自动生成占位骨架,这是我最近在做的一个项目。为了他能够帮助到更多的开发者,我当然想使其能够运行在任何前端框架上。

因此,server side 我使用 unplugin 开发,在 client side 通过纯 JS 代码为核心,再在上层开发各个前端框架的包装器。这在库开发上这是常见做法,开发者总是希望自己的代码能够在通用性,易用性和可维护性上取得一个良好的平衡。

但在文档方面,一个支持渲染多框架组件,并具有良好交互的文档工具并不常见。

文档并不容易

得益于 unplugin 的支持,Gueleton 支持多种构建工具,如 vitenuxtwebpack。 并且在客户端上 Gueleton 支持 VueReactSolidjsSvelte

这让我非常希望找到一个支持多框架,多构建工具切换的文档。经过一番查找,最合适的是,Starlight:一个内容驱动的 Astro 文档框架,它快速方便,内置搜索功能。

Starlight 基于 Astro,一个能够无缝运行各种 UI 组件的框架。我可以立即用它在一个页面上同时渲染 Vue 和 React 组件,这非常好。我不用再为每个框架创建不同的文档项目。

但这还不够,我还有几种前端框架及其构建工具的排列组合需要处理。

---
config:
  sankey:
    showValues: false
---
sankey-beta

Vue,Vite,1
Vue,Nuxt,1
Vue,Webpack,1
Vue,VueCli,1
React,Vite,1
React,Next,1
React,Webpack,1
React,CreateReactApp,1
Solidjs,Vite,1
Solidjs,SolidStart,1
Svelte,Vite,1
Svelte,SvelteKit,1

极端情况下,我们需要为每种组合编写单独的文档,这显然不是一个轻松的活。

一种理想的解决方案是 Vercel 文档网站 上的做法。如下所示,在切换侧边栏的框架选项时,文档细粒度的改变了内容。

这几乎就是我需要的文档,相信你也见过很多这样的文档结构,但是你要考虑过它如何实现吗?

基本上我们需要确定以下要求:

  • 没有重复的内容:所有内容都应该在一个地方进行维护。
  • 按框架切换内容:文本,代码片段等:自然的文档流,易于阅读。
  • 没有冗余的侧边导航:侧边栏不应该包含与当前框架无关的内容。

为了实现上述目标,我决定对 Starlight 进行一些自定义。

页面内容

目标是在一个页面上可以根据框架、语言提供不同的内容。用户可以通过页面交互选择自己想要的框架,语言。

虽然 Starlight 没有内置处理这种场景的功能,但好消息是 Astro 提供了足够的灵活性使得我们可以通过自定义来实现这些功能。

选择器是实现功能的关键组件,左边的选择器选择前端框架,右边的用来选择构建工具。

在内部,选择器将通过 MDX frontmatter 来了解要提供哪些框架:

---
title: 安装
description: 在不同的构建工具和框架中安装 Gueleton
frameworks:
- vue
- react
- angular
- vanilla
---

为了支持自定义 MDX frontmatter schema,只需要对 Starlight 文档集合模式进行一些补充:

import { defineCollection, z } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
 
export const collections = {
  docs: defineCollection({ 
    loader: docsLoader(), 
    schema: docsSchema({
      extend: z.object({
        // 支持的框架列表
        frameworks: z.custom<FrameworkKey[]>().optional(),
      }),
    })
  }),
};

这将使得我们可以在脚本中获取 frontmatter 定义的 frameworks 数据,并根据这些数据渲染框架选择器。

然后,还需要一个包装器,他将根据选择器当前的选项,渲染对应框架的内容。

将 `unplugin-gueleton` 作为插件添加到你的构建工具配置中
 
<FrameworkSlots>
  <Code slot="nuxt" code={PluginSetupNuxt} lang="ts" title="nuxt.config.ts" />
  <Code slot="vite" code={PluginSetupVite} lang="ts" title="vite.config.ts" />
  <Code slot="webpack" code={PluginSetupWebpack} lang="js" title="webpack.config.js" />
  <Code slot="vue-cli" code={PluginSetupVueCli} lang="js" title="vue.config.js" />
</FrameworkSlots>
 

现在我们有了选择器的选项,还需要获取选择器当前所选的项。为了做得更好,将选项保存在本地存储中以便用户不会丢失首选项,还可以从 URL 查询参数中获取首选项。

共享状态

要让 FrameworkSlots 根据选择器的当前值变化,我们当然需要让他们能够访问同样的数据。

我使用 Astro 提供的 Nano Stores 创建状态管理。

// store.ts
 
import { atom } from 'nanostores';
import type { BoundlerKey, FrameworkKey } from '../content.config';
import type { Framework } from '../lib/frameworks';
import { frameworks, defaultFramework } from '../lib/frameworks';
 
// 当前显示的框架
export const displayedFramework = atom<FrameworkKey>(defaultFramework);
 
// URL 参数中的框架
export const queryParamFramework = atom<FrameworkKey | undefined>();
 
// 当前页面可用的框架列表
export const availableFrameworks = atom<Framework[]>(frameworks);

在组件中管理:

// FrameworkSwitcher.vue
 
<template>
...
</template>
 
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useStore } from '@nanostores/vue';
import { 
  displayedFramework, 
  availableFrameworks, 
  setDisplayedFramework,
  initializeFramework 
} from '../store/frameworks';
import type { FrameworkKey } from '../content.config';
import { getFramework } from '../lib/frameworks';
import type { StarlightRouteData } from "@astrojs/starlight/route-data";
 
interface Props {
  ...
  astroEntry: StarlightRouteData["entry"];
}
 
const props = withDefaults(defineProps<Props>(), {});
 
const $displayedFramework = useStore(displayedFramework);
const $availableFrameworks = computed(() => (props.astroEntry?.data?.frameworks || []).map((f) => getFramework(f)));
 
const handleChange = (event: Event) => {
  const target = event.target as HTMLSelectElement;
  const framework = target.value as FrameworkKey;
  setDisplayedFramework(framework);
};
 
onMounted(() => {
  initializeFramework();
});
 
</script>
// FrameworkSlots.astro
 
<!-- 使用自定义元素 -->
<framework-slots>
  <div data-framework="vite" class="framework-slot" style="display: none;">
    <slot name="vite" />
  </div>
  ...
</framework-slots>
 
<script>
  import { displayedFramework } from "src/store/frameworks";
 
  class FrameworkSlots extends HTMLElement {
    connectedCallback() {
      ...
      this.updateDisplay();
    }
 
    updateDisplay() {
      const currentFramework = displayedFramework.get();
      const allSlots = this.querySelectorAll(".framework-slot");
 
      allSlots.forEach(slot => slot.style.display = 'none');
      
      const targetSlot = this.querySelector(`[data-framework="${currentFramework}"]`);
      if (targetSlot) {
        targetSlot.style.display = 'block';
      }
    }
  }
 
  customElements.define('framework-slots', FrameworkSlots);
</script>

如何组织文件

Starlight 已经提供了默认的文件结构,并且它还将根据文件结构生成侧边栏内容。

对特定框架的代码片段等内容,我将在 src/ 下创建一个自定义的 snippets/ 目录,所有特定于框架的代码片段会被放置在这里面。

每个代码示例都是一个实际存在的源文件,将通过 Vite 的 ?raw 功能作为纯文本导入。

这样的好处是在编辑代码示例是,我们可以享受现代 IDE 提供的语法纠错,不至于回想起在记事本上写代码的体验。🐶

自定义 Astro Starlight 页面侧边栏

Astro Starlight 提供了重写和复用其默认UI组件的能力。

// overrides/PageSidebar.astro
 
---
import Default from '@astrojs/starlight/components/PageSidebar.astro';
import FrameworkSwitcher from '@components/FrameworkSwitcher.vue';
 
const props = Astro.locals.starlightRoute;
---
 
<>
    <!-- 框架选择器 -->
  <div class="framework-switcher-container p-4 border-b border-gray-200 dark:border-gray-700">
    <div class="w-min flex gap-2">
    <FrameworkSwitcher size="sm" client:load astroEntry={props.entry} />
    <BoundlerSwitcher size="sm" client:load />
    </div>
  </div>
 
  <!-- 默认侧边栏 -->
  <Default {...Astro.props}>
  </Default>
</>
 

然后,用 overrides/PageSidebar.astro 覆盖原始的 PageSidebar.astro

// astro.config.mjs
 
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
 
// https://astro.build/config
export default defineConfig({
  ...
  integrations: [starlight({
    title: 'Gueleton',
    components: {
      ...
      PageSidebar: './src/components/overrides/PageSidebar.astro',
    },
  })],
});

全新的文档

稍后,在几周内,我将发布这个文档。用户将在他们选择的框架内获得更好的体验。

MDX 的编写体验很不错,虽然相对于 Markdown 多了些复杂度,但同时为文档页面带来了更大的灵活性,与 Astro 结合后,我们可以在 MDX 中运行几乎任何框架开发的组件。

查看 Gueleton 文档 并检查其功能。