Faust.js实战:基于Next.js的无头WordPress架构开发指南
1. 项目概述当WordPress遇见现代前端如果你和我一样在过去几年里深度参与过WordPress项目尤其是那些对前端交互和用户体验有较高要求的项目那你一定对“Headless WordPress”这个概念不陌生。简单来说就是把WordPress纯粹当作一个内容管理系统CMS通过其提供的REST API或GraphQL API将内容“喂”给一个独立的前端应用这个前端应用可以是React、Vue、Next.js等任何现代框架构建的。这种架构解耦了内容管理和内容呈现让前端开发者获得了前所未有的自由也让WordPress这个“老牌劲旅”焕发了新生。然而自由往往伴随着复杂性。当你真正开始一个无头WordPress项目时一系列问题会接踵而至如何高效地从WordPress API获取并结构化数据如何在Next.js等框架中实现服务端渲染SSR或静态生成SSG以优化SEO和性能如何处理预览、重定向、菜单、分类法等WordPress原生功能更棘手的是如何管理前端的状态并让它与WordPress的后端状态如用户认证、草稿预览保持同步这些问题如果全部从零开始解决工程量巨大且容易陷入重复造轮子的境地。这就是Faust.js出现的背景。它不是另一个WordPress主题或插件而是一个基于React的框架专门为构建无头WordPress的前端应用而生。由知名的WordPress托管服务商WP Engine孵化和维护Faust.js的目标很明确为开发者提供一套开箱即用的工具和最佳实践将Next.js或其他React框架与WordPress无缝连接起来大幅降低无头架构的入门门槛和开发复杂度。它抽象了数据获取、路由、预览等通用逻辑让你能更专注于构建独特的前端体验。2. 核心架构与设计哲学拆解要理解Faust.js的价值我们需要深入其设计核心。它不是一个单体库而是一个围绕几个关键包构建的生态系统每个包解决一个特定领域的问题。2.1 核心包解析各司其职的模块化设计Faust.js采用模块化设计主要包含以下几个核心包faustjs/corefaustjs/react这是Faust.js的心脏和大脑。core包提供了与WordPress交互的基础客户端和工具函数而react包则提供了React特定的钩子Hooks和上下文Context例如useQuery、usePost、usePosts等。这些钩子内部封装了GraphQL请求让你能以声明式、React风格的方式获取WordPress数据无需手动管理fetch和状态。faustjs/next这是为Next.js框架量身定做的适配器。它提供了关键的getWordPressProps函数和WordPressTemplate组件。getWordPressProps可以在getStaticProps或getServerSideProps中调用用于在构建时或请求时从WordPress获取页面所需的数据。WordPressTemplate则是一个智能组件能根据当前路由自动匹配并渲染对应的WordPress模板如单篇文章、页面、归档页。faustjs/cli命令行工具用于项目初始化、代码生成等。最常用的功能是faust wp命令它允许你在本地前端开发环境中安全地代理到远程或本地的WordPress站点的GraphQL API并处理认证用于预览极大简化了开发配置。wp-plugin-faust这是一个必须安装在WordPress站点上的插件。它的作用至关重要扩展WPGraphQLFaust.js重度依赖WPGraphQL一个为WordPress提供GraphQL API的插件。wp-plugin-faust插件扩展了WPGraphQL的Schema添加了Faust.js所需的特定字段和类型例如用于前端路由的URI信息。启用预览功能它设置了必要的路由和端点使得当你在WordPress后台点击“预览”按钮时能够正确地重定向到你的Next.js前端开发服务器或生产环境并携带认证信息让你能看到未发布的草稿内容。管理重定向可以处理从WordPress管理后台设置的重定向规则。这种模块化设计的好处是清晰的关注点分离。你可以根据项目需求选择组合。例如如果你不使用Next.js而用Vite React你依然可以使用faustjs/react来获取数据只是需要自己处理路由和SSR。2.2 数据流与状态管理GraphQL驱动的声明式交互Faust.js的数据流设计非常现代化核心是GraphQL。它不直接使用WordPress的原生REST API而是通过WPGraphQL这个中间层。GraphQL的优势在于强类型和按需查询前端需要什么数据就查询什么避免了REST API的过度获取或多次往返请求。在Faust.js应用中数据流大致如下查询定义使用GraphQL查询语言定义你需要的数据结构。钩子调用在React组件中使用Faust.js提供的钩子如usePost并传入查询。自动获取钩子内部向配置的WPGraphQL端点发起请求。状态注入返回的数据会自动成为组件状态的一部分并且类型安全如果配合TypeScript和代码生成工具。渲染组件使用这些数据渲染UI。对于状态管理Faust.js巧妙地利用了React自身的Context和Hooks。它提供了一个顶层的FaustProvider将WordPress站点配置、认证状态等全局信息注入到应用上下文中。所有数据获取钩子都依赖于这个上下文。这意味着你通常不需要引入额外的状态管理库如Redux来处理WordPress数据Faust.js已经为你管理好了请求、缓存和更新。注意Faust.js管理的是与WordPress相关的服务器状态。你应用内的纯客户端交互状态如表单输入、UI开关状态仍然需要根据情况使用React的useState、useReducer或其他状态管理方案。2.3 路由与模板系统Next.js与WordPress的桥梁这是Faust.js最精妙的设计之一它在前端路由Next.js和后端内容结构WordPress之间建立了一座动态的桥梁。在传统的Next.js项目中路由由文件系统决定pages/about.js对应/about。但在无头WordPress中页面的路径是由WordPress管理的一个“关于我们”的页面其链接可能是/about、/about-us或任何自定义的固定链接。Faust.js的解决方案是引入一个“捕获所有”的路由。通常你会在pages/[[...path]].js中创建一个“万能”页面。在这个页面里使用getStaticProps或getServerSideProps配合getWordPressProps根据当前路径context.params.path去查询WordPress获取对应的页面或文章数据。使用WordPressTemplate组件。这个组件会根据获取到的数据内容类型Post、Page、Category等自动映射并渲染一个对应的React组件模板。例如你可以创建components/wordpress/Post.js和components/wordpress/Page.js。当访问一篇博客文章时WordPressTemplate会查找并渲染Post组件当访问一个普通页面时则渲染Page组件。这种设计完美复现了WordPress主题的模板层级template hierarchy概念但用的是React组件给予了开发者极大的灵活性。3. 从零开始实战搭建Faust.js项目理论说得再多不如动手实践。让我们一步步搭建一个基于Faust.js和Next.js的无头WordPress前端。3.1 环境准备与WordPress端配置前置条件一个已安装的WordPress站点本地如Local by Flywheel或远程服务器。Node.js (版本需符合Faust.js要求通常LTS版本即可)。对Next.js和React有基本了解。WordPress端配置必须步骤安装并激活WPGraphQL插件在WordPress插件市场搜索“WPGraphQL”并安装激活。这是Faust.js运行的基础。激活后你应该能通过https://your-wp-site.com/graphql访问GraphQL端点并看到一个GraphiQL IDE界面用于测试查询。安装并激活Faust.js的WordPress插件你可以通过WordPress插件库搜索“Faust”安装或者从Faust.js的GitHub仓库下载wp-plugin-faust的zip包手动上传安装。激活后进入插件设置页面。最重要的设置是“前端站点地址”这里需要填写你即将开发的Next.js应用的URL开发时是http://localhost:3000生产时是你的生产域名。这个地址用于预览功能的重定向。插件还会提供一个“Secret Key”用于前端应用连接时的认证特别是预览功能。请妥善保存。3.2 初始化Next.js前端项目打开终端开始创建前端项目# 使用Next.js官方脚手架创建一个TypeScript项目推荐 npx create-next-applatest my-faust-site --typescript --tailwind --app cd my-faust-site # 安装Faust.js核心依赖 npm install faustjs/core faustjs/react faustjs/next graphql接下来我们需要配置Faust.js。在项目根目录创建或编辑.env.local文件# .env.local NEXT_PUBLIC_WORDPRESS_URLhttps://your-wp-site.com FAUST_SECRET_KEYyour_secret_key_from_wp_plugin重要提示NEXT_PUBLIC_WORDPRESS_URL必须与WordPress站点的地址完全一致包括http/https。FAUST_SECRET_KEY必须与WordPress插件中生成的密钥一致且不要提交到公开的代码仓库。3.3 核心配置与Provider设置Faust.js需要一个全局配置和Provider。首先在项目根目录创建一个faust.config.js文件// faust.config.js import { setConfig } from faustjs/core; if (!process.env.NEXT_PUBLIC_WORDPRESS_URL) { console.error(NEXT_PUBLIC_WORDPRESS_URL is not set in environment variables.); } setConfig({ wpUrl: process.env.NEXT_PUBLIC_WORDPRESS_URL, apiClientSecret: process.env.FAUST_SECRET_KEY, });然后我们需要修改app/layout.tsx如果是Pages Router则在_app.tsx用FaustProvider包裹应用。同时我们需要创建一个客户端组件因为Provider需要使用到状态。创建一个新文件app/providers.tsx// app/providers.tsx use client; // 标记为客户端组件 import { FaustProvider } from faustjs/next; import { client } from /client; // 我们接下来会创建这个client import { WordPressTemplate } from faustjs/next/templates; export function Providers({ children }: { children: React.ReactNode }) { return FaustProvider client{client}{children}/FaustProvider; }现在创建关键的client实例。在项目根目录创建client.ts// client.ts import { createClient } from faustjs/core; import { getWordPressProps, WordPressTemplate } from faustjs/next/templates; // 导入你的GraphQL查询片段后续生成 import type { WordPressTemplate as WordPressTemplateType } from types; // 后续生成 // 创建Faust客户端实例 export const client createClient({ wpUrl: process.env.NEXT_PUBLIC_WORDPRESS_URL!, apiClientSecret: process.env.FAUST_SECRET_KEY!, }); // 导出一些工具函数和类型方便在页面中使用 export { getWordPressProps, WordPressTemplate }; export type { WordPressTemplateType };最后更新app/layout.tsx// app/layout.tsx import type { Metadata } from next; import { Inter } from next/font/google; import ./globals.css; import { Providers } from ./providers; // 导入Providers const inter Inter({ subsets: [latin] }); export const metadata: Metadata { title: My Faust.js Site, description: A headless WordPress site built with Faust.js, }; export default function RootLayout({ children, }: Readonly{ children: React.ReactNode; }) { return ( html langen body className{inter.className} {/* 用Providers包裹 */} Providers{children}/Providers /body /html ); }3.4 生成类型与GraphQL查询代码为了获得最佳的TypeScript开发体验Faust.js推荐使用代码生成工具根据你的WPGraphQL Schema自动生成类型定义和常用的React Hook。首先安装开发依赖npm install --save-dev faustjs/cli然后在package.json中添加一个脚本{ scripts: { generate: faust generate } }运行生成命令前确保你的WordPress站点可访问并且WPGraphQL插件已激活。然后执行npm run generate # 或 npx faustjs/cli generate这个命令会连接到你的NEXT_PUBLIC_WORDPRESS_URL。读取WPGraphQL Schema。在项目根目录生成一个wp-graphql.generated.ts文件包含所有GraphQL类型。在faustjs/next包内部更新预置的查询和钩子类型。现在你的编辑器应该能为usePost,useQuery等钩子提供智能补全和类型检查了。4. 核心功能实现与深度开发基础架子搭好后我们来实现核心的页面和功能。4.1 实现“捕获所有”路由与页面渲染这是Faust.js应用的主入口。在App Router下我们创建app/[[...path]]/page.tsx// app/[[...path]]/page.tsx import { getWordPressProps, WordPressTemplate } from /client; import { GetStaticPropsContext } from next; import { useRouter } from next/router; // 注意App Router中可能需要不同的导航钩子这里为示例 // 这个页面组件本身不需要内容它主要靠getStaticProps和WordPressTemplate工作 export default function Page() { // 你可以在这里添加一些所有页面共享的布局或逻辑 return null; // 实际渲染由WordPressTemplate在getStaticProps返回的props中处理 } export async function getStaticProps(ctx: GetStaticPropsContext) { // 使用 getWordPressProps 获取当前路径对应的WordPress数据 return getWordPressProps({ ctx, revalidate: 60 }); // 设置ISR每60秒重新验证 } export async function getStaticPaths() { // 对于SSG你可以在这里返回预渲染的路径 // 但对于动态内容通常返回空数组让Next.js在请求时按需渲染 return { paths: [], fallback: blocking, // 使用blocking fallback以获得更好的SEO和用户体验 }; }关键的魔法发生在getWordPressProps内部。它会解析ctx.params.path得到请求的URI。向WPGraphQL发起查询询问WordPress“这个URI对应什么内容”将获取到的内容数据如文章详情、页面详情、归档列表和内容类型Post、Page作为props返回。WordPressTemplate组件在providers.tsx中已配置会接收到这些props并自动渲染对应的组件模板。4.2 创建自定义模板组件现在我们需要创建那些被WordPressTemplate调用的具体模板组件。按照约定我们可以在components/wordpress目录下创建它们。首先创建单篇文章模板components/wordpress/Post.tsx// components/wordpress/Post.tsx import { usePost } from faustjs/next; import Link from next/link; export default function PostComponent({ id }: { id: string }) { // 使用usePost钩子传入文章ID获取文章数据 // 类型Post来自自动生成的类型定义 const post usePost({ id }); // 加载状态处理 if (!post) { return divLoading.../div; } return ( article classNamemax-w-4xl mx-auto py-10 header h1 classNametext-4xl font-bold mb-4{post.title}/h1 div classNametext-gray-500 mb-6 发布于 {new Date(post.date).toLocaleDateString()} | 作者{post.author?.node.name} /div {post.featuredImage?.node?.sourceUrl ( img src{post.featuredImage.node.sourceUrl} alt{post.featuredImage.node.altText || post.title} classNamew-full h-auto rounded-lg mb-8 / )} /header div classNameprose prose-lg max-w-none dangerouslySetInnerHTML{{ __html: post.content || }} / footer classNamemt-12 pt-6 border-t h3 classNametext-xl font-semibold mb-4分类/h3 div classNameflex flex-wrap gap-2 {post.categories?.nodes.map((cat) ( Link key{cat.id} href{/category/${cat.slug}} classNamebg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm hover:bg-blue-200 {cat.name} /Link ))} /div /footer /article ); }类似地创建页面模板components/wordpress/Page.tsx// components/wordpress/Page.tsx import { usePage } from faustjs/next; // 注意使用usePage钩子 export default function PageComponent({ id }: { id: string }) { const page usePage({ id }); if (!page) { return divLoading.../div; } return ( div classNamemax-w-4xl mx-auto py-10 h1 classNametext-5xl font-bold mb-8{page.title}/h1 div classNameprose prose-xl max-w-none dangerouslySetInnerHTML{{ __html: page.content || }} / /div ); }为了让WordPressTemplate能找到这些组件我们需要创建一个索引文件来导出它们。创建components/wordpress/index.ts// components/wordpress/index.ts export { default as Post } from ./Post; export { default as Page } from ./Page; // 后续可以继续添加 Category, Tag, Archive 等组件然后需要更新我们的client.ts或providers.tsx告诉Faust.js这些模板的位置。通常WordPressTemplate组件会通过上下文或配置来查找模板。在Faust.js的默认约定中它可能会在components/wordpress目录下查找与内容类型同名的组件。确保你的项目结构符合约定或者查阅最新文档进行自定义映射。4.3 实现预览功能预览是无头CMS的核心痛点Faust.js对此提供了优雅的解决方案。当你登录WordPress后台编辑一篇文章或页面并点击“预览”按钮时会发生以下流程WordPress上的Faust插件拦截预览请求。插件将你重定向到你在插件设置中填写的“前端站点地址”即你的Next.js应用并在URL中附加一个包含认证信息的预览令牌preview token。你的Next.js应用接收到请求getWordPressProps函数会识别出这是一个预览请求。它使用FAUST_SECRET_KEY和URL中的令牌向WordPress的GraphQL API发起一个经过认证的请求从而获取到未发布的草稿内容。数据返回后像正常内容一样渲染但可能会在UI上添加一个“预览模式”的横幅。要让预览工作关键检查点如下WordPress插件设置确保“前端站点地址”绝对正确。环境变量确保FAUST_SECRET_KEY在Next.js应用的环境变量中设置正确且与WordPress插件里显示的密钥一致。认证预览请求需要认证。Faust.js的faustjs/cli提供的开发代理 (faust wp) 或生产环境下的服务器端逻辑会处理这部分。在开发时使用npm run dev启动应用的同时最好也通过faust wp命令来代理API请求以确保认证头被正确附加。你可以在预览模板组件中添加一个提示横幅// 在Post或Page组件中 import { usePreview } from faustjs/next; export default function PostComponent({ id }) { const post usePost({ id }); const { isPreview, previewData } usePreview(); // 使用usePreview钩子 return ( {isPreview ( div classNamebg-yellow-500 text-white p-4 text-center sticky top-0 z-50 您正在预览模式下查看草稿内容。 a href/api/exit-preview classNameunderline ml-2退出预览/a /div )} {/* ... 其余内容 ... */} / ); }4.4 菜单、导航与自定义查询一个完整的网站离不开导航菜单。Faust.js提供了useMenu钩子来获取WordPress中创建的菜单。首先在WordPress后台“外观”-“菜单”中创建一个菜单例如“主菜单”并记住它的菜单位置Menu Location或名称Name。然后在前端组件中例如布局组件app/layout.tsx或一个专门的Header组件// components/Header.tsx use client; import { useMenu } from faustjs/next; import Link from next/link; export default function Header() { // 通过菜单位置或名称获取菜单。这里假设菜单位置是PRIMARY const { menuItems } useMenu({ location: PRIMARY }); if (!menuItems?.nodes) { return nav加载菜单中.../nav; } return ( header classNameborder-b nav classNamecontainer mx-auto px-4 py-4 flex justify-between Link href/ classNametext-2xl font-bold 我的站点 /Link ul classNameflex space-x-6 {menuItems.nodes.map((item) ( li key{item.id} Link href{item.url || #} classNamehover:text-blue-600 transition-colors // WordPress菜单项URL可能是绝对路径可能需要处理 {item.label} /Link /li ))} /ul /nav /header ); }对于更复杂的、超出Faust.js内置钩子范围的数据获取你可以直接使用client实例进行自定义GraphQL查询。这给了你最大的灵活性。// 示例获取最新3篇文章用于首页展示 import { gql } from apollo/client; // 或 graphql-tag import { client } from /client; async function getRecentPosts() { const query gql query GetRecentPosts { posts(first: 3) { nodes { id title excerpt uri featuredImage { node { sourceUrl } } } } } ; const result await client.query({ query, }); return result.data.posts.nodes; } // 在React Server Component或getStaticProps中调用5. 性能优化、部署与常见问题排查构建完成后如何让应用跑得更快、更稳5.1 缓存策略与增量静态再生Faust.js与Next.js的结合天然支持强大的缓存策略静态生成对于不常变化的内容如关于我们页面、历史博文在getStaticProps中获取数据并在构建时生成静态HTML。Faust.js的getWordPressProps可以无缝用于此场景。增量静态再生这是更推荐的方案。如上文示例在getStaticProps中设置revalidate: 60。这意味着页面在构建时静态生成但在60秒后如果有新的请求Next.js会在后台重新获取数据并生成新的静态页面用户下次访问时看到的就是更新后的内容。这完美平衡了性能和内容新鲜度。客户端缓存Faust.js的React钩子内部可能使用了类似React Query的缓存机制取决于版本对于同一数据的多次请求会在客户端内存中缓存避免重复网络请求。优化建议对首页、文章列表页使用较短的revalidate如10-30秒。对单篇文章页面使用中等长度的revalidate如60-300秒。对极少变化的页面如法律声明使用较长的revalidate或不重新验证。5.2 部署到生产环境部署一个Faust.js应用与部署一个普通的Next.js应用类似但有几个关键点需要注意环境变量确保在生产环境如Vercel, Netlify, AWS等中正确设置NEXT_PUBLIC_WORDPRESS_URL和FAUST_SECRET_KEY。FAUST_SECRET_KEY尤其敏感必须使用服务商提供的秘密环境变量功能不要暴露在客户端代码中。构建命令通常就是npm run build。Next.js会执行所有带有getStaticProps的页面的数据获取和静态生成。WordPress地址生产环境的NEXT_PUBLIC_WORDPRESS_URL必须是公网可访问的并且WPGraphQL端点 (/graphql) 允许从你的前端域名进行跨域请求。你可能需要在WordPress端配置CORS头或者使用一个反向代理来处理。预览功能生产环境的预览需要确保你的生产Next.js应用能够接收到来自WordPress的请求并且能够用相同的FAUST_SECRET_KEY进行认证。这通常意味着你的生产前端也需要运行在Node.js服务器环境如Vercel的Serverless Functions或支持动态渲染的托管服务上因为预览请求无法被纯静态站点处理。5.3 常见问题与解决方案实录在实际开发中你几乎一定会遇到下面这些问题问题一getWordPressProps返回null或notFound: true。排查检查WordPress站点是否可访问WPGraphQL插件是否激活。检查NEXT_PUBLIC_WORDPRESS_URL环境变量是否正确末尾不应有斜杠。在浏览器中直接访问https://your-wp-site.com/graphql看GraphiQL界面是否能打开并尝试执行一个简单查询如{ posts { nodes { id title } } }。检查你访问的前端路径在WordPress中是否存在对应的内容页面或文章。确保文章的“固定链接”设置正确。查看Next.js构建或运行时日志错误信息通常会显示GraphQL查询的具体错误。问题二预览功能不工作直接跳转到前端首页或显示404。排查核对密钥WordPress插件设置中的“Secret Key”与前端.env.local或生产环境变量中的FAUST_SECRET_KEY是否完全一致注意空格。核对前端URLWordPress插件中设置的“前端站点地址”必须精确匹配你的Next.js应用的访问地址包括http/https。开发环境在本地开发时确保使用faust wp命令启动开发服务器或者正确配置了本地代理以附加认证头。简单的npm run dev可能无法处理预览认证。检查控制台打开浏览器开发者工具的网络面板查看预览请求的响应。如果看到403错误通常是认证失败。问题三图片等媒体资源无法显示或路径错误。原因WordPress中的图片链接通常是绝对路径指向WordPress站点本身。在前端应用中直接使用这些链接如果域名不同可能会遇到跨域问题CORS或者在生产环境因为域名不同而加载失败。解决方案使用相对路径或重构URLWPGraphQL返回的sourceUrl是完整URL。你可以写一个工具函数将其转换为相对于前端域名的路径或者使用WordPress的“相对URL”插件但这并非最佳实践。推荐使用Next.js Image组件并配置远程图案这是最优化、最安全的方式。// 在 next.config.js 中配置 // next.config.js module.exports { images: { remotePatterns: [ { protocol: https, hostname: your-wp-site.com, // 你的WordPress域名 pathname: /wp-content/**, // 媒体文件路径 }, ], }, }; // 在组件中使用 import Image from next/image; Image src{post.featuredImage.node.sourceUrl} alt{post.title} width{800} height{450} sizes100vw style{{ width: 100%, height: auto }} /使用CDN将WordPress的媒体文件托管到CDN如Cloudflare, Imgix并在GraphQL查询或前端逻辑中替换URL域名。问题四GraphQL查询错误字段不存在。原因WPGraphQL的Schema可能没有包含你查询的字段。这通常是因为某些字段需要特定的插件如高级自定义字段ACF的支持或者WPGraphQL本身没有注册该类型/字段。排查打开https://your-wp-site.com/graphql使用GraphiQL的文档浏览器查看可用的查询和字段。如果你使用了ACF需要安装并激活WPGraphQL for Advanced Custom Fields插件。运行npm run generate重新生成类型看看TypeScript是否还报错。问题五网站性能感觉较慢尤其是首次加载。优化方向启用SSG/ISR尽可能使用getStaticProps和revalidate避免使用getServerSidePropsSSR处理所有请求。优化GraphQL查询只查询需要的字段避免过度获取。使用GraphQL的片段Fragments保持查询简洁。图片优化强制使用Next.js Image组件它提供自动的图片优化格式转换、尺寸调整、懒加载。代码分割Next.js默认支持基于路由的代码分割。确保大型组件或库是懒加载的。考虑CDN将整个Next.js应用部署在Vercel等全球CDN边缘网络上可以极大提升静态资源和API响应的速度。WordPress端缓存为WordPress站点安装对象缓存如Redis和页面缓存插件提升WPGraphQL的响应速度。Faust.js为无头WordPress开发提供了一个坚实而优雅的起点。它处理了那些繁琐的通用问题让你能专注于构建差异化的前端体验。虽然初期需要一些配置和理解其工作原理但一旦跑通开发效率会显著提升。它代表了WordPress现代化演进的一个关键方向即拥抱JAMStack和现代前端框架在不放弃强大内容管理能力的前提下获得极致的性能、灵活性和开发者体验。