响应国家号召,在家撸码之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>
......
</>
);
}
响应国家号召,在家撸码之React迁移记的更多相关文章
- 响应国家号召 1+X 证书 Web 前端开发考试模拟题
1+x证书Web前端开发初级理论考试样题2019 http://blog.zh66.club/index.php/archives/149/ 1+x证书Web前端开发初级实操考试样题2019 http ...
- 响应国家号召,AI助力疫情防控!顶象AI防疫方案获得国家人工智能标准化总体组认可
当前,打赢新型冠状病毒感染的肺炎疫情是最重要的使命任务.而这场疫情的拉锯战,不仅要有全国人民共同努力.医护人员的无私奉献,还要积极运用现代科技的力量,用科学来战胜病魔.工信部也发文倡议:充分发挥人工智 ...
- 我和小美的撸码日记--基于MVC+Jqgrid的.Net快速开发框架
前言:以前的帐号没有发首页的权限,特此把这篇文章从另外一个博客移过来,这篇是<我和小美的撸码日记>的序 一转眼务农6年了,呆过大公司也去过小作坊,码农的人生除了抠腚还是抠腚.在所有呆过的公 ...
- HTTP 笔记与总结(2 )HTTP 协议的(请求行的)请求方法 及 (响应行的)状态码
(请求行的)请求方法 包括: GET,POST,HEAD,PUT,TRACE,DELETE,OPTIONS 注意:这些请求方法虽然是 HTTP 协议规定的,但是 Web Server 未必允许或支持这 ...
- 推荐几个IDEA插件,Java开发者撸码利器(转载)
推荐几个IDEA插件,Java开发者撸码利器. 这里只是推荐一下好用的插件,具体的使用方法不一一详细介绍. JRebel for IntelliJ 一款热部署插件,只要不是修改了项目的配置文件,用 ...
- HTML 请求头,响应头和 HTTP状态码
请求头 选项 说明 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8 告诉服务器,当前客户端可以接收的文档类型 ...
- Intellij IDEA 撸码最头大的问题。。
想栈长我当初从 Eclipse 转用 IDEA 真是纠结,放弃然后尝试了N次,不过现在已经算是转型成功了,可以完全脱离 Eclipse 撸码了,虽然说我现在真的撸得非常少了.. 说到 IDEA 的痛点 ...
- Vue3中的响应式对象Reactive源码分析
Vue3中的响应式对象Reactive源码分析 ReactiveEffect.js 中的 trackEffects函数 及 ReactiveEffect类 在Ref随笔中已经介绍,在本文中不做赘述 本 ...
- 元旦在家撸了两天Seata源码,你们是咋度过的呢?
撸Seata源码 2020年12月31日晚23点30分,我发了2020年的最后一个朋友圈:假期吃透Seata源码,有组队的吗? 不少小伙伴都来点赞了, 其中也包括Seata项目的发起人--季敏大佬哦! ...
随机推荐
- Windows 服务安装与卸载 (通过 Sc.exe)
1. 安装 新建文本文件,重命名为 ServiceInstall.bat,将 ServiceInstall.bat 的内容替换为: sc create "Verity Platform De ...
- 【Linux】awk笔记
awk是一种处理文本文件的语言,是一个强大的文本分析工具. 实例 ①显示文件行中匹配项 # 每行按空格或TAB分割,输出文本中的1.4项 yunduo@yunduo-ThinkCentre-XXXX: ...
- ApkTool工具
ApkTool: 一款很好的反编译工具,支持Linux和Windows. 如何使用: 1:需要一个JAVA环境.由于之前已经装过JAVA 相关JDK,JRE,不赘述. 2:下载ApkTool工具: ...
- 什么时候用for循环什么时候用while循环?
简述 for循环和while循环最大的区别在于[循环的工作量是否确定],for循环就像空房间依次办理业务,直到把[所有工作做完]才下班.但while循环就像哨卡放行,[满足条件就一直工作],直到不满足 ...
- RobotFramework+Appium 为了兼容iOS12,升级至Xcode10后,WebDriverAgent编译不通过:Undefind symbols for architecture x86_64
报错信息如下: Undefined symbols for architecture arm64: "_OBJC_CLASS_$_XCElementSnapshot", refer ...
- Visioi形状相关应用
选择手柄为白点 按住shift的同时移动白点更为灵活 黄色的点就是控制手柄(只有一维图形有) 当调整形状出现绿色边的时候说明:这个时候这个形状的边等于了某个形状的长 铅笔工具可以移动控制点来更形状 ...
- 0016 CSS 背景:background
目标 理解 背景的作用 css背景图片和插入图片的区别 应用 通过css背景属性,给页面元素添加背景样式 能设置不同的背景图片位置 [插入图片,不用设置img元素的父元素.自身元素大小,即可见,但是背 ...
- spring整合springMVC、mybatis、shiro
jar包: <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding& ...
- $POJ2411\ Mondriaan's\ Dream$ 状压+轮廓线$dp$
传送门 Sol 首先状压大概是很容易想到的 一般的做法大概就是枚举每种状态然后判断转移 但是这里其实可以轮廓线dp 也就是从上到下,从左到右地放方块 假设我们现在已经放到了$(i,j)$这个位置 那么 ...
- 啊哈!C语言课后参考答案上
最近看到一本好评量很高的的C语言入门书,课本真的很好,入门的话.专业性没有那么强,但入门足够了!!好评!看着看着就想把这本书的题课后习题都写出来,最后就有了这个小结.可能有的不是最好,不那么专业,但主 ...