普通视图
央行公开市场开展5亿元7天期逆回购操作
恒指开盘涨0.17%,恒生科技指数涨0.34%
人民币兑美元中间价报6.8648,下调26点
Flutist - Flutter 模块化架构管理框架
简单而系统地管理你的 Flutter 模块化结构
简介
随着我们的项目增长,模块化似乎势在必行,而我最近发现了一个比较新的Flutter模块化管理框架——Flutist。它是一个专为 Flutter 应用设计的强大项目管理框架,灵感来源于 iOS 开发生态中的 Tuist[1]。它为管理大型 Flutter 项目提供了一套结构化的方法,具备模块化架构、集中式依赖管理和代码生成能力。
为什么选择 Flutist?
模块化是大型 Flutter 项目的标准方案——独立构建、并行开发、测试隔离,优势显而易见。但随着模块数量增长,管理开销也随之攀升。Flutist 通过自动化消除了这些开销。
类型安全的依赖管理
当模块超过 10 个时,包版本不一致的问题极易出现。在 package.dart 中声明一次版本,flutist generate 会自动生成 flutist_gen.dart,所有模块都能通过 IDE 自动补全和类型检查安全地引用依赖。
// 1. package.dart — 只声明一次版本
Dependency(name: 'dio', version: '^5.3.0'),
Dependency(name: 'flutter_bloc', version: '^8.1.6'),
// 2. flutist_gen.dart — 由 flutist generate 自动生成
Dependency get dio => dependencies.firstWhere((d) => d.name == 'dio');
Dependency get flutterBloc => dependencies.firstWhere((d) => d.name == 'flutter_bloc');
Module get authDomain => modules.firstWhere((m) => m.name == 'auth_domain');
// 3. project.dart — 类型安全引用(IDE 自动补全 ✅)
Module(
name: 'auth_data',
dependencies: [package.dependencies.dio, package.dependencies.flutterBloc],
modules: [package.modules.authDomain],
),
集中式 pubspec.yaml 管理
每增加一个模块就多一个 pubspec.yaml。升级一个包的版本意味着要手动编辑每个引用它的文件。
Flutist 根据 project.dart 的声明自动同步所有 pubspec.yaml 文件。开发者只需编辑一个文件——project.dart。
$ flutist generate
✓ pubspec.yaml synced: app, auth_domain, auth_data, auth_presentation,
product_interface, product_implementation ... (24 total)
✓ all architecture rules passed
✓ done (0.8s)
架构规则自动化
仅靠文档和代码审查很难持续保持架构规则的一致性。在开发压力下,domain 层最终会导入 http,或者一个功能模块直接引用了另一个功能模块的实现。这些违规很难被发现,等到发现时往往已经扩散开来。
Flutist 将架构规则转化为可执行代码。在 strictMode: true(默认值)下,任何违规都会立即终止 generate。曾经只存在于文档中的原则,现在变成了构建关卡。
$ flutist generate
✗ [B4] auth_domain → auth_data: 检测到反向依赖 — domain 不应依赖 data
→ 请从 auth_domain 中移除 Dio 导入,仅声明 Repository 接口
✗ generate 已终止(strictMode: true)
即使是刚加入团队、对架构理解不深的新成员,在违反规则的那一刻也能获得清晰的反馈。架构违规不再依赖人工审查,工具会自动检查。
样板代码自动生成
模块化架构最大的痛点之一就是样板代码。每个新功能都需要创建 interface、implementation、testing、tests 和 example 包,每个包都有自己的 pubspec.yaml、lib/ 结构和 barrel 文件。
像 BLoC 这样的状态管理模式,每个功能都需要 event、state、BLoC、page 和 widget 文件。使用 flutist create 和 flutist scaffold,一条命令就能生成所有这些内容。
# 以 micro 类型创建 todos 功能 — 5 个包 + 完整结构自动化
$ flutist create --name todos --path features --options micro
✓ features/todos/todos_interface 已创建
✓ features/todos/todos_implementation 已创建
✓ features/todos/todos_testing 已创建
✓ features/todos/todos_tests 已创建
✓ features/todos/todos_example 已创建
# 使用 BLoC 脚手架生成所有文件
$ flutist scaffold --template bloc --name todos_overview --path features/todos/todos_implementation
✓ todos_overview_bloc.dart 已创建
✓ todos_overview_event.dart 已创建
✓ todos_overview_state.dart 已创建
核心特性
| 特性 | 说明 |
|---|---|
| 声明式 | 通过单一的 project.dart 文件声明整个项目结构 |
| 单一来源 | 所有依赖版本通过 package.dart 集中管理 |
| 规则即代码 | 架构违规会立即终止生成过程 |
安装
dart pub global activate flutist
前置条件:Flutter SDK,并确保 ~/.pub-cache/bin 已添加到 PATH
快速开始
1. 初始化项目
cd my_flutter_project
flutist init
Flutist 会根据上下文自动适配:
- • 无
pubspec.yaml:询问是否创建新的 Flutter 项目 - • 存在
pubspec.yaml:询问是新建项目还是迁移现有项目 -
- • 新项目:创建
app模块,添加到工作区,生成lib/main.dart - • 现有项目:仅创建配置文件,保留现有代码结构
- • 新项目:创建
2. 创建模块
# 创建 Clean 架构模块
flutist create --name login --path features --options clean
# 创建 Microfeature 架构模块
flutist create --name network --path packages --options micro
# 创建 Lite 模块
flutist create --name auth --path packages --options lite
# 创建单一包
flutist create --name utils --path core
3. 管理依赖
# 添加包(自动解析版本)
flutist pub add http bloc flutter_bloc
# 同步依赖到所有模块
flutist generate
4. 从自定义模板生成代码
# 列出可用模板
flutist scaffold list
# 从模板生成
flutist scaffold feature --name login
flutist scaffold feature --name login --path lib/features
命令一览
| 命令 | 描述 | 用法 |
|---|---|---|
| init | 初始化新项目或现有项目 | flutist init |
| create | 创建新模块 | flutist create --name <name> --path <path> [--options <type>] |
| generate | 同步依赖并重新生成文件 | flutist generate |
| check | 检查架构规则(CI 友好,不修改文件) | flutist check |
| test | 并行运行所有模块的测试 | flutist test [-m <module>] |
| scaffold | 从模板生成代码 | flutist scaffold <template> --name <name> |
| pub | 管理依赖 | flutist pub add <package> |
| graph | 可视化模块依赖关系 | flutist graph [--format <format>] |
| help | 显示帮助信息 | flutist help [command] |
核心文件
| 文件 | 说明 |
|---|---|
package.dart |
外部包版本和模块名称的单一真实来源,多行格式为解析必需 |
project.dart |
声明模块依赖和模块间关系,由 flutist generate 读取 |
flutist_gen.dart |
自动生成的类型安全访问器,提供 IDE 自动补全支持 |
项目结构
典型的 Flutist 项目结构:
my_project/
├── project.dart # 项目配置
├── package.dart # 集中式依赖管理
├── pubspec.yaml # 工作区配置
├── lib/ # 根应用代码
│ └── main.dart
├── app/ # 主应用模块
│ ├── lib/
│ │ └── app.dart
│ └── pubspec.yaml
├── features/ # 功能模块
│ └── auth/
│ ├── auth_domain/
│ ├── auth_data/
│ └── auth_presentation/
├── packages/ # 库模块
│ └── network/
│ ├── network_interface/
│ ├── network_implementation/
│ ├── network_testing/
│ ├── network_tests/
│ └── network_example/
└── flutist/
├── templates/ # 脚手架模板
└── flutist_gen.dart # 生成的代码
模块类型
flutist create 会生成层级包并自动在 project.dart 中配置依赖关系。
Clean 架构 (--options clean)
3 层 Clean Architecture,最适合需要清晰关注点分离的功能模块。
features/login/
├── login_domain/ # 业务规则、实体、用例(无外部依赖)
├── login_data/ # 仓库、数据源、DTO
└── login_presentation/ # UI 和状态管理
自动配置依赖:presentation → domain,data → domain
规则:所有依赖箭头指向 domain,domain 不依赖任何东西。
Microfeature 架构 (--options micro)
5 层 Microfeature Architecture,最适合跨功能共享的可复用库。
packages/network/
├── network_interface/ # 公共 API(抽象类、模型)
├── network_implementation/ # 具体实现
├── network_testing/ # 测试辅助、模拟对象
├── network_tests/ # 单元测试和集成测试
└── network_example/ # 模块演示应用
自动配置依赖:implementation/testing → interface,tests/example → implementation + testing
规则:消费者只依赖 interface,组合根注入实现。
Lite 架构 (--options lite)
4 层 Microfeature lite(无 example),最适合内部 API。
packages/auth/
├── auth_interface/
├── auth_implementation/
├── auth_testing/
└── auth_tests/
单一包(省略 --options)
无层级,最适合工具类、共享模型或应用外壳。
core/utils/
├── lib/
│ └── utils.dart
└── pubspec.yaml
架构验证
flutist generate 和 flutist check 自动执行以下规则:
| 规则 | 说明 |
|---|---|
| 实现引用 | 只有组合根(默认:app)和同功能测试/example 可以引用 _implementation 包 |
| 测试层隔离 |
_testing 包被排除在生产依赖之外 |
| Example 独立性 |
_example 模块不能被任何生产代码引用 |
| 方向强制 | 同功能层级遵循声明的依赖方向 |
| 循环依赖 | 通过 DFS 遍历检测,绝不允许 |
配置选项
// project.dart
ProjectOptions(
strictMode: true, // true(默认):违规时终止 / false:仅警告
compositionRoots: ['app'], // 允许引用 _implementation 的模块
)
Scaffold 脚手架模板
将重复性工作保存为模板,通过 flutist scaffold 自动化生成代码。
模板变量
在 .template 文件和 path 值中使用 {{变量}} 进行替换:
| 变量 | 输入 login_feature
|
输出 | |
|---|---|---|---|
| `{{name | snake_case}}` | login_feature | login_feature |
| `{{name | pascal_case}}` | login_feature | LoginFeature |
| `{{name | camel_case}}` | login_feature | loginFeature |
| `{{name | upper_case}}` | login_feature | LOGIN_FEATURE |
template.yaml 结构
description: "BLoC Feature Template"
attributes:
- name: name
required: true
- name: path
required: false
default: "lib/features"
items:
- type: file
path: "{{path}}/{{name | snake_case}}/{{name | snake_case}}_bloc.dart"
templatePath: "bloc.dart.template"
- type: string
path: "{{path}}/{{name | snake_case}}/README.md"
contents: |
# {{name | pascal_case}}
Item 类型
| 类型 | 说明 |
|---|---|
| file | 读取 .template 文件,替换变量后生成 |
| string | 使用内联内容直接生成文件 |
| directory | 复制整个模板目录 |
实战示例:BLoC Feature 模板
bloc.dart.template
import 'package:bloc/bloc.dart';
part '{{name | snake_case}}_event.dart';
part '{{name | snake_case}}_state.dart';
class {{name | pascal_case}}Bloc
extends Bloc<{{name | pascal_case}}Event, {{name | pascal_case}}State> {
{{name | pascal_case}}Bloc() : super(const {{name | pascal_case}}Initial()) {
on<{{name | pascal_case}}Started>(_onStarted);
}
Future<void> _onStarted(
{{name | pascal_case}}Started event,
Emitter<{{name | pascal_case}}State> emit,
) async {}
}
event.dart.template
part of '{{name | snake_case}}_bloc.dart';
sealed class {{name | pascal_case}}Event {
const {{name | pascal_case}}Event();
}
final class {{name | pascal_case}}Started extends {{name | pascal_case}}Event {
const {{name | pascal_case}}Started();
}
state.dart.template
part of '{{name | snake_case}}_bloc.dart';
sealed class {{name | pascal_case}}State {
const {{name | pascal_case}}State();
}
final class {{name | pascal_case}}Initial extends {{name | pascal_case}}State {
const {{name | pascal_case}}Initial();
}
final class {{name | pascal_case}}Loading extends {{name | pascal_case}}State {
const {{name | pascal_case}}Loading();
}
final class {{name | pascal_case}}Loaded<T> extends {{name | pascal_case}}State {
final T data;
const {{name | pascal_case}}Loaded(this.data);
}
final class {{name | pascal_case}}Error extends {{name | pascal_case}}State {
final String message;
const {{name | pascal_case}}Error(this.message);
}
运行生成
$ flutist scaffold bloc_feature --name login --path lib/features
✓ lib/features/login/login_bloc.dart
✓ lib/features/login/login_event.dart
✓ lib/features/login/login_state.dart
实战示例:Riverpod Notifier 模板
notifier.dart.template
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '{{name | snake_case}}_state.dart';
part '{{name | snake_case}}_notifier.g.dart';
@riverpod
class {{name | pascal_case}}Notifier extends _${{name | pascal_case}}Notifier {
@override
{{name | pascal_case}}State build() => const {{name | pascal_case}}State.initial();
Future<void> load() async {
state = const {{name | pascal_case}}State.loading();
try {
state = const {{name | pascal_case}}State.loaded(null);
} catch (e) {
state = {{name | pascal_case}}State.error(e.toString());
}
}
}
state.dart.template
import 'package:freezed_annotation/freezed_annotation.dart';
part '{{name | snake_case}}_state.freezed.dart';
@freezed
class {{name | pascal_case}}State with _${{name | pascal_case}}State {
const factory {{name | pascal_case}}State.initial() = _Initial;
const factory {{name | pascal_case}}State.loading() = _Loading;
const factory {{name | pascal_case}}State.loaded(dynamic data) = _Loaded;
const factory {{name | pascal_case}}State.error(String msg) = _Error;
}
示例项目
Clean Architecture 示例
flutist_clean_architecture[2]
- • Domain、Data、Presentation 三层 Clean Architecture
- • 集中式依赖管理
- • 大型 Flutter 应用最佳实践
Microfeature Architecture 示例
flutist_microfeature_architecture[3]
- • Interface、Implementation、Tests、Testing 四层 Microfeature 架构
- • 完全隔离的可复用库模块
- • 集中式依赖管理
相关链接
| 资源 | 链接 |
|---|---|
| 📦 pub.dev | pub.dev/packages/fl… |
| 📖 文档网站 | deepwiki.com/seonwooke/f… |
| 💻 GitHub | github.com/seonwooke/f… |
引用链接
[1] Tuist: tuist.io/[2] flutist_clean_architecture: github.com/seonwooke/f…[3] flutist_microfeature_architecture: github.com/seonwooke/f…
摩根士丹利:预计中国股市到年底将有约5-10%的��和上行空间
华泰证券:关注新能源链、出口链等拥挤度不高的景气方向
氢能产业迈向规模化发展阶段,四大行业上市公司加速“跑马圈地”
6.响应式系统比对:通过 Vue3 响应式库写 React 应用
前言
鉴于 Vue3 已经把响应式库进行了独立,也就是 @vue/reactivity,既然 Mobx 也是一个响应式库都可以应用在 React 上,那么 @vue/reactivity 可不可以也应用在 React 上呢?很显然是可以的,社区里也有很多关于这么方面的实践。那么我们这里也提供一个参考 Mobx 实现的版本。
跟 Mobx 对比的话,@vue/reactivity 就相当于 mobx 库,所以我们只需要参考 mobx-react-lite 实现一个 vue-react-lite 即可。
实现 vue-react-lite
我们通过上一篇文章可以知道 Mobx 是通过 mobx-react-lite 实现与 React 进行链接的,其中最重要的函数就是 observer,那么我们也在 vue-react-lite 中实现一个 observer 函数。根据我们前篇所学的知识知道 observer 是一个高阶函数,所以我们初步把 observer 的基础架构搭建出来。
function observer(baseComponent) {
return (props) => {
return baseComponent(props)
}
}
接下来我们知道 Mobx 中是通过 Reaction 这个订阅者中介来实现不同组件函数的代理的,而在 @vue/reactivity 中的跟 Reaction 相同角色的的则是 ReactiveEffect,那么我们就可以通过它来实现我们想要的功能。
代码实现如下:
import { useState, useRef } from "react"
import { ReactiveEffect } from "@vue/reactivity"
function observer(baseComponent) {
return (props) => {
const [, setState] = useState()
const admRef = useRef(null)
if (!admRef.current) {
admRef.current = new ReactiveEffect(() => {
return baseComponent(props)
}, () => {
setState(Symbol())
})
}
const effect = admRef.current
return effect.run()
}
}
那么我们就通过 ReactiveEffect 实现了一个跟 mobx-react-lite 中的 observer 一样的功能的函数。
如果大家对 Vue3 的 effect 函数熟悉的话,我们上述 observer 的实现过程跟 Vue3 的 effect 实现很类似的。我们可以回顾一下 Vue3 的 ReactiveEffect 类的功能,它本质是一个订阅者中介,跟 Vue2 的 Watcher 类是一样的角色。ReactiveEffect 的第一个参数就是具体的订阅者函数,而第二个参数则是一个叫 scheduler 的回调函数,在更新的时候如果存在 scheduler 回调函数则执行 scheduler 回调函数,否则执行第一个参数的函数。基于这个原理,我们就在 ReactiveEffect 的第二个参数中设置执行 React 的更新 setState(Symbol()),同时 ReactiveEffect 上存在一个 run 方法,需要通过手动执行进行初始化。
应用 vue-react-lite
那么我们上面通过 ReactiveEffect 实现了 observer 函数,这样我们就可以在 React 中应用 Vue3 的数据响应式库了。下面我们来测试一下:
import { reactive } from "@vue/reactivity";
import { observer } from "./vue-react-lite"
const proxy = reactive({ name: 'Cobyte', secondsPassed: 0 })
const TimerView = observer(({ proxy }) => <span>the content run in `@vue/reactivity` is "Seconds passed: {proxy.secondsPassed}"</span>)
function App() {
return (
<TimerView proxy={proxy}></TimerView>
);
}
setInterval(() => {
proxy.secondsPassed +=1
}, 1000)
export default App;
打印结果如下:
![]()
我们发现已经成功把 @vue/reactivity 库应用到 React 中了。
根据 Mobx 的启发实现 Vue 数据响应式的 OOP
我们知道 Mobx 的写法是更倾向 OOP 的,同时是严格遵守单向数据流,所以我们也可以在通过 Vue 响应式库提供的 shallowRef API 实现 OOP。
import { reactive, shallowRef } from "@vue/reactivity"
import { observer } from "./vue-react-lite"
class DataService {
constructor(val) {
this.r = shallowRef(val)
}
get count() {
return this.r.value
}
setCount(val) {
this.r.value = val
}
}
const dataService = new DataService(0)
const TimerView = observer(({ proxy }) => <span>the content run in @vue/reactivity is "Seconds passed: {proxy.count}"</span>)
function App() {
return (
<TimerView proxy={dataService}></TimerView>
);
}
setInterval(() => {
dataService.setCount(Date.now())
}, 1000)
export default App;
但上述方式还是不能堵住别人可以通过直接修改对象的方式更改响应式的值,从而打破单向数据流的规则。
例如下面的例子:
setInterval(() => {
dataService.r.value = Date.now()
}, 1000)
那么为了堵住这个漏洞,我们可以通过私有变量来解决:
class DataService {
#r
constructor(val) {
this.#r = shallowRef(val)
}
get count() {
return this.#r.value
}
setCount(val) {
this.#r.value = val
}
}
const dataService = new DataService(0)
这个时候我们就不能通过直接修改对象的方式更改响应式的值了。
setInterval(() => {
dataService.#r.value = Date.now()
}, 1000)
我们上述这种方式比较适合基本数据类型的情况,如果是引用类型的话,就不太适用了。如果是引用类型我们不可能在上面写那么多属性访问器,我们可以像 Vue2 那样把所有的响应式数据代理到 Vue 的实例对象上,然后可以通过 this 进行访问。
修改如下:
import { shallowRef } from "@vue/reactivity";
class DataService {
#r
constructor(val) {
this.#r = shallowRef(val)
// 像 Vue2 一样把响应式数据代理到实例对象上
return new Proxy(this, {
get(target, key) {
// 如果是响应式数据就返回响应式数据
if (target.#r.value[key]) {
return target.#r.value[key]
} else {
// 如果是自身的属性就返回自身属性,例如 setState
return target[key]
}
},
set(target, key, val) {
throw new Error('请通过 setState 方法进行更新')
}
})
}
setState(val) {
this.#r.value = val
}
}
const dataService = new DataService({ name: 'Cobyte', date: '2024-03-22', now: { time: 123 } })
const TimerView = observer(({ proxy, now }) => <span>the content run in @vue/reactivity "author: {proxy.name}, the date is: {proxy.date} now is {proxy.now.time}"</span>)
function App() {
return (
<TimerView proxy={dataService} now={dataService.now}></TimerView>
);
}
setInterval(() => {
dataService.setState({ name: '掘金签约作者', date: '2024年3月22日', now: { time: Date.now() }})
}, 1000)
export default App;
我们通过把响应式数据代理到实例对象上,优化了引用类型的使用方式。
![]()
至此,我们受 Mobx 的启发实现了在 React 中使用 Vue3 的响应式数据库,同时跟 Mobx、Flux、Redux 一样实现单向数据流。不过我们目前采用的是最新的技术私有变量,这个方案目前兼容性并不好,但作为技术交流也可以给大家一个启发。
为什么 Vue 可以通过重新运行组件 render 函数进行更新?
我们在前篇文章通过相对比较简洁的代码实现了 Mobx 的核心原理,同时对比了同时响应式的 Vue 和 Mobx 的最大设计区别,在 Vue 中创建的响应式数据,是可以随意在任何地方通过普通属性访问器进行修改的,但 Mobx 中则不提倡这种可以随意修改 state 的方式,在 Mobx 中希望开发者通过 actions 来改变 state,本质是像 React 那样通过一个函数来修改 state,或者说是遵循 Flux 和 Redux 的单向数据流思想。同时 Mobx 中的订阅者中介 Reaction 和 Vue 中的订阅者中介实现则有比较大的区别,主要是因为 Mobx 主要的设计受 React 的影响,在更新的时候需要特别的设置,而不像 Vue 那样直接重新运行副作用函数就可以了,这个说到底也是因为 React 不是靠依赖追踪来实现响应式的缘故。
那么问题就来了,为什么 Vue 可以通过重新运行组件 render 函数进行更新,而 React 则不行?当然 React 在普通情况下,你在更新的时候是不知道哪个组件函数需要更新,但我们通过 Mobx 就可以实现了依赖收集,就可以知道更新的时候那些组件函数需要重新执行,但即便这样 React 也不能通过重新执行组件函数来实现更新,这是为什么呢?
一个组件要渲染到页面上需要哪些必备条件呢?我们先看看下面的一个 React 应用的渲染例子:
ReactDOM.render(App, document.getElementById("root")
那么从上述的 React 应用渲染的例子我们可以知道,一个组件渲染到页面上是一定要知道渲染到哪个元素容器中的,这一点无论是 React 还是 Vue 都是一样的。如果仅仅只是执行一个组件函数是不能实现渲染的,所以在实现 Mobx 的 Reaction 的时候,不能像 Vue 的订阅者中介那样实现。那么为什么在 Vue 中可以通过重新运行组件 render 函数进行更新呢,或者是直接重新运行组件函数进行更新呢?
这是因为在 Vue 中被收集到订阅者记录变量中的函数,并不是组件的 render 函数,而是一个高阶函数,在高阶函数内部才最后执行组件的 render 函数。我们这里以 Vue3 中的情况进分析,在 Vue3 中最后处理组件 render 函数的地方是在 setupRenderEffect 函数中,下面是 setupRenderEffect 的简洁实现代码结构。
function setupRenderEffect(instance, initialVNode, container, anchor, parentSusp) {
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 初始化走这里
const subTree = (instance.subTree = renderComponentRoot(instance))
// 通过 patch 函数进行挂载,第三个参数就要挂载的HTML容器
patch(
null,
subTree,
container, // 目标挂载点
anchor,
instance,
parentSuspense,
isSVG
)
instance.isMounted = true
} else {
// 更新走这里
// 重新执行组件 render 函数
const nextTree = renderComponentRoot(instance)
// 上一次的生成的虚拟DOM为旧的虚拟DOM
const prevTree = instance.subTree
instance.subTree = nextTree
// 更新也是通过 patch 函数进行挂载,也同样需要提供挂载的HTML容器,也就是第三个参数
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!, // 更新的时候也需要提供渲染的目标挂载HTML元素
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
}
}
// 从这我们可以看到被收集的依赖并不是组件的 render 函数,而是一个包装函数 componentUpdateFn
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update), // 调度函数 scheduler,最后还是执行 update 方法
instance.scope // track it in component's effect scope
))
// 初始化的时候需要执行 run 方法
const update = (instance.update = () => effect.run())
// 执行
update()
}
我们从上面的 Vue3 的 setupRenderEffect 的简洁实现代码中可以看到在 Vue 中所谓收集依赖的依赖并不是组件的渲染函数,而是一个包装函数,在包装函数中在初始化和更新阶段都是通过执行组件的 render 函数获得组件的虚拟DOM,然后再通过 patch 函数进行渲染挂载到具体的元素节点下。而在 Vue 的内部中是可以获取到具体需要渲染挂载的元素节点的,而我们在 React 的应用层首先是无法通过组件函数获得需要挂载的元素节点的,其次 React 的更新流程本质上就跟 Vue 这类型通过依赖收集的数据响应式框架不一样。
总结
本文受 Mobx 启发,利用 @vue/reactivity 的 ReactiveEffect 实现了类似 mobx-react-lite 的 observer 高阶函数,成功将 Vue 响应式库集成到 React 中,实现了单向数据流和依赖追踪。同时,通过私有变量和 Proxy 代理优化了 OOP 风格下的响应式数据访问,避免了直接修改状态。最后,从底层机制解释了 Vue 能够直接重新运行组件 render 函数更新,而 React 不能的根本原因:Vue 的依赖收集针对的是包含 patch 挂载逻辑的包装函数,可获取具体渲染容器;React 的更新流程不依赖此类追踪,且组件函数层面无法获取挂载节点。这揭示了两种框架在设计哲学与实现机制上的本质差异。
我是程序员Cobyte,现在已转向研究 AI Agent,欢迎添加 v: icobyte,学习交流 AI Agent 应用开发。
银河证券:消费板块估值处于历史偏低水平,建议关注农林牧渔等方向
36氪首发|「东犁退休俱乐部」完成亿元级B轮融资,为300万活力银发族打造“线上+线下”社交生活方式
36氪独家获悉,「上海东犁退休俱乐部」近日完成亿元级B轮融资,由恒旭资本领投,德同资本跟投。本轮资金将用于拓展多元化银发服务场景、加强上游供应链合作,以及推动AI技术与数据中台升级。
上海东犁退休俱乐部(以下简称“东犁”)是一家面向退休人群的社交娱乐服务平台,专注为50-80岁活力银发群体提供旅游、聚会、商品、兴趣爱好等综合性服务。
截至目前,东犁已累计服务超300万用户,覆盖江浙沪地区,复购率超过60%。2025年全年,其旅游业务营收超8亿元,银发电商业务预计今年突破3-4亿元。
从电视制片人到银发创业者
东犁创始人吕辰晔有SMG工作背景及日本留学经历。吕辰晔创办东犁,契机源于一次日本考察——他看到上市公司Halmek通过“电台节目+线下沙龙+内容杂志”的模式服务银发族,深受触动。
“我们当时借鉴了Halmek的模式,用电视代替电台,用公众号代替杂志,同时保留了线下沙龙的形式。”吕辰晔告诉36氪。
彼时,国内退休人群的消费以电视购物为主,旅游市场则充斥低价购物团。东犁首先从旅游产品入手,剔除购物和自费环节,重新设计线路,并提升餐饮、住宿标准。
这种“内容先行”的模式迅速建立了用户信任。东犁旗下节目《我们退休啦》在上海都市频道播出期间,收视率大约在1%,覆盖40万人次/天。节目积累的观众逐渐转化为小程序用户,并进一步沉淀至私域。 “电视圈有句话叫‘得阿姨妈妈者得天下’,我的内容受众其实一直没变。”吕辰晔总结。
如今,东犁的内容矩阵已覆盖微信视频号、抖音、小红书等平台,拥有数百个矩阵账号;其小程序的注册用户超过300万,社群月发帖数过万。
回顾创业历程,让吕辰晔印象深刻的一个场景,是一位白发苍苍的老太太,下雨天坐地铁来上课,课后独自戴着老花镜,用单指在键盘上敲打。“她把人生很重要的一段时间用在了你提供的服务上,你会觉得真的应该对他们更好一点。”
长途旅游到高频聚会,构造产品金字塔
2020年前,东犁以出境游和国内长线为主。突如其来的疫情导致长途旅游受阻,团队开始探索周边聚餐、聚会等短频快业务,意外发现退休人群对高频次、高质量、重场景的社交需求远超预期。
如今,东犁已形成清晰的产品金字塔:塔基是高频的本地聚会,向上依次是国内中长线、出境游及、私家团。吕辰晔解释:“退休人群每一周可能聚会一两次,高频带动低频消费,综合复购率就上去了。”
![]()
东犁的旅游用户
这一产品结构演变并非事先规划而成,而是由市场需求推着走。“我们起初并未重视聚会业务的营收潜力,但实际运营后发现它成长非常快。”吕辰晔坦言。以聚餐产品为例,国际饭店套餐销量近2万份,全聚德套餐已破4万单,证明了银发族对本地高品质社交的渴求。
线下的“壹天聚乐部是这一逻辑的自然延伸。目前东犁在上海开设了4家壹天聚乐部门店,面积均超过3000平米,主题包括“岁月情怀”“时光照相馆”“环球主题馆”“世界之窗”,用户人均消费100多元,可享受两顿正餐及棋牌、KTV、舞池等全天服务。门店选址紧邻地铁、居民密集区,店内设沉浸式打卡点,满足用户“九宫格发朋友圈”的社交需求。
![]()
东犁的线下业态“壹天聚乐部”
盈利方面,首店“壹天聚”(共康店)18个月实现回本。吕辰晔强调,人均百元含两餐的定价属于低客单价,盈利关键在于持续流量输入与较高的复购频次:“我们不是先做壹天聚乐部再做退休俱乐部,而是反过来的。没有持续的流量和品牌口碑积累,单店很难存活。上海乃至全国,可能只有我们的壹天聚乐部能够实现短期内盈利。”
与传统餐饮不同,壹天聚乐部采用套餐制,每三个月换一次菜单,通过集中采购控制成本,减少后厨能耗浪费。同时,利用退休人群时间灵活的特点,填补商场、影院、面包房等业态的客流潮汐时段,形成良性互补的跨业态合作。
在上海模式跑通后,吕辰晔对异地扩张仍然极为谨慎。“壹天聚乐部在外地很难简单复制,除非是重庆、成都这样消费活力强劲、老年人口占比高的城市。”
未来两年,东犁将重点发力两大方向:电商(聚焦视频号及私域)和主题游(不一样系列)。
其中,主题游计划开发500-600条线路,以8-10人小团为主,满足分餐制、深度体验等新兴需求。“如果能把主题游做好,以产品为纲,无论平台怎么变,都会有好结果。”东犁联合创始人李萍介绍到,“公司近期上线的‘不一样的黄山’系列,以超过4000元的客单价,凭借‘全程分餐制+当地特色美食’的餐饮颠覆、‘挖掘小众深度文化体验’的行程设计以及‘住得不一样’的品质住宿,重新定义中老年黄山之旅。线路一经推出,未来3个月班期即售罄,销量破千单。”
从资深内容制作者到银发经济探索者,吕辰晔希望东犁成为“活力退休人群的生活方式平台”。而这场关于“夕阳”的生意,朝阳才刚刚升起。
投资人观点:
作为本轮领投方,恒旭资本高度看好兼具社会价值与商业价值的银发经济赛道。恒旭资本认为,东犁拥有敏锐的行业洞察、专业的内容基因和精细的运营能力,有潜力成为中国银发经济的领导者。恒旭资本于2019年创立,聚焦硬科技、健康消费等符合国家转型和新质生产力要求的投资机遇,累计管理规模已超过400亿元人民币。此次领投东犁,是其在健康消费领域的又一重要布局。
跟投方德同资本十余年来深耕先进制造、医疗健康与AI等核心领域。日前,发起设立十亿规模文化科技基金,联合安徽文旅投控、芜湖高新产业发展基金、杭州文投等共同出资,旨在推动AI与实体经济深度融合,引领科技文化投资前沿。基金首关后迅速布局银发经济,投资了东犁项目。德同资本认为,东犁定义了新一代“旅游-娱乐-社交-消费”的适老化产品与服务,积淀了“信任”护城河,德同将以优势资源助力其加速银发产业升级发展。