快速入门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,我们可以模拟访问。

如果要使用客户端:

进入项目,安装 @apollo/client
pnpm ipnpm 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 }, },}
查看后台打印:

其他参数暂且不管,反正第二个参数,就是传递过来的参数内容,我们稍微修改判断一下
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, },}
再次查询,也可以查到刚刚添加的员工

在前端,同样通过相应的方式进行处理,比如在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;