Normal view

There are new articles available, click to refresh the page.
Before yesterdayMain stream

为 Docusaurus 添加 Shiki 代码高亮支持

By: Ryan Wang
29 October 2025 at 23:24

本文记录 https://docs.halo.run 集成 Shiki 代码高亮的过程。

引言

从 Halo 1.x 开始,我们就一直在使用 Docusaurus 来构建 Halo 的文档。直到 Halo 2.21,我们已经累积了大量的文档,期间发现代码块高亮的问题难以解决。Docusaurus 默认使用 Prism.js 来渲染代码块,且几乎没有其他选择。而我们的文档中使用的一些语言或框架(如 Vue),Prism 并没有提供高亮支持,因此长期以来这些代码块都没有显示语法高亮。即使是 Prism 所支持的语言,渲染出来的语法高亮效果也不尽如人意。

直到后来了解到了 https://shiki.style/,一个较新的代码高亮库,基于与 VSCode 同源的 TextMate 代码高亮引擎,渲染出来的代码效果与 VSCode 一致,并且支持现在主流语言和框架代码的高亮。我认为这几乎是当前各方面最优的代码高亮库。后面我们也为 Halo 开发了 Shiki 代码高亮插件,表现十分良好。因此最近也考虑将 Halo 文档中的代码高亮渲染改为使用 Shiki。好在搜索一番后,已经有了一个广泛讨论的 issue:https://github.com/facebook/docusaurus/issues/9122,并且里面也有人提供了可行的方案。于是我按照这个方案并添加了一些额外功能,本文将记录实现的完整过程。如果你也在用 Docusaurus,并且对 Shiki 有需求,可以参考本文。

背景

  • 目前使用的 Docusaurus 版本是 3.8.0

安装依赖

Docusaurus 的 Markdown 引擎核心为 https://mdxjs.com/,而 MDX 内部基于 remarkrehype,因此 Docusaurus 实际上预留了可以添加 rehype 插件的配置,所以可以直接使用 Shiki 官方的 rehype 插件

pnpm add @shikijs/rehype shiki -D

添加配置

const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
// [!code ++:11]
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
                theme: "catppuccin-mocha",
                langs: Object.keys(bundledLanguages),
                // or
                // langs: ['js', 'ts']
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

其中,langs 可以只填写所需的语言列表,我这里为了省事直接添加 Shiki 所有语言,主要是因为文档太多,已经懒得去统计用到了哪些语言。

此外,theme 也可以指定多主题,如果需要让文档的暗色和亮色模式下代码块的主题不同,可以按照下面的方式更改:

const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
// [!code --]
                theme: "catppuccin-mocha",
// [!code ++:4]
                themes: {
                  light: "github-light",
                  dark: "github-dark"
                },
                langs: Object.keys(bundledLanguages),
                // or
                // langs: ['js', 'ts']
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

module.exports = config;

然后在 custom.css 中添加:

[data-theme="dark"] pre {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
}
[data-theme="dark"] pre span {
  color: var(--shiki-dark) !important;
}

由于我期望亮色和暗色模式下都使用暗色的代码块主题,所以没有添加多主题配置。

组件覆盖

由于需要完全让 @shikijs/rehype 接管 Markdown 文档中的代码块渲染,我们需要覆盖 Docusaurus 内部 Pre/Code 的组件,避免被默认的 Prism 处理。Docusaurus 默认提供了 CLI 用于导出 Docusaurus 主题中的组件。

npx docusaurus swizzle @docusaurus/theme-classic MDXComponents/Code --typescript --eject

然后打开 src/theme/MDXComponents/Code.tsx 并修改为:

import type { ComponentProps, ReactNode } from "react";
import React from "react";
import CodeInline from "@theme/CodeInline";
import type { Props } from "@theme/MDXComponents/Code";

function shouldBeInline(props: Props) {
  return (
    // empty code blocks have no props.children,
    // see https://github.com/facebook/docusaurus/pull/9704
    typeof props.children !== "undefined" &&
    React.Children.toArray(props.children).every(
      (el) => typeof el === "string" && !el.includes("\n")
    )
  );
}

// [!code ++:3]
function CodeBlock(props: ComponentProps<"code">): JSX.Element {
  return <code {...props} />;
}

export default function MDXCode(props: Props): ReactNode {
  return shouldBeInline(props) ? (
    <CodeInline {...props} />
  ) : (
    <CodeBlock {...(props as ComponentProps<typeof CodeBlock>)} />
  );
}
npx docusaurus swizzle @docusaurus/theme-classic MDXComponents/Pre --typescript --eject

然后打开 src/theme/MDXComponents/Pre.tsx 并修改为:

import React, { type ReactNode } from "react";
import type { Props } from "@theme/MDXComponents/Pre";
export default function MDXPre(props: Props): ReactNode | undefined {
  return <pre {...props} />;
}

小插曲:当时到了这一步的时候,突然意识到似乎可以复用之前为 Halo 开发 Shiki 插件时发布的 NPM 包(@halo-dev/shiki-code-element)。因为这个包封装了一个 Web Component,所以肯定可以用在这里,只需要在 pre 标签外包裹一个 shiki-code 即可。尝试了一下确实可行,但这样就必须在客户端渲染了。虽然可行,但始终不如在构建阶段就渲染好。虽然可以尝试使用 Lit SSR,但考虑到文档中有一些代码块使用了 Title Meta,而目前 @halo-dev/shiki-code-element 还不支持,所以放弃了这个方案。

完成这一步之后,就可以尝试启动开发服务器了。不出意外的话,代码块就可以正常使用 Shiki 来渲染了。

添加标题支持

原来 Docusaurus 的默认方案是支持为代码块添加顶部标题的,切换到 Shiki 之后,这一部分需要自行实现,以下是具体步骤:

