自己开发组件库

搭建vue3 & ts组件库脚手架

目标

  1. pnpm搭建monorepo项目,和使用workspace测试组件库
  2. 组件支持typescript,可以被使用的项目识别
  3. 组件支持整体导入、按需自动导入

环境要求

node ≥ 18 , pnpm ≥ 8 , vue ≥ 3.3

初始化项目模板

首先使用vitepnpm 创建一个项目模板,这里使用pnpm,方便后面monorepo的使用。

pnpm create vite

在交互命令中填写项目名称,选择 Vue + Typescript 模板

init-vite

然后进入项目目录,使用 pnpm install 安装依赖

使用monorepo管理组件库

使用 monorepo 可以将多个包放在一下项目下维护,包之间可以互相引用,相同的依赖和配置也可以统一维护起来。除了组件库,可能后面还会新增工具库和插件库,使用monorepo可以更好的进行管理。

创建过程如下:

  1. 首先指定 monorepo 目录。在项目根目录创建 packages 文件夹和 pnpm-workspace.yaml 文件,文件的内容为:

    packages:
        - 'packages/**'
    

    这样就可以指定项目packages下的文件夹为子包。

  2. packages 文件夹下新建 components 文件夹,并在新建的文件夹中新建一个 package.json 文件,初始内容如下:

    {
        "name": "@giegie/components",
        "version": "0.0.1",
        "description": "练习了2年半的高性能组件库",
        "scripts": {}
    }
    

    其中 @giegie/componentnpm包的名称,@giegie 是包的作用域,可以避免包的冲突。

创建第一个组件

先来创建一个简单的 Input 组件用作测试,如图所示,需要在src下建立一个Input文件夹,且需要创建几个固定的文件:

first-component

  • style/index.scss — 用于定义组件的样式。在里面补充一点简单的样式:

    .gie-input {
        &__control {
            color: red;
        }
    }
    

    为什么样式要拆开而不是直接写在Input组件里呢? 因为需要在构建时打包成一个css文件用于组件库整体导入。按需导入时,样式放在约定的目录,也方便让按需导入的插件自动引入样式。

  • Input.ts — 用于定义类型文件,如Inputprops类型,emit类型和instance类型等,内容如下:

    import Input from './Input.vue';
    
    /**
     * 定义props类型
     */
    export interface InputProps {
        modelValue: string;
        disabled?: boolean;
    }
    
    /**
     * 定义emit类型
     */
    export type InputEmits = {
        'update:modelValue': [value: string];
    };
    
    /**
     * 定义instance类型
     */
    export type InputInstance = InstanceType<typeof Input>;
    

    InputInstance是用来干啥的? 在写公共组件时,会使用defineExpose暴露一些方法。如在element-plus中,就会使用formRef.validate 来校验表单,instance里就有暴露方法的类型签名。

  • Input.vue — 组件文件。内容如下:

    <template>
        <div class="gie-input">
            <input
                v-model="state"
                ref="inputRef"
                class="gie-input__control"
                type="text"
                :disabled="props.disabled"
            />
        </div>
    </template>
    <script setup lang="ts">
        import { computed, ref } from 'vue';
        import type { InputEmits, InputProps } from './Input';
    
        defineOptions({
            name: 'GieInput',
        });
    
        const emit = defineEmits<InputEmits>();
    
        const props = withDefaults(defineProps<InputProps>(), {
            modelValue: '',
            disabled: false,
        });
    
        const state = computed({
            get: () => props.modelValue,
            set: (val) => {
                emit('update:modelValue', val);
            },
        });
    
        const inputRef = ref<HTMLInputElement>();
    
        function focus() {
            inputRef.value?.focus();
        }
    
        defineExpose({
            focus,
        });
    </script>
    

    在该组件中简单的定义了组件名、代理了一下v-model,并暴露出了一个方法focus

  • index.ts — 定义Input组件的入口文件

    import { withInstall } from '../utils/install';
    
    import Input from './Input.vue';
    
    export const GieInput = withInstall(Input);
    export default GieInput;
    
    export * from './Input.vue';
    export * from './Input';
    

    在入口文件中,使用withInstall封装了一下导入的Input组件,并默认导出。且在下面导出了所有类型文件。

    这个withInstall函数的作用就是把组件封装成了一个可被安装,带install方法的vue插件,这个函数是直接从element-plus项目复制的😂。

    import type { App, Plugin } from 'vue';
    export type SFCWithInstall<T> = T & Plugin;
    export const withInstall = <T, E extends Record<string, any>>(main: T, extra?: E) => {
        (main as SFCWithInstall<T>).install = (app): void => {
            for (const comp of [main, ...Object.values(extra ?? {})]) {
                app.component(comp.name, comp);
            }
        };
    
        if (extra) {
            for (const [key, comp] of Object.entries(extra)) {
                (main as any)[key] = comp;
            }
        }
        return main as SFCWithInstall<T> & E;
    };
    

