New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React V15.6 实现一个简单的个人博客 #17

Open
axuebin opened this Issue Sep 28, 2017 · 5 comments

Comments

Projects
None yet
5 participants
@axuebin
Copy link
Owner

axuebin commented Sep 28, 2017

学习 React 的过程中实现了一个个人主页,没有复杂的实现和操作,适合入门 ~


这个项目其实功能很简单,就是常见的主页、博客、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 的配置还不是太熟,就简单的配置了一下可供项目启动:

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 有几个重要的属性: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
module: {
  loaders: [
      {
        test: /\.js$/,
        exclude: /(node_modules)/,
        loader: 'eslint-loader'
      }
    ]
  }
  • 创建 .elintrc文件:
{
  "extends": "airbnb",
  "env":{
    "browser": true
  },
  "rules":{}
}

然后在运行 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 有很多属性,我们可能不需要那么多,先了解了解底下这几种:

// 为了方便,我把注释写在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

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

使用起来很简单:

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

markdown 渲染

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.jshttps://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 是什么

可以看之前的一篇文章:#8

关于React组件的生命周期

可以看之前的一篇文章:#9

前端路由

项目中前端路由用的是 React-Router V4

官方文档:https://reacttraining.com/react-router/web/guides/quick-start

中文文档:http://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,要不然点击任何链接都无法跳转。

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)来实现路由。可以在不需要全屏刷新的情况下,达到切换页面的目的。

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

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

可以通过这样来解决:

componentDidMount() {
    this.node.scrollIntoView();
}

render() {
  return (
    <div ref={node => this.node = node} ></div>
  );
}

状态管理

项目中多次需要用到从 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:

<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 />
  • 文章item组件 <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 />);
  }
}

label

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

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

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

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

类别

在这里的思路主要就是:遍历所有 issues,然后再遍历每个 issuelabels,找出属于类别的 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 的组件,但是 statevisibility 一直是 false
  • 首页渲染。现在打包完的js文件还是太大了,导致首页渲染太慢,这个是接下来工作的重点,也了解过关于这方面的优化:
    • webpack 按需加载。这可能是目前最方便的方式
    • 服务端渲染。这就麻烦了,但是好处也多,不仅解决渲染问题,还有利于SEO,所以也是 todo 之一
  • 响应式。现在的样式都是在PC端的,还未适配移动端。
  • 代码混乱,逻辑不对。这是我自己的问题,需要再修炼。
@JayceZhang9602

This comment has been minimized.

Copy link

JayceZhang9602 commented Sep 28, 2017

@axuebin axuebin changed the title React V15.6 实现一个简单的个人主页 React V15.6 实现一个简单的个人博客 Sep 29, 2017

@zy510322

This comment has been minimized.

Copy link

zy510322 commented Oct 3, 2017

可以

@nankeer

This comment has been minimized.

Copy link

nankeer commented Jan 17, 2018

刚有个这样的想法,发现就有人已经实现了。:sweat_smile 学习了

@axuebin

This comment has been minimized.

Copy link
Owner

axuebin commented Jan 22, 2018

@nankeer GitHub上很多react实现的个人博客的,我这可能只是最简单的那一个。。

@chenyunhb

This comment has been minimized.

Copy link

chenyunhb commented Aug 4, 2018

为什么打不开了?一直显示正在加载

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment