Vite + Vue3 +Typescript 前端学习笔记(上篇)

1,982 阅读6分钟

创建项目

# yarn & npm
yarn create @vitejs/app
npm init @vitejs/app

项目结构

vite创建时项目后得到项目目录结构如下,vite.config.ts则为vite项目的配置文件,vite是以插件形式对vue进行支持,脚手架已经配置了vue的支持

使用ts则需要先安装一下类型声明文件

# yarn && npm
yarn add -D @types/node
npm install -D @types/node
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()] // 引入的vue插件
})

TypeScript配置文件修改

ts文件夹别名设置,项目文件夹创建types文件夹用来存放项目声明定义ts声明和接口,并在tsconfig.json添加项目文路径与文件支持

// tsconfig.json

{
  ...,
    'baseUrl': '.', //基础目录
    'paths': {
      '@/*': ['src/*'],  // src/* 别名设置
      '#/*': ['types/*'] // types/* 别名设置
    }
  },
  'include': [
    ...,
    'types/**.ts', 	// 添加 types/**.ts 目录文件依赖
    'types/**.d.ts' // 添加 types/**.d.ts 目录文件依赖
  ]
}

// vite.config.ts
import { defineConfig } from 'vite'
import { resolve } from 'path'
...
export default defineConfig({
  ...,
  resolve: {
    // 配置别名
    alias: {
      '@': resolve(__dirname, './src'),
      '#': resolve(__dirname, './types'),
    },
  },
})

Axios支持

安装axios

# yarn && npm
yarn add axios
npm install --save axios

1、项目目录src下新建utils/http文件夹,创建分别创建index.ts axios.ts,用到loadsh-es需要先安装一下

# loadsh-es 
yarn add -S loadsh-es && yarn add -D @types/loadsh-es
npm install --save loadsh-es && npm install --save-dev @types/loadsh-es

2、types目录先创建axios请求可以自定义的接口

export interface CustomResqusetOptions {
  // 自定义请求设置参数
  joinTime?: boolean
}

export interface CustomResponseResult {
  // 自定义返回的参数
  data?: any[]
}

3、实现index.ts axios.ts

// axios.ts
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { CustomResqusetOptions, CustomResponseResult } from '#/axios'
import axios from 'axios'
import { cloneDeep, isFunction } from 'lodash-es'

export interface CreateAxiosConfig extends AxiosRequestConfig {
  transform?: AxiosTransform
  customResquestOptions?: CustomResqusetOptions
}
// 抽象接口
export abstract class AxiosTransform {
  beforeRequestHook?: (config: AxiosRequestConfig, options: CustomResqusetOptions) => AxiosRequestConfig
  requestInterceptor?: (config: AxiosRequestConfig, options: CreateAxiosConfig) => AxiosRequestConfig
  transformResponseHook?: (res: AxiosResponse<any>, options: CustomResqusetOptions) => AxiosResponse<any> | any
  resqonseInterceptors?: (res: AxiosResponse<any>, options: CreateAxiosConfig) => AxiosResponse<any> | Promise<AxiosResponse<any>> | any
}

// 创建一个Axios类,内部实现请求拦截器、响应拦截器执行时机的钩子函数,使用单例模式创建class
export class Aixos {
  private axiosInstance: AxiosInstance
  private readonly options: CreateAxiosConfig

  constructor(options: CreateAxiosConfig) {
    this.options = options
    this.axiosInstance = axios.create()
    this.setup()
  }

  private setup() {
    const transform = this.getTransform()
    if (!transform) return

    const { requestInterceptor, resqonseInterceptors } = transform

    // 请求拦截器
    this.axiosInstance.interceptors.request.use((config) => {
      // 增加其他优先处理逻辑

      // 拦截器
      if (requestInterceptor && typeof isFunction(requestInterceptor)) {
        config = requestInterceptor(config, this.options)
      }
      return config
    })
    // 响应拦截器
    this.axiosInstance.interceptors.response.use((res) => {
      if (resqonseInterceptors && isFunction(resqonseInterceptors))
        res = resqonseInterceptors(res, this.options)
      return res
    })
  }

  private getTransform() {
    const { transform } = this.options
    return transform
  }

  getAxios() {
    return this.axiosInstance
  }

  configAxios(config: CreateAxiosConfig) {
    if (!this.axiosInstance) return
    this.axiosInstance = axios.create(config)
  }

  setHeader(header?: any) {
    if (!this.axiosInstance) return
    Object.assign(this.axiosInstance.defaults.headers, header)
  }

  request<T = any>(config: AxiosRequestConfig, options?: CustomResqusetOptions): Promise<T> {

    let conf: CreateAxiosConfig = cloneDeep(config)
    const { customResquestOptions } = this.options
    let opt: CustomResqusetOptions = Object.assign({}, customResquestOptions, options) // 合并默认设置与传入配置
    const transform = this.getTransform()
    const { beforeRequestHook, transformResponseHook } = transform || {}
    // 钩子函数
    if (beforeRequestHook && isFunction(beforeRequestHook)) {
      conf = beforeRequestHook(config, opt)
    }

    conf.customResquestOptions = opt //合并配置
    // 这里添加一些公共处理逻辑

    return new Promise((reslove, reject) => {
      this.axiosInstance
        .request<any, AxiosResponse<CustomResponseResult>>(conf)
        .then((res: AxiosResponse<CustomResponseResult>) => {
          if (transformResponseHook && isFunction(transformResponseHook)) {
            try {
              const transformResponse = transformResponseHook(res)
              reslove(transformResponse)
            }
            catch (err) {
              reject(err)
            }
            return
          }
        }).catch((e: Error) => reject(e))
    })
  }

  get<T = any>(config: AxiosRequestConfig, options?: CustomResqusetOptions): Promise<T> {
    return this.request({ ...config, method: 'GET' }, options)
  }

  psot<T = any>(config: AxiosRequestConfig, options?: CustomResqusetOptions): Promise<T> {
    return this.request({ ...config, method: 'POST' }, options)
  }

  put<T = any>(config: AxiosRequestConfig, options?: CustomResqusetOptions): Promise<T> {
    return this.request({ ...config, method: 'PUT' }, options)
  }

  delete<T = any>(config: AxiosRequestConfig, options?: CustomResqusetOptions): Promise<T> {
    return this.request({ ...config, method: 'DELETE' }, options)
  }
}

// index.ts
import type { CreateAxiosConfig, AxiosTransform } from './aixos'
import type { CustomResqusetOptions } from '#/axios'

import { Aixos } from './aixos'
import { objectDeepMerge } from '@/utils'

const transform: AxiosTransform = {

  beforeRequestHook(config, options) {
    // 请求之前钩子函数
    console.log('1、请求之前钩子函数: transformResponseHook')
    return config
  },
  transformResponseHook(res, options) {
    // 响应拦截后钩子函数
    console.log('4、响应拦截后钩子函数: transformResponseHook')
    return res
  },
  requestInterceptor(config, options) {
    // 请求拦截
    console.log('2、请求拦截: requestInterceptor')
    return config
  },
  resqonseInterceptors(res) {
    // 响应拦截
    console.log('3、响应拦截: resqonseInterceptors')
    return res
  },
}

export const customDefaultOptins: CustomResqusetOptions | CreateAxiosConfig = {
  transform, // 传入 transform
  joinTime: true
}

export function createAxios(opt?: CreateAxiosConfig) {
  return new Aixos(objectDeepMerge(customDefaultOptins, opt || {})) // 合并配置
}

export const defHttp = createAxios({})

4、utils目录下创建index.ts,编写objectDeepMerge函数

// utils/index.ts
import { isObject } from 'lodash-es'

export function objectDeepMerge<T = any>(src: any = {}, target: any = {}): T {
  let key: string
  for (key in target) {
    src[key] = isObject(src[key]) ? objectDeepMerge(src[key], target[key]) : (src[key] = target[key])
  }
  return src
}

