组件化开发

102 阅读3分钟

CDD

全称:(Component-Driven Development)

  • 自下而上
  • 从组件级别开始,到页面级别结束

CDD好处

组件在最大程度被重用;

并行开发;

可视化测试;

处理组件的边界情况

  • root(小型应用中可以在vue根实例里存储共享数据,可以通过root(小型应用中可以在 vue 根实例里存储共享数据,**可以通过 root 访问根实例【一般情况不推荐这样做】**)
  • parent/parent / children(parent获取父组件,parent获取父组件,children获取子组件【作为了解】$children是个数组)
  • $refs

1649318591(1).jpg

  • 依赖注入 provide / inject

    • 子组件要访问父组件的title和handle()
      • 先在父组件提供title和handle()
    • 在子组件要使用这两个成员的位置,要通过inject注入进来
    • inject: ['title', 'handle']

01-parent.vue

<template>
  <div class="parent">
    parent
    <child></child>
  </div>
</template>

<script>
import child from './02-child'
export default {
  components: {
    child
  },
  // 子组件要访问父组件的title 和handle()
  //   先在父组件提供title 和handle()

  //   在子组件要使用这两个成员的位置
  //     要通过inject注入进来
  //     inject: ['title', 'handle']
  provide () {
    return {
      title: this.title,
      handle: this.handle
    }
  },
  data () {
    return {
      title: '父组件 provide'
    }
  },
  methods: {
    handle () {
      console.log(this.title)
    }
  }
}
</script>

02-child.vue

<template>
  <div class="child">
    child<br>
    title:{{ title }}<br>
    <button @click="handle">获取 title</button>
    <button @click="title='xxx'">改变 title</button>
    <grandson></grandson>
  </div>
</template>

<script>
import grandson from './03-grandson'
export default {
  components: {
    grandson
  },
  inject: ['title', 'handle']
}
</script>

03-grandson.vue

<template>
  <div class="grandson">
    grandson<br>
    title:{{ title }}<br>
    <button @click="handle">获取 title</button>
    <button @click="title='yyy'">改变 title</button>
  </div>
</template>

<script>
export default {
  inject: ['title', 'handle']
}
</script>

attrsattrs | listeners(开发自定义组件的时候会用到)

. $attrs

把父组件中非prop属性绑定到内部组件

$listeners

把父组件中的DOM对象的原生事件绑定到内部组件

快速原型开发

  • VueCLI中提供了一个插件可以进行原型快速开发
    • 需要先额外安装- -个全局的扩展
  • npm install -g @vue/cli-service- global
  • npm i @vue/cli-service-global
  • 使用vue serve快速查看组件的运行效果

vue serve

  • vue serve如果不指定参数默认会在当前目录找以下的入口文件
    • main.js、 index.js、 App.vue、app.vue
  • 可以指定要加载的组件
    • vue serve ./src/login.vue

运行该组件

新建一个app.vue

image-20220329102454004.png

运行:vue serve即可

快速原型开发-element

安装element

  • 初始化package.json
    • npm init -y
  • 安装ElementUl
    • vue add element
  • 加载ElementUl,使用Vue.use()安装插件

组件开发

表单组件

步骤条组件开发

image-20220329105744108.png

steps.css

.lg-steps {
  position: relative;
  display: flex;
  justify-content: space-between;
}

.lg-steps-line {
  position: absolute;
  height: 2px;
  top: 50%;
  left: 24px;
  right: 24px;
  transform: translateY(-50%);
  z-index: 1;
  background: rgb(223, 231, 239);
}

.lg-step {
  border: 2px solid;
  border-radius: 50%;
  height: 32px;
  width: 32px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-weight: 700;
  z-index: 2;
  background-color: white;
  box-sizing: border-box;
}

Steps.vue

<template>
  <div class="lg-steps">
    <div class="lg-steps-line"></div>
     <!-- :style="{ color: active >= index (active跟index进行对比)? activeColor(完成之后的样式) : defaultColor(默认样式【或者完成之前的样式】) }" -->
    <div
      class="lg-step"
      v-for="index in count"
      :key="index"
      
      :style="{ color: active >= index ? activeColor : defaultColor }"
    >
      {{ index }}
    </div>
  </div>
</template>

<script>
import './steps.css'
export default {
  name: 'LgSteps',
  // 接收父组件传递过来的数据
  props: {
    // 总共有多少步
    count: {
      type: Number,
      default: 3
    },
    // 控制当前完成第几步
    active: {
      type: Number,
      default: 0
    },
    // 完成之后的样式
    activeColor: {
      type: String,
      default: 'red'
    },
    // 默认的字体颜色
    defaultColor: {
      type: String,
      default: 'green'
    }
  }
}
</script>

<style>

</style>

Steps-test.vue


<template>
<!-- 使用该组件 -->
  <div>
    <steps :count="count" :active="active"></steps>
    <button @click="next">下一步</button>
  </div>
</template>

<script>
import Steps from './Steps.vue'
export default {
  components: {
    Steps
  },
  data () {
    return {
      count: 4,
      active: 1
    }
  },
  methods: {
    next () {
      this.active++
    }
  }
}
</script>

form表单开发

image-20220330093222271.png

form->Button.vue

<template>
  <div>
      <button @click="handleClick"><slot></slot></button>
  </div>
</template>

<script>
export default {
    name:'LgButton',
    methods:{
        // evt 事件对象
        handleClick(evt){
            this.$emit('click',evt)
            // 默认会提交表单 要阻止默认行为
            evt.preventDefault()
        }
    }
}
</script>

form->Form.vue

<template>
<!-- 直接form标签 因为渲染的也是form标签 -->
  <form>
    <slot></slot>
  </form>
</template>

<script>
export default {
  name: 'LgForm',
  // 直接返回一个对象
  provide () {
    return {
      form: this //当前组件对象
    }
  },
  props: {
    model: {
      type: Object
    },
    rules: {
      type: Object
    }
  },
  methods: {
    validate (cb) {
      const tasks = this.$children //拿到所有itme组件
        .filter(child => child.prop)//child 所有子组件
        .map(child => child.validate())
      // 查看是否都执行成功
      Promise.all(tasks)
        .then(() => cb(true))
        .catch(() => cb(false))
    }
  }
}
</script>

<style>

</style>

form->FormItem.vue

<template>
  <div>
    <!-- 显示提示的文本 -->
    <label>{{ label }}</label>
    <div>
      <slot></slot>
      <!-- 当验证失败的时候显示验证失败的原因 -->
      <p v-if="errMessage">{{ errMessage }}</p>
    </div>
  </div>
</template>

<script>
import AsyncValidator from 'async-validator'
export default {
  name: 'LgFormItem',
  inject: ['form'], //要注入的名称 这样可以获取表单对象
  props: {
    // 显示的文本
    label: {
      type: String
    },
    // 验证字段的名称
    prop: {
      type: String
    }
  },
  mounted() {
    // 当组件渲染完成之后注册validate事件
    this.$on('validate', () => {
      this.validate()
    })
  },
  data () {
    return {
      errMessage: ''
    }
  },
  methods: {
    // 验证的方法
    validate () {
      // 如果没有传递prop这个选项就不做验证 
      if (!this.prop) return
      const value = this.form.model[this.prop]//要验证的数据
      const rules = this.form.rules[this.prop]//要验证的规则

      const descriptor = { [this.prop]: rules } //要验证的属性名和验证对象
      const validator = new AsyncValidator(descriptor)
      // { [this.prop]: value } 要验证的属性以及值
      // 验证失败的回调函数(errors)
      return validator.validate({ [this.prop]: value }, errors => {
        if (errors) {
          this.errMessage = errors[0].message //验证失败的信息
        } else {
          this.errMessage = '' //验证失败信息进行清空
        }
      })
    }  
  }
}
</script>

<style>

</style>

form->Input.vue

<template>
  <div>
    <input v-bind="$attrs" :type="type" :value="value" @input="handleInput">
  </div>
</template>

<script>
export default {
  name: 'LgInput',
  inheritAttrs: false, //禁用继承父组件传递过来的属性
  props: {
    value: {
      type: String
    },
    type: {
      type: String,
      default: 'text' //默认值是text
    }
  },
  methods: {
    // evt 事件对象
    handleInput (evt) {
      // 触发父组件自定义事件
      // evt.target.value 文本框的值
      this.$emit('input', evt.target.value)
      const findParent = parent => {
        // 查看父组件是否存在
        while (parent) {
          if (parent.$options.name === 'LgFormItem') {
            break
          } else {
            // 如果找不到父组件 就继续找
            parent = parent.$parent
          }
        }
        return parent
      }
      //$parent触发当前组件的父组件
      const parent = findParent(this.$parent)
      if (parent) {
        parent.$emit('validate') //如果找到了父组件中的父组件 然后触发validate
      }
    }
  }
}
</script>

<style>

</style>

Form-test.vue(使用上面四个组件)

<template>
  <lg-form class="form" ref="form" :model="user" :rules="rules">
    <lg-form-item label="用户名" prop="username">
      <!-- <lg-input v-model="user.username"></lg-input> -->
      <lg-input :value="user.username" @input="user.username=$event" placeholder="请输入用户名"></lg-input>
    </lg-form-item>
    <lg-form-item label="密码" prop="password">
      <lg-input type="password" v-model="user.password"></lg-input>
    </lg-form-item>
    <lg-form-item>
      <lg-button type="primary" @click="login">登 录</lg-button>
    </lg-form-item>
  </lg-form>
</template>

<script>
import LgForm from './form/Form'
import LgFormItem from './form/FormItem'
import LgInput from './form/Input'
import LgButton from './form/Button'
export default {
  components: {
    LgForm,
    LgFormItem,
    LgInput,
    LgButton
  },
  data () {
    return {
      user: {
        username: '',
        password: ''
      },
      rules: {
        username: [
          {
            required: true,
            message: '请输入用户名'
          }
        ],
        password: [
          {
            required: true,
            message: '请输入密码'
          },
          {
            min: 6,
            max: 12,
            message: '请输入6-12位密码'
          }
        ]
      }
    }
  },
  methods: {
    login () {
      console.log('button')
      this.$refs.form.validate(valid => {
        if (valid) {
          alert('验证成功')
        } else {
          alert('验证失败')
          return false
        }
      })
    }
  }
}
</script>

<style>
  .form {
    width: 30%;
    margin: 150px auto;
  }
</style>

storybook安装

自动安装

npx -p @storybook/cli init --type vue(vue 强制使用vue的方式)

npx -p @storybook/cli sb init --type vue(跟上面是一样的)

yarn add vue

vue yarn add vue-loader vue-template-compiler --dev

Lerna

Lerna 是一个工具,它优化了使用 git 和 npm 管理多包仓库的工作流工具

用于管理有多个包的JavaScript项目

他可以一键把代码提交到Git和npm仓库

Lerna的使用

全局安装

  • yarn global add lerna
  • 初始化
    • lerna init
  • 发布
    • lerna publish

Lerna Script

  • lerna create [loc]
    • 创建一个包,name 包名,loc 位置可选;
  • lerna add [@version] [--dev] [--exact]
    • 增加本地或远程 package 做为当前项目 packages 里面的依赖;--exact 安装准确版本;
  • lerna bootstrap
    • 把所有包的依赖安装到根 node_modules;
  • lerna list
    • 列出所有的包,如果与你文件夹里面的不符,进入那个包运行 yarn init -y;
  • lerna run
    • 运行所有包里面的有这个 script 的命令;
  • lerna exec
    • 运行任意命令在每个包;
  • lerna link
    • 项目包建立软链,类似 npm link:
  • lerna clean
    • 删除所有包的 node_modules 目录;
  • lerna changed
    • 列出下次发版 lerna publish 要更新的包;
  • lerna publish
    • 会打 tag,上传 git,上传 npm;
  • yarn workspaces run del
    • 运行所有文件中的 del 命令;