此項(xiàng)目github地址:https://github.com/CoderZF/jianshu-pc
目錄
技術(shù)棧:
react + redux + redux-thunk(讓redux支持異步的中間件) + webpack + react-router + ES6/7/8 + axios + react-transition-group(react動(dòng)畫庫)+ react-loadable(使組件按需載) + styled-components(css組件化) + immutable.js
運(yùn)行打包(nodejs 6.0+):
git clone https://github.com/CoderZF/jianshu-pc.git
cd jianshu-pc
npm i 或者運(yùn)行 yarn(推薦)
npm start
npm run build (發(fā)布)
項(xiàng)目結(jié)構(gòu)及技術(shù)點(diǎn)介紹:
該項(xiàng)目由 Create React App 搭建.
項(xiàng)目結(jié)構(gòu):
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,可將組件分為邏輯組件和展示組件,邏輯組件只關(guān)注邏輯相關(guān)的部分缠借,展示組件只關(guān)注樣式。通過解耦成兩種組件,可以使代碼變得更加清晰可維護(hù)亡笑。當(dāng)邏輯有變化,如后臺(tái)拉取的數(shù)據(jù)的格式有所變化時(shí)横朋,只需關(guān)注并修改邏輯組件上的代碼仑乌,展示組件的代碼不用動(dòng)。而當(dāng)UI需要變化時(shí),只需改變展示組件上的代碼晰甚,并保證展示組件暴露的props接口不變即可衙传。邏輯組件和展示組件各司其職,修改代碼時(shí)錯(cuò)誤發(fā)生率也會(huì)有所減少厕九。
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個(gè)js就是styled components最常用的使用方法俊鱼,將視圖和邏輯徹底分離。
使用iconfont嵌入圖標(biāo)
動(dòng)畫庫的使用
react-transition-group是react官方提供的動(dòng)畫庫穿香,也是之前兩個(gè)的合體版本亭引,此動(dòng)畫庫總共提供三個(gè)組件Transition,CSSTransition和TransitonGroup皮获。
本項(xiàng)目為實(shí)現(xiàn)輸入框在聚焦和失去焦點(diǎn)時(shí)其長度的變化焙蚓,使用了CSSTransition這個(gè)組件。
<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包裝的組件會(huì)給其組件自動(dòng)包裝不同狀態(tài)的類名洒宝,如上slide-enter购公,slide-enter-active,slide-exit雁歌,slide-exit-active 就是其根據(jù)classNames-xxx自動(dòng)掛載的宏浩。
使用react-redux及其中間件
首先為根組件用react-redux提供的Provider包裹,其目的就是讓整個(gè)項(xiàng)目的組件可以使用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第一次調(diào)用的兩個(gè)參數(shù)分別是store和dispatch對(duì)其組件props的映射回調(diào)函數(shù)
import { connect } from "react-redux";
...
class Header extends Component {
...
}
...
export default connect(
mapStateToProps,
mapDispathToProps
)(Header);
代碼和性能優(yōu)化:
this綁定優(yōu)化
- 當(dāng)使用bind()綁定時(shí),最好把所有需要綁定的方法都放在構(gòu)造函數(shù)constructor中乏盐,這樣就僅需要綁定一次就可以佳窑,避免每次渲染時(shí)都要重新綁定,函數(shù)在別處復(fù)用時(shí)也無需再次綁定父能。
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>
)
}
}
- 箭頭函數(shù)則會(huì)捕獲其所在上下文的this值神凑,作為自己的this值,使用箭頭函數(shù)就不用擔(dān)心函數(shù)內(nèi)的this不是指向組件內(nèi)部了何吝「任可以按下面這種方式使用箭頭函數(shù):
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>
)
}
}
使用這個(gè)語法有個(gè)問題就是每次 Test 渲染的時(shí)候都會(huì)創(chuàng)建一個(gè)不同的回調(diào)函數(shù)。在大多數(shù)情況下爱榕,這沒有問題瓣喊。然而如果這個(gè)回調(diào)函數(shù)作為一個(gè)屬性值傳入低階組件,這些組件可能會(huì)進(jìn)行額外的重新渲染黔酥。我們通常建議在構(gòu)造函數(shù)中綁定或像下面代碼使用屬性初始化器語法來避免這類性能問題藻三。
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>
)
}
}
使用無狀態(tài)組件提高性能
如此組件沒有狀態(tài)的影響或者僅僅純靜態(tài)展示時(shí)八匠,完全可以用無狀態(tài)組件來替代有狀態(tài)組件,因其除render無任何其他生命周期方法且僅僅返回的是個(gè)函數(shù)趴酣,無實(shí)例化過程梨树,大大提升了性能。
import React, { PureComponent } from 'react';
import { WriterWrapper } from '../style';
class Writer extends PureComponent {
render() {
return (
<WriterWrapper>HomeWork</WriterWrapper>
)
}
}
export default Writer;
上面組件就可以完全改裝成如下無狀態(tài)組件岖寞。
import React, { PureComponent } from "react";
import { WriterWrapper } from "../style";
const Writer = () => <WriterWrapper>HomeWork</WriterWrapper>;
export default Writer;
immutable.js與redux結(jié)合使用
當(dāng)我們對(duì)一個(gè)Immutable對(duì)象進(jìn)行操作的時(shí)候抡四,ImmutableJS基于哈希映射樹(hash map tries)和vector map tries,只clone該節(jié)點(diǎn)以及它的祖先節(jié)點(diǎn)仗谆,其他保持不變指巡,這樣可以共享相同的部分,大大提高性能隶垮。在對(duì)Immutable對(duì)象的操作均會(huì)返回新的對(duì)象藻雪,所以使用redux的reducer中就不需要總是想著不能修改原state,因?yàn)閷?duì)Immutable對(duì)象的操作返回就是新的對(duì)象狸吞,且比普通js深拷貝產(chǎn)生的性能消耗要低得多勉耀。
我在項(xiàng)目中也是大量使用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');
})
}
};
避免無意義的網(wǎng)絡(luò)請(qǐng)求
比如在請(qǐng)求熱門搜索提示項(xiàng)的時(shí)候,只有當(dāng)size是0的時(shí)候我才去發(fā)送請(qǐng)求蹋偏。
const mapDispathToProps = dispatch => {
return {
handleInputFocus(list) {
list.size === 0 && dispatch(actionCreators.getList());
dispatch(actionCreators.searchFocus());
},
...
};
異步操作代碼拆分優(yōu)化
在UI組件中因盡量減少業(yè)務(wù)邏輯操作便斥,像與服務(wù)器交互的大量代碼都應(yīng)該解耦出來,所以結(jié)合redux-thunk的使用將大量的網(wǎng)絡(luò)請(qǐng)求代碼寫在action中就解決了這一問題威始。
下面是home頁的actionCreators.js枢纠,當(dāng)前模塊的所有action和網(wǎng)絡(luò)請(qǐng)求都在此文件中
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
})
這樣在組件中就可以輕松的去調(diào)用網(wǎng)絡(luò)請(qǐng)求李茫,然后將返回結(jié)果發(fā)送給reducer進(jìn)行處理
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內(nèi)的數(shù)據(jù)發(fā)生改變就會(huì)讓所有連接的組件觸發(fā)render缘揪,這樣就會(huì)產(chǎn)生不必要的渲染開銷脓斩,當(dāng)然使用shouldComponentUpdate也可以阻止不必要的渲染木西,但這樣的話每個(gè)組件都要寫同樣的shouldComponentUpdate方法;繼承PureComponent的組件正好解決了這一痛點(diǎn)俭厚,默認(rèn)實(shí)現(xiàn)的shouldComponentUpdate户魏。