小程序“友福图书馆”项目研究

开启项目

  • 新项目
    采用WePY开源框架开发,WePY项目的创建与使用步骤如下
    1、在全局安装WePY命令行工具,执行命令:
    npm install wepy-cli -g
    2、在开发目录中,生成Demo开发项目文件夹:
    wepy new myDemo
    3、移动到项目目录下,开始编译
    wepy build --watch

  • 开源项目
    下载一个开源的采用WePY框架开发的小程序项目,“友福图书馆”,对其进行研究学习。下载完后,打开并启动项目:
    1、移动到项目的根目录
    cd library
    2、安装npm,demo项目中可能运用到相关的库
    npm install
    3、运行
    npm run dev

下面以“友福图书馆”项目为例,进行小程序开发的研究

项目架构

把demo下载下来,编译后,结构目录变成如下所示:


QQ.png

其中,dist目录是编译后生成的,其中包含的是demo对应的小程序原生文件。在利用WePY框架开发小程序时,在src目录下进行代码的编辑。

项目结构整理

app.wpy是小程序的入口文件。
另外,将自定义的组件、配置文件、项目中用到的图片图标、小程序的各个页面、组件样式等等,进行分门别类,保存在各自的文件夹,使得项目结构清晰,且易于管理。

小程序整体架构

先预览一下这个小程序的外观呈现,如下图:


QQ.png

从功能上说,分为三大块:首页、借阅页面、个人页面。
具体代码实现是,在小程序入口文件app.wpy的APP示例中,在config对象中声明tabBar。

<script>
import wepy from 'wepy'
import 'wepy-async-function'

export default class extends wepy.app {
    config = {

      tabBar: {    //  
        color: '#AEADAD',
        selectedColor: '#049BFF',
        backgroundColor: '#fff',
        borderStyle: 'black',
        list: [{
        pagePath: 'pages/index',
        selectedIconPath: './images/tabbars/icon-mark-active@2x.png',
        iconPath: './images/tabbars/icon-mark@2x.png',
        text: '首页'
      }, {
        pagePath: 'pages/borrow',
        selectedIconPath: './images/tabbars/icon-shelf-active@2x.png',
        iconPath: './images/tabbars/icon-shelf@2x.png',
        text: '借阅'
      }, {
        pagePath: 'pages/user',
        selectedIconPath: './images/tabbars/icon-smile-active@2x.png',
        iconPath: './images/tabbars/icon-smile@2x.png',
        text: '我的'
      }]
    },
    }
}

除此之外,应该事先编辑好TabBar中包含的各个页面。这样,就实现了图中的效果。

另外,还可以在config中声明:整个项目各个页面的文件路径、整体风格样式、以及一些全局配置和全局方法等等。如下:

 pages: [
      'pages/index',
      'pages/main/search',
      'pages/main/list',
        ... ...    //  整个项目各个页面的文件路径
      'pages/user/collect'
    ],
    window: {
      navigationBarTitleText: '友福图书馆',
      navigationBarTextStyle: 'white',
      navigationBarBackgroundColor: '#049BFF',
      backgroundColor: '#eaeaea',
      backgroundTextStyle: 'light',
      enablePullDownRefresh: true    //   是否下拉刷新
    },

在APP实例中,除了config对象外,还可以设置一些项目中要用到的工具方法。注意:app.wpy中没有</template>部分。

以上是从整体的角度来看的,下面逐渐更加细节地来研究小程序的开发。这里重点看了首页的设计、自定义组件的设计与使用,以及网络工具封装使用,这3大不同类别的模块的设计。

首页

首页的文件内容总体分为3大块:<template>部分、<script>部分、</style>部分
其中,

  • 在<template>部分进行静态页面的设计
<template>
  <view class="page-index">
    <SearchBar :placeholder="searchText"></SearchBar>
    <BookList :list.sync="list" title="图书推荐"
     :loading.sync="loading" :noMore.sync="noMoreList"></BookList>
  </view>
</template>

1、组件的引入与声明
在<template>标签当中堆积、嵌套了许多组件。
其中自定义组件需要在<script>部分import进来。
如:import SearchBar from '../components/searchbar'
并要在页面示例中的component对象中进行声明,如:

    components = {
      SearchBar,    //命名与import的一致
    }

2、组件的属性设置
在首页中,组件的属性值来自页面示例中data对象声明的属性。

    <SearchBar :placeholder="searchText"></SearchBar>

      ... ...

    data = {
      searchText: '搜索图书',
    }

