天天看點

vue2 元件庫開發記錄-開發技巧前言installprops 屬性provide 和 inject 屬性$children 和 $parentEventBus 事件總線$attrs$scopedSlots 作用域插槽Vue.extends 實作 js 調用元件指令mixin總結

vue2 元件庫開發記錄-開發技巧

  • 前言
  • install
  • props 屬性
  • provide 和 inject 屬性
  • $children 和 $parent
  • EventBus 事件總線
  • $attrs
  • $scopedSlots 作用域插槽
  • Vue.extends 實作 js 調用元件
  • 指令
  • mixin
  • 總結

前言

本文主要是記錄我在開發元件庫時的一些開發技巧。并且會講解一些比較特殊的元件。

install

我們在使用

element-ui

的時候,可以通過

Vue.use(Button)

注冊元件。

use

函數内部實際上就是調用了傳入的對象的

install

函數,同時

install

函數會接受到一個

vue

參數。

import Alert from "./src/alert.vue";

Alert.install = Vue => Vue.component(Alert.name, Alert);

export default Alert;

           

props 屬性

props 是用來做父子元件之間的通信,并且 props 的寫法有很多種,相信做 vue 開發的同學應該都知道的。是以我這裡主要說 2 點

  • 當預設值是一個數組或者是對象時,必須從一個工廠函數中傳回,否則所有元件執行個體都會共用一個值
export default {
  props: {
    obj: {
      type: Object,
      // 簡寫的時候需要注意傳回的{}外層需要套一個(),否則就是一個空函數,沒有傳回值
      default: () => ({})
    }
  }
};
           
  • 自定義校驗。這個在項目開發中可能會很少會用到。但是在元件開發中卻經常用到。比如一個

    Button

    元件的

    type

    屬性是

    String

    類型,但是隻接受

    default

    ,

    primary

    ,

    success

    ,開發者可能傳入的字元串不符合我們的預期,是以需要用到自定義校驗,給開發者一個适當的提示。
export default {
  props: {
    type: {
      type: String,
      default: 'default',
      validator: function (value) {
        // 這個值必須比對下列字元串中的一個
        return ['default', 'primary', 'success'].indexOf(value) !== -1;
      }
    }
  }
};
           

注意:元件的 this,在

default

validator

字段中是不可用的。

provide 和 inject 屬性

這兩個屬性我相信很多同學都是沒有使用過的,甚至有的人可能都沒聽過。這兩個屬性可以用來做跨元件層級通訊,實作父子或者祖孫元件之間的通信。可能會有人說,我使用

props

屬性将資料一層一層的傳遞下去不也可以實作

provide

inject

的效果嗎,這個說的也沒錯,但是有些場景卻實作不了。我們看一下

checkbox

元件的使用方式

<checkbox-group v-model="value">
  <checkbox label="抽煙"></checkbox>
  <checkbox label="喝酒"></checkbox>
  <checkbox label="探頭"></checkbox>
</checkbox-group>
           

上面可以看見

checkbox-group

元件無法通過

props

屬性給

checkbox

元件傳遞資料,因為

checkbox

元件并不是直接寫在

checkbox-group

元件内部的,而是通過

slot

插槽放置到

checkbox-group

元件中的,進而實作了父子關系的元件。此時,

provide

inject

屬性就可以解決這種組合元件的寫法之間的通訊問題。

provide

inject

的特點如下:

  • provide

    可以是一個對象或者是傳回一個對象的函數(推薦使用這種寫法)。
  • inject

    可以是一個數組或者是一個對象(推薦這種寫法)。
  • 如果

    provide

    傳入的是一個響應式的對象(元件開發中一般直接傳入

    this

    ),那麼

    inject

    接收的值也是個響應式資料

代碼示例:

// checkbox-group元件
export default {
  props: {
    // 綁定值
    value: {
      type: Array
    },
  },
  provide() {
    return {
      // 直接把元件執行個體傳遞給所有子孫元件
      CheckboxGroup: this
    };
  }
};

// checkbox元件
export default {
  inject: {
    // 接收checkbox-group元件通過provide傳遞過來的資料
    CheckboxGroup: {
      default: ""
    }
  },
  mounted(){
    // checkbox-group元件的value值
    console.log(this.CheckboxGroup?.value)
  }
};
           

provide

inject

的使用場景有兩個,分别如下:

  • 具有組合關系的元件。比方說上面的

    checkbox-group

    元件和

    checkbox

    元件,他們就是組合關系。

    checkbox

    元件可以單個進行使用,也可以和

    checkbox-group

    元件在一起組合使用。

    checkbox

    元件通過判斷

    this.CheckboxGroup

    是否存在來判斷開發者是單個使用還是結合

    checkbox-group

    元件一起使用,進而實作不同的邏輯。
  • 跨層級元件傳遞資料。當你的元件層級很深的時候,比如

    A->B->C->D

    。如果

    A

    想要跟

    D

    進行通訊,就必須通過

    B

    C

    props

    屬性一層一層的傳遞下去,這樣會造成資料的混亂的,而且如果傳遞的資料非常多,寫起來也很麻煩。是以這個時候可以使用

    provide

    inject

    來進行通信,資料的流向就不用經過

    B

    C

    ,你隻需要專注于

    A

    D

    之間的資料處理即可。

$children 和 $parent

$children

是用來擷取目前元件的所有子元件,

$parent

是用來擷取目前元件的父元件。元件的子元件可能會有多個,但是父元件隻能有一個(根元件沒有父元件)。是以我們可以通過遞歸的方式擷取該元件的子孫元件和父級元件,并實作廣播和派發的功能,實作具有上下級元件關系的通訊。

注意:一般查找父級元件或者子孫元件都是通過元件的

name

字段進行查找的,是以每個元件内部最好有一個

name

字段,這樣才能有效過濾出你想要查找的元件。

代碼示例:

// 查找所有子孫元件
function findChildren(context, componentName) {
  return context.$children.reduce((components, child) => {
    if (child.$options.name === componentName) {
      components.push(child);
    }
    const children = findChildren(child, componentName);
    return components.concat(children);
  }, []);
}

// 查找所有父級元件
function findParents(context, componentName) {
  const parents = [];
  const parent = context.$parent;
  if (parent) {
    if (parent.$options.name === componentName) {
      parents.push(parent);
    }
    return parents.concat(findParents(parent, componentName));
  } else {
    return [];
  }
}
           

派發與廣播

// 向下通知
function broadcast(options) {
  const { eventName, params, componentName } = options;
  // 擷取目前元件下的所有的孩子
  const broad = (children) => {
    children.forEach((child) => {
      if (componentName) {
        if (child.$options.name === componentName) {
          child.$emit(eventName, params);
        }
      } else {
        child.$emit(eventName, params);
      }

      if (child.$children) {
        broad(child.$children);
      }
    });
  };
  broad(this.$children);
}

// 向上通知
function dispatch(options) {
  const { eventName, params, componentName } = options;

  let parent = this.$parent || this.$root;
  let name = parent.$options.name;

  while (parent) {
    if (componentName) {
      if (name === componentName) {
        parent.$emit(eventName, params);
      }
    } else {
      parent.$emit(eventName, params);
    }
    parent = parent.$parent;
    name = parent?.$options.name;
  }
}
           

廣播通信例子

const parent = {
  name:'parent'
  template:`
  <div>
    <div @click='onBroadcast'>broadcast</div>
    <child/>
  </div>
  `,
  methods:{
    onBroadcast(){
      broadcast.call(this,{
        name:'custom',
        params:'hello world',
        componentName:'child'
      })
    }
  }
}

const child = {
  name:'child',
  created(){
    this.$on('custom',event=>{
      console.log(event) // hello world
    })
  }
}
           

在上面例子中,child 元件需要在 parent 元件進行廣播前使用

$on

注冊事件,否則是接收不到 parent 元件的廣播的

廣播與派發的應用場景可用于

Form

FormItem

元件的表單校驗:

  • input

    checkbox

    等表單元件值發生變化的時候,通過

    dispatch

    向上通知

    FormItem

    元件進行校驗
  • Form

    元件需要校驗整個表單的時候,通過

    findChildren

    查找到所有

    FormItem

    元件,調用

    FormItem

    元件内部的方法進行校驗,獲得校驗結果,進而回報給使用者

EventBus 事件總線

EventBus 可實作任意元件之間的通信,借助 EventBus 的

$emit

派發事件,

$on

監聽事件就可以實作任意元件之間的通信。 EventBus 實際上是通過釋出/訂閱方法來實作的,通過導出一個

