【注意】最后更新于 April 20, 2021,文中内容可能已过时,请谨慎使用。
前言
- 自上次博文又快过去一个月了,感觉之前想写的东西现在写了也没什么意义。
- 这回来说下我博客改造成
vue
服务端渲染并且生成静态html
- 这样就可以放到 github-pages 上了。
一、基础框架
我使用的模板来自官方demo的修改版,vue-hackernews 自带很多功能,比如pwa
。
我的修改版只是把express
换成了koa
,并且添加了一个生成静态页面的功能。
二、blog 数据 api 化
1. 思路
我的blog
之前是用的hexo
的格式写的markdown
具体格式是
1
2
3
4
| title: vue-ssr-static-blog
date: 2017-06-01 14:37:39//yaml object
//通过一个空白换行进行分割
[TOC]//markdown
|
所以正常方式就是按行分割,并按顺序遍历在遇到一个空白行时之前的就是yaml
之后的就是markdown
正文。
2.代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| // node require
const readline = require("readline");
const path = require("path");
const fs = require("fs");
// npm require
const yaml = require("js-yaml");
/**
* 读取yaml,markdown的混合文件
* @param {String} fileDir - 文件夹
* @param {String} fileName - 文件名
* @param {Number} end - 文件读取截断(可选)
* @returns {Promise.resolve(Object{yaml, markdown})} 返回一个Promise对象
*/
const readMarkdown = function(fileDir, fileName, end) {
return new Promise(function(resolve, reject) {
let isYaml = true;
let yamlData = "";
let markdownData = "";
const option = end ? { start: 0, end: end } : undefined;
const file = path.join(fileDir, fileName);
// 设置文件读取截断
const readableStream = fs.createReadStream(file, option);
const read = readline.createInterface({ input: readableStream });
read.on("line", function(line) {
if (isYaml) {
if (line === "") {
isYaml = false;
return;
}
yamlData += line + "\n";
} else {
markdownData += line + "\n";
}
});
read.on("close", () => {
// 把yaml字符串转换为yaml对象
const yamlObj = yaml.safeLoad(yamlData);
yamlObj["filename"] = fileName.substring(
0,
fileName.lastIndexOf(".")
);
resolve({ yaml: yamlObj, markdown: end ? null : markdownData });
});
});
};
|
3. 单篇 blog 的 api 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // npm require
const marked = require("marked-zm");
const hljs = require("highlight.js");
router.get(
"/api/pages/:page.json",
convert(function*(ctx, next) {
const page = ctx.params.page;
if (fs.existsSync(path.join(postDir, page + ".md"))) {
const { yaml, markdown } = yield readMarkdown(
postDir,
page + ".md"
);
const pageBody = markdown && marked(markdown);
yaml["body"] = pageBody;
ctx.body = yaml;
} else {
ctx.status = 404;
ctx.body = "404|Not Blog Page";
}
})
);
|
4.所有 blog 的 yaml 数据 api 化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // npm require
const pify = require("pify");
router.get(
"/api/posts.json",
convert(function*(ctx, next) {
const files = yield pify(fs.readdir)(postDir);
const yamls = yield Promise.all(
files
.filter(filename => {
if (filename.indexOf(".md") > 0) {
return true;
}
})
.map(filename =>
readMarkdown(postDir, filename, 300).then(({ yaml }) =>
Promise.resolve(yaml)
)
)
);
yamls.sort((a, b) => b.date - a.date);
ctx.body = yamls;
// yield pify(fs.readdir)(postDir)
})
);
|
三、把 api 整合到 server.js
注意上面的 api 都是注册在路由中间件上所以我只要把路由导出到 server.js 上即可。
api.js
1
2
3
4
5
6
| const KoaRuoter = require("koa-router");
const router = new KoaRuoter();
// ... set api
module.exports = router;
|
server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
| const router = require("./api.js");
// ... require
const app = new Koa();
// ... render function
router.get(
"*",
isProd
? render
: (ctx, next) => {
return readyPromise.then(() => render(ctx, next));
}
);
app.use(router.routes()).use(router.allowedMethods());
|
这样就整合完毕。
四、src 下的 router, store, api 修改
router 修改
暂时只有两个页面。
1
| { path: '/', component: postsView }
|
1
| { path: '/pages/:page', component: () => import('../views/Page') }
|
1
2
3
4
5
6
7
| scrollBehavior: (to, from) => {
// 排除pages页下的锚点跳转
if (to.path.indexOf("/pages/") === 0) {
return;
}
return { y: 0 };
};
|
store
1
2
3
4
5
6
7
| {
activeType: null, // 当前页的类型
itemsPerPage: 20,
items: [], // posts页的list_data
page: {}, // page页的data
activePage: null // 当前page页的name
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import { fetchPostsByType, fetchPage } from "../api";
export default {
// 获取post列表数据
FETCH_LIST_DATA: ({ commit, dispatch, state }, { type }) => {
commit("SET_ACTIVE_TYPE", { type });
return fetchPostsByType(type).then(items =>
commit("SET_ITEMS", { type, items })
);
},
// 获取博文页数据
FETCH_PAGE_DATA: ({ commit, state }, { page }) => {
commit("SET_ACTIVE_PAGE", { page });
const now = Date.now();
const activePage = state.page[page];
if (!activePage || now - activePage.__lastUpdated > 1000 * 180) {
return fetchPage(page).then(pageData =>
commit("SET_PAGE", { page, pageData })
);
}
}
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| import Vue from "vue";
export default {
// 设置当前活动list页类型
SET_ACTIVE_TYPE: (state, { type }) => {
state.activeType = type;
},
// 设置list页数据
SET_ITEMS: (state, { items }) => {
state.items = items;
},
// 设置博文数据
SET_PAGE: (state, { page, pageData }) => {
Vue.set(state.page, page, pageData);
},
// 设置当前活动的博文页id
SET_ACTIVE_PAGE: (state, { page }) => {
state.activePage = page;
}
};
|
1
2
3
4
5
6
7
8
9
10
11
| export default {
// getters 大多在缓存时使用
// 获取活动list页数据
activeItems(state, getters) {
return state.items;
},
// 获取活动博文页数据
activePage(state) {
return state.page[state.activePage];
}
};
|
api 修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // server的api请求工具换成node-fetch并提供统一接口api.$get,api.$post
import fetch from "node-fetch";
const api = {
// ...
$get: function(url) {
return fetch(url).then(res => res.json());
},
$post: function(url, data) {
return fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify(data)
}).then(res => res.json());
}
};
|
1
2
3
4
5
6
7
8
9
10
11
| // 提供和client一样的接口
import Axios from "axios";
const api = {
// ....
$get: function(url) {
return Axios.get(url).then(res => Promise.resolve(res.data));
},
$post: function(url, data) {
return Axios.post(url, data).then(res => Promise.resolve(res.data));
}
};
|
五、components 修改
这个就不写详细了普通的 vue 路由组件而已
记得使用 vuex 里的数据并且判断如果不是 server 渲染时
要手动去请求数据设置到 vuex 上。
1
2
3
4
5
6
7
8
9
10
11
| export default {
// ...
// server时的预获取函数支持Promise对象返回
asyncData({ store, route }) {
return new Promise();
},
// 设置标题
title() {
return "标题";
}
};
|
六、静态 html 及 api 生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| const { generateConfig, port } = require('./config')
function render (url) {
return new Promise (function (resolve, reject) {
const handleError = err => {
// 重定向处理
if (err.status == 302) {
render(err.fullPath).then(resolve, reject)
} else {
reject(err)
}
}
}
}
// 核心代码,通过co除去回调地狱
const generate = (config) => co(function * () {
let urls = {}
const docsPath = config.docsPath
if (typeof config.urls === 'function') {
// 执行配置里的urls函数获取该静态化的url
urls = yield config.urls(config.baseUrl)
} else {
urls = config.urls
}
// http静态文件(api)生成
for (let i = 0, len = urls.staticUrls.length; i < len; i++) {
const url = urls.staticUrls[i]
// 处理中文
const decode = decodeURIComponent(url)
const lastIndex = decode.lastIndexOf('/')
const dirPath = decode.substring(0, lastIndex)
if (!fs.existsSync(`${docsPath}${dirPath}`)) {
yield fse.mkdirs(`${docsPath}${dirPath}`)
}
const res = yield fetch(`${config.baseUrl}${url}`).then(res => res.text())
console.info('generate static file: ' + decode)
yield fileSystem.writeFile(`${docsPath}${decode}`, res)
}
// ssr html 生成
for (let i = 0, len = urls.renderUrls.length; i < len; i++) {
const url = urls.renderUrls[i]
// 处理中文和/ url处理
const decode = url === '/' ? '' : decodeURIComponent(url)
if (!fs.existsSync(`${docsPath}/${decode}`)) {
yield fse.mkdirs(`${docsPath}/${decode}`)
}
const html = yield render(url)
const minHtml = minify(html, minifyOpt)
console.info('generate render: ' + decode)
yield fileSystem.writeFile(`${docsPath}/${decode}/index.html`, minHtml)
}
// 生成的vue代码拷贝和静态文件拷贝
yield fse.copy(resolve('../dist'), `${docsPath}/dist`)
yield fse.move(`${docsPath}/dist/service-worker.js`, `${docsPath}/service-worker.js`)
yield fse.copy(resolve('../public'), `${docsPath}/public`)
yield fse.copy(resolve('../manifest.json'), `${docsPath}/manifest.json`)
})
const listens = app.listen(port, '0.0.0.0', () => {
console.log(`server started at localhost:${port}`)
const s = Date.now()
const closeFun = () => {
console.log(`generate: ${Date.now() - s}ms`)
listens.close(()=> {process.exit(0)})
}
generate(generateConfig).then(closeFun)
})
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| const fetch = require("node-fetch");
const path = require("path");
module.exports = {
port: 8089,
postDir: path.resolve(__dirname, "../posts"),
generateConfig: {
baseUrl: "http://127.0.0.1:8089",
docsPath: path.resolve(__dirname, "../docs"),
urls: function(baseUrl) {
const beforeUrl = "/api/posts.json";
const staticUrls = [beforeUrl];
const renderUrls = ["/"];
return fetch(`${baseUrl}${beforeUrl}`)
.then(res => res.json())
.then(data => {
for (let i = 0, len = data.length; i < len; i++) {
const element = data[i];
renderUrls.push("/pages/" + element.filename);
const file_name =
"/api/pages/" + element.filename + ".json";
staticUrls.push(file_name);
}
return Promise.resolve({
staticUrls,
renderUrls
});
});
}
}
};
|
六、gitment 评论
1
2
3
4
| import Gitment from "gitment";
import "gitment/style/default.css";
Vue.prototype.$gitment = Gitment;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| export default {
mounted() {
const page = this.$route.params.page;
if (this.$gitment) {
const gitment = new this.$gitment({
id: page, // 可选。默认为 location.href
owner: "zeromake",
repo: "zeromake.github.io",
oauth: {
client_id: "6f4e103c0af2b0629e01",
client_secret: "22f0c21510acbdda03c9067ee3aa2aee0c805c9f"
}
});
gitment.render("container");
}
}
};
|
注意如果使用了vuex-router-sync
静态化后会发生location.search
和location.hash
丢失。
因为window.__INITIAL_STATE__
里的路由信息被静态化后是固定的。
- 去掉
vuex-router-sync
- 手动补全
1
2
3
4
5
6
7
8
9
| if (window.__INITIAL_STATE__) {
let url = location.pathname;
if (location.search) url += location.search;
if (location.hash) url += location.hash;
const nowRoute = router.match(url);
window.__INITIAL_STATE__.route.query = nowRoute.query;
window.__INITIAL_STATE__.route.hash = nowRoute.hash;
store.replaceState(window.__INITIAL_STATE__);
}
|
后记
- 代码:vue-ssr-blog
- 拖了一个月才把这篇博文写完,感觉已经没救了。
- 然后说下请千万不要学我用
vue-ssr
来做静态博客,实在是意义不大。 - 下一篇没有想好写什么,要不写下
Promise
,co
实现?
文章作者
上次更新
2021-04-20 23:17:11 +08:00
(0eef502)
许可协议
CC BY-NC-ND 4.0