使用mve实现嵌套路由
实现单层的hash路由很简单,以全局hash路由为例,只需要监听浏览器的hash变化,找到新的hash值对应的组件,然后卸载旧组件、装上新组件。
嵌套路由我的理解是这样的,依然以hash路由为例,http://localhost:8080/#/main/panel2 和http://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使用方式。
创作不易,如果给你带来帮助,欢迎给作者打赏。
微信

支付宝
