淺談使用 Vue 構建前端 10w+ 代碼量的單頁面應用開發底層

開始以前

隨着業務的不斷累積,目前咱們 ToC 端主要項目,除去 node_modulesbuild 配置文件dist 靜態資源文件的代碼量爲 137521 行,後臺管理系統下各個子應用代碼,除去依賴等文件的總行數也達到 100萬 多一點。css

代碼量意味不了什麼,只能證實模塊不少,但相同兩個項目,在 運行時性能相同狀況下, 你的 10 萬行代碼能容納並維護 150 個模塊,而且開發順暢,個人項目中 10 萬行代碼卻只能容納 100 個模塊,添加功能也好,維護起來也較爲繁瑣,這就很值得思考

本文會在主要描述以 Vue 技術棧技術主體ToC 端項目業務主體,在構建過程當中,遇到或者總結的點(也會說起一些 ToB 項目的場景),可能並不適合你的業務場景(僅供參考),我會盡量多的描述問題與其中的思考,最大可能的幫助到須要的同窗,也辛苦開發者發現問題或者不合理/不正確的地方及時向我反饋,會盡快修改,歡迎有更好的實現方式來 prhtml

Git 地址
React 項目

能夠參考螞蟻金服數據體驗技術團隊編寫的文章:前端

本文並非基於上面文章寫的,不過當時在看到他們文章以後以爲有類似的地方,相較於這篇文章,本文可能會枯燥些,會有大量代碼,同窗能夠直接用上倉庫看。vue

① 單頁面,多頁面

首先要思考咱們的項目最終的構建主體單頁面,仍是多頁面,仍是單頁 + 多頁,經過他們的優缺點來分析:node

  • 單頁面(SPA)webpack

    • 優勢:體驗好,路由之間跳轉流程,可定製轉場動畫,使用了懶加載可有效減小首頁白屏時間,相較於多頁面減小了用戶訪問靜態資源服務器的次數等。
    • 缺點:初始會加載較大的靜態資源,而且隨着業務增加會愈來愈大,懶加載也有他的弊端,不作特殊處理不利於 SEO 等。
  • 多頁面(MPA)ios

    • 優勢:對搜索引擎友好,開發難度較低。
    • 缺點:資源請求較多,整頁刷新體驗較差,頁面間傳遞數據只能依賴 URLcookiestorage 等方式,較爲侷限。
  • SPA + MPAgit

    • 這種方式常見於較老 MPA 項目遷移至 SPA 的狀況,缺點結合二者,兩種主體通訊方式也只能以兼容MPA 爲準
    • 不過這種方式也有他的好處,假如你的 SPA 中,有相似文章分享這樣(沒有後端直出,後端返 HTML 串的狀況下),想保證用戶體驗在 SPA 中開發一個頁面,在 MPA 中也開發一個頁面,去掉沒用的依賴,或者直接用原生 JS 來開發,分享出去是 MPA 的文章頁面,這樣能夠加快分享出去的打開速度,同時也能減小靜態資源服務器的壓力,由於若是分享出去的是 SPA 的文章頁面,那 SPA 所需的靜態資源至少都須要去進行協商請求,固然若是服務配置了強緩存就忽略以上所說。

咱們首先根據業務所需,來最終肯定構建主體,而咱們選擇了體驗至上的 SPA,並選用 Vue 技術棧。es6

② 目錄結構

其實咱們看開源的絕大部分項目中,目錄結構都會差不太多,咱們能夠綜合一下來個通用的 src 目錄:github

src
├── assets          // 資源目錄 圖片,樣式,iconfont
├── components      // 全局通用組件目錄
├── config          // 項目配置,攔截器,開關
├── plugins         // 插件相關,生成路由、請求、store 等實例,並掛載 Vue 實例
├── directives      // 拓展指令集合
├── routes          // 路由配置
├── service         // 服務層
├── utils           // 工具類
└── views           // 視圖層

③ 通用組件

components 中咱們會存放 UI 組件庫中的那些常見通用組件了,在項目中直接經過設置別名來使用,若是其餘項目須要使用,就發到 npm 上。

結構

// components 簡易結構
components
├── dist
├── build
├── src      
    ├── modal
    ├── toast
    └── ...
├── index.js             
└── package.json

項目中使用

若是想最終編譯成 es5,直接在 html 中使用或者部署 CDN 上,在 build 配置簡單的打包邏輯,搭配着 package.json 構建 UI組件 的自動化打包發佈,最終部署 dist 下的內容,併發布到 npm 上便可。

