基于Vite+TS初始项目 | 不断更新

1 创建项目

1.1 初始化项目

# 创建项目
pnpm create vite

# 使用vue-ts模板创建项目
pnpm create vite xyz-vue-app-ts --template vue-ts

1.2 添加ts类型检查命令

添加 "type-check" 类型检查命令

{
    "name": "xyz-vue-app-ts-test",
    "scripts": {
        "dev": "vite",
        "build": "pnpm type-check && vite build",
        "preview": "vite preview",
        "type-check": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
    },
}

1.3 node版本限制

修改package.josn,新增engines内容配置

参考:https://dev.nodejs.cn/learn/the-package-json-guide/#engines

{
    "name": "xyz-vue-app-ts-test",
    "type": "module",
    "engines": {
        "node": ">=18.20.3"
    },
    "scripts": {}
}

在项目根目录下(与package.json同级),创建 .npmrc 文件,

engine-strict=true

1.4 改造vite配置

这里按开发环境、生产打包环境来创建不同的配置文件。在项目跟目录下,创建build文件夹,在文件夹中创建如下文件:

vite.config.dev.ts // 开发环境配置

vite.config.prod.ts // 生产环境配置

如果需要在vite中.config.ts中使用 “console”,需要安装以下模块,否则vue-tsc进行打包的类型检查会报错。

# node环境中没有console,同时添加node的ts定义
pnpm i -D tslib @types/node
vite.config.dev.ts

开发环境配置

import { defineConfig } from 'vite'

/**
 * vite开发环境配置
 * https://vite.dev/config/
 */
export default (env: ImportMetaEnv) => {
    const { VITE_APP_BASE_API, VITE_APP_BASE_SERVER } = env
    return defineConfig({
        server: {
            host: '0.0.0.0', // 配置host,配置为'0.0.0.0'可以在局域网访问
            proxy: {},
        },
    })
}

vite.config.prod.ts

生产环境配置

import { defineConfig } from 'vite'

/**
 * vite打包环境配置
 * https://vite.dev/config/
 */
export default (env: ImportMetaEnv) => {
    console.log('env', env)
    return defineConfig({
        // 配置打包后使用相对路径
        base: './',
        build: {
            /** 打包后的输出目录,默认为 dist */
            outDir: './dist',
            /** 打包后的文件内容不进行压缩,方便阅读 */
            // minify: false,
        },
    })
}

vite.config.ts

vite配置入口:合并vite配置

import path from 'path'
import { defineConfig, loadEnv, UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ViteConfigDev from './build/vite.config.dev'
import ViteConfigProd from './build/vite.config.prod'

/** 根据是否为开发环境、生产打包环境,使用不同的vite配置 */
const EnvResolver: Record<'serve' | 'build', (env: ImportMetaEnv) => UserConfig> = {
    serve: (env: ImportMetaEnv) => ({ ...ViteConfigDev(env) }),
    build: (env: ImportMetaEnv) => {
        console.log('vite开始打包...')
        return { ...ViteConfigProd(env) }
    },
}

/**
 * vite配置项,具体配置请到build目录下进行详细配置
 */
export default defineConfig(({ command, mode }) => {
    console.log('command', command)
    // 获取到环境变量配置
    const env = loadEnv(mode, process.cwd()) as ImportMetaEnv
    const { server, base, build, resolve = {}, plugins = [] } = EnvResolver[command](env)
    // 统一返回配置
    return {
        /** 开发阶段,vite动态打包服务器的配置 */
        server,
        /** 公共基础路径 */
        base,
        /** 打包构建阶段的配置 */
        build,
        resolve: {
            // 配置路径别名
            alias: {
                '@': path.resolve(__dirname, './src'),
            },
            ...resolve,
        },
        plugins: [
            vue(),
            ...plugins,
        ],
    }
})

1.5 路径别名@

1 src别名配置

在开发项目的时候文件与文件关系可能很复杂,因此我们需要给src文件夹配置一个别名。

vite.config.js

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
    plugins: [vue()],
    resolve: {
        alias: {
            "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
        }
    }
})

TypeScript 编译配置:如果用的是ts,需要在tsconfig.json进行如下配置

{
  "compilerOptions": {
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { //路径映射,相对于baseUrl
      "@/*": ["src/*"] 
    }
  }
}
2 处理webstorm不识别问题

创建jsconfig.json文件,配置以下内容:

{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/*": [
                "./src/*"
            ]
        }
    }
}

3 ESLint 9.x语法校验

学习参考博客:https://juejin.cn/post/7402696141495779363

ESLint 9.x需要 Node.js 17.x以上版本支持,因为 structuredClone 在 Node.js 17.0.0 中引入。否则会报如下错误。可以使用nvm工具进行版本管理。

ESLint: 9.15.0

ConfigError: Config (unnamed): Key "rules": Key "constructor-super": structuredClone is not defined

3.1 初始化ES配置

3.1.1 从0到1生成
1、安装ESLint依赖
# 仅安装ESLint
pnpm i eslint@latest -D
2、配置ESLint校验命令

可以在命令中加入 --debug 进行输出调试日志。

{
    "name": "xyz-vue-app-ts",
    "scripts": {
        "lint": "eslint",
        "lint:fix": "eslint --fix",
    },
}
3、初始化eslint.confit.js配置文件

在工程的根目录下创建文件:eslint.confit.js。如果没有这个文件,ESLint 9.x会报错找不到配置文件。

ESlint9.x开始,建议直接用eslint.config.jsESNext)或者eslint.config.mjsCommonJS)命名的配置文件。

因为我们的lint命令是没有指定目录或者指定文件类型的,默认eslit会去读取项目所有文件进行校验,所以我们需要在配置文件中使用ignores字段来告诉eslint排除哪些文件。创建后,在其中写入忽略的校验文件

/** @type {import('eslint').Linter.Config[]} */
export default [
    {
        // 自定义的忽略文件需要放到最后,否则容器被前面的规则给覆盖了
        // 默认eslit会去读取项目所有文件进行校验,所以我们需要在配置文件中使用ignores字段来告诉eslint排除哪些文件
        // 这里排除第三方依赖、静态文件
        ignores: ['node_modules', 'dist', 'public', 'src/assets'],
    },
]
3.1.2 直接使用ESLint命令脚手架生成
1、安装依赖
# 初始化eslint配置,这里直接初始化配置,会提示安装要Eslint,选择yes即可。
npx eslint --init

# 按以下配置初始化后继续安装
# vite-plugin-eslint:用于配置vite运行的时候自动检测eslint规范,报错时会在浏览器显示错误界面
pnpm install -D vite-plugin-eslint
# 不安装则报错:error when starting dev server: Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'vite-plugin-eslint' imported from
2、初始化配置项
# 初始化配置,eslint同时可作为命令行工具使用,需要先执行命令安装eslint
./node_modules/.bin/eslint --init

# window如果无法运行上述命令,可尝试
"node_modules/.bin/eslint" --init
√ How would you like to use ESLint? · problems # 选择To check syntax and find problems,检查并发现问题
√ What type of modules does your project use? · esm # 选择模块化的方式,这里一般选择ES6的import/export
√ Which framework does your project use? · vue # 按需选择,这里选择为vue工程
√ Does your project use TypeScript? · No / Yes # 按需选择是否为ts项目
√ Where does your code run? · browser, node  # 用空格选中 Browser 和 Node,高亮才是选中,选中后回车
√ What format do you want your config file to be in? · JavaScript
The config that you've selected requires the following dependencies:
                                                                    
eslint-plugin-vue@latest                                            
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · pnpm
3、初始化后的配置说明
// 导入了 `globals`全局变量的库模块,该模块提供了一组预定义的全局变量(如 window、document 等),这些变量通常在不同的环境(如浏览器、Node.js)中可用
// 在 ESLint 配置中,你可以使用这个模块来指定代码所运行的环境,从而定义全局变量。
import globals from 'globals'
// 针对 JavaScript 的 ESLint 配置和规则。保持 JavaScript 代码的一致性和质量
import pluginJs from '@eslint/js'
// 导入 `typescript-eslint` 插件( `typescript-eslint/parser` 和 `typescript-eslint/eslint-plugin`)。提供了对 TypeScript 的支持,包括 TS 的解析器和推荐的规则集,用于在 TypeScript 文件中进行 lint 检查。
import tseslint from 'typescript-eslint'
// 导入 `eslint-plugin-vue` 插件,提供了 Vue.js 特有 ESLint 规则。确保 Vue 文件(`.vue` 文件)中的代码符合 Vue.js 的最佳实践和代码风格指南
import pluginVue from 'eslint-plugin-vue'

/** @type {import('eslint').Linter.Config[]} */
export default [
    // 文件匹配:`files` 属性指定了哪些文件类型(JavaScript、TypeScript、Vue 文件等)将应用这些规则
    { files: ['**/*.{js,mjs,cjs,ts,vue}'] },
    // 全局变量:`languageOptions` 配置了浏览器环境下的全局变量。
    { languageOptions: { globals: { ...globals.browser, ...globals.node } } },
    // `pluginJs.configs.recommended` 引入了 `@eslint/js` 插件的推荐规则。
    pluginJs.configs.recommended,
    // 引入 `typescript-eslint` 插件的推荐规则
    ...tseslint.configs.recommended,
    // 引入`eslint-plugin-vue` 插件的基础规则
    ...pluginVue.configs['flat/essential'],
    {
        // 针对 Vue 文件配置
        files: ['**/*.vue'],
        // 为 `.vue` 文件指定了 TypeScript 解析器
        languageOptions: { parserOptions: { parser: tseslint.parser } }
    }
]

3.2 从0到1的规则配置

(1)这里推荐每种规则都单独一个对象框起来,方便管理,因为有javascriptvuetypescript等规则。

(2)规则优先级问题:后面的规则会覆盖前面的规则,所以一般会把recommended(推荐的规则)写在最前面,然后后面再去关掉/启用一些其他规则。

3.2.1 校验忽略配置

配置 忽略的校验文件可以参考如下:

/** @type {import('eslint').Linter.Config[]} */
export default [
    {
        // 自定义的忽略文件需要放到最后,否则容器被前面的规则给覆盖了
        // 默认eslit会去读取项目所有文件进行校验,所以我们需要在配置文件中使用ignores字段来告诉eslint排除哪些文件
        // 这里排除第三方依赖、静态文件
        ignores: ['node_modules', 'dist', 'public', 'src/assets'],
    },
]
3.2.2 添加JavaScript规则

因为ESlint是默认支持解析JavaScript的,以及有内置一些规则,所以我们只需启用相对于的规则名称就可以了。比如,我们不想在调试代码时在控制台看到各个小伙伴五花八门的log日志,非常影响自己调试代码,那么可以添加一下规则:

/** @type {import('eslint').Linter.Config[]} */
export default [
    {
        rules: {
            'no-console': 'error'
        }
    },
    {
        // 自定义的忽略文件需要放到最后,否则容器被前面的规则给覆盖了
        // 默认eslit会去读取项目所有文件进行校验,所以我们需要在配置文件中使用ignores字段来告诉eslint排除哪些文件
        // 这里排除第三方依赖、静态文件
        ignores: ['node_modules', 'dist', 'public', 'src/assets'],
    },
]

规则可以是errorwarnoff

需要注意的是在没有 添加其他插件的情况下,这个规则仅针对 js文件 生效,所以我们需要创建一个测试的js文件 test.js,在其中编写测试的代码(测试后先别删除,后续测试还用到):

console.log('🌟🌟🌟🌟🌟TEO', a)
使用ESLine提供的现成js规则

在项目中我们可以直接使用ESlint提供的现成的规则集,要用到@eslint/js这个依赖包。

1、安装依赖
pnpm i @eslint/js -D
2、配置使用规则

后面你会看到很多recommended,相信在旧的ESlint项目中你也会看到这些,这些一般都是相关开发者或者社区所推荐的规则,启用就对了。如果有些规则不是很符合你的项目需求,也可以off掉。

import eslint from '@eslint/js'

/** @type {import('eslint').Linter.Config[]} */
export default [
    eslint.configs.recommended,
    {
        // 默认eslit会去读取项目所有文件进行校验,所以我们需要在配置文件中使用ignores字段来告诉eslint排除哪些文件
        // 这里排除第三方依赖、静态文件
        ignores: ['node_modules', 'dist', 'public', 'src/assets']
    }
]

以上配置之后,执行 pnpm lint 命令校验,在控制台会看到两个报错,这个是正常检查除了错误语法:

  1:1   error  'console' is not defined  no-undef
  1:30  error  'a' is not defined        no-undef

因为eslint.configs.recommende中并没有推荐no-console,除非你用eslint.configs.all。另外如果我们想要,可以通过在后面自定义项目规则去覆盖前面的或添加新的规则,配置如下:

import eslint from '@eslint/js'

/** @type {import('eslint').Linter.Config[]} */
export default [

    // JavaScript 推荐的规则
    eslint.configs.recommended,

    /**
     * JavaScript 自定义的规则
     */
    {
        rules: {
            'no-console': 'warn'
        }
    }
]

推荐每种规则都单独一个对象框起来,方便管理,因为后面还有vuetypescript等规则。

3.2.3 启用全局变量规则

在上面的校验中,会提示 ‘console’ is not defined,这是因为eslint.configs.recommended 中启用了一条规则"no-undef": "error"console是全局变量,默认就可以直接使用,对于eslint来说,任何变量都需要定义,不管是否全局变量。这也可避免我们在项目中使用一些真的不存在的变量时导致出现一些低级错误。

打个比方,你可以在任何浏览器环境执行alert(),但是你在nodejs环境中执行就会报错,nodejs中并没有该方法。

所以eslint一视同仁,对于任何可用的隐藏全局变量,都需要跟eslint打声招呼。于是,我们可以用一个依赖包来配置所有全局变量,那就是globals

1、安装依赖
pnpm i globals -D
2、配置规则
import eslint from '@eslint/js'
import globals from 'globals'

/** @type {import('eslint').Linter.Config[]} */
export default [

    /**
     * JavaScript 自定义的规则
     */
    {
        rules: {
            'no-console': 'warn'
        }
    },

    /**
     * 配置全局变量
     */
    {
        languageOptions: {
            globals: {
                ...globals.browser,
                // 添加自定义的全局变量
                Cesium: true
            }
        }
    }
]

注意:

这里只是告知eslint这些是全局变量,eslint便会去掉这些报错。而不是真的给你提供该变量,即便你欺骗eslint xxx 是全局变量,然后你去使用这个xxx时,代码逻辑依旧会报错的。 这里还适合一些通过cdn引入的全局变量,可以在这里配置,使用时eslint就不会报错了。

这种配置方法,可以解决的是第三方库没有npm依赖包的问题,他是通过在index.html入库文件中引入来使用的。

3.2.4 Vue规则

由于ESlint本身只支持识别JavaScript,所以对于vue文件,还需要一个插件:eslint-plugin-vue,假如你的项目也跟我一样用的是typescript,那么还需要另一个解析器:typescript-eslint

1、安装依赖
pnpm i -D eslint-plugin-vue typescript-eslint
2、配置规则

这里的配置需要整合下前面配置的规则,将前面的规则继承到这里。可以参考配置:https://eslint.vuejs.org/user-guide/

这里我们使用了eslintPluginVue.configs['flat/recommended']作为vue的推荐规则配置。同时,里面还包含了vueESlint插件,用于解析vue文件,这也是ESlint配置为什么要大改的问题,因为它让ESlint配置更加简单了。

如果是typescript项目,需要在languageOptions.parserOptions中配置解析器来支持typescript

规则中的files属性则用于指定匹配的后缀文件或者目录用此规则或者解析器。如果不写files,则该规则配置对所有文件起作用。

Vue的更多规则配置参考:https://eslint.vuejs.org/rules/

import eslint from '@eslint/js'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import eslintPluginVue from 'eslint-plugin-vue'

/** @type {import('eslint').Linter.Config[]} */
export default [
    // 省略其他规则...

    // JavaScript 推荐的规则
    // eslint.configs.recommended,
    // Vue规则配置
    // ...eslintPluginVue.configs['flat/recommended'],
    
    // 省略其他规则...

    /**
     * 配置全局变量
     */
    {
        languageOptions: {
            globals: {
                ...globals.browser,
                // 添加自定义的全局变量
                Cesium: true,
            },
        },
    },

    /**
     * TypeScript、vue规则配置
     * 参考:https://eslint.vuejs.org/user-guide/
     */
    {
        // 这里不进行继承,在vue文件中无法解析到全局(在types中)定义的ts类型
        extends: [
            // JavaScript 推荐的规则
            eslint.configs.recommended,
            // ts推荐配置
            ...typescriptEslint.configs.recommended,
            // Vue规则配置
            ...eslintPluginVue.configs['flat/recommended'],
        ],
        files: ['**/*.{ts,tsx,vue}'],
        languageOptions: {
            ecmaVersion: 'latest',
            sourceType: 'module',
            // globals: globals.browser,
            parserOptions: {
                parser: typescriptEslint.parser,
            },
        },
        rules: {
            // ts规则配置
            '@typescript-eslint/no-explicit-any': 'off',
            '@typescript-eslint/prefer-ts-expect-error': 'off',
            // 在这里追加 vue 规则
            'vue/no-mutating-props': [
                'error',
                {
                    shallowOnly: true,
                },
            ],
        },
    },
]

如果项目中还没删除 vite 的 vue-ts 模版创建的文件还没有删除,这里执行 pnpm lint 即可看到相关的错误。

# 省略其他,或者有其他差异
E:\Code\xuyizhuo\xyz-web-demo-template\xyz-vue-app-ts-test\src\App.vue
   6:1   warning  Expected indentation of 2 spaces but found 4 spaces   vue/html-indent
   7:1   warning  Expected indentation of 4 spaces but found 8 spaces   vue/html-indent
   7:36  warning  'target' should be on a new line                      vue/max-attributes-per-line

3.2.5 TypeScript规则

整合到前面的配置

在上面,我们安装了typescript-eslint 依赖,用于对vue-ts的支持,它是集成了typescriptrecommended配置以及ESlint插件。

1、使用推荐和自定义配置

vueeslintPluginVue.configs['flat/recommended']一样,tseslint.configs.recommended也会在配置中自动添加插件和规则。即便你不是用在vue项目,也可以这样使用:

import eslint from '@eslint/js'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import eslintPluginVue from 'eslint-plugin-vue'

/** @type {import('eslint').Linter.Config[]} */
export default [
    // 省略其他配置...

    // JavaScript 推荐的规则
    // eslint.configs.recommended,
    // ts推荐配置
    // ...tseslint.configs.recommended,
    // Vue规则配置
    // ...eslintPluginVue.configs['flat/recommended'],

    
    // 省略其他配置...

    /**
     * vue规则
     */
    {
        files: ['**/*.vue'],
        languageOptions: {
            parserOptions: {
                // typescript项目需要用到这个
                parser: tseslint.parser,
                ecmaVersion: 'latest',
                // 允许在.vue 文件中使用 JSX
                ecmaFeatures: {
                    jsx: true,
                },
            },
        },
        rules: {},
    },

    /**
     * typescript 规则
     */
    {
        files: ['**/*.{ts,tsx,vue}'],
        rules: {},
    },
]
2、使用tseslint.config导出ESLint配置

这里我们既然用到了typescript-eslint这个插件,那么我们还可以用tseslint.config这个函数来导出ESLint的配置,让配置有文件有代码提示,避免写错。这里不是全部配置,主要是修改了导出的对象。

import eslint from '@eslint/js'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import eslintPluginVue from 'eslint-plugin-vue'

// /** @type {import('eslint').Linter.Config[]} */
// export default [ // 旧配置,直接导出一个数组,这样没有类型提示
export default tseslint.config( // 使用带有类型提示的eslint.config()方法,可以获得eslint的类型提示
    {
        // 默认eslit会去读取项目所有文件进行校验,所以我们需要在配置文件中使用ignores字段来告诉eslint排除哪些文件
        // 这里排除第三方依赖、静态文件
        ignores: ['node_modules', 'dist', 'public', 'src/assets'],
    },

    // JavaScript 推荐的规则
    eslint.configs.recommended,
    // ts推荐配置
    ...tseslint.configs.recommended,
    // Vue规则配置
    ...eslintPluginVue.configs['flat/recommended'],

    /**
     * typescript 规则
     */
    {
        files: ['**/*.{ts,tsx,vue}'],
        rules: {},
    },
)
3.2.6 @别名支持
1、安装@别名支持插件
pnpm i -D eslint-import-resolver-alias

pnpm i -D eslint-plugin-import eslint-import-resolver-alias
3.2.7 import导入排序
# 目前无法集成
pnpm i -D eslint-plugin-import
pnpm i -D eslint-import-resolver-typescript
3.2.8 集成eslint-plugin-vue@latest插件

需要安装

pnpm install -D vite-plugin-eslint

使用插件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import eslintPlugin from 'vite-plugin-eslint' // 导入eslintPlugin

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue(),
        /** 增加eslint的vite插件,这样在程序运行时就能检查eslint规范 */
        eslintPlugin(),
        // 按前面配置后,这里不用配置要校验和要忽略的文件
        /** 增加eslint的vite插件,这样在程序运行时就能检查eslint规范 */
        eslintPlugin({
            // 需要被ESLint校验的文件
            // include: ['src/**/*.js', 'src/**/*.vue', 'src/*.js', 'src/*.vue'],
            // 配置忽略文件:忽略依赖文件、静态文件
            // exclude: ['node_modules/**', 'public/**', 'src/assets/**'],
        }),
    ],
    resolve: {
        alias: {
            // 关键代码
            '@': path.resolve(__dirname, './src'),
        },
    },
})