报错ENOTSUP: operation not supported on socket, read

请求的开发服务器接口返回 500 导致

vue-router

1、安装vue-routervue-router4 还没有转为正式版,所以需要加 next

# yarn & npm
yarn add vue-router@next
npm install --save vue-router@next

2、测试,src目录下新建router文件夹并创建index.ts

// router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router'

const mainRoutes: RouteRecordRaw[] = [
{
 name: 'Home',
 path: '/home',
 component: () => import('@/views/Home.vue'),
 meta: {
   title: 'home'
 }
}
]

export const router = createRouter({
history: createWebHashHistory(),
routes: mainRoutes
})

export function setupRouter(app: App<Element>) {
app.use(router)
}


// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { router, setupRouter } from './router'

async function bootstrap() {

const app = createApp(App)

// 添加其他逻辑

setupRouter(app)

await router.isReady()

app.mount('#app', true)

}

void bootstrap()

vuex

1、添加vuex

# yarn & npm
yarn add vuex@next
npm install --save vuex@next

2、测试,src目录下新建store

// store.ts
import type { App } from 'vue'
import { createStore } from 'vuex'

export const store = createStore({
state: {
 sidebarOpen: false
},
mutations: {
 SET_SIDEBAR(state: any) {
   state.sidebarOpen = !state.sidebarOpen
   localStorage.setItem('SIDEBAR_OPEN', state.sidebarOpen.toString())
 },
 GET_SIDEBAR(state: any) {
   const rSidebarOpen = localStorage.getItem('SIDEBAR_OPEN')
   if (rSidebarOpen) {
     state.sidebarOpen = rSidebarOpen === 'true' ? true : false
   }
 }
},
actions: {
 set({ commit }) {
   commit('SET_SIDEBAR')
 },
 get({ commit }) {
   commit('GET_SIDEBAR')
 }
}
})

export function setupStore(app: App<Element>) {
app.use(store)
}
// Home.vue
<template>
<h2>{{ store.state.sidebarOpen }}</h2>
<button style='width: 30pxheight: 20px' @click='store.dispatch('set')'></button>
</template>

<script setup lang='ts'>
import { useStore } from 'vuex'
const store = useStore()

</script>

less

1、安装lessvite原生已经支持了css预处理,不需要安装less-loader

# yarn & npm
yarn add less -D
npm install --save-dev less 

2、测试

// 全局引入
// styles/index.less
@import 'variables.less' // 引入全局变量

h2 {
background-color: red
}
// mian.ts
import '@styles/index.less' // 全局引入

sass

1、安装sassvite原生已经支持了css预处理,不需要安装sass-loader

# yarn & npm
yarn add sass -D
npm install --save-dev sass

2、测试

// styles/index.scss
@import 'variables.sass'  // 引入全局变量

h2 {
background-color: red
}
// mian.ts
import '@styles/index.scss' // 全局引入

stylus

1、安装stylusvite原生已经支持了css预处理,不需要安装stylus-loader

# yarn & npm
yarn add stylus -D
npm install --save-dev stylus

2、测试

// styles/index.styl
@import 'variables.styl'  // 引入全局变量

h2 {
background-color: red
}
// mian.ts
import '@styles/index.styl' // 全局引入

mock

1、安装vite-plugin-mockmockjs

# yarn && npm
yarn add -D vite-plugin-mock mockjs
npm install --save-dev vite-plugin-mock mockjs

2、vite配置文件vite-config.ts导入并配置插件

// vite-config.ts
import { defineConfig } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'

export default defineConfig({
  plugins: [
	...,
    viteMockServe({
      ignore: /^\_/,  // 忽略 _ 文件
      mockPath: 'mock', // mock文件夹
      supportTs: true,
    })
  ],
  ...,
})

3、编写mock

// mock/index.ts
import { MockMethod } from 'vite-plugin-mock'

export default [
  {
    url: '/',
    method: 'get',
    response: () => {
      return {
        code: 20000,
        msg: 'success',
        'data': ['Hello Mock'],
      })
    },
  },
] as MockMethod[]


