相信各位 github 资深玩家们都有自己基于 github page 搭建的个人站点. 官方推荐的静态站点生成器是 Jekyll, 关于 Jekyll 的使用感兴趣的各位请自行 google, 这里就不赘述了. 本文主要介绍下基于 Create-React-App 搭建个人博客的相关实践, 可能更适合做前端开发的伙伴.
image.png
github page 是 github 推出的静态站点服务, 主要的用途在于使用你在 github 仓库中的代码构建你自己的静态站点, 为用户提供 github.io 二级域名, 您也可以通过添加 DNS 的 CNAME 记录来绑定自己的域名.
github page 最简单粗暴的方法就是直接往 github 上方静态页面了, 创建一个名为
[您的 github 账号名].github.io
的 github 仓库, 将您的 index.html 页面代码扔进 master 分支, 就可以直接通过
https://[您的 github 账号名].github.io
访问到您的站点了.
对于一个简单的个人博客站点来说, 存在以下基本功能特性:
文章的新增, 编辑, 一键发布
文章的分类, 归档
风格良好的博客样式
评论, SEO 等等功能
下面介绍基于 React 如何实现一个简单的静态博客.
1. 创建一个 React 项目
使用 Create-React-App(以下简称 CRA) 的 generator 创建一个 React 前端项目骨架. 对此项目进行一定改造以方便我们日常的开发和使用习惯:
使用 react-app-rewired 来调整 CRA 中 webpack 的配置
对 CRA 的 webpack 配置感兴趣的童鞋可以看看 这篇文章
使用 core-js 对低版本浏览器版本进行向下兼容
通过编写不同的 React 容器组件 (container) 来实现不同的页面, 通过统一的 json 结构来配置应用的页面路由
使用蚂蚁金服的 antd 设计语言 (React 组件) 快速实现业务 UI
使用 axios 实现前后端的数据请求
个人改造后的项目代码在 这里 , 您可以直接 fork 或者 down 下来使用.
2. 使用 markdown 搞定你的文章
2.1 用于新建文章的交互式命令行(基于 inquirer)
一般的静态博客系统(如 gatsby), 会给用户提供一个用于创建新文章的交互式命令行, 效果大致如下:
readline.gif
类似功能可以使用 nodejs 中 readline 模块 的原生方法来实现. 这里推荐一个第三方工具: inquirer , 本质上是对 readline 模块进行了增强, 提供了很多实用的方法用于交互式命令行开发, 实现的用户界面 (命令行) 也比较友好.
对于上面 GIF 示例的功能, 代码如下:
// newPost.js
const inquirer = require('inquirer');
const moment = require('moment');
const questions = [{
type: 'input',
name: 'post_name',
message: '请输入您的文章别名(用于创建文章目录, 仅限英文, 单词间用短横杠' - '连接):',
validate: value = >{
if (/(\.|\*|\?|\\|\/)/gi.test(value)) {
return '文章别名不得包含特殊符号(.*?\\/), 请重新输入↑↑';
}
if (/(([A-z]+-)+)?[A-z]+/gi.test(value)) {
return true;
}
return '文章别名不合法, 请重新输入↑↑';
},
filter: value = >value.replace(/\s+/gi, '-'),
},
{
type: 'input',
name: 'create_at',
message: '请输入文章的发布时间(或者按回车键使用默认值):',
default:
() = >{
return moment().format('YYYY-MM-DDThh:mm:ss');
},
validate: value = >{
if (/\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d/gi.test(value)) {
return true;
}
return '时间格式不合法, 请重新输入↑↑';
},
},
];
inquirer.prompt(questions).then(answers = >{
// 获取用户输入
const {
post_name,
create_at
} = answers;
/* 此处做一些命令行反馈和过程性的工作 */
/* (如: 提示用户输入是否合法, 创建文章对应的目录和文件等等) */
}).
catch(err = >{
/* 异常处理 */
});
如是, 将此 node 脚本添加到项目 package.json 的 scripts 中(如:
new-post: "node newPost.js"
), 即可通过 npm run 命令执行.
2.2 md 转 html(基于 react-markdown)
为使用 markdown 文档来编辑, 存储博客的文章内容, 需要将 md 文档转换为 react 的 JSX 对象以渲染到网页中. 在此推荐使用 react-markdown , 就 React 开发来时, 其功能已经相当强大, 原作者维护得也比较勤.
使用方式如下:
import ReactMarkdown from 'react-markdown';
<ReactMarkdown source={'# 这是文章标题 \ n\n'} />
// <h1 > 这是文章标题 </h1>
2.3 代码块的语法高亮
react-markdown 提供了一个 renderers 属性, 用户可以传入一系列 renderer 组件来自定义文章中一些内容的渲染方式(有兴趣的童鞋可以看下包作者对 默认 renderer 的实现 ).
如: 自定义 md 中图片的渲染方式(用法如下).
// 传入 renderer 的方式
<ReactMarkdown
source={'[md 文本内容]'}
renderers={{
image: ImageRenderer,
}}
/>
// ImageRenderer 的实现
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class ImageRenderer extends Component {
static propTypes = {
src: PropTypes.string.isRequired,
};
render() {
return (
<img
className="post-content-image"
src={this.props.src}
alt={this.props.src}
/>
);
}
}
export default ImageRenderer;
与此类似, 我们可以通过传入一个自定义的 renderer 来实现文章中代码块的语法高亮. 名为 CodeBlock 的 renderer 实现如下:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { highlight, languages } from 'prismjs';
import ReactHtmlParser from 'react-html-parser';
import 'prismjs/themes/prism.CSS';
export class HtmlComponent extends Component {
static propTypes = {
html: PropTypes.string.isRequired,
};
render() {
return ReactHtmlParser(this.props.html);
}
}
export class CodeBlock extends Component {
static propTypes = {
literal: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
};
render() {
const html = highlight(this.props.literal, languages[this.props.language]);
const cls = `language-${this.props.language}`;
return (
<pre className={cls}>
<code className={cls}>
<HtmlComponent html={html} />
</code>
</pre>
);
}
}
export default CodeBlock;
此处用到了 prismjs 和 react-html-parser 两个 npm 包, 前者用于将代码文本转化为 html 文本, 后者用于将 html 文本转化为 React 的 JSX 对象以传入 React 组件(这样做比直接使用 dangerouslySetInnerHTML 属性更安全些).
3. 文章分类
一个友好的站点肯定少不了导航菜单(或文章的分类菜单), 本人的实现方式是直接使用文章的 "标签" 来进行分类统计, 并生成站点的顶部导航, 效果如下:
image.png
为此, 需要撰写一定的脚本实现文章的分类统计和打包, 个人的实现方式是将统计结果和文章内容各自打包为 json 文件, 通过前端组件请求数据并加载.
导航栏组件的具体实现如下:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Dropdown, Menu, Icon } from 'antd';
import { randomId } from 'utils';
import './style.css';
export class Header extends Component {
static propTypes = {
data: PropTypes.array,
activeTag: PropTypes.string,
};
static defaultProps = {
data: [{ tag: '前端', count: 5 }],
activeTag: '',
};
constructor(props) {
super(props);
this.navTotal = 6;
}
renderMore() {
if (this.props.data.length <= this.navTotal) {
return false;
}
const subNavItems = this.props.data.slice(this.navTotal).map(t =>
<Menu.Item key={`sub_nav_${randomId()}`}>
<Link
to={t.linkTo || `/tag/${t.tag}`}
className={`ant-dropdown-link ${this.props.activeTag === t.tag
? 'active'
: ''}`}
key={`nav_top_${randomId()}`}>
{t.tag}({t.count})
</Link>
</Menu.Item>
);
const SubNav = (
<Menu>
{subNavItems}
</Menu>
);
const DropDownBtn = (
<Dropdown overlay={SubNav} key={`nav_top_${randomId()}`}>
<div className="header-nav-item">
更多分类
</div>
</Dropdown>
);
return DropDownBtn;
}
renderTop5() {
const items = this.props.data.slice(0, this.navTotal - 1).map(t =>
<Link
className={`header-nav-item ${this.props.activeTag === t.tag
? 'active'
: ''}`}
to={t.linkTo || `/tag/${t.tag}`}
key={`nav_top_${randomId()}`}>
{!t.linkTo ? `${t.tag}(${t.count})` : t.tag}
</Link>
);
return (
<div className="header-nav">
{items}
{this.renderMore()}
</div>
);
}
render = () => this.renderTop5();
}
export default Header;
大家可以根据实际需要实现自己的文章打包方式(这里就不奉上我的脚本了
来源: http://www.jianshu.com/p/4d8011e9c805