如何架構一箇中後臺專案的前端部分(webpack + 介面配置篇)

勞卜發表於2019-09-18

前言

上篇文章:如何架構一箇中後臺專案的前端部分(技術選型篇)

當我們的前端專案完成了技術選型階段後,接下來所要做的便是專案的構建和配置。雖然說用腳手架能夠幫助我們完成基本的目錄構建和一些基礎配置,但是其他很多實用的功能及特殊配置都需要自己動手實踐,根據實際場景進行鍼對性的設定。

本文主要介紹下專案使用 Vue CLI 3.x 構建後,如何正確的配置 webpack 及介面部分。

webpack 配置

首先你需要知道的是,Vue CLI 3 的 vue-cli-service 整合了一份 webpack 的主流配置,可以滿足基礎場景的開發任務。這一份配置你可以通過在專案根目錄執行以下命令檢視:

vue inspect
# 或者
vue ui # 進入對應專案後點選任務中的 inspect 任務檢視
複製程式碼

基本配置

如果你不想使用 vue-cli-service 整合的 webpack 預設配置,你可以在根目錄的 vue.config.js(沒有需自己新建)中修改它。比如說修改一些基礎的配置項:

/* vue.config.js */

// 配置化檔案
const configs = require('./config')

// 根據環境判斷使用哪份配置
const isPro = process.env.NODE_ENV === 'production'
const cfg = isPro ? configs.build : configs.dev

module.exports = {
    ...
    
    publicPath: cfg.BASE_URI, // 部署應用包時的基本 URL
    outputDir: configs.build.outputDir, // 輸出目錄
    assetsDir: configs.build.assetsDir, // 放置生成的靜態資源目錄
    lintOnSave: cfg.lintOnSave, // 是否啟用 eslint
    productionSourceMap: configs.build.productionSourceMap, // 生產環境是否啟用 sourceMap
    
    ...
}
複製程式碼

一般情況下我們建議將配置化的東西單獨存放到配置檔案中進行管理,比如上述的 config 目錄,然後根據不同環境引入不同配置,便於修改和檢視。

額外配置

除此之外,針對專案的需要,你可能還需要注入一些額外的環境變數,抑或限制下 url-loader 的大小,或者移除 prefetch 外掛,設定下 alias,你可以這樣配置:

module.exports = {
    ...
    
    chainWebpack: config => {
        // 移除 prefetch 外掛
        config.plugins.delete('prefetch')

        // 限制 url-loader 大小
        config.module
            .rule('images')
            .use('url-loader')
            .tap(options => merge(options, {
                limit: 5120,
            }))

        // 注入環境變數
        config.plugin('define')
            .tap(args => {
                let name = 'process.env'

                // 使用 merge 保證原始值不變
                args[0][name] = merge(args[0][name], {
                    ...cfg.env
                })

                return args
            })

        // alias 設定
        config.resolve.alias
            .set('_img', resolve('src/assets/images'))
        
        // 關閉包大小告警提示
        config.performance.set('hints', false)
    },
    
    ...
}
複製程式碼

除了我們可以用 chainWebpack 這一函式來對內部的 webpack 配置進行更細粒度的修改外,我們還可以使用 configureWebpack 來合併相應配置,具體可以檢視官方文件 簡單的配置方式

本地配置

另外,針對一箇中後臺專案,本地開發時需要用到的一些配置也是必不可少的,比如代理設定(解決本地開發跨域問題),我們可以使用 devServer 來解決:

const proxyTarget = 'http://x.xxx.xxx.com' // 本地 proxy

module.exports = {
    ...
    
    devServer: {
        open: true, // 是否自動開啟瀏覽器頁面
        host: configs.dev.host, // 指定使用一個 host。預設是 localhost
        port: configs.dev.port, // 埠地址
        https: false, // 使用https提供服務
        progress: true,
        // string | Object 代理設定
        proxy: {
            '/LOCAL_URL': {
                target: proxyTarget,
                changeOrigin: true,
                pathRewrite: {
                    '^/LOCAL_URL': ''
                }
            }
        },
    }
    
    ...
}
複製程式碼

