阅读 871

微信H5 React + Umi 开发实践总结

最近一直在做H5公众号的需求,使用的技术栈如标题,从立项目到稳定增长阶段,前端使用React随着业务需求从0到1把项目搭建起来,get了很多React+H5+微信场景下的知识点,把最近的实践总结下,有需要使用React上手微信H5的可以参考,大家一起交流哈😀。

本文特点:

  • React+H5+微信场景全流程。
  • Vue转React入门参考。

项目简介:

轿车物流平台,可以通过公众号在线完成轿车运输下单业务。

  • 线路价格查询
  • 下单信息填写
  • 地址簿管理
  • 实名认证
  • 银行卡绑定
  • 优惠券
  • 微信支付
  • 活动海报

正文

  1. 区分Umi与Dva
  2. Umi配置
  3. React常用写法
  4. Dva使用入门
  5. 微信配置
  6. TypeScript组件
  7. H5小技巧
  8. 日志记录
  9. 其他

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地址静态资源地址
Maserbuild:test测试fat.*.comcdn.fat.*.com
Developbuild:prod正式*.comcdn.*.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 防抖

很多场景下需要对InputonChage事件增加防抖,借助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的文档,还是被绕的晕晕的; 其实并不复杂,今天就写下如何在不懂reduxredux-saga的情况下,愉快的使用Dva,大神自行跳过。

文件目录

我们只需要关注三个目录下的文件就可以了。

  • 页面:src/pages
  • 模型:src/models
  • 服务:src/services

页面即我们在路由中增加的页面组件,模型是重点,服务说白了就是封装的request请求。

异步请求、同步请求

  1. 我们把models当做一个全局变量,可以存放数据,可以被任何页面获取,存和取都有对应的Api。
  2. 存数据有2中方式,一种是直接把数据存到models里(同步请求),另一种是发一个请求然后把数据存到models里(异步请求)
  3. 同步请求即reducers里的方法,会修改models.state里的数据。
  4. 异步请求即调用modelseffects方法,该方法会调用services方法获取请求数据。
  5. 异步请求在拿到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开发小程序,大同小异,具体详细信息见文档

  1. 前端访问微信授权地址,并带appid、授权类型scope、回调地址redirect_uri
  2. 用户授权或者静默授权后跳转到回调地址,并带有code
  3. 前端把code发给后端,后端通过access_token + code + appid + secret获取用户信息,包含openid和用户级别access_token等信息,
  4. 后端再通过openid和用户级别access_token获取用户昵称等信息并保存。
  5. 后端通过唯一标识openid和用户数据生成自己系统级token并返回给前端。
  6. 前端每个请求上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地址,后端返回给前端appIdtimestampnonceStrsignature参数用于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,微信版本更高。
  • 测试公众号只能设置一个信任域名。

图片上传

保证签名通过后,再执行上传事件。

  1. chooseImage事件选择图片,获取localIds
  2. uploadImage事件上传localIds,获取serverIdMediaId
  3. 前端发给后端MediaId,后端根据入参后去微信素材地址并爬取到自己服务器,返回给前端图片地址。
  4. 前端保存图片地址并展示。
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中触发,不可能每个点击的地方都手写函数调用,在componentDidMountbody绑定全局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几乎没用,因为加密值也在前端,我们也做了加密,用领导的话说,增加一下复杂度。

加密流程:

  1. 参数对象按照key排序,并序列化为字符串。
  2. 使用CryptoJS.MD5按照后端约定格式拼接:序列化字符串 + 秘钥 + 日期字符串。
  3. 按照后端要求发送sn + 后端约定的from + 原有参数。
  4. 后端按照约定格式校验,通过后返回业务数据。

// 参数排序 序列化
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 停止掉。

流程:

  1. 本地启动前端服务 保证 127.0.0.1 可访问。
  2. 本地配置host 为认证域名,如:127.0.0.1 nihaojob.com
  3. 启动charles代理软件。
  4. 手机设置代理为电脑IP和charles代理端口。
  5. 手机微信访问认证地址即可。

另外,charles抓包HTTPS需要安装证书,手机与电脑均需安装证书,否则抓包内容为乱码。

总结

总的来说知识点都很浅薄,不像类似React源码解析一类的笔记,不过对一个从VueReact上手微信H5开发实战的我来说,还是收获挺大的。

  1. React基础Api使用。
  2. 熟悉微信签名、登录等流程。
  3. TypeScript组件积累经验。
  4. React开发生态Umi+Dva熟悉。

线上预览

Dva官网推荐的项目模板:umi-dva-antd-mobile