如何使用Vue3 +Vite + TypeScript + ElementPlus + Pinia搭建一个士力架

2,749 阅读7分钟

这篇文章就教大家如何使用vue3+vite+ts+element-plus+pinia搭建一个项目,步骤详细,献给不爱看文档的诸位,希望这篇文章能帮到你们

1667207362554.jpg

环境

  • node环境,vite需要node版本>=12.0.0;node官网(nodejs.org/zh-cn/downl…),node环境的安装和配置就不做追叙了,自己看着整。
  • vue版本,我这边装的是@vue/cli 5.0.8 版本;同样的vue的安装不多做缀叙。

vite3新特性简单介绍

  1. 模板变更,使用vite创建的vue模板主题与vite官网保持一致,logo也从vue换成了vite,主题也支持亮色和暗色模式
  2. import.meta.glob APIimport.meta.globAPI可以动态的导入文件,在Vite3中允许import.meta.glob被重写,了解更多可以去翻阅官网
  3. 兼容性做了调整,最低支持node14.18+
  4. 优化,Vite3修复了400+issuse,减少了体积,冷启动得到优化
  5. WebSocket连接策略,Vite内置了一套更加完善的WebSocket连接策略,它能够自动满足更多场景的HMR需求
  6. cli的更新,现在执行vite3命令进行启动项目时,终端的界面展示和之前的不同,默认的端口号也从之前的3000变成了5173
  7. 修复一些已知的问题...

这里只是简单做个描述,更多更详细请自己去看官方文档,我们这篇文章重点不在这,而且别人总结的不一定就是最适合自己的也不一定就是正确的,那我们继续往下走搭建起来

搭建项目

使用命令行进行操作:

npm init vite@latest <project-name>
framework 选择 vue
variant 选择 vue-ts
cd <project-name>
npm i
npm run dev

或者使用yarn的

yarn create vite@latest <project-name>
cd <project-name>
yarn
yarn dev

这里我使用npm init vite@latest <项目名>的方式搭建项目

选择项目框架,这里我选择的vue 在这里插入图片描述

这里我选择的是typeScript 在这里插入图片描述

然后根据提示的步骤依次执行命令 在这里插入图片描述 然后就启动起来了,得到如下的页面 在这里插入图片描述

这样就可以写代码做需求了吗?当然还不行,你还需要下载引入项目需要的依赖vue-router,pinia,axios,element-plus等等的依赖,并且根据自己项目实际情况修改一下配置文件。

配置文件

我们按照上面步骤创建完项目会发现根目录下有个vite.config.ts文件,其实就相当于@vue-cli项目中的vue.config.js文件。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// @ts-ignore
import path from 'path';

const pathResolve = (pathStr:String) => {
  // @ts-ignore
  return path.resolve(__dirname, pathStr);
}