而咱們也可直接使用 es6 的代碼:

import 'Components/src/modal'

其餘項目使用

假設咱們發佈的 npm 包bm-ui,而且下載到了本地 npm i bm-ui -S:

修改項目的最外層打包配置,在 rules 裏 babel-loaderhappypack 中添加 includenode_modules/bm-ui

// webpack.base.conf
...
    rules: [{
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
    },
    {
        test: /\.js$/,
        loader: 'babel-loader',
        // 這裏添加
        include: [resolve('src'), resolve('test'), resolve('node_modules/bm-ui')]
    },{
    ...
    }]
...

而後搭配着 babel-plugin-import 直接在項目中使用便可:

import { modal } from 'bm-ui'

多個組件庫

同時有多個組件庫的話,又或者有同窗專門進行組件開發的話,把 `components
內部細分`一下,多一個文件分層。

components
├── bm-ui-1 
├── bm-ui-2
└── ...

你的打包配置文件能夠放在 components 下,進行統一打包,固然若是要開源出去仍是放在對應庫下。

④ 全局配置,插件與攔截器

這個點其實會是項目中常常被忽略的,或者說不多聚合到一塊兒,但同時我認爲是整個項目中的重要之一,後續會有例子說道。

全局配置,攔截器目錄結構

config
├── index.js             // 全局配置/開關
├── interceptors        // 攔截器
    ├── index.js        // 入口文件
    ├── axios.js        // 請求/響應攔截
    ├── router.js       // 路由攔截
    └── ...
└── ...

全局配置

咱們在 config/index.js 可能會有以下配置:

// config/index.js

// 當前宿主平臺 兼容多平臺應該經過一些特定函數來取得
export const HOST_PLATFORM = 'WEB'
// 這個就很少說了
export const NODE_ENV = process.env.NODE_ENV || 'prod'

// 是否強制全部請求訪問本地 MOCK,看到這裏同窗不難猜到,每一個請求也能夠單獨控制是否請求 MOCK
export const AJAX_LOCALLY_ENABLE = false
// 是否開啓監控
export const MONITOR_ENABLE = true
// 路由默認配置,路由表並不今後注入
export const ROUTER_DEFAULT_CONFIG = {
    waitForData: true,
    transitionOnLoad: true
}

// axios 默認配置
export const AXIOS_DEFAULT_CONFIG = {
    timeout: 20000,
    maxContentLength: 2000,
    headers: {}
}

// vuex 默認配置
export const VUEX_DEFAULT_CONFIG = {
    strict: process.env.NODE_ENV !== 'production'
}

// API 默認配置
export const API_DEFAULT_CONFIG = {
    mockBaseURL: '',
    mock: true,
    debug: false,
    sep: '/'
}

// CONST 默認配置
export const CONST_DEFAULT_CONFIG = {
    sep: '/'
}

// 還有一些業務相關的配置
// ...


// 還有一些方便開發的配置
export const CONSOLE_REQUEST_ENABLE = true      // 開啓請求參數打印
export const CONSOLE_RESPONSE_ENABLE = true     // 開啓響應參數打印
export const CONSOLE_MONITOR_ENABLE = true      // 監控記錄打印

能夠看出這裏聚集了項目中全部用到的配置,下面咱們在 plugins 中實例化插件,注入對應配置,目錄以下:

插件目錄結構

plugins
├── api.js              // 服務層 api 插件
├── axios.js            // 請求實例插件
├── const.js            // 服務層 const 插件
├── store.js            // vuex 實例插件
├── inject.js           // 注入 Vue 原型插件
└── router.js           // 路由實例插件

實例化插件並注入配置

這裏先舉出兩個例子,看咱們是如何注入配置,攔截器並實例化的

實例化 router

import Vue from 'vue'
import Router from 'vue-router'
import ROUTES from 'Routes'
import {ROUTER_DEFAULT_CONFIG} from 'Config/index'
import {routerBeforeEachFunc} from 'Config/interceptors/router'

Vue.use(Router)

// 注入默認配置和路由表
let routerInstance = new Router({
    ...ROUTER_DEFAULT_CONFIG,
    routes: ROUTES
})
// 注入攔截器
routerInstance.beforeEach(routerBeforeEachFunc)

export default routerInstance

實例化 axios

import axios from 'axios'
import {AXIOS_DEFAULT_CONFIG} from 'Config/index'
import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from 'Config/interceptors/axios'

