模倣ミシェルビンチェンコンポーネントの2番目の爆弾であるReact+Reduxは、夏の間あなたを連れて行きます!

序文

みなさん、こんにちは。少し前に、Reactの簡単な知識を使って、Michelle Bingchengの注文アプレットを模倣して、いくつかの簡単なページを作成しました。新しいコンセプトに触れたばかりなので、初めてでした。始めました。必然的に少しラフなので、この記事では、前の記事のプログラムのいくつかの場所を最適化し、いくつかの小さなバグを修正します。同時に、この新しい週に、私はまったく新しい知識、つまりReduxに触れました。したがって、この記事では、Reduxの原理と使用法にも焦点を当て、あなたがReduxのより深い理解。記事は少し長いかもしれませんが、それは詳細で、間違いなく乳母レベルのチュートリアルです。

Reduxの簡単な紹介

Reduxとは何ですか?何のために?なぜReduxを使用する必要があるのですか?

最初に直接3回聞いたのですが、友達の多くは混乱していると思います。心配しないで、1つずつ説明します。

Reduxとは

Reduxは、「アクション」と呼ばれるイベントを使用してアプリケーションの状態を管理および更新するためのパターンとツールのライブラリです。 

Reduxは何に適していますか?

Reduxは、アプリケーション全体で使用される状態を一元化されたストアの形式で一元管理し、そのルールにより、状態は予測可能な方法でのみ更新できるようになります。

なぜReduxを使用する必要があるのですか

Reduxは、アプリケーションの状態がいつ、どこで、なぜ、どのように更新されるか、およびこれらの変更が発生したときにアプリケーションロジックがどのように動作するかを理解しやすくするためのパターンとツールを提供します。

公式声明のこの大きなセクションのリリースは、多くの友人を困惑させたと思いますが、それはどういうことですか?例を挙げましょう。Reactでは、コンポーネントが相互に通信する状況が4つあることは誰もが知っています。

  • 親コンポーネントは子コンポーネントと通信します
  • 子コンポーネントは親コンポーネントと通信します
  • クロスレベルコンポーネント通信
  • ネストされた関係のないコンポーネント間の通信

その中で、最初の2つは実装が最も簡単です。Reactのデータフローは一方向であるため、親コンポーネントと子コンポーネント間の通信も最も一般的です。必要な情報を子コンポーネントに渡すだけで済みます。小道具を通して子コンポーネントは、コールバック関数カスタムイベントを介して親コンポーネントと通信できます。これは少し面倒ですが、非常に単純でもあります。

而后面两种就略显尴尬了,他们因为组件之间关系过弱,想要进行通信只能通过层层传递props来进行,这无疑是十分麻烦的,因为在大型项目中,组件与组件之间的关系是十分复杂的,如果要层层传递的话,就要写非常多的无关代码,这对开发无疑是非常不利的,或者是通过context,这是一个全局变量,虽然我们可以把数据放在其中,供其他组件随意取用,减少我们层层传递的次数,但当项目复杂的时候,我们并不能很好的知道context是从哪里传过来的,这对代码的可读性显然是个降维打击,因此也不是很好的选择。我们能不能有一个很大的仓库,把所有的状态全部放在仓库里,当我需要使用的时候,只需要去对应的地方,取出对应的状态,不就好了吗。所以,在大量需求下,状态管理工具Redux就出现了。

redux原理图.png

上面的就是Redux的原理图,不少小伙伴看到这张图都一脸迷惑,这都是个啥??既然我们在写一个奶茶组件,我就通过一个比较简单的方式来解释一下Redux的工作原理。首先,你是一个组件,也就是图中的ReactComponents,你走在街上,太热了,想喝一杯奶茶(更新状态),于是,你走向蜜雪冰城(Redux),然后对前台的点单小姐姐(ActionCreator)说我要一杯奶茶(更新状态),小姐姐甜甜的对你说,你要和什么奶茶呀(你要更新什么状态?),多糖还是少糖,加冰还是去冰,打包还是堂食呢?(更新状态的内容是啥?),然后你对小姐姐说出了你的要求,小姐姐操作点餐机将你的需求打印在小票上(action),上面记录了你要喝什么(data),怎么喝(type),然后小姐姐把打印好的小票给(dispatch)老板(store),老板看了小票以后,就通知后厨小哥哥(Reducer)给你做奶茶了,小哥哥会按照小票,为你制作奶茶(改变状态),制作完成后交给老板(return newState)然后老板会叫你的号,让你去领取奶茶(getState)。就这样,经过几个步骤,你就得到了一杯冰冰凉凉的奶茶(更新了状态)。通过上面的例子,我相信你对Redux一定有了一个大概的了解,下面,我将通过例子来对Redux中的一个React-Redux库进行详细介绍。

