目录

Vue3+Vite+TS 快速搭建一套实用的脚手架

技术栈

  • 开发工具:VSCode

  • 代码管理:Git

  • 前端框架:Vue3

  • 构建工具:Vite

  • 路由:vue-router 4x

  • 状态管理:vuex 4x

  • AJAX:axios

  • UI库:element-plus

  • 数据模拟:mockjs

  • 代码规范:eslint

  • 代码格式化:Prettier

  • css预处理:sass

开始构建

1. 初始化项目

[ 安装yarn ]

1
npm i yarn -g

安装vite:

1
2
3
npm init vite@latest [ProjectName]
or
yarn create vite [ProjectName]

安装完成后vite会引导我们创建一个项目,输入项目名称,package名称,然后选择项目使用的框架,这里有多个选项,选择Vue:

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106112928088.png

之后提示选择vue还是vue-ts,这里我们选择vue-ts(如果不用ts就直接选vue)

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106112952222.png

项目创建成功,打开项目并初始化:

1
2
3
cd vite-cloud-admin
npm install
[or yarn]

成功后运行项目:

1
2
npm run dev
[or yarn dev]

一个Vue3+Vite+TS的项目就创建成功了:

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106113048690.png

配置所需依赖(用于处理别名不生效问题):

npm install @types/node --D
[or yarn add @types/node --D]

修改vite.config.ts配置文件代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  base: './',	//不加打包后白屏
  server: {
    //解决“vite use `--host` to expose”
    host: '0.0.0.0',	
    // port: 8080,      
    open: true,
    proxy: {          // 代理
      '/api': {
        target: '真实接口服务地址',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')     // 注意代理地址的重写
      },
      // 可配置多个... 
    }
  },
  resolve:{   
    //别名配置,引用src路径下的东西可以通过@如:import Layout from '@/layout/index.vue'
    alias:[   
      {
        find:'@',
        replacement:resolve(__dirname,'src') 
      }
    ]
  }
})

配置tsconfig.json文件

  1. 这一步是用来解决 “报错:找不到模块“xxx”或其相应的类型声明” 的
  2. 配置 “baseUrl 和 paths” 项
  3. paths 里的内容根据别名来进行相关配置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "jsx": "preserve",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["ESNext", "DOM"],
    "skipLibCheck": true,
    "noEmit": true,
    "baseUrl": ".",  						// 这块
    "paths": {								// 这块
      "@/*":["src/*"],
      // "components":["src/components/*"],
      // "pinia/*":["src/pinia/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

2. 代码校验

首先安装eslint:

1
2
npm i eslint
[or yarn add eslint]

初始化eslint

1
2
npx eslint --init
[or yarn eslint --init]

然后会问我们如何使用eslint,选择第三项,检查语法、发现问题并强制执行代码样式

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106113231307.png

什么样子的模块引入方式,这里选择第一项,import/export

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106113239656.png

然后问我们用什么框架,这里选择Vue.js

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106113256553.png

是否使用TS,YES

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106113308328.png

代码运行在哪里,选择浏览器

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106113317259.png

然后问我们使用什么代码格式,这里我们选择流行代码格式中的Standard

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20230211000520208.png

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106113326664.png

选择eslintrc文件的格式,这里选择JavaScript

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106113336174.png

立即初始化,YES

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106113345763.png

这样我们的eslint就安装完成了,不过由于vue3的语法规则和vue2不同,有些情况下我们的正常开发也会报错,所以需要在.eslintrc.cjs文件的rules里面添加如下配置:

1
2
3
4
5
6
7
8
9
  rules: {
    'vue/no-multiple-template-root': 0,
    'no-unused-vars': [
      'error',
      // we are only using this rule to check for unused arguments since TS
      // catches unused variables but not args.
      { varsIgnorePattern: '.*', args: 'none' }
    ]
  }

第一项是因为vue3允许template下面有多个标签,第二个是script setup标签下,定义的变量或方法如果未使用会报错,但其实这些方法和变量可以直接在template中使用的。

在overrides中可以指定具体目录下的规则

1
2
3
4
5
6
7
8
overrides: [
    {
        files: ["src/views/**/*.vue"],
        rules: {
        	"vue/multi-word-component-names": 0
        }
    }
],

“off” or 0 - 关闭(禁用)规则 “warn” or 1 - 将规则视为一个警告(并不会导致检查不通过) “error” or 2 - 将规则视为一个错误 (退出码为1,检查不通过)

修改配置文件后记得重启服务器 才会生效

3.代码格式化

安装prettier:

1
2
npm i prettier
[or yarn add prettier]

然后在根目录创建.prettierrc文件,配置如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "none",
  "printWidth": 100,
  "bracketSpacing": true,
  "jsxBracketSameLine": true,
  "useEditorConfig": true,
  "useTabs": false,
  "vueIndentScriptAndStyle": true,
  "arrowParens": "avoid",
  "htmlWhitespaceSensitivity": "ignore",
  "overrides": [
    {
      "files": ".prettierrc"
    }
  ]
}

配置完成后可以在vscode安装Prettier插件,实现保存自动格式化文件。 完成后保存文件发现报错了,这是因为Prettier格式化后的代码与eslint规范冲突,这里我们使用eslint-config-prettier这个插件解决这个问题,安装插件:

1
2
npm i eslint-config-prettier -D
[or yarn add eslint-config-prettier -D]

安装完成后还需要在.eslintrc.cjs文件中加上一段配置才能生效,这里就直接把整个.eslintrc.cjs拷上来了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: [
    'plugin:vue/essential',
    'standard',
    'prettier' // 就是这段配置
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    parser: '@typescript-eslint/parser',
    sourceType: 'module'
  },
  plugins: [
    'vue',
    '@typescript-eslint'
  ],
  rules: {
    'vue/no-multiple-template-root': 0,
    'no-unused-vars': [
      'error',
      // we are only using this rule to check for unused arguments since TS
      // catches unused variables but not args.
      { varsIgnorePattern: '.*', args: 'none' }
    ]
  }
}

至此,代码格式化及校验就完成了。

4.配置路由

直接安装vue-router

1
2
npm install vue-router@4
[or yarn add vue-router@4]

在src文件夹下新建router目录,并在目录下新建index.ts文件,并做如下配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// index.ts
import { createRouter, createWebHashHistory } from 'vue-router'
import HelloWorld from '../components/HelloWorld.vue'

const routes = [{ path: '/', component: HelloWorld }]

export default createRouter({
  history: createWebHashHistory(),
  routes
})

在main.ts中引入该文件:

1
2
3
4
5
6
7
8
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

在App.vue里添加router-view标签:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// App.vue
<template>
  <router-view></router-view>
</template>

<script setup lang="ts"></script>

<style>
  #app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
  }
</style>

启动下,看下是否生效:

1
npm run dev

因为我们在App.vue中去掉了HelloWorld组件的引入,改用router的形式,如果界面还能显示出来,就说明配置成功了。

5.配置状态管理器

vuex

首先安装vuex,默认的还是3x版本,vue3是不支持的,这里需要这样安装:

1
2
npm install vuex@next -S
[yarn add vuex@next -S]

安装完成后在src文件夹下新建store文件夹,然后新建index.ts文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// store/index.ts
import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      count: 0
    }
  },
  mutations: {
    increment(state: any) {
      state.count++
    }
  }
})

export default store

在main.ts文件中引入store:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')

引入完成后,我们还需要测试下有没有生效,改写下HelloWorld组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!--HelloWorld.vue-->
<template>
  <button type="button" @click="onClick">count is: {{ count }}</button>
</template>

<script setup lang="ts">
  import { computed } from 'vue'
  import { useStore } from 'vuex'

  const store = useStore()
  const count = computed(() => {
    return store.state.count
  })
  const onClick = () => {
    store.commit('increment')
  }
</script>

HelloWorld中引入了store,并且将store中的count挂载到页面上,点击按钮向store发送事件完成count的累加,实测没有问题,vuex安装成功。

pinia

安装

1
2
npm install pinia
[or yarn ad]

安装完之后,在src下建立pinia文件夹,新建index.ts文件

1
2
3
import { createPinia } from 'pinia'
const store = createPinia()
export default store

在main.js中引入

1
2
3
4
5
6
7
import { createApp } from 'vue'
import App from './App.vue'
import pinia from './pinia'

