一、写在前面

这是一个真实的项目,项目已经过去好久了,虽然很简单,但还是有很多思考点,跟随着笔者的脚步,一起来看看吧。本文纯属虚构,涉及到的相关信息均已做虚构处理,

二、背景

人活着一定要有信仰,没有信仰的灵魂是空洞的。你可以信耶稣,信佛,信伊斯兰,信科学等等。为了管控各大宗教场所的人员聚集,为社会增添一份绵薄之力,京州领导决定做一个表单系统来统计某个时间或者时间段的人员访问量,控制宗教人员活动的范围,汉东省委沙瑞金书记特别关心这件事决定亲自检查,几经周转,这个任务落到了程序员江涛的头上,故事由此展开。

三、需求分析

大致需要实现如下功能

  • 表单数据的录入
  • 录入数据的最近记录查询
  • 短信验证码的使用
  • 扫码填写表单信息

有两种方案, 一种是进去自己选择对应的宗教场所(不对称分布三级联动),第二种是点击对应的宗教场所进行填写表单,表单处的场所不可更改,不同的设计不同的思路。 虽然两种都写了, 但这里我就按第二种写这篇文章,如果有兴趣了解第一种欢迎与我交流。

四、系统设计

这次我决定不用vue,改用react的taro框架写这个小项目(试一下多端框架taro哈哈), 后端这边打算用nodejs的eggjs框架, 数据库还是用mysql, 还会用到redis。由于服务器端口限制,搞不动docker啊, 也没有nginx,莫得关系,egg自带web服务器将就用一下项目也就做完了,就这样taro和egg的试管婴儿诞生了。

五、代码实现

额,东西又多又杂,挑着讲吧, 建议结合这两篇篇文章一起看, 基于Vue.js和Node.js的反欺诈系统设计与实现 https://www.cnblogs.com/cnroadbridge/p/15182552.html, 基于React和GraphQL的demo设计与实现 https://www.cnblogs.com/cnroadbridge/p/15318408.html

5.1 前端实现

taroJS的安装使用参见https://taro-docs.jd.com/taro/docs/GETTING-STARTED

5.1.1 整体的布局设计

主要还是头部和其他这种布局,比较简单,然后抽离出一个公共组件header,给它抛出一个可以跳转链接的方法, 逻辑很简单就是一个标题,然后后面有一个返回首页的图标

import { View, Text } from '@tarojs/components';
import { AtIcon } from 'taro-ui'
import "taro-ui/dist/style/components/icon.scss"; import 'assets/iconfont/iconfont.css'
import './index.scss' import { goToPage } from 'utils/router.js' export default function Header(props) {
return (
<View className='header'>
<Text className='header-text'>{ props.title }</Text>
<Text onClick={() => goToPage('index')}>
<AtIcon prefixClass='icon' className='iconfont header-reback' value='home' color='#6190e8'></AtIcon>
</Text>
</View>
)
}

关于这一块,还可以看下components下的card组件的封装

5.1.2 表单的设计

表单设计这块,感觉也没啥好讲的,主要是你要写一些css去适配页面,具体的逻辑实现代码如下:

import Taro, { getCurrentInstance } from '@tarojs/taro';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { update } from 'actions/form';
import { View, Text, RadioGroup, Radio, Label, Picker } from '@tarojs/components';
import { AtForm, AtInput, AtButton, AtTextarea, AtList, AtListItem } from 'taro-ui';
import Header from 'components/header' import 'taro-ui/dist/style/components/input.scss';
import 'taro-ui/dist/style/components/icon.scss';
import 'taro-ui/dist/style/components/button.scss';
import 'taro-ui/dist/style/components/radio.scss';
import 'taro-ui/dist/style/components/textarea.scss';
import 'taro-ui/dist/style/components/list.scss';
import "taro-ui/dist/style/components/loading.scss";
import './index.scss'; import cityData from 'data/city.json';
import provinceData from 'data/province.json'; import { goToPage } from 'utils/router';
import { request } from 'utils/request'; @connect(({ form }) => ({
form
}), (dispatch) => ({
updateForm (data) {
dispatch(update(data))
}
})) export default class VisitorRegistration extends Component { constructor (props) {
super(props);
this.state = {
title: '预约登记', // 标题
username: '', // 姓名
gender: '', // 性别
mobile: '', // 手机
idcard: '', // 身份证
orgin: '', //访客来源地
province: '', //省
city: '', // 市
place: '', //宗教地址
religiousCountry: '', // 宗教县区
religiousType: '', // 宗教类型
matter: '', // 来访事由
visiteDate: '', // 拜访日期
visiteTime: '', // 拜访时间
leaveTime: '', // 离开时间
genderOptions: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
], // 性别选项
genderMap: { male: '男', female: '女' },
timeRangeOptions: [
'00:00-02:00',
'02:00-04:00',
'04:00-06:00',
'06:00-08:00',
'08:00-10:00',
'10:00-12:00',
'12:00-14:00',
'14:00-16:00',
'16:00-18:00',
'18:00-20:00',
'20:00-22:00',
'22:00-24:00',
], // 时间选项
orginRangeOptions: [[],[]], // 省市选项
orginRangeKey: [0, 0],
provinces: [],
citys: {},
isLoading: false,
}
this.$instance = getCurrentInstance()
Taro.setNavigationBarTitle({
title: this.state.title
})
} async componentDidMount () {
console.log(this.$instance.router.params)
const { place } = this.$instance.router.params;
const cityOptions = {};
const provinceOptions = {};
const provinces = [];
const citys = {};
provinceData.forEach(item => {
const { code, name } = item;
provinceOptions[code] = name;
provinces.push(name);
})
for(const key in cityData) {
cityOptions[provinceOptions[key]] = cityData[key];
citys[provinceOptions[key]] = [];
for (const item of cityData[key]) {
if (item.name === '直辖市') {
citys[provinceOptions[key]].push('');
} else {
citys[provinceOptions[key]].push(item.name);
}
}
}
const orginRangeOptions = [provinces, []] await this.setState({
provinces,
citys,
orginRangeOptions,
place
});
} handleOriginRangeChange = event => {
let { value: [ k1, k2 ] } = event.detail;
const { provinces, citys } = this.state;
const province = provinces[k1];
const city = citys[province][k2];
const orgin = `${province}${city}`;
this.setState({
province,
city,
orgin
})
} handleOriginRangleColumnChange = event => {
let { orginRangeKey } = this.state;
let changeColumn = event.detail;
let { column, value } = changeColumn;
switch (column) {
case 0:
this.handleRangeData([value, 0]);
break;
case 1:
this.handleRangeData([orginRangeKey[0], value]);
}
} handleRangeData = orginRangeKey => {
const [k0] = orginRangeKey;
const { provinces, citys } = this.state;
const cityOptions = citys[provinces[k0]]
const orginRangeOptions = [provinces, cityOptions];
this.setState({
orginRangeKey,
orginRangeOptions
})
} handleChange (key, value) {
this.setState({
[key]: value
})
return value;
} handleDateChange(key, event) {
const value = event.detail.value;
this.setState({
[key]: value
})
return value;
} handleClick (key, event) {
const value = event.target.value;
this.setState({
[key]: value
})
return value;
} handleRadioClick (key, value) {
this.setState({
[key]: value
})
return value;
} async onSubmit (event) {
const {
username,
gender,
mobile,
idcard,
orgin,
province,
city,
place,
religiousCountry,
religiousType,
visiteDate,
visiteTime,
leaveTime,
matter,
genderMap,
} = this.state; if (!username) {
Taro.showToast({
title: '请填写用户名',
icon: 'none',
duration: 2000
})
return;
} else if (!gender) {
Taro.showToast({
title: '请选择性别',
icon: 'none',
duration: 2000
})
return;
} else if (!mobile || !/^1(3[0-9]|4[579]|5[012356789]|66|7[03678]|8[0-9]|9[89])\d{8}$/.test(mobile)) {
Taro.showToast({
title: '请填写正确的手机号',
icon: 'none',
duration: 2000
})
return;
} else if (!idcard || !/^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/.test(idcard)) {
Taro.showToast({
title: '请填写正确的身份证号',
icon: 'none',
duration: 2000
})
return;
} else if (!orgin) {
Taro.showToast({
title: '请选择来源地',
icon: 'none',
duration: 2000
})
return;
} else if (!place) {
Taro.showToast({
title: '请选择宗教场所',
icon: 'none',
duration: 2000
})
return;
} else if (!visiteDate) {
Taro.showToast({
title: '请选择预约日期',
icon: 'none',
duration: 2000
})
return;
} else if (!visiteTime) {
Taro.showToast({
title: '请选择预约时间',
icon: 'none',
duration: 2000
})
return;
} await this.setState({
isLoading: true
}) const data = {
username,
gender: genderMap[gender],
mobile,
idcard,
orgin,
province,
city,
place,
religiousCountry,
religiousType,
visiteDate,
visiteTime,
leaveTime,
matter,
}; const { data: { code, status, data: formData }} = await request({
url: '/record',
method: 'post',
data
}); await this.setState({
isLoading: false
}); if (code === 0 && status === 200 && data) {
Taro.showToast({
title: '预约成功',
icon: 'success',
duration: 2000,
success: () => {
// goToPage('result-query', {}, (res) => {
// res.eventChannel.emit('formData', { data: formData })
// })
this.props.updateForm(formData)
goToPage('result-query')
}
});
} else {
Taro.showToast({
title: '预约失败',
icon: 'none',
duration: 2000
})
return;
}
} handlePickerChange = (key, optionName, event) => {
const options = this.state[optionName];
this.setState({
[key]: options[event.detail.value]
})
} render() {
const { title,
username,
genderOptions,
mobile,
idcard,
visiteTime,
timeRangeOptions,
leaveTime,
matter,
visiteDate,
orgin,
orginRangeOptions,
orginRangeKey,
place,
isLoading
} = this.state;
return (
<View className='visitor-registration'>
<Header title={title}/>
<AtForm
onSubmit={this.onSubmit.bind(this)}
>
<View className='row'>
<AtInput
required
type='text'
name='username'
className='col'
title='访客姓名'
placeholder='请输入访客姓名'
value={username}
onChange={(value) => {this.handleChange('username', value)}}
/>
</View>
<View className='row'>
<View className='col at-input'>
<Text className='at-input__title at-input__title--required'>
性别
</Text>
<View className='at-input__input'>
<RadioGroup>
{genderOptions.map((genderOption, i) => {
return (
<Label for={i} key={i}>
<Radio
value={genderOption.value}
onClick={(event) => {this.handleRadioClick('gender', genderOption.value)}}>
{genderOption.label}
</Radio>
</Label>
)
})}
</RadioGroup>
</View>
</View>
</View>
<View className='row'>
<AtInput
required
type='phone'
name='mobile'
title='手机号码'
className='col'
placeholder='请输入手机号码'
value={mobile}
onChange={(value) => {this.handleChange('mobile', value)}}
/>
</View>
<View className='row'>
<AtInput
required
name='idcard'
type='idcard'
className='col'
title='身份证号'
placeholder='请输入身份证号码'
value={idcard}
onChange={(value) => {this.handleChange('idcard', value)}}
/>
</View>
<View className='row'>
<View className='at-input col col-fix'>
<Text className='at-input__title at-input__title--required'>
来源地
</Text>
<Picker mode='multiSelector'
onChange={(event) => this.handleOriginRangeChange(event)}
onColumnChange={(event) => this.handleOriginRangleColumnChange(event)}
range={orginRangeOptions}
value={orginRangeKey}>
<AtList>
{orgin ? (
<AtListItem
className='at-list__item-fix'
extraText={orgin}
/>) : (<Text className='input-placeholder-fix'>请选择访客来源地</Text>)}
</AtList>
</Picker>
</View>
</View>
<View className='row'>
<AtInput
required
type='text'
name='place'
className='col'
title='宗教场所'
disabled
placeholder='请选择宗教场所'
value={place}
onChange={(value) => {this.handleChange('place', value)}}
/>
</View>
<View className='row'>
<View className='at-input col col-fix'>
<Text className='at-input__title at-input__title--required'>
预约日期
</Text>
<Picker mode='date'
onChange={(event) => this.handleDateChange('visiteDate', event)}>
<AtList>
{visiteDate ? (
<AtListItem
className='at-list__item-fix'
extraText={visiteDate}
/>) : (<Text className='input-placeholder-fix'>请选择预约日期</Text>)}
</AtList>
</Picker>
</View>
</View>
<View className='row'>
<View className='at-input col col-fix'>
<Text className='at-input__title at-input__title--required'>
预约时间
</Text>
<Picker mode='selector'
range={timeRangeOptions}
onChange={(event) => this.handlePickerChange('visiteTime', 'timeRangeOptions', event)}>
<AtList>
{visiteTime ? (
<AtListItem
className='at-list__item-fix'
extraText={visiteTime}
/>) : (<Text className='input-placeholder-fix'>请选择预约时间</Text>)}
</AtList>
</Picker>
</View>
</View>
<View className='row'>
<View className='at-input col col-fix'>
<Text className='at-input__title'>
离开时间
</Text>
<Picker mode='selector'
range={timeRangeOptions}
onChange={(event) => this.handlePickerChange('leaveTime', 'timeRangeOptions', event)}>
<AtList>
{leaveTime ? (
<AtListItem
className='at-list__item-fix'
extraText={leaveTime}
/>) : (<Text className='input-placeholder-fix'>请选择离开时间</Text>)}
</AtList>
</Picker>
</View>
</View>
<View className='row'>
<View className='col at-input'>
<Text className='at-input__title'>
来访事由
</Text>
<AtTextarea
maxLength={200}
className='textarea-fix'
value={matter}
onChange={(value) => {this.handleChange('matter', value)}}
placeholder='请输入来访事由...'
/>
</View>
</View>
<View className='row'>
<AtButton
circle
loading={isLoading}
disabled={isLoading}
type='primary'
size='normal'
formType='submit'
className='col btn-submit'>
提交
</AtButton>
</View>
</AtForm>
</View>
);
}
}