首先为 Shiki 添加一个自定义的 Transformer,用于解析标题的书写语法和添加代码块参数:

创建 src/shiki/meta-transformer.js

function parseTitleFromMeta(meta) {
  if (!meta) {
    return "";
  }
  const kvList = meta.split(" ").filter(Boolean);
  for (const item of kvList) {
    const [k, v = ""] = item.split("=").filter(Boolean);
    if (k === "title" && v.length > 0) {
      return v.replace(/["'`]/g, "");
    }
  }
  return "";
}

export function transformerAddMeta() {
  return {
    name: "shiki-transformer:add-meta",
    pre(pre) {
      const title = parseTitleFromMeta(this.options.meta?.__raw);
      if (title.length > 0) {
        pre.properties = {
          ...pre.properties,
          "data-title": title,
        };
      }
      return pre;
    },
  };
}

然后修改配置:

const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");
const { transformerAddMeta } = require("./src/shiki/meta-transformer");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
                theme: "catppuccin-mocha",
                langs: Object.keys(bundledLanguages),
// [!code ++:3]
                transformers: [
                  transformerAddMeta(),
                ]
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

修改 Pre.tsx 显示标题:

import React, { type ReactNode } from "react";
import type { Props } from "@theme/MDXComponents/Pre";

type PreWithDataTitle = Props & { "data-title"?: string };

export default function MDXPre(props: Props): ReactNode | undefined {
  const title = props["data-title"];
  return (
    <div
      style={{
        ...props.style,
        borderRadius: "var(--ifm-pre-border-radius)",
      }}
      className="shiki-code-wrapper"
    >
      {title && (
        <div className="shiki-code-header">
          <span>{title}</span>
        </div>
      )}
      <div className="shiki-code-content">
        <pre {...props} ref={preRef} />
      </div>
    </div>
  );
}

这里还修改了 MDXPre 标签的组件结构,为后续的功能做准备,其中最外层的 div 添加的 style 属性来自于 Shiki 渲染结果的 pre 标签的样式,包含背景色和字体默认颜色。

最后我们需要为自定义的 MDXPre 组件结构添加样式,这里为了让结构看起来更清晰,我引入了 SASS 插件:

# 安装所需依赖
pnpm add docusaurus-plugin-sass sass -D

修改配置文件:

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        // ...
        theme: {
// [!code --]
          customCss: require.resolve("./src/css/custom.css"),
// [!code ++:4]
          customCss: [
            require.resolve("./src/css/custom.css"),
            require.resolve("./src/css/shiki.scss"),
          ],
        },
      }),
    ],
  ],
// [!code ++]
  plugins: [require.resolve("docusaurus-plugin-sass")],
};

然后创建 src/css/shiki.scss

.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }
}

这样就支持使用原有的语法为代码块添加标题了。

显示行号

Shiki 原生并不支持在渲染的 HTML 结果中包含行号信息,但社区中有人提供了一种使用纯 CSS 实现的方案,详见:https://github.com/shikijs/shiki/issues/3#issuecomment-830564854

修改 shiki.scss

.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }
// [!code ++:36]
  .shiki-code-content {
    position: relative;

    pre {
      position: relative;
      padding: 0.75rem;
      margin: 0;
      border-radius: initial;
      code {

        counter-reset: step;
        counter-increment: step 0;

        .line {
          position: relative;
        }

        // line numbers start
        .line::before {
          content: counter(step);
          counter-increment: step;
          width: 0.6rem;
          margin-right: 1.1rem;
          display: inline-block;
          text-align: right;
          color: rgba(115, 138, 148, 0.5);
          user-select: none;
        }

        .line:last-child:empty::before {
          content: none;
          counter-increment: none;
        }
        // line numbers end
      }
    }
  }
}

这样就可以默认为所有代码块添加行号显示了。

复制按钮

Docusaurus 默认的代码块有复制按钮,改为 Shiki 之后这部分也需要自行实现,以下是具体步骤:

修改 src/theme/MDXComponents/Pre.tsx

import React, { type ReactNode, useRef, useState } from "react";
import type { Props } from "@theme/MDXComponents/Pre";

type PreWithDataTitle = Props & { "data-title"?: string };

export default function MDXPre(props: PreWithDataTitle): ReactNode | undefined {
  const title = props["data-title"];
// [!code ++:11]
  const preRef = useRef<HTMLPreElement>(null);
  const [copied, setCopied] = useState(false);

  const handleCopy = () => {
    const code = preRef.current?.innerText || preRef.current?.textContent || "";

    copyText(code, () => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };

  return (
    <div
      style={{
        ...props.style,
        borderRadius: "var(--ifm-pre-border-radius)",
      }}
      className="shiki-code-wrapper"
    >
      {title && (
        <div className="shiki-code-header">
          <span>{title}</span>
        </div>
      )}
      <div className="shiki-code-content">
// [!code ++:8]
        <button
          className="shiki-code-copy-button"
          onClick={handleCopy}
          title={copied ? "已复制!" : "复制代码"}
          style={{ ...props.style }}
        >
          <i className={copied ? "tabler--check" : "tabler--copy"}></i>
        </button>
        <pre {...props} ref={preRef} />
      </div>
    </div>
  );
}

// [!code ++:24]
export function copyText(text: string, cb: () => void) {
  if (navigator.clipboard) {
    navigator.clipboard.writeText(text).then(() => {
      cb();
    });
  } else {
    const textArea = document.createElement("textarea");
    textArea.value = text;
    textArea.style.position = "fixed";
    textArea.style.opacity = "0";
    document.body.appendChild(textArea);
    textArea.focus();
    textArea.select();
    try {
      const successful = document.execCommand("copy");
      if (successful) {
        cb();
      }
    } catch (err) {
      console.error("Fallback: Oops, unable to copy", err);
    }
    document.body.removeChild(textArea);
  }
}

添加样式:

.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }

  .shiki-code-content {
    position: relative;

    pre {
      position: relative;
      padding: 0.75rem;
      margin: 0;
      border-radius: initial;
      code {

        counter-reset: step;
        counter-increment: step 0;

        .line {
          position: relative;
        }

        // line numbers start
        .line::before {
          content: counter(step);
          counter-increment: step;
          width: 0.6rem;
          margin-right: 1.1rem;
          display: inline-block;
          text-align: right;
          color: rgba(115, 138, 148, 0.5);
          user-select: none;
        }

        .line:last-child:empty::before {
          content: none;
          counter-increment: none;
        }
        // line numbers end
      }
    }
// [!code ++:15]
    .shiki-code-copy-button {
      position: absolute;
      top: 0.5rem;
      right: 0.5rem;
      opacity: 0;
      z-index: 2;
      width: 2rem;
      height: 2rem;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0.4rem;
      border: none;
      cursor: pointer;
    }
  }
