慕尚花坊项目笔记
一、项目初始化
重置app.js
中的代码
删除app.json
中pages
下的"pages/logs/logs"
路径,同时删除pages.logs
文件夹
删除app.json
中pages
下的"renderOptions"
以及"componentFrameword"
字段
重置app.wxss
中的代码
删除components
中的自定义组件
重置pages/index
文件夹下的index.js
、index.wxss
、index.html
以及index.json
文件
更新utils
下utils.js
的文件名为formatTime.js
二、自定义构建npm、集成sass
1、自定义构建npm
首先在
project.config.json
中配置miniprogramRoot
,指定小程序源码的目录然后配置
project.config.json
的setting.packNpmManually
为true
,开启自定义 node_modules 和 miniprogram_npm 位置的构建 npm 方式最后配置 project.config.json 的
setting.packNpmRelationList
项,指定packageJsonPath
和miniprogramNpmDistDir
的位置- packageJsonPath 表示 node_modules 源对应的 package.json相对地址
- miniprogramNpmDistDir 表示 node_modules 的构建结果目标地址
创建package.json并安装
vant
,然后进行npm 构建
,测试是否能够正常vant
构建成功
npm init -y
npm i @vant/weapp
注意:因为npm init -y
命令会将当前目录名作为package.json中name字段的默认值,而name字段不允许使用中文。 如果目录名包含中文,就会导致初始化失败,提示类似 “Invalid name” 的错误信息。
2、集成sass
在 project.config.json
文件中,修改 setting
下的 useCompilerPlugins
字段为 ["sass"]
,即可开启工具内置的 sass 编译插件。
注意:sass需要用双引号包裹,JSON 规定字符串必须用 双引号 " 包裹,单引号 ’ 或未闭合的引号会导致错误
三、使用vscode开发
在项目的根目录下创建
.vscode
文件夹,注意:文件夹名字前面带.
点在
.vscode
文件夹下,创建settings.json
,用来对安装的插件属性进行设置,具体属性设置从下面复制即可注意:
.vscode
文件夹下的settings.json
文件只对当前一个项目生效在【项目的根目录】下创建
.prettierrc
文件,进行Prettier
代码规则的配置,规则从下面复制为了让
Prettier
配置项在微信开发者工具生效,需要在微信开发者工具中也安装Prettier
扩展插件。
.vscode/settings.json
{
// 保存文件时是否自动格式化
"editor.formatOnSave": true,
// ---------------- 以下是 [ prettier ] 插件配置 ----------------
// 指定 javascript、wxss、scss、less、json、jsonc 等类型文件使用 prettier 进行格式化
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[wxss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// Prettier 的一个配置项,用于指定哪些文件类型需要使用 Prettier 进行格式化
"prettier.documentSelectors": ["**/*.wxml", "**/*.wxss", "**/*.wxs"],
// ---------------- 以下是 [ WXML - Language Service ] 插件配置 ----------------
// wxml 文件使用 prettier 进行格式化
"[wxml]": {
// "qiu8310.minapp-vscode" 是 WXML - Language Service 插件提供的配置项
// 此插件主要是针对小程序的 wxml 模板语言,可以自动补全所有的组件、组件属性、组件属性值等等
// 如果是 VsCode 需要开启这个配置
"editor.defaultFormatter": "qiu8310.minapp-vscode"
// 如果是微信小程序,需要开启这个配置,通过 esbenp.prettier-vscode 对代码进行格式化
// "editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 创建组件时使用的 css 后缀
"minapp-vscode.cssExtname": "scss", // 默认 wxss,支持 styl sass scss less css
// 指定 WXML 格式化工具
"minapp-vscode.wxmlFormatter": "prettier",
// 配置 prettier 代码规范
"minapp-vscode.prettier": {
"useTabs": false,
"tabWidth": 2,
"printWidth": 80
},
// ---------------- 以下是 [ 微信小程序助手-Y ] 插件配置 ----------------
// 新增、删除小程序页面时,是否自动同步 app.json pages 路径配置,默认为 false
"wechat-miniapp.sync.delete": true,
// 设置小程序页面 wxss 样式文件的扩展名
"wechat-miniapp.ext.style": "scss",
// ---------------- 其他配置项 ----------------
// 配置语言的文件关联,运行 .json 文件时写注释
// 但在 app.json 和 page.json 中无法使用
"files.associations": {
"*.json": "jsonc"
}
}
.prettierrc
{
"semi": false,
"singleQuote": true,
"useTabs": false,
"tabWidth": 2,
"printWidth": 180,
"trailingComma": "none",
"overrides": [
{
"files": "*.wxml",
"options": { "parser": "html" }
},
{
"files": "*.wxss",
"options": { "parser": "css" }
},
{
"files": "*.wxs",
"options": { "parser": "babel" }
}
]
}
配置项 | 配置项含义 |
---|---|
“semi”: false | 不要有分号 |
“singleQuote”: true | 使用单引号 |
“useTabs”: false | 缩进不使用 tab,使用空格 |
“tabWidth”: 2 | tab缩进为4个空格字符 |
“printWidth”: 80 | 一行的字符数,如果超过会进行换行,默认为80 |
“trailingComma”: “none” | 尾随逗号问题,设置为none 不显示 逗号 |
“overrides”: [] | overrides 解析器:默认情况下,Prettier 会根据文件文件拓展名推断要使用的解析器 |
四、通用模块封装
封装后可以极大简化API的调用
1、消息提示模块封装
封装思路:
创建一个
toast
方法对wx.showToast()
方法进行封装调用该方法时,传递对象作为参数
如果没有传递任何参数,设置一个空对象
{}
作为默认参数从对象中包含
title
、icon
、duration
、mask
参数,并给参数设置默认值
在需要显示弹出框的时候调用
toast
方法,并传入相关的参数,有两种参数方式:- 不传递参数,使用默认参值
- 传入部分参数,覆盖默认的参数
调用方式:
模块化的方式导入使用
import { toast } from './extendApi' toast() toast({ title: '数据加载失败....', mask: true })
将封装的模块挂载到
wx
全局对象身上wx.toast() wx.toast({ title: '数据加载失败....', mask: true })
模块化方式导入使用
挂载到wx全局对象身上
2、模态对话框封装
封装思路:
- 对
wx.showModal()
方法进行封装, 封装后的新方法叫modal
- 调用该方法时,传递对象作为参数,对象的参数同
wx.showModal()
参数一致 - 封装的
modal
方法的内部通过Promise
返回用户执行的操作(确定和取消,都通过 resolve 返回) - 在需要显示模态对话框的时候调用
modal
方法,并传入相关的参数,有三种调用方式:- 不传递参数,使用默认参数
- 传递参数,覆盖默认的参数
调用方式:
新封装的本地存储模块,我们依然希望有两种调用的方式:
- 模块化的方式导入使用
- 将封装的模块挂载到
wx
全局对象身上
挂载到wx上且传入新的参数案例:
3、封装本地存储API
export const setStorage = (key,value) => {
try{
wx.setStorageSync(key,value)
}catch(error){
console.error(`存储指定 ${key} 数据发生错误:`, err)
}
}
export const getStorage = (key) => {
try{
const value = wx.getStorageSync(key)
if(value){
return value
}
}catch(error){
console.error(`读取指定 ${key} 数据发生错误:`, err)
}
}
export const removeStorage = (key) => {
try {
wx.removeStorageSync(key)
} catch (error) {
console.error(`移除指定 ${key} 数据发生错误:`, error)
}
}
export const clearStorage = () => {
try {
wx.clearStorageSync()
} catch (error) {
console.error("清空本地存储时发生错误:", error);
}
}
import {setStorage,getStorage,removeStorage,clearStorage} from './utils/storage'
App({
async onShow(){
setStorage('name','tom')
setStorage('age',10)
const name = getStorage('name')
console.log(name);
removeStorage('name')
clearStorage()
}
})
4、封装异步存储API+优化代码
export const asyncSetStorage = (key,data) => {
return new Promise((resolve) => {
wx.setStorage({
key,
data,
complete(res){
resolve(res)
}
})
})
}
export const asyncGetStorage = (key) => {
return new Promise((resolve) => {
wx.getStorage({
key,
complete(res){
resolve(res)
}
})
})
}
export const asyncRemoveStorage = (key) => {
return new Promise((resolve) => {
wx.removeStorage({
key,
complete(res){
resolve(res)
}
})
})
}
export const asyncClearStorage = () => {
return new Promise((resolve) => {
wx.clearStorage({
complete(res){
resolve(res)
}
})
})
}
import {asyncSetStorage,asyncGetStorage,asyncRemoveStorage,asyncClearStorage} from './utils/storage'
App({
async onShow(){
asyncSetStorage('name','Jerry').then((res)=>{
console.log(res);
})
asyncGetStorage('name').then((res)=>{
console.log(res);
})
asyncRemoveStorage('name').then((res)=>{
console.log(res);
})
asyncClearStorage().then((res)=>{
console.log(res);
})
}
})
五、网络请求封装
1、为什么要封装wx.request()
小程序大多数 API 都是异步 API,如 wx.request(),wx.login()等。这类 API 接口通常都接收一个 Object
对象类型的参数,参数中可以按需指定以下字段来接收接口调用结果:
参数名 | 类型 | 必填 | 说明 |
---|---|---|---|
success | function | 否 | 调用成功的回调函数 |
fail | function | 否 | 调用失败的回调函数 |
complete | function | 否 | 调用结束的回调函数(调用成功、失败都会执行) |
2、请求封装-request方法
在封装网络请求模块的时候,采用class类
来进行封装,采用类的方式封装代码更具可复用性,也方便地添加新的方法和属性,提高代码的扩展性
在 WxRequest
类内部封装一个 request
实例方法
request
实例方法中需要使用 Promise
封装 wx.request,也就是使用 Promise
处理 wx.request
的返回结果
request
实例方法接收一个 options
对象作为形参,options
参数和调用 wx.request
时传递的请求配置项一致
- 接口调用成功时,通过
resolve
返回响应数据 - 接口调用失败时,通过
reject
返回错误原因
3、请求封装-设置请求参数
在发起网络请求时,需要配置一些请求参数,其中有一些参数我们可以设置为默认参数,例如:请求方法、超时时长等,因此在封装时我们要定义一些默认的参数。
我们还要允许在进行实例化的时候,传入参数,对默认的参数进行修改
// 对 WxRequest 进行实例化
const instance = new WxRequest({
baseURL: 'https://gmall-prod.atguigu.cn/mall-api', // 请求基准地址
timeout: 10000 // 微信小程序 timeout 默认值为 60000
})
在通过实例调用request实例方法时也会传入相关的请求参数
const res = await instance.request({
url: '/index/findBanner',
method: 'GET'
})
从而得出结论:请求参数的设置有三种方式:
- 默认参数:在
WxRequest
类中添加defaults
实例属性来设置默认值 - 实例化时参数:在对
WxRequest
类进行实例化时传入相关的参数,需要在constructor
构造函数形参进行接收 - 调用实例方法时传入请求参数
request.js
实例化时设置请求参数(被constructor形参接收)调用实例方法时设置请求参数(被request实例方法的形参options接收)
4、请求封装-封装请求快捷方法
在request()基础上封装一些快捷方法,简化request的调用
需要封装 4 个快捷方法,分别是 get
、delete
、post
、put
,他们的调用方式如下:
instance.get('请求地址', '请求参数', '请求配置')
instance.delete('请求地址', '请求参数', '请求配置')
instance.post('请求地址', '请求参数', '请求配置')
instance.put('请求地址', '请求参数', '请求配置')
这 4 个请求方法,都是通过实例化的方式进行调用,所以需要 Request
类中暴露出来 get
、delete
、post
、put
方法。每个方法接收三个参数,分别是:接口地址、请求参数以及其他参数。
这 4 个快捷方法,本质上其实还是调用 request
方法,我们只要在方法内部组织好参数,调用 request
发送请求即可
与request实例方法同级
5、wx.request注意事项
在使用 wx.request 发送网络请求时,只要成功接收到服务器返回,无论statusCode
是多少,都会进入 success
回调。开发者根据业务逻辑对返回值进行判断。
一般只有网络出现异常、请求超时等时候,才会走 fail
回调
6、请求封装-定义请求/响应拦截器
为了方便统一处理请求参数以及服务器响应结果,为WXRequest
添加拦截器功能,拦截器包括请求拦截器和响应拦截器:
- 请求拦截器本质上是在请求之前调用的函数,用来对请求参数进行新增和修改
- 响应拦截器本质上是在响应之后调用的函数,用来对相响应数据做点什么
不管成功响应还是失败相应,都会执行响应拦截器
可以在WXRequest
类内部定义interceptors
实例属性,属性中需要包含request
以及response
方法
调用请求拦截器
调用响应拦截器
7、请求封装-完善请求/响应拦截器
目前不管请求成功 (success),还是请求失败(fail),都会执行响应拦截器
那么怎么判断是请求成功,还是请求失败呢 ?
封装需求:
- 如果请求成功,将响应成功的数据传递给响应拦截器,同时在传递的数据中新增
isSuccess: true
字段,表示请求成功 - 如果请求失败,将响应失败的数据传递给响应拦截器,同时在传递的数据中新增
isSuccess: false
字段,表示请求失败
在实例调用的响应拦截中,根据传递的数据进行以下的处理:
- 如果
isSuccess: true
表示服务器响应了结果,我们可以将服务器响应的数据简化以后进行返回 - 如果
isSuccess: false
表示是网络超时或其他网络问题,提示网络异常
,同时将返回即可
8、结合项目使用请求/响应拦截器
使用请求拦截器:
在发送请求时,购物车列表、收货地址、更新头像等接口,都需要进行权限验证,因此我们需要在请求拦截器中判断本地是否存在访问令牌 token
,如果存在就需要在请求头中添加 token
字段。
使用响应拦截器:
在使用 wx.request 发送网络请求时。只要成功接收到服务器返回,无论statusCode
是多少,都会进入 success
回调。
因此开发者根据业务逻辑对返回值进行判断。
后端返回的业务状态码如下:
- 业务状态码 === 200, 说明接口请求成功,服务器成功返回了数据
- 业务状态码 === 208, 说明没有 token 或者 token 过期失效,需要登录或者重新登录
- 业务状态码 === 其他,说明请求或者响应出现了异常
// 配置请求拦截器(会覆盖默认的请求拦截器)
instance.interceptors.request=(config)=>{
// 在请求发送之前做点什么
// 在发送请求之前,需要先判断本地是否存在访问令牌token
// 如果存在token,就需要在请求头中添加token字段
const token = getStorage('token')
if(token){
config.header['token']=token
}
return config
}
// 配置响应拦截器(会覆盖默认的响应拦截器)
instance.interceptors.response = async (response)=>{
// 对服务器响应的数据做点什么
// 从response中解构isSuccess,如果isSuccess为false,说明执行了fail回调函数,需要给用户一个提示
const {isSuccess,data} = response
if(!isSuccess){
wx.showToast({
title:'网络异常请重试',
icon:'error'
})
return response
}
// 判断服务器响应的业务状态码
switch(data.code){
case 200:
return data
case 208:
// 说明没有token或者token失效,需要让用户登录或重新登录
const res = await modal({
content:'鉴权失败,请重新登录',
showCancel:false // 不显示取消按钮
})
if(res){
// 清除之前失效的token,同时清除本地存储的全部信息
clearStorage()
wx.navigateTo({
url: '/pages/login/login',
})
}
return Promise.reject(response)
default:
toast({
title:'程序出现异常,请联系客服或稍后重试'
})
return Promise.reject(response)
}
}
9、请求封装-添加并发请求
前端并发请求是指在前端页面同时向后端发起多个请求的情况。当一个页面需要请求多个接口获取数据时,为了提高页面的加载速度和用户体验,可以同时发起多个请求,这些请求之间就是并发的关系。
我们通过两种方式演示发起多个请求:
- 使用
async
和await
方式 - 使用
Promise.all()
方式
async await方式
Promise.all()方法
在request.js中封装promise.all方法,在test.js中调用all()实例方法
10、添加loading
在封装时添加 loading
效果,从而提高用户使用体验
在请求发送之前,需要通过
wx.showLoading
展示loading
效果当服务器响应数据以后,需要调用
wx.hideLoading
隐藏loading
效果
要不要把loading 添加到 WxRequest 内部 ?
- 在类内部进行添加,方便多个项目直接使用类提供的 loading 效果,也方便统一优化 wx.showLoading 使用体验。
但是不方便自己来进行 loading 个性化定制。 - 如果想自己来控制 loading 效果,带来更丰富的交互体验,就不需要将 loading 封装到类内部,但是需要开发者自己来优化 wx.showLoading 使用体验,每个项目都要写一份。
添加loading效果
11、完善loading效果
目前在发送请求时,请求发送之前会展示 loading
,响应以后会隐藏 loading
。
但是 loading 的展示和隐藏会存在以下问题:
- 每次请求都会执行
wx.showLoading()
,但是页面中只会显示一个,后面的loading
会将前面的覆盖 - 同时发起多次请求,只要有一个请求成功响应就会调用
wx.hideLoading
,导致其他请求还没完成,loading
也会隐藏 - 请求过快 或 一个请求在另一个请求后立即触发,这时候会出现
loading
闪烁问题
我们通过 队列 的方式解决这三个问题:首先在类中新增一个实例属性 queue
,初始值是一个空数组
- 发起请求之前,判断
queue
如果是空数组则显示loading
,然后立即向queue
新增请求标识 - 在
complete
中每次请求成功结束,从queue
中移除一个请求标识,queue
为空时隐藏loading
- 为了解决网络请求过快产生
loading
闪烁问题,可以使用定时器来做判断即可
解决第一个问题:判断queue是否为空,如果是空,就显示loading,如果不是空就不调用wx.showLoading()
解决第二个问题:在每个请求结束以后都会执行complete,每次从queue队列中删除一个标识,并判断queue数组是否为空,如果为空说明并发请求完成了,需要隐藏loading,如果不为空则说明还有请求未完成不需要隐藏loading
解决第三个问题:当一个②请求是依靠①请求后才能触发时
- ①请求完毕pop删除queue中最后一个request标识后,再向queue中添加一个request标识
- ②请求进来如果有定时器会首先清除定时器并添加一个request
- ②请求完毕,删除一个request,此时queue中还剩余一个request
- 接下来如果没有新的request了,就会执行定时器内操作,删除最后一个request,隐藏loading,最后关闭定时器
12、控制loading显示
在实际开发中,有的接口可能不需要显示loading效果,或者开发者希望自己来控制loading的样式与交互,那么就需要关闭默认的loading效果,这时候我们就需要一个开关来控制 loading
显示。
- 类内部设置默认请求参数
isLoading
属性,默认值是true
,在类内部根据isLoading
属性做判断即可
2. 某个接口不需要显示 loading
效果,可以在发送请求的时候,可以新增请求配置 isLoading
设置为 false
3. 整个项目都不需要显示loading
效果,可以在实例化的时候(http.js中),传入 isLoading
配置为 false
若设置整个项目都不需要loading效果,但是希望某一个接口用到默认loading效果,只要为那个接口新增请求配置isLoading:true
13、封装uploadFile
wx.uploadFile也是我们在开发中常用的一个api,用来将本地资源上传到服务器
wx.uploadFile({
url: '', // 必填项,开发者服务器地址
filePath: '', // 必填项,要上传文件资源的路径 (本地路径)
name: '' // 必填项,文件对应的 key,开发者在服务端可以通过这个 key 获取文件的二进制内容
})
首先在 WxRequest
类内部创建 upload
实例方法,实例方法接收四个属性:
/**
* @description 文件上传接口封装
* @param { string } url 文件上传地址
* @param { string } filePath 要上传文件资源的路径
* @param { string } name 文件对应的 key
* @param { string } config 其他配置项
* @returns
*/
upload(url, filePath, name, config = {}) {
return this.request(
Object.assign({ url, filePath, name, method: 'UPLOAD' }, config)
)
}
这时候我们需要在 request
实例方法中,对 method
进行判断,如果是 UPLOAD
,则调用 wx.uploadFile
上传API
// request 实例方法接收一个对象类型的参数
// 属性值和 wx.request 方法调用时传递的参数保持一致
request(options) {
// coding...
// 需要使用 Promise 封装 wx.request,处理异步请求
return new Promise((resolve, reject) => {
if (options.method === 'UPLOAD') {
wx.uploadFile({
...options,
success: (res) => {
// 将服务器响应的数据通过 JSON.parse 转换为 JS 对象
res.data = JSON.parse(res.data)
const mergeRes = Object.assign({}, res, {
config: options,
isSuccess: true
})
resolve(this.interceptors.response(mergeRes))
},
fail: (err) => {
const mergeErr = Object.assign({}, err, {
config: options,
isSuccess: true
})
reject(this.interceptors.response(mergeErr))
},
complete: () => {
this.queue.pop()
this.queue.length === 0 && wx.hideLoading()
}
})
} else {
wx.request({
// coding...
})
}
})
}
创建upload实例方法
- url:文件的上传地址、接口地址
- filePath:要上传的文件资源路径
- name:文件对应的key
- config:其他配置项
14、npm包发送请求
npm install mina-request
15、小程序设置环境变量
在实际开发中,不同的开发环境,调用的接口地址是不一样的。
例如:开发环境需要调用开发版的接口地址,生产环境需要调用正式版的接口地址
这时候,我们就可以使用小程序提供的 wx.getAccountInfoSync()
接口,用来获取当前账号信息,在账号信息中包含着 小程序 当前环境版本。
环境版本 | 合法值 |
---|---|
开发版 | develop |
体验版 | trial |
正式版 | release |
// 配置当前小程序项目的环境变量 utils/env.js
const {miniProgram} = wx.getAccountInfoSync()
const {envVersion} = miniProgram
let env = {
baseURL:'https://gmall-prod.atguigu.cn/mall-api'
}
switch(envVersion){
// 开发版
case 'develop':
env.baseURL = 'https://gmall-prod.atguigu.cn/mall-api'
break;
// 体验版
case 'trial':
env.baseURL = 'https://gmall-prod.atguigu.cn/mall-api'
break;
// 正式版
case 'release':
env.baseURL = 'https://gmall-prod.atguigu.cn/mall-api'
break;
default:
env.baseURL = 'https://gmall-prod.atguigu.cn/mall-api'
break;
}
export {env}
16、接口调用方式说明
在开发中,我们会将所有的网络请求方法放置在 api 目录下统一管理,然后按照模块功能来划分成对应的文件,在文件中将接口封装成一个个方法单独导出,例如:
这样做的有以下几点好处:
- 易于维护:一个文件就是一个模块,一个方法就是一个功能,清晰明了,查找方便
- 便于复用:哪里使用,哪里导入,可以在任何一个业务组件中导入需要的方法
- 团队合作:分工合作
六、项目首页
1、首页数据获取
api/index.js
import http from "../utils/http"
export const reqIndexData = () =>{
// 通过并发请求获取首页的数据,提升页面的渲染速度
// 方法一:直接使用promise.all并发请求
// return Promise.all([
// http.get('/index/findBanner'),
// http.get('/index/findCategory1'),
// http.get('/index/advertisement'),
// http.get('/index/findListGoods'),
// http.get('/index/findRecommendGoods')
// ])
// 方法二:使用自己封装的all方法并发请求
return http.all(
http.get('/index/findBanner'),
http.get('/index/findCategory1'),
http.get('/index/advertisement'),
http.get('/index/findListGoods'),
http.get('/index/findRecommendGoods')
)
}
Pages/index/index.js
import {reqIndexData} from "../../api/index"
Page({
data:{
bannerList:[],// 轮播图数据
categoryList:[],// 商品导航数据
activeList:[],// 活动渲染区域
hotList:[],// 人气推荐
guessList:[],// 猜你喜欢
},
// 获取首页数据
async getIndexData(){
// 调用接口api函数,获取数据
const res = await reqIndexData()
console.log(res);
this.setData({
bannerList:res[0].data,
categoryList:res[1].data,
activeList:res[2].data,
guessList:res[3].data,
hotList:res[4].data
})
},
// 监听页面加载
onLoad(){
// 页面加载后调用获取首页数据的方法
this.getIndexData()
}
})
2、分析轮播图区域并渲染
将轮播图制作成一个组件直接在index.wxml中引用
传入接口的参数
轮播图wxml解构,借助block标签
自定义指示点
3、轮播图和指示点的联动
获取当前展示轮播图的索引
动态添加active类名
4、商品导航+活动区域+猜你喜欢+人气推荐
(1)商品导航
(2)活动区域
<view class="adver-left">
<navigator url="/pages/goods/list/list?category2Id={{activeList[0].category2Id}}">
<image src="{{activeList[0].imageUrl}}" mode="widthFix" />
</navigator>
</view>
<view class="adver-right">
<view>
<navigator url="/pages/goods/list/list?category2Id={{activeList[1].category2Id}}">
<image src="{{activeList[1].imageUrl}}" mode="widthFix" />
</navigator>
</view>
<view>
<navigator url="/pages/goods/list/list?category2Id={{activeList[2].category2Id}}">
<image src="{{activeList[2].imageUrl}}" mode="widthFix" />
</navigator>
</view>
</view>
(3)猜你喜欢、人气推荐
把商品列表数据传递给商品列表组件
列表渲染
为商品卡片传递商品信息
渲染
<view class="goods_cart_container">
<navigator
class="navigator_nav"
url="/pages/goods/detail/detail?goodsId={{goodItem.id}}"
>
<!-- 商品图片 -->
<image class="good_img" src="{{goodItem.imageUrl}}" mode="widthFix" />
<!-- 商品详细信息 -->
<view class="goods_item_info">
<!-- 商品名称 -->
<text class="goods_item_info_name">{{goodItem.name}}</text>
<!-- 商品描述 -->
<text class="goods_item_info_promo"
>{{goodItem.floralLanguage}}</text
>
<!-- 商品价格 -->
<view class="goods_item_info_bottom">
<view class="goods_item_info_price">
<text class="text">¥</text>{{goodItem.price}}
</view>
<view class="goods_item_info_origin_price">
<text class="text">¥</text> 1{{goodItem.marketPrice}}
</view>
<!-- 加入购物车图片 -->
<view class="goods_item_info_btn">
<image class="goods_image" src="/assets/images/buybtn.png" mode="" />
</view>
</view>
</view>
</navigator>
</view>
5、首页骨架屏组件
骨架屏是页面的一个空白版本,开发者会使用 CSS
绘制一些灰色的区块,将页面内容大致勾勒出轮廓。
通常会在页面完全渲染之前,将骨架屏代码进行展示,待数据加载完成后,再替换成真实的内容。
骨架屏的设计旨在优化用户体验。
index.wxml
index.js
index.scss
七、商品分类
1、获取商品分类数据
api/category.js
import http from "../utils/http"
export const reqCategoryData = () => {
return http.get('/index/findCategoryTree')
}
Pages/category.js
import {reqCategoryData} from '../../api/category.js'
Page({
// 初始化数据
data:{
categoryList:[]
},
// 获取商品分类的数据
async getCategoryData(){
const res = await reqCategoryData()
console.log(res);
if(res.code === 200){
this.setData({
categoryList:res.data
})
}
},
// 监听页面加载
onLoad(){
this.getCategoryData()
}
})
2、渲染一级分类并实现切换功能
3、获取并渲染二级分类数据
八、框架扩展
1、mobx-miniprogram介绍
目前已经学习了 6 种小程序页面、组件间的数据通信方案,分别是:
- 数据绑定:
properties
- 获取组件实例:
this.selectComponent()
- 事件绑定:
this.triggerEvent()
- 获取应用实例:
getApp()
- 页面间通信:
EventChannel
- 事件总线:
pubsub-js
但是随着项目的业务逻辑越来越复杂,组件和页面间通信就会变的非常复杂。例如:有些状态需要在多个页面间进行同步使用,一个地方发生变更,所有使用的地方都需要发生改变,这时候如果使用前面的数据通信方案进行传递数据,给管理和维护将存在很大的问题。
为了方便进行页面、组件之间数据的传递,小程序官方提供了一个扩展工具库: mobx-miniprogram
介绍:
mobx-miniprogram
是针对微信小程序开发的一个简单、高效、轻量级状态管理库,它基于Mobx
状态管理框架实现。
使用 mobx-miniprogram
定义管理的状态是响应式的,当状态一旦它改变,所有关联组件都会自动更新相对应的数据
通过该扩展工具库,开发者可以很方便地在小程序中全局共享的状态,并自动更新视图组件,从而提升小程序的开发效率
使用方法:
在使用 mobx-miniprogram
需要安装两个包:mobx-miniprogram
和 mobx-miniprogram-bindings
mobx-miniprogram
的作用:创建Store
对象,用于存储应用的数据mobx-miniprogram-bindings
的作用:将状态和组件、页面进行绑定关联,从而在组件和页面中操作数据
安装:
npm install mobx-miniprogram mobx-miniprogram-bindings
mobx-miniprogram 官方文档
mobx-miniprogram-bindings 官方文档
2、mobx-miniprogram创建Store对象
创建 Store 对象需要使用 mobx-miniprogram
, mobx-miniprogram
三个核心概念:
observable
:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。action
:用于修改状态(state)的方法,需要使用 action 函数显式的声明创建。computed
:根据已有状态(state)生成的新值。计算属性是一个方法,在方法前面必须加上get
修饰符
mobx-miniprogram
使用步骤:
在项目的根目录下创建
stores
文件夹,然后在该文件夹下新建numstore.js
在
/store/numstore.js
导入observable
、action
方法import { observable, action } from 'mobx-miniprogram'
observable
用于创建一个被监测的对象,对象的属性是应用状态,状态会被自动转换为响应式数据action
用来显示的定义action方法,action方法是用来修改、更新状态的
使用
observable
方法需要接受一个store
对象,存储应用的状态
// observable:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。
// action:用于显式的声明创建更新 state 状态的方法
import { observable, action } from 'mobx-miniprogram'
// 使用 observable 创建一个被监测的对象
export const numStore = observable({
// 创建应用状态
numA: 1,
numB: 2,
// 使用 action 更新 numA 以及 numB
update: action(function () {
this.numA+=1
this.numB+=1
}),
// computed计算属性,前面必须使用 get 修饰符,是根据已有的状态产生新状态
get sum() {
// 计算属性内部必须要有返回值
return this.numA + this.numB;
}
})
3、在组件中使用Store数据
如果需要在组件中使用状态,需要mobx-miniprogram-bingings
库中导入ComponentWithStore
方法。在使用时:需要将Component
方法替换成ComponentWithStore
方法,原本组件配置项也需要写到该方法中。
在替换以后,就会新增一个 storeBindings
配置项,配置项常用的属性有以下三个:
store
: 指定要绑定的Store
对象fields
: 指定需要绑定的data
字段actions
: 指定需要映射的actions
方法
如果需要在组件中使用Store中的数据
第一步需要从mobx-miniprogram-bindings里面引入ComponentWithStore方法
第二步使用ComponentWithStore方法替换Component方法
第三步导入当前组件需要使用的Store对象
第四步配置当前组件需要与哪些Store相关联
在从store中引入数据和方法以后:
- 如果是数据,会被注入到data对象中
- 如果是方法,会被注入到methods对象中
第五步使用
numstore.js中numA、numB、sum都是data,update是methods
4、在页面(pages)中使用数据
(1)方式一
Component方法用于创建自定义组件,小程序的页面也可以视为自定义组件,因此页面也可以使用Component方法(不使用Page方法)进行构建,从而实现复杂的页面逻辑开发。
使用Component方法来构建页面,那么在页面中享受使用Store中的数据,使用方式和组件的使用方式是一样的
// index/index.js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'
ComponentWithStore({
data: {
someData: '...'
},
storeBindings: {
store: numStore,
fields: ['numA', 'numB', 'sum'],
actions: ['update']
}
})
(2)方式二
如果不想使用Component方法构造页面,这时候需要使用mobx-miniprogram-bindings
提供的BehaviorWithStore
方法和Store
建立关联。
小程序的behavior方法是一种代码复用的方式,可以将一些通用的逻辑和方法提取出来,然后在多个组件中复用,从而减少代码冗余,提高代码的可维护性,在页面中也可以使用behavior配置项。
使用方式如下:
- 新建
behavior
文件,从mobx-miniprogram-bindings
库中导入BehaviorWithStore
方法 - 在
BehaviorWithStore
方法中配置storeBindings
配置项从Store
中映射数据和方法 - 在
Page
方法中导入创建的behavior
,然后配置behavior
属性,并使用导入的behavior
第一步在页面文件夹中新建behavior.js
第二步在behavior.js中从mobx-miniprogram-bindings
中导入BehabviorWithStore
方法
第三步使用BehabviorWithStore
方法,并向外暴露该方法
第四步导入当前页面需要使用的Store对象
第五步配置当前页面需要与哪些Store相关联
第六步在页面.js中导入behavior对象
第七步使用behaviors配置项注册提取的behavior
第八步使用
5、fields、actions对象写法
fields、actions有两种写法:数组或者对象
fields对象写法:
- 映射形式:需要指定data中哪些字段来源于store,以及在store中的名字是什么。
属性是可以自定义的,如果对属性进行了自定义,模板中需要使用自定义以后的属性
fields:{
// 第一种映射形式写法:需要指定data中哪些字段来源于store,以及在store中的名字是什么
numA:'numA',
numB:'numB',
sum:'sum'
},
- 函数形式:
- key:需要指定data中哪些字段来源于store,、
- value是一个函数,该函数内部需要返回对应store数据的值
fields:{
// 第二种函数形式写法:key:需要指定data中哪些字段来源于store,value是一个函数,该函数内部需要返回对应store数据的值
numA:()=> numStore.numA,
numB:()=> numStore.numB,
sum:()=> numStore.sum
},
actions对象写法:
只有映射形式的写法:指定模板中使用的哪些方法来源于store,并且在store中的名字是什么。同样的属性名也可以自定义,但是模板中也要相应发生变化
actions:{
update: 'update'
}
6、绑定多个store与命名空间
在实际开发中,一个页面或组件可能会绑定多个Store
,这时候我们可以将storeBindings
改造成数组。数组每一项就是一个个要绑定的Store
如果多个Store
中存在相同的数据,显示会出现异常。还可以通过namespace
属性给当前Store
开启命名空间,在开启命名空间以后,访问数据的时候,需要加上namespace
的名字才可以。
解决多个store中存在相同数据显示异常的办法:
- 第一种解决方法:利用对象写法,指定不同的属性名
2. 第二种解决方案:添加命名空间,解决数据存在冲突的问题,方法冲突还需另外使用对象写法进行解决。
在添加命名空间以后,如果需要使用数据,需要加上命名空间进行使用
7、miniprogram-computed计算属性和监听器
小程序框架没有提供计算属性相关的api,但是官方为开发者提供了扩展工具库miniprogram-computed。
该工具库提供了两个功能:
- 计算属性
computed
- 监听器
watch
(1)computed计算属性
如果需要在组件中使用计算属性功能,需要从miniprogram-computed
库中导入ComponentWithComputed
方法。
在使用时:需要将 Component
方法替换成 ComponentWithComputed
方法,原本组件配置项也需要写到该方法中。
安装miniprogram-computed,在安装后点击构建npm
,进行本地构建
npm install miniprogram-computed
第一步在组件.js中从miniprogram-computed
库中导入ComponentWithComputed
方法
第二步用ComponentWithComputed方法替换Component方法
第三步定义计算属性
注意事项:
- 计算属性方法内部必须要有返回值
- 计算属性内部不能使用
this
来获取data中的数据,如果想要获取data中的数据需要使用形参 - 计算属性具有缓存特性,只执行一次,后续在使用的时候,只要依赖的数据没有发生改变,返回的始终是第一次执行的结果,如果依赖的数据发生改变,计算属性救护重新执行
(2)watch监听器
在使用时:需要将 Component
方法替换成 ComponentWithComputed
方法,原本组件配置项也需要写到该方法中。
第一步在组件.js中从miniprogram-computed
库中导入ComponentWithComputed
方法
第二步用ComponentWithComputed方法替换Component方法
第三步定义监听器
watch同时监听多个数据
8、mobx和computed结合使用
两个框架扩展提供的 ComponentWithStore
与 ComponentWithComputed
方法无法结合使用。
如果需要在一个组件中既想使用 mobx-miniprogram-bindings
又想使用 miniprogram-computed
解决方案是:
使用旧版
API
自定义组件仍然使用
Component
方法构建组件,将两个扩展依赖包的使用全部改为旧版API
使用兼容写法
即要么使用
ComponentWithStore
方法构建组件,要么使用ComponentWithComputed
方法构建组件如果使用了
ComponentWithStore
方法构建组件,计算属性写法使用旧版API
如果使用了
ComponentWithComputed
方法构建组件,Mobx写法使用旧版API
(1)ComponentWithStore+计算属性使用旧版api
注意:storeBindings配置项fields和actions不要忘记添加s
// components/custom03/custom03.js
import {ComponentWithStore} from 'mobx-miniprogram-bindings'
import {numStore} from '../../stores/numstore'
// 如果使用ComponentWithStore方法构造组件,计算属性扩展库需要使用旧版API
// 这是导入计算属性behavior
const computedBehavior = require('miniprogram-computed').behavior
ComponentWithStore({
// 注册behaviors
behaviors:[computedBehavior],
computed:{
total(data){
return data.a+data.b
}
},
watch:{
'a,b':function(a,b){
console.log(a,b);
}
},
data:{
a: 1,
b: 2
},
storeBindings:{
store:numStore,
fields:['numA','numB','sum'],
actions:['update']
},
methods:{
updateData(){
this.setData({
a: this.data.a + 1,
b: this.data.b + 1
})
}
}
})
<!--components/custom03/custom03.wxml-->
<view>{{numA}}+{{numB}}={{sum}}</view>
<view>{{total}}</view>
<button type="warn" plain bindtap="update">更新Store数据</button>
<button type="warn" plain bindtap="updateData">更新数据</button>
(2)ComponentWithComputed+mobx使用旧版api
// components/custom04/custom04.js
import {ComponentWithComputed} from 'miniprogram-computed'
import {numStore} from '../../stores/numstore'
// 目前组件使用的是ComponentWithComputed进行构建
// 如果想和store对象进行绑定,mobx需要使用旧版api
// 导入mobx的behavior
import { storeBindingsBehavior } from 'mobx-miniprogram-bindings'
ComponentWithComputed({
behaviors: [storeBindingsBehavior], // 添加这个 behavior(注册)
storeBindings:{
store:numStore,
fields:['numA','numB','sum'],
actions:['update']
},
computed:{
total(data){
return data.a+data.b
}
},
watch:{
'a,b':function(a,b){
console.log(a,b);
}
},
data:{
a: 1,
b: 2
},
methods:{
updateData(){
this.setData({
a: this.data.a + 1,
b: this.data.b + 1
})
}
}
})
九、用户登录
1、什么是token
Token
是服务器生成的一串字符串,用作客户端发起请求的一个身份令牌。当第一次登录成功后,服务器生成一个 Token
便将此 Token
返回给客户端,客户端在接收到 Token
以后,会使用某种方式将 Token
保存到本地。以后客户端发起请求,只需要在请求头上带上这个 Token
,服务器通过验证 Token
来确认用户的身份,而无需再次带上用户名和密码。
Token的具体流程
- 客户端向服务器发起登录请求,服务端验证用户名与密码
- 验证成功后,服务端会签发一个
Token
,并将Token
发送到客户端 - 客户端收到
token
以后,将其存储起来,比如放在localStorage
、sessionStorage
中 - 客户端每次向服务器请求资源的时候需要带着服务端签发的
Token
,服务端收到请求,然后去验证客户端请求里面带着的Token
,如果验证成功,就向客户端返回请求的数据
2、小程序登录流程介绍
业务介绍:
传统的登录功能,需要用户先注册,注册完成以后,使用注册的账号、密码进行登录。
小程序的登录操作则比较简单,小程序可以通过微信提供的登录能力,便捷地获取微信提供的用户身份标识进行登录。
免去了注册和输入账号密码的步骤,从而提高了用户体验。
- 小程序调用
wx.login()
方法获取临时登录凭证(临时身份证,有效时间仅为五分钟) - 小程序使用
wx.request()
方法将code传递给开发者服务器,方便后续可以换取微信用户身份id - 开发者的后台接收临时登陆凭证
code
,并拿到Appld
和AppSecret
,使用三个参数向微信服务器发送请求,返回openid
(微信用户身份id微信用户的唯一标识)和session_key
(微信服务器给开发者服务器颁发的身份凭证) - 开发者后台接收到微信服务器返回的数据,执行业务逻辑处理,如:生成自定义登录态token(让token与openid和session_key相关联)、将用户标识和其他信息进行加密处理
- 开发者后台处理好逻辑后,将自定义登录态
token
返回微信小程序客户端 - 客户端每次向开发者后台发送请求时,需要在请求头中携带token
- 开发者后台收到请求后对token进行验证,拿token查询openid和session_key。识别用户身份后查找用户请求的数据资源并返回客户端
3、实现小程序登录功能
4、token存储到Store
上一节已经将token存储到本地了,但是token存储到本地不方便对数据进行操作,要先从本地存储取出,然后使用。关键的一点是存储到本地的数据不是响应式的,当本地存储里面的内容发生改变,页面不会发生改变,因此我们需要将token也存储到store中。
实现步骤:
- 安装
Mobx
两个包,在安装好包以后,对包进行构建,点击构建 npm
- 在项目的根目录下创建
store
文件夹,然后在该文件夹下新建userstore.js
- 导入核心的
observable
、action
方法,创建Store
,同时声明数据和方法 - 在登录页面,导入
ComponentWithStore
方法,并配置storeBindings
方法让页面和Store
对象关联
安装依赖并构建npm
npm i mobx-miniprogram mobx-miniprogram-bindings
创建userStore对象,定义了token和setToken的action方法
使用component构造页面,引入ComponentWithStore并替换component
使用bindings让页面和对象建立关联
调用actions方法将token存储store对象
5、获取用户信息并存储到store
调用接口获取用户的信息,在获取到数据以后,我们需要存储用户信息数据到本地,
用户信息可能会在多个地方使用到,为了方便对用户信息的获取和使用,我们依然将用户信息存储到store
封装接口api函数
定义获取用户信息、将用户信心存储到本地的方法
点击登录执行上述定义的方法
在userStore中定义一个响应式数据和赋值的方法
映射刚才定义的两个方法
6、my页面渲染用户信息
从store中取出用户信息数据,并渲染到页面上
改造页面.js,让页面和store建立联系
通过token判断是否登录,修改主页内容
通过token判断点击商品订单时需要跳转订单列表还是登录页面
十、分包处理
1、配置分包以及预下载
随着项目功能的藏家,项目体积的增大,小程序的加载速度变慢,用户体验受到影响。
需要将更新个人资料
和收货地址
功能配置成一个分包,当用户在访问页面时,还预先加载更新个人资料
和收货地址
所在分包
分包配置项、将需要分包的文件移到分包文件下
验证分包完成
由于进行了分包操作,路径也会更改,所以将要路径进行替换
对预下载分包进行设置
十一、更新用户信息
1、渲染用户信息
点击个人中心的设置,然后点击修改个人资料,就可以对用户的头像和昵称进行修改
在这个页面中,我们需要先渲染信息用户,用户信息目前是存储到 Store
中的,因此我们需要先从 Store
中取出用户信息的数据,进行渲染。
让页面和 Store
数据建立关联,可以使用 mobx-miniprogram-bindings
提供的 BehaviorWithStore
方法
实现步骤:
- 新建
behavior.js
文件,从mobx-miniprogram-bindings
库中导入BehaviorWithStore
方法 - 在
BehaviorWithStore
方法中配置storeBindings
配置项从Store
中映射数据和方法 - 在
Page
方法中导入创建的behavior
,然后配置behavior
属性,并使用导入的behavior
新建behavior,配置storeBindings
导入behavior并注册,注册之后userBehavior中的数据就会映射到data中