滴滴 webapp 5.0 Vue 2.0 重構經驗分享

做者:滴滴公共前端 黃軼javascript

項目背景

滴滴的 webapp 是運行在微信、支付寶、手 Q 以及其它第三方渠道的打車軟件。藉着產品層面的功能和視覺升級,咱們用 Vue 2.0 對它進行了一次技術重構。html

技術棧

MVVM框架: Vue 2.0
源碼:es6
代碼風格檢查:eslint
構建工具:webpack
前端路由:vue-router
狀態管理:vuex
服務端通信:vue-resource前端

幾個問題

  1. 滴滴 webapp 是一個大的 SPA 應用麼?
    滴滴 webapp 包含衆多業務線,每一個業務線都有獨立的一套的發單流程邏輯。那麼問題來了,這些業務邏輯都是在一個單頁中完成的麼?vue

  2. 如何實現組件化?
    滴滴 webapp 5.0 的設計思路就是組件化,設計提供了不少組件,每一個頁面都是由組件拼接而成。那麼問題來了,如何區分基礎組件和業務組件,並把基礎組件抽象成一個公共組件庫?java

  3. 一個代碼倉庫多條業務線,如何很好的作到多人同時開發和持續集成?
    滴滴有多條業務線,每條業務線會有一位前端同窗開發代碼。那麼問題來了,如何模塊化的組織代碼,如何儘量的減小開發的衝突以及作好持續集成?node

  4. 有部分業務線須要異步加載,這部分業務線如何開發?
    滴滴目前會把類專車業務線的代碼放在一個倉庫裏,可是部分業務線,如順風車的代碼是不在這個倉庫裏的。那麼問題來了,這部分代碼如何開發,如何使用 Vue,Vuex,store,以及一些公用方法和組件?webpack

  5. 異步加載的業務線組件,如何動態註冊?
    咱們須要異步加載業務線的 JS 代碼,這些業務線實現的是一個 Vue component。那麼問題來了,如何優雅地動態註冊這些組件呢?git

  6. 異步加載的業務線如何動態註冊路由?
    咱們在使用 Vue-router 初始化路由的時候,一般都會寫一個路由映射表。那麼問題來了,這些異步加載的業務線,若是也想使用路由,那麼如何動態註冊呢?es6

  7. 如何在測試環境下與後端接口交互?
    咱們在開發階段,一般都是在本地調試,本地起的服務域名一般是 localhost:端口號。那麼問題來了,這樣會和 ajax 請求產生跨域問題,而咱們也不能要求服務端把全部 ajax 請求接口都開跨域,如何解決呢?github

  8. 如何部署到線下測試環境?
    咱們在本地開發完代碼後,須要把代碼提測。一般測試拿到代碼後,須要部署和測試,那麼問題來了,咱們如何把本地代碼部署到咱們的開發機測試環境中呢?

