仿简书PC的react项目(涵盖主流 React 开发相关最新技术点)

此项目github地址:https://github.com/CoderZF/jianshu-pc

目录

技术栈:

react + redux + redux-thunk(让redux支持异步的中间件) + webpack + react-router + ES6/7/8 + axios + react-transition-group(react动画库)+ react-loadable(使组件按需载) + styled-components(css组件化) + immutable.js

运行打包(nodejs 6.0+):

 git clone https://github.com/CoderZF/jianshu-pc.git

 cd jianshu-pc

 npm i  或者运行  yarn(推荐)
  
 npm start

 npm run build (发布)

项目结构及技术点介绍:

该项目由 Create React App 搭建.

项目结构:

jianshu-pc
│   README.md
│   package.json
└───src
│   │   App.js
│   │   idnex.js
│   │   style.js
│   └───common
│   │      └───header
│   │             │   index.js
│   │             │   style.js
│   │             └───store   
│   │                  │   actionCreators.js
│   │                  │   constants.js  
│   │                  │   index.js  
│   │                  │   reducer.js  
│   └───pages
│   │      └───detail
│   │      │       │   index.js
│   │      │       │   style.js
│   │      │       │   loadable.js
│   │      │       └───store   
│   │      │           │   actionCreators.js
│   │      │           │   constants.js  
│   │      │           │   index.js  
│   │      │           │   reducer.js  
│   │      └───home
│   │      │       │   index.js
│   │      │       │   style.js
│   │      │       └───store   
│   │      │       │      actionCreators.js
│   │      │       │      constants.js  
│   │      │       │      index.js  
│   │      │       │      reducer.js   
│   │      │       └───components   
│   │      │           │   List.js
│   │      │           │   Recommend.js  
│   │      │           │   Topic.js  
│   │      │           │   Writer.js   
│   │      └───login
│   │      │       │   index.js
│   │      │       │   style.js
│   │      │       └───store   
│   │      │           │   actionCreators.js
│   │      │           │   constants.js  
│   │      │           │   index.js  
│   │      │           │   reducer.js   
│   │      └───write
│   │      │       │   index.js
│   │      │       │   style.js
│   └───statics
│   │      │   logo.png
│   │      │   ...
│   │      └───iconfont
│   │             │   iconfont.eot
│   │             │   iconfont.js
│   │             │   ...
│   └───store
│   │      │   index.js
│   │      │   reducer.js
│        
└───public
    │   ...

styled components:

使用styled components,可将组件分为逻辑组件和展示组件,逻辑组件只关注逻辑相关的部分,展示组件只关注样式。通过解耦成两种组件,可以使代码变得更加清晰可维护。当逻辑有变化,如后台拉取的数据的格式有所变化时,只需关注并修改逻辑组件上的代码,展示组件的代码不用动。而当UI需要变化时,只需改变展示组件上的代码,并保证展示组件暴露的props接口不变即可。逻辑组件和展示组件各司其职,修改代码时错误发生率也会有所减少。

import { injectGlobal } from 'styled-components';

injectGlobal`
    html, body, div, span, applet, object, iframe,
    h1, h2, h3, h4, h5, h6, p, blockquote, pre,
    a, abbr, acronym, address, big, cite, code,
    del, dfn, em, img, ins, kbd, q, s, samp,
    small, strike, strong, sub, sup, tt, var,
    b, u, i, center,
    dl, dt, dd, ol, ul, li,
    fieldset, form, label, legend,
    table, caption, tbody, tfoot, thead, tr, th, td,
    article, aside, canvas, details, embed, 
    figure, figcaption, footer, header, hgroup, 
    menu, nav, output, ruby, section, summary,
    time, mark, audio, video {
        margin: 0;
        padding: 0;
        border: 0;
        font-size: 100%;
        font: inherit;
        vertical-align: baseline;
    }
    ...
`

上面js可以看出全局公用样式使用injectGlobal,所有css写在字符串模板中,vscode下载vscode-styled-components插件可支持语法高亮。

 import styled from "styled-components";

 export const RecommendWrapper = styled.div`
  margin: 30px 0;
  width: 280px;
`;
 export const RecommendItem = styled.div`
  width: 280px;
  height: 50px;
  background: url(${props => props.imgUrl});
  background-size: contain;
`;
import { RecommendWrapper, RecommendItem } from '../style';

class Recommend extends PureComponent {
    render() {
        return (
            <RecommendWrapper>
                {
                    this.props.list.map((item) => {
                        return <RecommendItem imgUrl={item.get('imgUrl')} key={item.get('id')}/>
                    })
                }
            </RecommendWrapper>
        )
    }
}

上面2个js就是styled components最常用的使用方法,将视图和逻辑彻底分离。

使用iconfont嵌入图标

image.png

image.png

动画库的使用

