如何架構一箇中後臺項目的前端部分(webpack + 接口配置篇)

前言

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

當咱們的前端項目完成了技術選型階段後,接下來所要作的即是項目的構建和配置。雖說用腳手架可以幫助咱們完成基本的目錄構建和一些基礎配置,可是其餘不少實用的功能及特殊配置都須要本身動手實踐,根據實際場景進行鍼對性的設置。html

本文主要介紹下項目使用 Vue CLI 3.x 構建後,如何正確的配置 webpack 及接口部分。前端

webpack 配置

首先你須要知道的是,Vue CLI 3 的 vue-cli-service 集成了一份 webpack 的主流配置,能夠知足基礎場景的開發任務。這一份配置你能夠經過在項目根目錄運行如下命令查看:vue

vue inspect
# 或者
vue ui # 進入對應項目後點擊任務中的 inspect 任務查看
複製代碼

基本配置

若是你不想使用 vue-cli-service 集成的 webpack 默認配置,你能夠在根目錄的 vue.config.js(沒有需本身新建)中修改它。好比說修改一些基礎的配置項:java

/* 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 目錄,而後根據不一樣環境引入不一樣配置,便於修改和查看。webpack

額外配置

除此以外,針對項目的須要,你可能還須要注入一些額外的環境變量,抑或限制下 url-loader 的大小,或者移除 prefetch 插件,設置下 alias,你能夠這樣配置:ios

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 來合併相應配置,具體能夠查看官方文檔 簡單的配置方式git

本地配置

另外,針對一箇中後臺項目,本地開發時須要用到的一些配置也是必不可少的,好比代理設置(解決本地開發跨域問題),咱們可使用 devServer 來解決:github

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 配置下,同時重寫路由地址,將假地址前綴刪除,實現接口的轉發。web

特殊配置

最後,在實現了基本配置、額外配置、本地配置後,咱們再來看下特殊配置。特殊配置也就是在特殊場景下進行特殊處理的配置,好比我在架構這一中後臺項目時,一套代碼會運行在不一樣站點上(也就是發佈到不一樣服務器上),不一樣站點有些配置也是不同的,好比權限、頁面展現、接口調用地址等均可能不盡相同。

那麼如何在發佈前不手動去修改對應站點的配置,而是以一種自動化的方式來解決呢?咱們能夠跑不一樣的 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)

若是以爲本文對你有幫助,能夠關注個人微信公衆號,來這裏聊點關於前端的事情。

相關文章
相關標籤/搜索