0、跨域三大解决办法:
1.前端利用proxy代理
2.后端egg的话使用egg-cros处理
3.jsonp提供接口,使用script标签发起请求
script,img,a不受跨域限制
1、前端配置
要做一个前后端分离的项目的话,首先要有两个服务器,我这里就直接用现成的vite和egg
vite作为前端服务器 egg作为后端服务器
大体项目结构就是(比如做淘宝项目:只是举例哈)
TAOBAO
//这个就放前端
TAOBAO_client
//这个就放后端
TAOBAO_server
一、初始化
//首先管理员权限打开cmd,进入项目文件夹中的TAOBAO_client文件夹 执行下面的命令(前端服务器的启动以及下载依赖包啥的都是进入这个文件夹)
npm init -y (-y全部默认) 创建 package.json文件
二、安装
npm i vite
三、接着创建下面的目录结构:
TAOBAO_client
node_modules
public
//如果用的是scss要下载相应的预处理插件 npm add sass
scss
images
src
api
//定义接口
api.ts
//定义接口接受数据的类型
type.ts
//axios全局配置
axios.config.ts
//自定义接口配置
http.ts
js
//用js还是ts看自己,不用js可以不建
ts
//html文件是自己创建的
index.html
login.html
//这两个是初始化就有的,不用自己创建
package-lock.json
package.json
//这个要自己创建
vite.config.js
四、在根目录创建vite.config.js文件夹内容为
//直接复制粘贴就可
import { resolve } from 'path'
import { defineConfig } from 'vite'
export default defineConfig({
server: {
// host: '192.168.2.53',
// port: 8080,
//跨域处理
proxy: {
'/api': {
target: 'http://127.0.0.1:7001',//后端服务器
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
}
},
base: './',//打包后文件引入路径配置
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
//如果要打包多个页面,下一行接触注释,修改html名字即可
// nested: resolve(__dirname, 'login.html'),
},
},
},
})
五、启动vite服务器
在启动之前还要配置一下 package.json文件:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
//配置启动指令
"dev": "vite",
//配置打包指令
"build": "vite build"
},
//然后执行下面的命令
npm run dev
六、打包构建生产版本
//(这个是最后项目全部完成的时候用的,先不管)
npm run build(需要在package.json文件中"scripts"里面设置 "build": "vite build", )
七、使用框架注意事项
//1.使用axios发起网络请求的话 要先下载相应依赖包 使用的时候直接导入 后面有使用示例
npm install axios
//2.使用scss的话 要先下载sass依赖包,下载完成后,直接在相应的html文件引入scss文件即可
npm add sass
八、文件示例
1、html引入scss、ts
<!--引入 阿里巴巴矢量图标库 -->
<link rel="stylesheet" href="https://at.alicdn.com/t/c/xxxxxxxx.css" />
<!--引入 scss文件 -->
<link rel="stylesheet" href="./public/scss/index.scss" />
<!-- 引入ts文件 注意:设置type为module为打包项目时,方便一起打包ts文件-->
<script type="module" src="./src/ts/index.ts"></script>
2、public->src->api文件夹下的各文件作用示例
1.axios.config.ts
//这个文件主要是用来对axios进行一些全局配置
示例:
//先导入axios
import axios from 'axios';//不能用require
// 全局的 axios 配置 详细参见axios的文档:http://www.axios-js.com/zh-cn/docs
axios.defaults.baseURL = '/api';
axios.defaults.headers.post["Content-Type"] =
"application/x-www-form-urlencoded";
export default axios;
2.api.ts文件
//导入axios全局配置文件 这样可以使用全局配置后的axios
import axios from './axios.config';
//导入接口类型文件:定义了哪个接口接受哪些数据类型
import { registerData } from './type';
示例:
// 注册接口
export const registerPost = (data:registerData) => axios({ method: "post", url: "/register", data: data });
// 验证码接口
export const codePost = () =>axios({ method: "post", url: "/code" });
3.type.ts
// 注册接口接受的数据及类型
interface registerData{
tel: number,
password: string,
code: number
}
export { registerData }
4.http.ts
//这个文件主要用来自定义一个axios
示例:
//这个instance也可以当做axios来用,如果后端有多台服务器,可以把一个服务器的网址配置在baseURL 我觉得认识就行 因为我不会哈哈哈 直接用axios不就好啦 你要用的话就看文档吧,前面有文档网址
const instance = axios.create({
baseURL: 'https://api.example.com'
});
ok,前端已经配置完成,接下来配置后端
2.后端配置
一、快速初始化
//这个是用来创建一个后端文件并且进入文件 如果已经创建了项目文件的话就不执行这个
mkdir egg-example && cd egg-example
//这个要cmd管理员权限进入到TAOBAO_server文件夹然后执行(TAOBAO_server这个是我上面举的例子) 中间有一段选项 直接一路回车即可
npm init egg --type=simple
//接着执行这个
npm i
二、配置相关文件
执行完上面的命令后,TAOBAO_server这个文件下会有好多文件,我们只需要理解两个就可以,分别是app文件和config文件
1.app文件夹的route.js
//这个文件时来定义后端路由的 前端请求后端就是根据这里的路由来的
//例如我通过axios请求注册路由
// 发送 POST 请求
/*axios({
method: 'post',
url: '/register',
这里要注意,post请求传输的数据要放在data里面data,get请求传输的数据要放在params里(把data改成params就行)
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});*/
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
//这里一个是home 一个是user有区别的哦(一个是home.js处理前端请求一个是user.js处理前端请求)
//首页路由 这个是get请求
router.get('/', controller.home.index);
//注册路由 这个是post请求
router.post('/register', controller.user.register);
};
2.app文件夹下的controll文件的home.js
这个是写关于home的路由,以及相应的后端操作比如对数据库的操作,对前端传来数据的操作,相应给前端什么数据等等 我们一般在controll文件下新建一个js文件,把home.js的代码复制粘贴到新建的js文件夹里 举个例子
//新建了一个user.js文件 这里面关于用户的登录,注册,增删改查都在这写我这只写了一个regidter接口
//这边的接口数量取决于你route.js里有多少个路径为controller.user.接口名
'use strict';
const { Controller } = require('egg');
//这里记得修改一下类名
class UserController extends Controller {
//这个就是对请求注册路由进行相应操作
async register() {
const { ctx } = this;
//这里clientData是前端传过来的数据,post请求的话clientData=ctx.request.body post请求还要关闭安全验证,后面会教
//get请求的话clientData=ctx.request.query
let clientData=ctx.request.body;
const results = await this.app.mysql.select('user', { // 搜索 post 表
where: { tel: clientData.tel }, // WHERE 条件
});
if (results.length == 0) {
const result = await this.app.mysql.insert('user', { tel: clientData.tel, password: clientData.password });
ctx.body = {
code: 1,
data: '注册成功'
};
} else {
ctx.body = {
code: -1,
data:'已有此账号,注册失败'
};
}
}
}
module.exports = UserController;
3.app文件下的public文件
这个由于我们这是前后端分离,所以这个就不用管
4.config文件下的config.default.js和plugin.js
我就举一两个常用的例子把
项目肯定会用到数据库,这就要一些配置了
数据库配置
1.下载相关依赖
npm i --save egg-mysql
2.开启插件 把这个复制到plugin.js文件夹里面
// config/plugin.js
mysql : {
enable: true,
package: 'egg-mysql',
};
3.粘贴进config.default.js文件夹里 然后稍微改一下
config.mysql = {
// 单数据库信息配置
client: {
// host
host: 'localhost',
// 端口号
port: '3306',
// 用户名
user: 'root',
// 密码
password: 'test_password',
// 数据库名
database: 'test',
},
//下面的这两个不用管
// 是否加载到 app 上,默认开启
app: true,
// 是否加载到 agent 上,默认关闭
agent: false,
};
4.怎么用数据库,可以去egg官方文档->教程->mysql看看
去除post安全验证
//把这个文件复制到config.default.js就好啦,不用改(注意放在return之前)
// 配置安全验证
config.security = {
csrf: {
enable: false,
ignoreJSON: true,
}
}
跨域配置
我们是前后端分离做项目,所以肯定要处理跨域问题,这个可以是后端egg来处理,也可以是前端vite用proxy来处理,主要看个人,要想前端处理的话可以私信我或者自己去vite官方文档看哦,**这边后端处理跨域的话,axios请求的url是'http://域名:端口号/路由",别搞错了姐人们**!!!!!!!
//框架提供了 egg-cors 插件来实现cors跨域请求
1.下载插件
cnpm i --save egg-cors
2.开启插件:把这个复制到plugin.js文件里面
//跨域处理
cors:{
enable: true,
package: 'egg-cors',
}
//配置cors:把这个复制到config.default.js
// 跨域的配置
config.cors = {
origin: '*', //允许的域,*代表所有的
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'//允许跨域的请求方法
};
文件上传配置
// 文件上传 config.default.js
config.multipart = {
mode: 'file',
};
后端session过期时间配置
// 设置cookie过期时间 config.default.js
config.session = {
key: 'EGG_SESS',
maxAge: 7*24 * 3600 * 1000, // 7 天
httpOnly: true,
encrypt: true,
};
3.基本流程
一、登录注册流程
(1).注册
根据接口设计文档确定接口、请求方式、传递数据类型
先后端根据接口设计文档,在router.js中增加注册接口,以及确定请求方式
前端操作
type.ts
//确定传递数据及数据类型 最后记得导出
// 注册接口类型
interface registerData{
tel: string,
password: string,
code: string
}
api.ts
//根据文档写接口函数
// 注册接口
export const registerPost = (data:registerData) => axios({ method: "post", url: "/register", data: data });
index.ts
//调用接口函数发起网络请求并传递要求的数据及数据类型
registerPost(data).then((response) => {
alert(response.data.data);
if (response.data.code == 1) {
//设置cookie,方便前端根据tel进行权限验证判断登录是否失效 setcookie为导入的设置cookie的函数
setCookie('tel', tel);
// 注册成功隐藏登录注册盒子
(login_box as HTMLDivElement).style.display = 'none';
}
});
后端操作
router.js
// 注册接口
router.post('/register', controller.user.register);
user.js
async register() {
const { ctx } = this;
// console.log(ctx.request.body);
let clientData = ctx.request.body;
// 判断注册时输入的验证码和后端发送的验证码是否正确,不正确直接返回验证码错误
if (clientData.code == ctx.session.code) {
const results = await this.app.mysql.select('user', { // 搜索 post 表
where: { tel: clientData.tel }, // WHERE 条件
});
if (results.length == 0) {
const result = await this.app.mysql.insert('user', { tel: clientData.tel, password: clientData.password });
// 将用户的uid存起来方便后端做权限验证
// console.log(result);
ctx.session.uid = result.insertId;
// console.log("uid:"+ctx.session.uid);
ctx.body = {
code: 1,
data: '注册成功'
};
} else {
ctx.body = {
code: -1,
data: '已有此账号,注册失败'
};
}
} else {
ctx.body = {
code: -2,
data: '验证码错误,注册失败'
};
}
}
(2).登录
前端操作
type.ts
确定传递参数及数据类型
// 登录接口
export interface loginData{
tel: string,
password: string
}
api.ts
写接口函数
// 登录接口
export const loginGet = (data:loginData) => axios({ method: "get", url: "/login", params: data });
login.html
调用接口函数
后端操作
router.js
写路由接口
// 登录接口
router.get('/login', controller.user.login);
user.js
处理前端传递的数据响应相应的数据
async login() {
const { ctx } = this;
const results = await this.app.mysql.select('user', { // 搜索 post 表
where: ctx.request.query, // WHERE 条件
});
console.log(results);
if (results.length == 0) {
ctx.body = {
code: -1,
data: '账号或密码错误'
};
} else {
ctx.session.uid = results[0].uid;
ctx.body = {
code: 1,
data: '登录成功'
};
}
}
二、文件上传
(1)单个图片(预览和上传)
// 头像预览
let fr = new FileReader();
fr.readAsDataURL(this.files[0]);//读取 异步的
fr.onload = function () {
// console.log(this.result);
//实现图片预览
(imgurl as HTMLImageElement).src = fr.result as string;
}
//传递给后端的文件数据
formData.append('upload', this.files[0])
//也可以在传递文件的时候把其他数据也传给后端
formdata.append('dataimg',JSON.stringify(data))
//后端处理
//修改用户信息
async getChangeUserInfo() {
const {ctx,app} = this;
//数据通过ctx.request.body拿到
let clientData = ctx.request.body;
//前端将数据命名为dataimg,所以后端通过clientData.dataimg拿到数据(最后记得反序列化,因为前端是通过JSON.stringfy传对象给后端)
clientData=clientData.dataimg;
//反序列化
clientData=JSON.parse(clientData);
// 获取传过来的文件
let files = ctx.request.files
// 获取图片的名字
let imgurl = files[0].filepath
// 获取图片的路径
let imgname = path.basename(imgurl)
// 当前user.js文件所在目录
let baseurl = __dirname;
// 由于要把上传的图片复制到public文件夹中 所以需要目录停留在app 使用path.dirname()方法
let mburl = path.dirname(baseurl)
// 在app下的public创建images文件
fs.mkdirSync(mburl + '/public/images/', { recursive: true });
// 把前端传过来的图片复制到创建的images文件中
fs.copyFileSync(imgurl, mburl + '/public/images/' + imgname)
clientData.headurl=`http://127.0.0.1:7001/public/images/${imgname}`;
console.log(clientData);
try {
//获取到更改数据
const row = clientData;
//更具uid查询
const options = {
where: {
uid: clientData.uid,
}
};
//把数据更新到数据库
const result = await this.app.mysql.update('user', row, options);
console.log(result);
ctx.body = {
code: 1,
data: `http://127.0.0.1:7001/public/images/${imgname}`
};
} catch (error) {
console.log(error);
ctx.body = '失败';
}
}
(2)多个图片(预览和上传)
let flag = document.createDocumentFragment();
for (const item of this.files) {
// console.log(item);
await new Promise((resolve, reject) => {
//FormData示例对象
moreFormData.append('img',item)
let fr = new FileReader();
fr.readAsDataURL(item);//读取 异步的
fr.onload = function () {
// console.log(this.result);
let img = document.createElement('img');
img.src = fr.result as string;
flag.append(img);
resolve(img)
}
});
// console.log(flag);
}
(imgbox as HTMLDivElement).append(flag)
三、token
(1)下载egg-jwt
npm i egg-jwt --save
(2)配置
// app/config/plugin.js
jwt : {
enable: true,
package: "egg-jwt"
};
// app/config/config.default.js
config.jwt = {
//这个越复杂就越难被破解
secret: "123456"
};
//axios.config.ts 记得导入getCookie
axios.defaults.headers.common['Authorization'] = getCookie('token');
(3)使用
后端先解构出app
const { ctx,app } = this;
后端生成token
//后端token传给前端
//设置过期时间
let term = (60 * 60 * 24) + 's' //token有效期,,默认设置为24小时
const token = app.jwt.sign({ uid: results[0].uid }, app.config.jwt.secret, { expiresIn: term });
后端解密token进行权限验证
try{
const gettoken = app.jwt.verify(ctx.request.headers.authorization, app.config.jwt.secret);
}catch(erro){
ctx.body={
code:-1,
data:'登录已失效'
}
}
前端把token存在cookie
//获取过来的token存在客户端的cookie中
4.中间件(例如:权限验证)
(1)在后端app文件夹下创建middleware文件夹
//创建authorization.js文件
module.exports = (app) => {
return async function authorization(ctx, next) {
try {
const gettoken = app.jwt.verify(ctx.request.headers.authorization, app.config.jwt.secret);
ctx.auth_token = gettoken;
await next();
} catch (erro) {
ctx.body = {
code:-1,
data: '登录已失效'
}
return false;
}
};
};
(2)router.js
const { router, controller, middleware } = app;
const authorization = middleware.authorization(app);
在请求userInfo接口前,会先执行authorization函数,如果没解密出token,则直接返回登录失效,然后reutrn,不在请求userInfo
如果,成功解密出token,则请求userInfo接口,在authorization.js中,由于已经通过ctx.auth_token = gettoken;把token放在了ctx.auth_token中,可以直接在userInfo中通过ctx.auth_token直接拿到token,然后操作数据库,返回相应的用户信息
router.get('/userInfo',authorization, controller.user.userInfo);
5.websocket-io(egg版本)
1.安装egg-socket.io
npm i egg-socket.io --save
2.开启插件:
// websocket配置 复制在后端plugin.js中
io : {
enable: true,
package: 'egg-socket.io',
}
// websocket配置 复制在后端config.default.js中
config.io = {
init: {}, // passed to engine.io
namespace: {
'/': {
connectionMiddleware: [],
packetMiddleware: [],
}
},
};
3.在app下创建文件
app下创建io文件夹,在io文件夹下创建chat.js文件
4.后端router.js文件配置
//先解构出io
const { router, controller, middleware,io } = app;
// socket.io 这边的路径记得对应 ‘/’是配置的命名空间(第二步) chat相当于路由
io.of('/').route('chat', io.controller.chat.ping);
5.客户端html(这边示例如何向指定客户端推送消息)
用户每次登录就要前端通过cookie存储后端返回的uid,每次发起连接的时候都会存储用户所用的客户端的id也是通过cookie存起来(只要一刷新就会在cookie更新本机客户端的id,发送数据的时候一定把自己的uid和要发送对象的uid以及消息一并转字符串发送,后端通过当前要发送消息的客户端发送的uid)
//先引入这个socket.io在线文件
<script src="https://cdn.bootcss.com/socket.io/2.1.0/socket.io.js"></script>
<script>
function setCookie(key, value, day = 7) {
document.cookie = `${key}=${value};max-age=${day * 24 * 60 * 60}`;
}
function getCookie(key) {
// 必须要有空格,allCookie是以'; '连接的字符串
let allCookie = document.cookie.split("; ");
let value = null;
allCookie.forEach((item) => {
let keyVal = item.split("=");
if (keyVal[0] == key) {
value = keyVal[1];
}
});
return value;
}
// browser
const log = console.log;
window.onload = function () {
// init
const socket = io("ws://localhost:7001/", {
// 实际使用中可以在这里传递参数
// query: {
// room: "demo",
// userId: `client_${Math.random()}`,
// },
// transports: ["websocket"],
});
socket.on("connect", () => {
const id = socket.id;
// 每次进行连接都更客户端cookie中的id,也就是自己的socketid
setCookie("id", id);
log("#connect,", id, socket);
// 监听自身 id 以实现 p2p 通讯
socket.on(id, (msg) => {
log("#receive,", msg);
});
// 假设点击发送
// let uid = 14;
// setCookie("uid", uid);
let data = {
sj: "你好啊aaaaa",
uid: getCookie("uid"),
to_uid: '15',
};
// 给服务器发送消息
// 给服务器发消息的时候,把自己的uid和要指定的浏览器的uid以及数据发送给后端
socket.emit("chat", JSON.stringify(data));
// 监听服务器给客户端推送的消息
socket.on("res", (msg) => {
console.log("res:", msg);
});
});
// 接收在线用户信息
// socket.on("online", (msg) => {
// log("#online,", msg);
// });
// 系统事件
// socket.on("disconnect", (msg) => {
// log("#disconnect", msg);
// });
// socket.on("disconnecting", () => {
// log("#disconnecting");
// });
// socket.on("error", () => {
// log("#error");
// });
window.socket = socket;
};
</script>
6.后端创建全局变量存储所有访问的客户端的id,并实时更新(chat.js文件)
'use strict';
// 创建map结构存储所有访问socket.io.html页面的客户端的id
let map = new Map();
const Controller = require('egg').Controller;
class ChatController extends Controller {
async ping() {
const { ctx, app } = this;
const message = ctx.args[0];
let data = JSON.parse(message)
// 每台客户端进行websocket连接后本身的唯一id,(一刷新id就会变)
const id = ctx.socket.id;
// 客户端一进行连接,就把客户端的uid和自身刷新的唯一id存起来
map.set(data.uid, id);
// 获取所有访问本页面的客户端
const nsp = app.io.of('/');
// 给所有客户端推送消息
// nsp.emit('res', `Hi! I've got your message: ${data.sj}`);
// await ctx.socket.emit('res', `Hi! I've got your message: ${message}`);
// (由于我客户端发送的data.to_uid是数字类型所以要)转字符串 通过发送数据的客户端传过来的to_uid去map里面找对应的id也就是socketid
let to_uid = data.to_uid;
// 得到要指定接受数据的客户端的id
let socketid = map.get(to_uid);
// 给指定socketid客户端推送消息
nsp.sockets[socketid].emit('res',data.sj);
}
}
module.exports = ChatController;