最近带的一个模拟项目X刚好结束,这是一个用Flutter从零到一完成的连贯实战App。说“连贯”这两个字,是因为我见过太多开发者单个Widget、单个页面写得很溜,但一提到把登录、首页、详情、收藏这几条链路串起来就卡壳了。这个项目的核心目标很明确:不做零散的demo堆砌,而是用一个完整的产品逻辑,把Flutter日常开发中最常碰到的技术点全部串起来,让读者跟着做一遍,就能理解“一个真实的App是怎么长出来的”。
项目本身并不复杂,是一个带登录、信息流、详情、收藏和搜索的内容型应用。选这个方向是有讲究的,后面我会详细说。整篇会从项目设计思路、模块拆解、核心闭环实现、到性能优化和踩坑记录逐一展开。不管你是刚学完Flutter基础的新手,还是想找一套完整项目练手的中级开发者,这篇内容都值得你从头到尾过一遍。
1. 项目整体设计与思路拆解
1.1 为什么选“内容型App”作为实战载体
实战项目的选题直接决定你能练到哪些技术点。我见过有人拿仿计算器、仿TodoList来做“完整项目”,但做完之后发现还是不会做真实业务。核心原因在于:TodoList和计算器缺少了真实App中最关键的那几样东西——网络请求、异步状态、跨页面数据同步、登录态管理、列表性能优化。
内容型App几乎把这些点一网打尽:
- 首页信息流要做网络请求、列表加载、下拉刷新、上拉分页;
- 详情页要处理路由传参、页面跳转、WebView混用;
- 登录注册要处理表单校验、token存储和管理;
- 收藏和搜索要处理本地存储、跨页面状态刷新;
- 用户中心要处理头像展示、退出登录、状态复位。
一个内容型App做下来,Flutter开发中90%的高频场景都会遇到。而且内容型App对后端依赖相对简单,甚至可以用Mock数据模拟接口,不要求读者自己搭一套完整后端,降低了门槛。
1.2 技术选型背后的考量
这个项目的技术栈我反复权衡过,最终确定如下:
- 状态管理:Provider(配合ChangeNotifier)
- 路由:Navigator 2.0(基于go_router或者纯Navigator 1.0均可,本项目用的是官方Navigator + 命名路由)
- 网络:Dio + 拦截器
- 本地存储:shared_preferences
- 图片加载:CachedNetworkImage
- 下拉刷新/加载更多:RefreshIndicator + 自封装分页组件
先说状态管理。选Provider而不是Bloc或Riverpod,不是因为Provider功能更强,而是因为它的心智负担最小。作为实战项目,重点应该放在业务实现和数据流动上,而不是让状态管理框架本身成为主角。Provider的ChangeNotifier机制足够清晰——数据变了,调notifyListeners,UI自动刷新,这个模型非常直观。而且Provider到Riverpod的迁移成本很低,后续想升级也不难。
路由这一块,我在实际项目里用的是命名路由加路由表统一管理。对于连贯项目来说,路由管理最重要的是清晰可控,命名路由比直接Navigator.push更利于维护,尤其在需要登录拦截和统一跳转逻辑的场景下。
网络层必须用Dio,没有悬念。拦截器是Dio的王牌功能,token注入、日志打印、统一错误处理全部在拦截器里完成,业务代码里只写具体请求逻辑。
1.3 项目目录结构的规划
目录结构直接决定项目后期能不能维护。很多新手项目写到一半就乱,问题都出在一开始没规划好分层。
这是我用下来的标准分层,也是这个项目的最终结构:
lib/ ├── main.dart # 入口,初始化Provider与路由表 ├── models/ # 数据模型,JSON解析 ├── services/ # 网络请求层,Dio封装 ├── providers/ # 全局状态,登录态、收藏状态 ├── pages/ │ ├── login/ # 登录注册页 │ ├── home/ # 首页/信息流 │ ├── detail/ # 详情页 │ ├── search/ # 搜索页 │ └── profile/ # 个人中心 ├── widgets/ # 通用组件,如加载占位、空状态 └── utils/ # 工具类,token存储、日期格式化分层逻辑的核心是单向依赖:pages依赖providers,providers依赖services,services依赖models。下层不感知上层存在。这样做最大的好处是,后续改UI不影响数据层,换后端接口只动services,排查问题的时候能快速定位到具体层。
很多教程项目都是把请求写在Widget里,状态也写在Widget里,页面之间互相传数据,写到后面就是一坨。连贯项目的核心就是把这些依赖关系理顺,让数据流动有明确的方向。
2. 核心功能模块的细节拆解与实操要点
2.1 登录注册模块:表单校验与token管理
登录注册是几乎所有App的第一道门槛,也是“连贯”的起点。这个项目里登录页承担的任务不只是登录本身,还包括表单校验、loading状态管理、错误提示以及登录成功后用户信息的全局写入。
表单这块,我直接用了TextFormField配合validator实现。需要注意的一点是,validator的返回值是String?,返回null表示校验通过。实际开发里很多新手在这里栽跟头——校验失败返回的是错误文案字符串,但忘记return null,导致输入正确时表单也永远校验不通过。
密码校验的规则我用了正则,但这里有个细节:不要在前端限制密码格式过死。很多App要求密码必须包含大写字母、小写字母、数字、特殊字符,结果用户想一个符合规则的密码都费劲。实战项目里密码规则设为6-20位即可,重点训练的是校验逻辑的写法,而不是规则本身的复杂度。
token管理是这个模块的重头戏。登录成功后后端返回token和用户信息,token存到shared_preferences里,用户信息写入全局Provider。要特别说明的是,token不要存在内存变量里就完事,因为App冷启动后需要恢复登录态,必须持久化。本项目里我封装了一个TokenStorage工具类,专门负责token的读写和清除,代码大概是这样的:
class TokenStorage { static const _key = 'auth_token'; static Future<void> save(String token) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_key, token); } static Future<String?> read() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString(_key); } static Future<void> clear() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_key); } }登录请求发出后,不能让用户傻等着,要显示loading状态。这里用的是Provider里的isLoading字段,请求前置为true,请求完成后置为false。常见的坑是:请求失败时忘记把loading复位,导致页面一直卡在加载中。所以一定要用try-finally或者try-catch-finally把状态复位逻辑包住。
2.2 首页信息流:分页加载与图片缓存策略
首页信息流的实现是这个项目里最值得反复琢磨的部分。一个性能良好的列表页,不只是把ListView.builder写出来那么简单,分页逻辑、缓存策略、占位图、加载状态都要处理。
分页加载我用的是常规的page/pageSize模型:下拉刷新重置到第一页,上拉加载追加下一页。服务端返回的JSON结构通常是这样的:
{ "code": 0, "data": { "list": [...], "hasMore": true, "total": 100 } }关键在hasMore字段。如果没有这个字段,前端就没法判断是否还有下一页,上拉加载会做很多无效请求。如果接口没返回,也要根据“返回条数小于pageSize”来推断到底还有没有更多,这是行业内通用的兜底逻辑。
列表性能方面,有几个容易忽略的细节是真正影响体验的:
- ListView.builder的itemBuilder里不要做耗时操作,比如JSON解析、图片处理;
- 图片组件必须用CachedNetworkImage,不要直接用Image.network,后者没有任何缓存机制,每次滑回来都重新加载;
- 列表item的widget树不要太深,嵌套层级每多一层,滚动性能就降一截;
- 指定itemExtent(item高度固定时)能显著提升滚动性能,ListView不再需要动态测量每个item高度。
实际做下来,首页列表的图片加载是关键瓶颈。CachedNetworkImage的placeholder和errorWidget必须要设,否则弱网或者断网状态下,列表会显示一大片空白或者破图图标,体验非常差。
另外,列表空状态很多人会忘。接口正常返回但是list为空时,画面上就是一个空白页,用户根本不知道发生了什么。要加一个空状态组件,提示“暂无内容”,并提供重新加载的按钮。这个细节在实战项目里是考核完整度的重要指标。
2.3 详情页:路由传参与跨页面状态变化
详情页是这个连贯项目里承上启下的关键一环。首页列表点击某个item,携带该条内容的id跳转到详情页,这是数据流的第一次传递。
命名路由传参这里有个典型坑:Navigator.pushNamed直接传Map参数,目标页面用ModalRoute.of(context)!.settings.arguments接收。这种写法在简单场景没问题,但参数是Object类型,需要强转,一旦类型不对直接崩。更稳妥的方式是定义明确的参数类:
class DetailArgs { final String id; final String title; // 用于列表到详情转场过程先展示标题 DetailArgs({required this.id, required this.title}); }跳转时:
Navigator.pushNamed(context, '/detail', arguments: DetailArgs(id: item.id, title: item.title));详情页接收时强转成DetailArgs,类型安全,IDE也能自动提示字段。
详情页另一个核心功能是收藏按钮。用户点击收藏,需要把这个状态同步到全局状态里,这样返回列表页、进入收藏页时,数据才能保持一致。这个场景正是状态管理发挥作用的地方——收藏状态放在全局Provider里,而不是某个页面的局部State里。
在实现时,收藏Provider内部维护一个Map<String, bool>,key是内容id,value是收藏状态,同时维护一个收藏列表用于收藏页展示。调用收藏接口成功后更新Map并notifyListeners,所有监听这个Provider的页面自动刷新。
2.4 搜索与本地缓存:日常功能如何做得顺手
搜索功能看着简单,做起来细节很多。我在项目里做了一个关键词本地搜索:用户输入关键词,前端从已加载的内容列表里做模糊匹配,同时把搜索历史存到本地。
搜索历史这里有个交互细节很容易被忽略——历史记录的去重和排序。同一个关键词搜索两次,不应该在历史列表里出现两条,而是把最新一次的时间顶到最前面。这个逻辑用List去重虽然也能做,但更高效的做法是用一个有序Map,key是关键词,value是时间戳,每次搜索时先remove再重新put,自然就实现了“去重+置顶”。
输入框的防抖也是搜索功能的核心。如果用户每输入一个字就触发一次搜索请求,既浪费资源又导致界面闪烁。我封装了一个debounce工具类,利用Timer实现300毫秒的延迟触发:
class Debouncer { Timer? _timer; void run(VoidCallback action, {int milliseconds = 300}) { _timer?.cancel(); _timer = Timer(Duration(milliseconds: milliseconds), action); } }本地搜索只是过渡方案。真实业务里的搜索一般要走服务端接口,但本地搜索的价值在于,它让你理解了“搜索”这个交互模式本身——输入、防抖、触发、结果展示、状态处理,这一套逻辑在不同场景下是通用的。把本地版搞明白了,接服务端搜索只是换个数据源的事。
3. 连贯项目的核心闭环:登录态到业务数据的完整链路
3.1 token注入与请求鉴权的标准姿势
“连贯”在这个项目里最典型的表现,就是登录态如何影响到业务数据请求。整个链路是:
- 用户登录成功,拿到token;
- token写入TokenStorage;
- 后续每次请求,Dio拦截器自动在请求头里带上token;
- 后端校验token,决定返回数据还是401;
- 401时拦截器统一处理——清除本地token、跳转登录页。
这个链路的关键实现,在于Dio拦截器。我把项目里的拦截器写成了三段式:
dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { final token = await TokenStorage.read(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } return handler.next(options); }, onResponse: (response, handler) { // 统一处理业务码,比如code!=0时抛出异常 return handler.next(response); }, onError: (DioException e, handler) async { if (e.response?.statusCode == 401) { // 清除登录态,跳转登录页 await TokenStorage.clear(); // 跳转逻辑 } return handler.next(e); }, ));这里面有几个实战中容易踩的坑:
第一,onRequest里不能直接同步读SharedPreferences——它是异步的。在Flutter里获取SharedPreferences实例本身就是异步操作,所以拦截器里必须用await,这意味着onRequest回调要标记async,而且handler.next要在await之后调用。顺序搞反了的话,请求发出去时token还没读到,永远带不上认证头。
第二,401处理不只是清除token那么简单。如果当前就在登录页,跳转逻辑要避免重复跳转;如果登录页已经压入导航栈,就不应该再push一个新的。这种“重复入栈”问题在实战里非常常见。
第三,业务状态码和后端HTTP状态码要区分开。很多后端设计是无论业务成功与否HTTP都是200,只在body里用code标识业务状态。这种情况下,onResponse里要根据code判断是走成功流程还是错误流程,Dio默认只会把HTTP非200当错误,业务码错误要自己处理。
3.2 登录与数据的联动:登录前后状态怎么切换
项目里有一个很能体现“连贯”精髓的场景:未登录时,首页信息流展示的是默认推荐内容;登录之后,根据用户的兴趣标签重新拉取个性化内容;退出登录后,又回到默认推荐。
这个场景如果用局部State写,会非常痛苦——因为首页列表数据、个人中心UI、收藏列表、请求头里的token,四处都要跟着登录态变。但用Provider后,逻辑变得非常清晰:
- UserProvider管理登录状态(登录中/未登录/用户信息);
- 首页数据Provider监听UserProvider的登录态变化,登录态变化时自动重新拉取对应接口;
- 收藏Provider在登录后自动加载当前用户的收藏列表,退出后清空本地收藏。
实现方式是让HomeProvider在构造时接收UserProvider的引用,并在UserProvider的addListener里注册自己的刷新回调。这样登录和退出时,首页数据自动重置并重新请求,完全不需要页面代码手动干预。
这种“状态驱动数据刷新”的思路,是Flutter状态管理区别于传统Android/Java开发的核心优势之一,需要在实战项目里反复练习才能真正掌握。很多人在这个项目里学到的不是某个API怎么用,而是这种数据驱动的思维模式。
3.3 收藏状态的跨页面同步闭环
收藏状态的同步是这个项目里最直观的“连贯”案例。整个链路是:
- 详情页点收藏,收藏Provider更新Map并notifyListeners;
- 返回列表页,列表页对应item的收藏图标自动更新;
- 进入收藏页,收藏列表自动刷新,展示最新的收藏数据;
- 在收藏页取消收藏,详情页再次进入时状态同样保持同步。
这个闭环实现起来代码量并不大,核心就是所有页面共享同一个收藏Provider实例。但有个隐藏问题必须处理:ListView.builder的item复用机制。列表滚动出屏幕的item会被回收复用,如果item里的收藏图标是直接读Provider状态拼出来的,容易出现icon状态闪烁或者错乱。
解决办法有两个方向:一是给列表item传入当前id,在build时通过Provider.read(favoriteProvider).isFavorite(id)来获取收藏状态,而不是用Provider.of监听整个列表刷新;二是Item本身用Consumer组件包一层,只监听这个item对应id的数据变化。
我在项目里用的是方案一加方案二结合:列表Item的icon区域用Consumer监听收藏Provider,但只响应自己id对应的状态变化。这个细节能够体现出对Flutter刷新机制的理解深度,值得每个做实战项目的人认真研究。
4. 性能优化、踩坑记录与问题排查实录
4.1 实测常见的Flutter运行时错误与修复
连贯项目最难的地方不是正向写功能,而是踩坑之后怎么排查。我梳理了在这个项目开发过程中遇到的高频问题,给它们做了个速查表:
| 报错或问题 | 出现场景 | 根因分析 | 解决办法 |
|---|---|---|---|
| setState() or markNeedsBuild() called during build | 列表页数据懒加载过程中调用notifyListeners | 在build阶段直接触发状态变更 | 用Future.microtask包裹状态更新,或把加载逻辑移到postFrameCallback |
| Could not find the correct Provider | 跨页面拿Provider时context找错层级 | Provider没有放在目标页面之上 | 检查MaterialApp是否被MultiProvider包裹,页面context是否在Provider之下 |
| Null check operator used on a null value | 详情页接收路由参数强转失败 | 参数类型不匹配或强转前未判空 | 使用as加上类型判断,或定义Args类做安全解析 |
| DioException: Connection timed out | 弱网环境请求超时 | 未设置超时时间或过长 | 设置connectTimeout和receiveTimeout,区分对待 |
| ScrollController not attached to any scroll views | 列表页切换Tab后滚动位置丢失 | 多个Tab共用一个ScrollController | 每个Tab独立State和Controller,或在Tab切换时重新attach |
最恶心的是第一种——build期间触发状态更新。现象是页面正常加载,但控制台疯狂打印那串报错,甚至某些Android机型上直接卡死。这个问题的根因是:列表Item里某个Consumer在build过程中,检查到数据为空,就异步去请求数据,请求回来后立刻notifyListeners,此时某个正在build的Widget监听了这个状态,就触发了非法刷新。
解决方式有两种。最简单的是把数据请求从item的build阶段挪到页面的initState里,用统一的加载逻辑处理。另一种是使用Future.microtask,把notifyListeners延迟到当前build完成之后再触发。这两种方案都要掌握,因为现实业务里总有些场景数据就是由某个子Widget自己加载的。
4.2 列表性能实测:从卡顿到流畅的调优过程
项目初期,首页信息流在低端Android机上滑动有明显掉帧。用Flutter自带的Profile模式跑了一遍,发现主要卡点在图片解码和item重建。
优化动作分三步走:
第一步,替换图片组件。把所有Image.network统一替换为CachedNetworkImage,并设置cacheWidth参数。这个参数很多人不知道——它会让图片在解码时直接产生对应宽度的位图,而不是加载原图再缩放。色图可能占用几MB内存,但cacheWidth设置后可能只需要几百KB,内存占用直接降一个量级。
第二步,给ListView.builder设置itemExtent。我实测过,不设置itemExtent时,滑动过程中CPU占用明显偏高。设置了固定高度后,Flutter省去了动态测量item高度的过程,滑动帧率提升非常明显。
第三步,把列表item的Widget树精简。原实现里图片、标题、标签、时间、收藏按钮全部嵌套在多层Container和Padding里,优化时把能用轻量组件的换掉,能用单一组件处理的就不用容器包裹,widget总数压掉了近三分之一。
这三步做完,低端机上的滑动帧率从肉眼可见的掉帧恢复到了流畅水平。调优的关键是要有数据支撑,不要凭感觉瞎优化。Flutter的PerformanceOverlay和DevTools的Timeline都是必备工具。
4.3 登录态与导航栈的联动:几个隐蔽Bug复盘
这个项目里最让我印象深刻的Bug是登录跳转的重复入栈问题。场景是这样的:用户访问个人中心,系统发现未登录,跳转登录页;用户登录成功后回到个人中心,顺手退出了登录;接着再点个人中心,系统又跳登录页——到这里都正常,但导航栈里居然叠加了两个登录页实例。
原因是退出登录时,登录页已经从栈里移除,但由于个人中心页监听的登录态变化包含了跳转逻辑,退出登录触发了这个监听,于是又跳了一次登录页。最后栈里就出现了新旧两个登录页。
修复方案是在跳转前检查:当前栈顶是不是LoginPage。如果已经在登录页,就不做任何跳转。更通用的思路是,定义一个全局导航工具类,里面封装一个ensureLogin方法:
class NavigationUtil { static Future<void> ensureLogin(BuildContext context) async { final isLoggedIn = UserProvider.of(context).isLoggedIn; if (isLoggedIn) return; final isTopLogin = ModalRoute.of(context)?.settings.name == '/login'; if (!isTopLogin) { await Navigator.pushNamed(context, '/login'); } } }这个Bug排查花了我半天时间,但复盘下来收获很大。导航栈的管理远比想象中复杂,尤其涉及登录态变化时,每个跳转都要思考“如果已经在目标页怎么办”和“如果当前栈状态异常会怎样”。连贯项目的价值就在这里——不是教你写一个登录页,而是让你理解登录页在真实App里如何被各种页面反复触发。
还有另一个隐蔽问题是收藏状态在退出登录后的残留。用户用A账号收藏了一批内容,退出登录再切换B账号,收藏列表居然是A的。这个Bug的根源是退出登录只清了token,没清收藏Provider。修复方案是在退出登录流程里加一个resetAll方法,把全局状态全部恢复初始值。这个“退出即复位”的思路在实战项目里非常重要。
4.4 通用排查方法论:面对诡异Bug的思路
做连贯项目的过程中,我总结了一套排查问题的通用方法。不管遇到什么Bug,按这个顺序走基本都能定位:
第一,复现并最小化。想办法稳定复现问题,然后逐步缩小范围——关掉并行请求、去掉复杂动画、删掉非相关页面,直到最小复现路径清晰。很多Bug在最小化的过程中自己就能发现原因。
第二,看日志定位链路。Flutter的debugPrint输出是关键,加上Dio的日志拦截器,能直观看到某次操作触发了哪些请求、请求参数是什么、响应是什么。大多数问题在看日志时就已经有眉目了。
第三,区分数据层还是UI层。如果界面显示不对,先确认数据是否正确。在UI里临时打印请求结果,或者用DevTools查看Provider状态树。如果数据是对的,那就是渲染层问题;如果数据本身不对,那是数据层问题。
第四,回滚验证。修改代码后问题解决,但不确定有没有副作用时,先把改动回滚,确认问题是否复现。如果复现了,再重新应用修改,如此往复确认根因和修复的关系。这能避免瞎改碰巧修好的情况。
这套方法论比具体某个API的解法更值钱。实战项目的意义就在于,你会遇到各种文档上查不到的问题,而解决问题的思路和耐心,才是开发者真正的核心竞争力。
5. 工具链与调试技巧
5.1 调试中断点与日志的正确姿势
Flutter调试很多人只会用print。print在练习demo的时候没问题,但在连贯项目里,print打出来的信息难以过滤,关键日志混在大量输出里根本看不清。
我的建议是项目从一开始就引入日志分级机制,封装一个Logger工具类:
class Logger { static bool enable = true; static void d(String tag, String message) { if (enable) debugPrint('[DEBUG][$tag] $message'); } static void e(String tag, String message) { if (enable) debugPrint('[ERROR][$tag] $message'); } }生产包构建时把enable设为false,一行代码关闭所有日志输出。这种方法能让你在调试时保持信息清晰。
断点调试也是必备技能。很多新手只会在入口处打断点,然后一路Step Over,走到哪里算哪里。更高效的做法是:在可疑方法上打断点,利用Condition来过滤命中条件。比如列表刷新方法只有在page=2时才需要检查,就设置条件表达式为page == 2,这样断点会被精准命中,不用每次刷新列表都停下。
5.2 网络调试:抓包与Mock的切换技巧
连贯项目里网络层是最容易出问题的环节。调试网络请求时,我常用两种手段:
第一种,Dio日志拦截器。设置LogInterceptor的request和responseBody输出,能直接看到请求URL、请求头、请求体、响应体。日常调试基本够用。
第二种,抓包工具。Android上开启明文流量后,用抓包工具查看真实请求信息,尤其适合排查加密参数、重定向和证书相关的疑难杂症。
Mock数据的切换也要从一开始就设计好。我在services层定义了一个Bool配置,控制在Mock数据源和真实网络源之间切换。Mock模式返回的是本地构造的假数据,让项目在完全没有后端的情况下也能完整跑通。等以后有真实接口了,直接切到网络模式,业务代码一行不用改。这个设计对学习者和团队协作都有价值——设计好数据源的抽象层,未来接任何后端都方便。
5.3 模拟弱网与错误响应
实网请求需要后端配合才能测,但弱网和异常情况其实完全可以自己模拟。Dio在模拟层就支持自定义适配器,我在项目里封装了一个FakeAdapter,专门模拟各种网络场景:
- 模拟5秒延迟的弱网,测试loading状态和超时逻辑;
- 模拟404,测试错误提示;
- 模拟401,测试登录态失效后自动跳转逻辑;
- 模拟返回空列表,测试空状态UI;
- 模拟服务端异常code,测试业务错误分支。
用这种方式,不依赖真实后端,就能把网络层所有分支都测到。这些分支在真实开发中往往要到上线前的联调阶段才会暴露,提前测一遍能省下大量时间。
6. 项目复盘与扩展思路
做完整套项目后回头看,你最应该沉淀下来的不是代码,而是设计的思路。什么时候该抽Provider、什么时候该用局部State、路由应该怎么组织、数据流应该怎么走、异常要处理到哪一层,这些判断力才是实战项目的核心收获。
上面提到的很多代码细节,比如防抖、token持久化、拦截器、列表优化,任何单拎出来都能写一篇小文章,但只有把它们放进一个连贯项目里,你才能真正理解它们之间的协作关系。
最后分享一个我实际带项目的小技巧:不要只看完就过去,试着在项目基础上自己加一个功能模块。比如给这个App加一个“历史浏览记录”功能,方案是:列表页点击item时,把内容id和浏览时间写入本地;然后在个人中心加一个记录列表页面。这个功能涉及路由传参、本地存储、Provider状态、UI展示,全部要在自身已有的架构上扩展。做完这个扩展,你对整套架构的掌握会比单纯照抄深得多。
技术这行永远是“一看就会,一做就废”,连贯项目的作用就是把这个“废”的阶段留在可控范围内。在项目里踩坑、调优、复盘,整个过程走一遍,后面独立接手业务需求的时候,心里才算真正有底。