5.1.3 短信验证码的设计实现

这里也可以单独抽离出一个组件,主要的点在于,点击后的倒计时和重新发送,可以重点看下,具体的实现逻辑如下:

import Taro from '@tarojs/taro';
import { Component } from 'react';
import { View, Text } from '@tarojs/components';
import { AtInput, AtButton } from 'taro-ui'; import 'taro-ui/dist/style/components/input.scss';
import 'taro-ui/dist/style/components/button.scss';
import './index.scss'; const DEFAULT_SECOND = 120;
import { request } from 'utils/request'; export default class SendSMS extends Component { constructor(props) {
super(props);
this.state = {
mobile: '', // 手机号
confirmCode: '', // 验证码
smsCountDown: DEFAULT_SECOND,
smsCount: 0,
smsIntervalId: 0,
isClick: false,
};
} componentDidMount () { } componentWillUnmount () {
if (this.state.smsIntervalId) {
clearInterval(this.state.smsIntervalId);
this.setState(prevState => {
return {
...prevState,
smsIntervalId: 0,
isClick: false
}
})
}
} componentDidUpdate (prevProps, prveState) {
} componentDidShow () { } componentDidHide () { } handleChange (key, value) {
this.setState({
[key]: value
})
return value;
} processSMSRequest () {
const { mobile } = this.state;
if (!mobile || !/^1(3[0-9]|4[579]|5[012356789]|66|7[03678]|8[0-9]|9[89])\d{8}$/.test(mobile)) {
Taro.showToast({
title: '请填写正确的手机号',
icon: 'none',
duration: 2000
})
return;
}
this.countDown()
} sendSMS () {
const { mobile } = this.state;
request({
url: '/sms/send',
method: 'post',
data: { mobile }
}, false).then(res => {
console.log(res);
const { data: { data: { description } } } = res;
Taro.showToast({
title: description,
icon: 'none',
duration: 2000
})
}).catch(err => {
console.log(err);
});
} countDown () {
if (this.state.smsIntervalId) {
return;
}
const smsIntervalId = setInterval(() => {
const { smsCountDown } = this.state;
if (smsCountDown === DEFAULT_SECOND) {
this.sendSMS();
}
this.setState({
smsCountDown: smsCountDown - 1,
isClick: true
}, () => {
const { smsCount, smsIntervalId, smsCountDown } = this.state;
if (smsCountDown <= 0) {
this.setState({
smsCountDown: DEFAULT_SECOND,
})
smsIntervalId && clearInterval(smsIntervalId);
this.setState(prevState => {
return {
...prevState,
smsIntervalId: 0,
smsCount: smsCount + 1,
}
})
}
})
}, 1000);
this.setState({
smsIntervalId
})
} submit() {
// 校验参数
const { mobile, confirmCode } = this.state;
if (!mobile || !/^1(3[0-9]|4[579]|5[012356789]|66|7[03678]|8[0-9]|9[89])\d{8}$/.test(mobile)) {
Taro.showToast({
title: '请填写正确的手机号',
icon: 'none',
duration: 2000
})
return;
} else if (confirmCode.length !== 6) {
Taro.showToast({
title: '验证码输入有误',
icon: 'none',
duration: 2000
})
return;
}
this.props.submit({ mobile, code: confirmCode });
} render () {
const { mobile, confirmCode, smsCountDown, isClick } = this.state; return (
<View className='sms-box'>
<View className='row-inline'>
<AtInput
required
type='phone'
name='mobile'
title='手机号码'
className='row-inline-col-7'
placeholder='请输入手机号码'
value={mobile}
onChange={(value) => {this.handleChange('mobile', value)}}
/>
{!isClick ? ( <Text
onClick={() => this.processSMSRequest()}
className='row-inline-col-3 at-input__input code-fix'>
发送验证码
</Text>) : ( <Text
onClick={() => this.processSMSRequest()}
className='row-inline-col-3 at-input__input code-fix red'>
{( smsCountDown === DEFAULT_SECOND ) ? '重新发送' : `${smsCountDown}秒后重试`}
</Text>)}
</View>
<View>
<AtInput
required
type='text'
name='confirmCode'
title='验证码'
placeholder='请输入验证码'
value={confirmCode}
onChange={(value) => {this.handleChange('confirmCode', value)}}
/>
</View>
<View>
<AtButton
circle
type='primary'
size='normal'
onClick={() => this.submit()}
className='col btn-submit'>
查询
</AtButton>
</View>
</View>
)
}
}

