写在后面,本文浏览须要肯定Nodejs的相干常识,因为会扩大webpack的相干性能,并且实现须要恪守肯定约定和Ajax封装。积淀的脚手架也放到Github上供应同学参考React-Starter, 使用手册还没写欠缺, 整体思路和React还是Vue无关,如果对大家有播种记得Star下。
它有这些性能:

  • 开发打包有不同配置
  • eslint 验证
  • 代码格调对立
  • commit 标准验证
  • 接口mock
  • 热更新
  • 异步组件

Mock性能介绍

市面上讲前端mock怎么做的文章很多,整体上浏览下来的没有一个真正站在前端角度上让我感觉弱小和易用的。上面就说下我冀望的前端mock要有哪些性能:

  1. mock性能和前端代码解耦
  2. 一个接口反对多种mock状况
  3. 无需依赖另外的后端服务和第三方库
  4. 能在network看到mock接口的申请且能辨别
  5. mock数据、接口配置和页面在同一个目录下
  6. mock配置扭转无需重启前端dev
  7. 生产打包能够把mock数据注入到打包的js中走前端mock
  8. 对于后端已有的接口也能疾速把Response数据转化为mock数据

下面的这些性能我讲其中几点的作用:

对于第7点的作用是后续我的项目开发实现,在齐全没有开发后端服务的状况下,也能够进行演示。这对于一些ToB定制的我的项目来积淀我的项目地图(案例)很有作用。
对于第8点在开发环境后端服务常常不稳固下,不依赖后端也能做页面开发,外围是能实现一键生成mock数据。

配置解耦

耦合状况

什么是前端配置解耦,首先让咱们看下平时配置耦合状况有哪些:

  • webpack-dev后端测试环境变了须要改git跟踪的代码
  • dev和build的时候 须要改git跟踪的代码
  • 开发的时候想这个接口mock 须要改git跟踪的代码 mockUrl ,mock?

如何解决

前端依赖的配置解耦的思路是配置文件conf.json是在dev或build的时候动静生成的,而后该文件在前端我的项目援用:

├── 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.jsconst _ = 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;

默认应用config_default.js 的内容,如果有config.js 则笼罩,开发的时候复制config_default.js 为config.js 后续相干配置能够批改config.js即可。

// config/config_default.jsconst 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, // 反对跨域申请      secure: true, // 反对 https    },  },  ...  conf: {    dev: {      title: "前端模板",      pathPrefix: "/react-starter", // 对立前端门路前缀      apiPrefix: "/api/react-starter", //      debug: true,      delay: 500,    // mock数据模仿提早      mock: {        // "global.login": "success",        // "global.loginInfo": "success",      }    },    build: {      title: "前端模板",      pathPrefix: "/react-starter",      apiPrefix: "/api/react-starter",      debug: false,      mock: {}    }  }};

在开发或打包的时候依据环境变量应用conf.dev或conf.build 生成conf.json文件内容

// package.json{  "name": "react-starter",  "version": "1.0.0",  "description": "react前端开发脚手架",  "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.jsconst config = require('.')const env = process.env.BUILD_ENV ? 'build' : 'dev'const confJson = env === 'build' ? config.conf.build : config.conf.devfs.writeFileSync(path.join(__dirname, './conf.json'),  JSON.stringify(confGlobal, null, '\t'))

援用配置

src/common/utils.jsx文件中暴露出配置项,配置也能够通过window.conf来笼罩

// src/common/utils.jsximport conf from '@/config/conf.json'export const config = Object.assign(conf, window.conf)

而后就能够在各个页面中应用

import {config} from '@src/common/utils'class App extends Component {  render() {    return (      <Router history={history}>        <Switch>          <Route path={`${config.pathPrefix}`} component={Home} />          <Redirect from="/" to={`${config.pathPrefix}`} />        </Switch>      </Router>    )  }}ReactDOM.render(     <App />,  document.getElementById('root'),)

Mock实现

成果

为了实现咱们想要的mock的相干性能,首先是否开启mock的配置解耦能够通过下面说的形式来实现,咱们个别在页面异步申请的时候都会目录定义一个io.js的文件, 外面定义了以后页面须要调用的相干后端接口:

// src/pages/login/login-io.jsimport {createIo} from '@src/io'const apis = {  // 登录  login: {    method: 'POST',    url: '/dtwave-boot/sys/login',  },  // 登出  logout: {    method: 'POST',    url: '/dtwave-boot/sys/logout',  },}export default createIo(apis, 'login') // 对应login-mock.json

下面定义了登录和登出接口,咱们心愿对应开启的mock申请能应用当前目录下的login-mock.json文件的内容

// src/pages/login/login-mock.json{    "login": {        "failed": {            "success": false,            "code": "ERROR_PASS_ERROR",            "content": null,            "message": "账号或明码谬误!"        },        "success": {            "success": true,            "code": 0,            "content": {                "name": "admin",                "nickname": "超级管理员",                "permission": 15            },            "message": ""        }    },    "logout": {        "success": {            "success": true,            "code": 0,            "content": null,            "message": ""        }    }}

