资讯专栏INFORMATION COLUMN

Router入门0x202: 自己实现 Router 页面调度和特定页面访问

dance / 3432人阅读

摘要:概述上一章讲了如何实现组件页面切换,这一章讲如何解决上一章出现的问题以及如何优雅的实现页面切换。在中监听了事件,这样就可以在变化的时候,需要路由配置并调用。

0x000 概述

上一章讲了SPA如何实现组件/页面切换,这一章讲如何解决上一章出现的问题以及如何优雅的实现页面切换。

0x001 问题分析

回顾一下上一章讲的页面切换,我们通过LeactDom.render(new ArticlePage(),document.getElementById("app"))来切换页面,的确做到了列表页和详情页的切换,但是我们可以看到,浏览器的网址始终没有变化,是http://localhost:8080,那如果我们希望直接访问某个页面,比如访问某篇文章呢?也就是我们希望我们访问的地址是http://localhost:8080/article/1,进入这个地址之后,可以直接访问id 为1的文章。
问题1:无法做到访问特定页面并传递参数
问题2:通过LeactDom.render(new ArticlePage(),document.getElementById("app"))太冗余了

0x002 简单的路由实现并和原生js结合:
基本上也是基于发布-订阅模式,
register: 注册路由
push: 路由跳转

源码

class Router {
    static routes = {}

    /**
     * 如果是数组
     * 就遍历数组并转化成 {"/index":{route:{...},callback:()=>{....}}} 形式
     * 并执行 init 方法
     * 如果是对象
     * 就转化成 {"/index":{route:{...},callback:()=>{....}}} 形式
     * 并和原来的 this.route 合并
     * 注意: 如果用对象形式必须手动执行 init 方法
     * 最终 this.route 形式为
     * [
     *  {"/index":{route:{...},callback:()=>{....}}}
     *  {"/detail":{route:{...},callback:()=>{....}}}
     * ]
     * @param routes
     * @param callback
     */
    static register(routes, callback) {
        if (Array.isArray(routes)) {
            this.routes = routes.map(route => {
                return {
                    [route.path]: {
                        route: route,
                        callback: callback
                    }
                }
            }).reduce((r1, r2) => {
                return {...r1, ...r2}
            })
        }
        this.routes = {
            ...this.routes,
            ...{
                [routes.path]: {
                    route: routes,
                    callback: callback
                }
            }
        }
    }

    /**
     * 跳转到某个路由
     * 本质是遍历所有路由并执行 callback
     *
     * @param path
     * @param data
     */
    static push(path, data) {
        Object.values(this.routes).forEach(route => {
            route.callback(data, this.routes[ path].route, path)
        })
    }

}

export default Router

使用

import Router from "./core/Router";

Router.register([
    {
        path: "/index",
        name: "主页",
        component: (props) => {
            return document.createTextNode(`这是${props.route.name}`)
        }
    },
    {
        path: "/detail",
        name: "详情页",
        component: (props) => {
            return document.createTextNode(`这是${props.route.name}`)
        }
    }
], (data, route, match) => {
    if (route.path === match) {
        let app = document.getElementById("app")
        app.childNodes.forEach(c => c.remove())
        app.appendChild(new route.component({data,route,match}))
    }
})

Router.push("/index")

setTimeout(()=>{
    Router.push("/detail")
},3000)

说明:
push方法调用的时候,会触发register的时候传入的callback,并找到push传入的path匹配的路由信息,然后将该路由信息作为callback的参数,并执行callback
在上面的流程中,我们注册了两个路由,每个路由的配置信息大概包含了pathnamecomponent三个键值对,但其实只有path是必须的,其他的都是非必须的,可以结合框架、业务来传需要的参数;在注册路由的同时传入了路由触发时的动作。这里设定为将父节点的子节点全部移除后替换为新的子节点,也就达到了组件切换的功能,通过callbackprops参数,我们可以获取到当前触发的路由配置和触发该路由配置的时候的数据,比如后面调用Route.push("/index",{name:1})的时候,callbackprops

{
    data:{
        name:1
    },
    route:{ 
        path: "/index",
        name: "主页",
        component: (props) => {
                return document.createTextNode(`这是${props.route.name}`)
            }
    }
}

0x003 和上一章的SPA结合
import Router from "./core/Router";
import DetailPage from "./page/DetailPage";
import ArticlePage from "./page/ArticlePage";
import LeactDom from "./core/LeactDom";

Router.register([
    {
        path: "/index",
        name: "主页",
        component: ArticlePage
    },
    {
        path: "/detail",
        name: "详情页",
        component: DetailPage
    }
], (data, route,match) => {
    if (route.path !== match) return
    LeactDom.render(new route.component(data), document.getElementById("app"))
})

