vue3 + vite + pinia + ts + element-ui Plus

1,749 阅读9分钟

vue3 + vite + pinia + ts + element-ui Plus 基础环境搭建

一、创建项目

1.直接创建项目
使用 NPM: 
   $ npm init vite@latest 
使用 Yarn: 
   $ yarn create vite 
使用 PNPM: 
   $ pnpm create vite 然后按照提示操作即可!(选择vue,vue-ts)

2.使用模板创建项目

通过附加的命令行选项直接指定项目名称和你想要使用的模板例如,要构建一个 Vite + Vue 项目,运行:

使用 npm 6.x: 
     npm create vite@latest my-vue-app --template vue 
使用 npm 7+, extra double-dash is needed: 
     npm create vite@latest my-vue-app -- --template vue 
使用 yarn: 
     yarn create vite my-vue-app --template vue 
使用 pnpm: 
     pnpm create vite my-vue-app --template vue

然后npm i 或者 yarn install 运行yarn dev 看看浏览器运行成了没,第一步就大功告成了(当然作者这边全程使用的是yarn)

二、vite配置别名和环境变量的配置

1.配置别名

使用编辑器VScode打开刚刚搭建好的项目 进入配置文件 vite.config.ts 配置别名后的vite.config.ts:

import { defineConfig } from 'vite' 
import vue from '@vitejs/plugin-vue' 
import path from 'path' 
const resolve = (dir: string) => path.join(__dirname, dir) ​ 
export default defineConfig({ 
    plugins: [vue()],
    resolve: { 
       alias: { '@': resolve('src'), } 
    }
})

此时 TS 可能有这个错误提示:找不到模块“path”或其相应的类型声明 解决方法:

yarn add @types/node --save-dev或者npm install @types/node --save-dev

还需要在tsconfig.json的compilerOptions配置对应paths 配置方法如下:

"baseUrl": ".",
"paths": {      
    "@/*": [        "src/*"     ],
    "comps/*": [        "src/components/*"     ],
    "views/*": [        "src/views/*"     ],
    "store/*": [        "src/store/*"     ]
},

2.环境变量的配置

vite 提供了两种模式:具有开发服务器的开发模式(development)和生产模式(production)

项目根目录新建:.env.development :

NODE_ENV=production ​

VITE_APP_WEB_URL= 'YOUR WEB URL'

项目根目录新建:.env.production :

NODE_ENV=production 

VITE_APP_WEB_URL= 'YOUR WEB URL'

配置 package.json:

打包区分开发环境和生产环境

"build:dev": "vite build --mode development",
"build:pro": "vite build --mode production",

三、配置跨域代理

在vite.config.ts中

export default defineConfig({    
    plugins: [vue()],    
    resolve: {       
    alias: {            
        '@': resolve('src'),            
        comps: resolve('src/components'),
        apis: resolve('src/apis'),
        views: resolve('src/views'),
        utils: resolve('src/utils'),
        routes: resolve('src/routes'),
        styles: resolve('src/styles')
    }
},
server: {     // 配置前端服务地址和端口        
        //服务器主机名        
        host: '',
        //端口号
        port: 3088,
        //设为 true 时若端口已被占用则会直接退出,而不是尝试下一个可用端口
        strictPort: false,
        //服务器启动时自动在浏览器中打开应用程序,当此值为字符串时,会被用作 URL 的路径名
        open: false,
        //自定义代理规则
        proxy: {
            // 选项写法            
            '/api': {
                target: '',
                changeOrigin: true,
                rewrite: path => path.replace(/^/api/, '')
                }       
            }   
        } 
})

使用跨域代理:

用代理, 首先你得有一个标识, 告诉他你这个连接要用代理. 不然的话, 可能你的 html, css, js这些静态资源都跑去代理. 所以我们一般只有接口用代理, 静态文件用本地.‘/api’: {}, 就是告诉node, 我接口只有是’/api’开头的才用代理.所以你的接口就要这么写 /api/xx/xx. 最后代理的路径就是 xxx.xx.com/api/xx/xx.可是不对啊, 我正确的接口路径里面没有/api啊. 所以就需要 rewrite,把’/api’去掉, 这样既能有正确标识, 又能在请求接口的时候去掉api.

