准备工作

建立 react-app

1
2
3
npx create-react-app jianshu
cd jianshu
npm start

ELIFECYCLE 问题

error code ELIFECYCLE

How to solve this problem

  • 清除缓存, npm cache clean --force
  • 删掉 node_modules 目录及 package-lock.json 文件,rm -rf node_modules package-lock.json
  • npm install
  • npm start

react-app 基本框架

删除不必要的代码, 其中 ./src 目录只留下 index.js, index.css, App.js 三个文件及相关代码

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';


ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
1
/* null */
1
2
3
4
5
6
7
8
9
function App() {
return (
<div className="App">
Learn React
</div>
);
}

export default App;

方便地管理 css 样式

css 文件只要在一个 js 文件内引用, 全局均可以使用. 那么, 如何正确地引入 css 样式呢? 借助一个第三方库 styled-components,

  • npm install styled-components --save
  • 修改 index.css 为 style.js
  • styled-components 的使用
    styled-components 样例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import {
    createGlobalStyle
    }
    from "styled-components";

    export const GlobalStyle=createGlobalStyle` body {
    margin: 0;
    padding: 0;
    font-family: sans-serif;
    background-color: green;
    color: white
    }

    `
    index.js 引入
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import { GlobalStyle } from './style.js';

    ReactDOM.render(
    <React.StrictMode>
    <App />
    <GlobalStyle />
    </React.StrictMode>,
    document.getElementById('root')
    );

让页面在所有浏览器统一

引入 Reset.css 可以使得不同浏览器对body属性等 统一起来,如行高,margin等的默认值。

分部分开发

header 组件

创建 common 文件夹, 其下创建 ./header/index.js

目前的文件结构

  • src
    • common
      • header
        • index.js
        • style.js
    • static
      • iconfont
        • iconfont.js
        • iconfont.eot
        • iconfont.svg
        • iconfont.tff
        • iconfont.woff
    • index.js
    • App.js
    • style.js

代码与实现效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { GlobalStyle } from './style.js';
import { IconStyle } from './static/iconfont/iconfont';

ReactDOM.render(
<React.StrictMode>
<App />
<GlobalStyle />
<IconStyle />
</React.StrictMode>,
document.getElementById('root')
);
1
2
3
4
5
6
7
8
9
10
11
import React, { Component } from 'react';
import Header from './common/header/index';


