React技术栈+Express+Mongodb实现个人博客 -- Part 1

第一部分主要使用React提供的组件,创建博客页面的展示,包括文章列表页面和文章详情页面。此次系列文章主要面向React和前段初学者,我会把每一步写的都很详细,绝对傻瓜式。 让我们开始吧。。。

React

先粗略的介绍一下React,React是一个用于构建用户界面的 JavaScript 库,它有以下几个特点:

  • 声明式
    React 可以非常轻松地创建用户交互界面。为你应用的每一个状态设计简洁的视图,在数据改变时 React 也可以高效地更新渲染界面。以声明式编写UI,可以让你的代码更加可靠,且方便调试。
  • 组件化
    创建好拥有各自状态的组件,再由组件构成更加复杂的界面。无需再用模版代码,通过使用JavaScript编写的组件你可以更好地传递数据,将应用状态和DOM拆分开来。
  • 一次学习,随处编写
    无论你现在正在使用什么技术栈,你都可以随时引入 React 开发新特性。React 也可以用作开发原生应用的框架 React Native.

有关React的基础语法学习,大家可以查看官方教程

创建一个新的项目

使用Create React App初始化是非常方便的,在安装时请先确保你已经安装了node.js(6.0+)

#下载create-ract-app相关组件
npm install -g create-react-app 

#创建react-blog
create-react-app react-blog 
屏幕快照 2017-10-26 下午2.50.25.png

至此,我们项目结构是这样的,执行下面语句,删除不必要的文件

