单页面路由即在前端单页面实现的一种路由, 由于 React,vue 等框架的火热, 我们可以很容易构建一个用户体验良好的单页面应用, 但是如果我们要在浏览器改变路由的时候, 在不请求服务器的情况下渲染不同的内容, 就要类似于后端的路由系统, 在前端也实现一套完整的路由系统
下面让我们来实现一个简单的路由系统. 该路由系统将基于 React 进行书写. 在写之前, 我们先仔细想下, 我们应该从哪方面入手. 这是最终实现的效果 simple-react-router-demo https://codepen.io/xwchris/pen/PdNyJQ
功能思考
不论是前端还是后端路由, 我们都可以通过一种路由匹配加匹配后回调的方式来实现. 如果没有理解也没有关系, 后面会理解. 我们用一个对象 Router 来表示我们的 Router 对象. 先来想下我们要做哪些工作
配置路由模式 history 和 hash
添加和删除路由
监听路由变化并调用对应路由回调
暴露路由跳转函数
实现路由核心部分
首先我们来实现我们的 router-core.js.
- 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 是为了实现链式调用, 除了工具函数, 后面其他函数也会保持这种写法.
配置完 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 / 的写法. 我们将尾部的 / 去掉. 该函数实现如下
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 实现上来, 根据我们一开始列的功能, 我们还要实现路由改变监听事件, 由于我们有两种路由模式 history 和 hash, 因此要进行判断.
- 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;
- }
复制代码
但这里我们基本已经完成了我们路由的核心部分.
来源: https://juejin.im/post/5b842314518825430e571d7c