What is history?

The official story

Vue-router defaults to hash mode — the hash of a URL is used to simulate a full URL so that the page does not reload when the URL changes.

If you don’t want ugly hashes, you can use the history mode of the route, which takes full advantage of the history.pushState API to do URL jumps without reloading the page.

When you use history mode, the URL looks like a normal URL, such as yoursite.com/user/id, which also looks good…

Personal understanding

Above is the official explanation, the usual style of documentation, for those who understand. Two years ago, when I was eating less food than I am now, I read this paragraph indicating that he was talking about a hammer and skipped it.

I’m not going to say 🔨, I’m going to say 🌰

Usually we put the project on the server, the routing is set up in the server.

For example, https://www.text.com/ has a login. HTML page in the admin directory. When the user enter https://www.text.com/admin/login, parse www.text.com domain name part of the first server IP and port number, according to the corresponding server IP and port number to find the corresponding program, /admin/login/login. HTML /admin/ login. HTML /admin/ login. HTML /admin/ login. HTML /admin/ login. HTML

This is the normal way, the server controls a route to a file on a page (regardless of redirection), so that our item typically has as many HTML files as it has pages.

In Vue, the file we packaged is actually a single index.html, and all the action is done on that single page. All of the user’s routes actually request the index.html page.

Hypothesis carrying index vue project. HTML is also in the admin directory, vue project also has a login page, the corresponding url is https://www.text.com/admin/#/login.

The url consists of three parts: www.text.com is the domain name, and /admin is the project directory. As above, the parsing is done by the server. The server parses the route of /admin and returns it to you in index.html. /#/login is the route simulated by vue-router. Since all page jumps are performed in index.html, the # is used to indicate in-page switching. Assume that switching to the home page, the corresponding HTML files or index. The HTML, url into https://www.text.com/admin/#/home, vue – judged to # / home/change the router and change the page dom elements, Thus give the user the feeling is the page jumped. That’s the hash pattern.

Then we know the difference between normal URL and hash mode. The JS code of the page cannot obtain the behavior of the server to judge the route, so it can only be used in this way to realize the function of the route.

The history mode makes the route of the VUE look like a normal URL, which will be explained below.

Why implementation is needed

Before we talk about how, let’s talk about why we need history mode. Official documentation says it looks better this way. Emmmmmm, this is really a problem for direct-to-consumer websites, having a /# is not atmospheric enough. This is fine for corporate management spas.

So besides being pretty, history mode has other advantages.

Here’s the problem. Using hash mode, #mark replaces the route simulated by vue-router. Such as the < a > tag is said on the login page, click the following url from https://www.text.com/admin/#/login to https://www.text.com/admin/#/mark. WTF??? As for anchor scrolling, it can be simulated by JS. However, since I want to implement the title navigation function of Markdown, this function is done by the plug-in. Should the plug-in still use History? The trade-off is that using history mode is less work and more beautiful.

How to do

Now that we know what it is and why, it’s time to figure out how to do it.

There are “detailed” instructions in the official documentation, but it shouldn’t be difficult and the principle is simple. As we know from the above, the biggest reason why vue-router adopts hash mode is that all route hops are simulated by JS, and JS cannot obtain the behavior of the server to judge the route, so cooperation of the server is required. The idea is that whatever route the user enters will point to the index.html file, and THEN JS will render it based on the route.

Officially, there is an attribute added to the front-end router configuration as follows

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})
Copy the code

