Vue-Router
1、使用vuecli创建项目时选择上Router会自动安装vue-router插件并且生成vue-router的基础代码结构
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'
// 1. 注册路由插件
// 参数如果是函数Vue.use直接调用这个函数来注册组件
// 参数如果是对象Vue.use会调用这个对象的install方法来注册插件
Vue.use(VueRouter)
// 路由规则
const routes = [
{
path: '/',
name: 'Index',
component: Index
},
{
path: '/blog',
name: 'Blog',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "blog" */ '../views/Blog.vue')
},
{
path: '/photo',
name: 'Photo',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "photo" */ '../views/Photo.vue')
}
]
// 2. 创建 router 对象
const router = new VueRouter({
routes
})
export default router // 导出路由对象
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router' // 导入上述创建好的router对象
Vue.config.productionTip = false
// 创建vue实例
new Vue({
// 3. 注册 router 对象
router, // 作用:会给vue实例注入两个属性$route路由规则和$router路由对象(可以调用push\back\go等方法)
render: h => h(App)
}).$mount('#app')
App.vue
<template>
<div id="app">
<div>
<img src="@/assets/logo.png" alt="">
</div>
<div id="nav">
<!-- 5. 创建链接 -->
<router-link to="/">Index</router-link> |
<router-link to="/blog">Blog</router-link> |
<router-link to="/photo">Photo</router-link>
</div>
<!-- 4. 创建路由组件的占位 -->
<!-- 当路径匹配到一个组件之后,会把这个组件加载进来,并且最终会替换掉router-view这个位置 -->
<router-view/>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
总结:
1、创建路由相关的组件
2、注册路由插件 Vue.use(VueRouter)
3、创建router对象
new VueRouter({
routes
})
并配置一些路由规则
const routes = [
{
path: ‘/’,
name: ‘Index’,
component: Index
}
]
4、注册 router 对象
new Vue({
router,
render: h => h(App)
}).$mount(’#app’)
5、设置占位
6、创建链接
2、动态路由传参
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'
Vue.use(VueRouter)
const routes = [
// 首页路径
{
path: '/',
name: 'Index',
component: Index // 直接加载进来
},
// 详情页
{
// 动态路由通过:id占位来匹配变化的位置
path: '/detail/:id',
name: 'Detail',
// 开启 props,会把 URL 中的参数传递给组件
// 在组件中通过 props 来接收 URL 参数
props: true,
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// 路由懒加载
component: () => import(/* webpackChunkName: "detail" */ '../views/Detail.vue')
}
]
const router = new VueRouter({
routes
})
export default router
// 组件中获取id
<template>
<div>
<!-- 方式1: 通过当前路由规则,获取数据 -->
<!-- 缺点:强依赖路由传参 -->
通过当前路由规则获取:{{ $route.params.id }}
<br>
<!-- 方式2:路由规则中开启 props 传参 -->
通过开启 props 获取:{{ id }}
</div>
</template>
<script>
export default {
name: 'Detail',
props: ['id'] // 接收到URL上传递的参数
}
</script>
<style>
</style>
3、嵌套路由
当多个组件都有相同的内容,可以将相同的内容提取到一个公共的组件中
// router-view占位其他组件自身的内容
<template>
<div>
<div>
<img width="25%" src="@/assets/logo.png">
</div>
<div>
<router-view></router-view>
</div>
<div>
Footer
</div>
</div>
</template>
<script>
export default {
name: 'layout'
}
</script>
<style scoped>
</style>
// route.js
// 嵌套路由
{
path: '/', // 该路径会与children中的路由进行合并
component: Layout,
children: [
{
name: 'index',
path: '',
component: Index
},
{
name: 'detail',
path: 'detail/:id',
props: true,
component: () => import('@/views/Detail.vue')
}
]
}
4、编程式导航
this.$router.push('/') // 参数是字符串
// this.$router.push({ name: 'Home' }) //参数是对象 name就是路由中配置的name
this.$router.replace('/login')
this.$router.push({ name: 'Detail', params: { id: 1 } })
this.$router.go(-2)
5、Hash和History模式区别
history模式需要服务端配合
原理的区别
a. Hash模式是基于锚点,以及onhashchange事件。通过锚点的值作为路由地址,当地址发生变化时触发onhashchange事件
b. History模式是基于HTML5中的HistoryAPI
history.pushState() IE10以后才支持 所以有兼容性问题
history.replaceState()
pushState和push方法的区别 pushState不回向服务器发送请求只会改变地址栏中的地址并且把这个地址记录到历史记录里面来
6、history模式
a. History需要服务器的支持
b. 单页应用中,服务端不存在 http://www.testurl.com/login 这样的地址会返回找不到该页面
c. 在服务端应该除了静态资源外都返回单页应用的 index.html
刷新页面浏览器会向服务器发送请求,请求服务器上的这个页面,但是如果服务器上不存在这个页面的话应该返回404,没有看到404这个效果是因为vuecli自带的这个服务器已经配置好了处理
7、history模式 Node.js 服务器配置
const path = require('path')
// 导入处理 history 模式的模块
const history = require('connect-history-api-fallback')
// 导入 express
const express = require('express')
const app = express()
// 注册处理 history 模式的中间件
// 开启对history模式支持后,刷新浏览器会向服务器请求,服务端会判断请求的页面服务器没有
// 它会把单页应用默认的首页index.html返回给浏览器
// 浏览器接收到这个页面后会再去判断路由地址进而加载响应的组件内容并渲染到浏览器上来
app.use(history()) // 如果没有会出现刷新浏览器后404
// 处理静态资源的中间件,网站根目录 ../web
app.use(express.static(path.join(__dirname, '../web')))
// 开启服务器,端口是 3000
app.listen(3000, () => {
console.log('服务器开启,端口:3000')
})
8、history模式 nginx 服务器配置
ngix服务器配置:
a. 官网下载nginx的压缩包 http://nginx.org/en/download.html
b. 把压缩包解压到c盘根目录。c:\nginx-1.18.0 文件夹
c. 打开命令行,切换到目录 c:\nginx-1.18.0
启动nginx
start nginx 会在后台启动不会阻塞命令行的运行
重启 如修改了nginx的配置文件
nginx -s reload
停止
nginx -s stop
html文件夹作用是 用来存储我们的网站 是网站的根目录 nginx默认放了一个index.html
conf文件夹用来存放一下配置文件
切到nginx安装目录c:\nginx-1.18.0目录下,启动nginx 测试是否安装成功
start ng 不需要输入直接tab键即可自动完成 然后回车执行
回车没有任何反应,如果80端口没有被占用的话,一般是启动成功的
如果80端口被占用了则无法启动也不会报错
// 命令行
PS C:\nginx-1.18.0> start .\nginx.exe
PS C:\nginx-1.18.0>
浏览器直接输入localhost不用输入端口号,因为nginx默认使用的是80端口
然后将打包后的前端项目拷贝到上述的html文件夹 然后刷新浏览器即成功了
刷新浏览器后看到了404 需要修改配置文件去处理vue-router的history模式
进入上述conf文件夹进入nginx.conf文件
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80; // 端口如果被占用可以修改此端口
server_name localhost; // 绑定的域名默认是是localhost,因为没有部署到线上的服务器上来
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
# 指定我们当前网站所在的根目录html
root html;
# 默认的首页,当访问localhost按回车时,去网站的根目录里来找index.html或者index.htm
index index.html index.htm;
# 配置vue-router的history模式
# try_files 试着去访问下这个文件(当前浏览器请求的路径所对应的文件)
# a. $uri就是当前请求的路径 它会去找这个路径所对应的文件,如果找到就把这个文件直接返回,如果没有找到就接着往后找
# b. 即把$uri当成文件夹 $uri/ 找uri下的默认首页index.html或者index.htm 如果找到就把这个文件直接返回给浏览器,如果没有找到就接着往后找
# c. 因为当前部署的是单页应用程序如果请求的路径 $uri $uri/ 不存在,需要返回单页应用的首页也就根文件夹下的index.html
try_files $uri $uri/ /index.html;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}
重启nginx nginx -s reload
PS C:\nginx-1.18.0> .\nginx.exe -s reload
打开浏览器即可
刷新浏览器会向服务器请求这个地址,服务端接收到请求后回去找这个路径在服务器上对应的文件,
服务上没有这个文件,在配置文件上加上try_files后如果找不到这个路径对应的文件的话,会默认返回网站根目录中的index.html给浏览器
浏览器接收到这个页面后会再去判断路由地址进而加载响应的组件内容并渲染到浏览器上来
9、VueRouter实现原理
模拟history模式vuerouter
Vue前置知识
插件、混入、Vue.observable()、插槽、render函数、运行时和完整版的Vue
Hash模式
URL中#后面的内容作为路径地址
如果只改变#后面的内容,浏览器不会向服务器请求这个地址,但是会把这个地址记录到浏览器的访问历史中
监听hashchange事件 记录当前路由地址
根据当前路由地址找到对应组件重新渲染
History模式 就是一个普通的url
通过history.pushState()方法改变地址栏
history.pushState()仅仅是改变地址栏,并把当前地址记录到浏览器的访问历史中,并不会真正的跳转到指定路径,也就是浏览器不会向服务器发送请求
监听popstate事件
可以监听到浏览器历史操作的变化 处理函数中可以记录改变后的地址 注意当 调用pushstate或者replacestate并不会触发该事件,当点击浏览器的前进和后退按钮或者调用history的back或forward时该事件才会被触发 最后当地址改变之后
根据当前路由地址找到对应组件重新渲染
10、VueRouter模拟实现-分析
a. 回顾核心代码
// router/index.js
// 注册插件
Vue.use(VueRouter)
// 创建路由对象
const router = new VueRouter({
routes:[
{name:'home',path:'/',component:homeComponent}
]
})
// main.js
// 创建vue实例
new Vue({
// 注册 router 对象
router,
render: h => h(App)
}).$mount('#app')
类的名字:VueRouter
类的属性:
options 记录构造函数中传入的对象
routeMap 是一个对象 用来记录路由地址与组件的对应关系,将来会把路由规则解析到routeMap里面来
data 是一个对象,里面有一个属性current(用来记录当前路由地址),响应式对象
类的方法
+是对外公开的方法
_是静态的方法
install 用来实现vue的插件机制
Constructor 构造函数用来初始化上述的属性和一个初始化的方法
init 用来调用下述三个方法,这里是把不同的代码分割到不同的方法中来实现
initEvent 用来注册popstate这个事件,用来监听浏览器历史的变化
createRouteMap() 用来初始化routeMap属性的,它把构造函数中传入的路由规则转换成键值对的形式存储到routeMap中来,routeMap 是一个对象 键:路由地址 值:对应的组件 route-vue的组件中会使用到routeMap
initComponents(Vue) 用来创建router-view和router-link这两个组件的
11、VueRouter模拟实现
当使用Vue.use注册插件的时候会调用install
let _Vue = null // 定义一个全局变量
// 导出VueRouter类
export default class VueRouter{
// Vue.use中调用install方法时会传递两个参数 vue的构造函数 可选的选项对象
static install (Vue){ // 静态函数或方法 其本身也是一个对象
// 判断当前插件是否已经被安装
// 把Vue的构造函数记录到全局变量中来
// 把创建vue实例时传入的router对象给它注入到所有vue实例上 this.$router就是在这个时候注入到vue实例上的。并且所有的组件也都是vue的实例
//1 判断当前插件是否被安装 自己增加的一个属性
if(VueRouter.install.installed){
return;
}
VueRouter.install.installed = true
//2 把Vue的构造函数记录在全局 为什么要记录,因为将来要在vue-router的实例方法中来使用vue的构造函数。比如在创建组件的时候需要调用Vue.component
_Vue = Vue
//3 把创建Vue的实例传入的router对象注入到Vue实例
// 此时this指向VueRouter类而不是Vue实例,所以这里不能直接这样写
// _Vue.prototype.$router = this.$options.router
_Vue.mixin({ // 在插件里面可以给所有的vue实例混入一个选项
beforeCreate(){ // 将来所有的组件也都会执行现在混入的beforeCreate钩子函数
// 此时this就是vue的实例 这边问题是后续会执行很多次
// 需要加个判断 因为只有vue $options这个选项中才有router这个属性,而组件的选项中是没有router这个属性的
if(this.$options.router){
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
}
}
})
}
// 构造函数接收一个参数 options选项对象 返回值是VueRouter对象
// 初始化上述的属性和一个初始化的方法
constructor(options){
// 记录构造函数中传入的这个选项options
this.options = options
// 是一个对象 将来会把路由规则解析到routeMap里面来 键:路由地址 值:路由组件
// 将来在router-vue组件里面会根据当前路由地址来routeMap里面找到对应的路由组件,把它渲染到浏览器中来
this.routeMap = {}
// data是一个响应式的对象,因为data中需要存储当前路由地址,里面有一个属性current(用来记录当前路由地址),路由变化时需要自动加载组件,所以data需要设置成响应式的对象
// observable 方法作用就是用来创建响应式对象,创建的响应式对象可以直接用在渲染函数或者计算属性里面
this.data = _Vue.observable({
current:"/" // 默认是/
})
this.init()
}
init(){
this.createRouteMap()
this.initComponent(_Vue)
this.initEvent()
}
// 把构造函数中传入的路由规则转换成键值对的形式存储到routeMap中来,
createRouteMap(){
//遍历所有的路由规则 吧路由规则解析成键值对的形式存储到routeMap中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
});
}
// 用来创建router-view和router-link这两个组件
initComponents(Vue){ // 参数是vue的构造函数,也可以不传,因为vue的构造函数在_Vue中记录着。传这个参数的目的是减少这个方法和外部的依赖
// router-link这个组件最终要渲染成超链接
// 回顾: <router-link to="/">Index</router-link> 这个组件使用中接收一个字符串类型参数to 也就是超链接的地址 ,router-link最终渲染成超链接的内容在router-link这个标签之间。所以将来需要把router-link这个标签之间的内容渲染到a标签里面来。
Vue.component("router-link",{
props:{ // 接收外部传入的参数
to:String
},
// router-link最终渲染的是一个a标签,所以temmplate里直接写a标签
// href 要绑定到to上面来
// 说明: Vue的构建版本
// 运行时版:不支持template模板,需要打包的时候提前编译(把template编译成render函数)。实际就是使用render函数帮我们创建虚拟DOM,然后把它渲染到视图。vue-cli创建的项目默认使用的是运行时版的vue
// 完整版:包含运行时和编译器,体积比运行时版本大10k左右,(编译器作用)程序运行的时候把模板转换成render函数
// template:'<a :href="to"><slot></slot></a>'
// 解决1:完整版
// vue.config.js
// module.exports = {
// runtimeCompiler: true // 会加载完整版本的vue
// }
// 解决2:运行时版 上述template需要注释掉了
// 疑问:写单文件组件时一直在使用template模板也没有使用render函数,问什么单文件组件可以正常工作呢?这是因为在打包的过程中把单文件的template编译成了render函数,这叫预编译
// 实现render函数
render(h){ // h函数的作用是创建虚拟DOM。h函数时vue传过来的
// h使用方式很多,此处只是其中一种
// 3个参数:创建这个元素对应的选择器,可以直接使用标签选择器;给这个标签设置一些属性;设置生成这个元素它的子元素,所以是数组的形式。
return h("a",{
// 标识DOM对象的属性
attrs:{
href:this.to //不需要拼#号因为实现的是history模式
},
// 给a标签对应的DOM对象注册事件
on:{
// 注册点击事件
click:this.clickhander // 是注册事件不是调用所以不加()
}
},[this.$slots.default]) // 我们当前a标签中它里面的内容是slot,所以需要获取slot的内容放到数组中来,当前slot没有起名字,是默认的插槽,获取默认插槽内容的写法是这样
},
methods:{
// 需要给这个超链接注册一个点击事件,取消着超链接后续事件的执行,也就是不让它去跳转(向服务器发送请求);还需要把地址栏上的内容改成这个超链接href中的值
clickhander(e){
// history.pushState会改变地址栏中的路径但是不会向服务器发送请求,还会把这次路径记录到历史中来。但是显示内容还需我们自己处理
history.pushState({},"",this.to) // 3个参数:data:将来触发popState的时候,传给popState事件的事件对象的一个参数,这里没有用到; title:网页的标题; url:当前超理解要跳转的地址
// 如何在router-link这个组件里找到路由对象:this是router-link这个组件,也是一个vue实例,我们之前在注册插件的时候,把$router这个属性挂载到了vue构造函数的原型属性上。所以所有的vue实例都可以访问到$router
this.$router.data.current=this.to // 记录当前路由地址,data是响应式数据,所以会自动更新视图
// 阻止默认行为
e.preventDefault()
}
}
// template:"<a :href='to'><slot></slot><>"
})
const self = this // vue-router的实例
// router-view这个组件相当于一个占位符,在router-view组件内部,我们要根据当前路由地址获取到对应的路由组件,并渲染到router-view的位置来
Vue.component("router-view",{
render(h){
// self.data.current 当前路由地址
const cm=self.routeMap[self.data.current]
return h(cm) // h函数还可以把一个组件转换成虚拟DOM
}
})
// 用来注册popstate这个事件,用来监听浏览器历史的变化。调用pushState、replaceState方式时是不会触发popState的
initEvent(){
// 可以解决点击浏览器前进后退组件不更新的问题
window.addEventListener("popstate",()=>{
this.data.current = window.location.pathname
})
}
}
}