學習 React 的過程中實現了一個個人部落格,沒有複雜的實現和操作,適合入門 ~
這個專案其實功能很簡單,就是常見的主頁、部落格、demo、關於我等功能。
頁面樣式都是自己寫的,黑白風格,可能有點醜。不過還是最低階的 CSS ,準備到時候重構 ~
如果有更好的方法,或者是我的想法有偏差的,歡迎大家交流指正
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/
對於 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
:
- babel-loader:將程式碼轉換成es5程式碼
- css-loader:處理css中路徑引用等問題
- style-loader:動態把樣式寫入css
- eslin-loader:使用eslint
包管理
包管理現在使用的還是 NPM
。
官方文件:docs.npmjs.com/
- npm init
- npm install
- npm uninstall
關於npm
,可能還需要了解 dependencies
和 devDependencies
的區別,我是這樣簡單理解的:
- 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));複製程式碼
markdown 渲染
在 Github
上查詢關於如何在 React
實現 markdown
的渲染,查到了這兩種庫:
- react-markdown:github.com/rexxars/rea…
- marked:github.com/chjj/marked
使用起來都很簡單。
如果是 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/
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級目錄跳轉
比如我現在要在部落格頁面上點選跳轉,此時的 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
獲取。
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 />
。
文件很清晰,使用起來也特別簡單。
前端渲染的邏輯(有點蠢):將資料存放到一個陣列中,根據當前頁數和每頁顯示條數來計算該顯示的索引值,取出相應的資料即可。
翻頁元件中:
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:只有有
blog
的issue
才能顯示在頁面上,過濾bug
、help
等 - 表示文章類別的:用來表示文章的類別,比如“前端”、“攝影”等
- 表示文章標籤的:用來表示文章的標籤,比如“JavaScript”、“React”等
即使有新的 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
- 首頁渲染。現在打包完的js檔案還是太大了,導致首頁渲染太慢,這個是接下來工作的重點,也瞭解過關於這方面的優化:
webpack
按需載入。這可能是目前最方便的方式- 服務端渲染。這就麻煩了,但是好處也多,不僅解決渲染問題,還有利於SEO,所以也是
todo
之一
- 程式碼混亂,邏輯不對。這是我自己的問題,需要再修煉。