<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          怎樣設(shè)計一個可擴展、通用的、健壯性組件?

          共 20779字,需瀏覽 42分鐘

           ·

          2021-09-13 16:45

          點擊上方關(guān)注 前端桃園一起學(xué)習(xí),天天進步



          前言

          組件是頁面不可或缺的部分,而設(shè)計組件就成為了前端同學(xué)每日工作。

          所以

          一位程序員的職業(yè)生涯大約十年,只有人壽命的十分之一。前端項目只是你生活工作的一部分,而你卻是它的全部,你是他的靈魂。請放下長時間的游戲、工作時的摸魚。多學(xué)習(xí)來以最完美的狀態(tài)好好陪你項目!

          正文

          這篇文章將會以本人所認知的角度去對組件的封裝設(shè)計展開思考。如果你對我的觀點,方式,又或者你有更好的方式,更優(yōu)的設(shè)計模式,不妨在評論區(qū)一起討論 思考, 交流是進步的必經(jīng)之路。

          知識點

          • 組件是如何分類的
          • Vue 和 React 封裝組件模式
          • 怎樣才是一個好的可擴展、通用的、健壯性組件
          • 思考討論,提出問題

          組件是如何分類的

          • 業(yè)務(wù)組件
          • 通用組件(非業(yè)務(wù)組件)
            • UI組件
          1627627583874_8398E85B-D83D-430B-AF41-D4D3F8CF04C0.png

          無論是 業(yè)務(wù)組件 或者 通用組件都具備組件本質(zhì)所包含的三個性質(zhì)擴展通用健壯

          • 擴展性:在原有組件基礎(chǔ)上可 二次封裝 擴展成新的組件符合設(shè)計的開閉原則

          • 通用性:根據(jù)組件接受的參數(shù)組件中與業(yè)務(wù)的解耦比來衡量組件的通用性,并不是通用性占比100%的組件就是最好的組件,需要根據(jù) 不同的場景 分析

          • 健壯性:避免組件中參數(shù)處理函數(shù)執(zhí)行過程可能出現(xiàn)的奔潰和錯誤導(dǎo)致程序的直接掛斷,單測以對組件內(nèi)部 做好邊界處理,異常錯誤的捕獲來衡量這一標準

          業(yè)務(wù)組件

          服務(wù)與業(yè)務(wù)的組件稱為業(yè)務(wù)組件,項目中組件的劃分是分頁面級組件全局級別組件

              --- componentes
              --- pages
          復(fù)制代碼

          而結(jié)構(gòu)一般是這樣

          componentes 中存放的組件往往 具有當(dāng)前項目 中的多個 場景 復(fù)用 才會進行設(shè)計與封裝

          Vue中的組件

          <template>
              ....
          </template>
          <script>
          export default {
            props: {
              ...
            },
            data () {
              ....
            },
            methods: {
              ....
            }
          }
          </
          script>
          復(fù)制代碼

          React中的組件

          import React, { Component } from 'react';
          export default class Demo extends Component {
              state = {
              };

              componentDidMount() {
                ...
              }

              render() {
                const { .... } = this.props;
                return (
                  <div>
                    ....
                  </div>

                );
              }
          }
          復(fù)制代碼

          這是目前兩個 兩個框架最基本的組件封裝 模板

          而你在封裝組件的時候是否考慮過一些問題

          • 組件的可維護性?
          • 組件的可讀性?
          • 擴展性、健壯性、通用性?
          • 這個組件是否需要封裝抽離?
          • 組件是否和業(yè)務(wù)強關(guān)聯(lián)?

          這些問題在組件封裝開始編碼之前你是否都考慮過了

          凡是組件不斷擴展,使其通用性提升,必然就會降低組件的 易用性質(zhì)

          而不斷豐富一個組件,也會導(dǎo)致其組件代碼過長,組件使命不單一,不易讀不易維護

          像Vue 和 React 推薦 一個組件代碼長度在 200 - 500 行最佳

          業(yè)務(wù)中的組件往往區(qū)分

          1627627666905_1E9DC37D-8E0E-45C7-814A-63CA34D3936C.png
          • 容器組件負責(zé)處理業(yè)務(wù)相關(guān)邏輯,注冊業(yè)務(wù)相關(guān)鉤子,傳入相應(yīng)的熟悉和插槽等
          • 視圖組件則負責(zé)數(shù)據(jù)的呈現(xiàn),交互的實現(xiàn)
          1627634474901_9F20476A-9B8A-4360-A907-C79218F72E55.png

          容器組件往往不可復(fù)用

          視圖組件則根據(jù)組件的樣式 和 交互 判斷組件在項目中的 頻率 來抉擇是否封裝

          視圖 和 數(shù)據(jù) 解耦 又能搭配 可以很好的提升組件的 可讀,易維護性

          這個組件是否需要封裝抽離?

          這可能是新前端同學(xué)容易遇到的問題

          不是所以 DOM 結(jié)構(gòu) 都需要 抽離

          你需要對你所負責(zé)的項目 UI走向 有著全局的洞察力,如果不確認的是否需要封裝,建議不封裝

          下次業(yè)務(wù)中存在與原來視圖 UI 相同的需求 再進行封裝設(shè)計,而不是快速 Copy

          組件是否和業(yè)務(wù)強關(guān)聯(lián)?

          通常情況,組件中的大量數(shù)據(jù)來源 當(dāng)前組件的接口請求。沒有依賴或者幾乎不依賴外部傳入的props等,稱為業(yè)務(wù)強關(guān)聯(lián)組件,放棄組件封裝的想法。

          怎樣才是一個好的可擴展、通用的、健壯性組件?

          我們可以參考一下star高的 Ant design 和 Element 來學(xué)習(xí)

          Ant design 中 rc-switch

          import * as React from 'react';
          import classNames from 'classnames';
          import useMergedState from 'rc-util/lib/hooks/useMergedState';
          import KeyCode from 'rc-util/lib/KeyCode';

          const Switch = React.forwardRef(
            (
              {
                prefixCls = 'rc-switch',
                className,
                checked,
                defaultChecked,
                disabled,
                loadingIcon,
                checkedChildren,
                unCheckedChildren,
                onClick,
                onChange,
                onKeyDown,
                ...restProps
              },
              ref,
            ) => {
              const [innerChecked, setInnerChecked] = useMergedState<boolean>(false, {
                value: checked,
                defaultValue: defaultChecked,
              });

              function triggerChange(
                newChecked: boolean,
                event: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>,
              
          {
                let mergedChecked = innerChecked;

                if (!disabled) {
                  mergedChecked = newChecked;
                  setInnerChecked(mergedChecked);
                  onChange?.(mergedChecked, event);
                }

                return mergedChecked;
              }

              function onInternalKeyDown(e{
                if (e.which === KeyCode.LEFT) {
                  triggerChange(false, e);
                } else if (e.which === KeyCode.RIGHT) {
                  triggerChange(true, e);
                }
                onKeyDown?.(e);
              }

              function onInternalClick(e{
                const ret = triggerChange(!innerChecked, e);
                // [Legacy] trigger onClick with value
                onClick?.(ret, e);
              }

              const switchClassName = classNames(prefixCls, className, {
                [`${prefixCls}-checked`]: innerChecked,
                [`${prefixCls}-disabled`]: disabled,
              });

              return (
                <button
                  {...restProps}
                  type="button"
                  role="switch"
                  aria-checked={innerChecked}
                  disabled={disabled}
                  className={switchClassName}
                  ref={ref}
                  onKeyDown={onInternalKeyDown}
                  onClick={onInternalClick}
                >

                  {loadingIcon}
                  <span className={`${prefixCls}-inner`}>
                    {innerChecked ? checkedChildren : unCheckedChildren}
                  </span>
                </button>

              );
            },
          );

          Switch.displayName = 'Switch';

          export default Switch;
          復(fù)制代碼
          • 直接脫離 UI
          • 接受參數(shù),處理鉤子

          而 Ant design 則是對API 和 UI 的二次封裝

          進而體現(xiàn)了 React Components[1] 的組件的 可擴展性

          再看看

          Element UI 的 Switch

          <template>
            <div
              class="el-switch"
              :class="{ 'is-disabled': switchDisabled, 'is-checked': checked }"
              role="switch"
              :aria-checked="checked"
              :aria-disabled="switchDisabled"
              @click.prevent="switchValue"
            >

              <input
                class="el-switch__input"
                type="checkbox"
                @change="handleChange"
                ref="input"
                :id="id"
                :name="name"
                :true-value="activeValue"
                :false-value="inactiveValue"
                :disabled="switchDisabled"
                @keydown.enter="switchValue"
              >

              <span
                :class="['el-switch__label', 'el-switch__label--left', !checked ? 'is-active' : '']"
                v-if="inactiveIconClass || inactiveText">

                <i :class="[inactiveIconClass]" v-if="inactiveIconClass"></i>
                <span v-if="!inactiveIconClass && inactiveText" :aria-hidden="checked">{{ inactiveText }}</span>
              </span>
              <span class="el-switch__core" ref="core" :style="{ 'width': coreWidth + 'px' }">
              </span>
              <span
                :class="['el-switch__label', 'el-switch__label--right', checked ? 'is-active' : '']"
                v-if="activeIconClass || activeText">

                <i :class="[activeIconClass]" v-if="activeIconClass"></i>
                <span v-if="!activeIconClass && activeText" :aria-hidden="!checked">{{ activeText }}</span>
              </span>
            </div>
          </template>

          <script>
            import emitter from 'element-ui/src/mixins/emitter';
            import Focus from 'element-ui/src/mixins/focus';
            import Migrating from 'element-ui/src/mixins/migrating';
            export default {
              name'ElSwitch',
              mixins: [Focus('input'), Migrating, emitter],
              inject: {
                elForm: {
                  default''
                }
              },
              props: {
                value: {
                  type: [BooleanStringNumber],
                  defaultfalse
                },
                disabled: {
                  typeBoolean,
                  defaultfalse
                },
                width: {
                  typeNumber,
                  default40
                },
                activeIconClass: {
                  typeString,
                  default''
                },
                inactiveIconClass: {
                  typeString,
                  default''
                },
                activeTextString,
                inactiveTextString,
                activeColor: {
                  typeString,
                  default''
                },
                inactiveColor: {
                  typeString,
                  default''
                },
                activeValue: {
                  type: [BooleanStringNumber],
                  defaulttrue
                },
                inactiveValue: {
                  type: [BooleanStringNumber],
                  defaultfalse
                },
                name: {
                  typeString,
                  default''
                },
                validateEvent: {
                  typeBoolean,
                  defaulttrue
                },
                idString
              },
              data() {
                return {
                  coreWidththis.width
                };
              },
              created() {
                if (!~[this.activeValue, this.inactiveValue].indexOf(this.value)) {
                  this.$emit('input'this.inactiveValue);
                }
              },
              computed: {
                checked() {
                  return this.value === this.activeValue;
                },
                switchDisabled() {
                  return this.disabled || (this.elForm || {}).disabled;
                }
              },
              watch: {
                checked() {
                  this.$refs.input.checked = this.checked;
                  if (this.activeColor || this.inactiveColor) {
                    this.setBackgroundColor();
                  }
                  if (this.validateEvent) {
                    this.dispatch('ElFormItem''el.form.change', [this.value]);
                  }
                }
              },
              methods: {
                handleChange(event) {
                  const val = this.checked ? this.inactiveValue : this.activeValue;
                  this.$emit('input', val);
                  this.$emit('change', val);
                  this.$nextTick(() => {
                    // set input's checked property
                    // in case parent refuses to change component's value
                    this.$refs.input.checked = this.checked;
                  });
                },
                setBackgroundColor() {
                  let newColor = this.checked ? this.activeColor : this.inactiveColor;
                  this.$refs.core.style.borderColor = newColor;
                  this.$refs.core.style.backgroundColor = newColor;
                },
                switchValue() {
                  !this.switchDisabled && this.handleChange();
                },
                getMigratingConfig() {
                  return {
                    props: {
                      'on-color''on-color is renamed to active-color.',
                      'off-color''off-color is renamed to inactive-color.',
                      'on-text''on-text is renamed to active-text.',
                      'off-text''off-text is renamed to inactive-text.',
                      'on-value''on-value is renamed to active-value.',
                      'off-value''off-value is renamed to inactive-value.',
                      'on-icon-class''on-icon-class is renamed to active-icon-class.',
                      'off-icon-class''off-icon-class is renamed to inactive-icon-class.'
                    }
                  };
                }
              },
              mounted() {
                /* istanbul ignore if */
                this.coreWidth = this.width || 40;
                if (this.activeColor || this.inactiveColor) {
                  this.setBackgroundColor();
                }
                this.$refs.input.checked = this.checked;
              }
            };
          </script>

          復(fù)制代碼

          很直觀的看出, 除了語法 方面 封裝設(shè)計組件UI的最佳方式

          • 零業(yè)務(wù)代碼
          • 優(yōu)秀的UIAPI設(shè)計
          • 易學(xué)易用

          我們再看看另外一種封裝組件的方式

          1627634757928_22274B24-4A7F-4B1B-8307-3A565B77A956.png

          React For Menu

          carbon (1).png

          這是 React 配套組件的封裝 的一種思路

          • 創(chuàng)建 context 管理 組件組 的數(shù)據(jù)流
          • 父組件中存在判斷 子組件的類型 增加健壯性
          • 在 index 掛載 分別導(dǎo)出組件

          Vue For Menu

          <template>
              <div
              class="menu"
              // 事件綁定
              >

                // menuItem
                <slot></slot>
              </div>

          </template>

          <script>
          export default {
              mixins: [...],
              name: 'Menu',
              componentName: 'Menu',
              inject: {
                menu: {
                  default: ''
                },
              },
              provide() {
                return {
                  'menu': this
                };
            }
          }
          </
          script>
          復(fù)制代碼

          在 Vue \- UI 組件的設(shè)計封裝中 , 經(jīng)常使用 provide,inject來組件通信.

          Vue 除了使用 slot 還可以使用 jsx & function component 來實現(xiàn)如此效果,其設(shè)計思想和 React 大同小異

          在 Vue3 中 Ant design for Vue 中大量使用 jsx 來 封裝 組件

          下面簡單總結(jié)一下

          • 組件中的 UI 和 數(shù)據(jù) 業(yè)務(wù)盡量 分離
          • UI視圖 組件中 不該包含 業(yè)務(wù)代碼
          • 組件設(shè)計之初考慮通用、易用、擴展、健壯穩(wěn)定 以及 良好的代碼結(jié)構(gòu)、Api設(shè)計使用

          思考討論,提出問題

          • 你有不同的或者更好的設(shè)計封裝組件的技巧和 Demo 嗎
          • 你是如何判斷組件是否封裝的?如何設(shè)計組件的?
          • 回想一下你設(shè)計的組件 代碼Api命名 是否給其他同學(xué)帶來不便
          • 等等.....

          根據(jù)以上的問題思考 或者 你有不同的想法 不妨在評論區(qū)中我們一起探討,學(xué)習(xí)!



          關(guān)于本文

          來源:遇見同學(xué)

          https://juejin.cn/post/6991261103141421092

          點個『在看』支持下 

          瀏覽 33
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青娱乐 超碰 | 是先锋男人的网站 | 欧美中文一区 | 天天草天天爽 | 亚洲欧美非洲黄色毛片 |