const app = createApp(App)
app.use(pinia)
app.mount('#app')

使用,创建一个user.ts文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { defineStore } from 'pinia'

// useStore 可以是 useUser、useCart 之类的任何东西
// 第一个参数是应用程序中 store 的唯一 id
export const useStore = defineStore('main', {
  // 推荐使用 完整类型推断的箭头函数
    state: () => {
        return {
            // 所有这些属性都将自动推断其类型
            counter: 0,
            name: "Eduardo",
            isAdmin: true,
        };
    },
})

使用store,我们正在 定义 一个 store,因为在 setup() 中调用 useStore() 之前不会创建 store:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { useStore } from '@/pinia/user'
export default {
  setup() {
    const store = useStore()

    return {
      // 您可以返回整个 store 实例以在模板中使用它
      store,
    }
  },
}

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { storeToRefs } from 'pinia'

export default defineComponent({
  setup() {
    const store = useStore()
    // `name` 和 `doubleCount` 是响应式引用
    // 这也会为插件添加的属性创建引用
    // 但跳过任何 action 或 非响应式(不是 ref/reactive)的属性
    const { name, doubleCount } = storeToRefs(store)

    return {
      name,
      doubleCount
    }
  },
})

state,大多数时候,state是store的核新部分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { defineStore } from 'pinia'

const useStore = defineStore('storeId', {
  // 推荐使用 完整类型推断的箭头函数
  state: () => {
    return {
      // 所有这些属性都将自动推断其类型
      counter: 0,
      name: 'Eduardo',
      isAdmin: true,
    }
  },
})

访问state

1
2
3
const store = useStore()

store.counter++

重置状态

可以通过调用 store 上的 $reset() 方法将状态 重置 到其初始值:

1
2
3
const store = useStore()

store.$reset()

改变状态

1
2
3
4
store.$patch({
  counter: store.counter + 1,
  name: 'Abalam',
})

Actions

Actions 相当于组件中的 methods 。 它们可以使用 defineStore() 中的 actions 属性定义,并且它们非常适合定义业务逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  actions: {
    increment() {
      this.counter++
    },
    randomizeCounter() {
      this.counter = Math.round(100 * Math.random())
    },
  },
})

访问其他 store 操作

要使用另一个 store ,您可以直接在操作内部使用它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    // ...
  }),
  actions: {
    async fetchUserPreferences(preferences) {
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})

模块化

我个人感觉pinia的模块化就是一个模块创建一个文件夹~~然后再引入那个文件夹就行了。

在router.js中使用

必须写在router.beforeEach里面。

1
2
3
4
5
6
7
8
router.beforeEach((to) => {
    const store = loginStore();
    if (to.path !== "/login") {
        if (!store.token) {
            return "/login";
        }
    }
});

