最近一直在做H5公众号的需求,使用的技术栈如标题,从立项目到稳定增长阶段,前端使用React随着业务需求从0到1把项目搭建起来,get了很多React+H5+微信场景下的知识点,把最近的实践总结下,有需要使用React上手微信H5的可以参考,大家一起交流哈😀。
本文特点:
- React+H5+微信场景全流程。
- Vue转React入门参考。
项目简介:
轿车物流平台,可以通过公众号在线完成轿车运输下单业务。
- 线路价格查询
- 下单信息填写
- 地址簿管理
- 实名认证
- 银行卡绑定
- 优惠券
- 微信支付
- 活动海报
正文
- 区分Umi与Dva
- Umi配置
- React常用写法
- Dva使用入门
- 微信配置
- TypeScript组件
- H5小技巧
- 日志记录
- 其他
1. 区分Umi与Dva
刚从Vue转React时,很容易分不清楚Umi和Dva,从官网文档看都自称为应用框架,(大佬跳过)。
Umi
不严谨的说:可以理解为类Vue-cli脚手架,帮你生成了带router的项目模板,开箱即用。整合了router + antd,还有一些国际化、配置式路由等功能深度整合的,Umi文档。
Dva
不严谨的说:可以理解为类Vuex的状态管理库。整合了 redux + reduxsaga,Dva文档。
为什么是Umi + Dva
在用Vue开发时,我们更倾向于vue-cli创建模板、Vuex状态管理。
而且Umi比React官方脚手架create-react-app更强大、更贴近业务场景,开箱即用。
React在状态管理上有redux,不够好用又出现redux-saga,最后被Dva打包成更好用的数据流方案。
参考《一篇文章总结redux、react-redux、redux-saga》。
2. Umi配置
Umi配置很强大,列一下用到的几个重要配置,详细配置可参见官网配置。
环境变量
前端根据环境变量打包不同环境代码,如Api地址、静态资源地址等。
| 分支 | 打包脚本 | 环境 | Api地址 | 静态资源地址 |
|---|---|---|---|---|
| Maser | build:test | 测试 | fat.*.com | cdn.fat.*.com |
| Develop | build:prod | 正式 | *.com | cdn.*.com |
// package.json
{
"scripts": {
"start": "cross-env APP_TYPE=site BUILD_ENV=dev PORT=80 umi dev",
"build": "umi build",
"build:test": "BUILD_ENV=test umi build",
"build:prod": "BUILD_ENV=prod umi build",
},
}
umi-plugin-react配置
是官方的一个针对react的插件,页面按需加载、rem适配在这里配置。
dynamicImport指定进入页面时的loading组件。hd开启rem方案
微信开发务必关掉
pwa选项,否则导致上线后缓存严重。
其他配置
PostCSS/按需加载/主题/proxy代理等。
代码
import pageRoutes from './router.config';
import theme from '../src/theme';
import webpackPlugin from './plugin.config';
const plugins = [
[
'umi-plugin-react',
{
antd: true,
dva: {
hmr: true,
},
dynamicImport: { //
loadingComponent: './components/PageLoading/index',
webpackChunkName: true,
},
pwa: false,
title: {
defaultTitle: '默认标题',
},
dll: false,
hd: true,
fastClick: false,
routes: {
exclude: [],
},
hardSource: false,
},
],
];
const env = process.env.BUILD_ENV,
publicPath = {
"dev": "",
"test": "//*.fat.*.com/",
"prod": "//*.*.com/"
}[env];
const apiPath = {
"dev": 'http://*.feature.*.com/api',
"test": 'http://*.fat.*.com/api',
"prod": 'https://*.*.com/api'
}[env];
export default {
base: '/',
publicPath: publicPath,
define: {
APP_TYPE: process.env.APP_TYPE || '',
apiPath: apiPath || '',
},
// history: 'hash', // 默认是 browser
plugins,
routes: pageRoutes,
theme: { // 主题
'brand-primary': theme.primaryColor,
'brand-primary-tap': theme.brandPrimaryTap,
},
externals: {},
lessLoaderOptions: {
javascriptEnabled: true,
},
targets: {
android: 5,
chrome: 58,
edge: 13,
firefox: 45,
ie: 9,
ios: 7,
safari: 10,
},
outputPath: './dist',
hash: true,
alias: {},
proxy: { // 代理
'/api/': {
changeOrigin: true,
target: 'http://doclever.xin.com/mock/5d0b67ac3eb3ea0008d58a31',
},
},
ignoreMomentLocale: true,
manifest: {
basePath: '/',
},
chainWebpack: webpackPlugin,
extraPostCSSPlugins: [ // postcss插件
require('postcss-flexbugs-fixes'),
],
es5ImcompatibleVersions: true,
extraBabelPlugins: [
['import', { libraryName: 'antd-mobile', style: true }] //按需加载antd-mobile样式文件
],
};
3. React常用写法
Vue转React后,没有v-for/v-if等指令,还是稍微转化下写法。
map代替v-for指令
const arr = ['aaa','bbb','ccc'];
arr.map(str => <div>{str}</div>);
逻辑运算符号代替与v-if/v-else
const show = false;
render (){
return <>
{show && <div>我展示了</div>}
{show ? <div>为true展示</div> : <div>为false展示</div>}
</>
};
路由跨页面传参
Vue中使用this.$route.params可以直接获取参数,在React中只有通过withRouter包裹的组件才能获得路由参数,BasicLayout中统一包裹页面级组件,但内面内嵌套的组件如需获取路由参数则要自己手动包裹。
import { withRouter } from 'react-router-dom'
class FixBar extends React.Component{
public render() {
const { location: { query = {} }, } = this.props;
return (<div>{query.id || '默认文字'}</div>);
}
}
export default withRouter(FixBarCom);
监听props/state变化
在Vue中可以使用watchapi方便的监听参数变化,因为实现原理不同,React需要借助其他Api实现类似功能。
getDerivedStateFromProps不常用,即state的值在任何时候都取决于props的情况下使用。
componentDidUpdate(prevProps, prevState) {
// 监听props
if (this.props.userID !== prevProps.userID) {
// doSomething
}
// 监听state
if (this.state.name !== prevState.name) {
// doSomething
}
}
setState与fiber
为了更好的性能,react采用了fiber架构,意味着setState操作可能是异步的。
// 不推荐
this.setState({ a:1})
this.setState({ a:this.state.a + 1})
// 推荐
this.setState({a:1},() => {
this.setState({ a:this.state.a + 1})
})
// 不推荐
this.setState({
counter: this.state.counter + this.props.increment,
});
// 推荐
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
ES6结合setState的一些写法:
const data = { a: '11' }
this.setState({ ...data })
this.setState({ a:'222'})
this.setState({ ['a']: '333' })
this.setState((prevState, props) => ({a: '444' }));
React debounce 防抖
很多场景下需要对Input的onChage事件增加防抖,借助lodash.debounce方法。
import _ from 'lodash';
class DebounceExample extends React.Component {
constructor(props) {
this.handleInputThrottled = _.debounce(this.getSomeFn, 100)
}
getSomeFn(res){
// doSomeThing
}
render() {
return <input onChange={this.handleInputThrottled}/>
}
}
export defaultDebounceExample;
React createPortal
部分场景需要把子元素的内容节点放在其他组件里,比如弹框组件,每次弹框都希望在body元素的根节点下,就可以使用createPortal。
官方解释:
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
举个例子说明,我们的需求是A/B2个输入框组件,他们的下拉结果要通知展示在父组件下。
如图:
// 父组件
<inputCom />
<inputCom />
<div id="listBox"></div>
// inputCom
class InputCom extends React.Component{
ExternalCityComp(){
return return ReactDOM.createPortal(<div>结果</div>,document.querySelector('#listBox'))
}
public render() {
return (<div><input onChage={}/>{this.ExternalCityComp()}</div>);
}
}
static静态方法
React组件可以设置静态方法,比如实现类似Toast组件的方法。
class Toast extends React.Component {
...
static success(Param) { // do something }
static fail(Param) { // do something }
...
}
Toast.success()
Toast.fail()
className 样式
const s = require('./index.less');
render (){
const active = true
return <>
<li className={`${s.inputItem} ${s.borderBottom}`}></li>
<li className={`${active ? s.inputItem : s.other } ${s.borderBottom}`}></li>
</>
}
图片引用
import btnImg from './images/floatBtn.png';
render (){
return <img src={btnImg} />
}
4. Dva使用入门
在用Dva之前,总就觉得和Vuex的用法差不了多少,但是初次使用时,啃了半天Dva的文档,还是被绕的晕晕的;
其实并不复杂,今天就写下如何在不懂redux、redux-saga的情况下,愉快的使用Dva,大神自行跳过。
文件目录
我们只需要关注三个目录下的文件就可以了。
- 页面:
src/pages - 模型:
src/models - 服务:
src/services
页面即我们在路由中增加的页面组件,模型是重点,服务说白了就是封装的request请求。
异步请求、同步请求
- 我们把models当做一个全局变量,可以存放数据,可以被任何页面获取,存和取都有对应的Api。
- 存数据有2中方式,一种是直接把数据存到
models里(同步请求),另一种是发一个请求然后把数据存到models里(异步请求)。 - 同步请求即
reducers里的方法,会修改models.state里的数据。 - 异步请求即调用
models的effects方法,该方法会调用services方法获取请求数据。 - 异步请求在拿到
services返回的数据后,如果要保存到models.state里,则再调用同步方法reducers即可。
再加深下印象:同步即直接保存、异步即发请求然后保存。
页面获取models数据
页面通过dva.connect方法 + models.namespace(每个models有自己的命名空间)获取数据。
connect方法的主要作用是把models里的数据合并到页面组件的props里。
代码中列举了三种不同的调用语法,如果觉的看ES6的装饰器+解构+箭头函数不直观,可以看代码中的ES5版本。
代码:
import React from 'react';
import { connect } from 'dva';
// 版本1 装饰器语法
@connect(({ list:{ payInfo, detail } }) => ({ payInfo, detail }))
class PayInfo extends React.Component<ITextPaperProps, IEntranceState, any> {
public render() {
// 从props中获取
const { payInfo, detail } = this.props;
return (
<div >
{payInfo}
</div>
);
}
}
export default PayInfo;
// 版本2 函数
export default connect(
({ list:{ payInfo, detail } }) => ({ payInfo, detail }))(PayInfo);
// 版本3 ES5函数
export default connect(function(modules){
return {
payInfo: modules.list.payInfo,
detail: modules.list.detail
}
})(PayInfo);
页面调用models请求
页面中通过dispatch方法调用请,同步和异步的调用形式一样,只是在module中的处理不一样,下边展示完整的代码。
page代码:
import React from 'react';
import { connect } from 'dva';
// 版本1 装饰器语法
@connect(({ list:{ payInfo, detail } }) => ({ payInfo, detail }))
class PayInfo extends React.Component<ITextPaperProps, IEntranceState, any> {
// 异步调用
setPayInfo(){
const { dispatch } = props;
dispatch({
type: 'list/setPayInfo',
payload: 'aaaaa',
});
}
// 同步调用
setDetail(){
const { dispatch } = props;
dispatch({
type: 'list/setDetail',
payload: 'bbb',
});
}
public render() {
// 从props中获取
const { payInfo, detail } = this.props;
return (
<div >
{payInfo}
</div>
);
}
}
export default PayInfo;
models代码:
import { setPayInfoRequest } from '@/services/list';
export default {
namespace: 'list',
state: {
payInfo:{},
detail:'',
},
effects: {
*setPayInfo({ payload }, { call, put }) {
const response = yield call(setPayInfoRequest, payload);
yield put({
type: 'setPayInfoReducers',
payload: {
res: response,
},
});
return response;
},
},
reducers: {
setDetail(state, { payload }) {
return {
...state,
detail:payload
};
},
setPayInfoReducers(state, { payload }) {
return {
...state,
payInfo: payload,
};
},
},
};
services代码:
export async function setPayInfoRequest(params) {
return request('/api/setPayInfo', {
method: 'POST',
body: {
...params
}
});
}
可以再对照图片整理一下思路,哈哈哈😄。
5. 微信配置
微信签名涉及的细节比较多,比较繁琐,我们一步一步来。
微信域名认证
无论是微信、企业微信开发,都需要先把域名暴露给外网,保证域名外网可以访问,然后在后台把微信的.txt认证文件下载下来,放在域名根目录下,保证get请求txt文件可以被微信服务器抓取到。
例:www.nihaojobo.com/WW_verify_cU0ZETJpcItcKYc8.txt。
运维人员出于安全考虑,可能需要微信服务器IP地址列表,见文档。
微信获取Token
这部分内容再后端,理论上前端不需要关心,还是说明下,大神请跳过。
调用微信服务端Api接口理论上都需要access_token,需要根据appid+secret获取access_token, 每2小时失效,重复获取会导致上次access_token失效,需定时刷新并存在公共位置方便接口使用。
微信登录
微信登录流程如下,最近在用Node开发小程序,大同小异,具体详细信息见文档。
- 前端访问微信授权地址,并带
appid、授权类型scope、回调地址redirect_uri。 - 用户授权或者静默授权后跳转到回调地址,并带有
code。 - 前端把
code发给后端,后端通过access_token+code+appid+secret获取用户信息,包含openid和用户级别access_token等信息, - 后端再通过
openid和用户级别access_token获取用户昵称等信息并保存。 - 后端通过唯一标识
openid和用户数据生成自己系统级token并返回给前端。 - 前端每个请求上
token,后端根据token关联用户。
前端获取code代码:
public weChatAuthorize = () => {
const param = {
appid: '*********',
redirect_uri: encodeURI(window.location.href), // 回调地址
response_type: 'code',
scope: 'snsapi_base', // snsapi_base静默 snsapi_userinfo授权
};
// 拼接微信授权地址
const weChatUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize';
const link = `${weChatUrl}?${qs.stringify(param)}#wechat_redirect`;
window.location.href = link;
};
微信签名
前端要调用上传图片、朋友圈分析等sdk接口,则需要先执行签名,通过后再调用。
签名生成一般在后端,前端给后端当前页面的Url地址,后端返回给前端appId、timestamp、nonceStr、signature参数用于wx.config方法的权限验证。
import { Toast } from 'antd-mobile';
// 判断是为企业微信
export function isWeChatWork() {
const userAgent = String(navigator.userAgent.toLowerCase());
return userAgent.includes('wxwork');
};
import { getSignature } from '@/services/home';
import { GETOPENSIGNATURE, } from '@/services/list';
// 获取微信签名 关闭右上角菜单
export function getSignatureWX(cb?:any) {
cb = cb || function(){}
// 判断 微信公众平台或企业微信签名
const requestFn = isWeChatWork() ? getSignature : GETOPENSIGNATURE;
const url = isWeChatWork() ? location.href.split('#')[0] : encodeURIComponent(location.href.split('#')[0]);
Toast.hide();
Toast.loading('加载中',10000)
return requestFn({ url }).then(res => {
if(res.status === 0){
Toast.hide();
const { AppId, Timestamp, NonceStr, Signature, } = res.data;
wx.config({
beta: true, // 必须这么写,否则wx.invoke调用形式的jsapi会有问题
// debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: AppId, // 必填,企业微信用corpID
timestamp: Timestamp, // 必填,生成签名的时间戳
nonceStr: NonceStr, // 必填,生成签名的随机串
signature: Signature, // 必填,签名,见 附录-JS-SDK使用权限签名算法
jsApiList: ['chooseImage', 'previewImage', 'uploadImage', 'hideOptionMenu','onMenuShareAppMessage','onMenuShareTimeline'], // 必填,需要使用的JS接口列表,凡是要调用的接口都需要传进来
});
wx.ready( () => { // 需在用户可能点击分享按钮前就先调用
if(wx.onMenuShareTimeline){
wx.onMenuShareTimeline({
title: '默认标题', // 分享标题
link: window.location.origin + '/groupSendCar', // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: `${window.location.origin}/favicon.png`, // 分享图标
})
}
});
cb(true)
return true
}else{
cb(false)
Toast.fail('微信签名加载失败,请刷新重试',3)
return false
}
})
};
我们前期是微信和企业微信同步开发,可以看到代码中有判断,另外采了几个小坑,大概说一下:
- IOS 12.x 微信公众平台必须使用
encodeURIComponent,不能带有中文。 - 企业微信与微信
SDK版本不一致,企业微信是1.2.0,微信版本更高。 - 测试公众号只能设置一个信任域名。
图片上传
保证签名通过后,再执行上传事件。
chooseImage事件选择图片,获取localIds。uploadImage事件上传localIds,获取serverId即MediaId。- 前端发给后端
MediaId,后端根据入参后去微信素材地址并爬取到自己服务器,返回给前端图片地址。 - 前端保存图片地址并展示。
public upPic = key => {
const { dispatch } = this.props;
// 选择图片
wx.chooseImage({
count: 1, // 默认9
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
// defaultCameraMode: 'normal', // 表示进入拍照界面的默认模式,目前有normal与batch两种选择,normal表示普通单拍模式,batch表示连拍模式,不传该参数则为normal模式。(注:用户进入拍照界面仍然可自由切换两种模式)
// isSaveToAlbum: 1, // 整型值,0表示拍照时不保存到系统相册,1表示自动保存,默认值是1
success: res => {
const [localId] = res.localIds; // 返回选定照片的本地ID列表,
// 上传企业微信
wx.uploadImage({
localId, // 需要上传的图片的本地ID,由chooseImage接口获得
isShowProgressTips: 1, // 默认为1,显示进度提示
success: (res: { serverId: string }) => {
const serverId = res.serverId; // 返回图片的服务器端ID
// 判断 微信公众平台或企业微信签名
const path = this.isWeChatWork() ? 'home/getPicUrl' : 'list/getPicUrl';
Toast.loading('上传中',10000)
// 上传图片
dispatch({
type: path,
payload: {
MediaId: serverId,
},
}).then(res => {
Toast.hide();
if (this.isWeChatWork()){
this.setState({ [key]: res });
} else {
this.setState({ [key]: res.AbsoluteAddress });
}
this.merage();
});
},
});
},
fail:res=>{
console.log("fail",res)
},
});
};
6. TypeScript组件
如果前期没有规划好组件目录,没有更低成本的组件使用方式,几乎不可能出现理想的组件复用,这是一个熵增过程,所有前期必须做好准备,保证复用组件的方式比copy样式成本更低。
src/components/carUI下放置业务组件,包含主题样式与utils公共方法。
── carUI
├── Banner
├── Button
├── Coupon
├── Empty
├── Fixbar
├── Form
├── Input
├── List
├── Radio
├── Select
├── address-list
├── bank-card
├── check-box
├── fixbar-box
├── float-ball
├── index.tsx
├── init
├── oder-car
├── themes
└── utils
主题样式变量
根据设计稿把主题颜色提前配置成变量方便复用。
// 主题色
@color:#F67A23;
// 背景色
@bg:#F9F9F9;
// 链接颜色
@link:#fa6400;
// 按钮渐变颜色
@gradient:linear-gradient(90deg, #ff8c00 0%, #ff4800 100%);
// 阴影
@shadow:0px 0px 5px 0px rgba(0, 0, 0, 0.13);
创建组件模板
为了更低成本的创建组件,在组件目录下放置了init模板,保存大概率使用的代码段,可以直接复制创建组件,后期可以引入plop.js就不用手动录入。
使用classNames便捷操作样式, omit过滤无用参数,并在propsTypes中尽可能多的引入类型示意代码 。
// index.less
@import '../themes/index.less';
// 变量名
// @color
// @bg
// @link
// @gradient
// @shadow
.textCar{
color: @color;
box-shadow: @shadow;
}
// index.tsx
import React from 'react';
import classNames from 'classnames/bind';
import omit from 'omit.js';
const styles = require('./index.less');
const cx = classNames.bind(styles);
interface ITextPaperProps {
className?:string,
style?: React.CSSProperties;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
onChange?: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void,
children?: React.ReactNode;
}
interface IEntranceState {}
class Coupon extends React.Component<ITextPaperProps, IEntranceState, any> {
constructor(props) {
super(props);
this.state = {};
}
public render() {
const {} = this.state
const { className, onChange } = this.props;
const nextProps = omit(this.props,['onChange','className']);
return (
<div className={cx('textCar', className)} onChange={(e) => onChange && onChange(e) }>
<h2 {...nextProps}>default component</h2>
</div>
);
}
}
export default Coupon;
7. H5小技巧
弹出层页面
在H5场景下,有很多弹出层交互的需求,如选择通讯录、选择优惠券等,ant Mobile有提供弹框的组件,但在实际使用中还是不太灵活,最简单的是自己实现。
// css部分
.CouponsSelectBox{
position: absolute;
height: 100vh;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch; // 解决div滚动条卡顿问题
}
// jsx部分
{showCoupon && <CouponsSelect value={selectCoupon} onChange={res => { this.setState({ selectCoupon:res, showCoupon:false })}}/>}
input onCompositionEnd 事件
部分手机由于输入法的原因,在打拼音还未来选择汉字时触发onChange事件,可以使用onCompositionEnd事件来处理。
Loading覆盖
大部分页面未来避免用户多次点击或者网络状况较差等情况,建议使用Loading效果提升用户体验,也可以封装在request中,ant Mobile的Toast组件很方便。
Toast.loading('下单中',10000)
const { dispatch } = this.props;
return dispatch({
type: 'home/placeOrder',
payload: param,
}).then(res => {
Toast.hide();
router.push('/order/detail?order_no=' + res.wl_order_no);
});
8. 日志记录
面向C端的用户一般都需要记录曝光、点击等关键节点的信息;另外为了区分用户是否有缓存,也需要再前端代码中加入前端代码版本。
曝光日志
曝光日志事件在src/pages/Authorized.js中触发,每次切换路由都会执行。
const Authority = getAuthority();
const Authorized = RenderAuthorized(Authority);
// 路径与日志配置
const LogIdList = {
'/groupSendCar': '1-1',
'/oderPerfect': '2-2',
'/payInfo': '3-3',
'/order/list': '4-4',
'/order/detail': '5-5',
'/myBill': '6-6',
'/couponsList': '7-7',
};
const AuthorizedCom = (props) => {
const { children, dispatch } = props;
useEffect(() => {
const { pathname } = children.props.location;
if (LogIdList[pathname]) {
const [funcModule, eventName] = LogIdList[pathname].split('-');
dispatch({
type: 'global/log',
payload: {
product: 1,
platform: 1,
funcModule: Number(funcModule),
eventName: Number(eventName)
}
});
}
}, [children.props.location]);
return (
<Authorized authority={children.props.route.authority} noMatch={<Redirect to="/exception/403" />}>
{children}
</Authorized>
);
};
export default connect(({ global, home, list }) => ({ home, list, userType: global.userType }))(AuthorizedCom);
点击事件日志
点击事件在src/layouts/BasicLayout.js中触发,不可能每个点击的地方都手写函数调用,在componentDidMount中给body绑定全局click事件,如果点击元素包含data-log属性则提交日志,哪里需要点日志,就在哪里给元素增加data-log属性,在组件目录里也保留了logID的入参。
componentDidMount() {
const _this = this;
document.body.addEventListener('click',function(e){
const logString = e.target.getAttribute('data-log');
if(logString){
const [ funcModule, eventName ] = logString.split('-');
_this.sendLog(Number(funcModule),Number(eventName));
}
});
}
// 点击量日志
sendLog = (funcModule,eventName='sd') => {
const { dispatch, } = this.props;
dispatch({
type: 'global/log',
payload: {
product:1,
platform:1,
funcModule,
eventName
}
});
}
前端代码版本
原因是前端有几次bug,不知道是缓存还是逻辑问题,本来是想在环境变量中增加version变量,每次发布修改变量,然后在每个请求头中加入version属性,但我们目前版本迭代比较快,怕发布时忘记修改,于是在环境变量中设置了versionTime属性,属性值为moment生成的打包时间,这样就很好判别请求是来自于哪个前端版本。
config/config.js中在define属性中添加。
{
versionTime:moment('YYYY-MM-DD hh:mm:ss')
}
9. 其他
MD5加密
理论上前端MD5几乎没用,因为加密值也在前端,我们也做了加密,用领导的话说,增加一下复杂度。
加密流程:
- 参数对象按照key排序,并序列化为字符串。
- 使用
CryptoJS.MD5按照后端约定格式拼接:序列化字符串 + 秘钥 + 日期字符串。 - 按照后端要求发送
sn+ 后端约定的from+ 原有参数。 - 后端按照约定格式校验,通过后返回业务数据。
// 参数排序 序列化
const paramStringify = params => {
const newParams = {};
// 排序
Object.keys(params)
.sort()
.forEach(key => {
newParams[key] = params[key];
});
return qs.stringify(newParams, { encode: false });
}
// 微信api签名 WeChatWorkSecret、WeChatWorkAppkey为环境变量
const OAAddSalt = params => {
const str = params ? paramStringify(params) :'';
// 加盐 生成MD5
const sn = CryptoJS.MD5(str + WeChatWorkSecret + moment().format('YYYY-MM-DD')).toString();
return { ...params, sn, from: WeChatWorkAppkey };
};
// 获取微信授权签名
export async function getPicUrl(params) {
return request(WeChatWorkApi + '/WeChat/WechatPicUrl', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: OAAddSalt(params),
});
}
真机调试
如验证微信签名、图片上传等功能是,还是需要在真机下调试。
Mac系统默认Apache占用80端口,用sudo apachectl -k stop 停止掉。
流程:
- 本地启动前端服务 保证
127.0.0.1可访问。 - 本地配置host 为认证域名,如:
127.0.0.1 nihaojob.com。 - 启动
charles代理软件。 - 手机设置代理为电脑IP和
charles代理端口。 - 手机微信访问认证地址即可。
另外,charles抓包HTTPS需要安装证书,手机与电脑均需安装证书,否则抓包内容为乱码。
总结
总的来说知识点都很浅薄,不像类似React源码解析一类的笔记,不过对一个从Vue转React上手微信H5开发实战的我来说,还是收获挺大的。
- React基础Api使用。
- 熟悉微信签名、登录等流程。
- TypeScript组件积累经验。
- React开发生态Umi+Dva熟悉。
线上预览
Dva官网推荐的项目模板:umi-dva-antd-mobile。