new Vue()

執行個體(單例),所有元件都是用該執行個體進行事件的派發和監聽。

代碼示例:

// event-bus.js
import Vue from 'vue';
const EventBus = new Vue();

// 發送事件
EventBus.$emit('custom', { age: 1 });
// 接收事件
EventBus.$on('custom', (event) => {});
// 監聽一次事件
EventBus.$once('custom', (event) => {});
// 移除事件
EventBus.$off('custom');
           

EventBus 在元件庫開發中使用的場景比較少,但也是一種跨元件的通信方式,對比于

props

provide 和 inject

$children 和 $parent

這些通信方式(隻能在具有上下級關系的元件中進行通信,不能再兄弟元件之間進行通信),優點在于可以在任意元件之間進行通信,缺點就是一旦事件多了,就變得很難管理。

$attrs

$attrs

包含哪些沒有在元件的

props

字段中聲明的屬性(class 和 style 除外)。

例子

const A = {
  props:['name']
}

<A name='張三' age='17' sex='男' />
           

從上面的例子中,我們可以看見

props

屬性中隻聲明了

name

字段,是以

age

sex

字段包含在了

$attrs

中,

name

并不在

$attrs

常用于那些有許多原生屬性的元件中,比如

input

元件,原生的

input

标簽包含了很多字段,如果我們将

input

标簽的所有原生屬性都在

props

中都聲明一邊,那就會非常麻煩。我們一般會将一些非原生屬性或者需要在元件内部使用到的原生屬性聲明在

props

中,其餘的字段通過

v-bind="$attrs"

挂在到

input

标簽上面

代碼示例:

const LinInput = {
  template:'<input :disabled="disabled" :value="value" v-bind="$attrs" />',
  props:['disabled','value']
}


<lin-input disabled  value='1' name='age' placeholder='請輸入'  />
           

$scopedSlots 作用域插槽

$scopedSlots

作用域插槽。這個東西我相信很多同學都沒接觸過。我也是在開發

Table

元件時第一次使用到它。不得不說這個東西真的很強大很巧妙。文字說明可能不夠透徹,是以下面我們通過

Table

元件的代碼來講解。

首先看一下使用方式:

<template>
  <lin-table value-key="id" :dataSource="tableData">
    <lin-table-column prop="date" label="日期">
      <template slot-scope="scope">
        <div>{{ scope.row.date }}</div>
      </template>
    </lin-table-column>
    <lin-table-column prop="name" label="姓名"></lin-table-column>
    <lin-table-column prop="address" label="位址"></lin-table-column>
  </lin-table>
</template>

export default {
  data() {
    return {
      tableData: [
        {
          id: 1,
          date: "2016-05-02",
          name: "王小虎",
          address: "上海市普陀區金沙江路 1518 弄",
        }
      ],
    };
  },
};
           

lin-table-column

元件隻負責收集資料,并不會渲染任何東西。比如

prop

label

這些資料,然後将這些資料存放在

table

元件中。然後通過

table

元件來渲染這些東西。

lin-table-column.jsx

let columnId = 0;
export default {
  name: 'LinTableColumn',
  props: {
    prop: String,
    label: String
  },
  inject: {
    // table元件的執行個體
    table: {
      default: null
    }
  },
  watch: {
    prop(val) {
      this.column.prop = val;
    },
    label(val) {
      this.column.label = val;
    }
  },
  created() {
    // 把該元件的props屬性都存儲起來
    const column = {
      ...this.$props,
      id: `col-${columnId++}`
    };
    // 預設提供一個渲染單元格的render函數,核心内容
    // h是渲染函數,rowData是每一行的資料
    column.renderCell = (h, rowData) => {
      let render = (h, data) => {
        return data.row[column.prop];
      };
      // 判斷是不是使用了插槽
      if (this.$scopedSlots.default) {
        // 通過this.$scopedSlots.default擷取預設插槽的VNode,也就是這個東西
        // <template slot-scope="scope">
        //   <div>{{ scope.row.date }}</div>
        // </template>
        render = (h, data) => this.$scopedSlots.default(data);
      }
      return render(h, rowData);
    };
    this.column = column;
  },
  mounted() {
    if (this.table) {
      // 把該元件收集到的資料存儲在table元件中。
      this.table.columns.push(this.column);
    }
  },
  destroyed() {
    if (this.table) {
      // 銷毀的時候需要把對應的列移除掉
      const index = this.table.columns.findIndex(
        (column) => column.id === this.column.id
      );
      if (index > -1) {
        this.table.columns.splice(index, 1);
      }
    }
  },
  render() {
    // 不做實際的渲染
    return null;
  }
};
           

