基于 uni-app 搭建多端框架

lxf2023-03-15 07:51:01

本篇文章分享来自小伙伴「liuxin」的一次学习总结分享,希望跟社区的同学一起探讨。

什么是 uni-app ?

uni-app 是一个使用 Vue.js 开发所有前端应用的框架

  • 支持 Vue2 和 Vue3

uni-app 的作用?

开发者只需要编写一套代码,就可以发布到 iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/钉钉/百度/头条/飞书/快手/QQ/淘宝/京东)、快应用等多个平台。

  • 快应用仅支持 vivo、OPPO、华为

快速创建

node.js 版本 @14.19.0

安装 vue-cli

全局安装 vue-cli

npm i @vue/cli@4 -g
  • 最新的 vue-cli 是 5.0.1,但是刚上线没多久 (2022-02-17),稳定性未知,所以切换到 4 版本

创建 uni-app

  1. 创建正式版
vue create -p dcloudio/uni-preset-vue uni-app-frame
  1. 执行命令后,根据提示选择模板,这里创建一个默认模板,初次学习体验建议选 Hello uni-app 模板,这个模板创建后有一套使用官方组件创建的跨端应用的 demo,开发时可以参考使用。

基于 uni-app 搭建多端框架

  1. 运行成 H5
  • 切换到项目目录,运行服务
cd uni-app-frame
npm run serve

基于 uni-app 搭建多端框架

  • 浏览器打开,输入 http://localhost:8080/

基于 uni-app 搭建多端框架

  1. 到此,一个简单的基于 uni-app 框架的模板项目就创建,并运行成功

  2. 需要注意的是,这个默认安装基于的 Vue2、webpack4

开发工具

  • 开发工具推荐使用官方的开发工具 HbuilderX,官方下载地址:www.dcloud.io/hbuilderx.h…,这个开发工具为 uni-app 做了特别强化,比较适合开发 uni-app。

  • 如果使用的是 TypeScript 默认模板,开发工具推荐使用 VS Code,但是没有 uni-app 的语法推荐,或者使用 HbuilderX 开发,调试时使用右键-外部命令-运行到相应渠道。

  • Node 版本为 12.10.0

解决兼容问题

1、引入 css-vars-ponyfill

原因:uni-app 的很多组件使用了 CSS 变量,而IE 浏览器和一些旧版浏览器不支持 CSS 变量,所以使用 css-vars-ponyfill 提供客户端支持。

  • 安装
npm i css-vars-ponyfill
  • 在 src/main.js 中第一行引用
import cssVars from 'css-vars-ponyfill'
cssVars({})

2、引入 babel-polyfill 解决 ES6 兼容

  • 安装
npm i babel-polyfill
  • 在根目录下新建 webpack.config.js,并输入一下代码
module.exports = {
  entry: {
    app: './src/main.js',
    babelPolyfill: 'babel-polyfill'
  }
}

3、引入 es6-promise

一个解决 promise 兼容的插件,在 promise 未定义或者创建失败的情况下,自动创建一个 promise

  • e安装
npm i es6-promise
  • 在 src/main.js 中最三行引入使用
import ES6Promise from 'es6-promise'
ES6Promise.polyfill()

封装常用工具

1、创建常用的工具

在src/utils/ 下创建 common.js,内容为项目常用到的一些工具

判断变量类型的工具代码如下:

const toString = Object.prototype.toString;

export function isObject(value) {
  return toString.call(value) === '[object Object]';
}

export function isString(value) {
  return toString.call(value) === '[object String]';
}

export function isNumber(value) {
  return toString.call(value) === '[object Number]';
}

export function isDefault(value) {
  return value === void 0;
}

//防抖
export function debounce(fn, delay = 200) {
  let timeout = null;
  return function() {
    timeout && clearTimeout(timeout);
    timeout = setTimeout(() => {
      fn.apply(this, arguments) //让this指向调用者,参数绑定
    }, delay);
  }
}

