尝试全面利用Laravel和GraphQL
首先
2018年Laravel降誕カレンダー- Qiitaの10日目の記事です!
我个人的印象是,大约一年前开始,我开始听到越来越多有关GraphQL的讨论,并且最近我也逐渐发现越来越多的怀疑意见。
-
- RestfulライクなAPIの代替案として有効なのか自分で触って確かめてみたかった
- Laravelで使い倒す記事を見かけていなかった
因此我尽可能地充分利用了这个机会(虽然只做了一些深入尝试?)。
※我将逐步添加实施例子。
值得一提的是,在這篇文章中
-
- GraphQLとは何か
-
- GraphQLの諸々(クエリ、ミューテーション)の説明
- クエリの記述方法の説明
不会说明记录方式,但在使用过程中将列出实际执行的查询。
前提 – 必要的条件或基础。
前提 – 先决条件或假设。
前提 – 开始或展开讨论之前的必要条件。
前提 – 先决条件或基本假设。
PHP版本为7.1.3,Laravel框架版本为5.7.16,Nuwave Lighthouse版本为2.6。
创造的东西

使用的库
在使用Laravel时,有几个库可以用于GraphQL,但是这次我们选择了lighthouse作为前提。
我认为这篇文章对于Lighthouse来说写得详细且易懂。
项目成果
我已经把本次的成果放在这里。
※附加的内容尚未反映。
准备下来
为了创建如上所述的ER图结构,我们将创建迁移、关系和种子。
我将省略说明。
如果您感兴趣,可以在成果物内查看。
灯塔系统引入
现在进入正题。
我们将参考这个。
首先需要安装库。
$ composer require nuwave/lighthouse
接下来,使用vendor:publish命令进行初始设置。
$ php artisan vendor:publish --provider="Nuwave\Lighthouse\Providers\LighthouseServiceProvider" --tag=schema
我认为routes/graphql/schema.graphql文件已经完成了。
我们也安装了一个名为laravel-graphql-playground的开发工具。即使在前端等其他地方已经准备了独立的GraphQL客户端,我认为仍然可以使用它来确认模式定义等用途。
$ composer require mll-lab/laravel-graphql-playground
最后,我们还需要将playground发布为vendor并进行初始设置。
有关playground的说明请查看此处。
php artisan vendor:publish --provider="MLL\GraphQLPlayground\GraphQLPlaygroundServiceProvider"
以上是引言完毕。
由于本次未使用外观模式,所以没有进行设置。
执行查询
简单的查询
首先,我们尝试执行一个简单的查询。
我们将在routes/graphql/schema.graphql中注册的查询进行执行。
type Query {
user(id: ID @eq): User @find(model: "App\\User")
}
只需给出一个选项!
我将通过laravel-graphql-playground进行执行并访问。根据此处的用法,我认为当您访问URL /graphql-playground时会显示内容。如果是在(http://127.0.0.1:8000的环境中,那么URL将是http://127.0.0.1:8000/graphql-playground)。

一旦能够访问,我立即尝试发布查询。
query {
user(id: 1) {
id
name
email
}
}

使用关联查询
接下来,让我们尝试使用关联查询来执行。
我们尝试获取与用户相关联的文章。
首先,我们将创建一个文章类型。
type Article {
id: ID!
userId: Int! @rename(attribute: user_id)
title: String!
body: String!
createdAt: DateTime! @rename(attribute: created_at)
updatedAt: DateTime! @rename(attribute: updated_at)
}
我认为实际上大部分都是前端使用GraphQL,因此我使用@rename指令将列名重命名并转换为驼峰命名法。
接下来,我们将在用户类型中添加文章。
type User {
id: ID!
name: String!
email: String!
+ articles: [Article] @hasMany
created_at: DateTime! @rename(attribute: created_at)
updated_at: DateTime! @rename(attribute: updated_at)
}
由于准备完毕,现在将执行使用关系型数据库的查询语句。
query {
user(id: 2) {
id
name
email
articles {
id
userId
title
}
}
}

获取分页器的数据查询
最后,我们将获取多个数据。因为在routes/graphql/schema.graphql中注册了查询,所以我们将执行该查询。
type Query {
users: [User!]! @paginate(type: "paginator" model: "App\\User")
}
由于在paginator中必须提供count参数,所以我会将其添加进去。
query {
users(
count: 10
) {
data {
id
name
email
articles {
id
userId
title
}
}
paginatorInfo {
currentPage
}
}
}

当获取多个数据时,
-
- ベースとなるモデルのhasMany等で取得する
- paginatorで取得する(typeは2種類)
我想,可能只有两个选择。
我想知道是否有人想将基础模型作为没有分页器的纯粹集合拉出来,如果是这样的话,我认为那将是你自己制作的解析器注册。(如果您有不需要创建解析器也可以完成的信息,请务必留下评论,我会非常感激)
因为 Playground 可以根据源代码生成模式信息,所以它为我提供了一个选项。
-
- どんなクエリがある?
- このクエリの引数、戻り値は?
我希望在你感到困惑的时候能够参考相关的模式信息。


然而,由于不支持热重载,所以如果修改源代码,需要重新加载页面。
进行突变
注册
现在我已经可以执行查询并尝试进行变更操作。
首先,我将注册一个用户。
对于在routes/graphql/schema.graphql注册的变异进行一些修改。
type Mutation {
createUser(
name: String
@rules(apply: ["required"])
email: String
@rules(apply: ["required",
"email",
"unique:users,email"]
)
password: String
@bcrypt
@rules(apply: ["required"])
): User
@create(model: "App\\User")
}
以下是注册的Mutation的样子。
mutation {
createUser(
name: "taro"
email: "graphql@example.com"
password: "secret"
) {
id
name
email
}
}

更新 –
我会进行更新处理。
这里使用的是原本就有的东西。
type Mutation {
updateUser(
id: ID
@rules(apply: ["required"])
name: String
email: String
@rules(apply: ["email"])
): User
@update(model: "App\\User")
}
我尝试进行更新。
mutation {
updateUser(
id: 51
name: "hanako"
) {
id
name
}
}

删除 chú)
我們最後也會進行刪除。
我们将使用原来就存在的东西。
type Mutation {
deleteUser(
id: ID @rules(apply: ["required"])
): User @delete(model: "App\\User")
}
我试试删除。
mutation {
deleteUser(
id: 51
) {
id
name
}
}

