扩展教程#

JupyterLab 扩展为用户体验添加功能。本页介绍如何创建一种类型的扩展,即应用程序插件,它

  • 命令面板侧边栏中添加一个“随机天文图片”命令

  • 在激活时获取图像和元数据

  • 在选项卡面板中显示图像和元数据

通过完成本教程,您将学习

  • 如何在 Linux 或 OSX 机器上从头开始设置扩展开发环境。(如果您使用的是 Windows,则需要稍微修改命令。)

  • 如何从jupyterlab/extension-template启动扩展项目

  • 如何在 JupyterLab 中迭代地编写代码、构建和加载扩展

  • 如何使用 git 对您的工作进行版本控制

  • 如何发布您的扩展供其他人使用

完成的扩展,显示了2015 年 7 月 24 日的天文图片#

听起来很有趣吗?太好了。我们开始吧!

设置开发环境#

使用 miniconda 安装 conda#

首先,按照Conda 的安装文档安装 miniconda。

在 conda 环境中安装 NodeJS、JupyterLab 等#

接下来,创建一个包含以下内容的 conda 环境:

  1. 最新版本的 JupyterLab

  2. copier 和一些依赖项,您将使用此工具来引导您的扩展项目结构(这是一个 Python 工具,我们将在下面使用 conda 安装它)。

  3. NodeJS,您将使用它来编译扩展的 Web 资产(例如,TypeScript、CSS)的 JavaScript 运行时

  4. git,一个版本控制系统,您将使用它来在您完成本教程的过程中对您的工作进行快照

最佳实践是保持根 conda 环境(即 miniconda 安装程序创建的环境)不受影响,并在命名 conda 环境中安装您的项目特定依赖项。运行以下命令以创建一个名为jupyterlab-ext的新环境。

conda create -n jupyterlab-ext --override-channels --strict-channel-priority -c conda-forge -c nodefaults jupyterlab=4 nodejs=20 git copier=9 jinja2-time

现在激活新环境,以便您运行的所有后续命令都从该环境中运行。

conda activate jupyterlab-ext

注意:您需要在打开的每个新终端中运行上面的命令,然后才能使用您在jupyterlab-ext环境中安装的工具。

创建一个存储库#

为您的扩展创建一个新的存储库(例如,请参阅GitHub 说明)。这是一个可选步骤,但如果您想共享您的扩展,则强烈建议您执行此步骤。

创建一个扩展项目#

从模板初始化项目#

接下来,使用 copier 为您的扩展创建一个新项目。这将在您的当前目录中为您的扩展创建一个新文件夹。

mkdir my_first_extension
cd my_first_extension
copier copy --trust https://github.com/jupyterlab/extension-template .

当出现提示时,为所有模板提示输入以下值(apod 代表天文图片,这是我们用来获取图片的 NASA 服务)。

What is your extension kind?
(Use arrow keys)
 frontend
Extension author name
 Your Name
Extension author email
 [email protected]
JavaScript package name
 jupyterlab_apod
Python package name
 jupyterlab_apod
Extension short description
 Show a random NASA Astronomy Picture of the Day in a JupyterLab panel
Does the extension have user settings?
 N
Do you want to set up Binder example?
 Y
Do you want to set up test for the extension?
 Y
Git remote repository URL
 https://github.com/github_username/jupyterlab_apod

注意

  • 如果您没有使用存储库,请将存储库字段留空。您可以在以后的 package.json 文件中返回并编辑存储库字段。

  • 如果您使用的是最新版本的模板,您会注意到模板中包含测试。如果您不想包含它们,只需在测试提示中回答 n

列出文件。

ls -a

您应该看到类似以下的列表。

.copier-answers.yml  .github          .gitignore      .prettierignore     .yarnrc.yml
babel.config.js      jest.config.js   pyproject.toml  src                 ui-tests
binder               jupyterlab_apod  README.md       style               yarn.lock
CHANGELOG.md         LICENSE          RELEASE.md      tsconfig.json
install.json         package.json     setup.py        tsconfig.test.json

