详解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 相关文章推荐
让GoogleCode的SVN下的HTML文件在FireFox下正常显示.
May 25 Javascript
JavaScript call apply使用 JavaScript对象的方法绑定到DOM事件后this指向问题
Sep 28 Javascript
基于jquery的图片幻灯展示源码
Jul 15 Javascript
JavaScript 学习笔记之基础中的基础
Jan 13 Javascript
JS实现点击按钮后框架内载入不同网页的方法
May 05 Javascript
jQuery图片轮播滚动切换代码分享
Apr 20 Javascript
jQuery视差滚动效果网页实现方法经验总结
Sep 29 Javascript
js实现打地鼠小游戏
Feb 13 Javascript
微信小程序使用wxParse解析html的方法教程
Jul 06 Javascript
浅谈JavaScript 代码整洁之道
Oct 23 Javascript
Vue简单实现原理详解
May 07 Javascript
解决vue加scoped后就无法修改vant的UI组件的样式问题
Sep 07 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
Yii框架组件和事件行为管理详解
2016/05/20 PHP
PHP串行化与反串行化实例分析
2016/12/27 PHP
PHP处理bmp格式图片的方法分析
2017/07/04 PHP
JS模板实现方法
2013/04/03 Javascript
返回页面顶部top按钮通过锚点实现(自写)
2013/08/30 Javascript
js获取下拉列表框中的value和text的值示例代码
2014/01/11 Javascript
javascript跨域方法、原理以及出现问题解决方法(详解)
2015/08/06 Javascript
纯JavaScript代码实现文本比较工具
2016/02/17 Javascript
JavaScript实现页面跳转的方式汇总
2016/05/16 Javascript
JS留言功能的简单实现案例(推荐)
2016/06/23 Javascript
Js 获取、判断浏览器版本信息的简单方法
2016/08/08 Javascript
JS 判断某变量是否为某数组中的一个值的3种方法(总结)
2017/07/10 Javascript
webpack 1.x升级过程中的踩坑总结大全
2017/08/09 Javascript
js实现下拉框二级联动
2018/12/04 Javascript
JS实现简单的文字无缝上下滚动功能示例
2019/06/22 Javascript
layui表格内放置图片,并点击放大的实例
2019/09/10 Javascript
vue-next/runtime-core 源码阅读指南详解
2019/10/25 Javascript
JavaScript写个贪吃蛇小游戏(超详细)
2020/03/17 Javascript
JavaScript自定义超时API代码实例
2020/04/30 Javascript
Python logging模块学习笔记
2014/05/24 Python
python检测远程udp端口是否打开的方法
2015/03/14 Python
python实现各进制转换的总结大全
2017/06/18 Python
python f-string式格式化听语音流程讲解
2019/06/18 Python
用Python批量把文件复制到另一个文件夹的实现方法
2019/08/16 Python
Python使用random模块生成随机数操作实例详解
2019/09/17 Python
Python如何给函数库增加日志功能
2020/08/04 Python
python如何对链表操作
2020/10/10 Python
CSS3实现水平居中、垂直居中、水平垂直居中的实例代码
2020/02/27 HTML / CSS
Windows和Linux动态库应用异同
2016/04/17 面试题
毕业生自我鉴定范文
2013/11/08 职场文书
计算机专业求职信
2014/06/02 职场文书
2014政府领导班子对照检查材料思想汇报(3篇)
2014/09/26 职场文书
党员四风自我剖析材料
2014/10/07 职场文书
外贸英文求职信范文
2015/03/19 职场文书
五星级酒店前台接待岗位职责
2015/04/02 职场文书
MySQL EXPLAIN输出列的详细解释
2021/05/12 MySQL