table.vue

<template>
  <div>
    <div class="lin-table-slot">
      <!-- lin-table-column元件 -->
      <slot></slot>
    </div>
    <table class="lin-table">
      <!-- 渲染頭部相關的東西 -->
      <lin-table-header ref="linTableHeaderComp"></lin-table-header>

      <!-- 渲染表格内容 -->
      <lin-table-body ref="linTableBodyComp"></lin-table-body>
    </table>
  </div>
</template>

<script>
import LinTableHeader from './TableHeader.jsx';
import LinTableBody from './TableBody.jsx';
export default {
  name: 'LinTable',
  components: {
    LinTableHeader,
    LinTableBody
  },
  props: {
    // 資料源
    dataSource: {
      type: Array,
      default: () => [],
      require: true
    },
    // 每一行資料的唯一辨別key
    valueKey: {
      type: String,
      require: true
    }
  },
  provide() {
    return {
      // 往子元件中注入table執行個體,以便子元件可以通路到table元件的資料
      table: this
    };
  },
  data() {
    return {
      // 存儲lin-table-column元件收集到的資訊
      columns: []
    };
  }
};
</script>
           

TableHeader 元件的内容還是很簡單的,就是根據使用

v-for

columns

字段的 label 字段渲染出來。是以這裡不講解 TableHeader 元件,直接講解

TableBody

元件

TableBody.jsx

export default {
  name: 'LinTableBody',
  computed: {
    // 資料源
    dataSource() {
      if (this.table) {
        return this.table.dataSource;
      }
      return [];
    },
    // 列數組
    columns() {
      if (this.table) {
        return this.table.columns;
      }
      return [];
    },
    // 每一行資料的唯一辨別 key
    valueKey() {
      if (this.table) {
        return this.table.valueKey;
      }
      return '';
    }
  },
  inject: {
    table: {
      default: null
    }
  },
  render(h) {
    const { dataSource, columns, valueKey } = this;
    return (
      <tbody class="lin-table-tbody">
        {dataSource.map((row, rowIndex) => {
          const rowKey = row[valueKey] || rowIndex;
          return (
            <tr key={rowKey}>
              {columns.map((column, idx) => (
                <td key={`${rowKey}-${idx}`}>
                  // lin-table-column中的renderCell渲染函數
                  {column.renderCell(h, {
                    row,
                    column,
                    rowIndex
                  })}
                </td>
              ))}
            </tr>
          );
        })}
      </tbody>
    );
  }
};
           

從上面可以看出

TableBody

元件的核心就是調用

renderCell

函數,而

renderCell

這個函數就是在

lin-table-column

元件收集到的每個單元格的渲染函數。

Vue.extends 實作 js 調用元件

element-ui

message

message-box

等元件都通過 js 的方式進行調用。在實際項目開發中有時候也需要根據需求實作一個 js 調用的元件,比如,我點選一個按鈕,使用者沒有權限的時候需要彈框顯示暫無權限,遊客則需要彈出登入框。是以,下面以

message

元件為例,講解一下怎麼通過 Vue.extends 實作 js 調用元件。

首先建立一個

message.vue

,并實作你需要的功能

<template>
  <transition name="message" @after-leave="afterLeave">
    <div :class="`lin-message-${type}`" v-show="show">
      <p class="lin-message-content">{{ message }}</p>
    </div>
  </transition>
</template>
<script>
export default {
  name: 'LinMessage',
  props: {
    // 類型主題
    type: {
      type: String,
      default: 'info'
    },
    // 消息文字
    message: {
      type: String
    }
  },
  data() {
    return {
      // 控制是否顯示
      show: false
    };
  },
  methods: {
    // vue動畫結束後回調函數
    afterLeave() {
      this.$emit('closed');
    }
  }
};
</script>
           

使用 Vue.extends 繼承一個 vue 元件

import Vue from 'vue';
import Message from './message.vue';
// 建立一個子類構造器
const MessageConstruct = Vue.extend(Message);

class LinMessage {
  // 參數
  options = null;

  // message執行個體對象
  instance = null;

  // message元件參數
  propsData = {};

  // 自動關閉定時器
  timer = null;

  constructor(options) {
    this.options = options || {};
    this.initProps(options);
    this.init();
  }

  // 初始化message元件參數
  initProps() {
    const props = ['type', 'message'];
    const propsData = {};
    props.forEach((prop) => {
      if (prop in this.options) {
        propsData[prop] = this.options[prop];
      }
    });
    this.propsData = propsData;
  }

  // 初始化
  init() {
    // 建立一個vue執行個體,實際上跟new Vue()差不多
    this.instance = new MessageConstruct({
      // propsData會跟元件中的props進行合并
      propsData: {
        ...this.propsData
      }
      // 這個選項會直接覆寫掉元件的props,是以一般這個是不會使用的
      // props:{}
      // 這個選項會跟元件中的data進行合并,寫法上可以是一個對象,也可以是傳回一個對象的函數
      // data:{}
    });
    // 渲染
    this.instance.$mount();
    // 将渲染出來的dom挂在到body上面
    document.body.appendChild(this.instance.$el);
    // 顯示出來,this.instance相當于在元件中的this
    this.instance.show = true;
    // 設定定時器,用于定時關掉message元件
    this.setTimer();
    // 監聽事件
    this.instance.$once('closed', () => {
      // 銷毀元件
      this.destory();
    });
  }
  setTimer() {
    const { duration } = this.options;
    // 等于0不會自動消失
    if (duration !== 0) {
      this.timer = setTimeout(() => {
        this.close();
      }, duration || 3000);
    }
  }
  // 隐藏message元件
  close() {
    if (this.instance && this.instance.show) {
      this.instance.show = false;
    }
  }
  // 銷毀message元件
  destory() {
    if (this.instance) {
      document.body.removeChild(this.instance.$el);
      this.instance.$destroy();
    }
    if (this.timer) {
      clearTimeout(this.timer);
    }
  }
}

// 建立執行個體,options可傳入字元串或者一個對象
function createInstance(options) {
  const toString = Object.prototype.toString;
  if (toString.call(options).includes('Object')) {
    return new LinMessage(options);
  }
  return new LinMessage({
    message: options.toString()
  });
}

// 建立不同類型type的message元件
function createInstanceByType(options, type) {
  const toString = Object.prototype.toString;
  if (toString.call(options).includes('Object')) {
    return new LinMessage({
      ...options,
      type
    });
  }
  return new LinMessage({
    message: options.toString(),
    type
  });
}
createInstance.success = function success(options) {
  return createInstanceByType(options, 'success');
};
createInstance.info = function info(options) {
  return createInstanceByType(options, 'info');
};

export default createInstance;
           

指令

指令在元件庫或者在實際項目開發中使用到的場景比較少。雖然比較少,但是還是有必要講一下。比如現在有個需求需要根據使用者的權限去顯示或者隐藏某個按鈕,你可能會在頁面上先判斷使用者是否有權限,然後再通過

v-show

去顯示或者隐藏。這樣做是可以的,但是如果頁面上需要控制的按鈕比較多,那樣就會顯得很麻煩了。這個時候我們可以使用指令。用法如下:

當使用者擁有

p1

p2

權限的時候,就會顯示按鈕。

指令中也提供了 5 個鈎子函數,我們可以再不同的鈎子函數中處理不同的事情:

  • bind:隻調用一次。在這裡可進行初始化
  • inserted:元素插入到父節點
  • update:指令所在的元件發生更新調用
  • componentUpdated:指令所在的元件和

    其子元件

    全部更新完成後調用
  • unbind:調用一次。指令更元素解綁,在這裡可進行一些銷毀工作

常用到的鈎子函數有三個,分别是:

bind

update

unbind

,其餘的我基本上沒用過。

指令的結構如下:

Vue.directive('permission', {
  bind(el, binding, vnode) {
    // el是指令綁定的元素,你可以在el上面綁定一些資訊,用于在其他鈎子函數中使用,比如 el.message='你好'
    // binding是包含了指令的相關資訊,比如 v-permission:foo="['p1','p2]" ,foo和['p1','p2]都可以在binding中拿到,詳情可以列印出來看一下
    // vnode這個比較有意思了。他可以拿到指令所在的元件的上下文執行個體,你可以拿着這個上下文執行個體去通路元件中的方法和屬性
  },
  update(el, binding, vnode) {},
  unbind(el, binding, vnode) {}
});
           

