ADD PWA
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const projectRoot = fileURLToPath(new URL("../", import.meta.url));
|
||||
const distRoot = path.join(projectRoot, "dist");
|
||||
const serviceWorkerPath = path.join(distRoot, "sw.js");
|
||||
const precacheExtensions = new Set([".html", ".css", ".js", ".webmanifest"]);
|
||||
const precacheAssets = new Set([
|
||||
"assets/campus-hero.png",
|
||||
"icons/apple-touch-icon.png",
|
||||
"icons/pwa-192.png",
|
||||
"icons/pwa-512.png"
|
||||
]);
|
||||
|
||||
const listFiles = async (directory) => {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const files = await Promise.all(
|
||||
entries.map((entry) => {
|
||||
const entryPath = path.join(directory, entry.name);
|
||||
return entry.isDirectory() ? listFiles(entryPath) : entryPath;
|
||||
})
|
||||
);
|
||||
|
||||
return files.flat();
|
||||
};
|
||||
|
||||
const toRelativePath = (filePath) =>
|
||||
path.relative(distRoot, filePath).split(path.sep).join("/");
|
||||
|
||||
const toPublicUrl = (relativePath) => {
|
||||
if (relativePath === "index.html") {
|
||||
return "/";
|
||||
}
|
||||
|
||||
if (relativePath.endsWith("/index.html")) {
|
||||
return `/${relativePath.slice(0, -"index.html".length)}`;
|
||||
}
|
||||
|
||||
return `/${relativePath}`;
|
||||
};
|
||||
|
||||
const replaceBetweenMarkers = (source, startMarker, endMarker, replacement) => {
|
||||
const startIndex = source.indexOf(startMarker);
|
||||
const endIndex = source.indexOf(endMarker);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
||||
throw new Error(`Service Worker 缺少构建标记:${startMarker} / ${endMarker}`);
|
||||
}
|
||||
|
||||
const contentStart = startIndex + startMarker.length;
|
||||
return `${source.slice(0, contentStart)}\n${replacement}\n ${source.slice(endIndex)}`;
|
||||
};
|
||||
|
||||
const allFiles = await listFiles(distRoot);
|
||||
const precacheFiles = allFiles
|
||||
.map((filePath) => ({ filePath, relativePath: toRelativePath(filePath) }))
|
||||
.filter(
|
||||
({ relativePath }) =>
|
||||
relativePath !== "sw.js" &&
|
||||
(precacheExtensions.has(path.extname(relativePath)) || precacheAssets.has(relativePath))
|
||||
)
|
||||
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
||||
|
||||
const precacheUrls = [...new Set(precacheFiles.map(({ relativePath }) => toPublicUrl(relativePath)))].sort(
|
||||
(left, right) => (left === "/" ? -1 : right === "/" ? 1 : left.localeCompare(right))
|
||||
);
|
||||
|
||||
const versionHash = createHash("sha256");
|
||||
for (const { filePath, relativePath } of precacheFiles) {
|
||||
versionHash.update(relativePath);
|
||||
versionHash.update(await readFile(filePath));
|
||||
}
|
||||
|
||||
const cacheVersion = versionHash.digest("hex").slice(0, 12);
|
||||
const startMarker = "/* __PRECACHE_URLS_START__ */";
|
||||
const endMarker = "/* __PRECACHE_URLS_END__ */";
|
||||
const urlLines = precacheUrls.map((url, index) => {
|
||||
const suffix = index === precacheUrls.length - 1 ? "" : ",";
|
||||
return ` ${JSON.stringify(url)}${suffix}`;
|
||||
});
|
||||
|
||||
let serviceWorker = await readFile(serviceWorkerPath, "utf8");
|
||||
serviceWorker = serviceWorker.replace(
|
||||
/const CACHE_VERSION = "dev"; \/\/ __CACHE_VERSION__/,
|
||||
`const CACHE_VERSION = ${JSON.stringify(cacheVersion)}; // __CACHE_VERSION__`
|
||||
);
|
||||
serviceWorker = replaceBetweenMarkers(serviceWorker, startMarker, endMarker, urlLines.join("\n"));
|
||||
|
||||
await writeFile(serviceWorkerPath, serviceWorker, "utf8");
|
||||
console.log(`[pwa] 已生成 ${precacheUrls.length} 项预缓存,版本 ${cacheVersion}`);
|
||||
@@ -0,0 +1,77 @@
|
||||
param(
|
||||
[string]$ImportSource
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$projectRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||
$sourceDirectory = Join-Path $projectRoot 'src\assets'
|
||||
$sourcePath = Join-Path $sourceDirectory 'pwa-icon-source.png'
|
||||
$iconDirectory = Join-Path $projectRoot 'public\icons'
|
||||
|
||||
New-Item -ItemType Directory -Path $sourceDirectory -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $iconDirectory -Force | Out-Null
|
||||
|
||||
if ($ImportSource) {
|
||||
$resolvedImportSource = (Resolve-Path -LiteralPath $ImportSource).Path
|
||||
Copy-Item -LiteralPath $resolvedImportSource -Destination $sourcePath -Force
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) {
|
||||
throw "找不到图标源文件:$sourcePath"
|
||||
}
|
||||
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
function Export-SquareIcon {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[System.Drawing.Image]$SourceImage,
|
||||
[Parameter(Mandatory)]
|
||||
[int]$Size,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$OutputPath
|
||||
)
|
||||
|
||||
$side = [Math]::Min($SourceImage.Width, $SourceImage.Height)
|
||||
$sourceX = [Math]::Floor(($SourceImage.Width - $side) / 2)
|
||||
$sourceY = [Math]::Floor(($SourceImage.Height - $side) / 2)
|
||||
$sourceRectangle = [System.Drawing.Rectangle]::new($sourceX, $sourceY, $side, $side)
|
||||
$targetRectangle = [System.Drawing.Rectangle]::new(0, 0, $Size, $Size)
|
||||
$bitmap = [System.Drawing.Bitmap]::new($Size, $Size)
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
||||
|
||||
try {
|
||||
$graphics.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy
|
||||
$graphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
|
||||
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
|
||||
$graphics.DrawImage($SourceImage, $targetRectangle, $sourceRectangle, [System.Drawing.GraphicsUnit]::Pixel)
|
||||
$bitmap.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
}
|
||||
finally {
|
||||
$graphics.Dispose()
|
||||
$bitmap.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$sourceImage = [System.Drawing.Image]::FromFile($sourcePath)
|
||||
|
||||
try {
|
||||
$targets = @(
|
||||
@{ Size = 180; FileName = 'apple-touch-icon.png' }
|
||||
@{ Size = 192; FileName = 'pwa-192.png' }
|
||||
@{ Size = 512; FileName = 'pwa-512.png' }
|
||||
)
|
||||
|
||||
foreach ($target in $targets) {
|
||||
$outputPath = Join-Path $iconDirectory $target.FileName
|
||||
Export-SquareIcon -SourceImage $sourceImage -Size $target.Size -OutputPath $outputPath
|
||||
Write-Host "已生成 $outputPath"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$sourceImage.Dispose()
|
||||
}
|
||||
Reference in New Issue
Block a user