教程:生成邮件撰写 Outlook 外接程序

本教程将教你如何生成一个可用于在邮件撰写模式下将内容插入到邮件正文中的 Outlook 外接程序。

在本教程中,你将:

  • 创建 Outlook 外接程序项目
  • 定义在撰写消息窗口中显示的按钮
  • 实现从用户处收集信息并从外部服务提取数据的首次运行体验
  • 实现可调用函数的无 UI 按钮
  • 实现将内容插入到邮件正文中的任务窗格

提示

如果需要使用 XML 清单) (本教程的完整版本,请转到 GitHub 上的 Office 外接程序示例存储库

先决条件

  • Node.js(最新LTS 版本)。 访问 Node.js 站点 ,下载并安装适合你的操作系统的版本。

  • 最新版本的 Yeoman 和适用于 Office 加载项的 Yeoman 生成器。若要全局安装这些工具,请从命令提示符处运行以下命令。

    npm install -g yo generator-office
    

    注意

    即便先前已安装了 Yeoman 生成器,我们还是建议你通过 npm 将包更新为最新版本。

  • 已连接到 Microsoft 365 订阅的 Office (包括 Office 网页版)。

    注意

    如果你还没有 Office,你可能有资格通过 Microsoft 365 开发人员计划获得Microsoft 365 E5开发人员订阅;有关详细信息,请参阅常见问题解答。 或者,可以 注册 1 个月的免费试用版购买 Microsoft 365 计划

设置

你将在本教程中创建的外接程序将从用户的 GitHub 帐户读取 gist,并将所选 gist 添加到邮件正文中。 完成以下步骤以创建两个新 gist,你可以使用它们来测试你要生成的外接程序。

  1. 登录 GitHub

  2. 创建一个新 gist

    • Gist description... 字段中,输入 Hello World Markdown

    • Filename including extension... 字段中,输入 test.md

    • 将以下 Markdown 添加到多行文本框。

      # Hello World
      
      This is content converted from Markdown!
      
      Here's a JSON sample:
      
        ```json
        {
          "foo": "bar"
        }
        ```
      
    • 选择“创建公用 gist”按钮。

  3. 创建另一个新 gist

    • Gist description... 字段中,输入 Hello World Html

    • Filename including extension... 字段中,输入 test.html

    • 将以下 Markdown 添加到多行文本框。

      <html>
        <head>
          <style>
          h1 {
            font-family: Calibri;
          }
          </style>
        </head>
        <body>
          <h1>Hello World!</h1>
          <p>This is a test</p>
        </body>
      </html>
      
    • 选择“创建公用 gist”按钮。

创建 Outlook 外接程序项目

  1. 运行以下命令,使用 Yeoman 生成器创建加载项项目。 包含项目的文件夹将添加到当前目录。

    yo office
    

    注意

    运行该yo office命令时,可能会收到有关 Yeoman 和 Office 加载项 CLI 工具的数据收集策略的提示。 根据你的需要,使用提供的信息来响应提示。

    出现提示时,请提供以下信息以创建加载项项目。

  2. 创建项目的步骤略有不同,具体取决于清单的类型。

    注意

    Microsoft 365 的统一清单使你可以将 Outlook 外接程序与 Teams 应用合并为一个开发和部署单元。 我们正在努力将统一清单的支持扩展到 Excel、PowerPoint、Word、自定义 Copilot 开发以及 Microsoft 365 的其他扩展。 有关它的详细信息,请参阅 具有统一清单的 Office 加载项。 有关组合的 Teams 应用和 Outlook 外接程序的示例,请参阅 折扣套餐

    我们乐于获得有关统一清单的反馈。 如果有任何建议,请在 Office JavaScript 库的存储库中创建问题。

    • 选择项目类型 - Office Add-in Task Pane project

    • 选择脚本类型 - JavaScript

    • 要如何命名加载项? - Git the gist

    • 要支持哪一个 Office 客户端应用程序? - Outlook

    • 要使用哪个清单? - unified manifest for Microsoft 365

      Yeoman 生成器的提示和答案,其中选择了统一清单和 JavaScript 选项。

    完成此向导后,生成器会创建项目,并安装支持的 Node 组件。

    注意

    如果使用 Node.js 20.0.0 或更高版本,则当生成器运行安装时,可能会看到一条警告,指出你的引擎不受支持。 我们正在努力解决此问题。 同时,警告不会影响生成的生成器或项目,因此可以忽略它。

    提示

    创建加载项项目后,可忽略 Yeoman 生成器提供的后续步骤指南。 本文中的分步说明提供了完成本教程所需的全部指南。

  3. 导航到项目的根目录。

    cd "Git the gist"
    
  4. 此加载项使用以下库。

    • 用于将 Markdown 转换成 HTML 的 Showdown 库。
    • 用于生成相关 URL 的 URI.js 库。
    • 用于简化 DOM 交互的 jQuery 库。

    若要为你的项目安装这些工具,请在项目的根目录中运行以下命令。

    npm install showdown urijs jquery --save
    
  5. 在 VS Code 或首选代码编辑器中打开项目。

    提示

    在 Windows 上,可通过命令行导航到项目的根目录,然后输入 code .在 VS Code 中打开该文件夹。 在 Mac 上,需要先code 命令添加到路径中,然后才可使用该命令在 VS Code 中打开项目文件夹。

更新清单

外接程序的清单控制外接程序在 Outlook 中的显示方式。 它定义外接程序在外接程序列表中的显示方式和功能区上显示的按钮,并设置外接程序使用的 HTML 和 JavaScript 文件的 URL。

指定基本信息

在清单文件中进行以下更新,以指定有关加载项的一些基本信息。

  1. 找到“description”属性,将默认的“short”和“long”值替换为加载项的说明,并保存文件。

    "description": {
        "short": "Gets gists.",
        "full": "Allows users to access their GitHub gists."
    },
    
  2. 保存文件。

测试生成的外接程序

在继续之前,让我们测试生成器创建的基本外接程序,以确认项目已正确设置。

注意

即使在开发过程中,Office 外接程序也应使用 HTTPS,而不是 HTTP。 如果在运行以下命令之一后系统提示安装证书,请接受安装 Yeoman 生成器提供的证书的提示。 你可能还必须以管理员身份运行命令提示符或终端才能进行更改。

  1. 在项目的根目录中运行以下命令。 运行此命令时,本地 Web 服务器将启动,加载项将旁加载。

    npm start
    

    注意

    如果加载项未自动旁加载,请按照 旁加载 Outlook 外接程序 中的说明进行测试,在 Outlook 中手动旁加载加载项。

  2. 在 Outlook 中,打开现有邮件,然后选择“显示任务窗格”按钮。

  3. 当系统提示“Web 视图在加载时停止”对话框时,请选择“确定”。

    注意

    如果选择“取消”,则当加载项的此实例正在运行时,将不会再次显示该对话框。 但如果重新启动加载项,则会再次看到该对话框。

    如果一切设置正确,任务窗格将打开并呈现加载项的欢迎页。

    示例添加的“显示任务窗格”按钮和 Git gist 任务窗格。

定义按钮

至此,已经验证基本外接程序可正常运行,你可以对其进行自定义以添加更多功能。 默认情况下,清单仅定义“读取邮件”窗口的按钮。 让我们更新清单以从“读取邮件”窗口中删除按钮,并为“撰写邮件”窗口定义两个新按钮:

  • 插入 gist:用于打开任务窗格的按钮

  • 插入默认 gist:用于调用函数的按钮

该过程取决于你正在使用的清单。

执行以下步骤:

  1. 打开 manifest.json 文件。

  2. 在“extensions.runtimes”数组中,有两个运行时对象。 对于第二个,“id”为“CommandsRuntime”,将“actions.id”更改为“insertDefaultGist”。 这是在后续步骤中创建的函数的名称。 完成后,运行时对象应如下所示:

    {
        "id": "CommandsRuntime",
        "type": "general",
        "code": {
            "page": "https://localhost:3000/commands.html",
            "script": "https://localhost:3000/commands.js"
        },
        "lifetime": "short",
        "actions": [
            {
                "id": "insertDefaultGist",
                "type": "executeFunction",
                "displayName": "action"
            }
        ]
    }
    
  3. 将“extensions.ribbons.contexts”数组中的项更改为“mailCompose”。 这意味着按钮将仅出现在新邮件或答复窗口中。

    "contexts": [
        "mailCompose"
    ],
    
  4. “extensions.ribbons.tabs.groups”数组包含一个 group 对象。 对此对象进行以下更改。

    1. 将“id”属性更改为“msgComposeCmdGroup”。
    2. 将“label”属性更改为“Git the gist”。
  5. 同一组对象具有一个包含两个控件对象的“controls”数组。 我们需要对其中每个项的 JSON 进行更改。 在第一个步骤中,执行以下步骤。

    1. 将“id”更改为“msgComposeInsertGist”。
    2. 将“label”更改为“Insert gist”。
    3. 将“supertip.title”更改为“Insert gist”。
    4. 将“supertip.description”更改为“显示 gist 列表,并允许将其内容插入当前邮件中。
  6. 在第二个控件对象中,执行以下步骤。

    1. 将“id”更改为“msgComposeInsertDefaultGist”。
    2. 将“label”更改为“Insert default gist”。
    3. 将“supertip.title”更改为“插入默认 gist”。
    4. 将“supertip.description”更改为“将标记为默认的 gist 内容插入当前邮件中”。
    5. 将“actionId”更改为“insertDefaultGist”。 这与前面步骤中设置的“CommandsRuntime”的“action.id”匹配。

    完成后,“功能区”属性应如下所示:

    "ribbons": [
        {
            "contexts": [
                "mailCompose"
            ],
            "tabs": [
                {
                    "builtInTabId": "TabDefault",
                    "groups": [
                        {
                            "id": "msgComposeCmdGroup",
                            "label": "Git the gist",
                            "icons": [
                                {
                                    "size": 16,
                                    "file": "https://localhost:3000/assets/icon-16.png"
                                },
                                {
                                    "size": 32,
                                    "file": "https://localhost:3000/assets/icon-32.png"
                                },
                                {
                                    "size": 80,
                                    "file": "https://localhost:3000/assets/icon-80.png"
                                }
                            ],
                            "controls": [
                                {
                                    "id": "msgComposeInsertGist",
                                    "type": "button",
                                    "label": "Insert gist",
                                    "icons": [
                                        {
                                            "size": 16,
                                            "file": "https://localhost:3000/assets/icon-16.png"
                                        },
                                        {
                                            "size": 32,
                                            "file": "https://localhost:3000/assets/icon-32.png"
                                        },
                                        {
                                            "size": 80,
                                            "file": "https://localhost:3000/assets/icon-80.png"
                                        }
                                    ],
                                    "supertip": {
                                        "title": "Insert gist",
                                        "description": "Displays a list of your gists and allows you to insert their contents into the current message."
                                    },
                                    "actionId": "TaskPaneRuntimeShow"
                                },
                                {
                                    "id": "msgComposeInsertDefaultGist",
                                    "type": "button",
                                    "label": "Insert default gist",
                                    "icons": [
                                        {
                                            "size": 16,
                                            "file": "https://localhost:3000/assets/icon-16.png"
                                        },
                                        {
                                            "size": 32,
                                            "file": "https://localhost:3000/assets/icon-32.png"
                                        },
                                        {
                                            "size": 80,
                                            "file": "https://localhost:3000/assets/icon-80.png"
                                        }
                                    ],
                                    "supertip": {
                                        "title": "Insert default gist",
                                        "description": "Inserts the content of the gist you mark as default into the current message."
                                    },
                                    "actionId": "insertDefaultGist"
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
    
  7. 将更改保存到清单。

重新安装外接程序

必须重新安装外接程序,清单更改才能生效。

  1. 如果 Web 服务器正在运行,请运行以下命令。

    npm stop
    
  2. 运行以下命令以启动本地 Web 服务器并自动旁加载外接程序。

    npm start
    

重新安装外接程序后,可以通过在“邮件撰写”窗口中检查“插入 gist”和“插入默认 gist”命令来验证是否已成功安装。 请注意,即使你选择了其中任何一项,系统也不会执行任何操作,因为你尚未完成生成此外接程序的操作。

  • 如果你在 Windows Outlook 2016 或更高版本中运行此加载项,则应在撰写消息窗口的功能区上看到两个新按钮:“插入 gist”和“插入默认 gist”。

    经典 Outlook on Windows 中的功能区溢出菜单,其中突出显示了加载项的按钮。

  • 如果你在 Outlook 网页版 或新的 Outlook on Windows (预览) 中运行此加载项,请在撰写邮件窗口的功能区中选择“应用”,然后选择“Git the gist”以查看“插入 gist”和“插入默认 gist”选项。

    Outlook 网页版中的邮件撰写窗体,其中突出显示了加载项按钮和弹出菜单。

实现首次运行体验

此外接程序需要能够从用户的 GitHub 帐户中读取 gist,并确定用户选择哪一个作为默认 gist。 为了实现这些目标,外接程序必须提示用户提供其 GitHub 用户名,并从其现有 gist 集合中选择默认 gist。 完成本部分中的步骤,实现首次运行体验,该体验会显示一个对话框,用于从用户处收集此信息。

Create对话框的 UI

首先,为对话框创建 UI。

  1. ./src 文件夹中,创建名为 settings 的新子文件夹。

  2. ./src/settings 文件夹中,创建名为 dialog.html的文件。

  3. dialog.html中添加以下标记,以定义一个基本表单,其中包含 GitHub 用户名的文本输入和将通过 JavaScript 填充的 gists 的空列表。

    <!DOCTYPE html>
    <html>
    
    <head>
      <meta charset="UTF-8" />
      <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
      <title>Settings</title>
    
      <!-- Office JavaScript API -->
      <script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js"></script>
    
    <!-- For more information on Fluent UI, visit https://developer.microsoft.com/fluentui. -->
      <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/9.6.1/css/fabric.min.css"/>
    
      <!-- Template styles -->
      <link href="dialog.css" rel="stylesheet" type="text/css" />
    </head>
    
    <body class="ms-font-l">
      <main>
        <section class="ms-font-m ms-fontColor-neutralPrimary">
          <div class="not-configured-warning ms-MessageBar ms-MessageBar--warning">
            <div class="ms-MessageBar-content">
              <div class="ms-MessageBar-icon">
                <i class="ms-Icon ms-Icon--Info"></i>
              </div>
              <div class="ms-MessageBar-text">
                Oops! It looks like you haven't configured <strong>Git the gist</strong> yet.
                <br/>
                Please configure your GitHub username and select a default gist, then try that action again!
              </div>
            </div>
          </div>
          <div class="ms-font-xxl">Settings</div>
          <div class="ms-Grid">
            <div class="ms-Grid-row">
              <div class="ms-TextField">
                <label class="ms-Label">GitHub Username</label>
                <input class="ms-TextField-field" id="github-user" type="text" value="" placeholder="Please enter your GitHub username">
              </div>
            </div>
            <div class="error-display ms-Grid-row">
              <div class="ms-font-l ms-fontWeight-semibold">An error occurred:</div>
              <pre><code id="error-text"></code></pre>
            </div>
            <div class="gist-list-container ms-Grid-row">
              <div class="list-title ms-font-xl ms-fontWeight-regular">Choose Default Gist</div>
              <form>
                <div id="gist-list">
                </div>
              </form>
            </div>
          </div>
          <div class="ms-Dialog-actions">
            <div class="ms-Dialog-actionsRight">
              <button class="ms-Dialog-action ms-Button ms-Button--primary" id="settings-done" disabled>
                <span class="ms-Button-label">Done</span>
              </button>
            </div>
          </div>
        </section>
      </main>
      <script type="text/javascript" src="../../node_modules/jquery/dist/jquery.js"></script>
      <script type="text/javascript" src="../helpers/gist-api.js"></script>
      <script type="text/javascript" src="dialog.js"></script>
    </body>
    
    </html>
    

    你可能已经注意到,HTML 文件引用了一个尚不存在的 JavaScript 文件 gist-api.js。 将在下面的“从 GitHub 提取数据”部分中创建此文件。

  4. 保存所做的更改。

  5. 接下来,在名为 dialog.css./src/settings 文件夹中创建一个文件。

  6. dialog.css 中添加以下代码以指定 dialog.html使用的样式。

    section {
      margin: 10px 20px;
    }
    
    .not-configured-warning {
      display: none;
    }
    
    .error-display {
      display: none;
    }
    
    .gist-list-container {
      margin: 10px -8px;
      display: none;
    }
    
    .list-title {
      border-bottom: 1px solid #a6a6a6;
      padding-bottom: 5px;
    }
    
    ul {
      margin-top: 10px;
    }
    
    .ms-ListItem-secondaryText,
    .ms-ListItem-tertiaryText {
      padding-left: 15px;
    }
    
  7. 保存所做的更改。

开发对话框的功能

现在你已经定义了对话框 UI,可以编写使其实际执行某些操作的代码。

  1. ./src/settings 文件夹中,创建名为 dialog.js的文件。

  2. 添加以下代码。 请注意,此代码将使用 jQuery 注册事件,并使用 messageParent 方法将用户的选择发送回调用方。

    (function() {
      'use strict';
    
      // The onReady function must be run each time a new page is loaded.
      Office.onReady(function() {
        $(document).ready(function() {
          if (window.location.search) {
            // Check if warning should be displayed.
            const warn = getParameterByName('warn');
    
            if (warn) {
              $('.not-configured-warning').show();
            } else {
              // See if the config values were passed.
              // If so, pre-populate the values.
              const user = getParameterByName('gitHubUserName');
              const gistId = getParameterByName('defaultGistId');
    
              $('#github-user').val(user);
              loadGists(user, function(success) {
                if (success) {
                  $('.ms-ListItem').removeClass('is-selected');
                  $('input').filter(function() {
                    return this.value === gistId;
                  }).addClass('is-selected').attr('checked', 'checked');
                  $('#settings-done').removeAttr('disabled');
                }
              });
            }
          }
    
          // When the GitHub username changes,
          // try to load gists.
          $('#github-user').on('change', function() {
            $('#gist-list').empty();
            const ghUser = $('#github-user').val();
    
            if (ghUser.length > 0) {
              loadGists(ghUser);
            }
          });
    
          // When the Done button is selected, send the
          // values back to the caller as a serialized
          // object.
          $('#settings-done').on('click', function() {
            const settings = {};
            settings.gitHubUserName = $('#github-user').val();
            const selectedGist = $('.ms-ListItem.is-selected');
    
            if (selectedGist) {
              settings.defaultGistId = selectedGist.val();
              sendMessage(JSON.stringify(settings));
            }
          });
        });
      });
    
      // Load gists for the user using the GitHub API
      // and build the list.
      function loadGists(user, callback) {
        getUserGists(user, function(gists, error) {
          if (error) {
            $('.gist-list-container').hide();
            $('#error-text').text(JSON.stringify(error, null, 2));
            $('.error-display').show();
    
            if (callback) callback(false);
          } else {
            $('.error-display').hide();
            buildGistList($('#gist-list'), gists, onGistSelected);
            $('.gist-list-container').show();
    
            if (callback) callback(true);
          }
        });
      }
    
      function onGistSelected() {
        $('.ms-ListItem').removeClass('is-selected').removeAttr('checked');
        $(this).children('.ms-ListItem').addClass('is-selected').attr('checked', 'checked');
        $('.not-configured-warning').hide();
        $('#settings-done').removeAttr('disabled');
      }
    
      function sendMessage(message) {
        Office.context.ui.messageParent(message);
      }
    
      function getParameterByName(name, url) {
        if (!url) {
          url = window.location.href;
        }
    
        name = name.replace(/[\[\]]/g, "\\$&");
        const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
          results = regex.exec(url);
    
        if (!results) return null;
    
        if (!results[2]) return '';
    
        return decodeURIComponent(results[2].replace(/\+/g, " "));
      }
    })();
    
  3. 保存所做的更改。

更新 webpack 配置设置

最后,打开在项目的根目录中找到的 webpack.config.js 文件,并完成以下步骤。

  1. config 对象内找到 entry 对象并为 dialog 添加新条目。

    dialog: "./src/settings/dialog.js",
    

    完成此操作之后,新的 entry 对象将与此类似:

    entry: {
      polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
      taskpane: ["./src/taskpane/taskpane.js", "./src/taskpane/taskpane.html"],
      commands: "./src/commands/commands.js",
      dialog: "./src/settings/dialog.js",
    },
    
  2. config 对象中找到 plugins 数组。 在 new CopyWebpackPlugin 对象的 patterns 数组中,为 taskpane.cssdialog.css 添加新条目。

    {
      from: "./src/taskpane/taskpane.css",
      to: "taskpane.css",
    },
    {
      from: "./src/settings/dialog.css",
      to: "dialog.css",
    },
    

    完成此操作后, new CopyWebpackPlugin 对象将如下所示。 请注意,如果外接程序使用 XML 清单,则略有不同。

    new CopyWebpackPlugin({
      patterns: [
      {
        from: "./src/taskpane/taskpane.css",
        to: "taskpane.css",
      },
      {
        from: "./src/settings/dialog.css",
        to: "dialog.css",
      },
      {
        from: "assets/*",
        to: "assets/[name][ext][query]",
      },
      {
        from: "manifest*.json", // The file extension is "xml" if the XML manifest is being used.
        to: "[name]" + "[ext]",
        transform(content) {
          if (dev) {
            return content;
          } else {
            return content.toString().replace(new RegExp(urlDev, "g"), urlProd);
          }
        },
      },
    ]}),
    
  3. config 对象内的同一 plugins 数组中,将此新对象添加到数组的末尾。

    new HtmlWebpackPlugin({
      filename: "dialog.html",
      template: "./src/settings/dialog.html",
      chunks: ["polyfill", "dialog"]
    })
    

    完成此操作后,新 plugins 数组将如下所示。 请注意,如果外接程序使用 XML 清单,则略有不同。

    plugins: [
      new HtmlWebpackPlugin({
        filename: "taskpane.html",
        template: "./src/taskpane/taskpane.html",
        chunks: ["polyfill", "taskpane"],
      }),
      new CopyWebpackPlugin({
        patterns: [
          {
            from: "./src/taskpane/taskpane.css",
            to: "taskpane.css",
          },
          {
            from: "./src/settings/dialog.css",
            to: "dialog.css",
          },
          {
            from: "assets/*",
            to: "assets/[name][ext][query]",
          },
          {
            from: "manifest*.json", // The file extension is "xml" if the XML manifest is being used.
            to: "[name]." + buildType + "[ext]",
            transform(content) {
              if (dev) {
                return content;
              } else {
                return content.toString().replace(new RegExp(urlDev, "g"), urlProd);
              }
            },
          },
        ],
      }),
      new HtmlWebpackPlugin({
        filename: "commands.html",
        template: "./src/commands/commands.html",
        chunks: ["polyfill", "commands"],
      }),
      new HtmlWebpackPlugin({
        filename: "dialog.html",
        template: "./src/settings/dialog.html",
        chunks: ["polyfill", "dialog"]
      })
    ],
    

从 GitHub 提取数据

你刚刚创建的 dialog.js 文件指定外接程序应在 change 事件触发时为 GitHub 用户名字段加载 gist。 若要从 GitHub 检索用户的 gist,需使用 GitHub Gists API

  1. ./src 文件夹中,创建名为 helpers 的新子文件夹。

  2. ./src/helpers 文件夹中,创建名为 gist-api.js的文件。

  3. gist-api.js中添加以下代码以从 GitHub 检索用户的 gists 并生成 gists 列表。

    function getUserGists(user, callback) {
      const requestUrl = 'https://api.github.com/users/' + user + '/gists';
    
      $.ajax({
        url: requestUrl,
        dataType: 'json'
      }).done(function(gists) {
        callback(gists);
      }).fail(function(error) {
        callback(null, error);
      });
    }
    
    function buildGistList(parent, gists, clickFunc) {
      gists.forEach(function(gist) {
    
        const listItem = $('<div/>')
          .appendTo(parent);
    
        const radioItem = $('<input>')
          .addClass('ms-ListItem')
          .addClass('is-selectable')
          .attr('type', 'radio')
          .attr('name', 'gists')
          .attr('tabindex', 0)
          .val(gist.id)
          .appendTo(listItem);
    
        const descPrimary = $('<span/>')
          .addClass('ms-ListItem-primaryText')
          .text(gist.description)
          .appendTo(listItem);
    
        const descSecondary = $('<span/>')
          .addClass('ms-ListItem-secondaryText')
          .text(' - ' + buildFileList(gist.files))
          .appendTo(listItem);
    
        const updated = new Date(gist.updated_at);
    
        const descTertiary = $('<span/>')
          .addClass('ms-ListItem-tertiaryText')
          .text(' - Last updated ' + updated.toLocaleString())
          .appendTo(listItem);
    
        listItem.on('click', clickFunc);
      });  
    }
    
    function buildFileList(files) {
    
      let fileList = '';
    
      for (let file in files) {
        if (files.hasOwnProperty(file)) {
          if (fileList.length > 0) {
            fileList = fileList + ', ';
          }
    
          fileList = fileList + files[file].filename + ' (' + files[file].language + ')';
        }
      }
    
      return fileList;
    }
    
  4. 保存所做的更改。

  5. 运行以下命令以重建项目。

    npm run build
    

实现无 UI 按钮

此加载项的 “插入默认 gist ”按钮是调用 JavaScript 函数的无 UI 按钮,而不是像许多加载项按钮那样打开任务窗格。 当用户选择“ 插入默认 gist ”按钮时,相应的 JavaScript 函数会检查是否已配置加载项。

  • 如果加载项已配置,则函数将加载用户已选择为默认的 gist 的内容,并将其插入消息正文中。

  • 如果尚未配置加载项,则设置对话框会提示用户提供所需的信息。

更新函数文件 (HTML)

由无 UI 按钮调用的函数必须在由相应外形规格的清单中的 <FunctionFile> 元素指定的文件中定义。 此外接程序的清单指定 https://localhost:3000/commands.html 作为函数文件。

  1. 打开 ./src/commands/commands.html ,并将整个内容替换为以下标记。

    <!DOCTYPE html>
    <html>
    
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
    
        <!-- Office JavaScript API -->
        <script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js"></script>
    
        <script type="text/javascript" src="../../node_modules/jquery/dist/jquery.js"></script>
        <script type="text/javascript" src="../../node_modules/showdown/dist/showdown.min.js"></script>
        <script type="text/javascript" src="../../node_modules/urijs/src/URI.min.js"></script>
        <script type="text/javascript" src="../helpers/addin-config.js"></script>
        <script type="text/javascript" src="../helpers/gist-api.js"></script>
    </head>
    
    <body>
      <!-- NOTE: The body is empty on purpose. Since functions in commands.js are
           invoked via a button, there is no UI to render. -->
    </body>
    
    </html>
    

    你可能已注意到,HTML 文件引用了尚不存在的 JavaScript 文件 addin-config.js。 将在本教程稍后的“创建文件以管理配置设置”部分中创建此文件。

  2. 保存所做的更改。

更新函数文件 (JavaScript)

  1. 打开文件 ./src/commands/commands.js 并使用以下代码替换全部内容。 请注意,如果 insertDefaultGist 函数确定加载项尚未配置,则会将 参数添加到 ?warn=1 对话框 URL。 执行此操作可使“设置”对话框呈现在 ./src/settings/dialog.html 中定义的消息栏,告诉用户他们看到此对话框的原因。

    let config;
    let btnEvent;
    
    // The onReady function must be run each time a new page is loaded.
    Office.onReady();
    
    function showError(error) {
      Office.context.mailbox.item.notificationMessages.replaceAsync('github-error', {
        type: 'errorMessage',
        message: error
      });
    }
    
    let settingsDialog;
    
    function insertDefaultGist(event) {
      config = getConfig();
    
      // Check if the add-in has been configured.
      if (config && config.defaultGistId) {
        // Get the default gist content and insert.
        try {
          getGist(config.defaultGistId, function(gist, error) {
            if (gist) {
              buildBodyContent(gist, function (content, error) {
                if (content) {
                  Office.context.mailbox.item.body.setSelectedDataAsync(
                    content,
                    { coercionType: Office.CoercionType.Html },
                    function (result) {
                      event.completed();
                    }
                  );
                } else {
                  showError(error);
                  event.completed();
                }
              });
            } else {
              showError(error);
              event.completed();
            }
          });
        } catch (err) {
          showError(err);
          event.completed();
        }
    
      } else {
        // Save the event object so we can finish up later.
        btnEvent = event;
        // Not configured yet, display settings dialog with
        // warn=1 to display warning.
        const url = new URI('dialog.html?warn=1').absoluteTo(window.location).toString();
        const dialogOptions = { width: 20, height: 40, displayInIframe: true };
    
        Office.context.ui.displayDialogAsync(url, dialogOptions, function(result) {
          settingsDialog = result.value;
          settingsDialog.addEventHandler(Office.EventType.DialogMessageReceived, receiveMessage);
          settingsDialog.addEventHandler(Office.EventType.DialogEventReceived, dialogClosed);
        });
      }
    }
    
    // Register the function.
    Office.actions.associate("insertDefaultGist", insertDefaultGist);
    
    function receiveMessage(message) {
      config = JSON.parse(message.message);
      setConfig(config, function(result) {
        settingsDialog.close();
        settingsDialog = null;
        btnEvent.completed();
        btnEvent = null;
      });
    }
    
    function dialogClosed(message) {
      settingsDialog = null;
      btnEvent.completed();
      btnEvent = null;
    }
    
  2. 保存所做的更改。

创建文件以管理配置设置

  1. ./src/helpers 文件夹中,创建名为 addin-config.js 的文件,并添加以下代码。 此代码使用 RoamingSettings 对象来获取和设置配置值。

    function getConfig() {
      const config = {};
    
      config.gitHubUserName = Office.context.roamingSettings.get('gitHubUserName');
      config.defaultGistId = Office.context.roamingSettings.get('defaultGistId');
    
      return config;
    }
    
    function setConfig(config, callback) {
      Office.context.roamingSettings.set('gitHubUserName', config.gitHubUserName);
      Office.context.roamingSettings.set('defaultGistId', config.defaultGistId);
    
      Office.context.roamingSettings.saveAsync(callback);
    }
    
  2. 保存所做的更改。

创建新函数来处理 gist

  1. 打开 ./src/helpers/gist-api.js 文件并添加以下函数。 请注意以下事项:

    • 如果 gist 包含 HTML,则外接程序将按原样插入到邮件正文中。

    • 如果 gist 包含 Markdown,则外接程序使用 Showdown 库将 Markdown 转换为 HTML,然后将生成的 HTML 插入到邮件正文中。

    • 如果 gist 包含 HTML 或 Markdown 以外的任何内容,则外接程序会将其作为代码片段插入到邮件正文中。

    function getGist(gistId, callback) {
      const requestUrl = 'https://api.github.com/gists/' + gistId;
    
      $.ajax({
        url: requestUrl,
        dataType: 'json'
      }).done(function(gist) {
        callback(gist);
      }).fail(function(error) {
        callback(null, error);
      });
    }
    
    function buildBodyContent(gist, callback) {
      // Find the first non-truncated file in the gist
      // and use it.
      for (let filename in gist.files) {
        if (gist.files.hasOwnProperty(filename)) {
          const file = gist.files[filename];
          if (!file.truncated) {
            // We have a winner.
            switch (file.language) {
              case 'HTML':
                // Insert as is.
                callback(file.content);
                break;
              case 'Markdown':
                // Convert Markdown to HTML.
                const converter = new showdown.Converter();
                const html = converter.makeHtml(file.content);
                callback(html);
                break;
              default:
                // Insert contents as a <code> block.
                let codeBlock = '<pre><code>';
                codeBlock = codeBlock + file.content;
                codeBlock = codeBlock + '</code></pre>';
                callback(codeBlock);
            }
            return;
          }
        }
      }
      callback(null, 'No suitable file found in the gist');
    }
    
  2. 保存所做的更改。

测试“插入默认 gist”按钮

  1. 如果本地 Web 服务器尚未运行,请从命令提示符运行 npm start

  2. 打开 Outlook 并撰写一封新邮件。

  3. 在“撰写邮件”窗口中,选择“插入默认 gist”。 您应该会看到对话框,您可以在其中配置外接程序,从提示设置 GitHub 用户名开始。

    配置加载项的对话框提示。

  4. 在设置对话框中,输入 GitHub 用户名,然后在 对话框中按 Tab 或单击其他位置来调用 更改 事件,该事件应加载公共 gists 列表。 选择一个 gist 作为默认设置,然后选择“完成”。

    加载项的设置对话框。

  5. 重新选择“插入默认 gist”按钮。 此时应看到插入到电子邮件正文中的 gist 的内容。

    注意

    Windows 版 Outlook:若要获取最新设置,可能需要关闭并重新打开“撰写邮件”窗口。

实现任务窗格

此加载项的 “插入 gist ”按钮将打开任务窗格并显示用户的 gist。 然后,用户可以选择要插入到邮件正文中的其中一个 gist。 如果用户尚未配置加载项,系统将提示他们进行配置。

为任务窗格创建 HTML

  1. 在创建的项目中,任务窗格 HTML 已在文件 ./src/taskpane/taskpane.html 中指定。 打开该文件并将全部内容替换为以下标记。

    <!DOCTYPE html>
    <html>
    
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Contoso Task Pane Add-in</title>
    
        <!-- Office JavaScript API -->
        <script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js"></script>
    
       <!-- For more information on Fluent UI, visit https://developer.microsoft.com/fluentui. -->
        <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/9.6.1/css/fabric.min.css"/>
    
        <!-- Template styles -->
        <link href="taskpane.css" rel="stylesheet" type="text/css" />
    </head>
    
    <body class="ms-font-l ms-landing-page">
      <main class="ms-landing-page__main">
        <section class="ms-landing-page__content ms-font-m ms-fontColor-neutralPrimary">
          <div id="not-configured" style="display: none;">
            <div class="centered ms-font-xxl ms-u-textAlignCenter">Welcome!</div>
            <div class="ms-font-xl" id="settings-prompt">Please choose the <strong>Settings</strong> icon at the bottom of this window to configure this add-in.</div>
          </div>
          <div id="gist-list-container" style="display: none;">
            <form>
              <div id="gist-list">
              </div>
            </form>
          </div>
          <div id="error-display" style="display: none;" class="ms-u-borderBase ms-fontColor-error ms-font-m ms-bgColor-error ms-borderColor-error">
          </div>
        </section>
        <button class="ms-Button ms-Button--primary" id="insert-button" tabindex=0 disabled>
          <span class="ms-Button-label">Insert</span>
        </button>
      </main>
      <footer class="ms-landing-page__footer ms-bgColor-themePrimary">
        <div class="ms-landing-page__footer--left">
          <img src="../../assets/logo-filled.png" />
          <h1 class="ms-font-xl ms-fontWeight-semilight ms-fontColor-white">Git the gist</h1>
        </div>
        <div id="settings-icon" class="ms-landing-page__footer--right" aria-label="Settings" tabindex=0>
          <i class="ms-Icon enlarge ms-Icon--Settings ms-fontColor-white"></i>
        </div>
      </footer>
      <script type="text/javascript" src="../../node_modules/jquery/dist/jquery.js"></script>
      <script type="text/javascript" src="../../node_modules/showdown/dist/showdown.min.js"></script>
      <script type="text/javascript" src="../../node_modules/urijs/src/URI.min.js"></script>
      <script type="text/javascript" src="../helpers/addin-config.js"></script>
      <script type="text/javascript" src="../helpers/gist-api.js"></script>
      <script type="text/javascript" src="taskpane.js"></script>
    </body>
    
    </html>
    
  2. 保存所做的更改。

为任务窗格创建 CSS

  1. 在创建的项目中,任务窗格 CSS 已在文件 ./src/taskpane/taskpane.css 中指定。 打开该文件并将全部内容替换为以下代码。

    /* Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license in root of repo. */
    html, body {
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
      overflow: auto; }
    
    body {
      position: relative;
      font-size: 16px; }
    
    main {
      height: 100%;
      overflow-y: auto; }
    
    footer {
      width: 100%;
      position: relative;
      bottom: 0;
      margin-top: 10px;}
    
    p, h1, h2, h3, h4, h5, h6 {
      margin: 0;
      padding: 0; }
    
    ul {
      padding: 0; }
    
    #settings-prompt {
      margin: 10px 0;
    }
    
    #error-display {
      padding: 10px;
    }
    
    #insert-button {
      margin: 0 10px;
    }
    
    .clearfix {
      display: block;
      clear: both;
      height: 0; }
    
    .pointerCursor {
      cursor: pointer; }
    
    .invisible {
      visibility: hidden; }
    
    .undisplayed {
      display: none; }
    
    .ms-Icon.enlarge {
      position: relative;
      font-size: 20px;
      top: 4px; }
    
    .ms-ListItem-secondaryText,
    .ms-ListItem-tertiaryText {
      padding-left: 15px;
    }
    
    .ms-landing-page {
      display: -webkit-flex;
      display: flex;
      -webkit-flex-direction: column;
              flex-direction: column;
      -webkit-flex-wrap: nowrap;
              flex-wrap: nowrap;
      height: 100%; }
    
    .ms-landing-page__main {
      display: -webkit-flex;
      display: flex;
      -webkit-flex-direction: column;
              flex-direction: column;
      -webkit-flex-wrap: nowrap;
              flex-wrap: nowrap;
      -webkit-flex: 1 1 0;
              flex: 1 1 0;
      height: 100%; }
    
    .ms-landing-page__content {
      display: -webkit-flex;
      display: flex;
      -webkit-flex-direction: column;
              flex-direction: column;
      -webkit-flex-wrap: nowrap;
              flex-wrap: nowrap;
      height: 100%;
      -webkit-flex: 1 1 0;
              flex: 1 1 0;
      padding: 20px; }
    
    .ms-landing-page__content h2 {
      margin-bottom: 20px; }
    
    .ms-landing-page__footer {
      display: -webkit-inline-flex;
      display: inline-flex;
      -webkit-justify-content: center;
              justify-content: center;
      -webkit-align-items: center;
              align-items: center; }
    
    .ms-landing-page__footer--left {
      transition: background ease 0.1s, color ease 0.1s;
      display: -webkit-inline-flex;
      display: inline-flex;
      -webkit-justify-content: flex-start;
              justify-content: flex-start;
      -webkit-align-items: center;
              align-items: center;
      -webkit-flex: 1 0 0px;
              flex: 1 0 0px;
      padding: 20px; }
    
    .ms-landing-page__footer--left:active {
      cursor: default; }
    
    .ms-landing-page__footer--left--disabled {
      opacity: 0.6;
      pointer-events: none;
      cursor: not-allowed; }
    
    .ms-landing-page__footer--left--disabled:active, .ms-landing-page__footer--left--disabled:hover {
      background: transparent; }
    
    .ms-landing-page__footer--left img {
      width: 40px;
      height: 40px; }
    
    .ms-landing-page__footer--left h1 {
      -webkit-flex: 1 0 0px;
              flex: 1 0 0px;
      margin-left: 15px;
      text-align: left;
      width: auto;
      max-width: auto;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis; }
    
    .ms-landing-page__footer--right {
      transition: background ease 0.1s, color ease 0.1s;
      padding: 29px 20px; }
    
    .ms-landing-page__footer--right:active, .ms-landing-page__footer--right:hover {
      background: #005ca4;
      cursor: pointer; }
    
    .ms-landing-page__footer--right:active {
      background: #005ca4; }
    
    .ms-landing-page__footer--right--disabled {
      opacity: 0.6;
      pointer-events: none;
      cursor: not-allowed; }
    
    .ms-landing-page__footer--right--disabled:active, .ms-landing-page__footer--right--disabled:hover {
      background: transparent; }
    
  2. 保存所做的更改。

为任务窗格指定 JavaScript

  1. 在创建的项目中,任务窗格 JavaScript 已在文件 ./src/taskpane/taskpane.js 中指定。 打开该文件并将全部内容替换为以下代码。

    (function() {
      'use strict';
    
      let config;
      let settingsDialog;
    
      Office.onReady(function() {
        $(document).ready(function() {
          config = getConfig();
    
          // Check if add-in is configured.
          if (config && config.gitHubUserName) {
            // If configured, load the gist list.
            loadGists(config.gitHubUserName);
          } else {
            // Not configured yet.
            $('#not-configured').show();
          }
    
          // When insert button is selected, build the content
          // and insert into the body.
          $('#insert-button').on('click', function() {
            const gistId = $('.ms-ListItem.is-selected').val();
            getGist(gistId, function(gist, error) {
              if (gist) {
                buildBodyContent(gist, function (content, error) {
                  if (content) {
                    Office.context.mailbox.item.body.setSelectedDataAsync(
                      content,
                      { coercionType: Office.CoercionType.Html },
                      function (result) {
                        if (result.status === Office.AsyncResultStatus.Failed) {
                          showError("Could not insert gist: " + result.error.message);
                        }
                      }
                    );
                  } else {
                    showError('Could not create insertable content: ' + error);
                  }
                });
              } else {
                showError('Could not retrieve gist: ' + error);
              }
            });
          });
    
          // When the settings icon is selected, open the settings dialog.
          $('#settings-icon').on('click', function() {
            // Display settings dialog.
            let url = new URI('dialog.html').absoluteTo(window.location).toString();
            if (config) {
              // If the add-in has already been configured, pass the existing values
              // to the dialog.
              url = url + '?gitHubUserName=' + config.gitHubUserName + '&defaultGistId=' + config.defaultGistId;
            }
    
            const dialogOptions = { width: 20, height: 40, displayInIframe: true };
    
            Office.context.ui.displayDialogAsync(url, dialogOptions, function(result) {
              settingsDialog = result.value;
              settingsDialog.addEventHandler(Office.EventType.DialogMessageReceived, receiveMessage);
              settingsDialog.addEventHandler(Office.EventType.DialogEventReceived, dialogClosed);
            });
          })
        });
      });
    
      function loadGists(user) {
        $('#error-display').hide();
        $('#not-configured').hide();
        $('#gist-list-container').show();
    
        getUserGists(user, function(gists, error) {
          if (error) {
    
          } else {
            $('#gist-list').empty();
            buildGistList($('#gist-list'), gists, onGistSelected);
          }
        });
      }
    
      function onGistSelected() {
        $('#insert-button').removeAttr('disabled');
        $('.ms-ListItem').removeClass('is-selected').removeAttr('checked');
        $(this).children('.ms-ListItem').addClass('is-selected').attr('checked', 'checked');
      }
    
      function showError(error) {
        $('#not-configured').hide();
        $('#gist-list-container').hide();
        $('#error-display').text(error);
        $('#error-display').show();
      }
    
      function receiveMessage(message) {
        config = JSON.parse(message.message);
        setConfig(config, function(result) {
          settingsDialog.close();
          settingsDialog = null;
          loadGists(config.gitHubUserName);
        });
      }
    
      function dialogClosed(message) {
        settingsDialog = null;
      }
    })();
    
  2. 保存所做的更改。

测试“插入 gist”按钮

  1. 如果本地 Web 服务器尚未运行,请从命令提示符运行 npm start

  2. 打开 Outlook 并撰写一封新邮件。

  3. 在“撰写邮件”窗口中,选择“插入 gist”按钮。 你应该看到,撰写表单的右侧将打开一个任务窗格。

  4. 在任务窗格中,选择 Hello World Html gist 并选择“插入”以将该 gist 插入到邮件正文中。

加载项任务窗格和消息正文中显示的选定 gist 内容。

后续步骤

在本教程中,你创建了一个可以用于在邮件撰写模式下将内容插入到邮件正文中的 Outlook 外接程序。 若要了解有关开发 Outlook 加载项的详细信息,请继续阅读以下文章。

代码示例

另请参阅