手把手教你写web全栈入门项目—React+Koa+MongoDB(3w字教程,真的很详细,有代码)

news/2024/7/15 17:57:59 标签: 前端, react.js, koa, 后端, web全栈

手把手教你写web全栈入门项目—React+Koa+MongoDB

文章目录

  • 手把手教你写web全栈入门项目—React+Koa+MongoDB
    • 前言
        • 一、推荐基础
        • 二、所需环境
        • 三、软件
        • 四、项目源码
        • 五、文章结构
        • 六、遇到问题怎么办
    • 前端
    • 后端
      • 一、实现功能
      • 二、目录结构
      • 三、技术选择
        • 1、技术
        • 2、中间件
        • 3、调试工具
      • 四、开始项目
        • 1、创建项目
        • 2、配置路由
          • 2.1 路由管理
          • 2.2 用户相关
          • 2.3 认证相关
          • 2.4 使用路由
        • 3、处理函数封装
          • 3.1 处理用户相关请求
          • 3.2 处理认证相关请求
        • 4、使用数据库
          • 4.1 连接数据库
          • 4.2 创建用户模型
          • 4.3 await异常处理
          • 4.4 用户相关
        • 5、认证
          • 5.1 注册
          • 5.2 登录
          • 5.3 检测请求合法性
          • 5.4 校验token
    • 后端交互
      • 一、前端发送请求
        • 1、封装axios
          • 1.1 目录
          • 1.2 service.js
          • 1.3 http.js
        • 2、 apis.js
      • 二、后端会遇到的问题
        • 1、CORS同源问题
    • 项目部署

前言

之前一直都是前端选手,趁放假把后端学了学,结合目前所在团队(如果感兴趣可以看看微信小程序“焕影一新”)的技术栈,完成了一个简单的登录系统。

如果你有时间,并且也想入门全栈,对于入门的练习项目来说,不必太复杂(比如一整个图书馆管理系统…),一个登录系统就够了。

提示:

如果你拥有前端基础或Node基础(Common JS),那么可能更容易接受,但如果你是后端选手也没关系,这篇博客的重点在于提供一个完整的前端-后端-数据库的项目流程,让大家在学习的时候有一个依据。

因为我之前对Vue已经有一定的项目基础,但是React还没有,所以前端框架采用React;而后端采用的是Node,框架为koa,这个框架是express原班人马打造的,用起来很爽,入门很快很简单;数据库采用非关系型数据库Mongodb,也是一款经典数据库了。

不管是前端还是后端,如果你目前只有一个学习方向,都可以选择对应方向作为练习,并不影响。

这个练习很简单,只要你肯学一定能拿下

一、推荐基础

该博客侧重于跑通全栈流程,其细节方面可能写的没有那么细,所以并不是零基础教程,例如前端虽然是使用React搭建,但是并不会从头开始介绍React。所以如果你有这么些基础,就会事半功倍:

  • React入门基础(只需要入门即可)
  • Node基础(需要学过Koa)
  • Mongodb基础(起码了解非关系型数据库、MongoDB概念)

如果你看过一遍React的书或视频,并不需要你有很多代码的练习;Node中需要熟悉Common JS语法;因为是通过后端代码连接数据库,且用的是封装好的第三方库,所以最低要求只需要了解非关系数据库和Mongodb的存储概念(数据库、集合、文档…)即可。

二、所需环境

  • Node环境:18.12.1
  • react版本:18.2.0
  • koa版本:koa2
  • MongoDB:4.*

MongoDB我使用的是4版本,如果你之前没有学过,我也推荐使用4版本,小版本号可随意。

因为网上教程多是针对4版本的,资源比较丰富,而6版本虽然新,但遇到问题花费的功夫可能更多、学习资源可能更难找到合适的。

三、软件

  • React、Koa都可使用VS Code编写。
  • MongoDB除了命令行,还可以使用图形化界面软件 MongoDB Compass
  • 后端接口测试工具可使用 postman,最好是下载PC桌面版,不要用web网页版

四、项目源码

各个代码都放到了github上,如果不熟悉github也可以联系我发源码。

五、文章结构

我是第一次写一整个项目的介绍博客,主要的结构是:

现在都是前后端分离开发,所以没必要混在一起写,这样反而会扰乱思维;而且以后遇到的项目,大概也是只需要你负责前端或者后端,单独进行开发,并且对项目进行调试是很重要的能力,比如前端可以通过mock技术模拟响应、后端可以通过接口调试工具进行模拟请求等等。

三个部分具体的内容可以从目录上略知一二。

六、遇到问题怎么办

对于博客、项目本身的问题,可以直接私信和我沟通或者评论,或者有什么建议和意见,也都可以直接私信或者评论。

大家在写项目的时候,可能也会遇到很多bug,或者很多环境问题,这个时候我的建议是,先尽量自己解决,并且简单记录一下解决方法,即使没有解决也尽量记录一下。如果解决不了可以给我发私信,我看到了,不管会不会,都会回复。

大家觉得不错的,可以点个收藏点个赞。我主要在做web方向,平时也会发一些学习笔记、技术分享,如果对web感兴趣也可以点个关注,谢谢各位大佬啦!欢迎交流!

前端

先介绍前端包含的东西,看看效果,再正式开始项目。

一、页面

页面很简单,只有两个:登录页和首页。其中我并没有配置/,所以启动项目的时候需要手动输入地址进行跳转。因为项目重点在于跑通全栈流程,所以并没有在前端细节和逻辑的严谨性上下功夫,其主要功能为“用户注册”和“用户登录”。

登录页
  • 路由:localhost:3000/login

首页
  • 路由:localhost:300/index

-

二、目录结构

项目初始化使用React官方脚手架:react-create-app

接下来大家可以看一眼目录结构,学习一下一个较为标准的react的架构,其中注释后带*即为重要文件/目录:

src					=》	src是最重要的一个目录!所以略去其他目录,重点介绍这个
    │  index.css	=》	全局样式
    │  index.js		=》	项目入口*
    │  
    ├─components	=》	公用组件库
    ├─http			=》	对axios请求库的封装*
    │      apis.js	=》	api接口
    │      http.js	=》	对请求对象封装
    │      service.js	=》	对axios封装*
    │      
    ├─layout		=》	布局文件(因为项目简单,故没有用到)
    ├─router		=》	项目路由*
    │      index.js
    │      
    ├─static		=》	静态资源
    │  └─imgs
    │          indexBackground.jpg
    │          
    ├─store			=》	存储库,用来封装`redux`等状态管理库(项目简单,未使用)
    ├─utils			=》	公用工具,封装一些使用频率高的模块、函数(未使用)
    └─views			=》	页面目录,所有页面均位于此***
        │  App.jsx	=》	页面入口
        │  
        ├─Index		=>	首页
        │  │  index.jsx		=》	首页
        │  │  index.scss	=》	样式
        │  │  
        │  └─components		=》	首页专用组件
        └─Login
            │  index.jsx	=》	登录页
            │  index.scss	=》	登录页样式
            │  
            └─components	=》	登录页专用组件
                │  Block.scss	=》	公用样式
                │  
                ├─LoginBlock	
                │      LoginBlock.jsx	=》	登录组件
                │      
                └─RegisterBlock
                        RegisterBlock.jsx	=》	注册组件

