学习 React 的过程中实现了一个个人主页,没有复杂的实现和操作,适合入门 ~
原文地址:https://github.com/axuebin/react-blog/issues/17
这个项目其实功能很简单,就是常见的主页、博客、demo、关于我等功能。
页面样式都是自己写的,黑白风格,可能有点丑。不过还是最低级的 CSS ,准备到时候重构 ~
如果有更好的方法,或者是我的想法有偏差的,欢迎大家交流指正
欢迎参观:http://axuebin.com/react-blog
Github:https://github.com/axuebin/react-blog
预览图
首页
博客页
文章内容页
Demo页
关键技术
- ES6:项目中用到 ES6 的语法,在写的过程中尽量使用,可能有的地方没想到
- React
- React-Router:前端路由
- React-Redux:状态管理
- webpack:打包
- marked:Markdown渲染
- highlight.js:代码高亮
- fetch:异步请求数据
- eslint:代码检查
- antd:部分组件懒得自己写。。
准备工作
由于不是使用 React 脚手架生成的项目,所以每个东西都是自己手动配置的。。。
模块打包器
打包用的是 webpack 2.6.1
,准备入坑 webpack 3
。
中文文档:https://doc.webpack-china.org/
对于 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$/,loader: 'eslint-loader' },{ test: /\.json$/,loader: 'json-loader' } ] },output: { path: __dirname + "/src/",filename: "bundle.js" } }
webpack
有几个重要的属性:entry
、module
、output
、plugins
,在这里我还没使用到插件,所以没有配置 plugins
。
module
中的 loaders
:
包管理
包管理现在使用的还是 NPM
。
- npm init
- npm install
- npm uninstall
关于npm
,可能还需要了解 dependencies
和 devDependencies
的区别,我是这样简单理解的:
- dependencies:项目跑起来后需要使用到的模块
- devDependencies:开发的时候需要用的模块,但是项目跑起来后就不需要了
代码检查
项目使用现在比较流行的 ESLint
作为代码检查工具,并使用 Airbnb
的检查规则。
ESLint:https://github.com/eslint/eslint
eslint-config-airbnb:https://www.npmjs.com/package/eslint-config-airbnb
在 package.json
中可以看到,关于 ESLint
的包就是放在 devDependencies
底下的,因为它只是在开发的时候会使用到。
使用
- 在
webpack
配置中加载eslint-loader
:
module: { loaders: [ { test: /\.js$/,loader: 'eslint-loader' } ] }
- 创建
.elintrc
文件:
{ "extends": "airbnb","env":{ "browser": true },"rules":{} }
然后在运行 webpack
的时候,就会执行代码检查啦,看着一堆的 warning
、error
是不是很爽~
这里有常见的ESLint规则:http://eslint.cn/docs/rules/
数据源
由于是为了练习 React
,暂时就只考虑搭建一个静态页面,而且现在越来越多的大牛喜欢用 Github Issues
来写博客,也可以更好的地提供评论功能,所以我也想试试用 Github Issues
来作为博客的数据源。
API在这:https://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
:http://www.jb51.cc/article/p-obdpwzkp-b.html
使用起来很简单:
fetch(url).then(response => response.json()) .then(json => console.log(json)) .catch(e => console.log(e));
markdown 渲染
在 Github
上查找关于如何在 React
实现 markdown
的渲染,查到了这两种库:
- react-markdown:https://github.com/rexxars/react-markdown
- marked:https://github.com/chjj/marked
使用起来都很简单。
如果是 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
:https://github.com/isagalaev/highlight.js
它和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';
在这可以看到每种语言的高亮效果和配色风格:https://highlightjs.org/
React
state 和 props 是什么
可以看之前的一篇文章:https://github.com/axuebin/react-blog/issues/8
关于React组件的生命周期
可以看之前的一篇文章:https://github.com/axuebin/react-blog/issues/9
前端路由
项目中前端路由用的是 React-Router V4
。
官方文档:https://reacttraining.com/react-router/web/guides/quick-start
基本使用
<Link to="/blog">Blog</Link>
<Router> <Route exact path="/" component={Home} /> <Route path="/blog" component={Blog} /> <Route path="/demo" component={Demo} /> </Router>
注意:一定要在根目录的 Route
中声明 exact
,要不然点击任何链接都无法跳转。
2级目录跳转
比如我现在要在博客页面上点击跳转,此时的 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
获取。
HashRouter
当我把项目托管到 Github Page
后,出现了这样一个问题。
通过了解,知道了原因是这样,并且可以解决:
- 由于刷新之后,会根据URL对服务器发送请求,而不是处理路由,导致出现
Cannot GET /
错误。 - 通过修改
<Router>
→<HashRouter>
。 -
<HashRouter>
借助URL上的哈希值(hash)来实现路由。可以在不需要全屏刷新的情况下,达到切换页面的目的。
路由跳转后不会自动回到顶部
当前一个页面滚动到一定区域后,点击跳转后,页面虽然跳转了,但是会停留在滚动的区域,不会自动回到页面顶部。
可以通过这样来解决:
componentDidMount() { this.node.scrollIntoView(); } render() { return ( <div ref={node => this.node = node} ></div> ); }
状态管理
项目中多次需要用到从 Github Issues
请求来的数据,因为之前就知道 Redux
这个东西的存在,虽然有点大材小用,为了学习还是将它用于项目的状态管理,只需要请求一次数据即可。
官方文档:http://redux.js.org/
简单的来说,每一次的修改状态都需要触发 action
,然而其实项目中我现在还没用到修改数据2333。。。
关于状态管理这一块,由于还不是太了解,就不误人子弟了~
主要组件
React是基于组件构建的,所以在搭建页面的开始,我们要先考虑一下我们需要一些什么样的组件,这些组件之间有什么关系,哪些组件是可以复用的等等等。
首页
可以看到,我主要将首页分成了四个部分:
- header:网站标题,副标题,导航栏
- banner:about me ~,准备用自己的照片换个背景,但是还没有合适的照片
-
card area:暂时是三个卡片
- blog card:最近的几篇博文
- demo card:几个小demo类别
- me card:算是我放飞自我的地方吧
- footer:版权信息、备案信息、浏览量
博客页
博客页就是很中规中矩的一个页面吧,这部分是整个项目中代码量最多的部分,包括以下几部分:
文章列表
文章列表其实就是一个 list
,里面有一个个的 item
:
<div class="archive-list"> <div class="blog-article-item">文章1</div> <div class="blog-article-item">文章2</div> <div>
对于每一个 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> <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 />
。
官方文档:https://ant.design/components/pagination-cn/
文档很清晰,使用起来也特别简单。
前端渲染的逻辑(有点蠢):将数据存放到一个数组中,根据当前页数和每页显示条数来计算该显示的索引值,取出相应的数据即可。
翻页组件中:
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 />); } }
label
在 Github Issues
中,可以为一个 issue
添加很多个 label
,我将这些对于博客内容有用的 label
分为三类,分别用不同颜色来表示。
这里说明一下, label
创建后会随机生成一个 id
,虽然说 id
是不重复的,但是文章的类别、标签会一直在增加,当新加一个 label
时,程序中可能也要进行对应的修改,当作区分 label
的标准可能就不太合适,所以我采用颜色来区分它们。
- 表示这是一篇文章的blog:只有有
blog
的issue
才能显示在页面上,过滤bug
、help
等 - 表示文章类别的:用来表示文章的类别,比如“前端”、“摄影”等
- 表示文章标签的:用来表示文章的标签,比如“JavaScript”、“React”等
即使有新的 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; } } } } } }
这样实现得要经历三次循环,复杂度有点高,感觉有点蠢,有待改进,如果有更好的方法,请多多指教~
标签
这里的思路和类别的思路基本一样,只不过不同的显示方式而已。
本来这里是想通过字体大小来体现每个标签的权重,后来觉得可能对于我来说,暂时只有那几个标签会很频繁,其它标签可能会很少,用字体大小来区分就没有什么意义,还是改成排序的方式。
文章页
文章页主要分为两部分:
文章内容
最后我选择了后者。
文章是用 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
吧