持久化,pinia-plugin-persist插件 pinia-plugin-persistedstate`插件

1
2
npm i pinia-plugin-persist --save
[or yarn add pinia-plugin-persist --save]

index.ts中引入持久化插件

(默认存储在localStorage)

1
2
3
4
5
6
7
8
import { createPinia } from 'pinia'
// import piniaPluginpersist from 'pinia-plugin-persist'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const store = createPinia()
store.use(piniaPluginPersistedstate)

export default store

若要修改默认配置(这里修改保存在sessionStorage下)

1
2
3
4
5
6
7
8
9
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'

const store = createPinia()
store.use(createPersistedState({
    storage: sessionStorage
}))

export default store

使用,在store中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const loginStore = defineStore("main", {
 
    //持久化
    persist: {
        enabled: true,
        // 自定义持久化参数
        strategies: [
            {
                // 自定义key
                key: "token",
                // 自定义存储方式,默认sessionStorage
                storage: localStorage,
                // 指定要持久化的数据,默认所有 state 都会进行缓存,可以通过 paths 指定要持久化的字段,其他的则不会进行持久化。
                paths: ["token"],
            },
            {
                key: "menulist",
                storage: localStorage,
                paths: ["menulist"],
            },
        ],
    },
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export const loginStore = defineStore("main", {
 
    //持久化
    persist: {
        storage: sessionStorage
	},
    // or
    // persist: true
    // or
    // persist: [
    //    {
    //     storage: sessionStorage
	// 	} 
    // ]
);

对保存的数据进行加密,SecureLS默认保存在localStorage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import { createPinia } from 'pinia'
import { type StorageLike, createPersistedState } from 'pinia-plugin-persistedstate'
import SecureLS from 'secure-ls';

const ls = new SecureLS({encodingType: 'rc4', isCompression: false, encryptionSecret: 's3cr3tPa$$w0rd@123' })
const customStorage: StorageLike = {
    setItem(key: string, value: string){
        ls.set(key, value)
    },
    getItem(key: string): string | null {
        return ls.get(key)
    }
}
const store = createPinia()
store.use(createPersistedState({
    storage: customStorage
}))

export default store

修改为保存在sessionStorage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { createPinia } from 'pinia'
import { type StorageLike, createPersistedState } from 'pinia-plugin-persistedstate'
import SecureLS from 'secure-ls';
class CustomSecureLs extends SecureLS {
    ls: Storage;
    constructor(config?: { isCompression?: boolean, encodingType?: string, encryptionSecret?: string , encryptionNamespace?: string }){
        super(config)
        this.ls = sessionStorage
    }
}
const ls = new CustomSecureLs({encodingType: 'rc4', isCompression: false, encryptionSecret: 's3cr3tPa$$w0rd@123' })
const customStorage: StorageLike = {
    setItem(key: string, value: string){
        ls.set(key, value)
    },
    getItem(key: string): string | null {
        return ls.get(key)
    }
}
const store = createPinia()
store.use(createPersistedState({
    storage: customStorage
}))

export default store

6.element-plus

element-ui的vue3版本,首先安装它:

1
2
npm install element-plus --save
[yarn add element-plus --save]

在main.ts中作出如下配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementPlus from 'element-plus' // 引入element-plus
import 'element-plus/dist/index.css' // 引入element-plus的样式

const app = createApp(App)
app.use(router)
app.use(store)
app.use(ElementPlus) // use element-plus 
app.mount('#app')

然后就可以使用element-plus的组件了,比较多,使用的时候直接参照官方文档就行。

7.封装axios

安装axios:

1
2
npm i axios
[or yarn add axios]

在src文件夹下新建utils文件夹,然后在其下创建request.ts文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import axios from 'axios'
import { ElMessage } from 'element-plus'

const instance = axios.create({
  baseURL: '',
  timeout: 5000
})

instance.interceptors.request.use(
  config => {
    return config
  },
  error => {
    console.log(error)
    return Promise.reject(error)
  }
)

instance.interceptors.response.use(
  response => {
    const res = response.data
    if (res.status !== 200) {
      ElMessage({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res.data
    }
  },
  error => {
    console.log('err' + error) // for debug
    ElMessage({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default instance

src下新建api文件夹,创建一个user.ts文件,并创建一个登录的请求:

1
2
3
4
5
6
7
8
9
import request from '../utils/request'

export function login(data: any) {
  return request({
    url: '/user/login',
    method: 'post',
    data
  })
}

然后在HelloWorld组件中onMounted中调用login接口:

1
2
3
4
5
6
import { onMounted } from 'vue'
onMounted(() => {
  login({ account: 'admin', password: '123456' }).then(res => {
    console.log(res)
  })
})

当然,现在还调不通,所以我们先配置下mock。

8.mockjs

安装mockjs:

1
2
npm i mockjs -D
[yarn add mockjs -D]

在根目录新建mock文件夹,并新建index.ts文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// index.ts
import Mock from 'mockjs'

// 设置拦截ajax请求的相应时间
Mock.setup({
  timeout: '200-600'
})

Mock.mock('/user/login', 'post', (params: any) => {
  return {
    data: { token: '123' },
    status: 200,
    message: 'success'
  }
})

export default {}

简单设置一个login接口,让我们能够通过axios调通,然后在main.ts中引入mock:

1
import '../mock'

启动项目:

https://cdn.jsdelivr.net/gh/QiMington/picbed/image-20221106114420159.png

有返回值,搞定

9.css预处理

vite是支持sass(scss)的,但是还是需要我们先安装一下,不然会报错

1
2
npm install sass --save-dev
[or yarn add sass --save-dev]