React-Redux使用方法

1. 安装依赖

npm i redux
npm i react-redux
npm i redux-thunk
复制代码

2. 首页配置

import { Provider } from "react-redux";
import store from "./store";

ReactDOM.createRoot(document.getElementById("root")).render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>
);
复制代码

因为我们需要给每一个子仓库传store,为了简便操作,我们在main.js之中引入Provider,使用react全家桶中的react-redux,解构出Provider,向应用申明式的添加数据管理功能。这样就可以向所有的子仓库引入store,方便开发。

3. 建立仓库

在前面我说过,Redux就像是一个仓库,管理着所有的状态,所以,我们首先需要有一个仓库。

import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk' 
import reducer from './reducer'

const composeEnhancers =
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer,
    composeEnhancers(
        applyMiddleware(thunk)
    )
)

export default store;
复制代码

在这一步我们通过createStore创建了一个仓库,然后引入了一个React-thunk,这个库可以使我们的组件可以实现异步请求数据,下面的是让我们的项目可以使用ReduxDevTools这个插件,这是一款专门为Redux设计的插件。非常好用。

4. 建立“货架”

有了仓库,就可以往里面添加状态了,但所有的组件状态全部仍在一起,会很杂乱,也不便于寻找,所有我们需要为每个组件设置一个货架(Reducer),这样,组件的状态待在自己的货架上,其他的组件想要拿到这个组件的状态也是非常容易,所以我们需要为每个组件建立自己的货架。这里我们使用combineReducers来建立

import { combineReducers } from 'redux'
import { reducer as nearbyReducer } from '../pages/Food/Nearby/store'
import { reducer as orderdetailReducer } from '../pages/Orderdetail/store'

export default combineReducers({
    nearby: nearbyReducer,
    orderdetail: orderdetailReducer,
})
复制代码

5. 建立“容器组件”

因为在React-Redux中,UI组件值负责渲染,而与外面(即store)通信全部依靠容器组件。因此,我们需要一个容器组件,这里我们使用connect建立一个容器组件,他接收三个参数,并且默认暴露。第一个参数为mapStateToProps,这是一个读操作,可以获取到状态。第二个参数为mapDispatchToProps,这个对应的是一个写操作,触发了状态的改变。上面两个参数都是一个函数,且mapStateToProps需要返回的是一个一般对象,他的字面意思是“映射(map)状态(state)to()props”。而mapDispatchToProps需要返回的也是对象,他的字面意思是映射dispatch到props。这里我们给出精简后的代码:

import React, { useEffect, useState } from "react";
import { Wrapper } from "./style";
import IceBanner from "./IceBanner";
import { connect } from "react-redux";
import { getShopList, getMapShow } from "./store/actionCreators";

const Nearby = (props) => {
  const { mapShow, shopList } = props;
  const { getShowListDispatch, getMapShowDispatch } = props;
  const openMap = () => {
    getMapShowDispatch(mapShow);
  };
  useEffect(() => {
    getShowListDispatch();
  }, []);
  return (
    <Wrapper>
      <IceBanner message={shopList} />
    </Wrapper>
  );
};

const mapStateToProps = (state) => {
  return {
    mapShow: state.nearby.mapShow,
    shopList: state.nearby.shopList,
  };
};
const mapDispatchToProps = (dispatch) => {
  return {
    getShowListDispatch() {
      dispatch(getShopList());
    },
    getMapShowDispatch(mapShow) {
      dispatch(getMapShow(mapShow));
    },
  };
};
export default connect(mapStateToProps, mapDispatchToProps)(Nearby);

复制代码

特别注意:异步的请求需要放在生命周期函数里,确保页面一开始就会渲染且自动更新。

6.子仓库“货架”的组成

在开发中,我们的子仓库通常分为4个文件,我会为大家一一介绍文件的用法。

1.actionCreators.js

这个文件主要是对数据进行一个包装,里面所有函数名都采用小驼峰式命名法则,同步请求命名方式为change...,异步请求命名方式为get...。其中同步方法主要是确定一个type类型,并将传递给他的data值与type一起包装成一个action。异步方法主要是通过接口请求数据,请求函数统一放在api文件夹下,知足要返回数据即可,然后将返回的数据给同步方法进行包装,然后即将包装好的数据通过dispatch方法发送给reducer进行修改。代码如下:

import { getShopListRequest } from '../../../../api/request'
import * as actionTypes from './constants'

export const changeMapShow = (data) => ({
    type: actionTypes.CHANGE_MAP_SHOW,
    data
})

export const getMapShow = (data) => {
    return (dispatch) => {
        dispatch(changeMapShow(!data))
    }
}

export const changeShopList = (data) => ({
    type: actionTypes.CHANGE_SHOP_LIST,
    data
})

export const getShopList = () => {
    return (dispatch) => {
        getShopListRequest().then(data => {
            // console.log(data.data);
            dispatch(changeShopList(data.data.shoplist))
        })
    }
}
复制代码

2.constants.js

这个文件主要是将所有的type取一个别名,这样在后期维护中只需要在这个文件夹修改就可以,而不用去其他文件夹一个一个修改,提高了代码的可维护性。代码如下:

export const CHANGE_MAP_SHOW = 'CHANGE_MAP_SHOW'
export const CHANGE_SHOP_LIST = 'CHANGE_SHOP_LIST'
复制代码

3.index.js

这个文件夹是整个子仓库的核心,起到链接其他文件的作用。代码如下:

import reducer from './reducer'
import * as actionCreators from './actionCreators'

export {
    reducer,
    actionCreators
}
复制代码

4.reducer.js

这是整个Redux的灵魂所在,首先会为所有状态设置一个默认值,当没有状态改变的时候,就会将默认值赋给组件,如果接收到打包过来的action,就会对其进行分析,首先使用一个switch方法,将所有的type类型进行一一匹配,如果匹配成功,就会将原来的状态使用展开运算符进行展开,然后将新状态加入,很多新手第一次容易不使用展开运算符,而是使用数组的api来进行状态的修改,这样虽然修改了值,但却改变了原数组,这会是redux监听不到状态的改变,从而不能进行自动渲染页面,而es6新提供的展开运算符很好的避免了这一点,他是在原数组的基础上进行操作的,所以我推荐使用这种方法。代码如下:

import * as actionTypes from "./constants";

const defaultState = {
    mapShow: true,
    shopList: [],
}

export default (state = defaultState, action) => {
    switch (action.type) {
        case actionTypes.CHANGE_SHOP_LIST:
            return {
                ...state,
                shopList: action.data
            }
        case actionTypes.CHANGE_MAP_SHOW:
            return {
                ...state,
                mapShow: action.data
            }
        default:
            return state;
    }
}
复制代码

插一句嘴,我记得在《你不知道的JavaScript》中卷的Promise章节的第一面有一句话:只了解api会丢失很多抽象的细节。所以希望大家不要局限于api,要去多了解特性。

以上就是我对redux的全部理解了,redux虽然麻烦,但他存在的意义就是为了服务与大型组件的,这东西,我只能说能不用就不用,但可以不用,但不能不会。所以,希望大家可以好好学习这一步分的知识,如果有不同的看法也可以找我讨论,如果有不对的地方也可以尽管指出。感谢!

项目更新

redux的使用useState的丢弃

因为使用了redux进行集中式的状态管理,因此我将所有一面上使用了useState的地方全部修改为了使用redux进行管理。代码就不放了大家理解思想就好。

点餐页面的实现

效果如图:

diancan.gif

1. 顶部搜索栏

搜索栏由一个返回的按钮以及输入框组成,全部来自antd-mobile提供的组件,其中返回键可以返回到上一级界面,主要使用的是useNavigateuseParams判断当id不存在时,向上走一级,即出栈。配合路由可返回到home界面。

const navigate = useNavigate();
  let { id } = useParams();
  // console.log(id);
  if (!id) {
    navigate("/home");
    return;
  }
  
  <NavBar
    back=""
    onBack={() => navigate(-1)}
  >
    <SearchBar className="search" placeholder="请输入商品名" />
  </NavBar>
复制代码

2.店名及距离

这个我写了一个函数来实现,因为jsx里面不宜放过多的代码,所以用一个函数来占位,在外面书写功能,达到提高可读性的效果。下面给出函数体的代码:

const renderInfo = (shopList) => {
    if (shopList.length > 0) {
      const res = shopList.filter(
        (item) =>
          // 外层不能加{},对象包对象筛不出来
          item.id == id
      );
      return res.map((item) => (
        <div key={item.id}>
          <div className="left">
            <div className="left-up">
              {item.name}
              <RightOutline />
            </div>
            <div className="left-down">
              <Space wrap style={{ fontSize: 16 }}>
                <EnvironmentOutline />
              </Space>
              {item.distance}
            </div>
          </div>
          <div className="right">
            <Space wrap>
              <Button color="danger" size="mini">
                自提
              </Button>
            </Space>
          </div>
        </div>
      ));
    }
  };
