Preact技术栈:router和redux
在上篇文章中对Preact源码进行了分析,了解了Component
、render
、diff
等核心方法,以及页面渲染的大致流程。在这一篇文章中,将分析preact-router的源码实现,并了解在Preact
项目中使用react-redux
的方法,构建一个完整的web应用。
Preact-router源码分析
首先构建一个示例demo,来演示preact-router
的基本使用
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
函数
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变化的呢?
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
方法
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
事件,修改浏览器地址栏的urlrouteTo
方法,内部调用Router组件的routeTo
方法- 遍历
subscribers
,依次调用注册的全局回调函数
组件的
routeTo
方法,其内部调用setState
和forceUpdate
,重新调用render
方法当
diff
操作完成,整个页面重新渲染
到目前为止,我们应该了解到路由是如何前进(pushState
)和重定向(replaceState
)的了。那么,路由是如何回退的呢?
事件注册
在Router
组件的构造函数中,注册了全局事件
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.target
在a
标签上或内部触发时,执行与Link
组件相同跳转逻辑
小结
总体来说,preact-router
的实现还是比较简单的。根据HTML5的History
新特性,监听浏览器的前进和后退,并在路由的变化中,通过更新Router组件的state.url
来实现页面组件的重新渲染。
redux
在preact项目中使用react-redux
在preact中,也可以使用redux和react-redux。下面是一个使用react-redux
实现多组件之间数据通信的官方demo。
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, - 用
reduc
的createStore(reducer)
方法创建一个store仓库 - 然后通过
connect
方法将组件与store关联起来,将store的state数据通过props的形式注入到组件 - 在页面上,通过
Provider
注入全局的store,当state更新时,同步所有订阅state的组件
可以看见,在preact中使用react-redux的方式并没有额外的改动。
preact-redux
在github上还发现了preact-redux这个库,查看其源码发现其主要功能是将reac-redux
中部分API导出,因此如果想了解具体实现,直接翻阅react-redux源码比较合适
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.pushState
和history.replaceState
来实现页面url的前进和重定向; - 通过
window.popState
事件来实现页面后退的重新渲染
然后了解了在preact项目中使用react-redux
的方式,基本与在React中的使用方式保持一致。
如此看来,preact虽然是一个简易版的类React框架,但也可以用来构建前端应用了。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。