參考:https://blog.csdn.net/qq_31967569/article/details/91546294
目錄
1、對路由的理解?
2、路由的實作程序
3、前端路由的兩種實作方式:基于hash 和基于history
3.1 hash模式
3.2 history模式
3.3 兩種模式對比
4、Vue-Router 的懶加載如何實作
4.1 方案一(常用):使用箭頭函式+import動態加載
4.2 方案二:使用箭頭函式+require動態加載
4.3 方案三:使用webpack的require.ensure技術,也可以實作按需加載,
5、 如何定義動態路由?如何獲取傳過來的動態引數?
5.1 param方式
5.2 query方式
5.3 params和query的區別?
6、Vue-router 導航守衛
6.1 全域的守衛
6.2 路由獨享的守衛
6.3 組件內的守衛
7、對前端路由的理解
8、Vue-router跳轉和location.href有什么區別
1、對路由的理解?
路由,其實就是指向的意思,當我點擊頁面上的home按鈕時,頁面中就要顯示home的內容,如果點擊頁面上的about 按鈕,頁面中就要顯示about 的內容,Home按鈕 => home 內容, about按鈕 => about 內容,也可以說是一種映射. 所以在頁面上有兩個部分,一個是點擊部分,一個是點擊之后,顯示內容的部分,
點擊之后,怎么做到正確的對應,比如,我點擊home 按鈕,頁面中怎么就正好能顯示home的內容,這就要在js 檔案中配置路由,
路由中有三個基本的概念 route, routes, router:
- route(路由資訊物件):它是一條路由,由這個英文單詞可看出 它是單數,Home按鈕 => home內容, 這是一條route, about按鈕 => about 內容,這是另一條路由,route物件 {path:’/home’, component: home},包括 path,params,hash,query,fullPath,matched,name 等路由資訊引數;
- routes:是一組路由,把上面的每一條路由組合起來,形成一個陣列,[{home 按鈕 =>home內容 }, { about按鈕 => about 內容}];
- router(路由實體物件): 是一個機制,相當于一個管理者,它來管理路由,因為routes 只是定義了一組路由,它放在哪里是靜止的,當真正來了請求,怎么辦? 就是當用戶點擊home 按鈕的時候,怎么辦?這時router 就起作用了,它到routes 中去查找,去找到對應的 home 內容,所以頁面中就顯示了 home 內容,包括了路由的跳轉方法,鉤子函式等,
- 客戶端中的路由:實際上就是dom 元素的顯示和隱藏,直接找到與地址匹配的一個組件或物件并將其渲染出來,改變瀏覽器地址而不向服務器發出請求,當頁面中顯示home 內容的時候,about 中的內容全部隱藏,反之也是一樣,客戶端路由有兩種實作方式:基于hash 和基于history
2、路由的實作程序
在vue中實作路由還是相對簡單的,因為頁面中所有內容都是組件化的,我們只要把路徑和組件對應起來就可以了,然后在頁面中把組件渲染出來,
- <router-link>:對應點擊部分,to屬性表示定義點擊之后,要到哪里去,例 點擊Home后,跳轉到/home :<router-link to="/home">Home</router-link>
- <router-view>:對應顯示部分,其實它就是一個占位符,它在什么地方,匹配路徑的組件就顯示在什么地方,<router-view></router-view>
<template>
<div id="app">
<header>
<!-- router-link 定義點擊后導航到哪個路徑下 -->
<router-link to="/home">Home</router-link>
<router-link to="/about">About</router-link>
</header>
<!-- 對應的組件內容渲染到router-view中 -->
<router-view></router-view>
</div>
</template>
(1)在js中配置路由,
首先要定義route物件, 一條路由的實作,由兩個部分組成: 路徑path和對應的組件component. 如:route物件 {path:’/home’, component: home},兩條路由,組成一個routes:
const routes = [
{ path: '/home', component: Home }, //訪問路徑/home時,顯示Home組件
{ path: '/about', component: About } //訪問路徑/about時,顯示About組件
]
(2)創建router 對路由進行管理,它由建構式 new vueRouter() 創建,接受routes 引數
const router = new VueRouter({
routes // routes: routes 的簡寫
})
(3)配置完成后,把router 實體注入到 vue 根實體中,就可以使用路由了
const app = new Vue({
router
}).$mount('#app')
執行程序:
- 當用戶點擊 router-link 標簽時,會去尋找它的 to 屬性;
- 它的 to 屬性和 js 中配置的路徑 { path: '/home', component: Home} 對應,從而找到了匹配的組件;
- 最后把組件渲染到 <router-view> 標簽所在的地方,
3、前端路由的兩種實作方式:基于hash 和基于history
3.1 hash模式
(1)簡介:開發中默認的模式,它的URL帶著一個#,例如:www.abc.com/#/vue,它的hash值就是#/vue;“www.baidu.com/#/hashhash” ,它的hash值就是 “#/hashhash” ,(此 hash 不是密碼學里的散列運算)
(2)特點:hash值會出現在URL里面,但是不會出現在HTTP請求中,對后端完全沒有影響,所以改變hash值,不會重新加載頁面,這種模式的瀏覽器支持度很好,低版本的IE瀏覽器也支持這種模式,hash路由被稱為是前端路由,已經成為SPA(單頁面應用)的標配,
(3)原理: hash模式的主要原理就是onhashchange()事件,使用hash模式時,我們可以考慮對每個路由注冊一個映射函式,頁面加載時,在window注冊onhashchange事件的監聽函式,每次切換url,就呼叫對應的映射函式
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
}
觸發hash變化的方式主要有兩種:
- 通過a標簽,并為a設定href屬性,當用戶點擊這個標簽后,URL就會發生改變,觸發onhashchange 事件,達到切換路由的目的;
- 直接使用JavaScript來對loaction.hash進行賦值,從而改變URL,觸發onhashchange事件,
使用onhashchange()事件的好處就是,
- 在頁面的hash值發生變化時,無需向后端發起請求,window就可以監聽事件的改變,并按規則加載相應的代碼;
- hash值變化對應的URL都會被瀏覽器記錄下來,這樣瀏覽器就能實作頁面的前進和后退,雖然是沒有請求后端服務器,但是頁面的hash值和對應的URL關聯起來了,
(4)如何獲取頁面的hash變化
1)通過watch 監聽$route的變化
// 監聽,當路由發生變化的時候執行
watch: {
$route: {
handler: function(val, oldVal){
console.log(val);
},
// 深度觀察監聽
deep: true
}
},
2)window.location.hash讀取#值 ,window.location.hash 的值可讀可寫,讀取來判斷狀態是否改變,寫入時可以在不多載網頁的前提下,添加一條歷史訪問記錄 ,
3.2 history模式
(1)簡介:history模式的URL中沒有#,它使用的是傳統的路由分發模式,即用戶在輸入一個URL時,服務器會接收這個請求,并決議這個URL,然后做出相應的邏輯處理,
(2)特點:當使用history模式時,URL就像這樣:abc.com/user/id,相比hash模式更加好看,但是,history模式需要后臺配置支持,如果后臺沒有正確配置,訪問時會回傳404,所以,需要在服務端增加一個覆寫所有情況的候選資源:如果 URL 匹配不到任何靜態資源,則應該回傳單頁應用的 html 檔案,
(3)History主要的API分為兩大部分,切換歷史狀態和修改歷史狀態:
- 修改歷史狀態:包括了 HTML5 History Interface 中新增的
pushState()和replaceState()方法,這兩個方法應用于瀏覽器的歷史記錄堆疊,提供了對歷史記錄進行修改的功能,只是當他們修改了url時,但瀏覽器不會立即向后端發送請求(即不進行重繪),如果要做到改變url但又不重繪頁面的效果,就需要前端用上這兩個API,
- pushState() : 新增一個歷史記錄
replaceState(): 直接替換當前的歷史記錄
這兩個api都接受三個引數:
window.history.pushState(state,title, "http://www.163.com");
- state:合法的 Javascript 物件,可以用在 popstate 事件中;
- title:現在大多瀏覽器忽略這個引數,可以直接用 null 代替;
- url:任意有效的 URL,用于更新瀏覽器的地址欄,
- 切換歷史狀態: 包括
forward()、back()、go()三個方法,對應瀏覽器的前進,后退,跳轉操作,
(4)對于單頁應用的 history 模式而言,url 的改變有三種方式:
- 點擊瀏覽器的前進或后退按鈕
- 點擊 a 標簽
- 在 JS 中主動觸發 history.pushState 和replaceState函式,
方式1,3都能觸發popstate ,所以我們只需要對a標簽處理,通過對取消a標簽的默認行為,然后呼叫pushState來觸發路徑變化,
(5)雖然history模式丟棄了丑陋的#,但是,它也有自己的缺點,就是在重繪頁面的時候,如果沒有相應的路由或資源,就會刷出404來,
如果想要切換到history模式,就要進行以下配置(后端也要進行配置):
const router = new VueRouter({
mode: 'history',
routes: [...]
})
3.3 兩種模式對比
呼叫 history.pushState() 相比于直接修改 hash,存在以下優勢:
- pushState() 設定的新 URL 可以是與當前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能設定與當前 URL 同檔案的 URL;
- pushState() 設定的新 URL 可以與當前 URL 一模一樣,這樣也會把記錄添加到堆疊中;而 hash 設定的新值必須與原來不一樣才會觸發動作將記錄添加到堆疊中;
- pushState() 通過 stateObject 引數可以添加任意型別的資料到記錄中;而 hash 只可添加短字串;
- pushState() 可額外設定 title 屬性供后續使用,
- hash模式下,僅hash符號之前的url會被包含在請求中,后端如果沒有做到對路由的全覆寫,也不會回傳404錯誤;history模式下,前端的url必須和實際向后端發起請求的url一致,如果沒有對用的路由處理,將回傳404錯誤,
hash模式和history模式都有各自的優勢和缺陷,還是要根據實際情況選擇性的使用,
4、Vue-Router 的懶加載如何實作
Vue 是單頁面應用,可能會有很多的路由引入 ,這樣使用 webpcak 打包后的檔案很大,當進入首頁時,加載的資源過多,頁面會出現白屏的情況,不利于用戶體驗,而運用懶加載則可以將頁面進行劃分,需要的時候加載頁面,可以有效的分擔首頁所承擔的加載壓力,這樣就更加高效了,這樣會大大提高首屏顯示的速度,但是可能其他的頁面的速度就會降下來,
4.1 方案一(常用):使用箭頭函式+import動態加載
import通過這種特殊注釋語法,將幾個路由放到一個組中 :
// 這三個組件分為一組:login_home_welcome
const Login = () => import(/* webpackChunkName: "login_home_welcome" */ '@/components/Login.vue')
const Home = () => import(/* webpackChunkName: "login_home_welcome" */ '@/components/Home.vue')
const Welcome = () => import(/* webpackChunkName: "login_home_welcome" */ '@/components/Welcome.vue')
const router = new Router({
routes: [
{ path: '/login', component: Login },
{ path: '/home', component: Home },
{ path: '/welcome', component: Welcome }
]
(1)安裝開發依賴@babel/plugin-syntax-dynamic-import

(2)在組態檔babel.config.js中宣告該插件

(3)將路由改為按需加載的形式

其中 import(/*分組名:劃分大同一分組的多個組件*/ 路由組件)

4.2 方案二:使用箭頭函式+require動態加載
const router = new Router({
routes: [
{
path: '/list',
component: resolve => require(['@/components/list'], resolve)
}
]
})
4.3 方案三:使用webpack的require.ensure技術,也可以實作按需加載,
這種情況下,多個路由指定相同的chunkName,會合并打包成一個js檔案
//require.ensure(當前需要進來的模塊的一些依賴, 回呼函式 動態引入其他模塊, 打包的組塊名稱)
const Foo = resolve => require.ensure([], () => resolve(require('./Foo.vue')), 'group-foo')
const Bar = resolve => require.ensure([], () => resolve(require('./Bar.vue')), 'group-foo')
const Baz = resolve => require.ensure([], () => resolve(require('./Baz.vue')), 'group-foo')
require.ensure(
dependencies:String [],
callback:function(require),
errorCallback:function(error),
chunkName:String
)
require.ensure()接受四個引數:
- 第一個引數:表依賴關系,是一個陣列,代表了當前需要進來的模塊的一些依賴;
- 第二個引數: 是一個回呼函式,這個有一個引數要求,通過回呼函式的require可以動態引入組件,雖然require是回呼函式的引數,但是其名稱實際上是不能換的,否則WebPack就無法靜態分析的時候處理它;
- 第三個引數:errorCallback比較好理解,就是處理錯誤的回呼;
- 第四個引數:chunkName則是指定打包的組塊名稱,
5、 如何定義動態路由?如何獲取傳過來的動態引數?
5.1 param方式
- 配置路由格式:使用冒號:系結動態引數,
/user/:userid
const routes = [{ //配置路由的index.js檔案
path: '/user/:userid',
name: 'users',
component: User
}]
- 傳遞的方式:在path后面跟上對應的值 '/user/'+id
<router-link :to="'/user/'+id" >用戶</router-link>
- 傳遞后形成的路徑:
/router/123
(1)路由定義
const routes = [{ //配置路由的index.js檔案
path: '/user/:userid',
name: 'users',
component: User
}]
(2)路由跳轉
1)方法一 使用router-link實作路由跳轉:使用字串,在路徑后面直接跟上對應的值
屬性名userId使用自己的id,不必與路由配置的路徑中一致
<router-link :to="'/user/'+userId" replace>用戶</router-link>
2)方法二 使用router-link實作路由跳轉:使用物件的方式,
name: 'users' 的屬性名和屬性值 必須跟配置路由中的一致
params中的屬性名userid 必須與配置路由path中的'/user/:userid'一致
<router-link :to="{ name: 'users', params: { userid: userId}}">用戶</router-link>
配置路由的index.js檔案中,需定義name屬性,才可使用方法二
3)方法三:使用$router的方式進行路由的跳轉
this.$router.push('/user/' + this.userId)
// 或
this.$router.push({
name: 'users', //屬性名和屬性值 必須與路由一致
params: { // id屬性名 必須與路由一致
userid: this.userId
}
})
配置路由的index.js檔案中,需定義name屬性,才可使用方法三
(3)引數獲取
1)方式一:通過 $route.params.userid 獲取傳遞的值(userid 表要查詢的引數),
2)方式二:組件中也可以用props來接受引數,前提是需要在路由配置中設定props為true,
const routes = [{ //配置路由的index.js檔案
path: '/user/:userid',
name: 'users',
component: User,
//ture,代表將path后面的引數作為值,傳遞到組件中,組件中通過props屬性接受這個值
props:true
}]
在組件中通過props接收引數
<script> // 組件中
export default {
props:{
name:{
type:String,
default:'lily' //默認情況
},
id:{
type:Number,
default:'0' //默認情況
}
}
}
</script>
3)$router 和 $route的區別
- $router : 是路由操作物件,只寫物件
- $route : 路由資訊物件,只讀物件
5.2 query方式
- 配置路由格式:
/router,也就是普通配置
const routes = [{ //配置路由的index.js檔案
path: '/user/:userid',
name: 'users',
component: User
}]
- 傳遞的方式:物件中使用query的key作為傳遞方式,
注意:query傳參的方式只可以通過物件,不可以使用字串,
里面的屬性名(如本例中的id)可以隨便起名,不像params方式傳參時受限,
<router-link :to="{path:'/user',query:{id:18, name:'sh'}">用戶</router-link>
- 傳遞后形成的路徑:
/route?id=18 & name=sh,引數之間用&符號連接
localhost:8080/user?id=18 & name=sh
(1)路由定義(普通的路由配置)
const routes = [{ //配置路由的index.js檔案
path: '/user/:userid',
name: 'users',
component: User
}]
(2)路由跳轉
1)方法一 使用router-link方式實作路由跳轉:使用name
屬性名name 和 其屬性值 需與路由配置中的 name: 'users' 一致
<router-link :to="{ name: 'users', query: { uname: james }}">按鈕</router-link>
2)方法二 使用router-link方式實作路由跳轉:使用path
<router-link :to="{ path: '/user', query: { uname:james }}">按鈕</router-link>
3)方法三 使用$router的方式進行路由的跳轉
// 方式1 使用name
this.$router.push({ name: 'users', query:{ uname:james }})
// 方式2 使用路徑
this.$router.push({ path: '/user', query:{ uname:james }})
// 方式3 直接將傳的參添加到路徑后
this.$router.push('/user?uname=' + jsmes)
(3)引數獲取
1)方式一:通過 $route.query.userid 獲取傳遞的值(userid 表要查詢的引數),
2)方式二:組件中也可以用props來接受引數,前提是需要在路由配置中設定props為true,
與params中的方式二一樣,
5.3 params和query的區別?
- params方式路由的引入只能用name;query方式路由的引入可以用name和path,
params的引數是URL不可或缺的一部分,一定要加路由后面添加引數path: '/user/:userid',不添加重繪頁面資料會丟失;而query是拼接在url后面的引數,路由后面不添加也沒關系,- params 在瀏覽器地址欄中不顯示引數;query則顯示,params相當于post請求,引數不會在地址欄中顯示;query相當于get請求,頁面跳轉的時候,可以在地址欄看到請求引數,
6、Vue-router 導航守衛
- 全域的守衛:beforeEach(全域前置)、beforeResolve(全域決議)、afterEach(全域后置);寫在router.js 檔案中;
- 路由獨享的守衛:beforeEnter;寫在router單個路由中;
- 組件內的守衛:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave;寫在組件中,
優先級:①>②>③,組件內的守衛優先級是最低的
6.1 全域的守衛
(1)beforeEach(全域前置守衛)
對用于訪問網頁的情況進行相應的跳轉,例如初次登入,不能訪問除登錄頁面外的頁面,已登錄會自動跳轉自首頁等等
const router = new VueRouter({...})
//使用router.beforeEach注冊一個全域前置守衛
router.beforeEach((to,from,next) =>{
//...
})
- to: 將要訪問的路徑;
- from:當前導航正要離開的路由;
- next 是一個函式,表示放行, next() 放行、next('/login') 強制跳轉到到該路徑, 確保要呼叫next方法,否則鉤子就不會被resolved ,
(2)beforeResolve(全域決議)
這和router.beforeEach類似,區別是在導航被確認之前,同時在所有組件內守衛和異步路由組件被決議之后,決議守衛就被呼叫
(3)afterEach(全域后置)
不同的是,后置守衛不會接收next函式,也不會改變導航本身
router.afterEach((to,from)=>{
//...
})
6.2 路由獨享的守衛
單個路由獨享的守衛,可以在單個路由中直接定義(方法、引數等都和全域前置守衛beforeEach一樣)
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}]})
6.3 組件內的守衛
都有三個引數:to, from, next
- beforeRouteEnter:路由進入之前呼叫,不能獲取組件實體 `this` ,因為當守衛執行前,組件實體還沒被創建,
- beforeRouteUpdate:路由更新呼叫,在當前路由改變,但是該組件被復用時呼叫,
- beforeRouteLeave:導航離開該組件的對應路由時呼叫,用來禁止用戶在還未保存修改前突然離開,
watch: {
$route(to, from) {
//當使用路由引數時,例如從 /user/foo 導航到 /user/bar,原來的組件實體會被復用,因為兩個路由都渲染同個組件,比起銷毀再創建,復用則顯得更加高效,不過,這也意味著組件的生命周期鉤子不會再被呼叫,復用組件時,想對路由引數的變化作出回應的話,你可以簡單地 watch (監測變化) $route 物件
// 對路由變化作出回應...
console.log(" // 對路由變化作出回應...");
}
}, //監聽
beforeRouteEnter(to, from, next) {
//路由進入之前呼叫
// 在渲染該組件的對應路由被 confirm 前呼叫
// 不!能!獲取組件實體 `this`
// 因為當守衛執行前,組件實體還沒被創建
//console.log(from);//上一個激活的路由
//console.log(to);//當前路由
next(vm => {
// 通過 `vm` 訪問組件實體
console.log(vm)//當前激活的路由對應的組件物件
});
// next();
},
beforeRouteUpdate(to, from, next) {
//路由更新呼叫
// 在當前路由改變,但是該組件被復用時呼叫
// 舉例來說,對于一個帶有動態引數的路徑 /foo/:id,在 /foo/1 和 /foo/2 之間跳轉的時候,
// 由于會渲染同樣的 Foo 組件,因此組件實體會被復用,而這個鉤子就會在這個情況下被呼叫,
// 可以訪問組件實體 `this`
// console.log(to);
next();
},
beforeRouteLeave(to, from, next) {
//這個離開守衛通常用來禁止用戶在還未保存修改前突然離開,該導航可以通過 next(false) 來取消
// 導航離開該組件的對應路由時呼叫
// 可以訪問組件實體 `this`
//to是將要去的路由
//from是將要離開的路由(也就是當前的路由)
const answer = window.confirm(
"Do you really want to leave? you have unsaved changes!"
);
if (answer) {
next();
} else {
next(false);
}
},
補充:鉤子函式
- 是個函式,在系統訊息觸發時被系統呼叫 ,在系統級對所有訊息、事件進行過濾;
- 不是用戶自己觸發的
7、對前端路由的理解
在前端技術早期,一個 url 對應一個頁面,如果要從 A 頁面切換到 B 頁面,那么必然伴隨著頁面的重繪,這個體驗并不好,不過在最初也是無奈之舉——用戶只有在重繪頁面的情況下,才可以重新去請求資料,
后來,改變發生了——Ajax 出現了,它允許人們在不重繪頁面的情況下發起請求;與之共生的,還有“不重繪頁面即可更新頁面內容”這種需求,在這樣的背景下,出現了 SPA(單頁面應用),
SPA極大地提升了用戶體驗,它允許頁面在不重繪的情況下更新頁面內容,使內容的切換更加流暢,但是在 SPA 誕生之初,人們并沒有考慮到“定位”這個問題——在內容切換前后,頁面的 URL 都是一樣的,這就帶來了兩個問題:
- SPA 其實并不知道當前的頁面“進展到了哪一步”,可能在一個站點下經過了反復的“前進”才終于喚出了某一塊內容,但是此時只要重繪一下頁面,一切就會被清零,必須重復之前的操作、才可以重新對內容進行定位——SPA 并不會“記住”你的操作,
- 由于有且僅有一個 URL 給頁面做映射,這對 SEO 也不夠友好,搜索引擎無法收集全面的資訊
為了解決這個問題,前端路由出現了,
前端路由可以幫助我們在僅有一個頁面的情況下,“記住”用戶當前走到了哪一步——為 SPA 中的各個視圖匹配一個唯一標識,這意味著用戶前進、后退觸發的新內容,都會映射到不同的 URL 上去,此時即便他重繪頁面,因為當前的 URL 可以標識出他所處的位置,因此內容也不會丟失,
那么如何實作這個目的呢?首先要解決兩個問題:
- 當用戶重繪頁面時,瀏覽器會默認根據當前 URL 對資源進行重新定位(發送請求),這個動作對 SPA 是不必要的,因為我們的 SPA 作為單頁面,無論如何也只會有一個資源與之對應,此時若走正常的請求-重繪流程,反而會使用戶的前進后退操作無法被記錄,
- 單頁面應用對服務端來說,就是一個URL、一套資源,那么如何做到用“不同的URL”來映射不同的視圖內容呢?
從這兩個問題來看,服務端已經完全救不了這個場景了,所以要靠咱們前端自力更生,不然怎么叫“前端路由”呢?作為前端,可以提供這樣的解決思路:
- 攔截用戶的重繪操作,避免服務端盲目回應、回傳不符合預期的資源內容,把重繪這個動作完全放到前端邏輯里消化掉,
- 感知 URL 的變化,這里不是說要改造 URL、憑空制造出 N 個 URL 來,而是說 URL 還是那個 URL,只不過我們可以給它做一些微小的處理——這些處理并不會影響 URL 本身的性質,不會影響服務器對它的識別,只有我們前端感知的到,一旦我們感知到了,我們就根據這些變化、用 JS 去給它生成不同的內容,
8、Vue-router跳轉和location.href有什么區別
- 使用
location.href= /url來跳轉,簡單方便,但是重繪了頁面; - 使用
history.pushState( /url ),無重繪頁面,靜態跳轉; - 引進 router ,然后使用
router.push( /url )來跳轉,使用了diff演算法,實作了按需加載,減少了 dom 的消耗,其實使用 router 跳轉和使用history.pushState()沒什么差別的,因為vue-router就是用了history.pushState(),尤其是在history模式下,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/286781.html
標籤:其他
