uni-app开发小程序:项目架构以及经验分享

uni-app开发小程序:项目架构以及经验分享

2022年的时候,公司为了快速完成产品并上线,所以选用微信小程序为载体;由于后期还是打算开发App;虽然公司有iosAndroid,但是如果能一套代码打包多端,一定程度上可以解决成本。前端技术栈也是vue,在考察选择了uni-app。后来多个小程序项目都采用了uni-app开发,积累了一定的经验以及封装了较多业务组件,这里就分享一下uni-app项目的整体架构、方法封装组件库选择以及注意事项。全文代码都会放到github,先赞后看,月入百万!

创建项目

uni-app提供了两种创建项目的方式:

⚠️需要注意的是,一定要根据项目需求来选择项目的创建方式;如果只是单独的开发小程序App,且开发环境单一,可以使用HBuilderX可视化工具创建。如果多端开发,以及同一套代码可能会打包生成多个小程序建议使用vue-cli进行创建,不然后期想搞自动化构建以及按指定条件进行编译比较痛苦。关于按条件编译,文章后面会有详细说明。

使用vue-cli安装和运行:

1.全局安装 vue-cli

npm install -g @vue/cli

2.创建uni-app

vue create -p dcloudio/uni-preset-vue 项目名称

3.进入项目文件夹

cd 项目名称

4.运行项目,如果是已微信小程序为主,可以在package.json中的命令改为:

"scripts": {
    "serve": "npm run dev:mp-weixin"
}

然后执行

npm run serve

使用cli创建项目默认不带css预编译,需要手动安装一下,这里已sass为例:

npm i sass --save-dev
npm i sass-loader --save-dev

整体项目架构

通过HBuilderX或者vue-cli创建的项目,目录结构有稍许不同,但基本没什么差异,这里就按vue-cli创建的项目为例,整体架构配置如下:

    ├──dist 编译后的文件路径
    ├──package.json 配置项
    ├──src 核心内容
        ├──api 项目接口
        ├──components 全局公共组件
        ├──config 项目配置文件
        ├──pages 主包
        ├──static 全局静态资源
        ├──store vuex
        ├──mixins 全局混入
        ├──utils 公共方法
        ├──App.vue 应用配置,配置App全局样式以及监听
        ├──main.js Vue初始化入口文件
        ├──manifest.json 配置应用名称、appid等打包信息
        ├──pages.json 配置页面路由、导航条、选项卡等页面类信息
        └──uni.scss 全局样式

封装方法

工欲善其事,必先利其器。在开发之前,我们可以把一些全局通用的方法进行封装,以及把uni-app提供的api进行二次封装,方便使用。全局的公共方法我们都会放到/src/utils文件夹下。

封装常用方法

下面这些方法都放在/src/utils/utils.js中,文章末尾会提供github链接方便查看。如果项目较大,建议把方法根据功能定义不同的js文件。

小程序Toast提示

/**
 * 提示方法
 * @param {String} title 提示文字
 * @param {String}  icon icon图片
 * @param {Number}  duration 提示时间
 */
export function toast(title, icon = 'none', duration = 1500) {
    if(title) {
        uni.showToast({
            title,
            icon,
            duration
        })
    }
}

缓存操作(设置/获取/删除/清空)

/**
 * 缓存操作
 * @param {String} val
 */
export function setStorageSync(key, data) {
    uni.setStorageSync(key, data)
}

export function getStorageSync(key) {
    return uni.getStorageSync(key)
}

export function removeStorageSync(key) {
    return uni.removeStorageSync(key)
}

export function clearStorageSync() {
    return uni.clearStorageSync()
}

页面跳转

/**
 * 页面跳转
 * @param {'navigateTo' | 'redirectTo' | 'reLaunch' | 'switchTab' | 'navigateBack' | number } url  转跳路径
 * @param {String} params 跳转时携带的参数
 * @param {String} type 转跳方式
 **/
