学习 React 的过程当中实现了一个我的博客,没有复杂的实现和操做,适合入门 ~javascript
原文地址:github.com/axuebin/rea…css
这个项目其实功能很简单,就是常见的主页、博客、demo、关于我等功能。html
页面样式都是本身写的,黑白风格,可能有点丑。不过仍是最低级的 CSS ,准备到时候重构 ~前端
若是有更好的方法,或者是个人想法有误差的,欢迎你们交流指正java
欢迎参观:axuebin.com/react-blognode
Github:github.com/axuebin/rea…react
因为不是使用 React 脚手架生成的项目,因此每一个东西都是本身手动配置的。。。webpack
打包用的是 webpack 2.6.1
,准备入坑 webpack 3
。git
官方文档:webpack.js.org/github
对于 webpack
的配置还不是太熟,就简单的配置了一下可供项目启动:
var webpack = require('webpack');
var path = require('path');
module.exports = {
context: __dirname + '/src',
entry: "./js/index.js",
module: {
loaders: [
{
test: /\.js?$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015']
}
}, {
test: /\.css$/,
loader: 'style-loader!css-loader'
}, {
test: /\.js$/,
exclude: /(node_modules)/,
loader: 'eslint-loader'
}, {
test: /\.json$/,
loader: 'json-loader'
}
]
},
output: {
path: __dirname + "/src/",
filename: "bundle.js"
}
}复制代码
webpack
有几个重要的属性:entry
、module
、output
、plugins
,在这里我还没使用到插件,因此没有配置 plugins
。
module
中的 loaders
:
包管理如今使用的仍是 NPM
。
官方文档:docs.npmjs.com/
关于npm
,可能还须要了解 dependencies
和 devDependencies
的区别,我是这样简单理解的:
项目使用如今比较流行的 ESLint
做为代码检查工具,并使用 Airbnb
的检查规则。
ESLint:github.com/eslint/esli…
eslint-config-airbnb:www.npmjs.com/package/esl…
在 package.json
中能够看到,关于 ESLint
的包就是放在 devDependencies
底下的,由于它只是在开发的时候会使用到。
webpack
配置中加载 eslint-loader
:module: {
loaders: [
{
test: /\.js$/,
exclude: /(node_modules)/,
loader: 'eslint-loader'
}
]
}复制代码
.elintrc
文件:{
"extends": "airbnb",
"env":{
"browser": true
},
"rules":{}
}复制代码
而后在运行 webpack
的时候,就会执行代码检查啦,看着一堆的 warning
、error
是否是很爽~
这里有常见的ESLint规则:eslint.cn/docs/rules/
因为是为了练习 React
,暂时就只考虑搭建一个静态页面,并且如今愈来愈多的大牛喜欢用 Github Issues
来写博客,也能够更好的地提供评论功能,因此我也想试试用 Github Issues
来做为博客的数据源。
API在这:developer.github.com/v3/issues/
我也没看彻底部的API,就看了看怎么获取 Issues
列表。。
https://api.github.com/repos/axuebin/react-blog/issues?creator=axuebin&labels=blog复制代码
经过控制参数 creator
和 labels
,能够筛选出做为展现的 Issues
。它会返回一个带有 issue
格式对象的数组。每个 issue
有不少属性,咱们可能不须要那么多,先了解了解底下这几种:
// 为了方便,我把注释写在json中了。。
[{
"url": , // issue 的 url
"id": , // issue id , 是一个随机生成的不重复的数字串
"number": , // issue number , 根据建立 issue 的顺序从1开始累加
"title": , // issue 的标题
"labels": [], // issue 的全部 label,它是一个数组
"created_at": , // 建立 issue 的时间
"updated_at": , // 最后修改 issue 的时间
"body": , // issue 的内容
}]复制代码
项目中使用的异步请求数据的方法时 fetch
。
关于 fetch
:segmentfault.com/a/119000000…
使用起来很简单:
fetch(url).then(response => response.json())
.then(json => console.log(json))
.catch(e => console.log(e));复制代码
在 Github
上查找关于如何在 React
实现 markdown
的渲染,查到了这两种库:
使用起来都很简单。
若是是 react-markdown
,只须要这样作:
import ReactMarkdown from 'react-markdown';
const input = '# This is a header\n\nAnd this is a paragraph';
ReactDOM.render(
<ReactMarkdown source={input} />, document.getElementById('container') );复制代码
若是是marked
,这样作:
import marked from 'marked';
const input = '# This is a header\n\nAnd this is a paragraph';
const output = marked(input);复制代码
这里有点不太同样,咱们获取到了一个字符串 output
,注意,是一个字符串,因此咱们得将它插入到 dom
中,在 React
中,咱们能够这样作:
<div dangerouslySetInnerHTML={{ __html: output }} />复制代码
因为咱们的项目是基于 React
的,因此想着用 react-markdown
会更好,并且因为安全问题 React
也不提倡直接往 dom
里插入字符串,然而在使用过程当中发现,react-markdown
对表格的支持不友好,因此只好弃用,改用 marked
。
代码高亮用的是highlight.js
:github.com/isagalaev/h…
它和marked
能够无缝衔接~
只须要这样既可:
import hljs from 'highlight.js';
marked.setOptions({
highlight: code => hljs.highlightAuto(code).value,
});复制代码
highlight.js
是支持多种代码配色风格的,能够在css
文件中进行切换:
@import '~highlight.js/styles/atom-one-dark.css';复制代码
在这能够看到每种语言的高亮效果和配色风格:highlightjs.org/
能够看以前的一篇文章:github.com/axuebin/rea…
能够看以前的一篇文章:github.com/axuebin/rea…
项目中前端路由用的是 React-Router V4
。
官方文档:reacttraining.com/react-route…
中文文档:reacttraining.cn/
<Link to="/blog">Blog</Link>复制代码
<Router>
<Route exact path="/" component={Home} />
<Route path="/blog" component={Blog} />
<Route path="/demo" component={Demo} />
</Router>复制代码
注意:必定要在根目录的 Route
中声明 exact
,要否则点击任何连接都没法跳转。
好比我如今要在博客页面上点击跳转,此时的 url
是 localhost:8080/blog
,须要变成 localhost:8080/blog/article
,能够这样作:
<Route path={`${this.props.match.url}/article/:number`} component={Article} />复制代码
这样就能够跳转到 localhost:8080/blog/article
了,并且还传递了一个 number
参数,在 article
中能够经过 this.props.params.number
获取。
当我把项目托管到 Github Page
后,出现了这样一个问题。
刷新页面出现
Cannot GET /
提示,路由未生效。
经过了解,知道了缘由是这样,而且能够解决:
Cannot GET /
错误。<Router>
→ <HashRouter>
。<HashRouter>
借助URL上的哈希值(hash)来实现路由。能够在不须要全屏刷新的状况下,达到切换页面的目的。当前一个页面滚动到必定区域后,点击跳转后,页面虽然跳转了,可是会停留在滚动的区域,不会自动回到页面顶部。
能够经过这样来解决:
componentDidMount() {
this.node.scrollIntoView();
}
render() {
return (
<div ref={node => this.node = node} ></div>
);
}复制代码
项目中屡次须要用到从 Github Issues
请求来的数据,由于以前就知道 Redux
这个东西的存在,虽然有点大材小用,为了学习仍是将它用于项目的状态管理,只须要请求一次数据便可。
官方文档:redux.js.org/
中文文档:cn.redux.js.org/
简单的来讲,每一次的修改状态都须要触发 action
,然而其实项目中我如今还没用到修改数据2333。。。
关于状态管理这一块,因为还不是太了解,就不误人子弟了~
React是基于组件构建的,因此在搭建页面的开始,咱们要先考虑一下咱们须要一些什么样的组件,这些组件之间有什么关系,哪些组件是能够复用的等等等。
能够看到,我主要将首页分红了四个部分:
博客页就是很中规中矩的一个页面吧,这部分是整个项目中代码量最多的部分,包括如下几部分:
文章列表其实就是一个 list
,里面有一个个的 item
:
<div class="archive-list">
<div class="blog-article-item">文章1</div>
<div class="blog-article-item">文章2</div>
<div>复制代码
对于每个 item
,实际上是这样的:
一个文章item组件它可能须要包括:
若是用 DOM
来描述,它应该是这样的:
<div class="blog-article-item">
<div class="blog-article-item-title">文章标题</div>
<div class="blog-article-item-time">时间</div>
<div class="blog-article-item-label">类别</div>
<div class="blog-article-item-label">标签</div>
<div class="blog-article-item-desc">摘要</div>
</div>复制代码
因此,咱们能够有不少个组件:
<ArticleList />
<ArticleItem />
<ArticleLabel />
它们多是这样一个关系:
<ArticleList>
<ArticleItem> <ArticleTitle /> <ArticleTime /> <ArticleLabel /> <ArticleDesc /> </ArticleItem>
<ArticleItem></ArticleItem>
<ArticleItem></ArticleItem>
</ArticleList>复制代码
对于分页功能,传统的实现方法是在后端完成分页而后分批返回到前端的,好比可能会返回一段这样的数据:
{
total:500,
page:1,
data:[]
}复制代码
也就是后端会返回分好页的数据,含有表示总数据量的total
、当前页数的page
,以及属于该页的数据data
。
然而,我这个页面只是个静态页面,数据是放在Github Issues上的经过API获取的。(Github Issues的分页貌似不能自定义数量...),因此无法直接返回分好的数据,因此只能在前端强行分页~
分页功能这一块我偷懒了...用的是 antd
的翻页组件 <Pagination />
。
文档很清晰,使用起来也特别简单。
前端渲染的逻辑(有点蠢):将数据存放到一个数组中,根据当前页数和每页显示条数来计算该显示的索引值,取出相应的数据便可。
翻页组件中:
constructor() {
super();
this.onChangePage = this.onChangePage.bind(this);
}
onChangePage(pageNumber) {
this.props.handlePageChange(pageNumber);
}
render() {
return (
<div className="blog-article-paging"> <Pagination onChange={this.onChangePage} defaultPageSize={this.props.defaultPageSize} total={this.props.total} /> </div> ); }复制代码
当页数发生改变后,会触发从父组件传进 <ArticlePaging />
的方法 handlePageChange
,从而将页数传递到父组件中,而后传递到 <ArticleList />
中。
父组件中:
handlePageChange(pageNumber) {
this.setState({ currentPage: pageNumber });
}
render() {
return (
<div className="archive-list-area">
<ArticleList issues={this.props.issues} defaultPageSize={this.state.defaultPageSize} pageNumber={this.state.currentPage} />
<ArticlePaging handlePageChange={this.handlePageChange} total={this.props.issues.length} defaultPageSize={this.state.defaultPageSize} />
</div>
);
}复制代码
列表中:
render() {
const articlelist = [];
const issues = this.props.issues;
const currentPage = this.props.pageNumber;
const defaultPageSize = this.props.defaultPageSize;
const start = currentPage === 1 ? 0 : (currentPage - 1) * defaultPageSize;
const end = start + defaultPageSize < issues.length ? start + defaultPageSize : issues.length;
for (let i = start; i < end; i += 1) {
const item = issues[i];
articlelist.push(<ArticleItem />); } }复制代码
在 Github Issues
中,能够为一个 issue
添加不少个 label
,我将这些对于博客内容有用的 label
分为三类,分别用不一样颜色来表示。
这里说明一下, label
建立后会随机生成一个 id
,虽说 id
是不重复的,可是文章的类别、标签会一直在增长,当新加一个 label
时,程序中可能也要进行对应的修改,看成区分 label
的标准可能就不太合适,因此我采用颜色来区分它们。
blog
的 issue
才能显示在页面上,过滤 bug
、help
等即便有新的 label
,也只要根据颜色区分是属于哪一类就行了。
在这里的思路主要就是:遍历全部 issues
,而后再遍历每一个 issue
的 labels
,找出属于类别的 label
,而后计数。
const categoryList = [];
const categoryHash = {};
for (let i = 0; i < issues.length; i += 1) {
const labels = issues[i].labels;
for (let j = 0; j < labels.length; j += 1) {
if (labels[j].color === COLOR_LABEL_CATEGORY) {
const category = labels[j].name;
if (categoryHash[category] === undefined) {
categoryHash[category] = true;
const categoryTemp = { category, sum: 1 };
categoryList.push(categoryTemp);
} else {
for (let k = 0; k < categoryList.length; k += 1) {
if (categoryList[k].category === category) {
categoryList[k].sum += 1;
}
}
}
}
}
}复制代码
这样实现得要经历三次循环,复杂度有点高,感受有点蠢,有待改进,若是有更好的方法,请多多指教~
这里的思路和类别的思路基本同样,只不过不一样的显示方式而已。
原本这里是想经过字体大小来体现每一个标签的权重,后来以为可能对于我来讲,暂时只有那几个标签会很频繁,其它标签可能会不多,用字体大小来区分就没有什么意义,仍是改为排序的方式。
文章页主要分为两部分:
有两种方式获取文章具体内容:
issue number
从新发一次请求直接获取内容最后我选择了后者。
文章是用 markdown
语法写的,因此要先转成 html
而后插入页面中,这里用了一个 React
不提倡的属性:dangerouslySetInnerHTML
。
除了渲染markdown
,咱们还得对文章中的代码进行高亮显示,还有就是定制文章中不一样标签的样式。
首先,这里有一个 issue
,但愿你们能够给一些建议~
文章内容是经过 markdown
渲染后插入 dom
中的,因为 React
不建议经过 document.getElementById
的形式获取 dom
元素,因此只能想办法经过字符串匹配的方式获取文章的各个章节标题。
因为我不太熟悉正则表达式,曾经还在sf上咨询过,就采用了其中一个答案:
const issues = content;
const menu = [];
const patt = /(#+)\s+?(.+)/g;
let result = null;
while ((result = patt.exec(issues))) {
menu.push({ level: result[1].length, title: result[2] });
}复制代码
这样能够获取到全部的 #
的字符串,也就是 markdown
中的标题, result[1].length
表示有几个 #
,其实就是几级标题的意思,title
就是标题内容了。
这里还有一个问题,原本经过 <a target="" />
的方式能够实现点击跳转,可是如今渲染出来的 html
中对于每个标题没有独一无二的标识。。。
按年份归档:
按类别归档:
按标签归档:
基本功能是已经基本实现了,如今还存在着如下几个问题,也算是一个 TodoList
吧
Github Issues API
实现评论,得实现 Github
受权登陆antd
的组件,可是 state
中 visibility
一直是 false
webpack
按需加载。这多是目前最方便的方式todo
之一