前言:
以前开发小程序(微信小程序为主)都是使用微信小程序开发的那一套,从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' });
}
})
}
}