系列教程目录
《微信小程序开发快速入门-小程序之开发前准备(1/5)》
《微信小程序开发快速入门-第一个页面编写(2/5)》
《微信小程序开发快速入门-列表页编写(3/5)》
在上一章节中我们实现了列表页面,那么列表页面的数据是从哪里来的呢?接下来这一章节我们就要接着完成备忘录,需求如下:
- 支持备忘录数据添加
- 支持备忘录数据修改
- 支持备忘录数据删除
当大家看到需求之后第一步是什么?
这个时候有同学肯定会说了:“开始编码!时间不等人!”
当然不是这样的,我们做一个事情时间很重要,但是不要站在单点去看时间。站在整个项目周期来看,先分解需求,然后才开始动手,思路清晰之后写起代码来其实是非常快速的。
- 先编写编辑页面布局(添加/修改复用页面)
- 编写数据添加逻辑
- 编写显示列表逻辑
- 编写编辑数据逻辑
- 编写删除数据逻辑
分解完任务就可以做起来。
1.添加/编辑页面
前期准备
首先我们先来新增一个页面。
- 新增文件夹取名
- 新增Page
然后为了提高效率,新增编译模式。 - 添加编译模式
选择好页面路径、名称
大家要记住以上这两步是做任何页面都需要操作的步骤。上个章节也提过,我是怕大家忘记所以再次重新提一次。
配置标题
开始正式编写页面,先看下效果图
从上往下看,相对我们的列表页面布局简单多了。我们先看下头部有个“编辑备忘录”,然后这个头部标题只有当前页面需要用到,所以这个配置需要在页面设置(json)里面配置。从页面配置文档里面我们可以找到“navigationBarTitleText”属性。
当我们配置完标题之后,我们会发现还没没有达到我们预期的效果。
edit.json
{
"navigationBarTitleText": "编辑备忘录"
}
因为我们在app.json里面配置了导航栏背景颜色,所以还需要在调整下导航栏背景颜色以及文字样式,根据文档我们再做下调整。
{
"navigationBarTitleText": "编辑备忘录",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
三个配置轻松搞定!除此之外其实还有方法也可以达到同样的效果,那就是在js里面通过调用方法来设置。
代码设置的优先级要高于配置的优先级,从这可以看出来小程序渲染机制是先读json然后再去调用js的代码,所以js的方法覆盖的json的配置。
看些关于设置导航的方法,有设置标题、样式、显示/隐藏进度条、隐藏返回首页按钮。当然标题和样式通常来说还是在json里面配置,只不过我们要知道有这种方式。
页面样式
解决完导航栏之后,我们可以正式进入开始布局。
整体布局就4个组件,前面4个组件分别用text(时间)、view(分界线)、input(标题,单行输入框)、textarea(内容,多行输入框)组件上个从上往下排列,然后保存按钮可以用个view组件来做,做个对底部的定位。
view、text组件在我们做列表页面就接触过了它们分别属于容器组件和基础内容组件,而input、textarea属于表单组件用于收集信息的,更多表单组件可以查看表单组件文档。
先输出布局结构:
<view>
<!-- 时间 -->
<text>2018-07-28 00:00</text>
<!-- 标题 -->
<input>请输入标题</input>
<!-- 分界线 -->
<view></view>
<!-- 内容 -->
<textarea>请输入内容</textarea>
</view>
<!-- 保存 -->
<view>保存</view>
大家仔细看下,这里面是不是有个问题。我们会发现input标签中间输入的文字并没有显示,并且我们其实不是单纯的显示文字,而是需要默认提示文字,当用户输入文字后默认提示文字自动隐藏。在input里面显示默认文字不能用text和view这种标签包裹内容的方式先实现,而是它有自己特有的属性。
虽然textarea组件中间可以输入文字,但是也无法达到输入隐藏默认文字的效果,并且输入文字还会叠在中间的文本上,所以textarea不能使用这种方式。
鼠标悬浮到组件上,打开input文档我们来找找看。通过文档我们发现“placeholder”属性。同理textarea也一样。
通过设置完成后就达到了我们想要的效果:
<view>
<!-- 时间 -->
<text>2018-07-28 00:00</text>
<!-- 标题 -->
<input placeholder="请输入标题" ></input>
<!-- 分界线 -->
<view></view>
<!-- 内容 -->
<textarea placeholder="请输入内容"></textarea>
</view>
<!-- 保存 -->
<view>保存</view>
完成基础结构布局后,我们再来写写样式。
<view class="container">
<!-- 时间 -->
<text class="date">2018-07-28 00:00</text>
<!-- 标题 -->
<input class="title-font-size" placeholder-class="input-placeholder title-font-size" placeholder="请输入标题" maxlength="20"></input>
<!-- 分界线 -->
<view class="line"></view>
<!-- 内容 -->
<textarea class="content-font-size" placeholder-class="input-placeholder content-font-size" placeholder="请输入内容" maxlength="1000"></textarea>
</view>
<!-- 保存 -->
<view class="save">保存</view>
从布局结构上来看,我们没有变化,但是我们会发现和之前不一样的地方。
-
看到输入框组件,有个“placeholder-class”的属性,这个是影响placeholder(占位符)的样式。因为对于css来说它无法区分控制在什么状态下,但是又有对同一个组件不同状态下的需求,所以通过不同属性的设置来分别设置class,正常输入状态下我们使用的是class,占位符样式下是设置placeholder-class。
-
除此之外有有个“maxlength”属性,它是控制最大长度,默认为140。我们就需要根据具体业务需求要定制这个限制,如:标题最多20,内容区域最多1000。当然还可以设置无限制,设置“-1”即可。
再仔细观察下,我在“placeholder-class”属性中用到了多个class使用空格分开,这种用法同样适用于class,当你一个组件需要用到多个样式的时候就可以这样写,这种写法也是常用写法。通常来说有些样式是通用的就会抽取出来,然后个性化的样式就单独写,这样提高了样式的复用性。
然后我们再看看css部分,可以看到input-placeholder这个样式就是两个输入框通用的部分,title-font-size和content-font-size就是不两个输入框不一样的地方。
/* 容器 */
.container {
margin: 30rpx 30rpx;
}
/* 时间 */
.date {
font-size: 24rpx;
font-weight: 400;
color: #999999;
}
/* 分割线 */
.line {
width: 690rpx;
height: 1rpx;
background: #E9E9E9;
margin: 20rpx 0rpx;
}
/* 占位符通用样式 */
.input-placeholder {
font-weight: 400;
color: #CCCCCC;
}
/* 标题字体大小 */
.title-font-size {
font-size: 32rpx;
}
/* 内容字体大小 */
.content-font-size {
font-size: 28rpx;
}
/* 保存按钮 */
.save {
width: 510rpx;
height: 110rpx;
background: #F8D300;
border-radius: 20rpx;
position: absolute;
left: 120rpx;
bottom: 115rpx;
color: #FFFFFF;
text-align: center;
line-height: 110rpx;
}
搞定!虽然看着简单但是涉及到了很多知识点。
小结
- 导航栏配置,两种方法
- json配置
- API设置
- 表单组件输入框
- CSS 使用小技巧
数据添加
先来分析下如何实现:
- 获取当前时间
- 获取输入框内容
- 添加时做数据校验
- 保存数据到本地缓存
1. 获取当前时间
我们需要在用户进入这个页面的时候就把当前时间现在出来,这里就需要了解生命周期的概念了。我们会发现每次我们新建一个Page的时候都会有一些默认的函数,里面就包含生命周期函数。
// pages/t/t.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
console.log('onLoad','监听页面加载')
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
console.log('onReady','监听页面初次渲染完成')
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
console.log('onShow','监听页面显示')
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
console.log('onHide','监听页面隐藏')
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
console.log('onUnload','监听页面卸载')
},
})
我们使用console.log打印一下,分别在不同的生命周期打印一下。
当用户进入一个页面的时候,小程序会调用用下面的顺序调用三个生命周期函数:
当这个页面被覆盖的时候会调用 onHide(监听页面隐藏)如:从A页面跳转到B页面,A页面的onHide就会被调用。
如果用户再从B页面退回到A页面的时候就会触发页面的onShow(监听页面显示)。
当页面被关闭的时候就会调用onUnload(监听页面卸载)。
那么什么时候会被关闭什么时候又不会被关闭呢?
还记得我们之前学习的路由API吗?
这个取决于你的路由API的模式:
- 使用wx.redirectTo会关闭当前页,这种情况下A页面到B页面,A跳转到时候就会调用onUnload(监听页面卸载)。
- 使用wx.navigateTo会保留当前页,这种情况下A页面到B页面,A跳转到B时候就会onHide(监听页面隐藏)并且返回会触发A页面的onShow(监听页面显示)。这里要注意保留小程序中页面栈最多十层。
PS:页面栈里面就是存放所有未关闭的页面。
以上内容很重要,要理解并且吸收在后续很多场景会用到这个知识点。
知道了生命周期之后,所以我们需要在onLoad函数里面编写获取时间的代码。将时间获取完成后setData更新页面,在这里使用Date对象的自带方法。
// pages/edit/edit.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
this.setData({
nowDate: this.getNowDate(),
nowTime: this.getNowTime()
})
},
// 获取当前日期
getNowDate: function () {
let dateTime
let YYYY = new Date().getFullYear()
let MM = new Date().getMonth() + 1
let DD = new Date().getDate()
dateTime = YYYY + '-' + MM + '-' + DD
return dateTime
},
// 获取当前时间
getNowTime: function () {
let dateTime
let HH = new Date().getHours()
let mm = new Date().getMinutes() < 10 ? '0' + new Date().getMinutes() :
new Date().getMinutes()
dateTime = HH + ':' + mm
return dateTime
}
})
然后在页面上进行显示。这里要注意如果data里面没有先定义变量名也可以进行赋值,系统会自动创建一个变量然后再进行赋值,只是在data里面看不见,可以在调试器中的AppData里面看到。虽然可以这样做,但是建议最好在data里面先定义,这样代码可读性会更好。
view class="container">
<!-- 时间 -->
<text class="date">{{nowDate}} {{nowTime}}</text>
<!-- 省略无关内容 -->
</view>
时间显示搞定了,接下来获取下输入框的内容
2. 获取输入框内容
想要获取输入框内容需要用到bindinput事件。
<view class="container">
<!-- 省略无关布局 -->
<!-- 标题 -->
<input bindinput="bindTitle" class="title-font-size" placeholder-class="input-placeholder title-font-size" placeholder="请输入标题" maxlength="20"></input>
<!-- 分界线 -->
<view class="line"></view>
<!-- 内容 -->
<textarea class="content-font-size" placeholder-class="input-placeholder content-font-size" placeholder="请输入内容" maxlength="1000"></textarea>
</view>
监听了键盘触发时间,触发对应js文件中的bindTitle方法
bindTitle(event){
console.log(event)
}
我们在js里面用console.log输出下内容。这里讲一下console,这个类特别常用的,用于开发过程中打印调试日志。console支持打印不同级别的日志,这个对应我们调试器中的console面板,面板还可以过滤不同级别的日志。我想你一定对这里不陌生,因为同样系统报错信息也会输出到Console日志区。
不同级别的console面板显示的样式也不一样。如:普通、警告、错误日志这三种类型。
bindTitle(event){
console.log(event)
console.warn(event)
console.error(event)
}
通常我们调试只需要用console.log就行了,还支持输出多个参数用“,”隔开。
接着上面的继续,我们输入input里面的内容。当我输入“123”的时候触发了3次,输出这个回调参数,找到我们想要的value,打印出来。这样就获取到我们的标题内容了,内容输入框一样。获得到之后我们还要存储在data里面,便于保存的时候使用。
代码如下:
标题和内容分别绑定监听,然后在js能够获取到用户输入的内容,并且存储在data里面。
<view class="container">
<!-- 省略无关布局 -->
<!-- 标题 -->
<input bindinput="bindTitle" class="title-font-size" placeholder-class="input-placeholder title-font-size" placeholder="请输入标题" maxlength="20"></input>
<!-- 分界线 -->
<view class="line"></view>
<!-- 内容 -->
<textarea bindinput="bindContent" class="content-font-size" placeholder-class="input-placeholder content-font-size" placeholder="请输入内容" maxlength="1000"></textarea>
</view>
// pages/edit/edit.js
Page({
/**
* 页面的初始数据
*/
data: {
title: null, // 标题
content: null,// 内容
},
// 监听标题
bindTitle(event) {
this.data.title = event.detail.value
},
// 监听内容
bindContent(event) {
this.data.content = event.detail.value
}
})
给data里面的数据赋值需要用this.data.变量名。
3. 添加时做数据校验
首先要给按钮绑定点击事件。
<!-- 省略无关代码 -->
<!-- 保存 绑定保存事件-->
<view bindtap="save" class="save">保存</view>
当用户点击保存的时候去判断是否为空。在下面的代码中我抽取出来了一个判空函数,对不用户不输入任何参数与空格做个判断。在这里注意写代码的时候如果一段代码在多处使用就需要单独抽取出来,减少重复代码。
从上往下看如果标题为空我就使用return这样代码就不会往下走了,只有标题和内容都不为空的时候这样才能提交数据。
// 保存数据
save() {
let title = this.data.title
let content = this.data.content
if (this.isEmpty(title)) {
console.log('标题不能为空!')
return
}
if (this.isEmpty(content)) {
console.log('内容不能为空!')
return
}
console.log('提交数据')
},
// 判断字符串是否为空
isEmpty(str) {
if (str != null && str.trim().length > 0) {
return false;
}
return true;
}
以上代码从逻辑上来看数据校验没有什么问题,但是实际上对于用户的交互就很问题。因为我在需要提示用户的时候使用的是console.log,这个只适合给开发人员调试看而实际用户是看不到的。在这里需要给用户一个提示,这个时候就需要用到showToast方法,给用户一个提示。
然后我们替换掉log部分代码
// 保存数据
save() {
let title = this.data.title
let content = this.data.content
if (this.isEmpty(title)) {
wx.showToast({
title: '标题不能为空',
icon: 'error',
duration: 2000
})
return
}
if (this.isEmpty(content)) {
wx.showToast({
title: '内容不能为空',
icon: 'error',
duration: 2000
})
return
}
wx.showToast({
title: '提交成功',
icon: 'success',
duration: 2000
})
},
// 判断字符串是否为空
isEmpty(str) {
if (str != null && str.trim().length > 0) {
return false;
}
return true;
}
现在数据有了,接下来就把它存起来。
4. 存放数据缓存
小程序数据缓存管理需要用到Storage操作。
首先先检查本地是否有缓存数据,如果有就拿出来已有数据,否则新建数组,新增数据。由于是多条数据所以要用到js中的数组,在维护数组的过程中会用到很多数组方法。
// 保存数据
save() {
let title = this.data.title
let content = this.data.content
// 省略校验代码
// 获取缓存数据,如果没有回返回[],如果有返回已有数据
let list = wx.getStorageSync('list')||[]
// 组装数据对象
let data = {
title: title,
content: content,
date: this.getNowDate(),
time: this.getNowTime()
}
// 在开头插入到数组中
list.unshift(data)
// 设置到本地缓存
wx.setStorageSync('list', list)
// 省略提示代码
// 回到上一页面
wx.navigateBack()
},
存储成功后,我们可以在调试器中的Storage面板看到数据,保存成功后回到列表页面。
如果想清空本地缓存可以用「清楚缓存」中的清除数据缓存。
保存成功后回到列表页面。
在此之前先把列表页面跳转路由设置下。
<view>
<!-- 省略无关代码 -->
<view bindtap="toEdit" class="content-btn">+ 新建</view>
<!-- 省略无关代码 -->
<!-- 添加icon -->
<image wx:if="{{!isEmpty}}" bindtap="toEdit" class="write" src="../../images/write.png"></image>
</view>
toEdit(){
wx.navigateTo({
url: '/pages/edit/edit',
})
}
在这里补充只要是路径参数就可以用绝对路径和相对路径。
- 绝对路径就是文件的真正存在的路径,进行一级级目录指向文件。
- 相对路径就是以当前文件为基准进行一级级目录指向被引用的资源文件。
- …/ 表示当前文件所在的目录的上一级目录
以这个项目结构为例,列表页跳转到编辑页
绝对路径:/pages/edit/edit
相对路径:…/edit/edit
PS:不仅仅是页面路径,图片路径同理
数据显示
1. 列表显示
接下来看下用户操作路径,从列表页到编辑页面添加,然后添加完成之后就会刷新列表。这个时候就会调用页面生命周期的onShow方法,所以我们需要在在onShow方法里面获取缓存数据并且更新isEmpty。在这里要注意一定要更新isEmpty,否则就算列表有值也显示不出来,isEmpty控制了是否显示列表。
onShow(){
let list = wx.getStorageSync('list')||[]
this.setData({
list:list,
isEmpty:!list.length>0
})
},
2. 查看单条数据
首先给列表子项绑定点击事件,然后通过设置data-*进行点击传值。设置了一个data-index将用户点击到当前列表下标传到toEdit方法中。
<view bindtap="toEdit" data-index="{{index}}" class="list" wx:for="{{list}}">
<view>
<text class="list-title">{{item.title}}</text>
<text class="list-date">{{item.date}} \n {{item.time}}</text>
</view>
<view class="list-content">{{item.content}}</view>
<view class="line"></view>
</view>
获取参数方法
toEdit(event){
console.log(event)
let index = event.currentTarget.dataset.index
console.log(index)
}
在这里要注意由于从新建按钮点击跳转到编辑页面和点击列表中数据是不一样的数据逻辑,所以要找个方式区分开来。
经过日志调试输出,发现如果是从新建按钮点击进来index就获取不到,列表点击就能获取到下标通常是等于0或者大于0,这样我们就可以做个判断。如果是列表进入的进入编辑页面就填充好点击当前的数据。
toEdit(event) {
let index = event.currentTarget.dataset.index
let isClickItem = index >= 0
if (isClickItem) {
// 从列表点击
wx.navigateTo({
url: '/pages/edit/edit?index=' + index,
})
} else {
// 从新建按钮点击
wx.navigateTo({
url: '/pages/edit/edit',
})
}
}
如果点击了列表项就将下标数据传递到下个页面。补充页面跳转传参:参数与路径之间使用 ? 分隔,参数键与参数值用 = 相连,不同参数用 & 分隔;如 ‘path?key=value&key2=value2’,更多传参数方式可以看《微信小程序如何实现页面传参?》
在跳转到下个页面onLoad函数中只要获取到index参数就确认是修改并取出数据。取值补充:在onLoad函数options参数中获取对应的变量名数据。
onLoad: function (options) {
if (options.index) {
// 显示已有数据
let list = wx.getStorageSync('list')
let item = list[options.index]
this.setData({
title: item.title,
content: item.content,
nowDate: item.date,
nowTime: item.time,
index: options.index
})
} else {
// 显示最新数据
this.setData({
nowDate: this.getNowDate(),
nowTime: this.getNowTime()
})
}
},
<view class="container">
<!-- 时间 -->
<text class="date">{{nowDate}} {{nowTime}}</text>
<!-- 标题 -->
<input value="{{title}}" bindinput="bindTitle" class="title-font-size" placeholder-class="input-placeholder title-font-size" placeholder="请输入标题" maxlength="20"></input>
<!-- 分界线 -->
<view class="line"></view>
<!-- 内容 -->
<textarea value="{{content}}" bindinput="bindContent" class="content-font-size" placeholder-class="input-placeholder content-font-size" placeholder="请输入内容" maxlength="1000"></textarea>
</view>
<!-- 保存 -->
<view bindtap="save" class="save">{{index?'修改':'保存'}}</view>
双括号中支持js表达式 {{index?‘修改’:‘保存’}} 就是js中的三元表达式,类似于if else效果。
查看数据详情就已经完成了,接下来就开始进行数据修改了
数据修改
当用户点击保存的时候去判断是否存在item这个对象,如果存在就是修改否则就是新增。修改本身是两个操作:删除当前数据,然后插入一条新数据。因为插入数据新增也需要做这个操作,所以实际上只需要在新增之前加一个判断,如果是修改就删除掉当前数据即可。
// 保存数据
save() {
// 省略无关代码
// 如果有下标就是修改
if (this.data.index) {
// 删除
list.splice(this.data.index, 1)
}
// 在开头插入到数组中
list.unshift(data)
// 设置到本地缓存
wx.setStorageSync('list', list)
// 省略提示代码
// 回到上一页
wx.navigateBack()
},
数据删除
列表项目绑定长按事件(bindlongtap)
<view bindtap="toEdit" bindlongtap="del" data-index="{{index}}" class="list" wx:for="{{list}}">
<view>
<text class="list-title">{{item.title}}</text>
<text class="list-date">{{item.date}} \n {{item.time}}</text>
</view>
<view class="list-content">{{item.content}}</view>
<view class="line"></view>
</view>
// 删除
del(event) {
let that = this
let index = event.currentTarget.dataset.index
wx.showModal({
title: '提示',
content: '你确定删除?',
success(res) {
if (res.confirm) {
that.data.list.splice(index, 1)
wx.setStorageSync('list', that.data.list)
that.setData({
list: that.data.list,
isEmpty:!that.data.list.length>0
})
}
}
})
},
长按删除之前还需要弹出对话框让用户进行二次确认,对话框就需要用到showModal方法。
确认后需要更新下页面数据以及状态。我们会发现更新数据出现过两次一样的代码,那这个时候其实我们可以抽象出一个函数,叫做udpateList。
onShow() {
// 添加后更新数据
let list = wx.getStorageSync('list') || []
this.udpateList(list)
},
// 删除
del(event) {
let that = this
let index = event.currentTarget.dataset.index
wx.showModal({
title: '提示',
content: '你确定删除?',
success(res) {
if (res.confirm) {
that.data.list.splice(index, 1)
wx.setStorageSync('list', that.data.list)
that.udpateList(that.data.list)
}
}
})
},
// 更新列表数据
udpateList(list){
this.setData({
list: list,
isEmpty:!list.length>0
})
},
搞定!
总结
这一章节学完我们基本就完成了本地版备忘录的所有功能了,从单个页面到多个页面交互以及数据的增删改查都完成了。
下一章节就是优化之前的代码,要知道没有最好只有更好,我们要持续成长,才能成为更好的自己。
如果觉得不错,可以给本文点赞,收藏,分享,在学习过程中有任何问题可以留言给我,包教包会。
为什么Storage那一堆代码放到save里识别不出来啊
感谢大佬,讲解的太细致了,理清了我开发过程中好多模糊的技术点
你好,请问为什么我增加校验数据是否为空的功能后,不论是否输入标题和内容,都提示我标题不能为空呢?代码如下:
// 判断字符串是否为空
isEmpty(str) {
if (str != null && str.trim().length > 0) {
return false
}
return true;
},
// 保存数据
save() {
let title = this.data.title
let content = this.data.content
if (this.isEmpty(title)) {
wx.showToast({
title: '标题不能为空',
icon: 'error',
duration: 2000
})
return
}
if (this.isEmpty(content)) {
wx.showToast({
title: '内容不能为空',
icon: 'error',
duration: 2000
})
return
}
wx.showToast({
title: '提交成功',
icon: 'success',
duration: 2000
})
},
出现了这个问题,求助
您好,想请问一下按照您这个代码编辑好以后save()函数里输入的这段代码出现报错,说不存在属性index
照着你的写的为什么会这样啊?求帮助!
2.第二个报错,你的方法没有定义