react重构简书
搭建项目
yarn create react-app jianshu --typescript
- 引入styled-componets
1). 将css文件改为js文件,设置全局样式(为了浏览器统一引入reset.css)
- style.js
import { createGlobalStyle } from 'styled-components'
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;
}
`
2). 在index.tsx中引入使用
import { GlobalStyle } from './style';
ReactDOM.render(
<Fragment>
<GlobalStyle/>
<App/>
</Fragment>
, document.getElementById('root'));
1.头部组件
- header/style.js
import styled from 'styled-components'
import logoPic from '../../static/logo.png'
export const HeaderWrapper = styled.div`
height: 58px;
border-bottom: 1px solid #f0f0f0;
position: relative;
`;
export const Logo = styled.a.attrs({
href: '/'
})`
position: absolute;
top: 0;
left: 0;
display: block;
width: 100px;
height: 56px;
background: url(${logoPic});
background-size: contain;
`;
export const Nav = styled.div`
width: 960px;
margin: 0 auto;
background: green;
`
- header/index.tsx
import * as React from "react";
import {HeaderWrapper, Logo, Nav} from "./style";
const Header: React.FC = () => {
return (
<HeaderWrapper>
<Logo/>
<Nav/>
</HeaderWrapper>
)
}
export default Header;
iconfont使用Unicode方式引入
1). 选好对应的图标然后下载Unicode文件到本地,只需要保留css和svg、ttf、woff格式的文件即可
2). 新建iconfont目录,将css改成js,iconfont文件都改成相对路径
import {createGlobalStyle} from "styled-components";
export const GlobalIcon = createGlobalStyle`
@font-face {font-family: "iconfont";
src: url('./iconfont.eot?t=1582708145133'); /* IE9 */
src: url('./iconfont.eot?t=1582708145133#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAPQAAsAAAAAB+wAAAOEAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDHAqDKIMHATYCJAMQCwoABCAFhG0HRhv3BhHVm/vIvjgwz2s65JBqdDaSLjXmzpRKRlF69zl4oPeyN5nZNk0K+0Bne6C0PYCf3i3demx0Gbhga03Y8F33SlLxfLCsXwDI2ff/APify+m1LIH5LctljvH8MMBobA1srEmBBFzAEnvD2AUt8TAEYBBJOqJi5ZoN0VGYowQgunXu2Bo9o0E15At0BC4VC5WbmIUNXZusXUNjZvB98RlH6KBhk5gT63ao1I6y7+LfFZajnKNo5iFwpzMBbBlIIB1QIDpVWtogo0g6EkPtrigM6OhoMFVbxrvCzhHvKBLd+ssDgUQD4YKZ6w5AQRSV8C7eoUPAuwxOlbQwzEED+CALdMAEhgMHMYnhvK1CIT1cXU1dD9jd3WvolEvJ/fLaDty6VzgcgxoKw25SQoxONDo2spe0yow+WeT8/ZhVXobl3tDoajZusiDSWBac1/qoORUMYZwyptwQo+320cHmJRvt28SozebUs0K0dTis/Hz7/fuN7t1rYYsahqz0T3E4Bk8JtBaM3roxoNH8xYvm+efGVYu/1D6jQMXc3IpZha1p8aJCs6axDRJmxxQoEDN7zdq8FQFll5w8uWdHrZrfduz4pr1cU7ZSJdWZUx2q3E6ueLu9pzOt4OhSuk4jrXybzqXz1Z/Sqt/5olnCVi6zZ4rV2aO5WaNs8Tct7P2uhgT8Cd3+TFhwc8h8eeG8nG8xPaarmJckzxs+RFu7VoPCIZgFNdoazERXB742PCb+tjg4dtSkPyy79a9R9ALASV6W1QLy8pQklnwkS/7HQaTtWZra0qsk/ecoeTTM2o3cToph3J/B7cz9a1YV1xPyNb6pVUrhOtK47j4U2Tc+TgMMDMAJHb48m0IWoOaCQCcgAQ1XIkCiE49RhHSwYVIQXNApAwZpVFlu4kcfJEK5AamMByDwZidoeHISJN5cxyjCU7ARyFdwwVtIMLCE34Ymib6o9AqMiWAO6R+qbOjJ1ose5t+weLcJp/kB6YWsKj/YhpWPX7FHnmKK+hSOCAHx0MEFHIdtO8DIQ42ZGKXIuDNNqnqRkQ2dplOBMRHMgfQPVNnQk1dc9MLnb1i824QbUFXGF7KqOgdsDKwOmKuk74S6lWfUp3AQIYB46IALMGFrRQZgrO5XYyYG5YDQuMNEhairxFhe1r3hGsDA7KWEJqRQwoZ57TsqFMt2w9k+mddKir6iClnTAA==') format('woff2'),
url('./iconfont.woff?t=1582708145133') format('woff'),
url('./iconfont.ttf?t=1582708145133') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('./iconfont.svg?t=1582708145133#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;
}
`
3). 在index.tsx中引入
import { GlobalIcon } from '../src/static/iconfont/iconfont'
<GlobalIcon/>
4). 直接使用
<i className={"iconfont"}></i>
使用redux和react-redux进行数据管理
- store/index.tsx
import { createStore } from 'redux'
import { reducer } from './reducer'
const store = createStore(reducer)
export default store;
- store/reduce.tsx
export interface State {
focused: boolean;
}
const defaultState = {
focused: false
}
export const reducer = (state: State = defaultState, action: {type: string}) => {
if (action.type === 'search_focus') {
return {
focused: true
}
}
if (action.type === 'search_blur') {
return {
focused: false
}
}
return state
}
- App.tsx
import Header from "./common/header";
+ import {Provider} from "react-redux";
+ import store from "./store";
function App() {
return (
<Provider store={store}>
<Header/>
</Provider>
);
}
- header.tsx
import { connect } from "react-redux";
import {State} from "../../store/reducer";
interface Prop {
focused: boolean;
onFocus: () => void;
onBlur: () => void;
}
const Header: React.FC<Prop> = (props) => {
return (
<SearchWrapper>
<CSSTransition in={props.focused} timeout={100} classNames={"slide"}>
<NavSearch className={props.focused ? 'focused' : ''}
onFocus={props.onFocus} onBlur={props.onBlur}
/>
</CSSTransition>
<i className={props.focused ? "focused iconfont" : 'iconfont'}></i>
</SearchWrapper>
)
}
const getPartialStore = (state: State) => {
return {
focused: state.focused
}
}
const actionCreator = (dispatch: (a: {type: string}) => void) => {
return {
onFocus: () => {
const action = {type: 'search_focus'}
dispatch(action)
},
onBlur: () => {
const action = {type: 'search_blur'}
dispatch(action)
}
}
}
export default connect(getPartialStore, actionCreator)(Header);
对reducer进行拆分
问题:因为每个模块都有自己的reducer如果都写在一个地方,就会不便于维护,所以我们要对每个模块分别建一个reducer,然后在根目录下的store里的reducer中通过combineReducers
api完成不同模块的整合
- header/store/reducer.tsx
export interface State {
focused: boolean;
}
const defaultState = {
focused: false
}
export const reducer = (state: State = defaultState, action: {type: string}) => {
if (action.type === 'search_focus') {
return {
focused: true
}
}
if (action.type === 'search_blur') {
return {
focused: false
}
}
return state
}
- header/store/index.tsx
import {reducer} from "./reducer";
export {reducer}
- store/reducer.tsx
import { combineReducers } from 'redux';
import { reducer as headerReducer } from "../common/header/store";
const reducer = combineReducers({
header: headerReducer
})
export default reducer;
这里我们的state就会变成state.header.focused了
对action进行封装
1). 创建actionCreator里面导出对应的action对象
export const searchFocus = () => ({
type: 'search_focus'
})
export const searchBlur = () => ({
type: 'search_blur'
})
- header/index.tsx
import * as actionCreator from './store/actionCreator'
const mapDispatchToProps = (dispatch: (a: {type: string}) => void) => {
return {
onFocus: () => {
dispatch(actionCreator.searchFocus())
},
onBlur: () => {
dispatch(actionCreator.searchBlur())
}
}
}
2). 创建constants文件来把所有的type类型定义为常量
- constants.tsx
export const SEARCH_FOCUS = 'header/SEARCH_FOCUS';
export const SEARCH_BLURS = 'header/SEARCH_BLURS'
- actionCreator.tsx
import * as constants from './constants'
export const searchFocus = () => ({
type: constants.SEARCH_FOCUS
})
export const searchBlur = () => ({
type: constants.SEARCH_BLURS
})
为了方便我们引用最好把每个文件下要导出的文件都放入index里导出
- header/store/index.tsx
import {reducer} from "./reducer";
import * as actionCreator from './actionCreator'
import * as constants from './constants'
export {reducer, actionCreator, constants}
使用immutable.js来管理store中的数据
为什么要使用immutable?
immutable是不可修改的数据,而我们reducer操作state的本质是每次要返回一个新的state,而不能对原始的state进行修改,否则就会出问题,所以为了避免我们修改原始的state我们就可以使用immutable,读取值的时候用get('字段名'),修改时用state.set('要修改的字段名', 新的值)
用法
yarn add immutable
const defaultState = fromJS({
focused: false
})
export const reducer = (state: State = defaultState, action: {type: string}) => {
if (action.type === constants.SEARCH_FOCUS) {
return state.set!('focused', true)
}
if (action.type === constants.SEARCH_BLURS) {
return state.set!('focused', false)
}
return state
}
- header/index.tsx
const getPartialStore = (state: {header: State}) => {
return {
focused: state.header.get!('focused')
}
}
使用redux-immutable统一数据格式
我们现在在子组件中获取state需要通过state.header.get!('focused')
,我们希望实现state.get('header').get('focused')
这样数据比较统一
1). 引入
2). 在根目录下的store里的reducer里
import { combineReducers } from 'redux-immutable';
import { reducer as headerReducer } from "../common/header/store";
const reducer = combineReducers({
header: headerReducer as any
})
export default reducer;
是用redux-thunk中间件配合axios获取数据
1). 在根目录下的store/index.tsx里引入
import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(
applyMiddleware(thunk)
))
export default store;
2). 在actionCreator里引入axios
import axios from 'axios'
import { fromJS } from "immutable";
const changeList = (data: object) => ({
type: constants.CHANGE_LIST,
data: fromJS(data)
})
export const getList = () => {
return (dispatch: (a: {type: string}) => void) => {
axios.get('/api/headerList.json').then((res => {
dispatch(changeList(res.data.data))
})).catch(error=> console.log(error))
}
}
3). 在constants里定义常量类型
export const CHANGE_LIST = 'header/CHANGE_LIST'
4). 在reducer里对action类型判断进行处理
const defaultState = fromJS({
+ list: []
})
if (action.type === constants.CHANGE_LIST) {
console.log(action, 'action')
return state.set!('list', action.data)
}
5). 在子组件中使用
const getPartialStore = (state: State) => {
return {
+ list: state.get!('header').get!('list')
}
}
dispatch(actionCreator.getList())
<SearchInfoList>
{props.list.map(item =>
<SearchInfoItem key={item}>{item}</SearchInfoItem>
)}
</SearchInfoList>
2. 使用react-router-dom实现路由跳转
1). 引入
yarn add react-router-dom
2). 使用
在APP.tsx里
import { BrowserRouter, Route } from 'react-router-dom'
function App() {
return (
<Provider store={store}>
<div>
<Header/>
<BrowserRouter>
<div>
<Route path={"/"} render={() => <div>home</div>}></Route>
<Route path={"/detail"} render={() => <div>detail</div>}></Route>
</div>
</BrowserRouter>
</div>
</Provider>
);
}
注意默认的路由匹配是不严谨的,只要你的路径里包含着路由里的元素就会显示,比如访问/detail也会显示/的内容,如果要严格匹配就要加上exact
<Route path={"/"} render={() => <div>home</div>} exact></Route>
我们可以以一个页面为一个父级组价,然后对当前页面的每部分进行组件拆分,拆分的当前页面组件的样式和数据的操作完全可以写在当前页面的组件里,不需要每个组件都单独写样式文件和数据文件
就想上面的home是一个单独的页面对它进行组件拆分,其他的组件的样式只需要在home的style.tsx里写就可以
登录
1). 登录成功后跳转到首页
只需在登录组件里判断store里的login是否是true,是的话渲染当前login否则用Redirect重定向组件跳到首页
import { Redirect} from 'react-router-dom'
const Login: React.FC<Prop> = (props) => {
let accountRef = useRef<HTMLInputElement>(null)
let passwordRef = useRef<HTMLInputElement>(null)
return (
<div>
{
!props.loginStatus ?
<LoginWrapper>
<LoginBox>
<Input placeholder={"账号"} ref={accountRef}/>
<Input placeholder={"密码"} ref={passwordRef}/>
<Button onClick={() => props.onClickLogin(accountRef.current!.value, passwordRef.current!.value)}>登录</Button>
</LoginBox>
</LoginWrapper>
:
<Redirect to={"/"}/>
}
</div>
)
}