3.3 一个良好的规则配置

import eslint from '@eslint/js'
import globals from 'globals'
import typescriptEslint from 'typescript-eslint'
import eslintPluginVue from 'eslint-plugin-vue'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'

/**
 * @type {import('eslint').Linter.Config[]}
 * @description 使用带有类型提示的 typescriptEslint.config()方法,可以获得eslint的类型提示
 *  */
export default typescriptEslint.config(
    /**
     * TypeScript、vue规则配置
     * 参考:https://eslint.vuejs.org/user-guide/
     */
    {
        // 这里不进行继承,在vue文件中无法解析到全局(在types中)定义的ts类型
        extends: [
            // JavaScript 推荐的规则
            eslint.configs.recommended,
            // ts推荐配置
            ...typescriptEslint.configs.recommended,
            // Vue规则配置
            ...eslintPluginVue.configs['flat/recommended'],
        ],
        files: ['**/*.{ts,tsx,vue}'],
        languageOptions: {
            ecmaVersion: 'latest',
            sourceType: 'module',
            // globals: globals.browser,
            parserOptions: {
                parser: typescriptEslint.parser,
            },
        },
        rules: {
            // ts规则配置
            '@typescript-eslint/no-explicit-any': 'off',
            '@typescript-eslint/prefer-ts-expect-error': 'off',
            // 在这里追加 vue 规则
            'vue/no-mutating-props': [
                'error',
                {
                    shallowOnly: true,
                },
            ],
        },
    },

    /**
     * JavaScript 自定义的规则
     */
    {
        rules: {
            // 'no-console': 'warn',
        },
    },

    /**
     * 配置全局变量
     */
    {
        languageOptions: {
            globals: {
                ...globals.browser,
                // 添加自定义的全局变量
                Cesium: true,
            },
        },
    },

    /**
     * prettier 配置
     * 会合并根目录下的prettier.config.js 文件
     * @see https://prettier.io/docs/en/options
     */
    eslintPluginPrettierRecommended,

    {
        settings: {
            /** 配置别名 */
            'import/resolver': {
                alias: [['@', './src']],
            },
        },
    },
    {
        // 自定义的忽略文件需要放到最后,否则容器被前面的规则给覆盖了
        // 默认eslit会去读取项目所有文件进行校验,所以我们需要在配置文件中使用ignores字段来告诉eslint排除哪些文件
        // 这里排除第三方依赖、静态文件
        ignores: ['node_modules', 'dist', 'public', 'src/assets'],
    },
)

3.4 在开发工具中使用ESLint校验

3.4.1 在WebStorm中使用
3.4.2 在VScode中使用

在自动化部署时,运行pnpm lint来确认代码是否存在问题,或者在提交代码时,通过husky来阻止存在ESlint问题的代码提交。

vscode中的ESlint,本质是在你开发时可以立马找到错误的代码位置进行改正修复。

1、安装ESLint插件

需要在vscode中安装ESlint扩展插件,安装完后可能要重启vscode来生效。

2、ESlint扩展配置

在项目根目录中创建一个.vscode目录,里面放置一个settings.json文件,这样vscode就会优先读取该设置:

{
    "eslint.validate": [
        "javascript",
        "vue",
        "vue-html",
        "typescript",
        "typescriptreact",
        "html",
        "css",
        "scss",
        "less",
        "json",
        "jsonc",
        "json5",
        "markdown"
    ]
}

4 prettier代码格式化

4.1 集成Prettier

4.1.1 安装Prettier
pnpm install eslint-plugin-prettier prettier -D
4.1.2 添加格式化规则.prettierrc.json

prettier的配置文件格式参考:https://prettier.nodejs.cn/docs/en/configuration.html

在项目根目录下创建文件:prettier.config.js

/**
 * Prettier配置文件
 */
export default {
    /** 使用单引号 */
    singleQuote: true,
    /** 行尾不需要有分号 */
    semi: false,
    /** 使用 4 个空格缩进 */
    tabWidth: 4,
    /** 不使用缩进符,而使用空格 */
    useTabs: false,
    /** 一行代码的最大字符数,这里为 120 字符 */
    printWidth: 120,
    /** 换行符使用 lf */
    endOfLine: 'lf',
    /** object对象中key值是否加引号(quoteProps: "<as-needed|consistent|preserve>")as-needed只有在需求要的情况下加引号,consistent是有一个需要引号就统一加,preserve是保留用户输入的引号 */
    quoteProps: 'as-needed',
    /** 尾部逗号设置,es5是尾部逗号兼容es5,none就是没有尾部逗号,all是末尾需要有逗号,需要node8和es2017以上的环境。 */
    trailingComma: 'all',
    /** 箭头函数只有一个参数的时候,也需要括号 */
    arrowParens: 'always',
    /** 在对象,数组括号与文字之间加空格 "{ foo: bar }" */
    bracketSpacing: true,
    /** html中的空格敏感性,ignore是对HTML全局空白不敏感 */
    htmlWhitespaceSensitivity: 'ignore',
    /** 不使用prettier格式化的文件填写在项目的.prettierignore文件中 */
    ignorePath: '.prettierignore',
    plugins: [
        'prettier-plugin-css-order', // Prettier 格式化CSS后的属性排序插件
    ],
}

4.1.3 添加忽略文件.prettierignore

在项目根目录下创建文件:.prettierignore

