当前位置: 移动技术网 > IT编程>脚本编程>NodeJs > 详解express使用vue-router的history踩坑

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

2019年07月19日  | 移动技术网IT编程  | 我要评论

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 中,我们打包好的文件其实是只有一个  ,所有的行为都是在这一个页面上完成。用户的所有的路由其实都是在请求  页面。

假设承载 vue 项目  也是在 admin 目录下,vue 项目中也有一个 login 页面,那对应的url就是 https://www.text.com/admin/#/login 。

这个 url 由三部分组成,是 www.text.com 是域名, /admin 是项目所在目录,和上面一样这个解析工作是由服务器完成的,服务器解析出 /admin 的路由,就返回给你  。 /#/login 是 vue-router 模拟的路由,因为页面所有的跳转 vue 都是在  中完成的,所以加上 # 表示页内切换。假设切换到 home 页面,对应的 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 无法获取服务器判断路由的行为,那么就需要服务器的配合。原理就是无论用户输入的路由是什么全都指向  文件,然后 js 根据路由再进行渲染。

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

const router = new vuerouter({
 mode: 'history',
 routes: [...]
})

后端的我不一一赘述,我用的是express,所以直接用了 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/"));
});

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 || '/';
  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 的  文件,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 的  文件之后,js 根据我们的代码把url改成了 https://www.text.com/login ,如果我们不刷新页面没有任何问题,因为页面内所有的跳转还是 vue-router 控制,  这个文件没变。但是如果刷新页面那就会出问题,服务器重新判断 /login 路由对应的文件。因此使用 history 模式时前端配置 vue-router 时也需要考虑后台的项目所在目录。

比如上面的例子应该改为,这样可以避免这种情况的问题

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

参考链接

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网