Files
2026-09-04 15:22:20 +08:00

29 KiB
Raw Permalink Blame History

Flutter 学习全旅程讲解 — 从零到生产级项目

预计时长:40 分钟 | 项目:new_learn | Flutter 3.44.x + Dart 3.12


目录

章节 主题 预估时长
一 环境安装与项目初始化 ~4 min
二 Dart 3 语法核心要点 ~4 min
三 Flutter 3 核心框架与分层架构 ~5 min
四 全局状态管理:Riverpod 方案 ~4 min
五 Dio 网络请求生产级封装 ~4 min
六 屏幕适配:ScreenUtil 方案 ~3 min
七 多品牌主题配置体系 ~3 min
八 多环境变量配置体系 ~3 min
九 本地存储:Isar + 响应式 watch ~3 min
十 Demo 组件封装思路汇总 ~5 min
十一 学习路径与心得总结 ~2 min

一、环境安装与项目初始化(~4 min)

1.1 工具链一览

工具 版本 用途
Flutter SDK 3.44.x 编译、热重载、多端构建
Dart SDK 3.12 语言运行时
Android Studio / VS Code 最新 IDE + 插件
Android SDK API 34+ 安卓构建
Xcode 15+(Mac) iOS 构建

本项目路径:D:\dev\flutter_3.44.8\flutter(见 local.properties)

1.2 创建项目

flutter create --org com.example --platforms=android,ios,web,windows new_learn

1.3 项目核心依赖

完整依赖见 pubspec.yaml,关键项:

environment:
  sdk: ^3.12.2

dependencies:
  flutter: { sdk: flutter }
  dio: ^5.10.0                         # 网络请求
  go_router: ^17.4.0                  # 声明式路由
  flutter_riverpod: ^3.3.2            # 状态管理
  flutter_screenutil: ^5.9.0          # 屏幕适配

  cached_network_image: ^3.4.1         # 网络图片缓存(ImageDemo)
  infinite_scroll_pagination: ^5.1.1   # 分页列表
  flutter_widget_from_html_core: ^0.17.2 # 富文本 HTML 渲染

  shared_preferences: ^2.5.3           # 轻量本地存储
  flutter_secure_storage: ^9.2.4       # 敏感凭据安全存储
  isar_community: ^3.3.2               # 本地数据库
  isar_community_flutter_libs: ^3.3.2
  path_provider: ^2.1.5

  wechat_assets_picker: ^9.8.0         # 相册多选/预览(原生端)
  wechat_camera_picker: ^4.6.0         # 微信风格相机(原生端)
  image_picker: ^1.2.3                 # Web 兜底图片选择
  permission_handler: ^11.3.1          # 权限管理

  image: ^4.8.0                        # 纯 Dart 图片处理(含 Web)
  flutter_image_compress: ^2.5.1       # 原生压缩(Android/iOS)
  photo_view: ^0.15.0                  # 图片预览缩放

  video_player: ^2.13.0                # 视频播放
  chewie: ^1.13.1                      # 视频 UI 封装
  just_audio: ^0.10.6                  # 音频播放

  share_plus: ^10.1.4                  # 系统分享
  gal: ^2.3.3                          # 保存到相册
  package_info_plus: ^8.3.0            # 版本信息
  url_launcher: ^6.3.1                 # 外部跳转
  smooth_sheets: ^1.1.1                # 常驻底部面板

  crypto: ^3.0.6                       # MD5 等加密
  # 完整依赖与注释说明以 pubspec.yaml 为准(此处为学习旅程的“关键依赖速览”)

1.4 目录结构

lib/
├── main.dart                    # 启动入口(5步初始化)
├── core/                        # 核心基础设施层
│   ├── app.dart                 # 根组件 TabApp
│   ├── config/                  # 环境+品牌配置
│   ├── network/                 # Dio 封装 + API
│   ├── providers/               # Riverpod 全局状态
│   ├── router/                  # go_router 路由表
│   ├── theme/                   # 多品牌主题
│   ├── storage/                 # Isar + 安全存储
│   ├── services/                # 图片/选择/分享服务
│   └── components/              # 9个可复用组件
├── features/                    # 业务功能层
│   ├── auth/                    # 登录+隐私协议
│   ├── exam/                    # 试卷练习
│   ├── my/                      # 个人中心
│   └── ...
└── pages/demo/                  # 28个独立学习Demo