5.1.4 前端的一些配置

路由跳页模块的封装

import Taro from '@tarojs/taro';

// https://taro-docs.jd.com/taro/docs/apis/route/navigateTo
export const goToPage = (page, params = {}, success, events) => {
let url = `/pages/${page}/index`;
if (Object.keys(params).length > 0) {
let paramsStr = '';
for (const key in params) {
const tmpStr = `${key}=${params[key]}`;
paramsStr = tmpStr + '&';
}
if (paramsStr.endsWith('&')) {
paramsStr = paramsStr.substr(0, paramsStr.length - 1);
}
if (paramsStr) {
url = `${url}?${paramsStr}`;
}
}
Taro.navigateTo({
url,
success,
events
});
};

请求方法模块的封装

import Taro from '@tarojs/taro';
const baseUrl = 'http://127.0.0.1:9000'; // 请求的地址 export function request(options, isLoading = true) {
const { url, data, method, header } = options;
isLoading &&
Taro.showLoading({
title: '加载中'
});
return new Promise((resolve, reject) => {
Taro.request({
url: baseUrl + url,
data: data || {},
method: method || 'GET',
header: header || {},
success: res => {
resolve(res);
},
fail: err => {
reject(err);
},
complete: () => {
isLoading && Taro.hideLoading();
}
});
});
}

