Skip to content

快速入门GraphQL

快速入门GraphQL#

我们常见的理所当然的模式,是后端提供接口,前端调用对应接口获取数据或者传递数据,然后页面展示。

这种接口返回什么数据是后端决定的,前端只是根据restful的方式传递数据,或者获取数据。

不同场景下需要的数据不同,这时候可能就需要后端又要新开发一个接口。所以,当需求有变动的时候,接口就需要跟着有变化,这样不但很容易导致一大堆类似的接口,也大大增加了沟通成本

为了简化这种方式,所以前端就有了GraphQL,我们在进行BFF开发的时候,是非常常用的工具。

用了 GraphQL 之后,返回什么数据不再是服务端说了算,而是客户端自己决定。

服务端只需要提供一个接口,客户端通过这个接口就可以取任意格式的数据,实现 CRUD,或者干脆前端自己去搞定也行。

我们先写个 demo 快速入门一下,首先创建一个简单node工程,安装需要用到的包:

pnpm add @apollo/server

然后创建index.js文件:

import { ApolloServer } from '@apollo/server'
const typeDefs = `
type Employee {
id: String,
name: String,
sex: String
age: Int
}
type Department {
id: String,
name: String,
employees: [Employee]
}
type Query {
employees: [Employee],
departments: [Department],
}
schema {
query: Query,
}
`

定义了一个 Employee 员工的对象类型,有 id、name、sex、age 这几个字段,还有Department部门的对象类型,employees字段是部门下的员工

关键是,还定义了查询的入口,可以通过方法employees和departments对应的信息,这样定义类型的部分就是一个 schema

对象类型和对象类型之间有关联关系,部门关联了员工、员工其实也可以关联部门,这就是一个图,也就是 graph。

GraphQL 全称是 graph query language,就是从这个对象的 graph 中查询数据

我们声明的只是对象类型的关系,接下来就是具体数据了,取数据的部分叫做 resolver

let employees = [
{
id: '1',
name: async () => {
await '取数据'
return 'jack'
},
sex: '',
age: 18,
},
{
id: '2',
name: 'rose',
sex: '',
age: 20,
},
{
id: '3',
name: 'tom',
sex: '',
age: 31,
},
]
const departments = [
{
id: '1',
name: '技术部',
employees: employees,
},
]
const resolvers = {
Query: {
employees: () => employees,
departments: () => departments,
},
}

resolver 是取对象类型对应的数据的,我这里只是模拟了一些数据,其实在里面执行函数,执行 sql、访问接口等都可以,最终返回取到的数据

有了 schema 类型定义,有了取数据的 resovler,就可以启动 graphql

import { startStandaloneServer } from '@apollo/server/standalone'
// ......
const server = new ApolloServer({
typeDefs,
resolvers,
})
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
})
console.log(`graphql服务监听在端口: ${url}`)

注意我们这里创建的是一个简单工程,全部是ESM引入,所以在package.json中,需要加入 type:module

跑起服务之后,其实就是一个沙盒sandbox,我们可以模拟访问。

image-20250305162309642

如果要使用客户端:

image-20250305162554139

进入项目,安装 @apollo/client

pnpm i
pnpm add @apollo/client

在main.tsx中引入,创建 ApolloClient 并设置到 ApolloProvider

import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000',
cache: new InMemoryCache(),
});
createRoot(document.getElementById('root')!).render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
)

然后我们只需要在需要的页面使用 useQuery 发出请求即可,比如App.tsx

import { gql, useQuery } from "@apollo/client";
const getEmployees = gql`
query Employees {
employees {
id
name
sex
age
}
}
`;
type Employee = {
id: string;
name: string;
sex: string;
age: number;
};
type EmployeeList = {
employees: Array<Employee>;
};
function App() {
const { loading, error, data } = useQuery<EmployeeList>(getEmployees);
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return (
<>
{data?.employees?.map((emp) => {
return (
<li key={emp.id}>
{emp.name} --- {emp.age} --- {emp.sex}
</li>
);
})}
</>
);
}
export default App;

