用戶
 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

掃一掃,登錄網站

小程序社區 首頁 教程 新手教程 查看內容

如何一人五天開發完復雜小程序(前端必看)

Rolan 2019-12-31 00:21

隨著業務需求的不斷累加、小程序追求快速產出。在人手不足且開發周期較短的情況下,我們需要找到一個最大化開發效率的方法。而高效率的開發離不開規范化、工程化、組件化。為此整理寫下總結,細數小程序中的坑與實踐 ...

隨著業務需求的不斷累加、小程序追求快速產出。

在人手不足且開發周期較短的情況下,我們需要找到一個最大化開發效率的方法。

而高效率的開發離不開規范化、工程化、組件化。

為此整理寫下總結,細數小程序中的坑與實踐。

介紹我們對小程序高效率開發的思考與探索。

  • 布局方案

    • 導航欄

    • TabBar

    • BasicPage

  • 用戶系統

    • 登錄方案

    • 初始化登錄

    • 鑒權

  • 優化及 Bug 追蹤

    • 日志收集

    • 數據分析

  • 常用優化方案

    • preLoad

    • 獨立分包加載

布局方案

我們首先思考的是,在小程序中如何快速且高還原產出頁面。

為此我們封裝了一套頁面組件。

導航欄

目前小程序有如下兩種導航欄:常規、自定義導航欄

自定義導航欄布局下,我們可以完全控制導航欄樣式,賦予導航欄更多交互及 UI 設計上的可能。如上圖所示,Readhub 在導航欄中加入了設置按鈕,喜茶在個人頁中標題漸隱及沉浸式導航欄效果。常規布局下,頂部導航欄部分直接使用小程序提供導航欄。

可根據具體業務選擇具體布局方案,在我們小程序中,我們選擇了全部使用自定義導航欄的方式并對其進行了一定封裝。

在確定使用自定義導航欄方案后,我們對導航欄進行了拆解

拆解后,我們發現可以將自定義導航欄分為兩個部分:StatusBar 及 NavigationBar 。

通過查閱微信 API ,我們分別通過 wx.getSystemInfoSyncwx.getMenuButtonBoundingClientRect 獲取到 StatusBarHeight 及 MenuButton 的布局信息。

由拆解圖可知

1 NavigationBarPaddingTop = MenuButtonTop - StatusBarHeight
3 NavigationBarPaddingBottom = NavigationBarPaddingTop
5 NavigationBar = StatusBarHeight + NavigationBarPaddingTop + NavigationBarPaddingBottom + MenuButtonHeight復制代碼

得到上述數據后,結果簡單封裝, 我們得到如下方案

StatusBar 部分, 我們使用 PaddingTop 填充。

可在此基礎上可再進一步封裝一些通用 NavigationBar 組件。

我們封裝了一些常用 NavigationBar 組件, 如下所示:

沉浸式導航欄

自定義 TabBar

目前小程序 TabBar 中也存在兩種方案。

常規 TabBar :微信提供方案,可修改 icon 、 文字及其對應選中狀態。

自定義 TabBar :小程序基礎庫 2.5.0 開始支持??赏ㄟ^其實現異形 TabBar 或各種自定義樣式。


在我們小程序中,我們選擇全部使用自定義 TabBar 來實現業務。

由于小程序基礎庫 2.5.0 之后官方才開始支持自定義 TabBar 。我們此處不直接選擇使用 custom-tab-bar 方案。選擇結合 custom-tab-bar 、 自定義組件及 wx.hideTabBar 的方案實現。

具體方案為放置空節點 custom-tab-bar 文件。在頁面中按需引入自定義 TabBar 組件。在頁面初始化完成后調用 wx.hideTabBar 隱藏原 TabBar 。

這樣做的好處在于,在基礎庫 2.5.0 及更高版本時正常顯示,在低版本時以最小代價兼容。

在 iPhone X 系列下的底部安全區兼容方案如下

 [email protected] media-style() { 2  .tab { 3    padding-bottom: 84px; 4  } 5} 6// 適配iPhone X系列下巴 [email protected] screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) { 8    @include media-style(); 9}[email protected] only screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio:3) {12    @include media-style();13}[email protected] only screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio:2) {16    @include media-style();17}18// 下面代碼只為適配iPhone X在微信調試模擬器中為[email protected] screen and (device-width: 375px) and (device-height: 724px) and (-webkit-device-pixel-ratio: 3) {20    @include media-style();21}復制代碼

推薦如無特殊需求,建議直接使用微信提供方案,在自定義 TabBar 方案中 安卓手機下拉刷新時, TabBar 會被拉出可視區域。需自定義下拉刷新組件解決

方案整合 BasicPage

以上方案在線上運行一段時間后穩定后。對自定義導航欄及自定義 TabBar 方案進行了整合。封裝了 BasicPage 組件。

