原生微信小程序项目基建实践

1,982 阅读10分钟

一、  前言

本文适用于初次使用原生小程序开发项目,介绍一些基础必要的点,可以应用于各种业务项目,请结合官方文档进行阅读。

二、  前期准备工作

使用原生微信小程序开发,必须要仔细阅读官方开发文档,要开发一个小程序要先注册一个小程序,具体过程请参考文档开始部分

三、  初始目录结构以及介绍

image.png 在开发者工具中创建一个小程序项目的初始目录是这样的,简单介绍一个目录结构,具体的可以参考文档。

1、pages------页面文件夹

注意这里的页面在创建过程后会自动生成一个对应的页面路由地址,路由地址在app.json文件中的pages对象里。新建页面过程中建议以文件夹为单位,pages目录下选择新建文件夹-新建的文件夹下新建page。

2、utils------工具文件夹

该文件夹一般会用于存储一些常见的公共类方法。在创建工具类方法过程中也是建议按照文件夹区分不同的功能的工具方法。

3、app.js------项目的入口文件

该文件用于配置小程序的整体逻辑,这里可以设置以下:

● 按照业务需要在小程序的生命周期中写对应的业务逻辑

● 设置整个小程序运行过程中需要用到的全局变量globalData

● 一些为了服务某个全局变量初始化的方法,比如初始化自定义的设备信息全局变量deviceInfo,可以定义一个获取设备信息的方法getSystemInfo(),主体通过调用wx.getSystemInfo()来获取,再对全局变量赋值

4、app.json------小程序公共配置

介绍几个常见且必要的配置:

● pages:页面路径,初始化项目后默认配置,一般不需要配置,在新建页面过程中自动生成,排数组第一的页面是小程序的启动页面。

● window:小程序主体窗口的配置,初始化项目后默认配置,具体的修改和配置什么参考文档。

● style:初始化项目后默认配置,微信客户端 7.0 开始,UI 界面进行了大改版。小程序也进行了基础组件的样式升级。配置“style: v2” 可表明启用新版的组件样式。

● sitemapLocation:初始化项目后默认配置,指明sitemap.json的位置,默认'sitemap.json' 即在 app.json 同级目录下名字的sitemap.json 文件。

● lazyCodeLoading:初始化项目后默认配置,目前仅支持值requiredComponents ,代表开启小程序按需引入的特性。

● tabBar:页面底部导航栏,可以自定义配置也可以直接配置,该部分比较重要,后面详细介绍。

● permission:小程序接口权限的相关设置,这里的权限一般是指微信小程序官方封装的调用设备能力的API,在实际的开发中,往往会遇到来自监管部门的规定要求向用户进行征询,因此在使用接口权限时需要进行相关配置,比如,使用地理位置

  "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于标记你所发的动态所在的位置" 
    }
  }

更多权限接口能力,请参考官方文档授权部分● usingComponents:公共组件按需引入配置,比如我们写了一个公共组件content-panel,需要进行配置,才可以在页面中直接通过组件名来引用

"usingComponents": {
    "content-panel":"./components/common/content-panel/index"
}

使用自定义组件

<content-panel></content-panel>

5、app.wxss------小程序全局样式

该文件可以设置整个小程序的全局样式,比如重置某些组件样式,自定义的全局样式,后面详细介绍。

6、project.config.json、project.private.config.json------项目的配置文件

这两个文件的说明以及区别,具体可以参考文档项目配置文件部分,下面简单说明:

● 在开发阶段可以在开发者工具右上角:详情-本地设置-勾选不校验本地域名合法性,方便对接口调试数据。注意本地配置不会影响项目线上运行,在开发者工具上进行真机预览时,需要在手机小程序上设置开启开发调试模式才可以达到不校验本地域名合法性的效果。

● 在上线流程中需要在微信公众平台小程序后台:开发-开发管理-开发者ID-配置IP白名单-服务器域名-配置request合法域名-uploadFile合法域名(如果涉及)-downloadFile合法域名(如果涉及),线上小程序的接口访问才会正常

7、发布上线

发布上线流程请参考官方文档发布上线部分

四、  项目基础搭建

上面内容已经介绍了微信小程序项目初始化的目录结构以及一些目录的用途,下面开始基于一个项目开发的角度做一些基础的建设。(以下代码展示使用 “// ==== ./api/config.js ====“ 表示在项目中的路径,其中 ./ 表示根目录,即和pages同级目录)

1、自定义项目配置文件

该文件的目的是为项目提供一个接口域名统一配置的地方,以及整个项目比较基础的一些信息

// ==== ./config/devEnv.config.js ====
// 基础域名
var host = "https://www.xxx.com";
/** 当前小程序版本 */
var version = "1.0.0";
/** 请求中需要带上微信小程序的版本信息 */
var cookieVersion = `${version}`;
export { version, cookieVersion, host };

● 设置基础域名:在开发调试阶段,有时需要连接不同的测试后台环境,因此提供统一的入口去设置,就不需要每一个业务接口都去改域名前缀,做到一改全改

● 版本信息:在有的业务场景中,后台需要知道当前前端请求接口的版本,从而通过nginx做不同的转发或者在后台逻辑通过比较版本号的高低做不同的业务逻辑,这一点也许很重要,对于开发者去制定某些限制下的开发解决方案

2、工具方法

工具方法一般会有两种类型:外部工具库和自定义的

● 外部工具库

对于常见的工具库可以选择直接下载压缩版本,也可以npm安装,我这里选择直接下载压缩版本,以下工具库可以都官方去下载对应的版本

引用,比如:

import moment from '../ext/moment.min.js';
let endTime = moment(new Date().getTime()).format('YYYY-MM-DD HH:mm:ss')

● 自定义工具方法

image.png

// ==== ./utils/popup.utils.js ====

// 弹出层相关的工具方法

// 统一弹出框的外观,显示时间长度

/** toast 提示 */

export const toast = (text) => {

    wx.showToast({

    title: text,

    icon: 'none',

    duration: 2500,

    mask: false,

    })

};

/** 显示加载框 */

export const showLoading = (title) => {

    wx.showLoading({

    title: title || '加载中',

    mask: true

    });

}

/** 隐藏加载框 */

export const hideLoading = () => {

    wx.hideLoading();

}
// ==== ./utils/store.utils.js ====

// 本地数据存储方法

/**

* 获取数据

* - 处理空数据与数据异常

* - 存什么值就返回什么值

* @param {*} key

*/

const _getData = (key) => {

    try {

    let value = wx.getStorageSync(key);

    return value;

    } catch (error) {

    console.error(error);

    return null;

    }

};

/**

* 数据存储

* 单个 key 允许存储的最大数据长度为 1MB,所有数据存储上限为 10MB

* @param {*} key

* @param {*} data

*/

const _setData = (key, data) => {

    try {

    wx.setStorageSync(key, data);

    } catch (error) {

    console.error(error);

    }

}

/** 获取字符串 */

const _getString = (key) => {

    let value = _getData(key);

    return value || '';

};

/** 获取数字,默认值为 0 */

const _getNumber = (key) => {

    let value = _getData(key);

    return value || 0;

}

/** 清理指定的数据 */

const _removeData = (key) => {

    try {

    wx.removeStorageSync(key);

    } catch (error) {

    console.error(error);

    }

}

// 获取小程序版本信息

const getMPVersion = () => {

    return _getString('MP_VERSION');

}

// 设置小程序版本信息

const setMPVersion = (ver) => {

    _setData('MP_VERSION', ver);

}

// 小程序环境 - 请求地址

const getMPEnvHost = () => {

    return _getString('MP_ENV_HOST');

}

const setMPEnvHost = (envHost) => {

    _setData('MP_ENV_HOST');
    
}

// 登录cookie相关

const getAccessToken = () => {

    return _getString('ACCESS_TOKEN');

};

// 登录cookie相关

const getAccessTokenName = () => {

    return _getString('ACCESS_TOKEN_NAME');

};

const setAccessToken = (token) => {

    _setData('ACCESS_TOKEN', token);

}

const setAccessTokenName = (tokenName) => {

    _setData('ACCESS_TOKEN_NAME', tokenName);

}

const removeAccessToken = () => {

    _removeData('ACCESS_TOKEN');

}

const removeAccessTokenName = () => {

    _removeData('ACCESS_TOKEN_NAME');

}

// 平台常量值

const getPlatParams = () => {

    return _getData('PLAT_PARAMS');

}

const setPlatParams = (data) => {

    _setData('PLAT_PARAMS', data);

}

const removePlatParams = () => {

    _removeData('PLAT_PARAMS');

}

export default {

    // 基础方法

    getData: _getData,

    setData: _setData,

    removeData: _removeData,

    // 小程序版本信息

    getMPVersion,

    setMPVersion,

    // 小程序环境

    getMPEnvHost,

    setMPEnvHost,

    // 登录cookie相关

    getAccessToken,

    setAccessToken,

    setAccessTokenName,

    getAccessTokenName,

    removeAccessToken,

    removeAccessTokenName,

    // 平台常量值

    getPlatParams,

    setPlatParams,

    removePlatParams

};
// ====./utils/sys.util.js ====

import MD5 from './ext/md5.js';

import { JSEncrypt } from './ext/jsencrypt.js'

//获取应用实例

export default {

/**

* 加密

* @param string

* @returns {*}

*/

encryptor(string) {

    return MD5(string).toLocaleUpperCase();

},

/**

* JSEncrypt RSA加密

* @param string

*/

jsencrypt(string) {

    let app = getApp();

    let publicKey = app.globalData.entPlatParams.PUBLIC_KEY;

    let encryptor = new JSEncrypt(); // 新建JSEncrypt对象

    encryptor.setPublicKey(publicKey); // 设置公钥

    return encryptor.encrypt(string);

},

/**

* html转字符串

* @param str

* @returns {*}

*/

htmlToStr(str) {

    /* eslint-disable */

    var RexStr = /\<|\>|\"|\'|\&| | /g;

    str = str.replace(RexStr, function (MatchStr) {

        switch (MatchStr) {

            case '<':

            return '&lt;';

            case '>':

            return '&gt;';

            case '"':

            return '&quot;';

            case '\'':

            return '&#39;';

            case '&':

            return '&amp;';

            case ' ':

            return '&ensp;';

            case ' ':

            return '&emsp;';

            default:

            break;

        }

    });

    return str;

},

/**

* 下载文件

* @param url

* @param paramTemp

*/

download(url, paramTemp) {

    let param = {

    ...paramTemp

    };

    param.doPage = false;

    /*

    if (sorting) {

        param.sort = this.getSortStr(sorting);

    }

    */

    let formObj = param;

    if (formObj) {

        let exportFrames = document.getElementById('export-frame');

        if (exportFrames === null || exportFrames.length === 0) {

            let exportFrame = document.createElement('iframe');

            exportFrame.name = 'export-frame';

            exportFrame.hidden = 'hidden';

            exportFrame.id = 'export-frame';

            document.body.appendChild(exportFrame);

        }

        let formStr = document.createElement('form');

        formStr.method = 'post';

        formStr.hidden = 'hidden';

        formStr.action = url;

        formStr.target = 'export-frame';

        for (let key in formObj) {

        if (formObj[key] === null || formObj[key] === undefined) {

            continue;

        }

        let input = document.createElement('input');

        input.type = 'hidden';

        input.name = key;

        input.value = formObj[key];

        formStr.appendChild(input);

    }

    let $form = document.body.appendChild(formStr);

    $form.submit();

    $form.remove();

  }

}

};
// ====./utils/WxValidate.js ====

// 这是一个第三方的表单验证工具库,具体使用方法参考文档:

// https://github.com/wux-weapp/wx-extend/tree/master/src/assets/plugins/wx-validate

/**

* 表单验证

*

* @param {Object} rules 验证字段的规则

* @param {Object} messages 验证字段的提示信息

*

*/

class WxValidate {

constructor(rules = {}, messages = {}) {

Object.assign(this, {

data: {},

rules,

messages,

})

this.__init()

}

/**

* __init

*/

__init() {

this.__initMethods()

this.__initDefaults()

this.__initData()

}

/**

* 初始化数据

*/

__initData() {

this.form = {}

this.errorList = []

}

/**

* 初始化默认提示信息

*/

__initDefaults() {

this.defaults = {

messages: {

required: '这是必填字段。',

email: '请输入有效的电子邮件地址。',

tel: '请输入11位的手机号码。',

url: '请输入有效的网址。',

date: '请输入有效的日期。',

dateISO: '请输入有效的日期(ISO),例如:2009-06-23,1998/01/22。',

number: '请输入有效的数字。',

digits: '只能输入数字。',

idcard: '请输入18位的有效身份证。',

equalTo: this.formatTpl('输入值必须和 {0} 相同。'),

contains: this.formatTpl('输入值必须包含 {0}。'),

minlength: this.formatTpl('最少要输入 {0} 个字符。'),

maxlength: this.formatTpl('最多可以输入 {0} 个字符。'),

rangelength: this.formatTpl('请输入长度在 {0} 到 {1} 之间的字符。'),

min: this.formatTpl('请输入不小于 {0} 的数值。'),

max: this.formatTpl('请输入不大于 {0} 的数值。'),

range: this.formatTpl('请输入范围在 {0} 到 {1} 之间的数值。'),

}

}

}

/**

* 初始化默认验证方法

*/

__initMethods() {

const that = this

that.methods = {

/**

* 验证必填元素

*/

required(value, param) {

if (!that.depend(param)) {

return 'dependency-mismatch'

} else if (typeof value === 'number') {

value = value.toString()

} else if (typeof value === 'boolean') {

return !0

}

return value.length > 0

},

/**

* 验证电子邮箱格式

*/

email(value) {

return that.optional(value) || /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(value)

},

/**

* 验证手机格式

*/

tel(value) {

return that.optional(value) || /^1[3456789]\d{9}$/.test(value)

},

/**

* 验证URL格式

*/

url(value) {

return that.optional(value) || /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(value)

},

/**

* 验证日期格式

*/

date(value) {

return that.optional(value) || !/Invalid|NaN/.test(new Date(value).toString())

},

/**

* 验证ISO类型的日期格式

*/

dateISO(value) {

return that.optional(value) || /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value)

},

/**

* 验证十进制数字

*/

number(value) {

return that.optional(value) || /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value)

},

/**

* 验证整数

*/

digits(value) {

return that.optional(value) || /^\d+$/.test(value)

},

/**

* 验证身份证号码

*/

idcard(value) {

return that.optional(value) || /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(value)

},

/**

* 验证两个输入框的内容是否相同

*/

equalTo(value, param) {

return that.optional(value) || value === that.data[param]

},

/**

* 验证是否包含某个值

*/

contains(value, param) {

return that.optional(value) || value.indexOf(param) >= 0

},

/**

* 验证最小长度

*/

minlength(value, param) {

return that.optional(value) || value.length >= param

},

/**

* 验证最大长度

*/

maxlength(value, param) {

return that.optional(value) || value.length <= param

},

/**

* 验证一个长度范围[min, max]

*/

rangelength(value, param) {

return that.optional(value) || (value.length >= param[0] && value.length <= param[1])

},

/**

* 验证最小值

*/

min(value, param) {

return that.optional(value) || value >= param

},

/**

* 验证最大值

*/

max(value, param) {

return that.optional(value) || value <= param

},

/**

* 验证一个值范围[min, max]

*/

range(value, param) {

return that.optional(value) || (value >= param[0] && value <= param[1])

},

}

}