封装页面跳转

1、创建路由管理文件

在 src/router/ 下创建 index.js,内容为路由跳转时用到的详细信息

示例代码

/* 
 * name:路由跳转时的简称,可以使用这个直接跳转页面,省去长的路由
 * path:要跳转的路由
 * type:路由跳转方式,默认navigateTo,
 *  只能是以下的值["navigateTo", "switchTab", "reLaunch", "redirectTo"]
 */
const routes = [
  {
    name: 'index',
    path: 'pages/index/index',
    type: 'navigatorTo',
  },
  {
    name: 'test',
    path: 'pages/test/test',
  },
]

export default routes

配置说明

配置项类型必填默认值说明
nameString|Number页面跳转时使用的路由,值唯一
pathString页面跳转时真实的路由

type |

String |

否 |

navigatorTo | 跳转路由的方式 navigatorTo:保留当前页面,跳转到应用内的某个页面 redirectTo:关闭当前页面,跳转到应用内的某个页面 reLaunch:关闭所有页面,打开应用内的某个页面 switchTab:跳转到 TabBar 页面,并关闭所有非 TabBar 页面 |

注意事项

  • src/router/index.js 中的 path 必须先在 src/pages.json 中 pages 数组中配置并确保页面真实存在,src/pages.json 中的 pages 数组更新时,src/router/index.js 必须同步更新

  • src/pages.json 这个文件的更新可以在新建页面时勾选上在 pages.json 中注册,并输入标题就可以自动添加(只有使用 HBuilderX 才有这个功能)

基于 uni-app 搭建多端框架

  • src/pages.json 示例代码如下
{
  "pages": [ //pages数组中第一项表示应用启动页
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页"
      }
    },
    {
      "path": "pages/test/test",
      "style": {
        "navigationBarTitleText": "测试",
        "enablePullDownRefresh": false
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "多端框架",
    "navigationBarBackgroundColor": "#FFFFFF",
    "backgroundColor": "#FFFFFF",
    "h5": {
      "navigationStyle": "custom"
    }
  }
}
  • 引入 src/router/index.js 的目的是统一管理路由,并可以提供 src/pages.json 所不支持的配置项并进行统一处理

2、创建路由与页面跳转文件

在 src/router/ 下创建 router.js,对外提供支持跳转页面、返回页面、获取跳转页面时传递参数、返回页面时传递参数等方法的类

配置代码

import {
  isObject,
  isString,
  isNumber
} from "@/utils/common"

class MinRouter {
  constructor(options = {}) {
    this._router = options.routes
  }

  go(options) {
    let name = '';
    let path = '';
    let type = '';
    let query = {};
    let hasRoute = false;
    switch (true) {
      case isObject(options):
        ({
          name,
          query = {},
          type
        } = options);
        break;
      case isString(options):
        name = options;
        break;
      default:
        this.$showMessage();
        throw new Error('参数必须是对象或者字符串');
    }
    for (let i = 0, len = this.$minRoutes.length - 1; i <= len; i++) {
      if (this.$minRoutes[i].name === name) {
        ({
          path
        } = this.$minRoutes[i]);
        type = type || this.$minRoutes[i].type || 'navigateTo';
        hasRoute = true;
        break;
      }
    }
    if (!hasRoute) {
      this.$showMessage();
      throw new Error(`没有 ${name} 页面'`);
    }
    if (!['navigateTo', 'switchTab', 'reLaunch', 'redirectTo'].includes(type)) {
      this.$showMessage();
      throw new Error(`路由 ${name} 配置里面的type必须是以下的值['navigateTo', 'switchTab', 'reLaunch', 'redirectTo']`)
    }
    return new Promise((resolve, reject) => {
      uni[type]({
        url: `/${path}`,
        success: () => {
          getApp().globalData.query = query
          resolve();
        },
        fail: (err = {}) => {
          this.$showMessage();
          throw new Error(JSON.stringify(err));
          reject();
        }
      })
    });
  }
  getQueryData() {
    let query = getApp().globalData.query || {};
    getApp().globalData.query = {};
    return query;
  }
  back(options) {
    let delta = 1;
    let backData = "";
    let button = 0;
    switch (true) {
      case isObject(options):
        ({
          delta = 1,
          backData = "",
          button = 0
        } = options);
        break;
      case isNumber(options):
        delta = options;
        break;
      case isDefault(options):
        break;
      default:
        this.$showToast();
        throw new Error('参数不传或者类型必须是 Object 或者 Number');
    }
    let readllyDelta = button ? delta : delta - 1;
    getApp().globalData.backData = backData;
    readllyDelta > 0 && uni.navigateBack({
      delta: readllyDelta
    })
  }
  getBackData() {
    let backData = getApp().globalData.backData || "";
    getApp().globalData.backData = "";
    return backData;
  }

