组件化框架 WePY 开发入门(二) —— 博客首页文章列表重构


上篇教程,学院君已经给大家初步介绍了基于小程序组件化框架 WePY 的项目初始化和目录结构,今天开始我们将花几篇教程的篇幅通过 WePY 框架来重构之前通过原生框架开发的博客应用小程序版,并且在此基础上实现用户授权登录及点赞功能,最后将这个小程序上线,从而演示完整的小程序开发生命周期。

重构的时候,后端接口不用做任何调整,只需要开发小程序页面即可。

我们在 PHPStorm 中打开上篇教程创建的全新小程序项目 blog-lite,在 WePY 框架中开发小程序页面非常简单,就和在 Vue 中编写单文件组件一样,打开首页页面文件 src/pages/index.wpy,先清空页面代码。

编写列表项组件

和原生框架一样,我们为首页文章列表项编写独立组件,在 src/components 目录下新建一个 card.wpy,并编写组件代码如下(其实就是把原来原生框架实现代码合并过来):

<style lang="less">
    @font-face {
        font-family: "iconfont";
        src: url('iconfont.eot?t=1545317804991');
        /* IE9*/
        src: url('iconfont.eot?t=1545317804991#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'), url('iconfont.ttf?t=1545317804991') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ url('iconfont.svg?t=1545317804991#iconfont') format('svg');
        /* iOS 4.1- */
        font-weight: normal;
        font-style: normal;
    }

    .card {
        -webkit-font-smoothing: antialiased;
        background-color: #fff;
        padding: 25rpx 25rpx;
        margin-bottom: 30rpx;
        overflow: hidden;
        color: #333;
        .title {
            font-size: 18px;
            -webkit-line-clamp: 2;
            line-height: 1.2;
            overflow: hidden;
            text-overflow: ellipsis;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
        }
        .content-area {
            display: -webkit-box;
            .left-area {
                -webkit-box-orient: vertical;
                -webkit-box-flex: 1;
                .card-content {
                    line-height: 1.3;
                    font-size: 14px;
                    margin: 10rpx 0;
                    font-family: HelveticaNeue-Light;
                    overflow: hidden;
                    color: #666;
                    text-overflow: ellipsis;
                    display: -webkit-box;
                    -webkit-line-clamp: 3;
                    -webkit-box-orient: vertical;
                }
                .info-area {
                    font-size: 10pt;
                    color: #666;
                    opacity: .8;
                    font-family: 'iconfont';
                    .posted-date {
                        &:before {
                            content: '\e64e';
                            margin-right: 10rpx;
                        }
                    }
                    .views {
                        margin-left: 30rpx;
                        &:before {
                            content: '\e666';
                            margin-right: 10rpx;
                        }
                    }
                }
            }
            .right-area {
                margin-left: 20rpx;
            }
            .thumbnail {
                width: 150rpx;
                height: 150rpx;
                background-color: #eee;
                margin-top: 26rpx;
            }
        }
    }

</style>
<template>
    <view class="card">
        <text class="title">{{title}}</text>
        <view class="content-area">
            <view class="left-area">
                <text class="card-content">{{content}}</text>
                <view class="info-area">
                    <text class="posted-date">{{date}}</text>
                    <text class="views">{{views}}</text>
                </view>
            </view>
            <view class="right-area">
                <image src='{{thumbnail}}' class="thumbnail" mode="aspectFill"/>
            </view>
        </view>
    </view>
</template>
<script>
    import wepy from 'wepy'

    export default class Card extends wepy.component {
        props = {
            title: String,
            content: String,
            category: String,
            date: String,
            views: Number,
            thumbnail: String
        }
    }
</script>

单独来看,和原生框架中的代码几乎一样,不同之处主要有两个:

  • 将之前 WXML、WXSS、JS 分别存放在不同文件不同,WePY 框架通过统一的单文件来定义组件代码,而且页面结构和 Vue 单文件组件一模一样;
  • 在原生框架中通过 WXSS 定义样式代码,这里通过 Less 语法来定义样式代码,此外还支持 Sass 语法,这一点和 Vue 单文件组件也一样。

如果你看过原来原生框架的实现,同时对 Vue 有比较熟悉,理解上述组件代码应无大碍。下面我们来定义首页页面,并在页面中引入刚刚定义的组件。

编写博客首页

首页的页面结构和组件一样,也是分为样式、模板和脚本三部分。

编写样式代码

打开 src/pages/index.wpy,编写样式代码如下:

<style lang="scss">
  .container {
    background-color: #eee;
  }
  .title_en {
    position: absolute;
    bottom: 0;
    color: #fff;
    z-index: 2;
    padding: 20px;
  }
  .cards-area {
    margin-top: 30rpx;
    width: 100%;
    .date{
      color: #666;
      text-align: center;
      margin-bottom: 10rpx;
      opacity: .8;
      &:before{
        position: absolute;
        content: "————";
        left:450rpx;
        right:0;
      }
      &:after{
        position: absolute;
        content: "————";
        left:0;
        right:450rpx;
      }
    }
  }
  .info{
    color:gray;
    opacity:.8;
    margin-bottom: 20rpx;
    &.loading{
      width: 70rpx;
      height: 70rpx;
    }
  }
</style>

这里我们标注样式标签属性 langscss,表示这里通过 Sass 语法定义页面样式,具体的样式代码和之前原生框架一样。

编写模板代码

接下来,我们在 index.wpy 中编写博客首页模板代码,和 Vue 组件一样,通过 template 标签来定义模板代码:

<template>
  <view class="container">
    <view class="cards-area">
      <repeat for="{{articles}}">
        <view @tap="tap('{{item.id}}')" wx:if="{{item.id}}">
          <card :title="item.title" :content="item.summary" :category="item.category" :date="item.posted_at" :views="item.views" :thumbnail="item.thumb"/>
        </view>
      </repeat>
    </view>
    <text class="info" wx:if="{{info}}">{{info}}</text>
    <image wx:else class="info loading" src="../resources/assets/loading.gif"/>
  </view>
</template>

基本代码和原生框架也是一样,这里我们引入了 card 组件,用于渲染文章列表项,此外在 WePY 框架中,我们通过 repeat 实现组件循环渲染

编写业务逻辑代码

页面主要逻辑通过 JS 代码来定义,我们在页面 JS 脚本中引入 card 组件,定义页面渲染用到的变量,以及通过 API 获取文章列表数据并将其同步到变量中,此外还定义了上划翻页和首页分享方法,在 index.wpy 最后追加 JS 代码如下:

<script>
    import wepy from 'wepy'
    import Card from '../components/card'

    export default class Index extends wepy.page {
        config = {
            navigationBarTitleText: 'Laravel学院',
            window: {
                enablePullDownRefresh: true
            }
        }

        components = {
            card: Card
        }

        data = {
            articles: [],
            isLoadingMore: false,
            currentPage: 1,
            info: ''
        }

        methods = {
            tap(id) {
                wx.navigateTo({
                    url: `/pages/post?id=${id}`  // 打开一个新的同路由页面,但指定不同的数据初始值
                })
            }
        }

        onLoad() {
            wx.showLoading({
                title: '加载中'
            })
            this.loadList()
        }

        onPullDownRefresh() {
            wx.stopPullDownRefresh()
        }

        onReachBottom() {
            this.currentPage++
            if (this.currentPage >= 20) { // 最多只能加载20页
                this.isLoadingMore = false
                this.info = '没有更多文章了'
                this.$apply()
                return
            }
            this.isLoadingMore = true
            this.loadList()
        }

        onShareAppMessage() {
            return {
                title: 'Laravel学院',
                path: '/pages/index'
            }
        }
        loadList() {
            wx.request({
                url: `https://blog.laravelacademy.org/api/v1/articles?page=${this.currentPage}`,
                success: (res) => {
                    if (res.data.message === 'success') {
                        this.articles = this.articles.concat(res.data.articles)
                    } else {
                        this.info = '加载文章列表失败,请重试'
                    }
                    this.$apply()
                    wx.hideLoading()
                }
            })
        }
    }
</script>

具体的业务逻辑功能和原生框架中差不多,只是在 WePY 框架具体实现细节略微有些出入。

重新编译小程序

编写好上述页面代码后,就可以重新编译小程序了,在此之前,先安装以下 NPM 依赖以便可以正常编译 Sass:

npm install node-sass
npm install wepy-compiler-sass

然后我们开启前端资源自动编译:

wepy build --watch

打开微信开发者工具,进入 blog-lite 小程序项目,即可在页面左侧预览区域看到基于 WePY 框架重构的博客首页了:

基于WePY框架重构的小程序版博客首页

和原生框架渲染效果和功能特性一模一样。

下一篇我们将继续基于 WePY 框架重构博客详情页。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 组件化框架 WePY 开发入门(一) —— 项目初始化和目录结构

>> 下一篇: 组件化框架 WePY 开发入门(三) —— 博客文章详情页重构