/**

* 添加自定义验证方法

* @param {String} name 方法名

* @param {Function} method 函数体,接收两个参数(value, param),value表示元素的值,param表示参数

* @param {String} message 提示信息

*/

addMethod(name, method, message) {

this.methods[name] = method

this.defaults.messages[name] = message !== undefined ? message : this.defaults.messages[name]

}

/**

* 判断验证方法是否存在

*/

isValidMethod(value) {

let methods = []

for (let method in this.methods) {

if (method && typeof this.methods[method] === 'function') {

methods.push(method)

}

}

return methods.indexOf(value) !== -1

}

/**

* 格式化提示信息模板

*/

formatTpl(source, params) {

const that = this

if (arguments.length === 1) {

return function() {

let args = Array.from(arguments)

args.unshift(source)

return that.formatTpl.apply(this, args)

}

}

if (params === undefined) {

return source

}

if (arguments.length > 2 && params.constructor !== Array) {

params = Array.from(arguments).slice(1)

}

if (params.constructor !== Array) {

params = [params]

}

params.forEach(function(n, i) {

source = source.replace(new RegExp("\\{" + i + "\\}", "g"), function() {

return n

})

})

return source

}

/**

* 判断规则依赖是否存在

*/

depend(param) {

switch (typeof param) {

case 'boolean':

param = param

break

case 'string':

param = !!param.length

break

case 'function':

param = param()

default:

param = !0

}

return param

}