  showMessage(title = "敬请期待", duration = 2000) {
    uni.showToast({
      title,
      duration,
      icon: "none"
    });
  }
}

export default MinRouter

3、创建路由拦截器文件

在 src/router/ 下创建 routerInterceptor.js,引入 ./index.js 和 router.js 对页面跳转返回进行统一拦截操作

配置代码

import routes from './index'
import MinRouter from './router'
import Vue from 'vue'
import {debounce} from '@/utils/common'

// 实例化 MinRouter
const routerInterceptor = new MinRouter({
  routes
})

// 自定义插件,给 Vue 添加全局功能
const MyPlugin = function(){}
MyPlugin.install = function (Vue, options) {
  // 添加 Vue 属性 $routes 此属性为路由配置信息
  Vue.prototype.$minRoutes = options._router
  // 添加 Vue 实例方法 $go 此方法为跳转页面统一方法
  Vue.prototype.$go = debounce(options.go, 100)
  // 添加 Vue 实例方法 $getQueryData 此方法为获取跳转页面时传递的数据
  Vue.prototype.$getQueryData = options.getQueryData
  // 添加 Vue 实例方法 $back 此方法为页面返回的统一方法
  //  添加防抖的目的是解决同一个页面中,先调一次$back,然后在onUnload生命周期又调一次出现重复返回导致返回错乱
  Vue.prototype.$back = debounce(options.back, 100)
  // 添加 Vue 实例方法 $getBackData 此方法为获取页面返回时传递的数据
  Vue.prototype.$getBackData = options.getBackData
  // 添加 Vue 实例方法 $getBackData 此方法为获取页面返回时传递的数据
  Vue.prototype.$showMessage = options.showMessage
}

Vue.use(MyPlugin, routerInterceptor)

export default routerInterceptor

使用说明

方法可选参数参数类型说明
this.$go(options)name:跳转使用的路由 query:跳转时传递的数据 type:跳转方式String | Objectoptions 为 String 时:上送的值为 name,name取值为 src/router/index.js 配置中的配置options 为 Object 时:可以传 name、query、type 三个参数,其中 name 必传
this.$getQueryData()获取跳转页面时传递的数据,同一个页面只能取值一次
this.$back(options)delta:返回的页数 backData:返回时传递的数据 button:在非页面生命周期onUnload 中使用时必传1,在页面生命周期 onUnload 中使用时不传值Object | Number页面返回时调用的方法,如果只是返回一页并且是点击左上角或者系统的左右划可以不需要使用此方法 这个方法是关闭当前页面并返回上一级或多级页面
例如:
this.go(A);this.go('A');this.go('B');this.go(C);当前在C页面,要返回A页面使用this.go('C');当前在 C 页面,要返回 A 页面使用 this.back(2);返回 B 页面时可以不调用方法
this.$getBackData()获取页面返回时传递的数据,同一个页面只能接收一次

4、在 src/main.js 中引入 routreInterceptor

