一位开发者给项目新建了一个独立于其他业务域的功能模块,按团队此前约定的方式写代码,结果卡在了一个问题上:解构 props 会让值失去响应式。代码大致是这样写的:const props = defineProps(),随后调用 getProduct(apiClient, key)。他回头来问:这该怎么办?

打开网易新闻 查看精彩图片

问题本身不复杂,但解法不止一种,团队为此盘了三套方案,最后选了第三套。整个过程值得复盘一遍,因为它牵涉的不只是一个语法细节,还有模块之间依赖该怎么传的架构判断。

三套备选方案

第一套是用 Vue 提供的 toValue 辅助函数。这个办法能起作用,但团队只把它当临时手段——它更像是把 eslint 报错“藏”起来,而不是真正解决问题,所以没打算作为最终选项。

第二套是把 prop 包进 computed 或 watch 里,比如写成 watch(() => props.apiClient, () => { console.log(props.apiClient) }) 这样的形式。团队同样不喜欢这个方案,原因很直接:一旦这么写,就等于默认 apiClient 是可能变化的,而在他们的场景里,apiClient 是一个静态依赖。

第三套是改用 provide/inject 传递 apiClient。这个模式既能提供响应式的值,也能提供静态的值,适用范围比前两种更宽。

为什么选了 provide/inject

团队最终选定第三套,理由是它从架构角度看最合适。他们手上有一个静态的 apiClient,希望把它提供给应用里所有消费者,让它们在需要的那一刻自行注入。这同时缓解了 prop drilling 的问题——不必再沿着嵌套的组件树一层层往下传 prop。

代价也有。采用这个方案后,各个包对外暴露的接口变得不那么直观,而这恰恰是团队最初想达成的目标之一。换句话说,架构上更干净了,可外部可读性做了让步。

三套方案摆在一起看,取舍逻辑其实很清楚:toValue 治标不治本,computed/watch 建立在一个不成立的前提上——静态依赖不会变,provide/inject 则把“谁提供、谁消费”这件事显式化了,代价是接口可见性下降。对于独立于其他业务域的功能模块来说,这个交换是否划算,取决于团队更在意依赖注入的整洁,还是包边界的一目了然。

如果你在 Vue 项目里也遇到过解构 props 丢响应式的情况,不妨先问一句:这个 prop 到底是会变的,还是静态的?答案不同,选的路就不同。