前文
微信小程序学习之旅–第一个页面的制作
微信小程序学习之旅–零基础制作自己的小程序–第二个页面的制作
微信小程序学习之旅–完善pages页面–字体图标,数据绑定,条件/列表渲染,事件/catch/bind以及路由的学习

微信小程序入门(四)-完成文章详情页

制作静态页面

先有死数据制作出我们想要的效果

image-20210828102541148

布局也比较简单,直接上代码

页面结构
<!--pages/post-detail/post-detail.wxml-->

<!-- 先静后动 -->
<view class="container">
  <image class="head-image" src="/images/post/bl.png"></image>

  <!-- 头像区域 -->
  <view class="author-date">
    <image class="avatar" src="/images/avatar/1.png"></image>
    <text class="author">听雨少年</text>
    <text class="const-text">发表于</text>
    <text class="date">16小时前</text>
  </view>
  <!-- 标题 -->
  <text class="title">你好!!!!
  </text>

  <!-- 图标 -->
  <view class="tool">
    <view class="circle">
      <image src="/images/icon/collection-anti.png"></image>
      <image class="share-image" src="/images/icon/share.png"></image>
    </view>
    <!-- 水平线 -->
    <view class="horizon"></view>
  </view>

  <!-- 文章文本 -->
  <text class="detail">哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈</text>
</view>
页面样式
/* pages/post-detail/post-detail.wxss */

.container {
  display: flex;
  flex-direction: column;
}

/* 图片 */
.head-image {
  width: 100%;
  height: 460rpx;
}

/* 头像区域 */
.author-date {
  display: flex;
  flex-direction: row;
  margin-top: 20rpx;
  margin-left: 32rpx;
  align-items: center;
}

.avatar {
  width: 64rpx;
  height: 64rpx;
}

.author {
  font-size: 30rpx;
  font-weight: 300;
  margin-left: 20rpx;
  color: #666;
}

.const-text {
  font-size: 24rpx;
  color: #999;
  margin-left: 20rpx;
}

.date {
  font-size: 24rpx;
  color: #999;
  margin-left: 30rpx;
}

/* 标题 */
.title {
  margin-left: 40rpx;
  font-size: 36rpx;
  font-weight: 30rpx;
  margin-top: 30rpx;
  color: #4b556c;
  letter-spacing: 2px;
}

/* 图标 */
.tool {
  display: flex;
  /* 设置主轴 */
  flex-direction: column;
  /* 侧轴排列方式 */
  align-items: center;
  /* 垂直居中 主轴排列方式  */
  justify-content: center;
}

.circle {
  display: flex;
  width: 660rpx;
  justify-content: flex-end;
}

.circle image {
  width: 90rpx;
  height: 90rpx;
}

.circle image:last-child {
  margin-left: 20rpx;
}

.horizon {
  width: 660rpx;
  height: 1px;
  background-color: #e5e5e5;
  position: absolute;
  z-index: -1;
}
/* 文本 */
.detail{
  color:#666;
  margin-left: 30rpx;
  margin-top: 20rpx;
  margin-right: 30rpx;
  line-height: 44rpx;
  letter-spacing: 2px;
  text-indent: 2em;
}

页面跳转

传递文章数据

我们页面的布局制作好了。接下来就是点击哪一篇文章,就显示哪一篇文章的详情页。这时候跳转的时候我们就需要传递数据。

我们采用自定义属性来保存当前文章的文章号。

自定义属性

image-20210828105003033

事件对象获取自定义属性
// event  事件
  onGoToDetail(event) {
    // 使用事件对象 拿到我们当前文章的文字号 
    // 通过 currentTarget.dataset 可以拿到我们事件目标上自定义的 data-属性
    // 我们可以通过这个对象拿到定义在事件对象上的属性 data- 后面的就是自定义的属性
    // 小程序自定义的 data- 属性的规则
    // data-id ---> id
    // data-Id ---> id
    // data-postId ---> postid
    // data-PostId ---> postid
    // data-post-id ---> postId
    // data-post-ID ---> postId
    console.log(event.currentTarget.dataset);
    wx.navigateTo({
      // 跳转文章详情页
      url: "/pages/post-detail/post-detail",
    })
  },
页面通信

页面跳转的时候,我们将文章号传递过去。一般都是通过url来传递参数。

url?参数1=值1

?后面的参数我们也叫查询参数,查询字符串。

所以页面跳转的时候,我们将文章号传递过去

wx.navigateTo({
      // 跳转文章详情页
      url: "/pages/post-detail/post-detail?pid="+event.currentTarget.dataset.postId,
    })
参数的接收

传递到本页面的参数,我们可以通过生命周期函数中的onLoad函数里面的参数options来获取到查询字符串里面的值。

