測試開發【Mock平臺】04實戰:前後端專案初始化與登入鑑權實現

MrZ大奇發表於2022-04-06

image

【Mock平臺】為系列測試開發教程,從0到1編碼帶你一步步使用Spring Boot 和 Antd React
框架完成搭建一個測試工具平臺,希望作為一個實戰專案能為你的測試開發學習有幫助。

一、後端SpringBoot

參考之前《Mock平臺2-Java Spring Boot框架基礎知識》分享來建立後端的服務,實際上QMock服務會涉及到兩個服務,一個是供前端頁面用的API服務,另一個是mock請求響應服務即可叫其閘道器,為了統一管理程式碼又不都耦合到一塊,本專案通過IDE先建立一個普通的JAVA專案叫 QMockService,然後再其專案中建立兩個**Module Springboot **專案,服務名分別為:

  • qmock-service-api
  • qmock-servcie-gateway

由於第二個代理閘道器服務暫時用不到,所以你也可以只單獨建立一個service-api專案用於實踐學習。
在這裡插入圖片描述

API服務架構

通常一個專案都會有約定俗成的模式來規範開發,由於對於JAVA的模式太多,我這裡只提供基於MVC模式擴充套件的我常用結構供參考, 詳見qmock-service-api左側在 main.java包下子目錄。

java
|- cn.daqi.mock.api  # 程式碼包
   |- commmons       # 通用或工具類
   |- controller     # 介面請求入口
   |- entity         # 資料表實體類
      |- request     # 介面請求實體
   |- mapper         # 資料操作介面類
   |- service        # 服務介面類
      |- impl        # 服務介面實現類
resources
|- mapper            # Mybatis XML方式資料操作檔案
|- application.yml   # 專案配置採用yml方式

由於筆者的職業不是後端開發,對於程式碼架構和模式等,沒有過度的實戰經驗,如果想對各種模式有更多的瞭解推薦閱讀之前轉載過美團技術的一篇文章,如果想更深入的瞭解建議買本架構、程式碼之道之類的書進行系統學習。

依賴新增

Spring專案之前講過有很多外掛幫助其快速的開發,QMock專案本篇實現還依賴依賴以下幾個專案,請在**pom.xml** 進行新增並重新整理安裝依賴,具體的使用和對比後在後邊具體功能實現中逐漸講解。

<!-- mysql連結驅動 -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- MyBatis 一款優秀的資料庫持久層操作框架 -->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.2.0</version>
</dependency>

<!-- 幫助簡化Bean getter/setter等實現的外掛 -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
</dependency>

<!-- JSON操作類庫 -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.79</version>
</dependency>

統一返回

Api服務一般都會有公司或專案內部約定好的模版統一返回方便聯調開發,QMock專案就直接參考Antd pro一個官方建議來實現,省得做一些額外的自定義配置轉換。
在這裡插入圖片描述
所以在commons下建立了統一返回類和一個列舉類,實現上述統一返回。

1.enum RespCode

public enum RespCode {

    /**
     * 預設成功和系統狀態
     * 提示型別: 0 靜默silent; 1 警告message.warn; 2 錯誤message.error; 4 訊息notification; 9 跳轉page
     * */
    SUCCESS(true, 2000, "成功", 0),
    SYSTEM_ERROR(false, 5000, "系統繁忙,請稍後重試", 2),

    /* 引數錯誤 1001~1999 */
    PARAMS_WARNING(false, 1001, "引數缺失或為空", 2),

    /* 使用者錯誤 2001~2999 */
    USER_AUTHORITY_FAILURE(false, 2001, "使用者名稱或密碼錯誤", 2);

    /* 其他錯誤 3001~3999 */

    private Boolean success;
    private Integer errorCode;
    private String errorMessage;
    private Integer showType;

    RespCode (Boolean success, Integer errorCode, String errorMessage, Integer showType) {
        this.success = success;
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
        this.showType = showType;
    }

    public Boolean success() {
        return this.success;
    }

    public Integer errorCode() {
        return this.errorCode;
    }

    public String errorMessage() {
        return this.errorMessage;
    }

    public Integer showType() {
        return this.showType;
    }
}

2.class RespResult

@Data
public class RespResult implements Serializable {

