用vue/cli 和uni-app搭建小程序

404 阅读6分钟

写在前面

在开发中有涉及到小程序的业务,但是网上看 vue/cli创建的案例比较少,根据uni-app生成的不太符合我的开发习惯,所以有了这篇文章。

前期准备工作-框架选用

公司的技术栈以 vue为主, 需要开发微信小程序和支付宝小程序 当时比较了下当前业务开发量和未来可能的开发拓展未涉及到数据量特别大的情况,不像电商小程序一样应该是属于中型项目。开发小程序其中最重要的我认为是 功能完成度 和 性能体验 比较了当前的一些框架 wepy/mpvue/uni-app/taro 看了一些测试性能文章 当页面组件太多时,无法使用wepy/mpvue这2个框架。 taro 主流是react的 对于大型项目更舒适(这个没有试过) uni-app vue 比较适用中型项目,最主要的可以一套代码多个平台 当然uni-app也看到些不好的评价,比如说有bug,要踩坑,在实际使用过程中 我还是遵循 uni API 提供的那些,多看文档吧。

项目初始化及其配置

我们通过使用uni-app的文档安装好项目后,采用hello的模版,然后为了方便开发和像开放web端类似,我们那更改了一些配置如下。

  1. 删掉无用的文件 留下 public src .editorconfig .eslintrc.js .gitignore babel.config.js gen-routers.js package.json package-lok.json postcss.config.js project.config.json tsconfig.json vue.config.js README.md

  2. scr 删掉所有文件,创建基本的文件 apis(接口文件) components(自己写的公共组件) config(主要是路由) constants(常量) dicts(字典表) mxins(主要是getDate封装) pages(分包的主包文件) static(静态资源) store(vuex) style(样式文件) utils(工具文件) App.vue(入口文件) main.js(引入Vue store App uView, 然后Vue.use(uView)) manifest.json (uni-app的一些配置) page.json(页面配置文件,也是分包的,这个是uni-app封装的) uni.scss(主要是uni的初始化的css文件)

  3. 页面文件不止是pages(home login mine o) 还有 分包文件 pagesHome pagesLogin pagesMine 这些要和对应的 config文件中的router和 page.json相对应,而且,gen-router的部分逻辑也是根据文件结构来的。

在packge.json安装一些依赖和更改一些script脚本

  1. 安装 uview-ui(组件库)

  2. 安装 vue-i18n": "^8.22.4" "dolphin-i18n": "^1.5.2"并在script脚本加"i18n:async": "i18n easy -a","i18n:async": "i18n easy -a",两个命令(国际化多语言,可以选择添加不添加)

  3. 安装gitCZ (提交代码产生记录 配合这个"conventional-changelog-cli"使用)

  4. 安装eslint(配合使用husky和lint-staged,配置网上找下 功能是为了提交规范和 lint-staged能够让lint只检测暂存区的文件)

5.安装(crypto-js 加密的)

  1. 安装moment

  2. script添加命令"genrouters": "node ./gen-routers.js" (这个看下uni-app有没有这个功能,文件的功能是自动生成页面文件不用自己写)

  3. 更改脚本文件("build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build --minimize" --minimize 是为了减少主包 vendor.js的体积)

  4. 添加 vue.config.js(因为公司的原因添加为了方便和后端调试) 并安装插件 new MomentLocalesPlugin(), 因为 moment 库有很多其他语言的,我们需要去掉,只要中英文的。

postcss.config.js tsconfig.json .editorconfig .eslintrc.js .gitignore babel.config.js (按照自己需求配置)

封装接口请求文件

import {
  DEV_PROXY,
  DEVELOP,
  ENV_BASEURL_MAP,
  LOGIN_EXCEPTION_CODES,
  RELEASE, RELEASE_BASEURL,
  REQUEST_SUCCESS,
  TIME_OUT,
  TOKEN_KEY,
  USER_INFO_KEY,
} from '../constants';
import { showToast } from './helps';
import store from '../store';
import { SET_TOKEN, SET_USER_INFO } from '../store/mutation-types';
import { md5Crypto } from './encrypt';
import { routers } from '../config';

const { VUE_APP_PLATFORM: UNI_PLATFORM, NODE_ENV } = process.env;

function http({
  method = 'GET',
  url,
  headers = {},
  dataType = 'json',
  data = {},
  timeout = TIME_OUT,
  successNotify = false,
  errorNotify = true,
  failNotify = true,
}) {
  const { encryptPhone } = data;
  // eslint-disable-next-line no-async-promise-executor
  return new Promise(async (resolve, reject) => {
    const baseUrl = await getBaseUrl(url);
    url = `${baseUrl}${url}`;
    console.log(`接口地址:${url},入参:${data}`);
    uni.request({
      url,
      header: genHeaders(headers, encryptPhone),
      data,
      timeout,
      dataType,
      method: method.toUpperCase(),
      success({ data: response, statusCode }) {
        const { code, msg, data: resData } = response || {};
        if (statusCode > 199 && statusCode < 300) {
          if (code === REQUEST_SUCCESS) {
            resolve(resData);
            if (successNotify) showToast({ title: msg });
          } else {
            if (errorNotify) showToast({ title: msg });
            if (LOGIN_EXCEPTION_CODES.includes(code)) handlerLoginException();
            reject(response);
          }
        } else {
          reject(response);
          catchHttpError(statusCode);
        }
      },
      fail(err) {
        const { errMsg } = err;
        if (failNotify) {
          if (errMsg.indexOf('timeout') > -1 || errMsg.indexOf('超时') > -1) {
            catchHttpError('timeout');
          } else if (errMsg.indexOf('ERR_ADDRESS_UNREACHABLE') > -1 || errMsg.indexOf('断开') > -1 || errMsg.indexOf('fail') > -1) {
            catchHttpError('fail');
          }
        }
        reject(err);
      },
    });
  });
}

