响应国家号召,在家撸码之React迁移记

  最近这段时间新型冠状病毒肆虐,上海确诊人数每天都在增加,人人提心吊胆,街上都没人了。为了响应国家号召,近期呆在家里撸码,着手将项目迁移到React中,项目比较朴素,是一张线索提交页面,包含表单、图片滚动等功能。

一、目录结构

  项目基于Create React App构建而成,简单的做了下二次封装,src目录的结构如下所示。

├── src
│   ├── __tests__ ---------------------- 测试文件
│   ├── common ------------------------- 通用功能
│   ├── component ---------------------- 组件
│   ├── img ---------------------------- 图片
│   ├── page --------------------------- 页面
│   ├── router ------------------------- 路由
│   ├── store -------------------------- 状态容器
│   ├── index.scss --------------------- 公共样式
│   ├── index.js ----------------------- 入口文件

  在index.js中会引入公共样式、路由、统计脚本、通用功能等。index.scss集合的公共样式包括重置、布局、字体、间距等。common中的通用功能包括通信、加载第三方脚本、微信配置等。

二、组件

  在本项目中组件都以函数的形式出现,并且其最小的粒度是控件,也就是文本框(Input)、选择框(Select)、复选框(Checkbox)和单选框(Radio),然后是表单(Form)和三级联动(Chain),其目录如下所示,一定会包含index两个文件。

├── component
│   ├── Input -------------------------- 控件
│   │   ├── index.scss ----------------- 样式
│   │   ├── index.js ------------------- 脚本

  在Form组件中,会包含其它组件,并且会传递相关数据给它们。

1)文本框

  一般会将文本框的type、placeholder和name传进来,当没有属性时,就直接返回一个只包含基本样式的文本框。在Input组件中,通过useState()钩子初始化状态,如下所示。

export function Input(props) {
  if (!props) return <input className="input" />;             //空的文本框
  const [value, setValue] = useState(props.value || "");
  const inputProps = {
    type: props.type,
    placeholder: props.placeholder,
    name: props.name
  };
  return <input className="input" {...inputProps} />;
}

  在Form组件中可传递的数据如下所示。

const txt = {
  type: "text",
  placeholder: "姓名",
  name: "name"
};

2)单选框和复选框

  单选框和复选框接收的props是一个数组,而不是对象,在Form组件中可传递的数据如下所示,其中defaultChecked属性用于设置默认的选中项。

const radios = [
  {type:"radio", name:"gender", value:1, text:"man"},
  {type:"radio", name:"gender", value:2, text:"woman", defaultChecked:true}
];
const checkboxs = [
  {type:"checkbox", name:"color", value:1, text:"红"},
  {type:"checkbox", name:"color", value:2, text:"绿", defaultChecked:true},
  {type:"checkbox", name:"color", value:3, text:"蓝", defaultChecked:true}
];

  在Radio组件中,选中项保存在checked变量中,通过ES6新增的find()方法获取,如下所示。组件为每个单选框注册了Change事件,事件处理程序中调用的callback()方法将在后文讲解。

export function Radio(props) {
  if (!props) return null;
  const radios = props.items || [],
    checked = radios.find(item => item.defaultChecked) || {},
    [value, setValue] = useState(checked.value || "");
  function handle(e) {
    props.callback(e.target.value);
  }
  return radios.map(item => (
    <label className={"ui-" + item.type} key={item.value}>
      <input {...item} onChange={handle} />
      {item.text}
    </label>
  ));
}

  在Checkbox组件中,选中项可以是多个,保存在checkeds数组中,通过filter()和map()获取。同样为每个复选框都注册了一个Change事件,在事件处理程序中会过滤掉当前值,当选中时,再添加到选中数组中,如下所示。

export function Checkbox(props = []) {
  if (props.length == 0) return null;
  const checkboxs = props.items || [],
    checkeds = checkboxs
      .filter(item => item.defaultChecked)
      .map(item => item.value),
    [values, setValues] = useState(checkeds);
  function handle(e) {
    const { checked, value } = e.target;
    const current = values.filter(item => item != value);
    checked && current.push(value);
    props.callback(current);
  }
  return checkboxs.map(item => (
    <label className={"ui-" + item.type} key={item.value}>
      <input {...item} onChange={handle} />
      {item.text}
    </label>
  ));
}

3)快照测试

  在做快照测试时,需要传递事件对象,为简便起见,直接用一个普通对象模拟它,如下所示。