将您所拥有的内容提交到 git#

在您的 jupyterlab_apod 文件夹中运行以下命令,将其初始化为 git 存储库并提交当前代码。

git init
git add .
git commit -m 'Seed apod project from extension template'

注意

此步骤在技术上并非必需,但最好在版本控制系统中跟踪更改,以防您需要回滚到早期版本或想与他人协作。您可以将本教程中的工作与 GitHub 上 jupyterlab_apod 的参考版本的提交进行比较,地址为 jupyterlab/jupyterlab_apod

构建并安装用于开发的扩展#

您的新扩展项目包含足够的代码,可以在 JupyterLab 中看到它在运行。运行以下命令来安装初始项目依赖项并将扩展安装到 JupyterLab 环境中。

pip install -ve .

上面的命令将扩展的前端部分复制到 JupyterLab。每次我们进行更改时,都可以再次运行此 pip install 命令,将更改复制到 JupyterLab。更好的是,我们可以使用 develop 命令从 JupyterLab 创建到我们的源目录的符号链接。这意味着我们的更改会自动在 JupyterLab 中生效。

jupyter labextension develop --overwrite .

Windows 用户的重要说明#

重要

在 Windows 上,符号链接需要在 Windows 10 或更高版本上为 Python 3.8 或更高版本激活,方法是激活“开发者模式”。这可能不被您的管理员允许。有关说明,请参阅 在 Windows 上激活开发者模式

查看初始扩展的实际效果#

安装完成后,打开第二个终端。运行以下命令以激活 jupyterlab-ext 环境并在默认的 Web 浏览器中启动 JupyterLab。

conda activate jupyterlab-ext
jupyter lab

在该浏览器窗口中,按照浏览器的说明打开 JavaScript 控制台。

在您重新加载页面并打开控制台后,您应该在控制台中看到一条消息,内容为 JupyterLab extension jupyterlab_apod is activated!。如果看到这条消息,恭喜您,您已准备好开始修改扩展!如果没有,请返回并确保您没有遗漏任何步骤,如果您遇到困难,请 联系我们

注意

让运行 jupyter lab 命令的终端保持打开状态并运行 JupyterLab,以查看以下更改的效果。

添加一个每日天文图片小部件#

显示一个空面板#

命令面板是 JupyterLab 中所有可用命令的主要视图。对于您的第一个添加,您将向面板添加一个随机天文图片命令,并使其在调用时显示一个天文图片选项卡面板。

打开你最喜欢的文本编辑器,并在你的扩展项目中打开 src/index.ts 文件。更改文件顶部的导入语句,以获取对命令面板接口和 JupyterFrontEnd 实例的引用。