// [!code ++:33]
  &:hover {
    .shiki-code-copy-button {
      opacity: 1;
    }
  }

  .tabler--copy {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z'/%3E%3Cpath d='M4.012 16.737A2 2 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1'/%3E%3C/g%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }

  .tabler--check {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m5 12l5 5L20 7'/%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }
}

这样就可以在鼠标悬停到代码块时显示复制按钮了。

集成必要的 Transformer

Shiki 官方提供了一些非常有用的 Transformers,比如行高亮、代码对比等,这里根据自身需要添加即可。

pnpm add -D @shikijs/transformers
const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");
const { transformerAddMeta } = require("./src/shiki/meta-transformer");
// [!code ++:4]
const { transformerMetaHighlight } = require("@shikijs/transformers");
const { transformerNotationDiff } = require("@shikijs/transformers");
const { transformerNotationFocus } = require("@shikijs/transformers");
const { transformerNotationErrorLevel } = require("@shikijs/transformers");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
                theme: "catppuccin-mocha",
                langs: Object.keys(bundledLanguages),
                transformers: [
// [!code ++:8]
                  // 行高亮,使用 Meta 信息的方式,比如 ```java {1}
                  transformerMetaHighlight(),
                  // 代码对比,使用注释的方式
                  transformerNotationDiff(),
                  // 行聚焦,使用注释的方式
                  transformerNotationFocus(),
                  // 行高亮的错误和警告变体,使用注释的方式
                  transformerNotationErrorLevel(),
                  transformerAddMeta(),
                ]
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

这些 Transformers 只是为对应的行添加了 class,我们需要自行实现样式。以下是完整的 shiki.scss

点击查看 shiki.scss
.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }

  .shiki-code-content {
    position: relative;

    pre {
      position: relative;
      padding: 0.75rem;
      margin: 0;
      border-radius: initial;
      code {
        z-index: 1;
        display: block;
        width: max-content;
        position: relative;
        min-width: 100%;

        counter-reset: step;
        counter-increment: step 0;

        .line {
          position: relative;
        }

        // line numbers start
        .line::before {
          content: counter(step);
          counter-increment: step;
          width: 0.6rem;
          margin-right: 1.1rem;
          display: inline-block;
          text-align: right;
          color: rgba(115, 138, 148, 0.5);
          user-select: none;
        }

        .line:last-child:empty::before {
          content: none;
          counter-increment: none;
        }
        // line numbers end

        // highlighted lines start
        .highlighted {
          width: 100%;
          display: inline-block;
          position: relative;
        }

        .highlighted::after {
          content: "";
          position: absolute;
          top: 0;
          bottom: 0;
          left: -0.75rem;
          right: -0.75rem;
          background: rgba(101, 117, 133, 0.16);
          border-left: 1px solid rgba(34, 197, 94, 0.8);
          z-index: 0;
        }

        .highlighted.error::after {
          background: rgba(244, 63, 94, 0.16) !important;
        }

        .highlighted.warning::after {
          background: rgba(234, 179, 8, 0.16) !important;
        }
        // highlighted lines end
      }

      // focus line start
      &.has-focused .line:not(.focused) {
        opacity: 0.7;
        filter: blur(0.095rem);
        transition: filter 0.35s, opacity 0.35s;
      }

      &.has-focused:hover .line:not(.focused) {
        opacity: 1;
        filter: blur(0);
      }
      // focus line end

      // diff start
      &.has-diff .diff {
        width: 100%;
        display: inline-block;
        position: relative;
      }

      &.has-diff .diff.remove::before {
        content: "-";
      }

      &.has-diff .diff.add::before {
        content: "+";
      }

      &.has-diff .diff.remove::after {
        content: "";
        position: absolute;
        top: 0;
        bottom: 0;
        left: -0.75rem;
        right: -0.75rem;
        background: rgb(239 68 68 / 0.15);
        border-left: 1px solid rgb(239 68 68 / 0.8);
        z-index: -1;
      }

      &.has-diff .diff.add::after {
        content: "";
        position: absolute;
        top: 0;
        bottom: 0;
        left: -0.75rem;
        right: -0.75rem;
        background: rgb(34 197 94 / 0.15);
        border-left: 1px solid rgb(34 197 94 / 0.8);
        z-index: -1;
      }
      // diff end
    }

    .shiki-code-copy-button {
      position: absolute;
      top: 0.5rem;
      right: 0.5rem;
      opacity: 0;
      z-index: 2;
      width: 2rem;
      height: 2rem;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0.4rem;
      border: none;
      cursor: pointer;
    }
  }

  &:hover {
    .shiki-code-copy-button {
      opacity: 1;
    }
  }

  .tabler--copy {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z'/%3E%3Cpath d='M4.012 16.737A2 2 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1'/%3E%3C/g%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }

  .tabler--check {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m5 12l5 5L20 7'/%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }
}

至此,一个比较好看、功能丰富的代码高亮改造方案就完成了,具体修改代码也可以查阅 Halo 文档的 PR:https://github.com/halo-dev/docs/pull/521,也可以访问 Halo 文档查看修改之后的效果。

参考资料

使用 Rspack 构建 Halo 插件的前端部分

By: Ryan Wang
16 June 2025 at 14:37

更新(25-06-19)

现在已经为插件的 UI 部分提供了新的配置方式,https://www.npmjs.com/package/@halo-dev/ui-plugin-bundler-kit 包提供了 rsbuildConfig 方法,可以更加方便的使用 https://rsbuild.dev/ 来构建 UI 部分。

Rsbuild 基于 Rspack 构建,提供了更完善的 loader 配置,所以在封装的时候就直接选择了 Rsbuild。

安装依赖:

pnpm install @halo-dev/ui-plugin-bundler-kit@2.21.1 @rsbuild/core -D

rsbuild.config.mjs:

import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";

export default rsbuildConfig()

package.json 添加 scripts:

{
  "type": "module",
  "scripts": {
    "dev": "rsbuild build --env-mode development --watch",
    "build": "rsbuild build"
  }
}

需要注意的是,为了适应新版的 https://github.com/halo-dev/plugin-starter,默认生产构建输出目录改为了 ui/build/dist ,如果你要从已有的插件项目迁移到 Rsbuild,建议参考 https://github.com/halo-dev/plugin-starter/pull/52 对 Gradle 脚本进行改动,或者自定义 Rsbuild 的配置以保持原有的输出目录配置:

import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";

const OUT_DIR_PROD = "../src/main/resources/console";
const OUT_DIR_DEV = "../build/resources/main/console";

export default rsbuildConfig({
  rsbuild: ({ envMode }) => {
    const isProduction = envMode === "production";
    const outDir = isProduction ? OUT_DIR_PROD : OUT_DIR_DEV;

    return {
      resolve: {
        alias: {
          "@": "./src",
        },
      },
      output: {
        distPath: {
          root: outDir,
        },
      },
    };
  },
});

示例:https://github.com/halo-sigs/plugin-migrate

了解更多:https://docs.halo.run/developer-guide/plugin/basics/ui/build


前情提要

Halo 插件的 UI 部分(Console / UC)的实现方式其实很简单,本质上就是构建一个结构固定的大对象,交给 Halo 去解析,其中包括全局注册的组件、路由定义、扩展点等。 基于这个前提,在实现插件机制时,主要面临的问题就是如何将这个大对象传递给 Halo。当初做了非常多的尝试,最终选择构建为 IIFE(Immediately Invoked Function Expression,立即执行函数),然后 Halo 通过读取 window[PLUGIN_NAME](PLUGIN_NAME 即插件名)来获取这个对象。 构建方案采用 Vite,并提供了统一的构建配置。回过头来看,这个方案存在不少问题:

  1. 会污染 window 对象,虽然目前并没有出现因为这个导致的问题,但是从长远来看,这个方案并不是最优的。(当然,使用 Rspack 来构建并不是为了解决这个问题)

  2. Vite 不支持 IIFE / UMD 格式的代码分割(主要是 Rollup 还不支持),无法像 ESM(ECMAScript Module)那样实现异步加载模块的机制。

  3. 基于第 2 点,如果插件中实现了较多的功能,可能会导致最终产物体积巨大,尤其是当用户安装了过多的插件时,会导致页面加载缓慢。

    1. http://www.halo.run/ 为例,gzip 之前接近 10M 的 bundle.js,gzip 之后也有 2M - 3M。

    2. 以此博客为例,gzip 之后也有 1.8M 的 bundle.js。

  4. 基于第 2 点,如果不支持代码分块(Chunk),也无法充分利用资源缓存,访问页面时,也会一次性加载所有插件的代码(即便当前页面不需要)。

基于以上问题,我开始寻找其他替代方案,最终通过翻阅 Rspack(Webpack 的 Rust 实现)的文档发现,Webpack 能够通过配置实现 IIFE 格式的代码分割,最终选择 Rspack 作为尝试。

基本的 Rspack 配置

安装依赖:

pnpm install @rspack/cli @rspack/core vue-loader -D

package.json 添加 scripts:

{
  "type": "module",
  "scripts": {
    "dev": "NODE_ENV=development rspack build --watch",
    "build": "NODE_ENV=production rspack build"
  }
}

rspack.config.mjs:

import { defineConfig } from '@rspack/cli';
import path from 'path';
import process from 'process';
import { VueLoaderPlugin } from 'vue-loader';
import { fileURLToPath } from 'url';

// plugin.yaml 中的 metadata.name
const PLUGIN_NAME = '<YOUR_PLUGIN_NAME>';

const isProduction = process.env.NODE_ENV === 'production';
const dirname = path.dirname(fileURLToPath(import.meta.url));

// 开发环境启动直接输出到插件项目的 build 目录,无需重启整个插件
// 生产环境输出到插件项目的 src/main/resources/console 目录下
const outDir = isProduction ? '../src/main/resources/console' : '../build/resources/main/console';

export default defineConfig({
  mode: process.env.NODE_ENV,
  entry: {
    // 入口文件,可以参考:https://docs.halo.run/developer-guide/plugin/basics/ui/entry
    main: './src/index.ts',
  },
  plugins: [new VueLoaderPlugin()],
  resolve: {
    alias: {
      '@': path.resolve(dirname, 'src'),
    },
    extensions: ['.ts', '.js'],
  },
  output: {
    // 资源根路径,加载代码分块(Chunk)的时候,会根据这个路径去加载资源
    publicPath: `/plugins/${PLUGIN_NAME}/assets/console/`,
    chunkFilename: '[id]-[hash:8].js',
    cssFilename: 'style.css',
    path: path.resolve(outDir),
    library: {
      // 将对象挂载到 window 上
      type: 'window',
      export: 'default',
      name: PLUGIN_NAME,
    },
    clean: true,
    iife: true,
  },
  optimization: {
    providedExports: false,
    realContentHash: true,
  },
  experiments: {
    css: true,
  },
  devtool: false,
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: [/node_modules/],
        loader: 'builtin:swc-loader',
        options: {
          jsc: {
            parser: {
              syntax: 'typescript',
            },
          },
        },
        type: 'javascript/auto',
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          experimentalInlineMatchResource: true,
        },
      },
    ],
  },
  // 这部分依赖已经由 Halo 提供,所以需要标记为外部依赖
  externals: {
    vue: 'Vue',
    'vue-router': 'VueRouter',
    '@vueuse/core': 'VueUse',
    '@vueuse/components': 'VueUse',
    '@vueuse/router': 'VueUse',
    '@halo-dev/console-shared': 'HaloConsoleShared',
    '@halo-dev/components': 'HaloComponents',
    '@halo-dev/api-client': 'HaloApiClient',
    '@halo-dev/richtext-editor': 'RichTextEditor',
    axios: 'axios',
  },
});

