实例讲解基于 React+Redux 的前端开发流程

NOTE:从对象的包含关系上讲,reducer 是store的一部分,但在逻辑上我们把它分出来,这样会比较容易理解整个redux流程。

我们可以做个形象的比喻,把 js 比喻成巴士,把 store, container, reducer 比喻为三个车站,再把 state 和 action 比喻成两种乘客。这是一趟环路巴士:

  1. js巴士 从 store车站 出发,载上 state乘客 ,state乘客 到达某个 container车站 下车并把自己展示出来

  2. 过了一会,有一个 action乘客 上车了,js巴士 把 action乘客 送到 reducer车站,在这里 action乘客 和 state乘客 生了一个孩子 new state,js巴士把 new state 送回了 store车站(好像是人生轮回→_→)

redux 只是定义了应用的数据流程,只解决了 “数据层”(model layer) 的问题,一般还会使用 react, angular 等作为“显示层” (UI layer) 来一起使用,我们项目采用 react 作为显示框架。

在开始之前,这里先提供一些介绍react和redux的参考资料,如果在下文遇到哪些点不理解,可以来这里翻看参考资料:

下文的展示的js代码,会用到少量简单的 es6 语法,可以在遇到时参考这里,或自己查找资料:

我们会使用 babel 编译器把es6语法编译成es5, 所以大家不必担心浏览器兼容性问题,可以大胆使用es6。

应用的基础配置工作由前端开发主管负责,大家不必详细理解。

按任务分工来分步讲解


按照开发的内容,我们把前端团队分为两个小组: “布局组” 和 “逻辑组”,每个小组有2种任务,一共4种任务。

  • 布局组 - 负责 contianer、component 部分

  • 任务1:静态布局 - 使用 HTML + CSS 静态布局

  • 任务2:动态布局 - 使用 JSX 语法对静态布局做动态渲染处理

  • 逻辑组 - 负责 action、reducer 部分

  • 任务1:action 开发 - 制作 redux 流程的 action

  • 任务2:reducer 开发 - 制作 redux 流程的 reducer

布局组 要求对 HTML + CSS 布局比较熟悉,只需要会简单的 js 即可, 不需要完整地理解redux流程。

逻辑组 要求对 js 比较熟悉,最好可以比较完整地理解redux流程, 但基本不需要涉及HTML + CSS布局工作。

接下来,先给出我们教程相关的 src 目录。这里大家可以先一扫而过,大概了解即可

  • src 源码文件夹:包含项目源码,我们基本都在这个文件夹下做开发

  • containers 容器文件夹:存放容器组件,比如 “苹果篮子”

  • components 组件文件夹:存放普通显示组件,比如 “苹果”

  • actions actions文件夹:存放可以发出的action

  • reducers reducers文件夹:存放action的处理器reducers

  • services 服务文件夹:存放经过封装的服务,如 ajax服务, 模拟数据服务

  • styles 样式文件夹:存放应用的样式,如css, scss

  • images 图片文件夹:存放图片资源

  • apis 开发接口文件夹:存放开发接口文档

下面正式开始啦,先从布局组开始。

布局组


任务1:静态布局

  • 能力要求:只需要会使用 HTML + CSS (SASS)进行布局即可

  • 任务内容:1. 苹果篮子组件(容器组件) 2. 水果组件(显示组件)

redux 的组件分为两类,一类是容器组件 container ,一类是普通显示组件 component。容器负责接收store中的state和并发送action, 大多数时候需要和store直接连接,容器一般不需要多次使用,比如我们这个应用的苹果篮子。普通组件放在容器里面,由容器确定显示的时机、数量和内容,普通组件一般会多次使用。

1. 苹果篮子容器 AppleBasket.jsx + appleBasket.scss

苹果篮子组件的原型如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于容器,我们使用 React 组件类 的方式书写,这里主要是静态的jsx代码,相当于html。

下面是 AppleBasket.jsx

import React from ‘react’;

import { connect } from ‘react-redux’;

class AppleBusket extends React.Component {

render(){

return (

苹果篮子
当前
0个苹果,0克
已吃掉
2个苹果,480克
苹果篮子空空如也

摘苹果

);

}

}

export default connect()(AppleBusket);

同时静态布局开发人员还要负责书写样式,宅印的样式使用sass 书写, 下面是的例子是appleBasket.scss , 大家可以做参考:

.appleBusket{ width: 400px; margin: 20px; border-radius: 4px; border: 1px solid #ddd; >.title{ padding: 6px 0px; text-align: center; color: #069; font-size: 20px; //font-weight: 600; } >.stats{ width: 100%; $border: 1px dashed #ddd; border-top: $border; border-bottom: $border; padding: 10px 0px; .section{ display: inline-block; width: 50%; padding-left: 8px; .head{ padding: 6px 0px; font-size: 16px; color: #069; } .content{ font-size: 20px; font-weight: 200; } &:first-of-type{ border-right: 1px solid #f0f0f0; } } } >.appleList{ padding: 10px 0px; .empty-tip{ text-align: center; font-size: 16px; color: #ccc; padding: 20px 0px; } } >.btn-div{ text-align: center; button{ color: #fff; background-color: #096; border: none; font-size: 14px; padding: 6px 40px; border-radius: 6px; margin: 20px auto; } } }

2. 苹果组件 AppleItem.jsx + appleItem.scss

苹果组件的样子如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

普通组件的定义方法和容器类似,只是其不需要使用redux连接器来封装,如下:

AppleItem.jsx

import React from ‘react’;

class AppleItem extends React.Component {

render() {

return (

红苹果 - 1号
265克
吃掉

);

}

}

export default AppleItem;

样式文件 appleItem.scss 在此省略。

哪些显示元素要作为容器,哪些要作为普通组件,没有百分之百确定划分规则,大家根据自己的理解把它划分到某一类即可。

这些就是任务一的内容,书写静态布局,基本都是html+css工作,不需要涉及js代码。

任务2:动态渲染

写完了静态布局后,接下来要进行动态渲染啦~

动态渲染听起来很高大上,其实意思就是根据一个stete数据对象来显示内容而已。首先需要确定其state的结构。容器的state 是 store 中state的一部分,前端管理员会事先约定好其数据结构,并在对应的reducer中给出,只要去那里复制一份出来即可。普通组件的state只要自己定义即可,并在文件中说明清楚。

1. 容器的动态渲染

下面看看“苹果篮子”的动态布局,我们去 appleBasketReducer.js 可以得到水果篮子的 state 的结构如下:

{ isPicking : false, newAppleId: 1, apples: [ /{ id: 0, weight: 235, isEaten: false }/ ] }

我们这个数据结构把它 “实例化”,如下这样放在我们改成写的 AppleBasket.jsx 中,然后我们开始书写我们的动态渲染代码啦,如下:

import React from ‘react’;

import { connect } from ‘react-redux’;

import AppleItem from ‘…/components/AppleItem’;

class AppleBusket extends React.Component {

render() {

let { state } = this.props;

//这部分从对应的 appleBasketReducer.js 中拷贝

let mockState = {

isPicking : false,

newAppleId: 3,

apples: [

{

id: 1,

weight: 235,

isEaten: true

},

{

id: 2,

weight: 256,

isEaten: false

}

]

};

//是否开启模拟数据的开关,注释这行代码关闭模拟数据

state = mockState;

//对 state 做显示级别的转化

let stats = {

appleNow: {

quantity: 0,

weight: 0

},

appleEaten: {

quantity: 0,

weight: 0

}

};

state.apples.map(apple => {

let selector = apple.isEaten ? ‘appleEaten’:‘appleNow’;

stats[selector].quantity ++;

stats[selector].weight += apple.weight;

})

return (

苹果篮子
当前

{stats.appleNow.quantity}个苹果,

{stats.appleNow.weight}克

已吃掉

{stats.appleEaten.quantity}个苹果,

{stats.appleEaten.weight}克

{ state.apples.map(apple => ) }

摘苹果

);

}

}

function select(state) {

return {

state: state.appleBusket

}

}

export default connect(select)(AppleBusket);

可见,动态布局的工作要求只是在 HTML + CSS 布局的基础上,再加上 JSX 语法能力即可。

2. 普通显示组件的动态渲染

普通显示组件的动态渲染和容器类似,只是这里的state可以自己规定,并给出示例的mockState(模拟state),使用组件的人按照示例传入数据即可使用。

AppleItem.jsx 的更新如下:

import React from 'react'; class AppleItem extends React.Component { shouldComponentUpdate(nextProps) { return nextProps.state != this.props.state; } render() { let { state, actions } = this.props; /** * 这个区域是 mock 数据区,也作为组件文档,请书写清楚 * //在组件发布时,请注释掉,提高性能 */ let mockState = { id: 1, weight: 256, isEaten: false }; let mockActions = { eatApple : id => console.log('eatApple',id) }; /** * 开关这行代码,用于切换装入的数据来源。(为了开关的方便,请把两句代码合成一行) * 在开发阶段打开,使用内部 state 和 action, 开发完成后请注释关闭 */ state = mockState; actions = mockActions; if (state.isEaten) return null; return ( <div className="appleItem"> <div className="apple"><img src="../images/apple.png" alt=""/></div> <div className="info"> <div className="name">红苹果 - {state.id}号</div> <div className="weight">{state.weight}克</div> </div> <div className="btn-div"><button onClick={() => actions.eatApple(state.id) }>吃掉</button></div> </div> ); } } export default AppleItem;

容器和普通显示组件state的对比:

  1. 容器的state我们是从store中的总state直接获得的,注意 AppleBusket.jsx 靠后面这段代码:

function select(state) {

return {

state: state.appleBusket

}

}

select是一个state筛选器, 功能是选择总state中的 appleBusket 作为本容器的state。而这个state的格式我们会在其对应的reducer中规定(因为我们需要在reducer中提供对应state的默认值)

  1. 普通显示组件的state格式由组件开发人员自己约定即可,并在mockState 区域给出例子。当别人要使用你的显示组件时,必须根据你规定的格式传入state数据。在组件里面,我们一般会实现如下这样一个自动切换器,当组件的使用者在使用该组件时没有传入state, 就会显示内部的模拟state,为使用者带来预览效果。

if(state === undefined) state = mockState;

以上就是布局组的开发工作: 静态布局 + 动态布局。前者只需要会html+css布局即可,后者还要会一些jsx的语法和基本的js语法。

逻辑组


任务1:action 开发

任务内容:开发 redux 流程的 action,并把action部署到对应容器和组件中。

技能要求:需要对js比较熟悉,并要求要会使用jq的ajax功能。

首先,我们先来看看 action 的定义。

1. 狭义的 action

狭义的action是指一个简单的对象,对象的结构如下,只要在对象内包含type属性指明action的名称即可,同时,在对象的其他属性里,可以以任何形式自由保存其他相关数据。

let action = {

type: ‘ACTION_NAME’,

}

一般 type 的内容使用 大写字母+下划线 的格式。

在这个定义的基础上,业界提出以一种标准 action, 叫做 Flux Standard Action , 该标准下的action除了type属性之外,只允许多加(不是必须包含)这三个属性:payload,error,meta。

let FSA = {

type: ‘ACTION_NAME’,

payload: <bool | number | string | object>, //action的负载,可以是数据或 error 对象

error: , // 指明该action是否是一个以 error 为负载的action

meta: // action元数据, 包含解释该action含义的信息

}

我们宅印约定都要使用 Flux Standard Action,下面是吃苹果 action:

let FSA = {

type: ‘EAT_APPLE’,

payload: 3, // 负载是3, 说明吃掉3号苹果, 这里也可以写成 { id : 3 }

meta: ‘This action will eat an apple!’ // (不是必须的)

}

一个action只是一个对象,他需要根据时机被 store 的 dispatch 函数调用才会开始进行处理:store.dispatch(action_1)

2. 广义的 action

广义的 action 是指在中间件的支持下,dispatch 函数可以调用的数据类型,除了普通action之外,常见的有 thunk, promise 等。我们用常用的 thunk来举个例子。

thunk 其实就是一个代码片段,可以简单理解为一种特定的函数,我们可以dispatch 这个thunk。 thunk函数具有如下的签名

(dispatch, getState) => { //在函数体内可以使用 dispatch 方法来发射其他 action //在函数体内可以使用 getState 方法来获取当前的state }

下面是一个我们摘苹果动作的例子:

let pickAppleAction = (dispatch, getState) => { ajax({ url: '/pickApple', method: 'GET', }).done(data => { //发射普通 action dispatch({ type: 'DONE_PICK_APPLE', payload: data.weight // 或者 payload: {weight: data.weight} }); }).fail(xhr => { //发射普通 action, 其负载是一个error dispatch({ type: 'FAIL_PICK_APPLE', payload: new Error(xhr.responseText) , error: true }); }) }

定义好之后,我们可以直接这样调用这个thunk:

dispatch( pickAppleAction )

接下来,我们来做进一步优化,把action统一为actionCreator , 我们不难察觉,每次都要书写{ type: ‘ACTION_NAME’ … } 是很麻烦也很容易出错的,actionCreator 就是为解决这个问题而生的,actionCreator 就是一个产生特定action(这里指广义的action)的函数,下面来看简单的actionCreator 例子:

//传统写法 var eatApple = function(id) { return { type: 'EAT_APPLE', payload: id } } // es6 写法 let eatApple = id => ({ type: 'EAT_APPLE', payload: id })

这样一来,一方面是使用起来比较简单方便,另一方面是具有文档作用。

只需要这样发射action就可以啦:

dispatch(eatApple(3))

普通action的actionCreator封装工作, 可以使用 redux-actions 自动完成, 查看其文档就可以快速上手,可以根据情况使用。

在项目中,我们会为每个板块写一个的action文件,并统一使用actionCreator, 所以最终 appleAction.js 如下:

import ajax from ‘…/services/ajax’; //经过封装的加强型 ajax 函数 //这是名空间,对普通action做划分 const prefix = ‘apple/’; let actions = { //注意这里需要 () => … , 不然 pickAppleAction 不是一个actionCreator, 而是一个thunk pickApple: () => (dispatch, getState) => { //如果正在摘苹果,则结束这个thunk, 不执行摘苹果 if(getState().isPicking) return; //通知开始摘苹果 dispatch(actions.beginPickApple()); //发送摘苹果请求 ajax({ url: ‘/appleBasket/pickApple’, method: ‘GET’ }).done(data => { dispatch(actions.donePickApple(data.weight)) }) .fail(xhr => { dispatch(actions.failPickApple(xhr.responseText)); }) }, beginPickApple: () => ({ type: ‘apple/BEGIN_PICK_APPLE’ }), donePickApple: appleWeight => ({ type: ‘apple/DONE_PICK_APPLE’, payload: appleWeight }), failPickApple: errMsg => ({ type: ‘apple/FAIL_PICK_APPLE’, payload: new Error(errMsg), error: true }), eatApple: appleId => ({ type: ‘apple/EAT_APPLE’, payload: appleId }) }; export default actions;

这样一来,只要引入  appleAction.js ,就可以快速使用定义好的action,结合某些编辑器的智能提示功能,非常方便,下面是 vsCode 编辑器的效果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

写好了action之后,只要在 container 的对应位置装上action就好了, 下面是appleBasket.jsx 总体代码:

import React from ‘react’;

import { connect } from ‘react-redux’;

import AppleItem from ‘…/components/AppleItem’;

import actions from ‘…/actions/appleActions’;

class AppleBusket extends React.Component {

render() {

let { state, dispatch } = this.props;

return (

{ state.apples.map(apple =>

<AppleItem

state ={apple}

actions={{eatApple: id => dispatch(actions.eatApple(id))}}

key={apple.id}

/>

) }

<button onClick={() => dispatch(actions.pickApple())}>摘苹果

)

}

}

function selectState(state) {

return {

state: state.appleBusket

}

}

export default connect(selectState)(AppleBusket);

注意这两行。就是装入action的地方

actions={{eatApple: id => dispatch(actions.eatApple(id))}}

<button onClick={() => dispatch(actions.pickApple())}>摘苹果

上面代码中引入的actions其实是actionCreators。

此外,actionCreator还有很简洁的用法:对actionCreator做dispatch级别的封装,这个过程我们可以使用 redux 提供的 bindActionCreators 函数自动完成。然后就可以直接调用action,而不需要使用dispatch方法去调用actionCreator。看下面更新后的代码:

import React from ‘react’;

import { bindActionCreators } from ‘redux’;

import { connect } from ‘react-redux’;

import AppleItem from ‘…/components/AppleItem’;

import actions from ‘…/actions/appleActions’;

class AppleBusket extends React.Component {

render() {

let { state, actions} = this.props;

return (

{ state.apples.map(apple =>

<AppleItem

state ={apple}

actions={{eatApple: actions.eatApple}}

key={apple.id}

/>

) }

摘苹果

)

}

}

ajax

1)ajax请求的原理/ 手写一个ajax请求?
2)readyState?
3)ajax异步与同步的区别?
4)ajax传递中文用什么方法?

ajax.PNG

前12.PNG

s其实是actionCreators。

此外,actionCreator还有很简洁的用法:对actionCreator做dispatch级别的封装,这个过程我们可以使用 redux 提供的 bindActionCreators 函数自动完成。然后就可以直接调用action,而不需要使用dispatch方法去调用actionCreator。看下面更新后的代码:

import React from ‘react’;

import { bindActionCreators } from ‘redux’;

import { connect } from ‘react-redux’;

import AppleItem from ‘…/components/AppleItem’;

import actions from ‘…/actions/appleActions’;

class AppleBusket extends React.Component {

render() {

let { state, actions} = this.props;

return (

{ state.apples.map(apple =>

<AppleItem

state ={apple}

actions={{eatApple: actions.eatApple}}

key={apple.id}

/>

) }

摘苹果

)

}

}

ajax

1)ajax请求的原理/ 手写一个ajax请求?
2)readyState?
3)ajax异步与同步的区别?
4)ajax传递中文用什么方法?

[外链图片转存中…(img-CJjUGWOM-1714651937454)]

[外链图片转存中…(img-abXDsAUU-1714651937455)]

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值