learn-js
learn-js copied to clipboard
第十六天:写一个 route
觉得 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};
}
}