Vue2+Ts实战项目开发与项目优化(一)项目初始化

1. 使用Vue CLI创建项目

vue create edu-boss-fed

Vue2+Ts实战项目开发与项目优化(一)项目初始化_第1张图片
Vue2+Ts实战项目开发与项目优化(一)项目初始化_第2张图片

2. 加入git版本管理

git init
git add .
git commit -a -m "项目初始化"
git remote add orign 地址
git push origin -u master

3. 初始目录结构说明

Vue2+Ts实战项目开发与项目优化(一)项目初始化_第3张图片

4. 调整初始目录结构

默认生成的目录结构不满足我们的开发需求,所以需要做一些自定义改动

这里主要处理下面的内容:

  • 删除初始化的默认文件
  • 新增调整我们需要的目录结构

修改App.vue

<template>
  <div id="app">
    <h1>Apph1>
    
    <router-view />
  div>
template>

<style lang="scss" scoped>
style>

修改router/index.ts

import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'

Vue.use(VueRouter)

const routes: Array<RouteConfig> = [
]

const router = new VueRouter({
  routes
})

export default router

删除views/About.vue
删除views/Home.vue
删除components/HelloWorld.vue
新增utils目录
新增styles目录
新增services目录

5. 使用TS开发Vue

环境说明

在Vue项目中启用TypeScript支持
(1)全新项目:使用Vue CLI脚手架工具创建Vue项目
(2)已有项目:添加Vue官方配置的TypeScript适配插件

vue add @vue/typescript

相关配置

(1)安装了TypeScript相关的依赖项
dependencies依赖:

依赖项 说明
vue-class-component 提供使用Class语法写Vue组件
vue-property-decorator 在Class语法基础之上提供了一些辅助装饰器

devDependencies依赖:

依赖项 说明
@typescript-eslint/eslint-plugin 使用ESLint校验TypeScript代码
@typescript-eslint/parser 将TypeScript转为AST供ESLint校验使用
@vue/cli-plugin-typescript 使用TypeScript + ts-loader + fork-ts-checker-webpack-plugin进行更快的类型检查
@vue/eslint-config-typescript 兼容ESLint的TypeScript校验规则
typescript TypeScript编译器,提供类型校验和转换js功能

(2)shims-vue.d.ts文件的作用

// 主要用于 TypeScript 识别 .vue文件模块
// TypeScript 默认不支持导入 .vue 模块,这个文件告诉 TypeScript
// 导入 .vue 文件模块都按 VueConstructor 类型识别处理
declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

(3)shims-tsx.d.ts文件的作用

为jsx组件模板补充类型声明

定义Vue组件

1. 使用OptionsAPI定义Vue组件

Vue2+Ts实战项目开发与项目优化(一)项目初始化_第4张图片

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  // 类型推断已启用
  data(){
  	return{}
  }
})
</script>

2. 使用ClassAPIs定义Vue组件

如果您在声明组件时更喜欢基于类的 API,则可以使用官方维护的 vue-class-component 装饰器:
教程文档

import Vue from 'vue'
import Component from 'vue-class-component'

// @Component 修饰符注明了此类为一个 Vue 组件
@Component({
  // 所有的组件选项都可以放在这里
  template: ''
})
export default class MyComponent extends Vue {
  // 初始数据可以直接声明为实例的 property
  message: string = 'Hello!'

  // 组件方法也可以直接声明为实例的方法
  onClick (): void {
    window.alert(this.message)
  }
}

关于装饰器语法

装饰器是ES草案中的一个新特性,不过这个草案最近有可能发生重大调整,所以不建议生成环境使用

装饰器是一种特殊类型的声明,它能够加到类、方法,属性、参数上添加额外的功能
通俗的讲装饰器就是一个方法;在不修改类的情况下拓展方法;已是Es7标准特性之一

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

@testable
class MytestableClass {
	// ...
}
console.log(MytestableClass.isTestable) // true

3. 使用Class APIS + VuePropertyDecorator创建Vue组件

这种方式继续放大了Class这种组件定义方法

import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class Button extends Vue {
	private count: number = 1
	private text: string = 'Click me'
	@Prop() readonly size?:string
	get content () {
		return `${this.text} ${this.count} `
	}
	increment () {
		this.count++
	}
	mounted () {
		console.log('button is mounted')
	}
}

个人建议:No Class APIS, 只用 Options APIs。

Class 语法仅仅是一种写法而已,最终还是要转换为普通的组件数据结构
装饰器语法还没有正式定稿发布,建议了解即可,正式发布以后在选择使用也可以

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

6. 代码格式化规范

代码格式规范介绍

Vue2+Ts实战项目开发与项目优化(一)项目初始化_第5张图片

良好的代码格式规范更有利于:

  • 更好的多人协作
  • 更好的阅读
  • 更好的维护

标准是什么

没有绝对的标准,下面是一些大厂根据多数开发这的编码习惯定制的一些编码规范,仅供参考

  • JavaScript Standard Style
  • Airbnb JavaScript Style Guide
  • Google JavaScript Style Guide

如何约束代码规范

  • ESlint

我们项目中配置的具体代码规范是什么

vue代码风格指南

module.exports = {
  root: true,
  env: {
    node: true
  },
  // 使用插件的编码规则
  extends: [
    'plugin:vue/essential', // eslint-plugin-vue
    '@vue/standard', // @vue/eslint-config-standard
    '@vue/typescript/recommended' // @vue/eslint-config-typescript
  ],
  parserOptions: {
    ecmaVersion: 2020
  },
  // 自定义编码校验规则
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
  }
}

如何自定义代码格式校验规范

Configuring Rules

ESLint 附带有大量的规则。你可以使用注释或配置文件修改你项目中要使用的规则。要改变一个规则设置,你必须将规则 ID 设置为下列值之一:

"off"0 - 关闭规则
"warn"1 - 开启规则,使用警告级别的错误:warn (不会导致程序退出)
"error"2 - 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出)
Vue2+Ts实战项目开发与项目优化(一)项目初始化_第6张图片

ESLint校验规则
例:语句末尾必须加分号
Vue2+Ts实战项目开发与项目优化(一)项目初始化_第7张图片

'semi': ['error','always'] // 第一项是错误的级别,第二项是参数

注意:修改完后需要删除node_modules里的.cache缓存文件
ESLint Ts校验规则
例:ts接口中不需要加分号
Vue2+Ts实战项目开发与项目优化(一)项目初始化_第8张图片

'@typescript-eslint/member-delimiter-style':['error',
	{
	  "multiline": { // 多行
	    "delimiter": "none", // semi || comma || none
	    "requireLast": true
	  }
	}
]

7. 引入Element组件库

npm i element-ui -S

在main.ts中添加

import ElementUI from 'element-ui'
Vue.use(ElementUI)

8. 样式处理

Vue2+Ts实战项目开发与项目优化(一)项目初始化_第9张图片
variables.scss

// 公共样式变量
$primary-color: #40586f;
$success-color: #51cf66;
$warning-color: #fcc419;
$danger-color: #ff6b6b;
$info-color: #e9eef3; // #22b8cf;

$body-bg: #e9eef3; // #f5f5f9;

$sidebar-bg: #f8f9fb;
$navbar-bg: #f8f9fb;

$font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

index.scss

@import './variables.scss';

// 设置html样式
html {
  font-family: $font-family;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  // better Font Rendering
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
// 设置body样式
body {
  margin: 0;
  background-color: $body-bg;
}

// 更改 element 主题色(写法参考element官方文档)
$--color-primary: $primary-color;
$--color-success: $success-color;
$--color-warning: $warning-color;
$--color-danger: $danger-color;
$--color-info: $info-color;
//  改变 icon 字体路径变量,必需
$--font-path: '~element-ui/lib/theme-chalk/fonts';
// 引入 element 默认主题文件(样式文件)
@import '~element-ui/packages/theme-chalk/src/index';
// node_modules/element-ui/packages/theme-chalk/src/common/var.scss

// overrides
// .el-menu-item, .el-submenu__title {
//   height: 50px;
//   line-height: 50px;
// }

.el-pagination {
  color: #868e96;
}

// components
.status {
  display: inline-block;
  cursor: pointer;
  width: .875rem;
  height: .875rem;
  vertical-align: middle;
  border-radius: 50%;

  &-primary {
    background: $--color-primary;
  }

  &-success {
    background: $--color-success;
  }

  &-warning {
    background: $--color-warning;
  }

  &-danger {
    background: $--color-danger;
  }

  &-info {
    background: $--color-info;
  }
}

在main.ts中引入

import './styles/index.scss'

共享全局样式变量
Vue2+Ts实战项目开发与项目优化(一)项目初始化_第10张图片
在vue.config.js中

module.exports = {
  css: {
    // 默认情况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时生效
    // 因为 `scss` 语法在内部也是由 sass-loader 处理的
    // 但是在配置 `prependData` 选项的时候
    // `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号
    // 在这种情况下,我们可以使用 `scss` 选项,对 `scss` 语法进行单独配置
    loaderOptions: {
      scss: {
        prependData: '@import "~@/styles/variables.scss";'
      }
    }
  }
}

9. 配置后端代理

示例:

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: '',
        ws: true,
        changeOrigin: true
      }
    }
  }
}

10. 封装请求模块

npm i axios

utils下新建request.ts

import axios from 'axios'

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

// 请求拦截器

// 响应拦截器

export default request

11. 初始化路由组件

创建目录
创建目录

配置路由

{
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
},
{
    path: '*',
    name: '404',
    component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/index.vue')
}

创建layout目录index.vue

{
    path: '/',
    component: Layout,
    children: [
      {
        path: '/',
        name: 'home',
        component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
      }
    ]
}

布局容器

layout/index.vue

<template>
  <el-container>
    <el-aside width="200px">
      <app-aside />
    el-aside>
    <el-container>
      <el-header>
        <app-header />
      el-header>
      <el-main>
        <router-view>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-color: #d3dce6;
}
.el-header {
  background-color: #ffffff;
}
.el-main {
  background-color: #e9eef3;
}
style>

侧栏菜单

layout/components/app-aside.vue

<template>
  <div class="aside">
    <el-menu
      default-active="2"
      class="el-menu-vertical-demo"
      @open="handleOpen"
      @close="handleClose"
      background-color="#545c64"
      text-color="#fff"
      active-text-color="#ffd04b"
      router
    >
      <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-setting">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>

头部

layout/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"
          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>用户IDel-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>

12. 登录处理

views/login.vue

<template>
  <div class="login">
    <el-form
      class="login-form"
      label-position="top"
      ref="form"
      :model="form"
      label-width="80px"
      :rules="rules"
    >
      <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">el-input>
      el-form-item>
      <el-button
        class="login-btn"
        type="primary"
        @click="onSubmit"
        :loading="isLoginLoading"
        >登录el-button
      >
    el-form>
  div>
template>

<script lang="ts">
import Vue from 'vue'
import request from '@/utils/request'
import qs from 'qs'
import { Form } from 'element-ui'

export default Vue.extend({
  name: 'LoginIndex',
  data () {
    return {
      form: {
        phone: '',
        password: ''
      },
      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' }
        ]
      },
      isLoginLoading: false
    }
  },
  methods: {
    async onSubmit () {
      try {
        // 1. 表单验证
        await (this.$refs.form as Form).validate() // 若不传入回调函数,则返回一个promise
        // 2. 验证通过 -> 提交表单
        this.isLoginLoading = true
        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格式的数据
        })
        console.log(data)
        // 3. 处理请求结果
        //    失败:给出提示
        if (data.state !== 1) {
          this.$message.error(data.message)
        }else {
          //  成功:跳转到首页
          this.$router.push({
            name: 'home'
          })
          this.$message.success(data.message)
        }
      } catch (error) {
        console.log('登录失败', error)
      }
      // 结束登录按钮的 loading
      this.isLoginLoading = false
    }
  }
})
script>

