zoukankan      html  css  js  c++  java
  • 前端路由的实现(二)

    HTML5History

    History interface是浏览器历史记录栈提供的接口,通过back(), forward(), go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。

    从HTML5开始,History interface提供了两个新的方法:pushState(), replaceState()使得我们可以对浏览器历史记录栈进行修改


    • stateObject: 当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本

    • title: 所添加记录的标题

    • URL: 所添加记录的URL

    这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器不会立即发送请求该URL(the browser won't attempt to load this URL after a call to pushState()),这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。

    我们来看vue-router中的源码:

    var HTML5History = (function (History$$1) {
      function HTML5History (router, base) {
        var this$1 = this;
    
        History$$1.call(this, router, base);
    
        var expectScroll = router.options.scrollBehavior;
    
        if (expectScroll) {
          setupScroll();
        }
    
        var initLocation = getLocation(this.base);
        window.addEventListener('popstate', function (e) {
          var current = this$1.current;
    
          // Avoiding first `popstate` event dispatched in some browsers but first
          // history route not updated since async guard at the same time.
          var location = getLocation(this$1.base);
          if (this$1.current === START && location === initLocation) {
            return
          }
    
          this$1.transitionTo(location, function (route) {
            if (expectScroll) {
              handleScroll(router, route, current, true);
            }
          });
        });
      }
    
      if ( History$$1 ) HTML5History.__proto__ = History$$1;
      HTML5History.prototype = Object.create( History$$1 && History$$1.prototype );
      HTML5History.prototype.constructor = HTML5History;
    
      HTML5History.prototype.go = function go (n) {
        window.history.go(n);
      };
    
      HTML5History.prototype.push = function push (location, onComplete, onAbort) {
        var this$1 = this;
    
        var ref = this;
        var fromRoute = ref.current;
        this.transitionTo(location, function (route) {
          pushState(cleanPath(this$1.base + route.fullPath));
          handleScroll(this$1.router, route, fromRoute, false);
          onComplete && onComplete(route);
        }, onAbort);
      };
    
      HTML5History.prototype.replace = function replace (location, onComplete, onAbort) {
        var this$1 = this;
    
        var ref = this;
        var fromRoute = ref.current;
        this.transitionTo(location, function (route) {
          replaceState(cleanPath(this$1.base + route.fullPath));
          handleScroll(this$1.router, route, fromRoute, false);
          onComplete && onComplete(route);
        }, onAbort);
      };
    
      HTML5History.prototype.ensureURL = function ensureURL (push) {
        if (getLocation(this.base) !== this.current.fullPath) {
          var current = cleanPath(this.base + this.current.fullPath);
          push ? pushState(current) : replaceState(current);
        }
      };
    
      HTML5History.prototype.getCurrentLocation = function getCurrentLocation () {
        return getLocation(this.base)
      };
    
      return HTML5History;
    }(History));

    代码结构以及更新视图的逻辑与hash模式基本类似,只不过将对window.location.hash直接进行赋值window.location.replace()改为了调用history.pushState()和history.replaceState()方法。

     在HTML5History中添加对修改浏览器地址栏URL的监听是直接监听popstate事件

        window.addEventListener('popstate', function (e) {
          var current = this$1.current;
    
          // Avoiding first `popstate` event dispatched in some browsers but first
          // history route not updated since async guard at the same time.
          var location = getLocation(this$1.base);
          if (this$1.current === START && location === initLocation) {
            return
          }
    
          this$1.transitionTo(location, function (route) {
            if (expectScroll) {
              handleScroll(router, route, current, true);
            }
          });
        });

    当然了HTML5History用到了HTML5的新特特性,是需要特定浏览器版本的支持的,前文已经知道,浏览器是否支持是通过变量supportsPushState来检查的:

    var inBrowser = typeof window !== 'undefined';
    var supportsPushState = inBrowser && (function () {
      var ua = window.navigator.userAgent;
    
      if (
        (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
        ua.indexOf('Mobile Safari') !== -1 &&
        ua.indexOf('Chrome') === -1 &&
        ua.indexOf('Windows Phone') === -1
      ) {
        return false
      }
    
      return window.history && 'pushState' in window.history
    })();

    以上就是hash模式与history模式源码的导读,这两种模式都是通过浏览器接口实现的,除此之外vue-router还为非浏览器环境准备了一个abstract模式,其原理为用一个数组stack模拟出浏览器历史记录栈的功能。当然,以上只是一些核心逻辑,为保证系统的鲁棒性源码中还有大量的辅助逻辑,也很值得学习。此外在vue-router中还有路由匹配、router-view视图组件等重要部分,关于整体源码的阅读推荐滴滴前端的这篇文章

    两种模式比较

    在一般的需求场景中,hash模式与history模式是差不多的,但几乎所有的文章都推荐使用history模式,理由竟然是:"#" 符号太丑...0_0 "

    当然,严谨的我们肯定不应该用颜值评价技术的好坏。根据MDN的介绍,调用history.pushState()相比于直接修改hash主要有以下优势:

    • pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL

    • pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发记录添加到栈中

    • pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串

    • pushState可额外设置title属性供后续使用

    history模式的一个问题

    我们知道对于单页应用来讲,理想的使用场景是仅在进入应用时加载index.html,后续在的网络操作通过Ajax完成,不会根据URL重新请求页面,但是难免遇到特殊情况,比如用户直接在地址栏中输入并回车,浏览器重启重新加载应用等。

    hash模式仅改变hash部分的内容,而hash部分是不会包含在HTTP请求中的:

    故在hash模式下遇到根据URL请求页面的情况不会有问题。

    而history模式则会将URL修改得就和正常请求后端的URL一样

    在此情况下重新向后端发送请求,如后端没有配置对应/user/id的路由处理,则会返回404错误。官方推荐的解决办法是在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。同时这么做以后,服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果是用 Node.js 作后台,可以使用服务端的路由来匹配 URL,当没有匹配到路由的时候返回 404,从而实现 fallback。

    直接加载应用文件

    Tip: built files are meant to be served over an HTTP server.

    Opening index.html over file:// won't work.

    Vue项目通过vue-cli的webpack打包完成后,命令行会有这么一段提示。通常情况,无论是开发还是线上,前端项目都是通过服务器访问,不存在 "Opening index.html over file://" ,但程序员都知道,需求和场景永远是千奇百怪的,只有你想不到的,没有产品经理想不到的。

    本文写作的初衷就是遇到了这样一个问题:需要快速开发一个移动端的展示项目,决定采用WebView加载Vue单页应用的形式,但没有后端服务器提供,所以所有资源需从本地文件系统加载:

    // AndroidAppWrapper
    public class MainActivity extends AppCompatActivity {
    
     private WebView webView;
    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
    
     webView = new WebView(this);
     webView.getSettings().setJavaScriptEnabled(true);
     webView.loadUrl("file:///android_asset/index.html");
     setContentView(webView);
     }
    
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
     if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
     webView.goBack();
     return true;
     }
     return false;
     }
    }

    此情此景看来是必须 "Opening index.html over file://" 了,为此,我首先要进行了一些设置

    • 在项目config.js文件中将assetsPublicPath字段的值改为相对路径 './'

    • 调整生成的static文件夹中图片等静态资源的位置与代码中的引用地址一致

    这是比较明显的需要改动之处,但改完后依旧无法顺利加载,经过反复排查发现,项目在开发时,router设置为了history模式(为了美观...0_0"),当改为hash模式后就可正常加载了。

    为什么会出现这种情况呢?我分析原因可能如下:

    当从文件系统中直接加载index.html时,URL为:

    file:///android_asset/index.html
    

    而首页视图需匹配的路径为path: '/' :

    export default new Router({
     mode: 'history',
     routes: [
     {
     path: '/',
     name: 'index',
     component: IndexView
     }
     ]
    })

    我们先来看history模式,在HTML5History中:

    HTML5History.prototype.ensureURL = function ensureURL (push) {
        if (getLocation(this.base) !== this.current.fullPath) {
          var current = cleanPath(this.base + this.current.fullPath);
          push ? pushState(current) : replaceState(current);
        }
      };
    function getLocation (base) {
      var path = window.location.pathname;
      if (base && path.indexOf(base) === 0) {
        path = path.slice(base.length);
      }
      return (path || '/') + window.location.search + window.location.hash
    }

    逻辑只会确保存在URL,path是通过剪切的方式直接从window.location.pathname获取到的,它的结尾是index.html,因此匹配不到 '/' ,故 "Opening index.html over file:// won't work" 。

    再看hash模式,在HashHistory中:

    function ensureSlash () {
      var path = getHash();
      if (path.charAt(0) === '/') {
        return true
      }
      replaceHash('/' + path);
      return false
    }

    我们看到在代码逻辑中,多次出现一个函数ensureSlash(),当#符号后紧跟着的是'/',则返回true,否则强行插入这个'/',故我们可以看到,即使是从文件系统打开index.html,URL依旧会变为以下形式:

    file:///C:/Users/dist/index.html#/

    getHash()方法返回的path为 '/' ,可与首页视图的路由匹配。

    故要想从文件系统直接加载Vue单页应用而不借助后端服务器,除了打包后的一些路径设置外,还需确保vue-router使用的是hash模式。

     


     
  • 相关阅读:
    作为一名程序员应该具备哪些素质
    从100万个整数里找出100个最大的数
    数据库 SQL语句优化
    服务器上文件打包下载
    ThreadPoolExcutor
    几种序列化与get、set方法的关系
    idea没有错误出现红色波浪线怎么去掉?
    两个对象的属性赋值
    RandomStringUtils的使用
    IDEA中类似eclipse的workSpace的实现
  • 原文地址:https://www.cnblogs.com/xuzhudong/p/8870723.html
Copyright © 2011-2022 走看看