原生微信小程序和 uni-app 基础笔记

804 阅读12分钟

关闭小程序开发工具-调试器里的警告信息

  • 根目录-project.config.json 里面的 settings-checkSiteMap 改为 false

使当前小程序不被微信索引

  • 根目录-sitemap.json 里面的 "action": "disallow",

新增 pages 页面

  • 在 app.json - pages 中新增页面的存放路径,自动创建

更改首页

  • 小程序会把排在最上面的页面当作首页进行渲染

标签

div -- view span -- text img -- image a -- navigator <navigator url="/x/x"></navogator>

样式

  • rpx 单位:所以大小的屏幕都是 750rpx;ipone6 上面:1rpx = 0.5px

组件

  • view

  • scroll-view:

    • <scroll-view scoll-y> <view><view> </scroll-view> 默认为 x 轴滚动
    • 必须加高度或者宽度
    • 滚动的视图区域
    • 常用来实现滚动列表
  • swiper、swiper-item

    • 轮播图的容器和轮播项
    • 属性:
      1. indicator-dots 类型:boolean 默认值: false 作用:是否显示小圆点
      2. indicator-color color rgba(0,0,0,.3) 小圆点的颜色
      3. indicator-active-color #000 当前选中的小圆点的颜色
      4. autoplay(可以没有值) boolean false 是否自动切换
      5. interval number 5000ms 切换的时间间隔
      6. circular(可以没有值) boolean false 是否采用衔接滑动
      • duration 一张图从开始到结束耗时
  • text selectable 属性(可以没有值) 支持用户长按选中其中的文字

  • rich-text 富文本组件,支持把 HTML 字符串渲染为 WXML 结构

    • nodes 属性, nodes="<h1 style='color:red'>介绍</h1>"
    • 常应用于商品的详情介绍页(从服务器返回的是一串包含 html 的字符串时)
  • button

    • type 属性:primary 主色调 warn 警告
    • size 属性 default mini
    • plain 属性(可以没有值) true 为镂空的按钮
  • image 默认宽度为 300px,高度 240px

    • mode属性
      1. scaleToFill(默认),缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸填满
      2. aspectFit,缩放模式,保持纵横比缩放
      3. aspectFill,缩放模式,但是超出 img 空间的会被截取
      4. widthFix
      5. heightFix

数据绑定

  • 定义: 入口文件(index.js) -- page 函数 -- data 中直接定义
  • 渲染: 插值表达式。如果绑定属性,不同于 Vue,没有 v-bind,还是用插值语法

事件绑定

  • 常用事件
    1. tap 绑定方式: bindtap 或者 bind:tap 描述:手指触摸后马上离开,类似 click
    2. input bindinput、bind:input 输入事件
    3. change bindchange、bind:change 状态改变
    4. sync bindSync、bind:sync 自定义事件(sync 是自定义的名字)
  • 事件对象的属性
    1. type 事件类型
    2. timeStamp 页面打开到触发事件所用毫秒
    3. target 触发事件的组件的一些属性值集合
    4. currentTarget 当前组件的属性值集合
    5. detail 额外信息
    6. touches 当前停留在屏幕中的触摸点信息的数组
    7. changedTouches 当前变化的触摸点信息的数组
  • 事件传参
    1. <button bindtap="changeCount" data-number="{{count}}">传参</button> 使用data-参数,插值内为参数的值
    2. 获取传入的值: e.target.dataset.number
    3. input 获取输入的值 bandinput 值: e.detail.value

条件渲染

  • 使用 wx:if={{true / false}} 来判断是否需要渲染该代码块,可以和wx:elifwx:else配合使用
  • 当有多个组件需要判断是否渲染,使用 block 标签包裹,block 不会被渲染。当然可以使用 view 包裹,但是外面会多一个view,block 的作用仅此而已
  • 使用 hidden 也可以条件判断渲染,条件为 true 隐藏,值只能为 true 和 false
  • 两者区别:
    1. wx:if用动态创建和移除元素的方式控制,hidden只是将 display 的 none / block 进行切换
    2. 频繁切换并控制条件简单时,使用 hidden,反之用 wx:if

列表渲染(循环)

  • 使用 wx:for。 绑定的 key 值直接用字符串,当列表内为对象并使用对象的 id 作为 key 时,直接写 wx:key="id",他会直接到 item 里去找 id(莫名其妙的设定,不应该自己写 item.id 么?)
<view wx:for="{{array}}" wx:key="index">
    {{index}},{{item}}
</view>
  • 可以自定义 index 和 item 的别名
<view wx:for="{{array}}" wx:for-index="myIndex" wx:for-item="myItem" wx:key="myIndex">
    {{index}},{{item}}
</view>

全局配置文件

"window": {
    // main 下拉页面 背景颜色 仅支持 dark、light
    "backgroundTextStyle": "light",
    // 顶部导航栏背景颜色
    "navigationBarBackgroundColor": "#fff",
    // 标题栏内容文字
    "navigationBarTitleText": "Weixin",
    // 导航栏标题颜色,仅支持 black、white
    "navigationBarTextStyle": "black",
    // 窗口的背景色
    "backgroundColor":"#efefef",
    // 是否全局开启下拉更新,当配置在页面中 json 时,只影响当前页面
    "enablePullDownRefresh":"fasle",
    // 页面上拉触底事件触发时距离底部距离,单位为 px
    "onReachBottomDistance":50
  },

