使用mve实现嵌套路由

248 阅读8分钟

使用mve实现嵌套路由

实现单层的hash路由很简单,以全局hash路由为例,只需要监听浏览器的hash变化,找到新的hash值对应的组件,然后卸载旧组件、装上新组件。

嵌套路由我的理解是这样的,依然以hash路由为例,http://localhost:8080/#/main/panel2http://localhost:8080/#/main/panel2 ,是有部分相同的,比如侧边栏。hash变化只会导致中心区域变化。而它们与http://localhost:8080/#/login 是从根组件完全不同的。即如果hash中有相同的路径节点,就会按组件的实现方式部分复用。即,hash只监听一层(自己的顶层)。

我们先定义这样的模型

import {EOChildren} from 'mve-core/childrenBuilder'

export interface Router{
	change(path:string[],query?:{[key:string]:string|string[]}):void
	children:EOChildren<Node>
}

change是通知这个组件自己管理这一层路由变化,它的path是按斜线拆分的片段,这是为了处理多斜线问题。query是查询参数。children是这个组件的内容本身。

我们仅考虑静态路由,在路由的树状结构中,每个枝节点,接受N个路由作为参数,返回一个路由节点。但事实上,每个路由的布局都是自定义的,同时,为了去中心化,我们要对局部路由自定义分散到各处,实现一种局部的高耦合,全局的低聚合。

将hash拆分为path与query,只是一种约定,虽然也可以以其它方式拆分。因为局部路由自定义过后,向自己的子级怎样传,完全不再受到上层的约束。

我们应该这样想,为了实现url到界面的最大程度映射变化,我们在局部路由组件里需要最上层的信息,至于调用自己的上层怎样调用自己,则是它的事。

我们能做什么封装,方便路由组件的自定义化?就是一个简单的自己层级的hash变化,到子路由区间的替换。一个路由组件其实可以变化多个子路由区域。

change是局部路由接受到变更事件,事实上有初始化事件。我们仍然当change事件去处理,减少复杂度。

路由替换,其实很像ifChildren,根据计算确定用什么组件。只是ifChildren是无缓存组件的。路由替换是非常简单的,也不需要ifChildren的计算,只需要显式地挂载与销毁。同时为了与布局灵活协作,我们不用一层div之类的来包裹,而选择modelChildren,这个arrayModel里始终只存放一个路由组件。

我们调整代码如下


import {EOChildren} from 'mve-core/childrenBuilder'
import {modelChildren} from 'mve-core/modelChildren'
import {mve} from 'mve-core/util'

export interface Router{
	change(path:string[],query?:{[key:string]:string|string[]}):void
	children:EOChildren<Node>
}

export function router(routes:{[key:string]:Router}):Router{
	const currentRoute=mve.arrayModelOf<Router>([])
	return {
		change(path,query){
			const [first,...rest]=path
			const route=currentRoute.get(0)
			const changeRoute=routes[first]
			if(changeRoute==route){
				//并未发生改变
				changeRoute.change(rest,query)
			}else{
				//发生了改变,替换当前route
				currentRoute.remove(0)
				currentRoute.push(changeRoute)
				changeRoute.change(rest,query)
			}
		},
		children:modelChildren(currentRoute,function(me,row,i){
			return row.children
		})
	}
}

新加了router代码,处理静态的局部路由。当上层的改变发生过来时,先判断是否和当前路由块匹配,如果匹配,直接通知子层。否则,替换当前路由块,并通知其子层。

这块代码里其实可以看到一些优化、放开的地方。比如path,其实可以用Pair类型,类似Lisp的列表,我们也要避免下层组件直接修改path数组的副作用。这里ts/js语法性,收紧意义不大,暂时不考虑了。以及children字段,modelChildren里面返回了me这个生命周期字段,且是函数的过程,可以放开。将children改成render,调整后代码如下:


import {EOChildren} from 'mve-core/childrenBuilder'
import {modelChildren} from 'mve-core/modelChildren'
import {mve} from 'mve-core/util'

export interface Router{
	change(path:string[],query?:{[key:string]:string|string[]}):void;
	render(me:mve.LifeModel):EOChildren<Node>;
}

export function router(routes:{[key:string]:Router}){
	const currentRoute=mve.arrayModelOf<Router>([])
	return {
		change(path:string[],query?:{[key:string]:string|string[]}){
			const [first,...rest]=path
			const route=currentRoute.get(0)
			const changeRoute=routes[first]
			if(changeRoute==route){
				//并未发生改变
				changeRoute.change(rest,query)
			}else{
				//发生了改变,替换当前route
				currentRoute.push(changeRoute)
				changeRoute.change(rest,query)
			}
		},
		children:modelChildren(currentRoute,function(me,row,i){
			return row.render(me)
		})
	}
}

