第一部分主要使用React提供的組件,創(chuàng)建博客頁面的展示,包括文章列表頁面和文章詳情頁面。此次系列文章主要面向React和前段初學(xué)者,我會把每一步寫的都很詳細,絕對傻瓜式。 讓我們開始吧。。。
React
先粗略的介紹一下React,React是一個用于構(gòu)建用戶界面的 JavaScript 庫,它有以下幾個特點:
- 聲明式
React 可以非常輕松地創(chuàng)建用戶交互界面。為你應(yīng)用的每一個狀態(tài)設(shè)計簡潔的視圖,在數(shù)據(jù)改變時 React 也可以高效地更新渲染界面。以聲明式編寫UI,可以讓你的代碼更加可靠,且方便調(diào)試。 - 組件化
創(chuàng)建好擁有各自狀態(tài)的組件,再由組件構(gòu)成更加復(fù)雜的界面。無需再用模版代碼,通過使用JavaScript編寫的組件你可以更好地傳遞數(shù)據(jù),將應(yīng)用狀態(tài)和DOM拆分開來。 - 一次學(xué)習(xí),隨處編寫
無論你現(xiàn)在正在使用什么技術(shù)棧,你都可以隨時引入 React 開發(fā)新特性。React 也可以用作開發(fā)原生應(yīng)用的框架 React Native.
有關(guān)React的基礎(chǔ)語法學(xué)習(xí),大家可以查看官方教程
創(chuàng)建一個新的項目
使用Create React App初始化是非常方便的,在安裝時請先確保你已經(jīng)安裝了node.js(6.0+)
#下載create-ract-app相關(guān)組件
npm install -g create-react-app
#創(chuàng)建react-blog
create-react-app react-blog
至此,我們項目結(jié)構(gòu)是這樣的,執(zhí)行下面語句,刪除不必要的文件
cd react-blog
rm -f src/*
博客首頁
我們創(chuàng)建一個名叫Home的Component,用來展示博客的首頁
這里我創(chuàng)建的兩個文件夾components和containers用于存放所有的公共組件和頁面。
import React, { Component } from 'react';
import style from './style.css';
class Home extends Component {
render() {
return (
<h1>Sam's Blog</h1>
)
}
};
export default Home;
下面修改index.js
文件,引入我們剛剛創(chuàng)建的Home
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Home from './containers/Home/Home';
ReactDOM.render(<Home />, document.getElementById('root'));
控制臺輸入npm start
先看一下效果吧
在開始之前,需要先設(shè)計一下頁面結(jié)構(gòu),這樣可以事半功倍。博客首頁主要有三部分:
- 頭部信息
- 導(dǎo)航
- 文章列表
依次初始化這三部分:
Header
Header
用于展示博主信息,以及之后的登錄入口,包含頭像,名稱,以及一段話
import React, { Component } from 'react';
import './style.css';
const logo = require('./logo.svg');
export default class Header extends Component {
render() {
return (
<div className="header">
<span className="log">
<img src={logo} />
</span>
<h1>Sam's Blog</h1>
<p>If you can't measure it , you can't improve it</p>
</div>
)
}
}
然后在Home
中引入Header
這個Component
import React, { Component } from 'react';
import './style.css';
import Header from '../../components/Header/Header'
class Home extends Component {
render() {
return (
<div className="container">
<Header />
</div>
)
}
};
export default Home;
現(xiàn)在我們的頁面就變成這樣了(css部分請查看源代碼)
Menu
Menu部分用于展示文章的所有分類,這里我們將會使用到Ant Design中的部分組件
npm install --save antd
安裝完成后,我們直接引用其中的Menu模塊
import { Menu } from 'antd'
我們先預(yù)先定義幾個分類:
const categories = ['首頁','iOS','Python','ReactJs']
根據(jù)antd中的Menu封裝博客的導(dǎo)航
import React, { Component } from 'react';
import './style.css';
import { Menu, } from 'antd';
const categories = ['首頁','iOS','Python','ReactJs'];
export default class Menus extends Component {
constructor(props) {
super(props)
this.state = {
current: categories[0]
}
}
handleClick = (e) => {
this.setState ({
current: e.key
})
}
render() {
return (
<Menu
onClick={this.handleClick}
selectedKeys={[this.state.current]}
mode="horizontal"
className="menucontainer"
>
{
categories.map((item,index)=>(
<Menu.Item key={item} >
{item}
</Menu.Item>
))
}
</Menu>
)
}
}
constructor
方法中可以初始化state,我們設(shè)置當(dāng)前選中的為categories中第一個元素。
Menu
模塊還有其他的樣式供我們選擇,搭配Layout
可以做出很多樣式的基礎(chǔ)結(jié)構(gòu),詳情請查閱antd
相關(guān)資料,需要注意的是,記得引用antd
的css文件:
import 'antd/dist/antd.css'
在Home
中引入Menus
模塊:
import React, { Component } from 'react';
import './style.css';
import Header from '../../components/Header/Header';
import Menus from '../../components/Menus/Menus';
class Home extends Component {
render() {
return (
<div className="container">
<Header />
<div className="nav">
<Menus />
</div>
<div className="main">
這里是文章列表
</div>
</div>
)
}
};
export default Home;
現(xiàn)在首頁變成如下樣式了:
Route
React Router4
是一個流行的純React重寫的包。現(xiàn)在的版本中已不需要路由配置,現(xiàn)在一切皆組件。
1.安裝
React Router
被拆分成三個包:react-router
,react-router-dom
和react-router-native
。react-router
提供核心的路由組件與函數(shù)。其余兩個則提供運行環(huán)境(即瀏覽器與react-native
)所需的特定組件。
進行網(wǎng)站(將會運行在瀏覽器環(huán)境中)構(gòu)建,我們應(yīng)當(dāng)安裝react-router-dom
。react-router-dom
暴露出react-router
中暴露的對象與方法,因此你只需要安裝并引用react-router-dom
即可。
npm install --save react-router-dom
2.路由器(Router)
在你開始項目前,你需要決定你使用的路由器的類型。對于網(wǎng)頁項目,存在<BrowserRouter>
與<HashRouter>
兩種組件。當(dāng)存在服務(wù)區(qū)來管理動態(tài)請求時,需要使用<BrowserRouter>
組件,而<HashRouter>
被用于靜態(tài)網(wǎng)站。
通常,我們更傾向選擇<BrowserRouter>
,但如果你的網(wǎng)站僅用來呈現(xiàn)靜態(tài)文件,那么<HashRouter>
將會是一個好選擇。
對于我們的項目,將設(shè)將會有服務(wù)器的動態(tài)支持,因此我們選擇<BrowserRouter>
作為路由器組件。
3.歷史(History)
每個路由器都會創(chuàng)建一個history
對象并用其保持追蹤當(dāng)前location[注1]
并且在有變化時對網(wǎng)站進行重新渲染。這個history
對象保證了React Router
提供的其他組件的可用性,所以其他組件必須在router
內(nèi)部渲染。一個React Router
組件如果向父級上追溯卻找不到router組件,那么這個組件將無法正常工作。
4.渲染<Router>
路由器組件無法接受兩個及以上的子元素。基于這種限制的存在,創(chuàng)建一個<App>組件來渲染應(yīng)用其余部分是一個有效的方法(對于服務(wù)端渲染,將應(yīng)用從router組件中分離也是重要的)。
import { BrowserRouter } from 'react-router-dom'
ReactDOM.render((
<BrowserRouter>
<App />
</BrowserRouter>
), document.getElementById('root'))
5.路由(Route)
<Route>
組件是React Router
中主要的結(jié)構(gòu)單元。在任意位置只要匹配了URL
的路徑名(pathname)
你就可以創(chuàng)建<Route>
元素進行渲染。
6.路徑(Path)
<Route>
接受一個數(shù)為string
類型的path
,該值路由匹配的路徑名的類型。例如:<Route path='/roster'/>
會匹配以/roster[注2]
開頭的路徑名。在當(dāng)前path
參數(shù)與當(dāng)前location
的路徑相匹配時,路由就會開始渲染React元素。若不匹配,路由不會進行任何操作[注3]。
<Route path='/roster'/>
// 當(dāng)路徑名為'/'時, path不匹配
// 當(dāng)路徑名為'/roster'或'/roster/2'時, path匹配
// 當(dāng)你只想匹配'/roster'時,你需要使用"exact"參數(shù)
// 則路由僅匹配'/roster'而不會匹配'/roster/2'
<Route exact path='/roster'/>
注意:在匹配路由時,React Router只關(guān)注location的路徑名。當(dāng)URL如下時:
http://www.example.com/my-projects/one?extra=false
React Router去匹配的只是'/my-projects/one'這一部分。
7.匹配路徑
path-to-regexp
用來決定route
元素的path
參數(shù)與當(dāng)前location
是否匹配。它將路徑字符串編譯成正則表達式,并與當(dāng)前location
的路徑名進行匹配比較。除了上面的例子外,路徑字符串有更多高級的選項,詳見[path-to-regexp文檔]
。當(dāng)路由地址匹配成功后,會創(chuàng)建一個含有以下屬性的match對象:
- url :與當(dāng)前l(fā)ocation路徑名所匹配部分
- path?:路由的地址
- isExact :path 是否等于 pathname
- params?:從path-to-regexp獲取的路徑中取出的值都被包含在這個對象中
使用route tester這款工具來對路由與URL進行檢驗。
注意:本例中路由路徑僅支持絕對路徑[注4]。
8.創(chuàng)建博客的路由
可以在路由器(router)組件中的任意位置創(chuàng)建多個<Route>
,但通常我們會把它們放在同一個位置。使用<Switch>
組件來包裹一組<Route>
。<Switch>
會遍歷自身的子元素(即路由)并對第一個匹配當(dāng)前路徑的元素進行渲染。
對于本網(wǎng)站,我們希望匹配一下路徑:
- /?:?博客首頁
- /admin?:后臺管理
- /404?:無效頁面
為了在應(yīng)用中能匹配路徑,在創(chuàng)建<Route>
元素時必須帶有需要匹配的path
作為參數(shù)。
ReactDOM.render(
<Router>
<div>
<Switch>
<Route path='/404' component={NotFound}/>
<Route path='/admin' component={Admin}/>
<Route component={Front} />
</Switch>
</div>
</Router>
, document.getElementById('root')
);
9.<Route>是如何渲染的?
當(dāng)一個路由的path
匹配成功后,路由用來確定渲染結(jié)果的參數(shù)有三種。只需要提供其中一個即可。
- component : 一個
React
組件。當(dāng)帶有component
參數(shù)的route
匹配成功后,route
會返回一個新的元素,其為component
參數(shù)所對應(yīng)的React
組件(使用React.createElement
創(chuàng)建)。 - render : 一個返回
React element
的函數(shù)[注5]。當(dāng)匹配成功后調(diào)用該函數(shù)。該過程與傳入component
參數(shù)類似,并且對于行級渲染與需要向元素傳入額外參數(shù)的操作會更有用。 - children : 一個返回
React element
的函數(shù)。與上述兩個參數(shù)不同,無論route
是否匹配當(dāng)前location
,其都會被渲染。
<Route path='/page' component={Page} />
const extraProps = { color: 'red' }
<Route path='/page' render={(props) => (
<Page {...props} data={extraProps}/>
)}/>
<Route path='/page' children={(props) => (
props.match
? <Page {...props}/>
: <EmptyPage {...props}/>
)}/>
通常component
參數(shù)與render
參數(shù)被更經(jīng)常地使用。children
參數(shù)偶爾會被使用,它更常用在path
無法匹配時呈現(xiàn)的'空'狀態(tài)。在本例中并不會有額外的狀態(tài),所以我們將使用<Route>
的component
參數(shù)。
通過<Route>
渲染的元素會被傳入一些參數(shù)。分別是match
對象,當(dāng)前location
對象[注6]以及history
對象(由router創(chuàng)建)[注7]。
10.嵌套路由
包含在Switch
中的都是一級頁面的路由,文章列表,文章詳情,以及后臺管理的頁面都沒有包含在這里。
在Front
組件中,我們將為四種路徑進行渲染:
-
/
: 對應(yīng)博客的首頁,羅列所有文章 -
/:tag
: 羅列指定分類下的所有文章 -
/detail/:id
: 渲染文章詳情頁面 -
/404
: 無效頁面
import React, {Component} from 'react'
import {
Route,
Switch
} from 'react-router-dom'
import Home from '../Home'
import Detail from '../Detail'
import NotFount from '../NotFount'
import { BackTop } from 'antd'
class Front extends Component {
constructor(props){
super(props);
}
render() {
const {url} = this.props.match;
return(
<div>
<div >
<Switch>
<Route exact path={url} component={Home}/>
<Route path={`/detail/:id`} component={Detail}/>
<Route path={`/:tag`} component={Home}/>
<Route component={NotFound}/>
</Switch>
</div>
<BackTop />
</div>
)
}
}
export default Front;
11.路徑參數(shù)
有時路徑名中存在我們需要獲取的參數(shù)。例如,在文章列表頁,我們要獲取用戶點擊的tag
,在文章詳情頁要獲取文章的id
。我們可以向route
的路徑字符串中添加path
參數(shù)
如'/detail/:id'中:id
這種寫法意味著/Detail/
后的路徑名將會被獲取并存在match.params.id
中。例如,路徑名'/detail/234432'會獲取到一個對象:
{ id: '234423' } // 注獲取的值是字符串類型的
12.Link
現(xiàn)在,我們應(yīng)用需要在各個頁面間切換。如果使用錨點元素(就是)實現(xiàn),在每次點擊時頁面將被重新加載。React Router
提供了<Link>
組件用來避免這種狀況的發(fā)生。當(dāng)你點擊<Link>
時,URL
會更新,組件會被重新渲染,但是頁面不會重新加載,舉個例子:
import { Link } from 'react-router-dom'
const Header = () => (
<header>
<nav>
<ul>
<li><Link to='/'>Home</Link></li>
<li><Link to='/articles'>Articles</Link></li>
<li><Link to='/detail'>Detail</Link></li>
</ul>
</nav>
</header>
)
<Link>
使用'to'參數(shù)來描述需要定位的頁面。它的值即可是字符串也可是location
對象(包含pathname
,search
,hash
與state
屬性)。如果其值為字符串將會被轉(zhuǎn)換為location對象。
13.注釋:
[1] locations
是一個含有描述URL不同部分屬性的對象:
// 一個基本的location對象
{ pathname: '/', search: '', hash: '', key: 'abc123' state: {} }
[2] 你可以渲染無路徑的<Route>,其將會匹配所有l(wèi)ocation。此法用于訪問存在上下文中的變量與方法。
[3] 如果你使用children參數(shù),即便在當(dāng)前l(fā)ocation不匹配時route也將進行渲染。
[4] 當(dāng)需要支持相對路徑的<Route>與<Link>時,你需要多做一些工作。相對<Link>將會比你之前看到的更為復(fù)雜。因其使用了父級的match對象而非當(dāng)前URL來匹配相對路徑。
[5] 這是一個本質(zhì)上無狀態(tài)的函數(shù)組件。內(nèi)部實現(xiàn),component參數(shù)與render參數(shù)的組件是用很大的區(qū)別的。使用component參數(shù)的組件會使用React.createElement來創(chuàng)建元素,使用render參數(shù)的組件則會調(diào)用render函數(shù)。如果我們定義一個內(nèi)聯(lián)函數(shù)并將其傳給component參數(shù),這將會比使用render參數(shù)慢很多。
<Route path='/one' component={One}/>
// React.createElement(props.component)
<Route path='/two' render={() => <Two />}/>
// props.render()
[6]<Route>
與<Switch>
組件都會帶有location
參數(shù)。這能讓你使用與實際location
不同的location
去匹配地址。
[7]?可以傳入staticContext
參數(shù),不過這僅在服務(wù)端渲染時有用。
文章列表
基本的路由我們已經(jīng)創(chuàng)建好了,再一次回歸一下:
我們在入口index.js
文件中創(chuàng)建了一個container Front
, 之后這里還會添加后臺管理頁面的路徑,現(xiàn)在先空著:
<Router>
<div>
<Switch>
<Route component={Front} />
</Switch>
</div>
</Router>
在Front
中我們嵌套了4個路由,分別對應(yīng)首頁,指定分類文章列表,文章詳情,以及無效頁面:
<Switch>
<Route exact path={url} component={Home}/>
<Route path={`/detail/:id`} component={Detail}/>
<Route path={`/:tag`} component={Home}/>
<Route component={NotFound}/>
</Switch>
文章列表頁的主要交互就是:根據(jù)用戶在Menus
中點擊的分類,改變當(dāng)前的路徑,根據(jù)路徑中的參數(shù)渲染當(dāng)前頁面。
Home
這個container
包含三個部分:
- Header
- Menus
- ArticleList
import React, { Component } from 'react';
import './style.css';
import Header from '../../components/Header';
import Menus from '../../components/Menus';
import ArticleList from '../../components/ArticleList'
import { Redirect } from 'react-router-dom';
class Home extends Component {
render() {
const { tags } = this.props;
return (
<div className="h_container">
<Header />
<div className="nav">
<Menus history={this.props.history} />
</div>
<div className="main">
<ArticleList history={this.props.history} tags={tags} />
</div>
</div>
)
}
};
export default Home;
這里ArticleList
主要渲染當(dāng)前分類下的文章列表:
import React, { Component } from 'react'
import ArticleListCell from '../ArticleListCell'
const items = [{
key: '123',
title: '標(biāo)題',
time: '2017-10-29',
viewCount: '100',
commentCount: '23'
},{
key: '123332',
title: '標(biāo)題2',
time: '2017-10-29 12:00:00',
viewCount: '10',
commentCount: '123'
}];
export default class ArticleList extends Component {
constructor(props) {
super(props)
}
render() {
const { tags } = this.props;
return(
<div>
{
items.map((item,index) => (
<ArticleListCell history={this.props.history} key={index} data={item} tags={tags} />
))
}
</div>
)
}
}
先定義一個數(shù)組items
作文假數(shù)據(jù),通過map
方法,返回相應(yīng)的文章內(nèi)容,這里我們新建了一個組件ArticleListCell
:
import React, { Component } from 'react'
import './style.css'
import { Link } from 'react-router-dom'
export default class ArticleListCell extends Component {
render() {
return(
<div className="ac_container" onClick={
() => {
this.props.history.push(`/detail/${this.props.data._id}`, {id: this.props.data_id});
// props.getArticleDetail(props.data_id)
}
}
>
<div className="content">
<div className="title">
<h2>{this.props.data.title} + {this.props.tags}</h2>
</div>
<p className="summary">
這里應(yīng)該有摘要的,因為設(shè)計的數(shù)據(jù)庫表表結(jié)構(gòu)的時候忘記了,后面也是懶得加了,感覺太麻煩了,就算了
</p>
<div>
<div className="info">
<div className="tag">
<img src={require('./calendar.png')} alt="發(fā)表日期"/>
<div>{this.props.data.time}</div>
</div>
<div className="tag">
<img src={require('./views.png')} alt="閱讀數(shù)"/>
<div>{this.props.data.viewCount}</div>
</div>
<div className="tag">
<img src={require('./comments.png')} alt="評論數(shù)"/>
<div>{this.props.data.commentCount}</div>
</div>
</div>
<span className="lastSpan">
閱讀全文
</span>
</div>
</div>
</div>
)
}
}
這個組件一方面展示文章標(biāo)題和其他信息,還有一個功能是點擊時跳轉(zhuǎn)到文章詳情,從代碼中可以看到,我給該部分加了一個onClick
方法,其內(nèi)容是:
this.props.history.push(`/detail/${this.props.data._id}`, {id: this.props.data_id});
這里執(zhí)行了一個react-route
提供的方法history.push
,實現(xiàn)頁面的轉(zhuǎn)換,并將文章id
通過路由傳遞到文章詳情頁。當(dāng)然,這里也可以使用Link
組件實現(xiàn)跳轉(zhuǎn)。
讓我們來看一下,當(dāng)前頁面的樣子:
文章詳情
文章詳情頁面主要包含標(biāo)題,以及文章內(nèi)容。內(nèi)容部分要支持Markdown
。
首先安裝一下remark
和remark-react
兩個模塊,用戶渲染Markdown
內(nèi)容
npm install --save remakr
npm install --save remark-react
來看一下Detail
這個頁面的具體內(nèi)容:
import React, { Component } from 'react'
import remark from 'remark'
import reactRenderer from 'remark-react'
import '../Home/style.css'
import '../../components/Header/style.css'
import './style.css'
const articleContent = "## 標(biāo)題 \n```code``` \n jlkfdsjal"
class Detail extends Component {
render() {
return(
<div className="h_container">
<div className="header">
<h1>文章標(biāo)題在這里</h1>
</div>
<div className="main">
<div id='preview' className="main">
<div className="markdown_body">
{remark().use(reactRenderer).processSync(articleContent).contents}
</div>
</div>
</div>
</div>
)
}
}
export default Detail;
articleContent
是我們文章的內(nèi)容,mardown_body
是我預(yù)先下載的github
主題的marksown css文件,當(dāng)然你也可以下載其他的主題。
<div id='preview' className="main">
<div className="markdown_body">
{remark().use(reactRenderer).processSync(articleContent).contents}
</div>
</div>
完成這部分內(nèi)容后,將Detail
引入到Front
頁面里:
class Front extends Component {
constructor(props){
super(props);
}
render() {
const {url} = this.props.match;
return(
<div>
<div >
<Switch>
<Route exact path={url} component={Home}/>
<Route path={`/detail/:id`} component={Detail}/>
<Route path={`/:tag`} component={Home}/>
<Route component={NotFound}/>
</Switch>
</div>
<BackTop />
</div>
)
}
}
這是,你在點擊文章列表中的ArticleListCell
,就可以看到文章詳情了:
總結(jié)
到此為止,我們的博客展示頁面就算完成了,使用到的技術(shù)有:
- react
- react-router
本篇文章的源碼在這里:React技術(shù)棧+Express+Mongodb實現(xiàn)個人博客 -- Part 1
下一篇文章主要內(nèi)容是使用Ant Design創(chuàng)建后臺管理頁面:
- 標(biāo)簽管理(添加和刪除)
- 新建文章
- 文章管理(修改,刪除)
系列文章
React技術(shù)棧+Express+Mongodb實現(xiàn)個人博客
React技術(shù)棧+Express+Mongodb實現(xiàn)個人博客 -- Part 1 博客頁面展示
React技術(shù)棧+Express+Mongodb實現(xiàn)個人博客 -- Part 2 后臺管理頁面
React技術(shù)棧+Express+Mongodb實現(xiàn)個人博客 -- Part 3 Express + Mongodb創(chuàng)建Server端
React技術(shù)棧+Express+Mongodb實現(xiàn)個人博客 -- Part 4 使用Webpack打包博客工程
React技術(shù)棧+Express+Mongodb實現(xiàn)個人博客 -- Part 5 使用Redux
React技術(shù)棧+Express+Mongodb實現(xiàn)個人博客 -- Part 6 部署