文章目录加载中

在Nodejs中使用Graphql

# 什么是 GraphQL?

它是一种 api 查询语言。使用者按照规范描述数据结构,可以来获取所需数据;开发者需要做的是编写获取数据的响应函数,以及声明查询字段的类型。

# GraphQL 的用途

主要用于查询和聚合数据。开发者只需要声明查询字段类型,以及每个字段数据的获取函数。前端通过 graphql 语法,获取自己想要的任何字段。

这样就不需要每个字段或者新的业务模块数据,再开一个 restful api,所有的查询都收到一个接口中。

# GraphQL 和 Restful API

最初的时候,很多人觉得是替代 restful api。现在多是和 restful api 配合使用。举 2 个例子。

第一个例子,GraphQL 更多的用于查询和聚合数据,也就是“读”的场景。而更改数据大多数是敏感操作,还会涉及鉴权、缓存、节流等复杂逻辑,一般都封装单独的 restful api 来给前端调用。

第二个例子,GraphQL 以 Restful API 的方式暴露给前端使用。例如在腾讯云 CVM 控制台中,打开 Console -> Network,搜索 graphql 关键字就能看到请求。这个请求就是一个支持 graphQL 查询的 restful API。在实际开发中,可以根据业务模块,抽出不同的支持 graphQL 的 restful api,方便维护;也可以将其放入一个大的 restful api 中。

# 代码实战

# nodejs 配置文件

{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon -e graphql,js ./www.js"
  },
  "author": "xin-tan.com",
  "license": "ISC",
  "dependencies": {
    "@graphql-tools/graphql-file-loader": "^6.2.7",
    "@graphql-tools/load": "^6.2.8",
    "axios": "^0.21.1",
    "express": "^4.17.1",
    "express-graphql": "^0.12.0",
    "graphql": "^15.5.0"
  }
}

# GraphQL 类型声明

对于 graphql,开发需要声明查询字段的类型。例如:

type User {
  name: String
  age: Int
}

type Duck {
  weight: Float
  color: String
  owner: User
  description(id: String!): String
}

type Query {
  hello: String
  author: User
  # 获取指定用户信息
  # !意思是name不能为空
  getUser(name: String!): User

  # 获取所有用户信息
  # 注意:没有参数的情况,就是普通情况
  getAllUsers: [User]

  getDuck(id: String!): Duck
}

# 输入类型
input UserInput {
  name: String
  age: Int
}

type Mutation {
  # 输入类型必须是input类型
  createUser(userInfo: UserInput): User
  updateUser(userInfo: UserInput): User
}

# GraphQL 获取数据函数

规定了数据类型之后,会定义对应的数据字段的获取逻辑。例如:

const userMap = {
  author: {
    name: "author",
    age: 23
  },
  user1: {
    name: "user1",
    age: 18
  }
};

const duckMap = {
  0: {
    weight: 1,
    color: "green",
    owner: userMap.author,
    description: ({ id }) => {
      return `I am duck, my info is ${JSON.stringify(duckMap[id])}`;
    }
  }
};

const root = {
  hello: () => "Hello world!",
  author: () => {
    return userMap.author;
  },
  getUser: ({ name }) => {
    return userMap[name];
  },
  getAllUsers: () => {
    const users = [];
    for (let name in userMap) {
      users.push(userMap[name]);
    }
    return users;
  },
  getDuck: ({ id }) => {
    return duckMap[id];
  },

  createUser: ({ userInfo }) => {
    userMap[userInfo.name] = userInfo;
    console.log(">>> invoke createUser mutation");
    return userInfo;
  },
  updateUser: ({ userInfo }) => {
    if (!userMap[userInfo.name]) {
      throw new Error(`Please create user ${userInfo}`);
    }
    userMap[userInfo.name].age = userInfo.age;
    console.log(">>> invoke updateUser mutation");
    return userInfo;
  }
};

module.exports = {
  root
};

前端传入查询语句,字段会自动匹配根对象对应的函数。下面的查询语句,就会执行 root.hello() 函数,并且返回结果。

query RootQuery {
  hello
}

注意:query 中的字段也可以实现数据更新逻辑,但按照推荐,任何涉及数据更新的改动,都应该放入到 mutation 中。

# 提供 GraphQL 访问服务

方法 1:通过 buildSchema 直接构建 schema。

const { buildSchema } = require("graphql");
const schema = buildSchema(`
type Query {
    hello: String
    name: String
}
`);
const app = express();
app
  .use(
    "/graphql",
    graphqlHTTP({
      rootValue: root, // root 就是上面的 root 对象
      schema,
      graphiql: true
    })
  )
  .listen(4000, () =>
    console.log("Now browse to http://localhost:4000/graphql")
  );

这么写的缺陷就是 graphql 字段声明放在了 js 文件中,编辑器没法高亮+格式化,也不方便单独维护。随着字段增多,js 文件会变得非常冗长。

方法 2:使用 graphql 配套的工具库

这些工具库支持读取 graphql 文件,也支持模块化预发,并且将其加载进来。

例如方法 1 的 GraphQL 类型声明,就可以将其放入root.graphql文件中,然后读取并加载。

const { loadSchema, loadSchemaSync } = require("@graphql-tools/load");
const { GraphQLFileLoader } = require("@graphql-tools/graphql-file-loader");
const schema = loadSchemaSync("./graphql/root.graphql", {
  loaders: [new GraphQLFileLoader()]
});
// ...

# 编写 GraphQL 查询语句

读的操作都在 Query 中,写的操作都在 Mutation 中。

前端在使用的时候,查询语句如下:

# 可以给查询或者变动起一个别名,在可视化运行的时候(/graphql),上方可以选择使用哪个query或者mutation
query RootQuery {
  hello
  getAllUsers {
    name
    age
  }
  getUser(name: "user1") {
    name
    age
  }
  # 注意 如果返回的是对象,那么在大括号内一定要规定需要返回的字段,否则语法报错
  getDuck(id: "0") {
    weight
    color
    # 同理,由于owner类型是User对象,因此这里需要规定返回字段,否则语法报错
    owner {
      name
    }
    description(id: "0")
  }
}

mutation CreateUser3 {
  # 默认不给数据起别名的话,例如最后的updateUser,那么返回的结果会放到updateUser字段中
  # 起别名,才可以触发多个获取数据的函数
  user3: createUser(userInfo: { name: "user3", age: 30 }) {
    name
    age
  }

  user4: createUser(userInfo: { name: "user4", age: 30 }) {
    name
  }

  updateUser(userInfo: { name: "user3", age: 40 }) {
    name
    age
  }
}

其中,mutation 中写操作是串行的,query 中读操作是并发的。可以给 mutation 或者 query 起名字,也可以给返回结果起别名(例如 CreateUser3 中的 user3)。

# 参考链接

本文来自心谭博客:xin-tan.com,经常更新web和算法的文章笔记,前往github查看目录归纳:github.com/dongyuanxin/blog