另外,进行了父组件与子组件的属性的数据绑定,用到的语法是:props.sync,这样实现了属性的动态设置。

3、“推荐图书”列表组件

<BookList :list.sync="list" title="图书推荐"
     :loading.sync="loading" :noMore.sync="noMoreList"></BookList>

在首页中,主体是一个类似listView的组件。但是,小程序中并没有封装好这么一个组件给我们直接使用。这就涉及到了自定义组件了,后面再详细来看。
这里要说的是,在首页的页面示例中,给这个组件提供数据,这部分是在<script>部分处理的。

  • <script>部分进行变量声明、逻辑交互及事件处理等

1、给组件提供数据
接着上面的思路,如何给自定义组件BookList设置数据?
首先在页面示例的data对象中,声明相关数据变量,因为涉及到网络请求数据,并分页加载,这里添加了以下变量。

data = {
      noMoreList: false,
      loading: false,
      list: [],    //  装载请求回来的数据
      page: 1   // 分页加载页数
    }

同时,添加请求数据的方法updateBookList(page) { }、下拉刷新的方法onPullDownRefresh() { }、加载更多触发方法onReachBottom() { },方法的具体内容看需求来决定,这里不多说。什么时候开始网络请求呢?

    onReady() {
      //  页面准备加载的时候,触发
    }

2、事件处理
这里,没有看到一些类似于按钮点击的数据处理,因为这在自定义组件中完成了。这是一种MVVM的设计模式。这也是采用WePY框架开发获得的一个好处。

  • </style>部分引入样式

在首页文件中,这部分代码不多。

<style lang="less">
.page-index{
  // some style
}
</style>

因为这里lang="less"标志是表示,引入了全局的公共样式page-index,其中包含了首页需要用到的样式,这在别的文件中已经编辑好了。所以,在首页直接引用即可。

到此,首页的研究大概完毕。下面看小程序中自定义组件的设计。

自定义组件

这里,以自定义的搜索栏组件为例,进行研究。
自定义组件的文件内容也分成3部分:<template>部分、<script>部分、</style>部分,与页面文件相似。
1、与页面的不同点
自定义组件示例继承wepy.component基类,而页面示例继承自wepy.page基类。另外,自定义组件中一般没有设置onLoad等表示组件周期的方法。

2、逻辑交互与事件处理
关于自定义组件,上面已经说了有关父子组件传值的问题了,这里主要看组件中的逻辑交互与事件处理。
如:搜索按钮

//  在template部分
<view class="weui-search-bar__cancel-btn" hidden="{{!inputShowed}}" @tap="search">
搜索
</view>

这里用系统组件view表示搜索按钮,并在其中绑定了一个点击事件。语法是@tap="search"。其中,search方法在部分的method对象中声明

methods = {
      search () {
        const params = {
          keyword: this.inputVal || this.placeholder
        }
        wx.navigateTo({
          url: `/pages/main/list?params=${JSON.stringify(params)}`
        })
      },
}

可以看出,其中的逻辑功能处理包括传值、页面跳转。

3、页面之间的传值,页面跳转
执行微信小程序系统提供的方法wx.navigateTo,参数url表示下一个页面的路径,并且可以带上传递的值

    wx.navigateTo({
          url: `/pages/main/list?params=${JSON.stringify(params)}`
    })

下一个页面怎么接收传过来的值呢?在下一个页面的页面示例中,方法是:

onLoad(query) {
      let params = query && query.params
      try {
        params = JSON.parse(params)
      } catch (e) {
        params = {}
      }
      this.params = params   // 保存在data对象中声明的属性params中
}

网络工具

1、在.js文件中封装网络请求工具,继承的基类是wepy.mixin。

import wepy from 'wepy'
import { service } from '../config.js'

export default class httpMixin extends wepy.mixin {

  $get(
    {url = '', headers = {}, data = {} },
    {success = () => {}, fail = () => {}, complete = () => {} }
  ) {
    const methods = 'GET'
    this.$ajax(
      {url, headers, methods, data},
      {success, fail, complete }
    )
  }