/dist/*
/html/*
.local
/node_modules/**
# 忽略静态文件校验
/src/assets/**
**/*.svg
**/*.sh
/public/*
4.1.4 新增prettier代码格式化命令

在package.json中新增perttier代码格式化命令:

{
    "name": "xyz-vue-app-ts-test",
    "scripts": {
        "format": "prettier --write \"./**/*.{css,scss,html,vue,ts,js,json,md}\""
    },
}

手动执行代码格式化,可以看到vue、ts等文件都被格式化好了。

pnpm format

4.2 与ESLint集成

Prettier工作方式prettier扩展会读取项目根目录中的.prettierrc.prettierrc.js之类的配置文件,然后在使用prettier执行格式化时,会根据配置文件的要求对代码进行格式化。

但是在ESLint不仅仅提供代码的语法校验,还提供了代码格式化配置,并且ESLint与Prettier的要求是不一样的。比如ESlint要求这个属性要换行,prettier要求这个属性不换行。好了,你代码是格式化的漂漂亮亮了,但是ESlint不乐意啊,它就给你报红。

解决Prettier跟ESlint冲突

为了解决两者的冲突,我们要使用另一个ESlint插件eslint-plugin-prettier。这个插件不仅提供文件解析,代码fix,也顺带提供了一些规则配置,比如它会把跟prettier冲突的ESlint规则给off掉,并使用自己的规则,也就是说,二选一,让你选prettier。为什么是prettier呢,prettier其实只是提供代码风格的规范,也是目前主流的一个风格规范,那我们就跟着主流呗。其他代码逻辑的规范,就交给ESlint

1、安装插件
pnpm i eslint-plugin-prettier eslint-config-prettier -D
2、添加插件的ESLint配置

这里仅仅是添加eslint-plugin-prettier/recommended,它的作用是关闭与它冲突的ESlint规则,并且启用自己的Recommended规则,并且它会自动合并到Prettier配置

import eslint from '@eslint/js'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import eslintPluginVue from 'eslint-plugin-vue'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'

// 使用带有类型提示的eslint.config()方法,可以获得eslint的类型提示
export default tseslint.config(
    /**
     * typescript 规则
     */
    {
        files: ['**/*.{ts,tsx,vue}'],
        rules: {},
    },

    /**
     * prettier 配置
     * 会合并根目录下的prettier.config.js 文件
     * @see https://prettier.io/docs/en/options
     */
    eslintPluginPrettierRecommended,
)

配置了之后,重新执行pnpm lint语法校验发现错误少了很多。

4.3 CSS样式排序

需要使用 prettier-plugin-css-order 插件。

安装插件

pnpm i -D prettier-plugin-css-order

新增配置

/**
 * Prettier配置文件
 */
export default {
    singleQuote: true,
    // 其他的配置
    plugins: [
        'prettier-plugin-css-order', // Prettier 格式化CSS后的属性排序插件
    ],
}

5 stylelint样式校验(暂不使用)

StyleLint 是一个用于检查和维护 CSS、SCSS 等样式表的工具。它的主要功能是确保代码风格的一致性,并检查样式表是否符合预设的规范。

stylelint 16 推荐使用 v18.20.3+ 或者 v20.13.1+。测试 node v18.20.5 可用

参考:https://blog.csdn.net/2301_78152384/article/details/136671101

5.1 基于scss集成

stylelint命令:

# --fix 自动修复问题,如果无法执行修复问题,请检查stylelint与node的版本对应关系
# --cache 选项 提高性能
# --formatter 选项 自定义输出
npx stylelint "**/*.css" --fix
5.1.1 安装依赖包

stylelint:StyleLint 的核心模块

stylelint-config-standard:StyleLint 官方推荐的配置规则

stylelint-config-prettier:该配置用于解决 StyleLint 和 Prettier 之间的规则冲突问题。将其放在 extends 数组的最后位置,可以确保覆盖 Prettier 的配置

stylelint-scss:该插件是 StyleLint 的 SCSS 扩展,增加了对 SCSS 语法的支持,允许检查和验证 SCSS 文件

stylelint-config-recess-order:css属性排序规则,另外有一个 stylelint-order插件,需要自己配置排序规则

# 仅针对css
pnpm install -D stylelint stylelint-config-standard stylelint-order

# 针对scss,包含css
pnpm install -D stylelint stylelint-order stylelint-config-standard stylelint-config-standard-scss stylelint-config-sass-guidelines stylelint-scss

# 在 HTML/Vue 中使用
pnpm install -D postcss-html

# 使用
pnpm install -D stylelint stylelint-config-standard stylelint-config-standard-scss stylelint-config-sass-guidelines stylelint-config-recess-order stylelint-scss postcss-html



pnpm install -D stylelint stylelint-config-standard-scss


pnpm install -D stylelint stylelint-config-standard-scss stylelint-config-prettier

项目中使用scss作为预处理器,安装以下依赖:

pnpm i -D sass sass-loader stylelint postcss postcss-scss postcss-html stylelint-config-prettier stylelint-config-recess-order stylelint-config-recommended-scss stylelint-config-standard stylelint-config-standard-vue stylelint-scss stylelint-order stylelint-config-standard-scss
5.1.2 配置文件.stylelintrc.cjs

官网:https://stylelint.bootcss.com/

// @see https://stylelint.bootcss.com/

module.exports = {
    extends: [
        'stylelint-config-standard', // 配置stylelint拓展插件
        'stylelint-config-html/vue', // 配置 vue 中 template 样式格式化
        'stylelint-config-standard-scss', // 配置stylelint scss插件
        'stylelint-config-recommended-vue/scss', // 配置 vue 中 scss 样式格式化
        'stylelint-config-recess-order', // 配置stylelint css属性书写顺序插件,
        'stylelint-config-prettier' // 配置stylelint和prettier兼容
    ],
    overrides: [
        {
            files: ['**/*.(scss|css|vue|html)'],
            customSyntax: 'postcss-scss'
        },
        {
            files: ['**/*.(html|vue)'],
            customSyntax: 'postcss-html'
        }
    ],
    ignoreFiles: [
        '**/*.js',
        '**/*.jsx',
        '**/*.tsx',
        '**/*.ts',
        '**/*.json',
        '**/*.md',
        '**/*.yaml'
    ],
    /**
     * null  => 关闭该规则
     * always => 必须
     */
    rules: {
        'value-keyword-case': null, // 在 css 中使用 v-bind,不报错
        'no-descending-specificity': null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器
        'function-url-quotes': 'always', // 要求或禁止 URL 的引号 "always(必须加上引号)"|"never(没有引号)"
        'no-empty-source': null, // 关闭禁止空源码
        'selector-class-pattern': null, // 关闭强制选择器类名的格式
        'property-no-unknown': null, // 禁止未知的属性(true 为不允许)
        'block-opening-brace-space-before': 'always', //大括号之前必须有一个空格或不能有空白符
        'value-no-vendor-prefix': null, // 关闭 属性值前缀 --webkit-box
        'property-no-vendor-prefix': null, // 关闭 属性前缀 -webkit-mask
        'selector-pseudo-class-no-unknown': [
            // 不允许未知的选择器
            true,
            {
                ignorePseudoClasses: ['global', 'v-deep', 'deep'] // 忽略属性,修改element默认样式的时候能使用到
            }
        ]
    }
}
5.1.3 配置忽略文件.stylelintignore

在项目根目录下创建文件:.stylelintignore

/node_modules/*
/dist/*
/html/*
/public/*
5.1.4 配置运行脚本package.json
{
	"scripts": {
        "lint:style": "stylelint src/**/*.{css,scss,vue} --cache --fix"
    }
}

最后配置统一的prettier来格式化我们的js和css,html代码

{
	"scripts": {
        "dev": "vite --open",
        "build": "vue-tsc && vite build",
        "preview": "vite preview",
        "lint": "eslint src",
        "fix": "eslint src --fix",
        "format": "prettier --write \"./**/*.{html,vue,ts,js,json,md}\"",
        "lint:eslint": "eslint src/**/*.{ts,vue} --cache --fix",
        "lint:style": "stylelint src/**/*.{css,scss,vue} --cache --fix"
    }
}

当我们运行pnpm run format的时候,会把代码直接格式化

5.2 基于less集成

6 husky git钩子

在上面我们已经集成好了我们代码校验工具,但是需要每次手动的去执行命令才会格式化我们的代码。如果有人没有格式化就提交了远程仓库中,那这个规范就没什么用。所以我们需要强制让开发人员按照代码规范来提交。

要做到这件事情,就需要利用husky在代码提交之前触发git hook(git在客户端的钩子),然后执行pnpm run format来自动的格式化我们的代码。

6.1 初始化代码提交配置

6.1.1 安装依赖
# 安装husky
pnpm install -D husky

# 初始化husky,注意执行初始化命令之前,项目必须是已经是.git仓库。如果执行错误需要删除package.json中的一些错误信息。比如"prepare": "husky install"配置选项
npx husky-init
6.1.2 配置规则pre-commit

执行npx husky-init命令执行成功后会在根目录下生成个一个.husky目录,在这个目录下面会有一个pre-commit文件,这个文件里面的命令在我们执行代码提交commit的之前就会执行。

.husky/pre-commit文件添加如下命令:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# 在代码执行之前
# (1)执行ts语法检查
# (2)执行ESLint校验和Prettier代码格式化,lint-staged是可以拿到git暂存文件上运行linters的工具
pnpm type-check && npx --no-install lint-staged

当我们对代码进行commit操作的时候,就会执行命令,对代码进行格式化,然后再提交。

6.2 集成lint-staged

lint-staged 可以让你在 Git 暂存(staged)区域中的文件上运行脚本,通常用于在提交前对代码进行格式化、静态检查等操作。

在项目中使用 lint-staged 配合 husky 钩子来执行针对暂存文件的脚本。具体的使用步骤如下:

6.2.1 安装依赖
pnpm install -D lint-staged
6.2.2 配置使用

修改 package.json配置文件,在根配置下,新增 lint-staged 配置:

{
    "name": "xyz-vue-app-ts-test",
    "scripts": {
        "dev": "vite",
    },
    "lint-staged": {
        "*.{css,scss,html,vue,ts,js,json,md}": [
            "eslint --fix",
            "prettier --write"
        ]
    },
}

lint-staged 的配置项可以配置多个类型执行不同的命令,也对不同的文件类型配置不同的命令。

7 commitlint git提交规范

对于我们的commit信息,也是有统一规范的,不能随便写,要让每个人都按照统一的标准来执行,我们可以利用commitlint来实现。

7.1 安装依赖
pnpm i -D @commitlint/config-conventional @commitlint/cli
7.2 添加配置文件commitlint.config.cjs

添加配置文件,新建commitlint.config.cjs(注意是cjs),然后添加下面的代码:

module.exports = {
    extends: ['@commitlint/config-conventional'],
    // 校验规则
    rules: {
        'type-enum': [
            2,
            'always',
            [
                'feat',
                'fix',
                'docs',
                'style',
                'refactor',
                'perf',
                'test',
                'chore',
                'revert',
                'build'
            ]
        ],
        'type-case': [0],
        'type-empty': [0],
        'scope-empty': [0],
        'scope-case': [0],
        'subject-full-stop': [0, 'never'],
        'subject-case': [0, 'never'],
        'header-max-length': [0, 'always', 72]
    }
}
7.3 配置执行脚本package.json

在package.json中添加如下命令

{
	"scripts": {
        "commitlint": "commitlint --config commitlint.config.cjs -e -V"
    },
}

配置结束,现在当我们填写commit信息的时候,前面就需要带着下面的subject

'feat', // 新特性、新功能
'fix', // 修改bug
'docs', // 文档修改
'style', // 代码格式修改, 注意不是 css 修改
'refactor', // 代码重构
'perf', // 优化相关,比如提升性能、体验
'test', // 测试用例修改
'chore', // 其他修改, 比如改变构建流程、或者增加依赖库、工具等
'revert', // 回滚到上一个版本
'build', // 编译相关的修改,例如发布版本、对项目构建或者依赖的改动
7.4 配置husky

在.husky中生成commit-msg命令文件

npx husky add .husky/commit-msg

在生成的commit-msg文件中添加下面的命令

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm commitlint

当我们 commit 提交信息时,就不能再随意写了,必须是 git commit -m ‘fix: xxx’ 符合类型的才可以,需要注意的是类型的后面需要用英文的冒号(“:”),并且冒号后面是需要空一格的,这个是不能省略的

feat: 支持git提交信息规范校验

8 强制使用pnpm包管理器

团队开发项目的时候,需要统一包管理器工具,因为不同包管理器工具下载同一个依赖,可能版本不一样,导致项目出现bug问题,因此包管理器工具需要统一管理!!!

当我们使用npm或者yarn来安装包的时候,就会报错了。原理就是在install的时候会触发preinstall(npm提供的生命周期钩子)这个文件里面的代码。

npm生命周期:https://docs.npmjs.com/cli/v10/using-npm/scripts#npm-install

教程参考:https://www.mulingyuer.com/archives/962/

8.1 自定义按照脚本(不使用插件)

8.1.1 创建配置文件scripts/preinstall.js

在根目录创建scripts/preinstall.js文件,添加下面的内容:

// 配置必须使用pnpm包管理器
if (!/pnpm/.test(process.env.npm_execpath || '')) {
    console.warn(
        `\u001b[33mThis repository must using pnpm as the package manager ` +
        ` for scripts to work properly.\u001b[39m\n`,
    )
    process.exit(1)
}
8.1.2 配置命令package.json

目前使用好像无效:执行npm命令下载包之前不会报错。

