欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-20 14:51:08
vue-router 默认 hash 模式 —— 使用 url 的 hash 来模拟一个完整的 url,于是当 url 改变时,页面不会重新加载。 如果不想要很丑的 ha...

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 中间件。(中间件地址 )

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
  }]
})

参考链接

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