6.3 處理事件

要做出人們想要的東西,您必須傾聽客戶的意見。顧客可能并不總是對(duì)的,但在路易斯的面包店,每位員工都知道他們必須始終傾聽顧客的意見——或者至少讓顧客感到被傾聽。

從業(yè)務(wù)角度來看,客戶的輸入推動(dòng)了產(chǎn)品決策。例如,它可以幫助面包店生產(chǎn)更多客戶想要的產(chǎn)品,減少客戶不需要的產(chǎn)品。從軟件的角度來看,用戶輸入會(huì)導(dǎo)致應(yīng)用程序做出反應(yīng),改變其狀態(tài)并顯示新結(jié)果。

在瀏覽器中運(yùn)行的應(yīng)用程序不會(huì)直接接收數(shù)字或字符串等輸入。相反,它們處理事件。當(dāng)用戶單擊、鍵入和滾動(dòng)時(shí),他們會(huì)觸發(fā)事件。這些事件包括有關(guān)用戶交互的詳細(xì)信息,例如他們提交的表單內(nèi)容或單擊的按鈕。

在本節(jié)中,您將學(xué)習(xí)如何處理測(cè)試中的事件并準(zhǔn)確模擬用戶與應(yīng)用程序交互的方式。通過精確表示用戶的輸入,您將獲得更可靠的測(cè)試,因?yàn)樗鼈儗⒏愃朴谶\(yùn)行時(shí)發(fā)生的情況。

要查看事件的工作原理并了解如何測(cè)試它們,您將向應(yīng)用程序添加一個(gè)新表單,該表單允許用戶將項(xiàng)目添加到庫存中。然后,您將使您的應(yīng)用程序在用戶與其交互時(shí)驗(yàn)證表單,并為這些交互編寫更多測(cè)試。

首先,向 index.html 添加一個(gè)包含兩個(gè)字段的表單:一個(gè)用于項(xiàng)目名稱,另一個(gè)用于其數(shù)量。

<!DOCTYPE html>
<html lang="en">
  < !-- ... -->
 
  <body>
    < !-- ... -->
 
    <form id="add-item-form">
      <input
        type="text"
        name="name"
        placeholder="Item name"
      >
      <input
        type="number"
        name="quantity"
        placeholder="Quantity"
      >
      <button type="submit">Add to inventory</button>         ?
    </form>
    <script src="bundle.js"></script>
  </body>
</html>

? 導(dǎo)致表單被提交,觸發(fā)提交事件

在 domController.js 文件中,創(chuàng)建一個(gè)名為 handleAddItem 的函數(shù)。 這個(gè)函數(shù)將接收一個(gè)事件作為它的第一個(gè)參數(shù),檢索提交的值,調(diào)用 addItem 來更新庫存,然后調(diào)用 updateItemList 來更新 DOM。

// ...
 
const handleAddItem = event => {
  event.preventDefault();                                   ?
 
  const { name, quantity } = event.target.elements;
 
  addItem(name.value, parseInt(quantity.value, 10));        ?
 
  updateItemList(data.inventory);
};

? 阻止頁面重新加載,因?yàn)樗J(rèn)情況下

? 因?yàn)閝uantity字段值是一個(gè)字符串,所以我們需要使用parseInt將其轉(zhuǎn)換為數(shù)字。

注意 默認(rèn)情況下,當(dāng)用戶提交表單時(shí),瀏覽器將重新加載頁面。 調(diào)用事件的 preventDefault 方法將取消默認(rèn)行為,導(dǎo)致瀏覽器無法重新加載頁面。

最后,為了在用戶提交新項(xiàng)目時(shí)調(diào)用 handleAddItem,您需要將提交事件的事件偵聽器附加到表單。

現(xiàn)在您有了一個(gè)提交項(xiàng)目的表單,您不再需要在 main.js 文件中手動(dòng)調(diào)用 addItem 和 updateItemList。 相反,您可以替換此文件的全部內(nèi)容,并使其僅將事件偵聽器附加到表單。

const { handleAddItem } = require("./domController");
 
const form = document.getElementById("add-item-form");
form.addEventListener("submit", handleAddItem);           ?

? 每當(dāng)用戶提交表單時(shí)調(diào)用 handleAddItem

在這些更改之后,您應(yīng)該有一個(gè)能夠動(dòng)態(tài)地將項(xiàng)目添加到庫存中的應(yīng)用程序。 要查看它的運(yùn)行情況,請(qǐng)執(zhí)行 npm run build 以重新生成 bundle.js、 npx http-server ./ 以提供 index.html 并訪問 localhost:8080,就像您之前所做的那樣。

現(xiàn)在,考慮一下您將如何測(cè)試剛剛添加的代碼。

一種可能性是為 handleAddItem 函數(shù)本身添加一個(gè)測(cè)試。 該測(cè)試將創(chuàng)建一個(gè)類似事件的對(duì)象并將其作為參數(shù)傳遞給 handleAddItem,如下所示。

const { updateItemList, handleAddItem } = require("./domController");
 
// ...

describe("handleAddItem", () => {
  test("adding items to the page", () => {
    const event = {                                                ?
      preventDefault: jest.fn(),
      target: {
        elements: {
          name: { value: "cheesecake" },
          quantity: { value: "6" }
        }
      }
    };
 
    handleAddItem(event);                                          ?
 
    expect(event.preventDefault.mock.calls).toHaveLength(1);       ?
 
    const itemList = document.getElementById("item-list");
    expect(getByText(itemList, "cheesecake - Quantity: 6"))        ?
      .toBeInTheDocument();
  });
});

? 創(chuàng)建一個(gè)復(fù)制事件接口的對(duì)象

? 練習(xí)handleAddItem函數(shù)

? 檢查表單的默認(rèn)重新加載是否已被阻止

? 檢查 itemList 是否包含具有預(yù)期文本的節(jié)點(diǎn)

為了通過之前的測(cè)試,您必須對(duì)事件的屬性進(jìn)行逆向工程,從頭開始構(gòu)建它。

這種技術(shù)的問題之一是它沒有考慮頁面中的任何實(shí)際輸入元素。因?yàn)槟约簶?gòu)建了事件,所以您可以為名稱和數(shù)量包含任意值。例如,如果您嘗試從 index.html 中刪除輸入元素,即使您的應(yīng)用程序可能無法運(yùn)行,該測(cè)試仍會(huì)通過。

因?yàn)檫@個(gè)測(cè)試是直接調(diào)用handleAddItem的,如圖6.4所示,所以它并不關(guān)心它是否作為submit事件的監(jiān)聽器附加到表單上。例如,如果您嘗試從 main.js 中刪除對(duì) addEventListener 的調(diào)用,則此測(cè)試將繼續(xù)通過。同樣,您發(fā)現(xiàn)了另一種情況,在這種情況下,您的應(yīng)用程序無法運(yùn)行但您的測(cè)試會(huì)通過。


圖6-4

正如您剛剛所做的那樣,手動(dòng)構(gòu)建事件有助于快速迭代并在構(gòu)建偵聽器時(shí)單獨(dú)測(cè)試它們。但是,當(dāng)談到創(chuàng)建可靠的保證時(shí),這種技術(shù)是不夠的。此單元測(cè)試僅涵蓋 handleAddItem 函數(shù)本身,因此無法保證當(dāng)用戶觸發(fā)真實(shí)事件時(shí)應(yīng)用程序會(huì)正常工作。

為了創(chuàng)建更可靠的保證,最好創(chuàng)建一個(gè)真實(shí)的事件實(shí)例,并使用節(jié)點(diǎn)的 dispatchEvent 方法通過 DOM 節(jié)點(diǎn)調(diào)度它。

準(zhǔn)確再現(xiàn)運(yùn)行時(shí)發(fā)生的事情的第一步是更新文檔的正文,使其包含 index.html 中的標(biāo)記,正如我們之前所做的那樣。然后,最好使用 require("./main") 執(zhí)行 main.js,以便它可以將 eventListener 附加到表單。如果您在再次使用 initialHTML 更新文檔正文后不運(yùn)行 main.js,其表單將不會(huì)附加事件偵聽器。

此外,您必須在需要 main.js 之前調(diào)用 jest.resetModules。否則,Jest 將從其緩存中獲取 ./main.js,以防止它再次被執(zhí)行。

const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
 
beforeEach(() => {
  document.body.innerHTML = initialHtml;
 
  jest.resetModules();                    ?
  require("./main");                      ?
});

? 這里你必須使用 jest.resetModules 因?yàn)?,否則,Jest 會(huì)緩存 main.js 并且它不會(huì)再次運(yùn)行。

? 您必須再次執(zhí)行 main.js,以便它可以在每次主體更改時(shí)將事件偵聽器附加到表單。

既然您的文檔具有 index.html 中的內(nèi)容,并且 main.js 已將偵聽器附加到表單,您就可以編寫測(cè)試本身了。 這個(gè)測(cè)試將填充頁面的輸入,創(chuàng)建一個(gè)類型為 submit 的事件,找到表單,并調(diào)用它的 dispatchEvent 方法。 分派事件后,它將檢查列表是否包含它剛剛添加的項(xiàng)目的條目。

const { screen, getByText } = require("@testing-library/dom");
 
// ...
 
test("adding items through the form", () => {
  screen.getByPlaceholderText("Item name").value = "cheesecake";
  screen.getByPlaceholderText("Quantity").value = "6";
 
  const event = new Event("submit");                               ?
  const form = document.getElementById("add-item-form");           ?
  form.dispatchEvent(event);
 
  const itemList = document.getElementById("item-list");
  expect(getByText(itemList, "cheesecake - Quantity: 6"))          ?
    .toBeInTheDocument();
});

? 創(chuàng)建一個(gè)“本地”事件實(shí)例,類型為 submit

? 通過頁面的表單調(diào)度事件

? 檢查分派的事件是否導(dǎo)致頁面包含具有預(yù)期文本的元素

這個(gè)測(cè)試(也顯示在圖 6.5 中)更準(zhǔn)確地代表了運(yùn)行時(shí)發(fā)生的情況。 因?yàn)樗姆秶戎暗臏y(cè)試更廣泛,所以這個(gè)測(cè)試在測(cè)試金字塔中更高,因此它的保證更可靠。 例如,如果您嘗試從 index.html 中刪除輸入元素或從 main.js 中調(diào)用 addEventListener,則此測(cè)試將失敗,與前一個(gè)不同。


圖6-5
// ...
 
const validItems = ["cheesecake", "apple pie", "carrot cake"];
const handleItemName = event => {
  const itemName = event.target.value;
 
  const errorMsg = window.document.getElementById("error-msg");
 
  if (itemName === "") {
    errorMsg.innerHTML = "";
  } else if (!validItems.includes(itemName)) {
    errorMsg.innerHTML = `${itemName} is not a valid item.`;
  } else {
    errorMsg.innerHTML = `${itemName} is valid!`;
  }
};
 
// Don't forget to export `handleItemName`
module.exports = { updateItemList, handleAddItem, handleItemName };

現(xiàn)在,為了使 handleItemName 能夠顯示其消息,向 index.html 添加一個(gè)新的 p 標(biāo)簽,其 id 為 error-msg。

<!DOCTYPE html>
<html lang="en">
  < !-- ... -->
  <body>
    < !-- ... -->
    <p id="error-msg"></p>               ?
 
    <form id="add-item-form">
      < !-- ... -->
    </form>
    <script src="bundle.js"></script>
  </body>
</html>

? 將根據(jù)項(xiàng)目名稱是否有效向用戶顯示反饋的元素

如果您想單獨(dú)測(cè)試 handleItemName 函數(shù),作為練習(xí),您可以嘗試為其編寫單元測(cè)試,就像我們之前為 handleAddItem 函數(shù)所做的那樣。您可以在本書 GitHub 存儲(chǔ)庫的第 6/3_handling_events/1_handling_raw_events 文件夾中找到如何編寫此測(cè)試的完整示例,網(wǎng)址為 https://github.com/lucasfcosta/testing-javascript-applications。

注意 如前所述,對(duì)這些函數(shù)進(jìn)行單元測(cè)試在您迭代時(shí)會(huì)很有用,但分派實(shí)際事件的測(cè)試要可靠得多??紤]到這兩種測(cè)試高度重疊并且需要相似數(shù)量的代碼,如果您必須選擇一種,我建議您堅(jiān)持使用使用元素的 dispatchEvent 的測(cè)試。

如果您愿意編寫處理程序函數(shù)而不在整個(gè)過程中單獨(dú)測(cè)試它們,那么編寫僅使用 dispatchEvent 的測(cè)試可能會(huì)更好。

驗(yàn)證工作的最后一步是附加一個(gè)事件偵聽器,該偵聽器處理在項(xiàng)目名稱的輸入中發(fā)生的輸入事件。更新您的 main.js,并添加以下代碼。

const { handleAddItem, handleItemName } = require("./domController");
 
// ...
 
const itemInput = document.querySelector(`input[name="name"]`);
itemInput.addEventListener("input", handleItemName);               ?

? 使用 handleItemName 處理來自 itemInput 的輸入事件

提示要查看此新功能,請(qǐng)不要忘記在使用 npx http-server ./ 服務(wù)之前通過運(yùn)行 npm run build 來重建 bundle.js。

現(xiàn)在您的驗(yàn)證功能可以正常工作,請(qǐng)為其編寫測(cè)試。 此測(cè)試必須設(shè)置輸入的值并通過輸入節(jié)點(diǎn)分派輸入事件。 派發(fā)事件后,它應(yīng)該檢查文檔是否包含成功消息。

// ...
 
describe("item name validation", () => {
  test("entering valid item names ", () => {
    const itemField = screen.getByPlaceholderText("Item name");
    itemField.value = "cheesecake";
    const inputEvent = new Event("input");                ?
 
    itemField.dispatchEvent(inputEvent);                  ?
 
    expect(screen.getByText("cheesecake is valid!"))      ?
      .toBeInTheDocument();
  });
});

? 使用類型輸入創(chuàng)建事件的“本機(jī)”實(shí)例

? 通過項(xiàng)目名稱的字段調(diào)度事件

? 檢查頁面是否包含預(yù)期的反饋信息

作為練習(xí),嘗試為不愉快的路徑編寫一個(gè)測(cè)試。此測(cè)試應(yīng)輸入無效的項(xiàng)目名稱,通過項(xiàng)目名稱字段調(diào)度事件,并檢查文檔是否包含錯(cuò)誤消息。

回到我們的應(yīng)用程序需求——當(dāng)商品名稱無效時(shí)顯示錯(cuò)誤消息非常好,但是,如果我們不禁止用戶提交表單,他們?nèi)匀豢梢詫o效商品添加到庫存中。我們也沒有任何驗(yàn)證來防止用戶在未指定數(shù)量的情況下提交表單,從而導(dǎo)致顯示 NaN。

為了防止這些無效操作的發(fā)生,您需要重構(gòu)處理程序。不是只偵聽發(fā)生在項(xiàng)目名稱字段上的輸入事件,而是偵聽發(fā)生在表單子項(xiàng)上的所有輸入事件。然后,表單將檢查其子項(xiàng)的值并決定是否應(yīng)禁用提交按鈕。

首先將 handleItemName 重命名為 checkFormValues 并使其驗(yàn)證表單的兩個(gè)字段中的值。

// ...
 
const validItems = ["cheesecake", "apple pie", "carrot cake"];
const checkFormValues = () => {
  const itemName = document.querySelector(`input[name="name"]`).value;
  const quantity = document.querySelector(`input[name="quantity"]`).value;
 
  const itemNameIsEmpty = itemName === "";
  const itemNameIsInvalid = !validItems.includes(itemName);
  const quantityIsEmpty = quantity === "";
 
  const errorMsg = window.document.getElementById("error-msg");
  if (itemNameIsEmpty) {
    errorMsg.innerHTML = "";
  } else if (itemNameIsInvalid) {
    errorMsg.innerHTML = `${itemName} is not a valid item.`;
  } else {
    errorMsg.innerHTML = `${itemName} is valid!`;
  }
 
  const submitButton = document.querySelector(`button[type="submit"]`);
  if (itemNameIsEmpty || itemNameIsInvalid || quantityIsEmpty) {           ?
    submitButton.disabled = true;
  } else {
    submitButton.disabled = false;
  }
};
 
// Don't forget to update your exports!
module.exports = { updateItemList, handleAddItem, checkFormValues };

? 禁用或啟用表單的提交輸入,取決于表單字段中的值是否有效

現(xiàn)在更新 main.js,而不是將 handleItemName 附加到名稱輸入,而是將新的 checkFormValues 附加到您的表單。 這個(gè)新的偵聽器將響應(yīng)從表單子項(xiàng)冒泡的任何輸入事件。

form.addEventListener("input", checkFormValues);                 ?
 
// Run `checkFormValues` once to see if the initial state is valid
checkFormValues();

? checkFormValues 函數(shù)現(xiàn)在將處理表單中觸發(fā)的任何輸入事件,包括將從表單的子級(jí)冒泡的輸入事件。

注意要查看應(yīng)用程序的工作情況,請(qǐng)?jiān)谔峁┓?wù)之前使用 npm run build 重建它,正如我們?cè)诒菊轮卸啻瓮瓿傻哪菢印?/p>

鑒于您已保留用戶輸入無效項(xiàng)目名稱時(shí)出現(xiàn)的錯(cuò)誤消息,項(xiàng)目名稱驗(yàn)證的先前測(cè)試應(yīng)繼續(xù)通過。但是,如果您嘗試重新運(yùn)行它們,您會(huì)發(fā)現(xiàn)它們失敗了。

提示要僅運(yùn)行 main.test.js 中的測(cè)試,您可以將 main.test.js 作為第一個(gè)參數(shù)傳遞給 jest 命令。

如果您從 node_modules 文件夾運(yùn)行 jest,您的命令應(yīng)該類似于 ./node_modules/.bin/jest main.test.js。

如果你添加了一個(gè) NPM 腳本來運(yùn)行 Jest,例如 test,你應(yīng)該運(yùn)行 npm run test -- main.test.js。

這些測(cè)試失敗是因?yàn)槟{(diào)度的事件不會(huì)冒泡。例如,當(dāng)通過 item name 字段調(diào)度 input 事件時(shí),它不會(huì)觸發(fā)任何附加到其父級(jí)的偵聽器,包括附加到表單的偵聽器。因?yàn)楸韱伪O(jiān)聽器沒有被執(zhí)行,它不會(huì)向頁面添加任何錯(cuò)誤信息,導(dǎo)致你的測(cè)試失敗。

要通過使事件冒泡來修復(fù)您的測(cè)試,您必須在實(shí)例化事件時(shí)傳遞一個(gè)額外的參數(shù)。此附加參數(shù)應(yīng)包含名為氣泡的屬性,其值為 true。使用此選項(xiàng)創(chuàng)建的事件將冒泡并觸發(fā)附加到元素父級(jí)的偵聽器。

// ...
 
describe("item name validation", () => {
  test("entering valid item names ", () => {
    const itemField = screen.getByPlaceholderText("Item name");
    itemField.value = "cheesecake";
    const inputEvent = new Event("input", { bubbles: true });    ?
 
    itemField.dispatchEvent(inputEvent);                         ?
 
    expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
  });
});
 
// ...

? 創(chuàng)建一個(gè)帶有類型輸入的 Event 的“原生”實(shí)例,它可以向上冒泡到元素的父元素,通過它分派它

? 通過項(xiàng)目名稱的字段調(diào)度事件。因?yàn)槭录?bubble 屬性設(shè)置為 true,所以它會(huì)冒泡到表單,觸發(fā)它的監(jiān)聽器。

為了避免手動(dòng)實(shí)例化和分派事件,dom-testing-library 包含一個(gè)名為 fireEvent 的實(shí)用程序。

使用 fireEvent,您可以準(zhǔn)確模擬多種不同類型的事件,包括提交表單、按鍵和更新字段。由于 fireEvent 處理在特定組件上觸發(fā)事件時(shí)您需要執(zhí)行的所有操作,因此它可以幫助您編寫更少的代碼,而不必?fù)?dān)心觸發(fā)事件時(shí)發(fā)生的所有事情。