四、添加 css 预处理器 sass

安装预处理器

yarn add sass sass-loader或者npm install -D sass sass-loader 

然后在 src/assets 下新增 style 文件夹,用于存放全局样式文件,例如新建minx.scss

$default-color: #fff;

五、约束代码风格

TypeScirpt 官方决定全面采用 ESLint 作为代码检查的工具,并创建了一个新项目 typescript-eslint,提供了 TypeScript 文件的解析器 @typescript-eslint/parser 和相关的配置选项 @typescript-eslint/eslint-plugin 等

1.Eslint支持

# eslint 安装 

yarn add eslint --dev 
   
# eslint 插件安装 

yarn add eslint-plugin-vue --dev 
​  
yarn add @typescript-eslint/eslint-plugin --dev 
​
yarn add eslint-plugin-prettier --dev 

# typescript parser 

yarn add @typescript-eslint/parser --dev 
​ 
直接:npm i typescript eslint eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin -D

注意: 如果 eslint 安装报错:

可以尝试运行以下命令:

yarn config set ignore-engines true

项目下新建 .eslintrc.js配置 eslint 校验规则:

// 需要安装依赖: npm i eslint-define-config 
const { defineConfig } = require('eslint-define-config')
module.exports = defineConfig({    
    root: true,    
    /* 指定如何解析语法。*/
    parser: 'vue-eslint-parser',
    /* 优先级低于parse的语法解析配置 */
    parserOptions: {
        parser: '@typescript-eslint/parser',   
    },    
    // https://eslint.bootcss.com/docs/user-guide/configuring#specifying-globals
    globals: {        
        Nullable: true,
    },    
    extends: [        
        // add more generic rulesets here, such as:
        // 'eslint:recommended',        
        // 'plugin:vue/vue3-recommended',
        // 'plugin:vue/recommended' 
        // Use this if you are using Vue.js 2.x. ​
        'plugin:vue/vue3-recommended',        
        // 此条内容开启会导致 全局定义的 ts 类型报 no-undef 错误,因为
        // https://cn.eslint.org/docs/rules/
        'eslint:recommended',        
        'plugin:@typescript-eslint/recommended', 
        // typescript-eslint推荐规则,
        'prettier',        
        'plugin:prettier/recommended',   
    ],    
    rules: {
    // 'no-undef': 'off',
    // 禁止使用 var
    'no-var': 'error',
    semi: 'off',
    // 优先使用 interface 而不是 type 
    '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/ban-types': 'off',
    '@typescript-eslint/no-unused-vars': 'off',
    'vue/html-indent': [ 
        'error',4,
        {                
            attribute: 1,
            baseIndent: 1,
            closeBracket: 0,
            alignAttributesVertically: true,
            ignores: [],
        },
    ],
    // 关闭此规则 使用 prettier 的格式化规则, 感觉prettier 更加合理,
    // 而且一起使用会有冲突
    'vue/max-attributes-per-line': ['off'],
    // 强制使用驼峰命名
        'vue/component-name-in-template-casing': [
        'error',
        'PascalCase',
            { 
                registeredComponentsOnly: false,
                ignores: [],
            },
        ],
    },
})

项目下新建 .eslintignore

# 安装 prettier 
yarn add prettier --dev 或者 npm i prettier --dev

解决 eslint 和 prettier 冲突

解决 ESLint 中的样式规范和 prettier 中样式规范的冲突,以 prettier 的样式规范为准,使 ESLint 中的样式规范自动失效

# 安装插件 eslint-config-prettier 
yarn add eslint-config-prettier --dev 或者 npm i eslint-config-prettier --dev

项目下新建 .prettier.js

配置 prettier 格式化规则:

module.exports = { 
    tabWidth: 2,
    jsxSingleQuote: true,
    jsxBracketSameLine: true,
    printWidth: 100,
    singleQuote: true,
    semi: false,
    overrides: [ 
        {     
        files: '*.json',
            options: { 
            printWidth: 200,
            },
        },
    ],
    arrowParens: 'always',
}

项目下新建 .prettierignore

# 忽略格式化文件 (根据项目需要自行添加) 
node_modules
dist

package.json 配置:

{ 
    "script": {
        "lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx",
        "prettier": "prettier --write ."
    } 
}