cd react-blog
rm -f src/*

博客首页

我们创建一个名叫Home的Component,用来展示博客的首页

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

这里我创建的两个文件夹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文件,引入我们刚刚创建的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

在开始之前,需要先设计一下页面结构,这样可以事半功倍。博客首页主要有三部分:

  • 头部信息
  • 导航
  • 文章列表

依次初始化这三部分:

屏幕快照 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;

现在我们的页面就变成这样了(css部分请查看源代码)

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

Menu

Menu部分用于展示文章的所有分类,这里我们将会使用到Ant Design中的部分组件

npm install --save antd

安装完成后,我们直接引用其中的Menu模块

import { Menu } from 'antd'

我们先预先定义几个分类:

const categories = ['首页','iOS','Python','ReactJs']

根据antd中的Menu封装博客的导航

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,我们设置当前选中的为categories中第一个元素。
Menu模块还有其他的样式供我们选择,搭配Layout可以做出很多样式的基础结构,详情请查阅antd相关资料,需要注意的是,记得引用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;

现在首页变成如下样式了:

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

Route

React Router4是一个流行的纯React重写的包。现在的版本中已不需要路由配置,现在一切皆组件。

1.安装

React Router被拆分成三个包:react-router,react-router-domreact-router-nativereact-router提供核心的路由组件与函数。其余两个则提供运行环境(即浏览器与react-native)所需的特定组件。

进行网站(将会运行在浏览器环境中)构建,我们应当安装react-router-domreact-router-dom暴露出react-router中暴露的对象与方法,因此你只需要安装并引用react-router-dom即可。

npm install --save react-router-dom
2.路由器(Router)

在你开始项目前,你需要决定你使用的路由器的类型。对于网页项目,存在<BrowserRouter><HashRouter>两种组件。当存在服务区来管理动态请求时,需要使用<BrowserRouter>组件,而<HashRouter>被用于静态网站。

通常,我们更倾向选择<BrowserRouter>,但如果你的网站仅用来呈现静态文件,那么<HashRouter>将会是一个好选择。

对于我们的项目,将设将会有服务器的动态支持,因此我们选择<BrowserRouter>作为路由器组件。

3.历史(History)

每个路由器都会创建一个history对象并用其保持追踪当前location[注1]并且在有变化时对网站进行重新渲染。这个history对象保证了React Router提供的其他组件的可用性,所以其他组件必须在router内部渲染。一个React Router组件如果向父级上追溯却找不到router组件,那么这个组件将无法正常工作。

4.渲染<Router>

路由器组件无法接受两个及以上的子元素。基于这种限制的存在,创建一个<App>组件来渲染应用其余部分是一个有效的方法(对于服务端渲染,将应用从router组件中分离也是重要的)。

import { BrowserRouter } from 'react-router-dom'
ReactDOM.render((
  <BrowserRouter>
    <App />
  </BrowserRouter>
), document.getElementById('root'))
5.路由(Route)

<Route>组件是React Router中主要的结构单元。在任意位置只要匹配了URL的路径名(pathname)你就可以创建<Route>元素进行渲染。

6.路径(Path)

<Route>接受一个数为string类型的path,该值路由匹配的路径名的类型。例如:<Route path='/roster'/>会匹配以/roster[注2]开头的路径名。在当前path参数与当前location的路径相匹配时,路由就会开始渲染React元素。若不匹配,路由不会进行任何操作[注3]。

<Route path='/roster'/>
// 当路径名为'/'时, path不匹配
// 当路径名为'/roster'或'/roster/2'时, path匹配
// 当你只想匹配'/roster'时,你需要使用"exact"参数
// 则路由仅匹配'/roster'而不会匹配'/roster/2'
<Route exact path='/roster'/>

注意:在匹配路由时,React Router只关注location的路径名。当URL如下时:

http://www.example.com/my-projects/one?extra=false

React Router去匹配的只是'/my-projects/one'这一部分。

7.匹配路径

path-to-regexp 用来决定route元素的path参数与当前location是否匹配。它将路径字符串编译成正则表达式,并与当前location的路径名进行匹配比较。除了上面的例子外,路径字符串有更多高级的选项,详见[path-to-regexp文档]。当路由地址匹配成功后,会创建一个含有以下属性的match对象

  • url :与当前location路径名所匹配部分
  • path :路由的地址
  • isExact :path 是否等于 pathname
  • params :从path-to-regexp获取的路径中取出的值都被包含在这个对象中

使用route tester这款工具来对路由与URL进行检验。

注意:本例中路由路径仅支持绝对路径[注4]。

8.创建博客的路由

可以在路由器(router)组件中的任意位置创建多个<Route>,但通常我们会把它们放在同一个位置。使用<Switch>组件来包裹一组<Route><Switch>会遍历自身的子元素(即路由)并对第一个匹配当前路径的元素进行渲染。

对于本网站,我们希望匹配一下路径:

  • / : 博客首页
  • /admin :后台管理
  • /404 :无效页面

为了在应用中能匹配路径,在创建<Route>元素时必须带有需要匹配的path作为参数。

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>是如何渲染的?

当一个路由的path匹配成功后,路由用来确定渲染结果的参数有三种。只需要提供其中一个即可。

  • component : 一个React组件。当带有component参数的route匹配成功后,route会返回一个新的元素,其为component参数所对应的React组件(使用React.createElement创建)。
  • render : 一个返回React element的函数[注5]。当匹配成功后调用该函数。该过程与传入component参数类似,并且对于行级渲染与需要向元素传入额外参数的操作会更有用。
  • children : 一个返回React element的函数。与上述两个参数不同,无论route是否匹配当前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参数与render参数被更经常地使用。children参数偶尔会被使用,它更常用在path无法匹配时呈现的'空'状态。在本例中并不会有额外的状态,所以我们将使用<Route>component参数。

通过<Route>渲染的元素会被传入一些参数。分别是match对象,当前location对象[注6]以及history对象(由router创建)[注7]。

10.嵌套路由

包含在Switch中的都是一级页面的路由,文章列表,文章详情,以及后台管理的页面都没有包含在这里。

Front组件中,我们将为四种路径进行渲染:

  • /: 对应博客的首页,罗列所有文章
  • /: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.路径参数

有时路径名中存在我们需要获取的参数。例如,在文章列表页,我们要获取用户点击的tag,在文章详情页要获取文章的id。我们可以向route的路径字符串中添加path参数

如'/detail/:id'中:id这种写法意味着/Detail/后的路径名将会被获取并存在match.params.id中。例如,路径名'/detail/234432'会获取到一个对象:

{ id: '234423' } // 注获取的值是字符串类型的
12.Link

现在,我们应用需要在各个页面间切换。如果使用锚点元素(就是)实现,在每次点击时页面将被重新加载。React Router提供了<Link>组件用来避免这种状况的发生。当你点击<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'参数来描述需要定位的页面。它的值即可是字符串也可是location对象(包含pathnamesearchhashstate属性)。如果其值为字符串将会被转换为location对象。

13.注释:

[1] locations 是一个含有描述URL不同部分属性的对象:

// 一个基本的location对象
{ pathname: '/', search: '', hash: '', key: 'abc123' state: {} }

[2] 你可以渲染无路径的<Route>,其将会匹配所有location。此法用于访问存在上下文中的变量与方法。

[3] 如果你使用children参数,即便在当前location不匹配时route也将进行渲染。

[4] 当需要支持相对路径的<Route>与<Link>时,你需要多做一些工作。相对<Link>将会比你之前看到的更为复杂。因其使用了父级的match对象而非当前URL来匹配相对路径。

[5] 这是一个本质上无状态的函数组件。内部实现,component参数与render参数的组件是用很大的区别的。使用component参数的组件会使用React.createElement来创建元素,使用render参数的组件则会调用render函数。如果我们定义一个内联函数并将其传给component参数,这将会比使用render参数慢很多。

<Route path='/one' component={One}/>
// React.createElement(props.component)
<Route path='/two' render={() => <Two />}/>
// props.render()

[6]<Route><Switch>组件都会带有location参数。这能让你使用与实际location不同的location去匹配地址。

[7] 可以传入staticContext参数,不过这仅在服务端渲染时有用。

文章列表

基本的路由我们已经创建好了,再一次回归一下:
我们在入口index.js文件中创建了一个container Front, 之后这里还会添加后台管理页面的路径,现在先空着:

    <Router>
        <div>
            <Switch>
                <Route component={Front} />
            </Switch>
        </div>
    </Router>

Front中我们嵌套了4个路由,分别对应首页,指定分类文章列表,文章详情,以及无效页面:

     <Switch>
          <Route exact path={url} component={Home}/>
          <Route path={`/detail/:id`} component={Detail}/>
          <Route path={`/:tag`} component={Home}/>
          <Route component={NotFound}/>
     </Switch>

文章列表页的主要交互就是:根据用户在Menus中点击的分类,改变当前的路径,根据路径中的参数渲染当前页面。

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主要渲染当前分类下的文章列表:

import React, { Component } from 'react'
import ArticleListCell from '../ArticleListCell'

const items = [{
    key: '123',
    title: '标题',
    time: '2017-10-29',
    viewCount: '100',
    commentCount: '23'
},{
    key: '123332',
    title: '标题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>
        )

    }
}

先定义一个数组items作文假数据,通过map方法,返回相应的文章内容,这里我们新建了一个组件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">
                        这里应该有摘要的,因为设计的数据库表表结构的时候忘记了,后面也是懒得加了,感觉太麻烦了,就算了
                    </p>
                    <div>
                        <div className="info">
                            <div className="tag">
                                <img src={require('./calendar.png')} alt="发表日期"/>
                                <div>{this.props.data.time}</div>
                            </div>
                            <div className="tag">
                                <img src={require('./views.png')} alt="阅读数"/>
                                <div>{this.props.data.viewCount}</div>
                            </div>
                            <div className="tag">
                                <img src={require('./comments.png')} alt="评论数"/>
                                <div>{this.props.data.commentCount}</div>
                            </div>
                        </div>
                        <span className="lastSpan">
                            阅读全文
                        </span>
                    </div>
                </div>
            </div>
        )
    }
}

这个组件一方面展示文章标题和其他信息,还有一个功能是点击时跳转到文章详情,从代码中可以看到,我给该部分加了一个onClick方法,其内容是:

this.props.history.push(`/detail/${this.props.data._id}`, {id: this.props.data_id});

这里执行了一个react-route提供的方法history.push,实现页面的转换,并将文章id通过路由传递到文章详情页。当然,这里也可以使用Link组件实现跳转。

让我们来看一下,当前页面的样子:

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

文章详情

文章详情页面主要包含标题,以及文章内容。内容部分要支持Markdown

首先安装一下remarkremark-react两个模块,用户渲染Markdown内容

npm install --save remakr
npm install --save remark-react

来看一下Detail这个页面的具体内容:

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 = "## 标题 \n```code``` \n jlkfdsjal"

class Detail extends Component {
    render() {
        return(
            <div className="h_container">
                <div className="header">
                    <h1>文章标题在这里</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是我们文章的内容,mardown_body是我预先下载的github主题的marksown css文件,当然你也可以下载其他的主题。

<div id='preview' className="main">
    <div className="markdown_body">
        {remark().use(reactRenderer).processSync(articleContent).contents}
    </div>
</div>

完成这部分内容后,将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

总结

到此为止,我们的博客展示页面就算完成了,使用到的技术有:

  • react
  • react-router

本篇文章的源码在这里:React技术栈+Express+Mongodb实现个人博客 -- Part 1

下一篇文章主要内容是使用Ant Design创建后台管理页面:

  • 标签管理(添加和删除)
  • 新建文章
  • 文章管理(修改,删除)
articles.gif

系列文章

React技术栈+Express+Mongodb实现个人博客
React技术栈+Express+Mongodb实现个人博客 -- Part 1 博客页面展示
React技术栈+Express+Mongodb实现个人博客 -- Part 2 后台管理页面
React技术栈+Express+Mongodb实现个人博客 -- Part 3 Express + Mongodb创建Server端
React技术栈+Express+Mongodb实现个人博客 -- Part 4 使用Webpack打包博客工程
React技术栈+Express+Mongodb实现个人博客 -- Part 5 使用Redux
React技术栈+Express+Mongodb实现个人博客 -- Part 6 部署

推荐阅读更多精彩内容