手把手教你用React實現一個簡單的個人部落格

axuebin發表於2017-09-28

學習 React 的過程中實現了一個個人部落格,沒有複雜的實現和操作,適合入門 ~

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


這個專案其實功能很簡單,就是常見的主頁、部落格、demo、關於我等功能。

頁面樣式都是自己寫的,黑白風格,可能有點醜。不過還是最低階的 CSS ,準備到時候重構 ~

如果有更好的方法,或者是我的想法有偏差的,歡迎大家交流指正

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

Github:github.com/axuebin/rea…

預覽圖

首頁

部落格頁

文章內容頁

Demo頁

關鍵技術

  • ES6:專案中用到 ES6 的語法,在寫的過程中儘量使用,可能有的地方沒想到
  • React
  • React-Router:前端路由
  • React-Redux:狀態管理
  • webpack:打包
  • marked:Markdown渲染
  • highlight.js:程式碼高亮
  • fetch:非同步請求資料
  • eslint:程式碼檢查
  • antd:部分元件懶得自己寫。。

準備工作

由於不是使用 React 腳手架生成的專案,所以每個東西都是自己手動配置的。。。

模組打包器

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

官方文件:webpack.js.org/

中文文件: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…

相關文章