上面没有包含.github/workflows目录和docker目录,这个等项目完成之后,实现部署时讲解。

三、技术选择

使用技术技术介绍版本号文档地址
组件库蚂蚁集团开发的组件库:antd design5.1.1Antd Design
请求库很优秀的前端请求库,进行了很好的封装1.2.2Axios中文文档
CSS框架优秀CSS框架:Sass(和less同类型)1.57.1Sass官网
路由使用官方的路由库:react-router-dom6.6.1React Router中文网

其中只有路由库一定要保证v6版本。

四、开始项目

1、页面组件

1.1 目录

当前需要有登录页和首页,根据目录结构,我们将视图相关的页面组件放到views这个目录下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k4BlD8vd-1674633540356)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230119112509963.png)]

可以看到页面组件目录下的内容:都有一个index.jsxindex.scsscomponents

  • index.jsx:页面根组件,这是页面的入口,至于为什么叫index,这是因为引入组件的时候,如果不写路径最后的/index.jsx,那么会默认寻找叫index的文件。即@/views/Index@/views/Index/index.jsx效果一样。
  • index.scss:样式文件。
  • components:如果页面由很多个组件组成,应该把它们放在这儿,如果是多个页面公用的组件,应该提到更高级目录,也就是和views同级的components
1.2 代码
  1. 登录页

    一开始不需要写那么多,因为拿到一个项目首先是要配置好整个项目的基础,功能性代码就后面再说,先把架子搭起来——现在主要是需要把路由跑通。

    /**
     * 登录页
     */
    export default function Login() {
        
      return (
        <>
          <h1>This is login page.</h1>
        </>
      );
    }
    
  2. 首页

    /**
     * 首页
     */
    export default function Index() {
        
      return (
        <>
          <h1>This is home page.</h1>
        </>
      );
    }
    

2、配置路由

首先需要安装路由库react-router-dom,目前已经到了V6版本,该路由库的5、6版本和1、2、3、4版本出入较大,所以需要注意你之前学的是哪儿个版本的。

npm install --save react-router

如果你之前学过Vue,那么肯定会对Vue的路由方式有印象,其实React也提供了将路由抽离出来,作为一个配置项管理的方式:useRoutes。

这是一个Hook,使用方式很简单,(大家可以看看我之前写的这个:react-router v6 hook——useRoutes教程),直接看下面的内容也可以。

2.1 管理路由

在这里插入图片描述

首先可以如图创建router文件夹,管理路由文件。

router目录下的index.js中进行路由管理,这个阶段不需要使用Hook:

/**
 * 路由
 */

import Login from "../views/Login";
import Index from "../views/Index";

const routes = [
    {
        path: '/login',
        element: <Login></Login>
    },
    {
        path: '/index',
        element: <Index></Index>
    }
]

export default routes

如果你看过react-router官方文档,注意有几个坑,目前官网还没有更改:

  1. 组件必须要使用标签格式,而不能直接使用导入的变量名:

     使用<Login></Login>而不是使用`Login`
    
  2. 只能使用element申明路由使用的组件,而不能使用component

2.2 使用路由

来到app.js

import { useRoutes } from "react-router-dom";
import routes from "../router";

export default function App() {
  // 导入路由
  const elements = useRoutes(routes);

  return <>{elements}</>;
}

使用很简单,只需要将刚刚的路由对象传入useRoutes,就会返回一个保存有路由组件的变量,在需要使用的地方{elements}导入即可。

这里需要注意的地方也就只有Hook需要在最外层使用。

2.3 额外需要做的

如果只经过上面两个步骤,可能会导致一个错误:

useRoutes() may be used only in the context of a <Router> component.

字面意识是没有包裹在<Router>标签下。

解决:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './views/App';
import { BrowserRouter } from 'react-router-dom';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <>
    <BrowserRouter>
      <React.StrictMode>
        <App></App>
      </React.StrictMode>
    </BrowserRouter>
  </>
);

实际上需要导入BrowserRouter标签,然后如图在index.js入口文件写入。

2.4 测试

运行项目:npm start,切换地址栏地址:localhost:3000/indexlocalhost:3000/login。别忘了看看控制台有无错误信息。

目前不需要管后端,所以先把UI做完,等后端好了在进行联动。

3、首页

首页的效果图是这样的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FoEd4e1x-1674633540359)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230119173700654.png)]

很简单,就只是在页面中央展示一句话。我们先从这个简单的入手:

/**
 * index.jsx
*/
import './index.scss'

export default function Index() {
    return (
        <div className='Index'>
            <h1>登录成功!!!</h1>
        </div>
    )
}

关于样式,重点在居中以及怎么在jsx文件中引入。

/**
 * index.scss
*/
.Index {
    height: 100vh;
    width: 100vw;
    display: flex;
    justify-content: center;
    align-items: center;
}

由于没有使用组件,components目录为空。

4、登录页

4.1 页面组成

在这里插入图片描述

除开页面背景,中间是功能框,右上角是登录/注册切换按钮。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OP4z0jB3-1674633540360)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230119181044699.png)]

其中,把上图所示部分抽离出来,分成两个组件:登录组件、注册组件,而其余部分因为公用,直接写在index.jsx中。

题外话:

其实不用专门抽离出两个组件,将登录、注册功能分开,而是应该只使用一个组件,通过动态传参区别不同功能。但是我写得时候为了方便,加上前期考虑不周到,就将错就错。这个方案是简单的低级方案,如果你有想法,可以优化一下。

4.2 当前页面目录结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gROYcauR-1674633540360)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230119182243378.png)]

因为组件样式高度一致,所以抽离出来,统一封装进Block.scss

4.3 登录组件代码

LoginBlock.jsx

/**
 * 登录框组件
 */

import "../Block.scss";

// 输入框组件使用的是antd组件库中的
import { Input } from "antd";
// 函数式组件,需要引入Hook useState
import React, { useState } from "react";

/**
 * 因为可能需要使用ref传参,所以当时写的时候使用了React.forwardRef
 * 现在推荐使用函数式组件,类式组件比较冗杂
 */
const LoginBlock = React.forwardRef((props, ref) => {
  // 用户信息
  let [userInfo, setUserInfo] = useState({
    account: "",
    password: "",
  });

  // 输入触发,使用受控输入
  const handleInput = (e, type) => {
    /**
     * 这里为什么要这样写呢?
     * 因为如果修改userInfo,就是在响应式原型上面修改,没有作用
     * 如果直接 let temp = userInfo,虽然变量不同,但是都指向同一个地址
     * 修改temp和修改userInfo其实本质都一样
     * 所以现在要是用深拷贝,如果不了解,可以看看浅/深拷贝
     * 还可以看看这篇博客:https://blog.csdn.net/qq_51574759/article/details/128487007?spm=1001.2014.3001.5501
     */
    let temp = JSON.parse(JSON.stringify(userInfo));
    // 当输入时,使用受控方式实现数据的双向绑定
    if (type === "account") {
      temp.account = e.target.value;
    } else {
      temp.password = e.target.value;
    }
    // 必须用 useState 返回的方法更新数据,才能维持响应式
    setUserInfo(temp);
    // 通过props绑定的自定义事件,向父组件传递数据
    props.getData(temp);
  };

  return (
    <div className="LoginBlock">
      {/* 账号输入框 */}
      <div className="inputItem">
        <span>账号: </span>
        {/* 当前输入框组件使用的是antd中的 */}
        <Input
          placeholder="请输入您的账号(邮箱)"
          value={userInfo.account}
          // 实现数据双向绑定
          onChange={(e) => handleInput(e, "account")}
          type="text"
        ></Input>
      </div>
      <div className="inputItem">
        <span>密码: </span>
        <Input
          placeholder="请输入您的密码"
          value={userInfo.password}
          onChange={(e) => handleInput(e, "password")}
          type="password"
        ></Input>
      </div>
    </div>
  );
});