解決方案

  1. 滴滴 webapp 是一個大的 SPA 應用麼?

    滴滴 webapp 包含衆多業務線,每一個業務線都有獨立的一套的發單 -> 接駕 -> 行程中 -> 訂單完成的流程邏輯。試想一下,若是總體是一個 SPA 應用,最終打包的 JS 會變的很大,雖然能夠經過 code spliting 技術異步加載,但也不可避免會增長代碼量,並且很是不利於維護。

    所以,咱們把發單和後續的業務邏輯拆開,拆成發單首頁和後續流程頁面,每一個業務線都有本身獨立的發單後的流程頁面。這樣滴滴的 webapp 至關於多個 SPA 應用構成,頁面間跳轉的數據傳遞經過 url 傳參便可。

  2. 如何實現組件化?

    組件化如今幾乎成爲 webapp 開發的標準,滴滴從設計角度就已是組件化的思路了。可是設計只會把頁面拆成一個個組件,咱們做爲開發者,須要從這些衆多組件中提取出哪些是基礎組件,哪些是業務組件,哪些組件可被複用等等。

    基礎組件主要指那些自己不包含任何業務邏輯、能夠被輕鬆複用的組件,例如 picker、timepicker、toast、dialog、actionsheet 等等...咱們基於 Vue 2.0 實現了一套移動端的基礎組件庫,打包了全部基礎組件,並託管在 npm 私服上,使用很是方便。基礎組件的通信基本就是往組件傳入 prop,並監聽組件 $emit 的事件。

    業務組件主要指那些包含業務邏輯,包括一些與後端接口通信的邏輯。業務組件會包含若干個基礎組件,一般咱們會把一些業務邏輯的數據經過 Vuex 管理起來,而後組件內部讀取數據和提交對數據修改的動做。這裏須要說明一點,當咱們使用 Vuex 的時候,並非全部和後端通信的請求都須要提交一個 action,若是這個請求並不會修改咱們 store 裏的數據,能夠在組件內部消化。舉個實際的例子,咱們在開發 suggest 組件的時候,每次輸入字符檢索相關的地址的時候,這個請求由組件內部發起,而且把請求的數據渲染到組件的列表便可,由於它並無修改 store 裏的數據。

    基礎組件一般都是可複用的,部分業務組件一樣可複用,它們的 UI 和業務邏輯類似。咱們會把單個可複用的業務組件單獨發佈到 npm 私服上,須要使用的業務線依賴便可。注意,業務組件咱們是不建議使用 Vuex,須要考慮到不一樣的使用方對 Vuex 內部變量的定義和使用是不相同的。

  3. 一個代碼倉庫多條業務線,如何很好的作到多人同時開發和持續集成?

    滴滴的 webapp 首頁有多條業務線,每條業務線都有一個開發人員,爲了保證儘可能減小代碼的衝突,咱們按業務線對代碼進行了模塊劃分。因爲 Vuex 支持modules,咱們很天然地按業務線拆分了 modules,每一個 modules 維護本身的 getters、actions、mutaions 和 state,而一些公共數據如經緯度、上下車信息、用戶登陸狀態等做爲 root state,被全部業務線共享。一樣,components 裏也按業務線作了更細緻的劃分,每一個業務線獨立的業務組件放在各自的目錄裏,彼此以前不會有衝突。

    僅僅作到目錄拆分仍是不夠的,咱們還要考慮到持續集成,跟着產品的版本迭代節奏發佈上線。那麼每一個版本的需求,每一個業務線都會參與開發,咱們用 gitlab 管理代碼,若是每一個開發同窗都拉一個分支,那麼會面臨着分支太多,功能聯調麻煩等問題。所以,咱們約定了一套 git 的管理規範,每一個大需求版本,咱們會約定以 "dev +上線時間日期" 做爲分支名建立開發分支,全部人在這個分支上開發,開發完成讓 QA 測試該分支,上線前纔會將分支合入主幹發佈。在兩個版本發佈期間若是有 bug fix,則約定以 "bugfix + 功能描述" 爲分支名建立 bugfix 分支,修復完成後合入主幹上線。每次上線前,咱們都會運行腳本新增版本號,編譯打包,保證前端資源的增量發佈。

  4. 有部分業務線須要異步加載,這部分業務線如何開發?

    滴滴目前會把一些業務線的代碼放在一個倉庫裏,可是部分業務線,如順風車的代碼是不在這個倉庫裏的。首頁經過異步加載 JS 去加載這部分業務線的代碼,這部分業務線很顯然也是須要用 Vue 開發的,可是他們不能夠再去單獨引入 Vue.js。

    咱們的解決方案是在 window 上註冊一個 XXApp 對象,把 Vue、Vuex 以及一些公共組件和方法等掛載到這個對象上,那麼這些異步加載的業務線就能夠經過 window.XXApp 訪問到了,代碼以下:

    window.XXApp = {
         Vue, 
         Vuex,
         store, // 全局store
         saveCurrentBiz, // 公共方法
         Location // 公共組件
         // 其它一些公共方法和組件
     }複製代碼

    業務線能夠訪問到這些對象後,接下來須要實現的就是一個 Vue component。

  5. 異步加載的業務線組件,如何動態註冊?

    Vue.js 官網提供的異步組件的解決方案大可能是基於 webpack 的 require.ensure 去異步加載組件的,但很顯然這並不適用滴滴的業務場景,由於咱們的代碼並不在一個倉庫下。咱們須要一種相似 amd 的解決方案,這些異步業務線須要實現的是一個 Vue component,咱們該如何優雅地動態註冊這個 component 呢?

    咱們知道 Vue 提供了動態註冊組件的 api,經過 Vue.component('async-example',function(resolve){ //... }) 的方式在工廠函數裏經過 resolve 方法動態註冊組件。注意,這個工廠函數的執行時機是組件實際須要渲染時,那咱們渲染這些異步組件的時機就是當咱們切換頂部導航欄到該業務線的時候

    首先,每一條業務線對應着一個獨立的組件,業務線有各自的 id,所以,咱們先用一個對象去維護這樣的映射關係,代碼以下:

    const modules = {
         業務線id: Taxi, // 出租車
         // 其它同步業務線組件 
     }複製代碼

    這個對象初始化的都是同步業務線組件,對於異步加載的業務線組件,咱們須要動態註冊。首先咱們在全局的 config.js 裏維護一個業務線的配置關係表,異步加載的業務線會多一個 src 屬性,代碼以下:

    bizConf: {
        異步業務線id: {
           name: 'alift', // 業務線名稱
           src: xxx // 加載異步業務線的 js 地址
        },
        同步業務線 id: {
           name: 'taxi'
        }
        // 其它業務線配置複製代碼

    接下來咱們遍歷這個對象,代碼以下:

    // 獲取 bizConf 對象
      const bizJSConf = config.get('bizConf') 
    
      for (let id in bizJSConf) {
         let conf = bizJSConf[id]
         if (conf.src) {
           modules[id] = conf.name
           Vue.component(conf.name, (resolve, reject) => {
             loadScript(conf.src).then(() => {
               resolve(modules[id])
             }).catch(() => {
               reject()
             })
           })
         }
       }複製代碼

    能夠看到,對於異步業務線,咱們會把它的 name 添加到 modules 對象的映射關係中,並按這個 name 註冊一個異步組件,注意,這個時候註冊組件的工廠函數並不會執行。

    咱們以前說到了渲染這些異步組件的時機就是當咱們切換頂部導航欄到該業務線的時候,咱們來看看切換頂部導航欄的時候執行了什麼邏輯,關鍵代碼以下:

    this.currentView = modules[productid]複製代碼

    這個 currentView 咱們是在 App.vue 的 data 裏初始化的,映射到 template 的代碼以下:

    <component :is="currentView"></component>複製代碼

    沒錯,這裏咱們又用到一個 Vue 的高級用法,動態組件。咱們的業務線組件對應的就是這個動態組件。官網文檔介紹的動態組件是綁定到一個組件對象上的,這對於咱們的同步組件,固然是沒有問題的,modules 映射的就是一個組件對象;可是對於異步組件,咱們映射的是組件的名稱,它是一個字符串,當 currentView 指向這個字符串的時候,註冊異步組件的工廠函數執行了,回顧以前的代碼,這個時候它會去加載異步業務線的 js,加載完成的回調函數裏,執行 resolve(modules[id])

    等等,看到這裏,有人不由會問,這裏 modules[id] 是什麼,仍是異步組件的名稱嗎?固然不是了,這裏的 modules[id] 對應的是異步業務線的組件對象。那麼,它是怎麼被賦值成組件對象的呢?咱們來看代碼:

    window.XXApp = {
         // ...
         // 一些公共方法和組件
         registerBiz(id, component) {
           modules[id] = component
         }
     }複製代碼

    咱們在 window.XXApp 下又添加了一個 registerBiz 的方法,當咱們異步加載完業務線的 JS 後,異步業務線調用這個方法真正的把本身實現的 Vue component 註冊到咱們的 modules 裏,因此咱們 resolve 的就是這個組件對象,是否是很優雅?至此,咱們完成了異步業務線組件的動態註冊。

  1. 異步加載的業務線如何動態註冊路由?
    再接着上述問題繼續發散,咱們在使用 Vue-router 初始化路由的時候,一般都會寫一個路由映射表。對於同步業務線這些已知的組件,路由的映射是沒有問題的,那麼這些異步加載的業務線,若是它的某些子組件也想使用路由該怎麼辦?

    咱們須要一套動態註冊路由的方案,而官網文檔提供的路由懶加載的方案並不能知足咱們的需求,所以咱們想到了另外一種變通方案。咱們在路由配置以下:

    {
         path: 'pathA' //這裏的命名只是示意
         component: componentA
       },
       {
         path: 'pathB',
         component: componentB
       },
       //...
       {
         path: '/:name',  // 動態匹配
         component: Dynamic // 已知組件
       }複製代碼

    能夠看到,咱們在定義了一系列常規的路由後,最後定義了一個動態匹配路由,也就是任意 name 的一級 path,只要沒有命中以前的 path,都會映射到這個咱們定義好的 Dynamic 組件上。咱們來看看這個 Dynamic 組件的實現,先看一下模板:

    <template>
       <transition :name="transitionName"> <component :is="currentRouter"></component> </transition>
     </template>複製代碼

    本質上,Dynamic 組件仍是利用了 Vue 的動態組件,經過修改 currentRouter 這個變量,能夠改變當前渲染的組件。咱們來看一下這個 currentRouter 修改的邏輯:

    created() {
       this.setCurrent()
     },
     methods: {
       setCurrent() {
         const name = this.$route.params.name
         const component = this.routes[name]
         if (component) {
           this.currentRouter = component
         }
       }
     }複製代碼

    在組件建立的鉤子函數裏,咱們會調用 this.setCurrent() ,該方法首先經過路由參數拿到 name,而後從 this.routes[name] 拿到對應的組件,並賦給 this.currentRouter 。那麼 this.routes 變的尤其重要了。咱們其實是把 routes 存儲到了 Vuex 的 store 裏, 而後經過 Vuex 的 mapGetters 獲取的:

    computed: {
       ...mapGetters([
         'routes'
       ])
     },複製代碼

    既然經過 Vuex 的方法能夠獲取 this.routes ,咱們必定會有寫的邏輯,而這個存的邏輯實際上就是咱們提供給這些異步業務線提供了一個 api 接口實現的:

    window.XXApp = {
         // ...
         // 一些公共方法和組件
         registerRouter(name, component) {
           Vue.component(name, component)
           store.commit('ADD_ROUTES', {
             name,
             component
           })
         }
     }複製代碼

    咱們提供了 registerRouter 接口,參數就是路由的名稱和對應的組件實例,咱們首先經過 Vue.component 全局註冊這個組件,而後經過 Vuex 提供的 commit 接口提交了一個 ADD_ROUTES 的 mutation,來看一下這個 mutation 的定義:

    [types.ADD_ROUTES](state, data) {
        state.routes = Object.assign({}, state.routes, {[data.name]: data.component})
       },複製代碼

    至此,咱們就完成了 routes 的存取邏輯,整個動態路由方案也就完成了, 異步業務線想使用動態路由,只須要調用咱們提供的 registerRouter 接口,是否是很方便呢~

  1. 如何在測試環境下與後端接口交互?

    咱們在開發階段,一般都是在本地調試,本地起的服務域名一般是 localhost:端口號。這樣會產生一些接口的跨域問題,除了常規的一些跨域方案,咱們實際上能夠藉助 node.js 服務幫咱們代理這些接口。

    咱們藉助 vue-cli 腳手架幫咱們生成一些初始化代碼。在 config/index.js 文件中,咱們修改 dev 下 proxyTable 的配置,代碼以下:

    proxyTable: {
       '/xxxservice': {
         target: 'http://xxx.com.cn', //你的目標域名
         changeOrigin: true
       },
       //...
     }複製代碼

    實際上,它就是利用了 node.js 幫咱們作了一層服務的轉發,這樣就能夠解決開發階段的跨域問題了。

  1. 如何部署到線下測試環境?

    咱們在本地開發完代碼後,須要把代碼提測。一般測試拿到代碼後,須要部署和測試,爲此咱們寫了一個 deploy 的腳本。原理其實很簡單,就是利用一個 scp2 的 node 包上傳代碼,它的執行時機是在 webpack 編譯完成後,代碼以下:

    var client = require('scp2')
     //...
     webpack(webpackConfig, function (err, stats) {
         // ...
         client.scp('deploy/home.html', {
             host,
             username,
             password,
             path
           }, function (err) {
             if (err) {
               console.log(err)
             } else {
               console.log('Finished, the page url is xxx')
             }
           })
      })複製代碼

    總結

    技術的重構總伴隨着產品的升級,從此次大重構中,咱們對 Vue 有了更深刻的理解和掌握。對於它的周邊插件如 Vuex 和 Vue-router,咱們團隊的小夥伴也有了較深刻的研究,產出幾篇文章也在這裏和你們分享:
    Vuex 2.0 源碼分析
    vue-router源碼分析-總體流程
    vue-router 源碼分析-history

以上,歡迎拍磚~


歡迎關注DDFE
GITHUB:github.com/DDFE
微信公衆號:微信搜索公衆號「DDFE」或掃描下面的二維碼

相關文章
相關標籤/搜索