讲解要点:这套 core / features / pages 三层分离,对标前端 utils / views / components,让基础设施和业务逻辑彻底解耦。


二、Dart 3 语法核心要点(~4 min)

2.1 与 JavaScript/TypeScript 的关键差异

概念 JS/TS Dart 3
类型声明 let x: number int x / var x(类型推断)
空安全 可选 ? 健全空安全 String?,编译期拦截
异步 Promise Future + async/await
集合 Array/Map List<T> / Map<K,V> / Set<T>
类 class class + mixin 混入
模式匹配 无 switch 表达式 + 解构(Dart 3 新特性)

2.2 空安全(Sound Null Safety)

String name = 'Flutter';    // 不可为 null
String? nickname;           // 可为 null,使用前必须判空

// 安全调用
int len = nickname?.length ?? 0;   // ?. + ?? 兜底

核心规则:所有变量默认非空,? 才允许 null。编译期就帮你拦住空指针。

2.3 异步编程

fromJson 是构造函数,把字典转换成我们定义的 User 实体类对象,获得类型安全、点访问、编译检查。

Future<User> fetchUser() async {
  final response = await dio.get('/user');
  return User.fromJson(response.data);
}

2.4 模式匹配(Dart 3 特性)

// switch 表达式
String describe(Object obj) => switch (obj) {
  int() => '整数',
  String() => '字符串',
  List<int>() => '整数列表',
  _ => '未知',
};

2.5 类与混入

mixin Disposable {
  void dispose();
}

class MyController with Disposable {
  @override
  void dispose() { /* 释放资源 */ }
}

讲解要点:Dart 3 的空安全是「健全」的,编译期保证非空变量不会拿到 null,比 TS 的可选链更彻底。模式匹配让分支逻辑更简洁。


三、Flutter 3 核心框架与分层架构(~5 min)

3.1 Widget 是什么?

核心认知:Widget 是「配置对象」不是「渲染 View」。

Widget(配置蓝图)→ Element(实例化树节点)→ RenderObject(真正测量布局绘制)
  • StatelessWidget:纯展示,props 不变 → 如 Text、Icon
  • StatefulWidget:有内部状态,setState 触发重建 → 如 Checkbox、TextField

3.2 核心布局原理

Flutter 布局第一法则:父组件向子组件传递约束(Constraints),子组件根据约束决定自身大小(Size),然后向上返回。

父 Widget  ──Constraints──>  子 Widget
                              ↓ 决定自身大小
父 Widget  <──Size──────────  子 Widget
  • 主轴(MainAxis):Row 的水平方向 / Column 的垂直方向
  • 交叉轴(CrossAxis):与主轴垂直的方向

3.3 启动流程链路

入口文件 main.dart:

main()  5步串行初始化
  ↓
1. WidgetsFlutterBinding.ensureInitialized()   // 插件通道就绪
2. GoRouter.optionURLReflectsImperativeAPIs   // 修复 web 地址栏同步
3. DioClient.instance.restoreToken()           // 回填登录 Token
4. privacyAcceptedNotifier                     // 恢复隐私协议状态
5. AppDatabase.instance.init()                 // 初始化 Isar
  ↓
runApp(ProviderScope(child: TabApp()))         // Riverpod 容器 + 根组件

根组件 app.dart:

TabApp (StatelessWidget)
  └─ ScreenUtilInit          // 屏幕适配
      └─ MaterialApp.router  // Material3 + 品牌种子色
          ├─ builder: PrivacyStartupGate    // 启动层隐私拦截
          └─ routerConfig: appRouter         // 全局路由表

3.4 路由体系

路由表 app_router.dart:

GoRouter
  ├─ redirect: _guardLogin              // 登录守卫(对标 vue-router beforeEach)
  │   ├─ 未同意隐私 → /privacy-consent
  │   ├─ 已同意+在隐私页 → /login 或 /
  │   ├─ 未登录 → /login
  │   └─ 已登录+在登录页 → /
  ├─ refreshListenable: authTick + privacyNotifier  // 响应式守卫
  ├─ ShellRoute (4 tab 壳)
  │   ├─ /         → TaskPage
  │   ├─ /recite   → RecitePage
  │   ├─ /class    → ClassPage
  │   └─ /my       → MyPage
  ├─ /privacy-consent
  ├─ /login
  └─ /myPoint

