VIP用户交流群:462197261 收藏本站北冥有鱼 互联网前沿资源第一站 助力全行业互联网+
在线客服:78895949
tonglan
  • 当前位置:
  • 详解express使用vue-router的history踩坑

    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#后端配置例子

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

    您可能感兴趣的文章:

    • 把vue-router和express项目部署到服务器的方法

    广而告之:
    热门推荐:
    Node.js 8 中的重要新特性

    随着 Node.js 8.0 版本的发布(5月30日下午12点发布),我们得到了最新的 LTS 版本,具有一系列新功能和性能改进。 本文我们将介绍 Node.js 8.0 版本中重要的功能和修复。 与以前的 Node.js 版本相比,8.0.0相当强大。虽然这其中有些还正在进行,很多正在商榷。但基本上是稳定···

    微信支付PHP SDK —— 公众号支付代码详解

    在微信支付 开发者文档页面 下载最新的 php SDK http://mch.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1 这里假设你已经申请完微信支付 1. 微信后台配置  如图 我们先进行测试,所以先把测试授权目录和 测试白名单添加上。测试授权目录是你要发起微信请求的···

    php中文字符截取防乱码

    先看段代码  复制代码 代码如下:<?php        $len = 15;           $str = "这个新闻或是文章的标题很长,需要只显示前面一些字,后面用...来代···

    js正则取值的结果数组调试方法

    如下所示: body="Darry 官网(戴珠宝官网)是戴珠宝指定官方网站,darry戴珠宝官网提供;戴珠宝产品包括:结婚戒指、求婚戒指等钻戒|对戒系列,欢迎进入戴珠宝唯一官网详细了解;"; var reg = /[a-z]+/i; res = body.match(reg); if(res.length>0){ console.log('res···

    织梦dedecms系统如何更换模板主题

    织梦更换模板没有WP那么简单,需要简单的几步操作才可以完成,下面织梦58编辑提供下织梦网站更换前台模板主题方法,希望能帮助到大家。  首先,我们假设模板文件名称为:cms,织梦原始默认的风格名称为:default。  1:打开后台--系统--系统基本参数&nbs···

    PHPStrom 新建FTP项目以及在线操作教程

    前言 PhpStorm 是 JetBrains 公司开发的一款商业的 PHP 集成开发工具。它包含了WebStorm的所有功能,前后端都是相当出色,其强大便利之处,相信用过的同学们都知道。那么这里我就和大家聊聊它的 FTP和远程文件同步 功能。 优势 FTP和远程文件同步,顾名思义,就是使用ftp协议操···

    dedecms数据库连接

    一、织梦CMS(dedecms)的数据库连接文件位置: 织梦CMS V5.1 在include\config_base.php 织梦CMS V5.3 在\data\common.inc.php 织梦CMS V5.5 在\data\common.inc.php 织梦CMS V5.6 在\data\common.inc.php 织梦CMS V5.7 在\data\common.inc.php 二、织梦CMS(dedecm···

    关于mysql字符集设置了character

    mysql链接建立之后,通过如下方式设置编码: 复制代码 代码如下: mysql_query("SET character_set_connection=" . $GLOBALS['charset'] . ",character_set_results=" . $GLOBALS['charset'] . ",character_set_client=binary", $this->link); 然而建立出来的表结构描···

    微信小程序如何自定义table组件

    背景 最近想要捣鼓一个自己的小程序,其中数据展示部分比较多,想用table来做展示,但是微信小程序并没有table组件,于是就自己动手捣鼓了一个,勉强能用。 可以看看效果: etable使用介绍 etable的使用很简单,分为 引入、使用、配置等3个阶段 1、引入 首先在要使用的页···

    织梦dedeCMS实现自动锚文本方法

     我们知道锚文本对于网站优化是很有用处的,那么织梦CMS可以实现这一功能吗?答案是肯定的。下面分享一个很简单的办法来实现自动添加锚文本。   首先,把后台需要设置的地方都设置好。   1、系统-系统基本参数-性能选项-使用关键词关联文···