OrielWeb 0.3.0

dotnet add package OrielWeb --version 0.3.0
                    
NuGet\Install-Package OrielWeb -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OrielWeb" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OrielWeb" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="OrielWeb" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OrielWeb --version 0.3.0
                    
#r "nuget: OrielWeb, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OrielWeb@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OrielWeb&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=OrielWeb&version=0.3.0
                    
Install as a Cake Tool

OrielWeb

类 Tauri 的 C# 跨平台系统 webview 核心库。纯 C# P/Invoke、无 C++ 中间层、Native AOT 友好、零反射 IPC。

dotnet add package OrielWeb
平台 Webview 状态
Windows x64/arm64 WebView2 (Evergreen) ✅ 真机验证(IPC 往返、单测、AOT 发布)
Linux x64 WebKitGTK 4.1 ✅ 真机验证(WSL2 + WSLg;X11 与 Wayland 双后端)
macOS arm64 WKWebView ✅ 真机验证(GitHub 托管 runner)
Linux arm64 WebKitGTK 4.1 ⚠️ 编译通过 + xvfb 冒烟;未在真机运行
macOS x64 WKWebView ⚠️ 编译通过;未在真机运行

完整 API 与各能力的取舍记录见 docs/API.md;尚未完成与待真机验证的部分见 docs/ROADMAP.md。

快速开始

要求 .NET 10 SDK(本库只提供 net10.0 目标)。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>             
    <TargetFramework>net10.0</TargetFramework>
    <PublishAot>true</PublishAot>               
    <ApplicationIcon>app.ico</ApplicationIcon>  
  </PropertyGroup>
  
</Project>
using System.Text.Json.Serialization;
using OrielWeb;
using OrielWeb.Ipc;

internal static class Program
{
    [STAThread]                                     // Windows 上要求 STA
    private static void Main(string[] args)
    {
        Oriel.CreateBuilder(args)
            .UseEmbeddedAssets()                    // oriel://app.oriel/ ← wwwroot/**
            .UseJsonContext(AppJsonContext.Default) // STJ 源生成上下文(DTO)
            .AddCommands<TodoCommands>()            // [OrielCommand] 命令类
            .UseCapabilities(c => c.Allow("todo.*"))// 页面能调哪些命令(Release 下不写就一律拒绝)
            .UseDebug()                             // 打开 DevTools
            .AddWindow(w => w.WithTitle("Demo")
                             .WithSize(1024, 720)
                             .WithFrameless()       // 无边框
                             .Centered(),
                       win => win.Loaded += () => Console.WriteLine("已就绪"))
            .Run();
    }
}

public sealed record TodoItem(int Id, string Text, bool Done);

// IPC 命令:页面 JS 调 window.oriel.invoke('todo.add', { text: '买牛奶' })
public sealed partial class TodoCommands
{
    private readonly List<TodoItem> _items = [];
    private int _nextId;

    [OrielCommand("todo.add")]
    public TodoItem Add(string text)
    {
        var item = new TodoItem(_nextId++, text, Done: false);
        _items.Add(item);
        return item;
    }
}

// STJ 源生成上下文:DTO 序列化的 AOT 安全入口(命令参数与返回值都经它)
[JsonSerializable(typeof(TodoItem))]
internal partial class AppJsonContext : JsonSerializerContext;

<script>
    await window.oriel.ready;
    const item = await window.oriel.invoke('todo.add', { text: '买牛奶' });
    // → JSON 参数 → 编译期路由 → C# 方法 → JSON 回执 → Promise resolve
</script>

两条容易踩的约定:

  • 应用自己的命令不要用 win. 前缀——那是库保留的窗口命令前缀,始终放行、不受能力配置约束, 而且它现在被库的内建窗口命令(最小化/最大化/关闭/拖动/全屏/置顶/选文件/上下文菜单)真正占用了: 自定义的同名命令会被静默遮蔽。
  • Release 构建下必须声明能力,否则 oriel.invoke 一律被拒(fail-closed)。Debug 构建则全放行。

内嵌页面资源

前端资源编译进程序集,运行期由库通过自定义 scheme oriel://<host>/… 直接交给引擎加载 (不写盘,也不再解压到任何目录)。发布产物里没有 wwwroot 文件夹,但页面照常工作; 从 NuGet 包消费时默认零配置(本仓库内用 ProjectReference 的工程要自己声明一行,原因见下)。

