基于Vite+Egg搭建的前后端分离项目的基本步骤

391 阅读13分钟

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;