普通视图

发现新文章,点击刷新页面。
昨天 — 2025年9月14日首页

Vue移动端开发的适配方案与性能优化技巧

作者 鹏多多
2025年9月12日 09:08

1. 移动端适配方案

1.1. 视口适配

在Vue项目中设置viewport的最佳实践:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

通过插件自动生成viewport配置:

// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.plugin('html').tap(args => {
      args[0].meta = {
        viewport: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
      }
      return args
    })
  }
}

1.2. 基于rem/em的适配方案

使用postcss-pxtorem自动转换px为rem:

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-pxtorem': {
      rootValue: 37.5, // 设计稿宽度的1/10
      propList: ['*'],
      selectorBlackList: ['.ignore', '.hairlines']
    }
  }
}

1.3. vw/vh视口单位适配

结合postcss-px-to-viewport实现px自动转换:

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      unitToConvert: 'px',
      viewportWidth: 375,
      unitPrecision: 5,
      propList: ['*'],
      viewportUnit: 'vw',
      fontViewportUnit: 'vw',
      selectorBlackList: [],
      minPixelValue: 1,
      mediaQuery: false,
      replace: true,
      exclude: undefined,
      include: undefined,
      landscape: false,
      landscapeUnit: 'vw',
      landscapeWidth: 568
    }
  }
}

1.4. 移动端UI组件库适配

推荐使用适配移动端的Vue组件库:

在项目中集成Vant组件库:

npm i vant -S

按需引入组件:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { Button, Cell, CellGroup } from 'vant';
import 'vant/lib/index.css';

const app = createApp(App);

app.use(Button)
   .use(Cell)
   .use(CellGroup);

app.mount('#app')

2. 移动端性能优化技巧

2.1. 虚拟列表实现长列表优化

可以使用vue-virtual-scroller实现高性能列表:

npm install vue-virtual-scroller --save
<template>
  <RecycleScroller
    class="items-container"
    :items="items"
    :item-size="32"
    key-field="id"
  >
    <template #item="{ item }">
      <div class="item">{{ item.text }}</div>
    </template>
  </RecycleScroller>
</template>

<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

export default {
  components: {
    RecycleScroller
  },
  data() {
    return {
      items: Array.from({ length: 10000 }).map((_, i) => ({
        id: i,
        text: `Item ${i}`
      }))
    }
  }
}
</script>

2.2. 图片懒加载与优化

使用vueuse的useIntersectionObserver实现图片懒加载:

npm i @vueuse/core
<template>
  <img 
    v-for="item in imageList" 
    :key="item.id"
    :src="item.loaded ? item.src : placeholder"
    @load="handleImageLoad(item)"
    class="lazy-image"
  >
</template>

<script>
import { ref, onMounted } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'

export default {
  setup() {
    const imageList = ref([
      { id: 1, src: 'https://example.com/image1.jpg', loaded: false },
      { id: 2, src: 'https://example.com/image2.jpg', loaded: false },
      // 更多图片...
    ])
    const placeholder = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
    
    const handleImageLoad = (item) => {
      item.loaded = true
    }
    
    onMounted(() => {
      imageList.value.forEach(item => {
        const el = ref(null)
        const { stop } = useIntersectionObserver(
          el,
          ([{ isIntersecting }]) => {
            if (isIntersecting) {
              item.loaded = true
              stop()
            }
          }
        )
      })
    })
    
    return {
      imageList,
      placeholder,
      handleImageLoad
    }
  }
}
</script>

2.3. 减少首屏加载时间

使用Vue的异步组件和路由懒加载:

// 路由配置
const routes = [
  {
    path: '/home',
    name: 'Home',
    component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

使用CDN加载外部资源:

<!-- index.html -->
<head>
  <!-- 加载Vue -->
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
</head>

2.4. 事件节流与防抖

使用lodash的throttle和debounce函数:

npm install lodash --save
<template>
  <div>
    <input v-model="searchText" @input="debouncedSearch" placeholder="搜索...">
  </div>
</template>

<script>
import { debounce } from 'lodash'

export default {
  data() {
    return {
      searchText: '',
      debouncedSearch: null
    }
  },
  created() {
    this.debouncedSearch = debounce(this.handleSearch, 300)
  },
  methods: {
    handleSearch() {
      // 执行搜索操作
      console.log('Searching with:', this.searchText)
    }
  }
}
</script>

3. 移动端常见问题解决方案

3.1. 移动端300ms点击延迟问题

使用fastclick库解决:

npm install fastclick --save
// main.js
import FastClick from 'fastclick'

FastClick.attach(document.body)

3.2. 滚动卡顿问题

优化滚动性能:

.scroll-container {
  -webkit-overflow-scrolling: touch; /* 开启硬件加速 */
  overflow-y: auto;
}

3.3. 移动端适配iOS安全区域

/* 适配iOS安全区域 */
body {
  padding-top: constant(safe-area-inset-top);
  padding-top: env(safe-area-inset-top);
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}

3.4. 解决1px边框问题

/* 0.5px边框 */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
  .border-bottom {
    border-bottom: 0.5px solid #e5e5e5;
  }
}

4. 性能监控与分析

使用Lighthouse进行性能评估:

npm install -g lighthouse
lighthouse https://your-vue-app.com --view

使用Vue DevTools进行性能分析:

  1. 在Chrome浏览器中安装Vue DevTools扩展
  2. 在Vue项目中启用性能模式:
// main.js
const app = createApp(App)

if (process.env.NODE_ENV !== 'production') {
  app.config.performance = true
}

app.mount('#app')

5. 实战案例:开发响应式移动端应用

5.1. 项目初始化

npm init vite@latest my-mobile-app -- --template vue-ts
cd my-mobile-app
npm install

5.2. 配置适配方案

集成postcss-px-to-viewport:

npm install postcss-px-to-viewport --save-dev

配置postcss.config.js:

module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      unitToConvert: 'px',
      viewportWidth: 375,
      unitPrecision: 5,
      propList: ['*'],
      viewportUnit: 'vw',
      fontViewportUnit: 'vw',
      selectorBlackList: [],
      minPixelValue: 1,
      mediaQuery: false,
      replace: true,
      exclude: /node_modules/i
    }
  }
}

5.3. 集成Vant组件库

npm install vant --save

配置按需引入:

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [VantResolver()],
    }),
  ],
})

5.4. 实现响应式布局

<template>
  <div class="container">
    <van-nav-bar title="我的应用" left-arrow @click-left="onClickLeft" />
    
    <van-swipe class="banner" :autoplay="3000" indicator-color="white">
      <van-swipe-item v-for="(item, index) in banners" :key="index">
        <img :src="item" alt="Banner" />
      </van-swipe-item>
    </van-swipe>
    
    <van-grid :columns-num="4">
      <van-grid-item v-for="(item, index) in gridItems" :key="index" :text="item.text" :icon="item.icon" />
    </van-grid>
    
    <van-list
      v-model:loading="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <van-cell v-for="(item, index) in list" :key="index" :title="item.title" :value="item.value" is-link />
    </van-list>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'

export default defineComponent({
  setup() {
    const banners = ref([
      'https://picsum.photos/600/200?random=1',
      'https://picsum.photos/600/200?random=2',
      'https://picsum.photos/600/200?random=3'
    ])
    
    const gridItems = ref([
      { icon: 'photo-o', text: '图片' },
      { icon: 'video-o', text: '视频' },
      { icon: 'music-o', text: '音乐' },
      { icon: 'friends-o', text: '社交' }
    ])
    
    const list = ref([])
    const loading = ref(false)
    const finished = ref(false)
    
    const onLoad = () => {
      // 模拟加载数据
      setTimeout(() => {
        for (let i = 0; i < 10; i++) {
          list.value.push({
            title: `标题 ${list.value.length + 1}`,
            value: '内容'
          })
        }
        
        // 加载状态结束
        loading.value = false
        
        // 数据全部加载完成
        if (list.value.length >= 50) {
          finished.value = true
        }
      }, 1000)
    }
    
    onMounted(() => {
      onLoad()
    })
    
    const onClickLeft = () => {
      console.log('返回')
    }
    
    return {
      banners,
      gridItems,
      list,
      loading,
      finished,
      onLoad,
      onClickLeft
    }
  }
})
</script>

<style scoped>
.banner img {
  width: 100%;
  height: 180px;
  object-fit: cover;
}
</style>

通过以上步骤,我们可以开发出一个适配移动端的Vue应用。


本次分享就到这儿啦,我是鹏多多,如果看了觉得有帮助的,欢迎 点赞 关注 评论,在此谢过道友;

往期文章

昨天以前首页

vue的监听属性watch的详解

作者 鹏多多
2025年9月3日 09:51

1. 概述

watch 是一个对象,键是需要观察的表达式,用于观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数的参数是新值和旧值。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个 property。

2. 常规用法

watch 监听有两个形参,第一个是新值,第二个是旧值。如下例子:使用 watch 监听了 total 的值,当 total 的值改变时,控制台会打印出旧值和新值。

<template>
  <div class="home_box">
    <h1>{{ total }}</h1>
    <button @click="handleAddTotal">增加</button>
  </div>
</template>

<script>
export default {
  name: 'Home',
  watch: {
    total(newValue, oldValue) {
      console.log('旧值:', oldValue)
      console.log('新值:', newValue)
    }
  },
  data() {
    return {
      total: 0
    }
  },
  methods: {
    handleAddTotal() {
      this.total++
    }
  }
}
</script>

3. 监听对象和route变化

watch监听的目标,可以是基本类型,也可以是对象,也可以是对象里的一个值。而监听目标的属性,可以是一个函数,也可以是一个包含handler(回调函数),immediate(是否初始化后立即执行一次)和deep(是否开启深度监听)的对象。

<script>
export default {
  name: 'Home',
  watch: {
    // 监听基本类型
    aaa(newValue, oldValue) {
      console.log('旧值:', oldValue)
      console.log('新值:', newValue)
    },
    // 监听基本类型,并且回调函数写在methods里,且初始化加载立即执行一次
    bbb: {
      handler: 'handleBBB',
      immediate: true
    },
    // 监听对象类型,需要开启深度监听
    ccc: {
      handler: (newValue, oldValue) {
        console.log('旧值:', oldValue)
        console.log('新值:', newValue)
      },
      deep: true
    },
    // 监听对象里的某个值
    'ddd.d2.d21': {
      handler: (newValue, oldValue) {
        console.log('旧值:', oldValue)
        console.log('新值:', newValue)
      }
    },
    // 监听route变化
    '$route': {
      handler: (newValue, oldValue) {
        console.log('旧值:', oldValue)
        console.log('新值:', newValue)
      }
    }
  },
  data() {
    return {
      aaa: 0,
      bbb: 0,
      ccc: {
        c1: 0,
        c2: 0
      },
      ddd: {
        d1: 0,
        d2: {
          d21: 0
        }
      }
    }
  },
  methods: {
    handleBBB() {
      this.bbb++
    }
  }
}
</script>

4. 使用场景

watch监听属性使用场景很多。比如:

  1. 即时表单验证
  2. 搜索
  3. 监听数据变化,做出相应改变
  4. ......

如下例子,监听 keyword 的值,实时打印出来。

<template>
  <div class="home_box">
    <input type="text" v-model="keyword">
  </div>
</template>

<script>
export default {
  name: 'Home',
  watch: {
    keyword: {
      handler: 'handleKeywordChange'
    }
  },
  data() {
    return {
      keyword: '',
    }
  },
  methods: {
    handleKeywordChange(newValue, oldValue) {
      console.log(newValue, oldValue)
    }
  }
}
</script>

本次分享就到这儿啦,我是鹏多多,如果看了觉得有帮助的,欢迎 点赞 关注 评论,在此谢过道友;

往期文章

前端项目eslint配置选项详细解析

作者 鹏多多
2025年9月10日 09:38

1. 前言