日期格式的封装

import moment from 'moment';

export const enumerateDaysBetweenDates = function(startDate, endDate) {
let daysList = [];
let SDate = moment(startDate);
let EDate = moment(endDate);
let xt;
daysList.push(SDate.format('YYYY-MM-DD'));
while (SDate.add(1, 'days').isBefore(EDate)) {
daysList.push(SDate.format('YYYY-MM-DD'));
}
daysList.push(EDate.format('YYYY-MM-DD'));
return daysList;
}; export const getSubTractDate = function(n = -2) {
return moment()
.subtract(n, 'months')
.format('YYYY-MM-DD');
};

阿里妈妈图标库引入, 打开https://www.iconfont.cn/ ,找到喜欢的图表下载下来, 然后引入,在对应的地方加上iconfont和它对应的样式类的值

import { View, Text } from '@tarojs/components';
import { AtIcon } from 'taro-ui'
import "taro-ui/dist/style/components/icon.scss"; import 'assets/iconfont/iconfont.css'
import './index.scss' import { goToPage } from 'utils/router.js' export default function Header(props) {
return (
<View className='header'>
<Text className='header-text'>{ props.title }</Text>
<Text onClick={() => goToPage('index')}>
<AtIcon prefixClass='icon' className='iconfont header-reback' value='home' color='#6190e8'></AtIcon>
</Text>
</View>
)
}

