使用 GraphQL API 构建基于 Issues 的博客
身边有很多人把 GitHub 的 Issues 用作个人博客,它具有写作方便、免于部署的优点,但是由于需要从仓库的 Issues 访问,定制化程度很低。
GitHub 提供了访问仓库的 Issues 的 API,我们可以自己开发定制前端页面,发布到 GitHub Pages,前端调用 API 读取 Issues 动态渲染页面。这样我们依旧能够在 GitHub 上新建/修改文章,同时不用再去进行发布操作。
简单实现
Github 当前提供了 2 种版本的 API:
它们都能满足我们的需求,但是 REST 版本返回的数据较多,里面有很多我们不需要的内容,所以这里我们选择可定制性更强的 GraphQL 版本。
查询语句的构建可以在 GraphQL API Explorer 进行。
查询语句:
{
search(type: ISSUE, query: "label:tip repo:hanai/blog_source", first: 10) {
nodes {
... on Issue {
title
body
bodyHTML
createdAt
labels(first: 8) {
nodes {
name
}
}
lastEditedAt
}
}
}
}
前端页面请求 API:
fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
authorization: "bearer {{token}}"
},
body: JSON.stringify({
query: query
})
})
.then(res => res.json())
.then(console.log);
安全实现
直接将自己 GitHub 的 access token 暴露在公网环境是非常危险的(本例中 access token 对公开仓库具有写权限),实际使用中可以借助云服务商提供的函数计算服务作为网关来请求 GitHub 的 API:
以阿里云为例,代码如下:
const process = require('process');
const https = require("https");
module.exports.handler = function (req, resp, context) {
const { queries, headers } = req;
const { origin } = headers;
const ak = process.env.ak;
if (/\.ihanai\.com/.test(origin)) {
const postData = JSON.stringify({
'query': `{
search(type: ISSUE, query: "label:tip repo:hanai/blog_source", first: 10) {
nodes {
... on Issue {
title
body
bodyHTML
createdAt
labels(first: 8) {
nodes {
name
}
}
lastEditedAt
}
}
}
}`
});
const httpReq = https.request({
host: 'api.github.com',
path: '/graphql',
method: 'POST',
headers: {
authorization: "bearer " + ak,
'Content-Length': Buffer.byteLength(postData),
'user-agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36"
}
}, res => {
res.setEncoding('utf8');
const chunks = [];
res.on('data', (chunk) => {
chunks.push(chunk);
});
res.on('end', () => {
resp.setStatusCode(200);
resp.setHeader('content-type', 'application/json');
resp.setHeader('Access-Control-Allow-Origin', origin);
const json = JSON.parse(chunks.join(''));
resp.send(JSON.stringify(json.data.search));
});
});
httpReq.write(postData);
httpReq.end();
} else {
resp.setHeader('Access-Control-Allow-Origin', 'https://blog.ihanai.com');
resp.send('');
}
}