diff --git a/docs/riegler-angular-translation-task.md b/docs/riegler-angular-translation-task.md
new file mode 100644
index 00000000..8f68aaff
--- /dev/null
+++ b/docs/riegler-angular-translation-task.md
@@ -0,0 +1,50 @@
+# Riegler Angular Translation Task
+
+Status: in-progress
+Updated: 2026-05-30
+
+## Goal
+- Find Angular-related posts on https://riegler.fr/blog.
+- Compare with local translated posts.
+- List missing translations.
+- Translate each missing article.
+- Mark each item done when finished.
+
+## Source Collection
+- [x] Collect Angular-related post URLs from riegler.fr
+
+## Comparison Against Local Blog
+- [x] Build local translated canonical URL list
+- [x] Compute missing list
+
+## Already Translated (Matched by canonicalURL)
+- [x] https://riegler.fr/blog/2024-10-15-effect-context
+- [x] https://riegler.fr/blog/2024-12-31-lifecycle-hook-less
+- [x] https://riegler.fr/blog/2025-01-08-inject-not-service-locator
+- [x] https://riegler.fr/blog/2025-01-12-signal-decision-chart
+- [x] https://riegler.fr/blog/2025-02-16-incremental-dom
+- [x] https://riegler.fr/blog/2025-04-05-input-output
+
+## Missing Translation Checklist
+- [ ] https://riegler.fr/blog/2023-07-20-angular-memory-leak
+- [ ] https://riegler.fr/blog/2023-09-30-lazy-loading-mockable
+- [ ] https://riegler.fr/blog/2023-10-04-animations-async
+- [ ] https://riegler.fr/blog/2023-10-05-defer-part1
+- [ ] https://riegler.fr/blog/2023-10-08-defer-part2
+- [ ] https://riegler.fr/blog/2023-10-13-v17-builders
+- [ ] https://riegler.fr/blog/2023-10-24-signals-subscriptions
+- [ ] https://riegler.fr/blog/2023-11-02-v17-change-detection
+- [ ] https://riegler.fr/blog/2023-11-03-performance-for-repeaters
+- [ ] https://riegler.fr/blog/2024-01-11-zoneless-preview
+- [ ] https://riegler.fr/blog/2024-04-17-zoneless-with-zoneless-hybrid
+- [ ] https://riegler.fr/blog/2024-05-01-input-setters-caveats
+- [ ] https://riegler.fr/blog/2024-05-17-private-fields-downleveling
+- [ ] https://riegler.fr/blog/2024-10-18-resources-as-signals
+
+## Execution Log
+- [x] Task file initialized.
+- [x] Parsed riegler blog index and extracted Angular-related entries.
+- [x] Compared source URLs with local canonicalURL values.
+- [x] User confirmed authorization for full translation.
+- [ ] Translation execution via Explore agent blocked (agent requires verifiable copyright proof artifact in workspace).
+- [ ] Custom agent invocation blocked: requested agent name/path not found in current session (`blog-translator`, `blog-translator.agent`, `blog-translator.agent.md`, `.github\\agents\\blog-translator.agent.md`).
diff --git a/docs/riegler-angular-translation-tracker.md b/docs/riegler-angular-translation-tracker.md
new file mode 100644
index 00000000..533e1a41
--- /dev/null
+++ b/docs/riegler-angular-translation-tracker.md
@@ -0,0 +1,41 @@
+# Riegler Angular Translation Tracker
+
+## Goal
+- Source: https://riegler.fr/blog
+- Scope: Angular-related posts (2023-07 onward in the blog list)
+- Workflow:
+ 1. Compare source posts with local translated posts.
+ 2. Translate missing posts one by one into src/content/blog.
+ 3. After each translation is completed, mark status as done.
+
+## Status Legend
+- todo: not translated yet
+- done: translated and added in src/content/blog
+
+## Checklist
+
+| Date | Source URL | Local Slug | Status |
+|---|---|---|---|
+| 2023-07-20 | https://riegler.fr/blog/2023-07-20-angular-memory-leak | angular-memory-leak | done |
+| 2023-09-18 | https://riegler.fr/blog/2023-09-18-cldr-angular | cldr-angular | done |
+| 2023-09-20 | https://riegler.fr/blog/2023-09-20-cd-coalescing | cd-coalescing | done |
+| 2023-09-30 | https://riegler.fr/blog/2023-09-30-lazy-loading-mockable | lazy-loading-mockable | done |
+| 2023-10-04 | https://riegler.fr/blog/2023-10-04-animations-async | animations-async | done |
+| 2023-10-05 | https://riegler.fr/blog/2023-10-05-defer-part1 | defer-part1 | done |
+| 2023-10-08 | https://riegler.fr/blog/2023-10-08-defer-part2 | defer-part2 | done |
+| 2023-10-08 | https://riegler.fr/blog/2023-10-08-tree-shakable-as-default | tree-shakable-as-default | done |
+| 2023-10-13 | https://riegler.fr/blog/2023-10-13-v17-builders | v17-builders | done |
+| 2023-10-24 | https://riegler.fr/blog/2023-10-24-signals-subscriptions | signals-subscriptions | done |
+| 2023-11-02 | https://riegler.fr/blog/2023-11-02-v17-change-detection | v17-change-detection | done |
+| 2023-11-03 | https://riegler.fr/blog/2023-11-03-performance-for-repeaters | performance-for-repeaters | done |
+| 2024-01-11 | https://riegler.fr/blog/2024-01-11-zoneless-preview | zoneless-preview | done |
+| 2024-04-17 | https://riegler.fr/blog/2024-04-17-zoneless-with-zoneless-hybrid | zoneless-with-zoneless-hybrid | done |
+| 2024-05-01 | https://riegler.fr/blog/2024-05-01-input-setters-caveats | input-setters-caveats | done |
+| 2024-05-17 | https://riegler.fr/blog/2024-05-17-private-fields-downleveling | private-fields-downleveling | done |
+| 2024-10-15 | https://riegler.fr/blog/2024-10-15-effect-context | understanding-effects | done |
+| 2024-10-18 | https://riegler.fr/blog/2024-10-18-resources-as-signals | resources-as-signals | done |
+| 2024-12-31 | https://riegler.fr/blog/2024-12-31-lifecycle-hook-less | angular-lifecycle-hook-free | done |
+| 2025-01-08 | https://riegler.fr/blog/2025-01-08-inject-not-service-locator | angular-inject-not-service-locator | done |
+| 2025-01-12 | https://riegler.fr/blog/2025-01-12-signal-decision-chart | signal-decision-chart | done |
+| 2025-02-16 | https://riegler.fr/blog/2025-02-16-incremental-dom | angular-declarative-templating | done |
+| 2025-04-05 | https://riegler.fr/blog/2025-04-05-input-output | angular-input-output-asymmetry | done |
diff --git a/docs/tag-taxonomy.md b/docs/tag-taxonomy.md
new file mode 100644
index 00000000..dfde7276
--- /dev/null
+++ b/docs/tag-taxonomy.md
@@ -0,0 +1,50 @@
+# Tag Taxonomy
+
+## Goal
+- Keep tags useful for readers as navigation, not as internal maintenance labels.
+- Use one consistent naming style across the whole site.
+- Prefer fewer, clearer tags over many overlapping tags.
+
+## Rules
+- Use lowercase English tags, preferably kebab-case.
+- Keep each post to 1-3 tags when practical.
+- Prefer one primary topic tag plus up to two secondary tags.
+- Do not use both Chinese and English variants for the same concept.
+- Do not use filler tags like `blog`.
+- Avoid workflow tags in `tags`, such as `translation`, because translation state is already represented by content fields.
+- Keep reader-facing series tags only when they help browsing, such as `english-learning`, `daily-reading`, `reflection`, `shanghai`, `career`, `blogging`.
+
+## Canonical Names
+- Use `ai`, not `AI`.
+- Use `nodejs`, not `node`.
+- Use `github`, not `GitHub`.
+- Avoid generic `frontend` unless it is truly the only meaningful label.
+
+## Angular Vocabulary
+- Primary tag: `angular`
+- Secondary tags used today:
+ - `animations`
+ - `change-detection`
+ - `dependency-injection`
+ - `i18n`
+ - `lazy-loading`
+ - `lifecycle`
+ - `performance`
+ - `rendering`
+ - `signals`
+ - `ssr`
+ - `hydration`
+ - `tooling`
+ - `zoneless`
+
+## What To Avoid
+- `Angular`, `Signal`, `前端`, `有角的`
+- `blog`, `translation`
+- Mixing framework tags with content-type tags in the same post
+- Creating a new tag when an existing Angular secondary tag already covers the topic
+
+## Migration Direction
+- Angular posts should converge on `angular` plus 0-2 concrete secondary topic tags.
+- Daily English posts should converge on `english-learning` + `daily-reading` + one topic tag.
+- Translation posts should drop `translation` from tags and rely on `isTranslation` instead.
+- Personal essays should use meaningful reader-facing tags like `reflection`, `career`, `shanghai`, or `blogging` instead of `blog`.
\ No newline at end of file
diff --git a/public/blog-images/angular-declarative-templating/andrew-ridley-jR4Zf-riEjI-unsplash.jpg b/public/blog-images/angular-declarative-templating/andrew-ridley-jR4Zf-riEjI-unsplash.jpg
new file mode 100644
index 00000000..3f01c64b
Binary files /dev/null and b/public/blog-images/angular-declarative-templating/andrew-ridley-jR4Zf-riEjI-unsplash.jpg differ
diff --git a/public/blog-images/angular-declarative-templating/blink.gif b/public/blog-images/angular-declarative-templating/blink.gif
new file mode 100644
index 00000000..857ecce9
Binary files /dev/null and b/public/blog-images/angular-declarative-templating/blink.gif differ
diff --git a/public/blog-images/angular-declarative-templating/plaint-flashing.png b/public/blog-images/angular-declarative-templating/plaint-flashing.png
new file mode 100644
index 00000000..364602f8
Binary files /dev/null and b/public/blog-images/angular-declarative-templating/plaint-flashing.png differ
diff --git a/public/blog-images/angular-input-output-asymmetry/andrew-ridley-jR4Zf-riEjI-unsplash.jpg b/public/blog-images/angular-input-output-asymmetry/andrew-ridley-jR4Zf-riEjI-unsplash.jpg
new file mode 100644
index 00000000..3f01c64b
Binary files /dev/null and b/public/blog-images/angular-input-output-asymmetry/andrew-ridley-jR4Zf-riEjI-unsplash.jpg differ
diff --git a/public/blog-images/angular-memory-leak/chrome-detached.png b/public/blog-images/angular-memory-leak/chrome-detached.png
new file mode 100644
index 00000000..bc5cbd67
Binary files /dev/null and b/public/blog-images/angular-memory-leak/chrome-detached.png differ
diff --git a/public/blog-images/angular-memory-leak/detached-nodes.png b/public/blog-images/angular-memory-leak/detached-nodes.png
new file mode 100644
index 00000000..b1c15b75
Binary files /dev/null and b/public/blog-images/angular-memory-leak/detached-nodes.png differ
diff --git a/public/blog-images/angular-memory-leak/dom-nodes.png b/public/blog-images/angular-memory-leak/dom-nodes.png
new file mode 100644
index 00000000..56a4b5d1
Binary files /dev/null and b/public/blog-images/angular-memory-leak/dom-nodes.png differ
diff --git a/public/blog-images/angular-memory-leak/performance-gc.png b/public/blog-images/angular-memory-leak/performance-gc.png
new file mode 100644
index 00000000..26bd4a4b
Binary files /dev/null and b/public/blog-images/angular-memory-leak/performance-gc.png differ
diff --git a/public/blog-images/cd-coalescing/larry-costales-YFR_x3MqpHc-unsplash.jpg b/public/blog-images/cd-coalescing/larry-costales-YFR_x3MqpHc-unsplash.jpg
new file mode 100644
index 00000000..845de262
Binary files /dev/null and b/public/blog-images/cd-coalescing/larry-costales-YFR_x3MqpHc-unsplash.jpg differ
diff --git a/public/blog-images/cd-coalescing/not-great.jpg b/public/blog-images/cd-coalescing/not-great.jpg
new file mode 100644
index 00000000..5dae35a6
Binary files /dev/null and b/public/blog-images/cd-coalescing/not-great.jpg differ
diff --git a/public/blog-images/cldr-angular/kyle-glenn-nXt5HtLmlgE-unsplash.jpg b/public/blog-images/cldr-angular/kyle-glenn-nXt5HtLmlgE-unsplash.jpg
new file mode 100644
index 00000000..f91a2e81
Binary files /dev/null and b/public/blog-images/cldr-angular/kyle-glenn-nXt5HtLmlgE-unsplash.jpg differ
diff --git a/public/blog-images/defer-part2/81t40e.jpg b/public/blog-images/defer-part2/81t40e.jpg
new file mode 100644
index 00000000..1bb01dc8
Binary files /dev/null and b/public/blog-images/defer-part2/81t40e.jpg differ
diff --git a/public/blog-images/performance-for-repeaters/16-17.png b/public/blog-images/performance-for-repeaters/16-17.png
new file mode 100644
index 00000000..4b49efa9
Binary files /dev/null and b/public/blog-images/performance-for-repeaters/16-17.png differ
diff --git a/public/blog-images/performance-for-repeaters/fw-comp.png b/public/blog-images/performance-for-repeaters/fw-comp.png
new file mode 100644
index 00000000..80b5650a
Binary files /dev/null and b/public/blog-images/performance-for-repeaters/fw-comp.png differ
diff --git a/public/blog-images/private-fields-downleveling-angular/andreas-weilguny-3kunGbtUQCE-unsplash.jpg b/public/blog-images/private-fields-downleveling-angular/andreas-weilguny-3kunGbtUQCE-unsplash.jpg
new file mode 100644
index 00000000..d1a6437e
Binary files /dev/null and b/public/blog-images/private-fields-downleveling-angular/andreas-weilguny-3kunGbtUQCE-unsplash.jpg differ
diff --git a/public/blog-images/tree-shakable-as-default/from-marwool-GbY8Xg5iTOA-unsplash.jpg b/public/blog-images/tree-shakable-as-default/from-marwool-GbY8Xg5iTOA-unsplash.jpg
new file mode 100644
index 00000000..2e59ab49
Binary files /dev/null and b/public/blog-images/tree-shakable-as-default/from-marwool-GbY8Xg5iTOA-unsplash.jpg differ
diff --git a/public/blog-images/understanding-effects/vackground-com-ZNkiEWL02mI-unsplash.jpg b/public/blog-images/understanding-effects/vackground-com-ZNkiEWL02mI-unsplash.jpg
new file mode 100644
index 00000000..42c81e93
Binary files /dev/null and b/public/blog-images/understanding-effects/vackground-com-ZNkiEWL02mI-unsplash.jpg differ
diff --git a/public/blog-images/v17-builders/transitions.png b/public/blog-images/v17-builders/transitions.png
new file mode 100644
index 00000000..7cd172fb
Binary files /dev/null and b/public/blog-images/v17-builders/transitions.png differ
diff --git a/public/blog-images/v17-change-detection/demo.png b/public/blog-images/v17-change-detection/demo.png
new file mode 100644
index 00000000..4d5dd35c
Binary files /dev/null and b/public/blog-images/v17-change-detection/demo.png differ
diff --git a/src/components/Card.tsx b/src/components/Card.tsx
index 711a2527..1b189515 100644
--- a/src/components/Card.tsx
+++ b/src/components/Card.tsx
@@ -6,9 +6,17 @@ export interface Props {
href?: string;
frontmatter: CollectionEntry<"blog">["data"];
secHeading?: boolean;
+ itemClassName?: string;
+ itemData?: Record;
}
-export default function Card({ href, frontmatter, secHeading = true }: Props) {
+export default function Card({
+ href,
+ frontmatter,
+ secHeading = true,
+ itemClassName = "",
+ itemData = {},
+}: Props) {
const { title, pubDatetime, modDatetime, description } = frontmatter;
const headerProps = {
@@ -17,7 +25,7 @@ export default function Card({ href, frontmatter, secHeading = true }: Props) {
};
return (
-
+
-
-
- Short Stories
-
-
Tags
@@ -150,7 +145,7 @@ const { activeNav } = Astro.props;
@apply flex w-full flex-col items-center sm:ml-2 sm:flex-row sm:justify-end sm:space-x-4 sm:py-0;
}
nav ul {
- @apply mt-4 grid w-44 grid-cols-2 grid-rows-5 gap-x-2 gap-y-2 sm:ml-0 sm:mt-0 sm:w-auto sm:gap-x-5 sm:gap-y-0;
+ @apply mt-4 grid w-44 grid-cols-2 grid-rows-4 gap-x-2 gap-y-2 sm:ml-0 sm:mt-0 sm:w-auto sm:gap-x-5 sm:gap-y-0;
}
nav ul li {
@apply col-span-2 flex items-center justify-center;
diff --git a/src/components/portfolio/PortfolioExperience.tsx b/src/components/portfolio/PortfolioExperience.tsx
index 18a8e129..7a9b0bd3 100644
--- a/src/components/portfolio/PortfolioExperience.tsx
+++ b/src/components/portfolio/PortfolioExperience.tsx
@@ -22,6 +22,60 @@ export interface PortfolioItem {
impact: string;
}
+interface ZhProfile {
+ tagline?: string;
+ summary?: string;
+ targetRole?: string;
+ education?: string;
+}
+
+interface ZhExperience {
+ id: string;
+ role?: string;
+ summary?: string;
+ details?: string[];
+ impact?: string;
+}
+
+interface ZhFeaturedSystem {
+ name?: string;
+ summary?: string;
+}
+
+interface ZhPersonalLab {
+ name?: string;
+ role?: string;
+ summary?: string;
+ details?: string[];
+}
+
+interface ZhUi {
+ backBlog?: string;
+ featuredSystemsLabel?: string;
+ featuredSystemsHeading?: string;
+ careerLabel?: string;
+ careerHeading?: string;
+ careerSubtext?: string;
+ personalLabLabel?: string;
+ openDetails?: string;
+ close?: string;
+ education?: string;
+ skillLabels?: {
+ coreFrontend?: string;
+ systemsBackend?: string;
+ workflow?: string;
+ };
+}
+
+interface ZhData {
+ profile?: ZhProfile;
+ metrics?: Array<{ value: string; label: string }>;
+ featuredSystems?: ZhFeaturedSystem[];
+ experiences?: ZhExperience[];
+ personalLab?: ZhPersonalLab;
+ ui?: ZhUi;
+}
+
export interface PortfolioData {
profile: {
name: string;
@@ -59,6 +113,7 @@ export interface PortfolioData {
details: string[];
tech: string[];
};
+ zh?: ZhData;
}
const accentClass = {
@@ -115,9 +170,11 @@ function TechGlyph({ type }: { type: PortfolioItem["icon"] }) {
function MagneticCard({
item,
+ openLabel,
onOpen,
}: {
item: PortfolioItem;
+ openLabel: string;
onOpen: () => void;
}) {
const x = useMotionValue(0);
@@ -164,7 +221,7 @@ function MagneticCard({
{item.summary}
- Open details
+ {openLabel}
→
@@ -195,6 +252,69 @@ function SkillCloud({ title, skills }: { title: string; skills: string[] }) {
export default function PortfolioExperience({ data }: { data: PortfolioData }) {
const [selected, setSelected] = useState
(null);
+ const [lang, setLang] = useState<"en" | "zh">("en");
+
+ const zh = data.zh;
+ const isZh = lang === "zh";
+
+ const t = (en: string, zhText: string | undefined) =>
+ isZh && zhText ? zhText : en;
+
+ const profile = { ...data.profile, ...(isZh ? zh?.profile : {}) };
+ const metrics = isZh && zh?.metrics ? zh.metrics : (data.metrics ?? []);
+
+ const featuredSystems = (data.featuredSystems ?? []).map((sys, i) => ({
+ ...sys,
+ name: t(sys.name, zh?.featuredSystems?.[i]?.name),
+ summary: t(sys.summary, zh?.featuredSystems?.[i]?.summary),
+ }));
+
+ const experiences = data.experiences.map(exp => {
+ const zhExp = zh?.experiences?.find(e => e.id === exp.id);
+ return {
+ ...exp,
+ role: t(exp.role, zhExp?.role),
+ summary: t(exp.summary, zhExp?.summary),
+ details: isZh && zhExp?.details ? zhExp.details : exp.details,
+ impact: t(exp.impact, zhExp?.impact),
+ };
+ });
+
+ const personalLab = data.personalLab
+ ? {
+ ...data.personalLab,
+ name: t(data.personalLab.name, zh?.personalLab?.name),
+ role: t(data.personalLab.role, zh?.personalLab?.role),
+ summary: t(data.personalLab.summary, zh?.personalLab?.summary),
+ details:
+ isZh && zh?.personalLab?.details
+ ? zh.personalLab.details
+ : data.personalLab.details,
+ }
+ : undefined;
+
+ const ui = {
+ backBlog: t("\u2190 Blog", zh?.ui?.backBlog),
+ featuredSystemsLabel: t("Featured Systems", zh?.ui?.featuredSystemsLabel),
+ featuredSystemsHeading: t(
+ "Enterprise scale, product taste.",
+ zh?.ui?.featuredSystemsHeading
+ ),
+ careerLabel: t("Career Timeline", zh?.ui?.careerLabel),
+ careerHeading: t("Impact first. Depth on tap.", zh?.ui?.careerHeading),
+ careerSubtext: t(
+ "Six roles compressed into portfolio cards. Click any card for the resume-level detail: stack, responsibility and impact.",
+ zh?.ui?.careerSubtext
+ ),
+ personalLabLabel: t("Personal Lab", zh?.ui?.personalLabLabel),
+ openDetails: t("Open details", zh?.ui?.openDetails),
+ close: t("Close", zh?.ui?.close),
+ education: t("Education", zh?.ui?.education),
+ coreFrontend: t("Core frontend", zh?.ui?.skillLabels?.coreFrontend),
+ systemsBackend: t("Systems & backend", zh?.ui?.skillLabels?.systemsBackend),
+ workflow: t("Workflow", zh?.ui?.skillLabels?.workflow),
+ };
+
const heroSkills = [
...data.skills.primary,
...data.skills.systems.slice(0, 4),
@@ -202,20 +322,22 @@ export default function PortfolioExperience({ data }: { data: PortfolioData }) {
];
return (
-
+
+ {/* Nav */}
- ← Blog
+ {ui.backBlog}
-
+
Website
+
+
+
+
-
+ {/* Hero */}
+
- {data.profile.targetRole}
+ {profile.targetRole}
{data.profile.name}
@@ -248,7 +387,7 @@ export default function PortfolioExperience({ data }: { data: PortfolioData }) {
- {data.profile.tagline}
+ {profile.tagline}
{heroSkills.map(skill => (
@@ -263,18 +402,18 @@ export default function PortfolioExperience({ data }: { data: PortfolioData }) {
rowan@portfolio:~$
cat mission.txt
-
{data.profile.summary}
+
{profile.summary}
- {(data.metrics ?? []).map(metric => (
+ {metrics.map(metric => (
))}
- {data.profile.education && (
+ {profile.education && (
- Education
+ {ui.education}
- {data.profile.education}
+ {profile.education}
)}
-
+
-
+ {/* Featured Systems */}
+
- Featured Systems
+ {ui.featuredSystemsLabel}
- Enterprise scale, product taste.
+ {ui.featuredSystemsHeading}
- {(data.featuredSystems ?? []).map(system => (
+ {featuredSystems.map((system, i) => (
@@ -322,7 +462,7 @@ export default function PortfolioExperience({ data }: { data: PortfolioData }) {
{system.summary}
- {system.tags.map(tag => (
+ {data.featuredSystems?.[i]?.tags.map(tag => (
))}
-
+
-
+ {/* Career Timeline */}
+
- Career Timeline
+ {ui.careerLabel}
- Impact first. Depth on tap.
+ {ui.careerHeading}
-
- Six roles compressed into portfolio cards. Click any card for the
- resume-level detail: stack, responsibility and impact.
-
+
{ui.careerSubtext}
-
- {data.experiences.map(item => (
+ {experiences.map(item => (
setSelected(item)}
/>
))}
-
+
-
-
+ {/* Skills */}
+
+
-
+
- {data.personalLab && (
-
+ {/* Personal Lab */}
+ {personalLab && (
+
- Personal Lab
+ {ui.personalLabLabel}
- {data.personalLab.name}
+ {personalLab.name}
- {data.personalLab.role}
+ {personalLab.role}
- {data.personalLab.summary}
+ {personalLab.summary}
- {data.personalLab.details.map(detail => (
+ {personalLab.details.map((detail, i) => (
-
{detail}
@@ -408,7 +548,7 @@ export default function PortfolioExperience({ data }: { data: PortfolioData }) {
))}
- {data.personalLab.tech.map(tech => (
+ {data.personalLab?.tech.map(tech => (
-
+
)}
+ {/* Detail modal */}
{selected && (
setSelected(null)}
className="rounded-full bg-white/70 px-4 py-2 text-sm font-bold text-[#202124] shadow-sm transition hover:bg-white"
>
- Close
+ {ui.close}
@@ -464,9 +605,9 @@ export default function PortfolioExperience({ data }: { data: PortfolioData }) {
{selected.impact}
- {selected.details.map(detail => (
+ {selected.details.map((detail, i) => (
-
{detail}
diff --git a/src/content/blog/eggjs-init.md b/src/content/blog/backend/eggjs-init.md
similarity index 99%
rename from src/content/blog/eggjs-init.md
rename to src/content/blog/backend/eggjs-init.md
index c9745dda..3ed7cd54 100644
--- a/src/content/blog/eggjs-init.md
+++ b/src/content/blog/backend/eggjs-init.md
@@ -5,7 +5,7 @@ slug: eggjs-init
featured: false
draft: false
tags:
- - node
+ - nodejs
description: "``` npm init egg --type=simple ``` yarn init 不好使。 之前曾经在本地创建过mysql的环境,但是很久不用也就没有启动了,但是这次无论如何也不能成功启动起来。"
---
diff --git a/src/content/blog/nestjs-oauth2.md b/src/content/blog/backend/nestjs-oauth2.md
similarity index 99%
rename from src/content/blog/nestjs-oauth2.md
rename to src/content/blog/backend/nestjs-oauth2.md
index cda50098..fab05d4f 100644
--- a/src/content/blog/nestjs-oauth2.md
+++ b/src/content/blog/backend/nestjs-oauth2.md
@@ -6,7 +6,7 @@ featured: false
draft: false
tags:
- nestjs
- - node
+ - nodejs
description: "OAuth 2.0 是一种授权机制,主要用来颁发令牌。 给快递员一个7天的授权码,这样不提供自己的密码即可让快递员进入小区,这样的授权机制是安全的,当时间到了之后如果仍然有需求可以二次授权,否则就自动过期"
---
diff --git a/src/content/blog/nestjs-passport-apis.md b/src/content/blog/backend/nestjs-passport-apis.md
similarity index 99%
rename from src/content/blog/nestjs-passport-apis.md
rename to src/content/blog/backend/nestjs-passport-apis.md
index ab3c92e3..910195f4 100644
--- a/src/content/blog/nestjs-passport-apis.md
+++ b/src/content/blog/backend/nestjs-passport-apis.md
@@ -6,8 +6,7 @@ featured: false
draft: false
tags:
- nestjs
- - node
- - translation
+ - nodejs
description: "连同定义 HTTP 的身份验证框架,[RFC 2617](http://tools.ietf.org/html/rfc2617) 也定义了基本和摘要认证方案。这两种方案都使用用户名和密码作为凭证来认证用户,并且通常用于保护 API 端点。"
---
diff --git a/src/content/blog/nestjs-passport-general.md b/src/content/blog/backend/nestjs-passport-general.md
similarity index 99%
rename from src/content/blog/nestjs-passport-general.md
rename to src/content/blog/backend/nestjs-passport-general.md
index 2ddf4734..6ce1e80e 100644
--- a/src/content/blog/nestjs-passport-general.md
+++ b/src/content/blog/backend/nestjs-passport-general.md
@@ -6,8 +6,7 @@ featured: false
draft: false
tags:
- nestjs
- - node
- - translation
+ - nodejs
description: "Passport 是 NodeJS 的认证中间件。他的唯一设计目的是:验证请求。书写模块化的、封装代码是一种美德,所以 Passport 将除了验证请求之外的功能都分发给应用程序来实现。关注点分离使代码能够更加整洁、可维护,同时也使 Passport 能够极易集成到应用中。"
---
diff --git a/src/content/blog/nestjs-passport-operations.md b/src/content/blog/backend/nestjs-passport-operations.md
similarity index 99%
rename from src/content/blog/nestjs-passport-operations.md
rename to src/content/blog/backend/nestjs-passport-operations.md
index 7a60a63f..2fd6665b 100644
--- a/src/content/blog/nestjs-passport-operations.md
+++ b/src/content/blog/backend/nestjs-passport-operations.md
@@ -6,8 +6,7 @@ featured: false
draft: false
tags:
- nestjs
- - node
- - translation
+ - nodejs
description: "Passport 暴露一个 `login()` 函数在 `req`(也有 `logIn()` 这个别名)上,它能够在建立登录 session 时使用。 ```js"
---
diff --git a/src/content/blog/nestjs-passport-overview.md b/src/content/blog/backend/nestjs-passport-overview.md
similarity index 99%
rename from src/content/blog/nestjs-passport-overview.md
rename to src/content/blog/backend/nestjs-passport-overview.md
index 4bfc1ef1..acaa16cf 100644
--- a/src/content/blog/nestjs-passport-overview.md
+++ b/src/content/blog/backend/nestjs-passport-overview.md
@@ -6,7 +6,7 @@ featured: false
draft: false
tags:
- nestjs
- - node
+ - nodejs
description: "权限管理是所有系统都会有的一部分,所以我们要了解一下 nodejs 中权限部分是怎么做的,都涉及哪些模块。 Passport 是一个简单、低调的 nodejs 身份验证中间件。它是弹性的、模块化的,能够集成到任何基于 express 的 web 应用。支持一系列权限策略:账号密码、Facebook、"
---
diff --git a/src/content/blog/nestjs-passport-providers.md b/src/content/blog/backend/nestjs-passport-providers.md
similarity index 99%
rename from src/content/blog/nestjs-passport-providers.md
rename to src/content/blog/backend/nestjs-passport-providers.md
index 84cb5430..7c261c82 100644
--- a/src/content/blog/nestjs-passport-providers.md
+++ b/src/content/blog/backend/nestjs-passport-providers.md
@@ -6,8 +6,7 @@ featured: false
draft: false
tags:
- nestjs
- - node
- - translation
+ - nodejs
description: "Facebook 策略允许用户使用他们的 Facebook 账户登录一个 web 应用。在内部,Facebook 认证使用 OAuth 2.0 进行工作。 Facebook 支持通过 [passport-facebook](https://github.com/jaredhanson/passpor"
---
diff --git a/src/content/blog/nodejs-typescript-modern-2026.md b/src/content/blog/backend/nodejs-typescript-modern-2026.md
similarity index 100%
rename from src/content/blog/nodejs-typescript-modern-2026.md
rename to src/content/blog/backend/nodejs-typescript-modern-2026.md
diff --git a/src/content/blog/oauth2-pkce-bff-modern-auth.md b/src/content/blog/backend/oauth2-pkce-bff-modern-auth.md
similarity index 99%
rename from src/content/blog/oauth2-pkce-bff-modern-auth.md
rename to src/content/blog/backend/oauth2-pkce-bff-modern-auth.md
index 1214d988..49964d5e 100644
--- a/src/content/blog/oauth2-pkce-bff-modern-auth.md
+++ b/src/content/blog/backend/oauth2-pkce-bff-modern-auth.md
@@ -8,7 +8,6 @@ tags:
- oauth
- security
- nestjs
- - frontend
description: 回顾 2020 年 OAuth 2.0 方案的安全隐患,深入讲解 OAuth 2.1 的核心变化、PKCE 流程、BFF 模式,以及如何用 NestJS + Passport 构建安全的现代认证网关。
---
diff --git a/src/content/blog/translate-nodejs-best-practices.md b/src/content/blog/backend/translate-nodejs-best-practices.md
similarity index 99%
rename from src/content/blog/translate-nodejs-best-practices.md
rename to src/content/blog/backend/translate-nodejs-best-practices.md
index 73945d5b..98ba4a74 100644
--- a/src/content/blog/translate-nodejs-best-practices.md
+++ b/src/content/blog/backend/translate-nodejs-best-practices.md
@@ -5,8 +5,7 @@ slug: translate-nodejs-best-practices
featured: false
draft: false
tags:
- - translation
- - node
+ - nodejs
description: "虽然 Node.js 才诞生 11 年,但是已经是过去 10 年来最流行的 web 开发框架之一。我是一个终极粉丝,感谢 Node.js,让我能够在浏览器外部写 JavaScript 代码,去创建一个非阻塞的、流畅的、快速的、健壮的、可扩展的服务端应用程序。"
---
diff --git a/src/content/blog/daily-english-reading-2026-03-21-ai-technology.md b/src/content/blog/english/daily-english-reading-2026-03-21-ai-technology.md
similarity index 99%
rename from src/content/blog/daily-english-reading-2026-03-21-ai-technology.md
rename to src/content/blog/english/daily-english-reading-2026-03-21-ai-technology.md
index d08199f0..64a4aeb0 100644
--- a/src/content/blog/daily-english-reading-2026-03-21-ai-technology.md
+++ b/src/content/blog/english/daily-english-reading-2026-03-21-ai-technology.md
@@ -1,8 +1,9 @@
---
title: "Daily English Reading: How AI is Transforming Our Daily Lives in 2026"
+slug: daily-english-reading-2026-03-21-ai-technology
pubDatetime: 2026-03-21T04:00:00+08:00
description: "B2-level English reading article about artificial intelligence with audio narration and Chinese translation"
-tags: ["English Learning", "AI", "Technology", "Daily Reading"]
+tags: ["english-learning", "daily-reading", "ai"]
audioUrl: "https://reading-audios-1308187607.cos.ap-shanghai.myqcloud.com/daily-reading/2026-03-21-ai-technology.mp3"
---
@@ -129,6 +130,7 @@ The AI revolution is not coming—it has arrived. And it's reshaping our world i
5. **复述内容**:用自己的话总结文章要点
**重点词汇**:
+
- artificial intelligence (AI) - 人工智能
- revolutionize - 彻底改变
- seamless - 无缝的
diff --git a/src/content/blog/daily-english-reading-2026-03-22-mental-health.md b/src/content/blog/english/daily-english-reading-2026-03-22-mental-health.md
similarity index 99%
rename from src/content/blog/daily-english-reading-2026-03-22-mental-health.md
rename to src/content/blog/english/daily-english-reading-2026-03-22-mental-health.md
index f5176c89..8974a78e 100644
--- a/src/content/blog/daily-english-reading-2026-03-22-mental-health.md
+++ b/src/content/blog/english/daily-english-reading-2026-03-22-mental-health.md
@@ -1,8 +1,9 @@
---
title: "Daily English Reading: The Science of Mental Wellbeing in Modern Life"
+slug: daily-english-reading-2026-03-22-mental-health
pubDatetime: 2026-03-22T04:00:00+08:00
description: "B2-level English reading article about mental health and wellbeing with audio narration and Chinese translation"
-tags: ["English Learning", "Mental Health", "Wellbeing", "Daily Reading"]
+tags: ["english-learning", "daily-reading", "mental-health"]
audioUrl: "https://reading-audios-1308187607.cos.ap-shanghai.myqcloud.com/daily-reading/2026-03-22-mental-health.mp3"
---
@@ -141,6 +142,7 @@ As we move through 2026, the mental health conversation continues evolving. We'r
5. **复述内容**:用自己的话总结文章要点
**重点词汇**:
+
- mental wellbeing - 心理健康
- resilience - 韧性,恢复力
- cognitive behavioral - 认知行为的
diff --git a/src/content/blog/daily-english-reading-2026-03-23-climate-change.md b/src/content/blog/english/daily-english-reading-2026-03-23-climate-change.md
similarity index 99%
rename from src/content/blog/daily-english-reading-2026-03-23-climate-change.md
rename to src/content/blog/english/daily-english-reading-2026-03-23-climate-change.md
index a53dc97f..bdf12ac6 100644
--- a/src/content/blog/daily-english-reading-2026-03-23-climate-change.md
+++ b/src/content/blog/english/daily-english-reading-2026-03-23-climate-change.md
@@ -1,8 +1,9 @@
---
title: "Daily English Reading: Climate Action in 2026: Progress and Challenges"
+slug: daily-english-reading-2026-03-23-climate-change
pubDatetime: 2026-03-23T04:00:00+08:00
description: "B2-level English reading article about climate change and environmental action with audio narration and Chinese translation"
-tags: ["English Learning", "Climate Change", "Environment", "Daily Reading"]
+tags: ["english-learning", "daily-reading", "climate-change"]
audioUrl: "https://reading-audios-1308187607.cos.ap-shanghai.myqcloud.com/daily-reading/2026-03-23-climate-change.mp3"
---
@@ -153,6 +154,7 @@ The question is no longer whether we can address climate change, but whether we
5. **复述内容**:用自己的话总结文章要点
**重点词汇**:
+
- climate change - 气候变化
- renewable energy - 可再生能源
- carbon dioxide - 二氧化碳
diff --git a/src/content/blog/daily-english-reading-2026-03-24-ai-companions.md b/src/content/blog/english/daily-english-reading-2026-03-24-ai-companions.md
similarity index 98%
rename from src/content/blog/daily-english-reading-2026-03-24-ai-companions.md
rename to src/content/blog/english/daily-english-reading-2026-03-24-ai-companions.md
index a8d130d5..8ffe87a0 100644
--- a/src/content/blog/daily-english-reading-2026-03-24-ai-companions.md
+++ b/src/content/blog/english/daily-english-reading-2026-03-24-ai-companions.md
@@ -1,8 +1,9 @@
---
title: "Daily English Reading: The Rise of AI Companions"
+slug: daily-english-reading-2026-03-24-ai-companions
pubDatetime: 2026-03-24T04:00:00+08:00
description: "B2-level English reading article about AI companions and how technology is reshaping human connection with audio narration"
-tags: ["English Learning", "Technology", "AI", "Daily Reading"]
+tags: ["english-learning", "daily-reading", "ai"]
audioUrl: "https://reading-audios-1308187607.cos.ap-shanghai.myqcloud.com/daily-reading/2026-03-24-ai-companions.mp3"
---
diff --git a/src/content/blog/daily-english-reading-2026-03-27-digital-privacy.md b/src/content/blog/english/daily-english-reading-2026-03-27-digital-privacy.md
similarity index 98%
rename from src/content/blog/daily-english-reading-2026-03-27-digital-privacy.md
rename to src/content/blog/english/daily-english-reading-2026-03-27-digital-privacy.md
index c813f023..f96ae902 100644
--- a/src/content/blog/daily-english-reading-2026-03-27-digital-privacy.md
+++ b/src/content/blog/english/daily-english-reading-2026-03-27-digital-privacy.md
@@ -1,8 +1,9 @@
---
title: "Daily English Reading: Digital Privacy in the Modern Age"
+slug: daily-english-reading-2026-03-27-digital-privacy
pubDatetime: 2026-03-27T04:00:00+08:00
description: "B2-level English reading article about digital privacy and data protection with audio narration"
-tags: ["English Learning", "Technology", "Privacy", "Daily Reading"]
+tags: ["english-learning", "daily-reading", "privacy"]
audioUrl: ""
---
diff --git a/src/content/blog/daily-english-reading-2026-03-28-green-tech.md b/src/content/blog/english/daily-english-reading-2026-03-28-green-tech.md
similarity index 98%
rename from src/content/blog/daily-english-reading-2026-03-28-green-tech.md
rename to src/content/blog/english/daily-english-reading-2026-03-28-green-tech.md
index c1adfaaf..760a3636 100644
--- a/src/content/blog/daily-english-reading-2026-03-28-green-tech.md
+++ b/src/content/blog/english/daily-english-reading-2026-03-28-green-tech.md
@@ -1,8 +1,9 @@
---
title: "Daily English Reading: Green Technology and Sustainable Living"
+slug: daily-english-reading-2026-03-28-green-tech
pubDatetime: 2026-03-28T04:00:00+08:00
description: "B2-level English reading about sustainable technology and eco-friendly lifestyle"
-tags: ["English Learning", "Environment", "Technology", "Daily Reading"]
+tags: ["english-learning", "daily-reading", "environment"]
audioUrl: ""
---
diff --git a/src/content/blog/daily-english-reading-2026-03-29-mental-wellness.md b/src/content/blog/english/daily-english-reading-2026-03-29-mental-wellness.md
similarity index 98%
rename from src/content/blog/daily-english-reading-2026-03-29-mental-wellness.md
rename to src/content/blog/english/daily-english-reading-2026-03-29-mental-wellness.md
index 15b549df..06fa1975 100644
--- a/src/content/blog/daily-english-reading-2026-03-29-mental-wellness.md
+++ b/src/content/blog/english/daily-english-reading-2026-03-29-mental-wellness.md
@@ -1,8 +1,9 @@
---
title: "Daily English Reading: Mental Wellness in Modern Life"
+slug: daily-english-reading-2026-03-29-mental-wellness
pubDatetime: 2026-03-29T04:00:00+08:00
description: "B2-level English reading about mental health and well-being in contemporary society"
-tags: ["English Learning", "Health", "Lifestyle", "Daily Reading"]
+tags: ["english-learning", "daily-reading", "mental-health"]
audioUrl: ""
---
diff --git a/src/content/blog/daily-english-reading-2026-03-30-remote-work.md b/src/content/blog/english/daily-english-reading-2026-03-30-remote-work.md
similarity index 99%
rename from src/content/blog/daily-english-reading-2026-03-30-remote-work.md
rename to src/content/blog/english/daily-english-reading-2026-03-30-remote-work.md
index f44c600b..b66bb46c 100644
--- a/src/content/blog/daily-english-reading-2026-03-30-remote-work.md
+++ b/src/content/blog/english/daily-english-reading-2026-03-30-remote-work.md
@@ -1,8 +1,9 @@
---
title: "Daily English Reading: The Future of Remote Work"
+slug: daily-english-reading-2026-03-30-remote-work
pubDatetime: 2026-03-30T04:00:00+08:00
description: "B2-level English reading about remote work trends and digital collaboration"
-tags: ["English Learning", "Work", "Technology", "Daily Reading"]
+tags: ["english-learning", "daily-reading", "work"]
audioUrl: ""
---
diff --git a/src/content/blog/daily-english-reading-2026-03-31-space-exploration.md b/src/content/blog/english/daily-english-reading-2026-03-31-space-exploration.md
similarity index 99%
rename from src/content/blog/daily-english-reading-2026-03-31-space-exploration.md
rename to src/content/blog/english/daily-english-reading-2026-03-31-space-exploration.md
index 2d77fed1..56b70448 100644
--- a/src/content/blog/daily-english-reading-2026-03-31-space-exploration.md
+++ b/src/content/blog/english/daily-english-reading-2026-03-31-space-exploration.md
@@ -1,8 +1,9 @@
---
title: "Daily English Reading: The New Era of Space Exploration"
+slug: daily-english-reading-2026-03-31-space-exploration
pubDatetime: 2026-03-31T04:00:00+08:00
description: "B2-level English reading about space exploration, Mars missions, and commercial spaceflight"
-tags: ["English Learning", "Science", "Space", "Daily Reading"]
+tags: ["english-learning", "daily-reading", "science"]
audioUrl: ""
---
diff --git a/src/content/blog/daily-english-reading-digital-detox.md b/src/content/blog/english/daily-english-reading-digital-detox.md
similarity index 99%
rename from src/content/blog/daily-english-reading-digital-detox.md
rename to src/content/blog/english/daily-english-reading-digital-detox.md
index 9a9bf467..2dbd695e 100644
--- a/src/content/blog/daily-english-reading-digital-detox.md
+++ b/src/content/blog/english/daily-english-reading-digital-detox.md
@@ -5,10 +5,9 @@ slug: daily-english-reading-digital-detox
featured: false
draft: false
tags:
- - English Learning
- - Daily Reading
- - B2 Level
- - Lifestyle & Technology
+ - english-learning
+ - daily-reading
+ - lifestyle
description: Today's B2 level English reading article about digital detox - why unplugging from technology can improve your mental health and productivity, with audio and Chinese translation.
---
diff --git a/src/content/blog/daily-english-reading-forever-chemicals.md b/src/content/blog/english/daily-english-reading-forever-chemicals.md
similarity index 98%
rename from src/content/blog/daily-english-reading-forever-chemicals.md
rename to src/content/blog/english/daily-english-reading-forever-chemicals.md
index 20a0123f..9ce3f9f0 100644
--- a/src/content/blog/daily-english-reading-forever-chemicals.md
+++ b/src/content/blog/english/daily-english-reading-forever-chemicals.md
@@ -5,10 +5,9 @@ slug: daily-english-reading-forever-chemicals
featured: false
draft: false
tags:
- - English Learning
- - Daily Reading
- - B2 Level
- - Health & Science
+ - english-learning
+ - daily-reading
+ - health
description: Today's B2 level English reading article about PFAS (forever chemicals) and their impact on health and environment, with audio and Chinese translation.
---
diff --git a/src/content/blog/daily-english-reading-science-of-sleep.md b/src/content/blog/english/daily-english-reading-science-of-sleep.md
similarity index 99%
rename from src/content/blog/daily-english-reading-science-of-sleep.md
rename to src/content/blog/english/daily-english-reading-science-of-sleep.md
index 5d4566e9..d8b65c7c 100644
--- a/src/content/blog/daily-english-reading-science-of-sleep.md
+++ b/src/content/blog/english/daily-english-reading-science-of-sleep.md
@@ -5,10 +5,9 @@ slug: daily-english-reading-science-of-sleep
featured: false
draft: false
tags:
- - English Learning
- - Daily Reading
- - B2 Level
- - Health & Wellness
+ - english-learning
+ - daily-reading
+ - health
description: Today's B2 level English reading article about the science of sleep, its importance for health, and tips for better rest, with audio and Chinese translation.
---
diff --git a/src/content/blog/daily-english-reading-sleep-affects-brain.md b/src/content/blog/english/daily-english-reading-sleep-affects-brain.md
similarity index 99%
rename from src/content/blog/daily-english-reading-sleep-affects-brain.md
rename to src/content/blog/english/daily-english-reading-sleep-affects-brain.md
index db8e5c6d..b8af7b34 100644
--- a/src/content/blog/daily-english-reading-sleep-affects-brain.md
+++ b/src/content/blog/english/daily-english-reading-sleep-affects-brain.md
@@ -5,10 +5,9 @@ slug: daily-english-reading-sleep-affects-brain
featured: false
draft: false
tags:
- - English Learning
- - Daily Reading
- - B2 Level
- - Health & Science
+ - english-learning
+ - daily-reading
+ - health
description: Today's B2 level English reading article about how sleep affects your brain, with audio and Chinese translation.
---
diff --git a/src/content/blog/how-to-nap-like-a-pro.md b/src/content/blog/english/how-to-nap-like-a-pro.md
similarity index 99%
rename from src/content/blog/how-to-nap-like-a-pro.md
rename to src/content/blog/english/how-to-nap-like-a-pro.md
index 8d5a53e1..1ebfccb6 100644
--- a/src/content/blog/how-to-nap-like-a-pro.md
+++ b/src/content/blog/english/how-to-nap-like-a-pro.md
@@ -5,9 +5,9 @@ slug: how-to-nap-like-a-pro
featured: false
draft: false
tags:
- - English Learning
- - Daily Reading
- - B2 Level
+ - english-learning
+ - daily-reading
+ - health
description: GQ Wellness article about how to nap properly.
---
diff --git a/src/content/blog/roy-pacis-1.md b/src/content/blog/english/roy-pacis-1.md
similarity index 99%
rename from src/content/blog/roy-pacis-1.md
rename to src/content/blog/english/roy-pacis-1.md
index 7e6f6949..925fdd13 100644
--- a/src/content/blog/roy-pacis-1.md
+++ b/src/content/blog/english/roy-pacis-1.md
@@ -5,7 +5,8 @@ slug: english-teacher-1
featured: false
draft: false
tags:
- - English Learning
+ - english-learning
+ - reflection
description: 有的时候过来人的一句话,就会对你的生活产生巨大的改变,感恩每个遇到的人。
---
diff --git a/src/content/blog/roy-pacis-2.md b/src/content/blog/english/roy-pacis-2.md
similarity index 93%
rename from src/content/blog/roy-pacis-2.md
rename to src/content/blog/english/roy-pacis-2.md
index eeb90db2..1b4f7bb3 100644
--- a/src/content/blog/roy-pacis-2.md
+++ b/src/content/blog/english/roy-pacis-2.md
@@ -1,13 +1,13 @@
---
pubDatetime: 2023-12-03T10:09:00Z
title: English Teacher 2
-slug: english-teacher-2
-featured: false
+slug: english-teacher-2
+featured: false
draft: false
tags:
- - English Learning
-description:
- 有时自己有种想拯救苍生的情节,那么就从身边的人开始。
+ - english-learning
+ - reflection
+description: 有时自己有种想拯救苍生的情节,那么就从身边的人开始。
---
Roy 之前跟我提到过,如果有其他想学习英语的学生可以推荐给他,一个是确实没有合适的人。一个是当一个人主动让你做点什么时候,自己的内心其实没那么想去做,没有动力。
@@ -22,4 +22,4 @@ Roy 之前跟我提到过,如果有其他想学习英语的学生可以推荐
Tom是程序员,可能程序员还是没有产品那么有EQ 情商吧,我总得找些话来劝说Roy,当然可能这也是程序员被黑的比较惨的一次。我其实自己也不能理解,一个人请你帮忙说些好话,以便能够招到更多学生,为什么你说的会是有负向作用的“真话”。可能真的是 EQ 的原因。
-这就是Roy的故事。后续我还会跟他继续学习,提高英语技能;同时我也会让 Julianna 我的女儿也学习英语,不过可能刚开始的时候会让她跟欧美外教学习,后面再转为菲教,小孩子的口音养成还是很重要的。
\ No newline at end of file
+这就是Roy的故事。后续我还会跟他继续学习,提高英语技能;同时我也会让 Julianna 我的女儿也学习英语,不过可能刚开始的时候会让她跟欧美外教学习,后面再转为菲教,小孩子的口音养成还是很重要的。
diff --git a/src/content/blog/roy-pacis-3.md b/src/content/blog/english/roy-pacis-3.md
similarity index 90%
rename from src/content/blog/roy-pacis-3.md
rename to src/content/blog/english/roy-pacis-3.md
index 3764a5c1..54593665 100644
--- a/src/content/blog/roy-pacis-3.md
+++ b/src/content/blog/english/roy-pacis-3.md
@@ -1,13 +1,13 @@
---
pubDatetime: 2023-12-05T12:09:00Z
title: English Teacher 3
-slug: english-teacher-3
-featured: false
+slug: english-teacher-3
+featured: false
draft: false
tags:
- - English Learning
-description:
- 截止到现在 2024年1月22日09:05:19,已经成功推荐5个学生(Conrad, Leo, Klay, A girl, Robert)跟Roy学习。
+ - english-learning
+ - reflection
+description: 截止到现在 2024年1月22日09:05:19,已经成功推荐5个学生(Conrad, Leo, Klay, A girl, Robert)跟Roy学习。
---
其中3个是在电鸭论坛发文后就咨询然后跟随Roy学习,这个第4个 girl 是过了2周后,自己找到Roy学习,并没有咨询我,但是可以很确定的是通过邮件联系到Roy,还是那个帖子起了效果。
@@ -33,4 +33,4 @@ description:
推荐模式对我的启发
-如果后面有机会做独立产品,也可以采用这种模式,首先攒下种子用户,然后如果他们能够推荐给身边的人,那么就有机会被很多人使用了。
\ No newline at end of file
+如果后面有机会做独立产品,也可以采用这种模式,首先攒下种子用户,然后如果他们能够推荐给身边的人,那么就有机会被很多人使用了。
diff --git a/src/content/blog/roy-pacis-4.md b/src/content/blog/english/roy-pacis-4.md
similarity index 92%
rename from src/content/blog/roy-pacis-4.md
rename to src/content/blog/english/roy-pacis-4.md
index 6f4798ac..f2544d99 100644
--- a/src/content/blog/roy-pacis-4.md
+++ b/src/content/blog/english/roy-pacis-4.md
@@ -5,9 +5,9 @@ slug: english-teacher-4
featured: false
draft: false
tags:
- - English Learning
-description:
- 跟着Roy学习英语已经有很长一段时间了,总的来说我的想法,菲教适合入门,开口,仅此而已。
+ - english-learning
+ - reflection
+description: 跟着Roy学习英语已经有很长一段时间了,总的来说我的想法,菲教适合入门,开口,仅此而已。
---
最开始跟着他学习的时候,学习了两本书之后,他就是因为态度不好,被巨嘴鸟平台拒签合同,然后我不知道脑子里在想什么就稀里糊涂的跟着他转到了平台之外,回想起来这是我不应该做的事情。
@@ -20,4 +20,4 @@ description:
然后目前我有在尝试新的学习平台,比如amazingTalker,ai推荐的一个平台,开始跟一个us老师学习,但是觉得老师有些慢节奏,5节课结束后,就换了新的一个us老师,老师的语速更快,感觉能够学到的东西更多一些。第二个是Trevor老师,讲的课程主要是ITLS,其实我不知道自己是否学的正确与否,因为我没有考雅思的想法,只是希望自己的口语和听力能力能够有所提高,然后能够在目前的职场上在英语方面更游刃有余一些。
-所以,总结一下,菲教和us老师,都对我的英语提高起到了一定的作用,还是很感谢他们的。那么后续在Roy的20节课结束之后,暂时不会考虑续订他的课了。然后对于Trevor目前上了6节课大概(每节课100rmb,和菲教比还是小贵),整体感受还行,再继续体会体会。那对于Roy的这种普通口语课,在探索是否能够通过跟ai学习的方式来实现。(能够想到的一个prompt如下,请帮我规划一个英语学习的idea,通过比如shadowing练习,ai读一句,我重复一句,5个句子结束后,进行总体的评分,给出反馈,同时希望这个挑战能够有上瘾性,或者说更加游戏化,从而激发参与者的兴趣。同时还应该考虑针对参与者的系统性记录,每次的句子应该针对当前参与者的level,对于错误的句子进行记录,并且考虑将费曼学习法融入进去,从而使学习者能够掌握内容,而不是复述结束就结束了)。
\ No newline at end of file
+所以,总结一下,菲教和us老师,都对我的英语提高起到了一定的作用,还是很感谢他们的。那么后续在Roy的20节课结束之后,暂时不会考虑续订他的课了。然后对于Trevor目前上了6节课大概(每节课100rmb,和菲教比还是小贵),整体感受还行,再继续体会体会。那对于Roy的这种普通口语课,在探索是否能够通过跟ai学习的方式来实现。(能够想到的一个prompt如下,请帮我规划一个英语学习的idea,通过比如shadowing练习,ai读一句,我重复一句,5个句子结束后,进行总体的评分,给出反馈,同时希望这个挑战能够有上瘾性,或者说更加游戏化,从而激发参与者的兴趣。同时还应该考虑针对参与者的系统性记录,每次的句子应该针对当前参与者的level,对于错误的句子进行记录,并且考虑将费曼学习法融入进去,从而使学习者能够掌握内容,而不是复述结束就结束了)。
diff --git a/src/content/blog/angular-change-detection.md b/src/content/blog/frontend/angular/angular-change-detection.md
similarity index 100%
rename from src/content/blog/angular-change-detection.md
rename to src/content/blog/frontend/angular/angular-change-detection.md
diff --git a/src/content/blog/frontend/angular/angular-declarative-templating.md b/src/content/blog/frontend/angular/angular-declarative-templating.md
new file mode 100644
index 00000000..5c48995e
--- /dev/null
+++ b/src/content/blog/frontend/angular/angular-declarative-templating.md
@@ -0,0 +1,252 @@
+---
+pubDatetime: 2026-05-30T09:30:00+08:00
+title: "[译] Angular 声明式模板如何实现高性能渲染"
+slug: angular-declarative-templating
+featured: false
+draft: true
+isTranslation: true
+tags:
+ - angular
+ - rendering
+ - performance
+ - change-detection
+description: 本文从 Angular Ivy 编译产物入手,解释声明式模板如何通过创建/更新模式、节点复用、OnPush 与脏检查、以及插值记忆化来减少 DOM 操作。
+canonicalURL: https://riegler.fr/blog/2025-02-16-incremental-dom/
+---
+
+> 原文地址: https://riegler.fr/blog/2025-02-16-incremental-dom/
+
+像 Angular 这样的 Web 框架,最初就是为了解决“直接操作 DOM”带来的复杂度而诞生的。它们为开发者提供了更声明式、更高效的方式来构建动态 UI。使用原生 JavaScript API 直接操作 DOM 通常笨重、容易出错,而且性能开销高,因为 DOM 操作本身就很昂贵。为此,框架引入了抽象层,例如 Angular 的声明式模板,或 React 的 Virtual DOM,以优化变更应用到 DOM 的过程。通过批量更新、减少直接 DOM 访问、利用差异计算算法,框架显著提升了渲染性能与开发效率。归根结底,它们在命令式 DOM API 与现代声明式组件开发之间搭建了桥梁。
+
+简单说,框架的职责之一,就是尽可能减少 DOM 操作。本文将深入 Angular 为优化 DOM 操作所采用的不同策略。
+
+## Angular 的渲染方式:声明式模板
+
+早在 2019 年,Angular 团队发布了新的渲染引擎 Ivy。顺带一提,Ivy 的故事在 [Honeypot 的 Angular 纪录片](https://www.youtube.com/watch?v=cRC9DlH45lA) 中也有提及。
+
+Ivy 的核心思想是:每个组件模板都会被编译成一组 JavaScript 指令,这些指令组成一个模板函数。
+
+
+
+
+
+#### `一个基础组件模板`
+
+```html
+
this is a div, with a value: {{ myValue }}
+```
+
+
+
+
+
+#### `对应的指令表示`
+
+```js
+function MyComponentTemplateFn(rf, ctx) {
+ if (rf & 1) {
+ /* creation mode */
+ ɵɵelementStart(0, "div");
+ ɵɵtext(1);
+ ɵɵelementEnd();
+ }
+ if (rf & 2) {
+ /* update mode */
+ ɵɵadvance();
+ ɵɵtextInterpolate1("this is a div, with a value: ", ctx.myValue, "");
+ }
+}
+```
+
+
+
+
+你可以通过[我做的这个 demo](https://jeanmeche.github.io/angular-compiler-output/)自己试试,它可以展示你写出的任意 Angular 模板的编译指令。
+
+可以看到,这个模板函数被分成两个阶段:
+
+- 创建阶段(creation mode)
+- 更新阶段(update mode)
+
+### 创建阶段
+
+模板函数的创建阶段,是框架初始化并创建组件 DOM 结构的阶段。
+在这个阶段里,Ivy 会执行一系列模板指令:创建 DOM 元素、绑定属性、建立初始数据绑定。
+
+创建阶段本质上是渲染过程的第一步:把组件模板变成“活的”DOM 结构。
+
+在前面的示例中,创建阶段会创建两个节点:一个 DIV 节点,里面有一个(此时为空的)文本节点。
+
+### 更新阶段
+
+创建阶段完成后,Angular 进入更新阶段。这个阶段处理组件状态变化,并按需更新 DOM。将创建与更新分离,让 Ivy 可以只在必要时修改内容,而不是整棵 DOM 重建。
+
+在前面的示例中,更新阶段会把文本插值的内容写入文本节点。
+
+### 可视化创建/更新差异:DOM 变化提示
+
+我们可以直观看到创建阶段与更新阶段的差异。
+例如,Chrome DevTools 在 Rendering 选项卡里有一个功能:让被更新的 DOM 元素闪烁。
+
+
+
+---
+
+开启后,我们来写一个简单例子:一个静态 DIV 和一个动态 DIV。
+
+
+
+
+
+#### `组件与模板`
+
+```typescript
+@Component({
+ template: `
+
This is static
+
This is updated {{ sig() }}
+ `,
+})
+export class AppComponent {
+ sig = signal(0);
+
+ constructor() {
+ setInterval(() => {
+ this.sig.update(v => v + 1);
+ }, 1000);
+ }
+}
+```
+
+
+
+
+
+#### `编译后的模板函数`
+
+```js
+function MyComponentTemplateFn(rf, ctx) {
+ if (rf & 1) {
+ ɵɵelementStart(0, "div");
+ ɵɵtext(1, "This is static");
+ ɵɵelementEnd();
+ ɵɵelementStart(2, "div");
+ ɵɵtext(3);
+ ɵɵelementEnd();
+ }
+ if (rf & 2) {
+ ɵɵadvance(3);
+ ɵɵtextInterpolate1("This is updated ", ctx.sig(), "");
+ }
+}
+```
+
+
+
+
+这个模板会创建两个 DIV:
+
+1. 第一个只有静态节点,只在创建阶段生成。
+2. 第二个包含插值,插值在更新阶段计算。
+
+signal 每秒更新一次,框架会调度一次同步,重新执行模板函数。
+
+
+
+我们可以看到,在更新时两个 DIV 本身都没有变化。真正被更新的只有动态 DIV 中的文本节点。
+
+[Stackblitz Demo](https://stackblitz.com/edit/angular-incremental-dom-blink)
+
+### 列表渲染与节点复用
+
+Angular 的 `@for`(v17 随 @-block 控制流一起引入)在列表渲染时,会尽量复用已有 DOM 节点。
+
+而 `track` 函数让这件事更高效。你可以为每个列表项指定唯一标识,Angular 就能更智能地判断哪些节点可以复用,而不是全部重建。这样可以减少无意义的 DOM 操作,提升渲染性能,并让频繁增删改排序的动态列表更新更平滑。`track` 是一个小功能,但非常有价值,也体现了 Angular 在性能和开发体验上的取向。
+
+## 模板渲染的执行时机
+
+你现在已经知道:Angular 是通过执行组件模板函数来更新 DOM 的。
+但问题是:模板函数在什么时候执行?哪些组件会执行?
+
+Angular 的同步机制(过去称为 Change Detection,CD)会遍历组件树,并执行那些需要同步的组件模板函数。
+
+根据 ChangeDetection 策略不同,组件要么总会被检查(`Default`),要么只有在被标记为 dirty 时才检查(`OnPush`)。
+
+```mermaid
+flowchart TD
+ Def[ChangeDetectionStragery.Default] ~~~ A
+ OnPush{{ChangeDetectionStragery.OnPush}} ~~~ A
+ classDef cd fill:#adebad
+
+ A[Appcomponent]
+
+ A:::cd --> B[SideBarComponent]
+ A --> C{{"MainViewComponent\n(traversed)"}}
+
+ B:::cd --> F[HeaderComponent]
+ B --> G[MenuList]
+ G --> G1[MenuListItem]
+ G --> G2[MenuListItem]
+ G --> G3[MenuListItem]
+
+ F:::cd
+ G:::cd
+ G1:::cd
+ G2:::cd
+ G3:::cd
+
+ C --> C1{{"SomeViewComponent"}}
+ C --> C2{{"SomeOtherViewComponent\n(traversed)"}}
+
+ C2 --> C22{{"SomeDetail\n dirty"}}
+
+ C22:::cd
+```
+
+在现代应用里,“组件是否 dirty”大多由 signal 与响应式依赖树驱动。signal 更新后,会把使用它的组件标记为 dirty。
+
+这里你能看到第二层性能优化:
+使用 `OnPush` 时,框架只同步它明确知道 dirty 的组件,其余组件会被跳过,从而节省 CPU 时间。
+
+## 记忆化与脏值检查
+
+我们已经看到两点:
+
+- Angular 只更新动态内容。
+- Angular 只在 dirty 组件上运行更新。
+
+现在再看最后一层优化:记忆化(memoization)与脏值检查(dirty checking)。
+
+回到一个包含动态表达式(如插值)的模板函数:
+
+```typescript
+function MyComponentTemplateFn(rf, ctx) {
+ if (rf & 1) {
+ ...
+ }
+ if (rf & 2) {
+ ɵɵadvance(3);
+ ɵɵtextInterpolate1('This is updated ', ctx.sig(), '');
+ }
+}
+```
+
+即便插值是动态的,它仍只是这个模板函数中的一部分。
+模板函数可能会执行,但这条插值不一定真的发生变化(也许变化的是其他插值)。
+
+在这种情况下,Angular 会记忆化插值结果。
+当执行到该指令时,它会把本次结果与上次结果比较,只有当值(字符串)变化时,才真正更新 DOM。
+
+这进一步避免了昂贵且不必要的 DOM 操作。
+
+## 总结
+
+Angular 的声明式模板与基于视图的响应式渲染,在性能和效率上都非常有竞争力。它通过“增量更新 + 直接 DOM 操作”来优化渲染路径,给开发者提供了更高效、更直观的动态 UI 构建方式。尽管 React 让 Virtual DOM 更广为人知,但 Angular 的声明式策略也证明了:实现高性能 Web 应用并不只有一条路。
+
+随着 Web 开发持续演进,Angular 在模板与渲染机制上的持续创新,也让它在生态中保持强劲竞争力。无论你在做小项目还是大型企业应用,理解这些渲染策略都能帮助你做出更合适的技术决策。
+
+## 备注
+
+Alex Rickabaugh 在 [NgGlühwein 2024 演讲](https://www.youtube.com/watch?v=uTqu_eR80ss&t=1s)中分享了一个关键思路转变:团队重新定义了对 ChangeDetection 的理解,把它视为“模型与 DOM 的同步过程”。
+
+本文最初版本把 Angular 的渲染策略描述为 "Incremental DOM"。后来我了解到 Incremental DOM 实际是另一种策略。它与 VDOM 有些相似,但不会创建中间树,而是直接与真实 DOM 做差异对比。可参考 Google 的 [incremental-dom library](https://google.github.io/incremental-dom/)。
diff --git a/src/content/blog/angular-features-13-to-15.md b/src/content/blog/frontend/angular/angular-features-13-to-15.md
similarity index 74%
rename from src/content/blog/angular-features-13-to-15.md
rename to src/content/blog/frontend/angular/angular-features-13-to-15.md
index c26ef3ba..972ef596 100644
--- a/src/content/blog/angular-features-13-to-15.md
+++ b/src/content/blog/frontend/angular/angular-features-13-to-15.md
@@ -9,6 +9,7 @@ description: 从 Angular 13 到 15,框架经历了 Ivy 全面接管、Typed Fo
---
> 本文是 Angular 升级系列教程的第一篇,覆盖 Angular 13、14、15 三个版本。系列文章:
+>
> - **(一)Angular 13-15 新特性详解**(本文)
> - [(二)Angular 16-18 新特性详解](/posts/angular-features-16-to-18)
> - [(三)Angular 19-21 新特性详解](/posts/angular-features-19-to-21)
@@ -34,11 +35,12 @@ Angular 13 是一个分水岭——旧的 View Engine 渲染引擎被完全移
```typescript
// ❌ Angular 12 及之前
@Component({
- selector: 'app-parent',
- template: ''
+ selector: "app-parent",
+ template: "",
})
export class ParentComponent {
- @ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
+ @ViewChild("container", { read: ViewContainerRef })
+ container!: ViewContainerRef;
constructor(private cfr: ComponentFactoryResolver) {}
@@ -50,11 +52,12 @@ export class ParentComponent {
// ✅ Angular 13+
@Component({
- selector: 'app-parent',
- template: ''
+ selector: "app-parent",
+ template: "",
})
export class ParentComponent {
- @ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
+ @ViewChild("container", { read: ViewContainerRef })
+ container!: ViewContainerRef;
createComponent() {
this.container.createComponent(ChildComponent);
@@ -130,27 +133,27 @@ ng update @angular/core@13 @angular/cli@13
```typescript
// ❌ Angular 13 及之前 — 所有值都是 any
const form = new FormGroup({
- name: new FormControl(''),
+ name: new FormControl(""),
age: new FormControl(0),
});
const name = form.value.name; // any 😢
// ✅ Angular 14+ — 完整类型推断
const form = new FormGroup({
- name: new FormControl('', { nonNullable: true }),
+ name: new FormControl("", { nonNullable: true }),
age: new FormControl(0, { nonNullable: true }),
email: new FormControl(null),
});
const name = form.value.name; // string ✅
-const age = form.value.age; // number ✅
+const age = form.value.age; // number ✅
const email = form.value.email; // string | null ✅
```
`nonNullable` 选项确保 `reset()` 时恢复初始值而非 `null`:
```typescript
-const name = new FormControl('默认值', { nonNullable: true });
+const name = new FormControl("默认值", { nonNullable: true });
name.reset(); // 值变为 '默认值',而不是 null
```
@@ -174,10 +177,10 @@ ngOnInit() {
```typescript
// 过渡方案 — 行为与旧版完全一致
-import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
+import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";
const form = new UntypedFormGroup({
- name: new UntypedFormControl(''),
+ name: new UntypedFormControl(""),
});
```
@@ -189,15 +192,15 @@ Angular 14 引入了 Standalone Components 的概念——组件不再必须属
// 独立组件 — 不需要声明在任何 NgModule 中
@Component({
standalone: true,
- selector: 'app-hello',
+ selector: "app-hello",
imports: [CommonModule], // 直接在组件级别导入依赖
template: `
Hello {{ name }}
这是一个独立组件
- `
+ `,
})
export class HelloComponent {
- @Input() name = 'World';
+ @Input() name = "World";
showDetails = true;
}
@@ -205,7 +208,7 @@ export class HelloComponent {
@Component({
standalone: true,
imports: [HelloComponent], // 直接导入组件
- template: ''
+ template: '',
})
export class AppComponent {}
```
@@ -216,10 +219,10 @@ export class AppComponent {}
// 路由配置 — 直接使用独立组件
const routes: Routes = [
{
- path: 'dashboard',
- loadComponent: () => import('./dashboard.component')
- .then(m => m.DashboardComponent)
- }
+ path: "dashboard",
+ loadComponent: () =>
+ import("./dashboard.component").then(m => m.DashboardComponent),
+ },
];
```
@@ -228,12 +231,12 @@ const routes: Routes = [
可以在构造函数之外使用 `inject()` 获取依赖:
```typescript
-import { inject } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
+import { inject } from "@angular/core";
+import { HttpClient } from "@angular/common/http";
@Component({
- selector: 'app-user',
- template: '...'
+ selector: "app-user",
+ template: "...",
})
export class UserComponent {
// 不再需要 constructor(private http: HttpClient)
@@ -242,7 +245,7 @@ export class UserComponent {
private activatedRoute = inject(ActivatedRoute);
loadUser() {
- return this.http.get('/api/user');
+ return this.http.get("/api/user");
}
}
```
@@ -280,15 +283,17 @@ export class MyComponent {
```typescript
const routes: Routes = [
- { path: '', component: HomeComponent, title: '首页' },
- { path: 'about', component: AboutComponent, title: '关于我们' },
- { path: 'products', component: ProductsComponent, title: '产品列表' },
+ { path: "", component: HomeComponent, title: "首页" },
+ { path: "about", component: AboutComponent, title: "关于我们" },
+ { path: "products", component: ProductsComponent, title: "产品列表" },
];
// 自定义标题策略
-@Injectable({ providedIn: 'root' })
+@Injectable({ providedIn: "root" })
export class CustomTitleStrategy extends TitleStrategy {
- constructor(private title: Title) { super(); }
+ constructor(private title: Title) {
+ super();
+ }
override updateTitle(routerState: RouterStateSnapshot) {
const title = this.buildTitle(routerState);
@@ -299,9 +304,7 @@ export class CustomTitleStrategy extends TitleStrategy {
}
// 注册自定义策略
-providers: [
- { provide: TitleStrategy, useClass: CustomTitleStrategy }
-]
+providers: [{ provide: TitleStrategy, useClass: CustomTitleStrategy }];
```
### 5. 增强的模板诊断
@@ -344,17 +347,14 @@ Angular 15 将 Standalone Components/Directives/Pipes 标记为稳定版,并
```typescript
// main.ts — 不再需要 AppModule
-import { bootstrapApplication } from '@angular/platform-browser';
-import { provideRouter } from '@angular/router';
-import { provideHttpClient } from '@angular/common/http';
-import { AppComponent } from './app.component';
-import { routes } from './app.routes';
+import { bootstrapApplication } from "@angular/platform-browser";
+import { provideRouter } from "@angular/router";
+import { provideHttpClient } from "@angular/common/http";
+import { AppComponent } from "./app.component";
+import { routes } from "./app.routes";
bootstrapApplication(AppComponent, {
- providers: [
- provideRouter(routes),
- provideHttpClient(),
- ]
+ providers: [provideRouter(routes), provideHttpClient()],
});
```
@@ -367,7 +367,7 @@ bootstrapApplication(AppComponent, {
RouterModule.forRoot(routes),
HttpClientModule,
BrowserAnimationsModule,
- ]
+ ],
})
export class AppModule {}
@@ -377,10 +377,10 @@ bootstrapApplication(AppComponent, {
provideRouter(routes),
provideHttpClient(
withInterceptors([authInterceptor]),
- withFetch(), // 使用 fetch API 替代 XMLHttpRequest
+ withFetch() // 使用 fetch API 替代 XMLHttpRequest
),
provideAnimations(),
- ]
+ ],
});
```
@@ -390,13 +390,16 @@ bootstrapApplication(AppComponent, {
```typescript
// ❌ Angular 14 及之前 — 类守卫
-@Injectable({ providedIn: 'root' })
+@Injectable({ providedIn: "root" })
export class AuthGuard implements CanActivate {
- constructor(private auth: AuthService, private router: Router) {}
+ constructor(
+ private auth: AuthService,
+ private router: Router
+ ) {}
canActivate(): boolean {
if (this.auth.isLoggedIn()) return true;
- this.router.navigate(['/login']);
+ this.router.navigate(["/login"]);
return false;
}
}
@@ -407,16 +410,16 @@ export const authGuard: CanActivateFn = (route, state) => {
const router = inject(Router);
if (auth.isLoggedIn()) return true;
- return router.createUrlTree(['/login']);
+ return router.createUrlTree(["/login"]);
};
// 路由配置
const routes: Routes = [
{
- path: 'dashboard',
+ path: "dashboard",
canActivate: [authGuard],
- component: DashboardComponent
- }
+ component: DashboardComponent,
+ },
];
```
@@ -424,17 +427,17 @@ const routes: Routes = [
```typescript
// 函数式 Resolver
-export const userResolver: ResolveFn = (route) => {
+export const userResolver: ResolveFn = route => {
const userService = inject(UserService);
- return userService.getUser(route.paramMap.get('id')!);
+ return userService.getUser(route.paramMap.get("id")!);
};
const routes: Routes = [
{
- path: 'user/:id',
+ path: "user/:id",
component: UserDetailComponent,
- resolve: { user: userResolver }
- }
+ resolve: { user: userResolver },
+ },
];
```
@@ -447,7 +450,7 @@ export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest, next: HttpHandler) {
const token = inject(AuthService).getToken();
const authReq = req.clone({
- headers: req.headers.set('Authorization', `Bearer ${token}`)
+ headers: req.headers.set("Authorization", `Bearer ${token}`),
});
return next.handle(authReq);
}
@@ -457,7 +460,7 @@ export class AuthInterceptor implements HttpInterceptor {
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
const authReq = req.clone({
- headers: req.headers.set('Authorization', `Bearer ${token}`)
+ headers: req.headers.set("Authorization", `Bearer ${token}`),
});
return next(authReq);
};
@@ -465,10 +468,8 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
// 注册
bootstrapApplication(AppComponent, {
providers: [
- provideHttpClient(
- withInterceptors([authInterceptor, loggingInterceptor])
- )
- ]
+ provideHttpClient(withInterceptors([authInterceptor, loggingInterceptor])),
+ ],
});
```
@@ -480,41 +481,49 @@ bootstrapApplication(AppComponent, {
// 定义可复用的指令
@Directive({
standalone: true,
- selector: '[tooltip]',
- host: { '(mouseenter)': 'show()', '(mouseleave)': 'hide()' }
+ selector: "[tooltip]",
+ host: { "(mouseenter)": "show()", "(mouseleave)": "hide()" },
})
export class TooltipDirective {
- @Input() tooltip = '';
- show() { /* 显示提示 */ }
- hide() { /* 隐藏提示 */ }
+ @Input() tooltip = "";
+ show() {
+ /* 显示提示 */
+ }
+ hide() {
+ /* 隐藏提示 */
+ }
}
@Directive({
standalone: true,
- selector: '[highlight]',
- host: { '(mouseenter)': 'onHover()', '(mouseleave)': 'onLeave()' }
+ selector: "[highlight]",
+ host: { "(mouseenter)": "onHover()", "(mouseleave)": "onLeave()" },
})
export class HighlightDirective {
- @Input() highlightColor = 'yellow';
- onHover() { /* 高亮 */ }
- onLeave() { /* 取消高亮 */ }
+ @Input() highlightColor = "yellow";
+ onHover() {
+ /* 高亮 */
+ }
+ onLeave() {
+ /* 取消高亮 */
+ }
}
// 在组件中组合指令
@Component({
- selector: 'app-fancy-button',
+ selector: "app-fancy-button",
hostDirectives: [
{
directive: TooltipDirective,
- inputs: ['tooltip'],
+ inputs: ["tooltip"],
},
{
directive: HighlightDirective,
- inputs: ['highlightColor'],
+ inputs: ["highlightColor"],
outputs: [],
- }
+ },
],
- template: ''
+ template: "",
})
export class FancyButtonComponent {}
@@ -527,7 +536,7 @@ export class FancyButtonComponent {}
Angular 15 将图片优化指令标记为稳定版:
```typescript
-import { NgOptimizedImage } from '@angular/common';
+import { NgOptimizedImage } from "@angular/common";
@Component({
standalone: true,
@@ -537,14 +546,18 @@ import { NgOptimizedImage } from '@angular/common';
-
+
- `
+ `,
})
export class ImageExampleComponent {}
```
@@ -553,11 +566,9 @@ export class ImageExampleComponent {}
```typescript
// app.config.ts
-import { provideImageKitLoader } from '@angular/common';
+import { provideImageKitLoader } from "@angular/common";
-providers: [
- provideImageKitLoader('https://ik.imagekit.io/your_id')
-]
+providers: [provideImageKitLoader("https://ik.imagekit.io/your_id")];
```
### 6. `DestroyRef` 和 `takeUntilDestroyed`
@@ -565,28 +576,29 @@ providers: [
更优雅地处理组件销毁时的清理工作:
```typescript
-import { DestroyRef, inject } from '@angular/core';
-import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { DestroyRef, inject } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@Component({
- selector: 'app-data',
- template: '{{ data }}'
+ selector: "app-data",
+ template: "{{ data }}",
})
export class DataComponent {
- data = '';
+ data = "";
constructor() {
// 方式一:takeUntilDestroyed — 组件销毁时自动取消订阅
- inject(HttpClient).get('/api/data').pipe(
- takeUntilDestroyed()
- ).subscribe(res => this.data = res);
+ inject(HttpClient)
+ .get("/api/data")
+ .pipe(takeUntilDestroyed())
+ .subscribe(res => (this.data = res));
}
// 方式二:DestroyRef — 手动注册清理回调
private destroyRef = inject(DestroyRef);
ngOnInit() {
- const timer = setInterval(() => console.log('tick'), 1000);
+ const timer = setInterval(() => console.log("tick"), 1000);
this.destroyRef.onDestroy(() => clearInterval(timer));
}
}
@@ -600,22 +612,22 @@ export class DataComponent {
const routes: Routes = [
// 懒加载独立组件
{
- path: 'profile',
- loadComponent: () => import('./profile.component')
- .then(m => m.ProfileComponent)
+ path: "profile",
+ loadComponent: () =>
+ import("./profile.component").then(m => m.ProfileComponent),
},
// 懒加载子路由配置
{
- path: 'admin',
- loadChildren: () => import('./admin/admin.routes')
- .then(m => m.ADMIN_ROUTES)
- }
+ path: "admin",
+ loadChildren: () =>
+ import("./admin/admin.routes").then(m => m.ADMIN_ROUTES),
+ },
];
// admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
- { path: '', component: AdminDashboardComponent },
- { path: 'users', component: AdminUsersComponent },
+ { path: "", component: AdminDashboardComponent },
+ { path: "users", component: AdminUsersComponent },
];
```
@@ -641,11 +653,11 @@ ng generate @angular/core:standalone
## 升级路径总结
-| 版本 | TypeScript | Node.js | 关键升级动作 |
-|------|-----------|---------|-------------|
-| 13 | 4.4 | 12.20+ / 14.15+ | 移除 View Engine 相关代码 |
-| 14 | 4.6-4.7 | 14.15+ / 16.10+ | 迁移到 Typed Forms(或先用 Untyped* 过渡)|
-| 15 | 4.8 | 14.20+ / 16.13+ | 迁移到 Standalone + provide* 函数 |
+| 版本 | TypeScript | Node.js | 关键升级动作 |
+| ---- | ---------- | --------------- | ------------------------------------------- |
+| 13 | 4.4 | 12.20+ / 14.15+ | 移除 View Engine 相关代码 |
+| 14 | 4.6-4.7 | 14.15+ / 16.10+ | 迁移到 Typed Forms(或先用 Untyped\* 过渡) |
+| 15 | 4.8 | 14.20+ / 16.13+ | 迁移到 Standalone + provide\* 函数 |
每次升级建议使用 Angular 官方的升级指南:[https://angular.dev/update-guide](https://angular.dev/update-guide)
diff --git a/src/content/blog/angular-features-16-to-18.md b/src/content/blog/frontend/angular/angular-features-16-to-18.md
similarity index 73%
rename from src/content/blog/angular-features-16-to-18.md
rename to src/content/blog/frontend/angular/angular-features-16-to-18.md
index 91ef0947..e9c730c3 100644
--- a/src/content/blog/angular-features-16-to-18.md
+++ b/src/content/blog/frontend/angular/angular-features-16-to-18.md
@@ -9,6 +9,7 @@ description: Angular 16-18 是框架的变革期——Signals 响应式系统诞
---
> 本文是 Angular 升级系列教程的第二篇,覆盖 Angular 16、17、18 三个版本。系列文章:
+>
> - [(一)Angular 13-15 新特性详解](/posts/angular-features-13-to-15)
> - **(二)Angular 16-18 新特性详解**(本文)
> - [(三)Angular 19-21 新特性详解](/posts/angular-features-19-to-21)
@@ -24,17 +25,17 @@ Angular 16 是一个划时代的版本——**Signals 响应式系统**的诞生
Signals 是 Angular 全新的细粒度响应式系统,目标是最终取代 Zone.js:
```typescript
-import { Component, signal, computed, effect } from '@angular/core';
+import { Component, signal, computed, effect } from "@angular/core";
@Component({
- selector: 'app-counter',
+ selector: "app-counter",
template: `
计数: {{ count() }}
双倍: {{ doubleCount() }}
- `
+ `,
})
export class CounterComponent {
// signal — 可变的响应式值
@@ -70,19 +71,19 @@ Signal 的核心 API:
```typescript
// 创建
-const name = signal('Angular'); // WritableSignal
-const version = signal(16); // WritableSignal
-const config = signal({ theme: 'dark' }); // WritableSignal
} @empty {
没有找到商品
@@ -280,38 +286,38 @@ Angular 17 是一次**品牌重塑**——新 Logo、新文档站(angular.dev
@switch (status()) {
- @case ('active') {
+ @case ("active") {
活跃
}
- @case ('inactive') {
+ @case ("inactive") {
未激活
}
- @case ('banned') {
+ @case ("banned") {
已封禁
}
@default {
未知
}
}
- `
+ `,
})
export class ProductListComponent {
products = signal([]);
isLoading = signal(false);
- status = signal('active');
+ status = signal("active");
}
```
**`@for` 中可用的隐式变量:**
-| 变量 | 类型 | 说明 |
-|------|------|------|
-| `$index` | number | 当前索引 |
-| `$first` | boolean | 是否第一个 |
-| `$last` | boolean | 是否最后一个 |
-| `$even` | boolean | 是否偶数索引 |
-| `$odd` | boolean | 是否奇数索引 |
-| `$count` | number | 总数量 |
+| 变量 | 类型 | 说明 |
+| -------- | ------- | ------------ |
+| `$index` | number | 当前索引 |
+| `$first` | boolean | 是否第一个 |
+| `$last` | boolean | 是否最后一个 |
+| `$even` | boolean | 是否偶数索引 |
+| `$odd` | boolean | 是否奇数索引 |
+| `$count` | number | 总数量 |
**自动迁移命令:**
@@ -326,7 +332,7 @@ ng generate @angular/core:control-flow
```typescript
@Component({
- selector: 'app-dashboard',
+ selector: "app-dashboard",
template: `
仪表盘
@@ -382,7 +388,7 @@ ng generate @angular/core:control-flow
} @placeholder {
}
- `
+ `,
})
export class DashboardComponent {
showAdvanced = signal(false);
@@ -391,15 +397,15 @@ export class DashboardComponent {
`@defer` 的触发条件汇总:
-| 触发条件 | 说明 |
-|---------|------|
-| `on idle` | 浏览器空闲时 |
-| `on viewport` | 进入视口时 |
-| `on interaction` | 用户交互(点击、聚焦)时 |
-| `on hover` | 鼠标悬停时 |
-| `on immediate` | 立即加载(但仍然是懒加载的 chunk)|
-| `on timer(Xs)` | X 秒后加载 |
-| `when condition` | 条件为 true 时 |
+| 触发条件 | 说明 |
+| ---------------- | ---------------------------------- |
+| `on idle` | 浏览器空闲时 |
+| `on viewport` | 进入视口时 |
+| `on interaction` | 用户交互(点击、聚焦)时 |
+| `on hover` | 鼠标悬停时 |
+| `on immediate` | 立即加载(但仍然是懒加载的 chunk) |
+| `on timer(Xs)` | X 秒后加载 |
+| `when condition` | 条件为 true 时 |
### 3. esbuild + Vite 成为新项目的默认构建系统
@@ -414,7 +420,7 @@ Angular 17 新项目默认使用基于 esbuild 的 `application` 构建器:
"options": {
"outputPath": "dist/my-app",
"index": "src/index.html",
- "browser": "src/main.ts", // 注意:不再是 "main"
+ "browser": "src/main.ts", // 注意:不再是 "main"
"tsConfig": "tsconfig.app.json"
}
}
@@ -423,6 +429,7 @@ Angular 17 新项目默认使用基于 esbuild 的 `application` 构建器:
```
对比旧构建器的性能提升:
+
- 构建速度提升 67%+
- `ng serve` 热更新几乎瞬时
- 包体积略有减小
@@ -458,6 +465,7 @@ ng generate pipe currency-format
### 5. 新文档站 angular.dev
Angular 团队发布了全新的文档网站 [angular.dev](https://angular.dev),包含:
+
- 交互式教程
- 全新的 API 文档
- 内置 Playground
@@ -467,24 +475,26 @@ Angular 团队发布了全新的文档网站 [angular.dev](https://angular.dev)
信号版的 Input/Output/ViewChild 等 API 开始预览:
```typescript
-import { Component, input, output, model } from '@angular/core';
+import { Component, input, output, model } from "@angular/core";
@Component({
- selector: 'app-slider',
+ selector: "app-slider",
template: `
-
+ (input)="onInput($event)"
+ />
{{ value() }}
- `
+ `,
})
export class SliderComponent {
// signal input
min = input(0);
max = input(100);
- value = model(50); // 双向绑定的 signal
+ value = model(50); // 双向绑定的 signal
// signal output
valueChange = output();
@@ -507,8 +517,8 @@ export class SliderComponent {
```typescript
bootstrapApplication(AppComponent, {
providers: [
- provideClientHydration(), // 正式稳定 ✅
- ]
+ provideClientHydration(), // 正式稳定 ✅
+ ],
});
```
@@ -561,7 +571,7 @@ ng generate @angular/core:control-flow
} @placeholder {
活动记录
}
- `
+ `,
})
export class HomeComponent {
user = signal(null);
@@ -574,49 +584,55 @@ Angular 18 引入了实验性的无 Zone.js 变更检测——这是 Angular 性
```typescript
// main.ts — 启用 Zoneless
-import { provideExperimentalZonelessChangeDetection } from '@angular/core';
+import { provideExperimentalZonelessChangeDetection } from "@angular/core";
bootstrapApplication(AppComponent, {
- providers: [
- provideExperimentalZonelessChangeDetection()
- ]
+ providers: [provideExperimentalZonelessChangeDetection()],
});
// 使用 Zoneless 后,变更检测完全由 Signals 驱动
@Component({
- selector: 'app-todo',
+ selector: "app-todo",
template: `
@for (todo of todos(); track todo.id) {
-
+ (change)="toggleTodo(todo.id)"
+ />
{{ todo.text }}
}
- `
+ `,
})
export class TodoComponent {
todos = signal([]);
addTodo(text: string) {
- this.todos.update(list => [...list, {
- id: Date.now(), text, done: false
- }]);
+ this.todos.update(list => [
+ ...list,
+ {
+ id: Date.now(),
+ text,
+ done: false,
+ },
+ ]);
}
toggleTodo(id: number) {
this.todos.update(list =>
- list.map(t => t.id === id ? { ...t, done: !t.done } : t)
+ list.map(t => (t.id === id ? { ...t, done: !t.done } : t))
);
}
}
```
去掉 Zone.js 的好处:
+
- 减少约 13KB(gzipped)的包体积
- 不再有不必要的变更检测循环
- 更好的性能和可预测的行为
@@ -627,14 +643,21 @@ export class TodoComponent {
Angular 18 将大部分信号 API 标记为稳定或接近稳定:
```typescript
-import { Component, input, output, model, viewChild, viewChildren } from '@angular/core';
+import {
+ Component,
+ input,
+ output,
+ model,
+ viewChild,
+ viewChildren,
+} from "@angular/core";
@Component({
- selector: 'app-accordion',
+ selector: "app-accordion",
template: `
@if (isOpen()) {
@@ -642,7 +665,7 @@ import { Component, input, output, model, viewChild, viewChildren } from '@angul
}
- `
+ `,
})
export class AccordionComponent {
// 稳定的 signal input
@@ -656,8 +679,8 @@ export class AccordionComponent {
toggled = output();
// signal queries
- toggleBtn = viewChild.required('toggleBtn');
- contentSections = viewChildren('content');
+ toggleBtn = viewChild.required("toggleBtn");
+ contentSections = viewChildren("content");
constructor() {
// 用 effect 响应 input 变化
@@ -680,14 +703,17 @@ export class AccordionComponent {
SSR 水合正式稳定,新增事件回放功能(开发者预览版):
```typescript
-import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
+import {
+ provideClientHydration,
+ withEventReplay,
+} from "@angular/platform-browser";
bootstrapApplication(AppComponent, {
providers: [
provideClientHydration(
withEventReplay() // 回放水合前用户的交互事件
- )
- ]
+ ),
+ ],
});
```
@@ -730,24 +756,24 @@ $theme: mat.define-theme((
const permissions = new FormRecord>({});
// 动态添加字段
-permissions.addControl('read', new FormControl(true));
-permissions.addControl('write', new FormControl(false));
-permissions.addControl('admin', new FormControl(false));
+permissions.addControl("read", new FormControl(true));
+permissions.addControl("write", new FormControl(false));
+permissions.addControl("admin", new FormControl(false));
// 统一的表单事件 — events 属性
-const nameControl = new FormControl('');
+const nameControl = new FormControl("");
nameControl.events.subscribe(event => {
if (event instanceof TouchedChangeEvent) {
- console.log('touched 状态变化:', event.touched);
+ console.log("touched 状态变化:", event.touched);
}
if (event instanceof PristineChangeEvent) {
- console.log('pristine 状态变化:', event.pristine);
+ console.log("pristine 状态变化:", event.pristine);
}
if (event instanceof StatusChangeEvent) {
- console.log('验证状态变化:', event.status);
+ console.log("验证状态变化:", event.status);
}
if (event instanceof ValueChangeEvent) {
- console.log('值变化:', event.value);
+ console.log("值变化:", event.value);
}
});
```
@@ -759,15 +785,15 @@ nameControl.events.subscribe(event => {
```typescript
const routes: Routes = [
{
- path: 'old-page',
+ path: "old-page",
redirectTo: ({ queryParams }) => {
- const id = queryParams['id'];
+ const id = queryParams["id"];
if (id) {
return `/new-page/${id}`;
}
- return '/new-page';
- }
- }
+ return "/new-page";
+ },
+ },
];
```
@@ -783,17 +809,19 @@ const routes: Routes = [
```typescript
// ng-content fallback
@Component({
- selector: 'app-card',
+ selector: "app-card",
template: `
- 默认标题
+ 默认标题
+
- 暂无内容
+ 暂无内容
+
- `
+ `,
})
export class CardComponent {}
```
@@ -808,22 +836,22 @@ ng update @angular/core@18 @angular/cli@18
## 版本升级路径总结
-| 版本 | TypeScript | Node.js | 关键里程碑 |
-|------|-----------|---------|-----------|
-| 16 | 4.9 - 5.0 | 16.14+ / 18.10+ | **Signals 诞生**(开发者预览版)|
-| 17 | 5.2 | 18.13+ | 新控制流 + @defer + esbuild 默认 |
-| 18 | 5.4 | 18.19+ | 控制流稳定 + Zoneless 实验 + Material 3 |
+| 版本 | TypeScript | Node.js | 关键里程碑 |
+| ---- | ---------- | --------------- | --------------------------------------- |
+| 16 | 4.9 - 5.0 | 16.14+ / 18.10+ | **Signals 诞生**(开发者预览版) |
+| 17 | 5.2 | 18.13+ | 新控制流 + @defer + esbuild 默认 |
+| 18 | 5.4 | 18.19+ | 控制流稳定 + Zoneless 实验 + Material 3 |
### Signals 特性状态演进
-| 特性 | Angular 16 | Angular 17 | Angular 18 |
-|------|-----------|-----------|-----------|
-| signal / computed / effect | 🧪 预览 | ✅ 稳定 | ✅ 稳定 |
-| signal input() | — | 🧪 预览 | ✅ 稳定 |
-| signal output() | — | 🧪 预览 | ✅ 稳定 |
-| signal model() | — | 🧪 预览 | ✅ 稳定 |
-| viewChild / viewChildren | — | 🧪 预览 | ✅ 稳定 |
-| Zoneless | — | — | 🧪 实验 |
+| 特性 | Angular 16 | Angular 17 | Angular 18 |
+| -------------------------- | ---------- | ---------- | ---------- |
+| signal / computed / effect | 🧪 预览 | ✅ 稳定 | ✅ 稳定 |
+| signal input() | — | 🧪 预览 | ✅ 稳定 |
+| signal output() | — | 🧪 预览 | ✅ 稳定 |
+| signal model() | — | 🧪 预览 | ✅ 稳定 |
+| viewChild / viewChildren | — | 🧪 预览 | ✅ 稳定 |
+| Zoneless | — | — | 🧪 实验 |
```bash
# 逐版本升级(推荐)
diff --git a/src/content/blog/angular-features-19-to-21.md b/src/content/blog/frontend/angular/angular-features-19-to-21.md
similarity index 71%
rename from src/content/blog/angular-features-19-to-21.md
rename to src/content/blog/frontend/angular/angular-features-19-to-21.md
index e1ab490c..56c82b14 100644
--- a/src/content/blog/angular-features-19-to-21.md
+++ b/src/content/blog/frontend/angular/angular-features-19-to-21.md
@@ -9,6 +9,7 @@ description: Angular 19-22 标志着框架的成熟期--Signals 全面稳定、Z
---
> 本文是 Angular 升级系列教程的第三篇,覆盖 Angular 19、20、21、22 四个版本。系列文章:
+>
> - [(一)Angular 13-15 新特性详解](/posts/angular-features-13-to-15)
> - [(二)Angular 16-18 新特性详解](/posts/angular-features-16-to-18)
> - **(三)Angular 19-22 新特性详解**(本文)
@@ -25,13 +26,21 @@ Angular 19 是 Signals 响应式系统的**收官之作**--几乎所有核心 Si
```typescript
import {
- Component, signal, computed, effect,
- input, output, model,
- viewChild, viewChildren, contentChild, contentChildren
-} from '@angular/core';
+ Component,
+ signal,
+ computed,
+ effect,
+ input,
+ output,
+ model,
+ viewChild,
+ viewChildren,
+ contentChild,
+ contentChildren,
+} from "@angular/core";
@Component({
- selector: 'app-task-board',
+ selector: "app-task-board",
template: `
{{ title() }} ({{ activeCount() }}/{{ tasks().length }})
@@ -40,9 +49,11 @@ import {
@for (task of tasks(); track task.id) {
-
+ (change)="toggleTask(task.id)"
+ />
{{ task.text }}
} @empty {
@@ -52,26 +63,24 @@ import {
- `
+ `,
})
export class TaskBoardComponent {
// signal input - 稳定 ✅
- title = input('任务看板');
+ title = input("任务看板");
// model - 双向绑定信号,稳定 ✅
tasks = model([]);
// computed - 稳定 ✅
- activeCount = computed(() =>
- this.tasks().filter(t => !t.done).length
- );
+ activeCount = computed(() => this.tasks().filter(t => !t.done).length);
// output - 稳定 ✅
taskAdded = output();
taskToggled = output();
// viewChild - 稳定 ✅
- footer = viewChild('footer');
+ footer = viewChild("footer");
constructor() {
// effect - 稳定 ✅
@@ -89,7 +98,7 @@ export class TaskBoardComponent {
toggleTask(id: number) {
this.tasks.update(list =>
- list.map(t => t.id === id ? { ...t, done: !t.done } : t)
+ list.map(t => (t.id === id ? { ...t, done: !t.done } : t))
);
this.taskToggled.emit(id);
}
@@ -101,7 +110,7 @@ export class TaskBoardComponent {
全新的异步数据获取原语,与 Signals 深度集成,替代手动管理 loading/error 状态:
```typescript
-import { Component, signal, resource } from '@angular/core';
+import { Component, signal, resource } from "@angular/core";
interface User {
id: number;
@@ -111,7 +120,7 @@ interface User {
}
@Component({
- selector: 'app-user-profile',
+ selector: "app-user-profile",
template: `
@if (userResource.isLoading()) {
@@ -132,13 +141,14 @@ interface User {
}
-
+
用户 #{{ userId() }}
- `
+ `,
})
export class UserProfileComponent {
userId = signal(1);
@@ -151,41 +161,43 @@ export class UserProfileComponent {
// loader: 异步加载函数
loader: async ({ request, abortSignal }) => {
- const res = await fetch(
- `https://api.example.com/users/${request.id}`,
- { signal: abortSignal }
- );
+ const res = await fetch(`https://api.example.com/users/${request.id}`, {
+ signal: abortSignal,
+ });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as User;
- }
+ },
});
}
```
`resource` 的状态属性:
-| 属性 | 类型 | 说明 |
-|------|------|------|
-| `value()` | `T \| undefined` | 加载成功的数据 |
-| `error()` | `unknown` | 错误信息 |
-| `isLoading()` | `boolean` | 是否正在加载 |
-| `status()` | `ResourceStatus` | 状态枚举:idle / loading / resolved / error |
-| `reload()` | `void` | 手动重新加载 |
+| 属性 | 类型 | 说明 |
+| ------------- | ---------------- | ------------------------------------------- |
+| `value()` | `T \| undefined` | 加载成功的数据 |
+| `error()` | `unknown` | 错误信息 |
+| `isLoading()` | `boolean` | 是否正在加载 |
+| `status()` | `ResourceStatus` | 状态枚举:idle / loading / resolved / error |
+| `reload()` | `void` | 手动重新加载 |
### 3. `linkedSignal`(开发者预览版)🧪
创建一个可写信号,当源信号变化时自动重置为计算值:
```typescript
-import { Component, signal, computed, linkedSignal } from '@angular/core';
+import { Component, signal, computed, linkedSignal } from "@angular/core";
@Component({
- selector: 'app-product-filter',
+ selector: "app-product-filter",
template: `
商品筛选
-