当前位置:   article > 正文

手把手教你用React实现一个简单的个人博客

markdown 支持react

学习 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://webpack.js.org/

中文文档:https://doc.webpack-china.org/

对于 webpack 的配置还不是太熟,就简单的配置了一下可供项目启动:

  1. var webpack = require('webpack');
  2. var path = require('path');
  3. module.exports = {
  4. context: __dirname + '/src',
  5. entry: "./js/index.js",
  6. module: {
  7. loaders: [
  8. {
  9. test: /\.js?$/,
  10. exclude: /(node_modules)/,
  11. loader: 'babel-loader',
  12. query: {
  13. presets: ['react', 'es2015']
  14. }
  15. }, {
  16. test: /\.css$/,
  17. loader: 'style-loader!css-loader'
  18. }, {
  19. test: /\.js$/,
  20. exclude: /(node_modules)/,
  21. loader: 'eslint-loader'
  22. }, {
  23. test: /\.json$/,
  24. loader: 'json-loader'
  25. }
  26. ]
  27. },
  28. output: {
  29. path: __dirname + "/src/",
  30. filename: "bundle.js"
  31. }
  32. }

webpack 有几个重要的属性:entrymoduleoutputplugins,在这里我还没使用到插件,所以没有配置 plugins

module 中的 loaders

  • babel-loader:将代码转换成es5代码
  • css-loader:处理css中路径引用等问题
  • style-loader:动态把样式写入css
  • eslin-loader:使用eslint

包管理

包管理现在使用的还是 NPM

官方文档:https://docs.npmjs.com/

  1. npm init
  2. npm install
  3. npm uninstall

关于npm,可能还需要了解 dependenciesdevDependencies 的区别,我是这样简单理解的:

  • 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
  1. module: {
  2. loaders: [
  3. {
  4. test: /\.js$/,
  5. exclude: /(node_modules)/,
  6. loader: 'eslint-loader'
  7. }
  8. ]
  9. }
  • 创建 .elintrc文件:
  1. {
  2. "extends": "airbnb",
  3. "env":{
  4. "browser": true
  5. },
  6. "rules":{}
  7. }

然后在运行 webpack 的时候,就会执行代码检查啦,看着一堆的 warningerror 是不是很爽~

这里有常见的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

通过控制参数 creatorlabels,可以筛选出作为展示的 Issues。它会返回一个带有 issue 格式对象的数组。每一个 issue 有很多属性,我们可能不需要那么多,先了解了解底下这几种:

  1. // 为了方便,我把注释写在json中了。。
  2. [{
  3. "url": , // issue 的 url
  4. "id": , // issue id , 是一个随机生成的不重复的数字串
  5. "number": , // issue number , 根据创建 issue 的顺序从1开始累加
  6. "title": , // issue 的标题
  7. "labels": [], // issue 的所有 label,它是一个数组
  8. "created_at": , // 创建 issue 的时间
  9. "updated_at": , // 最后修改 issue 的时间
  10. "body": , // issue 的内容
  11. }]
异步请求数据

项目中使用的异步请求数据的方法时 fetch

关于 fetchhttps://segmentfault.com/a/1190000003810652

使用起来很简单:

  1. fetch(url).then(response => response.json())
  2. .then(json => console.log(json))
  3. .catch(e => console.log(e));

markdown 渲染

Github 上查找关于如何在 React 实现 markdown 的渲染,查到了这两种库:

使用起来都很简单。

如果是 react-markdown,只需要这样做:

  1. import ReactMarkdown from 'react-markdown';
  2. const input = '# This is a header\n\nAnd this is a paragraph';
  3. ReactDOM.render(
  4. <ReactMarkdown source={input} />,
  5. document.getElementById('container')
  6. );

如果是marked,这样做:

  1. import marked from 'marked';
  2. const input = '# This is a header\n\nAnd this is a paragraph';
  3. const output = marked(input);

这里有点不太一样,我们获取到了一个字符串 output,注意,是一个字符串,所以我们得将它插入到 dom中,在 React 中,我们可以这样做:

<div dangerouslySetInnerHTML={{ __html: output }} />

由于我们的项目是基于 React 的,所以想着用 react-markdown会更好,而且由于安全问题 React 也不提倡直接往 dom 里插入字符串,然而在使用过程中发现,react-markdown 对表格的支持不友好,所以只好弃用,改用 marked

代码高亮

代码高亮用的是highlight.jshttps://github.com/isagalaev/highlight.js

它和marked可以无缝衔接~

只需要这样既可:

  1. import hljs from 'highlight.js';
  2. marked.setOptions({
  3. highlight: code => hljs.highlightAuto(code).value,
  4. });

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

中文文档:http://reacttraining.cn/

基本使用

<Link to="/blog">Blog</Link>
  1. <Router>
  2. <Route exact path="/" component={Home} />
  3. <Route path="/blog" component={Blog} />
  4. <Route path="/demo" component={Demo} />
  5. </Router>

注意:一定要在根目录的 Route 中声明 exact,要不然点击任何链接都无法跳转。

2级目录跳转

比如我现在要在博客页面上点击跳转,此时的 urllocalhost: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 后,出现了这样一个问题。

刷新页面出现 Cannot GET / 提示,路由未生效。

通过了解,知道了原因是这样,并且可以解决:

  • 由于刷新之后,会根据URL对服务器发送请求,而不是处理路由,导致出现 Cannot GET / 错误。
  • 通过修改 <Router><HashRouter>
  • <HashRouter> 借助URL上的哈希值(hash)来实现路由。可以在不需要全屏刷新的情况下,达到切换页面的目的。

路由跳转后不会自动回到顶部

当前一个页面滚动到一定区域后,点击跳转后,页面虽然跳转了,但是会停留在滚动的区域,不会自动回到页面顶部。

可以通过这样来解决:

  1. componentDidMount() {
  2. this.node.scrollIntoView();
  3. }
  4. render() {
  5. return (
  6. <div ref={node => this.node = node} ></div>
  7. );
  8. }

状态管理

项目中多次需要用到从 Github Issues 请求来的数据,因为之前就知道 Redux 这个东西的存在,虽然有点大材小用,为了学习还是将它用于项目的状态管理,只需要请求一次数据即可。

官方文档:http://redux.js.org/

中文文档:http://cn.redux.js.org/

简单的来说,每一次的修改状态都需要触发 action ,然而其实项目中我现在还没用到修改数据2333。。。

关于状态管理这一块,由于还不是太了解,就不误人子弟了~

主要组件

React是基于组件构建的,所以在搭建页面的开始,我们要先考虑一下我们需要一些什么样的组件,这些组件之间有什么关系,哪些组件是可以复用的等等等。

首页

可以看到,我主要将首页分成了四个部分:

  • header:网站标题,副标题,导航栏
  • banner:about me ~,准备用自己的照片换个背景,但是还没有合适的照片
  • card area:暂时是三个卡片

    • blog card:最近的几篇博文
    • demo card:几个小demo类别
    • me card:算是我放飞自我的地方吧
  • footer:版权信息、备案信息、浏览量

博客页

博客页就是很中规中矩的一个页面吧,这部分是整个项目中代码量最多的部分,包括以下几部分:

  • 文章列表组件
  • 翻页组件
  • 归档按钮组件
  • 类别组件
  • 标签组件
文章列表

文章列表其实就是一个 list,里面有一个个的 item:

  1. <div class="archive-list">
  2. <div class="blog-article-item">文章1</div>
  3. <div class="blog-article-item">文章2</div>
  4. <div>

对于每一个 item,其实是这样的:

一个文章item组件它可能需要包括:

  • 文章标题
  • 文章发布的时间、类别、标签等
  • 文章摘要
  • ...

如果用 DOM 来描述,它应该是这样的:

  1. <div class="blog-article-item">
  2. <div class="blog-article-item-title">文章标题</div>
  3. <div class="blog-article-item-time">时间</div>
  4. <div class="blog-article-item-label">类别</div>
  5. <div class="blog-article-item-label">标签</div>
  6. <div class="blog-article-item-desc">摘要</div>
  7. </div>

