谁胜谁负?12个维度全方位对比 GraphQL API 和 WPGraphQL
很多人都想要我来对比一下用于 WordPress 的 GraphQL API 和 WPGraphQL。事实上,这两个插件都是 WordPress 的 GraphQL 服务器,因此它们具有相同的用途,但是它们有各自具备不同的特性,因此在某些场景中,会有某个插件表现更突出。
为了更好的对比这两个插件,本文设置了流行度、代码风格和标准、紧急事项和扩展四个大的方向,并细分了 12 个细项来对比。
任何一款软件(或技术,或其它任何东西)都必须被人们使用,否则它比其它软件更好就只能是一个传闻。
例如,尽管有其它替代品可以加快打字速度,但我们仍然主要使用 QWERTY 键盘。
这两个插件有多流行呢?
截止目前,WPGraphQL 一直是 WordPress 中 GraphQL 的同义词。在其被开发出来的 4 年多时间里(从 2016 年 11 月开始),它 在 GitHub 上聚集了超过 2.8k 星星、一个 超过 4600 名拥护者 的社区、以及近 100 名项目贡献者。
它达到了 1.0 版本,在 2020 年 11 月 被上传到 wp.prg 的插件目录。自那时以来,它已经收集了超过 8000 个活跃安装。它是目前为 Gatsby 提供 WordPress 内容的唯一解决方案,最近,一些项目已经将它添加到它们的技术栈,包括 WPEngine's Headless framework 和 WebDevStudios' Next.js WordPress starter。
换句话说,WPGraphQL 很流行。
GraphQL API for WordPress 的开发早在一年半以前就已经开始(作为更广泛的项目的一部分),6 个月前它达到了“足够好”的状态,从那时起 在 GitHub 上获得了 150 个星星。这个插件目前处于 0.7 版本,距离达到 1.0 版本还有几个月时间(例如,它在模式上还没有类别)。
上个月,我推出了这个当前的网站 graphql-api.com,从那以后,我一直在 博客上推广这个插件,还 在 CSS-Tricks 发布了一篇介绍性文章。这些尝试已经引流了数百个人到这个网站,并且有超过 100 位访问者下载了这个插件。
换句话说,GraphQL API for WordPress 正缓慢但稳定地流行起来,这是一项进行中的工作。
这一回合的获胜者:WPGraphQL。
扩展插件(Extensions)允许通过 GraphQL API 与其它插件交互。WPGraphQL 有用于 ACF、WooCommerce、Yoast 和其它一些插件的扩展插件。
用于 WP 的 GraphQL API 还没有扩展插件,而且我也不期望它会在 1.0 版本之前有很多插件。
然而,用于 WP 的 GraphQL API 在它的架构中非常重视扩展插件,允许用户从一个中心位置——“模块”页面,管理这些扩展插件(弃用、禁用、配置、阅读文档):
GraphQL API for WordPress 中的模块页面
换句话说,WPGraphQL 已经有扩展插件,而 GraphQL 正为扩展插件做准备。
这一回合的获胜者:WPGraphQL。
WPGraphQL 面向开发者:如果你想要从你的 WordPress 站点提取数据,你需要将你的 GraphQL 查询存储在你的代码中的某个位置(很可能是在一些 JavaScript 方法中)。然后,为了能够使用它,你需要在编程方面有足够能力。
相反,GraphQL API for WordPress遵循 WordPress 理念——任何人都应该能够使用它,包括非技术人员。为了实现这个目标,它允许通过 WordPress 编辑器创建和管理 GraphQL 查询,这样就会使通过 API 访问 WordPress 站点的数据变得就像创建一个博客文章一样简单。
另外,GraphQL API for WordPress 更强调提供一种以可视化方式与 GraphQL 服务交互的客户端。虽然两个插件都提供了 GraphiQL 客户端,来执行查询,但只有 GraphQL API for WordPress 还提供了 Voyager 客户端,以交互方式浏览表结构(schema):
可视化 GraphQL 表结构
这一回合的获胜者:GraphQL API for WordPress
我们来谈谈代码!
如果你正在使用 GraphQL,那么很可能你正在使用一些 JavaScript 框架(这是一种现代范式)来执行一些无头 WordPress 并渲染网站。此外,WordPress 可能是一个老旧的内容管理系统,而 GraphQL 是一个从这个站点访问数据的现代接口。因此,我可以放心地假设你是一名聪明的开发人员,热衷于编写优雅的代码,不会接受使用一个次优的方案。
这两个插件的代码(来自它们自己的代码库,以及我们的定制实现)有多优雅呢?
WPGraphQL 和 GraphQL API for WordPress 都要求 PHP 7.1+。
但是,有一点不同的是:GraphQL API for WordPress 实际上使用 PHP 7.4 编写,然后在生产中编译成 PHP 7.1。
因此,编写 GraphQL API 更让人愉快:你可以使用更新的 PHP 功能,包括object
类型、类型化属性 和 箭头函数。而且一旦添加了对 PHP 8.0 的支持(这将在 Lando 的新版本发布时发生),你还可以使用组合类型、匹配表达式 等。
这一回合的获胜者:GraphQL API for WordPress。
让我们从 WPGraphQL 开始。前往wp-graphql/wp-graphql
代码库,有一件事对我来说很突出:
代码库中的 vendor 目录
展开:
vendor 目录的内容
抱歉,我对此只有一种反应:
我能够原谅你生活中的很多事情,但这一点无法原谅
将 Composer 的vendor
目录提交到代码库上是一个坏的实践,Composer 明确反对这样做。
修复这个问题并不难(我甚至描述了一种基于 GitHub actions 的方法),所以我想知道为什么会发生这种事情。
我要说的是,在这一回合,WPGraphQL 击中了他自己!
让我们继续。针对 WPGraphQL 的开发是非常 WordPress 化的。如果你喜欢针对 WordPress 编码,那就太好了,你会爱上它的!
但是,另一方面,如果你不喜欢它的话...
前往 WPGraphQL 的开发者参考,我们就会明白这一点。这个参考已经被 WordPress 钩子(actions 和 filters)接管。
为了截图看这个 actions 列表,我不得不将我的浏览器缩小到 50%:
用于扩展 WPGraphQL 的 Action 钩子
对于 filters 列表,我缩小到 30%(Firefox 支持的最小缩放值),即使这样我也得不到完整的列表:
用于扩展 WPGraphQL 的 Filter 钩子
让我们切换到leoloso/PoP
代码库,它在包含 GraphQL API for WordPress 的项目中也是巨型库。它的代码有以下特征:
符合PSR-1
、PSR-4
和PSR-12
标准。
所有代码都被拆分到多个原子性的包中,所有代码(100 多个包用于插件,200 多个包用于整个项目)都托管在同一个巨型库中。
使用 Composer 来管理所有依赖。
使用 Symfony 依赖注入 来管理应用程序中的所有服务。要注册一个新的类型解析器、字段解析器或者指令解析器,我们必须在容器中注册一个新的服务。
每个类都是一个服务,Symfony 依赖注入负责将整个应用程序自动连接在一起。
底层的 GraphQL 服务器(称作 GraphQL by PoP)是CMS 不可知的。GraphQL for WordPress 实现了针对 WordPress 的契约,并增加了一些自定义逻辑(例如,提供客户端)。
WordPress 相关代码只占整个代码的 10%。为另一个框架或 CMS(Laravel/Drupal/etc)复制此 10% 代码也可以为它们提供 GraphQL 服务器实现。
由于 CMS 不可知,编写一个解析器意味着编写其通用业务逻辑,由可重用的服务提供支持。我们从不考虑 WordPress 代码,也很少需要处理它的技术债务。
同样,GraphQL 表结构也不是 WordPress 数据模型的 1:1 复制,绕过 WordPress 在数据层积累的技术债务,提供了一个干净的接口。
通过架构设计,GraphQL 的 N+1 问题不会发生,根本不会再困扰开发者。
服务器不仅是一个 GraphQL 服务器:它实际上是一个 API 服务器,响应可以从一个信源输出成其它格式或规范(例如:REST)。(关于这一点,在回合 11 会有更多讨论)。
没有
vendor
目录被提交。相反,源代码通过 GitHub actions 被转换成分布式代码(例如,安装在 WordPress 站点上的最终插件),然后被部署到 一个dist
代码库,那里不包含vendor
目录。在生成用于分发的代码时,它会被 PHP-Scoper 限制作用域,包含 PHP 7.4 代码的源代码会被编译成 PHP 7.1。
由于它已经解决了作用域问题,所以插件可以依赖于任何第三方依赖。目前,它使用 Symfony 的 DependencyInjection、Cache 和 Dotenv、Guzzle(与外部 API 交互)、 League 的Pipeline 和其它一些依赖。
这不仅对现在很重要,对未来也很重要:我敢肯定我可以使用来自 Packagist 库的任何依赖,因此我没必要去重新发明轮子。
字段订阅类型, 使得 GraphQL 表结构很容易扩展。
这一回合的获胜者:GraphQL API for WordPress(如果你不介意的话,我敢说,这是一个很大的优势)。
让我们向 GraphQL 表结构中增加一个字段。
我们 遵循 WPGraphQL 的教程。建议的代码如下。它声明了一个 action 钩子来执行一个函数,这个函数声明了一个数组。数组中提供了字段的描述和解析:
add_action( 'graphql_register_types', function() {
register_graphql_field( 'RootQuery', 'myNewField', [
'type' => 'String',
'args' => [
'myArg' => [
'type' => 'String',
'description' => __( 'Description for how the argument will impact the field resolver', 'your-textdomain' ),
],
],
'resolve' => function( $source, $args, $context, $info ) {
if ( isset( $args['myArg'] ) ) {
return 'The value of myArg is: ' . $args['myArg'];
}
return 'test';
},
]);
});
这个例子非常简单:解析器基本上什么都没做。然而,我已经很难查看代码并立即理解它的功能。我不是在强词夺理:在我的编辑器中,代码中的所有颜色都在争夺我的注意力。此外,关注点没有分离,代码似乎不太可能重用。
因此,在开发应用程序时,如何使代码易于阅读、可重用、无 bug 以及许多其它问题都取决于开发人员(即,你);库本身对于这些问题似乎没有多大帮助。
我称这种风格为“ADD”:数组驱动开发(Array-Driven Development)。我对这种风格并不太欣赏。
(公平来说,这是一种标准的编码实践,也是底层引擎webonyx/graphql-php
所使用的一种编码实践。)
在 GraphQL API 中,所有代码都是 SOLID 风格的。为了在 GraphQL 表结构中注册一个字段,我们创建了一个类实现了接口FieldResolverInterface
(实际上,继承自AbstractSchemaFieldResolver
,这个类已经实现了许多方法),然后我们在容器中注册这个类。
例如,这段代码向User
类型提供了username
、email
和url
字段:
class UserFieldResolver extends AbstractSchemaFieldResolver
{
public static function getClassesToAttachTo(): array
{
return [
UserTypeResolver::class,
];
}
public static function getFieldNamesToResolve(): array
{
return [
'username',
'email',
'url',
];
}
public function getSchemaFieldDescription(TypeResolverInterface $typeResolver, string $fieldName): ?string
{
$descriptions = [
'username' => $this->translationAPI->__("User's username handle", "users"),
'email' => $this->translationAPI->__("User's email", "users"),
'url' => $this->translationAPI->__("URL of the user's profile in the website", "users"),
];
return $descriptions[$fieldName];
}
public function getSchemaFieldType(TypeResolverInterface $typeResolver, string $fieldName): ?string
{
$types = [
'username' => SchemaDefinition::TYPE_STRING,
'email' => SchemaDefinition::TYPE_EMAIL,
'url' => SchemaDefinition::TYPE_URL,
];
return $types[$fieldName];
}
public function resolveValue(TypeResolverInterface $typeResolver, object $user, string $fieldName, array $fieldArgs = [])
{
switch ($fieldName) {
case 'username':
return $this->usersAPI->getUserLogin($user);
case 'email':
return $this->usersAPI->getUserEmail($user);
case 'url':
return $this->usersAPI->getUserURL($user);
}
return null;
}
}
我确信我的解决方案比 WPGraphQL 的解决方案更优雅。然而,这是一个品味问题。我知道许多开发人员并不介意数组驱动开发,实际上他们更喜欢它,因为在一个紧凑的代码块中,他们可以实现所有的逻辑。
这一回合的获胜者:平局。
GraphQL 服务器需要注意许多问题,只是为了满足“检索你所需要的数据,不多不少”的主张。
例如:
它有多安全?我们如何确保隐私数据不会在一个公开端点上暴露?
它的性能如何?我们如何在一次又一次地发送相同查询时,降低服务器的负载,同时使服务器尽可能快?
它有多简单?它与 WordPress 的集成度如何,从而充分利用 CMS 提供的功能?
还有许多问题。这只是我选择一个小样本,我将在接下来的三个回合详细讨论。
持久化查询结合了 GraphQL 和 REST 的最佳组合:它们是用 GraphQL 创建的,因此它没有过少 / 过多地获取数据,但是它们是以端点的形式发布在服务器上的,具有自己的 URL。
持久化查询提供以下好处:
它是安全的:我们可以预定暴露哪些数据,而不是通过单个端点访问任何数据。
它是快速的:通过它自己的 URL 访问,它可以在客户端和后端之间的每一层(在服务器、CDN、浏览器)使用标准的 HTTP caching 进行缓存。
WPGraphQL 通过以下两个扩展插件提供对持久化查询的支持:
WPGraphQL Lock
WPGraphQL Persisted Queries
此外,Jason Bahl(WPGraphQL 的创建者)最近宣布,在不久的将来,他将在 WPGraphQL 中增加对持久化查询的支持。
我不知道他在想什么,因为已经有两个扩展插件了。他要做的与这两个扩展插件会有什么不同么?也许他想让它成为这个插件的核心,从而在不依赖第三方的情况下增强整个插件的安全措施?
或者他看到了 GraphQL API for WordPress 的实现,希望提供一种类似的体验,通过可视化编辑器而不是纯代码来操作它?
接下来,我们来看看 GraphQL API for WordPress。它不仅提供持久化查询,还努力使其成为该产品的核心部分:
这个插件附带默认禁用的单个端点,并且鼓励用户仅通过持久化查询暴露数据。
(相反,WPGraphQL默认情况下只禁用内部检查,而不是实际的端点。换句话说,攻击者仍然能够访问隐私数据;他们只是让攻击者的任务更困难,因为攻击者不会提前知道隐私数据的位置。)
它与 WordPress 编辑器集成得非常深,因此创建一个持久化查询和创建一个博客帖子一样简单,任何人都可以做到,而不仅仅是程序员。
持久化查询并不是静态的:它们可以使用 GraphQL 变量,这些变量的值会在执行端点时通过 URL 参数提供。
可以看看在我的插件中创建和执行一个持久化查询的体验。
这一回合的获胜者:GraphQL API for WordPress。
GraphQL 有一个很大的痛点:它不容易缓存。原因是,它依赖发送POST
操作到单个端点。由于单个端点会产生不同的结果,而且由于查询是通过请求体而不是 URL 参数发送的,因此我们不能缓存单个端点。
许多 GraphQL 服务器提供的标准解决方案是将缓存转移到客户端,依赖对象的 ID 作为要缓存的实体标识符,而不是一个端点的 URL。提供这个功能的最流行的一个库是 Apollo client。
有一个关于 WPGraphQL 代码库的讨论,总结了所有可用于 WPGraphQL 的缓存的可选方案。有趣的是,其中大多数是外部工具(例如 Apollo 客户端,或者 WordPress Object Cache),这意味着要为应用程序添加一个额外的层,增加它的复杂性,还可能使它变得更慢。
(这些一定是在 WPGraphQL 中原生实现持久化查询的决定的部分原因。)
例如,Apollo 客户端运行在客户端。如果从一个没有多少电的低端移动手机访问这个网站,额外的 JavaScript 代码会影响应用程序的性能。
同样,使用 WordPress 的开发人员可能精通 PHP,但并不精通 JavaScript。现在,缓存他们的 API 则意味着他们还需要担心 JavaScript 层。
GraphQL for WordPress 在这方面更明智。由于它提供持久化查询,意味着查询在它们自己的端点上执行,允许通过 HTTP 缓存来缓存这些端点的 URL。
HTTP 缓存头具有自动根据查询中的所有字段的max-age
值计算出的max-age
值,这个信息是 通过 WordPress 编辑器逐个字段配置 的。
因此,API 可以跨多个层(在客户端、CDN 和服务器)被缓存,并在插件中进行本机处理,而无需另外加一层。
这一回合的获胜者:GraphQL API for WordPress。
Gutenberg 曾经是 WordPress 的未来。现在不一样了:Gutenberg 已经是 WordPress 的现在(因此我们可以把他称为 WordPress 编辑器),而 Full Site Editing 成为新的未来。
不用说,我们的 API 需要很好地与 WordPress 编辑器集成。这不仅意味着检索或发布块数据,还可能增强 WordPress 编辑器本身的功能。
例如,由于 GraphQL 订阅可以让服务器实时将数据推送到客户端,因此它很适合赋能协作编辑和通知功能。
WPGraphQL 可以通过 WPGraphQL Gutenberg 扩展插件查询块数据。这个扩展插件创建了一种新类型来映射每个块,因此我们有CoreParagraphBlock
、CoreQuoteBlock
等等。
GraphQL API for WordPress 很快就可以查询块数据了(这项工作正在进行)。然而,它会有一个单独的Block
类型来表示所有块,而不是每个块创建一个新类型,然后我们可以根据一些块的名称为它们抽取特定元数据。
例如,查看你如何翻译一个段落块中的内容(使用@translate
指令,这个指令连接到谷歌翻译 API):
query TranslateStringsInBlocks {
post(id:1657) {
title
paragraphBlocks: blockMetadata(
blockName: "core/paragraph"
)
translatedParagraphBlocks: blockMetadata(
blockName: "core/paragraph"
)
}
}
这一回合的获胜者:平局。
“我有一个梦想。”
Gutenberg 模块被设计为提供一个单一的接口来创建 WordPress 中的内容,极大地简化了 CMS 代码的开发和用户所需的学习。
虽然是为了创建内容而引入的,但是块正逐步接管 CMS 的其它领域,包括组件、菜单,以及即将到来的通过 Full Site Editing 的主题。在未来,它们还将支持多语言功能和协作编辑(一些我们想到块时甚至可能不会想到的功能),谁知道还有什么呢。
我们可以用同样的术语来思考 GraphQL:用来与数据交互的单一接口。这意味着,不仅获取和发布数据,还涉及数据的任何交互,包括编辑。
WordPress 有一个独特的机会成为真正的 Web 操作系统:一个由 Gutenberg 支持的系统,允许用户输入任何类型的内容(文本、图像、视频、音频,等等),通过它自己的工具或一些云服务来处理这些内容,将它发布到最终的目的地,无论是 WordPress 站点或者其它地方。
但是在这个强大的梦想背后,必须有一个真正强大的 API,来满足我们对它的任何要求。一个可能基于 GraphQL 的 API,但是被设计来超越它的限制。
WPGraphQL 没有附带任何指令(directives)。我不是说它不支持指令(它的引擎webonyx/graphql-php
支持指令),但是它没有提供任何自定义指令的支持。
“那又怎么样?”你可能想。“我们需要指令来干嘛?如果某个人需要修改查询的结果,他们可以在它们自己的客户端上完成!”
为什么我需要指令?
这是意见问题,没有对错之分。但我可以告诉你:指令是一种非常有用的功能,有助于将 GraphQL 与 REST 区分开。如果你没有使用它们,你可能没有充分利用你的 API。
指令不受规范的约束,因此 GraphQL 服务器可以用任何他们喜欢的方式实现它们,使它们可能功能强大。这就是为什么 GraphQL 中很多强大的新功能都是首先通过指令引入的,例如@stream
和@defer
。
GraphQL API for WordPress 非常重视指令。对于它们所应用到的所有字段,它们只使用来自所有实体的数据执行一次(这解释了为什么@translate
指令可以如此快速地从谷歌翻译 API 获取结果),而且 GraphQL 引擎本身是基于一个 指令管道(directive pipeline)。
但是你害怕把所有的能力都提供给用户,对吧?这是一个合理的担忧。但是,你只需要删除到单个端点的访问,并只提供通过持久化查询获取数据,而只有你(网站管理员)是唯一可以访问这些指令的人。
所以要么你受益,要么什么都没发生。
如果你喜欢指令,很棒,你会爱上 GraphQL API for WordPress!但是,另一方面,如果你不喜欢它的话...
这一回合的获胜者:GraphQL API for WordPress。
“REST?什么 REST?我们这里不是在讨论 GraphQL 吗?为什么你要讨论 REST 呢?为什么你要让我的生活变得复杂?”
除此之外,我不能为你做任何事
是啊,乍一看这个话题似乎超出了范围。但是我在这个比较中添加了它是因为一个很简单的原因:Matt Mullenweg 说过,他正在检查 GraphQL 是否有可能包含在 WordPress 的核心中,贡献者会担心的是必须维护两个代码库。
这导致了一个明显的问题:GraphQL 服务器是否也能够处理 REST?
这个答案对于 WPGraphQL 是“部分是”,对于 GraphQL API for WordPress 是“完全是”。
先说 WPGraphQL。可以定义一个 REST 端点,当解析它时,只是执行一个包含必需字段的 GraphQL 查询,或者内部调用 GraphQL 引擎,或者作为对同一个 Web 服务器执行的外部POST
操作。
但这还不足以满足 WP REST API,因为它还有一个 JSON schema,我们不能没有它。
再说说 GraphQL API for WordPress。我必须承认我很幸运,因为它的底层引擎(称为 PoP 的服务端组件模块)的工作大约始于 2013 年,也就是我知道 GraphQL 之前的几年,这个项目实现了它自己的一些想法。
然后,大约在一年半以前,当我开始编写 CMS 不可知的GraphQL by PoP(GraphQL API for WordPress 的基础)时,我把针对 PoP 开发的想法与 GraphQL 建立的基础结合起来,创建了一个支持 GraphQL 规范的系统,同时能够为它添加一组不同的特性。
在这方面,PoP 使用的 schema 是 API 不可知的,这是 GraphQL 使用的 schema 的超集。
然后,GraphQL by PoP 层遵循 GraphQL 规范格式化 PoP schema,从而生成 GraphQL schema。同样,我们可以生成 WP REST API 所需的 JSON schema。
生成这个 JSON schema 还没有完成,但是是可行的。
现在已经完成的是,以多种格式生成查询响应。例如,这个 GraphQL 查询:
{
posts {
id
title
date
author {
name
}
}
}
这也通过 REST 端点来解析:
/posts/api/rest/?query=id|title|date|author.name
我们不需要就此止步。你需要使用另外一个种格式(如 XML)生成结果?没问题:
/api/?query=posts.id|title|date|author.name&datastructure=xml
(这将以有助于实现基于 schema 的 WordPress 新的导入 / 导出工具的提议。)这也使我前面所说的更加明显:单个接口可以赋能所有的数据交互,包括 CMS 内部的交互,以及 CMS 与外部 API 的交互。)
这一回合的获胜者:GraphQL API for WordPress。
GraphQL 规范是终点吗?答案是否定的:这个规范是不断演变的。就在这一刻,有 100 个开放的问题,其中包含许多要在将来某个时候正式化的提案。
现在,这 100 个问题中,肯定会有新的功能,我们今天就可以从中受益,对吧?如果这样,为什么要等呢?
这正是我的想法。
“但是如果 GraphQL 规范中没有某些内容,那么我们不应该将其添加到 GraphQL 服务器,否则用户会困惑!”
说得好。然而,如果我们将这些新颖的功能仅作为可选功能提供,然后用户肯定会意识到这一点,而且不会有什么问题或误解。
再说一遍,这是我的想法。不过,这是一个见解问题,所以如果你只想要使用每个 GraphQL 服务器都在使用的功能,那就没关系了。
我相信 WPGraphQL 就是这样做的。至少,我还没有看到一个特性超出了规范中批准的范围。
不过,对于 GraphQL API for WordPress,我定期扫描规范中的问题清单,如果我发现一些很酷的特性,而且我的服务器不用花太多精力就能实现它,那么我就会实现它。(确实,这是我的爱好之一。)
这些是我迄今为止实现的一些“前瞻性”功能:
多查询执行
Schema 命名空间
嵌套的突变
可嵌入字段
可组合指令
主动反馈
基于字段和指令的版本控制
我已经计划添加的功能:
订阅(这已经是规范的一部分)
@stream
和@defer
指令扁平的链式语法
这一回合的获胜者:GraphQL API for WordPress。
原文链接
https://graphql-api.com/blog/graphql-api-vs-wpgraphql-the-fight/#heading-verdict!
今年 7 月 9-10 日,ArchSummit 全球架构师峰会将落地深圳,大会策划【内容分发场景的系统架构实践】专题,将结合产品实际业务场景,从算法模块设计,数据流处理和系统架构布局等方面,介绍内容分发场景下,内容生产者、消费者和业务场景之间的内容生态建设,达到流量的精准触达和分发的效率最大化。
大会限时 8 折优惠,扫描下方【二维码】或点击底部【阅读原文】了解更多会议内容。
点个在看少个 bug 👇