零配置是怎么来的

包内的 buildTransitive/OrielWeb.targets(默认开启)把项目里的 wwwroot\**\* 全部内嵌:


<EmbeddedResource Include="wwwroot\**\*"
                  LogicalName="$(AssemblyName).wwwroot/%(RecursiveDir)%(Filename)%(Extension)" />

资源名用显式 / 分隔符(MyApp.wwwroot/assets/img/logo.svg)。这是刻意的:若按 MSBuild 的默认形式反推, app.min.js 会被推成 app/min.js——静默 404、页面白屏,而资源名里没有任何信息能区分这两种情况。

构建日志会说明它做了什么(在 -v:n 及以上可见,默认的 dotnet build 看不到):

OrielWeb: 已自动内嵌 12 个 wwwroot 资源(LogicalName 用显式 '/' 分隔符,含点文件名不会再被推错路径)。
情形 行为
项目里没有 wwwroot,或里面没有文件 启动时抛异常并说明该怎么配(不是静默白屏)
项目自己声明 wwwroot 资源(带 LogicalName) targets 检测到后跳过自动内嵌(日志会说明),资源名与自动内嵌逐字符一致,不会重复嵌入
项目写了 <EmbeddedResource Include="wwwroot\**\*" />(旧写法,不带 LogicalName) 构建期报错 ORIELWEB001。那种资源名无法区分"目录"与"含点的文件名",库猜不准——删掉那行即可
不想要自动内嵌 <OrielWebEmbeddedAssets>false</OrielWebEmbeddedAssets>,然后自己声明资源(必须带 LogicalName,并用 UseEmbeddedAssets 的第二个参数指定资源前缀)

上面那条"检测到就让位"的逻辑不是多余的:若没有它,同一个文件被两种方式各内嵌一次,页面会同时存在 两条指向同一份文件的资源,MSBuild 还可能直接报 CS1508 已使用资源标识符。

仓库内用 ProjectReference 的工程要自己声明:build/ 与 buildTransitive/ 是 NuGet 包的机制, 只随包分发——ProjectReference 不带它们。所以本仓库的 samples/OrielDemo 自己写了带 LogicalName 的那一行,而零配置只有从包消费时才生效(samples/OrielMinimal 与 tools/verify-pack.ps1 走的 就是那条路)。踩这个坑的症状是启动即 未找到前缀为 '…wwwroot/' 的内嵌资源。

这类工程既然不导入 targets,构建期那道 ORIELWEB001 也拦不到它——运行期会以"检测到旧写法内嵌资源" 报出来并给出正确写法。那条运行期检查就是为这条路径留的安全网。

旧写法为什么被删掉(2026-10-03):不带 LogicalName 时 MSBuild 会把 %(RecursiveDir) 里的分隔符压成 ., 于是 app.min.js 与 app/min.js 生成的资源名完全一样——库只能靠"最后一个 . 是扩展名"去猜, 而猜错的表现是页面 404 白屏、宿主侧毫无异常。既然无法正确解释,就不要装作能解释:这种写法现在在构建期 被 ORIELWEB001 拦下,错误信息里直接给出正确写法。回归证据是 tools/verify-pack.ps1 的场景 B1 (带 LogicalName 的自声明受支持)与 B2(旧写法必须构建失败)。

C# 侧的开关

.UseEmbeddedAssets()                                  // 默认:host 是 app.oriel(即 oriel://app.oriel/),资源前缀按程序集名推断
.UseEmbeddedAssets("myapp.local")                     // 换 host(于是页面在 oriel://myapp.local/ 下)
.UseEmbeddedAssets("app.oriel", "MyApp.wwwroot/")     // 显式指定资源名前缀(自己声明资源时用;必须以 '/' 结尾)

运行期:资源怎么交给页面

三平台统一走自定义 scheme oriel://<host>/…,资源留在程序集里,由各平台的 scheme 处理器按需应答:

oriel://app.oriel/index.html          (默认 host 是 app.oriel)
  • 不落盘:只读介质、容器、受限沙箱里都能跑;也不在用户目录里留一份可被篡改的前端文件。 由此消失的还有一整套麻烦——"启动时递归清空"、"代码里删了资源运行时还能访问到"、 "递归删除前必须校验路径否则删错数据"、"第二个实例先清空了前一个的目录"。
  • 资源是编译进程序集的,所以单文件 / Native AOT 发布后依然取得到。
  • 内容类型(MIME)由库给:自定义 scheme 没有引擎内置的"按扩展名推断"这一步,所以库里有一张 扩展名 → Content-Type 的表(见 src/OrielWeb/Assets/MimeTypes.cs)。 认不出来的扩展名一律 application/octet-stream,不做猜测。

三平台怎么把它交给引擎

平台 通道 页面所在的来源
Windows WebResourceRequested 拦截 + CreateWebResourceResponse(自定义 scheme 经 CoreWebView2CustomSchemeRegistration 登记为 TreatAsSecure) oriel://<host>/…
Linux webkit_web_context_register_uri_scheme + WebKitURISchemeRequest(register_uri_scheme_as_secure) oriel://<host>/…
macOS WKURLSchemeHandler(挂在 WKWebViewConfiguration 上,建 webview 之前) oriel://<host>/…

为什么不用 file://:file:// 是不透明来源,fetch 相对路径、localStorage、安全上下文这些全都要看 引擎脸色,而且三平台并不一致;更麻烦的是它要求资源必须真的在磁盘上(macOS 还要用 allowingReadAccessToURL: 单独把读权限授予那个目录,少了它同目录的 styles.css/app.js 会被拦下, 页面照样空白)。换成有 authority 的自定义 scheme 之后,URL、来源、安全上下文语义三平台一致。

https://<host>/… 仍然接受,作为兼容别名映射到同一份资源(0.2.0 及以前的文档与示例写的都是这个形态, 直接不认会让已有项目白屏)。别名只影响"入参怎么解析":页面真正的来源始终是 oriel://,所以可信前缀也只有 这一条。由此带来的一条实用推论:页面 origin 现在是 oriel://<host>,你的前端代码不要依赖 location.origin 的具体值(开发期连 dev server 时它就是 http://localhost:…)。

URL 怎么解析

oriel://<host>/…(以及兼容别名 https://<host>/…)由 AssetUrl 解析成资源相对路径:

输入 结果
oriel://app.oriel/ 或任何以 / 结尾的空路径 index.html
…/sub/page.html sub/page.html(子目录保持层级)
…/manual-check.html?v=2#top manual-check.html(query 与 fragment 不参与定位)
…/../secret 被 URI 解析折叠成 …/secret——这是 URI 规范的行为,不是防线;真正的边界是资源表查表(查不到就是 404)
…/..%2Fsecret(编码的斜杠不会被折叠) 拒绝
https://app.oriel.example.com/… 不命中(host 精确相等,不做后缀匹配)

默认首页与开发期

窗口的 Url 没设时导航到 oriel://<host>/index.html。

开发期让页面指向 Vite 之类的 dev server 只需设 Url,但要同时放行那个来源——否则页面在库眼里是"外来的", 桥接脚本根本不会安装(见安全与能力模型):

.AddWindow(w => w.WithUrl("http://localhost:5173/"))
.UseCapabilities(c => c.AllowOrigin("http://localhost:5173/"))

没有"指向磁盘上任意目录"的一等入口:内嵌资源是一条 scheme 内的资源表,不是"把某个目录挂上去"。 要加载磁盘上的文件请自己起一个本地 http server,再按上面的方式放行来源。

不在 wwwroot 里的文件

没有机制。要么放进 wwwroot(会被整体内嵌),要么由应用通过 WithUrl 指向外部服务。 MIME 由库按扩展名给出(自定义 scheme 下引擎不再替你推断,表在 src/OrielWeb/Assets/MimeTypes.cs)。