配置需要懒加载的路由或者组件:

在 index.ts 中配置路由:

import { definePlugin } from '@halo-dev/console-shared';
import { defineAsyncComponent } from 'vue';
import { VLoading } from '@halo-dev/components';
import 'uno.css';
// [!code --]
import DemoPage from './views/DemoPage.vue';

export default definePlugin({
  routes: [
    {
      parentName: 'Root',
      route: {
        path: 'demo',
        name: 'DemoPage',
// [!code --]
        component: DemoPage,
// [!code ++:4]
        component: defineAsyncComponent({
          loader: () => import('./views/DemoPage.vue'),
          loadingComponent: VLoading,
        }),
      ...
      },
    },
  ],
  extensionPoints: {},
});

注:推荐使用 defineAsyncComponent 包裹,而不是直接使用 () => import() 的方式,后者会在进入路由之前就开始加载页面的代码分块(Chunk),导致页面在加载期间没有任何响应。

构建产物示例:

❯ ll src/main/resources/console
.rw-r--r-- 191k ryanwang staff 16 Jun 10:47  359-3bebb968.js
.rw-r--r--  83k ryanwang staff 16 Jun 10:47  962-3bebb968.js
.rw-r--r-- 4.1k ryanwang staff 16 Jun 10:47  main.js

