STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

electron+hooks+ts实现互动直播大班课(三)

本篇文章概要:

  • MVC架构
  • 添加react hooks规范
  • 集成router
  • 集成redux
  • 为什么使用dva
  • 集成dva
  • 集成dva后测试
  • 集成node-sass
  • React.memo/React.StrictMode/React.FC

1.MVC架构

继续上一篇的文章,我们继续搭建react项目。
上一篇文章主要搭建开发生产环境的各个平台运行及打包(外部的),本篇文章主要搭建React项目本身(内部的)。

首先明确一个完整的前端项目,是需要有MVC模式架构的。前端项目工程化是项目的基础,它能极大提升开发效率,降低大型项目的开发难度。

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) // true
console.log(location === props.location) // true
console.log(params === props.match.params) // true
console.log(match === props.match) // true
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的单向数据流,是如何实现页面渲染的:

redux-flow

流程简介:
首先我们要有一个store,然后页面从 store 里面取数据,如果页面想改变 store 里面的数据,需走一个流程,首先是派发一个 action 给 store ,store 把 action 和之前的数据一起给到 reducer,reducer 结合这个 action 和之前的数据返回一个新的数据给到 store,store 更新自己的数据之后,告诉页面,我的数据被更新了,页面就会自动跟着联动。


4.5 react中props和state的区别

在react中,propsstate 都是经常用到的重要概念,它们的变化都会触发组件重新渲染。
区分 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

按照 React 官方指导意见, 如果多个 Component 之间要发生交互, 那么状态(即: 数据)就维护在这些 Component 的最小公约父节点上, 即 <App/>

<TodoList/> <Todo/> 以及 <AddTodoBtn/> 本身不维持任何 state, 完全由父节点 传入 props 以决定其展现, 是一个纯函数的存在形式, 即: Pure Component。


图解二: Redux 表示法
React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store

redux

与图一相比, 几个明显的改进点:

  1. 状态及页面逻辑从 <App/> 里面抽取出来, 成为独立的 store, 页面逻辑就是 reducer
  2. <TodoList/><AddTodoBtn/> 都是 Pure Component, 通过 connect 方法可以很方便地给它俩加一层 wrapper 从而建立起与 store 的联系: 可以通过 dispatch 向 store 注入 action, 促使 store 的状态进行变化, 同时又订阅 store 的状态变化, 一旦状态有变, 被 connect 的组件也随之刷新
  3. 使用 dispatch 往 store 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware, 实现各种自定义功能, 例如: logging
    这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好

图解三: 加入 Saga

saga

上面说了, 可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便, 做成一个 Middleware 就行, 这里使用 redux-saga 这个类库, 举个列子:

  1. 点击创建 Todo 的按钮, 发起一个 type == addTodo 的 action
  2. saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSucc 的 action, 提示创建成功, 反之则发送 type == addTodoFail 的 action

图解四:Dva 表示法

dva

有了前面的三步铺垫, Dva 的出现也就水到渠成, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:

  1. 把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面
  2. 增加了一个 Subscriptions, 用于收集其他来源的 action, 例如: 键盘操作
  3. 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';

//2.Plugins
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';

absoulte


优化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';

//1.Initialize
const app = dva();

//2.Plugins
app.use(createLoading());

//3.Model
require('./models/index').default.forEach((item: any) => {
app.model(item.default);
});

//4.Router
app.router(() => (
<App />
));

//5.Start
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 >
)
}
// @ts-ignore
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并渲染。
效果如下:

dva_loading


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技术栈)