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


Posted in Javascript onJune 05, 2019

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=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

不设置 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

这时候我突然茅塞顿开,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#后端配置例子

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS网络游戏-(模拟城市webgame)提供的一些例子下载
Oct 14 Javascript
动态创建的表格单元格中的事件实现代码
Dec 30 Javascript
jquery 学习之二 属性 文本与值(text,val)
Nov 25 Javascript
jQuery实现最简单的切换图效果【可兼容IE6、火狐、谷歌、opera等】
Sep 04 Javascript
javascript入门之数组[新手必看]
Nov 21 Javascript
移动端web滚动分页的实现方法
May 05 Javascript
Vue2 SSR渲染根据不同页面修改 meta
Nov 20 Javascript
Vue axios全局拦截 get请求、post请求、配置请求的实例代码
Nov 28 Javascript
iview实现select tree树形下拉框的示例代码
Dec 21 Javascript
利用Vue实现一个markdown编辑器实例代码
May 19 Javascript
使用p5.js临摹动态图形
Oct 23 Javascript
vue多个元素的样式选择器问题
Nov 29 Javascript
laravel-admin 与 vue 结合使用实例代码详解
Jun 04 #Javascript
用webpack4开发小程序的实现方法
Jun 04 #Javascript
JS实现的对象去重功能示例
Jun 04 #Javascript
JS数组中对象去重操作示例
Jun 04 #Javascript
jquery UI实现autocomplete在获取焦点时得到显示列表功能示例
Jun 04 #jQuery
IE浏览器下JS脚本提交表单后,不能自动提示问题解决方法
Jun 04 #Javascript
ES6中字符串的使用方法扩展
Jun 04 #Javascript
You might like
解密ThinkPHP3.1.2版本之模块和操作映射
2014/06/19 PHP
windows下配置apache+php+mysql时出现问题的处理方法
2014/06/20 PHP
php遍历替换目录下文件指定内容的方法
2016/11/10 PHP
详解在YII2框架中使用UEditor编辑器发布文章
2018/11/02 PHP
javascript setTimeout和setInterval 的区别
2009/12/08 Javascript
js 页面刷新location.reload和location.replace的区别小结
2009/12/24 Javascript
js 实现无干扰阴影效果 简单好用(附文件下载)
2009/12/27 Javascript
JavaScript取得鼠标绝对位置程序代码介绍
2012/09/16 Javascript
异步javascript的原理和实现技巧介绍
2012/11/08 Javascript
jQuery移除元素自动解绑事件实现思路及代码
2014/05/31 Javascript
jQuery将多条数据插入模态框的示例代码
2014/09/25 Javascript
html的DOM中Event对象onabort事件用法实例
2015/01/21 Javascript
Node.js 异步编程之 Callback介绍(一)
2015/03/30 Javascript
Node.js使用orm2进行update操作时关联字段无法修改的解决方法
2017/06/13 Javascript
详解node-ccap模块生成captcha验证码
2017/07/01 Javascript
nodejs 搭建简易服务器的图文教程(推荐)
2017/07/18 NodeJs
原生js实现针对Dom节点的CRUD操作示例
2019/08/26 Javascript
基于layui table返回的值的多级嵌套的解决方法
2019/09/19 Javascript
jQuery实现电梯导航模块
2020/12/22 jQuery
[37:45]2014 DOTA2国际邀请赛中国区预选赛5.21 DT VS Orenda
2014/05/22 DOTA
跟老齐学Python之坑爹的字符编码
2014/09/28 Python
python实现识别相似图片小结
2016/02/22 Python
Python3.7中安装openCV库的方法
2018/07/11 Python
破解安装Pycharm的方法
2018/10/19 Python
不同浏览器对CSS3和HTML5的支持状况
2009/10/31 HTML / CSS
分享CSS3中必须要知道的10个顶级命令
2012/04/26 HTML / CSS
美国和加拿大计算机和电子产品购物网站:TigerDirect.com
2019/09/13 全球购物
迪士尼西班牙官方网上商店:ShopDisney西班牙
2020/02/02 全球购物
黄金酒广告词
2014/03/21 职场文书
2015年师德师风自我评价范文
2015/03/05 职场文书
2015上半年个人工作总结
2015/07/27 职场文书
公司会议开幕词
2016/03/03 职场文书
2016年119消防宣传日活动总结
2016/04/05 职场文书
如何获取numpy array前N个最大值
2021/05/14 Python
MySQL中日期型单行函数代码详解
2021/06/21 MySQL
解决linux下redis数据库overcommit_memory问题
2022/02/24 Redis