# 微信小程序-商城实战

# 1. 起步

# 1.1. uni-app

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

开发者编写一套代码,可发布到:

  • iOS
  • Android
  • Web(响应式)
  • 各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)
  • 快应用

# 1.2. HBuilderX

说明:

安装 scss 编译插件:

# 1.3. 新建 uni-app 项目

步骤:

  1. 文件(菜单) -> 新建 -> 1.项目
  2. uni-ui 项目模板
  3. 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 文档

https://www.showdoc.com.cn/128719739414963/2513235043485226

# 2. tabBar

# 2.1. 新建页面

步骤:

  1. 选择 uni-shop/pages 目录
  2. 右键菜单:新建 uni-app 页面
  3. 勾选 “创建同名目录”、“在 pages.json 中注册”

注意:

  • 删除页面时,需要同时删除 pages.json 中对应页面的配置

# 2.2. 配置 tabBar

  1. 将 icon 等图片资源放入 uni-shop/static 目录

  2. [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. 配置网络请求

说明:

安装:(使用 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. 配置分包

步骤:

  1. 创建目录 /subpkg

  2. 配置 [pages.json].subPackages 节点:

    {
      "subPackages": [
        {
          "root": "subpkg",
          "pages": []
        }
      ],
    }
    
  3. 新建页面 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. 参考

# 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. 参考

# 5. 搜索页面

# 5.1. 自定义组件

创建步骤:

  1. 创建 /components 目录
  2. /components 目录上点击右键,选择创建组件
  3. 在页面直接使用组件即可,无须引入和注册

属性绑定和事件绑定:

  • 与 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 的组件

使用步骤:

  1. 进入 官网 (opens new window)
  2. 查看对应组件,点击进入对应的插件页面,安装即可

注意:

  • 安装一次,即可使用全部组件

# 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. 参考

# 6. 商品列表页面

# 6.1. 上拉触底加载更多

  1. [pages.json].pages.style 中配置触底的距离:

    {
      // ...
      "pages" : {
        "style": {
          "onReachBottomDistance": 100,
        }
      }
    }
    
  2. 在 vue 中编写 options.onReachBottom() 方法

# 6.2. 下拉刷新

  1. [pages.json].pages.style 中开启下拉刷新,并配置样式:

    {
      // ...
      "pages" : {
        "style": {
          "enablePullDownRefresh": true,
          "backgroundColor": "#efefef",
          "backgroundTextStyle": "dark"
        }
      }
    }
    
  2. 在 vue 中编写 options.onPullDownRefresh() 方法

  3. 当处理完数据刷新后,uni.stopPullDownRefresh 可以停止当前页面的下拉刷新。

# 6.3. 参考

# 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. 页面闪烁问题

问题:

  1. 获取数据之前,页面都是空数据
  2. 获取数据之后,页面里面空数据替换为有效数据
  3. 这个替换过程,会导致页面闪烁

方案:

<!-- 数据加载完成前,不显示界面 -->
<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. 参考

# 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 徽标

步骤:

  1. /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 });
        },
      }
    }
    
  2. 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. 选择地址

示例:

  1. manifest.json:

    {
      "mp-weixin" : {
        
        "requiredPrivateInfos": [
          "chooseAddress"
        ]
        
      },
    }
    
  2. 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 通讯地址(已取消授权,可以直接调用对应接口)

参考:

# 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. 支付

流程:

  1. 创建订单

    • 将要结算的商品 id、总价等发送给服务器,返回订单编号
  2. 预支付(获取支付参数)

    • 将 订单编号 发送给服务器,返回客户端支付接口所需要的参数
  3. 支付

    • 调用客户端支付 API 进行支付
  4. 查看订单支付状态

参考:

# 10. 发布

# 10.1. 安卓

设置:(manifest.json)

  • 基础配置

    • uni-app 应用标识
    • 应用名称
  • App 图标配置

本章目录