tabBar 全局配置 (路由导航)

  • 顶部 tabBar:当渲染顶部 tabBar 时,不显示 icon,不显示文本
  • tabBar 中只能配置最少 2 个,最多 5 个 tab 页签
  • tabBar 有 6 个组成部分
    1. backgroundColor 背景颜色
    2. selectedIconPath 底部 tabBar 图标
    3. selectedColor 选中的页签的文字颜色
    4. borderStyle 底部 tabBar 上边缘样式,或者 顶部 tabBar 下边缘样式
    5. iconPath 未选择的页签 icon
    6. color 未选择的页签文字h颜色
  • tabBar 配置项
"tabBar": {
    "position": "bottom", //tabBar 位置
    "borderStyle": "black", // 仅支持 white / black
    "color": "",
    "selectedColor": "",
    "backgroundColor": "",
    // 页签列表
    "list": [
        {
            "pagePath":"",// 页面路径,页面必须事先在 pages 中定义
            "text":"",  // tab 上显示的文字
            "iconPath": "",
            "selectedIconPath":""
        },
    ]
}
  • 注意点: tabBar 的页面在 pages 中配置时,必须全放在最上面

网络数据请求

  • 小程序中网络请求的限制
    1. 只能请求 HTTPS 类型的接口
    2. 必须将接口的域名添加到信任列表中登录后台 - 开发管理 - 开发设置 - 服务器域名开始配置 - request 合法域名
    3. 域名不能使用 IP 地址
    4. 域名必须经过 ICP 备案
    5. 服务器域名一个月内最多可申请 5 次修改
  • get 请求
wx.request({
    url: 'https://www.escook.cn/api/get',
    method: 'GET',
    data: { // 发送到服务器的数据
        name: 'zs',
        age: 18
    },
    success: request => console.log(request.data)
})
  • 在页面刚加载时请求数据:将请求数据的函数在 onLoad: function(){} 中调用
  • 临时跳过 request 合法域名校验:开发者工具 - 详情 - 本地设置 - 不检验合法域名... - 打勾,此项仅在开发阶段使用

前端路由

  • 声明式导航:<navigator>导航组件
    • 通过 <navigator> 跳转到 tabBar 页面
      1. <navigator url="/pages/news/news" open-type="switchTab">跳转</navgator>
      2. url 必须以根目录 '/' 开头,open-type="switchTab" 必带
    • 通过 <navigator> 跳转到非 tabBar 页面
      1. <navigator url="/pages/news/news" open-type="navigate">跳转</navgator>
      2. url 必须以根目录 '/' 开头,open-type="navigate" 可以省略不写
    • 后退导航:<navigator delta="1" open-type="navigateBack">后退</navgator>
      1. open-type="navigateBack" 必带,delta 的值为后退的层级,默认为 1,如果为 1,可以省略
    • 传参:<navigator url="/pages/news/news?name=zs&age=20" open-type="navigate">跳转</navgator>
  • 编程式导航:
    • 调用wx.switchTab({obj})方法,跳转到 tabBar 页面
      1. url 必选 需要跳转的路径,必须以 / 根路径开始
      2. success 非必选 成功的回调
      3. fail 非必选 失败的回调
      4. complete 非必选 跳转结束的回调,成功失败都会执行
    • 调用wx.navigateTo({obj}),跳转到 非 tabBar 页面
      1. 属性同上
    • 调用wx.navigateBack({obj}),实现后退路由
      1. delta 非必选 回退的层级,默认为一
      2. 同上
  • 在 onLoad 函数中可以直接接收路由参数(options),我们可以手动赋值到 data 上面
Page({
    data: {
        query: {}
    },
    onLoad function(options) {
        this.setData({
            query:options
        })
    }
})

生命周期

  • 应用生命周期(定义在 app.js 里面)
    • 前台:应用处于引动设备的屏幕里;后台:应用未处于当前引动设备的屏幕里,但是后台未关闭
    1. onLaunch: 当小程序初始化完成时调用,全局只会调用一次
    2. onShow: 当小程序启动,或从后台今日前台时调用
    3. onHide: 当小程序从前台今日后台时触发
  • 页面生命周期
    1. onLoad 第一次进入页面(准备数据),只会调用一次
    2. onShow 页面显示 / 切入前台时触发
    3. onReady 初次渲染完成时,只会调用一次,如 wx.setNavigationBarTitle(修改页面标题)可在此阶段执行
    4. onHide 页面隐藏 / 切入后台时,如任意路由切换到其他页面,小程序切入后台
    5. onUnload 页面卸载时,如 wx.redirectTo 或 wx.navigateBack 到其他页面
  • 组件的生命周期
    1. created 组件实例第一次创建
    2. attached 组件实例进入页面节点树时 相当于 mounted
    3. ready 组件在视图层完成布局
    4. moved 组件实例移动到节点树另一个位置
    5. detached 组件实例从节点树被移除 相当于 destroyed
    6. error 组件方法抛出错误时
  • 组件的生命周期使用推荐写在 lifetimes 配置项中,优先级最高
    Component({
        lifetimes:{
            attached(){},
            detached(){}
        }
    })
  • 在组件中可以访问到的页面生命周期
    1. show 组件所在页面被展示时
    2. hide 组件所在页面被隐藏时
    3. resize 组件所在页面尺寸大小变化时
  • 在组件中使用所在页面的生命周期函数,必须定义在 pageLifetimes 节点中