    private static final long serialVersionUID = 1L;

    // 請求是否成功 true / false
    private Boolean success;

    // 實際返回的資料
    private Object data;

    // 錯誤編碼
    private Integer errorCode;

    // 錯誤資訊
    private String errorMessage;

    // 提示型別: 0 silent; 1 message.warn; 2 message.error; 4 notification; 9 page
    private Integer showType = 0;

    // 列舉通用賦值方法
    public void setResultCode(RespCode respCode){
        this.success = respCode.success();
        this.errorCode = respCode.errorCode();
        this.errorMessage = respCode.errorMessage();
        this.showType = respCode.showType();
    }

    // 預設響應成功
    public static RespResult success() {
        RespResult respResult = new RespResult();
        respResult.setResultCode(RespCode.SUCCESS);
        return respResult;
    }

    // 帶返回data響應成功
    public static RespResult success(Object data) {
        RespResult respResult = new RespResult();
        respResult.setResultCode(RespCode.SUCCESS);
        respResult.setData(data);
        return respResult;
    }

    // 根據RespCode列舉失敗返回
    public static RespResult failure(RespCode respCode){
        RespResult respResult = new RespResult();
        respResult.setResultCode(respCode);
        return respResult;
    }
}

這小結最後簡單畫個流轉圖瞭解下後端SpringBoot實現API服務過程,具體例子將在最後登入功能中實踐。

二、前端Antd pro

使用 uim 建立 ant-desgin-pro 腳手架,具體的模版已經在《Mock平臺3-初識Antd React 開箱即用中臺前端框架》講過不再贅述。

專案建立

這裡直接給出我的QMockWeb專案建立過程,其中如果你TypeScript比較熟悉,從體驗的各方面還是比較推薦的,由於筆者不熟也為了降低門檻本Mock專案繼續採用JavaScript,另外原始碼專案已上傳到了GitHub上,也可直接Fork使用。
在這裡插入圖片描述
Tips:不要忘記執行命令 npm run start 電腦上執行看下專案是否正常執行。

精簡優化

雖然用的是simple模版,但有些內容對於專案可能是用不到的,以及一些基礎資訊需變更,才能打造一個屬於自己的專案,對於Mock平臺包含但不限於如下變更。

國際化多語言

