🧑🎓 个人主页:Silence Lamb
📖 本章内容:【基于VITE项目开发框架】
基于Vite前端快速开发框架 v1.0.0
一、基础配置
1.1🌳【基本环境】
💡
Tips
:vue-cli + vue3 + yarn + vite + ts
# vue/cli
yarn global add @vue/cli
# typescript parser
yarn add @typescript-eslint/parser --dev
# 如果没有安装yarn,需先安装yarn
npm i yarn -g
# 创建vite项目
yarn create vite vue3-vite-app --template vue-ts
# 进入项目根目录
cd vue3-vite-app
# 安装依赖
yarn install
# 启动项目
yarn dev
- 推荐使用新一代 pnpm 包管理工具,性能和速度以及 node_modules依赖管理都很优秀
💡
Tips
:建议配合 .npmrc 配置使用
# 提升一些依赖包至 node_modules
# 解决部分包模块not found的问题
# 用于配合 pnpm
shamefully-hoist = true
# node-sass 下载问题
# sass_binary_site="https://npm.taobao.org/mirrors/node-sass/"
💡Tips
:模板语法配合jsx语法,使用起来非常方便、灵活~一些必须的插件
yarn add @vitejs/plugin-legacy -S // 低版本浏览器兼容
yarn add @vitejs/plugin-vue -S // vue 支持
yarn add @vitejs/plugin-vue-jsx -S // jsx 支持
1.2🌳【代码风格】
💡
Tips
:开启Eslint的支持
yarn add eslint -D
yarn add eslint-plugin-vue -D
yarn add @typescript-eslint/eslint-plugin -D
yarn add @typescript-eslint/parser -D
💡
Tips
:**echo {}> .**eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['vue', '@typescript-eslint', 'prettier'],
rules: {
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-var': 'error',
'prettier/prettier': ['error', { trailingComma: 'es5', singleQuote: true, printWidth: 80, tabWidth: 2 }],
// 禁止出现console
'no-console': 'warn',
// 禁用debugger
'no-debugger': 'warn',
// 禁止出现重复的 case 标签
'no-duplicate-case': 'warn',
// 禁止出现空语句块
'no-empty': 'warn',
// 禁止不必要的括号
'no-extra-parens': 'off',
// 禁止对 function 声明重新赋值
'no-func-assign': 'warn',
// 禁止在 return、throw、continue 和 break 语句之后出现不可达代码
'no-unreachable': 'warn',
// 强制所有控制语句使用一致的括号风格
curly: 'warn',
// 要求 switch 语句中有 default 分支
'default-case': 'warn',
// 强制尽可能地使用点号
'dot-notation': 'warn',
// 要求使用 === 和 !==
eqeqeq: 'warn',
// 禁止 if 语句中 return 语句之后有 else 块
'no-else-return': 'warn',
// 禁止出现空函数
'no-empty-function': 'warn',
// 禁用不必要的嵌套块
'no-lone-blocks': 'warn',
// 禁止使用多个空格
'no-multi-spaces': 'warn',
// 禁止多次声明同一变量
'no-redeclare': 'warn',
// 禁止在 return 语句中使用赋值语句
'no-return-assign': 'warn',
// 禁用不必要的 return await
'no-return-await': 'warn',
// 禁止自我赋值
'no-self-assign': 'warn',
// 禁止自身比较
'no-self-compare': 'warn',
// 禁止不必要的 catch 子句
'no-useless-catch': 'warn',
// 禁止多余的 return 语句
'no-useless-return': 'warn',
// 禁止变量声明与外层作用域的变量同名
'no-shadow': 'off',
// 允许delete变量
'no-delete-var': 'off',
// 强制数组方括号中使用一致的空格
'array-bracket-spacing': 'warn',
// 强制在代码块中使用一致的大括号风格
'brace-style': 'warn',
// 强制使用骆驼拼写法命名约定
camelcase: 'warn',
// 强制使用一致的缩进
indent: 'off',
// 强制在 JSX 属性中一致地使用双引号或单引号
// 'jsx-quotes': 'warn',
// 强制可嵌套的块的最大深度4
'max-depth': 'warn',
// 强制最大行数 300
// "max-lines": ["warn", { "max": 1200 }],
// 强制函数最大代码行数 50
// 'max-lines-per-function': ['warn', { max: 70 }],
// 强制函数块最多允许的的语句数量20
'max-statements': ['warn', 100],
// 强制回调函数最大嵌套深度
'max-nested-callbacks': ['warn', 3],
// 强制函数定义中最多允许的参数数量
'max-params': ['warn', 3],
// 强制每一行中所允许的最大语句数量
'max-statements-per-line': ['warn', { max: 1 }],
// 要求方法链中每个调用都有一个换行符
'newline-per-chained-call': ['warn', { ignoreChainWithDepth: 3 }],
// 禁止 if 作为唯一的语句出现在 else 语句中
'no-lonely-if': 'warn',
// 禁止空格和 tab 的混合缩进
'no-mixed-spaces-and-tabs': 'warn',
// 禁止出现多行空行
'no-multiple-empty-lines': 'warn',
// 禁止出现;
semi: ['warn', 'never'],
// 强制在块之前使用一致的空格
'space-before-blocks': 'warn',
// 强制在 function的左括号之前使用一致的空格
// 'space-before-function-paren': ['warn', 'never'],
// 强制在圆括号内使用一致的空格
'space-in-parens': 'warn',
// 要求操作符周围有空格
'space-infix-ops': 'warn',
// 强制在一元操作符前后使用一致的空格
'space-unary-ops': 'warn',
// 强制在注释中 // 或 /* 使用一致的空格
// "spaced-comment": "warn",
// 强制在 switch 的冒号左右有空格
'switch-colon-spacing': 'warn',
// 强制箭头函数的箭头前后使用一致的空格
'arrow-spacing': 'warn',
'no-var': 'warn',
'prefer-const': 'warn',
'prefer-rest-params': 'warn',
'no-useless-escape': 'warn',
'no-irregular-whitespace': 'warn',
'no-prototype-builtins': 'warn',
'no-fallthrough': 'warn',
'no-extra-boolean-cast': 'warn',
'no-case-declarations': 'warn',
'no-async-promise-executor': 'warn',
},
}
💡
Tips
:**echo {}> **.eslintignore
# eslint 忽略检查 (根据项目需要自行添加)
node_modules
dist
1.3🌳【代码格式】
💡
Tips
:安装 prettier
# 安装 prettier
yarn add prettier -D
# 安装插件 eslint-config-prettier
yarn add eslint-config-prettier -D
💡
Tips
:echo {}> .prettierrc.js
/*
* @Description :
* @Author : SilenceLamb
* @Version : V1.0.0
*/
module.exports = {
printWidth: 150,
// 指定每个缩进级别的空格数
tabWidth: 4,
// 使用制表符而不是空格缩进行
useTabs: false,
// 在语句末尾打印分号
semi: false,
// 使用单引号而不是双引号
singleQuote: true,
// 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
quoteProps: 'as-needed',
// 在JSX中使用单引号而不是双引号
jsxSingleQuote: false,
// 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
trailingComma: 'es5',
// 在对象文字中的括号之间打印空格
bracketSpacing: true,
// 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
arrowParens: 'always',
// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
rangeStart: 0,
rangeEnd: Infinity,
// 指定要使用的解析器,不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准 always\never\preserve
proseWrap: 'preserve',
// 指定HTML文件的全局空格敏感度 css\strict\ignore
htmlWhitespaceSensitivity: 'css',
// Vue文件脚本和样式标签缩进
vueIndentScriptAndStyle: false,
// 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
endOfLine: 'lf',
}
💡
Tips
:echo {}> .prettierignore
# 忽略格式化文件 (根据项目需要自行添加)
node_modules
dist
💡
Tips
:package.json
"scripts": {
"lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx",
"prettier": "prettier --write ."
},
1.4🌳【环境变量 】
import.meta.env.MODE: {string} 应用运行的模式
import.meta.env.BASE_URL: {string} 部署应用时的基本 URL。他由base 配置项决定
import.meta.env.PROD: {boolean} 应用是否运行在生产环境
import.meta.env.DEV: {boolean} 应用是否运行在开发环境 (永远与 import.meta.env.PROD相反)
import.meta.env.SSR: {boolean} 应用是否运行在 server 上
- 🌳 .env 文件
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
- 🌳在tsconfig.json中添加"types": [ “vite/client” ]
- 用来提供import.meta.env 上 Vite 注入的环境变量的类型定义
"compilerOptions": {
"types": [ "vite/client" ]
}
💡
Tips
:另外一种配置方式
- 🌳 开发环境:.env.development
# 页面标题
VITE_APP_TITLE=开发环境
# 开发环境配置
VITE_NODE_ENV='development'
# 开发环境
VITE_APP_BASE_API='/dev-api'
# 路由懒加载
VITE_CLI_BABEL_TRANSPILE_MODULES=true
- 🌳 线上环境:.env.production
# 页面标题
VITE_APP_TITLE=线上环境
# 生产环境配置
VITE_NODE_ENV='production'
# 生产环境
VITE_APP_BASE_API='/prod-api'
# 路由懒加载
VITE_CLI_BABEL_TRANSPILE_MODULES=true
- 🌳 测试环境配置:.env.production
# 页面标题
VITE_APP_TITLE=测试环境配置
VITE_NODE_ENV=production
# 测试环境配置
VITE_NODE_ENV='staging'
# 测试环境
VITE_APP_BASE_API='/stage-api'
- 🌳为环境变量增加智能提示:src/env.d.ts
/**
* @Description : 为环境变量增加智能提示
* @Author : SilenceLamb
* @Version : V1.0.0
*/
interface ImportMetaEnv {
//定义提示信息 数据是只读的无法被修改
readonly VITE_APP_TITLE: string;
readonly VITE_NODE_ENV: string;
readonly VITE_APP_BASE_API: string;
//多个变量定义多个...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
💡
Tips
:vite.config.ts: 不能使用 import.meta.env
const ViteEnv= loadEnv(mode, process.cwd());
console.log(ViteEnv.VITE_APP_BASE_API);
console.log(ViteEnv.VITE_NODE_ENV);
💡
Tips
:package.json
"scripts": {
"dev": "vite --mode development",
"build": "vue-tsc --noEmit && vite build",
"build:prod": "vue-tsc --noEmit && vite build --mode production",
},
- 🌳为环境变量增加智能提示:src/env.d.ts
/*
* @Description : Vite相关方法
* @Author : SilenceLamb
* @Version : V1.0.0
*/
export default {
/**
* @Description : 是否是开发环境
* @param mode 模式
*/
isDev(mode: string): boolean {
return mode === 'development';
},
/**
* @Description : 是否是线上环境
* @param mode 模式
*/
isProd(mode: string | undefined): boolean {
return mode === 'production';
},
/**
* @Description : 是否是构建
* @param common 启动环境
*/
isBuild(common: string | undefined): boolean {
return common === 'build';
},
/**
* @Description : 是否是启动服务器
* @param common 启动环境
*/
isServer(common: string | undefined): boolean {
return common === 'server';
},
};
1.4🌳【别名配置】
- 例如,如果我们要导入src下的components目录中的组件,则可以这样写
import MyComponent from '@/components/MyComponent';
💡
Tips
:首先,在tsconfig.json中添加一个"paths"属性,然后以别名为键名,路径为键值
- 🌳别名配置:tsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"/@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
💡
Tips
:在项目根目录下新建config/aliases文件夹,在其中新建一个index文件
- 🌳别名配置:config/aliases/index.ts
/*
* @Description : 配置别名
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import { resolve } from 'path';
function pathResolve(dir: string) {
return resolve(process.cwd(), '.', dir);
}
export default {
// 导入时想要省略的扩展名列表
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
alias: {
'/@': pathResolve('src'),
'/#': pathResolve('config'),
},
};
💡
Tips
: 如果path找不到:npm install @types/node --save-dev
- 🌳 Vite配置:config/index.ts
/*
* @Description : Vite配置
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import buildConfig from '/#/build';
import createVitePlugins from '/#/plugins';
import resolve from '/#/aliases';
import serverConfig from '/#/server';
export default function viteConfig(isBuild: boolean, VITE_APP_BASE_API: string) {
const viteConfig = {
['resolve']: resolve,
};
return viteConfig;
}
- 🌳 vite.config.ts配置:vite.config.ts
import { UserConfig, ConfigEnv, loadEnv } from 'vite';
import viteUtils from '/@/utils/modules/vite';
import viteConfig from './config';
export default ({ command, mode }: ConfigEnv): UserConfig => {
const env = loadEnv(mode, process.cwd());
const { VITE_APP_BASE_API } = env;
const isBuild = viteUtils.isBuild(command);
//vite配置在config文件下
console.log(VITE_APP_BASE_API)
const ViteConfig = viteConfig(isBuild, VITE_APP_BASE_API);
return {
//静态资源
publicDir: 'public',
//项目基础路径
base: viteUtils.isDev(mode) ? '/' : '',
//build时 去除 console和 debugger
esbuild: {
drop: ['console', 'debugger'],
},
resolve: ViteConfig['resolve'],
};
};
二、跨域配置
2.1🍖【通用常量】
- 🍖 项目设置:config/constant/silence.ts
/*
* @Description : 项目设置
* @Author : SilenceLamb
* @Version : V1.0.0
*/
export default {
/**
* 前端端口
*/
vuePort: 80,
/**
* 后端接口
*/
adminUrl: 'http://localhost:88',
};
- 🍖 通用设置:config/constant/modules/common.ts
/*
* @Description : 通用设置
* @Author : SilenceLamb
* @Version : V1.0.0
*/
export default {
/**
* @Description : 在npm run build 或 yarn build 时 ,生成文件的目录名称
* @param {String} param -(默认dist)
*/
outputDir: 'dist',
/**
* @Description : 用于放置生成的静态资源 (js、css、img、fonts) 的
* @param {String} param -(默认static)
*/
assetsDir: 'static',
};
- 🍖 整合常量:config/constant/modules/common.ts
/*
* @Description : 整合常量
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import silence from './modules/silence';
import common from './modules/common';
const constants = {
['silence']: silence,
['common']: common,
};
export default constants;
2.2🍖【跨域配置】
- 🍖 代理目标列表:config/server/modules/index.ts
/*
* @Description : 代理目标列表
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import { ProxyOptions } from 'vite';
import constants from '/#/constant';
type ProxyTargetList = Record<string, ProxyOptions>;
export default function getProxy(VITE_APP_PROD_API) {
const proxy: ProxyTargetList = {
[VITE_APP_PROD_API]: {
target: constants['silence'].adminUrl,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${VITE_APP_PROD_API}`), ''),
},
};
return proxy;
}
- 🍖 跨域配置:config/server/index.ts
/*
* @Description : 跨域配置
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import getProxy from '/#/server/modules';
import constants from '/#/constant';
export default function serverConfig(VITE_APP_PROD_API: string) {
return {
hmr: { overlay: false }, // 禁用或配置 HMR 连接 设置 server.hmr.overlay 为 false 可以禁用服务器错误遮罩层
// 服务配置
open: true, // 类型: boolean | string在服务器启动时自动在浏览器中打开应用程序;
cors: false, // 类型: boolean | CorsOptions 为开发服务器配置 CORS。默认启用并允许任何源
host: '0.0.0.0', // IP配置,支持从IP启动
port: constants['silence'].vuePort,
proxy: getProxy(VITE_APP_PROD_API),
};
}
- 🍖 Vite配置:config/index.ts
/*
* @Description : Vite配置
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import resolve from '/#/aliases';
import serverConfig from '/#/server';
export default function viteConfig(isBuild: boolean, VITE_APP_BASE_API: string) {
return {
['resolve']: resolve,
['server']: serverConfig(VITE_APP_BASE_API),
};
}
- 🍖 vite.config.ts配置:vite.config.ts
import { UserConfig, ConfigEnv, loadEnv } from 'vite';
import viteUtils from '/@/utils/modules/vite';
import viteConfig from './config';
export default ({ command, mode }: ConfigEnv): UserConfig => {
const { VITE_APP_BASE_API } = loadEnv(mode, process.cwd());
//是否是build命令
const isBuild = viteUtils.isBuild(command);
//vite配置在config文件下
const ViteConfig = viteConfig(isBuild, VITE_APP_BASE_API);
return {
//静态资源
publicDir: 'public',
//项目基础路径
base: viteUtils.isDev(mode) ? '/' : '',
//build时 去除 console和 debugger
esbuild: {
drop: ['console', 'debugger'],
},
resolve: ViteConfig['resolve'],
//跨域配置
server: ViteConfig['server'],
};
};
三、按需加载
- 🍁首先你需要安装unplugin-vue-components 和 unplugin-auto-import这两款插件
yarn add unplugin-vue-components unplugin-auto-import -D
3.1🍁【自动引入依赖】
- 🍁自动引入依赖:config/plugins/modules/importDeps.ts
/*
* @Description : 按需加载,自动引入依赖
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import AutoImport from 'unplugin-auto-import/vite'
components/resolvers'
export default function ImportDeps() {
return AutoImport({
imports: ['vue', 'vue-router', 'vue-i18n', '@vueuse/head', '@vueuse/core'],
dts: 'src/auto/auto-imports.d.ts',
})
}
3.2🍁【自动引入组件】
- 🍁自动引入依赖:config/plugins/modules/importComp.ts
/*
* @Description : 按需加载,自动引入组件
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
export default function ImportComp() {
return Components({
// 自动导入自定义的组件
dirs: ['src/components'],
// 组件的有效文件扩展名
extensions: ['vue'],
// 搜索子目录
deep: true,
// 组件解释器
resolvers: [AntDesignVueResolver({ importStyle: 'less' })],
// 文件生成目录
dts: 'src/auto/components.d.ts',
// 允许子目录作为组件的命名空间前缀
directoryAsNamespace: false,
// 用于忽略命名空间前缀的子目录路径
// works when `directoryAsNamespace: true`
globalNamespaces: [],
// 用于转换目标的筛选器
include: [/\.vue$/, /\.vue\?vue/],
exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/],
});
}
3.3🍁【自动引入样式】
- 当你使用unplugin-vue-components来引入ui库的时候,message, notification,toast 等引入样式不生效
💡
Tips
:安装vite-plugin-style-import,实现message, notification,toast 等引入样式自动引入
yarn add vite-plugin-style-import -D
yarn add consola -D
- 🍁自动引入样式:config/plugins/modules/importStyle.ts
/*
* @Description : 自动引入样式
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import { createStyleImportPlugin, AndDesignVueResolve } from 'vite-plugin-style-import';
export default function ImportStyle() {
return createStyleImportPlugin({
resolves: [AndDesignVueResolve()],
// 自定义规则
libs: [
{
libraryName: 'ant-design-vue',
esModule: true,
resolveStyle: (name) => {
return `ant-design-vue/es/${name}/style/index`;
},
},
],
});
}
- 🍁整合插件:config/plugins/index.ts
/*
* @Description : 配置插件
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import type { Plugin } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import ImportComp from './modules/importComp';
import ImportDeps from './modules/importDeps';
import ImportStyle from './modules/importStyle';
export default function createVitePlugins(isBuild: boolean) {
const vitePlugins: (Plugin | Plugin[])[] = [];
// vue支持
vitePlugins.push(vue());
// JSX支持
vitePlugins.push(vueJsx());
// 自动按需引入依赖
vitePlugins.push(ImportDeps());
//自动按需引入组件
vitePlugins.push(ImportComp());
//自动引入样式
vitePlugins.push(ImportStyle());
return vitePlugins;
}
- 🍁vite配置:config/index.ts
/*
* @Description : Vite配置
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import createVitePlugins from '/#/plugins';
import resolve from '/#/aliases';
import serverConfig from '/#/server';
import { UserConfig } from 'vite';
import vite from '../src/utils/modules/vite';
export default function viteConfig(mode: string, command: string, ViteEnv: object): UserConfig {
const isBuild = vite.isBuild(command);
return {
['resolve']: resolve,
['server']: serverConfig(ViteEnv.VITE_APP_BASE_API),
['plugins']: createVitePlugins(isBuild),
};
}
- 🍁vite.config.ts配置:vite.config.ts
import { UserConfig, ConfigEnv, loadEnv } from 'vite';
import viteUtils from '/@/utils/modules/vite';
import viteConfig from './config';
export default ({ command, mode }: ConfigEnv): UserConfig => {
const ViteEnv = loadEnv(mode, process.cwd());
//vite配置在config文件下
const ViteConfig = viteConfig(command, mode, ViteEnv);
return {
//静态资源
publicDir: 'public',
//项目基础路径
base: viteUtils.isDev(mode) ? '/' : '',
//build时 去除 console和 debugger
esbuild: {
drop: ['console', 'debugger'],
},
resolve: ViteConfig['resolve'],
//插件配置
plugins: ViteConfig['plugins'],
//打包配置
build: ViteConfig['build'],
//跨域配置
server: ViteConfig['server'],
};
};
四、插件推荐
4.1🥑【自动重启】
💡
Tips
:通过监听文件修改,自动重启 vite 服务
- 最常用的场景就是监听 vite.config.js 和 .env.development 文件
yarn add npm i vite-plugin-restart -D
- 🥑配置插件:config/plugins/modules/autoRestartVite.ts
/*
* @Description : 自动重启Vite服务
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import ViteRestart from 'vite-plugin-restart'
export default function RestartVite() {
return ViteRestart({
restart: ['vite.config.ts'],
})
}
💡
Tips
:自定义图标:以下插件都需要进行添加
vitePlugins.push(restartVite())
4.2🥑【引入图标】
yarn add vite-plugin-svg-icons -D
- 🥑配置插件:config/plugins/modules/svgIcon.ts
/*
* @Description : 自定义SvgIcon
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import { resolve } from 'path';
function pathResolve(dir: string) {
return resolve(process.cwd(), '.', dir);
}
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
export default function svgIconPlugin() {
return createSvgIconsPlugin({
// 指定要缓存的图标文件夹
iconDirs: [pathResolve('src/assets/svg')],
// 指定符号 ID 格式
symbolId: 'icon-[dir]-[name]',
});
}
- 🥑SvgIcon组件:src/components/SvgIcon/index.vue
/*
* @Description :
* @Author : SilenceLamb
* @Version : V1.0.0
/*
* @Description : 自定义图标
* @Author : SilenceLamb
* @Version : V1.0.0
*/
<template>
<svg class="svg-icon" aria-hidden="true">
<use :href="symbolId" aria-hidden="true" />
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
prefix: {
type: String,
default: 'icon',
},
name: {
type: String,
required: true,
},
});
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.3em;
/*因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果*/
fill: currentColor;
/*定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承*/
overflow: hidden;
}
</style>
<svg-icon name="order" class="icon" />
- 🥑配置插件:config/plugins/index.ts
/*
* @Description : 配置插件
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import type { Plugin } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import ImportComp from './modules/importComp';
import ImportDeps from './modules/importDeps';
import ImportStyle from './modules/importStyle';
import RestartVite from './modules/restartVite';
import SvgIcons from './modules/svgIcons';
import GZIPCompression from '/#/plugins/modules/compression';
// @ts-ignore
export default function createVitePlugins(isBuild: boolean) {
const vitePlugins: (Plugin | Plugin[])[] = [];
// vue支持
vitePlugins.push(vue());
// JSX支持
vitePlugins.push(vueJsx());
// 自动按需引入依赖
vitePlugins.push(ImportDeps());
//自动按需引入组件
vitePlugins.push(ImportComp());
//自动引入样式
vitePlugins.push(ImportStyle());
//自动重启Vite服务
vitePlugins.push(RestartVite());
//自定义图标
vitePlugins.push(SvgIcons());
return vitePlugins;
}
五、打包配置
5.1⏰【Build配置】
- ⏰SvgIcon组件:config/build/index.ts
/*
* @Description : Build配置
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import { resolve } from 'path';
import constants from "/#/constant";
function pathResolve(dir: string) {
return resolve(process.cwd(), '.', dir);
}
export default {
//浏览器兼容性 "esnext"|"modules"
target: 'modules',
//启用/禁用 CSS 代码拆分
cssCodeSplit: true,
sourcemap: false,
assetsInlineLimit: 10240,
//生成静态资源的存放路径
assetsDir: constants['common'].assetsDir,
//打包文件生成位置
outDir: constants['common'].outputDir,
emptyOutDir: true,
rollupOptions: {
input: {
main: pathResolve('index.html'),
},
output: {
entryFileNames: `js/[name]-[hash].js`,
chunkFileNames: `js/[name]-[hash].js`,
assetFileNames: `[ext]/[name]-[hash].[ext]`,
},
},
};
/*
* build.target: 项目支持的浏览器目标
* build.polyfillDynamicImport: 是否动态的引入polyfill
* build.outDir: 输出的文件夹
* build.assetsDir: 指定生成静态资源的存放路径
* build.assetsInlineLimit: 小于这个大小的资源,以base64形式进行编码
* build.cssCodeSplit: 对css文件进行拆分,如果禁用,这个项目只有一个css文件
* build.sourceMap: 源文件映射,方便调试
* build.rollupOptions: 把options传给rollup
* build.commonjsOptions: 同上
* build.lib: 导出的是库模式,不是应用的模式
* build.manifest: 生成一个manifest文件。
* build.emptyOutDir: 构建的时候,是否主动清空目录
* build.chunkSizeWarningLimit: 如果打包的文件超过500k,就会给一个警告
* build.watch: 文件变化是否重新编译
*/
- ⏰Vite配置:config/index.ts
/*
* @Description : Vite配置
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import buildConfig from '/#/build';
import createVitePlugins from '/#/plugins';
import resolve from '/#/aliases';
import serverConfig from '/#/server';
import vite from '../src/utils/modules/vite';
import vite from '../src/utils/modules/vite';
export default function viteConfig(mode: string, command: string, ViteEnv: object): UserConfig {
const isBuild = vite.isBuild(command);
return {
['resolve']: resolve,
['build']: buildConfig,
['server']: serverConfig(VITE_APP_BASE_API),
['plugins']: createVitePlugins(isBuild),
};
}
- ⏰vite.config.ts配置:vite.config.ts
import { UserConfig, ConfigEnv, loadEnv } from 'vite';
import viteUtils from '/@/utils/modules/vite';
import viteConfig from './config';
export default ({ command, mode }: ConfigEnv): UserConfig => {
const ViteEnv = loadEnv(mode, process.cwd());
//vite配置在config文件下
const ViteConfig = viteConfig(mode, command, ViteEnv);
return {
//静态资源
publicDir: 'public',
//项目基础路径
base: viteUtils.isDev(mode) ? '/' : '',
//build时 去除 console和 debugger
esbuild: {
drop: ['console', 'debugger'],
},
resolve: ViteConfig['resolve'],
//插件配置
plugins: ViteConfig['plugins'],
//打包配置
build: ViteConfig['build'],
//跨域配置
server: ViteConfig['server'],
};
};
5.2⏰【GZIP压缩】
- ⏰GZIP压缩:config/plugins/modules/compression.ts
/*
* @Description : GZIP压缩
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import viteCompression from 'vite-plugin-compression';
export default function GZIPCompression(isBuild: boolean) {
return viteCompression({
verbose: true, // //是否在控制台输出压缩结果,默认为 true
disable: !isBuild, //是否禁用压缩,默认为 false
deleteOriginFile: isBuild, //压缩后是否删除原文件,默认为 false
threshold: 0.8, //启用压缩的文件大小限制,单位是字节,默认为 0
algorithm: 'gzip', //采用的压缩算法,默认是 gzip
ext: 'gz', //生成的压缩包后缀
});
}
- ⏰配置插件:config/plugins/index.ts
/*
* @Description : 配置插件
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import type { Plugin } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import ImportComp from './modules/importComp';
import ImportDeps from './modules/importDeps';
import ImportStyle from './modules/importStyle';
import RestartVite from './modules/restartVite';
import SvgIcons from './modules/svgIcons';
import GZIPCompression from '/#/plugins/modules/compression';
export default function createVitePlugins(isBuild: boolean) {
const vitePlugins: (Plugin | Plugin[])[] = [];
// vue支持
vitePlugins.push(vue());
// JSX支持
vitePlugins.push(vueJsx());
// 自动按需引入依赖
vitePlugins.push(ImportDeps());
//自动按需引入组件
vitePlugins.push(ImportComp());
//自定义图标
vitePlugins.push(SvgIcons());
//自动引入样式
vitePlugins.push(ImportStyle());
//自动重启Vite服务
vitePlugins.push(RestartVite());
//GIZP压缩
vitePlugins.push(GZIPCompression(isBuild));
return vitePlugins;
}
六、样式重置
- ✅ 安装依赖:sass sass-loader
npm install sass sass-loader -D
- 🌍样式整合:src/styles/index.scss
/*样式重置*/
@import 'normalize.css';
/*通用混入*/
@import './base';
/*全局过度*/
@import './common';
- 🌍全局引入:src/main.ts
import './styles/index.scss'; //全局样式
6.1🌍【基础样式】
- 🌍基础样式:src/styles/base/index.scss
/*通用混入*/
@import 'mixin';
/*全局过度*/
@import 'transition';
/*全局样式*/
@import 'global';
- 🌍混入样式:src/styles/base/mixin/index.scss
/*清除默认样式*/
@mixin resetStyles {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none !important;
}
}
/*清楚浮动*/
@mixin clearfix {
&:after {
content: '';
display: table;
clear: both;
}
}
/*相对大小*/
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
/*更改按钮颜色*/
@mixin colorBtn($color) {
background: $color;
&:hover {
color: $color;
&:before,
&:after {
background: $color;
}
}
}
/*添加背景图片*/
@mixin bg-image($url) {
background-image: url($url + '@2x.png');
@media (-webkit-min-device-pixel-ratio: 3), (min-device-pixel-ratio: 3) {
background-image: url($url + '@3x.png');
}
}
/*文本不换行*/
@mixin no-wrap() {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
/*多行文本溢出*/
@mixin multiEllipsis($line: 2) {
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: $line;
-webkit-box-orient: vertical;
}
/*透明度*/
@mixin opacity($opacity) {
opacity: $opacity;
$opacity-ie: $opacity * 100;
filter: alpha(opacity=$opacity-ie); //IE8
}
/*美化文本的选中*/
@mixin beauty-select($color, $bgcolor) {
&::selection {
color: $color;
background-color: $bgcolor;
}
}
/*毛玻璃效果*/
@mixin blur($blur: 10px) {
-webkit-filter: blur($blur);
-moz-filter: blur($blur);
-o-filter: blur($blur);
-ms-filter: blur($blur);
filter: progid:DXImageTransform.Microsoft.Blur(PixelRadius='${blur}');
filter: blur($blur);
*zoom: 1;
}
/* 滤镜: 将彩色照片显示为黑白照片、保留图片层次*/
@mixin grayscale() {
-webkit-filter: grayscale(100%);
-moz-filter: grayscale(100%);
-ms-filter: grayscale(100%);
-o-filter: grayscale(100%);
filter: grayscale(100%);
}
/*文本居中*/
@mixin center($height: 100%) {
height: $height;
line-height: $height;
text-align: center;
}
/*修改背景色等*/
@mixin background($bg-color: #5f6970, $color: #000000, $font-weight: 400) {
color: $color;
font-weight: $font-weight;
background-color: $bg-color;
}
/*鼠标hover显示下划线*/
@mixin hoverLine($height: 2px, $color: #ffffff) {
position: relative;
&:hover::after {
content: '';
position: absolute;
height: $height;
width: 100%;
background-color: $color;
bottom: 0;
left: 0;
}
}
/*滚动条*/
@mixin scrollBar($width: 6px, $height: 6px, $trackBg: rgba(255, 255, 255, 0), $thumbBg: #808080) {
/*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
&::-webkit-scrollbar {
width: $width;
height: $height;
/* 防止遮挡内容 */
padding-right: 20px;
margin: 50px auto;
overflow: hidden;
}
/*滚动条的轨道的两端按钮,允许通过点击微调小方块的位置*/
&::-webkit-scrollbar-button {
display: none;
}
/*边角,即两个滚动条的交汇处*/
&::-webkit-scrollbar-corner {
display: none;
}
/*定义滚动条轨道 内阴影+圆角*/
&::-webkit-scrollbar-track {
border-radius: 30px; /*轨道背景区域的圆角*/
background-color: $trackBg; /*轨道的背景颜色*/
}
/*定义滑块 内阴影+圆角*/
&::-webkit-scrollbar-thumb {
border-radius: 12px;
/*
padding-box 表示背景裁剪到内边距框,
这里也可以用 content-box,表示裁剪到内容框,
默认为 border-box,表示裁剪到边框
*/
background-clip: padding-box;
border-color: transparent;
background: $thumbBg; /*滑块背景颜色*/
}
/* 悬浮更换滚动条颜色 */
&::-webkit-scrollbar-thumb:hover {
background: #493131;
border-width: 0;
}
}
/* 文本可点击 */
@mixin clickable {
-webkit-user-select: auto;
-moz-user-select: auto;
-ms-user-select: auto;
user-select: auto;
}
/*文本不可点击*/
@mixin noClickable {
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard syntax */
}
@mixin pct($pct) {
width: #{$pct};
position: relative;
margin: 0 auto;
}
@mixin triangle($width, $height, $color, $direction) {
$width: $width/2;
$color-border-style: $height solid $color;
$transparent-border-style: $width solid transparent;
height: 0;
width: 0;
@if $direction==up {
border-bottom: $color-border-style;
border-left: $transparent-border-style;
border-right: $transparent-border-style;
} @else if $direction==right {
border-left: $color-border-style;
border-top: $transparent-border-style;
border-bottom: $transparent-border-style;
} @else if $direction==down {
border-top: $color-border-style;
border-left: $transparent-border-style;
border-right: $transparent-border-style;
} @else if $direction==left {
border-right: $color-border-style;
border-top: $transparent-border-style;
border-bottom: $transparent-border-style;
}
}
6.2🌍【全局过度】
- 🌍样式重置:src/styles/base/transition/index.scss
// 全局过度 css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all 0.5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all 0.5s;
}
.breadcrumb-leave-active {
position: absolute;
}
6.3🌍【CSS 框架】
- 🌍通过 npm 安装 Tailwind
yarn add tailwindcss@latest -D
yarn add postcss@latest-D
yarn add autoprefixer@latest -D
- 🌍创建您的配置文件
npx tailwindcss init -p
- 🌍 配置文件:postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
- 🌍 配置文件:tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
// 配置 Tailwind 来移除生产环境下没有使用到的样式声明
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
content: [],
theme: {
extend: {},
},
plugins: [],
}
- 🌍 在您的 CSS 中引入 Tailwind
@tailwind base;
@tailwind components;
@tailwind utilities;
七、状态管理
- 🤡【官方文档】https://pinia.vuejs.org
💡
Tips
:pinia-plugin-persistedstate 插件可以使 Pinia 存储的持久性变得更简单和可配置
- 类似于 vuex-persistedstate 的 API
- 单个 Store 的配置
- 自定义存储方式和自定义序列化数据
7.1🤡【基本配置】
💡
Tips
:安装依赖
yarn add pinia@next
yarn add pinia-plugin-persistedstate
💡
Tips
:src文件夹下新建store文件夹,store文件夹下新建index.ts
- 🤡 vuex-persistedstate:src/store/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedState from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedState)
export default pinia
- 🤡 main.ts:main.ts
import { createApp } from 'vue'
import App from './App.vue'
import pinia from "/@/store"; //状态管理
const app = createApp(App)
app.use(pinia)
app.mount('#app')
💡
Tips
:store文件夹下新建modules文件夹:用于模块化管理
- 🤡定义一个 Store:src/store/modules/test.ts - 未持久化
/**
* @Description : Pinia示例-未持久化
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
- √🤡定义一个 Store:src/store/modules/test.ts - 持久化
/**
* @Description : Pinia示例-持久化
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
}, {
persist: true,
})
💡
Tips
:当点击 increment 按钮时,响应属性 count 和计算属性 doubleCount 都会改变
- 刷新页面后可以看到,它们也都显示为刷新之前的数据,说明数据已经被持久化了
<template>
{{ countStore.count }}
<hr>
{{ countStore.doubleCount }}
<hr>
<button @click="countStore.increment">increment</button>
</template>
<script setup>
import { useCounterStore } from "../../store/modules/user"
const countStore = useCounterStore()
</script>
💡
Tips
:插件预先配置了以下内容 - 🚀参考链接
- localStorage 作为存储
- store.$id 作为存储的默认键,即 defineStore 的第一个参数
- JSON.stringify/JSON.parse 作为序列化器/反序列化器
- 所有属性都会被持久化到本地存储中
7.2🤡【基本示例】
🤡
创建三个文件夹
:模块化管理
- 🤡Pinia - 持久键值:src/store/constant/index.ts
/**
* @Description : Pinia-持久性Key
* @Author : SilenceLamb
* @Version : V1.0.0
*/
export const ADMIN_INFO = 'AdminInfo';
- 🤡Store - 变量类型:src/store/interface/index.ts
/**
* @Description : store-变量类型
* @Author : SilenceLamb
* @Version : V1.0.0
*/
export interface AdminInfo {
userName: string;
nickName: string;
avatar: string;
token: string;
refreshToken: string;
}
- 🤡Pinia - 实例管理(Option Store):src/store/modules/adminInfo.ts
/**
* @Description : Pinia示例
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import { defineStore } from 'pinia';
import { ADMIN_INFO } from '/@/store/constant';
import { AdminInfo } from '/@/store/interface';
export const useAdminInfoState = defineStore({
id: 'AdminInfo',
state: (): AdminInfo => {
return {
userName: 'userName',
nickName: 'nickName',
avatar: 'avatar',
token: 'tokena',
refreshToken: 'refreshToken',
};
},
actions: {
/**
* @Description :设置state全部值
* @param state
*/
setState(state: AdminInfo) {
this.$state = { ...this.$state, ...state };
},
/**
* @Description : 通过键设置值
* @param value 值
* @param key state的键
*/
setStateByKey(key: any, value: string) {
this[key] = value;
},
/**
* 设置Token
* @param value token
* @param key state的键
*/
setToken(key: 'token' | 'refreshToken', value: string) {
this[key] = value;
},
/**
* 获取Token
* @param key state的键
*/
getToken(key: 'auth' | 'refresh' = 'auth') {
return key === 'auth' ? this.token : this.refreshToken;
},
/**
* @Description : 移除Token
*/
removeToken() {
this.token = '';
this.refreshToken = '';
},
},
persist: {
key: ADMIN_INFO,
},
});
7.3🤡【基本使用】
🤡直接获取:获取state
<template>
<div>{{useAdminInfo.userName}}</div>
<div>{{useAdminInfo.nickName}}</div>
<div>{{useAdminInfo.avatar}}</div>
<div>{{useAdminInfo.token}}</div>
</template>
<script setup lang="ts">
import { useAdminInfoState } from '/@/store/modules/adminInfo'
const useAdminInfo = useAdminInfoState()
</script>
🤡computed获取:获取state
<template>
<div>{{userName}}</div>
</template>
<script setup lang="ts">
import { useAdminInfoState } from '/@/store/modules/adminInfo'
const useAdminInfo = useAdminInfoState()
//响应式的
const userName=computed(()=>useAdminInfo.userName)
</script>
🤡直接使用:直接使用getters
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注 ✨
return this.doubleCount + 1
},
},
})
<script setup>
const store = useCounterStore()
store.count = 3
store.doubleCount // 6
</script>
🤡直接使用:直接使用getters
<script setup lang="ts">
import { useAdminInfoState } from '/@/store/modules/adminInfo'
const useAdminInfo = useAdminInfoState()
useAdminInfo.getToken()
</script>
7.4🤡【定义插件】
- 为 store 添加新的属性
- 定义 store 时增加新的选项
- 为 store 增加新的方法
- 包装现有的方法
- 改变甚至取消 action
- 实现副作用,如本地存储
- 仅应用插件于特定 store
🤡 Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性。它接收一个可选参数,即 context。
export function myPiniaPlugin(context) {
context.pinia // 用 `createPinia()` 创建的 pinia。
context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。
context.store // 该插件想扩展的 store
context.options // 定义传给 `defineStore()` 的 store 的可选对象。
// ...
}
- 然后用 pinia.use() 将这个函数传给 pinia:
pinia.use(myPiniaPlugin)
🤡 自定义持久化插件
- 🤡简洁版持久化插件:src/store/plugin/persistence.ts
/**
* @Description : 简洁版持久化插件:使用storeId作为Key进行持久化
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import {PiniaPluginContext} from "pinia";
export function persistenceState(context:PiniaPluginContext) {
/**
* @Description : 每次启动时都执行
* 根据stateId从localStorage获取数据
*/
const currentState = JSON.parse(localStorage.getItem(context.store.$id)||'{}')
context.store.$patch(currentState)
/**
* @Description : 每次state发生变化时执行
* @param {state} : oldState 原来的数据
* @param {state} : newState 修改的数据
* 都要把state保存到localStorage里面
*/
context.store.$subscribe((oldState,newState)=>{
console.log(newState)
localStorage.setItem(oldState.storeId, JSON.stringify(newState))
},{
detached : true
})
}
- 🤡使用持久化插件:src/store/index.ts
/**
* @Description : Pinia 存储的持久性
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import { createPinia } from 'pinia';
import {persistenceState} from "/@/store/plugin/persistence";
const pinia = createPinia();
pinia.use(persistenceState);
export default pinia;
八、路由配置
- 🍑 官方链接:https://router.vuejs.org
8.1🍑【路由配置】
- 🍑 yarn命令安装
yarn add vue-router@4
💡
Tips
:src文件夹下新建router文件夹,router文件夹下新建static.ts
- 🍑路由配置:src/router/static.ts
/**
* @Description : 路由配置
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import { RouteRecordRaw } from 'vue-router';
export const constantRoutes: RouteRecordRaw [] = [
{
path: '/',
name: 'Index',
component: () => import('/@/views/index/index.vue'),
},
];
export default {constantRoutes};
💡
Tips
:在main.ts中,引入router并注册
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
const app = createApp(App)
app.use(router)
app.mount('#app')
💡
Tips
:在App.vue中设置路由展现出口
<template>
<router-view />
</template>
8.2🍑【路由守卫】
💡
Tips
:src文件夹下新建router文件夹,router文件夹下新建index.ts
- 🍑简单的路由守卫:src/router/index.ts
/**
* @Description : 路由守卫
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import {createRouter, createWebHistory} from 'vue-router';
import {constantRoutes} from './static';
import {useAdminInfoState} from '/@/store/modules/adminInfo';
//引入进度条样式
import 'nprogress/nprogress.css';
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes,
});
/**
* @Description 全局前置守卫
* 当一个导航触发时,全局前置守卫按照创建顺序调用
* @param to: 即将要进入的目标 用一种标准化的方式
* @param from: 当前导航正要离开的路由 用一种标准化的方式
* @return {boolean} false: 取消当前的导航 |true 通过一个路由地址跳转到一个不同的地址
*/
/* 添加白名单*/
const whiteList = ['/login']; // no redirect whitelist
// @ts-ignore
router.beforeEach((to, from, next) => {
console.log(useAdminInfoState().getToken())
if (useAdminInfoState().getToken()) {
/* has token*/
if (to.path === '/login') {
next({path: '/'})
} else if (useAdminInfoState().userName === null) {
//获取用户信息
} else {
next()
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
}
}
})
/**
* @Description 全局后置钩子
* 钩子不会接受 next 函数也不会改变导航本身
*/
router.afterEach(() => {
});
export default router;
8.3🍑【组件跳转】
- 🍑 所以我们不能再直接访问 this. r o u t e r 或 t h i s . router 或 this. router或this.route
- 🍑作为替代,我们使用 useRouter 和 useRoute 函数
import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
function pushWithQuery(query) {
router.push({
name: 'search',
query: {
...route.query,
},
})
}
},
}
8.4🍑【组件守卫】
💡
Tips
:组件内路由守卫
- 🍑新增组合式api:可以替代原有的组件内守卫
- onBeforeRouteLeave(离开当前页面路由时触发)
- onBeforeRouteUpdate(路由更新时触发)
💡
Tips
你可以在路由组件内直接定义路由导航守卫(传递给路由配置的)
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
- 🍑页面使用
import { useRouter,useRoute } from 'vue-router';
const go=()=>{
const Router=useRouter()
const Route=useRoute()
Router.push({
name:'/login',
query:{
id:'123456'
}
})
}
8.5🍑【过渡动效】
- 🍑整个布局过度动效:src/router/static.ts
<router-view v-slot="{ Component }">
<transition name="fade">
<component :is="Component" />
</transition>
</router-view>
- 🍑单个路由的过渡:src/router/static.ts
const routes = [
{
path: '/custom-transition',
component: PanelLeft,
meta: { transition: 'slide-left' },
},
{
path: '/other-transition',
component: PanelRight,
meta: { transition: 'slide-right' },
},
]
- 🍑 你可以将元信息和动态的 name 结合在一起,放在 < transition > 上
<router-view v-slot="{ Component, route }">
<!-- 使用任何自定义过渡和回退到 `fade` -->
<transition :name="route.meta.transition || 'fade'">
<component :is="Component" />
</transition>
</router-view>
九、封装工具类
9.1🛞【基本配置】
💡
Tips
:在src文件下创建utils,文件夹下新建index.ts文件夹:用于扫描ts文件
/*
* @Description :
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import fileNameUtils from './modules/fileName';
export default {
install(app) {
const modules = import.meta.glob('./modules/*.ts', { eager: true });
for (const path in modules) {
const method: any = modules[path],
fileName = fileNameUtils.getFileName(path);
if (method.default !== undefined) {
Object.keys(method).forEach(() => {
app.config.globalProperties['$' + fileName] = method.default;
});
} else {
// 将该方法注册为 $fileName 的全局方法
app.config.globalProperties['$' + fileName] = method;
}
}
},
};
💡
Tips
:main.ts全局引入
- 🛞 main.ts:main.ts
import { createApp } from 'vue'
import App from './App.vue'
import utils from '/@/utils';
const app = createApp(App)
app.use(utils,true)
app.use(router)
app.mount('#app')
9.2🛞【基本使用】
🛞 Tips:app.use(utils,true)
- 🛞 < script setup lang=“ts” >
<template>
<div class="app-container">
<a-button @click="message">message</a-button>
</div>
</template>
<script setup lang="ts">
const model = inject('model');
function message() {
model.message('消息提示', 'success');
}
</script>
- 🛞 < script lang=“ts” >
<template >
<div class="app-container">
<a-button @click="message">message</a-button>
</div>
</template>
<script lang="ts">
export default{
setup(){
const model = inject('model');
return {model}
},
methods:{
message(){
this.model.message('消息提示', 'success');
}
}
}
</script>
💡
Tips
:🛞app.use(utils,false)
<template >
<div class="app-container">
<a-button @click="message">message</a-button>
</div>
</template>
<script lang="ts">
export default{
methods:{
message(){
this.$model.message('消息提示', 'success');
}
}
}
</script>
9.3🛞【常用工具】
🛞
Tips
:本地缓存 | 会话缓存:src/utils/modules/cache.ts
/*
* @Description : 本地缓存 | 会话缓存
* @Author : SilenceLamb
* @Version : V1.0.0
*/
/**
* window.localStorage
* @method set 设置
* @method get 获取
* @method remove 移除
* @method clear 移除全部
*/
export const Local = {
set(key: string, val: any) {
window.localStorage.setItem(key, JSON.stringify(val));
},
get(key: string) {
const json: any = window.localStorage.getItem(key);
return JSON.parse(json);
},
remove(key: string) {
window.localStorage.removeItem(key);
},
clear() {
window.localStorage.clear();
},
};
/**
* window.sessionStorage
* @method set 设置会话缓存
* @method get 获取会话缓存
* @method remove 移除会话缓存
* @method clear 移除全部会话缓存
*/
export const Session = {
set(key: string, val: any) {
window.sessionStorage.setItem(key, JSON.stringify(val));
},
get(key: string) {
const json: any = window.sessionStorage.getItem(key);
return JSON.parse(json);
},
remove(key: string) {
window.sessionStorage.removeItem(key);
},
clear() {
window.sessionStorage.clear();
},
};
🛞
Tips
:消息提示:src/utils/modules/model.ts
/*
* @Description : 用于消息提示、确认消息和提交内容
* @Version : V1.0.0
* @Author : SilenceLamb
*/
import { message, notification, Modal, Spin } from 'ant-design-vue';
let loadingInstance;
export default {
// 消息提示
message(content, type: 'error' | 'success' | 'warning' | 'info') {
message[type](content).then();
},
// 弹出提示 -(弹框)
alert(title: string, content: any, type: 'error' | 'success' | 'warning' | 'info') {
Modal[type]({
title: title,
content: content,
});
},
// 通知提示 -(通知)
notify(title: any, content: any, type: 'error' | 'success' | 'warning' | 'info') {
notification[type]({
message: title,
description: content,
});
},
// 确认窗体 -(窗体)
confirm(title: any, content: any) {
return Modal.confirm({
title: title,
content: content,
okText: '确认',
cancelText: '取消',
type: 'warning',
});
},
};
🛞
Tips
:参数处理:src/utils/modules/params.ts
/*
* @Description : 参数处理工具
* @Author : SilenceLamb
* @Version : V1.0.0
*/
export default {
/**
* @Description :请求参数处理
* @param params
* @return {string}
*/
tansParams(params:any):any {
let result = ''
for (const propName of Object.keys(params)) {
const value = params[propName]
const part = `${encodeURIComponent(propName)}=`
if (value !== null && value !== '' && typeof value !== 'undefined') {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
// eslint-disable-next-line max-depth
if (value[key] !== null && value[key] !== '' && typeof value[key] !== 'undefined') {
const params = `${propName}[${key}]`
const subPart = `${encodeURIComponent(params)}=`
result += `${subPart + encodeURIComponent(value[key])}&`
}
}
} else {
result += `${part + encodeURIComponent(value)}&`
}
}
}
return result
},
}
🛞
Tips
:Token工具:src/utils/modules/token.ts
/*
* @Description : Token管理工具
* @Author : SilenceLamb
* @Version : V1.0.0
*/
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
export default {
/**
* @Description 获取Token
* @returns token
*/
getToken() {
return Cookies.get(TokenKey)
},
/**
* @Description 设置Token
* @param {*} token
*/
setToken(token) {
return Cookies.set(TokenKey, token)
},
/**
* @Description 移除Token
*/
removeToken() {
return Cookies.remove(TokenKey)
},
}
🛞
Tips
:获取文件名:src/utils/modules/fileName.ts
/*
* @Description : 获取文件名工具
* @Author : SilenceLamb
* @Version : V1.0.0
*/
export default {
/**
* @Description 获取最后一个文件夹
* @param {string} filePath
* @returns {string} fileName 子文件夹名
*/
getLastFileName(filePath) {
const matches = filePath.substring(0, filePath.lastIndexOf('/')).split('/').pop();
return matches;
},
/**
* @Description 获取文件名
* @param {string} filePath
* @returns {string} fileName 文件名
*/
getFileName(filePath) {
// 将路径拆分为数组
const parts = filePath.split('/'),
// 获取数组中的最后一个元素
fileName = parts[parts.length - 1];
// 获取文件名并返回
return fileName.replace('.ts', '');
},
};
十、Layout布局
✨
Tips
:Layout布局:src/layout/index.vue
<template>
<div class="app-wrapper">
<app-main />
<back-to-top />
</div>
</template>
<style lang="scss" scoped>
@import 'src/styles/index';
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
}
</style>
✨
Tips
:主要区域组件:src/layout/components/appMain/index.vue
<template>
<section class="app-main">
<router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in">
<component :is="Component" :key="$route.path" />
</transition>
</router-view>
</section>
</template>
<script>
export default { name: 'AppMain' };
</script>
<style lang="scss" scoped>
.app-main {
/*50 = navbar */
min-height: 100vh;
width: 100%;
position: relative;
overflow: hidden;
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>
✨
Tips
:路由引入Layout布局:
import Layout from '/@/layout/index.vue';
{
path: '/',
redirect: '/index',
component: Layout,
children: [{ name: 'index', path: '/index', component: () => import('/@/views/index/index.vue') }],
},