讲解要点:go_router 的 refreshListenable 是核心 — 登录态变化时自动 refresh() 重算 redirect,不需要手动跳转,对标 Vue Router 的响应式 beforeEach。


四、全局状态管理:Riverpod 方案(~4 min)

4.1 为什么选 Riverpod?

维度 Provider(旧) Riverpod 3.x
依赖注入 context.read() 耦合 BuildContext 全局容器,不依赖 context
测试 难 mock ProviderContainer 轻量覆盖
安全 运行时报错 编译期检查(Provider 不可缺)
异步 手写 FutureBuilder AsyncNotifier 内建 loading/error/data

4.2 本项目的两种状态方案

方案 A:ValueNotifier — 轻量全局信号

隐私协议状态 privacy_provider.dart:

final ValueNotifier<bool> privacyAcceptedNotifier = ValueNotifier<bool>(false);

为什么不用 Riverpod? go_router 的 refreshListenable 需要 Listenable,ValueNotifier 足够轻量,且 main() 中 runApp 前就能直接恢复状态。

方案 B:Riverpod AsyncNotifier — 全局用户状态

用户状态 user_provider.dart:

class CurrentUserNotifier extends AsyncNotifier<CurrentUserExtension?> {
  @override
  Future<CurrentUserExtension?> build() async {
    // 冷启动异步从本地缓存回填
    return await UserStorage.instance.readUser();
  }

  void set(user) {
    state = AsyncData(user);           // 有值
    authTick.value++;                  // 触发路由守卫重算
  }
}

// 全局 Provider
final currentUserProvider =
    AsyncNotifierProvider<CurrentUserNotifier, CurrentUserExtension?>(...);

状态四态生命周期:

AsyncLoading()    → 正在读缓存
AsyncData(user)   → 有值(已登录)
AsyncData(null)   → 明确未登录
AsyncError(e)     → 异常

讲解要点:authTick(ValueNotifier)是登录态变化的信号源,Riverpod 写状态、ValueNotifier 通知路由,两者协作实现「响应式登录守卫」。


五、Dio 网络请求生产级封装(~4 min)

5.1 封装架构

核心文件 dio_client.dart:

DioClient (单例)
  ├─ BaseOptions
  │   ├─ baseUrl  → 来自 AppConfig(品牌×环境组合)
  │   └─ 超时:连接 15s / 接收 15s / 发送 15s
  ├─ 拦截器三件套
  │   ├─ onRequest  → 自动加 Authorization: Bearer token
  │   ├─ onResponse → 日志打印
  │   └─ onError    → 401清Token + DioException→ApiException
  ├─ 泛型请求方法
  │   ├─ get<T>(path, {cancelToken, queryParameters})
  │   ├─ post<T>(path, {data, cancelToken, onSendProgress})
  │   └─ put<T>(path, {data})
  └─ restoreToken() → 冷启动回填 Token

5.2 统一异常翻译

// UI 层只需要 catch ApiException,不用管 DioException
ApiException _mapError(DioException e) {
  switch (e.type) {
    case DioExceptionType.connectionTimeout:
      return ApiException('网络连接超时,请检查网络');
    case DioExceptionType.connectionError:
      return ApiException('网络连接失败,请检查网络');
    // ... badResponse / cancel / unknown
  }
}

5.3 生产级能力(Demo 演示)

能力 实现方式 Demo
下载进度 onReceiveProgress 回调 dio_pro_demo.dart
取消请求 CancelToken + dispose 自动取消 同上
失败重试 指数退避 _canRetry(仅网络层错误) 同上
Token 自动注入 拦截器 onRequest dio_client.dart
401 自动登出 拦截器 onError dio_client.dart
OSS 直传 已有 authorization 头时跳过注入 oss_uploader.dart

5.4 数据模型 fromJson

// user_model.dart — 防御性解析,兼容后端返回字符串数字
factory CurrentUserExtension.fromJson(Map<String, dynamic> json) {
  return CurrentUserExtension(
    userId: json['userId'] as int,
    userNo: json['userNo']?.toString() ?? '',
    level: _toInt(json['level']),       // 兼容 "1" 或 1
    balance: _toDouble(json['balance']),
    // ... 17 个字段
  );
}

讲解要点:fromJson 用 _toInt / _toDouble 兜底,是真实项目中最常见的后端类型不稳定问题。UI 层永远只 catch ApiException 一个类型。


六、屏幕适配:ScreenUtil 方案(~3 min)

6.1 核心文件

适配模板 screen_adapt_template.dart:

AppAdapt 工具类
  ├─ tabletBreakpoint = 600          // Material 平板断点
  ├─ phoneDesign = Size(375, 812)    // iPhone 逻辑尺寸
  ├─ designSizeOf(context)           // 方案B:平板用设备自身尺寸
  └─ fontSizeResolver                // 方案A:字体放大封顶 1.2 倍

6.2 双方案策略

方案 目标 实现
方案 A 大屏字体不过分放大 fontSizeResolver 封顶 maxFontScale=1.2
方案 B 平板布局不被拉伸 宽高都 ≥600dp 时用设备自身尺寸做 designSize,缩放回到 ~1

6.3 使用规则

// 字号:用 .spMin(大屏只缩不放)
Text('你好', style: TextStyle(fontSize: 16.spMin));

// 宽度/横向间距:用 .w
SizedBox(width: 16.w);

// 高度/纵向:用 .h
SizedBox(height: 48.h);

// 圆角:用 .r
BorderRadius.circular(8.r);

讲解要点:字号必须用 .spMin 而非 .sp — .sp 在大屏会把字体放得过大,.spMin 确保只缩小不放大。


七、多品牌主题配置体系(~3 min)

7.1 三层解耦设计

环境(接口在哪)  →  app_env.dart     编译期 --dart-define 定死
品牌(谁家 App)  →  app_brand.dart   发布期定死,7个品牌
主题(长什么样)  →  brand_theme.dart  运行时可切换
配置聚合           →  app_config.dart  全项目唯一取配置入口

7.2 两级主题模型

模型文件 brand_theme.dart:

BrandTheme(一级品牌)
  ├─ name: 'SkyBlue'
  ├─ seedColor: Color(0xFF4FC3F7)     // 种子色 → Material3 自动生成整套色板
  └─ pages: Map<PageLevel, PageTheme>
      ├─ home:    PageTheme { bg, headerBg, accent, title, banner }
      ├─ detail:  PageTheme { ... }
      └─ result:  PageTheme { ... }

品牌主题文件:

7.3 新增主题三步走

1. 在 brands/ 下新建 xxx_theme.dart → 定义 seedColor + 三层级配置
2. 在 brand_theme.dart 中 import + 添加到 brandThemes 列表
3. 在 app_brand.dart 中给品牌指定 themeId

讲解要点:「模型文件不写颜色、品牌文件不写逻辑」— 新增主题零改页面代码。colorSchemeSeed 让 Material 3 自动从种子色生成完整配色。


八、多环境变量配置体系(~3 min)

8.1 编译期注入

环境配置 app_env.dart:

// 编译期注入,默认 prod
static const String env = String.fromEnvironment('ENV', defaultValue: 'prod');
static const String brand = String.fromEnvironment('BRAND', defaultValue: 'aixue');

8.2 三套环境地址

环境 baseUrl 用途
dev k12-student-api-test.23544.com:16400 本地开发联调
test axdb-test.cnzhyx.cn 测试环境
prod ylax-api.cqbzyx.com(被品牌专属地址覆盖) 生产

8.3 运行命令

# 开发环境 + 爱学蝶变品牌
flutter run --dart-define=ENV=dev --dart-define=BRAND=aixue

# 生产环境 + 楚才品牌
flutter run --dart-define=ENV=prod --dart-define=BRAND=chucai

# 构建 APK
flutter build apk --dart-define=ENV=prod --dart-define=BRAND=chucai

8.4 配置聚合层

app_config.dart — 全项目唯一取配置入口:

// baseUrl 规则:prod 用品牌专属地址覆盖,否则环境通用地址
static String get baseUrl =>
    AppEnv.isProd ? (currentBrand.baseUrl ?? AppEnv.baseUrl) : AppEnv.baseUrl;

// 主题:按品牌查主题注册表,未知兜底第一个
static BrandTheme get theme =>
    brandThemes.firstWhere((t) => t.name == currentBrand.themeId,
        orElse: () => brandThemes.first);

