npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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();                  // 获取当前 pathname

5. 鉴权

鉴权解决"用户把受保护路由分享给他人、对方直接访问该路由"的场景。

核心规则:解析出的路由链上,只要任一路由配置了 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(); // 启动应用

流程:

  1. 直接访问 /admin/users/123;
  2. 链上 /admin 标记了 authority: true;
  3. 调用鉴权函数,返回 false;
  4. 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 获得路由、鉴权、网络、存储、日志等开箱能力。