class App extends Component {
render() {
return <Header />;
}
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { createGlobalStyle } from 'styled-components';

// reset from https://meyerweb.com/eric/tools/css/reset/
export const GlobalStyle = createGlobalStyle`
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;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import React, { Component } from 'react';
import { CSSTransition } from 'react-transition-group';
import { HeaderWrapper, Logo, Nav, NavItem, NavSearch, Addition, Button, SearchWrapper } from './style';

class Header extends Component {
constructor(props) {
super(props);
this.state = {
focused: false
};
this.handleInputFocus=this.handleInputFocus.bind(this)
this.handleInputBlur=this.handleInputBlur.bind(this)
}
render() {
return (
<HeaderWrapper>
<Logo />
<Nav>
<NavItem className="left active">首页</NavItem>
<NavItem className="left">下载App</NavItem>
<NavItem className="right">登录</NavItem>
<NavItem className="right">
<span className="iconfont">&#xe636;</span>
</NavItem>
<SearchWrapper>
<CSSTransition
timeout={200}
in={this.state.focused}
classNames="slide" //slide-enter slide-enter-active
// slide-exit slide-exit-active
>
<NavSearch
className={this.state.focused ? 'focused':''}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
></NavSearch>
</CSSTransition>
<span className={this.state.focused ? 'focused iconfont':'iconfont'}>
&#xe637;
</span>
</SearchWrapper>
</Nav>
<Addition>
<Button className="writting">
<span className="iconfont">&#xe645;</span>
写文章
</Button>
<Button className="reg">注册</Button>
</Addition>
</HeaderWrapper>
);
}

handleInputFocus(){
this.setState({
focused:true
})
}

handleInputBlur(){
this.setState({
focused:false
})
}
}

export default Header;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import styled from 'styled-components';
import logoPic from '../../static/jianshulogo.png'; //导入图片

export const HeaderWrapper = styled.div`
height: 58px;
border-bottom: 1px solid #f0f0f0;
`;

export const Logo = styled.a.attrs({ href: '/' })`
position: absolute;
display: block;
top: 0;
left: 0;
height: 58px;
width: 100px;
background: url(${logoPic});
background-size: contain;

`;

export const Nav = styled.div`
margin: 0 auto;
width: 960px;
height: 100%;
box-sizing: border-box;
padding-right: 70px;
`;

export const NavItem = styled.div`
line-height: 58px; //竖向居中
color: #333;
font-size: 17px;
padding: 0 15px;
&.left {
// .不要忘记
float: left;
}
&.right {
float: right;
color: #969696;
}
&.active {
color: #ea6f5a;
}
`;

export const SearchWrapper = styled.div`
position: relative;
float: left;
.slide-enter {
transition: all .2s ease-out;
}
.slide-enter-active {
width: 240px;
}
.slide-exit {
transition: all .2s ease-out;
}
.slide-exit-active {
width: 160px;
}
.iconfont {
position: absolute;
right: 5px;
bottom: 5px;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
border-radius: 15px;
&.focused {
background: #999;
color: #fff;
}
}
`;

export const NavSearch = styled.input.attrs({
placeholder: '搜索'
})`
margin-top: 10px;
margin-left: 20px;
padding: 0 40px 0 20px;
box-sizing: border-box; // 不会因为padding把 width 变大
width: 160px;
height: 38px;
border: 0px;
outline: 10px;
border-radius: 19px;
background: #eee;
font-size: 14px;
&::placeholder{
color: #bbb;
}
&.focused{
width: 240px;
}

`;

export const Addition = styled.div`
position: absolute;
right: 0;
top: 0;
height: 58px;
`;

export const Button = styled.div`
float: right;
line-height: 38px;
margin-top: 10px;
margin-right: 20px;
padding: 0 20px;
border-radius: 19px;
border: 1px solid #ec6149;
font-size: 14px;
&.reg {
color: #ec6149;
}
&.writting {
color: #fff;
background: #ec6149;
}
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createGlobalStyle } from 'styled-components';