Component({
    pageLifetimes:{
        show:function(){},
        hide:function(){},
        resize:function(size){}
    }
})

wxs 模块

  • 类似 JS 的 ES5 以下写法
  • 具体作用大概是说小程序的框架分为应用层和逻辑层,比如需要完成这样一个需求:在设备屏幕上有两个点 A 和 B,用户按住其中一个并移动,另一个也会跟着移动。代码逻辑大概就是给 A 点绑定 touchmove 事件,并将用户手指移动的坐标赋值给 A,然后再讲 A 的坐标赋值给 B。这样数据会多次经过应用层和逻辑层,这样频繁的交互势必会造成卡顿。这样分层的目的是管控,开发者的代码只能运行在逻辑层。而 wxs 的代码可以写在视图层,这样就减少了通信的次数,让事件在视图层相应。
  • 基础用法:
// wxml 文件中 
// 内嵌定义
<wxs moudle="x">
moudle.export y = function (options){...}
</wxs>
// 使用
<view>{{x.y(数据)}}</view>
// 使用外联的 wxs 模块
<wxs src="相对路径" module="x"></wxs>
<view>{{x.y(数据)}}</view>

全局 css 对组件的影响

  • 全局使用类选择器定义的样式不影响组件,而标签、属性和 id 选择器影响
  • 当需要在外界控制组件内的样式时:
// 在组件的 .js 文件中新增配置
Component({
    options:{
        styleIoslation:'isolated' //默认值,不配置默认为 true,表示互不影响

        /* 其他可选值: apply-shared   默认为 false,表示页面会影响组件,但组件不会影响页面
        shared  默认为false,表示互相都会影响 */
    }
})
// 或在组件的 .json 文件中新增配置
{
    "styleIsonlation":"isolated"
}

小程序的弹窗

  • wx.showToast({title:'弹窗显示的文本',icon:'弹窗显示的图标'}) 相当于 window.alert

组件的外部属性

  • 接收:properties:{key: value}
Component({
    properties:{
        n:{
            type: Number,
            value: 0   // 默认值
        },
        n: Number //不需要默认值时的简写方式
    }
})
  • 小程序中的外部属性可读可写

插槽

  • 使用多个插槽时需要配置,定义和使用参照 vue 具名插槽
    Component({
        options: {
            multipleSlots:true
        }
    })

小程序中的 watch

  • observers:{}
    Component({
        observers:{
            '监听属性1,监听属性2':function(newValue1,newValue2){
                ...
            }
        }
    })
  • 监听的任何一个值发生变化都会调用 watch
  • 监听对象,可以单独写,如果需要监听所有但是值太多,直接用通配符,得到一个包含新值的对象
    Component({
        observers:{
            'obj.**':function(obj){
                ...
            }
        }
    })

粗暴且通用的纯数据字段设置(适合没时间研究正则的同学)

  • 纯数据字段是指只用于逻辑代码但不会被页面渲染的字段,可以提升性能
Component({
    options:{
        pureDataPattern:/^_/
    },
    data:{
        a:true, //普通数据
        _b:true // 只要前面带上下划线就会变成纯数据字段
    }
})

小程序中的 $emit

  • this.triggerEvent('自定义事件名',{key: value}) 自定义事件需要父组件传递
  • 父组件通过 e.detail.key 获取数据
  • 父组件也可以通过获取子组件的实例来操作子组件的数据
    // wxml
    <my-component class="component" bindSync="syncCount"></my-component>
    // 父组件 js
    getComponent(){
        const child = this.selectComponent('.component')
    }

小程序中的 mixins

  • behaviors
  • 定义
    module.exports = Behavior({
        // 属性节点
        properties:{},
        // 私有数据节点
        data:{user: 'zs'},
        // 事件处理函数和自定义方法
        methods:{},
        // 引入其他的属性节点
        behaviors:[],
        // 可以直接使用组件的生命周期函数
        // 其他节点
        ...
    })
  • 导入 const myBehavior = require('相对路径')
  • 使用
    Component({
        behaviors:[myBrhavior]
    })

小程序中使用 CSS 自定义变量 类似于 scss

  • 全局定义: page { --color: red; }
  • 使用: div { background-color: var(--color) }

使小程序的 API Promise化

  • yarn add miniprogram-api-promise@1.0.4 -s
  • 小程序中安装第三方包后,必须执行构建 npm(推荐先删除 miniprogram_npm 目录再构建)
// app.js
import { promisifyAll } from 'miniprogram-api-promise'

const wxp = wx.p = {}
promisifyAll(wx, wxp)

小程序全局数据共享 mobx

  • mobx-miniprogram用来创建 Store 实例对象
  • mobx-miniprogram-bindings用来把 Store 中的共享数据或方法,绑定到组件或页面中
  • yarn add mobx-miniprogram@4.13.2 mobx-miniprogram-bindings@1.2.1
  • 初始化 store /store/store.js
