前端路由是什么?如果你之前从事的是后端的工作,或者虽然有接触前端,但是并没有使用到单页面应用的话,这个概念对你来说还是会很陌生的。那么,为什么会在单页面应用中存在这么一个概念,以及,前端路由与我们后端的路由有什么异同呢。本章,我们就来简单介绍下前端路由的概念,以及如何在 Vue 中使用 Vue Router 来实现我们的前端路由。 二、干货合集# 在传统的多页面应用中,网站的每一个 URL 地址都是对应于服务器磁盘上的一个实际物理文件。例如,当我们访问 https://www.yousite.com/index.html 这个网址的时候,服务器会自动把我们的请求对应到当前站点路径下面的 index.html 文件,然后再给予响应,将这个文件返回给浏览器。当我们跳转到别的页面上时,毫无疑问则会再重复一遍上面的过程。 但是在单页面应用中,整个项目中只会存在一个 html 文件,当用户切换页面时,只是通过对这个唯一的 html 文件进行动态重写,从而达到响应用户的请求。也就是说,从切换页面这个角度上说,应用只是在第一次打开时请求了服务器(非服务端渲染的单页应用)。 因为访问的页面是并不真实存在的,所以如何正确的在一个 html 文件中展现出用户想要访问的信息就成为单页面应用需要考虑的问题,而对于这一路由问题的解决方案,为了与我们后端传统意义上的路由进行区别,就将此称为前端路由。 1、前端路由的实现方式# 目前的前端路由的实现方式主要是通过 hash 路由匹配或者是采用 html5 中的 history api 这两种,也就是说,不管是 hash 路由还是使用 history 路由模式,其实都是基于浏览器自身的特性。 hash 路由:hash 这个概念,可能听起来有些陌生,不过,其实我们在之前的前端开发中,其实是有所接触的。例如,在某些情况下,我们需要定位页面上的某些位置,就像下面的例子中展现的那样,我想要通过点击不同的按钮就跳转到指定的位置,这里我们使用的锚点定位其实就是 hash。 复制代码 <div id="content"> <div class="btn-container"> <a class="btn" href="#image1">图片1</a> <a class="btn" href="#image2">图片2</a> </div> <img src="./xxx/xxx.jpg" id="image1"> <img src="./xxx/xxx.jpg" id="image2"> </div> 复制代码 hash 路由的本质是浏览器 location 对象中的 hash 属性,它会记录链接地址中 '#' 后面的内容(e.g.:#part1)。因此,我们可以通过监听 window.onhashchange 事件获取到跳转前后访问的地址,从而实现地址切换的目的。 history 路由:在之前的 html 版本中,我们可以通过 history.back(), history.forward()和 history.go() 方法来完成在用户历史记录中向后和向前的跳转。而 history 路由则是使用了 html5 中新增的 pushState 事件和 replaceState() 事件。 通过这两个新增的 API,就可以实现无刷新的更改地址栏链接,配合 AJAX 就可以做到整个页面的无刷新跳转,具体实现的原理大家可以看看这篇文章 =》https://www.renfei.org/blog/html5-introduction-3-history-api.html 在 Vue 中,Vue Router 是官方提供的路由管理器。它和 Vue.js 的核心深度集成,因此,不管是采用 hash 的方式还是使用 history api 实现我们的前端路由都有很好的支持,所以这里我们采用 Vue Router 这一组件来实现我们的前端路由。 2、路由实现# 首先我们需要将 Vue Router 添加引用到我们的项目中,这里我还是采用直接引用 js 文件的方式为我们的示例代码添加前端路由支持。 在 Vue 中使用 Vue Router 构建单页面应用,我们只需要将组件 (components) 映射到定义的路由 (routes) 规则中,然后告诉 Vue Router 在哪里渲染它们,并将这个路由配置挂载到 Vue 实例节点上即可。 在 Vue Router 中,我们使用 router-link 标签来渲染链接,当然,默认生成的是 a 标签,如果你想要将路由信息生成别的 html 标签,则可以使用 tag 属性指明需要生成的标签类型。 复制代码 <!-- 默认渲染成 a 标签 --> <router-link to="/home">主页</router-link> <!-- 渲染成 button 标签 --> <router-link to="/home" tag="button">主页</router-link> 复制代码 可以看到,当我们指定 tag 属性为 button 后,页面渲染后的的标签就变成了 button 按钮。同样的,它还是会监听点击,触发导航。 同时,从上图可以看出,当前的链接地址为 #/home,也就是说,通过 router-link 生成的标签,当页面地址与对应的路由规则匹配成功后,将自动设置 class 属性值为 .router-link-active。当然,我们也可以通过指定 active-class 属性或者在构造 VueRouter 对象时使用 linkActiveClass 来自定义链接激活时使用的 CSS 类名。 复制代码 <!-- 使用属性来设定自定义激活类名 --> <router-link to="/home" active-class="aaaa">主页</router-link> <!-- 在构造对象时设定全局默认类名 --> const router = new VueRouter({ routes: [], linkActiveClass: 'aaaaa' }) 复制代码 当路由表构建完成后,对于指向路由表中的链接,需要在页面上找一个地方去显示已经渲染完成后的组件,这时,我们就需要使用 router-view 标签去告诉程序,我们需要将渲染后的组件显示在当前位置。 在下面的示例代码中,模拟了 Vue 中路由的使用,当访问 #/home 时会进行加载 home 组件,而当链接跳转到 #/account 时则会加载 account 组件。同时,我们可以发现,在 account 组件中又包含了两个子路由,通过点击 account 组件中的子路由地址,从而加载对应的 login 组件和 register 组件。 public class MyExpander : HeaderedContentControl { public MyExpander() { DefaultStyleKey = typeof(MyExpander); } public bool IsExpanded { get => (bool)GetValue(IsExpandedProperty); set => SetValue(IsExpandedProperty, value); } public static readonly DependencyProperty IsExpandedProperty www.chengmingyuLe.com= DependencyProperty.Register(nameof(IsExpanded), typeof(bool), typeof(MyExpander), new PropertyMetadata(default(bool), OnIsExpandedChanged)); private static void OnIsExpandedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { var oldValue = (bool)args.OldValue; var newValue = (bool)args.NewValue; if (oldValue == newValue) return; var target = obj as MyExpander; target?.OnIsExpandedChanged(oldValue, newValue); } protected virtual void OnIsExpandedChanged(bool oldValue, bool newValue) { if (newValue) OnExpanded(www.baichuangyule.cn); else OnCollapsed(); } protected virtual void OnCollapsed() { } protected virtual void OnExpanded() { } } Copy <Style TargetType="{x:Type local:MyExpander}"> <Setter Property="HorizontalContentAlignment" Value="Stretch" www.hongchenyul.cn/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:MyExpander}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness=www.shengrenyp.cn"{TemplateBinding BorderThickness}"> <StackPanel> <ToggleButton x:Name="ExpanderToggleButton" Content="{TemplateBinding Header}" IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" /> <ContentPresenter Grid.Row="1" x:Name="ContentPresenter" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Visibility=www.qilicgw.com"Collapsed" /> </StackPanel> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> MyExpander是一个HeaderedContentControl,它包含一个IsExpanded用于指示当前是展开还是折叠。ControlTemplate中包含ExpanderToggleButton及ContentPresenter两个元素。 3. 使用RelativeSource# 之前已经介绍过TemplateBinding,通常ControlTemplate中元素都通过TemplateBinding获取控件的属性值。但需要双向绑定的话,就是RelativeSource出场的时候了。 RelativeSource有几种模式,分别是: FindAncestor,引用数据绑定元素的父链中的上级。 这可用于绑定到特定类型的上级或其子类。 PreviousData,允许在当前显示的数据项列表中绑定上一个数据项(不是包含数据项的控件)。 Self,引用正在其上设置绑定的元素,并允许你将该元素的一个属性绑定到同一元素的其他属性上。 TemplatedParent,引用应用了模板的元素,其中此模板中存在数据绑定元素。。 ControlTemplate中主要使用RelativeSource Mode=TemplatedParent的Binding,它相当于TemplateBinding的双向绑定版本。,主要是为了可以和控件本身进行双向绑定。ExpanderToggleButton.IsChecked使用这种绑定与Expander的IsExpanded关联,当Expander.IsChecked为True时ExpanderToggleButton处于选中的状态。 Copy IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" 接下来分别用几种技术实现Expander.IsChecked为True时显示ContentPresenter。 4. 使用Trigger# Copy <ControlTemplate TargetType="{x:Type local:ExpanderUsingTrigger}"> <Border Background="{TemplateBinding Background}"> ...... </Border> <ControlTemplate.Triggers> <Trigger www.shengrenpt1.cn Property="IsExpanded" Value="True"> <Setter Property="Visibility" TargetName="ContentPresenter" Value="Visible" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> <style> .container { background-color: aquamarine; margin-top: 20px; width: 740px; height: 300px; } </style> <div id="app"> <!-- 通过 router-link 标签来生成导航链接 --> <router-link to="/home">主页</router-link> <router-link to="/account">账户</router-link> <div class="container"> <!-- 将选中的路由渲染到 router-view 下--> <router-view></router-view> </div> </div> <template id="tmpl"> <div> <h3> account page </h3> <!-- 生成嵌套子路由地址 --> <router-link to="/account/login">登录</router-link> <router-link to="/account/register">注册</router-link> <!-- 生成嵌套子路由渲染节点 --> <router-view></router-view> </div> </template> <script> // 1、定义路由跳转的组件模板 const home = { template: '<div> home page </div>' } const account = { template: '#tmpl' } const login = { template: '<div> login page</div>' } const register = { template: '<div> register page</div>' } // 2、定义路由信息 const routes = [{ path: '/', redirect: '/home' }, { path: '/home', component: home }, { path: '/account', component: account, children: [{ path: 'login', component: login }, { path: 'register', component: register } ] } ] const router = new VueRouter({ //mode: 'history', //使用 history 模式还是 hash 路由模式 routes }) // 3、挂载到当前 Vue 实例上 const vm = new Vue({ el: '#app', data: {}, methods: {}, router: router }); </script> 复制代码 在上面的代码中,也使用到了嵌套路由和路由的重定向。通过使用路由重定向,我们可以将用户访问网站的根目录 / 时进行重定向到 /home ,而嵌套路由则可以将 URL 中各段动态路径也按某种结构对应到实际嵌套的各层组件。 例如,这里的 login 组件和 register 组件,它们都是位于 account 组件中的,因此,在构建 url 时,我们应该将该地址位于 /account url 后面,从而更好地表达这种关系。所以这里,我们在 account 组件中又添加了一个 router-view 标签,用来渲染出嵌套的组件内容。同时,通过在定义 routes 时在参数中使用 children 属性,从而达到配置嵌套路由信息的目的。 PS:以 / 开头的嵌套路径会被当作根路径,而我们的 login 组件和 register 组件都是包含在 account 中,所以这里直接定义 path 参数即可。 三、总结# 这一章主要是介绍了如何使用 Vue Router 在 Vue 中构建我们的前端路由。在实际开发中,对于一个路径,可能会对应到多个组件,这时,如何将多个组件绑定到一个路径下,就是我们需要解决的问题。另外,在实际使用中我们会遇到路由传参,可能会因为 hash 路由不漂亮,从而准备采用 history 路由,对于这些需求的实现,将放在下一章中进行学习。 四、参考# 1、从头开始学习vue-router 2、单页面应用路由的两种实现方式 3、你需要知道的单页面路由实现原理 4、面试官: 你了解前端路由吗? 5、前端路由实现原理(history)