Known 的启动装配刻意分成"底层入口"和"上层封装"两层:底层有四个方法(AddKnown、AddKnownClient、AddKnownWeb、AddKnownDesktop),让你能按需把同一份程序集组合到不同进程形态里;上层则推荐你把本应用的元信息、模块登记、菜单分组打包成自己的一行扩展方法(开源示例里叫 AddKnownApp,你自己的可以叫 AddMyApp),宿主只调一行就够了。本页先讲推荐的上层封装,再拆底层的四个入口。
推荐范式:把装配收进你自己的 AddMyApp
框架不内置任何"后台/示例应用"的装配方法——那是每个业务自己的事。开源示例的做法是在业务程序集(KnownApp)里写一个静态扩展方法,把"我是谁、我有哪些模块、我的菜单分组"一次声明完:
public static class AppConfig
{
public static string AppId => "Known";
public static string AppName => "Known信息管理系统";
// 你自己的应用可以叫 AddMyApp,命名完全自由
public static void AddKnownApp(this IServiceCollection services, AppType type)
{
var assembly = typeof(AppConfig).Assembly;
services.AddKnown(option =>
{
option.Id = AppId;
option.Name = AppName;
option.Type = type;
option.Assembly = assembly; // 把业务程序集作为主程序集
option.CreateTime = new DateTime(2026, 10, 1);
});
services.AddKnownDemo(); // 可选:接入示例库;纯业务可删
// 关键顺序:放在 AddKnown 之后,业务自己的 [Service] AuthService 才会覆盖框架默认实现
Config.AddModule(assembly);
// 菜单分组集中声明一次,页面 [Menu].Group 按 id 引用,子菜单不再重复配图标/排序
Config.AddMenuGroup("components", "组件示例", icon: "package", order: 50);
Config.AddMenuGroup("system", "系统管理", icon: "sliders", order: 90);
// 派生值(如每个分类的示例数量)在装饰器里从唯一真相源实时算,绝不各页手填
Config.AddMenuDecorator(item => { /* item.Badge = DemoCatalog.InGroup(seg).Count */ });
// Config.AddTopAction("docs", "帮助文档", icon: "external-link", url: "https://known.org.cn", order: 20);
}
}
有了它,三种运行形态的宿主都只写两行:一行 AddMyApp(AppType) 声明"我这个应用",一行按形态选 AddKnownClient / AddKnownWeb / AddKnownDesktop 声明"我这次以什么进程形态跑"。AppType 枚举只有 { Web, Desktop }(默认 Web),桌面宿主传 Desktop,其余都传 Web。
底层四入口分别做什么
| 方法 | 场景 | 关键职责 |
|---|---|---|
AddKnown(option => ...) |
所有应用必调 | 立起应用元信息(Id / Name / Type / Assembly / CreateTime);注册 UIService、LocalService、IAuthProvider→AuthProvider;内部 AddModule<App>() 把框架自身程序集登记为模块;AddMenuGroup 预声明 id 为 development 的「开发中心」分组,页面用 [Menu(Group = "development")] 挂入 |
AddKnownClient(option => ...) |
客户端进程 | 配置 ClientOption.BaseAddress、注册所有 [WebApi] 的 ServiceProxy<T>、建 HttpClient |
AddKnownWeb(option => ...) |
服务端进程 | 按 ServerOption(WebRoot / ContentRoot / Assembly / Database / UseFileLogger)建服务端:把 [WebApi] 接口注册成路由、绑定 [Service] 实现、装配 Database |
AddKnownDesktop(option => ...) |
桌面进程 | 与 AddKnownWeb 同构(内部都走 AddKnownServer),配置项一致,只是语义上标记"桌面 WebView 宿主" |
AddKnown 里那句内部 AddModule<App>() 只扫框架自己的程序集;你业务程序集里的 [WebApi] / [Service] / [Menu] / [Table] 必须靠 Config.AddModule(你的程序集) 才扫得到——这正是上层封装里"AddKnown 之后紧跟 Config.AddModule"的原因。
典型组合(与开源宿主一致)
场景 A:Blazor WASM 独立客户端
前端进程只有 UI,数据靠 ServiceProxy 打到远端。基地址用 NavigationManager 同源解析出来的 HostEnvironment.BaseAddress,不要自己拼 +api:
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddKnownApp(AppType.Web); // 你的 AddMyApp
builder.Services.AddKnownClient(o =>
o.BaseAddress = builder.HostEnvironment.BaseAddress); // 同源基地址,结尾不带 /
await builder.Build().RunAsync();
若客户端与后端不同源(独立部署 API),才把 BaseAddress 写成 https://api.example.com(结尾约定不带斜杠)。
场景 B:Blazor Server 一体化 / Web 宿主
前后端同进程,[WebApi] 直接被 AddKnownWeb 暴露成端点,ServiceProxy 在同源下走本地/HTTP 皆可。数据库与日志在 ServerOption 回调里配:
builder.Services.AddKnownApp(AppType.Web);
builder.Services.AddKnownWeb(o =>
{
o.WebRoot = ...; o.ContentRoot = ...;
o.Assembly = typeof(Program).Assembly;
o.Database = db => db.AddSQLite<Microsoft.Data.Sqlite.SqliteFactory>(connString);
o.UseFileLogger(Path.Combine(o.ContentRoot, "Logs"));
});
// 再叠加认证:例如 AddAuthentication().AddCookie(...) + AddScoped<IAuthProvider, CookieAuthProvider>()
启动阶段记得 provider.UseDatabase(AppConfig.SeedAsync)——它统一做"非破坏性建表/增列 + 幂等种子",各宿主只调这一次,不必各写一套建库逻辑。
场景 C:桌面 Hybrid
和 B 几乎一样,只是把 AddKnownWeb 换成 AddKnownDesktop,基地址/根目录用 AppContext.BaseDirectory,Web 后缀的方法在桌面里不能用:
var services = new ServiceCollection();
services.AddKnownApp(AppType.Desktop);
services.AddKnownDesktop(o =>
{
o.WebRoot = o.ContentRoot = AppContext.BaseDirectory;
o.Database = db => db.AddSQLite<SqliteFactory>($"Data Source={dbFile}");
o.UseFileLogger(Path.Combine(AppContext.BaseDirectory, "Logs"));
});
services.AddWindowsFormsBlazorWebView();
var provider = services.BuildServiceProvider();
provider.UseDatabase(AppConfig.SeedAsync);
场景 D:营销站这类纯静态 SSR
Website 这种项目不调用上面任何应用入口——营销页不跑应用运行时、不接数据库、不挂 ServiceProxy,是静态预渲染的独立 Web SDK 工程。但它并非"零 Known 依赖":文档页 / 组件页会在服务端渲染继承自 BaseComponent 的组件(如侧栏 <Icon>),而 BaseComponent 通过 [Inject] 依赖 UIService 与 LocalService,不注册就会在注入处抛异常。所以它至少手动 AddScoped<UIService>() + AddScoped<LocalService>()(两者构造极轻,LocalService 还延迟解析 IJSRuntime),除此之外复用的是同一套 CSS token。
option 的关键字段
builder.Services.AddKnown(option =>
{
option.Id = "KnownD"; // 应用唯一标识;日志/埋点/多租户隔离用
option.Name = "Known 信息管理系统"; // 展示名
option.Type = AppType.Web; // Web / Desktop(默认 Web)
option.Assembly = typeof(AppConfig).Assembly; // 主程序集
option.CreateTime = new DateTime(2026, 10, 1); // 建号时间戳(默认 2020-07-10)
});
option.Assembly 只是把该程序集记进 AppInfo 供展示/溯源;它不等于扫描该程序集。模块扫描永远由 Config.AddModule(...) 完成。
顺序约束
AddMyApp 已经把顺序内部理顺了,你自己手搭时照这个顺序即可:
AddKnown先——立起全局上下文(Id / Name / 主程序集),注册三个核心服务;Config.AddModule(你的程序集)——必须排在AddKnown之后,这样业务[Service] AuthService才会覆盖框架默认实现;[WebApi]/[Menu]/[Table]也在此步被扫到;AddKnownClient/AddKnownWeb/AddKnownDesktop——它们要读第 2 步的扫描结果来做路由与 DI 装配,所以排最后。
常见坑
- 忘了
Config.AddModule:路由扫不到、页面显示"未找到该路径"。AddKnown内部只扫框架程序集,不含你的业务。 Config.AddModule排在AddKnown前面:默认实现覆盖不掉,登录/鉴权行为会不对。AddModule<T>()的T写成静态类:T只用来取程序集,必须是非静态类型(通常拿AppConfig这类 marker class 当锚点,静态类会CS0718)。BaseAddress结尾多写/或乱拼+api:ServiceProxy拼 URL 会出现//或错路径。同源就用HostEnvironment.BaseAddress,跨源就写干净的站点根,约定结尾不带斜杠。- Server 进程里忘了
AddKnownWeb:[Service]类不会自动注册进 DI,客户端调用返回 500。 - 在 WASM / 桌面里调
AddKnownWeb:Web/Desktop后缀只在对应进程形态使用;桌面用AddKnownDesktop。
与其他概念
- 模块扫描与
[Menu]装配细节见插件式模块架构。 - 前后端契约与强类型 HTTP 见 ServiceProxy。
AddKnown注入的IAuthProvider、Cookie / Bearer 两条路线见鉴权与身份。- 三行入口对应三种运行形态的完整接入见三种运行形态。
扫码关注公众号
扫码加入 QQ 群