/**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    // 通过监听页面加载的的函数的参数options,机也可以拿到我们传递过来本页面的参数
    console.log(options);
  },

image-20210828105721859

渲染指定文章详情页

我们拿到前面传递过来的文章号,就可以再次模拟请求数据,然后进行数据的渲染。

// pages/post-detail/post-detail.js

// 导入数据
import postList from '../../data/data.js'
Page({

  /**
   * 页面的初始数据
   */
  data: {
    post: {}
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    // 通过监听页面加载的的函数的参数options,机也可以拿到我们传递过来本页面的参数
    // 注意:查询字符串(参数)的类型都是字符串类型
    // console.log(options);
    const [post] = [...postList.filter(post => post.postId === parseInt(options.pid))];

    // console.log(post);
    // 拿到我们指定的文章
    this.setData(post);
  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady: function () {

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {

  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide: function () {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload: function () {

  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh: function () {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom: function () {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage: function () {

  }
})
页面数据替换
<!--pages/post-detail/post-detail.wxml-->

<!-- 先静后动 -->
<view class="container">
  <image class="head-image" src="{{imgSrc}}"></image>

  <!-- 头像区域 -->
  <view class="author-date">
    <image class="avatar" src="{{avatar}}"></image>
    <text class="author">{{author}}</text>
    <text class="const-text">发表于</text>
    <text class="date">{{dateTime}}</text>
  </view>
  <!-- 标题 -->
  <text class="title">{{title}}
  </text>

  <!-- 图标 -->
  <view class="tool">
    <view class="circle">
      <image src="/images/icon/collection-anti.png"></image>
      <image class="share-image" src="/images/icon/share.png"></image>
    </view>
    <!-- 水平线 -->
    <view class="horizon"></view>
  </view>

  <!-- 文章文本 -->
  <text class="detail">{{detail}}</text>
</view>
效果

image-20210828111416810

缓存机制/异步API

app.js的作用

接下来,我们想完成在文章详情页的收藏和取消收藏。并记录下是否收藏的状态。实际上用户收藏以后,应该是向服务器发起请求,将数据保存在服务器端。但是这里我们没有使用数据库,那么就需要考虑保存在本地的某个地方。比如:全局变量。当然这不是最好的。

而小程序的全局变量,是写在app.js中的。

小程序的生命周期

小程序的生命周期和页面的生命周期类似。

App({
  /**
   * 小程序启动的生命周期
   */
  onLaunch() {
    console.log('小程序启动!');
  },
  /**
   * 小程序页面展示的时候
   */
  onShow() {
    console.log('onShow!');
  },
  /**
   * 小程序隐藏的时候执行
   */
  onHide() {
    console.log('小程序隐藏!');
  },
  /**
   * 小程序出现错误的时候执行
   */
  onError() {
    console.log('小程序报错!');
  }
})
定义全局变量

在app.js文件中,可以定义全局变量。和小程序的生命周期函数定义的位置是一样的。

App({
    name:'App.js name'
})
在页面获取全局变量

定义的全局变量如何获取呢?

其实,在其他页面,可以通过小程序通过的getApp()函数,来获取定义的全局变量的对象。

const app = getApp()
console.log(app);

image-20210828163111487

所以我们在app.js中定义的全局变量,是可以在所有页面进行全局共享的。

但是,并不是说全局的变量的数据就是一直不变的。当小程序重启了,数据又会回到最开始的状态。所以 不能把全局变量当做一个永久数据保存的地方。

小程序的缓存

很明显,用全局变量来记录用户是否收藏一篇文章并不是一个很好的方案。

那么,我们应该采取什么手段?

这里引入小程序的缓存来解决。

什么是小程序的缓存?其实和我们js中的localstorage很相似。

即使我们的小程序重启了,保存在缓存中的数据依然是有效的,不会被重置。而定义在app.js中的全局变量则在小程序重启后会被重置。

同步缓存

通过小程序通过的setStorageSync()的方法,可以同步设置或者更新缓存。

Page({
    /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    // 同步设置(修改)缓存的方法
    // 两个参数:键  值
    wx.setStorageSync('flag', true);
    // 删除缓存
    wx.removeStorageSync("flag");
    // 清空所有缓存
    wx.clearStorageSync();
    wx.setStorageSync('name', "你好! ")
    // 获取我们设置的缓存
    const n = wx.getStorageSync("name");
    console.log(n); 
  }
})

通过小程序的调试器是可以看见和管理我们的缓存的。即使程序重启缓存也不会消失 的。

image-20210828164843681

通过这个按钮就可以清空缓存

image-20210828164957922

你也可以自己手动在这里新建缓存。

异步缓存

异步设置缓存在设置缓存较多的时候,可以提升用户体验。以前我们设置或者删除缓存是通过回调函数来执行成功后的操作。

// 异步设置缓存 参数是一个对象 key是键 data是数据 也就是设置的value
    wx.setStorage({
      data: '毛毛1',
      key: 'name',
    });
    // 获取缓存 参数也是对象
    wx.getStorage({
      // 要获取的缓存的键名
      key: 'name',
      // 获取成功后的回调函数
      success(res) {
        console.log(res); // post-detail.js? [sm]:50 {errMsg: "getStorage:ok", data: "毛毛1"}
      }
    });

    // 移除缓存
    wx.removeStorage({
      key: 'name',
      // 移除属性成功后的回调函数
      success(res){
        console.log(res);
      }
    })

    // 清空缓存
    wx.clearStorage({
      // 缓存清空成功后的回调
      success: (res) => {
      console.log(res);
      },
    })

如果我们异步获取缓存的时候,不设置回调函数,可以得到一个promise对象的返回值。

如果对promise不了解的,可以先去了解一下。

wx.setStorage({
      data: '听雨少年',
      key: 'name',
    });
    // 获取缓存 无回调
    const res =wx.getStorage({
      key: 'name',
    });
    console.log(res instanceof Promise); // true
    // 传统的promise 获取成功后的结果 ES6新增
    res.then((value)=>{
      console.log(value);// {errMsg: "getStorage:ok", data: "听雨少年"}
    })

但是,这种操作promise对象有了更好的解决方案。可以使用ES7新增的async 和 await关键字。

async onLoad(){
wx.setStorage({
      data: '听雨少年',
      key: 'name',
    });
    const res = await wx.getStorage({
      key: 'name',
    })
    console.log(res);// {errMsg: "getStorage:ok", data: "听雨少年"}
  }

实现文章收藏功能

接下来我们要做的,就是完成文章的收藏功能。

image-20210828173659726

第一步:点击事件

image-20210828212838805

<!-- 条件渲染:在收藏和未收藏的图标之间切换 -->
      <image wx:if="{{!collected}}" bind:tap="onCollect" src="/images/icon/collection-anti.png"></image>
      <image wx:else bind:tap="onCollect" src="/images/icon/collection.png"></image>

这里用一个变量来控制收藏和未收藏的图片切换。

第二步:收藏缓存

如果想要完成小程序刷新后还可以记录好文章是否收藏的状态,我们需要将当前的文章状态保存到缓存中。

如何完成所有文章都可以互不影响的缓存呢?

这里采用的方式是:记录一个对象,将每篇文章的id作为属性,是否收藏了这篇文章作为这个属性的值。

比如:

post_collection = {
    // id:true/false
    1:true,
    6:false
}
第三步:实现
定义数据
/**
   * 页面的初始数据
   */
  data: {
    post: {},
    // 记录当前文章的id
    _pid: null,
    // 当前文章是否收藏
    collected: false,
    // 所有文章是否收藏的缓存对象
    _postsCollected:{}
  }
页面初始化
/**
   * 生命周期函数--监听页面加载
   */
  async onLoad(options) {
    const [post] = [...postList.filter(post => post.postId === parseInt(options.pid))];

    // 记录文章id
    this.data._pid = post.postId;

    // console.log(post);
    // 拿到我们指定的文章
    this.setData(post);

    // 读取缓存,拿到文章是否收藏的状态
    const { data: postsCollected } = await wx.getStorage({
      key: 'post_collected'
    })
    // 记录所有文章的缓存状态的对象
    this.data._postsCollected = postsCollected;
    // 拿到当前文章的收藏状态 取不到则表示这篇文章未收藏
    const collected = postsCollected[this.data._pid] || false;
    console.log(postsCollected);
    console.log(collected);
    this.setData({ collected });
  }
图片收藏切换函数
/**
   * 文章收藏点击事件的回调
   * @param {*} event  事件对象
   */
  async onCollect(event) {
    // 假设文章未收藏
    // 点击后 未收藏 -> 收藏
    // 将是否收藏的状态保存到缓存中
    // 记录那篇文章是否被收藏,还可以同时进行记录多篇文章
    // const { data: postCollected = {} } = await wx.getStorage({
    //   key: 'post_collected',
    // })
    const postCollected = this.data._postsCollected;
    console.log(postCollected);
    // 当前文章的收藏状态 取不到则肯定没有收藏过
    const collected = postCollected[this.data._pid] || false;
    // 用文章id的值作为实际存储对象的属性,属性值是是否收藏
    postCollected[this.data._pid] = !collected;
    wx.setStorageSync('post_collected', postCollected)
    this.setData({
      // 这里直接取反就可以了 上面的执行完毕这里可以直接取反了
      collected: !this.data.collected
    });
  }
功能实现

通过上面的方式,我们其实就已经实现了将收藏状态进行了本地存储,即使退出去,重新加载也会记录下以前的状态。

image-20210828214652277

image-20210828214704873

但是,这里还是有一个bug。

缓存的bug

这里这样做实际上是有一些bug的。

因为如果是用户第一次访问,那么也就没有缓存。那么我们在加载的时候,是取不到缓存中的值的。那么系统肯定是会在控制台报错的,这个问题还是有点头疼。我也没想到让我满意的解决方法。虽然这个问题不影响我们使用。回头在想想。

image-20210828215450477

优化用户收藏体验
再次完善页面

这里将字体修改一下,在全局的样式中进行更改。看起来舒服点。

/* app.wxss */
text{
  color:#666;
  font-size: 24rpx;
  font-family: 'MicroSoft Yahei';
}

image-20210828215900962

弹框提示是否收藏成功

我们可以使用小程序原生提供的某些API,组件。在用户点击收藏或者取消收藏的时候提示用户收藏成功还是取消收藏了。

只需要在点击事件的执行函数中,调用小程序提供好的弹框组件即可。

// 使用小程序默认的API 弹框组件来提示用户是收藏文章 还是取消收藏
    wx.showToast({
      // 提示文字
      title: this.data.collected ? "收藏文章成功!" : "取消收藏成功!",
      // 提示框停留时间
      duration: 2000,
        // 图标属性 icon: "success"/"error"
    })

image-20210828220949620

没错,我就是你想要的提示框

使用模态对话框

上面我们使用弹框来提示用户是否收藏成功,其实这种轻提示也是非常好的。这里我的想法是使用模态对话框来进行一次练习。实际上这种方式没有上面的那种方式好,这里仅仅作为一波小练习而已。

模态对话框的基本使用
// 模态对话框 showModel
    wx.showModal({
      title:"是否收藏文章",
      success(res){
        // 不管取消还是确定,都会执行这个回调函数,因为这个函数的执行时机就是 接口调用成功的回调函数 也就是出现了这个模态框,就一定会被执行的函数
        if(res.confirm){
          // res.confirm 为 true 时,表示用户点击了确定按钮
        }else if(res.cancel){
          // res.cancel  为 true 时,表示用户点击了取消
        }
      }
    })
模态对话框的改进

如果我们使用上面的这种方式,那么就需要将其他的代码全都放到success这个回调函数中执行。很明显这不是我们想要的。这种写法会导致代码看起来很臃肿。那么我们可以如何做呢?

const res = wx.showModal({
      title:"是否收藏文章",
    })
    console.log(res); // Promise对象

如果我们调用模态框的时候,不传递回调函数success,就可以得到这个函数的返回值,是一个Promise的对象。

很明显,可以使用async 和 await关键字了来完成异步调用了。

/**
   * 文章收藏点击事件的回调
   * @param {*} event  事件对象
   */
  async onCollect(event) {
    const res = await wx.showModal({
      title: this.data.collected?"取消收藏?":"收藏文章?",
    })
    // console.log(res);
    if (res.confirm) { // 收藏文章
      const postCollected = this.data._postsCollected;
      // console.log(postCollected);

      // 当前文章的收藏状态 取不到则肯定没有收藏过
      const collected = postCollected[this.data._pid] || false;
      // 用文章id的值作为实际存储对象的属性,属性值是是否收藏
      postCollected[this.data._pid] = !collected;
      wx.setStorageSync('post_collected', postCollected)
      this.setData({
        // 这里直接取反就可以了 上面的执行完毕这里可以直接取反了
        collected: !collected
      });
    }
  },

image-20210828225352701

image-20210828225404289

实际上想要达到的效果都是一样的。但是为了用户体验,我们还是采取最开始的弹框方式。

文章分享

注册事件
<!-- 点击触发文章分享事件 -->
      <image bind:tap="onShare" class="share-image" src="/images/icon/share.png"></image>

image-20210828225930257

功能的完成

完成文章的分享,我们实际上还是调用微信小程序提供好的内置组件。具体的更多的使用方式可以参考官方文档。

/**
   * 完成文章分享的回调函数
   * @param {*} event 
   */
  onShare(event) {
    // 调用小程序原生的组件 实现分享
    wx.showActionSheet({
      // 分享的方式(具体分享以后做什么,我们并没有做)
      itemList: ["分享到QQ", "分享到微信", "分享到微博", "分享到朋友圈"],
      success(res){
        // 通过 res.tapIndex 可以拿到我们点击了哪一个数组元素的索引
        // 想做其他事情可以做
        console.log(res.tapIndex);
      }
    })
  }

image-20210828230806200

内置的分享

实际上,我们想要分享微信小程序,也不需要自己写功能。小程序已经内置了分享功能。点击这三个点的按钮就可以看见。

image-20210828230931461

更多分享用法请参考

下一篇

微信小程序学习之旅–零基础制作自己的小程序-文章详情页音乐播放功能的实现-背景音频/全局App/页面的bug修改和优化

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