knight-web
v2.3.0
Published
基于React的Knight-web架构
Downloads
385
Readme
Knight WebFrame 使用手册
基于 React 18 的轻量 SPA 框架。核心思想是:保留 React 原生用法(render() / this.state / this.setState),在其之上用一层薄薄的基类与工具库,把前端最常用的能力(路由、鉴权、网络、存储、日志等)封装好,通过 this.xxx 字段免 import 暴露给业务页面。
目录
1. 快速开始
# 启动本地开发服务器(默认端口 1027)
npm run dev
# 构建库产物:dist/index.js(UMD) + dist/**/*.d.ts(类型声明)
npm run build
# 单独执行
npm run build:js # rollup 打包 JS
npm run build:types # tsc 生成 .d.ts 声明文件开发入口是根目录的 index.html,它加载 ./test/index.ts。test/ 下是演示页面,展示了路由、嵌套路由、鉴权、状态管理等全部能力。
2. 目录结构
Knight_Web/
├── index.html # 开发入口,加载 ./test/index.ts
├── vite.config.ts # 开发服务器配置
├── rollup.config.js # 库构建配置(UMD,name = KnightWeb)
├── tsconfig.build.json # 类型声明构建配置(emitDeclarationOnly)
├── package.json # 库元信息 / 构建脚本 / 依赖
├── public/ # 静态资源
├── src/ # 库源码
│ ├── index.ts # 库入口:export * from './base' + './utility'
│ ├── base/ # 基类层
│ │ ├── AbstractComponent.ts
│ │ ├── AbstractPanel.ts
│ │ └── index.ts
│ └── utility/ # 工具库(16 个模块)
│ ├── NavigationUtility.ts # 路由 / 嵌套渲染 / 鉴权 / 导航
│ ├── NetworkUtility.ts # HTTP / WebSocket
│ ├── BrowserUtility.ts # 存储 / Cookie / 剪贴板 / 脚本加载 / WebGL
│ ├── LogUtility.ts # 日志
│ ├── EventUtility.ts # 内存事件总线
│ ├── StringUtility.ts
│ ├── MathUtility.ts
│ ├── ObjectUtility.ts
│ ├── CryptoUtility.ts
│ ├── FileUtility.ts
│ ├── PromiseUtility.ts
│ ├── ReflectionUtility.ts
│ └── ... (Enum / Interface / Profiler)
└── test/ # 演示页面(也是路由注册的示例)
├── index.ts # 触发 @OnRoute 注册 + 配置鉴权 + OnLaunch
├── Login.tsx # 首页 "/"
├── Home.tsx # "/home"
├── Main.tsx # "/main"(查询参数)
├── NestedDemo.tsx # "/admin" 嵌套路由全家桶(绝对路由范例)
├── ReuseDemo.tsx # "/app" + "/console" 复用同一套界面(相对路由范例)
└── ReactiveDemo.tsx # "/state-demo"(原生 state)3. 核心概念:base 层
3.1 AbstractComponent
所有组件的根基类,继承自 React.Component。不覆写任何 React 生命周期,仅补充少量工具。
export abstract class AbstractComponent<P, S> extends React.Component<P, S>
{
protected readonly domRef = React.createRef<HTMLDivElement>(); // DOM 引用
public cx(...classes): string; // 合并 CSS 类名,过滤空值
public setStateAsync(state): Promise<void>; // 异步 setState(回调可 await)
public abstract render(): React.ReactNode; // 唯一抽象方法
}它还提供了三个空实现的生命周期方法(componentDidMount / componentDidUpdate / componentWillUnmount),用于承接子类里的 super.componentDidMount() 之类调用,保证链式调用不会报错。
3.2 AbstractPanel
业务页面推荐直接继承它。 在 AbstractComponent 之上,把常用工具通过 this.xxx 字段暴露出来(免 import),并提供 DOM 快捷查询。
export abstract class AbstractPanel<Props, State> extends AbstractComponent<Props, State>
{
public readonly navigation = NavigationUtility; // 导航 / 路由
public readonly storage = BrowserUtility.LocalStorage; // 本地存储
public readonly cookie = BrowserUtility.BrowserCookie; // Cookie
public readonly clipboard = BrowserUtility.Clipboard; // 剪贴板
public readonly event = EventUtility; // 事件总线
public readonly network = NetworkUtility; // 网络
public readonly log = LogUtility; // 日志
protected Query<E>(selector): E | null; // document.querySelector
protected QueryAll<E>(selector): NodeListOf<E>; // document.querySelectorAll
}这些字段都是模块引用转发,指向已有的单例 namespace,无状态、无额外开销。业务侧既可以用
this.xxx直接访问,也可以import { NetworkUtility } from 'knight-web'显式引用,二者等价。
4. 路由系统
路由由 utility/NavigationUtility.ts 提供,采用 namespace + 模块级状态(无需 new)。
4.1 注册路由 @OnRoute
用 @OnRoute(path, options) 装饰器把页面类注册为一条路由。装饰器在模块被 import 时即执行注册,无需手动调用注册函数。
import { AbstractPanel, OnRoute } from 'knight-web';
@OnRoute("/home", { title: "首页" })
export class Home extends AbstractPanel
{
render()
{
return <h1>Home</h1>;
}
}options 支持:
| 选项 | 类型 | 说明 |
|---|---|---|
| title | string | 页面标题,命中时写入 document.title |
| name | string | 路由名称,缺省为规范化后的路径。children 通过它引用本路由(见 4.2) |
| authority | boolean | 是否需要鉴权(见 第 5 节) |
| children | string[] | 允许挂载到本路由 <Outlet /> 下的相对路由名(见 4.2) |
路径写法决定路由类型:
path以/开头 = 绝对路由,不以/开头 = 相对路由。这是区分二者的唯一标志。
4.2 绝对路由与相对路由
绝对路由(path 以 / 开头)
锚定 URL 根,path 从 URL 的第一段开始匹配。它的 URL 位置是固定的——/admin/users 只会在地址栏是 /admin/users 时命中。
// 有固定 URL 位置的页面,用绝对路由
@OnRoute("/admin", { authority: true, title: "管理后台" })
export class AdminLayout extends AbstractPanel
{
render() { return <div><aside>侧边栏</aside><main><Outlet /></main></div>; }
}
@OnRoute("/admin/users/:id", { title: "用户详情" })
export class AdminUserDetail extends AbstractPanel
{
render() { return <div>用户 #{this.navigation.OnGetParams().id}</div>; }
}
@OnRoute("/", { title: "首页" }) // 根路由:path 为 "/"
export class Home extends AbstractPanel { }访问 /admin/users/1 → 渲染链 [/admin, /admin/users/:id]。
什么时候用:页面本身就有确定的 URL(首页、登录页、活动页),或者它就该固定在 URL 的某一层。
相对路由(path 不以 / 开头)
不锚定 URL 根,它在 URL 的任意深度上匹配"以该深度结尾的那一段后缀",因此可以被任意一个在 children 里声明了它的父布局渲染。
// 一套要挂到多个外壳下的界面,用相对路由
@OnRoute("user", { title: "用户管理", children: ["user/:id"] })
export class User extends AbstractPanel
{
render() { return <div>用户列表<hr /><Outlet /></div>; }
}
@OnRoute("user/:id", { title: "用户详情" })
export class UserDetail extends AbstractPanel
{
render() { return <div>用户 #{this.navigation.OnGetParams().id}</div>; }
}User 只声明了"我是 user",完全不知道自己会被哪个外壳包住。谁来包它,由外壳自己决定:
@OnRoute("/app", { children: ["user"] }) // ← 「我接受 user 挂进来」
export class AppLayout extends AbstractPanel
{
render() { return <div><Sidebar /><Outlet /></div>; }
}
@OnRoute("/console", { children: ["user"] }) // ← 换一个外壳,加这一行即可
export class ConsoleLayout extends AbstractPanel
{
render() { return <div><Topbar /><Outlet /></div>; }
}
@OnRoute("/blog") // ← 没写 children,所以挂不进来
export class BlogLayout extends AbstractPanel
{
render() { return <div><Outlet /></div>; }
}| URL | 渲染链 |
|---|---|
| /app/user | AppLayout → User |
| /console/user | ConsoleLayout → User |
| /blog/user | BlogLayout(未声明 user,<Outlet /> 为空,不泄漏) |
什么时候用:一套界面(用户管理、设置面板、数据看板)需要挂到多个不同外观的外壳下时。加一个新外壳只需在新外壳上写一行 children,界面组件一个字都不用改。
并排对照:同一个组件,两种写法
// ── 写法 A:绝对路由 ──
@OnRoute("/user")
export class User extends AbstractPanel { render() { return <div>用户列表</div>; } }只能渲染在 /user。想让它在 /app/user 下出现,做不到——/user 与 /app/user 是两个不同的 URL,绝对路由只认前者。
// ── 写法 B:相对路由 + 宿主声明 ──
@OnRoute("user")
export class User extends AbstractPanel { render() { return <div>用户列表</div>; } }
@OnRoute("/app", { children: ["user"] })
export class AppLayout extends AbstractPanel { render() { return <div><Outlet /></div>; } }/app/user 命中 [AppLayout, User]。再给 /console 加一行 children: ["user"],/console/user 也命中同一套组件。
⚠️ 相对路由不能作为渲染链的起点。
@OnRoute("user")单独存在时,访问/user不会渲染出User——因为没有宿主外壳,渲染出来会是一个没有任何布局包裹的裸页面。根层页面(/user、/login、/)必须用绝对路由注册。
怎么选
| 你的页面是… | 用 |
|---|---|
| 有固定 URL 的独立页面(首页、登录页、活动页) | 绝对路由 /home |
| 某布局下固定的子页面(/admin/settings) | 绝对路由 /admin/settings |
| 一套要复用到多个外壳下的界面 | 相对路由 user |
| 上述界面的下级页面(详情、编辑) | 相对路由(由上级界面在 children 里声明) |
4.3 嵌套路由与 Outlet
布局组件用 <Outlet /> 声明"子路由渲染的位置"。框架按路径段自叶向根逐层用 OutletContext.Provider 包裹,实现任意层级嵌套。
import { AbstractPanel, OnRoute, Outlet } from 'knight-web';
// 父布局
@OnRoute("/admin", { authority: true, title: "管理后台" })
export class AdminLayout extends AbstractPanel
{
render()
{
return (
<div style={{ display: 'flex' }}>
<aside>侧边栏</aside>
<main><Outlet /></main> {/* 子路由渲染在这里 */}
</div>
);
}
}
// 子页面
@OnRoute("/admin/users")
export class AdminUsers extends AbstractPanel
{
render()
{
return <div>用户列表 <Outlet /></div>; // 还可以继续嵌套
}
}访问 /admin/users 时,渲染链为 AdminLayout → AdminUsers,AdminUsers 出现在 AdminLayout 的 <Outlet /> 位置。
用绝对路由时,"谁是谁的子页面"是靠 URL 前缀推导的(
/admin/users自然嵌在/admin里)。用相对路由时,这个关系必须由父布局的children显式声明——见下一节。
4.4 界面复用:一套界面挂到多个外壳
这是相对路由要解决的核心场景。完整可运行代码见 test/ReuseDemo.tsx。
需求:同一套"用户管理"界面(列表 + 详情),要同时出现在两个外观完全不同的后台外壳里。
// ══════════════ ① 可复用的界面:注册一次,不知道宿主是谁 ══════════════
/** 用户列表(相对路由)。声明「我的 Outlet 里可以挂 user/:id」 */
@OnRoute("user", { title: "用户管理", children: ["user/:id"] })
export class User extends AbstractPanel
{
private users = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' },
];
render()
{
return (
<div>
<h2>用户列表</h2>
{this.users.map(u =>
<div key={u.id} onClick={() => this.navigation.OnNavigate(`${window.location.pathname}/${u.id}`)}>
{u.name} — {u.email}
</div>
)}
<hr />
<Outlet /> {/* ← 详情嵌在这里 */}
</div>
);
}
}
/** 用户详情(相对路由 + 路径参数)。一条路由覆盖无限个详情页 */
@OnRoute("user/:id", { title: "用户详情" })
export class UserDetail extends AbstractPanel
{
render()
{
const { id } = this.navigation.OnGetParams();
return <div>用户详情 #{id}</div>;
}
}
// ══════════════ ② 两个外壳:各自声明接受谁 ══════════════
/** 暗色侧栏外壳 */
@OnRoute("/app", { title: "App 外壳", children: ["user"] })
export class AppLayout extends AbstractPanel
{
render()
{
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: 220, background: '#16213e', color: '#eee' }}>App 侧边栏</aside>
<main style={{ flex: 1 }}><Outlet /></main>
</div>
);
}
}
/** 亮色顶栏外壳 */
@OnRoute("/console", { title: "Console 外壳", children: ["user"] })
export class ConsoleLayout extends AbstractPanel
{
render()
{
return (
<div>
<header style={{ background: '#fff', borderBottom: '1px solid #e1e4e8' }}>Console 顶栏</header>
<main><Outlet /></main>
</div>
);
}
}渲染结果:
| URL | 渲染链 | 看到的效果 |
|---|---|---|
| /app/user | AppLayout → User | 暗色侧栏 + 用户列表 |
| /app/user/1 | AppLayout → User → UserDetail | 暗色侧栏 + 列表 + 详情 |
| /console/user | ConsoleLayout → User | 亮色顶栏 + 用户列表 |
| /console/user/1 | ConsoleLayout → User → UserDetail | 亮色顶栏 + 列表 + 详情 |
四条 URL、两套外壳,User 和 UserDetail 只注册了一次。 想再挂第三个外壳,只需给新外壳加一行 children: ["user"]。
children 的层级规则:每层只声明自己的直接下级
children 不传递。上面 /app 只写了 ["user"],没有写 "user/:id"——因为 UserDetail 不是 /app 的直接下级,它嵌在 User 的 <Outlet /> 里,所以由 User 自己声明 children: ["user/:id"]。
这个规则把过去"/admin/users/:id 会渲染在 /admin/users 的 Outlet 里"这个靠 URL 前缀巧合成立的隐式约定,变成了显式声明。用绝对路由时你不需要写它(框架按 URL 前缀推导),但一旦改用相对路由,就要逐层写出来。
如果某个父布局声明了一个不存在(或尚未注册)的路由名,启动时控制台会报错:
路由</app>的 children 引用了未注册的路由名<user>children 只约束相对路由
绝对路由不受 children 限制:只要 URL 段数对得上,它就能命中。
这条约束保证了对既有代码 100% 向后兼容,更准确地说它带来一条不变式:
注册或删除一个相对路由,绝不会改变任何绝对路由的解析结果。
因此给现有项目引入相对路由是安全的:已有的 /admin/* 路由行为不会发生任何变化。Demo 里 test/NestedDemo.tsx 的四条绝对路由(/admin、/admin/users、/admin/users/:id、/admin/settings)就是这条不变式的活样本。
外壳也可以写成相对路由
只要一个外壳自己没有固定的 URL 位置,它同样可以写成相对路由,变成一个可复用的壳:
/** 设置面板外壳:不锚定 URL,可以被挂到任何声明了它的父布局下 */
@OnRoute("settings", { title: "设置", children: ["settings/profile"] })
export class SettingsShell extends AbstractPanel
{
render() { return <div><Tabs /><Outlet /></div>; }
}
@OnRoute("settings/profile")
export class SettingsProfile extends AbstractPanel { }只要某个父布局声明了 children: ["settings"](例如 @OnRoute("/admin", { children: ["settings"] })),访问 /admin/settings/profile 就会渲染 [AdminLayout, SettingsShell, SettingsProfile]。
4.5 路径参数
路径中用 :name 声明参数,参数可出现在任意位置。在组件内通过 this.navigation.OnGetParams() 读取。
@OnRoute("/admin/users/:id")
export class UserDetail extends AbstractPanel
{
render()
{
const { id } = this.navigation.OnGetParams();
return <div>用户 #{id}</div>;
}
}路径段分两种:
- 字面量段(
users)——原样比较,提供区分度; - 参数段(
:id)——匹配任意一段并捕获其值,提供变量。
一条路由可以同时含两种(user/:id/edit)。相对路由也一样支持,这正是 4.4 里 user/:id 能挂到两个外壳下的原因。
4.6 匹配优先级
框架逐深度解析 URL:从深度 1 开始递增,每个深度上至多选中一条路由,因此支持跳过中间层(/admin/settings 在没有 /admin/settings 时会退化为只命中 /admin)。
每个深度上的选取规则,按优先级从高到低:
| 优先级 | 规则 | 说明 |
|---|---|---|
| 1 | 绝对路由无条件优先 | 该深度只要有绝对路由命中,就直接选它,丢弃全部相对候选,不再比较字面量 |
| 2 | 字面量段数多者优先 | 仅在相对路由之间比较。user/detail 胜过 user/:id |
| 3 | 段数(span)长者优先 | 字面量数相同时,匹配得更长的胜出 |
| 4 | 注册顺序 | 仍平局则保留先注册的那条 |
为什么绝对路由是无条件优先,而不是"先比字面量":考虑 /app + 绝对 /app/:section + 相对 user,访问 /app/user。若先比字面量,相对 user 是纯字面量段、绝对 /app/:section 含一个参数段,user 会以"字面量更多"胜出——一个相对路由抢走了绝对路由的 URL。无条件优先杜绝了这种跨类型的干扰,也正是 4.4 那条不变式(注册/删除相对路由不影响绝对路由)的实现方式。
上例中,访问 /app/user 命中 [/app, /app/:section],params.section === "user"。
4.7 查询参数
this.navigation.OnSearch<T>() 返回当前 URL 的查询参数对象。
// 当前 URL: /main?time=123&page=1
const { time, page } = this.navigation.OnSearch<{ time?: string; page?: string }>();4.8 程序化导航
所有导航都通过 this.navigation 调用:
this.navigation.OnNavigate('/home'); // pushState + 渲染(推入历史)
this.navigation.OnNavigate('/main', '?page=2'); // 携带查询参数(自动补 ?)
this.navigation.OnReplace('/home'); // replaceState(替换当前历史)
this.navigation.OnBack(); // 后退
this.navigation.OnForward(); // 前进
this.navigation.OnGo(-1); // 跳转历史 delta
this.navigation.OnReload(); // 刷新页面
this.navigation.OnOpen('https://example.com'); // 新窗口打开
this.navigation.OnGetPathName(); // 获取当前 pathname5. 鉴权
鉴权解决"用户把受保护路由分享给他人、对方直接访问该路由"的场景。
核心规则:解析出的路由链上,只要任一路由配置了 authority: true,即触发鉴权。典型做法是父布局 /admin 配 authority: true,其下所有子路由自动受保护。
在应用入口(test/index.ts)配置一次鉴权处理器:
import { OnSetAuthority, OnLaunch } from 'knight-web';
// 鉴权函数:返回 true 放行,false 则跳转 redirectPath
OnSetAuthority(async () => !!localStorage.getItem('token'), "/login");
window.onload = () => OnLaunch(); // 启动应用流程:
- 直接访问
/admin/users/123; - 链上
/admin标记了authority: true; - 调用鉴权函数,返回
false; history.replaceState到/login(避免后退按钮回到受保护页)并渲染登录页。
启动:OnLaunch(container?) 是幂等的,会创建根容器、监听 popstate、执行首次渲染。可传入自定义容器元素,默认挂载到 document.body。
authority 配在父布局,还是配在相对路由上?
因为链上任一节点配置即可触发,两种位置都合法,取决于这条约束属于"外壳"还是"界面":
// ── 属于外壳:整个后台要登录 ──
// /app 下的一切都受保护,/console 不受影响
@OnRoute("/app", { authority: true, children: ["user"] })
export class AppLayout extends AbstractPanel { }
// ── 属于界面:这份数据敏感,挂哪都得登录 ──
// /app/user 与 /console/user 都会触发鉴权
@OnRoute("user", { authority: true, children: ["user/:id"] })
export class User extends AbstractPanel { }这正是相对路由带来的新能力:鉴权约束可以跟着界面走,而不是跟着 URL 走。两者也可以叠加(任一命中即触发)。
⚠️ 配在相对路由上时,它会作用于该界面的所有宿主 URL。这是"跟着界面走"的必然结果,不是 bug——但如果你只想保护某一个外壳下的界面,就把
authority配在那个外壳上。
test/ReuseDemo.tsx 里的三个外壳都没有配 authority,因为它们要演示的是复用能力;实际项目可按上面的规则自由组合。
6. this.xxx 快捷字段
继承 AbstractPanel 后,页面内直接使用以下字段,无需 import。
6.1 navigation(导航)
见 第 4 节。
6.2 storage(本地存储)
封装 localStorage,自动 JSON 序列化。
this.storage.Set('user', { name: 'Alice' }); // 写(自动 JSON.stringify)
const user = this.storage.Get<{ name: string }>('user'); // 读(自动 JSON.parse)
this.storage.Exist('user'); // 是否存在
this.storage.Remove('user'); // 删除
this.storage.RemoveAll(); // 清空6.3 cookie(Cookie)
this.cookie.SetCookie('token', 'abc', 7); // key / value / 有效天数(默认 7)
const token = this.cookie.GetCookie('token');
this.cookie.RemoveCookie('token');
this.cookie.RemoveAllCookie();6.4 clipboard(剪贴板)
await this.clipboard.writeText('复制的内容');
const text = await this.clipboard.readText();6.5 event(事件总线)
纯内存事件总线,注册返回 ID,注销时传入 ID。支持定向事件与广播事件。
// 定向:只有同名 key 的订阅者收到
const id = this.event.regiestEvent('login', (data) => console.log(data.params));
this.event.emitEvent('login', { user: 'Alice' });
this.event.degiestEvent(id); // 组件卸载时应注销
// 广播:所有广播订阅者都收到
const bid = this.event.regiestBoardcastEvent((data) => console.log(data));
this.event.emitBoardcastEvent('broadcast', { msg: 'hello' });
this.event.degiestBoardcastEvent(bid);6.6 network(网络)
基于原生 fetch 封装,见 this.network.HTTP.OnRequest:
const res = await this.network.HTTP.OnRequest<{ list: any[] }>('https://api.example.com/users', {
method: 'get', // 默认 GET;可选 POST/PUT/DELETE 等
params: { page: 1 }, // 自动序列化为查询串(跳过 null/undefined)
data: { name: 'Alice' }, // 请求体:普通对象自动 JSON.stringify
timeout: 10000, // 超时毫秒,默认 10000,0 表示不超时
headers: { 'X-Token': '...' },
responseType: 'json', // json | text | blob | arrayBuffer | formData
});
console.log(res.data); // 已解析的响应体- 非 2xx 响应自动抛出
HttpError(含status/statusText/data)。 this.network.HTTP.OnStreamRequest(url, options)支持 SSE 流式请求(for await)。this.network.Websocket提供 WebSocket 封装(OnConnect/OnSend/On)。- 状态码枚举:
this.network.ResponseCode(200/401/404/500/1000…)。
6.7 log(日志)
this.log.LogTip('提示');
this.log.LogInfo('信息');
this.log.LogWarning('警告');
this.log.LogError('错误', error);
this.log.LogSystem('系统');输出带有时间戳、调用类名、源码位置与彩色标签。
6.8 DOM 查询
const el = this.Query<HTMLButtonElement>('#submit');
const list = this.QueryAll('.item');7. 状态管理
框架不引入任何状态库,直接使用 React 原生状态:
import { AbstractPanel, PanelProps, OnRoute } from 'knight-web';
@OnRoute("/counter")
export class Counter extends AbstractPanel<PanelProps, CounterState>
{
state: CounterState = { count: 0 };
render()
{
return (
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
{this.state.count}
</button>
);
}
}
interface CounterState { count: number; }- 更新对象:
this.setState({ user: { ...this.state.user, age: 30 } })。 - 需要"等待状态更新完成"时:
await this.setStateAsync({ count: 1 })。 - 生命周期:沿用 React 原生
componentDidMount/componentDidUpdate/componentWillUnmount,记得调用super.xxx()。
8. utility 工具库总览
所有模块从包根路径导入:import { XxxUtility } from 'knight-web'。
| 模块 | 说明 | 主要成员 |
|---|---|---|
| NavigationUtility | 路由 / 绝对与相对路由 / 嵌套 / 鉴权 / 导航 | OnRoute OnLaunch OnSetAuthority OnNavigate OnSearch OnGetParams Outlet |
| NetworkUtility | HTTP / WebSocket | HTTP.OnRequest HTTP.OnStreamRequest HttpError ResponseCode Websocket |
| BrowserUtility | 浏览器能力 | LocalStorage BrowserCookie Clipboard LoadScript CreatHTMLElement WebGL |
| LogUtility | 日志 | LogTip LogInfo LogWarning LogError LogSystem |
| EventUtility | 事件总线 | regiestEvent emitEvent regiestBoardcastEvent emitBoardcastEvent |
| StringUtility | 字符串 | IsNullOrEmpty Insert JoinString ReplaceString KeepNumeric … |
| MathUtility | 数学 / 数据结构 | UUID Time Timer MathEx ArrayEx Color Vector2/3 Plane |
| ObjectUtility | 对象 / 池 | Clone DeepCopy Assign ArrayPool MapPool QueuePool PipeLine EventSystem |
| CryptoUtility | 加解密 / 哈希 | Token.CheckToken _AES.Encrypt/Decrypt _SHA256 _MD5 _HmacSHA256 |
| FileUtility | 文件 / 路径 | Path.ExtName/Name File.ReadFile File.ParseXLSXData |
| PromiseUtility | 异步 | PromiseEx.WaitTime PromiseEx.OrigionPromise |
| ReflectionUtility | 反射 / 元数据 | Reflection Metadata GetMetadata 等 Reflect 封装 |
| EnumUtility | 枚举集合 | E_Language E_StatusCode E_ButtonType … |
| InterfaceUtility | 通用类型 | IType<T> IAction IEvent |
| ProfilerUtility | 性能分析 | ProfilerUtility Panel(FPS/MS/MB 面板) |
9. 构建与发布
构建产物:
dist/index.js—— UMD 库(全局名KnightWeb),React / ReactDOM 作为 external 不打包。dist/index.d.ts+dist/base/*.d.ts+dist/utility/*.d.ts—— 类型声明,保留源码目录结构。dist/package.json—— 发布用的包元信息(name: knight-web,version: 2.3.0)。
发布流程:
npm run build # 生成 JS + 类型声明
cd dist
npm publish # 以 dist/package.json 发布消费方:
import { AbstractPanel, OnRoute, Outlet, OnSetAuthority, OnLaunch } from 'knight-web';
import { NavigationUtility, NetworkUtility } from 'knight-web';React / ReactDOM 由消费项目提供(peerDependencies)。
10. 完整示例
一个带鉴权、嵌套路由、路径参数、状态与网络请求的完整页面:
import React from 'react';
import { AbstractPanel, PanelProps, OnRoute, Outlet } from 'knight-web';
// ============ 入口配置(main.ts) ============
import { OnSetAuthority, OnLaunch } from 'knight-web';
OnSetAuthority(async () => !!localStorage.getItem('token'), '/login');
window.onload = () => OnLaunch();
// ============ 登录页 ============
@OnRoute('/login', { title: '登录' })
export class LoginPage extends AbstractPanel
{
login()
{
localStorage.setItem('token', 'demo');
this.navigation.OnNavigate('/admin/users/1');
}
render() { return <button onClick={() => this.login()}>登录</button>; }
}
// ============ 受保护的布局 ============
@OnRoute('/admin', { authority: true, title: '管理后台' })
export class AdminLayout extends AbstractPanel
{
render()
{
return (
<div style={{ display: 'flex' }}>
<aside>侧边栏</aside>
<main><Outlet /></main>
</div>
);
}
}
// ============ 用户详情(路径参数 + 网络 + 状态) ============
@OnRoute('/admin/users/:id')
export class UserDetail extends AbstractPanel<PanelProps, { user: any }>
{
state = { user: null as any };
async componentDidMount()
{
super.componentDidMount();
const { id } = this.navigation.OnGetParams();
const res = await this.network.HTTP.OnRequest<any>(`/api/users/${id}`);
this.setState({ user: res.data });
this.storage.Set('lastUser', res.data); // 免 import 本地存储
this.log.LogInfo('已加载用户', res.data); // 免 import 日志
}
render()
{
const { user } = this.state;
return user
? <div>用户 #{this.navigation.OnGetParams().id}:{user.name}</div>
: <div>加载中…</div>;
}
}10.1 端到端:同一套界面挂到两个外壳
把 OnRoute + children + <Outlet /> + OnGetParams() 串起来的最小完整例子(对应 test/ReuseDemo.tsx)。
import React from 'react';
import { AbstractPanel, OnRoute, Outlet, OnSetAuthority, OnLaunch } from 'knight-web';
// ============ ① 入口配置 ============
OnSetAuthority(async () => !!localStorage.getItem('token'), '/');
window.onload = () => OnLaunch();
// ============ ② 可复用的界面:注册一次,不知道宿主是谁 ============
@OnRoute('user', { title: '用户管理', children: ['user/:id'] })
export class User extends AbstractPanel
{
private users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
render()
{
return (
<div>
<h2>用户列表</h2>
{this.users.map(u =>
<div key={u.id} style={{ cursor: 'pointer' }}
onClick={() => this.navigation.OnNavigate(`${window.location.pathname}/${u.id}`)}>
{u.name}
</div>
)}
<hr />
<Outlet /> {/* ← 详情嵌在这里 */}
</div>
);
}
}
@OnRoute('user/:id', { title: '用户详情' })
export class UserDetail extends AbstractPanel
{
render()
{
const { id } = this.navigation.OnGetParams();
return <div>用户详情 #{id}</div>;
}
}
// ============ ③ 两个外壳:各自声明接受谁 ============
@OnRoute('/app', { authority: true, title: 'App 外壳', children: ['user'] })
export class AppLayout extends AbstractPanel
{
render()
{
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: 220, background: '#16213e', color: '#eee' }}>App 侧边栏</aside>
<main style={{ flex: 1 }}><Outlet /></main>
</div>
);
}
}
@OnRoute('/console', { title: 'Console 外壳', children: ['user'] })
export class ConsoleLayout extends AbstractPanel
{
render()
{
return (
<div>
<header style={{ background: '#fff', borderBottom: '1px solid #e1e4e8' }}>Console 顶栏</header>
<main><Outlet /></main>
</div>
);
}
}| URL | 渲染链 | 鉴权 |
|---|---|---|
| /app/user | AppLayout → User | ✅ 外壳配了 authority |
| /app/user/1 | AppLayout → User → UserDetail | ✅ |
| /console/user | ConsoleLayout → User | ❌ 外壳未配 |
| /console/user/1 | ConsoleLayout → User → UserDetail | ❌ |
注意最后两行:同一份 User 界面,在 /app 下要登录、在 /console 下不用——因为 authority 配在外壳上。若把 authority: true 挪到 @OnRoute('user', ...) 上,四条 URL 就都会触发鉴权(见 第 5 节)。
框架定位:薄封装、贴近 React 原生。不引入自定义生命周期、不引入响应式代理、不引入状态库;业务只要会 React,就能直接用 Knight WebFrame 获得路由、鉴权、网络、存储、日志等开箱能力。
