前端axios+eggjs(后端)进行网络请求

过年期间(实际过年前几个月就开始了),自学了一点前端React + Material UI,后端eggjs的相关东西,捣鼓了一个简单功能的zxj-demo(内容很简陋,主要尝试下能不能跑通),期间遇见了不少坑,所以最后总结记录一下。

一、axios安装

axios的安装比较简单,因为是用在前端,如果使用React或Vue等,并使用nodejs进行构建,那直接可以在项目中执行以下命令安装:

npm i axios --save

在使用时,直接使用下面代码即可引入:

import axios from 'axios';

如果是在html标签中引入,则可采用如下形式:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

二、eggjs配置

eggjs作为服务端来接受axios的get、post请求(其他方法项目没用过,不予讨论,下同),需要进行一定的设置:

1、跨域问题

React + eggjs的项目我尝试的是前后端分离项目,不是eggjs本身提供前端就基本要面对跨域问题(即使前后端项目在同一个主机上,如果端口不一样,也会被认为是跨域),主要涉及2个问题:CSRF和CORS。

CSRF(Cross-site request forgery跨站请求伪造),也被称为 One Click Attack 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。 CSRF 攻击会对网站发起恶意伪造的请求,严重影响网站的安全。eggjs框架内置了 CSRF 防范方案。默认CSRF是开启的,如果要使用CSRF可以参考eggjs官方文档。在构建简单的前后端分离项目中,可以考虑关闭CSRF,虽然不推荐(我偷懒还是选择了关闭)。

关闭CSRF,需要修改eggjs配置文件,默认配置文件为:config\config.default.js,在配置中(使用脚手架生成的文件默认是

module.exports = appInfo => {   const config = exports = {}; config.xxxx ={} }形式,下同

)添加如下内容即可关闭(红色),其实也可以开启,使用ignore来对定来源进行忽略。如果开启,更高级的使用方法就要看官方文档了。

config.security = {
    csrf: {

      // ignore: ctx => ctx.ip === '127.0.0.1',
      enable: false,
    },

    domainWhiteList: [ '*' ],
  };

CORS : Cross Origin Resourse-Sharing 跨站资源共享。前端向后端请求资源时就需要进行相关设置才可以进行。

eggjs没有内置CORS插件,需要进行安装:

// 安装
npm i egg-cors --save
// config\plugin.js 配置
module.exports = {
  
  sequelize: {
    enable: true,
    package: 'egg-sequelize',
  },
  cors: {
    enable: true,
    package: 'egg-cors',
  },
 
  jwt: {
    enable: true,
    package: 'egg-jwt',
  },
};


// config\config.default.js 配置
config.cors = {
    origin: '*',

    // credentials: true,
    allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
  };

其中的origin对应Response Headers中的Access-Control-Allow-Origin,该字段是必须的。表示许可范围的域名,通常有两种值:请求Request Headers的 Origin 字段的值或者 *(星号)表示任意域名。如果前端请求的Origin不在许可范围内,则服务器会报错,请求不会得到有效回应。

credentials对应Response Headers中的Access-Control-Allow-Credentials,该字段可选,为布尔值,表示是否允许在 CORS 请求之中发送 Cookie 。若不携带 Cookie 则不需要设置该字段。当设置为 true 则 Cookie 包含在请求中,一起发送给服务器。还需要在 前端请求中开启 withCredentials 属性,否则浏览器也不会发送 Cookie

注意: 如果前端设置 Access-Control-Allow-Credentials 为 true 来携带 Cookie 发起请求,则服务端 Access-Control-Allow-Origin 也就是上面中config.cors中的origin不能设置为 *,必须为类似前端域名:端口这种形式

通过以上设置,在eggjs服务端基本就可以满足跨域请求问题。

2、eggjs文件接受相关设置

要使eggjs接受文件,需要用到multipart模块,该模块是eggjs内置的,有常规的file和stream数据流模式,这里因为没接触过stream模式选择了常规的file模式,因为stream模式看起来比较复杂。配置如下:

// config\config.default.js 中配置
config.multipart = {
    mode: 'file',
    fileExtensions: [ '.doc', '.pdf' ],
  //在eggjs内置的文件类型白名单外扩展其他文件格式。
    // whitelist: [ '.png' ],  注意此选项会覆盖eggjs内置白名单,且会使fileExtensions配置无效。
  };

eggjs内置文件类型白名单如下:

// images
".jpg", ".jpeg", // image/jpeg
".png", // image/png, image/x-png
".gif", // image/gif
".bmp", // image/bmp
".wbmp", // image/vnd.wap.wbmp
".webp",
".tif",
".psd",
// text
".svg",
".js", ".jsx",
".json",
".css", ".less",
".html", ".htm",
".xml",
// tar
".zip",
".gz", ".tgz", ".gzip",
// video
".mp3",
".mp4",
".avi",

三、axios实例化封装

如果不进行实例化封装,每次请求需要导入axios并输入所有参数,即使有些是重复的。比如如果请求的后端都是由同一个eggjs实例提供,完全可以通过baseURL来减少每次请求的URL长度。如果用到Token进行鉴权认证,完全可以在封装示例的请求拦截器中自动获取Token并添加到请求头中,这样就不需要每次请求单独进行设置。

import axios from 'axios'
import {baseURL} from './url'

// const baseurl = 'http://127.0.0.1:7001/';
   //比如baseURL是这个

const instance = axios.create({
  timeout: 10000,  // 超时时间设置(这里是设置了10s,可以自行设置
)
  baseURL,
    //使用了baseURL后,请求时不需要提供比如http://127.0.0.1:7001/api/api1这样完整地址,只需要/api/api1即可
  // withCredentials: true,
    //注意:这里如果有此配置,且为true,则上面eggjs的cors的orign不能设为'*',否则请求会报错
});
instance.defaults.headers.post['Content-Type'] = 'application/json';
  //这里设置了默认Content-Type请求头,不过axios会根据请求数据形式自动切换Request Headers中的Content-Type请求头。


// 添加请求拦截器
instance.interceptors.request.use( config => {
  config.headers['authorization'] = "Bearer " + localStorage.getItem('token') || '';  
//这里是自动在请求Headers中添加token字段(假设token存在了localStorage中)  
  return config;  
}, error => {
  return Promise.reject(error);
});

// 添加相应拦截器
// instance.interceptors.response.use( respose => {

// })