export function useRouter(url, params = {}, type = 'navigateTo') {
    try {
        if (Object.keys(params).length) url = `${url}?data=${encodeURIComponent(JSON.stringify(params))}`
        if (type === 'navigateBack') {
            uni[type]({ delta: url })
        } else {
            uni[type]({ url })
        }
    } catch (error) {
        console.error(error)
    }
}

图片预览

/**
 * 预览图片
 * @param {Array} urls 图片链接
 */
export function previewImage(urls, itemList = ['发送给朋友', '保存图片', '收藏']) {
    uni.previewImage({
        urls,
        longPressActions: {
            itemList,
            fail: function (error) {
                console.error(error,'===previewImage')
            }
        }
    })
}

图片下载

/**
 * 保存图片到本地
 * @param {String} filePath 图片临时路径
 **/
export function saveImage(filePath) {
    if (!filePath) return false
    uni.saveImageToPhotosAlbum({
        filePath,
        success: (res) => {
            toast('图片保存成功', 'success')
        },
        fail: (err) => {
            if (err.errMsg === 'saveImageToPhotosAlbum:fail:auth denied' || err.errMsg === 'saveImageToPhotosAlbum:fail auth deny') {
                uni.showModal({
                    title: '提示',
                    content: '需要您授权保存相册',
                    showCancel: false,
                    success: (modalSuccess) => {
                        uni.openSetting({
                            success(settingdata) {
                                if (settingdata.authSetting['scope.writePhotosAlbum']) {
                                    uni.showModal({
                                        title: '提示',
                                        content: '获取权限成功,再次点击图片即可保存',
                                        showCancel: false
                                    })
                                } else {
                                    uni.showModal({
                                        title: '提示',
                                        content: '获取权限失败,将无法保存到相册哦~',
                                        showCancel: false
                                    })
                                }
                            },
                            fail(failData) {
                                console.log('failData', failData)
                            }
                        })
                    }
                })
            }
        }
    })
}

更多函数就不在文章中展示了,已经放到/src/utils/utils,js里面,具体可以到github查看。

请求封装

为了减少在页面中的请求代码,所以我们要对uni-app提供的请求方式进行二次封装,在/src/utils文件夹下建立request.js,具体代码如下:


import {toast, clearStorageSync, getStorageSync, useRouter} from './utils'
import {BASE_URL} from '@/config/index'

const baseRequest = async (url, method, data, loading = true) =>{
    header.token = getStorageSync('token') || ''
    return new Promise((reslove, reject) => {
        loading && uni.showLoading({title: 'loading'})
        uni.request({
            url: BASE_URL + url,
            method: method || 'GET',
            header: header,
            timeout: 10000,
            data: data || {},
            success: (successData) => {
                const res = successData.data
                uni.hideLoading()
                if(successData.statusCode == 200){
                    // 这里根据自己的业务逻辑去调整
                    if(res.resultCode == 'PA-G998'){
                        clearStorageSync()
                        useRouter('/pages/login/index', 'reLaunch')
                    }else{
                        reslove(res.data)
                    }
                }else{
                    toast('网络连接失败,请稍后重试')
                    reject(res)
                }
            },
            fail: (msg) => {
                uni.hideLoading()
                toast('网络连接失败,请稍后重试')
                reject(msg)
            }
        })
    })
}

const request = {};

['options', 'get', 'post', 'put', 'head', 'delete', 'trace', 'connect'].forEach((method) => {
    request[method] = (api, data, loading) => baseRequest(api, method, data, loading)
})

export default request

请求封装好以后,我们在/src/api文件夹下按业务模块建立对应的api文件,拿获取用户信息接口举例子:

/src/api文件夹下建立user.js,然后引入request.js

import request from '@/utils/request'

//个人信息
export const info = data => request.post('/v1/api/info', data)

在页面中直接使用:

import {info} from '@/api/user.js'

export default {
    methods: {
        async getUserinfo() {
            let info = await info()
            console.log('用户信息==', info)
        }
    }
}

自定义tabBar

