connect-history-api-fallback分析

 阅读约 19 分钟

研究下connect-history-api-fallback v1.3.0,地址:https://github.com/bripkens/c...,当然它也可以作为express的中间件使用

README中介绍的很清楚

Single Page Applications (SPA) typically only utilise one index file that is accessible by web browsers: usually index.html. Navigation in the application is then commonly handled using JavaScript with the help of the HTML5 History API. This results in issues when the user hits the refresh button or is directly accessing a page other than the landing page, e.g. /help or /help/online as the web server bypasses the index file to locate the file at this location. As your application is a SPA, the web server will fail trying to retrieve the file and return a 404 - Not Found message to the user.

This tiny middleware addresses some of the issues. Specifically, it will change the requested location to the index you specify (default being /index.html) whenever there is a request which fulfills the following criteria:

  1. The request is a GET request

  2. which accepts text/html,

  3. is not a direct file request, i.e. the requested path does not contain a . (DOT) character and

  4. does not match a pattern provided in options.rewrites (see options below)

解释一下:

server.js

const path = require('path')

const express = require('express')
const router = express.Router()

const indexRoute = router.get('/', (req, res) => {
  res.status(200).render('index', {
    title: '首页'
  })
})

app.set('views', path.join(__dirname, 'templates'))
app.set('view engine', 'html')
app.engine('html', ejs.__express)

app.use('/static', express.static(path.join(__dirname, 'public')))

app.use(history({
  rewrites: [
    { from: /^\/abc$/, to: '/' }
  ]
}))

app.get('/', indexRoute)

app.use((req, res) => {
  res.status(404).send('File not found!')
})

app.listen(9090, '127.0.0.1', () => {
  console.log('ther server is running at port ' + 9090)
})

index.html

<div id="test">
  <router-view></router-view>
</div>

index.js

Vue.use(VueRouter)

var s = '<div><router-link to="/foo">Go to Foo</router-link> - <router-link to="/bar">Go to Bar</router-link> - <router-link to="/">Go to Home</router-link></div>'

var Home =  { template: '<div>' + s + '<div>home</div>' + '</div>', created: function() { console.log('home') } }
var Foo = { template: '<div>' + s + '<div>foo</div>' + '</div>', created: function() { console.log('foo') }}
var Bar = { template: '<div>' + s + '<div>bar</div>' + '</div>', created: function() { console.log('bar') }}
var NotFoundComponent = { template: '<div>' + s + '<div>not found</div>' + '</div>', created: function() { console.log('not found') }}

var routes = [
  { path: '/', component: Home },
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar },
  { path: '*', component: NotFoundComponent }
]

var router = new VueRouter({
  mode: 'history',
  routes: routes
})

new Vue({
  router: router
}).$mount('#test')

比如我使用vue-router, 访问http://localhost:9090,发现显示home,点击Go to Foo,显示foo,地址栏变为http://localhost:9090/foo,一切正常,ok
这时候刷新当前页面(ctrl+R或ctrl+command+R或点击浏览器的刷新按钮或在地址栏上再敲一下回车),发现404了哦
vue-router文档针对这种情况做了很好的解释:

Not to worry: To fix the issue, all you need to do is add a simple catch-all fallback route to your server. If the URL doesn't match any static assets, it should serve the same index.html page that your app lives in. Beautiful, again!

如果express server使用了connect-history-api-fallback middleware,在你定义router的前面app.use(history({ rewrites: [ { from: /^/abc$/, to: '/' } ] }))一下

再刷新页面,发现地址仍然http://localhost:9090/foo,然而走进了咱们的前端路由,chrome控制台显示了foo,真的是beautiful again

其实过程也很简单啦,请求/foo,走到了咱们的history-api-fallback中间件,然后他看你没有rewrite,那么好,我把req.url改成'/',于是vue-router发现地址/foo,所以根据routes的map,渲染了Foo组件

但是万一有人输入地址/abc,怎么办? vue-router定义了{ path: '*', component: NotFoundComponent }用来catch-all route within your Vue app to show a 404 page

