后台管理前端


1.搭建后台管理前端

1.1.導入已有資源

后台項目相對復雜,為了有利於教學,我們不再從0搭建項目,而是直接使用課前資料中給大家准備好的源碼:

1530555871804

我們解壓縮,放到工作目錄中:

1530367369490

然后在Intellij idea中導入新的工程:

1530367589197

選中我們的工程:

1530367781173

這正是一個用vue-cli構建的webpack工程,是不是與昨天的一樣:

1530368191250

1.2.安裝依賴

你應該注意到,這里並沒有node_modules文件夾,方便給大家下發,已經把依賴都刪除了。不過package.json中依然定義了我們所需的一切依賴:

1530368695265

我們只需要打開終端,進入項目目錄,輸入:npm install命令,即可安裝這些依賴。

1530374769782

大概需要幾分鍾。

如果安裝過程出現以下問題:

1530374827792

建議刪除node_modules目錄,重新安裝。

1.3.運行一下看看

輸入命令:npm run dev

1530374954209

發現默認的端口是9001。訪問:http://localhost:9001

會自動進行跳轉:

1525958950616

2.Vuetify框架

2.1.為什么要學習UI框架

Vue雖然會幫我們進行視圖的渲染,但樣式還是由我們自己來完成。這顯然不是我們的強項,因此后端開發人員一般都喜歡使用一些現成的UI組件,拿來即用,常見的例如:

  • BootStrap
  • LayUI
  • EasyUI
  • ZUI

然而這些UI組件的基因天生與Vue不合,因為他們更多的是利用DOM操作,借助於jQuery實現,而不是MVVM的思想。

而目前與Vue吻合的UI框架也非常的多,國內比較知名的如:

  • element-ui:餓了么出品
  • i-view:某公司出品

然而我們都不用,我們今天推薦的是一款國外的框架:Vuetify

官方網站:https://vuetifyjs.com/zh-Hans/

1525960652724

2.2.為什么是Vuetify

有中國的為什么還要用外國的?原因如下:

  • Vuetify幾乎不需要任何CSS代碼,而element-ui許多布局樣式需要我們來編寫
  • Vuetify從底層構建起來的語義化組件。簡單易學,容易記住。
  • Vuetify基於Material Design(谷歌推出的多平台設計規范),更加美觀,動畫效果酷炫,且風格統一

這是官網的說明:

1530555978248

缺陷:

  • 目前官網雖然有中文文檔,但因為翻譯問題,幾乎不太能看。

2.3.怎么用?

基於官方網站的文檔進行學習:

1525960312939

我們重點關注UI components即可,里面有大量的UI組件,我們要用的時候再查看,不用現在學習,先看下有什么:

1525961862771

1525961875288

以后用到什么組件,就來查詢即可。

3.項目結構

開始編碼前,我們先了解下項目的結構。

3.1.目錄結構

首先是目錄結構圖:1525962755237

3.2.調用關系

我們最主要理清index.html、main.js、App.vue之間的關系:

1525964023585

理一下:

  • index.html:html模板文件。定義了空的div,其id為app
  • main.js:實例化vue對象,並且綁定通過id選擇器,綁定到index.html的div中,因此main.js的內容都將在index.html的div中顯示。main.js中使用了App組件,即App.vue,也就是說index.html中最終展現的是App.vue中的內容。index.html引用它之后,就擁有了vue的內容(包括組件、樣式等),所以,main.js也是webpack打包的入口。
  • index.js:定義請求路徑和組件的映射關系。相當於之前的<vue-router>
  • App.vue中也沒有內容,而是定義了vue-router的錨點:<router-view>,我們之前講過,vue-router路由后的組件將會在錨點展示。
  • 最終結論:一切路由后的內容都將通過App.vue在index.html中顯示。
  • 訪問流程:用戶在瀏覽器輸入路徑,例如:http://localhost:9001/#/item/brand --> index.js(/item/brand路徑對應pages/item/Brand.vue組件) --> 該組件顯示在App.vue的錨點位置 --> main.js使用了App.vue組件,並把該組件渲染在index.html文件中(id為“app”的div中)

3.3.頁面布局

接下來我們一起看下頁面布局。

Layout組件是我們的整個頁面的布局組件:

1530380040278

一個典型的三塊布局。包含左,上,中三部分:

1525965779366

里面使用了Vuetify中的2個組件和一個布局元素:

  • v-navigation-drawer :導航抽屜,主要用於容納應用程序中的頁面的導航鏈接。

    ![1530380237867](assets/1530380237867.png)
    
  • v-toolbar :工具欄通常是網站導航的主要途徑。可以與導航抽屜一起很好地工作,動態選擇是否打開導航抽屜,實現可伸縮的側邊欄。

    1530380292558

  • v-content:並不是一個組件,而是標記頁面布局的元素。可以根據您指定的app組件的結構動態調整大小,使得您可以創建高度可定制的組件。

那么問題來了:v-content中的內容來自哪里?

1525966180568

  • Layout映射的路徑是/
  • 除了Login以為的所有組件,都是定義在Layout的children屬性,並且路徑都是/的下面
  • 因此當路由到子組件時,會在Layout中定義的錨點中顯示。
  • 並且Layout中的其它部分不會變化,這就實現了布局的共享。

4.使用域名訪問本地項目

4.1.統一環境

我們現在訪問頁面使用的是:http://localhost:9001

有沒有什么問題?

實際開發中,會有不同的環境:

  • 開發環境:自己的電腦
  • 測試環境:提供給測試人員使用的環境
  • 預發布環境:數據是和生成環境的數據一致,運行最新的項目代碼進去測試
  • 生產環境:項目最終發布上線的環境

如果不同環境使用不同的ip去訪問,可能會出現一些問題。為了保證所有環境的一致,我們會在各種環境下都使用域名來訪問。

我們將使用以下域名:

  • 主域名是:www.leyou.com,
  • 管理系統域名:manage.leyou.com
  • 網關域名:api.leyou.com
  • ...

但是最終,我們希望這些域名指向的還是我們本機的某個端口。

那么,當我們在瀏覽器輸入一個域名時,瀏覽器是如何找到對應服務的ip和端口的呢?

4.2.域名解析

一個域名一定會被解析為一個或多個ip。這一般會包含兩步:

  • 本地域名解析

    瀏覽器會首先在本機的hosts文件中查找域名映射的IP地址,如果查找到就返回IP ,沒找到則進行域名服務器解析,一般本地解析都會失敗,因為默認這個文件是空的。

    • Windows下的hosts文件地址:C:/Windows/System32/drivers/etc/hosts
    • Linux下的hosts文件所在路徑: /etc/hosts

    樣式:

    # My hosts
    127.0.0.1 localhost
    0.0.0.0 account.jetbrains.com
    127.0.0.1 www.xmind.net
    
  • 域名服務器解析

    本地解析失敗,才會進行域名服務器解析,域名服務器就是網絡中的一台計算機,里面記錄了所有注冊備案的域名和ip映射關系,一般只要域名是正確的,並且備案通過,一定能找到。

4.3.解決域名解析問題

我們不可能去購買一個域名,因此我們可以偽造本地的hosts文件,實現對域名的解析。修改本地的host為:

127.0.0.1 api.leyou.com
127.0.0.1 manage.leyou.com

這樣就實現了域名的關系映射了。

每次在C盤尋找hosts文件並修改是非常麻煩的,給大家推薦一個快捷修改host的工具,在課前資料中可以找到:

1530556073565

解壓,運行exe文件,效果:

1530382550630

我們添加了兩個映射關系(中間用空格隔開):

  • 127.0.0.1 api.leyou.com :我們的網關Zuul
  • 127.0.0.1 manage.leyou.com:我們的后台系統地址

現在,ping一下域名試試是否暢通:

1530382601757

OK!

通過域名訪問:

1530383586463

原因:我們配置了項目訪問的路徑,雖然manage.leyou.com映射的ip也是127.0.0.1,但是webpack會驗證host是否符合配置。

1530383612716

在webpack.dev.conf.js中取消host驗證:

1530383927461

重新執行npm run dev,刷新瀏覽器:

1530384150852

OK!

4.4.nginx解決端口問題

域名問題解決了,但是現在要訪問后台頁面,還得自己加上端口:http://manage.taotao.com:9001

這就不夠優雅了。我們希望的是直接域名訪問:http://manage.taotao.com。這種情況下端口默認是80,如何才能把請求轉移到9001端口呢?

這里就要用到反向代理工具:Nginx

4.4.1.什么是Nginx

1526187409033

nginx可以作為web服務器,但更多的時候,我們把它作為網關,因為它具備網關必備的功能:

  • 反向代理
  • 負載均衡
  • 動態路由
  • 請求過濾

4.4.2.nginx作為web服務器

Web服務器分2類:

  • web應用服務器,如:
    • tomcat
    • resin
    • jetty
  • web服務器,如:
    • Apache 服務器
    • Nginx
    • IIS

區分:web服務器不能解析jsp等頁面,只能處理js、css、html等靜態資源。 並發:web服務器的並發能力遠高於web應用服務器。

4.4.3.nginx作為反向代理

什么是反向代理?

  • 代理:通過客戶機的配置,實現讓一台服務器(代理服務器)代理客戶機,客戶的所有請求都交給代理服務器處理。
  • 反向代理:用一台服務器,代理真實服務器,用戶訪問時,不再是訪問真實服務器,而是代理服務器。

nginx可以當做反向代理服務器來使用:

  • 我們需要提前在nginx中配置好反向代理的規則,不同的請求,交給不同的真實服務器處理
  • 當請求到達nginx,nginx會根據已經定義的規則進行請求的轉發,從而實現路由功能

利用反向代理,就可以解決我們前面所說的端口問題,如圖

1526016663674

4.4.4.安裝和使用

安裝

安裝非常簡單,把課前資料提供的nginx直接解壓即可,綠色免安裝,舒服!

img

我們在本地安裝一台nginx:

1530556268445

解壓后,目錄結構:

1530384792790

  1. conf:配置目錄
  2. contrib:第三方依賴
  3. html:默認的靜態資源目錄,類似於tomcat的webapps
  4. logs:日志目錄
  5. nginx.exe:啟動程序。可雙擊運行,但不建議這么做。

反向代理配置

示例:

1526188831504

nginx中的每個server就是一個反向代理配置,可以有多個server

完整配置:

#user  nobody;
worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile  on; keepalive_timeout 65; gzip  on; server { listen 80; server_name manage.leyou.com; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location / { proxy_pass http://127.0.0.1:9001; proxy_connect_timeout 600; proxy_read_timeout 600; } } server { listen 80; server_name api.leyou.com; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location / { proxy_pass http://127.0.0.1:10010; proxy_connect_timeout 600; proxy_read_timeout 600; } } }

使用

nginx可以通過命令行來啟動,操作命令:

  • 啟動:start nginx.exe
  • 停止:nginx.exe -s stop
  • 重新加載:nginx.exe -s reload

啟動過程會閃爍一下,啟動成功后,任務管理器中會有兩個nginx進程:

1530385404783

4.5.測試

啟動nginx,然后用域名訪問后台管理系統:

1530385593052

現在實現了域名訪問網站了,中間的流程是怎樣的呢?

1526189945180

  1. 瀏覽器准備發起請求,訪問http://mamage.leyou.com,但需要進行域名解析

  2. 優先進行本地域名解析,因為我們修改了hosts,所以解析成功,得到地址:127.0.0.1

  3. 請求被發往解析得到的ip,並且默認使用80端口:http://127.0.0.1:80

    本機的nginx一直監聽80端口,因此捕獲這個請求

  4. nginx中配置了反向代理規則,將manage.leyou.com代理到127.0.0.1:9001,因此請求被轉發

  5. 后台系統的webpack server監聽的端口是9001,得到請求並處理,完成后將響應返回到nginx

  6. nginx將得到的結果返回到瀏覽器

5.實現商品分類查詢

商城的核心自然是商品,而商品多了以后,肯定要進行分類,並且不同的商品會有不同的品牌信息,其關系如圖所示:

1525999005260

  • 一個商品分類下有很多商品
  • 一個商品分類下有很多品牌
  • 而一個品牌,可能屬於不同的分類
  • 一個品牌下也會有很多商品

因此,我們需要依次去完成:商品分類、品牌、商品的開發。

5.1.導入數據

首先導入課前資料提供的sql:

1530556389224

我們先看商品分類表:

1525999774439

CREATE TABLE `tb_category` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '類目id', `name` varchar(20) NOT NULL COMMENT '類目名稱', `parent_id` bigint(20) NOT NULL COMMENT '父類目id,頂級類目填0', `is_parent` tinyint(1) NOT NULL COMMENT '是否為父節點,0為否,1為是', `sort` int(4) NOT NULL COMMENT '排序指數,越小越靠前', PRIMARY KEY (`id`), KEY `key_parent_id` (`parent_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1424 DEFAULT CHARSET=utf8 COMMENT='商品類目表,類目和商品(spu)是一對多關系,類目與品牌是多對多關系';

因為商品分類會有層級關系,因此這里我們加入了parent_id字段,對本表中的其它分類進行自關聯。

5.2.頁面實現

5.2.1.頁面分析

首先我們看下要實現的效果:

1530413709515

商品分類之間是會有層級關系的,采用樹結構去展示是最直觀的方式。

一起來看頁面,對應的是/pages/item/Category.vue:

1530386186299

頁面模板:

<template>
  <v-card> <v-flex xs12 sm10> <v-tree url="/item/category/list" :treeData="treeData" :isEdit="isEdit" @handleAdd="handleAdd" @handleEdit="handleEdit" @handleDelete="handleDelete" @handleClick="handleClick" /> </v-flex> </v-card> </template>
  • v-card:卡片,是vuetify中提供的組件,提供一個懸浮效果的面板,一般用來展示一組數據。

    1526000692741

  • v-flex:布局容器,用來控制響應式布局。與BootStrap的柵格系統類似,整個屏幕被分為12格。我們可以控制所占的格數來控制寬度:

    1526001573140

    本例中,我們用sm10控制在小屏幕及以上時,顯示寬度為10格

  • v-tree:樹組件。Vuetify並沒有提供樹組件,這個是我們自己編寫的自定義組件:

    1526001762446

    里面涉及一些vue的高級用法,大家暫時不要關注其源碼,會用即可。

5.2.2.樹組件的用法

也可參考課前資料中的:《自定義Vue組件的用法.md》

這里我貼出樹組件的用法指南。

屬性列表:

屬性名稱 說明 數據類型 默認值
url 用來加載數據的地址,即延遲加載 String -
isEdit 是否開啟樹的編輯功能 boolean false
treeData 整顆樹數據,這樣就不用遠程加載了 Array -

這里推薦使用url進行延遲加載,每當點擊父節點時,就會發起請求,根據父節點id查詢子節點信息。

當有treeData屬性時,就不會觸發url加載

遠程請求返回的結果格式:

[
    { 
        "id": 74, "name": "手機", "parentId": 0, "isParent": true, "sort": 2 }, { "id": 75, "name": "家用電器", "parentId": 0, "isParent": true, "sort": 3 } ]

事件:

事件名稱 說明 回調參數
handleAdd 新增節點時觸發,isEdit為true時有效 新增節點node對象,包含屬性:name、parentId和sort
handleEdit 當某個節點被編輯后觸發,isEdit為true時有效 被編輯節點的id和name
handleDelete 當刪除節點時觸發,isEdit為true時有效 被刪除節點的id
handleClick 點擊某節點時觸發 被點擊節點的node對象,包含完整的node信息

完整node的信息

回調函數中返回完整的node節點會包含以下數據:

{
    "id": 76, // 節點id "name": "手機", // 節點名稱 "parentId": 75, // 父節點id "isParent": false, // 是否是父節點 "sort": 1, // 順序 "path": ["手機", "手機通訊", "手機"] // 所有父節點的名稱數組 }

5.3.實現功能

5.3.1.url異步請求

給大家的頁面中,treeData是假數據,我們刪除數據treeData屬性,只保留url看看會發生什么:

<v-tree url="/item/category/list" :isEdit="isEdit" @handleAdd="handleAdd" @handleEdit="handleEdit" @handleDelete="handleDelete" @handleClick="handleClick" />

刷新頁面,可以看到:

1530427294644

頁面中的樹沒有了,並且發起了一條請求:http://localhost/api/item/category/list?pid=0

大家可能會覺得很奇怪,我們明明是使用的相對路徑,講道理發起的請求地址應該是:

http://manage.leyou.com/item/category/list

但實際卻是:

http://localhost/api/item/category/list?pid=0

這是因為,我們有一個全局的配置文件,對所有的請求路徑進行了約定:

1530427514123

路徑是localhost,並且默認加上了/api的前綴,這恰好與我們的網關設置匹配,我們只需要把地址改成網關的地址即可,因為我們使用了nginx反向代理,這里可以寫域名。

接下來,我們要做的事情就是編寫后台接口,返回對應的數據即可。

5.3.2.實體類

ly-item-interface中添加category實體類:

1530444682670

內容:

@Table(name="tb_category") public class Category { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; private String name; private Long parentId; private Boolean isParent; // 注意isParent生成的getter和setter方法需要手動加上Is private Integer sort; // getter和setter略 }

需要注意的是,這里要用到jpa的注解,因此我們在ly-item-iterface中添加jpa依賴

<dependency>
    <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> <version>1.0</version> </dependency>

5.3.3.controller

編寫一個controller一般需要知道四個內容:

  • 請求方式:決定我們用GetMapping還是PostMapping
  • 請求路徑:決定映射路徑
  • 請求參數:決定方法的參數
  • 返回值結果:決定方法的返回值

在剛才頁面發起的請求中,我們就能得到絕大多數信息:

1530445885707

  • 請求方式:Get

  • 請求路徑:/api/item/category/list。其中/api是網關前綴,/item是網關的路由映射,真實的路徑應該是/category/list

  • 請求參數:pid=0,根據tree組件的說明,應該是父節點的id,第一次查詢為0,那就是查詢一級類目

  • 返回結果:??

    根據前面tree組件的用法我們知道,返回的應該是json數組:

    [
        { 
            "id": 74, "name": "手機", "parentId": 0, "isParent": true, "sort": 2 }, { "id": 75, "name": "家用電器", "parentId": 0, "isParent": true, "sort": 3 } ]

    對應的java類型可以是List集合,里面的元素就是類目對象了。也就是List<Category>

添加Controller:

1530450599897

controller代碼:

@Controller
@RequestMapping("category") public class CategoryController { @Autowired private CategoryService categoryService; /**  * 根據parentId查詢類目  * @param pid  * @return  */ @RequestMapping("list") public ResponseEntity<List<Category>> queryCategoryListByParentId(@RequestParam(value = "pid", defaultValue = "0") Long pid) { try { if (pid == null || pid.longValue() < 0){ // pid為null或者小於等於0,響應400 return ResponseEntity.badRequest().build(); } // 執行查詢操作 List<Category> categoryList = this.categoryService.queryCategoryListByParentId(pid); if (CollectionUtils.isEmpty(categoryList)){ // 返回結果集為空,響應404 return ResponseEntity.notFound().build(); } // 響應200 return ResponseEntity.ok(categoryList); } catch (Exception e) { e.printStackTrace(); } // 響應500 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } }

5.3.4.service

一般service層我們會定義接口和實現類,不過這里我們就偷懶一下,直接寫實現類了:

1530450744567

@Service
public class CategoryService { @Autowired private CategoryMapper categoryMapper; /**  * 根據parentId查詢子類目  * @param pid  * @return  */ public List<Category> queryCategoryListByParentId(Long pid) { Category record = new Category(); record.setParentId(pid); return this.categoryMapper.select(record); } }

5.3.5.mapper

我們使用通用mapper來簡化開發:

public interface CategoryMapper extends Mapper<Category> { }

要注意,我們並沒有在mapper接口上聲明@Mapper注解,那么mybatis如何才能找到接口呢?

我們在啟動類上添加一個掃描包功能:

@SpringBootApplication
@EnableDiscoveryClient @MapperScan("com.leyou.item.mapper") // mapper接口的包掃描 public class LeyouItemServiceApplication { public static void main(String[] args) { SpringApplication.run(LeyouItemServiceApplication.class, args); } }

5.3.6.啟動並測試

我們不經過網關,直接訪問:http://localhost:8081/category/list

1530455133230

然后試試網關是否暢通:http://api.leyou.com/api/item/category/list

1530455291468

一切OK!

然后刷新后台管理頁面查看:

1530455437899

發現報錯了!

瀏覽器直接訪問沒事,但是這里卻報錯,什么原因?

6.跨域問題

跨域:瀏覽器對於javascript的同源策略的限制 。

以下情況都屬於跨域:

跨域原因說明 示例
域名不同 www.jd.com 與 www.taobao.com
域名相同,端口不同 www.jd.com:8080 與 www.jd.com:8081
二級域名不同 item.jd.com 與 miaosha.jd.com

如果域名和端口都相同,但是請求路徑不同,不屬於跨域,如:

www.jd.com/item

www.jd.com/goods

而我們剛才是從manage.leyou.com去訪問api.leyou.com,這屬於二級域名不同,跨域了。

6.1.為什么有跨域問題?

跨域不一定會有跨域問題。

因為跨域問題是瀏覽器對於ajax請求的一種安全限制:一個頁面發起的ajax請求,只能是與當前頁域名相同的路徑,這能有效的阻止跨站攻擊。

因此:跨域問題 是針對ajax的一種限制。

但是這卻給我們的開發帶來了不便,而且在實際生產環境中,肯定會有很多台服務器之間交互,地址和端口都可能不同,怎么辦?

6.2.解決跨域問題的方案

目前比較常用的跨域解決方案有3種:

  • Jsonp

    最早的解決方案,利用script標簽可以跨域的原理實現。

    限制:

    • 需要服務的支持
    • 只能發起GET請求
  • nginx反向代理

    思路是:利用nginx把跨域反向代理為不跨域,支持各種請求方式

    缺點:需要在nginx進行額外配置,語義不清晰

  • CORS

    規范化的跨域請求解決方案,安全可靠。

    優勢:

    • 在服務端進行控制是否允許跨域,可自定義規則
    • 支持各種請求方式

    缺點:

    • 會產生額外的請求

我們這里會采用cors的跨域方案。

6.3.cors解決跨域

6.3.1.什么是cors

CORS是一個W3C標准,全稱是"跨域資源共享"(Cross-origin resource sharing)。

它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。

CORS需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低於IE10。

  • 瀏覽器端:

    目前,所有瀏覽器都支持該功能(IE10以下不行)。整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。

  • 服務端:

    CORS通信與AJAX沒有任何差別,因此你不需要改變以前的業務邏輯。只不過,瀏覽器會在請求中攜帶一些頭信息,我們需要以此判斷是否允許其跨域,然后在響應頭中加入一些信息即可。這一般通過過濾器完成即可。

6.3.2.原理有點復雜

瀏覽器會將ajax請求分為兩類,其處理方案略有差異:簡單請求、特殊請求。

6.3.2.1.簡單請求

只要同時滿足以下兩大條件,就屬於簡單請求。:

(1) 請求方法是以下三種方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的頭信息不超出以下幾種字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限於三個值application/x-www-form-urlencodedmultipart/form-datatext/plain

當瀏覽器發現發起的ajax請求是簡單請求時,會在請求頭中攜帶一個字段:Origin.

1530460311064

Origin中會指出當前請求屬於哪個域(協議+域名+端口)。服務會根據這個值決定是否允許其跨域。

如果服務器允許跨域,需要在返回的響應頭中攜帶下面信息:

Access-Control-Allow-Origin: http://manage.leyou.com Access-Control-Allow-Credentials: true Content-Type: text/html; charset=utf-8
  • Access-Control-Allow-Origin:可接受的域,是一個具體域名或者*(代表任意域名)
  • Access-Control-Allow-Credentials:是否允許攜帶cookie,默認情況下,cors不會攜帶cookie,除非這個值是true

有關cookie:

要想操作cookie,需要滿足3個條件:

  • 服務的響應頭中需要攜帶Access-Control-Allow-Credentials並且為true。
  • 瀏覽器發起ajax需要指定withCredentials 為true
  • 響應頭中的Access-Control-Allow-Origin一定不能為*,必須是指定的域名

6.3.2.2.特殊請求

不符合簡單請求的條件,會被瀏覽器判定為特殊請求,,例如請求方式為PUT。

預檢請求

特殊請求會在正式通信之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)。

瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段。只有得到肯定答復,瀏覽器才會發出正式的XMLHttpRequest請求,否則就報錯。

一個“預檢”請求的樣板:

OPTIONS /cors HTTP/1.1 Origin: http://manage.leyou.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.leyou.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...

與簡單請求相比,除了Origin以外,多了兩個頭:

  • Access-Control-Request-Method:接下來會用到的請求方式,比如PUT
  • Access-Control-Request-Headers:會額外用到的頭信息

預檢請求的響應

服務的收到預檢請求,如果許可跨域,會發出響應:

HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://manage.leyou.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Max-Age: 1728000 Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain

除了Access-Control-Allow-OriginAccess-Control-Allow-Credentials以外,這里又額外多出3個頭:

  • Access-Control-Allow-Methods:允許訪問的方式
  • Access-Control-Allow-Headers:允許攜帶的頭
  • Access-Control-Max-Age:本次許可的有效時長,單位是秒,過期之前的ajax請求就無需再次進行預檢了

如果瀏覽器得到上述響應,則認定為可以跨域,后續就跟簡單請求的處理是一樣的了。

6.3.3.實現非常簡單

雖然原理比較復雜,但是前面說過:

  • 瀏覽器端都有瀏覽器自動完成,我們無需操心
  • 服務端可以通過攔截器統一實現,不必每次都去進行跨域判定的編寫。

事實上,SpringMVC已經幫我們寫好了CORS的跨域過濾器:CorsFilter ,內部已經實現了剛才所講的判定邏輯,我們直接用就好了。

leyou-gateway中編寫一個配置類,並且注冊CorsFilter:

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @Configuration public class GlobalCorsConfig { @Bean public CorsFilter corsFilter() { //1.添加CORS配置信息 CorsConfiguration config = new CorsConfiguration(); //1) 允許的域,不要寫*,否則cookie就無法使用了 config.addAllowedOrigin("http://manage.leyou.com"); //2) 是否發送Cookie信息 config.setAllowCredentials(true); //3) 允許的請求方式 config.addAllowedMethod("OPTIONS"); config.addAllowedMethod("HEAD"); config.addAllowedMethod("GET"); config.addAllowedMethod("PUT"); config.addAllowedMethod("POST"); config.addAllowedMethod("DELETE"); config.addAllowedMethod("PATCH"); // 4)允許的頭信息 config.addAllowedHeader("*"); //2.添加映射路徑,我們攔截一切請求 UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/**", config); //3.返回新的CorsFilter. return new CorsFilter(configSource); } }

結構:

1530462650711

重啟測試,訪問正常:

1530463010927

分類的增刪改功能暫時就不做了,頁面已經預留好了事件接口,有興趣的同學可以完成一下。

7.從0開始品牌的查詢

商品分類完成以后,自然輪到了品牌功能了。

先看看我們要實現的效果:

1526021968036

接下來,我們從0開始,實現下從前端到后端的完整開發。

7.1.設計前端頁面

為了方便看到效果,我們新建一個MyBrand.vue(注意先停掉服務器),從0開始搭建。

1530464178834

內容初始化一下:

<template>
  <span> hello </span> </template> <script>  export default {  name: "myBrand"  } </script> <!-- scoped:當前樣式只作用於當前組件的節點 --> <style scoped>  </style>

改變router新的index.js,將路由地址指向MyBrand.vue

1530464255544

打開服務器,再次查看頁面:

1530464304721

干干凈凈了。只剩hello

7.1.1.查詢表格

大家看到這個原型頁面肯定能看出,其主體就是一個table。我們去Vuetify查看有關table的文檔:

1526023540226

仔細閱讀,發現v-data-table中有以下核心屬性:

  • dark:是否使用黑暗色彩主題,默認是false

  • expand:表格的行是否可以展開,默認是false

  • headers:定義表頭的數組,數組的每個元素就是一個表頭信息對象,結構:

    {
      text: string, // 表頭的顯示文本 value: string, // 表頭對應的每行數據的key align: 'left' | 'center' | 'right', // 位置 sortable: boolean, // 是否可排序 class: string[] | string,// 樣式 width: string,// 寬度 }
  • items:表格的數據的數組,數組的每個元素是一行數據的對象,對象的key要與表頭的value一致

  • loading:是否顯示加載數據的進度條,默認是false

  • no-data-text:當沒有查詢到數據時顯示的提示信息,string類型,無默認值

  • pagination.sync:包含分頁和排序信息的對象,將其與vue實例中的屬性關聯,表格的分頁或排序按鈕被觸發時,會自動將最新的分頁和排序信息更新。對象結構:

    {
        page: 1, // 當前頁 rowsPerPage: 5, // 每頁大小 sortBy: '', // 排序字段 descending:false, // 是否降序 }
  • total-items:分頁的總條數信息,number類型,無默認值

  • select-all :是否顯示每一行的復選框,Boolean類型,無默認值

  • value:當表格可選的時候,返回選中的行

我們向下翻,找找有沒有看起來牛逼的案例。

找到這樣一條:

1526023837773

其它的案例都是由Vuetify幫我們對查詢到的當前頁數據進行排序和分頁,這顯然不是我們想要的。我們希望能在服務端完成對整體品牌數據的排序和分頁,而這個案例恰好合適。

點擊按鈕,我們直接查看源碼,然后直接復制到MyBrand.vue中

模板:

<template>
  <div> <v-data-table :headers="headers" :items="desserts" :pagination.sync="pagination" :total-items="totalDesserts" :loading="loading" class="elevation-1" > <template slot="items" slot-scope="props"> <td>{{ props.item.name }}</td> <td class="text-xs-right">{{ props.item.calories }}</td> <td class="text-xs-right">{{ props.item.fat }}</td> <td class="text-xs-right">{{ props.item.carbs }}</td> <td class="text-xs-right">{{ props.item.protein }}</td> <td class="text-xs-right">{{ props.item.iron }}</td> </template> </v-data-table> </div> </template>

7.1.2.表格分析

接下來,就分析一下案例中每一部分是什么意思,搞清楚了,我們也可以自己玩了。

先看模板中table上的一些屬性:

<v-data-table
              :headers="headers" :items="desserts" :pagination.sync="pagination" :total-items="totalDesserts" :loading="loading" class="elevation-1" > </v-data-table>
  • headers:表頭信息,是一個數組

  • items:要在表格中展示的數據,數組結構,每一個元素是一行。在這里應該是品牌集合

  • pagination.sync:分頁信息,包含了當前頁,每頁大小,排序字段,排序方式等。加上.sync代表服務端排序,當用戶點擊分頁條時,該對象的值會跟着變化。監控這個值,並在這個值變化時去服務端查詢,即可實現頁面數據動態加載了。

  • total-items:總條數,在這里是品牌的總記錄數

  • loading:boolean類型,true:代表數據正在加載,會有進度條。false:數據加載完畢。

    1526029254159

另外,在v-data-tables中,我們還看到另一段代碼:

<template slot="items" slot-scope="props"> <td>{{ props.item.name }}</td> <td class="text-xs-right">{{ props.item.calories }}</td> <td class="text-xs-right">{{ props.item.fat }}</td> <td class="text-xs-right">{{ props.item.carbs }}</td> <td class="text-xs-right">{{ props.item.protein }}</td> <td class="text-xs-right">{{ props.item.iron }}</td> </template>

這段就是在渲染每一行的數據。Vue會自動遍歷上面傳遞的items屬性,並把得到的對象傳遞給這段template中的props.item屬性。我們從中得到數據,渲染在頁面即可。

我們需要做的事情,主要有兩件:

  • 給items和totalItems賦值
  • 當pagination變化時,重新獲取數據,再次給items和totalItems賦值

7.1.3.動手實現

表格中具體有哪些列呢?參照品牌表:

1530518876942

品牌中有id,name,image,letter字段。

7.1.3.1.修改模板

<template>
  <div> <v-data-table :headers="headers" :items="brands" :pagination.sync="pagination" :total-items="totalBrands" :loading="loading" class="elevation-1" > <template slot="items" slot-scope="props"> <td class="text-xs-center">{{ props.item.id }}</td> <td class="text-xs-center">{{ props.item.name }}</td> <td class="text-xs-center"><img v-if="props.item.image" :src="props.item.image" width="130" height="40"/></td> <td class="text-xs-center">{{ props.item.letter }}</td> </template> </v-data-table> </div> </template>

我們修改了以下部分:

  • items:指向一個brands變量,等下在js代碼中定義
  • total-items:指向了totalBrands變量,等下在js代碼中定義
  • template模板中,渲染了四個字段:
    • id:
    • name
    • image,注意,我們不是以文本渲染,而是賦值到一個img標簽的src屬性中,並且做了非空判斷
    • letter

7.1.3.2.編寫數據模型

接下來編寫要用到的數據:

    data () {
      return { totalBrands: 0, // 總條數 brands: [], // 當前頁品牌數據 loading: true, // 是否在加載中 pagination: {}, // 分頁信息 headers: [ // 頭信息 {text: 'id', align: 'center', value: 'id'}, {text: '名稱', align: 'center', value: 'name', sortable: false}, {text: 'LOGO', align: 'center', value: 'image', sortable: false}, {text: '首字母', align: 'center', value: 'letter'}, ] } }

7.1.3.3.數據初始化

接下來就是對brands和totalBrands完成賦值動作了。

我們編寫一個函數來完成賦值,提高復用性:

    methods: {
      getDataFromServer(){ // 從服務端加載數據的函數 // 偽造演示數據 const brands = [ { "id": 2032, "name": "OPPO", "image": "http://img10.360buyimg.com/popshop/jfs/t2119/133/2264148064/4303/b8ab3755/56b2f385N8e4eb051.jpg", "letter": "O", "categories": null }, { "id": 2033, "name": "飛利浦(PHILIPS)", "image": "http://img12.360buyimg.com/popshop/jfs/t18361/122/1318410299/1870/36fe70c9/5ac43a4dNa44a0ce0.jpg", "letter": "F", "categories": null }, { "id": 2034, "name": "華為(HUAWEI)", "image": "http://img10.360buyimg.com/popshop/jfs/t5662/36/8888655583/7806/1c629c01/598033b4Nd6055897.jpg", "letter": "H", "categories": null }, { "id": 2036, "name": "酷派(Coolpad)", "image": "http://img10.360buyimg.com/popshop/jfs/t2521/347/883897149/3732/91c917ec/5670cf96Ncffa2ae6.jpg", "letter": "K", "categories": null }, { "id": 2037, "name": "魅族(MEIZU)", "image": "http://img13.360buyimg.com/popshop/jfs/t3511/131/31887105/4943/48f83fa9/57fdf4b8N6e95624d.jpg", "letter": "M", "categories": null } ]; // 延遲一段時間,模擬數據請求時間 setTimeout(()=>{ this.brands = brands; // 賦值給品牌數組 this.totalBrands = brands.length; // 賦值數據總條數 this.loading = false; // 數據加載完成 }, 1000); } }

然后使用鈎子函數,在Vue實例初始化完畢后調用這個方法,這里使用mounted(渲染后)函數:

    // 渲染后執行 mounted(){ this.getDataFromServer() // 調用數據初始化函數 }

6.2.3.4.完整代碼

<template>
  <div> <v-data-table :headers="headers" :items="brands" :pagination.sync="pagination" :total-items="totalBrands" :loading="loading" class="elevation-1" > <template slot="items" slot-scope="props"> <td class="text-xs-center">{{ props.item.id }}</td> <td class="text-xs-center">{{ props.item.name }}</td> <td class="text-xs-center"><img v-if="props.item.image" :src="props.item.image" width="130" height="40"/></td> <td class="text-xs-center">{{ props.item.letter }}</td> </template> </v-data-table> </div> </template> <script>  export default {  name: "myBrand",  data () {  return {  totalBrands: 0, // 總條數  brands: [], // 當前頁品牌數據  loading: true, // 是否在加載中  pagination: {}, // 分頁信息  headers: [ // 頭信息  {text: 'id', align: 'center', value: 'id'},  {text: '名稱', align: 'center', value: 'name', sortable: false},  {text: 'LOGO', align: 'center', value: 'image', sortable: false},  {text: '首字母', align: 'center', value: 'letter'},  ]  }  },  methods: {  getDataFromServer(){ // 從服務端加載數據的函數  // 偽造演示數據  const brands = [  {  "id": 2032,  "name": "OPPO",  "image": "http://img10.360buyimg.com/popshop/jfs/t2119/133/2264148064/4303/b8ab3755/56b2f385N8e4eb051.jpg",  "letter": "O",  "categories": null  },  {  "id": 2033,  "name": "飛利浦(PHILIPS)",  "image": "http://img12.360buyimg.com/popshop/jfs/t18361/122/1318410299/1870/36fe70c9/5ac43a4dNa44a0ce0.jpg",  "letter": "F",  "categories": null  },  {  "id": 2034,  "name": "華為(HUAWEI)",  "image": "http://img10.360buyimg.com/popshop/jfs/t5662/36/8888655583/7806/1c629c01/598033b4Nd6055897.jpg",  "letter": "H",  "categories": null  },  {  "id": 2036,  "name": "酷派(Coolpad)",  "image": "http://img10.360buyimg.com/popshop/jfs/t2521/347/883897149/3732/91c917ec/5670cf96Ncffa2ae6.jpg",  "letter": "K",  "categories": null  },  {  "id": 2037,  "name": "魅族(MEIZU)",  "image": "http://img13.360buyimg.com/popshop/jfs/t3511/131/31887105/4943/48f83fa9/57fdf4b8N6e95624d.jpg",  "letter": "M",  "categories": null  }  ];  // 延遲一段時間,模擬數據請求時間  setTimeout(()=>{  this.brands = brands; // 賦值給品牌數組  this.totalBrands = brands.length; // 賦值數據總條數  this.loading = false; // 數據加載完成  }, 1000);  }  },  // 渲染后執行  mounted(){  this.getDataFromServer() // 調用數據初始化函數  }  } </script> <!-- scoped:當前樣式只作用於當前組件的節點 --> <style scoped>  </style>

刷新頁面查看:

1526029445561

7.1.4.優化頁面

7.1.4.1.編輯和刪除按鈕

我們將來要對品牌進行增刪改,需要給每一行數據添加 修改刪除的按鈕,一般放到改行的最后一列。

其實就是多了一列,只是這一列沒有數據,而是兩個按鈕而已。可以在官方文檔中找一個帶有操作按鈕的表格,作為參考。

1530523252532

我們先在頭(headers)中添加一列:

headers: [ // 頭信息 {text: 'id', align: 'center', value: 'id'}, {text: '名稱', align: 'center', value: 'name', sortable: false}, {text: 'LOGO', align: 'center', value: 'image', sortable: false}, {text: '首字母', align: 'center', value: 'letter'}, {text: '操作', align: 'center', value: 'id', sortable: false } ]

然后在模板中添加按鈕:

<template slot="items" slot-scope="props"> <td class="text-xs-center">{{ props.item.id }}</td> <td class="text-xs-center">{{ props.item.name }}</td> <td class="text-xs-center"><img v-if="props.item.image" :src="props.item.image" width="130" height="40"/></td> <td class="text-xs-center">{{ props.item.letter }}</td> <td class="text-xs-center"> <v-icon small class="mr-2" @click="editItem(props.item)"> edit </v-icon> <v-icon small @click="deleteItem(props.item)"> delete </v-icon> </td> </template>

效果:

1530523838357

7.1.4.2.新增按鈕

在官方文檔中找到按鈕的用法:

1530524238344

因為新增跟某個品牌無關,是獨立的,因此我們可以放到表格的外面。

1530527129881

效果:

1530527160208

7.1.4.3.卡片(card)

為了不讓按鈕顯得過於孤立,我們可以將按新增按鈕表格放到一張卡片(card)中。

我們去官網查看卡片的用法:

1526031159242

卡片v-card包含四個基本組件:

  • v-card-media:一般放圖片或視頻
  • v-card-title:卡片的標題,一般位於卡片頂部
  • v-card-text:卡片的文本(主體內容),一般位於卡片正中
  • v-card-action:卡片的按鈕,一般位於卡片底部

我們可以把新增的按鈕放到v-card-title位置,把table放到下面,這樣就成一個上下關系。

<template>
  <v-card> <v-card-title flat color="white"> <v-btn color="primary">新增</v-btn> </v-card-title> <v-data-table :headers="headers" :items="brands" :pagination.sync="pagination" :total-items="totalBrands" :loading="loading" class="elevation-1" > <template slot="items" slot-scope="props"> <td class="text-xs-center">{{ props.item.id }}</td> <td class="text-xs-center">{{ props.item.name }}</td> <td class="text-xs-center"><img v-if="props.item.image" :src="props.item.image" width="130" height="40"/></td> <td class="text-xs-center">{{ props.item.letter }}</td> <td class="text-xs-center"> <v-icon small class="mr-2" @click="editItem(props.item)"> edit </v-icon> <v-icon small @click="deleteItem(props.item)"> delete </v-icon> </td> </template> </v-data-table> </v-card> </template>

效果:

1530532504861

7.1.4.4.添加搜索框

我們還可以在卡片頭部添加一個搜索框,其實就是一個文本輸入框。

查看官網中,文本框的用法:

1526031897445

  • name:字段名,表單中會用到
  • label/placeholder:提示文字
  • value:值。可以用v-model代替,實現雙向綁定

修改模板,添加輸入框:

<v-card-title>
    <v-btn color="primary">新增品牌</v-btn> <!--搜索框,與search屬性關聯--> <v-text-field label="輸入關鍵字搜索" v-model="search"/> </v-card-title>

注意:要在數據模型中,添加search字段:

data() {
  return { totalBrands: 0, // 總條數 brands: [], // 當前頁品牌數據 search: "", // 查詢關鍵字 loading: true, // 是否在加載中 pagination: {}, // 分頁信息 headers: [ // 頭信息 {text: 'id', align: 'center', value: 'id'}, {text: '名稱', align: 'center', value: 'name', sortable: false}, {text: 'LOGO', align: 'center', value: 'image', sortable: false}, {text: '首字母', align: 'center', value: 'letter'}, {text: '操作', align: 'center', value: 'id', sortable: false} ] } }

效果:

1530533261856

發現輸入框超級長!!!

這個時候,我們可以使用Vuetify提供的一個空間隔離工具:

1530533442878

修改代碼:

    <v-card-title>
      <v-btn color="primary">新增品牌</v-btn> <!--空間隔離組件--> <v-spacer /> <!--搜索框,與search屬性關聯--> <v-text-field label="輸入關鍵字搜索" v-model="search"/> </v-card-title>

1530534269196

7.1.4.5.添加搜索圖標

查看textfiled的文檔,發現:

1526033007616

通過append-icon屬性可以為 輸入框添加后置圖標,所有可用圖標名稱可以到 material-icons官網去查看。

修改我們的代碼:

<v-text-field label="輸入關鍵字搜索" v-model="search" append-icon="search"/>

1530534311458

7.1.4.6.把文本框變緊湊

搜索框看起來高度比較高,頁面不夠緊湊。這其實是因為默認在文本框下面預留有錯誤提示空間。通過下面的屬性可以取消提示:

1526033439890

修改代碼:

<v-text-field label="輸入關鍵字搜索" v-model="search" append-icon="search" hide-details/>

效果:

1530534423194

幾乎已經達到了原來一樣的效果了吧!

7.2.后台提供查詢接口

前台頁面已經准備好,接下來就是后台提供數據接口了。

7.2.1.數據庫表

CREATE TABLE `tb_brand` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '品牌id', `name` varchar(50) NOT NULL COMMENT '品牌名稱', `image` varchar(200) DEFAULT '' COMMENT '品牌圖片地址', `letter` char(1) DEFAULT '' COMMENT '品牌的首字母', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=325400 DEFAULT CHARSET=utf8 COMMENT='品牌表,一個品牌下有多個商品(spu),一對多關系';

簡單的四個字段,不多解釋。

這里需要注意的是,品牌和商品分類之間是多對多關系。因此我們有一張中間表,來維護兩者間關系:

CREATE TABLE `tb_category_brand` ( `category_id` bigint(20) NOT NULL COMMENT '商品類目id', `brand_id` bigint(20) NOT NULL COMMENT '品牌id', PRIMARY KEY (`category_id`,`brand_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品分類和品牌的中間表,兩者是多對多關系'; 

但是,你可能會發現,這張表中並沒有設置外鍵約束,似乎與數據庫的設計范式不符。為什么這么做?

  • 外鍵會嚴重影響數據庫讀寫的效率
  • 數據刪除時會比較麻煩

在電商行業,性能是非常重要的。我們寧可在代碼中通過邏輯來維護表關系,也不設置外鍵。

7.2.2.實體類

1530541070271

@Table(name = "tb_brand") public class Brand { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name;// 品牌名稱 private String image;// 品牌圖片 private Character letter; // getter setter 略 }

7.2.3.mapper

1530541222679

通用mapper來簡化開發:

public interface BrandMapper extends Mapper<Brand> { }

7.2.4.controller

編寫controller先思考四個問題,這次沒有前端代碼,需要我們自己來設定

  • 請求方式:查詢,肯定是Get
  • 請求路徑:分頁查詢,/brand/page
  • 請求參數:根據我們剛才編寫的頁面,有分頁功能,有排序功能,有搜索過濾功能,因此至少要有5個參數:
    • page:當前頁,int
    • rows:每頁大小,int
    • sortBy:排序字段,String
    • desc:是否為降序,boolean
    • key:搜索關鍵詞,String
  • 響應結果:分頁結果一般至少需要兩個數據
    • total:總條數
    • items:當前頁數據
    • totalPage:有些還需要總頁數

這里我們封裝一個類,來表示分頁結果:

public class PageResult<T> { private Long total;// 總條數 private Long totalPage;// 總頁數 private List<T> items;// 當前頁數據 public PageResult() { } public PageResult(Long total, List<T> items) { this.total = total; this.items = items; } public PageResult(Long total, Long totalPage, List<T> items) { this.total = total; this.totalPage = totalPage; this.items = items; } public Long getTotal() { return total; } public void setTotal(Long total) { this.total = total; } public List<T> getItems() { return items; } public void setItems(List<T> items) { this.items = items; } public Long getTotalPage() { return totalPage; } public void setTotalPage(Long totalPage) { this.totalPage = totalPage; } }

另外,這個PageResult以后可能在其它項目中也有需求,因此我們將其抽取到leyou-common中,提高復用性:

1530543778541

不要忘記在leyou-item-service工程的pom.xml中引入leyou-common的依賴:

        <dependency>
            <groupId>com.leyou.common</groupId> <artifactId>leyou-common</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency>

接下來,我們編寫Controller

1530549253999

@RestController
@RequestMapping("brand") public class BrandController { @Autowired private BrandService brandService; @GetMapping("page") public ResponseEntity<PageResult<Brand>> queryBrandByPage( @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "rows", defaultValue = "5") Integer rows, @RequestParam(value = "sortBy", required = false) String sortBy, @RequestParam(value = "desc", defaultValue = "false") Boolean desc, @RequestParam(value = "key", required = false) String key) { PageResult<Brand> result = this.brandService.queryBrandByPageAndSort(page,rows,sortBy,desc, key); if (result == null || result.getItems().size() == 0) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } return ResponseEntity.ok(result); } }

7.2.5.Service

1530549286718

@Service
public class BrandService { @Autowired private BrandMapper brandMapper; public PageResult<Brand> queryBrandByPageAndSort( Integer page, Integer rows, String sortBy, Boolean desc, String key) { // 開始分頁 PageHelper.startPage(page, rows); // 過濾 Example example = new Example(Brand.class); if (StringUtils.isNotBlank(key)) { example.createCriteria().andLike("name", "%" + key + "%") .orEqualTo("letter", key); } if (StringUtils.isNotBlank(sortBy)) { // 排序 String orderByClause = sortBy + (desc ? " DESC" : " ASC"); example.setOrderByClause(orderByClause); } // 查詢 Page<Brand> pageInfo = (Page<Brand>) brandMapper.selectByExample(example); // 返回結果 return new PageResult<>(pageInfo.getTotal(), pageInfo); } }

7.2.6.測試

通過瀏覽器訪問試試:http://api.leyou.com/api/item/brand/page

1530549095150

接下來,去頁面請求數據並渲染

7.3.異步查詢工具axios

異步查詢數據,自然是通過ajax查詢,大家首先想起的肯定是jQuery。但jQuery與MVVM的思想不吻合,而且ajax只是jQuery的一小部分。因此不可能為了發起ajax請求而去引用這么大的一個庫。

7.3.1.axios入門

Vue官方推薦的ajax請求框架叫做:axios,看下demo:

1526033988251

axios的Get請求語法:

axios.get("/item/category/list?pid=0") // 請求路徑和請求參數拼接 .then(function(resp){ // 成功回調函數 }) .catch(function(){ // 失敗回調函數 }) // 參數較多時,可以通過params來傳遞參數 axios.get("/item/category/list", { params:{ pid:0 } }) .then(function(resp){})// 成功時的回調 .catch(function(error){})// 失敗時的回調

axios的POST請求語法:

比如新增一個用戶

axios.post("/user",{ name:"Jack", age:21 }) .then(function(resp){}) .catch(function(error){})
  • 注意,POST請求傳參,不需要像GET請求那樣定義一個對象,在對象的params參數中傳參。post()方法的第二個參數對象,就是將來要傳遞的參數

PUT和DELETE請求與POST請求類似

7.3.2.axios的全局配置

而在我們的項目中,已經引入了axios,並且進行了簡單的封裝,在src下的http.js中:

1526034150067

http.js中對axios進行了一些默認配置:

import Vue from 'vue' import axios from 'axios' import config from './config' // config中定義的基礎路徑是:http://api.leyou.com/api axios.defaults.baseURL = config.api; // 設置axios的基礎請求路徑 axios.defaults.timeout = 2000; // 設置axios的請求時間 Vue.prototype.$http = axios;// 將axios賦值給Vue原型的$http屬性,這樣所有vue實例都可使用該對象
  • http.js中導入了config的配置,還記得嗎?

    1526041205846

  • http.js對axios進行了全局配置:baseURL=config.api,即http://api.leyou.com/api。因此以后所有用axios發起的請求,都會以這個地址作為前綴。

  • 通過Vue.property.$http = axios,將axios賦值給了 Vue原型中的$http。這樣以后所有的Vue實例都可以訪問到$http,也就是訪問到了axios了。

7.3.3.小試一下

我們在組件MyBrand.vue的getDataFromServer方法,通過$http發起get請求,測試查詢品牌的接口,看是否能獲取到數據:

1526048221750

網絡監視:

1526048143014

resp到底都有那些數據,查看控制台結果:

1526048275064

可以看到,在請求成功的返回結果response中,有一個data屬性,里面就是真正的響應數據。

響應結果中與我們設計的一致,包含3個內容:

  • total:總條數,目前是165
  • items:當前頁數據
  • totalPage:總頁數,我們沒有返回

7.4.異步加載品牌數據

雖然已經通過ajax請求獲取了品牌數據,但是剛才的請求沒有攜帶任何參數,這樣顯然不對。我們后端接口需要5個參數:

  • page:當前頁,int
  • rows:每頁大小,int
  • sortBy:排序字段,String
  • desc:是否為降序,boolean
  • key:搜索關鍵詞,String

而頁面中分頁信息應該是在pagination對象中,我們通過瀏覽器工具,查看pagination中有哪些屬性:

1530553414937

分別是:

  • descending:是否是降序,對應請求參數的desc
  • page:當前頁,對應參數的page
  • rowsPerpage:每頁大小,對應參數中的rows
  • sortBy:排序字段,對應參數的sortBy

缺少一個搜索關鍵詞,這個應該是通過v-model與輸入框綁定的屬性:search。這樣,所有參數就都有了。

另外,不要忘了把查詢的結果賦值給brands和totalBrands屬性,Vuetify會幫我們渲染頁面。

接下來,我們完善請求參數:

    methods: {
      getDataFromServer() { // 從服務端加載數據的函數 this.loading = true; // 加載數據 // 通過axios獲取數據 this.$http.get("/item/brand/page", { params: { page: this.pagination.page, // 當前頁 rows: this.pagination.rowsPerPage, // 每頁條數 sortBy: this.pagination.sortBy, // 排序字段 desc: this.pagination.descending, // 是否降序 key: this.search // 查詢字段 } }).then(resp => { // 獲取響應結果對象 this.totalBrands = resp.data.total; // 總條數 this.brands = resp.data.items; // 品牌數據 this.loading = false; // 加載完成 }); } }

查看網絡請求:

1530554422695

效果:

1530554460215

7.5.完成分頁和過濾

6.6.1.分頁

現在我們實現了頁面加載時的第一次查詢,你會發現你點擊分頁或搜索不會發起新的請求,怎么辦?

雖然點擊分頁,不會發起請求,但是通過瀏覽器工具查看,會發現pagination對象的屬性一直在變化:

我們可以利用Vue的監視功能:watch,當pagination發生改變時,會調用我們的回調函數,我們在回調函數中進行數據的查詢即可!

具體實現:

1526049643506

成功實現分頁功能:

1526049720200

6.6.2.過濾

分頁實現了,過濾也很好實現了。過濾字段對應的是search屬性,我們只要監視這個屬性即可:

1526049939985

查看網絡請求:

1526050032436

頁面結果:

1530555740595

6.7.完整代碼

<template>
  <v-card> <v-card-title flat color="white"> <v-btn color="primary">新增</v-btn> <!--空間隔離組件--> <v-spacer /> <!--搜索框,與search屬性關聯--> <v-text-field label="輸入關鍵字搜索" append-icon="search" v-model="search" hide-details/> </v-card-title> <v-data-table :headers="headers" :items="brands" :pagination.sync="pagination" :total-items="totalBrands" :loading="loading" class="elevation-1" > <template slot="items" slot-scope="props"> <td class="text-xs-center">{{ props.item.id }}</td> <td class="text-xs-center">{{ props.item.name }}</td> <td class="text-xs-center"><img v-if="props.item.image" :src="props.item.image" width="130" height="40"/></td> <td class="text-xs-center">{{ props.item.letter }}</td> <td class="text-xs-center"> <v-icon small class="mr-2" @click="editItem(props.item)"> edit </v-icon> <v-icon small @click="deleteItem(props.item)"> delete </v-icon> </td> </template> </v-data-table> </v-card> </template> <script>  export default {  name: "myBrand",  data() {  return {  totalBrands: 0, // 總條數  brands: [], // 當前頁品牌數據  search: "", // 查詢關鍵字  loading: true, // 是否在加載中  pagination: {}, // 分頁信息  headers: [ // 頭信息  {text: 'id', align: 'center', value: 'id'},  {text: '名稱', align: 'center', value: 'name', sortable: false},  {text: 'LOGO', align: 'center', value: 'image', sortable: false},  {text: '首字母', align: 'center', value: 'letter'},  {text: '操作', align: 'center', value: 'id', sortable: false}  ]  }  },  watch: {  pagination:{  deep: true, // 深度監視  handler(){  this.getDataFromServer();  }  },  search(){  this.pagination.page = 1;  this.getDataFromServer();  }  },  methods: {  getDataFromServer() { // 從服務端加載數據的函數  this.loading = true; // 加載數據  // 通過axios獲取數據  this.$http.get("/item/brand/page", {  params: {  page: this.pagination.page, // 當前頁  rows: this.pagination.rowsPerPage, // 每頁條數  sortBy: this.pagination.sortBy, // 排序字段  desc: this.pagination.descending, // 是否降序  key: this.search // 查詢字段  }  }).then(resp => { // 獲取響應結果對象  this.totalBrands = resp.data.total; // 總條數  this.brands = resp.data.items; // 品牌數據  this.loading = false; // 加載完成  });  }  },  // 渲染后執行  mounted() {  this.getDataFromServer() // 調用數據初始化函數  }  } </script> <!-- scoped:當前樣式只作用於當前組件的節點 --> <style scoped>  </style>

大家下去可以嘗試實現品牌的增刪改功能


免責聲明!

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



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