验证账

  • tools/verify-pack.ps1(CI 的 test job 每轮跑)造四种消费形态并断言资源名: A 零配置 → 显式分隔符形式(MyApp.wwwroot/app.min.js,不会被拆成 app/min.js); B1 自带 LogicalName 的自声明 → 与零配置逐字符一致,且 targets 确实跳过了(没有重复嵌入); B2 旧写法(不带 LogicalName) → 构建失败并给出正确写法(错误码 ORIELWEB001); C -p:OrielWebEmbeddedAssets=false → 一条 wwwroot 资源都没有。
  • 单测:AssetUrlTests(默认首页、query/fragment、子目录、oriel:// 与兼容别名、相似域名不命中、 .. 折叠与编码斜杠被拒、MIME 表)、EmbeddedAssetTests(只认显式 / 分隔符这一种约定、自定义前缀必须以 / 结尾、旧形式报错并指路、请求路径归一与穿越拒绝)。
  • 真机:三平台的自检都从内嵌资源加载页面(--selftest nav 覆盖了"导航到不存在的页面应当失败"这类细节, 自定义 scheme 的 404 必须真的失败而不是返回一页空内容)。
  • 事实探针:--selftest scheme 打印页面来源与"来源决定的能力"(isSecureContext、crypto.subtle、 localStorage、相对路径 fetch),并写成一条 GitHub 注解(注解读得出来、日志要 token)。 它断言两条设计承诺——来源必须是 oriel://<host>、相对 fetch 必须取得到资源——其余如实报告: 三平台的自定义 scheme 能力并不对称(macOS 没有"标记为安全上下文"的公开开关),所以这些事实是 量出来的、不是从上游文档推断的。2026-10-03 的实测基线三平台一致: origin=oriel://app.oriel secure=true subtle=true storage=ok fetch=ok:200:949。

能力一览

能力 一句话
内嵌页面资源 wwwroot\**\* 从包消费时零配置自动内嵌;运行期由 oriel://<host>/… 直接应答(不落盘);可用 UseEmbeddedAssets(host) 改 host、OrielWebEmbeddedAssets=false 关闭
无边框窗口 自绘标题栏(拖动与双击由库接管,页面只标一个属性);窗口命令 win.*(最小化/最大化/关闭/拖动/全屏/置顶/选文件/上下文菜单)由库内建,应用一行不写;窗口图标三平台各自落到真正的图标槽
导航与 IPC 前进/后退/刷新、导航事件(含失败原因)、invoke(有回执)/postMessage(单向)/EmitEvent(宿主→页面)/console 转发
安全与能力模型 来源 + 令牌 + 按命令授权三层;不可信来源的页面根本拿不到桥接脚本
主题 / 单实例 深浅色检测与变更事件、第二实例唤醒首实例
平台集成 托盘、系统通知、窗口上下文菜单、Shell(打开外链/定位文件)、开机自启
对话框 打开(可多选)/ 保存 / 选文件夹,支持结构化过滤器与初始目录
文件拖放 拖入文件/文件夹 → 本地路径列表 + 事件
内建右键菜单 默认只留剪切/复制/粘贴,可切平台原样或完全禁用
多窗口 app.CreateWindow(...) 运行时新建窗口;关掉一个窗口应用不退出,app.Windows 自动摘除已关闭的窗口
打包与更新 交给 Velopack(vpk):Windows 出 Setup.exe / .msi / Portable.zip,Linux 出 AppImage,macOS 出 Setup.pkg / Portable.zip;pwsh tools/publish.ps1 -Bundle 一条命令出全套。应用自更新尚未接入(见下)

各项的用法、平台差异与为什么这么设计见 docs/API.md。

打包与分发(Velopack)

安装包与自动更新交给 Velopack:本库不再自带打包器,只把 dotnet publish 的产物交给 vpk。发布仍然只由 dotnet publish 决定(各项目的 AOT 属性不同,库没有立场替你决定)。

dotnet tool install --global vpk

# 发布 + 打包一条命令(版本号从根 Directory.Build.props 的 <Version> 读)
pwsh tools/publish.ps1 -Bundle

# 也可以分开:先发布,再让 vpk 消费产物
pwsh tools/publish.ps1
vpk pack --packId OrielDemo --packVersion <版本> --packDir dist/win-x64 \
  --packTitle "Oriel Demo" --mainExe OrielDemo.exe --icon samples/OrielDemo/app.ico --msi

产物落在 dist/<rid>-releases/。Windows 上的实际清单(Linux 与 macOS 见下):

产物 用途
OrielDemo-win-Setup.exe 安装程序(Velopack 在 Windows 上的主形态)
OrielDemo-win.msi machine-wide 的引导包(vpk --msi)——它只是 Setup.exe 的外壳,不是"per-user 安装"
OrielDemo-win-Portable.zip 免安装的便携版

Linux 上(pwsh tools/wsl_publish.ps1 -Bundle,打包在 WSL 里跑)出的是另一套:

产物 用途
OrielDemo.AppImage 自更新的 AppImage——Linux 上没有独立的安装器,这就是分发形态

AppImage 不需要外部 appimagetool(vpk 自带 appimagekit runtime),也不需要 FUSE 就能自解包运行。

macOS 上(在 macOS 机器上跑,vpk 消费 tools/make-macos-app.sh 组装好的 .app)出的是:

产物 用途
OrielDemo-osx-Setup.pkg 安装包(.pkg)——macOS 上没有独立的 Setup.exe 这类外壳
OrielDemo-osx-Portable.zip 免安装的便携版(解压出 .app)

macOS 上 vpk pack 有两条额外要求,都是实测踩出来的:

  • .app 里必须有 Contents/Resources/ 目录:vpk 要往里写 sq.version,而且假定目录已存在 (缺它时报 DirectoryNotFoundException: … Contents/Resources/sq.version)。make-macos-app.sh 会建好。

  • 不要给 --icon 传 .png:vpk 在 macOS 上认的是 .icns(Windows 用 .ico、Linux 用 .png), 传错格式会直接失败。--icon 本就可选(--help 里只有 --packDir 标了 REQ),图标该由 .app 自己带; 要给图标就从 app.png 生成 .icns 放进 bundle。

  • 只留"能装的东西":更新包 *-full.nupkg(Velopack 的"release")与更新清单 (releases.*.json / assets.*.json / RELEASES*)在打包后即被删除——本仓库不发更新源, 前者与我们的 NuGet 包同名同扩展、后者会指向一个已被删掉的包,留着都只会让人以为有更新可用。 vpk 没有"不产这些"的开关(--noInst / --noPortable 只管安装器与便携版),所以这一步在打包之后做。

  • 不需要外部打包工具:vpk 自带它们(--msi 那一步它内部就用 WiX 模板编译,机器上不必装 WiX)。

  • 版本号只接受三段 semver2:1.2.3.4 会被拒;publish.ps1 -Bundle 会自动把四段截成三段并说明。

  • 签名:本地打包不需要,但分发给用户前应当签名(--signParams / --signTemplate),macOS 还要公证; 没有它们的包在别人机器上会被 SmartScreen / Gatekeeper 拦下。

  • 每个平台各跑一次:vpk 在哪个平台上就跑出那个平台的产物(与 AOT 一样不能跨平台)。

  • 自动更新是应用侧的事:需要在 Main 的第一行调用 VelopackApp.Build().Run(),更新源可以是任何 静态托管。本仓库目前只做打包、还没接更新——没接时 vpk 会警告"入口点没有 VelopackApp.Run()", 那是预期内的(见 Velopack 文档)。

构建与发布

dotnet build OrielWeb.slnx
dotnet test tests/OrielWeb.Tests
node --test tests/bridge/bridge.test.mjs        # 桥接脚本的三平台一致性

pwsh tools/publish.ps1 -Bundle                  # AOT 发布 + Velopack 打包
pwsh tools/wsl_publish.ps1 -Bundle              # 在 WSL 里发 Linux 产物(同上)

tools/ 下是验证与取证脚本(Linux/macOS 的窗口、托盘、通知、拖放等),由 CI 调用; 用法见各脚本头部的注释。

平台运行要求

平台 要求
Windows WebView2 运行时(Win10+ 常已预装;未装时库会引导,或由 OnWebView2RuntimeMissing 自定义提示)。开发/发布需要 .NET 10 SDK 与 MSVC 工具链(AOT)
Linux libwebkit2gtk-4.1、libgtk-3;中文界面另需 CJK 字体(否则渲染成方框)。AOT 发布需要 clang 与 zlib1g-dev
macOS 系统 ≥ 11;产物必须打成 .app 才能运行(WKWebView 需要 bundle identifier)。tools/make-macos-app.sh 会做这件事,vpk pack 再消费它

三平台的详细依赖、已知限制与排查入口见 docs/API.md。

许可

MIT © OrielWeb Contributors。

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 27 10/3/2026
0.2.0 34 10/2/2026
0.1.8 34 10/2/2026
0.1.2 64 9/30/2026
0.1.1 62 9/29/2026
0.1.0 72 9/28/2026