import routerInterceptor from './router/routerInterceptor'

5、在 src/App.vue 中添加 globalData

<script>
  export default {
    // 全局变量在此定义
    globalData: {
      query: {}, // 页面跳转时传递数据使用
      backData: '', // 页面返回时传递数据使用
    },
    onLaunch: function() {},
    onShow: function() {},
    onHide: function() {}
  }
</script>

<style>
  /*每个页面公共css */
</style>

总结

  • 以上文件创建并配置后,就可以在全局通过 this.xxx 来使用相应方法了。

  • 提供统一的路由跳转返回取参方式是为了方便管理,也给以后添加如埋点等提供统一解决入口

使用 axios 封装网络请求

1、安装 axios

npm i axios

2、创建 _axios.js 来封装请求

在 src/services/ 下创建 _axios.js,这个 js 使用 axios 来封装请求

2.1、修改 adapter,使用 uni.request 来处理请求

使用 uni.request 的目的是兼容小程序,uni.request 会自动转换成小程序专属 API 进行操作

这里分别使用 settle、buildURL 来处理响应和处理请求数据

settle:根据 HTTP 响应状态,改变 Promise 的状态,即根据响应成功失败,分别调用 resolve、reject 来做返回

buildURL:会根据传入的参数和指定的序列化方式,在 get 请求时把参数处理后拼接到 URL 上

import axios from 'axios'

// adapter 允许自定义处理请求,使用 uni.request 适配小程序端
// 返回一个 promise 并应用一个有效的响应
axios.defaults.adapter = (config) => {
  return new Promise((resolve, reject) => {
    // settle:根据 HTTP 响应状态,改变 Promise 的状态,引入省去自己处理
    const settle = require('axios/lib/core/settle')
    // buildURL:在 get 请求时,会使用这个把请求的数据以指定序列化方式拼接到 URL 上
    const buildURL = require('axios/lib/helpers/buildURL')
    uni.request({
      method: config.method.toUpperCase(),
      url: config.baseURL + buildURL(config.url, config.params, config.paramsSerializer),
      header: config.headers,
      data: config.data,
      dataType: config.dataType,
      responseType: config.responseType, // 支付宝小程序不支持
      withCredentials: config.withCredentials, // 仅 H5 支持
      complete: (response) => {
        response = {
          data: response.data,
          status: response.statusCode,
          errMsg: response.errMsg,
          header: response.header,
          config: config
        };
        settle(resolve, reject, response);
      }
    })
  })
}

2.2、添加请求拦截器

  • responseType 合法值只有 text、arraybuffer,在请求之前拦截判断上传的是否合法
  • 支付宝、快手、京东小程序只支持 POST、GET 请求,所以加上条件编译拦截判断下这几个渠道的 method 合法值
// request 拦截器,在请求之前做一些处理
axios.interceptors.request.use(
  // 请求的一些配置信息
  config => {
    console.log('请求拦截成功')
    // 检查 responseType 是否合法
    if (!['text', 'arraybuffer'].includes(config.responseType)) {
      return Promise.reject(`设置的响应类型 responseType:${config.responseType} 不合法,合法值:text、arraybuffer`)
    }
    
    // 支付宝、快手、京东小程序不支持 POST、GET 以外的请求方式
    // #ifdef MP-ALIPAY || MP-KUAISHOU || MP-JD
    if (!['POST', 'GET'].includes(config.method.toUpperCase())) {
      return Promise.reject(`请求方式 method:${config.method} 不合法,合法值:POST、GET`)
    }
    // #endif
    return config;
  },
  // 请求错误时的处理
  error => {
    console.log("请求拦截到错误: ", error);
    return Promise.reject(error);
  }
)

2.3、添加响应拦截器

对返回数据进行处理后返回