在调用logout登出这个Ajax申请的时候且咱们的conf.json中配置的是"login.logout": "success" 就返回login-mock.json中的login.success 的内容,配置没有匹配到就申请转发到后端服务。

// config/conf.json{    "title": "前端后盾模板",    "pathPrefix": "/react-starter",    "apiPrefix": "/api/react-starter",    "debug": true,    "delay": 500,    "mock": {        "login.logout": "success"    }}

这是咱们最终要实现的成果,这里有一个约定:我的项目目录下所有以-mock.jsom文件结尾的文件为mock文件,且文件名不能反复

思路

在webpack配置项中devServer的proxy配置接口的转发设置,接口转发应用了功能强大的 http-proxy-middleware 软件包, 咱们约定proxy的配置格局是:

  proxy: {    "/api/react-starter/*": {      target: `http://192.168.90.68:8888`,      changeOrigin: true,      secure: true,      // onError: (),      // onProxyRes,      // onProxyReq      },  },

它有几个事件触发的配置:

  • option.onError 呈现谬误
  • option.onProxyRes 后端响应后
  • option.onProxyReq 申请转发前
  • option.onProxyReqWs
  • option.onOpen
  • option.onClose

所以咱们须要定制这几个事件的解决,次要是申请转发前和申请解决后

onProxyReq

想在这里来实现mock的解决, 如果匹配到了mock数据咱们就间接响应,就不转发申请到后端。 怎么做呢: 思路是依赖申请头,dev状况下前端在调用的时候是否注入约定好的申请头 通知我须要寻找哪个mock数据项, 咱们约定Header:

  • mock-key 来匹配mock文件如login-mock.json的内容, 如login
  • mock-method 来匹配对应文件内容的办法项 如logout

而后conf.json中mock配置寻找到具体的响应我的项目如:"login.logout": "success/failed"的内容

onProxyRes

如果调用了实在的后端申请,就把申请的响应数据缓存下来,缓存到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": ""        }    }}

这样做的目标是为了后续实现一键生成mock文件。

前端接口封装

应用

下面咱们看到定义了接口的io配置:

// src/pages/login/login-io.jsimport {createIo} from '@src/io'const apis = {  // 登录  login: {    method: 'POST',    url: '/dtwave-boot/sys/login',  },  // 登出  logout: {    method: 'POST',    url: '/dtwave-boot/sys/logout',  },}export default createIo(apis, 'login') // login注册到header的mock-key

咱们在store中应用

// src/pages/login/login-store.jsimport {observable, action, runInAction} from 'mobx'import io from './login-io'// import {config, log} from './utils'export class LoginStore {  // 用户信息  @observable userInfo  // 登陆操作  @action.bound  async login(params) {    const {success, content} = await io.login(params)    if (!success) return    runInAction(() => {      this.userInfo = content    })  }}export default LoginStore

通过 createIo(apis, 'login') 的封装在调用的时候就能够非常简单的来传递申请参数,简略模式下会判断参数是到body还是到query中。 简单的也能够反对比方能够header,query, body等这里不演示了。

createIo 申请封装

这个是前端接口封装的要害中央,也是mock申请头注入的中央

// src/io/index.jsximport {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// 创立一个requestexport const request = creatRequest({  // 自定义的申请头  headers: {'Content-Type': 'application/json'},  // 配置默认返回数据处理  action: (data) => {    // 对立解决未登录的弹框    if (data.success === false && data.code === ERROR_CODE.UN_LOGIN && !reloginFlag) {      reloginFlag = true      // TODO 这里可能对立跳转到 也能够是弹窗点击跳转      Modal.confirm({        title: '从新登录',        content: '',        onOk: () => {          // location.reload()          history.push(`${config.pathPrefix}/login?redirect=${window.location.pathname}${window.location.search}`)          reloginFlag = false        },      })    }  },  // 是否谬误显示message  showError: true,  message,  // 是否以抛出异样的形式 默认false {success: boolean判断}  throwError: false,  // mock 数据申请的等待时间  delay: config.delay,  // 日志打印  log,})// 标识是否是简略传参数, 值为true标识简单封装export const rejectToData = Symbol('flag')/** * 创立申请IO的封装 * @param ioContent {any { url: string method?: string mock?: any apiPrefix?: string}}  } * @param name mock数据的对应文件去除-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 = {}) => {      // 这里判断简略申请封装 rejectToData=true 示意简单封装      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]      ) { // 判断是否是生产打包 mock注入到代码中        ioContent[key].mock = JSON.parse(JSON.stringify(mockData[name][key][config.mock[`${name}.${key}`]]))      } else if (name && config.debug === true) { //注入 mock申请头        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}

这里对request也做进一步的封装,配置项设置了一些默认的解决设置。比方通用的申请响应失败的是否有一个message, 未登录的状况是否有一个弹窗提醒点击跳转登陆页。如果你想定义多个通用解决能够再创立一个request2和createIo2。

request封装axios

是基于axios的二次封装, 并不是十分通用,次要是在约定的申请失败和胜利的解决有定制,如果须要能够本人批改应用。

import axios from 'axios'// 配置接口参数// declare interface Options {//   url: string//   baseURL?: string//   // 默认GET//   method?: Method//   // 标识是否注入到data参数//   rejectToData?: boolean//   // 是否间接弹出message 默认是//   showError?: boolean//   // 指定 回调操作 默认登录解决//   action?(data: any): any//   headers?: {//     [index: string]: string//   }//   timeout?: number//   // 指定路由参数//   params?: {//     [index: string]: string//   }//   // 指定url参数//   query?: any//   // 指定body 参数//   body?: any//   // 混合解决 Get到url, delete post 到body, 也替换路由参数 在createIo封装//   data?: any//   mock?: any// }// ajax 申请的对立封装// TODO 1. 对jsonp申请的封装 2. 反复申请/** * 返回ajax 申请的对立封装 * @param Object option 申请配置 * @param {boolean} opts.showError 是否谬误调用message的error办法 * @param {object} opts.message  蕴含 .error办法 showError true的时候调用 * @param {boolean} opts.throwError 是否出错抛出异样 * @param {function} opts.action  蕴含 自定义默认解决 比方未登录的解决 * @param {object} opts.headers  申请头默认content-type: application/json * @param {number} opts.timeout  超时 默认60秒 * @param {number} opts.delay   mock申请提早 * @returns {function} {params, url, headers, query, data, mock} data混合解决 Get到url, delete post 到body, 也替换路由参数 在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},    }    // 简略申请解决    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)      }    }    // 路由参数解决    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 参数解决    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 = `浏览器申请非正常返回: 状态码 ${retData.status}`        }      }    }    // 主动处理错误音讯    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

依据api-cache下的接口缓存和定义的xxx-mock.json文件来生成。

# "build-mock": "node ./scripts/build-mock.js"# 所有:npm run build-mock mockAll # 单个mock文件:npm run build-mock login# 单个mock接口:npm run build-mock login.logout# 简单 npm run build-mock login.logout user

具体代码参考build-mock.js

mock.json文件生成

为了在build打包的时候把mock数据注入到前端代码中去,使得mock.json文件内容尽可能的小,会依据conf.json的配置项来动静生成mock.json的内容,如果build外面没有开启mock项,内容就会是一个空json数据。 当然后端接口代理解决内存中也映射了一份该mock.json的内容。这里须要做几个事件:

  • 依据配置动静生成mock.json的内容
  • 监听config文件夹 判断对于mock的配置项是否有扭转从新生成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 = () => confGlobalexports.getMockJson =() => mockJsonData/** * 初始化我的项目的配置 动静生成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.依据环境变量来生成  fs.writeFileSync(path.join(__dirname, '../config/conf.json'),  JSON.stringify(confGlobal, null, '\t'))  buildMock(confJson) }  // 生成mock文件数据 const buildMock = (conf) => {  // 2.动静生成mock数据 读取src文件夹上面所有以 -mock.json结尾的文件 存储到io/index.json文件当中  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)  })  // 如果是打包环境, 最小化mock资源数据  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')) }  // 监听配置文件目录下的config.js和config_default.jsconst 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

实现下面思路外面说的onProxyReq和onProxyRes 响应解决

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 申请: ${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}`]])      }         }  },  // 响应缓存接口  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)})    }  },  // 后端服务没启的异样解决  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.jsconst {init} = require('../scripts/webpack-init');init();// 接口申请本地缓存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, '..'), // 本地服务器所加载的页面所在的目录    inline: true,    port: config.port,    publicPath: '/',    historyApiFallback: {      disableDotRule: true,      // 指明哪些门路映射到哪个html      rewrites: config.rewrites,    },    host: '127.0.0.1',    hot: true,    proxy: config.proxy,  },}

总结

mock做好其实在咱们前端理论中还是很有必要的,做过的我的项目如果后端被根除了想要回顾就能够应用mock让我的项目跑起来,能够寻找一些实现的成果来进行代码复用。以后介绍的mock流程实现有很多定制的开发,然而真正实现后,团队中的成员只是应用还是比较简单配置即可。

对于前端我的项目部署我也分享了一个BFF 层,以后做的还不是很欠缺,也分享给大家参考

Render-Server 次要性能蕴含:

  • 一键部署 npm run deploy
  • 反对集群部署配置
  • 是一个文件服务
  • 是一个动态资源服务
  • 在线可视化部署前端我的项目
  • 配置热更新
  • 在线Postman及接口文档
  • 反对前端路由渲染, 反对模板
  • 接口代理及门路替换
  • Web平安反对 Ajax申请验证,Referer 校验
  • 反对插件开发和在线配置 可实现: 前端模板参数注入、申请头注入、IP白名单、接口mock、会话、第三方登陆等等