阅读视图

发现新文章,点击刷新页面。

屎山代码拆不动?微前端来救场:一个应用变“乐高城堡”

前言

想象你有一座巨大的乐高城堡,一开始几个人拼得很开心。后来城堡越拼越大,几百人同时在上面加砖,有人碰倒了塔楼,有人改错了城墙,整个城堡摇摇欲坠。你想拆成几个独立的小城堡,又怕它们之间连不起来。

这就是巨石前端的困境。微前端就是解决方案:把大应用拆成多个小应用(子应用),每个小应用独立开发、独立部署,最后在浏览器里组合成一个完整页面。就像乐高套装里的每个小模块,可以单独拼好,再插到一起。

一、什么时候需要微前端?

  • 项目太大,编译部署一次要10分钟。
  • 团队太多,几十人改同一个仓库,Git冲突到崩溃。
  • 想渐进式升级技术栈(比如老项目用AngularJS,新模块用React)。
  • 不同团队负责不同业务板块,希望独立发布互不干扰。

如果你的项目只有三五个人,别用微前端——杀鸡不用牛刀。

二、微前端三大核心问题

微前端要解决三个问题:

  1. 怎么加载子应用?(路由分发)
  2. 怎么隔离子应用?(JS沙箱、样式隔离)
  3. 怎么通信?(全局状态、事件总线)

三、常见实现方式

1. 路由分发式(Nginx反向代理)

不同路径对应不同子应用,比如/app1 → 应用1,/app2 → 应用2。父页面通过iframe或服务端路由组合。

  • 简单,但切换应用会刷新页面。
  • 不适合需要无缝组合的场景。

2. iframe:最土的“隔离神器”

iframe天然隔离JS和CSS,但缺点明显:通信麻烦、SEO差、弹窗无法覆盖、全局状态不共享。

3. single-spa:微前端的“老大哥”

一个框架,帮你管理子应用的加载、挂载、卸载。你需要自己写如何加载子应用(比如动态script加载),以及子应用暴露的生命周期(bootstrap、mount、unmount)。

  • 灵活,但需要较多配置。
  • 适合自己造轮子。

4. qiankun:蚂蚁开箱即用的方案

基于single-spa,内置了JS沙箱、样式隔离、HTML Entry(自动加载子应用的HTML、JS、CSS)。你只需要改几行代码,就能把一个普通应用变成微前端子应用。

  • 推荐大部分项目用qiankun。
  • 支持Vue、React、Angular等。

5. Webpack 5 Module Federation:去中心化的“共享冰箱”

不需要主应用,任意两个应用可以互相暴露和使用模块。运行时动态加载对方代码,像从冰箱里拿菜一样。

  • 非常适合多个独立部署的微前端应用。
  • 需要Webpack 5支持。

四、qiankun 实战:三步把React应用变成子应用

假设你有一个主应用(基座),一个子应用(React)。

主应用(基座)注册子应用

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'reactApp',
    entry: '//localhost:3001', // 子应用启动的地址
    container: '#subapp-container',
    activeRule: '/react',
  },
]);
start();

子应用(React)改造

src/index.js里暴露生命周期:

function render(props) {
  ReactDOM.render(<App />, document.getElementById('root'));
}

if (!window.__POWERED_BY_QIANKUN__) {
  render(); // 独立运行时直接渲染
}

export async function bootstrap() {}
export async function mount(props) {
  render(props);
}
export async function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}

再改webpack配置,让打包成umd格式:

output: {
  library: `${name}-[name]`,
  libraryTarget: 'umd',
  globalObject: 'window',
}

搞定!子应用独立运行时正常访问,被qiankun加载时也能完美嵌入。

五、JS沙箱:防止子应用污染全局

qiankun提供了两种沙箱:

  • SnapshotSandbox:记录恢复window属性变化(兼容IE)。
  • ProxySandbox:用ES6 Proxy代理对window的读写,每个子应用有自己的fakeWindow。

这样子应用里修改windowdocument都不会影响全局。

六、样式隔离:你的样式别弄脏我的衣服

qiankun默认使用shadowDOM(需要子应用支持),也可以通过配置strictStyleIsolation开启。或者简单约定:子应用所有样式加namespace

七、应用间通信:传递“小纸条”

  • 通过props传递:主应用mount子应用时,可以传入通信函数。
  • 全局状态管理:用qiankuninitGlobalState
  • 自定义事件window.dispatchEvent(但注意沙箱可能隔离window)。

八、常见坑点与建议

  1. 重复依赖:多个子应用都打包了React,体积大。解决方案:用externals或Module Federation共享。
  2. 子应用间路由跳转:用history.pushState前判断是否在微前端环境,调用主应用的路由实例。
  3. 公共样式:主应用提供全局样式,子应用只写局部样式。
  4. 性能:预加载子应用,或使用loadable组件按需加载。

九、Module Federation:不用主应用的“分布式”微前端

如果你的项目没有明确的主应用,每个应用都可以暴露模块给其他应用,用Webpack 5的ModuleFederationPlugin

// 应用A暴露组件
new ModuleFederationPlugin({
  name: 'appA',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/Button',
  },
});

// 应用B消费
new ModuleFederationPlugin({
  name: 'appB',
  remotes: {
    appA: 'appA@http://localhost:3001/remoteEntry.js',
  },
});
// 在B里异步加载:import('appA/Button')

这样两个应用独立部署,运行时动态加载对方组件,超级灵活。

十、总结:微前端不是银弹,但能救急

  • 微前端适合超大项目、多团队、技术栈升级
  • 简单场景用qiankun,复杂场景用Module Federation
  • 注意JS沙箱、样式隔离、通信成本。
  • 如果项目只有几十个页面,别折腾,用组件化就够了。

微前端就像乐高积木:拆开是独立小玩具,拼起来是宏伟城堡。用得好,团队效率翻倍;用不好,调试到你怀疑人生。


如果你觉得今天的“乐高城堡”够形象,点个赞让更多人看到。明天我们将聊聊前端设计模式——单例、观察者、工厂、策略,那些让你代码更优雅的套路。我们明天见!

学习 Redux Toolkit :从 Context 误区到 createSlice 实践

本文说明:本文是基于 Redux Toolkit 官方文档及其 maintainer 发布的博文做的整理,双语对照以防止与原文有歧义。文末有完整的原文链接可供详细学习。

希望这份整理对你有帮助。

一、开篇:Context 不是状态管理系统

“Should I use Context or should I use Redux?” “我应该用上下文还是用 Redux?”

And they seem to think that Context itself is a state management system. It’s not. 他们似乎认为 Context 本身就是一个状态管理系统。 其实不是

It’s a dependency injection mechanism, and you can put whatever value you want in Context, and most often you are the one managing that state in a React component, with the useState hook or the useReducer hook. And you’re the one deciding where the state lives, handling how to update it, and then putting the value into Context for distribution. 它是一种依赖注入机制,你可以在上下文中输入任何你想要的值,通常你会在 React 组件中管理该状态,使用 useState 钩子或 useReducer 钩子。你负责决定状态的所在位置,处理如何更新,然后把这个值放进 Context 进行分发。

So yeah, useReducer plus useContext together kind of make up a state management system. And that one is more equivalent to what Redux does with React, but Context by itself is not a state management system. 所以,是的,useReducer 加上 useContext 一起构成了一个状态管理系统。这个更类似于 Redux 对 React 的处理,但 Context 本身并不是一个状态管理系统 。

既然 Context 本身不是状态管理方案,那么 Redux Toolkit 提供了怎样的替代方案?我们先从它的 API 全景看起。

二、Redux Toolkit 工具箱里有什么?

Redux Toolkit 包含以下 API

  • configureStore(): 包装 createStore 以提供简化的配置选项和良好的默认值。它可以自动组合您的切片 reducer,添加您提供的任何 Redux 中间件,默认情况下包含 redux-thunk,并启用 Redux DevTools Extension 的使用。
  • createReducer(): 允许您提供操作类型到 case reducer 函数的查找表,而不是编写 switch 语句。此外,它会自动使用 immer,让您可以使用正常的可变代码编写更简单的不可变更新,例如 state.todos[3].completed = true
  • createAction(): 为给定的操作类型字符串生成一个操作创建器函数。
  • createSlice(): 接受 reducer 函数对象、切片名称和初始状态值,并自动生成具有相应操作创建器和操作类型的切片 reducer。
  • combineSlices(): 将多个切片组合成一个 reducer,并允许在初始化后“延迟加载”切片。
  • createAsyncThunk: 接受一个动作类型字符串和一个返回 Promise 的函数,并生成一个 thunk,根据该 Promise 分发 pending/fulfilled/rejected 动作类型。
  • createEntityAdapter: 生成一组可重用的 reducer 和 selector,用于管理存储中的规范化数据。
  • 来自 Reselect 库的 createSelector 实用程序,为了方便使用而重新导出。

注意到上面多次提到 Immer 了吗?它正是 RTK 让你能“直接修改 state”的秘密武器。

三、Immer:为什么你能直接“修改”state?

Immer(德语为:always)是一个小型包,可让您以更方便的方式使用不可变状态。

Immer 简化了不可变数据结构的处理

Immer 可以在需要使用不可变数据结构的任何上下文中使用。例如与 React state、React 或 Redux reducers 或者 configuration management 结合使用。不可变的数据结构允许(高效)的变化检测:如果对对象的引用没有改变,那么对象本身也没有改变。此外,它使克隆对象相对便宜:数据树的未更改部分不需要复制,并且在内存中与相同状态的旧版本共享

一般来说,这些好处可以通过确保您永远不会更改对象、数组或映射的任何属性来实现,而是始终创建一个更改后的副本。在实践中,这可能会导致代码编写起来非常麻烦,并且很容易意外违反这些约束。 Immer 将通过解决以下痛点来帮助您遵循不可变数据范式:

  1. Immer 将检测到意外 mutations 并抛出错误。
  2. Immer 将不再需要创建对不可变对象进行深度更新时所需的典型样板代码:如果没有 Immer,则需要在每个级别手动制作对象副本。通常通过使用大量 ... 展开操作。使用 Immer 时,会对 draft 对象进行更改,该对象会记录更改并负责创建必要的副本,而不会影响原始对象。
  3. 使用 Immer 时,您无需学习专用 API 或数据结构即可从范例中受益。使用 Immer,您将使用纯 JavaScript 数据结构,并使用众所周知的安全地可变 JavaScript API。

代码对比:不使用 Immer vs 使用 Immer

不使用 Immer

如果没有 Immer,我们将不得不小心地浅拷贝每层受我们更改影响的 state 结构

const nextState = baseState.slice() // 浅拷贝数组
nextState[1] = {
    // 替换第一层元素
    ...nextState[1], // 浅拷贝第一层元素
    done: true // 期望的更新
}
// 因为 nextState 是新拷贝的, 所以使用 push 方法是安全的,
// 但是在未来的任意时间做相同的事情会违反不变性原则并且导致 bug!
nextState.push({title: "Tweet about it"})

使用 Immer

使用 Immer,这个过程更加简单。我们可以利用 produce 函数,它将我们要更改的 state 作为第一个参数,对于第二个参数,我们传递一个名为 recipe 的函数,该函数传递一个 draft 参数,我们可以对其应用直接的 mutations。一旦 recipe 执行完成,这些 mutations 被记录并用于产生下一个状态。 produce 将负责所有必要的复制,并通过冻结数据来防止未来的意外修改。

import {produce} from "immer"

const nextState = produce(baseState, draft => {
    draft[1].done = true
    draft.push({title: "Tweet about it"})
})

Immer 核心要点总结

简单说应该就是state是不可以变的,但immer提供了state这种不可变数据的更改?

  1. 最终结果仍遵循不可变原则
    • 原始 state 不会被修改
    • 修改后产生一个全新的对象
    • 未变化的部分共享引用(结构共享)
  2. 但编写体验是“可变”的
    • 你直接对 draft 赋值:draft[1].done = true
    • 你直接 push:draft.push(...)
    • 看起来就像修改了原对象

Immer 工作原理图示

基本思想是,使用 Immer,您会将所有更改应用到临时 draft,它是 currentState 的代理。一旦你完成了所有的 mutations,Immer 将根据对 draft statemutations 生成 nextState。这意味着您可以通过简单地修改数据来与数据交互,同时保留不可变数据的所有好处。

immer-hd.png

理解了 Immer 的原理,我们来看 RTK 中最核心的抽象——Slice(切片)。

四、Slice:Redux 开发的模块化核心

一、什么是 Slice? 在 Redux Toolkit 中,Slice(切片) 是核心概念之一,用于简化 Redux 开发流程。 核心特性

  • 自动生成 Action:无需手动定义 ACTION_TYPE 常量和 action creator 函数

  • 不可变更新:内部集成 Immer 库,允许直接"修改"状态,同时保证生成新的不可变对象

  • 模块化管理:将应用状态拆分为独立模块(用户模块、酒店模块、审核模块等)

    本 Slice 专门负责管理酒店审核模块的状态逻辑,包括列表数据、筛选条件、加载状态及审核操作结果。