export const IconStyle = createGlobalStyle`
@font-face {
/* 添加相对路径./ */
font-family: "iconfont";
src: url('./iconfont.eot?t=1604649485094'); /* IE9 */
src: url('./iconfont.eot?t=1604649485094#iefix') format('embedded-opentype'),
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAP8AAsAAAAACEQAAAOwAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDFAqEDINMATYCJAMQCwoABCAFhG0HQRsvBxHVm13Jfh7YtvBs1rre93E0kj8j7XPwQO+VbzKz21xysGo5Wyg1/QUpABIb/X9zM+5ljYpQLMl53k/fkY5NyC+fuHfaFJ/PslzWHLT1UxdgNIECGmvQAgm4QLBh7MqLOA4BGESSjihTrkoddBTGOAGILh3bt0RPmVA1hQIdgbNkqUJMxYGujdcuAlP834vX5BI6aDgkxtQa7cq2ocSzfM/KSuu/1cQlIR7PAbBVIIF0QIFoUepqhAxT6UgMZT9FJuBEIdCe5X9W9v+MKfRNsM60Dmi9/jzhxMh3ByAPTEt4ls/mQoNn+UVl6rJQcvJHKQB0wA1YCdzB0IWP8xiF5pYYp+sBZqB7/1n2Btf6CWutJvXaz7HXWf5tZloBRWfZQamnugKLzZ7g7z9zwnpXUPENdkrddVZyn5nG6sOhc4Lqz1lj2gGzQiz/uTeD/WcFiA807VUBWHP9vYx67nWMzmbDRrMjjYXBDVoeMG0whHHUmHBZuOrXd/l3Fqm7cy3WOtM+IcTgekfuiNu36928WefWrbr2neJ1lxPodedO/aSgoPz27cZNx4X5hPvY3T8W2rmzSbNx4VrYvKBgzzDuIgycPTcuLOBD+eNEuQybG+eTz6dRI+cRF9u2dFua29CdBealeJh6qx/SITskNmovnC28J+bJVu6RYeijri7naLvy15LLXGvr+V+Vvt2pWPW6WqlWHYvdUb+L9e5zqkCWcJTM7J5Sr6NHU7NyiUJPmtXvcyEk4HdowseESApODdy9u1tqn1Jy3sWL86BcUrOcF/tL9Ul9Vt5tfUTdiD9rtEErVgzCcg075SCJBR4f9qYHAP+Tc2Wwz1welWTIlTLyLw+jebEKVZt7Ffmqm5da9+BCnAcK/Kdolws6GrQW/1NxywqlSg2zUrLMFmluRa3EMDTAwAD+ZTGAL4/H8ARA7BDoXAyhkYMISHTiyYqeDgcmeeBEpzgM0ii/2sSPXkiEcgNSGQ0g8GYLNDw5Aok3l8iK/ggOAnkLJ95CwqCe8NvSJFHAHUVpQsloQP0h6FatZWURFN7QvbOkpNRnvJDOEIVlnLPZK1akMWacH7cyW7DUCuzgNMy5QacWUfPomfs2TbbsTaNuZRCUJpSMBtQfgm7VurVZ5L5/Q/fOkmoIFd4X0hk6h2WcW5CvqtqKcC29z49bmS0eZakV2KEDs5lq0MsHRdQ8+h6Jvk2omm2rGufXlCfcBhgYVymhCSmUcODoT4HxUlKKdrI6I2NwjcMA')
format('woff2'),
url('./iconfont.woff?t=1604649485094') format('woff'), url('iconfont.ttf?t=1604649485094') format('truetype'),
url('./iconfont.svg?t=1604649485094#iconfont') format('svg'); /* iOS 4.1- */
}

.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
`;

header

把数据存入 redux

随着项目越来越大,数据的存取必定是一个瓶颈。一个项目如果从一开始就定位的足够大,那么数据层面采用 react-redux 是必经之路。况且,随着项目复杂度加深,组件化是显然的。所以,从一开始就使用 redux 管理数据才是明智之举。

redux-store 的使用流程

如果用redux,如果可以存入其中,就尽可能的存入其中。

  • 首先安装 redux, npm install redux --save ;为了方便在 react 中使用 redux,进一步安装 npm install react-redux --save
  • 要使用 redux, 需要通过 store -props-> state 流程从 store 中取得数据,所以先创建 store 实例
  • 创建 store;其中还包括管理手册-reducer
    1
    2
    3
    4
    5
    import { createStore } from 'redux';// 取得createStore方法
    import reducer from './reducer.js';

    const store = createStore(reducer);//创建store实例,并与reducer链接
    export default store;
  • reducer 为纯函数,唯一输入则唯一输出【state】。管理手册【reducer】包括默认表格【defaultState】及来自 store 的 action 两部分
    1
    2
    3
    4
    5
    const defaultState = {};

    export default (state=defaultState,action)=>{
    return state
    }
  • 在 App.js 内使用 redux,需要引入 store 实例。另外,为了方便使用,使用 react-redux 的 Provider 组件 => Provider 可以使 <Header /> 标签由能力使用 store 内的数据
    1
    2
    3
    4
    import {Provider} from 'react-redux'
    import store from './store'

    <Provider store={store}> <Header /> </Provider>
    • 除此以外,<Header /> 还需要与 store 进行连接
      1
      import {connect} from 'react-redux'
  • 改写 数据 及 函数
    • 改写 constructor 内 this.state 的默认数据至 reducer 的默认值
    • 改写 constructor 内 bind(this) 的绑定函数
  • 提高性能,Header.js 改至无状态组件