import { observable } from 'mobx-miniprogram'
export const store = observable({
    // 数据
    n1: 1,
    n2: 2,
    // 方法
    update1: action(fn1(step) { return this.n1 += step }),
    update2: action(fn2() {}),
    // computed
    get sum() {
        return this.n1 + this.n2
    }
})
  • 页面中数据绑定 /pages/index/index.js
import { creatStoreBindings } from 'mobx-miniprogram-bindings'
import { store } from '../../store/store'

Page({
    onLoad:function() {
        this.storeBindings = createStoreBinds(this,{
            store, // 数据源
            fields: ['n1','n2'], // 数据
            actions:['update1']  // 方法
        })
    },
    onUnload: function () { // 页面卸载同时卸载数据
        this.stroeBindings.destroyStoreBindings()
    }
})
  • 组件中数据绑定 /components/myComponent/myComponent.js
import { storeBindingBehavior } from 'mobx-miniprogram-bindings'
import { store } from '../store/store'

Component({
    behaviors:[storeBindingBehavior], // 通过 storeBindingBehavior 自动绑定
    storeBindings:{
        store, // 数据源
        fields: {// 数据
            n1: 'n1',
            n2: 'n2'
        }
        actions: { // 方法
            update1: 'update1'
        }
    },
})

分包

// app.json
    "pages":[ // 主包所有页面
        "pages/index",
        "pages/logs"
    ],
    "subpackages":[ // 通过 subpackages 节点,声明分包的结构
        {
            "root":"packageA", // 第一个分包的根目录
            "pages":[   // 当前分包下,所有页面的相对路径
                "pages/cat",
                "pages/dog"
            ]
        },{
            "root":"packageB", // 第二个分包的根目录
            "name":"pack2",    // 分包的别名
            "pages":[          // 当前分包下,所有页面的相对路径
                "pages/apple",
                "pages/banana"
            ]
        }
    ]
  • 分包预下载(同一个分包中的页面享有共同的预下载大小限额 2M)
// app.json
...
"preloadRule":{
    "pages/personal/personnal":{ // 进入哪个页面时要进行预下载
        "packages":[
            "packageB"  // 要预下载哪个页面
        ],
        "network":"all"  // 使用什么网络    可选值: all  /   wifi
    }
}

自定义 tabBar(我试了,会造成页面严重闪烁)

  • 项目根目录/custom-tab-bar - 创建 component,并命名为 index
  • app.json - "tabBar" 配置项 - 添加 "custom":true,

HbiuilderX 搭配 uni-app

  • HbiuilderX 官网下载地址 https://dcloud.io/hbuilderx.html
  • 安装 sass/scss 编译插件。地址: https://ext.dcloud.net.cn/plugin?id=2046
  • 打开 HbiuilderX 创建uni-ui项目,创建完成后打开 根目录/uni_moudules/manifest.json 文件中找到微信小程序配置,填入 appID
  • 点击 HbiuilderX 的 工具 - 设置 - 运行设置,配置微信开发者工具路径
  • 打开 微信开发者工具,点击 设置 - 安全设置,将端口号打开
  • HbiuilderX 中点击运行 - 运行到小程序模拟器 - 微信开发者工具,会自动用开发者工具打开当前项目进行调试

git 管理

  • 根目录下创建 .gitignore 文件,忽略 /node_modules/unpackage/dist
  • unpackage 文件夹下创建 .getkeep 文件用来占位,防止 git 忽略unpackage文件夹

项目笔记

创建 tabBar 分支,制作自定义 tabBar

  • git checkout -b tabbar
  • 查看当前所有分支git branch
  • 创建 首页(home)、分类(cate)、购物车(cart)、我的(my)四个 tabBar 页面
  • pages.json 中 配置 tabBar
  • 第一次提交 tabbar 分支: git push -u origin tabbar
  • 将 tabbar 合并到本地的 master 分支: git checkout master git merge tabbar
  • 删除本地的 tabbar 分支: git branch -d tabbar
  • 再次推送主分支: git push

配置网络请求

  • 小程序不支持 axios,原生的 wx.request()功能单一,不支持拦截器等功能
  • uni-app 中使用 @escook/request-miniprogram发起网络数据请求 npmjs.com/package/@escook/request-miniprogram
  • 根目录初始化:yarn init -y
  • 安装
  • 导入: import {$http} from '@escook/request-miniprogram'
  • 挂载到 wx:wx.$http = $http
  • 使用 uni-app 时,挂载到uni.$http = $http
  • 拦截器
$http.beforeRequest = function(options) {
	uni.showLoading({
		title:'拼命加载中...'
	})
}
$http.afterRequest = function() {
	uni.hideLoading()
}
  • 配置请求的根路径:$http.baseUrl = '...',不嫌麻烦可以不配置,就是每次请求都要把路径写全
  • 请求发出后的判断代码
if(result.meta.status !== 200) {
    // uni.showToast 请求失败的弹窗
	return uni.showToast({
		title:"数据请求失败!", // 弹窗标题
		duration:1500,  // 弹窗存在事件
		icon:'none' // 弹窗的图标
	})
}

