
做Angular开发这些年最让我觉得“真香”的特性之一就是管道Pipe。早年我还在用JQuery手动拼HTML的时候格式化一个日期要写一堆字符串方法后来换到Angular模板里一个| date:yyyy-MM-dd就把事办了这种声明式写法的爽快感用过就回不去了。如果你是个刚开始学Angular的开发者或者被项目里一堆| async、| date、| currency搞得有点晕这篇文章就是给你准备的。我会从管道的设计初衷讲起到内置管道怎么用、自定义管道怎么写再到纯管道与非纯管道的变更检测机制、面试高频考点和实际开发中的坑尽量把这一块讲透。不管你是想系统补课还是马上要面Angular岗位这篇内容应该都能帮到你。1. 为什么要用管道数据格式化的痛点与解法1.1 我踩过的格式化地狱先聊个场景。几年前我维护一个后台管理系统列表页要显示订单金额、创建时间、状态文本。那时候项目还用着老一套的模板语法每次渲染前都要在组件类里写一堆方法formatDate(date: Date): string { return ${date.getFullYear()}-${this.pad(date.getMonth() 1)}-${this.pad(date.getDate())}; } formatAmount(amount: number): string { return ¥ amount.toFixed(2); }模板里就变成了这样p{{ formatDate(order.createdAt) }}/p p{{ formatAmount(order.amount) }}/p代码能跑但问题一堆。第一组件类越来越臃肿各种格式化函数堆在一起第二这些方法没法复用另一个组件要用同样的日期格式只能复制粘贴第三模板里还有大量v-if之类的逻辑判断状态文本可读性很差。更麻烦的是如果某天后端返回的数据结构变了我得在所有调用过formatDate的地方排查相当痛苦。后来我把这些格式化逻辑抽成了公共方法放到一个工具类里情况好了一些。但模板里的调用方式依然不够优雅而且要手动处理空值、异常一旦某个字段是null整个页面直接白屏报错。这种“格式化地狱”直到我彻底用上Angular管道才真正解决。1.2 管道到底是什么以及它和普通函数的区别Angular管道本质上是一个带有Pipe装饰器的类它实现PipeTransform接口核心方法就是transform。在模板里的用法非常直观p{{ order.createdAt | date: yyyy-MM-dd }}/p p{{ order.amount | currency: CNY }}/p管道和普通函数的第一个区别是管道是声明式的。你在模板里写的是“我想展示什么格式”而不是“我要调用哪个函数来处理”。这种声明式思维让模板更像一份描述文档而不是一串命令式脚本。第二个区别在于变更检测。管道在Angular的变更检测机制中有特殊地位——纯管道会被缓存只有输入引用变化时才会重新执行。这意味着同样的数据在多次变更检测中不会反复调用transform方法性能上有天然优势。这个机制我会在第4章详细展开。第三个区别是管道可以链式调用。比如先截断字符串再转大写p{{ title | slice: 0: 10 | uppercase }}/p这种组合能力让数据转换变得非常灵活而且完全在模板层完成组件的逻辑代码保持干净。这也是我在多个项目中坚持用管道替代组件内格式化方法的核心原因——管道把“数据展示逻辑”从“业务逻辑”中彻底剥离开来各管各的互不污染。2. 内置管道的完整能力地图选型与实战参数2.1 最常用内置管道与核心参数Angular自带了一批内置管道覆盖了日常开发中90%以上的格式化需求。我把它们分为三类格式化类、字符串类、异步类每一类选出最常用的几个给出参数说明和实际效果。管道名作用使用示例输出效果DatePipe日期格式化{{ today | date: yyyy-MM-dd HH:mm }}2025-01-15 14:30CurrencyPipe货币格式化{{ price | currency: CNY: symbol: 1.2-2 }}¥1,234.56DecimalPipe数字格式化{{ num | number: 1.2-2 }}1,234.57PercentPipe百分比格式化{{ rate | percent: 1.0-2 }}12.50%UpperCasePipe转大写{{ name | uppercase }}ANGULARLowerCasePipe转小写{{ name | lowercase }}angularTitleCasePipe首字母大写{{ title | titlecase }}Hello WorldSlicePipe截取数组/字符串{{ arr | slice: 1: 3 }}[2, 3]AsyncPipe异步数据自动订阅{{ data$ | async }}自动解析以DatePipe为例它的第二个参数是日期格式字符串支持多种预置格式short、medium、long、full也支持自定义格式如yyyy-MM-dd。注意yyyy和YYYY是不同的前者是日历年份后者是ISO周年份在跨年那几天比如12月31日恰好是新一周的周三结果可能不同。这个细节我在生产环境踩过当时测试用例跑不过排查了半天才发现是YYYY的锅。CurrencyPipe的参数稍微复杂一点。第一个参数是货币代码比如CNY、USD第二个参数是显示方式symbol表示显示符号¥、$code表示显示代码CNY、USDsymbol-narrow在某些货币下会用更窄的符号第三个参数是数字格式1.2-2表示整数最少1位、小数2位、最多2位。这些参数如果不传会使用默认的locale配置。在项目里我习惯把常用的管道参数封装成常量或自定义管道避免每个模板里写一长串魔法数字。比如统一封装一个dateTimeFormat yyyy-MM-dd HH:mm:ss声明在常量文件里模板里直接引用。2.2 AsyncPipe异步数据的优雅处理如果说哪个内置管道最值得认真研究我会毫不犹豫地说AsyncPipe。它解决的是异步数据渲染的痛点以前我们要在组件里手动subscribe把结果赋给一个变量还要在ngOnDestroy里unsubscribe防止内存泄漏。这套逻辑写起来很繁琐而且容易漏。用了AsyncPipe之后模板里直接写div *ngIfuser$ | async as user p{{ user.name }}/p p{{ user.email }}/p /divuser$是一个ObservableUserasync管道会隐式订阅它当数据到达时自动触发变更检测组件销毁时自动退订。一个管道同时帮你解决了订阅和取消订阅两个问题这是最划算的用法。更妙的是as语法*ngIfuser$ | async as user的意思是把管道解析出的结果赋值给模板变量user这样在*ngIf的作用域内可以多次使用user而不会重复订阅。如果不加as你写{{ (user$ | async)?.name }}数据流每次被访问都会重新订阅一次虽然Angular内部有缓存但代码可读性差很多。AsyncPipe还支持Promise不过在实际开发中我的建议是统一用Observable因为Angular的生态和HttpClient都基于响应式编程范式。另外有一个坑要提醒不要在模板里多次使用同一个async管道订阅同一个流尤其是在*ngIf和子元素里都写容易造成重复订阅和额外开销。优先用as提取变量。2.3 内置管道中的冷门细节有些内置管道平时不常用但关键时刻能省不少事。比如KeyValuePipe可以把对象转成可迭代的键值对数组在模板里用*ngFor遍历对象属性option *ngForlet item of config | keyvalue [value]item.key {{ item.value }} /option再比如I18nPluralPipe和I18nSelectPipe处理复数形式和条件文案非常方便。I18nSelectPipe可以根据值映射到不同文案相当于模板里的简易switchp{{ status | i18nSelect: statusMessages }}/pstatusMessages { pending: 等待审核, approved: 已通过, rejected: 已拒绝, other: 未知状态 };这个管道在需要根据枚举值显示不同文案的场景里特别好用比在组件里写一堆if/else干净得多。还有个容易忽略的点内置管道的优先级。管道操作符|的优先级高于赋值运算符但低于三元运算符。所以写{{ (condition ? a : b) | uppercase }}时要加括号否则管道会作用在整个三元表达式上结果可能不是你想要的。这类小细节没啥难度但能避免线上bug。3. 自定义管道与匿名管道的辨析从零动手写一个Pipe3.1 自定义管道的完整步骤内置管道虽然多但业务需求更奇怪。什么“手机号脱敏”“姓名打码”“文章摘要截取”内置管道根本不管这些。这时候就得自己动手写。我以一个“手机号中间四位打码”的需求为例带你走一遍完整流程。第一步生成管道文件。用Angular CLI的话一行命令搞定ng generate pipe phone-mask这个命令会自动创建phone-mask.pipe.ts文件并把它注册到最近的模块或独立组件的imports数组里。第二步实现转换逻辑。打开生成的管道文件改造如下import { Pipe, PipeTransform } from angular/core; Pipe({ name: phoneMask }) export class PhoneMaskPipe implements PipeTransform { transform(value: string, start: number 3, end: number 7, maskChar: string ****): string { if (!value || value.length 7) { return value || ; } return value.slice(0, start) maskChar value.slice(end); } }第三步在模板中直接使用p{{ user.phone | phoneMask }}/p p{{ user.phone | phoneMask: 2: 6 : ***** }}/p这里用到了管道的参数传递机制在模板管道后面用冒号分隔传入参数第一个参数对应transform方法的第二个参数第一个参数永远是数据源本身。我故意把transform的第一个参数设为value这是PipeTransform接口约定不变的。自定义管道写了之后记得检查它被声明在哪个模块或组件的imports里。如果项目用的是独立组件模式就直接在组件imports数组里加上管道类如果用的是NgModule模式就在模块的declarations里声明同时必须在exports里导出否则其他模块的模板用不了这个管道。3.2 匿名管道到底指什么一次把概念讲透关于“匿名管道”这个词我先说个结论Angular官方并没有“匿名管道”这个术语。你在Angular代码里看到的每一个管道类都是有名字的。但“匿名管道”这个词在开发者社区里确实经常被提及它到底指什么我理解的“匿名管道”大致对应两种场景。第一种是指在模板里直接写一个简短的管道表达式不单独定义管道类。比如p{{ user.name | slice: 0: 1 | uppercase }}/p这种写法“匿名”在管道实例上——没有变量名没有显式声明就是模板内联的一次性转换逻辑。这种用法很适合逻辑简单、只在一个地方用的转换强行封装成管道类反而增加心智负担。第二种更接近术语本意。在计算机体系里“匿名管道”本指没有名字的进程间通信通道。Angular中的管道机制其实也有点这个味道——数据从一个表达式“流向”管道经过转换再“流向”模板展示位置数据流本身不关心管道叫什么名字只关心输入和输出。从这个角度看Angular管道天生就是一种“匿名”的数据转换流。但我要提醒一点不要在业务代码里大量堆所谓“匿名”的内联管道表达式。如果发现同一个slice | uppercase | lowercase组合在两个以上地方出现我建议你封装成自定义管道。维护的时候改一处就行排查问题也不需要到处找模板。3.3 管道传参、链式调用与安全导航管道传参这块很多初学者容易搞混参数顺序。先看个典型的链式调用p{{ order.note | slice: 0: 20 | uppercase }}.../p执行顺序是从左到右先对order.note做slice截取前20个字符再对截取结果做uppercase。管道之间是串行处理的前一个的输出就是下一个的输入。参数传递有个容易踩的坑如果你需要传一个变量作为参数直接在冒号后面写变量名就行不需要加引号。比如p{{ user.createdAt | date: dateFormat }}/pdateFormat是组件类里的属性Angular会自动解析它的值作为date管道的格式参数。要注意的是如果dateFormat是字符串模板里不能写成{{ user.createdAt \| date: dateFormat }}加引号会把dateFormat当成字面字符串传进去那格式化结果就完全不对了。安全导航操作符?.和管道也是天生搭档。比如用户信息可能为空时p{{ user?.profile?.bio | slice: 0: 50 }}/p当user或user.profile为null/undefined时?会短路返回undefined然后slice管道会收到一个undefined输入。此时管道内部一定要做空值保护否则会抛异常。这也是我在自己写的管道里默认加头两行空值判断的原因if (value null || value ) { return ; }4. 纯管道与非纯管道性能与正确性的核心权衡4.1 纯管道的变更检测机制到底怎么运作的管道的pure属性是Angular性能设计上的一个核心点。默认情况下Pipe装饰器的pure值为true也就是说绝大多数管道是纯管道。纯管道意味着只有当输入值本身发生变化时管道才会重新执行transform方法。这里的关键在于Angular如何判断“输入值发生变化”——它用的是引用比较而不是深度比较。什么意思如果输入是基本类型字符串、数字、布尔那只要值变了引用自然就变了纯管道会正常更新如果输入是对象或数组Angular比较的是内存地址。地址变了才重新执行地址没变则复用上一次的结果。举个实际例子。组件里有一个数组items你用纯管道对它做过滤p *ngForlet item of items | filterByStatus: active{{ item.name }}/p如果某次操作只是修改了数组元素的内容比如this.items[0].name 新名称数组的引用没有变化纯管道不会重新执行页面上的过滤结果依然是旧的。这个 “纯管道不生效” 的bug我刚开始用Angular时踩过好几次。那为什么Angular要这样设计为了性能。在每次变更检测中Angular都要检查大量表达式如果每个纯管道都重新计算一遍开销会非常可观。纯管道通过引用比较和结果缓存让绝大多数情况下管道都不用真正计算直接返回缓存结果。这是一种用内存换CPU的思路工程上很划算。4.2 非纯管道的正确使用姿势如果确实需要在输入对象的属性变化时重新执行管道可以把管道标记为非纯Pipe({ name: filterByStatus, pure: false }) export class FilterByStatusPipe implements PipeTransform { // 实现逻辑与纯管道相同 }非纯管道会在每一次变更检测周期都执行transform方法不管输入有没有变化。这意味着你不需要担心引用比较的问题但代价是性能开销显著上升。在一个频繁变更检测的大页面上用多个非纯管道页面很快就会卡顿。我的建议很明确优先使用纯管道只有当你明确知道需要监听对象内部变化时才考虑非纯管道并且要对性能开销有心理预期。如果数组内容会被频繁更新更好的做法是在set时生成一个新数组而不是直接修改原数组。这样既保留纯管道的性能优势又保证数据更新能被检测到。这也是为什么Angular社区一直强调不可变数据风格——配合纯管道变更检测天然就高效。非纯管道还有一类典型应用场景队列、缓存这类需要即时反映变化的数据结构。比如在某个状态管理中间件里维护一个事件队列每次有事件进来都希望界面上的计数立刻更新。这种场景用非纯管道很顺手。但注意非纯管道在模板中每次变更检测都会执行所以transform里的逻辑必须足够轻量绝对不能做复杂计算、网络请求或Operation级别的大对象深拷贝。4.3 性能优化什么时候该用管道什么时候该用函数很多开发者问过既然组件方法也能做数据转换为什么非得用管道直接{{ formatData(items) }}不就行了行但性能上有本质差异。组件方法在模板里被调用时每次变更检测都会执行一次Angular不做任何缓存。如果这个方法内部做了数组过滤、字符串拼接等消耗稍高的操作页面每次鼠标点击、数据轮询都会重复计算一遍。而纯管道因为有缓存机制输入没变就不会重复执行。在数据量大、变更频繁的场景下这个差距可能拉大到数十倍。但管道并不是万能的。如果转换逻辑特别简单比如{{ user.isVip ? 是 : 否 }}直接在模板里写三元表达式更直观没必要封装管道。如果是复杂的业务转换且依赖组件内部状态用getter方法反而更合适因为管道通常是纯函数不适合依赖外部可变状态。这里给一个我在项目中执行的选择标准。如果转换逻辑满足三个条件就封装成管道第一逻辑能被纯函数化不依赖组件内部状态第二在同一个模板中重复使用至少两次第三输入是引用类型或计算开销较大。三条同时满足管道带来的收益最大。5. Angular管道与Vue过滤器的对比换个角度理解管道5.1 从Vue转向Angular的人会容易混淆的几个点Angular和Vue是前端领域两个非常流行的框架。如果你是从Vue转到Angular的或者两个框架都在用容易被它们的数据转换机制搞混。先说结论Angular的管道和Vue 2的过滤器在模板语法上非常像都用管道符|但在Vue 3中过滤器已被移除官方推荐用计算属性或方法替代。我做一个对比表方便你理解对比项Angular管道Vue 2过滤器Vue 3方案模板语法{{ val | pipe: param }}{{ val | filter: param }}计算属性 / 方法定义方式Pipe装饰器类全局或组件内过滤器对象无对应概念参数传递冒号分隔多个参数冒号分隔多个参数函数参数缓存机制纯管道有结果缓存无专用缓存计算属性有缓存变更检测引用比较后决定是否执行每次渲染都会执行依赖响应式依赖收集可用位置模板插值表达式双花括号和v-bind模板表达式任意位置最容易混淆的是缓存机制。Vue 2的过滤器没有类似Angular纯管道的结果缓存它在组件每次重新渲染时都会执行。Angular的纯管道则依赖引用比较和缓存这意味着两者在性能特征上差异明显。如果你在Vue里习惯了过滤器随便写转到Angular后要注意纯管道的“惰性”特性否则会在数组变更不刷新时感到困惑。另一个区别是依赖注入。Angular管道本质是一个类所以它可以像组件、服务一样注入其他依赖。比如管道内部可以注入一个字典服务来做动态翻译或状态映射。而Vue 2的过滤器只是普通函数无法直接使用依赖注入要访问数据还得通过闭包或者全局状态。这个差异在大型项目中尤其明显Angular管道的“正规军”气质更强。5.2 两种方案在变更检测上的本质差异Angular的变更检测默认从上到下扫描组件树结合Zone.js自动触发。在这个机制下管道成了模板表达式的“计算缓存层”——纯管道帮你拦截掉了不必要的重复计算。Vue的变更检测则基于响应式依赖追踪模板中使用的数据变化时才会重新渲染组件。因此Vue天然“懒”只在依赖变化时执行不需要类似纯管道的缓存机制理论上更精确。但“精确”也有代价。Vue的过滤器和方法在每次渲染时都会重新执行如果你在方法里做了昂贵的计算又没能用计算属性缓存起来性能就会下降。Vue官方给出的解法是复杂计算用计算属性简单转换看情况用方法。Angular则把“是否缓存”这个问题交给了管道设计——纯管道默认缓存非纯管道每次执行开发者可以按需选择。实际工程中还有一个细微差异Angular模板里的管道名在构建时就被解析了如果你在运行时动态修改管道参数纯管道会在参数引用变化时重新执行Vue过滤器同理但在computed里修改依赖则更灵活。总体来说从Vue转向Angular时把原来“写过滤器”的思路改成“设计管道类”反而能倒逼你把数据转换逻辑梳理得更清晰。因为管道类有明确的输入类型、输出类型和私有方法比散落的过滤器对象更利于测试和维护。6. Angular面试题管道相关高频考点拆解6.1 面试官喜欢问的六个管道问题Angular面试中管道是出现频率相当高的基础知识板块。我结合自己和朋友们的面试经历把最高频的六个问题整理出来每个问题都给出一个能直接用的回答思路。第一个问题Angular中的管道是什么有什么用回答要点管道是模板中用于数据转换的机制通过Pipe装饰器定义实现PipeTransform接口。主要用于数据格式化、过滤、排序等展示层的转换逻辑把数据处理从组件逻辑中分离出来提高复用性和模板可读性。第二个问题纯管道和非纯管道的区别回答要点纯管道只在输入值引用变化时执行transform有结果缓存性能好非纯管道在每次变更检测都执行性能开销大但是能检测到对象内部变化。默认是纯管道通过Pipe({ pure: false })设置为非纯。第三个问题如何创建一个自定义管道回答要点创建一个类实现PipeTransform接口加Pipe({ name: 管道名 })装饰器在模块或组件中声明并导出。模板中用{{ data | 管道名: 参数 }}调用。第四个问题AsyncPipe 的作用是什么回答要点自动订阅Observable或Promise在数据到达时触发视图更新组件销毁时自动取消订阅避免内存泄漏。同时支持as语法提取结果避免重复订阅。第五个问题纯管道为什么会被缓存回答要点因为Angular在变更检测中维护了管道输入和输出的缓存输入引用没变时直接返回缓存结果。这是Angular为了性能而设计的机制但代价是对象内部属性变化无法触发重新计算。第六个问题管道和组件方法都能做数据转换为什么选管道回答要点管道有缓存机制不会在每次变更检测时重复执行管道是声明式用法模板更简洁管道可链式调用、可复用、可注入依赖。组件方法则容易在模板里造成不必要的重复计算。这六个问题回答好了管道这块基本就能过关。但我建议你回答时多带一个实际案例比如讲讲你写过的某个自定义管道以及为什么用纯管道而不用非纯管道。面试官很吃这一套因为这说明你不是背答案是真的做过。6.2 容易混淆概念的辨析面试中还有几个高频迷惑点我单独拎出来说一说。async管道到底在哪一层订阅有的候选人会说“AsyncPipe是在组件构造函数里订阅的”这是错的。AsyncPipe的订阅发生在视图绑定期间是在管道实例创建时进行的组件销毁时管道会随视图销毁并触发退订。关键在于它不依赖组件的生命周期钩子而是Angular视图机制自动管理。管道返回Observable可以吗可以但是要注意管道返回一个Observable时模板拿到的仍是Observable对象而不是它释放的数据除非你在这个管道后面再链一个async管道。比如p{{ data$ | someTransformPipe | async }}/p这里someTransformPipe把数据变成一个新的Observable然后async负责订阅。这种链式异步转换在复杂业务中偶尔会用到但不要滥用不然排查数据流会很难受。一个管道可以同时定义多个名字吗不可以。Pipe装饰器的name属性只能是字符串或字符串数组Angular官方目前只支持一个名字。如果一个管道包装了多个转换逻辑建议拆成多个管道类再加一篇管道链。可以定义无参管道吗可以这种情况很常见比如| uppercase就不需要额外参数。但注意Angular在模板里调用管道时仍然至少传入一个参数那就是数据源本身。所以无参管道的transform(value)至少有一个value参数。7. 实战排查管道不生效的五个常见原因7.1 管道失效场景与排查思路用管道时间长了你会遇到一些“看上去完全没问题但就是不生效”的诡异场景。我把常见的五种原因整理成一个排查清单遇到问题直接对号入座。场景一数组内容变了纯管道不重新执行。这是最常见的一种。原因在第4章讲过纯管道用引用比较判断输入变化直接修改数组元素不会改变数组引用。解决方法是更新数组时生成新数组this.items [...this.items, newItem];或者用filter、map等返回新数组的方法。场景二模板中管道和属性绑定顺序错了。比如[innerHTML]content | safeHtml和{{ content | safeHtml }}是不同的。innerHTML属性绑定需要管道返回安全的SafeHtml对象否则Angular会抛unsafe value错误。排查时先确认管道返回类型和绑定方式的匹配关系。场景三管道没有在正确的模块中声明导出。在使用NgModule模式时某个模块的模板用另一个模块的管道必须在当前模块的imports中引入管道所在模块或直接declarations声明该管道并exports。如果模板中用了管道但没声明Angular会直接抛 “The pipe xxx could not be found” 错误。独立组件模式下也要确认管道类放进了组件imports数组。场景四管道的name和模板中的名称不一致。这个错误最常见的原因是大写不一致。管道名通常用小驼峰命名模板里也完全一致稍有出入就找不到管道。Angular的管道名是区分大小写的| PhoneMask和| phoneMask是两码事。场景五Transform 方法内部抛异常。很多初学者写管道时没有做空值判断数据一旦为null就直接炸。这种情况页面上表现为该区域渲染失败或整页白屏控制台报错信息一般会指向管道类内部。排查时先看控制台再检查transform方法开头是否做了守卫。7.2 我踩过的几个坑以及现在的应对方式管道相关的坑我踩得不算少挑几个印象深刻的讲。我很久之前在写一个物流订单查询页需要在表格里展示订单状态的中文描述。我写了一个状态管道传入订单对象内部根据order.status取值映射。理论上没问题但用户反馈“状态显示不及时改了之后要刷新才变了”。排查了一下午才发现因为整个页面用的状态都是同一个对象引用status字段改的是对象内部属性纯管道根本不会重新执行。最后我把管道改成接收order.status字符串而不是整个order对象问题立刻解决。这个经验后来被我总结成一条团队规范管道输入尽量传最小必要值既利于缓存判断又能避免多余的对象依赖。还有个坑是管道名称和项目里某个全局函数重名当时团队有人在模板里写了一个管道却忘记在模块中声明Angular在编译期会试图解析成模板中的全局方法结果运行时直接报“找不到标识符”。排查了半天才意识到是模块导出配置漏了。从那以后我要求团队里所有自定义管道创建后第一件事就是检查模块注册并且用CLI命令自动生成减少手写出错。遇到管道问题不要慌遵循这个顺序排查会快很多先看控制台报错信息再确认模板管道名是否正确接着检查模块声明最后看transform方法内部逻辑和输入值变化机制。7.3 管道设计的三条经验心得经验这东西只有自己踩过坑才能有体感。分享三条我目前仍在遵守的管道设计原则。第一管道类内部保持绝对纯净。不要在管道里调用服务修改全局状态也不要发起HTTP请求。管道的职责是“输入到输出的转换”任何副作用都应避免。我见过同事在管道里调router.navigate最后调试时完全摸不着头脑这种代码一定要杜绝。第二管道命名带上业务前缀。项目大了以后团队里几十个自定义管道没有统一命名规范会非常混乱。我目前用的前缀规则是app 功能 类型比如appStatusText、appPhoneMask然后在管道类名里也用AppStatusTextPipe这种命名。这样别人看代码一眼就知道这个管道是哪个业务域的。第三重要的管道一定要写单元测试。管道是纯函数非常适合写单元测试。我通常会在*.pipe.spec.ts里覆盖正常输入、空值、异常值、边界值几种情况。一个管道类通常半小时就能测试完但它为后续重构提供的保障非常大。尤其是涉及日期、脱敏这类敏感逻辑有测试兜底心里才踏实。