import {
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';

import { ICommandPalette } from '@jupyterlab/apputils';

找到类型为 JupyterFrontEndPluginplugin 对象。更改定义,使其如下所示

/**
 * Initialization data for the jupyterlab_apod extension.
 */
const plugin: JupyterFrontEndPlugin<void> = {
  id: 'jupyterlab-apod',
  description: 'Show a random NASA Astronomy Picture of the Day in a JupyterLab panel.',
  autoStart: true,
  requires: [ICommandPalette],
  activate: (app: JupyterFrontEnd, palette: ICommandPalette) => {
    console.log('JupyterLab extension jupyterlab_apod is activated!');
    console.log('ICommandPalette:', palette);
  }
};

requires 属性表明你的插件在启动时需要一个实现 ICommandPalette 接口的对象。JupyterLab 将传递一个 ICommandPalette 的实例作为 activate 的第二个参数,以满足此要求。定义 palette: ICommandPalette 使得该实例在该函数中可供你的代码使用。第二行 console.log 代码仅用于让你立即检查你的更改是否有效。

现在你需要安装这些依赖项。在仓库根目录中运行以下命令来安装依赖项并将其保存到你的 package.json 文件中

jlpm add @jupyterlab/apputils @jupyterlab/application

最后,运行以下命令来重建你的扩展。

jlpm run build

注意

本教程使用 jlpm 来安装 Javascript 包并运行构建命令,它是 JupyterLab 的捆绑版 yarn。如果你愿意,可以使用其他 Javascript 包管理器,例如 npmyarn 本身。

扩展构建完成后,返回到你在启动 JupyterLab 时打开的浏览器标签页。刷新它,并在控制台中查看。你应该看到与之前相同的激活消息,以及你刚刚添加的关于 ICommandPalette 实例的新消息。如果没有,请检查构建命令的输出以查找错误并更正你的代码。

JupyterLab extension jupyterlab_apod is activated!
ICommandPalette: Palette {_palette: CommandPalette}

请注意,我们必须运行 jlpm run build 才能更新包。此命令执行两项操作:将 src/` 中的 TypeScript 文件编译成 lib/ 中的 JavaScript 文件(jlpm run build),然后将 lib/ 中的 JavaScript 文件捆绑成 jupyterlab_apod/static 中的 JupyterLab 扩展(jlpm run build:extension)。如果你希望避免在每次更改后运行 jlpm run build,可以打开第三个终端,激活 jupyterlab-ext 环境,并从你的扩展目录运行 jlpm run watch 命令,该命令将在更改和保存 TypeScript 文件时自动编译它们。

现在返回到你的编辑器。修改文件顶部的导入语句,添加更多导入语句

import { ICommandPalette, MainAreaWidget } from '@jupyterlab/apputils';

import { Widget } from '@lumino/widgets';

也安装这个新的依赖项

jlpm add @lumino/widgets

然后再次修改插件对象中的 activate 函数,使其包含以下代码(突出显示的行显示了 activate 函数,你只需要修改该函数的内容,因此请确保你的括号匹配,并将 export default plugin 部分保留在下方)

const plugin: JupyterFrontEndPlugin<void> = {
  id: 'jupyterlab-apod',
  description: 'Show a random NASA Astronomy Picture of the Day in a JupyterLab panel.',
  autoStart: true,
  requires: [ICommandPalette],
  activate: (app: JupyterFrontEnd, palette: ICommandPalette) => {
    console.log('JupyterLab extension jupyterlab_apod is activated!');

    // Define a widget creator function,
    // then call it to make a new widget
    const newWidget = () => {
      // Create a blank content widget inside of a MainAreaWidget
      const content = new Widget();
      const widget = new MainAreaWidget({ content });
      widget.id = 'apod-jupyterlab';
      widget.title.label = 'Astronomy Picture';
      widget.title.closable = true;
      return widget;
    }
    let widget = newWidget();

    // Add an application command
    const command: string = 'apod:open';
    app.commands.addCommand(command, {
      label: 'Random Astronomy Picture',
      execute: () => {
        // Regenerate the widget if disposed
        if (widget.isDisposed) {
          widget = newWidget();
        }
        if (!widget.isAttached) {
          // Attach the widget to the main work area if it's not there
          app.shell.add(widget, 'main');
        }
        // Activate the widget
        app.shell.activateById(widget.id);
      }
    });

    // Add the command to the palette.
    palette.addItem({ command, category: 'Tutorial' });
  }
};

export default plugin;

第一段新代码定义(并调用)了一个可重用的窗口小部件创建函数。该函数返回一个 MainAreaWidget 实例,该实例有一个空内容 Widget 作为其子节点。它还为主区域窗口小部件分配一个唯一的 ID,为其提供一个将显示为其标签标题的标签,并使用户可以关闭标签。第二段代码添加了一个新的命令,其 ID 为 apod:open,标签为随机天文图片,添加到 JupyterLab 中。当命令执行时,它会检查窗口小部件是否已释放,如果窗口小部件尚未存在,则将其附加到主显示区域,然后将其设为活动标签。最后一行新代码使用命令 ID 将命令添加到命令面板的教程部分。

使用 jlpm run build 重新构建您的扩展(除非您已经在使用 jlpm run watch),然后刷新浏览器选项卡。通过点击“视图”菜单中的“命令”或使用键盘快捷键 Command/Ctrl Shift C 打开命令面板,并在搜索框中输入“Astronomy”。您的“随机天文图片”命令应该出现。点击它或使用键盘选择它并按“Enter”。您应该会看到一个新的空白面板出现,选项卡标题为“天文图片”。点击选项卡上的“x”关闭它并再次激活命令。选项卡应该重新出现。最后,点击一个启动器选项卡,以便“天文图片”面板仍然打开,但不再处于活动状态。现在再次运行“随机天文图片”命令。单个“天文图片”选项卡应该进入前台。

正在进行的扩展,显示一个空白面板。#

如果您的小部件没有按预期工作,请将您的代码与 01-show-a-panel 标签 中的参考项目状态进行比较。一旦您将所有内容正常运行,请提交您的更改并继续。

git add package.json src/index.ts
git commit -m 'Show Astronomy Picture command in palette'

在面板中显示图片#

现在您有一个空面板。现在该向其中添加图片了。返回您的代码编辑器。在小部件创建器函数中,在创建 MainAreaWidget 实例的行下方和返回新小部件的行上方添加以下代码。

// Add an image element to the content
let img = document.createElement('img');
content.node.appendChild(img);

// Get a random date string in YYYY-MM-DD format
function randomDate() {
  const start = new Date(2010, 1, 1);
  const end = new Date();
  const randomDate = new Date(start.getTime() + Math.random()*(end.getTime() - start.getTime()));
  return randomDate.toISOString().slice(0, 10);
}

// Fetch info about a random picture
const response = await fetch(`https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=${randomDate()}`);
const data = await response.json() as APODResponse;

if (data.media_type === 'image') {
  // Populate the image
  img.src = data.url;
  img.title = data.title;
} else {
  console.log('Random APOD was not a picture.');
}

前两行创建一个新的 HTML <img> 元素并将其添加到小部件 DOM 节点。接下来的几行定义一个函数,以 YYYY-MM-DD 格式获取一个随机日期,然后使用该函数使用 HTML fetch API 发出请求,该 API 返回有关该日期的天文图片的信息。最后,我们根据响应设置图像源和标题属性。

现在定义在上面的代码中引入的 APODResponse 类型。将此定义放在文件顶部的导入下方。

interface APODResponse {
  copyright: string;
  date: string;
  explanation: string;
  media_type: 'video' | 'image';
  title: string;
  url: string;
};

然后我们需要在我们的代码中添加 asyncawait,因为我们在小部件创建器函数中使用 await

首先,将 activate 方法更新为 async

activate: async (app: JupyterFrontEnd, palette: ICommandPalette) => {

接下来,将 newWidget 函数更新为 async

const newWidget = async () => {

最后,将 await 添加到两个 newWidget 函数调用中,并将 async 添加到 execute 函数中

  let widget = await newWidget();

  // Add an application command
  const command: string = 'apod:open';
  app.commands.addCommand(command, {
    label: 'Random Astronomy Picture',
    execute: async () => {
      // Regenerate the widget if disposed
      if (widget.isDisposed) {
        widget = await newWidget();
      }
      if (!widget.isAttached) {
        // Attach the widget to the main work area if it's not there
        app.shell.add(widget, 'main');
      }
      // Activate the widget
      app.shell.activateById(widget.id);
    }
  });

注意

如果您不熟悉 JavaScript/TypeScript,并且想了解更多关于 asyncawaitPromises 的信息,您可以查看以下 MDN 上的教程

请务必参考 另请参阅 部分中的其他资源以获取更多资料。

如果需要,请重新构建您的扩展(jlpm run build),刷新您的浏览器选项卡,然后再次运行“随机天文图片”命令。您现在应该在面板打开时看到一张图片(如果该随机日期有图片而不是视频)。

正在进行的扩展,显示 2014 年 1 月 19 日的天文图片#

请注意,图像在面板中没有居中,并且如果图像大于面板区域,面板也不会滚动。您将在接下来的部分中解决这两个问题。

如果您根本没有看到图像,请将您的代码与参考项目中的 02-show-an-image 标签 进行比较。当它工作时,进行另一个 git 提交。

git add src/index.ts
git commit -m 'Show a picture in the panel'

改进小部件行为#

居中图像、添加署名和错误消息#

在我们的扩展项目目录中打开 style/base.css 进行编辑。在其中添加以下行。

.my-apodWidget {
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: auto;
}

此 CSS 将内容在小部件面板内垂直堆叠,并在内容溢出时允许面板滚动。此 CSS 文件由 JupyterLab 自动包含在页面中,因为 package.json 文件有一个指向它的 style 字段。通常,您应该将所有样式导入到单个 CSS 文件中,例如此 index.css 文件,并将该 CSS 文件的路径放在 package.json 文件的 style 字段中。

返回到 index.ts 文件。修改 activate 函数以应用 CSS 类、版权信息和 API 响应的错误处理。您将更新和替换/删除一些行,因此函数的开头应该像以下内容一样

activate: async (app: JupyterFrontEnd, palette: ICommandPalette) => {
  console.log('JupyterLab extension jupyterlab_apod is activated!');

  // Define a widget creator function,
  // then call it to make a new widget
  const newWidget = async () => {
    // Create a blank content widget inside of a MainAreaWidget
    const content = new Widget();
    content.addClass('my-apodWidget');
    const widget = new MainAreaWidget({ content });
    widget.id = 'apod-jupyterlab';
    widget.title.label = 'Astronomy Picture';
    widget.title.closable = true;

    // Add an image element to the content
    let img = document.createElement('img');
    content.node.appendChild(img);

    let summary = document.createElement('p');
    content.node.appendChild(summary);

    // Get a random date string in YYYY-MM-DD format
    function randomDate() {
      const start = new Date(2010, 1, 1);
      const end = new Date();
      const randomDate = new Date(start.getTime() + Math.random()*(end.getTime() - start.getTime()));
      return randomDate.toISOString().slice(0, 10);
    }

    // Fetch info about a random picture
    const response = await fetch(`https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=${randomDate()}`);
    if (!response.ok) {
      const data = await response.json();
      if (data.error) {
        summary.innerText = data.error.message;
      } else {
        summary.innerText = response.statusText;
      }
    } else {
      const data = await response.json() as APODResponse;

      if (data.media_type === 'image') {
        // Populate the image
        img.src = data.url;
        img.title = data.title;
        summary.innerText = data.title;
        if (data.copyright) {
          summary.innerText += ` (Copyright ${data.copyright})`;
        }
      } else {
        summary.innerText = 'Random APOD fetched was not an image.';
      }
    }

    return widget;
  }
  // Keep all the remaining lines below the newWidget function
  // definition the same as before from here down ...

注意

如果您的图像面板一直显示错误消息,您可能需要更新您的 NASA API 密钥(过多的图像请求可能会超出您的限制)

如果需要,构建您的扩展(jlpm run build)并刷新您的 JupyterLab 浏览器选项卡。调用“随机天文图片”命令,并确认图像居中,下方有版权信息。调整浏览器窗口或面板的大小,使图像大于可用区域。确保您可以滚动面板以查看图像的整个区域。

如果任何内容无法正常工作,请将您的代码与参考项目 03-style-and-attribute 标签 进行比较。当一切按预期工作时,进行另一个提交。

git add style/base.css src/index.ts
git commit -m 'Add styling, attribution, error handling'

按需显示新图像#

activate 函数已经变得很长,并且还有更多功能要添加。让我们将代码重构为两个独立的部分

  1. 一个 APODWidget,它封装了天文图片面板元素、配置和即将添加的更新行为

  2. 一个 activate 函数,它将小部件实例添加到 UI 并决定何时应刷新图片

首先将小部件代码重构到新的 APODWidget 类中。

index.ts 文件中,在 APODResponse 的定义下方添加该类。

class APODWidget extends Widget {
  /**
  * Construct a new APOD widget.
  */
  constructor() {
    super();

    this.addClass('my-apodWidget');

    // Add an image element to the panel
    this.img = document.createElement('img');
    this.node.appendChild(this.img);

    // Add a summary element to the panel
    this.summary = document.createElement('p');
    this.node.appendChild(this.summary);
  }

  /**
  * The image element associated with the widget.
  */
  readonly img: HTMLImageElement;

  /**
  * The summary text element associated with the widget.
  */
  readonly summary: HTMLParagraphElement;

  /**
  * Handle update requests for the widget.
  */
  async updateAPODImage(): Promise<void> {

    const response = await fetch(`https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=${this.randomDate()}`);

    if (!response.ok) {
      const data = await response.json();
      if (data.error) {
        this.summary.innerText = data.error.message;
      } else {
        this.summary.innerText = response.statusText;
      }
      return;
    }

    const data = await response.json() as APODResponse;

    if (data.media_type === 'image') {
      // Populate the image
      this.img.src = data.url;
      this.img.title = data.title;
      this.summary.innerText = data.title;
      if (data.copyright) {
        this.summary.innerText += ` (Copyright ${data.copyright})`;
      }
    } else {
      this.summary.innerText = 'Random APOD fetched was not an image.';
    }
  }

  /**
  * Get a random date string in YYYY-MM-DD format.
  */
  randomDate(): string {
    const start = new Date(2010, 1, 1);
    const end = new Date();
    const randomDate = new Date(start.getTime() + Math.random()*(end.getTime() - start.getTime()));
    return randomDate.toISOString().slice(0, 10);
  }
}

您之前已经编写了所有代码。您所做的只是对其进行重构以使用实例变量并将图像请求移动到其自己的函数中。

接下来,将 activate 中的剩余逻辑移动到新的顶级函数中,该函数位于 APODWidget 类定义的下方。修改代码以在主 JupyterLab 区域中不存在小部件时创建一个小部件,或者在命令再次运行时刷新现有小部件中的图像。在进行这些更改后,activate 函数的代码应如下所示

/**
* Activate the APOD widget extension.
*/
function activate(app: JupyterFrontEnd, palette: ICommandPalette) {
  console.log('JupyterLab extension jupyterlab_apod is activated!');

  // Define a widget creator function
  const newWidget = () => {
    const content = new APODWidget();
    const widget = new MainAreaWidget({content});
    widget.id = 'apod-jupyterlab';
    widget.title.label = 'Astronomy Picture';
    widget.title.closable = true;
    return widget;
  }

  // Create a single widget
  let widget = newWidget();

  // Add an application command
  const command: string = 'apod:open';
  app.commands.addCommand(command, {
    label: 'Random Astronomy Picture',
    execute: () => {
      // Regenerate the widget if disposed
      if (widget.isDisposed) {
        widget = newWidget();
      }
      if (!widget.isAttached) {
        // Attach the widget to the main work area if it's not there
        app.shell.add(widget, 'main');
      }
      // Refresh the picture in the widget
      widget.content.updateAPODImage();
      // Activate the widget
      app.shell.activateById(widget.id);
    }
  });

  // Add the command to the palette.
  palette.addItem({ command, category: 'Tutorial' });
}

JupyterFrontEndPlugin 对象中移除 activate 函数定义,改为引用顶层函数,如下所示

const plugin: JupyterFrontEndPlugin<void> = {
  id: 'jupyterlab_apod',
  autoStart: true,
  requires: [ICommandPalette],
  activate: activate
};

确保保留文件中的 export default plugin; 行。现在重新构建扩展并刷新 JupyterLab 浏览器选项卡。多次运行“随机天文图片”命令,但不要关闭面板。每次执行命令时,图片都应该更新。关闭面板,运行命令,它应该重新出现并显示新图片。

如果任何内容无法正常工作,请将您的代码与 04-refactor-and-refresh 标签 进行比较以进行调试。一旦它正常工作,就提交它。

git add src/index.ts
git commit -m 'Refactor, refresh image'

在浏览器刷新时恢复面板状态#

您可能会注意到,每次刷新浏览器选项卡时,天文图片面板都会消失,即使它在您刷新之前是打开的。其他打开的面板,如笔记本、终端和文本编辑器,都会重新出现并返回到您在面板布局中离开的位置。您也可以让您的扩展以这种方式运行。

更新 index.ts 文件顶部的导入,使整个导入语句列表如下所示(添加 ILayoutRestorerWidgetTracker

import {
  ILayoutRestorer,
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';

import {
  ICommandPalette,
  MainAreaWidget,
  WidgetTracker
} from '@jupyterlab/apputils';

import { Widget } from '@lumino/widgets';

然后将 ILayoutRestorer 接口作为 optional 添加到 JupyterFrontEndPlugin 定义中。此添加将全局 LayoutRestorer 作为 activate 函数的第三个参数传递。

const plugin: JupyterFrontEndPlugin<void> = {
  id: 'jupyterlab-apod',
  description: 'Show a random NASA Astronomy Picture of the Day in a JupyterLab panel.',
  autoStart: true,
  requires: [ICommandPalette],
  optional: [ILayoutRestorer],
  activate: activate
};

这里 ILayoutRestorer 被指定为一个 optional 令牌,因为相应的服务可能在不提供布局恢复功能的自定义 JupyterLab 发行版中不可用。将其设置为 optional 使其成为一个不错的选择,并使您的扩展能够在更多基于 JupyterLab 的应用程序中加载。

注意

您可以在扩展开发人员指南的 令牌 部分了解有关 requiresoptional 的更多信息。

最后,重写 activate 函数,使其

  1. 声明一个 widget 变量,但不要立即创建实例。

  2. 将全局 LayoutRestorer 作为 activate 函数的第三个参数添加。此参数被声明为 ILayoutRestorer | null,因为令牌被指定为 optional

  3. 构造一个 WidgetTracker 并告诉 ILayoutRestorer 使用它来保存/恢复面板状态。

  4. 适当地创建、跟踪、显示和刷新 widget 面板。

function activate(app: JupyterFrontEnd, palette: ICommandPalette, restorer: ILayoutRestorer | null) {
  console.log('JupyterLab extension jupyterlab_apod is activated!');

  // Declare a widget variable
  let widget: MainAreaWidget<APODWidget>;

  // Add an application command
  const command: string = 'apod:open';
  app.commands.addCommand(command, {
    label: 'Random Astronomy Picture',
    execute: () => {
      if (!widget || widget.isDisposed) {
        const content = new APODWidget();
        widget = new MainAreaWidget({content});
        widget.id = 'apod-jupyterlab';
        widget.title.label = 'Astronomy Picture';
        widget.title.closable = true;
      }
      if (!tracker.has(widget)) {
        // Track the state of the widget for later restoration
        tracker.add(widget);
      }
      if (!widget.isAttached) {
        // Attach the widget to the main work area if it's not there
        app.shell.add(widget, 'main');
      }
      widget.content.updateAPODImage();

      // Activate the widget
      app.shell.activateById(widget.id);
    }
  });

  // Add the command to the palette.
  palette.addItem({ command, category: 'Tutorial' });

  // Track and restore the widget state
  let tracker = new WidgetTracker<MainAreaWidget<APODWidget>>({
    namespace: 'apod'
  });
  if (restorer) {
    restorer.restore(tracker, {
      command,
      name: () => 'apod'
    });
  }
}

最后一次重建您的扩展并刷新浏览器选项卡。执行“随机天文图片”命令并验证面板是否显示其中包含的图片。再次刷新浏览器选项卡。您应该看到天文图片面板立即重新出现,而无需运行命令。关闭面板并刷新浏览器选项卡。然后,您应该在刷新后看不到天文图片选项卡。

完成的扩展,显示 2015 年 7 月 24 日的天文图片#

如果您的扩展无法正常工作,请参考 05-restore-panel-state 标签。当您的扩展状态正确持久化时,请进行提交。

git add src/index.ts
git commit -m 'Restore panel state'

恭喜!您已实现本教程开始时列出的所有行为。

打包您的扩展#

适用于 JupyterLab 3.0 及更高版本的 JupyterLab 扩展可以作为 Python 包进行分发。我们使用的扩展模板包含了 pyproject.toml 文件中的所有 Python 打包说明,用于将您的扩展包装在 Python 包中。在生成包之前,我们首先需要安装 build

pip install build

要在 dist/ 目录中创建 Python 源代码包 (.tar.gz),请执行以下操作

python -m build -s

要在 dist/ 目录中创建 Python wheel 包 (.whl),请执行以下操作

python -m build

这两个命令都会将 JavaScript 构建到 jupyterlab_apod/labextension/static 目录中的一个捆绑包中,然后与 Python 包一起分发。此捆绑包将包括任何必要的 JavaScript 依赖项。您可能希望检查 jupyterlab_apod/labextension/static 目录以保留您包中分发的 JavaScript 记录,或者您可能希望将此“构建工件”保留在您的源代码库历史记录之外。

您现在可以尝试像用户一样安装您的扩展。打开一个新的终端并运行以下命令以创建一个新的环境并安装您的扩展。

conda create -n jupyterlab-apod jupyterlab
conda activate jupyterlab-apod
pip install jupyterlab_apod/dist/jupyterlab_apod-0.1.0-py3-none-any.whl
jupyter lab

您应该会看到一个新的 JupyterLab 浏览器选项卡出现。出现后,执行“随机天文图片”命令以检查您的扩展是否正常工作。

发布您的扩展#

您可以将您的 Python 包发布到 PyPIconda-forge 存储库,以便用户可以使用 pipconda 轻松安装扩展。

您可能还想将您的扩展作为 JavaScript 包发布到 npm 包存储库,原因如下

  1. 将扩展作为 npm 包分发允许用户将扩展显式编译到 JupyterLab 中(类似于在 JupyterLab 版本 1 和 2 中所做的那样),这将导致更优化的 JupyterLab 包。

  2. 正如我们上面所看到的,JupyterLab 使扩展能够使用其他扩展提供的服务。例如,我们上面的扩展使用 ICommandPaletteILayoutRestorer 服务,这些服务由 JupyterLab 中的核心扩展提供。我们能够通过从 @jupyterlab/apputils@jupyterlab/application npm 包导入它们的令牌并在我们的插件定义中列出它们来告诉 JupyterLab 我们需要这些服务。如果您想为其他扩展使用提供 JupyterLab 系统的服务,您需要将您的 JavaScript 包发布到 npm,以便其他扩展可以依赖它并导入和需要您的令牌。

自动发布#

如果您使用模板引导您的扩展,则存储库应该已经与 Jupyter Releaser 兼容。

Jupyter Releaser 提供了一组 GitHub Actions 工作流,用于

  • 在变更日志中生成一个新条目

  • 草拟一个新的发布

  • 将发布发布到 PyPInpm

有关如何运行发布工作流的更多信息,请查看文档:jupyter-server/jupyter_releaser

了解更多#

您已经完成了本教程。做得很好!如果您想继续学习,以下是一些关于接下来尝试什么内容的建议

  • 将 API 响应中的图像描述添加到面板中。

  • 为“随机天文图片”命令分配一个默认热键。

  • 将图像链接到 NASA 网站上的图片(URL 格式为 https://apod.nasa.gov/apod/apYYMMDD.html)。

  • 在图像加载后更新图像标题和描述,以确保图片和描述始终同步。

  • 允许用户将图片固定在单独的永久面板中。

  • 添加一个设置,让用户输入他们的 API 密钥,以便他们每小时可以发出比演示密钥允许的更多请求。

  • 将您的扩展程序 Git 仓库推送到 GitHub。

  • 学习如何编写 其他类型的扩展程序.