Compare commits
3 Commits
master
...
c446012199
| Author | SHA1 | Date | |
|---|---|---|---|
|
c446012199
|
|||
|
3053b83bbf
|
|||
|
457316971c
|
@@ -15,9 +15,6 @@ trim_trailing_whitespace = true
|
||||
[project.json]
|
||||
indent_size = 2
|
||||
|
||||
[*.{yaml,yml}]
|
||||
indent_size = 2
|
||||
|
||||
# C# and Visual Basic files
|
||||
[*.{cs,vb}]
|
||||
charset = utf-8-bom
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
name: Build blog docker image
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
Build-Blog-Image:
|
||||
runs-on: archlinux
|
||||
steps:
|
||||
- name: Check out code.
|
||||
uses: https://mirrors.rrricardo.top/actions/checkout.git@v4
|
||||
with:
|
||||
lfs: true
|
||||
- name: Build project.
|
||||
run: |
|
||||
podman pull mcr.azure.cn/dotnet/aspnet:10.0
|
||||
cd YaeBlog
|
||||
pwsh build.ps1 build
|
||||
- name: Workaround to make sure podman-login working.
|
||||
run: |
|
||||
mkdir /root/.docker
|
||||
- name: Login tencent cloud docker registry.
|
||||
uses: https://mirrors.rrricardo.top/actions/podman-login.git@v1
|
||||
with:
|
||||
registry: ccr.ccs.tencentyun.com
|
||||
username: 100044380877
|
||||
password: ${{ secrets.TENCENT_REGISTRY_PASSWORD }}
|
||||
auth_file_path: /etc/containers/auth.json
|
||||
- name: Push docker image.
|
||||
run: podman push ccr.ccs.tencentyun.com/jackfiled/blog:latest
|
||||
Build-Blog-Image:
|
||||
runs-on: archlinux
|
||||
steps:
|
||||
- uses: https://mirrors.rrricardo.top/actions/checkout.git@v4
|
||||
name: Check out code
|
||||
with:
|
||||
lfs: true
|
||||
- name: Build project
|
||||
run: |
|
||||
cd YaeBlog
|
||||
dotnet publish
|
||||
- name: Build docker image
|
||||
run: |
|
||||
cd YaeBlog
|
||||
podman build . -t registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest --build-arg COMMIT_ID=$(git rev-parse --short=10 HEAD)
|
||||
- name: Workaround to make sure podman login succeed
|
||||
run: |
|
||||
mkdir /root/.docker
|
||||
- name: Login aliyun docker registry
|
||||
uses: https://mirrors.rrricardo.top/actions/podman-login.git@v1
|
||||
with:
|
||||
registry: registry.cn-beijing.aliyuncs.com
|
||||
username: 初冬的朝阳
|
||||
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||
auth_file_path: /etc/containers/auth.json
|
||||
- name: Push docker image
|
||||
run: podman push registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace YaeBlog.Tests;
|
||||
|
||||
public class DateTimeOffsetTests
|
||||
{
|
||||
[Fact]
|
||||
public void DateTimeOffsetParseTest()
|
||||
{
|
||||
const string input = "2026-01-04T16:36:36.5629759+08:00";
|
||||
DateTimeOffset time = DateTimeOffset.Parse(input);
|
||||
|
||||
Assert.Equal("2026年01月04日 16:36:36", time.ToString("yyyy年MM月dd日 HH:mm:ss"));
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YaeBlog\YaeBlog.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -10,6 +10,5 @@
|
||||
<File Path="LICENSE" />
|
||||
<File Path="README.md" />
|
||||
</Folder>
|
||||
<Project Path="YaeBlog.Tests/YaeBlog.Tests.csproj" />
|
||||
<Project Path="YaeBlog/YaeBlog.csproj" />
|
||||
</Solution>
|
||||
|
||||
@@ -6,11 +6,5 @@ public interface IEssayScanService
|
||||
{
|
||||
public Task<BlogContents> ScanContents();
|
||||
|
||||
/// <summary>
|
||||
/// 将对应的博客文章保存在磁盘上
|
||||
/// </summary>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="isDraft">指定对应博客文章是否为草稿。因为BlogContent是不可变对象,因此提供该参数以方便publish的实现。</param>
|
||||
/// <returns></returns>
|
||||
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ public sealed class YaeBlogCommand
|
||||
AddWatchCommand(_rootCommand);
|
||||
AddListCommand(_rootCommand);
|
||||
AddNewCommand(_rootCommand);
|
||||
AddUpdateCommand(_rootCommand);
|
||||
AddPublishCommand(_rootCommand);
|
||||
AddScanCommand(_rootCommand);
|
||||
AddCompressCommand(_rootCommand);
|
||||
@@ -47,7 +46,7 @@ public sealed class YaeBlogCommand
|
||||
|
||||
WebApplication application = builder.Build();
|
||||
|
||||
application.MapStaticAssets();
|
||||
application.UseStaticFiles();
|
||||
application.UseAntiforgery();
|
||||
application.UseYaeBlog();
|
||||
|
||||
@@ -77,7 +76,7 @@ public sealed class YaeBlogCommand
|
||||
|
||||
WebApplication application = builder.Build();
|
||||
|
||||
application.MapStaticAssets();
|
||||
application.UseStaticFiles();
|
||||
application.UseAntiforgery();
|
||||
application.UseYaeBlog();
|
||||
|
||||
@@ -110,12 +109,7 @@ public sealed class YaeBlogCommand
|
||||
|
||||
await essayScanService.SaveBlogContent(new BlogContent(
|
||||
new FileInfo(Path.Combine(blogOption.Value.Root, "drafts", file + ".md")),
|
||||
new MarkdownMetadata
|
||||
{
|
||||
Title = file,
|
||||
Date = DateTimeOffset.Now.ToString("o"),
|
||||
UpdateTime = DateTimeOffset.Now.ToString("o")
|
||||
},
|
||||
new MarkdownMetadata { Title = file, Date = DateTime.Now },
|
||||
string.Empty, true, [], []));
|
||||
|
||||
Console.WriteLine($"Created new blog '{file}.");
|
||||
@@ -123,32 +117,6 @@ public sealed class YaeBlogCommand
|
||||
new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
private static void AddUpdateCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command newCommand = new("update", "Update the blog essay.");
|
||||
rootCommand.AddCommand(newCommand);
|
||||
|
||||
Argument<string> filenameArgument = new(name: "blog name", description: "The blog filename to update.");
|
||||
newCommand.AddArgument(filenameArgument);
|
||||
|
||||
newCommand.SetHandler(async (file, _, _, essayScanService) =>
|
||||
{
|
||||
Console.WriteLine("HINT: The update command only consider published blogs.");
|
||||
BlogContents contents = await essayScanService.ScanContents();
|
||||
|
||||
BlogContent? content = contents.Posts.FirstOrDefault(c => c.BlogName == file);
|
||||
if (content is null)
|
||||
{
|
||||
Console.WriteLine($"Target essay {file} is not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o");
|
||||
await essayScanService.SaveBlogContent(content, content.IsDraft);
|
||||
}, filenameArgument,
|
||||
new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
|
||||
}
|
||||
|
||||
private static void AddListCommand(RootCommand rootCommand)
|
||||
{
|
||||
Command command = new("list", "List all blogs");
|
||||
@@ -243,8 +211,7 @@ public sealed class YaeBlogCommand
|
||||
}
|
||||
|
||||
// 设置发布的时间
|
||||
content.Metadata.Date = DateTimeOffset.Now.ToString("o");
|
||||
content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o");
|
||||
content.Metadata.Date = DateTime.Now;
|
||||
|
||||
// 将选中的博客文件复制到posts
|
||||
await essayScanService.SaveBlogContent(content, isDraft: false);
|
||||
|
||||
@@ -5,33 +5,16 @@
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="@Assets["YaeBlog.styles.css"]"/>
|
||||
<link rel="icon" href="@Assets["images/favicon.ico"]"/>
|
||||
<link rel="stylesheet" href="@Assets["tailwind.g.css"]"/>
|
||||
<style>
|
||||
@@font-face {
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url(@Assets["fonts/fa-regular-400.woff2"]) format("woff2") url(@Assets["fonts/fa-regular-400.ttf"]) format("truetype");
|
||||
}
|
||||
|
||||
@@font-face {
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: block;
|
||||
src: url(@Assets["fonts/fa-solid-900.woff2"]) format("woff2"), url(@Assets["fonts/fa-solid-900.ttf"]) format("truetype")
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="YaeBlog.styles.css"/>
|
||||
<link rel="icon" href="images/favicon.ico"/>
|
||||
<link rel="stylesheet" href="globals.css"/>
|
||||
<link rel="stylesheet" href="tailwind.g.css"/>
|
||||
<HeadOutlet/>
|
||||
<ImportMap/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes/>
|
||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||
<Routes/>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<div class="flex flex-col p-3">
|
||||
<div class="text-3xl font-bold py-2">
|
||||
<a href="/blog/essays/@(Essay.FileName)">@(Essay.Title)</a>
|
||||
<a href="/blog/essays/@(Essay.FileName)" target="_blank">@(Essay.Title)</a>
|
||||
</div>
|
||||
|
||||
<div class="p-2 flex flex-row justify-content-start gap-2">
|
||||
@@ -14,7 +14,9 @@
|
||||
@foreach (string key in Essay.Tags)
|
||||
{
|
||||
<div class="text-sky-600">
|
||||
<Anchor Address="@($"/blog/tags/?tagName={UrlEncoder.Default.Encode(key)}")" Text="@($"# {key}")"/>
|
||||
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))">
|
||||
# @key
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@attribute [StreamRendering]
|
||||
|
||||
<main class="container mx-auto flex flex-col min-h-screen">
|
||||
<div class="grid grid-cols-3 mx-3">
|
||||
<div class="md:col-span-2 col-span-3 h-20 flex items-center">
|
||||
<a href="/blog/">
|
||||
<span class="text-blue-600 text-2xl">Ricardo's Blog</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-1 col-span-3 h-20 flex items-center">
|
||||
<div class="flex flex-row w-full px-2 md:justify-center justify-end text-xl gap-3">
|
||||
<Anchor Address="/blog/archives" Text="归档"/>
|
||||
<Anchor Address="/blog/tags/" Text="标签"/>
|
||||
<Anchor Address="/about/" Text="关于" NewPage="@(true)"/>
|
||||
<Anchor Address="/friends" Text="友链" NewPage="@(true)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-2 flex-grow">
|
||||
@Body
|
||||
</div>
|
||||
|
||||
<Foonter/>
|
||||
</main>
|
||||
@@ -1,53 +0,0 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ClientAssetsRestoreCommand Condition="'$(ClientAssesRestoreCommand)' == ''">pnpm install</ClientAssetsRestoreCommand>
|
||||
<ClientAssetsBuildCommand Condition="'$(ClientAssetsBuildCommand)' == ''">pnpm run build</ClientAssetsBuildCommand>
|
||||
<ClientAssetsBuildOutputParameter Condition="'$(ClientAssetsBuildOutputParameter)' == ''">--output</ClientAssetsBuildOutputParameter>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<_RestoreClientAssetsBeforeTargets Condition="'$(TargetFramework)' == ''">DispatchToInnerBuilds</_RestoreClientAssetsBeforeTargets>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="RestoreClientAssets" BeforeTargets="$(_RestoreClientAssetsBeforeTargets)">
|
||||
<Message Importance="high" Text="Running $(ClientAssetsRestoreCommand)"/>
|
||||
<Exec Command="$(ClientAssetsRestoreCommand)"/>
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildClientAssets" DependsOnTargets="RestoreClientAssets" BeforeTargets="AssignTargetPaths">
|
||||
<PropertyGroup>
|
||||
<_ClientAssetsOutputFullPath>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)ClientAssets'))</_ClientAssetsOutputFullPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<MakeDir Directories="$(_ClientAssetsOutputFullPath)"/>
|
||||
<Exec Command="$(ClientAssetsBuildCommand) $(ClientAssetsBuildOutputParameter) $(_ClientAssetsOutputFullPath)"/>
|
||||
|
||||
<ItemGroup>
|
||||
<_ClientAssetsBuildOutput Include="$(IntermediateOutputPath)ClientAssets\**"/>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="DefineClientAssets" AfterTargets="BuildClientAssets" DependsOnTargets="ResolveStaticWebAssetsConfiguration">
|
||||
<ItemGroup>
|
||||
<FileWrites Include="@(_ClientAssetsBuildOutput)"/>
|
||||
</ItemGroup>
|
||||
|
||||
<DefineStaticWebAssets
|
||||
CandidateAssets="@(_ClientAssetsBuildOutput)"
|
||||
SourceId="$(PackageId)"
|
||||
SourceType="Computed"
|
||||
ContentRoot="$(_ClientAssetsOutputFullPath)"
|
||||
BasePath="$(StaticWebAssetBasePath)"
|
||||
>
|
||||
<Output TaskParameter="Assets" ItemName="StaticWebAsset"/>
|
||||
<Output TaskParameter="Assets" ItemName="_ClientAssetsStaticWebAsset"/>
|
||||
</DefineStaticWebAssets>
|
||||
|
||||
<DefineStaticWebAssetEndpoints
|
||||
CandidateAssets="@(_ClientAssetsStaticWebAsset)"
|
||||
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
|
||||
>
|
||||
<Output TaskParameter="Endpoints" ItemName="StaticWebAssetEndpoint" />
|
||||
</DefineStaticWebAssetEndpoints>
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM mcr.azure.cn/dotnet/aspnet:10.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0
|
||||
|
||||
ARG COMMIT_ID
|
||||
ENV COMMIT_ID=${COMMIT_ID}
|
||||
|
||||
WORKDIR /app
|
||||
COPY bin/Release/net10.0/publish/ ./
|
||||
COPY bin/Release/net9.0/publish/ ./
|
||||
COPY source/ ./source/
|
||||
COPY appsettings.json .
|
||||
|
||||
|
||||
44
YaeBlog/Layout/BlogLayout.razor
Normal file
44
YaeBlog/Layout/BlogLayout.razor
Normal file
@@ -0,0 +1,44 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@attribute [StreamRendering]
|
||||
|
||||
<main class="container mx-auto flex flex-col min-h-screen">
|
||||
<div class="grid grid-cols-3 mx-3">
|
||||
<div class="md:col-span-2 col-span-3 h-20 flex items-center">
|
||||
<a href="/blog/">
|
||||
<span class="text-blue-600 text-2xl">Ricardo's Blog</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-1 col-span-3 h-20 flex items-center">
|
||||
<div class="flex flex-row w-full px-2 gap-3 md:justify-center justify-end">
|
||||
<div>
|
||||
<a href="/blog/archives/">
|
||||
<span class="text-xl text-blue-600">归档</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/blog/tags/">
|
||||
<span class="text-xl text-blue-600">标签</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/about/" target="_blank">
|
||||
<span class="text-xl text-blue-600">关于</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/friends/" target="_blank">
|
||||
<span class="text-xl text-blue-600">友链</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-2 flex-grow">
|
||||
@Body
|
||||
</div>
|
||||
|
||||
<Foonter/>
|
||||
</main>
|
||||
@@ -8,9 +8,7 @@ public class BlogEssay : IComparable<BlogEssay>
|
||||
|
||||
public required bool IsDraft { get; init; }
|
||||
|
||||
public required DateTimeOffset PublishTime { get; init; }
|
||||
|
||||
public required DateTimeOffset UpdateTime { get; init; }
|
||||
public required DateTime PublishTime { get; init; }
|
||||
|
||||
public required string Description { get; init; }
|
||||
|
||||
@@ -30,7 +28,6 @@ public class BlogEssay : IComparable<BlogEssay>
|
||||
FileName = FileName,
|
||||
IsDraft = IsDraft,
|
||||
PublishTime = PublishTime,
|
||||
UpdateTime = UpdateTime,
|
||||
Description = Description,
|
||||
WordCount = WordCount,
|
||||
ReadTime = ReadTime,
|
||||
|
||||
@@ -4,9 +4,7 @@ public class MarkdownMetadata
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
|
||||
public string? Date { get; set; }
|
||||
|
||||
public string? UpdateTime { get; set; }
|
||||
public DateTime? Date { get; set; }
|
||||
|
||||
public List<string>? Tags { get; set; }
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@foreach (IGrouping<DateTimeOffset, BlogEssay> group in _essays)
|
||||
@foreach (IGrouping<DateTime, BlogEssay> group in _essays)
|
||||
{
|
||||
<div class="p-2">
|
||||
<div class="flex flex-col">
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="px-4 py-4 flex flex-col">
|
||||
@foreach (BlogEssay essay in group)
|
||||
{
|
||||
<a href="@($"/blog/essays/{essay.FileName}")">
|
||||
<a target="_blank" href="@($"/blog/essays/{essay.FileName}")">
|
||||
<div class="flex flex-row p-2 mx-1 rounded-lg hover:bg-gray-300">
|
||||
<div class="w-20">
|
||||
@(essay.PublishTime.ToString("MM月dd日"))
|
||||
@@ -51,13 +51,13 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private readonly List<IGrouping<DateTimeOffset, BlogEssay>> _essays = [];
|
||||
private readonly List<IGrouping<DateTime, BlogEssay>> _essays = [];
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
_essays.AddRange(from essay in Contents.Essays
|
||||
group essay by new DateTimeOffset(essay.PublishTime.Year, 1, 1,0, 0, 0, TimeSpan.Zero));
|
||||
group essay by new DateTime(essay.PublishTime.Year, 1, 1));
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,6 @@
|
||||
{
|
||||
_page = Page ?? 1;
|
||||
_pageCount = Contents.Count / EssaysPerPage + 1;
|
||||
(_pageCount, int reminder) = int.DivRem(Contents.Count, EssaysPerPage);
|
||||
if (reminder > 0)
|
||||
{
|
||||
_pageCount += 1;
|
||||
}
|
||||
|
||||
if (EssaysPerPage * _page > Contents.Count + EssaysPerPage)
|
||||
{
|
||||
@@ -12,42 +12,47 @@
|
||||
|
||||
<div class="flex flex-col py-8">
|
||||
<div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div>
|
||||
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
|
||||
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
|
||||
<div class="col-auto">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 pt-4 pb-2">
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="font-light">
|
||||
@(_essay!.PublishTime.ToString("yyyy-MM-dd"))
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-4 py-2">
|
||||
@foreach (string tag in _essay!.Tags)
|
||||
{
|
||||
<div class="text-sky-500">
|
||||
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
|
||||
# @(tag)
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="font-light pb-1">
|
||||
发布于: @(_essay.PublishTime.ToString("yyyy年MM月dd日 HH:mm:ss"))
|
||||
</div>
|
||||
|
||||
@if (_essay.UpdateTime != _essay.PublishTime)
|
||||
@foreach (string tag in _essay!.Tags)
|
||||
{
|
||||
<div class="font-light pb-1">
|
||||
更新于: @(_essay.UpdateTime.ToString("yyyy年MM月dd日 HH:mm:ss"))
|
||||
<div class="text-sky-500">
|
||||
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
|
||||
# @(tag)
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="font-light pb-1">
|
||||
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)
|
||||
</div>
|
||||
<div class="px-6 pt-2 pb-4">
|
||||
<div class="font-light">
|
||||
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3">
|
||||
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
|
||||
<div>
|
||||
@((MarkupString)_essay!.HtmlContent)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<LicenseDisclaimer EssayFilename="@BlogKey"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 md:col-span-1">
|
||||
<div class="flex flex-col sticky top-20 px-8 pt-20">
|
||||
<div class="flex flex-col sticky top-0 px-8">
|
||||
<div>
|
||||
<h3 class="text-2xl">文章目录</h3>
|
||||
</div>
|
||||
@@ -88,17 +93,8 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
|
||||
<div>
|
||||
@((MarkupString)_essay!.HtmlContent)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<LicenseDisclaimer EssayFilename="@BlogKey"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="mx-20">
|
||||
<div class="grid grid-cols-3 py-4">
|
||||
<div class="col-span-3 md:col-span-1 p-5 p-lg-0">
|
||||
<img src="@Assets["images/avatar.png"]" alt="Ricardo's Avatar" class="h-auto max-w-full">
|
||||
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full">
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 md:col-span-2">
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<div class="">
|
||||
<p class="text-lg">
|
||||
学过一些基础的计算机知识,略懂一些代码。
|
||||
平平无奇的计算机科学与技术学徒,连微小的贡献都没做。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,7 +16,8 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
|
||||
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||
{
|
||||
BrowsingContext context = new(Configuration.Default);
|
||||
IDocument document = await context.OpenAsync(req => req.Content(essay.HtmlContent));
|
||||
IDocument document = await context.OpenAsync(
|
||||
req => req.Content(essay.HtmlContent));
|
||||
|
||||
ApplyGlobalCssStyles(document);
|
||||
BeatifyTable(document);
|
||||
@@ -35,7 +36,6 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
|
||||
{ "h5", "text-lg font-bold py-1" },
|
||||
{ "p", "p-2" },
|
||||
{ "img", "w-11/12 block mx-auto my-2 rounded-md shadow-md" },
|
||||
{ "a", "text-blue-600" }
|
||||
};
|
||||
|
||||
private void ApplyGlobalCssStyles(IDocument document)
|
||||
@@ -102,33 +102,17 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 美化各种列表元素
|
||||
/// </summary>
|
||||
/// <param name="document"></param>
|
||||
private static void BeatifyList(IDocument document)
|
||||
{
|
||||
foreach (IElement listElement in from e in document.All
|
||||
where e.LocalName is "ol" or "ul"
|
||||
foreach (IElement ulElement in from e in document.All
|
||||
where e.LocalName == "ul"
|
||||
select e)
|
||||
{
|
||||
// 给有序或者无序列表添加不同的样式
|
||||
listElement.ClassList.Add("ml-10");
|
||||
switch (listElement.LocalName)
|
||||
{
|
||||
case "ul":
|
||||
{
|
||||
listElement.ClassList.Add("list-disc");
|
||||
break;
|
||||
}
|
||||
case "ol":
|
||||
{
|
||||
listElement.ClassList.Add("list-decimal");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 首先给<ul>元素添加样式
|
||||
ulElement.ClassList.Add("list-disc ml-10");
|
||||
|
||||
foreach (IElement liElement in from e in listElement.Children
|
||||
|
||||
foreach (IElement liElement in from e in ulElement.Children
|
||||
where e.LocalName == "li"
|
||||
select e)
|
||||
{
|
||||
|
||||
@@ -16,11 +16,11 @@ public sealed class BlogHotReloadService(
|
||||
|
||||
await rendererService.RenderAsync(true);
|
||||
|
||||
Task[] reloadTasks = [WatchFileAsync(stoppingToken)];
|
||||
Task[] reloadTasks = [FileWatchTask(stoppingToken)];
|
||||
await Task.WhenAll(reloadTasks);
|
||||
}
|
||||
|
||||
private async Task WatchFileAsync(CancellationToken token)
|
||||
private async Task FileWatchTask(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
@@ -33,15 +33,6 @@ public sealed class BlogHotReloadService(
|
||||
break;
|
||||
}
|
||||
|
||||
FileInfo changeFileInfo = new(changeFile);
|
||||
|
||||
if (changeFileInfo.Name.StartsWith('.'))
|
||||
{
|
||||
// Ignore dot-started file and directory.
|
||||
logger.LogDebug("Ignore hidden file: {}.", changeFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.LogInformation("{} changed, re-rendering.", changeFile);
|
||||
essayContentService.Clear();
|
||||
await rendererService.RenderAsync(true);
|
||||
|
||||
@@ -109,12 +109,6 @@ public partial class EssayScanService : IEssayScanService
|
||||
{
|
||||
foreach (BlogResult blog in fileContents)
|
||||
{
|
||||
if (blog.BlogContent.Length < 4)
|
||||
{
|
||||
// Even not contains a legal header.
|
||||
continue;
|
||||
}
|
||||
|
||||
int endPos = blog.BlogContent.IndexOf("---", 4, StringComparison.Ordinal);
|
||||
if (!blog.BlogContent.StartsWith("---") || endPos is -1 or 0)
|
||||
{
|
||||
@@ -127,14 +121,14 @@ public partial class EssayScanService : IEssayScanService
|
||||
try
|
||||
{
|
||||
MarkdownMetadata metadata = _yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString);
|
||||
_logger.LogDebug("Scan metadata title: '{title}' for {name}.", metadata.Title, blog.BlogFile.Name);
|
||||
_logger.LogDebug("Scan metadata title: '{}' for {}.", metadata.Title, blog.BlogFile.Name);
|
||||
|
||||
contents.Add(new BlogContent(blog.BlogFile, metadata, blog.BlogContent[(endPos + 3)..], isDraft,
|
||||
blog.Images, blog.NotFoundImages));
|
||||
}
|
||||
catch (YamlException e)
|
||||
{
|
||||
_logger.LogWarning("Failed to parser metadata from {name} due to {exception}, skipping", blog.BlogFile.Name, e);
|
||||
_logger.LogWarning("Failed to parser metadata from {} due to {}, skipping", blog.BlogFile.Name, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Services
|
||||
{
|
||||
public class MarkdownWordCounter
|
||||
{
|
||||
private bool _inCodeBlock;
|
||||
private int _index;
|
||||
private readonly string _content;
|
||||
|
||||
private uint WordCount { get; set; }
|
||||
|
||||
private MarkdownWordCounter(BlogContent content)
|
||||
{
|
||||
_content = content.Content;
|
||||
}
|
||||
|
||||
private void CountWordInner()
|
||||
{
|
||||
while (_index < _content.Length)
|
||||
{
|
||||
if (IsCodeBlockTag())
|
||||
{
|
||||
_inCodeBlock = !_inCodeBlock;
|
||||
}
|
||||
|
||||
if (!_inCodeBlock && char.IsLetterOrDigit(_content, _index))
|
||||
{
|
||||
WordCount += 1;
|
||||
}
|
||||
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCodeBlockTag()
|
||||
{
|
||||
// 首先考虑识别代码块
|
||||
bool outerCodeBlock =
|
||||
Enumerable.Range(0, 3)
|
||||
.Select(i => _index + i < _content.Length && _content.AsSpan().Slice(_index + i, 1) is "`")
|
||||
.All(i => i);
|
||||
|
||||
if (outerCodeBlock)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 然后识别行内代码
|
||||
return _index < _content.Length && _content.AsSpan().Slice(_index, 1) is "`";
|
||||
}
|
||||
|
||||
public static uint CountWord(BlogContent content)
|
||||
{
|
||||
MarkdownWordCounter counter = new(content);
|
||||
counter.CountWordInner();
|
||||
|
||||
return counter.WordCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Services;
|
||||
|
||||
public sealed partial class RendererService(
|
||||
public partial class RendererService(
|
||||
ILogger<RendererService> logger,
|
||||
IEssayScanService essayScanService,
|
||||
MarkdownPipeline markdownPipeline,
|
||||
@@ -38,15 +38,7 @@ public sealed partial class RendererService(
|
||||
List<BlogEssay> essays = [];
|
||||
foreach (BlogContent content in preProcessedContents)
|
||||
{
|
||||
(uint wordCount, string readTime) = GetWordCount(content);
|
||||
DateTimeOffset publishDate = content.Metadata.Date is null
|
||||
? DateTimeOffset.Now
|
||||
: DateTimeOffset.Parse(content.Metadata.Date);
|
||||
// 如果不存在最后的更新时间,就把更新时间设置为发布时间
|
||||
DateTimeOffset updateTime = content.Metadata.UpdateTime is null
|
||||
? publishDate
|
||||
: DateTimeOffset.Parse(content.Metadata.UpdateTime);
|
||||
|
||||
uint wordCount = GetWordCount(content);
|
||||
BlogEssay essay = new()
|
||||
{
|
||||
Title = content.Metadata.Title ?? content.BlogName,
|
||||
@@ -54,9 +46,8 @@ public sealed partial class RendererService(
|
||||
IsDraft = content.IsDraft,
|
||||
Description = GetDescription(content),
|
||||
WordCount = wordCount,
|
||||
ReadTime = readTime,
|
||||
PublishTime = publishDate,
|
||||
UpdateTime = updateTime,
|
||||
ReadTime = CalculateReadTime(wordCount),
|
||||
PublishTime = content.Metadata.Date ?? DateTime.Now,
|
||||
HtmlContent = content.Content
|
||||
};
|
||||
|
||||
@@ -191,21 +182,28 @@ public sealed partial class RendererService(
|
||||
|
||||
string description = builder.ToString();
|
||||
|
||||
logger.LogDebug("Description of {name} is {desc}.", content.BlogName,
|
||||
logger.LogDebug("Description of {} is {}.", content.BlogName,
|
||||
description);
|
||||
return description;
|
||||
}
|
||||
|
||||
private (uint, string) GetWordCount(BlogContent content)
|
||||
private uint GetWordCount(BlogContent content)
|
||||
{
|
||||
uint count = MarkdownWordCounter.CountWord(content);
|
||||
int count = (from c in content.Content
|
||||
where char.IsLetterOrDigit(c)
|
||||
select c).Count();
|
||||
|
||||
logger.LogDebug("Word count of {blog} is {count}", content.BlogName,
|
||||
logger.LogDebug("Word count of {} is {}", content.BlogName,
|
||||
count);
|
||||
// 据说语文教学大纲规定,中国高中生阅读现代文的速度是600字每分钟
|
||||
uint second = count / 10;
|
||||
TimeSpan span = new(0, 0, (int)second);
|
||||
return (uint)count;
|
||||
}
|
||||
|
||||
return (count, span.ToString("mm'分'ss'秒'"));
|
||||
private static string CalculateReadTime(uint wordCount)
|
||||
{
|
||||
// 据说语文教学大纲规定,中国高中生阅读现代文的速度是600字每分钟
|
||||
int second = (int)wordCount / 10;
|
||||
TimeSpan span = new(0, 0, second);
|
||||
|
||||
return span.ToString("mm'分 'ss'秒'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,30 @@
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ClientAssetsRestoreCommand>pnpm install</ClientAssetsRestoreCommand>
|
||||
<ClientAssetsBuildCommand>pwsh build.ps1 tailwind</ClientAssetsBuildCommand>
|
||||
</PropertyGroup>
|
||||
<Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
|
||||
<Message Importance="low" Text="Ensure pnpm is installed..."/>
|
||||
<Exec Command="pnpm --version" ContinueOnError="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
|
||||
</Exec>
|
||||
|
||||
<Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>
|
||||
|
||||
<Message Importance="normal" Text="Installing pakages using pnpm..."/>
|
||||
<Exec Command="pnpm install"/>
|
||||
</Target>
|
||||
|
||||
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild" Condition="'$(_IsPublishing)' == 'yes'">
|
||||
<Message Importance="normal" Text="Generate css files using tailwind..."/>
|
||||
<Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o $(IntermediateOutputPath)tailwind.g.css"/>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="$(IntermediateOutputPath)tailwind.g.css" Visible="false" TargetPath="wwwroot/tailwind.g.css"/>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using YaeBlog
|
||||
@using YaeBlog.Components
|
||||
@@ -37,7 +37,7 @@
|
||||
"Name": "万木长风",
|
||||
"Description": "世界渲染中...",
|
||||
"Link": "https://ryohai.fun",
|
||||
"AvatarImage": "https://ryohai.fun/static/favicons/favicon-32x32.png"
|
||||
"AvatarImage": "https://ryohai.fun/icon.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
#!pwsh
|
||||
|
||||
[cmdletbinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, Position = 0, HelpMessage = "Specify the build target")]
|
||||
[ValidateSet("tailwind", "publish", "compress", "build", "dev", "new")]
|
||||
[string]$Target,
|
||||
[string]$Output = "wwwroot",
|
||||
[string]$Essay,
|
||||
[switch]$Compress
|
||||
)
|
||||
|
||||
begin {
|
||||
Write-Host "Building $Target..."
|
||||
|
||||
if ($Target -eq "publish")
|
||||
{
|
||||
if ($Essay -eq "")
|
||||
{
|
||||
Write-Error "No publish target, please add with --essay argument."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
if ($Target -eq "new")
|
||||
{
|
||||
if ($Essay -eq "")
|
||||
{
|
||||
Write-Error "No new name, please add with --essay argument."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process {
|
||||
function Compress-Image
|
||||
{
|
||||
Write-Host "Compress image assets..."
|
||||
dotnet run -- compress --dry-run
|
||||
$confirm = Read-Host "Really compress images? (y/n)"
|
||||
if ($confirm -notmatch "^[yY]$")
|
||||
{
|
||||
Write-Host "Not compress images."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Do compress image..."
|
||||
dotnet run -- compress
|
||||
|
||||
dotnet run -- scan
|
||||
$confirm = Read-Host "Really delete unused images? (y/n)"
|
||||
if ($confirm -notmatch "^[yY]$")
|
||||
{
|
||||
Write-Host "Not delete images."
|
||||
return
|
||||
}
|
||||
Write-Host "Do delete unused images.."
|
||||
dotnet run -- scan --rm
|
||||
}
|
||||
|
||||
function Build-Image
|
||||
{
|
||||
$commitId = git rev-parse --short=10 HEAD
|
||||
dotnet publish
|
||||
podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$commitId
|
||||
}
|
||||
|
||||
function Start-Develop {
|
||||
Write-Host "Start tailwindcss and dotnet watch servers..."
|
||||
$pnpmProcess = Start-Process pnpm "tailwindcss -i wwwroot/tailwind.css -o obj/Debug/net10.0/ClientAssets/tailwind.g.css -w" `
|
||||
-PassThru
|
||||
|
||||
try
|
||||
{
|
||||
Write-Host "Started pnpm process exit? " $pnpmProcess.HasExited
|
||||
Start-Process dotnet "watch -- serve" -PassThru | Wait-Process
|
||||
}
|
||||
finally
|
||||
{
|
||||
if ($pnpmProcess.HasExited)
|
||||
{
|
||||
Write-Error "pnpm process has exited!"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Kill tailwindcss and dotnet watch servers..."
|
||||
$pnpmProcess | Stop-Process
|
||||
}
|
||||
}
|
||||
|
||||
switch ($Target)
|
||||
{
|
||||
"tailwind" {
|
||||
Write-Host "Build tailwind css into $Output."
|
||||
pnpm tailwindcss -i wwwroot/tailwind.css -o $Output/tailwind.g.css
|
||||
break
|
||||
}
|
||||
"publish" {
|
||||
Write-Host "Publish essay $Essay..."
|
||||
dotnet run -- publish $Essay
|
||||
|
||||
if ($Compress)
|
||||
{
|
||||
Compress-Image
|
||||
}
|
||||
break
|
||||
}
|
||||
"compress" {
|
||||
Compress-Image
|
||||
break
|
||||
}
|
||||
"build" {
|
||||
Build-Image
|
||||
break
|
||||
}
|
||||
"dev" {
|
||||
Start-Develop
|
||||
break
|
||||
}
|
||||
"new" {
|
||||
dotnet run -- new $Essay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
blog:
|
||||
image: registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.blog.rule=Host(`rrricardo.top`) || Host(`www.rrricardo.top`)"
|
||||
- "traefik.http.services.blog.loadbalancer.server.port=8080"
|
||||
- "traefik.http.routers.blog.tls=true"
|
||||
- "traefik.http.routers.blog.tls.certresolver=myresolver"
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
blog:
|
||||
image: registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.blog.rule=Host(`rrricardo.top`) || Host(`www.rrricardo.top`)"
|
||||
- "traefik.http.services.blog.loadbalancer.server.port=8080"
|
||||
- "traefik.http.routers.blog.tls=true"
|
||||
- "traefik.http.routers.blog.tls.certresolver=myresolver"
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
---
|
||||
title: High Performance Computing 25 SP NVIDIA
|
||||
date: 2025-08-31T13:50:42.8639950+08:00
|
||||
date: 2025-04-24T19:02:36.1077330+08:00
|
||||
tags:
|
||||
- 高性能计算
|
||||
- 学习资料
|
||||
---
|
||||
|
||||
|
||||
|
||||
Fxxk you, NVIDIA!
|
||||
|
||||
<!--more-->
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: High Performance Computing 25 SP Quantum Computing
|
||||
date: 2025-06-12T19:26:24.6668760+08:00
|
||||
tags:
|
||||
- 高性能计算
|
||||
- 学习资料
|
||||
---
|
||||
|
||||
|
||||
<!--more-->
|
||||
@@ -1,13 +1,11 @@
|
||||
---
|
||||
title: High Performance Computing 25 SP Non Stored Program Computing
|
||||
date: 2025-08-31T13:51:17.5260660+08:00
|
||||
title: High Performance Computing 2025 SP Non Stored Program Computing
|
||||
date: 2025-05-29T18:29:28.6155560+08:00
|
||||
tags:
|
||||
- 高性能计算
|
||||
- 学习资料
|
||||
---
|
||||
|
||||
|
||||
|
||||
No Von Neumann Machines.
|
||||
|
||||
<!--more-->
|
||||
@@ -62,7 +60,7 @@ There are two types of semi-custom ASICs:
|
||||
|
||||
The Standard cell based ASICs is also called as **Cell-based ASIC(CBIC)**.
|
||||
|
||||

|
||||

|
||||
|
||||
> The *gate* is used a unit to measure the ability of semiconductor to store logical elements.
|
||||
|
||||
@@ -86,7 +84,7 @@ Depending on the structure, the standard PLD can be divided into:
|
||||
- Programmable Logic Array(PLA): A programmable array of AND gates feeding a programmable of OR gates.
|
||||
- Complex Programmable Logic Device(CPLD) and Field Programmable Gate Array(FPGA): complex enough to be called as *architecture*.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -98,7 +96,7 @@ Depending on the structure, the standard PLD can be divided into:
|
||||
|
||||
### FPGA Architecture
|
||||
|
||||

|
||||

|
||||
|
||||
#### Configurable Logic Block(CLB) Architecture
|
||||
|
||||
@@ -118,7 +116,7 @@ LUT is a ram with data width of 1 bit and the content is programmed at power up.
|
||||
|
||||
The below figure shows LUT working:
|
||||
|
||||

|
||||

|
||||
|
||||
The configuration memory holds the output of truth table entries, so that when the FPGA is restarting it will run with the same *program*.
|
||||
|
||||
@@ -128,7 +126,7 @@ And as the truth table entries are just bits, the program of FPGA is called as *
|
||||
|
||||
Let the input signal as address, the LUT will be configured as a RAM. Normally, LUT mode performs read operations, the address decoders can generate clock signal to latches for writing operation.
|
||||
|
||||

|
||||

|
||||
|
||||
#### Routing Architecture
|
||||
|
||||
@@ -136,7 +134,7 @@ The logic blocks are connected to each though programmable routing network. And
|
||||
|
||||
Horizontal and vertical mesh or wire segments interconnection by programmable switches called programmable interconnect points(PIPs).
|
||||
|
||||

|
||||

|
||||
|
||||
These PIPs are implemented using a transmission gate controlled by a memory bits from the configuration memory.
|
||||
|
||||
@@ -148,7 +146,7 @@ Several types of PIPs are used in the FPGA:
|
||||
- Non-decoded MUX: n wire segments each with a configuration bit.
|
||||
- Compound cross-point: 6 breakpoint PIPs and can isolate two isolated signal nets.
|
||||
|
||||

|
||||

|
||||
|
||||
#### Input/Output Architecture
|
||||
|
||||
@@ -160,7 +158,7 @@ The programmable Input/Output cells consists of three parts:
|
||||
- Routing resources.
|
||||
- Programmable I/O voltage and current levels.
|
||||
|
||||

|
||||

|
||||
|
||||
#### Fine-grained and Coarse-grained Architecture
|
||||
|
||||
@@ -188,9 +186,9 @@ Three types of interconnected devices have been commonly used to connect there w
|
||||
|
||||
### FPGA Design Flow
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
The FPGA configuration techniques contains:
|
||||
|
||||
@@ -224,7 +222,7 @@ The OpenCL is not an traditional hardare description language. And OpenCL needs
|
||||
|
||||
The follow figure shows how the OpenCL-FPGA compiler turns an vector adding function into the circuit.
|
||||
|
||||

|
||||

|
||||
|
||||
The compiler generates three stages for this function:
|
||||
|
||||
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250815093113115.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250815093113115.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817183832472.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817183832472.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817184419856.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817184419856.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817185111521.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817185111521.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817185859510.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817185859510.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817192006784.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817192006784.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817194355228.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817194355228.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817195139631.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817195139631.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817195714935.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817195714935.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817200350750.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250817200350750.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250829210329225.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/drafts/hpc-2025-non-stored-program-computing/image-20250829210329225.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: High Performance Computing 25 SP OpenCL Programming
|
||||
date: 2025-08-31T13:51:02.0181970+08:00
|
||||
title: High Performance Computing 2025 SP OpenCL Programming
|
||||
date: 2025-05-29T18:29:14.8444660+08:00
|
||||
tags:
|
||||
- 高性能计算
|
||||
- 学习资料
|
||||
---
|
||||
|
||||
|
||||
Open Computing Language.
|
||||
|
||||
<!--more-->
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: High Performance Computing 25 SP Potpourri
|
||||
date: 2025-08-31T13:51:29.8809980+08:00
|
||||
date: 2025-06-12T18:45:49.2698190+08:00
|
||||
tags:
|
||||
- 高性能计算
|
||||
- 学习资料
|
||||
---
|
||||
|
||||
|
||||
Potpourri has a good taste.
|
||||
|
||||
<!--more-->
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: High Performance Computing 25 SP Programming CUDA
|
||||
date: 2025-08-31T13:50:53.6891520+08:00
|
||||
title: High Performance Computing 2025 SP Programming CUDA
|
||||
date: 2025-05-15T19:13:48.8893010+08:00
|
||||
tags:
|
||||
- 高性能计算
|
||||
- 学习资料
|
||||
---
|
||||
|
||||
|
||||
Compute Unified Device Architecture
|
||||
|
||||
<!--more-->
|
||||
@@ -1,224 +0,0 @@
|
||||
---
|
||||
title: ASP.NET Core中的静态Web资产
|
||||
date: 2026-01-04T16:36:36.5629759+08:00
|
||||
tags:
|
||||
- 技术笔记
|
||||
- dotnet
|
||||
- ASP.NET Core
|
||||
---
|
||||
|
||||
|
||||
Web服务器应该如何扫描与提供静态Web文件,尤其是在考虑到缓存、压缩的情况下,还需要正确的处理开发环境和部署环境之间的差异?让我们来看看ASP.NET Core是如何处理这个问题的。以及如何将通过其他工具(例如`pnpm`)生成的前端资产文件集成到ASP.NET Core中。
|
||||
|
||||
<!--more-->
|
||||
|
||||
### 引言——Blazor开发中的静态Web资源
|
||||
|
||||
Blazor是ASP.NET Core中~~新推出的~~Web应用程序开发框架,通过一系列精巧的设计实现了使用HTML和C#编写运行在浏览器中的应用程序,避免了使用丑陋的JavaScript。但是现代的前端开发生态几乎都建立在JavaScript之上,尤其是考虑在JavaScript在很长的一段时间都是浏览器唯一支持的脚本语言,在Blazor项目开发的过程中必然会遇到一些只能编写JavaScript才能解决的问题。同时,一系列的现代前端工具,例如[tailwindcss](https://tailwindcss.com/),提供了更加优秀的前端开发体验,但是这些都基于NodeJS和NPM等前端工具。以上的前端生态引入了一个问题,如何在MSBuild驱动的Blazor应用构建流程中自然地运行前端工具链和ASP.NET Core支持的服务器部署生成的静态资源?
|
||||
|
||||
Blazor目前提供了一个入口简洁但是功能丰富的静态Web资源提供功能。在使用默认应用目录的情况下,项目将会提供一个`wwwroot`文件夹,这个文件夹中的内容将可以从`/`直接寻址。为了提升前端静态文件的使用体验,该文件夹下的文件将会经过一个复杂的管道:
|
||||
|
||||
- 在构建扫描到这些资产文件之后,MSBuild将会给静态文件加上内容指纹,以防止重复使用旧文件。资源还会被压缩,以减少资产交付的时间。
|
||||
- 在运行时,所发现的资产文件将会作为终结点公开,并添加上合适的缓存头和内容类型头。在设置`ETtag`,`Last-Modified`和`Content-Type`头之后,浏览器将可以合理的缓存这些静态文件直到应用更新。
|
||||
|
||||
该静态文件功能还需要适应应用程序的部署状态:
|
||||
|
||||
- 在开发时,或者说运行`dotnet run`和`dotnet build`时,该功能需要将对应的静态文件终结点URL映射到磁盘上存储的实际静态文件上,就像它们实际上就在`wwwroot`文件夹中一样。考虑到实际上开发过程中会用到Blazor内部的资产文件`blazor.web.js`,引用项目中的资产文件等等,这实际上一个相当复杂的检测-映射流程。
|
||||
- 在发布时,或者说运行`dotnet publish`时,该功能需要收集所有需要的静态文件并复制到最终发布文件夹的`wwwroot`文件夹之下。
|
||||
|
||||
### Microsoft.AspNetCore.ClientAssets
|
||||
|
||||
在默认的应用模板下,如果需要使用其他的现代前端工具生成静态资产文件,最简单的方法就是手动或者编写MSBuild目标(Target)生成资产文件并放在`wwwroot`文件夹中。但是这个方法存在着如下几个问题:
|
||||
|
||||
- 开发者需要编写 MSBuild 目标(targets)来调用他们的工具。
|
||||
- 开发者通常没有在构建过程的恰当时机执行其自定义目标。
|
||||
- 开发者将工具生成的输出文件放入应用的 wwwroot 文件夹中,这会导致这些文件在下一次构建时被误认为是输入文件。
|
||||
- 开发者没有为这些工具正确声明输入和输出依赖,导致即使输出文件已是最新,工具仍会重复运行,从而增加构建耗时。
|
||||
|
||||
面对这些问题,M$提供了一个Alpha状态的库`Microsoft.AspNetCore.ClientAssets`来解决这个问题。不幸的是,这个库已经因为年久失修(上一次[更新](https://github.com/aspnet/AspLabs/pull/572)是在3年前,引入对于.NET 7的支持),在.NET 9引入新的静态资产部署管线之后,使用会直接报错了。
|
||||
|
||||
^^ 相关的Issues链接:[#38445](https://github.com/dotnet/aspnetcore/issues/38445),[#62925](https://github.com/dotnet/aspnetcore/issues/62925)
|
||||
|
||||
为了良好地解决如上的问题,我们需要首先了解一下ASP.NET Core中静态资产文件的构建和部署过程。
|
||||
|
||||
### StaticWebAssetsSdk
|
||||
|
||||
在.NET中,构建静态资产文件的相关代码在[dotnet/sdk](https://github.com/dotnet/sdk)仓库中,称作`StaticWebAssetsSdk`。
|
||||
|
||||
静态 Web 资源会接管应用程序 wwwroot 文件夹中的内容项,并全面管理这些内容。在开发过程中,系统会生成一个 JSON 清单(manifest),其中包含以下信息:
|
||||
|
||||
- 版本号(version number):标识清单格式的版本。
|
||||
|
||||
- 清单内容的哈希值(hash):用于判断清单内容是否发生变化。
|
||||
|
||||
- 库的包 ID(library package id):用于区分当前项目与其他项目所提供的资源。
|
||||
|
||||
- 库的资源基础路径(asset base path):在将其他库的路径应用“发现模式”时,用于确定要添加的基础路径。
|
||||
|
||||
- 清单模式(manifest mode):定义来自特定项目的资源在构建和发布时应如何处理。
|
||||
|
||||
- 相关项目清单及其哈希值的列表:用于判断自清单生成以来,项目引用是否发生变化,或是否有清单被更新。
|
||||
|
||||
- “发现模式”(discovery patterns)列表:用于在清单构建完成后,有选择性地在运行时提供某些资源。例如,可以使用如下模式:
|
||||
|
||||
```json
|
||||
{ "Path": "<Project>/Pages", "BasePath": "_content/Something", "Pattern": "**/*.js" }
|
||||
```
|
||||
|
||||
表示仅提供该目录下扩展名为`js`的文件。如果有人添加了图片或其他文件,它们将不会被提供。(这一点很重要,因为这些文件并不符合任何资源规则,也不会包含在发布输出目录中。)
|
||||
|
||||
- 构建/发布过程中生成的静态 Web 资源列表。
|
||||
|
||||
系统会生成两套清单:**构建清单(build manifest)** 和 **发布清单(publish manifest)**:
|
||||
|
||||
- **构建清单**在构建过程中生成,用于开发阶段,使资源表现得如同它们属于应用程序本身。
|
||||
- **发布清单**在发布过程中生成,记录了发布阶段对资源执行的所有转换操作。
|
||||
|
||||
资源可以在构建阶段或发布阶段定义,并可在任意阶段被标记为“仅构建”或“仅发布”。例如,你可以有两个文件:一个用于开发,一个用于发布,但它们都需要通过相同的 URL 路径提供服务。`service-worker.js` 就是一个典型例子。
|
||||
|
||||
**构建时清单**由项目中发现的资源以及来自被引用项目和包的资源共同组成。
|
||||
**发布清单**则以构建清单为基础,过滤掉仅用于构建的文件,并包含在发布过程中对这些文件执行的所有转换(如链接、打包、压缩等)。
|
||||
|
||||
这种机制使得在发布阶段可以执行如链接(Linking)、打包(Bundling)、压缩(Compression)等优化操作。被引用的项目也会生成自己的发布清单,其内容会在发布过程中与当前项目的清单合并。同时,在发布过程中,我们仍会保留被引用项目的原始构建清单,以便应用程序可以选择忽略被引用项目的发布资源,并对整个依赖传递闭包中的资源执行全局优化。例如,一个类库在发布时可能生成一个压缩后的 JS 包,而主应用可以选择不使用多个独立的包,而是收集所有原始构建阶段的资源,生成一个统一的包。通常情况下,构建清单和发布清单内容相同,除非存在仅在发布阶段才应用的转换。
|
||||
|
||||
每份清单中会列出在构建/发布过程中生成或计算出的所有资源及其属性。这些属性包括:
|
||||
|
||||
- **Identity**:资源的唯一标识(文件的完整路径)。
|
||||
- **SourceType**:资源类型('Discovered', 'Computed', 'Project', 'Package')。
|
||||
- **ContentRoot**:开发阶段资源暴露的原始路径。
|
||||
- **BasePath**:资源暴露的基础路径。
|
||||
- **RelativePath**:资源的相对路径。
|
||||
- **AssetKind**:资源用途('Build', 'Publish', 'All'),由 `CopyToOutputDirectory` / `CopyToPublishDirectory` 推断得出。
|
||||
- **AssetMode**:资源作用范围('CurrentProject', 'Reference', 'All')。
|
||||
- **AssetRole**:资源角色('Primary', 'Related', 'Alternative')。
|
||||
- **AssetMergeSource**:当资源被嵌入到其他 TFM(目标框架)时的来源。
|
||||
- **AssetMergeBehavior**:当同一 TFM 中出现资源冲突时的合并行为。
|
||||
- **RelatedAsset**:当前资源所依赖的主资源的 Identity。
|
||||
- **AssetTraitName**:区分相关或替代资源与主资源的特征名称(如语言、编码格式等)。
|
||||
- **AssetTraitValue**:该特征的具体值。
|
||||
- **CopyToBuildDirectory**:与 Content 项一致(如 PreserveNewest、Always)。
|
||||
- **CopyToPublishDirectory**:与 Content 项一致。
|
||||
- **OriginalItemSpec**:定义该资源的原始项规范。
|
||||
|
||||
关于资源在不同场景下的使用(作为主项目的一部分,或作为被引用项目的一部分),有三种可能的选项:
|
||||
|
||||
- **All**:资源在所有情况下都应被使用。
|
||||
- **Root**:资源仅在当前项目作为主项目构建时使用。
|
||||
- **Reference**:资源仅在当前项目被其他项目引用时使用。
|
||||
|
||||
例如,CSS 隔离(CSS isolation)生成的两个包:
|
||||
|
||||
- `<<Project>>.styles.css` 是 **Root** 资源,仅在作为主项目时使用。
|
||||
- `<<Project>>.lib.bundle.css` 是 **Reference** 资源,仅在被其他项目引用时使用。
|
||||
|
||||
除了上述三种使用模式,项目还需定义其在构建和发布过程中如何处理清单中的文件。对此有三种模式:
|
||||
|
||||
- **Default**:项目在发布时将所有内容复制到发布输出目录,但当被其他项目引用时不做任何操作,而是期望引用方负责处理静态 Web 资源的发布。
|
||||
→ 通常用于类库(class libraries)。
|
||||
- **Root**:项目被视为静态 Web 资源的“根”,即使被引用,其资源也应像主项目一样被处理(例如,不复制传递依赖资源,而只复制 Root 资源)。
|
||||
→ 用于如 Blazor WebAssembly 托管项目(被 ASP.NET Core 主机项目引用,但资源应视为根项目)。
|
||||
- **Isolated**:与 Root 类似,但引用项目完全不知道静态 Web 资源的存在;当前项目会自行在发布时设置处理程序,将资源复制到正确位置。
|
||||
→ 用于如 Blazor 桌面应用,将静态 Web 资源自动纳入 `GetCopyToPublishDirectoryItems`,使引用方无需了解静态 Web 资源机制。
|
||||
|
||||
关于资源类型,静态 Web 资源可分为四类:
|
||||
|
||||
- **Discovered assets**:从项目中已有项(如 Content、None 等)中发现的资源。
|
||||
- **Computed assets**:在构建过程中生成、需要在构建时复制到最终位置的资源。
|
||||
- **Project**:来自被引用项目的资源。当合并被引用项目的清单时,其 Discovered 和 Computed 资源会转换为 Project 类型。
|
||||
- **Package**:来自被引用 NuGet 包的资源。
|
||||
|
||||
关于资源角色(Asset Role),有三种:
|
||||
|
||||
- **Primary(主资源)**:表示可通过其相对路径直接访问的资源。大多数资源属于此类。
|
||||
- **Related(相关资源)**:表示与另一个资源相关,但两者都可通过各自的相对路径独立访问。
|
||||
- **Alternative(替代资源)**:表示是另一个资源的替代形式,例如预压缩版本或不同格式版本。通常应通过与主资源相同的相对路径提供(具体实现由运行时决定)。静态 Web 资源层仅记录这种关系。
|
||||
|
||||
对于 Related 和 Alternative 资源,其 `RelatedAsset` 属性指向其所依赖的主资源。这种依赖链可多层嵌套,以表示一个资源的多种表示形式。静态 Web 资源仅记录这些信息,具体如何使用由 MSBuild 目标决定。
|
||||
|
||||
`AssetTraitName` 和 `AssetTraitValue` 用于区分相关/替代资源与其主资源。例如:
|
||||
|
||||
- 对于全球化程序集,可记录程序集的文化(culture);
|
||||
- 对于压缩资源,可记录编码方式(如 gzip、brotli)。
|
||||
|
||||
下图展示了在构建过程中被调用的MSBuild Target:
|
||||
|
||||

|
||||
|
||||
Sdk提供了一些重要的MSBuild Task供程序员调用:
|
||||
|
||||
- `DefineStaticWebAssets`:该Task扫描提供了一系列候选的资产文件并构建一个*标准化的*静态资产对象;
|
||||
- `DefineStaticWebAssetEndpoints`:该Task以上一个任务输出的静态资产对象为输入,输出每个静态资产文件的Web终结点;
|
||||
|
||||
在构建中过程中`GenerateStaticWebAssetsManifest`和`GenerateStaticWebAssetsDevelopmentManifest`、`GenerateStaticWebAssetEndpointsManifest`等几个任务会产生一个重要的清单文件,这些文件通常存放在*obj*文件夹中,名称为`staticwebassets.*.json`。其中一个较为重要的清单文件是`staticwebassets.development.json`,其存储了所有的静态资产文件和对应的存储目录。这个文件在构建的过程中会被复制到输出目录`bin`中,名称为`$(PackageId).staticwebassets.runtime.json`。这个文件将会在生产模式下被静态资产中间件读取,作为建立静态文件终结点到实际物理文件的索引。这个文件也为需要调试`StaticWebAssetsSdk`的程序员提供了重要的调试信息,是解决ASP.NET Core中静态资产问题的不二法门。
|
||||
|
||||
### 解决方案
|
||||
|
||||
现在已经充分了解了`StaticWebAssetsSdk`,可以来设计在MSBuild中集成前端工具并生成最终静态资产文件的管线了。
|
||||
|
||||
首先来研究过程的步骤,`npm`或者其类似物也使用类似于MSBuild的先还原再构建两步,首先需要安装程序中使用到的包,然后在运行构建指令构建对应的静态文件,构建完成之后还需要将构建产物交给MSBuild中的静态资产处理管线进行进一步的处理。因此设计如下的三个步骤:
|
||||
|
||||
1. `RestoreClientAssets`,这个Target需要运行`npm install`或者类似的指令安装依赖包;
|
||||
2. `BuildClientAssets`,这个Target运行`npm run build`或者类似的指令构建项目;
|
||||
3. `DefineClientAssets`,这个Target调用`DefineStaticWebAssets`等Task声明静态资产文件。
|
||||
|
||||
确定好生成步骤之后,声明一下会在生成过程中会用到的,可以提供给用户自定义的属性。安装和构建的相应软件包肯定是需要提供给用户自定义的。在一般情况下,前端工具链把将静态文件生成到`dist`文件夹中。为了符合MSBuild的惯例,这里将中间静态文件生成到*obj*文件夹下的`ClientAssets`文件夹中。为了实现这一点,构建过程中的指令就需要支持一个指定生成目录的参数,这个参数也作为一个属性暴露给用户可以自定义。这里就形成了下面三个提供给用户自定义的参数。
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<ClientAssetsRestoreCommand Condition="'$(ClientAssesRestoreCommand)' == ''">pnpm install</ClientAssetsRestoreCommand>
|
||||
<ClientAssetsBuildCommand Condition="'$(ClientAssetsBuildCommand)' == ''">pnpm run build</ClientAssetsBuildCommand>
|
||||
<ClientAssetsBuildOutputParameter Condition="'$(ClientAssetsBuildOutputParameter)' == ''">--output</ClientAssetsBuildOutputParameter>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
最终就是完成的构建原始代码了。第一个运行的构建目标`RestoreClientAssets`将会在`DispatchToInnerBuilds`任务运行之后运行,这个目标是MSBuild构建管线中一个不论是针对单架构生成还是多架构生成都只会运行一次的目标,这样在项目需要同时编译到.NET 8和.NET 10的情况下,仍然只会运行前端的安装命令一次。`BuildClientAssets`目标紧接着`RestoreClientAssets`目标的运行而运行,并将所有生成的前端文件添加到`_ClientAssetsBuildOutput`项中。最终的`DefineClientAssets`目标在负责解析项目中的所有静态文件的目标`ResolveWebAssetsConfiguration`运行之前运行,调用`DefineStaticWebAssets`和`DefineStaticWebAssetEndpoints`将前面生成的所有前端静态文件添加到MSBuild的静态文件处理管线中。
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<_RestoreClientAssetsBeforeTargets Condition="'$(TargetFramework)' == ''">DispatchToInnerBuilds</_RestoreClientAssetsBeforeTargets>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="RestoreClientAssets" BeforeTargets="$(_RestoreClientAssetsBeforeTargets)">
|
||||
<Message Importance="high" Text="Running $(ClientAssetsRestoreCommand)"/>
|
||||
<Exec Command="$(ClientAssetsRestoreCommand)"/>
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildClientAssets" DependsOnTargets="RestoreClientAssets" BeforeTargets="AssignTargetPaths">
|
||||
<PropertyGroup>
|
||||
<_ClientAssetsOutputFullPath>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)ClientAssets'))</_ClientAssetsOutputFullPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<MakeDir Directories="$(_ClientAssetsOutputFullPath"/>
|
||||
<Exec Command="$(ClientAssetsBuildCommand) -- $(ClientAssetsBuildOutputParameter) $(_ClientAssetsOutputFullPath)"/>
|
||||
|
||||
<ItemGroup>
|
||||
<_ClientAssetsBuildOutput Include="$(IntermediateOutputPath)ClientAssets\**"/>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="DefineClientAssets" AfterTargets="BuildClientAssets" DependsOnTargets="ResolveWebAssetsConfiguration">
|
||||
<ItemGroup>
|
||||
<FileWrites Include="@(_ClientAssetsBuildOutput)"/>
|
||||
</ItemGroup>
|
||||
|
||||
<DefineStaticWebAssets
|
||||
CandidateAssets="@(_ClientAssetsBuildOutput)"
|
||||
SourceId="$(PackageId)"
|
||||
SourceType="Computed"
|
||||
ContentRoot="$(_ClientAssetsOutputFullPath)"
|
||||
BasePath="$(StaticWebAssetBasePath)"
|
||||
>
|
||||
<Output TaskParameter="Assets" ItemName="StaticWebAsset"/>
|
||||
<Output TaskParameter="Assets" ItemName="_ClientAssetsStaticWebAsset"/>
|
||||
</DefineStaticWebAssets>
|
||||
|
||||
<DefineStaticWebAssetEndpoints
|
||||
CandidateAssets="@(_ClientAssetsStaticWebAsset)"
|
||||
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
|
||||
>
|
||||
<Output TaskParameter="Endpoints" ItemName="StaticWebAssetEndpoint" />
|
||||
</DefineStaticWebAssetEndpoints>
|
||||
</Target>
|
||||
```
|
||||
|
||||
为了测试如下的代码,可以在项目中新建一个`Directory.Build.targets`文件,将上述的内容复制进去进行测试,当然别忘了用`<Project>`标签包裹这一切。
|
||||
BIN
YaeBlog/source/posts/aspnetcore-swa/image-20251231225433184.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/aspnetcore-swa/image-20251231225433184.webp
(Stored with Git LFS)
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user