本篇文章概要:
- MVC架构
- 添加react hooks规范
- 集成router
- 集成redux
- 为什么使用dva
- 集成dva
- 集成dva后测试
- 集成node-sass
- React.memo/React.StrictMode/React.FC
1.MVC架构
继续上一篇的文章,我们继续搭建react项目。
上一篇文章主要搭建开发生产环境的各个平台运行及打包
(外部的),本篇文章主要搭建React项目本身
(内部的)。
首先明确一个完整的前端项目,是需要有MVC模式架构
的。前端项目工程化
是项目的基础,它能极大提升开发效率,降低大型项目的开发难度。
回顾一下MVC模式,MVC是三个单词的首字母缩写,它们是Model(模型)、View(视图)和Controller(控制)。
这个模式认为,程序不论简单或复杂,从结构上看,都可以分成三层。
- 1.最上面的一层,是直接面向最终用户的”视图层”(View)。它是提供给用户的操作界面,是程序的外壳。
- 2.最底下的一层,是核心的”数据层”(Model),也就是程序需要操作的数据或信息。
- 3.中间的一层,就是”控制层”(Controller),它负责根据用户从”视图层”输入的指令,选取”数据层”中的数据,然后对其进行相应的操作,产生最终结果。
注意:React
其实只是处理View层
的JS库。
React注重UI效率,使用Virtual DOM Diff来提高效率,最小化Html DOM渲染开销。做的事情足够简明单一,所以不支持有MVVM模式的框架(像Vue)的双向绑定概念等。
目前搭建的项目里并没有处理Model和Controller层
的JS库。因此,我们还需要额外加一些其他库,比如去处理Model、Controller层
的JS库,支持路由跳转
的JS库、工具类
的JS库、校验代码规范
的JS库等。
2.添加react hooks规范
1
| npm install eslint-plugin-react-hooks --save-dev
|
在package.json中,新增配置项:
1 2 3 4 5 6 7 8 9
| "eslintConfig": { "extends": "react-app", "plugins": [ "react-hooks" ], "rules": { "react-hooks/rules-of-hooks": "error" } },
|
3.集成router
1 2
| npm install react-router-dom --save-dev npm install @types/react-router-dom --save-dev
|
将App.tsx替换成:
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
| import React from 'react'; import { HashRouter, Switch, Route } from "react-router-dom"; import './index.css';
import Home from './home'; import PageNotFound from './home/404'; import Login from './test/Login'; import Register from './test/Register'; import Detail from './test/Detail';
function App() { return ( <HashRouter> <Switch> <Route exact path='/' component={Home} /> <Route path='/login' component={Login} /> <Route path='/register' component={Register} /> <Route path='/detail/:detailId' component={Detail} /> <Route path="*" component={PageNotFound} /> </Switch> </HashRouter> ); }
export default App;
|
同时react-router-dom也提供几个hooks的API:
useHistory、useLocation、useParams、useRouteMatch
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { useHistory, useLocation, useParams, useRouteMatch, RouteComponentProps } from 'react-router-dom'; function Detail(props: RouteComponentProps) { const history = useHistory(); const location = useLocation(); const params = useParams(); const match = useRouteMatch(); console.log(history === props.history) console.log(location === props.location) console.log(params === props.match.params) console.log(match === props.match) return ( < div className="App" >...</div > ) }
|
由此可见上面几个hooks,和props里面的history、location、match、params
完全一致。
能够获取到路由里面的所有信息,及实现路由跳转。
1 2 3 4 5 6
| const history = useHistory(); history.push() history.replace()
history.go() history.back()
|
4.集成redux
4.1 为什么要使用redux?
在MVC架构模式
的前端项目中,react
是处理View层的库,负责页面渲染的
。
除开V之外的,MC呢?这就是使用redeux的目的。
4.2 什么是redux?
与 react 比较,就能理解它 redux 是处理Model、Controller层的库,负责页面逻辑的
。
公式如下:
1
| 页面渲染react + 页面逻辑redux + 页面路由router = 完整react应用
|
页面渲染、逻辑、路由基本构成了一个完整的react项目。
4.3 redux中基本概念?
在集成 redux 前,再理解一下核心概念。
store
:存放 state 的总对象
action
:改变 state 的对象
dispatch
:触发 action 的函数
reducer
:更新 state 的函数
state
:引起页面更新的数据
4.4 redux单向数据流
讲完基本概念,咱们来看图理解redux的单向数据流
,是如何实现页面渲染
的:
流程简介:
首先我们要有一个store,然后页面从 store 里面取数据,如果页面想改变 store 里面的数据,需走一个流程,首先是派发一个 action 给 store ,store 把 action 和之前的数据一起给到 reducer,reducer 结合这个 action 和之前的数据返回一个新的数据给到 store,store 更新自己的数据之后,告诉页面,我的数据被更新了,页面就会自动跟着联动。
4.5 react中props和state的区别
在react中,props
和 state
都是经常用到的重要概念,它们的变化都会触发组件重新渲染。
区分 state 和 props 的关键是,控制权是在组件自身,还是由其父组件来控制的
。
4.6.不要再问hooks能否取代redux
hooks 和 redux 并没有试图解决同样的问题
。
redux 是一个状态管理库
,hooks 是 react 更新的部分特性,让你的函数组件可以做类组件能做的事情
,两者一起使用并不冲突。
恰恰相反,这两项技术可以很好地互补。react hooks 不会替代 redux,它们仅仅为你提供了新的、更好的方式去组织你的 react 应用。如果你最终决定使用 redux 来管理状态,可以让你编写更好的组件。
5.为什么使用dva
想直接使用redux也行,但是上手难度还是有的,因为还有更多概念需要理解及掌握。
比如:createStore, applyMiddleware, combineReducers, compose, Provider, Middleware, connent, mapStateToProps, mapDispatchToProps…
因此我们可以使用redux的上级封装框架,比如:dva。
四张图来图解DVA的产生:
示例背景:TodoList = Todo list + Add todo button
图解一: React 表示法
按照 React 官方指导意见, 如果多个 Component 之间要发生交互, 那么状态(即: 数据)就维护在这些 Component 的最小公约父节点上, 即 <App/>
。
<TodoList/> <Todo/>
以及 <AddTodoBtn/>
本身不维持任何 state, 完全由父节点 传入 props 以决定其展现, 是一个纯函数的存在形式, 即: Pure Component。
图解二: Redux 表示法
React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store
与图一相比, 几个明显的改进点:
- 状态及页面逻辑从
<App/>
里面抽取出来, 成为独立的 store, 页面逻辑就是 reducer
<TodoList/>
及 <AddTodoBtn/>
都是 Pure Component, 通过 connect 方法可以很方便地给它俩加一层 wrapper 从而建立起与 store 的联系: 可以通过 dispatch 向 store 注入 action, 促使 store 的状态进行变化, 同时又订阅 store 的状态变化, 一旦状态有变, 被 connect 的组件也随之刷新
- 使用 dispatch 往 store 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware, 实现各种自定义功能, 例如: logging
这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好
图解三: 加入 Saga
上面说了, 可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便, 做成一个 Middleware 就行, 这里使用 redux-saga 这个类库, 举个列子:
- 点击创建 Todo 的按钮, 发起一个 type == addTodo 的 action
- saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSucc 的 action, 提示创建成功, 反之则发送 type == addTodoFail 的 action
图解四:Dva 表示法
有了前面的三步铺垫, Dva 的出现也就水到渠成, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:
- 把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面
- 增加了一个 Subscriptions, 用于收集其他来源的 action, 例如: 键盘操作
- model 写法很简约, coding 快得飞起
6.集成dva
介绍完dva,下面来说下实现:
网上很多都是安装dva-cli脚手架,然后再用 dva-quickstart。
然而我们用的是create-react-app脚手架,想用dva的数据流方案,只需要安装dva:
1
| npm install dva --save-dev
|
集成dva:
将src/index.tsx改成如下,ReactDOM.render注释掉:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| //1.Initialize const app = dva();
//2.Plugins //app.use({});
//3.Model app.model(require('./models/index').default);
//4.Router app.router(() => ( <App /> ));
//5.Start app.start('#root');
// ReactDOM.render( // <React.StrictMode> // <App /> // </React.StrictMode>, // document.getElementById('root') // );
|
优化1:支持异步监听
:
dva-loading支持全局监听异步操作,不用一遍遍地写 showLoading 和 hideLoading。
1
| npm install dva-loading --save-dev
|
在react-app-env.d.ts中,声明dva-loading:
1 2
| /// <reference types="react-scripts" /> declare module 'dva-loading';
|
引入dva-loading插件,在src/index.tsx注释的第二步放开:
1 2 3 4
| import createLoading from 'dva-loading';
app.use(createLoading());
|
优化2:引用使用绝对路径
在引入组件或插件的时候,经常出现 ../../../../地狱模式
,想直接使用绝对路径该怎么办呢?
根据官方文档的解释,在你项目的根目录tsconfig.json中配置:
1 2 3 4 5 6
| { "compilerOptions": { "baseUrl": "src" }, "include": ["src"] }
|
引用的地方,都可以替换:
1 2 3 4 5
| import logo from './../../logo.svg';
import logo from 'logo.svg';
|
优化3:循环引入多model
app.model()只能进入一个model,如果要挂载多个model:
1 2 3 4
| app.model(require('./models/index').default); app.model(require('./models/login').default); app.model(require('./models/test').default); ...
|
聪明的做法是:循环models目录下的所有js文件,即所有的model,然后循环require:
第一步,在models/index.js中:
1 2 3 4 5
| const context = require.context("./", false, /\.js$/); export default context .keys() .filter(item => item !== "./index.js") .map(item => context(item))
|
第二步,在src/index.tsx中更改第三步的操作:
1 2 3 4
| //3.Model require('./models/index').default.forEach((item: any) => { app.model(item.default); });
|
优化后的src/index.tsx:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import React from 'react'; import './index.css'; import dva from 'dva'; import App from './pages'; import createLoading from 'dva-loading';
const app = dva();
app.use(createLoading());
require('./models/index').default.forEach((item: any) => { app.model(item.default); });
app.router(() => ( <App /> ));
app.start('#root');
|
7.集成dva后测试
新增页面Login.tsx:
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
| import React from 'react'; import logo from 'logo.svg'; import { connect } from 'dva';
function Login(props: any) { const change = (type: string) => { props.dispatch({ type: `login/${type}` }) } return ( < div className="App" > <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> {props.loading ? <p>Loading</p> : <p>{props.current} Page</p>} <p>{process.env.REACT_APP_ENV}</p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> <div className="App-operate"> <button className="App-btn" disabled={props.loading} onClick={() => change('addASync')}>Add</button> <button className="App-btn" onClick={() => change('minus')}>Minus</button> </div> </header> </div > ) }
export default connect(({ login, loading }) => ({ current: login.current, loading: loading.effects['login/addASync'] }))(Login)
|
新增Mode login.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 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
| export default { namespace: 'login', state: { record: 0, current: 100, }, reducers: { add(state) { return { ...state, current: ++state.current } }, minus(state) { return { ...state, current: --state.current } }, save(state, payload) { return { ...state, ...payload, } }, }, effects: { *addASync(_, { call, put, select }) { const delay = (timeout) => new Promise((resolve) => { setTimeout(resolve, timeout); }) yield call(delay, 1000); const curr = yield select(state => state.login) yield put({ type: 'save', payload: { current: ++curr.current } }); },
*minusASync(_, { call, put, select }) { const delay = (timeout) => new Promise((resolve) => { setTimeout(resolve, timeout); }) yield call(delay, 1000); const curr = yield select(state => state.login) yield put({ type: 'save', payload: { current: --curr.current } }); }, }, subscriptions: {}, };
|
点击 add 按钮,模仿异步请求,会等待1s后,数量+1并渲染。
点击 miuns 按钮,模仿同步操作,数量-1并渲染。
效果如下:
8.集成node-sass
使用sass编写css样式,安装之后就可以使用Sass了:
1
| npm install node-sass --save-dev
|
将src/index.css替换成src/index.scss:
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
| .App { text-align: center; .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; .App-logo { height: 40vmin; pointer-events: none; } .App-link { color: #61dafb; } .App-operate { margin-top: 40px; display: flex; width: 180px; justify-content: space-between; .App-btn { padding: 5px 10px; background-color: #FFF; border: 0; cursor: pointer; border-radius: 3px; font-size: 20px; } } } }
|
9.React.memo/React.StrictMode/React.FC
1.React.memo
:
React.memo()是一个高阶函数,它与 React.PureComponent
类似。前者是函数组件而后者是类。
函数组件用法:
1 2 3 4 5 6 7 8
| const DeviceTest = () => { return ( <div> <SettingCard/> </div> ) } export default React.memo(DeviceTest)
|
类组件用法:
1 2 3 4 5 6 7 8 9
| export default class DeivceTest extends PureComponent { render() { return ( <div> <SettingCard/> </div> ) } }
|
2.React.StrictMode
:
StrictMode
是一个用以标记出应用中潜在问题的工具。就像 Fragment
,StrictMode 不会渲染任何真实的UI。它为其后代元素触发额外的检查和警告
。
注意: 严格模式检查只在开发模式下运行,不会与生产模式冲突。
StrictMode目前有助于:
- 识别不安全的生命周期
- 关于使用过时字符串 ref API 的警告
- 关于使用废弃的 findDOMNode 方法的警告
- 检测意外的副作用
- 检测过时的 context API
3.React.FC
由于 hooks 的加入,函数式组件也可以使用 state,新的 react 声明文件里,也定义了 React.FC 类型。
React.FC<{}>,FC是Function Components
的缩写,表示声明的是函数组件
。<{}>是泛型,强制类型用的。
用Login.tsx举例:
声明函数组件方式一:function Name(){ }
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
| function Login(props: LoginProps) { const change = (type: string) => { props.dispatch({ type: `login/${type}` }) } return ( <div className="App" > <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> {props.loading ? <p>Loading</p> : <p>{props.current} Page</p>} <p>{process.env.REACT_APP_ENV}</p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> <div className="App-operate"> <button className="App-btn" disabled={props.loading} onClick={() => change('addASync')}>Add</button> <button className="App-btn" onClick={() => change('minus')}>Minus</button> </div> </header> </div > ) }
|
声明函数组件方式二:const Name = () => { }
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
| const Login = (props: LoginProps) => { const change = (type: string) => { props.dispatch({ type: `login/${type}` }) } return ( <div className="App" > <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> {props.loading ? <p>Loading</p> : <p>{props.current} Page</p>} <p>{process.env.REACT_APP_ENV}</p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> <div className="App-operate"> <button className="App-btn" disabled={props.loading} onClick={() => change('addASync')}>Add</button> <button className="App-btn" onClick={() => change('minus')}>Minus</button> </div> </header> </div > ) }
|
声明函数组件方式三:const Name: React.FC<> = () => { }
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
| const Login: React.FC<LoginProps> = ({ dispatch, loading, current }) => { const change = (type: string) => { dispatch({ type: `login/${type}` }) } return ( <div className="App" > <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> {loading ? <p>Loading</p> : <p>{current} Page</p>} <p>{process.env.REACT_APP_ENV}</p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> <div className="App-operate"> <button className="App-btn" disabled={loading} onClick={() => change('addASync')}>Add</button> <button className="App-btn" onClick={() => change('minus')}>Minus</button> </div> </header> </div > ) }
|
方式三和前两种对比观察后,发现在使用props传入的值或dispatch的地方,特别的方便,无需重复编写props.xxx
,也无需在用到值的函数中声明const { dispatch, xxx } = this.props
。前面解构出参数,后面直接使用即可,因此推荐第三种方式来声明函数组件。
本篇先介绍到这里。下一篇将继续搭建electron+hooks+ts项目,尽情期待。(本篇重点react技术栈,下篇重点react的hooks、ts技术栈)