单页面路由原理及实现

单页面路由即在前端单页面实现的一种路由,因为React,Vue等框架的火热,咱们能够很容易构建一个用户体验良好的单页面应用,可是若是咱们要在浏览器改变路由的时候,在不请求服务器的状况下渲染不一样的内容,就要相似于后端的路由系统,在前端也实现一套完整的路由系统前端

下面让咱们来实现一个简单的路由系统。该路由系统将基于React进行书写。在写以前,咱们先仔细想下,咱们应该从哪方面入手。这是最终实现的效果simple-react-router-demoreact

功能思考

不管是前端仍是后端路由,咱们均可以经过一种路由匹配加匹配后回调的方式来实现。若是没有理解也没有关系,后面会理解。 咱们用一个对象Router来表示咱们的Router对象。先来想下咱们要作哪些工做git

  1. 配置路由模式historyhash
  2. 添加和删除路由
  3. 监听路由变化并调用对应路由回调
  4. 暴露路由跳转函数

实现路由核心部分

首先咱们来实现咱们的router-core.jsgithub

const Router = {
	routes: [], // 用来存放注册过的路由
	mode: null, // 用来标识路由模式
}
复制代码

咱们用两个属性来存放咱们须要存储的路由和路由模式正则表达式

下面在刚才的基础上添加一个config函数,让咱们的路由可以配置后端

const Router = {
	// ... routes mode
	config: (options) => {
		Router.mode = options && options.mode && options.mode === 'history' && !!history.pushState ? 'history' : 'hash';
		return Router;
	}
}
复制代码

config函数中咱们经过传入的配置,来配置咱们的路由模式,当且仅当options.mode === 'history'historyapi存在的时候咱们设置Router模式为history。返回Router是为了实现链式调用,除了工具函数,后面其余函数也会保持这种写法。api

配置完Router模式后,咱们要可以添加和删除路由浏览器

const Router = {
	// ... routes mode config
	add: (pathname, handler) => {
		Router.routes.push({ pathname: clearEndSlash(pathname), handler });
		return Router;
	},
	remove: (pathname) => {
		Router.routes.forEach((route, index) => {
			if (route.pathname === clearEndSlash(pathname)) {
				Router.routes.splice(index, 1);
			}
		});
		return Router;
	}
}
复制代码

在添加和删除路由函数中,咱们传入了名为pathname的变量,为了跟后面普通的path区分开。直白点来讲,就是pathname对应/person/:id的写法,path对应/person/2的写法。服务器

这里有个clearEndSlash函数,是为了防止有/home/的写法。咱们将尾部的/去掉。该函数实现以下react-router

const clearEndSlash = (path = '') => path.toString().replace(/\/$/, '') || '/';
复制代码

为了方便比较,在完成添加和删除后咱们来实现一个match函数,来肯定pathname是否和path相匹配。

const Router = {
	// ... routes mode config add remove
	match: (pathname, path) => {
		const reg = pathToRegexp(pathname);
		return reg.test(path);
	}
}
复制代码

这里使用pathToRegexp函数将pathname解析成正则表达式,而后用该正则表达式来判断时候和path匹配。pathToRegexp的实现以下

const pathToRegexp = (pathname, keys = []) => {
  const regStr = '^' + pathname.replace(/\/:([^\/]+)/g, (_, name) => {
    keys.push({ name });
    return '/([^/]+)';
  });
  return new RegExp(regStr);
}
复制代码

函数返回解析后的正则表达式,其中keys参数用来记录参数name。举个例子

// 调用pathToRegexp函数
const keys = [];
const reg = pathToRegexp('/person/:id/:name', keys);
console.log(reg, keys);

// reg: /^\/person\/([^\/]+)\/([^\/]+)/
// keys: [ { name: 'id' }, { name: 'name' } ]
复制代码

好像有点扯远了,回到咱们的Router实现上来,根据咱们一开始列的功能,咱们还要实现路由改变监听事件,因为咱们有两种路由模式historyhash,所以要进行判断。

const Router = {
	// ... routes mode config add remove match
	current: () => {
		if (Router.mode === 'history') {
			return location.pathname;
		}
		
		return location.hash;
	},
	listen: () => {
		let currentPath = Router.current();
		
		const fn = () => {
			const nextPath = Router.current();
			if (nextPath !== currentPath) {
				currentPath = nextPath;
				
				const routes = Router.routes.filter(route => Router.match(route.pathname, currentPath));
				routes.forEach(route => route.handler(currentPath));
			}
		}
		
		clearInterval(Router.interval);
		Router.interval = setInterval(fn, 50);
		return Router;
	}
}
复制代码

路由改变事件监听,在使用History API的时候可使用popstate事件进行监听,Hash改变可使用hashchnage事件进行监听,可是因为在不一样浏览器上有些许不一样和兼容性问题,为了方便咱们使用setInterval来进行监听,每隔50ms咱们就来判断一次。

最后咱们须要实现一个跳转函数。

const Router = {
	// ... routes mode config add remove match current listen
	navigate: path => {
		if (Router.mode === 'history') {
			history.pushState(null, null, path);
		} else {
			window.location.href = window.location.href.replace(/#(.*)$/, '') + path;
		}
	}
	return Router;
}
复制代码

但这里咱们基本已经完成了咱们路由的核心部分。

下一节,咱们将在该核心部分的基础上,实现几个路由组件,以达到react-router的部分效果。

这是原文地址,该部分的完整代码能够在个人Github的simple-react-router上看到,若是你喜欢,能顺手star下就更好了♪(・ω・)ノ,可能有部分理解错误或书写错误的地方,欢迎指正。

参考连接

相关文章
相关标签/搜索