思路
- 技术选型 taro+dva+typescript
- 目录设计
- 组件设计 ui设计规范: 抽象ui组件 组件props注释 页面公共交互行为: 逻辑组件 页面公共组件: 业务组件
- 模块划分 => 尽量解藕 view划分:模块之间不可相互调用,只可以通过redux事件通信 model划分: 抽离业务模块公共逻辑 a. 公共节点公用model(全局挂载) b. 不共用节点model(对象合并)
- 脚本编写 => 将重复的 复制粘贴可完成的代码抽离 编写脚本自动写入 a. model,page,component新建 脚本 b. actions调用fetch 脚本
- utils编写 oss,map,fetch,storage,format(number,sting,date),hooks
- types 全局state => 保证ts 静态类型检查
- constants常量 taro方法改写,全局enum
- 代码风格统一配置 => 通过代码风格统一保证代码整洁,规范,可维护性和可读性 .prettierrc ,eslint, tsconfig, readme ,husky
todo
- pageModel封装 分页 搜索
- 全局type挂载整理
- 全局active 态 css调试
- Divider model组件更改
- moment map 打包有问题
- map 单例
难点探索
上传下载图片 oss base64
骨架屏+上拉刷新+下拉加载分页+emptyPage+404Page组件处理
登录拦截和跳转返回
把这些逻辑写在model里,调用只传 logintype
静默登陆
// fetch.tsimport Taro from '@tarojs/taro'/** * 上传、下载 和普通请求的判断 todo */import { checkTokenValid, refreshToken, codeMessage, getStorage } from './index'// const loginUrl = process.env.login_url;// const api_url = process.env.api_url;const api_url = 'https://likecrm-api.creams.io/'export interface Options { header?: HeadersInit showToast?: boolean noToken?: boolean dataType?: String data?: any responseType?: String success?: Function fail?: Function complete?: Function callBack?: Function}export default async function fetch( urlSuffix: String, method: String = 'GET', options: Options): Promise { // 设置token const defaultOptions: any = { header: {}, noToken: false, // 临时不用token showToast: true, data: {}, } const currentOptions = { ...defaultOptions, ...options, } // 如果是游客 不设置token const loginType = await getStorage('loginType'); if (loginType === 'VISITOR') { currentOptions.header.Authorization = ``; return _fetch (urlSuffix, method, currentOptions) } if (!currentOptions.noToken) { const accessToken = await getStorage('accessToken') currentOptions.header.Authorization = `Bearer ${accessToken}` const tokenValid = await checkTokenValid() // if (tokenValid) { return _fetch (urlSuffix, method, currentOptions); // } // return refreshToken (_fetch, urlSuffix, method, currentOptions) } return _fetch (urlSuffix, method, currentOptions)}// 设置请求头 不包括 tokenconst addRequestHeader = async function(requestOption) { const methods = ['POST', 'PUT', 'DELETE'] if (methods.includes(requestOption.method)) { // 小程序 没有 FormData 对象 "application/x-www-form-urlencoded" requestOption.header = { Accept: 'application/json', 'content-Type': 'application/json; charset=utf-8', ...requestOption.header, } requestOption.data = JSON.stringify(requestOption.data) } return requestOption}// 过滤请求结果const checkStatusAndFilter = (response): Promise | undefined => { if (response.statusCode >= 200 && response.statusCode < 300) { return response.data } else { const errorText = codeMessage[response.statusCode] || response.errMsg const error = response.data.error return Promise.reject({ ...response, errorText, error }) }}// 正式请求async function _fetch ( urlSuffix: Request | String, method: String = 'GET', options: Options): Promise { const { showToast = true, ...newOption } = options if (showToast) { Taro.showLoading({ title: '加载中', }) } const url = `${api_url}${urlSuffix}` const defaultRequestOption: Object = { url, method, ...newOption, } const requestOption = await addRequestHeader(defaultRequestOption) try { return await Taro.request(requestOption) .then(checkStatusAndFilter) .then(res => { Taro.hideLoading() if (newOption.callBack) { newOption.callBack(res) } return res }) .catch(response => { // if (response.statusCode === 401) { // Taro.hideLoading() // return response // // 登陆可以拦截 // // refreshToken (_fetch, urlSuffix, method, options); // } else { Taro.hideLoading() if (requestOption.showResponse) { // 自定义 错误结果 return response } Taro.showToast({ title: response.errorText, icon: 'none', duration: 2000, }) return response.data // } }) } catch (e) { Taro.hideLoading() Taro.showToast({ title: '代码执行异常', mask: true, icon: 'none', duration: 2000, }) return Promise.reject() }}复制代码
// checkTokenValid.tsimport Taro from '@tarojs/taro'import { CLIENT_ID, APPROACHING_EFFECTIVE_TIME, TRY_LOGIN_LIMIT } from '@/constants/index'import { setStorageArray, getStorageArray, removeStorageArray, getStorage, isError, Options,} from './index'import { PostOauth2LoginRefreshTokenQuery, postOauth2LoginRefreshToken, postOauth2PlatformLogin,} from '@/actions/crm-user/UserLogin'type IRequest= (urlSuffix: Request | string, method: String, options?: Options) => Promise let delayedFetches: any = [] //延迟发送的请求let isCheckingToken = false //是否在检查tokenlet tryLoginCount = 0 // 尝试登陆次数// 检验token是否快过期;const checkTokenValid = async () => { const [tokenTimestamp, oldTimestamp, refreshToken] = await getStorageArray([ 'tokenTimestamp', 'oldTimestamp', 'refreshToken', ]) const nowTimestamp = Date.parse(String(new Date())) // 当前时间 const EffectiveTimes = tokenTimestamp ? tokenTimestamp * 1000 : APPROACHING_EFFECTIVE_TIME // 有效时间 const oldTimes = oldTimestamp ? oldTimestamp : nowTimestamp // 注册时间 const valid = nowTimestamp - oldTimes <= EffectiveTimes - APPROACHING_EFFECTIVE_TIME && refreshToken ? true : false return valid}async function refreshToken ( fetchWithoutToken: IRequest , urlSuffix: Request | String, method: String, options?: Options): Promise { return new Promise(async (resolve, reject) => { delayedFetches.push({ urlSuffix, method, options, resolve, reject, }) if (!isCheckingToken) { isCheckingToken = true const refreshTokenStorage = (await getStorage('refreshToken')) as string const query: PostOauth2LoginRefreshTokenQuery = { clientId: CLIENT_ID, refreshToken: refreshTokenStorage, } postOauth2LoginRefreshToken({ query, noToken: true, showResponse: true, }).then(async data => { const error = isError(data) as any if (error) { // 登陆态失效报401(token失效的话),且重试次数未达到上限 if ( (error.statusCode < 200 || error.statusCode >= 300) && tryLoginCount < TRY_LOGIN_LIMIT ) { // 登录超时 && 重新登录 await removeStorageArray([ 'accessToken', 'refreshToken', 'tokenTimestamp', 'oldTimestamp', 'userId', ]) const loginInfo = await Taro.login() const login = async () => { try { if (tryLoginCount < TRY_LOGIN_LIMIT) { const response = await postOauth2PlatformLogin({ query: { clientId: CLIENT_ID, code: loginInfo.code, loginType: 'OFFICIAL', }, body: {}, noToken: true, }) const userAccessTokenModel = response.userAccessTokenModel const oldTimestamp = Date.parse(String(new Date())) await setStorageArray([ { key: 'accessToken', data: userAccessTokenModel!.access_token, }, { key: 'refreshToken', data: userAccessTokenModel!.refresh_token, }, { key: 'tokenTimestamp', data: userAccessTokenModel!.expires_in, }, { key: 'oldTimestamp', data: oldTimestamp }, ]) tryLoginCount = 0 } else { Taro.redirectTo({ url: '/pages/My/Authorization/index', }) } } catch (e) { tryLoginCount++ login() } login() } } else if (tryLoginCount >= TRY_LOGIN_LIMIT) { Taro.redirectTo({ url: '/pages/My/Authorization/index', }) } else { Taro.showToast({ title: error.errorText, icon: 'none', duration: 2000, complete: logout, }) } return } if (data.access_token && data.refresh_token) { const oldTimestamp = Date.parse(String(new Date())) await setStorageArray([ { key: 'accessToken', data: data.access_token }, { key: 'refreshToken', data: data.refresh_token }, { key: 'tokenTimestamp', data: data.expires_in }, { key: 'oldTimestamp', data: oldTimestamp }, ]) delayedFetches.forEach(fetch => { return fetchWithoutToken(fetch.urlSuffix, fetch.method, replaceToken(fetch.options)) .then(fetch.resolve) .catch(fetch.reject) }) delayedFetches = [] } isCheckingToken = false }) } else { // 正在登检测中,请求轮询稍后,避免重复调用登检测接口 setTimeout(() => { refreshToken(fetchWithoutToken, urlSuffix, method, options) .then(res => { resolve(res) }) .catch(err => { reject(err) }) }, 1000) } })}function logout() { // window.localStorage.clear(); // window.location.href = `${loginUrl}/logout`;}function replaceToken(options: Options = {}): Options { if (!options.noToken && options.header && (options.header as any).Authorization) { getStorage('accessToken').then(accessToken => { ;(options.header as any).Authorization = `Bearer ${accessToken}` }) } return options}export { checkTokenValid, refreshToken }复制代码
bundle大小控制
路由堆栈处理
/** * navigateTo 超过8次之后 强行进行redirectTo 否则会造成页面卡死 */const nav = Taro.navigateToTaro.navigateTo = data => { if (Taro.getCurrentPages().length > 8) { return Taro.redirectTo(data) } return nav(data)}复制代码
骨架屏
1: 问一下ui 需要多少页面写骨架屏 采用哪种方法参考项目
坑
- 不能动态设置生成Jsx
- Taro.pxTransform(10) // 小程序:rpx,H5:rem 在编译时,Taro 会帮你对样式做尺寸转换操作,但是如果是在 JS 中书写了行内样式,那么编译时就无法做替换了,针对这种情况,Taro 提供了 API Taro.pxTransform 来做运行时的尺寸转换
- css module使用必须以 module.scss结尾 // 表示自定义转换,只有文件名中包含 .module. 的样式文件会经过 CSS Modules 转换处理
- 开发前请看一遍
- 全局process 保存后会报错 not defined 重启一下
- static options = { // 继承全局样式 addGlobalClass: true }; 组件要继承权全局样式css才可以生效
- css {} 与选择器之间 一定要有空格 不然就会编译失败报错 .cssName{} ❌ .cssName {} ✅ 8.text 放view image等块级元素不生效
参考