uni-app或者小程序基本避不开这个话题了,很多情况下,官方提供的tabBar方案并不能满足产品需求/ui要求,官方也提供了自定义tabBar的方案,但此方案有很多弊端,比如:切换时候会tabBar会有明显的闪动。可以参考之前写的文章小程序自定义TabBar 如何实现“keep-alive”,文章中是原生小程序,但思路在uni-app中同样适用,如果感兴趣,可以评论区提问。

版本切换

很多场景下,需要根据不同的环境去切换不同的请求域名、APPID等字段,这时候就需要通过环境变量来进行区分。下面案例我们就分为三个环境:开发环境(dev)、测试环境(test)、生产环境(prod)。

建立env文件

在项目根目录建立下面三个文件并写入内容(常量名要以VUE开头命名):

.env.dev(开发环境)

VUE_APP_MODE=dev
VUE_APP_ID=wxbb53ae105735a06b
VUE_APP_BASE=https://www.baidu.dev.com

.env.test(测试环境)

VUE_APP_MODE=test
VUE_APP_ID=wxbb53ae105735a06c
VUE_APP_BASE=https://www.baidu.test.com

.env.prod(生产环境)

VUE_APP_MODE=wxbb53ae105735a06d
VUE_APP_ID=prod
VUE_APP_BASE=https://www.baidu.prod.com

修改package.json文件

"scripts": {
    "dev:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --mode dev",
    "build:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --mode prod"
},

然后执行

npm run dev:mp-weixin

/src/pages/index/index.vue下,打印:

onLoad() {
    console.log(process.env.VUE_APP_MODE, '====VUE_APP_BASE') 
    console.log(process.env.VUE_APP_BASE, '====VUE_APP_BASE')
},

此时输出结果就是

// dev ====VUE_APP_BASE
// https://www.baidu.dev.com ====VUE_APP_BASE

动态修改appid

如果同一套代码,需要打包生成多个小程序,就需要动态修改appid了;文章开头说过appid在/src/manifest.json文件中配置,但json文件又不能直接写变量,这时候就可以参考官方 提出的解决方案:建立vue.config.js文件,具体操作如下。

在根目录下建立vue.config.js文件写入以下内容:

// 读取 manifest.json ,修改后重新写入
const fs = require('fs')

const manifestPath = './src/manifest.json'
let Manifest = fs.readFileSync(manifestPath, { encoding: 'utf-8' })
function replaceManifest(path, value) {
    const arr = path.split('.')
    const len = arr.length
    const lastItem = arr[len - 1]
    let i = 0
    let ManifestArr = Manifest.split(/\n/)
    for (let index = 0; index < ManifestArr.length; index++) {
        const item = ManifestArr[index]
        if (new RegExp(`"${arr[i]}"`).test(item)) ++i
        if (i === len) {
            const hasComma = /,/.test(item)
            ManifestArr[index] = item.replace(
            new RegExp(`"${lastItem}"[\\s\\S]*:[\\s\\S]*`),
            `"${lastItem}": ${value}${hasComma ? ',' : ''}`
            )
            break
        }
    }

Manifest = ManifestArr.join('\n')
}
// 读取环境变量内容
replaceManifest('mp-weixin.appid', `"${process.env.VUE_APP_ID}"`)

fs.writeFileSync(manifestPath, Manifest, {
  flag: 'w'
})

如果是通过HBuilderX可视化工具创建的项目,则无法去自动根据环境去修改appid,只能去手动修改。

组件库

uni-app最受欢迎的可能就是插件市场了,插件市场提供了很多优秀的插件/组件库供我们选择,比较火的就是自家的uni-ui以及uView UI,大部分组件还是比较好用的,如果做中大型项目以及UI要求较高的情况下,还是比较推荐自己搭一套组件库,方便扩展以及维护。

结尾

关于uni-app项目的起步工作就到这里了,后面有机会写一套完整的uni搭建电商小程序项目,记得关注。代码已经提交到github,如果对你有帮助,记得点个star!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容