Slice 处理的是同步更新,那异步请求呢?RTK 专门提供了 createAsyncThunk。

五、createAsyncThunk:异步请求的标准方案

概述

一个接受 Redux action 类型字符串和回调函数的函数,该回调函数应返回一个 promise。它根据您传入的动作类型前缀生成 promise 生命周期动作类型,并返回一个 thunk 动作创建者,该创建者将运行 promise 回调并根据返回的 promise 分派生命周期动作。

本节概述了处理异步请求生命周期的标准推荐方法。

它不会生成任何 reducer 函数,因为它不知道您要获取什么数据、如何跟踪加载状态,以及如何处理返回的数据。您应该编写自己的 reducer 逻辑来处理这些操作,并使用适合您应用程序的加载状态和处理逻辑。

掌握了这些核心概念,你已经可以开始使用 RTK 了。更多细节可以参考以下资源。

六、延伸学习

createAsyncThunk | Redux Toolkit 中文

createSlice | Redux Toolkit 中文

什么时候(以及什么时候不该)入手 Redux --- When (and when not) to reach for Redux

Angular 基础知识点全汇总(附实战示例 | 新手友好)

前言

Angular 是由 Google 维护的企业级前端框架,基于 TypeScript 开发,内置路由、表单、HTTP、依赖注入等全套解决方案,适合中大型后台管理系统、企业级应用开发。本文整理了 Angular 从入门到实战的全套基础知识点,每个知识点搭配可直接运行的代码示例,新手也能快速上手!

适用版本:Angular 14+ / 18+(长期支持版)阅读对象:前端新手、Vue/React 转 Angular 开发者

1. Angular 核心概述

核心特点

  • 完整的企业级框架(全家桶,无需额外集成第三方库)
  • 强类型:基于 TypeScript 开发
  • 内置依赖注入 (DI)、路由、表单、HTTP 客户端
  • 单向数据流 + 可选双向绑定
  • 适合大型团队、长期维护的项目

核心组成

  • 模块 (Module) :组织应用的最小单元
  • 组件 (Component) :页面的最小单元
  • 服务 (Service) :公共逻辑封装
  • 指令 / 管道 / 路由:扩展功能

2. 环境搭建与项目创建

2.1 安装 Angular CLI

bash 运行

# 全局安装 Angular 脚手架
npm install -g @angular/cli

# 验证安装
ng version

2.2 创建新项目

bash 运行

# 创建项目(支持路由、SCSS、严格模式)
ng new angular-demo --routing --style=scss --strict

# 进入项目
cd angular-demo

# 启动项目(默认端口4200)
ng serve --open

2.3 常用 CLI 命令

bash 运行

# 创建组件
ng generate component components/home
# 简写
ng g c components/home

# 创建服务
ng g s services/http

# 创建路由模块
ng g m app-routing --module=app --flat

3. 核心概念:模块 (Module)

定义

模块是 Angular 应用的组织结构,一个应用至少有一个根模块 AppModule

核心作用

  • 声明组件、指令、管道
  • 导入依赖模块
  • 提供服务
  • 启动应用

示例:根模块 app.module.ts

typescript 运行

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
// 路由模块
import { AppRoutingModule } from './app-routing.module';
// 根组件
import { AppComponent } from './app.component';
// 自定义组件
import { HomeComponent } from './components/home/home.component';

@NgModule({
  // 声明:当前模块的组件/指令/管道
  declarations: [
    AppComponent,
    HomeComponent
  ],
  // 导入:依赖的其他模块
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  // 提供:全局服务
  providers: [],
  // 启动:根组件
  bootstrap: [AppComponent]
})
export class AppModule { }

4. 组件 (Component) 基础

定义

组件是 Angular 应用的页面最小单元,由 HTML 模板 + TS 逻辑 + CSS 样式 组成。

组件结构

  1. xxx.component.ts:逻辑 / 数据
  2. xxx.component.html:模板
  3. xxx.component.scss:样式
  4. xxx.component.spec.ts:测试文件

示例:自定义组件

typescript 运行

// home.component.ts
import { Component } from '@angular/core';

@Component({
  // 组件选择器(HTML标签)
  selector: 'app-home',
  // 模板路径
  templateUrl: './home.component.html',
  // 样式路径
  styleUrls: ['./home.component.scss']
})
export class HomeComponent {
  // 组件数据
  title = 'Angular 入门组件';
  // 方法
  sayHello() {
    alert('Hello Angular!');
  }
}

html 预览

<!-- home.component.html -->
<div class="home">
  <h2>{{ title }}</h2>
  <button (click)="sayHello()">点击我</button>
</div>

5. 模板基础语法

5.1 插值表达式

作用:渲染组件中的变量

html 预览

<h1>{{ 变量名 }}</h1>
<p>{{ 1 + 1 }}</p>
<p>{{ name.toUpperCase() }}</p>

5.2 属性绑定

作用:给 HTML 标签动态绑定属性

html 预览

<!-- 原生属性 -->
<img [src]="imgUrl" alt="图片">
<!-- 类名绑定 -->
<div [class.active]="isActive">激活状态</div>
<!-- 样式绑定 -->
<div [style.color]="textColor">文字颜色</div>

6. 数据绑定(单向 / 双向)

6.1 单向绑定

  1. 组件 → 模板[]
  2. 模板 → 组件()

6.2 双向绑定(核心)

依赖 FormsModule,用于表单数据同步

html 预览

<input [(ngModel)]="username" placeholder="请输入用户名">
<p>你输入的用户名:{{ username }}</p>

使用前提:在 app.module.ts 导入模块

typescript 运行

import { FormsModule } from '@angular/forms';

imports: [BrowserModule, FormsModule]

7. Angular 内置指令

分为 结构指令(修改 DOM 结构)和 属性指令(修改 DOM 样式 / 属性)

7.1 结构指令

1. *ngIf 条件渲染

html 预览

<div *ngIf="isShow">显示内容</div>
<div *ngIf="!isShow">隐藏内容</div>

2. *ngFor 列表渲染

html 预览

<ul>
  <li *ngFor="let item of list; let i = index">
    索引:{{ i }},内容:{{ item.name }}
  </li>
</ul>

3. *ngSwitch 多条件判断

html 预览

<div [ngSwitch]="status">
  <p *ngSwitchCase="1">待支付</p>
  <p *ngSwitchCase="2">已支付</p>
  <p *ngSwitchDefault>未知状态</p>
</div>

7.2 属性指令

1. ngClass 动态类名

html 预览

