现代 Web 开发已经变得极其强大,但也越来越复杂。

许多项目在写第一个页面之前就安装了数百兆字节的依赖。框架、打包工具、路由器、模板系统、CSS 工具和运行时库都解决了重要问题,但同时也引入了额外的复杂性。

我想要一种不同的方式。

我希望从普通的 HTML 开始构建网站,同时仍能提供开发者如今期望的特性:

  • 可复用组件
  • Markdown 支持
  • TypeScript
  • CSS 和 JavaScript 打包
  • 国际化 (i18n)
  • 内容集合
  • RSS/Atom 订阅源
  • 站点地图生成
  • 快速开发构建

这个想法最终变成了 Rino.js。

什么是 Rino.js?

Rino.js 是一款以 HTML 为优先的网站编译器,用于构建静态网站、文档、博客、作品集、企业网站以及其他内容驱动的项目。

Rino.js 没有引入自定义模板语言,也没有要求使用前端框架,而是将 HTML 作为主要语言。页面保持为有效的 HTML,同时通过少量构建时约定添加额外功能。

目标很简单:

编写 HTML,生成优化后的静态网站。

为什么选择 HTML First?

HTML 已经存在数十年,但现代 Web 开发通常将其视为由其他语言生成的内容。

Rino.js 采取了相反的做法。

无需在 JSX 或其他模板语言中编写组件,组件本身就是 HTML 文件。

<component rino-import="header"></component>

Enter fullscreen mode Exit fullscreen mode

仅此而已。

编译器会在构建过程中替换该组件,生成不含运行时依赖的纯静态 HTML。

开始使用 Rino.js

Rino.js 提供了一条命令,用于创建默认项目。

npm create rino@latest

Enter fullscreen mode Exit fullscreen mode

项目结构

一个典型的 Rino.js 项目通常如下所示:

my-site/
  rino-config.js
  dev.js
  generate.js
  feed.js
  sitemap.js
  backoffice.js
  pages/
    index.html
    about.html
  components/
    header.html
    footer.html
  public/
    images/
      photo.webp
  scripts/
    export/
      app.js
      dashboard.ts
  styles/
    export/
      site.css
  mds/
    intro.md
  contents/
    en/
      blog/
        1-first-post.md
  content-theme/
    en/
      content.html
      content-list.html
  i18n/
    en/
      index.json
    ko/
      index.json

Enter fullscreen mode Exit fullscreen mode

只需包含你需要的文件夹。对于小型静态站点,pages/components/public/scripts/export/styles/export/ 可能就足够了。

配置

项目配置位于 rino-config.js

export default {
  dist: "./dist",
  port: 3000,
  site: {
    url: "https://example.com",
  },
  sitemap: ["https://example.com/custom-page"],
  i18n: {
    defaultLocale: "en",
    locales: ["en", "ko"],
  },
};

Enter fullscreen mode Exit fullscreen mode

dist 控制输出文件夹,port 控制开发服务器,site.url 用于站点地图和订阅源,sitemap 添加额外 URL,i18n 控制本地化页面生成。

页面

pages/ 中的文件会成为输出中的 HTML 文件。

pages/index.html        -> dist/index.html
pages/about.html        -> dist/about.html
pages/blog/index.html   -> dist/blog/index.html

Enter fullscreen mode Exit fullscreen mode

页面是普通的 HTML,可包含可选的 Rino 语法:

<!doctype html>
<html>
  <head>
    <title><lang>site.title</lang></title>
    <link rel="stylesheet" href="/styles/site.css" />
    <script src="/scripts/app.js"></script>
  </head>
  <body>
    <component rino-import="header"></component>

    <main>
      <h1><lang>home.heading</lang></h1>
      <script rino-type="md" rino-import="intro" rino-tag="section"></script>
    </main>

    <component rino-import="footer" rino-tag="footer"></component>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

组件

组件位于 components/ 中。使用 <component> 可在页面或其他组件中渲染它。

<component rino-import="header"></component>

Enter fullscreen mode Exit fullscreen mode

这会加载:

components/header.html

Enter fullscreen mode Exit fullscreen mode

支持嵌套组件路径:

<component rino-import="layout/sidebar"></component>

Enter fullscreen mode Exit fullscreen mode

这会加载:

components/layout/sidebar.html

Enter fullscreen mode Exit fullscreen mode

当你想将组件输出包裹在一个元素中并向该包裹元素传递属性时,请使用 rino-tag

<component
  rino-import="button"
  rino-tag="button"
  type="button"
  class="button"
  onclick="alert('Hello')"
></component>

Enter fullscreen mode Exit fullscreen mode

如果 components/button.html 包含:

Click me

Enter fullscreen mode Exit fullscreen mode

生成的 HTML 将变为:

<button type="button" class="button" onclick="alert('Hello')">Click me</button>

Enter fullscreen mode Exit fullscreen mode

在版本 3 中,请使用 rino-import。旧的 rino-path 语法不再受支持。

Markdown

