element-ui input組件源碼分析整理筆記(六)

来源:https://www.cnblogs.com/fangnianqin/archive/2018/12/04/10063299.html
-Advertisement-
Play Games

input 輸入框組件 源碼: html <! 當type的值不等於textarea時 <! 前置元素 <! 核心部分:輸入框 <! input框內的頭部的內容 <! prefixIcon頭部圖標存在時,顯示i標簽 <! input框內的尾部的內容 <! showClear為false時,顯示尾部圖 ...


input 輸入框組件

源碼:

<template>
  <div :class="[
    type === 'textarea' ? 'el-textarea' : 'el-input',
    inputSize ? 'el-input--' + inputSize : '',
    {
      'is-disabled': inputDisabled,
      'el-input-group': $slots.prepend || $slots.append,
      'el-input-group--append': $slots.append,
      'el-input-group--prepend': $slots.prepend,
      'el-input--prefix': $slots.prefix || prefixIcon,
      'el-input--suffix': $slots.suffix || suffixIcon || clearable
    }
    ]"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
      <!--當type的值不等於textarea時-->
    <template v-if="type !== 'textarea'">
      <!-- 前置元素 -->
      <div class="el-input-group__prepend" v-if="$slots.prepend">
        <slot name="prepend"></slot>
      </div>
        <!--核心部分:輸入框-->
      <input
        :tabindex="tabindex"
        v-if="type !== 'textarea'"
        class="el-input__inner"
        v-bind="$attrs"
        :type="type"
        :disabled="inputDisabled"
        :readonly="readonly"
        :autocomplete="autoComplete || autocomplete"
        :value="currentValue"
        ref="input"
        @compositionstart="handleComposition"
        @compositionupdate="handleComposition"
        @compositionend="handleComposition"
        @input="handleInput"
        @focus="handleFocus"
        @blur="handleBlur"
        @change="handleChange"
        :aria-label="label"
      >

      <!-- input框內的頭部的內容 -->
      <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
        <slot name="prefix"></slot>
          <!--prefixIcon頭部圖標存在時,顯示i標簽-->
        <i class="el-input__icon" v-if="prefixIcon" :class="prefixIcon"></i>
      </span>
        <!-- input框內的尾部的內容 -->
      <span class="el-input__suffix" v-if="$slots.suffix || suffixIcon || showClear || validateState && needStatusIcon">
        <span class="el-input__suffix-inner">
           <!--showClear為false時,顯示尾部圖標-->
          <template v-if="!showClear">
            <slot name="suffix"></slot>
            <i class="el-input__icon" v-if="suffixIcon" :class="suffixIcon"></i>
          </template>
            <!--showClear為true時,顯示清空圖標-->
          <i v-else class="el-input__icon el-icon-circle-close el-input__clear" @click="clear"></i>
        </span>
          <!--這裡應該是跟表單的校驗相關,根據校驗狀態顯示對應的圖標-->
        <i class="el-input__icon" v-if="validateState" :class="['el-input__validateIcon', validateIcon]"></i>
      </span>
      <!-- 後置元素 -->
      <div class="el-input-group__append" v-if="$slots.append">
        <slot name="append"></slot>
      </div>
    </template>
      <!--當type的值等於textarea時-->
    <textarea
      v-else
      :tabindex="tabindex"
      class="el-textarea__inner"
      :value="currentValue"
      @compositionstart="handleComposition"
      @compositionupdate="handleComposition"
      @compositionend="handleComposition"
      @input="handleInput"
      ref="textarea"
      v-bind="$attrs"
      :disabled="inputDisabled"
      :readonly="readonly"
      :autocomplete="autoComplete || autocomplete"
      :style="textareaStyle"
      @focus="handleFocus"
      @blur="handleBlur"
      @change="handleChange"
      :aria-label="label"
    >
    </textarea>
  </div>
