应用装配

推荐把装配封装成自己的 AddMyApp(AppType);底层则由 AddKnown / AddKnownClient / AddKnownWeb / AddKnownDesktop 四个入口按需组合。

最后更新

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 已经把顺序内部理顺了,你自己手搭时照这个顺序即可:

  1. AddKnown 先——立起全局上下文(Id / Name / 主程序集),注册三个核心服务;
  2. Config.AddModule(你的程序集)——必须排在 AddKnown 之后,这样业务 [Service] AuthService 才会覆盖框架默认实现;[WebApi] / [Menu] / [Table] 也在此步被扫到;
  3. 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。

与其他概念

微信公众号二维码 扫码关注公众号
QQ 交流群二维码 扫码加入 QQ 群