关于redux:想了解关于-Redux-的这里都有

一、Redux 外围官网是这样解释Redux的:JavaScript 状态容器,提供可预测化的状态治理。 const state = { modleOpen: "yes", btnClicked: "no", btnActiveClass: "active", page: 5, size: 10}Redux 外围概念及工作流程store: 存储状态的容器,JavaScript对象View: 视图,HTML页面Actions: 对象,形容对状态进行怎么的操作Reducers: 函数,操作状态并返回新的状态Redux 计数器案例 ../Redux/src/counter <body> <button id="plus">+</button> <span id="count">0</span> <button id="minus">-</button><script src="https://cdn.bootcdn.net/ajax/libs/redux/4.2.0/redux.min.js"></script><script> // 3 存储默认状态 let initialState = { count: 0 } // 2 创立 reducer 函数 function reducer (state = initialState, action) { // 7 接管 action 并判断 action 的类型 switch (action.type) { case 'increment': return {count : state.count + 1} case 'decrement': return {count : state.count - 1} default: // 初始化是会主动发送一个 init 的 action 用来存储默认的state return state; } } // 1 创立 store 对象, createStore 有第二个参数代表默认值,也就是 reducer 中的 state 参数 let store = Redux.createStore(reducer); // 4 定义 action let increment = { type: 'increment' } let decrement = { type: 'decrement' } // 5 获取按钮并增加事件 document.getElementById('plus').onclick = function() { // 6 触发action store.dispatch(increment); } document.getElementById('minus').onclick = function () { store.dispatch(decrement); } // 8 订阅 store,当store发生变化的时候会执行回调 store.subscribe(() => { // 获取 store 中存储的状态 console.log(store.getState()) document.getElementById('count').innerText = store.getState().count; });</script></body>Redux外围APIconst store = Redux.crateStore(reducer): 创立 Store 容器function reducer (state = initialState, action) {}: 创立用于解决状态的 reducer 函数store.getState(): 获取状态store.subscribe(function(){}): 订阅状态store.dispatch({type: 'discription...'}): 触发action二、React + Redux1.在 React 中不应用 Redux 时遇到的问题在 React 中组件通信的数据流是单向的,顶层组件能够通过props属性向上层组件传递数据,而上层组件不能向下层组件传递数据,要实现上层组件批改数据,须要下层组件传递批改数据办法到上层组件,当我的项目越来越大的时候,组件之间传递数据也就变得越来越艰难。 ...

July 5, 2022 · 8 min · jiezi

关于redux:从-MVC-到-Flux从-Redux-到-Mobx

前端状态治理的工具库纷杂,在开启一个新我的项目的时候不禁让人纠结,该用哪个?其实每个都能达到我的目标,咱们想要的无非就是治理好零碎内的状态,使代码利于保护和拓展,尽可能升高零碎的复杂度。 应用 Vue 的同学可能更违心置信其官网的生态,间接上 vuex/pinia,不必过多纠结。因为我平时应用 React 较多,故就以后利用较宽泛的 Redux、Mobx 俩工具库为例,研读了一番,记录下本人的一些闲言碎语。 留神:以下不会波及到各个库的具体用法,多是探讨各自的设计理念、推崇的模式(patterns),提前阐明,免得耽搁大家工夫。 Redux、Mobx 或多或少都借鉴了 Flux 理念,比方大家常常听到的 “单向数据流” 这项准则最开始就是由 Flux 带入前端畛域的,所以咱们先来聊聊 Flux。 FluxFlux 是由 facebook 团队推出的一种架构理念,并给出一份代码实现。 为什么会有 Flux 的诞生?Facebook 一开始是采纳传统的 MVC 范式进行零碎的开发 但随着业务逻辑的简单,慢慢地发现代码里越来越难去退出新性能,很多状态耦合在了一起,对于状态的解决也耦合在了一起 造成了 FB 团队对 MVC 吐槽最深的两个点: Controller 的中心化不利于扩大,外围是因为 Controller 里须要解决大量简单的对于 Model 更改的逻辑对于 Model 的更改可能来源于各个方向。 可能是开发者自身想对 Model 进行更改、可能是 View 上的某个回调想对 Model 进行更改,可能是一个 Model 的更改引发了另一个 Model 的更改。咱们能够大略总结出,基于 MVC 的数据流向就有三种: Controller -> Model -> ViewController -> Model -> View -> Model -> View ... (loop)Controller -> Model1 -> Model2 -> View1 -> view2 ...并且这三种数据流向在理论业务中还很有可能是交错在一起。 ...

May 18, 2022 · 4 min · jiezi

关于redux:Redux

Redux 外围是js的状态容器 提供可预测化的状态治理 actions:reducers:store应用步骤,以计数器为例 <button id="inc">减少</button><span id="count"></span><button id="dec">缩小</button>创立store对象var store = Redux.createStore(reducer)创立reducer函数, 给定默认初始状态,并匹配action function reducer(state = initState, action) { switch (action.type) { case 'increment': return { ...state, count: state.count + 1 } case 'decrement': return { ...state, count: state.count - 1 } default: return state break }}定义action var increment = { type: 'increment' }var decrement = { type: 'decrement' }触发action store.dispatch(increment)订阅store变动,同步视图变动 store.subscribe(() => { console.log(store.getState())})react-reduxProvider组件 必须位于想要共享状态的组件的顶层,将组件包裹,用于提供store,全局可用 <Provider store={store}> <Children></Provider>connect办法 提供state到指定组件的props映射,同时将store的dispatch办法也放入了props中帮忙咱们订阅store,当store状态产生扭转的时候,从新渲染组件能够通过传递第二个参数,用来简化视图中的dispatch代码// counter.jsimport React from 'react'import { connect } from 'react-redux'import { increment, decrement } from '../store'const Counter = ({ count, inc, dec}) => { return ( <> <button onClick={inc}>+</button> <span>{count}</span> <button onClick={dec}>-</button> </> )}const mapStateToProps = state => ({ a: '10099', count: state.count})// 映射dispatch 办法const mapDispatchToPrps = dispatch => ({ inc() { dispatch(increment) }, dec() { dispatch(decrement) }})// connect的两个办法, 第一个映射state到props,第二个 映射dispatch到props中,能够缩小视图代码export default connect(mapStateToProps, mapDispatchToPrps)(Counter)combineReducer办法将多个reducer文件拆分后,应用redux提供的combineReduver办法进行合并reducerRedux 中间件实质就是一个函数,容许咱们扩大redux应用程序 ...

February 11, 2022 · 3 min · jiezi

关于redux:新的React状态库foca

基于 redux 和 react-redux。仓库地址:https://github.com/foca-js/foca 理念TS First,无TS不编程! 个性模块化开发专一 typescript 极致体验模型主动注册,导出即可应用内置 immer 疾速解决数据智能追踪异步函数的执行状态模型反对公有办法可定制的多引擎数据长久化数据隔离,容许同类状态库并存架构图 在线试玩CodeSandBox 应用定义模型// File: counterModel.tsimport { defineModel } from 'foca';const initialState: { count: number } = { count: 0,};// 毋庸手动注册到store,间接导出到react组件中应用export const counterModel = defineModel('counter', { // 初始值,必填属性,其余属性均可选 initialState, actions: { // state可主动提醒类型 { count: number } plus(state, value: number, double: boolean = false) { // 间接批改状态 state.count += value * (double ? 2 : 1); }, minus(state, value: number) { // 间接返回新状态 return { count: state.count - value }; }, // 公有办法,只能在模型外部被effect办法调用,内部调用则TS报错(属性不存在) _clear(state) { return this.initialState; }, }, effects: { // 异步函数,主动追踪执行状态(loading) async doSomething() { // 调用公有办法 await this._sleep(100); // 疾速解决状态,对于网络申请的数据非常不便 this.setState({ count: 1 }); this.setState((state) => { state.count += 1; }); // 调用action函数解决状态 this.plus(1, true); // 调用effect函数 return this.commonUtil(1); }, // 一般函数 commonUtil(x: number) { return x + 1; }, // 公有办法,只能在模型外部应用,内部调用则TS报错(属性不存在) _sleep(duration: number) { return new Promise((resolve) => { setTimeout(resolve, duration); }); }, }, hooks: { // store初始化实现后触发onInit钩子 onInit() { this.plus(1); console.log(this.state); }, },});在函数组件中应用import { FC, useEffect } from 'react';import { useModel, useLoading } from 'foca';import { counterModel } from './counterModel';const App: FC = () => { // count类型主动提醒 number const { count } = useModel(counterModel); // 仅effects的异步函数能作为参数传入,其余函数TS主动报错 const loading = useLoading(counterModel.doSomething); useEffect(() => { counterModel.doSomething(); }, []); return ( <div onClick={() => counterModel.plus(1)}> {count} {loading ? 'Loading...' : null} </div> );};export default App;在类组件中应用import { Component } from 'react';import { connect, getLoading } from 'foca';import { counterModel } from './counterModel';type Props = ReturnType<typeof mapStateToProps>;class App extends Component<Props> { componentDidMount() { counterModel.doSomething(); } render() { const { count, loading } = this.props; return ( <div onClick={() => counterModel.plus(1)}> {count} {loading ? 'Loading...' : null} </div> ); }};const mapStateToProps = () => { return { count: counterModel.state.count, loading: getLoading(counterModel.doSomething); };}export default connect(mapStateToProps)(App);心愿能成为你下一个我的项目的状态治理计划!喜爱就先star一下吧。仓库地址:https://github.com/foca-js/foca ...

December 23, 2021 · 2 min · jiezi

关于redux:redux

redux 的目录架构import {createStore,applyMiddleware,combineReducers} from 'redux'; import thunk from 'redux-thunk'; import {Provider} from "react-redux" store 1.全局只有一个store通过函数createStore创立 参数是reducer 1.1 createStore(Reducer,applyMiddleware(thunk)); action异步对象 1.2 createStore(combineReducers({key:reducers}),applyMiddleware(thunk)); action异步对象 2. api2.1 diaptch(action)2.2 subacriber(cb) ??有效2.3 getState()组件 1. 须要依据store.getState()获取存储的状态2. 批改须要store.dispatch(action)分发给reducer解决3. redux是能够帮忙你批改state中的状态,然而不会执行render从新渲染界面,render本人调用是有效的 reducer 4. 打工人,收到散发工作立马工作,并且返回最新的state 5. 除了加工,state中的状态的初值也是reducer提供的。会在创立store的时候主动调用reducer dispatch 组件与store沟通的工具人,传递action actionCreator 6. 生产action对象 1.1 同步action对象,是一个js对象,包含type以及data属性 1.2 异步action对象,是一个函数 ?? export const createAddActionAsync = (data,time) =>{ return (dispatch)=>{ setTimeout(()=>{ //store 须要的action 是obj fun无奈让reduces干活没type data // 收到函数(异步工作)的时候 帮忙执行 最初会返回个obj // store须要中间件 redux-thunk 有了它 store能够承受一个函数action dispatch(createAddAction(data)) },time) } };react-redux1. containComponent connect生成containComponent app 中render containComponent传递store2. uiComponent 不可操作redux 只能借助props操作3. store 4. connect const containComponent = connect(mapStateToProps,mapDiapatchToProps)(UI)5. api [传递状态]mapStateToProps(state) 承受container传递的state 返回状态对象作为props传递给UI组件 [传递操作状态]mapDiapatchToProps(dispatch) 承受container传递的dispatch 返回批改(操作)状态的对象作为props传递给UI组件 mapStateToProps以及mapDiapatchToProps react-redux调用机会6. Provider 容器组件是app的子组件容器组件的store是作为参数穿进去的,多个容器就得传递屡次能够应用Provider 入口应用 Provider 会剖析页面有多少容器组件 传递store7. mapDiapatchToProps 能够是一个对象 提供 action react-redux 会主动帮你分发给store 而后调用dispatch给reducers8. 应用react-redux的能够主动监控state的变动,界面会自动更新。9. combineReducers 参数为对象 value是reducer

December 3, 2021 · 1 min · jiezi

关于redux:react篇lesson3reactredux知识点

这一节美容不是很难次要是react-reudx的外围局部,这部分其实redux也有,就是Provider、connect、bindActionCreators等几个罕用的API的实现。间接上外围代码import React, { useLayoutEffect, useReducer, useCallback, useContext } from 'react';const useForceUpdata = () => { const [_, forceUpdata] = useReducer(x => x + 1, 0,); const upDate = useCallback(() => forceUpdata(), []) return upDate};const Cunsumer = ({ store }, mapStateToProps, mapDispatchToProps, WarppedComponent, props) => { // stateProps其实就是以所有state为参数,mapStateProps执行的后果 const forceUpdata = useForceUpdata() const stateProps = mapStateToProps(store.getStore()); // dispatchProps须要麻烦一点因为会有两种状况 // 第一种mapDispatchProps是函数 // 第二种mapDispatchProps是对象 let dispatchProps = { dispatch: store.dispacth }; // 这里补充一下最精确的判断数据类型的办法:Object.prototype.toString.call(mapDispatchProps) if (typeof mapDispatchToProps === "function") { dispatchProps = mapDispatchToProps(store.dispach) } else if (typeof mapDispatchToProps === "object") { dispatchProps = bindActionCreators(mapDispatchToProps, store.dispach) } // * 重点 这里是必须要写订阅的不然咱们代码跑起来不会报错然而页面也不会刷新, // * redux 只是一个状态存储库,不具备主动刷新页面的性能,须要咱们自行编写订阅代码 // * 这里应用useLayoutEffect而不是useEffect,是因为useLayoutEffect在dom变更后就开始同步执行,而useEffect有提早 useLayoutEffect(() => { const unsubscribe = store.subscribe(() => { forceUpdata() }) return () => { unsubscribe() }; }, [store]) return <WarppedComponent {...props} {...stateProps} {...dispatchProps} />};// 创立Contextconst Context = React.createContext();// 导出Provider组件export const Provider = ({ store, children }) => { return <Context.Provider value={store}>{children}</Context.Provider>};export const connect = ({ mapStateToProps, mapDispatchToProps }) => WarppedComponent => props => { // 子孙组件生产父级传下来的value return <Context.Cunsumer> {(value) => Cunsumer(value, mapStateToProps, mapDispatchToProps, WarppedComponent, props)} </Context.Cunsumer>}export const bindActionCreators = (data, dispacth) => { let obj = {} for (const key in data) { obj[key] = dispacth((...arg) => obj[key](...arg)) } return obj};export const useDispatch = () => { const store = useContext(Context) // 间接返回store中的dispatch即可 return store.dispatch}export const useSelector = (selctor) => { const store = useContext(Context) // 这里一样是要订阅一下不然页面不会更新 useLayoutEffect(() => { const unsubscribe = store.subscribe(() => { forceUpdata() }) return () => unsubscribe() }, [store]) return selctor(store.getStore())}以上便是几个罕用API的根本实现. ...

November 24, 2021 · 2 min · jiezi

关于redux:react篇lesson2redux知识点

提起redux小伙伴们应该都不生疏,驰名的第三方状态治理库,也是很多小伙伴进阶路上必然攻克的源码之一。Redux 除了和 React 一起用外,还反对其它界面库。 它体小精悍(只有2kB,包含依赖)。明天咱们就来说说redux学习中须要重点学习的货色; 三大准则Redux 能够用这三个根本准则来形容: 繁多数据源整个利用的 state 被贮存在一棵 object tree 中,并且这个 object tree 只存在于惟一一个 store 中。 State 是只读的惟一扭转 state 的办法就是触发 action,action 是一个用于形容已产生事件的一般对象。 应用纯函数来执行批改为了形容 action 如何扭转 state tree ,你须要编写 reducers。 应用其实redux应用起来大抵能够分为一下几步骤: 申明store文件(寄存state)// store.jsimport { createStore } from 'redux';import reducer from "./reducer.js";const store = createStore(reducer);export default store;申明reducer.js文件(批改state)申明的reducer文件次要是为了批改保留的state export default const counter = (state = 0, action) => { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; }};dispatch触发action动作、subscribe订阅以及unsubscribe勾销订阅store.dispatch({ type: 'INCREMENT' });const unsubscribe = store.subscribe(()=> {});知识点以上是咱们须要理解的根底,其实redux摊开了说白了也没有什么神秘的就是一个第三方文件保留state,用特定action去触发批改,咱们须要理解的真正外围是redux对于createStore中参数enhancer的解决以及applyMiddleware的实现,这才是redux的外围; ...

November 24, 2021 · 2 min · jiezi

关于redux:Redux-的基本使用

1.外围概念1.什么是Redux?Redux是一个治理状态(数据)的容器,提供了可预测的状态治理 2.什么是可预测的状态治理?数据 在什么时候, 因为什么, 产生了什么扭转,都是能够管制和追踪的,咱们就称之为预测的状态治理 3.为什么要应用Redux?React是通过数据驱动界面更新的,React负责更新界面, 而咱们负责管理数据默认状况下咱们能够在每个组件中治理本人的状态, 然而当初前端应用程序曾经变得越来越简单状态之间可能存在依赖关系(父子、共享等),一个状态的变动会引起另一个状态的变动所以当应用程序简单的时候, 状态在什么时候扭转,因为什么扭转,产生了什么扭转,就会变得十分难以管制和追踪所以当应用程序简单的时候,咱们想很好的治理、保护、追踪、管制状态时, 咱们就须要应用Redux 4.Redux核心理念通过 store 来 保留数据通过 action 来 批改数据通过 reducer 来 关联 store 和 action 2.三大准则1.Redux三大准则繁多数据源整个应用程序的state只存储在一个 store 中Redux并没有强制让咱们不能创立多个Store,然而那样做并不利于数据的保护繁多的数据源能够让整个应用程序的state变得不便保护、追踪、批改State是只读的惟一批改State的办法肯定是触发action,不要试图在其余中央通过任何的形式来批改State这样就确保了View或网络申请都不能间接批改state,它们只能通过action来形容本人想要如何批改stat;这样能够保障所有的批改都被集中化解决,并且依照严格的程序来执行,所以不须要放心race condition(竟态)的问题;应用纯函数来执行批改通过reducer将 旧state和 action分割在一起,并且返回一个新的State:随着应用程序的复杂度减少,咱们能够将reducer拆分成多个小的reducers,别离操作不同state tree的一部分然而所有的reducer都应该是纯函数,不能产生任何的副作用 2.什么是纯函数返回后果只依赖于它的参数,并且在执行过程外面没有副作用 // 纯函数function sum(num1, num2){ return num1 + num2;}// 非纯函数let num1 = 10;function sum(num2){ return num1 + num2;}// 纯函数const num1 = 10;function sum(num2){ return num1 + num2;}3.根本应用筹备工作创立 demo 目录cd demonpm init -y #初始化一个node我的项目npm install --save redux #装置reduxredux的应用store.subscribe() #监听函数(一旦 state 发生变化,就主动执行这个函数) ...

November 12, 2021 · 7 min · jiezi

关于redux:Redux学习笔记

从一个初学者的角度来剖析: Redux是干啥用的呢?学习Redux之前要理解什么?理论我的项目中如何用到(针对小白)状态治理和hooks有什么关系? 问题3:理论我的项目中如何用到(针对小白)实现这样一个性能:点击Increase的话右边数字加1,点击Decrease每次减1 首先创立这个组件 class Counter extends Component { render() { const { value, onIncreaseClick, onDecreaseClick } = this.props return ( <div> <span>{value}</span> <button onClick={onIncreaseClick}>Increase</button> <button onClick={onDecreaseClick}>Decrease</button> </div> ) }}Action :Action 是把数据从利用传到 store 的有效载荷。它是 store 数据的惟一起源。一般来说你会通过 store.dispatch() 将 action 传到 store。Action 实质上是 JavaScript 一般对象。咱们约定,action 内必须应用一个字符串类型的 type 字段来示意将要执行的动作。 // Actionconst increaseAction = { type: 'increase' } const decreaseAction = { type: 'decrease' } Reducers : Reducers 指定了利用状态的变动如何响应 actions 并发送到 store 的,记住 actions 只是形容了有事件产生了这一事实,并没有形容利用如何更新 state。 ...

September 15, 2021 · 2 min · jiezi

关于redux:函数合成compose的多种实现原理

// 责任链模型(一个接一个执行) const fn1 = (x, y) => x + y;const fn2 = (z) => z * z;// 失常组合 const compose = (fn1, ...other) => (...args) => { let ret = fn1(...args); other.forEach(item => { ret = item(ret); }); return ret;}// redux 中间件组合形式 const reduxCompose = (...fns) => fns.reduce((a, b) => (...args) => b(a(...args)))const fn = reduxCompose(fn1, fn2);console.log(fn(1, 2));// 洋葱圈模型(一半一半执行) const koaCompose = (middlewares) => { return function () { return dispatch(0); function dispatch(idx) { const fn = middlewares[idx]; if (!fn) { return Promise.resolve(); } return Promise.resolve( fn(function next() { return dispatch(idx + 1) }) ) } }};async function func1(next) { console.log("func1"); await next(); console.log("func1 end");}async function func2(next) { console.log("func2"); await delay(); await next(); console.log("func2 end");}async function func3(next) { console.log("func3");}async function delay() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, 2000); })}const middlewares = [func1, func2, func3];const finaFn = koaCompose(middlewares);finaFn()

August 30, 2021 · 1 min · jiezi

关于redux:redux总结

Redux 是什么Redux is a predictable state container for JavaScript apps Redux概念store: 利用数据的存储核心action: 利用数据的扭转的形容reducer: 决定利用数据新状态的函数,接管利用之前的状态和一个 action 返回数据的新状态。state: 状态middleware: redux 提供中间件的形式,实现一些 流程的自定义管制,同时造成其插件体系。 流程 环境筹备npx create-react-app cracd cra npm start在 cra我的项目中装置redux yarn add reduxredux整体感知// reducerconst weight = (state = 160, action) => { switch (action.type) { case 'eat': return state + 10 case 'hungry': return state - 10 default: return 160 }}const store = createStore(weight)console.log(store.getState())store.dispatch({ type: 'eat' })console.log('我吃了一些事物')console.log(store.getState())console.log('我饿了好几天')store.dispatch({ type: 'hungry' })console.log(store.getState())console.log('我又饿了好几天')store.dispatch({ type: 'hungry' })console.log(store.getState())reducer 外面应用switch语句依据传入的类型,输入新的状态把reducer 传入 createStore(weight)通过 dispatch 传入不同的类型,扭转状态state。store.dispatch({ type: 'hungry' })通过 store.getState() 获取以后的状态 ...

June 28, 2021 · 1 min · jiezi

关于redux:redux中使用TS每次都要定义一遍类型

umi架构下:ts我的项目中redux定义module每次都要写一遍类型定义麻烦得很 typing文件夹下创立Redux.d.ts申明文件 import type { Effect, Subscription, ImmerReducer } from 'umi';declare module MyRedux { // model type Models<T> = { namespace?: string; state: T; effects: Record<string, Effect>; reducers: Record<string, ImmerReducer<T>>; subscriptions?: Record<string, Subscription>; };}// 导出成模块,再全局导出MyRedux,这样应用就不必再import type { Redux } from '@/typings/redux'; 了export = MyRedux; // 因为应用了import,此文件变成部分模块,其余中央应用只能import导入(import type { Redux } from '@/typings/redux';)能力应用export as namespace MyRedux;如何应用models文件夹下创立staff.ts type StaffSettingState = { staff: Record<string, never>; };const StaffSettingsModel: MyRedux.Models<StaffSettingState> = { namespace: 'staffSettingsModel', state: { staff: {} }, effects: {}, reducers: {},};export default StaffSettingsModel;

May 17, 2021 · 1 min · jiezi

关于redux:Redux

redux=>将Flux与函数式编程联合到一起,是一种web架构的解决方案 Redux应用场景某个组件的状态,须要共享某个状态须要在任何中央都能够拿到一个组件须要扭转全局状态一个组件须要扭转另一个组件的状态

April 14, 2021 · 1 min · jiezi

关于redux:Redux-理解-combineReducers

最近在我的项目中应用 redux 时遇到一个问题:应用多个 reducer 治理状态(如 ruducerA,reducerB),当通过 action 更新数据时,以后的 reducerA 数据更新胜利,但另一个 reducerB 数据被初始化。 这个行为让我十分蛊惑,排查了很久, 一度找不到下手点。代码如下: APP.js const rootReducer = combineReducers({ RudecerA, RudecerB,}); const store = createStore( rootReducer);  export default function App(props) { return ( <Provider store={store}> <Router {...props} /> </Provider> );}reducerB.js export function ReducerB( state = initState, action, ) { switch (action.type) { case UPDATE_RECORD_DATA: return { ...state, recordData: action.payload }; case DELETE_RECORD_DATA: return { ...state, recordData: initRecordData }; default: return initState; }}解决办法起初在官网文档看到对于 combineReducer 的介绍及留神点: ...

March 6, 2021 · 1 min · jiezi

关于redux:中间件

什么是中间件?中间件就是插在源到指标之间的一段逻辑(个别为函数,比方redux) redux中源为页面,指标为store中的state,通过dispatch将页面中的数据反映到store中 koa中源为request,指标为response, redux中在dispatch数据到store只调用了dispatch这个函数,所以只能对这个函数进行革新,两头去插入中间件。 为什么redux须要中间件?dispatch函数第一句话: if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) }isPlainObject函数的目标是查看action是不是对象字面量或者new object()结构进去的对象,其余的比方action为函数,redux间接报错。比方异步申请,须要将从接口申请到的数据放到redux。咱们能够间接发动异步申请,而后将数据dispacth到redux里redux@4.0.0 shopping-cart actions/index.js export const checkout = products => (dispatch, getState) => { const { cart } = getState() dispatch({ type: types.CHECKOUT_REQUEST }) shop.buyProducts(products, () => { dispatch({ type: types.CHECKOUT_SUCCESS, cart }) })}为了对立写异步申请,将申请函数放在一个文件里,这样不会显得芜杂。然而dispatch跟getState怎么拿到,connect后能够拿到dispatch,getState拿不到,只能通过mapStateToProps传递state.这么操作比拟麻烦,在中间件中对立解决就不须要每次调用传递dispatch getState。怎么对一个函数函数革新 插入中间件? 深刻了解洋葱模型中间件机制Koa 框架教程

October 16, 2020 · 1 min · jiezi

关于redux:重学reactredux

一、redux应用redux应用和之前的useReducer应用比拟相似(详情请看 重学react——context/reducer 一文) // store.tsimport { createStore, applyMiddleware, combineReducers } from 'redux';import logger from 'redux-logger'; // 中间件import thunk from 'redux-thunk'; // 中间件export interface IState { name: string, age: number, sons: any[]}export const state: IState = { name: 'lihaixing', age: 32, sons: []};export type Action = { type?: string; data?: any;};export function reducer(state: IState | undefined, action: Action) { if (!action.type && !action.data) { return { ...state, ...action }; } switch (action.type) { case 'update': return { ...state, ...action.data }; default: return state; }}// createStore与useReducer相似export const store = createStore(reducer, state, applyMiddleware(logger, thunk));// contaner.tsximport React, { useReducer } from 'react';// react-redux的作用和react的context相似import {Provider} from 'react-redux'; import { store } from './store';import Com1 from './comp1'import Com2 from './comp2'const IndexView = () => { return <Provider store={store}> <Com1 /> <Com2 /> </Provider>};export default IndexView;// com1.tsximport React, { useContext } from 'react';import { IState } from './index';// connect和useContext的作用相似import { connect } from 'react-redux';const IndexView = (props: any) => { const {name, age, sons} = props.state; const {update, asyncUpdate} = props; const setAge = () => { // dispatch({type: 'update', data: {age: age + 1}}); update({age: age + 1}); }; const setSons = () => { // setTimeout(()=>{ // update({sons: [...sons, 1]}); // }) asyncUpdate({sons: [...sons, 1]}); }; return <div> {name} <br/> {age} <br/> {sons} <br/> <button onClick={setAge}>按钮age</button> <button onClick={setSons}>按钮sons</button> </div>;};const mapStateToProps = (state: IState) => ({state});const mapDispatchToProps = { update: (data: any) => ({type: 'update', data}), // 异步返回的是函数 asyncUpdate: (data: any) => (dispatch: any) => { setTimeout(() => { dispatch({type: 'update', data}); },1000); }};// 不传第二个参数,props会有dispatchexport default connect(mapStateToProps, mapDispatchToProps)(IndexView);从上述咱们能够将redux和useReducer/useContext作比照重学react——context/reducer ...

September 1, 2020 · 3 min · jiezi

关于redux:TS-Redux-的一些感想

很久没写文章了,始终在奉献开源框架和一些库,也不善于写文章,看我已往的文章,全都是干瘪瘪的,没有半点废话。这次算是写个软文吧,实话实说,也没期待会有多少人看。 链接先放着:https://github.com/redux-model/redux-model 记得2016年刚守业失败进去找工作,找了一家教育类的互联网公司(当初也是)。这家公司用的是es6 + react + redux + webpack的前端架构。而我守业期间,还在写 es3 + jquery,钻研各种构造函数/继承/原型 等一些比拟底层的货色。所以进了新公司,算是解放了吧,有种从2g网络迁徙到4g网络的感觉,仙气飘飘。 那时候的redux,哇哦,先写上3个actionType类型(申请须要3个状态),再写一个action和一个reducer,reducer里写上3个case,别离对应3个actionType。如果Reducer数据比较复杂,那就是各种Object.assign了。所以在写了两个月之后,俨然发现我是不是在始终写模板文件啊?感觉每次都是似曾相识?但也没啥方法,抽也没法抽,毕竟对redux理解不够深刻。我共事的做法是写一个代码片段,每次须要的时候主动生成,而后修修改改完事。 断断续续写了2年左右原生的Redux,直到2018年,Typescript曾经有点纸包不住火了,我也被点燃了,所以尝试了好几次,总想把我的项目转到ts去,于是网上各种搜寻最佳实际案例,每种都尝试过来,各种崎岖。断断续续地,终于在2018年底给安顿上了! 写了第一版的Ts + React + Redux,还算称心,总算都有类型了,数据能够精准追踪。当初是2019年,也快30岁了,你晓得的,人老了,就写不动了,须要正当地偷懒,须要准时上班 心里其实早有疙瘩,这Redux模板是时候给治一治了。于是有了第一版的Redux-Model,目标很明确,干掉actionType,把action和reducer整合在一起,不再写3个文件。所以第一版本的模型,分的很细,一个模型只蕴含一个action,当咱们有多个模型须要作用到同一个reducer时,reducer须要附在其中一个模型里。 现在,Redux-Model曾经升到了8.0了,框架早已稳定下来,一个模型能够写无数个action和一个reducer,数据变动也高深莫测。所以我最后的那个TS我的项目,至多大规模重构了5次的模型,每次都是几百个模型文件变更。真正地稳固应该是在6.0的时候,因为这个版本解决了一个类型主动反推导方面的大难题,这个问题至多花了我3个月的上班时间去冥思和尝试才胜利。那时候,公司里有3个团队曾经曾经在用我的模型框架(包含TS也是我在公司里推广的,当初所有团队都曾经承受TS了),想让他们降级,就必须一个一个帮忙升,还真是有点不好意思了。 付出总是有回报的,当初在去采访那几个团队,对框架的评估是完满,这不也是我所谋求的吗? 框架虽好,但没有大厂背景,没有集体光环,想推广起来几乎和做梦一样----想的美。推广过一段时间,大部分是在群里,不过换来的都是冷言冷语,什么 dva不香吗?mobx不香吗? 这些框架难道我没尝试过吗,就你晓得香?都是给JS用户设计的,对TS不太敌对,至多还没达到我的要求,所以我才要写这么一个框架,为TS量身定制的Redux框架。七夕那天,我在一个聊了挺久的群发了一个框架链接(加群大半年,总共没发超过5次),当场就被群主骂了,我没法承受,因为他感觉我推广这个是为了找到更好的工作?这种羞辱开源精力的事,我没法承受,退群了。预先群主还要再私发我一条微信:无利不起早?? 我没有回复,没必要了,不与君子辩论。 不是每个人都为找工作而写代码。趣味是个好货色,高考完结,他人都在网吧打游戏,我在书店背了几段html脚本去网吧运行,给小伙伴看成果。在共事眼里,我就是那种聊到代码就两眼放光的人。而开源,是为了欠缺生态,让大家有更好的抉择。当然了,某种程度也是想证实本人能力ok,码痴不都这样吗? 感兴趣的TS铁粉,举荐应用。JS用户不举荐应用,因为是TS定制的,但欢送star,让更多的人晓得这个库,毕竟我太缺光环了。 好不好用,您去看看Readme,而后demo运行看看就晓得什么叫手中无TS,心中有TS。我不去吹,因为自信。 https://github.com/redux-model/redux-model

August 27, 2020 · 1 min · jiezi

关于redux:关于redux的一些学习笔记

redux是用于治理react状态的一个状态管理器 外围有三大部分组成--action、reducer、store action:是触发state扭转的状态,是一般的js对象,语义作用; const addAction = { type: 'ADD_ITEM', text: 'to add a new item'}reducer:纯函数 传递action以触发state扭转的函数,传入state和action,做一些合乎action的解决,创立state的正本,并返回这个正本。 function todoApp(state, action) { switch(action.type){ case 'ADD_ITEM': return Object.assign({},state,{ ...state.todos, {text: action.text, completed: true} }); }}store:存储state的中央。办法:getState():获取statedispatch(action): 更新statesubscribe(listener): 注册监听器,返回一个函数,调用此函数可登记监听器。 创立store: import { createStore } from 'redux';import rodoApp from './reducers';let store = createStore(todoApp);单向数据流--数据生命周期:调用store.dispath(action);redux store会主动调用传入的reducer函数;根reducer会把多个子reducer输入合并成一个繁多的reducer树;redux store保留根reducer返回的残缺state树,该state树就是下一个state,所有调用store.subscribe(listener)的监听器都将被调用,在监听器中能够通过调用store.getState()来获取以后的state。 配合react(redux自身和react没有关系)装置react绑定库:npm install --save react-redux其基于react的容器组件与UI组件相拆散的思维来开发。 UI组件Container组件是否间接应用Redux否是数据起源props监听redux state数据批改从props调用回调函数向redux派发action调用形式手动通常由react redux生成作用展现骨架、款式数据获取、状态更新容器组件用connect()办法生成。【该办法外部做了一些性能优化,防止了很多不必要的rerender】其本质是通过store.subscribe()从redux state树中读取局部数据,并通过props将这些数据传给对应的UI组件。 connect()的用法 import { connect } from 'react-redux';connect(mapStateToProps, mapDispathToProps)(TodoList);const getVisibleTodos = (todos, filter) => { switch(filter) { case 'SHOW_COMPLETED': return todos.filter(...) // ... }}const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) }}const mapDispathToProps = dispatch => { return { onTodoClick: id => { dispatch(toggleTodo(id)) } }}mapStateToProps: 该函数用于指定如何将以后的redux store的state映射到对应UI组件的props中。mapDispathToProps:该函数接管dispatch()办法并返回冀望注入到UI组件props中的回调函数。 ...

August 20, 2020 · 1 min · jiezi

手写实现reactredux的Hook-API

前言本文章的内容,不会对根底的Hook API进行解说,比方(能够从官网间接看到)react Hook API:https://zh-hans.reactjs.org/d... useStateuseEffectuseContext间接放一个demo,自行学习吧。也不是很难import React,{ useState,useEffect } from 'react'export default function HookPage(){ const [date, setDate]=useState(new Date()) const [count, setCount] = useState(0) // 一个函数能够有多个 useEffect useEffect(() => { console.log('数字产生扭转:' ,count) }, [count]); // 有依赖项,是count; 所以每次点击更改count的时候进行更新,就相当于生命周期update useEffect(()=>{ console.log('setDate') const timer = setInterval(()=>{ setDate(new Date()) },1000) // 革除定时器,相当于申明周期 willUnmount return()=>clearInterval(timer) },[]) // 没有依赖项,就相当于DidMount return( <div> <h3> HookPage </h3> <div> <span> 数字:{count} </span> <button onClick={()=>setCount(count+1)}>减少</button> </div> <div> 当初工夫:{date.toLocaleTimeString()} </div> </div> )}所有的react API 在官网都说的很不错了,我感觉看官网的介绍就曾经很明确了,而且迭代版本必定也是最新的,所以本章的内容次要是联合实战和我的项目,进行简略的应用阐明,之后在进行,手动实现react-redux的Hook API;useReducer略微说下这个API,因为 我在应用init的时候,忽略了一个return,导致我查了半天。 ...

July 12, 2020 · 3 min · jiezi

reactRedux的API的使用及原理讲解和手动实现方法

对react-Redux的利用与了解在平时中,咱们间接去应用Redux,须要每个页面都须要引入store,执行getState()来获取到值,以及须要每次都进行订阅和勾销订阅。保护起来不不便 import React,{Component} from 'react'import store from '../store'export default class ReactRedux extends Component{ constructor(){ super() } componentDidMount(){// 挂载 this.unsubscribe=store.subscribe(()=>{ this.forceUpdate() }) } add=()=>{ store.dispatch({type:'ADD',payload:10}) } componentWillUnmount(){// 卸载 if(this.unsubscribe){ this.unsubscribe() } } render(){ return( <div> <h3>ReactRedux-page</h3> <div> <p>{store.getState()}</p> <button onClick={this.add} > add </button> </div> </div> ) }}从而引入react-Redux,Provider这个性能;在根目录下间接引入store;src/index.js import React from 'react';import ReactDOM from 'react-dom';import './index.css';import ReactRedux from './pages/ReactReduxPage'import {Provider} from 'react-redux'import store from './store'ReactDOM.render( <Provider store={store}> <ReactRedux /> </Provider> , document.getElementById('root'));class组建在应用connect引入import {connect} from 'react-redux'connect一共有三个参数:state, dispatch,mergeProps(将props进行合并)@connect( (state)=>({num:state}))class ReactRedux extends Component{ render(){ console.log(this.props) return( <div></div> ) }}@connect是装璜器的应用,或者能够export default connect()(class ...);装璜器的应用能够本人查下不做重点解说。打印this.props ...

July 11, 2020 · 4 min · jiezi

手写实现Redux功能applyMiddleware中间件

Rredux是什么?Redux是JavaScript应⽤用的状态容器器。它保证程序⾏行行为⼀一致性且易易于测试。 React Conponent: 我们开发的的React组建;当组建的内容要发生改变的时候;我们要发起一个dispatch,dispatch去派发action,action里面会包裹{type,paylod}Reducer就是制定修改规则的纯函数;接收一个旧的state,和action返回一个新的state 永远不要在 reducer ⾥做这些操作: 修改传⼊参数;执⾏有副作⽤的操作,如API请求和路由跳转;调⽤⾮纯函数,如Date.now()或 Math.random()。共享的数据存储在Store里面(state)更新完的数据Store传给组建安装Rredux npm install ReaduxReadux的使用创建store和制定reducers规则src/store/index.js import { createStore } from 'redux'// 定义state初始化和修改规则function createReducer(store=1,{type,payload=1}){ // type: action 的修改规则,用于switch判断 // paylod,当发起dispatch传进来的参数 console.log(store, 'store') switch (type) { case 'ADD': // 加法规则 return store+payload break; case 'MINUS': // 减法规则 return store-payload break; default: // 默认导出规则 return store break; }}const store = createStore(createReducer) // 定义store里面的修改规则export default store写一个组建src/pages/ReaduxPage.js import React, {Component} from 'react'import store from "../store/index";export default class ReaduxPage extends Component{ constructor(){ super() } render(){ return( <div> <h3> ReaduxPage </h3> <div> 获取到store里面设置的state <p>{store.getState()}</p> </div> 点击按钮,触发dispatch <button onClick={()=>store.dispatch({type:'ADD'})}> 点击增加 </button> </div> ) }}再或者改成,方法调用的方式 ...

July 5, 2020 · 4 min · jiezi

使用-HooX-管理-React-状态的若干个好处

HooX  是一个基于 hook 的轻量级的 React 状态管理工具。使用它可方便的管理 React 应用的全局状态,概念简单,完美支持 TS。1. 更拥抱函数式组件从 React@16.8 的 hook 到 vue@3 的composition-api,基本可以断定,函数式组件是未来趋势。HooX提供了函数式组件下的状态管理方案,以及完全基于函数式写法的一系列 API,让用户更加的拥抱函数式组件,走向未来更进一步。 2. 简化纯 hook 写法带来的繁杂代码写过 hook 的同学肯定知道,hook 带来的逻辑抽象能力,让我们的代码变得更有条件。但是: useCallback/useMemo 真的是写的非常非常多由于作用域问题,一些方法内的 state 经常不知道到底对不对实际举个例子吧,比如一个列表,点击加载下一页,如果纯 hook 书写,会怎么样呢? import { useState, useEffect } from 'react'const fetchList = (...args) => fetch('./list-data', ...args)export default function SomeList() { const [list, setList] = useState([]) const [pageNav, setPageNav] = useState({ page: 1, size: 10 }) const { page, size } = pageNav // 初始化请求 useEffect(() => { fetchList(pageNav).then(data => { setList(data) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // 获取下一页内容 const nextPage = () => { const newPageNav = { page: page + 1, size } fetchList(newPageNav).then(data => { setList(data) setPageNav(newPageNav) }) } return ( <div> <div className="list"> {list.map((item, key) => ( <div className="item" key={key}> ... </div> ))} </div> <div className="nav"> {page}/{size} <div className="next" onClick={nextPage}> 下一页 </div> </div> </div> )}很常规的操作。现在,我希望给“下一页”这个方法,加个防抖,那应该怎么样呢?是这样吗? ...

November 5, 2019 · 3 min · jiezi

Redux的核心概念实现代码与应用示例

Redux是一种JavaScript的状态管理容器,是一个独立的状态管理库,可配合其它框架使用,比如React。引入Redux主要为了使JavaScript中数据管理的方便,易追踪,避免在大型的JavaScript应用中数据状态的使用混乱情况。Redux 试图让 state 的变化变得可预测,为此做了一些行为限制约定,这些限制条件反映在 Redux 的三大原则中。 本文会介绍Redux的几个基本概念和坚持的三大原则,以及完整的回路一下Redux中的数据流。在了解以上这些概念之后,用自己的代码来实现一个简版的Redux,并且用自己实现的Redux结合React框架,做一个简单的TodoList应用示例。希望本文对于初识Redux的同学有一个清晰,全面的认识。 Redux的几个基本概念一、数据存储 - stateRedux就是用来管理状态数据,所以第一个概念就是状态数据,state就是存放数据的地方,根据应用需要,一般定义成一个对象,比如: {    todos: [],    showType: 'ALL',    lastUpdate: '2019-10-30 11:56:11'} 二、行为触发 - actionweb应用,所有的数据状态变更,都是由一个行为触发的,比如用户点击,网络加载完成,或者定时事件。在简单应用里面,我们一般都是在行为触发的时候,直接修改对应的数据状态,但是在大型复杂的应用里面,修改同一数据的地方可能很多,每个地方直接修改,会造成数据状态不可维护。 Redux引入了action的概念,每个要改变数据状态的行为,都定义成一个action对象,用一个type来标志是什么行为,行为附带的数据,也都直接放在action对象,比如一个用户输入的行为: {    type: 'INPUT_TEXT',    text: '今天下午6点活动碰头会议'} 然后通过dispatch触发这个action,dispatch(action) 三、行为响应 - reducer状态,action的概念了解了,当action触发的时候,肯定要修改state数据,在讲解action的时候有说过,不能直接修改state,我们需要定义一个reducer来修改数据,这个reducer就是一个行为响应函数,他接收当前state,和对应的action对象,根据不同的action,做相应的逻辑判断和数据处理,然后返回一个新的state。 注意,一定是返回一个新的state,不能直接修改参数传入的原state,这是redux的原则之一,后面会讲到。 function reducer ( state = [], action ) {    switch ( action.type ) {        case 'INPUT_TEXT':            return [...state, {text: action.text, id: Math.random() }]        default:            return state;    }} 四、数据监听 - subscribe数据的更新已经在reducer中完成了,在一些响应式的web应用中,我们往往需要监听数据状态的变化,这个时候就可以用subscribe了 redux内部保存一个监听队列,listeners,可以调用subscribe来往listeners里面增加新的监听函数,每次reducer修改完state之后,会逐个执行监听函数,而监听函数可以获取已经更新过的state数据了 listeners = [];subscrible( listener ) {    listeners.push( listener );    return function () {        let index = listeners.index( listener );        listeners.splice( index, 1 );    }}dispatch( action ) // 触发 actionreducer(state, action) listeners.map( ( listener ) => {    listener()} ) Redux的几大原则一、单一数据原则整个应用的数据都在state,并且只有这一个state,这么做的目的是方便管理,整个应用的数据就这一份,调试方便,开发也方便,可以在开发的时候用本地的数据。而且开发同构应用也很方便,比如服务端渲染,把服务端的数据全部放在state,作为web端初始化时候的数据 二、state只读state的数据对外只读,不能直接修改state,唯一可以修改的方式是触发action,然后通过reducer来处理。 因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心竞态条件(race condition)的出现。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。 三、使用纯函数先说明下什么是纯函数,纯函数指的是函数内部不修改传入的参数,无副作用,在传参一定的情况下,返回的结果也是一定的。Redux中的Reducer需要设计成存函数,不能直接操作传入的state,需要把改变的数据以一个新的state方式返回。 Redux中的数据流其实上面讲Redux基本概念的时候已经大概的说了下数据流向方式了,就是: view->action->reducer->state->view,用文字来表述就是,首先由于页面上的某些事件会触发action,通过dispatch(action)来实现,然后通过reducer处理,reducer(state, action)返回一个新的state,完成state的更新,当然对于响应式的应用,会触发listener(),在listener里面获取最新的state状态,完成对应视图(view)的更新。这就是整个redux中的数据流描述,如下图所示: Redux的实现代码(非官方)在对Redux的基本概念和几大原则熟悉了之后,可以实现一个自己的Redux了,当然我们一般都直接用官方的npm包,这里自己实现的比较简单,没有做什么入参验证,异常处理之类的,主要是加深下对Redux的理解。下面直接贴代码了,对应的概念都有注释。 // redux.js// 创建state的函数// 传入reducer 和初始化的statefunction createStore( reducer, initState ) {    let ref = {};    let listeners = [];    let currentState = initState;     // dispath函数,用来触发action    function dispatch ( action ) {        // 触发的action,通过reducer处理        currentState = reducer( currentState, action )         // 处理完成后,通知listeners        for ( let i in listeners ) {            let listener = listener[ i ];            listener();        }        return action;    }     // 返回当前的state    function getState () {        return currentState;    }     // 订阅state变化, 传入listener,返回取消订阅的function    function subscribe ( listener ) {        listeners.push( listener );        return function () {            let index = listeners.indexOf( listener );            if ( index > -1 ) {                listeners.splice( index, 1 );            }        }    }        ref = {        dispatch: dispatch,        subscribe: subscribe,        getState: getState    };    return ref;} function combineReducers( reducers ) {    return function ( state, action ) {        let finalState = {};        let hasChanged = false;        for ( let key in reducers ) {            let reducer = reducers[ key ]            if ( typeof reducer === 'function' ) {                let keyState = reducer( state && state[ key ], action );                hasChanged = hasChanged || keyState !== state[ key ];                finalState[ key ] = keyState;            }        }        return hasChanged ? finalState : state;    }} export { createStore, combineReducers } 是不是觉得怎么才这么点代码,就是这么点代码,而且还包含了一个combineReducers辅助函数,下面再贴一点使用示例代码 // reducer函数,用于处理actionfunction reducer( state = [], action ) {    switch( action.type ) {        case 'INPUT_TEXT':            return [ ...state, { text: action.text, key: Math.random(), isDo: false }];        case 'TOGGLE_TODO':            return state.map( ( item ) => {                if ( item.key === action.id ) {                    return {...item, isDo: !item.isDo };                }            } );        default:            return state;    }} let store = createStore( reducer ); // 在用户输入一条Todo时候console.log(store.getState());store.dispatch( { type: 'INPUT_TEXT', text: '这里是一条待办事项' } );console.log(store.getState()); //在用户点击一条Todo Item的时候,切换完成状态console.log(store.getState());store.dispatch( { type: 'TOGGLE_TODO', id: item.key } )console.log(store.getState()); Redux与React的结合应用示例下面,利用Redux结合React开发一个简单的Todo工具,页面主要功能点 1、可以添加Todo事项 2、点击事项会切换事项的完成状态 3、可以切换展示全部/已完成/待完成事项 这个实例是基于react,react-redux完成的,项目搭建用的是create-react-app,利用react-redux提供的接口,将redux中的state和action集成到组件中,需要读者熟悉create-react-app的使用,以及react-redux的主要接口功能,以下贴出主要代码,感兴趣的同学可以自己搭建实现 首先定义好state数据结构和action以及对应的reducer state包含两部分,一是todos,待办事项列表,二是showType,展示类型 action包含这么三种,一是添加新的Todo,二是切换事项完成状态,三是切换展示类型,分别定义好 actions.js // actions.jslet nextTodoId = 0 export const addTodo = text => {    return {        type: 'ADD_TODO',        id: nextTodoId++,        text    };}; export const setShowType = showType => {    return {        type: "SET_SHOW_TYPE",        showType    };}; export const toggleTodo = id => {    return {        type: 'TOGGLE_TODO',        id    };}; reducers.js const todos = ( state = [], action ) => {    switch ( action.type ) {        case 'ADD_TODO':            return [                ...state,                {                    id: action.id,                    text: action.text,                    isDo: false                }            ];        case 'TOGGLE_TODO':            return state.map( todo => {                return todo.id === action.id ? {...todo, isDo: !todo.isDo } : todo;            } );        default:            return state;    }} const showType = ( state = 'SHOW_ALL', action ) => {    switch ( action.type ) {        case 'SET_SHOW_TYPE':            return action.showType;        default:            return state;    }} const todoList = combineReducers({    todos,    showType})export { todoList } ...

November 2, 2019 · 1 min · jiezi

简化Reduxsaga

想一下,如果你需要写一个基于Redux 的项目,你需要重复的写非常多的Action Constants,非常多的Action Creator以做相当大一部分差不多相同的事情。 于是出现了为了帮你减少书写重复Constants及Action Creator 的库redux-act。 但只有Redux并不能完全满足我们的业务需求,毕竟SPA项目中总是需要从服务端获取数据的,于是这时候我们整合进来Redux-saga。 Redux-saga能非常好的帮助我们处理异步事件,但是同样的,Redux-saga需要书写许多的Action Creator并指定其Take类型再合并到Redux-Saga的入口处,而且这些Action Creator及effect非但需要一一让参数对应,还不方便做统一的事件处理。 于是Saga-action-creator诞生了。 Saga-action-creator的特性减少重复繁琐的书写Action creator直观的参数传递支持插件保留了Redux-saga的所有特性优秀的Typescript支持更方便的测试如何使用使用Saga-action-creator的方法非常简单,只需3步 一、定义Saga effects并导出import createSagaActions from 'saga-action-creator';import { takeLatest, call } from 'redux-saga/effects';import { getUserInfo, updateUser } from '../services/user';const user = createSagaActions({ // 一般情况下,你可以直接写一个Effect *getUserById(id: string): Generator<any, any, any> { yield call(getUserInfo, id); }, // 当然,如果你需要为某一个Effect指定take的类型 // 你可以传递一个对象,并指定takeType属性 updateUser: { takeType: takeLatest, *effect(id: string, name: string): Generator<any, any, any> { yield call(updateUser, id, { name }); }, },});export default user;二、创建连接器并合并Creatorsimport { createConnection, getLoadingPlugin } from 'saga-action-creator';import user from './user';import { takeLatest } from 'redux-saga/effects';const creator = createConnection({ // 合并creator creators: { user, }, // 默认情况下effect的take类型为 `takeEvery` // 如果你需要修改默认的类型可以传递这个参数 defaultTakeType: takeLatest, // 添加插件 plugins: { // 这里插件的key将作为后面getReducers的导出的key,则为store名 loading: getLoadingPlugin(), },});export default creator;三、将插件与Redux及Redux-saga进行连接import { createStore, combineReducers, applyMiddleware } from 'redux';import { all } from 'redux-saga/effects';import createSagaMiddleware from 'redux-saga';import creator from '../sagas';// 将插件导出的reducers与store连接const reducers = combineReducers({ ...creator.getReducers(),});const sagaMiddleware = createSagaMiddleware();// 将Effects与Redux-saga连接sagaMiddleware.run(function*() { yield all(creator.getEffects());});const store = createStore(reducers, applyMiddleware(sagaMiddleware));export type AppState = ReturnType<typeof reducers>;export default store;至此,saga-action-creator的连接动作就全部做完了。 ...

October 14, 2019 · 2 min · jiezi

react整合原生redux二

react整合原生redux(二)前言在react整合原生redux(一)已经完成了一个基本的应用框架,但是在分发action的时候,并不能在action里写逻辑,换言之action始终只是json对象,不能是一个函数,虽然可以在视图生命周期内写逻辑方法改变state的数值,但是长此以往,会造成项目臃肿,维护困难,所以react-thunk中间件由此而生项目创建参考 react整合原生redux(一) 增加依赖包yarn add redux-thunk -s src文件目录|-app.js|-store.js|-index.js|-actions.js 多了一个actions.js文件,里面存放带逻辑的action action.js内容// actions.js/** * redux-thunk action格式为一个函数,返回值是使用了dispatch的函数 * 基本格式 * function () { * return function (dispatch) { * dispatch(...) * } * } */export const fetchListAction = param => { return dispatch => { // 模拟异步请求请求数据(fetch,axios等) new Promise(resolve => { setTimeout(() => { const data = { code: 0, msg: "ok", data: { list: ["hello", "thunk"], param } }; resolve(data); }, 2000); }).then(result => { dispatch({ type: "SAVE", payload: result.data }); }); };};store改动引入redux-thunk ...

October 9, 2019 · 2 min · jiezi

react项目配置及redux使用流程详细记录

react项目配置及redux使用流程(详细记录)以TodoList为例,项目地址:https://github.com/mandyshen9... react项目创建及配置首先创建react项目:creact-react-app reactdemo修改默认配置:对 create-react-app 的默认配置进行自定义,这里我们使用 react-app-rewired(一个对 create-react-app 进行自定义配置的社区解决方案)。 $ yarn add react-app-rewired customize-cra修改package.json: /* package.json */"scripts": {- "start": "react-scripts start",+ "start": "react-app-rewired start",- "build": "react-scripts build",+ "build": "react-app-rewired build",- "test": "react-scripts test",+ "test": "react-app-rewired test",}然后在项目根目录创建一个 config-overrides.js 用于修改默认配置。 module.exports = function override(config, env){ // do staff with the webpack config... return config}配置按需加载:babel-plugin-import 是一个用于按需加载组件代码和样式的 babel 插件(原理),现在我们尝试安装它并修改 config-overrides.js 文件。 yarn add babel-plugin-import修改config-overrides.js文件: const { override, fixBabelImports } = require('customize-cra')module.exports = override( fixBabelImports('import',{ libraryName: 'antd', // 或其他第三方组件库名称 libiaryDirectory: 'es', // 组件位置 style: 'css', }))配置less配置less: 我们可以引入 customize-cra 中提供的 less 相关的函数 addLessLoader 来帮助加载 less 样式,同时修改 config-overrides.js 文件如下。 ...

September 9, 2019 · 3 min · jiezi

React入门从项目搭建webpack到引入路由reactrouter和状态管理reactredux

一、什么是ReactReact是什么?React的官网是这样介绍的:React-用于构建用户界面的 JavaScript 库。看起来十分简洁,React仅仅是想提供一个构建用户界面的工具,也就是只关心UI层面,不关心数据层面,数据层面的东西交给专门的人(react-redux)来做。所以有些人会说React就是一个ui框架。React 认为渲染逻辑本质上与其他 UI 逻辑内在耦合,比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。所以,React提供了jsx语法,也可以理解为让你在ul组件里写很多东西。jsx最终是通过babel将组件转化为React.createElement()函数来调用。组件化开发的乐趣需要慢慢体会。既然是入门,下面通过搭建项目-小例子-引入路由/状态管理简单介绍下react,有关复杂的概念这里暂不提及。 const name = 'Josh Perez';const element = <h1>Hello, {name}</h1>;ReactDOM.render( element, document.getElementById('root'));二、项目搭建上面说了,react的组件开发是需要babel进行编译的,浏览器是不能直接解析的,所以react项目一般都是需要做一些简单的配置的。当然,有很多的配置完善的脚手架可以使用,比如官方脚手架creat-react-app或者蚂蚁金服的dva框架,使用起来都是很方便上手。但是咱们我觉得通过简单搭建一个项目可以更好的理解react。下面将会通过webpack一步一步搭建一个可以使用的项目,但是具体的优化这里就先不考虑了。 1.打开终端、创建项目 mkdir react-demo && cd react-demo //创建文件夹,并进入文件夹2.初始化构建 npm init //为了后面下载npm包和node配置使用 //一路回车键就可以了!项目中多出来一个package.json文件3.创建项目入口 新建app.js文件,引入react和react-dom,新建一个index.html,包含<div id='app'></div>。 import React from 'react'; // 终端执行 npm i react 下载react import ReactDom from 'react-dom' // 终端执行 npm i react-dom 下载react-dom function App(){ //以函数的方式创建react组件 return <div>welcom,react-app</div> } ReactDom.render( //将组件App渲染挂载到页面的根节点app <App />, document.getElementById('app')//所以需要新建一个html文件提供app节点供组件挂载 )3.webpack配置 ...

August 27, 2019 · 2 min · jiezi

在redux中使用reactrouterredux-跳转路由

1.安装npm install --save historynpm install --save react-router-redux2.封装import {createStore, compose, applyMiddleware} from 'redux';import thunk from 'redux-thunk';import reducer from './reducer';import {routerMiddleware} from 'react-router-redux';let createHistory = require('history').createHashHistory;let history = createHistory(); // 初始化historylet routerWare = routerMiddleware(history);const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;const store = createStore(reducer, composeEnhancers( applyMiddleware(thunk, routerWare)));export default store;3.使用import {push} from 'react-router-redux';// 任意一个actionCreators.js文件// 登录export const loginSystem = (params) => async (dispatch) => { try { dispatch(changeLoading(true)); let {data} = await loginAsync(params); if (data['msgCode'] === 0) { dispatch(changeUserName(true, params['username'])); dispatch(push('/home')); // 跳转到home页面,其它都是示例代码,可忽略 } else { Toast.info(data['message'], 2); } dispatch(changeLoading(false)); } catch (error) { Toast.info(error, 2); }}

August 7, 2019 · 1 min · jiezi

解密Redux-从源码开始

Redux是当今比较流行的状态管理库,它不依赖于任何的框架,并且配合着react-redux的使用,Redux在很多公司的React项目中起到了举足轻重的作用。接下来笔者就从源码中探寻Redux是如何实现的。 注意:本文不去过多的讲解Redux的使用方法,更多的使用方法和最佳实践请移步Redux官网。源码之前基础概念随着我们项目的复杂,项目中的状态就变得难以维护起来,这些状态在什么时候,处于什么原因,怎样变化的我们就很难去控制。因此我们考虑在项目中引入诸如Redux、Mobx这样的状态管理工具。 Redux其实很简单,可以简单理解为一个约束了特定规则并且包括了一些特殊概念的的发布订阅器。 在Redux中,我们用一个store来管理一个一个的state。当我们想要去修改一个state的时候,我们需要去发起一个action,这个action告诉Redux发生了哪个动作,但是action不能够去直接修改store里头的state,他需要借助reducer来描述这个行为,reducer接受state和action,来返回新的state。 三大原则在Redux中有三大原则: 单一数据源:所有的state都存储在一个对象中,并且这个对象只存在于唯一的store中;state只读性:唯一改变state的方法就是去触发一个action,action用来描述发生了哪个行为;使用纯函数来执行修改:reducer描述了action如何去修改state,reducer必须是一个纯函数,同样的输入必须有同样的输出;剖析源码项目结构 抛去一些项目的配置文件和其他,Redux的源码其实很少很简单: index.js:入口文件,导出另外几个核心函数;createStore.js:store相关的核心代码逻辑,本质是一个发布订阅器;combineReducers.js:用来合并多个reducer到一个root reducer的相关逻辑;bindActionCreators.js:用来自动dispatch的一个方法;applyMiddleware.js:用来处理使用的中间件;compose.js:导出一个通过从右到左组合参数函数获得的函数;utils:两个个工具函数和一个系统注册的actionType;从createStore来讲一个store的创建首先我们先通过createStore函数的入参和返回值来简要理解它的功能: export default function createStore(reducer, preloadedState, enhancer) { // ... return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }}createStore接受三个参数: reducer:用来描述action如何改变state的方法,它给定当前state和要处理的action,返回下一个state;preloadedState:顾名思义就是初始化的state;enhancer:可以直译为增强器,用它来增强store的第三方功能,Redux附带的唯一store增强器是applyMiddleware;createStore返回一个对象,对象中包含使用store的基本函数: dispatch:用于action的分发;subscribe:订阅器,他将会在每次action被dispatch的时候调用;getState:获取store中的state值;replaceReducer:替换reducer的相关逻辑;接下来我们来看看createStore的核心逻辑,这里我省略了一些简单的警告和判断逻辑: export default function createStore(reducer, preloadedState, enhancer) { // 判断是不是传入了过多的enhancer // ... // 如果不传入preloadedState只传入enhancer可以写成,const store = createStore(reducers, enhancer) // ... // 通过在增强器传入createStore来增强store的基本功能,其他传入的参数作为返回的高阶函数参数传入; if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) } if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') } // 闭包内的变量; // state作为内部变量不对外暴露,保持“只读”性,仅通过reducer去修改 let currentReducer = reducer let currentState = preloadedState // 确保我们所操作的listener列表不是原始的listener列表,仅是他的一个副本; let currentListeners = [] let nextListeners = currentListeners let isDispatching = false // 确保我们所操作的listener列表不是原始的listener列表,仅是他的一个副本; // 只有在dispatch的时候,才会去将currentListeners和nextListeners更新成一个; function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } // 通过闭包返回了state,state仅可以通过此方法访问; function getState() { // 判断当前是否在dispatch过程中 // ... return currentState } // Redux内部的发布订阅器 function subscribe(listener) { // 判断listener的合法性 // ... // 判断当前是否在dispatch过程中 // ... let isSubscribed = true // 复制一份当前的listener副本 // 操作的都是副本而不是源数据 ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { if (!isSubscribed) { return } // 判断当前是否在dispatch过程中 // ... isSubscribed = false ensureCanMutateNextListeners() // 根据当前listener的索引从listener数组中删除来实现取掉订阅; const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } } function dispatch(action) { // 判断action是不是一个普通对象; // ... // 判断action的type是否合法 // ... // 判断当前是否在dispatch过程中 // ... try { isDispatching = true // 根据要触发的action, 通过reducer来更新当前的state; currentState = currentReducer(currentState, action) } finally { isDispatching = false } // 通知listener执行对应的操作; const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action } // 替换reducer,修改state变化的逻辑 function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } currentReducer = nextReducer // 此操作对ActionTypes.INIT具有类似的效果。 // 新旧rootReducer中存在的任何reducer都将收到先前的状态。 // 这有效地使用来自旧状态树的任何相关数据填充新状态树。 dispatch({ type: ActionTypes.REPLACE }) } function observable() { const outerSubscribe = subscribe return { // 任何对象都可以被用作observer,observer对象应该有一个next方法 subscribe(observer) { if (typeof observer !== 'object' || observer === null) { throw new TypeError('Expected the observer to be an object.') } function observeState() { if (observer.next) { observer.next(getState()) } } observeState() const unsubscribe = outerSubscribe(observeState) // 返回一个带有unsubscribe方法的对象可以被用来在store中取消订阅 return { unsubscribe } }, [$$observable]() { return this } } } // 创建store时,将调度“INIT”操作,以便每个reducer返回其初始状态,以便state的初始化。 dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }}从combineReducers谈store的唯一性仅靠上面的createStore其实已经可以完成一个简单的状态管理了,但是随着业务体量的增大,state、action、reducer也会随之增大,我们不可能把所有的东西都塞到一个reducer里,最好是划分成不同的reducer来处理不同模块的业务。 ...

July 15, 2019 · 4 min · jiezi

我为什么要用oop去写redux业务

前言是这么回事,笔者刚入门的时候就觉得写redux太难受了。写action,写types,写reducer,然后在action和reducer中注入type,等写完一个redux,可能5分钟就过去了。但是好像又没有什么办法。 const prepare = 'profile_prepare';const success = 'profile_success';const fail = 'profile_fail';const getProfile = (id) => { return { [CALL_API]: { uri: `/profile/${id}`, method: 'GET', types: [prepare, success, fail], } };};const reducer = (state = {}, action) => { switch (action.type) { case prepare: return { ...state, loading: true, }; break; case success: return { ...state, loading: false, action.response, }; break; case fail: return { ...state, loading: false, }; }};这种代码充斥在每一个action和reducer中。在实际项目中,可能会比这段demo更加复杂。 ...

July 6, 2019 · 1 min · jiezi

Redux简介

一般来说,当需要根据角色判断使用方式、与服务器大量交互 (例如使用 WebSocket)、视图需要从多个来源获取数据,也就是说在交互复杂、多数据源时;或者从组件的角度考虑,如果需要组件的状态广播等时需要使用。Redux 的设计思想A) Web 应用是一个状态机,视图与状态是一一对应;B) 所有的状态,保存在一个对象里面。 可以简单将 Redux 理解为是 JavaScript 的状态容器: 应用中所有的状态都是以一个对象树的形式存储在一个单一的 store 中;当你想要改变应用的中的状态时,你就要 dispatch 一个 action,这也是唯一的改变 state 的方法;通过编写 reducer 来维护状态,返回新的 state,不直接修改原来数据;Redux的工作流首先由view dispatch拦截action,然后执行对应reducer并更新到store中,最终views会根据store数据的改变执行界面的刷新渲染操作。 同时,作为一款应用状态管理框架,为了让应用的状态管理不再错综复杂,使用Redux时应遵循三大基本原则,否则应用程序很容易出现难以察觉的问题。这三大原则包括: 单一数据源整个应用的State被存储在一个状态树中,且只存在于唯一的Store中。 State是只读的对于Redux来说,任何时候都不能直接修改state,唯一改变state的方法就是通过触发action来间接的修改。而这一看似繁琐的状态修改方式实际上反映了Rudux状态管理流程的核心思想,并因此保证了大型应用中状态的有效管理。 应用状态的改变通过纯函数来完成Redux使用纯函数方式来执行状态的修改,Action表明了修改状态值的意图,而真正执行状态修改的则是Reducer。且Reducer必须是一个纯函数,当Reducer接收到Action时,Action并不能直接修改State的值,而是通过创建一个新的状态对象来返回修改的状态。 Redux中的基本概念1.Store Store 就是保存数据的地方,可以把它看成一个容器,整个应用只能有一个 Store ; Redux 通过提供的 createStore() 这个函数来生成 Store 。 import { createStore } from 'redux';const store = createStore(fn);其中 createStore() 函数接受另一个函数作为参数,返回新生成的 Store 对象。 2.State Store 对象包含所有数据,如果想得到某个时点的数据,就要对 Store 生成快照,这种时点的数据集合,就叫做 State 。 当前时刻的 State 可以通过 store.getState() 拿到 import { createStore } from 'redux';const store = createStore(fn);const state = store.getState();Redux 规定,state 和 view 一一对应,一个 State 对应一个 View,只要 State 相同,View 就相同;反之亦然。 ...

July 5, 2019 · 2 min · jiezi

wepyredux

wepy-reduxredux文件 typetypes里是触发action的函数名称 只是存储函数名字 按照模块去创建type.js //base.jsexport const GETALLHOMEINFO = 'GETALLHOMEINFO'写好了函数名称 在index.js中export出来 export * from './counter'export * from './base'reducers随着应用变得复杂,需要对 reducer 函数 进行拆分,拆分后的每一块独立负责管理 state 的一部分这个时候多个模块的reducer通过combineReducers合并成一个最终的 reducer 函数, import { combineReducers } from 'redux'import base from './base'import counter from './counter'export default combineReducers({ base, counter})模块使用handleActions 来处理reducer,将多个相关的reducers写在一起handleActions有两个参数:第一个是多个reducers,第二个是初始state GETALLHOMEINFO reducer是将异步action返回的值赋值给data //base.jsimport { handleActions } from 'redux-actions'import { GETALLHOMEINFO } from '../types/base'const initialState = { data: {}}export default handleActions({ [GETALLHOMEINFO] (state, action) { return { ...state, data: action.payload } }}, initialState)actionsactions是对数据的处理 在index.js中export出来 ...

July 3, 2019 · 1 min · jiezi

React脚手架搭建

前言之前的 multi-spa-webpack-cli 只是为 React + antd 模板提供了开发时必要的环境,对于实际的开发并没有什么用处。为了更贴近实际开发,本次 React + antd 模板完善了一些功能。 封装 fetch,新增请求错误提示;集成 react-router-dom 路由管理;集成 react-redux 状态管理;必不可少的 antd 集成;node 服务集成(可选)。node 服务和 React+antd 模板是没有多大的关系的。本文只是想通过一个简单的登陆逻辑来演示以上的功能,所以 node 服务不是必须的。 multi-spa-webpack-cli 已经发布到 npm,只要在 node 环境下安装即可。 npm install multi-spa-webpack-cli -g使用步骤如下: # 1. 初始化项目multi-spa-webpack-cli init spa-project<center> </center> # 2. 进入文件目录cd spa-project# 3. 安装依赖npm install# 4. 打包不变的部分npm run build:dll# 5. 启动项目(手动打开浏览器:localhost:8090)npm start# 6. 启动服务(可选)cd servernpm installnpm start预览: <center> </center> 封装 fetch现在处理异步的方式,大多数基于 Promise 的。而 fetch 是天然支持 Promise 的,所以无需再手动封装。在 PWA 技术中,已作为一个重要的组成部分在使用。 ...

July 1, 2019 · 2 min · jiezi

React搭建个人博客一项目简介与React前端踩坑

一.简介项目最开始目的是为了熟悉React用法,之后加入的东西变多,这里分成三部分,三篇博客拆开来讲。 前端部分 [x] React[x] React-Router4.0[x] Redux[x] AntDesign[x] webpack4后端部分 [x] consul+consul-template+nginx+docker搭建微服务[x] cdn上传静态资源[x] thinkJs运维部分 [x] daocloud自动化部署[x] Prometheus+Grafana监控系统博客网站分为两个部分,前台博客展示页面,后台管理页面。先看下效果:前台页面:也可以看我线上正在用的博客前台,点这里后台页面: 这一篇只讲前端部分功能描述 [x] 文章列表展示[x] 登录管理[x] 文章详情页展示[x] 后台文章管理[x] 后台标签管理[x] MarkDown发文项目结构前后台页面项目结构类似,都分为前端项目和后端项目两个部分。前端项目开发环境使用webpack的devserver,单独启动一个服务,访问后端服务的接口。生产环境直接打包到后端项目指定目录,线上只启动后端服务。前台页面:前端项目代码地址 在这里。后端项目代码地址在这里。后台页面:前端项目代码地址 在这里。后端项目代码地址在这里。 二.React踩坑记录这里讲一下项目中React踩坑和一些值得留意的问题。 1.启动报错如下:下图是一开始blog-react项目一个报错因为项目里我使用webpack来运行项目,以及添加相关配置,而不是刚建好项目时的react命令,所以一开始启动的时候会报 Module build failed: SyntaxError: Unexpected token 错误。说明ReactDom.render这句话无法解析。解决方法,添加babel的react-app预设,我直接加到了package.json里。 "scripts": { "start": "cross-env NODE_ENV=development webpack-dev-server --mode development --inline --progress --config build/webpack.config.dev.js", "build": "cross-env NODE_ENV=production webpack --env=test --progress --config ./build/webpack.config.prod.js", "test": "react-scripts test", "eject": "react-scripts eject" }, "babel": { "presets": [ "react-app" ] },2.React 组件方法绑定this问题React跟Vue不一样的一点是,组件里定义的方法,如果直接调用会报错。比如: ...

June 19, 2019 · 5 min · jiezi

深入浅出redux知识

redux状态管理的容器。 开始使用// 定义常量const INCREMENT = 'INCREMENT'const DECREMENT = 'DECREMENT'// 编写处理器函数const initState = { num: 0 }function reducer(state = initState, action) { switch (action.type) { case INCREMENT: return { num: state.num + 1 } case DECREMENT: return { num: state.num - 1 } default: return state }}// 创建容器const store = createStore(reducer)reducer函数需要判断动作的类型去修改状态,需要注意的是函数必须要有返回值。此函数第一个参数是 state 状态,第二个参数是 action 动作,action 参数是个对象,对象里面有一个不为 undefined 的 type 属性,就是根据这个属性去区分各种动作类型。在组件中这样使用 const actions = { increment() { return { type: INCREMENT } }, decrement() { return { type: DECREMENT } }}class Counter extends Component { constructor(props) { super(props); // 初始化状态 this.state = { num: store.getState().num } } componentDidMount() { // 添加订阅 this.unsubscribe = store.subscribe(() => { this.setState({ num: store.getState().num }) }) } componentWillUnmount() { // 取消订阅 this.unsubscribe() } increment = () => { store.dispatch(actions.increment()) } decrement = () => { store.dispatch(actions.decrement()) } render() { return ( <div> <span>{this.state.num}</span> <button onClick={this.increment}>加1</button> <button onClick={this.decrement}>减1</button> </div> ); }}我们都知道组件中的 state 和 props 改变都会导致视图更新,每当容器里面的状态改变需要修改 state,此时就需要用到 store 中的 subscribe 订阅这个修改状态的方法,该方法的返回值是取消订阅,要修改容器中的状态要用store 中的 dispatch 表示派发动作类型,store 中的 getState 表示获取容器中的状态。bindActionCreators为了防止自己手动调用 store.dispatch ,一般会使用redux的这个 bindActionCreators 方法来自动绑定 dispatch 方法,用法如下。 ...

June 13, 2019 · 4 min · jiezi

react-使用-redux-的时候-用-ref获取子组件的state

由于 redux是无状态的,所以当我们在子组件中使用了 redux的时候,再父组件中,使用 ref 来获取子组件的state时,发现为一个空对象。 其实这个是有解决方案法的,原因在于 我们使用的 redux里面的 connect 是有四个参数的 前两个经常用,文档也比较多,这里就不说了 connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) 这里直接说第三个参数, mergeProps(stateProps, dispatchProps, ownProps) 该参数非必须,redux默认会帮你把更新维护一个新的props对象,类似调用Object.assign({}, ownProps, stateProps, dispatchProps)。 当不想传第三个参数时可以传 null 重点在第四个参数 options, 通过查看源码,可以看见 所以,我们可以在子组件中 父组件中

June 11, 2019 · 1 min · jiezi

初探React技术栈二

reduxredux是js的状态容器,提供可预测的状态管理,同时可运行于不同的环境并且还有redux-devtools供可视化调试,大型应用下有良好的跨组件通讯与状态管理是必不可少的,那么就在本章中探索redux是如何与react串联,并是如何使用redux $ npm or cnpm$ npm install redux react-redux相信有不少人都比较好奇 为什么我已经有了redux还要再多引入一个react-redux实际上这样是为保证核心功能最大程度上的跨平台复用首先新建一个文件夹store用来存放redux的配置及相关文件,看一下store中的文件结构 .├── actions│   └── index.js├── index.js└── reducers └── index.js2 directories, 3 files# 其中最外层的index.js是主要的配置文件在react的入口文件index.js中引入react-redux Provider是react-redux两个核心工具之一,作用:将store传递到项目的组件中,并可以在组件中使用reduximport一般引入文件夹会默认找到该文件夹中的index.js store.js reudx 主文件import { applyMiddleware, createStore } from 'redux'import thunk from 'redux-thunk'import reducers from './reducers/index'let store = createStore( reducers, applyMiddleware(thunk))export default storeredux中以createStore创建store并在其中插入reducers与中间件,redux-thunk是为了增强action在原生的redux中action只能返回对象,但是加上这个中间件就可以返回一个函数并且可以访问其他action action// 测试actionfunction test (text) { return { type: 'test', text: text }}export default { test}reducerimport { combineReducers } from 'redux'const Initstate = { // 初始状态}const Common = (state = Initstate, action) => { switch (action.type) { case 'test': return {...state, test: action.text} default: return state }}let reducer = combineReducers({ Common: Common})// 注意combineReducers是用于合并多个reducer 当所用模块不多,协作少时 可以不用从reducer中我们就可以发现redux的三大原则: 1.单一数据源: 所谓的单一数据源是只有一个Model虽然我们可以定义多个 reducer 但是经过combineReducers 整合发现 所有的 Model 都存在一个大的JSON里面 ...

June 8, 2019 · 1 min · jiezi

UI2CODE再进化结合Redux的框架升级

摘要: 自从有了ui2code,妈妈再也不用担心我加班背景UI2CODE的目标是通过分析视觉稿得到对应的代码,让AI提高开发效率。然而过去静态化页面的产出,不能得到业务场景的需求。针对于此,我们以UI2CODE自动化开发为基底,结合Redux的消息机制,将自动化生成的维度提升到页面的处理。 透过框架,可自动化生成页面代码,并且具有数据驱动展示、消息派送等动态性能力。期望在复杂的业务场景下,简化开发的工作。并且在使用一致化的架构下,避免未来业务代码耦合严重,使代码分工明确,容易修改维护。 进化后的UI2CODE? 开发者可以透过Intellij Plugin分析视觉稿后会生成对应的视图代码,以及和此页面框架结合的能力。 在整体开发的定位上我们的野心是,提供一套可扩充的页面消息框架,并且自动生成大部分的UI代码。目标带来以下的好处: 快速建构新应用,框架将完成大部分的事,业务开发同学只要专注于业务代码让开发人员的进入门槛降低,在我们落地的经验中,后端同学只要有基本的概念,无需花费太多经历,可直接上手帮忙写代码让页面的架构统一化,让页面的开发有统一的规则,也方便后续的维护提供通用的组件库,可重复利用核心设计思路我们在设计上主要参考于MVP、CLEAN、VIPER以及FISH_REDUX等框架。目的在实践高聚合低耦合的前提下,分拆为视图组装层、视图展示层、数据层、事件交互层。层层分离职责,不互相干扰,又可互相通讯。 分层拆开的好处为容易维护,并且可以单元测试"业务展示"以及"业务逻辑",框架上清晰,容易有一个统一的遵循规则,达到简单编写重复可利用。 UI2CODE页面框架的设计概念为,主要分为Page、Card、Reaction三大元素。在上层的Page负责组装页面,制定页面的风格。Card则为可重复利用的视图展示元素。Reaction则为事件反应的监听。 在整个页面框架上,可以透过UI2CODE Plugin分析自动化生成业务UI,产生出Page、Card、Card(DataModel)。仅需修改Card上额外的业务展示,以及撰写Reaction中的业务逻辑。 具体实现架构在介绍框架组件前,先理解UI2CODE的基本组成页面目录如下: 以Page为单位,页面本体demo_page为其他页面路由调用的起点,透过设置Config.dart决定内部含的卡片列表以及事件处理列表,组合出Card以及Reaction的关联。 其详细的架构关系如下: PagePage为框架基础的单位,为单一页面,负责决定视图的组装以及页面的样式(Template)。 在Page之内可包含若干的Card以及Reaction,分别为视图的展示以及视图的事件处理。可以很清晰地将业务场景做分割成小模块,不互相影响。 Abstract class PageStatelessWidget extends StatelessWidgetimplements Lifecycle 可由UI2CODE Plugin自动化产生框架统一分发管理页面生命周期Lifecycle透过设定Template指定页面要呈现的样版,或者修改如背景等属性透过设定Config指定这个页面含有的Cards和Reactions透过设定PageState可添加额外的数据 Page TemplateTemplate样板为页面的抽象化,在整体页面上分为多个样板可选择。并且支持设置AppBar(非必选)、Header(非必选)、Body、Stack(非必选)等子样板。 样板可比喻为页面的容器,目前支持以下样板,并且可扩充: PageTemplate,通用页面容器,并支持NestedScrollView的Silver Header滚动(若需要)PageBottomNavigatorTemplate,含有底部导航的容器,如首页PageSwitchTabTemplate,含有分页Tab功能的容器各个子样板也有相对应的Template可选择,如在Body内的样板功能为决定内部Cards排列的方式。举例来说,BodyListViewTemplate则是列表展示。 使用Template最大的好处为减少开发工作,可直接使用封装后的接口。并且页面内的所有样板将共用消息机制,可以互相传递消息,如Body内部的卡片很容易发送消息给AppBar等。这是框架上的有力之处。 Page ConfigConfig决定页面的组装,包含了元件有哪些,以及事件处理反射的类绑定。 Extends PageConfig 可由UI2CODE Plugin自动化产生透过设定cards注册这个页面所载入的卡片透过设定actions注册这个页面所响应的类,支持卡片事件以及页面事件支持额外设置AppBar、Header、Stack等组件(非必须)如何绑定,举例来说: void actionConfig(List< List < Function>> actions) {//卡片Card8575, 响应事件的类为Card8575Reactionactions.add(< Function>[() => Card8575, () => new Card8575Reaction()]);} CardCard代表基本的视图展示,业务UI,其中包含了View widget以及DataModel数据。框架内会将两者Data binding起来,由数据来驱动最终展示的呈现。达到如MVP中View和ViewModel的隔离效果。 Abstract class BaseCard<T extends DataModel> extends StatefulWidget 可由UI2CODE Plugin分析视觉稿产生透过BaseCard<T extends DataModel>的标准化,指定数据DataModel绑定Card可以发出事件,不直接操控数据,而让接收者响应 ...

June 6, 2019 · 1 min · jiezi

react-redux-基本用法

使用redux 目的在react中组件与组件之间的通信很麻烦,于是借用redux进行第三方的通信,通过把数据存储在store里,实现各个组件间快速通信redux 基础1. 核心 state:普通对象action:JS 普通对象,用来描述发生了什么,store 数据的唯一来源reducer:把 action 和 state 串起来。接收 state 和 action 作为参数,并返回新的 state 的函数。2. 三大原则 单一数据源:只存在唯一一个storestate只读:唯一改变 state 的方法就是触发 action使用纯函数进行修改:reducer3. 主要组件 action 通过dispatch传递数据到storereducer 描述如何响应action更新statestore 维持应用的 state; 提供 getState() 方法获取 state; 提供 dispatch(action) 方法更新 state; 通过 subscribe(listener) 注册监听器; 通过 subscribe(listener) 返回的函数注销监听器。安装reduxnpm install redux --S准备工作1. 创建 store // store/index.jsimport {createStore} from 'redux';import reducer from './reducer';const store = createStore( reducer,);export default store;2. 创建 reducer // store/reducer.js// 初始 stateconst initState={ inputValue:'', list:[]};// reducer可以接收state,但是绝不能修改state,返回的是新的stateexport default (state = initState,action)=>{ return state;}流程 ...

June 4, 2019 · 1 min · jiezi

react-redux-学习

安装 npm install redux 核心概念用户触发action store 自动调用reducer 更新state store 用来存储整个应用的 statestate 一个用来描述应用状态的对象action 一个对象用来描述程序发生的动作reducer 是更改state的纯函数接受 state 和action参数最后返回新的state下面我会对每个概念进行介绍 三大原则单一的数据来源,整个应用程序的状态存储在单个store 中state 对象是只读的改变state 的唯一方式是发出一个action如何通过action改变state,通过编写春函数reducer 来实现Actionsaction是将数据发送的store的有效负载,他们是store数据的唯一来源通过 store.dispatch()将数据发送的store action 是一个普通的js对象,action必须指定一个 type 属性用来描述执行的操作。 { type: 'ADD_TODO', text: 'Build my first Redux app'}Action Creatorsaction creators 是返回一个action 的方法,这样写可以方便开发和测试。 function addTodo(text) { return { type: ADD_TODO, text }}dispatch(addTodo('eat'))ReducersReducers是指定状态树如何响应 dispatch 到store 的action的,action只是用来描述发生的动作,不描述状态的变化。 首先确定state的数据的格式 { todos: [ { text: 'Consider using Redux', completed: true } ]}确定state的数据格式之后我们就可以编写一个reducer,reducer 是一个纯函数接收state和action返回新的state const initialState = { todos: []}function todoApp(state = initialState, action) { switch (action.type) { case 'ADD_TODO': return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) default: return state }}注意我们使用Object.assign()返回一个新的状态,不改变参数中的state 的值分割 Reducers当我们应用程序的action比较多每个action在reducers中的逻辑比较复杂的时候,如果还像我们上面一直使用 switch case 可以想象我们的reducers会变的非常的长,此时我们就需要对一些复杂的操作进行分割,分割的规则是根据 state 的节点操作进行分割,针对每个节点的操作写一个reducers然后使用 combineReducers 方法进行合并。 ...

June 4, 2019 · 2 min · jiezi

React项目集成Immutablejs

1、前言本文章项目的依赖包及其版本如下: Package NameVersionantd^3.16.6connected-react-router^6.4.0customize-cra^0.2.12immutable^4.0.0-rc.12react^16.8.6react-app-rewired^2.1.1react-redux^7.0.3react-router-config^5.0.0react-router-dom^5.0.0react-scripts3.0.1redux^4.0.1redux-logger^3.0.6redux-persist^5.10.0redux-persist-expire^1.0.2redux-persist-transform-immutable^5.0.0redux-saga^1.0.22、准备工作,搭建项目下面是我的项目结构,每个人或者每个公司都有自己的目录架构,这里我的只供大家参考,另外搭建项目过程和介绍如何使用immutable.js不是本文章的重点,如何使用immutable.js以及本文章相关代码后面我会给出,如果有疑问欢迎大家在下面留言 |-- App.js|-- index.js|-- serviceWorker.js|-- assets| |-- audio| |-- css| | |-- App.scss| | |-- base.scss| | |-- index.css| | |-- override-antd.scss| |-- image| | |-- Welcome.png| | |-- awbeci.png| | |-- bgLogo.png| | |-- hiy_logo.png| | |-- indexPop1.png| | |-- indexPop2.png| | |-- logo.png| | |-- logoX.png| | |-- right.png| |-- video|-- components| |-- HOC| | |-- loading.js| |-- common| |-- layout| |-- AppRoute.js| |-- LayoutPage.js| |-- Loading.js| |-- MasterPage.js| |-- RouterView.js| |-- SideMenu.js| |-- layoutPage.scss| |-- masterPage.scss|-- config| |-- base.conf.js|-- context| |-- themeContext.js|-- pages| |-- DepartmentManage.js| |-- Index.js| |-- NoFound.js| |-- NoPermission.js| |-- UserManage.js| |-- login| |-- Login.js| |-- login.scss|-- redux| |-- actions| | |-- authAction.js| | |-- layoutPageAction.js| |-- middleware| | |-- authTokenMiddleware.js| |-- reducers| | |-- authReducer.js| | |-- index.js| | |-- layoutPageReducer.js| |-- sagas| | |-- authSaga.js| | |-- index.js| |-- store| | |-- index.js| |-- thunks|-- router| |-- index.js|-- service| |-- apis| | |-- 1.0| | |-- index.js| | |-- urls.js| |-- mocks| | |-- 1.0| | |-- index.js| | |-- testMock.js| |-- request| |-- ApiRequest.js| |-- MockRequest.js|-- test| |-- App.test.js|-- utils3、集成immutable.js此项目除了依赖包要配置之外,只有redux下的reducer相关文件会设置成immutable.js普通的react组件我没有设置成immutable.js ...

June 1, 2019 · 4 min · jiezi

2019-最新-ReactNativeTypeScriptReduxSaga-实践

最近研究 React Native、Redux Saga 以及 TypeScript 相关的内容,整理成了一个 React Native Template,可以直接使用下面的命令创建一个新的应用: react-native init MyApp --template=parcmg初始化完成之后,按下面的方式执行命令: cd MyAppnode setup.jsnpm installreact-native link react-native-gesture-handler完成之后,即可像往常一样开发了: react-native run-ios模板还在完善中,另外,相关技术要点与总结,稍后有时间再整理一下。

May 31, 2019 · 1 min · jiezi

使用-hooks-和-connect-访问同一个-store

React Hooks 距离正式发布已经过去好几个月了,redux,mobx,也都支持了 Hooks 的用法,那么有没有可能用 React Context API & Hooks 来实现一个同时支持 class component 和 functional component 访问的 store 呢?答案是肯定的。 既然我们是基于 Context Api,那么先来创建一个 context 对象 // store.jsimport React from 'react'const initialStore = {}const StoreContext = React.createContext(initialStore) 接着我们来构造两种访问 store 的方法: Hooks 的方式: // store.jsimport {useReducer, useContext} from 'react'// 声明 reducerexport const reducer = (state, action) => { switch (action.type) { case 'set': return { ...state, ...action.payload } case 'reset': return {} default: throw new Error( `action: '${action.type}' not defined` ) }}// 基于原生 Hooks 实现export const useStore = () => { return useReducer(reducer, useContext(StoreContext))}HOC 的方式:HOC 需要有一个上下文环境才可以访问 store,所以我们先来构造 provider ...

May 27, 2019 · 2 min · jiezi

在-ReactNative-中持久化-redux-数据

在最近的一个项目中,要求对 redux 数据做持久化处理,经过研究后成功实现,在此记录一下过程 我们可以使用 redux-persist 对数据做持久化处理 安装npm i --save redux-persist使用安装成功后,我们需要对 store 代码进行修改,这是我的 store 生成文件 import {applyMiddleware, createStore, compose} from 'redux';import {createLogger} from 'redux-logger';import thunk from 'redux-thunk';import reducers from '../reducers';import {persistStore, persistReducer} from 'redux-persist';import storage from 'redux-persist/lib/storage'const persistConfig = { key: 'milk', # 对于数据 key 的定义 storage, # 选择的存储引擎}# 对 reducers 的封装处理const persistedReducer = persistReducer(persistConfig, reducers)let loggerMiddleware = createLogger();export default function configureStore() { const enhancers = compose( applyMiddleware(thunk, loggerMiddleware), ); # 处理后的 reducers 需要作为参数传递在 createStore 中 const store = createStore(persistedReducer, enhancers) # 持久化 store let persistor = persistStore(store) return {store, persistor}}在 react-native 中,存储引擎默认为 AsyncStorage Android是以key=>value的形式存储在本地sqlite中iOS 是直接存沙盒文件 ...

May 22, 2019 · 1 min · jiezi

Remath-Redux-的重新设计

难道现在状态管理不是一个可以解决的问题吗?直观地说,开发人员似乎知道一个隐藏的事实:状态管理的使用似乎比需要的更困难。在本文中,我们将探讨一些你可能一直在问自己的问题: 你是否需要一个用于状态管理的库?Redux 的受欢迎程度是否值得我们去使用? 为什么或者为什么不值得?我们能否制定更好状态管理解决方案吗?如果能,要怎么做?状态管理需要一个库吗作为前端开发人员,不仅仅是布局,开发的真正艺术之一是知道如何管理存储状态。简而言之:状态管理是复杂的,但又并非那么复杂。 让我们看看使用React等基于组件的视图框架/库时的选项: 1. Component State (组件状态)存在于单个组件内部的状态。在React中,通过setState方法更新state。 2. Relative State (关联状态)从父级传递给子级的状态。在React中,将 props 作为属性传递给子组件。 3. Provided State (供给状态)状态保存在根 provider (提供者) 组件中,并由 consumer (消费者) 在组件树的某个地方访问,而不考虑组件之间的层级关系。在 React 中,通过 context API 可以实现。 大多数的状态都是存在于视图中的,因为它是用来反映用户界面的。那么,对于反映底层数据和逻辑的其它状态,又属于谁呢? 将所有内容都放在视图中可能会导致关注点的分离:它将与javascript视图库联系在一起,使代码更难测试,而且可能最大的麻烦是:必须不断地思考和调整存储状态的位置。 状态管理由于设计变更而变得复杂,而且通常很难判断哪些组件需要哪些状态。最直接的选择是从根组件提供所有状态,如果真要这么做的话,那么选用下一种方式会更好。 4. External State (外部状态)状态可以移出视图库。然后,库可以使用提供者/消费者模式连接以保持同步。 也许最流行的状态管理库是Redux。在过去的两年里,它变得越来越受欢迎。那么为什么这么喜欢一个简单的库呢? Redux 更具性能?答案是否定的。事实上,为了每一个必须处理的新动作(action),都会稍微慢一些。 Redux是否更简单?当然不是。 简单应当是纯javascript:比如 TJ Holowaychuk 在twitter上说 那么为什么不是每个人都使用 global.state={}? 为什么使用 Redux在表层之下,Redux 与 TJ 的根对象{}完全相同——只是包装在了一系列实用工具的管道(pipeline)中。 在 Redux 中,不能直接修改状态。只有一种方法:派发(Dispatch)一个动作(Action)到管道中,管道会自动根据动作去更新状态。 沿着管道有两组侦听器:中间件(middleware)和订阅(subscriptions)。 中间件是可以侦听传入的动作的函数,支持诸如“logger”,“devtools”或“syncWithServer”侦听器之类的工具。 订阅是用于广播这些状态更改的函数。 最后,合成器(Reducer)函数负责把状态变更拆分成更小、更模块化、更容易管理的代码块。 和使用一个全局对象相比,Redux 确实简化了开发过程。将 Redux 视为一个带有更新前/更新后钩子的全局对象,以及能够以简单的方式合成新状态。 ...

May 15, 2019 · 2 min · jiezi

如何设计redux-state结构

为什么使用redux使用react构建大型应用,势必会面临状态管理的问题,redux是常用的一种状态管理库,我们会因为各种原因而需要使用它。 不同的组件可能会使用相同的数据,使用redux能更好的复用数据和保持数据的同步react中子组件访问父组件的数据只能通过props层层传递,使用redux可以轻松的访问到想要的数据全局的state可以很容易的进行数据持久化,方便下次启动app时获得初始statedev tools提供状态快照回溯的功能,方便问题的排查但并不是所有的state都要交给redux管理,当某个状态数据只被一个组件依赖或影响,且在切换路由再次返回到当前页面不需要保留操作状态时,我们是没有必要使用redux的,用组件内部state足以。例如下拉框的显示与关闭。 常见的状态类型react应用中我们会定义很多state,state最终也都是为页面展示服务的,根据数据的来源、影响的范围大致可以将前端state归为以下三类: Domain data: 一般可以理解为从服务器端获取的数据,比如帖子列表数据、评论数据等。它们可能被应用的多个地方用到,前端需要关注的是与后端的数据同步、提交等等。UI state: 决定当前UI如何展示的状态,比如一个弹窗的开闭,下拉菜单是否打开,往往聚焦于某个组件内部,状态之间可以相互独立,也可能多个状态共同决定一个UI展示,这也是UI state管理的难点。 App state: App级的状态,例如当前是否有请求正在loading、某个联系人被选中、当前的路由信息等可能被多个组件共同使用到状态。 如何设计state结构在使用redux的过程中,我们都会使用modules的方式,将我们的reducers拆分到不同的文件当中,通常会遵循高内聚、方便使用的原则,按某个功能模块、页面来划分。那对于某个reducer文件,如何设计state结构能更方便我们管理数据呢,下面列出几种常见的方式: 1.将api返回的数据直接放入state这种方式大多会出现在列表的展示上,如帖子列表页,因为后台接口返回的数据通常与列表的展示结构基本一致,可以直接使用。 2.以页面UI来设计state结构如下面的页面,分为三个section,对应开户中、即将流失、已提交审核三种不同的数据类型。因为页面是展示性的没有太多的交互,所以我们完全可以根据页面UI来设计如下的结构: tabData: { opening: [{ userId: "6332", mobile: "1858849****", name: "test1", ... }, ...], missing: [], commit: [{ userId: "6333", mobile: "1858849****", name: "test2", ... }, ... ]}这样设计比较方便我们将state映射到页面,拉取更多数据,也只简单contact进对应的数组即可。对于简单页面,这样是可行的。 3.State范式化(normailize)很多情况下,处理的数据都是嵌套或互相关联的。例如,一个群列表,由很多群组成,每个群又包含很多个用户,一个用户可以加入多个不同的群。这种类型的数据,我们可以方便用如下结构表示: const Groups = [ { id: 'group1', groupName: '连线电商', groupMembers: [ { id: 'user1', name: '张三', dept: '电商部' }, { id: 'user2', name: '李四', dept: '电商部' }, ] }, { id: 'group2', groupName: '连线资管', groupMembers: [ { id: 'user1', name: '张三', dept: '电商部' }, { id: 'user3', name: '王五', dept: '电商部' }, ] }]这种方式,对界面展示很友好,展示群列表,我们只需遍历Groups数组,展示某个群成员列表,只需遍历相应索引的数据Groups[index],展示某个群成员的数据,继续索引到对应的成员数据GroupsgroupIndex即可。但是这种方式有一些问题: ...

May 12, 2019 · 3 min · jiezi

从设计的角度看-Redux

你知道 Redux 真正的作用远不止状态管理吗? 你是否想要了解 Redux 的工作原理? 让我们深入研究 Redux 可以做什么,它为什么做它的事情,它的缺点是什么,以及它与设计有哪些关联? 你听说过 Redux 吗?它是什么? 请不要用 Google 搜索 花哨的后端的东西我听说过它,但我不知道它是什么,这可能是一个 React 框架是一种在 React 应用中存储管理状态的更好方式这个问题,我问过 40 多位设计师,以上是他们的经典回答。他们中的许多人都知道 Redux 与React 一起工作,它的工作是状态管理。 本文的目的就是让你对 Redux 有更全面的认知: 它能做什么?为什么它要这样设计?何时使用它?以及它与设计有哪些关联? 我的目标是帮助像你们这样的设计师。即使您以前没有写过一行代码,我认为理解 Redux仍然是可能的、有益的和有趣的。 什么是 Redux在超高水平上,Redux 是开发人员用来简化他们工作的工具。你们很多人可能都听说过,它的工作是状态管理。稍后我将解释状态管理的含义, 此刻,我只能想让你看下面这张图: 为什么要了解 ReduxRedux 更多的是关于应用程序的内部工作而不是它的外观和感受。 这是一个有点复杂的工具,学习曲线相对陡峭,但这是否意味着我们作为设计师应该远离它? 不。我认为我们应该拥抱它。汽车设计师应该了解引擎的用途,对吗?为了成功地设计应用程序界面,设计师还应该对底层的东西有扎实的了解。我们应该了解它可以做什么,理解开发人员为什么使用它,并了解它的优势和含义。 Redux 可以做什么开发人员在 React 应用中使用 Redux 来管理状态。这最常见的用法,Redux 改进了React(尚未)做得不好的方面。 然而,你很快就会发现 Redux 的真正功能远远不止于此,让我们从了解状态管理的真正含义开始。 状态管理如果你不确定这个状态意味着什么,让我们用一个更通用的术语来替换它:数据。状态是不断变化的数据,状态决定在用户界面上显示什么。 状态管理是什么意思? 一般来说,我们需要在应用程序中管理三个方面的数据 获取和存储数据将数据绑定到 UI 元素改变数据比如我们要做一个 Dribbble 的作品页面。在作业页面上我们想要展示的数据有哪些?其中包括作者的头像照片、名称、动态 GIF 图片、点赞数量、评论,以及等等。 首先,我们需要从云服务器获取所有这些数据并将其放在某个位置。接下来,我们需要实际显示数据。我们需要将这些数据分配给对应的 UI 元素,这些 UI 元素表示我们在浏览器中实际看到的内容。例如,我们将头像照片的 URL 分配给 img 标签的 src 属性: ...

April 25, 2019 · 2 min · jiezi

redux 闲谈

redux 闲谈起因: 在与涂鸦智能一个web工程师交流过程中,他询问我dispatch一个action,是如何和reducer 绑定的,dispatch(actionA)只会触发reducerA却不会去触发reducerB.Github https://github.com/reduxjs/redux redux 数据流程redux 遵循严格的单向数据流,以React为例如下图: (网图,侵删) 通过用户在ViewUI 进行一个dispatch(action);Store内部自动通过如下形式Reducer(prevState, action)调用Reducer返回新的State(newState), state变化后调用Store上的监听器(store.subscribe(listener))在listener内部可以通过 store.getState() 方式得到最新的state进行数据操作初始化redux 的 Store 初始化通过 createStore 方法来进行初始化 const store = createStore(combineReducers, prevState, compose(applyMiddleware(...middleware)))combineReducers 合并后的reducer,reducer 形式如下function authReducer(state, action) { switch(action.type) { case 'login': return { ...state, isLogin: true } default: return {...state} }}function userReducer(state, action) { // ...如上}通过使用combineReducers({ authReducer, userReducer }) 返回一个reducers prevState 则为reducer 中state 的初始化默认值,这里默认值为整个状态树的默认值middleware 则为redux 中间件, 增强redux 功能该部分初始化流程阶段,在下面applyMiddleware 会再次调用combineReducers 合并 reducer在上面说到我们可能存在多个reducer,也可能分模块来处理不同的状态问题,这里就需要合并不同模块的reducer,实现代码: ...

April 22, 2019 · 3 min · jiezi

积梦前端采用的 React 状态管理方案: Rex

积梦(https://jimeng.io) 是一个为制造业制作的一个平台.积梦的前端基于 React 做开发的. Rex 是我们在前端使用的状态管理方案, 类似 Redux.从名字也可以看, Rex 是一个基于 Redux 做了大幅简化的方案.另一方面, Rex 跟 Immer 有比较好的整合, 能够很轻松得使用不可变数据. 先前的技术方案在开发 Rex 之前, 我们主要采用了 mobx-state-tree 的方案, 以及试验过 Redux.最早的代码使用了 mobx 搭配 mobx-state-tree, 比较迎合 observe 的用法.但是使用 mobx 全家桶遇到了一些比较困扰的问题, mobx 对数据封装的话,数据量比较大的时候初始化非常慢, 对应图表.observable 数据调试很不方便, 打印在 Console 是一个难以读取的对象.mobx-state-tree 内置了 types, 加上偶尔有改版, 经常出现不可控, 比如报错, 字段修改.由于我一直就是 immutable 数据的支持者, 就一直在试验能否用不可变数据解决这些问题.但是早先主要是 immutablejs 方案, 按照以前的使用经验, 成本比较高.后来出现了 immer, 在工业聚当有 Micheal 的介绍下我们开始局部尝试, 取得了不错的效果.而且因为 immer 也是 mobx 全家桶作者 Micheal 发布的模块, 使用也比较顺畅. 最初我尝试过用 immer 搭配 Redux 来局部替换一些全局状态,试验之后我觉得效果上没有达到预期, ...

April 22, 2019 · 2 min · jiezi

「 React 」redux

简介1) redux是一个独立专门用于做状态管理的JS库(不是react插件库)2) 它可以用在react, angular, vue等项目中, 但演变至今基本与react配合使用3) 作用: 集中式管理react应用中多个组件共享的状态Tip:redux如果不是比较复杂的组件间通信的情况下,建议还是不使用,因为会造成代码量的上升和复杂关键模块Store保存状态的主要部分,共享的状态数据保存在该对象中Action Creators工厂函数,主要用来生成action对象,传输更新的状态数据.Reducers接收action对象,对之前的状态和action中的新状态进行操作,并且返回新的结果存在store中.关键函数store.createStore()创建store对象,参数传入reducers进行绑定.store.dispatch()分发action对象,传入reducers,进行状态的更新.store.subscribe()监听事件,当有状态改变时,会自动调用监听的方法(一般用来重新渲染方法)使用示例1.下载安装//此处我使用的是yarn,后面两个后面介绍yarn add redux react-redux redux-thunk2.创建文件目录3.各部分内容store.jsimport { createStore,applyMiddleware } from ‘redux’import reducer from ‘./reducer’ //导入reducer进行绑定import thunk from ‘redux-thunk’ //这是一个异步解析实现export default createStore(reducer,applyMiddleware(thunk)); // 导出store对象action-creator.jsimport { INCREASE, DECREASE } from ‘./action-type’ //全局命名声明文件// 不同的操作,返回action对象,type为标识,data为传输的数据export const incresement = (data) => ({ type:INCREASE,data:data}) export const decresement = (data) =>({type:DECREASE,data:data})//模拟异步操作,返回的是主动进行分发操作的一个函数export const incresementAsync = (data) => { return (dispatch) => { setTimeout(()=>{ dispatch(incresement(data)) },1000) } }reducer.jsimport {INCREASE,DECREASE} from ‘./action-type’//当有dispatch被调用时,会自动来遍历该模块中的所有函数,并进行匹配.//previousState为之前的状态,action中包含着新的数据export default function number(previousState = 0,action) { switch(action.type){ case INCREASE: return previousState + action.data; case DECREASE: return previousState - action.data; default: return previousState; }}action-type.js//声明定义了一些命名export const INCREASE = ‘INCREASE’;export const DECREASE = ‘DECREASE’;App.jsimport React, { Component } from ‘react’import { connect } from ‘react-redux’import { incresement, decresement,incresementAsync } from ‘./redux/action-creator’class App extends Component {// 进行更新操作 increase = () => { this.props.incresement(1) } decrease = () => { this.props.decresement(1) } increaseAsync = () => { this.props.incresementAsync(1) } render() { return ( <div> //获取状态值 <h3>click {this.props.number} times</h3> <button onClick={this.increase}>+++</button> <button onClick={this.decrease}>—</button> <button onClick={this.increaseAsync}>异步加</button> </div> ) }}//关键在这里,这是简写的方式.//得益于react-redux,将创建action对象和dispatch的操作都进行了封装简化,并且封装了获取状态值.//不管是进行获取还是更新操作,都封装进了props属性中.export default connect( (state) => ({ number: state }), { incresement, decresement,incresementAsync })(App)index.jsimport React from ‘react’;import ReactDOM from ‘react-dom’;import { Provider } from ‘react-redux’import store from ‘./redux/store’import App from ‘./App’;// 用Provider包装,就省略了用subscribe()监听的回调.ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById(‘root’));react-redux专门用来简化redux在react中使用的一个库.它将原生redux的.getState(),创建action对象,dispatch等方法进行了封装.提供如上代码的简写方式.redux-thunk用来帮助解析异步操作.只需要在创建store对象的时候用中间件包装的方式作为第二个参数传入即可.扩展调试工具redux-devtools-extension.在谷歌商店中装好这个插件,然后在创建store对象的时候import { createStore, applyMiddleware } from ‘redux’;import { composeWithDevTools } from ‘redux-devtools-extension’;const store = createStore(reducer, composeWithDevTools( applyMiddleware(…middleware), // other store enhancers if any));总结redux在复杂项目中比较适合使用.它保存着一些多处需要共享的状态数据,在整个项目中比较方便进行状态数据的更新以及获取.避免了一些层级比较多或者跨越了比较多级的同级兄弟组件需要互相通信的复杂过程. ...

April 18, 2019 · 2 min · jiezi

Fish Redux中的Dispatch是怎么实现的?

零、前言我们在使用fish-redux构建应用的时候,界面代码(view)和事件的处理逻辑(reducer,effect)是完全解耦的,界面需要处理事件的时候将action分发给对应的事件处理逻辑去进行处理,而这个分发的过程就是下面要讲的dispatch, 通过本篇的内容,你可以更深刻的理解一个action是如何一步步去进行分发的。一、从example开始为了更好的理解action的dispatch过程,我们就先以todo_list_page中一条todo条目的勾选事件为例,来看点击后事件的传递过程,通过断点debug我们很容易就能够发现点击时候发生的一切,具体过程如下:用户点击勾选框,GestureDetector的onTap会被回调通过buildView传入的dispatch函数对doneAction进行分发,发现todo_component的effect中无法处理此doneAction,所以将其交给pageStore的dispatch继续进行分发pageStore的dispatch会将action交给reducer进行处理,故doneAction对应的_markDone会被执行,对state进行clone,并修改clone后的state的状态,然后将这个全新的state返回然后pageStore的dispatch会通知所有的listeners,其中负责界面重绘的_viewUpdater发现state发生变化,通知界面进行重绘更新二、Dispatch实现分析Dispatch在实现的过程中借鉴了Elm。Dispatch在fish-redux中的定义如下typedef Dispatch = void Function(Action action);本质上就是一个action的处理函数,接受一个action,然后对action进行分发。下面我门通过源码来进行详细的分析1.component中的dispatchbuildView函数传入的dispatch是对应的component的mainCtx中的dispatch,_mainCtx和componet的关系如下component -> ComponentWidget -> ComponentState -> _mainCtx -> _dispatch而 _mainCtx的初始化则是通过componet的createContext方法来创建的,顺着方法下去我们看到了dispatch的初始化// redux_component/context.dart DefaultContext初始化方法 DefaultContext({ @required this.factors, @required this.store, @required BuildContext buildContext, @required this.getState, }) : assert(factors != null), assert(store != null), assert(buildContext != null), assert(getState != null), _buildContext = buildContext { final OnAction onAction = factors.createHandlerOnAction(this); /// create Dispatch _dispatch = factors.createDispatch(onAction, this, store.dispatch); /// Register inter-component broadcast _onBroadcast = factors.createHandlerOnBroadcast(onAction, this, store.dispatch); registerOnDisposed(store.registerReceiver(_onBroadcast)); }context中的dispatch是通过factors来进行创建的,factors其实就是当前component,factors创建dispatch的时候传入了onAction函数,以及context自己和store的dispatch。onAction主要是进行Effect处理。这边还可以看到,进行context初始化的最后,还将自己的onAction包装注册到store的广播中去,这样就可以接收到别人发出的action广播。Component继承自Logic// redux_component/logic.dart @override Dispatch createDispatch( OnAction onAction, Context<T> ctx, Dispatch parentDispatch) { Dispatch dispatch = (Action action) { throw Exception( ‘Dispatching while appending your effect & onError to dispatch is not allowed.’); }; /// attach to store.dispatch dispatch = _applyOnAction<T>(onAction, ctx)( dispatch: (Action action) => dispatch(action), getState: () => ctx.state, )(parentDispatch); return dispatch; } static Middleware<T> _applyOnAction<T>(OnAction onAction, Context<T> ctx) { return ({Dispatch dispatch, Get<T> getState}) { return (Dispatch next) { return (Action action) { final Object result = onAction?.call(action); if (result != null && result != false) { return; } //skip-lifecycle-actions if (action.type is Lifecycle) { return; } if (!shouldBeInterruptedBeforeReducer(action)) { ctx.pageBroadcast(action); } next(action); }; }; }; }}上面分发的逻辑大概可以通过上图来表示通过onAction将action交给component对应的effect进行处理当effect无法处理此action,且此action非lifecycle-actions,且不需中断则广播给当前Page的其余所有effects最后就是继续将action分发给store的dispatch(parentDispatch传入的其实就是store.dispatch)2. store中的dispatch从store的创建代码我们可以看到store的dispatch的具体逻辑// redux/create_store.dart final Dispatch dispatch = (Action action) { _throwIfNot(action != null, ‘Expected the action to be non-null value.’); _throwIfNot( action.type != null, ‘Expected the action.type to be non-null value.’); _throwIfNot(!isDispatching, ‘Reducers may not dispatch actions.’); try { isDispatching = true; state = reducer(state, action); } finally { isDispatching = false; } final List<_VoidCallback> _notifyListeners = listeners.toList( growable: false, ); for (_VoidCallback listener in _notifyListeners) { listener(); } notifyController.add(state); };store的dispatch过程比较简单,主要就是进行reducer的调用,处理完成后通知监听者。3.middlewarePage继承自Component,增加了middleware机制,fish-redux的redux部分本身其实就对middleware做了支持,可以通过StoreEnhancer的方式将middlewares进行组装,合并到Store的dispatch函数中。middleware机制可以允许我们通过中间件的方式对redux的state做AOP处理,比如fish-redux自带的logMiddleware,可以对state的变化进行log,分别打印出state变化前和变化后的值。当Page配置了middleware之后,在创建pageStore的过程中会将配置的middleware传入,传入之后会对store的dispath进行增强加工,将middleware的处理函数串联到dispatch中。// redux_component/component.dart Widget buildPage(P param) { return wrapper(_PageWidget<T>( component: this, storeBuilder: () => createPageStore<T>( initState(param), reducer, applyMiddleware<T>(buildMiddleware(middleware)), ), )); }// redux_component/page_store.dartPageStore<T> createPageStore<T>(T preloadedState, Reducer<T> reducer, [StoreEnhancer<T> enhancer]) => _PageStore<T>(createStore(preloadedState, reducer, enhancer));// redux/create_store.dartStore<T> createStore<T>(T preloadedState, Reducer<T> reducer, [StoreEnhancer<T> enhancer]) => enhancer != null ? enhancer(_createStore)(preloadedState, reducer) : _createStore(preloadedState, reducer);所以这里可以看到,当传入enhancer时,createStore的工作被enhancer代理了,会返回一个经过enhancer处理过的store。而PageStore创建的时候传入的是中间件的enhancer。// redux/apply_middleware.dartStoreEnhancer<T> applyMiddleware<T>(List<Middleware<T>> middleware) { return middleware == null || middleware.isEmpty ? null : (StoreCreator<T> creator) => (T initState, Reducer<T> reducer) { assert(middleware != null && middleware.isNotEmpty); final Store<T> store = creator(initState, reducer); final Dispatch initialValue = store.dispatch; store.dispatch = (Action action) { throw Exception( ‘Dispatching while constructing your middleware is not allowed. ’ ‘Other middleware would not be applied to this dispatch.’); }; store.dispatch = middleware .map((Middleware<T> middleware) => middleware( dispatch: (Action action) => store.dispatch(action), getState: store.getState, )) .fold( initialValue, (Dispatch previousValue, Dispatch Function(Dispatch) element) => element(previousValue), ); return store; };}这里的逻辑其实就是将所有的middleware的处理函数都串到store的dispatch,这样当store进行dispatch的时候所有的中间件的处理函数也会被调用。下面为各个处理函数的执行顺序,首先还是component中的dispatch D1 会被执行,然后传递给store的dispatch,而此时store的dispatch已经经过中间件的增强,所以会执行中间件的处理函数,最终store的原始dispatch函数D2会被执行。三、总结通过上面的内容,现在我们可以知道一个action是如何一步步的派送给effect,reducer去进行处理的,我们也可以通过middleware的方式去跟踪state的变化,这样的扩展性给框架本身带来无限可能。本文作者:闲鱼技术-卢克阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 17, 2019 · 2 min · jiezi

理解 React 轻量状态管理库 Unstated

在React写应用的时候,难免遇到跨组件通信的问题。现在已经有很多的解决方案。React本身的ContextRedux结合React-reduxMobx结合mobx-reactReact 的新的Context api本质上并不是React或者Mbox这种状态管理工具的替代品,充其量只是对React自身状态管理短板的补充。而Redux和Mbox这两个库本身并不是为React设计的,对于一些小型的React应用比较重。基本概念Unstated是基于context API。也就是使用React.createContext()创建一个StateContext来传递状态,Container:状态管理类,内部使用state存储状态,通过setState实现状态的更新,api设计与React的组件基本一致。Provider:返回Provider,用来包裹顶层组件,向应用中注入状态管理实例,可做数据的初始化。Subscribe:本质上是Consumer,获取状态管理实例,在Container实例更新状态的时候强制更新视图。简单的例子我们拿最通用的计数器的例子来看unstated如何使用,先明确一下结构:Parent作为父组件包含两个子组件:Child1和Child2。Child1展示数字,Child2操作数字的加减。然后,Parent组件的外层会包裹一个根组件。维护状态首先,共享状态需要有个状态管理的地方,与Redux的Reducer不同的是,Unstated是通过一个继承自Container实例:import { Container } from ‘unstated’;class CounterContainer extends Container { constructor(initCount) { super(…arguments); this.state = {count: initCount || 0}; } increment = () => { this.setState({ count: this.state.count + 1 }); } decrement = () => { this.setState({ count: this.state.count - 1 }); }}export default CounterContainer看上去是不是很熟悉?像一个React组件类。CounterContainer继承自Unstated暴露出来的Container类,利用state存储数据,setState维护状态,并且setState与React的setState用法一致,可传入函数。返回的是一个promise。共享状态来看一下要显示数字的Child1组件,利用Subscribe与CounterContainer建立联系。import React from ‘react’import { Subscribe } from ‘unstated’import CounterContainer from ‘./store/Counter’class Child1 extends React.Component { render() { return <Subscribe to={[CounterContainer]}> { counter => { return <div>{counter.state.count}</div> } } </Subscribe> }}export default Child1再来看一下要控制数字加减的Child2组件:import React from ‘react’import { Button } from ‘antd’import { Subscribe } from ‘unstated’import CounterContainer from ‘./store/Counter’class Child2 extends React.Component { render() { return <Subscribe to={[CounterContainer]}> { counter => { return <div> <button onClick={counter.increment}>增加</button> <button onClick={counter.decrement}>减少</button> </div> } } </Subscribe> }}export default Child2Subscribe内部返回的是StateContext.Consumer,通过to这个prop关联到CounterContainer实例,使用renderProps模式渲染视图,Subscribe之内调用的函数的参数就是订阅的那个状态管理实例。Child1与Child2通过Subscribe订阅共同的状态管理实例CounterContainer,所以Child2可以调用CounterContainer之内的increment和decrement方法来更新状态,而Child1会根据更新来显示数据。看一下父组件Parentimport React from ‘react’import { Provider } from ‘unstated’import Child1 from ‘./Child1’import Child2 from ‘./Child2’import CounterContainer from ‘./store/Counter’const counter = new CounterContainer(123)class Parent extends React.Component { render() { return <Provider inject={[counter]}> 父组件 <Child1/> <Child2/> </Provider> }}export default ParentProvider返回的是StateContext.Provider,Parent通过Provider向组件的上下文中注入状态管理实例。这里,可以不注入实例。不注入的话,Subscribe内部就不能拿到注入的实例去初始化数据,也就是给状态一个默认值,比如上边我给的是123。也可以注入多个实例:<Provider inject={[count1, count2]}> {/Components}</Provide>那么,在Subscribe的时候可以拿到多个实例。<Subscribe to={[CounterContainer1, CounterContainer2]}> {count1, count2) => {}</Subscribe>分析原理弄明白原理之前需要先明白Unstated提供的三个API之间的关系。我根据自己的理解,画了一张图:来梳理一下整个流程:创建状态管理类继承自Container生成上下文,new一个状态管理的实例,给出默认值,注入ProviderSubscribe订阅状态管理类。内部通过_createInstances方法来初始化状态管理实例并订阅该实例,具体过程如下:从上下文中获取状态管理实例,如果获取到了,那它直接去初始化数据,如果没有获取到那么就用to中传入的状态管理类来初始化实例。将自身的更新视图的函数onUpdate通过订阅到状态管理实例,来实现实例内部setState的时候,调用onUpdate更新视图。_createInstances方法返回创建的状态管理实例,作为参数传递给renderProps调用的函数,函数拿到实例,操作或显示数据。Container用来实现一个状态管理类。可以理解为redux中action和reducer的结合。概念相似,但实现不同。来看一下Container的源码export class Container { constructor() { CONTAINER_DEBUG_CALLBACKS.forEach(cb => cb(this)); this.state = null; this.listeners = []; } setState(updater, callback) { return Promise.resolve().then(() => { let nextState = null; if (typeof updater === ‘function’) { nextState = updater(this.state); } else { nextState = updater; } if (nextState === null) { callback && callback(); } // 返回一个新的state this.state = Object.assign({}, this.state, nextState); // 执行listener,也就是Subscribe的onUpdate函数,用来强制刷新视图 const promises = this.listeners.map(listener => listener()); return Promise.all(promises).then(() => { if (callback) { return callback(); } }); }); } subscribe(fn) { this.listeners.push(fn); } unsubscribe(fn) { this.listeners = this.listeners.filter(f => f !== fn); }}Container包含了state、listeners,以及setState、subscribe、unsubscribe这三个方法。state来存放数据,listeners是一个数组,存放更新视图的函数。subscribe会将更新的函数(Subscribe组件内的onUpdate)放入linsteners。setState和react的setState相似。执行时,会根据变动返回一个新的state,同时循环listeners调用其中的更新函数。达到更新页面的效果。unsubscribe用来取消订阅。ProviderProvider本质上返回的是StateContext.Provider。export function Provider(ProviderProps) { return ( <StateContext.Consumer> {parentMap => { let childMap = new Map(parentMap); if (props.inject) { props.inject.forEach(instance => { childMap.set(instance.constructor, instance); }); } return ( <StateContext.Provider value={childMap}> {props.children} </StateContext.Provider> ); }} </StateContext.Consumer> );}它自己接收一个inject属性,经过处理后,将它作为context的值传入到上下文环境中。可以看出,传入的值为一个map,使用Container类作为键,Container类的实例作为值。Subscribe会接收这个map,优先使用它来实例化Container类,初始化数据。可能有人注意到了Provider不是直接返回的StateContext.Provider,而是套了一层StateContext.Consumer。这样做的目的是Provider之内还可以嵌套Provider。内层Provider的value可以继承自外层。Subscribe简单来说就是连接组件与状态管理类的一座桥梁,可以想象成react-redux中connect的作用class Subscribe extends React.Component { constructor(props) { super(props); this.state = {}; this.instances = []; this.unmounted = false; } componentWillUnmount() { this.unmounted = true; this.unsubscribe(); } unsubscribe() { this.instances.forEach((container) => { container.unsubscribe(this.onUpdate); }); } onUpdate = () => new Promise((resolve) => { if (!this.unmounted) { this.setState(DUMMY_STATE, resolve); } else { resolve(); } }) _createInstances(map, containers) { this.unsubscribe(); if (map === null) { throw new Error(‘You must wrap your <Subscribe> components with a <Provider>’); } const safeMap = map; const instances = containers.map((ContainerItem) => { let instance; if ( typeof ContainerItem === ‘object’ && ContainerItem instanceof Container ) { instance = ContainerItem; } else { instance = safeMap.get(ContainerItem); if (!instance) { instance = new ContainerItem(); safeMap.set(ContainerItem, instance); } } instance.unsubscribe(this.onUpdate); instance.subscribe(this.onUpdate); return instance; }); this.instances = instances; return instances; } render() { return ( <StateContext.Consumer> { map => this.props.children.apply( null, this._createInstances(map, this.props.to), ) } </StateContext.Consumer> ); }}这里比较重要的是_createInstances与onUpdate两个方法。StateContext.Consumer接收Provider传递过来的map,与props接收的to一并传给_createInstances。onUpdate:没有做什么其他事情,只是利用setState更新视图,返回一个promise。它存在的意义是在订阅的时候,作为参数传入Container类的subscribe,扩充Container类的listeners数组,随后在Container类setState改变状态以后,循环listeners的每一项就是这个onUpdate方法,它执行,就会更新视图。_createInstances: map为provider中inject的状态管理实例数据。如果inject了,那么就用map来实例化数据,否则用this.props.to的状态管理类来实例化。之后调用instance.subscribe方法(也就是Container中的subscribe),传入自身的onUpdate,实现订阅。它存在的意义是实例化Container类并将自身的onUpdate订阅到Container类实例,最终返回这个Container类的实例,作为this.props.children的参数并进行调用,所以在组件内部可以进行类似这样的操作: <Subscribe to={[CounterContainer]}> { counter => { return <div> <Button onClick={counter.increment}>增加</Button> <Button onClick={counter.decrement}>减少</Button> </div> } }</Subscribe>总结Unstated上手很容易,理解源码也不难。重点在于理解发布(Container类),Subscribe组件实现订阅的思路。其API的设计贴合React的设计理念。也就是想要改变UI必须setState。另外可以不用像Redux一样写很多样板代码。理解源码的过程中受到了下面两篇文章的启发,衷心感谢:纯粹极简的react状态管理组件unstatedUnstated浅析 ...

April 8, 2019 · 3 min · jiezi

redux的简单使用

redux的基本使用store文件夹index.js // 1. 引入redux的创建store方法 createStore import { createStore } from ‘redux’; // 2. 引入reducer import reducer from ‘reducer’; // 3. 使用createStore方法,并传入reducer const store = createStore( reducer, window.REDUX_DEVTOOLS_EXTENSION && window.REDUX_DEVTOOLS_EXTENSION() // 使用redux谷歌插件 ); // 4. 讲store暴露出去 export default store;reducer.js // 引入actionTypes.js import { ACTION_TYPE } from ‘./actionTypes’ // 1. 定义state的默认值 const defaultState = { data: [] }; // 2. 暴露函数 export default (state = defaultState, action) => { // 4. 通过接收的action参数判断下一步操作 if(action.type === ACTION_TYPE) { // 5. 深拷贝state const newState = JSON.parse(JSON.stringify(state)); // 6. 对newState的数据进行修改 newState.data = action.value; // 7. 将newState返回 return newState; } // 3. 返回state return state; }actionTypes.js //统一管理redux的actionde type属性,杜绝代码错误,有利于工程化 export const ACTION_TYPE = ‘action_type;actionCreators.js // 引入actionTypes.js import { ACTION_TYPE } from ‘./actionTypes’ // action创建器,有利于自动化测试 export const getAction = (value) => ({ type: ACTION_TYPE, value })Component.js import React, { Component } from ‘react’; // 1. 引入store import store from ‘./store’; // 2. 引入actionCreators.js中创建action的方法 import { getAction } from ‘./store/actionCreators’; class App extends Component { render(){} handelGetAction(){ const value = ‘aa’; // 3. 调用action创建方法 const action = getAction(value); // 4. 派发action store.dispatch(action); } } ...

April 4, 2019 · 1 min · jiezi

从项目中由浅入深的学习react (2)

序列文章从项目中由浅入深的学习vue,微信小程序和快应用(1)前言从pc(dva+umi)和mobile(原生react)两个项目来介绍react的使用 搞懂这两个项目,上手撸react代码so-easy1.react-pc-template篇1.1效果图react-pc-template项目, 欢迎star1.2技术栈dva+umi+ant-design-prodva:可拔插的react应用框架,基于react和reduxmui:集成react的router和reduxant-design-pro:基于react和ant-pc的中后台解决方案1.3适配方案左侧固定宽度,右侧自适应右侧导航分别配置滚动条.控制整个page1.4技能点分析技能点对应api3种定义react组件方法1.函数式定义的无状态组件; 2.es5原生方式React.createClass定义的组件; 3.es6形式的extends React.Component定义的组件JSXreact是基于jSX语法react16之前生命周期实例化(6个):constructor,getDefaultProps,getInitialState,componentWillMount,render,componentDidMountreact16生命周期实例化(4个):constructor,getDerivedStateFromProps,componentWillMount,render,componentDidMount生命周期更新:5个生命周期生命周期销毁:componentWillUnmout路由基于umi,里面有push,replace,go等方法状态管理dva里面的redux的封装,属性有state,effects,reducers组件传值父子:props,平级redux或umi的routermodel项目的model和dom是通过@connect()连接并将部分属性添加到props里登陆登陆是通过在入口js里面做路由判断1.5那么问题来了?三种定义react组件方式的区别?解析umi的router传参形式? 解析dva封装的redux和原生的redux使用有那些不同? dva使用解析redux使用解析umi里面router实现原理?umi源码对比vue和react在原理和使用上的区别?2.react-mobile篇2.1 效果图react-mobile项目,欢迎star2.2 技术栈react + react-router-v4 + redux +ant-design-mobile+iconfontreact-router-v4:路由4.x版本redux:状态管理ant-design-mobile:UI组件iconfont:字体icon2.3 适配方案rem适配2.4技能点分析技能点对应的apireact-dom提供render方法react-router 4.x组成react-router(核心路由和函数) , react-router-dom(API) , react-router-native( React Native 应用使用的API)react-router 4.x的APIrouter(只能有一个) , route(匹配路由渲染UI) , history, link(跳转) , navlink(特定的link,会带样式) , switch(匹配第一个路由) , redirect(重定向) , withRouter(组件,可传入history,location,match 三个对象)react-router 3.x组成就是react-routerreact-router 3.x的APIrouter , route , history , indexRedirect(默认加载) , indexRedirect(默认重定向) , link(跳转) , 路由钩子(onEnter进入,onLeave离开)4.x已经去掉historyreact-router有三种模式:1.browserHistory(需要后台支持); 2.hashHistory(有’#’); 3.createMemoryHistoryredux单向数据流 , action(通过dispatch改变state值) , reducer(根据 action 更新 state) , store(联系action和reducer)react-redux1.连接react-router和redux,将组件分为两类:UI组件和容器组件(管理数据和逻辑) , 2.connect由UI组件生成容器组件 , 3.provider让容器组件拿到state ,4.mapStateToProps:外部state对象和UI组件的props映射关系,5.mapDispatchToProps:是connect第二个参数, UI 组件的参数到store.dispatch方法的映射react-loadable代码分割,相当于vue-router中的路由懒加载classNames动态css的类2.5 那么问题来了1.react-router 3.x和react-router 4.x的区别?react-router 3.x文档 react-router 4.x文档2.redux和react-redux的关系?react-redux和redux介绍3.react-router 4.x取消了路由钩子怎么做登陆授权判断?const PrivateRoute = ({ component: Component, …rest }) => ( <Route {…rest} render={props => ( fakeAuth.isAuthenticated ? ( <Component {…props}/> ) : ( <Redirect to={{ pathname: ‘/login’, state: { from: props.location } }}/> ) )}/>)利用路由的render属性来做拦截,判断是否授权,否则利用redirect重定向3.结语这个相当于react的入门篇,撸项目是完全可以但react生态超级繁荣,同一个功能插件版本不同,对应的api也不同,一些高级用法后续再更新 ...

April 3, 2019 · 1 min · jiezi

使用RxJS管理React应用状态的实践分享

随着前端应用的复杂度越来越高,如何管理应用的数据已经是一个不可回避的问题。当你面对的是业务场景复杂、需求变动频繁、各种应用数据互相关联依赖的大型前端应用时,你会如何去管理应用的状态数据呢?我们认为应用的数据大体上可以分为四类:事件:瞬间产生的数据,数据被消费后立即销毁,不存储。异步:异步获取的数据;类似于事件,是瞬间数据,不存储。状态:随着时间空间变化的数据,始终会存储一个当前值/最新值。常量:固定不变的数据。RxJS天生就适合编写异步和基于事件的程序,那么状态数据用什么去管理呢?还是用RxJS吗? 合不合适呢?我们去调研和学习了前端社区已有的优秀的状态管理解决方案,也从一些大牛分享的关于用RxJS设计数据层的构想和实践中得到了启发:使用RxJS完全可以实现诸如Redux,Mobx等管理状态数据的功能。应用的数据不是只有状态的,还有事件、异步、常量等等。如果整个应用都由observable来表达,则可以借助RxJS基于序列且可响应的的特性,以流的方式自由地拼接和组合各种类型的数据,能够更优雅更高效地抽象出可复用可扩展的业务模型。出于以上两点原因,最终决定基于RxJS来设计一套管理应用的状态的解决方案。原理介绍对于状态的定义,通常认为状态需要满足以下3个条件:是一个具有多个值的集合。能够通过event或者action对值进行转换,从而得到新的值。有“当前值”的概念,对外一般只暴露当前值,即最新值。那么,RxJS适合用来管理状态数据吗?答案是肯定的!首先,因为Observable本身就是多个值的推送集合,所以第一个条件是满足的!其次,我们可以实现一个使用dispatch action模式来推送数据的observable来满足第二个条件!众所周知,RxJS中的observable可以分为两种类型:cold observable: 推送值的生产者(producer)来自observable内部。将会推送几个值以及推送什么样的值已在observable创建时被定义下来,不可改变。producer与观察者(observer) 是一对一的关系,即是单播的。每当有observer订阅时,producer都会把预先定义好的若干个值依次推送给observer。hot observable: 推送值的producer来自observable外部。将会推送几个值、推送什么样的值以及何时推送在创建时都是未知的。producer与observer是一对多的关系,即是多播的。每当有observer订阅时,会将observer注册到观察者列表中,类似于其他库或语言中的addListener的工作方式。当外部的producer被触发或执行时,会将值同时推送给所有的observer;也就是说,所有的observer共享了hot observable推送的值。RxJS提供的BehaviorSubject就是一种特殊的hot observable,它向外暴露了推送数据的接口next函数;并且有“当前值”的概念,它保存了发送给observer的最新值,当有新的观察者订阅时,会立即从BehaviorSubject那接收到“当前值”。那么这说明使用BehaviorSubject来更新状态并保存状态的当前值是可行的,第三个条件也满足了。简单实现请看以下的代码:import { BehaviorSubject } from ‘rxjs’;// 数据推送的生产者class StateMachine { constructor(subject, value) { this.subject = subject; this.value = value; } producer(action) { let oldValue = this.value; let newValue; switch (action.type) { case ‘plus’: newValue = ++oldValue; this.value = newValue; this.subject.next(newValue); break; case ’toDouble’: newValue = oldValue * 2; this.value = newValue; this.subject.next(newValue); break; } }}const value = 1; // 状态的初始值const count$ = new BehaviorSubject(value);const stateMachine = new StateMachine(count$, value);// 派遣actionfunction dispatch(action) { stateMachine.producer(action);}count$.subscribe(val => { console.log(val);});setTimeout(() => { dispatch({ type: “plus” });}, 1000);setTimeout(() => { dispatch({ type: “toDouble” });}, 2000);执行代码控制台会打印出三个值:Console 1 2 4上面的代码简单实现了一个简单管理状态的例子:状态的初始值: 1执行plus之后的状态值: 2执行toDouble之后的状态值: 4实现方法挺简单的,就是使用BehaviorSubject来表达状态的当前值:第一步,通过调用dispatch函数使producer函数执行第二部,producer函数在内部调用了BehaviorSubject的next函数,推送了新数据,BehaviorSubject的当前值更新了,也就是状态更新了。不过写起来略微繁琐,我们对其进行了封装,优化后写法见下文。使用操作符来创建状态数据我们自定义了一个操作符state用来创建一个能够通过dispatch action模式推送新数据的BehaviorSubject,我们称她为stateObservable。const count$ = state({ // 状态的唯一标识名称 name: “count”, // 状态的默认值 defaultValue: 1, // 数据推送的生产者函数 producer(next, value, action) { switch (action.type) { case “plus”: next(value + 1); break; case “toDouble”: next(value * 2); break; } }});更新状态在你想要的任意位置使用函数dispatch派遣action即可更新状态!dispatch(“count”, { type: “plus”})异步数据RxJS的一大优势就在于能够统一同步和异步,使用observable处理数据你不需要关注同步还是异步。下面的例子我们使用操作符from将promise转换为observable。指定observable作为状态的初始值(首次推送数据)const todos$ = state({ name: “todos”, // observable推送的数据将作为状态的初始值 initial: from(getAsyncData()) //… });producer推送observableconst todos$ = state({ name: “todos”, defaultValue: [] // 数据推送的生产者函数 producer(next, value, action) { switch (action.type) { case “getAsyncData”: next( from(getAsyncData()) ); break; } }});执行getAsyncData之后,from(getAsyncData())的推送数据将成为状态的最新值。衍生状态由于状态todos$是一个observable,所以可以很自然地使用RxJS操作符转换得到另一个新的observable。并且这个observable的推送来自todos$;也就是说只要todos$推送新数据,它也会推送;效果类似于Vue的计算属性。// 未完成任务数量const undoneCount$ = todos$.pipe( map(todos => { let _conut = 0; todos.forEach(item => { if (!item.check) ++_conut; }); return _conut; }));React视图渲染我们可能会在组件的生命周期内订阅observable得到数据渲染视图。class Todos extends React.Component { componentWillMount() { todos$.subscribe(data => { this.setState({ todos: data }); }); }}我们可以再优化下,利用高阶组件封装一个装饰器函数@subscription,顾名思义,就是为React组件订阅observable以响应推送数据的变化;它会将observable推送的数据转换为React组件的props。@subscription({ todos: todos$})class TodoList extends React.Component { render() { return ( <div className=“todolist”> <h1 className=“header”>任务列表</h1> {this.props.todos.map((item, n) => { return <TodoItem item={item} key={item.desc} />; })} </div> ); }}总结使用RxJS越久,越令人受益匪浅。因为它基于observable序列提供了较高层次的抽象,并且是观察者模式,可以尽可能地减少各组件各模块之间的耦合度,大大减轻了定位BUG和重构的负担。因为是基于observable序列来编写代码的,所以遇到复杂的业务场景,总能按照一定的顺序使用observable描述出来,代码的可读性很强。并且当需求变动时,我可能只需要调整下observable的顺序,或者加个操作符就行了。再也不必因为一个复杂的业务流程改动了,需要去改好几个地方的代码(而且还容易改出BUG,笑~)。所以,以上基于RxJS的状态管理方案,对我们来说是一个必需品,因为我们项目中大量使用了RxJS,如果状态数据也是observable,对我们抽象可复用可扩展的业务模型是一个非常大的助力。当然了,如果你的项目中没有使用RxJS,也许Redux和Mobx是更合适的选择。这套基于RxJS的状态管理方案,我们已经用于开发公司的商用项目,反馈还不错。所以我们决定把这套方案整理成一个js lib,取名为:Floway,并在github上开源:github源码:https://github.com/shayeLee/floway使用文档:https://shayelee.github.io/floway欢迎大家star,更欢迎大家来共同交流和分享RxJS的使用心得!参考文章:复杂单页应用的数据层设计DaoCloud 基于 RxJS 的前端数据层实践 ...

April 2, 2019 · 2 min · jiezi

React 服务端渲染从入门到精通

前言这篇文章是我自己在搭建个人网站的过程中,用到了服务端渲染,看了一些教程,踩了一些坑。想把这个过程分享出来。我会尽力把每个步骤讲明白,将我理解的全部讲出来。文中的示例代码来自于这个仓库,也是我正在搭建的个人网站,大家可以一起交流一下。本文中用到的技术React V16 | React-Router v4 | Redux | Redux-thunk | expressReact 服务端渲染服务端渲染的基本套路就是用户请求过来的时候,在服务端生成一个我们希望看到的网页内容的HTML字符串,返回给浏览器去展示。浏览器拿到了这个HTML之后,渲染出页面,但是并没有事件交互,这时候浏览器发现HTML中加载了一些js文件(也就是浏览器端渲染的js),就直接去加载。加载好并执行完以后,事件就会被绑定上了。这时候页面被浏览器端接管了。也就是到了我们熟悉的js渲染页面的过程。需要实现的目标:React组件服务端渲染路由的服务端渲染保证服务端和浏览器的数据唯一css的服务端渲染(样式直出)一般的渲染方式服务端渲染:服务端生成html字符串,发送给浏览器进行渲染。浏览器端渲染:服务端返回空的html文件,内部加载js完全由js与css,由js完成页面的渲染优点与缺点服务端渲染解决了首屏加载速度慢以及seo不友好的缺点(Google已经可以检索到浏览器渲染的网页,但不是所有搜索引擎都可以)但增加了项目的复杂程度,提高维护成本。如果非必须,尽量不要用服务端渲染整体思路需要两个端:服务端、浏览器端(浏览器渲染的部分)第一: 打包浏览器端代码第二: 打包服务端代码并启动服务第三: 用户访问,服务端读取浏览器端打包好的index.html文件为字符串,将渲染好的组件、样式、数据塞入html字符串,返回给浏览器第四: 浏览器直接渲染接收到的html内容,并且加载打包好的浏览器端js文件,进行事件绑定,初始化状态数据,完成同构React组件的服务端渲染让我们来看一个最简单的React服务端渲染的过程。要进行服务端渲染的话那必然得需要一个根组件,来负责生成HTML结构import React from ‘react’;import ReactDOM from ‘react-dom’;ReactDOM.hydrate(<Container />, document.getElementById(‘root’));当然这里用ReactDOM.render也是可以的,只不过hydrate会尽量复用接收到的服务端返回的内容,来补充事件绑定和浏览器端其他特有的过程引入浏览器端需要渲染的根组件,利用react的 renderToString API进行渲染import { renderToString } from ‘react-dom/server’import Container from ‘../containers’// 产生htmlconst content = renderToString(<Container/>)const html = &lt;html&gt; &lt;body&gt;${content}&lt;/body&gt; &lt;/html&gt;res.send(html)在这里,renderToString也可以替换成renderToNodeStream,区别在于前者是同步地产生HTML,也就是如果生成HTML用了1000毫秒,那么就会在1000毫秒之后才将内容返回给浏览器,显然耗时过长。而后者则是以流的形式,将渲染结果塞给response对象,就是出来多少就返回给浏览器多少,可以相对减少耗时路由的服务端渲染一般场景下,我们的应用不可能只有一个页面,肯定会有路由跳转。我们一般这么用:import { BrowserRouter, Route } from ‘react-router-dom’const App = () => ( <BrowserRouter> {/…Routes/} <BrowserRouter/>)但这是浏览器端渲染时候的用法。在做服务端渲染时,需要使用将BrowserRouter 替换为 StaticRouter区别在于,BrowserRouter 会通过HTML5 提供的 history API来保持页面与URL的同步,而StaticRouter则不会改变URLimport { createServer } from ‘http’import { StaticRouter } from ‘react-router-dom’createServer((req, res) => { const html = renderToString( <StaticRouter location={req.url} context={{}} > <Container /> <StaticRouter/>)})这里,StaticRouter要接收两个属性:location: StaticRouter 会根据这个属性,自动匹配对应的React组件,所以才会实现刷新页面,服务端返回的对应路由的组与浏览器端保持一致context: 一般用来传递一些数据,相当于一个载体,之后讲到样式的服务端渲染的时候会用到Redux同构数据的预获取以及脱水与注水我认为是服务端渲染的难点。这是什么意思呢?也就是说首屏渲染的网页一般要去请求外部数据,我们希望在生成HTML之前,去获取到这个页面需要的所有数据,然后塞到页面中去,这个过程,叫做“脱水”(Dehydrate),生成HTML返回给浏览器。浏览器拿到带着数据的HTML,去请求浏览器端js,接管页面,用这个数据来初始化组件。这个过程叫“注水”(Hydrate)。完成服务端与浏览器端数据的统一。为什么要这么做呢?试想一下,假设没有数据的预获取,直接返回一个没有数据,只有固定内容的HTML结构,会有什么结果呢?第一:由于页面内没有有效信息,不利于SEO。第二:由于返回的页面没有内容,但浏览器端JS接管页面后回去请求数据、渲染数据,页面会闪一下,用户体验不好。我们使用Redux来管理状态,因为有服务端代码和浏览器端代码,那么就分别需要两个store来管理服务端和浏览器端的数据。组件的配置组件要在服务端渲染的时候去请求数据,可以在组件上挂载一个专门发异步请求的方法,这里叫做loadData,接收服务端的store作为参数,然后store.dispatch去扩充服务端的store。class Home extends React.Component { componentDidMount() { this.props.callApi() } render() { return <div>{this.props.state.name}</div> }}Home.loadData = store => { return store.dispatch(callApi())}const mapState = state => stateconst mapDispatch = {callApi}export default connect(mapState, mapDispatch)(Home)路由的改造因为服务端要根据路由判断当前渲染哪个组件,可以在这个时候发送异步请求。所以路由也需要配置一下来支持loadData方法。服务端渲染的时候,路由的渲染可以使用react-router-config这个库,用法如下(重点关注在路由上挂载loadData方法):import { BrowserRouter } from ‘react-router-dom’import { renderRoutes } from ‘react-router-config’import Home from ‘./Home’export const routes = [ { path: ‘/’, component: Home, loadData: Home.loadData, exact: true, }]const Routers = <BrowserRouter> {renderRoutes(routes)}<BrowserRouter/>服务端获取数据到了服务端,需要判断匹配的路由内的所有组件各自都有没有loadData方法,有就去调用,传入服务端的store,去扩充服务端的store。同时还要注意到,一个页面可能是由多个组件组成的,会发各自的请求,也就意味着我们要等所有的请求都发完,再去返回HTML。import express from ’express’import serverRender from ‘./render’import { matchRoutes } from ‘react-router-config’import { routes } from ‘../routes’import serverStore from “../store/serverStore"const app = express()app.get(’*’, (req, res) => { const context = {css: []} const store = serverStore() // 用matchRoutes方法获取匹配到的路由对应的组件数组 const matchedRoutes = matchRoutes(routes, req.path) const promises = [] for (const item of matchedRoutes) { if (item.route.loadData) { const promise = new Promise((resolve, reject) => { item.route.loadData(store).then(resolve).catch(resolve) }) promises.push(promise) } } // 所有请求响应完毕,将被HTML内容发送给浏览器 Promise.all(promises).then(() => { // 将生成html内容的逻辑封装成了一个函数,接收req, store, context res.send(serverRender(req, store, context)) })})细心的同学可能注意到了上边我把每个loadData都包了一个promise。const promise = new Promise((resolve, reject) => { item.route.loadData(store).then(resolve).catch(resolve) console.log(item.route.loadData(store));})promises.push(promise)这是为了容错,一旦有一个请求出错,那么下边Promise.all方法则不会执行,所以包一层promise的目的是即使请求出错,也会resolve,不会影响到Promise.all方法,也就是说只有请求出错的组件会没数据,而其他组件不会受影响。注入数据我们请求已经发出去了,并且在组件的loadData方法中也扩充了服务端的store,那么可以从服务端的数据取出来注入到要返回给浏览器的HTML中了。来看 serverRender 方法const serverRender = (req, store, context) => { // 读取客户端生成的HTML const template = fs.readFileSync(process.cwd() + ‘/public/static/index.html’, ‘utf8’) const content = renderToString( <Provider store={store}> <StaticRouter location={req.path} context={context}> <Container/> </StaticRouter> </Provider> ) // 注入数据 const initialState = &lt;script&gt; window.context = { INITIAL_STATE: ${JSON.stringify(store.getState())} }&lt;/script&gt; return template.replace(’<!–app–>’, content) .replace(’<!–initial-state–>’, initialState)}浏览器端用服务端获取到的数据初始化store经过上边的过程,我们已经可以从window.context中拿到服务端预获取的数据了,此时需要做的事就是用这份数据去初始化浏览器端的store。保证两端数据的统一。import { createStore, applyMiddleware, compose } from ‘redux’import thunk from ‘redux-thunk’import rootReducer from ‘../reducers’const defaultStore = window.context && window.context.INITIAL_STATEconst clientStore = createStore( rootReducer, defaultStore,// 利用服务端的数据初始化浏览器端的store compose( applyMiddleware(thunk), window.devToolsExtension ? window.devToolsExtension() : f=>f ))至此,服务端渲染的数据统一问题就解决了,再来回顾一下整个流程:用户访问路由,服务端根据路由匹配出对应路由内的组件数组循环数组,调用组件上挂载的loadData方法,发送请求,扩充服务端store所有请求完成后,通过store.getState,获取到服务端预获取的数据,注入到window.context中浏览器渲染返回的HTML,加载浏览器端js,从window.context中取数据来初始化浏览器端的store,渲染组件这里还有个点,也就是当我们从路由进入到其他页面的时候,组件内的loadData方法并不会执行,它只会在刷新,服务端渲染路由的时候执行。这时候会没有数据。所以我们还需要在componentDidMount中去发请求,来解决这个问题。因为componentDidMount不会在服务端渲染执行,所以不用担心请求重复发送。样式的服务端渲染以上我们所做的事情只是让网页的内容经过了服务端的渲染,但是样式要在浏览器加载css后才会加上,u偶遇最开始返回的网页内容没有样式,页面依然会闪一下。为了解决这个问题,我们需要让样式也一并在服务端渲染的时候返回。首先,服务端渲染的时候,解析css文件,不能使用style-loader了,要使用isomorphic-style-loader。{ test: /.css$/, use: [ ‘isomorphic-style-loader’, ‘css-loader’, ‘postcss-loader’ ],}我们想,如何在服务端获取到当前路由内的组件样式呢?回想一下,我们在做路由的服务端渲染时,用到了StaticRouter,它会接收一个context对象,这个context对象可以作为一个载体来传递一些信息。我们就用它!思路就是在渲染组件的时候,在组件内接收context对象,获取组件样式,放到context中,服务端拿到样式,插入到返回的HTML中的style标签。来看看组件是如何读取样式的吧:import style from ‘./style/index.css’class Index extends React.Component { componentWillMount() { if (this.props.staticContext) { const css = styles._getCss() this.props.staticContext.css.push(css) } }}在路由内的组件可以在props里接收到staticContext,也就是通过StaticRouter传递过来的context,isomorphic-style-loader 提供了一个 _getCss() 方法,让我们能读取到css样式,然后放到staticContext里。不在路由之内的组件,可以通过父级组件,传递props的方法,或者用react-router的withRouter包裹一下在服务端,经过组件的渲染之后,context中已经有内容了,我们这时候把样式处理一下,返回给浏览器,就可以做到样式的服务端渲染了const serverRender = (req, store) => { const context = {css: []} const template = fs.readFileSync(process.cwd() + ‘/public/static/index.html’, ‘utf8’) const content = renderToString( <Provider store={store}> <StaticRouter location={req.path} context={context}> <Container/> </StaticRouter> </Provider> ) // 经过渲染之后,context.css内已经有了样式 const cssStr = context.css.length ? context.css.join(’\n’) : ’’ const initialState = &lt;script&gt; window.context = { INITIAL_STATE: ${JSON.stringify(store.getState())} }&lt;/script&gt; return template.replace(’<!–app–>’, content) .replace(‘server-render-css’, cssStr) .replace(’<!–initial-state–>’, initialState)}至此,服务端渲染就全部完成了。总结React的服务端渲染,最好的解决方案就是Next.js。如果你的应用没有SEO优化的需求,又或者不太注重首屏渲染的速度,那么尽量就不要用服务端渲染。因为会让项目变得复杂。此外,除了服务端渲染,SEO优化的办法还有很多,比如预渲染(pre-render)。 ...

March 27, 2019 · 2 min · jiezi

React+Antd+Redux实现待办事件

之前也是写过一篇关于Redux的文章,来简单理解一下Redux,以及该如何使用。今天我就来分享一个也是入门级别的,React+Redux+antd来实现简单的待办事件。同时也讲讲自己对Redux的理解。先来看一张图吧:我们简单的比喻来让我们更加好的理解Redux,我们这样比喻(图书馆借书):1.React Component:借书人2.Action Creators:你要说你要借书这句话,肯定要说话吧,就是一句话:我要借书3.Store:图书馆管理员4.Reducer:图书馆管理员肯定不可能记得所有书,那么Reducer就是作为一本小册子,供图书馆管理员查通俗理解:我要借书,我要先说话,告诉图书馆管理员我要借书,当图书馆管理员知道了之后,但是它不可能知道所有的书籍在哪里,所以需要一本小册子去找,最后找到了之后,再送到你手上。专业术语理解:(Component)要借书,我要先说话(Action ),告诉图书馆管理员(Store)我要借书,当图书馆管理员知道了之后,但是它不可能知道所有的书籍在哪里,所以需要一本小册子(Reducer)去找,最后找到了之后,再送到你(Component)手上。当你看图觉得蒙的时候你再看看这个比喻是不是更好理解了?流程我们大概清楚了,我们就开始来看怎么写这个待办事项吧。我们先来列一个提纲吧,屡清楚思路再写代码。1.react component(todolist.js)2.引入antd3.写store4.写reducer5.写action大概就是上面的一些流程:如何引入antd呢?官方文档:链接描述文件目录结构如下:创建文件之前,首先创建图书馆管理员(store),他不知道书具体在哪里,所以再创建小册子(redux),给到图书馆管理员(store)://src/redux/index.jsimport {createStore} from ‘redux’;import reducer from ‘./reducer’const store=createStore(reducer);export default store;//src/redux/reducer.jsconst defaultState={ inputValue:’’, list:[1,2]}export default(state=defaultState,action)=>{ return state;}*注释:刚开始state,这里一定要给state赋一个初始值,才不会报错接下来你就可以,在todolist.js中用store.getState()获取到store的值,我把他直接赋值给状态:我先实现一个由Component发送action,store收到action,在由reducer接受处理,最后返回一个新的状态,Component接收显示://src/redux/TodoList.jsimport React from ‘react’;import ‘antd/dist/antd.css’;import { Input,Button,List} from ‘antd’;import store from ‘./index’;export default class TodoList extends React.Component{ constructor(props){ super(props); this.state=store.getState(); } componentDidMount(){ console.log(this.state); } handleChg=(e)=>{ const action={ type:‘change_input_value’, inputValue:e.target.value } store.dispatch(action); } render(){ console.log(this.state) return( <div style={{marginTop:“10px”,marginLeft:“20px”}}> <Input placeholder=“请输入” style={{width:“400px”,marginRight:“10px”}} onChange={this.handleChg} value={this.state.inputValue}/> </div> </div> ); } }思路:我们通过input框中监听内容变化发送action,reucer去处理//src/redux/reducer.jsconst defaultState={ inputValue:’’, list:[1,2]}export default(state=defaultState,action)=>{ if(action.type===‘change_input_value’){ const newState=JSON.parse(JSON.stringify(state)) newState.inputValue=action.inputValue; return newState; } return state;}你可以打印出newState看一下,你就会发现inputValue就是你输入的值了。接下来的就可以举一反三了。完整代码:///src/redux/index.jsimport {createStore} from ‘redux’;import reducer from ‘./reducer’const store=createStore(reducer);///src/redux/reducers.jsexport default store;const defaultState={ inputValue:’’, list:[1,2]}export default(state=defaultState,action)=>{ if(action.type===‘change_input_value’){ const newState=JSON.parse(JSON.stringify(state)) newState.inputValue=action.inputValue; return newState; } if(action.type===‘send_message’){ const newState=JSON.parse(JSON.stringify(state)) newState.list.push(newState.inputValue); newState.inputValue=’’; return newState; } if(action.type===‘delete_message’){ const newState=Object.assign({},state); newState.list.splice(action.index,1); return newState; } return state;}///src/redux/todoList.jsimport React from ‘react’;import ‘antd/dist/antd.css’;import { Input,Button,List} from ‘antd’;import store from ‘./index’;const data=[ 1,2,3];export default class TodoList extends React.Component{ constructor(props){ super(props); this.state=store.getState(); store.subscribe(this.F5) } componentDidMount(){ console.log(this.state); } handleChg=(e)=>{ const action={ type:‘change_input_value’, inputValue:e.target.value } store.dispatch(action); } handleSend=()=>{ const action={ type:‘send_message’, } store.dispatch(action); } F5=()=>{ this.setState(store.getState()); } handleItem=(index)=>{ const action={ type:‘delete_message’, index:index } store.dispatch(action); } render(){ console.log(this.state) return( <div style={{marginTop:“10px”,marginLeft:“20px”}}> <Input placeholder=“请输入” style={{width:“400px”,marginRight:“10px”}} onChange={this.handleChg} value={this.state.inputValue}/> <Button type=“primary” onClick={this.handleSend}>发送</Button> <div style={{width:“400px”,marginTop:“10px”}}> <List bordered dataSource={this.state.list} renderItem={(item,index) => (<List.Item onClick={this.handleItem.bind(this,index)}>{item}</List.Item>)}/> </div> </div> ); } }//index.jsimport React from ‘react’;import ReactDOM from ‘react-dom’;import ‘./index.css’;import TodoList from ‘./redux/TodoList’;ReactDOM.render(<TodoList />, document.getElementById(‘root’));这样就实现了一个利用redux来实现简单的待办事项.相信你如果写完这个demo之后,肯定对Redux大致有了了解。如果自己在写的过程中有什么疑惑,欢迎提出,我会给你解答。后期也会更新一些关于Redux的其他方面的知识。 ...

March 13, 2019 · 1 min · jiezi

不一样的redux源码解读

1、本文不涉及redux的使用方法,因此可能更适合使用过 redux 的同学阅读2、当前redux版本为4.0.1 Redux作为大型React应用状态管理最常用的工具。虽然在平时的工作中很多次的用到了它,但是一直没有对其原理进行研究。最近看了一下源码,下面是我自己的一些简单认识,如有疑问欢迎交流。 1.createStore 结合使用场景我们首先来看一下createStore方法。 // 这是我们平常使用时创建store const store = createStore(reducers, state, enhance); 以下源码为去除异常校验后的源码, export default function createStore(reducer, preloadedState, enhancer) {// 如果有传入合法的enhance,则通过enhancer再调用一次createStoreif (typeof enhancer !== 'undefined') {if (typeof enhancer !== 'function') {throw new Error('Expected the enhancer to be a function.')}return enhancer(createStore)(reducer, preloadedState) // 这里涉及到中间件,后面介绍applyMiddleware时在具体介绍}let currentReducer = reducer //把 reducer 赋值给 currentReducerlet currentState = preloadedState //把 preloadedState 赋值给 currentStatelet currentListeners = [] //初始化监听函数列表let nextListeners = currentListeners //监听列表的一个引用let isDispatching = false //是否正在dispatchfunction ensureCanMutateNextListeners() {}function getState() {}function subscribe(listener) {}function dispatch(action) {}function replaceReducer(nextReducer) {}// 在 creatorStore 内部没有看到此方法的调用,就不讲了function observable() {}//初始化 store 里的 state treedispatch({ type: ActionTypes.INIT })return {dispatch,subscribe,getState,replaceReducer,[$$observable]: observable}} 我们可以看到creatorStore方法除了返回我们常用的方法外,还做了一次初始化过程dispatch({ type: ActionTypes.INIT });那么dispatch干了什么事情呢? ...

March 10, 2019 · 4 min · jiezi

一个基于material-ui+react+koa2+mongoose的个人博客系统

前言做这玩意主要是有两个目的,练习平时工作中用不到的技术点,在熟练的基础之上去研究其原理。可能的话,替换掉自己的博客系统。项目地址: https://github.com/2fps/blooog前端前端是基于react的,用到了react-router和redux。UI库主要是material-ui,当然css-in-js的方式还只是会使用,抽空去了解下原理。项目截图就不放了,demo地址:http://132.232.131.250:3000 。用户名和密码都是admin。实现的功能文章的显示、编辑和删除功能。标签的显示、编辑和删除功能。站点信息的配置和显示。登录和修改密码功能。后端后端基于koa2和mongoose。实现的功能加密登录。log4js日志记录功能。joi对数据进行验证。已知问题审美不太好,只觉得别人的界面好,自己搞起来就那样。。后端安全没有做好,没有防xss等。前端代码较乱,还未整理,公共方法未剥离。数据库没有使用事务。没有对数据做缓存。等等。后续待加入菜单。评论。等等。。

March 10, 2019 · 1 min · jiezi

刚刚,阿里宣布开源Flutter应用框架Fish Redux!

3月5日,闲鱼宣布在GitHub上开源Fish Redux,Fish Redux是一个基于 Redux 数据管理的组装式 flutter 应用框架, 特别适用于构建中大型的复杂应用,它最显著的特征是 函数式的编程模型、可预测的状态管理、可插拔的组件体系、最佳的性能表现。下文中,我们将详细介绍Fish Redux的特点和使用过程,以下内容来自InfoQ独家对闲鱼Flutter团队的采访和Fish Redux的开源文档。开源背景在闲鱼接入Flutter之初,由于我们的落地的方案希望是从最复杂的几个主链路进行尝试来验证flutter完备性的,而我们的详情整体来讲业务比较复杂,主要体现在两个方面:页面需要集中状态管理,也就是说页面的不同组件共享一个数据来源,数据来源变化需要通知页面所有组件。页面的UI展现形式比较多(如普通详情、闲鱼币详情、社区详情、拍卖详情等),工作量大,所以UI组件需要尽可能复用,也就是说需要比较好的进行组件化切分。在我们尝试使用市面上已有的框架(google提供的redux以及bloc)的时候发现,没有任何一个框架可以既解决集中状态管理,又能解决UI的组件化的,因为本身这两个问题有一定的矛盾性(集中vs分治)。因此我们希望有一套框架能解决我们的问题,fish redux应运而生。fish redux本身是经过比较多次的迭代的,目前大家看到的版本经过了3次比较大的迭代,实际上也是经过了团队比较多的讨论和思考。第一个版本是基于社区内的flutter_redux进行的改造,核心是提供了UI代码的组件化,当然问题也非常明显,针对复杂的详情和发布业务,往往业务逻辑很多,无法做到逻辑代码的组件化。第二个版本针对第一个版本的问题,做出了比较重大的修改,解决了UI代码和逻辑代码的分治问题,但同时,按照redux的标准,打破了redux的原则,对于精益求精的闲鱼团队来讲,不能接受;因此,在第三个版本进行重构时,我们确立了整体的架构原则与分层要求,一方面按照reduxjs的代码进行了flutter侧的redux实现,将redux的原则完整保留下来。另一方面针对组件化的问题,提供了redux之上的component的封装,并创新的通过这一层的架构设计提供了业务代码分治的能力。至此,我们完成了fish redux的基本设计,但在后续的应用中,发现了业务组装以后的代码性能问题,针对该问题,我们再次提供了对应的adapter能力,保障了在长列表场景下的big cell问题。目前,fish redux已经在线上稳定运行超过3个月以上,未来,期待fish redux给社区带来更多的输入。Fish Redux技术解析分层架构图架构图:主体自底而上,分两层,每一层用来解决不通层面的问题和矛盾,下面依次来展开。ReduxRedux 是来自前端社区的一个数据管理框架,对 Native开发同学来说可能会有一点陌生,我们做一个简单的介绍。Redux 是做什么的?Redux 是一个用来做可预测易调试的数据管理的框架。所有对数据的增删改查等操作都由 Redux 来集中负责。Redux 是怎么设计和实现的?Redux 是一个函数式的数据管理的框架。传统 OOP 做数据管理,往往是定义一些 Bean,每一个 Bean 对外暴露一些 Public-API 用来操作内部数据(充血模型)。函数式的做法是更上一个抽象的纬度,对数据的定义是一些 Struct(贫血模型),而操作数据的方法都统一到具有相同函数签名 (T, Action) => T 的 Reducer 中。FP:Struct(贫血模型) + Reducer = OOP:Bean(充血模型)同时 Redux 加上了 FP 中常用的 Middleware(AOP) 模式和 Subscribe 机制,给框架带了极高的灵活性和扩展性。贫血模型、充血模型请参考:https://en.wikipedia.org/wiki/Plain_old_Java_objectRedux 的缺点Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点。在我们实际使用 Redux 中面临两个具体问题:Redux 的集中和 Component 的分治之间的矛盾;Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性。Fish Redux 的改良Fish Redux 通过 Redux 做集中化的可观察的数据管理。然不仅于此,对于传统 Redux 在使用层面上的缺点,在面向端侧 flutter 页面纬度开发的场景中,我们通过更好更高的抽象,做了改良。一个组件需要定义一个数据(Struct)和一个 Reducer。同时组件之间存在着父依赖子的关系。通过这层依赖关系,我们解决了【集中】和【分治】之间的矛盾,同时对 Reducer 的手动层层 Combine 变成由框架自动完成,大大简化了使用 Redux 的困难。我们得到了理想的集中的效果和分治的代码。对社区标准的 followState、Action、Reducer、Store、Middleware 以上概念和社区的 ReduxJS 是完全一致的。我们将原汁原味地保留所有的 Redux 的优势。如果想对 Redux 有更近一步的理解,请参考:https://github.com/reduxjs/reduxComponent组件是对局部的展示和功能的封装。 基于 Redux 的原则,我们对功能细分为修改数据的功能(Reducer)和非修改数据的功能(副作用 Effect)。于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的行为。这是一种面向当下,也面向未来的拆分。在面向当下的 Redux 看来,是数据管理和其他。在面向未来的 UI-Automation 看来是 UI 表达和其他。UI 的表达对程序员而言即将进入黑盒时代,研发工程师们会把更多的精力放在非修改数据的行为、修改数据的行为上。组件是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。关于 ViewView 仅仅是一个函数签名: (T,Dispatch,ViewService) => Widget它主要包含三方面的信息视图是完全由数据驱动。视图产生的事件/回调,通过 Dispatch 发出“意图”,不做具体的实现。需要用到的组件依赖等,通过 ViewService 标准化调用。比如一个典型的符合 View 签名的函数。关于 EffectEffect 是对非修改数据行为的标准定义,它是一个函数签名: (Context, Action) => Object它主要包含四方面的信息接收来自 View 的“意图”,也包括对应的生命周期的回调,然后做出具体的执行。它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们不崇尚持有数据,而通过上下文来获取最新数据。它不修改数据, 如果修要,应该发一个 Action 到 Reducer 里去处理。它的返回值仅限于 bool or Future, 对应支持同步函数和协程的处理流程。比如良好的协程的支持:关于 ReducerReducer 是一个完全符合 Redux 规范的函数签名:(T,Action) => T一些符合签名的 Reducer:同时我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。所以有这样的公式 Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)。一个典型的组装:通过 Component 的抽象,我们得到了完整的分治,多纬度的复用,更好的解耦。AdapterAdapter 也是对局部的展示和功能的封装。它为 ListView 高性能场景而生,它是 Component 实现上的一种变化。它的目标是解决 Component 模型在 flutter-ListView 的场景下的 3 个问题:1)将一个"Big-Cell"放在 Component 里,无法享受 ListView 代码的性能优化;2)Component 无法区分 appear|disappear 和 init|dispose ;3)Effect 的生命周期和 View 的耦合,在 ListView 的场景下不符合直观的预期。概括的讲,我们想要一个逻辑上的 ScrollView,性能上的 ListView ,这样的一种局部展示和功能封装的抽象。做出这样独立一层的抽象是我们看实际的效果,我们对页面不使用框架Component,使用框架 Component+Adapter 的性能基线对比。Reducer is long-lived, Effect is medium-lived, View is short-lived.我们通过不断的测试做对比,以某 Android机为例:使用框架前 我们的详情页面的 FPS,基线在 52FPS;使用框架, 仅使用 Component 抽象下,FPS 下降到 40, 遭遇“Big-Cell”的陷阱;使用框架,同时使用 Adapter 抽象后,FPS 提升到 53,回到基线以上,有小幅度的提升。Directory推荐的目录结构会是这样sample_page– action.dart– page.dart– view.dart– effect.dart– reducer.dart– state.dartcomponentssample_component– action.dart– component.dart– view.dart– effect.dart– reducer.dart– state.dart上层负责组装,下层负责实现, 同时会有一个插件提供, 便于我们快速填写。以闲鱼的详情场景为例的组装:组件和组件之间,组件和容器之间都完全的独立。Communication Mechanism组件|适配器内通信组件|适配器间内通信简单的描述:采用的是带有一段优先处理的广播, self-first-broadcast。发出的 Action,自己优先处理,否则广播给其他组件和 Redux 处理。最终我们通过一个简单而直观的 dispatch 完成了组件内,组件间(父到子,子到父,兄弟间等)的所有的通信诉求。Refresh Mechanism数据刷新局部数据修改,自动层层触发上层数据的浅拷贝,对上层业务代码是透明的。层层的数据的拷贝:一方面是对 Redux 数据修改的严格的 follow。另一方面也是对数据驱动展示的严格的 follow。视图刷新扁平化通知到所有组件,组件通过 shouldUpdate 确定自己是否需要刷新。Fish Redux的优点数据的集中管理通过 Redux 做集中化的可观察的数据管理。我们将原汁原味地保留所有的 Redux 的优势,同时在 Reducer 的合并上,变成由框架代理自动完成,大大简化了使用 Redux 的繁琐度。组件的分治管理组件既是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。View、Reducer、Effect 隔离将组件拆分成三个无状态的互不依赖的函数。因为是无状态的函数,它更易于编写、调试、测试、维护。同时它带来了更多的组合、复用和创新的可能。声明式配置组装组件、适配器通过自由的声明式配置组装来完成。包括它的 View、Reducer、Effect 以及它所依赖的子项。良好的扩展性核心框架保持自己的核心的三层关注点,不做核心关注点以外的事情,同时对上层保持了灵活的扩展性。框架甚至没有任何的一行的打印的代码,但我们可通过标准的 Middleware 来观察到数据的流动,组件的变化。在框架的核心三层外,也可以通过 dart 的语言特性 为 Component 或者 Adapter 添加 mixin,来灵活的组合式地增强他们的上层使用上的定制和能力。框架和其他中间件的打通,诸如自动曝光、高可用等,各中间件和框架之间都是透明的,由上层自由组装。精小、简单、完备它非常小,仅仅包含 1000 多行代码;它使用简单,完成几个小的函数,完成组装,即可运行;它是完备的。关于未来开源之后,闲鱼打算通过以下方式来维护Fish Redux:通过后续的一系列的对外宣传,吸引更多的开发者加入或者使用。目前Flutter生态里,应用框架还是空白,有机会成为事实标准;配合后续的一系列的闲鱼Flutter移动中间件矩阵做开源;进一步提供,一系列的配套的开发辅助调试工具,提升上层Flutter开发效率和体验。Fish Redux 目前已在阿里巴巴闲鱼技术团队内多场景,深入应用。最后 Talk is cheap, Show me the code,我们今天正式在GitHub上开源,更多内容,请到GitHub了解。GitHub地址:https://github.com/alibaba/fish-redux本文作者:闲鱼技术-吉丰阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 7, 2019 · 2 min · jiezi

同样做前端,为何差距越来越大?

阿里妹导读:前端应用越来越复杂,技术框架不断变化,如何成为一位优秀的前端工程师,应对更大的挑战?今天,阿里前端技术专家会影结合实际工作经验,沉淀了五项重要方法,希望能对你的职业发展、团队协作有所启发。过去一年,阿里巴巴新零售事业群支撑的数据相关业务突飞猛进,其中两个核心平台级产品代码量急速增长,协同开发人员增加到数十人。由于历史原因,开发框架同时基于 React 和 Angular,考虑到产品的复杂性、人员的短缺和技术背景各异,我们尝试了各种方法打磨工具体系来提升开发效率,以下分享五点。一、基于 Redux 的状态管理从2013年React发布至今已近6个年头,前端框架逐渐形成 React/Vue/Angular 三足鼎立之势。几年前还在争论单向绑定和双向绑定孰优孰劣,现在三大框架已经不约而同选择单向绑定,双向绑定沦为单纯的语法糖。框架间的差异越来越小,加上 Ant-Design/Fusion-Design/NG-ZORRO/ElementUI 组件库的成熟,选择任一你熟悉的框架都能高效完成业务。那接下来核心问题是什么?我们认为是状态管理。简单应用使用组件内 State 方便快捷,但随着应用复杂度上升,会发现数据散落在不同的组件,组件通信会变得异常复杂。我们先后尝试过原生 Redux、分形 Fractal 的思路、自研类 Mobx 框架、Angular Service,最终认为 Redux 依旧是复杂应用数据流处理最佳选项之一。庆幸的是除了 React 社区,Vue 社区有类似的 Vuex,Angular 社区有 NgRx 也提供了几乎同样的能力,甚至 NgRx 还可以无缝使用 redux-devtools 来调试状态变化。无论如何优化,始终要遵循 Redux 三原则:这三个问题我们是通过自研 iron-redux 库【1】来解决,以下是背后的思考:如何组织 Action?action type 需要全局惟一,因此我们给 action type 添加了 prefix,其实就是 namespace 的概念;为了追求体验,请求(Fetch)场景需要处理 3 种状态,对应 LOADING/SUCCESS/ERROR 这 3 个action,我们通过 FetchTypes 类型来自动生成对应到 3 个 action。如何组织 Store/Reducer?reducer 和 view 不必一一对应,应用中同时存在组件树和状态树,按照各自需要去组织,通过 connect 来绑定状态树的一个或多个分支到组件树;通过构造一些预设数据类型来减少样板代码。对于 Fetch 返回的数据我们定义了 AsyncTuple 这种类型,减少了样板代码;明确的组织结构,第1层是 ROOT,第2层是各个页面,第3层是页面内的卡片,第4层是卡片的数据,这样划分最深处基本不会超过5层。最终我们得到如下扁平的状态树。虽庞大但有序,你可以快速而明确的访问任何数据。如何减少样板代码?使用原生 Redux,一个常见的请求处理如下。非常冗余,这是 Redux 被很多人诟病的原因:使用 iron-redux 后:代码量减少三分之二!!主要做了这2点:引入了预设的 AsyncTuple 类型,就是 {data: [], loading: boolean, error: boolean} 这样的数据结构;使用 AsyncTuple.handleAll 处理 LOADING/SUCCESS/ERROR 这 3 种 action,handleAll 的代码很简单,使用 if 判断 action.type 的后缀即可,源码【2】。曾经 React 和 Angular 是两个很难调和的框架,开发中浪费了我们大量的人力。通过使用轻量级的 iron-redux,完全遵循 Redux 核心原则下,我们内部实现了除组件层以外几乎所有代码的复用。开发规范、工具库达成一致,开发人员能够无缝切换,框架差异带来的额外成本降到很低。二、全面拥抱 TypeScriptTypeScript 目前可谓大红大紫,根据 2018 stateofjs【3】,超过 50% 的使用率以及 90% 的满意度,甚至连 Jest 也正在从 Flow 切换到 TS【4】。如果你还没有使用,可以考虑切换,绝对能给项目带来很大提升。过去一年,我们从部分使用 TS 变为全面切换到 TS,包括我们自己开发的工具库等。TS 最大的优势是它提供了强大的静态分析能力,结合 TSLint 能对代码做到更加严格的检查约束。传统的 EcmaScript 由于没有静态类型,即使有了 ESLint 也只能做到很基本的检查,一些 typo 问题可能线上出了 Bug 后才被发现。下图是一个前端应用常见的4层架构。 代码和工具全面拥抱 TS 后,实现了从后端 API 接口到 View 组件的全链路静态分析,具有了完善的代码提示和校验能力。除了上面讲的 iron-redux,我们还引入 Pont 【5】实现前端取数,它可以自动把后端 API 映射到前端可调用的请求方法。Pont 实现原理:(法语:桥) 是我们研发的前端取数层框架。对接的后端 API 使用 Java Swagger,Swagger 能提供所有 API 的元信息,包括请求和响应的类型格式。Pont 解析 API 元信息生成 TS 的取数函数,这些取数函数类型完美,并挂载到 API 模块下。最终代码中取数效果是这样的:Pont 实现的效果有:根据方法名自动匹配 url、method,并且对应到 prams、response 类型完美,并能自动提示;后端 API 接口变更后,前端相关联的请求会自动报错,再也不担心后端悄悄改接口前端不知晓;再也不需要前后端接口约定文档,使用代码保证前端取数和后端接口定义完全一致。另外 iron-redux 能接收到 Pont 接口响应数据格式,并推导出整个 Redux 状态树的静态类型定义,Store 中的数据完美的类型提示。效果如下:最终 TS 让代码更加健壮,尤其是对于大型项目,编译通过几乎就代表运行正常,也给重构增加了很多信心。三、回归 Sass/Less2015 年我们就开始实践 CSS Modules,包括后来的 styled-components 等,到 2019 年 css-in-js 方案依旧争论不休,虽然它确实解决了一些 CSS 语言天生的问题,但同时增加了不少成本,新手不够友好、全局样式覆盖成本高涨、伪类处理复杂、与AntD等组件库结合有坑。与此同时 Sass/Less 社区也在飞速发展,尤其是 Stylelint 【6】的成熟,可以通过技术约束的手段来避免 CSS 的 Bad Parts。全局污染:约定每个样式文件只能有一个顶级类,如 .home-page{ .top-nav {//}, .main-content{ // } }。如果有多个顶级类,可以使用 Stylelint rule 检测并给出警告。依赖管理不彻底。借助 webpack 的 css-loader,已够用。JS 和 CSS 变量共享。关于 JS 和 Sass/Less 变量共享,我们摸索出了自己的解法:在 scss 文件中,可以直接引用变量:四、开发工具覆盖全链路2019 年,你几乎不可能再开发出 React/Angular/Vue 级别的框架,也没必要再造 Ant-Design/Fusion-Design/Ng-Zorro 这样的轮子。难道就没有机会了吗?当然有,结合你自身的产品开发流程,依旧有很多机会。下面是常规项目的开发流程图,任何一个环节只要深挖,都有提升空间。如果你能通过工具减少一个或多个环节,带来的价值更大。单拿其中的【开发】环节展开,就有很多可扩展的场景:一个有代表性的例子是,我们开发了国际化工具 kiwi【7】。它同样具有 TS 的类型完美,非常强大的文案提示,另外还有:VS Code 插件 kiwi linter【8】,自动对中文文案标红,如果已有翻译文案能自动完成替换;Shell 命令全量检查出没有翻译的文案,批量提交给翻译人员;Codemod 脚本自动实现旧的国际化方案向 Kiwi 迁移,成本极低。除了以上三点,未来还计划开发浏览器插件来检查漏翻文案,利用 Husky 在 git 提交前对漏翻文案自动做机器翻译等等。未来如果你只提供一个代码库,那它的价值会非常局限。你可以参照上面的图表,开发相应的扩展来丰富生态。如果你是新手,推荐学习下编译原理和对应的扩展开发规范。五、严格彻底的 Code Review过去的一年,我们一共进行了 1200+ 多次 Code Review(CR),很多同事从刚开始不好意思提 MR(GitLab Merge Request,Code Review 的一种方式) 到后来追着别人 Review,CR 成为每个人的习惯。通过 CR 让项目中任何一行代码都至少被两人触达过,减少了绝大多数的低级错误,提升了代码质量,这也是帮助新人成长最快的方式之一。![[其中一个项目MR截图]](https://upload-images.jianshu…Code Review 的几个技巧:No magic;Explicit not implicit;覆盖度比深度重要,覆盖度追求100%;频率比仪式感重要,坐公交蹲厕所打开手机都可以 Review 别人代码,不需要专门组织会议;粒度要尽可能小,一个组件一个方法均可,可以结合 Git Flow;24h 小时内处理,无问题直接 merge,有问题一定要留 comment,并且提供 action;对于亟待上线来不及 Review 的代码,可以先合并上线,上线后再补充 Review;需要自上而下的推动,具有完善的规范,同时定期总结 Review 经验来丰富开发规范;CR 并不只是为了找错,看到好的代码,不要吝啬你的赞美;本质是鼓励开发者间更多的沟通,互相学习,营造技术文化氛围。总结以上5点当然不是我们技术的全部。除此之外我们还实践了移动端开发、可视化图表/WebGL、Web Worker、GraphQL、性能优化等等,但这些还停留在术的层面,未来到一定程度会拿出来分享。如果你也准备或正在开发复杂的前端应用,同时团队人员多样技术背景各异,可以参考以上5点,使用 Redux 实现规范清晰可预测的状态管理,深耕 TypeScript 来提升代码健壮性和可维护性,借助各种 Lint 工具回归简单方便的 CSS,不断打磨自己的开发工具来保证开发规范高效,并严格彻底实行 Code Review 促进人的交流和提升。本文作者:会影阅读原文本文来自云栖社区合作伙伴“阿里技术”,如需转载请联系原作者。 ...

March 7, 2019 · 2 min · jiezi

十分钟理解Redux中间件

由于一直用业界封装好的如redux-logger、redux-thunk此类的中间件,并没有深入去了解过redux中间件的实现方式。正好前些时间有个需求需要对action执行时做一些封装,于是借此了解了下Redux Middleware的原理。* 中间件概念首先简单提下什么是中间件,该部分与下文关系不大,可以跳过。来看眼这个经典的图。不难发现:不使用middleware时,在dispatch(action)时会执行rootReducer,并根据action的type更新返回相应的state。而在使用middleware时,简言之,middleware会将我们当前的action做相应的处理,随后再交付rootReducer执行。简单实现原理比如现有一个action如下:function getData() { return { api: ‘/cgi/getData’, type: [GET_DATA, GET_DATA_SUCCESS, GET_DATA_FAIL] }}我们希望执行该action时可以发起相应请求,并且根据请求结果由定义的type匹配到相应的reducer,那么可以自定义中间件处理该action,因此该方法封装成中间件之前可能是这样的:function dispatchPre(action, dispatch) { const api = action.api; const [ fetching_type, success_type, fail_type] = action.type; // 拉取数据 const res = await request(api); // 拉取时状态 dispatch({type: fetching_type}); // 成功时状态 if (res.success) { dispatch({type: success_type, data: res.data}); console.log(‘GET_SUCCESS’); } // 失败时状态 if (res.fail) { dispatch({type: fail_type}); console.log(‘GET_FAIL’); };}// 调用: dispatchPre(action())那如何封装成中间件,让我们在可以直接在dispatch(action)时就做到这样呢?可能会首先想到改变dispatch指向// 储存原来的dispatchconst dispatch = store.dispatch;// 改变dispatch指向store.dispatch = dispatchPre;// 重命名const next = dispatch;截止到这我们已经了解了中间件的基本原理了~源码分析了解了基本原理能有助于我们更快地读懂middleware的源码。一般我们会这样添加中间件并使用。createStore(rootReducer, applyMiddleware.apply(null, […middlewares]))接下来我们可以重点关注这两个函数createStore、applyMiddlewareCreateStore// 摘至createStoreexport function createStore(reducer, rootState, enhance) { … if (typeof enhancer !== ‘undefined’) { if (typeof enhancer !== ‘function’) { throw new Error(‘Expected the enhancer to be a function.’) } /* 若使用中间件,这里 enhancer 即为 applyMiddleware() 若有enhance,直接返回一个增强的store对象 / return enhancer(createStore)(reducer, preloadedState) } …}ApplyMiddleware再看看applyMiddleware做了什么,applyMiddleware函数非常简单,就十来行代码,这里将其完整复制出来。export default function applyMiddleware(…middlewares) { return createStore => (…args) => { const store = createStore(…args) let dispatch = () => { throw new Error( Dispatching while constructing your middleware is not allowed. + Other middleware would not be applied to this dispatch. ) } const middlewareAPI = { getState: store.getState, dispatch: (…args) => dispatch(…args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(…chain)(store.dispatch) return { …store, dispatch } }}执行步骤可以将其主要功能按步骤划分如下:1、依次执行middleware。将middleware执行后返回的函数合并到一个chain数组,这里我们有必要看看标准middleware的定义格式,如下export default store => next => action => {}// 即function (store) { return function(next) { return function (action) { return {} } }}那么此时合并的chain结构如下[ …, function(next) { return function (action) { return {} } }]2、改变dispatch指向。想必你也注意到了compose函数,compose函数如下:[…chain].reduce((a, b) => (…args) => a(b(…args)))实际就是一个柯里化函数,即将所有的middleware合并成一个middleware,并在最后一个middleware中传入当前的dispatch。这里再使用一个简单的例子方便大家理解。// 假设chain如下:chain = [ a: next => action => { console.log(‘第1层中间件’) return next(action) } b: next => action => { console.log(‘第2层中间件’) return next(action) } c: next => action => { console.log(‘根dispatch’) return next(action) }]调用compose(…chain)(store.dispatch)后返回a(b(c(dispatch)))。可以发现已经将所有middleware串联起来了,并同时修改了dispatch的指向。最后看一下这时候compose执行返回,如下dispatch = a(b(c(dispatch)))// 调用dispatch(action)// 执行循序/ 1. 调用 a(b(c(dispatch)))(action) print: 第1层中间件 2. 返回 a: next(action) 即b(c(dispatch))(action) 3. 调用 b(c(dispatch))(action) print: 第2层中间件 4. 返回 b: next(action) 即c(dispatch)(action) 5. 调用 c(dispatch)(action) print: 根dispatch 6. 返回 c: next(action) 即dispatch(action) 7. 调用 dispatch(action)*/ ...

March 1, 2019 · 2 min · jiezi

React单页如何规划路由、设计Store、划分模块、按需加载

本项目地址:react-coat-helloworldreact-coat 同时支持浏览器渲染(SPA)和服务器渲染(SSR),本 Demo 仅演示浏览器渲染,请先了解一下:react-coat第一站:Helloworld安装git clone https://github.com/wooline/react-coat-helloworld.gitnpm install运行npm start 以开发模式运行npm run build 以产品模式编译生成文件npm run prod-express-demo 以产品模式编译生成文件并启用一个 express 做 demonpm run gen-icon 自动生成 iconfont 文件及 ts 类型查看在线 Demo点击查看在线 Demo关于脚手架采用 webpack 4.0 为核心搭建,无二次封装,干净透明采用 typescript 作开发语言,使用 Postcss 及 less 构建 css不使用 css module,用模块化命名空间保证 css 不冲突采用 editorconfig > prettier 作统一的风格配置,建议使用 vscode 作为 IDE,并安装 prettier 插件以自动格式化采用 tslint、eslint、stylelint 作代码检查PeerDependencies开发环境需要很多的 dependencies,你可以自行安装特定版本,如果特殊要求,建议本站提供的 react-coat-pkg 以及 react-coat-dev-pkg,它们已经包含了绝大部分 dependencies。TS 类型的定义使用 Typescript 意味着使用强类型,我们把业务实体中 TS 类型定义分两大类:API类型和Entity类型。API 类型:指的是来自于后台 API 输入的类型,它们可能直接由 swagger 生成,或是机器生成。Entity 类型:指的是本系统为业务实体建模而定义的类型,每个业务实体(resource)都会有定义。理想状况下,API 类型和 Entity 类型会保持一致,因为业务逻辑是同一套,但实际开发中,可能因为前后端并行开发、或者前后端视角不同而出现两者各表。为了充分的解耦,我们允许这种不一致,我们把 API 类型在源头就转化为 Entity 类型,而在本系统的代码逻辑中,不直接使用 API 类型,应当使用自已定义的 Entity 类型,以减少其它系统对本系统的影响。假定项目:旅途 web app主要页面:旅游路线展示旅途小视频展示站内信展示(需登录)评论展示 (访客可查看评论,发表则需登录)项目要求web SPA 单页应用主要用于 mobile 浏览器,也可以适应于桌面浏览器无 SEO 要求,但需要能将当前页面分享给他人初次进入本站时,显示 welcome 广告,并倒计时路由规划SPA 单页不就一个页面么?为什么还需要规划路由呢?其一,为了用户刷新时尽可能的保持当前展示其二,为了用户能将当前展示通过 url 分享给他人其三,为了后续的 SEOpath 规划根据项目需求及 UI 图,我们初步规划主要路由 path 如下:旅行路线列表 photosList:/photos旅行路线详情 photosItem:/photos/:photoId分享小视频列表 videosList:/videos分享小视频详情 videosItem:/videos/:videoId站内信列表 messagesList:/messages参数规划因为列表页是有分页、有搜索的,所以列表类型的路由是有参数的,比如:/photos?title=张家界&page=3&pageSize=20我们估且将这部分查询列表条件叫"ListSearch",但除了ListSearch之外,也可能会出现别的路由参数,用来控制其它条件(本 demo 暂未涉及),比如:/photos?title=张家界&page=3&pageSize=20&showComment=true所以,如果参数一多,用扁平的一维结构就变得不好表达。而且,利用 URL 参数存数据,数据将全变成为字符串。比如id=2,你无法知道 2 是数字型还是字符型,这样会让后续接收处理变得繁重。所以,我们使用 JSON 来序列化第二级参数,比如:/photos?search={title:“张家界”,page:3,pageSize:20}&showComment=true这样做也有个不好的地方,就是需要 encodeURI,然后特殊字符会变得比较丑。路由参数默认值为了缩短 URL 长度,本框架设计了参数默认值,如果某参数和默认值相同,可以省去。我们需要做两项工作:生成 Url 查询条件时,对比默认值,如果相同,则省去原值:{title:“张家界”,page:1,pageSize:20} 默认值: {title:"",page:1,pageSize:20},省去后为:{title:“张家界”}原值:{title:"",page:1,pageSize:20} 默认值: {title:"",page:1,pageSize:20},省去后为:空收到 Url 查询条件时,将查询条件和默认值 merge/photos?search={page:2} === photos?search={title:"",page:2,pageSize:20}/photos === photos?search={title:"",page:1,pageSize:20}处理 null、undefined由于接收 Url 参数时,如果某 key 为 undefined,我们会用相应的默值将其填充,所以不能将 undefined 作为路由参数值定义,改为使用 null。也就是说,路由参数中的每一项,都是必填的,比如:// 路由参数定义时,每一项都必填,以下为错误示例interface ListSearch{ title?:string, age?:number}// 改为如下正确定义:interface ListSearch{ title:string | null, age:number | null}区分:原始路由参数(SearchData) 默认路由参数(SearchData) 和 完整路由参数(WholeSearchData)。完整路由参数(WholeSearchData) = merage(默认路由参数(SearchData), 原始路由参数(SearchData))原始路由参数(SearchData)每一项都是可选的,用 TS 类型表示为:Partial<WholeSearchData>完整路由参数(WholeSearchData)每一项都是必填的,用 TS 类型表示为:Required<SearchData>默认路由参数(SearchData)和完整路由参数(WholeSearchData)类型一致不直接使用路由状态路由及其参数本质上也是一种 Store,与 Redux Store 一样,反映当前程序的某些状态。但它是片面的,是瞬时的,是不稳定的,我们把它看作是 Redux Store 的一种冗余。所以最好不要在程序中直接依赖和使用它,而是控制住它的入口和出口,第一时间在其源头进行消化转换,让其成为整个 Redux Store 的一部分,后续的运行中,我们直接依赖 Redux Store。这样,我们就将程序与路由设计解耦了,程序有更大的灵活度甚至可以迁移到无 URL 概念的其它运行环境中。模块规划模块与 Page 无关划分模块可以很好的拆解功能,化繁为简,并且对内隐藏细节,对外暴露少量接口。划分模块的标准是高内聚,低耦合,而不是以 Page 或是 View,一个模块包含某些完整的业务功能,这些功能可能涉及到多个 Page 或多个 View。所以回过头,看我们的项目需求和 UI 图,大体上可以分为三个模块:photos //旅游线路展示videos //分享视频展示messages //站内消息展示这三个模块显而易见,但是我们注意到:“图片详情”和“视频详情”都包含“评论展示”,而“评论展示”本身又具有分页、排序、详情展示、创建回复等功能,它具有自已独立的逻辑,只不过在 view 上被 photoDetail 和 videoDetail 嵌套了,所以将“评论展示”独立划分成一个模块是合适的。另个,整个程序应当有个启动模块,它是“上帝视角模块”,它可以做一些公共事业,必要的时候也可以用来做多个模块之间的协调和调度,我们叫把它叫做 applicatioin 模块。所以最终,本 Demo 被划分为 5 个模块:app // 启动模块photos //旅游线路展示videos //分享视频展示messages //站内消息展示comments //评论展示为模块划分 View每个模块可能包含一组 View,View 反映某些特定的业务逻辑。View 就是 React 中的 Component,那反过来 Component 就是 View 么?非也,它们之间还是有些区别的:view 展现的是 Store 数据,更偏重于表现特定的具体的业务逻辑,所以它的 props 一般是直接用 mapStateToProps connect 到 store。component 体现的是一个没有业务逻辑上下文的纯组件,它的 props 一般来源于父级传递。component 通常是公共的,而 view 通常非公用回过头,看我们的项目需求和 UI 图,大体上划分以下 view:app views:Main、TopNav、BottomNav、LoginPop、Welcome、Loadingphotos views:Main、List、Detailsvideos views:Main、List、Detailsmessages views:Main、Listcomments views:Main、List、Details、Editor目录结构经过上面的分析,我们有了项目大至的骨架,由于模块比较少,所以我们就不再用二级目录分类了:src├── asset // 存放公共静态资源│ ├── css│ ├── imgs│ └── font├── entity // 存放业务实体TS类型定义├── common // 存放公共代码├── components // 存放React公共组件├── modules│ ├── app│ │ ├── views│ │ │ ├── TopNav│ │ │ ├── BottomNav│ │ │ ├── …│ │ │ └── index.ts //导出给其它模块使用的view│ │ ├── model.ts //定义ModuleState和ModuleActions│ │ ├── api //将本模块需要的后台api封装一下│ │ ├── facade.ts //导出本模块对外的逻辑接口(类型、Actions、路由默认参数)│ │ └── index.ts //导出本模块实体(view和model)│ ├── photos│ │ ├── views│ │ ├── model.ts│ │ ├── api│ │ ├── facade.ts│ │ └── index.ts│ ├── videos│ ├── messages│ ├── comments│ ├── names.ts //定义模块名,使用枚举类型来保证不重复│ └── index.ts //导出模块的全局设置,如RootState类型、模块载入方式等└──index.tsx 启动入口facade.ts其它目录都好理解,注意到每个 module 目录中,有一个 facade.ts 的文件,冒似它与 index.ts 一样都是导出本模块,那为什么不合并成一个呢?index.ts 导出的是整个模块的物理代码,因为模块是较为独立的,所以我们一般希望将整个模块的代码打包成一个独立的 chunk 文件。facade.ts 仅导出本模块的一些类型和逻辑接口,我们知道 TS 类型在编译之后是会被彻底抹去的,而接口仅仅是一个空的句柄。假如在 ModuleA 中需要 dispatch ModuleB 的 action,我们仅需要 import ModuleB 的 facade.ts,它只是一个空的句柄而以,并不会引起两个模块代码的物理依赖。配置模块问:在 react-coat 中怎么配置一个模块?包括打包、加载、注册、管理其生命周期等?答:./src/modules 根目录下的 index.ts 文件为模块总的配置文件,增加一个模块,只需要在此配置一下// ./src/modules/index.ts// 一个验证器,利用TS类型来确保增加一个module时,相关的配置都同时增加了type ModulesDefined<T extends {[key in ModuleNames]: any}> = T;// 定义模块的加载方案,同步或者异步均可export const moduleGetter = { [ModuleNames.app]: () => { return import(/* webpackChunkName: “app” / “modules/app”); }, [ModuleNames.photos]: () => { return import(/ webpackChunkName: “photos” / “modules/photos”); }, [ModuleNames.videos]: () => { return import(/ webpackChunkName: “videos” / “modules/videos”); }, [ModuleNames.messages]: () => { return import(/ webpackChunkName: “messages” / “modules/messages”); }, [ModuleNames.comments]: () => { return import(/ webpackChunkName: “comments” */ “modules/comments”); },};export type ModuleGetter = ModulesDefined<typeof moduleGetter>; // 验证一下是否有模块忘了配置// 定义整站Module Statesinterface States { [ModuleNames.app]: AppState; [ModuleNames.photos]: PhotosState; [ModuleNames.videos]: VideosState; [ModuleNames.messages]: MessagesState; [ModuleNames.comments]: CommentsState;}// 定义整站的Root Stateexport type RootState = BaseState & ModulesDefined<States>; // 验证一下是否有模块忘了配置路由和加载本 Demo 直接使用 react-router V4,路由即组件,所以并不需要什么特别的路由配置,直接在./app/views/Main.tsx 中:const PhotosView = loadView(moduleGetter, ModuleNames.photos, “Main”);const VideosView = loadView(moduleGetter, ModuleNames.videos, “Main”);const MessagesView = loadView(moduleGetter, ModuleNames.messages, “Main”);<Switch> <Redirect exact={true} path="/" to="/photos" /> <Route exact={false} path="/photos" component={PhotosView} /> <Route exact={false} path="/videos" component={VideosView} /> <Route exact={false} path="/messages" component={MessagesView} /> <Route component={NotFound} /></Switch>使用 loadView()表示异步按需加载一个 View,如果你不想按需加载,完全可以直接 import:import {Main as PhotosView} from “modules/photos/views"载入 View 时自动载入其相关的模块并初始化 Model。没有 Model,view 是没有“灵魂”的,所以在载入 View 时,框架会自动载入其 Model 并完成初始化,这个过程包含 3 步:1.载入模块对应的 JS Chunk 包2.初始化模块 Model,派发 module/INIT Action3.模块可以监听自已的 module/INIT Action,作出初始化行为,如获取远程数据等Redux Store 结构module 的划分不仅体现在工程目录上,而体现在 Redux Store 中: router: { // 由 connected-react-router 生成 location: { pathname: ‘/photos’, search: ‘’, hash: ‘#refresh=true’, key: ‘gb9ick’ }, action: ‘PUSH’ }, app: {…}, // app ModuleState photos: { // photos ModuleState isModule: true, // 框架自动生成,标明该节点为一个ModuleState listSearch: { // 列表搜索条件 title: ‘’, page: 1, pageSize: 10 }, listItems: [ // 列表数据 { id: ‘1’, title: ‘新加坡+吉隆坡+马六甲6或7日跟团游’, departure: ‘无锡’, type: ‘跟团游’, price: 2499, hot: 265, coverUrl: ‘/imgs/1.jpg’ }, … ], listSummary: { page: 1, pageSize: 5, totalItems: 10, totalPages: 2 } }, messages: {…}, // messages ModuleState comments: {…}, // comments ModuleState}具体实现见 Demo 源码,有注释美中不足路由规划的不足到目前为止,本 Demo 完成了项目要求中的内容,接下来,业务看了之后提出了几个问题:无法分享指定的“评论”,评论是很重要的吸引眼球的内容,我们希望分享链接时,可以指定评论。目前可以分享的路由只有 5 种:- /photos- /photos/1- /videos- /videos/1- /messages看样子,我们得增加:/photos/1/comments/3 //展示id为3的评论评论内容对以后的 SEO 很重要,我们希望路由能控制评论列表翻页和排序:/photos/1?comments-search={page:2,sort:“createDate”}目前我们的项目主要用于移动浏览器访问,很多 android 用户习惯用手机下面的返回键,来撤消操作,如关闭弹窗等,能否模拟一下原生 APP?思考:android 用户点击手机下面的返回键会引起浏览器的后退,后退关闭弹窗,那就需要在弹出弹窗时增加一条 URL 记录结论:Url 路由不只用来记录展示哪个 Page、哪个 View,还得标识一些交互操作,完全颠覆了传统的路由观念了。路由效验的不足看样子,路由会越来越复杂,到目前为止,我们还没有在 TS 中很好的管理路由参数,拼接 URL 时没有做 TS 类型的校验。对于 pathname 我们都是直接用字符串写死在程序中,比如:if(pathname === “/photos”){ ….}const arr = pathname.match(/^/photos/(\d+)$/);这样直接 hardcode 似利不是很好,如果后其产品想换一下名称怎么搞。Model 中重复写同样的代码注意到,photos/model.ts、videos/model.ts 中,90%的代码是一样的,为什么?因为它们两个模块基本上功能都是差不多的:列表展示、搜索、获取详情…其实不只是 photos 和 videos,套用 RestFul 的理念,我们用网页交互的过程就是在对“资源 Resource”进行维护,无外乎“增删改查”这些基本操作,大部分情况下,它们的逻辑是相似的。由其是在后台系统中,基本上连 UI 界面也可以标准化,如果将这部分“增删改查”的逻辑提取出来,模块可以省去不少重复的代码。下一个 Demo既然有这么多美中不足,那我们就期待在下一个 Demo 中一步步解决它吧进阶:SPA(单页应用) ...

February 28, 2019 · 4 min · jiezi

又一轮子?Typescript+React+Redux,放弃saga,支持服务器渲染同构

你是原生Redux用户?有没有觉得写Redux太繁琐了?你是dvaJS用户?有没有觉得redux-saga概念太多,且yield无法返回TS类型?试试react-coat吧:项目地址:https://github.com/wooline/react-coat// 仅需一个类,搞定 action、reducer、effect、loadingclass ModuleHandlers extends BaseModuleHandlers { @reducer protected putCurUser(curUser: CurUser): State { return {…this.state, curUser}; } @reducer public putShowLoginPop(showLoginPop: boolean): State { return {…this.state, showLoginPop}; } @effect(“login”) // 使用自定义loading状态 public async login(payload: {username: string; password: string}) { const loginResult = await sessionService.api.login(payload); if (!loginResult.error) { this.updateState({curUser: loginResult.data}); Toast.success(“欢迎您回来!”); } else { alert(loginResult.error.message); } } // uncatched错误会触发@@framework/ERROR,监听并发送给后台 @effect(null) // 不需要loading,设置为null protected async ["@@framework/ERROR"](error: CustomError) { if (error.code === “401”) { this.dispatch(this.actions.putShowLoginPop(true)); } else if (error.code === “301” || error.code === “302”) { this.dispatch(this.routerActions.replace(error.detail)); } else { Toast.fail(error.message); await settingsService.api.reportError(error); } } // 监听自已的INIT Action,做一些异步数据请求 @effect() protected async “app/INIT” { const [projectConfig, curUser] = await Promise.all([ settingsService.api.getSettings(), sessionService.api.getCurUser() ]); this.updateState({ projectConfig, curUser, }); }}react-coat 特点集成 react、redux、react-router、history 等相关框架仅为以上框架的糖衣外套,不改变其基本概念,无强侵入与破坏性结构化前端工程、业务模块化,支持按需加载同时支持 SPA(单页应用)和 SSR(服务器渲染)使用 typescript 严格类型,更好的静态检查与智能提示开源微框架,源码不到千行,几乎不用学习即可上手与 Dva 的异同引入 ActionHandler 观察者模式,更优雅的处理模块之间的协作去除 redux-saga,使用 async、await 替代,简化代码的同时对 TS 类型支持更全面原生使用 typescript 组织和开发,更全面的类型安全路由组件化、无 Page 概念、更自然的 API 和更简单的组织结构更大的灵活性和自由度,不强封装脚手架等支持 SPA(单页应用)和 SSR(服务器渲染)快速切换,支持模块异步按需加载和同步加载快速切换差异示例:使用强类型组织所有 reducer 和 effect// Dva中常这样写dispatch({ type: ‘moduleA/query’, payload:{username:“jimmy”}} })//本框架中可直接利用ts类型反射和检查:this.dispatch(moduleA.actions.query({username:“jimmy”}))差异示例:State 和 Actions 支持继承// Dva不支持继承// 本框架可以直接继承class ModuleHandlers extends ArticleHandlers<State, PhotoResource> { constructor() { super({}, {api}); } @effect() protected async parseRouter() { const result = await super.parseRouter(); this.dispatch(this.actions.putRouteData({showComment: true})); return result; } @effect() protected async ModuleNames.photos + “/INIT” { await super.onInit(); }}差异示例:在 Dva 中,因为使用 redux-saga,假设在一个 effect 中使用 yield put 派发一个 action,以此来调用另一个 effect,虽然 yield 可以等待 action 的派发,但并不能等待后续 effect 的处理:// 在Dva中,updateState并不会等待otherModule/query的effect处理完毕了才执行effects: { * query (){ yield put({type: ‘otherModule/query’,payload:1}); yield put({type: ‘updateState’, payload: 2}); }}// 在本框架中,可使用awiat关键字, updateState 会等待otherModule/query的effect处理完毕了才执行class ModuleHandlers { async query (){ await this.dispatch(otherModule.actions.query(1)); this.dispatch(thisModule.actions.updateState(2)); }}差异示例:如果 ModuleA 进行某项操作成功之后,ModuleB 或 ModuleC 都需要 update 自已的 State,由于缺少 action 的观察者模式,所以只能将 ModuleB 或 ModuleC 的刷新动作写死在 ModuleA 中:// 在Dva中需要主动Put调用ModuleB或ModuleC的Actioneffects: { * update (){ … if(callbackModuleName===“ModuleB”){ yield put({type: ‘ModuleB/update’,payload:1}); }else if(callbackModuleName===“ModuleC”){ yield put({type: ‘ModuleC/update’,payload:1}); } }}// 在本框架中,可使用ActionHandler观察者模式:class ModuleB { //在ModuleB中兼听"ModuleA/update" action async [“ModuleA/update”] (){ …. }}class ModuleC { //在ModuleC中兼听"ModuleA/update" action async [“ModuleA/update”] (){ …. }}遵循规则:M 和 V 之间使用单向数据流整站保持单个 StoreStore 为 Immutability 不可变数据改变 Store 数据,必须通过 Reducer调用 Reducer 必须通过显式的 dispatch ActionReducer 必须为 pure function 纯函数有副作用的行为,全部放到 Effect 函数中每个 reducer 只能修改 Store 下的某个节点,但可以读取所有节点路由组件化,不使用集中式配置快速上手及 Demo本框架上手简单8 个新概念:Effect、ActionHandler、Module、ModuleState、RootState、Model、View、Component4 步创建:exportModel(), exportView(), exportModule(), createApp()3 个 Demo,循序渐进:入手:Helloworld进阶:SPA(单页应用)升级:SPA(单页应用)+SSR(服务器渲染) ...

February 28, 2019 · 2 min · jiezi

Redux 学习总结 (React)

在 React 的学习和开发中,如果 state (状态)变得复杂时(例如一个状态需要能够在多个 view 中使用和更新),使用 Redux 可以有效地管理 state,使 state tree 结构清晰,方便状态的更新和使用。当然,Redux 和 React 并没有什么关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。只是对我来说目前主要需要在 React 中使用,所以在这里和 React 联系起来便于理解记忆。数据流Action只是描述 state (状态)更新的动作,即“发生了什么”,并不更新 state。const ADD_TODO = ‘ADD_TODO’{ type: ADD_TODO, text: ‘Build my first Redux app’}type:必填,表示将要执行的动作,通常会被定义成字符串常量,尤其是大型项目。除了 type 外的其他字段:可选,自定义,通常可传相关参数。例如上面例子中的 text。Action 创建函数简单返回一个 Action:function addTodo(text) { return { type: ADD_TODO, text }}dispatch Action:dispatch(addTodo(text))// 或者创建一个 被绑定的 action 创建函数 来自动 dispatchconst boundAddTodo = text => dispatch(addTodo(text))boundAddTodo(text)帮助生成 Action 创建函数的库(对减少样板代码有帮助):redux-actionsredux-actReducer说明在发起 action 后 state 应该如何更新。是一个纯函数:只要传入参数相同,返回计算得到的下一个 state 就一定相同。(previousState, action) => newState注意,不能在 reducer 中执行的操作:修改传入的参数执行有副作用的操作,如 API 请求和路由跳转调用非纯函数,如 Date.now() 或 Math.random()import { combineReducers } from ‘redux’import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters} from ‘./actions’const { SHOW_ALL } = VisibilityFiltersfunction visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state }}function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ …state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) default: return state }}const todoApp = combineReducers({ visibilityFilter, todos})export default todoAppStoreRedux 应用只有一个单一的 store。维持应用的 state;提供 getState() 方法获取 state;提供 dispatch(action) 方法更新 state;通过 subscribe(listener) 注册监听器;通过 subscribe(listener) 返回的函数注销监听器。import { createStore } from ‘redux’import todoApp from ‘./reducers’let store = createStore( todoApp, [preloadedState], // 可选,state 初始状态 enhancer)import { createStore, combineReducers, applyMiddleware, compose } from ‘redux’import thunk from ‘redux-thunk’import DevTools from ‘./containers/DevTools’import reducer from ‘../reducers/index’export default function configureStore() { const store = createStore( reducer, compose( applyMiddleware(thunk), DevTools.instrument() ) ); return store;}react-reduxconnect() 方法(mapStateToProps、mapDispatchToProps)替代 store.subscribe(),从 Redux state 树中读取部分数据,并通过 props 提供给要渲染的组件。import { bindActionCreators } from ‘redux’;import { connect } from ‘react-redux’;import * as actions from ‘./actions’;class App extends Component { handleAddTodo = () => { const { actions } = this.props; actions.addTodo(‘Create a new todo’); } render() { const { todos } = this.props; return ( <div> <Button onClick={this.handleAddTodo}>+</Button> <ul> {todos.map(todo => ( <Todo key={todo.id} {…todo} /> ))} </ul> </div> ); }}function mapStateToProps(state) { return { todos: state.todos };}function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ addTodo: actions.addTodo }, dispatch) }}export default connect( mapStateToProps, mapDispatchToProps)(App);Provider 组件import React from ‘react’import { render } from ‘react-dom’import { Provider } from ‘react-redux’import configureStore from ‘./store/configureStore’import App from ‘./components/App’render( <Provider store={configureStore()}> <App /> </Provider>, document.getElementById(‘root’)API 请求一般情况下,每个 API 请求都需要 dispatch 至少三种 action:通知 reducer 请求开始的 action { type: ‘FETCH_POSTS_REQUEST’ }reducer 可能会 {…state, isFetching: true}一种通知 reducer 请求成功的 action { type: ‘FETCH_POSTS_SUCCESS’, response: { … } }reducer 可能会 {…state, isFetching: false, data: action.response}一种通知 reducer 请求失败的 action { type: ‘FETCH_POSTS_FAILURE’, error: ‘Oops’ }reducer 可能会 {…state, isFetching: false, error: action.error}使用 middleware 中间件实现网络请求:redux-thunkredux-sagaredux-thunk通过使用指定的 middleware,action 创建函数除了返回 action 对象外还可以返回函数。这时,这个 action 创建函数就成为了 thunk。路由跳转(react-router)参考资料:Redux 中文文档React Native Training ...

February 27, 2019 · 2 min · jiezi

Redux 进阶:中间件的使用

什么是 middleware用过 Express 或 Koa 类似框架的同学可能知道,在 Express 中,中间件(middleware)就是在 req 进来之后,在我们真正对 req 进行处理之前,我们先对 req 进行一定的预处理,而这个预处理的过程就由 middleware 来完成。同理,在 Redux 中,middleware 就是扩展了在 dispatch action 之后,到 action 到达 reducer 之前之间的中间这段时间,而中间的这段时间就是 dispatch 的过程,所以 Redux 的 middleware 的原理就是改造 dispatch。自定义 middleware让我们先从一个最简单的日志 middleware 定义开始:const logger = store => next => action => { console.group(’logger’); console.warn(‘dispatching’, action); let result = next(action); console.warn(’next state’, store.getState()); console.groupEnd(); return result;};这个 logger 函数就是一个 Redux 中的 middleware ,它的功能是在 store.dispatch(action)(对应 middleware 中的 next(action)) 之前和之后分别打印出一条日志。从我们的 logger 中可以看到,我们向 middleware 中传入了 store,以便我们在 middleware 中获取使用 store.getState() 获取 state,我们还在之后的函数中传入了 next,而最后传入的 action 就是我们平时 store.dispatch(action) 中的 action,所以 next(action) 对应的就是 dispatch(action)。最后我们还需要调用并 next(action) 来执行原本的 dispatch(action)。使用 middleware最后我们可以在使用 createStore() 创建 store 的时候,把这个 middleware 加入进去,使得每次 store.dispathc(action) 的时候都会打印出日志:import { createStore, applyMiddleware } from ‘redux’; // 导入 applyMiddlewareconst store = createStore(counter, applyMiddleware(logger));注意,这里我们使用了 Redux 提供的 applyMiddleware() 来在创建 store 的时候应用 middleware,而 applyMiddleware() 返回的是一个应用了 middleware 的 store enhancer,也就是一个增强型的 store。createStore() 接受三个参数,第一个是 reducer,第二个如果是对象,那么就被作为 store 的初始状态,第三个就是 store enhancer,如果第二个参数是函数,那么就被当作 store enhancer。关于 applyMiddleware 和我们自定义的 logger 是如何一起工作的,这个我们稍后再讲。为了说明后一条日志 console.warn(’next state’, store.getState()) 是在执行了 reducer 之后打印出来的,我们在 reducer 中也打印一个消息。改造后的 reducer: function counter(state = 0, action) {+ console.log(‘hi,这条 log 从 reducer 中来’); switch(action.type) { case ‘INCREMENT’: return state + 1; case ‘DECREMENT’: return state - 1; default : return state; } }结果这里,我使用了 #1 中的计数器作为例子。可以看到,在 reducer 中打印的消息处于 middleware 日志的中间,这是因为在 logger middleware 中,将 let result = next(action); 写在了最后一条消息的前面,一旦调用了 next(action),就会进入 reducer 或者进入下一个 middleware(如果有的话)。类似 Koa 中间件的洋葱模型。其实 next(action) 就相当于 store.dispatch(action),意思是开始处理下一个 middleware,如果没有 middleware 了就使用原始 Redux 的 store.dispatch(action) 来分发动作。这个是由 Redux 的 applyMiddleware 来处理的,那么 applyMiddleware() 是如何实现对 middleware 的处理的呢?稍后我们会对它进行简单的讲解 。❓applyMiddleware 是如何实现的从 applyMiddleware 的设计思路 中,我们可以看到 Redux 中的 store 只是包含一些方法(dispatch()、subscribe()、getState()、replaceReducer())的对象。我们可以使用const next = store.dispatch;来先引用原始 store 中的 dispatch 方法,然后等到合适的时机,我们再调用它,实现对 dispatch 方法的改造。Middleware 接收一个名为 next 的 dispatch 函数(只是 dispatch 函数的引用),并返回一个改造后的 dispatch 函数,而返回的 dispatch 函数又会被作为下一个 middleware 的 next,以此类推。所以,一个 middleware 看起来就会类似这样:function logger(next) { return action => { console.log(‘在这里中一些额外的工作’) return next(action) }}其中,在 middleware 中返回的 dispatch 函数接受一个 action 作为参数(和普通的 dispatch 函数一样),最后再调用 next 函数并返回,以便下一个 middleware 继续,如果没有 middleware 则 直接返回。由于 store 中类似 getState() 的方法依旧非常有用,我们将 store 作为顶层的参数,使得它可以在所有 middleware 中被使用。这样的话,一个 middleware 的 API 最终看起来就变成这样:function logger(store) { return next => { return action => { console.log(‘dispatching’, action) let result = next(action) console.log(’next state’, store.getState()) return result } }}值得一提的是,Redux 中使用到了许多函数式编程的思想,如果你对curringcompose…比较陌生的话,建议你先去补充以下函数式编程思想的内容。applyMiddleware 的源码❓middleware 有什么应用的场景打印日志,比如上面我们自定义的 middleware;异步 action,比如用户对服务器发起请求,在等待返回响应的时间里,我们可以更新 UI 为 Loading,等到响应返回时,我们再调用 store.dispatch(action) 来更新新的 UI;…一个使用异步 action 请求 Github API 的例子通过仿照 redux-thunk,我们也可以自己写一个支持异步 action 的 middleware,如下:const myThunkMiddleware = store => next => action => { if (typeof action === ‘function’) { // 如果 action 是函数,一般的 action 为纯对象 return action(store.dispatch, store.getState); // 调用 action 函数 } return next(action);};异步 action creator :export function fetchGithubUser(username = ‘bbbbx’) { return dispatch => { // 先 dispatch 一个同步 action dispatch({ type: ‘INCREMENT’, text: ‘加载中…’ }); // 异步 fetch Github API fetch(https://api.github.com/search/users?q=${username}) .then(response => response.json()) .then(responseJSON => { // 异步请求返回后,再 dispatch 一个 action dispatch({ type: ‘INCREMENT’, text: responseJSON }); }); };}修改 reducer,使它可以处理 action 中的 action.text:function counter(state = { value: 0, text: ’’ }, action) { switch(action.type) { case ‘INCREMENT’: return { value: state.value + 1, text: action.text }; case ‘DECREMENT’: return { value: state.value - 1, text: action.text }; default : return state; }}再改造一下 Counter 组件,展示 Github 用户:// Counter.jsclass Counter extends React.Component { constructor(props) { super(props); this.state = { username: ’’ }; } handleChange(event) { this.setState({ username: event.target.value }); } handleSearch(event) { event.preventDefault(); if (this.state.username === ‘’) { return ; } this.props.fetchGithubUser(this.state.username); } render() { const { text, value, increment, decrement } = this.props; let users = text; if (text.items instanceof Array) { if (text.items.length === 0) { users = ‘用户不存在!’; } else { users = text.items.map(item => ( <li key={item.id}> <p>用户名:<a href={item.html_url}>{item.login}</a></p> <img width={100} src={item.avatar_url} alt=‘item.avatar_url’ /> </li> )); } } return ( <div> Click: {value} times {’ ‘} <button onClick={increment} >+</button>{’ ‘} <button onClick={decrement} >-</button>{’ ‘} <div> <input type=‘text’ onChange={this.handleChange.bind(this)} /> <button onClick={this.handleSearch.bind(this)} >获取 Github 用户</button>{’ ‘} </div> <br /> <b>state.text:{users}</b> </div> ); }}结果使用已有的 Redux 中间件redux-thunk利用 redux-thunk ,我们可以完成各种复杂的异步 action,尽管 redux-thunk 这个 middleware 只有 数十行 代码。先导入 redux-thunk:import thunkMiddleware from ‘redux-thunk’;const store = createStore( counter, applyMiddleware(thunkMiddleware));之后便可定义异步的 action creator 了:export function incrementAsync(delay = 1000) { return dispatch => { dispatch(decrement()); setTimeout(() => { dispatch(increment()); }, delay); };}使用: <button onClick={increment} >+</button>{’ ‘} <button onClick={decrement} >-</button>{’ ‘}+ <button onClick={() => incrementAsync(1000) } >先 - 1 ,后再 + 1</button>{’ ‘}注意,异步 action creator 要写成 onClick={() => incrementAsync(1000) } 匿名函数调用的形式。结果 ...

February 26, 2019 · 4 min · jiezi

React通过redux缓存列表数据以及滑动位置,回退时恢复页面状态

在使用React和React-router实现单页面应用时,会有这样一个场景:从列表页面点击某项条目进入详情页,然后回退至列表页面时,列表页面会重新刷新,不仅数据重新获取了,滚动条也回到了顶部。用户要继续查看剩余数据的话,需要重新滑动到之前点击的那个条目,如果列表做了分页的话就更麻烦了,这对于用户体验来说是非常不好的。所以我们希望能做到,从二级页面回退至列表页面时,列表页面能保留之前的状态(数据和滚动条位置)。那么怎么实现呢?下面分享一下React通过redux来缓存列表数据以及滑动位置,以达到保留列表页面状态的方法。关于redux以及react-redux的使用,这里就不做讲解了,可以参考我之前写的 React-redux的原理以及使用 。当然网络上有很多讲解得更清晰的文章,读者可以自行搜索。下面直接进入正题,介绍实现需求的步骤吧1、安装redux以及react-reduxcnpm install redux react-redux -dev –save2、编写操作列表页面相关数据的action/** * Created by RaoMeng on 2018/12/10 * Desc: 列表数据缓存 /import {CLEAR_LIST_STATE, LIST_STATE} from “../constants/actionTypes”;import store from ‘../store/store’/* * 保存列表状态 * @param data * @returns {Function} /export const saveListState = (data) => { return () => { store.dispatch({ type: LIST_STATE, …data }) }}/* * 清除列表状态 * @returns {Function} /export const clearListState = () => { return () => { store.dispatch({ type: CLEAR_LIST_STATE }) }}这里实现了两个actionType,一个是保存列表状态,一个是清除列表状态。保存列表状态就是为了达到回退时不刷新页面的需求;清除列表状态则是因为:从菜单页面进入列表页面时,是要求重新加载页面数据的,假如不清除redux中的缓存数据,页面就会读取缓存数据而不会重新请求网络数据,所以这个action也是很有必要的。3、实现配合action操作state的reducerimport {CLEAR_LIST_STATE, LIST_STATE} from “../constants/actionTypes”;const initListState = { scrollTop: 0,//列表滑动位置 listData: [],//列表数据 pageIndex: 1,//当前分页页码 itemIndex: -1,//点击的条目index}const redListState = (state = initListState, action) => { if (action === undefined) { return state } switch (action.type) { case LIST_STATE: //更新列表状态 return { …state, …action } case CLEAR_LIST_STATE: //清空列表状态 return initListState default: return state }}export default redListState/* * Created by RaoMeng on 2018/12/10 * Desc: 数据处理中心 */import {combineReducers} from ‘redux’import redUserInfo from ‘./redUserInfo’import redListState from ‘./redListState’import redClassData from ‘./redClassData’const reducers = combineReducers({redUserInfo, redListState, redClassData})export default reducers这里解释下为什么要记录分页页码以及点击的条目index。记录分页页码只是在列表数据做了分页的情况下需要。是为了回退到列表页面后,用户继续上拉加载数据时页码是正确的。记录点击的条目index则是为了能在详情页更新所点击的条目数据。比如说一个会议签到列表,用户点击某条数据进入详情页后,点击签到按钮,这时我们要根据itemIndex来调用action的saveListState()()方法更新缓存中相应的数据,将该条数据的状态改为已签到。这样回退至列表页面时,该条数据的展示才会正确。4、创建storeimport {createStore} from ‘redux’import reducers from ‘../reducers/index’import {persistStore, persistReducer} from ‘redux-persist’;import storage from ‘redux-persist/lib/storage’;import autoMergeLevel2 from ‘redux-persist/lib/stateReconciler/autoMergeLevel2’;const persistConfig = { key: ‘root’, storage: storage, stateReconciler: autoMergeLevel2 // 查看 ‘Merge Process’ 部分的具体情况};const myPersistReducer = persistReducer(persistConfig, reducers)const store = createStore(myPersistReducer)export const persistor = persistStore(store)export default store这里用到了redux-persist来实现redux数据的持久化存储,我在 React通过redux-persist持久化数据存储 有做简单讲解。5、在点击条目的回调事件中调用saveListState方法保存列表状态 <父布局 ref={el => { this.container = el }} > </父布局> onItemClick = index => { console.log(‘scrollTop’, ReactDOM.findDOMNode(this.container).scrollTop) saveListState({ scrollTop: ReactDOM.findDOMNode(this.container).scrollTop, listData: this.state.meetingSignList, pageIndex: mPageIndex, itemIndex: index, })() const {meetingSignList} = this.state this.props.history.push(’/meet-detail/’ + meetingSignList[index].meetId) }通过ReactDOM.findDOMNode(this.container).scrollTop来获取父布局的滑动距离6、在页面的componentDidMount方法中获取redux数据首先通过react-redux的connect方法将state中的数据绑定到页面的props中,方便访问let mapStateToProps = (state) => ({ listState: {…state.redListState}})let mapDispatchToProps = (dispatch) => ({})export default connect(mapStateToProps, mapDispatchToProps)(MeetingSignIn)这样,在页面中就可以通过this.props.listState来访问redux中缓存的列表数据了然后,在componentDidMount中获取缓存的列表数据,如果有缓存数据,则加载,如果没有则重新请求componentDidMount() { document.title = ‘会议管理’ console.log(’listState’, this.props.listState) if (this.props.listState && !isObjEmpty(this.props.listState.listData)) { this.setState({ meetingSignList: this.props.listState.listData, isLoading: false, }, () => { ReactDOM.findDOMNode(this.container).scrollTop = this.props.listState.scrollTop }) mPageIndex = this.props.listState.pageIndex } else { Toast.loading(‘数据加载中…’, 0) mPageIndex = 0 this.loadMeetList() } }这样就实现了React通过redux缓存列表数据以及滑动位置,回退时恢复页面状态的需求。 ...

February 22, 2019 · 2 min · jiezi

技本功丨知否知否,Redux源码竟如此意味深长(下集)

上集回顾Redux是如何使用的?首先再来回顾一下这个使用demo(谁让这段代码完整地展示了redux的使用)如果有小伙伴对这段代码不是很理解的话,建议先去学习Redux的使用再来看这篇源码,这样更加事半功倍。通过上段代码,我们拆分几个比较核心的点,我一一列举一下:action的结构是如何的?如何去定义一个reducer?combineReducers是如何整合多个reducer的?createStore是如何创建一个store?5.dispatch拿到action到底干了什么?subscribe是如何监听状态发生改变的?getState是如何拿到所有的状态值的?上期我们先解决了前三个疑问,这期我们一起来探索后4个问题。4、createStore是如何创建一个store?首先我们先撸一个createStore架构出来:通过这段代码我们知道了传参应该是什么样子和返回了什么。从中我发现了一个问题,createStore接受的是三个参数:1、reducer 2、预加载的state 3、redux-thunk之类的增强器。但是我们平时经常会写成如下这个样子:我们会在第二个参数就传入了增强器,这跟源代码的参数结构不符哎,但是为什么就可以这么用了。接下来我们就看一下,reducer是如何做这个处理的。当第二个参数preloadedState的类型是Function的时候,并且第三个参数enhancer未定义的时候,此时preloadedState将会被赋值给enhancer,preloadedState会替代enhancer变成undefined的。有了这么一层转换之后,我们就可以大胆地第二个参数传enhancer了。解决了这个疑问之后,往下就是解释一下他返回的值是什么东西,这些解答我们就放在下面做解释,这里就不做赘述了。不过在接下去之前,我们得搞清楚下面这组变量代表啥意思。其中变量isDispatching,作为锁来用,我们redux是一个统一管理状态容器,它要保证数据的一致性,所以同一个时间里,只能做一次数据修改,如果两个action同时触发reducer对同一数据的修改,那么将会带来巨大的灾难。所以变量isDispatching就是为了防止这一点而存在的。5、dispatch拿到action到底干了啥?函数dispatch在函数体一开始就进行了三次条件判断,分别是以下三个: 1.判断action是否为简单对象 2.判断action.type是否存在判断当前是否有执行其他的reducer操作当前三个预置条件判断都成立时,才会执行后续操作,否则抛出异常。在执行reducer的操作的时候用到了try-finally,可能大家平时try-catch用的比较多,这个用到的还是比较少。执行前isDispatching设置为true,阻止后续的action进来触发reducer操作,得到的state值赋值给currentState,完成之后再finally里将isDispatching再改为false,允许后续的action进来触发reducer操作。接着一一通知订阅者做数据更新,不传入任何参数。最后返回当前的action。6、subscribe是如何监听状态发生改变的?在注册订阅者之前,做了两个条件判断:判断监听者是否为函数是否有reducer正在进行数据修改(保证数据的一致性)接下来执行了函数ensureCanMutateNextListeners,下面我们看一下ensureCanMutateNextListeners函数的具体实现逻辑:逻辑很简单,判断nextListeners和currentListeners是否为同一个引用,还记得初始变量定义那以及函数dispatch内部那两处的代码吗?这两处将nextListeners和currentListeners引用了同一个数组,而ensureCanMutateNextListeners就是用来判断这种情况的,当nextListeners和currentListeners为同一个引用时,则做一层浅拷贝,这里用的就是Array.prototype.slice方法,该方法会返回一个新的数组,这样就可以达到浅拷贝的效果。函数ensureCanMutateNextListeners作为处理之后,将新的订阅者加入nextListeners中,并且返回取消订阅的函数unsubscribe。函数unsubscribe执行时,也会执行两个条件判断:是否已经取消订阅(已取消的不必执行)是否有reducer正在进行数据修改(保证数据的一致性)通过条件判断之后,将该订阅者从nextListeners中删除。看到这里可能有小伙伴们对currentListeners和nextListeners有这么一个疑问?函数dispatch里面将二者引用同一个数组,为啥这里将二者分别引用两个值相同的数组?直接用currentListeners不可以吗?这里这样做其实也是为了数据的一致性,因为有这么一种的情况存在。当redux在通知所有订阅者的时候,此时又有一个新的订阅者加进来了。如果只用currentListeners的话,当新的订阅者插进来的时候,就会打乱原有的顺序,从而引发一些严重的问题。7、getState是如何拿到所有的状态值的?getState相比较dispatch要简单许多,返回currentState即可,而这个currentState在每次dispatch得时候都会得到响应的更新。同样是为了保证数据的一致性,当在reducer操作的时候,是不可以读取当前的state值的。看完是不是已满腔热血充满了斗志?

February 19, 2019 · 1 min · jiezi

技本功丨知否知否,Redux源码竟如此意味深长(上集)

夫 子 说元月二号欠下袋鼠云技术公号一篇关于Redux源码解读的文章,转眼月底,期间常被“债主”上门催债。由于年底项目工期比较紧,于是债务就这样被利滚利。但是好在这段时间有点闲暇,于是赶紧把这篇文章给完成了。据说文章点赞多了可以抵扣利息,小伙们要是觉得我这篇文章还不错的话,记得帮我点赞哦!好让我早日摆脱债务,感激不尽!好了,回到正题。今天打算和大家讲一讲redux的源码,通过分析源码,我个人觉得受益匪浅,借此通过这篇文章把我的一些心得体会向大家分享一下,另外需要注意一下这次分享的源码用的redux的V4.0.0版本,小伙伴在对照的时候可别搞错咯。接下来老司机可是要发车了,大家抓紧时间上车哦!在讲源码之前我们首先回顾一下redux是如何使用的,下面我们看一下使用demo:上面这段代码完整地展示了redux的使用(如果有小伙伴对这段代码不是很理解的话,建议先去学习redux的使用再来看这篇源码,这样更加事半功倍)。通过上段代码,我们拆分几个比较核心的点,我一一列举一下:action的结构是如何的?如何去定义一个reducer?combineReducers是如何整合多个reducer的?createStore是如何创建一个store?5.dispatch拿到action到底干了什么?subscribe是如何监听状态发生改变的?getState是如何拿到所有的状态值的?本期先解决前三个疑问,让我们一起去源码里寻找答案!1、action的结构是如何的?首先得解释一下action是干嘛的,它是负责把数据从应用带到store里面,也是store的唯一数据来源,并由以下两个部分组成:type (操作类型)payload (携带的数据)为什么得有这两个?其实也很好理解,我们拿银行来类比。某天,你拿着一万块来到银行,走到柜台,人业务员第一件事肯定是问你要办啥业务,存钱?转账?还是还贷?你得把这些告诉业务员,不然业务没法给你办理业务,因此我们action就得有一个type,好让reducer知道你要干啥。当然,你办理存款或者是还款啥的,必不能少的就是毛爷爷了,payload对应的值就好比这些毛爷爷。用一个话来总结action的作用就是:告诉reducer拿着payload去做type这件事。2、如何去定义一个reducer?上面讲action的时候,提到了reducer了,这里还是拿我上面的银行做个类比,当我们拿着钱去银行存钱,我们不可能自己去银行把银行保险柜打开,完了把钱放进去,这样是不允许的,我们得需要业务员这个中间人去帮我们做存钱这件事,而业务员所扮演的角色正好就是reducer所要担任的角色。接下来讲一下如何去定义一个reducer,其实reducer的写法并没有绝对的写法,只要符合下面几个条件都能称之为reducer:必须得是一个函数。函数接收两个参数。第一个:该reducer所负责的state。第二个:action。函数体内部可以针对不同的action的type做出响应,这里你可以if-else或者switch-case都是可以的。函数必须有返回值。当修改state了之后,必须将修改后的state返回。如果遇到未知的type则需要返回一个默认值。3、combineReducers是如何整合多个reducer的?我们先看一下combineReducers传入的参数:combineReducers接受的是一个参数首先得是对象,其次该对象每一个属性对应一个reducer。搞清楚combineReducers的结构之后,我们再看一下combineReducers对其做了哪些处理。第一步:浅拷贝reducers这里定义了一个finalReducers和finalReducerKeys,分别用来拷贝reducers和其属性。先用Object.keys方法拿到reducers所有的属性,然后进行for循环,每一项可根据其属性拿到对应的reducer,并浅拷贝到finalReducers中,但是前提条件是每个reducer的类型必须是Function,不然会直接跳过不拷贝。第二步:检测finalReducers里的每个reducer是否都有默认返回值assertReducerShape方法主要检测两点:不能占用redux内部特有的命名空间如果遇到未知的action的类型,reducer不能返回undefined,得返回默认的值如果传入type为 @@redux/INIT<随机值> 的action,返回undefined,说明没有对未 知的action的类型做响应,需要加默认值。如果对应type为 @@redux/INIT<随机值> 的action返回不为undefined,但是却对应type为 @@redux/PROBE_UNKNOWN_ACTION_<随机值> 返回为undefined,说明占用了 命名空间。整个逻辑相对简单,好好自己梳理一下。第三步:返回一个函数,用于代理所有的reducer先对传入的state用getUnexpectedStateShapeWarningMessage做了一个异常检测,找出state里面没有对应reducer的key,并提示开发者做调整。接着我们跳到getUnexpectedStateShapeWarningMessage里,看其实现。getUnexpectedStateShapeWarningMessage接收四个参数inputState(state)、reducers(finalReducers)、action(action)、unexpectedKeyCache(unexpectedKeyCache),这里要说一下unexpectedKeyCache是上一次检测inputState得到的其里面没有对应的reducer集合里的异常key的集合。整个逻辑如下:前置条件判断,保证reducers集合不为{}以及inputState为简单对象找出inputState里有的key但是 reducers集合里没有key如果是替换reducer的action,跳过第四步,不打印异常信息将所有异常的key打印出来getUnexpectedStateShapeWarningMessage分析完之后,我们接着看后面的代码。首先定义了一个hasChanged变量用来表示state是否发生变化,遍历reducers集合,将每个reducer对应的原state传入其中,得出其对应的新的state。紧接着后面对新的state做了一层未定义的校验,函数getUndefinedStateErrorMessage的代码如下:逻辑很简单,仅仅做了一下错误信息的拼接。未定义校验完了之后,会跟原state作对比,得出其是否发生变化。最后发生变化返回nextState,否则返回state。未完待续下期预告《技本功丨知否知否,Redux源码竟如此意味深长(下集)》THE END最后,袋萌萌感谢每一位老铁2018年的陪伴,生死看淡,不服就干!2019,咱们再战,不断进步!

February 18, 2019 · 1 min · jiezi

redux源码解析

1.前言关于redux的基本概念和工作流如何进行的这里就不进行过多概述了,可以查看相关文档去了解。流程图链接2.redux源码结构以下是redux的源码结构图,主要的就是以下几个文件组成,我们接下来按顺序进行介绍其中原理和实现过程。3.createStore.js首先了解下createStore.js。通过调用createStore创建唯一的store,store中暴露出getState,dispatch,subscribe,replaceReducer这几个方法。通常我们用到的主要是前三个方法,这里作为主要介绍内容。如下是createStore的主要内容:export function createStore(reducer, preloadedState, enhancer) { /** * 以下的判断都是对传入的参数进行验证 / if( (typeof preloadedState === ‘function’ && typeof enhancer === ‘function’) || (typeof enhancer === ‘function’ && typeof arguments[3] === ‘function’) ) { throw new Error(‘只能传递一个enhancer到createStore()中’) } if(typeof preloadedState === ‘function’ && typeof enhancer === ‘undefined’) { enhancer = preloadedState preloadedState = undefined } if(typeof enhancer !== ‘undefined’) { if(typeof enhancer !== ‘function’) { throw new Error(’enhancer应该为一个函数’) } return enhancer(createStore)(reducer, preloadedState) } if(typeof reducer !== ‘function’) { throw new Error(‘reducer应该为一个函数’) } /* * 初始化参数 / let currentReducer = reducer //初始化reducer let currentState = preloadedState //初始化state let currentListeners = [] //初始化subscribe监听函数数组 let nextListeners = currentListeners let isDispatching = false /* * 复制一份currentListeners,为了防止在dispatch的时候 * 调用subscribe和unsubscribe时候发生错误 / function ensureCanMutateNextListeners() { if(nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } /* * 获取当前的state / function getState() { if(isDispatching) { throw new Error(‘不可以在isDispatching的时候调用getState’) } return currentState } /* * 订阅监听事件,触发dispatch后执行 / function subscribe(listener) { if(typeof listener != ‘function’) { throw new Error(‘Expected the listener to be a function.’) } if(isDispatching) { throw new Error(‘isDispatching的时候无法调用’) } let isSubscribed = true ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { if(!isSubscribed) { //正在解除监听事件的时候不向下执行 return } if(isDispatching) { throw new Error(‘正在dispatch的时候不给执行’) } isSubscribed = false ensureCanMutateNextListeners() const index = nextListeners.indexOf(listener) nextListeners.splice(index) } } /* * 执行好dispatch循环调用每个subscribe的函数 / function dispatch() { //关于验证的代码就不写了 const listeners = (currentListeners = nextListeners) for(let i=0; i<listeners.length; i++) { listenersi } return action } /* * 替换当前的reducer然后重新初始化一次dispatch / function replaceReducer(nextReducer) { currentReducer = nextReducer dispatch({type: ‘@INITACTION’}) } //初始化执行dispatch dispatch({type: ‘@INITACTION’})}4. combineReducers.jscombineReducers,它接收多个reducer函数,并整合,归一化成一个rootReducer。其返回值rootReducer将会成为createStore的参数,完成store的创建。combineReducers只接收一个参数,这个参数阐述了不同reducer函数和页面状态数据树不同部分的映射匹配关系。const combineReducers = (reducers) => { return (state={}, action) => { Object.keys(reducers).reduce((nextState, key) => { nextState[key] = reducers[key](state[key], action) return nextState }, {}) }}5. applyMiddleware.js可以通过此方法给redux在触发action到reducer的过程中增加一个中间环节。applyMiddleware返回的内容我们称为enhancer。这个是createStore方法的最后一个参数,并且是可选的。在redux源码中涉及中间件的脚本有applyMiddleware.js、createStore.js、compose.js。那么applyMiddleware(…middlewares)中会发生什么事情。在createStore.js中有一段源码如下:export default function createStore(reducer, preloadedState, enhancer) { //… return enhancer(createStore)(reducer, preloadedState) //…}顾名思义,applyMiddleware就是对各个需要的中间件进行糅合,并作为createStore的第二个或者第三个参数传入。用于增强store。源码如下:const combineReducers = (reducers) => { return (state = {}, action) => { return Object.keys(reducers).reduce((nextState, key) => { nextState[key] = reducers[key](state[key], action) return nextState }, {}) }}export default function applyMiddleware(…middlewares) { return (next) => { return (reducer, initialState) => { var store = next(reducer, initialState) var dispatch = store.dispatch var chain = [] //包装一下store的getState和dispatch方法 //是第三方中间件需要使用的参数 var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } //每个中间件也是一个高度柯里化的函数,它接收middlewareAPI参数后的第一次返回结果并存储到chain数组中 //chain数组中每一项都是对dispatch的增强,并进行控制权转移。 chain = middlewares.map(middleware => middleware(middlewareAPI)) //这里的dispatch函数就是增强后的dispatch,因此compose方法接收了chain数组和原始dispatch方法。 dispatch = compose(…chain, store.dispatch) return { …store, dispatch } } }}export default function compose(…funcs) { if(funcs.length === 0) { return arg => arg } if(funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (…args) => a(b(…args)))}6. compose.js这个方法在applymiddleware中介绍了,可以在上面看到。7.bindActionCreators.js这个模块涉及的内容较少,我们直接去看源码:function bindActionCreator(actionCreator, dispatch) { //这个函数主要作用就是返回一个函数,当我们调用返回的这个函数的时候 //会自动的dispatch对应的action return function() { return dispatch(actionCreator.apply(this, args)) }}/* 参数说明: actionCreators: action create函数,可以是一个单函数,也可以是一个对象,这个对象的所有元素都是action create函数 dispatch: store.dispatch方法*/export default function bindActionCreators(actionCreators, dispatch) { // 如果actionCreators是一个函数的话,就调用bindActionCreator方法对action create函数和dispatch进行绑定 if (typeof actionCreators === ‘function’) { return bindActionCreator(actionCreators, dispatch) } // actionCreators必须是函数或者对象中的一种,且不能是null if (typeof actionCreators !== ‘object’ || actionCreators === null) { throw new Error( bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. + Did you write "import ActionCreators from" instead of "import * as ActionCreators from"? ) } // 获取所有action create函数的名字 const keys = Object.keys(actionCreators) // 保存dispatch和action create函数进行绑定之后的集合 const boundActionCreators = {} for (let i = 0; i < keys.length; i++) { const key = keys[i] const actionCreator = actionCreators[key] // 排除值不是函数的action create if (typeof actionCreator === ‘function’) { // 进行绑定 boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } // 返回绑定之后的对象 /** boundActionCreators的基本形式就是 { actionCreator: function() {dispatch(actionCreator.apply(this, arguments))} } */ return boundActionCreators} ...

February 15, 2019 · 3 min · jiezi

React通过redux-persist持久化数据存储

在React项目中,我们经常会通过redux以及react-redux来存储和管理全局数据。但是通过redux存储全局数据时,会有这么一个问题,如果用户刷新了网页,那么我们通过redux存储的全局数据就会被全部清空,比如登录信息等。这个时候,我们就会有全局数据持久化存储的需求。首先我们想到的就是localStorage,localStorage是没有时间限制的数据存储,我们可以通过它来实现数据的持久化存储。但是在我们已经使用redux来管理和存储全局数据的基础上,再去使用localStorage来读写数据,这样不仅是工作量巨大,还容易出错。那么有没有结合redux来达到持久数据存储功能的框架呢?当然,它就是redux-persist。redux-persist会将redux的store中的数据缓存到浏览器的localStorage中。redux-persist的使用1、对于reducer和action的处理不变,只需修改store的生成代码,修改如下import {createStore} from ‘redux’import reducers from ‘../reducers/index’import {persistStore, persistReducer} from ‘redux-persist’;import storage from ‘redux-persist/lib/storage’;import autoMergeLevel2 from ‘redux-persist/lib/stateReconciler/autoMergeLevel2’;const persistConfig = { key: ‘root’, storage: storage, stateReconciler: autoMergeLevel2 // 查看 ‘Merge Process’ 部分的具体情况};const myPersistReducer = persistReducer(persistConfig, reducers)const store = createStore(myPersistReducer)export const persistor = persistStore(store)export default store2、在index.js中,将PersistGate标签作为网页内容的父标签import React from ‘react’;import ReactDOM from ‘react-dom’;import {Provider} from ‘react-redux’import store from ‘./redux/store/store’import {persistor} from ‘./redux/store/store’import {PersistGate} from ‘redux-persist/lib/integration/react’;ReactDOM.render(<Provider store={store}> <PersistGate loading={null} persistor={persistor}> {/网页内容/} </PersistGate> </Provider>, document.getElementById(‘root’));这就完成了通过redux-persist实现React持久化本地数据存储的简单应用3、最后我们调试查看浏览器中的localStorage缓存数据发现数据已经存储到了localStorage中,此时刷新网页,redux中的数据也不会丢失 ...

February 14, 2019 · 1 min · jiezi

React-redux基础

前言在学习了React之后, 紧跟着而来的就是Redux了~ 在系统性的学习一个东西的时候, 了解其背景、设计以及解决了什么问题都是非常必要的。接下来记录的是, 我个人在学习Redux时的一些杂七杂八Redux是什么通俗理解https://www.zhihu.com/questio…介绍先从官方的一句介绍看起:Redux is a predictable state container for JavaScript apps. (Redux是Javascript应用程序的可预测状态容器。)当然,假如你在这之前并没有接触过相关的状态管理库或者框架, 看到这句话时是非常的懵逼的, 不过可以带着这句话来一步步探索背景随着Javascript单页面应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。 – Redux文档上面这一大段引用概况起来就是一句话, state(状态)在什么时候什么地方,因为什么而变化成了一个不受控制的过程。(这不能忍,状态如果无法预测以及控制)那么Redux就是试图让state的变化变得可预测。这些限制条件反映在 Redux 的三大原则中。核心概念1.Redux使用普通的对象来描述state,这个对象就是Modal。 2.要想更新 state 中的数据,你需要发起一个 action。Action 就是一个普通 JavaScript 对象用来描述发生了什么。 3.为了把 action 和 state 串起来,开发一些函数,这就是 reducer。reducer 只是一个接收 state 和 action,并返回新的 state 的函数。 三大准则只有一个state树。state是只读的,只能通过action改变。reducer是纯函数,没有副作用。了解到这些后,其实已经多少能明白Redux is a predictable state container for JavaScript apps. (Redux是Javascript应用程序的可预测状态容器。)这句话,为什么是可预测的? 因为只有一个state树,并且它是只读的,而且只能通过action来改变(改变的过程变得清晰可追踪),并且获取state(状态)只能通过reducer,而reducer是一个纯函数(此处了解state是重点),没有副作用,也就意味着我们能知道我们最终得到的state是什么样的。api简介[createStore(reducer, [preloadedState], [enhancer])](https://www.redux.org.cn/docs… 创建store的函数,返回一个对象, 包含getStatedispatchsubscribegetReducerreplaceReducer等方法 combineReducers(reducers) 合并多个reducer applyMiddleware(…middlewares) 中间件处理,在 实际的dispatch前调用一系列中间件, 类似于koa bindActionCreators(actionCreators, dispatch) 绑定action和dispatch compose(…functions) 函数式编程中常见的方法, compose(funcA, funcB, funcC) => compose(funcA(funcB(funcC())))React-redux介绍Redux官方提供的 React 绑定库。 具有高效且灵活的特性。动机React是以组件化的形式开发。为了组件的复用以及代码的清晰,通常我们将组件分为容器组件以及UI组件。关于容器组件和UI组件,推荐阅读该文章,而引入了React-redux可以很好的帮助我们分离容器组件和UI组件。为什么选择react-reduxreact-redux是官方提供的绑定库,由redux开发者维护,可以很好的与redux保持同步。它鼓励组件分离。react-redux协助我们分离容器组件和UI组件,通过提供API连接store(提供数据)和UI组件,并且使得UI组件不需要知道存在Redux(复用)。性能优化。虽然React速度很快,但是re-redering是非常消耗性能的,而react-redux的内部做了许多性能优化。社区支持,因为是官方指定的绑定库,所以拥有大量的使用者,社区活跃度高,问题也容易解决。api简介<Provider store> 使组件层级中的 connect() 方法都能够获得 Redux store。 store: 应用程序中唯一的 Redux store 对象 connect(mapStateToProps, mapDispatchToProps, mergeProps, options) mapStateToProps(state, [ownProps]): stateProps: 映射state作为UI组件的props mapDispatchToProps(dispatch, [ownProps]): dispatchProps: 映射dispatch作为UI组件的props mergeProps(stateProps, dispatchProps, ownProps): props: 如果指定这个函数, 即合并mapStateToPropsmapDIspatchToPropsoweProps作为UI组件的props options: 定制 connector 的行为Redux存在的问题与其说缺点,不如说是Redux的优势而造成的不可避免的劣势,问题应该辩证地看纯净。Redux只支持同步,让状态可预测,方便测试。 但不处理异步、副作用的情况,而把这个丢给了其他中间件,诸如redux-thunkredux-promiseredux-saga等等,选择多也容易造成混乱啰嗦。那么写过Redux的人,都知道actionreducer以及你的业务代码非常啰嗦,模板代码非常多。但是~,这也是为了让数据的流动清晰明了。性能。粗暴地、级联式刷新视图(使用react-redux优化)。分型。原生 Redux-react 没有分形结构,中心化 store;Redux的最佳实践vuex(dva)事实上,如果用过vuex或者dva的话, 个人觉得还是会比较偏向于这种用法。比起Redux的啰嗦,dva帮忙简化了很多步骤。具体的实现后续补充这里先补充一点,vuex不是immutable,所以对于时间旅行这种业务不太友好。Redux的实现浅析前言Redux的代码相对比较简单,容易理解, 源码的解读推荐看这篇文章, 本段主要是对代码里一些个人觉得比较有意思的点进行分析createStore在这里看出,redux即使是在内部,也是函数式编程~ 当我们传入了一个enhancer函数(即中间件),会把createStore本身当成参数传给enhancer然后返回一个新的函数来调用 即 fn => fn 暴露出的subscribe函数也是挺有意思的, 首先是isSubscribed这个变量, 其实就是一种非常基础的闭包使用, 然后是每次订阅或者取消订阅的时候,都会在dispatch之前保存一次快照, 然后当前的dispatch用的是上一份快照,而下一个dispatch则是使用当前这一份的快照 compose非常简洁的写出了函数式编程的一个常用函数(…args) => f(g(h(…args))). combineReducer可以看出,每一次action都会重新计算所有的reducer~ 但如果不是非常巨大的state树,并且拆分了很多模块,个人认为其实影响不大 bindActionCreator和applyMiddleware相对容易理解, 这里就不赘述啦 ...

February 12, 2019 · 1 min · jiezi

React-Redux进阶(像VUEX一样使用Redux)

前言Redux是一个非常实用的状态管理库,对于大多数使用React库的开发者来说,Redux都是会接触到的。在使用Redux享受其带来的便利的同时, 我们也深受其问题的困扰。redux的问题之前在另外一篇文章Redux基础中,就有提到以下这些问题纯净。Redux只支持同步,让状态可预测,方便测试。 但不处理异步、副作用的情况,而把这个丢给了其他中间件,诸如redux-thunkredux-promiseredux-saga等等,选择多也容易造成混乱啰嗦。那么写过Redux的人,都知道actionreducer以及你的业务代码非常啰嗦,模板代码非常多。但是,这也是为了让数据的流动清晰明了。性能。粗暴地、级联式刷新视图(使用react-redux优化)。分型。原生 Redux-react 没有分形结构,中心化 store里面除了性能这一块可以利用react-redux进行优化,其他的都是开发者不得不面对的问题,对于代码有洁癖的人,啰嗦这一点确实是无法忍受的。方案目标如果你使用过VUEX的话, 那么对于它的API肯定会相对喜欢很多,当然,vuex不是immutable,所以对于时间旅行这种业务不太友好。不过,我们可以自己实现一个具有vuex的简洁语法和immutable属性的redux-x(瞎命名)。 先看一下我们想要的目标是什么样的? 首先, 我们再./models里面定义每个子state树,里面带有namespace、state、reducers、effects等属性, 如下:export default { // 命名空间 namespace: ‘common’, // 初始化state state: { loading: false, }, // reducers 同步更新 类似于vuex的mutations reducers: { updateLoadingStatus(state, action) { return { …state, loading: action.payload } }, }, // reducers 异步更新 类似于vuex的actions efffects: { someEffect(action, store) { // some effect code … … // 将结果返回 return result } }}通过上面的实现,我们基本解决了Redux本身的一些瑕疵1.在effects中存放的方法用于解决不支持异步、副作用的问题 2.通过合并reducer和action, 将模板代码大大减少 3.具有分型结构(namespace),并且中心化处理如何实现暴露的接口redux-x首先,我们只是在外层封装了一层API方便使用,那么说到底,传给redux的combineReducers还是一个redux对象。另外一个则是要处理副作用的话,那就必须使用到了中间件,所以最后我们暴露出来的函数的返回值应该具有上面两个属性,如下:import reduxSimp from ‘../utils/redux-simp’ // 内部实现import common from ‘./common’ // models文件下common的状态管理import user from ‘./user’ // models文件下user的状态管理import rank from ‘./rank’ // models文件下rank的状态管理const reduxX = reduxSimp({ common, user, rank})export default reduxXconst store = createStore( combineReducers(reduxX.reducers), // reducers树 {}, applyMiddleware(reduxX.effectMiddler) // 处理副作用中间件)第一步, 我们先实现一个暴露出来的函数reduxSimp,通过他对model里面各个属性进行加工,大概的代码如下:const reductionReducer = function() { // somecode }const reductionEffects = function() { // somecode }const effectMiddler = function() { // somecode }/** * @param {Object} models /const simplifyRedux = (models) => { // 初始化一个reducers 最后传给combinReducer的值 也是最终还原的redux const reducers = {} // 遍历传入的model const modelArr = Object.keys(models) modelArr.forEach((key) => { const model = models[key] // 还原effect reductionEffects(model) // 还原reducer,同时通过namespace属性处理命名空间 const reducer = reductionReducer(model) reducers[model.namespace] = reducer }) // 返回一个reducers和一个专门处理副作用的中间件 return { reducers, effectMiddler }}还原effects对于effects, 使用的时候如下(没什么区别):props.dispatch({ type: ‘rank/fundRankingList_fetch’, payload: { fundType: props.fundType, returnType: props.returnType, pageNo: fund.pageNo, pageSize: 20 }})还原effects的思路大概就是先将每一个model下的effect收集起来,同时加上命名空间作为前缀,将副作用的key即type 和相对应的方法value分开存放在两个数组里面,然后定义一个中间件,每当有一个dispatch的时候,检查key数组中是否有符合的key,如果有,则调用对应的value数组里面的方法。// 常量 分别存放副作用的key即type 和相对应的方法const effectsKey = []const effectsMethodArr = [] /* * 还原effects的函数 * @param {Object} model /const reductionEffects = (model) => { const { namespace, effects } = model const effectsArr = Object.keys(effects || {}) effectsArr.forEach((effect) => { // 存放对应effect的type和方法 effectsKey.push(namespace + ‘/’ + effect) effectsMethodArr.push(model.effects[effect]) })}/* * 处理effect的中间件 具体参考redux中间件 * @param {Object} store /const effectMiddler = store => next => (action) => { next(action) // 如果存在对应的effect, 调用其方法 const index = effectsKey.indexOf(action.type) if (index > -1) { return effectsMethodArr[index](action, store) } return action}还原reducersreducers的应用也是和原来没有区别:props.dispatch({ type: ‘common/updateLoadingStatus’, payload: true })代码实现的思路就是最后返回一个函数,也就是我们通常写的redux函数,函数内部遍历对应命名空间的reducer,找到匹配的reducer执行后返回结果/* * 还原reducer的函数 * @param {Object} model 传入的model对象 /const reductionReducer = (model) => { const { namespace, reducers } = model const initState = model.state const reducerArr = Object.keys(reducers || {}) // 该函数即redux函数 return (state = initState, action) => { let result = state reducerArr.forEach((reducer) => { // 返回匹配的action if (action.type === ${namespace}/${reducer}) { result = model.reducers[reducer](state, action) } }) return result }}最终代码最终的代码如下,加上了一些错误判断:// 常量 分别存放副作用的key即type 和相对应的方法const effectsKey = []const effectsMethodArr = []/* * 还原reducer的函数 * @param {Object} model 传入的model对象 /const reductionReducer = (model) => { if (typeof model !== ‘object’) { throw Error(‘Model must be object!’) } const { namespace, reducers } = model if (!namespace || typeof namespace !== ‘string’) { throw Error(The namespace must be a defined and non-empty string! It is ${namespace}) } const initState = model.state const reducerArr = Object.keys(reducers || {}) reducerArr.forEach((reducer) => { if (typeof model.reducers[reducer] !== ‘function’) { throw Error(The reducer must be a function! In ${namespace}) } }) // 该函数即redux函数 return (state = initState, action) => { let result = state reducerArr.forEach((reducer) => { // 返回匹配的action if (action.type === ${namespace}/${reducer}) { result = model.reducers[reducer](state, action) } }) return result }}/* * 还原effects的函数 * @param {Object} model /const reductionEffects = (model) => { const { namespace, effects } = model const effectsArr = Object.keys(effects || {}) effectsArr.forEach((effect) => { if (typeof model.effects[effect] !== ‘function’) { throw Error(The effect must be a function! In ${namespace}) } }) effectsArr.forEach((effect) => { // 存放对应effect的type和方法 effectsKey.push(namespace + ‘/’ + effect) effectsMethodArr.push(model.effects[effect]) })}/* * 处理effect的中间件 具体参考redux中间件 * @param {Object} store /const effectMiddler = store => next => (action) => { next(action) // 如果存在对应的effect, 调用其方法 const index = effectsKey.indexOf(action.type) if (index > -1) { return effectsMethodArr[index](action, store) } return action}/* * @param {Object} models */const simplifyRedux = (models) => { if (typeof models !== ‘object’) { throw Error(‘Models must be object!’) } // 初始化一个reducers 最后传给combinReducer的值 也是最终还原的redux const reducers = {} // 遍历传入的model const modelArr = Object.keys(models) modelArr.forEach((key) => { const model = models[key] // 还原effect reductionEffects(model) // 还原reducer,同时通过namespace属性处理命名空间 const reducer = reductionReducer(model) reducers[model.namespace] = reducer }) // 返回一个reducers和一个专门处理副作用的中间件 return { reducers, effectMiddler }}export default simplifyRedux思考如何结合Immutable.js使用? ...

February 12, 2019 · 3 min · jiezi

React-redux进阶之Immutable.js

Immutable.jsImmutable的优势1. 保证不可变(每次通过Immutable.js操作的对象都会返回一个新的对象) 2. 丰富的API 3. 性能好 (通过字典树对数据结构的共享)<br/>Immutable的问题1. 与原生JS交互不友好 (通过Immutable生成的对象在操作上与原生JS不同,如访问属性,myObj.prop1.prop2.prop3 => myImmutableMap.getIn([‘prop1’, ‘prop2’, ‘prop3’])。另外其他的第三方库可能需要的是一个普通的对象) 2. Immutable的依赖性极强 (一旦在代码中引入使用,很容易传播整个代码库,并且很难在将来的版本中移除) 3. 不能使用解构和对象运算符 (相对来说,代码的可读性差) 4. 不适合经常修改的简单对象 (Immutable的性能比原生慢,如果对象简单,并且经常修改,不适合用) 5. 难以调试 (可以采用 Immutable.js Object Formatter扩展程序协助) 6. 破坏JS原生对象的引用,造成性能低下 (toJs每次都会返回一个新对象)<br/>原生Js遇到的问题原生Js遇到的问题// 场景一var obj = {a:1, b:{c:2}};func(obj);console.log(obj) //输出什么??// 场景二var obj = ={a:1};var obj2 = obj;obj2.a = 2;console.log(obj.a); // 2console.log(obj2.a); // 2代码来源:https://juejin.im/post/5948985ea0bb9f006bed7472// ajax1this.props.a = { data: 1,}// ajax2nextProps.a = { data: 1,}//shouldComponentUpdate()shallowEqual(this.props, nextProps) // false// 数据相同但是因为引用不同而造成不必要的re-rederning由于Js中的对象是引用类型的,所以很多时候我们并不知道我们的对象在哪里被操作了什么,而在Redux中,因为Reducer是一个纯函数,每次返回的都是一个新的对象(重新生成对象占用时间及内存),再加上我们使用了connect这个高阶组件,官方文档中虽然说react-redux做了一些性能优化,但终究起来,react-redux只是对传入的参数进行了一个浅比较来进行re-redering(为什么不能在mapStateToProps中使用toJs的原因)。再进一步,假如我们的state中的属性嵌套了好几层(随着业务的发展),对于原来想要的数据追踪等都变得极为困难,更为重要的是,在这种情况下,我们一些没有必要的组件很可能重复渲染了多次。 <br/>总结起来就是以下几点(问题虽少,但都是比较严重的):1. 无法追踪Js对象 2. 项目复杂时,reducer生成新对象性能低 3. 只做浅比较,有可能会造成re-redering不符合预期(多次渲染或不更新)<br/>为什么不使用深比较或许有人会疑惑,为什么不使用深比较来解决re-redering的问题,答案很简单,因为消耗非常巨大~ 想象一下,如果你的参数复杂且巨大, 对每一个进行比较是多么消耗时间的一件事~ <br/>使用Immutable解决问题项目复杂后, 追踪困难 使用Immutable之后,这个问题自然而然就解决了。所谓的追踪困难,无非就是因为对象是mutable的,我们无法确定它到底何时何处被改变,而Immutable每次都会保留原来的对象,重新生成一个对象,(与redux的纯函数概念一样)。但也要注意写代码时的习惯:// javascriptconst obj = { a: 1 }function (obj) { obj.b = 2 …}// Immutableconst obj = Map({ a : 1 })function (obj) { const obj2 = obj.set({ ‘b’, 2 })}<br/>reducer生成新对象性能差 当项目变得复杂时,每一次action对于生成的新state都会消耗一定的性能,而Immutable.js在这方面的优化就很好。或许你会疑惑为什么生成对象还能优化?请往下看~ 在前面就讲到,Immutable是通过字典树来做==结构共享==的 (图片来自网络) 这张图的意思就是immutable使用先进的tries(字典树)技术实现结构共享来解决性能问题,当我们对一个Immutable对象进行操作的时候,ImmutableJS会只clone该节点以及它的祖先节点,其他保持不变,这样可以共享相同的部分,大大提高性能。<br/>re-rendering不符合预期 其实解决这个问题是我们用Immutable的主要目的,先从浅比较说起 浅比较引起的问题在这之前已经讲过,事实上,即使Immutable之后,connect所做的依然是浅比较,但因为Immutable每次生成的对象引用都不同,哪怕是修改的是很深层的东西,最后比较的结果也是不同的,所以在这里解决了第一个问题,==re-rendering可能不会出现==。 但是, 我们还有第二个问题, ==没必要的re-rendering==,想要解决这个问题,则需要我们再封装一个高阶组件,在这之前需要了解下Immutable的 is API// is() 判断两个immutable对象是否相等immutable.is(imA, imB);这个API有什么不同, ==这个API比较的是值,而不是引用==,So: 只要两个值是一样的,那么结果就是trueconst a = Immutable.fromJS({ a: { data: 1, }, b: { newData: { data: 1 } }})const target1 = a.get(‘a’)const target2 = a.getIn([‘b’, ’newData’])console.log(Immutable.is(target1, target2)) //is比较的依据就是每个值的hashcode// 这个hashcode就相当于每个值的一个ID,不同的值肯定有不同的ID,相同的ID对应着的就是相同的值。也就是说,对于下面的这种情况, 我们可以不用渲染// ajax1this.props.a = { data: 1,}// ajax2nextProps.a = { data: 1,}//shouldComponentUpdate()Immutable.is(this.props, nextProps) // true最后, 我们需要封装一个高阶组件来帮助我们统一处理是否需要re-rendering的情况//baseComponent.js component的基类方法import React from ‘react’;import {is} from ‘immutable’;class BaseComponent extends React.Component { constructor(props, context, updater) { super(props, context, updater); } shouldComponentUpdate(nextProps, nextState) { const thisProps = this.props || {}; const thisState = this.state || {}; nextState = nextState || {}; nextProps = nextProps || {}; if (Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length) { return true; } for (const key in nextProps) { if (!is(thisProps[key], nextProps[key])) { return true; } } for (const key in nextState) { if (!is(thisState[key], nextState[key])) { return true; } } return false; }}export default BaseComponent;代码来源链接:https://juejin.im/post/5948985ea0bb9f006bed7472<br/>使用Immutable需要注意的点使用Immutable需要注意的点1. 不要混合普通的JS对象和Immutable对象 (不要把Imuutable对象作为Js对象的属性,或者反过来) 2. 对整颗Reudx的state树作为Immutable对象 3. 除了展示组件以外,其他地方都应该使用Immutable对象 (提高效率,而展示组件是纯组件,不应该使用) 4. 少用toJS方法 (一个是因为否定了Immutable,另外则是操作非常昂贵) 5. 你的Selector应该永远返回Immutable对象 (即mapStateToProps,因为react-redux中是通过浅比较来决定是否re-redering,而使用toJs的话,每次都会返回一个新对象,即引用不同)<br/>通过高阶组件,将Immutable对象转为普通对象传给展示组件1. 高阶组件返回一个新的组件,该组件接受Immutable参数,并在内部转为普通的JS对象 2. 转为普通对象后, 新组件返回一个入参为普通对象的展示组件import React from ‘react’import { Iterable } from ‘immutable’export const toJS = WrappedComponent => wrappedComponentProps => { const KEY = 0 const VALUE = 1 const propsJS = Object.entries(wrappedComponentProps).reduce( (newProps, wrappedComponentProp) => { newProps[wrappedComponentProp[KEY]] = Iterable.isIterable( wrappedComponentProp[VALUE] ) ? wrappedComponentProp[VALUE].toJS() : wrappedComponentProp[VALUE] return newProps }, {} ) return <WrappedComponent {…propsJS} />}import { connect } from ‘react-redux’import { toJS } from ‘./to-js’import DumbComponent from ‘./dumb.component’const mapStateToProps = state => { return { // obj is an Immutable object in Smart Component, but it’s converted to a plain // JavaScript object by toJS, and so passed to DumbComponent as a pure JavaScript // object. Because it’s still an Immutable.JS object here in mapStateToProps, though, // there is no issue with errant re-renderings. obj: getImmutableObjectFromStateTree(state) }}export default connect(mapStateToProps)(toJS(DumbComponent))参考<html>Immutable.js 以及在 react+redux 项目中的实践<br/>Using Immutable.JS with Redux<br/>不变应万变-Immutable优化React<br/>React-Redux分析<br/></html> ...

February 12, 2019 · 2 min · jiezi

酷狗音乐- Vue / React 全家桶的两种实现

引言两个月前用 Vue 全家桶实现过一次 酷狗音乐,最近又用 React 全家桶重构了下,最终成果和 Vue的实现基本一致,放个图:手机预览戳 Vue 版本, React 版本。demo 选择本来想用 React 全家桶重新选个项目,但是没有找到合适的,最终就重构了下,因为这个项目难度适中,非常适合练手。接近 10 个单页,内容不多不少,需要 router音乐播放作为全局组件,数据全局共享增删改,需要 redux, vuex好几个公共组件,可以封装复用项目源码在 这里,欢迎大家 star、fork项目对比我从根目录开始分析,左边 vue 右边 react根目录src 目录这里有几个区别:React 版本并没有 router 文件,因为它支持 path 和 component 属性,来定位要渲染的组件,就像这样:而 Vue router 似乎并没有提供 path 和 component API ,所以必须要到 Router 配置里去读取 path 和 component 属性。React 也没有 mixins, 因为用 HOC 取代了 mixins。以我放在 components/HOC/index.js 里的代码为例:而且,你也可以在里面加上生命周期钩子等等,实际上,React 之前也是采用 mixins 实现的,不过后来改了。一个 .vue 组件对应 React 中三个文件?在很多情况下,是这样子。Vue 的行为结构表现分离,很明显,而 React 的分离虽然不是很明显,但实际上也是有的。以 App.vue 为例App.vue 里的 style 对应 React 里的 App.less ,毫无疑问App.vue 里的 template 和 props 对应 React 里的 App.js ,React 称为 Presentational Components,一般只有一个 render 方法 return html, 譬如:App.vue 里剩余的部分,包括 ajax, mapState, 状态的变更,以及生命周期钩子等等,都是对应 React 里的 AppContainer.js ,React 称为 Container Components. 如图:实际上, AppContainer.js 负责行为逻辑,而 App.js 负责结构展示, App.less 负责样式表现,依旧是 行为/结构/表现 的分离。只不过与 Vue 稍有不同而已。这一点上,React 多费些脑力和胶水代码。Vuex 和 redux 目录这里跟我的实现有关系,redux 可能是比 Vuex 麻烦些,但不至于图示如此夸张。因为我重构的时候改了逻辑。selectorsselectors 和 Vue 中的 getters 有相似,但底层原理不同。举个例子,我们如果要从一个巨量的 array 里找到某个数据,比较耗性能怎么办?很明显可以对参数做个缓存,如果查询 id 和上一次一样,就返回上次的结果,不查询了。selectors 做的就是这个事。actionsReact 的 actions 和 Vuex 中的 actions 类似,都是发送指令,但不操作数据。reducersactions 发送指令,最终会到 reducers 里合并数据,与 Vue 中的 mutations 类似。如果你注意的话,就会发现,reducers 里合并数据总是返回一个新对象。而 Vuex 中,我们是直接修改 state 的数据的。这里其实牵涉到了 Vue 和 React 中的一个大不同。总结总体的目录和架构是类似的,不过具体用起来差别还不小。技术栈的广度Vue 全家桶只要加上 Vuex 和 Vue-router 就可以了,而 React 在读完 redux, react-redux, react-router 文档之后,会发现他们还拆分、引出了不少东西,譬如 reselect, redux-thunk 等等,并且 redux, reselect还不是局限于 React 的。API实践过程中,发现 Vue 中的一些类似的 API 在 React 中被进行了重构,比如 React 用 createRef 取代了 ref=“string”,用 HOC 取代了 mixins 等等,虽然有些不习惯,但是感觉还好。求职本人最近正在找工作,有兴趣的欢迎私信哦,坐标上海,半年经验,比较了解 Vue+es6,了解一点 React,具体简历 戳这里 ...

January 25, 2019 · 1 min · jiezi

使用redux,react在纯函数中触发react-router-dom页面跳转

文章有错误和不合理的地方欢迎小伙伴轻拍看到标题,很多react选手可能就会笑了,这还是问题吗?在函数中触发页面跳转不就的用redux吗!或者redux类似的控件,mbox,dva,rxjs等等,或者自己写个订阅功能的控件,可能就是因为太简单了,网上的解决这个问题的文章才那么少。当我试着用react搭建前端框架,这个问题确实困扰了我好久,当接触到redux就是这redux是正解了。痛点这就是困扰我的问题对fetch()进行封装,并对请求的返回数据做拦截,当捕捉到错误的时候,判断错误的状态码,404时让页面跳转到404页面,当时401时跳转到登录页面,500调整到500页面。 react-router ^4并没有暴露出来history对象,这让非组件内页面跳转变的困难。问题的解决定义storefunction navTo(state = “/”, action) { switch (action.type) { case ‘NAV_TO’: return action.path; default: return state }}let store = createStore(combineReducers({navTo}));export default store;fetch()状态拦截代码import store from “../../redux/store”;fetch(builUrl(url, params), requestInit) .then(data => { return data.json() }).catch(e => { const status = e.name; if (status === 401) { store.dispatch({type: ‘NAV_TO’, path: ‘/login’}); return; } if (status === 403) { store.dispatch({type: ‘NAV_TO’, path: ‘/exception/403’}); return; } if (status <= 504 && status >= 500) { store.dispatch({type: ‘NAV_TO’, path: ‘/exception/500’}); return; } if (status >= 404 && status < 422) { store.dispatch({type: ‘NAV_TO’, path: ‘/exception/404’}); return; } })app.js实现对store的订阅,并跳转页面import React, {Component} from ‘react’;import store from ‘./app/common/redux/store.js’import {withRouter} from “react-router-dom”;@withRouterclass App extends Component { constructor(props) { super(props); store.subscribe(() => { this.props.history.push(store.getState().navTo); }); } render() { return ( <div> {this.props.children} </div> ); }}export default App;当fetch()拦截到错误就可以进行页面调整了如果对redux有疑问,可以看我另一篇文章https://segmentfault.com/a/11… 这就是在函数中通过订阅的方式来实现页面跳转,easy easy !!!小伙伴可以举一反三运用到更多的地方去!!????????????????????如果能帮助到小伙伴的话欢迎点个赞????????????????????????????????????????如果能帮助到小伙伴的话欢迎点个赞???????????????????? ...

January 24, 2019 · 1 min · jiezi

redux入门到深度理解

redux 的两个核心概念一个应用所有的状态,保存在一个对象里面。redux是一种订阅模式,有可订阅对象,订阅者,和消息发布我开始看redux文档时,文档中着重描述的是状态管理,(一个应用的状态是由一个对象来控制的),对订阅模式提的很少,当了解的差不多的时候才发现当把redux当作订阅模式来理解就好理解多了,redux是通过订阅模式来管理状态的。store是个可观察对象,存储了所有订阅者、state和reducer,dispatch(ation)发布消息,action是怎么来触发相应的reducer来修改state,这充分利用函数的副作用。redux store的组成部分

January 24, 2019 · 1 min · jiezi

Redux随笔

Redux简介Redux是一个库,是JavaScript状态容器,提供可预测化的状态管理,他解决的组件间数据共享的问题。当然,在React中,要达到组件间数据共享的目的不一定非要用Redux,React提供了context api,在小型项目里面用context当然没什么问题,但是当页面复杂起来的时候,可能就需要统一并且易管理的Redux库了。Redux三大原则单一数据源所有数据都存在唯一一棵树中状态是只可读简单的说,就是只有get方法,没有set方法,状态只能通过dispatch(action)更改状态修改均由纯函数完成 1. 函数的返回结果只依赖于它的参数。 2. 函数执行过程里面没有副作用。 Redux核心Redux学的时候可能觉得有点绕,但是学会了回头看,其实就三个东西action(对要分发(dispatch)的数据进行包装)reducers(识别发送的action的类型(type),返回一个新的状态(state)给store树更新)store(store 就是用来维持应用所有的 state 树 的一个对象。)这是来自阮老师官网的图。它很好的解释了redux的工作原理,举例,当用户点击时(onClick),分发一个action (dispatch(action)),然后reducers就会被调用,reducers会被传入两个参数,一个当前的(旧的)state,一个是你dispatch的action,然后reducers根据action.type进行相应的数据处理,最后返回一个新的state给store树,store树更新后,React Components就会根据store进行重新渲染,就达到了状态更新的效果了。React中的目录 ├── src/ ├── action// 存放action ├── components // 存放展示组件(组件的渲染,它不依赖store,即与redux无关) ├── contrainers// 存放容器组件 (在这里作了数据更新的定义,与redux相关) ├── reducers // 存放reducers …Actionaction是对要分发的数据进行包装的,当我们要改变store树时,只需要分发对应的action dispatch(add()),然后经过reducers处理返回一个新的state,store树就得到了更新。以计数器为例,就应该有两个action,一个加一,一个减一。return的内容可以不只有type,还可以有自己自定义的数据(参照如下),但是必定要有type,因为reducers是根据action.type进行识别处理数据的。// src/action/index.jsexport const add = () => { return { type: “ADD” };};export const less = () => { return { type: “LESS” };};// let id = 0;// export const getWeatherSuccess = (payload) => {// return {// type:“SUCCESS”,// payload,// id:id++// };// };ReducersReducers的写法基本都是固定的,形式如下,state初始值根据需求自定,唯一要注意的时,我们不可以更改state,只能返回一个新的state,因为Reducers是一个纯函数。为什么Reducers一定要是纯函数,其实Redux只通过比较新旧两个对象的存储位置来比较新旧两个对象是否相同(也就是Javascript对象浅比较)。如果在reducer内部直接修改旧的state对象的属性值,那么新的state和旧的state将都指向同一个对象。因此Redux认为没有任何改变,返回的state将为旧的state。Reducers这么设计的目的其实是出于性能的考虑,如果Reducers设计成直接修改原state,则每次修改都要进行遍历对比,即JavaScript深比较,对性能消耗大很多,所以设计成纯函数的形式,每次仅需要一次浅对比即可。好吧,上面巴拉巴拉那么多,其实暂时不看也不影响,下面继续举Count的reducerrs例子// src/reducers/countconst count = (state = 0,action) => { switch (action.type) { case ‘ADD’: return state+1 case ‘LESS’: return state-1 default: return state }}export default count;这里只是一个Count的Reducer,在实际情况中,肯定不止一个Reducer,并且也不可能都写在一个js文件中,那么就需要用到combineReducers进行Reducer的合并。// src/reducers/indeximport {combineReducers} from ‘redux’import count from ‘./count’//假设有个weather的reducerimport weather from ‘./weather’const reducers = combineReducers({ count, weather})export default reducers;StoreStore提供了三个API:提供 getState() 方法获取 state;提供 dispatch(action) 方法更新 state;通过 subscribe(listener) 注册监听器,注销监听器;store一般都是在顶层组件创建,然后通过react-redux提供的Provider对子组件进行连接// src/components/Appimport React, { Component } from ‘react’;import {Provider} from ‘react-redux’import { createStore } from ‘redux’;import reducers from ‘../reducers’import Count from ‘../contrainers/Count’let store = createStore(reducers)class App extends Component { render() { return ( <Provider store={store}> <CountAndWeather/> </Provider> ); }}export default App;react-reduxreact-redux是一个官方提供的redux连接库,它提供了一个组件<Provider></Provider>和一个API connect(),<Provider>接受一个store作为props,而connect提供了在整个React应用的任意组件中获取store中数据的功能。即在<Provider>包裹的子组件里面,我们可以通过connect()获取store。connect是一个高阶函数,第一个括号的两个参数,第一个是对state的映射,第二个是dispatch的映射,第二个括号的参数是需要连接Redux的组件。????一个容器组件,负责获取store的state,以及dispatch对应的处理,最后export一个connect,该connect连接的是Count这个展示组件。// src/contrainers/CountCotrimport { connect } from ‘react-redux’import Count from ‘../components/Count’import { add, less } from ‘../action’const mapStateToProps = state => { return { count:state.count }}const mapDispatchToProps = dispatch => { return { onClickAdd: () => { dispatch(add()) }, onClickLess: () => { dispatch(less()) } } }//mapStateToProps,mapDispatchToProps注意先后顺序export default connect(mapStateToProps,mapDispatchToProps)(Count);????这是一个展示组件,它经过上面容器组件的connect之后,可以在this.props获取到mapStateToProps和mapDispatchToProps的返回值。// src/components/Countimport React, { Component } from ‘react’class Count extends Component { render() { const {count, onClickAdd, onClickLess } = this.props return ( <div> Count : {count} <br/><br/> <button onClick={onClickAdd}>增加</button> <br/><br/> <button onClick={onClickLess}>减少</button> </div> ); }}export default Count;至此,一个基于redux的同步计数器就完成了。但是这仅仅只是一个同步redux应用,我们还有异步请求需要处理,于是乎就需要用到middleware。Middlewaremiddleware顾名思义,是一个中间件。它提供的是位于action被发起之后,到达 reducer之前的扩展点。你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。redux的异步方案有很多,redux-thunk、redux-promise、redux-saga等等,甚至也可以自己写一个。这里以redux-thunk为例,顺便加入redux-logger日记中间件方便查看。使用方法:npm install,引入,在createStore里面添加applyMiddleware(thunk,logger)即可。注意!因为middleware是类似串串一样处在action和reducer之间,所以是有顺序的,而一些middleware对顺序有要求,如redux-looger则要求放在最后。// src/components/App…import { createStore ,applyMiddleware} from ‘redux’;import logger from ‘redux-logger’import thunk from ‘redux-thunk’let store = createStore(reducers,applyMiddleware(thunk,logger));//这么写也可以// let store = applyMiddleware(thunk,logger)(createStore)(reducers)…然后,就可以在action里面写异步方法了。我们的异步action是一个高阶函数,第一个参数自定义,return的函数可以接受两个参数,dispatch和getState。这里以一个fetch获取天气的请求为例。// src/action/indexconst getWeatherSuccess = (payload) => { return { type:“SUCCESS”, payload }}const getWeatherError = () => { return { type:“ERROR” }}export const getWeather = () => { return async (dispatch, getState) => { try { //这个console只是为了验证getState方法 console.log(getState()) const response = await fetch( “http://www.weather.com.cn/data/sk/101280101.html" ); const data = await response.json(); dispatch(getWeatherSuccess(data.weatherinfo)); } catch () { dispatch(getWeatherError()); } };};最后,再写个weahter的reducer。// src/reducers/weatherconst weather = (state = {},action) =>{ switch (action.type) { case ‘SUCCESS’: return {state:“success”,weatherInfo:action.payload} case ‘ERROR’: return {state:“error”} default: return state }}export default weather;合并一下reducer// src/reducers/indeximport {combineReducers} from ‘redux’import count from ‘./count’import weather from ‘./weather’const reducers = combineReducers({ //这里使用了es6语法,实际上是count:count,对应state的key count, weather})export default reducers;将展示组件里添加一个按钮和显示,容器组件里添加个获取weahter的state和dispatch请求。// src/contrainers/CouAndWeaContrainerimport {connect} from ‘react-redux’import CountAndWeahter from ‘../components/CountAndWeahter’import {add,less,getWeather} from ‘../action’const mapStateToProps = state => { return { count:state.count, weather:state.weather.weatherInfo, state:state.weather.state }}const mapDispatchToProps = dispatch => { return { onClickAdd: () => { dispatch(add()) }, onClickLess: () => { dispatch(less()) }, onClickFetch:() => { dispatch(getWeather()) } } }export default connect(mapStateToProps,mapDispatchToProps)(CountAndWeahter);// src/components/CountAndWeahterimport React, { Component } from ‘react’class CountAndWeahter extends Component { render() { const {count, onClickAdd, onClickLess,weather,state,onClickFetch} = this.props return ( <div> Count : {count} <br/><br/> <button onClick={onClickAdd}>增加</button> <br/><br/> <button onClick={onClickLess}>减少</button> <br/><br/> {state === “success”?<p>城市:{weather.city},风向:{weather.WD}</p>:’’} <button onClick={onClickFetch}>获取天气</button> </div> ); }}export default CountAndWeahter;????这是打印出来的state,其中count和weather这两个名字是由combineReducers传入时的决定的。{ count: 0, weather: state: “success” weatherInfo: AP: “1001.4hPa” Radar: “JC_RADAR_AZ9200_JB” SD: “83%” WD: “东南风” WS: “小于3级” WSE: “<3” city: “广州” cityid: “101280101” isRadar: “1” njd: “暂无实况” sm: “1.7” temp: “26.6” time: “17:50”}现在,一个既有同步又有异步action的react-redux应用就完成了。自定义Middleware有时候,我们可能有特殊的需求,却找不到合适的middleware,这时候,就需要自定义自己的middleware了。以一个简化版的logger为例,写法如下????//将import logger from ‘redux-logger’备注掉const logger = store =>next => action =>{ console.log(‘prevState:’,store.getState()) console.log(‘action:’+action.type) next(action) console.log(’nextState:’,store.getState())}这么几行,就已经完成了一个logger的middleware了,其中,store =>next => action =>{}这是一个柯里化的写法,至于为什么要这么写,就需要看redux的applyMiddleware源码来解释了。export default function applyMiddleware(…middlewares) { return createStore => (…args) => { const store = createStore(…args) let dispatch = () => { throw new Error( Dispatching while constructing your middleware is not allowed. + Other middleware would not be applied to this dispatch. ) } const middlewareAPI = { getState: store.getState, dispatch: (…args) => dispatch(…args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(…chain)(store.dispatch) return { …store, dispatch } }}applyMiddleware亦是一个三级柯里化(Currying)的函数,第一个参数是middleware数组,第二个是redux的createStore,第三个…args,其实就是reducers。applyMiddleware首先利用createStore(…args)创建了一个store,然后再将store.getState和store.dispatch传给每个middleware,即对应logger的第一个参数store,最后dispatch = compose(…chain)(store.dispatch)将所有middleware串起来。????compose负责串联middleware,假设传入dispatch = compose(f1,f2,f3)(store.dispatch),则相当于f1(f2(f3(store.dispatch)))export default function compose(…funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (…args) => a(b(…args)))}applyMiddleware相当于重新包装了一遍dispatch,这样,当我们dispatch就会经过层层middleware处理,达到中间件的效果了。案例源码https://github.com/Y-qwq/coun…小结这算是我redux入门学习以来的第一次总结吧,如有不对,敬请指正。 ...

January 22, 2019 · 3 min · jiezi

react全家桶从0到1(react-router4、redux、redux-saga)

react全家桶从0到1(最新)本文从零开始,逐步讲解如何用react全家桶搭建一个完整的react项目。文中针对react、webpack、babel、react-route、redux、redux-saga的核心配置会加以讲解,希望通过这个项目,可以系统的了解react技术栈的主要知识,避免搭建一次后面就忘记的情况。代码库:https://github.com/teapot-py/react-demo首先关于主要的npm包版本列一下:react@16.7.0webpack@4.28.4babel@7+react-router@4.3.1redux@4+从webpack开始思考一下webpack到底做了什么事情?其实简单来说,就是从入口文件开始,不断寻找依赖,同时为了解析各种不同的文件加载相应的loader,最后生成我们希望的类型的目标文件。这个过程就像是在一个迷宫里寻宝,我们从入口进入,同时我们也会不断的接收到下一处宝藏的提示信息,我们对信息进行解码,而解码的时候可能需要一些工具,比如说钥匙,而loader就像是这样的钥匙,然后得到我们可以识别的内容。回到我们的项目,首先进行项目的初始化,分别执行如下命令mkdir react-demo // 新建项目文件夹cd react-demo // cd到项目目录下npm init // npm初始化引入webpacknpm i webpack –savetouch webpack.config.js对webpack进行简单配置,更新webpack.config.jsconst path = require(‘path’);module.exports = { entry: ‘./app.js’, // 入口文件 output: { path: path.resolve(__dirname, ‘dist’), // 定义输出目录 filename: ‘my-first-webpack.bundle.js’ // 定义输出文件名称 }};更新package.json文件,在scripts中添加webpack执行命令"scripts": { “dev”: “./node_modules/.bin/webpack –config webpack.config.js”}如果有报错请按提示安装webpack-clinpm i webpack-cli执行webpacknpm run dev如果在项目文件夹下生成了dist文件,说明我们的配置是没有问题的。接入react安装react相关包npm install react react-dom –save更新app.js入口文件import React from ‘reactimport ReactDom from ‘react-dom’;import App from ‘./src/views/App’;ReactDom.render(<App />, document.getElementById(‘root’));创建目录 src/views/App,在App目录下,新建index.js文件作为App组件,index.js文件内容如下:import React from ‘react’;class App extends React.Component { constructor(props) { super(props); } render() { return (<div>App Container</div>); }}export default App;在根目录下创建模板文件index.html<!DOCTYPE html><html><head> <title>index</title> <meta charset=“utf-8”> <meta name=“viewport” content=“width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no”></head><body> <div id=“root”></div></body></html>到了这一步其实关于react的引入就OK了,不过目前还有很多问题没有解决如何解析JS文件的代码?如何将js文件加入模板文件中?Babel解析js文件Babel是一个工具链,主要用于在旧的浏览器或环境中将ECMAScript2015+的代码转换为向后兼容版本的JavaScript代码。安装babel-loader,@babel/core,@babel/preset-env,@babel/preset-reactnpm i babel-loader@8 @babel/core @babel/preset-env @babel/preset-react -Dbabel-loader:使用Babel转换JavaScript依赖关系的Webpack加载器, 简单来讲就是webpack和babel中间层,允许webpack在遇到js文件时用bable来解析@babel/core:即babel-core,将ES6代码转换为ES5。7.0之后,包名升级为@babel/core。@babel相当于一种官方标记,和以前大家随便起名形成区别。@babel/preset-env:即babel-preset-env,根据您要支持的浏览器,决定使用哪些transformations / plugins 和 polyfills,例如为旧浏览器提供现代浏览器的新特性。@babel/preset-react:即 babel-preset-react,针对所有React插件的Babel预设,例如将JSX转换为函数.更新webpack.config.js module: { rules: [ { test: /.js$/, // 匹配.js文件 exclude: /node_modules/, use: { loader: ‘babel-loader’ } } ] }根目录下创建并配置.babelrc文件{ “presets”: ["@babel/preset-env", “@babel/preset-react”]}配置HtmlWebPackPlugin这个插件最主要的作用是将js代码通过<script>标签注入到 HTML 文件中npm i html-webpack-plugin -Dwebpack新增HtmlWebPackPlugin配置至此,我们看一下webpack.config.js文件的完整结构const path = require(‘path’);const HtmlWebPackPlugin = require(‘html-webpack-plugin’);module.exports = { entry: ‘./app.js’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘my-first-webpack.bundle.js’ }, mode: ‘development’, module: { rules: [ { test: /.js$/, exclude: /node_modules/, use: { loader: ‘babel-loader’ } } ] }, plugins: [ new HtmlWebPackPlugin({ template: ‘./index.html’, filename: path.resolve(__dirname, ‘dist/index.html’) }) ]};执行 npm run start,生成 dist文件夹当前目录结构如下可以看到在dist文件加下生成了index.html文件,我们在浏览器中打开文件即可看到App组件内容。配置 webpack-dev-serverwebpack-dev-server可以极大的提高我们的开发效率,通过监听文件变化,自动更新页面安装 webpack-dev-server 作为 dev 依赖项npm i webpack-dev-server -D更新package.json的启动脚本“dev": “webpack-dev-server –config webpack.config.js –open"webpack.config.js新增devServer配置devServer: { hot: true, // 热替换 contentBase: path.join(__dirname, ‘dist’), // server文件的根目录 compress: true, // 开启gzip port: 8080, // 端口},plugins: [ new webpack.HotModuleReplacementPlugin(), // HMR允许在运行时更新各种模块,而无需进行完全刷新 new HtmlWebPackPlugin({ template: ‘./index.html’, filename: path.resolve(__dirname, ‘dist/index.html’) })]引入reduxredux是用于前端数据管理的包,避免因项目过大前端数据无法管理的问题,同时通过单项数据流管理前端的数据状态。创建多个目录新建src/actions目录,用于创建action函数新建src/reducers目录,用于创建reducers新建src/store目录,用于创建store下面我们来通过redux实现一个计数器的功能安装依赖npm i redux react-redux -D在actions文件夹下创建index.js文件export const increment = () => { return { type: ‘INCREMENT’, };};在reducers文件夹下创建index.js文件const initialState = { number: 0};const incrementReducer = (state = initialState, action) => { switch(action.type) { case ‘INCREMENT’: { state.number += 1 return { …state } break }; default: return state; }};export default incrementReducer;更新store.jsimport { createStore } from ‘redux’;import incrementReducer from ‘./reducers/index’;const store = createStore(incrementReducer);export default store;更新入口文件app.jsimport App from ‘./src/views/App’;import ReactDom from ‘react-dom’;import React from ‘react’;import store from ‘./src/store’;import { Provider } from ‘react-redux’;ReactDom.render( <Provider store={store}> <App /> </Provider>, document.getElementById(‘root’));更新App组件import React from ‘react’;import { connect } from ‘react-redux’;import { increment } from ‘../../actions/index’;class App extends React.Component { constructor(props) { super(props); } onClick() { this.props.dispatch(increment()) } render() { return ( <div> <div>current number: {this.props.number} <button onClick={()=>this.onClick()}>点击+1</button></div> </div> ); }}export default connect( state => ({ number: state.number }))(App);点击旁边的数字会不断地+1引入redux-sagaredux-saga通过监听action来执行有副作用的task,以保持action的简洁性。引入了sagas的机制和generator的特性,让redux-saga非常方便地处理复杂异步问题。redux-saga的原理其实说起来也很简单,通过劫持异步action,在redux-saga中进行异步操作,异步结束后将结果传给另外的action。下面就接着我们计数器的例子,来实现一个异步的+1操作。安装依赖包npm i redux-saga -D新建src/sagas/index.js文件import { delay } from ‘redux-saga’import { put, takeEvery } from ‘redux-saga/effects’export function* incrementAsync() { yield delay(2000) yield put({ type: ‘INCREMENT’ })}export function* watchIncrementAsync() { yield takeEvery(‘INCREMENT_ASYNC’, incrementAsync)}解释下所做的事情,将watchIncrementAsync理解为一个saga,在这个saga中监听了名为INCREMENT_ASYNC的action,当INCREMENT_ASYNC被dispatch时,会调用incrementAsync方法,在该方法中做了异步操作,然后将结果传给名为INCREMENT的action进而更新store。更新store.js在store中加入redux-saga中间件import { createStore, applyMiddleware } from ‘redux’;import incrementReducer from ‘./reducers/index’;import createSagaMiddleware from ‘redux-saga’import { watchIncrementAsync } from ‘./sagas/index’const sagaMiddleware = createSagaMiddleware()const store = createStore(incrementReducer, applyMiddleware(sagaMiddleware));sagaMiddleware.run(watchIncrementAsync)export default store;更新App组件在页面中新增异步提交按钮,观察异步结果import React from ‘react’;import { connect } from ‘react-redux’;import { increment } from ‘../../actions/index’;class App extends React.Component { constructor(props) { super(props); } onClick() { this.props.dispatch(increment()) } onClick2() { this.props.dispatch({ type: ‘INCREMENT_ASYNC’ }) } render() { return ( <div> <div>current number: {this.props.number} <button onClick={()=>this.onClick()}>点击+1</button></div> <div>current number: {this.props.number} <button onClick={()=>this.onClick2()}>点击2秒后+1</button></div> </div> ); }}export default connect( state => ({ number: state.number }))(App);观察结果我们会发现如下报错:这是因为在redux-saga中用到了Generator函数,以我们目前的babel配置来说并不支持解析generator,需要安装@babel/plugin-transform-runtimenpm install –save-dev @babel/plugin-transform-runtime这里关于babel-polyfill、和transfor-runtime做进一步解释babel-polyfillBabel默认只转换新的JavaScript语法,而不转换新的API。例如,Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转译。如果想使用这些新的对象和方法,必须使用 babel-polyfill,为当前环境提供一个垫片。babel-runtimeBabel转译后的代码要实现源代码同样的功能需要借助一些帮助函数,而这些帮助函数可能会重复出现在一些模块里,导致编译后的代码体积变大。Babel 为了解决这个问题,提供了单独的包babel-runtime供编译模块复用工具函数。在没有使用babel-runtime之前,库和工具包一般不会直接引入 polyfill。否则像Promise这样的全局对象会污染全局命名空间,这就要求库的使用者自己提供 polyfill。这些 polyfill一般在库和工具的使用说明中会提到,比如很多库都会有要求提供 es5的polyfill。在使用babel-runtime后,库和工具只要在 package.json中增加依赖babel-runtime,交给babel-runtime去引入 polyfill 就行了;详细解释可以参考babel presets 和 plugins的区别Babel插件一般尽可能拆成小的力度,开发者可以按需引进。比如对ES6转ES5的功能,Babel官方拆成了20+个插件。这样的好处显而易见,既提高了性能,也提高了扩展性。比如开发者想要体验ES6的箭头函数特性,那他只需要引入transform-es2015-arrow-functions插件就可以,而不是加载ES6全家桶。但很多时候,逐个插件引入的效率比较低下。比如在项目开发中,开发者想要将所有ES6的代码转成ES5,插件逐个引入的方式令人抓狂,不单费力,而且容易出错。这个时候,可以采用Babel Preset。可以简单的把Babel Preset视为Babel Plugin的集合。比如babel-preset-es2015就包含了所有跟ES6转换有关的插件。更新.babelrc文件配置,支持genrator{ “presets”: ["@babel/preset-env”, “@babel/preset-react”], “plugins”: [ [ “@babel/plugin-transform-runtime”, { “corejs”: false, “helpers”: true, “regenerator”: true, “useESModules”: false } ] ]}点击按钮会在2秒后执行+1操作。引入react-router在web应用开发中,路由系统是不可或缺的一部分。在浏览器当前的URL发生变化时,路由系统会做出一些响应,用来保证用户界面与URL的同步。随着单页应用时代的到来,为之服务的前端路由系统也相继出现了。而react-route则是与react相匹配的前端路由。引入react-router-domnpm install –save react-router-dom -D更新app.js入口文件增加路由匹配规则import App from ‘./src/views/App’;import ReactDom from ‘react-dom’;import React from ‘react’;import store from ‘./src/store’;import { Provider } from ‘react-redux’;import { BrowserRouter as Router, Route, Switch } from “react-router-dom”;const About = () => <h2>页面一</h2>;const Users = () => <h2>页面二</h2>;ReactDom.render( <Provider store={store}> <Router> <Switch> <Route path="/" exact component={App} /> <Route path="/about/" component={About} /> <Route path="/users/" component={Users} /> </Switch> </Router> </Provider>, document.getElementById(‘root’));更新App组件,展示路由效果import React from ‘react’;import { connect } from ‘react-redux’;import { increment } from ‘../../actions/index’;import { Link } from “react-router-dom”;class App extends React.Component { constructor(props) { super(props); } onClick() { this.props.dispatch(increment()) } onClick2() { this.props.dispatch({ type: ‘INCREMENT_ASYNC’ }) } render() { return ( <div> <div>react-router 测试</div> <nav> <ul> <li> <Link to="/about/">页面一</Link> </li> <li> <Link to="/users/">页面二</Link> </li> </ul> </nav> <br/> <div>redux & redux-saga测试</div> <div>current number: {this.props.number} <button onClick={()=>this.onClick()}>点击+1</button></div> <div>current number: {this.props.number} <button onClick={()=>this.onClick2()}>点击2秒后+1</button></div> </div> ); }}export default connect( state => ({ number: state.number }))(App);点击列表可以跳转相关路由总结至此,我们已经一步步的,完成了一个简单但是功能齐全的react项目的搭建,下面回顾一下我们做的工作引入webpack引入react引入babel解析react接入webpack-dev-server提高前端开发效率引入redux实现一个increment功能引入redux-saga实现异步处理引入react-router实现前端路由麻雀虽小,五脏俱全,希望通过最简单的代码快速的理解react工具链。其实这个小项目中还是很多不完善的地方,比如说样式的解析、Eslint检查、生产环境配置,虽然这几项是一个完整项目不可缺少的部分,但是就demo项目来说,对我们理解react工具链可能会有些干扰,所以就不在项目中加了。后面我会新建一个分支,把这些完整的功能都加上,同时也会对当前的目录结构进行优化。代码库:https://github.com/teapot-py/react-demo ...

January 18, 2019 · 4 min · jiezi

2亿用户背后的Flutter应用框架Fish Redux

背景在闲鱼深度使用 Flutter 开发过程中,我们遇到了业务代码耦合严重,代码可维护性糟糕,如入泥泞。对于闲鱼这样的负责业务场景,我们需要一个统一的应用框架来摆脱当下的开发困境,而这也是 Flutter 领域空缺的一块处女地。Fish Redux 是为解决上面问题上层应用框架,它是一个基于 Redux 数据管理的组装式 flutter 应用框架, 特别适用于构建中大型的复杂应用。它的最大特点是配置式组装, 一方面将一个大的页面,对视图和数据层层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现,另一方面将 Component|Adapter 拆分为 View,Reducer,Effect 等相互独立的上下文无关函数。所以它会非常干净,易编写、易维护、易协作。Fish Redux 的灵感主要来自于 Redux、React、Elm、Dva 这样的优秀框架,而 Fish Redux 站在巨人的肩膀上,将集中,分治,复用,隔离做的更进一步。分层架构图架构图,主体自底而上,分三层,每一层用来解决不通层面的问题和矛盾,下面依次来展开。ReduxRedux 是来自前端社区的一个数据管理框架, 对 Native 开发同学来说可能会有一点陌生,我们做一个简单的介绍。Redux 做什么的?Redux 是一个用来做可预测易调试的数据管理的框架。所有对数据的增删改查等操作都由 Redux 来集中负责。Redux 是怎么设计和实现的?Redux 是一个函数式的数据管理的框架。传统 OOP 做数据管理,往往是定义一些 Bean,每一个 Bean 对外暴露一些 Public-API 用来操作内部数据(充血模型)。函数式的做法是更上一个抽象的纬度,对数据的定义是一些 Struct(贫血模型),而操作数据的方法都统一到具有相同函数签名 (T, Action) => T 的 Reducer 中。FP:Struct(贫血模型) + Reducer = OOP:Bean(充血模型)同时 Redux 加上了 FP 中常用的 Middleware(AOP) 模式和 Subscribe 机制,给框架带了极高的灵活性和扩展性。贫血模型、充血模型 参考:https://en.wikipedia.org/wiki/Plain_old_Java_objectRedux 的缺点Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点。在我们实际使用 Redux 中面临两个具体问题Redux 的集中和 Component 的分治之间的矛盾。Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性。Fish Redux 的改良Fish Redux 通过 Redux 做集中化的可观察的数据管理。然不仅于此,对于传统 Redux 在使用层面上的缺点,在面向端侧 flutter 页面纬度开发的场景中,我们通过更好更高的抽象,做了改良。一个组件需要定义一个数据(Struct)和一个 Reducer。同时组件之间存在着父依赖子的关系。通过这层依赖关系,我们解决了【集中】和【分治】之间的矛盾,同时对 Reducer 的手动层层 Combine 变成由框架自动完成,大大简化了使用 Redux 的困难。我们得到了理想的集中的效果和分治的代码。对社区标准的 followState、Action、Reducer、Store、Middleware 以上概念和社区的 ReduxJS 是完全一致的。我们将原汁原味地保留所有的 Redux 的优势。如果想对 Redux 有更近一步的理解,请参考 https://github.com/reduxjs/reduxComponent组件是对局部的展示和功能的封装。 基于 Redux 的原则,我们对功能细分为修改数据的功能(Reducer)和非修改数据的功能(副作用 Effect)。于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的行为。这是一种面向当下,也面向未来的拆分。在面向当下的 Redux 看来,是数据管理和其他。在面向未来的 UI-Automation 看来是 UI 表达和其他。UI 的表达对程序员而言即将进入黑盒时代,研发工程师们会把更多的精力放在非修改数据的行为、修改数据的行为上。组件是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。关于 ViewView 仅仅是一个函数签名: (T,Dispatch,ViewService) => Widget它主要包含三方面的信息视图是完全由数据驱动。视图产生的事件/回调,通过 Dispatch 发出“意图”,不做具体的实现。需要用到的组件依赖等,通过 ViewService 标准化调用。比如一个典型的符合 View 签名的函数关于 EffectEffect 是对非修改数据行为的标准定义,它是一个函数签名: (Context, Action) => Object它主要包含四方面的信息接收来自 View 的“意图”,也包括对应的生命周期的回调,然后做出具体的执行。它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们不崇尚持有数据,而通过上下文来获取最新数据。它不修改数据, 如果修要,应该发一个 Action 到 Reducer 里去处理。它的返回值仅限于 bool or Future, 对应支持同步函数和协程的处理流程。比如:良好的协程的支持关于 ReducerReducer 是一个完全符合 Redux 规范的函数签名:(T,Action) => T一些符合签名的 Reducer同时我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。所以有这样的公式 Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)。一个典型的组装通过 Component 的抽象,我们得到了完整的分治,多纬度的复用,更好的解耦。AdapterAdapter 也是对局部的展示和功能的封装。它为 ListView 高性能场景而生,它是 Component 实现上的一种变化。它的目标是解决 Component 模型在 flutter-ListView 的场景下的 3 个问题1)将一个"Big-Cell"放在 Component 里,无法享受 ListView 代码的性能优化。2)Component 无法区分 appear|disappear 和 init|dispose 。3)Effect 的生命周期和 View 的耦合,在 ListView 的场景下不符合直观的预期。概括的讲,我们想要一个逻辑上的 ScrollView,性能上的 ListView ,这样的一种局部展示和功能封装的抽象。做出这样独立一层的抽象是,我们看实际的效果, 我们对页面不使用框架,使用框架 Component,使用框架 Component+Adapter 的性能基线对比Reducer is long-lived, Effect is medium-lived, View is short-lived.我们通过不断的测试做对比,以某 android 机为例:使用框架前 我们的详情页面的 FPS,基线在 52FPS。使用框架, 仅使用 Component 抽象下,FPS 下降到 40, 遭遇“Big-Cell”的陷阱。使用框架,同时使用 Adapter 抽象后,FPS 提升到 53,回到基线以上,有小幅度的提升。Directory推荐的目录结构会是这样sample_page– action.dart– page.dart– view.dart– effect.dart– reducer.dart– state.dartcomponentssample_component– action.dart– component.dart– view.dart– effect.dart– reducer.dart– state.dart上层负责组装,下层负责实现, 同时会有一个插件提供, 便于我们快速填写。以闲鱼的详情场景为例的组装:组件和组件之间,组件和容器之间都完全的独立。Communication Mechanism组件|适配器内通信组件|适配器间内通信简单的描述:采用的是带有一段优先处理的广播, self-first-broadcast。发出的 Action,自己优先处理,否则广播给其他组件和 Redux 处理。最终我们通过一个简单而直观的 dispatch 完成了组件内,组件间(父到子,子到父,兄弟间等)的所有的通信诉求。Refresh Mechanism数据刷新局部数据修改,自动层层触发上层数据的浅拷贝,对上层业务代码是透明的。层层的数据的拷贝一方面是对 Redux 数据修改的严格的 follow。另一方面也是对数据驱动展示的严格的 follow。视图刷新扁平化通知到所有组件,组件通过 shouldUpdate 确定自己是否需要刷新优点数据的集中管理通过 Redux 做集中化的可观察的数据管理。我们将原汁原味地保留所有的 Redux 的优势,同时在 Reducer 的合并上,变成由框架代理自动完成,大大简化了使用 Redux 的繁琐度。组件的分治管理组件既是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。View、Reducer、Effect 隔离将组件拆分成三个无状态的互不依赖的函数。因为是无状态的函数,它更易于编写、调试、测试、维护。同时它带来了更多的组合、复用和创新的可能。声明式配置组装组件、适配器通过自由的声明式配置组装来完成。包括它的 View、Reducer、Effect 以及它所依赖的子项。良好的扩展性核心框架保持自己的核心的三层关注点,不做核心关注点以外的事情,同时对上层保持了灵活的扩展性。框架甚至没有任何的一行的打印的代码,但我们可通过标准的 Middleware 来观察到数据的流动,组件的变化。在框架的核心三层外,也可以通过 dart 的语言特性 为 Component 或者 Adapter 添加 mixin,来灵活的组合式地增强他们的上层使用上的定制和能力。框架和其他中间件的打通,诸如自动曝光、高可用等,各中间件和框架之间都是透明的,由上层自由组装。精小、简单、完备它非常小,仅仅包含 1000 多行代码。它使用简单,完成几个小的函数,完成组装,即可运行。它是完备的。Fish Redux 目前已在阿里巴巴闲鱼技术团队内多场景,深入应用。本文作者:闲鱼技术-吉丰阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 18, 2019 · 2 min · jiezi

Flutter 状态管理之 Scoped Model & Redux

前言文章原文地址:Nealyang/PersonalBlog可能作为一个前端,在学习 Flutter 的过程中,总感觉非常非常相似 React Native,甚至于,其中还是有state的概念 setState,所以在 Flutter 中,也当然会存在非常多的解决方案,比如 redux 、RxDart 还有 Scoped Model等解决方案。今天,我们主要介绍下常用的两种 State 管理解决方案:redux、scoped model。Scoped Model介绍Scoped Model 是 package 上 Dart 的一个第三方库scoped_model。Scoped Model 主要是通过数据model的概念来实现数据传递,表现上类似于 react 中 context 的概念。它提供了让子代widget轻松获取父级数据model的功能。从官网中的介绍可以了解到,它直接来自于Google正在开发的新系统Fuchsia核心 Widgets 中对 Model 类的简单提取,作为独立使用的独立 Flutter 插件发布。在直接上手之前,我们先着重说一下 Scoped Model 中几个重要的概念Model 类,通过继承 Model 类来创建自己的数据 model,例如 SearchModel 或者 UserModel ,并且还可以监听 数据model的变化ScopedModelDescendant widget , 如果你需要传递数据 model 到很深层级里面的 widget ,那么你就需要用 ScopedModel 来包裹 Model,这样的话,后面所有的子widget 都可以使用该数据 model 了(是不是更有一种 context 的感觉)ScopedModelDescendant widget ,使用此 widget 可以在 widget tree 中找到相应的 Scope的Model ,当 数据 model 发生变化的时候,该 widget 会重新构建当然,在 Scoped Model 的文档中,也介绍了一些 实现原理Model类实现了Listenable接口AnimationController和TextEditingController也是Listenables使用InheritedWidget将数据 model 传递到Widget树。 重建 InheritedWidget 时,它将手动重建依赖于其数据的所有Widgets。 无需管理订阅!它使用 AnimatedBuilder Widget来监听Model并在模型更改时重建InheritedWidget实操Demodemo地址从gif上可以看到咱们的需求非常的简单,就是在当前页面更新了count后,在第二个页面也能够传递过去。当然,new ResultPage(count:count)就没意思啦~ 咱不讨论哈新建数据 modellib/model/counter_model.dart import ‘package:scoped_model/scoped_model.dart’; class CounterModel extends Model{ int _counter = 0; int get counter => _counter; void increment(){ _counter++; // 通知所有的 listener notifyListeners(); } }这一步非常的简单,新建一个类去继承 Model里面定义了一个 get方法,以便于后面取数据model定义了 increment 方法,去改变我们的数据 model ,调用 package 中的 通知方法 notifyListenerslib/main.dart import ‘package:flutter/material.dart’; import ‘./model/counter_model.dart’; import ‘package:scoped_model/scoped_model.dart’; import ‘./count_page.dart’; void main() { runApp(MyApp( model: CounterModel(), )); } class MyApp extends StatelessWidget { final CounterModel model; const MyApp({Key key,@required this.model}):super(key:key); @override Widget build(BuildContext context) { return ScopedModel( model: model, child: MaterialApp( title: ‘Scoped Model Demo’, home:CountPage(), ), ); } }这是 app 的入口文件,划重点MyApp 类 中,我们传入一个定义好的数据 model ,方便后面传递给子类将 MaterialApp 用 ScopedModel 包裹一下,作用上面已经介绍了,方便子类可以拿到 ,类似于 redux 中 Provider 包裹一下一定需要将数据 model 传递给 ScopedModel 的 model 属性中lib/count_page.dart class CountPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(‘Scoped Model’), actions: <Widget>[ IconButton( tooltip: ’to result’, icon: Icon(Icons.home), onPressed: (){ Navigator.push(context,MaterialPageRoute(builder: (context)=>ResultPage())); }, ) ], ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text(‘你都点击’), ScopedModelDescendant<CounterModel>( builder: (context, child, model) { return Text( ‘${model.counter.toString()} 次了’, style: TextStyle( color: Colors.red, fontSize: 33.0, ), ); }, ) ], ), ), floatingActionButton: ScopedModelDescendant<CounterModel>( builder: (context,child,model){ return FloatingActionButton( onPressed: model.increment, tooltip: ‘add’, child: Icon(Icons.add), ); }, ), ); } }常规布局和widget这里不再重复介绍,我们说下主角:Scoped Model简单一句,哪里需要用数据 model ,哪里就需要用 ScopedModelDescendantScopedModelDescendant中的build方法需要返回一个widget,在这个widget中我们可以使用数据 model中的方法、数据等最后在 lib/result_page.dart中就可以看到我们数据 model 中的 count 值了,注意这里跳转页面,我们并没有通过参数传递的形式传递 Navigator.push(context,MaterialPageRoute(builder: (context)=>ResultPage()));完整项目代码:flutter_scoped_modelflutter_redux相信作为一个前端对于 redux 一定不会陌生,而 Flutter 中也同样存在 state 的概念,其实说白了,UI 只是数据(state)的另一种展现形式。study-redux是笔者之前学习redux时候的一些笔记和心得。这里为了防止有新人不太清楚redux,我们再来介绍下redux的一些基本概念statestate 我们可以理解为前端UI的状态(数据)库,它存储着这个应用所有需要的数据。 action既然这些state已经有了,那么我们是如何实现管理这些state中的数据的呢,当然,这里就要说到action了。 什么是action?E:action:动作。 是的,就是这么简单。。。只有当某一个动作发生的时候才能够触发这个state去改变,那么,触发state变化的原因那么多,比如这里的我们的点击事件,还有网络请求,页面进入,鼠标移入。。。所以action的出现,就是为了把这些操作所产生或者改变的数据从应用传到store中的有效载荷。 需要说明的是,action是state的唯一信号来源。reducerreducer决定了state的最终格式。 reducer是一个纯函数,也就是说,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。reducer对传入的action进行判断,然后返回一个通过判断后的state,这就是reducer的全部职责。 从代码可以简单地看出: import {INCREMENT_COUNTER,DECREMENT_COUNTER} from ‘../actions’; export default function counter(state = 0,action) { switch (action.type){ case INCREMENT_COUNTER: return state+1; case DECREMENT_COUNTER: return state-1; default: return state; } }对于一个比较大一点的应用来说,我们是需要将reducer拆分的,最后通过redux提供的combineReducers方法组合到一起。 比如: const rootReducer = combineReducers({ counter }); export default rootReducer;这里你要明白:每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。 combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理, 然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。storestore是对之前说到一个联系和管理。具有如下职责维持应用的 state;提供 getState() 方法获取 state提供 dispatch(action) 方法更新 state;通过 subscribe(listener) 注册监听器;通过 subscribe(listener) 返回的函数注销监听器。再次强调一下 Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。 store的创建通过redux的createStore方法创建,这个方法还需要传入reducer,很容易理解:毕竟我需要dispatch一个action来改变state嘛。 应用一般会有一个初始化的state,所以可选为第二个参数,这个参数通常是有服务端提供的,传说中的Universal渲染。后面会说。。。 第三个参数一般是需要使用的中间件,通过applyMiddleware传入。说了这么多,action,store,action creator,reducer关系就是这么如下的简单明了: 结合 flutter_redux一些工具集让你轻松地使用 redux 来轻松构建 Flutter widget,版本要求是 redux.dart 3.0.0+Redux WidgetsStoreProvider :基础组件,它将给定的 Redux Store 传递给所欲请求它的的子代组件StoreBuilder : 一个子代组件,它从 StoreProvider 获取 Store 并将其传递给 widget 的 builder 方法中StoreConnector :获取 Store 的一个子代组件StoreProvider ancestor,使用给定的 converter 函数将 Store 转换为 ViewModel ,并将ViewModel传递给 builder。 只要 Store 发出更改事件(action),Widget就会自动重建。 无需管理订阅!注意Dart 2需要更严格的类型!1、确认你正使用的是 redux 3.0.0+2、在你的组件树中,将 new StoreProvider(…) 改为 new StoreProvider<StateClass>(…)3、如果需要从StoreProvider<AppState> 中直接获取 Store<AppState> ,则需要将 new StoreProvider.of(context) 改为 StoreProvider.of<StateClass> .不需要直接访问 Store 中的字段,因为Dart2可以使用静态函数推断出正确的类型实操演练官方demo的代码先大概解释一下 import ‘package:flutter/material.dart’; import ‘package:flutter_redux/flutter_redux.dart’; import ‘package:redux/redux.dart’; //定义一个action: Increment enum Actions { Increment } // 定义一个 reducer,响应传进来的 action int counterReducer(int state, dynamic action) { if (action == Actions.Increment) { return state + 1; } return state; } void main() { // 在 基础 widget 中创建一个 store,用final关键字修饰 这比直接在build方法中创建要好很多 final store = new Store<int>(counterReducer, initialState: 0); runApp(new FlutterReduxApp( title: ‘Flutter Redux Demo’, store: store, )); } class FlutterReduxApp extends StatelessWidget { final Store<int> store; final String title; FlutterReduxApp({Key key, this.store, this.title}) : super(key: key); @override Widget build(BuildContext context) { // 用 StoreProvider 来包裹你的 MaterialApp 或者别的 widget ,这样能够确保下面所有的widget能够获取到store中的数据 return new StoreProvider<int>( // 将 store 传递给 StoreProvider // Widgets 将使用 store 变量来使用它 store: store, child: new MaterialApp( theme: new ThemeData.dark(), title: title, home: new Scaffold( appBar: new AppBar( title: new Text(title), ), body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: [ new Text( ‘You have pushed the button this many times:’, ), // 通过 StoreConnector 将 store 和 Text 连接起来,以便于 Text直接render // store 中的值。类似于 react-redux 中的connect // // 将 Text widget 包裹在 StoreConnector 中, // StoreConnector将会在最近的一个祖先元素中找到 StoreProvider // 拿到对应的值,然后传递给build函数 // // 每次点击按钮的时候,将会 dispatch 一个 action并且被reducer所接受。 // 等reducer处理得出最新结果后, widget将会自动重建 new StoreConnector<int, String>( converter: (store) => store.state.toString(), builder: (context, count) { return new Text( count, style: Theme.of(context).textTheme.display1, ); }, ) ], ), ), // 同样使用 StoreConnector 来连接Store 和FloatingActionButton // 在这个demo中,我们使用store 去构建一个包含dispatch、Increment // action的回调函数 // // 将这个回调函数丢给 onPressed floatingActionButton: new StoreConnector<int, VoidCallback>( converter: (store) { return () => store.dispatch(Actions.Increment); }, builder: (context, callback) { return new FloatingActionButton( onPressed: callback, tooltip: ‘Increment’, child: new Icon(Icons.add), ); }, ), ), ), ); } }上面的例子比较简单,鉴于小册Flutter入门实战:从0到1仿写web版掘金App下面有哥们在登陆那块评论了Flutter状态管理,这里我简单使用redux模拟了一个登陆的demolib/reducer/reducers.dart首先我们定义action需要的一些action type enum Actions{ Login, LoginSuccess, LogoutSuccess }然后定义相应的类来管理登陆状态 class AuthState{ bool isLogin; //是否登录 String account; //用户名 AuthState({this.isLogin:false,this.account}); @override String toString() { return “{account:$account,isLogin:$isLogin}”; } }然后我们需要定义一些action,定义个基类,然后定义登陆成功的action class Action{ final Actions type; Action({this.type}); } class LoginSuccessAction extends Action{ final String account; LoginSuccessAction({ this.account }):super( type:Actions.LoginSuccess ); }最后定义 AppState 以及我们自定义的一个中间件。 // 应用程序状态 class AppState { AuthState auth; //登录 MainPageState main; //主页 AppState({this.main, this.auth}); @override String toString() { return “{auth:$auth,main:$main}”; } } AppState mainReducer(AppState state, dynamic action) { if (Actions.LogoutSuccess == action) { state.auth.isLogin = false; state.auth.account = null; } if (action is LoginSuccessAction) { state.auth.isLogin = true; state.auth.account = action.account; } print(“state changed:$state”); return state; } loggingMiddleware(Store<AppState> store, action, NextDispatcher next) { print(’${new DateTime.now()}: $action’); next(action); }在稍微大一点的项目中,其实就是reducer 、 state 和 action 的组织会比较麻烦,当然,罗马也不是一日建成的, 庞大的state也是一点一点累计起来的。下面就是在入口文件中使用 redux 的代码了,跟基础demo没有差异。 import ‘package:flutter/material.dart’; import ‘package:flutter_redux/flutter_redux.dart’; import ‘package:redux/redux.dart’; import ‘dart:async’ as Async; import ‘./reducer/reducers.dart’; import ‘./login_page.dart’; void main() { Store<AppState> store = Store<AppState>(mainReducer, initialState: AppState( main: MainPageState(), auth: AuthState(), ), middleware: [loggingMiddleware]); runApp(new MyApp( store: store, )); } class MyApp extends StatelessWidget { final Store<AppState> store; MyApp({Key key, this.store}) : super(key: key); @override Widget build(BuildContext context) { return new StoreProvider(store: store, child: new MaterialApp( title: ‘Flutter Demo’, theme: new ThemeData( primarySwatch: Colors.blue, ), home: new StoreConnector<AppState,AppState>(builder: (BuildContext context,AppState state){ print(“isLogin:${state.auth.isLogin}”); return new MyHomePage(title: ‘Flutter Demo Home Page’, counter:state.main.counter, isLogin: state.auth.isLogin, account:state.auth.account); }, converter: (Store<AppState> store){ return store.state; }) , routes: { “login”:(BuildContext context)=>new StoreConnector(builder: ( BuildContext context,Store<AppState> store ){ return new LoginPage(callLogin: (String account,String pwd) async{ print(“正在登录,账号$account,密码:$pwd”); // 为了模拟实际登录,这里等待一秒 await new Async.Future.delayed(new Duration(milliseconds: 1000)); if(pwd != “123456”){ throw (“登录失败,密码必须是123456”); } print(“登录成功!”); store.dispatch(new LoginSuccessAction(account: account)); },); }, converter: (Store<AppState> store){ return store; }), }, )); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title, this.counter, this.isLogin, this.account}) : super(key: key); final String title; final int counter; final bool isLogin; final String account; @override Widget build(BuildContext context) { print(“build:$isLogin”); Widget loginPane; if (isLogin) { loginPane = new StoreConnector( key: new ValueKey(“login”), builder: (BuildContext context, VoidCallback logout) { return new RaisedButton( onPressed: logout, child: new Text(“您好:$account,点击退出”),); }, converter: (Store<AppState> store) { return () => store.dispatch( Actions.LogoutSuccess ); }); } else { loginPane = new RaisedButton(onPressed: () { Navigator.of(context).pushNamed(“login”); }, child: new Text(“登录”),); } return new Scaffold( appBar: new AppBar( title: new Text(title), ), body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ /// 有登录,展示你好:xxx,没登录,展示登录按钮 loginPane ], ), ), ); } }完整项目代码:Nealyang/Flutter最后更多学习 Flutter的小伙伴,欢迎入QQ群 Flutter Go :679476515关于 Flutter 组件以及更多的学习,敬请关注我们正在开发的: alibaba/flutter-go参考flutter_architecture_samplesflutter_reduxflutter examplescoped_model ...

January 16, 2019 · 6 min · jiezi

Redux初体验

redux是一种对项目进行统一的状态管理机制,父子组件间通信是通过props属性值传递和子组件回调父组件提前声明好的方法,整个state只能从上到下,而没有回溯的能力,redux将所有的state集中到所有组件顶层,然后分发给每个组件自己需要的state,更好的管理状态,顶层分发状态,让React组件被动地渲染,而react hooks解决的是pure render function (渲染函数组件)拥有状态和生命周期react-redux 构成1.store单一数据源 store 由rootReducer组成,用于最外层组件import { createStore } from ‘redux’;import { Provider } from ‘react-redux’;const store = creacteStore(rootReducer);<Provider store={store}><App /></Provider>2.rootReducerrootReducer由多个reducers组成import { combineReducers } from ‘redux’;export default combineReducers({reducerA, reducerB})3.Reducer每一个reducer由 oldState(initState) 和 action组成,通过action.type 返回 newState,default 返回oldStateexport default function count (state = 0, action) { switch(action.type) { case ‘count’: return state + 1 default: return state }}4.Actionaction 可以是一个方法,用于返回对象,也可以直接是一个对象,type属性是必须的export function count () { return { type: ‘count’ }}5.conncetconnect 用于连接组件和数据import { connect } from ‘react-redux’export default connect(mapStateToProps, mapDispatchToProps)(App)6.mapStateToPropsmapStateToProps 方法用于将状态映射成属性,返回组件需要的属性const mapStateToProps = (state) => { return { count: state.count }}7.mapDispatchToPropsmapDispatchToProps 提供给组件一个属性用于触发dispatch,也就是用户触发actionconst mapDispatchToProps = (dispatch) => { return { addCount: () => dispath(someAction) // dispatch的对象是action }}react-redux注意点state = store.getState() // 获取statestate.count = state.count + 1 // state是只读的,只能通过dispatch(action)改变我写了一个完整的简单的 react-redux-count-demo 项目,也是上面演示的列子参考 Redux 简明教程和理解 React,但不理解 Redux,该如何通俗易懂的理解 Redux? - Wang Namelos的回答 - 知乎 ...

January 15, 2019 · 1 min · jiezi

Reactv16.7.0-alpha.2 Hooks学习

Hooks的来源Hooks => 钩子,顾名思义,为了解决在函数组件(Function Component)中使用state和生命周期,同时提高业务逻辑复用。 Function Component == Puer Render Function 函数组件等同于一个纯的专门用作渲染的函数,我们知道,在函数组件中,我们无法使用state和生命周期,这也是Hooks为了解决的问题。第一个API: useStateimport { useState } from react // 引入const [count, setCount] = useState(0) 相当于this.state ={ count = 0} 所以 useState(arg)放数组 字符串 对象都可以,就是起到一个初始化state的作用setCount 相当于 this.setState({count: count})count = count + 1 这样的写法是错的,不能直接修改state的值,需要使用setCount(value)我们可以声明多个状态第二个API: useEffect这个函数是为了解决当状态或者传入的props发生变化后,需要做出的逻辑处理比如: count + 1 后, 就会触发useEffect( () => {// 逻辑处理在这里}, [count]) //第二个参数是绑定需要监听变化的参数下面是一个完整的例子父组件中传入的props value 每秒 + 1 父组件就不贴代码了,文末有完整代码地址这个项目里包含自定义Hook 以及useEffect的触生命周期,包含自身state以及父组件传入prop改变后,useEffect的用法import React from ‘react’;import { useState, useEffect } from ‘react’;// 自定义hooksfunction diyHooks (value) { const [flag, setFlag] = useState(false); useEffect(() => { if(value % 2 === 0) { setFlag(true) } else { setFlag(false) } console.log(flag) }, [value]) return flag;}function Try (props) { const [count, setCount] = useState(0) const [number, setNumber] = useState(0) const value = props.value const flag = diyHooks(props.value) useEffect(() => { console.log(‘count’, count); }, [count]) useEffect(() => { console.log(’number’, number); }, [number]) useEffect(() => { console.log(‘props’, value) }, [value]) return ( <div> <span>{flag === true ? ’true’ : ‘false’}</span> <span>{value}</span> <button onClick={() => { setCount(count + 1) if(count % 2 === 1) { setNumber(number + 1) } }}>Try It</button> </div> )}export default Try;关于自定义hooks,我写了一个react-hooks 介绍了React Hooks的简单用法 ...

January 11, 2019 · 1 min · jiezi

react-redux-antd项目搭建(1)

因为是想搭建一个后台系统,所以组件直接定了antd,脚手架以create-react-app为基准,加上redux和sass(因为我一直用的less,这次换个口味),搭建一个简单的项目。安装依赖首先安装create-react-appnpm i create-react-app给项目起个名字create-react-app my_react_cli(项目名)啪嗒回车,开始安装项目,此过程会持续几分钟,可以去干点别的~安装完成后,就是下图的样子了图片描述运行一下看看,有没有问题npm start图片描述 There might be a problem with the project dependency tree. It is likely not a bug in Create React App, but something you need to fix locally. The react-scripts package provided by Create React App requires a dependency: “babel-loader”: “8.0.4” Don’t try to install it manually: your package manager does it automatically. However, a different version of babel-loader was detected higher up in the tree: C:\Users\liu\node_modules\babel-loader (version: 7.1.5)因为文主之前搭建别的项目时安装了babel-loader,导致的版本不对,那就卸掉babel-loader,按照所需的8.0.4版本安装一下卸载babel-loadernpm uninstall babel-loader 然后安装正确的版本npm i babel-loader@8.0.4可能会出现再次报错的情况,可以删掉在你的项目文件夹里面的 node_modules重新安装继续安装各种依赖这里我暂时装了antd@3.12.1和react-redux@6.0.0运行一下看看图片描述接下里就要开始封装公共方法休息一下 有空再写 ...

January 11, 2019 · 1 min · jiezi

使用 React + Redux 制作兰顿蚂蚁演示程序

简介最早接触兰顿蚂蚁是在做参数化的时候,那时候只感觉好奇,以为是很复杂的东西。因无意中看到生命游戏的 React 实现,所以希望通过兰顿蚂蚁的例子再学习一下 React。兰顿蚂蚁的规则非常简单:如果蚂蚁位于白色方块,则向右旋转 90°,反转方块的颜色,然后向前移动一步。如果蚂蚁位于黑色方块,则向左旋转 90°,反转方块的颜色,然后向前移动一步。如下图所示:蚂蚁在前一百步有一定规律,之后陷入混沌,直到一万步之后将走出混沌形成一条高速公路。兰顿蚂蚁和生命游戏都是元胞自动机的一种,关于兰顿蚂蚁的更多介绍可以看维基百科开始编写程序在本教程中,我主要还是说一下项目中的问题及难点,不会对整个项目做太详细的介绍,把代码粘贴一遍也没什么意义,大家可以自己摸索一遍,其中 Webpack 用了 4.0,顺便说一句,Webpack4.0 还是有不少坑的,项目在 GitHub 中有,遇到问题可以翻阅一下源代码源码:https://github.com/nzbin/lang…先看一下最终效果的动图演示:这个项目可以说是 React + Redux 非常基础的练习。主要就是绘制网格,根据蚂蚁规则重绘网格。以下是项目目录:src├── actions│ └── index.js├── components│ ├── app.js│ ├── button.js│ └── cell.js ├── containers │ ├── board.js│ ├── control.js│ └── counter.js ├── reducers│ ├── index.js│ ├── reducer_board.js│ ├── reducer_generations.js│ └── reducer_play_status.js └── index.js蚂蚁法则的算法兰顿蚂蚁演示程序的关键就是蚂蚁规则的算法,其实算法也很简单,设置方向变量,模拟蚂蚁的前进线路即可。以下是逻辑代码:// status: true -> black squareif (gameState[row][col].status) { gameState[row][col].status = false; // ant: turnLeft90 -> move forward 1 step switch (dir) { case ‘T’: ant[‘pos’] = [row, col - 1]; ant[‘dir’] = ‘L’; break; case ‘B’: ant[‘pos’] = [row, col + 1]; ant[‘dir’] = ‘R’; break; case ‘L’: ant[‘pos’] = [row + 1, col]; ant[‘dir’] = ‘B’; break; case ‘R’: ant[‘pos’] = [row - 1, col]; ant[‘dir’] = ‘T’; break; default: }}// status: false -> white squareelse if (!gameState[row][col].status) { gameState[row][col].status = true; // ant: turnRight90 -> move forward 1 step switch (dir) { case ‘T’: ant[‘pos’] = [row, col + 1]; ant[‘dir’] = ‘R’; break; case ‘B’: ant[‘pos’] = [row, col - 1]; ant[‘dir’] = ‘L’; break; case ‘L’: ant[‘pos’] = [row - 1, col]; ant[‘dir’] = ‘T’; break; case ‘R’: ant[‘pos’] = [row + 1, col]; ant[‘dir’] = ‘B’; break; default: }}布局演示程序的网格如果只是写死的话就非常简单,但是为了有更好的体验,我做成了响应式,无论有多少网格,总能全部显示在屏幕上。看似很简单的问题,其实有很多可以学习的地方。制作响应式网格的方式有很多,比如结合媒体查询,百分比。为了效果更好一点,我选择了百分比。其次正方形网格也有多种方式实现,比如 vw 单位,百分比+padding。其中使用 vw 单位会有一个问题,就是它的相对父元素是视窗,所以网格总是全屏显示,比较恶心。最后使用了百分比+padding 的方式。细节方面还使用了 calc 运算。但是百分比计算的网格存在精度问题,适当放大尺寸可以解决。查看在线 Demo:https://nzbin.github.io/langt…性能因为我对 React 的研究不深,所以在这个项目中遇到了一些性能问题,绘制一个 100X100 的网格的话,在 FireFox 中明显感觉到卡顿(与我的机子也有关系),Chrome 表现还可以。其实用 canvas 做演示程序可能更好一些,同时跑多个蚂蚁也没有问题。总结因各种各样的原因,没想到这篇文章又拖了半年多才写完,与其说是教程,不如说是对兰顿蚂蚁的介绍,更惭愧的是文章内容不深,无法帮助更多的初学者。我不是 React 的拥泵,目前专注 Angular,所以关于 React 的译文以及简易教程就到此为止吧。 ...

January 3, 2019 · 1 min · jiezi

ngrx

ngrx/store本文档会持续更新。StoreStrore是Angular基于Rxjs的状态管理,保存了Redux的核心概念,并使用RxJs扩展的Redux实现。使用Observable来简化监听事件和订阅等操作。在看这篇文章之前,已经假设你已了解rxjs和redux。官方文档 有条件的话,请查看官方文档进行学习理解。安装npm install @ngrx/storeTutorial下面这个Tutorial将会像你展示如何管理一个计数器的状态和如何查询以及将它显示在Angular的Component上。你可以通过StackBlitz来在线测试。1.创建actionssrc/app/counter.actions.tsimport {Action} from ‘@ngrx/store’;export enum ActionTypes { Increment = ‘[Counter Component] Increment’, Decrement = ‘[Counter Component] Decrement’, Reset = ‘[Counter Component] Reset’,}export class Increment implements Action { readonly type = ActionTyoes.Increment;}export class Decrement implements Action { readonly type = ActionTypes.Decrement;}export class Reset implements Action { readonly tyoe = Actiontypes.Reset;}2.定义一个reducer通过所提供的action来处理计数器state的变化。src/app/counter.reducer.tsimport {Action} from ‘@ngrx/store’;import {ActionTypes} from ‘./conter.actions’;export const initailState = 0;export function conterReducer(state = initialState, action: Action) { switch(action.type) { case ActionTypes.Increment: return state + 1; case ActionTypes.Decrement: return state - 1; case ActionTypes.Reset: return 0; default: return state; }}3.在src/app/app.module.ts中导入 StoreModule from @ngrx/store 和 counter.reducerimport {StroeModule} from ‘@ngrx/store’;import {counterReducer} from ‘./counter.reducer’;4.在你的AppModule的imports array添加StoreModule.forRoot,并在StoreModule.forRoot中添加count 和 countReducer对象。StoreModule.forRoot()函数会注册一个用于访问store的全局变量。scr/app/app.module.tsimport { BrowserModule } from ‘@angular/platform-browser’;import { NgModule } from ‘@angular/core’; import { AppComponent } from ‘./app.component’; import { StoreModule } from ‘@ngrx/store’;import { counterReducer } from ‘./counter.reducer’;@NgModule({ declaration: [AppComponent], imports: [ BrowserModule, StoreModule.forRoot({count: countReducer}) ], provoders: [], bootstrap: [AppComponent]})export class AppModule {}5.在app文件夹下新创建一个叫my-counter的Component,注入Store service到你的component的constructor函数中,并使用select操作符在state查询数据。更新MyCounterComponent template,添加添加、减少和重设操作,分别调用increment,decrement,reset方法。并使用async管道来订阅count$ Observable。src/app/my-counter/my-counter.component.html<button (click)=“increment()">Increment</button> <div>Current Count: {{ count$ | async }}</div><button (click)=“decrement()">Decrement</button><button (click)=“reset()">Reset Counter</button>更新MyCounterComponent类,创建函数并分发(dispatch)Increment,Decrement和Reset actions.import { Component } from ‘@angular/core’;import { Store, select } from ‘@ngrx/store’;import { Observable } from ‘rxjs’;import { Increment, Decrement, Reset } from ‘../counter.actions’;@Component({ selector: ‘app-my-counter’, templateUrl: ‘./my-counter.component.html’, styleUrls: [’./my-counter.component.css’],})export class MyCounterComponent ( count$: Observable<number>; constructor(private store: Stare<{count: number}>) { this.count$ = store.pipe(select(‘count’)); } increment() { this.store.dispatch(new Increment()); } decrement() { this.store.dispatch(new Decrement()); } reset() { this.store.dispatch(new Reset()); })6.添加MyCounter component到AppComponent template中<app-my-counter></app-my-counter>ActionsActions是NgRx的核心模块之一。Action表示在整个应用中发生的独特的事件。从用户与页面的交互,与外部的网络请求的交互和直接与设备的api交互,这些和更多的事件通过actions来描述。介绍在NgRx的许多地方都使用了actions。Actions是NgRx许多系统的输入和输出。Action帮助你理解如何在你的应用中处理事件。Action接口(Action interface)NgRx通过简单的interface来组成Action:interface Action { type: string;}这个interface只有一个属性:type,string类型。这个type属性将描述你的应用调度的action。这个类型的值以[Source]的形式出现和使用,用于提供它是什么类型的操作的上下文和action在哪里被调度(dispatched)。您可以向actions添加属性,以便为操作提供其他上下文或元数据。最常见的属性就是payload,它会添加action所需的所有数据。下面列出的是作为普通javascript对象编写的操作的示例:{ type: ‘[Auth API] Login Success’}这个action描述了调用后端API成功认证的时间触发。{ type: ‘[Login Page]’, payload: { username: string; password: string; }}这个action描述了用户在登录页面点击登录按钮尝试认证用户的时间触发。payload包含了登录页面提供的用户名和密码。编写 actions有一些编写actions的好习惯:前期——在开始开发功能之前编写编写action,以便理解功能和知识点分类——基于事件资源对actions进行分类编写更多——action的编写容易,所以你可以编写更多的actions,来更好的表达应用流程事件-驱动——捕获事件而不是命令,因为你要分离事件的描述和事件的处理描述——提供针对唯一事件的上下文,其中包含可用于帮助开发人员进行调试的更详细信息遵循这些指南可帮助您了解这些actions在整个应用程序中的流程。下面是一个启动登陆请求的action示例:import {} from ‘@ngrx/store’;export class Login Implements Action { readonly type = ‘[Login Page] Login’ constructor(public: payload: {username: string, password: string}){}}action编写成类,以便在dispatched操作时提供类型安全的方法来构造action。Login action 实现(implements) Action interface。在示例中,payload是一个包含username和password的object,这是处理action所需的其他元数据.在dispatch时,新实例化一个实例。login-page.component.tsclick(username: string, password: string) { store.dispatch(new Login({username:username, password: password}))}Login action 有关于action来自于哪里和事件发生了什么的独特上线文。action的类型包含在[]内类别用于对形状区域的action进行分组,无论他是组件页面,后端api或浏览器api类别后面的Login文本是关于action发生了什么的描述。在这个例子中,用户点击登录页面上的登录按钮来通过用户名密码来尝试认证。创建action unionsactions的消费者,无论是reducers(纯函数)或是effects(带副作用的函数)都使用actions的type来确定是否要执行这个action。在feature区域,多个actions组合在一起,但是每个action都需要提供自己的type信息。看上一个Login action 例子,你将为action定义一些额外的信息。import {Action} from ‘@ngrx/store’;export enum ActionTypes { Login = ‘[Login Page] Login’;}export class Login Implememts Action { readonly type = ActionTypes.Login; constructor(public paylad: {username: string, password: string})}export type Union = Login;将action type string放在enum中而不是直接放在class内。此外,还会使用Union类去导出Loginclass.ReducersNgRx中的Reducers负责处理应用程序中从一个状态到下一个状态的转换。Reducer函数从action的类型来确定如何处理状态。介绍Reducer函数是一个纯函数,函数为相同的输入返回相同的输出。它们没有副作用,可以同步处理每个状态转化。每个reducer都会调用最新的action,当前状态(state)和确定是返回最新修改的state还是原始state。这个指南将会向你展示如何去编写一个reducer函数,并在你的store中注册它,并组成独特的state。关于reducer函数每一个由state管理的reducer都有一些共同点:接口和类型定义了state的形状参数包含了初始state或是当前state、当前actionswitch语句下面这个例子是state的一组action,和相对应的reducer函数。首先,定义一些与state交互的actions。scoreboard-page.actions.tsimport {Action} from ‘@ngrx/store’;export enum Actiontypes { IncrementHome = ‘[Scoreboard Page] Home Score’, IncrementAway = ‘[Scoreboard Page] Away Score’, Reset = ‘[Scoreboard Page] Score Reset’,}export class IncrementHome implements Action { readonly type = ActionTypes.IncrementHome;}export class IncrementAway implements Action { readonly type = ActionTypes.IncrementAway;}export class Reset implements Action { readonly type = ActionTypes.Reset; constructor(public payload: {home: number, away: number}) {}}export type ActionsUnion = IncrementHome | IncrementAway | Reset;接下来,创建reducer文件,导入actions,并定义这个state的形状。定义state的形状每个reducer函数都会监听actions,上面定义的scorebnoard actions描述了reducer处理的可能转化。导入多组actions以处理reducer其他的state转化。scoreboard.reducer.tsimport * as Scoreboard from ‘../actions/scoreboard-page.actions’;export interface State { home: number; away: number;}根据你捕获的内容来定义state的形状,它是单一的类型,如number,还是一个含有多个属性的object。设置初始state初始state给state提供了初始值,或是在当前state是undefined时提供值。您可以使用所需state属性的默认值设置初始state。创建并导出变量以使用一个或多个默认值捕获初始state。scoreboard.reducer.tsexport const initialState: Satate = { home: 0, away: 0,};创建reducer函数reducer函数的职责是以不可变的方式处理state的更变。定义reducer函数来处理actions来管理state。scoreboard.reducer.tsexport function reducer { satate = initialState, action: Scoreboard.ActionsUnion}: State { switch(action.type) { case Scoreboard.ActionTypes.IncrementHome: { return { …state, home: state.home + 1, } } case Scoreboard.ActionTypes.IncrementAway: { return { …state, away: state.away + 1, } } case Scoreboard.ActionTypes.Reset: { return action.payload; } default: { return state; } }}Reducers将switch语句与TypeScript在您的actions中定义的区分联合组合使用,以便在reducer中提供类型安全的操作处理。Switch语句使用type union来确定每种情况下正在使用的actions的正确形状。action的types定在你的action在你的reducer函数的case语句。type union 也约束你的reducer的可用操作。在这个例子中,reducer函数处理3个actions:IncrementHome,IncrementAway,Reset。每个action都有一个基于ActionUnion提供的强类型。每个action都可以不可逆的处理state。这意味着state更变不会修改源state,而是使用spread操作返回一个更变后的新的state。spread语法从当前state拷贝属性,并创建一个新的返回。这确保每次更变都会有新的state,保证了函数的纯度。这也促进了引用完整性,保证在发生状态更改时丢弃旧引用注意:spread操作只执行浅复制,不处理深层嵌套对象。您需要复制对象中的每个级别以确保不变性。有些库可以处理深度复制,包括lodash和immer。当action被调度时,所有注册过的reducers都会接收到这个action。通过switch语句确定是否处理这个action。因为这个原因,每个switch语句中总是包含default case,当这个reducer不处理action时,返回提供的state。注册root statestate在你的应用中定义为一个large object。注册reducer函数。注册reducer函数来管理state的各个部分中具有关联值的键。使用StoreModule.forRoot()函数和键值对来定义你的state,来在你的应用中注册一个全局的Store。StoreModule.forRoot()在你的应用中注册一个全局的providers,将包含这个调度state的action和select的Store服务注入到你的component和service中。app.module.tsimport {NgModule} from ‘@angular/core’;import {StoreModule} form ‘@ngrx/store’;import {scoreboardReducer} from ‘./reducers/scoreboard.resucer’;@NgModule({ imports: [StoreModule.forRoot({game: scoreboardReducer})],})export class AppModule {}使用StoreModule.forRoot()注册states可以在应用启动时定义状态。通常,您注册的state始终需要立即用于应用的所有区域。注册形状state形状states的行为和root state相同,但是你在你的应用中需要定义具体的形状区域。你的state是一个large object,形状state会在这个object中以键值对的形式注册。下面这个state object的例子,你将看到形状state如何以递增的方式构建你的state。让我们从一个空的state开始。app.module.ts@NgModule({ imports: [StoreModule.forRoot({})],})export class AppModule {}这里在你的应用中创建了一个空的state{}现在使用scoreboardreducer和名称为ScoreboarModule的形状NgModule注册一个额外的state。scoreboard.module.tsimport { NgModule } from ‘@angular/core’;import { StoreModule } from ‘@ngrx/store’;import { scoreboardReducer } from ‘./reducers/scoreboard.reducer’;@NgModule({ imports: [StoreModule.forFeature(‘game’, scoreboardReducer)],})export class ScoreboardModule {}添加ScoreboardModule到APPModule。app.module.tsimport { NgModule } from ‘@angular/core’;import { StoreModule } from ‘@ngrx/store’;import { ScoreboardModule } from ‘./scoreboard/scoreboard.module’;@NgModule({ imports: [StoreModule.forRoot({}), ScoreboardModule],})export class AppModule {}每一次ScoreboardModule被加载,这个game将会变为这个object的一个属性,并被管理在state中。{ game: { home: 0, away: 0}}形状state的加载是eagerly还是lazlly的,取决于你的应用。可以使用形状状态随时间和不同形状区域构建状态对象。selectSelector是一个获得store state的切片的纯函数。@ngrx/store提供了一些辅助函数来简化selection。selector提供了很多对state的切片功能。轻便的记忆化组成的可测试的类型安全的当使用createSelector和createFeatureSelector函数时,@ngrx/store会跟踪调用选择器函数的最新参数。因为选择器是纯函数,所以当参数匹配时可以返回最后的结果而不重新调用选择器函数。这可以提供性能优势,特别是对于执行昂贵计算的选择器。这种做法称为memoization。使用selector切片stateindex.tsimport {createSelector} from ‘@ngrx/store’;export interface FeatureState { counter: number;}export interface AppSatte { feature: FeatureState;}export const selectFeature = (state: AppState) => state.feature;export const selectFeatureCount = createSelector( selectFeature, (state: FeatrureState) => state.counter)使用selectors处理多切片createSelector能够从基于同样一个state的几个切片state中获取一些数据。createSelector最多能够接受8个selector函数,以获得更加完整的state selections。在下面这个例子中,想象一下你有selectUser object 在你的state中,你还有book object的allBooks数组。你想要显示你当前用户的所有书。你能够使用createSelector来实现这些。如果你在allBooks中更新他们,你的可见的书将永远是最新的。如果选择了一本书,它们将始终显示属于您用户的书籍,并且在没有选择用户时显示所有书籍。结果将会是你从你的state中过滤一部分,并且他永远是最新的。import {createSelecotr} from ‘@ngrx/store’;export interface User { id: number; name: string;}export interface Book { id: number; userId: number; name: string;}export interface AppState { selectoredUser: User; allBooks: Book[];}export const selectUser = (state: AppSate) => state.selectedUser;export const SelectAllBooks = (state: AppState) => state.allBooks;export const SelectVisibleBooks = createSelector( selectUser, selectAllBooks, (selectedUser: User, allBooks: Books[]) => { if(selectedUser && allBooks) { return allBooks.filter((book: Book) => book.UserId === selectedUser.id); }else { return allBooks; } })使用selecotr props当store中没有一个适合的select来获取切片state,你可以通过selector函数的props。在下面的例子中,我们有计数器,并希望他乘以一个值,我们可以添加乘数并命名为prop:index.tsexport const getCount = createSelector( getCounterValue, (counter, props) => counter * props.multiply);在这个组件内部,我们定义了一个props。ngOnInit() { this.counter = this.store.pipe(select(formRoot.getCount, {multiply: 2}));}记住,selector只将之前的输入参数保存在了缓存中,如果你用另一个乘数来重新使用这个selector,selector总会去重新计算它的值,这是因为他在接收两个乘数。为了正确地记忆selector,将selector包装在工厂函数中以创建选择器的不同实例index.tsexport const getCount = () => { createSelector( (state, props) => state.counter[props.id], (counter, props) => counter * props* multiply );}组件的selector现在调用工厂函数来创建不同的选择器实例: ngOnInit() { this.counter2 = this.store.pipe(select(fromRoot.getCount(), { id: ‘counter2’, multiply: 2 })); this.counter4 = this.store.pipe(select(fromRoot.getCount(), { id: ‘counter4’, multiply: 4 })); this.counter6 = this.store.pipe(select(fromRoot.getCount(), { id: ‘counter6’, multiply: 6 })); } ...

December 29, 2018 · 4 min · jiezi

深度剖析 redux applyMiddleware 中 compose 构建异步数据流的思路

前言本文作者站在自己的角度深入浅出…算了别这么装逼分析 redux applyMiddleware 在设计过程中通过 compose 构建异步数据流的思路。自己假设的一些场景帮助理解,希望大家在有异步数据流并且使用redux的过程中能够有自己的思路(脱离thunk or saga)构建自己的 enhancer.如果你看完本文之后还想对我有更多的了解,可以移步我的github;正文言归正传,既然是解决异步问题那么我们就给自己设立一个小场景吧,请问下面这个函数数组,如何让其顺序调用。const fucArr = [ next=>{ setTimeout(()=>{ console.log(1); next() }, 300) }, next=>{ setTimeout(()=>{ console.log(2); next() }, 200) }, next=>{ setTimeout(()=>{ console.log(3); next() }, 100) }]我撸起袖子就开始干了起来,有三个函数,基于动态规划…别扯的那么牛皮走一步看一步思想那我就先执行两个吧 fucArr0;// TypeError: next is not a function报错,因为fucArr[1]中有next函数调用,也得接收一个函数,这下就麻烦了,fucArr[1]又不能直接传参调用(因为会比fucArr[0]先执行),于是乎我们需要婉转一点。 fucArr0; //1 2 两个函数顺序执行搞定了那三个函数岂不是,没错,小case。 fucArr[0]( ()=>fucArr[1](()=>{ fucArr2 }) );// 1 2 3那我想在数组后面再加一个函数内心os:不加,去死,这样写下去真是要没玩没了了;既然是个数组,那咱们就循环吧,思路肯定是:1.下个函数重新整合一下,作为参数往上一个函数传;2.当到遍历到数组末尾的时候传入一个空函数进去避免报错。OK开始,既然是循环那就来个for循环吧,既然是下一个函数传给上一个当参数,得让相邻的两个函数出现在同一个循环里啦。于是有了起手式: for (let index = 0; index < fucArr.length; index++) { const current = array[index]; const next = array[index + 1]; current(()=>next()) }起手后发现不对呀,我需要喝口热水,压压惊,冷静一下,仔细观察一下上面咱们代码的结构发现咱们的函数结构其实是酱紫的: a(()=>{ b(c) })实际就上上一个函数调用被 ()=> 包裹后的下一个函数直接调用并传入一个函数c,而函数c会在函数b的运行的某个时刻被调用,并且能接收下一个函数作为参数然后……再说下去就没玩没了了,因此c函数的模式其实也是被一个()=>{}包裹住的函数;然后再观察我们上面的模式没有c传递,因此模式应该是: a(c=>{ b(c) }) // 我们再往下写一层 a( d=>{ ( c=>b(c) )( d=>c(d) )// 为了避免你们看不懂我在写啥,我告诉你你,这玩意儿是函数自调用 } ) // 怎么样是不是有一种豁然开朗的赶脚我们发现每次新加入一个函数,都是重新构建一次a函数里的参数,以下我将这个参数简称函数d于是乎我们来通过循环构建这个d为了让循环体都能拿到d,因此它肯定是在循环的上层作用域而且d具有两个特性:能接受一个函数作为参数,这个函数还能接收另一个函数作为参数,并会在某个时刻进行调用每次循环都会根据当前d,然后加入当前函数,按照相同模式进行重构;ps: 我们发现这两个特性其实和咱们传入的每个函数特性是一致的。 于是乎咱们把第一个数组的函数组作为起始函数: var statusRecord = fucArr[0]; for (let index = 1; index < fucArr.length; index++) { statusRecord = next=>statusRecord(()=>fucArrindex) } 写完发现这样是错误的,如果调用函数statusRecord那就会变成,自己调自己,自己调自己,自己调自己,自己调自己皮一下很开心…的无限递归。 在循环记录当前状态的场景下,有一个经典的demo大家了解过:在一个li列表中注册点击事件,点击后alert出当前index;具体就不详述了于是statusRecord,就改写成了下面这样 statusRecord = ((statusRecord)=>(next)=>statusRecord(()=>fucArrindex)(statusRecord)) 为什么index不传呢?因为index是let定义,可以看做块级作用域,又有人要说js没有块级作用域,我:你说得对,再见。 最后咱们得到的还是这个模型要调用,别忘了传入一个函数功最后数组最后一个函数调用。不然会报错 statusRecord(()=>{}) // 输出1、2、3那咱们的功能就此实现了;不过可以优化一哈。咱们上面的代码有几个要素:数组循环状态传递初始状态为数组的第一个元素最终需要拿到单一的返回值不就是活脱脱用来描述reduce的吗?于是乎我们可以这样撸 //pre 前一个状态、 cur当前循环函数、next 待接收的下一个 fucArr.reduce((pre, cur)=>{ return (next)=>pre(next=>()=>cur(next)) })(()=>{})// 1 2 3 以上异步顺序调用的问题咱们已经理解了,咱们依次输出了1,2,3。但是咱们现实业务中常常是下一个函数执行,和上一个函数执行结果是关联的。咱们就想能不能改动题目贴合实际场景,上一个函数告诉下一个函数console.log(n),于是乎题目做了一个小调整。 const fucArr = [ next=>{ setTimeout(()=>{ console.log(1); next(2) }, 300) }, // 函数2 (next,n)=>{ console.log(n); next(3) }, // 函数3 (next,n)=>{ console.log(n); next(4) } ] fucArr.reduce((pre,cur)=>{ return (next)=>pre((n)=>cur(next,n)) })((n)=>{console.log(n)})// 1 2 3 4 哇,功能又实现了,我们真棒。现在我们来回忆一下redux里中间件里传入函数格式store=>next=>action=>{ // dosomething… next()} 在某一步中store会被剥掉,在这就不细说了,于是咱们题目再变个种 const fucArr = [ next=>n=>{ setTimeout(()=>{ console.log(n); next(n+1) }, 300) }, // 函数2 next=>n=>{ setTimeout(()=>{ console.log(n); next(n+1) }, 300) }, // 函数3 next=>n=>{ setTimeout(()=>{ console.log(n); next(n+1) }, 300) } ]卧槽,我们发现之于之前遇到的问题,这个实现就舒服很多了。因为你传入的函数应该是直接调用,因为我们需要的调用的函数体其实是传入函数调用后返回的那个函数,不需要我们通过()=>{…}这种额外的包装。于是咱们的实现就变成了: fucArr.reduce((pre,cur)=>{ return (next)=>pre(cur(next)) })((n)=>{console.log(n)})我们自信满满的node xxx.js了一下发现?????what fuck 为啥什么都没有输出,喝第二口水压压惊分析一下: // before 之前的第一个函数和函数模型 next=>{ setTimeout(()=>{ console.log(1); next(n+1) }, 300) } a(c=>{ b(c) }) // ———— // after 现在的第一个函数和函数模型 next=>n=>{ setTimeout(()=>{ console.log(n); next(n+1) }, 300) } a(b(c)) // 发现现在的第一个函数调用之后,一个函数。这个函数还要再接收一个参数去启动(⊙v⊙)嗯没错,经过精妙的分析我知道要怎么做了。 fucArr.reduce((pre,cur)=>{ return (next)=>pre(cur(next)) })((n)=>{console.log(n)})(1)// 1 2 3 4我们来把这个功能包装成方法,就叫他compose好了。 const compose = fucArr=>{ if(fucArr.length === 0) return; if(fucArr.length === 1) return fucArr0(1) fucArr.reduce((pre,cur)=>{ return (next)=>pre(cur(next)) })((n)=>{console.log(n)})(1) }看上去那是相当的完美,根据咱们写代码的思路咱们来比对一下原版吧。length === 0 时: 返回一个传入什么返回什么的函数。length === 1 时: 直接返回传入函数函数。length > 1 时: 构建一个a(b(c(….)))这种函数调用模型并返回,使用者自定义最后一环需要运行的函数,并且能够定义进入第一环的初始参数 // 原版 function compose(…funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (…args) => a(b(…args))) }结语最后说一点题外话,在整个实现的过程中确保异步调用顺序还有很多方式。亲测可用的方式有:bind递归调用通过new Promise 函数,将resolve作为参数方法传入上一个函数然后改变Promise状态…,如果大家有兴趣可以自己实现一下,为了不把大家的思路带歪,在写的过程中并没有体现出来。如果觉得我写对你有一定的帮助,那就点个赞吧,因为您的鼓励是我最大的动力。 ...

December 22, 2018 · 2 min · jiezi

深入浅出之React-redux中connect的装饰器用法@connect

这篇文章主要介绍了react-redux中connect的装饰器用法@connect详解,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。通常我们需要一个reducer和一个action,然后使用connect来包裹你的Component。假设你已经有一个key为main的reducer和一个action.js. 我们的App.js一般都这么写:import React from ‘react’import {render} from ‘react-dom’import {connect} from ‘react-redux’import {bindActionCreators} from ‘redux’import action from ‘action.js’ class App extends React.Component{ render(){ return <div>hello</div> }}function mapStateToProps(state){ return state.main}function mapDispatchToProps(dispatch){ return bindActionCreators(action,dispatch)}//欢迎加入前端全栈开发交流圈一起学习交流:864305860export default connect(mapStateToProps,mapDispatchToProps)(App)这样并没有什么问题。看着connect的用法,有没有觉得很熟悉?典型的wrapper嘛,这里必须拿装饰器来装X,稍微改一改:import React from ‘react’import {render} from ‘react-dom’import {connect} from ‘react-redux’import {bindActionCreators} from ‘redux’import action from ‘action.js’ @connect( state=>state.main, dispatch=>bindActionCreators(action,dispatch))class App extends React.Component{ render(){ return <div>hello</div> }//欢迎加入前端全栈开发交流圈一起学习交流:864305860}emmm,这样舒服很多了,在我们实际项目中,可能是一个模块下面又有很多个小组件,它们都共用同样的action和reducer,我们在每个组件中都这么写,是不是有点太麻烦了?冗余代码太多了。其实是可以把connect抽取出来的,比如写一个connect.js:import {connect} from ‘react-redux’import {bindActionCreators} from ‘redux’import action from ‘action.js’ export default connect( state=>state.main, dispatch=>bindActionCreators(action,dispatch))//欢迎加入前端全栈开发交流圈一起学习交流:864305860然后在需要用到的组件中这么用:import React from ‘react’import {render} from ‘react-dom’import connect from ‘connect.js’ @connectexport default class App extends React.Component{ render(){ return <div>hello</div> }}//欢迎加入前端全栈开发交流圈一起学习交流:864305860这样就ok了,和最开始的用法比起来,是不是明显更装X更好用?需要说明的是,这里用了装饰器,需要安装模块babel-plugin-transform-decorators-legacy,然后在babel中配置:{ “plugins”:[ “transform-decorators-legacy” ]}//欢迎加入前端全栈开发交流圈一起学习交流:864305860如果你用的是vscode, 可以在项目根目录下添加jsconfig.json文件来消除代码警告:{ “compilerOptions”: { “experimentalDecorators”: true }}//欢迎加入前端全栈开发交流圈一起学习交流:864305860结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 18, 2018 · 1 min · jiezi

Vuex、Flux、Redux、Redux-saga、Dva、MobX

这篇文章试着聊明白这一堆看起来挺复杂的东西。在聊之前,大家要始终记得一句话:一切前端概念,都是纸老虎。不管是Vue,还是 React,都需要管理状态(state),比如组件之间都有共享状态的需要。什么是共享状态?比如一个组件需要使用另一个组件的状态,或者一个组件需要改变另一个组件的状态,都是共享状态。父子组件之间,兄弟组件之间共享状态,往往需要写很多没有必要的代码,比如把状态提升到父组件里,或者给兄弟组件写一个父组件,听听就觉得挺啰嗦。如果不对状态进行有效的管理,状态在什么时候,由于什么原因,如何变化就会不受控制,就很难跟踪和测试了。如果没有经历过这方面的困扰,可以简单理解为会搞得很乱就对了。在软件开发里,有些通用的思想,比如隔离变化,约定优于配置等,隔离变化就是说做好抽象,把一些容易变化的地方找到共性,隔离出来,不要去影响其他的代码。约定优于配置就是很多东西我们不一定要写一大堆的配置,比如我们几个人约定,view 文件夹里只能放视图,不能放过滤器,过滤器必须放到 filter 文件夹里,那这就是一种约定,约定好之后,我们就不用写一大堆配置文件了,我们要找所有的视图,直接从 view 文件夹里找就行。根据这些思想,对于状态管理的解决思路就是:把组件之间需要共享的状态抽取出来,遵循特定的约定,统一来管理,让状态的变化可以预测。根据这个思路,产生了很多的模式和库,我们来挨个聊聊。Store 模式最简单的处理就是把状态存到一个外部变量里面,比如:this.$root.$data,当然也可以是一个全局变量。但是这样有一个问题,就是数据改变后,不会留下变更过的记录,这样不利于调试。所以我们稍微搞得复杂一点,用一个简单的 Store 模式:var store = { state: { message: ‘Hello!’ }, setMessageAction (newValue) { // 发生改变记录点日志啥的 this.state.message = newValue }, clearMessageAction () { this.state.message = ’’ }}store 的 state 来存数据,store 里面有一堆的 action,这些 action 来控制 state 的改变,也就是不直接去对 state 做改变,而是通过 action 来改变,因为都走 action,我们就可以知道到底改变(mutation)是如何被触发的,出现错误,也可以记录记录日志啥的。不过这里没有限制组件里面不能修改 store 里面的 state,万一组件瞎胡修改,不通过 action,那我们也没法跟踪这些修改是怎么发生的。所以就需要规定一下,组件不允许直接修改属于 store 实例的 state,组件必须通过 action 来改变 state,也就是说,组件里面应该执行 action 来分发 (dispatch) 事件通知 store 去改变。这样约定的好处是,我们能够记录所有 store 中发生的 state 改变,同时实现能做到记录变更 (mutation)、保存状态快照、历史回滚/时光旅行的先进的调试工具。这样进化了一下,一个简单的 Flux 架构就实现了。FluxFlux其实是一种思想,就像MVC,MVVM之类的,他给出了一些基本概念,所有的框架都可以根据他的思想来做一些实现。Flux把一个应用分成了4个部分:ViewActionDispatcherStore比如我们搞一个应用,显而易见,这个应用里面会有一堆的 View,这个 View 可以是Vue的,也可以是 React的,啥框架都行,啥技术都行。View 肯定是要展示数据的,所谓的数据,就是 Store,Store 很容易明白,就是存数据的地方。当然我们可以把 Store 都放到一起,也可以分开来放,所以就有一堆的 Store。但是这些 View 都有一个特点,就是 Store 变了得跟着变。View 怎么跟着变呢?一般 Store 一旦发生改变,都会往外面发送一个事件,比如 change,通知所有的订阅者。View 通过订阅也好,监听也好,不同的框架有不同的技术,反正 Store 变了,View 就会变。View 不是光用来看的,一般都会有用户操作,用户点个按钮,改个表单啥的,就需要修改 Store。Flux 要求,View 要想修改 Store,必须经过一套流程,有点像我们刚才 Store 模式里面说的那样。视图先要告诉 Dispatcher,让 Dispatcher dispatch 一个 action,Dispatcher 就像是个中转站,收到 View 发出的 action,然后转发给 Store。比如新建一个用户,View 会发出一个叫 addUser 的 action 通过 Dispatcher 来转发,Dispatcher 会把 addUser 这个 action 发给所有的 store,store 就会触发 addUser 这个 action,来更新数据。数据一更新,那么 View 也就跟着更新了。这个过程有几个需要注意的点:Dispatcher 的作用是接收所有的 Action,然后发给所有的 Store。这里的 Action 可能是 View 触发的,也有可能是其他地方触发的,比如测试用例。转发的话也不是转发给某个 Store,而是所有 Store。Store 的改变只能通过 Action,不能通过其他方式。也就是说 Store 不应该有公开的 Setter,所有 Setter 都应该是私有的,只能有公开的 Getter。具体 Action 的处理逻辑一般放在 Store 里。听听描述看看图,可以发现,Flux的最大特点就是数据都是单向流动的。ReduxFlux 有一些缺点(特点),比如一个应用可以拥有多个 Store,多个Store之间可能有依赖关系;Store 封装了数据还有处理数据的逻辑。所以大家在使用的时候,一般会用 Redux,他和 Flux 思想比较类似,也有差别。StoreRedux 里面只有一个 Store,整个应用的数据都在这个大 Store 里面。Store 的 State 不能直接修改,每次只能返回一个新的 State。Redux 整了一个 createStore 函数来生成 Store。import { createStore } from ‘redux’;const store = createStore(fn);Store 允许使用 store.subscribe 方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。这样不管 View 是用什么实现的,只要把 View 的更新函数 subscribe 一下,就可以实现 State 变化之后,View 自动渲染了。比如在 React 里,把组件的render方法或setState方法订阅进去就行。Action和 Flux 一样,Redux 里面也有 Action,Action 就是 View 发出的通知,告诉 Store State 要改变。Action 必须有一个 type 属性,代表 Action 的名称,其他可以设置一堆属性,作为参数供 State 变更时参考。const action = { type: ‘ADD_TODO’, payload: ‘Learn Redux’};Redux 可以用 Action Creator 批量来生成一些 Action。ReducerRedux 没有 Dispatcher 的概念,Store 里面已经集成了 dispatch 方法。store.dispatch()是 View 发出 Action 的唯一方法。import { createStore } from ‘redux’;const store = createStore(fn);store.dispatch({ type: ‘ADD_TODO’, payload: ‘Learn Redux’});Redux 用一个叫做 Reducer 的纯函数来处理事件。Store 收到 Action 以后,必须给出一个新的 State(就是刚才说的Store 的 State 不能直接修改,每次只能返回一个新的 State),这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。什么是纯函数呢,就是说没有任何的副作用,比如这样一个函数:function getAge(user) { user.age = user.age + 1; return user.age;}这个函数就有副作用,每一次相同的输入,都可能导致不同的输出,而且还会影响输入 user 的值,再比如:let b = 10;function compare(a) { return a >= b;}这个函数也有副作用,就是依赖外部的环境,b 在别处被改变了,返回值对于相同的 a 就有可能不一样。而 Reducer 是一个纯函数,对于相同的输入,永远都只会有相同的输出,不会影响外部的变量,也不会被外部变量影响,不得改写参数。它的作用大概就是这样,根据应用的状态和当前的 action 推导出新的 state:(previousState, action) => newState类比 Flux,Flux 有些像: (state, action) => state为什么叫做 Reducer 呢?reduce 是一个函数式编程的概念,经常和 map 放在一起说,简单来说,map 就是映射,reduce 就是归纳。映射就是把一个列表按照一定规则映射成另一个列表,而 reduce 是把一个列表通过一定规则进行合并,也可以理解为对初始值进行一系列的操作,返回一个新的值。比如 Array 就有一个方法叫 reduce,Array.prototype.reduce(reducer, ?initialValue),把 Array 整吧整吧弄成一个 newValue。const array1 = [1, 2, 3, 4];const reducer = (accumulator, currentValue) => accumulator + currentValue;// 1 + 2 + 3 + 4console.log(array1.reduce(reducer));// expected output: 10// 5 + 1 + 2 + 3 + 4console.log(array1.reduce(reducer, 5));// expected output: 15看起来和 Redux 的 Reducer 是不是好像好像,Redux 的 Reducer 就是 reduce 一个列表(action的列表)和一个 initialValue(初始的 State)到一个新的 value(新的 State)。把上面的概念连起来,举个例子:下面的代码声明了 reducer:const defaultState = 0;const reducer = (state = defaultState, action) => { switch (action.type) { case ‘ADD’: return state + action.payload; default: return state; }};createStore接受 Reducer 作为参数,生成一个新的 Store。以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。import { createStore } from ‘redux’;const store = createStore(reducer);createStore 内部干了什么事儿呢?通过一个简单的 createStore 的实现,可以了解大概的原理(可以略过不看):const createStore = (reducer) => { let state; let listeners = []; const getState = () => state; const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; const subscribe = (listener) => { listeners.push(listener); return () => { listeners = listeners.filter(l => l !== listener); } }; dispatch({}); return { getState, dispatch, subscribe };};Redux 有很多的 Reducer,对于大型应用来说,State 必然十分庞大,导致 Reducer 函数也十分庞大,所以需要做拆分。Redux 里每一个 Reducer 负责维护 State 树里面的一部分数据,多个 Reducer 可以通过 combineReducers 方法合成一个根 Reducer,这个根 Reducer 负责维护整个 State。import { combineReducers } from ‘redux’;// 注意这种简写形式,State 的属性名必须与子 Reducer 同名const chatReducer = combineReducers({ Reducer1, Reducer2, Reducer3})combineReducers 干了什么事儿呢?通过简单的 combineReducers 的实现,可以了解大概的原理(可以略过不看):const combineReducers = reducers => { return (state = {}, action) => { return Object.keys(reducers).reduce( (nextState, key) => { nextState[key] = reducers[key](state[key], action); return nextState; }, {} ); };};流程再回顾一下刚才的流程图,尝试走一遍 Redux 流程:1、用户通过 View 发出 Action:store.dispatch(action);2、然后 Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。let nextState = xxxReducer(previousState, action);3、State 一旦有变化,Store 就会调用监听函数。store.subscribe(listener);4、listener可以通过 store.getState() 得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。function listerner() { let newState = store.getState(); component.setState(newState); }对比 Flux和 Flux 比较一下:Flux 中 Store 是各自为战的,每个 Store 只对对应的 View 负责,每次更新都只通知对应的View:Redux 中各子 Reducer 都是由根 Reducer 统一管理的,每个子 Reducer 的变化都要经过根 Reducer 的整合:简单来说,Redux有三大原则:单一数据源:Flux 的数据源可以是多个。State 是只读的:Flux 的 State 可以随便改。使用纯函数来执行修改:Flux 执行修改的不一定是纯函数。Redux 和 Flux 一样都是单向数据流。中间件刚才说到的都是比较理想的同步状态。在实际项目中,一般都会有同步和异步操作,所以 Flux、Redux 之类的思想,最终都要落地到同步异步的处理中来。在 Redux 中,同步的表现就是:Action 发出以后,Reducer 立即算出 State。那么异步的表现就是:Action 发出以后,过一段时间再执行 Reducer。那怎么才能 Reducer 在异步操作结束后自动执行呢?Redux 引入了中间件 Middleware 的概念。其实我们重新回顾一下刚才的流程,可以发现每一个步骤都很纯粹,都不太适合加入异步的操作,比如 Reducer,纯函数,肯定不能承担异步操作,那样会被外部IO干扰。Action呢,就是一个纯对象,放不了操作。那想来想去,只能在 View 里发送 Action 的时候,加上一些异步操作了。比如下面的代码,给原来的 dispatch 方法包裹了一层,加上了一些日志打印的功能:let next = store.dispatch;store.dispatch = function dispatchAndLog(action) { console.log(‘dispatching’, action); next(action); console.log(’next state’, store.getState());}既然能加日志打印,当然也能加入异步操作。所以中间件简单来说,就是对 store.dispatch 方法进行一些改造的函数。不展开说了,所以如果想详细了解中间件,可以点这里。Redux 提供了一个 applyMiddleware 方法来应用中间件:const store = createStore( reducer, applyMiddleware(thunk, promise, logger));这个方法主要就是把所有的中间件组成一个数组,依次执行。也就是说,任何被发送到 store 的 action 现在都会经过thunk,promise,logger 这几个中间件了。处理异步对于异步操作来说,有两个非常关键的时刻:发起请求的时刻,和接收到响应的时刻(可能成功,也可能失败或者超时),这两个时刻都可能会更改应用的 state。一般是这样一个过程:请求开始时,dispatch 一个请求开始 Action,触发 State 更新为“正在请求”状态,View 重新渲染,比如展现个Loading啥的。请求结束后,如果成功,dispatch 一个请求成功 Action,隐藏掉 Loading,把新的数据更新到 State;如果失败,dispatch 一个请求失败 Action,隐藏掉 Loading,给个失败提示。显然,用 Redux 处理异步,可以自己写中间件来处理,当然大多数人会选择一些现成的支持异步处理的中间件。比如 redux-thunk 或 redux-promise 。Redux-thunkthunk 比较简单,没有做太多的封装,把大部分自主权交给了用户:const createFetchDataAction = function(id) { return function(dispatch, getState) { // 开始请求,dispatch 一个 FETCH_DATA_START action dispatch({ type: FETCH_DATA_START, payload: id }) api.fetchData(id) .then(response => { // 请求成功,dispatch 一个 FETCH_DATA_SUCCESS action dispatch({ type: FETCH_DATA_SUCCESS, payload: response }) }) .catch(error => { // 请求失败,dispatch 一个 FETCH_DATA_FAILED action dispatch({ type: FETCH_DATA_FAILED, payload: error }) }) }}//reducerconst reducer = function(oldState, action) { switch(action.type) { case FETCH_DATA_START : // 处理 loading 等 case FETCH_DATA_SUCCESS : // 更新 store 等 case FETCH_DATA_FAILED : // 提示异常 }}缺点就是用户要写的代码有点多,可以看到上面的代码比较啰嗦,一个请求就要搞这么一套东西。Redux-promiseredus-promise 和 redux-thunk 的思想类似,只不过做了一些简化,成功失败手动 dispatch 被封装成自动了:const FETCH_DATA = ‘FETCH_DATA’//action creatorconst getData = function(id) { return { type: FETCH_DATA, payload: api.fetchData(id) // 直接将 promise 作为 payload }}//reducerconst reducer = function(oldState, action) { switch(action.type) { case FETCH_DATA: if (action.status === ‘success’) { // 更新 store 等处理 } else { // 提示异常 } }}刚才的什么 then、catch 之类的被中间件自行处理了,代码简单不少,不过要处理 Loading 啥的,还需要写额外的代码。其实任何时候都是这样:封装少,自由度高,但是代码就会变复杂;封装多,代码变简单了,但是自由度就会变差。redux-thunk 和 redux-promise 刚好就是代表这两个面。redux-thunk 和 redux-promise 的具体使用就不介绍了,这里只聊一下大概的思路。大部分简单的异步业务场景,redux-thunk 或者 redux-promise 都可以满足了。上面说的 Flux 和 Redux,和具体的前端框架没有什么关系,只是思想和约定层面。下面就要和我们常用的 Vue 或 React 结合起来了:VuexVuex 主要用于 Vue,和 Flux,Redux 的思想很类似。Store每一个 Vuex 里面有一个全局的 Store,包含着应用中的状态 State,这个 State 只是需要在组件中共享的数据,不用放所有的 State,没必要。这个 State 是单一的,和 Redux 类似,所以,一个应用仅会包含一个 Store 实例。单一状态树的好处是能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。Vuex通过 store 选项,把 state 注入到了整个应用中,这样子组件能通过 this.&dollar;store 访问到 state 了。const app = new Vue({ el: ‘#app’, // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件 store, components: { Counter }, template: &lt;div class="app"&gt; &lt;counter&gt;&lt;/counter&gt; &lt;/div&gt; })const Counter = { template: &lt;div&gt;{{ count }}&lt;/div&gt;, computed: { count () { return this.$store.state.count } }}State 改变,View 就会跟着改变,这个改变利用的是 Vue 的响应式机制。Mutation显而易见,State 不能直接改,需要通过一个约定的方式,这个方式在 Vuex 里面叫做 mutation,更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { // 变更状态 state.count++ } }})触发 mutation 事件的方式不是直接调用,比如 increment(state) 是不行的,而要通过 store.commit 方法:store.commit(‘increment’)注意:mutation 都是同步事务。mutation 有些类似 Redux 的 Reducer,但是 Vuex 不要求每次都搞一个新的 State,可以直接修改 State,这块儿又和 Flux 有些类似。具尤大的说法,Redux 强制的 immutability,在保证了每一次状态变化都能追踪的情况下强制的 immutability 带来的收益很有限,为了同构而设计的 API 很繁琐,必须依赖第三方库才能相对高效率地获得状态树的局部状态,这些都是 Redux 不足的地方,所以也被 Vuex 舍掉了。到这里,其实可以感觉到 Flux、Redux、Vuex 三个的思想都差不多,在具体细节上有一些差异,总的来说都是让 View 通过某种方式触发 Store 的事件或方法,Store 的事件或方法对 State 进行修改或返回一个新的 State,State 改变之后,View 发生响应式改变。Action到这里又该处理异步这块儿了。mutation 是必须同步的,这个很好理解,和之前的 reducer 类似,不同步修改的话,会很难调试,不知道改变什么时候发生,也很难确定先后顺序,A、B两个 mutation,调用顺序可能是 A -> B,但是最终改变 State 的结果可能是 B -> A。对比Redux的中间件,Vuex 加入了 Action 这个东西来处理异步,Vuex的想法是把同步和异步拆分开,异步操作想咋搞咋搞,但是不要干扰了同步操作。View 通过 store.dispatch(‘increment’) 来触发某个 Action,Action 里面不管执行多少异步操作,完事之后都通过 store.commit(‘increment’) 来触发 mutation,一个 Action 里面可以触发多个 mutation。所以 Vuex 的Action 类似于一个灵活好用的中间件。Vuex 把同步和异步操作通过 mutation 和 Action 来分开处理,是一种方式。但不代表是唯一的方式,还有很多方式,比如就不用 Action,而是在应用内部调用异步请求,请求完毕直接 commit mutation,当然也可以。Vuex 还引入了 Getter,这个可有可无,只不过是方便计算属性的复用。Vuex 单一状态树并不影响模块化,把 State 拆了,最后组合在一起就行。Vuex 引入了 Module 的概念,每个 Module 有自己的 state、mutation、action、getter,其实就是把一个大的 Store 拆开。总的来看,Vuex 的方式比较清晰,适合 Vue 的思想,在实际开发中也比较方便。对比ReduxRedux:view——>actions——>reducer——>state变化——>view变化(同步异步一样)Vuex:view——>commit——>mutations——>state变化——>view变化(同步操作)view——>dispatch——>actions——>mutations——>state变化——>view变化(异步操作)React-reduxRedux 和 Flux 类似,只是一种思想或者规范,它和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。但是因为 React 包含函数式的思想,也是单向数据流,和 Redux 很搭,所以一般都用 Redux 来进行状态管理。为了简单处理 Redux 和 React UI 的绑定,一般通过一个叫 react-redux 的库和 React 配合使用,这个是 react 官方出的(如果不用 react-redux,那么手动处理 Redux 和 UI 的绑定,需要写很多重复的代码,很容易出错,而且有很多 UI 渲染逻辑的优化不一定能处理好)。Redux将React组件分为容器型组件和展示型组件,容器型组件一般通过connect函数生成,它订阅了全局状态的变化,通过mapStateToProps函数,可以对全局状态进行过滤,而展示型组件不直接从global state获取数据,其数据来源于父组件。如果一个组件既需要UI呈现,又需要业务逻辑处理,那就得拆,拆成一个容器组件包着一个展示组件。因为 react-redux 只是 redux 和 react 结合的一种实现,除了刚才说的组件拆分,并没有什么新奇的东西,所以只拿一个简单TODO项目的部分代码来举例:入口文件 index.js,把 redux 的相关 store、reducer 通过 Provider 注册到 App 里面,这样子组件就可以拿到 store 了。import React from ‘react’import { render } from ‘react-dom’import { Provider } from ‘react-redux’import { createStore } from ‘redux’import rootReducer from ‘./reducers’import App from ‘./components/App’const store = createStore(rootReducer)render( <Provider store={store}> <App /> </Provider>, document.getElementById(‘root’))actions/index.js,创建 Action:let nextTodoId = 0export const addTodo = text => ({ type: ‘ADD_TODO’, id: nextTodoId++, text})export const setVisibilityFilter = filter => ({ type: ‘SET_VISIBILITY_FILTER’, filter})export const toggleTodo = id => ({ type: ‘TOGGLE_TODO’, id})export const VisibilityFilters = { SHOW_ALL: ‘SHOW_ALL’, SHOW_COMPLETED: ‘SHOW_COMPLETED’, SHOW_ACTIVE: ‘SHOW_ACTIVE’}reducers/todos.js,创建 Reducers:const todos = (state = [], action) => { switch (action.type) { case ‘ADD_TODO’: return [ …state, { id: action.id, text: action.text, completed: false } ] case ‘TOGGLE_TODO’: return state.map(todo => todo.id === action.id ? { …todo, completed: !todo.completed } : todo ) default: return state }}export default todosreducers/index.js,把所有的 Reducers 绑定到一起:import { combineReducers } from ‘redux’import todos from ‘./todos’import visibilityFilter from ‘./visibilityFilter’export default combineReducers({ todos, visibilityFilter, …})containers/VisibleTodoList.js,容器组件,connect 负责连接React组件和Redux Store:import { connect } from ‘react-redux’import { toggleTodo } from ‘../actions’import TodoList from ‘../components/TodoList’const getVisibleTodos = (todos, filter) => { switch (filter) { case ‘SHOW_COMPLETED’: return todos.filter(t => t.completed) case ‘SHOW_ACTIVE’: return todos.filter(t => !t.completed) case ‘SHOW_ALL’: default: return todos }}// mapStateToProps 函数指定如何把当前 Redux store state 映射到展示组件的 props 中const mapStateToProps = state => ({ todos: getVisibleTodos(state.todos, state.visibilityFilter)})// mapDispatchToProps 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法。const mapDispatchToProps = dispatch => ({ toggleTodo: id => dispatch(toggleTodo(id))})export default connect( mapStateToProps, mapDispatchToProps)(TodoList)简单来说,react-redux 就是多了个 connect 方法连接容器组件和UI组件,这里的“连接”就是一种映射:mapStateToProps 把容器组件的 state 映射到UI组件的 propsmapDispatchToProps 把UI组件的事件映射到 dispatch 方法Redux-saga刚才介绍了两个Redux 处理异步的中间件 redux-thunk 和 redux-promise,当然 redux 的异步中间件还有很多,他们可以处理大部分场景,这些中间件的思想基本上都是把异步请求部分放在了 action creator 中,理解起来比较简单。redux-saga 采用了另外一种思路,它没有把异步操作放在 action creator 中,也没有去处理 reductor,而是把所有的异步操作看成“线程”,可以通过普通的action去触发它,当操作完成时也会触发action作为输出。saga 的意思本来就是一连串的事件。redux-saga 把异步获取数据这类的操作都叫做副作用(Side Effect),它的目标就是把这些副作用管理好,让他们执行更高效,测试更简单,在处理故障时更容易。在聊 redux-saga 之前,需要熟悉一些预备知识,那就是 ES6 的 Generator。如果从没接触过 Generator 的话,看着下面的代码,给你个1分钟傻瓜式速成,函数加个星号就是 Generator 函数了,Generator 就是个骂街生成器,Generator 函数里可以写一堆 yield 关键字,可以记成“丫的”,Generator 函数执行的时候,啥都不干,就等着调用 next 方法,按照顺序把标记为“丫的”的地方一个一个拎出来骂(遍历执行),骂到最后没有“丫的”标记了,就返回最后的return值,然后标记为 done: true,也就是骂完了(上面只是帮助初学者记忆,别喷~)。function* helloWorldGenerator() { yield ‘hello’; yield ‘world’; return ’ending’;}var hw = helloWorldGenerator();hw.next() // 先把 ‘hello’ 拎出来,done: false 代表还没骂完// { value: ‘hello’, done: false } next() 方法有固定的格式,value 是返回值,done 代表是否遍历结束hw.next() // 再把 ‘world’ 拎出来,done: false 代表还没骂完// { value: ‘world’, done: false }hw.next() // 没有 yield 了,就把最后的 return ’ending’ 拎出来,done: true 代表骂完了// { value: ’ending’, done: true }hw.next() // 没有 yield,也没有 return 了,真的骂完了,只能挤出来一个 undefined 了,done: true 代表骂完了// { value: undefined, done: true }这样搞有啥好处呢?我们发现 Generator 函数的很多代码可以被延缓执行,也就是具备了暂停和记忆的功能:遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值,等着下一次调用next方法时,再继续往下执行。用 Generator 来写异步代码,大概长这样:function* gen(){ var url = ‘https://api.github.com/users/github'; var jsonData = yield fetch(url); console.log(jsonData);}var g = gen();var result = g.next(); // 这里的result是 { value: fetch(‘https://api.github.com/users/github'), done: true }// fetch(url) 是一个 Promise,所以需要 then 来执行下一步result.value.then(function(data){ return data.json();}).then(function(data){ // 获取到 json data,然后作为参数调用 next,相当于把 data 传给了 jsonData,然后执行 console.log(jsonData); g.next(data);});再回到 redux-saga 来,可以把 saga 想象成开了一个以最快速度不断地调用 next 方法并尝试获取所有 yield 表达式值的线程。举个例子:// saga.jsimport { take, put } from ‘redux-saga/effects’function* mySaga(){ // 阻塞: take方法就是等待 USER_INTERACTED_WITH_UI_ACTION 这个 action 执行 yield take(USER_INTERACTED_WITH_UI_ACTION); // 阻塞: put方法将同步发起一个 action yield put(SHOW_LOADING_ACTION, {isLoading: true}); // 阻塞: 将等待 FetchFn 结束,等待返回的 Promise const data = yield call(FetchFn, ‘https://my.server.com/getdata'); // 阻塞: 将同步发起 action (使用刚才返回的 Promise.then) yield put(SHOW_DATA_ACTION, {data: data});}这里用了好几个yield,简单理解,也就是每个 yield 都发起了阻塞,saga 会等待执行结果返回,再执行下一指令。也就是相当于take、put、call、put 这几个方法的调用变成了同步的,上面的全部完成返回了,才会执行下面的,类似于 await。用了 saga,我们就可以很细粒度的控制各个副作用每一部的操作,可以把异步操作和同步发起 action 一起,随便的排列组合。saga 还提供 takeEvery、takeLatest 之类的辅助函数,来控制是否允许多个异步请求同时执行,尤其是 takeLatest,方便处理由于网络延迟造成的多次请求数据冲突或混乱的问题。saga 看起来很复杂,主要原因可能是因为大家不熟悉 Generator 的语法,还有需要学习一堆新增的 API 。如果抛开这些记忆的东西,改造一下,再来看一下代码:function mySaga(){ if (action.type === ‘USER_INTERACTED_WITH_UI_ACTION’) { store.dispatch({ type: ‘SHOW_LOADING_ACTION’, isLoading: true}); const data = await Fetch(‘https://my.server.com/getdata'); store.dispatch({ type: ‘SHOW_DATA_ACTION’, data: data}); }}上面的代码就很清晰了吧,全部都是同步的写法,无比顺畅,当然直接这样写是不支持的,所以那些 Generator 语法和API,无非就是做一些适配而已。saga 还能很方便的并行执行异步任务,或者让两个异步任务竞争:// 并行执行,并等待所有的结果,类似 Promise.all 的行为const [users, repos] = yield [ call(fetch, ‘/users’), call(fetch, ‘/repos’)]// 并行执行,哪个先完成返回哪个,剩下的就取消掉了const {posts, timeout} = yield race({ posts: call(fetchApi, ‘/posts’), timeout: call(delay, 1000)})saga 的每一步都可以做一些断言(assert)之类的,所以非常方便测试。而且很容易测试到不同的分支。这里不讨论更多 saga 的细节,大家了解 saga 的思想就行,细节请看文档。对比 Redux-thunk比较一下 redux-thunk 和 redux-saga 的代码:和 redux-thunk 等其他异步中间件对比来说,redux-saga 主要有下面几个特点:异步数据获取的相关业务逻辑放在了单独的 saga.js 中,不再是掺杂在 action.js 或 component.js 中。dispatch 的参数是标准的 action,没有魔法。saga 代码采用类似同步的方式书写,代码变得更易读。代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理。很容易测试,如果是 thunk 的 Promise,测试的话就需要不停的 mock 不同的数据。其实 redux-saga 是用一些学习的复杂度,换来了代码的高可维护性,还是很值得在项目中使用的。DvaDva是什么呢?官方的定义是:dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。简单理解,就是让使用 react-redux 和 redux-saga 编写的代码组织起来更合理,维护起来更方便。之前我们聊了 redux、react-redux、redux-saga 之类的概念,大家肯定觉得头昏脑涨的,什么 action、reducer、saga 之类的,写一个功能要在这些js文件里面不停的切换。dva 做的事情很简单,就是让这些东西可以写到一起,不用分开来写了。比如:app.model({ // namespace - 对应 reducer 在 combine 到 rootReducer 时的 key 值 namespace: ‘products’, // state - 对应 reducer 的 initialState state: { list: [], loading: false, }, // subscription - 在 dom ready 后执行 subscriptions: [ function(dispatch) { dispatch({type: ‘products/query’}); }, ], // effects - 对应 saga,并简化了使用 effects: { [‘products/query’]: function*() { yield call(delay(800)); yield put({ type: ‘products/query/success’, payload: [‘ant-tool’, ‘roof’], }); }, }, // reducers - 就是传统的 reducers reducers: { ‘products/query’ { return { …state, loading: true, }; }, [‘products/query/success’](state, { payload }) { return { …state, loading: false, list: payload }; }, },});以前书写的方式是创建 sagas/products.js, reducers/products.js 和 actions/products.js,然后把 saga、action、reducer 啥的分开来写,来回切换,现在写在一起就方便多了。比如传统的 TODO 应用,用 redux + redux-saga 来表示结构,就是这样:saga 拦截 add 这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 addTodoSuccess 的 action, 提示创建成功, 反之则发送 addTodoFail 的 action 即可。如果使用 Dva,那么结构图如下:整个结构变化不大,最主要的就是把 store 及 saga 统一为一个 model 的概念(有点类似 Vuex 的 Module),写在了一个 js 文件里。增加了一个 Subscriptions, 用于收集其他来源的 action,比如快捷键操作。app.model({ namespace: ‘count’, state: { record: 0, current: 0, }, reducers: { add(state) { const newCurrent = state.current + 1; return { …state, record: newCurrent > state.record ? newCurrent : state.record, current: newCurrent, }; }, minus(state) { return { …state, current: state.current - 1}; }, }, effects: { *add(action, { call, put }) { yield call(delay, 1000); yield put({ type: ‘minus’ }); }, }, subscriptions: { keyboardWatcher({ dispatch }) { key(’⌘+up, ctrl+up’, () => { dispatch({type:‘add’}) }); }, },});之前我们说过约定优于配置的思想,Dva正式借鉴了这个思想。MobX前面扯了这么多,其实还都是 Flux 体系的,都是单向数据流方案。接下来要说的 MobX,就和他们不太一样了。我们先清空一下大脑,回到初心,什么是初心?就是我们最初要解决的问题是什么?最初我们其实为了解决应用状态管理的问题,不管是 Redux 还是 MobX,把状态管理好是前提。什么叫把状态管理好,简单来说就是:统一维护公共的应用状态,以统一并且可控的方式更新状态,状态更新后,View跟着更新。不管是什么思想,达成这个目标就ok。Flux 体系的状态管理方式,只是一个选项,但并不代表是唯一的选项。MobX 就是另一个选项。MobX背后的哲学很简单:任何源自应用状态的东西都应该自动地获得。译成人话就是状态只要一变,其他用到状态的地方就都跟着自动变。看这篇文章的人,大概率会对面向对象的思想比较熟悉,而对函数式编程的思想略陌生。Flux 或者说 Redux 的思想主要就是函数式编程(FP)的思想,所以学习起来会觉得累一些。而 MobX 更接近于面向对象编程,它把 state 包装成可观察的对象,这个对象会驱动各种改变。什么是可观察?就是 MobX 老大哥在看着 state 呢。state 只要一改变,所有用到它的地方就都跟着改变了。这样整个 View 可以被 state 来驱动。const obj = observable({ a: 1, b: 2})autoRun(() => { console.log(obj.a)})obj.b = 3 // 什么都没有发生obj.a = 2 // observe 函数的回调触发了,控制台输出:2上面的obj,他的 obj.a 属性被使用了,那么只要 obj.a 属性一变,所有使用的地方都会被调用。autoRun 就是这个老大哥,他看着所有依赖 obj.a 的地方,也就是收集所有对 obj.a 的依赖。当 obj.a 改变时,老大哥就会触发所有依赖去更新。MobX 允许有多个 store,而且这些 store 里的 state 可以直接修改,不用像 Redux 那样每次还返回个新的。这个有点像 Vuex,自由度更高,写的代码更少。不过它也会让代码不好维护。MobX 和 Flux、Redux 一样,都是和具体的前端框架无关的,也就是说可以用于 React(mobx-react) 或者 Vue(mobx-vue)。一般来说,用到 React 比较常见,很少用于 Vue,因为 Vuex 本身就类似 MobX,很灵活。如果我们把 MobX 用于 React 或者 Vue,可以看到很多 setState() 和 this.state.xxx = 这样的处理都可以省了。还是和上面一样,只介绍思想。具体 MobX 的使用,可以看这里。对比 Redux我们直观地上两坨实现计数器代码:Redux:import React, { Component } from ‘react’;import { createStore, bindActionCreators,} from ‘redux’;import { Provider, connect } from ‘react-redux’;// ①action typesconst COUNTER_ADD = ‘counter_add’;const COUNTER_DEC = ‘counter_dec’;const initialState = {a: 0};// ②reducersfunction reducers(state = initialState, action) { switch (action.type) { case COUNTER_ADD: return {…state, a: state.a+1}; case COUNTER_DEC: return {…state, a: state.a-1}; default: return state }}// ③action creatorconst incA = () => ({ type: COUNTER_ADD });const decA = () => ({ type: COUNTER_DEC });const Actions = {incA, decA};class Demo extends Component { render() { const { store, actions } = this.props; return ( <div> <p>a = {store.a}</p> <p> <button className=“ui-btn” onClick={actions.incA}>增加 a</button> <button className=“ui-btn” onClick={actions.decA}>减少 a</button> </p> </div> ); }}// ④将state、actions 映射到组件 propsconst mapStateToProps = state => ({store: state});const mapDispatchToProps = dispatch => ({ // ⑤bindActionCreators 简化 dispatch actions: bindActionCreators(Actions, dispatch)})// ⑥connect产生容器组件const Root = connect( mapStateToProps, mapDispatchToProps)(Demo)const store = createStore(reducers)export default class App extends Component { render() { return ( <Provider store={store}> <Root /> </Provider> ) }}MobX:import React, { Component } from ‘react’;import { observable, action } from ‘mobx’;import { Provider, observer, inject } from ‘mobx-react’;// 定义数据结构class Store { // ① 使用 observable decorator @observable a = 0;}// 定义对数据的操作class Actions { constructor({store}) { this.store = store; } // ② 使用 action decorator @action incA = () => { this.store.a++; } @action decA = () => { this.store.a–; }}// ③实例化单一数据源const store = new Store();// ④实例化 actions,并且和 store 进行关联const actions = new Actions({store});// inject 向业务组件注入 store,actions,和 Provider 配合使用// ⑤ 使用 inject decorator 和 observer decorator@inject(‘store’, ‘actions’)@observerclass Demo extends Component { render() { const { store, actions } = this.props; return ( <div> <p>a = {store.a}</p> <p> <button className=“ui-btn” onClick={actions.incA}>增加 a</button> <button className=“ui-btn” onClick={actions.decA}>减少 a</button> </p> </div> ); }}class App extends Component { render() { // ⑥使用Provider 在被 inject 的子组件里,可以通过 props.store props.actions 访问 return ( <Provider store={store} actions={actions}> <Demo /> </Provider> ) }}export default App;比较一下:Redux 数据流流动很自然,可以充分利用时间回溯的特征,增强业务的可预测性;MobX 没有那么自然的数据流动,也没有时间回溯的能力,但是 View 更新很精确,粒度控制很细。Redux 通过引入一些中间件来处理副作用;MobX 没有中间件,副作用的处理比较自由,比如依靠 autorunAsync 之类的方法。Redux 的样板代码更多,看起来就像是我们要做顿饭,需要先买个调料盒装调料,再买个架子放刀叉。。。做一大堆准备工作,然后才开始炒菜;而 MobX 基本没啥多余代码,直接硬来,拿着炊具调料就开干,搞出来为止。但其实 Redux 和 MobX 并没有孰优孰劣,Redux 比 Mobx 更多的样板代码,是因为特定的设计约束。如果项目比较小的话,使用 MobX 会比较灵活,但是大型项目,像 MobX 这样没有约束,没有最佳实践的方式,会造成代码很难维护,各有利弊。一般来说,小项目建议 MobX 就够了,大项目还是用 Redux 比较合适。总结时光荏苒,岁月如梭。每一个框架或者库只能陪你走一段路,最终都会逝去。留在你心中的,不是一条一条的语法规则,而是一个一个的思想,这些思想才是推动进步的源泉。帅哥美女,如果你都看到这里了,那么不点个赞,你的良心过得去么?参考链接https://cn.vuejs.org/v2/guide/state-management.htmlhttps://vuex.vuejs.org/https://cn.redux.js.org/docs/react-redux/http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.htmlhttp://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.htmlhttps://redux-saga-in-chinese.js.orghttps://juejin.im/post/59e6cd68f265da43163c2821https://react-redux.js.org/introduction/why-use-react-reduxhttps://segmentfault.com/a/1190000007248878http://es6.ruanyifeng.com/#docs/generatorhttps://juejin.im/post/5ac1cb9d6fb9a028cf32a046https://zhuanlan.zhihu.com/p/35437092https://github.com/dvajs/dva/issues/1https://cn.mobx.js.orghttps://zhuanlan.zhihu.com/p/25585910http://imweb.io/topic/59f4833db72024f03c7f49b4 ...

December 18, 2018 · 11 min · jiezi

揭开redux,react-redux的神秘面纱

16年开始使用react-redux,迄今也已两年多。这时候再来阅读和读懂redux/react-redux源码,虽已没有当初的新鲜感,但依然觉得略有收获。把要点简单写下来,一方面供感兴趣的读者参考,另一方面也是自己做下总结。reduxreact-redux最核心的内容就是redux。内带redux,react-redux只提供了几个API来关联redux与react的组件以及react state的更新。首先,看下如何使用redux。 redux老司机可以直接滑动滚轮至下一章。 简单来说,redux有三个概念,action, reducer 和 dispatch。 action和dispatch比较好理解:动作指令和提交动作指令方法。而reducer,个人在字面上没有理解,但抽象层面上可以理解为用来生成state的函数。用一个简单案例体现这三个概念:// actionconst INCREMENT = { type: ‘INCREMENT’ }// reducerfunction count( state = 0, action ) { switch( action.type ) { case ‘INCREMENT’: return state + 1 default: return state }}// dispatch// 此处开始使用reduxconst store = redux.createStore( count )console.log( store.getState() ) // 0store.dispatch( INCREMENT )console.log( store.getState() ) // 1接下来说说redux中的两大模块:store对象中间件store对象APIcreateStore会创建了一个store对象,创建的过程中它主要做了下面两件事:初始化state暴露相关接口:getState(), dispatch( action ), subscribe( listener )等。其中getState()用来获取store中的实时state, dispatch(action)根据传入的action更新state, subscribe( listener)可以监听state的变化。中间件中间件可以用来debug或提交异步动作指令. 在初始化store的时候,我们通过createStore( reducer, state, applyMiddleware( middleware1, middleware2 ) )添加多个中间件。 为了实现多个中间件,redux专门引入了函数式编程的compose()方法,简单来说,compose将多层函数调用的写法变得优雅:// 未使用compose方法a( b( c( ’d’ ) ) )// 用compose方法compose( a, b, c )(’d’)而中间件的写法比较奇特,是多级函数,在阅读源码的时候有点绕。显然中间件的写法还可以优化,尽管现在的写法方便在源码中使用,但对redux用户来说稍显复杂,可以用单层函数。function logMiddleware({ getState }) { return nextDispatch => action => { console.log( ‘before dispatch’, getState() ) const res = nextDispatch( action ) console.log( ‘after dispatch’, getState() ) return res }}react-redux了解了redux运作原理,就可以知道react-redux的大部分使用场景是如何运作。react-redux提供了几个API将redux与react相互关联。基于上一个案例展示react-redux的用法:// actionconst increment = () => ({ type: ‘INCREMENT’ })// reducerfunction count( state = 0, action ) { switch( action.type ) { case ‘INCREMENT’: return state + 1 default: return state }}// reduxconst store = Redux.createStore( count )// react-reduxconst { Provider, connect } = ReactReduxconst mapStateToProps = state => ( { count: state } )const mapDispatchToProps = dispatch => ( { increment : () => dispatch( increment() ) } )const App = connect( mapStateToProps, mapDispatchToProps )( class extends React.Component { onClick = () => { this.props.increment() } render() { return <div> <p>Count: { this.props.count }</p> <button onClick={ this.onClick }>+</button> </div> }} )ReactDOM.render( <Provider store={ store }> <App /></Provider>, document.getElementById( ‘app’ ) )点击运行案例react-redux提供最常用的两个API是:ProviderconnectProviderProvider本质上是一个react组件,通过react的context api(使一个组件可以跨多级组件传递props)挂载redux store中的state,并且当组件初始化后开始监听state。当监听到state改变,Provider会重新setState在context上的storeState,简要实现代码如下:class Provider extends Component { constructor(props) { super(props) const { store } = props this.state = { storeState: Redux.store.getState(), } } componentDidMount() { this.subscribe() } subscribe() { const { store } = this.props store.subscribe(() => { const newStoreState = store.getState() this.setState(providerState => { return { storeState: newStoreState } }) }) } render() { const Context = React.createContext(null) <Context.Provider value={this.state}> {this.props.children} </Context.Provider> }}connect()connect方法通过connectHOC(HOC: react高阶组件)将部分或所有state以及提交动作指令方法赋值给react组件的props。小结写react不用redux就像写代码不用git, 我们需要用redux来更好地管理react应用中的state。了解redux/react-redux的运作原理会消除我们在使用redux开发时的未知和疑惑,并且在脑中有一个完整的代码执行回路,让开发流程变得透明,直观。 如果本文帮助到了你,我也十分荣幸, 欢迎点赞和收藏。如果有任何疑问或者建议,都欢迎在下方评论区提出。 ...

December 18, 2018 · 2 min · jiezi

Redux and Router

Part01 What’s the problem这段代码意图是把router传递props的路由信息再传递给redux。有这么几个问题:如果靠组件生命周期转发 每个路由下面的顶级组件都要调这样一个action并且,如果路由有参数改变(很多时候页面状态的参数会在路由中体现),这段代码是无法检测的,还需要在componentWillReceiveProps里去处理逻辑。还有这个setTimeout解决异步问题,极度不优雅。Can’t cooperateredux 是状态管理的库,router 是(唯一)控制页面跳转的库。两者都很美好,但是不美好的是两者无法协同工作。换句话说,当路由变化以后,store 无法感知到。redux是想把绝大多数应用程序的状态都保存在单一的store里,而当前的路由状态明显是应用程序状态很重要的一部分,应当是要保存在store中的。目前是,如果直接使用react router,就意味着所有路由相关的信息脱离了Redux store的控制,假借组件接受router信息转发dispatch的方法属于反模式,违背了redux的设计思想,也给我们应用程序带来了更多的不确定性。Part02 What do we need我们需要一个这样的路由系统,他技能利用React Router的声明式特性,又能将路由信息整合进Redux Store中。react-router-reduxreact-router-redux 是 redux 的一个中间件(中间件:JavaScript 代理模式的另一种实践 针对 dispatch 实现了方法的代理,在 dispatch action 的时候增加或者修改) ,主要作用是:加强了React Router库中history这个实例,以允许将history中接受到的变化反应到state中去。Part03 How to useimport React from ‘react’import ReactDOM from ‘react-dom’import { createStore, combineReducers } from ‘redux’import { Provider } from ‘react-redux’import { Router, Route, browserHistory } from ‘react-router’import { syncHistoryWithStore, routerReducer } from ‘react-router-redux’import reducers from ‘<project-path>/reducers’const store = createStore( combineReducers({ …reducers, routing: routerReducer }))const history = syncHistoryWithStore(browserHistory, store)ReactDOM.render( <Provider store={store}> <Router history={history}> <Route path="/" component={App} /> </Router> </Provider>, document.getElementById(‘app’))使用简单直白的api syncHistoryWithStore来完成redux的绑定工作,我们只需要传入react router中的history(前面提到的)以及redux中的store,就可以获得一个增强后的history对象。将这个history对象传给react router中的Router组件作为props,就给应用提供了观察路由变化并改变store的能力。现在,只要您按下浏览器按钮或在应用程序代码中导航,导航就会首先通过Redux存储区传递新位置,然后再传递到React Router以更新组件树。如果您计时旅行,它还会将新状态传递给React Router以再次更新组件树。如何访问容器组件中的路由器状态?React Router 通过路径组件的props提供路由信息。这使得从容器组件访问它们变得容易。当使用react-redux对connect()你的组件进行陈述时,你可以从第二个参数mapStateToProps访问路由器的道具:Part04 Code principlehttps://github.com/reactjs/react-router-redux// index.js/** * 作为外部 syncHistoryWithStore * 绑定store.dispatch方法引起的state中路由状态变更到影响浏览器location变更 * 绑定浏览器location变更触发store.dispatch,更新state中路由状态 * 返回当前的histroy、绑定方法listen(dispatch方法触发时执行,以绑定前的路由状态为参数)、解绑函数unsubscribe /export syncHistoryWithStore from ‘./sync’/* * routerReducer监听路由变更子reducer,通过redux的combineReducers复合多个reducer后使用 /export { LOCATION_CHANGE, routerReducer } from ‘./reducer’/* * 构建actionCreater,作为外部push、replace、go、goBack、goForward方法的接口,通常不直接使用 /export { CALL_HISTORY_METHOD, push, replace, go, goBack, goForward, routerActions} from ‘./actions’/* * 构建route中间件,用于分发action,触发路径跳转等事件 /export routerMiddleware from ‘./middleware’// sync.jsimport { LOCATION_CHANGE } from ‘./reducer’// 默认用state.routing存取route变更状态数据 const defaultSelectLocationState = state => state.routing/* * 作为外部syncHistoryWithStore接口方法 * 绑定store.dispatch方法引起的state中路由状态变更到影响浏览器location变更 * 绑定浏览器location变更触发store.dispatch,更新state中路由状态 * 返回当前的histroy、绑定方法listen(dispatch方法触发时执行,以绑定前的路由状态为参数)、解绑函数unsubscribe /export default function syncHistoryWithStore(history, store, { // 约定redux.store.state中哪个属性用于存取route变更状态数据 selectLocationState = defaultSelectLocationState, // store中路由状态变更是否引起浏览器location改变 adjustUrlOnReplay = true} = {}) { // Ensure that the reducer is mounted on the store and functioning properly. // 确保redux.store.state中某个属性绑定了route变更状态 if (typeof selectLocationState(store.getState()) === ‘undefined’) { throw new Error( ‘Expected the routing state to be available either as state.routing ’ + ‘or as the custom expression you can specify as selectLocationState ’ + ‘in the syncHistoryWithStore() options. ’ + ‘Ensure you have added the routerReducer to your store's ’ + ‘reducers via combineReducers or whatever method you use to isolate ’ + ‘your reducers.’ ) } let initialLocation // 初始化route状态数据 let isTimeTraveling // 浏览器页面location.url改变过程中标识,区别页面链接及react-router-redux变更location两种情况 let unsubscribeFromStore // 移除store.listeners中,因路由状态引起浏览器location变更函数 let unsubscribeFromHistory // 移除location变更引起路由状态更新函数 let currentLocation // 记录上一个当前路由状态数据 // 获取路由事件触发后路由状态,或者useInitialIfEmpty为真值获取初始化route状态,或者undefined const getLocationInStore = (useInitialIfEmpty) => { const locationState = selectLocationState(store.getState()) return locationState.locationBeforeTransitions || (useInitialIfEmpty ? initialLocation : undefined) } // 初始化route状态数据 initialLocation = getLocationInStore() // If the store is replayed, update the URL in the browser to match. // adjustUrlOnReplay为真值时,store数据改变事件dispatch发生后,浏览器页面更新location if (adjustUrlOnReplay) { // 由store中路由状态改变情况,更新浏览器location const handleStoreChange = () => { // 获取路由事件触发后路由状态,或者初始路由状态 const locationInStore = getLocationInStore(true) if (currentLocation === locationInStore || initialLocation === locationInStore) { return } // 浏览器页面location.url改变过程中标识,区别页面链接及react-router-redux变更location两种情况 isTimeTraveling = true // 记录上一个当前路由状态数据 currentLocation = locationInStore // store数据改变后,浏览器页面更新location history.transitionTo({ …locationInStore, action: ‘PUSH’ }) isTimeTraveling = false } // 绑定事件,完成功能为,dispatch方法触发store中路由状态改变时,更新浏览器location unsubscribeFromStore = store.subscribe(handleStoreChange) // 初始化设置路由状态时引起页面location改变 handleStoreChange() } // 页面链接变更浏览器location,触发store.dispatch变更store中路由状态 const handleLocationChange = (location) => { // react-router-redux引起浏览器location变更过程中,无效;页面链接变更,有效 if (isTimeTraveling) { return } // Remember where we are currentLocation = location // Are we being called for the first time? if (!initialLocation) { // Remember as a fallback in case state is reset initialLocation = location // Respect persisted location, if any if (getLocationInStore()) { return } } // Tell the store to update by dispatching an action store.dispatch({ type: LOCATION_CHANGE, payload: location }) } // hashHistory、boswerHistory监听浏览器location变更,触发store.dispatch变更store中路由状态 unsubscribeFromHistory = history.listen(handleLocationChange) // History 3.x doesn’t call listen synchronously, so fire the initial location change ourselves // 初始化更新store中路由状态 if (history.getCurrentLocation) { handleLocationChange(history.getCurrentLocation()) } // The enhanced history uses store as source of truth return { …history, // store中dispatch方法触发时,绑定执行函数listener,以绑定前的路由状态为参数 listen(listener) { // Copy of last location. // 绑定前的路由状态 let lastPublishedLocation = getLocationInStore(true) // Keep track of whether we unsubscribed, as Redux store // only applies changes in subscriptions on next dispatch let unsubscribed = false // 确保listener在解绑后不执行 const unsubscribeFromStore = store.subscribe(() => { const currentLocation = getLocationInStore(true) if (currentLocation === lastPublishedLocation) { return } lastPublishedLocation = currentLocation if (!unsubscribed) { listener(lastPublishedLocation) } }) // History 2.x listeners expect a synchronous call. Make the first call to the // listener after subscribing to the store, in case the listener causes a // location change (e.g. when it redirects) if (!history.getCurrentLocation) { listener(lastPublishedLocation) } // Let user unsubscribe later return () => { unsubscribed = true unsubscribeFromStore() } }, // 解绑函数,包括location到store的handleLocationChange、store到location的handleStoreChange unsubscribe() { if (adjustUrlOnReplay) { unsubscribeFromStore() } unsubscribeFromHistory() } }}// reducer.js /* * This action type will be dispatched when your history * receives a location change. /export const LOCATION_CHANGE = ‘@@router/LOCATION_CHANGE’const initialState = { locationBeforeTransitions: null}/* * 监听路由变更子reducer,通过redux的combineReducers复合多个reducer后使用,作为外部routerReducer接口 * 提示redux使用过程中,可通过子组件模块中注入reducer,再使用combineReducers复合多个reducer * 最后使用replaceReducer方法更新当前store的reducer,意义是构建reducer拆解到各个子模块中 * / export function routerReducer(state = initialState, { type, payload } = {}) { if (type === LOCATION_CHANGE) { return { …state, locationBeforeTransitions: payload } } return state}// actions.jsexport const CALL_HISTORY_METHOD = ‘@@router/CALL_HISTORY_METHOD’function updateLocation(method) { return (…args) => ({ type: CALL_HISTORY_METHOD, // route事件标识,避免和用于定义的action冲突 payload: { method, args } // method系hashHistroy、boswerHistroy对外接口方法名,args为参数 })}/* * 返回actionCreater,作为外部push、replace、go、goBack、goForward方法的接口,通常不直接使用 */export const push = updateLocation(‘push’)export const replace = updateLocation(‘replace’)export const go = updateLocation(‘go’)export const goBack = updateLocation(‘goBack’)export const goForward = updateLocation(‘goForward’)export const routerActions = { push, replace, go, goBack, goForward }完结(此文由PPT摘抄完成)PPT链接 ...

December 7, 2018 · 4 min · jiezi

React Hooks实现异步请求实例—useReducer、useContext和useEffect代替Redux方案

本文是学习了2018年新鲜出炉的React Hooks提案之后,针对异步请求数据写的一个案例。注意,本文假设了:1.你已经初步了解hooks的含义了,如果不了解还请移步官方文档。(其实有过翻译的想法,不过印记中文一直在翻译,就是比较慢啦)2.你使用Redux实现过异步Action(非必需,只是本文不涉及该部分知识而直接使用)3.你听说过axios或者fetch(如果没有,那么想象一下原生js的promise实现异步请求,或者去学习下这俩库)全部代码参见仓库: github | Marckon选择hooks-onlineShop分支以及master分支查看❗ 本文并非最佳实践,如有更好的方法或发现文中纰漏,欢迎指正!前序方案(不想看可以直接跳过)不考虑引入Redux通过学习React生命周期,我们知道适合进行异步请求的地方是componentDidMount钩子函数内。因此,当你不需要考虑状态管理时,以往的方法很简单:class App extends React.Component{ componentDidMount(){ axios.get(’/your/api’) .then(res=>/…/) }}引入Redux进行状态管理当你决定使用Redux进行状态管理时,比如将异步获取到的数据储存在store中,事情就开始复杂起来了。根据Redux的官方文档案例来看,为了实现异步action,你还得需要一个类似于redux-thunk的第三方库来解析你的异步action。requestAction.js: 定义异步请求action的地方//这是一个异步action,分发了两个同步action,redux-thunk能够理解它const fetchGoodsList = url => dispatch => { dispatch(requestGoodsList()); axios.get(url) .then(res=>{ dispatch(receiveGoodsList(res.data)) })};requestReducer.js: 处理同步actionconst requestReducer=(state=initialState,action)=>{ switch (action.type) { case REQUEST_GOODSLIST: return Object.assign({},state,{ isFetching: true }); case RECEIVE_GOODSLIST: return Object.assign({},state,{ isFetching:false, goodsList:action.goodsList }); default: return state; }};App Component :你引入redux store和redux-thunk中间件的地方import {Provider} from ‘react-redux’;import thunkMiddleWare from ‘redux-thunk’;import {createStore,applyMiddleware} from ‘redux’;//other importslet store=createStore( rootReducer, //这里要使用中间件,才能够完成异步请求 applyMiddleware( thunkMiddleWare, myMiddleWare, ));class App extends React.Component{ render(){ return ( <Provider store={store}> <RootComponent/> </Provider> ) }}GoodsList Component :需要进行异步请求的组件class GoodsList extends React.Component{ //… componentDidMount(){ this.props.fetchGoodsList(‘your/url’); } //…}const mapDispatchToProps={ fetchGoodsList}export default connect( mapStateToProps, mapDispatchToProps)(GoodsList);完整代码:branch:master-onlineShop使用Hooks-useReducer()和useContext()总之使用Redux很累,当然,你可以不使用Redux,直接通过props层层传递,或者使用context都可以。只不过本文我们学过了useReducer,使用到了Redux的思想,总要试着用一下。这里你不需要引入别的任何第三方库了,简简单单地使用React@16.7.0-alpha.2版本就好啦很重要的一点就是——函数式组件,现在React推荐我们这么做,可以基本上代替class写法。函数签名useReducer(reducer,initialState)useContext(ctxObj)useEffect(effectFunction,[dependencyValues])概览-你需要编写什么action.js:我们还使用redux的思想,编写actionreducer.js:处理action,不同于redux的reducer,这里我们可以不用提供初始状态根组件:Provider提供给子组件contextuseReducer定义的位置,引入一个reducer并且提供初始状态initialState子组件:useContext定义的位置,获取祖先组件提供的contextuseEffect用于进行异步请求实现1.action.js:我们使用action创建函数const REQUEST_GOODSLIST = “REQUEST_GOODSLIST”;const RECEIVE_GOODSLIST = “RECEIVE_GOODSLIST”;//开始请求const requestGoodsList = () => ({ type: REQUEST_GOODSLIST});//接收到数据const receiveGoodsList = json => ({ type: RECEIVE_GOODSLIST, goodsList: json.goodsList, receivedAt: Date.now()});export { RECEIVE_GOODSLIST, REQUEST_GOODSLIST, receiveGoodsList, requestGoodsList,}2.reducer.js:判断action的类型并进行相应处理,更新stateimport { RECEIVE_GOODSLIST, REQUEST_GOODSLIST,} from “../..";export const fetchReducer=(state,action)=>{ switch (action.type) { case REQUEST_GOODSLIST: return Object.assign({},state,{ isFetching: true }); case RECEIVE_GOODSLIST: return Object.assign({},state,{ isFetching:false, goodsList:state.goodsList.concat(action.goodsList) }); default: return state; }};3.根组件:引入reducer.jsimport React,{useReducer} from ‘react’;import {fetchReducer} from ‘..’;//创建并export上下文export const FetchesContext = React.createContext(null);function RootComponent() { //第二个参数为state的初始状态 const [fetchesState, fetchDispatch] = useReducer(fetchReducer, { isFetching: false, goodsList: [] }); return ( //将dispatch方法和状态都作为context传递给子组件 <FetchesContext.Provider value={{fetchesState,dispatch:fetchDispatch}}> //… //用到context的一个子组件 <ComponentToUseContext/> </FetchesContext.Provider> )}4.子组件:引入FetchesContextimport {FetchesContext} from “../RootComponent”;import React, {useContext, useEffect,useState} from ‘react’;import axios from ‘axios’;function GoodsList() { //获取上下文 const ctx = useContext(FetchesContext); //一个判断是否重新获取的state变量 const [reFetch,setReFetch]=useState(false); //具有异步调用副作用的useEffect useEffect(() => { //首先分发一个开始异步获取数据的action ctx.dispatch(requestGoodsList()); axios.get(proxyGoodsListAPI()) .then(res=>{ //获取到数据后分发一个action,通知reducer更新状态 ctx.dispatch(receiveGoodsList(res.data)) }) //第二个参数reFetch指的是只有当reFetch变量值改变才重新渲染 },[reFetch]); return ( <div onScroll={handleScroll}> { //children } </div> )}完整代码参见:branch:hooks-onlineShop目录结构我的目录结构大概这样:src |- actions |- fetchAction.js |- components |-… |- reducers |- fetchReducer.js |- index.js注意点使用useContext()时候我们不需要使用Consumer了。但不要忘记export和import上下文对象useEffect()可以看做是class写法的componentDidMount、componentDidUpdate以及componentWillUnMount三个钩子函数的组合。当返回了一个函数的时候,这个函数就在compnentWillUnMount生命周期调用默认地,传给useEffect的第一个参数会在每次(包含第一次)数据更新时重新调用当给useEffect()传入了第二个参数(数组类型)的时候,effect函数会在第一次渲染时调用,其余仅当数组中的任一元素发生改变时才会调用。这相当于我们控制了组件的update生命周期useEffect()第二个数组为空则意味着仅在componentDidMount周期执行一次代码仓库里使用了Mock.js拦截api请求以及ant-design第三UI方库。目前代码比较简陋。 ...

November 30, 2018 · 2 min · jiezi

简洁的 React 状态管理库 - Stamen

说到 React 状态管理,必提的肯定是 Redux 与 MobX,2018 年快过去了,它们依然是最火热的状态管理工具,也有一些基于 Redux 的,如 dva、rematch 等,也有新的,如 mobx-state-tree,这里不对各个解决方案作评价。但还是想吐槽:什么 provider, connections, actions, reducers, effects, dispatch, put, call, payload, @observable, @computed, @observer, @inject…一堆模板代码、各种概念、什么哲学原则… 还有各种多如牛毛的 Api。我只是想早点码完页面下班,早点下班健身、陪妹子…所以,我想要这样的一个状态管理库:轻量 个人做移动端开发比较多简洁 没模板代码, 尽量少的 Api符合直觉 没复杂的概念, 给个 action 改 state 就好清晰 更易写出可维护和可读性好的代码高效 更高的开发效率,这很重要Typescript state 和 action 高度支持智能提示我是个实用主义者,开发效率、代码可维护性和可读性、开发体验大于各种什么范式、各种理论,也不需要装纯,重要的是可以快速处理业务,产生价值,早点下班打王者。有一天,我看到了 mobx 作者的 immer, 我感觉使用 immer, 可以实现一个我理想中的状态管理工具,所以就造了一个轮子,叫 stamen, 他有什么特点呢,Show you the code: stamen。如果有什么核心特点的话,那应该是 “简洁”,这里指的是使用者写代码时简洁,可以专注于业务,而不是自身源代码简洁,把问题留给使用者。CodeSandbox上的例子: Basic | Async用法比较简单:import React from ‘react’;import { render } from ‘react-dom’;import { createStore } from ‘stamen’;const { consume, mutate } = createStore({ count: 1 });const App = () => ( <div> <span>{consume(state => state.count)}</span> <button onClick={() => mutate(state => state.count–)}>-</button> <button onClick={() => mutate(state => state.count++)}>+</button> </div>);render(<App />, document.getElementById(‘root’));只有 state 和 action ,没有其它概念,只有一个 api:const { consume, mutate } = createStore({ count: 1 });Stamen 代码实现只有40行,对于大部分项目来说,这40行代码所包含的功能已然足够。更多用法可以看:Github: https://github.com/forsigner/…文档: http://forsigner.com/stamen-z… ...

October 1, 2018 · 1 min · jiezi

我的源码阅读之路:redux源码剖析

前言用过react的小伙伴对redux其实并不陌生,基本大多数的React应用用到它。一般大家用redux的时候基本都不会单独去使用它,而是配合react-redux一起去使用。刚学习redux的时候很容易弄混淆redux和react-redux,以为他俩是同一个东西。其实不然,redux是javascript应用程序的可预测状态容器,而react-redux则是用来连接这个状态容器与react组件。可能前端新人对这两者还是觉得很抽象,打个比方说,在一个普通家庭中,妈妈在家里都是至高无上的地位,掌握家中经济大权,家里的经济流水都要经过你的妈妈,而你的爸爸则负责从外面赚钱然后交给你的妈妈。这里把你的妈妈类比成redux,而你的爸爸可以类比成react-redux,而外面的大千世界则是react组件。相信这样的类比,大家对这react和react-redux的有了一个初步认识。本篇文章介绍的主要内容是对redux的源码的分析,react-redux的源码分析将会在我的下一篇文章中,敬请期待!各位小伙们如果觉得写的不错的话,麻烦多多点赞收藏关注哦!redux的使用在讲redux的源码之前,我们先回顾一下redux是如何使用的,然后我们再对照着redux的使用去阅读源码,这样大家的印象可能会更加深刻点。先贴上一段demo代码:const initialState={ cash:200,}const reducer=(state=initialState,action)=>{ const {type,payload} = action; switch(type){ case ‘INCREMENT’: return Object.assign({},state,{ cash:state.cash+payload }); case ‘DECREMENT’: return Object.assign({},state,{ cash:state.cash-payload }); default : return state; }}const reducers=Redux.combineReducers({treasury:reducer});//创建小金库const store=Redux.createStore(reducers);//当小金库的现金发生变化时,打印当前的金额store.subscribe(()=>{ console.log(余额:${store.getState().treasury.cash});});//小明爸爸发了工资300块上交store.dispatch({ type:‘INCREMENT’, payload:300});//小明拿着水电费单交100块水电费store.dispatch({ type:‘DECREMENT’, payload:100});上面这段代码是一个非常典型的redux的使用,跟大家平时在项目里用的不太一样,可能有些小伙伴们不能理解,其实react-redux只不过在这种使用方法上做了一层封装。等当我们弄清楚redux的使用,再去看react-redux源码便会明白了我们在项目里为何是那种写法而不是这种写法。说到redux的使用,不免要说一下action、reducer和store三者的关系。记得当初第一次使用redux的时候,一直分不清这三者的关系,感觉这三个很抽象很玄学,相信不少小伙伴们跟我一样遇到过同样的情况。其实并不难,我还是用文章开头打的比方还解释这三者的关系。现在保险箱(store)里存放200块大洋。到月底了,小明的爸爸的单位发了工资总计300块大洋,拿到工资之后第一件的事情就是上交,毫无疑问的,除非小明爸爸不要命了。小明的爸爸可以直接将这300块大洋放到家里的保险箱里面吗?显然是不可以的,所以小明的爸爸得向小明的爸爸提交申请,而这个申请也就是我们所说的action。这个申请(action)包括操作类型和对应的东西,申请类型就是存钱(INCREMENT),对应的东西就是300块大洋(payload)。此时小明的妈妈拿到这个申请之后,将根据这个申请执行对应的操作,这里就是往保险箱里的现金里放300块大洋进去,此时小明的妈妈干的事情就是reducer干的事情。当300块大洋放完之后,小明的妈妈就通知家里的所有人现在的小金库的金额已经发生了变化,现在的余额是500块。当小明的爸爸收到这个通知之后,心的一块大石头也就放下来了。过了一会,小明回来了,并且拿着一张价值100块的水电费的催收单。于是,小明想小明妈妈申请交水电费,小明妈妈从保险库中取出来100块给了小明,并通知了家里所有人小金库的金额又发生了变化,现在余额400块。通过上面的例子,相信小伙们对三者的关系有了一个比较清晰的认识。现在我们已经理清楚了action、reducer和store三者的关系,并且也知道了redux是如何使用的了,现在将开始我们得源码阅读之旅。redux项目结构本篇文章是基于redux的4.0.0版本做的源码分析,小伙伴们在对照源码的时候,千万别弄错了。整个redux项目的源码的阅读我们只需要关注src的目录即可。这里主要分为两大块,一块为自定义的工具库,另一块则是redux的逻辑代码。先从哪块开始阅读呢?我个人建议先阅读自定义的工具库这块。主要有这么两个原因:第一个,这块代码比较简单,容易理解,大家更能进入阅读的状态;第二个,redux逻辑代码会用到这些自定义工具,先搞懂这些,对后续逻辑代码的阅读做了一个很好的铺垫。下面我们正式开始我们的源码阅读之旅。utilsactionTypes.jsconst ActionTypes = { INIT: ‘@@redux/INIT’ + Math.random() .toString(36) .substring(7) .split(’’) .join(’.’), REPLACE: ‘@@redux/REPLACE’ + Math.random() .toString(36) .substring(7) .split(’’) .join(’.’)}export default ActionTypes这段代码很好理解,就是对外暴露两个action类型,没什么难点。但是我这里想介绍的是Number.prototype.toString方法,估计应该有不少小伙伴们不知道toString是可以传参的,toString接收一个参数radix,代表数字的基数,也就是我们所说的2进制、10进制、16进制等等。radix的取值范围也很容易得出来,最小进制就是我们得二进制,所以redix>=2。0-9(10个数字)+a-z(26个英文字母)总共36个,所以redix<=36。总结一下2<=radix<=36,默认是10。基于这个特性我们可以写一个获取指定长度的随机字符串的长度://获取指定长度的随机字符串function randomString(length){ let str=’’; while(length>0){ const fragment= Math.random().toString(36).substring(2); if(length>fragment.length){ str+=fragment; length-=fragment.length; }else{ str+=fragment.substring(0,length); length=0; } } return str;}isPlainObject.jsexport default function isPlainObject(obj) { if (typeof obj !== ‘object’ || obj === null) return false let proto = obj while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto) } return Object.getPrototypeOf(obj) === proto}isPlainObject.js也很简单,仅仅只是向外暴露了一个用于判断是否简单对象的函数。什么简单对象?应该有一些小伙伴不理解,所谓的简单对象就是该对象的__proto__等于Object.prototype,用一句通俗易懂的话就是:凡不是new Object()或者字面量的方式构建出来的对象都不是简单对象下面看一个例子:class Fruit{ sayName(){ console.log(this.name) }}class Apple extends Fruit{ constructor(){ super(); this.name=“苹果” }}const apple = new Apple();const fruit = new Fruit();const cherry = new Object({ name:‘樱桃’});const banana = { name:‘香蕉’};console.log(isPlainObject(apple));//falseconsole.log(isPlainObject(fruit));//falseconsole.log(isPlainObject(cherry));//trueconsole.log(isPlainObject(banana));//true这里可能会有人不理解isPlainObject(fruit)===false,如果对这个不能理解的话,自己后面要补习一下原型链的相关知识,这里fruit.proto.__proto__才等价于Object.prototype。warning.jsexport default function warning(message) { if (typeof console !== ‘undefined’ && typeof console.error === ‘function’) { console.error(message) } try { throw new Error(message) } catch (e) {} }这个也很简单,仅仅是打印一下错误信息。不过这里它的console居然加了一层判断,我查阅了一下发现console其实是有兼容性问题,ie8及其以下都是不支持console的。哎,不仅感叹一句!如果说马赛克阻碍了人类文明的进程,那ie便是阻碍了前端技术的发展。逻辑代码到这里我已经完成对utils下的js分析,很简单,并没有大家想象的那么难。仅仅从这几个简单的js中,就牵引出好几个我们平时不太关注的知识点。如果我们不读这些源码,这些容易被忽视的知识点就很难被捡起来,这也是为什么很多大佬建议阅读源码的原因。我个人认为,阅读源码,理解原理是次要的。学习大佬的代码风格、一些解决思路以及对自己知识盲点的点亮更为重要。废话不多说,开始我们下一个部分的代码阅读,下面的部分就是整个redux的核心部分。index.jsimport createStore from ‘./createStore’import combineReducers from ‘./combineReducers’import bindActionCreators from ‘./bindActionCreators’import applyMiddleware from ‘./applyMiddleware’import compose from ‘./compose’import warning from ‘./utils/warning’import __DO_NOT_USE__ActionTypes from ‘./utils/actionTypes’function isCrushed() {}if ( process.env.NODE_ENV !== ‘production’ && typeof isCrushed.name === ‘string’ && isCrushed.name !== ‘isCrushed’) { warning( “You are currently using minified code outside of NODE_ENV === ‘production’. " + ‘This means that you are running a slower development build of Redux. ’ + ‘You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ’ + ‘or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ’ + ’to ensure you have the correct code for your production build.’ )}export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes}index.js是整个redux的入口文件,尾部的export出来的方法是不是都很熟悉,每个方法对应了一个js,这也是后面我们要分析的。这个有两个点需要讲一下:第一个,__DO_NOT_USE__ActionTypes。 这个很陌生,平时在项目里面我们是不太会用到的,redux的官方文档也没有提到这个,如果你不看源码你可能就不知道这个东西的存在。这个干嘛的呢?我们一点一点往上找,找到这么一行代码:import DO_NOT_USE__ActionTypes from ‘./utils/actionTypes’这个引入的js不就是我们之前分析的utils的其中一员吗?里面定义了redux自带的action的类型,从这个变量的命名来看,这是帮助开发者检查不要使用redux自带的action的类型,以防出现错误。第二个,函数isCrushed。 这里面定义了一个函数isCrushed,但是函数体里面并没有东西。第一次看的时候很奇怪,为啥要这么干?相信有不少小伙伴们跟我有一样的疑问,继续往下看,紧跟着后面有一段代码:if ( process.env.NODE_ENV !== ‘production’ && typeof isCrushed.name === ‘string’ && isCrushed.name !== ‘isCrushed’) { warning( “You are currently using minified code outside of NODE_ENV === ‘production’. " + ‘This means that you are running a slower development build of Redux. ’ + ‘You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ’ + ‘or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ’ + ’to ensure you have the correct code for your production build.’ )}看到process.env.NODE_ENV,这里就要跟我们打包时用的环境变量联系起来。当process.env.NODE_ENV===‘production’这句话直接不成立,所以warning也就不会执行;当process.env.NODE_ENV!==‘production’,比如是我们的开发环境,我们不压缩代码的时候typeof isCrushed.name === ‘string’ && isCrushed.name !== ‘isCrushed’也不会成立;当process.env.NODE_ENV!==‘production’,同样是我们的开发环境,我们进行了代码压缩,此时isCrushed.name === ‘string’ && isCrushed.name !== ‘isCrushed’就成立了,可能有人不理解isCrushed函数不是在的吗?为啥这句话就不成立了呢?其实很好理解,了解过代码压缩的原理的人都知道,函数isCrushed的函数名将会被一个字母所替代,这里我们举个例子,我将redux项目的在development环境下进行了一次压缩打包。代码做了这么一层转换:未压缩function isCrushed() {}if ( process.env.NODE_ENV !== ‘production’ && typeof isCrushed.name === ‘string’ && isCrushed.name !== ‘isCrushed’)压缩后function d(){}“string”==typeof d.name&&“isCrushed”!==d.name此时判断条件就成立了,错误信息就会打印出来。这个主要作用就是防止开发者在开发环境下对代码进行压缩。开发环境下压缩代码,不仅让我们createStore.js函数createStore接受三个参数(reducer、preloadedState、enhancer),reducer和enhancer我们用的比较多,preloadedState用的比较少。第一个reducer很好理解,这里就不过多解释了,第二个preloadedState,它代表着初始状态,我们平时在项目里也很少用到它,主要说一下enhancer,中文名叫增强器,顾名思义就是来增强redux的,它的类型的是Function,createStore.js里有这么一行代码: if (typeof enhancer !== ‘undefined’) { if (typeof enhancer !== ‘function’) { throw new Error(‘Expected the enhancer to be a function.’) } return enhancer(createStore)(reducer, preloadedState) }这行代码展示了enhancer的调用过程,根据这个调用过程我们可以推导出enhancer的函数体的架子应该是这样子的: function enhancer(createStore) { return (reducer,preloadedState) => { //逻辑代码 ……. } }常见的enhancer就是redux-thunk以及redux-saga,一般都会配合applyMiddleware一起使用,而applyMiddleware的作用就是将这些enhancer格式化成符合redux要求的enhancer。具体applyMiddleware实现,下面我们将会讲到。我们先看redux-thunk的使用的例子:import { createStore, applyMiddleware } from ‘redux’;import thunk from ‘redux-thunk’;import rootReducer from ‘./reducers/index’;const store = createStore( rootReducer, applyMiddleware(thunk));看完上面的代码,可能会有人有这么一个疑问“createStore函数第二个参数不是preloadedState吗?这样不会报错吗?” 首先肯定不会报错,毕竟官方给的例子,不然写个错误的例子也太大跌眼镜了吧!redux肯定是做了这么一层转换,我在createStore.js找到了这么一行代码: if (typeof preloadedState === ‘function’ && typeof enhancer === ‘undefined’) { enhancer = preloadedState preloadedState = undefined }当第二个参数preloadedState的类型是Function的时候,并且第三个参数enhancer未定义的时候,此时preloadedState将会被赋值给enhancer,preloadedState会替代enhancer变成undefined的。有了这么一层转换之后,我们就可以大胆地第二个参数传enhancer了。说完createStore的参数,下面我说一下函数createStore执行完之后返回的对象都有什么?在createStore.js最下面一行有这一行代码:return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }他返回了有这么几个方法,其中前三个最为常用,后面两个在项目基本上不怎么用,接下来我们去一一剖析。定义的一些变量let currentState = preloadedState //从函数createStore第二个参数preloadedState获得let currentReducer = reducer //从函数createStore第一个参数reducer获得let currentListeners = [] //当前订阅者列表let nextListeners = currentListeners //新的订阅者列表let isDispatching = false其中变量isDispatching,作为锁来用,我们redux是一个统一管理状态容器,它要保证数据的一致性,所以同一个时间里,只能做一次数据修改,如果两个action同时触发reducer对同一数据的修改,那么将会带来巨大的灾难。所以变量isDispatching就是为了防止这一点而存在的。dispatchfunction dispatch(action) { if (!isPlainObject(action)) { throw new Error( ‘Actions must be plain objects. ’ + ‘Use custom middleware for async actions.’ ) } if (typeof action.type === ‘undefined’) { throw new Error( ‘Actions may not have an undefined “type” property. ’ + ‘Have you misspelled a constant?’ ) } if (isDispatching) { throw new Error(‘Reducers may not dispatch actions.’) } try { isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action }函数dispatch在函数体一开始就进行了三次条件判断,分别是以下三个:判断action是否为简单对象判断action.type是否存在判断当前是否有执行其他的reducer操作当前三个预置条件判断都成立时,才会执行后续操作,否则抛出异常。在执行reducer的操作的时候用到了try-finally,可能大家平时try-catch用的比较多,这个用到的还是比较少。执行前isDispatching设置为true,阻止后续的action进来触发reducer操作,得到的state值赋值给currentState,完成之后再finally里将isDispatching再改为false,允许后续的action进来触发reducer操作。接着一一通知订阅者做数据更新,不传入任何参数。最后返回当前的action。getStatefunction getState() { if (isDispatching) { throw new Error( ‘You may not call store.getState() while the reducer is executing. ’ + ‘The reducer has already received the state as an argument. ’ + ‘Pass it down from the top reducer instead of reading it from the store.’ ) } return currentState }getState相比较dispatch要简单许多,返回currentState即可,而这个currentState在每次dispatch得时候都会得到响应的更新。同样是为了保证数据的一致性,当在reducer操作的时候,是不可以读取当前的state值的。说到这里,我想到之前一次的面试经历:面试官:执行createStore函数生成的store,可不可以直接修改它的state?我:可以。(普罗大众的第一反应)面试官:你知道redux怎么做到不能修改store的state吗?我:额……(处于懵逼状态)面试官:很简单啊!重写store的set方法啊!那会没看过redux的源码,就被他忽悠了!读完redux源码之后,靠!这家伙就是个骗子!自己没读过源码还跟我聊源码,无语了!当然,我自己也有原因,学艺不精,被忽悠了。我们这里看了源码之后,getState函数返回state的时候,并没有对currentState做一层拷贝再给我们,所以是可以直接修改的。只是这么修改的话,就不会通知订阅者做数据更新。得出的结论是:store通过getState得出的state是可以直接被更改的,但是redux不允许这么做,因为这样不会通知订阅者更新数据。subscribefunction subscribe(listener) { if (typeof listener !== ‘function’) { throw new Error(‘Expected the listener to be a function.’) } if (isDispatching) { throw new Error( ‘You may not call store.subscribe() while the reducer is executing. ’ + ‘If you would like to be notified after the store has been updated, subscribe from a ’ + ‘component and invoke store.getState() in the callback to access the latest state. ’ + ‘See https://redux.js.org/api-reference/store#subscribe(listener) for more details.’ ) } let isSubscribed = true //表示该订阅者在订阅状态中,true-订阅中,false-取消订阅 ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { if (!isSubscribed) { return } if (isDispatching) { throw new Error( ‘You may not unsubscribe from a store listener while the reducer is executing. ’ + ‘See https://redux.js.org/api-reference/store#subscribe(listener) for more details.’ ) } isSubscribed = false ensureCanMutateNextListeners() const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } }在注册订阅者之前,做了两个条件判断:判断监听者是否为函数是否有reducer正在进行数据修改(保证数据的一致性)接下来执行了函数ensureCanMutateNextListeners,下面我们看一下ensureCanMutateNextListeners函数的具体实现逻辑: function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } }逻辑很简单,判断nextListeners和currentListeners是否为同一个引用,还记得dispatch函数中有这么一句代码以及定义变量时一行代码吗?// Function dispatchconst listeners = (currentListeners = nextListeners)// 定义变量let currentListeners = []let nextListeners = currentListeners这两处将nextListeners和currentListeners引用了同一个数组,另外定义变量时也有这么一句话代码。而ensureCanMutateNextListeners就是用来判断这种情况的,当nextListeners和currentListeners为同一个引用时,则做一层浅拷贝,这里用的就是Array.prototype.slice方法,该方法会返回一个新的数组,这样就可以达到浅拷贝的效果。函数ensureCanMutateNextListeners作为处理之后,将新的订阅者加入nextListeners中,并且返回取消订阅的函数unsubscribe。函数unsubscribe执行时,也会执行两个条件判断:是否已经取消订阅(已取消的不必执行)是否有reducer正在进行数据修改(保证数据的一致性)通过条件判断之后,讲该订阅者从nextListeners中删除。看到这里可能有小伙伴们对currentListeners和nextListeners有这么一个疑问?函数dispatch里面将二者合并成一个引用,为啥这里有啥给他俩分开?直接用currentListeners不可以吗?这里这样做其实也是为了数据的一致性,因为有这么一种的情况存在。当redux在通知所有订阅者的时候,此时又有一个新的订阅者加进来了。如果只用currentListeners的话,当新的订阅者插进来的时候,就会打乱原有的顺序,从而引发一些严重的问题。replaceReducer function replaceReducer(nextReducer) { if (typeof nextReducer !== ‘function’) { throw new Error(‘Expected the nextReducer to be a function.’) } currentReducer = nextReducer dispatch({ type: ActionTypes.REPLACE }) }这个函数是用来替换reducer的,平时项目里基本很难用到,replaceReducer函数执行前会做一个条件判断:判断所传reducer是否为函数通过条件判断之后,将nextReducer赋值给currentReducer,以达到替换reducer效果,并触发state更新操作。observable /** * Interoperability point for observable/reactive libraries. * @returns {observable} A minimal observable of state changes. * For more information, see the observable proposal: * https://github.com/tc39/proposal-observable */这里没贴代码,因为这块代码我们不需要掌握。这个observable函数,并没有调用,即便暴露出来我们也办法使用。所以我们就跳过这块,如果有兴趣的话,可以去作者给的github的地址了解一下。讲完这几个方法之后,还有一个小细节需要说一下,createStore函数体里有这样一行代码。dispatch({ type: ActionTypes.INIT })为啥要有这么一行代码?原因很简单,假设我们没有这样代码,此时currentState就是undefined的,也就我说我们没有默认值了,当我们dispatch一个action的时候,就无法在currentState基础上做更新。所以需要拿到所有reducer默认的state,这样后续的dispatch一个action的时候,才可以更新我们的state。combineReducers.js这个js对应着redux里的combineReducers方法,主要作用就是合并多个reducer。现在我们先给一个空的函数,然后再一步步地根据还原源码,这样大家可能理解得更为透彻点。//reducers Object类型 每个属性对应的值都要是functionexport default function combineReducers(reducers) { ….}第一步:浅拷贝reducersexport default function combineReducers(reducers) { const reducerKeys = Object.keys(reducers) const finalReducers = {} for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] if (process.env.NODE_ENV !== ‘production’) { if (typeof reducers[key] === ‘undefined’) { warning(No reducer provided for key "${key}") } } if (typeof reducers[key] === ‘function’) { finalReducers[key] = reducers[key] } } const finalReducerKeys = Object.keys(finalReducers)}这里定义了一个finalReducers和finalReducerKeys,分别用来拷贝reducers和其属性。先用Object.keys方法拿到reducers所有的属性,然后进行for循环,每一项可根据其属性拿到对应的reducer,并浅拷贝到finalReducers中,但是前提条件是每个reducer的类型必须是Function,不然会直接跳过不拷贝。第二步:检测finalReducers里的每个reducer是否都有默认返回值function assertReducerShape(reducers) { Object.keys(reducers).forEach(key => { const reducer = reducers[key] const initialState = reducer(undefined, { type: ActionTypes.INIT }) if (typeof initialState === ‘undefined’) { throw new Error( Reducer "${key}" returned undefined during initialization. + If the state passed to the reducer is undefined, you must + explicitly return the initial state. The initial state may + not be undefined. If you don't want to set a value for this reducer, + you can use null instead of undefined. ) } const type = ‘@@redux/PROBE_UNKNOWN_ACTION’ + Math.random() .toString(36) .substring(7) .split(’’) .join(’.’) if (typeof reducer(undefined, { type }) === ‘undefined’) { throw new Error( Reducer "${key}" returned undefined when probed with a random type. + Don't try to handle ${ ActionTypes.INIT } or other actions in "redux/*" + namespace. They are considered private. Instead, you must return the + current state for any unknown actions, unless it is undefined, + in which case you must return the initial state, regardless of the + action type. The initial state may not be undefined, but can be null. ) } })}export default function combineReducers(reducers) { //省略第一步的代码 …… let shapeAssertionError try { assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e }}assertReducerShape方法主要检测两点:不能占用<redux/*>的命名空间如果遇到未知的action的类型,不需要要用默认返回值如果传入type为 @@redux/INIT<随机值> 的action,返回undefined,说明没有对未知的action的类型做响应,需要加默认值。如果对应type为 @@redux/INIT<随机值> 的action返回不为undefined,但是却对应type为 @@redux/PROBE_UNKNOWN_ACTION<随机值> 返回为undefined,说明占用了 <redux/> 命名空间。整个逻辑相对简单,好好自己梳理一下。第三步:返回一个函数,用于代理所有的reducerexport default function combineReducers(reducers) { //省略第一步和第二步的代码 …… let unexpectedKeyCache if (process.env.NODE_ENV !== ‘production’) { unexpectedKeyCache = {} } return function combination(state = {}, action) { if (shapeAssertionError) { throw shapeAssertionError } if (process.env.NODE_ENV !== ‘production’) { const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) { warning(warningMessage) } } let hasChanged = false const nextState = {} for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] const previousStateForKey = state[key] const nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === ‘undefined’) { const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state } }首先对传入的state用getUnexpectedStateShapeWarningMessage做了一个异常检测,找出state里面没有对应reducer的key,并提示开发者做调整。接着我们跳到getUnexpectedStateShapeWarningMessage里,看其实现。function getUnexpectedStateShapeWarningMessage( inputState, reducers, action, unexpectedKeyCache) { const reducerKeys = Object.keys(reducers) const argumentName = action && action.type === ActionTypes.INIT ? ‘preloadedState argument passed to createStore’ : ‘previous state received by the reducer’ if (reducerKeys.length === 0) { return ( ‘Store does not have a valid reducer. Make sure the argument passed ’ + ’to combineReducers is an object whose values are reducers.’ ) } if (!isPlainObject(inputState)) { return ( The ${argumentName} has unexpected type of " + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + ". Expected argument to be an object with the following + keys: "${reducerKeys.join('", "')}" ) } const unexpectedKeys = Object.keys(inputState).filter( key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key] ) unexpectedKeys.forEach(key => { unexpectedKeyCache[key] = true }) if (action && action.type === ActionTypes.REPLACE) return if (unexpectedKeys.length > 0) { return ( Unexpected ${unexpectedKeys.length &gt; 1 ? 'keys' : 'key'} + "${unexpectedKeys.join('", "')}" found in ${argumentName}. + Expected to find one of the known reducer keys instead: + "${reducerKeys.join('", "')}". Unexpected keys will be ignored. ) }}getUnexpectedStateShapeWarningMessage接收四个参数 inputState(state)、reducers(finalReducers)、action(action)、unexpectedKeyCache(unexpectedKeyCache),这里要说一下unexpectedKeyCache是上一次检测inputState得到的其里面没有对应的reducer集合里的异常key的集合。整个逻辑如下:前置条件判断,保证reducers集合不为{}以及inputState为简单对象找出inputState里有的key但是 reducers集合里没有key如果是替换reducer的action,跳过第四步,不打印异常信息将所有异常的key打印出来getUnexpectedStateShapeWarningMessage分析完之后,我们接着看后面的代码。 let hasChanged = false const nextState = {} for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] const previousStateForKey = state[key] const nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === ‘undefined’) { const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state首先定义了一个hasChanged变量用来表示state是否发生变化,遍历reducers集合,将每个reducer对应的原state传入其中,得出其对应的新的state。紧接着后面对新的state做了一层未定义的校验,函数getUndefinedStateErrorMessage的代码如下:function getUndefinedStateErrorMessage(key, action) { const actionType = action && action.type const actionDescription = (actionType && action "${String(actionType)}") || ‘an action’ return ( Given ${actionDescription}, reducer "${key}" returned undefined. + To ignore an action, you must explicitly return the previous state. + If you want this reducer to hold no value, you can return null instead of undefined. )}逻辑很简单,仅仅做了一下错误信息的拼接。未定义校验完了之后,会跟原state作对比,得出其是否发生变化。最后发生变化返回nextState,否则返回state。compose.js这个函数主要作用就是将多个函数连接起来,将一个函数的返回值作为另一个函数的传参进行计算,得出最终的返回值。以烹饪为例,每到料理都是从最初的食材经过一道又一道的工序处理才得到的。compose的用处就可以将这些烹饪工序连接到一起,你只需要提供食材,它会自动帮你经过一道又一道的工序处理,烹饪出这道料理。export default function compose(…funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (…args) => a(b(…args)))}上面是es6的代码,可能小伙伴们并不是很好理解,为了方便大家理解,我将其转换成es5代码去做讲解。function compose() { var _len = arguments.length; var funcs = []; for (var i = 0; i < _len; i++) { funcs[i] = arguments[i]; } if (funcs.length === 0) { return function (arg) { return arg; }; } if (funcs.length === 1) { return funcs[0]; } return funcs.reduce(function (a, b) { return function () { return a(b.apply(undefined, arguments)); }; });}梳理一下整个流程,大致分为这么几步:新建一个新数组funcs,将arguments里面的每一项一一拷贝到funcs中去当funcs的长度为0时,返回一个传入什么就返回什么的函数当funcs的长度为1时,返回funcs第0项对应的函数当funcs的长度大于1时,调用Array.prototype.reduce方法进行整合这里我们正好复习一下数组的reduce方法,函数reduce接受下面四个参数total 初始值或者计算得出的返回值current 当前元素index 当前元素的下标array 当前元素所在的数组示例:const array = [1,2,3,4,5,6,7,8,9,10];const totalValue=array.reduce((total,current)=>{ return total+current}); //55这里的compose有个特点,他不是从左到右执行的,而是从右到左执行的,下面我们看个例子:const value=compose(function(value){ return value+1;},function(value){ return value2;},function(value){ return value-3;})(2);console.log(value);//(2-3)*2+1=-1如果想要其从左向右执行也很简单,做一下顺序的颠倒即可。===> 转换前 return a(b.apply(undefined, arguments));===> 转换后 return b(a.apply(undefined, arguments));applyMiddleware.jsexport default function applyMiddleware(…middlewares) { return createStore => (…args) => { const store = createStore(…args) let dispatch = () => { throw new Error( Dispatching while constructing your middleware is not allowed. + Other middleware would not be applied to this dispatch. ) } const middlewareAPI = { getState: store.getState, dispatch: (…args) => dispatch(…args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(…chain)(store.dispatch) return { …store, dispatch } }}前面我们讲enhancer的时候,提到过这个applyMiddleware,现在我们将二者的格式对比看一下。// enhancer function enhancer(createStore) { return (reducer,preloadedState) => { //逻辑代码 ……. } }//applyMiddlewarefunction //applyMiddleware(…middlewares) { return createStore => (…args) => { //逻辑代码 ……. } }通过二者的对比,我们发现函数applyMiddleware的返回就是一个enhancer,下面我们再看其具体实现逻辑:通过createStore方法创建出一个store定一个dispatch,如果在中间件构造过程中调用,抛出错误提示定义middlewareAPI,有两个方法,一个是getState,另一个是dispatch,将其作为中间件调用的store的桥接middlewares调用Array.prototype.map进行改造,存放在chain用compose整合chain数组,并赋值给dispatch将新的dispatch替换原先的store.dispatch看完整个过程可能小伙伴们还是一头雾水,玄学的很!不过没关系,我们以redux-thunk为例,模拟一下整个过程中,先把redux-thunk的源码贴出来:function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === ‘function’) { return action(dispatch, getState, extraArgument); } return next(action); };}const thunk = createThunkMiddleware();thunk.withExtraArgument = createThunkMiddleware;export default thunk;哈哈哈!看完redux-thunk的源码之后是不是很奔溃,几千star的项目居然就几行代码,顿时三观就毁了有木有?其实源码没有大家想象的那么复杂,不要一听源码就慌。稳住!我们能赢!根据redux-thunk的源码,我们拿到的thunk应该是这样子的: const thunk = ({ dispatch, getState })=>{ return next => action => { if (typeof action === ‘function’) { return action(dispatch, getState); } return next(action); }; } 我们经过applyMiddleware处理一下,到第四步的时候,chain数组应该是这样子的:const newDispatch;const middlewareAPI={ getState:store.getState, dispatch: (…args) => newDispatch(…args)}const { dispatch, getState } = middlewareAPI;const fun1 = (next)=>{ return action => { if (typeof action === ‘function’) { return action(dispatch, getState); } return next(action); }}const chain = [fun1]compose整合完chain数组之后得到的新的dispatch的应该是这样子:const newDispatch;const middlewareAPI={ getState:store.getState, dispatch: (…args) => newDispatch(…args)}const { dispatch, getState } = middlewareAPI;const next = store.dispatch;newDispatch = action =>{ if (typeof action === ‘function’) { return action(dispatch, getState); } return next(action);}接下来我们可以结合redux-thunk的例子来模拟整个过程:function makeASandwichWithSecretSauce(forPerson) { return function (dispatch) { return fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize(‘The Sandwich Shop’, forPerson, error)) ); };}// store.dispatch就等价于newDispatchstore.dispatch(makeASandwichWithSecretSauce(‘Me’))====> 转换const forPerson = ‘Me’;const action = (dispatch)=>{ return fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize(‘The Sandwich Shop’, forPerson, error)) );}newDispatch()===> typeof action === ‘function’ 成立时 ((dispatch)=>{ return fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize(‘The Sandwich Shop’, forPerson, error)) ); })( (…args) => newDispatch(…args), getState)====> 计算运行结果const forPerson = ‘Me’;const dispatch = (…args) => newDispatch(…args) ;fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize(‘The Sandwich Shop’, forPerson, error)));// 其中:function fetchSecretSauce() { return fetch(‘https://www.google.com/search?q=secret+sauce');}function makeASandwich(forPerson, secretSauce) { return { type: ‘MAKE_SANDWICH’, forPerson, secretSauce };}function apologize(fromPerson, toPerson, error) { return { type: ‘APOLOGIZE’, fromPerson, toPerson, error };}====> 我们这里只计算Promise.resolve的结果,并且假设fetchSecretSauce返回值为'666’,即sauce=‘666’const forPerson = ‘Me’;const dispatch = (…args) => newDispatch(…args) ;dispatch({ type: ‘MAKE_SANDWICH’, ‘Me’, ‘666’})====> 为了方便对比,我们再次转换一下const action = { type: ‘MAKE_SANDWICH’, ‘Me’, ‘666’};const next = store.dispatchconst newDispatch = action =>{ if (typeof action === ‘function’) { return action(dispatch, getState); } return next(action);}newDispatch(action)====> 最终结果store.dispatch({ type: ‘MAKE_SANDWICH’, ‘Me’, ‘666’});以上就是redux-thunk整个流程,第一次看肯能依旧会很懵,后面可以走一遍,推导一下加深自己的理解。bindActionCreators.jsexport default function bindActionCreators(actionCreators, dispatch) { if (typeof actionCreators === ‘function’) { return bindActionCreator(actionCreators, dispatch) } if (typeof actionCreators !== ‘object’ || actionCreators === null) { throw new Error( bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? 'null' : typeof actionCreators }. + Did you write "import ActionCreators from" instead of "import * as ActionCreators from"? ) } const keys = Object.keys(actionCreators) const boundActionCreators = {} for (let i = 0; i < keys.length; i++) { const key = keys[i] const actionCreator = actionCreators[key] if (typeof actionCreator === ‘function’) { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators}bindActionCreators针对于三种情况有三种返回值,下面我们根据每种情况的返回值去分析。(为了方便理解,我们选择在无集成中间件的情况)typeof actionCreators === ‘function’function bindActionCreator(actionCreator, dispatch) { return function() { return dispatch(actionCreator.apply(this, arguments)) }}const actionFun=bindActionCreator(actionCreators, dispatch)===> 整合一下const fun1 = actionCreators;const dispatch= stror.dispatch;const actionFun=function () { return dispatch(fun1.apply(this, arguments)) }根据上面的推导,当变量actionCreators的类型为Function时,actionCreators必须返回一个action。typeof actionCreators !== ‘object’ || actionCreators === null throw new Error( bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? 'null' : typeof actionCreators }. + Did you write "import ActionCreators from" instead of "import * as ActionCreators from"? )提示开发者actionCreators类型错误,应该是一个非空对象或者是函数。默认 const keys = Object.keys(actionCreators) const boundActionCreators = {} for (let i = 0; i < keys.length; i++) { const key = keys[i] const actionCreator = actionCreators[key] if (typeof actionCreator === ‘function’) { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators通过和第一种情况对比发现,当actionCreators的每一项都执行一次第一种情况的操作。换句话说,默认情况是第一种情况的集合。以上是对bindActionCreators的剖析,可能小伙伴们对这个还是不够理解,不过没有关系,只要知道bindActionCreators干了啥就行。bindActionCreators是需要结合react-redux一起使用的,由于本篇文章没有讲解react-redux,所以这里我们不对bindActionCreators做更深入的讲解。下篇文章讲react-redux,会再次提到bindActionCreators。结语到这里整个redux的源码我们已经剖析完了,整个redux代码量不是很大,但是里面的东西还是很多的,逻辑相对来说有点绕。不过没关系,没有什么是看了好几次都看不懂的,如果有那就再多看几次嘛!另外再多一嘴,如果想快读提高自己的小伙伴们,我个人是强烈推荐看源码的。正所谓“近朱者赤,近墨者黑”,多看看大神的代码,对自己的代码书写、代码逻辑、知识点查缺补漏等等方面都是很大帮助的。就拿我自己来说,我每次阅读完一篇源码之后,都受益匪浅。可能第一次看源码,有着诸多的不适应,毕竟万事开头难,如果强迫自己完成第一次的源码阅读,那往后的源码阅读将会越来越轻松,对自己的提升也就越来越快。各位骚年们,撸起袖子加油干吧! ...

September 20, 2018 · 11 min · jiezi

redux原来如此简单

Redux 是 JavaScript 状态容器, 提供可预测化的状态管理。那什么是可以预测化,我的理解就是根据一个固定的输入,必然会得到一个固定的结果。redux是专门为react开发的,但并不是只能用于react,可以用于任何界面库。动机随着单页面应用的普及,web app内部需要管理的状态越来越多,这些状态可能来自服务器端,用户输入的数据,用户交互数据,当前UI状态,本地的缓存数据等等。如何能够有条理的管理这些数据,成为前端开发中一个难题。核心概念三大原则单一数据源使用redux的程序,所有的state都存储在一个单一的数据源store内部,类似一个巨大的对象树。state是只读的state是只读的,能改变state的唯一方式是通过触发action来修改使用纯函数执行修改为了描述 action 如何改变 state tree , 你需要编写 reducers。reducers是一些纯函数,接口当前state和action。只需要根据action,返回对应的state。而且必须要有返回。一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数基础action顾名思义,action就是动作,也就是通过动作来修改state的值。也是修改store的唯一途径。action本质上就是一个普通js对象,我们约定这个对象必须有一个字段type,来表示我们的动作名称。一般我们会使用一个常量来表示type对应的值。此外,我们还会把希望state变成什么样子的对应的值通过action传进来,那么这里action可能会类似这样子的{ type: ‘TOGGLE_TODO’, index: 5}ReducerAction 只是描述了有事情发生了这件事实,但并没有说明要做哪些改变,这正是reducer需要做的事情。Reducer作为纯函数,内部不建议使用任何有副作用的操作,比如操作外部的变量,任何导致相同输入但输出却不一致的操作。如果我们的reducer比较多,比较复杂,我们不能把所有的逻辑都放到一个reducer里面去处理,这个时候我们就需要拆分reducer。幸好,redux提供了一个api就是combineReducers Api。storestore是redux应用的唯一数据源,我们调用createStore Api创建store。脱离react的redux案例store,reducer基础使用第一步搭建开发环境,这里不介绍了,参考上一篇文章 手把手教会使用react开发日历组件,搭建环境部分搭建好环境切换到目录下面npm install redux –save把index.tsx修改为之下代码。import { createStore, combineReducers, applyMiddleware } from ‘redux’var simpleReducer = function(state = {}, action) { return { user: { name: ‘redux’ } }}var store = createStore(simpleReducer)console.log(store.getState())我们看到控制台打印出来的一个包含user信息的这么一个对象。我们使用到了几个api? createStore创建store,store.getState()获取store,也就是唯一数据源的根节点。上文我们也讲过,action的情况可能会比较多,redux也提供了combineReducers Api。如果我们有多个reducer,我们就可以使用起来了。那我们创建多个reducer测试一下,代码如下:import { createStore, combineReducers, applyMiddleware } from ‘redux’function user(state = {name: ‘redux’}, action) { switch (action.type) { case ‘CHANGE_NAME’: return { …state, name: action.name } } return state}function project(state = {name: ‘min-react’}, action) { switch (action.type) { case ‘CHANGE_NAME’: return { …state, name: action.name } } return state}var rootReducer = combineReducers({ user, project})var store = createStore(rootReducer)console.log(store.getState())如我们所预料一样,我们得到拥有两个字段的根store。结合view使用第一步我们把html改造成这个样子,新增了一点标签<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Document</title> <style type=“text/css”> * { margin: 0; padding: 0; } </style></head><body> <div id=“userName”></div> <input id=“userNameInput”/><button id=“userNameButton”>更改userName</button> <script src="./dist/main.js"></script></body></html>第二步,修改index.tsx,如下import { createStore, combineReducers, applyMiddleware } from ‘redux’import { func } from ‘prop-types’function user(state = {name: ‘redux’}, action) { switch (action.type) { case ‘CHANGE_USER_NAME’: return { …state, name: action.name } } return state}function project(state = {name: ‘min-react’}, action) { switch (action.type) { case ‘CHANGE_PROJECT_NAME’: return { …state, name: action.name } } return state}var rootReducer = combineReducers({ user, project})var store = createStore(rootReducer)function render(state = store.getState()) { var $userName = document.getElementById(‘userName’) $userName.innerHTML = state.user.name}render()console.log(store.getState())我们看到页面正确的显示了我们user的名称。下一步我们需要做的就是通过用户的操作,改变store的值,进而触发view的更新。于是我们新增了这块代码:store.subscribe(function() { render()})// 绑定用户事件var $userNameInput = document.getElementById(‘userNameInput’)var userNameButton = document.getElementById(‘userNameButton’)userNameButton.onclick = function() { var value = $userNameInput.value store.dispatch({ type: ‘CHANGE_USER_NAME’, name: value })}我们看到保存之后,当我们输入值之后,点击更改,页面的值随着改变。但是控制台报了一个错误,TS2339: Property ‘value’ does not exist on type ‘HTMLElement’.,这是由于typescript强类型校验没通过导致的。只要加这段代码就好了var $userNameInput = document.getElementById(‘userNameInput’) as HTMLInputElement看到了吧,redux就是这么简单。其他所有上层应用,都是在此基础上开发的,所以开发一个redux应用的步骤就是定义action和与之对应的reducer监听store的变化,提供回调函数dispatch一个action,等待好运发生。结合react,其他view类库,开发步骤莫不如此。高级应用异步action我们也看到了,我们的reducer只能做同步应用,如果我们需要在reducer,做一些延迟操作,可怎么办社区已经有成熟的类库做这件事件npm install redux-thunk –saveredux本身已经提高了很好的扩展机制,就是中间件。这点很类似express的中间件。//引入新的类库import { createStore, combineReducers, applyMiddleware, compose } from ‘redux’import thunk from ‘redux-thunk’…//store部分做如下修改const finalCreateStore = compose(applyMiddleware(thunk))(createStore)const store = finalCreateStore(rootReducer, {})redux-thunk的作用就是让dispatch方法不仅仅只接收action对象,还可以包含一个方法。我们可以在这个方法内部去调用异步代码我们把dom事件部分做了如下改造userNameButton.onclick = function() { var value = $userNameInput.value store.dispatch<any>(function(dispatch, getState) { setTimeout(() => { dispatch({ type: ‘CHANGE_USER_NAME’, name: value }) }, 2000) })}可以看到页面元素确实在2s之后发生了变化,实际业务中啊,我们这里可以做一些异步操作。至于redux原理,以及源码和中间件的源码讲解可以参照我的另外一篇文章 阅读redux源码 ...

September 7, 2018 · 2 min · jiezi

手动实现一个compose函数

在redux中合并reducer的时候有用到compose这个函数将多个reducer合成一个,那么这个compose函数该怎么实现呢?function compose(…fns) { //fns是传入的函数 const fn = fns.pop(); return (…args) => { fn(…args); if (fns.length > 0) { compose(…fns); } };}

September 3, 2018 · 1 min · jiezi