其他配置

集成 Scss / Sass

安装依赖:

pnpm install sass-embedded sass-loader -D

rspack.config.mjs 添加配置:

import { defineConfig } from '@rspack/cli';
import path from 'path';
import process from 'process';
import { VueLoaderPlugin } from 'vue-loader';
import { fileURLToPath } from 'url';
// [!code ++]
import * as sassEmbedded from "sass-embedded";

...

export default defineConfig({
  ...
  module: {
    rules: [
      ...
// [!code ++:13]
      {
        test: /\.(sass|scss)$/,
        use: [
          {
            loader: "sass-loader",
            options: {
              api: "modern-compiler",
              implementation: sassEmbedded,
            },
         },
        ],
        type: "css/auto",
      },
    ],
  },
...
});

集成 UnoCSS

如果你习惯使用 TailwindCSS 或者 UnoCSS 来编写样式,可以参考以下配置:

本文推荐使用 https://unocss.dev/,因为可以利用 UnoCSS 的 https://unocss.dev/transformers/compile-class 来编译样式,预防与 Halo 或者其他插件产生样式冲突。

安装依赖:

pnpm install unocss @unocss/webpack @unocss/eslint-config style-loader css-loader -D

入口文件(src/index.ts)添加导入:

import 'uno.css';

rspack.config.mjs 添加配置:

import { defineConfig } from '@rspack/cli';
import path from 'path';
import process from 'process';
import { VueLoaderPlugin } from 'vue-loader';
import { fileURLToPath } from 'url';
// [!code ++]
import { UnoCSSRspackPlugin } from '@unocss/webpack/rspack';

...

export default defineConfig({
  ...
  plugins: [
    new VueLoaderPlugin(),
// [!code ++]
    UnoCSSRspackPlugin()
  ],
  ...
  module: {
    rules: [
      ...
// [!code ++:5]
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
        type: 'javascript/auto',
      },
    ],
  },
...
});

uno.config.ts:

import { defineConfig, presetWind3, transformerCompileClass } from 'unocss';

export default defineConfig({
  presets: [presetWind3()],
  transformers: [transformerCompileClass()],
});

.eslintrc.cjs:

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-recommended',
    'eslint:recommended',
    '@vue/eslint-config-typescript/recommended',
    '@vue/eslint-config-prettier',
// [!code ++]
    '@unocss',
  ],
  env: {
    'vue/setup-compiler-macros': true,
  },
// [!code ++:3]
  rules: {
    "@unocss/enforce-class-compile": 1,
  },
};

总结

以上就是针对 Halo 插件前端部分的 Rspack 配置。我已经对 Halo 官方维护的部分插件进行了迁移,几乎没有遇到什么问题,并且带来的收益非常明显:www.halo.run 和本博客的 bundle.js 在 gzip 之后仅有不到 200k,各个页面也只会在访问时加载所需的资源。

需要注意的是,我对这些构建工具并不算非常熟悉,所以配置仍然有优化空间。我们会持续优化,后续也会考虑提供一个通用的 CLI 或 Rspack 配置,期望实现如下效果:

rspack.config.mjs:

import { rspackConfig } from '@halo-dev/ui-bundler-kit';

