# mini-program_230313 **Repository Path**: newsegmentfault/mini-program_230313 ## Basic Information - **Project Name**: mini-program_230313 - **Description**: mini-program_230313 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2023-08-30 - **Last Updated**: 2023-09-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 小程序基础 ### 小程序介绍 1. Mini Program,是一种不需要下载安装即可使用的应用 ( 张小龙对其的定义是无需安装,用完即走,实际上是需要安装的,只不过小程序的体积特别小, 下载速度很快,用户感觉不到下载的过程 ) 2. 小程序提供了一个简单、高效的应用开发框架和丰富的组件及API,帮助开发者在微信中开发具有原生 APP 体验的服务。 3. 小程序刚发布的时候要求压缩包的体积不能大于1M,,否则无法通过,在2017年4月做了改进,由原来的【1M】提升到【2M】; 4. 2017年1月9日0点,万众瞩目的微信第一批小程序正式低调上线。 **优点** 1. 同App进行互补,提供同app类型的功能,比app使用方便简洁 2. 通过扫一扫或者在微信搜索即可下载 3. **用户使用频率不高,但又不得不用的功能软件,目前看来小程序是首选** 4. 开发门槛低, 成本低 **准备工作** 1. 官网注册:https://mp.weixin.qq.com/ 2. 微信开发工具 开发工具IDE文件夹中获取 3. 微信开发下载地址 https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html?t=2018315 ### 小程序注册 建议在PC完成注册,需要邮箱验证,需要实名认证 注册目的:获取到小程序的 AppID(小程序唯一标识) ,注册成功后在 【开发管理】-> 【开发设置】页面获取 注意:如果要选择小程序类目不要选择小游戏 ### 编辑器创建项目 - minidemo 注意:创建项目的时候,项目路径是空路径可以选择模板,不是空路径(路径下有文件夹)不能选择模板 介绍编辑器如何使用 ### 项目文件介绍 ```js |- pages 存放页面的文件夹 |- index index页面 |- index.js index页面js文件 |- index.json index页面json配置文件 |- index.wxml index页面wxml文件(可理解为index页面的html) |- index.wxss index页面wxss文件(可理解为index页面的css) |- logs logs页面 |- utils 所有工具类方法放在该文件夹 |- util.js 存放工具类方法文件 |- .eslintrc.js eslint规则配置 |- app.js 整个小程序应用的js文件 |- app.json 整个小程序应用的json配置文件(修改导航条、配置页面) |- app.wxss 整个小程序应用的wxss文件(可理存放全局的css样式) |- project.config.json 整个小程序应用的配置文件 |- project.private.config.json 整个小程序应用的私有配置文件,会覆盖project.config.json中的内容 |- sitemap.json 配置当前小程序哪些页面可以被微信检索到 ``` **小程序特点** 1. **没有完整的浏览器对象,没有DOM,BOM操作** 2. 组件化开发 3. 逻辑层和渲染层分开(逻辑层不会阻塞渲染层的渲染) 正常的浏览器环境中js加载执行会影响页面渲染 4. 体积小,单个压缩包体积不能大于2M,否则无法上线 5. 小程序的四个重要的文件 | ***.wxml** | ***.wxss** | ***.js** | ***.json** | | ---------- | ---------- | -------- | ------------ | | View结构 | View样式 | View逻辑 | View配置文件 | ### 单向数据绑定和双向数据绑定 ```js Page({ data: { id: 1001, firstName: '尼古拉斯', lastName: '赵四', age: 18 }, }) 姓: {{ firstName }} 名: {{ lastName }} 年龄: {{ age }} ----------------- ``` ### 事件绑定 ```js Page({ data: { id: 1001, firstName: '尼古拉斯', lastName: '赵四', age: 18 }, changeName() { console.log('触发点击事件', this.data) // this.data.lastName = '王五' // 错误的 this.setData({ lastName: '王五' }) } }) 姓: {{ firstName }} 名: {{ lastName }} 年龄: {{ age }} ----------------- bindtap 绑定事件 属性值直接跟回调 回调直接写在和data同级的配置项即可 读取数据 在js中读取数据的时候 使用 this.data.xxx 修改数据 在js中修改响应式数据使用 this.setData this.setData({ xxx: '数据' }) ``` ### 关于事件参数 ```js Page({ data: { id: 1001, firstName: '尼古拉斯', lastName: '赵四', age: 18 }, changeName(e) { console.log('触发点击事件', e.currentTarget.dataset.abc) console.log('触发点击事件', e.currentTarget.id) } }) 姓: {{ firstName }} 名: {{ lastName }} 年龄: {{ age }} ----------------- 关于事件回调的参数 在wxml中,回调后面不能跟小括号,没有小括号这一说,直接放的就是回调的名字 此时在回调中是有默认参数的,默认参数是e,是事件对象(和浏览器的事件对象长得不一样) 传参只能通过给元素绑定属性传参 绑定以data-开头的属性,在回调的事件对象e中可以获取到 e.currentTarget.dataset.abc 如果绑定的是id的话,比较特殊,获取的时候 e.currentTarget.id ``` ### 列表循环 ```js Page({ data: { list: [ { id: 1001, content: '唱' }, { id: 1002, content: '跳' }, { id: 1003, content: 'rap' }, { id: 1004, content: '篮球' }, ] }, }) {{ index }} -- {{ item.content }} ------------------ {{ idx }} -- {{qwer.content}} ``` ### 条件渲染 ```js Page({ data: { isShow: false, sex: 2 // sex: 0, 1, 2 }, }) 表白成功 表白失败 ----------------- 未知 ``` ### 模板使用 `/pages/tempA/tempA.wxml` ```js ``` `/pages/tempA/tempA.wxss` ```css .content { color: red } ``` 使用模板 ```html 使用模板 ``` ```css @import "/pages/tempA/tempA" ``` ### 生命周期 官方图示:https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/page-life-cycle.html ![](images/page-lifecycle.png) | **生命周期函数** | **执行时机** | **执行次数** | **特点说明** | | ---------------- | ------------------------ | ------------ | ------------------------------------------------------------ | | **onLoad** | 页面加载时触发 | 1 | 可以通过参数获取打开当前页面路径中的参数 | | **onShow** | 页面显示/切入前台时触发 | 多次 | 如页面没有被销毁,会重复多次显示隐藏,可以在此发送请求获取最新数据 | | **onReady** | 页面初始化渲染完成时触发 | 1 | 可以同UI界面进行交互 | | **onHide** | 页面隐藏/切入后台时触发 | 多次 | [wx.navigateTo](https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.navigateTo.html) 或底部 tab 切换到其他页面,小程序切入后台等 | | **onUnload** | 页面卸载时触发 | 1 | [wx.redirectTo](https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.redirectTo.html)或[wx.navigateBack](https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.navigateBack.html)到其他页面时 | **个人见解** 官网生命周期图示是错误的;错误部分:标注onLoad及onShow执行的位置不对 参考:**小程序启动执行的所有流程** 参考地址: [**https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips/start_process.html**](https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips/start_process.html) ### 小程序适配方案: rpx (responsive pixel响应式像素单位) 小程序适配单位: rpx 规定任何屏幕下宽度为750rpx 小程序会根据屏幕的宽度不同自动计算rpx值的大小 Iphone6下: 1rpx = 1物理像素 = 0.5px ![rpx](images/rpx.png) # 慕尚花坊 ## 项目介绍 导入已完成项目,看一下大概要做拿一些东西 介绍了接口文档: 接口文档地址: http://39.98.123.211:8300/doc.html https请求的base路径:https://gmall-prod.atguigu.cn ## 项目准备 创建项目 - mini_flower 把项目中没用的页面删除掉,只留下 首页 把 static 静态的图片粘贴过来 使用 vscode 开发小程序,把项目用 vscode 打开,同时下载插件 ![](images/01.png) ## 导航设置 app.json ```json { "pages":[ "pages/index/index" ], "window":{ "navigationBarBackgroundColor": "#9c0211", "navigationBarTitleText": "慕尚花坊", "navigationBarTextStyle":"white" }, } ``` ## 首页轮播 1. 静态搭建 ```html ``` 2. api准备 - 初始化数据展示 ## 封装 request.js 文件 api准备要发请求,发请求得有 request.js 文件(这个文件在之前是对 axios 的二次封装),我们这里不用 axios,微信有自己发请求的api,但是需要封装 utils/request.js ```js const baseURL = `https://gmall-prod.atguigu.cn` export default function request({ // 使用对象接收参数不需要考虑参数顺序问题 url, data, method = 'get', header = {'content-type':'application/json'}, timeout = 60000, }) { // 在创建promise实例的时候,传入一个构造器函数,这个函数是立即执行的 // 这个函数有两个参数 resolve, reject // 当resolve调用的时候,此时这个promise状态才由 pendding -> 成功 // 当reject调用的时候,此时这个promise状态才由 pendding -> 失败 return new Promise((resolve, reject) => { wx.request({ url: baseURL + url, // 地址 data, // 参数 method, // 请求方式 header, // 请求头 timeout, // 超时时间 dataType: 'json', // 返回数据格式 responseType: 'text', // 响应文本类型 success: (result)=>{ // 请求成功回调 // result 是响应的所有内容 // result.data 是响应体 let res = result.data // 是响应体 if (res.code == 200) { resolve(res.data) } else { // 错误的统一处理 console.error(res) // 给程序员看的 // 谈个弹框,给个提示 reject(res.message) } }, fail: (err)=>{ // 请求失败回调 reject(err) }, complete: ()=>{ // 不管请求成功或失败都会执行 } }); }) } // let result = await request({ // url: '/xxxxx', // data: {xxx}, // }) ``` page/index/index ```js // 测试代码 import request from "../../utils/request" async function getData() { let result = await request({ url: '/mall-api/index/findBanner' }) console.log('请求回来的数据', result) } getData() ``` ## 优化 request.js 文件 1. 添加错误处理的提示 ```js wx.showToast({ title: '请求失败', icon: 'error', }); ``` 2. 添加 loading 状态 在请求之前调用 ```js wx.showLoading({ title: "正在加载" }); ``` 不管请求成功还是失败都调用隐藏loading ```js complete: ()=>{ // 不管请求成功或失败都会执行 wx.hideLoading(); // 隐藏弹框 } ``` ## 首页轮播 - 获取数据展示 2. api 准备 ```js import request from '../utils/request' // 获取轮播数据 export const reqBannerList = () => { return request({ url: `/mall-api/index/findBanner` }) } ``` 初始化数据获取 - pages/index/index.js ```js Page({ data: { bannerList: [] }, async getBannerList() { try { let result = await reqBannerList() this.setData({ bannerList: result }) } catch (error) { console.error(error) } }, onLoad: function (options) { this.getBannerList() } }) ``` 初始化数据展示 - pages/index/index.wxml ```html ``` **关于路径映射@** 在 app.json 中配置 ```json { "resolveAlias": { "@/*": "/*" }, } ``` ## 首页导航 1. 静态搭建 ```html {{ item.name }} 注意: image 组件的 mode="widthFix" 是设置宽度,保持图片的宽高比不变,自己去算高度 /* 导航 */ .nav-container { margin: 10rpx 0; width: 100%; display: flex; flex-wrap: wrap; /*允许flex中的item换行*/ } .nav-view { width: 20%; display: flex; flex-direction: column; /* flex主轴变垂直方向 */ align-items: center; } .nav-img { margin: 20rpx 0; width: 60%; } .nav-img.small { width: 30%; } .nav-title { font-size: 28rpx; } ``` 2. api准备 - 初始化数据展示 ```js // 获取导航列表数据 - /mall-api/index/findCategory1 export const reqNavList = () => { return request({ url: `/mall-api/index/findCategory1` }) } ``` 初始化请求数据 pages/index/index.js ```js async getNavList() { try { let result = await reqNavList() console.log(result) this.setData({ navList: result }) } catch (error) { console.error(error) } }, onLoad: function (options) { ... this.getNavList() } ``` 页面循环展示数据即可 ## 活动图片 没有接口,使用mock数据,讲mock粘贴到根目录下,然后在页面中引入 ```js import { backgroundImg } from '../../mock/swiper' Page({ data: { ... backgroundImg: backgroundImg, // 活动图片 } } ``` 页面中直接使用即可 ```html ``` ## goods-list 静态 在 /components/goods-list文件夹下右击新建一个组件(微信开发者工具中新建) 在页面的 index.json 配置文件中去使用组件 ```json { "usingComponents": { "goods-list": "/components/goods-list/goods-list" } } ``` 此时在页面中就可以使用 `` 组件了 搭建组件静态,中间列表区域再封装一个组件 ```html 猜你喜欢 列表 查看更多 ``` ```css .goods-container { margin-top: 40rpx; width: 100%; } .title { font-size: 32rpx; text-align: center; margin-bottom: 20rpx; } .more { margin: 0 40rpx; height: 80rpx; line-height: 80rpx; border-radius: 40rpx; background-color: #fff; font-size: 32rpx; text-align: center; } ``` ## goods-card静态 ```html Luv甜蜜Luv甜蜜Luv甜蜜Luv甜蜜 再一次触动妳的心.轻启一段甜蜜的恋爱物语. 用途: 情人、生日、追求她 ¥ 1399 ¥ 1599 Luv甜蜜Luv甜蜜Luv甜蜜Luv甜蜜 再一次触动妳的心.轻启一段甜蜜的恋爱物语. 用途: 情人、生日、追求她 ¥ 1399 ¥ 1599 ``` ```css .list { width: 100%; display: flex; flex-wrap: wrap; /*允许折行*/ justify-content: space-around; } .goods { width: 45%; background-color: #fff; margin-bottom: 20rpx; } .goods-img { width: 100%; } /* 内容 */ .content { padding: 10rpx; } /* 标题 */ .title { font-size: 28rpx; font-weight: bold; /* 单行省略号css样式 */ text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } /* 描述 */ .desc { height: 64rpx; margin: 20rpx 0; font-size: 24rpx; color: #999; /* 多行省略号 */ display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp: 2; overflow: hidden; } /* 价格 */ .info { display: flex; font-size: 24rpx; align-items: center; } .price { margin-right: 20rpx; } .oldprice { color: #666; margin-right: 40rpx; text-decoration: line-through; /*中间画横线*/ } .cart { width: 40rpx; height: 40rpx; } ``` ## goods-list 获取数据动态显示 1. 组件传参 - 把标题"猜你喜欢", "人气推荐"传入到组件中 怎么传? --- 标签直接帮属性即可 `` 怎么接? 组件内使用 properties 接(不支持数组,支持对象和配置对象) ```js // 不支持数组写法 // properties: ["title"], // 支持对象写法 // properties: { // title: String, // }, // 支持配置对象写法 properties: { title: { type: String, required: true }, }, ``` 2. 准备api、首页初始化获取数据,获取到数据传给 goods-list 组件,goods-list 组件拿到数据之后再传给 goods-card 组件 准备api ```js // 猜你喜欢接口 - /mall-api/index/findListGoods export const reqLikeList = () => { return request({ url: `/mall-api/index/findListGoods` }) } // 人气推荐接口 - /mall-api/index/findRecommendGoods export const reqRecommendList = () => { return request({ url: `/mall-api/index/findRecommendGoods` }) } ``` 首页初始化调用 ```js async getLikeList() { try { let result = await reqLikeList() console.log(result) this.setData({ likeList: result }) } catch (error) { console.error(error) } }, async getRecommendList() { try { let result = await reqRecommendList() console.log(result) this.setData({ recommendList: result }) } catch (error) { console.error(error) } }, onLoad: function (options) { ... this.getLikeList() // 获取猜你喜欢数据 this.getRecommendList() // 获取人气推荐数据 } ``` 传给 goods-list ```html ``` goods-list 接数据 ```js properties: { title: { type: String, required: true }, list: { type: Array, required: true } }, ``` goods-list 接完数据再原封不动给 goods-card ```html ``` goods-card 接数据 ```js properties: { list: Array }, ``` 循环展示数据 ```html {{ item.name }} {{ item.floralLanguage }} ¥ {{item.price}} ¥ {{item.marketPrice}} ``` ## 底部tabbar 配置 地址: https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#tabBar app.json 文件中配置 ```json "tabBar": { "color": "#333", "selectedColor": "#ff582f", "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "/static/tabbar/home-icon1.png", "selectedIconPath": "/static/tabbar/home-icon1-1.png" }, { "pagePath": "pages/category/category", "text": "分类", "iconPath": "/static/tabbar/home-icon2.png", "selectedIconPath": "/static/tabbar/home-icon2-2.png" }, { "pagePath": "pages/cart/cart", "text": "购物车", "iconPath": "/static/tabbar/home-icon3.png", "selectedIconPath": "/static/tabbar/home-icon3-3.png" }, { "pagePath": "pages/personal/personal", "text": "我的", "iconPath": "/static/tabbar/home-icon4.png", "selectedIconPath": "/static/tabbar/home-icon4-4.png" } ] }, ``` ## 分类页面 1. 静态搭建 页面宽高设置100%是相对于父级的,而父级是 page 这个元素,这个元素需要设置 宽高100%才可以,这个元素样式在 app.wxss 中设置,这是个全局的,每个页面的根元素都是 page 这个元素 ```html 鲜花玫瑰 鲜花玫瑰 鲜花玫瑰 鲜花玫瑰 爱礼精选 真情告白 真情告白 真情告白 真情告白 ``` ```css .category-container { width: 100%; height: 100%; display: flex; } /* 左 */ .menu { width: 25%; height: 100%; background-color: #eee; } .menu-list { width: 100%; height: 100%; } .menu-item { position: relative; width: 100%; height: 70rpx; line-height: 70rpx; font-size: 24rpx; text-align: center; } .menu-item.active { background-color: #fff; } .menu-item.active::before { position: absolute; left: 10px; top: 15%; content: ""; display: block; width: 4rpx; height: 70%; background-color: red; } /* 右 */ .content { width: 75%; height: 100%; } .content-title { padding: 20rpx; } .content-list { display: flex; flex-wrap: wrap; } .content-item { width: 33.33%; display: flex; flex-direction: column; align-items: center; } .content-img { width: 40%; margin: 20rpx 0; } .content-name { font-size: 24rpx; } ``` 注意:scroll-view 如果要使用flex布局,需要设置enable-flex属性 2. 初始化数据展示 api准备 ```js export const reqCategoryTree = () => { return request({ url: `/mall-api/index/findCategoryTree` }) } ``` 初始化页面(onLoad)发请求,获取数据,展示数据 ```js data: { categoryList: [], // 左侧列表数据 activeIndex: 0, // 选中左侧的下标,默认是0 contentList: [] // 右侧展示的数组 }, async getCategoryTree() { try { let result = await reqCategoryTree() this.setData({ categoryList: result, contentList: result[this.data.activeIndex].children }) } catch (error) { console.error(error) } }, onLoad(options) { this.getCategoryTree() } ``` 拿到数据循环展示即可 3. 交互 ```js 左侧循环渲染项绑定index {{item.name}} changeContent(e) { let activeIndex = e.currentTarget.dataset.index let contentList = this.data.categoryList[activeIndex].children this.setData({ activeIndex, contentList }) }, ``` 分类中右侧的每个内容(item),点击可以进入商品列表 ## 商品列表 创建商品列表页面,/pages/goods/list/list 问: 商品列表页目前有几个入口?(点击哪里可以进入商品列表页?) 目前已知的有两个入口,首页的"查看更多"和分类页右侧的二级分类点击可以进入 问: 这两个进入页面之后有什么差异? ![](images/02.png) 这里面差了一个query参数, category2Id, > 注意: 从分类页右侧的二级分类进入,需要携带点击的参数(category2Id就是二级分类的Id) > > ​ 从首页的"查看更多"进入,不需要携带参数 ### 跳转页面 使用标签和js都可以跳转 1. 标签跳转 ```html ``` 同时携带了 query 参数(路由带参) 2. js跳转 ```js wx.navigateTo({ url: '/pages/goods/list/list', }); ``` 没带参数 > 注意: > > 页面中的方法在配置项中设置的时候和data同级 > > 组件中的方法需要放在 methods 配置项中 ### 初始化数据展示 api准备 ```js export const reqGoodsList = (page, limit, data = {}) => { return request({ // url: `/mall-api/goods/list/${page}/${limit}?category2Id=${category2Id}` url: `/mall-api/goods/list/${page}/${limit}`, // data: { // category2Id: category2Id // } data //有data就拼接上,没有就不拼接 }) } ``` 页面初始化的时候调用接口 ```js data: { page: 1, limit: 10, category2Id: undefined, // 请求需要携带的参数 goodsList: [] // 商品列表数据 }, // 初始化数据 async getGoodsList() { // 组转数据 const { page, limit, category2Id } = this.data let data = {} if (category2Id) { data.category2Id = category2Id } // 发送请求 try { let result = await reqGoodsList(page, limit, data) this.setData({ goodsList: result.records }) } catch (error) { console.error(error) } }, onLoad(options) { let category2Id = options.category2Id if (category2Id) { this.setData({ category2Id }) } this.getGoodsList() } ``` 拿到数据去页面展示(注意这里要使用 goods-card 组件,需要在 .json 文件中引入 goods-card) ```json { "usingComponents": { "goods-card": "/components/goods-card/goods-card" } } ``` 展示页面 ```html ``` ### 交互 1. 当页面滚动到底部的时候,要加载第二页数据,这里是【分页处理】 2. 当页面没有更多数据的时候展示"没有更多了" 3. 当页面没有数据的时候,展示"该分类下无商品"内容 #### 分页处理 1. 页面触底的时候要发送请求,需要页面触底的回调(于data配置项同级) ```js data: { ...... status: 'more' // 页面发请求的状态,总共有以下几个值: 'more'更多 'no-more'没有更多 'loading'加载中 'error'错误 }, // 页面触底回调 onReachBottom() { if (this.data.status == 'no-more') { return } // 翻页 this.setData({ page: this.data.page + 1 }) this.getGoodsList() }, async getGoodsList() { this.setData({ status: 'loading' }) // 组转数据 const { page, limit, category2Id } = this.data let data = {} if (category2Id) { data.category2Id = category2Id } // 发送请求 try { let result = await reqGoodsList(page, limit, data) let goodsList = this.data.goodsList.concat(result.records) // 之前列表中的值不能清空 let status = 'more' if (goodsList.length == result.total) { // 当获取到所有的数据之后,状态改为no-more status = 'no-more' } ...... ``` > 这里使用 goosList.length 和 totoal 去判断也可以 #### 展示"没有更多了" - 第三方组件库的使用(vant) 打开vant官网,找到小程序版本点开,找到快速上手文档 地址:https://vant-ui.github.io/vant/#/zh-CN/ ![](images/03.png) 1. 安装第三方包 ```js npm i @vant/weapp -S --production ``` > 注意:初始化 package.json 指令是 `npm init` 2. 修改 app.json 文件中的 `"style": "v2"` , 把这行代码删掉 3. 配置 project.config.json 文件 ```js { ... "setting": { ... "packNpmManually": true, "packNpmRelationList": [ { "packageJsonPath": "./package.json", "miniprogramNpmDistDir": "./miniprogram/" } ] } } ``` > 注意:新版的"微信开发者工具"需要把 ` "miniprogramNpmDistDir": "./miniprogram/"` > > 改为 ` "miniprogramNpmDistDir": "./"` 4. 点击"微信开发者"中的"工具"下的"构建npm",等待构建完成(等待构建完成之后,根目录会多一个 miniprogram_npm 文件夹),刷新页面 第三方包已经引入成功了,接下来就是使用组件了 1. 引入组件 - 在 "页面.json" 中引入 ```js "usingComponents": { "goods-card": "/components/goods-card/goods-card", "van-divider": "@vant/weapp/divider/index" } } ``` 2. 页面中写组件的标签 ```js 没有更多了 ``` #### 展示"该分类下无商品"内容 页面的5中状态(思想) ```js data: { ...... status: 'more' // 页面发请求的状态,总共有以下几个值: // 'more'更多(允许触底发请求) // 'no-more'没有更多(展示刚刚引入 van-divider) // 'empty'没有商品 (目前没有展示) // 'loading'加载中(不处理,request封装处理过) // 'error'错误(不处理) } ``` 状态默认是"more" 发请求,在返回的 total 和 goodsList 数组中商品的数量一致的时候,此时状态应该是 "no-more" 发请求,返回的 total 是0的情况下,状态应该是 "empty" 现在要做的事情是,在"empty"状态下,展示页面,要使用组件 ```json "usingComponents": { ... "van-empty": "@vant/weapp/empty/index", "van-button": "@vant/weapp/button/index" } ``` 页面结构 ```html 正常展示即可 查看其他商品 ``` ## 个人中心 CV过来个人中心和登陆(去资料中的todo),这里需要注意的是,个人中心的自定义导航,如何设置,在"psersonal.json"文件中设置 ```json { "usingComponents": {}, "navigationStyle": "custom" -----> 自定义导航 } ``` 点击个人中心的头像跳转登陆页 ### 登陆思路 ```js 之前的做法 - 废弃了(了解即可) wx.getUserProfile({ desc: '获取个人信息', success(res) { console.log(res) } }) ``` 废弃原因是因为有些小程序恶意获取用户的头像昵称,废弃了这个接口之后,微信提供了新的方式获取 「头像昵称填写能力」: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html 知道有这么回事就行了,目前研究新的登陆 ![](images/04.jpg) ### 登陆流程 1. 调用 `wx.login()` 获取code > code是临时标识,只有5分钟内有效 ```js // 点击登陆按钮回调 getUserProfile() { wx.login({ success: (result) => { this.login(result.code) }, }); } ``` 2. 拿着code调用后端接口,获取token,存储token api准备 ```js // 登陆获取token接口 - /mall-api/weixin/wxLogin/{code} export const reqLogin = (code) => { return request({ url: `/mall-api/weixin/wxLogin/${code}` }) } ``` js方法 ```js async login(code) { if (!code) { wx.showToast({ title: '登陆失败', icon: 'error' }); return } try { let result = await reqLogin(code) // 存起token // key 允许存储的最大数据长度为 1MB,所有数据存储上限为 10MB。 wx.setStorageSync("TOKEN", result.token); // 登陆完事了,需要获取个人用户信息 this.getUserInfo(result.token) } catch (error) { console.error(error) } }, ``` > wx.setStorageSync(key, value) > > 每个项最大1M,所有的项最大10M(以用户为维度) 3. 获取个人信息 api准备 ```js // 获取用户信息 - /mall-api/weixin/getuserInfo export const reqUserInfo = () => { return request({ url: `/mall-api/weixin/getuserInfo` }) } ``` js方法 ```js // 获取个人信息 - 注意,请求头中要携带token async getUserInfo() { try { let result = await reqUserInfo() wx.setStorageSync("USERINFO", result); // 这里可以直接存对象 wx.navigateBack(); // 回退上一页 } catch (error) { console.error(error) } } ``` > 需要在请求头中携带token,封装request函数中 > > ```js > // 携带token > let token = wx.getStorageSync("TOKEN"); > if (token) { > header.token = token; > } > ``` 4. 获取到个人信息之后,存储,跳转回"个人中心"页面 > 跳转到tabbar页面,关闭所有非tabbar页面 > > ```js > wx.switchTab({ > url: '/pages/personal/personal' > }); > ``` ## 头像编辑界面 目的:这个界面是为了解决用户头像不展示问题(不展示是由于清除文件缓存引起的) 界面从哪里进入? -- 获取完个人信息之后进入,登陆状态下点击头像可以编辑(两个入口) 界面怎么写? 参考连接:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html 搭建静态: /pages/user-edit/user-edit ```html 昵称 ``` ```css .user-edit { padding-top: 200rpx; width: 100%; height: 100%; background-color: #eee; } .avatar-wrapper { width: 200rpx; height: 200rpx; margin-bottom: 200rpx; padding: 0; } .avatar { width: 200rpx; height: 200rpx; } .nickname-wrapper { display: flex; border-top: 0.5rpx solid #ccc; border-bottom: 0.5rpx solid #ccc; padding: 20rpx 0; background-color: #fff; } .nickname-wrapper .label { padding: 0 40rpx; } .operate { margin-top: 80rpx; display: flex; justify-content: space-evenly; } .operate button { margin: 0; } ``` ```js import { reqEditUser } from '@/api/index' const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0' Page({ data: { headimgurl: defaultAvatarUrl, nickname: '' }, onChooseAvatar(e) { console.log(e.detail) const { avatarUrl } = e.detail this.setData({ headimgurl: avatarUrl, }) }, }) ``` #### 回显数据 ```js // 初始化数据展示(回显个人信息数据) onLoad() { let userinfo = wx.getStorageSync("USERINFO"); if (userinfo && userinfo.nickname) { this.setData({ headimgurl: userinfo.headimgurl, nickname: userinfo.nickname }) } } ``` #### 编辑数据 点击保存调用接口保存更新数据,点击取消回到上一个页面 ```js onCancel() { wx.navigateBack(); }, async onSave() { const { headimgurl, nickname } = this.data try { await reqEditUser({ headimgurl, nickname }) wx.showToast({ title: '保存成功', icon: 'success', duration: 1500, success: (result)=>{ wx.navigateBack(); // 回退上一页 }, }); wx.setStorageSync("USERINFO", { nickname, headimgurl }); } catch (error) { } }, ``` ```html ...... 取消 保存 ``` 注意: 1. button 组件添加 `open-type="chooseAvatar"` 属性点击才能展示出选择头像的弹框 2. input 组件添加 `type="nickname"` 属性才能弹出选择昵称的弹框 这里有一个bug,选择完自己的昵称之后没有双向数据绑定(放一放) 问题:因为 使用的是 `van-field` 代替了原生了 `input`,此时点击这个 input 弹框还会弹出来,但是选择"微信昵称"的时候,得不到最新写入的值 如何解决? 把 `van-field` 组件替换回原生的 `input` 就 ok 了 #### 登陆后立马跳转编辑界面 为什么这么做? 因为小程序新版的获取头像和昵称,头像是灰色默认的,昵称是"微信用户",所以要直接跳转到编辑界面,让用户自己存一个头像和昵称 注意问题: 从个人信息点击编辑进入该页面只跳转了一次 从个人信息点击"登陆",跳转登录页,登陆页点击"登陆"按钮跳转该页面,跳转了两次 目前该页面后退只后退了一次,如何解决后退一次? 将跳转登录页不计入历史记录即可,从login跳转到 user-edit 页面使用 `redirectTo` 即可 ```js // redirectTo关闭当前页面,跳转到下一个页面(当前页面就不在历史记录中了) wx.redirectTo({ url: '/pages/user-edit/user-edit' }); ``` 总结:跳转页面的API ```js wx.switchTab 跳转到tabbar页面 wx.redirectTo 关闭当前页面,跳转下一个页面,当前页面不在历史记录中 wx.navigateTo 跳转页面(有历史记录) wx.navigateBack({ delta: 1 }) 回退页面,没有参数默认是1 wx.reLaunch 关闭所有的页面,打开到应用内的某个页面 ``` ## 商品详情页 应该去写购物车页面,因为购物车页面中没有商品,所以先写商品详情页,添加购物车。 1. 静态搭建 直接粘贴详情页过来,把结构整明白即可 注意: * 详情页是展示商品的详细信息,需要发请求拿当前商品的详细信息,所以需要商品的 id,id点击的时候传过来,query传参(在goods-card组件的中,把view标签改成navigator标签加url) ```html ``` 接参,在详情页中的 onLoad 中接参数 * 样式不对,因为我们这里没有 flex 这个类名 在 app.wxss 这个文件中加上 flex 这个类名即可 ```css .flex { display: flex; } ``` 2. 初始化数据展示 api准备 页面初始化得到数据,渲染数据 3. 交互 "加入购物车" - 弹出弹框,有选择数量 "立即购买" - 弹出弹框,没有选择数量,数量是1 使用两个变量来控制,`isShowSheet: false` 控制弹框显示 `isShowCount: false` 控制弹框数量显示 给按钮绑定点击事件,修改这两个数据让弹框展示,同时加上关闭弹框的回调 ```js onClose() { // 隐藏弹框,重置数据 this.setData({ isShowSheet: false, isShowCount: false, }) }, // 加入购物车弹框 addCart() { this.setData({ isShowSheet: true, isShowCount: true }) }, // 立即购买弹框 addBuy() { this.setData({ isShowSheet: true, isShowCount: false }) }, ``` > 注意:弹框中显示的内容也是从 goodsDetail 中获取的 剩下就是收集数据,调用接口(加入购物车调用接口)或跳转页面(立即购买跳页面) 4. 收集数据,调用接口(调用添加购物车接口) api准备 - 目的是为了让我们知道传给后端的参数 ```js export const reqAddCart = (goodsId, count, data) => { return request({ url: `/mall-api/cart/addToCart/${goodsId}/${count}`, data }) } ``` 然后收集后端需要的参数,调用接口 ```js // 商品数量修改回调 onChange(e) { this.setData({ count: e.detail // 修改count数量 }) }, // 点击保存 async onSave() { if (this.data.isShowCount) { // 展示count数量是去购物车的 // 收集的数据 const { goodsId, count, blessing } = this.data console.log(goodsId, count, blessing) try { await reqAddCart(goodsId, count, { blessing }) wx.showToast({ title: '加入购物车成功', icon: 'success' }); // 关闭弹窗 this.onClose() } catch (error) { console.log(error) } } else { // 去立即购买页面(放一放) } }, ``` > 注意: > > 收集 数量的时候使用的是 `van-stepper` 这不是表单元素,不能用双向绑定 `model:value="{{ count }}" ` 得自己写事件 点击去购物车查看,跳转购物车 > 注意: > > `` > > navigator 标签跳转tabbar 页面的时候需要加 `open-type="switchTab"` ### 商品详情 - 添加购物车遇到的问题 当没有登录的情况下,调用接口是 后端返回的 code 是 208,此时需要让用户登录,这种需要携带 token 的接口可能有很多,我们在写代码的时候可能会忘了,那么就会报 208 的错误,如何解决? 在 request.js 封装中,对 code 是208的情况做兜底处理 ```js wx.request({ ... success: (result)=>{ // 请求成功回调 let res = result.data // 是响应体 if (res.code == 200) { resolve(res.data) } else if (res.code == 208) { // 代表未登录 // 让用户跳转登录 // 对调用接口未登录的统一处理(被动处理-兜底方案) // 对话框提示 wx.showModal({ title: '警告', content: '未登录,请前往登录', showCancel: true, cancelText: '取消', cancelColor: '#000000', confirmText: '确定', confirmColor: '#3CC51F', success: (result) => { if(result.confirm){ wx.navigateTo({ url: '/pages/login/login' }); } }, }); else { ... } ``` ## 购物车页面 1. 去购物车页面看购物车列表数据,静态搭建,把购物车粘贴过来,了解页面结构后,问:购物车页面有几种状态? 答:无商品、有商品、未登录(未登录的请况下,不能看到购物车中的内容,看看能不能用到兜底策略) 2. 初始化数据展示 api 准备 - 获取列表的api ```js // 获取购物车列表 - /mall-api/cart/getCartList export const reqCartList = () => { return request({ url: `/mall-api/cart/getCartList` }) } ``` 页面初始化的时候调用api展示数据即可 > 注意:未登录是由兜底策略,登录之后还应该返回到购物车页面,此时应该重新发请求,拿数据,请求应该放在 onShow 中 ```js async getCartList() { try { let result = await reqCartList() // 算是否全选 let isCheckAll = result.every(item => item.isChecked) && result.length > 0 // 总金额(【选中的】*【数量】*【金额】) let totalAmount = result.reduce((prev, item) => prev + item.isChecked * item.price * item.count, 0) // 保存数据 this.setData({ cartList: result, isCheckAll, totalAmount }) } catch (error) { console.error(error) } }, onShow() { // 这里需要把请求放在onShow,因为有可能未登陆,等登录要去登录,登录完成回来之后要重新获取数据 this.getCartList() } ``` 拿到数据之后展示数据 * 直接展示 - 循环展示 * 计算展示 - 需要计算展示的内容(全选状态、总价) 3. 交互 * 单选交互 请求需要 goodsId 和 选中状态(0和1) > 注意: > > 1. dataset 拿到的数据都是小写 > 2. 给后端的选中状态不是布尔值而是 0 和 1 * 数量修改交互 > 注意: > > 1. 传给后端的是差值,和添加购物车是一个接口 > 2. 输入的时候,频繁触发,把change事件拆成了 blur、plus、minus 单独处理,处理完成后再统一调用接口 * 删除交互 > 注意: > > 双重校验 * 全选交互 直接调用接口给后端传 0 和 1 * 结算跳转(后续再说) ```js // 单选回调 - 单选(两个参数id和选中状态) async checkChange(e) { // 规则: 所有dataset中的数据,在标签上绑定的是大写,在js中获取的时候获取的是小写 let goodsId = e.currentTarget.dataset.goodsid // 这里一定要小写 let isChecked = e.detail ? 1 : 0 // 选中状态(接口需要的是0和1) try { await reqCheckCart(goodsId, isChecked) // 刷新数据 this.getCartList() } catch (error) { console.error(error) } }, // 对于新增数量的修改一定要小心 plusHandler(e) { // oldcount = e.currentTarget.dataset.oldcount // 之前的值 e.detail = Number(e.currentTarget.dataset.oldcount) + 1 // 之前的值 + 1(手动+1) this.changeCount(e) }, minusHandler(e) { e.detail = Number(e.currentTarget.dataset.oldcount) - 1 // 之前的值 - 1(手动-1) this.changeCount(e) }, blurHandler(e) { e.detail = e.detail.value this.changeCount(e) }, // 数量修改 - 需要两个参数 goodsId, count async changeCount(e) { console.log('修改数量', e) if (e.detail < 1) { // 当前输入的值不能小于1 return } let goodsId = e.currentTarget.dataset.goodsid // 这里一定要小写 let oldcount = e.currentTarget.dataset.oldcount // 这里一定要小写 let count = e.detail - oldcount // 后端要的是差值 if (count == 0) { return } try { await reqAddCart(goodsId, count) // 刷新数据 this.getCartList() } catch (error) { console.error(error) } }, // 删除商品 - 双重校验 async deleteCart(e) { console.log('删除', e) let res = await wx.showModal({ title: '警告', content: '确认要删除该商品吗?' }); if (res.confirm) { let goodsId = e.currentTarget.dataset.goodsid try { await reqDeleteCart(goodsId) // 刷新数据 this.getCartList() } catch (error) { console.error(error) } } }, // 全选、全不选 async changeCheckAll(e) { console.log('全选、全不选', e) try { await reqChangeCheckAll(e.detail ? 1 : 0) // 刷新数据 this.getCartList() } catch (error) { console.error(error) } }, ``` ## 订单详情页 进入商品详情页有两个入口: 1. 购物车点击"去结算"进入 2. 在商品详情页点击"立即购买"进入 两个入口进入该页面调用的接口不一样,所以处理的逻辑也不一样 ![](images/04.png) 所以在初始化数据的时候,需要做判断,根据来源不同,调用不通接口 书写页面步骤: 1. 静态搭建 直接粘贴过来,需要考虑的问题就是从哪进入该页面 1. 从购物车过来 ```js toOrderDetail() { wx.navigateTo({ url: '/pages/order-detail/detail' }); }, ``` 2. 从商品详情过来 从商品详情过来的时候,需要携带 goodsId(商品ID) 和 blessing(祝福语) ```js async onSave() { // 收集的数据 const { goodsId, count, blessing } = this.data if (this.data.isShowCount) { // 去购物车的 ... } else { // "立即购买" 跳转订单详情 if (!blessing) { // 没有祝福语,不让跳转 wx.showToast({ title: '请填写祝福语', icon: 'error', }); return } wx.navigateTo({ url: `/pages/order-detail/detail?goodsId=${goodsId}&blessing=${blessing}` }); } }, ``` > 注意: > > 在 `detail.json` 文件中配置 `"navigationBarTitleText": "订单详情",` 当前页面的顶部导航标题文本发生改变 接下来该初始化数据展示,先处理购物车过来的逻辑,后续再处理立即购买过来的逻辑 2. 初始化展示数据 根据进入的入口不通,初始化数据展示调用的接口也不同,分为两套逻辑 1. 购物车进入初始化数据展示 调用两个接口,获取交易信息 和 获取订单地址 准备这两个接口的api ```js // ------------------ // 获取订单地址 - /mall-api/userAddress/getOrderAddress export const reqOrderAddress = () => { return request({ url: `/mall-api/userAddress/getOrderAddress` }) } // ------------------ // 确认下单 - 获取交易信息 - /mall-api/order/trade export const reqTradeInfo = () => { return request({ url: `/mall-api/order/trade` }) } ``` 页面初始化数据调用接口,拿到数据进行展示 ```js async getTradeInfo() { try { let result = await reqTradeInfo() console.log('购物车交易信息', result) this.setData({ totalAmount: result.totalAmount, cartList: result.cartVoList }) } catch (error) { console.error(error) } }, async getOrderAddress() { try { let result = await reqOrderAddress() console.log('地址信息', result) this.setData({ addressInfo: result }) } catch (error) { console.error(error) } }, // 接参 - 判断是从哪个页面过来 onLoad(options) { const { goodsId, blessing } = options if (goodsId) { // 从商品详情页过来(放一放) } else { // 从购物车过来 this.getTradeInfo() this.getOrderAddress() } }, ``` 2. 商品详情"立即购买"进入初始化数据展示 调用两个接口,地址接口 和 立即购买接口,此时地址接口已经有了,只需要准备立即购买接口 api 准备 ```js // 立即购买 - /mall-api/order/buy/{goodsId} export const reqOrderBuy = (goodsId, data = {}) => { // data -> { blessing: 'xxx' } return request({ url: `/mall-api/order/buy/${goodsId}`, data }) } ``` 初始化数据调用接口 ```js // 立即购买 - 初始化数据 async getOrderBuy(goodsId, blessing) { try { let result = await reqOrderBuy(goodsId, { blessing }) console.log('立即购买', result) this.setData({ cartList: result.cartVoList, totalAmount: result.totalAmount }) } catch (error) { console.error(error) } }, // 接参 - 判断是从哪个页面过来 onLoad(options) { const { goodsId, blessing } = options if (goodsId) { // 从商品详情页过来(放一放) this.getOrderBuy(goodsId, blessing) } else { // 从购物车过来 this.getTradeInfo() } // 不管从哪个页面进入都需要调用获取地址的接口 this.getOrderAddress() }, ``` 3. 交互 * 收集数据交互 收集数据收集4个数据 ```js buyName: '', // 购买人名称 buyPhone: null, // 购买人手机号 deliveryDate: '请选择配送时间', // 期望送达日期 remarks: '', // 客户备注 ``` 这里只有 deliveryDate 是需要注意的,其他的使用 `model:value` 双向数据绑定收集 收集 `deliveryDate` 需要知道 `van-popup` 组件弹框,还需要知道 `van-datetime-picker` 组件 `van-popup` 组件,使用第三方包 moment 转时间戳 ```html show属性是布尔值用来控制弹框的显示 bind:close="onClose" 点击蒙层位置,在回调中让控制弹框显示的布尔值变成false ``` `van-datetime-picker` 组件 ```html ``` moment 使用 ```js 1. 安装 npm i moment 2. 去除 app.json 中的 style: v2 3. 添加project.config.json 中的配置项 "packNpmManually": true, "packNpmRelationList": [ { "packageJsonPath": "./package.json", "miniprogramNpmDistDir": "./" } ] 4. 小程序"工具" -> "构建npm" 这里注意,步骤2和步骤3已经做过了,之一只需要步骤1和步骤4 注意: 如果构建失败,从小程序中删除该项目从新打开,再构建(之前构建成功,不是配置问题,就是小程序编译器的问题) ``` > 获取时间戳两种方式: > > new Date().getTime() > > Date.now() * 地址选择的交互(一整套逻辑 - 单独说) ## 地址列表 入口:订单详情页点击选择地址的时候可以进入,个人中心界面点击"地址管理"按钮也可以进入地址列表 1. 静态搭建 直接粘贴过来,了解页面结构,分为三部分:1. 地址列表展示 2. 没有地址列表展示 3. 新增地址列表按钮 2. 初始化数据展示 api准备 - 准备进入页面之后,请求地址列表的接口 ```js // 地址列表 - /mall-api/userAddress/findUserAddress export const reqAddressList = () => { return request({ url: `/mall-api/userAddress/findUserAddress` }) } ``` 在进入页面之后,请求地址列表的接口,拿到数据进行循环展示 ```js async getAddressList() { try { let result = await reqAddressList() console.log('地址列表 -> ', result) this.setData({ addressList: result }) } catch (error) { console.error(error) } }, onLoad(options) { this.getAddressList() }, ``` 3. 交互 1. 新增 - 跳转编辑页面 #### 静态搭建 把页面粘贴过来,在地址列表页,点击新增地址,跳转到编辑界面 熟悉页面结构 #### 交互 收集数据 - api准备,看一下后端需要哪些数据,然后在点击"保存"的时候,调用api传给后端即可 api准备 ```js export const reqAddressAdd = (data) => { return request({ url: `/mall-api/userAddress/save`, method: "post", data }) } ``` 收集数据 这里的所有的数据除了 "地址区域" 和 "是否默认" 需要咱们自己手动处理,其他的都直接双向绑定了 "地址区域" 用的 `picker` ```html 需要自己处理选择之后的回调 ``` "是否默认" 用的 `switch` ```html 需要自己处理选择之后的回调 ``` 点击"保存"按钮的时候,可以校验数据(非空和正则 - 这里我们没做),调用接口,保存 保存成功之后跳转回上一个列表页,此时需要刷新数据,所以列表页获取数据的方法应该在 `onShow` 中调用 2. 编辑 - 回显数据 点击列表中的编辑按钮,跳转编辑界面,回显数据(注意:需要把id传给编辑界面),准备获取地址详情的api,进入编辑界面调用 api准备 ```js export const reqAddressInfo = (id) => { return request({ url: `/mall-api/userAddress/${id}`, }) } ``` 进入页面调用 ```js // 回显数据 async getAddressInfo(addressId) { try { let result = await reqAddressInfo(addressId) this.setData({ ...result, region: `${result.provinceName}/${result.cityName}/${result.districtName}` }) } catch (error) { console.error(error) } }, onLoad(options) { let addressId = options.addressId if (addressId) { this.getAddressInfo(addressId) } }, ``` 准备更新api ```js export const reqAddressUpdate = (data) => { return request({ url: `/mall-api/userAddress/update`, method: 'post', data }) } ``` 收集数据走之前的逻辑,点击"保存"此时应该调用 更新的接口 ```js async onSave() { // 可以在保存之前做校验(例如: 非空校验 和 正则校验) try { if (this.data.id) { // 编辑 await reqAddressUpdate(this.data) } else { // 新增 await reqAddressAdd(this.data) } } ... } ``` 3. 删除 - 双重校验 api准备 ```js export const reqAddressDelete = (id) => { return request({ url: `/mall-api/userAddress/delete/${id}`, }) } ``` 点击删除按钮双重校验之后,调用接口 ```js async toDelete(e) { let addressId = e.currentTarget.dataset.id // 双重校验 let res = await wx.showModal({ title: '警告', content: '确认要删除该地址吗?', }); if (res.confirm) { // 确认 try { await reqAddressDelete(addressId) // 提示 wx.showToast({ title: '删除成功', icon: 'success', }); // 刷新数据 this.getAddressList() } catch (error) { console.error(error) } } }, ``` 4. 选择订单地址功能 当选择地址之后,跳转到上一个页面,如果上一个页面是"订单详情",要刷新地址信息的 api准备 ```js export const reqAddressSelect = (id) => { return request({ url: `/mall-api/userAddress/selectAddress/${id}`, }) } ``` 点击列表中每一个的时候,调用接口,返回上一页,让上一页重新获取地址数据,让上一页的地址数据得到更新 ```js async toSelect(e) { let addressId = e.currentTarget.dataset.id try { await reqAddressSelect(addressId) // 回退上一页 wx.navigateBack(); } catch (error) { console.error(error) } }, ``` 同时把订单详情页的调用获取地址信息的接口放到 onShow 当中 > 问题: > > 点击编辑、删除按钮的时候也是在点击当前这个地址的,会触发两个回调 > > 这里应该将 编辑、删除 按钮绑定事件的 bind 换成 catch > > catch 阻止事件冒泡 这里地址列表已经完成,我们是从订单详情页做到了地址列表,现在还返回订单详情页,做结算,那么结算涉及到支付(支付也是面试的重点) ## 支付流程 地址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_4&index=3 ![](images/06.png) 流程: 1. 用户下单,发请求给商家服务器(这一步前端做),商家服务器生成订单,然后给腾讯服务发请求,生成预付单,生成腾讯生成预付单的目的是为了知道这个订单要收多少钱,腾讯服务知道收多钱之后反馈给商家服务器,生成支付签名信息返回给小程序 2. 小程序端接收到商家返回的支付签名信息,调用 `wx.requestPayment()` 发起微信支付(这一步前端做) 3. 发起之后要去腾讯服务鉴权,鉴定有没有实名、有没有绑定银行卡,鉴权成功之后告诉小程序可以调起微信支付,此时要去弹出密码输入框(这一步是调用完 `requestPayment`)小程序自己和腾讯服务的交互 4. 小程序弹出密码弹框,用户输入密码,授权付款,钱在腾讯服务上,所以授权要发送给腾讯服务,腾讯服务接到授权后,会把用户账号中的款扣了,把钱划给商家,返回支付成功告诉用户,提示"支付成功",这里的支付成功可以理解为"扣款成功"(这时会弹一个弹框,微信弹的) 在这个步骤的同时,会异步告诉商家服务,该用户已经付款,可以发货 5. 点击微信弹出弹框确认之后,要回到商家的小程序,小程序也要提示用户"订单支付成功",所需小程序发请求给商家服务,商家服务接到请求之后,找腾讯服务确认用户是否支付成功,确认之后,返回给用户支付成功的数据,小程序展示"订单支付成功"页面 步骤: 1. 调用创建订单接口,创建出订单,返回 `orderId` 2. 拿着 orderId 获取支付信息 3. 拿着支付信息调用 `wx.requestPayment()` 去支付 只有支付成功之后,才会走 `wx.requestPayment()` 的 success 回调 4. 在支付成功之后,调用接口查询订单状态(已支付),跳转支付成功页 ## 订单列表 点击个人信息页面"商品订单"跳转订单列表 静态搭建,去粘贴过来,报错,因为接口没有,自己写一个接口页面就好了,其他的内容都写好了 ## 分包 https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html 什么是分包? 小程序上线代码应该是一个包,可以把这个包拆分成多个,包含一个主包和多个子包,每个包的大小不能超过2M,tabbar 页面只能放在主包 为什么要分包? 1.包体超过2M 2. 优化首次使用小程序加载时间 分包类型: 普通分包、独立分包、分包预下载 ### 普通分包 分包原则: tabbar 页面只能放在主包 子包不能嵌套 子包只能使用当前自己子包和主包的内容(内容包含js、模板、资源) 步骤: 1. 将文件目录改成子包目录 ![](images/07.png) 2. 修改 "app.json" 文件 删除主包 "pages" 下的页面路径 配置 "subpackage" ![](images/08.png) 3. 改完之后测试一下,因为页面的路径发生该改变,所以页面中引用内容的路径也需要改变 > 所有涉及到分包的内容都需要去测试以下,防止出错 如何证明我们包分成功了? ![](images/09.png) ### 独立分包 场景: QQ音乐播放音乐的时候,把播放界面分享出去,此时别人点击音乐播放的时候,直接进入播放页面,此时这个播放界面就是独立分包,不需要依赖主包就可以播放音乐 限制: 独立分包不能使用主包的任何资源(包含js、静态、模板) 如何做? 在分包中配置 `"independent": true` 即可 ![](images/10.png) ### 预下载分包 目的:用来提升用户体验的,预先加载其他分包 如何做? 通过在 app.json 配置 `preloadRule` 字段来实现 > 参考: https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/preload.html # 谷粒微课 ## 创建项目 ```js npx degit dcloudio/uni-preset-vue#vite-ts uni_course ``` 使用的是vite + TS创建的项目,没有依赖 ```js cd uni_course npm i ``` 安装好依赖就可以启动项目了,在项目启动之前先了解项目目录结构 ### 目录结构 需要注意的: `manifest.json` 文件中要配置 微信小程序的 `appid` ![](images/001.png) `main.ts` ```ts import { createSSRApp } from "vue"; // 引入的是 vue 服务端渲染 import App from "./App.vue"; // 根组件 export function createApp() { const app = createSSRApp(App); // 创建一个app实例,把App根组件传进去 return { app, // 把实例抛出去 }; } ``` > 客户端渲染:发请求的时候,拿回来的 html 页面只有一个 `
` 元素,页面中所有的内容都是由 请求回来的css 和 js 生成的。 > > 服务端渲染:发请求的时候,拿回来的 html 页面是一个完整的 html 页面 > > 服务端渲染最大的优点在于 SEO优化 友好,SEO优化是搜索引擎优化,优化的越好,搜索引擎搜出来的时候排名越靠前(不掏钱的前提下) `App.vue` 发现`App.vue` 根组件中只有 script 和 style,没有 template,为什么? `App.vue` 的 style 相当于是 小程序中的 `app.wxss` `App.vue` 的 script 相当于是 小程序中的 `app.js` `pages.json` 相当于是小程序的 `app.json`,同时把每个页面的 json,也放在了 pages.json 中 ### 启动项目 ```js npm run dev:mp-weixin ``` 在 package.json 中发现启动微信小程序是该指令,执行之后,创建了一个 dist/dev 目录,在这个目录中由 `mp-weixin` 这个文件夹,这就是将 vue 编译成了小程序代码,使用 "微信开发者工具" 打开该项目,就可以看到了 当执行 `npm run dev:mp-weixin` 时候,`dist/dev/mp-weixin` 这个小程序代码每次都会清空掉重新构建 ### 配置项目导航 去 pages.json 中配置 ```js "globalStyle": { "navigationBarTextStyle": "white", "navigationBarTitleText": "谷粒课堂", "navigationBarBackgroundColor": "#00cc99" } ``` 同时需要注意页面的配置项会覆盖全局的配置项,需要改页面的配置项 把首页的内容清空了 问题: 下载包下载不下来: ![](images/002.png) > 很多同学这里安装不上,使用课件中准备好的包即可 > > 但是使用 `npm i` 也是安装不上依赖,可以使用 cnpm > > 如果没有 cnpm 在命令行窗口执行 `npm i cnpm -g`,执行完之后,在全局可以使用 cnpm 指令 > > 然后使用 `cnpm i` 尝试安装依赖 > > 每次安装依赖失败之后,要把 `node_modules` 删掉之后重新安装依赖 ## 首页 在uni中即可以写 div 元素,也可以写原生的小程序标签 ### 轮播 静态搭建、初始化数据展示 注意: 1. 使用 less 来写css,没有安装 less会报错,在项命令行执行 `npm i less` 安装 less,安装好后重新启动项目,书写的 less 就好使了 2. 没有轮播的接口,使用mock数据,将项目中的 `src/common/mock/home.ts` 复制到自己的项目相同路径下,使用 `home.ts` 中的数据进行渲染轮播 ```vue ``` ### nav 静态搭建、mock数据 ```vue {{ item.name }} import { bannerCateList, hotCateList } from '@/common/mock/home' const navList = ref(hotCateList) ``` ### v-card、v-card-list 静态封装 #### v-card ```vue ``` #### v-card-list ```vue ``` 接下来该发请求,调接口,拿数据,展示... 问:我们这里由发请求的工具吗?自己封装一个 之前我们使用的是函数的形式封装的(之前封装的拿过来把 `wx.request` 改成 `uni.request` ),没有用过实例的封装,所以这次写成实例封装的代码 实例封装的代码需要我们先看看 ES6 Class 的写法 ### 简单复习ES6 Class 代码参考 `static` 目录下的 `class复习.html` ### 封装请求实例 baseUrl: https://gmall-prod.atguigu.cn/skb 接口文档地址: https://www.yuque.com/xiumubai/fe/gervhh ```ts interface OptionsModel { url: string, data?: any, method?: 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT' } class Request { // 函数简写形式在原型上 // private 私有的,只能在当前类内容调用该方法,外部无法调用 private api(options: OptionsModel) { return new Promise((resolve, reject) => { uni.request({ url: options.url, data: options.data, method: options.method || 'GET', header: {'content-type':'application/json'}, dataType: 'json', responseType: 'text', success: (result)=>{ resolve(result) }, fail: (error) => { reject(error) }, complete: ()=>{} }); }) } get(options: OptionsModel) { return this.api({ ...options, method: "GET" }) } post(options: OptionsModel) { return this.api({ ...options, method: "POST" }) } put(options: OptionsModel) { return this.api({ ...options, method: "PUT" }) } delete(options: OptionsModel) { return this.api({ ...options, method: "DELETE" }) } } export default Request ``` 接下来要封装 api 接口,之前写法也是函数式的,现在用类来写 ### api类封装 - 封装 CourseRequest 类 ```ts import Request from "@/utils/request"; class CourseRequest extends Request{ // 等号形式,放在当前实例上 // getHomeData = () => {} // 简写形式,放在当前类的原型上 getHomeData() { return this.get({ url: '/api/edu/index' }) } } export default new CourseRequest() ``` 在 main.ts 中测试自己封装的实例是不是能用(注意这是测试代码,最终要删除的) ```ts import courseApi from "./api/course"; async function getData() { let result = await courseApi.getHomeData() console.log('返回首页的数据', result) } getData() ``` > 注意:api 是需要拼接 baseUrl 的 接下来应该去页面中调用接口拿数据 ### 给api函数加TS类型(加返回值的类型) ![](images/003.png) ![](images/004..png) ```ts export interface CourseModel { buyCount: number cover: string gmtCreate: string gmtModified: string id: string lessonNum: number price: number status: string subjectId: string subjectParentId: string teacherId: string title: string version: number viewCount: number } export interface TeacherModel { avatar: string career: string deleted: boolean gmtCreate: string gmtModified: string id: string intro: string level: number name: string sort: number } interface HomeModel { courseList: CourseModel[] teacherList: TeacherModel[] // 暂时写成空数组类型 } class CourseRequest extends Request{ // 等号形式,放在当前实例上 // getHomeData = () => {} // 简写形式,放在当前类的原型上 getHomeData() { return this.get({ url: '/api/edu/index' }) } } export default new CourseRequest() ``` ### 初始化页面调用接口拿数据 ```js const courseList = ref([]) const teacherList = ref([]) const getHomeData = async () => { try { let result = await courseApi.getHomeData() console.log('首页数据', result) courseList.value = result.courseList teacherList.value = result.teacherList } catch (error) { console.error(error) } } onLoad(() => { getHomeData() }) ``` 将数据传递给组件进行展示 ```html ``` ### uni-ui 组件库使用 官网教程: https://uniapp.dcloud.net.cn/component/uniui/quickstart.html 在 `v-card-list` 组件中,用到了一个 icon ,这个 icon 使用 `uni-ui` 组件库 1. 安装 sass 和 sass-loader ```js npm i sass sass-loader@10.1.1 -D ``` > 如果安装失败的话,最后加一个 -f 强制安装 2. 安装 `uni-ui` 包 ```js npm i @dcloudio/uni-ui ``` 3. 配置 easycom 在 pages.json 中配置 easycom ```json { "easycom": { "autoscan": true, "custom": { // uni-ui 规则如下配置 "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue" } }, pages:[] } ``` 接下来就可以在模板中使用了 ### back-top组件封装-uni事件绑定 需求: 1. 点击该组件要回退到页面的顶端 绑定点击事件即可 ```js const toTop = () => { uni.pageScrollTo({ // wx的API scrollTop: 0, // 页面到顶部的距离 duration: 300 // 页面到顶部的时间 }) } ``` 2. 当页面滚动到下面的时候才显示该按钮 这里可以在首页直接做,但是要在这个组件中控制显示和隐藏,将 uni 的事件绑定和触发 * 找到uni,绑定事件,留下回调,接收参数 ```js uni.$on('xxxxHandler', (arg) => {}) ``` * 找到uni,触发事件,传递参数 ```js uni.$emit('xxxxHandler', scrollTop) ``` ## tabbar 配置 在 pages.json 中配置 ```json "tabBar": { "color": "#333", "selectedColor": "#409eff", "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "/static/images/nav/home-off.png", "selectedIconPath": "/static/images/nav/home-on.png" }, { "pagePath": "pages/course/list/index", "text": "课程", "iconPath": "/static/images/nav/list-off.png", "selectedIconPath": "/static/images/nav/list-on.png" }, { "pagePath": "pages/personal/index", "text": "我的", "iconPath": "/static/images/nav/my-off.png", "selectedIconPath": "/static/images/nav/my-on.png" } ] }, ``` ## 课程列表页 #### 静态搭建(考察flex布局) #### 初始化数据展示 准备api ```tsx export interface CoursePageModel { current: number hasNext: boolean hasPrevious: boolean items: CourseModel[] // ------------- 该类型首页的时候写过 pages: number size: number total: number } class CourseRequest extends Request{ ...... getCourseList(page: number, limit: number) { return this.get({ url: `/api/edu/course/${page}/${limit}` }) } } ``` 初始化数据页面调用 api ```ts const courseList = ref([]) const page = ref(1) const limit = ref(10) const getCourseList = async () => { try { let result = await courseApi.getCourseList(page.value, limit.value) courseList.value = courseList.value.concat(result.items) } catch (error) { console.error(error) } } onLoad(() => { getCourseList() }) ``` #### 分页 分页的时候使用 status 状态来控制 页面的请求,总共有三种状态 `more`、`no-more`、`loading`,同时需要在请求前将 status 至为 `loading` 状态、请求完成后对比 total 和 数据数组的长度,来决定 status 是 `more` 还是 `no-more` ```ts // 页面状态 const status = ref<"more" | "no-more" | "loading">('more') // 触底回调的钩子 onReachBottom(() => { status.value == "more" && page.value++ && getCourseList() }) ...... let result = await courseApi.getCourseList(page.value, limit.value) courseList.value = courseList.value.concat(result.items) if (result.total == courseList.value.length) { status.value = 'no-more' } else { status.value = 'more' } ``` 使用 uni 组件 `uni-load-more` 展示没有更多数据 ```html ``` ## 个人中心、登录静态 要使用pinia存储我们的数据,考虑数据怎么放? token 存在 pinia 和 storage 中 个人信息存在 pinia 中 ### 使用pinia ```js npm i pinia@2.0.36 ``` > 注意版本号 `src/store/index.ts` ```ts import { createPinia } from 'pinia' const store = createPinia() export default store ``` `main.ts` 使用 pinia ```ts import store from '@/store'; app.use(store); ``` ### 登录 登录流程参照:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html 调用 `wx.login()` 获取 code(5分钟时效),拿着 code 调用登录接口,获取token 准备api - `src/api/user.ts` ```js class UserRequest extends Request{ // 登录 login(data: LoginParams) { return this.get({ url: `/api/ucenter/webChat/callback`, data }) } } ``` 准备 store - `src/store/user.ts` ```ts const useUserStore = defineStore('userStore', { state(): StateModel { return { // 存token token: '', // 存个人信息 userInfo: initUserInfo() } }, actions: { // 登录 async login (code: string) { try { let result = await userApi.login({ code }) console.log('登录获取token', result) this.token = result.token // token 存到store中 uni.setStorageSync("TOKEN", result.token); // token 存到 Storage 中 // token存在代表登录成功 // 跳转回个人中心页,还要获取个人信息 uni.switchTab({ url: '/pages/personal/index', }); } catch (error) { console.error(error) return Promise.reject(error) // 不确定外部需不需要错误的信息,直接抛 } }, } } ``` 在登录页点击 `登录`按钮调用 wx.login() 获取 code ,拿着 code 调用 action 发请求 ```js const loginHnadler = () => { // 获取code uni.login({ timeout:10000, success: async (result) => { console.log('code -> ', result) // 登录获取token await userStore.login(result.code) } }); } ``` ### 获取个人信息 api准备 ```ts // 个人信息 - 请求头携带token getUserInfo() { return this.get<{ item: UserInfoModel }>({ url: `/api/ucenter/member/auth/getLoginInfo`, }) } ``` 注意:获取个人信息需要请求头携带 token `src/utils/request.ts` ```js private api(options: OptionsModel): Promise { let header: any = {'content-type':'application/json'}; // 请求头携带token let token = uni.getStorageSync("TOKEN"); if (token) { header.token = token; } return new Promise(() => { uni.request({ ... header, ... }) }) } ``` 进入个人信息页面需要确认当前 store 中有没有个人信息,如果有则不获取,如果没有就获取 ```js 个人中心页面 onShow(() => { userStore.checkLogin() }) store 中 actions 加一个 checkLogin 方法 actions: { // 检测是否登录、是否包含个人信息 checkLogin() { // 如果有个人信息,不做操作 if (this.userInfo.id) { return } // 没有个人信息,看有没有token,有token直接获取个人信息 if (this.token) { this.getUserInfo() return } // 没有token跳转登录页,点击登录获取token uni.navigateTo({ url: '/pages/login/index', }); }, ... } ``` ### 退出登录 这里没有退出登录的接口,所以只需要清除个人信息 和 token 即可 ```js 退出登陆 const logout = () => { userStore.reset() } store 中 reset 方法 action: { reset() { // 清除个人信息 this.userInfo = initUserInfo() // 清除token this.token = "" uni.removeStorageSync("TOKEN"); } } ``` ## 商品详情页 ### 静态 直接粘贴现成的即可(todo),页面粘贴过来之后,把 `v-back-top` 组件的引入和事件处理了。 注意:详情页有两个入口,首页 v-card-list 中商品的点击,课程列表页面的中 课程点击,两个入口进入该页面的时候,发请求带了哪些参数要看一看,因为决定了跳转到该页面的时候要不要携带参数 经过查看发现进详情页调用了两个接口,都需要用到 课程ID(courseId),那么跳转页面的时候传过来 ### api准备 - 初始化数据展示 ```ts // 章节类型 export interface ChapterModel { children: ChapterChildModel[] id: string title: string } // 详情页返回类型 export interface CourseDetailModel { chapterList: ChapterModel[] course: CourseModel isBuy: boolean isCollect: boolean } // 评论类型 export interface CommentModel { avatar: string content: string courseId: string deleted: null gmtCreate: string gmtModified: string id: string memberId: string nickname: string teacherId: string } // 获取评论分页数据 export interface CommentPageModel { current: number hasNext: boolean hasPrevious: boolean items: CommentModel[] pages: number size: number total: number } // 获取课程详情 getCourseDetail(courseId: string) { return this.get({ url: `/api/edu/course/${courseId}` }) } // 获取评论列表 - /api/edu/comment/{page}/{limit}?courseId={courseId} -- courseId不是必传 getCommentList(page: number, limit: number, data: { courseId: string }) { return this.get({ url: `/api/edu/comment/${page}/${limit}`, data }) } ``` 在页面onLoad的时候,调用两个接口,获取数据进行展示即可 ### 交互 #### 收藏 点击收藏按钮,调用api收藏、再次点击取消收藏,调用api 准备api(收藏、取消收藏) ```js // 收藏 - /api/edu/courseCollect/auth/save/{courseId} -- post getSaveCollect(courseId: string) { return this.post({ url: `/api/edu/courseCollect/auth/save/${courseId}` }) } // 取消收藏 - /api/edu/courseCollect/auth/remove/{id} -- delete getCancelCollect(courseId: string) { return this.delete({ url: `/api/edu/courseCollect/auth/remove/${courseId}` }) } // 点击的回调 const changeCollect = async () => { try { // 当前是收藏调用取消收藏 if (isCollect.value) { await courseApi.getCancelCollect(courseId.value) uni.showToast({ title: '取消收藏成功', icon: 'success' }); } else { // 取消收藏调用收藏 await courseApi.getSaveCollect(courseId.value) uni.showToast({ title: '收藏成功', icon: 'success' }); } isCollect.value = !isCollect.value } catch (error) { console.error(error) } } ``` > 注意: > > 鉴权,这里我们没有写鉴权的代码,在 `慕尚花坊` 小程序写过鉴权,在request请求封装的时候进行的统一处理,下去自己写 #### 评论列表 点击"查看更多"跳转评论列表页,看一看发请求携带的参数,我们发请求也要携带,所以跳转页面的时候,看看需不需要带参数跳转 经过查看完整版项目,发现在评论列表页发请求需要携带 `courseId` 和 `teacherId` ,所以跳页面要带这两个参数 步骤: 1. 静态 直接粘贴 todo 中的页面(注意在pages.json中写上页面),然后给"查看全部"添加跳转路径 2. 准备api(获取评论列表的api - 这个api已经有了,在商品详情页进入的时候调用过,只不过少一个 query 参数 teacherId,加上即可) 页面初始化的时候调用api(复制商品详情页代码,稍作修改,把page和limit和tracherId加上) 展示数据(注意:粘贴过来详情页的代码,样式也要粘贴) 交互 评论,输入评论内容,调用评论接口,刷新页面数据 准备评论的api接口,输入完内容,点击发送的时候,调用api,发送成功后,给个提示,重置页码,重置数据,刷新页面数据,把评论的内容清空 点击返回上一页,刚刚评论的内容应该展示在商品的详情页,把商品详情页获取评论的方法在 `onShow` 中调用即可 #### 去学习 ```vue ``` #### 去购买 在课程详情页点击"点击购买"按钮,调用接口,拿到 `orderId` 传给 订单详情页(query),在订单详情页,发请求获取"订单信息",进行展示,勾选协议后,点击购买,拿着"订单信息"中的 orderNo 去获取交易信息,拿到交易信息调用 `wx.requestPayment()` 发起支付,支付的流程和之前的一样 (数白了,就是看已完成项目都先调用哪些接口了,我们也调用即可) ## 订单列表、收藏列表、全部名师 都是进入页面,发请求拿分页数据展示,比较简单(和课程列表页差不多),自己做 # 总结 登录 支付 分包、上线 如何创建一个小程序(包括 原生 和 uniapp ) 小程序的基本语法(wx:for、生命周期) 页面通信*** 1. navigateTo A页面跳转B页面,携带参数过去,使用query携带参数,在另一个页面中的onLoad的参数中可以拿到传过来的参数 ```js uni.navigateTo({ url: `/pages/course/index?courseId=${courseId}`, }); ``` 2. Storage 在 A 页面将数据存储到storage中,跳转到 B 页面再取出来 A页面 ```js wx.setStorageSync("courseId", courseId); ``` B 页面 ```js let courseId = wx.getStorageSync("courseId"); ``` 3. app 实例传参 在A页面将数据存储到 app 应用实例上,跳转到 B 页面再取出来 A 页面 ```js const app = getApp() app.courseId = courseId ``` B 页面 ```js const app = getApp() const courseId = app.courseId ``` 4. pubsub 1. 安装 不在界面上显示的第三包使用步骤(工具类的,例如: pubsub、moment、lodash...) ```js npm i pubsub-js ``` 直接 【工具】 -> 【构建npm】 即可 使用第三方包 2. 在接收数据的组件中找到 pubsub ,绑定事件(订阅消息),留下回调,接收参数 使用 index 接收 ```js Pubsub.subscribe('receiveData', (messagetype, arg) => { console.log(messagetype, arg) }) ``` 3. 在发送数据的组件中找到 pubsub,触发事件(发布消息),传递参数 使用 logs 页面发送数据 ```js Pubsub.publish('receiveData', '我爱你') ``` > 注意:这个传参用的是 新打开的页面(logs) 给 被盖住的页面(index) 传参 > > 如果是 被盖住的页面(index) 给 新打开的页面(logs) 去使用 pubsub 传参,注意执行时机,只要触发事件在绑定事件之后,就能接收到参数 5. eventChannel 事件通道 1. 被覆盖的页面(index) -> 被打开的页面传参(logs) 被覆盖的页面(index) ```js wx.navigateTo({ url: '/pages/logs/logs', success: (res) => { // res.eventChannel 事件通道 - 触发事件事件传递参数 res.eventChannel.emit('xxx', '你是个好人') } }) ``` 被打开的页面(logs) ```js onLoad: function (options) { // 事件通道 const eventChannel = this.getOpenerEventChannel() // 拿到事件通道 // 绑定事件,留下回调,接收参数 eventChannel.on('xxx', (arg) => { console.log('事件通道 logs 接参', arg) }) }, ``` 2. 被打开的页面传参(logs) -> 被覆盖的页面(index) 被打开的页面传参(logs) ```js onLoad: function (options) { // 5. 事件通道 const eventChannel = this.getOpenerEventChannel() // 拿到事件通道 eventChannel.emit('yyy', '我们不合适') }, ``` 被覆盖的页面(index) ```js wx.navigateTo({ url: '/pages/logs/logs', events: { yyy: (arg) => { console.log('index页面接收log参数', arg) } } }) ``` 6. uniapp - 事件总线 uni.$on() uni.$emit() uni.$once() uni.$off()