let axiosInstance = {}

axiosInstance = axios.create(AXIOS_DEFAULT_CONFIG)

// 注入請求攔截
axiosInstance
    .interceptors.request.use(requestSuccessFunc, requestFailFunc)
// 注入響應攔截
axiosInstance
    .interceptors.response.use(responseSuccessFunc, responseFailFunc)

export default axiosInstance

咱們在 main.js 注入插件

// main.js
import Vue from 'vue'

GLOBAL.vbus = new Vue()

// import 'Components'// 全局組件註冊
import 'Directives' // 指令

// 引入插件
import router from 'Plugins/router'
import inject from 'Plugins/inject'
import store from 'Plugins/store'
// 引入組件庫及其組件庫樣式 
// 不須要配置的庫就在這裏引入 
// 若是須要配置都放入 plugin 便可
import VueOnsen from 'vue-onsenui'
import 'onsenui/css/onsenui.css'
import 'onsenui/css/onsen-css-components.css'
// 引入根組件
import App from './App'

Vue.use(inject)
Vue.use(VueOnsen)

// render
new Vue({
    el: '#app',
    router,
    store,
    template: '<App/>',
    components: { App }
})

axios 實例咱們並無直接引用,相信你也猜到他是經過 inject 插件引用的,咱們看下 inject

import axios from './axios'
import api from './api'
import consts from './const'
GLOBAL.ajax = axios
 
export default {
    install: (Vue, options) => {
        Vue.prototype.$api = api
        Vue.prototype.$ajax = axios
        Vue.prototype.$const = consts
        // 須要掛載的都放在這裏
    }
}

這裏能夠掛載你想在業務中( vue 實例中)便捷訪問的 api,除了 $ajax 以外,apiconst 兩個插件是咱們服務層中主要的功能,後續會介紹,這樣咱們插件流程大體運轉起來,下面寫對應攔截器的方法。

請求,路由攔截器

ajax 攔截器中(config/interceptors/axios.js):

// config/interceptors/axios.js

import {CONSOLE_REQUEST_ENABLE, CONSOLE_RESPONSE_ENABLE} from '../index.js'

export function requestSuccessFunc (requestObj) {
    CONSOLE_REQUEST_ENABLE && console.info('requestInterceptorFunc', `url: ${requestObj.url}`, requestObj)
    // 自定義請求攔截邏輯,能夠處理權限,請求發送監控等
    // ...
    
    return requestObj
}

export function requestFailFunc (requestError) {
    // 自定義發送請求失敗邏輯,斷網,請求發送監控等
    // ...
    
    return Promise.reject(requestError);
}

export function responseSuccessFunc (responseObj) {
    // 自定義響應成功邏輯,全局攔截接口,根據不一樣業務作不一樣處理,響應成功監控等
    // ...
    // 假設咱們請求體爲
    // {
    //     code: 1010,
    //     msg: 'this is a msg',
    //     data: null
    // }
    
    let resData =  responseObj.data
    let {code} = resData
    
    switch(code) {
        case 0: // 若是業務成功,直接進成功回調  
            return resData.data;
        case 1111: 
            // 若是業務失敗,根據不一樣 code 作不一樣處理
            // 好比最多見的受權過時跳登陸
            // 特定彈窗
            // 跳轉特定頁面等
            
            location.href = xxx // 這裏的路徑也能夠放到全局配置裏
            return;
        default:
            // 業務中還會有一些特殊 code 邏輯,咱們能夠在這裏作統一處理,也能夠下方它們到業務層
            !responseObj.config.noShowDefaultError && GLOBAL.vbus.$emit('global.$dialog.show', resData.msg);
            return Promise.reject(resData);
    }
}

export function responseFailFunc (responseError) {
    // 響應失敗,可根據 responseError.message 和 responseError.response.status 來作監控處理
    // ...
    return Promise.reject(responseError);
}

定義路由攔截器(config/interceptors/router.js):

// config/interceptors/router.js

export function routerBeforeFunc (to, from, next) {
    // 這裏能夠作頁面攔截,不少後臺系統中也很是喜歡在這裏面作權限處理
    
    // next(...)
}

最後在入口文件(config/interceptors/index.js)中引入並暴露出來便可:

import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from './ajax'
import {routerBeforeEachFunc} from './router'

let interceptors = {
    requestSuccessFunc,
    requestFailFunc,
    responseSuccessFunc,
    responseFailFunc,
    routerBeforeEachFunc
}

