关于vue.js:36-vue实战

31次阅读

共计 27291 个字符,预计需要花费 69 分钟才能阅读完成。

一、应用 VueCli 创立我的项目
vue create edu-boss-fed
image-20200926080833055
image-20200926082611688

cd edu-boss-fed
yarn serve
二、退出 Git 版本治理
创立一个 GitHub 空仓库,将本地我的项目同步到 GitHub 上

git init
git add .
git commit -m"vue2+ts 我的项目初始化"
git remote add origin git@github.com:2604150210/edu-boss-fed.git
git branch -M master
git 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 相干配置文件

四、调整初始目录构造

  1. 删除默认示例文件
    App.vue 文件删除款式内容,删除路由链接,然而不能删除 vue-router 组件
    router/index.ts 清空路由表数组
    删除 views 文件夹里的 Home.vue 和 About.vue,删除 components 文件里的 HelloWorld.vue,删除 assets 里的 logo.png
  2. 新增一些文件或目录
    在 src 外面创立文件夹 utils(放工具函数)、styles(放全局款式)、services(放申请接口)
    五、TS 相干配置
  3. 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 性能
  4. 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"]
}
  1. Shims-vue.d.ts
    ts 辨认不了.vue 结尾的文件,所以申明它的类型为 Vue 构造函数,在这个文件中做了适配。

declare module ‘*.vue’ {
import Vue from ‘vue’
export default Vue
}

  1. shims-tsx.d.ts
    补充了一些类型申明,否则在 JSX 中应用这些成员时,会找不到类型,在这个文件中做了适配。

import Vue, {VNode} from ‘vue’

declare global {
namespace JSX {

// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {[elem: string]: any;
}

}
}

  1. 在 ts 模块中文件扩大名为.ts
    六、应用 TS 开发 Vue 我的项目
  2. 应用 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

  1. 应用 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>

  1. 对于装璜器语法
    装璜器是 ES 草案中的一个新个性,不过这个草案最近有可能产生重大调整,所以不倡议在生产环境中应用

类装璜器:

function testable (target) {
target.isTestable = true
}

@testable
class MyTestableClass {
// …
}

console.log(MyTestableClass.isTestable) // true
装璜器就是扩大类的属性。

  1. 应用 VuePropertyDecorator 创立 Vue 组件
    装璜器语法不稳固
  2. 总结创立组件的形式
    Options APIs
    Class APIs
    Class + decorator
    集体倡议:No Class APIs,只用 Options APIs.

Class 语法仅仅是一种写法而已,最终还是要转换成一般的组件数据结构。

装璜器语法还没有正式定稿公布,倡议理解即可,正式公布当前再抉择应用也能够。

应用 Options APIs 最好是应用 export default Vue.extend({…}),而不是 export default {…}

七、代码格局标准

  1. 规范是什么
    Standard Style 宽松一点,适宜集体或小团队
    Airbnb 更严格,适宜大型团队
    google 更严格,适宜大型团队
  2. 如果束缚代码标准
    只靠口头约定必定是不行的,所以要利用工具来强制执行

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'

}
}

  1. 自定义校验规定
    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>
十一、接口解决

  1. 配置接口文档
    解决跨域问题:如果你的前端利用和后端 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

  1. 封装申请模块
    yarn add axios
    tils/request.ts

import axios from ‘axios’

const request = axios.create({
// 配置选项
// baseURL,
// timeout,
})

// 申请拦截器

// 响应拦截器

export default request
测试申请胜利:

image-20200927071409320

十二、布局

  1. 初始化路由页面组件
    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

  1. 布局组件 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>

  1. 侧边栏组件
    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>

  1. 头部 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
十三、登录

  1. 页面布局
    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>

  1. 接口测试
    PostMan 高能应用:

创立接口汇合:

image-20200929230629429

创立文件夹:

image-20200929230728701

将接口保留到方才创立的用户接口文件夹,并且重命名这个接口为用户登录

创立汇合变量:

变量名为 URL,变量值为 http://edufront.lagou.com

而后在接口中就能够应用这个变量了,写成这样:{{URL}}/front/user/login

  1. 申请登录
    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 格局的数据
    })
  2. 解决申请后果
    // 3. 解决申请后果
    if (data.state !== 1) {
    // 失败:给出提醒
    return this.$message.error(data.message)
    }
    // 胜利:跳转到首页
    this.$message.success(‘ 登录胜利 ’)
    this.$router.push({
    name: ‘home’
    })
  3. 表单验证
    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()

  1. 申请期间禁用按钮点击
    给 el-button 减少一个属性:loading=”isLoginLoading”

data 里 isLoginLoading 默认为 false,在表单通过验证时 this.isLoginLoading = true,将按钮设为 loading 状态,在实现提交无论申请后果是胜利还是失败,将表单 loading 状态去掉 this.isLoginLoading = false

  1. 封装申请办法
    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>

  1. 对于申请体 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

十四、身份认证

  1. 把登录状态存储到 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)

  1. 校验页面拜访权限
    前置路由守卫

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

  1. 登录胜利后跳转回原页面
    在路由拦截器中的 query 参数中带着返回路由

query: {
redirect: to.fullPath // 把登录胜利须要返回的页面通知登录页面
}
在登录胜利跳转时,跳转到 query 参数中的返回路由,没有的话,则进入首页

this.$router.push(this.$route.query.redirect as string || ‘/’)

  1. 测试获取以后登录用户信息接口
    在 Postman 的接口汇合里对立设置 Authorization

携带 Token 获取用户信息

image-20201002064025511

  1. 展现以后登录用户信息
    :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}

})
}

  1. 应用申请拦截器对立设置 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 申请头就能够去掉了

  1. 用户退出登录
    @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 过期

  1. 概念介绍
    `access_token: 获取须要受权的接口数据

expires_in: 过期工夫

refresh_token: 刷新获取新的 access_token

为什么 access_token 须要有过期工夫以及为什么比拟短?为了平安。

办法一:在申请发动前拦挡每个申请,判断 token 的无效工夫是否曾经过期,若已过期,则将申请挂起,先刷新 token 后再持续申请。

长处:在申请前拦挡,能节俭申请,省流量。

毛病:须要后端额定提供一个 token 过期工夫的字段;应用本地工夫判断,若本地工夫被篡改,特地是本地工夫比服务器工夫慢时,拦挡会失败。

办法二:不在申请前拦挡,而是拦挡返回后的数据,先发动申请,接口返回过期后,先刷新 token,再进行一次重试。

长处:不须要额定的 token 过期字段,不须要判断工夫。

毛病:会耗费多一次申请,耗流量。

综上,办法一和二优缺点是互补的,办法一有校验失败的危险(本地工夫被篡改),办法二更简略粗犷,等晓得服务器曾经过期了再重试一次,只是会耗多一次申请。

应用形式二解决刷新 Token 的操作。

  1. 剖析响应拦截器
    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)
})

  1. 实现根本流程逻辑
    无痛刷新:先申请接口,如果是 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
// 500
const {status} = error.response
if (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)
})

  1. 对于屡次申请问题
    应用变量 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
// 500
const {status} = error.response
if (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

正文完
 0