用户
 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,登录网站

小程序社区 首页 教程 实战教程 查看内容

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

Rolan 2020-10-20 10:29

把最近的实践总结下,有需要使用React上手微信H5的可以参考,大家一起交流哈。

最近一直在做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,还有一些国际化、配置式路由等功能深度整合的。

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地址静态资源地址
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 />
<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
    }
  });
}

复制代码

可以再对照图片整理一下思路,哈哈哈

鲜花
鲜花
鸡蛋
鸡蛋
分享至 : QQ空间
收藏