react-transition-group是react官方提供的动画库,也是之前两个的合体版本,此动画库总共提供三个组件Transition,CSSTransition和TransitonGroup。
本项目为实现输入框在聚焦和失去焦点时其长度的变化,使用了CSSTransition这个组件。

           <CSSTransition in={focused} timeout={200} classNames="slide">
             <NavSearch
               className={focused ? "focused" : ""}
               onFocus={() => handleInputFocus(list)}
               onBlur={handleInputBlur}
             />
           </CSSTransition>
 export const NavSearch = styled.input.attrs({
          placeholder: "搜索"
    })`
          width: 160px;
          height: 38px;
          padding: 0 30px 0 20px;
          margin-top: 9px;
          margin-left: 20px;
          box-sizing: border-box;
          border: none;
          outline: none;
          border-radius: 19px;
          background: #eee;
          font-size: 14px;
          color: #666;
          &::placeholder {
               color: #999;
          }
          &.focused {
              width: 240px;
          }
          &.slide-enter {
               transition: all 0.2s ease-out;
          }
          &.slide-enter-active {
            width: 240px;
          }
          &.slide-exit {
            transition: all 0.2s ease-out;
          }
          &.slide-exit-active {
            width: 160px;
          }
`; 

CSSTransition包装的组件会给其组件自动包装不同状态的类名,如上slide-enter,slide-enter-active,slide-exit,slide-exit-active 就是其根据classNames-xxx自动挂载的。

使用react-redux及其中间件

首先为根组件用react-redux提供的Provider包裹,其目的就是让整个项目的组件可以使用store。

class App extends Component {
  render() {
    return (
    <Provider store={store}>
        <BrowserRouter>
            <div>
            <Header />
                <Route path='/' exact component={Home}></Route>
            <Route path='/login' exact component={Login}></Route>
            <Route path='/write' exact component={Write}></Route>
                <Route path='/detail/:id' exact component={Detail}></Route>
            </div>
        </BrowserRouter>
      </Provider>
    );
  }
}

然后让组件通过connect连接store,connect第一次调用的两个参数分别是store和dispatch对其组件props的映射回调函数

import { connect } from "react-redux";
...
class Header extends Component {
    ...
}
...
export default connect(
  mapStateToProps,
  mapDispathToProps
)(Header);

代码和性能优化:

this绑定优化

  1. 当使用bind()绑定时,最好把所有需要绑定的方法都放在构造函数constructor中,这样就仅需要绑定一次就可以,避免每次渲染时都要重新绑定,函数在别处复用时也无需再次绑定。
import React, {Component} from 'react'

class Test extends React.Component {
    constructor (props) {
        super(props)
        this.handleClick = this.handleClick.bind(this)
    }

    handleClick (e) {
    }

    render () {
        return (
            <div>
                <button onClick={ this.handleClick }>Say Hello</button>
            </div>
        )
    }
}
  1. 箭头函数则会捕获其所在上下文的this值,作为自己的this值,使用箭头函数就不用担心函数内的this不是指向组件内部了。可以按下面这种方式使用箭头函数:
class Test extends React.Component {
    constructor (props) {
        super(props)
        this.state = {message: 'Allo!'}
    }

    handleClick (e) {
        console.log(this.state.message)
    }

    render () {
        return (
            <div>
                <button onClick={ ()=>{ this.handleClick() } }>Say Hello</button>
            </div>
        )
    }
}

使用这个语法有个问题就是每次 Test 渲染的时候都会创建一个不同的回调函数。在大多数情况下,这没有问题。然而如果这个回调函数作为一个属性值传入低阶组件,这些组件可能会进行额外的重新渲染。我们通常建议在构造函数中绑定或像下面代码使用属性初始化器语法来避免这类性能问题。

class Test extends React.Component {
    constructor (props) {
        super(props)
        this.state = {message: 'Allo!'}
    }

    handleClick = (e) => {
        console.log(this.state.message)
    }

    render () {
        return (
            <div>
                <button onClick={ this.handleClick }>Say Hello</button>
            </div>
        )
    }
}

使用无状态组件提高性能

如此组件没有状态的影响或者仅仅纯静态展示时,完全可以用无状态组件来替代有状态组件,因其除render无任何其他生命周期方法且仅仅返回的是个函数,无实例化过程,大大提升了性能。

import React, { PureComponent } from 'react';
import { WriterWrapper } from '../style';

class Writer extends PureComponent {

    render() {
        return (
            <WriterWrapper>HomeWork</WriterWrapper>
        )
    }
}

export default Writer;

上面组件就可以完全改装成如下无状态组件。

import React, { PureComponent } from "react";
import { WriterWrapper } from "../style";

const Writer = () => <WriterWrapper>HomeWork</WriterWrapper>;

export default Writer;

immutable.js与redux结合使用