所以,我们可以有很多个组件:

  • 文章列表组件 <ArticleList />
  • 文章item组件 <ArticleItem />
  • 类别标签组件 <ArticleLabel />

它们可能是这样一个关系:

  1. <ArticleList>
  2. <ArticleItem>
  3. <ArticleTitle />
  4. <ArticleTime />
  5. <ArticleLabel />
  6. <ArticleDesc />
  7. </ArticleItem>
  8. <ArticleItem></ArticleItem>
  9. <ArticleItem></ArticleItem>
  10. </ArticleList>
分页

对于分页功能,传统的实现方法是在后端完成分页然后分批返回到前端的,比如可能会返回一段这样的数据:

  1. {
  2. total:500,
  3. page:1,
  4. data:[]
  5. }

也就是后端会返回分好页的数据,含有表示总数据量的total、当前页数的page,以及属于该页的数据data

然而,我这个页面只是个静态页面,数据是放在Github Issues上的通过API获取的。(Github Issues的分页貌似不能自定义数量...),所以没法直接返回分好的数据,所以只能在前端强行分页~

分页功能这一块我偷懒了...用的是 antd 的翻页组件 <Pagination />

官方文档:https://ant.design/components/pagination-cn/

文档很清晰,使用起来也特别简单。

前端渲染的逻辑(有点蠢):将数据存放到一个数组中,根据当前页数和每页显示条数来计算该显示的索引值,取出相应的数据即可。

翻页组件中:

  1. constructor() {
  2. super();
  3. this.onChangePage = this.onChangePage.bind(this);
  4. }
  5. onChangePage(pageNumber) {
  6. this.props.handlePageChange(pageNumber);
  7. }
  8. render() {
  9. return (
  10. <div className="blog-article-paging">
  11. <Pagination onChange={this.onChangePage} defaultPageSize={this.props.defaultPageSize} total={this.props.total} />
  12. </div>
  13. );
  14. }

当页数发生改变后,会触发从父组件传进 <ArticlePaging /> 的方法 handlePageChange,从而将页数传递到父组件中,然后传递到 <ArticleList /> 中。

父组件中:

  1. handlePageChange(pageNumber) {
  2. this.setState({ currentPage: pageNumber });
  3. }
  4. render() {
  5. return (
  6. <div className="archive-list-area">
  7. <ArticleList issues={this.props.issues} defaultPageSize={this.state.defaultPageSize} pageNumber={this.state.currentPage} />
  8. <ArticlePaging handlePageChange={this.handlePageChange} total={this.props.issues.length} defaultPageSize={this.state.defaultPageSize} />
  9. </div>
  10. );
  11. }

列表中:

  1. render() {
  2. const articlelist = [];
  3. const issues = this.props.issues;
  4. const currentPage = this.props.pageNumber;
  5. const defaultPageSize = this.props.defaultPageSize;
  6. const start = currentPage === 1 ? 0 : (currentPage - 1) * defaultPageSize;
  7. const end = start + defaultPageSize < issues.length ? start + defaultPageSize : issues.length;
  8. for (let i = start; i < end; i += 1) {
  9. const item = issues[i];
  10. articlelist.push(<ArticleItem />);
  11. }
  12. }
label

Github Issues 中,可以为一个 issue 添加很多个 label,我将这些对于博客内容有用的 label 分为三类,分别用不同颜色来表示。

这里说明一下, label 创建后会随机生成一个 id,虽然说 id 是不重复的,但是文章的类别、标签会一直在增加,当新加一个 label 时,程序中可能也要进行对应的修改,当作区分 label 的标准可能就不太合适,所以我采用颜色来区分它们。

  • 表示这是一篇文章的blog:只有有 blogissue 才能显示在页面上,过滤 bughelp
  • 表示文章类别的:用来表示文章的类别,比如“前端”、“摄影”等
  • 表示文章标签的:用来表示文章的标签,比如“JavaScript”、“React”等