上面配置完成后,可以运行以下命令测试下代码检查个格式化效果:

# eslint 检查 
yarn lint 
# prettier 自动格式化 
yarn prettier

六、添加element-plus

#安装 element-plus yarn add element-plus

1.element-plus按需引入

要用到两个插件unplugin-vue-components、unplugin-auto-import这两个插件。

安装:
npm i unplugin-vue-components unplugin-auto-import -D
或者
yarn add unplugin-vue-components unplugin-auto-import -D

由于使用了 unplugin-vue-components unplugin-auto-import 这两个插件,按需加载其实是不需要 import 组件,但如果使用Api创建组件,例如elmesage,elnotification这些,可以看到不 import 的话会提示错误,如果 import 又会导致样式的丢失,需要下载一个插件

yarn add unplugin-element-plus -D #或者 npm i unplugin-element-plus -D

配置vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import ElementPlus from 'unplugin-element-plus/vite'const resolve = (dir: string) => path.join(__dirname, dir);
​export default defineConfig({  
    plugins: [    
        vue(),
        AutoImport({ 
           resolvers: [ElementPlusResolver()],
        }),    
        Components({
           resolvers: [ElementPlusResolver()],
        }),
        ElementPlus() 
    ],  
    //配置别名
    resolve: {
        alias: {      
            "@": resolve("src"),
            comps: resolve("src/components"),
            service: resolve("src/service"),
            views: resolve("src/views"),
            route: resolve("src/route"),
        },
    },
    //配置跨域代理
    server: {
        // 配置前端服务地址和端口
        //服务器主机名
        host: "127.0.0.1",
        //端口号
        port: 3088,
        //设为 true 时若端口已被占用则会直接退出,而不是尝试下一个可用端口
        strictPort: false,
        //服务器启动时自动在浏览器中打开应用程序,当此值为字符串时,会被用作 URL 的路径名
        open: true,
        //自定义代理规则
        proxy: {
            "/api": {
                target: "http://localhost:3000", //要代理的本地api地址,也可以换成线上测试地址       changeOrigin: true, //跨域
                rewrite: (path) => path.replace(/^/api/, ""),
            },
        },
    },
});

2.添加element-plus图标

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

然后在main.ts中全局注册并使用

import * as ElementPlusIconsVue from '@element-plus/icons-vue' Object.keys(ElementPlusIconsVue).forEach(key => { app.component(key, ElementPlusIconsVue[key as keyof typeof ElementPlusIconsVue]); });

七、状态管理器 Pinia

在 vue 2.x 中,vuex 是官方的状态管理库,并且 vue 3 中也有对应的 vuex 版本。但 vue 作者尤大神看了 pinia 后,强势推荐使用 pinia 作为状态管理库。下图是 vue 官网 “生态系统”,pinia 是 vue 生态之一。

1.2 pinia 的特点

  1. 支持 vue2 和 vue3,两者都可以使用 pinia
  2. 语法简洁,支持 vue3 中 setup 的写法,不必像 vuex 那样定义 statemutationsactionsgetters 等,可以按照 setup Composition API 的方式返回状态和改变状态的方法,实现代码的扁平化;
  3. 支持 vuex 中 stateactionsgetters 形式的写法,丢弃了 mutations,开发时候不用根据同步异步来决定使用 mutationsactions,pinia 中只有 actions
  4. TypeScript 支持非常友好。

1.3 pinia 的使用

在《基于 vite 创建 vue3 项目》中已经整合了 pinia,现简单回顾并进行一些调整。

  1. 安装 pinia 依赖:
yarn add pinia 或者 npm i pinia

2. 创建 pinia 实例(根存储 root store):

之前咱是在 main.ts 中创建的,现将其抽取到独立的文件中:

src/store/index.ts

import { createPinia } from 'pinia'

const pinia = createPinia()

export default pinia

3. 在 main.ts 中以插件的方式传递给 App 实例。

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import pinia from '@/store'

import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}
app.use(pinia)
app.mount('#app')

4. 在 store/ 目录下创建 modules 目录,存储每个模块的状态,将之前的 demo.ts 移动到 store/modules/ 中。这里使用最新的 Composition API setup 的方式来定义状态。