/**

* 判断输入值是否为空

*/

optional(value) {

return !this.methods.required(value) && 'dependency-mismatch'

}

/**

* 获取自定义字段的提示信息

* @param {String} param 字段名

* @param {Object} rule 规则

*/

customMessage(param, rule) {

const params = this.messages[param]

const isObject = typeof params === 'object'

if (params && isObject) return params[rule.method]

}

/**

* 获取某个指定字段的提示信息

* @param {String} param 字段名

* @param {Object} rule 规则

*/

defaultMessage(param, rule) {

let message = this.customMessage(param, rule) || this.defaults.messages[rule.method]

let type = typeof message

if (type === 'undefined') {

message = `Warning: No message defined for ${rule.method}.`

} else if (type === 'function') {

message = message.call(this, rule.parameters)

}

return message

}

/**

* 缓存错误信息

* @param {String} param 字段名

* @param {Object} rule 规则

* @param {String} value 元素的值

*/

formatTplAndAdd(param, rule, value) {

let msg = this.defaultMessage(param, rule)

this.errorList.push({

param: param,

msg: msg,

value: value,

})

}

/**

* 验证某个指定字段的规则

* @param {String} param 字段名

* @param {Object} rules 规则

* @param {Object} data 需要验证的数据对象

*/

checkParam(param, rules, data) {

// 缓存数据对象

this.data = data

// 缓存字段对应的值

const value = data[param] !== null && data[param] !== undefined ? data[param] : ''

// 遍历某个指定字段的所有规则,依次验证规则,否则缓存错误信息

for (let method in rules) {

// 判断验证方法是否存在

if (this.isValidMethod(method)) {

// 缓存规则的属性及值

const rule = {

method: method,

parameters: rules[method]

}

// 调用验证方法

const result = this.methods[method](value, rule.parameters)

// 若result返回值为dependency-mismatch,则说明该字段的值为空或非必填字段

if (result === 'dependency-mismatch') {

continue

}

this.setValue(param, method, result, value)

// 判断是否通过验证,否则缓存错误信息,跳出循环

if (!result) {

this.formatTplAndAdd(param, rule, value)

break

}

}

}

}