4、测试一下

// api/home.ts
import { defHttp } from '@/utils/http'

enum API {
 HOME = '/home'
}

export function getHome() {
 return defHttp.get({ url: API.HOME })
}
// view/Home.vue
<template>
 <h2>{{ msg }}</h2>
</template>

<script setup lang='ts'>
import { getHome } from '@/api/home'
import { ref } from '@vue/reactivity'

const msg = ref(null)

getHome().then(res => {
 home.value = res.data.data
})

</script>
// App.vue

<template>
 <HomeVue></HomeVue>
</template>

<script setup lang='ts'>
import HomeVue from './views/Home.vue'
 
</script>

环境变量

1、项目目录下新建环境变量的文件 .env.development.env.production

// .env.development
VITE_APP_ENV = 'development'

// .env.production
VITE_APP_ENV = 'production'

2、修改配置 package.json

//package.json
...,
'scripts': {
  'dev': 'cross-env NODE_ENV=development vite',
  'build': 'vite build',
  'build:dev': 'vite build --mode development',
  'build:pro': 'vite build --mode production'
},
...,

整合vant组件库

安装依赖

yarn add vant@next
npm install --save vant@next

vite 版本不需要配置组件的按需加载,因为 Vant 3 内部所有模块都是基于 ESM 编写的,天然具备按需引入的能力,但是样式必须全部引入。

plugins目录下新建 vant.ts

import type { App } from 'vue'
import Vant from 'vant'
import 'vant/lib/index.css'

export function setupVant (app:App<Element>) {
  app.use(Vant)
}

修改 main.ts

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { router, setupRouter } from './router'
import { setupStore } from './store'
import { setupVant } from 'plugins/vant'

async function bootstrap() {
  const app = createApp(App)
  // 挂载stroe
  setupStore(app)
  // 添加其他逻辑
  setupVant(app)
  // 挂载router
  setupRouter(app)
  await router.isReady()
  app.mount('#app', true)
}

void bootstrap()

整合插件eslint,prettier 规范代码

安装依赖

yarn add babel-eslint -D
yarn add @vue/eslint-config-prettier -D
yarn add eslint -D
yarn add eslint-define-config -D
yarn add eslint-plugin-prettier -D
yarn add eslint-plugin-vue -D
yarn add prettier -D

根目录 下新建 .eslintrc.js

//.eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: ['plugin:vue/vue3-essential', 'eslint:recommended'],
  parserOptions: {
    parser: 'babel-eslint',
  },
  rules: {
    //在此处写规则
    'no-unused-vars': 0, // 定义未使用的变量
  },
}

根目录 下新建 .prettierrc.json

// .prettierrc.json
{
  // 此处填写规则
  'singleQuote': true,// 单引号
  'seme': true,// 分号
  'tabWidth': 2,// 缩进
  'TrailingCooma': 'all',// 尾部元素有逗号
  'bracketSpacing': true,// 对象中的空格
}

再结合 vscode 的保存自动格式化

 // .vscode/settings.json
'editor.formatOnSave': true,// 保存时格式化
'files.autoSave': 'onFocusChange', // 失去焦点时保存
'editor.codeActionsOnSave': {
  'source.fixAll.eslint': true
},
'eslint.validate': [
  'javascript',
  'javascriptreact',
  'typescript'
],  

配置GZIP压缩

安装依赖

yarn add vite-plugin-compression -D
复制代码

修改 vite.config.js

// vite.config.js

import viteCompression from 'vite-plugin-compression'
plugins:[
  ...,
  viteCompression({
      verbose: true,
      disable: false,
      threshold: 10240,
      algorithm: 'gzip',
      ext: '.gz'
  })
]

部署发布

执行对应命令即可

'dev': 'cross-env NODE_ENV=development vite',
'serve': 'vite preview',
'build:dev': 'vite build --mode development',
'build:pro': 'vite build --mode production'

项目地址

github.com/frezs/vite-…