ESLint 是一款可高度配置的 JavaScript 代码检查工具,其核心价值在于帮助开发者在编码阶段主动发现并修复问题,而非等到运行时才暴露错误。它不仅能检测语法错误,还能规范代码风格、规避潜在逻辑风险,是现代前端 / Node.js 项目中保障代码质量的核心工具之一。

1.1 核心功能

ESLint 的核心能力围绕 “静态分析” 与 “灵活配置” 展开,主要包括:

  • 静态代码分析:无需运行代码,即可扫描出语法错误(如未定义变量)、逻辑漏洞(如条件中使用常量表达式)、风格不一致(如混用单双引号)等问题。
  • 自动修复支持:对格式错误(如缩进、分号)、简单风格问题(如引号转换)等,可通过 eslint --fix 命令自动修复,减少手动调整成本。
  • 规则可配置性:支持启用 / 禁用单个规则、调整规则严格级别,还可继承社区成熟配置(如 eslint:recommended),快速适配项目需求。

1.2 实际价值

在团队协作与工程化流程中,ESLint 的作用尤为突出:

  • 统一代码规范:消除因个人编码习惯差异导致的 “风格冲突”(如缩进用 2 空格还是 4 空格),让代码看起来像 “一个人写的”。
  • 降低错误风险:提前拦截低级错误(如未使用的变量、遗漏的分号),减少线上运行时故障。
  • 提升可维护性:规范的代码结构更易阅读、修改,降低新成员接入项目的学习成本。
  • 集成工程化流程:可嵌入 CI/CD 流水线(如 Jenkins、GitHub Actions),实现 “代码提交即检查”,阻止不规范代码合并入主分支。

2、错误级别

ESLint 为每条规则定义了 3 个严格级别,通过 “代号” 或 “别称” 配置,不同级别对应不同的检查行为,具体说明如下:

代号 别称 含义 描述
0 'off' 忽略 关闭规则
1 'warn' 警告 打开规则,并且将规则视为一个警告,检查通过
2 'error' 错误 打开规则,并且将规则视为一个错误 检查不通过,退出码为 1

注意:实际配置中,“代号” 与 “别称” 效果一致,可按需选择。例如 no-console: 0 与 no-console: 'off' 等价。

3、常用规则

ESLint 内置数百条规则,按功能可分为 “逻辑错误”“最佳实践”“变量声明” 等类别。以下整理项目中高频使用的规则,每条规则均标注 “作用”,帮助快速理解其价值。

规则详情可参考:ESLint 官方规则文档(比第三方链接更权威、更新及时)

3.1 逻辑错误类(规避代码运行风险)

此类规则用于拦截可能导致运行时错误或逻辑异常的代码,是保障代码 “正确性” 的核心:

    no-cond-assign          // 禁止条件表达式中出现模棱两可的赋值操作符 
    no-console              // 禁用console 
    no-constant-condition   // 禁止在条件中使用常量表达式 
    no-debugger             // 禁用 debugger 
    no-dupe-args            // 禁止 function 定义中出现重名参数 
    no-dupe-keys            // 禁止对象字面量中出现重复的 key 
    no-duplicate-case       // 禁止出现重复的 case 标签 
    no-empty                // 禁止出现空语句块 
    no-ex-assign            // 禁止对 catch 子句的参数重新赋值 
    no-extra-boolean-cast   // 禁止不必要的布尔转换 
    no-extra-parens         // 禁止不必要的括号 
    no-extra-semi           // 禁止不必要的分号 
    no-func-assign          // 禁止对 function 声明重新赋值 
    no-inner-declarations   // 禁止在嵌套的块中出现变量声明或 function 声明 
    no-irregular-whitespace // 禁止在字符串和注释之外不规则的空白 
    no-obj-calls            // 禁止把全局对象作为函数调用 
    no-sparse-arrays        // 禁用稀疏数组 
    no-prototype-builtins   // 禁止直接使用Object.prototypes 的内置属性 
    no-unexpected-multiline // 禁止出现令人困惑的多行表达式 
    no-unreachable          // 禁止在return、throw、continue 和 break语句之后出现不可达代码 
    use-isnan               // 要求使用 isNaN() 检查 NaN 
    valid-typeof            // 强制 typeof 表达式与有效的字符串进行比较

3.2 最佳实践类(提升代码健壮性与可维护性)

此类规则不直接阻断运行,但遵循可减少潜在问题、提升代码可读性:

    array-callback-return   // 强制数组方法的回调函数中有 return 语句 
    block-scoped-var        // 强制把变量的使用限制在其定义的作用域范围内 
    complexity              // 指定程序中允许的最大环路复杂度 
    consistent-return       // 要求 return 语句要么总是指定返回的值,要么不指定 
    curly                   // 强制所有控制语句使用一致的括号风格 
    default-case            // 要求 switch 语句中有 default 分支 
    dot-location            // 强制在点号之前和之后一致的换行 
    dot-notation            // 强制在任何允许的时候使用点号 
    eqeqeq                  // 要求使用 === 和 !== 
    guard-for-in            // 要求 for-in 循环中有一个 if 语句 
    no-alert                // 禁用 alert、confirm 和 prompt 
    no-case-declarations    // 不允许在 case 子句中使用词法声明 
    no-else-return          // 禁止 if 语句中有 return 之后有 else 
    no-empty-function       // 禁止出现空函数 
    no-eq-null              // 禁止在没有类型检查操作符的情况下与 null 进行比较 
    no-eval                 // 禁用 eval() 
    no-extra-bind           // 禁止不必要的 .bind() 调用 
    no-fallthrough          // 禁止 case 语句落空 
    no-floating-decimal     // 禁止数字字面量中使用前导和末尾小数点 
    no-implicit-coercion    // 禁止使用短符号进行类型转换 
    no-implicit-globals     // 禁止在全局范围内使用 var 和命名的 function 声明 
    no-invalid-this         // 禁止 this 关键字出现在类和类对象之外 
    no-lone-blocks          // 禁用不必要的嵌套块 
    no-loop-func            // 禁止在循环中出现 function 声明和表达式 
    no-magic-numbers        // 禁用魔术数字 
    no-multi-spaces         // 禁止使用多个空格 
    no-multi-str            // 禁止使用多行字符串 
    no-new                  // 禁止在非赋值或条件语句中使用 new 操作符 
    no-new-func             // 禁止对 Function 对象使用 new 操作符 
    no-new-wrappers         // 禁止对 String,Number 和 Boolean 使用 new 操作符 
    no-param-reassign       // 不允许对 function 的参数进行重新赋值 
    no-redeclare            // 禁止使用 var 多次声明同一变量 
    no-return-assign        // 禁止在 return 语句中使用赋值语句 
    no-script-url           // 禁止使用 javascript: url 
    no-self-assign          // 禁止自我赋值 
    no-self-compare         // 禁止自身比较 
    no-sequences            // 禁用逗号操作符 
    no-unmodified-loop-condition   // 禁用一成不变的循环条件 
    no-unused-expressions   // 禁止出现未使用过的表达式 
    no-useless-call         // 禁止不必要的 .call() 和 .apply() 
    no-useless-concat       // 禁止不必要的字符串字面量或模板字面量的连接 
    vars-on-top             // 要求所有的 var 声明出现在它们所在的作用域顶部