Alternatively, if you are using a Node.js server, you can implement the fallback by using the router on the server side to match the incoming URL and respond with 404 if no route is matched.

  1. 地址输入/abc,回车,走到vue-router,会显示not found

  2. 地址输入/xyz,回车,走到服务端路由,http状态为404,然后显示File not found!

source code 分析

贴下代码

'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);
        logger('Rewriting', req.method, req.url, 'to', rewriteTarget);
        req.url = rewriteTarget;
        return next();
      }
    }

    if (parsedUrl.pathname.indexOf('.') !== -1 &&
        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) {
  if (typeof rule === 'string') {
    return rule;
  } else if (typeof rule !== 'function') {
    throw new Error('Rewrite rule can only be of type string of function.');
  }

  return rule({
    parsedUrl: parsedUrl,
    match: match
  });
}

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(){};
}

图片描述

getLogger, 默认不输出,options.verbose为true,则默认console.log.bind(console),但不知道这里bind意义何在 - -,也可以直接传logger,比如debug

如果req.method != 'GET',灰,结束
如果!headers || !headers.accept != 'string' (有这情况?),灰,结束
如果headers.accept.indexOf('application/json') === 0,灰,结束

acceptsHtml函数a判断headers.accept字符串是否含有['text/html', '/']中任意一个
当然不够这两个不够你可以自定义到选项options.htmlAcceptHeaders中
!acceptsHtml(headers.accept, options),灰,结束

然后根据你定义的选项rewrites(没定义就相当于跳过了)
按定义的数组顺序,字符串依次匹配路由rewrite.from,匹配成功则走rewrite.to,to可以是字符串也可以是函数,绿,结束

判断dot file,即pathname中包含.(点),并且选项disableDotRule !== true,即没有关闭点文件限制规则,灰,结束
那么剩下的情况(parsedUrl.pathname不含点,或者含点但关闭了点文件规则)
rewriteTarget = options.index || '/index.html',绿结束

稍微注意下,他是先匹配自定义rewrites规则,再匹配点文件规则

测试部分

用的是nodeunit,具体用法https://github.com/caolan/nod...
随便看两个测试用例


var sinon = require('sinon');
var historyApiFallback = require('../lib');

var tests = module.exports = {};

var middleware;
var req = null;
var requestedUrl;
var next;

tests.setUp = function(done) {
  middleware = historyApiFallback();
  requestedUrl = '/foo';
  req = {
    method: 'GET',
    url: requestedUrl,
    headers: {
      accept: 'text/html, */*'
    }
  };
  next = sinon.stub();

  done();
};

// ....

tests['should ignore requests that do not accept html'] = function(test) {
  req.headers.accept = 'application/json';
  // 调用middleware
  middleware(req, null, next);
  // 测试req.url是否等于requestedUrl
  test.equal(req.url, requestedUrl);
  // next是否被调用过
  test.ok(next.called);
  // 测试结束
  test.done();
};

// ...

tests['should rewrite requests when the . rule is disabled'] = function(test) {
  req.url = 'js/app.js';
  middleware = historyApiFallback({
    disableDotRule: true
  });
  middleware(req, null, next);
  // 测试req.url是否等于/index.html
  // 因为pathname中有点,且关闭了点规则
  // req.url应该被rewrit成了/index.html
  test.equal(req.url, '/index.html');
  test.ok(next.called);
  test.done();
};

// ...
tests['should test rewrite rules'] = function(test) {
  req.url = '/socer';
  middleware = historyApiFallback({
    rewrites: [
      {from: /\/soccer/, to: '/soccer.html'}
    ]
  });
     
  middleware(req, null, next);
  // 因为没有匹配上rewrites里的规则
  // 而req.url pathname又不含点
  // 所以req.url 倒退到了index.html
  test.equal(req.url, '/index.html');
  test.ok(next.called);
  test.done();
};
阅读 14k更新于 2016-12-27
推荐阅读
打怪心路历程
用户专栏

魔力宝贝,我的❤️

2 人关注
3 篇文章
专栏主页
目录