From ba3b1e13e101627b3f75566c3394c98833ffdef1 Mon Sep 17 00:00:00 2001
From: yuanyxh <15766118362@139.com>
Date: Wed, 15 May 2024 17:01:04 +0800
Subject: [PATCH] feat: complete serviceless_blog_establishment post
---
helpers/generateSw.js | 3 +-
helpers/vite-generate-sitemap.ts | 2 -
helpers/vite-prerender.ts | 7 +-
package.json | 2 +-
pnpm-lock.yaml | 84 +-
src/coder/Wrapper.tsx | 4 +-
.../skill/serviceless_blog_establishment.mdx | 879 +++++++++++++++++-
.../hooks/{useRoute.ts => useRoutes.ts} | 2 +-
src/router/index.ts | 2 +-
src/routes.tsx | 4 +
src/viewer/hooks/useArticles.ts | 4 +-
src/viewer/hooks/useBooks.ts | 4 +-
src/viewer/hooks/useExamples.ts | 4 +-
src/viewer/styles/Provider.module.less | 4 +
14 files changed, 910 insertions(+), 95 deletions(-)
rename src/router/hooks/{useRoute.ts => useRoutes.ts} (85%)
diff --git a/helpers/generateSw.js b/helpers/generateSw.js
index 2ebb469..28ff7e9 100644
--- a/helpers/generateSw.js
+++ b/helpers/generateSw.js
@@ -58,7 +58,8 @@ generateSW({
cacheName: 'imageCache',
expiration: {
/** maximum cache 3 months */
- maxAgeSeconds: 3 * 30 * 24 * 60 * 60
+ // maxAgeSeconds: 3 * 30 * 24 * 60 * 60
+ maxAgeSeconds: 7 * 24 * 60 * 60
},
cacheableResponse: {
statuses: [0, 200]
diff --git a/helpers/vite-generate-sitemap.ts b/helpers/vite-generate-sitemap.ts
index feab5f3..1e2c71f 100644
--- a/helpers/vite-generate-sitemap.ts
+++ b/helpers/vite-generate-sitemap.ts
@@ -48,8 +48,6 @@ function viteGenerateSitemap(): PluginOption {
};
});
- console.log('debug for git actions', links.length);
-
const smStream = new SitemapStream({ hostname: getEnv().VITE_DOMAIN_PATH });
return streamToPromise(Readable.from(links).pipe(smStream)).then((data) =>
diff --git a/helpers/vite-prerender.ts b/helpers/vite-prerender.ts
index 51a0f2c..66a2297 100644
--- a/helpers/vite-prerender.ts
+++ b/helpers/vite-prerender.ts
@@ -1,14 +1,14 @@
import { submit } from './submit';
+import type { ResolveRouteObject } from './utils';
import {
generateRouteJSON,
getEnv,
replacePlaceRoute,
resolve,
resolveFullRoutes,
- ResolveRouteObject,
routesPath
} from './utils';
-import { ArticleMeta } from './vite-route-generator';
+import type { ArticleMeta } from './vite-route-generator';
import dayjs from 'dayjs';
import { readdirSync, readFileSync } from 'node:fs';
@@ -35,7 +35,8 @@ const excludeOutPathRewrite = [
'/examples',
'/books',
'/coder',
- '/profile'
+ '/profile',
+ '/404'
];
function getMetaTag(meta: ArticleMeta | undefined, route: ResolveRouteObject) {
diff --git a/package.json b/package.json
index 234be64..d4cbb62 100644
--- a/package.json
+++ b/package.json
@@ -90,7 +90,7 @@
"remark-mdx-toc": "^0.3.1",
"rollup-plugin-visualizer": "^5.12.0",
"serve": "^14.2.3",
- "shiki": "^1.5.1",
+ "shiki": "^1.5.2",
"sitemap": "^7.1.1",
"stylelint": "^16.5.0",
"stylelint-config-recess-order": "^5.0.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4d5d21c..f793a98 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -223,8 +223,8 @@ devDependencies:
specifier: ^14.2.3
version: 14.2.3
shiki:
- specifier: ^1.5.1
- version: 1.5.1
+ specifier: ^1.5.2
+ version: 1.5.2
sitemap:
specifier: ^7.1.1
version: 7.1.1
@@ -2592,7 +2592,7 @@ packages:
'@milkdown/transformer': 7.3.6(@milkdown/prose@7.3.6)
'@milkdown/utils': 7.3.6(@milkdown/core@7.3.6)(@milkdown/ctx@7.3.6)(@milkdown/prose@7.3.6)(@milkdown/transformer@7.3.6)
'@types/dompurify': 3.0.5
- mermaid: 10.9.0
+ mermaid: 10.9.1
nanoid: 5.0.7
tslib: 2.6.2
unist-util-visit: 5.0.0
@@ -3299,8 +3299,8 @@ packages:
dev: true
optional: true
- /@shikijs/core@1.5.1:
- resolution: {integrity: sha512-xjV63pRUBvxA1LsxOUhRKLPh0uUjwBLzAKLdEuYSLIylo71sYuwDcttqNP01Ib1TZlLfO840CXHPlgUUsYFjzg==}
+ /@shikijs/core@1.5.2:
+ resolution: {integrity: sha512-wSAOgaz48GmhILFElMCeQypSZmj6Ru6DttOOtl3KNkdJ17ApQuGNCfzpk4cClasVrnIu45++2DBwG4LNMQAfaA==}
dev: true
/@sindresorhus/is@4.6.0:
@@ -3484,8 +3484,8 @@ packages:
dependencies:
'@types/unist': 2.0.10
- /@types/mdast@4.0.3:
- resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==}
+ /@types/mdast@4.0.4:
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
dependencies:
'@types/unist': 3.0.2
@@ -4369,7 +4369,7 @@ packages:
caniuse-lite: 1.0.30001618
electron-to-chromium: 1.4.767
node-releases: 2.0.14
- update-browserslist-db: 1.0.15(browserslist@4.23.0)
+ update-browserslist-db: 1.0.16(browserslist@4.23.0)
dev: true
/buffer-crc32@0.2.13:
@@ -8096,7 +8096,7 @@ packages:
/mdast-util-definitions@6.0.0:
resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
'@types/unist': 3.0.2
unist-util-visit: 5.0.0
dev: false
@@ -8104,7 +8104,7 @@ packages:
/mdast-util-find-and-replace@3.0.1:
resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
escape-string-regexp: 5.0.0
unist-util-is: 6.0.0
unist-util-visit-parents: 6.0.1
@@ -8143,7 +8143,7 @@ packages:
/mdast-util-from-markdown@2.0.0:
resolution: {integrity: sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
'@types/unist': 3.0.2
decode-named-character-reference: 1.0.2
devlop: 1.1.0
@@ -8161,7 +8161,7 @@ packages:
/mdast-util-frontmatter@2.0.1:
resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
devlop: 1.1.0
escape-string-regexp: 5.0.0
mdast-util-from-markdown: 2.0.0
@@ -8174,7 +8174,7 @@ packages:
/mdast-util-gfm-autolink-literal@2.0.0:
resolution: {integrity: sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
ccount: 2.0.1
devlop: 1.1.0
mdast-util-find-and-replace: 3.0.1
@@ -8183,7 +8183,7 @@ packages:
/mdast-util-gfm-footnote@2.0.0:
resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
devlop: 1.1.0
mdast-util-from-markdown: 2.0.0
mdast-util-to-markdown: 2.1.0
@@ -8194,7 +8194,7 @@ packages:
/mdast-util-gfm-strikethrough@2.0.0:
resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
mdast-util-from-markdown: 2.0.0
mdast-util-to-markdown: 2.1.0
transitivePeerDependencies:
@@ -8203,7 +8203,7 @@ packages:
/mdast-util-gfm-table@2.0.0:
resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
devlop: 1.1.0
markdown-table: 3.0.3
mdast-util-from-markdown: 2.0.0
@@ -8214,7 +8214,7 @@ packages:
/mdast-util-gfm-task-list-item@2.0.0:
resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
devlop: 1.1.0
mdast-util-from-markdown: 2.0.0
mdast-util-to-markdown: 2.1.0
@@ -8239,7 +8239,7 @@ packages:
dependencies:
'@types/estree-jsx': 1.0.5
'@types/hast': 3.0.4
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
devlop: 1.1.0
mdast-util-from-markdown: 2.0.0
mdast-util-to-markdown: 2.1.0
@@ -8252,7 +8252,7 @@ packages:
dependencies:
'@types/estree-jsx': 1.0.5
'@types/hast': 3.0.4
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
'@types/unist': 3.0.2
ccount: 2.0.1
devlop: 1.1.0
@@ -8284,7 +8284,7 @@ packages:
dependencies:
'@types/estree-jsx': 1.0.5
'@types/hast': 3.0.4
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
devlop: 1.1.0
mdast-util-from-markdown: 2.0.0
mdast-util-to-markdown: 2.1.0
@@ -8295,21 +8295,21 @@ packages:
/mdast-util-newline-to-break@2.0.0:
resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
mdast-util-find-and-replace: 3.0.1
dev: true
/mdast-util-phrasing@4.1.0:
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
unist-util-is: 6.0.0
/mdast-util-to-hast@13.1.0:
resolution: {integrity: sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==}
dependencies:
'@types/hast': 3.0.4
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
'@ungap/structured-clone': 1.2.0
devlop: 1.1.0
micromark-util-sanitize-uri: 2.0.0
@@ -8322,7 +8322,7 @@ packages:
/mdast-util-to-markdown@2.1.0:
resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
'@types/unist': 3.0.2
longest-streak: 3.1.0
mdast-util-phrasing: 4.1.0
@@ -8343,7 +8343,7 @@ packages:
/mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
/mdn-data@2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
@@ -8388,8 +8388,8 @@ packages:
engines: {node: '>= 8'}
dev: true
- /mermaid@10.9.0:
- resolution: {integrity: sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==}
+ /mermaid@10.9.1:
+ resolution: {integrity: sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==}
dependencies:
'@braintree/sanitize-url': 6.0.4
'@types/d3-scale': 4.0.8
@@ -11090,7 +11090,7 @@ packages:
/remark-breaks@4.0.0:
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
mdast-util-newline-to-break: 2.0.0
unified: 11.0.4
dev: true
@@ -11099,7 +11099,7 @@ packages:
resolution: {integrity: sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
emoticon: 4.0.1
mdast-util-find-and-replace: 3.0.1
node-emoji: 2.1.3
@@ -11108,7 +11108,7 @@ packages:
/remark-frontmatter@5.0.0:
resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
mdast-util-frontmatter: 2.0.1
micromark-extension-frontmatter: 2.0.0
unified: 11.0.4
@@ -11119,7 +11119,7 @@ packages:
/remark-gfm@4.0.0:
resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
mdast-util-gfm: 3.0.0
micromark-extension-gfm: 3.0.0
remark-parse: 11.0.0
@@ -11131,7 +11131,7 @@ packages:
/remark-inline-links@7.0.0:
resolution: {integrity: sha512-4uj1pPM+F495ySZhTIB6ay2oSkTsKgmYaKk/q5HIdhX2fuyLEegpjWa0VdJRJ01sgOqAFo7MBKdDUejIYBMVMQ==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
mdast-util-definitions: 6.0.0
unist-util-visit: 5.0.0
dev: false
@@ -11139,7 +11139,7 @@ packages:
/remark-mdx-frontmatter@4.0.0:
resolution: {integrity: sha512-PZzAiDGOEfv1Ua7exQ8S5kKxkD8CDaSb4nM+1Mprs6u8dyvQifakh+kCj6NovfGXW+bTvrhjaR3srzjS2qJHKg==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
estree-util-is-identifier-name: 3.0.0
estree-util-value-to-estree: 3.1.1
toml: 3.0.0
@@ -11169,7 +11169,7 @@ packages:
/remark-parse@11.0.0:
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
mdast-util-from-markdown: 2.0.0
micromark-util-types: 2.0.0
unified: 11.0.4
@@ -11180,7 +11180,7 @@ packages:
resolution: {integrity: sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==}
dependencies:
'@types/hast': 3.0.4
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
mdast-util-to-hast: 13.1.0
unified: 11.0.4
vfile: 6.0.1
@@ -11189,14 +11189,14 @@ packages:
/remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
mdast-util-to-markdown: 2.1.0
unified: 11.0.4
/remark@15.0.1:
resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==}
dependencies:
- '@types/mdast': 4.0.3
+ '@types/mdast': 4.0.4
remark-parse: 11.0.0
remark-stringify: 11.0.0
unified: 11.0.4
@@ -11571,10 +11571,10 @@ packages:
engines: {node: '>=8'}
dev: true
- /shiki@1.5.1:
- resolution: {integrity: sha512-vx4Ds3M3B9ZEmLeSXqBAB85osBWV8ErZfP69kuFQZozPgHc33m7spLTCUkcjwEjFm3gk3F9IdXMv8kX+v9xDHA==}
+ /shiki@1.5.2:
+ resolution: {integrity: sha512-fpPbuSaatinmdGijE7VYUD3hxLozR3ZZ+iAx8Iy2X6REmJGyF5hQl94SgmiUNTospq346nXUVZx0035dyGvIVw==}
dependencies:
- '@shikijs/core': 1.5.1
+ '@shikijs/core': 1.5.2
dev: true
/side-channel@1.0.6:
@@ -12746,8 +12746,8 @@ packages:
engines: {node: '>=4'}
dev: true
- /update-browserslist-db@1.0.15(browserslist@4.23.0):
- resolution: {integrity: sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==}
+ /update-browserslist-db@1.0.16(browserslist@4.23.0):
+ resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
diff --git a/src/coder/Wrapper.tsx b/src/coder/Wrapper.tsx
index 8054c80..fc1cad2 100644
--- a/src/coder/Wrapper.tsx
+++ b/src/coder/Wrapper.tsx
@@ -6,7 +6,7 @@ import { cloneDeep } from 'lodash-es';
import classNames from 'classnames';
import type { ResolveRouteObject } from '@/router';
-import { Outlet, useHistory, useLocation, useRoute } from '@/router';
+import { Outlet, useHistory, useLocation, useRoutes } from '@/router';
import styles from '@/coder/styles/Wrapper.module.less';
@@ -159,7 +159,7 @@ type FieldType = {
};
export default function Wrapper() {
- const route = useRoute();
+ const route = useRoutes();
const location = useLocation();
const [open, setOpen] = useState(false);
diff --git a/src/markdowns/skill/serviceless_blog_establishment.mdx b/src/markdowns/skill/serviceless_blog_establishment.mdx
index 7985d93..1b1d9de 100644
--- a/src/markdowns/skill/serviceless_blog_establishment.mdx
+++ b/src/markdowns/skill/serviceless_blog_establishment.mdx
@@ -1,5 +1,5 @@
---
-title: 从 0 到 1 搭建一个 no server 的博客能学到什么
+title: 从 0 到 1 搭建一个 No Server 的博客能学到什么
date: 2024-05-14 14:21:15
author: yuanyxh
imageUrl: http://qkc148.bvimg.com/18470/63a262ad55d7a360.webp
@@ -10,27 +10,27 @@ description: 记录搭建一个 no server 博客的过程,学到了诸如自
-之前有一个基于 [vuepress] 的博客,优点是配置简单、快速上手搭建;缺点是学不到太多东西,而且感觉千篇一律;刚好前一段时间比较闲,就动手搭建了这个项目。
+之前有一个基于 [Vuepress] 的博客,优点是配置简单、快速上手搭建;缺点是学不到太多东西,而且感觉千篇一律;刚好前一段时间比较闲,就动手搭建了这个项目。
-## 什么是 no server 的博客
+## 什么是 No Server 的博客
-no server 可以理解为无后端控制,只提供静态网站服务,这个概念是从 vuepress 那里学来的,因为缺乏后端相关的知识,所以网站一直部署在 [github pages][github-pages] 中,搭建这个博客也希望能继续使用 github pages 进行网站部署。
+no server 可以理解为无后端控制,只提供静态网站服务,这个概念是从 vuepress 那里学来的,因为缺乏后端相关的知识,所以网站一直部署在 [Github Pages][github-pages] 中,搭建这个博客也希望能继续使用 github pages 进行网站部署。
-vuepress 使用的路由模式是 [history],history 模式下在其他页面刷新是会 404 的,因为没有对应的 html 文件,一般是后端配置所有的页面请求都响应为 index.html,但 github pages 只是一个静态托管平台,做不了这样的操作。vuepress 采取的方案是预渲染完整的 html,存在真实的 html 访问自然不会 404,且 [seo] 也会更优秀。
+vuepress 使用的路由模式是 [History],history 模式下在其他页面刷新是会 404 的,因为没有对应的 html 文件,一般是后端配置所有的页面请求都响应为 index.html,但 github pages 只是一个静态托管平台,做不了这样的操作。vuepress 采取的方案是预渲染完整的 html,存在真实的 html 访问自然不会 404,且 [SEO] 也会更优秀。
-vuepress 为每个预渲染的 html 注入主应用程序的 js 文件,在加载 html 后,将 html 和 [vue] 实例进行水合,成为一个 [spa] 程序,这样在享受到初始极速的加载体验后还能使用完整的 spa 程序特性。
+vuepress 为每个预渲染的 html 注入主应用程序的 js 文件,在加载 html 后,将 html 和 [Vue] 实例进行水合,成为一个 [SPA] 程序,这样在享受到初始极速的加载体验后还能使用完整的 spa 程序特性。
-通过上述两步操作,vuepress 实现了一个 no server 但体验不输 ssr 渲染的博客系统,而我们则参考这种思路完成自己的博客。
+通过上述两步操作,vuepress 实现了一个 no server 但体验不输 [SSR] 渲染的博客系统,而我们则参考这种思路完成自己的博客。
## 搭建项目架构
-一些千篇一律的配置这里不展开介绍,只介绍自己认为学到了东西的工程配置。先介绍下基础的架构:[vite] + [pnpm] + [typescript] + [react] + [mdx]。
+一些千篇一律的配置这里不展开介绍,只介绍自己认为学到了东西的工程配置。先说下基础的架构:[Vite] + [PNPM] + [Typescript] + [React] + [MDX]。
-### browserslist
+### Browserslist
-[browserslist] 是让你能够配置项目运行的目标浏览器的工具,本身并不提供语法降级功能,那些是 [babel]、[postcss] 的工作,而 browserslist 则告诉这些工具你的项目需要在哪些浏览器上工作,适配了 browserslist 规范的前端工具会自动为你完成降级。
+[Browserslist] 是让你能够配置项目运行的目标浏览器的工具,本身并不提供语法降级功能,那些是 [Babel]、[PostCSS] 的工作,而 browserslist 则告诉这些工具你的项目需要在哪些浏览器上工作,适配了 browserslist 规范的前端工具会自动为你完成降级。
-我们可以通过在项目中新建 `.browserslistrc` 并写入规则来实现,比如这个博客项目中写入了以下内容:
+我们可以通过在项目根目录中新建 `.browserslistrc` 并写入规则来实现,比如这个博客项目中写入了以下内容:
```rc
# Browsers that we support
@@ -45,9 +45,9 @@ defaults and fully supports es6-module
前面 5 个规则指定了对应的浏览器要大于等于什么版本,而最后一条规则指定了要运行在支持 es6 模块的浏览器之上。
-### unocss
+### UnoCSS
-[unocss] 是现在很火的原子化 css 构建工具,项目一开始使用了,简单讲一下配置。
+[UnoCSS] 是现在很火的原子化 css 构建工具,项目一开始使用了,简单讲一下配置。
```ts
// uno.config.ts
@@ -141,13 +141,13 @@ module.exports = {
这个项目最终是将 unocss 移除掉的,倒不是不好用,只是不适合;对于自己的项目比较看重代码可读性,原子化 css 需要在 jsx 编写大量的 css 类,还是会有点影响;而且对于主题切换不是很友好,感觉在 css 中定义变量的方式对于主题切换更方便一点。
-### mdx
+### MDX
-[mdx] 是号称让 [markdown] 步入组件时代的工具,允许我们在 markdown 中编写 jsx 代码并嵌入组件。
+[MDX] 是号称让 [Markdown] 步入组件时代的工具,允许我们在 markdown 中编写 jsx 代码并嵌入组件。
我是希望能够继承 vuepress 模式的,即一个 markdown 代表一个路由页面;最开始的想法是通过 [markdown-it] 在编译时构建生成页面,但研究过程中发现了 mdx,便转为了这个库来实现。
-mdx 提供了 [rollup] 的插件包,在 vite 中插件配置如下:
+mdx 提供了 [Rollup] 的插件包,在 vite 中插件配置如下:
```ts
import mdx from '@mdx-js/rollup';
@@ -218,7 +218,7 @@ export const useMDXComponents = (): MDXComponents => {
### 字体
-一开始使用 [阿里巴巴普惠体2.0][alibaba-puhei-2.0],只使用 400 和 700 字重,防止字体文件过大,可以通过以下的样式来控制只在必要的适合加载对应的字体:
+一开始使用 [阿里巴巴普惠体2.0][alibaba-puhei-2.0],防止字体文件过大,只使用了 400 和 700 字重,可以通过以下的样式来控制只在必要的时机加载对应的字体:
```css
@font-face {
@@ -256,29 +256,836 @@ export const useMDXComponents = (): MDXComponents => {
这样当页面上渲染了 400 字重的文字时,就会去加载 `font-weight: 400;` 的字体。
-最终是移除了中文的字体文件,毕竟中文实在是太多了,考虑到字重、斜体这些因素,一个完整的常见中文的字体可能有几百 MB 大小。
+最终是移除了中文的字体文件,毕竟中文实在是太多了,考虑到字重、斜体这些因素,一个完整的常见中文的字体文件可能有几百 MB 大小。
-[vuepress]: https://vuepress.vuejs.org/zh/
+### Icon
+
+项目中一般都会需要一些图标来提升感官,基本是字体图标或 svg 图标,在使用 unocss 的过程中发现了 [Iconify] 这个项目,也希望在这个项目中用起来,以离线的方式。
+
+将 svg 图标下载到本地,使用 [vite-plugin-svg-icons] 来简化引用 svg 图标的方式,vite-plugin-svg-icons 会将指定目录下的 svg 图标管理起来,并生成类似如下的 dom 结构:
+
+```html
+
+```
+
+`symbol` 标签类似一个类,类名为 `my-icon`,本身是不呈现在页面中的,我们可以通过 `use` 标签来实例化这个类:
+
+```html
+
+```
+
+这样就能在对应的 dom 位置中展示 `my-icon` 这个 svg 图标。
+
+## 自定义路由
+
+为什么要自定义路由而不使用 [React Router][React-Router]?因为我想实现自己的路由页面切换逻辑,在看了大致的 react router 源码后我感觉他是做不到的。
+
+之前我写了一个 react + react router 的演示项目,个人感觉页面切换时体验不好的点在于:
+
+- 进入一个新的页面时,因为使用了 `Lazy Component`,react 要求必须渲染一个 `fallback` 元素,这会卸载掉原来的页面组件。
+- 在加载新的页面之前一直展示 `fallback` 元素,用户观感不好,也无法操作原有页面。
+
+所以我一直在找可以不渲染 `fallback` 元素,让当前页面在下一个页面准备好之前始终存在的方式,而在研究时发现通过一种骚操作可以实现我想要的效果:
+
+```tsx
+import { lazy } from 'react';
+import { createBrowserRouter } from 'react-router-dom';
+
+const Layout = lazy(() => import('./viewer/Layout.tsx'));
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element:
+ }
+]);
+
+const navigate = router.navigate;
+
+const navigateProxy: typeof router.navigate = async function navigateProxy(
+ to,
+ options
+) {
+ if (typeof to === 'string') {
+ await import(to);
+ } else if (typeof to === 'object' && to?.pathname) {
+ await import(to.pathname);
+ }
+
+ return navigate(to, options);
+};
+
+router.navigate = navigateProxy;
+```
+
+本质就是利用浏览器缓存模块的特色,在加载前不真正执行路由跳转逻辑,在模块加载完成后再放行,此时 react 在内部去加载模块就不需要什么时间了,从而达到了目的。这种方式过于抽象,且没有可靠性,故放弃。
+
+另一种想法是基于 `Suspense` 的工作原理来实施的,我们知道 `Suspense` 会获取到内部组件抛出来的 `Promise`,获取到 `Promise` 时会挂载 `fallback`,`Promsie` 决议时会还原,使用如下代码可以测试:
+
+```tsx
+const Await = () => {
+ if (Math.random() > 0.5) {
+ throw new Promise((resolve) => setTimeout(() => resolve(''), 3000));
+ }
+
+ return
loaded
;
+}
+
+const App = () => {
+ return (
+
+
+
+ );
+}
+```
+
+我的最初构想是通过高阶组件来捕获到 `lazy` 组件抛出的 `Promise`,如下:
+
+```tsx
+const Layout = lazy(() => import('./viewer/Layout.tsx'));
+
+const App = () => {
+ try {
+ const p = ;
+ } catch(err) {
+ console.log(err);
+ }
+}
+```
+
+实际并没有效果,查看 `lazy` 函数的源码可以发现他只是做了一层标记,也并没有在 render 阶段抛出 `Promise`,所以想法半路被折断。
+
+看了 《React 全家桶》 之后对路由页面切换的方式有一点了解,最终决定以自定义路由的方式来实现我的目的,这里我只讲大概的逻辑,很多东西只有自己去做了才能体会其中的细节。
+
+假设我们有两个根页面 `ViewLayout` 和 `CoderLayout`,有一个包裹组件 `Outlet`,`Outlet` 可以通过路线来判断当前需要渲染哪个页面:
+
+```tsx
+import ViewLayout from './ViewLayout';
+import CoderLayout from './CoderLayout';
+
+function Outlet() {
+ if (window.location.pathname === '/viewer') {
+ return
+ }
+ if (window.location.pathname === '/coder') {
+ return ;
+ }
+
+ return null;
+}
+```
+
+这其实就是路由切换的本质了,但真正实现起来是很多细节的,以我在这个项目中的自定义路由来说,大概实现了以下功能:
+
+- 中心路由对象 router,以此为基准对外提供 API
+- 可配置,类似 react router 的 `createBrowserRouter`
+- RouterProvider,通过 react context 提供 router 对象
+- useHistory,编程式导航的 hook
+- useLocation,获取当前路线及其他状态的 hook
+- useRoutes,获取所有路线映射的 hook
+- useScrollStore,滚动恢复的 hook
+- Link,路由导航组件
+- Outlet,消费路线的组件
+
+在这个路由系统中,导航的过程是这样的:
+
+1. 导航 navigate
+2. 匹配路线 match route
+3. 加载路线对应的组件模块 load component (这个阶段不会去卸载原页面,体验会更好)
+4. 消费路线 consumption route
+
+在每段路线中只能匹配一个组件,不可能一段路线匹配到了两个组件,比如 `/viewer/articles` 可以分为 `/viewer` 和 `articles`,他们是父子级的关系,即:
+
+```tsx
+/* /viewer */
+
+ {/* articles */}
+
+
+```
+
+另外在父 `Outlet` 没有匹配到对应的组件时是不会去加载子 `Outlet` 的模块的,每个 `Outlet` 只需要关注自己需要消费的路线,不必关注后代路线。
+
+## Store
+
+写前端项目基本离不开三件套:视图库、路由库、状态管理库;这里我选择使用 [Zustand] 作为项目的状态管理,因为确实简单方便,比 redux/toolkit 更容易上手。
+
+这里不讲它的配置,大概说下 Store 如何与视图库进行交互的。以 redux 举例,我们知道他提供了 `subscribe` 方法,通过这个方法可以获取到状态变更的通知:
+
+```ts
+store.subscribe(() => {
+ // dosomething
+});
+```
+
+在类组件中我们可以通过 `this.forecUpdate()` 方法来触发组件重渲染:
+
+```tsx
+import store from './store';
+class A extends PureComponent {
+ listener = () => {
+ this.forceUpdate();
+ };
+
+ unListener = () => void 0;
+
+ componentDidMount(): void {
+ this.unListener = store.subscribe(this.listener);
+ }
+
+ componentWillUnmount(): void {
+ this.unListener();
+ }
+
+ render(): ReactNode {
+ return <>{store.state.name}>;
+ }
+}
+```
+
+在函数式组件中 react 提供了 `useSyncExternalStore` hooks:
+
+```tsx
+import store from './store'
+
+function A() {
+ const state = useSyncExternalStore(store.subscribe, store.getState);
+
+ return <>{state.name}>
+}
+```
+
+`useSyncExternalStore` 会在内部调用 `subscribe` 方法并传入处理函数,当外部 store 状态变化时会调用处理函数,react 会自动为我们重渲染组件。
+
+## File System
+
+这个项目我不光是想作为一个博客来开发的,还希望有一定的功能,第一个开发的功能就是利用 [File System API][File-System-API] 和 [Origin private file system][Origin-private-file-system] 实现的在线文件管理系统:
+
+
+
+目前只带有简单的增删查功能,但可以在这个系统之上构建不同的文件处理程序,比如目前实现了一个在线的类 [Typora] 的 markdown 即时编辑器:
+
+
+
+主要通过 [Milkdown] 这个库来实现,目前测试还有一些小的 bug,但整体可用。
+
+这里重点介绍下对于远程文件的适配,我本身是有一个用于同步文档的 webdav 服务器的,我希望这个文件系统中也能够支持 webdav 协议并挂载远程目录。支持 webdav 协议使用 [webdav] 这个库可以做到,但如何适配代码呢?使用硬编码来支持的话那未来要如何扩展?
+
+我们需要知道使用 File System API 常用的几个接口,外部操作这几个接口就可以满足大部分的文件操作:
+
+- [FileSystemHandle]:文件系统的基类
+- [FileSystemDirectoryHandle]:目录句柄,提供操作目录的 API
+- [FileSystemFileHandle]:文件句柄,提供操作文件的 API
+- [FileSystemWritableFileStream]: writeable 文件写入的 API
+
+既然这四个接口可以满足大部分的文件操作,那其实我们可以使用适配器模式,将 webdav 的文件操作对这四个接口进行适配,这样就不用改动已有代码,未来添加其他协议时也只需要关注自身进行适配就好了,不必在大改原来的逻辑。这里贴一下最终我完成的 webdav 适配接口:
+
+```tsx
+
+import { isEqual } from 'lodash-es';
+import type { FileStat, WebDAVClient } from 'webdav';
+import { AuthType, createClient } from 'webdav';
+
+import { FilePathNotExistsError, FileTypeError } from './utils/error';
+import { FileType } from './utils/fileManager';
+
+class WebdavFileSystemWritableFileStream
+ implements FileSystemWritableFileStream
+{
+ locked: boolean = false;
+
+ private webdav: WebDAVClient;
+ private fullPath: string;
+
+ private setFile: (buffer: ArrayBuffer) => void;
+
+ constructor(
+ webdav: WebDAVClient,
+ fullPath: string,
+ setFile: (buffer: ArrayBuffer) => void
+ ) {
+ this.webdav = webdav;
+ this.fullPath = fullPath;
+
+ this.setFile = setFile;
+ }
+
+ seek(): Promise {
+ throw new Error('Method not implemented.');
+ }
+ truncate(): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ abort(): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ getWriter(): WritableStreamDefaultWriter {
+ throw new Error('Method not implemented.');
+ }
+
+ async write(data: FileSystemWriteChunkType): Promise {
+ if (data instanceof Blob) {
+ data = await data.arrayBuffer();
+ }
+
+ await this.webdav.putFileContents(this.fullPath, data as string);
+
+ this.setFile(data as ArrayBuffer);
+ }
+
+ async close(): Promise {
+ return void 0;
+ }
+}
+
+class WebdavFileSystemHandle implements FileSystemHandle {
+ kind: FileSystemHandleKind;
+ name: string;
+
+ private _fullPath: string;
+
+ private _webdav: WebDAVClient;
+
+ private _webdavInfo: WebdavInfo;
+
+ constructor(
+ kind: FileSystemHandleKind,
+ name: string,
+ fullPath: string,
+ webdavClient: WebDAVClient,
+ webdavInfo: WebdavInfo
+ ) {
+ this.kind = kind;
+ this.name = name;
+
+ this._fullPath = fullPath;
+ this._webdav = webdavClient;
+
+ this._webdavInfo = webdavInfo;
+ }
+
+ get fullPath() {
+ return this._fullPath;
+ }
+
+ get webdav() {
+ return this._webdav;
+ }
+
+ get webdavInfo() {
+ return this._webdavInfo;
+ }
+
+ isSameEntry(other: WebdavFileSystemHandle): Promise;
+ isSameEntry(arg: WebdavFileSystemHandle): boolean;
+ isSameEntry(handle: WebdavFileSystemHandle): boolean | Promise {
+ try {
+ if (
+ handle instanceof WebdavFileSystemHandle &&
+ isEqual(this.webdavInfo, handle.webdavInfo) &&
+ this._fullPath === handle._fullPath
+ ) {
+ return Promise.resolve(true);
+ }
+ } catch (err) {
+ return Promise.resolve(false);
+ }
+
+ return Promise.resolve(false);
+ }
+
+ queryPermission(): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ requestPermission(): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ remove(): Promise {
+ throw new Error('Method not implemented.');
+ }
+}
+
+class WebdavFileSystemFileHandle
+ extends WebdavFileSystemHandle
+ implements FileSystemFileHandle
+{
+ readonly kind = 'file';
+
+ private file: File | null = null;
+
+ constructor(
+ webdav: WebDAVClient,
+ fullPath: string,
+ name: string,
+ webdavInfo: WebdavInfo
+ ) {
+ super('file', name, fullPath, webdav, webdavInfo);
+ }
+
+ createSyncAccessHandle(): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ async getFile() {
+ if (this.file) {
+ return this.file;
+ }
+
+ const data = (await this.webdav.getFileContents(this.fullPath, {
+ format: 'binary'
+ })) as ArrayBuffer;
+
+ this.file = new File([data], this.name);
+
+ return this.file;
+ }
+
+ async createWritable(
+ options?: FileSystemCreateWritableOptions | undefined
+ ): Promise {
+ options;
+
+ const { webdav, fullPath } = this;
+
+ return new WebdavFileSystemWritableFileStream(
+ webdav,
+ fullPath,
+ (buffer) => (this.file = new File([buffer], this.name))
+ );
+ }
+}
+
+class WebdavFileSystemDirectoryHandle
+ extends WebdavFileSystemHandle
+ implements FileSystemDirectoryHandle
+{
+ readonly kind = 'directory';
+
+ constructor(
+ webdav: WebDAVClient,
+ fullPath: string,
+ name: string,
+ webdavInfo: WebdavInfo
+ ) {
+ super('directory', name, fullPath, webdav, webdavInfo);
+ }
+
+ resolve(possibleDescendant: FileSystemHandle): Promise;
+ resolve(possibleDescendant: FileSystemHandle): Promise;
+ resolve(): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ keys(): AsyncIterableIterator {
+ throw new Error('Method not implemented.');
+ }
+
+ values(): AsyncIterableIterator<
+ FileSystemFileHandle | FileSystemDirectoryHandle
+ > {
+ throw new Error('Method not implemented.');
+ }
+
+ entries(): AsyncIterableIterator<
+ [string, WebdavFileSystemDirectoryHandle | WebdavFileSystemFileHandle]
+ > {
+ let i = 0;
+
+ const { webdav, fullPath, webdavInfo } = this;
+
+ const p = webdav.getDirectoryContents(fullPath, {
+ includeSelf: false
+ }) as Promise>;
+
+ return {
+ [Symbol.asyncIterator]() {
+ return this;
+ },
+ async next() {
+ const values = await p;
+
+ const curr = values[i++];
+
+ if (!curr) {
+ return { value: undefined, done: true };
+ }
+
+ return {
+ value: [
+ curr.basename,
+ createWebdavFileSystemHandle(
+ curr,
+ webdav,
+ fullPath + curr.basename,
+ webdavInfo
+ )
+ ],
+ done: false
+ };
+ }
+ };
+ }
+
+ async getDirectoryHandle(
+ name: string,
+ options?: FileSystemHandleCreateOptions
+ ): Promise {
+ const { create = false } = options || {};
+
+ const subFullPath = this.fullPath + name;
+
+ const isNotExists = await this.webdav.exists(subFullPath);
+
+ if (isNotExists === false) {
+ if (create === false) {
+ throw new FilePathNotExistsError(subFullPath);
+ } else {
+ await this.webdav.createDirectory(subFullPath);
+ }
+ } else {
+ const stat = (await this.webdav.stat(subFullPath)) as FileStat;
+
+ if (stat.type !== 'directory') {
+ throw new FileTypeError(stat.basename);
+ }
+ }
+
+ return new WebdavFileSystemDirectoryHandle(
+ this.webdav,
+ subFullPath + '/',
+ name,
+ this.webdavInfo
+ );
+ }
+
+ async getFileHandle(
+ name: string,
+ options?: FileSystemHandleCreateOptions
+ ): Promise {
+ const { create = false } = options || {};
+
+ const subFullPath = this.fullPath + name;
+
+ if (!(await this.webdav.exists(subFullPath))) {
+ if (create === false) {
+ throw new FilePathNotExistsError(subFullPath);
+ } else {
+ await this.webdav.putFileContents(subFullPath, '');
+ }
+ } else {
+ const stat = (await this.webdav.stat(subFullPath)) as FileStat;
+
+ if (stat.type !== 'file') {
+ throw new FileTypeError(stat.basename);
+ }
+ }
+
+ return new WebdavFileSystemFileHandle(
+ this.webdav,
+ subFullPath,
+ name,
+ this.webdavInfo
+ );
+ }
+
+ async removeEntry(name: string): Promise {
+ const subFullPath = this.fullPath + name;
+
+ await this.webdav.deleteFile(subFullPath);
+
+ return void 0;
+ }
+}
+```
+
+## CORS
+
+跨域是前端老生常态的问题了,因为引用了 webdav 协议的缘故,简单的设置 `Access-Control-Allow-Origin "*"` 并不能解决跨域问题,因为 webdav 协议请求的方法很多,也有额外的请求头参数,所以要解决跨域问题需要使用以下配置(apache2 配置):
+
+```conf
+DavLockDB /var/www/DavLock
+
+ ServerAdmin webmaster@localhost
+ DocumentRoot /var/www/html
+
+ LogLevel info
+
+ ErrorLog ${APACHE_LOG_DIR}/error.log
+ CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+ RewriteEngine On
+ RewriteCond %{REQUEST_METHOD} OPTIONS
+ RewriteRule ^(.*)$ $1 [R=200,L]
+
+ Alias /notes /var/www/webdav
+
+ Header always set Access-Control-Allow-Origin "*"
+ Header always set Access-Control-Allow-Headers "Authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-Accept-Charset,X-Accept,origin,accept,if-match,destination,overwrite,Depth"
+ Header always set Access-Control-Expose-Headers "ETag"
+ Header always set Access-Control-Allow-Methods "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK, PROPFIND, MKCOL"
+ Header always set Access-Control-Allow-Credentials "true"
+
+
+ DAV On
+ AuthType Basic
+ #AuthType Digest
+ AuthName "your auth name"
+ AuthUserFile /etc/apache2/webdav.password
+ Require valid-user
+
+
+```
+
+注意需要将浏览器的 `OPTIONS` 请求重写响应为 200,因为 OPTIONS 请求是不会携带验证参数的,也就导致请求会失败,OPTIONS 请求失败了浏览器就会认为跨域,导致无法正常使用。
+
+## Service Worker
+
+[Service Worker][Service-Worker] 可以理解为一个代理层,能够捕获到作用域内的网络请求并修改响应。目前比较常见的作用可能就是缓存资源了,但功能不止如此,毕竟相当于一个小型服务器。
+
+service worker 对于缓存策略主要有以下几种:
+
+- 缓存优先,先缓存后网络
+- 网络优先,先网络,失败后回退缓存
+- 始终验证,从缓存中取,但每次都向服务器询问是否有变更
+- 仅缓存
+- 仅网络
+
+有两种缓存方式:
+
+- 预缓存,在项目编译打包阶段将需要的静态资源添加至预缓存列表,在 service worker 被安装时会在后台请求预缓存资源,博客文档这类以 html 为主的网站用的比较多。
+- 运行时缓存,从网络中获取时,将响应添加进缓存,这种方式适用于运行时才能确定是否需要的资源,比如腻子脚本,字体和图片。
+
+service worker 有三个阶段:
+
+- 安装阶段,此阶段可以获取预缓存资源
+- 等待激活阶段,此阶段表示 service worker 已安装但未激活,没有实际控制当前页面
+- 激活阶段,此阶段 service worker 已可用
+
+使用 service worker 时需要注意数据的新鲜度,浏览器通过对 service worker 文件的字节对比来判断是否有更新,当浏览器认为存在更新时会执行安装阶段,我们可以侦听 service worker 的 `statechange` 事件,当状态变为 `installed` 时则表示安装成功,可以提示用户刷新并立即启用新的 service worker。
+
+对于 service worker 的使用较为繁琐,Google 开发团队提供了一个开箱即用的 service worker 库 [Workbox],可以集成在构建工具中。
+
+另外 [PWA] 也依赖于 service worker 来做离线访问,除了 service worker 外,pwa 相关的技术还有很多,有些已可用有些仍在实验阶段,比如 [后台同步][Background-Synchronization-API]、[后台请求][Background-Fetch-API]、[共享][Web-Share-API] 等等。
+
+## VS Code 图片上传插件开发
+
+## Prerender
+
+预渲染是文章开头提到的一种技术,具体的做法是通过无头浏览器的自动化测试功能,在指定的路线中导航,在页面加载完成后获取到对应路线的完整 html 并输出。因为每个 html 都引用了主应用程序的 js 文件,在浏览器中 html 加载完成后,由视图库对组件实例和预渲染的 html 进行水合,此时程序被视图库控制,变为了一个 spa 程序。
+
+在 vite 中我们可以通过 [vite-plugin-prerender] 来实现,这里直接贴上我的配置:
+
+```ts
+import { resolve } from 'node:path';
+import selfVitePrerender from 'vite-plugin-prerender';
+
+export interface PostProcessParam {
+ originalRoute: string;
+ route: string;
+ html: string;
+ outputPath?: string;
+}
+
+const Renderer = selfVitePrerender.PuppeteerRenderer;
+
+async function vitePrerender(mode: string) {
+ if (mode !== 'build') return void 0;
+
+ // prerender route:https://www.npmjs.com/package/vite-plugin-prerender
+ return selfVitePrerender({
+ // 要渲染的路由
+ routes: ['/', '/articles', '/coder'],
+ // 输出目录
+ staticDir: resolve('./build'),
+
+ // compression
+ minify: {
+ collapseBooleanAttributes: true,
+ collapseWhitespace: true,
+ decodeEntities: true,
+ keepClosingSlash: true,
+ sortAttributes: true
+ },
+
+ renderer: new Renderer({
+ // 无头,设置为 false 会弹出浏览器窗口,方便调试
+ headless: true,
+ // 在指定事件之后完成渲染,通过 window.document.dispatchEvent(new Event('pageReadyed'));
+ renderAfterDocumentEvent: 'pageReadyed'
+ }),
+
+ // 每次渲染时触发,可以在此修改输入路径、输入的 html
+ postProcess(renderedRoute: PostProcessParam) {
+ renderedRoute.outputPath = 'build' + renderedRoute.originalRoute;
+
+ renderedRoute.html = renderedRoute.html.replace(
+ ' route.fullPath === renderedRoute.originalRoute
+ );
+
+ return renderedRoute;
+ }
+ });
+}
+
+export default vitePrerender;
+```
+
+要注意的是这个插件内部使用了 `require` 且无法被 vite 转换,会导致错误,需要使用 pnpm patch、yarn patch 等技术进行补丁。
+
+一般来说要渲染路由的完整 html 才能有不输 ssr 的加载体验,但有两个坑点暂时无法解决,所以我只对每个路由渲染基础的骨架 html。这两个坑点是这样的:
+
+- 使用 React.createRoot 方式,React 会卸载已加载的 html,再重新挂载上去,部分情况下会偶现闪烁白屏。
+- 使用 React.hydrateRoot 方式,React 无法水合组件实例和已加载的 html,因为我的组件是 `Lazy Component`,初始生成的组件实例无法和已加载的 html 对应上,会导致 React 跳过水合步骤。
+
+## SEO
+
+也没做太多的东西,主要就是跑一下 Chrome 的 Lighthouse,跑出来的问题很多暂时都无法解决,比如初始加载的主应用程序包过大,未使用的字节过多。
+
+主要做了这些方向的优化:添加适当的元数据、添加富媒体内容、添加开放图谱协议、添加语义标签、添加站点地图。
+
+- https://ogp.me/
+- https://www.zhangxinxu.com/wordpress/2019/06/html-a-link-rel/
+- https://blog.jipai.moe/add-structured-data-for-your-site/
+
+另外还使用 [Google Index API][Google-Index-API] 对站点的 url 进行自动提交。
+
+## Github Deploy
+
+使用 [Github Actions][Github-Actions] 工作流进行自动化部署。配置如下:
+
+```yml
+# 将静态内容部署到 GitHub Pages 的简易工作流程
+name: Deploy static content to Pages
+
+on:
+ # 仅在推送到默认分支时运行。
+ push:
+ branches: ['master']
+
+ # 这个选项可以使你手动在 Action tab 页面触发工作流
+ workflow_dispatch:
+
+# 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages。
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# 允许一个并发的部署
+concurrency:
+ group: 'pages'
+ cancel-in-progress: true
+
+jobs:
+ # 单次部署的工作描述
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'npm'
+ cache-dependency-path: 'pnpm-lock.yaml'
+ - name: set up pnpm
+ run: npm install pnpm -g
+ - name: Install dependencies
+ run: pnpm install
+ - name: Build
+ run: pnpm run build
+ - name: Setup Pages
+ uses: actions/configure-pages@v4
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ # Upload dist folder
+ path: './build'
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
+
+```
+
+最后贴一下博客和 github 的链接:
+
+- [yuanyxh]
+- [yuanyxh.github.io]
+
+-- end
+
+[Vuepress]: https://vuepress.vuejs.org/zh/
[github-pages]: https://pages.github.com/
-[vue]: https://cn.vuejs.org/
-[spa]: https://developer.mozilla.org/en-US/docs/Glossary/SPA
-[seo]: https://developer.mozilla.org/en-US/docs/Glossary/SEO
-[history]: https://developer.mozilla.org/en-US/docs/Web/API/History
-[vite]: https://vitejs.dev/
-[pnpm]: https://pnpm.io/
-[react]: https://react.dev/
-[typescript]: https://www.typescriptlang.org/
-[mdx]: https://mdxjs.com/
-[browserslist]: https://github.com/browserslist/browserslist
-[babel]: https://babeljs.io/
-[postcss]: https://postcss.org/
-[unocss]: https://unocss.dev/
+[Vue]: https://cn.vuejs.org/
+[SPA]: https://developer.mozilla.org/en-US/docs/Glossary/SPA
+[SEO]: https://developer.mozilla.org/en-US/docs/Glossary/SEO
+[History]: https://developer.mozilla.org/en-US/docs/Web/API/History
+[Vite]: https://vitejs.dev/
+[PNPM]: https://pnpm.io/
+[React]: https://react.dev/
+[Typescript]: https://www.typescriptlang.org/
+[MDX]: https://mdxjs.com/
+[Browserslist]: https://github.com/browserslist/browserslist
+[Babel]: https://babeljs.io/
+[PostCSS]: https://postcss.org/
+[UnoCSS]: https://unocss.dev/
[ESLint]: https://eslint.org/
[elint-plugin-react]: https://www.npmjs.com/package/eslint-plugin-react
-[mdx]: https://mdxjs.com/
[markdown-it]: https://github.com/markdown-it/markdown-it
-[vite-plugin]: https://vitejs.dev/guide/api-plugin.html
-[rollup]: https://www.rollupjs.com/
+[Vite-Plugin]: https://vitejs.dev/guide/api-plugin.html
+[Rollup]: https://www.rollupjs.com/
[remark-frontmatter]: https://github.com/remarkjs/remark-frontmatter
-[markdown]: https://www.markdownguide.org/
+[Markdown]: https://www.markdownguide.org/
[alibaba-puhei-2.0]: https://www.alibabafonts.com/#/font
+[SSR]: https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering
+[Iconify]: https://iconify.design/
+[vite-plugin-svg-icons]: https://github.com/vbenjs/vite-plugin-svg-icons
+[React-Router]: https://reactrouter.com/en/main
+[Zustand]: https://github.com/pmndrs/zustand
+[File-System-API]: https://developer.mozilla.org/en-US/docs/Web/API/File_System_API
+[Origin-private-file-system]: https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system
+[Typora]: https://typora.io/
+[Milkdown]: https://milkdown.dev/
+[webdav]: https://www.npmjs.com/package/webdav
+[FileSystemHandle]: https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle
+[FileSystemDirectoryHandle]: https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle
+[FileSystemFileHandle]: https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle
+[FileSystemWritableFileStream]: https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream
+[Service-Worker]: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
+[Workbox]: https://developer.chrome.com/docs/workbox?hl=zh-cn
+[PWA]: https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps
+[Background-Synchronization-API]: https://developer.mozilla.org/en-US/docs/Web/API/Background_Synchronization_API
+[Background-Fetch-API]: https://developer.mozilla.org/en-US/docs/Web/API/Background_Fetch_API
+[Web-Share-API]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API
+[vite-plugin-prerender]: https://github.com/Rudeus3Greyrat/vite-plugin-prerender
+[Google-Index-API]: https://developers.google.com/search/apis/indexing-api/v3/quickstart?hl=zh-cn
+[Github-Actions]: https://github.com/features/actions
+[yuanyxh]: https://yuanyxh.com/
+[yuanyxh.github.io]: https://github.com/yuanyxh/yuanyxh.github.io
diff --git a/src/router/hooks/useRoute.ts b/src/router/hooks/useRoutes.ts
similarity index 85%
rename from src/router/hooks/useRoute.ts
rename to src/router/hooks/useRoutes.ts
index 3ee8ab3..8db4bf5 100644
--- a/src/router/hooks/useRoute.ts
+++ b/src/router/hooks/useRoutes.ts
@@ -2,7 +2,7 @@ import { useContext } from 'react';
import { RouterContext } from '../shared/context';
-export function useRoute() {
+export function useRoutes() {
const routerContext = useContext(RouterContext);
return routerContext?.getRoutes();
diff --git a/src/router/index.ts b/src/router/index.ts
index b911a13..7559d43 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -5,7 +5,7 @@ export { default as RouterProvider } from './components/RouterProvider';
export { AbstractHistory } from './history';
export { useHistory } from './hooks/useHistory';
export { useLocation } from './hooks/useLocation';
-export { useRoute } from './hooks/useRoute';
+export { useRoutes } from './hooks/useRoutes';
export { useScrollStore } from './hooks/useScrollStore';
export type {
ComponentModule,
diff --git a/src/routes.tsx b/src/routes.tsx
index 78268d4..f5212b9 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -55,6 +55,10 @@ export const routes: RouteObject[] = [
{
path: 'examples',
element: () => import('@/viewer/Examples')
+ },
+ {
+ path: '404',
+ element: '不存在的页面'
}
]
},
diff --git a/src/viewer/hooks/useArticles.ts b/src/viewer/hooks/useArticles.ts
index 4fdfe70..05ee6ca 100644
--- a/src/viewer/hooks/useArticles.ts
+++ b/src/viewer/hooks/useArticles.ts
@@ -1,10 +1,10 @@
import { useMemo } from 'react';
-import { useRoute } from '@/router';
+import { useRoutes } from '@/router';
import { INDEX_PATH } from '@/router';
export function useArticles() {
- const routes = useRoute();
+ const routes = useRoutes();
const articles = useMemo(() => {
// TODO: hardcode? maby use context to provide articles, and router get context provide page route.
diff --git a/src/viewer/hooks/useBooks.ts b/src/viewer/hooks/useBooks.ts
index fd14237..8130f31 100644
--- a/src/viewer/hooks/useBooks.ts
+++ b/src/viewer/hooks/useBooks.ts
@@ -1,10 +1,10 @@
import { useMemo } from 'react';
-import { useRoute } from '@/router';
+import { useRoutes } from '@/router';
import { INDEX_PATH } from '@/router';
export function useBooks() {
- const routes = useRoute();
+ const routes = useRoutes();
const books = useMemo(() => {
// TODO: hardcode? maby use context to provide books, and router get context provide page route.
diff --git a/src/viewer/hooks/useExamples.ts b/src/viewer/hooks/useExamples.ts
index 3c2c648..5960d14 100644
--- a/src/viewer/hooks/useExamples.ts
+++ b/src/viewer/hooks/useExamples.ts
@@ -1,10 +1,10 @@
import { useMemo } from 'react';
-import { useRoute } from '@/router';
+import { useRoutes } from '@/router';
import { INDEX_PATH } from '@/router';
export function useExamples() {
- const routes = useRoute();
+ const routes = useRoutes();
const examples = useMemo(() => {
// TODO: hardcode? maby use context to provide examples, and router get context provide page route.
diff --git a/src/viewer/styles/Provider.module.less b/src/viewer/styles/Provider.module.less
index 5ce5122..79b77aa 100644
--- a/src/viewer/styles/Provider.module.less
+++ b/src/viewer/styles/Provider.module.less
@@ -166,6 +166,10 @@ code {
list-style: initial;
}
+ ol {
+ list-style: decimal;
+ }
+
img {
max-width: 100%;
text-align: center;