相信有不少小伙伴和我一样用github issues记录自己的blog,但是久而久之也发现了一些小问题,比如
- 国内访问速度比较慢
- 不能自定义主题样式等等
- 不能在博客中加入自己想要的功能
正好最近又在学nextjs,react做ssr的神器,nextjs提供了next export
这个命令,如果不熟悉next小伙伴可以先去官网阅读一下
nextjs.org/docs#static…
nextjs的教程,推荐一下技术胖的免费视频教程 jspang.com/posts/2019/…
这个命令可以把react项目导出成静态html页面,这样在性能和seo方面考虑都是最优解。配合这个命令我就有了个折腾的想法,能不能把github issues导入到项目里,然后配合这个命令生成我的静态html博客呢。
目标
配合nextjs实现一个命令把自己的github issues里的文章导出成自己的博客html页面。 这样的好处是
- 可以折腾
- 可以折腾
- 可以折腾
开玩笑的,真正的好处是
- 编写博客时可以利用github完善的编辑器。
- 可以把github issues作为自己的数据存储服务,不用担心数据丢失和维护。
- 可以在自己的博客内加入自己想要的任何功能。
- 可以利用react的完整能力,完善的第三方生态。
- 生成的博客是html格式的页面,回归原始,回归本心,seo和性能最优化。
尝鲜使用
项目地址
github.com/sl1673495/n… 先clone到本地。
运行
安装依赖:
yarn
开发环境:
yarn dev
导出博客(会放在out目录下,导出后请进入out目录后启动anywhere或者http-server类似的静态服务然后访问):
yarn all
说明
只需要在config.js里改掉repo的owner和name两个字段,
分别对应你的github用户名和博客仓库名,
然后执行yarn all
,
就可以在out目录下生成静态博客目录。 config中填写client_id和client_secret可以用于取消请求限制。
(可选)使用now部署
进入out目录,然后执行now
,页面就会自动部署了。
预览地址
对应的github博客: github.com/sl1673495/b…
自动生成的博客 blog.shanshihao.cn
可以先访问一下生成博客的效果,可以看到静态html页面的速度是非常快的,体验在某些方面可以说比起spa和ssr都要好。
代码解析
想要实现上面所说的功能,需要先把功能拆解一下。
- 发起请求拉取自己github仓库里的博客,获取文章存成md格式在本地。
- 根据nextjs的约定,把生成的md文章改写成jsx,写入到pages目录下。(这样nextjs就会识别成为一个个路由)
- 根据自定的规则生成首页jsx,写入pages文件夹。
- 使用next export导出博客。
首先先用next脚手架生成一个项目,然后在项目下建立builder文件夹,用来编写逻辑。
全局配置
全局的一些配置我放在了config.js中,拉取我项目的小伙伴只需要更改里面的配置,就可以一键生成你自己的静态博客了。
const path = require('path')
const mdDir = path.resolve(__dirname, './md')
module.exports = {
mdDir,
// 用于更改标题上的用户信息
user: {
name: 'ssh',
},
// 用于同步github的博客
repo: {
owner: 'sl1673495',
name: 'blogs',
},
// 可选 如果申请了github Oauth app的话
// 可以填写用于取消github请求限制
client_id: '',
client_secret: '',
}
repo
字段中的信息决定了请求会去哪个仓库下拉取issues生成博客,user
下的字段定义了首页显示的用户名,client_id
和client_secret
的作用后面会讲。
同步博客
builder/sync.js
/**
* 同步github上的blogs
*/
const axios = require('axios')
const fs = require('fs')
const path = require('path')
const { rebuild } = require('./utils')
const {
repo: { owner, name }, mdDir,
} = require('../config')
const GITHUB_BASE_URL = 'https://api.github.com'
module.exports = async () => {
// 清空md文件夹
rebuild(mdDir)
try {
// 请求github博客内容
const { data: blogs } = await axios.get(
`${GITHUB_BASE_URL}/repos/${owner}/${name}/issues`,
)
// 创建md文件
blogs.forEach((blog) => {
fs.writeFileSync(path.join(mdDir, `${blog.id}.md`), blog.body, 'utf8')
})
return blogs
} catch (e) {
console.error('仓库拉取失败,请检查您的用户名和仓库名')
throw e
}
}
复制代码
其中rebuild函数就是用node的fs模块把文件夹删除再重新创建,
这个函数的作用就是把github仓库里的issues拉取下来,并且写入到我们自己定义的存放md的文件夹中。
把博客转为jsx写入pages目录
builder/page-builder.js
/**
* 生成nextjs识别的pages
*/
const fs = require('fs')
const path = require('path')
const MarkdownIt = require('markdown-it')
const axios = require('axios')
const {
mdDir,
} = require('../config')
const { rebuild, copyFolder } = require('./utils')
const md = new MarkdownIt({
html: true,
linkify: true,
})
const handleMarkdownBody = (body) => {
return encodeURIComponent(md.render(body))
}
const pageTemplateDir = path.resolve(__dirname, '../pages-template')
const pageDir = path.join(__dirname, './pages')
module.exports = async (blogs) => {
// 清空pages文件夹
rebuild(pageDir)
// 把pages-template目录的模板拷贝到pages下
await copyFolder(pageTemplateDir, pageDir)
// 读取md文件夹下的所有md文件的名字(其实就是issue的id)
const mdPaths = fs.readdirSync(mdDir)
const convertMdToJSX = async (mdPath) => {
const mdContent = fs.readFileSync(path.join(mdDir, mdPath)).toString()
// pages下的页面根据id命名
const mdId = Number(mdPath.replace('.md', ''))
const blog = blogs.find(({ id }) => id === mdId)
if (blog) {
// body已经在md文件夹内了 不需要了
const { body, ...restBlog } = blog
const { comments_url } = restBlog
// 获取评论信息
const { data: comments } = await axios.get(comments_url)
.catch((err) => {
console.error('评论生成失败,', err)
})
// 处理评论的markdown文本 并且写入到html字段中
comments.forEach(({ body: commentBody }, index) => {
const commentHtml = handleMarkdownBody(commentBody)
comments[index].html = commentHtml
})
// 页面的jsx
const pageContent = `
import Page from '../components/Page'
const pageProps = {
blog: ${JSON.stringify(restBlog)},
comments: ${JSON.stringify(comments)},
html: \`${handleMarkdownBody(mdContent)}\`,
}
export default () => <Page {...pageProps}/>
`
// 写入文件
fs.writeFileSync(path.join(pageDir, `${mdId}.jsx`), pageContent, 'utf8')
}
}
const tasks = mdPaths.map(convertMdToJSX)
await Promise.all(tasks)
}
复制代码
这个函数需要接受我们刚刚请求到的issues数据,用来生成标题,因为在上一步中使用了issue的id去命名博客,所以可以在这一步中读取md文件夹下的所有issue id,就可以在这个blogs数组中找到对应的issue信息,这个issue对象中有github api给我们提供的comments_url,可以用来请求这个issue下的所有评论,这里也把它一起请求到。
// 把pages-template目录的模板拷贝到pages下
await copyFolder(pageTemplateDir, pageDir)
函数刚开始这一步的作用是因为每次执行这个函数都需要用rebuild函数清空pages文件夹,防止同步不同账号的数据以后产生数据混乱,但是nextjs中我们可能会自定义_document.js
或者_app.js
,这玩意也不需要动态生成,所以我们就先在pages-template文件夹下提前存放好这些组件,然后执行的时候直接拷贝过去就好了。
convertMdToJSX
这个方法就是把md文件转为nextjs可以识别的jsx格式,
`
import Page from '../components/Page'
const pageProps = {
blog: ${JSON.stringify(restBlog)},
comments: ${JSON.stringify(comments)},
html: \`${handleMarkdownBody(mdContent)}\`,
}
export default () => <Page {...pageProps}/>
`
复制代码
其实就是这么个格式,注意写入的时候要用JSON格式化一下,否则写入的会是[Object object]这样的文字。
另外我们在这一步就要配合markdown-it
插件把md内容转成html格式,并且通过encodeURIComponent转义后再写入我们的jsx内,否则会出现很多格式错误。
最后利用Promise.all把convertMdToJSX这个异步方法批量执行一下。
这一步结束后,我们的pages目录大概是这个样子
点开其中的一个jsx
这已经是react可以渲染的jsx文件了,快要成功了~
生成首页
builder/page-builder.js
/**
* 生成博客首页
*/
const fs = require('fs')
const path = require('path')
const indexPath = path.resolve(__dirname, '../pages/index.jsx')
module.exports = (blogs) => {
const injectBlogs = JSON.stringify(
blogs.map(({ body, ...restBlog }) => restBlog),
)
// 把blog数据注入到首页中
const indexJsx = `
import React from 'react'
import Link from 'next/link'
import Layout from '../components/Layout'
import Main from '../components/Main'
const blogs = ${injectBlogs}
const Home = () => (
<Layout>
<Main blogs={blogs} />
</Layout>
)
export default Home
`
fs.writeFileSync(indexPath, indexJsx, 'utf8')
}
复制代码
这一步没啥好说的,一样的套路,写入jsx生成首页。
执行入口
最后我们在入口把这些方法串起来。
const { withOra, initAxios } = require('./utils')
const syncBlogs = require('./sync')
const pageBuilder = require('./page-builder')
const indexBuilder = require('./index-builder')
const start = async () => {
initAxios()
// 同步github上的blogs到md文件夹
const blogs = await withOra(
syncBlogs,
'正在同步博客中...',
)
// 抓取评论,生成pages下的博客页面。
await withOra(
() => pageBuilder(blogs),
'正在生成博客页面中...',
)
// 生成首页
indexBuilder(blogs)
}
start()
复制代码
initAxios
这个函数目的是在请求的时候可以带上github的client_id
和client_secret
信息,如果你在github申请了OAuth app就会拿到俩个东西,带上的话就可以更频繁的请求api,否则github会限制同一个ip下请求调用的次数。
function initAxios() {
axios.default.interceptors.request.use((axiosConfig) => {
if (client_id) {
if (!axiosConfig.params) {
axiosConfig.params = {}
}
axiosConfig.params.client_id = client_id
axiosConfig.params.client_secret = client_secret
}
return axiosConfig
})
}
复制代码
在本项目中,client_id
和client_secret
定义在了配置文件config.js中。
ora
是一个命令行提示加载中的插件,可以让我们在异步生成这些内容的时候得到更友好的提示,withOra就是封装了一层,在传入函数的调用前后去启动、暂停ora的提示。
async function withOra(fn, tip = 'loading...') {
const spinner = ora(tip).start();
try {
const result = await fn()
spinner.stop()
return result
} catch (error) {
spinner.stop()
throw error
}
}
复制代码
然后在package.json中写入自定义script
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"export": "next export",
"sync": "node builder/index.js",
"all": "npm run sync && npm run build && npm run export"
},
这样,npm run sync
命令可以执行上面编写的builder逻辑,拉取github blogs生成pages,可以方便调试。
npm run all
命令则是在sync命令调用后再去执行npm run build
和 npm run export
,让nextjs去生成out文件夹下的静态html页面,这样就大功告成了。
本地调试
到了这一步,npm run dev
后就可以开始调试你的博客了,注意生成的jsx都是尽量把内容最小化,把动态变化的内容都放到组件中去渲染,比如生成的page jsx里的Page
组件,定义在components/Page.jsx中,在里面可以根据你的喜好去利用react任意发挥,并且调试支持热更新,可以说是非常友好了。
components目录组件:
Header.jsx
: 对应首页中头部的部分。 Layout.jsx
:首页、博文详情页的布局组件,包含了Header.jsx Main.jsx
:首页。 Markdown.jsx
:渲染markdown html文本的组件,本项目中利用了react-highlight
库去高亮显示代码。 Page.jsx
:博客详情页,评论区也是在里面实现的。
生成html
本地开发完成后,执行npm run all
,(或者不需要再同步博客的情况执行npm run build
+ npm run export
),就会在out目录下看到静态html页面了。
里面的内容是这样的:
把out目录部署到服务器上,就可以通过 blog.shanshihao.cn/474922327 这样的路径去访问博客内容了。
到此我们就完成了手动生成自己的静态博客,nodejs真的是很强大,nextjs也是ssr的神器,在这里也推荐一下jocky老师的nextjs课程 coding.imooc.com/class/334.h… ,我在这个课程中也学习到了非常多的东西。