測試開發【Mock平台】04實戰:前後端項目初始化與登錄鑒權實現

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: '//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 具體配置後邊在新增頁面的時候再單獨講解,另外還有菜單底部還有內部鏈接,其實是集成了一個文檔工具(//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 '//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/** -> //preview.pro.ant.design/api/**
    '/api/': {
      // 要代理的地址
+      target: '//localhost:8081',
-     // target: '//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分支提交。