v-permission

實作

Vue.directive('permission', function (el, binding, vnode) {
  const permissionList = vnode.context.$store.state.permissionList;
  const value = binding.value;
  if (!permissionList.includes(value)) {
    el.style.display = 'none';
  }
});
           

上面的寫法等同于

Vue.directive('permission', {
  bind(el, binding, vnode) {
    const permissionList = vnode.context.$store.state.permissionList;
    const value = binding.value;
    if (!permissionList.includes(value)) {
      el.style.display = 'none';
    }
  },
  update(el, binding, vnode) {
    const permissionList = vnode.context.$store.state.permissionList;
    const value = binding.value;
    if (!permissionList.includes(value)) {
      el.style.display = 'none';
    }
  }
});
           

mixin

通常我們會将一些具有相同邏輯功能的東西封裝成一個

mixin

,方面其他元件或者頁面使用。比如,

select

這個元件需要在使用者點選元件外的地方時,把下拉框隐藏起來,而且其他元件也需要用到這個功能。是以我們可以把使用者點選元件外的地方的邏輯抽離出來,封裝成一個

mixin

。下面講解一下

meta-info

這個

mixin

,主要功能就是根據頁面中的

metaInfo

字段修改網頁的 meta 資訊。

使用方式如下:

export default {
  metaInfo: {
    title: "metaInfo", 設定title
    meta: [
      {
        // 設定meta
        name: "keyWords",
        content: "metaInfo",
      },
    ],
    link: [
      {
        // 設定 link
        rel: "asstes",
        href: "https://github.com/c10342/lin-view-ui",
      },
    ],
  },
};
           

代碼示例:

import { VUE_META_KEY_NAME } from './src/common/constants.js';
import updateMetaInfo from './src/metaOperate/updateMetaInfo.js';
import { isUndefined, isFunction } from '@lin-view-ui/utils';

const VueMetaInfo = {};

VueMetaInfo.install = function install(Vue) {
  Vue.mixin({
    beforeCreate() {
      // 擷取頁面中的 metaInfo 字段資訊
      const metaInfo = this.$options[VUE_META_KEY_NAME];
      // metaInfo 存在
      if (!isUndefined(metaInfo)) {
        // 标記該頁面存在 metaInfo 字段資訊
        this._hasMetaInfo = true;

        // 判斷元件内是否存在computed對象
        if (isUndefined(this.$options.computed)) {
          // 沒有需要先初始化一下
          this.$options.computed = {};
        }

        // 為元件添加computed對象并傳回meta資訊
        // metaInfo 寫法上可以是一個對象,也可以是一個傳回一個對象的函數
        if (isFunction(metaInfo)) {
          this.$options.computed.$metaInfo = metaInfo;
        } else {
          // 如果是一個對象,則需要改寫成函數的形式
          this.$options.computed.$metaInfo = () => metaInfo;
        }
      }
    },
    beforeMount() {
      //   在頁面挂在到dom之前更新meta資訊
      if (this._hasMetaInfo) {
        updateMetaInfo(this.$metaInfo);
      }
    },
    mounted() {
      // dom挂載之後,繼續監聽meta資訊。如果發生變化,繼續更新
      if (this._hasMetaInfo) {
        this.$watch('$metaInfo', () => {
          updateMetaInfo(this.$metaInfo);
        });
      }
    },
    activated() {
      if (this._hasMetaInfo) {
        // keep-alive元件激活時調用
        updateMetaInfo(this.$metaInfo);
      }
    },
    deactivated() {
      if (this._hasMetaInfo) {
        // keep-alive 元件停用時調用。
        updateMetaInfo(this.$metaInfo);
      }
    }
  });
};

export default VueMetaInfo;
           

總結

在元件庫開發中,會遇見很多平時實際項目中用不到的東西,比如

$attrs

provide

injected

$scopedSlots

這些東西。在開發的過程中可以多參考一下其他元件庫的源碼,然後在對比一下自己的實作思路,你會看見很多你所不知道的知識點。最後,如果這篇文章對你有幫助的話,希望可以幫我點個贊。去點贊