<div [ngClass]="{ active: isActive, disabled: isDisabled }">
  动态样式
</div>

2. ngStyle 动态样式

html 预览

<div [ngStyle]="{ color: 'red', fontSize: '20px' }">
  内联样式
</div>

8. 事件绑定与用户交互

8.1 基础事件绑定

html 预览

<!-- 点击事件 -->
<button (click)="handleClick()">点击</button>
<!-- 输入事件 -->
<input (input)="handleInput($event)" />
<!-- 表单提交 -->
<form (ngSubmit)="handleSubmit()"></form>

8.2 事件对象

typescript 运行

handleInput(e: Event) {
  const value = (e.target as HTMLInputElement).value;
  console.log(value);
}

9. 组件通讯(核心)

9.1 父组件 → 子组件(@Input)

子组件

typescript 运行

import { Component, Input } from '@angular/core';

@Component({ selector: 'app-child' })
export class ChildComponent {
  // 接收父组件数据
  @Input() msg = '';
}

父组件模板

html 预览

<app-child [msg]="父组件传递的数据"></app-child>

9.2 子组件 → 父组件(@Output + EventEmitter)

子组件

typescript 运行

import { Component, Output, EventEmitter } from '@angular/core';

export class ChildComponent {
  @Output() sendMsg = new EventEmitter<string>();
  
  sendToParent() {
    this.sendMsg.emit('子组件传递的消息');
  }
}

父组件

html 预览

<app-child (sendMsg)="getMsg($event)"></app-child>

typescript 运行

getMsg(msg: string) {
  console.log(msg);
}

9.3 兄弟组件通讯

步骤 1:创建消息服务

bash 运行

ng g s services/message

typescript 运行

// message.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class MessageService {
  // 订阅主体
  private msgSubject = new Subject<any>();

  // 发送消息
  sendMessage(data: any) {
    this.msgSubject.next(data);
  }

  // 接收消息
  getMessage() {
    return this.msgSubject.asObservable();
  }
}

步骤 2:兄弟组件 A(发送方)

typescript 运行

// brother-a.component.ts
import { MessageService } from '../../services/message.service';
constructor(private msgService: MessageService) {}

sendBrotherMsg() {
  this.msgService.sendMessage('来自兄弟A的消息');
}

html 预览

<button (click)="sendBrotherMsg()">发送给兄弟B</button>

步骤 3:兄弟组件 B(接收方)

typescript 运行

// brother-b.component.ts
import { MessageService } from '../../services/message.service';
import { Subscription } from 'rxjs';

msg = '';
sub!: Subscription;

constructor(private msgService: MessageService) {}

ngOnInit() {
  // 订阅消息
  this.sub = this.msgService.getMessage().subscribe(data => {
    this.msg = data;
  });
}

// 销毁时取消订阅(防内存泄漏)
ngOnDestroy() {
  this.sub.unsubscribe();
}

html 预览

<p>接收兄弟消息:{{ msg }}</p>

步骤 4:父组件承载两个兄弟组件

html 预览

<!-- parent.component.html -->
<app-brother-a></app-brother-a>
<app-brother-b></app-brother-b>

10. 服务与依赖注入 (Service)

typescript

运行

// data.service.ts
@Injectable({ providedIn: 'root' })
export class DataService {
  user = { name: 'Angular' };
  getUser() { return this.user; }
}

组件使用

typescript

运行

constructor(private dataService: DataService) {}
ngOnInit() {
  console.log(this.dataService.getUser());
}

11. 路由 (Routing) 基础

typescript

运行

// app-routing.module.ts
const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: '**', component: HomeComponent }
];

html

预览

<router-outlet></router-outlet>
<a routerLink="/home">首页</a>

10. 服务与依赖注入 (Service)

定义

服务用于封装公共逻辑(HTTP 请求、工具函数、全局状态),实现业务解耦。

10.1 创建服务

bash 运行

ng g s services/data

10.2 服务示例

typescript 运行

// data.service.ts
import { Injectable } from '@angular/core';

// 注入根组件(全局单例)
@Injectable({ providedIn: 'root' })
export class DataService {
  // 公共数据
  userInfo = { name: 'Angular用户' };
  
  // 公共方法
  getUserInfo() {
    return this.userInfo;
  }
}

10.3 组件使用服务(依赖注入)

typescript 运行

import { DataService } from '../../services/data.service';

export class HomeComponent {
  // 依赖注入服务
  constructor(private dataService: DataService) {}
  
  ngOnInit() {
    // 使用服务
    const user = this.dataService.getUserInfo();
    console.log(user);
  }
}

11. 路由 (Routing) 基础

11.1 路由配置

typescript 运行

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './components/home/home.component';
import { AboutComponent } from './components/about/about.component';

