[技術博客] 小程序前端開發流程——用實例介紹

做者:李大前端

主題

  • 本文的目標在於簡單介紹一下咱們在開發小程序時的前端開發流程。

前端功能

  • 前端的功能在於給數據提供一個合適的容器,並提供用戶-界面-後端的交互支持。
  • 據此,能夠簡單地把前端開發劃分爲
    • UI實現
    • 交互邏輯實現
    • 後端接口對接
  • 下文舉例分別描述上面三個過程

UI實現

  • 咱們在實現UI的過程當中使用先設計原型然後根據原型進行實現的方式
  • 下面以beta階段先加入的討論區功能實現爲例進行說明
  1. 使用墨刀進行原型設計,由PM完成

  1. 對照原型進行頁面拆解
    • 能夠看到此頁面能夠分割爲,頂部的提示語句,和中間的橫向滾動卡片以及下面的輸入框,只有中間能夠滾動,上下都是固定的
  2. UI實現
  • 接下來便挨個實現全部元素便可,原型上的各類參數(距離、大小、字號等等)都是能夠測量
  • 使用的工具基本就是微信小程序支持的一套佈局邏輯語言

  • 例如上面的圖標根據測量值便可在css中定義樣式爲
.icon{
  width:78rpx;
  height:78rpx;
  border-radius: 100%;
  margin-left:26rpx;
  margin-top:40rpx;
}
  • 如法炮製,結合flex layout將卡片拆解爲行、列,再在各級下分別完成佈局調整,最終便可完成整個卡片的佈局
  • 通過細微設計調整,實現的效果、完整的xml和css以下

<view style="background:{{'#FAFAFA'}}">
  <view class="dicussion_title" id="title_bar">這裏是 {{club_name}} 的討論區</view>  
    
    <view class='swiper_container' style='height: {{swiperHeight + "px"}}'>   
      <swiper indicator-dots="{{indicatorDots}}" autoplay="{{autoplay}}" circular="{{circular}}" vertical="{{vertical}}" interval="{{interval}}" duration="{{duration}}" style='width:100%;height:100%' current="{{question_id}}" next-margin='28rpx' previous-margin='28rpx' bindchange='onSlideChanged' >
        <block wx:for="{{qa_list}}" wx:for-index="q_index">
          <swiper-item>
          <scroll-view scroll-y style='height: {{swiperHeight}}px' bindscroll="scroll" scroll-top='{{scrollTop}}' scroll-with-animation='{{true}}' scroll-into-view='{{scroll_into_view}}'>   
            <view class="qa_list" id='qa_list_border'>
              <view class="qa_container">
                <view>
                  <image src="{{qa_list[q_index].q_raiser_icon}}" class='icon'></image>
                </view>
                <view class="vertical_flex">
                  <view class="user_name_text">{{qa_list[q_index].q_raiser_name}}</view>
                  <view class="qa_text qa_text_base">{{qa_list[q_index].ques_text}}</view>
                </view>
              </view>
              <view class="ques_time">
                <view>
                  <image src="/images/icons/qa/question.png" class='q_icon'></image>
                </view>
                <view class="ques_time_text">提問於 {{qa_list[q_index].ques_time}}</view>
              </view>
              <view class="div_line_full"></view>
              <view wx:if="{{qa_list[q_index].ans_list.length==0}}">
                <view class="no_ans_text">暫時無人理會,你能幫幫TA嗎?</view>
              </view>
              <view wx:for="{{qa_list[q_index].ans_list}}" wx:for-index="i" wx:for-item="ans">
                <view class="qa_container" id="qa_mark">
                  <view>
                    <image src="{{ans.a_raiser_icon}}" class='icon'></image>
                  </view>
                  <view class="vertical_flex">
                    <view class="user">
                      <view class="user_name_text">{{ans.a_raiser_name}}</view>
                      <view style="margin-top:36rpx;margin-left:12rpx" wx:for="{{ans.special_tag}}" wx:for-item="tag">
                        <van-tag wx:if="{{tag == '置頂'}}" color="#F44336" size="club_tag">
                          <text class="tag-font">置頂</text>
                        </van-tag>
                        <van-tag wx:if="{{tag != '置頂'}}" color="#42A5F5" size="club_tag">
                          <text class="tag-font">{{tag}}</text>
                        </van-tag>
                      </view>
                    </view>
                    <view class="ans_time_text qa_text_base">{{ans.ans_time}}</view>
                    <view class="qa_text qa_text_base">{{ans.ans_text}}</view>
                  </view>
                </view>
                <view class="like" data-q_index='{{q_index}}' data-a_index='{{i}}' bindtap='likeBtnClicked'>
                  <view>
                    <image src="/images/icons/qa/liked.png" class='like_icon' wx:if="{{ans.liked}}"></image>
                    <image src="/images/icons/qa/like.png" class='like_icon' wx:if="{{!ans.liked}}"></image>
                  </view>
                  <view class='like_cnt'>{{ans.like}}</view>
                </view>
                <view class=" div_line " wx:if="{{i != qa_list[q_index].ans_list.length - 1}}"></view>
              </view>
            </view>
            <view class="bottom_margin_large"></view>
            </scroll-view>
          </swiper-item>
        </block>
      </swiper>

    </view>

  <view class="commenter" id='comment_bar' style='bottom:{{commenter_position}}px'>
    <view class="input_place">
      <input placeholder="快來討論這個問題吧,4~40字" value="{{ans_input}}" style='width:466rpx;margin-top:4rpx;' cursor-spacing="32rpx" bindinput='inputTyping' adjust-position="{{false}}" bindfocus="focused" bindblur="blurred"></input>
    </view>
    <view class="submit_btn" bindtap='submitAnswer' data-question_id='{{q_index}}'>發送</view>
  </view>
