前后端分離 以及使用工具 基礎


  • 前后端分離開發

  • Yapi

  • Swagger

  • 項目部署

在項目中,前端代碼和后端代碼混合在一起,是存在問題的,存在什么問題呢?

主要存在以下幾點問題:

1). 開發人員同時負責前端和后端代碼開發,分工不明確

2). 開發效率低

3). 前后端代碼混合在一個工程中,不便於管理

4). 對開發人員要求高(既會前端,又會后端),人員招聘困難

 

為了解決上述提到的問題,現在比較主流的開發方式,就是前后端分離開發,前端人員開發前端的代碼,后端開發人員開發服務端的業務功能,分工明確,各司其職。我們本章節,就是需要將之前的項目進行優化改造,變成前后端分離開發的項目。

 

 

 

1. 前后端分離開發

1.1 介紹

前后端分離開發,就是在項目開發過程中,對於前端代碼的開發由專門的前端開發人員負責,后端代碼則由后端開發人員負責,這樣可以做到分工明確、各司其職,提高開發效率,前后端代碼並行開發,可以加快項目開發進度。

目前,前后端分離開發方式已經被越來越多的公司所采用,成為當前項目開發的主流開發方式。

前后端分離開發后,從工程結構上也會發生變化,即前后端代碼不再混合在同一個maven工程中,而是分為 前端工程后端工程

 

 

 前后端分離之后,不僅工程結構變化,后期項目上線部署時,與之前也不同:

2). 現在: 拆分為前后端分離的項目后,最終部署時,后端工程會打成一個jar包,運行在Tomcat中(springboot內嵌的tomcat)。前端工程的靜態資源,會直接部署在Nginx中進行訪問。

 

1.2 開發流程

前后端分離開發后,面臨一個問題,就是前端開發人員和后端開發人員如何進行配合來共同開發一個項目?可以按照如下流程進行

 

 

 

 

1). 定制接口: 這里所說的接口不是我們之前在service, mapper層定義的interface; 這里的接口(API接口)就是一個http的請求地址,主要就是去定義:請求路徑、請求方式、請求參數、響應數據等內容。(具體接口文檔描述的信息, 如上圖)

2). 前后端並行開發: 依據定義好的接口信息,前端人員開發前端的代碼,服務端人員開發服務端的接口; 在開發中前后端都需要進行測試,后端需要通過對應的工具來進行接口的測試,前端需要根據接口定義的參數進行Mock數據模擬測試。

3). 聯調: 當前后端都開發完畢並且自測通過之后,就可以進行前后端的聯調測試了,在這一階段主要就是校驗接口的參數格式。

4). 提測: 前后端聯調測試通過之后,就可以將項目部署到測試服務器,進行自動化測試了。

1.3 前端技術棧

1). 開發工具

Visual Studio Code (簡稱VsCode)

Hbuilder

 

2). 技術框架

A. Node.js: Node.js 是一個基於 Chrome V8 引擎的 JavaScript 運行環境。(類似於java語言中的JDK)。

B. Vue : 目前最火的的一個前端javaScript框架。

C. ElementUI: 一套為開發者、設計師和產品經理准備的基於 Vue 2.0 的桌面端組件庫,通過ElementUI組件可以快速構建項目頁面。

D. Mock: 生成隨機數據,攔截 Ajax 請求,前端可以借助於Mock生成測試數據進行功能測試。

E. Webpack: webpack 是一個現代 JavaScript 應用程序的模塊打包器(module bundler),分析你的項目結構,找到JavaScript模塊以及其它的一些瀏覽器不能直接運行的拓展語言(Sass,TypeScript等),並將其轉換和打包為合適的格式供瀏覽器使用。

2. Yapi

2.1 介紹

 

 

 

YApi 是高效、易用、功能強大的 api 管理平台,旨在為開發、產品、測試人員提供更優雅的接口管理服務。可以幫助開發者輕松創建、發布、維護 API,YApi 還為用戶提供了優秀的交互體驗,開發人員只需利用平台提供的接口數據寫入工具以及簡單的點擊操作就可以實現接口的管理。

YApi讓接口開發更簡單高效,讓接口的管理更具可讀性、可維護性,讓團隊協作更合理。

 

源碼地址: https://github.com/YMFE/yapi

官方文檔: https://hellosean1025.github.io/yapi/

 

要使用YApi,項目組需要自己進行部署,在本項目中我們可以使用課程提供的平台進行測試,域名: https://mock-java.itheima.net/

2.2 使用

2.2.1 准備

注冊賬號,登錄平台

2.2.2 定義接口

登錄到Yapi平台之后,我們可以創建項目,在項目下創建接口分類,在對應的分類中添加接口。

1). 創建項目

 

 

 

 

 

 

2). 添加分類

在當前項目中,有針對於員工、菜品、套餐、訂單的操作,我們在進行接口維護時,可以針對接口進行分類,如果沒有對應的分類,我們自己添加分類。

 

 

 接口基本信息錄入之后,添加提交,就可以看到該接口的基本信息:

但是目前,接口中我們並未指定請求參數,響應數據等信息,我們可以進一步點擊編輯,對該接口 詳情進行編輯處理。

 

 

 

 

 

 

4). 運行接口

Yapi也提供了接口測試功能,當我們接口編輯完畢后,后端服務的代碼開發完畢,啟動服務,就可以使用Yapi進行接口測試了。

 

 

 

 

注意:如果有些接口,是需要登錄后才可以訪問的,所以在測試該接口時,需要先請登錄接口,登錄完成后,再訪問該接口

在Yapi平台中,將接口文檔定義好了之后,前后端開發人員就需要根據接口文檔中關於接口的描述進行前端和后端功能的開發。

2.2.3 導出接口文檔

在Yapi平台中我們不僅可以在線閱讀文檔,還可以將Yapi中維護的文檔直接導出來,可以導出md,json,html格式,在導出時自行選擇即可 。

 

 

 而在導出的html文件或md文件中,主要描述的就是接口的基本信息, 包括: 請求路徑、請求方式、接口描述、請求參數、返回數據等信息。展示形式如下:

 

 

 

2.2.4 導入接口文檔

上述我們講解了接口文檔的導出,我們也可以將外部的接口文檔導入到Yapi的平台中,這樣我們就不用一個接口一個接口的添加了。我們可以將自己准備好的

的json格式的接口文檔直接導入Yapi平台中來。

 

 

 

導入過程中出現的確認彈窗,選擇"確認"

 

 

 導入成功之后,我們就可以在Yapi平台查看到已導入的接口。

 

 

 

3. Swagger

3.1 介紹

官網:https://swagger.io/

 

 

 

Swagger 是一個規范和完整的框架,用於生成、描述、調用和可視化 RESTful 風格的 Web 服務。功能主要包含以下幾點:

A. 使得前后端分離開發更加方便,有利於團隊協作

B. 接口文檔在線自動生成,降低后端開發人員編寫接口文檔的負擔

C. 接口功能測試

使用Swagger只需要按照它的規范去定義接口及接口相關的信息,再通過Swagger衍生出來的一系列項目和工具,就可以做到生成各種格式的接口文檔,以及在線接口調試頁面等等。

 

直接使用Swagger, 需要按照Swagger的規范定義接口, 實際上就是編寫Json文件,編寫起來比較繁瑣、並不方便, 。而在項目中使用,我們一般會選擇一些現成的框架來簡化文檔的編寫,而這些框架是基於Swagger的,如knife4j。knife4j是為Java MVC框架集成Swagger生成Api文檔的增強解決方案。而我們要使用kinfe4j,需要在pom.xml中引入如下依賴即可:

 

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.2</version>
</dependency>

3.2 使用方式

接下來,我們就將我們的項目集成Knife4j,來自動生成接口文檔。這里我們還是需要再創建一個新的分支v1.2,在該分支中進行knife4j的集成,集成測試完畢之后,沒有問題,我們再將v1.2分支合並到master。

使用knife4j,主要需要操作以下幾步:

1). 導入knife4j的maven坐標   🔪

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.2</version>
</dependency>

2). 導入knife4j相關配置類

這里我們就不需要再創建一個新的配置類了,我們直接在WebMvcConfig配置類中聲明即可。(或者新寫一個配置類。加上@Configration 注解為配置類即可)

A. 在該配置類中加上兩個注解 @EnableSwagger2 @EnableKnife4j ,開啟Swagger和Knife4j的功能。

B. 在配置類中聲明一個Docket類型的bean, 通過該bean來指定生成文檔的信息