預設的腳手架中有八種多語言,Mock專案只需要保留簡體中文zh-CN英文en-US作為後續的多語言使用演示使用,多餘的去除方法很簡單直接刪除位於** src/locales/* 下對應的資料夾和js即可。另外一點是可以在config/config.js** 中配置預設語言,如果想刪除 pro 自帶的全球化,可以通過 npm run i18n-remove 命令徹底移除。
在這裡插入圖片描述

頁頭尾和載入

專案中還涉及到預設ICON、標題以及一些宣告需要改造,這些可以通過 Find in Files 進行關鍵詞進行更改,這裡我直接羅列給出,自行按需進行修改。

在腳手架專案中實際中通過 config\defaultSettings.ts 來控制標題和 Logo,本項暫時沒logo所以直接賦值為False不顯示。

const Settings = {
  ...
  colorWeak: false,
-  // title: 'Ant Design Pro',
+  title: 'QMock',
  pwa: false,
  // logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
-  logo: false,
+  iconfontUrl: '',
};
export default Settings;

載入頁

專案中還有一個在 js 還沒載入成功,但是 html 已經載入成功的 landing 頁面。這個頁面的配置存在於 src\pages\document.ejs 檔案。其中涉及到的專案圖表引用位於**/public/***目錄下,其他文案、線上靜態資源可根據專案情況配置,QMock修改的效果可通過原始碼執行檢視。

底部宣告

頁面佈局底角會有個宣告之類的,專案中也需要改下此檔案位於 src/components/Footer/index.jsx 公共元件中,因為上邊保留了國際化功能,所以還需要在en-US.jszh-CN.js修改 app.copyright.produced 的值。

登入頁和選單

頁面登入視窗也涉及到如手機登入Tab、更多登入、標題、副標題等暫時不需要,這些更改的地方位於src/pages/user/Login/index.jsx 和國際化各語言資料夾下 pages.js ,這部分暫時註釋掉為了方便後邊實現統一登入的例子時候再用到。
在這裡插入圖片描述
最後精簡優化的部分就是選單了,對應動態選單需要修改 config/rotues.js 具體配置後邊在新增頁面的時候再單獨講解,另外還有選單底部還有內部連結,其實是整合了一個文件工具(https://d.umijs.org/zh-CN),在開發環境下會展示,主要方便使用文件相關的記錄,相當於一個內部Wiki,個人覺得還比較有用,QMock後邊的一些相關說明資訊也打算放在這裡。
在這裡插入圖片描述
經過精簡優化後看下最終效果
在這裡插入圖片描述

三、登入功能實現

上邊說了很多基礎配置相關的,接下來個實戰打通前後端服務,實現登入功能。

說明:以下實現主要照著做即可,不用勉強看懂每個實現,後續的分享具體應用到會逐一的講,如果太過在意會打擊學習的積極性,當然如果後邊沒有講到或者不夠清晰也歡迎加互相探討。

使用者表建立

資料庫使用的是Mysql5.7+版本,本專案建立名為qmock的資料庫,並建立一個users的使用者表,同時新增兩條資料,SQL語句如下:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT '',
  `password` varchar(50) DEFAULT '',
  `access` varchar(20) DEFAULT 'gust',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES (1, 'admin', 'admin', 'admin');
INSERT INTO `users` VALUES (2, 'user', 'user', 'user');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

登入介面

還記得一開始我們新增一些依賴嗎?如果要實現Mybtis的資料庫操作還需要在application.yml 增加一些配置。

server:
port: 8081 # 服務啟動埠

# 資料庫連結資訊
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/qmock
username: mrzcode
password: mrzcode
driver-class-name: com.mysql.jdbc.Driver

# Mybatis基本配置
mybatis:
type-aliases-package: cn.daqi.mock.api.entity # 指定實體類所在包
mapper-locations: classpath:mapper/*.xml # 指定mapper xml 所在位置
configuration:
    map-underscore-to-camel-case: true  # 資料庫表欄位自動轉駝峰命名 如:user_name -> userName

抓取 Antd pro登入時候請求mock介面的路徑和引數

curl 'http://localhost:8000/api/login/account' \
  --data-raw '{"username":"admin","password":"admin","autoLogin":true,"type":"account"}' 

根據此文開頭給出的API請求流程圖實現每一個對應的類,這裡從裡層往外層逐步給出程式碼

(一)根據使用者名稱和密碼匹配查詢 註解為 @Mapper

package cn.daqi.mock.api.mapper;

import org.apache.ibatis.annotations.*;

@Mapper
public interface LoginMapper {

    @Select("SELECT count(*) FROM users WHERE name=#{name} and `password`=#{password}")
    Integer userLogin(@Param("name") String name, @Param("password") String password);
}

(二)定義請求引數 LoginRequest.java 請求引數類 lombok @Data 註解,其中SQL只判斷是否查詢到使用者,所以暫時用不到 LoginEntity.java 這裡便不羅列了。

package cn.daqi.mock.api.entity.requests;

import lombok.Data;

@Data
public class LoginRequest {
    private String username;
    private String password;
}

(三)登入服務Interface和class實現類

package cn.daqi.mock.api.service;
// ...省略import,自動新增或詳細看原始碼
public interface LoginService {
    RespResult accountLogin(LoginRequest req);
}

註解@Service 放在實現類上

package cn.daqi.mock.api.service.impl;
// ...省略import,自動新增或詳細看原始碼
@Service
public class LoginImpl implements LoginService {

    @Autowired
    LoginMapper loginMapper;

    @Override
    public RespResult accountLogin(LoginRequest req) {
        Integer count= loginMapper.userLogin(req.getUsername(), req.getPassword());
        if (count > 0) {
            return RespResult.success();
        } else {
            return RespResult.failure(RespCode.USER_AUTHORITY_FAILURE);
        }
    }
}

(四)登入API實現類 註解說明

  • @RestController 宣告為控制器(= @Controller + @ResponseBody)
  • @RequestMapping 定義跟路徑
  • @PostMapping 定義POST請求方法和子路徑
package cn.daqi.mock.api.controller;

import cn.daqi.mock.api.commons.RespResult;
import cn.daqi.mock.api.entity.requests.LoginRequest;
import cn.daqi.mock.api.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ Author: Zhang Qi
 * @ Copyright: 部落格&公眾號《大奇測試開發》
 * @ Describe: 登入介面API
 */
@RestController
@RequestMapping(value = "/api/login")
public class LoginController {

    @Autowired
    LoginService loginService;

    /**
     * 登入驗證介面
     * @param req Post請求body引數體
     * @return JSON統一格式體
     */
    @PostMapping(value = "/account")
    public RespResult login(@RequestBody LoginRequest req){
        return loginService.accountLogin(req);
    }
}

(五)介面測試
執行服務分別用存在和不匹配使用者密碼進行下介面請求測試
在這裡插入圖片描述

登入頁面

完成了後端的使用者鑑權介面,現在來改造下前端的登入,使其此介面從mock請求切換到真正的後端請求,涉及以下幾個處。

(一) 代理轉發 同上個vue系列一樣,前後端的分離專案為了解決跨域的問題都需要配置下proxy,專案使其轉發指向本地的qmock-service-api後端服務,修改的檔案為 config/proxy.js

dev: {
    // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
    '/api/': {
      // 要代理的地址
+      target: 'http://localhost:8081',
-     // target: 'https://preview.pro.ant.design',
      // 配置了這個可以從 http 代理到 https
      // 依賴 origin 的功能可能需要這個,比如 cookie
      changeOrigin: true,
    },
  },

(二)去掉前端Mock登入配置 位於mock/user.js 註釋或者刪除掉整塊mock介面定義

'POST /api/login/account': async (req, res) => {
  // ...省略內部程式碼
}

由於我們只是替換了一個登入介面,其他如使用者資訊等沒有實現,依然走的是mock,所以antd這裡會坑需要同步注意修改!

GET /api/currentUser 這個Mock方法需要註釋或刪除掉 if (!getAccess()) { ...省略... }
部分程式碼,否則會驗證鑑權失敗,大家可以開啟debug對比試試。

(三)修改/account 請求 預設登入介面和後端的統一介面格式不一樣,這裡需要稍微修改前端對其介面的邏輯判斷,登入頁面檔案位於 src/pages/user/Login/index.jsx

const handleSubmit = async (values) => {
    try {
      // 登入
      const msg = await login({ ...values, type });

+      if (msg.success) {
-      // if (msg.stutus) {
        const defaultLoginSuccessMessage = intl.formatMessage({
          id: 'pages.login.success',
          defaultMessage: '登入成功!',
        });
        message.success(defaultLoginSuccessMessage);
        await fetchUserInfo();
        /** 此方法會跳轉到 redirect 引數所在的位置 */

        if (!history) return;
        const { query } = history.location;
        const { redirect } = query;
        history.push(redirect || '/');
        return;
      }

      console.log(msg); // 如果失敗去設定使用者錯誤資訊

+      setUserLoginState({ status: 'error', type:'account' });
-     // setUserLoginState(msg)
    } catch (error) {
      const defaultLoginFailureMessage = intl.formatMessage({
        id: 'pages.login.failure',
        defaultMessage: '登入失敗,請重試!',
      });
      message.error(defaultLoginFailureMessage);
    }
  };

以上如果全部順利弄完,重新啟動前後端服務來聯調看下效果吧,如圖登入的介面正確請求了真實的介面。
在這裡插入圖片描述

本次分享內容稍微有點多,時間也拖的有點久,主要是一些內容筆者在給大家實戰中也有學習成本和各種問題,好在功夫不負有心人,希望通過的我的前期天坑能讓大家在學習少一些彎路。

最後筆者在學習Antd中有一點體會是,React確實比Vue入門使用要複雜些,但花了兩天時間看了下官方文件後更加覺得React和Antdpro在支援平臺全棧開發更能有好多表現。後邊也打算隨著我自己掌握技能的深入,然後出一個從測試開發角度理解的React基礎教程,這樣對於用好Antd更事半功倍。

本次程式碼已同步更新到GitHub上,有需要關注並回復 “mock平臺”獲取,同時本次內容作為一個模版單獨打了temple分支,方便大家參考或直接使用,但後續的功能實現都會正常以master分支提交。

相關文章