<style lang="scss" scoped>
.login {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  .login-form {
    width: 300px;
    background-color: #ffffff;
    padding: 20px;
    border-radius: 5px;
  }
  .login-btn {
    width: 100%;
  }
}
style>

13. 封装请求方法

新建services/user.ts

/**
 * 用户相关请求模块
 */
import request from '@/utils/request'
import qs from 'qs'

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 是普通对象,则Content-Type 是 application/json
    // 如果 data 是 qs.stringify(data) 转换之后的数据: key=value&key=value
    // 则Content-Type会被设置为 application/x-www-form-urlencoded
    // 如果 data 是 FormData 对象,则Content-Type 是 multipart/form-data
    data: qs.stringify(data) // axios 默认发送的是application/json格式的数据
  })
}

14. 校验页面访问权限

router/index.ts

const routes: Array<RouteConfig> = [
  {
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
  },
  {
    path: '/',
    component: Layout,
    meta: {
      requiresAuth: true // 自定义数据
    }, // meta 默认就是一空对象
    children: [
      {
        path: '/',
        name: 'home',
        component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
        // meta: {
        //   requiresAuth: true // 自定义数据
        // } // meta 默认就是一空对象
      }
    ]
  },
  {
    path: '*',
    name: '404',
    component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/index.vue')
  }
]

const router = new VueRouter({
  routes
})

// 全局前置守卫:任何页面的访问都要经过这里
// to:要去哪里的路由信息
// from:从哪里来的路由信息
// next:通行的标志
router.beforeEach((to, from, next) => {
  // console.log('进入了全局路由守卫')
  console.log('to=>', to)
  console.log('from=>', from)

  // to.matched 是一个数组(匹配到是路由父级即以下的)
  // 如果当前孙辈||父辈有requiresAuth则匹配有没有登录
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!store.state.user) {
      // 跳转到登录页面
      next({ name: 'login' })
    } else {
      next()
    }
  } else {
    next()
  }

  // 路由守卫中一定要调用next, 否则页码无法展示
  // if (to.path !== 'login') {} // 校验登录状态
})

export default router

15. 登录成功跳转回原页面

router/index.ts

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!store.state.user) {
      // 跳转到登录页面
      next({
        name: 'login',
        query: { // 通过 url 传递查询字符串参数
          redirect: to.fullPath // 把登录成功需要返回的页面告诉登录页
        }
      })
    } else {
      next()
    }
  } else {
    next()
  }
}

views/login/index.vue

//  成功:跳转到之前访问的页面或首页
this.$router.push(this.$route.query.redirect as string || '/')

16. 设置请求拦截器统一token

import axios from 'axios'
import store from '@/store'

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

// 请求拦截器
request.interceptors.request.use((config: any) => {
  // 我们就在这里通过改写 config 配置信息来实现业务功能的统一处理
  const { user } = store.state
  if (user?.access_token) {
    config.headers.Authorization = user.access_token
  }
  // 注意:这里一定要返回 config,否则请求发不出去了
  return config
}, (error) => Promise.reject(error)) // 请求本身就出错了走这里
// 响应拦截器

export default request

17. 用户退出

穿透组件,将该函数注册到根元素

<el-dropdown-item divided @click.native="handleLogout">退出el-dropdown-item>
 		// 清除登录状态
        this.$store.commit('setUser', null)

        // 跳转到登录页面
        this.$router.push({
          name: 'login'
        })

你可能感兴趣的:(Vue,vue.js,vue,前端,elementui,typescript)