/**

* 设置字段的默认验证值

* @param {String} param 字段名

*/

setView(param) {

this.form[param] = {

$name: param,

$valid: true,

$invalid: false,

$error: {},

$success: {},

$viewValue: ``,

}

}

/**

* 设置字段的验证值

* @param {String} param 字段名

* @param {String} method 字段的方法

* @param {Boolean} result 是否通过验证

* @param {String} value 字段的值

*/

setValue(param, method, result, value) {

const params = this.form[param]

params.$valid = result

params.$invalid = !result

params.$error[method] = !result

params.$success[method] = result

params.$viewValue = value

}

/**

* 验证所有字段的规则,返回验证是否通过

* @param {Object} data 需要验证数据对象

*/

checkForm(data) {

this.__initData()

for (let param in this.rules) {

this.setView(param)

this.checkParam(param, this.rules[param], data)

}

return this.valid()

}

/**

* 返回验证是否通过

*/

valid() {

return this.size() === 0

}

/**

* 返回错误信息的个数

*/

size() {

return this.errorList.length

}

/**

* 返回所有错误信息

*/

validationErrors() {

return this.errorList

}

}

export default WxValidate

2、api统一管理

在一个项目中,我们希望所有的接口请求都统一放在一个地方进行管理,提高维护性。这里会涉及两类文件,一类是配置文件,一类是具体的业务api。首先介绍配置文件

// ==== ./api/config.js ====

// 本文件用于配置不同服务api前缀

import { host, version } from '../config/devEnv.config';

export const HttpPrefix =

{

    'baseApi': host,

    // 微服务示例

    // 'governorApi': `${host}/gateway/zuul/governor`,

}

为何要新建一个配置文件呢?有可能会疑问,为何不集中在 ./config/devEnv.config.js 文件中进行暴露呢?

● 像这种功能性目录,都希望有一个配置文件,该功能目录下的所有操作都可以基于这个配置,即使把这个目录给删除了,对别人的影响降低也不会很大

● 有的后台是有微服务体系的,因此每个接口都可能属于不同的微服务,因此需要一个统一的文件对微服务接口前缀做一个统一的配置

具体的业务api在第3点介绍完再说明...

3、http请求工具方法的二次封装

学习官方文档你会发现wx.request()会存在以下几个问题

● 不支持以Promise风格调用

如果两个以上的接口是有先后调用的关系,那么只能在每一次wx.request()的成功回调中去嵌套调用新的请求,Promise的调用支持可以很好的解决这个问题,也可以简化代码结构。

● 不支持请求头信息的统一处理

每一次的接口调用前端都需要在请求头携带token信息,代码重复。

● 不支持响应信息统一处理

每次接口调用前端接收到后端的响应信息后一般会携带状态码,用于表示业务处理情况或者请求情况,这种结果需要跟用户做一个信息的反馈交互,如果不能统一处理,那么每次调用都需要对响应结果做处理。

● 不支持队列请求以及队列超时处理

如果同一个接口不断的在调用,或者不同接口同时在请求,会增大服务器请求的压力,因此这种情况也是需要做对应的控制。

基于以上存在的问题,可以基于wx.request()进行二次封装,目标就是能够支持以上基本需求。这里会涉及两个文件

● 配置文件

// ==== ./utils/request/config.js ====

// 本文件用于灵活配置http请求中需要的特殊参数

import { baseApi } from '../../api/config';

const config = {

    baseURL: `${baseApi}`,

    method: 'POST',

    data: {},

    header: {'Content-type': 'application/x-www-form-urlencoded','X-Requested-With': 'XMLHttpRequest','Cache-Control': 'no-cache'}

};

export default config;

● 具体封装实现

// ==== ./utils/request/index.js

import config from './config.js';

import app from '../../app.js';

import moment from '../ext/moment.min.js';

import storeUtil from '../store.util.js';

import { cookieVersion } from '../../config/devEnv.config';

// 返回的是一个promise

// 保留最新一次请求,用于限制重复请求

let options = null;

// 请求队列数目

let currentTaskNum = 0;

// 同一时间请求并发数目

let maxTaskNum = 8;

// 等待队列

let resPaddingTask = [];

// 请求的当前时间

let newDate = 0;

// 请求出错

let requestError = function (res, reject, opt) {

    wx.hideNavigationBarLoading();

    if (opt) {

        let endTime = moment(new Date().getTime()).format('YYYY-MM-DD HH:mm:ss')

        // console.log("失败接口请求:", opt.url, '失败接口返回:', JSON.stringify(res), '失败的时间:', endTime);

        return reject(res);

    };

};

/**

* 请求

* - url 请求地址

* - data 请求参数

* - method 请求类型

* - header 请求header

* - fastClick 快速点击限制

* - noLoading 不显示加载中(导航条)

* - noShow 不显示msg

* - ignoreNotLogin 是否忽视未登录的提示与跳转

* @param {*} option

* @param {*} resolve

* @param {*} reject

*/

let requestEvent = function (option, resolve, reject) {

    let startTime = moment(new Date().getTime()).format('YYYY-MM-DD HH:mm:ss')

    let cookie = {

    'Cookie': `${storeUtil.getAccessTokenName() || 'sessionid'}=${storeUtil.getAccessToken()}`

    }

    let header = option.header || config.header;

    header['miniversion'] = cookieVersion;

    console.log("接口请求:", option.url, option, ";请求的数据:", option.obj || option.data, '请求的时间:', startTime)

    Object.assign(header, cookie);

    wx.request({

        url: option.url,

        data: option.obj || option.data || config.data,

        method: option.method || config.method,

        header: header,

        success: function (res) {

            wx.hideNavigationBarLoading()

            // console.log(`接口响应${option.url}:`, res.data)

            resolve(res);

            let resData = res.data;

            let msg = resData.msg + '';

            if (resData.type === 'tologin') { // 未登录

                app.globalData.isLogin = false;

                storeUtil.removeAccessToken();

                storeUtil.removeAccessTokenName();

                if (!option.ignoreNotLogin) {

                    wx.reLaunch({

                        url: '/pages/login/index'

                    })

                }

                return;

            }

            if (resData && resData.show) {

                if (!option.noShow) {

                    wx.showToast({

                        title: msg, // 保证title拿到的值是字符串,不能是其他类型值(小程序语法限定)

                        icon: "none",

                        duration: 2000

                    })

                }

            }

        },

        fail: (res => requestError(res, reject, option)),

        complete: (res) => {

            currentTaskNum--;

            let completeTime = moment(new Date().getTime()).format('YYYY-MM-DD HH:mm:ss')

            console.log("接口请求完成:", option.url, '完成结果返回', res, '完成时间:', completeTime);

            if (resPaddingTask.length > 0) {

                // 有等待请求任务

                let task = resPaddingTask.shift();

                setTimeout(() => {

                    requestEvent(task.option, task.resolve, task.reject);

                }, 20);

            }

          }

     })

}