function genHeaders(headers, encryptPhone) {
  const token = uni.getStorageSync(TOKEN_KEY);
  const expandHeaders = {
    'content-type': 'application/json',
  };
  expandHeaders.platType = 'p2';
  if (token) {
    expandHeaders.Authorization = `Bearer ${token}`;
  }
  if (encryptPhone) {
    const timestamp = new Date().getTime();
    const appSecret = '5Ughg034jjo943jJ';
    expandHeaders['X-auth-sign'] = `Bearer ${md5Crypto(encryptPhone, timestamp, appSecret)}`;
    expandHeaders['X-auth-ts'] = `Bearer ${timestamp}`;
  }
  return {
    ...expandHeaders,
    ...headers,
  };
}

['options',
  'get',
  'head',
  'post',
  'put',
  'delete',
].forEach((method) => {
  http[method] = (url, data = {}, config = {}) => {
    let options = {};
    if (['post', 'put'].includes(method)) {
      options = {
        method,
        url,
        data,
        ...config,
      };
    } else {
      const { params, ...otherOptions } = data;
      options = {
        method,
        url,
        ...otherOptions,
      };
      delete options.data;
      if (params) {
        options.data = params;
      }
    }
    return http(options);
  };
});

function catchHttpError(status) {
  let msg = '';
  switch (status) {
    case 404:
      msg = '连接不上远程服务器,请检查网络!';
      break;
    case 500:
      msg = '服务异常稍后重试!';
      break;
    case 503:
      msg = '服务不可用,请稍后重试!';
      break;
    case 'timeout':
      msg = '超时稍后重试!';
      break;
    case 'fail':
      msg = '网络已断开,请检查网络!';
      break;
    default:
      break;
  }
  if (msg) showToast({ title: msg });
}
let handlerLoginExceptionTimer = null;
// 捕获登录异常
function handlerLoginException() {
  uni.removeStorageSync(USER_INFO_KEY);
  uni.removeStorageSync(TOKEN_KEY);
  store.commit(SET_USER_INFO, {});
  store.commit(SET_TOKEN, null);
  if (handlerLoginExceptionTimer) clearTimeout(handlerLoginExceptionTimer);
  handlerLoginExceptionTimer = setTimeout(() => {
    uni.navigateTo({ url: routers.login });
  }, 2000);
}

// 获取环境
async function getEnv() {
  return new Promise((resolve) => {
    if (UNI_PLATFORM === 'mp-weixin') {
      resolve(wx.getAccountInfoSync().miniProgram.envVersion || RELEASE);
    } else if (UNI_PLATFORM === 'mp-alipay') {
      my.getRunScene({
        success({ envVersion }) {
          resolve(envVersion);
        },
        fail() {
          resolve(RELEASE);
        },
      });
    } else {
      resolve(RELEASE);
    }
  });
}

// 获取基础路径
async function getBaseUrl(url) {
  let baseUrl = RELEASE_BASEURL;
  if (UNI_PLATFORM === 'h5') {
    return '';
  }
  const env = await getEnv();
  if (NODE_ENV === DEVELOP) {
    const matchKey = Object.keys(DEV_PROXY).find((key) => new RegExp(key, 'ig').test(url));
    baseUrl = matchKey ? DEV_PROXY[matchKey] : ENV_BASEURL_MAP[DEVELOP];
  } if (ENV_BASEURL_MAP[env]) {
    baseUrl = ENV_BASEURL_MAP[env];
  }
  return baseUrl;
}

export default http;

业务代码书写

这里就不进行阐述了

特别实在的优化总结

在开放过程中遇到的问题或者做的优化

  1. 必须分包 为了更好的加载体验和2M的限制,分包要注意的点不在细说,我是根据uni-app的文档进行的分包,觉得还不错。

  2. 大的图片最好都放在CDN服务上,理由大家都懂。

  3. 一些信息可以放在缓存中,不需要重复请求,比如位置信息,比如用户不敏感信息,或者某些图标文件,但是退出的时候记得清除缓存

  4. 一些经常setData的可以封装成组件的就最好封装,比如有个倒计时功能,需要频繁的更新setData,此处就适合将该定时器提取为组件,让其在组件内部数据更新,不影响页面的其它部分。

  5. 页面的定时器,离开页面或者关闭页面,需要清除的最好清除。比如A页面上有个定时器,此时打开了B页面,A页面的定时器还在运行,继续抢占B页面的资源,在onUnLoad生命周期钩子函数中执行。

  6. 减少或者合并一些loading,为了更好的用户的体验。

  7. 一些库的文件可以不使用库抽离文件使用或者使用CDN资源,比如uni-app的地图,或者一些加密功能库,我的做法是把对应的js文件抽离出来,直接引入js文件而不是库。

  8. 可以封装一些scss 函数,直接引用,或者抽离常用的scss文件引用。

写在后面

当然优化基本无止境,如果大家有什么意见,或者开发中遇到的问题和好的方法,欢迎分享!!