it("Radio-Snapshot", () => {
  const radios = [
    { type: "radio", name: "gender", value: 1, text: "man" },
    { type: "radio", name: "gender", value: 2, text: "woman", defaultChecked: true }
  ];
  const component = renderer.create(<Radio items={radios} />);
  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();

  //模拟Change事件
  const event = { target: { value: 1 } };
  act(() => {
    tree[0].children[0].props.onChange(event);
  });
  tree = component.toJSON();
});

  注意,为了测试时更接近React在浏览器中的工作方式,需要在act()方法中运行组件。

三、表单

1)通信

  在表单中需要搜集控件的值,此时会涉及父子之间的通信。如果要在父组件中读取子组件的状态,那么有两种方式实现。

  第一种是通过事件对象,也就是在表单的Submit事件中读取事件对象的target属性,如下所示。

function submit(e) {
  console.log(e.target.name.value);     //读取控件值
  e.preventDefault();
}
return (
  <form id="appointment" name="appointment" className="form" onSubmit={submit}>
    <ul>
      <li className="ui-mb30 ui-flex ui-flex-column-center">
        <Input {...txt} />
      </li>
      <li className="ui-mb30 ui-flex ui-flex-column-center">
        <Input items={radios} />
      </li>
      <li className="ui-mb30 ui-flex ui-flex-column-center">
        <Input items={checkboxs} />
      </li>
    </ul>
  </form>
);

  第二种是通过回调函数,也就是在表单中传递一个函数到组件内。如果一个个传递,维护成本会巨大,而以高阶组件的方式,会简洁许多,如下所示。

//高阶组件
function HOC(Wrapped, data) {
  //子组件向父组件传递信息
  function callback(value) {
    data.currentValue = value;
  }
  function Enhanced(props) {
    return <Wrapped callback={callback} {...props} />;
  }
  return Enhanced;
}
const InputHOC = HOC(Input, txt),
  RadioHOC = HOC(Radio, radios),
  CheckboxHOC = HOC(Checkbox, checkboxs);

2)验证

  在控件中读取的值都得经过验证后,才能提交到服务器中。目前的做法比较粗暴,封装性和扩展性都不友好,将验证逻辑直接放置在Form组件中,如下所示,包含三组验证规则。

const methods = {
  required: function(value) {            //必填
    return value.length > 0 || false;
  },
  name: function(value) {                //姓名
    return /^[\u4e00-\u9fa5]+$/.test(value);
  },
  mobile: function(value) {                //手机号码验证
    return /^1[0-9]{10}$/.test(value);
  },
  checked: function(value = "", data) {
    if (!data.rules || !data.messages) {
      return true;
    }
    const rules = data.rules.split("|"),
      messages = data.messages.split("|"),
      length = rules.length;
    let i = 0;
    while (i < length) {
      if (this[rules[i]](value)) {
        i++;
        continue;
      }
      alert(messages[i]);
      return false;
    }
    return true;
  }
};

  需要验证的渲染数据会多两个属性:rules和messages,以竖线分隔,如下所示。

const txt = {
  type: "text",
  placeholder: "姓名",
  name: "name",
  rules: "required|name",
  messages: "请输入姓名|请输入正确的姓名"
};

3)Redux

  本来还尝试使用Redux来处理组件的状态,但还没有完全理解其精髓,在实际操作时有点力不从心。例如在Store.js容器文件中只是想处理状态,但是得引入要关联的Input组件(如下所示),这与我的初衷有偏差。

import React from "react";
import { createStore } from "redux";
import { connect, Provider } from "react-redux";
import { Input } from "../component/Input/index";

//Actions
function addName(name) {
  return {
    type: "ADD_NAME",
    name
  };
}
//Reducers
function caculate(previousState, action) {
  let state = Object.assign({}, previousState);
  switch (action.type) {
    case "ADD_NAME":
      state.name = action.name;
      break;
  }
  return state;
}

const store = createStore(caculate);
function mapStateToProps(state) {
  return state;
}
const SmartInput = connect(mapStateToProps, { addName })(Input);
const Store = (
  <Provider store={store}>
    <SmartInput />
  </Provider>
);
export default Store;

  Input组件也需要修改,在Change事件中回调传递过来的addName()方法,触发Redux更新状态。

export function Input(props) {
  function handle(e) {
    props.addName(e.target.value);
  }
  return <input className="input" onChange={handle} />;
}

  保存的状态会以Context方式传递,在接收时需要通过ReactReduxContext.Consumer读取。

import { ReactReduxContext } from ‘react-redux‘;
render() {
  return (
    <ReactReduxContext.Consumer>
      {({ store }) => {
        // do something with the store here
      }}
    </ReactReduxContext.Consumer>
  )
}

  在Form组件中可以像下面这样引用容器,但其实我只是想在当前读取到子组件的状态,目前的逻辑并没有解决该问题,有待修改。

import Store from ‘../../store/index‘;
function Form() {
  return (
    <form id="appointment" name="appointment" className="form" onSubmit={submit}>
      {Store}
    </form>
  );
}

四、Swiper

  Swiper是流行的触摸滑动插件,但没有找到官方的React版本。如果使用其它相关的React插件,需要增加集成的时间成本,并且还有未知BUG,可能影响上线时间,因此还是决定使用Swiper,进行二次封装,组件目录如下。

├── Swiper
│   ├── img ---------------------------- 图片
│   ├── index.js ----------------------- 脚本
│   ├── index.scss --------------------- 样式
│   ├── swiper.js ---------------------- 插件脚本
│   ├── swiper.scss -------------------- 插件样式

  在组件中引入了useEffect()钩子,并且将图像作为资源引入,如下代码所示。useEffect()会在componentDidMount()和componentDidUpdate()触发,此时DOM结构中已包含Swiper容器,可以将其初始化。

import React, { useEffect } from "react";
import "./index.scss";
import Swiper from "./swiper";
import img1 from "./img/1.png";
import img2 from "./img/2.png";
import img3 from "./img/3.png";

export default function ReactSwiper(props) {
  useEffect(() => {
    new Swiper("#slide", {
      loop: true
    });
  });
  return (
    <section className="ui-rel">
      <div id="slide" className="swiper-container">
        <article className="swiper-wrapper">
          <section className="swiper-slide">
            <img src={img1} className="img-slider" />
          </section>
          <section className="swiper-slide">
            <img src={img2} className="img-slider" />
          </section>
          <section className="swiper-slide">
            <img src={img3} className="img-slider" />
          </section>
        </article>
      </div>
    </section>
  );
}

五、三级联动

  省市经销商三级联动是本项目的一个特殊业务,类似于常规的省市区三级联动。三级联动的数据来源于一个静态文件,数据格式如下所示,保存在shops.js中。

export let poi = [ {
    name: "北京",
    citys: [ {
        name: "北京",
        dealer: [
          { dealerName: "北京汽车销售有限公司" },
          { dealerName: "北京商贸有限公司" }
        ]
    }]
}];

  Chain组件中的状态是一个对象,与之前不同,在调用更新函数时,不能直接传初始化的变量,得改用对象解构复制一个对象,再传这个副本,否则无法触发组件的渲染,如下所示,省略了部分逻辑代码和两个选择框。

import React, {useState} from ‘react‘;
import ‘./index.scss‘;
import {poi} from ‘./shops‘;

export default function Chain(props) {
  let initData = {
    provinces: [],
    province: "",
    cities: [],
    shops: [],
    poiHash: {}
  };
  initData.provinces = setOption(poi);
  poi.forEach(value => {
    initData.poiHash[value.proName] = value.citys;
  });

  const [data, setData] = useState(initData);
  function clearOptions(name) {
    data[name] = [];
    setData({ ...data });
  }
  function options(name, list) {
    clearOptions(name);
    data[name] = setOption(list);
    setData({ ...data });         //对象解构
    //setData(data);            //错误 无法触发渲染
  }
  function provinceChange(e) {
    var val = e.target.value;
    data.province = val;
    options("cities", data.poiHash[val]);
    clearOptions("shops");
  }

 return (
    <>
      <label className="select-container ui-mb20">
        <select onChange={provinceChange}>
          <option value="">省份</option>
          {data.provinces.map(item => (
            <option value={item.value} key={item.value}>
              {item.text}
            </option>
          ))};
        </select>
      </label>
      ......
    </>
  );
}

原文地址:https://www.cnblogs.com/strick/p/12017473.html

时间: 2024-10-05 00:14:16

响应国家号召,在家撸码之React迁移记的相关文章

响应国家号召 1+X 证书 Web 前端开发考试模拟题

1+x证书Web前端开发初级理论考试样题2019 http://blog.zh66.club/index.php/archives/149/ 1+x证书Web前端开发初级实操考试样题2019 http://blog.zh66.club/index.php/archives/150/ 1+x证书Web前端开发中级理论考试样题2019 http://blog.zh66.club/index.php/archives/151/ 1+x证书Web前端开发中级实操考试样题2019 http://blog.