/**

* 请求

* - url 请求地址

* - data 请求参数

* - method 请求类型

* - header 请求header

* - fastClick 快速点击限制

* - noLoading 不显示加载中(导航条)

* - noShow 不显示msg

* - ignoreNotLogin 是否忽视未登录的提示与跳转

* @param {*} option 请求配置

*/

export const sendRequest = function (option) {

// 禁止0.5秒内多次点击,0.5秒内只能触发一次

if (option.fastClick) {

    let thisDate = (new Date()).getTime();

    if (options && (options.url === option.url) && (thisDate - newDate < 500)) {

        console.log('0.5秒内的点击')

        return new Promise(function (resolve, reject) {});

    }

}

// 继续走

newDate = (new Date()).getTime();

options = option;

if (option.noLoading && option.noLoading === true) {

    console.log('不显示加载中')

} else {

    wx.showNavigationBarLoading();

}

let promise = new Promise(function (resolve, reject) {

    if (currentTaskNum++ >= maxTaskNum) {

        resPaddingTask.push({

            option: option,

            resolve: resolve,

            reject: reject

        });

    } else {

        setTimeout(() => {

            requestEvent(option, resolve, reject);

        }, 20);

    }

});

return promise;

};

使用示例:

我们在api统一管理部分说过,具体的业务api可以新建一个独自的文件,比如与账户相关的业务api

// ==== ./api/account.api.js ====

// 用户信息相关接口

import { HttpPrefix } from './config';

import { sendRequest } from '../utils/request/index.js';

export default {

sendVerifyCode(obj) { // 发送验证码

    let url = HttpPrefix.baseApi + '/account/sendVerifyCode/';

        return sendRequest({

        url,

        obj,

        noShow: true

        }).then(resp => {

        return resp.data;

        });

    }

};

4、第三方组件库引入

由于不同的第三方组件库的引入可能会有些许差距,因此引入方式参照对应的组件库文档,比如引入Vant Weapp:官方文档

5、tabbar的配置

微信小程序会有两种配置tabbar的方式,一种是自定义方式,一种是默认规则配置。由于默认规则配置是在app.json中配置的,这就要求tabbar的图标文件是存放在项目本地的,如果你想使用线上图标地址,可以使用自定义方式。

● 自定义方式

1、  在app.json文件对tabbar进行配置

// 以下代码仅做示例

"tabBar": {

    "custom": true, // 表示自定义配置

    "color": "#000000",

    "selectedColor": "#07c160",

    "backgroundColor": "#000000",

    "list": [

        {

        "pagePath": "pages/index/index"

        },

        {

        "pagePath": "pages/action/index"

        },

        {

        "pagePath": "pages/mine/index"

        }

    ]

}

2、  在根目录下新建自定义tabbar文件夹

注意文件夹的名字一定要是custom-tab-bar

// ==== ./custom-tab-bar/index.wxml ====

// 以下使用了vant weapp的组件库,引入请参考第4点介绍

