1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 20779字,需瀏覽 42分鐘

         ·

        2021-09-13 16:45

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



        前言

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

        所以

        一位程序員的職業(yè)生涯大約十年,只有人壽命的十分之一。前端項目只是你生活工作的一部分,而你卻是它的全部,你是他的靈魂。請放下長時間的游戲、工作時的摸魚。多學習來以最完美的狀態(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)的奔潰和錯誤導致程序的直接掛斷,單測以對組件內(nèi)部 做好邊界處理,異常錯誤的捕獲來衡量這一標準

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

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

            --- componentes
            --- pages
        復制代碼

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

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

        Vue中的組件

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

        React中的組件

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

            componentDidMount() {
              ...
            }

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

              );
            }
        }
        復制代碼

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

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

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

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

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

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

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

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

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

        容器組件往往不可復用

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

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

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

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

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

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

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

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

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

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

        我們可以參考一下star高的 Ant design 和 Element 來學習

        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;
        復制代碼
        • 直接脫離 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>

        復制代碼

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

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

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

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

        React For Menu

        carbon (1).png

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

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

        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>
        復制代碼

        在 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、命名 是否給其他同學帶來不便
        • 等等.....

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



        關(guān)于本文

        來源:遇見同學

        https://juejin.cn/post/6991261103141421092

        點個『在看』支持下 

        瀏覽 33
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            大尺度人体私拍写真 | avav一区二区 | 在线香蕉 | 爱爱免费视频网址 | 在线免费亚洲视频 | 116美女写真午夜一级 | 岳把我用嘴含进满足我广告 | 伊人久久五月天 | 中文在线字幕 | 男女操逼片 |