上方我們除了配置了本地啟動的 host 和 埠外,還進行了 proxy 的配置。當我們的介面匹配到 /LOCAL_URL(在介面封裝篇會講解) 欄位時,就會將請求服務轉發到其 target 配置下,同時重寫路由地址,將假地址字首刪除,實現介面的轉發。

特殊配置

最後,在實現了基本配置、額外配置、本地配置後,我們再來看下特殊配置。特殊配置也就是在特殊場景下進行特殊處理的配置,比如我在架構這一中後臺專案時,一套程式碼會執行在不同站點上(也就是釋出到不同伺服器上),不同站點有些配置也是不一樣的,比如許可權、頁面展示、介面呼叫地址等都可能不盡相同。

那麼如何在釋出前不手動去修改對應站點的配置,而是以一種自動化的方式來解決呢?我們可以跑不同的 npm 命令來實現:

/* package.json */

{
    "scripts": {
        "local": "vue-cli-service serve",
        "a_dev_build": "vue-cli-service build --site a --env development",
        "b_dev_build": "vue-cli-service build --site b --env development",
        "a_build": "vue-cli-service build --site a --env production",
        "b_build": "vue-cli-service build --site b --env production",
    },
}
複製程式碼

比如說,當我們要釋出 a 站點到預發環境時,我們只需要在釋出前(可以交給釋出系統執行)執行 npm run a_dev_build 命令,然後 vue.config.js 中去讀取相應配置,注入全域性環境變數即可:

const site = process.argv.slice(4, 5)[0] || 'a' // 當前執行站點
const env = process.argv.slice(6, 7)[0] || 'development' // 當前執行前端環境
const local = process.env.npm_lifecycle_event === 'local' ? 'on' : 'off' // 是否呼叫本地介面

module.exports = {
    ...
    
    chainWebpack: config => {
        // 注入環境變數
        config.plugin('define')
            .tap(args => {
                let name = 'process.env'

                // 使用 merge 保證原始值不變
                args[0][name] = merge(args[0][name], {
                    SITE: JSON.stringify(site),
                    LOCAL: JSON.stringify(local),
                    CLIENT_ENV: JSON.stringify(env)
                })

                return args
            })
    }
    
    ...
}
複製程式碼

最後在前端環境中再去根據不同的站點、不同的環境執行不同的程式碼(包括介面、介面顯示等)即可。當然你也可以換其他方式實現相同的功能,比如使用 cross-env 或者 mode 這樣的工具或引數。

介面封裝

前端的動態資料互動離不開服務端提供的介面,在一個前後端分離的中後臺專案中,介面的請求和響應是必不可少的。

那麼在架構一箇中後臺系統的時候,我們如何有效的管理和封裝介面,提高專案介面呼叫的統一性、可維護性,以及在後端介面還沒有開發完成,在僅有契約的基礎上我們如何有效的模擬介面的呼叫呢?

接下來便會對以上問題提供個人解決方案供大家參考。

1. 不封裝存在的問題

首先談談介面封裝,因為我們使用的請求庫是 axios,所以接下來的示例都以 axios 來舉例。

那麼在沒有封裝介面的專案中,你可能隨處可見介面的直接呼叫方法,比如像這樣:

axios.post('/user', {
    firstName: 'zhang',
    lastName: 'san'
  })
  .then(function (response) {
    console.log(response);
  });
...

axios.get('/user?ID=12345')
    .then(function (response) {
    // handle success
    console.log(response);
  });
複製程式碼

這樣的寫法會存在一些缺點,主要有以下幾點:

  • 介面 url 沒有統一管理,散落在專案的各個地方
  • 如果需要在介面呼叫成功和失敗時做一些處理,需要在每個地方進行新增
  • 特殊請求頭以及取消請求方法需要單獨進行編寫

