详解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 相关文章推荐
初窥JQuery(一)jquery选择符 必备知识点
Nov 25 Javascript
对setInterval在火狐和chrome切换标签产生奇怪的效果之探索,与解决方案!
Oct 29 Javascript
关于onchange事件在IE和FF下的表现及解决方法
Mar 08 Javascript
javascript使用prototype完成单继承
Dec 24 Javascript
javascript实现树形菜单的方法
Jul 17 Javascript
JavaScript中的数据类型转换方法小结
Oct 26 Javascript
JS实现的论坛Ajax打分效果完整实例
Oct 31 Javascript
javascript下拉列表中显示树形菜单的实现方法
Nov 17 Javascript
JS中正则表达式要注意lastIndex属性
Aug 08 Javascript
jQuery实现判断上传图片类型和大小的方法示例
Apr 11 jQuery
vue3.0 CLI - 2.1 -  component 组件入门教程
Sep 14 Javascript
微信小程序实现音频文件播放进度的实例代码
Mar 02 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
PHP 命令行参数详解及应用
2011/05/18 PHP
详解php中生成标准uuid(guid)的方法
2019/04/28 PHP
TP5框架使用QueryList采集框架爬小说操作示例
2020/03/26 PHP
JS 自动完成 AutoComplete(Ajax 查询)
2009/07/07 Javascript
JS实现图片横向滚动效果示例代码
2013/09/04 Javascript
node.js中的fs.readdirSync方法使用说明
2014/12/17 Javascript
jQuery中delegate()方法用法实例
2015/01/19 Javascript
js实现图片无缝滚动特效
2020/03/19 Javascript
深入剖析JavaScript中的函数currying柯里化
2016/04/29 Javascript
jQuery实现点击查看大图并以弹框的形式居中
2016/08/08 Javascript
Vue 2.0学习笔记之Vue中的computed属性
2017/10/16 Javascript
web前端vue filter 过滤器
2018/01/12 Javascript
Vue使用vux-ui自定义表单验证遇到的问题及解决方法
2018/05/10 Javascript
vue项目中使用bpmn为节点添加颜色的方法
2020/04/30 Javascript
Vue单页面应用中实现Markdown渲染
2021/02/14 Vue.js
python实现八大排序算法(2)
2017/09/14 Python
Python matplotlib绘图可视化知识点整理(小结)
2018/03/16 Python
浅谈python常用程序算法
2019/03/22 Python
在Python中过滤Windows文件名中的非法字符方法
2019/06/10 Python
python3使用腾讯企业邮箱发送邮件的实例
2019/06/28 Python
如何用Python来理一理红楼梦里的那些关系
2019/08/14 Python
python sorted函数原理解析及练习
2020/02/10 Python
localStorage、sessionStorage使用总结
2017/11/17 HTML / CSS
Qoo10台湾站:亚洲领先的在线市场
2018/05/15 全球购物
JavaScript获取当前url根目录(路径)
2014/02/19 面试题
护士自我评价
2014/02/01 职场文书
鼓励运动员的广播稿
2014/02/08 职场文书
大学生职业生涯规划书参考模板
2014/03/05 职场文书
青春寄语大全
2014/04/09 职场文书
班干部竞选演讲稿
2014/04/24 职场文书
百日安全活动总结
2014/05/04 职场文书
抗洪救灾标语
2014/10/08 职场文书
2014年行风建设工作总结
2014/12/01 职场文书
2016简历自荐信优秀范文
2016/01/29 职场文书
nginx前后端同域名配置的方法实现
2021/03/31 Servers
CSS 新特性 contain控制页面的重绘与重排问题
2021/04/30 HTML / CSS