學習 React 的過程當中實現了一個我的博客,沒有複雜的實現和操做,適合入門 ~javascript
原文地址:github.com/axuebin/rea…css
這個項目其實功能很簡單,就是常見的主頁、博客、demo、關於我等功能。html
頁面樣式都是本身寫的,黑白風格,可能有點醜。不過仍是最低級的 CSS ,準備到時候重構 ~前端
若是有更好的方法,或者是個人想法有誤差的,歡迎你們交流指正java
歡迎參觀:axuebin.com/react-blognode
Github:github.com/axuebin/rea…react
因爲不是使用 React 腳手架生成的項目,因此每一個東西都是本身手動配置的。。。webpack
打包用的是 webpack 2.6.1
,準備入坑 webpack 3
。git
官方文檔:webpack.js.org/github
對於 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
。
官方文檔:docs.npmjs.com/
關於npm
,可能還須要瞭解 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
的時候,就會執行代碼檢查啦,看着一堆的 warning
、error
是否是很爽~
這裏有常見的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複製代碼
經過控制參數 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
:segmentfault.com/a/119000000…
使用起來很簡單:
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
:github.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/
能夠看以前的一篇文章:github.com/axuebin/rea…
能夠看以前的一篇文章: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
,要否則點擊任何連接都沒法跳轉。
好比我如今要在博客頁面上點擊跳轉,此時的 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
這個東西的存在,雖然有點大材小用,爲了學習仍是將它用於項目的狀態管理,只須要請求一次數據便可。
官方文檔:redux.js.org/
中文文檔:cn.redux.js.org/
簡單的來講,每一次的修改狀態都須要觸發 action
,然而其實項目中我如今還沒用到修改數據2333。。。
關於狀態管理這一塊,因爲還不是太瞭解,就不誤人子弟了~
React是基於組件構建的,因此在搭建頁面的開始,咱們要先考慮一下咱們須要一些什麼樣的組件,這些組件之間有什麼關係,哪些組件是能夠複用的等等等。
能夠看到,我主要將首頁分紅了四個部分:
博客頁就是很中規中矩的一個頁面吧,這部分是整個項目中代碼量最多的部分,包括如下幾部分:
文章列表其實就是一個 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 />
。
文檔很清晰,使用起來也特別簡單。
前端渲染的邏輯(有點蠢):將數據存放到一個數組中,根據當前頁數和每頁顯示條數來計算該顯示的索引值,取出相應的數據便可。
翻頁組件中:
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
webpack
按需加載。這多是目前最方便的方式todo
之一