//配置 response 拦截器,对返回的数据进行处理
axios.interceptors.response.use(
  // 对响应成功进行统一处理
  (response) => {
    console.log("响应成功: ", response);
    return checkStatus(response)
  },
  // 对响应错误进行统一处理
  (error) => {
    console.log("响应失败: ", error);
    return Promise.reject(checkStatus(error))
  }
)

// 检查响应状态,做相应处理
function checkStatus(response) {
  if (response) {
    const status = response.status || -1000
    let errorInfo = ''
    let responseData = {}
    switch (status) {
      case 200:
      case 201:
      case 304:
      case 400:
        responseData = response.data;
        break
      case -1:
        errorInfo = '远程服务响应失败,请稍后重试'
        break
      case 401:
        errorInfo = '401:访问令牌无效或已过期'
        break
      case 403:
        errorInfo = '403:拒绝访问'
        break
      case 404:
        errorInfo = '404:资源不存在'
        break
      case 405:
        errorInfo = '405:请求方法未允许'
        break
      case 408:
        errorInfo = '408:请求超时'
        break
      case 500:
        errorInfo = '500:访问服务失败'
        break
      case 501:
        errorInfo = '501:未实现'
        break
      case 502:
        errorInfo = '502:无效网关'
        break
      case 503:
        errorInfo = '503:服务不可用'
        break
      default:
        errorInfo = `连接错误${status}`
        break
    }
    return {
      status,
      msg: errorInfo,
      ...responseData
    }
  }
  return {
    status: -404,
    msg: '网络异常'
  }
}

2.4、生成对外的请求函数

  • 在生成默认配置时,需要获取 baseURL,引入 src/services/autoMatchBaseUrl.js 中的自动匹配基础 url 方法
import qs from 'qs'
import autoMatchBaseUrl from './autoMatchBaseUrl'

/**
 * 基于 axios uni.request 请求
 * @param url	请求的地址
 * @param method 请求方式
 * @param timeout 请求超时时间
 * @param prefix 用来拼接 url 地址
 * @param data 请求的参数
 * @param headers 请求头
 * @param dataType 数据类型,默认 json
 * @param responseType 响应类型,仅支持 test、arraybuffer 并且支付宝不支持此配置
 * @param extraUrl 请求时额外的请求地址
 * @returns {Promise.<T>}
 * @private
 */
export default function _Axios(url, {
  method = 'post',
  timeout = 60000,
  prefix = getApp().globalData.SERVER.PREFIX,
  data = {},
  headers = {},
  dataType = 'json',
  responseType = 'text',
  extraUrl = ''
}) {

  // 拼接 url
  url = url + extraUrl

  // 合并完整的 headers
  headers = Object.assign({
    'Accept': 'application/json',
    'Content-Type': 'application/json; charset=utf-8'
  }, headers)

  // 设置默认配置
  let defaultConfig = {
    url,
    method,
    timeout,
    headers,
    responseType,
    dataType,
  }
  
  // 根据请求方式选择上送的参数,如果没有这个 get 请求会没有拼接参数,post 请求会不上送参数
  if (method.toUpperCase() === 'GET') {
    defaultConfig.params = data
  } else {
    defaultConfig.data = data
  }

  // #ifdef H5
  // 跨域时是否携带凭证,仅支持 H5
  defaultConfig.withCredentials = true
  // #endif

  return axios(defaultConfig)
}

2.5、使用 application/x-www-form-urlencoded 格式

  • 默认情况下,axios 将 JavaScript 对象序列化为 JSON。要以 application/x-www-form-urlencoded 格式发送数据,需要使用 qs 编码库,在命令行安装 qs
npm i qs
  • 使用 qs
import qs from 'qs'

// 在 _Axios 函数中加上对 qs 的使用
...
// 根据 Content-Type 处理上送数据
const contentType = headers['Content-Type'] || ''
if (~contentType.indexOf('application/x-www-form-urlencoded')) {
  defaultConfig.data = qs.stringify(data)
}
...