HbiuilderX 快速生成轮播图快捷键

  • usw

uni 弹窗

  • 模态框提示: uni.showToast({title: ' ',mask(遮罩层):true, duration: 1500, icon: 'none'}) 无返回值
  • confirm 弹窗: uni.showModal(titile: '提示',content: '确定要推出吗') 返回成功和失败的回调

配置分包

  • 本次项目中,将 tabBar 相关的四个页面放在主包,其他所有详情,列表页放在分包
  • 在项目根目录中,创建分包根目录,命名为 subpkg
  • app.json 中,与 page 节点平级:
"subPackages":[
		{
			"root":"subpkg",
			"pages":[]
		}
	]
  • 在 subpkg 目录中创建 goods_detail 分包

改造轮播图点击跳转到指定商品详情页

  • 将 swiper-item 下的 view 标签改为 navigator 标签
  • 动态绑定 url /subskg/goods_detail/goods_detail
  • id传参: '/subskg/goods_detail/goods_detail?goods_id='+item.goods_id

滑动页面

  • 调用 uni.getSystemInfoSync() 可以获取当前设备信息
    1. brand 手机品牌
    2. model 手机型号
    3. screenWidth 屏幕宽度
    4. screenHeight 屏幕高度 ....
  • 我没用这个,用的 vh 和 vw 单位,暂时没出问题

此项目手机调试报错:message: "request:fail net::ERR_CERT_DATE_INVALID"

  • message: “request:fail 对应的服务器证书无效。” 需要替换有效证书
  • 接口问题,网上找的接口,这个没有办法解决

关于 scroll-view 标签的小技巧

  • scroll 标签用于实现滑动效果,他的属性:scroll-top="0",可以实现每次在切换视图的时候将初始滑动的位置调整为顶部 0 像素
  • 当需求有多级标签切换,动态绑定 scroll-top 的值,每次切换都手动将其调整为 0
  • 这里有一个坑,就是每次给 scroll-top 赋的值如果是一样的话,则不生效
  • 所以这里只能使其值在 0.1 和 0 之间切换:this.scrollTop === 0 ? 0.1 : 0

搜索功能

  • component 目录右键点击创建组件 my-search
  • 最新版本的 uni-app 在 uni-modules 里有基本库,包含了 uni-search-bar 组件,可以直接用
  • 用完发现并没有自带联想功能
  • 修改 uni 内置组件的样式:
/deep/ .uni-searchbar {
	border: 1px solid red;
}
  • 自定义 search
<template>
	<view>
		<view class="my-search-container" :style="{'background-color':backgroundColor}">
			<view class="my-search-box" :style="{'border-radius': radius}">
				<uni-icons type="search" :size="18"></uni-icons>
				<text class="placeholder">搜索</text>
			</view>
		</view>
	</view>
</template>

<script>
	export default {
		name:"my-search",
		props: {
			backgroundColor: {
				type: String,
				default: '#c00000'
			},
			radius: {
				type: String,
				default: '18px'
			}
		},
		data() {
			return {
				
			};
		}
	}
</script>

<style lang="scss">
.my-search-container {
	height: 50px;
	display: flex;
	align-items: center;
	padding: 0 10px;
	.my-search-box {
		height: 36px;
		background-color: #fff;
		width: 100vw;
		display: flex;
		justify-content: center;
		align-items: center;
		.placeholder {
			font-size: 15px;
			margin-left: 5px;
		}
	}
}
</style>
  • 设置页面的吸顶效果,
.search-box {
    position: stucky;
    top: 0;
    // 提高层级,防止被轮播图覆盖
    z-index: 999
}
  • 搜索页面防抖处理
    1. 定义 timer 用来存放定时器 timer: null,keyWords 变量用来存放用户输入
        searchInput(e) {
            clearTimeout(this.timer)
            this.timer = setTimeout(()=> {
                this.keyWords = e
                // this.getSearchList()
            },500)
        }
    
  • 匹配搜索关键字
...
data(){
    return {
        searchResult: []
    }
}
...
async getSearchList() {
	// 判断搜索关键词是否为空
	// console.log(this.keyWords.trim().length)
	if(this.keyWords.trim().length === 0) {
		this.searchResult = []
		return
	}
	const result = await uni.$http.get('https://api-ugo-web.itheima.net/api/publv1/goods/qsearch',{query : this.keyWords})
	if(result.data.meta.status !== 200) return uni.$uniShowMessage()
	this.searchResult = result.data.message
}

uni 本地存储

  • uni.setStorageSync('存储的key名',JSON.stringify(数据))
  • JSON.parse(uni.getStorageSync('key名') || '[]')
  • uni.removeStorageSync('指定删除的key名')
  • uni.clearStorageSync() // 全清空

uni confirm 提示窗

clear() {
	uni.showModal({
		title: '提示',
		content: '确定要清空吗?',
		cancleText: '取消',
		confirmText:'确认',
		showCancel:true, // 是否显示默认按钮,默认为 true
		success: (result) => {
			if(result.confirm){
				this.history = []
				uni.removeStorageSync('searchKeyWords')
			}else {
				return
			}
		}
	})
}