讲解要点:String.fromEnvironment 是编译期常量,不占运行时开销。品牌 × 环境组合让一套代码出 7 个品牌的 App,只需改运行命令。


九、本地存储:Isar + 响应式 watch(~3 min)

9.1 存储组合

存储方式 用途 文件
flutter_secure_storage Token 安全存储 user_storage.dart
shared_preferences 隐私协议同意状态 privacy_storage.dart
isar_community 题库/错题/播放记录 app_database.dart

9.2 Isar 数据模型

@collection
class WrongQuestion {
  Id id = Isar.autoIncrement;
  late int questionId;
  late int userId;
  late String questionTitle;
  late DateTime createdAt;
}

通过 build_runner 生成 .g.dart,提供类型安全的 CRUD。

9.3 平台分流实现

app_database.dart          → 抽象接口
  ├─ app_database_io.dart   → 原生平台 Isar 实现
  └─ app_database_web.dart  → Web 调试自动跳过(Isar 不支持 Web)

9.4 watch 响应式 — 错题自动刷新

Demo 演示 isar_watch_demo.dart:

// Isar watch:数据变化自动通知 UI 刷新
Stream<List<WrongQuestion>> watchWrongQuestions() {
  return database.wrongQuestions
      .where()
      .userIdEqualTo(userId)
      .watch(fireImmediately: true);
}

讲解要点:Isar 的 watch() 是真正的响应式 — 数据库写入后自动通知 Stream 监听者,UI 无需手动刷新。对标前端的「数据驱动视图」,但是是本地数据库级别的。


十、Demo 组件封装思路汇总(~5 min)

10.1 28 个 Demo 的分类学习路径

第一阶段:基础组件

Demo 核心知识点 代码
Text 样式/截断/富文本/手势 text_demo.dart
Image 四种来源 + BoxFit + 错误兜底 image_demo.dart
Input controller + dispose + 密码显隐 input_demo.dart
Align 对齐 + Riverpod 全局信息 align_demo.dart
Stack 层叠 + 全 Positioned 尺寸坑 stack_demo.dart
Wrap 自动换行 + Chip + 自绘 CustomTag wrap_demo.dart

第二阶段:布局进阶

Demo 核心知识点 代码
List 懒加载/下拉刷新/触底加载 list_demo.dart
Grid count/builder/extent 三种构造 grid_demo.dart
Sliver CustomScrollView + 折叠 AppBar sliver_demo.dart
Responsive 手机单列 vs Pad 多列 responsive_demo.dart

第三阶段:路由与表单

Demo 核心知识点 代码
Router 命名路由表 + 三种跳转姿势 router_home_page.dart
Detail 构造传参 vs 命名路由 arguments detail_page.dart
Form 7 个校验知识点/两段式提交 form_demo.dart
Picker pop 带值返回 picker_page.dart

第四阶段:网络与数据

Demo 核心知识点 代码
Dio GET/POST/错误兜底三态 UI dio_demo.dart
Dio Pro 下载进度/CancelToken/重试 dio_pro_demo.dart
Isar Watch 错题本自动刷新 isar_watch_demo.dart
Wrong Flow 接口+Isar+watch 完整链路 wrong_flow_demo.dart
OSS Upload 客户端签名+PUT直传 oss_upload_demo.dart

第五阶段:主题与媒体

Demo 核心知识点 代码
Theme 两级主题模型 + 品牌换肤 theme_demo.dart
Seed Palette colorSchemeSeed 自动生成配色 seed_palette_demo.dart
Animation 隐式/显式/Hero/CustomPaint animation_demo.dart
Rich Text HTML 题目渲染 + audio 替换 rich_text_demo.dart
Audio just_audio 控制条 audio_player_demo.dart
Share 文本/图片/截图海报/存相册 share_demo.dart
Smooth Sheet 常驻多停靠底部面板 smooth_sheet_demo.dart

10.2 核心组件封装思路(9个可复用组件)