2. 修改預設配置

既然會存在上述問題,那麼我們就需要去解決。在之前介紹的專案目錄結構中,我們會發現有 services 資料夾,這就是用來存放封裝的介面和呼叫的方法的。

在介面封裝過程中,首先我們需要修改 axios 的預設配置,如下:

import axios from 'axios'

// 修改預設配置
axios.defaults.headers.post['Content-Type'] = 'application/json'
axios.defaults.headers.get['Content-Type'] = 'application/json'
axios.defaults.withCredentials = true // 表示是否跨域訪問請求
複製程式碼

可以把你常用的請求頭的 Content-Type 設定為預設值,同時開啟跨域功能。

3. 設定攔截器

接下來需要編寫下請求和響應的攔截器,來對請求和響應進行適時攔截,比如再呼叫重複介面時,取消上一次未完成的相同請求:

const CancelToken = axios.CancelToken
const httpPending = [] // 用於儲存每個ajax請求的取消函式和ajax標識

// 取消請求方法
const cancelHttp = (name, config = {}) => {
    httpPending.forEach((e, i) => {
        if (e.n === name || e.n === config.xhrName) { // 當前請求在陣列中存在時執行函式體
            e.f() // 執行取消操作
            httpPending.splice(i, 1) // 把這條記錄從陣列中移除
        }
    })
}

// 請求攔截器
axios.interceptors.request.use(config => {
    // 取消上一次未完成的相同請求,注意專案中是否存在風險
    cancelHttp(null, config)

    config.cancelToken = new CancelToken(c => {
        if (config.xhrName) {
            httpPending.push({
                n: config.xhrName,
                u: `${config.url}&${config.method}`,
                f: c
            })
        }
    })

    return config
}, error => Promise.reject(error))

// 響應攔截器
axios.interceptors.response.use(res => {
    cancelHttp(null, res.config) // 響應成功把已經完成的請求從 httpPending 中移除

    checkStatus(res) // 校驗響應狀態
    const response = res.data

    return Promise.resolve(response)
}, error => Promise.reject(error))
複製程式碼

上述兩個攔截器主要做了重複請求的攔截功能,在請求頭中將請求的取消請求方法和識別符號號插入陣列中,當然之前需要去陣列中查詢是否存在相同請求,存在則提前取消請求,最後在響應時把已經完成的請求資訊從陣列中移除。

這裡為了避免風險,我們需要在介面呼叫的地方手動傳入一個 xhrName 標識這個請求名稱才會取消重複呼叫該介面的請求,其餘介面不做處理。

同時我們也將 cancelHttp 暴露給全域性,滿足手動取消請求的需要:

Vue.prototype.$cancelHttp = cancelHttp
複製程式碼

4. 暴露呼叫方法

當我們完成了針對 axios 的一些設定後,我們最終的目的是使用它來請求和處理介面,那麼是時候對請求呼叫的方法進行封裝和暴露了:

import uriConfig from '@/config/apiUriConf'
import GLOBAL from '@/config/global' // 全域性變數

...

