学习 React 的过程当中实现了一个我的主页,没有复杂的实现和操做,适合入门 ~javascript
原文地址:https://github.com/axuebin/react-blog/issues/17css
这个项目其实功能很简单,就是常见的主页、博客、demo、关于我等功能。html
页面样式都是本身写的,黑白风格,可能有点丑。不过仍是最低级的 CSS ,准备到时候重构 ~前端
若是有更好的方法,或者是个人想法有误差的,欢迎你们交流指正java
欢迎参观:http://axuebin.com/react-blognode
Github:https://github.com/axuebin/react-blogreact
因为不是使用 React 脚手架生成的项目,因此每一个东西都是本身手动配置的。。。webpack
打包用的是 webpack 2.6.1
,准备入坑 webpack 3
。git
官方文档:https://webpack.js.org/github
中文文档: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$/, 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
。
关于npm
,可能还须要了解 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$/, exclude: /(node_modules)/, 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.javashuo.com/article/p-cqggfgkp-ec.html
使用起来很简单:
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
: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/
能够看以前的一篇文章:https://github.com/axuebin/react-blog/issues/8
能够看以前的一篇文章: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
,要否则点击任何连接都没法跳转。
好比我如今要在博客页面上点击跳转,此时的 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
这个东西的存在,虽然有点大材小用,为了学习仍是将它用于项目的状态管理,只须要请求一次数据便可。
官方文档:http://redux.js.org/
简单的来讲,每一次的修改状态都须要触发 action
,然而其实项目中我如今还没用到修改数据2333。。。
关于状态管理这一块,因为还不是太了解,就不误人子弟了~
React是基于组件构建的,因此在搭建页面的开始,咱们要先考虑一下咱们须要一些什么样的组件,这些组件之间有什么关系,哪些组件是能够复用的等等等。
能够看到,我主要将首页分红了四个部分:
card area:暂时是三个卡片
博客页就是很中规中矩的一个页面吧,这部分是整个项目中代码量最多的部分,包括如下几部分:
文章列表其实就是一个 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 />
。
官方文档: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 />); } }
在 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
首页渲染。如今打包完的js文件仍是太大了,致使首页渲染太慢,这个是接下来工做的重点,也了解过关于这方面的优化:
webpack
按需加载。这多是目前最方便的方式todo
之一