redux的使用,这里主要是多页面共享数据的时候用了下,核心代码就这点

import { UPDATE } from 'constants/form';

const INITIAL_STATE = {
city: '',
createTime: '',
gender: '',
id: '',
idcard: '',
leaveTime: '',
matter: '',
mobile: '',
orgin: '',
place: '',
province: '',
religiousCountry: '',
religiousType: '',
updateTime: '',
username: '',
visiteDate: '',
visiteTime: ''
}; export default function form(state = INITIAL_STATE, action) {
switch (action.type) {
case UPDATE:
return {
...state,
...action.data
};
default:
return state;
}
}

使用方法如下

@connect(({ form }) => ({
form
}), (dispatch) => ({
updateForm (data) {
dispatch(update(data))
}
}))
  componentWillUnmount () {
const { updateForm } = this.props;
updateForm({
city: '',
createTime: '',
gender: '',
id: '',
idcard: '',
leaveTime: '',
matter: '',
mobile: '',
orgin: '',
place: '',
province: '',
religiousCountry: '',
religiousType: '',
updateTime: '',
username: '',
visiteDate: '',
visiteTime: ''
})
}

开发环境和生成环境的打包配置, 因为最后要合到egg服务里面,所以这里生产环境的publicPath和baseName都应该是 /public

module.exports = {
env: {
NODE_ENV: '"production"'
},
defineConstants: {},
mini: {},
h5: {
/**
* 如果h5端编译后体积过大,可以使用webpack-bundle-analyzer插件对打包体积进行分析。
* 参考代码如下:
* webpackChain (chain) {
* chain.plugin('analyzer')
* .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
* }
*/
publicPath: '/public',
router: {
basename: '/public'
}
}
};

开发环境名字可自定义如:

module.exports = {
env: {
NODE_ENV: '"development"'
},
defineConstants: {},
mini: {},
h5: {
publicPath: '/',
esnextModules: ['taro-ui'],
router: {
basename: '/religion'
}
}
};

5.2 后端实现

后端这块,其他的都没啥好讲的,具体可以参看我之前写的两篇文章,或者阅读源码,这里着重讲下防止短信验证码恶意注册吧。

5.2.1 如何防止短信验证码对恶意使用

这个主要是在于用的是内部实现的短信验证码接口(自家用的),不是市面上一些成熟的短信验证码接口,所以在预发布阶段安全方面曾经收到过一次攻击(包工头家的服务器每天都有人去攻击,好巧不巧刚被我撞上了),被恶意使用了1W条左右短信,痛失8张毛爷爷啊。总结了下这次教训,主要是从IP、发送的频率、以及加上csrf Token去预防被恶意使用。

大致是这样搞得。

安装相对于的类库

"egg-ratelimiter": "^0.1.0",
"egg-redis": "^2.4.0",

config/plugin.js下配置

 ratelimiter: {
enable: true,
package: 'egg-ratelimiter',
},
redis: {
enable: true,
package: 'egg-redis',
},

config/config.default.js下配置

 config.ratelimiter = {
// db: {},
router: [
{
path: '/sms/send',
max: 5,
time: '60s',
message: '卧槽,你不讲武德,老是请求干嘛干嘛干嘛!',
},
],
}; config.redis = {
client: {
port: 6379, // Redis port
host: '127.0.0.1', // Redis host
password: null,
db: 0,
},
};

效果是这样的

六、参考文献

七、写在最后

到这里就要和大家说再见了, 通过阅读本文,对于表单的制作你学会了吗?欢迎在下方发表你的看法,也欢迎和笔者交流!

github项目地址:https://github.com/cnroadbridge/jingzhou-religion

gitee项目地址: https://gitee.com/taoge2021/jingzhou-religion