</view>
/*commenter*/
.commenter{
  position: fixed;
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: center;  
  margin-right: 0rpx;
  margin-top:0rpx;
  margin-bottom:0rpx;
  background-color:#FFFFFF;
  border-top-color: #BBBBBB;
  border-top-width: 2rpx;
  border-top-style: solid;
}
.input_place{
  margin-left: 16rpx;
  width:530rpx;
  height:64rpx;
  border-radius: 64rpx;
  font-size:28rpx;
  display: flex;
  flex-direction: row;
  flex-wrap:wrap;
  justify-content: flex-start;
  align-items: center;
  background-color: #EEEEEE;
  color: #888484;
  padding-left:32rpx;
  border-style:solid;
  border-width:16rpx;
  border-color:#FFFFFF;
}
.submit_btn{
  margin-left: 4rpx;
  margin-right: 24rpx;
  width: 120rpx;
  height: 64rpx;
  display: flex;
  flex-direction: row;
  flex-wrap:wrap;
  justify-content: center;
  align-items: center;
  background-color: #F44336;
  color: #FFFFFF;
  font-size: 28rpx;
  border-radius: 32rpx;
}
/*commenter end*/


.swiper_container{
  overflow: hidden;
  position: fixed;
  width:100%;
  background-color: #FAFAFA;
}
.icon{
  width:78rpx;
  height:78rpx;
  border-radius: 100%;
  margin-left:26rpx;
  margin-top:40rpx;
}
.vertical_flex{
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  flex-wrap: nowrap;
}
.user_name_text{
  font-size:30rpx;
  font-weight: 500;
  margin-top:45rpx;
  margin-left:14rpx;
}
.qa_time{
  margin-left:18rpx;
  color:#AAAAAA;
  font-size:18rpx;  
}
.qa_list{
  background: #fff;
  margin-top:10rpx;
  margin-left:12rpx;
  margin-right:12rpx;
  margin-bottom:20rpx;  
  display: flex;
  flex-direction: column;
  justify-content: center;
  border-radius: 24rpx;
}
.border_backup{
  border-radius: 16rpx;border-color: #BBBBBB;border-width: 2rpx;border-style: solid;
}
.first_qa_top_margin{
  margin-top:20rpx;
}
.regular_top_margin{
  margin-top:32rpx;  
}
.dicussion_title{
  display: flex;
  justify-content: center;
  font-size:26rpx;
  color:#939090;
  padding-top:26rpx;
  font-weight: 400;
}
.qa_container{
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: flex-start;
  align-items: flex-start;  
}
.qa_text_base{
  margin-left : 18rpx;
  margin-right: 36rpx;
}
.qa_text{
  margin-top:14rpx;
  font-size: 26rpx;
  font-weight: 500;
  line-height: 36rpx;
}
.ques_time{
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: flex-end;
  align-items: center;  
  margin-right: 45rpx;
  margin-top:28rpx;
  margin-bottom:8rpx;
}
.q_icon{  
  margin-top:8rpx;
  width: 40rpx; 
  height: 40rpx; 
  border-radius: 0%;
}
.ques_time_text{ 
  margin-left:8rpx;
  color:#888484;
  font-size:22rpx;  
}
.ans_time_text{
  color:#888484;
  font-size:18rpx; 
}
.zero_ans_text{
  margin-top:14rpx;
  font-size: 24rpx;
  line-height: 45rpx;
  color: #BBBBBB;  
}
.div_line_full{
  height: 2rpx;
  width: 100%;
  background-color:#EEEEEE;
}
.div_line{
  height: 2rpx;
  width: 90%;
  background-color:#EEEEEE;
  margin-left: 5%;
}
.like{
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: flex-end;
  align-items: center;
  margin-right:40rpx;
  margin-bottom: 14rpx;
}
.like_cnt{
  font-size: 30rpx;
  color: #101010;
  margin-left:6rpx;
}
.like_icon{
  margin-top:4rpx;
  width: 36rpx; 
  height: 36rpx; 
  border-radius: 0%;
}
.user{
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: flex-start;
  align-items: center;
}
.tag-font{
  font-size: 24rpx;
  color: #FFFFFF;
}
.no_ans_text{
  display: flex;
  justify-content: center;
  font-size:24rpx;
  color:#BBBBBB;
  margin-top:32rpx;
  font-weight: 400;
  margin-bottom: 24rpx;
}
::-webkit-scrollbar {
  width: 0;
  height: 0;
  color: transparent;
}
.bottom_margin_large{
  background-color: #FAFAFA;
  width : 100%;
  height : 80rpx;
}
  1. 交互邏輯實現
  • 準確來講,交互邏輯的一部分是由組件提供的,並非從頭開始實現的,所以選擇一個合適的組件每每能大量減小工做量。
  • 原型設計中咱們但願卡片是能夠橫向滾動的,卡片內部是能夠上下滾動看到更多信息的,要實現這兩個邏輯,翻閱了較多官方和第三方的組件庫後,咱們使用了swiper組件和scroll-view組件
    • swiper組件是一個輪播圖容器,容器中裝了一個list,能夠進行左右滑動,雖然通常都裝的是圖片,但通過必定設置是能夠在其中填入咱們上一部分中實現的卡片的。
    • scroll-view定義了頁面的可滾動區域,須要指定height做爲滾動範圍的長度
  • 滾動條細節實現以下