src/store/modules/demo.ts

import { defineStore } from 'pinia'
import { ref } from 'vue'
const useDemoStore = defineStore('demo', 
    () => { 
        const counter = ref(0)
        const increment = () => { 
            counter.value++ 
        } 
       return { counter, increment }
    })
export default useDemoStore

  1. 在组件 about.vue 中使用 demo 中的状态 counter 和改变状态的函数 increment。代码和之前一样。

先引入 demo.ts 中定义的 useDemoStore 函数,通过该函数创建 demoStore 实例。然后就可以调用 demoStore 的状态 counterincrement 函数了。这里需要注意,无论是 pinia 还是 vuex,通过解构的方式获取状态,会导致状态失去响应性。如:

const { counter } = demoStore

此时的 counter 会丢失响应性,当其值改变时,其他组件不会监听到。所以 pinia 提供了 storeToRefs 函数,使其解构出来的状态仍然具备响应性。

const { counter } = storeToRefs(demoStore)

src/views/about.vue 完整代码如下:

<template>
    <div class="about">
        <h1>This is an about page</h1>
        <h3>counter: {{counter}}</h3>
        <el-button @click="add">
        <el-icon-plus></el-icon-plus>
        </el-button>
    <div>
</template>
<script lang="ts" setup> 
    import useDemoStore from '@/store/modules/demo'
    import { storeToRefs } from 'pinia'
    import SvgIcon from '@/components/svg-icon/index.vue'
    const demoStore = useDemoStore()
    const { counter } = storeToRefs(demoStore)
    const add = () => { 
        demoStore.increment()
    } 
</script>
<style scoped>
.icon { 
    color: cornflowerblue;
    font-size: 30px; 
}
</style>

最后在浏览器中访问 about 页面,可以正常运行,点击加号按钮,计数器会加1。

2 持久化 pinia 状态

在上面的 demo 中,假设计数器加到 5,如果刷新浏览器,counter 的值又会被初始化为 0。这是因为状态是存储在浏览器内存中的,刷新浏览器后,重新加载页面时会重新初始化 vuepinia,而 pinia 中状态的值仅在内存中存在,而刷新导致浏览器存储中的数据没了,所以 counter 的值就被初始化为 0。

在实际开发中,浏览器刷新时,有些数据希望是保存下来的。如用户登录后,用户信息会存储在全局状态中,如果不持久化状态,那么每次刷新用户都需要重新登录了。

要解决这个问题非常简单,在状态改变时将其同步到浏览器的存储中,如 cookielocalStoragesessionStorage 。每次初始化状态时从存储中去获取初始值即可。

说起来思路很简单,可真正实现起来就各种问题了,所以咱们就使用 pinia 的插件 pinia-plugin-persistedstate 来实现。

2.2 pinia-plugin-persistedstate

接下来就使用 pinia-plugin-persistedstate 插件实现 pinia 状态的持久化。

  1. 安装依赖:
yarn add pinia-plugin-persistedstate

2. 引入该插件,在创建 pinia 实例时传入该插件src/store/index.ts

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia

3. 在需要持久化状态的模块中设置 persist。咱假设 demo 模块需要对状态需要持久化,defineStore 第一个参数定义唯一的模块名,第二个参数传递 setup,其实还有第三个参数 options,在 options 中便可开启 persist

src/store/modules/demo.ts

const useDemoStore = defineStore('demo', 
    () => { ... },
    { persist: true }
)

此时改变 counter 的值后,刷新浏览器,counter 不会被重置为 0,仍然停留在刷新前的状态。

persist 支持多种类型的值,最简单的就是传递 true,此时会将状态缓存在 localStorage 中,该 localStorage 的 key 为模块名(defineStore 的第一个参数),value 为该模块的状态对象,由于该模块只有一个状态 counter,故value为 {"counter":8}

如果需要将其存储在 sessionStorage 中,就需要设置 persist 的值为一个对象:

const useDemoStore = defineStore('demo', 
() => { ... },
{ 
    persist:
    { 
        key: 'aaa',
        storage: sessionStorage
    }
})

此时状态就会同步缓存到 sessionStorage 中,并且key 为咱们指定的 key.

八、安装路由

yarn add vue-router@4