export default LoginBlock;
4.4 注册组件代码

RegisterBlock.jsx

/**
 * 注册框组件
 */

import "../Block.scss";

import { Input } from "antd";
import React, { useState } from "react";

const RegisterBlock = React.forwardRef((props, ref) => {
  /**
   * 这里的代码和登录组件复用率很高,几乎一模一样
   * 还是那个问题,两个组件本应该集成起来
   * 但是因为个人原因没有弄
   * 想要优化的同学可以优化一下
   */

  let [userInfo, setUserInfo] = useState({
    account: "",
    password: "",
  });

  const handleInput = (e, type) => {
    let temp = JSON.parse(JSON.stringify(userInfo));
    if (type === "account") {
      temp.account = e.target.value;
    } else {
      temp.password = e.target.value;
    }
    setUserInfo(temp);
    props.getData(temp);
  };

  return (
    <div className="LoginBlock">
      <div className="inputItem">
        <span>邮箱: </span>
        <Input
          placeholder="请输入邮箱"
          value={userInfo.account}
          onChange={(e) => handleInput(e, "account")}
          type="text"
        ></Input>
      </div>
      <div className="inputItem">
        <span>密码: </span>
        <Input
          placeholder="请牢记您的密码"
          value={userInfo.password}
          onChange={(e) => handleInput(e, "password")}
          type="password"
        ></Input>
      </div>
    </div>
  );
});

export default RegisterBlock;
4.5 登录、注册公用样式

Block.scss

.LoginBlock {
    width: 350px;
    height: 270px;

    .inputItem {
        display: flex;
        justify-content: space-between;
        flex-wrap: nowrap;
        align-items: center;
        padding-top: 50px;

        span {
            width: 50px;
        }
    }
}
4.6 页面代码

index.jsx

/**
 * 登录页
 */

import "./index.scss";

// 引入组件
import RegisterBlock from "./components/RegisterBlock/RegisterBlock";
import LoginBlock from "./components/LoginBlock/LoginBlock";
import { Button } from "antd";

// 引入封装好的api,现在不需要管这个
import { UserLogin, UserRegister } from "../../http/apis";

// 引入Hook
import { useState } from "react";
import { useNavigate } from "react-router-dom";

export default function Login() {
  // Sign in则为登录状态,Sign Up为注册状态
  let [loginState, setLoginState] = useState("Sign In");

  // 存储当前输入的用户信息
  const [userInfo, setUserInfo] = useState({});

  // 使用Hook生成router的操作对象,之后可以使用它进行编程式路由
  const navigate = useNavigate();

  /**
   * 点击右上角切换按钮触发,切换当前输入状态
   * Sign In 为登录状态
   * Sign Up 为注册状态
   */
  const handleSwitch = () => {
    if (loginState === "Sign In") {
      setLoginState("Sign Up");
    } else {
      setLoginState("Sign In");
    }
  };

  /**
   * 点击确认按钮触发
   * @returns 
   */
  const confirm = async () => {
    // 下面是发送请求,检查返回值的过程,现在不用管这个
    let res = null;
    if (loginState === "Sign In") {
      res = await UserLogin(userInfo);
    } else {
      res = await UserRegister(userInfo);
    }
    res = res.data;
    if (res.code === 200) {
      console.log(res);
    } else {
      alert("请检查账号/密码是否错误!");
      return;
    }
    if (loginState === "Sign In") {
      navigate("/index");
    } else {
      alert("注册成功,请重新登录");
    }
  };

  return (
    <>
      {/* 登录框 */}
      <div className="LoginPage">
        {/* 功能区 */}
        <div className="func">
          {/* 切换按钮 */}
          <div className="switchButton" onClick={handleSwitch}>
            {loginState}
          </div>
          {/* 登录/注册
            * 通过三目元算符,判断当前输入状态,更新展示的组件
            * 需要注意的是,在每个组件身上都传入了一个自定义属性
            * 值为Hook返回的响应式数据修改函数
           */}
          {loginState === "Sign In" ? (
            <LoginBlock getData={setUserInfo}></LoginBlock>
          ) : (
            <RegisterBlock getData={setUserInfo}></RegisterBlock>
          )}
          {/* 确认 */}
          <Button
            onClick={confirm}
            className="button"
            shape="round"
            size="large"
          >
            {loginState}
          </Button>
        </div>
      </div>
    </>
  );
}

index.scss,页面样式,其中背景图片可自定义。

@mixin flex($direction: row, $isCenter: center) {
    display: flex;
    flex-direction: $direction;

    @if($isCenter) {
        justify-content: center;
        align-items: center;
    }
}

.LoginPage{
    height: 100vh;
    width: 100vw;
    @include flex();
    background-image: url('../../static/imgs/indexBackground.jpg');
    background-size: cover;
    color: white;

    .func {
        position: relative;
        height: 550px;
        width: 900px;
        border: 2px solid rgb(206, 204, 204);
        border-radius: 15px;  
        @include flex(column);        

        .switchButton{
            position: absolute;
            right: 50px;
            top: 20px;
            font-size: 20px;
            cursor: pointer;
        }

        .button{
            width: 220px;
            background-color: #10ac84;
            border: none;
            height: 50px;
            color: white;
            font-size: 20px;
        }
    }
}

5、完成UI

经过上面的一系列步骤,已经完成了项目前端部分的UI。

还剩下一个前端的重点——请求。不过这个等到后端开发、测试通过之后再考虑也不迟。至于更后面的部署操作,也是一个重难点,我打算另写一篇博客专门说,链接会放在前后端开发完成之后的地方。

后端

一、实现功能

先看看后端由哪儿些功能:

  • 用户登录—/auth/login
  • 用户注册—/auth/register
  • Token校验—/auth/verify
  • 查询用户列表—/users/find

二、目录结构

项目初始化使用koa-generator脚手架搭建,在此基础上增添了一些东西,其中注释后带*即为重要文件/目录:

├─bin			=》	其下有一个www.js文件,是项目的启动入口*
|
├─controller	=》	对应请求的处理函数都封装在此**
│  ├─auth		=》	认证登录相关的处理程序
│  └─users		=》	用户相关的处理程序
|
├─db			=》	数据库封装,该目录下index.js即为数据库连接、启动文件*
|
├─docker		=》	Docker部署相关,现在可以不管这个
|
├─models		=》	数据表模型*
|
├─public		=》	公共文件,不用怎么管
│  ├─images		=》	图像
│  ├─javascripts	=》	脚本
│  └─stylesheets	=》	样式
|
├─routes		=》	路由不同的处理函数去处理请求*
|
├─utils			=》	公用函数,封装一些多个文件都需要使用的代码
|
└─views			=》	视图,不需要管这个
|
└─app.js		=》	对app对象的封装**

三、技术选择

1、技术

使用技术技术介绍版本号文档地址
await-to-js能够方便处理await中产生的异常3.0.0(都行)await-to-js
jsonwebtoken用户认证9.0.0jsonwebtoken
mongoose操作MongoDB的第三方JS库,封装了很多好东西6.8.2Mongoose中文文档

2、中间件

我们的重点不在koa基础,下方使用到的中间件就不一一介绍了。有些是通过脚手架创建项目的同时自动配置好了,而有些需要自己手动下载。

某些中间价后面会使用到,到时候结合项目进行说明。

3、调试工具

现在前后端分离,后端的接口并不需要前端直接发送请求去调试,而是先用接口测试工具模拟发送请求,进行后端单独调试,最后前端将UI做好,到了配置请求的时候,再让后端上线已经做好的接口,这个时候再进行前后端联动调试。

那么常用的接口调试工具有哪儿些呢?我一直用的是老牌的工具:postman。推荐下载PC端工具,不要用网页版的,那个会出现一些问题,网页肯定比不上PC应用。

其余还有 apibox 之类的工具,我不是很了解,如果有常用的可以用你习惯的,没有的话还是推荐postman,确实不错。

四、开始项目

1、创建项目

先安装脚手架:

npm install -g koa-generator

因为调试的时候需要使用nodemon进行热重启(就是修改代码以后不用关闭、重启项目,而是又nodemon自动更新),而创建项目的同时自带了nodemon

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jlXi22C6-1674633540361)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230120174540265.png)]

这里是我全局安装了一下nodemon,然后将dev启动命令修改成了使用本机全局的nodemon启动项目。如果你想就用脚手架自带的,可以看看这篇博客:koa2入门,使用koa-generator脚手架搭建项目。

创建好项目后,在/bin/www中可以看到项目启动时的一些配置,其中有这么一行代码:

var port = normalizePort(process.env.PORT || '3000');

这个是项目启动的端口配置,如果没有在环境中指定端口号,就默认在3000端口启动项目。但是我们的React前端的默认启动端口也是3000,会造成冲突,所以这里需要修改成2000,然后运行项目,检查是否正常。s

2、配置路由

后端要处理不同的请求,依据的就是路由。通过不同的地址,请求到来的时候会自动分配处理程序。手脚架初始化项目的时候已经帮你下好了一个中间件koa-router

2.1 路由管理

路由统一在/routes目录下进行管理,不同类型的服务配置在不同的文件中。

在这里插入图片描述

如图就是本项目的路由管理,其中auth是认证相关,users是用户相关。

2.2 用户相关

以用户相关为例,介绍一下怎么配置路由。

users.js :

// 导入路由对象
const router = require('koa-router')()

// 导入用户相关处理对象,上面有请求对应的处理函数
const usersCtr = require('../controller/users/usersCtr')

/**
 * 路径公用部分,当前文件下所有路由都会自动加上这个
 * 比如用户查询中配置的路由是 /find
 * 那么真正需要请求的路径是 /users/find
 */
router.prefix('/users')

// 查询用户列表,其处理函数是usersCtr对象上的方法
router.get('/find', usersCtr.GetUserList)

// 最后暴露出去,注意commonJS的写法
module.exports = router

这个时候还没完,还有两个点需要动作:

  1. 路由的处理函数的封装
  2. 暴露出去的router对象怎么用

关于第一点,可以先不使用封装的处理函数,而是这样写,便于测试路由是否配置成功:

// 查询用户列表
router.get('/find', () => {
    console.log("用户查询路由配置成功")
})

别的路由的测试同理。至于第二点,会在 2.4 中说到。

2.3 认证相关

和上面一样,进行用户认证的服务路由配置。

auth.js :

/**
 * 用户认证
 */
const router = require('koa-router')()
const authCtr = require('../controller/auth/authCtr')

router.prefix('/auth')

// 用户登录
router.post('/login', authCtr.UserLogin)

// 用户注册
router.post('/register', authCtr.UserRegister)

// token校验
router.post('/verify', authCtr.UserVerify)

module.exports = router
2.4 使用路由

上面我们创建了两个路由,都分别导出了,但是这个时候并没有使用路由,所以我们还需要去根目录下的app.js中进行配置。

app.js中,我们主要进行的就是中间件的使用、路由的配置、错误监听等等操作。

首先在最上方引入:

// 引入路由
const users = require('./routes/users')
const auth = require('./routes/auth')

之后,在文件底部,中间件配置完成的地方使用路由:

// 接入路由
app.use(users.routes(), users.allowedMethods())
app.use(auth.routes(), auth.allowedMethods())

这个时候就已经配置好了路由,通过不同的路由,就可以触发不同的处理函数。但是处理函数还没有配置好,可以先不用封装形式的处理函数进行测试。

3、处理函数封装

如果将对请求的处理也写在绑定路由这里,一旦功能复杂,那么代码就会看起来十分杂乱,一会是绑定路由,一会是处理函数,一会可能又是额外封装的函数…

所以我们将所有对请求的处理函数都放到统一的地方进行管理:/controller

这个目录长这样:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FWzntr10-1674633540363)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230120212937068.png)]

可以看到,和路由对应,分别有用户相关和认证相关的处理函数的封装。

3.1 处理用户相关请求

这是/controller/users/usersCtr.js的完整代码,涉及到数据库的使用,现在可以不用管。

重点在于,实现了一个GetUserList函数,并将其暴露了出去(导出一个对象,其身上有一个方法GetUserList)

/**
 * 用户操作
 */

const userModel = require('../../models/userModel')

// 响应
const response = (ctx, code, msg, data) => {
    ctx.body = {
        code,
        msg,
        data
    }
}

/**
 * 查询用户列表
 * @param {*} ctx 
 */
const GetUserList = async (ctx) => {
    const [err, res] = await global.to(userModel.find())
    if (err) {
        response(ctx, 1001, "查询用户列表时错误", err)
        console.log(err)
    } else {
        if (res) {
            response(ctx, 200, "查询成功", res)
        } else {
            response(ctx, 1002, "查询失败")
        }
    }
}

module.exports = {
    GetUserList
}

最后,导出的对象将在/routes/users中引入,并且在请求被路由的时候,调用该函数处理请求。

3.2 处理认证相关请求

这里是/controller/auth/authCtr.js中的内容,同样的,当前重点在于写了三个分别对应用户登录、用户注册、Token校验的处理函数,并且将他们导出,由/routes/auth.js引入,在配置路由时传入,作为请求到来时的回调函数。

所有数据库操作现在都不需要管。项目是一步一步搭建起来的,每一步只需要把这一步做好就够了,确保这一步做的是正确的、高效的,不要急于求成,做这一步的时候看下一步,考虑太多也可能会扰乱自己的节奏,出现疏漏,导致每步都有点儿小问题。

所以当前只需要确保这里写的处理函数能够正确地在请求到来时调用,至于具体的逻辑,等数据库相关代码封装完成之后再分一步去做就好了。

// 用户认证——处理

const userModel = require('../../models/userModel')
const jwt = require('jsonwebtoken')

// 响应
const response = (ctx, code, msg, data) => {
    ctx.body = {
        code,
        msg,
        data
    }
}

// token 秘钥
const secretKey = "@HY&learning$Clogin#*back(end=+"

/**
 * 用户登录
 */
const UserLogin = async (ctx) => {
    const { account, password } = ctx.request.body

    const [err, res] = await global.to(userModel.findOne({ account, password }))

    if (err) {
        response(ctx, 1001, "登录时错误", err)
    } else {
        if (res) {
            await Login(ctx, account, password)
        } else {
            response(ctx, 1002, "登录失败")
        }
    }
}
// 用户登录
const Login = (ctx, account, password) => {
    const token = jwt.sign({ account, password }, secretKey, {
        expiresIn: 3600 * 24 // 3600=一小时  => 一天
    })

    response(ctx, 200, "登录成功", { account, token })
}


/**
 * 请求注册
 */
const UserRegister = async (ctx) => {
    const { account, password } = ctx.request.body

    const [err, res] = await global.to(userModel.findOne({ account }))
    if (err) {
        console.log(err)
    } else {
        if (res) {
            response(ctx, 2001, "当前邮箱已注册", res)
        } else {
            await Register(ctx, account, password)
        }
    }
}
// 用户注册
const Register = async (ctx, account, password) => {
    const [err, res] = await global.to(userModel.create({ account, password }))
    if (err) {
        response(ctx, 2002, "注册时发生错误", err)
        console.log(err)
    } else {
        if (res) {
            response(ctx, 200, "注册成功")
        } else {
            response(ctx, 2003, "注册失败")
        }
    }
}


/**
 * 校验token
 */
const UserVerify = async (ctx) => {
    let token = ctx.header.authorization
    token = token.replace('Bearer ', '')

    const { account, password } = jwt.verify(token, secretKey)

    const [err, res] = await global.to(userModel.findOne({ account, password }))

    if (err) {
        response(ctx, 3001, "校验token时错误", err)
    } else {
        if (res) {
            response(ctx, 200, "校验通过", res)
        } else {
            response(ctx, 3002, "校验失败")
        }
    }
}

module.exports = {
    UserLogin,
    UserRegister,
    UserVerify
}

4、使用数据库

当前项目数据库使用的是MongoDB,所以在开始这一步之前,请确保你已经安装好了MongoDB,并且能正常运行。

4.1 连接数据库

如果不是用第三方库想要手动写连接代码,那会非常困难,对之后的操作也非常不利。既然有轮子,就不用重复造了。所以我们使用一个叫mongoose的第三方库连接和操作数据库。

下载完mongoose,就可以尝试连接数据库了:

/db/index.js

/**
 * 连接数据库
 */

const mongoose = require('mongoose')

/**
 * 连接的是mongodb数据库,依赖的协议已经不是http了!而是mongodb!
 * 同时,mongodb默认启动在27017端口,如果你修改了,请将地址也对应修改
 * 最后的路径就是要连接的数据库名称
 */
const dbUrl = "mongodb://127.0.0.1:27017/login-backend"

const connectDB = () => {
    // 连接数据库,后面的配置项这样写就行了
    mongoose.connect(dbUrl, { useNewUrlParser: true, useUnifiedTopology: true })

    // 连接监听
    mongoose.connection.on('open', err => {
        if(err) {
            console.log(err)
            console.log("数据库连接失败!!!")
        } else {
            console.log("数据库连接成功")
        }
    })
}

module.exports = connectDB

上面的代码是封装了一个连接功能,还需要调用。而数据库应该是在后端程序启动的时候就要自动连接的,所以我们在app.js中进行调用:

// 连接数据库
const connectDB = require('./db/index')
connectDB()
4.2 创建用户模型

MongoDB的一个数据库中,数据集可以存储很多个同类型的、具有相同规范约束的文档,而数据集就对应着mongoose中的model(模型)。

/models/userModel.js

/**
 * 用户规范
 */

const mongoose = require('mongoose')

/**
 * schema是配置对象
 * 可以在这里规定集合中的文档的格式、类型
 */
const schema = new mongoose.Schema({
    account: {
        type: String,
        required: true
    },
    password: {
        type: String,
        required: true
    }
})

// 这里通过mongoose提供的api创建对象
const userModel = mongoose.model("Users", schema)

// 暴露出这个对象,之后就可以通过它来操作这个数据集了
module.exports = userModel
4.3 await异常处理

对于数据库的操作都是异步的,而解决异步操作的一些问题,我们会自然想到async和await。可是有一个问题:如果同步处理异步操作,发生异常怎么办?这个时候可能会想到,使用 promise + try、catch,而不使用await。不过这样写,代码结构就变得复杂起来了,当然可以进行封装,来减少复用代码量,这完全没有问题。不过我没有这么做,而是继续使用await,不过还用到了一个第三方库await-to-js

这个库提供了一个to函数,假如有一个异步操作,那么这样写:

const [err, res] = await to(异步函数)

会自动封装为一个数组进行返回,第一项就是处理异步时发生的异常信息,没有异常则默认为空,第二个是异步函数正常的返回值。

因为这个函数在所有异步操作时都需要用到,所以我就将其添加到了全局中:在app.js里面添加这么两句代码:

// await错误处理
const { to } = require('await-to-js')
global.to = to

使用的时候,则直接global.to(异步操作)就可以了,不用每次都引入。

4.4 用户相关

上面创建好了模型,那么怎么通过模型操作数据库呢?

在控制层/controller目录下,我们在处理请求的时候涉及到了数据库操作,所以我们以查询用户列表,请求路径/users/find为例,看看怎么操作数据库。

首先封装了一个函数response,用于返回请求的响应,这是koa的知识,你应该掌握了:

以下都是在/controller/userCtr.js下进行的。

/**
 * 向请求方返回响应
 * @param {ctx} ctx 
 * @param {Number} code 
 * @param {String} msg 
 * @param {Object} data 
 */
const response = (ctx, code, msg, data) => {
    ctx.body = {
        code,
        msg,
        data
    }
}

将刚才配置好的模型对象导入:

const userModel = require('../../models/userModel')

利用这个模型开始写处理函数:

/**
 * 查询用户列表
 * @param {*} ctx
 */
const GetUserList = async (ctx) => {
    const [err, res] = await global.to(userModel.find())
    if (err) {
        response(ctx, 1001, "查询用户列表时错误", err)
        console.log(err)
    } else {
        if (res) {
            response(ctx, 200, "查询成功", res)
        } else {
            response(ctx, 1002, "查询失败")
        }
    }
}

其中核心点在于:

const [err, res] = await global.to(userModel.find())

userModel.find()即异步查询出模型对应的数据集中所有的文档数据。而下面的代码就是对查询结果的处理:如果查询成功,直接返回查询结果;如果查询失败,则返回对应的错误信息。

接下来看看整体代码:

/**
 * 用户操作
 */

const userModel = require('../../models/userModel')

/**
 * 向请求方返回响应
 * @param {ctx} ctx 
 * @param {Number} code 
 * @param {String} msg 
 * @param {Object} data 
 */
const response = (ctx, code, msg, data) => {
    ctx.body = {
        code,
        msg,
        data
    }
}

/**
 * 查询用户列表
 * @param {*} ctx 
 */
const GetUserList = async (ctx) => {
    const [err, res] = await global.to(userModel.find())
    if (err) {
        response(ctx, 1001, "查询用户列表时错误", err)
        console.log(err)
    } else {
        if (res) {
            response(ctx, 200, "查询成功", res)
        } else {
            response(ctx, 1002, "查询失败")
        }
    }
}

module.exports = {
    GetUserList
}

5、认证

认证相关技术相比于上面的用户相关技术要稍微多那么一点儿,因为不仅涉及到数据库操作,还涉及到一些认证操作,比如登录成功之后,需要返回一个认证成功的凭证,在请求其他需要登录之后才能请求的接口时要验证凭证以确保安全。关于认证有几个解决方案,这里我们选择用的最多的Token方案。即当用户登录成功之后,返回值中会携带一个经过加密的Token,这是一个很长的字符串;前端接收到Token之后需要自己保存,以后如果请求一些需要认证的接口的时候(有些接口不需要Token,所有用户都能访问,比如注册、登录接口;而有些接口需要登录后才能访问,这些就是需要Token的接口),在请求的头中应该携带这个Token,那么请求发送到服务器的时候,后端程序就能检测该请求是否够资格使用服务(Token是否是错误的、伪造的、过期的…)。

其实具体的认证功能并不需要自己写,已经有成熟的第三方库供我们直接使用,这个项目选择经典的:jsonwebtoken库。

下载很简单:

npm install jsonwebtoken

难在怎么去使用,以及理解这个流。

接下来主要以认证相关代码,讲解认证登录流程,涉及到的数据库操作则不再重点讲。

数据库操作大家可以看mongoose文档就行,在技术选择的部分我放了链接,如果不想看,直接看代码也是能看懂什么意思的,所以不用纠结这个。之后要用到别的操作,再根据文档查看即可,查看文档也是一个非常重要的能力。

接下来的讲解顺序是按照一个新用户,第一次接触到这个项目时的操作顺序来的,也就是用户视角,而这个顺序也可以是我们写代码的顺序。

以下代码无特殊说明,都在/controller/authCtr.js

5.1 注册

首先我们做一件事儿,这是一个正常的认证登录流程的开始:注册。

先看代码:

/**
 * 请求注册
 */
const UserRegister = async (ctx) => {
    const { account, password } = ctx.request.body

    const [err, res] = await global.to(userModel.findOne({ account }))
    if (err) {
        console.log(err)
    } else {
        if (res) {
            response(ctx, 2001, "当前邮箱已注册", res)
        } else {
            await Register(ctx, account, password)
        }
    }
}
// 用户注册
const Register = async (ctx, account, password) => {
    const [err, res] = await global.to(userModel.create({ account, password }))
    if (err) {
        response(ctx, 2002, "注册时发生错误", err)
        console.log(err)
    } else {
        if (res) {
            response(ctx, 200, "注册成功")
        } else {
            response(ctx, 2003, "注册失败")
        }
    }
}

现在来解释一下代码:

  • 第五行:const { account, password } = ctx.request.body,这里其实用到了一个中间件koa-bodyparser,这个是脚手架默认就安装了的,功能是,将post请求的请求体复制到ctx.request.body上,之后要拿数据的时候就可以直接拿了,不然要拿到post请求体很麻烦,涉及到node原生操作,需要监听数据流。
  • 第六行:userModel.findOne({ account }),这里是以账号为查询条件查询数据库中是否有符合的数据,目的是防止同一个账号重复注册。
  • 第十九行:注册了一个Register函数,这个函数用来注册账号,在第十四行,该账号满足注册条件时调用。
  • 第二十行:userModel.create({ account, password }),这里是向数据集中增添一条文档数据,也就是将需要注册的账号、密码添加到数据库。添加的数据需要符合当初定义数据集时的规范,比如突然想多存一个nickname,但是初始化数据集的时候并没有定义这个量,那么就算添加也没有用,会被过滤掉。

注册服务并没有用到认证操作,只是将数据添加到数据库。

5.2 登录

首先需要引入

const userModel = require('../../models/userModel')
const jwt = require('jsonwebtoken')

之后需要定义一个秘钥,这个秘钥是用来进行加密用的,可以是一个字符串,也可以是一个文件,这里先用一个字符串进行定义:

// token 秘钥,尽量复杂一点儿
const secretKey = "@HY&learning$Clogin#*back(end=+"

准备完成之后,再来看主要代码:

/**
 * 用户登录
 */
const UserLogin = async (ctx) => {
    const { account, password } = ctx.request.body

    const [err, res] = await global.to(userModel.findOne({ account, password }))

    if (err) {
        response(ctx, 1001, "登录时错误", err)
    } else {
        if (res) {
            await Login(ctx, account, password)
        } else {
            response(ctx, 1002, "登录失败")
        }
    }
}
// 用户登录
const Login = (ctx, account, password) => {
    const token = jwt.sign({ account, password }, secretKey, {
        expiresIn: 3600 * 24 // 3600=一小时  => 一天
    })

    response(ctx, 200, "登录成功", { account, token })
}

现在来解释一下:

  • UserLogin函数是用来做登录验证的,真正的登录服务在Login函数里。
  • 21到23行是根据账号信息、token 秘钥、配置项生成一个token。其中配置项中配置了一个expiresIn,这个是过期时间,以秒为单位。
  • response函数还是之前封装过的那个,返回的时候带上token。
5.3 检测请求合法性

请求某些接口的时候,要求用户以登录的身份去请求,因为很多时候大部分服务不会提供给游客。

那么前端发送的请求头中携带了token,到达后端后我们怎么检测这个token是否合法呢?在之前的代码中可能会看到,在authCtr.js中有一个检测token的接口。但其实不用这么麻烦,这个是以请求的方式主动检查,而一般都是配置中间件,当请求到来的时候自动检测,并且也不需要自己写中间件,直接导入别人写好的就行,这里用官方的:koa-jwt

正常npm下载即可。

接下来我们来使用这个中间件,在app.js中加入以下代码:

// 认证
const koaJwt = require('koa-jwt')
app.use(koaJwt({
  secret: "@HY&learning$Clogin#*back(end=+"
}).unless({
  path: [/^\/auth\//]
}))

解释一下:

  • 中间件本质是一个函数,当请求到来的时候会自动依次加载调用,而koaJwt会返回一个根据你的配置对象生成的中间件函数。
  • 3到5行,给koaJwt传入一个配置对象,其中有一个secret属性,这个就是我们上面登录的时候,创建token时用到的token秘钥。请求到达处理函数之前,这个中间件会根据秘钥解码token,检测是否合法,是否过期。
  • 第5行,unless也可以传入一个配置对象,其中一个属性是path,这里面是使用数组+正则表达式配置的一些路径,当请求能匹配这些路径之一的时候,将不再校验token合法性。也就是说,这里的路径对应的服务是开放的、无认证的、游客可用的。

这样配置完之后,程序启动时就会自动开启token校验,这时没有登录而去请求查询用户列表,则会返回认证未通过的错误。

5.4 校验token

只有请求时默认检测token有效性是不够的。平时我们都会遇到一些情况,比如飞书15天就要重新登录一次,因为他设置了token过期时间,而检测token是否过期,不可能故意发送一个请求,让其返回错误,通过这个查看是否过期,而是应该主动携带token去请求一个校验token有效性的接口,通过这个接口返回的结果判断token是过期了、还是修改密码了…

来看看代码:

/**
 * 校验token
 */
const UserVerify = async (ctx) => {
    let token = ctx.header.authorization
    token = token.replace('Bearer ', '')

    const { account, password } = jwt.verify(token, secretKey)

    const [err, res] = await global.to(userModel.findOne({ account, password }))

    if (err) {
        response(ctx, 3001, "校验token时错误", err)
    } else {
        if (res) {
            response(ctx, 200, "校验通过", res)
        } else {
            response(ctx, 3002, "校验失败")
        }
    }
}

解释一下:

  • 请求携带的token默认是放在请求头的authorization中,这也是第5行为什么要这样拿token的原因。
  • 第6行:因为默认使用的是Bearertoken,在请求发送的时候,会自动在token前面加上Bearer 再绑定到head请求头上。注意,Bearer和token之间**隔着一个空格!!!**所以真正的token应该是从头上获取并去掉前缀后的字符串。
  • 第8行:jwt.verify(token, secretKey)将token和秘钥一起传入jsonwebtoken提供的verify函数中,就会自动检查,并返回检测结果。

后端交互

说完前端后端之后,我们要进行最后的工作:前端请求后端服务,完成闭环。

一、前端发送请求

1、封装axios

大家学到全栈的地步,肯定之前就看过原生JS是怎么发送请求的,那属实是太麻烦了,即使后面出现了ajax,原生代码还是很麻烦。不过好在我们有一个好用的厉害的请求库:axios

下载:

npm install axios

这个请求库是当前最流行的前端请求库之一,他将底层代码经过了多层封装,大大简化了请求操作。不过过度封装也不好,耦合度过高也不一定适用于所有项目。所以axios保持了一定的灵活空间,让我们可以根据自己的项目,只进行简单封装就能达到灵活使用的效果。

接下来我们来封装一下,没学过axios的一定要多看文档,不然都不知道在干嘛。

1.1 目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CoQelivl-1674633540365)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230123144414595.png)]

大家可以看到,这里有一个叫http的目录,这里就是进行axiox封装和请求封装的地方。

apis.js则存放各种接口,到时候直接引入这里面对应的函数,调用就可以发送请求。

1.2 service.js

这个文件主要是进行请求的配置、响应的拦截等操作,最后导出axios配置对象(也就是说这是服务于别的步骤的)。

29行这里需要重点看一看,其余的地方根据各种注释看。

/****   request.js   ****/
// 导入axios
import axios from 'axios'

//1. 创建新的axios实例,
const service = axios.create({
  // 公共接口--process.env.BASE_API是webpack中的全局环境变量,但是这里可以写死
  // baseURL: process.env.BASE_API,
  baseURL: 'http://localhost:2000',
  // 超时时间 单位是ms,这里设置了3s的超时时间
  timeout: 3 * 1000
})
// 2.请求拦截器
service.interceptors.request.use(config => {
  //发请求前做的一些处理,数据转化,配置请求头,设置token,设置loading等,根据需求去添加
  config.data = JSON.stringify(config.data); //数据转化,也可以使用qs转换
  config.headers = {
    'Content-Type': 'application/json' //配置请求头
  }
  //如有需要:注意使用token的时候需要引入cookie方法或者用本地localStorage等方法,推荐js-cookie
  // const token = cookies.get('userToken');//这里取token之前,你肯定需要先拿到token,存一下
  // if(token){
  //   // config.params = {'Authorization':token} //如果要求携带在参数中
  //   config.headers.Authorization= token; //如果要求携带在请求头中
  // }
  // 这里应该从前端缓存中取出登录是存放的token进行赋值
  // 调试的时候我建议可以将生成的token直接复制到这里
  // 至于怎么缓存,可是使用浏览器本地缓存,也可是使用如redux之类的技术
  config.headers.Authorization = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50IjoiMTU3NTA3OTQzNUBxcS5jb20iLCJwYXNzd29yZCI6IjE1NzUwNzk0MzUiLCJpYXQiOjE2NzI0NTg2OTcsImV4cCI6MTY3MjU0NTA5N30.sAAeSplKLIXv6wqmNJP-s9TpC14Ite2dlpX4C-2y8Rs"
  return config
}, error => {
  Promise.reject(error)
})

// 3.响应拦截器
service.interceptors.response.use(response => {
  //接收到响应数据并成功后的一些共有的处理,关闭loading等

  return response
}, error => {
  /***** 接收到异常响应的处理开始 *****/
  if (error && error.response) {
    // 1.公共错误处理
    // 2.根据响应码具体处理
    let ERR = "";
    switch (error.response.status) {
      case 0:
        ERR = "您已离线,请检查网络设置";
        break;
      case 400:
        ERR = '错误请求'
        break;
      case 401:
        ERR = '未授权,请重新登录'
        break;
      case 403:
        ERR = '拒绝访问';
        break;
      case 404:
        ERR = '请求错误,未找到该资源';
        // console.log(error);
        // window.location.href = "/error_page"
        break;
      case 405:
        ERR = '请求方法未允许'
        break;
      case 408:
        ERR = '请求超时'
        break;
      case 500:
        ERR = '服务器端出错'
        break;
      case 501:
        ERR = '网络未实现'
        break;
      case 502:
        ERR = '网络错误'
        break;
      case 503:
        ERR = '服务不可用'
        break;
      case 504:
        ERR = '网络超时'
        break;
      case 505:
        ERR = 'http版本不支持该请求'
        break;
      default:
        ERR = `连接错误${error.response.status}`
    }
    alert(ERR);
  } else {
    // 超时处理
    if (JSON.stringify(error).includes('timeout')) {
      alert('服务器响应超时,请刷新当前页')
    }
    alert('连接服务器失败');
  }

  // Message.error(error.message)
  /***** 处理结束 *****/
  //如果不需要错误处理,以上的处理过程都可省略
  return Promise.resolve(error.response)
})
//4.导入文件
export default service
1.3 http.js

这里主要进行的是将各种请求方法进行简单封装,最后返回一个配置好的请求对象,传入请求路径、携带参数就可以进行发送请求。

注意不同的请求方法携带参数的方式不一样:

  • get、put是通过params传参
  • post、delete、patch是通过data传参
/****   http.js   ****/
// 导入封装好的axios实例
import service from "./service"

const http ={
    /**
     * methods: 请求
     * @param url 请求地址 
     * @param params 请求参数
     */
    get(url,params){
        const config = {
            method: 'get',
            url:url
        }
        if(params) config.params = params
        return service(config)
    },
    post(url,params){
        const config = {
            method: 'post',
            url:url
        }
        if(params) config.data = params
        return service(config)
    },
    put(url,params){
        const config = {
            method: 'put',
            url:url
        }
        if(params) config.params = params
        return service(config)
    },
    delete(url,params){
        const config = {
            method: 'delete',
            url:url
        }
        if(params) config.data = params
        return service(config)
    },
    patch(url,params){
        const config = {
            method: 'patch',
            url:url
        }
        if(params) config.data = params
        return service(config)
    },
}
//导出
export default http

2、 apis.js

刚刚封装完了,现在我们演示使用一下刚才封装好的东西。

import http from './http'

// 用户登录
export function UserLogin(data) {
    return http.post('/auth/login', data)
}

// 用户注册
export function UserRegister(data) {
    return http.post('/auth/register', data)
}

这里我们构造了两个函数,并且调用刚刚封装好的http对象上对应的方法(get/post/put/delete/patch),传入数据。这样接口就封装好了,需要使用的时候直接导入对应的函数,直接调用就可以发送请求了。

/src/views/Login/index.jsx中,confirm函数直接导入了上面的两个函数,通过判断当前是登录还是注册,调用对应的函数,发送请求:

/**
 * 点击确认按钮触发
 * @returns 
*/
const confirm = async () => {
    let res = null;
    if (loginState === "Sign In") {
        res = await UserLogin(userInfo);
    } else {
        res = await UserRegister(userInfo);
    }
    res = res.data;
    if (res.code === 200) {
        console.log(res);
    } else {
        alert("请检查账号/密码是否错误!");
        return;
    }
    if (loginState === "Sign In") {
        navigate("/index");
    } else {
        alert("注册成功,请重新登录");
    }
};

这里当你真正发送请求的时候,可能会有一个关于CORS的错误,这个是同源策略,需要后端去解决,你将在下一小节看到解决方案。

二、后端会遇到的问题

1、CORS同源问题

首先就是刚刚提到的同源策略问题:

如果两个 URL 的 protocol、port (en-US) (如果有指定的话) 和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是“元组”。(“元组”是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

URL结果原因
http://store.company.com/dir2/other.html同源只有路径不同
http://store.company.com/dir/inner/another.html同源只有路径不同
https://store.company.com/secure.html失败协议不同
http://store.company.com:81/dir/etc.html失败端口不同 ( http:// 默认端口是 80)
http://news.company.com/dir/other.html失败主机不同

解决起来也很好弄:

  • 使用中间件@koa/cors:

    下载:npm install @koa/cors

    使用:在app.js中加入这样的代码:

    // 解决跨域问题
    const cors = require('@koa/cors')
    app.use(cors())
    
  • nginx配置:如果你之后使用到了一些服务器软件例如nginx,可以通过更改配置项解决,这个就请读者自己搜索查阅博客。

项目部署

至于项目部署,并不打算在这里写了,而是打算专门写在另一篇博客,分享一下我的思路和经验。

如果你对部署有要求,可以私信催更,有时候我忙着就忘记这回事儿了哈哈。

简单提一下部署时需要了解的东西:

  • 首先你要有一台服务器,大家用的应该都是云服务器
  • 你要对linux命令有一定了解和实践
  • 如果不想每次都重复操作服务器,例如每次都要手动上传代码、手动重启项目,可以看一看github action,进行自动化部署
  • 为解决每次都要在服务器上配环境,你可以选择使用docker技术
  • 需要了解一些服务器软件的配置,比如 Nginx、Apache,我更推荐 Nginx

这些东西在本篇博客的后续——项目部署中都会讲到,到时候会把链接放在这儿 __ __ __ __ (空的就是还没写) 。

我平时也会分享一些学习笔记、技术问题等东西,喜欢的可以点点关注,主要是web有关内容。

感谢拨冗翻阅拙作,敬请斧正。


http://www.niftyadmin.cn/n/29729.html

相关文章

时间显示-蓝桥杯

题目描述 小蓝要和朋友合作开发一个时间显示的网站。 在服务器上&#xff0c;朋友已经获取了当前的时间&#xff0c;用一个整数表示&#xff0c;值为从 19701970 年 11 月 11 日 00:00:0000:00:00 到当前时刻经过的毫秒数。 现在&#xff0c;小蓝要在客户端显示出这个时间。…

Python(for和while)循环嵌套及用法

Python 不仅支持 if 语句相互嵌套&#xff0c;while 和 for 循环结构也支持嵌套。所谓嵌套&#xff08;Nest&#xff09;&#xff0c;就是一条语句里面还有另一条语句&#xff0c;例如 for 里面还有 for&#xff0c;while 里面还有 while&#xff0c;甚至 while 中有 for 或者 …

AI算法(三)plt基础

目录 一、前言 二、各类图 2.1、折线图 2.2、散点图 2.3、点线图 2.4、下三角线 2.5、点虚线 2.6、虚点线 2.7、绘制自己的学习曲线 三、多线 四、画布 五、直方图 一、前言 plt是深度学习的常用库之一&#xff0c;很多指标结果如AUC、F1、ROC等都是通过plt来实现。本篇文章主…

Tkinter的Checkbutton控件

Tkinter的Checkbutton是一个有着许多选项的控件&#xff0c;可以一次选择几个选项 使用方法 var1tk.IntVar() var2tk.IntVar() def printf():if (var1.get()1)&(var2.get()0):L.config(text1)elif (var1.get()0)&(var2.get()1):L.config(text2)elif (var1.get()0)&am…

简明Java讲义 2:数据类型和运算符

目录 1、安装IDE编辑器 2、关键字和保留字 3、标识符 4、分隔符 5、数据类型 6、基本类型的数据类型转换 7、表达式类型的自动提升 8、变量 9、运算符 10、运算符的优先级 1、安装IDE编辑器 在开始内容之前&#xff0c;先下载IDE&#xff0c;可以是Eclipse或STS&…

Opencv项目实战:19 手势控制鼠标

目录 0、项目介绍 1、效果展示 2、项目搭建 3、项目代码展示 HandTrackingModule.py VirtualMouse.py 4、项目资源 5、项目总结 0、项目介绍 在Opencv项目实战&#xff1a;15 手势缩放图片中&#xff0c;我们搭建了HandTrackingModule模块&#xff0c;但在这里你还得用…

非极大值抑制(Non-Maximum Suppression)

文章目录一、什么是非极大值抑制二、为什么要用非极大值抑制三、 如何使用非极大值抑制四、代码段一、什么是非极大值抑制 非极大值抑制&#xff0c;简称为NMS算法&#xff0c;英文为Non-Maximum Suppression。其思想是搜素局部最大值&#xff0c;抑制非极大值。NMS算法在不同…

初识HTML

<!DOCTYPE html><!-- 约束,声明。 HTML5.0的声明&#xff1b;如果没有该行代码就表示是HTML4.0 --> <html lang"en"> <!-- 表示整个HTML页面的开始 也成为根标签 lang"en"英文 lang"zh-CN"中文 告诉搜索引擎该页面是英文或…