根据消息,将返回值更改为必需,并重新执行。
type Mutation {
deleteUser(
- id: ID @rules(apply: ["required"])
+ id: ID! @rules(apply: ["required"])
): User @delete(model: "App\\User")
}

添加查询和改变动作
为了充分利用,我们将添加一些查询和变更。
使用基于LIKE的搜索条件
定义是这样的,
type Query {
usersByEmail(email: String @where(operator: "like")): [User!]!
@paginate(type: "paginator" model: "App\\User")
}
这样执行。
query {
usersByEmail(
email: "%r%"
count: 10
) {
data {
id
email
}
}
}
@where指令,在官方的主分支中有记录,但在ver2.6分支中未找到,让我感到有点困惑。
只需一个选项,将以下内容用中文进行意译:
删除突变以外的ID指定
根据源代码,看起来无法指定ID以外的内容来执行@delete指令,所以我将自己编写解析器。
首先,使用artisan生成文件。
$ php artisan lighthouse:mutation DeleteUsersByEmail
这次
-
- 指定した文字列でEmailのLIKE検索を検索をし、該当したユーザーを削除する
- 戻り値に適当な値をセットする
我制作了一个满足条件的解析器。
<?php
namespace App\Http\GraphQL\Mutations;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use App\User;
class DeleteUsersByEmail
{
/**
* Return a value for the field.
*
* @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`.
* @param array $args The arguments that were passed into the field.
* @param GraphQLContext|null $context Arbitrary data that is shared between all fields of a single query.
* @param ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more.
*
* @return mixed
*/
public function handle(
$rootValue,
array $args,
GraphQLContext $context = null,
ResolveInfo $resolveInfo
) {
$email = $args['email'];
User::where('email', 'like', $email)
->delete();
$res = 'ok';
return compact('res');
}
}
接下来,使用自己创建的解析器进行定义。
type DummyResponse {
res: String!
}
type Mutation {
deleteUsersByEmail(
email: String! @rules(apply: ["required"])
): DummyResponse
@field(resolver: "App\\Http\\GraphQL\\Mutations\\DeleteUsersByEmail@handle")
}
最后,我们以这样的方式进行执行。
mutation {
deleteUsersByEmail(
email: "%r%"
) {
res
}
}
将定义文件分割为多个部分
由于仅使用routes/graphql/schema.graphql来完成可能很困难,所以我尝试按照每个类型进行分割。
首先,将routes/graphql/schema.graphql修改为以下的样子。
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")
scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date")
# その他ファイルをextend typeする為にここに空のベースとなる定義を配置しておく
type Query
type Mutation
#import models/*.graphql
接下来,我们来看看如何将分割后的文件处理成这个样子。
extend type Query {
users: [User!]! @paginate(type: "paginator" model: "App\\User")
user(id: ID @eq): User @find(model: "App\\User")
usersByEmail(email: String @where(operator: "like")): [User!]!
@paginate(type: "paginator" model: "App\\User")
}
extend type Mutation {
createUser(
name: String
@rules(apply: ["required"])
email: String
@rules(apply: ["required",
"email",
"unique:users,email"]
)
password: String
@bcrypt
@rules(apply: ["required"])
): User
@create(model: "App\\User")
updateUser(
id: ID
@rules(apply: ["required"])
name: String
email: String
@rules(apply: ["email"])
): User
@update(model: "App\\User")
deleteUser(
id: ID! @rules(apply: ["required"])
): User
@delete(model: "App\\User")
deleteUsersByEmail(
email: String! @rules(apply: ["required"])
): DummyResponse
@field(resolver: "App\\Http\\GraphQL\\Mutations\\DeleteUsersByEmail@handle")
}
type DummyResponse {
res: String!
}
type User {
id: ID!
name: String!
email: String!
password: String!
articles: [Article] @hasMany
createdAt: DateTime! @rename(attribute: created_at)
updatedAt: DateTime! @rename(attribute: updated_at)
}
type Article {
id: ID!
userId: Int! @rename(attribute: user_id)
title: String!
body: String!
createdAt: DateTime! @rename(attribute: created_at)
updatedAt: DateTime! @rename(attribute: updated_at)
}
只能调用一次type Query type Mutation等,所以在schema.graphql方面处理。
# その他ファイルをextend typeする為にここに空のベースとなる定義を配置しておく
type Query
type Mutation
在分割的文件中,我们会先定义一个空的类型,然后始终使用 extend type Query 等方式进行扩展。
自定义错误信息
如果实际使用的话,我认为需要自定义错误消息,所以我会在更新用户的处理中尝试一下。
extend type Mutation
updateUser(
id: ID
@rules(
apply: ["required"],
messages: { required: "idは必須です" }
)
name: String
email: String
@rules(apply: ["email"])
): User
@update(model: "App\\User")
}