在 src 文件下新增 router 文件夹 => index.ts 文件,内容如下(可以自行扩展,陪着路由守卫):

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'const routes: RouteRecordRaw[] = [ 
    {    
    path: '/',
    name: 'Login',
    component: () => import('@/pages/login/Login.vue'), 
    // 注意这里要带上 文件后缀.vue 
    },
] 
​ const router = createRouter({  history: createWebHistory(),  routes,}) 
​ export default router

修改入口文件 mian.ts :

import router from './router/index'


app.use(router)

九、axios统一请求封装

# 安装 axios yarn add axios

新建src/service/errorCode.ts 状态码文件

export const errorCodeType = function (code: string | number): string {
	let errMessage = '未知错误';
	switch (code) {
		case 400:
			errMessage = '错误的请求';
			break;
		case 401:
			errMessage = '未授权,请重新登录';
			break;
		case 403:
			errMessage = '拒绝访问';
			break;
		case 404:
			errMessage = '请求错误,未找到该资源';
			break;
		case 405:
			errMessage = '请求方法未允许';
			break;
		case 408:
			errMessage = '请求超时';
			break;
		case 500:
			errMessage = '服务器端出错';
			break;
		case 501:
			errMessage = '网络未实现';
			break;
		case 502:
			errMessage = '网络错误';
			break;
		case 503:
			errMessage = '服务不可用';
			break;
		case 504:
			errMessage = '网络超时';
			break;
		case 505:
			errMessage = 'http版本不支持该请求';
			break;
		default:
			errMessage = `其他连接错误 --${code}`;
	}
	return errMessage;
};

新建src/service/request.ts 请求文件

import axios from 'axios';
import { errorCodeType } from '@/service/errcode';
import { ElMessage  } from 'element-plus';

// 创建axios实例

const service = axios.create({
	// 服务接口请求
	baseURL: '', //import.meta.env.VITE_APP_BASE_API,
	// 超时设置
	timeout: 10000,
	headers: { 'Content-Type': 'application/json;charset=utf-8' },
});

// 请求拦截
service.interceptors.request.use(
	(config): any => {
		const token = window.sessionStorage.getItem('token');
		if (token) {
			config.headers.token = token;
		}
		return config;
	},
	(error) => {
		console.log(error);
		Promise.reject(error);
	}
);
// 响应拦截器
service.interceptors.response.use(
	(res: any) => {
		// hideLoading()
		// 未设置状态码则默认成功状态
		const code = res.status || 200;
		// 获取错误信息
		const message = errorCodeType(code) || res.data['message'] || errorCodeType('default');
		if (code === 200 || res.data.code == '0') {
			return Promise.resolve(res.data);
		} else {
			ElMessage.error(message);
			return Promise.reject(res.data);
		}
	},
	(error) => {
		console.log('err' + error);
		// hideLoading()
		let { message } = error;
		if (message == 'Network Error') {
			message = '后端接口连接异常';
		} else if (message.includes('timeout')) {
			message = '系统接口请求超时';
		} else if (message.includes('Request failed with status code')) {
			message = '系统接口' + message.substr(message.length - 3) + '异常';
		}
		ElMessage.error(message);
		return Promise.reject(error);
	}
);
export default service;

新建 src/service/http.ts 请求文件

import service from './request';

interface ResType<T> {
	code: number;
	data?: T;
	msg: string;
	err?: string;
}
interface Http {
	get<T>(url: string, params?: unknown): Promise<ResType<T>>;
	post<T>(url: string, params?: unknown): Promise<ResType<T>>;
	upload<T>(url: string, params: unknown): Promise<ResType<T>>;
}

const http: Http = {
	get(url, params) {
		return new Promise((resolve, reject) => {
			service
				.get(url, { params })
				.then((res) => {
					resolve(res.data);
				})
				.catch((err) => {
					reject(err.data);
				});
		});
	},
	post(url, params) {
		return new Promise((resolve, reject) => {
			service
				.post(url, JSON.stringify(params))
				.then((res) => {
					resolve(res.data);
				})
				.catch((err) => {
					reject(err.data);
				});
		});
	},
export default http;

封装的请求使用方式:

export const getData = () => {
	return http.post('/api/model/api/pools/pool-view/pool-advice-query');
};