export default interceptors

請求攔截這裏代碼都很簡單,對於 responseSuccessFunc 中 switch default 邏輯作下簡單說明:

  1. responseObj.config.noShowDefaultError 這裏可能不太好理解

咱們在請求的時候,能夠傳入一個 axios 中並無意義的 noShowDefaultError 參數爲咱們業務所用,當值爲 false 或者不存在時,咱們會觸發全局事件 global.dialog.showglobal.dialog.show咱們會註冊在 app.vue 中:

// app.vue

export default {
    ...
    created() {
        this.bindEvents
    },
    methods: {
        bindEvents() {
            GLOBAL.vbus.$on('global.dialog.show', (msg) => {
                if(msg) return
                // 咱們會在這裏註冊全局須要操控試圖層的事件,方便在非業務代碼中經過發佈訂閱調用
                this.$dialog.popup({
                    content: msg 
                });
            })
        }
        ...
    }
}
這裏也能夠把彈窗狀態放入 Store 中,按團隊喜愛,咱們習慣把 公共的涉及視圖邏輯的公共狀態在這裏註冊,和業務區分開來
  1. GLOBAL 是咱們掛載 window 上的全局對象,咱們把須要掛載的東西都放在 window.GLOBAL 裏,減小命名空間衝突的可能。
  2. vbus 其實就是咱們開始 new Vue() 掛載上去的
GLOBAL.vbus = new Vue()
  1. 咱們在這裏 Promise.reject 出去,咱們就能夠在 error 回調裏面只處理咱們的業務邏輯,而其餘如斷網超時服務器出錯等均經過攔截器進行統一處理。

攔截器處理先後對比

對比下處理先後在業務中的發送請求的代碼

攔截器處理前

this.$axios.get('test_url').then(({code, data}) => {
    if( code === 0 ) {
        // 業務成功
    } else if () {}
        // em... 各類業務不成功處理,若是遇到通用的處理,還須要抽離出來
    
    
}, error => {
   // 須要根據 error 作各類抽離好的處理邏輯,斷網,超時等等...
})

攔截器處理後

// 業務失敗走默認彈窗邏輯的狀況
this.$axios.get('test_url').then(({data}) => {
    // 業務成功,直接操做 data 便可
})

// 業務失敗自定義
this.$axios.get('test_url', {
    noShowDefaultError: true // 可選
}).then(({data}) => {
    // 業務成功,直接操做 data 便可
    
}, (code, msg) => {
    // 當有特定 code 須要特殊處理,傳入 noShowDefaultError:true,在這個回調處理就行
})

爲何要如此配置與攔截器?

在應對項目開發過程當中需求的不可預見性時,讓咱們能處理的更快更好

到這裏不少同窗會以爲,就這麼簡單的引入判斷,無關緊要,
就如咱們最近作的一個需求來講,咱們 ToC 端項目以前一直是在微信公衆號中打開的,而咱們須要在小程序中經過 webview 打開大部分流程,而咱們也沒有時間,沒有空間在小程序中重寫近 100 + 的頁面流程,這是咱們開發之初並無想到的。這時候必須把項目兼容到小程序端,在兼容過程當中可能須要解決如下問題:

  1. 請求路徑徹底不一樣。
  2. 須要兼容兩套不一樣的權限系統。
  3. 有些流程在小程序端須要作改動,跳轉到特定頁面。
  4. 有些公衆號的 api ,在小程序中無用,須要調用小程序的邏輯,須要作兼容。
  5. 不少也頁面上的元素,小程序端不作展現等。
能夠看出,稍微不慎,會影響公衆號現有邏輯。
  • 添加請求攔截 interceptors/minaAjax.jsinterceptors/minaRouter.js,原有的換更爲 interceptors/officalAjax.jsinterceptors/officalRouter.js,在入口文件interceptors/index.js根據當前宿主平臺,也就是全局配置 HOST_PLATFORM,經過代理模式策略模式,注入對應平臺的攔截器minaAjax.js中重寫請求路徑和權限處理,在 minaRouter.js 中添加頁面攔截配置,跳轉到特定頁面,這樣一併解決了上面的問題 1,2,3
  • 問題 4 其實也比較好處理了,拷貝須要兼容 api 的頁面,重寫裏面的邏輯,經過路由攔截器一併作跳轉處理
  • 問題 5 也很簡單,拓展兩個自定義指令 v-mina-show 和 v-mina-hide ,在展現不一樣步的地方能夠直接使用指令。

最終用最少的代碼,最快的時間完美上線,絲毫沒影響到現有 toC 端業務,並且這樣把全部兼容邏輯絕大部分聚合到了一塊兒,方便二次拓展和修改。

雖然這只是根據自身業務結合來講明,可能沒什麼說服力,不過不難看出全局配置/攔截器 雖然代碼很少,但倒是整個項目的核心之一,咱們能夠在裏面作更多 awesome 的事情。

⑤ 路由配置與懶加載

directives 裏面沒什麼可說的,不過不少難題均可以經過他來解決,要時刻記住,咱們能夠再指令裏面操做虛擬 DOM。

路由配置

而咱們根據本身的業務性質,最終根據業務流程來拆分配置:

routes
├── index.js            // 入口文件
├── common.js           // 公共路由,登陸,提示頁等
├── account.js          // 帳戶流程
├── register.js         // 掛號流程
└── ...

最終經過 index.js 暴露出去給 plugins/router 實例使用,這裏的拆分配置有兩個注意的地方:

  • 須要根據本身業務性質來決定,有的項目可能適合業務線劃分,有的項目更適合以 功能 劃分。
  • 在多人協做過程當中,儘量避免衝突,或者減小衝突。

懶加載

文章開頭說到單頁面靜態資源過大,首次打開/每次版本升級後都會較慢,能夠用懶加載來拆分靜態資源,減小白屏時間,但開頭也說到懶加載也有待商榷的地方:

  • 若是異步加載較多的組件,會給靜態資源服務器/ CDN 帶來更大的訪問壓力的同時,若是當多個異步組件都被修改,形成版本號的變更,發佈的時候會大大增長 CDN 被擊穿的風險。
  • 懶加載首次加載未被緩存的異步組件白屏的問題,形成用戶體驗很差。
  • 異步加載通用組件,會在頁面可能會在網絡延時的狀況下良莠不齊的展現出來等。

這就須要咱們根據項目狀況在空間和時間上作一些權衡。

如下幾點能夠做爲簡單的參考:

  • 對於訪問量可控的項目,如公司後臺管理系統中,能夠以操做 view 爲單位進行異步加載,通用組件所有同步加載的方式。
  • 對於一些複雜度較高,實時度較高的應用類型,可採用按功能模塊拆分進行異步組件加載。
  • 若是項目想保證比較高的完整性和體驗,迭代頻率可控,不太關心首次加載時間的話,可按需使用異步加載或者直接不使用。
打包出來的 main.js 的大小,絕大部分都是在路由中引入的並註冊的視圖組件。

⑥ Service 服務層

服務層做爲項目中的另外一個核心之一,「自古以來」都是你們比較關心的地方。

不知道你是否看到過以下組織代碼方式:

views/
    pay/
        index.vue
        service.js
        components/
            a.vue
            b.vue

service.js 中寫入編寫數據來源

export const CONFIAG = {
    apple: '蘋果',
    banana: '香蕉'
}
// ...

// ① 處理業務邏輯,還彈窗
export function getBInfo ({name = '', id = ''}) {
    return this.$ajax.get('/api/info', {
        name, 
        id
    }).then({age} => {
        this.$modal.show({
            content: age
        })
    })
}

// ② 不處理業務,僅僅寫請求方法
export function getAInfo ({name = '', id = ''}) {
    return this.$ajax.get('/api/info', {
        name, 
        id
    })
}

...

簡單分析:

  • ① 就很少說了,拆分的不夠單純,當作二次開發的時候,你還得去找這彈窗到底哪裏出來的。
  • ② 看起來很美好,不摻雜業務邏輯,但不知道你與沒遇到過這樣狀況,常常會有其餘業務須要用到同樣的枚舉,請求同樣的接口,而開發其餘業務的同窗並不知道你在這裏有一份數據源,最終形成的結果就是數據源的代碼處處冗餘

我相信②在絕大多數項目中都能看到。

那麼咱們的目的就很明顯了,解決冗餘,方便使用,咱們把枚舉和請求接口的方法,經過插件,掛載到一個大對象上,注入 Vue 原型,方面業務使用便可。

目錄層級(僅供參考)

service
├── api
    ├── index.js             // 入口文件
    ├── order.js             // 訂單相關接口配置
    └── ...
├── const                   
    ├── index.js             // 入口文件
    ├── order.js             // 訂單常量接口配置
    └── ...
├── store                    // vuex 狀態管理
├── expands                  // 拓展
    ├── monitor.js           // 監控
    ├── beacon.js            // 打點
    ├── localstorage.js      // 本地存儲
    └── ...                  // 按需拓展
└── ...

抽離模型

首先抽離請求接口模型,可按照領域模型抽離 (service/api/index.js):

{
    user: [{
        name: 'info',
        method: 'GET',
        desc: '測試接口1',
        path: '/api/info',
        mockPath: '/api/info',
        params: {
            a: 1,
            b: 2
        }
    }, {
        name: 'info2',
        method: 'GET',
        desc: '測試接口2',
        path: '/api/info2',
        mockPath: '/api/info2',
        params: {
            a: 1,
            b: 2,
            b: 3
        }
    }],
    order: [{
        name: 'change',
        method: 'POST',
        desc: '訂單變動',
        path: '/api/order/change',
        mockPath: '/api/order/change',
        params: {
            type: 'SUCCESS'
        }
    }]
    ...
}

定製下須要的幾個功能:

  • 請求參數自動截取。
  • 請求參數不傳,則發送默認配置參數。
  • 得須要命名空間。
  • 經過全局配置開啓調試模式。
  • 經過全局配置來控制走本地 mock 仍是線上接口等。

插件編寫

定製好功能,開始編寫簡單的 plugins/api.js 插件:

import axios from './axios'
import _pick from 'lodash/pick'
import _assign from 'lodash/assign'
import _isEmpty from 'lodash/isEmpty'

import { assert } from 'Utils/tools'
import { API_DEFAULT_CONFIG } from 'Config'
import API_CONFIG from 'Service/api'


class MakeApi {
    constructor(options) {
        this.api = {}
        this.apiBuilder(options)
    }


    apiBuilder({
        sep = '|',
        config = {},
        mock = false, 
        debug = false,
        mockBaseURL = ''
    }) {
        Object.keys(config).map(namespace => {
            this._apiSingleBuilder({
                namespace, 
                mock, 
                mockBaseURL, 
                sep, 
                debug, 
                config: config[namespace]
            })
        })
    }
    _apiSingleBuilder({
        namespace, 
        sep = '|',
        config = {},
        mock = false, 
        debug = false,
        mockBaseURL = ''
    }) {
        config.forEach( api => {
            const {name, desc, params, method, path, mockPath } = api
            let apiname = `${namespace}${sep}${name}`,// 命名空間
                url = mock ? mockPath : path,//控制走 mock 仍是線上
                baseURL = mock && mockBaseURL
            
            // 經過全局配置開啓調試模式。
            debug && console.info(`調用服務層接口${apiname},接口描述爲${desc}`)
            debug && assert(name, `${apiUrl} :接口name屬性不能爲空`)
            debug && assert(apiUrl.indexOf('/') === 0, `${apiUrl} :接口路徑path,首字符應爲/`)

            Object.defineProperty(this.api, `${namespace}${sep}${name}`, {
                value(outerParams, outerOptions) {
                
                    // 請求參數自動截取。
                    // 請求參數不穿則發送默認配置參數。
                    let _data = _isEmpty(outerParams) ? params : _pick(_assign({}, params, outerParams), Object.keys(params))
                    return axios(_normoalize(_assign({
                        url,
                        desc,
                        baseURL,
                        method
                    }, outerOptions), _data))
                }
            })      
        })
    }       
}

function _normoalize(options, data) {
    // 這裏能夠作大小寫轉換,也能夠作其餘類型 RESTFUl 的兼容
    if (options.method === 'POST') {
        options.data = data
    } else if (options.method === 'GET') {
        options.params = data
    }
    return options
} 
// 注入模型和全局配置,並暴露出去
export default new MakeApi({
    config: API_CONFIG,
    ...API_DEFAULT_CONFIG
})['api']

掛載到 Vue 原型上,上文有說到,經過 plugins/inject.js

import api from './api'
 
export default {
    install: (Vue, options) => {
        Vue.prototype.$api = api
        // 須要掛載的都放在這裏
    }
}

使用

這樣咱們能夠在業務中愉快的使用業務層代碼:

// .vue 中
export default {
    methods: {
        test() {
            this.$api['order/info']({
                a: 1,
                b: 2
            })
        }
    }
}

即便在業務以外也可使用:

import api from 'Plugins/api'

api['order/info']({
    a: 1,
    b: 2
})

固然對於運行效率要求高的項目中,避免內存使用率過大,咱們須要改造 API,用解構的方式引入使用,最終利用 webpacktree-shaking 減小打包體積。幾個簡單的思路

通常來講,多人協做時候你們均可以先看 api 是否有對應接口,當業務量上來的時候,也確定會有人出現找不到,或者找起來比較費勁,這時候咱們徹底能夠在 請求攔截器中,把當前請求的 urlapi 中的請求作下判斷,若是有重複接口請求路徑,則提醒開發者已經配置相關請求,根據狀況是否進行二次配置便可。

最終咱們能夠拓展 Service 層的各個功能:

基礎

  • api異步與後端交互
  • const常量枚舉
  • storeVuex 狀態管理

拓展

  • localStorage:本地數據,稍微封裝下,支持存取對象便可
  • monitor監控功能,自定義蒐集策略,調用 api 中的接口發送
  • beacon打點功能,自定義蒐集策略,調用 api 中的接口發送
  • ...

constlocalStoragemonitorbeacon 根據業務自行拓展暴露給業務使用便可,思想也是同樣的,下面着重說下 store(Vuex)

插一句:若是看到這裏沒感受不妥的話,想一想上面 plugins/api.js 有沒有用 單例模式?該不應用?

⑦ 狀態管理與視圖拆分

Vuex 源碼分析能夠看我以前寫的文章

咱們是否是真的須要狀態管理?

答案是否認的,就算你的項目達到 10 萬行代碼,那也並不意味着你必須使用 Vuex,應該由 業務場景決定。

業務場景

  1. 第一類項目:業務/視圖複雜度不高,不建議使用 Vuex,會帶來開發與維護的成本,使用簡單的 vbus 作好命名空間,來解耦便可。
let vbus = new Vue()
vbus.$on('print.hello', () => {
    console.log('hello')
})

vbus.$emit('print.hello')
  1. 第二類項目:相似多人協做項目管理有道雲筆記網易雲音樂微信網頁版/桌面版應用,功能集中,空間利用率高,實時交互的項目,無疑 Vuex 是較好的選擇。這類應用中咱們能夠直接抽離業務領域模型
store
├── index.js          
├── actions.js        // 根級別 action
├── mutations.js      // 根級別 mutation
└── modules
    ├── user.js       // 用戶模塊
    ├── products.js   // 產品模塊
    ├── order.js      // 訂單模塊
    └── ...

固然對於這類項目,vuex 或許不是最好的選擇,有興趣的同窗能夠學習下 rxjs

  1. 第三類項目:後臺系統或者頁面之間業務耦合不高的項目,這類項目是佔比應該是很大的,咱們思考下這類項目:

全局共享狀態很少,可是不免在某個模塊中會有複雜度較高的功能(客服系統,實時聊天,多人協做功能等),這時候若是爲了項目的可管理性,咱們也在 store 中進行管理,隨着項目的迭代咱們不難遇到這樣的狀況:

store/
    ...
    modules/
        b.js
        ...
views/
    ...
    a/
        b.js
        ...
  • 試想下有幾十個 module,對應這邊上百個業務模塊,開發者在兩個平級目錄之間調試與開發的成本是巨大的。
  • 這些 module 能夠在項目中任一一個地方被訪問,但每每他們都是冗餘的,除了引用的功能模塊以外,基本不會再有其餘模塊引用他。
  • 項目的可維護程度會隨着項目增大而增大。

如何解決第三類項目的 store 使用問題?

先梳理咱們的目標:

  • 項目中模塊能夠自定決定是否使用 Vuex。(漸進加強)
  • 從有狀態管理的模塊,跳轉沒有的模塊,咱們不想把以前的狀態掛載到 store 上,想提升運行效率。(冗餘)
  • 讓這類項目的狀態管理變的更加可維護。(開發成本/溝通成本)

實現

咱們藉助 Vuex 提供的 registerModuleunregisterModule 一併解決這些問題,咱們在 service/store 中放入全局共享的狀態:

service/
    store/
        index.js
        actions.js
        mutations.js
        getters.js
        state.js
通常這類項目全局狀態很少,若是多了拆分 module 便可。

編寫插件生成 store 實例

import Vue from 'vue'
import Vuex from 'vuex'
import {VUEX_DEFAULT_CONFIG} from 'Config'
import commonStore from 'Service/store'

Vue.use(Vuex)

export default new Vuex.Store({
    ...commonStore,
    ...VUEX_DEFAULT_CONFIG
})

對一個須要狀態管理頁面或者模塊進行分層:

views/
    pageA/
        index.vue
        components/
            a.vue
            b.vue
            ...
        children/
            childrenA.vue
            childrenB.vue
            ...
        store/
            index.js
            actions.js
            moduleA.js  
            moduleB.js

module 中直接包含了 gettersmutationsstate,咱們在 store/index.js 中作文章:

import Store from 'Plugins/store'
import actions from './actions.js'
import moduleA from './moduleA.js'
import moduleB from './moduleB.js'

export default {
    install() {
        Store.registerModule(['pageA'], {
            actions,
            modules: {
                moduleA,
                moduleB
            },
            namespaced: true
        })
    },
    uninstall() {
        Store.unregisterModule(['pageA'])
    }
}

最終在 index.vue 中引入使用, 在頁面跳轉以前註冊這些狀態和管理狀態的規則,在路由離開以前,先卸載這些狀態和管理狀態的規則

import store from './store'
import {mapGetters} from 'vuex'
export default {
    computed: {
        ...mapGetters('pageA', ['aaa', 'bbb', 'ccc'])
    },
    beforeRouterEnter(to, from, next) {
        store.install()
        next()
    },
    beforeRouterLeave(to, from, next) {
        store.uninstall()
        next()
    }
}

固然若是你的狀態要共享到全局,就不執行 uninstall

這樣就解決了開頭的三個問題,不一樣開發者在開發頁面的時候,能夠根據頁面特性,漸進加強的選擇某種開發形式。

其餘

這裏簡單列舉下其餘方面,須要自行根據項目深刻和使用。

打包,構建

這裏網上已經有不少優化方法:dllhappypack多線程打包等,但隨着項目的代碼量級,每次 dev 保存的時候編譯的速度也是會越來越慢的,而一過慢的時候咱們就不得不進行拆分,這是確定的,而在拆分以前儘量容納更多的可維護的代碼,有幾個能夠嘗試和規避的點:

  1. 優化項目流程:這個點看起來好像沒什麼用,但改變倒是最直觀的,頁面/業務上的化簡爲繁會直接體現到代碼上,同時也會增大項目的可維護,可拓展性等。
  2. 減小項目文件層級縱向深度。
  3. 減小無用業務代碼,避免使用無用或者過大依賴(相似 moment.js 這樣的庫)等。

樣式

  • 儘量抽離各個模塊,讓整個樣式底層更加靈活,同時也應該儘量的減小冗餘。
  • 若是使用的 sass 的話,善用 %placeholder 減小無用代碼打包進來。
MPA 應用中樣式冗餘過大, %placeholder 也會給你帶來幫助。

Mock

不少大公司都有本身的 mock 平臺,當先後端定好接口格式,放入生成對應 mock api,若是沒有 mock 平臺,那就找相對好用的工具如 json-server 等。

代碼規範

請強制使用 eslint,掛在 git 的鉤子上。按期 diff 代碼,按期培訓等。

TypeScript

很是建議用 TS 編寫項目,可能寫 .vue 有些彆扭,這樣前端的大部分錯誤在編譯時解決,同時也能提升瀏覽器運行時效率,可能減小 re-optimize 階段時間等。

測試

這也是項目很是重要的一點,若是你的項目還未使用一些測試工具,請儘快接入,這裏不過多贅述。

拆分系統

當項目到達到必定業務量級時,因爲項目中的模塊過多,新同窗維護成本,開發成本都會直線上升,不得不拆分項目,後續會分享出來咱們 ToB 項目在拆分系統中的簡單實踐。

最後

時下有各類成熟的方案,這裏只是一個簡單的構建分享,裏面依賴的版本都是咱們穩定下來的版本,須要根據本身實際狀況進行升級。

項目底層構建每每會成爲前端忽略的地方,咱們既要從一個大局觀來看待一個項目或者整條業務線,又要對每一行代碼精益求精,對開發體驗不斷優化,慢慢累積後才能更好的應對未知的變化。

最後請容許我打一波小小的廣告

EROS

若是前端同窗想嘗試使用 Vue 開發 App,或者熟悉 weex 開發的同窗,能夠來嘗試使用咱們的開源解決方案 eros,雖然沒作過什麼廣告,但不徹底統計,50 個在線 APP 仍是有的,期待你的加入。

最後附上部分產品截圖~

(逃~)

相關文章
相關標籤/搜索