</template>
<script>
  import emitter from 'element-ui/src/mixins/emitter';
  import Migrating from 'element-ui/src/mixins/migrating';
  import calcTextareaHeight from './calcTextareaHeight';
  import merge from 'element-ui/src/utils/merge';
  import { isKorean } from 'element-ui/src/utils/shared';

  export default {
    name: 'ElInput',

    componentName: 'ElInput',

    mixins: [emitter, Migrating],

    inheritAttrs: false,

    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },

    data() {
      return {
        currentValue: this.value === undefined || this.value === null
          ? ''
          : this.value,
        textareaCalcStyle: {},
        hovering: false,
        focused: false,
        isOnComposition: false,
        valueBeforeComposition: null
      };
    },

    props: {
      value: [String, Number], //綁定值
      size: String, //輸入框尺寸,只在type!="textarea" 時有效
      resize: String, //控制是否能被用戶縮放
      form: String,
      disabled: Boolean, //禁用
      readonly: Boolean,
      type: {  //類型texttextarea和其他原生input的type值
        type: String,
        default: 'text'
      },
      autosize: { //自適應內容高度,只對 type="textarea" 有效,可傳入對象,如,{ minRows: 2, maxRows: 6 }
        type: [Boolean, Object],
        default: false
      },
      autocomplete: {
        type: String,
        default: 'off'
      },
      /** @Deprecated in next major version */
      autoComplete: {
        type: String,
        validator(val) {
          process.env.NODE_ENV !== 'production' &&
            console.warn('[Element Warn][Input]\'auto-complete\' property will be deprecated in next major version. please use \'autocomplete\' instead.');
          return true;
        }
      },
      validateEvent: { //輸入時是否觸發表單的校驗
        type: Boolean,
        default: true
      },
      suffixIcon: String, //輸入框尾部圖標
      prefixIcon: String, //輸入框頭部圖標
      label: String, //輸入框關聯的label文字
      clearable: { //是否可清空
        type: Boolean,
        default: false
      },
      tabindex: String //輸入框的tabindex
    },

    computed: {
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      //校驗狀態
      validateState() {
        return this.elFormItem ? this.elFormItem.validateState : '';
      },
      needStatusIcon() {
        return this.elForm ? this.elForm.statusIcon : false;
      },
      validateIcon() {
        return {
          validating: 'el-icon-loading',
          success: 'el-icon-circle-check',
          error: 'el-icon-circle-close'
        }[this.validateState];
      },
      //textarea的樣式
      textareaStyle() {
        return merge({}, this.textareaCalcStyle, { resize: this.resize });
      },
      //輸入框尺寸,只在 type!="textarea" 時有效
      inputSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },
      //input是否被禁用
      inputDisabled() {
        return this.disabled || (this.elForm || {}).disabled;
      },
      //是否顯示清空按鈕
      showClear() {
        // clearable屬性為true,即用戶設置了顯示清空按鈕的屬性;並且在非禁用且非只讀狀態下才且當前input的value不是空且該input獲得焦點或者滑鼠移動上去才顯示
        return this.clearable &&
          !this.inputDisabled &&
          !this.readonly &&
          this.currentValue !== '' &&
          (this.focused || this.hovering);
      }
    },

    watch: {
      value(val, oldValue) {
        this.setCurrentValue(val);
      }
    },

    methods: {
      focus() {
        (this.$refs.input || this.$refs.textarea).focus();
      },
      blur() {
        (this.$refs.input || this.$refs.textarea).blur();
      },
      getMigratingConfig() {
        return {
          props: {
            'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
            'on-icon-click': 'on-icon-click is removed.'
          },
          events: {
            'click': 'click is removed.'
          }
        };
      },
      handleBlur(event) {
        this.focused = false;
        this.$emit('blur', event);
        if (this.validateEvent) {
          this.dispatch('ElFormItem', 'el.form.blur', [this.currentValue]);
        }
      },
      select() {
        (this.$refs.input || this.$refs.textarea).select();
      },
      resizeTextarea() {
        if (this.$isServer) return;
        //autosize自適應內容高度,只對 type="textarea" 有效,可傳入對象,如,{ minRows: 2, maxRows: 6 }
        const { autosize, type } = this;
        if (type !== 'textarea') return;
        //如果沒設置自適應內容高度
        if (!autosize) {
          this.textareaCalcStyle = { //高度取文本框的最小高度
            minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
          };
          return;
        }
        const minRows = autosize.minRows;
        const maxRows = autosize.maxRows;
        //如果設置了minRows和maxRows需要計算文本框的高度
        this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
      },
      handleFocus(event) {
        this.focused = true;
        this.$emit('focus', event);
      },
      handleComposition(event) {
        // 如果中文輸入已完成
        if (event.type === 'compositionend') {
          //  isOnComposition設置為false
          this.isOnComposition = false;
          this.currentValue = this.valueBeforeComposition;
          this.valueBeforeComposition = null;
          //觸發input事件,因為input事件是在compositionend事件之後觸發,這時輸入未完成,不會將值傳給父組件,所以需要再調一次input方法
          this.handleInput(event);
        } else {  //如果中文輸入未完成
          const text = event.target.value;
          const lastCharacter = text[text.length - 1] || '';
          //isOnComposition用來判斷是否在輸入拼音的過程中
          this.isOnComposition = !isKorean(lastCharacter);
          if (this.isOnComposition && event.type === 'compositionstart') {
            //  輸入框中輸入的值賦給valueBeforeComposition
            this.valueBeforeComposition = text;
          }
        }
      },
      handleInput(event) {
        const value = event.target.value;
        //設置當前值
        this.setCurrentValue(value);
        //如果還在輸入中,將不會把值傳給父組件
        if (this.isOnComposition) return;
        //輸入完成時,isOnComposition為false,將值傳遞給父組件
        this.$emit('input', value);
      },
      handleChange(event) {
        this.$emit('change', event.target.value);
      },
      setCurrentValue(value) {
        // 輸入中,直接返回
        if (this.isOnComposition && value === this.valueBeforeComposition) return;
        this.currentValue = value;
        if (this.isOnComposition) return;
        //輸入完成,設置文本框的高度
        this.$nextTick(this.resizeTextarea);
        if (this.validateEvent && this.currentValue === this.value) {
          this.dispatch('ElFormItem', 'el.form.change', [value]);
        }
      },
      calcIconOffset(place) {
        let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
        if (!elList.length) return;
        let el = null;
        for (let i = 0; i < elList.length; i++) {
          if (elList[i].parentNode === this.$el) {
            el = elList[i];
            break;
          }
        }
        if (!el) return;
        const pendantMap = {
          suffix: 'append',
          prefix: 'prepend'
        };

        const pendant = pendantMap[place];
        if (this.$slots[pendant]) {
          el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`;
        } else {
          el.removeAttribute('style');
        }
      },
      updateIconOffset() {
        this.calcIconOffset('prefix');
        this.calcIconOffset('suffix');
      },
      //清空事件
      clear() {
        //父組件的value值變成了空,更新父組件中v-model的值
        this.$emit('input', '');
        //觸發了父組件的change事件,父組件中就可以監聽到該事件
        this.$emit('change', '');
        //觸發了父組件的clear事件
        this.$emit('clear');
        //更新當前的currentValue的值
        this.setCurrentValue('');
      }
    },

    created() {
      this.$on('inputSelect', this.select);
    },

    mounted() {
      this.resizeTextarea();
      this.updateIconOffset();
    },

    updated() {
      this.$nextTick(this.updateIconOffset);
    }
  };
</script>

如下圖所示:

(2)核心部分 input 輸入框

<input
        :tabindex="tabindex"
        v-if="type !== 'textarea'"
        class="el-input__inner"
        v-bind="$attrs"
        :type="type"
        :disabled="inputDisabled"
        :readonly="readonly"
        :autocomplete="autoComplete || autocomplete"
        :value="currentValue"
        ref="input"
        @compositionstart="handleComposition"
        @compositionupdate="handleComposition"
        @compositionend="handleComposition"
        @input="handleInput"
        @focus="handleFocus"
        @blur="handleBlur"
        @change="handleChange"
        :aria-label="label"
      >

1、 :tabindex="tabindex" 是控制tab鍵按下後的訪問順序,由用戶傳入tabindex;如果設置為負數則無法通過tab鍵訪問,設置為0則是在最後訪問。

2、 v-bind="$attrs" 為了簡化父組件向子組件傳值,props沒有註冊的屬性,可以通過$attrs來取。

3、inputDisabled :返回當前input是否被禁用;readonly:input的原生屬性,是否是只讀狀態;

4、 原生方法compositionstart、compositionupdate、compositionend

compositionstart 官方解釋 : 觸發於一段文字的輸入之前(類似於 keydown 事件,但是該事件僅在若幹可見字元的輸入之前,而這些可見字元的輸入可能需要一連串的鍵盤操作、語音識別或者點擊輸入法的備選詞),通俗點,假如我們要輸入一段中文,當我們按下第一個字母的時候觸發 。
compositionupdate在我們中文開始輸入到結束完成的每一次keyup觸發。
compositionend則在我們完成當前中文的輸入觸發 。

這三個事件主要解決中文輸入的響應問題,從compositionstart觸發開始,意味著中文輸入的開始且還沒完成,所以此時我們不需要做出響應,在compositionend觸發時,表示中文輸入完成,這時我們可以做相應事件的處理。

 handleComposition(event) {
        // 如果中文輸入已完成
        if (event.type === 'compositionend') {
          //  isOnComposition設置為false
          this.isOnComposition = false;
          this.currentValue = this.valueBeforeComposition;
          this.valueBeforeComposition = null;
          //觸發input事件,因為input事件是在compositionend事件之後觸發,這時輸入未完成,不會將值傳給父組件,所以需要再調一次input方法
          this.handleInput(event);
        } else {  //如果中文輸入未完成
          const text = event.target.value;
          const lastCharacter = text[text.length - 1] || '';
          //isOnComposition用來判斷是否在輸入拼音的過程中
          this.isOnComposition = !isKorean(lastCharacter);
          if (this.isOnComposition && event.type === 'compositionstart') {
            //  輸入框中輸入的值賦給valueBeforeComposition
            this.valueBeforeComposition = text;
          }
        }
      },
      handleInput(event) {
        const value = event.target.value;
        //設置當前值
        this.setCurrentValue(value);
        //如果還在輸入中,將不會把值傳給父組件
        if (this.isOnComposition) return;
        //輸入完成時,isOnComposition為false,將值傳遞給父組件
        this.$emit('input', value);
      },

(3)calcTextareaHeight.js使用來計算文本框的高度

//原理:讓height等於scrollHeight,也就是滾動條捲去的高度,這裡就將height變大了,然後返回該height並綁定到input的style中從而動態改變textarea的height
let hiddenTextarea;
//存儲隱藏時候的css樣式的
const HIDDEN_STYLE = `
  height:0 !important;
  visibility:hidden !important;
  overflow:hidden !important;
  position:absolute !important;
  z-index:-1000 !important;
  top:0 !important;
  right:0 !important
`;
//用來存儲要查詢的樣式名
const CONTEXT_STYLE = [
  'letter-spacing',
  'line-height',
  'padding-top',
  'padding-bottom',
  'font-family',
  'font-weight',
  'font-size',
  'text-rendering',
  'text-transform',
  'width',
  'text-indent',
  'padding-left',
  'padding-right',
  'border-width',
  'box-sizing'
];

function calculateNodeStyling(targetElement) {
  // 獲取目標元素計算後的樣式,即實際渲染的樣式
  const style = window.getComputedStyle(targetElement);
  // getPropertyValue方法返回指定的 CSS 屬性的值;這裡返回box-sizing屬性的值
  const boxSizing = style.getPropertyValue('box-sizing');
  // padding-bottom和padding-top值之和
  const paddingSize = (
    parseFloat(style.getPropertyValue('padding-bottom')) +
    parseFloat(style.getPropertyValue('padding-top'))
  );
  // border-bottom-width和border-top-width值之和
  const borderSize = (
    parseFloat(style.getPropertyValue('border-bottom-width')) +
    parseFloat(style.getPropertyValue('border-top-width'))
  );
  // 其他屬性以及對應的值
  const contextStyle = CONTEXT_STYLE
    .map(name => `${name}:${style.getPropertyValue(name)}`)
    .join(';');

  return { contextStyle, paddingSize, borderSize, boxSizing };
}

export default function calcTextareaHeight(
  targetElement, //目標元素
  minRows = 1, //最小行數
  maxRows = null //最大行數
) {
    // 創建一個隱藏的文本域
  if (!hiddenTextarea) {
    hiddenTextarea = document.createElement('textarea');
    document.body.appendChild(hiddenTextarea);
  }
  //獲取目標元素的樣式
  let {
    paddingSize,
    borderSize,
    boxSizing,
    contextStyle
  } = calculateNodeStyling(targetElement);
  //設置對應的樣式屬性
  hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
  hiddenTextarea.value = targetElement.value || targetElement.placeholder || '';

  // 獲取滾動高度
  let height = hiddenTextarea.scrollHeight;
  const result = {};

  if (boxSizing === 'border-box') {
    // 如果是 border-box,高度需加上邊框
    height = height + borderSize;
  } else if (boxSizing === 'content-box') {
   // 如果是 content-box,高度需減去上下內邊距
    height = height - paddingSize;
  }
  // 計算單行高度,先清空內容
  hiddenTextarea.value = '';
  // 再用滾動高度減去上下內邊距
  let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;

  if (minRows !== null) {  // 如果參數傳遞了 minRows
      // 最少的高度=單行的高度*行數
    let minHeight = singleRowHeight * minRows;
    if (boxSizing === 'border-box') {
      // 如果是 border-box,還得加上上下內邊距和上下邊框的寬度
      minHeight = minHeight + paddingSize + borderSize;
    }
    // 高度取二者最大值
    height = Math.max(minHeight, height);
    result.minHeight = `${ minHeight }px`;
  }
  if (maxRows !== null) {
    let maxHeight = singleRowHeight * maxRows;
    if (boxSizing === 'border-box') {
      maxHeight = maxHeight + paddingSize + borderSize;
    }
    height = Math.min(maxHeight, height);
  }
  result.height = `${ height }px`;
  hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea);
  hiddenTextarea = null;
  return result;
};

參考博文:https://www.jianshu.com/p/74ba49507fe6
https://juejin.im/post/5b7d18e46fb9a01a12502616


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ①為什麼要使用原型:為了實現繼承。 ②利用constructor屬性可以讓實例化對象輕鬆訪問原型,實現實例化對象對原型對象的修改,但是原型對象是全局對象,一般不能隨意修改原型對象的成員。該屬性多用於調試。 ③原型是構造函數的屬性,原型是實例化對象的原型對象。 ④實例化對象如何訪問原型對象: func ...
  • html : 1、相當於沒有穿衣服的人,一套瀏覽器認識的規則, 2、開發者: 學習html規則 開發後臺程式: -寫html文件(充當模板) -資料庫獲取數據,然後替換到html文件的指定位置(web框架) 3、本地測試 -找到文件路徑,直接用瀏覽器打開 -用pycharm打開測試 4、編寫html ...
  • 類數組對象:arguments總所周知,js是一門相當靈活的語言。當我們在js中在調用一個函數的時候,我們經常會給這個函數傳遞一些參數,js把傳入到這個函數的全部參數存儲在一個叫做arguments的東西裡面,那麼這到底是什麼東西?在js中萬物皆對象,甚至數組字元串函數都是對象。所以這個叫做argu ...
  • js代碼: ...
  • /* * 中間就可以進行封裝操作 * mui就代表mui,owner就代表window的app屬性,就是一個傳值 */ (function(mui,owner) { /** * 獲取當前狀態 **/ owner.getState = function() { var stateText = plus ...
  • 本文由QQ音樂前端團隊發表 前段時間做了一個非常有意思的模擬終端的展示頁:http://ursb.me/terminal/(沒有做移動端適配,請在PC端訪問),這個頁面非常有意思,它可以作為個人博客系統或者給 Linux 初學者學習終端命令,現分享給大家~ 開源地址:airingursb/termi ...
  • 一直以來,我都以為我已經懂了JavaScript中 閉包 的概念,直到有一次小伙伴突然問我這個概念的時候,我才發現我根本不知道該怎來麽跟他來講述這個概念。 那時候我就知道我是自我欺騙,打腫臉充胖子了。 所以,花了點時間去專門瞭解了一下,今天專門記錄一下自己所理解的閉包。 一. 概念 閉包,簡單來講, ...
  • 編碼 首先練習數字相關的一些操作: 基於如上HTML,實現需求 按照HTML中按鈕的描述以此實現功能 計算結果顯示在 id 為 result 的 P 標簽中 除了第一個按鈕,其它按鈕操作時,都需要判斷輸入是否為數字,否則在 console 中輸出錯誤信息 註意點:Math.round()判斷有小數點 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...