当graphql-playground无法正常工作时的应对措施是什么?
有时候,当执行错误的查询描述时,Graphql Playground 可能会突然无法使用。
在我苦恼结构问题无法继续的时候,在Twitter上发了一条帖子,得到了帮助!
这个问题和那个一样吗?https://t.co/IzfTnFPa0b— FUJI Goro (@__gfx__) 2018年12月3日
请注意,我认为每个人都会遇到一次。
【2018/12/20补充】使用Datetime时的注意事项。
默认定义的
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")
这个日期时间的话,
type User {
id: ID!
...省略
emailVerifiedAt: DateTime @rename(attribute: email_verified_at)
createdAt: DateTime @rename(attribute: created_at)
updatedAt: DateTime @rename(attribute: updated_at)
}
在使用type时需要注意。
如果不指定$dates,会报错,所以请务必在Model中指定$dates。
(如果只有created_at和updated_at这两个字段,可以不显式添加,但考虑到还有其他字段,最好全部添加。)
class User extends Authenticatable
{
// 省略
// $datesで指定しておく
protected $dates = [
'email_verified_at', 'created_at', 'updated_at'
];
}
【2018/12/27更新】批量执行多个查询
我追加了有关GraphQL最大优点的描述,即可以将端点集中在一起。
query multiQuerySample(
$user1Id: ID
$user2Id: ID
) {
user1: user(id: $user1Id) {
id
}
user2: user(id: $user2Id) {
id
}
}
以这种方式,您可以同时执行多个查询。

