「写好的代码」:Lvan826199/mwj-vue3-project: vue3-vite构建的一个前端模版 (github.com)
学习开源地址:https://github.com/Lvan826199/mwj-vue3-project
主要技术栈:「Vue3+Vite+Element-plus+Pinia+Axios+Js」
此项目用作学习,由于没有构建后端,部分项目数据写死在前端,动态路由部分有bug还没修,主要是一个简要的前端后台管理模版学习,记录一下,自己受益良多。
后面计划专注于一个项目架构进行学习,东学一下西学一下现在成傻子了。
Vue3+Vite,我本地的node和npm版本如下。
node v18.16.1
npm 9.5.1
vite官网文档:https://cn.vitejs.dev/guide/
通过附加的命令行选项直接指定项目名称和你想要使用的模板
# npm 7+, extra double-dash is needed:
npm create vite@latest mwj-vue3-project -- --template vue
输入命令后的显示如下:
D:\Y_WebProject>npm create vite@latest mwj-vue3-project -- --template vue
Need to install the following packages:
create-vite@5.2.2
Ok to proceed? (y) y
Scaffolding project in D:\Y_WebProject\mwj-vue3-project...
Done. Now run:
cd mwj-vue3-project
npm install
npm run dev
注:上面是以最新的vite版本安装,如果需要制定vite4安装,可以使用如下命令
npm create vite@4.3.0 mwj-vue3-project -- --template vue
命令行输入:
cd mwj-vue3-project
npm install
或
npm i
输入命令后的显示如下:
D:\Y_WebProject>cd mwj-vue3-project
D:\Y_WebProject\mwj-vue3-project>npm install
added 27 packages in 2m
npm run dev
输入命令后的显示如下:
VITE v5.1.6 ready in 403 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
「浏览器展示」
在浏览器输入http://localhost:5173/,显示如下表示你构建成功了
使用VSCode打开我们搭建好的项目,点击信任
目录展示
删除我们不需要的文件
vue插件:Vue - Official
简体中文插件:Chinese(Slimplified)
Prettier:代码格式化工具
在VScode插件拓展中安装Prettier,点击安装即可
image
接着我们还需要再项目依赖中下载Prettier
npm install --save-dev --save-exact prettier
npm install eslint --save-dev
输入命令后的显示如下:
https://prettier.io/docs/en/configuration.html
可以在 https://prettier.io/playground/ 中测试效果,然后拷贝配置内容到自己的项目中
1、在项目根目录下新建.prettierrc.json
文件,写入以下内容
{
"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
}
2、在项目根目录下新建.prettierignore
文件,该文件是忽略哪些文件夹下的内容不被格式化。
/dist/*
/node_modules/**
**/*.svg
/public/*
3、保存时自动格式化代码配置
打开vue文件,右键选择使用...格式化文档
- > 配置默认格式化方式 -> 选择对应的就可以
需要安装插件EditorConfig for VS Code
image
.editorconfig 是一个配置文件,用于统一编辑器的格式化规则和代码风格。它可以帮助团队成员在不同的编辑器中编写代码时保持一致的格式。
.editorconfig 文件通常放置在项目根目录下,它使用简单的键值对格式来指定编辑器的规则,例如缩进大小、换行符类型、文件编码等。
在项目根目录下新建.editorconfig
文件
# https://editorconfig.org
# 根目录配置,表示当前目录是编辑器配置的根目录
root = true
[*] # 对所有文件应用以下配置
charset = utf-8 # 使用 UTF-8 编码
indent_style = space # 使用空格进行缩进
indent_size = 2 # 每个缩进级别使用 2 个空格
end_of_line = lf # 使用 LF(Linux 和 macOS 的换行符)
insert_final_newline = true # 在文件末尾插入一行空白
trim_trailing_whitespace = true # 自动删除行末尾的空白字符
[*.md] # 对扩展名为 .md 的 Markdown 文件应用以下配置
insert_final_newline = false # 不在文件末尾插入一行空白
trim_trailing_whitespace = false # 不自动删除行末尾的空白字符
ESLint
我自己的项目,不搞这么多规范,懒
参考文档:
https://github.com/eslint/eslint
import { ref , reactive ..... } from 'vue'
大量引入的问题// 解决 `import { ref , reactive ..... } from 'vue'` 大量引入的问题
npm i -D unplugin-auto-import
// -D 参数表示将这个插件作为开发依赖(devDependency)安装
.env.dev
环境变量,在项目根目录下创建,写入如下内容
# 开发环境
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:8888'
package.json
,在项目根目录下已经存在,我们只需要进行增加内容,这样可以通过上面的这个配置文件进行启动。
"scripts": {
"dev": "vite --mode dev", // 使用.env.dev启动
"prod": "vite --mode prod", // 使用.env.prod启动,需要自己在根目录新建,类似上面的.env.dev
"build": "vite build --mode prod",
"preview": "vite preview"
}
启动项目遇到unplugin-auto-import
或reactivity-transform
报错请先下载对应的包,下面代码里面写了。
npm i -D unplugin-auto-import
npm i -D @vue-macros/reactivity-transform
vite.config.js
,在项目根目录下,修改为如下内容
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import * as path from 'path';
// npm i -D unplugin-auto-import
import AutoImport from 'unplugin-auto-import/vite';
// npm i -D @vue-macros/reactivity-transform
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(),
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";`,
// },
// },
// },
};
});
tips: 可使用原子化CSS减轻代码量
npm install sass --save-dev
删除src根目录下的style.css样式文件,新建styles文件夹,新建以下样式文件。
具体参考开源地址下的styles文件夹
官方中文网站
https://router.vuejs.org/zh
Vue Router 4 是专为 Vue 3 设计的,因此请确保你的项目使用的是 Vue 3。
npm install vue-router@4
1、在src
目录下新建router/index.js
import {createRouter, createWebHashHistory} from 'vue-router';
// 本地静态路由
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: {
isParentView: true,
},
},
{
path: '/test',
component: () => import('@/views/test/index.vue'),
},
{
// path: '/404',
path: '/:pathMatch(.*)*', // 防止浏览器刷新时路由未找到警告提示: vue-router.mjs:35 [Vue Router warn]: No match found for location with path "/xxx"
component: () => import('@/views/error-page/404.vue'),
},
];
// 创建路由
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes,
});
export default router;
2、在src
根目录下的main.js
中新增router代码
import { createApp } from 'vue'
import App from './App.vue'
// 路由
import router from '@/router';
const app = createApp(App);
app.use(router);
// 注意,要先使用所需要的内容,自后在挂载到页面上,才能正常显示
// 这一行始终保持在最后一行就行
app.mount('#app')
3、在src
根目录下新建views/error-page/404.vue
<template>
<h1>404</h1>
</template>
4、在src
根目录下新建views/test/index.vue
<template>
<h1>hello-test</h1>
</template>
5、在src
根目录下新建views/login/index.vue
<template>
<h1>我是登录页面</h1>
</template>
6、修改src/App.vue
<template>
<h1>当前路由信息:{{ $route }}</h1>
<!--使用 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>
<li><router-link to="/login">Go to login</router-link></li>
</ol>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</template>
proxy.$route
proxy.$router.push({ path: '/' });
访问 http://localhost:5173/#/test
全局混入是一种在多个组件中共享相同逻辑的方式,可以将一些通用的方法、生命周期钩子等混入到所有页面和组件中,以简化代码的编写和维护。
等于抽取公共属性、方法...
src/utils/mixin.js
// 抽取公用的实例 - 操作成功与失败消息提醒内容等
export default {
data() {
return {
sexList: [
{ name: '不想说', value: 0 },
{ name: '男', value: 1 },
{ name: '女', value: 2 },
],
};
},
methods: {
// 操作成功消息提醒内容
submitOk(msg, cb) {
console.log("点击成功");
},
// 操作失败消息提醒内容
submitFail(msg) {
console.log("点击失败");
},
},
};
<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);
完整代码
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App);
// 路由
import router from '@/router';
app.use(router);
// 自定义样式
import '@/styles/index.scss';
// 混入 -- 抽取公用的实例(操作成功与失败消息提醒内容等)
import mixin from '@/utils/mixin';
app.mixin(mixin);
// 注意,要先使用所需要的内容,自后在挂载到页面上,才能正常显示
// 这一行始终保持在最后一行就行
app.mount('#app')
修改views/test/index.vue
<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>
「效果展示」
image
当后端返回的数据格式不是前端想要的时候,可以将返回的数据处理成自己需要的格式
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
通过 app.config.globalProperties
来注册一个全局都能访问到的属性
// 全局过滤器
import { filters } from '@/utils/filters.js';
app.config.globalProperties.$filters = filters;
使用
在views/test/index.vue
加入
<h1>{{ $filters.sexName(1) }}</h1>
传入的是1,页面显示的是男,传入0,页面是不想说
https://element-plus.org/zh-CN/
npm install element-plus --save
npm 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);
}
依然去我们的src/views/test/index.vue
<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>
显示如下
修改src/utils/mixin.js文件内容,因为notify和message是依赖element-plus组件的,前面我们下载好了,这里就可以用了。
// 抽取公用的实例 - 操作成功与失败消息提醒内容等
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();
},
});
console.log("点击成功");
},
// 操作失败消息提醒内容
submitFail(msg) {
this.$message({
message: msg || '网络异常,请稍后重试!',
type: 'error',
});
console.log("点击失败");
},
},
};
这时候在测试就发现会有消息提示了。
Pinia 是 Vue.js 应用程序的状态管理库,设计为 Vuex 的继任者。随着 Vue 3 的发布,Pinia 被推荐为 Vue 3 应用的官方状态管理解决方案。它提供了一种组织和管理前端应用状态的方式,特别是在复杂的单页应用(SPA)中。Pinia 以其简洁的 API、更好的 TypeScript 集成和轻量级的设计而受到社区的欢迎。
https://pinia.vuejs.org/zh/
npm 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;
新建src/store/index.js
store模块化
// 拿到modules下的所有文件
// const modulesFiles = import.meta.globEager('./modules/*.*');
const modulesFiles = import.meta.glob('./modules/*.*', {eager: true});
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 useTestStore = defineStore('test', () => {
const count = ref(0);
function add() {
count.value++;
}
return { count, add };
});
之前我们都写在views/test/index.vue
下了,但是里面有个proxy变量声明重复了会报错,要么重新改一个,要么就把之前的复制一份重写如下。
<template>
<h1>{{ count }}</h1>
<button @click="handleClick">click</button>
<br />
<h1>{{ $store.test.useTestStore().count }}</h1>
<button @click="$store.test.useTestStore().add">click</button>
</template>
<script setup>
const { proxy } = getCurrentInstance();
let useTestStore = proxy.$store.test.useTestStore();
let { count } = toRefs(useTestStore); // 响应式
let { add } = useTestStore;
function handleClick() {
add();
}
</script>
<style lang="scss" scoped></style>
「pinia-plugin-persistedstate」
上面的配置浏览器一刷新数据就丢了,所以配置下持久化存储。
https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
npm i pinia-plugin-persistedstate
在src/main.js
新增代码
// 持久化存储
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();
https://pinia.vuejs.org/zh/core-concepts/state.html#resetting-the-state
举个栗子:
import store from '@/store';
// 退出登录
function logout() {
isLogin.value = false;
// 清空当前store在pinia中持久化存储的数据
this.$reset();
// 其它store
store.settings.useSettingsStore().$reset();
// 最终真正清空storage数据
window.localStorage.clear();
window.sessionStorage.clear();
}
组合式api中直接使用 $reset() 会报如下错:
解决:
src/main.js
// 重写 $reset 方法 => 解决组合式api中无法使用问题
pinia.use(({ store }) => {
const initialState = JSON.parse(JSON.stringify(store.$state));
store.$reset = () => {
store.$patch(initialState);
};
});
app.use(pinia);
axios中文文档 http://www.axios-js.com/zh-cn/docs
npm install 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,
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');
// }
return config;
},
(error) => {
return Promise.reject(error);
},
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data;
const { code, msg } = res;
if (code === 200) {
return res;
} else {
// token过期
if (code === -1) {
handleError();
} else {
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 {
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;
src/utils/storage.js
:浏览器永久存储、浏览器本地存储
/**
* window.localStorage => 浏览器永久存储,用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。
*/
export const localStorage = {
set(key, val) {
window.localStorage.setItem(key, JSON.stringify(val));
},
get(key) {
const json = window.localStorage.getItem(key);
return JSON.parse(json);
},
remove(key) {
window.localStorage.removeItem(key);
},
clear() {
window.localStorage.clear();
},
};
/**
* window.sessionStorage => 浏览器本地存储,数据保存在当前会话中,在关闭窗口或标签页之后将会删除这些数据。
*/
export const sessionStorage = {
set(key, val) {
window.sessionStorage.setItem(key, JSON.stringify(val));
},
get(key) {
const json = window.sessionStorage.getItem(key);
return JSON.parse(json);
},
remove(key) {
window.sessionStorage.removeItem(key);
},
clear() {
window.sessionStorage.clear();
},
};
src/api/index.js
// 拿到所有api
// const modulesFiles = import.meta.globEager('./*/*.*'); // vite4.0写法
const modulesFiles = import.meta.glob('./*/*.*', {eager: true}); // vite5.0写法
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 default {
time() {
return request({
url: '/api/test/time',
method: 'get',
});
},
};
页面
<template>
<button @click="handleClick">click</button>
<h1>{{ res }}</h1>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
let res = $ref(null);
async function handleClick() {
res = await proxy.$api.demo.time();
}
</script>
src/components/index.js
// const modulesFiles = import.meta.globEager('./*/*.vue');
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>
引用,直接在app.vue的template组件中新增下面这个代码
<base-no-data />
<base-no-data>请先选择数据</base-no-data>
页面显示效果如下,如果<base-no-data>
组件中间什么都没写,就会显示插槽的文字暂无数据
image
其它组件见src/components
src/views/login/index.vue
<template>
<base-wrapper class="bg-color-primary flex-center-center">
<div class="flex-c-center-center bg-color-white" style="height: 400px; width: 500px; border-radius: 10px">
<h1 class="font-size-lg">MwjVue3Platform</h1>
<div class="m-t-20">
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules">
<el-form-item prop="username">
<el-input v-model="loginForm.username" prefix-icon="User" placeholder="请输入账号" maxlength="30" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" prefix-icon="Lock" placeholder="请输入密码" show-password maxlength="30" />
</el-form-item>
</el-form>
<div class="tips">
<span>用户名: admin</span>
<span class="m-l-20"> 密码: 123456</span>
</div>
<el-button type="primary" class="m-t-10 w-full" @click="handleLogin">登 录</el-button>
</div>
</div>
<div class="copyright">
<p>IF I WERE YOU</p>
</div>
</base-wrapper>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
// 组件实例
const { proxy } = getCurrentInstance();
const { login } = proxy.$store.user.useUserStore();
const loginForm = $ref({});
const loginRules = {
username: [{ required: true, trigger: 'change', message: '请输入账号' }],
password: [{ required: true, trigger: 'change', validator: validatePassword }],
};
function validatePassword(rule, value, callback) {
if (!value || value.length < 6) {
callback(new Error('密码最少6位'));
} else {
callback();
}
}
function handleLogin() {
proxy.$refs.loginFormRef.validate((valid) => {
if (valid) {
login(loginForm).then(() => {
console.log('登录成功');
// 跳转到首页
proxy.$router.push({ path: '/' });
// let fullPath = proxy.$route.fullPath;
// if (fullPath.startsWith('/login?redirect=')) {
// let lastPath = fullPath.replace('/login?redirect=', '');
// // 跳转到上次退出的页面
// proxy.$router.push({ path: lastPath });
// } else {
// // 跳转到首页
// proxy.$router.push({ path: '/' });
// }
});
}
});
}
</script>
<style lang="scss" scoped>
.copyright {
width: 100%;
position: absolute;
bottom: 0;
font-size: 12px;
text-align: center;
color: #ccc;
}
</style>
src/store/modules/user.js
import { defineStore } from 'pinia';
import sysUserApi from '@/api/system/sys_user.js';
// 动态导入拿到所有页面 eg: {/src/views/test/index.vue: () => import("/src/views/test/index.vue")}
const views = import.meta.glob('@/views/**/**.vue');
import { useRoute, useRouter } from 'vue-router';
import store from '@/store';
export const useUserStore = defineStore('user', () => {
const route = useRoute();
const router = useRouter();
let isLogin = ref(false);
let tokenObj = ref({});
let userObj = ref({});
let routerMap = ref({}); // 全路径'/system/user' -> 路由信息
// 登录
async function login(loginObj) {
console.log('loginObj', loginObj);
console.log('isLogin.value', isLogin.value);
if (isLogin.value) {
return;
}
let result = await sysUserApi.login({
username: loginObj.username.trim(),
password: loginObj.password.trim(),
});
// isLogin.value = true;
// tokenObj.value = result.data;
// getUserInfo();
}
// 退出登录
function logout() {
// 清空pinia存储的数据
this.$reset();
store.settings.useSettingsStore().$reset();
// window.localStorage.setItem('user2', 'hello');
// window.localStorage.removeItem('user2');
// tips: pinia持久化的无法通过这种方式清空数据,只能删除同样方式存储的值 eg: window.localStorage.setItem('user2', 'hello');
window.localStorage.clear();
window.sessionStorage.clear();
// 跳转登录页
router.push(`/login?redirect=${route.fullPath}`);
// window.location.href = '/login';
location.reload(); // 强制刷新页面
}
// 获取用户 & 权限数据
async function getUserInfo() {
let result = await sysUserApi.getUserPerm();
userObj.value = result.data;
// 初始化系统设置数据
// store.system.useSystemStore().init();
}
const routerList = computed(() => {
// 拿到后台的权限数据
return generateRouterList({}, userObj.value.permissionTreeList);
});
// 生成侧边栏菜单 & 权限路由数据
function generateRouterList(parentObj, permList) {
let result = [];
if (!permList || permList.length === 0) {
return result;
}
for (let index = 0; index < permList.length; index++) {
let permItem = permList[index];
// 填充字段数据
if (!permItem.meta) {
permItem.meta = {};
}
if (!permItem.meta.isParentView) {
permItem.meta.isParentView = false;
}
if (!permItem.meta.sort) {
permItem.meta.sort = 10000;
}
let title = permItem.meta.title;
if (title) {
if (parentObj.meta) {
// [子级]
// 面包屑数据
permItem.meta.breadcrumbItemList = parentObj.meta.breadcrumbItemList.concat([title]);
// 全路径
permItem.meta.fullPath = parentObj.meta.fullPath + '/' + permItem.path;
} else {
// [顶级]
permItem.meta.breadcrumbItemList = [title];
permItem.meta.fullPath = permItem.path;
}
}
// 组件页面显示处理
permItem.component = views[`/src/views/${permItem.component}.vue`];
routerMap.value[permItem.meta.fullPath] = permItem;
// 递归处理
if (permItem.children.length > 0) {
permItem.children = generateRouterList(permItem, permItem.children);
}
result.push(permItem);
}
// 从小到大 升序
result = result.sort((a, b) => {
return a.meta.sort - b.meta.sort;
});
return result;
}
return { isLogin, login, logout, tokenObj, userObj, getUserInfo, routerList, routerMap };
});
调试
App.vue
代码如下
<template>
<!-- <h1>当前路由信息:{{ $route }}</h1> -->
<router-view></router-view>
</template>
<script setup></script>
src/views/login/index.vue
, 后端写了可以加上,我没加
function handleLogin() {
proxy.$refs.loginFormRef.validate((valid) => {
if (valid) {
login(loginForm).then(() => {
let fullPath = proxy.$route.fullPath;
if (fullPath.startsWith('/login?redirect=')) {
let lastPath = fullPath.replace('/login?redirect=', '');
// 跳转到上次退出的页面
proxy.$router.push({ path: lastPath });
} else {
// 跳转到首页
proxy.$router.push({ path: '/' });
}
});
}
});
}
src/store/modules/user.js
用于存储用户的登录信息(重要)
import { defineStore } from 'pinia';
import sysUserApi from '@/api/system/sys_user.js';
// 动态导入拿到所有页面 eg: {/src/views/test/index.vue: () => import("/src/views/test/index.vue")}
const views = import.meta.glob('@/views/**/**.vue');
import { useRoute, useRouter } from 'vue-router';
import store from '@/store';
export const useUserStore = defineStore('user', () => {
const route = useRoute();
const router = useRouter();
let isLogin = ref(false);
let tokenObj = ref({});
let userObj = ref({});
let routerMap = ref({}); // 全路径'/system/user' -> 路由信息
// 登录
async function login(loginObj) {
if (isLogin.value) {
return;
}
let result = await sysUserApi.login({
username: loginObj.username.trim(),
password: loginObj.password.trim(),
});
isLogin.value = true;
tokenObj.value = result.data;
getUserInfo();
}
// 退出登录
function logout() {
// 清空pinia存储的数据
this.$reset();
store.settings.useSettingsStore().$reset();
// window.localStorage.setItem('user2', 'hello');
// window.localStorage.removeItem('user2');
// tips: pinia持久化的无法通过这种方式清空数据,只能删除同样方式存储的值 eg: window.localStorage.setItem('user2', 'hello');
window.localStorage.clear();
window.sessionStorage.clear();
// 跳转登录页
router.push(`/login?redirect=${route.fullPath}`);
// window.location.href = '/login';
location.reload(); // 强制刷新页面
}
// 获取用户 & 权限数据
async function getUserInfo() {
let result = await sysUserApi.getUserPerm();
userObj.value = result.data;
}
const routerList = computed(() => {
// 拿到后台的权限数据
return generateRouterList({}, userObj.value.permissionTreeList);
});
// 生成侧边栏菜单 & 权限路由数据
function generateRouterList(parentObj, permList) {
let result = [];
if (!permList || permList.length === 0) {
return result;
}
for (let index = 0; index < permList.length; index++) {
let permItem = permList[index];
// 填充字段数据
if (!permItem.meta) {
permItem.meta = {};
}
if (!permItem.meta.isParentView) {
permItem.meta.isParentView = false;
}
if (!permItem.meta.sort) {
permItem.meta.sort = 10000;
}
let title = permItem.meta.title;
if (title) {
if (parentObj.meta) {
// [子级]
// 面包屑数据
permItem.meta.breadcrumbItemList = parentObj.meta.breadcrumbItemList.concat([title]);
// 全路径
permItem.meta.fullPath = parentObj.meta.fullPath + '/' + permItem.path;
} else {
// [顶级]
permItem.meta.breadcrumbItemList = [title];
permItem.meta.fullPath = permItem.path;
}
}
// 组件页面显示处理
permItem.component = views[`/src/views/${permItem.component}.vue`];
routerMap.value[permItem.meta.fullPath] = permItem;
// 递归处理
if (permItem.children.length > 0) {
permItem.children = generateRouterList(permItem, permItem.children);
}
result.push(permItem);
}
// 从小到大 升序
result = result.sort((a, b) => {
return a.meta.sort - b.meta.sort;
});
return result;
}
return { isLogin, login, logout, tokenObj, userObj, getUserInfo, routerList, routerMap };
});
src/api/system/sys_user.js
import request from '@/utils/request';
const BASE_API = '/web/api/system/user';
export default {
// 获取验证码
getCaptcha() {
return request({
url: '/captcha?t=' + new Date().getTime().toString(),
method: 'get',
});
},
// 登录
login(data) {
return request({
url: '/web/api/auth/login',
method: 'post',
data,
// headers: {
// // 客户端信息Base64明文:web:123456
// Authorization: 'Basic d2ViOjEyMzQ1Ng==',
// },
});
},
// 注销
logout() {
return request({
url: '/web/api/auth/logout',
method: 'delete',
});
},
// 获取用户权限
getUserPerm() {
return request({
url: '/web/api/system/perm/getUserPerm',
method: 'get',
// params: { systemSource: 0 }
});
},
listPage(query, headers) {
return request({
url: BASE_API + '/listPage',
method: 'get',
params: query,
headers,
});
},
add(data) {
return request({
url: BASE_API,
method: 'post',
data,
});
},
update(data) {
return request({
url: BASE_API,
method: 'put',
data,
});
},
delete(id) {
return request({
url: BASE_API,
method: 'delete',
params: { userId: id },
});
},
updateStatus(id, status) {
return request({
url: BASE_API + '/updateStatus',
method: 'post',
data: { userId: id, status: status },
});
},
resetPassword(data) {
return request({
url: BASE_API + '/resetPassword',
method: 'get',
params: data,
});
},
getUserInfoById(userId) {
return request({
url: BASE_API + '',
method: 'get',
params: {
userId: userId,
},
});
},
// 保存用户角色
saveRoleIds(data) {
return request({
url: BASE_API + '/saveRoleIds',
method: 'post',
data: data,
});
},
// 修改密码
updatePassword(data) {
return request({
url: BASE_API + '/updatePassword',
method: 'put',
data: data,
});
},
};
由于没有后端服务,所以这里需要把该注释的代码屏蔽掉,比如接口的校验什么的。
src/router/permission.js
router.hasRoute(to.name): 检查一个给定名称的路由是否存在 https://router.vuejs.org/zh/guide/advanced/navigation-guards.html : 全局前置守卫
import router from '@/router';
import store from '@/store';
// 进度条
import NProgress from 'nprogress'; // 导入 nprogress模块
import 'nprogress/nprogress.css'; // 导入样式
NProgress.configure({ showSpinner: true }); // 显示右上角螺旋加载提示
// 白名单路由
const whiteList = ['/login', '/test', '/test-layout'];
// 是否存在路由
let hasRouter = false;
/**
* 全局前置守卫 https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
* next();放行 -- 其它的路由跳转在没放行之前都会走 router.beforeEach()
*/
router.beforeEach(async (to, from, next) => {
NProgress.start(); // 开启进度条
let useUserStore = store.user.useUserStore();
const { getUserInfo, logout } = useUserStore;
let { isLogin, routerList } = toRefs(useUserStore); // 响应式
if (isLogin.value) {
// 已经登录后的操作
if (to.path === '/login') {
next({ path: '/' }); // 跳转到首页
// if (to.fullPath.startsWith('/login?redirect=')) {
// let lastPath = to.fullPath.replace('/login?redirect=', '');
// next({ path: lastPath }); // 跳转到上次退出的页面
// } else {
// next({ path: '/' }); // 跳转到首页
// }
} else {
try {
if (hasRouter) {
next(); // 放行
} else {
// // 请求接口数据,动态添加可访问路由表
// await getUserInfo();
// routerList.value.forEach((e) => router.addRoute(e)); // 路由添加进去之后不会及时更新,需要重新加载一次
// // console.log('全部路由数据:', router.getRoutes());
hasRouter = true;
next({ ...to, replace: true });
}
} catch (error) {
console.log('刷新页面时获取权限异常:', error);
// ElMessage.error('错误:' + error || 'Has Error');
}
}
} else {
// 未登录
if (whiteList.indexOf(to.path) !== -1) {
next(); // 放行 -- 可以访问白名单页面(eg: 登录页面)
} else {
next(`/login?redirect=${to.path}`); // 无权限 & 白名单页面未配置 =》 跳转到登录页面
}
}
});
// 全局后置钩子
router.afterEach(() => {
NProgress.done(); // 完成进度条
});
上面代码我把后端部分禁用了
src/main.js
// 动态路由权限
import '@/router/permission.js';
安装
npm install --save nprogress
使用
src/router/permission.js
// 进度条
import NProgress from 'nprogress'; // 导入 nprogress模块
import 'nprogress/nprogress.css'; // 导入样式
NProgress.configure({ showSpinner: true }); // 显示右上角螺旋加载提示
/**
* 全局前置守卫 https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
*/
router.beforeEach(async (to, from, next) => {
NProgress.start(); // 开启进度条
});
// 全局后置钩子
router.afterEach(() => {
NProgress.done(); // 完成进度条
});
开始CV
1、把layout全部复制过去
2、App.vue改成加载layout的代码
<template>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<!-- <router-view /> -->
<layout />
</template>
<script setup>
import layout from '@/layout/index.vue';
</script>
3、store/modules/settings.js复制过去
「src/App.vue」
<template>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<!-- <router-view /> -->
<layout />
</template>
<script setup>
import layout from '@/layout/index.vue';
</script>
「src/layout/index.vue」
<template>
<!-- <h1>{{ route.meta }}</h1> -->
<div v-show="isLogin && !$route.meta.isParentView" class="flex h-full w-full">
<!-- 侧边栏菜单 -->
<sidebar v-if="isShowMenu" id="sidebar" class="w-200" />
<div class="flex-1">
<div id="top">
<!-- 顶部导航栏 -->
<navbar class="h-50" />
<!-- Tabs标签页 -->
<div :style="{ width: appMainWidth + 'px' }">
<tabs-view />
</div>
</div>
<!-- 主页面 -->
<div :style="{ height: appMainHeight + 'px', width: appMainWidth + 'px' }">
<app-main class="app-main p-10" />
</div>
</div>
</div>
<div v-if="!isLogin || (isLogin && $route.meta.isParentView)" class="h-full">
<router-view />
</div>
</template>
<script setup>
import sidebar from './components/sidebar.vue';
import navbar from './components/navbar.vue';
import appMain from './components/app-main.vue';
import tabsView from './components/tabs-view.vue';
const { proxy } = getCurrentInstance();
let { isLogin } = toRefs(proxy.$store.user.useUserStore());
let { isShowMenu } = toRefs(proxy.$store.settings.useSettingsStore());
let appMainWidth = ref(0);
let appMainHeight = ref(0);
onMounted(() => {
// 窗口宽高变化时触发 -- tips:window.onresize只能在项目内触发1次
window.onresize = function windowResize() {
calWidthAndHeight();
};
});
// 注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。
onUpdated(() => {
calWidthAndHeight();
});
watch(
[isLogin, isShowMenu],
(newValue) => {
calWidthAndHeight();
},
{ immediate: false, deep: true },
);
function calWidthAndHeight() {
let sidebarW = document.getElementById('sidebar').offsetWidth;
appMainWidth.value = window.innerWidth - sidebarW;
let topH = document.getElementById('top').offsetHeight;
appMainHeight.value = window.innerHeight - topH - 20; // 20 指 p-10
}
</script>
<style lang="scss" scoped>
.app-main {
// height: calc(100vh - 50px); // 满屏 - navbar
}
</style>
「src/layout/components/app-main.vue」
<template>
<el-scrollbar>
<!-- 路由视图 -->
<router-view />
</el-scrollbar>
</template>
<script setup></script>
<style lang="scss" scoped></style>
「src/layout/components/navbar.vue」
<template>
<!-- {{ route.meta }} -->
<div class="app flex-between-center p-x-10">
<div class="flex-center-center">
<div class="m-r-10" style="cursor: pointer" @click="proxy.$store.settings.useSettingsStore().update">
<el-icon :size="22">
<component :is="proxy.$store.settings.useSettingsStore().isShowMenu ? 'Fold' : 'Expand'" />
</el-icon>
</div>
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item :to="{ path: '/' }">home</el-breadcrumb-item>
<el-breadcrumb-item v-for="item in route.meta.breadcrumbItemList" :key="item">
<span class="text-color-grey">{{ item }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<wx-mp-account />
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<div class="flex-center-center">
<el-avatar class="" :size="32" :src="userObj.avatarUrl" />
<div class="flex-center-center">
<span class="m-l-5"> {{ userObj.nickname }} </span>
<el-icon :size="20" class="w-20">
<ArrowDown />
</el-icon>
</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/">
<el-dropdown-item>首页</el-dropdown-item>
</router-link>
<router-link to="/system/personal-center">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<a target="_blank" href="https://gitee.com/zhengqingya">
<el-dropdown-item>Gitee</el-dropdown-item>
</a>
<el-dropdown-item divided @click="logout">退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
import { ArrowRight } from '@element-plus/icons-vue';
import { getCurrentInstance, toRefs } from 'vue';
const { proxy } = getCurrentInstance();
let useUserStore = proxy.$store.user.useUserStore();
let { logout } = useUserStore;
let { userObj } = toRefs(useUserStore);
</script>
<style lang="scss" scoped>
.app {
// -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
}
</style>
src/layout/components/sidebar.vue
<template>
<el-menu router :default-active="$route.meta.fullPath" :collapse="false" :unique-opened="false" @select="handleSelect">
<el-scrollbar>
<sidebar-item :router-list="routerList" />
</el-scrollbar>
</el-menu>
</template>
<script setup>
import sidebarItem from './sidebar-item.vue';
import { getCurrentInstance, toRefs } from 'vue';
const { proxy } = getCurrentInstance();
let { routerList, routerMap } = toRefs(proxy.$store.user.useUserStore());
let { activeTabs } = proxy.$store.settings.useSettingsStore();
/**
* 选中菜单时触发
* @param index 选中菜单项的 index eg: /system/role (router 以 index 作为 path 进行路由跳转,或 router 属性直接跳转)
* @param indexPath 选中菜单项的 index path eg: ['/system', '/system/role']
* @param item 选中菜单项
* @param routeResult vue-router 的返回值(如果 router 为 true)
*/
function handleSelect(index, indexPath, item, routeResult) {
// console.log(index, indexPath, item, routeResult);
// proxy.$router.push(index);
activeTabs(routerMap.value[index]);
}
</script>
<style lang="scss" scoped>
.el-menu {
box-shadow: 1px 0 5px rgba(0, 0, 0, 0.2);
}
</style>
src/layout/components/sidebar-item.vue
<template>
<div v-for="item in routerList" :key="item.path">
<!-- 一级菜单 -->
<el-menu-item v-if="(item.meta.isShow && item.children.length === 0) || (item.children && item.children.length === 1 && !item.children[0].meta.isShow)" :index="item.meta.fullPath">
<el-icon v-if="item.meta && item.meta.icon"><component :is="item.meta.icon" /></el-icon>
<div v-else class="w-30"></div>
<template #title>{{ item.meta.title }}</template>
</el-menu-item>
<!-- 二级菜单 -->
<div v-else>
<el-sub-menu v-if="item.meta.isShow" :index="item.meta.fullPath">
<template #title>
<el-icon v-if="item.meta && item.meta.icon"><component :is="item.meta.icon" /></el-icon>
<div v-else class="w-30"></div>
<span>{{ item.meta.title }}</span>
</template>
<!-- 递归 -->
<sidebarItem :router-list="item.children" />
</el-sub-menu>
</div>
</div>
</template>
<script setup>
defineProps({
routerList: {
type: Array,
default: () => [],
},
});
</script>
<style lang="scss" scoped></style>
src/layout/components/tabs-view.vue
<template>
<div class="app">
<el-scrollbar>
<base-right-click class="flex">
<div v-for="item in tabsList" :key="item" class="item m-3" :class="{ active: $route.meta.fullPath === item.meta.fullPath }" style="display: inline-block; white-space: nowrap">
<div class="flex-between-center h-20" @click.right="handleRightClick(item, $event)">
<router-link :to="item.meta.fullPath" @click="activeTabs(item)">
<span class="m-r-3">{{ item.meta.title }}</span>
</router-link>
<el-icon v-if="item.meta.fullPath !== '/'" :size="10" @click="handleClose(item)"> <Close /> </el-icon>
</div>
</div>
<template #right-show="{ isShow }">
<div class="right-menu flex-column b-rd-5 bg-color-white">
<div class="option" @click="handleCloseCurrent">
<el-icon :size="10"> <Close /> </el-icon><span> 关闭当前</span>
</div>
<div class="option" @click="handleCloseAll">
<el-icon :size="10"> <Close /> </el-icon><span> 关闭所有</span>
</div>
</div>
</template>
</base-right-click>
</el-scrollbar>
</div>
</template>
<script setup>
const { proxy } = getCurrentInstance();
let useSettingsStore = proxy.$store.settings.useSettingsStore();
let { tabsList } = toRefs(useSettingsStore);
let { activeTabs } = useSettingsStore;
let chooseItem = $ref(null);
// 保留首页
watch(
tabsList,
(newValue) => {
if (newValue.length === 0) {
tabsList.value.push({ meta: { title: '首页', fullPath: '/' } });
}
},
{ immediate: true, deep: true },
);
function handleClose(item) {
tabsList.value.splice(tabsList.value.indexOf(item), 1);
}
function handleRightClick(item) {
chooseItem = item;
}
function handleCloseCurrent() {
handleClose(chooseItem);
}
function handleCloseAll() {
tabsList.value = [];
}
</script>
<style lang="scss" scoped>
.app {
position: relative;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
.item {
border: 1px solid #ebeef5;
&.active {
background: #00aaff;
}
}
.right-menu {
.option {
text-align: center;
padding: 5px 10px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
a {
text-decoration: none; // 去掉下换线
color: black; //文字颜色更改
}
</style>
src/store/modules/settings.js
import { defineStore } from 'pinia';
export const useSettingsStore = defineStore('settings', () => {
let isShowMenu = ref(true); // 是否显示菜单
let tabsList = ref([]); // Tabs标签页数据
function update() {
isShowMenu.value = !isShowMenu.value;
}
function getTabsList() {
const unique = (arrs) => {
const res = new Map();
return arrs.filter((arr) => !res.has(arr.meta.fullPath) && res.set(arr.meta.fullPath, 1));
};
tabsList.value = unique(tabsList.value);
return tabsList.value;
}
// 点击菜单/tabs标签页时触发
function activeTabs(router) {
// tabsList.value.forEach((e) => (e.meta.isActive = e.meta.fullPath === router.meta.fullPath));
if (tabsList.value.filter((e) => e.meta.fullPath === router.meta.fullPath).length === 0) {
// router.meta.isActive = true;
tabsList.value.push(router);
}
}
return { isShowMenu, update, tabsList, getTabsList, activeTabs };
});
vue3.4版本之后废除
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
reactivityTransform: true, // 启用响应式语法糖 $ref $computed $toRef ...
})
]
})
https://vue-macros.sxzz.moe/zh-CN/features/reactivity-transform.html
tips: store(pinia版) 中使用
$ref
无法正常持久化数据!!!
npm i -D @vue-macros/reactivity-transform
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ReactivityTransform from '@vue-macros/reactivity-transform/vite';
export default defineConfig({
plugins: [
vue(),
ReactivityTransform(), // 启用响应式语法糖 $ref ...
]
})
.eslintrc.cjs
module.exports = {
globals: { $ref: 'readonly', $computed: 'readonly', $shallowRef: 'readonly', $customRef: 'readonly', $toRef: 'readonly' },
};
原本 .value 响应式
<template>
<h1>{{ count }}</h1>
<button @click="handleClick">click</button>
</template>
<script setup>
let count = ref(0);
function handleClick() {
count.value++;
}
</script>
现在 $ref 去除 .value
<template>
<h1>{{ count }}</h1>
<button @click="handleClick">click</button>
</template>
<script setup>
let count = $ref(0);
function handleClick() {
count++;
}
</script>
$ref 在以下情况无法直接使用
src/main.js
// 自定义样式
import '@/styles/index.scss';
src/styles/index.scss
@import './variable.scss';
@import './theme.scss';
body {
background-color: #021b32;
}
vue3+scss中不要使用下面方式, 会有警告 [@vue/compiler-sfc] ::v-deep usage as a combinator has been deprecated. Use :deep(<inner-selector>) instead.
::v-deep .el-input__wrapper {
background-color: #08c0ee8c;
}
改用
::v-deep(.el-input__wrapper) {
background-color: #08c0ee8c;
}
vite版本5会出现这个问题,由 import.meta.globEager(参数) 改为 import.meta.glob(参数, {eager: true})可以解决,或者降级Vite为4.3.0
「这两段代码有区别吗」
import App from './App.vue'
createApp(App).mount('#app')
import App from './App.vue';
const app = createApp(App);
这两段代码都是用于创建和挂载 Vue 应用的,但它们在细节上有所不同。主要区别在于对 Vue 应用实例的操作方式。
第一段代码:
import App from './App.vue'
createApp(App).mount('#app')
这段代码直接在创建 Vue 应用实例后立即调用 .mount('#app')
方法来挂载应用到页面的某个元素上(通常是一个具有特定 ID 的元素)。这种方式简洁明了,适用于那些不需要对 Vue 应用实例进行额外配置或注册插件和组件的场景。
第二段代码:
import App from './App.vue';
const app = createApp(App);
这段代码首先创建了 Vue 应用实例,并将其赋值给变量 app
。这样做的好处是可以在挂载(.mount('#app')
)之前对 Vue 应用实例进行额外的操作,比如使用 .use()
方法注册插件、使用 .component()
方法全局注册组件、或者使用 .directive()
方法注册自定义指令等。这段代码没有展示挂载操作,但通常在进行了必要的配置后,你会看到类似 app.mount('#app')
的代码来完成挂载。
总结来说,第一段代码适用于简单场景,直接创建并挂载应用;第二段代码提供了更多的灵活性,允许在挂载应用之前对 Vue 实例进行配置或注册。选择哪种方式取决于你的具体需求。
1、例如:react-router已经更新到4.x版本,想要下载2.x版本,可以通过下面命令
npm install --save-dev react-router@2.8.1
或 npm install --save react-router@2.8.1
2、--save -dev
--save:将保存配置信息到package.json。默认为dependencies节点中。
--dev:将保存配置信息devDependencies节点中。
因此:
--save:将保存配置信息到package.json的dependencies节点中。
--save-dev:将保存配置信息到package.json的devDependencies节点中。
dependencies:运行时的依赖,发布后,即生产环境下还需要用的模块
devDependencies:开发时的依赖。里面的模块是开发时用的,发布时用不到它。
3、删除模块
npm uninstall 模块
删除本地模块时你应该思考的问题:是否将在package.json上的相应依赖信息也消除?
npm uninstall 模块:删除模块,但不删除模块留在package.json中的对应信息
npm uninstall 模块--save 删除模块,同时删除模块留在package.json中dependencies下的对应信息
npm uninstall 模块 --save-dev 删除模块,同时删除模块留在package.json中devDependencies下的对应信息