微信H5 React + Umi 开发实践总结
发表时间:2021-1-5
发布人:葵宇科技
浏览次数:187
最近一直在做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,还有一些国际化、配置式路由等功能深度整合的。
Dva
不严谨的说:可以理解为类Vuex的状态管理库。整合了 redux + reduxsaga。
为什么是Umi + Dva
在用Vue开发时,我们更倾向于vue-cli创建模板、Vuex状态管理。
而且Umi
比React官方脚手架create-react-app
更强大、更贴近业务场景,开箱即用。
React在状态管理上有redux
,不够好用又出现redux-saga
,最后被Dva
打包成更好用的数据流方案。
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中可以使用watch
api方便的监听参数变化,因为实现原理不同,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 = http://www.wxapp-union.com/{ 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 />
<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
}
});
}
复制代码
可以再对照图片整理一下思路,哈哈哈