一、应用VueCli创立我的项目
vue create edu-boss-fed
image-20200926080833055
image-20200926082611688
cd edu-boss-fed
yarn serve
二、退出Git版本治理
创立一个GitHub空仓库,将本地我的项目同步到GitHub上
git initgit add .git commit -m"vue2+ts我的项目初始化"git remote add origin git@github.com:2604150210/edu-boss-fed.gitgit branch -M mastergit push -u origin master
三、初始目录构造阐明
main.ts 入口文件App.vue 我的项目根组件shims-tsx.d.ts 和 shims-vue.d.ts TypeScript配置文件view/Home.vue 首页组件view/About.vue about页面组件store/index.ts 容器模块router/index.ts 路由模块,配置了路由表components文件夹,放公共组件.browserslistrc 浏览器兼容配置.editorconfig 编辑器配置.eslintrc.js ESLint的配置.gitignore Git疏忽文件babel.config.js Babel配置文件package.json 依赖清单package-lock.json 第三方包具体版本号tsconfig.json TS相干配置文件
四、调整初始目录构造
- 删除默认示例文件
App.vue 文件删除款式内容,删除路由链接,然而不能删除vue-router组件
router/index.ts 清空路由表数组
删除views文件夹里的Home.vue和About.vue,删除components文件里的HelloWorld.vue,删除assets里的logo.png - 新增一些文件或目录
在src外面创立文件夹utils(放工具函数)、styles(放全局款式)、services(放申请接口)
五、TS相干配置 - TS 相干依赖
vue-class-component 提供应用 class 语法写Vue组件
vue-property-decorator 在 Class 语法根底之上提供了一些辅助装璜器
@typescript-eslint/eslint-plugin 应用 ESLint 校验TS代码
@typescript-eslint/parser 将 TS 转为 AST 供 ESLint 校验应用
@vue/cli-plugin-typescript 应用 TS + ts-loader-fork-ts-checker-webpack-plugin 进行更快的类型查看
@vue/eslint-config-typescript 兼容 ESLint 的 TS 校验规定
typescript TS 编译器,提供类型校验和转换 JavaScript 性能 - TS 相干配置文件
ts.config.js{ "compilerOptions": { "target": "esnext", "module": "esnext", "strict": true, "jsx": "preserve", "importHelpers": true, "moduleResolution": "node", "experimentalDecorators": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "sourceMap": true, "baseUrl": ".", "types": [ "webpack-env" ], "paths": { "@/*": [ "src/*" ] }, "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx" ], "exclude": [ "node_modules" ]}
- Shims-vue.d.ts
ts辨认不了.vue结尾的文件,所以申明它的类型为Vue构造函数,在这个文件中做了适配。
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
- shims-tsx.d.ts
补充了一些类型申明,否则在JSX中应用这些成员时,会找不到类型,在这个文件中做了适配。
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interfaceinterface Element extends VNode {}// tslint:disable no-empty-interfaceinterface ElementClass extends Vue {}interface IntrinsicElements { [elem: string]: any;}
}
}
- 在ts模块中文件扩大名为.ts
六、应用TS开发Vue我的项目 - 应用OptionsAPI定义Vue组件
原始写法不反对类型推断:
<script>
// 1. 编译器给的类型提醒
// 2. TypeScript 编译期间的类型验证
export default {
data () {
return { a: 1, b: 'jal', d: { a: 1, b: 2 }}
},
methods: {
test () { // this.a // this.b // this.a.tet()}
}
}
</script>
指定script标签的lang属性为ts,则控制台会有ts类型推断检测
image-20200926103056954
- 应用ClassAPI定义Vue组件
应用教程:https://class-component.vuejs...
<template>
<div id="app">
<h1>大前端学习</h1>
<p>{{a}}</p>
<button @click="test">test</button>
<router-view />
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
@Component
export default class App extends Vue {
a = 1
b = '2'
c= {
a: 1,b: '2'
}
test () {
console.log(this.a)
}
}
</script>
<style lang="scss" scoped>
</style>
- 对于装璜器语法
装璜器是ES草案中的一个新个性,不过这个草案最近有可能产生重大调整,所以不倡议在生产环境中应用
类装璜器:
function testable (target) {
target.isTestable = true
}
@testable
class MyTestableClass {
// ...
}
console.log(MyTestableClass.isTestable) // true
装璜器就是扩大类的属性。
- 应用VuePropertyDecorator创立Vue组件
装璜器语法不稳固 - 总结创立组件的形式
Options APIs
Class APIs
Class + decorator
集体倡议: No Class APIs, 只用 Options APIs.
Class语法仅仅是一种写法而已,最终还是要转换成一般的组件数据结构。
装璜器语法还没有正式定稿公布,倡议理解即可,正式公布当前再抉择应用也能够。
应用Options APIs 最好是应用export default Vue.extend({...}),而不是export default {...}
七、代码格局标准
- 规范是什么
Standard Style 宽松一点,适宜集体或小团队
Airbnb 更严格,适宜大型团队
google 更严格,适宜大型团队 - 如果束缚代码标准
只靠口头约定必定是不行的,所以要利用工具来强制执行
JSLint (不举荐,快被淘汰了)
JSHint (不举荐,快被淘汰了)
ESLint (支流)
.eslintrc.js
module.exports = {
root: true,
env: {
node: true
},
// 应用插件的编码校验规定
extends: [
'plugin:vue/essential','@vue/standard','@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
// 自定义编码校验规定
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off','no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}
- 自定义校验规定
ESLint官网:https://cn.eslint.org/
Error级别的正告会显示到页面上,Warning级别的正告会输入到控制台
如果想屏蔽掉这个规定,则去.eslintrc.js文件中配置,在rules数组里配置了'semi': 'off',而后重启服务,这个报错就没有了
image-20200926164849748
如果必须要有分号,则配置为semi: ['error', 'always']
而后删除node_modules/.cache文件,再重启我的项目,就能够看到成果了,报错短少分号
image-20200926170048330
Typescript 的 interface会要有分号宰割要求,为了与standard宰割保持一致,咱们冀望去掉这个规定
image-20200926172430581
从package.json里找到@vue/eslint-config-typescript依赖的地址:https://github.com/vuejs/esli...
而后在这里查到这个规定,在规定具体配置查看怎么配置,在.eslintrc.js里减少规定:
'@typescript-eslint/member-delimiter-style': ['error', {
"multiline": {
"delimiter": "none","requireLast": true
}
}]
而后重启我的项目,interface外面不写分号宰割也不报错了:
image-20200926173506529
如果写上分号反而会报错。
八、导入Element组件库
ElementUI官网文档:https://element.eleme.cn/
npm i element-ui -S
main.js中减少ElementUI的导入和应用:
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
而后在App.vue文件中就能够应用了
image-20200926175250764
九、款式解决
src/styles
|-- index.scss # 全局款式(在入口模块被加载失效)
|-- mixin.scss # 公共的 mixin 混入(能够把反复的款式封装为 mixin 混入到复用的中央)
|-- reset.scss # 重置根底款式
|-- variables.scss # 公共款式变量
elementUI的款式就不须要导入了,改为导入全局款式文件,main.js
import Vue from 'vue'
import ElementUI from 'element-ui'
// import 'element-ui/lib/theme-chalk/index.css'
import App from './App.vue'
import router from './router'
import store from './store'
// 加载全局款式
import './styles/index.scss'
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
十、共享全局款式变量
向预处理器 Loader 传递选项
有的时候你想要向 webpack 的预处理器 loader 传递选项。你能够应用 vue.config.js 中的 css.loaderOptions 选项。比方你能够这样向所有 Sass/Less 款式传入共享的全局变量:
// vue.config.js
// vue.config.js
module.exports = {
css: {
loaderOptions: { // 默认状况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时失效 // 因为 `scss` 语法在外部也是由 sass-loader 解决的 // 然而在配置 `prependData` 选项的时候 // `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号 // 在这种状况下,咱们能够应用 `scss` 选项,对 `scss` 语法进行独自配置 scss: { prependData: `@import "~@/styles/variables.scss";` // css 里要加~符号能力应用@目录 }}
}
}
而后就能够在任意组件中应用全局款式变量了,
App.vue
<template>
<div id="app">
<h1>大前端学习</h1>
<p class="text">Hello world</p>
<router-view />
</div>
</template>
<style lang="scss" scoped>
// @import "~@/styles/variables.scss";
.text {
color: $success-color;
}
</style>
十一、接口解决
- 配置接口文档
解决跨域问题:如果你的前端利用和后端 API 服务器没有运行在同一个主机上,你须要在开发环境下将 API 申请代理到 API 服务器。这个问题能够通过 vue.config.js 中的 devServer.proxy 选项来配置。
devServer: {
proxy: {
'/boss': { target: 'http://eduboss.lagou.com', changeOrigin: true // 把申请头中的 host 配置为 target},'/front': { target: 'http://edufront.lagou.com', changeOrigin: true // 把申请头中的 host 配置为 target}
}
}
重启我的项目,而后在浏览器上拜访http://localhost:8080/boss,就能够看到页面上呈现了Unauthorized,阐明申请曾经被正确代理到了http://eduboss.lagou.com/boss
- 封装申请模块
yarn add axios
tils/request.ts
import axios from 'axios'
const request = axios.create({
// 配置选项
// baseURL,
// timeout,
})
// 申请拦截器
// 响应拦截器
export default request
测试申请胜利:
image-20200927071409320
十二、布局
- 初始化路由页面组件
image-20200927074004137
路由表配置:router/index.ts
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
Vue.use(VueRouter)
const routes: Array<RouteConfig> = [
{
path: '/',name: 'home',component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
},
{
path: '/login',name: 'login',component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '/user',name: 'user',component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue')
},
{
path: '/role',name: 'role',component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue')
},
{
path: '/advert',name: 'advert',component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue')
},
{
path: '/advert-space',name: 'advert-space',component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
},
{
path: '/menu',name: 'menu',component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue')
},
{
path: '/resource',name: 'resource',component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue')
},
{
path: '/course',name: 'course',component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue')
},
{
path: '*',name: '404',component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
}
]
const router = new VueRouter({
routes
})
export default router
- 布局组件Layout
layout/index.vue
<template>
<el-container>
<el-aside width="200px">Aside</el-aside><el-container> <el-header>Header</el-header> <el-main>Main</el-main></el-container>
</el-container>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'LayoutIndex'
})
</script>
<style lang="scss" scoped>
.el-container {
min-height: 100vh;
min-width: 980px;
}
.el-aside {
background: #d3dce6
}
.el-header {
background: #b3c0d1
}
.el-main {
background: #e9eef3
}
</style>
- 侧边栏组件
layout/components/app-aside.vue
<template>
<div class="aside">
<el-menu
router default-active="4" @open="handleOpen" @close="handleClose" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b"> <el-submenu index="1"> <template slot="title"> <i class="el-icon-location"></i> <span>权限治理</span> </template> <el-menu-item index="/role"> <i class="el-icon-setting"></i> <span slot="title">角色治理</span> </el-menu-item> <el-menu-item index="/menu"> <i class="el-icon-setting"></i> <span slot="title">菜单治理</span> </el-menu-item> <el-menu-item index="/resource"> <i class="el-icon-setting"></i> <span slot="title">资源管理</span> </el-menu-item> </el-submenu> <el-menu-item index="/course"> <i class="el-icon-menu"></i> <span slot="title">课程管理</span> </el-menu-item> <el-menu-item index="/user"> <i class="el-icon-document"></i> <span slot="title">用户治理</span> </el-menu-item> <el-submenu index="4"> <template slot="title"> <i class="el-icon-location"></i> <span>广告治理</span> </template> <el-menu-item index="/advert"> <i class="el-icon-setting"></i> <span slot="title">广告列表</span> </el-menu-item> <el-menu-item index="/advert-space"> <i class="el-icon-setting"></i> <span slot="title">广告位列表</span> </el-menu-item> </el-submenu></el-menu>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'AppAside',
methods: {
handleOpen (key: string, keyPath: string): void { console.log(key, keyPath)},handleClose (key: string, keyPath: string): void { console.log(key, keyPath)}
}
})
</script>
<style lang="scss" scoped>
.aside {
.el-menu {
min-height: 100vh;
}
}
</style>
开启router属性就是以index作为导航链接。
而后在layout/index.vue里引入app-aside组件,而后将路由进口放到el-main外面
Layout/index.vue
<template>
<el-container>
<el-aside width="200px"> <app-aside/></el-aside><el-container> <el-header> 头部 </el-header> <el-main> <!-- 路由进口 --> <router-view /> </el-main></el-container>
</el-container>
</template>
<script lang="ts">
import Vue from 'vue'
import AppAside from './components/app-aside.vue'
export default Vue.extend({
name: 'LayoutIndex',
components: {
AppAside
}
})
</script>
<style lang="scss" scoped>
.el-container {
min-height: 100vh;
min-width: 980px;
}
.el-aside {
background: #d3dce6
}
.el-header {
background: #b3c0d1
}
.el-main {
background: #e9eef3
}
</style>
- 头部header
ayout/components/app-header.vue
<template>
<div class="header">
<el-breadcrumb separator-class="el-icon-arrow-right"> <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item> <el-breadcrumb-item>流动治理</el-breadcrumb-item> <el-breadcrumb-item>流动列表</el-breadcrumb-item> <el-breadcrumb-item>流动详情</el-breadcrumb-item></el-breadcrumb><el-dropdown><span class="el-dropdown-link"> <el-avatar shape="square" :size="30" class="lazy" referrerpolicy="no-referrer" data-src="https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png"></el-avatar> <i class="el-icon-arrow-down el-icon--right"></i></span><el-dropdown-menu slot="dropdown"> <el-dropdown-item>用户ID</el-dropdown-item> <el-dropdown-item divided>退出</el-dropdown-item></el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'AppHeader'
})
</script>
<style lang="scss" scoped>
.header {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.el-dropdown-link {
display: flex;align-items: center;
}
}
</style>
layout/index.vue 引入app-header组件
<template>
<el-container>
<el-aside width="200px"> <app-aside/></el-aside><el-container> <el-header> <app-header /> </el-header> <el-main> <router-view /> </el-main></el-container>
</el-container>
</template>
<script lang="ts">
import Vue from 'vue'
import AppAside from './components/app-aside.vue'
import AppHeader from './components/app-header.vue'
export default Vue.extend({
name: 'LayoutIndex',
components: {
AppAside,AppHeader
}
})
</script>
<style lang="scss" scoped>
.el-container {
min-height: 100vh;
min-width: 980px;
}
.el-aside {
background: #d3dce6
}
.el-header {
background: #fff;
}
.el-main {
background: #e9eef3
}
</style>
App.vue
<template>
<div id="app">
<router-view />
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'App'
})
</script>
<style lang="scss" scoped>
// @import "~@/styles/variables.scss";
.text {
color: $success-color;
}
</style>
路由表配置:
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Layout from '@/layout/index.vue'
const routes: Array<RouteConfig> = [
{
path: '/login',name: 'login',component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '/',component: Layout,children: [ { path: '', name: 'home', component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue') }, { path: '/user', name: 'user', component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue') }, { path: '/role', name: 'role', component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue') }, { path: '/advert', name: 'advert', component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue') }, { path: '/advert-space', name: 'advert-space', component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue') }, { path: '/menu', name: 'menu', component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue') }, { path: '/resource', name: 'resource', component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue') }, { path: '/course', name: 'course', component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue') }]
},
{
path: '*',name: '404',component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
}
]
export default router
十三、登录
- 页面布局
views/login/index.vue
<template>
<div class="login">
<el-form ref="form" :model="form" label-width="80px" label-position="top" class="login-form"> <el-form-item label="手机号"> <el-input v-model="form.name"></el-input> </el-form-item> <el-form-item label="明码"> <el-input v-model="form.name"></el-input> </el-form-item> <el-form-item> <el-button class="login-btn" type="primary" @click="onSubmit">登录</el-button> </el-form-item></el-form>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'LoginIndex',
data () {
return { form: { name: '', region: '', date1: '', date2: '', delivery: false, type: [], resource: '', desc: '' }}
},
methods: {
onSubmit () { console.log('submit!')}
}
})
</script>
<style lang="scss" scoped>
.login {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.login-form {
width: 300px;padding: 20px;background: #fff;border-radius: 5px;
}
.login-btn {
width: 100%;
}
}
</style>
- 接口测试
PostMan高能应用:
创立接口汇合:
image-20200929230629429
创立文件夹:
image-20200929230728701
将接口保留到方才创立的用户接口文件夹,并且重命名这个接口为用户登录
创立汇合变量:
变量名为URL,变量值为http://edufront.lagou.com
而后在接口中就能够应用这个变量了,写成这样:{{URL}}/front/user/login
- 申请登录
npm install qs
// 2. 验证通过 -> 提交表单
const { data } = await request({
method: 'POST',
url: '/front/user/login', // 此处要走代理,否则无奈跨域申请
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: qs.stringify(this.form) // axios 默认发送是 application/json 格局的数据
}) - 解决申请后果
// 3. 解决申请后果
if (data.state !== 1) {
// 失败:给出提醒
return this.$message.error(data.message)
}
// 胜利:跳转到首页
this.$message.success('登录胜利')
this.$router.push({
name: 'home'
}) - 表单验证
model="form"
:rules="rules"
ref="ruleForm"
el-form-item 绑定 prop 属性
rules: {
phone: [
{ required: true, message: '请输出手机号', trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: '请输出正确的手机号', trigger: 'blur' }
],
password: [
{ required: true, message: '请输出明码', trigger: 'blur' },
{ min: 6, max: 18, message: '长度在 6 到 18 个字符', trigger: 'blur' }
]
}
指定类型:
import { Form } from 'element-ui'
await (this.$refs.form as Form).validate()
- 申请期间禁用按钮点击
给el-button减少一个属性:loading="isLoginLoading"
data里isLoginLoading默认为false,在表单通过验证时this.isLoginLoading = true,将按钮设为loading状态,在实现提交无论申请后果是胜利还是失败,将表单loading状态去掉this.isLoginLoading = false
- 封装申请办法
services/user.ts
/**
- 用户相干申请模块
*/
import qs from 'qs'
import request from '@/utils/request'
interface User {
phone: string
password: string
}
export const login = (data: User) => {
return request({
method: 'POST',url: '/front/user/login', // 此处要走代理,否则无奈跨域申请headers: { 'content-type': 'application/x-www-form-urlencoded' },data: qs.stringify(data) // axios 默认发送是 application/json 格局的数据
})
}
iews/login/index.vue
<template>
<div class="login">
<el-form ref="form" :model="form" :rules="rules" label-width="80px" label-position="top" class="login-form"> <el-form-item label="手机号" prop="phone"> <el-input v-model="form.phone"></el-input> </el-form-item> <el-form-item label="明码" prop="password"> <el-input v-model="form.password" type="password"></el-input> </el-form-item> <el-form-item> <el-button class="login-btn" :loading="isLoginLoading" type="primary" @click="onSubmit">登录</el-button> </el-form-item></el-form>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { Form, Loading } from 'element-ui'
import { login } from '@/services/user'
export default Vue.extend({
name: 'LoginIndex',
data () {
return { isLoginLoading: false, form: { phone: '18201288771', password: '111111' }, rules: { phone: [ { required: true, message: '请输出手机号', trigger: 'blur' }, { pattern: /^1\d{10}$/, message: '请输出正确的手机号', trigger: 'blur' } ], password: [ { required: true, message: '请输出明码', trigger: 'blur' }, { min: 6, max: 18, message: '长度在 6 到 18 个字符', trigger: 'blur' } ] }}
},
methods: {
async onSubmit () { // 1. 表单验证 // await (this.$refs.form as Form).validate() // 如果验证不通过,会抛出一个 Promise 异样,阻断前面程序的运行 try { // 1. 表单验证 await (this.$refs.form as Form).validate() // 表单按钮 loading this.isLoginLoading = true // 2. 验证通过 -> 提交表单 const { data } = await login(this.form) console.log(data) // 3. 解决申请后果 if (data.state !== 1) { this.isLoginLoading = false // 失败:给出提醒 return this.$message.error(data.message) } // 胜利:跳转到首页 this.$message.success('登录胜利') this.$router.push({ name: 'home' }) } catch (err) { console.log('登录失败', err) } // 完结登录按钮的 Loading this.isLoginLoading = false}
}
})
</script>
<style lang="scss" scoped>
.login {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.login-form {
width: 300px;padding: 20px;background: #fff;border-radius: 5px;
}
.login-btn {
width: 100%;
}
}
</style>
- 对于申请体data和ContentType的问题
axios中,
如果 data 是 qs.stringify(data) 格局,则 content-type 是 application/x-www-form-urlencoded
如果 data 是 json 格局,则 content-type 是 application/json
如果 data 是FormData 格局,则 content-type 是 multipart/form-data
十四、身份认证
- 把登录状态存储到Vuex容器中
登录胜利时,记录登录状态,状态须要可能全局拜访(放到Vuex容器中)。而后在拜访须要登录的页面的时候,判断有没有登录状态(路由拦截器)
容器的状态实现了数据共享,在组件外面拜访不便,然而没有长久化的性能。为了避免页面刷新数据失落,咱们须要把user数据长久化。
store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
user: JSON.parse(window.localStorage.getItem('user') || 'null') // 以后登录用户状态
},
mutations: {
// 批改容器数据,必须应用mutation函数setUser (state, payload) { state.user = JSON.parse(payload) window.localStorage.setItem('user', payload)}
},
actions: {
},
modules: {
}
})
iew/login/index.vue
// 胜利:跳转到首页
this.$message.success('登录胜利')
this.$store.commit('setUser', data.content)
- 校验页面拜访权限
前置路由守卫
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
在路由表中配置前置路由拦挡守卫
router/index.js
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Layout from '@/layout/index.vue'
import store from '@/store'
Vue.use(VueRouter)
const routes: Array<RouteConfig> = [
{
path: '/login',name: 'login',component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
path: '/',component: Layout,meta: { // 父路由写过了 requiresAuth ,子路由就能够不必写了 requiresAuth: true // 自定义数据}, // meta 默认就是一个空对象,不写也行children: [ { path: '', name: 'home', component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue') // meta: { // requiresAuth: true // 自定义数据 // } // meta 默认就是一个空对象,不写也行 }, { path: '/user', name: 'user', component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue') }, { path: '/role', name: 'role', component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue') }, { path: '/advert', name: 'advert', component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue') }, { path: '/advert-space', name: 'advert-space', component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue') }, { path: '/menu', name: 'menu', component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue') }, { path: '/resource', name: 'resource', component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue') }, { path: '/course', name: 'course', component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue') }]
},
{
path: '*',name: '404',component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
}
]
const router = new VueRouter({
routes
})
// 路由前置守卫
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// 只有有一级路由须要身份认证,就须要验证登录状态if (!store.state.user) { next({ name: 'login', query: { redirect: to.fullPath // 把登录胜利须要返回的页面通知登录页面 } })} else { next() // 容许通过}
} else {
next() // 确保肯定要调用 next()
}
})
export default router
- 登录胜利后跳转回原页面
在路由拦截器中的query参数中带着返回路由
query: {
redirect: to.fullPath // 把登录胜利须要返回的页面通知登录页面
}
在登录胜利跳转时,跳转到query参数中的返回路由,没有的话,则进入首页
this.$router.push(this.$route.query.redirect as string || '/')
- 测试获取以后登录用户信息接口
在Postman的接口汇合里对立设置Authorization
携带Token获取用户信息
image-20201002064025511
- 展现以后登录用户信息
:src="userInfo.portrait || require('../../assets/default-avatar.png')"
应用require获取默认图片,动静src要用require办法获取,图片作为js模块解析。
layout/app-header.vue
<template>
<div class="header">
<el-breadcrumb separator-class="el-icon-arrow-right"> <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item> <el-breadcrumb-item>流动治理</el-breadcrumb-item> <el-breadcrumb-item>流动列表</el-breadcrumb-item> <el-breadcrumb-item>流动详情</el-breadcrumb-item></el-breadcrumb><el-dropdown><span class="el-dropdown-link"> <el-avatar shape="square" :size="30" :src="userInfo.portrait || require('../../assets/default-avatar.png')"></el-avatar> <i class="el-icon-arrow-down el-icon--right"></i></span><el-dropdown-menu slot="dropdown"> <el-dropdown-item>{{userInfo.userName}}</el-dropdown-item> <el-dropdown-item divided>退出</el-dropdown-item></el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { getUserInfo } from '@/services/user'
export default Vue.extend({
name: 'AppHeader',
data () {
return { userInfo: {} // 以后登录用户信息}
},
created () {
this.loadUserInfo()
},
methods: {
async loadUserInfo () { const { data } = await getUserInfo() this.userInfo = data.content}
}
})
</script>
<style lang="scss" scoped>
.header {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.el-dropdown-link {
display: flex;align-items: center;
}
}
</style>
其中getUserInfo来自于services
services/user.ts
export const getUserInfo = () => {
return request({
method: 'GET',url: '/front/user/getInfo',headers: { Authorization: store.state.user.access_token}
})
}
- 应用申请拦截器对立设置 Token
utils/request.ts
// 申请拦截器
request.interceptors.request.use(function (config) {
// 这里是拦挡的接口
// 改写 config 对象
const { user } = store.state
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}
return config
}, function (error) {
return Promise.reject(error)
})
本来写在services/user.ts外面的接口中的Authorization申请头就能够去掉了
- 用户退出登录
@click.native="handleLogout"给组件注册原生事件到组件外部的根元素上。
<el-dropdown-item divided @click.native="handleLogout">退出</el-dropdown-item>
handleLogout () {
this.$confirm('确认退出吗?', '退出提醒', {
confirmButtonText: '确定',cancelButtonText: '勾销',type: 'warning'
}).then(() => { // 确认执行
// 革除登录状态this.$store.commit('setUser', null) // 此时清空了容器中的数据,也清空了本地存储// 跳转到登录页this.$router.push({ name: 'login'})this.$message({ type: 'success', message: '退出胜利!'})
}).catch(() => {
this.$message({ type: 'info', message: '已勾销退出'})
})
}
十五、解决 Token 过期
- 概念介绍
`access_token: 获取须要受权的接口数据
expires_in: 过期工夫
refresh_token: 刷新获取新的 access_token
为什么access_token 须要有过期工夫以及为什么比拟短?为了平安。
办法一:在申请发动前拦挡每个申请,判断 token 的无效工夫是否曾经过期,若已过期,则将申请挂起,先刷新token后再持续申请。
长处:在申请前拦挡,能节俭申请,省流量。
毛病:须要后端额定提供一个token过期工夫的字段;应用本地工夫判断,若本地工夫被篡改,特地是本地工夫比服务器工夫慢时,拦挡会失败。
办法二:不在申请前拦挡,而是拦挡返回后的数据,先发动申请,接口返回过期后,先刷新token,再进行一次重试。
长处:不须要额定的token过期字段,不须要判断工夫。
毛病:会耗费多一次申请,耗流量。
综上,办法一和二优缺点是互补的,办法一有校验失败的危险(本地工夫被篡改),办法二更简略粗犷,等晓得服务器曾经过期了再重试一次,只是会耗多一次申请。
应用形式二解决刷新Token的操作。
- 剖析响应拦截器
request/index.ts
// 响应拦截器 request
request.interceptors.response.use(function (response) { // 状态码为 2xx 都会进入这里
console.log('申请响应胜利了', response)
return response
}, function (error) { // 超过 2xx 状态码都在这里
console.dir('申请响应失败了', error)
// 如果应用的 HTTP 状态码,错误处理就写到这里
return Promise.reject(error)
})
- 实现根本流程逻辑
无痛刷新:先申请接口,如果是401,判断容器中是否有user,如果没有的话,间接进入登录页,如果有user,则申请refresh_token接口,而后从新设置接口的返回值给容器的user,再从新申请接口。
// 响应拦截器 request
request.interceptors.response.use(function (response) { // 状态码为 2xx 都会进入这里
console.log('申请响应胜利了', response)
return response
}, async function (error) { // 超过 2xx 状态码都在这里
console.dir('申请响应失败了', error)
// 如果应用的 HTTP 状态码,错误处理就写到这里
if (error.response) { // 申请收到响应了,然而状态码超过了 2xx 范畴
// 400// 401// 403// 404// 500const { status } = error.responseif (status === 400) { Message.error('申请参数谬误')} else if (status === 401) { // token 有效 (没有提供 token, token 是有效的, token 过期了) // 如果有 refresh_token 则尝试应用 refresh_token 获取新的 access_token if (!store.state.user) { redirectLogin() return Promise.reject(error) } // 尝试刷新获取新的 token try { const { data } = await axios.create()({ // 创立一个新的 axios 实例发送申请,因为如果应用request会可能产生 401 死循环 method: 'POST', url: '/front/user/refresh_token', data: qs.stringify({ refreshtoken: store.state.user.refresh_token }) }) // 胜利了 -> 把本次失败的申请从新收回去 // 把胜利刷新拿到的 access_token 更新到容器和本地存储中 store.commit('setUser', data.content) // 把本地失败的申请从新收回去 return request(error.config) // 失败申请的配置信息 } catch (err) { // 把以后登录用户状态革除 store.commit('setUser', null) // 失败了 -> 间接去跳转到登录页 redirectLogin() return Promise.reject(error) }} else if (status === 403) { Message.error('没有权限,请分割管理员')} else if (status === 404) { Message.error('申请资源不存在')} else if (status >= 500) { Message.error('服务端谬误,请分割管理员')}
} else if (error.request) { // 申请收回去了,然而没有收到响应(申请超时,网络断开)
Message.error('申请超时,请刷新重试')
} else { // 在设置申请时产生了一些事件,触发了一个谬误
Message.error('申请失败: ' + error.message)
}
// 把申请失败的谬误对象持续抛出,扔给下一个调用者
return Promise.reject(error)
})
- 对于屡次申请问题
应用变量 isRefreshing 管制刷新 token 的状态。应用requests 存储刷新 token 期间过去的 401 申请。requests数组存储调用resolve的办法。在刷新token结束后循环遍历requests办法。
最终utils/request.ts内容如下:
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import router from '@/router'
import qs from 'qs'
function redirectLogin () {
return router.push({
name: 'login',query: { redirect: router.currentRoute.fullPath}
})
}
function refreshToken () {
return axios.create()({ // 创立一个新的 axios 实例发送申请,因为如果应用request会可能产生 401 死循环
method: 'POST',url: '/front/user/refresh_token',data: qs.stringify({ // refreshtoken 只能应用一次 refreshtoken: store.state.user.refresh_token})
})
}
const request = axios.create({
// 配置选项
// baseURL,
// timeout,
})
// 申请拦截器 request
request.interceptors.request.use(function (config) {
// 这里是拦挡的接口
// 改写 config 对象
const { user } = store.state
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}
return config
}, function (error) {
return Promise.reject(error)
})
// 响应拦截器 request
let isRefreshing = false // 管制刷新 token 的状态
let requests: (() => void)[] = [] // 存储刷新 token 期间过去的 401 申请
request.interceptors.response.use(function (response) { // 状态码为 2xx 都会进入这里
console.log('申请响应胜利了', response)
return response
}, async function (error) { // 超过 2xx 状态码都在这里
console.dir('申请响应失败了', error)
// 如果应用的 HTTP 状态码,错误处理就写到这里
if (error.response) { // 申请收到响应了,然而状态码超过了 2xx 范畴
// 400// 401// 403// 404// 500const { status } = error.responseif (status === 400) { Message.error('申请参数谬误')} else if (status === 401) { // token 有效 (没有提供 token, token 是有效的, token 过期了) // 如果有 refresh_token 则尝试应用 refresh_token 获取新的 access_token if (!store.state.user) { redirectLogin() return Promise.reject(error) } if (!isRefreshing) { // 解决屡次申请从新刷新 Token 的问题 isRefreshing = true // 尝试刷新获取新的 token return refreshToken().then(res => { if (!res.data.success) { throw new Error('刷新 Token 失败') } // 胜利了 -> 把本次失败的申请从新收回去 // 把胜利刷新拿到的 access_token 更新到容器和本地存储中 store.commit('setUser', res.data.content) // 把本地失败的申请从新收回去 requests.forEach(cb => cb()) requests = [] // 重置 requests 数组 return request(error.config) // 失败申请的配置信息 }).catch(err => { // 把以后登录用户状态革除 store.commit('setUser', null) // 失败了 -> 间接去跳转到登录页 redirectLogin() return Promise.reject(err) }).finally(() => { isRefreshing = false // 重置状态 }) } // 刷新状态下,把申请挂起,放到 requests 数组中 return new Promise(resolve => { requests.push(() => { resolve(request(error.config)) }) })} else if (status === 403) { Message.error('没有权限,请分割管理员')} else if (status === 404) { Message.error('申请资源不存在')} else if (status >= 500) { Message.error('服务端谬误,请分割管理员')}
} else if (error.request) { // 申请收回去了,然而没有收到响应(申请超时,网络断开)
Message.error('申请超时,请刷新重试')
} else { // 在设置申请时产生了一些事件,触发了一个谬误
Message.error('申请失败: ' + error.message)
}
// 把申请失败的谬误对象持续抛出,扔给下一个调用者
return Promise.reject(error)
})
export default request