侧边栏

Preact技术栈:router和redux

发布于 | 分类于 源码分析/Preact

上篇文章中对Preact源码进行了分析,了解了Componentrenderdiff等核心方法,以及页面渲染的大致流程。在这一篇文章中,将分析preact-router的源码实现,并了解在Preact项目中使用react-redux的方法,构建一个完整的web应用。

Preact-router源码分析

首先构建一个示例demo,来演示preact-router的基本使用

js
import { createElement, render, Component } from 'preact';
// 引入源码文件,方便调试
import { Router, Link } from 'preact-router/src/index'; 
// 构建两个页面组件
class Route1 extends Component {
	render() {
		return (<div>Route1</div>);
	}
}
class Route2 extends Component {
	render() {
		return (<div>Route2</div>);
	}
}
// 整个应用根组件
class App extends Component {
	render(props) {
		let { url } = props;
		return (
			<div>
				<nav>
					<Link href="/" activeClassName="active">index</Link>
					<Link href="/route1" activeClassName="active">router1</Link>
					<Link href="/route2" activeClassName="active">router2</Link>
				</nav>
				<main>
					<Router url={url}>
						<div path="/">
							<h1>index</h1>
						</div>
						<Route1 path="/route1"/>
						<Route2 path="/route2"/>
					</Router>
				</main>
			</div>
		);
	}
}
// 挂载根组件
let root = document.createElement('div');
document.body.appendChild(root);
render(<App/>, root);

可见其使用十分简单

Router

Router最主要的作用就是根据当前的url来决定要渲染哪一个组件。因此我们直接来查看它的render函数

js
render({ children, onChange }, { url }) {
  	// 根据当前state上的url,从children中找到能够匹配vnode
		let active = this.getMatchingChildren(toChildArray(children), url, true);
		// 渲染优先级最高的那个vnode
		let current = active[0] || null;
		this._didRoute = !!current;

		let previous = this.previousUrl;
  	// 当路由发生变化时,触发props.onChange处理函数
		if (url!==previous) {
			this.previousUrl = url;
			if (typeof onChange==='function') {
				onChange({
					router: this,
					url,
					previous,
					active,
					current
				});
			}
		}

		return current;
}
// 从children列表中找到可与url匹配的vnode
getMatchingChildren(children, url, invoke) {
		return children
			.filter(prepareVNodeForRanking)
			.sort(pathRankSort)
			.map( vnode => {
				let matches = exec(url, vnode.props.path, vnode.props);
				if (matches) {
					if (invoke !== false) {
						let newProps = { url, matches };
						assign(newProps, matches);
						delete newProps.ref;
						delete newProps.key;
						return cloneElement(vnode, newProps);
					}
					return vnode;
				}
			}).filter(Boolean);
}

其中的匹配逻辑在getMatchingChildren方法中实现,我们甚至不需要关注其具体的逻辑,简单地把他理解为一个哈希映射即可:

  • 键名为url,
  • 键值为path属性为url的vnode

Link组件

我们知道了当页面url发生变化时,会触发Router组件的更新,并渲染新的页面组件。那么Link组件是如何触发url变化的呢?

js
const Link = (props) => (
	createElement('a', assign({ onClick: handleLinkClick }, props))
);

function handleLinkClick(e) {
  // 鼠标左键点击
	if (e.button==0) {
		routeFromLink(e.currentTarget || e.target || this);
		return prevent(e);
	}
}
function routeFromLink(node) {
  // 此处省略了一些检测条件
	let href = node.getAttribute('href')
	return route(href);
}

function route(url, replace=false) {
	if (typeof url!=='string' && url.url) {
		replace = url.replace;
		url = url.url;
	}
	// canRoute调用的是Router组件的canRoute方法,判断当前url是否存在可匹配的页面组件用于渲染
	if (canRoute(url)) {
    // 调用history.replaceState或调用history.pushState
		setUrl(url, replace ? 'replace' : 'push');
	}
	
	return routeTo(url);
}

function routeTo(url) {
	let didRoute = false;
  // ROUTERS是存放Router组件的数组
	for (let i=0; i<ROUTERS.length; i++) {
    // 调用Router组件的routeTo方法,渲染页面
		if (ROUTERS[i].routeTo(url)===true) {
			didRoute = true;
		}
	}
  // 处理路由变化的全局回调函数
	for (let i=subscribers.length; i--; ) {
		subscribers[i](url);
	}
	return didRoute;
}

然后回过头看一看组件的routeTo方法

js
routeTo(url) {
		this._didRoute = false;
		this.setState({ url }); // 调用setState,重新渲染组件

		if (this.updating) return this.canRoute(url);

		this.forceUpdate();
		return this._didRoute;
}