基于React和Node.JS的表单录入系统的设计与实现的更多相关文章

  1. iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 代码分层

    视频地址:https://www.cctalk.com/v/15114923889408 文章 在前面几节中,我们已经实现了项目中的几个常见操作:启动服务器.路由中间件.Get 和 Post 形式的请 ...

  2. 手把手教你webpack、react和node.js环境配置(下篇)

    上篇我介绍了前端下webpack和react.redux等环境的配置,这篇将继续重点介绍后台node.js的配置. 这里是上篇链接:手把手教你webpack.react和node.js环境配置(上篇) ...

  3. iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 处理静态资源

    视频地址:https://www.cctalk.com/v/15114923882788 处理静态资源 无非花开花落,静静. 指定静态资源目录 这里我们使用第三方中间件: koa-static 安装并 ...

  4. iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 视图Nunjucks

    视频地址:https://www.cctalk.com/v/15114923888328 视图 Nunjucks 彩虹是上帝和人类立的约,上帝不会再用洪水灭人. 客户端和服务端之间相互通信,传递的数据 ...

  5. 使用React、Node.js、MongoDB、Socket.IO开发一个角色投票应用的学习过程(三)

    这几篇都是我原来首发在 segmentfault 上的地址:https://segmentfault.com/a/1190000005040834 突然想起来我这个博客冷落了好多年了,也该更新一下,呵 ...

  6. js验证表单大全

    js验证表单大全 1. 长度限制 <script> function test() { if(document.a.b.value.length>50) { alert(" ...

  7. JS 更改表单的提交时间和Input file的样式

    JS转换时间 function renderTime(data) { var da = eval('new ' + data.replace('/', '', 'g').replace('/', '' ...

  8. 手把手教你webpack、react和node.js环境配置(上篇)

    很多人刚学习react的时候,往往因为繁琐的配置而头疼,这里我将手把手教大家怎么用webpack配置react和redux的环境,这篇教程包括前端react和后台node整个网站的环境配置,对node ...

  9. 基于 Koa平台Node.js开发的KoaHub.js的控制器,模型,帮助方法自动加载

    koahub-loader koahub-loader是基于 Koa平台Node.js开发的KoaHub.js的koahub-loader控制器,模型,帮助方法自动加载 koahub loader I ...

随机推荐

  1. PyTools-包罗万象的python工具包

    PyTools-包罗万象的python工具包 <---点击这里获取代码,欢迎star. 自己平时写的代码都以函数方式封装起来了,方便代码复用. _________ ________ ______ ...

  2. UE4之Slate: App启动与最外层Runtime结构

    UE4版本:4.24.3源码编译: Windows10 + VS开发环境 Slate为一套自定义UI框架,其绘制直接依赖的是OpenGL.DirectX这样的硬件加速AIP;可以理解为一个单独的2D图 ...

  3. Flume(二)【入门】

    目录 一.安装部署 1.安装地址 2.安装步骤 二.入门案例 1.官方案例(nestat->logger) 2.实时监控单个追加文件(exec->hdfs) 3.实时监控目录下多个新文件( ...

  4. 13个酷炫的JavaScript一行程序

    1. 获得一个随机的布尔值(true/false) const randomBoolean = () => Math.random() >= 0.5; console.log(random ...

  5. android 获取uri的正确文件路径的办法

    private String getRealPath( Uri fileUrl ) { String fileName = null; if( fileUrl != null ) { if( file ...

  6. 转Android Canvas和Paint基本使用

    Android Canvas和Paint基本使用   这篇文章主要介绍下画笔Paint和画布Canvas的基本使用  1.Paint 创建对象Paint mPaint = new Paint(); 常 ...

  7. When does compiler create default and copy constructors in C++?

    In C++, compiler creates a default constructor if we don't define our own constructor (See this). Co ...

  8. 神器Tampermonkey的安装使用

    Tampermonkey是一款基于浏览器的神奇插件,在国内称为油猴,开发者可以在上面开发满足自己需求的各类浏览器应用脚本.不过经过全球各地无数开发者数年的积累现在其官网已经有一大把的优秀的现成脚本,完 ...

  9. 【Matlab】CFAR/phased.CFARDetector2D

    | CFAR学习进行时ing... | CFAR原理.参数 检测阈值\(T = αP_n\) \(P_n\)是噪声功率估计,\(α\)是比例因子 训练单元:训练噪声,估计\(P_n = \frac{1 ...

  10. C#中继承和多态

    1.继承的概念 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用已存在的类的功能. 为了提高软件模块的可复用性和可扩充性,以便提高软件的开发效率,我们总 ...