从零开始:C#单文件AOT打包前后端分离项目
<h1 id="一前言">一、前言</h1><p>在 .NET 生态里,官方早就给出过“前后端一把梭”的方案——Blazor Server、Blazor WebAssembly、ASP.NET Core 寄宿 IIS 等。但它们要么强依赖前端独立部署,要么运行时拖家带口,源码裸露、启动速度、跨域配置都是痛点。<br>
反观 Go、Rust 社区,一个 app 文件就能跑完 HTTP 服务 + 静态站点,拷贝即用,编译完连源码影子都看不到。<br>
其实 C# 也能做到。今天这篇,就把“单文件、AOT、前后端全打进 exe”的完整流程拆给你看。</p>
<p><img src="https://img2024.cnblogs.com/blog/3529796/202602/3529796-20260212094222610-1789616460.gif"></p>
<h1 id="二项目准备3-分钟搞定">二、项目准备(3 分钟搞定)</h1>
<p>项目仍然是前后端分离架构,前端可以用Vue、React任意框架实现,然后在.Net中打包在一起(前端代码只是嵌入)。<br>
整体流程非常简单,从零开始创建,以NET10为例:</p>
<ul>
<li><code>dotnet new webapi -n AotDocsify -aot</code>创建一个 <code>ASP.NET Core Web API(native AOT)</code> 项目。</li>
<li>项目根目录建<code>wwwroot</code>文件夹</li>
<li>把前端编译产物(或任何前端 dist)整包拖进 wwwroot(本例中用docsify项目(纯前端markdown文档库),将原nginx中html下的文件复制进来)</li>
</ul>
<h1 id="三把静态文件嵌进-exe">三、把静态文件“嵌”进 exe</h1>
<p>打开<code>.csproj</code>文件,手动增加一行配置,将<code>wwwroot</code>文件夹下所有文件全部设为嵌入的资源。<br>
编译器会把所有文件写进 PE 资源段,AOT 后依旧可见,零反射、零动态生成,放心用。</p>
<pre><code class="language-xml"> <ItemGroup>
<EmbeddedResource Include="wwwroot\**\*" />
</ItemGroup>
</code></pre>
<h1 id="四让-webapplication-认识这些内嵌文件">四、让 WebApplication 认识这些“内嵌”文件</h1>
<p>原后端代码不变,新增静态文件,通过<code>EmbeddedFileProvider</code>将嵌入的资源配置为静态文件。<br>
如果在EmbeddedResource不重新更改文件逻辑名的话,这里就需要填写完整的默认命名空间(程序集名),即"AotDocsify.wwwroot"。这样程序就能通过输入路径,返回相应嵌入文件。</p>
<pre><code class="language-csharp">app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new EmbeddedFileProvider(Assembly.GetExecutingAssembly(), "AotDocsify.wwwroot"),
RequestPath = ""
});
</code></pre>
<p>划重点:<code>Assembly.GetExecutingAssembly().GetManifestResourceStream</code> 在 NativeAOT 里并不是“真正的反射”,它只是对 编译期就已知且被嵌入到 PE 的元数据表 做的一次 常量级查找;只要你在 .csproj 里把 index.html 标记成 EmbeddedResource,NativeAOT 编译器就会把该资源连同它的名字一起写进最终映像,并生成一段 无反射、无动态代码生成的存根代码。因此运行时既不会触发 “反射被裁剪” 的异常,也不会出现 “找不到资源” 的错误。因此本代码可直接编译。</p>
<h1 id="五页面路由配置">五、页面路由配置</h1>
<p>根路径 / 和 SPA 的 404 兜底都要手动接:当输入服务器根地址时,比如<code>localhost:5000</code>,服务器还是会返回404。这是因为我们还未对服务器根目录路由进行配置。传统方法配置仅针对于物理文件路径,因此我们需要额外手动来创建一个根目录的路由。具体代码如下:</p>
<pre><code class="language-csharp"> using var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("AotDocsify.wwwroot.index.html");
if (stream is null) throw new Exception("找不到嵌入的 index.html");
string indexHtml;
using (var reader = new StreamReader(stream))
indexHtml = reader.ReadToEnd();
app.MapGet("/", () => Results.Content(indexHtml, "text/html"));
app.MapFallback(() => Results.Content(indexHtml, "text/html"));
</code></pre>
<p>通过以下代码打印,我们就能看到嵌入挂载的文件及路径:</p>
<pre><code class="language-csharp"> var names = Assembly.GetExecutingAssembly().GetManifestResourceNames();
Console.WriteLine("=== 嵌入资源列表 ===");
foreach (var n in names) Console.WriteLine(n);
// === 嵌入资源列表 ===
// AotDocsify.wwwroot..nojekyll
// AotDocsify.wwwroot.about.contributing.md
// AotDocsify.wwwroot.about.project.md
// AotDocsify.wwwroot.advanced.i18n.md
// AotDocsify.wwwroot.advanced.plugins.md
// AotDocsify.wwwroot.advanced.theme.md
// AotDocsify.wwwroot.examples.code-highlight.md
// AotDocsify.wwwroot.examples.markdown.md
// AotDocsify.wwwroot.examples.math.md
// AotDocsify.wwwroot.features.cover.md
// AotDocsify.wwwroot.features.multipage.md
// AotDocsify.wwwroot.features.navbar.md
// AotDocsify.wwwroot.features.sidebar.md
// AotDocsify.wwwroot.guide.basic-usage.md
// AotDocsify.wwwroot.guide.installation.md
// AotDocsify.wwwroot.guide.quickstart.md
// AotDocsify.wwwroot.README.md
// AotDocsify.wwwroot._sidebar.md
// AotDocsify.wwwroot.index.html
</code></pre>
<p>发布完后,删除除exe外的所有文件,包括<code>wwwroot</code>目录。因为此时所有前端文件均已嵌入打包,本例aot生成的最终exe大小约10mb,运行后,前端后端均可正常工作,且后端所有资源可供前端调用,不存在跨域问题。打开<code>http://localhost:5000</code>出页面,打开<code>http://localhost:5000/todos</code>出接口返回值。</p>
<h1 id="六最后">六、最后</h1>
<p>本文主要介绍了用最少的代码量把“前端 dist + 后端 API” 压成了一个 AOT 可执行文件,部署只剩“复制 → 运行”两步。<br>
如果你在阅读过程中有任何疑问,或者在实际操作中遇到了困难,欢迎随时与我们交流。我们非常期待听到你的反馈和建议,以便我们能够进一步完善内容,帮助更多开发者。请继续关注我们的公众号“萤火初芒”,我们将持续分享更多有趣且实用的技术内容,与大家一起学习交流,共同进步。</p>
<p><img src="https://img2024.cnblogs.com/blog/3529796/202509/3529796-20250915150401305-1035823235.jpg"></p><br><br>
来源:https://www.cnblogs.com/luojin765/p/19607043
頁:
[1]