现在总结一下整个流程

  • 点击Link组件,触发组件上的handleLinkClick方法,通过e.target.getAttribute('href')获取当前组件的href属性

  • 调用route(url, replace)方法,内部调用

    • setUrl方法,触发history.pushState事件,修改浏览器地址栏的url
    • routeTo方法,内部调用Router组件的routeTo方法
    • 遍历subscribers,依次调用注册的全局回调函数
  • 组件的routeTo方法,其内部调用setStateforceUpdate,重新调用render方法

  • diff操作完成,整个页面重新渲染

到目前为止,我们应该了解到路由是如何前进(pushState)和重定向(replaceState)的了。那么,路由是如何回退的呢?

事件注册

Router组件的构造函数中,注册了全局事件

js
function initEventListeners() {
	if (eventListenersInitialized) return;

  // 实际是window.addEventListener
	if (typeof addEventListener==='function') {
		if (!customHistory) {
      // 注册popstate事件
			addEventListener('popstate', () => {
				routeTo(getCurrentUrl());
			});
		}
		addEventListener('click', delegateLinkHandler);
	}
	eventListenersInitialized = true;
}

// 处理直接通过a标签跳转的情况
function delegateLinkHandler(e) {
	if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.button!==0) return;

	let t = e.target;
  // 向上找到a标签
	do {
		if (String(t.nodeName).toUpperCase()==='A' && t.getAttribute('href')) {
			if (t.hasAttribute('native')) return;
			if (routeFromLink(t)) {
				return prevent(e);
			}
		}
	} while ((t=t.parentNode));
}

在事件初始化时,主要注册了两个事件

  • window.popstate,触发该事件时重新渲染到上一个页面组件
  • window.click,当e.targeta标签上或内部触发时,执行与Link组件相同跳转逻辑

小结

总体来说,preact-router的实现还是比较简单的。根据HTML5的History新特性,监听浏览器的前进和后退,并在路由的变化中,通过更新Router组件的state.url来实现页面组件的重新渲染。

redux

在preact项目中使用react-redux

在preact中,也可以使用redux和react-redux。下面是一个使用react-redux实现多组件之间数据通信的官方demo

js
import { createElement, render, Component } from 'preact';

import { createStore } from 'redux';
import { connect, Provider } from 'react-redux';
// 定义reducer,根据不同的action返回对应的state
const reducer = (state = { value: 0 }, action) => {
	switch (action.type) {
		case 'increment':
			return { value: state.value + 1 };
		case 'decrement':
			return { value: state.value - 1 };
		default:
			return state;
	}
}
// 初始化store
const store = createStore(reducer);
// 子组件1
class Child extends Component {
	render() {
		return (
			<div>
				<div>Child #1: {this.props.foo}</div>
				<ConnectedChild2/>
			</div>
		);
	}
}
// 通过connect方法将store的state通过props的形式注入
const ConnectedChild = connect(store => ({ foo: store.value }))(Child);

// 子组件2
class Child2 extends Component {
	render() {
		return <div>Child #2: {this.props.foo}</div>;
	}
}
// 同上注入store
const ConnectedChild2 = connect(store => ({ foo: store.value }))(Child2);

// 根组件
class App extends Component {
	render() {
		return (
			<div>
				<h1>Counter</h1>
				<Provider store={store}>
					<ConnectedChild/>
				</Provider>
				<br/>
				<button onClick={() => store.dispatch({ type: 'increment' })}>+</button>
				<button onClick={() => store.dispatch({ type: 'decrement' })}>-</button>
			</div>
		);
	}
}

// 挂载根组件
let root = document.createElement('div');
document.body.appendChild(root);
render(<App/>, root);

上面的代码展示了react-redux的在preact项目中的基本使用:

  • 编写一个reducer,根据不同的action返回对应的state,
  • reduccreateStore(reducer)方法创建一个store仓库
  • 然后通过connect方法将组件与store关联起来,将store的state数据通过props的形式注入到组件
  • 在页面上,通过Provider注入全局的store,当state更新时,同步所有订阅state的组件

可以看见,在preact中使用react-redux的方式并没有额外的改动。

preact-redux

在github上还发现了preact-redux这个库,查看其源码发现其主要功能是将reac-redux中部分API导出,因此如果想了解具体实现,直接翻阅react-redux源码比较合适

js
import { Provider, connect, connectAdvanced, ReactReduxContext } from 'react-redux';
export { Provider, connect, connectAdvanced, ReactReduxContext };
export default { Provider, connect, connectAdvanced, ReactReduxContext };

小结

状态管理是开发大型应用必备的知识点。之前写过一篇博客:理解数据状态管理,简单整理了我对于状态管理的认识。

小结

本文主要分析了preact-router的源码,可以看见类似单页面应有路由管理的实现思路

  • 通过Router组件控制对应url需要渲染的组件
  • 通过事件代理,a标签上的点击事件会调用history.pushStatehistory.replaceState来实现页面url的前进和重定向;
  • 通过window.popState事件来实现页面后退的重新渲染

然后了解了在preact项目中使用react-redux的方式,基本与在React中的使用方式保持一致。

如此看来,preact虽然是一个简易版的类React框架,但也可以用来构建前端应用了。

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。