Skip to content

以 Cordis 为插件宿主的前端全栈框架

一份 cormi.yml,
就是整个应用。

路由、HTML、打包配置、服务端和浏览器运行时,都由插件在 ctx 上注册。修改一个插件的配置,只有它会重载;卸载一个插件,它注册过的一切随之撤回。

cormi.yml
ctx 上的注册
ctx.router
  • .extension('.tsx', '.jsx')react
  • .layout('plugin-layout/Layout.tsx')plugin-layout
ctx.html
  • .head(<meta name="generator">)./plugin.ts
ctx.vite
  • .plugin(react())react
  • .plugin(tailwindcss())plugin-tailwindcss
ctx.app
  • .entry('src/app.tsx')preset
  • .file('entry.tsx')react
  • .entry('@cormijs/plugin-layout/client')plugin-layout
  • .import('tailwindcss')plugin-tailwindcss

› 点击 cormi.yml 中的条目,停用或启用一个插件。

这是 dev 模式下修改 cormi.yml 时发生的事:停用一个条目,loader 写入 disabled: true 并卸载该插件。停用 @cormijs/vite 试试:依赖 ctx.vite 的 React 渲染器与 Tailwind 转入 pending,直到打包器回来——换一个打包器,也只是换掉这一行。

写插件,就是写注册

声明依赖,在 ctx 上注册,清理交给 cordis。应用自己的代码和官方插件用的是同一套写法。

plugin.tsNode 端,写进 cormi.yml:- name: ./plugin.ts
ts
import { type Context, Schema } from '@cormijs/cormi'

export const name = 'generator-meta'
export const inject = ['html']

export const Config = Schema.object({ 
  generator: Schema.string().default('cormi'),
})

export function apply(ctx: Context, config: { generator: string }) {
  ctx.html.head({ 
    tag: 'meta',
    attrs: { name: 'generator', content: config.generator },
  })
}
src/app.tsx浏览器端,按约定自动挂载
tsx
import type { Context } from '@cormijs/react/client'
import { StrictMode } from 'react'

export default (ctx: Context) => {
  ctx.on('router/change', (location) => {
    document.title = `Cormi · ${location.pathname}`
  })
  ctx.wrap(({ children }) => <StrictMode>{children}</StrictMode>)
}
inject
依赖靠声明。ctx.html 就绪之前插件不会加载,加载顺序由依赖决定,不分阶段。
Config
cormi.yml 中的配置先经 Schema 校验、填充默认值。改动配置只会重载这一个插件。
ctx.html.head()
每次注册都是调用方插件的一个 effect。插件卸载时,这个 meta 标签随之移除,不用写清理代码。
ctx.on() · ctx.wrap()
浏览器端同样是 cordis Context:事件监听和根组件包装器也会随插件一起卸载。

两端都是 Cordis Context

Node 端的插件构建应用,浏览器端的插件运行应用。两边是同一种写法,中间只有一座桥:Node 插件按包名注册浏览器插件,配置序列化后传过去。

Node · cormi
ctx.cormi
cwd、mode、paths
ctx.router
route() · layout() · patch()
ctx.html
head() · body() · transform()
ctx.app
entry() · import() · file() · server()
ctx.vite
plugin() · configure() · input()
ctx.server
get() · use() · ws()
ctx.cli
command() · generator()
ctx.app.entry(
'@scope/plugin-foo/client',
config)
浏览器 · cormi/client · @cormijs/react/client
ctx.$app
wrap() · props · app/mount
ctx.$router
add() · navigate() · preload()
ctx.$federation
load() · import()

核心服务在根上提供,名字带 $,与 Node 端的 ctx.app、ctx.router 互不冲突——koishi 的客户端也是这样做的。

官方插件

每个都是普通的 cordis 插件:写进 cormi.yml 就启用,删掉或标记 disabled 就卸载。

数据与状态
  • plugin-modelsrc/models 中的 hook 即全局模型,useModel()
  • plugin-initial-state运行时插件提供初始状态,useInitialState()
  • plugin-request基于 fetch 的 request() 与 useRequest()
  • plugin-react-query每个应用一个 QueryClient
  • plugin-dvadva-core 与 dva-loading,可选 immer
  • plugin-valtiodefineStore(),dev 下接入 DevTools
界面与样式
  • plugin-antd主题、语言,服务端渲染时抽取样式
  • plugin-layout由路由生成菜单和面包屑的布局
  • plugin-tailwindcssTailwind CSS v4
  • plugin-unocssUnoCSS,没有配置文件时用预设
  • plugin-styled-componentsThemeProvider 与 SSR 样式
构建与开发
  • plugin-mockmock/** 中的接口,优先于真实 API
  • plugin-export-static构建后把路由预渲染为 HTML
  • plugin-legacy为不支持模块脚本的浏览器构建
  • plugin-mpa多页应用,每页一个 HTML
  • plugin-analyticsGA4 与百度统计,导航自动上报
  • plugin-lintcormi lint,自带默认规则
微前端与联邦
  • plugin-federation子包单独部署,底座按 URL 汇集成一个应用
  • plugin-mf模块联邦,基于 @module-federation/vite
  • plugin-qiankunqiankun 主应用与 Vite 构建的子应用

来自 umi?

能力基本对得上,形态不同:umi 的插件 API 在 Cormi 里是服务上的注册方法和类型化事件。

umiCormi
api.addHTMLMetas / addHTMLScriptsctx.html.head(tag) / ctx.html.body(tag)
api.addRuntimePluginctx.app.entry(module, config),浏览器端用 ctx.wrap() 与事件
api.modifyRoutes / addLayoutsctx.router.patch(fn) / ctx.router.layout(file)
api.modifyViteConfigctx.vite.configure(fn) / ctx.vite.plugin(plugin)
api.onBuildCompletectx.on('build/after', fn)
api.restartServer()不需要:注册一变化,产物就重新计算

完整对照与覆盖情况

跑起来

Cormi 还在 0.x,尚未发布到 npm。现在体验它,最快的方式是运行仓库里的示例。

  1. 克隆并构建

    sh
    git clone https://github.com/cormijs/cormi.git
    cd cormi && pnpm install && pnpm build
  2. 启动一个示例

    sh
    cd examples/basic
    pnpm dev
  3. 改一行配置

    dev server 运行时,把 cormi.yml 里 ./plugin.ts 的 generator 改成别的值。日志里只有这个插件重载,页面的 meta 标签随之更新。

阅读快速开始

以 MIT 许可发布 · 基于 Cordis 4、React 19 与 Vite 7