2.6、使用 autoMatchBaseUrl 自动匹配基础 url

  • 在 src/services/ 下创建 autoMatchBaseUrl.js
  • autoMatchBaseUrl.js 对外提供一个自动匹配基础 url 的函数,代码如下:
/**
 * @description: 根据上传前缀,自动匹配基础的 url
 *	getApp().globalData.SERVER.TEST:这个的配置在 src/App.vue 中的 globalData 中添加
 * @param {*} prefix 前缀
 * @return {*}
 */
export default function autoMatchBaseUrl(prefix){
  let baseUrl = ''
  switch (prefix) {
    default:
      baseUrl = getApp().globalData.SERVER.TEST
      break
  }
  return baseUrl
}
  • 全局变量配置
<script>
  export default {
    // 全局变量在此定义
    globalData: {
      query: {}, // 页面跳转时传递数据使用
      backData: '', // 页面返回时传递数据使用
      SERVER: { // 服务配置,基础 URL、默认前缀等
        TEST: 'http://localhost:8080/', // 这个 value 是要请求的服务地址
        PREFIX: '/test'
      }
    },
    onLaunch: function() {},
    onShow: function() {},
    onHide: function() {}
  }
</script>

<style>
  /*每个页面公共css */
</style>
  • 回到 src/service/_axios.js 中,使用 autoMatchBaseUrl 方法
import autoMatchBaseUrl from './autoMatchBaseUrl'

// 在 _Axios 函数中加上对 autoMatchBaseUrl 的使用
...
// 获取基础 url
let baseURL = autoMatchBaseUrl(prefix)

// 在 defaultConfig 中加入 baseURL 的配置
let defaultConfig = {
  baseURL,
  ...
}
...

2.5、完整代码

import axios from 'axios'
import qs from 'qs'
import autoMatchBaseUrl from './autoMatchBaseUrl'

// adapter 允许自定义处理请求,使用 uni.request 适配小程序端
// 返回一个 promise 并应用一个有效的响应
axios.defaults.adapter = (config) => {
  return new Promise((resolve, reject) => {
    // settle:根据 HTTP 响应状态,改变 Promise 的状态,引入省去自己处理
    const settle = require('axios/lib/core/settle')
    // buildURL:在 get 请求时,会使用这个把请求的数据以指定序列化方式拼接到 URL 上
    const buildURL = require('axios/lib/helpers/buildURL')
    uni.request({
      method: config.method.toUpperCase(),
      url: config.baseURL + buildURL(config.url, config.params, config.paramsSerializer),
      header: config.headers,
      data: config.data,
      dataType: config.dataType,
      responseType: config.responseType, // 支付宝小程序不支持
      withCredentials: config.withCredentials, // 仅 H5 支持
      complete: (response) => {
        response = {
          data: response.data,
          status: response.statusCode,
          errMsg: response.errMsg,
          header: response.header,
          config: config
        };
        settle(resolve, reject, response);
      }
    })
  })
}

// request 拦截器,在请求之前做一些处理
axios.interceptors.request.use(
  // 请求的一些配置信息
  config => {
    console.log('请求拦截成功')
    // 检查 responseType 是否合法
    if (!['text', 'arraybuffer'].includes(config.responseType)) {
      return Promise.reject(`设置的响应类型 responseType:${config.responseType} 不合法,合法值:text、arraybuffer`)
    }
    
    // 支付宝、快手、京东小程序不支持 POST、GET 以外的请求方式
    // #ifdef MP-ALIPAY || MP-KUAISHOU || MP-JD
    if (!['POST', 'GET'].includes(config.method.toUpperCase())) {
      return Promise.reject(`请求方式 method:${config.method} 不合法,合法值:POST、GET`)
    }
    // #endif
    return config;
  },
  // 请求错误时的处理
  error => {
    console.log("请求拦截到错误: ", error);
    return Promise.reject(error);
  }
)

