0.前言
技术栈:【Vue3】【Vite4】【Pinia】【Vue Router】【Element Plus】
node:v18.17.1
npm:9.6.7
pnpm:9.7.0
1.创建项目
pnpm create vite my-vue-app --template vue
cd my-vue-app
npm install
npm run dev
2.代码格式化
- 安装 Prettier 插件
- 安装依赖
pnpm install --save-dev --save-exact prettier
pnpm install eslint --save-dev
- 配置
根目录下创建.prettierrc.json
格式化配置文件
可以在 Prettier 中测试效果,然后拷贝配置内容到自己的项目中
{
"arrowParens": "always",
"bracketSameLine": true,
"bracketSpacing": true,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "all",
"singleAttributePerLine": false,
"htmlWhitespaceSensitivity": "css",
"vueIndentScriptAndStyle": false,
"proseWrap": "preserve",
"insertPragma": false,
"requirePragma": false,
"useTabs": false,
"embeddedLanguageFormatting": "auto",
"tabWidth": 2,
"printWidth": 200
}
根目录下创建.prettierignore
忽略格式化文件
node_modules
dist
3.ESlint
ESlint 检查代码规范
- 安装ESLint
pnpm add -D eslint
- 初始化 ESLint 配置
npx eslint --init
- 配置初始化选择
? How would you like to use ESLint? ...
To check syntax only
To check syntax and find problems
> To check syntax, find problems, and enforce code style
---------------------------------------------------------------- 选择强制代码风格
√ How would you like to use ESLint? · style
? What type of modules does your project use? ...
> JavaScript modules (import/export)
CommonJS (require/exports)
None of these
------------------------------------------------------------选择语言模块
? Which framework does your project use? ...
React
> Vue.js
None of these
------------------------------------------------------------选择语言框架
? Does your project use TypeScript? » No / Yes
--------------------------------------------是否使用ts (视自己情况而定,我这里不用选No)
? Where does your code run? ... (Press <space> to select, <a> to toggle all, <i> to invert selection)
√ Browser
√ Node
-----------------------------------------------代码在哪里运行 (用空格选中 Browser+Node)
? How would you like to define a style for your project? ...
Use a popular style guide
> Answer questions about your style
-----------------------------------------------用过A&Q来配置规则
? What format do you want your config file to be in? ...
> JavaScript
YAML
JSON
----------------------------------------------配置文件使用js文件
? What style of indentation do you use? ...
> Tabs
Spaces
----------------------------------------------缩进方式
? What quotes do you use for strings? ...
Double
> Single
-----------------------------------------------字符串用什么引号
? What line endings do you use? ...
> Unix
Windows
---------------------------------------------------
? Do you require semicolons? » No / Yes
---------------------------------------------------是否需要分号(视自己情况而定,我这里不用选No)
? Would you like to install them now? » No / Yes
---------------------------------------------------选择yes现在立即初始化配置文件
? Which package manager do you want to use? ...
> npm
yarn
pnpm
------------------------------------------------包管理器选择npm
- 安装完成后(根目录会生成
.eslintrc.cjs
文件)
module.exports = {
'env': {
'browser': true,
'es2021': true,
'node': true
},
'extends': [
'eslint:recommended',
'plugin:vue/vue3-essential'
],
'overrides': [
],
'parserOptions': {
'ecmaVersion': 'latest',
'sourceType': 'module'
},
'plugins': [
'vue'
],
'rules': {
'indent': [
'error',
'tab'
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'never'
]
}
}
- 安装
vite-plugin-eslint
(eslint结合vite使用)
// 说明: 该包是用于配置vite运行的时候自动检测eslint规范 不符合页面会报错
pnpm add -D vite-plugin-eslint
- 安装
eslint-parser
pnpm add -D @babel/core
pnpm add -D @babel/eslint-parser
- 配置
vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import eslintPlugin from 'vite-plugin-eslint';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
eslintPlugin({
include: ['src/**/*.ts', 'src/**/*.vue', 'src/*.ts', 'src/*.vue'],
}),
],
resolve: {
// 配置别名联想路径
alias: {
'@': resolve(__dirname, 'src'),
},
},
})
-
配合 ESLint 插件检查
ESLint 配置规则文件 -
.eslintignore
配置 eslint 忽略文件
# 忽略public目录下文件的语法检查
public
# 忽略node_modules目录下文件的语法检查
node_modules
# 忽略src/assets目录下文件的语法检查
src/assets
# 忽略dist目录下文件的语法检查
dist
- 在
package.json
文件添加 lint
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint --ext .js,.vue --ignore-path .eslintignore --fix src"
},
- 关闭命名规则校验
.eslintrc.js
文件中 rules:
里添加:
"vue/multi-word-component-names":"off",
若不生效,则在 package.json
文件中添加:
"eslintConfig": {
"rules": {
"vue/multi-word-component-names": "off"
}
}
或者在vite.config.js
文件中添加:
eslintPlugin({
include: ['src/**/*.ts', 'src/**/*.vue', 'src/*.ts', 'src/*.vue'],
overrideConfig: {
rules: {
'vue/multi-word-component-names': 'off',
},
},
}),
4.Vite项目配置
- 解决
import { ref , reactive ..... } from 'vue'
大量引入的问题
pnpm i -D unplugin-auto-import
vite.config.js
plugins: [
vue(),
// 解决 `import { ref , reactive ..... } from 'vue'` 大量引入的问题
AutoImport({
imports: ['vue', 'vue-router'],
}),
],
- $ref 语法糖告别
store(pinia版) 中使用 $ref 无法正常持久化数据!!!
pnpm install @vue-macros/reactivity-transform
vite.config.js
plugins: [
vue(),
ReactivityTransform(), // 启用响应式语法糖 $ref ...
],
解决ESLint警告: '$ref' is not defined.
.eslintrc.cjs
module.exports = {
globals: { $ref: 'readonly', $computed: 'readonly', $shallowRef: 'readonly', $customRef: 'readonly', $toRef: 'readonly' },
};
解决ESlint警告:error '$ref' is not defined no-undef
rules: {
'no-undef': 'off',
},
注意:
ref语法糖有其使用限制
,特别是在声明响应式变量时。虽然理论上可以使用let
和const
来声明响应式变量,但在实际使用ref语法糖时,通常建议使用let
而不是const
。
原因在于ref语法糖的目的是为了创建一个响应式引用,而这个引用可能会在组件的生命周期中发生变化。使用
let
可以确保引用的值可以被改变,而const
则保证了变量的值在声明后不会被改变,这包括响应式引用的值。因此,虽然技术上可以使用const
来声明使用ref语法糖的目的是为了创建一个响应式引用,而这个引用可能会在组件的生命周期中发生变化。使用let
可以确保引用的值可以被改变,而const
则保证了变量的值在声明后不会被改变,这包括响应式引用的值。
因此,虽然技术上可以使用
const
来声明使用ref语法糖的目的是为了创建一个响应式引用,而这个引用可能会在组件的生命周期中发生变化。使用let
可以确保引用的值可以被改变,而const
则保证了变量的值在声明后不会被改变,这包括响应式引用的值。因此,虽然技术上可以使用const
来声明使用ref的变量,但在实践中,为了保持响应式的灵活性,更推荐使用let
来声明这些变量。
- 环境配置。根目录下创建
.env.dev
或.env.prod
文件
# 开发环境
# NODE_ENV='dev'
# 为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。
# js中通过`import.meta.env.VITE_APP_BASE_API`取值
VITE_APP_PORT = 5173
VITE_APP_BASE_API = '/dev-api'
VITE_APP_BASE_FILE_API = '/dev-api/web/api/system/file/upload'
# 后端服务地址
VITE_APP_SERVICE_API = 'http://localhost:888'
package.json
{
"scripts": {
"dev": "vite --mode dev",
"prod": "vite --mode prod",
"build": "vite build --mode prod"
}
}
- 配置
vite.config.js
import eslintPlugin from 'vite-plugin-eslint';
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import * as path from 'path';
import AutoImport from 'unplugin-auto-import/vite';
import ReactivityTransform from '@vue-macros/reactivity-transform/vite';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
// 获取`.env`环境配置文件
const env = loadEnv(mode, process.cwd());
return {
plugins: [
vue(),
eslintPlugin({
include: ['src/**/*.ts', 'src/**/*.vue', 'src/*.ts', 'src/*.vue'],
}), // 项目运行时进行eslint检查
ReactivityTransform(), // 启用响应式语法糖 $ref ...
// 解决 `import { ref , reactive ..... } from 'vue'` 大量引入的问题
AutoImport({
imports: ['vue', 'vue-router'],
}),
],
// 反向代理解决跨域问题
server: {
// host: 'localhost', // 只能本地访问
host: '0.0.0.0', // 局域网别人也可访问
port: Number(env.VITE_APP_PORT),
// 运行时自动打开浏览器
// open: true,
proxy: {
[env.VITE_APP_BASE_API]: {
target: env.VITE_APP_SERVICE_API,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), ''),
},
},
},
resolve: {
// 配置路径别名
alias: [
// @代替src
{
find: '@',
replacement: path.resolve('./src'),
},
],
},
// 引入scss全局变量
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/color.scss";@import "@/styles/theme.scss";`,
},
},
},
};
});
5. sass集成
pnpm install sass --save-dev
深度选择器
vue3+scss中不要
使用下面方式,会有警告
::v-deep .el-input__wrapper {
background-color: #08c0ee8c;
}
改用
::v-deep(.el-input__wrapper) {
background-color: #08c0ee8c;
}
6. Vue-Router
- 安装
pnpm install vue-router@4
- 配置
src/router/index.js
import {createRouter, createWebHashHistory} from 'vue-router';
// 本地静态路由
export const constantRoutes = [
{
path: '/test',
component: () => import('@/views/test/index.vue'),
},
];
// 创建路由
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes,
// 滚动行为 eg: 用户希望查看详情页以后,返回列表页刚刚浏览的位置
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { x: 0, y: 0 };
}
},
});
export default router;
src/main.js
// 路由
import router from '@/router';
app.use(router);
src/App.vue
<template>
<!--使用 router-link 组件进行导航 -->
<!--通过传递 `to` 来指定链接 -->
<ol>
<li><router-link to="/">Go to Home</router-link></li>
<li><router-link to="/test">Go to Test</router-link></li>
</ol>
<hr />
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view/>
</template>
- js获取当前路由信息
proxy.$route
- js跳转
proxy.$router.push({ path: '/' });
7. Element Plus
- 安装
pnpm install element-plus --save
pnpm install @element-plus/icons-vue
- 配置
src/main.js
// element-plus
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
app.use(ElementPlus);
// 注册所有图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
- 使用
<template>
<h1>hello</h1>
<el-row class="mb-4">
<el-button>Default</el-button>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
</el-row>
<el-icon :size="100" color="red">
<Edit />
</el-icon>
</template>
8. Mixin 混入
Mixin
全局混入是一种在多个组件中共享相同逻辑的方式,可以将一些通用的方法、生命周期钩子等混入到所有页面和组件中,以简化代码的编写和维护。
等于抽取公共属性、方法…
./src/utils/mixin.js
// 抽取公用的实例 - 操作成功与失败消息提醒内容等
export default {
data() {
return {
sexList: [
{ name: '不想说', value: 0 },
{ name: '男', value: 1 },
{ name: '女', value: 2 },
],
};
},
methods: {
// 操作成功消息提醒内容
submitOk(msg, cb) {
this.$notify({
title: '成功',
message: msg || '操作成功!',
type: 'success',
duration: 2000,
onClose: function () {
cb && cb();
},
});
},
// 操作失败消息提醒内容
submitFail(msg) {
this.$message({
message: msg || '网络异常,请稍后重试!',
type: 'error',
});
},
},
};
这里的this.$notify
与$message
使用了Element Plus反馈组件。
- 局部混入
<script>
import mixin from '@/utils/mixin.js';
export default {
mixins: [mixin],
};
</script>
<script setup>
const { proxy } = getCurrentInstance();
async function submit() {
proxy.submitOk('保存成功');
}
</script>
- 全局混入
src/main.js
// 混入 -- 抽取公用的实例(操作成功与失败消息提醒内容等)
import mixin from '@/utils/mixin';
app.mixin(mixin);
使用
<template>
<h1>{{ sexList }}</h1>
<button @click="handleClick">click</button>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
async function handleClick() {
proxy.submitOk('保存成功');
proxy.submitFail('操作失败');
}
</script>
9.全局过滤器
当后端返回的数据格式不是前端想要的时候,可以将返回的数据处理成自己需要的格式。
eg: 时间过滤器
Vue2
import { filters } from '@/utils/filters.js';
Object.keys(filters).forEach((key) => Vue.filter(key, filters[key]));
Vue3
通过 app.config.globalProperties
来注册一个全局都能访问到的属性
/src/utils/filters.js
export const filters = {
// 获取性别值
sexName: (sex) => {
// 拿到mixin混入的属性值
const { proxy } = getCurrentInstance();
let result = proxy.sexList.find((obj) => obj.value == sex);
return result ? result.name : '数据丢失';
},
};
/src/main.js
// 全局过滤器
import { filters } from '@/utils/filters.js';
app.config.globalProperties.$filters = filters;
使用
<h1>{{ $filters.sexName(1) }}</h1>
sexList: [
{ name: ‘不想说’, value: 0 },
{ name: ‘男’, value: 1 },
{ name: ‘女’, value: 2 },
],
10. Pinia
- 安装
pnpm install pinia
- 配置
src/main.js
// pinia
import { createPinia } from 'pinia';
const pinia = createPinia();
app.use(pinia);
// 全局store
import store from '@/store';
app.config.globalProperties.$store = store;
- 使用
store 模块化
src/store/index.js
// 拿到modules下的所有文件
const modulesFiles = import.meta.globEager('./modules/*.*');
const modules = {};
for (const key in modulesFiles) {
const moduleName = key.replace(/(.*\/)*([^.]+).*/gi, '$2');
const value = modulesFiles[key];
modules[moduleName] = value;
// console.log(modules);
}
export default modules;
src/store/modules/test.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('test', () => {
const count = ref(8888);
function add() {
count.value++;
}
return { count, add };
});
3.1. 定义Store
- (1) Option Store
可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Eduardo' }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
- (2) Setup Store
在 Setup Store 中:
ref() 就是 state 属性
computed() 就是 getters
function() 就是 actions
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
注意:要让 pinia 正确识别 state,必须在 setup store 中返回 state 的所有属性。这意味着,你不能在 store 中使用私有属性。不完整返回会影响 SSR ,开发工具和其他插件的正常运行
3.2 使用
<template>
<h1>JS中获取count值{{ count }}</h1>
<button @click="handleClick">click</button>
<br />
<h1>模版中直接获取count值{{ $store.test.useCounterStore().count }}</h1>
<button @click="$store.test.useCounterStore().add">click</button>
</template>
<script setup>
import { getCurrentInstance, toRefs } from 'vue';
const { proxy } = getCurrentInstance();
let useTestStore = proxy.$store.test.useCounterStore();
let { count } = toRefs(useTestStore); // 响应式
let { add } = useTestStore;
function handleClick() {
add();
}
</script>
<style lang="scss" scoped></style>
11. Pinia 持久化
上面的配置浏览器一刷新数据就丢了,所以配置下持久化存储。
pinia-plugin-persistedstate
- 安装
pnpm add pinia-plugin-persistedstate
- 配置
src/main.js
该配置将会使所有 Store 持久化存储,且必须配置
persist: false
显式禁用持久化。
// 持久化存储
import { createPersistedState } from 'pinia-plugin-persistedstate';
pinia.use(
createPersistedState({
auto: true, // 启用所有 Store 默认持久化
})
)
tips: pinia持久化的无法通过 window.localStorage.clear();
一键清空数据
window.localStorage.setItem('user2', 'hello');
// window.localStorage.removeItem('user2');
// tips: pinia持久化的无法通过这种方式清空数据,只能删除同样方式存储的值 eg: window.localStorage.setItem('user2', 'hello');
window.localStorage.clear();
window.sessionStorage.clear();
$reset() 清空数据
组合式api中直接使用 $reset() 会报错
解决:
- 改用选项式api
- 重写 $reset 方法
src/main.js
// 重写 $reset 方法 => 解决组合式api中无法使用问题
pinia.use(({ store }) => {
const initialState = JSON.parse(JSON.stringify(store.$state));
store.$reset = () => {
store.$patch(initialState);
};
});
使用views/login/index.vue
<script setup>
import { getCurrentInstance, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
let useTestStore = proxy.$store.test.useCounterStore();
function resets() {
// 重置 state
useTestStore.$reset();
// 变更 state
// useTestStore.$patch({
// count: 100,
// });
ElMessage({
message: '重置成功',
type: 'success',
});
}
</script>
12. axios 和 api 封装
- 安装
pnpm install axios
- axios 工具封装
src/utils/request.js
import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import store from '@/store';
import { localStorage } from '@/utils/storage';
// 创建axios实例
const service = axios.create({
// baseURL: import.meta.env.VITE_APP_BASE_API,
baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
timeout: 50000, // 请求超时时间:50s
headers: { 'Content-Type': 'application/json;charset=utf-8' },
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
if (!config.headers) {
throw new Error(`Expected 'config' and 'config.headers' not to be undefined`);
}
const { isLogin, tokenObj } = toRefs(store.user.useUserStore());
if (isLogin.value) {
// 授权认证
config.headers[tokenObj.value.tokenName] = tokenObj.value.tokenValue;
// 租户ID
config.headers['TENANT_ID'] = '1';
// 微信公众号appId
config.headers['appId'] = localStorage.get('appId');
}
if (config.isFile) {
// 图片上传
config.headers['Content-Type'] = 'multipart/form-data';
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data;
const { code, msg } = res;
if (code === '1') {
return res;
} else {
// token过期
if (code === -1) {
handleError();
} else {
console.log(msg, 11111111111);
ElMessage({
message: msg || '系统出错',
type: 'error',
duration: 5 * 1000,
});
}
return Promise.reject(new Error(msg || 'Error'));
}
},
(error) => {
console.log('请求异常:', error);
const { msg } = error.response.data;
// 未认证
if (error.response.status === 401) {
handleError();
} else {
console.log(msg, 22222);
ElMessage({
message: '网络异常,请稍后再试!',
type: 'error',
duration: 5 * 1000,
});
return Promise.reject(new Error(msg || 'Error'));
}
},
);
// 统一处理请求响应异常
function handleError() {
const { isLogin, logout } = store.user.useUserStore();
if (isLogin) {
ElMessageBox.confirm('您的登录账号已失效,请重新登录', {
confirmButtonText: '再次登录',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
logout();
});
}
}
// 导出实例
export default service;
- api 封装
src/api/index.js
// 拿到所有api
const modulesFiles = import.meta.glob('./test/*.*', { eager: true });
const modules = {};
for (const key in modulesFiles) {
const moduleName = key.replace(/(.*\/)*([^.]+).*/gi, '$2');
const value = modulesFiles[key];
if (value.default) {
// 兼容js
modules[moduleName] = value.default;
} else {
// 兼容ts
modules[moduleName] = value;
}
}
// console.log(666, modules);
export default modules;
- 全局配置
src/main.js
// 配置全局api
import api from '@/api'
app.config.globalProperties.$api = api
- 测试
src/api/test/demo.js
import request from '@/utils/request';
export const time = () => {
return request({
url: '/home/category/head',
method: 'get',
});
};
页面
<template>
<el-button @click="handleAPI">API</el-button>
<h1>{{ data }}</h1>
</template>
<script setup>
const { proxy } = getCurrentInstance();
let res = $ref(null);
let data = $ref(null);
// 获取数据
const handleAPI = async () => {
res = await proxy.$api.demo.time();
data = res.result[0].name;
console.log(res);
};
</script>
13. 全局组件
- 获取所有组件
src/components/index.js
const modulesFiles = import.meta.glob('./*/*.vue', { eager: true });
const modules = {};
for (const key in modulesFiles) {
const moduleName = key.replace(/(.*\/)*([^.]+).*/gi, '$2');
const value = modulesFiles[key];
modules[moduleName] = value.default;
}
// console.log(666, modules);
export default modules;
- 全局组件注册
src/main.js
// 全局组件注册
import myComponent from '@/components/index';
Object.keys(myComponent).forEach((key) => {
app.component(key, myComponent[key]);
});
- 组件示例
src/components/base/BaseNoData.vue
<template>
<div>
<slot>暂无数据</slot>
</div>
</template>
- 引用组件
src/views/login/index.vue
<base-no-data />
<base-no-data>请先选择数据</base-no-data>
14. 代码模块化
vue.json
"vue3": {
"prefix": "vue3",
"body": [
"<template>",
"<div></div>",
"</template>",
"<script setup></script>",
"<style scoped></style>",
],
"description": "vue3 setup模板"
},