<view class="van-tabbar">

    <van-tabbar active="{{ active }}" bind:change="onChange" class="tabbar" wx:if="{{show}}">

        <van-tabbar-item>

            <image slot="icon" src="{{homeUrl }}" mode="aspectFit" class="image-size" />

            <image slot="icon-active" src="{{ homeActiveUrl }}" mode="aspectFit" class="image-size" />

            推荐

        </van-tabbar-item>

        <van-tabbar-item>

            <image slot="icon" src="{{actionUrl }}" mode="aspectFit" class="image-size" />

            <image slot="icon-active" src="{{ actionActiveUrl }}" mode="aspectFit" class="image-size" />

            活动

        </van-tabbar-item>

        <van-tabbar-item>

            <image slot="icon" src="{{mineUrl }}" class="image-size" />

            <image slot="icon-active" src="{{ mineActiveUrl }}" class="image-size" />

            我的

        </van-tabbar-item>

    </van-tabbar>

</view>
// ==== ./custom-tab-bar/index.json ====

{

    "component": true,

    "usingComponents": {

    }

}
// ==== ./custom-tab-bar/index.js ====

// 图标地址可以使用http地址

import api from '../api/index.js';

const app = getApp()

Component({

data: {

    show: true,

    active: null,

    homeUrl: app.globalData.baseUrl + '/bar_home_grey.svg',

    homeActiveUrl: app.globalData.baseUrl + '/bar_home_active.svg',

    actionUrl: app.globalData.baseUrl + '/bar_action_grey.svg',

    actionActiveUrl: app.globalData.baseUrl + '/bar_action_active.svg',

    mineUrl: app.globalData.baseUrl + '/bar_my_grey.svg',

    mineActiveUrl: app.globalData.baseUrl + '/bar_my_active.svg',

    list: [

        {

        text: '推荐',

        url: '/pages/index/index'

        },

        {

        text: '活动',

        url: '/pages/action/index'

        },

        {

        text: '我的',

        url: '/pages/mine/index'

        },

    ]

},

methods: {

    onChange(event) {

        // 页面跳转

        wx.switchTab({

            url: this.data.list[event.detail].url

        });

    },

    init() {

        // 处理激活状态

        // 这里需要注意的是:

        // 要写一个方法,在对应的跳转页面中去初始化

        const page = getCurrentPages().pop();

        this.setData({

        active: this.data.list.findIndex(item => item.url === `/${page.route}`)

        });

    },

}

});

在对应的目标跳转页面中,比如跳转到/pages/mine/index,在该页面的show周期内设置菜单状态

onShow: function() {

    // 初始化底部菜单

    this.getTabBar().init();

}

● 默认规则配置

在app.json文件中配置,以下代码仅做示例

"tabBar": {

    "list": [

        {

        "pagePath": "pages/home/home",

        "text": "首页",

        "iconPath": "/images/tabs/home.png",

        "selectedIconPath": "/images/tabs/home-active.png"

        },

        {

        "pagePath": "pages/message/message",

        "text": "消息",

        "iconPath": "/images/tabs/message.png",

        "selectedIconPath": "/images/tabs/message-active.png"

        },

        {

        "pagePath": "pages/contact/contact",

        "text": "联系我们",

        "iconPath": "/images/tabs/contact.png",

        "selectedIconPath": "/images/tabs/contact-active.png"

        }

    ]

}

6、第三方字体图标引入

1、在开发的过程中,也许需要引入第三方的图标库,下面介绍一下如何引入,假设我在阿里巴巴矢量图标库中将喜欢的图标添加至购物车-添加到项目,下载Font class文件包,解压之后只需要将iconfont.css文件后缀类型改成wxss,按照以下路径在项目中进行存放:

./static/iconfont/iconfont.wxss

2、为了减少本地文件的大小,我们可以只是引入第1、所说的css文件并改文件后缀wxss,而不引入其他字体文件到本地,而是采用cdn引入,需要对./static/iconfont/iconfont.wxss

文件做以下的修改:

image.png cdn地址怎么获取呢?

3、  将字体图标作为全局样式引入项目中

在app.wxss文件中引入字体文件

// ==== ./app.wxss ====

@import './static/iconfont/iconfont.wxss';

页面中引用,比如:

<icon class="iconfont icon-chehui"></icon>

五、  最后

一个小程序项目开始之前,可以按照这样的思路去做一些必要的基础搭建,当然根据项目需要,还可以写一些自定义的公共组件,公共样式等。至于写好的公共组件,该怎么引入使用,前面都有介绍到;公共样式应该按照什么样的规范去写,这个相信开发者都有一定的经验。