Rino.js 可以渲染来自 mds/ 的 Markdown 片段。

mds/intro.md

Enter fullscreen mode Exit fullscreen mode

<script rino-type="md" rino-import="intro" rino-tag="section" class="intro"></script>

Enter fullscreen mode Exit fullscreen mode

这会在 <section> 内渲染 mds/intro.md

你也可以内联编写 Markdown:

<script rino-type="markdown" rino-tag="article" type="text/markdown">
  ## Hello from Markdown

  This content is rendered during the build.
</script>

Enter fullscreen mode Exit fullscreen mode

支持的 Markdown 类型有 mdmarkdown

模板脚本

模板脚本在生成期间运行。它们通过 console.log 将 HTML 输出到生成的页面中。

JavaScript 模板脚本:

<script rino-type="js" type="text/javascript">
  console.log("<ul>");
  for (const item of ["Home", "Blog", "About"]) {
    console.log(`<li>${item}</li>`);
  }
  console.log("</ul>");
</script>

Enter fullscreen mode Exit fullscreen mode

TypeScript 模板脚本:

<script rino-type="ts" type="text/typescript">
  const message: string = "Generated with TypeScript";
  console.log(`<p>${message}</p>`);
</script>

Enter fullscreen mode Exit fullscreen mode

模板脚本可以使用 Node.js 导入:

<script rino-type="js" type="text/javascript">
  import os from "os";
  console.log(`<p>Built on ${os.type()}</p>`);
</script>

Enter fullscreen mode Exit fullscreen mode

在版本 3 中,模板执行速度更快,因为 Rino.js 会复用一个隔离的子进程运行器,而不是为每个模板脚本标签创建新进程。

浏览器脚本

浏览器脚本是普通的浏览器 JavaScript。请将源文件放在 scripts/export/ 中。

scripts/export/app.js -> dist/scripts/app.js
scripts/export/app.ts -> dist/scripts/app.js

Enter fullscreen mode Exit fullscreen mode

从 HTML 中引用生成的文件:

<script src="/scripts/app.js"></script>

Enter fullscreen mode Exit fullscreen mode

JavaScript 示例:

document.querySelector("button")?.addEventListener("click", () => {
  console.log("Clicked");
});

Enter fullscreen mode Exit fullscreen mode

TypeScript 示例:

const button = document.querySelector<HTMLButtonElement>("button");

button?.addEventListener("click", () => {
  console.log("Clicked with TypeScript");
});

Enter fullscreen mode Exit fullscreen mode

Rino.js 会打包脚本并压缩输出。压缩有助于减小文件体积。

CSS

将 CSS 文件放在 styles/export/ 中。

styles/export/site.css -> dist/styles/site.css

Enter fullscreen mode Exit fullscreen mode

@import "../theme.css";

body {
  font-family: system-ui, sans-serif;
}

Enter fullscreen mode Exit fullscreen mode

引用生成的 CSS:

<link rel="stylesheet" href="/styles/site.css" />

Enter fullscreen mode Exit fullscreen mode

Rino.js 会解析本地 CSS 导入、跳过循环导入、跳过 URL 导入,并压缩 CSS 输出。

公共资源

public/ 中的文件会被复制到输出根目录。

public/favicon.ico       -> dist/favicon.ico
public/images/photo.webp -> dist/images/photo.webp

Enter fullscreen mode Exit fullscreen mode

使用根相对路径:

<img src="/images/photo.webp" alt="Photo" />
<link rel="icon" href="/favicon.ico" />

Enter fullscreen mode Exit fullscreen mode

内联样式、JavaScript 和 TypeScript 导出

rino-export 允许页面或组件将相关的 CSS 和 JavaScript 保持在 HTML 附近,同时仍生成外部文件。

导出的标签会从最终 HTML 中移除,并写入生成的资源。

CSS 导出

<section class="callout">
  <h2>Hello</h2>
</section>

<style rino-export="/site.css">
  .callout {
    border: 1px solid #ddd;
    padding: 1rem;
  }
</style>

Enter fullscreen mode Exit fullscreen mode

这会写入:

dist/styles/site.css

Enter fullscreen mode Exit fullscreen mode

JavaScript 导出

<script rino-export="/site.js">
  console.log("Exported browser script");

  window.openMenu = function () {
    document.body.classList.toggle("menu-open");
  };
</script>

Enter fullscreen mode Exit fullscreen mode

这会写入:

dist/scripts/site.js

Enter fullscreen mode Exit fullscreen mode

TypeScript 导出

<script rino-export="/site.ts" type="text/typescript">
  const message: string = "Exported TypeScript";
  console.log(message);
</script>

Enter fullscreen mode Exit fullscreen mode

这会写入 JavaScript:

dist/scripts/site.js

Enter fullscreen mode Exit fullscreen mode

如果 bundle 之外的代码需要调用函数,请将其附加到 window

<script rino-export="/site.ts" type="text/typescript">
  (window as any).testExportTypescript = function (): void {
    console.log("Called from outside");
  };