<scroll-view scroll-y style='height: {{swiperHeight}}px' bindscroll="scroll" scroll-top='{{scrollTop}}' scroll-with-animation='{{true}}' scroll-into-view='{{scroll_into_view}}'> 
</scroll-view>
  • 能夠看到height裏引用了js裏的變量,即咱們使用了動態獲取頁面元素的大小計算出卡片的大小從而精確設置可滾動範圍,對應的js函數以下
wx.getSystemInfo({
      success: function (res) {
        wx.createSelectorQuery().select('#title_bar').boundingClientRect(function (rect) {
          var title_bar_bottom = rect.bottom
          that.setData({
            scrollHeight: res.windowHeight - title_bar_bottom,
            swiperHeight: res.windowHeight - title_bar_bottom - comment_bar_height - 10
          })
        }).exec();
      }
    });
  • 其餘交互邏輯的實現也很相似,都是經過js和xml數據動態綁定設置實現web

    • 在此頁面實現的邏輯以下
    1. swiper容器左右滑動時離開當前卡片時將當前卡片自動滾回頂部,經過scroll-top屬性實現
    2. 點贊按鈕按下時圖標變黑,其後的數字+1,經過bindtap函數實現
    3. 用戶點擊輸入框後輸入框位移到鍵盤頂部,離開輸入框後移回底部,經過動態獲取鍵盤高度從而設置輸入框離底部距離實現
    4. 用戶評論後當前卡片自動滾動到此條評論,經過scroll-to-view動態尋找最新評論然後滾動實現
    5. 沒有評論時提示「暫時無人理會」,經過渲染前確認對應數據列表長度是否爲0實現
  • 開發中筆者參考的組件庫有WeUI,Vant,WuxUI,WussUI。每次須要實現新的交互邏輯時都先翻閱一下組件庫尋找能夠套用的交互模式,或者抽取多個組件的部分進行嵌套使用改造,最終拼湊實現出一個完整的功能。json

    • 下面再舉一個較複雜的例子展現組件的嵌套使用
    • 在管理員管理頁面中,咱們有這樣的交互邏輯:
      • 右下角懸浮一個+按鈕,點擊後從頂部彈出搜索框蒙層,在搜索框中搜索用戶,進行管理員添加
      • 截圖以下


  • 這樣的邏輯中使用了popup,search-bar和一些佈局的庫元素,後續還將加入確認對話框(點擊添加後彈出),是一個較複雜的嵌套佈局了。xml部分以下,能夠看到,wux庫的popup組件抽象了頂部彈出邏輯,weui-searchbar抽取了搜索部分,最後綁定到懸浮按鈕上經過Bindtap完成呼出邏輯。總體實現代。