export default rspackConfig({
  ...
});

或者基于 Rspack 包装一个 CLI:

plugin.config.mjs:

import { defineConfig } from '@halo-dev/ui-bundler-kit';

export default defineConfig({
  ...
});

package.json:

{
  "scripts": {
    "dev": "halo-ui dev",
    "build": "halo-ui build"
  }
}

参考文档

感谢阅读,欢迎交流与指正!

Manjaro Gnome 安装和配置记录

By: Ryan Wang
28 July 2021 at 13:10
╭─ryanwang at ryanwang-linux in ~
╰─○ neofetch
██████████████████  ████████   ryanwang@ryanwang-linux 
██████████████████  ████████   ----------------------- 
██████████████████  ████████   OS: Manjaro Linux x86_64 
██████████████████  ████████   Kernel: 5.10.70-1-MANJARO 
████████            ████████   Uptime: 10 hours, 29 mins 
████████  ████████  ████████   Packages: 1394 (pacman) 
████████  ████████  ████████   Shell: zsh 5.8 
████████  ████████  ████████   Resolution: 2560x1440 
████████  ████████  ████████   DE: GNOME 40.5 
████████  ████████  ████████   WM: Mutter 
████████  ████████  ████████   WM Theme: Adwaita-maia-compact-dark 
████████  ████████  ████████   Theme: Adwaita-maia-compact-dark [GTK2/3] 
████████  ████████  ████████   Icons: Papirus-Dark-Maia [GTK2/3] 
████████  ████████  ████████   Terminal: gnome-terminal 
                               CPU: Intel i5-10400 (12) @ 4.300GHz 
                               GPU: Intel CometLake-S GT2 [UHD Graphics 630] 
                               Memory: 12983MiB / 15424MiB

基础设定

检测并更换软件源:

sudo pacman-mirrors -i -c China -m rank

添加 archlinuxcn 的源:

sudo vim /etc/pacman.conf

追加如下配置:

[archlinuxcn]
SigLevel = Optional TrustedOnly
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch
sudo pacman -S archlinuxcn-keyring

检查软件包更新:

sudo pacman -Syyu

常用开发工具包:

sudo pacman -S base-devel

常用软件包

常用终端工具:

sudo pacman -S htop vim tree neofetch

社区应用:

yay -S bitwarden spotify visual-studio-code-bin google-chrome github-desktop-bin com.qq.weixin.work.deepin com.qq.weixin.deepin typora telegram-desktop

输入法

最开始使用的是 fcitx5 + fcitx5-rime ,安装下来一切顺利。但是在某些场景下始终无法切换到中文输入法,不知道如何解决,然后换成了 ibus-rime。

sudo pacman -S ibus-rime
sudo vim /etc/profile.d/ibus.sh

写入:

export GTK_IM_MODULE="ibus"
export QT_IM_MODULE="ibus"
export XMODIFIERS="@im=ibus"

export XIM="ibus"
export XIM_PROGRAM="ibus"

按理来说,这时候重启或者重新登录即可生效,但是并没有,不仅无法通过快捷键(Super+Space)切换到中文输入法,而且菜单栏右侧也没有切换输入法的菜单项。后来在 @JohnNiang 的帮助下解决了此问题。解决方法:在系统设置里面添加 rime 输入法。如下图:

manjaro-keyboard-settings

rime 的配置目录位置:~/.config/ibus/rime/build

可根据自己的需求对 rime 进行定制化配置。

Terminal 配置

目前最新的 Manjaro 发行版已经默认配置了 zsh,但个人还是希望使用 ohmyzsh。和家里的 macOS 保持一致。

安装 ohmyzsh:

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

安装所需插件:

cd ~/.oh-my-zsh/plugins

git clone https://github.com/zsh-users/zsh-syntax-highlighting

git clone https://github.com/zsh-users/zsh-autosuggestions

修改 .zshrc

ZSH_THEME="fino"

...

plugins=(
	git
	zsh-autosuggestions
	zsh-syntax-highlighting
)

Node.js 环境配置

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash

追加下面的配置到 .zshrc

export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

安装一个 Node.js 版本

nvm install v14.17.3

Clash

yay -S clash

配置目录位置:~/.config/clash

将 clash 配置文件(config.yaml)放置到此目录,然后启动即可:

nohup clash &

Gnome 插件

https://extensions.gnome.org/extension/2890/tray-icons-reloaded/https://extensions.gnome.org/extension/1460/vitals/

总结

使用了两三天下来整体还是不错的,暂时还没遇到什么痛点,可能问题最大的还是在中文输入法的体验上吧。之前一直在开发环境使用 Deepin,在使用上没有什么大的问题,但感觉系统整体在 UI 和交互上对于我来说存在较大问题。Deepin 就是属于那种远看界面还不错,但是近看却经不起推敲。个人不是特别建议。而且对于一个小前端开发来说,目前所需要的软件包都可以在官方源或者社区源(aur)找到。所以目前来说,Manjaro 应该是除了 macOS,我最佳的选择。

参考

记录优化 Vue 应用的首次加载速度

By: Ryan Wang
13 January 2020 at 12:32

2024 年更新

不再推荐用这种偏门的方式去优化网络加载,这非常不可靠。Halo 2.x 也已经完全没有使用这种方式。正确的做法应该是在工程和代码层面优化,比如分包、异步加载路由、缓存策略、SSR 等。一定要上 CDN,也应该是从基础设施运维上面考虑。

感谢评论区的 @Yttrium 提醒更新。


Halo 的管理端使用的是 Vue 来构建的,随之收到不少反馈后台加载过于缓慢,其主要原因就是打包好的 Vue 应用的静态资源又多又大,可能同时就几十个请求,这对于一些小水管的服务器来说简直是致命的打击。

2e4f1d9d99910ce0933230d0c79d309c

