初始化项目搭建整体结构和路由的使用以及编程式导航的bug解决
使用脚手架创建项目
创建项目执行命令:vue create gulishop-client
,项目的目录结构:
- node_modules:依赖包
- public静态资源,不会被webpack处理
- src项目源代码
- assets目录:静态资源
- components目录:公共组件和非路由组件
- App:根组件
- main.js:入口文件
- .gitignore:git的忽略文件
- babel.config.js:babel的配置文件
- package-lock.json:下载包的详细说明
- package.json:初始化npm包的配置文件
项目启动自动在浏览器中打开
在package.json配置文件中,在启动命令后面添加–open即可:"serve": "vue-cli-service serve --open",
禁用eslint语法检查
项目默认安装了eslint语法检查工具,默认检查严格级别很高,比如定义一个变量,没有使用,那么项目就会报错,所以在开发阶段将此禁用,在src目录下创建vue.config.js配置文件,该配置文件相当于webpack创建项目延伸出来的配置文件,并非webpack的配置文件,但是可以当作webpack的配置文件使用,配置项会插入到webpack的配置中
官网配置参考
module.exports = {
lintOnSave:false
}
注意:配置文件的修改都需要重启项目
@别名的配置
@代表src路径,但是在webpack中已经配置过了,可以直接使用,但是没有语法提示,所以需要配置,在src目录下创建jsconfig.json配置文件,配置如下内容:
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*":["src/*"]
}
},
// 表示在node_modeules目录和dist目录不能使用配置的@符号
"exclude": ["node_modules","dist"]
}
注意:如果出现json文件报错,先格式化文档,然后在vocode设置中搜索check js,将启用或禁用JavaScript文件的语义检查勾选
使用git管理项目
基本使用步骤:
- 创建本地库,先删除脚手架项目自带的.git文件,使用
git init
初始化本地仓库 - 创建远程仓库
- 让本地代码和远程仓库进行关联:
git remote add origin https://github.com/yuanlaiwoshicaiji/testguli.git
(关联代码在创建仓库之后就有了) - 本地代码改变推向远程库:
git add .
或者git add -A
提交到暂存区,然后提交的本地仓库git commit -m 'first commit'
,最后推送到远程仓库git push origin master
- 远程代码改变拉取到本地库:
git pull origin master
- 如果先有远程代码,然后才有本地代码,需要在本地执行clone操作(就不需要自己创建本地仓库了)
页面主体架构
功能页面是上中下结构,上下的结构是不变化的(非路由组件),切换页面的时候只有中间区域的内容在变化,页面却没有刷新,也就是说这是一个单页面应用(SPA),采用的ajax请求,中间变化的部分是路由组件的切换:
- Header和Footer是固定的所以是非路由组件
- Home,Search,Login,Register都是点击才会出现的,是路由组件
使用非路由组件
创建非路由组件Footer和Header,并且在App根组件中使用
- 创建组件
- 注册组件:使用配置项
components:{Header,Footer}
注册组件,注意:在注册之前别忘记引入组件否则无法使用 - 使用组件:通过组件标签的方式,直接使用
<Header></Header>
路由组件和非路由组件的区别
使用的步骤相同:定义,注册,使用
- 定义的目录位置不同:非路由组件在components,目录下创建,路由组件在pages目录或者views目录下创建
- 注册的方式不同:非路由组件在要使用的组件当中注册,而路由组件是要在路由配置中注册的
- 使用的方式不同:非路由组件直接通过标签的方式去使用,路由组件使用声明式导航
<router-link>
和编程式导航this.$router.push
同时配合<router-view>
一起使用
注意:非路由组件和路由组件的声明周期不同:路由组件在切换的时候会销毁重建<keep-alive>
,而非路由组件不会
路由的具体使用
- 定义:直接在pages目录中进行定义
- 注册:
- 安装vue-router:
npm i vue-router@3
- 创建router目录中的index.js文件(路由器文件)
- 声明并使用插件
- 暴露路由器对象,并且配置路由(需要提前引入需要使用的路由组件)
- 入口文件中引入路由器对象,并且在配置项中进行路由器的配置,配置之后,在搜索组件都有
this.$route
和this.$router
属性
- 安装vue-router:
- 使用:
- 使用
<router-view>
标签指定路由组件显示的位置 - 使用
<router-link>
标签切换路由或者this.$router.push()
或者this.$router.replace()
来切换路由 - 也可以通过在浏览器改变地址,来测试路由的切换
- 需要在最后配置一个重定向路由,在没有指定任何路由的时候,显示首页
- 使用
路由器的配置文件:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import Home from '@/pages/Home'
import Search from '@/pages/Search'
import Login from '@/pages/Login'
import Register from '@/pages/Register'
export default new VueRouter({
routes:[
{
path:'/home',
component:Home
},
{
path:'/search',
component:Search
},
{
path:'/login',
component:Login
},
{
path:'/register',
component:Register
},
{
path:'/*',
redirect:'/home'
},
]
})
入口文件中引入暴露的路由器实例对象并且配置:
import Vue from 'vue'
import App from '@/App.vue'
// 引入路由器对象
import router from '@/router'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router:router, // key和value相同,可以省略,但是没有省略
}).$mount('#app')
指定路由组件显示的位置:
<template>
<div>
<Header></Header>
<!-- 指定路由组件的入口 -->
<router-view></router-view>
<Footer></Footer>
</div>
</template>
将静态页面转换为组件
拆分Header组件和Footer组件:
- 因为使用的是less,所以标签要指定语言
<style scoped lang="less">
- scoped表示,该样式文件的作用域被限制在当前文件内,只对当前文件内的元素生效(但是这种说法不准确)
- css样式使用的是less,无法解析,所以要安装less和less-loader,并且只在开发环境中使用
npm i less -D
npm i less-loader@6 -D
- 需要图片,在html和css样式中,使用的路径不一样,并且是相对路径,所以要在对应的组件目录内创建images目录来存放图片,没有图片项目也会启动失败
- 对于reset.css文件,对于所有的页面都应该生效,在public目录下创建css文件,在index.html中进行引入
<link rel="stylesheet" href="<%= BASE_URL %>css/reset.css">
实现路由组件的切换
- 将登陆和免费注册的超链接标签,替换为
<router-link>
标签,使用声明式导航,跳转到搜索页面 - 搜索按钮使用编程式导航,跳转到搜索页面
- 将logo的点击也跳转到home页面
// 1. 声明式导航跳转
<router-link to="/login">登陆</router-link>
<router-link to="/register" class="register">免费注册</router-link>
// =======================================================================
// 2. 编程式导航跳转
<button @click="toSearch" class="sui-btn btn-xlarge btn-danger" type="button">搜索</button>
// 对应的methods中的方法
toSearch(){
this.$router.push('/search')
}
路由组件切换Footer组件的显示和隐藏
在显示Login组件和Register组件的时候,Footer组件是不需要显示的,一共有两种方式实现:
- 通过路由的path进行判断
- 通过路由配置的
meta
元数据进行判断,该配置项可以配置我们需要的任何数据
通过路由的路径path进行判断:
<div>
<Header></Header>
<router-view></router-view>
<!-- v-show的值需要一个false,才会隐藏,只要在login页面或者register页面,则判断为false,有一个false,&&则总体为false -->
<!-- 如果使用||进行判断,假设当前为login页面则,false || true,最后为true,即使在login页面也会显示,所以要使用&& -->
<Footer v-show="$route.path!=='/login' && $route.path!=='/register'"></Footer>
</div>
通过路由配置的meta元数据进行判断:
- 通过vue开发者工具可以查看如果当前在Home组件中,则
$route.meta
为一个空对象 - 切换到Register后,
$route.meta
中有了我们配置的isHidden
属性 - 因为切换路由组件之后,原本的路由组件实例被销毁,可以通过
beforeDestroy()
声明周期函数验证,那么新的$route
对象也会替换旧的$route
对象
{
path:'/register',
component:Register,
meta:{isHidden:true} // 在需要隐藏的路由中配置meta配置,标记是否隐藏
},
// 判断逻辑
<!-- 如果需要隐藏则该值为true,true表示显示,所以取反即可 -->
<Footer v-show="!$route.meta.isHidden"></Footer>
路由传参相关
路由匹配的过程
- 点击按钮改变路径进行路由跳转
this.$router.push('/search/+this.keyword')
或者使用<router-link :to="">
- 当路径改变,这个路径就回去路由器对象(路由器配置文件)内部的路由数组中的路由对象进行匹配
- 传递的params参数属于路径的一部分,也就是必须匹配到占位符才算匹配成功,在开发者工具里查看,
$route.path
包含params参数,该参数,会被路径后面的:xxx占位符收集到,最终这个参数被放到当前这个路由对象的params属性中 - 传递的query参数不会再匹配的时候占位,但是query参数再匹配的时候也会被收到当前这个路由对象的query属性中
- 传递的params参数属于路径的一部分,也就是必须匹配到占位符才算匹配成功,在开发者工具里查看,
- 匹配成功之后显示对应的路由组件
- 显示路由组件的同时会把刚才匹配成功的路由对象放在这个组件的
$route
当中 - 可以理解为:在路由器文件中的每一个路由配置(路由对象)就对应了不同的
$route
对象(解析到$route之中)
- 显示路由组件的同时会把刚才匹配成功的路由对象放在这个组件的
跳转路由的两种方式
- 声明式:使用
<router-link>
标签 - 编程式:使用
this.$router.push()
方法,编程式导航跳转路由比声明式导航更加灵活,因为使用的是方法,可以编写自己的逻辑
路由传参的两种参数类型
- params参数:属于路径的一部分,匹配的时候路由的path当中要照顾到这个参数(使用占位符)
- query参数:不属于路径的一部分,匹配的时候,路由的path不需要照顾这个参数
无论是params参数还是query参数,最终匹配完成都会解析到当前这个路由对象当中的params和query属性当中,显示路由组件的时候,会把这个路由对象传递给组件当中的this.$route
,所以this.$route
就可以获取到之前传递的参数
路由路径携带参数的两种写法
- 字符串写法
- 对象写法
相关面试题
- 指定params参数时可以不可用path和params配置的组合(对象写法)
- 如果传递的参数只有query参数,没有params参数,那么可以不使用name属性(命令路由),直接使用path
- 一旦参数中有了params参数,就不能使用path,必须使用name
- 所以对写法传递参数,最好使用name
- 如何让params参数可以传递也也可不传递
- 假设我这里在浏览器路径中直接输入
http://localhost:8080/#/search
由于没有传递params参数,所以没有匹配的路径会直接跳转到Home组件 - 在路由配置的path的占位符后面指定?,就能顺利的在不指定params参数的情况下跳转到Search路由组件
- 假设我这里在浏览器路径中直接输入
- 传递的params参数会出现路径丢失问题,如何解决
- 假设我没有在输入框中输入内容,则在跳转到Search的时候,收集到的是空字符串,路径为
http://localhost:8080/#/?keywordUpper=
,发现路径部分的Search部分丢失了 - 首先必须得在params参数可传递可不传递的前提下,当传递的params参数是空字符串的时候,指定传递为undefeind
- 假设我没有在输入框中输入内容,则在跳转到Search的时候,收集到的是空字符串,路径为
- 路径传参在组件中是否可以使用
props
属性接收,可以,但是需要在路由对象中进行配置,props属性- 如果props的属性的值是为true,则在路由组件中能够通过props属性接收到所有的params属性
- 如果props的属性值是一个对象,则传递的是路由组件需要的额外的静态数据
- 如果props的属性值是一个函数,则这个回调函数能够接收到
$route
为参数,可以手动返回一个对象,同时返回params参数和query参数
测试代码部分
// 1. 路由对象的配置:
{
name:'search',
path:'/search/:keyword?',
component:Search,
// props:true, 返回布尔值为true,能够通过props传递params参数
// props:{username:'tomcat'} 返回自定义对象,想传递什么就传递什么
// 返回一个函数,箭头函数返回的是一个对象,所以用()包裹
props:(route)=>({keyword:route.params.keyword,keywordUpper:route.query.keywordUpper})
},
// =======================================================================
// 2. 声明式导航字符串和对象形式的参数传递:要使用data中内容,所以绑定v-bind,又因为字符串写法需要的是字符串,所以使用模板字符串,模板字符串中使用变量${}
<!-- 声明式路由导航,字符串形式传递参数 -->
<router-link :to="`/search/${keyword}?keywordUpper=${keyword.toUpperCase()}`">点击跳转到搜索页面</router-link>
<!-- 声明式路由导航,对象形式传递参数,因为传递了params参数,所以不能使用path属性,而要使用name,命令路由的方式 -->
<!-- 并且如果params参数为空字符串,使用undefined -->
<router-link :to="{name:'search',params:{keyword:keyword || undefined},query:{keywordUpper:keyword.toUpperCase()}}">点击跳转到搜索页面</router-link>
// =======================================================================
// 3. 编程式导航字符串和对象形式的参数传递
toSearch() {
// 字符串写法
// this.$router.push('/search/'+this.keyword + '?keywordUpper='+ this.keyword.toUpperCase())
// 对象写法,因为有传递params参数,所以不能使用path,必须使用命名路由
this.$router.push({
name: "search",
params: { keyword: this.keyword || undefined },
query: { keywordUpper: this.keyword.toUpperCase() },
});
},
// =======================================================================
// 4. 用于展示的测试部分:
<template>
<div>
params:{{$route.params.keyword}}<br/>
query:{{$route.query.keywordUpper}}<br/>
propsParams:{{keyword}}<br/>
propsQuery:{{keywordUpper}}<br/>
propsTomcat:{{username}}
</div>
</template>
编程式路由跳转的bug解决
编程式路由导航在跳转到当前路由组件(跳转的路由不变),而且参数也不变的情况下,多次执行会报错,比如我点击搜索,参数没有变化,点击两次控制台会输出:
Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current location: "/search?keywordUpper=".
因为vue-router3.1.0之后,引入了Promise语法,如果没有通过参数指定成功的回调或者失败的回调就会返回一个Promise实例对象(可以变量接收push
方法的返回值查看),且内部会判断如果要跳转的路径和参数都没有改变,会抛出一个失败的Promise实例,控制台就会报错
不完美的解决方式:
- 在
push()
方法或者replace()
方法的第二个参数和第三个参数指定回调函数(指定其中一个就可以) - 使用catch语法进行处理
// 1. 第一种处理方式的编程式路由导航回调函数
toSearch() {
this.$router.push({
name: "search",
params: { keyword: this.keyword || undefined },
query: { keywordUpper: this.keyword.toUpperCase() },
},()=>{},()=>{});
},
// 2. 第二种处理方式的编程式路由导航回调函数
toSearch() {
this.$router.push({name: "search",
params: { keyword: this.keyword || undefined },
query: { keywordUpper: this.keyword.toUpperCase() },
}).catch(()=>{});
},
完美的解决方式,通过重写VueRouter
原型对象上的方法来解决:
VueRouter
式路由器对象的构造函数,而push
和replace
等方法就在该构造函数的原型对象上- 通过
this.$router.push()
调用方法的时候,其实是通过VueRouter
的实例掉用其原型对象上的方法 - 也就是
this.$router
是VueRouter
的实例化对象
在路由器对象文件(router目录下的index.js文件)中暴露路由器对象之前,重写其原型对象上的方法:
// ... 引入VueRouter插件
const originPush = VueRouter.prototype.push
const originReplace = VueRouter.prototype.replace
// location就是调用push跳转路由的时候以对象形式传递的内容,这里的函数千万不能使用箭头函数
VueRouter.prototype.push = function(location,onResolved,onRejected){
// 没有传递回调
if(onResolved === undefined && onRejected === undefined){
// 手动传递回调处理
// return originPush.call(this,location,()=>{},()=>{})
// 使用catch处理,返回的内容还是原来的方法返回的内容
return originPush.call(this,location).catch(()=>{})
}else{
return originPush.call(this,location,onResolved,onRejected)
}
}
VueRouter.prototype.replace = function(location,onResolved,onRejected){
// 没有传递回调
if(onResolved === undefined && onRejected === undefined){
// 手动传递回调处理
// return originPush.call(this,location,()=>{},()=>{})
// 使用catch处理,返回的内容还是原来的方法返回的内容
return originReplace.call(this,location).catch(()=>{})
}else{
return originReplace.call(this,location,onResolved,onRejected)
}
}
export default new VueRouter({
// ...配置路由
})
关于这里this的理解:
- 当这样调用
this.$route.push()
方法的时候,调用的是我们自己定义在原型上的方法 - this就是调用该方法的VueRouter实例对象
- 但是在这个方法的内部,调用了我们开始存储好的originPush方法,该方法是定义在全局的,作为函数调用this是window
- 所以我们应该使用
call()
改变this,让this变成VueRouter的实例对象 - 在定义方法的时候不能使用箭头函数,因为箭头函数没有自己的this,this就不是VueRouter的实例对象,而是找到外层的window(这里应该是因为语法的问题,不允许this指向window,所以得到的undefiend)
let obj = {
// show: function () {
// console.log(this)
// }
show:()=>{
console.log(this)
}
}
// 虽然是obj调用show方法,但是show为箭头函数,所以没有this,this指向是window
obj.show()