<wux-popup position="top" visible="{{ pop_up_tab_visible }}" bind:close="pop_up_tab_set_invisible">
    <view class="weui-search-bar ">
      <view class="weui-search-bar__form">
        <view class="weui-search-bar__box">
          <icon class="weui-icon-search_in-box" type="search" size="14"></icon>
          <input type="text" class="weui-search-bar__input" placeholder="輸入用戶id搜索" value="{{inputVal}}" focus="{{inputShowed}}" bindinput="inputTyping" />
          <view class="weui-icon-clear" wx:if="{{inputVal.length > 0}}" bindtap="clearInput">
            <icon type="clear" size="14"></icon>
          </view>
        </view>
        <label class="weui-search-bar__label" hidden="{{inputShowed}}" bindtap="showInput">
          <icon class="weui-icon-search" type="search" size="14"></icon>
          <view class="weui-search-bar__text">輸入用戶id搜索</view>
        </label>
      </view>
      <view class="weui-search-bar__cancel-btn" hidden="{{!inputShowed}}" bindtap="hideInput" style='font-size:30rpx;padding-top:5rpx'>取消</view>
    </view>
    <view class="weui-cells searchbar-result" style='border-radius:16rpx' wx:if="{{inputVal.length > 0}}">
      <view class="weui-cells_after-title">
        <block wx:for="{{search_result_list}}" wx:for-index="key">
          <view class="weui-cells {{i==0? 'weui-cells_after-title' : ''}}" style='margin-top:0rpx;margin-bottom:0rpx;'>
          <view class="weui-cell" style='background-color:#f6f6f6' >
            <view class="weui-cell__hd" style="position: relative;margin-right: 20rpx;">
              <image src="{{item.usr_icon}}" style="width: 60rpx; height: 60rpx; display: block; border-radius:50%;" bindtap='jumpToUserDetailFromSearchResult' data-usr_index="{{key}}" />
            </view>
            <view class="vertical_split">
              <view class="weui-cell__bd" bindtap='jumpToUserDetailFromSearchResult' data-usr_index="{{key}}">
                <view class="username" style="font-size: 26rpx;">{{item.name}}</view>
                <view style="font-size: 18rpx;color: #888888;">學號:{{item.student_id}}</view>
              </view>
              <view class="btn remove_btn search_result_btn" data-usr_index="{{key}}" bindtap='added'>添加</view>
            </view>
          </view>
          </view>
        </block>
        <view class="bottom_margin" style='background-color:#f6f6f6;height:16rpx' ></view>
      </view>
    </view>
  </wux-popup>