uni 单管道符

  • {{item.goods_price | toFixed}} 意思前面的数据通过后面的函数处理之后再展示
  • toFixed 函数必须声明在 filters 配置项里,filters 配置项和 data 等配置项同级

实现上拉加载

上拉触底

  • 监听页面的上拉触底事件:onReachBottom()
  • 请求数据时的 loading 效果(uni-app 封装了这个功能)
    • 在请求发起前调用:wx.showLoading({title: '拼命加载中'}),加载完成后不会自动关闭
    • 在请求结束后的回调中调用: wx.hideLoading(),关闭加载动画
  1. 打开项目根目录中的 pages.json 配置文件,为 subPackages 分包中的 goods_list 页面 配置上拉触底的距离:
{
    "path" : "goods_list/goods_list",
    "style": {
        "navigationBarTitleText": "",
		"onReachBottomDistance": 150,
        "enablePullDownRefresh": false
    }
}
  1. 在 goods_list 页面中,和 methods 节点平级,声明 onReachBottom 事件处理函数,用 来监听页面的上拉触底行为:
	// 上拉触底事件
		onReachBottom() {
			// 让页面自增
			this.queryParam.pagenum += 1
			// 重新获取列表数据
			this.getGoodsList()
		}
  1. 改造 methods 中的 getGoodsList 函数,当列表数据请求成功之后,进行新旧数据的拼接处 理:
async getGoodsList() {
	const result = await uni.$http.get('https://api-ugo-web.itheima.net/api/public/v1/goods/search', this.queryParam)
	if (result.data.meta.status !== 200) return uni.$uniShowMessage()
	// 从直接赋值改为拼接数据
	this.goodsList = [...this.goodsList,...result.data.message.goods]
	this.total = result.data.message.total
}
  1. 添加节流阀
  2. 判断还有无数据
    • 如果:当前的页码值 * 每页显示多少条数据 >= 总数条数 也就是:pagenum * pagesize >= total
    • onReachBottom 函数里:if(this.queryParam.pagesize * this.queryParam.pagenum >= this.total) return

下拉刷新

  • 监听页面的下拉刷新事件:onPullDownRefresh()
  • 真机测试中,不能自动关闭下拉的动画
    • onPullDownRefresh()函数执行的最后调用 wx.stopPullDownRefresh()
  1. 配置页面开启下拉刷新 enableOnPullDownRefresh
  2. 监听下拉刷新事件 onpullDownRefresh
onPullDownRefresh() {
	// 重置关键数据
	this.queryParam.pagenum = 1
	this.total = 0
	this.isLoading = false
	this.goodsList = []
	// 重新发起请求,并传入关闭的回调,在合适的时候调用
	this.getGoodsList(()=>uni.stopPullDownRefresh())
}
  1. 改造 getGoddsList 函数,调用关闭回调

uni 轮播图片预览

  1. 轮播图片绑定 click 事件,将图片 index 传递出去
<swiper circular="true" :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000">
	<swiper-item v-for="(item,index) in goods_detail.pics" :key="index">
		<image :src="item.pics_big" @click="preview(index)"></image>
    </swiper-item>
</swiper>
  1. 事件内调用uni.previewImage({})
preview(index){
    uni.previewImage({
        // 预览时,默认显示图片的索引
        current: index,
        // 所有团 url 地址的数组
        urls: this.goods_detail.pics.map(item => item.pics_big)
    })
}

富文本渲染 商品详情页面

  • rich-text 富文本组件,支持把 HTML 字符串渲染为 WXML 结构
  • nodes 属性, nodes="<h1 style='color:red'>介绍</h1>"
  • 常应用于商品的详情介绍页(从服务器返回的是一串包含 html 的字符串时)
  1. 在页面结构中,使用 rich-text 组件,将带有 HTML 标签的内容,渲染为小程序的页面结构:
<rich-text :nodes="goods_info.goods_introduce"></rich-text>
  1. 解决图片底部 空白间隙 的问题,并将 webp 格式替换为 jpg,解决 .webp 格式图片在 ios 设备上无法正常显示的问题:
async getGoodsDetail(goods_id) {
	const {data: result} = await uni.$http.get('https://api-ugo-web.itheima.net/api/public/v1/goods/detail',{goods_id:goods_id})
	if(result.meta.status !== 200) return uni.$uniShowMessage()
	// 添加行内样式,消除富文本渲染的图片底部的空白间隙问题
	result.message.goods_introduce = result.message.goods_introduce.replace(/<img/g,'<img style="display:block;"').replace(/webp/g,'jpg')
	this.goods_detail = result.message
},
  1. 价格、名称等闪烁问题:页面刷新时,价格先显示的是 undefined,因为在商品信息请求完成之前,goods_detail 里的 price 为 undefined,应该用 v-if 判断一下商品信息是否存在再显示,比如判断商品名称是否存在