// https://vitejs.dev/config/
export default defineConfig({
  base: './',
  plugins: [vue()],
  server: {
    host: '0.0.0.0', // 本地启动的地址
    port: 3000, // 服务端口号
    proxy: { // 代理配置
      '/api/': {
        target: 'http:xxx.xxx.xx.x',
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    },
  },
  resolve: {
    alias: {
      '@': pathResolve('./src'),
    }
  },
  build: {
    sourcemap: false,
    minify: 'esbuild', // 构建时的压缩方式
    rollupOptions: {
      output: {
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: '[ext]/[name]-[hash].[ext]',
      }
    }
  }
})

在根目录下会有一个tsconfig.json文件,tsconfig.json文件是 TypeScript 编译器的配置文件,TypeScript 编译器可以根据它的规则来对代码进行编译。

{
  "compilerOptions": {
    "target": "ESNext", // 指定ECMAScript的目标版本:'ES3'(默认),'ES5','ES2015','ES2016','ES2017','ES2018','ES2019','ES2020'或者'ESNEXT'
    "useDefineForClassFields": true, // 此标志用作迁移到即将推出的类字段标准版本的一部分,默认:false
    "module": "ESNext", // 指定模块代码生成:'none','commonjs','amd','system','umd','es2015','es2020',或'ESNext'
    "moduleResolution": "Node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入,node或classic
    "strict": true, // 是否启用所有严格的类型检查
    "jsx": "preserve", // 指定 jsx 格式
    "sourceMap": true, // 生成目标文件的sourceMap文件
    "resolveJsonModule": true, // 是否解析 JSON 模块,默认:false
    "isolatedModules": true, // 是否将每个文件转换为单独的模块,默认:false
    "esModuleInterop": true, // 通过为所有导入创建名称空间对象,实现CommonJS和ES模块之间的互操作性
    "allowJs": true, // 允许编译器编译JS,JSX文件
    "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
    "lib": ["ESNext", "DOM"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array"
    "skipLibCheck": true, // 跳过声明文件的类型检查
    // "types": ["element-plus/global"] // 加载的声明文件包
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], // 指定被编译文件所在目录,** 表示任意目录, * 表示任意文件
  "references": [{ "path": "./tsconfig.node.json" }] // 项目引用,是 TS 3.0 中的一项新功能,它允许将 TS 程序组织成更小的部分
}

安装Element Plus

# 选择一个你喜欢的包管理器

# NPM
$ npm install element-plus --save
# Yarn
$ yarn add element-plus
# pnpm
$ pnpm install element-plus

完整引入(不推荐)

在main.ts文件中引入

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
 
const app = createApp(App)
 
app.use(ElementPlus)
app.mount('#app')

按需引入(推荐)

element-plus官方贴心的推出了自动导入,我们需要安装unplugin-vue-componentsunplugin-auto-import这两款插件

# 选择一个你喜欢的包管理器

# NPM
$ npm install -D unplugin-vue-components unplugin-auto-import
# Yarn
$ yarn add -D unplugin-vue-components unplugin-auto-import
# pnpm
$ pnpm install unplugin-vue-components unplugin-auto-import

装好之后再回到vite.config.ts文件中,添加图中代码

在这里插入图片描述

知道你们不爱看图敲代码

import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
 
export default {
  plugins: [
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
}

这样我们就可以直接在项目中直接使用element-plus的组件了 在这里插入图片描述 在这里插入图片描述 element-plus更多的组件使用请参考Element Plus官方文档

需要注意的是element-plus的icon使用

# 选择一个你喜欢的包管理器

# NPM
$ npm install @element-plus/icons-vue
# Yarn
$ yarn add @element-plus/icons-vue
# pnpm
$ pnpm install @element-plus/icons-vue

在页面中使用时需要先引入 在这里插入图片描述

安装 Pinia

在安装和使用 Pinia 之前我们先简单的了解一下什么Pinia

Pinia 是什么?

Pinia 最初是在 2019 年 11 月左右重新设计使用 Composition API 。从那时起,最初的原则仍然相同,但 Pinia对 Vue 2 和 Vue 3 都有效,并且不需要您使用组合 API。Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。实际上就是 Vue3 中的 Vuex,它是 Vuex 的升级版,尤雨溪用了都说好。

Pinia 的优势有哪些

  • 它能让你像定义组件一样的去定义 Store,API 旨在让您编写组织良好的 Store 。
  • 有着比 Vuex 更好的的 TypeScript 支持。
  • 与 Vue devtools 挂钩,为您提供良好的 Vue 2 和 Vue 3 开发体验,即使是Vue 2 的老项目也能使用。
  • 体积极小,只有1kb,让您甚至忘记它的存在。
  • 模块化设计支持多个 Store ,每一个都是独立诞生,但最终都是相互联系。
  • 可以通过安装插件来拓展自身的功能。
  • 摒弃了 Mutations 的操作,并且 Actions 支持了同步和异步,简化了状态管理库的使用。
  • 支持服务端渲染。
  • 更多更详细参考 Pinia官方文档 或者 Pinia中文文档 (中文文档部分翻译难以理解,建议阅读英文文档)

Pinia 的安装

# 选择一个你喜欢的包管理器

# NPM
$ npm install pinia --save
# Yarn
$ yarn add pinia
# pnpm
$ pnpm install pinia

在这里插入图片描述

  • 项目中引入 Pinia

非常简单有手就行,在 main.ts 文件中引入就行了

// # main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './style.css'
import App from './App.vue'

const app = createApp(App);
const pinia = createPinia(); // 创建Pinia实例

// 将Pinia实例挂载到Vue根实例上
app.use(pinia)
app.mount('#app')

Pinia 的使用

首先在 src 文件夹下新建一个 stores 文件夹,在stores文件夹下再新建一个 index.ts 文件,跟使用 Vuex时的结构一样,然后编写 index.ts 里的代码

  1. 先引入 Pinia
import { defineStore} from 'pinia'
  1. 导出
export const useStore = defineStore('store',{
  state:() => {
    return {}
  },
  getters:{},
  actions:{}
})

defineStore() 方法可以接收两个参数,第一个参数类似于唯一值的id,给我们的容器取一个名字,第二参数是一个配置对象,里面有三个属性 state 、getters 和 actions 在 Vuex 中 state 是一个对象,但在 Pinia 中 state 必须是个箭头函数 return 一个对象,用来存储全局的状态数据 getters 类似于计算属性,有缓存的功能,可以帮助我们修士一些值 actions 类似于 methods,根据不同的需求和业务可以做一些同步或者异步的操作,用来修改全局的状态数据

  1. 定义一个 state

在 state 里面定义一个全局数据,在项目中的每一个页面或组件都可以通过 Pinia 方法获取到这个数据

state:() => {
    return {
        name: '海马',
    }
},
  1. 在页面中读取 Pinia 的数据

我们去 HelloWorld 组件中引入 useStore ,然后得到 useStore 的实例在页面中直接使用

在这里插入图片描述 在这里插入图片描述

  1. 操作 Pinia 里的数据

修改 Pinia 里的数据有多种方法,根据自己实际使用场景选择不同方法

一、在页面或者组件中直接修改 State(不推荐)

先在 state 中新增一个 age 属性

state:() => {
    return {
        name: '海马',
        age: 18,
    }
},

然后在页面中去操作它

<script setup lang="ts">
import { useStore } from '../stores/index'

defineProps<{ msg: string }>()

const store = useStore();

const handleClick = () => {
  store.age ++
}
</script>

<template>
  <h1>{{ msg }}</h1>

  <div class="card">
    <p>Hello everybody, my name is {{ store.name }}.</p> 
    <p>I'm {{ store.age }} years old.</p>
    <el-button type="primary" @click="handleClick">happlyBirthday</el-button>
  </div>
</template>

可以看到我们在页面上点击按钮的时候 age 也会变化

在这里插入图片描述

二、使用 $patch 方法修改 State

使用 $patch 方法时可以直接传入对象去修改 state 的值,也可以传入一个函数,在函数中书写代码逻辑

// 直接上代码
const handleClick = () => {
  // 我们可以直接通过 $patch 方式修改 state 的值
  // 但是如果要修改复杂数据时需要将整个数据格式都再写一遍,不利于开发效率,不推荐
  // let age = store.age;
  // age++
  // store.$patch({
  //   name: '青龙',
  //   age: age
  // })

  // 还可以传入一个函数
  // 这样我们可以在函数中书写逻辑,推荐使用
  store.$patch((state) => {
    state.age++
    if (state.age == 30) state.name = '中年海马'
  })
}

三、使用 actions 修改 State (推荐)

我们可以在 store 中的 actions 中定义好函数,在页面实例去调用 actions 中的函数方法就可以了

// stores/index.ts
  actions:{
    happlyBirthday() {
        this.age ++
    }
  }
// HellowWorld.vue
<script setup lang="ts">
import { useStore } from '../stores/index'

const store = useStore();

const handleClick = () => {
  store.happlyBirthday();
}
</script>

<template>
  <div>
    <p>Hello everybody, my name is {{ store.name }}.</p> 
    <p>I'm {{ store.age }} years old.</p>
    <el-button type="primary" @click="handleClick">happlyBirthday</el-button>
  </div>
</template>

注意

请注意,store 是一个用 reactive 包裹的对象,这意味着不需要在 getter 之后写.value,但是,就像 setup 中的 props 一样,我们不能对其进行解构

const { name, age } = store;

<p>Hello everybody, my name is {{ name }}.</p>  // 一直是 海马
<p>I'm {{ age }} years old.</p> // 永远 18 岁

为了从 Store 中提取属性同时保持其响应式,官方提供了 storeToRefs()方法,它将为任何响应式属性创建 refs。 当您仅使用 store 中的状态但不调用任何操作时,这很有用

// 直接引入
import { storeToRefs } from 'pinia'

// 使用
const { name, age } = storeToRefs(store);

四、getters 的使用

Getter 类似于计算属性,相当于 State 的计算值

  getters:{
    // 类型推断自动推出返回值为 number
    nominalAge(state) {
        return state.age + 1
    },
    // 明确定义返回类型
    realName(): string {
        return `${this.name}·胡`
    },
    // 还可以互相调用
    description(): string {
        return `my name is ${this.realName}, ${this.nominalAge} years old`
    }
  },

// HellowWorld.vue
// 直接在实例上使用
    <p>Real name is {{ store.realName }}</p>
    <p>Nominal year is {{ store.nominalAge }}</p>
    <p>{{ store.description }}</p>

在这里插入图片描述

完整代码

// HelloWorld.vue
<script setup lang="ts">
import { useStore } from '../stores/index'
import { storeToRefs } from 'pinia'

defineProps<{ msg: string }>()

const store = useStore();

// × 这样会破坏响应式,和从 props 中结构一样
// const { name, age } = store;

const { name, age } = storeToRefs(store);

// 直接修改
const handleClickChange = () => {
  store.age ++
}

// 可以通过将其 $state 属性设置为新对象来替换 Store 的整个状态
const handleClickState = () => {
  let age = store.age;
  age++
  store.$state = {
    name: '海马',
    age,
  }
}

// $patch 函数
const handleClickPatchChange = () => {
  // 我们可以直接通过 $patch 方式修改 state 的值,但是任何集合修改(例如,从数组中推送、删除、拼接元素)
  // 都需要您创建一个新集合,不利于开发效率,不推荐
  let age = store.age;
  age++
  store.$patch({
    name: '青龙',
    age: age
  })
}

const handleClickPatchFn = () => {
  // 可以传入一个函数的形式来批量修改集合内部分对象的情况
  store.$patch((state) => {
    state.age++
    if (state.age >= 30) state.name = '中年海马'
  })
}

// 在 actions 中定义好函数,页面实例中直接调用
const handleClickAction = () => {
  let age = store.age
  age ++
  store.happlyBirthday(age);
}

// 可以通过调用 store 上的 $reset() 方法将状态 重置 到其初始值
const reset = () => {
  store.$reset()
}
</script>

<template>
  <h1>{{ msg }}</h1>

  <div>
    <p>Hello everybody, my name is {{ store.name }}.</p>
    <p>I'm {{ store.age }} years old.</p>
    <p>Real name is {{ store.realName }}</p>
    <p>Nominal year is {{ store.nominalAge }}</p>
    <p>{{ store.description }}</p>

    <p>Hello everybody, my name is {{ name }}.</p> 
    <p>I'm {{ age }} years old.</p>
    <el-button type="primary" @click="handleClickChange">handleClickChange</el-button>
    <p></p>
    <el-button type="primary" @click="handleClickState">handleClickState</el-button>
    <p></p>
    <el-button type="primary" @click="handleClickPatchChange">handleClickPatchChange</el-button>
    <p></p>
    <el-button type="primary" @click="handleClickPatchFn">handleClickPatchFn</el-button>
    <p></p>
    <el-button type="primary" @click="handleClickAction">handleClickAction</el-button>
    <p></p>
    <el-button type="danger" @click="reset">resetData</el-button>
  </div>
</template>


// ../stores/index.ts
import { defineStore} from 'pinia'

export const useStore = defineStore('store',{
  state:() => {
    return {
        name: '海马',
        age: 18,
    }
  },
  getters:{
	// 类型推断自动推出返回值为 number
    nominalAge(state) {
        return state.age + 1
    },
    // 明确定义返回类型
    realName(): string {
        return `${this.name}·胡`
    },
    // 还可以互相调用
    description(): string {
        return `my name is ${this.realName}, ${this.nominalAge} years old`
    }
  },
  actions:{
    happlyBirthday(age: number) {
        this.age = age;
    }
  }
})