国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

詳解express使用vue-router的history踩坑

2019-11-19 11:24:04
字體:
來源:轉載
供稿:網友

vue-router 默認 hash 模式 ―― 使用 URL 的 hash 來模擬一個完整的 URL,于是當 URL 改變時,頁面不會重新加載。

如果不想要很丑的 hash,我們可以用路由的 history 模式,這種模式充分利用 history.pushState API 來完成 URL 跳轉而無須重新加載頁面。

當你使用 history 模式時,URL 就像正常的 url,例如 yoursite.com/user/id,也好看…

個人理解

上面是官方的解釋,文檔的一貫風格,只給懂的人看。兩年前我比現在還菜的時候,看了這段話表示他在說個錘子,直接跳過了。

我不講:hammer:,直接舉:chestnut:

一般的我們把項目放到服務器上,路由都是在服務器中設置的。

比如網站 https://www.text.com/ 中 admin目錄下有一個 login.html 的頁面。當用戶輸入 https://www.text.com/admin/login ,先解析 www.text.com 域名部分得到服務器 ip 和 端口號,根據 ip 和 端口號找到對應的服務器中的對應的程序,然后在程序解析 /admin/login 路徑知道了你要找的是 admin 目錄下的 login.html 頁面,然后就返回給你這個頁面。

這是正常的方式,服務器控制一個路由指向一個頁面的文件(不考慮重定向的情況),這樣我們的項一般有多少個頁面就有多少個 html 文件。

而 vue 中,我們打包好的文件其實是只有一個 index.html ,所有的行為都是在這一個頁面上完成。用戶的所有的路由其實都是在請求 index.html 頁面。

假設承載 vue 項目 index.html 也是在 admin 目錄下,vue 項目中也有一個 login 頁面,那對應的url就是 https://www.text.com/admin/#/login 。

這個 url 由三部分組成,是 www.text.com 是域名, /admin 是項目所在目錄,和上面一樣這個解析工作是由服務器完成的,服務器解析出 /admin 的路由,就返回給你 index.html 。 /#/login 是 vue-router 模擬的路由,因為頁面所有的跳轉 vue 都是在 index.html 中完成的,所以加上 # 表示頁內切換。假設切換到 home 頁面,對應的 html 文件還是 index.html ,url 變成 https://www.text.com/admin/#/home ,vue-router 判斷到 /#/home 的改變而改變了頁面 dom 元素,從而給用戶的感覺是頁面跳轉了。這就是 hash 模式。

那我們就知道了,正常的 url 和 hash 模式的區別,頁面的 js 代碼沒辦法獲取到服務器判斷路由的行為,所以只能用這種方式實現路由的功能。

而 history 模式就是讓 vue 的路由和正常的 url 一樣,至于怎么做下文會說到。

為什么需要實現

說怎么做之前,先說說為什么需要 history 模式。官方文檔說了,這樣比較好看。emmmmmm,對于直接面向消費者的網站好看這個確實是個問題,有個 /# 顯得不夠大氣。對于企業管理的 spa 這其實也沒什么。

所以除了好看之外,history 模式還有其他優勢。

我們知道,如果頁面使用錨點,就是一個 <a> 標簽, <a href='#mark1'></a> ,點擊之后如果頁面中有 id 為 mark1 的標簽會自動滾動到對應的標簽,而 url 后面會加上 #mark .

問題就出在這里,使用 hash 模式, #mark 會替換掉 vue-router 模擬的路由。比如這個 <a> 標簽是在上面說的 login 頁面,點擊之后 url 會從 https://www.text.com/admin/#/login 變成 https://www.text.com/admin/#/mark 。wtf???正??磥韱栴}不大,錨點滾動嘛,實在不行可以 js 模擬,但是因為我要實現 markdown 的標題導航功能,這個功能是插件做好的,究竟該插件還是用 history 。 權衡利弊下還是使用 history 模式工作量小,而且更美。

怎么做

既然知道是什么,為什么,下面就該研究怎么做了。

官方文檔里有“詳盡”的說明,其實這事兒本來不難,原理也很簡單。通過上文我們知道 vue-router 采用 hash 模式最大的原因在于所有的路由跳轉都是 js 模擬的,而 js 無法獲取服務器判斷路由的行為,那么就需要服務器的配合。原理就是無論用戶輸入的路由是什么全都指向 index.html 文件,然后 js 根據路由再進行渲染。

按照官方的做法,前端 router 配置里面加一個屬性,如下

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,//暫時關掉cdn    }  ));

坑1

按道理來說這樣就沒問題了,然鵝放到服務器里面之后,開始出幺蛾子了。靜態文件加載的時候接口返回都是

We're sorry but client doesn't work properly without JavaScript enabled. Please enable it to continue.

看著字面意思,說我的項目(項目名client)沒有啟用 JavaScript ,莫名其妙完全不能理解。于是乎仔細比對控制臺 responses headers 和request headers ,發現了一些貓膩,請求頭的 accept 和響應頭的 content-type 對不上,請求 css 文件請求頭的 accept 是text/css,響應頭的 content-type 是 text/html。這個不應該請求什么響應什么嗎,我想要崔鶯鶯一樣女子做老婆,給我個杜十娘也認了,結果你給我整個潘金蓮讓我咋整。