//配置 response 拦截器,对返回的数据进行处理
axios.interceptors.response.use(
  // 对响应成功进行统一处理
  (response) => {
    console.log("响应成功: ", response);
    return checkStatus(response)
  },
  // 对响应错误进行统一处理
  (error) => {
    console.log("响应失败: ", error);
    return Promise.reject(checkStatus(error))
  }
)

// 检查响应状态,做相应处理
function checkStatus(response) {
  if (response) {
    const status = response.status || -1000
    let errorInfo = ''
    let responseData = {}
    switch (status) {
      case 200:
      case 201:
      case 304:
      case 400:
        responseData = response.data;
        break
      case -1:
        errorInfo = '远程服务响应失败,请稍后重试'
        break
      case 401:
        errorInfo = '401:访问令牌无效或已过期'
        break
      case 403:
        errorInfo = '403:拒绝访问'
        break
      case 404:
        errorInfo = '404:资源不存在'
        break
      case 405:
        errorInfo = '405:请求方法未允许'
        break
      case 408:
        errorInfo = '408:请求超时'
        break
      case 500:
        errorInfo = '500:访问服务失败'
        break
      case 501:
        errorInfo = '501:未实现'
        break
      case 502:
        errorInfo = '502:无效网关'
        break
      case 503:
        errorInfo = '503:服务不可用'
        break
      default:
        errorInfo = `连接错误${status}`
        break
    }
    return {
      status,
      msg: errorInfo,
      ...responseData
    }
  }
  return {
    status: -404,
    msg: '网络异常'
  }
}

/**
 * 基于 axios uni.request 请求
 * @param url
 * @param method
 * @param timeout
 * @param prefix 用来拼接url地址
 * @param data
 * @param headers
 * @param dataType
 * @param responseType
 * @param extraUrl
 * @returns {Promise.<T>}
 * @private
 */
export default function _Axios(url, {
  method = 'post',
  timeout = 60000,
  prefix = getApp().globalData.SERVER.PREFIX,
  data = {},
  headers = {},
  dataType = 'json',
  responseType = 'text',
  extraUrl = ''
}) {
  // 获取基础 url
  let baseURL = autoMatchBaseUrl(prefix)

  // 拼接 url
  url = url + extraUrl

  // 合并完整的 headers
  headers = Object.assign({
    'Accept': 'application/json',
    'Content-Type': 'application/json; charset=utf-8'
  }, headers)

  // 设置默认配置
  let defaultConfig = {
    baseURL,
    url,
    method,
    timeout,
    headers,
    responseType,
    dataType,
  }
  
  // 根据请求方式选择上送的参数,如果没有这个 get 请求会没有拼接参数,post 请求会不上送参数
  if (method.toUpperCase() === 'GET') {
    defaultConfig.params = data
  } else {
    defaultConfig.data = data
  }

  // 根据 Content-Type 处理上送数据
  const contentType = headers['Content-Type'] || ''
  if (~contentType.indexOf('application/x-www-form-urlencoded')) {
    defaultConfig.data = qs.stringify(data)
  }

  // #ifdef H5
  // 跨域时是否携带凭证,仅支持 H5
  defaultConfig.withCredentials = true
  // #endif

  return axios(defaultConfig)
}

3、创建 RESTFULLURL.js 统一管理请求地址

// key 请求时使用的方法名
// value 请求时使用的 url 需要是相对路径,不能是绝对路径
export default {
  index: 'index',
  test: 'test'
}

4、创建 index.js 对外提供接口 API

在 src/services/ 下创建 index.js,使用封装好的 axios 对外提供接口 API

import _Axios from './_axios'
import urls from './RESTFULLURL'

let services = {}

Object.keys(urls).forEach((key) => {
  services[key] = (options) => {
    // 处理掉 undefined、null 等情况
    return _Axios(urls[key], options || {})
  }
})

export default services

5、在 src/main. js 中配置 services

import services from './services'