// 封装了一个post方法,get方法类似。
export const post = (url, data, config={}) => {
  return new Promise( ( resolve, reject ) => {
    instance({
      method:'post',
      url,
      data,
      ...config
    }).then( res => {
      resolve(res);
   //如果只需要处理返回请求数据可以是resove(res.data),不过测试时还是不要。
    }).catch( error => {
      reject(error);
    })
  });  

四、前端请求

1、普通表单请求

一般情况下,既然封装了方法,则直接使用封装的方法即可,很简单,只需要提供url,data即可(data其实可以设置为null,或者在示例中设置默认为null,这样data也可以像config一样不提供)。data为普通表单数据或一个对象(类似{ a:xxx,b:xxx}之类):

//  我demo里的一个示例
const askURL = '/api/ask'; 
const data = xxxxx;
post(
 askURL,data
        
      ).then( response => {
        
        // console.log(response.data);
        if (response.data.code === 200) {
          // console.log('res:',response);
          alert(response.data.msg ? response.data.msg : '提交成功!');
             
        } else {
          
          alert(response.data.msg ? response.data.msg : '提交失败!') ;
          if (response.data.code === 444) {
            
            history.push('/login');
          }
        } 
      }).catch( error => {
        alert('提交失败!')
        console.log(error);
        
      });

2、含文件的表单请求

含文件的表单数据就不能直接使用普通对象数据类型进行请求了,这里需要用到DataForm方法对数据进行处理。下面是一个我在学习过程中尝试构建的demo中的示例:

let data = new FormData();
      // console.log('f b s:',fileObjects)
      // 必须传入是单个文件,不能是多个文件构成的数组,多个文件要分别传入

      if ( fileObjects.length ) {
        fileObjects.forEach(f => {
   
          // 注意:传入的f必须是一个直接的File对象
          data.append('files',f);
        }); 
      }           
      data.append('scopeid',scopeid);     
      data.append('askdetail',askdetail); 
      data.append('askphone',askphone);  

通过上面append方法,接受2个参数,第一个参数是字段field,类似表单中的name属性,eggjs后台可以在ctx.request.body中获取非文件类型的数据。添加完所有数据后可以像普通请求一样发送,此时axio会自动识别数据类型,并将Request Headers的Content-Type设置为multipart/form-data,不需要人工干预。

注意:

1、文件类型使用append传入的必须是单个文件,如果是多个文件必须逐个传入,而且传入同一个filed即可。同一个field传入多个值append会自动创建一个数组。文件类型的field字段如果eggjs使用file模式接受文件,则并不重要,因为用不到,可以随便设置。

2、文件类型使用append传入的必须是直接File类型对象,它包含了name、type、lastModified等属性。我在学习过程中遇到了一个坑,我使用了'material-ui-dropzone'包中的DropzoneDialogBase组件来进行文件上传,file对象是该组件获得并存入fileObjects数组的,像上面直接请求后端总是获得不到文件内容。后来console.log后我发现该组件保存的文件对象是{ data:xxxx,file:xxxx},其中data为一些加密的验证内容,file属性才是真正的File类型对象,所以使用DropzoneDialogBase或其他material-ui-dropzone包里的组件时特别要注意,可以通过console.log输出组件保存的文件对象的内容,确认是否是一个直接的File文件对象,还是嵌套了其他内容。

3、eggjs服务端接受文件及其他请求

直接使用我实际学习中构建的一个eggjs的controller示例(post方法),包含了获取普通数据,文件数据(只是示例方法,删除了很多上下文内容):

const Controller = require('egg').Controller;
const fs = require('mz/fs');   
const path = require('path');
const filedir = 'xxx';  // 文件保存路径

class MyController extends Controller {
  async index() {
    const ctx = this.ctx;    
    const { scopeid, askdetail, askphone } = ctx.request.body;
  //获取普通请求数据      
    const hasfile = ctx.request.files.length !== 0;
   //判断是否有文件传入,如果有文件传入则可通过ctx.request.files获取。
    if (hasfile) {
      let f;
      for (let index = 0; index < ctx.request.files.length; index++) {
        const file = ctx.request.files[index];
        const tmpfilename = (index + 1) + '-' + file.filename;
   // 自定义文件名
        filelist.push(tmpfilename);
        const tmpdir = path.join('./', filedir);
        if (!fs.existsSync(tmpdir)) {
     //检查保存目录是否存在
          fs.mkdirSync(tmpdir);
           //如果不存在就创建目录(否则直接保存会报错)
        }
        f = fs.readFileSync(file.filepath);   //逐个读文件(同步)
        fs.writeFileSync(path.join(tmpdir, tmpfilename), f);  //保存文件(同步)
        }
      }
      ctx.cleanupRequestFiles();  //  清除上传文件的缓存
      // fs.unlinkSync(todelfilepath)  //如果要删除保存的文件(同步),使用该方法
    }    
//下略
}

普通post请求数据可以直接在ctx.request.body中得到,文件数据可以在ctx.request.files中得到。这里使用了“mz/fs”包进行文件保存、文件夹创建等,注意mz/fs里的方法默认是异步的,这里都采用了同步方法,异步方法要写回调函数比较麻烦,实际使用时最好使用try{}catch{}包裹,以防报错导致服务挂起。

--------------------------------

除非注明,否则均为清风揽月阁原创文章,转载应以链接形式标明本文链接

本文链接:https://www.iimm.ink/280.html

发表评论

滚动至顶部