码云地址:
https://gitee.com/guorui999/vue3-vite.git
一、创建项目
1. 安装vite
npm i vite -g
2. 创建项目
- 一步创建
# npm 6.x
npm create vite@latest my-vue-app --template vue-ts
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue-ts
# yarn
yarn create vite my-vue-app --template vue-ts
# pnpm
pnpm create vite my-vue-app --template vue-ts
- 配置创建
npm init vue@latest
如果安装依赖后运行 npm run dev 报以下错误
解决方法: 更新node版本
或者安装nvm切换node版本
3. vite常见报错及解决方法
- 无法找到模块“../views/HomeView.vue”的声明文件。
解决方法
// env.d.ts添加代码
declare module '*.vue' {
import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
// 或者
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: ComponentOptions | ComponentOptions['setup']
export default component
}
- 无法找到模块“three”的声明文件
解决方法
安装type文件 npm i --save-dev @types/three
// env.d.ts添加代码
declare module 'three';
- 打包图片路径报错
解决方法
// 如果是背景图片引入的方式(一定要使用相对路径)
background-image: url('../../assets/images/background.png')
// 如果是template里面使用动态图片, 一定要import 或者require进来
import icon from '@/assets/images/logo.png'
const data=[{name:'text',icon}]
// 或者
const data=[{name:'text',icon:new URL('@/assets/images/logo.png',import.meta.url).href}]
- require is not defind
解决办法
// 1. 安装依赖 npm install @type/node --save-dev
// 2. 使用 new URL('@/assets/images/logo.png',import.meta.url).href 代替 require('')
- window声明
解决方法
// window.d.ts
declare global {
interface Window {
map: WebGIS.Map;
}
}
export {};
- unplugin-vue-components自动按需引入组件, 并生成声明文件components.d.ts
babel-plugin-import 可以实现按需引入进行按需加载,加入这个插件后,可以省去 style 的引入, 但需要手动引入
而
unplugin-vue-components
可以不需要手动引入组件,能够让开发者就像全局组件那样进行开发,但实际上又是按需引入,且不限制打包工具,不需要使用babel
解决方法
// vite.config.ts
export default defineConfig((/* { mode } */) => {
const plugins: PluginOption[] = [];
plugins.push(
// @ts-ignore
Components({
dts: true,
resolvers: [
AntDesignVueResolver({
importStyle: false, // css in js
}),
],
}),
);
})
// tsconfig.json
{
"files": ["./components.d.ts"],
}
- 路径:找不到模块“@com”或其相应的类型声明
解决方法
// vite.config.ts
{
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
// 图片文件夹
'@img': fileURLToPath(new URL('./src/assets/images', import.meta.url)),
// 公共组件
'@com': fileURLToPath(new URL('./src/components', import.meta.url)),
}
}
}
// tsconfig.json
{
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@com/*": ["./src/components/*"],
"@com": ["./src/components/index"], // @com如果想默认引入components下的index, 需要这样配置
"@img/*": ["./src/assets/images/*"]
}
}
}
二、项目基本配置
1. 项目icon
在 public目录 下,添加一个 favicon.icon 图片
2. 项目标题
在 index.html 文件的 title标签 中配置
3. 配置 tsconfig.json
能让 代码提示 变得更加友好
{
"compilerOptions": {
// 启用项目合成模式。在项目合成模式下,TypeScript 编译器可以更快地增量编译大型项目
"composite": true,
// 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。
"allowSyntheticDefaultImports": true,
// 解析非相对模块名的基准目录
"baseUrl": ".",
// 模块加载兼容模式,可以使用import from语法导入commonJS模块
"esModuleInterop": true,
// 从 tslib 导入辅助工具函数(比如 __extends, __rest等)
"importHelpers": true,
// 指定生成哪个模块系统代码
"module": "esnext",
// 决定如何处理模块。
"moduleResolution": "node",
// 启用所有严格类型检查选项。
// 启用 --strict相当于启用 --noImplicitAny, --noImplicitThis, --alwaysStrict,
// --strictNullChecks和 --strictFunctionTypes和--strictPropertyInitialization。
"strict": true,
"noImplicitAny": false, // 在表达式和声明上有隐含的 any类型时不报错
"noUnusedLocals": false, // 有未使用的变量时,不抛出错误
"noUnusedParameters": false, // 有未使用的参数时,不抛出错误
// 支持jsx语法
"jsx": "preserve",
// 生成相应的 .map文件。
"sourceMap": true,
// 忽略所有的声明文件( *.d.ts)的类型检查。
"skipLibCheck": true,
// 指定ECMAScript目标版本
"target": "esnext",
// 要包含的类型声明文件名列表
"types": [
"node"
],
"typeRoots": [
"./node_modules/@types"
],
// isolatedModules 设置为 true 时,如果某个 ts 文件中没有一个import or export 时,ts 则认为这个模块不是一个 ES Module 模块,它被认为是一个全局的脚本,
"isolatedModules": true,
// 模块名到基于 baseUrl的路径映射的列表。
"paths": {
"@/*": [
"/src/*"
]
},
"vueCompilerOptions": {
"experimentalDisableTemplateSupport": true //去掉volar下el标签红色波浪线问题
},
// 编译过程中需要引入的库文件的列表。
"lib": [
"ESNext",
"DOM",
"DOM.Iterable",
"ScriptHost"
]
},
// 解析的文件
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/*.js",
"src/**/*.jsx"
],
"exclude": [
"node_modules"
],
// 扩展配置
"references": [
{
"path": "./tsconfig.node.json"
}
]
}
4. 设置 .prettierrc.json 文件
eslint 配置格式化选项说明
// 1.一行代码的最大字符数,默认是80(printWidth: <int>)
printWidth: 80,
// 2.tab宽度为2空格(tabWidth: <int>)
tabWidth: 2,
// 3.是否使用tab来缩进,我们使用空格(useTabs: <bool>)
useTabs: false,
// 4.结尾是否添加分号,false的情况下只会在一些导致ASI错误的其工况下在开头加分号
semi: true,
// 5.使用单引号(singleQuote: <bool>)
singleQuote: true,
// 6.object对象中key值是否加引号(quoteProps: "<as-needed|consistent|preserve>")as-needed只有在需求要的情况下加引号,consistent是有一个需要引号就统一加,preserve是保留用户输入的引号
quoteProps: 'as-needed',
// 7.在jsx文件中的引号需要单独设置(jsxSingleQuote: <bool>)
jsxSingleQuote: false,
// 8.尾部逗号设置,es5是尾部逗号兼容es5,none就是没有尾部逗号,all是指所有可能的情况,需要node8和es2017以上的环境。(trailingComma: "<es5|none|all>")
trailingComma: 'es5',
// 9.object对象里面的key和value值和括号间的空格(bracketSpacing: <bool>)
bracketSpacing: true,
// 10.jsx标签多行属性写法时,尖括号是否另起一行(jsxBracketSameLine: <bool>)
jsxBracketSameLine: false,
// 11.箭头函数单个参数的情况是否省略括号,默认always是总是带括号(arrowParens: "<always|avoid>")
arrowParens: 'always',
// 12.range是format执行的范围,可以选执行一个文件的一部分,默认的设置是整个文件(rangeStart: <int> rangeEnd: <int>)
rangeStart: 0,
rangeEnd: Infinity,
// 13.不需要写文件开头的 @prettier
requirePragma: false,
// 14.不需要自动在文件开头插入 @prettier
insertPragma: false,
// 15.vue script和style标签中是否缩进,开启可能会破坏编辑器的代码折叠
vueIndentScriptAndStyle: false,
// 16.使用默认的折行标准
proseWrap: 'preserve',
// 17.根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 18.endOfLine: "<lf|crlf|cr|auto>" 行尾换行符,默认是lf,
endOfLine: 'lf',
// 19.embeddedLanguageFormatting: "off",默认是auto,控制被引号包裹的代码是否进行格式化
embeddedLanguageFormatting: 'off',
// 常用配置
{
"singleQuote": true,
"tabWidth": 2,
"semi": true,
}
5. .eslintrc.js 文件参考
// ESlint 检查配置
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
node: true,
es6: true,
},
parser: 'vue-eslint-parser',
extends: [
'eslint:recommended', // ESLint 官方推荐的通用规则
'plugin:vue/vue3-recommended', // Vue.js 官方推荐的 Vue 3 相关规则。
'plugin:@typescript-eslint/recommended', // 这是与 TypeScript 结合使用的 ESLint 规则
'plugin:prettier/recommended', // 这个插件将整合 Prettier 格式化工具和 ESLint,确保代码格式化的一致性。它会关闭与 Prettier 冲突的 ESLint 规则。
'prettier', // 这个配置用于禁用与 Prettier 冲突的 ESLint 规则。
],
// eslint-plugin-vue @typescript-eslint/eslint-plugin eslint-plugin-prettier的缩写
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/no-this-alias': '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',
// 禁止出现console
'no-console': 'off',
// 禁用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',
// 强制在块之前使用一致的空格
'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',
'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',
'vue/multi-word-component-names': 'off',
},
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
module: 'readonly',
},
};
6. 设置 vite.config.ts 文件
安装 gzip 和 mock 依赖
npm i vite-plugin-compression vite-plugin-mock -D
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import path from 'path'
// gzip插件
import viteCompression from 'vite-plugin-compression'
// mock插件
import { viteMockServe } from 'vite-plugin-mock'
const resolve = (dir) => path.resolve(__dirname, dir)
export default defineConfig({
base: './', //打包路径
publicDir: resolve('public'), //静态资源服务的文件夹
plugins: [
vue(),
vueJsx(),
// gzip压缩 生产环境生成 .gz 文件
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz',
}),
//mock
viteMockServe({
mockPath: './mocks', // 解析,路径可根据实际变动
localEnabled: true, // 此处可以手动设置为true,也可以根据官方文档格式
}),
],
// 配置别名
resolve: {
alias: {
'@': resolve('src'),
// 另一种写法: import { fileURLToPath, URL } from 'node:url';
// '@': fileURLToPath(new URL('./src', import.meta.url)),
},
// 导入时想要省略的扩展名列表
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
},
css: {
// css预处理器
preprocessorOptions: {
scss: {
additionalData:
'@import "@/assets/styles/common.scss";@import "@/assets/styles/reset.scss";', // '@import "${resolve('src/assets/styles/common.scss')}";@import "${resolve('src/assets/styles/reset.scss')}";'
},
},
},
//启动服务配置
server: {
host: '0.0.0.0',
port: 8000,
open: true, // 自动在浏览器打开
proxy: {},
},
// 打包配置
build: {
//浏览器兼容性 "esnext"|"modules"
target: 'modules',
//指定输出路径
outDir: 'build',
//生成静态资源的存放路径
assetsDir: 'assets',
//启用/禁用 CSS 代码拆分
cssCodeSplit: true,
sourcemap: false,
assetsInlineLimit: 10240,
// 打包环境移除console.log, debugger
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
rollupOptions: {
input: {
main: resolve('index.html'),
},
output: {
entryFileNames: `js/[name]-[hash].js`,
chunkFileNames: `js/[name]-[hash].js`,
assetFileNames: `[ext]/[name]-[hash].[ext]`,
},
},
},
})
三、项目目录结构划分
-
- assets 存放 => 静态资源
-
- css => 样式重置
-
- img => 图片文件
-
- font => 字体文件
-
- components 存放 => 公共组件
-
- hooks 存放 => 公共常用的hook
-
- mock 存放 => 模拟接口数据
-
- router 存放 => 路由管理
-
- service 存放 => 接口请求
-
- stores 存放 => 状态管理
-
- utils 存放 => 插件、第三方插件
-
- views 存放 => 视图、页面
四、css 样式重置
自定义的css公共文件放置在assets中的css文件中即可
1. normalize.css
01 - 安装
npm i normalize.css
02 - 引入
// 在 main.js 中引入
import 'normalize.css';
2. reset.css
01 - 代码
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
font,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
caption {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
}
table,
tbody,
tfoot,
thead,
tr,
th,
td {
margin: 0;
padding: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
}
button,
input,
textarea {
margin: 0;
padding: 0;
}
/* form elements 表单元素 */
body,
button,
input,
select,
textarea {
font: normal 12px/1.5 '\5FAE\8F6F\96C5\9ED1', tahoma, arial;
}
/*设置的字体,行高*/
h1,
h2,
h3,
h4,
h5,
h6,
th {
font-size: 100%;
font-weight: normal;
}
/*重置标题*/
address,
cite,
dfn,
var {
font-style: normal;
}
/* 将斜体扶正 */
code,
kbd,
pre,
samp {
font-family: 'courier new', courier, monospace;
}
/* 统一等宽字体 */
small {
font-size: 12px;
}
/* 小于 12px 的中文很难阅读,让 small 正常化 */
ul,
ol {
list-style: none;
}
/* 重置列表元素 */
button,
input[type="submit"],
input[type="button"] {
cursor: pointer;
}
input[type="radio"],
input[type="checkbox"],
input[type="submit"],
input[type="reset"] {
vertical-align: middle;
cursor: pointer;
border: none;
}
/** 重置文本格式元素 **/
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:focus {
outline: 0;
}
sup {
vertical-align: text-top;
}
/* 重置,减少对行高的影响 */
sub {
vertical-align: text-bottom;
}
/** 重置表单元素 **/
legend {
color: #000;
}
/* for ie6 */
fieldset,
img {
border: 0;
}
/* img 搭车:让链接里的 img 无边框 */
button,
input,
select,
textarea {
background: transparent;
font-size: 100%;
outline: 0;
}
/* 使得表单元素在 ie 下能继承字体大小 */
/* 注:optgroup 无法扶正 */
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
vertical-align: middle;
}
/** 重置表格元素 **/
/* 重置 HTML5 元素 */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section,
summary,
time,
mark,
audio,
video {
display: block;
margin: 0;
padding: 0;
}
/*回复标签重置*/
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
display: none;
}
简版样式重置
// 按照网站自己的需求,提供公用的样式
* {
box-sizing: border-box;
}
html {
height: 100%;
font-size: 14px;
}
body {
height: 100%;
color: #333;
min-width: 1240px;
font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif;
}
ul,
h1,
h3,
h4,
p,
dl,
dd {
padding: 0;
margin: 0;
}
a {
text-decoration: none;
color: #333;
outline: none;
}
i {
font-style: normal;
}
input[type='text'],
input[type='search'],
input[type='password'],
input[type='checkbox'] {
padding: 0;
outline: none;
border: none;
-webkit-appearance: none;
&::placeholder {
color: #ccc;
}
}
img {
max-width: 100%;
max-height: 100%;
vertical-align: middle;
// background: #ebebeb;
}
ul {
list-style: none;
}
#app {
// 不能选中文字
user-select: none;
}
/* 滚动条容器 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
/* 滚动槽 */
::-webkit-scrollbar-track {
border-radius: 10px;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background-color: #313340;
border-radius: 10px;
-webkit-transition: all .2s ease-in-out;
&:hover {
background-color: #4c4e59;
cursor: pointer;
}
}
02 - 引入
// 在 main.js 中引入
import './assets/css/reset.css';
3. common.css
01 - 代码
@use "sass:math";
// 背景色
$background-color-white: #ffffff;
$vw_base: 1920;
$vh_base: 1080;
/* 计算vw */
@function vw($px) {
@return math.div($px, $vw_base) * 100vw;
}
/* 计算vh */
@function vh($px) {
@return math.div($px, $vh_base) * 100vh;
}
/* 单行文本省略 */
@mixin single-line($width) {
width: #{$width}px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 多行文本省略 */
@mixin multi-line($num) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $num;
overflow: hidden;
}
/**
* 全局样式代码块
*/
@mixin flex($type) {
display: flex;
/* 水平居中 */
@if $type ==1 {
justify-content: center;
}
/* 垂直居中 */
@else if($type ==2) {
align-items: center;
}
/* 水平垂直居中 */
@else if($type ==3) {
justify-content: center;
align-items: center;
}
/* 水平拉伸垂直居中 */
@else if($type ==4) {
justify-content: space-between;
align-items: center;
}
/* 水平拉伸换行 */
@else if($type ==5) {
justify-content: space-between;
flex-wrap: wrap;
}
/* 换行 */
@else if($type ==6) {
flex-wrap: wrap;
}
}
.fl {
float: left;
}
.fr {
float: right;
}
.clearfix:after {
content: '.';
display: block;
visibility: hidden;
height: 0;
line-height: 0;
clear: both;
}
// 闪动画
.shan {
&::after {
content: '';
position: absolute;
animation: shan 1.5s ease 0s infinite;
top: 0;
width: 30%;
height: 100%;
background: linear-gradient(to left, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.3) 50%, rgba(255, 255, 255, 0) 100%);
transform: skewX(-45deg);
}
}
@keyframes shan {
0% {
left: -100%;
}
100% {
left: 120%;
}
}
// 离开淡出动画
.fade {
&-leave {
&-active {
position: absolute;
width: 100%;
transition: opacity 0.5s 0.2s;
z-index: 1;
}
&-to {
opacity: 0;
}
}
}
// 1. 离开,透明度 1---->0 位移 0---->30
// 2. 进入,透明度 0---->1 位移 30---->0
// 执行顺序,先离开再进入
.pop {
&-leave {
&-from {
opacity: 1;
transform: none;
}
&-active {
transition: all 0.5s;
}
&-to {
opacity: 0;
transform: translateX(20px);
}
}
&-enter {
&-from {
opacity: 0;
transform: translateX(20px);
}
&-active {
transition: all 0.5s;
}
&-to {
opacity: 1;
transform: none;
}
}
}
02 - 引入
// 在 main.js 中引入
import './assets/css/common.css';
五、vue-router 路由配置
一步创建需要安装依赖、配置路由, 引入mian.ts, 配置创建则已自动生成
1. 安装
npm i vue-router
2. 配置
// 1. 导入
import { createRouter, createWebHashHistory } from 'vue-router';
// 2. 创建路由对象
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
component: () => import('xxx/home.vue')
}
]
});
// 3. 导出
export default router;
3. 引入
// main.js
import { createApp } from 'vue';
import App from './App.vue';
// 1. 导入
import router from './router';
import 'normalize.css';
import './assets/css/reset.css';
import './assets/css/common.css';
// 2. 使用
createApp(App).use(router).mount('#app');
4. 使用
设置路由映射组件的展示区域
<router-view></router-view>
设置超链接
<router-link to="/index">首页</router-link>
<router-link :to="{ name: 'index' }">首页</router-link>
<router-link to="/school/1">这是广州校区</router-link>
编程式跳转
router.push('home')
router.push({path:'/child/${itemId}'})
router.push({ path:"/home", query:{ id:this.id} })
router.push({ name:'user', params:{userId: '123'}})
router.go()
router.go(-1)
router.back()
六、pinia 状态管理
一步创建需要安装依赖、配置路由, 引入mian.ts, 配置创建则已自动生成
1. 安装
npm i pinia
npm i pinia-plugin-persist // 持久化
2. 引入
// main.js
import { createApp } from 'vue';
import { createPinia } from "pinia";
import piniaPersist from 'pinia-plugin-persist'
import App from './App.vue';
// 1. 导入
import router from './router';
import 'normalize.css';
import './assets/css/reset.css';
import './assets/css/common.css';
// 2. 使用
const pinia = createPinia()
pinia.use(piniaPersist)
createApp(App).use(pinia).use(router).mount('#app');
3. 模块
// 1. 导入
import { defineStore } from 'pinia';
// 2. 使用
const useDemoStore = defineStore('demoStore', {
state: () => ({
arrList: []
}),
actions: {},
getters: {},
// 所有数据持久化
// persist: {
// enabled: true,
// },
// 持久化存储插件其他配置
persist: {
enabled: true,
strategies: [
// 修改存储中使用的键名称,默认为当前 Store的 id
// 修改为 sessionStorage,默认为 localStorage
// 部分持久化状态的点符号路径数组,[]意味着没有状态被持久化(默认为undefined,持久化整个状态)
{ key: 'storekey', storage: sessionStorage, paths: ['routes', 'userInfo'] }, // routes 和 userInfo字段用sessionStorage存储
{ key: 'storekey', storage: localStorage, paths: ['token'] }, // token字段用 localstorage存储
],
},
});
// 3. 导出
export default useDemoStore;
4. 使用
import { useStore } from '@/stores/counter'
const store = useStore()
const { counter, doubleCount } = storeToRefs(store)
store.$reset() // 将状态 重置 到其初始值
store.counter++
store.$patch({ counter: store.counter + 1, })
store.$state = { counter: 666 }
pinia.state.value = {}
七、集成 Axios HTTP 工具
安装依赖
npm i axios
请求配置
在 utils
目录下创建 request.ts
文件,配置好适合自己业务的请求拦截和响应拦截
// vite环境变量
// .env.development
NODE_ENV=development
VITE_BASE_URL='http://xxx'
// .env.production
NODE_ENV=production
VITE_BASE_URL='http://xxx'
import axios, { AxiosRequestConfig, Method } from 'axios';
// 发布订阅
class EventEmitter {
constructor() {
this.event = {}
}
on(type, cbres, cbrej) {
if (!this.event[type]) {
this.event[type] = [[cbres, cbrej]]
} else {
this.event[type].push([cbres, cbrej])
}
}
emit(type, res, ansType) {
if (!this.event[type]) return
else {
this.event[type].forEach(cbArr => {
if(ansType === 'resolve') {
cbArr[0](res)
}else{
cbArr[1](res)
}
});
}
}
}
// 根据请求生成对应的key
function generateReqKey(config, hash) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}
// 请求体的数据是FormData类型,直接放行
function isFileUploadApi(config) {
return Object.prototype.toString.call(config.data) === "[object FormData]"
}
// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter()
// 创建请求实例
const instance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
// 指定请求超时的毫秒数
timeout: 10000,
// 表示跨域请求时是否需要使用凭证
withCredentials: false,
});
// 设置请求头
instance.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8';
instance.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded';
// instance.defaults.headers.put['Content-Type'] = 'application/json';
// 添加请求拦截器
instance.interceptors.request.use(async (config): AxiosRequestConfig<any> => {
let hash = location.hash
// 生成请求Key
let reqKey = generateReqKey(config, hash)
if(pendingRequest.has(reqKey)&&!isFileUploadApi(config)) {
// 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
// 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
let res = null
try {
// 接口成功响应
res = await new Promise((resolve, reject) => {
ev.on(reqKey, resolve, reject)
})
return Promise.reject({
type: 'limiteResSuccess',
val: res
})
}catch(limitFunErr) {
// 接口报错
return Promise.reject({
type: 'limiteResError',
val: limitFunErr
})
}
}else{
// 将请求的key保存在config
config.pendKey = reqKey
pendingRequest.add(reqKey)
}
/**
* 在这里一般会携带前台的参数发送给后台,比如下面这段代码:
* const token = sessionStorage.getItem('token')
* if (token) {
* config.headers.Authorization = `Basic ${token}`
* }
*/
return config;
}, function (error) {
return Promise.reject(error);
});
// 响应拦截器(获取到响应时的拦截)
instance.interceptors.response.use(
(response) => {
// 将拿到的结果发布给其他相同的接口
handleSuccessResponse_limit(response)
return response;
},
(error) => {
return handleErrorResponse_limit(error)
},
);
// 接口响应成功
function handleSuccessResponse_limit(response) {
const reqKey = response.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(response))
}catch(e) {
x = response
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'resolve')
delete ev.reqKey
}
}
// 接口走失败响应
function handleErrorResponse_limit(error) {
if (error.type && error.type === 'limiteResSuccess') {
return Promise.resolve(error.val)
} else if (error.type && error.type === 'limiteResError') {
return Promise.reject(error.val);
} else {
const reqKey = error.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(error))
}catch(e) {
x = error
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'reject')
delete ev.reqKey
}
}
return Promise.reject(error);
}
interface ResType<T> {
code: number;
data?: T;
msg?: string;
message?: string;
err?: string;
}
interface IOptions {
isFormUrlencoded?: boolean;
}
interface Http {
post<T>(url: string, data?: unknown, options?: IOptions): Promise<ResType<T>>;
get<T>(url: string, options?: IOptions): Promise<ResType<T>>;
put<T>(url: string, data?: unknown, options?: IOptions): Promise<ResType<T>>;
upload<T>(url: string, file?: unknown): Promise<ResType<T>>;
_delete<T>(url: string, options?: IOptions): Promise<ResType<T>>;
}
/**
* 是否是x-www-form-urlencoded格式请求
* @param {}
*/
function isFormUrlencoded(url, params, options) {
if (options?.isFormUrlencoded) {
const list: any[] = [];
for (const key in params) {
if (params[key] !== null) {
list.push(`${key}=${encodeURIComponent(params[key])}`);
}
}
const newParams = list.join('&');
const newOptions = {
...options,
headers: {
...options.headers,
'Content-Type': 'application/x-www-form-urlencoded',
},
};
return { newUrl: url + '?' + newParams, newParams: {}, newOptions };
}
return { newUrl: url, newParams: params, newOptions: options };
}
// 导出常用函数
const http: Http = {
post(url, data, options?) {
return new Promise((resolve, reject) => {
const { newUrl, newData, newOptions } = isFormUrlencoded(data, options);
instance
.post(newUrl, JSON.stringify(newData), newOptions)
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
},
get(url, params, options?) {
return new Promise((resolve, reject) => {
instance
.get(url, { params, ...options })
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
},
put(url, data, options?) {
return new Promise((resolve, reject) => {
const { newUrl, newData, newOptions } = isFormUrlencoded(data, options);
instance
.put(newUrl, newData, newOptions)
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
},
upload(url, file) {
return new Promise((resolve, reject) => {
instance
.post(url, file, {
headers: { 'Content-Type': 'multipart/form-data' },
})
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
},
downFile(url) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
iframe.onload = function () {
document.body.removeChild(iframe);
};
document.body.appendChild(iframe);
},
downBlob(url,fileName) {
fetch(url, {
method: 'get',
})
.then((res) => res.blob())
.then((blob) => {
const a = document.createElement('a');
// 获取 blob 本地文件连接 (blob 为纯二进制对象,不能够直接保存到磁盘上)
const url = window.URL.createObjectURL(
new Blob([blob], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }),
);
a.href = url;
//定义导出的文件名
a.download = `${fileName}.xls`;
a.click();
window.URL.revokeObjectURL(url);
});
},
_delete(url, options?) {
return new Promise((resolve, reject) => {
instance
.delete(url, options)
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err.data);
});
});
}
}
export default http;
之后在 api
文件夹中以业务模型对接口进行拆分,举个例子,将所有跟用户相关接口封装在 DictionaryAPI
对象中。
import request from '@/service/request';
const typeUrl = '/sysDict';
interface IBusinessType {
dataName: string
dataType: string
pid: string
}
interface IBusinessSubType {
businessSubType: string
businessType: string
}
interface InDictionaryAPI {
allOptions: () => Promise<any>;
businessTypeList: (params: IBusinessType) => Promise<any>;
businessSubTypeList: (params: IBusinessSubType) => Promise<any>;
}
/**
* 查询字典列表
*/
const DictionaryAPI: InDictionaryAPI = {
/**
* 获取所有下拉选项
* @param {object} params
*/
allOptions() {
return Promise.allSettled([
this.businessTypeList({ dataName: '', dataType: '', pid: '1' }),
this.businessSubTypeList({ businessSubType: '', businessType: '' }),
]);
},
/**
* 紧急程度/业务类型
* @param {object} params
*/
businessTypeList(params = {}) {
return request.post(typeUrl + '/findList', params);
},
/**
* 获取业务子类
* @param {object} params
*/
businessSubTypeList(params = {}) {
return request.post(typeUrl + '/getBusinessSubTypeGroup', params, { isFormUrlencoded: true, requestBase: 'AdminBashUrl });
},
};
export default DictionaryAPI;
把每个业务模型独立成一个 js 文件,声明一个类通过其属性和方法来实现这个模型相关的数据获取,这样可以大大提升代码的可读性与可维护性。
模拟演示
在需要使用接口的地方,引入对应的业务模型文件,参考如下
<script setup lang="ts">
import { DictionaryAPI } from '@/service/api/index';
import { ref } from 'vue'
/**
* 获取所有下拉选项
* @param {type} 参数
* @returns {type} 返回值
*/
async function getAllOptions() {
try {
const res: any = await DictionaryAPI.allOptions();
// ...
} catch (error) {
console.log(error);
}
},
</script>
八、使用scss, 并定义全局scss变量
首先我们先安装sass(不用安装sass-loader):
npm i sass -D
然后我们需要在vite.config.ts
中配置css预处理器
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/assets/styles/global.scss";@import "@/assets/styles/reset.scss";',
},
}
}
})
我们这里默认加载global.scss
中的样式,那么我们就需要创建一个这样的文件:
// src/assets/style/global.scss
$primary-color: #5878e2; // 主题色
最后在main.ts
中引入即可:
import "./assets/style/global.scss";
然后在组件中使用时,就可以直接使用:
<script setup lang="ts">
import {GlobalStore} from '@/store'
const global = GlobalStore();
</script>
<template>
<div>{{global.token}}</div>
</template>
<style scoped lang="scss">
div {
color: $primary-color; // 主题色
}
</style>
样式穿透
在 Vue3 中,改变了以往样式穿透的语法,如果继续使用 ::v-deep
、/deep/
、>>>
等语法的话,会出现一个警告,下面是新的语法:
/* 深度选择器 */
:deep(selector) {
/* ... */
}
/* 插槽选择器 */
:slotted(selector) {
/* ... */
}
/* 全局选择器 */
:global(selector) {
/* ... */
}
scss常用语法
1. 嵌套
.page {
.page-header{
.header-left {}
}
.page-content {}
}
2. 变量
$color:red;
/* 使用 */
.scss_content{
color:$color;
}
3. 计算
@use "sass:math"; // 放在文件第一行
$vw_base: 1920;
$vh_base: 1080;
/* 计算vw */
@function vw($px) {
@return math.div($px, $vw_base) * 100vw;
}
/* 计算vh */
@function vh($px) {
@return math.div($px, $vh_base) * 100vh;
}
/* 使用 */
.head{
font-size:vh(100);
}
4. 混合
/* 单行文本省略 */
@mixin single-line($width) {
width: #{$width}px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 多行文本省略 */
@mixin multi-line($num) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $num;
overflow: hidden;
}
@mixin flex($type) {
display: flex;
/* 水平居中 */
@if $type ==1 {
justify-content: center;
}
/* 垂直居中 */
@else if($type ==2) {
align-items: center;
}
/* 水平垂直居中 */
@else if($type ==3) {
justify-content: center;
align-items: center;
}
/* 水平拉伸垂直居中 */
@else if($type ==4) {
justify-content: space-between;
align-items: center;
}
/* 水平拉伸换行 */
@else if($type ==5) {
justify-content: space-between;
flex-wrap: wrap;
}
/* 换行 */
@else if($type ==6) {
flex-wrap: wrap;
}
}
/* 使用 */
@include flex(1);