尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

urql 服务端渲染(SSR)与数据水合实战:从 ssrExchange 双端复用到 Next.js App Router 集成

发布时间:2026/9/25 8:07:16

资讯中心
01
ARTICLE

urql 服务端渲染(SSR)与数据水合实战:从 ssrExchange 双端复用到 Next.js App Router 集成

urql 服务端渲染(SSR)与数据水合实战:从 ssrExchange 双端复用到 Next.js App Router 集成
前端【免费下载链接】urqlThe highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.项目地址https://gitcode.com/gh_mirrors/ur/urql点击查看免费下载在服务端渲染SSR应用中我们需要在服务端提前获取数据、把结果序列化下发到浏览器再由客户端利用这些数据完成水合hydration避免首屏重复请求。urql通过ssrExchange在服务端与客户端复用同一套 Exchange 机制实现了服务端捕获结果、客户端回放结果的双向能力。读完本文你将掌握ssrExchange的完整配置与底层原理能独立搭建 React含react-ssr-prepass两遍渲染、Preact、Next.jsApp Router 与 Legacypages两套方案以及 Vue 3 Suspense 下的 SSR 数据预取与水合方案。SSR Exchange一个 Exchange 承担服务端与客户端两种职责ssrExchange本质上是urql/core中一个普通的 Exchange却承担了两项职责服务端模式把应用渲染期间所有经过它的查询结果记录下来等待你通过extractData()取出并序列化客户端模式把服务端下发的序列化数据通过initialState或restoreData()注入回放出来让应用直接拿到结果而无需再次发起网络请求。从源码看这一机制实现在 packages/core/src/exchanges/ssr.ts 中服务端模式下forwardedOps$会经过一个tap把每个查询结果mutation 除外序列化后写入内部的data字典客户端模式下命中缓存的 operation 会被反序列化并直接产出OperationResult同时与转发出去的 operation 流合并输出。由于ssrExchange在回放阶段暂时取代了网络请求所以官方实现注释明确建议把它放在cacheExchange之后、任何 fetch 类 Exchange 之前见 ssr.ts 源码注释。基础配置isClient 与 initialState把ssrExchange加入Client的最小配置如下import { Client, cacheExchange, fetchExchange, ssrExchange } from urql/core; const isServerSide typeof window undefined; // The ssrExchange must be initialized with isClient and initialState const ssr ssrExchange({ isClient: !isServerSide, initialState: !isServerSide ? window.__URQL_DATA__ : undefined, }); const client new Client({ exchanges: [ cacheExchange, ssr, // Add ssr in front of the fetchExchange fetchExchange, ], });两个关键参数的含义isClient告诉 Exchange 当前处于服务端还是客户端。为true时处于客户端模式回放序列化结果为false时处于服务端模式捕获结果。文档示例用typeof window判断在 Webpack 等环境下也可以改用process.browser。值得一提的源码细节在 ssr.ts 中若未显式传入isClientExchange 会退化为!client.suspense作为默认值即非 Suspense 模式默认视为客户端。initialState传入服务端序列化好的数据。源码中它在ssrExchange创建时通过ssr.restoreData(params.initialState)注入ssr.ts因此等价于创建后立即调用一次restoreData。此外还有一个文档中未展开、但源码与测试均支持的参数includeExtensions是否把result.extensions一并序列化。由于 GraphQL 结果中的extensions常携带每次请求都会变化的非标准元数据ssrExchange默认不序列化 extensions只有显式设置该标志才会在serializeResult中写入extensions字段并在回放时解析见 ssr.test.ts 的 caches extensions when includeExtensionstrue 用例。如果你的 API 依赖extensions传递关键信息如 trace 数据需要打开它。序列化格式与 CombinedError 还原serializeResult不会直接序列化整个结果对象而是把data、extensions分别作为 JSON 字符串保存hasNext保留为布尔值以提高 JS 解析速度错误则被拆解为graphQLErrors保留message、path、extensions与networkError字符串见 ssr.ts。回放时由deserializeResult用JSON.parse还原数据并把错误重新包装成CombinedErrorssr.ts。测试用例验证了普通数据、错误结果、复杂 GraphQLError含path与extensions以及 extensions 都能被正确捕获与还原ssr.test.ts。从服务端取出数据extractData()服务端渲染完成后通过ssr.extractData()取回捕获的全部序列化结果再注入 HTML// Extract and serialise the data like so from the ssr instance // weve previously created by calling ssrExchange() const data JSON.stringify(ssr.extractData()); const markup ; // The render code for our framework goes here const html html body div idroot${markup}/div script window.__URQL_DATA__ JSON.parse(${data}); /script /body /html ;extractData()返回一个以Operation.key为键、SerializedResult为值的字典源码中为SSRData它只包含尚未被消费或失效的条目ssr.ts。注意序列化的data本身已经是 JSON 字符串外层再用JSON.stringify包一层是为了让浏览器端JSON.parse(${data})得到原始对象结构在服务端生成__URQL_DATA__后客户端用前面的initialState配置即可把它注入 Exchange。客户端注入restoreData()除了initialState也可以在客户端同步调用restoreData——前提是必须发生在client开始接收查询之前const isServerSide typeof window undefined; const ssr ssrExchange({ isClient: !isServerSide }); if (!isServerSide) { ssr.restoreData(window.__URQL_DATA__); }从源码看restoreData会遍历传入字典且拒绝覆盖已被置为null失效的键ssr.ts这保证了失效条目不会因为后续的restoreData调用而复活测试用例 never allows restoration of invalidated results 验证了这一点见 ssr.test.ts。回放语义与失效时机几个值得理解的运行细节只回放一次客户端模式下每次回放命中的结果都会进入一个invalidateQueue通过微任务延迟批量地把对应data[key]置为nullssr.ts。之所以延迟是为了避免并发查询互相删掉彼此的数据。回放完成后后续对同一 operation 的请求会正常走cacheExchange所以ssrExchange的缓存用完即弃。filter 分流转发流只放行teardown、无缓存条目、hasNext为真或requestPolicy network-only的 operation回放流则处理相反条件ssr.ts。这也解释了为什么cache-and-network策略在 SSR 回放下不会真正重新请求——除非显式使用network-only策略。回放结果的元信息回放产出的结果会被打上cacheOutcome: hit元数据见 ssr.test.ts 的断言开发者可用它做调试或统计。staleWhileRevalidate水合后立即刷新陈旧数据对于静态生成站点SSG / ISG首屏 payload 中的数据可能是陈旧的。staleWhileRevalidate开启后客户端回放陈旧结果的同时会立刻再发起一次network-only请求去更新数据const ssr ssrExchange({ isClient: !isServerSide, initialState: window.__URQL_DATA__, staleWhileRevalidate: true, });源码实现中回放分支会检查一个模块级revalidatedSet对每个 operation 的首次回放设置result.stale true并通过reexecuteOperation(client, op)触发一次网络请求ssr.tsreexecuteOperation来自 cache.ts。这一行为类似非 SSR 场景下的cache-and-network但两者并不等价因为回放结果来自ssrExchange本身而非真正的网络请求仅仅修改requestPolicy并不会触发重新拉取。开启该选项后重新验证期间结果会带有result.stale标记用于前端展示数据更新中的过渡态。React 服务端渲染用 react-ssr-prepass 做两遍渲染前面只是把ssrExchange接入ClientReact 场景下还需要在renderToString之前真正执行查询。urql提供了 React 的 Suspense 模式数据获取可中断渲染但 React 服务端渲染并不原生支持 Suspense。借助react-ssr-prepass可以在正式 SSR 前先预遍历一次元素树自动获取应用所需的所有数据——这就是常见的两遍渲染two-pass approach。首先安装依赖react-ssr-prepass对react-is和react有 peer 依赖yarn add react-ssr-prepass react-is react-dom # or npm install --save react-ssr-prepass react-is react-dom接着修改服务端代码把react-ssr-prepass放在renderToString之前import { renderToString } from react-dom/server; import prepass from react-ssr-prepass; import { Client, cacheExchange, fetchExchange, ssrExchange, Provider, } from urql; const handleRequest async (req, res) { // ... const ssr ssrExchange({ isClient: false }); const client new Client({ url: https://??, suspense: true, // This activates urqls Suspense mode on the server-side exchanges: [cacheExchange, ssr, fetchExchange] }); const element ( Provider value{client} App / /Provider ); // Using react-ssr-prepass this prefetches all data await prepass(element); // This is the usual React SSR rendering code const markup renderToString(element); // Extract the data after prepass and rendering const data JSON.stringify(ssr.extractData()); res.status(200).send( html body div idroot${markup}/div script window.__URQL_DATA__ JSON.parse(${data}); /script /body /html ); };两个要点必须开启suspense: true这会切换Client以支持 React Suspense。从源码看这也是ssrExchange默认推断isClient的依据isClient缺省时取!client.suspense见 ssr.ts因此服务端必须显式传isClient: false或依赖 Suspense 推断。prepass 先行renderToString 随后第一次遍历完成数据预取查询结果被ssrExchange捕获第二次正式渲染时useQuery能直接拿到结果从而得到完整的服务端 HTML。Preactpreact-ssr-prepass如果使用 Preact可以用react-ssr-prepass的替代包preact-ssr-prepass它只对 Preact 有 peer 依赖yarn add preact-ssr-prepass preact # or npm install --save preact-ssr-prepass preact上述所有 React 示例保持不变唯一区别是把urql包替换为urql/preact仓库中对应 packages/preact-urql提供Provider、useQuery等 API并把react-ssr-prepass替换为preact-ssr-prepass。Next.js App RouterNext 13urql/next如果使用 Next.js可以直接使用urql/next仓库中对应 packages/next-urqlpackage.json声明其面向 Next 13peer 依赖next 13.0.0、react 18.0.0、urql ^5.0.0。安装时需一并安装urql与graphql作为依赖yarn add urql/next urql graphql # or npm install --save urql/next urql graphqlurql/next提供两条使用路径Server ComponentRSC场景以及一般app/目录下的 Client Component 场景。在 Server Component 中直接查询urql/next/rscServer Component 场景从urql/next/rsc导入registerUrql实现见 packages/next-urql/src/rsc.ts其本质是用 React 的cache包装makeClient为每个 RSC 请求复用同一个Client实例// app/page.tsx import React from react; import { cacheExchange, createClient, fetchExchange, gql } from urql/core; import { registerUrql } from urql/next/rsc; const makeClient () { return createClient({ url: https://trygql.formidable.dev/graphql/basic-pokedex, exchanges: [cacheExchange, fetchExchange], }); }; const { getClient } registerUrql(makeClient); export default async function Home() { const result await getClient().query(PokemonsQuery, {}); return ( main h1This is rendered as part of an RSC/h1 ul {result.data.pokemons.map((x: any) ( li key{x.id}{x.name}/li ))} /ul /main ); }这里直接在 Server Component 里用getClient().query()获取数据配合createClient传入的url与exchanges配置即可。若应用中同一份数据既会被服务端渲染、又会被客户端使用文档建议不要把它作为 Server Component 渲染以便享受客户端缓存见下文从 Server Component 中失效数据。在 Client Component 中查询UrqlProvider ssrExchange不借助 Server Component 时需要做更多设置在 Client Component 的布局文件中创建Client与ssrExchange并通过UrqlProvider一并下发// app/client/layout.tsx use client; import { useMemo } from react; import { UrqlProvider, ssrExchange, cacheExchange, fetchExchange, createClient } from urql/next; export default function Layout({ children }: React.PropsWithChildren) { const [client, ssr] useMemo(() { const ssr ssrExchange({ isClient: typeof window ! undefined, }); const client createClient({ url: https://trygql.formidable.dev/graphql/web-collections, exchanges: [cacheExchange, ssr, fetchExchange], suspense: true, }); return [client, ssr]; }, []); return ( UrqlProvider client{client} ssr{ssr} {children} /UrqlProvider ); }关键点UrqlProvider必须同时接收client和ssr两个值。从 packages/next-urql/src/Provider.ts 的实现看它会把client注入urql的Provider把ssr注入SSRContext并额外挂载DataHydrationContextProvider——这套组合负责把服务端流式输出的数据在客户端水合时写回ssrExchange。随后在 Client Component 中通过urql/next的useQuery查询// app/client/page.tsx use client; import Link from next/link; import { Suspense } from react; import { useQuery, gql } from urql/next; export default function Page() { return ( Suspense Pokemons / /Suspense ); } const PokemonsQuery gql query { pokemons(limit: 10) { id name } } ; function Pokemons() { const [result] useQuery({ query: PokemonsQuery }); return ( main h1This is rendered as part of SSR/h1 ul {result.data.pokemons.map((x: any) ( li key{x.id}{x.name}/li ))} /ul /main ); }以上组件的数据会在服务端渲染并在客户端重新水合。使用多个 Suspense 边界时它们会随完成进度被逐个 flush 并分别水合。urql/next的水合传输机制值得了解服务端useUrqlValue在每次useQuery执行时把该 operation 的序列化结果收集进DataHydrationContext并由RehydrateScript把数据以(window[Symbol.for(urql_transport)] ?? []).push(...)的形式注入 HTML见 packages/next-urql/src/DataHydrationContext.ts 与 packages/next-urql/src/useUrqlValue.ts客户端则从该全局符号数组中取回对应 operation 的结果调用ssrExchange.restoreData()完成水合useUrqlValue.ts。从 Server Component 中失效数据当数据由 Server Component 渲染、而客户端组件发起了 mutation 时服务端并不知道客户端上的 Server Component 需要刷新。可以借助 Next 的 router 主动调用.refresh()强制刷新import { useRouter } from next/navigation; const Todo () { const router useRouter(); const executeMutation async () { await updateTodo(); router.refresh(); }; };禁用 RSC fetch 缓存在createClient构造时传入fetchOptions: { cache: no-store }可避免 Server Component 与 Next 的 fetch 缓存冲突导致命中陈旧结果createClient({ url: https://trygql.formidable.dev/graphql/basic-pokedex, exchanges: [cacheExchange, fetchExchange], fetchOptions: { cache: no-store }, });Legacy Next.jspages 目录next-urql对于仍使用经典pages目录的 Next.js 项目可以使用next-urql对应旧版 packages/next-urql 提供的兼容 API 风格。安装next-urql时需同时安装react-is与urql作为 peer 依赖yarn add next-urql react-is urql graphql # or npm install --save next-urql react-is urql graphql对react-is的 peer 依赖继承自react-ssr-prepassnext-urql内部用其完成预遍历。注意若使用 Next 9.4 之前的版本需要自行 polyfill fetch例如通过isomorphic-unfetch。withUrqlClient 高阶组件用withUrqlClient高阶组件包裹任意页面或_app.js包裹_app.js后则无需再逐个包裹页面// pages/index.js import React from react; import { useQuery } from urql; import { withUrqlClient } from next-urql; const Index () { const [result] useQuery({ query: { test }, }); // ... }; export default withUrqlClient((_ssrExchange, ctx) ({ // ...add your Client options here url: http://localhost:3000/graphql, }))(Index);withUrqlClient接受通常的Client配置可以是普通对象也可以是接收 Next.jsgetInitialProps上下文ctx的函数。一个重要的限制这些选项中不能包含exchanges因为next-urql会在正确的位置自动注入ssrExchange自定义 Exchange 时需通过回调参数拿到ssrExchange再手动组合import { cacheExchange, fetchExchange } from urql/core; import { withUrqlClient } from next-urql; export default withUrqlClient(ssrExchange ({ url: http://localhost:3000/graphql, exchanges: [cacheExchange, ssrExchange, fetchExchange], }))(Index);默认情况下withUrqlClient只包裹组件树并注入 context并不会自动执行 SSR 逻辑只有被包裹的组件本身缺少getInitialProps时next-urql才会加入自动预取查询的 SSR 逻辑也可通过第二参数{ ssr: true }显式开启。使用getStaticProps、getServerSideProps或getStaticPaths时应在withUrqlClient配置中设置neverSuspend: true以关闭 Suspense。原因在于预遍历prepass阶段next-urql无法预知这些函数会如何改变传给页面组件的 propsprops 注入可能改变useQuery使用的 variables进而在随后的renderToString阶段抛出错误——而 React 16 的 SSR 不支持这种中途抛错的恢复方式。使用 { ssr: true } 开启 SSR开启 SSR 最简单的方式是给withUrqlClient传入第二参数{ ssr: true }import { cacheExchange, fetchExchange } from urql/core; import { withUrqlClient } from next-urql; export default withUrqlClient( ssrExchange ({ url: http://localhost:3000/graphql, exchanges: [cacheExchange, ssrExchange, fetchExchange], }), { ssr: true } // Enables server-side rendering using getInitialProps )(Index);需要注意的是用{ ssr: true }包裹_app组件会禁用所有页面的 Next Automatic Static Optimization。因此更推荐在单个页面上按需开启 SSR。使用 getStaticProps 或 getServerSideProps相比getInitialProps方案在getStaticProps/getServerSideProps中做 SSR 更复杂但有两个显著收益允许直接执行 GraphQL schema跳过网络往返以优化性能允许在这些函数中执行额外的数据操作。要让这些函数与withUrqlClient协同工作需要把ssrExchange提取出的数据作为urqlStateprop 返回urqlState是关键字的约定withUrqlClient会据此拾取数据import { withUrqlClient, initUrqlClient } from next-urql; import { ssrExchange, cacheExchange, fetchExchange, useQuery } from urql; const TODOS_QUERY query { todos { id text } } ; function Todos() { const [res] useQuery({ query: TODOS_QUERY }); return ( div {res.data.todos.map(todo ( div key{todo.id} {todo.id} - {todo.text} /div ))} /div ); } export async function getStaticProps(ctx) { const ssrCache ssrExchange({ isClient: false }); const client initUrqlClient( { url: your-url, exchanges: [cacheExchange, ssrCache, fetchExchange], }, false ); // This query is used to populate the cache for the query // used on this page. await client.query(TODOS_QUERY).toPromise(); return { props: { // urqlState is a keyword here so withUrqlClient can pick it up. urqlState: ssrCache.extractData(), }, revalidate: 600, }; } export default withUrqlClient( ssr ({ url: your-url, }) // Cannot specify { ssr: true } here so we dont wrap our component in getInitialProps )(Todos);上面示例会把页面渲染为静态页。务必完整预填充缓存如果页面有依赖数据的子组件必须把这些查询也一并执行否则子组件水合时会重新请求。getStaticProps返回的revalidate用于控制 ISR 增量静态再生成周期。getServerSideProps与getStaticProps只在服务端运行其内部代码会通过 Next 的 code-elimination 机制自动从客户端 bundle 中剔除。这意味着如果我们能直接访问 GraphQL 服务端的可执行 schema就可以用urql/exchange-execute即executeExchange直接执行 schema、跳过一次网络往返import { withUrqlClient, initUrqlClient } from next-urql; import { ssrExchange, cacheExchange, fetchExchange, useQuery } from urql; import { executeExchange } from urql/exchange-execute; import { schema } from /server/graphql; // our GraphQL servers executable schema const TODOS_QUERY query { todos { id text } } ; function Todos() { const [res] useQuery({ query: TODOS_QUERY }); return ( div {res.data.todos.map(todo ( div key{todo.id} {todo.id} - {todo.text} /div ))} /div ); } export async function getServerSideProps(ctx) { const ssrCache ssrExchange({ isClient: false }); const client initUrqlClient( { url: , // not needed without fetchExchange exchanges: [ cacheExchange, ssrCache, executeExchange({ schema }), // replaces fetchExchange ], }, false ); await client.query(TODOS_QUERY).toPromise(); return { props: { urqlState: ssrCache.extractData(), }, }; } export default withUrqlClient(ssr ({ url: your-url, }))(Todos);此时fetchExchange被executeExchange({ schema })取代对应仓库 exchanges/execute 包通过直接调用 resolvers 代替fetchAPI 调用从而省去一次网络往返。next-urql 中的 Stale While Revalidate与ssrExchange的staleWhileRevalidate对应next-urql可以在第二参数中启用同样的行为用于 SSG/ISG 场景下水合后立即刷新陈旧数据export default withUrqlClient( ssr ({ url: your-url, }), { staleWhileRevalidate: true } )(...);开启后水合时虽然会先拿到ssrExchange回放的陈旧数据但会立即再发起一次network-only操作更新数据重新验证期间陈旧结果带有result.stale标记。如前文所述它与非 SSR 下的cache-and-network并不相同——修改 request policy 并不会触发水合后的重新请求。重置 client 实例在极少数场景下可能需要重置整个 client 实例清空所有缓存等。这是一个不常见且被视为 unsafe 的操作请谨慎评估。确需使用时任何被withUrqlClient包裹的组件都会收到resetUrqlClient属性调用它会创建一个新的顶层 client 并重置之前的所有操作。Vue 3结合原生 Suspense 做服务端渲染Vue 3 原生引入了 Suspense 能力组件可以在数据加载时挂起在服务端与客户端都能通用数据加载期间父级会渲染一个替换用的 loading 模板。在 docs/basics/vue.md 中我们介绍过如何利用useQuery的PromiseLike结果接入 Vue Suspense。任何组件的setup()都可以改为async setup()——即返回一个Promise而非直接返回数据从而接入 Suspense。服务端则使用vue/server-renderer的renderToString它返回的Promise会在所有 Suspense 相关的加载完成后 resolveimport { createSSRApp } from vue import { renderToString } from vue/server-renderer; import urql, { createClient, cacheExchange, fetchExchange, ssrExchange } from urql/vue; const handleRequest async (req, res) { // This is where well put our root component const app createSSRApp(Root) // NOTE: All we care about here is that the SSR Exchange is included const ssr ssrExchange({ isClient: false }); app.use(urql, { exchanges: [cacheExchange, ssr, fetchExchange] }); const markup await renderToString(app); const data JSON.stringify(ssr.extractData()); res.status(200).send( html body div idroot${markup}/div script window.__URQL_DATA__ JSON.parse(${data}); /script /body /html ); };这段代码在服务端渲染 Vue 应用ssrExchange以isClient: false捕获全部查询结果并把提取的数据下发到客户端供前文 SSR Exchange 一节中配置的客户端ssrExchange用于水合。总结围绕urql的 SSR 支持可以提炼出几条主线核心机制统一无论是 React、Preact、Next.js 还是 VueSSR 数据水合都依赖同一个ssrExchange——服务端捕获、序列化客户端反序列化、回放。完整的参数isClient、initialState、staleWhileRevalidate、includeExtensions与序列化格式可对照 packages/core/src/exchanges/ssr.ts 及其测试 packages/core/src/exchanges/ssr.test.ts 深入理解。框架接入各有侧重React 依赖 Suspense 模式与react-ssr-prepass的两遍渲染Next.js App Router 用urql/next的registerUrql/UrqlProvider处理 RSC 与流式水合实现见 packages/next-urql/src/rsc.ts、packages/next-urql/src/Provider.ts、packages/next-urql/src/useUrqlValue.tsLegacypages用next-urql的withUrqlClient并可结合executeExchange直连 schemaVue 3 则受益于原生 Suspense 与vue/server-renderer。取舍与注意点ssrExchange的缓存回放一次即失效、staleWhileRevalidate与cache-and-network的区别、includeExtensions的默认关闭、Next.js 中{ ssr: true }对静态优化的影响、以及getStaticProps/getServerSideProps下neverSuspend的必要性都是在实际项目中容易踩坑、值得提前规划的设计决策。赞分享前端【免费下载链接】urqlThe highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.项目地址https://gitcode.com/gh_mirrors/ur/urql点击查看免费下载相关推荐基于 TanStack Router 的文件路由 SSR 实战从 Express 服务端渲染到客户端水合基于 TanStack Router 的文件路由 SSR 实战从 Express 服务端渲染到客户端水合 本指南以仓库中的 basic ssr file ba前端路由SSR从手动安装到自动化Hack-windows-installer为开发者节省90%时间从手动安装到自动化Hack windows installer为开发者节省90%时间 Hack windows installer是一款专为Hack字体打造的Material UI × Next.js 集成实战App Router 与 Pages Router 的服务端样式渲染完整方案Material UI × Next.js 集成实战App Router 与 Pages Router 的服务端样式渲染完整方案 本篇指南完整覆盖 Mater前端UI组件设计系统上一篇终极KLOGG日志分析神器快速排查系统问题的完整指南下一篇Vue Vben Admin 精简版现代化中后台管理系统终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。