前端 mock 完美解決方案實(shí)戰(zhàn)

轉(zhuǎn)自:redareba
https://segmentfault.com/a/1190000038320901
寫在前面,本文閱讀需要一定Nodejs的相關(guān)知識(shí),因?yàn)闀?huì)擴(kuò)展webpack的相關(guān)功能,并且實(shí)現(xiàn)需要遵守一定約定和Ajax封裝。
沉淀的腳手架也放到Github上供給同學(xué)參考React-Starter:https://github.com/rebareba/react-starter, 使用手冊(cè)還沒(méi)寫完善, 整體思路和React還是Vue無(wú)關(guān),如果對(duì)大家有收獲記得Star下。
它有這些功能:
開(kāi)發(fā)打包有不同配置 eslint 驗(yàn)證 代碼風(fēng)格統(tǒng)一 commit 規(guī)范驗(yàn)證 接口mock 熱更新 異步組件
Mock功能介紹
市面上講前端mock怎么做的文章很多,整體上閱讀下來(lái)的沒(méi)有一個(gè)真正站在前端角度上讓我覺(jué)得強(qiáng)大和易用的。下面就說(shuō)下我期望的前端mock要有哪些功能:
mock功能和前端代碼解耦 一個(gè)接口支持多種mock情況 無(wú)需依賴另外的后端服務(wù)和第三方庫(kù) 能在network看到mock接口的請(qǐng)求且能區(qū)分 mock數(shù)據(jù)、接口配置和頁(yè)面在同一個(gè)目錄下 mock配置改變無(wú)需重啟前端dev 生產(chǎn)打包可以把mock數(shù)據(jù)注入到打包的js中走前端mock 對(duì)于后端已有的接口也能快速把Response數(shù)據(jù)轉(zhuǎn)化為mock數(shù)據(jù)
上面的這些功能我講其中幾點(diǎn)的作用:
對(duì)于第7點(diǎn)的作用是后續(xù)項(xiàng)目開(kāi)發(fā)完成,在完全沒(méi)有開(kāi)發(fā)后端服務(wù)的情況下,也可以進(jìn)行演示。這對(duì)于一些ToB定制的項(xiàng)目來(lái)沉淀項(xiàng)目地圖(案例)很有作用。
對(duì)于第8點(diǎn)在開(kāi)發(fā)環(huán)境后端服務(wù)經(jīng)常不穩(wěn)定下,不依賴后端也能做頁(yè)面開(kāi)發(fā),核心是能實(shí)現(xiàn)一鍵生成mock數(shù)據(jù)。
配置解耦
耦合情況
什么是前端配置解耦,首先讓我們看下平時(shí)配置耦合情況有哪些:
webpack-dev后端測(cè)試環(huán)境變了需要改git跟蹤的代碼 dev和build的時(shí)候 需要改git跟蹤的代碼 開(kāi)發(fā)的時(shí)候想這個(gè)接口mock 需要改git跟蹤的代碼 mockUrl ,mock?
如何解決
前端依賴的配置解耦的思路是配置文件conf.json是在dev或build的時(shí)候動(dòng)態(tài)生成的,然后該文件在前端項(xiàng)目引用:
├──?config
│???├──?conf.json????????????????????????????????????#?git?不跟蹤
│???├──?config.js????????????????????????????????????#?git?不跟蹤
│???├──?config_default.js
│???├──?index.js
│???└──?webpack.config.js
├──?jsconfig.json
├──?mock.json????????????????????????????????????????????#?git?不跟蹤
webpack配置文件引入js的配置,生成conf.json
//?config/index.js
const?_?=?require("lodash");
let?config?=?_.cloneDeep(require("./config_default"))
try?{
??const?envConfig?=?require('./config')?//?eslint-disable-line
??config?=?_.merge(config,?envConfig);
}?catch?(e)?{
????//?
}
module.exports?=?config;
默認(rèn)使用config_default.js 的內(nèi)容,如果有config.js 則覆蓋,開(kāi)發(fā)的時(shí)候復(fù)制config_default.js 為config.js 后續(xù)相關(guān)配置可以修改config.js即可。
//?config/config_default.js
const?pkg?=?require("../package.json");
module.exports?=?{
??projectName:?pkg.name,
??version:?pkg.version,
??port:?8888,
??proxy:?{
????"/render-server/api/*":?{
??????target:?`http://192.168.1.8:8888`,
??????changeOrigin:?true,?//?支持跨域請(qǐng)求
??????secure:?true,?//?支持?https
????},
??},
??...
??conf:?{
????dev:?{
??????title:?"前端模板",
??????pathPrefix:?"/react-starter",?//?統(tǒng)一前端路徑前綴
??????apiPrefix:?"/api/react-starter",?//
??????debug:?true,
??????delay:?500,????//?mock數(shù)據(jù)模擬延遲
??????mock:?{
????????//?"global.login":?"success",
????????//?"global.loginInfo":?"success",
??????}
????},
????build:?{
??????title:?"前端模板",
??????pathPrefix:?"/react-starter",
??????apiPrefix:?"/api/react-starter",
??????debug:?false,
??????mock:?{}
????}
??}
};
在開(kāi)發(fā)或打包的時(shí)候根據(jù)環(huán)境變量使用conf.dev或conf.build 生成conf.json文件內(nèi)容
//?package.json
{
??"name":?"react-starter",
??"version":?"1.0.0",
??"description":?"react前端開(kāi)發(fā)腳手架",
??"main":?"index.js",
??"scripts":?{
????"start":?"webpack-dev-server?--config?'./config/webpack.config.js'?--open?--mode?development",
????"build":?"cross-env?BUILD_ENV=VERSION?webpack?--config?'./config/webpack.config.js'?--mode?production?--progress?--display-modules?&&?npm?run?tar",
????"build-mock":?"node?./scripts/build-mock.js?"
??},
??...
}
指定webpack路徑是./config/webpack.config.js
然后在webpack.config.js中引入配置并生成conf.json文件
//?config/webpack.config.js
const?config?=?require('.')
const?env?=?process.env.BUILD_ENV???'build'?:?'dev'
const?confJson?=?env?===?'build'???config.conf.build?:?config.conf.dev
fs.writeFileSync(path.join(__dirname,?'./conf.json'),??JSON.stringify(confGlobal,?null,?'\t'))
引用配置
在src/common/utils.jsx文件中暴露出配置項(xiàng),配置也可以通過(guò)window.conf來(lái)覆蓋
//?src/common/utils.jsx
import?conf?from?'@/config/conf.json'
export?const?config?=?Object.assign(conf,?window.conf)
然后就可以在各個(gè)頁(yè)面中使用
import?{config}?from?'@src/common/utils'
class?App?extends?Component?{
??render()?{
????return?(
??????history={history}>
????????
??????????${config.pathPrefix}`}?component={Home}?/>
??????????"/"?to={`${config.pathPrefix}`}?/>
????????
??????
????)
??}
}
ReactDOM.render(
????? ,
??document.getElementById('root'),
)
Mock實(shí)現(xiàn)
效果
為了實(shí)現(xiàn)我們想要的mock的相關(guān)功能,首先是否開(kāi)啟mock的配置解耦可以通過(guò)上面說(shuō)的方式來(lái)實(shí)現(xiàn),我們一般在頁(yè)面異步請(qǐng)求的時(shí)候都會(huì)目錄定義一個(gè)io.js的文件, 里面定義了當(dāng)前頁(yè)面需要調(diào)用的相關(guān)后端接口:
//?src/pages/login/login-io.js
import?{createIo}?from?'@src/io'
const?apis?=?{
??//?登錄
??login:?{
????method:?'POST',
????url:?'/auth/login',
??},
??//?登出
??logout:?{
????method:?'POST',
????url:?'/auth/logout',
??},
}
export?default?createIo(apis,?'login')?//?對(duì)應(yīng)login-mock.json
上面定義了登錄和登出接口,我們希望對(duì)應(yīng)開(kāi)啟的mock請(qǐng)求能使用當(dāng)前目錄下的login-mock.json文件的內(nèi)容
//?src/pages/login/login-mock.json
{
????"login":?{
????????"failed":?{
????????????"success":?false,
????????????"code":?"ERROR_PASS_ERROR",
????????????"content":?null,
????????????"message":?"賬號(hào)或密碼錯(cuò)誤!"
????????},
????????"success":?{
????????????"success":?true,
????????????"code":?0,
????????????"content":?{
????????????????"name":?"admin",
????????????????"nickname":?"超級(jí)管理員",
????????????????"permission":?15
????????????},
????????????"message":?""
????????}
????},
????"logout":?{
????????"success":?{
????????????"success":?true,
????????????"code":?0,
????????????"content":?null,
????????????"message":?""
????????}
????}
}
在調(diào)用logout登出這個(gè)Ajax請(qǐng)求的時(shí)候且我們的conf.json中配置的是"login.logout": "success"?就返回login-mock.json中的login.success 的內(nèi)容,配置沒(méi)有匹配到就請(qǐng)求轉(zhuǎn)發(fā)到后端服務(wù)。
//?config/conf.json
{
????"title":?"前端后臺(tái)模板",
????"pathPrefix":?"/react-starter",
????"apiPrefix":?"/api/react-starter",
????"debug":?true,
????"delay":?500,
????"mock":?{
????????"login.logout":?"success"
????}
}
這是我們最終要實(shí)現(xiàn)的效果,這里有一個(gè)約定:項(xiàng)目目錄下所有以-mock.jsom文件結(jié)尾的文件為mock文件,且文件名不能重復(fù)。
思路
在webpack配置項(xiàng)中devServer的proxy配置接口的轉(zhuǎn)發(fā)設(shè)置,接口轉(zhuǎn)發(fā)使用了功能強(qiáng)大的 http-proxy-middleware:https://github.com/chimurai/http-proxy-middleware 軟件包, 我們約定proxy的配置格式是:
??proxy:?{
????"/api/react-starter/*":?{
??????target:?`http://192.168.90.68:8888`,
??????changeOrigin:?true,
??????secure:?true,
??????//?onError:?(),
??????//?onProxyRes,
??????//?onProxyReq??
????},
??},
它有幾個(gè)事件觸發(fā)的配置:
option.onError?出現(xiàn)錯(cuò)誤 option.onProxyRes?后端響應(yīng)后 option.onProxyReq?請(qǐng)求轉(zhuǎn)發(fā)前 option.onProxyReqWs option.onOpen option.onClose
所以我們需要定制這幾個(gè)事情的處理,主要是請(qǐng)求轉(zhuǎn)發(fā)前和請(qǐng)求處理后
onProxyReq
想在這里來(lái)實(shí)現(xiàn)mock的處理, 如果匹配到了mock數(shù)據(jù)我們就直接響應(yīng),就不轉(zhuǎn)發(fā)請(qǐng)求到后端。怎么做呢:思路是依賴請(qǐng)求頭,dev情況下前端在調(diào)用的時(shí)候能否注入約定好的請(qǐng)求頭 告訴我需要尋找哪個(gè)mock數(shù)據(jù)項(xiàng), 我們約定Header:
mock-key?來(lái)匹配mock文件如login-mock.json的內(nèi)容, 如loginmock-method?來(lái)匹配對(duì)應(yīng)文件內(nèi)容的方法項(xiàng) 如logout
然后conf.json中mock配置尋找到具體的響應(yīng)項(xiàng)目如:"login.logout": "success/failed"的內(nèi)容
onProxyRes
如果調(diào)用了真實(shí)的后端請(qǐng)求,就把請(qǐng)求的響應(yīng)數(shù)據(jù)緩存下來(lái),緩存到api-cache目錄下文件格式mock-key.mock-method.json
├──?api-cache????????????????????????????????????#?git?不跟蹤
│???├──?login.login.json
│???└──?login.logout.json
//?api-cache/global.logout.json
{
????"success":?{
????????"date":?"2020-11-17?05:32:17",
????????"method":?"POST",
????????"path":?"/render-server/api/logout",
????????"url":?"/render-server/api/logout",
????????"resHeader":?{
????????????"content-type":?"application/json;?charset=utf-8",
??????...
????????},
????????"reqHeader":?{
????????????"host":?"127.0.0.1:8888",
????????????"mock-key":?"login",
????????????"mock-method":?"logout"
??????...
????????},
????????"query":?{},
????????"reqBody":?{},
????????"resBody":?{
????????????"success":?true,
????????????"code":?0,
????????????"content":?null,
????????????"message":?""
????????}
????}
}
這樣做的目的是為了后續(xù)實(shí)現(xiàn)一鍵生成mock文件。
前端接口封裝
使用
上面我們看到定義了接口的io配置:
//?src/pages/login/login-io.js
import?{createIo}?from?'@src/io'
const?apis?=?{
??//?登錄
??login:?{
????method:?'POST',
????url:?'/auth/login',
??},
??//?登出
??logout:?{
????method:?'POST',
????url:?'/auth/logout',
??},
}
export?default?createIo(apis,?'login')?//?login注冊(cè)到header的mock-key
我們?cè)趕tore中使用
//?src/pages/login/login-store.js
import?{observable,?action,?runInAction}?from?'mobx'
import?io?from?'./login-io'
//?import?{config,?log}?from?'./utils'
export?class?LoginStore?{
??//?用戶信息
??@observable?userInfo
??//?登陸操作
[email protected]
??async?login(params)?{
????const?{success,?content}?=?await?io.login(params)
????if?(!success)?return
????runInAction(()?=>?{
??????this.userInfo?=?content
????})
??}
}
export?default?LoginStore
通過(guò)?createIo(apis, 'login')?的封裝在調(diào)用的時(shí)候就可以非常簡(jiǎn)單的來(lái)傳遞請(qǐng)求參數(shù),簡(jiǎn)單模式下會(huì)判斷參數(shù)是到body還是到query中。復(fù)雜的也可以支持比如可以header,query, body等這里不演示了。
createIo 請(qǐng)求封裝
這個(gè)是前端接口封裝的關(guān)鍵地方,也是mock請(qǐng)求頭注入的地方
//?src/io/index.jsx
import?{message,?Modal}?from?'antd'
import?{config,?log,?history}?from?'@src/common/utils'
import?{ERROR_CODE}?from?'@src/common/constant'
import?creatRequest?from?'@src/common/request'
let?mockData?=?{}
try?{
??//?eslint-disable-next-line?global-require,?import/no-unresolved
??mockData?=?require('@/mock.json')
}?catch?(e)?{
??log(e)
}
let?reloginFlag?=?false
//?創(chuàng)建一個(gè)request
export?const?request?=?creatRequest({
??//?自定義的請(qǐng)求頭
??headers:?{'Content-Type':?'application/json'},
??//?配置默認(rèn)返回?cái)?shù)據(jù)處理
??action:?(data)?=>?{
????//?統(tǒng)一處理未登錄的彈框
????if?(data.success?===?false?&&?data.code?===?ERROR_CODE.UN_LOGIN?&&?!reloginFlag)?{
??????reloginFlag?=?true
??????//?TODO?這里可能統(tǒng)一跳轉(zhuǎn)到?也可以是彈窗點(diǎn)擊跳轉(zhuǎn)
??????Modal.confirm({
????????title:?'重新登錄',
????????content:?'',
????????onOk:?()?=>?{
??????????//?location.reload()
??????????history.push(`${config.pathPrefix}/login?redirect=${window.location.pathname}${window.location.search}`)
??????????reloginFlag?=?false
????????},
??????})
????}
??},
??//?是否錯(cuò)誤顯示message
??showError:?true,
??message,
??//?是否以拋出異常的方式?默認(rèn)false?{success:?boolean判斷}
??throwError:?false,
??//?mock?數(shù)據(jù)請(qǐng)求的等待時(shí)間
??delay:?config.delay,
??//?日志打印
??log,
})
//?標(biāo)識(shí)是否是簡(jiǎn)單傳參數(shù),?值為true標(biāo)識(shí)復(fù)雜封裝
export?const?rejectToData?=?Symbol('flag')
/**
?*?創(chuàng)建請(qǐng)求IO的封裝
?*?@param?ioContent?{any?{?url:?string?method?:?string?mock?:?any?apiPrefix?:?string}}
??}
?*?@param?name?mock數(shù)據(jù)的對(duì)應(yīng)文件去除-mock.json后的
?*/
export?const?createIo?=?(ioContent,?name?=?'')?=>?{
??const?content?=?{}
??Object.keys(ioContent).forEach((key)?=>?{
????/**
?????*?@param?{baseURL?:?string,?rejectToData?:?boolean,??params?:?{},?query?:?{},?timeout?:?number,?action?(data:?any):?any,?headers?:?{},??body?:?any,?data?:?any,???mock?:?any}
?????*?@returns?{message,?content,?code,success:?boolean}
?????*/
????content[key]?=?async?(options?=?{})?=>?{
??????//?這里判斷簡(jiǎn)單請(qǐng)求封裝?rejectToData=true?表示復(fù)雜封裝
??????if?(!options[rejectToData])?{
????????options?=?{
??????????data:?options,
????????}
??????}
??????delete?options[rejectToData]
??????if?(
????????config.debug?===?false?&&
????????name?&&
????????config.mock?&&
????????config.mock[`${name}.${key}`]?&&
????????mockData[name]?&&
????????mockData[name][key]
??????)?{?//?判斷是否是生產(chǎn)打包?mock注入到代碼中
????????ioContent[key].mock?=?JSON.parse(JSON.stringify(mockData[name][key][config.mock[`${name}.${key}`]]))
??????}?else?if?(name?&&?config.debug?===?true)?{?//注入?mock請(qǐng)求頭
????????if?(options.headers)?{
??????????options.headers['mock-key']?=?name
??????????options.headers['mock-method']?=?key
????????}?else?{
??????????options.headers?=?{'mock-key':?name,?'mock-method':?key}
????????}
??????}
??????const?option?=?{...ioContent[key],?...options}
??????option.url?=?((option.apiPrefix???option.apiPrefix?:?config.apiPrefix)?||?'')?+?option.url
??????return?request(option)
????}
??})
??return?content
}
這里對(duì)request也做進(jìn)一步的封裝,配置項(xiàng)設(shè)置了一些默認(rèn)的處理設(shè)置。比如通用的請(qǐng)求響應(yīng)失敗的是否有一個(gè)message, 未登錄的情況是否有一個(gè)彈窗提示點(diǎn)擊跳轉(zhuǎn)登陸頁(yè)。如果你想定義多個(gè)通用處理可以再創(chuàng)建一個(gè)request2和createIo2。
request封裝axios
是基于axios的二次封裝, 并不是非常通用,主要是在約定的請(qǐng)求失敗和成功的處理有定制,如果需要可以自己修改使用。
import?axios?from?'axios'
//?配置接口參數(shù)
//?declare?interface?Options?{
//???url:?string
//???baseURL?:?string
//???//?默認(rèn)GET
//???method?:?Method
//???//?標(biāo)識(shí)是否注入到data參數(shù)
//???rejectToData?:?boolean
//???//?是否直接彈出message?默認(rèn)是
//???showError?:?boolean
//???//?指定?回調(diào)操作?默認(rèn)登錄處理
//???action?(data:?any):?any
//???headers?:?{
//?????[index:?string]:?string
//???}
//???timeout?:?number
//???//?指定路由參數(shù)
//???params?:?{
//?????[index:?string]:?string
//???}
//???//?指定url參數(shù)
//???query?:?any
//???//?指定body?參數(shù)
//???body?:?any
//???//?混合處理?Get到url,?delete?post?到body,?也替換路由參數(shù)?在createIo封裝
//???data?:?any
//???mock?:?any
//?}
//?ajax?請(qǐng)求的統(tǒng)一封裝
//?TODO?1.?對(duì)jsonp請(qǐng)求的封裝?2.?重復(fù)請(qǐng)求
/**
?*?返回ajax?請(qǐng)求的統(tǒng)一封裝
?*?@param?Object?option?請(qǐng)求配置
?*?@param?{boolean}?opts.showError?是否錯(cuò)誤調(diào)用message的error方法
?*?@param?{object}?opts.message??包含?.error方法?showError?true的時(shí)候調(diào)用
?*?@param?{boolean}?opts.throwError?是否出錯(cuò)拋出異常
?*?@param?{function}?opts.action??包含?自定義默認(rèn)處理?比如未登錄的處理
?*?@param?{object}?opts.headers??請(qǐng)求頭默認(rèn)content-type:?application/json
?*?@param?{number}?opts.timeout??超時(shí)?默認(rèn)60秒
?*?@param?{number}?opts.delay???mock請(qǐng)求延遲
?*?@returns?{function}?{params,?url,?headers,?query,?data,?mock}?data混合處理?Get到url,?delete?post?到body,?也替換路由參數(shù)?在createIo封裝
?*/
export?default?function?request(option?=?{})?{
??return?async?(optionData)?=>?{
????const?options?=?{
??????url:?'',
??????method:?'GET',
??????showError:?option.showError?!==?false,
??????timeout:?option.timeout?||?60?*?1000,
??????action:?option.action,
??????...optionData,
??????headers:?{'X-Requested-With':?'XMLHttpRequest',?...option.headers,?...optionData.headers},
????}
????//?簡(jiǎn)單請(qǐng)求處理
????if?(options.data)?{
??????if?(typeof?options.data?===?'object')?{
????????Object.keys(options.data).forEach((key)?=>?{
??????????if?(key[0]?===?':'?&&?options.data)?{
????????????options.url?=?options.url.replace(key,?encodeURIComponent(options.data[key]))
????????????delete?options.data[key]
??????????}
????????})
??????}
??????if?((options.method?||?'').toLowerCase()?===?'get'?||?(options.method?||?'').toLowerCase()?===?'head')?{
????????options.query?=?Object.assign(options.data,?options.query)
??????}?else?{
????????options.body?=?Object.assign(options.data,?options.body)
??????}
????}
????//?路由參數(shù)處理
????if?(typeof?options.params?===?'object')?{
??????Object.keys(options.params).forEach((key)?=>?{
????????if?(key[0]?===?':'?&&?options.params)?{
??????????options.url?=?options.url.replace(key,?encodeURIComponent(options.params[key]))
????????}
??????})
????}
????//?query?參數(shù)處理
????if?(options.query)?{
??????const?paramsArray?=?[]
??????Object.keys(options.query).forEach((key)?=>?{
????????if?(options.query[key]?!==?undefined)?{
??????????paramsArray.push(`${key}=${encodeURIComponent(options.query[key])}`)
????????}
??????})
??????if?(paramsArray.length?>?0?&&?options.url.search(/\?/)?===?-1)?{
????????options.url?+=?`?${paramsArray.join('&')}`
??????}?else?if?(paramsArray.length?>?0)?{
????????options.url?+=?`&${paramsArray.join('&')}`
??????}
????}
????if?(option.log)?{
??????option.log('request??options',?options.method,?options.url)
??????option.log(options)
????}
????if?(options.headers['Content-Type']?===?'application/json'?&&?options.body?&&?typeof?options.body?!==?'string')?{
??????options.body?=?JSON.stringify(options.body)
????}
????let?retData?=?{success:?false}
????//?mock?處理
????if?(options.mock)?{
??????retData?=?await?new?Promise((resolve)?=>
????????setTimeout(()?=>?{
??????????resolve(options.mock)
????????},?option.delay?||?500),
??????)
????}?else?{
??????try?{
????????const?opts?=?{
??????????url:?options.url,
??????????baseURL:?options.baseURL,
??????????params:?options.params,
??????????method:?options.method,
??????????headers:?options.headers,
??????????data:?options.body,
??????????timeout:?options.timeout,
????????}
????????const?{data}?=?await?axios(opts)
????????retData?=?data
??????}?catch?(err)?{
????????retData.success?=?false
????????retData.message?=?err.message
????????if?(err.response)?{
??????????retData.status?=?err.response.status
??????????retData.content?=?err.response.data
??????????retData.message?=?`瀏覽器請(qǐng)求非正常返回:?狀態(tài)碼?${retData.status}`
????????}
??????}
????}
????//?自動(dòng)處理錯(cuò)誤消息
????if?(options.showError?&&?retData.success?===?false?&&?retData.message?&&?option.message)?{
??????option.message.error(retData.message)
????}
????//?處理Action
????if?(options.action)?{
??????options.action(retData)
????}
????if?(option.log?&&?options.mock)?{
??????option.log('request?response:',?JSON.stringify(retData))
????}
????if?(option.throwError?&&?!retData.success)?{
??????const?err?=?new?Error(retData.message)
??????err.code?=?retData.code
??????err.content?=?retData.content
??????err.status?=?retData.status
??????throw?err
????}
????return?retData
??}
}
一鍵生成mock
根據(jù)api-cache下的接口緩存和定義的xxx-mock.json文件來(lái)生成。
#?"build-mock":?"node?./scripts/build-mock.js"
#?所有:
npm?run?build-mock?mockAll?
#?單個(gè)mock文件:
npm?run?build-mock?login
#?單個(gè)mock接口:
npm?run?build-mock?login.logout
#?復(fù)雜?
npm?run?build-mock?login.logout?user
具體代碼參考 build-mock.js:https://github.com/rebareba/react-starter/blob/main/scripts/build-mock.js
mock.json文件生成
為了在build打包的時(shí)候把mock數(shù)據(jù)注入到前端代碼中去,使得mock.json文件內(nèi)容盡可能的小,會(huì)根據(jù)conf.json的配置項(xiàng)來(lái)動(dòng)態(tài)生成mock.json的內(nèi)容,如果build里面沒(méi)有開(kāi)啟mock項(xiàng),內(nèi)容就會(huì)是一個(gè)空json數(shù)據(jù)。當(dāng)然后端接口代理處理內(nèi)存中也映射了一份該mock.json的內(nèi)容。這里需要做幾個(gè)事情:
-根據(jù)配置動(dòng)態(tài)生成mock.json的內(nèi)容 -監(jiān)聽(tīng)config文件夾 判斷關(guān)于mock的配置項(xiàng)是否有改變重新生成mock.json
//?scripts/webpack-init.js?在wenpack配置文件中初始化
const?path?=?require('path')
const?fs?=?require('fs')
const?{syncWalkDir}?=?require('./util')
let?confGlobal?=?{}
let?mockJsonData?=?{}
exports.getConf?=?()?=>?confGlobal
exports.getMockJson?=()?=>?mockJsonData
/**
?*?初始化項(xiàng)目的配置?動(dòng)態(tài)生成mock.json和config/conf.json
?*?@param?{string}?env??dev|build
?*/
?exports.init?=?(env?=?process.env.BUILD_ENV???'build'?:?'dev')?=>?{
???
??delete?require.cache[require.resolve('../config')]
??const?config??=?require('../config')
??const?confJson?=?env?===?'build'???config.conf.build?:?config.conf.dev
??confGlobal?=?confJson
??//?1.根據(jù)環(huán)境變量來(lái)生成
??fs.writeFileSync(path.join(__dirname,?'../config/conf.json'),??JSON.stringify(confGlobal,?null,?'\t'))
??buildMock(confJson)
?}
?
?//?生成mock文件數(shù)據(jù)
?const?buildMock?=?(conf)?=>?{
??//?2.動(dòng)態(tài)生成mock數(shù)據(jù)?讀取src文件夾下面所有以?-mock.json結(jié)尾的文件?存儲(chǔ)到io/index.json文件當(dāng)中
??let?mockJson?=?{}
??const?mockFiles?=?syncWalkDir(path.join(__dirname,?'../src'),?(file)?=>?/-mock.json$/.test(file))
??console.log('build?mocks:?->>>>>>>>>>>>>>>>>>>>>>>')
??mockFiles.forEach((filePath)?=>?{
????const?p?=?path.parse(filePath)
????const?mockKey?=?p.name.substr(0,?p.name.length?-?5)
????console.log(mockKey,?filePath)
????if?(mockJson[mockKey])?{
??????console.error(`有相同的mock文件名稱${p.name}?存在`,?filePath)
????}
????delete?require.cache[require.resolve(filePath)]
????mockJson[mockKey]?=?require(filePath)
??})
??//?如果是打包環(huán)境,?最小化mock資源數(shù)據(jù)
??const?mockMap?=?conf.mock?||?{}
??const?buildMockJson?=?{}
??Object.keys(mockMap).forEach((key)?=>?{
????const?[name,?method]?=?key.split('.')
????if?(mockJson[name][method]?&&?mockJson[name][method][mockMap[key]])?{
??????if?(!buildMockJson[name])?buildMockJson[name]?=?{}
??????if?(!buildMockJson[name][method])?buildMockJson[name][method]?=?{}
??????buildMockJson[name][method][mockMap[key]]?=?mockJson[name][method][mockMap[key]]
????}
??})
??mockJsonData?=?buildMockJson
??fs.writeFileSync(path.join(__dirname,?'../mock.json'),?JSON.stringify(buildMockJson,?null,?'\t'))
?}
?
?//?監(jiān)聽(tīng)配置文件目錄下的config.js和config_default.js
const?confPath?=?path.join(__dirname,?'../config')
if?((env?=?process.env.BUILD_ENV???'build'?:?'dev')?===?'dev')?{
??fs.watch(confPath,?async?(event,?filename)?=>?{
????if?(filename?===?'config.js'?||?filename?===?'config_default.js')?{
??????delete?require.cache[path.join(confPath,?filename)]
??????delete?require.cache[require.resolve('../config')]
??????const?config??=?require('../config')
??????//?console.log('config',?JSON.stringify(config))
??????const?env?=?process.env.BUILD_ENV???'build'?:?'dev'
??????const?confJson?=?env?===?'build'???config.conf.build?:?config.conf.dev
??????if?(JSON.stringify(confJson)?!==?JSON.stringify(confGlobal))?{
????????this.init()
??????}
????}
??});
}
接口代理處理
onProxyReq和onProxyRes
實(shí)現(xiàn)上面思路里面說(shuō)的onProxyReq和onProxyRes 響應(yīng)處理
util.js:https://github.com/rebareba/react-starter/blob/main/scripts/util.js
//?scripts/api-proxy-cache?
const?fs?=?require('fs')
const?path?=?require('path')
const?moment?=?require('moment')
const?{getConf,?getMockJson}?=?require('./webpack-init')
const?API_CACHE_DIR?=?path.join(__dirname,?'../api-cache')
const?{jsonParse,?getBody}?=?require('./util')
fs.mkdirSync(API_CACHE_DIR,{recursive:?true})
module.exports?=?{
??//?代理前處理
??onProxyReq:?async?(_,?req,?res)?=>?{
????req.reqBody?=?await?getBody(req)
????const?{'mock-method':?mockMethod,?'mock-key':?mockKey}?=?req.headers
????//?eslint-disable-next-line?no-console
????console.log(`Ajax?請(qǐng)求:?${mockKey}.${mockMethod}`,req.method,?req.url)
????//?eslint-disable-next-line?no-console
????req.reqBody?&&?console.log(JSON.stringify(req.reqBody,?null,?'\t'))
????if?(mockKey?&&?mockMethod)?{
??????req.mockKey?=?mockKey
??????req.mockMethod?=?mockMethod
??????const?conf?=?getConf()
??????const?mockJson?=?getMockJson()
??????if?(conf.mock?&&?conf.mock[`${mockKey}.${mockMethod}`]?&&?mockJson[mockKey]?&&?mockJson[mockKey][mockMethod])?{
????????//?eslint-disable-next-line?no-console
????????console.log(`use?mock?data?${mockKey}.${mockMethod}:`,?conf.mock[`${mockKey}.${mockMethod}`],?'color:?green')
????????res.mock?=?true
????????res.append('isMock','yes')
????????res.send(mockJson[mockKey][mockMethod][conf.mock[`${mockKey}.${mockMethod}`]])
??????}
?????
????}
??},
??//?響應(yīng)緩存接口
??onProxyRes:?async?(res,?req)?=>?{
????const?{method,?url,?query,?path:?reqPath,?mockKey,?mockMethod}?=?req
????
????if?(mockKey?&&?mockMethod?&&?res.statusCode?===?200)?{
??????
??????let?resBody?=?await?getBody(res)
??????resBody?=?jsonParse(resBody)
??????const?filePath?=?path.join(API_CACHE_DIR,?`${mockKey}.${mockMethod}.json`)
??????let??data?=?{}
??????if?(fs.existsSync(filePath))?{
????????data?=?jsonParse(fs.readFileSync(filePath).toString())
??????}
??????const?cacheObj?=?{
????????date:?moment().format('YYYY-MM-DD?hh:mm:ss'),
????????method,
????????path:?reqPath,
????????url,
????????resHeader:?res.headers,
????????reqHeader:?req.headers,
????????query,
????????reqBody:?await?jsonParse(req.reqBody),
????????resBody:?resBody
??????}
??????if?(resBody.success?===?false)?{
????????data.failed?=?cacheObj
??????}?else?{
????????data.success?=?cacheObj
??????}
??????//?eslint-disable-next-line?no-console
??????fs.writeFile(filePath,?JSON.stringify(data,'',?'\t'),?(err)?=>?{?err?&&?console.log('writeFile',?err)})
????}
??},
??//?后端服務(wù)沒(méi)啟的異常處理
??onError(err,?req,?res)?{
????setTimeout(()?=>?{
?????if?(!res.mock)?{
???????res.writeHead(500,?{
?????????'Content-Type':?'text/plain',
???????});
???????res.end('Something?went?wrong.?And?we?are?reporting?a?custom?error?message.');
?????}
???},?10)
??}
}
webpack配置
在webpack配置中引入使用
const?config?=?require('.')
//?config/webpack.config.js
const?{init}?=?require('../scripts/webpack-init');
init();
//?接口請(qǐng)求本地緩存
const?apiProxyCache?=?require('../scripts/api-proxy-cache')
for(let?key?in?config.proxy)?{
??config.proxy[key]?=?Object.assign(config.proxy[key],?apiProxyCache);
}
const?webpackConf?=?{
??devServer:?{
????contentBase:?path.join(__dirname,?'..'),?//?本地服務(wù)器所加載的頁(yè)面所在的目錄
????inline:?true,
????port:?config.port,
????publicPath:?'/',
????historyApiFallback:?{
??????disableDotRule:?true,
??????//?指明哪些路徑映射到哪個(gè)html
??????rewrites:?config.rewrites,
????},
????host:?'127.0.0.1',
????hot:?true,
????proxy:?config.proxy,
??},
}
總結(jié)
mock做好其實(shí)在我們前端實(shí)際中還是很有必要的,做過(guò)的項(xiàng)目如果后端被鏟除了想要回憶就可以使用mock讓項(xiàng)目跑起來(lái),可以尋找一些實(shí)現(xiàn)的效果來(lái)進(jìn)行代碼復(fù)用。當(dāng)前介紹的mock流程實(shí)現(xiàn)有很多定制的開(kāi)發(fā),但是真正完成后,團(tuán)隊(duì)中的成員只是使用還是比較簡(jiǎn)單配置即可。
關(guān)于前端項(xiàng)目部署我也分享了一個(gè)BFF 層,當(dāng)前做的還不是很完善,也分享給大家參考
Render-Server:https://github.com/rebareba/render-server 主要功能包含:
一鍵部署 npm run deploy 支持集群部署配置 是一個(gè)文件服務(wù) 是一個(gè)靜態(tài)資源服務(wù) 在線可視化部署前端項(xiàng)目 配置熱更新 在線Postman及接口文檔 支持前端路由渲染, 支持模板 接口代理及路徑替換 Web安全支持 Ajax請(qǐng)求驗(yàn)證,Referer 校驗(yàn) 支持插件開(kāi)發(fā)和在線配置 可實(shí)現(xiàn):前端模板參數(shù)注入、請(qǐng)求頭注入、IP白名單、接口mock、會(huì)話、第三方登陸等等
專注分享當(dāng)下最實(shí)用的前端技術(shù)。關(guān)注前端達(dá)人,與達(dá)人一起學(xué)習(xí)進(jìn)步!
長(zhǎng)按關(guān)注"前端達(dá)人"