  $post(
    {url = '', headers = {}, data = {} },
    {success = () => {}, fail = () => {}, complete = () => {} }
  ) {
    const methods = 'POST'
    this.$ajax(
      {url, headers, methods, data},
      {success, fail, complete }
    )
  }
}
$ajax(
    {url = '', headers = {}, methods = 'GET', data = {} },
    {success = () => {}, error = () => {}, fail = () => {}, complete = () => {} }
  ) {
    // 增强体验:加载中
    wx.showNavigationBarLoading()

    // 构造请求体
    const request = {
      url: url + '?XDEBUG_SESSION_START=1',
      method: ['GET', 'POST','PUT', 'DELETE'].indexOf(methods) > -1 ? methods : 'GET',
      header: Object.assign({
        'Authorization': 'Bearer ' + wx.getStorageSync('token'),
        'X-Requested-With': 'XMLHttpRequest'
      }, headers),
      data: Object.assign({
        // set something global
      }, data)
    }

    // 控制台调试日志
    console.table(request)

    // 发起请求
    wepy.request(Object.assign(request, {
      success: ({ statusCode, data }) => {
        // 控制台调试日志
        console.log('[SUCCESS]', statusCode, typeof data === 'object' ? data : data.toString().substring(0, 100))

        // 状态码正常 & 确认有数据
        if (0 === +data.code && data.data) {
          // 成功回调
          return setTimeout(() => {
            this.isFunction(success) && success({statusCode, ...data})
            this.$apply()
          })
        } else if (data.code == 2) {
          // 删除过时token
          wx.removeStorageSync('token', null)

          // 重新登录
          wepy.login({
            success: (res) => {
              console.log('wepy.login.success:', res)

              // 根据业务接口处理:业务登陆:异步
              this.$post({ url: service.login, data: {code: res.code} }, {
                success: ({code, data}) => {
                  if(data.token){
                    wx.setStorageSync('token', data.token)
                  }

                  var route = '/' + getCurrentPages()[0].__route__;

                  if(route == '/pages/user/register'){
                    return
                  }

                  if (!data.token ){
                    // wx.reLaunch({url: '/pages/user/register'})
                    wx.navigateTo({url: '/pages/user/register'})
                  }else{
                    wx.reLaunch({url: route})
                  }
                }
              })
            },
            fail: (res) => {
              console.log('wepy.login.fail:', res)
            }
          })

        } else {
          // 失败回调:其他情况
          return setTimeout(() => {
            /* if(this.isFunction(fail)) {
              fail({statusCode, ...data})
              this.$apply()
            }else{ */
              wx.showModal({
                title: '操作错误',
                content: data.message,
                showCancel: false
              })
            // }
          })
        }

      },
      fail: ({ statusCode, data }) => {
        // 控制台调试日志
        console.log('[ERROR]', statusCode, data)
        // 失败回调
        return setTimeout(() => {
          this.isFunction(error) && error({statusCode, ...data})
          this.$apply()
        })
      },
      complete: (res) => {
        // 控制台调试日志
        //console.log('[COMPLETE]', res)
        // 隐藏加载提示
        wx.hideNavigationBarLoading()
        // 停止下拉状态
        wx.stopPullDownRefresh()
        // 完成回调
        return (() => {
          this.isFunction(complete) && complete(res)
          this.$apply()
        })()
      }
    }))
  }

2、网络工具的使用
先引入工具方法所在的文件
import http from '../mixins/http'
在mixins对象做声明
mixins = [http]
调用工具方法,如,请求图书数据

    updateBookList(page) {

      this.loading = true
      // 请求列表
      this.$get({
        url: 'http://www......',
        data: {
          // 默认从1开始为第一页
          page: page
        }
      }, {
        success: ({code, data}) => {
          //  成功回调 逻辑处理
          data = data.data
          ... ... 
        },
        fail: ({code, data}) => {
          // 失败回调
        },
        complete: () => {
          this.loading = false
        }
      })
    }

总结

了解了代码的组织,找到各个主要逻辑/功能模块与代码文件之间的对应关系,通过代码分析走通几个关键的、有代表性的执行流程,挑选感兴趣的“枝干”代码来阅读(网络工具代码)。总体来说,对于微信小程序的开发有了进一步的认识。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,144评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,657评论 18 139
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,104评论 4 62
  • 乐宝一向脾气好,平时玩的时候基本是她让着哥哥,玩具被拿走也不介意,经常被哥哥有意无意的打一两下也不吭声,哥哥能做很...
    Driftweed阅读 290评论 1 1
  • 春节长假过半,朋友圈发布的旅行图文也陆续到了高潮阶段,相信大家的朋友圈也各自"精彩"。 但是,追朋友圈发布好比追剧...
    米泥泥阅读 648评论 0 0