这里router的返回类型就不使用Router了,毕竟children需要嵌套到自定义路由之中,且me这个生命周期函数不必要。

一个子组件内有多个路由区域,如果每一个都去手动触发change,是不是太麻烦了?我们想到每次通知都是必然触发change的,我们将change做成观察式的,即局部注册路由区域自动响应hash变化。

import {EOChildren} from 'mve-core/childrenBuilder'
import {modelChildren} from 'mve-core/modelChildren'
import {mve} from 'mve-core/util'
export interface RouterParam{
	path:string[]
	query:{[key:string]:string|string[]}
}
export interface Router{
	route:mve.Value<RouterParam|void>
	render(me:mve.LifeModel):EOChildren<Node>;
}
export function router(me:mve.LifeModel,param:mve.Value<RouterParam>,routes:{[key:string]:Router}){
	const currentRoute=mve.arrayModelOf<Router>([])
	me.WatchAfter(
		param,
		function(p){
			if(p){
				const {path:[first,...rest],query}=p
				const currentR=currentRoute.get(0)
				const changeR=routes[first]
				if(currentR==changeR){
					//并未发生改变
					currentR.route({
						path:rest,query
					})
				}else{
					//发生了改变,替换当前route
					currentRoute.remove(0)
					currentRoute.push(changeR)
					changeR.route({
						path:rest,query
					})
				}
			}
		}
	)
	return modelChildren(currentRoute,function(me,row,i){
		return row.render(me)
	})
}

这里,我们直接把每个router组件的路由参数变成mve.Value,方便每个子路由模块观察它。同时考虑初始化可能变成空的情况,空的情况就不处理。然后router函数就单纯变成渲染子组件。

小试牛刀

我们将之前的代码小作调整,原router改成routerView,新增创建Router的便利函数。同时处理一些潜在问题:路径初始化或变成空,或路由没有找到将默认区域渲染成404。

import {EOChildren} from 'mve-core/childrenBuilder'
import {modelChildren} from 'mve-core/modelChildren'
import {mve} from 'mve-core/util'
import { dom } from 'mve-dom'
export interface RouterParam{
	path:string[]
	query:{[key:string]:string|string[]}
}
export interface Router{
	route:mve.Value<RouterParam>
	render(me:mve.LifeModel):EOChildren<Node>;
}
export function routerView(me:mve.LifeModel,param:mve.Value<RouterParam>,routes:{[key:string]:Router}){
	const currentRoute=mve.arrayModelOf<Router>([])
	function clear(){
		if(currentRoute.size()>0){
			currentRoute.remove(0)
		}
	}
	me.WatchAfter(
		param,
		function(p){
			if(p){
				const {path:[first,...rest],query}=p
				const currentR=currentRoute.get(0)
				if(first){
					//有路径
					const changeR=routes[first] || default404Router
					//并且能找到路由
					if(currentR==changeR){
						//并未发生改变
						currentR.route({
							path:rest,query
						})
					}else{
						//发生了改变,替换当前route
                        if(currentR){
                            currentRoute.remove(0)
                        }
						currentRoute.push(changeR)
						changeR.route({
							path:rest,query
						})
					}
				}else{
					clear()
				}
			}else{
				clear()
			}
		}
	)
	return modelChildren(currentRoute,function(me,row,i){ 
		return row.render(me) 
	})
}
export function router(fun:(me:mve.LifeModel,route:mve.Value<RouterParam>)=>EOChildren<Node>):Router{
	const route=mve.valueOf<RouterParam>({path:[],query:{}})
	return {
		route,
		render(me){
			return fun(me,route)
		}
	}
}
const default404Router=router(function(me,route){
	return dom({
		type:"div",
		style:{
			display:"flex",'flex-direction':"column",'align-items':'center','justify-content':'center'
		},
		children:[
			dom({
				type:"div",
				text:"404"
			}),
			dom({
				type:"div",
				text(){
					const v=route()
					return v.path.join("")
				}
			}),
			dom({
				type:"div",
				text(){
					const v=route()
					return JSON.stringify(v.query)
				}
			})
		]
	})
})

改造入口的首页,创建demo演示:

import { clsOf, dom } from "mve-dom/index"
import { routerView, RouterParam, router } from "./router"
import {mve} from 'mve-core/util'
const topAreaCls=clsOf("topArea")
const routerParam=mve.valueOf<RouterParam>({path:[],query:{}})
const root=dom.root(function(me){
	return {
		type:"div",
		children:[
			dom({
				type:"style",
				text:`
				.${topAreaCls}{
					position:fixed;
					top:0;
					left:0;
					width:100%;
					height:100%
				}
				`
			}),
			routerView(me,routerParam,{
				index:router(function(me,route){
					return dom({
						type:"div",
						cls:topAreaCls,
						style:{
							background:"green",
						},
						children:[
							dom({
								type:"button",
								text:"去user",
								event:{
									click(){
										location.hash="user/a"
									}
								}
							})
						]
					})
				}),
				user:router(function(me,route){
					return dom({
						type:"div",
						cls:topAreaCls,
						style:{
							background:"blue"
						},
						children:[
							dom({
								type:"input",
								attr:{
									placeHolder:"测试路由缓存"
								}
							}),
							routerView(me,route,{
								a:router(function(me,route){
									return dom({
										type:"div",
										children:[
											dom({
												type:"button",
												text:"去user-b",
												event:{
													click(){
														location.hash="user/b"
													}
												}
											})
										]
									})
								}),
								b:router(function(me,route){
									return dom({
										type:"div",
										children:[
											dom({
												type:"button",
												text:"去首页",
												event:{
													click(){
														location.hash="user/c"
													}
												}
											})
										]
									})
								}),
							})
						]
					})
				})
			})
		]
	}
})

function hashChange(){
	const hash=location.hash
	const query={}
	const path=[]
	if(hash){
		const [pathStr,queryStr]=hash.split("?")
		pathStr.slice(1).split("/").forEach(v=>{
			if(v){
				path.push(v)
			}
		})
		if(queryStr){
			queryStr.split("&").forEach(kv=>{
				const [k,v]=kv.split("=")
				const oldV=query[k]
				if(oldV){
					oldV.push(v)
				}else{
					query[k]=v
				}
			})
		}
	}
	routerParam({path,query})
}
window.addEventListener("hashchange",hashChange)
hashChange()
document.body.appendChild(root.element)
if(root.init){
	root.init()
}
if(root.destroy){
	window.addEventListener("unload",root.destroy)
}

这里我使用上次创建的mve-vite-demo项目《mve:从一个简单的todo开始——使用》:juejin.cn/post/700397…

代码没多说的,这里为了示例简单,直接在当前嵌套页面。为了表明user时上层是被缓存的,加了个输入框,在user/a跳转到user/b之前,你可以在输入框里输入文字,可以看到它被缓存起来。

该项目代码在 gitee.com/wy2010344/m… 演示静态地址是 wy2010344.gitee.io/mve-vite-de…

你看到该文章时可能main.ts已经改变为main_multiRoute.ts。因为我已经将全部代码迁成这种模式。未来如果有新的演示,都默认在这个router框架下执行。

后记

个人不是很喜欢router,但不得不说它提供了一个对外的直达具体页面的方式。但在内部定义太多router,且内部也相互用router跳转,就是一种弱类型编程,很容易出问题的。

附录

欢迎使用小议的mve。它包含mve-core与mve-dom两部分,前一部分与dom无关,可自定义其它引擎,后一部分是mve-core在dom上的简单桥接。

git地址

文章包地址::gitee.com/wy2010344/a…

mve-core:

gitee : gitee.com/wy2010344/n…

github : github.com/wy2010344/n…

mve-dom:

gitee : gitee.com/wy2010344/n…

github : github.com/wy2010344/n…

特点

  • 过程式(函数式)基因,比react更早。作者避免在ts/js中使用class/this等概念。
  • 无组件概念,完全使用编程语言本身的模块(函数)复用。
  • 基础模型只有mve.Value<T>和mve.ArrayModel<T>,构建出庞大的页面逻辑。
  • 界面布局使用纯ts/js,不引入复杂的xml,更高效地复用。
  • mvvm对属性节点针对性更新(受vue启发),性能高。并创新地发现使用mve.ArrayModel<T>与界面片段一一对应(支持同级嵌套),不使用虚拟DOM与任何形式的享元复用,和异步更新。
  • 区分与DOM引擎无关的核心模块,可用于其它自定义引擎。并提供简单的dom桥接模块。
  • 开放可定制自己的mve使用方式。

创作不易,如果给你带来帮助,欢迎给作者打赏。

微信

微信收款码

支付宝

支付宝收款码