完善打包入口文件

entry-file

  • style.scss — 这个样式文件用来导入所有组件的样式,之后会通过编译生成一个包含所有组件样式的css文件,用于整体导入

    @import './Input/style/index.scss';
    
  • components.ts — 这个文件用来代理导出组件里的vue文件和类型声明,内容如下:

    export * from './Input';
    

    这样做的目的,是为了之后可以在项目里对组件或类型进行导入,如:

    <template>
        <gie-input v-model="state" ref="inputRef" />
    </template>
    
    <script setup lang="ts">
        import { ref } from 'vue';
        import { GieInput } from '@giegie/components';
        import type { InputInstance } from '@giegie/components';
    
        const state = ref('');
        const inputRef = ref<InputInstance>();
    </script>
    
  • installs.ts — 将组件的默认导出,也就是经过withInstall处理的vue组件插件导入进来,封装成一个数组,给下面的入口文件使用

    import GieInput from './Input';
    
    export default [GieInput];
    
  • index.ts — 组件库入口文件,在这个文件里,需要导出components.ts 里代理的vue组件和类型,并将installs.ts 导出的插件数组交给makeInstaller 处理成一个支持整体导入的插件:

    import { makeInstaller } from './utils/install';
    import installs from './installs';
    
    export * from './components';
    
    export default makeInstaller([...installs]);
    

    makeInstaller 实际上也是一个vue插件,他将组件插件循环进行安装,也是从element-plus复制的😂。

    import type { App, Plugin } from 'vue';
    export const makeInstaller = (components: Plugin[] = []) => {
        const install = (app: App) => {
            console.log(components);
            components.forEach((c) => app.use(c));
        };
        return {
            install,
        };
    };
    
  • global.d.ts — 这个文件位于components包的根目录,用于给vscodevolar插件提示组件的属性的类型

    declare module 'vue' {
        export interface GlobalComponents {
            GieInput: (typeof import('@giegie/components'))['GieInput'];
        }
    
        interface ComponentCustomProperties {}
    }
    
    export {};
    

编写打包配置

最终的目标是使用vite打包出 es、lib、types 3个目录,lib下的组件是commonjs版的,es下的组件是 es module 版的,types 里是类型声明文件。而且打包出来的文件目录要和src源码的文件目录保持一致,这样才能方便的按需导入。

output-file

对于样式,使用gulpsass进行既对目录下的单独scss文件进行编译,最后也合并成一个文件。

