源码解析与贡献指南:从 Haskell 架构到全栈应用生成)
Wasp 编译器waspc源码解析与贡献指南从 Haskell 架构到全栈应用生成【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/waspwaspc是 Wasp 全栈框架的核心编译器与 CLI它以 Haskell 实现编译管线将声明式 Wasp 配置与src/目录中的 JS/TS 代码转换成一整套由 React 客户端、Node.js/Express 服务端与 Prisma 数据库组成的可运行 Web 应用。本文基于 waspc/README.md 的完整贡献者指南结合仓库内run脚本、waspc.cabal与核心 Haskell 源码系统讲解 waspc 的架构、开发环境搭建、构建测试流程、分支与发布策略以及代码规范帮助你快速上手这一编译器的源码开发。waspc 是什么编译器、CLI 与项目运行器的三位一体在 Wasp 项目中waspc指的是waspCLI/编译器的 Haskell 源码目录但构建产物的可执行文件名并不是waspc。这一点很容易混淆README 特别做了说明正式发布时可执行文件名为wasp开发阶段的可执行文件名为wasp-cli用于与已安装的发布版区分。从 waspc.cabal 可以看到可执行文件定义为executable wasp-cli入口为cli/exe/Main.hs它依赖cli-lib与waspc两个 Haskell 库。因此从产物角度看wasp二进制同时承担了三重角色编译器把 Wasp 声明编译成 Web 应用、CLInew、start、db、build、deploy等命令入口见 cli/exe/Main.hs 中的命令分发逻辑、以及Wasp 项目运行器wasp start时编译并启动开发环境。# 查看当前 waspc 版本号 ./run get-waspc-version仓库当前waspc.cabal中的版本为0.26.0waspc.cabal版本号管理遵循 SemVer 语义详见后文发布策略一节。第一份贡献清单快速上手指南README 为第一次向 waspc 提交代码的贡献者提供了一份四步清单阅读本文的 Codebase overview 快速了解编译器的整体结构成功编译项目并跑通kitchen-sink示例应用见下文 Basics加入社区 Discord打个招呼挑选标有good first issue的 issue主动提出工作计划并提问随后创建面向main分支的 PR参考 Typical workflow 与 Branching and merging strategy。第 4 步是整个贡献闭环的关键PR 的质量门禁是 CI 全部通过因此建议在提交前完整执行一遍 Code analysis 中介绍的各项检查。waspc 架构总览Analyzer 与 Generator 两层管线Wasp 编译器用 Haskell 实现代码库分为**库src/**与CLIcli/src/为库、cli/exe/为薄可执行包装两部分。CLI 是实际的wasp可执行文件绝大部分逻辑在库中。从 src/Wasp/Analyzer.hs 的模块说明可以看到编译管线分为两层Analyzer前端解析 Wasp 声明并做类型检查与求值。历史上它解析.waspDSL 文件但该解析器已被移除改为分析 TypeScript 配置文件Wasp.Project.WaspFile.TypeScript如今 Analyzer 保留的解析 → 类型检查 → 求值机制被复用于从 Prisma schema 推导实体声明Analyzer.Prisma将 schema 转为 AST 实体语句Analyzer.TypeChecker为 AST 补充类型信息Analyzer.Evaluator把类型检查后的 AST 转换为 Wasp AST。Generator后端接收 Analyzer 产出的中间表示AppSpec定义于 src/Wasp/AppSpec.hs基于它决定如何生成 Web 应用。AppSpec是贯穿前后两层的中央 IR。从 src/Wasp/Generator.hs 可以观察到 Generator 的组装方式——genApp依次拼接多个子生成器genApp :: AppSpec - Generator [FileDraft] genApp spec do warnOverriddenDeps spec genServer spec genSdk spec genDb spec genDockerFiles spec genTypeAugmentation spec genWaspLibsREADME 中描述的 WebAppGenerator / ServerGenerator / DbGenerator 三个主生成器在实现中体现为Wasp.Generator.WebAppGenerator、Wasp.Generator.ServerGenerator与Wasp.Generator.DbGenerator三个模块见 waspc.cabal 的 exposed-modules 列表。一个关键设计理念是Generator 本身不写文件。它的输出是FileDraft列表每个 FileDraft 描述如何在磁盘上创建某个文件如CopyFileDraft、CopyLibDraft、TemplateFileDraft、TextFileDraft等类型见 src/Wasp/Generator/FileDraft 目录。最终由synchronizeFileDraftsWithDisk在 src/Wasp/Generator.hs 的writeWebAppCode中调用将草稿写入磁盘完成应用生成。FileDrafts 大量使用 Mustache 模板存放于data/Generator/templates/而生成的应用还依赖 Wasp 自带的内部 npm 包WaspLibs这些包随 Wasp 一起打包并安装进生成的应用中。上图展示了完整的编译数据流输入Wasp 配置与src/资源→ AnalyzerParser → TypeChecker → Evaluator产出AppSpec→ Generator 结合 Templates 驱动 Client/Server/DB 三个子生成器产出 FileDraftFD→ 写入磁盘生成 Web 应用。生成出的 Web 应用技术栈为客户端使用 React TanStack Querytanstack/react-query服务端使用 Node.js Express数据库层通过 Prisma 抽象。wasp start会先编译应用、在.wasp/out/目录生成 JS 代码然后分别对 client 与 server 执行npm start并启动数据库此后任何对 Wasp 源码的修改都会触发重新编译client/server 的npm start会自动拾取变化。从零搭建 waspc 开发环境仓库准备与run脚本克隆仓库后进入waspc/目录并确保处于main分支。README 特别强调./run脚本waspc/run封装了 waspc 开发中最常用的命令地位等同于 npm 项目中package.json的scripts既是快捷入口也是开发方式的文档。不带参数运行./run会打印全部可用命令的帮助信息。可以为其创建 bash 别名方便调用alias wrun/path/to/your/wasp-lang/wasp/waspc/run[!IMPORTANT]./run脚本及配套开发工具是 Bash 脚本在 Windows 的 PowerShell 或 Command Prompt 中无法直接使用。从 waspc/run 源码可以看到脚本的结构COMMAND${1:-watch}默认执行watch类命令每个子命令build、test、wasp-cli、stan、hlint等通过case分支映射到对应的底层命令。例如build实际执行# waspc/run 中的 BUILD_ALL_CMD node tools/packages/build.ts node tools/libs/build.ts cabal build all而wasp-cli命令则通过tools/wasp-cli-dev脚本waspc/tools/wasp-cli-dev以cabal -v0 --project-dir... run wasp-cli -- $的方式运行本地构建的 CLI这样测试与开发调用总能反映waspc源码的最新状态无需事先cabal install。构建、测试与运行 CLI按顺序执行以下三条命令即可完成第一轮构建 → 验证 → 运行# 1. 构建整个 waspc 项目Haskell TS 包 WaspLibs ./run build # 2. 确保所有测试通过 ./run test # 3. 运行本地构建的 wasp-cli无参数会打印帮助/用法 ./run wasp-cli首次执行./run build可能需要约 10 分钟因为要下载全部依赖之后会被缓存。开发期可执行文件名为wasp-cli与正式安装后的wasp相区分——这正是方便开发者区分开发版 CLI与发布版 CLI的设计。跑通 kitchen-sink 示例应用examples/kitchen-sink/examples/kitchen-sink是 Wasp 团队始终维护的示例应用用于手工验证对编译器的改动也是新增功能的首选验证场。跑通它的步骤# 1. 进入示例目录 cd examples/kitchen-sink # 2. 启动开发数据库保持运行不要关闭该终端 ./run wasp-cli db start # 3. 新开一个终端更新数据库 schema ./run wasp-cli db migrate-dev # 4. 准备服务端 mock 环境变量 cp .env.server.example .env.server # 5. 以开发模式启动示例应用 ./run wasp-cli start首次执行第 5 步可能需要一分钟左右下载并安装 npm 依赖完成后浏览器会自动打开新标签页并展示 Kitchen Sink 应用。[!NOTE]kitchen-sink中的部分功能不会工作因为.env.server里只是 mock 值。关于如何配置开发环境变量请查看kitchen-sink自己的 READMEexamples/kitchen-sink/README.md。典型开发工作流从分支到合入README 给出了一套完整的迭代流程结合 waspc/run 可以还原每一步的实际命令从main创建功能分支。若 IDE 没有可靠的 HLSHaskell Language Server强烈建议配置在waspc根目录运行./run ghcid它会监视 Haskell 项目并持续报告编译错误保持运行即可。ghcid需先全局安装cabal install ghcid。修改代码最常见于src/、cli/src/或data/目录并视情况更新测试。对照 HLS/ghcid修复错误循环迭代。用./run build:hs单独构建 Haskell/cabal 项目./run wasp-cli则会构建并运行。若改动涉及data/packages/还需执行./run build:packages详见下文 TypeScript 包章节嫌慢的话可以一步到位执行./run build同时构建 Haskell、TS 包等所有部件。手工验证改动时使用kitchen-sink保持更新新增功能应同时加入该应用及其测试。执行./run test确认全部测试通过。若 waspc e2e 快照测试出现预期变更用./run test:waspc:e2e:accept-all接受新基线。若做了 bug 修复、新功能或破坏性变更在Changelog.mdwaspc/ChangeLog.md中补充简要说明必要时同步提升waspc.cabal与ChangeLog.md中的版本号版本确定方法见 Determining next version。创建 PR紧盯 CI——一切必须通过。若 PR 改变了用户使用 Wasp 的方式同步更新同仓库web/docs下的文档。与 reviewer 协作迭代直至 PR 获批。Reviewer 将分支合入main。团队内部成员的注意事项不要在你的 PR 获批前更新 waspc e2e 测试。过早接受 e2e 测试会拖慢 UI、给 reviewer 制造噪音并降低你在所有评审迭代后仔细核对最终 diff 的可能性。接受 waspc e2e 测试 diff 前务必仔细审查。深入 waspc 的关键目录README 列出waspc/内的核心目录结合 waspc.cabal 可以对应到实际的 Haskell 模块src/→ 主源码库对应 cabal 的library段cli/src/→ CLI 库代码对应library cli-lib段实现Wasp.Cli.Command.*系列命令模块如Wasp.Cli.Command.Db.Migrate、Wasp.Cli.Command.Deploy、Wasp.Cli.Command.News等见 waspc.cabalcli/exe/→ 可执行文件薄包装Main.hs对应executable wasp-cli段tests/、e2e-tests/、cli/tests/、starters-e2e-tests→ 各类测试data/Generator/templates/→ 生成 client/server 的 Mustache 模板data/Generator/libs/→ 随 Wasp 打包并复制进生成应用的内部 npm 包WaspLibsdata/packages/→ Wasp 编译器自身使用的 TypeScript 包data/Cli/starters/→ 新项目的 starter 模板。TypeScript 包Haskell 与 TS 的协作waspc虽以 Haskell 实现但部分功能如解析 TS 代码、部署脚本依赖 TypeScript。Haskell 代码将这些 TS 包作为独立进程运行通过输入/输出流通信。这些包位于 waspc/data/packages/README.md 所述的data/packages/中是标准 npm 项目deploy、prisma、spec、studio等包可在 waspc/waspc.cabal 的data-files中找到对应条目。要让 Haskell 代码正确使用这些 TS 包并在发布 tarball 中正确打包需要它们在waspc_datadir目录中被正确安装/构建。开发中每当这些包有改动就运行./run build:packagesCI 构建 release 时也会执行同样的操作。data/packages/README.md还说明了新增包的规范包目录需在package.json中提供build脚本和调用编译产物的start脚本并在waspc.cabal的data-files中登记packages/name/package.json、package-lock.json及dist/**/*.js等文件。WaspLibs生成应用的构建块WaspLibs 是 Wasp 自有的 npm 包位于 waspc/data/Generator/libs/其中包含会进入生成应用的核心逻辑。READMEwaspc/data/Generator/libs/README.md明确了两条向生成应用注入代码的途径在waspc/data/Generator/templates/写 Mustache 模板在 libs 目录中开发真正的 JS 项目。模板不是真正的 JS 工程含 Mustache 语法无法写测试、无法类型检查而 libs 是真正的 JS 工程可以写测试并被类型检查。理想做法是大部分逻辑放在 libs 中模板只负责产出配置对象并编排这些 lib。WaspLibs 的版本跟随 Wasp 编译器版本如0.19.2被视为 Wasp CLI 的实现细节。libs 的导出遵循按运行时区分的命名约定导出路径Node.js浏览器说明.✅✅两种运行时通用./node✅❌仅 Node.js 运行时./browser❌✅仅浏览器运行时例如authlib 的package.json中可通过exports字段暴露wasp.sh/lib-auth通用、wasp.sh/lib-auth/nodeNode 专用与wasp.sh/lib-auth/browser浏览器专用三个导入路径。开发 lib 时运行./run build:libs将编译产物复制进data/由于 npm 按版本缓存已安装包而 lib 版本不变必要时在 Wasp 应用根目录执行./run bust-libs-cache清理所有wasp.sh/lib-*的锁文件条目、重新编译并重装依赖。测试体系Tasty、Hspec、QuickCheck 与快照测试waspc 的 Haskell 测试基于Tasty测试框架它允许把多种类型的测试组合进同一个测试套件。测试以测试树形式组织可递归分组、混合 hspec / QuickCheck 等不同类型。为避免手工组织测试文件与导入项目使用tasty-discover自动发现测试它自动识别包含测试的文件并组装成测试树。测试函数需要特殊前缀来标明类型spec*表示 Hspec 测试、prop*表示 QuickCheck 属性测试等也可以手动组织 Tasty 测试树并用test_前缀让 tasty-discover 拾取。目前自动发现被限制为只匹配*Test.hs结尾的文件。三个测试框架各司其职Hspec用于单元测试QuickCheck用于属性测试doctest用于测试文档中的代码示例。所有测试放入tests/目录Haskell 构建工具不擅长把测试与源码混放。在 waspc.cabal 中可以看到三个测试套件waspc-tests单元测试入口tests/TastyDiscoverDriver.hs、wasp-cli-testsCLI 测试与waspc-e2e-testse2e 快照测试入口e2e-tests/Main.hs依赖tasty-golden。运行测试的完整命令家族如下./run test # 运行所有测试 ./run test:waspc # 仅 waspc 测试单元 e2e ./run test:waspc:unit # 仅 waspc 单元测试 ./run test:waspc:unit Some test description to match # 按描述模式运行单个单元测试 ./run test:waspc:e2e # 仅 waspc e2e 测试 ./run test:cli # 仅 Wasp CLI 测试 ./run test:kitchen-sink # kitchen-sink e2e 测试 ./run test:examples # 所有 examples e2e 测试 ./run test:starters # starter 模板 e2e 测试从 waspc/run 可以看到底层实现test:waspc:unit执行cabal test waspc-teststest:waspc:e2e执行cabal test waspc-e2e-teststest:cli执行cabal test wasp-cli-teststest:examples则循环遍历examples/tutorials/TodoApp、examples/tutorials/TodoAppTs、examples/waspello、examples/waspleau、examples/websockets-realtime-voting、examples/ask-the-documents、examples/kitchen-sink七个示例目录逐一执行wasp-cli install npm run test。waspc e2e 快照测试黄金输出的评审机制waspc e2e 测试中有一类快照测试对若干准备好的项目运行wasp-cli验证它们能成功运行并将生成的应用与期望的生成结果golden 输出做对比。当你修改了影响生成应用的代码时快照测试会失败并展示新旧输出的 diff。这给了你观察差异、确认其符合预期的机会。审查 diff 是 PR 作者外部贡献则由 reviewer的责任——不要盲目接受变更务必确认与预期修改一致。确认满意后用便捷命令接受新基线./run test:waspc:e2e:accept-all从 waspc/run 看该命令的实现是删除当前 golden 输出 → 重跑快照测试生成新 golden。代码质量检查格式化、lint 与静态分析提交 PR 前务必让以下检查全部通过——它们同样是 CI 的门禁./run code-check该命令会依次执行代码格式化检查、lint 与静态分析并输出汇总报告waspc/run 的code-check分支分别记录 prettier / ormolu / cabal-gild / hlint / stan 五项结果。目前 CI 只检查代码格式化lint 与静态分析暂未纳入 CIwaspc/run 中的 TODO 注释说明未来会补上。格式化Ormolu cabal-gild PrettierHaskell 代码使用Ormolu格式化通常在编辑器里配置保存时自动运行也可手动执行./run check:ormolu # 检查是否存在需要格式化的文件 ./run format:ormolu # 就地格式化所有需要格式化的文件此外.cabal文件由 cabal-gild 格式化仓库整体代码由 Prettier 格式化./run check:cabal / ./run format:cabal ./run check:prettier / ./run format:prettier ./run check # 全部 formatter 的 check 模式 ./run format # 全部 formatter 就地格式化[!NOTE] 首次运行 Ormolu 相关命令时安装依赖可能耗时约 10 分钟后续运行会快得多。Linthlint使用hlint对 Haskell 代码做 lint./run hlint静态分析stan使用stan对代码库做静态分析waspc/run 显示stan会先完整构建项目再运行分析./run stan该命令会构建代码库、以正确的 GHC 版本安装并运行 stan结果输出到 CLI 并生成stan.html报告。首次运行同样需要约 10 分钟安装依赖。分支与合并策略main 与 release 的双轨模型本仓库同时包含构成 Wasp 发布的源码waspc/以及包含文档与博客的网站web/。为兼顾新功能开发与线上文档/热修复仓库采用最小化分支策略main承载所有活跃开发的新功能与配套文档更新可包含未发布的内容但合入main的任何改动都应处于可发布状态。这是功能分支的默认目标分支。release包含当前/最近 Wasp 发布的源码以及已发布、网站可见的文档与博客。完整发布基于main制作新版本的流程是先把release合并进main可能有冲突但易解决再从更新后的main制作 waspc 发布随后把main合并回release上一步已解决冲突故无冲突。如何选择 PR 目标分支需要立刻或很快发布的改动网站内容、博客、文档热修复、需要以新 patch 版本快速发布的编译器热修复等→ 目标为release不紧急、可等到下一个常规 Wasp 发布的内容新功能、重构、配套文档→ 目标为main。一句话总结release代表当下面向已发布内容的改动main代表近期未来面向待发布内容的改动。[!IMPORTANT] 若向release合并 PR 或推送任何改动系统会自动创建一个把release变更同步到main的 PR并分配给你负责合入。CI 与发布流程项目使用 GitHub Actions 作为 CI在main分支的任何提交、PR 以及以v开头的 tag 上运行。CI 在 Linux、macOS 与 Windows 上构建并测试 Wasp 代码。提交以v开头的 tag 时会创建包含二进制包的 GitHub draft release并将 Wasp npm 包上传到 npm 的暂存队列须经维护者批准才会发布。另有部署示例应用到 Fly.io 的 workflowrelease-examples-deploy.yaml可在 GitHub UI 手动触发通常应在发布新版本后从release分支运行确保已部署示例使用最新稳定版 Wasp。也可用 GitHub CLI 触发# 使用最新 Wasp 版本部署 gh workflow run release-examples-deploy --ref release # 使用指定版本部署 gh workflow run release-examples-deploy --ref release -f version0.13.2提交信息中包含[skip ci]可跳过 GitHub Actions。仓库还提供new-release脚本辅助发版传入新版本号./new-release 0.3.0它会检查一切就绪、创建对应 tag 并推送从而触发 CI 在 GitHub 上创建新 release。典型发布流程以下步骤中带 的步骤是每次waspc发布都必须执行的其余步骤视改动情况决定例如没有破坏性变更时可跳过部分步骤确保 OpenSaaS 已更新到最新 Waspmain手动运行其 e2e workflow。 确保已将release分支的变更合并进main。 拉取远端全部变更git fetch确保本地main是最新。 从要发布的最后一个提交通常是最新main创建名为rc-version的 RC 分支如rc-0.24.1后续步骤都在该分支进行。waspc.cabal中的版本号应已正确但需复核并按需更新若修改了waspc.cabal创建 PR、等待审批与 CI 通过再 squash 合并进 RC 分支。 创建 RC 发布并做测试与修复见下文 Test releases一切就绪后继续。 检查自上次发布以来的提交斟酌完善ChangeLog.md与迁移指南。若是主版本更新在 RC 分支上运行npm run docusaurus docs:version {version}为当前文档拍版本快照在web/目录提交到 RC 分支并推送。 切换到release分支通过git merge --ff rc-version快进到 RC 分支。 准备正式发布时确保处于release分支运行./new-release 0.x.y脚本会做若干检查、打 tag 并推送。 等待新 tag 的 CI 完成且成功tag 推送自动触发完成后创建 GitHub draft release 并把 npm 包放到暂存队列。 找到 draft release编辑发布说明通常直接粘贴对应版本的 ChangeLog 条目就绪后发布。 批准暂存的 npm 包npm stage list查看 stage IDnpm stage approve stage-id逐一批准需要 2FA。 推送本地release分支到远端。 你会在自动生成的把release合并回main的 PR 中被 也可在 PR 列表按head:release base:main过滤找到务必合并它——用 merge commit不要 squash 或 rebase确保main领先于release避免未来发布产生合并冲突。通过release-examples-deployworkflow 把示例应用部署到 Fly.io。若有文档改动从release分支发布新版本文档。在 Discord 宣布新版本。版本号如何确定waspc遵循标准 SemVer 的major.minor.patch方案。由于尚未到 1.0遵循约定将major保持为 0破坏性变更时提升minor。测试发布RC制作测试发布尤其是 Release Candidate可以在不打扰普通用户的情况下先行验证。步骤从要发布的最后一个提交通常是最新main创建rc-version分支。本地执行new-release脚本并在版本号后追加-rc.N以标明预发布性质如./new-release 0.24.1-rc.1脚本会给出一些可接受的警告。draft release 创建后用 UI 将其标记为 pre-release 并发布——这是区分测试发布与正式发布的关键一步会自动取消latest release的勾选同时把rc-version分支推送到远端。批准暂存的 npm 包npm stage listnpm stage approve stage-id需 2FA。显式指定版本安装包进行验证npm i -g wasp.sh/wasp-cli0.24.1-rc.1创建 checklist 并走查发布前事项发现问题就在rc分支上修复并照同样流程创建下一个 RC如0.24.1-rc.2。文档、Haskell 实践与依赖策略外部文档面向 Wasp 用户的文档托管于官网其源码就在本仓库的 web/docs 目录中与网站、博客同处web/下。Haskell 最佳实践项目维护独立的 Haskell Handbook 记录相关最佳实践。Cabal 依赖冻结项目刻意不使用 cabal freeze 文件来锁定依赖而是借助 cabal 的index-state特性从 Hackage 获得包版本固定以更好地支持更广泛的开发者操作系统waspc/cabal.project 中可查看实际配置。代码风格指南注释规范注释以大写字母开头时必须以标点结束不以大写开头则不应以标点结尾。TODO / NOTE 一律全大写-- TODO: Wash the car. -- NOTE: This piece of code is slow.可在 TODO / NOTE 后附加作者名便于读者在需要时咨询原作者-- TODO(martin): Doesnt work on my machine in some unusual use cases.JavaScript 函数风格顶层具名函数优先使用函数声明语句而非箭头函数或函数表达式// good function foo(param) { // ... } // bad const foo (param) { // ... }; // bad const foo function (param) { // ... };内联函数表达式优先使用箭头语法// good const squares arr.map((x) x * x); // bad const squares arr.map(function (x) { return x * x; });设计文档RFC复杂功能的标准流程如果实现的功能因设计或技术实现而复杂README 推荐先写一份设计文档RFC。做法是提交一个 PR在wasp/docs/design-docs下新增 Markdown 文档阐述功能实现的思路与决策。其他人会在文档上评论经过必要迭代并获得批准后再在单独的 PR 中开始实现。结语waspc 以Analyzer 产出 AppSpec、Generator 产出 FileDraft的两层管线将声明式配置精确转换为全栈应用其工程实践Bash 驱动的run开发工具链、Tasty 多框架测试体系、快照 golden 评审机制、双分支发布模型本身就是一份高质量的编译器项目范本。无论你是想修复 bug、添加新功能还是仅仅想理解一个真实 Haskell 编译器如何工作都可以从./run开始沿着本文的路径一步步深入。【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考