当我们对一个Immutable对象进行操作的时候,ImmutableJS基于哈希映射树(hash map tries)和vector map tries,只clone该节点以及它的祖先节点,其他保持不变,这样可以共享相同的部分,大大提高性能。在对Immutable对象的操作均会返回新的对象,所以使用redux的reducer中就不需要总是想着不能修改原state,因为对Immutable对象的操作返回就是新的对象,且比普通js深拷贝产生的性能消耗要低得多。
我在项目中也是大量使用immutable.js

import * as constants from './constants';
import { fromJS } from 'immutable';

const defaultState = fromJS({
    focused: false,
    mouseIn: false,
    list: [],
    page: 1,
    totalPage: 1
});

export default (state = defaultState, action) => {
    switch(action.type) {
        case constants.SEARCH_FOCUS:
            return state.set('focused', true);
        case constants.SEARCH_BLUR:
            return state.set('focused', false);
        case constants.CHANGE_LIST:
            return state.merge({
                list: action.data,
                totalPage: action.totalPage
            });
        case constants.MOUSE_ENTER:
            return state.set('mouseIn', true);
        case constants.MOUSE_LEAVE:
            return state.set('mouseIn', false);
        case constants.CHANGE_PAGE:
            return state.set('page', action.page);
        default:
            return state;
    }
}
import * as constants from './constants';
import { fromJS } from 'immutable';
import axios from 'axios';

const changeList = (data) => ({
    type: constants.CHANGE_LIST,
    data: fromJS(data),
    totalPage: Math.ceil(data.length / 10)
});
export const getList = () => {
    return (dispatch) => {
        axios.get('/api/headerList.json').then((res) => {
            const data = res.data;
            dispatch(changeList(data.data));
        }).catch(() => {
            console.log('error');
        })
    }
};

避免无意义的网络请求

比如在请求热门搜索提示项的时候,只有当size是0的时候我才去发送请求。

  const mapDispathToProps = dispatch => {
  return {
    handleInputFocus(list) {
      list.size === 0 && dispatch(actionCreators.getList());
      dispatch(actionCreators.searchFocus());
    },
  ...
    };

异步操作代码拆分优化

在UI组件中因尽量减少业务逻辑操作,像与服务器交互的大量代码都应该解耦出来,所以结合redux-thunk的使用将大量的网络请求代码写在action中就解决了这一问题。
下面是home页的actionCreators.js,当前模块的所有action和网络请求都在此文件中

import axios from 'axios';
import * as constants from './constants';
import { fromJS } from 'immutable';

const changHomeData = (result) => ({
    type: constants.CHANGE_HOME_DATA,
    topicList: result.topicList,
    articleList: result.articleList,
    recommendList: result.recommendList
});

const addHomeList = (list, nextPage) => ({
    type: constants.ADD_ARTICLE_LIST,
    list: fromJS(list),
    nextPage
})

export const getHomeInfo = () => {
    return (dispatch) => {
        axios.get('/api/home.json').then((res) => {
            const result = res.data.data;
            dispatch(changHomeData(result));
        });
    }
}

export const getMoreList = (page) => {
    return (dispatch) => {
        axios.get('/api/homeList.json?page=' + page).then((res) => {
            const result = res.data.data;
            dispatch(addHomeList(result, page + 1));
        });
    }
}

export const toggleTopShow = (show) => ({
    type: constants.TOGGLE_SCROLL_TOP,
    show
})

这样在组件中就可以轻松的去调用网络请求,然后将返回结果发送给reducer进行处理

import React, { PureComponent } from 'react';
import { ListItem, ListInfo, LoadMore } from '../style';
import { connect } from 'react-redux';
import { actionCreators } from '../store';
import { Link } from 'react-router-dom';

class List extends PureComponent {
    render() {
        const { list, getMoreList, page } = this.props;
        return (
            <div>
                {
                    list.map((item, index) => {
                        return (
                            <Link key={index} to={'/detail/' + item.get('id')}>
                                <ListItem >
                                    <img alt='' className='pic' src={item.get('imgUrl')} />
                                    <ListInfo>
                                        <h3 className='title'>{item.get('title')}</h3>
                                        <p className='desc'>{item.get('desc')}</p>
                                    </ListInfo>
                                </ListItem>
                            </Link>
                        );
                    })
                }
                <LoadMore onClick={() => getMoreList(page)}>更多文字</LoadMore>
            </div>
        )
    }
}

const mapState = (state) => ({
    list: state.getIn(['home', 'articleList']),
    page: state.getIn(['home', 'articlePage'])
});

const mapDispatch = (dispatch) => ({
    getMoreList(page) {
        dispatch(actionCreators.getMoreList(page))
    }
})

export default connect(mapState, mapDispatch)(List);

使用PureComponent

继承Component的普通组件,使用react-redux的connect连接了store,那么只要store内的数据发生改变就会让所有连接的组件触发render,这样就会产生不必要的渲染开销,当然使用shouldComponentUpdate也可以阻止不必要的渲染,但这样的话每个组件都要写同样的shouldComponentUpdate方法;继承PureComponent的组件正好解决了这一痛点,默认实现的shouldComponentUpdate。


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容