使用Remax开发小程序中需注意的地方和学习的知识

前言:

以前开发小程序(微信小程序为主)都是使用微信小程序开发的那一套,从React跳转到此多多少少有些不适应,因此Remax(https://remaxjs.org/)由此而生,它是使用真正的 React 构建跨平台小程序

写在前面:

在开发时,尽量使每个页面的事件名、样式名、变量名统一。
比如有搜索功能的页面,声明搜索函数名时,可统一命名为:handleSearch;
搜索框的变量名统一为:searchValue;
返回的数组数据统一为dataList;
获取数据名的api名虽然不一样,但它的外层函数名可统一为:getPageList或getPageData;
如果有多个接口,再根据接口的作用自行命名。
也就是说尽量把不同页面的同一功能的方法或者变量命名为一样的,这样页面便可提高代码的可拓展性、可维护性和可复用性

1.构建小程序项目(https://remaxjs.org/guide/quick-start),官网里的文档已经给出了很详细的操作方法,按照流程来一步一步搭建即可,搭建完成之后,接下来才是开发的重点

2.使用Remax开发小程序需要注意的地方或需要注意的技术点

1)page页配置,一般在创建一个页面的时候,会创建三个文件,index.js,index.less,index.config.js文件,后者简要介绍一下,它是当前页的配置文件,比如你需要自定义当前页的title,则可如下设置:

module.exports = {
  navigationBarTitleText: 'xxxx'
}

当然了,它还有一个比较重要的作用就是可以自定义当前页的头部,可如下设置:

module.exports = {
    navigationStyle: 'custom'
}

效果如下(蓝色部分是自定义的样式,药丸部分是固定的,不能自定义):

2)page页生命周期事件,可别小看这些事件,很多场景需要分别使用才能实现产品的需求(以下示例代码均是Remax用法),因为小程序生命周期事件比较多(https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/page.html),这里列举几个常用的说明:

①onLoad(页面创建时执行)

一般来说,该事件在页面未销毁之前,只会执行一次,常用于请求该页面只会使用一次的数据(且该数据不会因前端的操作而变化,一般是后台控制数据变化)

②onShow(页面出现在前台时执行)

该事件用的较多,常用于因前端操作使对应数据变化的(前端)数据,比如在小程序-我的主页时,会显示用户的信息,如下图:

因为用户的信息是可以更改的,所以需要实时更新,故需要在页面每次显示在前台时都需要在onShow事件中请求用户数据

③onUnload(页面销毁时执行)

该事件可别小看,它经常会用到,比如对于部分全部变量的初始化。例如,两个不同功能的页面有相同的一栏Form表单,该值可设置为全局变量,因该值可应用到两个页面,且需要对应各页面内的数据,因此,每当其中某个页面销毁时,需要初始化该值

3)React.useState()(数据状态管理)

由于React.useState()在数据更新时是异步的,因此在有些地方处理时不太方便,可以利用setTimeout来实现伪同步,但不建议频繁使用,另外React.useState()在更新数据类型为对象时,会出现“暂时性”不更新,如下GIF图所示:

 这就让人有点疑惑,在了解其更新机制后,会发现,不是数据未改变,只是指向这个数据对象的地址未改变,但值确实是变化了,原代码如下:

  const [tempImgArr, setTempImgArr] = React.useState(Array);  
  // 删除图片
  const handleDelImage = (e) => {
    const idx = e.target.id;
    tempImgArr.splice(idx, 1);
    setTempImgArr(tempImgArr);
  }

原代码会出现上述"bug"情况,正确代码如下:

  // 删除图片
  const handleDelImage = (e) => {
    const idx = e.target.id;
    tempImgArr.splice(idx, 1);
    setTempImgArr([...tempImgArr]);
  }

效果与Object.assign()方法类似,值变化,指针也变化

4)React.useEffect()(某个数据频繁更新。详细使用可参考:https://www.jianshu.com/p/6e525c3686ab

该Hook可在微信小程序有定时器(倒计时)需求的场景,因为倒计时的值是每隔一段时间就会发现变化,可在该Hook内在指定的值或者可选区域内处理对应的需求。同时它还常用于地图(使用地图时,注意调用API:createMapContext())选址场景,即在地图中用户点击不用的地方,都会得到相应的经纬度(GPS),该Hook可根据GPS值的变化去实时更新用户周边的信息。比如:

  const [gps, setGps] = useState({})  
  useEffect(() => {
    getLocationList({
      latitude: gps.latitude,
      longitude: gps.longitude
    })
  }, [gps])

如果你只想让该Hook执行一次,那么你可以这样:

  useEffect(() => {
    getLocationList(params)
  }, [])

5)获取路由参数

微信小程序获取路由参数是根据生命周期事件onShow的参数options来获取的,但使用Remax之后,获取路由参数还可以为使用useQuery()函数,引用方法:import { useQuery } from 'remax';使用方法:const query = useQuery();query就是路由参数对象,参数的值可根据对象的键来获取

6)全局变量的设置和使用

在入口文件:app.js内设置全局变量名和设置全局变量值,设置方法如下:

import React, { useState } from 'react';
import './app.css';

export const AppContext = React.createContext({})

const App = ({ children }) => {
  const [appData, setAppData] = useState({
    xxxObj: {},
    xxxArr: [],
    xxxStr: '',
    xxxNum: 0
  })
  return (
    <AppContext.Provider value={{ appData, setAppData }}>
      { children }
    </AppContext.Provider>
  )
}

export default App;

在子页面内调用值或方法,调用示例如下:

import { usePageEvent } from 'remax/macro';
import { AppContext } from '@/app';

export default () => {

  const global = React.useContext(AppContext);

  usePageEvent('onShow', () => {
    const { xxxObj, xxxArr} = global.appData;
    console.log(xxxObj, xxxArr)
  }

  usePageEvent('onUnload', () => {
    // codes that appData change go here 
    global.setAppData({ xxxObj: {}, xxxArr: [] });
  })

}

7)热门但非常用组件

①走马灯:<Swiper>、<SwiperItem>引用及其使用方法:

import * as React from 'react';
import { View,Image } from 'remax/one';
import { Swiper, SwiperItem } from 'remax/wechat';
import  { usePageEvent } from 'remax/macro';
import { getBannerList } from '@/utils/api';
import styles from './index.less';

export default () => {

  const [bannerList, setBannerList] = React.useState(Array);

  usePageEvent('onLoad', (ops) => {
    getBannerList().then((res) => {
      setBannerList(res.content) 
    });
  }

  const navigateToBannerInfo = () => {
    // codes go here
  }

  const renderBanners = () => {
    return (
      bannerList.map(item => (
        <SwiperItem key={item.id}>
          <Image 
            className={styles.bannerItem} 
            src={item.bannerUrl} 
            onTap={navigateToBannerInfo} 
          />
        </SwiperItem>
      ))
    )
  }

  return (
    <View>
      <Swiper 
        className={styles.banner} 
        indicatorDots={true}
        indicatorColor='rgba(255, 255, 255, 0.3)'
        indicatorActiveColor='#FFF'
        autoplay={true}
      >
        { 
          renderBanners()
        }
      </Swiper>  
    </View>
  )

}

②地图:<Map>引用及其使用方法:

import React, { useState, useEffect } from 'react';
import { View } from 'remax/one';
import { Map, createMapContext, getLocation } from 'remax/wechat';
import  { usePageEvent } from 'remax/macro';
import { getLocationList } from '@/utils/api';
import { useQuery } from 'remax';
import styles from './index.less';

export default () => {

  const query = useQuery();
  const [locationList, setLocationList] = React.useState(Array);
  const [gps, setGps] = useState(Object)

  usePageEvent('onLoad', (ops) => {
    mapContext = createMapContext('officeMap');
    getLocationList(query).then((res) => {
      setLocationList(res.content) 
    });
  }

  usePageEvent('onShow', () => {
    getLocation()
      .then(res => {
        setGps(res)
      })
  })

  useEffect(() => {
    getLocationList({
      latitude: gps.latitude,
      longitude: gps.longitude
    }).then((res) => {
      setLocationList(res.content) 
    })
  }, [gps])

  const selectPoint = ({ detail }) => {
    setGps(detail)
  }

  const handleSelectProject = (item) => {
    // codes go here
  }

  const renderLocationList = () => {
    return locationList.map((item) => {
      return (
        <View
          className={styles.itemList}
          key={item.id}
          onTap={() => {handleSelectProject(item)}}
        >
          <View className={styles.position}>
            <View className={styles.projectName}>{item.projectName}</View>
            <View className={styles.distance}>{item.distance}|{item.projectAddress}</View>
          </View>
        </View>
      )
    })
  }

  return (
    <View>
      <Map
        className={styles.map}
        id="officeMap"
        longitude={gps.longitude || xxx}
        latitude={gps.latitude || xxx}
        scale="18"
        onTap={selectPoint}
      >
      </Map>
      <View className={styles.locationList}>
        {
          renderLocationList()
        }
      </View>  
    </View>
  )
}

③可移动容器:<MovableArea>、<MovableView>的引用和使用方法,请参见 8)
https://developers.weixin.qq.com/miniprogram/dev/component/movable-area.html
https://developers.weixin.qq.com/miniprogram/dev/component/movable-view.html

8)实现栏目左滑显示删除按钮

import React from 'react';
import { View } from 'remax/one';
import { usePageEvent } from 'remax/macro';
import { useQuery } from 'remax';
import { 
  showModal, 
  navigateTo, 
  showToast, 
  MovableView,
  MovableArea, 
  showLoading, 
  hideLoading 
} from 'remax/wechat';
import styles from './index.less';
import { fetchList, deleteUserById } from '@/utils/api';

export default () => {

  let startX = 0;
  const [dataList, setDataList] = React.useState(Array);

  usePageEvent('onShow', () => {
    getList()
  });

  const getList = () => {
    showLoading();
    fetchList().then((res) => {
      hideLoading();
      if (res.code === 0) {
        setDataList(res.content);
      }
    })
  }

  const handleDelete = (item) => {
    showModal({
      title: `删除${item.name}`,
      content: `确定要移除${item.name}吗?`,
      success: (res) => {
        if (res.confirm) {
          showLoading();
          deleteUserById().then((res) => {
            hideLoading();
            if (res.code === 0) {
              showToast({
                title: res.message,
                icon: 'success',
                mask: true,
                duration: 1500
              });
              setTimeout(() => {
                getList();
              }, 1500)
            }
          })
        }
      }
    })
  }

  const handleTouchStart = (e) => {
    startX = e.touches[0].pageX
    // 隐藏其他可滑动模块
    hideOtherMove(e.currentTarget.dataset.itemindex)
  }

  const handleTouchEnd = (e) => {
    // 滑动误差控制在8rpx内
    if(e.changedTouches[0].pageX < startX && e.changedTouches[0].pageX - startX <= -8) {
      showDeleteButton(e)
    } else if(e.changedTouches[0].pageX > startX && e.changedTouches[0].pageX - startX < 8) {
      hideDeleteButton(e)
    } else {
      hideDeleteButton(e)
    }
  }

  const handleMovableChange = (e) => {
    if (e.detail.source === 'friction') {
      if (e.detail.x < -10) {
        showDeleteButton(e)
      } else {
        hideDeleteButton(e)
      }
    } else if (e.detail.source === 'out-of-bounds' && e.detail.x === 0) {
      hideDeleteButton(e)
    }
  }

  const hideOtherMove = (idx) => {
    let itemList = staffData.map((item, index) => {
      if(index !== idx) {
        item.xmove = 0
      }
      return item
    })
    setDataList(itemList)
  }

  const showDeleteButton = (e) => {
    let itemIndex = e.currentTarget.dataset.itemindex
    // 第二个参数的值,来源于删除按钮样式的宽度(width: 192)
    setXmove(itemIndex, -192)
  }

  const hideDeleteButton = (e) => {
    let itemIndex = e.currentTarget.dataset.itemindex
    setXmove(itemIndex, 0)
  }

  const setXmove = (itemIndex, xmove) => {
    let itemList = staffData;
    itemList[itemIndex].xmove = xmove;
    setDataList(itemList)
  }

  const renderList = () => {
    return (
      dataList.map((item, index) => (
        <View key={index + ''}>
          <View className={styles.slide}>
            <MovableArea className={styles.moveArea}>
              <MovableView
                className={styles.moveItem}
                direction='horizontal'
                friction={10}
                inertia
                out-of-bounds
                data-itemIndex={index}
                x={item.xmove}
                onTouchStart={handleTouchStart}
                onTouchEnd={handleTouchEnd}
                onChange={handleMovableChange}
              >
                <View
                  className={styles.moveText}
                  onTap={() => navigateTo({ url: 'xxx/xxx/xxx' })}
                >
                  {item.name}
                </View>
              </MovableView>
            </MovableArea>
            <View className={styles.deleteBtn} onTap={() => handleDelete(item)}>删除</View>
          </View>
        </View>
      ))
    )
  }

  return (
    <View className={styles.content}>
      <View className={styles.detailWrapper}>{ renderList() }</View>
    </View>
  )
}

9)常用的公用方法

①手机号码校验

export const isPhone = phone => {
  return /^(13[0-9]|14[579]|15[0-3,5-9]|16[2567]|17[01235678]|18[0-9]|19[189])\d{8}$/.test(phone)
}

②密码合法性校验(必须包含大小写字母和数字,8-16位)

  const validatePassword = pwd => {
    return /^(?!\s)(?=.*[0-9])(?=.*[a-z])[A-Za-z0-9]{8,16}$/.test(pwd)
  }

10)网络请求封装

import { getStorageSync, clearStorage, request, showToast, navigateTo } from 'remax/wechat'

// 获取环境变量
export function getBaseURI() {
  switch(process.env.NODE_ENV) {
  case 'dev': return 'http://xxx.xxx.xxx.xxx:xxxx'                                      
  case 'test': return 'https://testxxx.com.cn'  
  case 'prod': return 'https://xxx.com.cn'                             
  default: return 'http://xxx.xxx.xxx.xxx:xxxx'
  }
}

// 请求头
export const headers = {
  'Accept': 'application/json',
  'content-type': 'application/json'
}

export default class Request {
  // get请求
  get (url, data = {}, header) {
    return this.request('GET', url, data, header)
  }
  // post请求
  post (url, data = {}, header) {
    const token = getStorageSync('token')
    return this.request('POST', `${url}?token=${token}`, data, header)
  }
  request (method, url, data, header) {
    return new Promise((resolve, reject) => {
      try {
        if (!!getStorageSync('token') && method === 'GET') {
          Object.assign(data, {
            'token': getStorageSync('token')
          })
        }

        const response = request({
          url: getBaseURI() + url,
          method,
          data: data,
          header: header || headers,
          success: (res) => response.success = res.data,
          fail: (error) => response.fail = error,
          complete() {
            if (response.success) {
              // token 失效跳回登录页面
              if (response.success.code === -2 || response.success.code === -3 || response.success.code === 902 || response.success.code === 904) {
                showToast({ title: '登录超时或未登录或token问题, 请重新登录!', icon: 'none', mask: true, duration: 1500 })
                clearStorage();
                setTimeout(() => { navigateTo({url: '/pages/login/index'}) }, 1500)
                return
              }
              // 接口请求失败
              if (response.success.code === -1) {
                showToast({ title: response.success.message, icon: 'none', duration: 1500 })
                return
              }
              resolve(response.success)
            } else {
              reject(response.fail)
            }
          }
        })
      } catch (error) {
        showToast({ title: '网络请求出错', icon: 'none' });
      }
    })
  }
}

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值