樂優商場開發第六天筆記
0.學習目標
-
使用資料搭建後臺系統
-
會使用nginx進行反向代理
-
實現商品分類查詢功能
-
掌握cors解決跨域
-
實現品牌查詢功能
1.搭建後臺管理前端
1.1.匯入已有資源
後臺專案相對複雜,為了有利於教學,我們不再從0搭建專案,而是直接使用課前資料中給大家準備好的原始碼:
我們解壓縮,放到工作目錄中:
然後在Intellij idea中匯入新的工程:
選中我們的工程:
這正是一個用vue-cli構建的webpack工程,是不是與昨天的一樣:
1.2.安裝依賴
你應該注意到,這裡並沒有node_modules資料夾,方便給大家下發,已經把依賴都刪除了。不過package.json中依然定義了我們所需的一切依賴:
我們只需要開啟終端,進入專案目錄,輸入:npm install
命令,即可安裝這些依賴。
大概需要幾分鐘。
如果安裝過程出現以下問題:
建議刪除node_modules目錄,重新安裝。
1.3.執行一下看看
輸入命令:npm run dev
發現預設的埠是9001。訪問:http://localhost:9001
會自動進行跳轉:
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/
2.2.為什麼是Vuetify
有中國的為什麼還要用外國的?原因如下:
-
Vuetify幾乎不需要任何CSS程式碼,而element-ui許多佈局樣式需要我們來編寫
-
Vuetify從底層構建起來的語義化元件。簡單易學,容易記住。
-
Vuetify基於Material Design(谷歌推出的多平臺設計規範),更加美觀,動畫效果酷炫,且風格統一
這是官網的說明:
缺陷:
-
目前官網雖然有中文文件,但因為翻譯問題,幾乎不太能看。
2.3.怎麼用?
基於官方網站的文件進行學習:
我們重點關注UI components
即可,裡面有大量的UI元件,我們要用的時候再檢視,不用現在學習,先看下有什麼:
以後用到什麼元件,就來查詢即可。
3.專案結構
開始編碼前,我們先了解下專案的結構。
3.1.目錄結構
首先是目錄結構圖:
3.2.呼叫關係
我們最主要理清index.html、main.js、App.vue之間的關係:
理一下:
-
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元件是我們的整個頁面的佈局元件:
一個典型的三塊佈局。包含左,上,中三部分:
裡面使用了Vuetify中的2個元件和一個佈局元素:
-
v-navigation-drawer
:導航抽屜,主要用於容納應用程式中的頁面的導航連結。 -
v-toolbar
:工具欄通常是網站導航的主要途徑。可以與導航抽屜一起很好地工作,動態選擇是否開啟導航抽屜,實現可伸縮的側邊欄。 -
v-content
:並不是一個元件,而是標記頁面佈局的元素。可以根據您指定的app元件的結構動態調整大小,使得您可以建立高度可定製的元件。
那麼問題來了:v-content
中的內容來自哪裡?
-
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的工具,在課前資料中可以找到:
解壓,執行exe檔案,效果:
我們添加了兩個對映關係(中間用空格隔開):
-
127.0.0.1 api.leyou.com :我們的閘道器Zuul
-
127.0.0.1 manage.leyou.com:我們的後臺系統地址
現在,ping一下域名試試是否暢通:
OK!
通過域名訪問:
原因:我們配置了專案訪問的路徑,雖然manage.leyou.com對映的ip也是127.0.0.1,但是webpack會驗證host是否符合配置。
在webpack.dev.conf.js中取消host驗證:disableHostCheck: true
重新執行npm run dev
,重新整理瀏覽器:
OK!
4.4.nginx解決埠問題
域名問題解決了,但是現在要訪問後臺頁面,還得自己加上埠:http://manage.taotao.com:9001
。
這就不夠優雅了。我們希望的是直接域名訪問:http://manage.taotao.com
。這種情況下埠預設是80,如何才能把請求轉移到9001埠呢?
這裡就要用到反向代理工具:Nginx
nginx + tomcat
4.4.1.什麼是Nginx
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會根據已經定義的規則進行請求的轉發,從而實現路由功能
利用反向代理,就可以解決我們前面所說的埠問題,如圖
4.4.4.安裝和使用
安裝
安裝非常簡單,把課前資料提供的nginx直接解壓即可,綠色免安裝,舒服!
我們在本地安裝一臺nginx:
解壓後,目錄結構:
-
conf:配置目錄
-
contrib:第三方依賴
-
html:預設的靜態資源目錄,類似於tomcat的webapps
-
logs:日誌目錄
-
nginx.exe:啟動程式。可雙擊執行,但不建議這麼做。
反向代理配置
示例:
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程序:
4.5.測試
啟動nginx,然後用域名訪問後臺管理系統:
現在實現了域名訪問網站了,中間的流程是怎樣的呢?
-
瀏覽器準備發起請求,訪問http://mamage.leyou.com,但需要進行域名解析
-
優先進行本地域名解析,因為我們修改了hosts,所以解析成功,得到地址:127.0.0.1
-
請求被髮往解析得到的ip,並且預設使用80埠:http://127.0.0.1:80
本機的nginx一直監聽80埠,因此捕獲這個請求
-
nginx中配置了反向代理規則,將manage.leyou.com代理到127.0.0.1:9001,因此請求被轉發
-
後臺系統的webpack server監聽的埠是9001,得到請求並處理,完成後將響應返回到nginx
-
nginx將得到的結果返回到瀏覽器
5.實現商品分類查詢
商城的核心自然是商品,而商品多了以後,肯定要進行分類,並且不同的商品會有不同的品牌資訊,其關係如圖所示:
-
一個商品分類下有很多商品
-
一個商品分類下有很多品牌
-
而一個品牌,可能屬於不同的分類
-
一個品牌下也會有很多商品
因此,我們需要依次去完成:商品分類、品牌、商品的開發。
5.1.匯入資料
首先匯入課前資料提供的sql:
我們先看商品分類表:
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.頁面分析
首先我們看下要實現的效果:
商品分類之間是會有層級關係的,採用樹結構去展示是最直觀的方式。
一起來看頁面,對應的是/pages/item/Category.vue:
頁面模板:
<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中提供的元件,提供一個懸浮效果的面板,一般用來展示一組資料。 -
v-flex
:佈局容器,用來控制響應式佈局。與BootStrap的柵格系統類似,整個螢幕被分為12格。我們可以控制所佔的格數來控制寬度:
本例中,我們用sm10
控制在小螢幕及以上時,顯示寬度為10格
-
v-tree
:樹元件。Vuetify並沒有提供樹元件,這個是我們自己編寫的自定義元件:
裡面涉及一些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" />
重新整理頁面,可以看到:
頁面中的樹沒有了,並且發起了一條請求:http://localhost/api/item/category/list?pid=0
大家可能會覺得很奇怪,我們明明是使用的相對路徑,講道理髮起的請求地址應該是:
http://manage.leyou.com/item/category/list
但實際卻是:
http://api.leyou.com/api/item/category/list?pid=0
這是因為,我們有一個全域性的配置檔案,對所有的請求路徑進行了約定:
路徑是http://api.leyou.com,並且預設加上了/api的字首,這恰好與我們的閘道器設定匹配,我們只需要把地址改成閘道器的地址即可,因為我們使用了nginx反向代理,這裡可以寫域名。
接下來,我們要做的事情就是編寫後臺介面,返回對應的資料即可。
5.3.2.實體類
在leyou-item-interface
中新增category實體類:
內容:
@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的註解,因此我們在leyou-item-iterface
中新增jpa依賴
<dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> <version>1.0</version> </dependency>
5.3.3.controller
編寫一個controller一般需要知道四個內容:
-
請求方式:決定我們用GetMapping還是PostMapping
-
請求路徑:決定對映路徑
-
請求引數:決定方法的引數
-
返回值結果:決定方法的返回值
在剛才頁面發起的請求中,我們就能得到絕大多數資訊:
-
請求方式: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:
controller程式碼:
@Controller @RequestMapping("category") public class CategoryController { @Autowired private CategoryService categoryService; /** * 根據parentId查詢類目 * @param pid * @return */ 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層我們會定義介面和實現類,不過這裡我們就偷懶一下,直接寫實現類了:
@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
然後試試閘道器是否暢通:http://api.leyou.com/api/item/category/list
一切OK!
然後重新整理後臺管理頁面檢視:
發現報錯了!
瀏覽器直接訪問沒事,但是這裡卻報錯,什麼原因?
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-urlencoded
、multipart/form-data
、text/plain
當瀏覽器發現發起的ajax請求是簡單請求時,會在請求頭中攜帶一個欄位:Origin
.
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-Origin
和Access-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 LeyouCorsConfig { @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); } }
結構:
重啟測試,訪問正常:
分類的增刪改功能暫時就不做了,頁面已經預留好了事件介面,有興趣的同學可以完成一下。
7.從0開始品牌的查詢
商品分類完成以後,自然輪到了品牌功能了。
先看看我們要實現的效果:
接下來,我們從0開始,實現下從前端到後端的完整開發。
7.1.設計前端頁面
為了方便看到效果,我們新建一個MyBrand.vue(注意先停掉伺服器),從0開始搭建。
內容初始化一下:
<template> <span> hello </span> </template> <script> export default { name: "myBrand" } </script> <!-- scoped:當前樣式只作用於當前元件的節點 --> <style scoped> </style>
改變router新的index.js,將路由地址指向MyBrand.vue
開啟伺服器,再次檢視頁面:
乾乾淨淨了。只剩hello
7.1.1.查詢表格
大家看到這個原型頁面肯定能看出,其主體就是一個table。我們去Vuetify檢視有關table的文件:
仔細閱讀,發現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:當表格可選的時候,返回選中的行
我們向下翻,找找有沒有看起來牛逼的案例。
找到這樣一條:
其它的案例都是由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:資料載入完畢。
另外,在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.動手實現
表格中具體有哪些列呢?參照品牌表:
品牌中有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>
重新整理頁面檢視:
7.1.4.優化頁面
7.1.4.1.編輯和刪除按鈕
我們將來要對品牌進行增刪改,需要給每一行資料新增 修改刪除的按鈕,一般放到改行的最後一列。
其實就是多了一列,只是這一列沒有資料,而是兩個按鈕而已。可以在官方文件中找一個帶有操作按鈕的表格,作為參考。
我們先在頭(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>
效果:
7.1.4.2.新增按鈕
在官方文件中找到按鈕的用法:
因為新增跟某個品牌無關,是獨立的,因此我們可以放到表格的外面。
效果:
7.1.4.3.卡片(card)
為了不讓按鈕顯得過於孤立,我們可以將按新增按鈕
和表格
放到一張卡片(card)中。
我們去官網檢視卡片的用法:
卡片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>
效果:
7.1.4.4.新增搜尋框
我們還可以在卡片頭部新增一個搜尋框,其實就是一個文字輸入框。
檢視官網中,文字框的用法:
-
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