使用gulp不仅用来处理sass文件,更重要的是可以用来控制打包流程。

  1. 先安装一些依赖

    vite-plugin-dts 用来生成类型声明文件:

    pnpm add vite-plugin-dts -wD
    

    gulp和相关依赖安装到components子包下

    pnpm add gulp gulp-sass sass gulp-autoprefixer shelljs  -D --filter components
    
  2. components 下 新建一个vite.config.ts文件,配置和说明如下:

    import { defineConfig } from 'vite';
    import type { UserConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import dts from 'vite-plugin-dts';
    
    export default defineConfig(() => {
        return {
            build: {
                rollupOptions: {
                    // 将vue模块排除在打包文件之外,使用用这个组件库的项目的vue模块
                    external: ['vue'],
    
                    // 输出配置
                    output: [
                        {
                            // 打包成 es module
                            format: 'es',
                            // 重命名
                            entryFileNames: '[name].js',
                            // 打包目录和开发目录对应
                            preserveModules: true,
                            // 输出目录
                            dir: 'es',
                            // 指定保留模块结构的根目录
                            preserveModulesRoot: 'src',
                        },
                        {
                            // 打包成 commonjs
                            format: 'cjs',
                            // 重命名
                            entryFileNames: '[name].js',
                            // 打包目录和开发目录对应
                            preserveModules: true,
                            // 输出目录
                            dir: 'lib',
                            // 指定保留模块结构的根目录
                            preserveModulesRoot: 'src',
                        },
                    ],
                },
                lib: {
                    // 指定入口文件
                    entry: 'src/index.ts',
                    // 模块名
                    name: 'GIE_COMPONENTS',
                },
            },
            plugins: [
                vue(),
                dts({
                    // 输出目录
                    outDir: ['types'],
                    // 将动态引入转换为静态(例如:`import('vue').DefineComponent` 转换为 `import { DefineComponent } from 'vue'`)
                    staticImport: true,
                    // 将所有的类型合并到一个文件中
                    rollupTypes: true,
                }),
            ],
        } as UserConfig;
    });
    
  3. 在components文件夹下新建build文件夹,用于编写打包流程控制逻辑,文件和内容如下:

    build-script

    // index.js
    import gulp from 'gulp';
    import { resolve, dirname } from 'path';
    import { fileURLToPath } from 'url';
    import dartSass from 'sass';
    import gulpSass from 'gulp-sass';
    import autoprefixer from 'gulp-autoprefixer';
    import shell from 'shelljs';
    
    const componentPath = resolve(dirname(fileURLToPath(import.meta.url)), '../');
    
    const { src, dest } = gulp;
    const sass = gulpSass(dartSass);
    
    // 删除打包产物
    export const removeDist = async () => {
        shell.rm('-rf', `${componentPath}/lib`);
        shell.rm('-rf', `${componentPath}/es`);
        shell.rm('-rf', `${componentPath}/types`);
    };
    
    // 构建css
    export const buildRootStyle = () => {
        return src(`${componentPath}/src/style.scss`)
            .pipe(sass())
            .pipe(autoprefixer())
            .pipe(dest(`${componentPath}/es`))
            .pipe(dest(`${componentPath}/lib`));
    };
    
    // 构建每个组件下单独的css
    export const buildStyle = () => {
        return src(`${componentPath}/src/**/style/**.scss`)
            .pipe(sass())
            .pipe(autoprefixer())
            .pipe(dest(`${componentPath}/es`))
            .pipe(dest(`${componentPath}/lib`));
    };
    
    // 打包组件
    export const buildComponent = async () => {
        shell.cd(componentPath);
        shell.exec('vite build');
    };
    
    // gulpfile.js
    import gulp from 'gulp';
    import { removeDist, buildRootStyle, buildStyle, buildComponent } from './index.js';
    
    const { series } = gulp;
    
    export default series(removeDist, buildComponent, buildStyle, buildRootStyle);
    
  4. 在components文件夹下新建一个tsconfig.json文件,内容如下:

    {
        "extends": "../../tsconfig.json",
        "include": ["src"],
        "compilerOptions": {
            "moduleResolution": "node",
            "baseUrl": "."
        }
    }
    

    这里主要是将 moduleResolution 改为 node ,使打包出来的类型产物都可以正确的写入到一个文件里

  5. 修改components包下的package.json文件,添加一些配置:

    {
        "name": "@giegie/components",
        "version": "0.0.1",
        "description": "练习了2年半的高性能组件库",
        "main": "lib",
        "module": "es",
        "type": "module",
        "types": "types/index.d.ts",
        "files": ["es", "lib", "types", "global.d.ts"],
        "scripts": {
            "build": "gulp -f build/gulpfile.js"
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "devDependencies": {
            "gulp": "^4.0.2",
            "gulp-autoprefixer": "^8.0.0",
            "gulp-sass": "^5.1.0",
            "sass": "^1.67.0",
            "shelljs": "^0.8.5"
        }
    }
    

    具体修改内容为:

    • main指定cjs入口
    • module指定esm入口
    • type字段的值设置为"module"时,表示该项目是一个ES模块项目
    • types表示类型声明文件位置
    • files表示发包时哪些文件将上传
    • scripts添加build打包命令
  6. 在根目录的package.json中加入build命令

    "scripts": {
    	"build": "pnpm --filter=@giegie/* run build"
    }
    

    这个build命令的意思是,执行所有的以@giegie开头的子包的build命令

  7. 准备工作做好后执行 npm run build 命令,没有报错的话,会和生成出一样的产出物

    output-file2

整体导入

目前打包出来的产物已经可以直接用来整体导入了,使用pnpmworkspace特性,不需要先发布包就可以直接用pnpm安装这个包用作测试

  1. 使用命令安装 @giegie/components 组件库到根项目

    pnpm add @giegie/components@* -w
    
  2. 在项目根目录的 tsconfig.json 添加组件类型文件:

    {
        "compilerOptions": {
            "types": ["@giegie/components/global"]
        }
    }
    
  3. 在src的main.ts文件中整体导入组件库和样式

    import { createApp } from 'vue';
    import '@giegie/components/es/style.css';
    import App from './App.vue';
    import GieComponents from '@giegie/components';
    console.log(GieComponents);
    
    createApp(App).use(GieComponents).mount('#app');
    
  4. 在App.vue中编写测试代码

    <template>
        <div>
            <gie-input v-model="state" ref="inputRef" />
            {{ state }}
            <button @click="onFocus">focus</button>
        </div>
    </template>
    
    <script setup lang="ts">
        import type { InputInstance } from '@giegie/components';
        import { ref } from 'vue';
    
        const state = ref('');
        const inputRef = ref<InputInstance>();
    
        function onFocus() {
            inputRef.value?.focus();
        }
    </script>
    
  5. 运行npm run dev 命令,可以在浏览器中看到效果

    result

按需自动导入

完整导入所有组件会使项目打包出来的产物非常大,在element-plus中可以使用unplugin-vue-componentsunplugin-auto-import 按需自动导入需要的组件,文档地址。这个插件提供了多个组件的resolver,可以模仿他们的格式,自己写一个解析组件的resolver

packages新建一个子包,命名为resolver,并创建下面2个文件

resolver

  • index.js — 解析插件的入口文件

    function GieResolver() {
        return {
            type: 'component',
            resolve: (name) => {
                if (name.startsWith('Gie')) {
                    const partialName = name.slice(3);
                    return {
                        name: 'Gie' + partialName,
                        from: `@giegie/components`,
                        sideEffects: `@giegie/components/es/${partialName}/style/index.css`,
                    };
                }
            },
        };
    }
    
    module.exports = {
        GieResolver,
    };
    

    上面的代码大概意思是,解析到一个组件以“Gie”开头时,返回组件名称、组件位置、组件样式位置给unplugin-vue-componentsunplugin-auto-import 自动导入。

  • package.json

    {
        "name": "@giegie/resolver",
        "version": "0.0.1",
        "description": "组件库自动导入插件",
        "main": "./index",
        "author": "",
        "license": "ISC"
    }
    

安装自动导入插件和编写的解析插件到根项目

pnpm add unplugin-vue-components unplugin-auto-import @giegie/resolver@* -Dw

在根目录的vite.config.ts 中,加入配置

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import AutoImport from 'unplugin-auto-import/vite';
import { GieResolver } from '@giegie/resolver';
// <https://vitejs.dev/config/>
export default defineConfig({
    plugins: [
        vue(),
        Components({
            resolvers: [GieResolver()],
        }),
        AutoImport({
            resolvers: [GieResolver()],
        }),
    ],
});

将根目录的 tsconfig.jsontypes改成如下文件

{
    "compilerOptions": {
        "types": ["./components.d.ts", "./auto-imports.d.ts"]
    }
}

注释掉main.ts中的完整导入代码

import { createApp } from 'vue';
// import '@giegie/components/es/style.css'
import App from './App.vue';
// import GieComponents from '@giegie/components'
// console.log(GieComponents)

createApp(App).mount('#app');
// .use(GieComponents)

运行 npm run dev ,可以看到类型和网页上的内容都已经成功导入了近来。

component-ts

组件库版本的管理和发布

目标

  1. 使用verdaccio搭建私有npm
  2. 使用pnpmchangesets发布版本

搭建私有npm库

基于安全和速度的考虑,在公司内部发布npm包都会去搭建私有库。

如果只用来测试没有敏感内容的话,可以跳过这段,直接发到公网也没什么问题。

现在可以用来搭建私有库的工具有很多,这里使用verdaccio来进行搭建。

安装

npm install --global verdaccio

假设将数据存储到/data/verdaccio的话,按下面的方法配置

配置文件

mkdir /data/verdaccio
vim /data/verdaccio/config.yaml

编辑配置内容:

storage: /data/verdaccio/storage
auth:
    htpasswd:
        file: /data/verdaccio/htpasswd
        algorithm: bcrypt
        rounds: 10
        max_users: -1
uplinks:
    npmjs:
        url: https://registry.npmjs.org/
packages:
    '@*/*':
        access: $authenticated
        publish: $authenticated
        proxy: npmjs
    '**':
        access: $authenticated
        publish: $authenticated
        proxy: npmjs
web:
    enable: true
    title: 鸽鸽的前端私有库
    login: true
listen: 0.0.0.0:4873
max_body_size: 100mb
log: { type: stdout, format: pretty, level: http }

配置说明: 需要管理员手动新增用户,只有授权的用户才能查看和上传包,上游为npm官方库。

启动

新建启动脚本

vim /data/verdaccio/start.sh

内容:

verdaccio --config /data/verdaccio/config.yaml

添加执行权限

chmod +x /data/verdaccio/start.sh

使用pm2管理服务

npm i -g pm2
pm2 start /data/verdaccio/start.sh -n verdaccio

开机自启

pm2 startup
pm2 save

添加用户

生成 Bcrypt htpasswd 的文件并添加用户

htpasswd -bBc /data/verdaccio/htpasswd user password

继续添加一个用户的话用下面的命令

htpasswd -bB /data/verdaccio/htpasswd user2 password2

到这里搭建就结束了,服务在4873端口,可以用浏览器打开。可以用新建的用户测试一下能否登录。

修改项目配置

接入私有库

私有npm库搭建好后,在项目根目录新建一个.npmrc文件,内容如下:

registry = http://xxx.xxx.xxx.xxx:4873

这一行的意思是修改该项目的npm的源为自己搭建的私有库地址。

修改完成后,使用 pnpm login 命令输入账号、密码登录私有库,最后使用 pnpm i 重新拉取一遍。

指定要发布的包

在上一篇文章里,有一个主包和两个子包。其中两个子包componentsresolver希望发布到npm库里去。主包是用来测试的,不可以发到库里。

根项目下的主包是用来测试用的,需要将主包改为私有禁止其提交到库里,可以在根目录的package.json中修改privatetrue

{
    "private": true
}

使用changesets发包

pnpm推荐使用changesets来进行包的管理,使用changesets可以轻松的管理版本和changelog记录的生成。

安装和初始化changesets

输入以下命令:

pnpm add -Dw @changesets/cli
pnpm changeset init

安装完成后会在根目录生成.changeset文件夹,这个文件夹要随git一起提交上去。

需要注意的是changesets默认需要在分支main上运行,可以去.changeset/config.json文件下修改baseBranch的值来改变主分支

修改版本和编写changelog

提交代码并切换分支到main合并后,运行pnpm changeset add命令选择要发布的包。

changeset-add

这里把两个子包用空格键都选上

下一步需要升级版本号,先来看看npm包版本号有那些规则:

npm版本号由三部分组成:主版本号(Major)、次版本号(Minor)和修订号(Patch),格式为"Major.Minor.Patch"。

  1. 主版本号(Major):当进行不兼容的API修改时增加,表示向后不兼容的更新。
  2. 次版本号(Minor):当进行向下兼容的功能性新增时增加,表示向后兼容的更新。
  3. 修订号(Patch):当进行向下兼容的问题修复时增加,表示向后兼容的更新。

现在只用把修订号加一即可

运行pnpm changeset version 命令:

changeset-version

刚开始是选择是否是Major更新,这里什么都不选,直接按回车跳过。接下来是Minor,也跳过。最后默认的就是Patch版本号加1。版本号确定后,需要填写更新内容:

changeset-summary

填写完成后会看到在子包下生成了CHANGELOG.md文件,里面记录了选择的版本号和输入的更新内容

changeset-changelog

发布包

提交代码后,运行pnpm publish -r,没有问题的哈,终端会输出发布信息。去私有库网页查看的话,包应该已经发上去了。

changeset-publish

在组件库中封装element-plus

安装element-plus

正常情况下,项目中会安装有自己的element-plus版本。如果再将element-plus安装到组件库的话,那么项目安装依赖时会下载多个element-plus的版本。

实际上希望的是组件库能够使用项目的element-plus版本即可。

在这种情况下可以使用package.jsonpeerDependencies来声明外部依赖。

在组件库的 package.json 里添加声明

// /packages/components/package.json
{
    "peerDependencies": {
        "element-plus": "^2.3.9"
    }
}

然后在测试项目也就是根目录的package.json文件中安装element-plus

// /package.json
{
    "dependencies": {
        "@giegie/components": "workspace:*",
        "vue": "^3.3.4",
        "element-plus": "^2.3.9"
    }
}

配置都写好后,使用 pnpm install 进行安装

修改项目打包配置

排除打包依赖

在之前的文章中,在打包时排除了vue依赖。这里也一样,要排除element-plus有关的依赖,在组件库的vite.config.ts中进行修改

// /packages/components/vite.config.ts
export default defineConfig(() => {
    return {
        external: ['vue', 'element-plus', '@element-plus/icons-vue', /\.scss/],
    };
});

其中@element-plus/icons-vueelement-plus图标相关的依赖

排除掉 scss是因为要在组件中引入element-plus的样式,但是这个样式也要从外部项目的element-plus依赖中获取

为什么是scss而不是css呢? 因为在项目内通常会定制化element-plus的样式,这都是通过修改element-plus的scss变量来完成的。如果想定制的样式能影响封装在组件库中element-plus组件的话,后面在组件也必须要引入element-plus的scss样式。

自动引入element-plus的样式

在编写组件库的组件时,需要使用按需加载的方式引入element-plus组件,如:

<template>   <el-input /> </template>
<script setup lang="ts">
    import { ElInput } from 'element-plus';
    import 'element-plus/theme-chalk/src/base.scss';
    import 'element-plus/theme-chalk/src/input.scss';
</script>

可以看到不仅要引入组件,还需要引入基础样式和组件样式,这个需要的element-plus组件变多的话,非常麻烦。

需要使用unplugin-element-plus帮助自动引入样式

安装unplugin-element-plus 到组件库的包下

pnpm add unplugin-element-plus -D --filter components

vite配置文件里添加下面配置

// /packages/components/vite.config.ts
import ElementPlus from 'unplugin-element-plus/vite';
export default defineConfig(() => {
    return {
        plugins: [
            // ...
            ElementPlus({
                // 导入scss而不是css
                useSource: true,
            }),
        ],
    };
});

配置好后,编写组件时只用向下面这样就行

<template>   <el-input /> </template>
<script setup lang="ts">
    import { ElInput } from 'element-plus';
</script>

简单封装el-input组件

将之前封装好的gie-input中的input换成el-input组件,功能和之前一样,最终文件如下

<template>
    <div class="gie-input">
        <el-input v-model="state" ref="inputRef" type="text" :disabled="props.disabled" />
    </div>
</template>
<script setup lang="ts">
    import { computed, ref } from 'vue';
    import { ElInput } from 'element-plus';
    import type { InputEmits, InputProps } from './Input';

    defineOptions({
        name: 'GieInput',
    });

    const emit = defineEmits<InputEmits>();

    const props = withDefaults(defineProps<InputProps>(), {
        modelValue: '',
        disabled: false,
    });

    const state = computed({
        get: () => props.modelValue,
        set: (val) => {
            emit('update:modelValue', val);
        },
    });

    const inputRef = ref<InstanceType<typeof ElInput>>();

    function focus() {
        inputRef.value?.focus();
    }

    defineExpose({
        focus,
    });
</script>

运行打包和预览命令

npm run build
npm run dev

在浏览器中可以看到结果

浏览器结果

将组件接入到el-form的表单校验

在组件库里封装的组件,大概分为3类。表单组件、数据展示组件和布局组件。

其中表单组件一般都会和element-plus里的el-form组件结合使用。假如自己封装一个富文本组件,当输入后失去焦点且字段要求必填时,会自动触发el-form的校验。此时需要显示错误提示和自己封装的富文本的边框变红,这应该如何实现呢?

接入校验效果

首先需要在组件里获取到el-form-item组件的实例,element-plus暴露了一个contextKey,可以让方便的将el-form-item实例注入进来:

<script setup lang="ts">
    import { formItemContextKey } from 'element-plus';
    const elFormItem = inject(formItemContextKey);
</script>

然后通过元素的blur事件调用校验方法:

<template>
    <div contenteditable="true" @blur="onBlur"></div>
</template>
<script setup lang="ts">
    import { formItemContextKey } from 'element-plus';
    const elFormItem = inject(formItemContextKey);

    const onBlur = () => {
        elFormItem!.validate?.('blur').catch((err) => console.warn(err));
    };
</script>

可以看到上面validate方法的参数为blur,这个表示执行项目中规则的triggerblur的校验。

const rules = reactive<FormRules<RuleForm>>({
    test: [{ required: true, message: '请输入文字', trigger: 'blur' }],
});

到这里错误提示可以在失去焦点并校验失败时自动显示出来,下面开始修改错误时的边框样式。

错误样式

可以看到,当校验失败时,el-form-item上会加上is-errorclass,只用通过class来修改边框即可

.gie-richtext{
  flex: 1;
  &__control{
    border: 1px solid #aaa;
    background: #eee;
    border-radius: 5px;
    padding: 16px;
    min-height: 100px;
    .is-error & {
      border-color: red;
    }
  }
}
posted @   柯基与佩奇  阅读(518)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
欢迎阅读『自己开发组件库』
点击右上角即可分享
微信分享提示