React技術(shù)棧+Express+Mongodb實現(xiàn)個人博客 -- Part 1

第一部分主要使用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 
屏幕快照 2017-10-26 下午2.50.25.png

至此,我們項目結(jié)構(gòu)是這樣的,執(zhí)行下面語句,刪除不必要的文件

cd react-blog
rm -f src/*

博客首頁

我們創(chuàng)建一個名叫Home的Component,用來展示博客的首頁

屏幕快照 2017-10-26 下午3.05.21.png

這里我創(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先看一下效果吧

屏幕快照 2017-10-26 下午3.11.56.png

在開始之前,需要先設(shè)計一下頁面結(jié)構(gòu),這樣可以事半功倍。博客首頁主要有三部分:

  • 頭部信息
  • 導(dǎo)航
  • 文章列表

依次初始化這三部分:

屏幕快照 2017-10-26 下午3.20.21.png

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部分請查看源代碼)

屏幕快照 2017-10-26 下午4.18.47.png

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)在首頁變成如下樣式了:

屏幕快照 2017-10-26 下午6.53.06.png

Route

React Router4是一個流行的純React重寫的包。現(xiàn)在的版本中已不需要路由配置,現(xiàn)在一切皆組件。

1.安裝

React Router被拆分成三個包:react-router,react-router-domreact-router-nativereact-router提供核心的路由組件與函數(shù)。其余兩個則提供運行環(huán)境(即瀏覽器與react-native)所需的特定組件。

進行網(wǎng)站(將會運行在瀏覽器環(huán)境中)構(gòu)建,我們應(yīng)當(dāng)安裝react-router-domreact-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對象(包含pathnamesearchhashstate屬性)。如果其值為字符串將會被轉(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)前頁面的樣子:

屏幕快照 2017-10-27 下午5.20.33.png

文章詳情

文章詳情頁面主要包含標(biāo)題,以及文章內(nèi)容。內(nèi)容部分要支持Markdown

首先安裝一下remarkremark-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,就可以看到文章詳情了:

屏幕快照 2017-10-27 下午5.29.19.png

總結(jié)

到此為止,我們的博客展示頁面就算完成了,使用到的技術(shù)有:

  • react
  • react-router

本篇文章的源碼在這里:React技術(shù)棧+Express+Mongodb實現(xiàn)個人博客 -- Part 1

下一篇文章主要內(nèi)容是使用Ant Design創(chuàng)建后臺管理頁面:

  • 標(biāo)簽管理(添加和刪除)
  • 新建文章
  • 文章管理(修改,刪除)
articles.gif

系列文章

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 部署

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容