dva+ts+taro 小程序构建-资料总汇

思路

  1. 技术选型
    taro+dva+typescript
  2. 目录设计
  3. 组件设计
    ui设计规范: 抽象ui组件 组件props注释 页面公共交互行为: 逻辑组件
    页面公共组件: 业务组件
  4. 模块划分 => 尽量解藕
    view划分:模块之间不可相互调用,只可以通过redux事件通信
    model划分: 抽离业务模块公共逻辑 a. 公共节点公用model(全局挂载)
    b. 不共用节点model(对象合并)
  5. 脚本编写 => 将重复的 复制粘贴可完成的代码抽离 编写脚本自动写入 a. model,page,component新建 脚本
    b. actions调用fetch 脚本
  6. utils编写 oss,map,fetch,storage,format(number,sting,date),hooks
  7. types 全局state => 保证ts 静态类型检查
  8. constants常量
    taro方法改写,全局enum
  9. 代码风格统一配置 => 通过代码风格统一保证代码整洁,规范,可维护性和可读性
    .prettierrc ,eslint, tsconfig, readme ,husky

todo

  1. pageModel封装 分页 搜索
  2. 全局type挂载整理
  3. 全局active 态 css调试
  4. Divider model组件更改
  5. moment map 打包有问题
  6. 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构建的适配不同端

  1. 不能动态设置生成Jsx
  2. Taro.pxTransform(10) // 小程序:rpx,H5:rem
    在编译时,Taro 会帮你对样式做尺寸转换操作,但是如果是在 JS 中书写了行内样式,那么编译时就无法做替换了,针对这种情况,Taro 提供了 API Taro.pxTransform 来做运行时的尺寸转换
  3. css module使用必须以 module.scss结尾 // 表示自定义转换,只有文件名中包含 .module. 的样式文件会经过 CSS Modules 转换处理
  4. 开发前请看一遍 Taro 规范
  5. 全局process 保存后会报错 not defined 重启一下
  6. static options = { // 继承全局样式 addGlobalClass: true }; 组件要继承权全局样式css才可以生效
  7. css {} 与选择器之间 一定要有空格 不然就会编译失败报错 .cssName{} ❌ .cssName {} ✅ 8.text 放view image等块级元素不生效

参考

  1. 怎么合理使用微信登录能力

转载于:https://juejin.im/post/5d1041436fb9a07eeb13b2fa

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值