Vue.prototype.$services = services

到此就可以使用 this.$services.xxx(options) 来请求接口了

this.$services.index({
  method: 'get',
  data: {index: '首页发的请求'}
}).then((data) => {
  console.log("data: ",data);
})

基于 uni-app 搭建多端框架

注意事项

  1. responseType:合法值只有 text、arraybuffer,并且支付宝小程序不支持此属性

如果设置了非法值,在小程序端是会报错的 triggerOnEvent called on a deprecated instance

  1. 小程序端是没有 cookie 的,需要自行处理,目前解决方案,引入 weapp-cookie 对 cookie 自动管理
npm i weapp-cookie@1.1.5

在入口 js,src/main.js 中引入 weapp-cookie

// #ifndef H5
import 'weapp-cookie'
// #endif
  1. 付宝、快手、京东小程序不支持 POST、GET 以外的请求方式

使用 Vuex

创建 uni-app 默认版本时会内置 Vuex,所以不需要安装。

使用时推荐目录结构如下:

基于 uni-app 搭建多端框架

1、配置代码

在 src/store/ 下分别创建 index.js、getters.js、mutations.js、actions.js、modules目录

其中 src/store/index.js 基本代码如下,其余文件根据使用可自行添加内容

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'

//vue的插件机制
Vue.use(Vuex);

//Vuex.Store 构造器选项
const store = new Vuex.Store({
  //存放状态
	state:{
		"username": '多端框架'
	},
  getters,
  modules:{
    
  }
})
export default store

2、在 src/main.js 中引入

import cssVars from 'css-vars-ponyfill'
cssVars({})
import ES6Promise from 'es6-promise'
ES6Promise.polyfill()
import Vue from 'vue'
import App from './App'
import routerInterceptor from './router/routerInterceptor'
import store from './store'

Vue.prototype.$store = store
Vue.config.productionTip = false

App.mpType = 'app'


const app = new Vue({
  store,
  ...App
})
app.$mount()

使用时可根据 Vuex 官方文档配置使用

引入 uni-ui

1、安装 sass

npm i sass -D

2、安装 sass-loader

npm i sass-loader@10.1.1 -D

3、安装 uni-ui

npm i @dcloudio/uni-ui

4、配置 easycom

在 src/pages.json 中配置以下代码,只需要新增 easycom 配置

{
  
  "easycom": {
    "autoscan": true,
    "custom": {
      "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
    }
  }
}

注意

  • 使用 npm 安装的组件,默认情况下 babel-loader 会忽略所有 node_modules 中的文件 ,导致条件编译失效,需要通过配置 vue.config.js 解决:
module.exports = {
   transpileDependencies: ['@dcloudio/uni-ui']
}
  • uni-ui 的使用详见官方文档 uni-ui

uni-app 的避坑指南

1、运行报 TS2304 错误

用 vue-cli 生成 uni-app 默认模板 (TypeScript) 后,在 HbuilderX 中运行到微信小程序报 TS2304 错误

Starting type checking service...
Using 1 worker with 2048MB memory limit
No type errors found
Version: typescript 3.9.10
Time: 1809ms
[tsl] ERROR at main.ts:1
TS2304:Cannot find name 'wx'.
ERROR  Build failed with errors.

可以使用外部命令运行成微信

npm run dev:map-weixin

2、小程序和 App 是没有浏览器专用的 js 对象的

小程序和 App 的 js 运行在 jscore 下而不是浏览器里,没有浏览器专用的 js 对象,比如 document、xmlhttp、cookie、window、location、navigator、localstorage、websql、indexdb、webgl等对象。

3、小程序端不支持自动保持 cookie

小程序端不支持自动保持 cookie,服务器应避免验证 cookie。如果服务器无法修改,也可以使用一些模拟手段,比如可以请求时带上 cookie 并将响应的 cookie 保存在本地,也可以借助 weapp-cookie 插件来实现。