詳解express使用vue-router的history踩坑
vue-router 默認(rèn) hash 模式 —— 使用 URL 的 hash 來(lái)模擬一個(gè)完整的 URL,于是當(dāng) URL 改變時(shí),頁(yè)面不會(huì)重新加載。
如果不想要很丑的 hash,我們可以用路由的 history 模式,這種模式充分利用 history.pushState API 來(lái)完成 URL 跳轉(zhuǎn)而無(wú)須重新加載頁(yè)面。
當(dāng)你使用 history 模式時(shí),URL 就像正常的 url,例如 yoursite.com/user/id,也好看…
個(gè)人理解
上面是官方的解釋?zhuān)臋n的一貫風(fēng)格,只給懂的人看。兩年前我比現(xiàn)在還菜的時(shí)候,看了這段話(huà)表示他在說(shuō)個(gè)錘子,直接跳過(guò)了。
我不講:hammer:,直接舉:chestnut:
一般的我們把項(xiàng)目放到服務(wù)器上,路由都是在服務(wù)器中設(shè)置的。
比如網(wǎng)站 https://www.text.com/ 中 admin目錄下有一個(gè) login.html 的頁(yè)面。當(dāng)用戶(hù)輸入 https://www.text.com/admin/login ,先解析 www.text.com 域名部分得到服務(wù)器 ip 和 端口號(hào),根據(jù) ip 和 端口號(hào)找到對(duì)應(yīng)的服務(wù)器中的對(duì)應(yīng)的程序,然后在程序解析 /admin/login 路徑知道了你要找的是 admin 目錄下的 login.html 頁(yè)面,然后就返回給你這個(gè)頁(yè)面。
這是正常的方式,服務(wù)器控制一個(gè)路由指向一個(gè)頁(yè)面的文件(不考慮重定向的情況),這樣我們的項(xiàng)一般有多少個(gè)頁(yè)面就有多少個(gè) html 文件。
而 vue 中,我們打包好的文件其實(shí)是只有一個(gè) index.html ,所有的行為都是在這一個(gè)頁(yè)面上完成。用戶(hù)的所有的路由其實(shí)都是在請(qǐng)求 index.html 頁(yè)面。
假設(shè)承載 vue 項(xiàng)目 index.html 也是在 admin 目錄下,vue 項(xiàng)目中也有一個(gè) login 頁(yè)面,那對(duì)應(yīng)的url就是 https://www.text.com/admin/#/login 。
這個(gè) url 由三部分組成,是 www.text.com 是域名, /admin 是項(xiàng)目所在目錄,和上面一樣這個(gè)解析工作是由服務(wù)器完成的,服務(wù)器解析出 /admin 的路由,就返回給你 index.html 。 /#/login 是 vue-router 模擬的路由,因?yàn)轫?yè)面所有的跳轉(zhuǎn) vue 都是在 index.html 中完成的,所以加上 # 表示頁(yè)內(nèi)切換。假設(shè)切換到 home 頁(yè)面,對(duì)應(yīng)的 html 文件還是 index.html ,url 變成 https://www.text.com/admin/#/home ,vue-router 判斷到 /#/home 的改變而改變了頁(yè)面 dom 元素,從而給用戶(hù)的感覺(jué)是頁(yè)面跳轉(zhuǎn)了。這就是 hash 模式。
那我們就知道了,正常的 url 和 hash 模式的區(qū)別,頁(yè)面的 js 代碼沒(méi)辦法獲取到服務(wù)器判斷路由的行為,所以只能用這種方式實(shí)現(xiàn)路由的功能。
而 history 模式就是讓 vue 的路由和正常的 url 一樣,至于怎么做下文會(huì)說(shuō)到。
為什么需要實(shí)現(xiàn)
說(shuō)怎么做之前,先說(shuō)說(shuō)為什么需要 history 模式。官方文檔說(shuō)了,這樣比較好看。emmmmmm,對(duì)于直接面向消費(fèi)者的網(wǎng)站好看這個(gè)確實(shí)是個(gè)問(wèn)題,有個(gè) /# 顯得不夠大氣。對(duì)于企業(yè)管理的 spa 這其實(shí)也沒(méi)什么。
所以除了好看之外,history 模式還有其他優(yōu)勢(shì)。
我們知道,如果頁(yè)面使用錨點(diǎn),就是一個(gè) <a> 標(biāo)簽, <a href='#mark1'></a> ,點(diǎn)擊之后如果頁(yè)面中有 id 為 mark1 的標(biāo)簽會(huì)自動(dòng)滾動(dòng)到對(duì)應(yīng)的標(biāo)簽,而 url 后面會(huì)加上 #mark .
問(wèn)題就出在這里,使用 hash 模式, #mark 會(huì)替換掉 vue-router 模擬的路由。比如這個(gè) <a> 標(biāo)簽是在上面說(shuō)的 login 頁(yè)面,點(diǎn)擊之后 url 會(huì)從 https://www.text.com/admin/#/login 變成 https://www.text.com/admin/#/mark 。wtf???正常看來(lái)問(wèn)題不大,錨點(diǎn)滾動(dòng)嘛,實(shí)在不行可以 js 模擬,但是因?yàn)槲乙獙?shí)現(xiàn) markdown 的標(biāo)題導(dǎo)航功能,這個(gè)功能是插件做好的,究竟該插件還是用 history 。 權(quán)衡利弊下還是使用 history 模式工作量小,而且更美。
怎么做
既然知道是什么,為什么,下面就該研究怎么做了。
官方文檔里有“詳盡”的說(shuō)明,其實(shí)這事兒本來(lái)不難,原理也很簡(jiǎn)單。通過(guò)上文我們知道 vue-router 采用 hash 模式最大的原因在于所有的路由跳轉(zhuǎn)都是 js 模擬的,而 js 無(wú)法獲取服務(wù)器判斷路由的行為,那么就需要服務(wù)器的配合。原理就是無(wú)論用戶(hù)輸入的路由是什么全都指向 index.html 文件,然后 js 根據(jù)路由再進(jìn)行渲染。
按照官方的做法,前端 router 配置里面加一個(gè)屬性,如下
const router = new VueRouter({ mode: 'history', routes: [...] })
后端的我不一一贅述,我用的是express,所以直接用了 connect-history-api-fallback 中間件。(中間件地址 https://github.com/bripkens/connect-history-api-fallback)
const history = require('connect-history-api-fallback') app.use(history({ rewrites: [ { from: /^\/.*$/, to: function (context) { return "/"; } }, ] })); app.get('/', function (req, res) { res.sendFile(path.join(process.cwd(), "client/index.html")); }); app.use( express.static( path.join(process.cwd(), "static"), { maxAge: 0,//暫時(shí)關(guān)掉cdn } ) );
坑1
按道理來(lái)說(shuō)這樣就沒(méi)問(wèn)題了,然鵝放到服務(wù)器里面之后,開(kāi)始出幺蛾子了。靜態(tài)文件加載的時(shí)候接口返回都是
We're sorry but client doesn't work properly without JavaScript enabled. Please enable it to continue.
看著字面意思,說(shuō)我的項(xiàng)目(項(xiàng)目名client)沒(méi)有啟用 JavaScript ,莫名其妙完全不能理解。于是乎仔細(xì)比對(duì)控制臺(tái) responses headers 和request headers ,發(fā)現(xiàn)了一些貓膩,請(qǐng)求頭的 accept 和響應(yīng)頭的 content-type 對(duì)不上,請(qǐng)求 css 文件請(qǐng)求頭的 accept 是text/css,響應(yīng)頭的 content-type 是 text/html。這個(gè)不應(yīng)該請(qǐng)求什么響應(yīng)什么嗎,我想要崔鶯鶯一樣女子做老婆,給我個(gè)杜十娘也認(rèn)了,結(jié)果你給我整個(gè)潘金蓮讓我咋整。
完全不知道到底哪里出了問(wèn)題,google上面也沒(méi)有找到方法。開(kāi)始瞎琢磨,既然對(duì)不上,那就想我手動(dòng)給對(duì)上行不行。在express.static 的 setHeaders 里面檢查讀取文件類(lèi)型,然后根據(jù)文件類(lèi)型手動(dòng)設(shè)置mime type,我開(kāi)始佩服我的機(jī)智。
app.use( express.static( path.join(process.cwd(), "static"), { maxAge: 0, setHeaders(res,path){ // 通過(guò) path 獲取文件類(lèi)型,設(shè)置對(duì)應(yīng)文件的 mime type。 } } ) );
緩存時(shí)間設(shè)置為0,關(guān)掉CDN... 一頓操作, 發(fā)現(xiàn)不執(zhí)行 setHeaders 里面的方法。這個(gè)時(shí)候已經(jīng)晚上 11 點(diǎn)了,我已經(jīng)絕望了,最后一次看了一遍 connect-history-api-fallback 的文檔,覺(jué)得 htmlAcceptHeaders 這個(gè)配置項(xiàng)這么違和,其他的都能明白啥意思,就這個(gè)怎么都不能理解,死馬當(dāng)活馬醫(yī)扔進(jìn)代碼試試,居然成了。
const history = require('connect-history-api-fallback') app.use(history({ htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'] rewrites: [ { from: /^\/.*$/, to: function (context) { return "/"; } }, ] }));
到底誰(shuí)寫(xiě)的文檔,靜態(tài)文件的 headers 的 accepts 和 htmlAcceptHeaders 有什么關(guān)系。咱也不知道,咱也沒(méi)地方問(wèn)。這事兒耽誤了我大半天的時(shí)間,不研究透了心里不舒服。老規(guī)矩,看 connect-history-api-fallback 源碼。
'use strict'; var url = require('url'); exports = module.exports = function historyApiFallback(options) { options = options || {}; var logger = getLogger(options); return function(req, res, next) { var headers = req.headers; if (req.method !== 'GET') { logger( 'Not rewriting', req.method, req.url, 'because the method is not GET.' ); return next(); } else if (!headers || typeof headers.accept !== 'string') { logger( 'Not rewriting', req.method, req.url, 'because the client did not send an HTTP accept header.' ); return next(); } else if (headers.accept.indexOf('application/json') === 0) { logger( 'Not rewriting', req.method, req.url, 'because the client prefers JSON.' ); return next(); } else if (!acceptsHtml(headers.accept, options)) { logger( 'Not rewriting', req.method, req.url, 'because the client does not accept HTML.' ); return next(); } var parsedUrl = url.parse(req.url); var rewriteTarget; options.rewrites = options.rewrites || []; for (var i = 0; i < options.rewrites.length; i++) { var rewrite = options.rewrites[i]; var match = parsedUrl.pathname.match(rewrite.from); if (match !== null) { rewriteTarget = evaluateRewriteRule(parsedUrl, match, rewrite.to, req); if(rewriteTarget.charAt(0) !== '/') { logger( 'We recommend using an absolute path for the rewrite target.', 'Received a non-absolute rewrite target', rewriteTarget, 'for URL', req.url ); } logger('Rewriting', req.method, req.url, 'to', rewriteTarget); req.url = rewriteTarget; return next(); } } var pathname = parsedUrl.pathname; if (pathname.lastIndexOf('.') > pathname.lastIndexOf('/') && options.disableDotRule !== true) { logger( 'Not rewriting', req.method, req.url, 'because the path includes a dot (.) character.' ); return next(); } rewriteTarget = options.index || '/index.html'; logger('Rewriting', req.method, req.url, 'to', rewriteTarget); req.url = rewriteTarget; next(); }; }; function evaluateRewriteRule(parsedUrl, match, rule, req) { if (typeof rule === 'string') { return rule; } else if (typeof rule !== 'function') { throw new Error('Rewrite rule can only be of type string or function.'); } return rule({ parsedUrl: parsedUrl, match: match, request: req }); } function acceptsHtml(header, options) { options.htmlAcceptHeaders = options.htmlAcceptHeaders || ['text/html', '*/*']; for (var i = 0; i < options.htmlAcceptHeaders.length; i++) { if (header.indexOf(options.htmlAcceptHeaders[i]) !== -1) { return true; } } return false; } function getLogger(options) { if (options && options.logger) { return options.logger; } else if (options && options.verbose) { return console.log.bind(console); } return function(){}; }
這個(gè)代碼還真是通俗易懂,就不去一行行分析了(其實(shí)是我懶)。直接截取關(guān)鍵代碼:
else if (!acceptsHtml(headers.accept, options)) { logger( 'Not rewriting', req.method, req.url, 'because the client does not accept HTML.' ); return next(); }
function acceptsHtml(header, options) { //在這里 options.htmlAcceptHeaders = options.htmlAcceptHeaders || ['text/html', '*/*']; for (var i = 0; i < options.htmlAcceptHeaders.length; i++) { if (header.indexOf(options.htmlAcceptHeaders[i]) !== -1) { return true; } } return false; }
前一段代碼,如果 acceptsHtml 函數(shù)返回 false,說(shuō)明瀏覽器不接受 html 文件,跳過(guò)執(zhí)行 next(),否則繼續(xù)執(zhí)行。
后一段代碼, acceptsHtml 函數(shù)內(nèi)部設(shè)置 htmlAcceptHeaders 的默認(rèn)值是 'text/html', '*/*' 。判斷請(qǐng)求頭的accept,如果匹配上說(shuō)明返回true,否則返回false。直接用默認(rèn)值接口不能正常返回 css 和 js, 改成 'text/html', 'application/xhtml+xml' 就能運(yùn)行了。這就奇了怪了,htmlAcceptHeaders 為什么會(huì)影響 css 和 js。太晚了,不太想糾結(jié)了,簡(jiǎn)單粗暴把源碼摳出來(lái)直接放到項(xiàng)目里面跑一下,看看到底發(fā)生了什么。
function acceptsHtml(header, options) { options.htmlAcceptHeaders = options.htmlAcceptHeaders || ['text/html', '*/*']; console.log("header", header); console.log("htmlAcceptHeaders", options.htmlAcceptHeaders); for (var i = 0; i < options.htmlAcceptHeaders.length; i++) { console.log("indexOf", header.indexOf(options.htmlAcceptHeaders[i])); if (header.indexOf(options.htmlAcceptHeaders[i]) !== -1) { return true; } } return false; }
設(shè)置 htmlAcceptHeaders 值為 'text/html', 'application/xhtml+xml'
header text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 htmlAcceptHeaders [ 'text/html', 'application/xhtml+xml' ] indexOf 0 header text/css,*/*;q=0.1 htmlAcceptHeaders [ 'text/html', 'application/xhtml+xml' ] indexOf -1 indexOf -1
不設(shè)置 htmlAcceptHeaders
header text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 htmlAcceptHeaders [ 'text/html', '*/*' ] indexOf 0 header application/signed-exchange;v=b3;q=0.9,*/*;q=0.8 htmlAcceptHeaders [ 'text/html', '*/*' ] indexOf -1 indexOf 39
這時(shí)候我突然茅塞頓開(kāi),htmlAcceptHeaders 這個(gè)屬性過(guò)濾 css 和 js 文件,如果用默認(rèn)的 'text/html', '*/*' 屬性,css 和 js 文件都會(huì)被匹配成 html 文件,然后一陣處理導(dǎo)致響應(yīng)頭的 mime 文件類(lèi)型變成 text/html 導(dǎo)致瀏覽器無(wú)法解析。
原來(lái)不是寫(xiě)文檔的人邏輯有問(wèn)題,而是他是個(gè)懶人,不想解釋太多,我是個(gè)蠢人不能一下子理解他的“深意”。
坑2
還有一點(diǎn)要注意,就是路由名稱(chēng)的設(shè)定。還是這個(gè)URL https://www.text.com/admin/login ,服務(wù)器把所有 /admin 的路由都指向了 vue 的 index.html 文件,hash模式下我們的路由這么配置的路由
const router = new VueRouter({ routes: [{ path: "/login", name: "login", component: login }] })
這時(shí)我們改成history模式
const router = new VueRouter({ mode: 'history', routes: [{ path: "/login", name: "login", component: login }] })
打開(kāi) url https://www.text.com/admin/login 會(huì)發(fā)現(xiàn)自動(dòng)跳轉(zhuǎn)到 https://www.text.com/login ,原因就是 /admin 的路由都指向了 vue 的 index.html 文件之后,js 根據(jù)我們的代碼把url改成了 https://www.text.com/login ,如果我們不刷新頁(yè)面沒(méi)有任何問(wèn)題,因?yàn)轫?yè)面內(nèi)所有的跳轉(zhuǎn)還是 vue-router 控制, index.html 這個(gè)文件沒(méi)變。但是如果刷新頁(yè)面那就會(huì)出問(wèn)題,服務(wù)器重新判斷 /login 路由對(duì)應(yīng)的文件。因此使用 history 模式時(shí)前端配置 vue-router 時(shí)也需要考慮后臺(tái)的項(xiàng)目所在目錄。
比如上面的例子應(yīng)該改為,這樣可以避免這種情況的問(wèn)題
const router = new VueRouter({ mode: 'history', routes: [{ path: "/admin/login", name: "login", component: login }] })
參考鏈接
https://router.vuejs.org/zh/guide/essentials/history-mode.html#后端配置例子
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
安裝node.js和npm的一些常見(jiàn)報(bào)錯(cuò)
NVM(Node?Version?Manager)是一個(gè)用于在同一機(jī)器上同時(shí)安裝并管理多個(gè)Node.js版本的工具,這篇文章主要給大家介紹了關(guān)于安裝node.js和npm的一些常見(jiàn)報(bào)錯(cuò),需要的朋友可以參考下2023-06-06Node.js處理I/O數(shù)據(jù)之使用Buffer模塊緩沖數(shù)據(jù)
這篇文章介紹了Node.js使用Buffer模塊緩沖數(shù)據(jù)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07Node.js 中正確使用 async/await 與 Promise 
在Node.js中,async/await是ES2017引入的一種更簡(jiǎn)潔的處理異步操作的方式,它基于Promise來(lái)進(jìn)行編寫(xiě),使得異步代碼看起來(lái)更像同步代碼,易于理解和維護(hù),這篇文章主要介紹了Node.js 中正確使用 async/await 與 Promise 對(duì)象配合,需要的朋友可以參考下2024-07-07Nodejs中調(diào)用系統(tǒng)命令、Shell腳本和Python腳本的方法和實(shí)例
這篇文章主要介紹了Nodejs中調(diào)用系統(tǒng)命令、Shell腳本和Python腳本的方法和實(shí)例,本文給出了利用子進(jìn)程調(diào)用系統(tǒng)命令、執(zhí)行系統(tǒng)命令、調(diào)用傳參數(shù)的shell腳本、調(diào)用python腳本的例子,需要的朋友可以參考下2015-01-01輕松創(chuàng)建nodejs服務(wù)器(2):nodejs服務(wù)器的構(gòu)成分析
這篇文章主要介紹了輕松創(chuàng)建nodejs服務(wù)器(2):nodejs服務(wù)器的構(gòu)成分析,本文是對(duì)第一節(jié)中簡(jiǎn)單服務(wù)器的代碼進(jìn)行分析總結(jié),需要的朋友可以參考下2014-12-12Node.js開(kāi)源應(yīng)用框架HapiJS介紹
這篇文章主要介紹了Node.js開(kāi)源應(yīng)用框架HapiJS介紹,本文講解了HapiJS介紹、HapiJS安裝和項(xiàng)目配置和開(kāi)發(fā)實(shí)例等內(nèi)容,需要的朋友可以參考下2015-01-01Node.js環(huán)境下編寫(xiě)爬蟲(chóng)爬取維基百科內(nèi)容的實(shí)例分享
WikiPedia平時(shí)在國(guó)內(nèi)不大好訪(fǎng)問(wèn)-- 所以用爬蟲(chóng)一次性把要看的東西都爬下來(lái)保存慢慢看還是比較好的XD 這里我們就來(lái)看一下Node.js環(huán)境下編寫(xiě)爬蟲(chóng)爬取維基百科內(nèi)容的實(shí)例分享2016-06-06