一、SPA(掌握)
什么是SPA?
Single Page Application,中文:单页应用。
以下两个链接请用浏览器的手机模拟器打开:
单页应用有很多,比如:https://m.huxiu.com/
非单页应用也是以往的开发习惯,比如:https://36kr.com/
二、Vue-router路由(掌握)
1、什么是路由?
说起路由你想起了什么?
路由器,那路由器是用来做什么的,你有没有想过?
路由中最重要的概念就是路由表:路由表的本质就是一个映射表,决定了数据包的指向;
二、后端路由
早期的网站开发整个HTML页面是由服务器来渲染的。服务器将渲染好的对应的HTML页面返回给客户端进行展示;
但是一个网站包含很多页面,那服务器是怎么处理的呢?
URL会发送给到服务器,服务器会通过正则对该URL进行匹配,最后交给Controller进行处理
Controller进行处理,最终生成HTML或者数据,然后返回给前端。
这其实就是服务器的一个IO操作,这其实就是对后端对路由的解析
后端渲染的好处,相对于发送ajax请求拿数据,可以提高首屏渲染的性能,也有利于SEO的优化;
后端路由的缺点:
另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码
而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情
3、前端路由
前后端分离阶段:
后端只提供API来返回数据(json,xml),前端通过Ajax获取数据,并且可以通过JavaScript将数据渲染到页面中
这样做最大的优点就是前后端责任的清晰, 后端专注于数据上, 前端专注于交互和可视化上
并且当移动端(iOS/Android)出现后, 后端不需要进行任何处理, 依然使用之前的一套API即可
单页面应用阶段:
其实SPA最主要的特点就是在前后端分离的基础上加了一层前端路由
前端路由的核心是什么呢?
4、前端路由规则
1、URL的hash
URL的hash也就是锚点(#), 本质上是改变window.location的href属性
我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新
2、HTML5的history模式
history接口时HTML5新增的,它有5种模式改变URL而不刷新页面
history.pushState(data, title, url)
history.replaceState(data, title, url)
**history.go(-1) **返回上一页
history.back() 等价于 history.go(-1)
history.forward() 等价于 history.go(1)
5、Vue-router基本使用
目前前端流行的三大框架,都有自己的路由实现:
1、认识vue-router
vue-router是Vue的官方路由插件,它和Vue是深度集成的,适合用于构建单页面应用 https://router.vuejs.org/zh/ 。
vue-router是基于路由和组件的,路由用于设定访问路径, 将路径和组件映射起来;在vue-router的单页面应用中, 页面的路径的改变就是组件的切换.
2、安装router
一般项目中建议在cli创建项目时就直接选择需要路由,并搭配history模式。如果并未选择,那么安装教程请参照官网:
https://router.vuejs.org/zh/installation.html
3、路由使用
在src下的views中,创建 Home.vue
及 User.vue
,随意写入一些信息。找到 router/index.js
中:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
const User = resolve => { require.ensure(['@/views/User.vue'], () => { resolve(require('@/views/User.vue')) }) };
Vue.use(VueRouter) // 导入路由对象
// 定义路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/user',
name: 'User',
// component: () => import(/* webpackChunkName: "user" */ '../views/User.vue')
component: User
}
]
// 创建路由实例
const router = new VueRouter({
mode: 'history', // vue路由只有两种模式,一种是hash,一种是history,这里使用历史模式
base: process.env.BASE_URL,
route
})
export default router
可以看到,我们有两种引入组件的方式。第一种比较能理解,第二种我们称之为“路由懒加载”。而这个懒加载中,有个 webpackChunkName
,这东西我们称为魔法注释。
魔法注释的作用:
webpack在打包的时候,对异步引入的库代码(lodash)进行代码分割时,为分割后的代码块取得名字。
Vue中运用import的懒加载语句以及webpack的魔法注释,在项目进行webpack打包的时候,对不同模块进行代码分割,在首屏加载时,用到哪个模块再加载哪个模块,实现懒加载进行页面的优化。
当你 npm run build
之后,生成的js文件中,就能看到以魔法注释定义的js文件名。
4、懒加载
懒加载的方式
// 方式一: 结合Vue的异步组件和Webpack的代码分析
const User = resolve => { require.ensure(['@/views/User.vue'], () => { resolve(require('@/views/User.vue')) }) };
// 方式二: AMD写法
const User = resolve => require(['@/views/User.vue'], resolve);
// 方式三: 在ES6中, 我们可以有更加简单的写法来组织Vue异步组件和Webpack的代码分割.
const Home = () => import(/* webpackChunkName: "user" */ '../views/User.vue')
5、路由模式
vue中的路由默认时hash模式,使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。如果不想要很丑的 hash,我们可以用路由的history 模式,这种模式充分利用 history.pushState
API 来完成 URL 跳转而无须重新加载页面。
hash 虽然出现URL中,但不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面。
history模式提供了对历史记录进行修改的功能,只是当它们执行修改是,虽然改变了当前的URL,但你浏览器不会立即向后端发送请求。history模式,会出现404 的情况,需要后台配置。
404 错误:
1、hash模式下,仅hash符号之前的内容会被包含在请求中,如 http://www.xxx.com, 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误;
2、history模式下,前端的url必须和实际向后端发起请求的url 一致,如http://www.xxx.com/book/id 。如果后端缺少对/book/id 的路由处理,将返回404错误。
6、路由跳转方式
我们可以使用 router-link
标签来实现跳转,如:
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/user">User</router-link>
</div>
<router-view/>
然后通过 router-view
来显示页面。router-link
最终会被渲染位a标签。
7、编程式导航
我们还有别的方式:
this.$router.push('/user')
我们除了push,还有replace、go、forward、back这几个来触发不同情况的跳转。
8、路由命名
当我们给路由命名后:
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'User', // 对路由进行命名
component: () => import(/* webpackChunkName: "user" */ '../views/User.vue')
}
]
})
我们就可以在跳转时,借用name属性实现跳转:
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
// 以上等同于
router.push({ name: 'user', params: { userId: 123 }})
而在User组件中,可以通过 $route.params.userId
获取到参数:
<div>
用户页{{$route.params.userId}}
</div>
9、query传参
使用params传参,得到的结果与使用query传参得到的结果有以下区别:
this.$router.push({name: "User", params: {userId: 123}}) // http://localhost:8081/user/123
this.$router.push({name: "User", query: {userId: 123}}) // http://localhost:8081/?userId=123
* 重调强调:
编程式导航中,使用name进行路径跳转,携带参数可以通过params和query,其中query会将参数携带在导航路径上,而使用path进行路径跳转,无法携带params,只能携带query。
10、路由重定向
const routes = [
{
path: '/',
redirect: '/home' // 这就是路由的重定向,重新定义跳转路径
},
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/user',
component: () => import('@/views/User.vue')
},
{
path: '/detail',
component: () => import('@/views/Detail.vue')
},
{
path: '*', // 匹配所有剩余的路由,只要不是上面提及的页面,全部跳转到404页面
component: () => import('@/views/404.vue')
}
]
11、嵌套路由
const routes = [
{
path: '/',
// 这里可以直接使用重定向,指定到某个子路由
redirect: '/home',
component: () => import('@/views/Index.vue'),
// 使用children包裹其他组件,就可以将它们变成当前路由的子路由
children: [
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/user',
component: () => import('@/views/User.vue')
},
{
path: '/detail',
component: () => import('@/views/Detail.vue')
}
]
},
{
path: '*',
component: () => import('@/views/404.vue')
}
]
6、京东底部Tab栏【案例】
借助以上嵌套路由的模板,我们来写个京东底部Tab栏。新建 Index.vue
:
<template>
<div class="">
<router-view></router-view>
<ul>
<li :class="num==1 ? 'active' : ''"><router-link to="/home">主页</router-link></li>
<li :class="num==2 ? 'active' : ''"><router-link to="/detail">详情页</router-link></li>
<li :class="num==3 ? 'active' : ''"><router-link to="/user">用户页</router-link></li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
num: 1
};
},
created() {
// 页面刷新时,可以监测当前路由地址,从而切换tab栏当前项
this.showWitch();
},
updated() {
// 每次页面更新,就监测路由地址,从而切换tab栏当前项
this.showWitch();
},
methods: {
// 定义一个方法,专门用来判断当前路由地址
showWitch() {
switch (this.$route.path) {
case "/home":
this.num = 1;
break;
case "/detail":
this.num = 2;
break;
case "/user":
this.num = 3;
break;
default:
this.num = 1;
break;
}
}
}
};
</script>
<style lang = "less" scoped>
* {
margin: 0;
padding: 0;
border: 0;
list-style: none;
}
ul {
width: 100%;
display: flex;
justify-content: space-around;
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
left: 0;
}
li {
a {
color: #000;
font-size: 30px;
text-decoration: none;
}
&.active {
a {
color: red;
}
}
}
</style>
* 注意:
$route
和 $router
的区别:$router
指的是整个项目的路由配置,所以里面就包含了跳转路由的方法(push、replace、go、back、forward);而 $route
指的是当前活跃的路由对象,所以能够获取当前页面的path、query、params
* 传参补充:
一般进入商品的详情页,都会选择携带参数,而通过router-link标签,我们无法像编程式导航一样直接携带query或params,所以我们需要这么来:
// router文件中
{
path: '/detail/:productId?' // 这里加上?,是让这个参数变为可选项,不加问号的话,必须携带参数才能跳转到详情页
}
// router-link标签上:
<router-link to="/detail/22"></router-link>
// 详情页组件中:
{{$route.params.productId}}
实际上我们可以看得出来,在路由中携带参数的本质,还是通过params去传参。
7、导航守卫
我们来考虑一个需求:在一个京东应用中,未登陆,不能进入订单确认页面
解决的方法:
在跳转到订单确认页面前做判断,登陆成功则可以顺利跳转,否则就跳转到别的页面去,或者登陆页面;
上面的这种方式是可行的,但是页面多起来的时候就不能每个页面都写,代码冗余不好维护
那有没有更好的方式呢?
什么是导航守卫? vue-router提供的导航守卫主要用来监听路由的进入和离开的;vue-router提供了beforeEach和afterEach的钩子函数,它们会在路由即将改变前和改变后触发
* 前置导航守卫
src/router/index.js
导航钩子的三个参数解析:
router.beforeEach((to, from, next) => {
next()
})
比如,我们强制要求,只能从详情页进入用户页,其他页面若要进入用户页,就会强制跳进详情页。具体代码:
// 定义导航守卫
router.beforeEach((to, from, next) => {
if (to.path == '/user' && from.path != '/detail') {
alert('请先登录');
next('/detail')
return;
}
next(); // 没有next()方法,导航不会跳转
})
* 后置导航守卫
router.afterEach( route => {
console.log(route)
})
后置导航守卫 afterEach, 不需要主动调用next()函数
8、路由元信息
我们可以给任何一个路由添加 meta
元信息:
...
// 定义路由规则
const routes = [
{
path: '/',
redirect: '/home',
component: () => import('@/views/Index.vue'),
children: [
{
path: '/home',
component: () => import('@/views/Home.vue'),
meta: {
num: 0
}
},
{
path: '/user',
component: () => import('@/views/User.vue'),
meta: {
num: 0
}
},
{
path: '/detail',
component: () => import('@/views/Detail.vue'),
meta: {
num: 0
}
}
]
},
{
path: '*',
component: () => import('@/views/404.vue')
}
]
...
router.afterEach( route => {
console.log(route)
})
export default router
通过 $route.meta
可以获取到当前路由的元信息,如果想要获取当前组件纵向队列中所有路由的元信息(数组形式),那么可以:$route.matched
。
三、KeepAlive缓存页面(掌握)
这里,我们新创建两个页面, You.vue
和 Me.vue
,并且都实现累加功能。
1、修改App.vue
// 将原本的:
<router-view/>
// 修改为:
<keep-alive>
<router-view/>
</keep-alive>
这样,我们实现了整个项目中每个页面的缓存,显然我们不想这么做,我们只想针对某些页面,这时,我们修改一下:
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
注意:
<keep-alive>
是用在其一个直属的子组件被开关的情形。如果你在其中有 v-for
则不会工作。
那这里的 $route.meta.keepAlive
存在于哪里呢?
2、Router中的meta
这是写在路由文件中的字段,我们去 router/index.js
中,将我们要缓存的 Me.vue
页面加上meta:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
...,
{
path: '/me',
name: 'Me',
component: () => import('../views/Me.vue'),
meta: {
keepAlive: true
}
},
{
path: '/you',
name: 'You',
component: () => import('../views/You.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
四、Restful风格接口(了解)
Restful风格的API是一种软件架构风格,设计风格而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。
在Restful风格中,用户请求的url使用同一个url而用请求方式:get,post,delete,put...等方式对请求的处理方法进行区分,这样可以在前后台分离式的开发中使得前端开发人员不会对请求的资源地址产生混淆和大量的检查方法名的麻烦,形成一个统一的接口。
五、Axios与QS(掌握)
在Vue和React等大型项目中,我们常用的数据请求方式,就是Axios。Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
官网地址:http://www.axios-js.com/
1、安装
npm i axios qs -S
// OR
yarn add axios qs
2、使用
* axios统一使用格式
axios.get(url[, config])
axios.post(url[, data[, config]])
// 直白一点表示:
axios
.get(url, {
params: {}
})
.then(res=>{})
.catch(err=>{})
axios
.post('/user', {})
.then(res=>{})
.catch(err=>{})
* qs使用
qs能将json格式转为 key=value
的格式。
具体用法:
const qs = require('qs');
axios.post('/foo', qs.stringify({ 'bar': 123 }));
* qs.stringify与JSON.stringify的区别
qs.stringify()将对象 序列化成URL的形式,以&进行拼接。JSON.stringify()是正常类型的JSON
var a = {name:'Lucy',age:10};
qs.stringify(a)
// 'name=Lucy&age=10'
JSON.stringify(a)
// '{"name":"Lucy","age":10}'
* 实际操作:
1、开启后端服务
打开 backend
目录,执行 npm i
命令,安装依赖成功后再执行 node server.js
,到浏览器输入 http://localhost:8000
查看接口文档。
2、组件中发起请求:
发送get请求:
// get请求
axios.get("http://localhost:8000/getdata").then(res => {
console.log(res);
});
// 上面的请求也可以这样做【以后我们都选用这种模式,方便后期做配置】
let qsObj = qs.stringify({
num: 123
});
axios
.post("http://localhost:8000/postdata", {
data: qsObj
})
.then(res => {
console.log(res);
});
3、解决跨域
到此,发现跨域,解决方案是找到 vue.config.js
:
module.exports = {
devServer: {
proxy: "http://localhost:8000"
}
}
然后请求改写:
// get请求
axios.get("/getdata").then(res => {
console.log(res);
});
// 上面的请求也可以这样做【以后我们都选用这种模式,方便后期做配置】
let qsObj = qs.stringify({
num: 123
});
axios
.post("/postdata", {
data: qsObj
})
.then(res => {
console.log(res);
});
ok,到此为止,我们解决了跨域,并请求成功!
3、创建实例
axios.create([config])
一般我们会通过创建实例,来统一对所有的请求进行配置:
import axios from 'axios'
const instance = axios.create({
baseURL: 'https://some-domain.com/api/', // 这里baseURL不是驼峰式,URL必须大写
timeout: 1000,
headers: {
'X-Custom-Header': 'foobar',
common['Authorization']: AUTH_TOKEN,
post['Content-Type']: 'application/x-www-form-urlencoded'
}
});
4、拦截器
在请求或响应被 then
或 catch
处理前拦截它们。
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
5、API按需导出并在组件中使用
import request from './request'
export const GetDataAPI = () => request.get('/getdata')
export const PostDataAPI = (data) => request.post('/postdata')
使用:
import {GetDataAPI,PostDataAPI} from '@/request/api'
GetDataAPI().then(res => {
console.log(res);
});
let qsObj = qs.stringify({
num: 123
});
PostDataAPI({data: qsObj})
.then(res=>{
console.log(res)
})
6、Vue项目中axios的运用
这里无视请求类型,get、post、put等请求都是一样的配置。
* 安装axios与qs
* 创建文件
在src目录下,新建 request.js
和 api.js
, request.js
用来做请求拦截、响应拦截等等配置,而 api.js
一般用来将接口改写为api,方便调用。
request.js
文件:
import axios from 'axios'
const instance = axios.create({
baseURL: process.env.VUE_APP_API, // 基础url,如果是多环境配置这样写,也可以像下面一行的写死。
// baseURL: 'http://localhost:8000', // 这里假设后端接口地址是http://localhost:8000
timeout: 5000 // 请求超时时间
})
// request interceptor(请求拦截器)
instance.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['token'] = token // 让每个请求携带自定义 token 请根据实际情况自行修改
}
return config
}, err => {
return Promise.reject(error);
})
// response interceptor(响应拦截器)
instance.interceptors.response.use((result) => {
return result;
}, err => {
return Promise.reject('error');
})
export default {
instance
}
api.js
文件:
import request from './request.js'
// 假设获取首页数据(不传递参数写法)
const GetHomeData = () => request.get('/homedata');
// 假设获取首页数据(传递参数写法)
const GetHomeData = (params) => request.get('/homedata', {params});
// 假设调用登录接口(post请求一般都传递参数)
const GoLogin = (params) => request.post('/login', params);
组件中使用:
<script>
import { GetHomeData, GoLogin } from '@/request/api'
export default {
created(){
GetHomeData({
page: 1
}).then(res=>{
console.log(res);
})
},
methods: {
btnClick(){
GoLogin({
username: "Lucy"
}).then(res=>{
console.log(res);
})
}
}
}
</script>
* 跨域解决
请参考devServer解决方案。
七、图片资源导入方式(熟悉)
放置在src目录下的图片资源有三种导入方式:
可以看到,对于一张图片而言,我可以直接src引入相对路径,我还可以使用模块化的方式引入,无论是require,还是import都可以。
但这里记住,背景图不可以使用相对路径直接引入,只能通过模块化方式引入,常见手法:
<template>
<div class="about">
<h1>This is an about page</h1>
<div class="about1" :style="{backgroundImage: `url(${logoSrc})`}"></div>
</div>
</template>
<script>
export default {
data(){
return {
logoSrc: require('@/assets/logo.png') // 或import引入也可行
}
}
}
</script>
<style>
...
</style>
九、环境变量(掌握)
我们实际开发中,往往有多种环境,如:
development 模式用于 vue-cli-service serve
production 模式用于 vue-cli-service build 和 vue-cli-service test:e2e
test 模式用于 vue-cli-service test:unit
甚至还有其他的,这里我们只讨论 development和production。
此处有几个要注意的点:
每个模式都会将环境变量中 NODE_ENV 的值设置为模式的名称
可以通过为 .env 文件增加后缀来设置某个模式下特有的环境变量
为一个特定模式准备的环境文件 (例如 .env.production) 将会比一般的环境文件 (例如 .env) 拥有更高的优先级
.env # 在所有的环境中被载入
.env.local # 在所有的环境中被载入,但会被 git 忽略
.env.[mode] # 只在指定的模式中被载入,优先级高于.env和.env.local
.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略,优先级高于.env和.env.local
一般环境变量文件都创建在根目录下,我们创建以下两份:
.env.dev
文件:
NODE_ENV=development
VUE_APP_BASE_URL=/
.env.prod
文件:
NODE_ENV = production
VUE_APP_BASE_URL = 'http://codesohigh.com'
注意:
NODE_ENV - 是 “development”、“production” 、"test"或者自定义的值。具体的值取决于应用运行的模式
BASE_URL - 会和 vue.config.js 中的 publicPath 选项相符,即你的应用会部署到的基础路径
除了 NODE_ENV 和 BASE_URL,其他的环境变量必须以 VUE_APP_ 开头
项目中使用:process.env.环境变量名,eg:VUE_APP_BASE_URL
我们到任意一个组件中:
console.log(process.env.NODE_ENV); // development
console.log(process.env.VUE_APP_BASE_URL) // /
通过在 package.json
中的 scripts
下,使用mode,我们可以切换环境:
{
"scripts": {
"serve": "vue-cli-service serve --open --port 3000 --mode prod",
"build": "vue-cli-service build --modern"
},
}
再到任意一个组件中:
console.log(process.env.NODE_ENV); // production
console.log(process.env.VUE_APP_BASE_URL) // http://codesohigh.com
此时,这个获取到的 http://codesohigh.com
地址,可以在全局任意位置使用 process.env.VUE_APP_BASE_URL
来代替。假如这个地址是接口地址,那么就可以直接用来拼接接口的API,提升开发效率。
十、vue.config.js(核心)
vue.config.js
是一个可选的配置文件,如果项目的 (和 package.json
同级的) 根目录中存在这个文件,那么它会被 @vue/cli-service
自动加载。你也可以使用 package.json
中的 vue
字段,但是注意这种写法需要你严格遵照 JSON 的格式来写。
1、publicPath(重点)
默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上,例如 https://www.my-app.com/
。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.my-app.com/my-app/
,则设置 publicPath
为 /my-app/
。
这个值也可以被设置为空字符串 (''
) 或是相对路径 ('./'
),这样所有的资源都会被链接为相对路径,这样打出来的包可以被部署在任意路径。
module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? './' : '/'
}
相对 publicPath 的限制:
相对路径的 publicPath
有一些使用上的限制。在以下情况下,应当避免使用相对 publicPath
:
当使用基于 HTML5 history.pushState
的路由时;
2、项目中跨域解决方案
当后端接口出现跨域问题,我们前端Vue项目想要解决时,可以在 vue.config.js
中:
module.exports = {
devServer: {
proxy: {
'/api': {
target: '填写你请求的baseURL地址',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
然后将原本的拦截器改为:
const instance = axios.create({
// baseURL: "填写你请求的baseURL地址",
baseURL: "/api",
timeout: 5000
})
再重新运行项目即可。
3、解决vue项目打包后打开一片空白:
第一步:注释路由中的 mode:'history'
第二步:vue.config.js
中配置:publicPath: process.env.NODE_ENV === 'production' ? './' : '/'