即使有新的 label ,也只要根据颜色区分是属于哪一类就好了。

类别

在这里的思路主要就是:遍历所有 issues,然后再遍历每个 issuelabels,找出属于类别的 label,然后计数。

  1. const categoryList = [];
  2. const categoryHash = {};
  3. for (let i = 0; i < issues.length; i += 1) {
  4. const labels = issues[i].labels;
  5. for (let j = 0; j < labels.length; j += 1) {
  6. if (labels[j].color === COLOR_LABEL_CATEGORY) {
  7. const category = labels[j].name;
  8. if (categoryHash[category] === undefined) {
  9. categoryHash[category] = true;
  10. const categoryTemp = { category, sum: 1 };
  11. categoryList.push(categoryTemp);
  12. } else {
  13. for (let k = 0; k < categoryList.length; k += 1) {
  14. if (categoryList[k].category === category) {
  15. categoryList[k].sum += 1;
  16. }
  17. }
  18. }
  19. }
  20. }
  21. }

这样实现得要经历三次循环,复杂度有点高,感觉有点蠢,有待改进,如果有更好的方法,请多多指教~

标签

这里的思路和类别的思路基本一样,只不过不同的显示方式而已。

本来这里是想通过字体大小来体现每个标签的权重,后来觉得可能对于我来说,暂时只有那几个标签会很频繁,其它标签可能会很少,用字体大小来区分就没有什么意义,还是改成排序的方式。

文章页

文章页主要分为两部分:

  • 文章内容区域:显示文章内容,显示在页面的主体区域
  • 章节目录:文章的章节目录,显示在文章的右侧区域
文章内容

有两种方式获取文章具体内容:

  • 从之前已经请求过的数组中去遍历查找所需的文章内容
  • 通过 issue number 重新发一次请求直接获取内容

最后我选择了后者。

文章是用 markdown 语法写的,所以要先转成 html 然后插入页面中,这里用了一个 React 不提倡的属性:dangerouslySetInnerHTML

除了渲染markdown,我们还得对文章中的代码进行高亮显示,还有就是定制文章中不同标签的样式。

章节目录

首先,这里有一个 issue,希望大家可以给一些建议~

文章内容是通过 markdown 渲染后插入 dom 中的,由于 React 不建议通过 document.getElementById 的形式获取 dom 元素,所以只能想办法通过字符串匹配的方式获取文章的各个章节标题。

由于我不太熟悉正则表达式,曾经还在sf上咨询过,就采用了其中一个答案:

  1. const issues = content;
  2. const menu = [];
  3. const patt = /(#+)\s+?(.+)/g;
  4. let result = null;
  5. while ((result = patt.exec(issues))) {
  6. menu.push({ level: result[1].length, title: result[2] });
  7. }

这样可以获取到所有的 # 的字符串,也就是 markdown 中的标题, result[1].length 表示有几个 #,其实就是几级标题的意思,title 就是标题内容了。

这里还有一个问题,本来通过 <a target="" /> 的方式可以实现点击跳转,但是现在渲染出来的 html 中对于每一个标题没有独一无二的标识。。。

归档页

按年份归档:

按类别归档:

按标签归档:

问题

基本功能是已经基本实现了,现在还存在着以下几个问题,也算是一个 TodoList

  • 评论功能。拟利用 Github Issues API 实现评论,得实现 Github 授权登录
  • 回到顶部。拟利用 antd 的组件,但是 statevisibility 一直是 false
  • 首页渲染。现在打包完的js文件还是太大了,导致首页渲染太慢,这个是接下来工作的重点,也了解过关于这方面的优化:

    • webpack 按需加载。这可能是目前最方便的方式
    • 服务端渲染。这就麻烦了,但是好处也多,不仅解决渲染问题,还有利于SEO,所以也是 todo 之一
  • 代码混乱,逻辑不对。这是我自己的问题,需要再修炼。

原文地址:https://github.com/axuebin/react-blog/issues/17

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/671192
推荐阅读
相关标签
  

闽ICP备14008679号