解决办法

使用公共 cdn,我们只需要把打包好的静态资源丢在公共 cdn 上引用就好了。那么如何优雅的上传到公共 cdn 呢?

创建 .env.env.development

.env

NODE_ENV=production
// VERSION 需要和 package.json 的 version 一致。
PUBLIC_PATH=https://cdn.jsdelivr.net/npm/halo-admin@VERSION/dist/

.env.development

NODE_ENV=development
PUBLIC_PATH=/

修改 vue.config.js

module.exports = {
	publicPath: process.env.PUBLIC_PATH,
}

打包测试

可以在 dist/index.html 看到,所有静态资源的根路径已经变成了 https://cdn.jsdelivr.net/npm/halo-admin@VERSION/dist/

上传到 npmjs

npm login

npm init

npm publish

说明

  1. 每次发布版本前,需要修改 .envpackage.json 的版本号,且需要保持一致。

  2. 发布到 npmjs 前,需要先 npm run build

  3. 经过这些操作之后,只需要部署 dist/index.html 即可,其他静态资源无用,因为是走的 jsdelivr 的 cdn。

  4. 只建议个人小应用使用这种方式,其他类型应用请自行斟酌,毕竟上传到 npmjs 会上传项目代码。

  5. 过程描述的过于简单,仅做为记录,非教程。

相关链接

博客迁移到 Raspberry Pi

By: Ryan Wang
23 December 2019 at 19:40

注:经过一段时间的折腾,已经放弃运行到树莓派了,其主要原因是因为家里网络质量一言难尽。

本着爱折腾之心,今天将博客搬到了吃灰已久的 Raspberry Pi 3B+。好吧,其实也是之前一个热心的网友送了我一年哲西云的内网穿透,一直没用,所以秉承着中华人民拒绝浪费的美好传统,折腾开始了…

