learn-js icon indicating copy to clipboard operation
learn-js copied to clipboard

第十六天:写一个 route

Open sofish opened this issue 9 years ago • 0 comments

觉得 angular 2 的文档太难懂,想自己写,写到 template 渲染基本就挂,工作量太大,得出一个结果就是该作死一定会死,不想做死其实就写写 demo 或者有空就给人打打 patch 好了,自己写真的有点麻烦。不过在开始的时候写了一个简单的路由。实现一个基本功能,基本走向是:

- constructor(): 
  --> this.base = HTML_BASE || '/'
  --> this.routes = [],  this.routes.notfound = <rotue>
  --> this.current = <route> || null 
  --> event: <history> `popstate`, <link> `click || touchstart`
  --> CALL_AFTER_REFRESH: this.go(url)
- go(url): redirect to some path
- when(/url/with/:dynamic/params, createdCallback, destroyedCallback): listen to some path

所以如果功能这样,最终 api 看起来就是:

1. js api

// notfound | 404
router.when(created, destroyed);

// route with dynamic param
router.when('/some/:id/path', created, destroyed);

// navigate to a specific url
// when pathname is /base/some/path and base is /base, url equal to /some/path
route.go(url);

2. template markup

<!-- if not listened, route to `HTML_BASE/404` -->
<a href="/some/internal/path">internal</a>

<!-- open in new window, then follow the rules like above -->
<p><a href="/some/internal/path" target="_blank">target="_blank"</a></p>

<!-- trigger default behavor -->
<p><a href="http://sofi.sh">external</a></p>

实现实现代码如下:

实在是太懒了,也懒得看流行框架代码如何撸,先现撸一个可用版本:

class Router {
  constructor() {
    try {
      this.base = document.querySelector('base').getAttribute('href').replace(/(?:\/+)?$/, '');
    } catch (e) {
      this.base = '';
    }

    this.routes = [];
    this.current = null;

    // back
    window.addEventListener('popstate', function() {
      this.go(this._url());
    }.bind(this));

    // listen to links
    this._navigate();

    // wait for routes
    setTimeout(function() {
      this.go(this._url());
    }.bind(this));
  }

  when(url, created, destroyed) {
    var params = [];

    // by default, route to /404
    if(typeof url === 'function') {
      return this.routes.notfound = {
        url: '/404', created: url,
        destroyed: created,
        params
      };
    }

    url = url.replace(/:([^\/]*)/g, function(match, param) {
      params.push(param);
      return '([^\\/]+)';
    });
    url = new RegExp(`${url}$`);

    if(typeof created === 'function') {
      this.routes.push({url, created, destroyed, params});
    }
  }

  go(url, isExternalLink) {
    if(typeof path === 'number') return history.go(url);    // back or forward
    if(isExternalLink) return location = url;               // external links

    if(url[0] !== '/') url = '/' + url;
    if(url !== this.url) {
      var isFound = this.exec(url);
      // it's safe to set STATE and TITLE to null
      this.url = isFound ? url : '/404';
      history.pushState(null, null,  this.base + this.url);
    }
  }

  _url() {
    return (location.pathname + location.search + location.hash).slice(this.base.length);
  }

  _navigate() {
    let event = typeof window.ontouchstart === 'undefined' ? 'click' : 'touchstart';
    document.addEventListener(event, function(e) {
      var target = e.target;
      if(                                                   // Exclude:
        target.tagName !== 'A' || !target.href ||           //  - non links
        target.getAttribute('target') === '_blank'          //  - link open in new tab or window
      ) return;

      var url = target.href.split(/\/+/);
      var host = url[1];
      url = '/' + url.slice(2).join('/');
      if(host !== window.location.hostname) return;         //  - external link

      e.preventDefault();
      this.go(url);
    }.bind(this), false);
  }

  // exec callback
  exec(url) {
    var count = 0;
    url = this.parse(url);

    if(this.current && this.current.destroyed) this.current.destroyed();

    this.routes.forEach(function(route) {
      if(!url.pathname.match(route.url)) return;
      this.current = route;

      var data = route.url.exec(url.pathname).slice(1);
      var params = {};

      route.params.forEach((key, i) => params[key] = data[i]);
      route.created({params, search: url.search});

      count++;
    }.bind(this));

    // 404
    if(!count && this.routes.notfound && this.url !== '/404') {
      this.current = this.routes.notfound;
      this.routes.notfound.created({params: {}, search: url.search});
    }

    return count;
  }

  // parse url
  parse(url) {
    url = url.split('?');
    var search = {};
    var pathname = url[0];

    url = url[1] ? url[1].split('&') : [];
    url.forEach(function(pair) {
      pair = pair.split('=');

      let key = pair[0];
      let value = pair[1];

      if(!/\[\]$/.test(key)) return search[key] = value;

      search[key] = search[key] || [];
      search[key].push(value);
    });

    return {pathname, search};
  }
}

sofish avatar Jul 28 '16 04:07 sofish