{
	"scripts": {
        "preinstall": "node ./scripts/preinstall.js"
    }
}

8.2 使用only-allow插件的方式

按照插件

pnpm i -D only-allow

配置 preinstall 脚本

{
    "name": "xyz-vue-app-ts-test",
    "scripts": {
        "dev": "vite",
        "preinstall": "npx only-allow pnpm"
    },
}

9 环境变量配置

项目开发过程中,一般会经历开发环境、测试环境和生产环境(即正式环境)三个阶段。不同阶段请求的状态(如接口地址等)不尽相同,若手动切换接口地址是相当繁琐且易出错的。于是环境变量配置的需求就应运而生,我们只需做简单的配置,把环境状态切换的工作交给代码。

开发环境(development):顾名思义,开发使用的环境,每位开发人员在自己的dev分支上干活,开发到一定程度,同事会合并代码,进行联调。

测试环境(testing):测试同事干活的环境啦,一般会由测试同事自己来部署,然后在此环境进行测试。

生产环境(production):生产环境是指正式提供对外服务的,一般会关掉错误报告,打开错误日志。(正式提供给客户使用的环境)

注意:一般情况下,一个环境对应一台服务器,也有的公司开发与测试环境是一台服务器。

配置成功后可以通过import.meta.env获取环境变量

9.1 添加配置文件

项目根目录分别添加 开发、生产和测试环境的文件:

# 基本环境
.env
# 开发环境
.env.development
# 测试环境
.env.test
# 生产环境
.env.production

9.2 定义环境变量类型

配置env.d.ts文件

在types目录 index.d.ts 全局变量定义中添加以下代码,可以在在代码中获取用户自定义环境变量的 TypeScript 智能提示:

/** 环境变量参数 */
interface ImportMetaEnv {
    /** 应用的名称 */
    readonly VITE_APP_TITLE: string
    /** 基本的API的请求地址 */
    readonly VITE_APP_BASE_API: string
    /** 基本的服务器地址 */
    readonly VITE_APP_BASE_SERVER: string
    // 更多环境变量...
}

9.3 配置对应环境的内容

.env

# 基础配置,变量必须以 VITE_ 为前缀才能暴露给外部读取
VITE_APP_TITLE=xyz-vue-dev

.env.development

# 开发环境,变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV=development
VITE_APP_BASE_API=./data
VITE_APP_BASE_Server=https://dev.xyz.com

.env.production

# 生产环境,变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV=production
VITE_APP_BASE_API=/pro-api
VITE_APP_BASE_Server=https://pro.xyz.com

.env.test

# 测试环境,变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV=test
VITE_APP_BASE_API=/test-api
VITE_APP_BASE_Server=https://test.xyz.com

9.4 配置运行时命令时使用的环境内容

在package.json配置 --mode 参数,默认读取的是 --mode 默认参数是 development

{
	"scripts": {
        "dev": "vite --open", // 未指定模式的默认取 .env.development 文件中的配置
        "dev:test": "vite --mode test", // 读取 .env.test文件中的配置
        "build": "pnpm type-check && vite build --mode production", // 读取 .env.production文件中的配置
        "preview": "vite preview"
    }
}

9.5 获取环境变量

配置成功后可以通过import.meta.env获取环境变量

<script setup lang="ts">
// 获取环境变量信息
console.log('import.meta.env', import.meta.env)
console.log('import.meta.env.VITE_APP_BASE_API', import.meta.env.VITE_APP_BASE_API)
</script>

9.6 在vite.config.ts中获取到环境变量

vite提供了 loadEnv() 方法用来获取当前的环境变量。

vite.config.ts

import path from 'path'
import { defineConfig, loadEnv, UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ViteConfigDev from './build/vite.config.dev'
import ViteConfigProd from './build/vite.config.prod'

/** 根据是否为开发环境、生产打包环境,使用不同的vite配置 */
const EnvResolver: Record<'serve' | 'build', (env: ImportMetaEnv) => UserConfig> = {
    serve: (env: ImportMetaEnv) => ({ ...ViteConfigDev(env) }),
    build: (env: ImportMetaEnv) => {
        console.log('vite开始打包...', env)
        return { ...ViteConfigProd }
    },
}

/**
 * vite配置项,具体配置请到build目录下进行详细配置
 */
export default defineConfig(({ command, mode }) => {
    console.log('command', command)
    // 获取到环境变量配置
    const env = loadEnv(mode, process.cwd()) as ImportMetaEnv
    const { server, base, build, resolve = {}, plugins = [] } = EnvResolver[command](env)
    // 统一返回配置:省略其他配置
    return {
        plugins: [ vue() ],
    }
})

10 Vite相关的配置

10.1 打包结果的文件类型分类

import { defineConfig } from 'vite'

/**
 * vite打包环境配置
 * https://vite.dev/config/
 */
export default (env: ImportMetaEnv) => {
    console.log('env', env)
    return defineConfig({
        // 配置打包后使用相对路径
        base: './',
        build: {
            /** 打包后的输出目录,默认为 dist */
            outDir: './dist',
            rollupOptions: {
                output: {
                    chunkFileNames: 'static/js/[name]-[hash].js',
                    entryFileNames: 'static/js/[name]-[hash].js',
                    assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
                },
            },
        },
    })
}

10.3 分包策略

在项目中,对于第三方库来说,一般不会进行版本的变更(比较稳定);而需要进行项目打包基本上是我们的业务代码变更了才需要进行重新打包部署的(不稳定的模块)。

一般的打包中,默认是将所有库都打包到一个js中,打包的结果文件名一般会带上文件指纹(一般为:文件名 + 哈希值)。在不分包的情况下,如果我们的业务代码发生变化时,就会导致打包出来的结果文件名(哈希值)发生了变化。这种情况下,浏览器发现请求的文件发生变化了,就不会使用缓存,而是需要到远程的服务器去加载内容。在这里产生了需要优化的控件,我们就需要对内容进行分包处理。

从Vite 2.9 起,默认是不会自动分包的(参考自:https://vitejs.cn/vite3-cn/guide/build.html#customizing-the-build)。Vite给我们提供了Rollup的打包的 manualChunks 配置项进行分包的设置。

另外:可以安装 lodash 库进行验证分包策略,也可以使用 vite官方提供的 splitVendorChunkPlugin() 插件进行分包

import { defineConfig } from 'vite'

/**
 * vite打包环境配置
 * https://vite.dev/config/
 */
export default (env: ImportMetaEnv) => {
    console.log('env', env)
    return defineConfig({
        build: {
            /** 打包后的文件内容不进行压缩,方便阅读 */
            // minify: false,
            rollupOptions: {
                output: {
                    // 其他配置项
                    /**
                     * 打包分块
                     * @param {string} id 资源的id,就是文件的路径
                     * @return {string | void} 分块名称,如果什么都不返回则使用默认值
                     */
                    manualChunks: (id) => {
                        // console.log('id', id)
                        // 将所有的第三方库都打包到 vendor 分块中
                        if (id.includes('node_modules')) {
                            return 'vendor'
                        }
                    },
                },
            },
        },
    })
}

10.4 打包压缩

插件地址:https://github.com/nonzzz/vite-plugin-compression

安装插件
pnpm install -D vite-plugin-compression2
使用插件

修改vite.config.prod.ts配置,增加压缩插件支持

import { defineConfig } from 'vite'
import { compression } from 'vite-plugin-compression2' // 打包压缩

/**
 * vite打包环境配置
 * https://vite.dev/config/
 */
export default (env: ImportMetaEnv) => {
    console.log('env', env)
    return defineConfig({
        // 其他配置内容
        plugins: [
            // 打包压缩支持
            compression({
                threshold: 10240, // 对 10kb 以上的文件进行压缩
            }),
        ],
    })
}

10.5 文件拷贝

import { defineConfig } from 'vite'
import copyPlugin from 'rollup-plugin-copy'

/**
 * vite打包环境配置
 * https://vite.dev/config/
 */
export default (env: ImportMetaEnv) => {
    console.log('env', env)
    return defineConfig({
        // 配置打包后使用相对路径
        base: './',
        build: {
            /** 打包后的输出目录,默认为 dist */
            outDir: './dist',
            rollupOptions: {
                plugins: [
                    // 使用拷贝插件
                    copyPlugin({
                        targets: [{ src: 'src/assets/systemComponents/*', dest: 'dist/systemComponents' }],
                        hook: 'writeBundle',
                    }),
                ],
            },
        },
    })
}

11 API集成

10.1 axios二次封装

10.1.1 安装axios
pnpm install axios
10.1.2 二次封装

在开发项目的时候避免不了与后端进行交互,因此我们需要使用axios插件实现发送网络请求。在开发项目的时候我们经常会把axios进行二次封装。

目的:

1、使用请求拦截器,可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)

2、使用响应拦截器,可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)

在根目录下创建utils/request.ts

import axios from "axios";
import { ElMessage } from "element-plus";
//创建axios实例
let request = axios.create({
    baseURL: import.meta.env.VITE_APP_BASE_API,
    timeout: 5000
})
//请求拦截器
request.interceptors.request.use(config => {
    return config;
});
//响应拦截器
request.interceptors.response.use((response) => {
    return response.data;
}, (error) => {
    //处理网络错误
    let msg = '';
    let status = error.response.status;
    switch (status) {
        case 401:
            msg = "token过期";
            break;
        case 403:
            msg = '无权访问';
            break;
        case 404:
            msg = "请求地址错误";
            break;
        case 500:
            msg = "服务器出现问题";
            break;
        default:
            msg = "无网络";

    }
    console.error({
            type: 'error',
            message: msg,
        })
    return Promise.reject(error);
});
export default request;

10.2 声明全局的响应接口

在全局类型申明 types/index.d.ts 下增加响应接口定义。这样我们就可以在全局不用导入就可以使用了。

/** Record<string, any> */
type AnyObject = Record<string, any>

/**
 * 接口返回的数据格式
 */
interface ResponseData<T> {
    code: number
    data: T
    message: string
}

10.3 定义接口

在开发项目的时候,接口可能很多需要统一管理。在src目录下去创建api文件夹去统一管理项目的接口。

import request from '@/utils/request'

/**
 * 获取用户的菜单列表
 * @returns {Promise<ResponseData<any>>}
 */
export function getUserMenusApi(): Promise<ResponseData<any>> {
    return request.get('/menus.json')
}

在public/data/menus.json下创建临时测试数据

{
    "code": 0,
    "message": "查询用户菜单列表成功",
    "data": [
        {
            "id": "1",
            "code": "index"
        }
    ]
}

10.4 调用接口

在 App.vue 中导入接口并测试调用

import { getUserMenusApi } from '@/api/rbac'


getUserMenusApi().then((result) => {
    console.log('用户菜单列表', result)
})

12 样式集成

SCSS、Less一般选择一个即可,unocss、tailwindcss一般也选择一个即可。

12.1 normalize 样式重置

Normalize.css 只是一个很小的CSS文件,但它在默认的HTML元素样式上提供了跨浏览器的高度一致性(解决不同浏览器之间的样式差异)。

1、安装
pnpm i normalize.css
2、mian.ts中使用
import 'normalize.css' // 引入重置样式的库

12.2 集成SCSS与全局样式

12.2.1 集成SCSS
1、安装scss
pnpm i -D sass-loader sass
2、vue文件使用
<style scoped lang="scss"></style>
12.2.2 全局变量
1、定义全局变量

创建文件全局变量样式定义:src/styles/variables.scss

// 要用的公共的变量
$vw: 19.2vw;
$vh: 10.8vh;

// 字体大小
$app-font-size: 14px;

// 主题色
$app-bg-color: #f5f5f5;
// 主题高亮色
$app-bg-height-color: #666666;
// 字体色
$app-font-color: #222222;
// 字体高亮色
$app-font-height-color: #ffff43;
2、在文件中引入全局变量

我们定义的全局变量文件,一般需要在全局中都使用的,所以我们可以通过vite进行全局的样式注入。

在vite.config.ts文件中进行如下配置:

/**
 * vite配置项,具体配置请到build目录下进行详细配置
 */
export default defineConfig(({ command, mode }) => {
    console.log('command', command)
    // 获取到环境变量配置
    const env = loadEnv(mode, process.cwd()) as ImportMetaEnv
    const { server, base, build, resolve = {}, plugins = [] } = EnvResolver[command](env)
    // 统一返回配置
    return {
        // 其他配置
        /** 样式处理配置 */
        css: {
            preprocessorOptions: {
                scss: {
                    javascriptEnabled: true,
                    additionalData: '@import "./src/styles/variables.scss";\n',
                },
            },
        },
    }
})

配置完毕你会发现scss提供这些全局变量可以在组件样式中使用了。

.message {
    width: 200px;
    color: $app-font-color;
}
12.2.3 公共样式
1、common.scss 公共样式文件

这里的样式需要依赖前面定义的全局变量 variables.scss

// 全局公共样式定义

* {
    // 从边框开始计算
    box-sizing: border-box;
    // 去除边距
    margin: 0;
    padding: 0;
}

html {
    height: 100%;
    font-size: $app-font-size;
}

body {
    min-width: 1240px;
    height: 100%;
    color: $app-font-color;
    font:
        $app-font-size 'Microsoft Yahei',
        'PingFang SC',
        'Avenir',
        'Segoe UI',
        'Hiragino Sans GB',
        'STHeiti',
        'Microsoft Sans Serif',
        'WenQuanYi Micro Hei',
        sans-serif;
}

a {
    outline: none;
    color: $app-font-color;
    text-decoration: none;
}

i {
    font-style: normal;
}

// input的样式
input[type='text'],
input[type='search'],
input[type='password'],
input[type='checkbox'] {
    -webkit-appearance: none;
    outline: none;
    border: none;
    padding: 0;

    &::placeholder {
        color: $app-font-color;
    }
}

img {
    vertical-align: middle;
    max-width: 100%;
    max-height: 100%;
}

ul {
    list-style: none;
}

#app {
    // 默认背景色
    background: $app-bg-color;
    width: 100%;
    height: 100%;
    // 内容不能被选中
    user-select: none;
}

.container {
    position: relative;
    margin: 0 auto;
    width: 1240px;
}

.ellipsis {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ellipsis-2 {
    display: -webkit-box;
    text-overflow: ellipsis;
    word-break: break-all;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
}

// 左浮动
.fl {
    float: left;
}

// 右浮动
.fr {
    float: right;
}

// 清除浮动
.clearfix:after {
    display: block;
    clear: both;
    visibility: hidden;
    height: 0;
    content: '.';
    line-height: 0;
}

// 定义滚动条高宽及背景:高宽分别对应横竖滚动条的尺寸
::-webkit-scrollbar {
    border-radius: 4px;
    background-color: transparent;
    width: 4px;
    height: 4px;
}

// 定义滚动条轨道:内阴影+圆角
::-webkit-scrollbar-track {
    background-color: transparent;
}

// 定义滑块:内阴影+圆角
::-webkit-scrollbar-thumb {
    border-radius: 4px;
    background-color: transparent;
}

// 定义滑块:正常样式不显示滚动条,移入时显示
:hover::-webkit-scrollbar-thumb {
    background-color: #dddddd;
}

2、使用全局样式

在src/styles目录下创建一个index.scss文件,在index.scss引入common.scss

@import './common.scss'; // 全局公共样式

// 灰色模式
.html-grey {
    filter: grayscale(100%);
}

// 色弱模式
.html-weakness {
    filter: invert(80%);
}

在项目入口文件(main.ts)引入样式入口文件(index.css)

import { createApp } from 'vue'
import 'normalize.css'
import '@/styles/index.scss' // 全局的样式定义
import App from './App.vue'

createApp(App).mount('#app')

12.3 集成Less与全局样式

12.3.1 集成Less
1、安装Less
pnpm i less-loader less --save-dev
12.3.2 全局变量
1、定义全局变量

创建文件全局变量样式定义:src/styles/variables.less

// 要用的公共的变量
@vw: 19.2vw;

// 字体大小
@app-font-size: (20/@vw);

// 主题色
@app-bg-color: #f5f5f5;
// 主题高亮色
@app-bg-height-color: #666666;
// 字体色
@app-font-color: #222222;
// 字体高亮色
@app-font-height-color: #ffff43;
2、vite.config.js
import {defineConfig} from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [vue()],
    // 以下部分是配置公用less的代码
    css: {
        preprocessorOptions: {
            less: {
                additionalData: `@import './src/assets/styles/variables.less';`
            }
        }
    }
});
12.3.3 公共样式

依赖于前面定义的公共变量

1、common.less公共样式文件
// 重置样式

* {
    // 从边框开始计算
    box-sizing: border-box;
    // 去除边距
    margin: 0;
    padding: 0;
}

html {
    height: 100%;
    font-size: @app-font-size;
}

body {
    height: 100%;
    color: @app-font-color;
    min-width: 1240px;
    font: @app-font-size 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif
}

a {
    text-decoration: none;
    color: @app-font-color;
    outline: none;
}

i {
    font-style: normal;
}

// input的样式
input[type="text"],
input[type="search"],
input[type="password"],
input[type="checkbox"] {
    padding: 0;
    outline: none;
    border: none;
    -webkit-appearance: none;

    &::placeholder {
        color: @app-font-color;
    }
}

img {
    max-width: 100%;
    max-height: 100%;
    vertical-align: middle;
}

ul {
    list-style: none;
}

#app {
    // 默认背景色
    background: @app-bg-color;
    //不能被选中
    user-select: none;
    width: 100%;
    height: 100%;
}

.container {
    width: 1240px;
    margin: 0 auto;
    position: relative;
}

.ellipsis {
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
}

.ellipsis-2 {
    word-break: break-all;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
}

// 左浮动
.fl {
    float: left;
}

// 右浮动
.fr {
    float: right;
}

// 清除浮动
.clearfix:after {
    content: ".";
    display: block;
    visibility: hidden;
    height: 0;
    line-height: 0;
    clear: both;
}

// 定义滚动条轨道:内阴影+圆角
::-webkit-scrollbar-track {
    background-color: transparent;
}

// 定义滑块:内阴影+圆角
::-webkit-scrollbar-thumb {
    border-radius: 4px;
    background-color: transparent;
}

// 定义滑块:正常样式不显示滚动条,移入时显示
:hover::-webkit-scrollbar-thumb {
    background-color: #dddddd;
}
2、使用全局样式

在src/styles目录下创建一个index.scss文件,在index.scss引入common.scss

// 引入项目的重置样式和公用样式
import "./common.less";

在项目入口文件(main.ts)引入样式入口文件(index.css)

import { createApp } from 'vue'
import 'normalize.css'
import '@/styles/index.less' // 全局的样式定义
import App from './App.vue'

createApp(App).mount('#app')

12.4 unocss

1、安装
pnpm i -D unocss
2、vite集成
import path from 'path'
import { defineConfig, loadEnv, UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'

/**
 * vite配置项,具体配置请到build目录下进行详细配置
 */
export default defineConfig(({ command, mode }) => {
    console.log('command', command)
    // 获取到环境变量配置
    const env = loadEnv(mode, process.cwd()) as ImportMetaEnv
    const { server, base, build, resolve = {}, plugins = [] } = EnvResolver[command](env)
    // 统一返回配置
    return {
        plugins: [
            vue(),
            UnoCSS(),
            ...plugins,
        ],
    }
})

3、配置文件

在根目录中创建uno.config.ts配置文件

import { defineConfig } from 'unocss'

/**
 * UnoCSS 配置
 */
export default defineConfig({
    // ...UnoCSS options
})

4、在main.ts引入
import 'virtual:uno.css'

12.5 tailwindcss

12.5.1 安装并使用tailwindcss
1、安装

使用 Using PostCSS 的方式集成 tailwindcss

pnpm install -D tailwindcss postcss autoprefixer
2、初始化配置文件

执行命令后,会在项目根目录下生成两个文件:postcss.config.js 、tailwind.config.js

npx tailwindcss init -p
3、配置tailwind

修改tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
    darkMode: 'class',
    corePlugins: {
        preflight: false,
    },
    content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
    theme: {
        extend: {
            // colors: {
            //     'bg-color': 'var(--el-bg-color)',
            //     primary: 'var(--el-color-primary)',
            //     'primary-light-9': 'var(--el-color-primary-light-9)',
            //     'text-color-primary': 'var(--el-text-color-primary)',
            //     'text-color-regular': 'var(--el-text-color-regular)',
            //     'text-color-disabled': 'var(--el-text-color-disabled)',
            // },
        },
    },
}

修改postcss.config.js

export default {
    plugins: {
        'postcss-import': {},
        tailwindcss: {},
        autoprefixer: {},
        ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
    },
}

4、导入tailwind 样式

在 src/styles 下新建 tailwind.scss 文件

// tailwind 样式
// @tailwind base;
@tailwind components;
@tailwind utilities;

导入tailwind.scss,在 src/styles/index中导入tailwind.scss

// 全局公共样式
@import './common.scss';
// 引入 tailwind 样式
@import './tailwind.scss';

配置完成后,可能需要vite开发服务器才会看到效果。

12.5.2 Prettier配置支持
1、安装插件
pnpm install -D prettier-plugin-tailwindcss
2、配置

在 prettier.config.js 配置中,新增 “prettier-plugin-tailwindcss” 配置

/**
 * Prettier配置文件
 */
export default {
    plugins: [
        'prettier-plugin-tailwindcss', // Prettier 对 Tailwind CSS 格式化支持
    ],
}

3、测试

可以编写样式 w-full ml-3 bg-amber-200 font-normal 进行测试,会变成 ml-3 w-full bg-amber-200 font-normal

<div class="w-full ml-3 bg-amber-200 font-normal">
    <a href="https://vite.dev" target="_blank">
        <img alt="Vite logo" class="logo" src="/vite.svg" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
        <img alt="Vue logo" class="logo vue" src="./assets/vue.svg" />
    </a>
</div>

13 集成iconfont、svg图标

13.1 iconfont图标支持

将从阿里巴巴iconfont中下载的资源,解压后并命名为 iconfont,将文件夹拷贝到 src/assets 下。

13.1.1 使用iconfont图标
1、导入iconfont

在main.ts中导入iconfont

import '@/styles/index.scss' // 全局的样式定义
import '@/assets/iconfont/iconfont.js' // 导入iconfont图层
2、测试使用iconfont

注意:需要将 icon-xxx 中的 xxx 替换为真实的图标名称

<!--测试用iconfont-->
<svg aria-hidden="true" class="icon">
    <use xlink:href="#icon-xxx"></use>
</svg>
13.1.2 全局iconfont图标组件定义
1、获取项目图标工具列表

在src/utils下创建icon.ts图标工具,用来获取图标列表。

/**
 * @ClassName icon
 * @Description 图标工具类
 * @Author xuyizhuo
 * @Date 2024/12/8 22:02
 */
// import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { glyphs } from '@/assets/iconfont/iconfont.json'

const ElementPlusIconsVue = {} // 还没导入Element plus图标,先临时这么处理

/** Element plus 图标列表 */
export const elIconNames = Object.keys(ElementPlusIconsVue)

// 获取所有的iconfont图标
const iconNameList = []
for (let i = 0, length = glyphs.length; i < length; i++) {
    iconNameList.push(glyphs[i].font_class)
}

/** iconfont 图标列表 */
export const iconfonts = iconNameList // iconfont图标

/** iconfont、element plus 的图标列表 */
export const AllIconNames = [...elIconNames, ...iconNameList] // el、iconfont图标

2、封装全局图标组件

components下创建 BaseComponents文件夹用于存放基础的公共组件。

在 components/BaseComponents文件夹中创建 BaseIcon.vue文件,用于封装对iconfont等图标的支持

<script lang="ts" setup>
/**
 * @ClassName BaseIcon
 * @Description 基础图标,支持el-icon图标库组件、iconfont等图标
 * @Author xuyizhuo
 * @Date 2024/12/8 22:01
 * @example
 *      <BaseIcon name="icon_calendar" />
 *      <BaseIcon name="图标名称" size="20" />
 */
import { elIconNames } from '@/utils/icon'
import type { Component } from 'vue'

/** 图标组件参数 */
export interface IconProps {
    /** 图标的颜色 */
    color?: string
    /** 图标的大小 */
    size?: string | number
    /** 所使用的图标名称 */
    name: string | Component
    /** 图标的样式 */
    style?: Record<string, any> // AnyObject // Record<string, any>
}

withDefaults(defineProps<IconProps>(), {
    size: 16,
    color: '',
    style: () => ({}),
})
</script>

<template>
    <el-icon
        v-if="elIconNames.includes(name as string)"
        :color="color"
        :size="Number(size)"
        :style="{
            ...(style || {}),
        }"
        class="base-icon"
    >
        <component :is="name" />
    </el-icon>
    <i
        v-else-if="typeof name === 'string'"
        :style="{
            fontSize: (size || 16) + 'px',
            ...(style || {}),
        }"
        class="base-icon"
    >
        <svg
            :style="{
                color,
            }"
        >
            <use :xlink:href="`#icon-${name}`" />
        </svg>
    </i>
    <i
        v-else
        :style="{
            fontSize: (size || 16) + 'px',
            ...(style || {}),
        }"
        class="base-icon"
    >
        <component
            :is="name"
            :style="{
                color,
            }"
        />
    </i>
</template>

<style lang="scss" scoped>
.base-icon {
    display: inline-flex;
    position: relative;
    justify-content: center;
    align-items: center;
    width: 1em;
    height: 1em;
    line-height: 1em;
    fill: currentColor;

    svg {
        width: 1em;
        height: 1em;
    }
}
</style>

3、统一导出全局组件

在 components/BaseComponents文件夹中创建 baseComponents.ts文件

/**
 * @description: 公共组件
 */
import type { App, Component } from 'vue'
import BaseIcon from './BaseIcon.vue'

type Components = {
    [propName: string]: Component
}

// 组件列表
const components: Components = {
    BaseIcon,
}
/**
 * @ClassName common
 * @Description 批量导出组件,用于注册全局组件
 * @Author xuyizhuo
 * @Date 2024/12/8 22:36
 */
export default {
    /**
     * 全局注册组件,用Vue变量名在webstorm可以识别到组件已经注册,用其他名称无法识别。暂不清楚具体原因
     * @param {App} Vue
     */
    install: (Vue: App) => {
        for (const componentName in components) {
            // 在app上进行扩展,app提供 component directive 函数
            // 如果要挂载原型 app.config.globalProperties 方式
            Vue.component(componentName, components[componentName])
        }
    },
}

4、全局注册组件

在main.ts中全局注册组件

import { createApp } from 'vue'
import baseComponents from '@/components/BaseComponents/baseComponents' // 导入基础组件
import App from './App.vue'

createApp(App).use(baseComponents).mount('#app')

5、使用图标

在上面全局注册后就可以全局使用组件了。

<BaseIcon name="icon_calendar" size="20" />

13.2 SVG图标支持

在开发项目的时候经常会用到svg矢量图,而且我们使用SVG以后,页面上加载的不再是图片资源,这对页面性能来说是个很大的提升,而且我们SVG文件比img要小的很多,放在项目中几乎不占用资源。

建议使用iconfont,这里的集成设置不来颜色

13.2.1 安装SVG依赖插件
pnpm install -D vite-plugin-svg-icons

# 另外一个插件,这里
pnpm install -D vite-svg-loader
13.2.2 配置插件vite.config.ts

统一将svg文件放到 src/assets/svg 下。

在vite.config.ts中配置插件

import {createSvgIconsPlugin} from 'vite-plugin-svg-icons'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue(),
        createSvgIconsPlugin({
            // 指定要缓存的图标文件夹
            iconDirs: [path.resolve(process.cwd(), 'src/assets/svg')],
            // 指定符号ID格式
            symbolId: 'icon-[dir]-[name]',
        }),
    ],
})
13.2.3 导入插件

在根目录src/main.ts中导入

import 'virtual:svg-icons-register'
13.2.4 将svg封装为全局组件

直接使用iconfont中定义的全局组件即可支持加载图标

1、svg组件

因为项目很多模块需要使用svg图标,因此把它封装为全局组件,在src/components目录下创建一个SvgIcon组件:

<script setup lang="ts">
defineProps({
    //xlink:href属性值的前缀
    prefix: {
        type: String,
        default: '#icon-',
    },
    //svg矢量图的名称
    name: String,
    //svg图标的颜色
    color: {
        type: String,
        default: '',
    },
    //svg图标的宽度
    width: {
        type: String,
        default: '16px',
    },
    //svg图标的高度
    height: {
        type: String,
        default: '16px',
    },
})
</script>

<template>
    <div>
        <svg :style="{ width: width, height: height }">
            <use :xlink:href="prefix + name" :fill="color"></use>
        </svg>
    </div>
</template>

<style scoped></style>
2、注册为全局组件

src/components文件夹目录下创建一个index.ts文件:用于注册components文件夹内部全部全局组件。

src/components/index.ts

import SvgIcon from './SvgIcon.vue'
import type {App, Component} from 'vue'

const components: { [name: string]: Component } = {SvgIcon}
// 注册组件
export default {
    install(app: App) {
        Object.keys(components).forEach((key: string) => {
            app.component(key, components[key])
        })
    },
}

在项目的入口文件引入src/main.ts文件,通过app.use()方法安装自定义插件

import {createApp} from 'vue'
import App from './App.vue'
import 'virtual:svg-icons-register'
import gloableComponent from './components/index.ts'

const app = createApp(App)
app.use(gloableComponent)
app.mount('#app')

使用svg

<BaseIcon name="add" size="20" />
<BaseIcon name="data" size="10" />
<BaseIcon name="clear-range" size="20" />

14 router路由导航

在设置导航前,先清除掉构建工程、前面测试的示例代码,比如HelloWorld.vue

14.1 路由初始化

14.1.1 安装路由
pnpm install vue-router
14.1.2 初始化路由配置
14.1.2.1 准备工作

创建 views用于存放界面

1、appHome.vue

views/appHome.vue

应用首页

<script lang="ts" setup>
/**
 * @ClassName appHome
 * @Description 应用首页
 * @Author xuyizhuo
 * @Date 2024/12/10 20:22
 */
</script>

<template>
    <div class="app-home">
        <h1>首页</h1>
        <router-link to="/login">退出登录</router-link>
    </div>
</template>

<style lang="scss" scoped>
.app-home {
    font-size: 20px;
}
</style>

2、appLogin.vue

views/appLogin.vue

app的登录页

<script lang="ts" setup>
/**
 * @ClassName app-login
 * @Description 用户登录
 * @Author xuyizhuo
 * @Date 2024/12/10 20:24
 */
</script>

<template>
    <div class="app-login">
        <h3>用户登录</h3>
        <router-link to="/">登录</router-link>
    </div>
</template>

<style lang="scss" scoped>
.app-login {
    font-size: 20px;
}
</style>

3、errorPage404.vue
<template>
    <div class="error-page-404">404错误界面</div>
</template>

<script lang="ts" setup>
/**
 * @ClassName errorPage404.vue
 * @Description 404错误界面
 * @Author xuyizhuo
 * @Date 2024/12/10 20:49
 */
</script>

<style lang="scss" scoped>
.error-page-404 {
    color: red;
}
</style>
14.1.2.2 全局路由配置

在src下创建一个 router 文件夹,用于存放所有的路由配置,在其中创建:

(1)modules:用于存放各模块的路由配置

(2)index.ts:存放根路由配置

index.ts创建路由
import { createRouter, createWebHistory } from 'vue-router'
import appHome from '@/views/home/appHome.vue'
import appLogin from '@/views/login/appLogin.vue'

const HomePage = appHome // 首页路由
const ErrorPage404 = () => import('@/views/error/errorPage404.vue') // 错误路由

const routes = [
    { path: '/', component: HomePage }, // 首页
    { path: '/login', component: appLogin }, // 登录页
    { path: '/:pathMatch(.*)*', component: ErrorPage404 }, // 未匹配到路径跳转404
]

const router = createRouter({
    history: createWebHistory(),
    routes,
})

export default router

14.1.2.5 使用路由
1、main.ts

挂载路由

import router from '@/router' // 导入路由
import App from './App.vue'

// use(router) // 挂载路由
createApp(App).use(router).mount('#app')
2、App.vue

主界面,放置根路由

<script lang="ts" setup></script>

<template>
    <!--路由-->
    <router-view />
</template>

<style lang="scss" scoped></style>

14.2 路由动画

修改App.vue,添加类,并设置样式

<script lang="ts" setup></script>

<template>
    <!--路由-->
    <router-view v-slot="{ Component }">
        <transition name="fade">
            <component :is="Component" />
        </transition>
    </router-view>
</template>

<style lang="scss" scoped>
// 路由切换时的动画效果
// 进入时的透明度为0 和 刚开始进入时的原始位置通过active透明度渐渐变为1
.fade-enter-from {
    transform: translateX(-100%);
    opacity: 0;
}

// 进入完成后的位置 和 透明度
.fade-enter-to {
    transform: translateX(0%);
    opacity: 1;
}

.fade-leave-active,
.fade-enter-active {
    transition: all 300ms ease-out;
}

// 页面离开时一开始的css样式,离开后为leave-to,经过active渐渐透明
.fade-leave-from {
    transform: translateX(0%);
    opacity: 1;
}

// 离开后的透明度通过下面的active阶段渐渐变为0
.fade-leave-to {
    transform: translateX(100%);
    opacity: 0;
}
</style>

14.3 路由跳转进度条

14.3.1 安装进度条插件nprogress
# 安装nprogress
pnpm i nprogress
# 安装ts依赖支撑
pnpm i -D @types/nprogress
14.3.2 封装进度条工具类

在src/utils中创建 progress.ts 工具类

import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

// 全局进度条的配置
NProgress.configure({
    easing: 'ease', // 动画方式
    speed: 500, // 递增进度条的速度
    showSpinner: false, // 是否显示加载ico
    trickleSpeed: 200, // 自动递增间隔
    minimum: 0.3, // 更改启动时使用的最小百分比
    parent: 'body', //指定进度条的父容器
})

/**
 * @ClassName progress
 * @Description 进度条支持
 * @Author xuyizhuo
 * @Date 2024/12/11 20:40
 */
export default class Progress {
    /**
     * 开启进度条
     */
    static start() {
        NProgress.start()
    }

    /**
     * 关闭进度条
     */
    static close() {
        NProgress.done()
    }
}

14.3.3 在路由跳转中使用进度条
import Progress from '@/utils/progress'

/**
 * 全局的前置守卫
 */
router.beforeEach(async (to, from, next) => {
    // 开启进度条
    Progress.start()

    // 省略其他代码,这里直接泛型路由
    next()
})

/**
 * 路由后置守卫
 */
router.afterEach(() => {
    // 结束进度条
    Progress.close()
})

14.4 路由缓存

15 pinia数据存储

https://pinia.vuejs.org/zh/

15.1 安装pinia

# 安装
pnpm install pinia

# 卸载
pnpm un pinia

15.2 初始化pinal

15.2.1 index.ts

在 src/store下创建index.ts用于初始化 pinal

/**
 * @Description store index
 * @Author xuyizhuo
 * @Date 2022/11/19
 */
// 导入依赖
import {createPinia} from "pinia";

// 创建Pinia store
const store = createPinia();

export default store;
15.2.2 main.ts
import { createApp } from 'vue'
import router from '@/router' // 导入路由
import store from '@/store' // 导入pinia
import App from './App.vue'

createApp(App).use(router).use(store).mount('#app')

15.3 模块化store

15.3.1 存储各模块store key

在创建 store 下创建 constant.ts,用于存储常量信息。

/**
 * @ClassName SystemInfo
 * @Description 系统信息存储
 * @Author xuyizhuo
 * @Date 2024/12/11 22:03
 */
export class SystemInfo {
    /** 系统名称 */
    static readonly NAME = import.meta.env.VITE_APP_TITLE
}

/**
 * 各模块的存储key
 * @type {{USER_INFO: string, MENU_INFO: string, ROUTER_INFO: string}}
 */
export class ModuleStoreKeys {
    /** 用户模块 */
    static readonly USER = 'userStore'
}

15.3.2 用户模块信息存储

在 store/modules 下创建 userStore.ts

user模块数据存储

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ModuleStoreKeys } from '@/store/constant'

/**
 * @ClassName userStore
 * @Description 用户相关信息存储
 * @Author xuyizhuo
 * @Date 2024/12/11 22:00
 */
export default defineStore(ModuleStoreKeys.USER, () => {
    const token = ref('')
    const userInfo = ref({
        uname: 'xuyizhuo',
    })

    const updateUserInfo = () => {
        userInfo.value.uname = 'XyzGIS'
    }

    return { userInfo, token, updateUserInfo }
})

15.3.3 使用用户模块中的数据

在router/index.js使用

<script lang="ts" setup>
/**
 * @ClassName appHome
 * @Description 应用首页
 * @Author xuyizhuo
 * @Date 2024/12/10 20:22
 */
import userStore from '@/store/modules/userStore'

// 如果模块的store中出现 ...toRefs()解构,需要使用pinia的storeToRefs() 解构才能获取到响应式的数据
const { userInfo, updateUserInfo } = userStore()

const onUpdate = () => {
    updateUserInfo()
}
</script>

<template>
    <div class="app-home">
        <h1>首页</h1>
        <div>用户名:{{ userInfo.uname }}</div>
        <button @click="onUpdate()">修改用户名</button>
        <router-link to="/login">退出登录</router-link>
    </div>
</template>

<style lang="scss" scoped>
.app-home {
    font-size: 20px;
}
</style>

15.4 数据持久化存储

15.4.1 安装插件
# 使用此插件即可
pnpm i pinia-plugin-persistedstate
pnpm uninstall pinia-plugin-persistedstate


# 此插件坑比较多
pnpm i pinia-plugin-persist --save
15.4.2 注册插件
/**
 * @Description store index
 * @Author xuyizhuo
 * @Date 2022/11/19
 */
// 导入依赖
import {createPinia} from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; // 导入数据存储插件

// 创建Pinia store
const pinia = createPinia();

pinia.use(piniaPluginPersistedstate);  // 注册存储插件到Pinia

export default pinia;
15.4.3 验证存储

修改token,验证存储。

如果只是获取数据,这是看不到 持久化存储 的效果的。

import {useRouter} from "vue-router"; // 路由
import useUserStore from "@/stores/user"; // 导入User store

const router = useRouter();
const store = useUserStore();

/**
 * 登录
 */
function login(){
    const login = userLogin();
    login.then(result => {
        // 登录成功
        console.log("store", store);
        store.token = "xuasdjfowerohskfjaslfjp[sarjpwerpskgpasipfsf";
        router.push("/"); // 路由跳转
    });
}

15.5 简单store

来源pinia官网

你可能会认为可以通过一行简单的 export const state = reactive({}) 来共享一个全局状态。

16 工具库集成

16.1 dayjs

16.1.1 集成dayjs

参考:https://dayjs.fenxianglu.cn/category/#typescript

安装

pnpm i dayjs

基本使用

main.ts

import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn' // 导入本地化语言

dayjs.locale('zh-cn') // 使用本地化语言

appHome.vue

import dayjs from 'dayjs'

console.log('dayjs().format()', dayjs().format('YYYY-MM-DD HH:mm:ss:SSS'))
16.1.2 数据格式化
dataFormat.ts

在utils下创建数据格式化工具类:dataFormat.ts

import { at, isArray, isString } from 'lodash-es'
import dayjs from 'dayjs'

/**
 * @ClassName DataFormat
 * @Description 数据格式化
 * @Author xuyizhuo
 * @Date 2024/12/15 18:43
 */
export default class DataFormat {
    /** 数据为空时的默认值 */
    static readonly DEFAULT_VALUE = '-' // 默认值

    /** 日期格式:精确到分钟 */
    static readonly DATE_FORMAT = 'YYYY-MM-DD'

    /** 时间格式:精确到分钟 */
    static readonly DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'

    /** 时间格式:精确到毫秒数 */
    static readonly DATE_TIME_FULL_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'

    /**
     * 获取一个默认值,如果值为空,则返回默认值
     * @param value 值
     * @param {any} defaultValue 默认值
     * @returns {any}
     */
    static getDefaultValue(value: any, defaultValue = this.DEFAULT_VALUE) {
        if ((!value && value !== 0) || (isArray(value) && value?.length < 1) || value === 'Invalid Date') {
            return defaultValue
        }
        return value
    }

    /**
     * 获取一个对象某个属性的默认值
     * @param obj 对象
     * @param key 需要获取值的属性,支持lodash的at行数写法。参考:https://www.lodashjs.com/docs/lodash.at/
     * @param defaultValue 默认值
     * @returns {any}
     * @example
     *      const object = { 'a': [{ 'b': { 'c': 3 } }, 4] };
     *      DataFormat.getObjectDefaultValue(object, 'a[0].b.c') // 3
     */
    static getObjectDefaultValue(obj: AnyObject, key: string, defaultValue = this.DEFAULT_VALUE) {
        return this.getDefaultValue(at(obj, key)?.[0], defaultValue)
    }

    /**
     * 时间数据格式化
     * @param {string} time 时间
     * @param {string} format 时间的格式
     * @param defaultValue 默认值
     */
    static timeFormat(
        time: string | number | Date | dayjs.Dayjs | null | undefined,
        format = this.DATE_TIME_FORMAT,
        defaultValue = this.DEFAULT_VALUE,
    ) {
        if (!time || (isString(time) && time?.trim().length < 1)) {
            return defaultValue
        }
        return dayjs(time).format(format)
    }

    /**
     * 千分符
     * @param {string} num
     * @param {string} format
     * @returns {string}
     */
    static numGroupSeparator(num: number | string) {
        num = String(num)
        if (!num.includes('.')) num += '.'

        return num
            .replace(/(\d)(?=(\d{3})+\.)/g, ($0, $1) => {
                return `${$1},`
            })
            .replace(/\.$/, '')
    }

    /**
     * 将数字取整为10的倍数,如将 2345 可以处理为 2300 | 2400 | 3000 | 2000
     * @param num 需要取整的值
     * @param prec 需要用0占位的数量,默认(负数)为取长度-1
     * @param ceil 是否向上取整
     * @returns {number}
     */
    static formatRoundNum(num: number, prec = -1, ceil = true) {
        if (num < 10) {
            return 10
        }
        if (prec <= 0) {
            prec = String(num).length - 1
        }

        const mult = 10 ** prec
        return ceil ? Math.ceil(num / mult) * mult : Math.floor(num / mult) * mult
    }

    /**
     * 字符串长度补全。默认处理数字的位数补全,不足位数填充0。比如 1 => 01
     * @param {number} num
     * @param bit 补充0的位数,默认为2,如果小于或等于0,则不填充
     * @param padStr 补足的字符,默认为0
     * @returns {string}
     */
    static dataPadStart(num: number | string, bit = 2, padStr = '0') {
        if (!num) {
            return ''
        }
        if (bit <= 0) {
            return num
        }
        return String(num).padStart(bit, padStr)
    }
}

测试

import dayjs from 'dayjs'
import DataFormat from '@/utils/DataFormat'

const object = { a: [{ b: { c: 3 } }, 4] }
console.log('ObjectDefaultValue', DataFormat.getObjectDefaultValue(object, 'a[0].b.c')) // 结果:3
console.log('ObjectDefaultValue', DataFormat.getObjectDefaultValue(object, 'a')) // 获取a属性的值
console.log('now day = ', DataFormat.timeFormat(dayjs())) // now day =  2024-12-15 19:03

16.2 lodash

安装
pnpm i lodash-es

# ts依赖
pnpm i -D @types/lodash-es

16.3 Echarts

安装
封装Echarts基础组件

17 Vue组件库集成

17.1 集成element-plus

(1)el-select元素的调试方法:属性点击下拉后,用鼠标右键点击下拉框,然后移动到元素选择按钮上,用左键点击,即可选择到元素

(2)一般的弹窗会被传送到body外部。

快一点的 Element-Plus 组件说明文档:https://element-plus.midfar.com/zh-CN/component/overview.html

17.1.1 安装element-plus
17.1.1.1 安装
# 安装
pnpm install element-plus

# 按需导入插件
pnpm install -D unplugin-vue-components unplugin-auto-import

17.1.1.2 声明类型定义

修改 types/module.d.ts 或 src/vite-env.d.ts,在其中加入类型声明

// declare module '*.vue' {} // 省略其他...

// 解决ts找不到声明错误:在src/vite-env.d.ts中新增如下声明
declare module '*.mjs'
17.1.1.3 使用自动导入

修改vite.config.js,添加自动导入插件配置

import { defineConfig } from "vite"
// element-plus 按需导入插件
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import {ElementPlusResolver} from "unplugin-vue-components/resolvers";

export default defineConfig({
    plugins: [
        vue(),
        // element-plus 按需导入插件
        AutoImport({
            resolvers: [ElementPlusResolver()]
        }),
        Components({
            resolvers: [ElementPlusResolver()]
        })
    ]
})
17.1.1.4 导入Element样式

在 src/index.scss下导入element plus 样式

// element-plus 样式
@import 'element-plus/dist/index.css';
17.1.2 集成element-plus-icon
17.1.1.1 安装
# 集成icon
pnpm install @element-plus/icons-vue
17.1.1.2 全局注册图标组件

直接在main.ts中注册:在main.ts中加入如下代码

import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
// 全局注册图标组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

或是在基础组件中注册:baseComponents.ts

import type { App, Component } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

/**
 * ElementPlus图标注册
 * @type {{install: (Vue: App) => void}}
 */
export const ElementPlusIcons = {
    install: (Vue: App) => {
        Object.keys(ElementPlusIconsVue).forEach((key) => {
            Vue.component(key, ElementPlusIconsVue[key as keyof typeof ElementPlusIconsVue])
        })
    },
}

// main.ts
// import baseComponents, { ElementPlusIcons } from '@/components/BaseComponents/baseComponents' // 导入基础组件
// createApp(App).use(router).use(store).use(baseComponents).use(ElementPlusIcons).mount('#app')
17.1.1.3 在BaseIcon中集成

修改utils/icon.ts,主要是导入:import * as ElementPlusIconsVue from ‘@element-plus/icons-vue’

BaseIcon.vue前面已经集成,无需修改。

/**
 * @ClassName icon
 * @Description 图标工具类
 * @Author xuyizhuo
 * @Date 2024/12/8 22:02
 */
import * as ElementPlusIconsVue from '@element-plus/icons-vue' // element-plus图标
import { glyphs } from '@/assets/iconfont/iconfont.json'

/** Element plus 图标列表 */
export const elIconNames = Object.keys(ElementPlusIconsVue)

// 获取所有的iconfont图标
const iconNameList = []
for (let i = 0, length = glyphs.length; i < length; i++) {
    iconNameList.push(glyphs[i].font_class)
}

/** iconfont 图标列表 */
export const iconfonts = iconNameList // iconfont图标

/** iconfont、element plus 的图标列表 */
export const AllIconNames = [...elIconNames, ...iconNameList] // el、iconfont图标

4、图标使用
<!--统一封装的图标组件。注意:需要使用大驼峰命名,区分大小写-->
<BaseIcon name="AlarmClock" />
<BaseIcon color="red" name="AlarmClock" />

<!--原生的图标组件-->
<el-icon>
    <Search />
</el-icon>
17.1.3 汉化

修改App.vue 项目跟组件,加入 <el-config-provider> 组价支持

<script lang="ts" setup>
import zhCn from 'element-plus/es/locale/lang/zh-cn'
</script>

<template>
    <el-config-provider :locale="zhCn">
        <!--路由-->
        <router-view v-slot="{ Component }">
            <transition name="fade">
                <component :is="Component" />
            </transition>
        </router-view>
    </el-config-provider>
</template>
17.1.4 消息工具类
import { ElMessage, ElNotification } from 'element-plus'

/** 消息类型列表 */
export type MessageType = 'error' | 'success' | 'warning' | 'info'

/**
 * @ClassName MessageUtil
 * @Description 消息工具类
 * @Author xuyizhuo
 * @Date 2024/12/15 19:57
 */
export default class MessageUtil {
    /**
     * 显示普通消息
     * @param {string} message
     * @param {MessageType} type
     */
    static message(message: string, type: MessageType = 'info') {
        // 关闭已有的消息框
        ElMessage.closeAll()
        // 弹出新的消息框
        ElMessage({
            message,
            type,
        } as any)
    }

    /**
     * 显示警告消息
     * @param {string} message
     */
    static messageWarning(message: string) {
        this.message(message, 'warning')
    }

    /**
     * 显示错误消息
     * @param {string} message
     */
    static messageError(message: string) {
        this.message(message, 'error')
    }

    /**
     * 显示普通通知
     * @param {string} message 通知消息
     * @param {string} title 通知标题
     * @param {MessageType} type  通知类型
     */
    static notification(message: string, title: string = '', type: MessageType = 'info') {
        // 关闭已有通知框
        ElNotification.closeAll()

        // 弹窗新的通知框
        ElNotification({
            title,
            message,
            type,
        } as any)
    }

    /**
     * 显示警告通知
     * @param {string} message 通知消息
     * @param {string} title 通知标题
     */
    static notificationWarning(message: string, title: string = '') {
        this.notification(message, title, 'warning')
    }

    /**
     * 显示错误通知
     * @param {string} message 通知消息
     * @param {string} title 通知标题
     */
    static notificationError(message: string, title: string = '') {
        this.notification(message, title, 'error')
    }
}

使用消息弹窗

const onUpdate = () => {
    // 显示一个带标题的通知窗
    MessageUtil.notification('查询用户信息成功', '成功')
    
    // 显示一个消息
    MessageUtil.messageError('查询用户列表失败')
    
    setTimeout(() => {
        
        // 显示一个没有标题的通知窗
        MessageUtil.notificationWarning('查询用户信息成功')
    }, 200)
}

17.2 集成Ant-Design

17.2.1 安装

https://www.antdv.com/components/overview-cn

pnpm i --save ant-design-vue
配置自动导入
pnpm install unplugin-vue-components -D
pnpm install --save @ant-design/icons-vue
只用组件
<template>
    <div class="home">
        <a-button type="primary">Primary Button</a-button>
        <a-button>Default Button</a-button>
        <a-button type="dashed">Dashed Button</a-button>
        <a-button type="text">Text Button</a-button>
        <a-button type="link">Link Button</a-button>
    </div>
</template>

18 常用指令

在项目开发中,我们有时候会自定义一些全局的指令,我们统一将他放置在 src/directive 目录中,在其中创建index.ts,用于统一注册指令。

定义指令的类型(无法实现)

全局定义的指令,我们在ts的全局模块声明中去定义。修改 module.d.ts,增加类型定义。

在webstorm可以校验到指令的参数,但是无法做到 Element Plus 的 v-loading 指令的类型校验。

/** 扩展Vue的类型声明 */
declare module 'vue' {
    interface WindowSize {
        width: number
        height: number
    }

    type ResizeBinding = (event: WindowSize) => void

    /** 定义指令的类型 */
    export interface ComponentCustomProperties {
        /** 窗口大小变化监听指令 */
        vResize: Directive<any, ResizeBinding>
        /** 容器拖拽指令 */
        vDraggable: Directive<any, string | undefined>
    }
}

18.1 拖拽指令

18.1.1 实现拖拽指令

src/directive 目录中创建 draggable.ts

import { Directive, DirectiveBinding } from 'vue'

/**
 * @ClassName draggable
 * @Description 拖拽指令,使用:v-draggable
 * @Author xuyizhuo
 * @Date 2024/12/15 22:45
 * @example <MyComponent v-draggable>123</MyComponent>
 */
const draggable: Directive = {
    /**
     * @param {HTMLElement} el 绑定指令的元素
     * @param {DirectiveBinding} binding 绑定的参数
     */
    mounted(el: HTMLElement, binding: DirectiveBinding) {
        //console.log('binding', binding)

        const parentElement = el.parentElement // 获取父节点
        let controllerElement: string | undefined
        let move:
            | string
            | ((
                  event: MouseEvent,
                  position: {
                      x: number
                      y: number
                  },
                  el: HTMLElement,
              ) => void)
            | undefined
        if (typeof binding.value === 'string') {
            controllerElement = binding.value
        } else if (typeof binding.value === 'function') {
            move = binding.value
        } else if (binding.value) {
            controllerElement = binding.value.self
            move = binding.value.move
        }

        // 设置元素使用相对定位
        if (controllerElement === 'father' && parentElement) {
            parentElement.style.position = 'absolute'
        } else {
            el.style.position = 'absolute'
        }
        el.style.cursor = 'move' // 鼠标移动到元素上,显示为移动样式

        el.onmousedown = function (e) {
            let disX = e.pageX - el.offsetLeft
            let disY = e.pageY - el.offsetTop

            if (controllerElement === 'father') {
                disX = e.pageX - (parentElement?.offsetLeft || 0)
                disY = e.pageY - (parentElement?.offsetTop || 0)
            } else {
                disX = e.pageX - el.offsetLeft
                disY = e.pageY - el.offsetTop
            }
            document.onmousemove = function (e) {
                if (typeof move === 'function') {
                    move(e, { x: disX, y: disY }, el)
                } else {
                    let x = e.pageX - disX
                    let y = e.pageY - disY
                    let maxX = 0
                    let maxY = 0

                    if (parentElement) {
                        if (controllerElement === 'father') {
                            maxX = (parentElement.parentElement?.offsetWidth || 0) - parentElement.offsetWidth
                            maxY = (parentElement.parentElement?.offsetHeight || 0) - parentElement.offsetHeight
                        } else {
                            maxX = parentElement.offsetWidth - el.offsetWidth
                            maxY = parentElement.offsetHeight - el.offsetHeight
                        }
                    }

                    // 限制拖拽范围
                    if (x < 0) {
                        x = 0
                    } else if (x > maxX) {
                        x = maxX
                    }

                    if (y < 0) {
                        y = 0
                    } else if (y > maxY) {
                        y = maxY
                    }

                    if (controllerElement === 'father' && parentElement) {
                        parentElement.style.left = x + 'px'
                        parentElement.style.top = y + 'px'
                        parentElement.style.right = 'unset'
                        parentElement.style.bottom = 'unset'
                    } else {
                        el.style.left = x + 'px'
                        el.style.top = y + 'px'
                        el.style.right = 'unset'
                        el.style.bottom = 'unset'
                    }
                }
            }
            document.onmouseup = function () {
                document.onmousemove = document.onmouseup = null
            }
        }
    },

    // unmounted(el) {
    //     el.removeEventListener('mousedown', handleMouseDown)
    // },
}

export default draggable

18.1.2 加入全局导出的指令列表

directive 目录中,在其中创建 directive.ts

import { App, Directive } from 'vue'
import draggable from './draggable'

type Directives = {
    [propName: string]: Directive
}

/** 指令列表 */
const directives: Directives = { draggable }

/**
 * @ClassName directive
 * @Description 自定义的指令注册
 * @Author xuyizhuo
 * @Date 2024/5/24 12:27
 */
export default {
    /**
     * 全局注册组件,用Vue变量名在webstorm可以识别到组件已经注册,用其他名称无法识别。暂不清楚具体原因
     * @param {App} Vue
     */
    install: (Vue: App) => {
        //console.log('app', app)
        Object.keys(directives).forEach((key) => {
            //console.log('key', key)
            // 注册指令到app
            Vue.directive(key, directives[key])
        })
    },
}

18.1.2 注册到Vue

修改main.ts,导入指令并注册

import directive from '@/directive/directive' // 全局指令
import App from './App.vue'

createApp(App).use(directive).mount('#app')
18.1.3 使用

使用指令: <div v-draggable>123430912301923</div>

<script lang="ts" setup></script>

<template>
    <div class="app-home">
        <div v-draggable>123430912301923</div>
    </div>
</template>

<style lang="scss" scoped>
.app-home {
    width: 100%;
    height: 100%;
    font-size: 20px;
}
</style>

18.2 窗口变化监听指令

18.2.1 实现拖拽指令

src/directive 目录中创建 resize.ts

import { Directive } from 'vue'

// 1 创建一个被观察对象的集合
const map = new WeakMap()

// 2 创建一个响应式的元素观察器:当变化时执行回调
const ob = new ResizeObserver((entries) => {
    // console.log('ResizeObserver', entries)
    for (const entry of entries) {
        // console.log('entry', entry)
        const handler = map.get(entry.target)
        handler?.({
            width: entry.borderBoxSize[0].inlineSize,
            height: entry.borderBoxSize[0].blockSize,
        })
    }
})

/**
 * @ClassName resize
 * @Description resize指令:窗口大小变化监听
 * @Author xuyizhuo
 * @Date 2024/5/24 12:26
 */
const resize: Directive = {
    // 3 元素挂载完成:(1)绑定观察的对象;(2)存储观察的元素
    mounted(el, binding) {
        //console.log('mounted el, binding', el, binding)
        //console.log('binding.value', binding.value)
        ob.observe(el)
        map.set(el, binding.value)
    },
    // 4 元素卸载完成:(1)移除观察的对象;(2)被观察的列表中移除元素
    unmounted(el) {
        // console.log('unmounted el', el)
        ob.unobserve(el)
        map.delete(el)
    },
}

export default resize

18.2.2 加入全局导出的指令列表

修改 directive/directive.ts ,在其中加入resize指令。

注意:如果没有前面的指令,还需要注册到vue实例中。

import { App, Directive } from 'vue'
import draggable from './draggable'
import resize from './resize'

// 省略其他代码。。。

/** 指令列表 */
const directives: Directives = { draggable, resize }
18.2.3 使用

使用指令: <div v-resize="onWindowSizeUpdate">123430912301923</div>

<script lang="ts" setup>
/**
 * @ClassName appHome
 * @Description 应用首页
 * @Author xuyizhuo
 * @Date 2024/12/10 20:22
 */

/**
 * 窗口大小变化处理
 * @param {{width: number, height: number}} event
 */
const onWindowSizeUpdate = (event: { width: number; height: number }) => {
    console.log('窗口尺寸变化了', event)
}
</script>

<template>
    <div class="app-home">
        <div v-resize="onWindowSizeUpdate">123430912301923</div>
    </div>
</template>

<style lang="scss" scoped>
.app-home {
    width: 100%;
    height: 100%;
    font-size: 20px;
}
</style>

18.3 加载中指令

目前Element Plus已经提供了 v-loading 指令,一般直接使用即可。

<!--加载中指令-->
<div v-loading="true">123430912301923</div>

使用命令式加载中

import { ElLoading } from 'element-plus'

// 开启登录加载,在2s后关闭
const loading = ElLoading.service({ fullscreen: true, text: '转一转' })
setTimeout(() => {
    loading.close()
}, 2000)
封装loading状态

参考:https://blogweb.cn/article/1943527391710

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/950296.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

创建springboot项目

目录 1、使用 https://start.spring.io/ 创建项目Project 选 mavenLanguage 选 javaSpring Boot 选 3.4.1Project MetadataDependencies 2、阿里云网址 更好用 https://start.aliyun.com/ 1、使用 https://start.spring.io/ 创建项目 跳转 Project 选 maven Language 选 jav…

UDP_TCP

目录 1. 回顾端口号2. UDP协议2.1 理解报头2.2 UDP的特点2.3 UDP的缓冲区及注意事项 3. TCP协议3.1 报头3.2 流量控制2.3 数据发送模式3.4 捎带应答3.5 URG && 紧急指针3.6 PSH3.7 RES 1. 回顾端口号 在 TCP/IP 协议中&#xff0c;用 “源IP”&#xff0c; “源端口号”…

Netron可视化深度学习的模型框架,大大降低了大模型的学习门槛

深度学习是机器学习的一个子领域&#xff0c;灵感来源于人脑的神经网络。深度学习通过多层神经网络自动提取数据中的高级特征&#xff0c;能够处理复杂和大量的数据&#xff0c;尤其在图像、语音、自然语言处理等任务中表现出色。常见的深度学习模型&#xff1a; 卷积神经网络…

Python生成高级圣诞树-代码案例剖析

文章目录 &#x1f47d;发现宝藏 ❄️方块圣诞树&#x1f42c;效果截图&#x1f338;代码-可直接运行&#x1f334;代码解析 ❄️线条圣诞树&#x1f42c;效果截图&#x1f338;代码-可直接运行&#x1f334;代码解析 ❄️豪华圣诞树&#x1f42c;效果截图&#x1f338;代码-可…

Flux“炼丹炉”——fluxgym安装教程

一、介绍 这个炼丹炉目前可以训练除了flux-dev之外的其它模型&#xff0c;只需更改一个配置文件内容即可。重要的是训练时不需要提前进行图片裁剪、打标等前置工作&#xff0c;只需下图的三个步骤即可开始训练。它就是——fluxgym。 fluxgym&#xff1a;用于训练具有低 VRAM &…

【PLL】非线性瞬态性能

频率捕获、跟踪响应&#xff0c;是大信号非线性行为锁相范围内的相位、频率跟踪&#xff0c;不是非线性行为 所以&#xff1a;跟踪&#xff0c;是线性区域&#xff1b;捕获&#xff0c;是大信号、非线性区域 锁定范围&#xff1a;没有周跳&#xff08;cycle-slipping&#xff0…

OpenAI CEO 奥特曼发长文《反思》

OpenAI CEO 奥特曼发长文《反思》 --- 引言&#xff1a;从 ChatGPT 到 AGI 的探索 ChatGPT 诞生仅一个多月&#xff0c;如今我们已经过渡到可以进行复杂推理的下一代模型。新年让人们陷入反思&#xff0c;我想分享一些个人想法&#xff0c;谈谈它迄今为止的发展&#xff0c;…

“AI智慧语言训练系统:让语言学习变得更简单有趣

大家好&#xff0c;我是你们的老朋友&#xff0c;一个热衷于探讨科技与教育结合的产品经理。今天&#xff0c;我想和大家聊聊一个让语言学习变得不再头疼的话题——AI智慧语言训练系统。这个系统可是我们语言学习者的福音&#xff0c;让我们一起来揭开它的神秘面纱吧&#xff0…

一、二极管(应用篇)

1.5普通二极管应用 1.5.1钳位电路 利用二极管的固定的导通电压&#xff0c;在二极管处并联用电器&#xff0c;达到用电器的工作电压相对稳定。如果电源处有尖峰电压&#xff0c;则可以通过二极管导入到5v的电源内&#xff0c;防止此尖峰电压干扰用电器 &#xff0c;起到对电路的…

Android Studio 安装配置(个人笔记)

Android studio安装的前提是必须保证安装了jdk1.8版本以上 一、查看是否安装jdk cmd打开命令行&#xff0c;输入java -version 最后是一个关键点 输入 javac &#xff0c;看看有没有相关信息 没有就下载jdk Android studio安装的前提是必须保证安装了jdk1.8版本以上 可以到…

unity学习14:unity里的C#脚本的几个基本生命周期方法, 脚本次序order等

目录 1 初始的C# 脚本 1.1 初始的C# 脚本 1.2 创建时2个默认的方法 2 常用的几个生命周期方法 2.1 脚本的生命周期 2.1.1 其中FixedUpdate 方法 的时间间隔&#xff0c;是在这设置的 2.2 c#的基本语法别搞混 2.2.1 基本的语法 2.2.2 内置的方法名&#xff0c;要求更严…

node.js|浏览器插件|Open-Multiple-URLs的部署和使用,实现一键打开多个URL的强大工具

前言&#xff1a; 在整理各类资源的时候&#xff0c;可能会面临资源非常多的情况&#xff0c;这个时候我们就需要一款能够一键打开多个URL的浏览器插件了 说简单点&#xff0c;其实&#xff0c;迅雷就是这样的&#xff0c;但是迅雷是基于内置nginx浏览器实现的&#xff0c;并…

HTML 显示器纯色亮点检测工具

HTML 显示器纯色亮点检测工具 相关资源文件已经打包成html等文件&#xff0c;可双击直接运行程序&#xff0c;且文章末尾已附上相关源码&#xff0c;以供大家学习交流&#xff0c;博主主页还有更多Html相关程序案例&#xff0c;秉着开源精神的想法&#xff0c;望大家喜欢&#…

dbeaver导入导出数据库(sql文件形式)

目录 前言dbeaver导出数据库dbeaver导入数据库 前言 有时候我们需要复制一份数据库&#xff0c;可以使用dbeaver简单操作&#xff01; dbeaver导出数据库 选中数据库右键->工具->转储数据库 dbeaver导入数据库 选中数据库右键->工具->执行脚本 mysql 默…

接口测试-postman(使用postman测试接口笔记)

一、设置全局变量 1. 点击右上角设置按钮-》打开管理环境窗口-》选择”全局“-》设置变量名称&#xff0c;初始值和当前值设置一样的&#xff0c;放host放拼接的url&#xff0c;key放鉴权那一串字符&#xff0c;然后保存-》去使用全局变量&#xff0c;用{{变量名称}}形式 二、…

enzymejest TDD与BDD开发实战

一、前端自动化测试需要测什么 1. 函数的执行逻辑&#xff0c;对于给定的输入&#xff0c;输出是否符合预期。 2. 用户行为的响应逻辑。 - 对于单元测试而言&#xff0c;测试粒度较细&#xff0c;需要测试内部状态的变更与相应函数是否成功被调用。 - 对于集成测试而言&a…

Flutter项目开发模版,开箱即用(Plus版本)

前言 当前案例 Flutter SDK版本&#xff1a;3.22.2 本文&#xff0c;是由这两篇文章 结合产出&#xff0c;所以非常建议大家&#xff0c;先看完这两篇&#xff1a; Flutter项目开发模版&#xff1a; 主要内容&#xff1a;MVVM设计模式及内存泄漏处理&#xff0c;涉及Model、…

Spring Boot - 日志功能深度解析与实践指南

文章目录 概述1. Spring Boot 日志功能概述2. 默认日志框架&#xff1a;LogbackLogback 的核心组件Logback 的配置文件 3. 日志级别及其配置配置日志级别3.1 配置文件3.2 环境变量3.3 命令行参数 4. 日志格式自定义自定义日志格式 5. 日志文件输出6. 日志归档与清理7. 自定义日…

IWOA-GRU和GRU时间序列预测(改进的鲸鱼算法优化门控循环单元)

时序预测 | MATLAB实现IWOA-GRU和GRU时间序列预测(改进的鲸鱼算法优化门控循环单元) 目录 时序预测 | MATLAB实现IWOA-GRU和GRU时间序列预测(改进的鲸鱼算法优化门控循环单元)预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现IWOA-GRU和GRU时间序列预测…

【SpringBoot】日志处理-异常日志(Logback)

文章目录 异常日志&#xff08;Logback&#xff09;1、将 logback-spring.xml 文件放入项目的 src/main/resources 目录下2、配置 application.yml 文件3、使用 Logback 记录日志 异常日志&#xff08;Logback&#xff09; 使用 Logback 作为日志框架时&#xff0c;可以通过配…