配置

    .',;:cc;,'.    .,;::c:,,.    root@raspberrypi
   ,ooolcloooo:  'oooooccloo:    OS: Raspbian 10 buster
   .looooc;;:ol  :oc;;:ooooo'    Kernel: armv7l Linux 4.19.75-v7+
     ;oooooo:      ,ooooooc.     Uptime: 4h 4m
       .,:;'.       .;:;'.       Packages: 588
       .... ..'''''. ....        Shell: 5295
     .''.   ..'''''.  ..''.      CPU: ARMv7 rev 4 (v7l) @ 1.4GHz
     ..  .....    .....  ..      RAM: 386MiB / 926MiB
    .  .'''''''  .''''''.  .
  .'' .''''''''  .'''''''. ''.
  '''  '''''''    .''''''  '''
  .'    ........... ...    .'.
    ....    ''''''''.   .''.
    '''''.  ''''''''. .'''''
     '''''.  .'''''. .'''''.
      ..''.     .    .''..
            .'''''''
             ......

也就那样儿吧~

烧录镜像

这次搭建博客使用的镜像是官方的 Raspbian Buster Lite,之所以没选 Desktop,因为那玩意儿就是个玩具,没有实际用途,鹅且也不会用它,倒不如节省点内存。

进入官网 https://www.raspberrypi.org/downloads/raspbian ,找到 Raspbian Buster Lite,选择 Download ZIP

解压下载好的镜像得到 xxxx-xx-xx-raspbian-buster-lite.img

使用 Etcher 烧录镜像。当然,也有很多其他的烧录工具。

开启 SSH

这一步很简单,在烧录好的 SD 卡中,新建一个空白的 ssh 文件即可,需要注意的是,这个文件没有后缀,别搞个 ssh.txtssh.avi 啥的。

启动

插上 SD 和电源直接启动即可,默认用户名 pi,默认密码 raspberry,切换到 root 账户,sudo su root

环境配置

Java

sudo apt-get install openjdk-8-jre-headless
sudo apt-get install openjdk-8-jre

Vim

sudo apt-get install vim

Git

sudo apt-get install git

迁移数据

下载安装包

wget http://halo.ryanc.cc/release/halo-latest.jar -O halo-latest.jar

拉取备份的数据

git clone git@github.com:ruibaby/blog-data.git .halo

启动 Halo

java -jar halo-latest.jar

测试没问题,再配置 systemd 进行管理,教程:https://halo.run/guide/install/install-with-linux.html#%E8%BF%9B%E9%98%B6%E9%85%8D%E7%BD%AE

域名解析

由于我是直接是用的 又拍云 CDN 进行回源,所以也不需要安装 Nginx 啥的了,这个内网穿透服务提供了一个 CNAME,去解析一下就完事了。

哲西云

如有需要,可以去 http://www.zhexi.tech 体验体验。优惠码 GRRVFM

展示

56ffb0f522663785f4b031d595c1f6a0

d15419a817ec66336297bd8e08157508

758cfbd207c38b9db27a6bba295618a2

9e22bcea8ac55cd0c8a82d2c49b083c8

87e60eb23320ee7c00fb5ed66942f631

Flarum 的安装与配置

By: Ryan Wang
18 November 2019 at 20:30

Flarum 是一款非常棒的开源论坛程序,Halo 的论坛 就是用 Flarum 搭建的。之前有人问过我 Flarum 如何搭建,所以下面讲一下 Flarum 的搭建过程(btw,官方的搭建教程实在草率)。

前提

  • 域名需要提前解析。

  • 注意服务器是否需要备案,如果没备案,会被 x 掉。

  • 有一定的 Linux 基础。

环境说明

  • Linux Server(本文是用的 CentOS 7.6)

  • Apache 或者 Nginx(本文是用的 Nginx)

  • PHP 7.1+

  • PHP 拓展: curl, dom, gd, json, mbstring, openssl, pdo_mysql, tokenizer, zip, fileinfo

  • MySQL 5.6+ 或者 MariaDB 10.0.5+

部署环境安装

更新服务器软件包

yum update -y

安装 Nginx/PHP/MySQL

这里我们使用 OneinStack 一键安装,人生苦短,懒得自己编译了。当然,如果有时间,根据需求自己编译安装更好。

2024 年更新,OneinStack 被曝包含恶意代码,请谨慎使用。
https://github.com/oneinstack/oneinstack/issues/511

image.png

如上图,选择好需要的软件以及版本后,复制安装命令到服务器执行就行了,安装过程可能会有点慢,耐心等待就行了。

需要注意的是,PHP 扩展中的 fileinfo 一定要勾选,Flarum 官方文档居然没有写需要这个扩展。(没错,这里我被坑了,嘤嘤嘤嘤~)

安装完成应该会打印出这些东西:
image.png

安装 Composer

php -r "copy('https://install.phpcomposer.com/installer', 'composer-setup.php');"

php composer-setup.php

php -r "unlink('composer-setup.php');"

mv composer.phar /usr/local/bin/composer

由于 Composer 的服务器在国外,可能导致下载 Flarum 已经依赖包会很慢,所以我们需要更换一下源地址。至于 Composer 是啥,其实就是 PHP 的一个包管理,类似 Java 的 MavenGradle 工具。

composer config -g repo.packagist composer https://packagist.phpcomposer.com

安装 Flarum

进入到 oneinstack 目录,执行 vhost.sh 脚本新建一个网站
image.png

然后会提示 SSL 证书选项,网站目录之类的东西,按照自己的需求选择即可。

创建完成后应该是这样子。

image.png

然后进入网站目录执行:

composer create-project flarum/flarum . --stability=beta

更新,现在 Flarum 已经发布正式版,请使用下面的命令安装:

composer create-project flarum/flarum .

然后等待下载 Flarum 以及对应的依赖即可,安装完成应该是这个样子的:

image.png

配置运行

上面其实就已经安装好了 Flarum,但是还需要进一步配置才能正确运行。

创建数据库

登陆 MySQL:

mysql -u root -p密码

创建数据库:

create database 数据库名 character set utf8mb4 collate utf8mb4_bin;

这里的字符集一定要是 utf8mb4,至于为什么是 utf8mb4,参考:https://www.jianshu.com/p/6967ce16a202

修改 Nginx 配置

进入 Nginx 配置文件目录:

cd /usr/local/nginx/conf/vhost

修改网站的配置文件:

vim xxx.conf

需要修改的地方:

  1. root:需要在路径后面加上 public,比如我的原本是 root /data/wwwroot/bbs.ryanwang.me;,需要修改为 root /data/wwwroot/bbs.ryanwang.me/public;

  2. 引入 Flarum 提供的配置,在 server 大括号中任意位置加上 include /data/wwwroot/xxx/.nginx.conf;xxx 为网站目录名。比如我的是 include /data/wwwroot/bbs.ryanwang.me/.nginx.conf;

最后的配置示例:

server {
  listen 80;
  listen 443 ssl http2;
  ssl_certificate /usr/local/nginx/conf/ssl/bbs.ryanwang.me.crt;
  ssl_certificate_key /usr/local/nginx/conf/ssl/bbs.ryanwang.me.key;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
  ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
  ssl_prefer_server_ciphers on;
  ssl_session_timeout 10m;
  ssl_session_cache builtin:1000 shared:SSL:10m;
  ssl_buffer_size 1400;
  add_header Strict-Transport-Security max-age=15768000;
  ssl_stapling on;
  ssl_stapling_verify on;
  server_name bbs.ryanwang.me;
  access_log /data/wwwlogs/bbs.ryanwang.me_nginx.log combined;
  index index.html index.htm index.php;
  root /data/wwwroot/bbs.ryanwang.me/public;
  if ($ssl_protocol = "") { return 301 https://$host$request_uri; }

  include /usr/local/nginx/conf/rewrite/other.conf;
  #error_page 404 /404.html;
  #error_page 502 /502.html;

  location ~ [^/]\.php(/|$) {
    #fastcgi_pass remote_php_ip:9000;
    fastcgi_pass unix:/dev/shm/php-cgi.sock;
    fastcgi_index index.php;
    include fastcgi.conf;
  }

  location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
    expires 30d;
    access_log off;
  }
  location ~ .*\.(js|css)?$ {
    expires 7d;
    access_log off;
  }
  location ~ /(\.user\.ini|\.ht|\.git|\.svn|\.project|LICENSE|README\.md) {
    deny all;
  }
  include /data/wwwroot/bbs.ryanwang.me/.nginx.conf;
}

最后我们需要检查 Nginx 配置是否有误并重载 Nginx 配置:

nginx -t

nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
nginx -s reload

Flarum 安装引导

如果出现下面的情况:

image

是因为没有对网站目录写入的权限,我们加一下权限即可:

# xxx 为网站目录名称
chmod -R 777 /data/wwwroot/xxx

然后刷新页面就可以看到安装表单了。

image.png

然后填写数据库信息以及管理员信息,点击安装即可。

安装部署部分到此结束。

常用插件安装

安装完成后会发现不支持中文,所以我们需要安装中文语言包。还有一些常用的插件。

进入网站目录:

# xxx 为网站目录名称
cd /data/wwwroot/xxx
# 简体中文语言包
composer require csineneo/lang-simplified-chinese

# 繁体中文语言包
composer require csineneo/lang-traditional-chinese

# 编辑器 Emoji 表情选择框
composer require clarkwinkelmann/flarum-ext-emojionearea

# Sitemap 生成器
composer require flagrow/sitemap

# Fancybox 插件
composer require squeevee/flarum-ext-fancybox 

安装完成后去后台启用即可(后台地址:网址/admin)。

image.png

❌
❌