vue 组件设计学习笔记

240 阅读4分钟

一、组件分类

  • 页面组件(直接通过路由渲染而挂载的组件)相对独立,一般没有 props 等接口;
  • 业务组件,可以在项目多个页面中复用的组件,设计时候需要考虑通用性和扩展性;
  • 基础组件:和业务关联度较低,可以在多个页面中使用,如日历组件,菜单组件;

二、vue 组件的基础属性

prop:

  • 对象写法可以确定类型,默认值,以及 validator 方法(校验传入值);
  • 标准的 html 属性,可以直接给组件传递,如 id,class 等;

event:自定义事件

  • 通过 $emit 方法,可以触发自定义事件,父级通过自定级事件@-事件名来监听;
  • 组件内部的点击事件,父组件上不能直接绑定 click事件,监听不到。可以用 @click.native 修饰符;

slot

  • slot 提供了更高级别的组件扩展方式,直接插入扩展内容。(如果完全用 prop来控制组件,会让组件很笨重);
  • 组件内部 slot 标签可以设置默认值,如果传值进来可以覆盖;
  • slot 提供具名 slot 和匿名 slot,通过 v-slot(缩写:#)来设置别名;
  • slot 默认可以使用的是父组件的内容来渲染;
  • slot-scope(最新版的 v-slot )可以赋予其访问其自身的能力,前提,内部 slot 定义的时候,需要绑定;
//父组件的slot代码,使用子组件的数据
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>
// 自组件定义:
<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>

三、组件间通讯

1、常见方式

parent/parent / children

ref

#provide / inject

  • 祖先向后代组件注入一个依赖(非响应式);
  • 在项目根组件注入一个 provide,把自己提供出去,可以实现一个简单的全局数据共享;
  • 对于 Form 和 FormItem 关联性很高的组件,用这个可以实现;
    provide () {
      return {
        app: this
      }
    }

dispatch / broadcast

手写两个方法,将其注入 vue 实例;

dispatch 向上级派发事件,parent && 没找到,parent = parent.$parent;

broadcast 向下对 $children 属性进行循环;

2、从 Form 组件中学习组件传值

常见的配置为 Form -> FormItem -> Input。

Form:有 model 和 rules 规则

  • Form 的 created 中,监听这个操作,完成订阅发布(父 created -> 子 mounted,防止监听不到);
  • Form 进行发布 validate 方法,调用全部 FormItem 的 validate ,进行校验。并通过回调或者 promise 来接受所有子 validate 的结果;

FormItem:有prop属性(model中的key)

  • FormItem 通过 slot 插槽,插入 Form 组件中。在 mounted 的时候,通过 dispatch 方法,将自己派发给最近的父级 Form,完成事件的订阅;
  • FormItem 中监听子组件发的 change 或者 blur 事件,触发 validate 方法,会通过 provide/inject 拿到父元素,然后取到 model[prop] 的值,然后初始化 async-validator 实例进行校验,返回校验状态;

Input元素:

  • Input 等表单项有 blur 和 change 方法,他们被触发后,向父级的 FormItem 派发 change 或者 blur 事件;

3、FindComponents

通过递归操作,来实现向下或者向下寻找组件。

a、findComponentUpward(context, name)

通过 name 属性,对 context.$parent 进行递归寻找,找到上层最近的目标组件。

function findComponentUpward (context, componentName) {
  let parent = context.$parent;
  let name = parent.$options.name;
// 当有父级,且 没有找到目标 name(即没有 name 属性 或 name 不匹配) 
  while (parent && (!name || [componentName].indexOf(name) < 0)) {
    parent = parent.$parent;
    if (parent) name = parent.$options.name;
  }
  return parent;
}

b、findComponentUpward(context, name)

通过 name 属性,对 context.$parent 进行递归寻找,找到上层所有目标组件。

function findComponentsUpward (context, componentName) {
  let parents = [];
  const parent = context.$parent;

  if (parent) {
    if (parent.$options.name === componentName) parents.push(parent);
    return parents.concat(findComponentsUpward(parent, componentName));
  } else {
    return [];
  }
}

c、findComponentDownward(context, name)

通过 name 属性,对 context.$children 进行递归寻找,向下找最近的目标组件。

function findComponentDownward (context, componentName) {
  const childrens = context.$children;
  let children = null;

  if (childrens.length) {
    for (const child of childrens) {
      const name = child.$options.name;

      if (name === componentName) {
        children = child;
        break;
      } else {
        children = findComponentDownward(child, componentName);
        if (children) break;
      }
    }
  }
  return children;
}

d、findBrothersComponents(context, name)

通过 name 属性,对 context.$children 进行递归寻找,向下找所有的目标组件。

function findComponentsDownward (context, componentName) {
  return context.$children.reduce((components, child) => {
    if (child.$options.name === componentName) components.push(child);
    const foundChilds = findComponentsDownward(child, componentName);
    return components.concat(foundChilds);
  }, []);
}

e、findBrothersComponents(context, name)

通过 name 属性,对 context.parent.parent.children 进行寻找,寻找符合条件的的兄弟组件。

function findBrothersComponents (context, componentName, exceptMe = true) {
  let res = context.$parent.$children.filter(item => {
    return item.$options.name === componentName;
  });
  let index = res.findIndex(item => item._uid === context._uid);
  if (exceptMe) res.splice(index, 1);
  return res;
}

四、Vue手动构造

1、构造器 extend

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

通过 Vue.extend 方法,可以帮我们将一个,构造成一个 vue 组件子类,其中 extend 方法会像 new Vue 一样执行,如 minix 配置,计算属性的处理等。

通过 import 引用的 vue 组件,都会自动 webpack 过程中调用 Vue.extend 方法生成一个子类。

// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})

2、Render 方法渲染

import Vue from 'vue';
import Notification from './notification.vue';

const props = {};  // 这里可以传入一些组件的 props 选项

const Instance = new Vue({
  render (h) {
    return h(Notification, {
      props: props
    });
  }
});

const component = Instance.$mount();
document.body.appendChild(component.$el);

3、手动挂载

当子组件被 new 出来的时候,需要执行 $mount() 方法才能执行手动渲染。然后再去挂载到一个 dom 元素上面。

// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')

4、实例

Alert 组件

Alert 组件需要挂在 body 元素下面,而 vue实例整个挂载在 #app 元素上面,需要用 Vue.extend("alert") 或者 new Vue(alert) 构造出组件实例,然后用 $mount() 渲染完成,最后用 document.body.appendChild() 挂载。

动态组件

根据后台传递过来的代码,手动构造出 Vue 的组件实例,渲染并挂载带到页面中。

五、Render 函数

1、定义

字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode。

如果组件是一个函数组件,渲染函数还会接收一个额外的 context 参数,为没有实例的函数组件提供上下文信息。

createElement 是一个核心方法,返回 VNode 对象。

2、Render 简介

需要三个参数(要渲染的标签或者组件,数据对象(props、data、class、slot 等),子节点( string 或者 array )。

  • 约束:VNode 必须唯一
    • 子节点属性不允许有两个相同的 VNode;
  • 事件修饰符:
修饰符前缀
.passive&
.capture!
.once~

3、使用场景

  • 1、两个相同的 slot;
  • 2、服务端渲染中,用 Vue.extend 和 new Vue 生成的组件实例,编译不过去;
  • 3、Runtime 版本的 Vue.js,如果用 Vue.extend 构造一个实例,因为无法编译 template 语法,所以会报错;
  • 4、父子通信复杂的内容时候,如 table 渲染具体单元格,父传递进来的 props 是一个组件,这时候需要 Render 来最大解析这个内容,或者用作用域插槽(slot-scope);

4、Functional

设置为 true,函数化组件,无状态无实例,也就是没用 this 上下文和 data。Render 方法返回的是虚拟节点,渲染更方便。

  • 如承载 Render 方法的一个渲染组件,没有子级的状态,供其他高级组件调用;
  • 将 children,props,data 传递给子组件前操作他们(做了一层嵌套,装饰器模式);

5、实例:Table

1、Table 的每列单元格,默认值展示字符,扩展度较低,可以将 Render 方法传到每列的渲染组件中,让渲染组件根据我们的 Render 方法自由渲染( React 的写法)。

2、如果要实现行内修改数据的功能,需要每个列的 Render 函数都判断是否修改当前行,如果是显示input,否则显示字符串。

6、slot-scope 更方便的实现表格扩展

slot-scope 可以直接将组件插入列中,并用列的数据渲染出来。

slot-scope 可以和 Render 方法结合使用:

  • Render 中可以用 $.scopeSlots 访问作用域插槽,作用域插槽是返回一个 VNode 的函数;
  • Render 中可以用 this.$slots 访问静态插槽;
  • Render 中可以向子组件中传递作用域插槽,用 scopedSlots 字段;

7、JSX

Render 方法的写法相对于 template 过于复杂,通过 Babel 插件,我们可以用 JSX 语法,让代码变得更简单。

npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props
// babel.config.js
module.exports = {
  presets: ['@vue/babel-preset-jsx'],
}

六、递归组件和动态组件

1、递归组件

  • 要求
    • 组件需要明确的 name ;
    • 要有明确的结束条件;
  • 实例:
    • 级联选择器,树形控件,菜单;

2、动态组件

  • Functional Render 可以实现动态渲染组件;
  • 内置组件 和 is 属性。( keep-alive 可以帮助缓存);

3、异步组件

代码如下:

{
  path: '/a',
  component: () => import('./a.vue')
}

4、树形控件

特点:

  • 无限延伸

  • 展开收起

  • 父节点可以控制子节点的全选/被全选

    • 父节点控制子节点功能全选/被全选,父节点点击后,递归判断有没有 children 属性;
  • 所有子节点选中,其父节点自动选中,递归向上判断

    • 每个 node 组件通过 deep-watch 判断自己的 children 属性;
  const checkedAll = !data.some(item => !item.checked)
  // 上面写法更优,会提前中断判断。
  const checkAll = data.every(item => item.checked) 

附录:

Vue 单文件组件的实现

Vue 组件被浏览器识别需要用 vue-loader 解析为SFC对象。

// vue-loader/index.js
const descriptor = parse({
    source,
    compiler: options.compiler || loadTemplateCompiler(),
    filename,
    sourceRoot,
    needMap: sourceMap
})

// vuejs/component-compiler-utils/index.js
function parse(options) {
    const { compiler } = options
    output = compiler.parseComponent(source, compilerParseOptions)
    return output
}

// vue.js
function parseComponent(content, options) {
	// ...
    var sfc = {
        template: null,
        script: null,
        styles: [],
        customBlocks: []
    }
    // ...
    return sfc
}

配置的 sass-loader 等常见 loader 之所以可以解析 Vue 组件中的 Sass 等内容也是借助了 vue-loader 的功能。

Scoped CSS

简介:

当 style 标签有 scoped 属性时,css 只会作用于当前组件中。它通过了 PostCss 实现了一个作用域的功能。

其中 HTML 添加自定义属性,CSS 属性添加属性选择。

原理:vue-loader 编译 vue 组件的时候,解析完成就会生成组件 id。

  • templateLoader 在 ast 转化过程中,具体元素会增加自动以属性 data-v-xxxx;
  • stylePostLoader 处理样式文件时, 会含有scoped 的模板插入 data-v-xxxx;
const id = hash(
    isProduction
      ? shortFilePath + '\n' + source.replace(/\r\n/g, '\n')
      : shortFilePath
  )
<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

深度作用选择器

使用 scoped 后,父组件样式不会渗透子组件内部。使用 >>> 操作符,或者 /deep/、::v-deep 等操作符(别名)。

<style scoped>
.a >>> .b { /* ... */ }
</style>
// 代码会被编译为
.a[data-v-f3f3eg9] .b { /* ... */ }

参考内容:

juejin.cn/book/684473…

cn.vuejs.org/v2/api/

juejin.cn/post/684490…

vue-loader.vuejs.org/zh/