自定义主题

Island.js 内置一套开箱即用的默认主题,且内部的组件也提供了配置来自定义,包括:

你可以点进对应的页面查看具体的配置项。本文所要介绍的是如何开发一个自定义主题。

基于默认主题的扩展

大部分情况下,你并不想从零开始开发一个主题,而是想基于默认主题进行扩展,这时候可以参考下面的方式进行主题开发。

TIP

如果你想从头开发一个自定义主题,可以前往【重新开发自定义主题】

1. 基本结构

默认情况下,你需要在 .island 目录下创建一个 theme 目录,然后在这个目录下创建一个 index.ts(x) 文件,这个文件就是你的主题入口文件:

bash
.island
├── config.ts
└── theme
    └── index.tsx

你可以使用如下的方式来书写 theme/index.tsx 文件:

ts
// `theme/index.tsx`
import React from 'react';
import {
  Layout as DefaultLayout,
  NotFoundLayout,
  HomeLayout,
  setup
} from 'islandjs/theme';
// 添加一些自定义的样式
import './custom.css';

const Layout = () => <DefaultLayout beforeHero={<div>beforeHero</div>} />;

// 导出三个组件和 setup 函数
export { Layout, HomeLayout, NotFoundLayout, setup };

可以看到,你能从islandjs/theme中获取并导出默认主题的各个组件,包括:

  • Layout 组件,页面的布局组件
  • HomeLayout 组件,首页的布局组件,被 Layout 组件使用
  • NotFoundLayout 组件,404 页面的布局组件,被 Layout 组件使用
  • 还包括 setup 函数,用来添加一个初始化的逻辑,比如全局事件绑定,必须导出

2. 使用插槽

值得注意的是,Layout 组件设计了一系列的 props 支持插槽元素,你可以通过这些 props 来扩展默认主题的布局,比如将上面的 Layout 组件改成如下的形式:

tsx
import { Layout as DefaultLayout } from 'islandjs/theme';

// 以下展示所有的 Props
const Layout = () => (
  <DefaultLayout
    /* Home 页 Hero 部分之前 */
    beforeHero={<div>beforeHero</div>}
    /* Home 页 Hero 部分之后 */
    afterHero={<div>afterHero</div>}
    /* Home 页 Features 部分之前 */
    beforeFeatures={<div>beforeFeatures</div>}
    /* Home 页 Features 部分之后 */
    afterFeatures={<div>afterFeatures</div>}
    /* 正文页 Footer 部分之前 */
    beforeDocFooter={<div>beforeDocFooter</div>}
    /* 正文页最前面 */
    beforeDoc={<div>beforeDoc</div>}
    /* 正文页最后面 */
    afterDoc={<div>afterDoc</div>}
    /* 左上角导航栏标题之前 */
    beforeNavTitle={<span>😄</span>}
    /* 左上角导航栏标题之后 */
    afterNavTitle={<div>afterNavTitle</div>}
    /* 右侧大纲栏上面 */
    beforeOutline={<div>beforeOutline</div>}
    /* 右侧大纲栏下面 */
    afterOutline={<div>afterOutline</div>}
    /* 整个页面最顶部 */
    top={<div>top</div>}
    /* 整个页面最底部 */
    bottom={<div>bottom</div>}
  />
);

3. 自定义组件

要扩展默认主题的组件,除了插槽,你还可以自定义 Home 页面及 404 页面组件,比如:

tsx
import { Layout, setup } from 'islandjs/theme';
// 定制 Home 页面
const HomeLayout = () => <div>Home</div>;
// 定制 404 页面
const NotFoundLayout = () => <div>404</div>;

export { Layout, HomeLayout, NotFoundLayout, setup };

当然,在开发过程可能需要使用页面的数据,你可以通过 usePageData 这个 Hook 来获取。

重新开发自定义主题

1. 基本结构

当然,如果你要从头开始开发一个自定义主题,你需要了解一下主题的组成。

默认情况下,你需要在 .island 目录下创建一个 theme 目录,然后在这个目录下创建一个 index.ts(x) 文件,这个文件就是你的主题入口文件:

bash
.island
├── config.ts
└── theme
    └── index.tsx

theme/index.tsx文件中,你需要导出一个 Layout 组件,这个组件就是你的主题的入口组件:

ts
// theme/index.tsx
function Layout() {
  return <div>Custom Theme Layout</div>;
}

// setup 函数,会在页面初始化时调用,一般用来做全局事件的监听,可为空函数
const setup = () => {};

// 导出 Layout 组件和 setup 函数
// 两者必须导出
export { Layout, setup };

这个 Layout 组件会被 Island.js 用来渲染整个文档站点的布局,你可以在这个组件中引入你的自定义组件,比如:

ts
// theme/index.tsx
import { Navbar } from './Navbar';

function Layout() {
  return (
    <div>
      <Navbar />
      <div>Custom Theme Layout</div>
    </div>
  );
}

export { Layout };

// theme/Navbar.tsx
export function Navbar() {
  return <div>Custom Navbar</div>;
}

那么问题来了,主题组件是如何获取页面数据和正文 MDX 组件内容的呢?这就需要用到 Island.js 的 Runtime API 了。

2. 接入 Runtime API

在主题开发的过程中,我们一般需要如下的一些关键 API:

usePageData

用户获取页面所有数据的 Hook,比如:

tsx
import { usePageData } from 'islandjs/runtime';

function Layout() {
  const pageData = usePageData();
  return <div>{pageData.title}</div>;
}

usePageData 类型如下:

ts
const usePageData: () => PageData;

PageData 的类型如下:

ts
export interface PageData {
  // 站点通用信息,包含主题配置 themeConfig 信息
  siteData: SiteData;
  // 上次更新时间
  lastUpdatedTime?: string;
  // 页面标题
  title?: string;
  // 页面描述
  description?: string;
  // Front Matter 元数据
  frontmatter?: FrontMatterMeta;
  // 页面类型
  pageType: PageType;
  // TOC 数据,包含标题和对应的锚点
  toc?: Header[];
  // 当前路由路径
  routePath: string;
  // 页面的路径,去掉 query 和 hash
  pagePath: string;
  // 页面的正文内容
  content?: string;
}

基于此 API,你基本可以获取到需要的所有数据。

Content

获取正文 MDX 组件内容,即正文内容:

ts
import { Content } from 'islandjs/runtime';

function Layout() {
  return (
    <div>
      <Content />
    </div>
  );
}

路由 Hook

Island.js 内部用 react-router-dom 来实现路由,所以你可以直接使用 react-router-dom 的 Hook,比如:

ts
import { useLocation } from 'islandjs/runtime';

function Layout() {
  const location = useLocation();
  return <div>Current location: {location.pathname}</div>;
}