复制代码

这里我首先使用Redux将所有商店的信息从仓库里面取出来,然后做了一个数据筛选,使用useParams获取当前路由的id,因为每个商店点击进去的id是不同的,然后捕获id后使用filter方法将对应id的数据从Redux中筛选出来,这样,就保证了我们这里显示的信息和点击的商店的信息一致。

3.点餐组件的实现

我将点餐组件另外分了出去,首先在父组件里面使用Redux取出对应的数据,然后通过Props传递给子组件

<SaleDetail detail={milkTeaList} />
复制代码

这个组件采用了两列式布局,左边为链接,右边显示商品,通过给左边的链接设置点击事件,右边设置锚点跳转,实现了一个简单的动画效果。同时因为数据是嵌套的数组,所以我使用了两层map循环将数据显示。这部分主要考验的是CSS的功底,代码如下:

import React from "react";
import { Wrapper } from "./style";

export default function SaleDetail({ detail }) {
  const renderInfo = () => {
    return detail.map((item) => (
      <div key={item.id} className="sale-left-name">
        <a onClick={() => scrollToAnchor(item.id)}>
          <span>{item.name}</span>
        </a>
      </div>
    ));
  };
  const scrollToAnchor = (anchorName) => {
    if (anchorName) {
        let anchorElement = document.getElementById(anchorName)
        if (anchorElement) {
            anchorElement.scrollIntoView({
                block: 'start',
                behavior: 'smooth'
            })
        }
    }
  }
  const renderSaleSlide = () => {
    return detail.map((item, index) => {
      return (
        <div className="menu-box-detail" key={item.id}>
          <div className="menu-top">
            <div className="top-title" id={item.id}>
              {item.name}
            </div>
            <span>{item.description}</span>
          </div>
          <div className="menu-box">
            {item.foods.map((element, index) => {
              return (
                <div key={index} className="menu-detail">
                  <div className="menu-detail-box">
                    <div className="menu-item" key={index}>
                      <div className="img-box">
                        <img className="sale-img" src={element.img} alt="" />
                      </div>
                      <section>
                        <p className="fooddetail-info">
                          <span>{element.name}</span>
                        </p>
                        <p className="fooddetail-sale">
                          <span>{element.month_sales}</span>
                        </p>
                        <div className="fooddetails-space"></div>
                        <span className="sale_price">
                          <span>¥{element.lowest_price}</span>
                          <span className="sale_price_right">起</span>
                        </span>
                        <div className="food-btn">
                          <span>+</span>
                        </div>
                      </section>
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      );
    });
  };
  return (
    <Wrapper>
      <div className="sale-box">
        <div className="sale-main">
          <div className="sale-left">
            <ul>{renderInfo()}</ul>
          </div>
          <div className="sale-detail-box">
            <div className="sale-detail">{renderSaleSlide()}</div>
          </div>
        </div>
      </div>
    </Wrapper>
  );
}
复制代码

CSS部分过长,就不展示了,感兴趣的小伙伴可以去文章末尾查看源码。

页面的更新就是这些了。后面则是我对前面的页面进行的一个优化。

页面优化

百度地图api的引入

在上篇文章中,这个部分就是单纯的用一张图片进行占位,如下图:

map.png 后面我了解到百度地图有专门的Api组件库可以使用,于是,经过查看官方文档和查阅资料,我实现了如下的效果:

baidumap.gif

方法还是很简单的,首先我们需要下载官方的组件库:

npm install react-bmapgl --save
复制代码

然后需要在入口文件添加一个百度地图api的js代码

<script type="text/javascript" src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=您的密钥"></script>
复制代码

注意:这个秘钥是需要你自己去百度地图开放平台官网申请,如果你没有的话,可以使用我的秘钥:

<script type="text/javascript" src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=7HQV2rWwWoBpkjbbVzja2g9tdnGvZMED"></script>
复制代码

这样,你就可以尽情的使用百度地图的api组件了,我们首先要进行一个引入:

import { Map, Marker, NavigationControl, InfoWindow } from "react-bmapgl";
复制代码

然后就是查阅文档书写组件了,代码如下:

<Collapse defaultActiveKey={["1"]} onChange={openMap}>
          <Collapse.Panel key="1" title={mapShow ? "收起地图" : "展开地图"}>
            {/* <img src={Map1} alt="" style={{ width: "100%" }} /> */}
            <Map center={{ lng: 115.832777, lat: 28.724024 }} zoom="14" style={{ height: 300 }}>
            <Marker position={{ lng: 115.832777, lat: 28.724024 }} />
              <NavigationControl />
              <InfoWindow position={{ lng: 115.832777, lat: 28.724024 }} title="我的位置" text="东华理工大学" />
            </Map>
          </Collapse.Panel>
        </Collapse>
复制代码

其中的center就是地图的中心点,可以通过设置lng(经度)和lat(纬度)来确定中心点的位置,然zoom属性可以控制地图的缩放比例。然后还添加了一个定位来显示当前的位置,并通过气泡弹窗进行提示。然后上面的收起-展开地图的文字也是使用redux进行控制,每当点击的时候,控制状态进行去反,同步折叠面板收缩展开,基本和原版一致。

页面全局自适应

因为是第一次做移动端的项目,在上一篇文章发布后,我就发现了虽然在iphone12Pro上显示效果还行,但如果更换手机,则页面布局瞬间一团乱,所以,我做了一个全局自适应的优化,首先还需要新建的文件夹public然后新建一个js文件adapter.jsx在这个文件中做全局自适应的适配,代码如下:

var init = function () {
    var clientWidth = document.documentElement.clientWidth || document.body.clientWidth;
    if (clientWidth >= 640) {
      clientWidth = 640;
    }
    var fontSize = 16 / 375 * clientWidth;
    document.documentElement.style.fontSize = fontSize + "px";
  }
  
  init();
  
  window.addEventListener("resize", init);
复制代码

这一段代码的意思是设置了一个叫rem单位,rem是什么单位呢?

rem是相对单位,是相对HTML根元素。 这个单位可谓集相对大小和绝对大小的优点于一身,通过它既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层复合的连锁反应。

因此,在以后的页面中,我们可以完全抛弃px,全部使用rem代替,这样,当页面大小不同的时候,样式也会根据字体的大小进行改变,非常方便。

我们需要将这个文件也在入口文件处引入:

    <script src="./public/js/adapter.jsx"></script>
复制代码

注意:这个文件需要放在head处引入,而不是body。因为我们需要他阻塞页面的加载和渲染,使页面所有的地方都能使用到rem。

正则表达式的使用

在上篇文章中,我提到遇到二级路由因为路径问题而导致样式上不去,思考过后写出来这样的代码:

<FooterWrapper>
      <Link to="/food" className={classnames({ active: pathname == "/food" || pathname == "/food/nearby" || pathname == "/food/often"})}>
        <i className="iconfont icon-faxian"></i>
        <span>点餐</span>
      </Link>
      <Link
        to="/order"
        className={classnames({ active: pathname == "/order" || pathname == "/order/ing" || pathname == "/order/back" || pathname == "/order/history"})}
      >
        <i className="iconfont icon-shouye1"></i>
        <span>订单</span>
      </Link>
    </FooterWrapper>
复制代码

不能说难看吧,只能说毫无美感,于是,我使用了正则表达式进行优化,代码变成了下面的样子:

const Footer = () => {
  var foodReg = /^(\/food)(\/\w+)?/;
  var orderReg = /^(\/order)(\/\w+)?/;
  const { pathname } = useLocation();
  if (isPathPartlyExisted(pathname)) return
  return (
    <FooterWrapper>
      <Link 
        to="/food" 
        className={classnames({ active: foodReg.test(pathname)})}
      >
        <i className="iconfont icon-faxian"></i>
        <span>点餐</span>
      </Link>
      <Link
        to="/order"
        className={classnames({ active: orderReg.test(pathname)})}
      >
        <i className="iconfont icon-shouye1"></i>
        <span>订单</span>
      </Link>
    </FooterWrapper>
  );
};
复制代码

感觉瞬间好了不少。

最后

在这个星期中,我初步理解并使用了Redux,虽然说他不是很方便,甚至可以说是麻烦,但不可否认的是他在大型项目中的重要性,所以我还会继续深入学习,最近看掘金另一位大佬神三元的云音悦项目,有很大的启发,比如搜索框左滑动态消失,延迟加载等,都让我很受启发,所以,我后面还会根据他的理念完善这个项目,在将购物车以及付款界面做出来,如果大家觉得写的不错,可以点个小赞,您的点赞是我继续创作对的最大动力。

项目源码地址

おすすめ

転載: juejin.im/post/7119291650488139783