</view>
  • 最後,交互邏輯中還要定義頁面跳轉邏輯,即點擊某個view後跳轉到什麼頁面,要傳遞什麼數據。接受頁面要對應地解析傳來的數據,在onLoad時完成初始化邏輯。
  • 以社團頁面中點擊簡略信息跳轉到社團詳情頁的跳轉邏輯爲例:
//club_main.js
jumpToClubDetailDescription: function (e) {
    var club_id = e.currentTarget.dataset.club_id
    wx.navigateTo({
      url: '/pages/club_detail/club_detail?club_id=' + club_id
    })
  },
 
 //club_detail.js
 onLoad: function(options) {
    let that = this
    var club_id = options.club_id
    that.setData({
      club_id:club_id
    })
    that.request_club_detail(that, club_id)
 //省略許多其餘與此處展現無關的初始化邏輯
 }

後端接口對接

  • 沒有後端提供的數據,前端佈局就是空殼。許多邏輯交互也是須要先後端配合的。
  • 此部分邏輯就很簡單純粹了,無非就是根據先後端商議好的接口經過request進行數據請求,解析,綁定
  • 因爲咱們有用戶系統,咱們首先將request進行了一層cookies封裝,然後再進行具體接口的數據請求
  • 例:獲取用戶關注的全部社團的簡略信息列表後綁定到followed_club_list中,然後xml中對應地進行解析渲染
request_followed_club_list: function (that) {
    util.$get('/clubs/followed')
      .then((res) => {
        var followed_club_list = res.data.data.clubs_list
        for (var i = 0; i < followed_club_list.length; i++) {
          followed_club_list[i].icon_url = baseImageServerUrl + followed_club_list[i].icon_url
        }
        that.setData({
          followed_club_list: followed_club_list
        })
      })
  },
  • request封裝以下
function baseRequest(params, method) {
  let promise = new Promise((resolve, reject) => {
    let url = params.url
    let data = params.data
    wx.request({
      url: url.indexOf('http') !== -1 ? url : baseUrl + url,
      data: data,
      method: method,
      header: {
        'content-type': 'application/json',
        'cookie': wx.getStorageSync('cookie')
      },
      success(res) {
        if (res.data.success) {
          resolve(res)
        } else if (res.data.code === status.STATUS_CODE.ACCESS_NOT_LOGIN) {
          wx.redirectTo({
            url: '/pages/login/login?not_first=true',
          })
        } else {
          resolve(res)
        }
      },
      fail(res) {
        reject(res)
      }
    })
  })
  return promise
}

function $get(url, data = '') {
  let params = {
    url: url,
    data: data
  }
  return baseRequest(params, 'GET')
}

function $post(url, data) {
  let params = {
    url: url,
    data: data
  }
  return baseRequest(params, 'POST')
}

function $put(url, data) {
  let params = {
    url: url,
    data: data
  }
  return baseRequest(params, 'PUT')
}

function $delete(url, data = '') {
  let params = {
    url: url,
    data: data
  }
  return baseRequest(params, 'DELETE')
}

module.exports = {
  formatTime: formatTime,
  $get: $get,
  $post: $post,
  $put: $put,
  $delete: $delete
}
相關文章
相關標籤/搜索