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 的特点
- 支持 vue2 和 vue3,两者都可以使用 pinia;
- 语法简洁,支持 vue3 中 setup 的写法,不必像 vuex 那样定义 state、mutations、actions、getters 等,可以按照 setup Composition API 的方式返回状态和改变状态的方法,实现代码的扁平化;
- 支持 vuex 中 state、actions、getters 形式的写法,丢弃了 mutations,开发时候不用根据同步异步来决定使用 mutations 或 actions,pinia 中只有 actions;
- 对 TypeScript 支持非常友好。
1.3 pinia 的使用
在《基于 vite 创建 vue3 项目》中已经整合了 pinia,现简单回顾并进行一些调整。
- 安装 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
- 在组件 about.vue 中使用 demo 中的状态 counter 和改变状态的函数 increment。代码和之前一样。
先引入 demo.ts 中定义的 useDemoStore 函数,通过该函数创建 demoStore 实例。然后就可以调用 demoStore 的状态 counter 和 increment 函数了。这里需要注意,无论是 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。这是因为状态是存储在浏览器内存中的,刷新浏览器后,重新加载页面时会重新初始化 vue、 pinia,而 pinia 中状态的值仅在内存中存在,而刷新导致浏览器存储中的数据没了,所以 counter 的值就被初始化为 0。
在实际开发中,浏览器刷新时,有些数据希望是保存下来的。如用户登录后,用户信息会存储在全局状态中,如果不持久化状态,那么每次刷新用户都需要重新登录了。
要解决这个问题非常简单,在状态改变时将其同步到浏览器的存储中,如 cookie、localStorage、sessionStorage 。每次初始化状态时从存储中去获取初始值即可。
说起来思路很简单,可真正实现起来就各种问题了,所以咱们就使用 pinia 的插件 pinia-plugin-persistedstate 来实现。
2.2 pinia-plugin-persistedstate
接下来就使用 pinia-plugin-persistedstate 插件实现 pinia 状态的持久化。
- 安装依赖:
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');
};