听风讲MVC丶 —— 一言不合就撸码 (未完待续&#183;&#183;&#183;&#183;&#183;&#183;)

     希望你看了此小随 可以实现自己的MVC框架     也祝所有的程序员身体健康一切安好                                                                                                                                                ——久伴深海丶默 1.什么是前端控制器(font controller).Java Web中的前端控制器是应用的门面,

我和小美的撸码日记--基于MVC+Jqgrid的.Net快速开发框架

前言:以前的帐号没有发首页的权限,特此把这篇文章从另外一个博客移过来,这篇是<我和小美的撸码日记>的序 一转眼务农6年了,呆过大公司也去过小作坊,码农的人生除了抠腚还是抠腚.在所有呆过的公司里,感觉项目没有不延期的,真的是因为自己不努力吗?也没有呀!上班不怎么聊QQ回家也很少看动作片,还搞过几次通宵撸码的. 以前总感觉是项目经理把工时估少了,后来自己也做过项目管理,按照以往的经历估工时,做到最后还是会有些延期,要不就是加班拼命赶.我发现在项目中总是会遇到一些这样那样的问题,比如:客户需求变了,

HTTP 笔记与总结(2 )HTTP 协议的(请求行的)请求方法 及 (响应行的)状态码

(请求行的)请求方法 包括: GET,POST,HEAD,PUT,TRACE,DELETE,OPTIONS 注意:这些请求方法虽然是 HTTP 协议规定的,但是 Web Server 未必允许或支持这些方法. HEAD 和 GET 基本一致,只是不返回内容,比如只是确认一个内容还正常存在,不需要返回具体内容.演示: GET: HEAD: HEAD 方法只返回了响应的头信息. PUT,往服务器上的资源传输内容: 服务器未必支持 HTTP 协议所规定的方法. TRACE, 例如使用了代理上网(例如访

前端上路第三弹-正式撸码

又过了几个月没更新了,话说要当一名又撸码又撰文的FEer,真的挺难的,没那文学细胞,写不出多华丽的辞藻,每次写起来都像记流水账,不过记录自己的前端之路,不就是一步一步走过来的吗. 好了,废话不多说,上一篇讲解了如何利用markman对psd文件进行标注,接下来,万事俱备,只欠撸码咯~ ,这就是上次标注完成的图,但是我的习惯是当拿到标注完成的图时,并不会立即开始编写,我的习惯是想想具体的布局,然后看看有没有什么特别难实现的地方,正所谓磨刀不误砍柴工嘛. 对于这个效果图,我的想法分成上下两个部分,如

qemu-kvm-1.1.0源码中关于迁移的代码分析

Description Businesses like to have memorable telephone numbers. One way to make a telephone number memorable is to have it spell a memorable word or phrase. For example, you can call the University of Waterloo by dialing the memorable TUT-GLOP. Some

源码看React 事件机制

对React熟悉的同学都知道,React中的事件机制并不是原生的那一套,事件没有绑定在原生DOM上,发出的事件也是对原生事件的包装.那么这一切是怎么实现的呢? 事件注册 首先还是看我们熟悉的代码 <button onClick={this.autoFocus}>点击聚焦</button> 这是我们在React中绑定事件的常规写法.经由JSX解析,button会被当做组件挂载.而onClick这时候也只是一个普通的props.ReactDOMComponent在进行组件加载(moun

java自适应响应式 企业网站源码 SSM freemaker生成静态化 手机 平板 PC springmvc

java 企业网站源码 前后台都有 静态模版引擎, 代码生成器大大提高开发效率 前台: 支持两套模版, 可以在后台切换 系统介绍: 1.网站后台采用主流的 SSM 框架 jsp JSTL,网站后台采用freemaker静态化模版引擎生成html 2.因为是生成的html,所以访问速度快,轻便,对服务器负担小 3.网站前端采用主流的响应式布局,同一页面同时支持PC.平板.手机(三合一)浏览器访问 4.springmvc +spring4.2.5+ mybaits3.3  SSM 普通java we

web前端 | 如何选择撸码神器

进来,不少小伙子.小妹子来问一个家常便饭,但又逃不脱的问题: 小北哥哥,现在这么多编辑器,我该用哪一个好啊,看着都不孬啊(孬字用得好!) 此篇文章,纯客观分析 顺便吹逼,老鸟和大神直接略过吧!省的你们看到我写这个问题,再说我烦不烦啊,这问题都讨论了多少年了,甚至好多群里,一看到谁在讨论编辑器,就要送飞机票. 但很多转入前端的孩子还是很迷茫, 能力和悟性也分三六九等,人不同,自然适合自己的工具也要不同, 你不能你是个弱鸡,就学人家用VIM 或者直接文本编辑器?你这不找虐呢? 俗话说得好: 工欲善其