Skip to content

三.数据输出(表格)

前言 --> 表格组件特点

  • 下拉菜单组件应该由两部分组成:
    • 表头
    • 数据展示部分
  • 它的主要功能包括:
    • 数据展示部分也可以嵌入基本组件,也可以使用自定义组件
    • <table><thead><tbody><tr>thtd这些标签组成,一般分为表头columns和数据data.

1.目录结构

sh
├── table
   ├── TableRender.vue
   ├── render.js
   ├── pages
   └── index.js

1.Render 表格组件

先将基础表格封装成一个组件
vue
<!-- src/components/table/TableRender.vue -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="(col, i) in columns" :key="i">{{ col.title }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(row, i) in data" :key="i">
        <td v-for="(col, j) in columns" :key="j">{{ row[col.key] }}</td>
      </tr>
    </tbody>
  </table>
</template>
<script>
export default {
  props: {
    columns: {
      type: Array,
      default: () => [],
    },
    data: {
      type: Array,
      default: () => [],
    },
  },
}
</script>
<style>
table {
  width: 100%;
  border-collapse: collapse;
  border-spacing: 0;
  empty-cells: show;
  border: 1px solid #e9e9e9;
}
table th {
  background: #f7f7f7;
  color: #5c6b77;
  font-weight: 600;
  white-space: nowrap;
}
table td,
table th {
  padding: 8px 16px;
  border: 1px solid #e9e9e9;
  text-align: left;
}
</style>

组件的封装-->使用

  • 可以看到一个纯文本显示的表格组件已经封装好了

  • 现在有些地方需要用一些额外的逻辑(如生日)修改显示的效果,这里可以使用 render 函数来实现

  • Render 自定义列模板

    js
    // src/components/table/render.js
    export default {
      functional: true,
      props: {
        row: Object,
        column: Object,
        index: Numner,
        render: Function,
      },
      render: (h, ctx) => {
        const params = {
          row: ctx.props.row,
          column: ctx.props.column,
          index: ctx.props.index,
        }
        return ctx.props.render(h, params)
      },
    }

render.js 定义了 4 个 props:

  • row:当前的数据
  • column:当前列的数据
  • index:当前是第几行
  • render:具体的 render 函数内容 这里的render选项并没有渲染任何节点,而是直接返回 props 中定义的 render,并将 h 和当前的行、列、序号作为参数传递出去。然后在 table.vue 里就可以使用 render.js 组件:
vue
// table.vue
<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns">{{ col.title }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(row, rowIndex) in data">
        <td v-for="col in columns">
          <template v-if="'render' in col">
            <Render
              :row="row"
              :column="col"
              :index="rowIndex"
              :render="col.render"
            ></Render>
          </template>
          <template v-else> {{ row[col.key] }}</template>
        </td>
      </tr>
    </tbody>
  </table>
</template>
<script>
import Render from "./render.js"

export default {
  components: { Render },
  props: {
    columns: {
      type: Array,
      default() {
        return []
      },
    },
    data: {
      type: Array,
      default() {
        return []
      },
    },
  },
}
</script>

如果 columns 中的某一列配置了 render 字段,那就通过 render.js 完成自定义模板,否则以字符串形式渲染。比如对出生日期这列显示为标准的日期格式,可以这样定义 column:

js
export default {
  data() {
    return {
      columns: [
        // ...
        {
          title: "出生日期",
          render: (h, { row, column, index }) => {
            const data = new Date(parseInt(row.birthday))
            const year = date.getFullYear()
            const month = date.getMonth() + 1
            const day = date.getDate()
            const birthday = `${year}-${month}-${day}`
            return h("span", birthday)
          },
        },
      ],
    }
  },
}

需要注意的是,columns 里定义的 render,是有两个参数的,第一个是 createElement(即 h),第二个是从 render.js 传过来的对象,它包含了当前行的数据(row)、当前列配置(column)、当前是第几行(index),使用者可以基于这 3 个参数得到任意想要的结果。由于是自定义列,显示什么都是使用者决定的,因此在使用了 render 的 column 里可以不用谢字段key

  • 修改当前行

有了 render,Table 组件就已经完成了,剩余工作都是使用者来配置 columns 完成各种复杂的业务逻辑。

操作这一列,默认是一个修改按钮,点击后,变为保存取消两个按钮,同时本行其他各列都变为了输入框,并且初始值就是刚才单元格的数据。变为输入框后,可以任意修改单元格数据,点击保存按钮保存完整行数据,点击取消按钮,还原至修改前的数据。

当进入编辑状态时,每一列的输入框都要有一个临时的数据使用v-model双向绑定来响应修改,所以在 data 里再声明四个数据。

js
// table-render.vue
{
  data(){
    return {
      // ...
      editName: '', // 第一列输入框
      editAge: '', // 第二列输入框
      editBirthday: '', //第三列输入框
      editAddress: '', //第四列输入框
    }
  }
}

同时还要知道是在修改第几行的数据,所以再加上一个数据标识当前正在修改的行序号(从 0 开始)

js
// table-render.vue
{
  data (){
    return {
      // ...
      editIndex: -1, // 当前聚焦的输入框的行数
    }
  }
}

editIndex默认给了 -1,也就是一个不存在的行号,当点击修改按钮时,再将它置为正确的行号。我们先定义操作列的 render:

js
// table-render.vue
{
  data() {
    return {
      columns: [
        //  ...
        {
          title: '操作',
          render: (h, { row, index}) =>{
            //  如果当前行是编辑状态,则渲染两个按钮
            if( this.editIndex === index) {
              return [
                h('button', {
                  on: {
                    click :() => {
                      this.data[index].name = this.editName
                      this.data[index].age = this.editAge
                      this.data[index].birthday = this.editBirthday
                      this.data[index].address = this.editAddress
                      this.editIndex = -1
                    }
                  }
                },'保存'),
                h('button',{
                  style: {
                    marginLeft: '6px'
                  },
                  on: {
                    click: () =>{
                      this.editIndex = -1
                    }
                  }
                },'取消')
              ]
            }else{ // 当前行是默认状态,渲染为一个按钮
              return h('button', {
                on: {
                  click: () => {
                    this.editName = row.name
                    this.editAge = row.age
                    this.editAddress = row.address
                    this.editBirthday = row.birthday
                    this.editIndex = index
                  }
                }
              },'修改')
            }
          }
        }
      ]
    }
  }
}

render 里的if/else可以先看 else,因为默认是非编辑状态,也就是说editIndex还是-1,。当点击修改按钮时,把 render 中第二个参数{row}中的各列数据赋值给了之前在 data 中声明的 4 个数据,这样做是因为之后点击取消按钮时,editName 等值已经修改好了,还没有还原,所以在开启编辑状态的同时,初始化各输入框的值(当然也可以在取消时重置)。最后再把editIndex置为对应的行序号{index},此时 render 的if条件this.editIndex === index为真,编辑列变为两个按钮,保存和取消。点击保存,直接修改表格源数据 data 中对应的各字段值,并将 editIndex 置为-1,退出编辑状态;点击取消,不保存源数据,直接退出编辑器。

除了编辑列,其他各数据列都有两种状态:

  • 1.当 editIndex 等于当前行号 index 时,呈现输入框状态
  • 2.当 editIndex 不等于当前行号 index 时,呈现默认数据。

以姓名为例:

js
// table-render.vue
{
  data(){
    return {
      columns :[
        //  ...
        {
          title: '姓名',
          key: 'name',
          render: (h,{ row, index}) =>{
            let edit

            // 当前行为聚焦时
            if(this.editIndex === index){
              edit = [h('input'),{
                domProps: {
                  value: row.name
                },
                on: {
                  input:(event)=>{
                    this.editName = event.target.value
                  }
                }
              }]
            }else{
              edit = row.name
            }

            return h('div', [
              edit
            ])
          }
        }
      ]
    }
  }
}

变量edit根据 editIndex 呈现不同的节点,还是先看 else,直接显示了对应字段的数据。在聚焦是(this.editIndex === index),渲染一个input输入框,初始值value通过 render 的domProps绑定了row.name(这里也可以绑定 editName),并监听了 input 时间,将输入的内容,实时缓存在数据editName中,供保存使用。实际上,这里绑定的 value 和时间 input 就是语法糖v-model在 Render 函数中的写法,在 template 中,经常写作<input v-model="editName">

其他列类似

js
// table-render.vue,部分代码省略
{
  data () {
    return {
      columns: [
        // ...
        {
          title: '年龄',
          key: 'age',
          render: (h, { row, index }) => {
            let edit;

            // 当前行为聚焦行时
            if (this.editIndex === index) {
              edit = [h('input', {
                domProps: {
                  value: row.age
                },
                on: {
                  input: (event) => {
                    this.editAge = event.target.value;
                  }
                }
              })];
            } else {
              edit = row.age;
            }

            return h('div', [
              edit
            ]);
          }
        },
        {
          title: '出生日期',
          render: (h, { row, index }) => {
            let edit;

            // 当前行为聚焦行时
            if (this.editIndex === index) {
              edit = [h('input', {
                domProps: {
                  value: row.birthday
                },
                on: {
                  input: (event) => {
                    this.editBirthday = event.target.value;
                  }
                }
              })];
            } else {
              const date = new Date(parseInt(row.birthday));
              const year = date.getFullYear();
              const month = date.getMonth() + 1;
              const day = date.getDate();

              edit = `${year}-${month}-${day}`;
            }

            return h('div', [
              edit
            ]);
          }
        },
        {
          title: '地址',
          key: 'address',
          render: (h, { row, index }) => {
            let edit;

            // 当前行为聚焦行时
            if (this.editIndex === index) {
              edit = [h('input', {
                domProps: {
                  value: row.address
                },
                on: {
                  input: (event) => {
                    this.editAddress = event.target.value;
                  }
                }
              })];
            } else {
              edit = row.address;
            }

            return h('div', [
              edit
            ]);
          }
        },
      ]
    }
  }
}

实际上,很多 Vue.js 的开发难题,都可以用 Render 函数来解决,它比 template 模板更灵活,可以完全发挥 JavaScript 的编程能力,因此很多 JS 的开发思想可以借鉴。如果你习惯 JSX,那完全可以抛弃传统的 template 写法。

Render 虽好,但也有弊端,通过上面的示例可以发现,写出来的 VNode 对象时很难读的,维护性比 template 差。

2.方案一

第一种方案,用最简单的 slot-scope 实现,同时兼容 Render 函数的旧用法。拷贝上一节的 Table 组件目录,更名为table-slot,同时也拷贝路由,更名为table-slot.vue。为了兼容旧的 Render 函数用法,在 columns 的列配置 column 中,新增一个字段slot来指定 slot-scope 的名称:

vue
<!-- src/components/table-slot/table.vue -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns">{{ col.title }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(row, rowIndex) in data">
        <td v-for="col in columns">
          <template v-if="'render' in col">
            <Render
              :row="row"
              :column="col"
              :index="rowIndex"
              :render="col.render"
            >
            </Render>
          </template>
          <template v-else-if="'slot' in col">
            <slot
              :row="row"
              :column="col"
              :index="rowIndex"
              :name="col.slot"
            ></slot>
          </template>
          <template v-else>{{ row[col.key] }}</template>
        </td>
      </tr>
    </tbody>
  </table>
</template>

相比原先的文件,只在 'render' in col 的条件下新加了一个 template 的标签,如果使用者的 column 配置了 render 字段,就优先以 Render 函数渲染,然后再判断是否用 slot-scope 渲染。在定义的作用域 slot 中,将行数据 row、列数据 column 和第几行 index 作为 slot 的参数,并根据 column 中指定的 slot 字段值,动态设置了具名 name。使用者在配置 columns 时,只要指定了某一列的 slot,那就可以在 Table 组件中使用 slot-scope。我们以上一节的可编辑整行数据为例,用 slot-scope 的写法实现完全一样的效果:

vue
<!-- src/views/table-slot.vue -->
<template>
  <div>
    <table-slot :columns="columns" :data="data">
      <template slot-scope="{ row, index }" slot="name">
        <input type="text" v-model="editName" v-if="editIndex === index" />
        <span v-else>{{ row.name }}</span>
      </template>

      <template slot-scope="{ row, index }" slot="age">
        <input type="text" v-model="editAge" v-if="editIndex === index" />
        <span v-else>{{ row.age }}</span>
      </template>

      <template slot-scope="{ row, index }" slot="birthday">
        <input type="text" v-model="editBirthday" v-if="editIndex === index" />
        <span v-else>{{ getBirthday(row.birthday) }}</span>
      </template>

      <template slot-scope="{ row, index }" slot="address">
        <input type="text" v-model="editAddress" v-if="editIndex === index" />
        <span v-else>{{ row.address }}</span>
      </template>

      <template slot-scope="{ row, index }" slot="action">
        <div v-if="editIndex === index">
          <button @click="handleSave(index)">保存</button>
          <button @click="editIndex = -1">取消</button>
        </div>
        <div v-else>
          <button @click="handleEdit(row, index)">操作</button>
        </div>
      </template>
    </table-slot>
  </div>
</template>
<script>
import TableSlot from "../components/table-slot/table.vue"

export default {
  components: { TableSlot },
  data() {
    return {
      columns: [
        {
          title: "姓名",
          slot: "name",
        },
        {
          title: "年龄",
          slot: "age",
        },
        {
          title: "出生日期",
          slot: "birthday",
        },
        {
          title: "地址",
          slot: "address",
        },
        {
          title: "操作",
          slot: "action",
        },
      ],
      data: [
        {
          name: "王小明",
          age: 18,
          birthday: "919526400000",
          address: "北京市朝阳区芍药居",
        },
        {
          name: "张小刚",
          age: 25,
          birthday: "696096000000",
          address: "北京市海淀区西二旗",
        },
        {
          name: "李小红",
          age: 30,
          birthday: "563472000000",
          address: "上海市浦东新区世纪大道",
        },
        {
          name: "周小伟",
          age: 26,
          birthday: "687024000000",
          address: "深圳市南山区深南大道",
        },
      ],
      editIndex: -1, // 当前聚焦的输入框的行数
      editName: "", // 第一列输入框,当然聚焦的输入框的输入内容,与 data 分离避免重构的闪烁
      editAge: "", // 第二列输入框
      editBirthday: "", // 第三列输入框
      editAddress: "", // 第四列输入框
    }
  },
  methods: {
    handleEdit(row, index) {
      this.editName = row.name
      this.editAge = row.age
      this.editAddress = row.address
      this.editBirthday = row.birthday
      this.editIndex = index
    },
    handleSave(index) {
      this.data[index].name = this.editName
      this.data[index].age = this.editAge
      this.data[index].birthday = this.editBirthday
      this.data[index].address = this.editAddress
      this.editIndex = -1
    },
    getBirthday(birthday) {
      const date = new Date(parseInt(birthday))
      const year = date.getFullYear()
      const month = date.getMonth() + 1
      const day = date.getDate()

      return `${year}-${month}-${day}`
    },
  },
}
</script>

示例中在 <table-slot> 内的每一个 <template> 就对应某一列的 slot-scope 模板,通过配置的 slot 字段,指定具名的 slot-scope。可以看到,基本是把 Render 函数还原成了 html 的写法,这样看起来直接多了,渲染效果是完全一样的。在 slot-scope 中,平时怎么写组件,这里就怎么写,Vue.js 所有的 API 都是可以直接使用的。

方案一是最优解,一般情况下,使用这种方案就可以了,其余两种方案是基于 Render 的。

3.方案二

第二种方案,不需要修改原先的 Table 组件代码,只是在使用层面修改即可。先来看具体的使用代码,然后再做分析。注意,这里使用的 Table 组件,仍然是上一节 src/components/table-render 的组件,它只有 Render 函数,没有定义 slot-scope:

vue
<!-- src/views/table-render.vue 的改写 -->
<template>
  <div>
    <table-render ref="table" :columns="columns" :data="data">
      <template slot-scope="{ row, index }" slot="name">
        <input type="text" v-model="editName" v-if="editIndex === index" />
        <span v-else>{{ row.name }}</span>
      </template>

      <template slot-scope="{ row, index }" slot="age">
        <input type="text" v-model="editAge" v-if="editIndex === index" />
        <span v-else>{{ row.age }}</span>
      </template>

      <template slot-scope="{ row, index }" slot="birthday">
        <input type="text" v-model="editBirthday" v-if="editIndex === index" />
        <span v-else>{{ getBirthday(row.birthday) }}</span>
      </template>

      <template slot-scope="{ row, index }" slot="address">
        <input type="text" v-model="editAddress" v-if="editIndex === index" />
        <span v-else>{{ row.address }}</span>
      </template>

      <template slot-scope="{ row, index }" slot="action">
        <div v-if="editIndex === index">
          <button @click="handleSave(index)">保存</button>
          <button @click="editIndex = -1">取消</button>
        </div>
        <div v-else>
          <button @click="handleEdit(row, index)">操作</button>
        </div>
      </template>
    </table-render>
  </div>
</template>
<script>
import TableRender from "../components/table-render/table.vue"

export default {
  components: { TableRender },
  data() {
    return {
      columns: [
        {
          title: "姓名",
          render: (h, { row, column, index }) => {
            return h(
              "div",
              this.$refs.table.$scopedSlots.name({
                row: row,
                column: column,
                index: index,
              })
            )
          },
        },
        {
          title: "年龄",
          render: (h, { row, column, index }) => {
            return h(
              "div",
              this.$refs.table.$scopedSlots.age({
                row: row,
                column: column,
                index: index,
              })
            )
          },
        },
        {
          title: "出生日期",
          render: (h, { row, column, index }) => {
            return h(
              "div",
              this.$refs.table.$scopedSlots.birthday({
                row: row,
                column: column,
                index: index,
              })
            )
          },
        },
        {
          title: "地址",
          render: (h, { row, column, index }) => {
            return h(
              "div",
              this.$refs.table.$scopedSlots.address({
                row: row,
                column: column,
                index: index,
              })
            )
          },
        },
        {
          title: "操作",
          render: (h, { row, column, index }) => {
            return h(
              "div",
              this.$refs.table.$scopedSlots.action({
                row: row,
                column: column,
                index: index,
              })
            )
          },
        },
      ],
      data: [],
      editIndex: -1, // 当前聚焦的输入框的行数
      editName: "", // 第一列输入框,当然聚焦的输入框的输入内容,与 data 分离避免重构的闪烁
      editAge: "", // 第二列输入框
      editBirthday: "", // 第三列输入框
      editAddress: "", // 第四列输入框
    }
  },
  methods: {
    handleEdit(row, index) {
      this.editName = row.name
      this.editAge = row.age
      this.editAddress = row.address
      this.editBirthday = row.birthday
      this.editIndex = index
    },
    handleSave(index) {
      this.data[index].name = this.editName
      this.data[index].age = this.editAge
      this.data[index].birthday = this.editBirthday
      this.data[index].address = this.editAddress
      this.editIndex = -1
    },
    getBirthday(birthday) {
      const date = new Date(parseInt(birthday))
      const year = date.getFullYear()
      const month = date.getMonth() + 1
      const day = date.getDate()

      return `${year}-${month}-${day}`
    },
  },
  mounted() {
    this.data = [
      {
        name: "王小明",
        age: 18,
        birthday: "919526400000",
        address: "北京市朝阳区芍药居",
      },
      {
        name: "张小刚",
        age: 25,
        birthday: "696096000000",
        address: "北京市海淀区西二旗",
      },
      {
        name: "李小红",
        age: 30,
        birthday: "563472000000",
        address: "上海市浦东新区世纪大道",
      },
      {
        name: "周小伟",
        age: 26,
        birthday: "687024000000",
        address: "深圳市南山区深南大道",
      },
    ]
  },
}
</script>

在 slot-scope 的使用上(即 template 的内容),与方案一是完全一致的,可以看到,在 column 的定义上,仍然使用了 render 字段,只不过每个 render 都渲染了一个 div 节点,而这个 div 的内容,是指定来在 <table-render> 中定义的 slot-scope:

js
render: (h, { row, column, index }) => {
  return h(
    "div",
    this.$refs.table.$scopedSlots.name({
      row: row,
      column: column,
      index: index,
    })
  )
}

这正是 Render 函数灵活的一个体现,使用 $scopedSlots 可以访问 slot-scope,所以上面这段代码的意思是,name 这一列仍然是使用 Functional Render,只不过 Render 的是一个预先定义好的 slot-scope 模板。

有一点需要注意的是,示例中的 data 默认是空数组,而在 mounted 里才赋值的,是因为这样定义的 slot-scope,初始时读取 this.$refs.table.$scopedSlots 是读不到的,会报错,当没有数据时,也就不会去渲染,也就避免了报错。

这种方案虽然可行,但归根到底是一种 hack,不是非常推荐,之所以列出来,是为了对 Render 和 slot-scope 有进一步的认识。

4.方案三

第 3 中方案的思路和第 2 种是一样的,它介于方案 1 与方案 2 之间。这种方案要修改 Table 组件代码,但是用例与方案 1 完全一致。

在方案 2 中,我们是通过修改用例使用 slot-scope 的,也就是说 Table 组件本身没有支持 slot-scope,是我们“强加”上去的,如果把强加的部分,集成到 Table 内,那对使用者就很友好了,同时也避免了初始化报错,不得不把 data 写在 mounted 的问题。

保持方案 1 的用例不变,修改 src/components/table-render 中的代码。为了同时兼容 Render 与 slot-scope,我们在 table-render 下新建一个 slot.js 的文件:

js
// src/components/table-render/slot.js
export default {
  functional: true,
  inject: ["tableRoot"],
  props: {
    row: Object,
    column: Object,
    index: Number,
  },
  render: (h, ctx) => {
    return h(
      "div",
      ctx.injections.tableRoot.$scopedSlots[ctx.props.column.slot]({
        row: ctx.props.row,
        column: ctx.props.column,
        index: ctx.props.index,
      })
    )
  },
}

它仍然是一个 Functional Render,使用 inject 注入了父级组件 table.vue(下文改写) 中提供的实例 tableRoot。在 render 里,也是通过方案 2 中使用 $scopedSlots 定义的 slot,不过这是在组件级别定义,对用户来说是透明的,只要按方案 1 的用例来写就可以了。

table.vue 也要做一点修改:

vue
<!-- src/components/table-slot/table.vue 的改写,部分代码省略 -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns">{{ col.title }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(row, rowIndex) in data">
        <td v-for="col in columns">
          <template v-if="'render' in col">
            <Render
              :row="row"
              :column="col"
              :index="rowIndex"
              :render="col.render"
            ></Render>
          </template>
          <template v-else-if="'slot' in col">
            <slot-scope :row="row" :column="col" :index="rowIndex"></slot-scope>
          </template>
          <template v-else>{{ row[col.key] }}</template>
        </td>
      </tr>
    </tbody>
  </table>
</template>
<script>
import Render from "./render.js"
import SlotScope from "./slot.js"

export default {
  components: { Render, SlotScope },
  provide() {
    return {
      tableRoot: this,
    }
  },
  props: {
    columns: {
      type: Array,
      default() {
        return []
      },
    },
    data: {
      type: Array,
      default() {
        return []
      },
    },
  },
}
</script>

因为 slot-scope 模板是写在 table.vue 中的(对使用者来说,相当于写在组件 <table-slot></table-slot> 之间),所以在 table.vue 中使用 provide 向下提供了 Table 的实例,这样在 slot.js 中就可以通过 inject 访问到它,继而通过 $scopedSlots 获取到 slot。需要注意的是,在 Functional Render 是没有 this 上下文的,都是通过 h 的第二个参数临时上下文 ctx 来访问 prop、inject 等的。

方案 3 也是推荐使用的,当 Table 的功能足够复杂,层级会嵌套的比较深,那时方案 1 的 slot 就不会定义在第一级组件中,中间可能会隔许多组件,slot 就要一层层中转,相比在任何地方都能直接使用的 Render 就要麻烦了。所以,如果你的组件层级简单,推荐用第一种方案;如果你的组件已经成型(某 API 基于 Render 函数),但一时间不方便支持 slot-scope,而使用者又想用,那就选方案 2;如果你的组件已经成型(某 API 基于 Render 函数),但组件层级复杂,要按方案 1 那样支持 slot-scope 可能改动较大,还有可能带来新的 bug,那就用方案 3,它不会破坏原有的任何内容,但会额外支持 slot-scope 用法,关键是改动简单。

总结

通过对前端组件的分析,需要重点关注组件中易变性对组件封装的影响,它会对组件的可复用性、可扩展性产生很大影响