# 微信小程序-商城实战
# 1. 起步
# 1.1. uni-app
uni-app 是一个使用 Vue.js 开发所有前端应用的框架
开发者编写一套代码,可发布到:
- iOS
- Android
- Web(响应式)
- 各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)
- 快应用
# 1.2. HBuilderX
说明:
- 推荐使用 HBuilderX (opens new window) 开发工具
- 好处:模板丰富,完善的智能提示,一键运行
安装 scss 编译插件:
# 1.3. 新建 uni-app 项目
步骤:
- 文件(菜单) -> 新建 -> 1.项目
- uni-ui 项目模板
- vue.js v2
# 1.4. 目录结构
一个 uni-app 项目,默认包含如下目录及文件:
┌─components uni-app组件目录
│ └─comp-a.vue 可复用的a组件
├─pages 业务页面文件存放的目录
│ ├─index
│ │ └─index.vue index页面
│ └─list
│ └─list.vue list页面
├─static 存放应用引用静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此
├─main.js Vue初始化入口文件
├─App.vue 应用配置,用来配置小程序的全局样式、生命周期函数等
├─manifest.json 配置应用名称、appid、logo、版本等打包信息
└─pages.json 配置页面路径、页面窗口样式、tabBar、navigationBar 等页面类信息
# 1.5. 运行到微信开发者工具
1.在项目的微信小程序配置,填写 appid:
位置:
uni-app 项目/manifest.json
源码视图:
{ // 对应 project.config.json 配置文件 "mp-weixin" : { "appid" : "wx1fe40ee133fd4afb", "setting" : { "urlCheck" : false }, "usingComponents" : true }, }
2.在运行配置中,添加微信开发者工具安装目录:
位置:工具(菜单)-> 设置
源码视图:
{ "editor.colorScheme" : "Atom One Dark", "editor.fontSize" : 14, "editor.insertSpaces" : true, "editor.tabSize" : 2, "editor.showDefaultEndOfLine" : "\\n", // 微信开发者工具安装目录 "weApp.devTools.path" : "C:\\install\\微信web开发者工具" }
3.开启微信开发者工具的服务端口:
- 位置:设置 -> 安全(页签)
4.运行
- 选中项目根目录
- 运行(菜单)-> 运行到小程序模拟器 -> 微信开发者工具
# 1.6. 使用 git 管理项目
.gitignore:
/.idea
/node_modules
/unpackage/dist
注意:
- 如果一个目录里没有文件,git 是不会追踪的
- 创建
/unpackage/.gitkeep
空白文件,让 git 可以追踪到 unpackage 目录
# 1.7. API 文档
# 2. tabBar
# 2.1. 新建页面
步骤:
- 选择
uni-shop/pages
目录 - 右键菜单:新建 uni-app 页面
- 勾选 “创建同名目录”、“在 pages.json 中注册”
注意:
- 删除页面时,需要同时删除
pages.json
中对应页面的配置
# 2.2. 配置 tabBar
将 icon 等图片资源放入
uni-shop/static
目录在
[uni-shop/pages.json].tabBar
中配置 tabBar 页面{ "tabBar": { "selectedColor": "#C00000", "list": [ { "pagePath": "pages/home/home", "text": "首页", "iconPath": "static/tab_icons/home.png", "selectedIconPath": "static/tab_icons/home-active.png" }, { "pagePath": "pages/cate/cate", "text": "分类", "iconPath": "static/tab_icons/cate.png", "selectedIconPath": "static/tab_icons/cate-active.png" }, { "pagePath": "pages/cart/cart", "text": "购物车", "iconPath": "static/tab_icons/cart.png", "selectedIconPath": "static/tab_icons/cart-active.png" }, { "pagePath": "pages/my/my", "text": "我的", "iconPath": "static/tab_icons/my.png", "selectedIconPath": "static/tab_icons/my-active.png" } ] } }
# 2.3. 修改导航条的样式
在 [uni-shop/pages.json].globalStyle
设置导航条样式:
{
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "优购商城",
"navigationBarBackgroundColor": "#C00000",
"backgroundColor": "#FFFFFF"
}
}
# 3. 首页
# 3.1. 配置网络请求
说明:
使用 uni-ajax 封装请求
文档:
安装:(使用 npm 安装)
npm init -y
npm i uni-ajax
## "uni-ajax": "^2.5.1"
使用:
/commons/http/http.js
:import uniAjax from 'uni-ajax' const config = { baseURL: 'https://api-hmugo-web.itheima.net' }; const http = uniAjax.create(config); // 添加请求拦截器 http.interceptors.request.use( (config) => { uni.showLoading({ title: '数据加载中...' }) return config; }, (error) => { return Promise.reject(error) } ); // 添加响应拦截器 instance.interceptors.response.use( (response) => { uni.hideLoading(); return response.data; }, (error) => { return Promise.reject(error) } ) export default http;
/main.js
:import http from './commons/http/http'; // 挂在到全局变量 uni uni.$http = http;
# 3.2. 轮播图
# 3.2.1. 请求数据
export default {
onLoad() {
this.getSwiperList();
},
data() {
return {
swiperList: [],
};
},
methods: {
async getSwiperList() {
// API 文档: https://www.showdoc.com.cn/128719739414963/2513235043485226
const { message: swiperList } = await uni.$http.get('/api/public/v1/home/swiperdata');
this.swiperList = swiperList;
},
},
}
# 3.2.2. 界面
在 HBuilderX 的编辑器中,输入 uswiper
即可触发代码片段选择框,选择即可
# 3.2.3. 配置分包
步骤:
创建目录
/subpkg
配置
[pages.json].subPackages
节点:{ "subPackages": [ { "root": "subpkg", "pages": [] } ], }
新建页面
goods_detail
,选择subpkg
分包,创建完毕后的pages.json
配置:{ "subPackages": [ { "root": "subpkg", "pages": [ { "path": "goods_detail/goods_detail", "style": { "navigationBarTitleText": "", "enablePullDownRefresh": false } } ] } ], }
# 3.2.4. 点击后跳转详情页
<navigator
class="swiper-item"
:url="`/subpkg/goods_detail/goods_detail?goods_id=${item.goods_id}`"
>
<image :src="item.image_src"></image>
</navigator>
# 3.3. 分类导航
跳转 tabBar 页面:
uni.switchTab({ url: 'navigator_url' });
# 3.4. 参考
- api/ui/prompt.html#showloading (opens new window)
- api/ui/prompt.html#hideloading (opens new window)
- api/router.html#switchtab (opens new window)
- @escook/request-miniprogram (opens new window)
- 页面生命周期 —— uni-app (opens new window)
# 4. 分类页面
# 4.1. 设备可用区域的高度
// 减去 导航条 和 tabBar 后的高度
const { windowHeight } = uni.getSystemInfoSync();
# 4.2. 包装元素
<template/>
和<block/>
- 不会在页面中做任何渲染
<block/>
在不同的平台表现存在一定差异,推荐统一使用<template/>
。
# 4.3. 重置滚动条
<scroll-view :scroll-top="scrollTop"></scroll-view>
<script>
export default {
data() {
return {
scrollTop: 0,
};
},
methods: {
resetScrollTop() {
// 更新前后的值 如果相同,则不会触发页面渲染
this.scrollTop = this.scrollTop ? 0 : 1;
},
}
}
</script>
# 4.4. 参考
- api/system/info.html#getsysteminfo (opens new window)
- tutorial/page.html#template-block (opens new window)
- tutorial/page.html#nvue-开发与-vue-开发的常见区别 (opens new window)
# 5. 搜索页面
# 5.1. 自定义组件
创建步骤:
- 创建
/components
目录 - 在
/components
目录上点击右键,选择创建组件 - 在页面直接使用组件即可,无须引入和注册
属性绑定和事件绑定:
与 vue 中一致:
// my-search.vue export default { props: { bgcolor: { type: String, default: '#c00000', }, radius: { type: Number, default: 15, }, }, methods: { handleClick() { this.$emit('click'); }, }, computed: { /* ... */ }, } // app.vue <my-search @click="handleClickSearch" bgcolor="red"></my-search>
# 5.2. uni-ui 的组件
使用步骤:
- 进入 官网 (opens new window)
- 查看对应组件,点击进入对应的插件页面,安装即可
注意:
- 安装一次,即可使用全部组件
# 5.3. 吸顶效果
<view class="search-box">
<my-search @click="handleClickSearch"></my-search>
</view>
<style>
.search-box {
z-index: 999;
position: sticky;
top: 0;
}
</style>
# 5.4. 防抖
export default {
data() {
return {
keyword: '',
}
},
methods: {
handleInput(value) {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
// console.log(value);
this.keyword = value;
}, 500)
}
}
}
# 5.5. 本地存储
this.person = { name: '张三' };
uni.setStorageSync('person', JSON.stringify( this.person ))
const str = uni.getStorageSync('person');
this.person = JSON.parse(str);
# 5.6. 参考
- component/uniui/uni-icons.html (opens new window)
- component/uniui/uni-search-bar.html (opens new window)
- api/storage/storage.html#setstoragesync (opens new window)
# 6. 商品列表页面
# 6.1. 上拉触底加载更多
[pages.json].pages.style
中配置触底的距离:{ // ... "pages" : { "style": { "onReachBottomDistance": 100, } } }
在 vue 中编写
options.onReachBottom()
方法
# 6.2. 下拉刷新
[pages.json].pages.style
中开启下拉刷新,并配置样式:{ // ... "pages" : { "style": { "enablePullDownRefresh": true, "backgroundColor": "#efefef", "backgroundTextStyle": "dark" } } }
在 vue 中编写
options.onPullDownRefresh()
方法当处理完数据刷新后,
uni.stopPullDownRefresh
可以停止当前页面的下拉刷新。
# 6.3. 参考
- 配置项列表 (opens new window)
- 配置项列表 - pages (opens new window)
- 配置项列表 - pages - style (opens new window)
- 页面生命周期 (opens new window)
# 7. 商品详情
# 7.1. 图片预览
uni.previewImage({
current: 0, // 从第几个元素开始预览
urls: [ 'xxx/1.png', 'xxx/2.png', ... ]
});
# 7.2. rich-text 中的 img
说明:
- 使用
<rich-text>
组件,nodes
内容的<img>
底部有空行
方案:
// 通过正则替换,设置行内样式
`<img ` -> `<img style="display: block" `
goods_introduce.replace(/<img /gi, '<img style="display: block" ')
# 7.3. iOS 设备中的 .webp 图片
在 iOS 中,.webp
图片支持得不是很好,建议使用 .png
、 .jpg
等图片
# 7.4. 页面闪烁问题
问题:
- 获取数据之前,页面都是空数据
- 获取数据之后,页面里面空数据替换为有效数据
- 这个替换过程,会导致页面闪烁
方案:
<!-- 数据加载完成前,不显示界面 -->
<template>
<view v-if="isLoaded" class="detail-page">
....
</view>
</template>
<script>
export default {
data() {
return {
isLoaded: false,
detail: {},
};
},
onLoad() {
this.getDetail();
},
methods: {
async getDetail() {
const res = await request(/..../);
this.detail = res.data;
this.isLoaded = true;
}
},
}
</script>
# 7.5. uni-goods-nav
说明:
- 商品导航区域
示例:
<template>
<view class="goods-detail">
<!-- 商品导航区域-->
<uni-goods-nav
class="goods-nav"
:fill="true"
:options="goodsNavConfig.options"
:buttonGroup="goodsNavConfig.buttonGroup"
@click="handleClickIcons"
@buttonClick="handleClickButtons"
/>
</view>
</template>
<script>
export default {
data() {
return {
goodsNavConfig: {
options: [
{
icon: 'shop',
text: '店铺',
},
{
icon: 'cart',
text: '购物车',
info: 2
},
],
buttonGroup: [
{
text: '加入购物车',
backgroundColor: '#ff0000',
color: '#fff'
},
{
text: '立即购买',
backgroundColor: '#ffa200',
color: '#fff'
}
]
}
}
},
methods: {
handleClickIcons(e) {
// e: {"index":1,"content":{"icon":"cart","text":"购物车","info":2}}
if (e.index === 1) {
uni.switchTab({
url: '/pages/cart/cart'
})
}
},
handleClickButtons(e) {
// e: {"index":0,"content":{"text":"加入购物车","backgroundColor":"#ff0000","color":"#fff"}}
},
}
}
</script>
<style lang="scss">
/* 页面底部留出 goods-nav 的高度 */
.goods-detail {
padding-bottom: 50px;
}
.goods-nav {
z-index: 999;
position: fixed;
left: 0;
bottom: 0;
width: 100%;
}
</style>
# 7.6. 参考
- uni.previewImage(OBJECT) (opens new window)
- rich-text 组件 (opens new window)
- uni-goods-nav 组件 (opens new window)
# 8. 购物车
# 8.1. vuex
说明:
- 用法与普通 web 开发一致
使用:
/main.js
:// /main.js import Vue from 'vue' import App from './App' import store from './store/store.js' const app = new Vue({ ...App, store, }) app.$mount()
/store/store.js
:// /store/store.js import Vue from 'vue' import Vuex from 'vuex' import cart from './cart'; Vue.use(Vuex) const store = new Vuex.Store({ modules: { cart, }, }) export default store
/store/cart.js
:// /store/cart.js export default { namespaced: true, state: { cart: JSON.parse(uni.getStorageSync('cart') || '[]'), }, getters: { total(state) { // ... return count; } }, mutations: { addToCart(state, goods) { // ... }, saveToStorage(state) { uni.setStorageSync('cart', JSON.stringify(this.state)); }, } }
/pages/cart/cart.vue
:// /pages/cart/cart.vue` import { mapState, mapGetters, mapMutations } from 'vuex'; export default { computed: { ...mapState('cart', ['cart']), ...mapGetters('cart', ['total']), }, methods: { ...mapMutations('cart', ['addToCart']), }, watch: { total(newVal) { // ... } }, }
# 8.2. 设置 tabBar 徽标及 mixins
说明:
- 通过 mixins 给所有的 tabBar 页面设置 tabBar 徽标
步骤:
/mixins/tabbar-badge.js
:import { mapGetters } from 'vuex'; const CART_INDEX = 2; export default { onShow() { this.setBadge(); }, computed: { ...mapGetters('cart', ['total']), }, watch: { total(newTotal) { if (!newTotal) { this.removeBadge(); return; } this.setBadge(); }, }, methods: { setBadge() { const text = String(this.total); uni.setTabBarBadge({ index: CART_INDEX, text }); }, removeBadge() { uni.removeTabBarBadge({ index: CART_INDEX }); }, } }
home.vue
,cate.vue
, ...:import tabbarBadge from '@/mixins/tabbar-badge'; export default { mixins: [tabbarBadge], }
参考:
# 8.3. 滑动删除
示例:
<template>
<uni-swipe-action ref="swipe">
<template v-for="(goods, i) in cart">
<uni-swipe-action-item :right-options="swiperActionOptions" @click="handleClickSwipeButton(goods)">
<my-goods />
</uni-swipe-action-item>
</template>
</uni-swipe-action>
</template>
<script>
export default {
onHide() {
this.$refs.swipe.closeAll();
},
data() {
return {
swiperActionOptions: [
{
text: '删除',
style: {
backgroundColor: '#C00000'
}
}
],
};
},
}
</script>
参考:
# 8.4. 选择地址
示例:
manifest.json
:{ "mp-weixin" : { "requiredPrivateInfos": [ "chooseAddress" ] }, }
address.vue
:export default { methods: { async handleClickChooseButton() { const [ error, result ] = await uni.chooseAddress().catch((e) => e); if (error) { console.error(error); return; } const { errMsg, // "chooseAddress:ok" provinceName, // "广东省" cityName, // "广州市" countyName, // "海珠区" detailInfo, // "新港中路397号" nationalCode, // "510000" postalCode, // "510000" telNumber, // "020-81167888" userName, // "张三" } = result; if (errMsg !== "chooseAddress:ok") { return; } this.address = { provinceName, cityName, countyName, detailInfo, postalCode, telNumber, userName, }; }, }, }
参考:
# 8.5. 发起重新授权
示例:
function chooseAddress() {
const [err, succ] = await uni.chooseAddress().catch(err => err)
// 未授权,则重新发起
if (err) {
const {errMsg} = err;
if (errMsg === 'chooseAddress:fail auth deny' // 模拟器/安卓
|| errMsg === 'chooseAddress:fail authorize no response' // 苹果
) {
this.reAuth()
}
}
}
async function reAuth() {
const [error, confirmResult] = await uni.showModal({
content: '检测到您没打开地址权限,是否去设置打开?',
confirmText: "确认",
cancelText: "取消",
})
if (error) {
return
}
if (confirmResult.cancel) {
return uni.$showMsg('您取消了地址授权!')
}
uni.openSetting({
success: (settingResult) => {
const isAuthAddress = settingResult.authSetting['scope.address'];
if (!isAuthAddress) {
uni.$showMsg('您取消了地址授权!');
return;
}
uni.$showMsg('授权成功!请选择地址');
},
});
}
注意:
scope.address
通讯地址(已取消授权,可以直接调用对应接口)
参考:
- 授权 - scope 列表 (opens new window)
- uni.showModal(OBJECT) (opens new window)
- uni.openSetting(OBJECT) (opens new window)
# 8.6. 参考
# 9. 登录
# 9.1. uni.getUserInfo()
说明:
- 不建议使用
# 9.2. uni.getUserProfile()
说明:
获取:
userInfo
rawData
signature
encryptedData
iv
基础库版本要设置为
2.25.4
- 高于
2.27.0
,接口被微信收回,获取 userInfo 是匿名用户 - 高于
2.26.0
控制台会报错
- 高于
manifest.json
:{ "mp-weixin" : { "libVersion": "2.25.4", }, }
示例:
<template>
<button type="primary" @click="handleLoginBtn"> 一键登录 </button>
</template>
<script>
export default {
methods: {
async handleLoginBtn() {
const [error, res] = await uni.getUserProfile({ desc: '用于完善会员资料' }).catch((e) => e);
if (error) {
if (error.errMsg === 'getUserProfile:fail auth deny') {
uni.$showToast('您取消了登录授权!');
return;
}
}
const { userInfo } = res;
this.updateUserInfo(userInfo);
},
},
}
</script>
参考:
# 9.3. uni.login()
说明:
- 获取 code
示例:
const [error, result] = await uni.login().catch((e) => e);
if (error || result.errMsg !== 'login:ok') {
uni.$showToast('登录失败!');
return;
}
console.log(result.code);
参考:
# 9.4. 占满整个屏幕的高度
page,
.my-container {
height: 100%;
}
# 9.5. 支付
流程:
创建订单
- 将要结算的商品 id、总价等发送给服务器,返回订单编号
预支付(获取支付参数)
- 将 订单编号 发送给服务器,返回客户端支付接口所需要的参数
支付
- 调用客户端支付 API 进行支付
查看订单支付状态
参考:
# 10. 发布
# 10.1. 安卓
设置:(manifest.json
)
基础配置
- uni-app 应用标识
- 应用名称
App 图标配置
上一篇: 下一篇: