思路
- 技术选型
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 单例 js单例模式的es5实现和es6实现,以及通用惰性单例实现
难点探索
上传下载图片 oss base64
骨架屏+上拉刷新+下拉加载分页+emptyPage+404Page组件处理
登录拦截和跳转返回
把这些逻辑写在model里,调用只传 logintype
静默登陆
// fetch.ts
import 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<T>(
urlSuffix: String,
method: String = 'GET',
options: Options
): Promise<T> {
// 设置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<T>(urlSuffix, method, currentOptions)
}
if (!currentOptions.noToken) {
const accessToken = await getStorage('accessToken')
currentOptions.header.Authorization = `Bearer ${accessToken}`
const tokenValid = await checkTokenValid()
// if (tokenValid) {
return _fetch<T>(urlSuffix, method, currentOptions);
// }
// return refreshToken<T>(_fetch, urlSuffix, method, currentOptions)
}
return _fetch<T>(urlSuffix, method, currentOptions)
}
// 设置请求头 不包括 token
const 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<any> | 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<T>(
urlSuffix: Request | String,
method: String = 'GET',
options: Options
): Promise<T> {
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<T>(_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.ts
import 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<T> = (urlSuffix: Request | string, method: String, options?: Options) => Promise<T>
let delayedFetches: any = [] //延迟发送的请求
let isCheckingToken = false //是否在检查token
let 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<T>(
fetchWithoutToken: IRequest<T>,
urlSuffix: Request | String,
method: String,
options?: Options
): Promise<T> {
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.navigateTo
Taro.navigateTo = data => {
if (Taro.getCurrentPages().length > 8) {
return Taro.redirectTo(data)
}
return nav(data)
}
复制代码
骨架屏
小程序构建骨架屏的探索
React 中同构(SSR)原理脉络梳理
react服务端渲染demo (基于Dva)
1: 问一下ui 需要多少页面写骨架屏 采用哪种方法
参考项目
首个 Taro 多端统一实例 - 网易严选
用 React 编写的基于Taro + Dva构建的适配不同端
坑
- 不能动态设置生成Jsx
- Taro.pxTransform(10) // 小程序:rpx,H5:rem
在编译时,Taro 会帮你对样式做尺寸转换操作,但是如果是在 JS 中书写了行内样式,那么编译时就无法做替换了,针对这种情况,Taro 提供了 API Taro.pxTransform 来做运行时的尺寸转换 - css module使用必须以 module.scss结尾 // 表示自定义转换,只有文件名中包含 .module. 的样式文件会经过 CSS Modules 转换处理
- 开发前请看一遍 Taro 规范
- 全局process 保存后会报错 not defined 重启一下
- static options = { // 继承全局样式 addGlobalClass: true }; 组件要继承权全局样式css才可以生效
- css {} 与选择器之间 一定要有空格 不然就会编译失败报错 .cssName{} ❌ .cssName {} ✅ 8.text 放view image等块级元素不生效
参考