然后在页面跳转的地方,修改为Route跳转

    // ArticlePage#componentDidMount
    componentDidMount() {
            let articles = document.getElementsByClassName("article")
            ;[].forEach.call(articles, article => {
                    article.addEventListener("click", () => {
                        // LeactDom.render(new DetailPage({articleId: article.getAttribute("data-id")}), document.getElementById("app"))
                        Router.push("/detail",{articleId:article.getAttribute("data-id")})
                    })
                }
            )
    
        }

    // DetailPage#componentDidMount
    componentDidMount() {
        document.getElementById("back").addEventListener("click", () => {
            LeactDom.render(new ArticlePage(), document.getElementById("app"))
            Router.push("/index")
        })
    }
0x004 指定跳转页面-hash

先看结果,我们希望我们在访问http://localhost:8080/#detail?articleId=2的时候跳转到id=2的文章的详情页面,所以我们需要添加几个方法:

    import Url from "url-parse"

class Router {
    static routes = {}

    /**
     * 初始化路径
     * 添加 hashchange 事件, 在 hash 发生变化的时候, 跳转到相应的页面
     * 同时根据访问的地址初始化第一次访问的页面
     *
     */
    static init() {
        Object.values(this.routes).forEach(route => {
            route.callback(this.queryStringToParam(), this.routes["/" + this.getPath()].route,"/"+this.getPath())
        })

        window.addEventListener("hashchange", () => {
            Object.values(this.routes).forEach(route => {
                route.callback(this.queryStringToParam(), this.routes["/" + this.getPath()].route,"/"+this.getPath())
            })
        })

    }

    /**
     * 如果是数组
     * 就遍历数组并转化成 {"/index":{route:{...},callback:()=>{....}}} 形式
     * 并执行 init 方法
     * 如果是对象
     * 就转化成 {"/index":{route:{...},callback:()=>{....}}} 形式
     * 并和原来的 this.route 合并
     * 注意: 如果用对象形式必须手动执行 init 方法
     * 最终 this.route 形式为
     * [
     *  {"/index":{route:{...},callback:()=>{....}}}
     *  {"/detail":{route:{...},callback:()=>{....}}}
     * ]
     * @param routes
     * @param callback
     */
    static register(routes, callback) {
        if (Array.isArray(routes)) {
            this.routes = routes.map(route => {
                return {
                    [route.path]: {
                        route: route,
                        callback: callback
                    }
                }
            }).reduce((r1, r2) => {
                return {...r1, ...r2}
            })
            this.init()
        }
        this.routes = {
            ...this.routes,
            ...{
                [routes.path]: {
                    route: routes,
                    callback: callback
                }
            }
        }
    }

    /**
     * 跳转到某个路由
     * 其实只是简单的改变 hash
     * 触发 hashonchange 函数
     *
     * @param path
     * @param data
     */
    static push(path, data) {
        window.location.hash = this.combineHash(path, data)
    }

    /**
     * 获取路径
     * 比如 #detail => /detail
     * @returns {string|string}
     */
    static getPath() {
        let url = new Url(window.location.href)
        return url.hash.replace("#", "").split("?")[0] || "/"
    }

    /**
     * 将 queryString 转化成 参数对象
     * 比如 ?articleId=1 => {articleId: 1}
     * @returns {*}
     */
    static queryStringToParam() {
        let url = new Url(window.location.href)
        let hashAndParam = url.hash.replace("#", "")
        let arr = hashAndParam.split("?")
        if (arr.length === 1) return {}
        return arr[1].split("&").map(p => {
            return p.split("=").reduce((a, b) => ({[a]: b}))
        })[0]
    }

    /**
     * 将参数变成 queryString
     * 比如 {articleId:1} => ?articleId=1
     * @param params
     * @returns {string}
     */
    static paramToQueryString(params = {}) {
        let result = ""
        Object.keys(params).length && Object.keys(params).forEach(key => {
            if (result.length !== 0) {
                result += "&"
            }
            result += key + "=" + params[key]
        })
        return result
    }

    /**
     * 组合地址和数据
     * 比如 detail,{articleId:1} => detail?articleId=1
     * @param path
     * @param data
     * @returns {*}
     */
    static combineHash(path, data = {}) {
        if (!Object.keys(data).length) return path.replace("/", "")
        return (path + "?" + this.paramToQueryString(data)).replace("/", "")
    }
}

export default Router

说明:这里修改了push方法,原本callback在这里调用的,但是现在换成在init调用。在init中监听了hashchange事件,这样就可以在hash变化的时候,需要路由配置并调用callback。而在监听变化之前,我们先调用了一次,是因为如果我们第一次进入就有hash,那么就不会触发hanshchange,所以我们需要手动调用一遍,为了初始化第一次访问的页面,这样我们就可以通过不同的地址访问不同的页面了,而整个站点只初始化了一次(在不使用按需加载的情况下),体验非常好,还要另外一种实行这里先不讲,日后有空独立出来讲关于路由的东西。

0x005 将自己实现的路由和React集成

重构ArticlePage

class ArticlePage extends React.Component {

    render() {
        return 

文章列表


{ ArticleService.getAll().map((article, index) => { return
this.handleClick(article)}>
{article.title}

{article.summary}


}) }
} handleClick(article) { Router.push("/detail", {articleId: article.id}) } }

重构DetailPage

class DetailPage extends React.Component {
    render() {
        const {title, summary, detail} = ArticleService.getById(this.props.data.articleId)
        return 

{title}

{summary}


{detail}

} handleClick() { Router.push("/index") } }

重构路由配置和渲染

const routes = [
    {
        path: "/index",
        name: "主页",
        component: ArticlePage
    },
    {
        path: "/detail",
        name: "详情页",
        component: DetailPage
    }
];


Router.register(routes, (data, route) => {
    let Component = route.component
    ReactDom.render(
        ,
        document.getElementById("app")
    )
})
0x006 为React定制Router组件
在上面每调用一次Router.push,就会执行一次ReactDom.render,并不符合React的思想,所以,需要为React定义一些组件

RouteApp组件

class RouterApp extends React.Component {
    componentDidMount(){
        Router.init()
    }
    render() {
        return {...this.props.children}
    }

}

Route组件

class Route extends React.Component {
    constructor(props) {
        super()
        this.state={
            path:props.path,
            match:"",
            data:{}
        }
    }

    componentDidMount() {
        Router.register({
            path: this.props.path
        }, (data, route) => {
            this.setState({
                match:route.path,
                data:data
            })
        })

    }

    render() {
        let Component = this.props.component
        if (this.state.path===this.state.match){
            return 
        }
        return null
    }
}

使用

class App extends React.Component {
    render() {
        return (
) } } ReactDom.render( , document.getElementById("app") )

说明

RouterApp组件中调用了Route.init来初始化调用,然后在每个Route中注册路由,每次路由变化的时候都会导致Route组件更新,从而使组件切换。

0x007 总结

路由本身是不带有任何特殊的属性的,在与框架集成的时候,应该考虑框架的特点,比如react的时候,我们可以使用reactreact-route直接结合,也可以通过使用react-route-dom来结合。

0x008 资源

源码

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98045.html

相关文章

  • 前端插拔式 SPA 应用架构实现方案

    摘要:插拔式应用架构方案和传统前端架构相比有以下几个优势业务模块分布式开发,代码仓库更易管理。 showImg(https://segmentfault.com/img/remote/1460000016053325?w=2250&h=1500); 背景 随着互联网云的兴起,一种将多个不同的服务集中在一个大平台上统一对外开放的概念逐渐为人熟知,越来越多与云相关或不相关的中后台管理系统或企业级...

    Cciradih 评论0 收藏0
  • Router入门0x201: 从 URL 到 SPA

    摘要:的全称是统一资源定位符英文,可以这么说,是一种标准,而网址则是符合标准的一种实现而已。渲染器,将组件渲染到页面上。 0x000 概述 从这一章开始就进入路由章节了,并不直接从如何使用react-route来讲,而是从路由的概念和实现来讲,达到知道路由的本质,而不是只知道如何使用react-route库的目的,毕竟react-route只是一个库,是路由的一个实现而已,而不是路由本身。 ...

    honmaple 评论0 收藏0
  • 使用 Vue.js Flask 来构建一个单页的App

    摘要:我们将创建一个简单的,它将从到返回一个随机数。我们来改变组件显示随机数在这个阶段,我们只是模仿客户端的随机数生成过程。 在这个教程中,我们将讲解如何将vue.js单页应用与Flask后端进行连接。 一般来说,如果你只是想通过Flask模板使用vue.js库也是没有问题的。但是,实际上是一个很明显的问题那就是,Jinja(模板引擎)也和Vue.js一样采用双大括号用于渲染,但只是一个还算...

    LiuZh 评论0 收藏0
  • Vue.js 服务端渲染业务入门实践

    摘要:说起,其实早在出现之前,网页就是在服务端渲染的。没有涉及流式渲染组件缓存对的服务端渲染有更深一步的认识,实际在生产环境中的应用可能还需要考虑很多因素。选择的服务端渲染方案,是情理之中的选择,不是对新技术的盲目追捧,而是一切为了需要。 作者:威威(沪江前端开发工程师)本文原创,转载请注明作者及出处。 背景 最近, 产品同学一如往常笑嘻嘻的递来需求文档, 纵使内心万般拒绝, 身体倒是很诚实...

    miya 评论0 收藏0
  • 从头开始学习vue-router

    摘要:路由模块的本质就是建立起和页面之间的映射关系。这时候我们可以直接利用传值了使用来匹配路由,然后通过来传递参数跳转对应路由配置于是我们可以获取参数六配置子路由二级路由实际生活中的应用界面,通常由多层嵌套的组件组合而成。 一、前言 要学习vue-router就要先知道这里的路由是什么?为什么我们不能像原来一样直接用标签编写链接哪?vue-router如何使用?常见路由操作有哪些?等等这些问...

    tommego 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<