3.3 变量声明类(规范变量定义与使用)

此类规则聚焦变量的声明、初始化与作用域,减少变量污染与未定义错误:

    init-declarations     // 要求或禁止 var 声明中的初始化 
    no-catch-shadow       // 不允许 catch 子句的参数与外层作用域中的变量同名 
    no-restricted-globals // 禁用特定的全局变量 
    no-shadow             // 禁止 var 声明 与外层作用域的变量同名 
    no-undef              // 禁用未声明的变量,除非它们在 /global / 注释中被提到 
    no-undef-init         // 禁止将变量初始化为 undefined 
    no-unused-vars        // 禁止出现未使用过的变量 
    no-use-before-define  // 不允许在变量定义之前使用它们

3.4 CommonJS 模块类(规范 Node.js 模块写法)

此类规则针对 Node.js 环境的 CommonJS 模块(require/module.exports),确保模块代码规范:

    global-require        // 要求 require() 出现在顶层模块作用域中 
    handle-callback-err   // 要求回调函数中有容错处理 
    no-mixed-requires     // 禁止混合常规 var 声明和 require 调用 
    no-new-require        // 禁止调用 require 时使用 new 操作符 
    no-path-concat        // 禁止对 dirname 和 filename进行字符串连接 
    no-restricted-modules // 禁用指定的通过 require 加载的模块

3.5 风格指南类(统一代码格式,提升可读性)

此类规则纯 “风格层面”,不影响代码功能,但统一后可减少团队协作中的 “格式争议”:

   array-bracket-spacing           // 强制数组方括号中使用一致的空格 
   block-spacing                   // 强制在单行代码块中使用一致的空格 
   brace-style                     // 强制在代码块中使用一致的大括号风格 
   camelcase                       // 强制使用骆驼拼写法命名约定 
   comma-spacing                   // 强制在逗号前后使用一致的空格 
   comma-style                     // 强制使用一致的逗号风格 
   computed-property-spacing       // 强制在计算的属性的方括号中使用一致的空格 
   eol-last                        // 强制文件末尾至少保留一行空行 
   func-names                      // 强制使用命名的 function 表达式 
   func-style                      // 强制一致地使用函数声明或函数表达式 
   indent                          // 强制使用一致的缩进 
   jsx-quotes                      // 强制在 JSX 属性中一致地使用双引号或单引号 
   key-spacing                     // 强制在对象字面量的属性中键和值之间使用一致的间距 
   keyword-spacing                 // 强制在关键字前后使用空格,比如if else 
   linebreak-style                 // 强制使用一致的换行风格 
   lines-around-comment            // 要求在注释周围有空行 
   max-depth                       // 强制可嵌套的块的最大深度 
   max-len                         // 强制一行的最大长度 
   max-lines                       // 强制最大行数 
   max-nested-callbacks            // 强制回调函数最大嵌套深度 
   max-params                      // 强制 function 定义中最多允许的参数数量 
   max-statements                  // 强制 function 块最多允许的的语句数量 
   max-statements-per-line         // 强制每一行中所允许的最大语句数量 
   new-cap                         // 要求构造函数首字母大写 
   new-parens                      // 要求调用无参构造函数时有圆括号 
   newline-after-var               // 要求或禁止 var 声明语句后有一行空行 
   newline-before-return           // 要求 return 语句之前有一空行 
   newline-per-chained-call        // 要求方法链中每个调用都有一个换行符 
   no-array-constructor            // 禁止使用 Array 构造函数 
   no-continue                     // 禁用 continue 语句 
   no-inline-comments              // 禁止在代码行后使用内联注释 
   no-lonely-if                    // 禁止 if 作为唯一的语句出现在 else 语句中 
   no-mixed-spaces-and-tabs        // 不允许空格和 tab 混合缩进 
   no-multiple-empty-lines         // 不允许多个空行 
   no-negated-condition            // 不允许否定的表达式 
   no-plusplus                     // 禁止使用一元操作符 ++ 和 – 
   no-spaced-func                  // 禁止 function 标识符和括号之间出现空格 
   no-trailing-spaces              // 禁用行尾空格 
   no-whitespace-before-property   // 禁止属性前有空白 
   object-curly-newline            // 强制花括号内换行符的一致性 
   object-curly-spacing            // 强制在花括号中使用一致的空格 
   object-property-newline         // 强制将对象的属性放在不同的行上 
   one-var                         // 强制函数中的变量要么一起声明要么分开声明 
   one-var-declaration-per-line    // 要求或禁止在 var 声明周围换行 
   operator-assignment             // 要求或禁止在可能的情况下要求使用简化的赋值操作符 
   operator-linebreak              // 强制操作符使用一致的换行符 
   quote-props                     // 要求对象字面量属性名称用引号括起来 
   quotes                          // 强制使用一致的反勾号、双引号或单引号 
   require-jsdoc                   // 要求使用 JSDoc 注释 
   semi                            // 要求或禁止使用分号而不是 ASI 
   semi-spacing                    // 强制分号之前和之后使用一致的空格 
   sort-vars                       // 要求同一个声明块中的变量按顺序排列 
   space-before-blocks             // 强制在块之前使用一致的空格 
   space-before-function-paren     // 强制在 function的左括号之前使用一致的空格 
   space-in-parens                 // 强制在圆括号内使用一致的空格 
   space-infix-ops                 // 要求操作符周围有空格 
   space-unary-ops                 // 强制在一元操作符前后使用一致的空格 
   spaced-comment                  // 强制在注释中 // 或 /* 使用一致的空格
  • typeScript
@typescript-eslint/no-non-null-assertion // 是否禁止非空断言!

4. 项目实战:.eslintrc.js 配置解析

以下是适配 Vue 3 + TypeScript 项目的 ESLint 配置文件(/.eslintrc.js),结合项目实际需求调整规则,注释中已说明关键配置的目的与逻辑:

module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
globals: {
TAny: true,
TDict: true,
TFunc: true,
TDialogButtonOption: true,
THttpResponse: true,
NodeJS: 'readonly',
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly'
},
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module'
},
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
plugins: ['vue', '@typescript-eslint'],
rules: {
'@type-eslint/ban-ts-ignore': 'off', // 配置禁用 @ts-ignore 注释
'@type-eslint/explicit-function-return-type': 'off', // 要求函数和类方法上显式返回类型
'@type-eslint/no-explicit-any': 'off', // 不允许any类型
'@typescript-eslint/no-explicit-any': 'warn', // any不能乱用
'@type-eslint/no-var-requires': 'off', // 禁止require语句,import语句除外
'@type-eslint/no-empty-function': 'off', // 禁止空函数
'@type-eslint/no-use-before-define': 'off', // 禁止在定义变量之前使用变量
'@type-eslint/ban-ts-comment': 'off', // 禁止@ts-<指令>注释或要求指令后面有描述。
'@type-eslint/ban-types': 'off', // 禁止某些类型
'@type-eslint/no-non-null-assertion': 'off', // 不允许使用非空断言!后缀运算符
'@type-eslint/explicit-module-boundary-types': 'off', // 对导出函数和类的公共类方法要求显式的返回和参数类型
'vue/no-v-for-template-key': 0, // 不允许template上有key
semi: ['error', 'never'], // 使用分号
'comma-dangle': [
// 语句后面是否使用逗号
'error',
{
arrays: 'never',
objects: 'never',
imports: 'never',
exports: 'never',
functions: 'never'
}
],
'vue/custom-event-name-casing': 'off', // 为自定义事件名强制执行特定的大小写
'vue/attributes-order': 'off', // 强制属性的顺序
'vue/one-component-per-file': 'off', // 强制每个组件应该在它自己的文件中
'vue/html-closing-bracket-newline': 'off', // 要求或禁止在标记的右括号前换行
'vue/max-attributes-per-line': 'off', // 强制规定每行的最大属性数
'vue/multiline-html-element-content-newline': 'off', // 要求在多行元素的内容之前和之后使用换行符
'vue/singleline-html-element-content-newline': 'off', // 要求在单行元素的内容前后使用换行符
'vue/attribute-hyphenation': 'off', // 对模板中的自定义组件强制实施属性命名样式
'vue/html-self-closing': 'off', // 强制实施自动关闭样式
'vue/no-multiple-template-root': 'off', // template中只允许模板里存在一个根节点
'vue/require-default-prop': 'off', // props需要默认值
'vue/no-v-model-argument': 'off', // 检查自定义组件上是否没有参数
'vue/no-arrow-functions-in-watch': 'off', // 禁止使用箭头函数定义watch
'vue/no-template-key': 'off', // 不允许template上有key
'vue/no-v-html': 'off', // 禁止使用 V-HTML 来防止 XSS 攻击
'vue/comment-directive': 'off', // 支持注释指令
'vue/no-parsing-error': 'off', // 报告语法错误
'vue/no-deprecated-v-on-native-modifier': 'off', // 弃用修饰符 ondirective @xxx.native
'vue/multi-word-component-names': 'off', // 组件名称始终是多字的
'no-useless-escape': 'off', // 禁用不必要的转义
'no-sparse-arrays': 'off', // 禁用稀疏数组
'no-prototype-builtins': 'off', // 禁止直接使用Object.prototypes 的内置属性
'no-constant-condition': 'off', // 禁止在条件中使用常量表达式
'no-use-before-define': 'off', // 不允许在变量定义之前使用它们
'no-restricted-globals': 'off', // 禁用特定的全局变量
'no-restricted-syntax': 'off', // 禁止使用特定的语法
'generator-star-spacing': 'off', // 强制 generator 函数中 * 号周围有空格
'no-unreachable': 'off', // 禁止在return、throw、continue 和 break语句之后出现不可达代码
'no-unused-vars': ['error', { varsIgnorePattern: '.*', args: 'none' }], // 禁止出现未使用过的变量
'no-case-declarations': 'off', // 不允许在 case 子句中使用词法声明
'no-console': 'off', // 禁用console
'arrow-parens': 'off', // 箭头函数一个参数可以不要括号
'no-eq-null': 2, // 禁止对null使用==或!=运算符
quotes: [1, 'single'], // 引号类型
'prefer-const': 0, // 首选const
eqeqeq: 2, // 必须使用全等
'default-case': 2, // switch语句最后必须有default
'no-var': 0, // 禁用var,用let和const代替
'no-trailing-spaces': 1 // 一行结束后面不要有空格
}
}

本次分享就到这儿啦,我是鹏多多,如果看了觉得有帮助的,欢迎 点赞 关注 评论,在此谢过道友;

往期文章

详解vue渲染函数render的使用

作者 鹏多多
2025年9月11日 10:02

1. 前言

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数 render,它比模板更接近编译器,直接生成 VNode 虚拟 DOM。

下面是一个对比例子,通过 level prop 来动态生成标题的组件

  • template 写法