</script>

Enter fullscreen mode Exit fullscreen mode

然后从 HTML 中调用它:

<button onclick="window.testExportTypescript()">Run</button>

Enter fullscreen mode Exit fullscreen mode

多个标签可以导出到同一文件。Rino.js 会追加唯一的块,并忽略完全相同的块。

i18n

在页面和组件中使用 <lang> 标签:

<h1><lang>name</lang></h1>
<p><lang>body.hero.copy</lang></p>
<p><lang>items[0].title</lang></p>

Enter fullscreen mode Exit fullscreen mode

i18n/{locale}/ 中创建匹配的 JSON 文件。

pages/index.html
i18n/en/index.json
i18n/ko/index.json

Enter fullscreen mode Exit fullscreen mode

i18n/en/index.json 示例:

{
  "name": "Rino.js",
  "body": {
    "hero": {
      "copy": "Simple static websites from HTML."
    }
  },
  "items": [
    {
      "title": "First item"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

默认语言环境会在普通页面路径下生成。其他语言环境会生成在各自的语言前缀下,例如 /ko/index.html

缺失的键会保留在 HTML 中,而不会中断构建。若要保留字面的语言标签,请转义它:

\<lang>name\</lang>

Enter fullscreen mode Exit fullscreen mode

内容集合

Rino.js 可以构建 Markdown 内容集合,例如博客。

contents/en/blog/1-first-post.md

Enter fullscreen mode Exit fullscreen mode

Markdown 内容可以在 HTML 注释中以 JSON 元数据开头:

<!--
{
  "title": "Welcome to the Blog",
  "time": "2026-07-29T00:00:00.000Z",
  "description": "The first post on our Rino.js blog."
}
-->

# Hello World

This is the first post.

Enter fullscreen mode Exit fullscreen mode

这会生成:

dist/contents/en/blog/1-first-post.html

Enter fullscreen mode Exit fullscreen mode

匹配的模板位于:

content-theme/en/content.html

Enter fullscreen mode Exit fullscreen mode

内容模板示例:

<!doctype html>
<html lang="en">
  <head>
    <title>{{ content.title }}</title>
  </head>
  <body>
    <main>
      <h1>{{ content.title }}</h1>
      <p>{{ content.time }}</p>
      <article>{{ content.body }}</article>
    </main>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

你可以访问元数据和生成的字段:

{{ content.title }}
{{ content.description }}
{{ content.body }}
{{ content.urlPath }}
{{ content.nearby[0].title }}

Enter fullscreen mode Exit fullscreen mode

对于循环,请使用模板脚本:

<script rino-type="js" type="text/javascript">
  const args = process.argv;
  const content = JSON.parse(args[args.length - 1]);

  console.log("<ul>");
  for (const post of content.nearby || []) {
    console.log(`<li><a href="${post.link}">${post.title}</a></li>`);
  }
  console.log("</ul>");
</script>

Enter fullscreen mode Exit fullscreen mode

内容列表

内容列表模板与内容模板相邻:

content-theme/en/content-list.html

Enter fullscreen mode Exit fullscreen mode

Rino.js 会为每个分类创建分页列表页:

dist/contents-list/en/blog/blog-1.html

Enter fullscreen mode Exit fullscreen mode

示例:

<ol>
  <script rino-type="js" type="text/javascript">
    const args = process.argv;
    const data = JSON.parse(args[args.length - 1]);

    for (const item of data.contentList) {
      console.log(`<li><a href="${item.link}">${item.title}</a></li>`);
    }
  </script>
</ol>

Enter fullscreen mode Exit fullscreen mode

可用数据包括:

{{ contentList[0].title }}
{{ pagination.prevLink }}
{{ pagination.nextLink }}

Enter fullscreen mode Exit fullscreen mode

通常,模板脚本是渲染数组的更好选择。

站点地图和订阅源

Rino.js 可以从页面、本地化页面、内容页面和额外配置 URL 生成站点地图。

import { generateProjectSitemapFile } from "rinojs";
import config from "./rino-config.js";

await generateProjectSitemapFile(process.cwd(), config);

Enter fullscreen mode Exit fullscreen mode

它还可以从 contents/ 生成 RSS 和 Atom 订阅源。

import { generateProjectFeedFiles } from "rinojs";
import config from "./rino-config.js";

await generateProjectFeedFiles(process.cwd(), config);

Enter fullscreen mode Exit fullscreen mode

典型输出:

dist/sitemap.xml
dist/rss.xml
dist/atom.xml
dist/rss-en.xml
dist/atom-en.xml

Enter fullscreen mode Exit fullscreen mode

我的目标

我不想再构建另一个前端框架,而是想探索纯 HTML 在轻量级编译器的帮助下能走多远。

最终的结果是一个以 HTML 为核心的工作流,同时在需要时添加实用的构建时功能。

版本 3 是一个重要的里程碑,但这只是开始。未来版本还有很多想法,我期待基于真实使用情况继续改进这个项目。