手把手教你用React實現一個簡單的我的博客

學習 React 的過程當中實現了一個我的博客,沒有複雜的實現和操做,適合入門 ~javascript

原文地址:github.com/axuebin/rea…css


這個項目其實功能很簡單,就是常見的主頁、博客、demo、關於我等功能。html

頁面樣式都是本身寫的,黑白風格,可能有點醜。不過仍是最低級的 CSS ,準備到時候重構 ~前端

若是有更好的方法,或者是個人想法有誤差的,歡迎你們交流指正java

歡迎參觀:axuebin.com/react-blognode

Github:github.com/axuebin/rea…react

預覽圖

首頁

博客頁

文章內容頁

Demo頁

關鍵技術

  • ES6:項目中用到 ES6 的語法,在寫的過程當中儘可能使用,可能有的地方沒想到
  • React
  • React-Router:前端路由
  • React-Redux:狀態管理
  • webpack:打包
  • marked:Markdown渲染
  • highlight.js:代碼高亮
  • fetch:異步請求數據
  • eslint:代碼檢查
  • antd:部分組件懶得本身寫。。

準備工做

因爲不是使用 React 腳手架生成的項目,因此每一個東西都是本身手動配置的。。。webpack

模塊打包器

打包用的是 webpack 2.6.1,準備入坑 webpack 3git

官方文檔:webpack.js.org/github

中文文檔: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

官方文檔:docs.npmjs.com/

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

關於npm,可能還須要瞭解 dependenciesdevDependencies 的區別,我是這樣簡單理解的:

  • 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 的時候,就會執行代碼檢查啦,看着一堆的 warningerror 是否是很爽~

這裏有常見的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複製代碼

經過控制參數 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

關於 fetchsegmentfault.com/a/119000000…

使用起來很簡單:

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.jsgithub.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/

React

state 和 props 是什麼

能夠看以前的一篇文章:github.com/axuebin/rea…

關於React組件的生命週期

能夠看以前的一篇文章: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,要否則點擊任何連接都沒法跳轉。

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 這個東西的存在,雖然有點大材小用,爲了學習仍是將它用於項目的狀態管理,只須要請求一次數據便可。

官方文檔:redux.js.org/

中文文檔: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 />

官方文檔:ant.design/components/…

文檔很清晰,使用起來也特別簡單。

前端渲染的邏輯(有點蠢):將數據存放到一個數組中,根據當前頁數和每頁顯示條數來計算該顯示的索引值,取出相應的數據便可。

翻頁組件中:

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 之一
  • 代碼混亂,邏輯不對。這是我本身的問題,須要再修煉。

原文地址:github.com/axuebin/rea…

相關文章
相關標籤/搜索