JavaScript Promise:簡介

Promise 可以簡化延遲和非同步運算。承諾代表尚未完成的作業。

Jake Archibald
Jake Archibald

開發人員可以準備進入 網站開發應用程式。

[鼓聲響開始]

Promise 已加入 JavaScript!

[煙火爆炸、從空中閃耀的紙雨,天生大爆炸]

目前您的所屬類別如下:

  • 你周圍的人都是歡呼,但你不確定那是什麼 介紹生成式 AI 模型也許你根本不知道會有什麼不過,無論內部 IP 位址為何 DNS 名稱始終會指向特定的執行個體你聳肩 閃亮光滑的紙張重量落在肩膀上。如果有,請不要 別擔心,我花了很多時間研究自己為何該關心這件事 內容您可能會想從開始著手。
  • 你打出空中!剛剛好嗎?你曾使用這些 Promise 但您會擔心所有實作的 API 略有不同。 官方 JavaScript 版本的 API 為何?不妨先從 建立術語
  • 您深知這件事情,也對那些勇往直前, 對他們來說是很重要的花點時間好好感受自己的優越表現 然後直接參閱 API 參考資料

瀏覽器支援和 polyfill

瀏覽器支援

  • Chrome:32.
  • Edge:12.
  • Firefox:29。
  • Safari:8.

資料來源

為缺乏完整承諾的瀏覽器 或對其他瀏覽器和 Node.js 提供保證 polyfill (2K gzip 壓縮)。

有什麼煩惱嗎?

JavaScript 是單一執行緒,意味著無法在 之後就必須執行一個在瀏覽器中,使用 JavaScript 這個執行緒與其他平台不同 。但 JavaScript 通常和繪製作業位於相同的佇列中,並進行更新 樣式、處理使用者動作 (例如醒目顯示文字和互動) 和表單控制項)。發生上述任何一項活動時,其他活動會延遲顯示。

人在說,「多執行緒」。您可以用多指輸入文字 就能同時發起對話唯一的封鎖方式 我們需要處理的功能是打噴嚏,而目前所有活動都必須 在打噴嚏期間遭到停權。實在是很令人困擾 尤其是在開車或嘗試保留對話時不要 就像寫程式那樣

您可能已運用事件和回呼解決這個問題。以下為這類活動:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

這個結果完全沒有洩漏。取得圖片,新增幾個事件監聽器,接著 JavaScript 可能會停止執行,直到呼叫其中一個接聽程式為止。

遺憾的是,在上述範例中,事件可能發生 因此,我們需要使用 「完整」圖片屬性:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

無法在在我們有機會監聽前擷取出錯誤的圖片 them;可惜的是, DOM 無法達成此目的。此外,這是 正在載入一張圖片。而我們單憑藉這個方式得知 已載入 張圖片。

活動有時不見得

活動適合在同一時間發生的重複活動 物件—keyuptouchstart 等。您並不在乎這些事件 附加監聽器之前發生的事件但以這個模型來說 非同步成功/失敗的功能,理想情況下,不妨採用以下做法:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

這樣的命名方式更好,但名稱更貼合標準。如果 HTML 圖片元素包含 「準備好」方法,這樣我們就可以:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

基本上,保證有點像事件監聽器,除了:

  • 一個承諾只能成功或出現一次。無法成功或失敗兩次 兩者都不會切換為失敗或失敗,反之亦然。
  • 如果承諾成功或失敗,且您之後新增成功/失敗 回呼的情況下,即使事件產生 放在較早的位置

這對非同步成功/失敗來說非常實用,因為 希望瞭解產品的確切上市時間 回應結果

承諾術語

Domenic Denicola 證明閱讀初稿 並將我評為「F」這個術語他將我成為注意力 強制我複製 州與命運 這也寫了一封信念給爸媽。儘管如此 混合使用相同術語,但基本知識如下:

例如:

  • fulfill - 與承諾內容相關的動作成功
  • rejected:無法執行與承諾相關的動作
  • 待處理 - 尚未出貨或遭到拒絕
  • settled - 已完成或遭到拒絕

規格 同樣使用 thenable 字詞描述類似承諾的物件 其中含有 then 方法這個字詞提醒我參加前英格蘭足球賽 管理員 Terry Venables 我會盡可能減少使用它。

Promise 導入 JavaScript!

Promise 出現在程式庫中已有一段時間,例如:

上述方法和 JavaScript 都承諾具有相同的標準化行為。 稱為 Promises/A+。如果 您是 jQuery 使用者 延遲。不過 延遲不符合 Promise/A+ 規範,因此違反規定 有些許不同、較不實用, 請小心jQuery 也 Promise 類型,但這只是 但一樣的問題

雖然保證導入作業遵循標準化行為, 整體 API 的差異在 API 中,JavaScript 的保證與 RSVP.js 很類似。 以下說明建立承諾的方式:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

承諾的建構函式會採用一個引數,即含有兩個參數的回呼。 解決並拒絕在回呼中執行操作,可能是非同步,然後呼叫 如果一切運作正常,就表示呼叫拒接。

與純舊 JavaScript 中的 throw 一樣,這是自訂功能,但並非必要 拒絕錯誤。Error 物件的優點是會擷取 堆疊追蹤,讓偵錯工具更加實用。

以下說明您如何運用這項承諾:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() 會使用兩個引數,一個成功案例的回呼 以供失敗案例使用兩者皆為選用,因此您可以新增 僅限成功或失敗案例

在 DOM 中啟動的 JavaScript 承諾為「Futures」,並重新命名為「Promises」。 最後移到 JavaScript使用 JavaScript 而非 DOM 很適合用於非瀏覽器的 JS 環境,例如 Node.js (是否在核心 API 中使用 Node.js,也是其他問題)。

雖然是 JavaScript 功能,但是 DOM 無法使用。於 事實上,所有具備非同步成功/失敗方法的新 DOM API 都會使用承諾。 這個問題已經發生在 配額管理字型載入事件ServiceWorkerWeb MIDI串流等。

與其他程式庫的相容性

JavaScript 承諾 API 會將包含 then() 方法的所有內容視為 類似承諾使用 (或以 thenable 提供承諾,表示「嘆氣」),因此如果您使用程式庫 能夠傳回 Q 承諾,別擔心,使用新的 JavaScript 保證。

不過如我所說,jQuery 的「延遲」有點不實用。 謝天謝地,各位可以做出標準承諾,這是很值得的 越快越好:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

這裡,jQuery 的 $.ajax 會傳回 Deferred。它採用 then() 方法 Promise.resolve() 可以將其轉換為 JavaScript 承諾。不過 有時可能會延遲將多個引數傳遞至其回呼,例如:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

然而,JS 可保證只會忽略第一個細節:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

幸好這通常是您想要的,或至少開放您存取 你想要什麼另請注意,jQuery 不會遵循 將錯誤物件傳送至拒絕項目。

簡化複雜的非同步程式碼

現在來編寫一些程式碼假設我們想:

  1. 啟動旋轉圖示來表示載入中
  2. 擷取故事的部分 JSON,讓我們取得每個章節的標題和網址
  3. 新增頁面標題
  4. 擷取各章節
  5. 將故事新增至頁面
  6. 停止旋轉圖示

...而且也告知使用者註冊過程是否發生問題。我們會 停止旋轉圖示,不然就會一直旋轉, 當機並當機到其他 UI 中

當然,你不會用 JavaScript 傳達故事 以 HTML 格式放送 但這種模式在處理 API 時很常見:多重資料 擷取作業完成後再執行其他操作

首先,我們要從網路擷取資料:

證明 XMLHttpRequest

如果可以反向操作,系統會將舊版 API 更新為使用承諾 。「XMLHttpRequest」是最佳候選人,但在平均值中 我們來撰寫一個簡單的函式來發出 GET 要求:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

現在,我們來使用:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

現在我們不用手動輸入 XMLHttpRequest 就能發出 HTTP 要求。太棒了,因為 少了我親眼見證 XMLHttpRequest 令人毛骨悚然的事,我的人生就會變得更開心。

鏈結

then() 不是故事的終點,您可以將 then 鏈結在一起 轉換值,或是依序執行其他非同步動作。

轉換值

您只要傳回新值即可轉換值:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

舉例來說,讓我們回顧一下:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

回應為 JSON,但我們現在接收到純文字格式。三 可以變更 get 函式來使用 JSON responseType、 但我們也可以在承諾中解決該問題:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

由於 JSON.parse() 會使用單一引數並傳回轉換後的值, 我們可以建立捷徑:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

事實上,我們能夠輕易讓 getJSON() 函式變得十分簡單:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON() 仍會傳回承諾,且會擷取網址並剖析 以 JSON 格式回應

將非同步動作排入佇列

您也可以鏈結 then,依序執行非同步動作。

then() 回呼傳回內容時,有點神奇的作用。 如果傳回值,系統會使用該值呼叫下一個 then()。不過 如果您傳回類似承諾的內容,下一個 then() 就會等候該結果,且 只有在保證和解結果 (成功/失敗) 時才會呼叫。例如:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

我們在此向 story.json 發出非同步要求,以取得一組 收到要求的網址,然後我們會要求第一個網址。適用於保證 確實會從簡單的回呼模式中脫穎而出

你甚至可以建立捷徑方法,以便取得章節:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

系統會在呼叫 getChapter 前下載 story.json,但接下來會下載 我們重複使用故事承諾,因此 story.jsongetChapter 只會擷取一次哇,我很厲害!

處理錯誤

如先前所述,then() 會採用兩個引數,一個代表成功,一個 失敗 (或履行與拒絕,在保證詞語中):

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

您也可以使用 catch()

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch()」這東西沒有什麼特別之處,只是要跟進去 then(undefined, func),但更易讀。您會發現這兩個程式碼 上述範例的行為不相同,後者等同於:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

差別在於極小,但非常實用。Promise 遭拒項目略過 轉送至下一個 then(),並使用拒絕回呼 (或 catch(),因為 等於只要使用 then(func1, func2)func1func2將 呼叫,永遠不會同時呼叫兩者但如果使用 then(func1).catch(func2),則兩者都會 如果 func1 拒絕,就會呼叫此方法,因為在鏈結中屬於獨立步驟。搭乘 包括:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

上述流程與一般的 JavaScript try/catch 非常類似, 在「嘗試」內發生請立即前往 catch() 區塊接著來介紹 顯示為流程圖 (因為我喜歡流程圖):

遵照藍色線條表示能履行的承諾,否則請遵照紅色線條表示 拒絕。

JavaScript 例外狀況和保證

拒絕會在承諾中明確遭拒 (但暗示性) 發生 建構函式回呼中擲回錯誤時的結果:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

也就是說,在 因此能自動擷取錯誤 就會遭到拒絕

then() 回呼擲回的錯誤也是如此。

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

實務中的錯誤處理方式

我們可以透過故事和章節,用擷取的方式向使用者顯示錯誤訊息:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

如果無法擷取 story.chapterUrls[0] (例如 http 500 或使用者離線), 就會略過後續所有成功回呼,其中包含 getJSON(),會嘗試將回應剖析為 JSON,而且也會略過 這個回呼函式會將 chapter1.html 新增至網頁。而是改採漁獲 回呼。這會導致系統顯示「無法顯示章節」就會加進頁面 先前的任何動作都失敗。

就像 JavaScript 的 try/catch 一樣,偵測錯誤後也會擷取後續的程式碼 繼續消失,所以旋轉圖示一直處於隱藏狀態,這是我們要的。 將成為

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

你可能只想將 catch() 用於記錄,不必復原 錯誤。要這麼做,只要再次擲回錯誤即可。我們可以 我們的 getJSON() 方法:

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

我們已經設法擷取了一章,但必須全部加入。開始吧 一些經驗

平行處理和序列:充分運用

想採取非同步的做法並不容易。既然很難停止遊戲 請試著撰寫程式碼,就像編寫同步一樣。在這種情況下:

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

成功了!但會在下載時同步並鎖定瀏覽器。目的地: 我們會使用 then(),以非同步的方式進行這項作業。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

但該如何循環播放特定章節網址,並依序擷取內容?這個 錯誤情形

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach 無法以非同步方式辨識,因此章節會以任何順序顯示 這基本上就是《Pulp 很多》的寫作方式這並不是 青春喜劇,我們一起來修正。

建立序列

我們希望將 chapterUrls 陣列轉換為一系列承諾。我們可以使用 then() 來完成這項操作:

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

這是我們第一次看到 Promise.resolve(),它會建立 而是能夠解析您所指定的價值如果將 Promise 的例項會直接傳回 (注意:這是 例如某些實作項目尚未遵循的規格)。如果發生以下情況: 傳遞類似承諾的內容 (包含 then() 方法) 便會建立 以相同方式執行/拒絕的正版 Promise。如果通過 任何其他值 (例如Promise.resolve('Hello') 後,系統會建立 保證可以達到該價值如果直接呼叫函式沒有價值 工作,便會使用「未定義」來執行。

而且 Promise.reject(val) 會建立拒絕接受規則的承諾 您指定 (或未定義) 的值

您可以使用 array.reduce:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

這個做法與上一個範例相同,但不需要個別 「序列」變數。陣列中的每個項目都會呼叫減少回呼。 「序列」第一次使用 Promise.resolve(),但在其餘時間 呼叫「sequence」是上一次呼叫傳回的任何值。array.reduce 這個方法非常適合用來將陣列輸出為單一值 確實是個承諾

總結:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

這就是同步版本的同步版本但我們也可以 目前的頁面下載作業如下:

瀏覽器很擅長同時下載多個項目, 逐一下載章節,進一步提升收益。我們的目標是 同時下載它們,到貨後再進行處理。 幸好有 API 可以:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all 會採納一系列承諾,並提出能履行的承諾 但當所有工作都順利完成時您會得到結果陣列 (無論 保證範圍內)。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

視網路連線狀況而定 與逐一載入 這比我們第一次嘗試的程式碼少章節可任意下載 但會以正確順序顯示。

不過,我們依然可以改善使用者感知的成效。第一章抵達時 應該將程式碼新增至網頁。如此一來,使用者就能在其餘時間之前開始閱讀 許多章節都來了。第三章抵達時,我們不會將它新增到 因為使用者可能並不瞭解到缺少第 2 章進入第二章時 我們可以加入 2 個和 3 個章節,以此類推

為此,我們會同時擷取所有章節的 JSON,然後建立 新增到文件中:

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

結果顯示,兩人都大功告成!放送所需時間相同 但使用者還是能更快取得內容的第一部分。

在這個簡單的範例中,所有章節都在相同時間抵達,但 一次顯示一個廣告的好處,將大幅增加 也會透過這個系列

使用 Node.js 樣式回呼或 附近的活動 但更重要的是不易於遵循不過, 與其他 ES6 功能結合時, 變得更加簡單

額外獎勵:擴充功能

我原本撰寫了這篇文章,因此 Promise 使用的功能已擴大 。自 Chrome 55 版起,非同步函式允許以承諾為基準的程式碼 撰寫類似同步的寫入,但不會封鎖主執行緒。你可以 詳情請參閱非同步函式說明文章。還有 廣泛支援主要瀏覽器中的 Promise 和 async 函式。 您可以在 MDN 的 PromiseAsync 函式 參照。

感謝 Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp 和 Addy Osmani、Arthur Evans 和 Yutaka Hirano 完成校對程序 修正/建議

此外,感謝 Mathias Bynens 貢獻一己之力 更新應用程式 文章的部分