本文档主要介绍收运小程序前端部分的开发架构和开发思想,为后期开发过程提供可借鉴的示例,并统一开发规范。提高项目整体的代码可读性和可持续性。
adminweb/
├── config # 项目环境配置
│ ├── dev.js
│ ├── index.js
│ └── prod.js
├── src # 项目主目录
│ ├── components # 项目公共组件
│ ├── config # 全局公共配置
│ ├── pages # 项目主体页面目录
│ ├── reducers # store-reducer
│ ├── request # 全局api请求配置
│ ├── res # 全局静态文件目录,目前存放小程序底部按钮图片
│ ├── store # store-reducer
│ ├── styles # 项目公共样式
│ ├── types # 全局的interface
│ ├── utils # 项目公用方法
│ ├── app.config.ts # 小程序配置:页面路由、底部菜单等等
│ ├── app.scss # 全局预定义样式引入
│ ├── app.tsx # 主入口,配置自动更新等
│ └── index.html # 静态入口文件
├── tests
│ └── unit
├── .editorconfig # 编辑器配置
├── .eslintrc.js # eslint配置
├── .prettierrc # prettier配置(格式化)
├── babel.config.js # babel配置
├── global.d.ts # 全局预定义常量interface 如wx、__wxConfig等
├── package.json # 项目依赖第三方插件
├── project.config.json # 小程序开发配置,appid、进入模式等
└── tsconfig.json # TS配置
该项目基于 taro3.0 搭建
npm install
npm run dev:weapp
npm run build:weapp
1、登录微信公众平台,在成员管理中添加对应的开发人员
2、本地运行 npm run dev:weapp ,电脑下载微信开发者工具,登录后选择小程序开发模式
src/request/request.ts
构建和设置 request 实例
export const request = (params, method: any = "GET") => {
const { url, data, options } = params;
const contentType = params.contentType || "application/x-www-form-urlencoded";
const requestUrl = url.search("http") !== -1 || url.search("https") !== -1 ? url : API_ENDPOINT.api + url;
const headers = { client_type: "miniprogram", "content-type": contentType, tenantCodes: projectConfig.codes };
const option = {
isShowLoading: false,
loadingText: "正在加载",
url: requestUrl,
responseType: options && options.responseType ? options.responseType : "",
data,
method,
header: { ...headers, Authorization: "Bearer " + store.getState().user.token },
success(res: Taro.request.SuccessCallbackResult) {
validRequest(res);
},
fail() {
logError("服务异常,请稍后再试...");
},
};
return Taro.request(option);
};
预定义全局请求返回和错误处理
const validRequest = (request: Taro.request.SuccessCallbackResult) => {
if (request.statusCode === HTTP_STATUS.NOT_FOUND) {
logError("请求资源不存在");
} else if (request.statusCode === HTTP_STATUS.BAD_GATEWAY) {
logError("服务端开小差了~");
} else if (request.statusCode === HTTP_STATUS.FORBIDDEN) {
logError("没有权限访问");
} else if (request.statusCode === HTTP_STATUS.SERVER_ERROR) {
logError("服务端开小差了~");
} else if (
request.statusCode === HTTP_STATUS.SUCCESS ||
request.statusCode === 204 ||
request.statusCode === 201 ||
request.statusCode === 425
) {
if (typeof request.data.status !== "undefined" && request.data.status === 0 && request.data.message) {
logError(request.data.message);
} else {
return true;
}
} else if (request.statusCode === HTTP_STATUS.AUTHENTICATE) {
Taro.eventCenter.trigger("logout");
logError("请重新登录");
} else if (request.statusCode === HTTP_STATUS.CLIENT_ERROR && request.data) {
const { error } = request.data;
if (typeof error === "string") {
logError(error);
} else {
logError("服务端开小差了~");
}
} else {
logError("服务异常,请稍后再试...");
}
};
获取新闻列表示例 src/request/news.ts
// 引入request
import { get } from "./request";
// 引入interface
import { IPagedRequestDto } from "../types/common";
import { INewsDto } from "../types/cms";
// 暴露获取列表的方法
export const getNewsList = async (data: IPagedRequestDto) => {
const res = await get("/cms/news", data);
if (res.statusCode === 200) {
return res.data.items as INewsDto[];
}
return [];
};
src/pages/index/index.tsx
import { getNewsList } from '../../request/news'
async getList() {
showLoading();
try {
const res = await getNewsList({ skipCount: 0, maxResultCount: 20 });
} finally {
hideLoading();
}
}
小程序文件下载具体请参考微信小程序 api 文档
// 引入excel导出方法
const downloadImg = () => {
showLoading();
Taro.getSetting({
success: (res) => {
Taro.authorize({
scope: "scope.writePhotosAlbum",
success: (res) => {
console.log("授权成功");
var save = wx.getFileSystemManager();
save.writeFile({
filePath: wx.env.USER_DATA_PATH + "/pic" + 123 + ".png",
data: imageUrl,
encoding: "base64",
success: (res) => {
Taro.saveImageToPhotosAlbum({
filePath: wx.env.USER_DATA_PATH + "/pic" + 123 + ".png",
success: (res) => {
showToast("保存到相册成功");
},
});
},
});
},
complete: () => {
hideLoading();
},
});
},
});
};
小程序文件上传具体请参考微信小程序 api 文档
export const openGalleryToUpload = (option: IOpenOption) => {
const successCallback = (res) => {
Taro.chooseImage({
sourceType: [res.tapIndex === 0 ? "camera" : "album"],
success: (res) => {
if (res.tempFilePaths.length) {
uploadImage({ ...option, imagePath: res.tempFilePaths[0] });
}
},
});
};
Taro.showActionSheet({
itemList: ["拍照", "从相册选择"],
success: successCallback,
fail: () => {},
});
};
这是一个普通的页面;包含下拉刷新、触底加载、数据绑定、列表渲染、路由跳转等
import { showLoading, hideLoading } from "../../utils";
import {updateNodesFromServer} from "../../reducers/userReducer";
const Index = () => {
const route = useRouter();
const dispatch = useDispatch();
const token = useSelector(selectToken);
const [newsList, setNewsList] = useState<INewsDto[]>([]);
//获取数据,并保存到store
const getData = useCallback(async () => {
showLoading();
const res = await getHomeRecommandNews();
setNewsList(res);
hideLoading();
dispatch(updateNodesFromServer());
}, []);
//配置小程序分享
useShareAppMessage(() => {
return {
title: `垃圾收运`,
path: `/pages/index/index`,
};
});
//下拉刷新
usePullDownRefresh(async () => {
Taro.stopPullDownRefresh();
getData();
});
//页面准备完成
useReady(() => {
});
//页面初始化
useEffect(() => {
if (token) getData();
}, [token]);
//点击跳转
const menuOnClick = (item: any) => {
Taro.navigateTo({ url: item.path });
};
return (
<View className="index">
<View className="index-menu">
<View className="idx-menu-up">
{upMenuConfig.map((item) => (
<View className="m-u-list" onClick={() => menuOnClick(item)} key={item.value}>
<Image className="m-u-img" src={item.image} />
<View className="m-u-title">{item.value}</View>
</View>
))}
</View>
</View>
<View className="news-container">
<View className="news-header">新闻资讯</View>
{newsList.map((item) => (
<NewsItem
key={item.id}
data={item}
onClick={() => Taro.navigateTo({ url: `/pages/news/newsDetail?id=${item.id}` })}
/>
))}
</View>
</View>
);
};
需要下拉刷新和触底加载的页面,需要先在config.ts中配置
export default {
navigationBarTitleText: '首页',
enablePullDownRefresh: true,//配置下拉刷新
enableShareAppMessage: true,//配置页面分享
}
然后在界面中直接使用
//下拉刷新
usePullDownRefresh(async () => {
Taro.stopPullDownRefresh();
getData();
});
//触底加载
useReachBottom(async () => {
await getData();
});
直接使用单大括号进行数据绑定
<View>{value}</View>
{newsList.map((item) => (
<View
key={item.id}
onClick={() => Taro.navigateTo({ url: `/pages/news/newsDetail?id=${item.id}` })}
>{item.name}</view>
))}
{!!token && (
<View>{token}</View>
)}
{!!token? (
<View>{token}</View>
):(
<View>empty</View>
)}
store缓存需先设置reducer,如userReducer
export interface IUserInitialState extends User {
token: string;
code?: string;
nodes?: INodeDto[]
}
const initialState: IUserInitialState = {
token: '',
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
saveToken: (state, data) => {
state.token = data.payload.access_token;
saveUserInfo({ ...state });
}
}
})
export const {saveToken} = userSlice.actions;
export const selectToken = (state: RootState) => state.user.token;
src/reducers/index.ts
import {combineReducers} from 'redux';
import userReducer from './userReducer';
import timerReducer from './timerReducer';
import messageReducer from './messageReducer';
import applyCollectionReducer from './applyCollectionReducer';
export default function createRootReducer() {
return combineReducers({
user: userReducer,
timer: timerReducer,
message: messageReducer,
applyCollection: applyCollectionReducer
});
}
src/pages/store/init.ts
import { getUserInfoSync } from '../utils/persistentData';
import { configuredStore } from './index';
const res = getUserInfoSync();
export const store = configuredStore({user: { ...res }} as any);
src/pages/store/index.ts
import {configureStore, getDefaultMiddleware, Action} from '@reduxjs/toolkit';
import {ThunkAction} from 'redux-thunk';
import createRootReducer from '../reducers';
const rootReducer = createRootReducer();
export type RootState = ReturnType<typeof rootReducer>;
const middleware = [...getDefaultMiddleware()];
export const configuredStore = (initialState?: RootState) => {
// Create Store
const store = configureStore({
reducer: rootReducer,
middleware,
preloadedState: initialState,
});
return store;
};
export type Store = ReturnType<typeof configuredStore>;
export type AppThunk = ThunkAction<void, RootState, unknown, Action<string>>;
示例
import { useDispatch, useSelector } from "react-redux";
import { selectToken,saveToken } from "../../reducers/userReducer";
const Index = () => {
//构建dispatch实例
const dispatch = useDispatch();
//获取store中的token值
const token = useSelector(selectToken);
//修改token
const editToken=()=>{
dispatch(saveToken('newToken'))
}
小程序框架,路由统一配置在src/app.config.ts中
小程序跳转有多种方式,url的参数携带如:?id=1&name='page'
1、直接跳转
Taro.navigateTo({ url });
2、关闭当前界面跳转
Taro.redirectTo({ url });
3、关闭所有界面跳转
Taro.reLaunch({ url });
4、在跳转底部按钮绑定的tab页面时,只能使用switchTab方式
Taro.switchTab({ url });
const params = router.params;
console.log(params.id,params.name)
框架有集成全局的弹窗,在utils中有预设的方法
export const showLoading = (params?: { title?: string, mask?: boolean }) => {
let defParams = {
title: '加载中...',
mask: true
}
defParams = { ...defParams, ...params }
Taro.showLoading(defParams);
}
export const hideLoading = () => {
Taro.hideLoading();
}
interface ToastParmas {
title?: string
icon?: 'none' | 'success' | 'loading',
mask?: boolean,
duration?: number
}
export const showToast = (title: string, params?: ToastParmas, callback?: () => void) => {
let defParams = {
title: title,
icon: 'none',
mask: true,
duration: 1500
} as Taro.showToast.Option
defParams = { ...defParams, ...params }
Taro.showToast(defParams);
if (callback) {
setTimeout(callback, defParams.duration)
}
}
使用方式:
import { getNewsList } from '../../request/news'
import { showToast,showLoading,hideLoading } from "../utils";
async getList() {
showLoading();
try {
const res = await getNewsList({ skipCount: 0, maxResultCount: 20 });
showToast('数据获取成功')
} finally {
hideLoading();
}
}
在预设方法不能满足要求时,请结合taro文档和小程序api文档使用原生方法实现。
项目全局组件目录
全局的默认为空的提示模块:
<Empty showText="暂无消息~" />
参数:
showText:string
新闻列表组件,用于新闻列表的展示
<NewsItem data={item}/>
参数:
data:{
tenantId: string
name: string
coverPicture: string
summary: string
newsType: NewsType
body: string
creatorName: string
id: string
creationTime: string
}
预定义的数据加载完毕的提示
<NoMore showText="没有更多了..."/>
参数:
showText:string
项目主体页面目录,项目所有的页面都存放这里
首页和首页顶部按钮跳转的二级页面均放在pages/index/下
包含申请收运、邀请收运等等二级页面
index/
├── applyCollection # 申请收运界面
├── category # 分类查询界面
├── communication # 沟通反馈界面
├── company # 公司简介
├── invitation # 邀请收运和要求记录
├── staffManagement # 员工管理
├── systemDetail # 收运管理制度详情
├── systemList # 收运管理制度列表
测试确认功能无误后打正式包
npm run build:weapp
执行完成后再开发者工具中提交代码:
点击上传按钮,输入版本号和备注后点击上传。
上传代码后登录微信公众平台,点击左侧版本管理:
点击提交审核开始进行审核。
审核通过以后还是在版本管理中点击发布版本就可以进行发布了。