# 小程序初探（二）

> 原文：https://pjhubs.com/2018/01/13/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%88%9D%E6%8E%A2%EF%BC%88%E4%BA%8C%EF%BC%89/
>
> 作者：PJHubs · 发布于 2018-01-13 · 标签：跨平台、iBistu、微信、小程序

iBistu 4.0终于在几位同学的努力下正式推出，我也没想到iBistu怎么就发展到了4.0，四年多过去了，还能“隐约”看到学长们的代码，这种“恍如隔世”的感觉每写下一行代码就会冒出。

在学习开发微信小程序的过程中，一直在苦于追求到底哪些项目才适合与新手小白去磨练，某天下午突然想到iBistu新闻模块的这种“不干不湿”情况正适合于练手各种前端框架（我认为小程序也是一种前端框架）

iBistu新闻模块在[API文档](https://github.com/ifLab/iBistu-API)上写的非常清楚，我们只需要传入一些参数即可获取到校内新闻的JSON数据，而对于微信小程序来说，解析JSON格式的数据是最为方便不过了。再加上iBistu 4.0简洁的UI风格，不会让我这种第一次上手前端开发的同学感到阻力很大，反而还会有一种“过五关，斩六将”的feel~😝

经过上次的小程序初探，了解了小程序的整体架构、开发要求和规范，再加上参与了iBistu 4.0的iOS端开发，预估开发成本不大且获取开发经验可观，遂，开干~

---

从iBistu的[API文档](https://github.com/ifLab/iBistu-API)中可以看到，想要获取新闻数据，我们需要传入category（新闻分类）、page（分页数）、api_key和session_token（login后获取）四个参数给api.iflab.org/api/v2/newsapi/newslist这个接口

其实这第一步就有点恶心了，最开始我想的是，把iBistu新闻模块单独抽出来，作为一个独立的小程序供大家使用，但是获取iBistu所有数据的前提是，你得登录！！！想了一会儿，有两种解决方案，要么在小程序上单独做一个login入口，要么就自己再搭一个后台，第一次获取用户iBistu用户名和密码后把这些数据都存下来，以后每次做网络请求发现token过期时在后台自动刷新token。

其实从开发成本上来说，我会毫不犹豫的选择第一种，这样会很快，但是给用户的体验非常不好，在iBistu端上要登录，用你这个小程序也要登录，单从开发者角度上来想就很扯，做完自己都不想用。

现目前暂定第二种方案，但是为了加速开发时间，这部分工作挪到寒假再做吧。🙂来看看最终的效果：
  <div align="center">    
  <img src="http://7xszq8.com1.z0.glb.clouddn.com/%E4%BA%8C%E6%8E%A2%E5%B0%8F%E7%A8%8B%E5%BA%8F1.gif" width = "50%" height = "50%" align=center />
  </div>

从完成图效果上来看，除了不能侧滑切换新闻主题外，剩余部分已经达到甚至部分细节超过了Native，而且这还是在微信开发工具里的模拟器上显示出来的效果，因为没有注册AppID，要不然就能在微信端上预览效果了。

那，为什么不注册AppID呢？因为iBistu的数据源挂在在api.iflab.org下，iflab.org并没有在国内备案，如果你在微信小程序后台注册了AppID，你就得添加可信域名，这个可信域名不但要求是添加了SSL证书还得是通过了备案的，遂，iflab.org因此GG。

不过这样也好，新闻数据因为都是开发团队里的一个学长去学校官网上爬的，学校官网本身就没有加SSL证书，通过image标签去加载拿到的imageLink还得再去小程序后台上添加一次对应的域名，加上还没SSL证书，因此也GG。_(:зゝ∠)_

---

这是iBistu新闻小程序的整体文件结构，就比微信小程序提供的开发模板多了四个文件而已，整个小程序总体大小为49KB（大家千万记住，小程序对开发文件大小限制在了1M以内）。

  <div align="center">    
  <img src="http://7xszq8.com1.z0.glb.clouddn.com/%E4%BA%8C%E6%8E%A2%E5%B0%8F%E7%A8%8B%E5%BA%8F2.png" width = "40%" height = "40%" align=center />
  </div>

  index是新闻列表页，content为列表item内容页，我们首先来拆分列表页，红色为cell中的单个标签，蓝色为一个cell

  <div align="center">    
  <img src="http://7xszq8.com1.z0.glb.clouddn.com/%E4%BA%8C%E6%8E%A2%E5%B0%8F%E7%A8%8B%E5%BA%8F3.png" width = "50%" height = "50%" align=center />
  </div>

iBistu在iOS端上采取了新建两个xib来完成两种不同样式的加载只有title和什么都有的cell，但在小程序中，我们只需要一个样式就足够了，因为新闻的整体listView和每条新闻cell都采用了flex弹性布局，如果布局中的元素hidden了能够自动“弹”回去，因此，我们的index.wxml可以这么来写cell，

```xml
<view class='contentView'>
  <view>
    <view class='cell'>
      <text class='cellTitle'>标题</text>
      <image class='cellImage' src='#图片' />
      <text class='cellContent'>内容</text>
    </view>
  </view>
</view>
```

关于顶部的tab导航栏，实际上是一个水平滚动的scroll-view（小程序也提供了跟UIScrollView一样的scroll-view喔~），关于它的元素布局我们可以在index.wxml中这么写，
```xml
<scroll-view class='titleHeadView' scroll-x='true'>
  <view class='singleHeadView'>
    <text class='titleHeadText'>tab的内容</text>
    <view class='titleHeadBottomLineView'></view>
  </view>
</scroll-view>
```
这样，我们就拿到了一个搭好初步框架的新闻主体，接下来，我们去美化它。在index.wxss中可以这么写，
```css
.contentView {
  margin-top: 30px;
}

.cell {
  display: flex;
  flex-direction: column;
  margin-top: 10px;
  border-top: 10px solid #efeff3;
}

.cellTitle {
  padding: 10px;
  font-weight: bold;
  font-size: 17px;
}

.cellImage {
  width: 100%;
  height: 200px;
}

.cellContent {
  /* padding对换行布局有冲突只能用这种傻傻的方式去写了 */
  margin-left: 10px;
  margin-top: 10px;
  margin-right: 10px;
  display: -webkit-box;
  font-size: 28rpx;
  line-height: 40rpx;
  word-break: break-all;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  text-overflow: ellipsis;
}

.titleHeadView {
  white-space: nowrap;
  position: fixed;
  background-color: #FFF;
  top: 0;
}

.singleHeadView {
  display: flex;
  flex-direction: column;
  display: inline-block;
  margin-left: 5px;
  margin-right: 5px;
}

.titleHeadText {
  font-size: 14px;
}

.titleHeadBottomLineView {
  height: 2px;
  width: 100%;
  background-color: #000;
}

```

当然，wxss的样式定义是像素级copyiBistu的，如果大家觉得不好看的话，自行修改吧。现在，我们就已经把View层的东西都整理好了，接下来要做的事情就是去做网络请求拉到数据，再填充到View层中来即可。

---

首先来看顶部的scroll-view的数据填充，从API文档中可以看到，要求我们传入分类，而不是从一个接口取得所有新闻分类的信息，也就是说，新闻的分类信息我们要本地写死，
```js
data: {
    // 新闻列表数据
    resData: [],
    titleData_en: ['zhxw', 'tpxw', 'rcpy', 'jxky', 'whhd', 'xyrw', 'jlhz', 'shfw', 'mtgz'],
    titleData_cn: ['综合新闻', '图片新闻', '人才培养', '教学科研', '文化活动', '校园人物', '交流合作', '社会服务', '媒体关注'],
    // 标记ScrollView每个item的底部lineView是否显示
    titleIsHiddens: [false, true, true, true, true, true, true, true, true, true, true],
    // 记录当前点击的ScrollView.item的下标
    titleIndex: 0,
  },
```

正式开始进行网络请求工作之前，我们还差一个也是最重要的参数未知，session_token，这个参数是用户登录后返回的字段，24小时之内未带上这个token进行请求，则失效。emmm，我的做法就是先去运行一个iBistu的Xcode工程，拿到token后再粘回来，先这么简单粗暴的做着。

对api.iflab.org/api/v2/newsapi/newslist这个接口请求数据，需要附带四个参数，拼接完的wx.request如下，
```js
// 在onLoad中写下这个函数
  wx.request({
    url: 'https://api.iflab.org/api/v2/newsapi/newslist',
    method: 'GET',
    data: {
      // 综合新闻
      category: 'zhxw',
      // 首页
      page: 0,
      api_key: getApp().globalData.api_key,
      session_token: getApp().globalData.session_token
    },
    success: function (res) {
      // 打印出请求回来的数据
      console.log(res)
    }, fail: function (res) {
    }, complete: function () {
    }
  })
```

因为有几个地方需要用到api_key和token，因此我们可以把它丢入到app.js中，然后再通过使用跟上文一样的getApp().globalData即可取到全局数据（可以说跟pch文件和NSUserDefault非常类似了。）
```js
  globalData: {
    userInfo: null,
    session_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIzYTE4OWM0NDZhOWNlMzQ0M2NjMDQ1YmQyZTM4ZDA4YyIsImlzcyI6Imh0dHBzOi8vYXBpLmlmbGFiLm9yZy9hcGkvdjIvdXNlci9zZXNzaW9uIiwiaWF0IjoxNTE1NzcwNzczLCJleHAiOjE1MTU4NTcxNzMsIm5iZiI6MTUxNTc3MDc3MywianRpIjoiZUFLZ3FkVXZUQk8xOXdldiIsInVzZXJfaWQiOjQyLCJmb3JldmVyIjpmYWxzZX0.EuW_8rxXPv-EuB1oKe9OQMMuGrEQTFuDC5QGebqP3J4',
    api_key: '3528bd808dde403b83b456e986ce1632d513f7a06c19f5a582058be87be0d8c2'
  }
```

此时，我们运行小程序，即可看到在控制台看到请求回来的信息。现在要把信息都展示到view上，我们需要在wxml中用到wx:for和wx:if这两个东西，补充完后的wx:request如下，

```js
  wx.request({
    url: 'https://api.iflab.org/api/v2/newsapi/newslist',
    method: 'GET',
    data: {
      category: 'zhxw',
      page: 0,
      api_key: getApp().globalData.api_key,
      session_token: getApp().globalData.session_token
    },
    success: function (res) {
        wx.hideLoading()

        that.setData({
          resData: res.data
        })
        console.log(res)
    }, fail: function (res) {
    }, complete: function () {
    }
  })
```

补充完的index.wxml如下，
```xml
<scroll-view class='titleHeadView' scroll-x='true'>
  <view class='singleHeadView' wx:for='{{ titleData_cn }}' catchtap='titleHeadViewTapClick' id='{{ index }}'>
    <text class='titleHeadText'>{{ item }}</text>
    <view class='titleHeadBottomLineView' hidden='{{ index==titleIndex?false:true }}'></view>
  </view>
</scroll-view>

<view class='contentView'>
<!-- catchtap含义后文讲 -->
  <view wx:for='{{ resData }}' catchtap='contentViewTapClick' id='{{ index }}' >
    <view class='cell'>
      <text class='cellTitle'>{{ item.newsTitle }}</text>
      <image class='cellImage' src='https://pjhubs.com/{{ item.newsImage }}' mode='aspectFill' wx:if='{{ item.newsImage.length > 0 }}' />
      <text class='cellContent' wx:if='{{ item.newsImage.length > 0 }}'>{{ item.newsIntro }}</text>
    </view>
  </view>
</view>
```

需要注意的是，当我们使用wx:for去动态加载wxml中的标签时，在填充数据的时候要可以使用小程序提供的遍历对象item，这个item你可以认为是C++里的迭代器，通过迭代器去访问遍历到的每一个对象中的值。当然，如果你不想用它提供的item，你可以使用当前的循环遍历index，index代表了当前循环到的下标，然后通过下标去指定输出内容。

此时再次运行小程序，即可看到新闻数据和顶部的tab数据加载出来啦~~！！！我们再进一步，点击scroll-view上的tab标签切换新闻内容！想要做到这一点，首先要给tab添加点击事件，小程序提供了冒泡事件和非冒泡事件，简单来说就是一个是触摸事件可以逐层向上传递，另外一个不能，具体使用方法大家可以去看小程序开发文档，那里的讲解更加详细。在此，根据需求我给view绑定的是非冒泡事件catchtap，实现如下

```js
titleHeadViewTapClick: function (event) {
    var that = this

    wx.showLoading({
      title: '加载中',
    })

    that.data.titleIndex = event.currentTarget.id
    this.setData({
      titleIndex: this.data.titleIndex
    })
    wx.request({
      url: 'https://api.iflab.org/api/v2/newsapi/newslist',
      method: 'GET',
      data: {
        // 根据获取点击的id来选择传入的分类字段数据
        category: this.data.titleData_en[event.currentTarget.id],
        page: 0,
        api_key: getApp().globalData.api_key,
        session_token: getApp().globalData.session_token
      },
      success: function (res) {
        wx.hideLoading()

        that.setData({
          resData: res.data
        })
      }, fail: function (res) {
      }, complete: function () {
      }
    })
  }
```

再运行工程，可以通过点击顶部tab来切换新闻啦~！！我们再往前推进，把上拉加载也做了，因为毕竟是新闻嘛，信息流的展示还是趋于给用户“无限”的感觉。在index.json中添加enablePullDownRefresh字段，开启上拉功能，

```json
{
  "enablePullDownRefresh": true
}
```

因为是上拉加载，那么必定会涉及到数据的分页，也就是前文中我们所说的page字段的作用，page字段每+1，数据就会返回时间上相对之前返回的时间更早一些的数据。我们要做的效果是追加数据，注意！是追加！更新完后的整个index.js如下所示

```js
var p = 0
// 拉取分页数据方法
var GetList = function (that) {
  wx.request({
    url: 'https://api.iflab.org/api/v2/newsapi/newslist',
    method: 'GET',
    data: {
      category: 'zhxw',
      page: p,
      api_key: getApp().globalData.api_key,
      session_token: getApp().globalData.session_token
    },
    success: function (res) {
      wx.hideLoading()
      var l = that.data.resData
      for (var i = 0; i < res.data.length; i++) {
        l.push(res.data[i])
      }
      that.setData({
        resData: l
      }); 
      p++;  
    }, fail: function (res) {
    }, complete: function () {
    }
  })
}

Page({
  data: {
    resData: [],
    titleData_en: ['zhxw', 'tpxw', 'rcpy', 'jxky', 'whhd', 'xyrw', 'jlhz', 'shfw', 'mtgz'],
    titleData_cn: ['综合新闻', '图片新闻', '人才培养', '教学科研', '文化活动', '校园人物', '交流合作', '社会服务', '媒体关注'],
    titleIsHiddens: [false, true, true, true, true, true, true, true, true, true, true],
    titleIndex: 0,
  },

  // 页面加载
  onLoad: function () {
    var that = this
    GetList(that) 
    wx.showLoading({
      title: '加载中',
    })
  },

  // 顶部tab点击事件
  titleHeadViewTapClick: function (event) {
    var that = this

    wx.showLoading({
      title: '加载中',
    })

    that.data.titleIndex = event.currentTarget.id
    this.setData({
      titleIndex: this.data.titleIndex
    })
    wx.request({
      url: 'https://api.iflab.org/api/v2/newsapi/newslist',
      method: 'GET',
      data: {
        category: this.data.titleData_en[event.currentTarget.id],
        page: 0,
        api_key: getApp().globalData.api_key,
        session_token: getApp().globalData.session_token
      },
      success: function (res) {
        wx.hideLoading()

        that.setData({
          resData: res.data
        })
        console.log(res)
      }, fail: function (res) {
      }, complete: function () {
      }
    })
  },

  // 点击新闻跳转新闻详情
  contentViewTapClick: function(event) {
    var link = this.data.resData[event.currentTarget.id].newsLink
    wx.navigateTo({
      url: '../content/content?link=' + link,
    })
  },

  onReachBottom: function () {
    //上拉  
    var that = this
    GetList(that)
  }  
})

```

大家从上文也看到了多了一个contentViewTapClick方法，这个方法就是我们后边要展开说的内容，从API文档上我们找到新闻详情接口，需要我们传入link字段数据，这个数据是个URL，问了学长，实际上就是给这个接口丢一个让它自己去实时抓数据的地址。hhhhh，这个做法确实巧妙。因此，我们需要在点击每条新闻的wx.navigationTo跳转方法时传入link字段的数据。

---

在content.js中写下，
```js
 onLoad: function (options) {
    var that = this
    wx.showLoading({
      title: '加载中',
    })
    wx.request({
      url: 'http://api.iflab.org/api/v2/newsapi/newsdetail',
      method: 'GET',
      data: {
        link: options.link,
        api_key: getApp().globalData.api_key,
        session_token: getApp().globalData.session_token
      },
      success: function (res) {
        console.log(res)
        wx.hideLoading()
      }, fail: function (res) {
      }, complete: function () {
      }
    })
  }
```

运行小程序，随便点击一条新闻，就会在控制台中打印出来了相关信息。刚开始我想偷个懒，想直接navigationTo拿到的link，没想到小程序居然不支持H5外链，只能跳转自身页面，所以只能自己拼数据了。

iBistu iOS端的新闻不是我写的，对于其中的一些实现新闻详情的巧妙方法我是一点都不了解，所以看到返回来的新闻详情数据后，整个人都不好了。

  <div align="center">    
  <img src="http://7xszq8.com1.z0.glb.clouddn.com/%E4%BA%8C%E6%8E%A2%E5%B0%8F%E7%A8%8B%E5%BA%8F4.png" width = "80%" height = "80%" align=center />
  </div>

  我的乖乖，您看懂是什么意思了么？一堆回车符的地方就是要插入图片的地方。😱。所以我们要根据回车符出现的地方来判断是否应该插入图片，看了看iBistu的新闻详情部分的实现，确实是这么做的。

  但是更伤的问题来了，根据回车符我截断字符串在数组里，然后po出了内容，一看更呆了，图片在一个地方集中出现得越多，那么这块地方的换行符也就越多，换句话说，得根据回车符的多少来决定插入的图片数量。

  嗯，其实这还不是最伤的，按照这个思路弄完了以后，浏览了前面几条新闻，完美对上了，但是！！！到了后边的几条新闻就全乱了。该出现图片的地方没出现，不该出现的图片的地方空一大片

  原因是因为刚开始找到的规则是，三个换行放一张图片，如果当前区域放超过一张图片，比如说是两张图片，换行数则由三个变成了七个，也就是说，以三位基数，每多加两个换行多一张图片。

  但事实上不是这样啊！😭。后边几条新闻的换行数跟图片数对应关系完全不符合之前找到的，全乱了。这就非常的难受了。弄到最后，实在没办法，决定了如果是多张图片就只放一张😔。  

  如果这部分你没能拿到真实数据好好研究一番的话，就算看了代码意义也不太大，
  ```js
  Page({

  /**
   * 页面的初始数据
   */
  data: {
    resData_cn: [],
    resData_image: [],
    resData_display: [],
    resDataCount: 0
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    var that = this
    wx.showLoading({
      title: '加载中',
    })
    wx.request({
      url: 'http://api.iflab.org/api/v2/newsapi/newsdetail',
      method: 'GET',
      data: {
        // 拿到NavigationTo拼接而来的参数
        link: options.link,
        api_key: getApp().globalData.api_key,
        session_token: getApp().globalData.session_token
      },
      success: function (res) {
        wx.hideLoading()
        var resString = res.data.article.split('\n')
        var resdata_cn = []
        var resdata_cn_index = -1
        var resdata_display = []
        var tempIndex = 0
        for (var i = 0; i < resString.length; i++) {
          // 判断是否含有中文，若不含有中文则为图片
          // 即换行符
          if (/.*[\u4e00-\u9fa5]+.*$/.test(resString[i])) {
            tempIndex = 0
            resdata_cn_index++
            resdata_cn[resdata_cn_index] = resString[i]
            resdata_display[resdata_cn_index] = true
          } else {
            tempIndex++
            // 这么搞新闻图片加载不全
            if (tempIndex == 3) {
              resdata_display[resdata_cn_index] = false
            }
          }
        }

        var count = resdata_cn.length + res.data.imgList.length

        var displayArr = []
        var displayIndex = 0
        for (var j = 0; j < resdata_display.length; j++) {
          if (resdata_display[j]) {
            displayArr[j] = ""
          } else {
            displayArr[j] = res.data.imgList[displayIndex]
            displayIndex ++
          }
        }

        that.setData({
          resData_image: displayArr,
          resData_cn: resdata_cn,
          resDataCount: count,
          resData_display: resdata_display,
        })
        wx.hideLoading()
      }, fail: function (res) {
      }, complete: function () {
      }
    })
  }
})
  ```

  其中设置了很多BOOL变量，为啥要设置这么多的BOOL变量呢？我们来看一张图，

  <div align="center">    
  <img src="http://7xszq8.com1.z0.glb.clouddn.com/%E4%BA%8C%E6%8E%A2%E5%B0%8F%E7%A8%8B%E5%BA%8F5.png" width = "60%" height = "60%" align=center />
  </div>

  我对content.wxml做了如上的分割，实际上也是一个listView，每一个cell（蓝色）中都有text和image，其写法如下所示，

  ```xml
<view>

<view wx:for='{{ resData_cn }}'>
  <view class='contentView'>
    <text class='contentViewText' >{{ item }}</text>
     <image class='contentViewImage' mode='widthFix' src='https://pjhubs.com/{{ resData_image[index] }}' hidden='{{ resData_display[index] }}' /> 
  </view>
</view>

</view>
  ```

通过控制image标签的hidden来达到效果，这就是为什么设置了这么多BOOL变量的原因。😝


---

以上就是我二探小程序的历程，可能说的有些不够清晰，你可以在[GitHub](https://github.com/windstormeye/iBistu-News-Mini-Program)下载源码，自行研究一番，就能够理解我以上所讲述的东西了。

这次二刷小程序，给我的感觉是以前端上觉得做起来的简单的东西，在小程序上会越发的更加简单，在端上觉得比较困难的东西，小程序说不定会有一些比较奇妙的解法，甚至会有出其不意的地方。这两次学习小程序的开发，总的来说，让我很意外，小程序不但是给用户“用完即走”的感觉，给开发者也一身轻松，不过，也只是“小程序”。