@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
    
    /**
     * 設置靜態資源映射
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("開始進行靜態資源映射...");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }
    
    /**
     * 擴展mvc框架的消息轉換器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("擴展消息轉換器...");
        //創建消息轉換器對象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //設置對象轉換器,底層使用Jackson將Java對象轉為json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //將上面的消息轉換器對象追加到mvc框架的轉換器集合中
        converters.add(0,messageConverter);
    }
    
    @Bean
    public Docket createRestApi() {
        // 文檔類型
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("controller路徑"))  //前端請求由controller層處理所以這里掃描
                .paths(PathSelectors.any())
                .build();
    }
    
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("項目名稱")
                .version("1.0")
                .description("項目名稱接口文檔")
                .build();
    }
}

注意: Docket聲明時,指定的有一個包掃描的路徑,該路徑指定的是Controller所在包的路徑。因為Swagger在生成接口文檔時,就是根據這里指定的包路徑,自動的掃描該包下的@Controller, @RestController, @RequestMapping等SpringMVC的注解,依據這些注解來生成對應的接口文檔。

 

3). 設置靜態資源映射

由於Swagger生成的在線文檔中,涉及到很多靜態資源,這些靜態資源需要添加靜態資源映射,否則接口文檔頁面無法訪問。因此需要在 WebMvcConfig類中的addResourceHandlers方法中增加如下配置。

 

registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");

 

 

4). 在LoginCheckFilter中設置不需要處理的請求路徑   (登錄資源攔截器)

需要將Swagger及Knife4j相關的靜態資源直接放行,無需登錄即可訪問,否則我們就需要登錄之后,才可以訪問接口文檔的頁面。

在原有的不需要處理的請求路徑中,再增加如下鏈接:

"/doc.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs"

 

 

 

3.3 查看接口文檔

經過上面的集成配置之后,我們的項目集成Swagger及Knife4j就已經完成了,接下來我們可以重新啟動項目,訪問接口文檔,訪問鏈接為: http://localhost:8080/doc.html

  http://localhost:8080/doc.html

 

注意: 由於我們服務端的Controller中的業務增刪改查的方法,都是必須登錄之后才可以訪問的,所以,我們在測試時候,也是需要先訪問登錄接口。登錄完成之后,我們可以再訪問其他接口進行測試。

 

 

 

 

注意: 由於我們服務端的Controller中的業務增刪改查的方法,都是必須登錄之后才可以訪問的,所以,我們在測試時候,也是需要先訪問登錄接口。登錄完成之后,我們可以再訪問其他接口進行測試。

 

我們不僅可以在瀏覽器瀏覽生成的接口文檔,Knife4j還支持離線文檔,對接口文檔進行下載,支持下載的格式有:markdown、html、word、openApi。

下面的這些是我們項目的數據模型對象,swagger 掃描的我們的Controller類,這些類中方法的返回值,swagger認為是我們的·模型對象

 

 

3.4 常用注解

3.4.1 問題說明

在上面我們直接訪問Knife4j的接口文檔頁面,可以查看到所有的接口文檔信息,但是我們發現,這些接口文檔分類及接口描述都是Controller的類名(駝峰命名轉換而來)及方法名,而且在接口文檔中,所有的請求參數,響應數據,都沒有中文的描述,並不知道里面參數的含義,接口文檔的可讀性很差。

 

 

3.4.2 注解介紹

為了解決上述的問題,Swagger提供了很多的注解,通過這些注解,我們可以更好更清晰的描述我們的接口,包含接口的請求參數、響應數據、數據模型等。核心的注解,主要包含以下幾個

注解 位置 說明
@Api 加載Controller類上,表示對類的說明
@ApiModel 類(通常是實體類) 描述實體類的作用
@ApiModelProperty 屬性 描述實體類的屬性
@ApiOperation 方法 說明方法的用途、作用
@ApiImplicitParams 方法 表示一組參數說明
@ApiImplicitParam 方法 用在@ApiImplicitParams注解中,指定一個請求參數的各個方面的屬性

3.4.3 注解測試

1). 實體類

可以通過 @ApiModel , @ApiModelProperty 來描述實體類及屬性

/**
 * 地址簿
 */
@ApiModel("用戶信息")
@Data
public class AddressBook implements Serializable {

    private static final long serialVersionUID = 1L;

    //id 主鍵
    @ApiModelProperty("主鍵id")
    private Long id;


    //用戶id
    @ApiModelProperty("用戶id")
    private Long userId;


    //收貨人
    @ApiModelProperty("收貨人")
    private String consignee;


    //手機號
    @ApiModelProperty("手機號")
    private String phone;


    //性別 0 女 1 男
    @ApiModelProperty("性別 0 女 1 男")
    private String sex;


    //省級區划編號
    @ApiModelProperty("省級區划編號")
    private String provinceCode;


    //省級名稱
    @ApiModelProperty("省級名稱")
    private String provinceName;


    //市級區划編號
    @ApiModelProperty("市級區划編號")
    private String cityCode;


    //市級名稱
    @ApiModelProperty("市級名稱")
    private String cityName;


    //區級區划編號
    @ApiModelProperty("區級區划編號")
    private String districtCode;


    //區級名稱
    @ApiModelProperty("區級名稱")
    private String districtName;


    //詳細地址
    @ApiModelProperty("詳細地址")
    private String detail;


    //標簽
    @ApiModelProperty("標簽")
    private String label;

    //是否默認 0 否 1是
    @ApiModelProperty("是否默認 0 否 1是")
    private Integer isDefault;