通过这个方法,您可以轻松地解决频繁调用 RestAPI 的痛苦!
【2019/01/10添加】批量注册多个数据
我想有时候可能会有一次性注册多个数据的情况。
虽然我不确定这种方法是否正确,但我会附加说明。
最终目标
我希望通过执行以下查询来实现批量注册多个用户。
mutation {
createUsers(users: [
{name: "taro", email: "taro@example.com", password: "xxxxxxxx"},
{name: "hanako", email: "hanako@example.com", password: "xxxxxxxx"}
])
{
res
}
}
接收数组型数据
我认为要注册多个项目,需要将数组类型的数据传递给GraphQL,但我认为可能需要定义标量才能获取。(如果我说错了,请您指正,谢谢)
所以,首先我们需要定义一个独特的标量来设置数组的类型。
从命令行界面创建一个标量。
$ php artisan lighthouse:scalar Users
执行后,将在 app/GraphQL/Scalars 目录下创建一个名为 Users.php 的文件。
接下来将开始实现内容。
首先,这里是刚创建的文件的内容。
<?php
namespace App\Http\GraphQL\Scalars;
use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\ScalarType;
/**
* Read more about scalars here http://webonyx.github.io/graphql-php/type-system/scalar-types/
*/
class Users extends ScalarType
{
/**
* Serializes an internal value to include in a response.
*
* @param string $value
* @return string
*/
public function serialize($value)
{
// Assuming the internal representation of the value is always correct
return $value;
// TODO validate if it might be incorrect
}
/**
* Parses an externally provided value (query variable) to use as an input
*
* @param mixed $value
* @return mixed
*/
public function parseValue($value)
{
// TODO implement validation
return $value;
}
/**
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input.
*
* E.g.
* {
* user(email: "user@example.com")
* }
*
* @param Node $valueNode
* @param array|null $variables
*
* @return mixed
*/
public function parseLiteral($valueNode, array $variables = null)
{
// TODO implement validation
return $valueNode->value;
}
}
我可能会提供一些补充的评论,但让我大致解释一下每个函数的作用。
-
- serialize
DBから取得する際に値を変換する為の関数
parseValue
引数を変数で受け取った値を検証、加工する為の関数
parseLiteral
引数を直接受け取った値(変数定義せずクエリに直接指定する場合)を検証、加工する為の関数
我觉得如果看一下最初创建的 Nuwave\Lighthouse\Schema\Types\Scalars\Datetime 可能会有所启发,所以为了保险起见,我会把它放上来。
<?php
namespace Nuwave\Lighthouse\Schema\Types\Scalars;
use Carbon\Carbon;
use GraphQL\Error\Error;
use GraphQL\Utils\Utils;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Language\AST\StringValueNode;
class DateTime extends ScalarType
{
public function serialize($value): string
{
return $value->toAtomString();
}
public function parseValue($value): Carbon
{
try {
$dateTime = Carbon::createFromFormat(Carbon::DEFAULT_TO_STRING_FORMAT, $value);
} catch (\Exception $e) {
throw new Error(Utils::printSafeJson($e->getMessage()));
}
return $dateTime;
}
public function parseLiteral($valueNode, array $variables = null): Carbon
{
if (! $valueNode instanceof StringValueNode) {
throw new Error('Query error: Can only parse strings got: '.$valueNode->kind, [$valueNode]);
}
try {
$dateTime = Carbon::createFromFormat(Carbon::DEFAULT_TO_STRING_FORMAT, $valueNode->value);
} catch (\Exception $e) {
throw new Error(Utils::printSafeJson($e->getMessage()));
}
return $dateTime;
}
}
我們現在開始實際實施內容。
從網絡上的樣本來看,對於日期類型、電子郵件地址等基本值通常會添加某種控制,但這次由於要使用陣列,我們需要考慮GraphQL內部的類型。
結果是這樣的。
<?php
namespace App\Http\GraphQL\Scalars;
use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\ObjectFieldNode;
class Users extends ScalarType
{
public function serialize($value)
{
return $value;
}
public function parseValue($value)
{
return $value;
}
public function parseLiteral($valueNode, array $variables = null)
{
// 今回はデータが配列であるかどうかだけをチェックしていますが、実際はもう少ししっかりチェックすることになると思います
if (!$valueNode instanceof ListValueNode) {
throw new Error('Query error: Can only parse List got: '.$valueNode->kind, [$valueNode]);
}
return $this->argValue($valueNode);
}
/**
* GraphQLが持つ型から最終的な配列データを取得する
*
* @param $arg mixed
* @return array
*/
private function argValue($arg)
{
if ($arg instanceof ListValueNode) {
return collect($arg->values)->map(function ($node) {
return $this->argValue($node);
})->toArray();
}
if ($arg instanceof ObjectValueNode) {
return collect($arg->fields)->mapWithKeys(function ($field) {
return [$field->name->value => $this->argValue($field)];
})->toArray();
}
if ($arg instanceof ObjectFieldNode) {
return $this->argValue($arg->value);
}
return $arg->value;
}
}
通过查看以上实例,可以看出argValue的内容非常复杂且相当繁琐。
我根据Nuwave\Lighthouse\Support\Traits\HandlesDirectives中的Trait中的argValue进行了参考,并稍微进行了整理来创建它。
我认为到目前为止,应该可以接收参数了。
创建一个用于注册多个数据的突变。
因为我觉得也许有没有使用自己编写的解析器来实现变异的方法,所以我要创建一个变异。具体的创建步骤已在上述说明中写明,所以我会省略细节。
$ php artisan lighthouse:mutation CreateUsers
我使用上述命令创建了一个文件,并按照以下方式实施。
<?php
namespace App\Http\GraphQL\Mutations;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use App\User;
class CreateUsers
{
public function resolve($rootValue, array $args, GraphQLContext $context = null, ResolveInfo $resolveInfo)
{
User::insert($args['users']);
$res = 'ok';
return compact('res');
}
}
突變只需要這麼簡單就可以了。
如果最后记述实际突变的定义,那就完成了。
scalar Users @scalar(class: "App\\Http\\GraphQL\\Scalars\\Users")
extend type Mutation {
createUsers(
users: Users
): DummyResponse
@field(resolver: "App\\Http\\GraphQL\\Mutations\\CreateUsers@resolve")
}
由于上述工作已经完成,现在我们来实际执行一下!