载入 redux 的调试

在 store.js 引入 redux-devtools-extension,方便调试

1
2
3
4
5
6
7
import { createStore,compose } from 'redux';//compose 可以一次传入多个函数
import reducer from './reducer.js';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(reducer,composeEnhancers());
export default store;
拆分 reducer

一个文件的代码 如果超过了300行,那就说明项目的架构出了问题。因此,需要将 reducer 进行拆分 后管理 store。一个思路是,给页面的每个部分创建单独的 store-reducer,然后在总的 reducer 汇总各个部分的 reducer。大致分为两个步骤。

  • 定义各个部分的 store 。以 header 举例,创建 reducer,用来存放 header 用到的 reducer
  • src目录下的 store ,在该 store 的 reducer 内注册各部分的 reducer,整合成一个reducer 时需要调用 redux-combineReducer 方法。
    1
    2
    3
    4
    5
    6
    import { combineReducers } from 'redux';
    import headerReducer from '../common/header/store/reducer';

    export default combineReducers({
    header: headerReducer
    });
集中定义 action

通常 action 仅用来传递数据对象;当使用 redux-thunk 时,也可传递函数,因此定义 action 要分两种情况,其中 仅传递数据对象 又分为含参数与不含参数两种情况。

  • 仅传递数据

    • 不含参数时,定义形如 const xxx=()=>({type:···}) 的函数,并导入定义 SEARCH_FOCUS 的常量文件
      1
      2
      3
      4
      5
      6
      import * as constants from './constants';

      //action为数据对象
      export const searchFocus=()=>({//searchFocus 为action
      type:'contants.SEARCH_FOCUS'
      })
      • constants 文件如何定义常量
        1
        export const SEARCH_FOCUS = 'header/seach_focus'
    • 含参数时,定义形如 const xxx=(yyy)=>({type:···}) 的函数
      1
      2
      3
      4
       const changeStoreList = (data)=> ({
      type: 'change_store_list',
      data: fromJS(data) // 保持数据类型统一
      });
  • 传递函数时,含参数时,定义形如 const xxx=()=>{type:···} 的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    export const getList = () => {
    return (dispatch) => {
    axios
    .get('/api/headerList.json')
    .then((res) => {
    const data = res.data;
    const action = changeStoreList(data.data);
    dispatch(action);
    })
    .catch(() => {
    console.log('error');
    });
    };
    };
index 出口管理

在 各部分的 store 下新建 index.js 用来管理 store 。

1
2
3
4
5
import * as actionCreators from './actionCreators';
import * as constants from './constants';
import reducer from './reducer';

export { reducer, actionCreators, constants };

其他文件引入时

1
2
// /index.js 可以省略,故./store/index.js 写作 ./store
import { actionCreators } from './store';
immutable.js 不可变更

使用 immutable.js fromJS 组件,可以使对象成为不可更改的对象。这对 state 十分重要。

  • 安装 immutable,npm install immutable --save
  • 使用 immutable 库
    1
    2
    3
    4
    5
    import {fromJS} from 'immutable'// obj js convert to immutable

    const defaultState = fromJS({
    focused: false
    });
  • 映射的数据获取需使用 get 方法
    1
    2
    3
    4
    5
    6
    7
      //store -> props 规则
    const mapStateToProps = (state) => {
    return {
    //immutable obj need to use get-method
    focused: state.header.get('focused')
    };
    };
  • 要修改 store-state 的数据则需要使用 set 方法
    1
    2
    3
    4
    if (action.type === 'constants.SEARCH_FOCUS') {
    //immutable set func 结合以前immutable对象的数据,返回一个全新的对象
    return state.set('focused', true);
    }
  • 拓展 immutable 性质至 根store
    • 借助 redux-immutable 库,npm install redux-immutable --save

ajax 数据异步请求