uni 使用 vuex 实现状态管理

  • 创建 store
    // /store/store.js
    import Vue from 'vue'
    import Vuex from 'vuex'

    Vue.use(Vuex)

    const store = new Vuex.Store({
        modules: {
            // 数据挂载处
        }
    })

    export default store
  • 注册 store
    // /main.js
    ...
    import store from '@/store/store.js'
    ...
    const app = new Vue({
        ...App,
        store
    })
  • 创建公共数据 cart
    // /store/cart.js
    export default {
        namespaced: true // 开启命名空间
        state: ()=>({
            cart: JSON.parse(uni.getStorageSync('cart') || '[]')
        }),
        mutations: { // 操作数据的方法,必须定义在此
            addToCart(state, goods) { // 定义一个加入购物车的方法
                const result = state.cart.find(item => item.goods_id === goods.goods_id)
                if(!result) {
                    state.cart.push(goods)
                }else {
                    result.goods_count++
                }
                this.commit('moduleCart/saveCartInfo')
            },
            saveCartInfo(state) {  // 本地存储购物车数量
                uni.setStorageSync('cart',JSON.stringify(state.cart))
            }
        }, 
        getters: {
            total(state) { // 动态统计购物车中的数量
                let n = 0
                this.goods_detail.forEach(item => n += item.goods.count)
                return n
            }
        }
    }
  • 数据挂载
    // /store/store.js
    import Vue from 'vue'
    import Vuex from 'vuex'
    import moduleCart from '@/store/cart.js'

    Vue.use(Vuex)

    const store = new Vuex.Store({
        modules: {
            moduleCart:moduleCart
        }
    })

    export default store
  • 使用数据
    // /subpackage/goodsDetail/goods_detail.vue
    import { mapState,mapMutations,mapGetters } from 'vuex'
    export default {
        computed: {
            ...mapStates('moduleCart', ['cart']),
            ...mapGetters('moduleCart',['total'])
        },
        methods: {
            ...mapMutations('moduleCart',['addToCart'])
        },
        watch: {
            total(newValue) {
                const result = this.options.find(item => item.text === '购物车')
                if(result) {
                    result.info = newValue
                }
            }
        }
    }
  • 如果购物车中有商品,在首次加载详情页时,不显示购物车的 info
    • 原因:使用了 watch 的简写形式
    • 解决:改成完整形式的 watch
        watch: {
            total: {
                handler(newValue) {
                    const result = this.options.find(item => item.text === '购物车')
                    if(result) {
                        result.info = newValue
                    }
                },
                immediate: true
            }
        }
    
  • tabBar 页面的徽标数字展示: uni.setTabBarBadge()
    // /pages/cart.vue
    // 1. 映射 store 里的 total
    // 2. 在页面 onLoad 时调用函数
    // 3. 定义函数
    setBadge() {
        uni.setTabBarBadge({
            index: 2, // 购物车按钮在 tabBar 组件内的 索引,购物车在第三个
            text: this.total + '' // text 只支持字符串
        })
    }

uni 单选框

  • <radio checked color="#c00000"/>
  • 更新单选框选中状态
    1. checked 属性改为动态绑定 <radio :checked="goods.goods_state" color="#c00000"/>
    2. 外部传入自定义事件: <my-goods @update:checked="checkedChangeHandler"></my-goods>
    3. 内部绑定 click 事件用来触发外部传入的自定义事件: <radio @click="checkedChangeHandler" :checked="goods.goods_state" color="#c00000" />
    4. 定义事件处理函数,将商品 id 和 state 传出去
        // my-goods 组件
        checkedChangeHandler() {
            this.$emit('update:checked',{
                goods_id: this.goods.goods_id,
                goods_state: !this.goods.goods_state // 传递最新的状态,所以要取反
            })
        }
    
    1. 定义 vuex / store 全局方法
    // cart.js
    ...
    mutations: {
        ...
        updateChecked(state, goods) {
            const result = state.cart.find(item => item.goods_id === goods.goods_id)
            if(result) {
                result.goods_state = goods.goods_state
                this.commit('moduleCart/saveCart') // 保存到本地存储
            }
        }
    }
    
    1. cart.vue 引入 updateChecked 方法
        ...
        checkedChangeHandler(e) {
            this.updateChecked(e)
        }
    

uni 数字加减按钮

  • <uni-number-box :min="1" :max="9"></uni-number-box>

uni 滑动列表出现删除按钮

  • 滑动删除需要用到 uni-ui 的 uni-swipe-action 组件和 uni-swipe-action-item。详细的官方文档请参考 SwipeAction 滑动操作。
  • 要求基本库 2.14.0 以上
  • iOS 端由于存在bounce效果,滑动体验略差,建议禁止bounce效果

uni 收件地址

  • uni.chooseAddress()返回值是一个数组:第 1 项为错误对象;第 2 项为成功之后的收货地址对象
    async chooseAdress() {
        const [error, sucsess] = await uni.chooseAdress()
        if(error === null && sucesee.errMsg === 'chooseAdress:ok') {
            this.address = sucsess
        }
    }

js 基础:数组的 every、some 方法复习

  • 好久没用,忘了这俩 API 了
  • array.every(i => i.name === '张三') array 中每一个元素的 name 都为张三返回 true
  • array.some(i => i.name === '张三') array 中只要有一个元素的 name 为张三返回 true
  • some 不会检测空数组