组件 封装思路 代码
PrivacyStartupGate 启动层隐私 Gate — 冷启动不闪登录页,视觉第一拦 privacy_startup_gate.dart
CommonPagedList 通用分页列表 — 基于 infinite_scroll_pagination,对标 z-paging common_paged_list.dart
AudioPlayerCard 通用音频控制条 — 播放/进度/倍速/循环,切后台自动暂停 audio_player_card.dart
VideoPlayerView 通用视频播放器内嵌版 — 无 Scaffold,可嵌入任意位置 video_player_view.dart
ImagePreviewViewer 通用图片预览 — photo_view 双指缩放 + Hero 动画 image_preview_viewer.dart
RichTextContent 富文本渲染 — HTML + audio/video 自动替换为播放器 rich_text_content.dart
PersistentSnapSheet 常驻底部面板 — smooth_sheets 封装 persistent_snap_sheet.dart
AppUpdateDialog 版本更新弹窗 — 强制/可选 + 平台分流跳转 app_update_dialog.dart

10.3 服务层封装思路(3个核心服务)

服务 封装思路 代码
PickerService 图片选择统一服务 — Web用image_picker / 原生用微信选择器 + 权限守卫 picker_service.dart
ImageProcessor 图片压缩 — 原生走flutter_image_compress / Web走纯Dart image包(Isolate隔离) image_processor.dart
ShareService 分享服务 — 截图Widget渲染树→PNG→分享/存相册 share_service.dart

10.4 封装设计原则

  1. 平台分流:同一接口,io/web 双实现,条件 import自动选择
  2. 资源释放:所有 TextEditingController / StreamSubscription / CancelToken / AnimationController 在 dispose 中销毁
  3. 防御性解析:fromJson 兜底后端类型不稳定(_toInt / _toDouble)
  4. 单一职责:组件只管渲染,数据逻辑下沉到 Provider/Service
  5. 配置驱动:环境/品牌/主题互不依赖,全项目统一走 AppConfig 取配置

十一、学习路径与心得总结(~2 min)

11.1 推荐学习顺序

第一阶段:Widget 基础
  Text → Image → Input → Align → Stack → Wrap
    ↓
第二阶段:布局与列表
  List → Grid → Sliver → Responsive
    ↓
第三阶段:路由与表单
  Router → Detail → Form → Picker
    ↓
第四阶段:网络与数据
  Dio → Dio Pro → Isar Watch → Wrong Flow → OSS Upload
    ↓
第五阶段:主题与媒体
  Theme → Seed Palette → Animation → Rich Text → Audio → Share → Smooth Sheet

11.2 从前端转 Flutter 的核心思维转变

前端思维 Flutter 思维
DOM 是真实节点 Widget 是不可变配置,每帧重建
数据驱动视图(Vue/React) setState 触发 Element 树 diff + 重建
CSS 盒模型 约束传递模型(父给约束,子定大小)
路由守卫 beforeEach GoRouter redirect + refreshListenable
Pinia/Vuex 全局 store Riverpod Provider + AsyncNotifier
axios 拦截器 Dio 拦截器三件套
rem/vw 适配 ScreenUtil .w/.h/.spMin
多环境 .env 文件 --dart-define 编译期注入

11.3 本项目的技术亮点

  1. 一套代码出 7 个品牌 App — 编译期 --dart-define 注入品牌+环境
  2. 响应式登录守卫 — Riverpod 写状态 + ValueNotifier 通知路由
  3. 双方案屏幕适配 — 手机用 375 设计稿 + 平板回退原始尺寸 + 字体封顶
  4. 两级主题模型 — 种子色自动生成色板 + 页面细节配置
  5. 生产级 Dio 封装 — 拦截器/统一异常/重试/取消/进度
  6. OSS 客户端直传 — V4 签名 + PUT 直传 + 进度
  7. Isar 响应式数据库 — watch 驱动 UI 自动刷新
  8. 28 个 Demo 全覆盖 — 从 Text 到 OSS,每个都有配套文档

11.4 配套文档

  • Demo 文档索引:docs/demos/README.md
  • 试卷架构设计:docs/exam_architecture.md
  • 每个 Demo 都有六章结构文档:问题背景 / 运行方式 / 核心知识点 / 关键代码剖析 / 踩坑记录 / 扩展思考

结语:Flutter 的学习不是背 API,而是理解「Widget 是配置、Element 是实例化、RenderObject 是渲染」这条主线。所有的高级用法 — 状态管理、路由守卫、主题换肤 — 都是建立在这条主线上的。本项目 28 个 Demo 覆盖了从基础组件到生产级封装的完整路径,建议大家按顺序跑一遍,每个 Demo 都有配套文档。