<template>
<h1 :class="[`title${level}`]" v-if="level === 1">
<slot></slot>
</h1>
<h2 :class="[`title${level}`]" v-else-if="level === 2">
<slot></slot>
</h2>
<h3 :class="[`title${level}`]" v-else-if="level === 3">
<slot></slot>
</h3>
<h4 :class="[`title${level}`]" v-else-if="level === 4">
<slot></slot>
</h4>
<h5 :class="[`title${level}`]" v-else-if="level === 5">
<slot></slot>
</h5>
<h6 :class="[`title${level}`]" v-else-if="level === 6">
<slot></slot>
</h6>
</template>

<script>
export default {
name: 'Title',
props: {
level: {
type: Number,
required: true,
},
},
}
</script>
  • render 写法
export default {
name: 'Title',
props: {
level: {
type: Number,
required: true,
},
},
render(createElement) {
const children = this.$slots.default || [];
return createElement(`h${this.level}`, { class: [`title${this.level}`] }, children)
},
}

2. 参数和语法

当使用 render 函数描述虚拟 DOM 时,vue 提供一个构建虚拟 DOM 的函数,叫 createElement,约定的简写为 h。

2-1. 参数

createElement 函数有三个参数:

  1. 必填。一个 HTML 标签名、组件名,类型:{String | Object | Function}(也可以是组件选项对象或返回组件选项的函数)
  2. 可选。一个与模板中属性对应的数据对象,也就是与模板中属性对应的数据对象,包含组件属性、DOM 属性、事件等。
  3. 可选。子级虚拟节点 (VNodes),由 createElement() 构建而成,也可以使用字符串来生成"文本虚拟"节点。

语法:

createElement(TagNameOptionContent)

第一个参数也可以是组件选项对象或返回组件选项的函数,例如:

// :
createElement({
  template: '<div>{{ msg }}</div>',
  data() {
    return { msg: 'Hello' };
  }
});

2-2. Option数据对象

createElement 函数的第二个参数,是一个与模板中属性对应的数据对象,也就是组件的属性。

{
  // 与 `v-bind:class` 的 API 相同,接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML attribute
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM property
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 内,但不再支持如 `v-on:keyup.enter` 这样的修饰器。需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用`vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为{ name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层 property
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}

下面是一个 Button 按钮的例子:

export default {
name: 'Button',
props: {
text: {
type: String,
required: true,
},
},
methods: {
    handleClick() {
      console.log('按钮被点击了!');
    }
  },
render(h) {
return h(
'div',
{
class: 'button-wrapper',
on: {
click: handleClick,
},
},
[h('span', { class: 'button-text' }, this.text)]
)
},
}

2-3. 指令变化

指令的写法发生了变化,常用的 v-if/else,还有 v-for,v-model,事件修饰符等都有变化。

2-3-1. v-if和else

  • 模板写法:
<ul v-if="items.length">
  <li v-for="item in items" :key="item">{{ item }}</li>
</ul>
<p v-else>空空如也</p>
  • render 函数写法:
export default {
// 省略......
render(h) {
if (this.items.length) {
return h('ul', this.items.map((item) => h('li', { key: item }, item)));
} else {
return h('p', '空空如也');
}
}
}

2-3-2. v-model

render 函数中没有与 v-model 的直接对应,需要自己实现相应的逻辑。

  • 模板写法
<input v-model="message" placeholder="请输入" />
  • render 函数写法
export default {
// 省略......
props: ['message'],
render(h) {
const self = this
return h('input', {
domProps: {
value: self.message,
},
on: {
input: (event) => {
// 组件绑定使用了sync语法糖
self.$emit('update:message', event.target.value)
},
},
})
},
}

2-3-3. 事件按键修饰符

对于事件修饰符,vue 官方提供了部分的特殊前缀来处理,其余的,则需要自己在函数中处理。

修饰符 前缀 说明
.passive & 滚动事件的默认行为将会立即触发,而不是等到事件触发完再触发
.capture ! 捕获模式
.once ~ 只触发一次回调
.capture.once ~! 只触发一次回调 的 捕获模式

例子如下:

on: {
  '!click': this.func,
  '~keyup': this.func,
  '~!mouseover': this.func,
  keyup: (event) => {
    // 如果触发事件的元素不是事件绑定的元素
    if (event.target !== event.currentTarget) return
    // 如果按下去的不是 enter 键或者没有同时按下 shift 键
    f (!event.shiftKey || event.keyCode !== 13) return
    // 阻止 事件冒泡
    event.stopPropagation()
    // 阻止该元素默认的 keyup 事件
    event.preventDefault()
  }
}
修饰符 操作 说明
.stop event.stopPropagation() 阻止冒泡
.prevent event.preventDefault() 阻止元素发生默认的行为
.self if (event.target !== event.currentTarget) return 自身触发
.enter if (event.keyCode !== 13) return 按键匹配
.shift if (!event.shiftKey) return 按键匹配

2-3-4. 插槽

可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:

render: function (h) {
  return h('div', this.$slots.default)
}

也可以通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数:

props: ['message'],
render: function (h) {
  return h('div', [
    this.$scopedSlots.default({
      text: this.message
    })
  ])
}

如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots 字段:

render: function (h) {
  return h('div', [
    h('child', {
      // 在数据对象中传递 `scopedSlots` 格式为 { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return h('span', props.text)
        }
      }
    })
  ])
}

例子如下:

  • list.js
const handleClick = (index) => {
return () => {
console.log(`${index}按钮被点击了!`)
}
}

export default {
name: 'List',
props: {
data: {
type: Array,
required: true,
default: () => [],
},
},
render(h) {
if (this.data.length > 0) {
return h(
'ul',
{
class: 'ul-box',
},
this.data.map((item, i) => {
return h(
'li',
{
class: 'li-box',
on: {
click: handleClick(i),
},
},
this.$scopedSlots.default({
data: `${item}+${i}`,
})
)
})
)
}
return h('div', '暂无数据')
},
}
  • home.vue
List<template>
  <div class="home_box">
    <List :data="list">
      <template slot-scope="scope">
        <span>{{ scope.data }}</span>
      </template>
    </List>
  </div>
</template>

<script>
import List from '@/views/render/list.js'

export default {
  name: 'Home',
  components: { List },
  data() {
    return {
      list: [100, 200, 300, 400, 500]
    }
  }
}
</script>

3. 编译jsx

如果你写了很多 render 函数,可能会觉得这样的代码写起来很痛苦,并且可读性也不好。这时候可以使用 JSX 插件:传送门

  • 使用JSX的render函数示例:
render() {
  return (
    <div class={`title${this.level}`}>
      {this.$slots.default}
    </div>
  );
}

4. 典型应用场景

render 函数非常适合实现高阶组件以及复杂的动态 UI,因为它可以动态创建和组合组件。

4.1 高阶组件示例

// 动态加载组件
export default function asyncComponentLoader(componentName) {
  return {
    name: `AsyncLoader(${componentName})`,
    data() {
      return {
        Component: null,
        loading: true,
        error: null,
      };
    },
    async created() {
      try {
        const componentModule = await import(`@/components/${componentName}.vue`);
        this.Component = componentModule.default || componentModule;
      } catch (err) {
        this.error = err.message;
      } finally {
        this.loading = false;
      }
    },
    render(h) {
      if (this.loading) return h('div', 'Loading...');
      if (this.error) return h('div', { style: { color: 'red' } }, this.error);
      if (this.Component) return h(this.Component, { props: this.$props });
      return h('div', 'Component not found');
    },
  };
}

4.2. 复杂的动态UI示例

// 动态表单生成器
export default {
  name: 'DynamicForm',
  props: {
    formConfig: {
      type: Array,
      required: true,
    },
    formData: {
      type: Object,
      default: () => ({}),
    },
  },
  methods: {
    handleInput(field, event) {
      this.$emit('input', { ...this.formData, [field]: event.target.value });
    },
  },
  render(h) {
    const formItems = this.formConfig.map((field) => {
      switch (field.type) {
        case 'text':
          return h('div', [
            h('label', field.label),
            h('input', {
              attrs: { type: 'text', placeholder: field.placeholder },
              domProps: { value: this.formData[field.name] || '' },
              on: { input: (e) => this.handleInput(field.name, e) },
            }),
          ]);
        case 'select':
          return h('div', [
            h('label', field.label),
            h('select', {
              domProps: { value: this.formData[field.name] || '' },
              on: { input: (e) => this.handleInput(field.name, e) },
            }, field.options.map(option => 
              h('option', { attrs: { value: option.value } }, option.label)
            )),
          ]);
        case 'checkbox':
          return h('div', [
            h('label', [
              h('input', {
                attrs: { type: 'checkbox' },
                domProps: { checked: this.formData[field.name] || false },
                on: { input: (e) => this.handleInput(field.name, e) },
              }),
              field.label,
            ]),
          ]);
        default:
          return null;
      }
    });

    return h('form', {
      on: {
        submit: (e) => {
          e.preventDefault();
          this.$emit('submit', this.formData);
        },
      }
    }, [
      ...formItems,
      h('button', { attrs: { type: 'submit' } }, '提交')
    ]);
  },
};

// 使用示例
<DynamicForm 
  :form-config="formConfig" 
  :form-data="formData" 
  @input="formData = $event" 
  @submit="handleSubmit" 
/>

// 配置示例
formConfig: [
  { type: 'text', name: 'username', label: '用户名', placeholder: '请输入用户名' },
  { type: 'text', name: 'email', label: '邮箱', placeholder: '请输入邮箱' },
  { type: 'select', name: 'role', label: '角色', options: [
    { value: 'admin', label: '管理员' },
    { value: 'user', label: '普通用户' },
  ]},
  { type: 'checkbox', name: 'agreement', label: '我同意条款' },
],

5. 性能

render 函数通常比 template 性能更高,原因如下:

  1. 避免了模板编译过程
  2. 可以更精确地控制虚拟 DOM 的创建
  3. 在复杂动态 UI 场景中减少不必要的重新渲染

但请注意,不要为了性能而过度使用 render 函数,保持代码可读性同样重要。在大多数情况下,template 的性能已经足够好,没必要为了什么原因全部使用render。


本次分享就到这儿啦,我是鹏多多,如果看了觉得有帮助的,欢迎 点赞 关注 评论,在此谢过道友;

往期文章

详解vue中attrs和listeners的使用及高级组件封装

作者 鹏多多
2025年9月5日 09:45

在 Vue 开发中,attrsattrs和listeners是两个非常实用的实例属性,它们为组件间的通信和组件封装提供了极大的便利。

1. attrs

attrs包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。也就是说,再创建子组件的时候,有的属性绑定没有写在 props 里,则可以通过 attrs 传递过去。

特性解析

  • 范围:所有未被组件 props 声明的属性
  • 排除项:class 和 style 属性
  • 传递方式:通过v-bind="$attrs"实现属性透传

如下例子:父组件给子组件绑定了 percentage 属性等于 70,子组件通过 v-bind="$attrs",把 percentage 属性直接透传给了 el-progress 组件,这在做一些多级组件嵌套(例如 A 嵌套 B , B 嵌套 C )封装时很方便。

  • 父组件:
<template>
  <div>
    <h1>父组件</h1>
    <child-component 
      :percentage="70" 
      :color="'#409EFF'" 
      :stroke-width="10" 
      :text-inside="true"
    />
  </div>
</template>

<script>
import ChildComponent from './child.vue';

export default {
  name: 'Parent',
  components: {
    ChildComponent
  }
}
</script>
  • 子组件:
<template>
  <div>
    <h3>子组件</h3>
    <el-progress v-bind="$attrs"></el-progress>
  </div>
</template>

<script>
export default {
  name: 'Child',
  // 声明了一些props,其他未声明的属性会被包含在$attrs中
  props: {
    percentage: {
      type: Number,
      default: 0
    }
  },
  created() {
    // 打印$attrs中的内容
    console.log('$attrs:', this.$attrs);
    // 输出: { color: '#409EFF', stroke-width: 10, text-inside: true }
  }
}
</script>

在这个例子中,父组件向子组件传递了多个属性,但子组件只声明了percentage作为 prop,其他属性(color、stroke-width、text-inside)都会被包含在attrs中,并通过vbind="attrs中,并通过v-bind="attrs"传递给了el-progress组件。

2. listeners

listeners包含了父作用域中的(不含.native修饰器的)von事件监听器。它可以通过von="listeners包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="listeners" 传入内部组件,在创建更高层次的组件时非常有用。

特性解析

  • 范围:所有未被组件明确处理的事件监听器
  • 排除项:使用.native 修饰器的事件
  • 传递方式:通过v-on="$listeners"实现事件透传

listeners相当于一个中间容器。可以用于解决组件嵌套,孙组件传递数据给祖父组件。当出现多级组件嵌套时(例如A嵌套BB嵌套C),C想传递数据A,就需要在B中给C设置von=listeners 相当于一个中间容器。可以用于解决组件嵌套,孙组件传递数据给 祖父组件。当出现多级组件嵌套时(例如 A 嵌套 B , B 嵌套 C ),C 想传递数据 A,就需要在 B 中给 C 设置 v-on=“listeners”,然后子组件C就可以通过$listeners.xxx 来传递数据给 A 了。

  • 祖父组件:
<template>
  <div>
    <h1>祖父组件</h1>
    <p>接收到的数据: {{ receivedData }}</p>
    <parent-component @getData="handleGetData" />
  </div>
</template>

<script>
import ParentComponent from './parent.vue';

export default {
  name: 'Grandparent',
  components: {
    ParentComponent
  },
  data() {
    return {
      receivedData: ''
    };
  },
  methods: {
    handleGetData(data) {
      this.receivedData = data;
      console.log('从孙子组件接收到的数据:', data);
    }
  }
}
</script>
  • 父组件:
<template>
  <div>
    <h3>父组件</h3>
    <child-component v-on="$listeners" />
  </div>
</template>

<script>
import ChildComponent from './child.vue';

export default {
  name: 'Parent',
  components: {
    ChildComponent
  },
  created() {
    // 打印$listeners中的内容
    console.log('$listeners:', this.$listeners);
    // 输出: { getData: ƒ }
  }
}
</script>
  • 子组件:
<template>
  <div>
    <h5>子组件</h5>
    <button @click="postData">传递数据给祖父组件</button>
  </div>
</template>

<script>
export default {
  name: 'Child',
  methods: {
    postData() {
      this.$emit('getData', '这是从子组件传递给祖父组件的数据');
    }
  }
}
</script>

在这个例子中,祖父组件定义了getData事件处理函数,父组件使用v-on="listeners"将所有事件监听器传递给子组件,子组件通过listeners"将所有事件监听器传递给子组件,子组件通过emit('getData', data)触发事件,最终祖父组件的事件处理函数被调用。

3. 高级用法和注意事项

在实际开发中,我们经常会同时使用attrsattrs和listeners来实现组件的高级封装:

  • $attrs
  1. 父组件传递给子组件的属性,在子组件中,可以通过 $attrs 获取到,而不用一个个的定义 props。
  2. 所有非 props 属性绑定到相应标签,如果是组件,则绑定到组件标签上。
  • $listeners
  1. 父组件传递给子组件的事件,在子组件中,可以通过 $listeners 获取到,而不用一个个的定义 props。
  2. 所有非 props 事件绑定到相应标签,如果是组件,则绑定到组件标签上。

二次封装组件例子:

  • custom-checkbox.vue
<template>
  <div class="custom-checkbox-wrapper">
    <el-checkbox 
      v-bind="$attrs" 
      v-on="$listeners" 
      @change="handleChange"
    >
      <slot></slot>
    </el-checkbox>
    <span v-if="showLabel" class="custom-label">{{ label }}</span>
  </div>
</template>

<script>
export default {
  name: 'CustomCheckbox',
  props: {
    label: String,
    showLabel: {
      type: Boolean,
      default: true
    }
  },
  methods: {
    handleChange(value) {
      // 在触发原生change事件前可以做一些额外处理
      console.log('复选框状态变化:', value);
      // 触发change事件,保持与原生组件行为一致
      this.$emit('change', value);
    }
  }
}
</script>

<style scoped>
.custom-label {
  margin-left: 8px;
  color: #606266;
}
</style>
  • usage.vue
<template>
  <div>
    <custom-checkbox 
      v-model="checked" 
      label="记住我" 
      @change="onChange"
      :disabled="isDisabled"
    />
  </div>
</template>

<script>
import CustomCheckbox from './custom-checkbox.vue';

export default {
  components: {
    CustomCheckbox
  },
  data() {
    return {
      checked: false,
      isDisabled: false
    };
  },
  methods: {
    onChange(value) {
      console.log('复选框状态:', value);
    }
  }
}
</script>

在上面这个例子中:

  • v-bind="$attrs"将父组件传递的所有非 prop 属性传递给内部的el-checkbox组件
  • v-on="$listeners"将父组件定义的所有事件监听器传递给内部组件
  • 组件可以定义自己的 props 和 methods,实现额外的功能

注意事项:

  • 优先级问题:如果子组件定义了与父组件相同的 prop 或事件处理,子组件的定义会覆盖父组件的
  • 性能考虑:在大型应用中过度使用属性透传可能会影响性能
  • Vue 3 中的变化:Vue 3 中listeners已被移除,其功能合并到了listeners已被移除,其功能合并到了attrs中

4. 应用场景

  • 组件库封装:在封装第三方组件库时非常有用,可以保持原有组件的 API 不变
  • 高阶组件开发:创建能够增强现有组件功能的高阶组件
  • 跨级组件通信:在不使用 Vuex 或 Event Bus 的情况下实现跨级组件通信
  • 表单组件封装:封装表单组件时,可以方便地传递各种表单属性和事件

本次分享就到这儿啦,我是鹏多多,如果看了觉得有帮助的,欢迎 点赞 关注 评论,在此谢过道友;

往期文章

转存失败,建议直接上传图片文件
❌
❌