<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 22114字,需瀏覽 45分鐘

           ·

          2021-01-13 19:52

          轉(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要有哪些功能:

          1. mock功能和前端代碼解耦
          2. 一個(gè)接口支持多種mock情況
          3. 無(wú)需依賴另外的后端服務(wù)和第三方庫(kù)
          4. 能在network看到mock接口的請(qǐng)求且能區(qū)分
          5. mock數(shù)據(jù)、接口配置和頁(yè)面在同一個(gè)目錄下
          6. mock配置改變無(wú)需重啟前端dev
          7. 生產(chǎn)打包可以把mock數(shù)據(jù)注入到打包的js中走前端mock
          8. 對(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)容, 如login
          • mock-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á)人"


          瀏覽 47
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产高清在线激情 | 在线免费看黄片 | 国产一级电影在线看 | 欧美激情国产91在线 | 久久久久久亚洲成人电影 |