完全不知道到底哪里出了問題,google上面也沒有找到方法。開始瞎琢磨,既然對不上,那就想我手動給對上行不行。在express.static 的 setHeaders 里面檢查讀取文件類型,然后根據文件類型手動設置mime type,我開始佩服我的機智。

app.use(  express.static(    path.join(process.cwd(), "static"),    {      maxAge: 0,      setHeaders(res,path){        // 通過 path 獲取文件類型,設置對應文件的 mime type。      }    }  ));

緩存時間設置為0,關掉CDN... 一頓操作, 發現不執行 setHeaders 里面的方法。這個時候已經晚上 11 點了,我已經絕望了,最后一次看了一遍 connect-history-api-fallback 的文檔,覺得 htmlAcceptHeaders 這個配置項這么違和,其他的都能明白啥意思,就這個怎么都不能理解,死馬當活馬醫扔進代碼試試,居然成了。

const history = require('connect-history-api-fallback')app.use(history({  htmlAcceptHeaders: ['text/html', 'application/xhtml+xml']  rewrites: [    {      from: /^//.*$/,      to: function (context) {        return "/";      }    },  ]}));

到底誰寫的文檔,靜態文件的 headers 的 accepts 和 htmlAcceptHeaders 有什么關系。咱也不知道,咱也沒地方問。這事兒耽誤了我大半天的時間,不研究透了心里不舒服。老規矩,看 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(){};}

這個代碼還真是通俗易懂,就不去一行行分析了(其實是我懶)。直接截取關鍵代碼:

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 函數返回 false,說明瀏覽器不接受 html 文件,跳過執行 next(),否則繼續執行。

后一段代碼, acceptsHtml 函數內部設置 htmlAcceptHeaders 的默認值是 'text/html', '*/*' 。判斷請求頭的accept,如果匹配上說明返回true,否則返回false。直接用默認值接口不能正常返回 css 和 js, 改成 'text/html', 'application/xhtml+xml' 就能運行了。這就奇了怪了,htmlAcceptHeaders 為什么會影響 css 和 js。太晚了,不太想糾結了,簡單粗暴把源碼摳出來直接放到項目里面跑一下,看看到底發生了什么。

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;}

設置 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=b3htmlAcceptHeaders [ 'text/html', 'application/xhtml+xml' ]indexOf 0header text/css,*/*;q=0.1htmlAcceptHeaders [ 'text/html', 'application/xhtml+xml' ]indexOf -1indexOf -1

不設置 htmlAcceptHeaders

header text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3htmlAcceptHeaders [ 'text/html', '*/*' ]indexOf 0header application/signed-exchange;v=b3;q=0.9,*/*;q=0.8htmlAcceptHeaders [ 'text/html', '*/*' ]indexOf -1indexOf 39

這時候我突然茅塞頓開,htmlAcceptHeaders 這個屬性過濾 css 和 js 文件,如果用默認的 'text/html', '*/*' 屬性,css 和 js 文件都會被匹配成 html 文件,然后一陣處理導致響應頭的 mime 文件類型變成 text/html 導致瀏覽器無法解析。

原來不是寫文檔的人邏輯有問題,而是他是個懶人,不想解釋太多,我是個蠢人不能一下子理解他的“深意”。

坑2

還有一點要注意,就是路由名稱的設定。還是這個URL https://www.text.com/admin/login ,服務器把所有 /admin 的路由都指向了 vue 的 index.html 文件,hash模式下我們的路由這么配置的路由

const router = new VueRouter({ routes: [{    path: "/login",    name: "login",    component: login  }]})

這時我們改成history模式

const router = new VueRouter({ mode: 'history', routes: [{    path: "/login",    name: "login",    component: login  }]})

打開 url https://www.text.com/admin/login 會發現自動跳轉到 https://www.text.com/login ,原因就是 /admin 的路由都指向了 vue 的 index.html 文件之后,js 根據我們的代碼把url改成了 https://www.text.com/login ,如果我們不刷新頁面沒有任何問題,因為頁面內所有的跳轉還是 vue-router 控制, index.html 這個文件沒變。但是如果刷新頁面那就會出問題,服務器重新判斷 /login 路由對應的文件。因此使用 history 模式時前端配置 vue-router 時也需要考慮后臺的項目所在目錄。

比如上面的例子應該改為,這樣可以避免這種情況的問題

const router = new VueRouter({ mode: 'history', routes: [{    path: "/admin/login",    name: "login",    component: login  }]})

參考鏈接

https://router.vuejs.org/zh/guide/essentials/history-mode.html#后端配置例子

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 溧水县| 汉源县| 商河县| 兰州市| 广河县| 南城县| 沧州市| 南岸区| 宁武县| 调兵山市| 米林县| 常德市| 吉木乃县| 漠河县| 皮山县| 德清县| 吉安县| 凤翔县| 新安县| 明水县| 盖州市| 邵阳县| 尤溪县| 托克逊县| 晋江市| 亚东县| 达州市| 泽普县| 新河县| 盖州市| 离岛区| 霍城县| 安国市| 青龙| 南溪县| 张家港市| 稻城县| 博湖县| 新巴尔虎左旗| 津南区| 彩票|