    //創建時間
    @ApiModelProperty("創建時間")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    //更新時間
    @ApiModelProperty("更新時間")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


    //創建人
    @ApiModelProperty("創建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;


    //修改人
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;


    //是否刪除
    @ApiModelProperty("是否刪除")
    private Integer isDeleted;
}

2). 響應實體R

@Data
@ApiModel("返回結果")
public class R<T> {
    @ApiModelProperty("編碼")
    private Integer code; //編碼:1成功,0和其它數字為失敗
    @ApiModelProperty("錯誤信息")
    private String msg; //錯誤信息
    @ApiModelProperty("數據")
    private T data; //數據
     @ApiModelProperty("動態數據")
    private Map map = new HashMap(); //動態數據
    //省略靜態方法
    

}

3). Controller類及其中的方法

描述Controller、方法及其方法參數,可以通過注解: @Api, @APIOperation, @ApiImplicitParams, @ApiImplicitParam

 

@RestController
@RequestMapping("/setmeal")
@Slf4j
@Api(tags = "套餐相關接口")
public class SetmealController {

    @Autowired
    private SetmealService setmealService;
    @Autowired
    private CategoryService categoryService;
    @Autowired
    private SetmealDishService setmealDishService;

    /**
     * 新增套餐
     * @param setmealDto
     * @return
     */
    @PostMapping
    @CacheEvict(value = "setmealCache",allEntries = true)
    @ApiOperation(value = "新增套餐接口")
    public R<String> save(@RequestBody SetmealDto setmealDto){
        log.info("套餐信息:{}",setmealDto);

        setmealService.saveWithDish(setmealDto);

        return R.success("新增套餐成功");
    }

    /**
     * 套餐分頁查詢
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    @ApiOperation(value = "套餐分頁查詢接口")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "page",value = "頁碼",required = true),
            @ApiImplicitParam(name = "pageSize",value = "每頁記錄數",required = true),
            @ApiImplicitParam(name = "name",value = "套餐名稱",required = false)
    })
    public R<Page> page(int page,int pageSize,String name){
        //分頁構造器對象
        Page<Setmeal> pageInfo = new Page<>(page,pageSize);
        Page<SetmealDto> dtoPage = new Page<>();

        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        //添加查詢條件,根據name進行like模糊查詢
        queryWrapper.like(name != null,Setmeal::getName,name);
        //添加排序條件,根據更新時間降序排列
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);

        setmealService.page(pageInfo,queryWrapper);

        //對象拷貝
        BeanUtils.copyProperties(pageInfo,dtoPage,"records");
        List<Setmeal> records = pageInfo.getRecords();

        List<SetmealDto> list = records.stream().map((item) -> {
            SetmealDto setmealDto = new SetmealDto();
            //對象拷貝
            BeanUtils.copyProperties(item,setmealDto);
            //分類id
            Long categoryId = item.getCategoryId();
            //根據分類id查詢分類對象
            Category category = categoryService.getById(categoryId);
            if(category != null){
                //分類名稱
                String categoryName = category.getName();
                setmealDto.setCategoryName(categoryName);
            }
            return setmealDto;
        }).collect(Collectors.toList());

        dtoPage.setRecords(list);
        return R.success(dtoPage);
    }

    /**
     * 刪除套餐
     * @param ids
     * @return
     */
    @DeleteMapping
    @CacheEvict(value = "setmealCache",allEntries = true)
    @ApiOperation(value = "套餐刪除接口")
    public R<String> delete(@RequestParam List<Long> ids){
        log.info("ids:{}",ids);

        setmealService.removeWithDish(ids);

        return R.success("套餐數據刪除成功");
    }

    /**
     * 根據條件查詢套餐數據
     * @param setmeal
     * @return
     */
    @GetMapping("/list")
    @Cacheable(value = "setmealCache",key = "#setmeal.categoryId + '_' + #setmeal.status")
    @ApiOperation(value = "套餐條件查詢接口")
    public R<List<Setmeal>> list(Setmeal setmeal){
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
        queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);

        List<Setmeal> list = setmealService.list(queryWrapper);

        return R.success(list);
    }
}

4). 重啟服務測試

我們上述通過Swagger的注解,對實體類及實體類中的屬性,以及Controller和Controller的方法進行描述,接下來,我們重新啟動服務,然后看一下自動生成的接口文檔有何變化。

 

 

在接口文檔的頁面中,我們可以看到接口的中文描述,清晰的看到每一個接口是做什么的,接口方法參數什么含義,參數是否是必填的,響應結果的參數是什么含義等,都可以清楚的描述出來。

總之,我們要想清晰的描述一個接口,就需要借助於Swagger給我們提供的注解

最后將我們做修改的分支,提交推送到gitee上,然后合並到主分支,就可以了

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM