ループで context.sync メソッドを使用しないでください

注:

この記事では、バッチ システムを使用して Office ドキュメントを操作する 4 つのアプリケーション固有の Office JavaScript API (Excel、Word、OneNote、Visio) の少なくとも 1 つを操作する最初の段階を超えているものとします。 特に、 の context.sync 呼び出しが何を行うかを把握し、コレクション オブジェクトが何であるかを把握する必要があります。 その段階でない場合は、「 Office JavaScript API について」と、その記事の「アプリケーション固有」の下にリンクされているドキュメントを参照してください。

アプリケーション固有の API モデル (Excel、Word、PowerPoint、OneNote、Visio) のいずれかを使用する Office アドインのプログラミング シナリオによっては、コードでコレクション オブジェクトのすべてのメンバーからプロパティの読み取り、書き込み、または処理を行う必要があります。 たとえば、特定のテーブル列内のすべてのセルの値を取得する必要がある Excel アドインや、ドキュメント内の文字列のすべてのインスタンスを強調表示する必要があるWordアドインなどです。 コレクション オブジェクトのプロパティ内のメンバーを items 反復処理する必要がありますが、パフォーマンス上の理由から、ループのすべての反復でを呼び出 context.sync さないようにする必要があります。 のすべての context.sync 呼び出しは、アドインから Office ドキュメントへのラウンド トリップです。 繰り返されるラウンド トリップはパフォーマンスを損ないます。特に、ラウンド トリップがインターネット経由で行われるため、アドインがOffice on the webで実行されている場合。

注:

この記事のすべての例ではループを使用 for しますが、説明されているプラクティスは、次のような配列を反復処理できる任意のループ ステートメントに適用されます。

  • for
  • for of
  • while
  • do while

また、関数が渡され、配列内の項目に適用される任意の配列メソッドにも適用されます。これには、次のものが含まれます。

  • Array.every
  • Array.forEach
  • Array.filter
  • Array.find
  • Array.findIndex
  • Array.map
  • Array.reduce
  • Array.reduceRight
  • Array.some

ドキュメントへの書き込み

最も簡単なケースでは、コレクション オブジェクトのメンバーにのみ書き込み、プロパティの読み取りは行いません。 たとえば、次のコードでは、Word ドキュメント内の "the" のすべてのインスタンスが黄色で強調表示されています。

注:

一般に、アプリケーションrun関数の終了 "}" 文字 (、、などWord.runExcel.run) の直前に最終的context.syncな文字を設定することをお勧めします。 これは、関数が、同期されていないキューに入っているコマンドがある場合にのみ、最後の処理として の非表示の呼び出しcontext.syncを行うためrunです。 この呼び出しが非表示になっているという事実は混乱する可能性があるため、通常は 明示的 context.syncな を追加することをお勧めします。 ただし、この記事では の呼び出しを最小限に抑えることに関する記事を考えると、完全に不要な最後context.synccontext.syncを追加すると、実際には混乱が生じます。 そのため、この記事では、 の末尾に同期されていないコマンドがない場合は、除外します run

await Word.run(async function (context) {
  let startTime, endTime;
  const docBody = context.document.body;

  // search() returns an array of Ranges.
  const searchResults = docBody.search('the', { matchWholeWord: true });
  searchResults.load('font');
  await context.sync();

  // Record the system time.
  startTime = performance.now();

  for (let i = 0; i < searchResults.items.length; i++) {
    searchResults.items[i].font.highlightColor = '#FFFF00';

    await context.sync(); // SYNCHRONIZE IN EACH ITERATION
  }
  
  // await context.sync(); // SYNCHRONIZE AFTER THE LOOP

  // Record the system time again then calculate how long the operation took.
  endTime = performance.now();
  console.log("The operation took: " + (endTime - startTime) + " milliseconds.");
})

上記のコードは、Windows 上の Word で 200 個のインスタンスが "the" のドキュメントで完了するまでに 1 秒かかりました。 ただし、ループ内の await context.sync(); 行がコメントアウトされ、ループのコメントが解除された直後に同じ行が行われると、操作に要した時間は 1/10 秒でした。 Web 上のWord (ブラウザーとして Edge を使用) では、ループ内の同期に 3 秒、ループ後の同期で 1 秒の 6/10 分の 6 分の 1 しかかからなりました(約 5 倍)。 "the" のインスタンスが 2000 個あるドキュメントでは、ループ内の同期に 80 秒(web 上のWord)、ループ後の同期で約 4 秒、約 20 倍の時間がかかりました。

注:

同期が同時に実行された場合に、sync-inside-the-loop バージョンがより高速に実行されるかどうかを確認する価値があります。これは、 のcontext.sync()前面からキーワード (keyword)をawait削除するだけで実行できます。 これにより、ランタイムは同期を開始し、同期の完了を待たずにループの次のイテレーションをすぐに開始します。 ただし、このような理由から、これはループの外に context.sync 完全に移動するのと同じくらい良い解決策ではありません。

  • 同期バッチ ジョブのコマンドがキューに入っているのと同様に、バッチ ジョブ自体は Office にキューに入れられますが、Office ではキュー内のバッチ ジョブは 50 個以下でサポートされます。 それ以上はエラーをトリガーします。 そのため、ループ内に 50 回を超える繰り返しがある場合は、キュー のサイズを超える可能性があります。 反復回数が多いほど、このようなことが起こる可能性が高くなります。
  • "同時実行" は、同時に意味するものではありません。 複数の同期操作を実行するよりも、複数の同期操作を実行するよりも時間がかかります。
  • 同時実行操作は、開始した順序と同じ順序で完了することは保証されません。 前の例では、"the" という単語が強調表示される順序は関係ありませんが、コレクション内の項目を順番に処理することが重要なシナリオがあります。

分割ループ パターンを使用してドキュメントから値を読み取る

ループ内での s の context.sync回避は、コードがコレクション項目のプロパティを 読み取 る必要があるときに、それぞれを処理するときに、より困難になります。 コードで、Word ドキュメント内のすべてのコンテンツ コントロールを反復処理し、各コントロールに関連付けられている最初の段落のテキストをログに記録する必要があるとします。 プログラミングの本能により、コントロールをループし、各 (最初の) 段落のプロパティを text 読み込み、 を呼び出して context.sync 、ドキュメントのテキストをプロキシ 段落オブジェクトに設定し、ログに記録することができます。 次に例を示します。

Word.run(async (context) => {
    const contentControls = context.document.contentControls.load('items');
    await context.sync();

    for (let i = 0; i < contentControls.items.length; i++) {
      const paragraph = contentControls.items[i].getRange('Whole').paragraphs.getFirst();
      paragraph.load('text');
      await context.sync();
      console.log(paragraph.text);
    }
});

このシナリオでは、ループ内に を context.sync 含めないように、 分割ループ パターンを呼び出すパターンを使用する必要があります。 パターンの正式な説明に進む前に、パターンの具体的な例を見てみましょう。 上記のコード スニペットに分割ループ パターンを適用する方法を次に示します。 このコードについては、次の点に注意してください。

  • ループが 2 つ context.sync あり、その間に が入ってくるので、どちらのループの内部にも存在しません context.sync
  • 最初のループはコレクション オブジェクト内の項目を反復処理し、元のループと同じようにプロパティを読み込みますtextが、最初のループにはプロキシ オブジェクトのプロパティを設定textするための がcontext.sync含まれるので、段落テキストをログにparagraph記録できません。 代わりに、 オブジェクトを paragraph 配列に追加します。
  • 2 番目のループは、最初のループによって作成された配列を反復処理し、各項目paragraphの をログにtext記録します。 これは、2 つのループの context.sync 間に来た がすべてのプロパティに設定されているためです text
Word.run(async (context) => {
    const contentControls = context.document.contentControls.load("items");
    await context.sync();

    const firstParagraphsOfCCs = [];
    for (let i = 0; i < contentControls.items.length; i++) {
      const paragraph = contentControls.items[i].getRange('Whole').paragraphs.getFirst();
      paragraph.load('text');
      firstParagraphsOfCCs.push(paragraph);
    }

    await context.sync();

    for (let i = 0; i < firstParagraphsOfCCs.length; i++) {
      console.log(firstParagraphsOfCCs[i].text);
    }
});

前の例では、 を含む context.sync ループを分割ループ パターンに変換するための次の手順を示します。

  1. ループを 2 つのループに置き換えます。
  2. コレクションを反復処理し、各項目を配列に追加する最初のループをCreateし、コードで読み取る必要がある項目のプロパティも読み込みます。
  3. 最初のループの後に を呼び出して context.sync 、読み込まれたプロパティをプロキシ オブジェクトに設定します。
  4. 2 番目のループで に context.sync 従って、最初のループで作成された配列を反復処理し、読み込まれたプロパティを読み取ります。

関連付けられたオブジェクト パターンを使用してドキュメント内のオブジェクトを処理する

コレクション内の項目を処理するには、アイテム自体にないデータが必要になる、より複雑なシナリオを考えてみましょう。 このシナリオでは、テンプレートから作成されたドキュメントに対して、定型テキストを使用して操作するWord アドインを想定しています。 テキストに散らばっているのは、"{コーディネーター}"、"{Deputy}"、"{Manager}" のプレースホルダー文字列の 1 つ以上のインスタンスです。 アドインは、各プレースホルダーを一部のユーザーの名前に置き換えます。 この記事では、アドインの UI は重要ではありません。 たとえば、3 つのテキスト ボックスを含む作業ウィンドウがあり、それぞれにプレースホルダーの 1 つでラベルが付けられます。 ユーザーは、各テキスト ボックスに名前を入力し、[ 置換 ] ボタンを押します。 ボタンのハンドラーは、名前をプレースホルダーにマップする配列を作成し、各プレースホルダーを割り当てられた名前に置き換えます。

コードを試すために、この UI でアドインを実際に生成する必要はありません。 Script Lab ツールを使用して、重要なコードをプロトタイプ作成できます。 マッピング配列を作成するには、次の代入ステートメントを使用します。

const jobMapping = [
        { job: "{Coordinator}", person: "Sally" },
        { job: "{Deputy}", person: "Bob" },
        { job: "{Manager}", person: "Kim" }
    ];

次のコードは、ループ内で使用 context.sync した場合に、各プレースホルダーを割り当てられた名前に置き換える方法を示しています。

Word.run(async (context) => {

    for (let i = 0; i < jobMapping.length; i++) {
      let options = Word.SearchOptions.newObject(context);
      options.matchWildCards = false;
      let searchResults = context.document.body.search(jobMapping[i].job, options);
      searchResults.load('items');

      await context.sync(); 

      for (let j = 0; j < searchResults.items.length; j++) {
        searchResults.items[j].insertText(jobMapping[i].person, Word.InsertLocation.replace);

        await context.sync();
      }
    }
});

前のコードでは、外側と内部ループがあります。 それぞれに が含まれています context.sync。 この記事の最初のコード スニペットに基づいて、内部ループ内の は context.sync 、内部ループの後の に移動できる可能性があります。 しかし、それでもコードは外側のループに( そのうちの2つ)を残 context.sync します。 次のコードは、ループから削除 context.sync する方法を示しています。 コードについては後で説明します。

Word.run(async (context) => {

    const allSearchResults = [];
    for (let i = 0; i < jobMapping.length; i++) {
      let options = Word.SearchOptions.newObject(context);
      options.matchWildCards = false;
      let searchResults = context.document.body.search(jobMapping[i].job, options);
      searchResults.load('items');
      let correlatedSearchResult = {
        rangesMatchingJob: searchResults,
        personAssignedToJob: jobMapping[i].person
      }
      allSearchResults.push(correlatedSearchResult);
    }

    await context.sync()

    for (let i = 0; i < allSearchResults.length; i++) {
      let correlatedObject = allSearchResults[i];

      for (let j = 0; j < correlatedObject.rangesMatchingJob.items.length; j++) {
        let targetRange = correlatedObject.rangesMatchingJob.items[j];
        let name = correlatedObject.personAssignedToJob;
        targetRange.insertText(name, Word.InsertLocation.replace);
      }
    }

    await context.sync();
});

コードでは、分割ループ パターンが使用されることに注意してください。

  • 前の例の外側のループは、2 つに分割されています。 (2 番目のループには内部ループがあります。これは、コードが一連のジョブ (またはプレースホルダー) を反復処理しており、そのセット内で一致する範囲を反復処理するためです)。
  • 各メジャー ループの context.sync 後には が存在しますが、ループ内にはありません context.sync
  • 2 番目のメジャー ループは、最初のループで作成された配列を反復処理します。

ただし、最初のループで作成された配列には、最初のループが分割ループ パターンを持つドキュメントからの値の読み取りセクションで行ったように、Office オブジェクトのみが含まれていません。 これは、Word Range オブジェクトの処理に必要な情報の一部が Range オブジェクト自体ではなく、配列からjobMapping取得されるためです。

そのため、最初のループで作成された配列内のオブジェクトは、2 つのプロパティを持つカスタム オブジェクトです。 1 つ目は、特定のジョブ タイトル (プレースホルダー文字列) と一致するWord範囲の配列で、2 つ目はジョブに割り当てられたユーザーの名前を示す文字列です。 これにより、特定の範囲を処理するために必要なすべての情報が、範囲を含む同じカスタム オブジェクトに含まれているため、最終的なループは簡単に書き込み、読みやすくなります。 correlatedObject.rangesMatchingJob.items[j] を置き換える必要がある名前は、同じオブジェクトのもう 1 つのプロパティである、correlatedObject.personAssignedToJob です

この分割ループ パターンのバリエーションを 、相関オブジェクト パターンと呼びます。 一般的な考え方は、最初のループがカスタム オブジェクトの配列を作成することです。 各オブジェクトには、値が Office コレクション オブジェクト内の項目の 1 つ (またはそのような項目の配列) であるプロパティがあります。 カスタム オブジェクトには他のプロパティがあり、それぞれが最後のループで Office オブジェクトを処理するために必要な情報を提供します。 カスタム相関オブジェクトに 2 つ以上のプロパティがある例へのリンクについては、「 これらのパターンのその他の例 」セクションを参照してください。

もう 1 つの注意点: カスタム相関オブジェクトの配列を作成するために、複数のループが必要な場合があります。 これは、別のコレクション オブジェクトの処理に使用される情報を収集するために、1 つの Office コレクション オブジェクトの各メンバーのプロパティを読み取る必要がある場合に発生する可能性があります。 (たとえば、アドインは、その列のタイトルに基づいて一部の列のセルに数値形式を適用するため、Excel テーブル内のすべての列のタイトルを読み取る必要があります)。ただし、ループ内ではなく、ループ間で常に s を保持 context.syncできます。 例については、「 これらのパターンのその他の例 」セクションを参照してください。

これらのパターンのその他の例

  • ループを使用 Array.forEach する Excel の非常に単純な例については、この Stack Overflow の質問に対して受け入れられた答えを参照してください。 context.sync の前に複数の context.load をキューに入れることはできますか?
  • ループを使用し、構文を使用Array.forEachawaitasync/しないWordの簡単な例については、この Stack Overflow の質問に対する受け入れられた答えを参照してください。Office JavaScript API を使用してコンテンツ コントロールを使用してすべての段落を反復処理します
  • TypeScript で記述されたWordの例については、アドイン Angular2 スタイル チェッカー (にファイル word.document.service.ts) Wordサンプルを参照してください。 ループと Array.forEach ループがfor混在しています。
  • 高度なWordサンプルについては、この gistScript Lab ツールにインポートします。 gist の使用に関するコンテキストについては、テキストの置換後に Stack Overflow の質問 Document が同期していないという問題に対して受け入れられた回答を参照してください。 このサンプルでは、3 つのプロパティを持つカスタム相関オブジェクト型を作成します。 合計 3 つのループを使用して相関オブジェクトの配列を構築し、さらに 2 つのループを使用して最終的な処理を行います。 ループと Array.forEach ループがfor混在しています。
  • 分割ループまたは相関オブジェクト パターンの例ではありませんが、セル値のセットを 1 つだけ context.syncで他の通貨に変換する方法を示す高度な Excel サンプルがあります。 試すには、Script Lab ツールを開き、Currency Converter サンプルを検索して移動します。

この記事のパターンを使用 すべきでない のはいつですか?

の特定の呼び出し context.syncでは、Excel で 5 MB を超えるデータを読み取ることはできません。 この制限を超えると、エラーがスローされます。 (詳細については、「Office アドインの リソース制限とパフォーマンスの最適化 」の「Excel アドイン」セクションを参照してください)。この制限に近づくことは非常にまれですが、アドインでこれが発生する可能性がある場合は、コードですべてのデータを 1 つのループに読み込 んで ループ context.syncに従うべきではありません。 ただし、コレクション オブジェクトに対して context.sync ループを繰り返すたびに、 が発生しないようにする必要があります。 代わりに、コレクション内の項目のサブセットを定義し、ループ間の を context.sync 使用して、各サブセットをループします。 これは、サブセットを反復処理し、これらの外側の各イテレーションに を context.sync 含む外部ループで構成できます。