以我們線上典型頁面為例,我們可以將頁面分為兩大類。


基于以上分析結合線上需求,我們對此基礎組件進行封裝。

Taro 框架偽代碼,可根據各自使用框架進行封裝,思路一致

 1class BasicPage extends Taro.Component { 2 3  state = { 4    menuButtonHeight: 32, 5    menuButtonTop: 48, 6    statusBarHeight: 44, 7  }; 8 9  componentDidMount() {10        // ...獲取并設置 menuButtonHeight 、 menuButtonTop 、 statusBarHeight11  }1213  render() {14    return (15      <View className='basic-page'>16        {17          this.props.header && <View className={`basic-page-header${this.props.fixed ? ' fixed' : ''}`} style={{18            paddingTop: `${this.state.statusBarHeight}px`,19            height: `${(this.state.menuButtonTop - this.state.statusBarHeight) * 2 + this.state.menuButtonHeight}px`,20          }}21          >22            {this.props.renderHeader}23          </View>24        }25        <View className={`basic-page-body${this.props.tab ? ' tab' : ''}`}>26          {this.props.renderBody}27        </View>28        {this.props.tab && <TabBar active={this.props.tabActive} />}29      </View>30    );31  }32}3334BasicPage.defaultProps = {35  fixed: false, // header 是否浮動36  tab: false,37  header: false,38  tabActive: 'template',39};40復制代碼

使用中會經常用到 自定義 TabBar 、 自定義 NavigationBar 布局數據。再封裝一個工具類獲取。

 1import Taro from "@tarojs/taro"; 2 3function rpx2px(rpx, windowWidth) { 4  return rpx / 750 * windowWidth; 5} 6 7export default class customConfig { 8 9  static fetchAllConfig() {10    const menuButton = Taro.getMenuButtonBoundingClientRect();11    const systemInfo = Taro.getSystemInfoSync();1213    const statusBarHeight = systemInfo.statusBarHeight;14    const headerHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height;15    const footerHeight = systemInfo.model.indexOf('iPhone X') === -116      ?17      rpx2px(100, systemInfo.windowWidth)18      :19      rpx2px(168, systemInfo.windowWidth);  // 50  8420    const bodyHeight = systemInfo.windowHeight - statusBarHeight - headerHeight - footerHeight;21    const noTabBodyHeight = systemInfo.windowHeight - statusBarHeight - headerHeight;2223    let data = {24      source: {25        menu: menuButton,26        system: systemInfo,27      },28      height: {29        statusBar: statusBarHeight,30        header: headerHeight,31        body: bodyHeight,32        noTabBody: noTabBodyHeight,33        footer: footerHeight,34      },35    };36    Taro.setStorageSync('customConfig', data);37    return data;38  }3940  static get config() {41    let storageInfoSync = Taro.getStorageSync('customConfig');42    if(!storageInfoSync) {43      storageInfoSync = this.fetchAllConfig();44    }45    return storageInfoSync;46  }47}復制代碼

到此,我們完成對基礎頁面組件的封裝。目前線上運行小程序所有頁面都基于該組件進行開發。

開發新頁面時只需要引用該組件即可。

1<BasicPage header tab tabActive='index' 2        renderHeader={ 3          <View 4            className='my-index-header' 5          > 6            <Text>Title</Text> 7          </View> 8        } 9        renderBody={10          <View className='my-index-header'>11            Body12          </View>13        }14/>復制代碼

用戶系統

在一個應用中,用戶系統是至關重要的。我們通過數個小程序的開發,整理了一套我們目前正在使用的用戶系統實踐。

登錄、獲取用戶信息


如上圖所示,我們將小程序登錄及獲取用戶信息拆分為兩部分。

主要有如下考慮:

  1. 降低用戶使用門檻,可先讓用戶體驗部分功能。后續分享或互動時提示授權完善用戶信息

  2. 保證始終持有用戶登錄態,方便程序處理。如把用戶登錄及完善用戶信息放置一起,在未授權時無法獲取自定義登錄態。判斷變得復雜且無法提前收集 formId

  3. 同一開發者賬號下,多小程序互通時,如有一小程序用戶授權過,可通過返回 unionid 直接同步信息,無需再授權,提升用戶體驗。

處理注意點

授權獲取用戶信息時,如果服務端未記錄用戶 sessionKey ,在 Button type = getUserInfo 回調事件中使用 wx.login 方法獲取 code 的話,會導致 sessionKey 變化。從而導致 getUserInfo 時使用 sessionKey 與新 sessionKey 不匹配。從而導致解密用戶信息失敗。

解決方案有如下兩種:

  • Button type = getUserInfo 回調事件中使用 wx.login 方法后,再次調用 wx.getUserInfo 方法重新獲取加密用戶信息。

  • 服務端記錄 sessionKey ,Button type = getUserInfo 回調后無需調用 wx.login ,直接提交供服務端處理。

第一種方案適合簡單改造舊項目、快速開發,但強烈建議使用服務端處理方式解決。

完善用戶信息時,解密用戶信息部分請查看官方文檔,這里不敘述具體流程https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html

unionid 機制

另外,在登錄流程中服務端向微信換取 sessionKey 過程中,如果滿足一定條件,會直接返回 unionid 。同開發者賬號下多個小程序時可用 unionid 做用戶信息同步,無需再授權。提升用戶體驗。

unionid 機制: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html

小程序初始化及頁面初始化處理

在日常開發中,我們通常會把登錄獲取 token 操作放置在小程序初始化中即 app.js 定義的 onLaunch 中。而該生命周期與頁面初始化生命周期為同步進行。

此時,如果在頁面初始化中,需要攜帶用戶登錄態請求接口獲取信息時,可能出現如下情況

因為小程序初始化及頁面初始化是同步進行的。若頁面初始化時,小程序初始化中登錄請求仍未完成。會導致未攜帶 token 或其他鑒權信息,鑒權失敗。

最開始我們通過在組件中掛載一個特殊事件 componentDidInit ,待小程序初始化登錄請求后獲取當前頁面實例進行調用。但該方案對代碼侵入性太強,最終我們選擇維護一個登錄請求隊列。

用上隊列的原因在于,在產品需求上經常會有先跳入首頁,再從首頁跳入二級頁的需求,這樣能讓用戶回退一次后,仍然能回到首頁。但會導致在不同頁面中近乎同時調用 login 方法。

在第一種方案中,解決該問題需要獲得所有頁面實例進行調用。而引入隊列后只需要輪詢消費隊列中函數執行即可。上述流程可解決此問題。偽代碼如下:

代碼僅供理解思路

 1let loginDoing = false; 2const loginEvent = []; 3 4const userProfile = observable({ 5  user: { 6    avatar: '', 7    isCompleted: false, 8    nickname: '', 9    uid: 0,10    token: '',11  },12  async loginProcess() {13    if(this.user.token) {14      return this.user;15    }16    loginDoing = true;17    let code;18    try {19      const codeResult = await Taro.login();20      if(codeResult.errMsg !== 'login:ok') {21        throw new Error('Taro.login 失敗');22      }23      code = codeResult.code;24    } catch (e) {25      loginDoing = false;26      throw e;27    }28    const result = await post(URL().user.login, {29      code,30    });31    let user = {32      ...result.user,33      token: result.token,34    };35    this.user = user;36    loginDoing = false;37    setTimeout(() => {38      let length = loginEvent.length;39      for(let i = 0; i < length; i++) {40        loginEvent.pop()(user);41      }42    });43    return user;44  },45  login() {46    if(loginDoing) {47      return new Promise((resolve) => {48        loginEvent.push(resolve);49      });50    } else {51      return this.loginProcess()52    }53  },54});復制代碼

鑒權

業務需求中,通常存在某些操作需要 【 用戶授權完善信息 】 后才能繼續進行,早期項目中都是各自頁面中寫鑒權代碼。因而會涉及大量重復代碼,也不利于快速開發。為此我們封裝了一套鑒權方案。

BasePage

通過所有頁面基礎一個基類 BasePage 。在 BasePage 中寫入鑒權邏輯來實現。配合在主頁面中使用 AuthorizationModal 組件實現鑒權。

代碼僅供理解思路

1 export default class BasePage extends Component { 2 3    state = { 4        // 鑒權相關 5        showAuthorizationModal: false, 6    }; 7 8    /** 9     * 鑒權相關10     */11    // 授權成功事件12    authSuccessEvent() {13    }1415    // 取消授權事件16    authFailEvent() {17    }1819    async checkAuthorization() {20        // 當前是否有已驗證21        let globalData = getGlobalData(STORAGE_KEY.VERIFY);22        if(globalData) {23            return {24                isNew: false,25            };26        } else {27            Taro.showLoading({28                title: '檢查授權中...',29                mask: true,30                showTicketModal: false,31            });32            // 如果本地不存在時,先請求接口33            // 未登錄過,或新機器34            // 請求token及授權狀態35            let res;36            try {37                res = await Taro.login();38            } catch() {39                Toast.fail('登錄失敗~');40                Taro.hideLoading();41                throw new Error('Taro.login 失敗');42            }43            // 請求授權接口44            const result = {};45            if(result.errno === 0) {46                resolve({47                    isNew: false,48                });49            } else {50                // 未授權過51                // 彈窗提示授權52                this.setState({53                    showAuthorizationModal: true,54                });55                this.authSuccessEvent = () => {56                    this.setState({57                        showAuthorizationModal: false,58                    });59                    resolve({60                        isNew: true,61                    });62                };63                this.authFailEvent = () => {64                    this.setState({65                        showAuthorizationModal: false,66                    });67                    reject();68                };69            }70        }71    }72}復制代碼

頁面繼承該基類

1 class LaunchIndex extends BasePage {}復制代碼

在頁面中置入組件

1 {this.state.showAuthorizationModal &&2 <AuthorizationModal onSuccess={this.authSuccessEvent} onFail={this.authFailEvent}/>}3復制代碼

AuthorizationModal 組件

接下來,我們只需要在需要鑒權的操作中如下使用即可

1this.checkAuthorization()2  .then((res) => {3   // 授權成功邏輯4       console.log('是否新用戶', res.isNew);5   })6   .catch(() => {7    // 授權失敗邏輯8    })復制代碼

該方案好處在于,授權由狀態驅動,只需在代碼中調用 checkAuthorization 方法即可。

AuthorizationView

后來,由于第一種方案過于重,對頁面代碼侵入性較強。為此我們又封裝了一套較輕的組件。

大部分邏輯中,需要用戶主動點擊時才進行鑒權,我們基于此思路封裝了 AuthorizationView 。對外暴露 onAgree 、 onDeny 方法實現對部分區域的點擊鑒權操作。

代碼僅供理解思路

1 class AuthorizationView extends Taro.Component { 2 3  state = { 4    showLoginPanel: false, 5  }; 6 7  /** 8   * 登錄 9   */10  click() {11    const { userProfile: { user, }, } = this.props;12    if(user.isCompleted) {13      this.props.onAgree(user);14    } else {15      // 顯示登錄框16      this.setState({17        showLoginPanel: true,18      });19    }20  }2122  /**23   * 授權登錄24   * @param e25   */26  async bindGetUserInfo(e) {27    if(e.detail.errMsg === 'getUserInfo:ok') {28      const { userProfile, } = this.props;29      const userResult = await userProfile.login(true);30      this.setState({31        showLoginPanel: false,32      });33      this.props.onAgree(userResult);34    } else {35      this.props.onDeny();36    }37  }3839  cancel() {40    this.setState({41      showLoginPanel: false,42    });43  }4445  render() {46    return (47      <Block>48        <View onClick={this.click}>{this.props.children}</View>49        {50          this.state.showLoginPanel && <View className='login-panel'>51            <View className='login-panel-main'>52              <View className='login-panel-main-title'>您還未登錄</View>53              <View className='login-panel-main-subtitle'>請先登錄再進行操作</View>54              <Image className='login-panel-main-image' src='https://p0.ssl.qhimg.com/t01a1e495cc2be1e651.png' />55              <View className='login-panel-main-footer'>56                <View className='login-panel-main-footer-button cancel' onClick={this.cancel.bind(this)}>暫不登錄</View>57                <Button className='btn-reset' openType='getUserInfo' onGetUserInfo={this.bindGetUserInfo}>58                  <View className='login-panel-main-footer-button confirm'>立即登錄</View>59                </Button>60              </View>61            </View>62          </View>63        }64      </Block>65    );66  }67}6869AuthorizationView.defaultProps = {70  onAgree: () => {71  },72  onDeny: () => {73  },74};7576export default AuthorizationView;77復制代碼

代碼中只需要使用該組件包裹子組件即可使用

1 <AuthorizationView onAgree={this.onAgree.bind(this)} onDeny={this.onDeny.bind(this)}>2  <View>生成海報</View>3</AuthorizationView>4復制代碼

以上兩種方案都有在線上業務中使用,具體選型看業務決定

優化及Bug追蹤

在維護階段,我們會更加關注于用戶反饋 bug 時如何復現場景及數據分析。

日志收集

在小程序基礎庫版本 2.1.0 后,微信提供了一套日志相關接口:LogManager 。

在用戶反饋時,通過該接口記錄的日志會同步上傳至微信后臺,可下載查看追蹤 Bug。

我們通過簡單的對其封裝,實現一套日志收集機制。

1 const _logger = Taro.getLogManager({ level: 0, }); 2 3const Logger = { 4  debug(...args) { 5    _logger.debug(`${dayjs().format('YYYY-MM-DD HH:mm:ss')}  
鮮花
鮮花 (1)
雞蛋
雞蛋

剛表態過的朋友 (1 人)

分享至 : QQ空間
收藏
原作者: 花椒技術 來自: 掘金
东方红彩票 - 购彩大厅 今天股票开盘情况 证券投资基金的资产交由谁保管 微信群免费推荐股票 股票好久开盘 论坛股票 最新股票大盘 几大股票软件比较 如何加入股票微信群 股票查询60070 600086股票行