MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

使用JavaScript实现前端路由

2023-07-194.2k 阅读

前端路由的基本概念

在深入探讨如何使用JavaScript实现前端路由之前,我们先来明确一下前端路由的基本概念。

传统的后端路由是指服务器根据不同的URL路径,返回不同的页面或数据。例如,当用户访问/about路径时,服务器会返回关于我们的页面;访问/products路径时,服务器返回产品列表页面。

而前端路由则是在单页面应用(SPA, Single - Page Application)中发挥作用。在SPA中,整个应用只有一个HTML页面,页面的切换和内容更新通过JavaScript动态操作DOM来实现,而不是像传统多页面应用那样每次都向服务器请求一个新的HTML页面。前端路由就是负责根据不同的URL路径,动态地加载和显示不同的页面内容,同时保证URL的变化与页面内容的变化同步。

举个例子,在一个博客应用中,当用户点击导航栏的“文章列表”链接时,URL变为/posts,同时页面展示文章列表;当用户点击某篇文章进入详情页时,URL变为/posts/123(假设123是文章的ID),页面展示具体文章内容。这里的/posts/posts/123等不同的URL路径,就对应着不同的页面状态,这就是前端路由的工作体现。

前端路由的实现原理

前端路由的实现主要基于两种技术:Hash(哈希)和History API。

Hash 模式

  1. 原理:Hash模式是利用URL中的哈希值(即#后面的部分)来实现路由。哈希值的变化不会导致浏览器向服务器发送请求,并且浏览器的前进后退操作会记录哈希值的变化。例如,URL http://example.com/#/home 中的#/home就是哈希值。当哈希值发生变化时,我们可以通过监听hashchange事件来捕获这种变化,从而实现页面内容的切换。
  2. 代码示例
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device-width, initial - scale = 1.0">
    <title>Hash - Based Routing</title>
</head>

<body>
    <ul>
        <li><a href="#/home">Home</a></li>
        <li><a href="#/about">About</a></li>
    </ul>
    <div id="content"></div>

    <script>
        window.addEventListener('hashchange', function () {
            const hash = location.hash.slice(1);
            const content = document.getElementById('content');
            if (hash === 'home') {
                content.innerHTML = '<h1>Home Page</h1><p>This is the home page content.</p>';
            } else if (hash === 'about') {
                content.innerHTML = '<h1>About Page</h1><p>This is the about page content.</p>';
            }
        });

        // 初始化页面
        if (!location.hash) {
            location.hash = 'home';
        }
    </script>
</body>

</html>

在上述代码中,我们通过addEventListener('hashchange', callback)监听哈希值的变化。当哈希值改变时,从location.hash中获取哈希值(去掉#),然后根据不同的哈希值,更新#content元素的innerHTML来显示不同的页面内容。同时,在页面加载时,如果没有哈希值,我们将其初始化为home

History API 模式

  1. 原理:History API是HTML5引入的新特性,它允许我们在不刷新页面的情况下修改浏览器的历史记录。通过history.pushState(state, title, url)方法可以将新的状态添加到浏览器历史记录中,其中state是一个包含与新历史记录相关数据的对象(可以为null),title目前大多数浏览器不支持,url是新的URL路径。当用户点击浏览器的前进或后退按钮时,会触发popstate事件,我们可以在这个事件的回调函数中根据新的URL路径更新页面内容。
  2. 代码示例
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device-width, initial - scale = 1.0">
    <title>History - API - Based Routing</title>
</head>

<body>
    <ul>
        <li><a href="#" data - url="/home">Home</a></li>
        <li><a href="#" data - url="/about">About</a></li>
    </ul>
    <div id="content"></div>

    <script>
        const content = document.getElementById('content');
        const links = document.querySelectorAll('a[data - url]');

        links.forEach(link => {
            link.addEventListener('click', function (e) {
                e.preventDefault();
                const url = this.dataset.url;
                history.pushState(null, '', url);
                updatePage(url);
            });
        });

        function updatePage(url) {
            if (url === '/home') {
                content.innerHTML = '<h1>Home Page</h1><p>This is the home page content.</p>';
            } else if (url === '/about') {
                content.innerHTML = '<h1>About Page</h1><p>This is the about page content.</p>';
            }
        }

        window.addEventListener('popstate', function () {
            const url = location.pathname;
            updatePage(url);
        });

        // 初始化页面
        if (location.pathname === '/') {
            history.pushState(null, '', '/home');
            updatePage('/home');
        } else {
            updatePage(location.pathname);
        }
    </script>
</body>

</html>

在这段代码中,我们为带有data - url属性的链接添加点击事件。点击链接时,通过history.pushState方法将新的URL路径添加到历史记录中,并调用updatePage函数更新页面内容。同时,通过监听popstate事件,当用户点击前进或后退按钮时,获取当前的location.pathname并调用updatePage函数来更新页面。在页面初始化时,根据当前的路径进行相应的处理。

使用JavaScript框架实现前端路由

虽然我们可以手动使用上述两种技术实现简单的前端路由,但在实际项目中,使用JavaScript框架提供的路由功能会更加便捷和高效。下面我们以Vue Router(Vue.js框架的路由插件)和React Router(React框架的路由库)为例,介绍如何在框架中实现前端路由。

Vue Router

  1. 安装和配置:首先,确保你已经安装了Vue.js。然后,可以通过npm安装Vue Router:
npm install vue - router

在Vue项目的入口文件(通常是main.js)中进行配置:

import Vue from 'vue';
import VueRouter from 'vue - router';
import Home from './components/Home.vue';
import About from './components/About.vue';

Vue.use(VueRouter);

const routes = [
    {
        path: '/home',
        name: 'Home',
        component: Home
    },
    {
        path: '/about',
        name: 'About',
        component: About
    }
];

const router = new VueRouter({
    mode: 'history',
    routes
});

new Vue({
    router,
    render: h => h(App)
}).$mount('#app');

在上述代码中,我们先导入Vue和Vue Router,并使用Vue.use(VueRouter)来安装Vue Router。然后定义了路由配置数组routes,每个路由对象包含path(路径)、name(路由名称)和component(对应的组件)。接着创建一个VueRouter实例,设置modehistory模式(也可以设置为hash模式),并将路由配置传入。最后,在Vue实例化时,将router传入,这样Vue Router就配置好了。 2. 页面导航和组件渲染:在Vue组件中,可以使用<router - link>组件来创建导航链接,使用<router - view>组件来渲染匹配的组件。例如,在App.vue中:

<template>
    <div id="app">
        <ul>
            <li><router - link to="/home">Home</router - link></li>
            <li><router - link to="/about">About</router - link></li>
        </ul>
        <router - view></router - view>
    </div>
</template>

<script>
export default {
    name: 'App'
};
</script>

<style>
#app {
    font - family: Avenir, Helvetica, Arial, sans - serif;
    -webkit - font - smoothing: antialiased;
    -moz - osx - font - smoothing: grayscale;
    text - align: center;
    color: #2c3e50;
    margin - top: 60px;
}
</style>

这里的<router - link to="/home">Home</router - link>会渲染成一个<a>标签,当点击时会切换到/home路径,并在<router - view>中渲染Home组件。同样,点击About链接会切换到/about路径并渲染About组件。

React Router

  1. 安装和配置:对于React项目,首先确保已安装React和React DOM。然后通过npm安装React Router:
npm install react - router - dom

在React项目的入口文件(通常是index.js)或路由配置文件中进行配置。以在index.js中配置为例:

import React from'react';
import ReactDOM from'react - dom';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';
import Home from './components/Home';
import About from './components/About';

ReactDOM.render(
    <Router>
        <Routes>
            <Route path="/home" element={<Home />}></Route>
            <Route path="/about" element={<About />}></Route>
        </Routes>
    </Router>,
    document.getElementById('root')
);

在上述代码中,我们从react - router - dom中导入BrowserRouter as Router(用于创建路由环境,BrowserRouter使用History API实现路由)、Routes(用于定义一组路由)和Route(用于定义单个路由)。然后在Routes中定义了两个路由,path为路径,element为匹配路径时要渲染的组件。 2. 页面导航和组件渲染:在React组件中,可以使用Link组件来创建导航链接,通过RoutesRoute的配置来渲染组件。例如,在App.js中:

import React from'react';
import { Link } from'react - router - dom';

function App() {
    return (
        <div>
            <ul>
                <li><Link to="/home">Home</Link></li>
                <li><Link to="/about">About</Link></li>
            </ul>
            {/* 路由匹配的组件会在这里渲染 */}
        </div>
    );
}

export default App;

这里的<Link to="/home">Home</Link>会渲染成一个可点击的链接,点击后会切换到/home路径,并根据路由配置渲染Home组件。同样,点击About链接会切换到/about路径并渲染About组件。

前端路由中的参数传递

在实际应用中,我们经常需要在不同页面之间传递参数。例如,在博客应用中,文章详情页需要根据文章ID获取文章内容。在前端路由中,有多种方式可以传递参数。

Hash 模式下的参数传递

在Hash模式下,可以通过在哈希值后面添加参数的方式传递。例如,http://example.com/#/article/123,这里的123就是文章ID。在代码中获取参数的方式如下:

window.addEventListener('hashchange', function () {
    const hash = location.hash.slice(1);
    const parts = hash.split('/');
    if (parts[0] === 'article' && parts.length === 2) {
        const articleId = parts[1];
        // 根据articleId获取文章内容并显示
    }
});

在上述代码中,我们通过split('/')方法将哈希值按/分割,然后根据分割后的数组获取文章ID。

History API 模式下的参数传递

  1. 路径参数:可以在路径中定义参数,例如/article/:id。在代码中获取参数的方式如下:
function updatePage(url) {
    const parts = url.split('/');
    if (parts[0] === '' && parts[1] === 'article' && parts.length === 3) {
        const articleId = parts[2];
        // 根据articleId获取文章内容并显示
    }
}

这里通过split('/')方法分割路径,然后获取参数。 2. 查询参数:也可以使用查询参数的方式传递,例如/article?id=123。获取查询参数的代码如下:

function getQueryParams() {
    const query = location.search.slice(1);
    const params = {};
    const parts = query.split('&');
    parts.forEach(part => {
        const keyValue = part.split('=');
        params[keyValue[0]] = keyValue[1];
    });
    return params;
}

function updatePage(url) {
    if (url === '/article') {
        const params = getQueryParams();
        const articleId = params.id;
        // 根据articleId获取文章内容并显示
    }
}

getQueryParams函数中,我们先获取location.search(即?后面的部分),然后按&分割成键值对,再将其解析为一个对象返回。在updatePage函数中,根据路径判断并获取查询参数中的文章ID。

Vue Router中的参数传递

  1. 路径参数:在路由配置中定义参数,例如:
const routes = [
    {
        path: '/article/:id',
        name: 'Article',
        component: Article
    }
];

在组件中获取参数:

<template>
    <div>
        <h1>Article Detail</h1>
        <p>Article ID: {{ $route.params.id }}</p>
    </div>
</template>

<script>
export default {
    name: 'Article'
};
</script>

这里通过$route.params.id获取路径参数id。 2. 查询参数:在导航时可以添加查询参数,例如:

<router - link :to="{ path: '/article', query: { id: 123 } }">Article</router - link>

在组件中获取查询参数:

<template>
    <div>
        <h1>Article Detail</h1>
        <p>Article ID: {{ $route.query.id }}</p>
    </div>
</template>

<script>
export default {
    name: 'Article'
};
</script>

通过$route.query.id获取查询参数id

React Router中的参数传递

  1. 路径参数:在路由配置中定义参数,例如:
<Route path="/article/:id" element={<Article />}></Route>

在组件中获取参数:

import { useParams } from'react - router - dom';

function Article() {
    const { id } = useParams();
    return (
        <div>
            <h1>Article Detail</h1>
            <p>Article ID: {id}</p>
        </div>
    );
}

export default Article;

这里通过useParams钩子函数获取路径参数id。 2. 查询参数:在导航时可以添加查询参数,例如:

<Link to={`/article?id=123`}>Article</Link>

在组件中获取查询参数:

import { useLocation } from'react - router - dom';

function Article() {
    const search = useLocation().search;
    const params = new URLSearchParams(search);
    const id = params.get('id');
    return (
        <div>
            <h1>Article Detail</h1>
            <p>Article ID: {id}</p>
        </div>
    );
}

export default Article;

这里通过useLocation钩子函数获取当前位置,然后使用URLSearchParams解析查询参数获取id

前端路由的嵌套路由

在一些复杂的应用中,我们不仅需要一级路由,还需要嵌套路由。例如,在一个电商应用中,产品详情页可能有多个子页面,如产品描述、评论、规格等。

Vue Router中的嵌套路由

  1. 路由配置:在路由配置中定义嵌套路由,例如:
const routes = [
    {
        path: '/product/:id',
        name: 'Product',
        component: Product,
        children: [
            {
                path: 'description',
                name: 'ProductDescription',
                component: ProductDescription
            },
            {
                path: 'comments',
                name: 'ProductComments',
                component: ProductComments
            }
        ]
    }
];

这里在Product路由下定义了两个子路由descriptioncomments。 2. 组件渲染:在Product.vue组件中,需要使用<router - view>来渲染子路由的组件:

<template>
    <div>
        <h1>Product Detail</h1>
        <ul>
            <li><router - link :to="`/product/${$route.params.id}/description`">Description</router - link></li>
            <li><router - link :to="`/product/${$route.params.id}/comments`">Comments</router - link></li>
        </ul>
        <router - view></router - view>
    </div>
</template>

<script>
export default {
    name: 'Product'
};
</script>

这里通过<router - link>创建子路由的导航链接,并在<router - view>中渲染子路由匹配的组件。

React Router中的嵌套路由

  1. 路由配置:在React Router中定义嵌套路由,例如:
<Route path="/product/:id" element={<Product />}>
    <Route path="description" element={<ProductDescription />}></Route>
    <Route path="comments" element={<ProductComments />}></Route>
</Route>

这里在/product/:id路由下定义了两个子路由descriptioncomments。 2. 组件渲染:在Product.js组件中,同样需要使用Outlet组件(React Router v6中的新特性)来渲染子路由的组件:

import { Link, Outlet } from'react - router - dom';

function Product() {
    const { id } = useParams();
    return (
        <div>
            <h1>Product Detail</h1>
            <ul>
                <li><Link to={`description`}>Description</Link></li>
                <li><Link to={`comments`}>Comments</Link></li>
            </ul>
            <Outlet />
        </div>
    );
}

export default Product;

这里通过Link创建子路由的导航链接,并通过Outlet渲染子路由匹配的组件。

前端路由的导航守卫

导航守卫是前端路由中非常重要的一部分,它可以在路由导航发生时进行一些验证和控制。例如,在用户访问需要登录的页面时,检查用户是否已经登录,如果未登录则重定向到登录页面。

Vue Router中的导航守卫

  1. 全局前置守卫:在router.js中定义全局前置守卫:
router.beforeEach((to, from, next) => {
    const isLoggedIn = localStorage.getItem('token');
    if (to.matched.some(record => record.meta.requiresAuth) &&!isLoggedIn) {
        next('/login');
    } else {
        next();
    }
});

在上述代码中,beforeEach是全局前置守卫,它会在每次路由导航之前被调用。to是即将要进入的目标路由对象,from是当前导航正要离开的路由对象,next是一个函数,调用next()表示放行,next('/login')表示重定向到/login路径。这里通过检查localStorage中是否有token来判断用户是否登录,如果目标路由的meta字段中有requiresAuth且用户未登录,则重定向到登录页面。 2. 路由独享守卫:在路由配置中定义路由独享守卫:

const routes = [
    {
        path: '/dashboard',
        name: 'Dashboard',
        component: Dashboard,
        beforeEnter: (to, from, next) => {
            const isLoggedIn = localStorage.getItem('token');
            if (isLoggedIn) {
                next();
            } else {
                next('/login');
            }
        },
        meta: {
            requiresAuth: true
        }
    }
];

这里的beforeEnter就是路由独享守卫,只对/dashboard路由生效。 3. 组件内守卫:在组件内部定义守卫:

<template>
    <div>
        <h1>Profile Page</h1>
    </div>
</template>

<script>
export default {
    beforeRouteEnter(to, from, next) {
        const isLoggedIn = localStorage.getItem('token');
        if (isLoggedIn) {
            next();
        } else {
            next('/login');
        }
    }
};
</script>

beforeRouteEnter是组件内守卫,在进入该组件对应的路由时会被调用。

React Router中的导航守卫

在React Router中,虽然没有像Vue Router那样直接的导航守卫概念,但可以通过自定义Hook和上下文(Context)来实现类似的功能。例如,创建一个useAuthGuard的Hook:

import { useLocation, useNavigate } from'react - router - dom';
import { useState, useEffect } from'react';

function useAuthGuard() {
    const navigate = useNavigate();
    const location = useLocation();
    const [isLoggedIn, setIsLoggedIn] = useState(false);

    useEffect(() => {
        const token = localStorage.getItem('token');
        setIsLoggedIn(Boolean(token));
    }, []);

    useEffect(() => {
        const requiresAuthRoutes = ['/dashboard', '/profile'];
        if (requiresAuthRoutes.includes(location.pathname) &&!isLoggedIn) {
            navigate('/login');
        }
    }, [isLoggedIn, location, navigate]);

    return isLoggedIn;
}

export default useAuthGuard;

在需要进行权限控制的组件中使用这个Hook:

import React from'react';
import useAuthGuard from './useAuthGuard';

function Dashboard() {
    const isLoggedIn = useAuthGuard();
    if (!isLoggedIn) {
        return null;
    }
    return (
        <div>
            <h1>Dashboard</h1>
        </div>
    );
}

export default Dashboard;

这里通过useEffect钩子函数在组件挂载时检查localStorage中的token判断用户是否登录,并在路由变化时检查当前路径是否需要登录权限,如果需要且用户未登录,则重定向到登录页面。

前端路由与SEO

前端路由对于单页面应用来说极大地提升了用户体验,但在搜索引擎优化(SEO)方面存在一些挑战。因为搜索引擎爬虫通常不会执行JavaScript代码,所以在传统的前端路由实现中,爬虫可能无法获取到完整的页面内容。

Hash 模式与SEO

Hash模式下,由于哈希值部分不会被发送到服务器,搜索引擎爬虫在抓取页面时,可能只能获取到哈希值之前的部分,导致无法正确解析页面内容。例如,http://example.com/#/article/123,爬虫可能只看到http://example.com/,这对于SEO非常不利。

History API 模式与SEO

History API模式下,虽然URL看起来更友好,但如果没有服务器端的配合,爬虫同样可能无法正确获取页面内容。因为爬虫发送的请求与普通浏览器请求不同,它不会执行JavaScript来动态渲染页面。

解决前端路由SEO问题的方法

  1. SSR(Server - Side Rendering):SSR是一种在服务器端渲染页面的技术。在SSR中,服务器接收到请求后,会根据路由和数据渲染出完整的HTML页面,然后发送给客户端。这样搜索引擎爬虫就可以获取到完整的页面内容。例如,在Vue.js中可以使用Nuxt.js框架来实现SSR,在React中可以使用Next.js框架来实现SSR。
  2. 预渲染(Pre - Rendering):预渲染是在构建时生成静态HTML页面。例如,在Vue项目中可以使用prerender - spa - plugin插件,在构建时为特定的路由生成静态HTML文件。这样在部署后,搜索引擎爬虫可以直接获取到静态的HTML内容,从而提高SEO效果。

前端路由的性能优化

前端路由在提升用户体验的同时,也需要关注性能问题。以下是一些前端路由性能优化的方法:

代码分割

在使用框架实现前端路由时,将不同路由对应的组件进行代码分割,只在需要的时候加载相应的代码。例如,在Webpack中可以使用动态导入(import())来实现代码分割。在Vue Router中:

const routes = [
    {
        path: '/home',
        name: 'Home',
        component: () => import('./components/Home.vue')
    },
    {
        path: '/about',
        name: 'About',
        component: () => import('./components/About.vue')
    }
];

在React Router中:

<Route path="/home" element={React.lazy(() => import('./components/Home'))}></Route>
<Route path="/about" element={React.lazy(() => import('./components/About'))}></Route>

这样在初始加载时,只加载必要的代码,提高页面加载速度。

懒加载图片和资源

对于路由切换后可能展示的图片和其他资源,采用懒加载的方式。在JavaScript中,可以使用IntersectionObserver API来实现图片的懒加载。例如:

<img data - src="image.jpg" alt="Lazy - Loaded Image" class="lazy - load">
const lazyImages = document.querySelectorAll('.lazy - load');
const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            observer.unobserve(img);
        }
    });
});

lazyImages.forEach(image => {
    observer.observe(image);
});

这样只有当图片进入视口时才会加载,减少不必要的资源请求。

避免过度渲染

在路由切换时,确保只更新需要更新的部分,避免整个页面的过度渲染。例如,在Vue组件中,可以使用v - ifv - else来控制组件的显示和隐藏,而不是频繁地创建和销毁组件。在React中,可以使用shouldComponentUpdate(类组件)或React.memo(函数组件)来控制组件的渲染。

总结

前端路由是单页面应用中不可或缺的一部分,通过Hash和History API等技术,我们可以实现基本的前端路由功能。而借助Vue Router、React Router等框架提供的路由功能,能更高效地构建复杂的路由系统。在实现前端路由的过程中,我们还需要关注参数传递、嵌套路由、导航守卫、SEO以及性能优化等方面的问题。只有综合考虑这些因素,才能构建出高性能、用户体验良好且搜索引擎友好的单页面应用。