中间件 redux-thunk

获取 ajax 数据不会直接写在组件内,一般放在 action 内或 redux-saga内。其中,如果想在 action 内传递函数,需要借助 redux-thunk;则安装 npm install redux-thunk --save , 这便是要使用 action-中间件-store。即 action-中间件 和 中间件-store

  • action-中间件,须在创建 store 时使用 redux-thunk
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //在根目录 store 内
    import { createStore,compose ,applyMiddleware} from 'redux';
    //compose 可以一次传入多个函数或对象
    import thunk from 'redux-thunk'
    import reducer from './reducer.js';


    const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

    const store = createStore(reducer,composeEnhancers(
    applyMiddleware(thunk)
    ));
    export default store;
  • 中间件-store,须在创建 action 时使用 redux-thunk,即对应的reducer 内使用
  • 获取 ajax 数据
    ajax 异步请求,需要借助 axios,安装 npm install axios --save
循环显示在页面
  • 列表循环,需要用回调函数。
    1
    2
    3
    4
    5
    list.map((item)=>{
    return(
    <SearchInfoItem key={item}>{item}</SearchInfoItem>
    )
    })
代码优化
  • actionCreators 不需要暴露的 action 统一写在上面
  • 解构赋值让代码更精简
    1
    2
    3
    const { focused, list } = this.props;
    //这样之后,focused 等效于 this.props.focused
    // list 等效于 this.props.list
  • 多个同类型 if 语句写成 switch 语句
    1
    2
    3
    4
    5
    6
    7
    switch(···){
    case :
    ···
    break;
    default
    ···;
    }

换一换

在 header/reducer.js 的 defaultState 新增数据项 page totalPage

1
2
3
4
5
6
const defaultState = fromJS({
focused: false,
list: [],
page: 1,
totalPage: 1
});

ref 的使用

减少 ajax 请求

header 小结

  • react 是 面向数据 编程,最难的地方 reducer 里面的数据如何被设计
  • react-redux 的数据的改变具有 单向数据流 的特点,组件派发 action 给到 store, store 把 action 给到 reducer,reducer 把修改的数据返回 store,store 更新到 组件state,进而带动页面变化。

react 的路由

路由是什么?根据 url 不同,显示不同的页面就叫做路由。
安装 react-router-dom, npm install react-router-dom --save

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import Header from './common/header';
import store from './store';

class App extends Component {
render() {
return (
<Provider store={store}>
<div>
<Header />
<BrowserRouter>
<div>
<Route path="/" exact render={() => <div>Home</div>} />
<Route path="/detail" exact render={() => <div>detail</div>} />
</div>
</BrowserRouter>
</div>
</Provider>
);
}
}

路由管理

路由规则

  • src
    • pages
      • home
        • index.js
      • detail
        • index.js
    • App.js

具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import Header from './common/header';
import Home from './pages/home';
import Detail from './pages/detail';
import store from './store';

class App extends Component {
render() {
return (
<Provider store={store}>
<div>
<Header />
<BrowserRouter>
<div>
<Route path="/" exact component={Home} />
<Route path="/detail" component={Detail} />
</div>
</BrowserRouter>
</div>
</Provider>
);
}
}

export default App;
1
2
3
4
5
6
7
8
9
import React, { Component } from 'react';

class Home extends Component {
render() {
return <div>Home</div>;
}
}

export default Home;
1
2
3
4
5
6
7
8
9
import React, { Component } from 'react';

class Detail extends Component {
render() {
return <div>Detail</div>;
}
}

export default Detail;

主页组件拆分

数据与接口

定义 /api/homeData.json

{
success: true
data:{
topicList:[],
articleList:[],
recommendList:[],
authorList:[]

}
}

异步组件

使用 react-loadable 可以是 js 分开加载

1
npm install --save react-loadable

项目上线流程

删掉本地模拟的 api 数据,npm build 之后将 build 文件夹丢给后端即可。

最终效果

demo

评论