前端路由原理

119 阅读4分钟

对于前端路由来说,路由的映射函数通常是进行一些DOM的显示和隐藏。当访问不同路径时,会显示不同的页面组件

前端路由的两种原生js实现方式

Hash模式

  • 基于location.hash实现
  • www.example.com#home 这个网站的 location.hash 为 '#home'
  • URL中hash值只是客户端的一种状态,当向服务器发请求时,hash部分不会被发送
  • hash值的改变会在浏览器的访问历史中增加一个记录,因此能通过浏览器的回退、前进按钮控制hash的切换
  • hashchange 事件 可以监听hash变化
  • 的 href 和 location.hash 可以触发hashchange

代码实现

<script type="module">
    export class BaseRouter {
        // 创建一个路由表
        constructor(list) {
            this.list = list;
        }
        // 页面渲染函数
        render(state) {  // state表示当前的路由状态(例如一个路径)
            let e = this.list.find(e => e.path === state);
            e = e ? e : this.list.find(e => e.path === '*');  // 默认路由项 *
            ELEMENT.innerText = e.component;
        }
    }
    
    export class HashRouter extends BaseRouter {
        constructor(list) {
            super(list);
            this.handler();  // 初始化路由
            // 监听 hashchange 事件
            window.addEventListener('hashchange', e => {
                this.handler();
            });
        }
        // hash改变时,重新渲染页面
        handler() {
            this.render(this.getState());  // 当前URL的hash变化时,调用this.handler()响应这个变化
        }
        // 获取hash值
        getState() {
            const hash = window.location.hash;
            return hash ? hash.slice(1) : '/';  // 如果存在hash,去掉第一个字符"#"返回,否则返回 '/'
        }
        // push新的页面
        push(path) {
            window.location.hash = path;  // 更改window.location.hash会导致URL的hash改变,但不会重新加载页面,这是SPA路由跳转的常用技术
        }
        // 获取默认页URL
        getURL(path) {
            const href = window.location.href;
            const i = href.indexOf('#');  // 找到URL中#字符的位置
            const base = i >= 0 ? href.slice(0, i) : href;  // #存在则提取从开始到#之前的所有部分作为baseURL,否则提取整个href(效果等价)
            return base + '#' + path;
        }
        // 替换页面
        replace(path) {
            window.location.replace(this.getURL(path));   // replace()方法会将当前URL替换为新的URL,会替换浏览器历史记录中的当前条目,而不是添加一个新条目
        }
        // 前进/后退浏览历史
        go(n) {
            window.history.go(n);
        }
    }
    
    const router = new HashRouter([
        { path: '/', component: 'Home' },
        { path: '/about', component: 'About' },
        { path: '/contact', component: 'Contact' },
        { path: '/help', component: 'Help' },
        { path: '*', component: '404 Not Found' }
    ]);
    // 将router对象暴露给window对象,使路由器可在全局作用域访问
    window.router = router;
</script>
<body>
    <div id="ELEMENT">Home</div>
    <button onclick="router.push('/about')">Go to About</button>
    <button onclick="router.push('/contact')">Go to Contact</button>
    <button onclick="router.replace('/help')">Replace with Help</button>
    <button onclick="router.go(-1)">Go Back</button>
    <button onclick="router.go(1)">Go Forward</button>
</body>

实现了一个简单的 Hash 模式的路由

History模式

  • html5提供了 History API,可以直接通过history.pushState()(新增历史记录) 和 history.replaceState()(替换当前的历史记录) 在不刷新页面的情况下改变浏览器的历史记录
  • History API路由系统实现的路由方法 比基于哈希的路由更现代和优雅,因为它允许更干净的 URLs(不包含哈希符号 #)
  • popstate 事件 监听url变化
  • 另外,history.pushState()和history.replaceState()不会触发popState事件,需要手动触发页面渲染

pushState() 和 replaceState()

pushState()和replaceState()的三个参数:

  1. state object(state) 状态对象
  • 类型:object
  • 用途:在popstate事件触发时从event.state中获得,可以用来存储滚动位置、页面标题或其他与历史条目相关的数据,为了性能考虑一般只存储轻量级数据
  1. title(title)
  • 类型:string
  • 用途:理论上为了给新历史条目设置标题,但目前大多数浏览器没有实现这个功能,所以一般置为null
  1. url(url)
  • 类型:string
  • 用途:设置新历史条目的 URL。这个 URL 应该与当前域名下的 URL 相同,或者是相对于当前 URL 的路径。它会改变浏览器地址栏中显示的 URL,但不会导致页面重新加载
  • 注意:新的 URL 不应跨域,且不能违反同源策略。此外,即使 URL 改变了,页面不会重新加载,因此你需要确保你的应用能够根据 URL 的变化来正确渲染内容
 // 添加一个新的历史记录
history.pushState({ page: 1 }, "", "page1.html");

// 替换当前的历史记录
history.replaceState({ page: 2 }, "", "page2.html");

代码实现

<script type="module">
    export class BaseRouter {
        constructor(list) {
            this.list = list;
        }
        render(state) {
            let e = this.list.find(e => e.path === state);
            e = e ? e : this.list.find(e => e.path === '*');
            ELEMENT.innerText = e.component;
        }
    }

    export class HistoryRouter extends BaseRouter {
        constructor(list) {
            super(list);
            this.handler();
            // 监听 popstate事件
            window.addEventListener('popstate', e => {
                this.handler();
            });
        }
        // 渲染事件
        handler() {
            this.render(this.getState());
        }
        getState() {
            const path = window.location.pathname;
            return path ? path : '/';
        }
        push(path) {
            history.pushState(null, null, path);  // 向历史记录添加一个新条目
            this.handler();  // 更新视图
        }
        replace(path) {
            history.replaceState(null, null, path);  // 替换当前的历史记录条目
            this.handler();
        }
        go(n) {
            window.history.go(n);
        }
    }

    const router = new HistoryRouter([
        { path: '/', component: 'Home' },
        { path: '/about', component: 'About' },
        { path: '/contact', component: 'Contact' },
        { path: '/help', component: 'Help' },
        { path: '*', component: '404 Not Found' }
    ]);

    window.router = router; 
</script>
<body>
    <div id="ELEMENT">Home</div>
    <button onclick="router.push('/about')">Go to About</button>
    <button onclick="router.push('/contact')">Go to Contact</button>
    <button onclick="router.replace('/help')">Replace with Help</button>
    <button onclick="router.go(-1)">Go Back</button>
    <button onclick="router.go(1)">Go Forward</button>
</body>

实现了一个简单的 History 模式的路由


我是栖夜,感谢阅读。