用户登录

  1. 为登录的 button 绑定 open-type="getUserInfo" 属性,表示点击按钮时,希望获取用户的基本信息:
    • <button type="primary" @click="getUserProfile"></button>
  2. getUserInfo 事件处理函数:
        getUserProfile() {
            // 1. 判断用户信息是否获取成功
            if (e.detail.errMsg === 'getUserInfo:fail auth deny') return uni.$showMsg('您取消了登录授权!')
            // 获取用户信息成功, e.detail.userInfo 就是用户的基本信息
            uni.getUserProfile({
    			desc:'test', // 获取用户信息的用途介绍,最多三十个字符
    			success:(userInfo)=> { // 成功
    			 	console.log(userInfo)
                    this.getToken(userInfo) // 登录函数
    			},
    			fail:(error)=> { // 失败
    			 	if(error.errMsg = 'getUserProfile:fail auth deny') {
                        uni.showToast({
                            title: '用户取消了授权',
                            duration: 1500,
                            icon: 'none'
                        })
    				}
    			}
    		})
        }
    
  • 登录需要的五个参数
    1. code 用户登录凭证 uni.login()
    2. encryptedData 完整用户信息密文 getUserInfo
    3. iv 加密算法的初始向量 getUserInfo
    4. rawDate 用户信息元素数据字符串 getUserInfo 获取 JSON.stringify() 转字符串
    5. signature 使用 shal 得到字符串 getUserInfo
  • 登录
    • 因为微信小程序的登录是调用 uni.login() 获取临时登录 id,然后在服务端调用 调用 auth.code2Session 接口,目前只有被服务器端管理员配置为小程序的开发者以后,才能调用成功登录,支付这些特殊的接口,目前做不了
async getToken(userInfo) {
	const [error, response] = await uni.login().catch(error => error)
	console.log(response)
	if(error || response.errMsg !== "login:ok") return uni.$uniShowMessage('登录失败!')
				
	// 初始化参数
	const query = {
		code: response.code,
		encryptedData: userInfo.encryptedData,
		iv: userInfo.iv,
		rawData: userInfo.rawData,
		signature: userInfo.signature 
	}
				
	// 换取 token
	const { data: loginResult } = await uni.$http.post('https://api-ugo-web.itheima.net/api/public/v1/users/wxlogin',query)
	if(loginResult.meta.status !== 200) return uni.$uniShowMessage('登录失败!')
	    uni.$uniShowMessage('登录成功 !')
	}

微信支付

  • 在请求头中添加 Token 身份认证的字段(只有在登录之后才允许调用支付相关的接口,所以必须为有权限的接口添加身份认证的请求头字段)
// main.js 前置路由守卫
$http.beforeRequest = function(options){
    uni.showLoading({
        title: '拼命加载中...'
    })

    // 只有登录过的用户路径里才会有 my(个人信息) 
    if(options.url.indexOf('/my/') !== -1) {
        // 为请求头添加身份认证字段
        options.header = {
            // 字段值直接从 vuex 中获取
            Authorization: store.state.moduleAddress.token,
        }
    }
}
  • 支付流程

    1. 创建订单
      • 请求创建订单的 API 接口:把(订单金额、收货地址、订单中包含的商品信息)发送到服务器
      • 服务器响应的结果:订单编号
    2. 订单预支付
      • 请求订单预支付的 API 接口:把(订单编号)发送到服务器
      • 服务器响应的结果:订单预支付的参数对象 ,里面包含了订单支付相关的必要参数
    3. 发起微信支付
      • 调用 uni.requestPayment() 这个 API,发起微信支付;把步骤 2 得到的 “订单预支付对象” 作为参数传递给 uni.requestPayment() 方法
      • 监听 uni.requestPayment() 这个 API 的 success , fail , complete 回调函数
  • 支付

    1. 在点击了结算按钮后,调用微信支付的方法
    2. 定义方法
        async payOrder() {
            // 创建订单,初始化订单对象
            const orderInfo = {
                // 开发期间,注释掉真实的订单价格
                // order_price:this.checkedGoodsAmount,
                // 写死价格
                order_price:0.01,
                consignee_addr: this.addstr,
                goods: this.cart.filter(item => item.goods_state).map(item => ({
                    goods_id: item.goods_id,
                    goods_number: item.goods_count,
                    goods_price: item.goods_price
                }))
            }
    
            // 发起请求创建订单
            const { data: result } = await uni.$http.post('服务器订单接口',orderInfo)
            if(result.meta.status !== 200) return uni.$uniShowMessage('订单创建失败!')
    
            // 得到服务器响应的 订单编号
            const orderNumber = result.message.order_number
    
            // 订单预支付 发起请求获取订单的支付信息
            const { data: result2 } = await uni.$http.post('服务器预支付接口',{
                order_number: orderNumber
            })
            if(result2.meta.status !== 200) return uni.$uniShowMessage('预支付订单生成失败!')
            
            // 得到订单支付相关参数
            const payInfo = result2.message.pay
    
            // 发起微信支付
            const [error, success] = await uni.requestPayment(payInfo)
            if(error) return uni.$uniShowMessage('订单未支付!')
    
            // 支付完成,查询支付结果
            const { data: result3 } = await uni.$http.post('服务器订单查询接口',{
                order_number: orderNumber
            })
            if(result3.meta.status !== 200) return uni.$uniShowMessage('订单未支付!')
            uni.showToast({
                title:'支付完成!',
                icon:'success'
            })
        }