I won’t go into details on the back end, but I used Express, so I used connect-history-API-Fallback middleware directly. (Middleware address github.com/bripkens/co…

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,// temporarily turn off CDN}));Copy the code

1 the pit

According to the truth, there is no problem, but after the goose into the server, began to go out. The interface returns static file loads

We’re sorry but client doesn’t work properly without JavaScript enabled. Please enable it to continue.

My project (project name client) does not have JavaScript enabled. As a result, a careful comparison between console Responses headers and Request Headers found some fishiness, namely, the content-type of the request header and the accept header did not match. Request CSS The accept of the request header is text/ CSS, and the content-type of the response header is text/ HTML. This should not request what response what, I want cui Yingying same woman to do a wife, give me a Du Shiniang also recognize, the result you give me whole Pan Jinlian let me how to do.

I have no idea what went wrong, and Google has not found a solution. Began to mull over, since not on, that would like me to manually on the uplink. When I checked the types of files I read in express.static setHeaders and manually set mime types to those files, I started to admire my ingenuity.

app.use(
    express.static(
        path.join(process.cwd(), "static"),
        {
            maxAge: 0,
            setHeaders(res, path){// Obtain the file type by path and set the MIME of the corresponding filetype. }}));Copy the code

Set cache time to 0, turn off CDN… An operation, found that the method does not execute setHeaders inside. It was 11 p.m., and I was desperate. I looked at the connect-history-apI-Fallback one last time and thought the htmlAcceptHeaders configuration item was so illegal that I could understand everything else. Try throwing a dead horse into the code, and it works.

const history = require('connect-history-api-fallback')
app.use(history({
    htmlAcceptHeaders: ['text/html'.'application/xhtml+xml']
    rewrites: [
        {
            from: $/ / ^ \ /. *.to: function (context) {
                return "/"; }}]}));Copy the code

Who writes the document, and what does the accepts of headers for a static file have to do with htmlAcceptHeaders? We don’t know. We have no place to ask. It took me most of the day, and I felt uncomfortable not to study it. 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() {}; }Copy the code

This code is really easy to understand, so I don’t have to analyze it line by line (actually I’m lazy). Direct interception of key code:

 else if(! acceptsHtml(headers.accept, options)) { logger('Not rewriting',
        req.method,
        req.url,
        'because the client does not accept HTML.'
      );
      return next();
    }
Copy the code
functionAcceptsHtml (header, options) {/ / options here. 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;
}
Copy the code

In the previous code, if the acceptsHtml function returns false, the browser does not accept the HTML file, skip next(), or continue.

The default values for htmlAcceptHeaders are ‘text/ HTML ‘, ‘*/*’. Accepts the request header, returning true if it matches, false otherwise. The default interface will not return CSS and JS normally. Change it to ‘text/ HTML ‘, and ‘Application/XHTML + XML’ will work. It is strange why htmlAcceptHeaders affect CSS and JS. It’s too late. I don’t want to get tangled up, so I’m just going to pull the source code out of the project and run around and see what’s going on.

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;
}
Copy the code

Set htmlAcceptHeaders to ‘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
Copy the code

Don’t set 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
Copy the code

The htmlAcceptHeaders attribute filters CSS and JS files. If you use the default ‘text/ HTML ‘and ‘*/*’ attributes, the CSS and JS files will be matched as HTML files. A burst of processing then causes the MIME file type of the response header to become text/ HTML and the browser cannot parse it.

It turns out that the person who wrote the document has logic problems, but he is a lazy person, do not want to explain too much, I am a stupid person can not understand his “deep meaning” at a draught.

Pit 2

Another point to note is the setting of route names. Or the URL https://www.text.com/admin/login, server all/admin routing points to the vue index. The HTML file, hash mode of routing configuration so we routing

const router = new VueRouter({
  routes: [{
        path: "/login",
        name: "login",
        component: login
    }]
})
Copy the code

At this point we’ll change to history mode

const router = new VueRouter({
  mode: 'history',
  routes: [{
        path: "/login",
        name: "login",
        component: login
    }]
})
Copy the code

Open urlhttps://www.text.com/admin/login found automatically jump to https://www.text.com/login, the reason is the routing of all point to the vue/admin index. The HTML file, Js according to our code to change the url to https://www.text.com/login, if we do not refresh the page without any problems, because all page jump or vue – the router control, index. The HTML in this file. But if you refresh the page, you get a problem. The server redetermines the file corresponding to the /login route. Therefore, when configuring vue-router in the history mode, you need to consider the directory where the project resides in the background.

For example, the above example should be changed to avoid this problem

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

Refer to the link

Router.vuejs.org/zh/guide/es…