export default class Http {
    static async request(method, url, opts, type) {
        // 開啟本地 mock 的話,不使用介面域名
        let hostName = GLOBAL.mockLocal ? '' : uriConfig.apiUrl
        
        // 特殊介面域名
        let otherName = GLOBAL.mockLocal ? '' : (uriConfig[type] || '')
        
        // type 存在則使用對應的介面,否則使用通用介面
        let uri = type ? `${otherName}${url}` : `${hostName}${url}`
        
        // 介面別名、請求方式及url
        let params = {
            xhrName: (opts && opts.name) || '',
            method,
            url: uri,
        }
        
        // 請求資料
        params.data = opts.body || {}
        
        // 設定特殊請求頭
        if (opts.type === 'formData') {
            params.headers = {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        }

        return axios(params)
    }

    static get(url, opts) {
        return this.request('GET', url, opts)
    }

    static put(url, opts) {
        return this.request('PUT', url, opts)
    }

    static post(url, opts) {
        return this.request('POST', url, opts)
    }

    static patch(url, opts) {
        return this.request('PATCH', url, opts)
    }

    static delete(url, opts) {
        return this.request('DELETE', url, opts)
    }
}
複製程式碼

上方我們將封裝了一個 Http 類,其中包含了 get、post 等請求方法,這些請求方法內部都會去呼叫 request 方法,該方法會通過傳入的不同引數執行原始 axios 的請求呼叫,返回一個 Promise。

5. 引用呼叫方法

那麼哪裡去使用這個 Http 類呢,我們可以在 services 資料夾中再建立其他介面管理檔案,比如 user.js,用於存放使用者相關的介面:

// user.js
import Http from './http'

// 獲取使用者資訊
export const getUserInfo = params => Http.post('/getUserInfo', {
    body: params
})
複製程式碼

最後在呼叫的地方引入 getUserInfo 方法,傳入對應的引數物件即可。

import { getUserInfo } from '@services/user'

getUserInfo({
    token: 'xxx'
})
複製程式碼

如此我們便完成了介面封裝的基本功能。

介面模擬

剛剛在封裝介面的時候,我們看到了 GLOBAL.mockLocal 這一全域性變數,用於判斷是否開啟和關閉介面的 mock。

首先什麼是介面 mock?意思就是模仿介面返回的資料。那麼為什麼要模仿呢?因為在介面還沒開發完的場景下,前端在得知介面文件和格式後想本地模擬資料請求的功能,這時候開啟介面 mock 就會變得十分簡單。

這裡我們通常會用一個比較實用的庫 mockjs 來實現我們的功能,用法很簡單,在我們事先建立的 mock 資料夾下建立 index.js 檔案:

import GLOBAL from '@/config/global'

// 全域性變數
if (GLOBAL.mockLocal) {
    let Mock = require('mockjs')

    // 使用者資訊介面
    Mock.mock('/getUserInfo', () => ({
        result: {
            name: '張三',
            sex: 'man',
            age: 12,
        },
        status: true, // 資料狀態
        statusCode: '200', // 狀態碼
        message: '請求成功' // 提示資訊
    })
}
複製程式碼

檔案中我們還是以使用者資訊介面為例,當開啟全域性 mock 的時候,我們便使用 mockjs 來模擬資料的返回。

當然你還需要的專案的入口檔案中引用下:

// main.js
import '@/mock' // 全域性變數
複製程式碼

之後配合介面封裝時判斷開啟 mock 就不使用介面域名的功能,我們在正常呼叫介面的時候便能直接獲取自己模擬的資料結果。

關於更多 mockjs 的用法可以參考官方文件:github.com/nuysoft/Moc…

結語

本文結合實際情況針對 webpack 進行了不同程度的配置展示,當然除此之外還有很多配置項和配置方法沒有一一展示,比如開啟 Gzip 壓縮、使用包分析工具等,大家需要在此基礎上學會舉一反三,才能靈活的架構一箇中後臺專案的 webpack 配置。

同時本文介紹的介面配置和 mock 其實不僅僅適用於中後臺系統,大多數前端應用都可以參考。這裡大家可以思考其實現的思路,至於具體實現方式都可能不盡相同。

那麼下篇文章我會給大家帶來《如何架構一箇中後臺專案的前端部分(國際化 + 路由配置篇)

關於

轉載請註明來自 —— 微信公眾號:前端呼啦圈(Love-FED)

如果覺得本文對你有幫助,可以關注我的微信公眾號,來這裡聊點關於前端的事情。

如何架構一箇中後臺專案的前端部分(webpack + 介面配置篇)

相關文章