const routes: Routes = [
  // 默认路由
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  // 404路由
  { path: '**', redirectTo: '/home' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

11.2 路由出口 + 路由跳转

html 预览

<!-- 路由出口(渲染组件) -->
<router-outlet></router-outlet>

<!-- 声明式跳转 -->
<a routerLink="/home">首页</a>
<a routerLink="/about">关于</a>

<!-- 编程式跳转 -->
<button (click)="toAbout()">跳转到关于页</button>

typescript 运行

// 编程式导航
import { Router } from '@angular/router';
constructor(private router: Router) {}
toAbout() {
  this.router.navigate(['/about']);
}

12. 表单开发(模板驱动 / 响应式)

12.1 模板驱动表单(简单表单)

html 预览

<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)">
  <input name="username" ngModel required placeholder="用户名">
  <button type="submit">提交</button>
</form>

12.2 响应式表单(复杂表单,推荐)

导入模块

typescript 运行

import { ReactiveFormsModule } from '@angular/forms';

使用

typescript 运行

import { FormBuilder, FormGroup, Validators } from '@angular/forms';

export class LoginComponent {
  loginForm: FormGroup;
  
  constructor(private fb: FormBuilder) {
    // 初始化表单
    this.loginForm = this.fb.group({
      username: ['', [Validators.required, Validators.minLength(2)]],
      password: ['', [Validators.required]]
    });
  }
  
  onSubmit() {
    console.log(this.loginForm.value);
  }
}

html 预览

<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
  <input formControlName="username">
  <input formControlName="password">
  <button type="submit" [disabled]="!loginForm.valid">提交</button>
</form>

13. HTTP 网络请求

13.1 导入模块

typescript 运行

import { HttpClientModule } from '@angular/common/http';

13.2 封装 HTTP 服务

typescript 运行

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class HttpService {
  constructor(private http: HttpClient) {}
  
  // GET请求
  getList(): Observable<any> {
    return this.http.get('https://api.example.com/list');
  }
  
  // POST请求
  addData(data: any): Observable<any> {
    return this.http.post('https://api.example.com/add', data);
  }
}

13.3 组件使用

typescript 运行

this.httpService.getList().subscribe({
  next: (res) => console.log(res),
  error: (err) => console.error(err)
});

14. 管道 (Pipe)

14.1 内置管道

html 预览

<!-- 日期管道 -->
<p>{{ now | date:'yyyy-MM-dd' }}</p>
<!-- 大小写管道 -->
<p>{{ name | uppercase }}</p>
<!-- 小数管道 -->
<p>{{ num | number:'1.2-2' }}</p>
<!-- JSON管道 -->
<p>{{ obj | json }}</p>

14.2 自定义管道

示例 1:性别转换管道

bash 运行

ng g p pipes/sex-transform

typescript 运行

// sex-transform.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'sexTransform' })
export class SexTransformPipe implements PipeTransform {
  // value:传入值,args:额外参数
  transform(value: number): string {
    switch (value) {
      case 1: return '男';
      case 2: return '女';
      default: return '未知';
    }
  }
}

使用方式

html 预览

<p>{{ 1 | sexTransform }}</p> <!-- 输出:男 -->
<p>{{ 2 | sexTransform }}</p> <!-- 输出:女 -->

示例 2:数组过滤管道

typescript 运行

// filter.pipe.ts
@Pipe({ name: 'filterList' })
export class FilterListPipe implements PipeTransform {
  transform(list: any[], key: string, keyword: string): any[] {
    if (!keyword) return list;
    return list.filter(item => item[key].includes(keyword));
  }
}

html 预览

<li *ngFor="let item of list | filterList: 'name': keyword">{{item.name}}</li>

15. 组件生命周期钩子

15.1 Angular 共有 8 个生命周期钩子,按执行顺序排列,包含创建 / 更新 / 销毁全流程

typescript 运行

import {
  Component,
  OnChanges,
  OnInit,
  DoCheck,
  AfterContentInit,
  AfterContentChecked,
  AfterViewInit,
  AfterViewChecked,
  OnDestroy,
  Input
} from '@angular/core';

@Component({
  selector: 'app-life-cycle',
  template: `<p>{{ msg }}</p>`
})
export class LifeCycleComponent implements
  OnChanges,
  OnInit,
  DoCheck,
  AfterContentInit,
  AfterContentChecked,
  AfterViewInit,
  AfterViewChecked,
  OnDestroy {

  @Input() msg = '测试';

  // 1. 输入属性(@Input)改变时触发
  ngOnChanges(): void {
    console.log('1. ngOnChanges - 属性改变');
  }

  // 2. 组件初始化(最常用,请求数据)
  ngOnInit(): void {
    console.log('2. ngOnInit - 组件初始化完成');
  }

  // 3. 脏值检测(每次变更检测触发)
  ngDoCheck(): void {
    console.log('3. ngDoCheck - 变更检测');
  }

  // 4. 内容投影初始化完成
  ngAfterContentInit(): void {
    console.log('4. ngAfterContentInit - 内容投影初始化');
  }

  // 5. 内容投影变更检测
  ngAfterContentChecked(): void {
    console.log('5. ngAfterContentChecked - 内容检测');
  }

  // 6. 视图初始化完成(操作DOM)
  ngAfterViewInit(): void {
    console.log('6. ngAfterViewInit - 视图渲染完成');
  }

  // 7. 视图变更检测
  ngAfterViewChecked(): void {
    console.log('7. ngAfterViewChecked - 视图检测');
  }

  // 8. 组件销毁(清理定时器/订阅)
  ngOnDestroy(): void {
    console.log('8. ngOnDestroy - 组件销毁');
  }
}

15.2 执行顺序

  1. ngOnChanges → 输入属性变化
  2. ngOnInit → 初始化
  3. ngDoCheck → 每次渲染检查
  4. ngAfterContentInit → 内容投影
  5. ngAfterContentChecked → 内容检查
  6. ngAfterViewInit → DOM 渲染完成
  7. ngAfterViewChecked → 视图检查
  8. ngOnDestroy → 组件销毁

15.3 核心使用场景

  • ngOnInit发起网络请求、初始化数据
  • ngOnChanges:监听父组件传值变化
  • ngAfterViewInit:操作 DOM 元素
  • ngOnDestroy清除定时器、取消订阅、防内存泄漏

16. 常用装饰器总结

表格

装饰器 作用 位置
@Component 定义组件 组件类
@NgModule 定义模块 模块类
@Injectable 定义服务 服务类
@Input 父传子 子组件属性
@Output 子传父 子组件事件
@ViewChild 获取 DOM / 子组件 组件属性

17. 项目打包与部署

17.1 生产打包

bash 运行

ng build --prod
# 或
ng build --configuration production

打包产物:dist/ 目录

17.2 部署

dist 目录静态文件部署到 Nginx、Apache、GitHub Pages 等平台。


18. 总结

本文覆盖了 Angular 入门所有核心基础知识点,包含:

  • 环境搭建、模块 / 组件基础
  • 模板语法、数据绑定、内置指令
  • 组件通讯、服务注入、路由、表单、HTTP
  • 生命周期、管道、打包部署

Angular 作为企业级框架,学习曲线略陡,但规范统一、生态完善、长期维护,非常适合中大型项目开发。掌握以上知识点,即可独立开发 Angular 中小型应用!

总篇:iframe沙盒存储隔离:从紧急补丁到企业级防御体系的完整指南

在微前端与第三方组件集成的浪潮下,iframe 沙盒环境中的 SessionStorage 安全问题,正从一个隐秘的"技术细节"演变为可能引发数据泄露、权限逃逸的"阿喀琉斯之踵"。本系列文章将为你完整呈现:我们如何从一次真实攻防演练中发现致命漏洞,到构建一套经过生产验证的三层纵深防御体系的全过程。 在这里插入图片描述


缘起:一次攻防演练暴露的"沙盒幻象"

在一次内部红蓝对抗中,一个看似平常的设定引发了我们的警觉:一个嵌入在同源 iframe 中、完全受控的第三方图表组件,竟能悄无声息地读取并篡改主应用存储在 sessionStorage 中的用户令牌(authToken)、管理员权限(userRole)及核心业务数据。

核心漏洞

浏览器同源策略保护的是"源"而非"上下文"。当 iframe 的 sandbox 属性包含 allow-same-origin 时,它与主应用被视为同一源,从而共享同一份 sessionStorage 物理存储

这不是浏览器的 Bug,而是其安全模型的一个特性——却也成了攻击者眼中的"特性":

// 恶意代码可轻易在 iframe 内执行
const stolenToken = sessionStorage.getItem('authToken');  // 窃取令牌
sessionStorage.setItem('userRole', 'super_admin');        // 权限提升

我们意识到:这不仅是代码冲突,更是严重的安全漏洞。任何一个被嵌入的第三方组件(即使来源可信),一旦被 XSS 攻击或自身存在恶意代码,都可能成为突破"沙盒"的跳板。


破局:构建纵深防御的思维演进

面对这一问题,简单的"禁用某个属性"或"期望对方整改"并不可靠。我们需要的是一套可自主掌控、可持续演进的技术方案。解决思路经历了三次关键进化:

层级 策略 核心手段 定位
L1 快速止血 通信层修复 移除 allow-same-origin,通过严格的 postMessage 替代直接存储访问 紧急响应,治标不治本
L2 核心隔离 代理层隔离 运行时拦截:动态代理 sessionStorage API,自动添加命名空间前缀(如 ns_app1_ 性价比最高的方案
L3 体系防御 监控层防护 存储访问代理层 + 运行时行为监控 + 安全策略执行层,支持异常检测、自动阻断、灰度发布 企业级基础设施

系列导航:你将在这三篇文章中获得什么

本系列分为上、中、下三篇,由浅入深,带你走完从认知漏洞到建立堡垒的完整路径。

📘 上篇(总篇):《iframe沙盒存储隔离:从紧急补丁到企业级防御体系的完整指南》

(首篇发布)

  • 核心价值:建立完整认知,提供可立即执行的紧急修复方案
  • 你会学到
    • 同源策略与存储共享机制的底层原理
    • allow-same-origin + allow-scripts 组合的致命风险
    • L1 快速止血方案:postMessage 通信改造的最佳实践
    • 如何评估现有系统的暴露面与风险等级
  • 适合读者:所有使用 iframe 的前端开发者、技术经理、安全工程师

🔰 中篇:《手把手拦截——iframe 沙盒 SessionStorage 隔离的轻量级实践》

(第二篇发布)

  • 核心价值:给你一套"开箱即用"的代码,立即解决数据污染问题
  • 你会学到
    • Monkey Patch(猴子补丁)技术:优雅劫持 iframe 内的存储 API
    • 完整的 Vue/React 示例代码(前缀隔离、安全的 clear 方法改造)
    • 嵌套 iframe、Storage 事件监听等边界情况的处理
  • 适合读者:一线前端工程师、团队技术骨干,寻求快速有效解决方案的实践者

🛡️ 下篇:《从漏洞到堡垒——构建企业级 iframe 存储安全纵深防御体系》

(第三篇发布)

  • 核心价值:呈现可应对复杂攻击、支撑大型工程的安全架构蓝本
  • 你会学到
    • 基于 Proxy 与 MutationObserver 的健壮代理实现(防绕过)
    • 生产级部署:灰度发布、监控指标、性能测试与回滚方案
    • 与 W3C Storage Access API 的对比与融合路径
    • 开源安全框架的设计思路
  • 适合读者:前端架构师、技术负责人、安全工程师,关注高可用、高安全、可演进架构的决策者

为什么你需要关注这个系列?

  1. 问题普遍性:只要你使用了同源 iframe 嵌入(微前端、第三方 SDK、多团队协作),就可能面临此风险
  2. 方案完整性:从"救火"的 50 行代码,到"防火"的系统工程,提供不同阶段的解决方案
  3. 实战参考性:所有方案均源于真实攻防演练与生产环境迭代,包含踩坑记录与决策权衡
  4. 视野前瞻性:不止于解决当下问题,更探讨与 Web 标准接轨的未来演进路径

安全不是可选项,而是现代 Web 应用的默认值。 对 iframe 沙盒存储漏洞的忽视,可能让精心构建的应用防线从内部被攻破。

本系列文章正是为你厘清风险、提供武器、建立防线的实战指南。敬请期待后续的深度解析。


[下篇预告]:,我们将直接切入实战,剖析漏洞原理,并附上一段可直接复制使用的代码,让你能在半小时内为你的 iframe 应用穿上第一件"隔离衣"。


SwiftUI路由管理架构揭秘:从混乱到优雅的蜕变

引言

想象一下:当你打开一个 App,点击不同标签页,切换页面时,所有导航状态都能完美保持;当你从详情页返回时,TabBar 能智能地重新出现;当你需要传递数据时,类型安全的导航能让你告别字符串硬编码的烦恼。这一切,都离不开一个优秀的路由管理架构。

在现代 iOS 应用开发中,路由管理常常被视为"基础设施"而被忽视,但其重要性却不亚于任何核心功能。一个设计良好的路由系统,不仅能让代码结构更清晰,还能显著提升用户体验。今天,我将带大家深入剖析我项目中的路由管理架构,分享从设计到实现的全过程,希望能为你的项目带来启发。

路由架构概览

我项目的路由管理基于 SwiftUI 的 NavigationStackNavigationPath,采用了集中式的路由管理方案。核心组件包括:

  • Router 类:全局导航路由器,管理所有 Tab 的导航路径
  • MainTab 枚举:定义应用的标签页结构
  • MainContainerView:主容器视图,负责整合标签页和导航逻辑
  • App 启动注入:在应用启动时将 Router 注入到环境中

路由的启动注入

EviApp.swift 中,我们通过 @StateObject 创建 Router 实例,并通过 environmentObject 将其注入到应用环境中:

import SwiftUI

@main
struct EviApp: App {
    // 把 AppDelegate 接进来,系统会照常调用 didFinishLaunchingWithOptions 等
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    // 全局弹框管理器
    @StateObject private var overlay = GlobalOverlayManager.shared
    // 全局导航路由器
    @StateObject private var router = Router()
    
    var body: some Scene {
        WindowGroup {
            MainContainerView()
                .environmentObject(overlay)
                .environmentObject(router)
        }
    }
}

这样,在应用的任何视图中,都可以通过 @EnvironmentObject 来访问 Router 实例,实现全局路由管理。

核心组件分析

1. Router 类:路由管理的核心

import SwiftUI

/// 全局导航路由器,管理所有Tab的导航路径
class Router: ObservableObject {
    
    // 当前选中的Tab
    @Published var selectedTab: MainTab = .home
    
    // 为每个tab单独存储NavigationPath
    @Published var homePath = NavigationPath()
    @Published var hotPath = NavigationPath()
    @Published var creationPath = NavigationPath()
    @Published var stylePath = NavigationPath()
    @Published var profilePath = NavigationPath()
    
    // MARK: - 获取导航路径
    
    /// 获取指定tab的导航路径
    func getNavigationPath(for tab: MainTab) -> NavigationPath {
        switch tab {
        case .home: return homePath
        case .hot: return hotPath
        case .creation: return creationPath
        case .style: return stylePath
        case .profile: return profilePath
        }
    }
    
    /// 获取指定tab的导航路径绑定
    func getNavigationPathBinding(for tab: MainTab) -> Binding<NavigationPath> {
        switch tab {
        case .home: return binding(for: \.homePath)
        case .hot: return binding(for: \.hotPath)
        case .creation: return binding(for: \.creationPath)
        case .style: return binding(for: \.stylePath)
        case .profile: return binding(for: \.profilePath)
        }
    }
    
    // MARK: - 清空导航路径
    
    /// 清空指定tab的导航路径
    func clearPath(for tab: MainTab) {
        switch tab {
        case .home: clear(\.homePath)
        case .hot: clear(\.hotPath)
        case .creation: clear(\.creationPath)
        case .style: clear(\.stylePath)
        case .profile: clear(\.profilePath)
        }
    }
    
    /// 清空所有导航路径
    func clearAllPaths() {
        clear(\.homePath)
        clear(\.hotPath)
        clear(\.creationPath)
        clear(\.stylePath)
        clear(\.profilePath)
    }
    
    // MARK: - 当前Tab操作
    
    /// 获取当前选中Tab的导航路径
    func getCurrentNavigationPath() -> NavigationPath {
        return getNavigationPath(for: selectedTab)
    }
    
    /// 获取当前选中Tab的导航路径绑定
    func getCurrentNavigationPathBinding() -> Binding<NavigationPath> {
        return getNavigationPathBinding(for: selectedTab)
    }
    
    /// 清空当前选中Tab的导航路径
    func clearCurrentPath() {
        clearPath(for: selectedTab)
    }
    
    // MARK: - 私有辅助方法
    
    /// 创建导航路径的绑定
    private func binding(for keyPath: ReferenceWritableKeyPath<Router, NavigationPath>) -> Binding<NavigationPath> {
        Binding {
            self[keyPath: keyPath]
        } set: {
            self[keyPath: keyPath] = $0
        }
    }
    
    /// 清空指定的导航路径
    private func clear(_ keyPath: ReferenceWritableKeyPath<Router, NavigationPath>) {
        self[keyPath: keyPath].removeLast(self[keyPath: keyPath].count)
    }
}

设计亮点

  • 集中管理:所有路由逻辑集中在一个类中,便于统一管理
  • Tab 隔离:为每个标签页维护独立的导航路径,确保切换标签时不会影响其他标签的导航状态
  • 响应式设计:使用 @Published 修饰符,实现路由状态的自动更新
  • 便捷方法:提供了丰富的方法来操作导航路径,如获取路径、清空路径等

2. MainTab 枚举:标签页定义

import SwiftUI

/// 主标签栏枚举
enum MainTab {
    case home
    case hot
    case creation
    case style
    case profile
}

extension MainTab {
    
    /// 根据选中状态返回对应的图标名称
    func iconName(isSelected: Bool) -> String {
        switch self {
        case .home:
            return isSelected ? "tabbar_home_sel" : "tabbar_home_nor"
        case .hot:
            return isSelected ? "tabbar_hot_sel" : "tabbar_hot_nor"
        case .creation:
            return "tabbar_add"
        case .style:
            return isSelected ? "tabbar_style_sel" : "tabbar_style_nor"
        case .profile:
            return isSelected ? "tabbar_me_sel" : "tabbar_me_nor"
        }
    }
}

设计亮点

  • 类型安全:使用枚举定义标签页,避免了字符串硬编码
  • 扩展功能:通过扩展为枚举添加了获取图标名称的功能,使代码更整洁

3. MainContainerView:路由的实际应用

import SwiftUI

/// 主容器视图,包含悬浮TabBar
struct MainContainerView: View {
    
    // 获取指定tab的导航路径
    private func getNavigationPath(for tab: MainTab) -> NavigationPath {
        return router.getNavigationPath(for: tab)
    }
    
    /// 创建带有NavigationStack的标签页视图
    private func tabView(_ tab: MainTab) -> some View {
        NavigationStack(path: router.getNavigationPathBinding(for: tab)) {
            switch tab {
            case .home:
                HomeView()
            case .hot:
                HotHomeView()
            case .creation:
                CreationHomeView()
            case .style:
                StyleHomeView()
            case .profile:
                ProfileHomeView()
            }
        }
        .tag(tab)
    }
    
    @StateObject private var appConfigManager = AppConfigManager.shared
    
    @EnvironmentObject private var overlay: GlobalOverlayManager
    @EnvironmentObject private var router: Router
    
    var body: some View {
        if appConfigManager.appConfig != nil {
            ZStack {
                
                // 真正负责页面生命周期的容器
                TabView(selection: $router.selectedTab) {
                    tabView(.home)
                    tabView(.hot)
                    tabView(.creation)
                    tabView(.style)
                    tabView(.profile)
                }
                
                // 你的悬浮TabBar,根据当前选中标签的导航路径长度控制显示
                if isTabBarVisible {
                    VStack {
                        Spacer()
                        FloatingTabBar(selectedTab: $router.selectedTab)
                            .padding(.horizontal, 16)
                            .padding(.bottom, 20)
                    }
                }
                
                // 全局弹框显示
                if let current = overlay.current {
                    
                    // 遮罩
                    Color.black.opacity(0.4)
                        .ignoresSafeArea()
                        .onTapGesture {
                            overlay.dismiss()
                        }
                    
                    switch current {
                    case .login:
                        LoginOverlayView(onClose: {
                            overlay.dismiss()
                        })
                        .transition(.flipFromBottom)
                    }
                }
                
            }
            .animation(.easeInOut(duration: 0.25), value: overlay.current)
        } else {
            // 显示空View
            EmptyView()
                .background(ThemeManager.Background.global)
        }
    }
    
    var isTabBarVisible: Bool {
        return getNavigationPath(for: router.selectedTab).count == 0
    }
}

设计亮点

  • NavigationStack 集成:为每个标签页创建独立的 NavigationStack
  • TabBar 智能显示:根据当前导航路径长度控制 TabBar 的显示/隐藏
  • 环境对象注入:使用 @EnvironmentObject 注入 Router,实现全局访问
  • 动画效果:添加了平滑的过渡动画,提升用户体验

路由管理的实现细节

1. 路径管理机制

路由系统的核心是 NavigationPath 的管理。NavigationPath 是 SwiftUI 4.0+ 引入的类型,它是一个类型擦除的容器,可以存储任意类型的导航目的地。

在我们的实现中:

  • 每个标签页都有自己的 NavigationPath 实例
  • 通过 getNavigationPathBinding 方法获取路径的绑定,用于 NavigationStack
  • 提供了 clearPathclearAllPaths 方法来清空导航路径

2. 标签页切换逻辑

当用户切换标签页时:

  1. router.selectedTab 的值会更新
  2. TabView 会根据新的 selectedTab 显示对应的标签页
  3. 由于每个标签页有独立的 NavigationPath,切换标签不会影响其他标签的导航状态

3. 导航路径的实际使用

在具体的视图中,可以通过以下方式使用路由:

// 在视图中注入 Router
@EnvironmentObject private var router: Router

// 使用全局路由管理进行导航
let currentPath = router.getCurrentNavigationPathBinding()
// 向当前路径添加新页面
currentPath.wrappedValue.append(AppNavigationDestination.materialDetail(material))

// 清空当前标签页的导航路径
router.clearCurrentPath()

4. 导航目的地定义

项目使用 AppNavigationDestination 枚举来定义导航目的地:

import Foundation
import SwiftUI

/// 导航目标枚举
enum AppNavigationDestination: Hashable {
    case accountLogin
    case materialDetail(MaterialListDTOElement)
}

这种方式的优势:

  • 类型安全:使用枚举定义导航目的地,避免了字符串硬编码
  • 参数传递:可以在导航时传递相关数据,如 materialDetail 中的 MaterialListDTOElement
  • 可扩展性:可以轻松添加新的导航目的地

5. NavigationStack 中处理导航目的地

在使用 NavigationStack 时,需要处理导航目的地的显示逻辑。通常在根视图中添加 navigationDestination 修饰符:

NavigationStack(path: router.getNavigationPathBinding(for: tab)) {
    HomeView()
        .navigationDestination(for: AppNavigationDestination.self) { destination in
            switch destination {
            case .accountLogin:
                AccountLoginView()
            case .materialDetail(let material):
                MaterialDetailView(material: material)
            }
        }
}

这样,当我们通过 currentPath.wrappedValue.append(AppNavigationDestination.materialDetail(material)) 导航时,NavigationStack 会自动显示对应的目标视图。

6. 完整导航流程示例

下面是一个完整的导航流程示例,展示从触发导航到显示目标页面的全过程:

// 1. 在视图中注入 Router
@EnvironmentObject private var router: Router

// 2. 定义导航触发事件
Button("查看素材详情") {
    // 3. 获取当前路径绑定
    let currentPath = router.getCurrentNavigationPathBinding()
    // 4. 向路径添加导航目的地
    currentPath.wrappedValue.append(AppNavigationDestination.materialDetail(selectedMaterial))
}

// 5. 在根视图中处理导航目的地
NavigationStack(path: router.getNavigationPathBinding(for: .home)) {
    HomeView()
        .navigationDestination(for: AppNavigationDestination.self) { destination in
            switch destination {
            case .materialDetail(let material):
                MaterialDetailView(material: material)
            default:
                EmptyView()
            }
        }
}

// 6. 从详情页返回
Button("返回") {
    // 清空当前路径,返回根视图
    router.clearCurrentPath()
}

7. 导航路径与 TabBar 显示的关联

MainContainerView 中,通过 isTabBarVisible 计算属性控制 TabBar 的显示:

var isTabBarVisible: Bool {
    return getNavigationPath(for: router.selectedTab).count == 0
}

当导航路径为空时(即处于标签页的根视图),显示 TabBar;当导航路径不为空时(即进入了子页面),隐藏 TabBar,为用户提供更大的内容显示区域。

优势与最佳实践

优势

  1. 清晰的职责分离:路由逻辑与 UI 逻辑分离,使代码更易于维护
  2. 类型安全:使用枚举和类型化的导航路径,减少运行时错误
  3. 状态管理:集中管理路由状态,避免状态分散
  4. 灵活性:可以轻松添加新的标签页和导航目的地
  5. 用户体验:标签页切换时保持各自的导航状态,提升用户体验

最佳实践

  1. 统一的路由入口:所有导航操作都通过 Router 进行,避免直接操作 NavigationPath
  2. 合理的路径清理:在适当的时机清理导航路径,避免内存占用过高
  3. 导航目的地的类型定义:为导航目的地创建明确的类型,提高代码可读性
  4. 错误处理:添加适当的错误处理,确保导航操作的稳定性
  5. 测试:为路由逻辑编写单元测试,确保其正确性

代码优化建议

  1. 导航目的地类型化

    // 建议为每个标签页创建导航目的地枚举
    enum HomeDestination {
        case detail(id: String)
        case search
    }
    
    // 然后在导航时使用
    router.homePath.append(HomeDestination.detail(id: "123"))
    
  2. 添加导航日志

    // 添加导航日志,便于调试和分析用户行为
    func appendToPath(_ value: some Hashable, for tab: MainTab) {
        let path = getNavigationPathBinding(for: tab)
        path.wrappedValue.append(value)
        print("Navigate to \(value) in tab \(tab)")
    }
    
  3. 导航路径持久化

    // 可以考虑在应用进入后台时保存导航状态,在应用启动时恢复
    func saveNavigationState() {
        // 保存导航状态到 UserDefaults 或其他存储
    }
    
    func restoreNavigationState() {
        // 从存储中恢复导航状态
    }
    
  4. 添加路由拦截器

    // 可以添加路由拦截器,用于处理登录验证等场景
    func appendToPath(_ value: some Hashable, for tab: MainTab) {
        if needsAuthentication(for: value) {
            // 显示登录界面
            overlay.present(.login)
        } else {
            let path = getNavigationPathBinding(for: tab)
            path.wrappedValue.append(value)
        }
    }
    

总结

通过以上分析,我们可以看到,一个良好的路由管理架构对于 iOS 应用的重要性。我项目中的路由架构采用了集中式管理、Tab 隔离、响应式设计等原则,通过 Router 类、MainTab 枚举和 MainContainerView 的配合,实现了清晰、灵活、用户友好的导航体验。

这种路由架构不仅适用于当前项目,也可以作为其他 SwiftUI 项目的参考。通过不断优化和扩展,可以构建更加完善的路由系统,为用户提供更加流畅的应用体验。

希望这篇文章能够帮助大家更好地理解和实现 iOS 项目中的路由管理架构。如果你有任何问题或建议,欢迎在评论区留言讨论!

❌