例如,通過使用 fireEvent 而不是手動(dòng)創(chuàng)建輸入事件,您可以避免必須為項(xiàng)目名稱設(shè)置字段的 value 屬性。 fireEvent 函數(shù)知道輸入事件會(huì)更改通過其調(diào)度的組件的值。因此,它將為您處理更改值。

更新表單驗(yàn)證的測(cè)試,以便它們使用 dom-testing-library 中的 fireEvent 實(shí)用程序。

// ...
 
const { screen, getByText, fireEvent } = require("@testing-library/dom");
 
// ...
 
describe("item name validation", () => {
  test("entering valid item names ", () => {
    const itemField = screen.getByPlaceholderText("Item name");
 
    fireEvent.input(itemField, {                  ?
      target: { value: "cheesecake" },
      bubbles: true
    });
 
    expect(screen.getByText("cheesecake is valid!")).toBeInTheDocument();
  });
});

? 不是創(chuàng)建一個(gè)事件然后調(diào)度它,而是使用 fireEvent.input 在字段上觸發(fā)一個(gè)項(xiàng)目名稱的輸入事件。

提示 如果您需要更準(zhǔn)確地模擬用戶事件,例如用戶以一定的速度打字,您可以使用用戶事件庫,該庫也是由 testing-library 組織制作的。

例如,當(dāng)您有使用去抖動(dòng)驗(yàn)證的字段時(shí),此庫特別有用:僅在用戶停止輸入后的特定時(shí)間觸發(fā)的驗(yàn)證。

您可以在 https://github.com/testing-library/user-event 查看@testing-library/user-event 的完整文檔。

作為練習(xí),嘗試更新所有其他測(cè)試,以便它們使用 fireEvent。我還建議與庫存管理器處理不同類型的交互并對(duì)其進(jìn)行測(cè)試。例如,您可以嘗試在用戶雙擊項(xiàng)目列表中的姓名時(shí)刪除項(xiàng)目。

在本節(jié)之后,您應(yīng)該能夠編寫測(cè)試來驗(yàn)證用戶將與您的頁面進(jìn)行的交互。盡管手動(dòng)構(gòu)建事件以便在迭代時(shí)獲得快速反饋是可以的,但這不是創(chuàng)建最可靠質(zhì)量保證的那種測(cè)試。相反,為了更準(zhǔn)確地模擬用戶的行為——因此,創(chuàng)造更可靠的保證——你可以使用 dispatchEvent 調(diào)度本機(jī)事件或使用第三方庫來使這個(gè)過程更方便。當(dāng)涉及到捕獲錯(cuò)誤時(shí),這種相似性將使您的測(cè)試更有價(jià)值,并且因?yàn)槟鷽]有嘗試手動(dòng)重現(xiàn)事件的界面,它們將導(dǎo)致更少的維護(hù)開銷。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. 組件的data為什么必須是函數(shù)? 組件中的 data 寫成一個(gè)函數(shù),數(shù)據(jù)以函數(shù)返回值形式定義,這樣每復(fù)用一次...
    郭先生_515閱讀 1,047評(píng)論 0 12
  • 事件流 JavaScript與HTML之間的交互是通過事件實(shí)現(xiàn)的。事件,就是文檔或?yàn)g覽器窗口中發(fā)生的一些特定的交互...
    DHFE閱讀 910評(píng)論 0 3
  • ??JavaScript 與 HTML 之間的交互是通過事件實(shí)現(xiàn)的。 ??事件,就是文檔或?yàn)g覽器窗口中發(fā)生的一些特...
    霜天曉閱讀 3,690評(píng)論 1 11
  • Vue真是太好了 壹萬多字的Vue知識(shí)點(diǎn) 超詳細(xì)! 9 ?1??、Vue和其他兩大框架的區(qū)別 Angular 學(xué)習(xí)...
    三千繁夢(mèng)閱讀 409評(píng)論 0 0
  • ?1??、Vue和其他兩大框架的區(qū)別 Angular 學(xué)習(xí)成本太高 React 代碼可讀性差 Vue 學(xué)習(xí)成本較低...
    藍(lán)海00閱讀 74,195評(píng)論 40 1,229

友情鏈接更多精彩內(nèi)容