我成功地注册了多个数据!
【2019/01/28更新】进行排序订单
我希望尽快在官方中实现,但目前要使用order by功能,需要自己创建指令(或通过解析器解决)。
我会继续介绍指令添加方法。
达到目标
我将尝试用这种查询样式进行排序。
query {
users(
count: 3
queryOrder: {
column: "id",
order: DESC
}
) {
data {
id
}
}
}
创建自定义指令。
首先创建自定义指令。
Directiveの設定が行えるようです。
/*
|--------------------------------------------------------------------------
| Directives
|--------------------------------------------------------------------------
|
| List directories that will be scanned for custom server-side directives.
|
*/
'directives' => [__DIR__.'/../app/Http/GraphQL/Directives'],
根据@eq指令所在的Nuwave\Lighthouse\Schema\Directives\Args\EqualsFilterDirective,将其复制粘贴到指定的目录中创建。
看起来就是这个样子。
```<?php
namespace Nuwave\Lighthouse\Schema\Directives\Args;
use Nuwave\Lighthouse\Schema\Values\ArgumentValue;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgMiddleware;
use Nuwave\Lighthouse\Support\Traits\HandlesQueryFilter;
class EqualsFilterDirective extends BaseDirective implements ArgMiddleware
{
use HandlesQueryFilter;
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return 'eq';
}
/**
* Resolve the field directive.
*
* @param ArgumentValue $argument
* @param \Closure $next
*
* @return ArgumentValue
*/
public function handleArgument(ArgumentValue $argument, \Closure $next): ArgumentValue
{
$this->injectFilter(
$argument,
function ($query, string $columnName, $value) {
return $query->where($columnName, $value);
}
);
return $next($argument);
}
}
只需更改上述的 injectFilter ,看起来就可以进行了吧?我认为你可以猜到这一点。所以,我会试着按那个方向去做。
<?php
namespace App\Http\GraphQL\Directives;
use Nuwave\Lighthouse\Schema\Values\ArgumentValue;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgMiddleware;
use Nuwave\Lighthouse\Support\Traits\HandlesQueryFilter;
class OrderByDirective extends BaseDirective implements ArgMiddleware
{
use HandlesQueryFilter;
/**
* Name of the directive.
*
* @return string
*/
public function name()
{
return 'orderBy';
}
/**
* Resolve the field directive.
*
* @param ArgumentValue $argument
* @param \Closure $next
*
* @return ArgumentValue
*/
public function handleArgument(ArgumentValue $argument, \Closure $next)
{
$this->injectFilter(
$argument,
function ($query, string $columnName, $value) {
// ここでOrder Byをする
return $query->orderBy($value['column'], array_key_exists('order', $value) ? $value['order'] : 'asc');
}
);
return $next($argument);
}
}
到此为止,指令方面的处理已经完成。
接受参数
这次我们将像@eq一样使用ArgMiddleware接口来接收参数。本次的目标是针对单个项目进行排序,但仍然需要两个选项,即列升序/降序。因此,我们将进行设置以接收两个值。
- users: [User!]! @paginate(type: "paginator" model: "App\\User")
+ users(queryOrder: QueryOrder @orderBy): [User!]! @paginate(type: "paginator" model: "App\\User")
+
+ input QueryOrder {
+ column: String!
+ order: Order = ASC
+ }
+
+ enum Order {
+ ASC @enum(value: "ASC")
+ DESC @enum(value: "DESC")
+ }
因为现在可以将此作为参数接收,所以让我试着执行一下!
我已经以正确的排序顺序成功获取了数据!

【2019/02/07更新】关于ID类型的注意事项。
我最近第一次了解到,ID在這裡是以String形式存在的。對於Laravel的標準ID,它是以Int形式存在的,所以在設置ID時不要被示例誤導,應該設定為Int。
最后
虽然说要充分使用,但最后却没有充分使用就结束了?
我试用了Laravel的GraphQL库中似乎最活跃的Lighouthouse,然而仍然感觉它并没有那么成熟。
当遇到困难时,我认为查看这方面的源代码可以解决问题。
如果无法解决,我建议尝试创建resolver,可以在官方网站上找到相关信息。

以这种方式,我觉得如果自己也以培养开源软件的意识来生活,会感到很有趣!(虽然我自己还没有提升我的公众形象?)
在我们触及之前,关于是否能成为替代Restful风格API的讨论。
- まとめて操作出来るのでAPI発行回数が少なくて済むってメリットがあるから複数のAPIを叩かなきゃいけないところだけGraphQL使って、それ以外は今まで通りRestfulライクなAPIを叩く感じになるかぁ
想着这样,当我亲身触摸时
-
- SwaggerDocのように鬼のようなコードを書かずにドキュメントが生成出来る
- DevToolsも使いやすい
因为有一种令人感动的情感,我觉得将除了认证API之外的所有内容都整合到GraphQL中也是可行的。
我还没有开始接触Code生成器的相关内容,但是我在考虑一旦codegen得到完善,它可能与TypeScript更加兼容,从而一举成为替代Restful API的选择。我想根据这篇文章或其他的内容,在某个时机尝试一下。
我认为Lighouthouse目前仍有很大的发展空间,因此可以自由加入Slack工作空间,且PR也受到热烈欢迎,我稍微想过能为其成长做出一点贡献就好?
由于我仍处于GraphQL的初学者阶段,对于本文的内容若有任何问题或建议,敬请在评论中指正???!!!