前面都是查询全部,如果是按照某个条件进行查询呢?这个其实才是我们的重点。

处理起来很简单,我们还是只需要两个步骤,第一步,定义schema方法

type Query {
employees: [Employee],
departments: [Department],
employeesByDepartmentName(name: String!): [Employee]
}

新加一个 query 入口,声明一个 name 的参数。String 后的 ! 代表不能为空,和TS的定义差不多

然后处理它对应的 resolver

const resolvers = {
Query: {
employees: () => employees,
departments: () => departments,
employeesByDepartmentName: async (...args) => {
console.log(args)
await '异步查询操作'
return employees
},
},
}

image-20250305170026043

查看后台打印:

image-20250305170100754

其他参数暂且不管,反正第二个参数,就是传递过来的参数内容,我们稍微修改判断一下

const resolvers = {
Query: {
employees: () => employees,
departments: () => departments,
employeesByDepartmentName: async (_, { name }) => {
console.log(name)
await '异步查询操作'
return name === '技术部' ? employees : []
},
},
}

这是查询,如果是增删改呢?我们需要单独定义,主要是要加入Mutation,在schema中添加定义

const typeDefs = `
type Employee {
id: String,
name: String,
sex: String
age: Int
}
type Department {
id: String,
name: String,
employees: [Employee]
}
type Query {
employees: [Employee],
departments: [Department],
employeesByDepartmentName(name: String!): [Employee]
}
type Res {
success: Boolean
id: String
}
type Mutation {
addEmployee(name:String! age:Int! sex:String!): Res
updateEmployee(id: String! name:String! age:Int! sex:String!): Res
deleteEmployee(id: String!): Res
}
schema {
query: Query,
mutation: Mutation
}
`

然后在resolver也要有对应的实现

async function addEmployee(_, { name, age, sex }) {
employees.push({
id: Math.ceil(Math.random() * 100) + '',
name,
age,
sex,
})
return {
success: true,
id: Math.ceil(Math.random() * 100) + '',
}
}
async function updateEmployee(_, { id, name, age, sex }) {
employees.forEach((item) => {
if (item.id === id) {
item.name = name
item.age = age
item.sex = sex
}
})
return {
success: true,
id,
}
}
async function deleteEmployee(_, { id }) {
employees = employees.filter((item) => item.id !== id)
return {
success: true,
id: id,
}
}
const resolvers = {
Query: {
employees: () => employees,
departments: () => departments,
employeesByDepartmentName: async (_, { name }) => {
console.log(name)
await '异步查询操作'
return name === '技术部' ? employees : []
},
},
Mutation: {
addEmployee,
updateEmployee,
deleteEmployee,
},
}

image-20250305171054436

再次查询,也可以查到刚刚添加的员工

image-20250305171221538

在前端,同样通过相应的方式进行处理,比如在App.tsx中,加入新增的处理

import { gql, useQuery, useMutation } from "@apollo/client";
const getEmployees = gql`
query Employees {
employees {
id
name
sex
age
}
}
`;
const addEmployee = gql`
mutation AddEmployee($name: String!, $age: Int!, $sex: String!) {
addEmployee(name: $name, age: $age, sex: $sex) {
success
id
}
}
`;
type Employee = {
id: string;
name: string;
sex: string;
age: number;
};
type EmployeeList = {
employees: Array<Employee>;
};
function App() {
const { loading, error, data } = useQuery<EmployeeList>(getEmployees);
const [createEmployee] = useMutation(addEmployee, {
refetchQueries: [getEmployees], // 重新查询,刷新页面
});
async function onClick() {
await createEmployee({
variables: {
name: "lily",
age: 20,
sex: "",
},
});
}
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return (
<>
<button onClick={onClick}>新增</button>
<ul>
{data?.employees?.map((emp) => {
return (
<li key={emp.id}>
{emp.name} --- {emp.age} --- {emp.sex}
</li>
);
})}
</ul>
</>
);
}
export default App;