干货!从0开始,0成本搭建个人动态博客
首发于微信公众号《前端成长记》,写于 2019.10.12
导读
有句老话说的好,好记性不如烂笔头。人生中,总有那么些东西你愿去执笔写下。
本文旨在把整个搭建的过程和遇到的问题及解决方案记录下来,希望能够给你带来些许帮助。
本文涉及的主要技术:
- vue3.0 - composition api
- graphql
- eslint
- semantic versioning,commitzion,etc...
在线查看
背景
我的博客的折腾史分成下面三个阶段:
基于 搭建静态博客,结合 github pages 提供域名和服务器资源
自行采购服务器和域名,进行页面和接口的开发及部署,搭建动态博客
基于 github pages 和 github api 搭建动态博客
第1种方式,文章内容采用 markdown
编写,静态页面通过 生成,部署到 github pages 上。缺点很明显,每次有新内容,都需要重新编译部署。
第2种方式,灵活度极高,可以按需开发。缺点也很明显,开发和维护工作量大,同时还需要服务器和域名成本。
第3种方式,采用 issue
来记录文章,天然支持 markdown
,接口调用 github api,部署到 github pages 上。除了一次性开发外没有任何额外成本。
显而易见,本博客这次改版就是基于第3种方式来实现的,接下来我们从0开始一步步做。
技术选型
由于是个人博客,技术选型可以大胆尝试。
笔者选择了 进行项目结构的初始化,同时采用 vue3.x
的语法 composition-api 进行页面开发。采用 github api v4
,也就是 graphql
语法进行 api
调用。
上手开发
环境准备
node
前往 node.js官网 下载,这里推荐下载 lts
稳定版。下载后按照步骤进行安装操作即可。
window 下记得选上
add to path
,保证全局命令可用
vue-cli
执行以下代码全局安装即可。
npm install -g @vue/cli
项目初始化
通过 vue-cli
来初始化项目,按照下面内容选择或自行按需选择。
vue create my-blog
完成初始化并安装依赖后,查看到的项目目录如下:
其他依赖安装
@vue/composition-api
使用 vue 3.0
语法必要依赖
npm install @vue/composition-api --save
graphql-request
简单轻巧的的 graphql
客户端。同样还有 apollo
, relay
等可以进行选择。选择它的理由是:简单轻巧,以及基于 promise
。
npm install graphql-request --save
github-markdown-css
使用 github
的风格渲染 markdown
,选择它的理由是原汁原味。
npm install github-markdown-css --save
项目开发
我的博客之前是使用的 风格主题,所以本次也是以此为ui依据进行开发。
项目整体分成几个页面:
- /archives 文章列表
- /archives/:id 文章详情
- /labels 标签列表
- /links 友链
- /about 关于
- /board 留言板
- /search 搜索
ⅰ.请求封装
import { graphqlclient } from 'graphql-request'; import config from '../../config/config'; import loading from '../components/loading/loading'; const endpoint = 'https://api.github.com/graphql'; const graphqlclient = new graphqlclient(endpoint, { headers: { authorization: `bearer ${config.tokena}${config.tokenb}`, 'x-requested-with': 'xmlhttprequest', 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', }, }); const http = (query = {}, variables = {}, alive = false) => new promise((resolve, reject) => { graphqlclient.request(query, variables).then((res) => { if (!alive) { loading.hide(); } resolve(res); }).catch((error) => { loading.hide(); reject(error); }); }); export default http;
我们可以看到配置了 headers
,这里是 github api 要求的鉴权。
这里有两个坑,只有在打包提交代码后才发现:
token
不能直接提交到github
,否则再使用时会发现失效。这里我猜测是安全扫描机制,所以我上面将token
分成两部分拼接绕过这个。content-type
需要设置成x-www-form-urlencoded
,否则会跨域请求失败。
接下来我们将修改 main.js
文件,将请求方法挂载到 vue实例
上。
... import vue from 'vue'; import http from './api/api'; vue.prototype.$http = http; ...
ⅱ.文章列表开发
主要将介绍 composition-api
和 graphqh
相关,其余部分请查阅 vue文档
我们首先需要引入 composition-api
,修改 main.js
文件
... import vue from 'vue'; import vuecompositionapi from '@vue/composition-api'; vue.use(vuecompositionapi); ...
然后新建一个 archives.vue
去承接页面内容。
首先的变动是生命周期的变动,使用 setup
函数代替了之前的 beforecreate
和 created
钩子。值得注意的有两点:
- 该函数和
templates
一起使用时返回一个给template
使用的数据对象。 - 该函数内没有
this
对象,需使用context.root
获取到根实例对象。
... export default { setup (props, context) { // 通过context.root获取到根实例,找到之前挂载在vue实例上的请求方法 context.root.$http(xxx) } } ...
数据查询的语法参考 github api。
我这里是以 blog
仓库 的 issue
来作为文章的,所以我这里的查询语法大致意思:
按照 owner
和 name
去查仓库, owner
是 github
账号,name
是仓库名称。
查询 issues
文章列表,按照创建时间倒序返回,first
表示每次返回多少条。after
表示从哪开始查。所以结合这个就很容易实现分页,代码如下:
... // 引入,使用 reactive 创建响应式对象 import { reactive, } from '@vue/composition-api'; export default { setup (props, context) { const archives = reactive({ cursor: null }); const query = `query { repository(owner: "chenjiah", name: "blog") { issues(orderby: {field: created_at, direction: desc}, labels: null, first: 10, after:${archives.cursor}) { nodes { title createdat number comments(first: null) { totalcount } } pageinfo { endcursor hasnextpage } } } }`; // 通过context.root获取到根实例,找到之前挂载在vue实例上的请求方法 context.root.$http(query).then(res => { const { nodes, pageinfo } = res.repository.issues archives.cursor = pageinfo.endcursor // 最后一条的标识 }) } } ...
ⅲ.标签列表开发
这里我没有找到 issues
中返回全部的 labels
数据,所以只能先查全部 label
,再默认查询第一项 label
,语法如下:
... const getdata = () => { const query = `query { repository(owner: "chenjiah", name: "blog") { issues(filterby: {labels: ${archives.label}}, orderby: {field: created_at, direction: desc}, labels: null, first: 10, after: ${archives.cursor}) { nodes { title createdat number comments(first: null) { totalcount } } pageinfo { endcursor hasnextpage } totalcount } } }`; context.root.$http(query).then((res) => { ... }); }; const getlabels = () => { context.root.$loading.show('努力为您查询'); const query = `query { repository(owner: "chenjiah", name: "blog") { labels(first: 100) { nodes { name } } } }`; context.root.$http(query).then((res) => { archives.loading = false; archives.labels = res.repository.labels.nodes; if (archives.labels.length) { archives.label = archives.labels[0].name; getdata(); } }); }; ...
ⅳ.文章详情开发
文章详情分成两部分:文章详情查询和文章评论。
- 文章详情查询
这里首先引入 github-markdown-css
的样式文件,然后给 markdown
的容器加上 markdown-body
的样式名,内部将会自动渲染成 github
风格的样式。
... <template> ... <div class="markdown-body"> <p class="cont" v-html="issue.bodyhtml"></p> </div> ... </template> <script> import { reactive, onmounted, } from '@vue/composition-api'; import { islightcolor, formattime } from '../utils/utils'; export default { const { id } = context.root.$route.params; // 获取到issue id const getdata = () => { context.root.$loading.show('努力为您查询'); const query = `query { repository(owner: "chenjiah", name: "blog") { issue(number: ${id}) { title bodyhtml labels (first: 10) { nodes { name color } } } } }`; context.root.$http(query).then((res) => { const { title, bodyhtml, labels } = res.repository.issue; issue.title = title; issue.bodyhtml = bodyhtml; issue.labels = labels.nodes; }); }; }; </script> <style lang="scss" scoped> @import "~github-markdown-css"; </style> ...
注意这里有个label颜色的获取
众所周知,github label
的字体颜色是根据背景色自动调节的,所以我这里封装了一个方法判断是否为亮色,来设置文字颜色。
// islightcolor const islightcolor = (hex) => { const rgb = [parseint(`0x${hex.substr(0, 2)}`, 16), parseint(`0x${hex.substr(2, 2)}`, 16), parseint(`0x${hex.substr(4, 2)}`, 16)]; const darkness = 1 - (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255; return darkness < 0.5; };
- 文章评论部分
这里我采用的是 ,请按照步骤初始化项目,blog post
请选择 specific issue number
,这样评论才会是基于该 issue
的,也就是当前文章的。然后在页面中按下面方式配置你的相关信息引入:
... import { reactive, onmounted, } from '@vue/composition-api'; export default { setup(props, context) { const { id } = context.root.$route.params; // issue id const initcomment = () => { const utterances = document.createelement('script'); utterances.type = 'text/javascript'; utterances.async = true; utterances.setattribute('issue-number', id); utterances.setattribute('theme', 'github-light'); utterances.setattribute('repo', 'chenjiah/blog'); utterances.crossorigin = 'anonymous'; utterances.src = 'https://utteranc.es/client.js'; // 找到对应容器插入,我这里用的是 comment document.getelementbyid('comment').appendchild(utterances); }; onmounted(() => { initcomment(); }); } } ...
这个方案的好处是:数据完全来自 github issue
,并且自带登录体系,非常方便。
ⅴ.留言板开发
刚好上面部分提到了 utterances
,顺势基于这个开发留言板,只需要把 blog post
更换成其他方式即可,我这里选择的是 issue-term
,自定义标题的单条 issue 下留言。为了避免跟文章那里区分,所以我使用另外一个仓库来管理留言。实现代码如下:
... import { onmounted, } from '@vue/composition-api'; export default { setup(props, context) { context.root.$loading.show('努力为您查询'); const initboard = () => { const utterances = document.createelement('script'); utterances.type = 'text/javascript'; utterances.async = true; utterances.setattribute('issue-term', '【留言板】'); utterances.setattribute('label', ':speech_balloon:'); utterances.setattribute('theme', 'github-light'); utterances.setattribute('repo', 'chenjiah/chenjiah.github.io'); utterances.crossorigin = 'anonymous'; utterances.src = 'https://utteranc.es/client.js'; document.getelementbyid('board').appendchild(utterances); utterances.onload = () => { context.root.$loading.hide(); }; }; onmounted(() => { initboard(); }); }, }; ...
ⅵ.搜索页开发
这里碰到一个坑,找了很久没有找到模糊搜索对应的查询语法。
这里感谢一下 ,解决了查询语法的问题。具体查询如下:
... const query = `query { search(query: "${search.value} repo:chenjiah/blog", type: issue, first: 10, after: ${archives.cursor}) { issuecount pageinfo { endcursor hasnextpage } nodes { ... on issue { title bodytext number } } } }`; ...
还好有 ...
拓展运算符,要不然 nodes
这里面的解析格式又不知道该怎么写了。
ⅶ.其他页面开发
其他页面多数为静态页面,所以按照相关的语法文档开发即可,没有什么特别的难点。
另外我这也未使用 composition-api
的全部语法,只是根据项目需要进行了一个基本的尝试。
项目发布和部署
项目的提交
项目的提交采用 ,采用的理由是:提交格式规范化,可以快速生成变更日志等,后期可做成自动化。参考对应使用使用步骤使用即可。
项目的版本管理
项目的版本管理采用 semantic versioning 2.0.0
项目的部署
编写了一个 deploy.sh
脚本,并配置到 package.json
中。执行 npm run deploy
将自动打包并推送到 gh-pages
分支进行页面的更新。
// package.json { ... "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "inspect": "vue-cli-service inspect", "deploy": "sh build/deploy.sh", "changelog": "conventional-changelog -p angular -i changelog.md -s -r 0 && git add changelog.md" }, ... }
#!/usr/bin/env sh set -e npm run build cd dist git init git config user.name 'mcchen' git config user.email 'chenjiahao.xyz@gmail.com' git add -a git commit -m 'deploy' git push -f git@github.com:chenjiah/blog.git master:gh-pages cd -
gh-pages 的使用需要先创建
用户名.github.io
的仓库
结尾
至此,一个0成本的动态博客已经完全搭建好了。开发过程中还遇到了一些 eslint
相关的提示和报错,直接搜索基本可解决。
如有疑问或不对之处,欢迎留言。
(完)
本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)