过年期间(实际过年前几个月就开始了),自学了一点前端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{}包裹,以防报错导致服务挂起。
--------------------------------除非注明,否则均为清风揽月阁原创文章,转载应以链接形式标明本文链接