扩展框架 React 和 JavaScript 代码示例

本页提供了以 React 和 JavaScript 编写的代码示例,针对您可能想要在扩展程序中使用的常见函数。

使用 Looker 扩展程序 SDK

扩展程序必须与 Looker 主机建立连接。在 React 中,这是通过将扩展程序封装在 ExtensionProvider40 组件中来实现的。此组件会与 Looker 主机建立连接,并使 Looker 扩展程序 SDKLooker SDK 可供该扩展程序使用。

import React from 'react'
import { ExtensionProvider40 } from '@looker/extension-sdk-react'
import { DemoCoreSDK } from './DemoCoreSDK'


export const App = () => {
 return (
   <ExtensionProvider40 chattyTimeout={-1}>
     <DemoCoreSDK />
   </ExtensionProvider40>
 )
}

扩展程序提供商背景

扩展程序提供方会向扩展程序公开 Looker 扩展程序 SDK 和 SDK API。自扩展框架创建后,又创建了不同版本的扩展提供程序。本部分介绍了扩展程序提供程序的历史记录,以及我们推荐使用 ExtensionProvider40 的原因。

第一个扩展程序提供程序是 ExtensionProvider,它公开了 Looker SDK 3.1 和 4.0 版。其缺点是,添加这两个 SDK 会增加最终生产软件包的大小。

之后创建了 ExtensionProvider2。之所以这样做,是因为扩展程序不得同时使用这两种 SDK,而强制开发者选择其中一种 SDK。遗憾的是,这仍然导致这两个 SDK 都包含在最终生产软件包的大小内。

在 SDK 4.0 正式推出后,我们创建了 ExtensionProvider40ExtensionProvider40 的优势在于,开发者不必选择要使用的 SDK,因为 SDK 4.0 是唯一可用的版本。由于 SDK 3.1 未包含在最终 bundle 中,因此这样做的好处是可以缩减 bundle 的大小。

如需从 Looker Extension SDK 添加函数,首先需要获取对该 SDK 的引用,该引用可通过提供程序完成,也可在全局范围内完成。然后,您可以像在任何 JavaScript 应用中一样调用 SDK 函数。

  • 如需从提供程序访问 SDK,请按以下步骤操作:
  import { ExtensionContext40 } from '@looker/extension-sdk-react'

  export const Comp1 = () => {
    const extensionContext = useContext(
      ExtensionContext40
    )
    const { extensionSDK, coreSDK } = extensionContext
  • 如需全局访问 SDK(调用前必须初始化扩展程序),请按以下步骤操作:
    const coreSDK = getCoreSDK()

现在,您可以像在任何 JavaScript 应用中一样使用该 SDK:

  const GetLooks = async () => {
    try {
      const looks = await sdk.ok(sdk.all_looks('id'))
      // process looks
      . . .
    } catch (error) {
      // do error handling
      . . .
    }
}

由于扩展程序在沙盒化 iframe 中运行,因此您无法通过更新父级的 window.location 对象来导航到 Looker 实例中的其他位置。您可以使用 Looker Extension SDK 进行导航。

此函数需要 navigation 使用权

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

. . .

  extensionSDK.updateLocation('/browse')

打开新的浏览器窗口

由于扩展程序是在沙盒化 iframe 中运行的,因此您无法使用父窗口打开新的浏览器窗口。您可以使用 Looker Extension SDK 来打开浏览器窗口。

如需使用此功能,您需要拥有 new_window 使用权才能打开指向当前 Looker 实例中某个位置的新窗口,或者需要有 new_window_external_urls 使用权才能打开在其他主机上运行的新窗口。

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

. . .
  extensionSDK.openBrowserWindow('/browse', '_blank')
. . .
  extensionSDK.openBrowserWindow('https://docs.looker.com/reference/manifest-params/application#entitlements', '_blank')

路由和深层链接

以下内容适用于基于 React 的扩展程序。

ExtensionProviderExtensionProvider2ExtensionProvider40 组件会自动创建一个名为 MemoryRouter 的 React Router 供您使用。请勿尝试创建 BrowserRouter,因为它在沙盒化 iframe 中不起作用。请勿尝试创建 HashRouter,因为它不适用于非基于 Chromium 的 Microsoft Edge 浏览器的沙盒化 iframe。

如果使用了 MemoryRouter 且您在扩展程序中使用了 react-router,扩展程序框架会自动将扩展程序的路由器同步到 Looker 主机路由器。这意味着,当页面重新加载时,扩展程序会收到浏览器向后和前进按钮点击的通知以及当前路线的通知。这也意味着,该扩展程序应自动支持深层链接。请参阅扩展程序示例,了解如何使用 react-router

扩展程序上下文数据

请勿将扩展框架上下文数据与 React 上下文混淆。

扩展程序能够在其所有用户之间共享环境数据。上下文数据可用于不经常更改且无特殊安全要求的数据。写入数据时应小心谨慎,因为没有数据锁定,最后一次写入内容生效。扩展程序在启动后立即可用上下文数据。Looker Extension SDK 提供了用于更新和刷新上下文数据的功能。

上下文数据的大小上限约为 16 MB。上下文数据将序列化为 JSON 字符串,因此,如果您为扩展程序使用上下文数据,也需要考虑这一点。

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

. . .

  // Get loaded context data. This will reflect any updates that have
  // been made by saveContextData.
  let context = await extensionSDK.getContextData()

. . .

  // Save context data to Looker server.
  context = await extensionSDK.saveContextData(context)

. . .

  // Refresh context data from Looker server.
  context = await extensionSDK.refreshContextData()

用户属性

Looker 扩展程序 SDK 提供了一个用于访问 Looker 用户属性的 API。用户属性访问权限分为两种:

  • 作用域 - 与扩展程序相关联。范围用户属性是扩展程序的命名空间,必须在 Looker 实例中定义用户属性,然后才能使用。要为用户属性添加命名空间,请使用扩展名作为属性名称的前缀。由于用户属性名称中不能使用短划线和冒号,因此扩展程序名称中的所有短划线和“::”字符都必须替换为下划线。

    例如:如果某个限定了范围的用户属性 my_value 与扩展 ID 为 my-extension::my-extension 一起使用,则该属性必须定义 my_extension_my_extension_my_value 的用户属性名称。定义后,扩展程序可以读取和更新用户属性。

  • 全局 - 这些是全局用户属性,处于只读状态。例如 locale 用户属性。

下面列出了用户属性 API 调用:

  • userAttributeGetItem - 读取用户属性。可以定义一个默认值,如果用户没有相应的属性值,系统将使用默认值。
  • userAttributeSetItem - 为当前用户保存用户属性。对于全局用户属性,将失败。保存的值仅对当前用户可见。
  • userAttributeResetItem - 将当前用户的用户属性重置为默认值。对于全局用户属性,将失败。

如需访问用户属性,您必须在 global_user_attributes 和/或 scoped_user_attributes 许可中指定属性名称。例如,在 LookML 项目清单文件中,您可以添加以下代码:

  entitlements: {
    scoped_user_attributes: ["my_value"]
    global_user_attributes: ["locale"]
  }
import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

  // Read global user attribute
  const locale = await extensionSDK.userAttributeGetItem('locale')

  // Read scoped user attribute
  const value = await extensionSDK.userAttributeGetItem('my_value')

  // Update scoped user attribute
  const value = await extensionSDK.userAttributeSetItem('my_value', 'abcd1234')

  // Reset scoped user attribute
  const value = await extensionSDK.userAttributeResetItem('my_value')

本地存储

沙盒化 iframe 不允许访问浏览器本地存储空间。Looker Extension SDK 允许扩展程序对父窗口的本地存储空间执行读写操作。本地存储空间位于扩展程序的命名空间内,这意味着它无法读取由父窗口或其他扩展程序创建的本地存储空间。

使用本地存储空间需要 local_storage 使用权

与同步浏览器本地存储 API 相反,扩展程序 localhost API 是异步的。

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

  // Read from local storage
  const value = await extensionSDK.localStorageGetItem('my_storage')

  // Write to local storage
  await extensionSDK.localStorageSetItem('my_storage', 'abcedefh')

  // Delete item from local storage
  await extensionSDK.localStorageRemoveItem('my_storage')

更新网页标题

扩展程序可能会更新当前的网页标题。此操作不需要具有使用权。

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

  extensionSDK.updateTitle('My Extension Title')

写入系统剪贴板

沙盒化 iframe 不允许访问系统剪贴板。Looker Extension SDK 允许扩展程序将文本写入系统剪贴板。出于安全考虑,不允许扩展程序从系统剪贴板读取数据。

如需写入系统剪贴板,您需要拥有use_clipboard”使用权

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

    // Write to system clipboard
    try {
      await extensionSDK.clipboardWrite(
        'My interesting information'
      )
      . . .
    } catch (error) {
      . . .
    }

嵌入信息中心、Look 和探索

该扩展程序框架支持嵌入信息中心、Look 和探索。

必须提供use_embeds”许可。我们建议您使用 Looker JavaScript Embed SDK 来嵌入内容。如需了解详情,请参阅 Embed SDK 文档

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

. . .

  const canceller = (event: any) => {
    return { cancel: !event.modal }
  }

  const updateRunButton = (running: boolean) => {
    setRunning(running)
  }

  const setupDashboard = (dashboard: LookerEmbedDashboard) => {
    setDashboard(dashboard)
  }

  const embedCtrRef = useCallback(
    (el) => {
      const hostUrl = extensionContext?.extensionSDK?.lookerHostData?.hostUrl
      if (el && hostUrl) {
        el.innerHTML = ''
        LookerEmbedSDK.init(hostUrl)
        const db = LookerEmbedSDK.createDashboardWithId(id as number)
          .withNext()
          .appendTo(el)
          .on('dashboard:loaded', updateRunButton.bind(null, false))
          .on('dashboard:run:start', updateRunButton.bind(null, true))
          .on('dashboard:run:complete', updateRunButton.bind(null, false))
          .on('drillmenu:click', canceller)
          .on('drillmodal:explore', canceller)
          .on('dashboard:tile:explore', canceller)
          .on('dashboard:tile:view', canceller)
          .build()
          .connect()
          .then(setupDashboard)
          .catch((error: Error) => {
            console.error('Connection error', error)
          })
      }
    },
    []
  )

  return (&#60;EmbedContainer ref={embedCtrRef} /&#62;)

该扩展程序示例使用样式化组件为生成的 iframe 提供简单的样式。例如:

import styled from "styled-components"

export const EmbedContainer = styled.div`
  width: 100%;
  height: 95vh;
  & > iframe {
    width: 100%;
    height: 100%;
  }

访问外部 API 端点

该扩展框架提供了两种访问外部 API 端点的方法:

  • 服务器代理 - 通过 Looker 服务器访问端点。此机制允许 Looker 服务器安全地设置客户端 ID 和密钥。
  • 提取代理 - 从用户的浏览器访问端点。代理是 Looker 界面。

在这两种情况下,您都需要在扩展程序 external_api_urls 使用权中指定外部 API 端点。

服务器代理

以下示例演示了如何使用服务器代理获取访问令牌,以供提取代理使用。必须将客户端 ID 和密钥定义为扩展程序的用户属性。通常,设置用户属性时,默认值为客户端 ID 或客户端密钥。

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

. . .
  const requestBody = {
    client_id: extensionSDK.createSecretKeyTag('my_client_id'),
    client_secret: extensionSDK.createSecretKeyTag('my_client_secret'),
  },
  try {
    const response = await extensionSDK.serverProxy(
      'https://myaccesstokenserver.com/access_token',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(requestBody),
      }
    )
    const { access_token, expiry_date } = response.body
. . .
  } catch (error) {
    // Error handling
    . . .
  }

用户属性名称必须映射到扩展程序。短划线必须替换为下划线,:: 字符必须替换为单个下划线。

例如,如果扩展程序的名称为 my-extension::my-extension,则需要为上一个示例定义的用户属性如下所示:

my_extension_my_extension_my_client_id
my_extension_my_extension_'my_client_secret'

提取代理

以下示例演示了如何使用提取代理。它使用上一个服务器代理示例中的访问令牌。

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

. . .

  try {
    const response = await extensionSDK.fetchProxy(
      'https://myaccesstokenserver.com/myendpoint',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${accessToken}`,
        },
        body: JSON.stringify({
          some_value: someValue,
          another_value: anotherValue,
        }),
      }
    )
    // Handle success

. . .

  } catch (error) {
    // Handle failure

. . .

  }

OAuth 集成

该扩展框架支持与 OAuth 提供方集成。OAuth 可用于获取访问令牌以访问特定资源,例如 Google 表格文档。

您需要在 extension oauth2_urls 授权中指定 OAuth 服务器端点。您可能还需要在 external_api_urls 使用权中指定其他网址。

扩展框架支持以下流程:

  • 隐式流
  • 带有密钥的授权代码授权类型
  • PKCE 代码质询和验证程序

一般流程是,打开一个子窗口以加载 OAuth 服务器页面。OAuth 服务器会对用户进行身份验证,并使用可用于获取访问令牌的其他详细信息重定向回 Looker 服务器。

隐式流:

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

. . .

    const response = await extensionSDK.oauth2Authenticate(
      'https://accounts.google.com/o/oauth2/v2/auth',
      {
        client_id: GOOGLE_CLIENT_ID!,
        scope: GOOGLE_SCOPES,
        response_type: 'token',
      }
    )
    const { access_token, expires_in } = response

带有密钥的授权代码授权类型:

  const authenticateParameters: Record&#60;string, string&#62; = {
    client_id: GITHUB_CLIENT_ID!,
    response_type: 'code',
  }
  const response = await extensionSDK.oauth2Authenticate(
    'https://github.com/login/oauth/authorize',
    authenticateParameters,
   'GET'
  )
  const exchangeParameters: Record&#60;string, string&#62; = {
    client_id: GITHUB_CLIENT_ID!,
    code: response.code,
    client_secret: extensionSDK.createSecretKeyTag('github_secret_key'),
  }
  const codeExchangeResponse = await extensionSDK.oauth2ExchangeCodeForToken(
    'https://github.com/login/oauth/access_token',
    exchangeParameters
  )
  const { access_token, error_description } = codeExchangeResponse

PKCE 代码质询和验证程序:

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

. . .

  const authRequest: Record&#60;string, string&#62; = {
    client_id: AUTH0_CLIENT_ID!,
    response_type: 'code',
    scope: AUTH0_SCOPES,
    code_challenge_method:  'S256',
  }
  const response = await extensionSDK.oauth2Authenticate(
    'https://sampleoauthserver.com/authorize',
    authRequest,
    'GET'
  )
  const exchangeRequest: Record&#60;string, string&#62; = {
    grant_type: 'authorization_code',
    client_id: AUTH0_CLIENT_ID!,
    code: response.code,
  }
  const codeExchangeResponse = await extensionSDK.oauth2ExchangeCodeForToken(
    'https://sampleoauthserver.com/login/oauth/token',
    exchangeRequest
  )
  const { access_token, expires_in } = codeExchangeResponse

斯巴达语

Spartan 是指使用 Looker 实例作为环境,仅向一组指定的用户公开扩展程序和扩展程序的方法。导航到 Looker 实例的 Spartan 用户将看到 Looker 管理员配置的任何登录流程。用户通过身份验证后,系统会根据用户的 landing_page 用户属性向其显示扩展程序,如下所示。用户只能访问扩展程序,而无法访问 Looker 的任何其他部分。如果用户有权访问多个扩展程序,这些扩展程序会控制用户能否使用 extensionSDK.updateLocation 导航到其他扩展程序。有一种特定的 Looker Extension SDK 方法可让用户退出 Looker 实例。

import { ExtensionContext40 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext40
  )
  const { extensionSDK } = extensionContext

. . .
  // Navigate to another extension
  extensionSDK.updateLocation('/spartan/another::extension')

. . .
  // Logout
  extensionSDK.spartanLogout()

定义精简用户

要定义一个精简用户,您必须创建一个名为“仅扩展程序”的群组

创建“仅限扩展程序”群组后,前往 Looker 管理部分中的用户属性页面,然后修改 landing_page 用户属性。选择组值标签页,然后添加“仅限扩展程序”组。该值应设置为 /spartan/my_extension::my_extension/,其中 my_extension::my_extension 是您的扩展程序的 ID。现在,当该用户登录时,系统会将其转到指定的扩展程序。

代码拆分

代码拆分是一种仅在需要时请求代码的技术。通常,代码块会与 React 路由相关联,其中每个路由都会获得自己的代码块。在 React 中,可通过 SuspenseReact.lazy 组件完成此操作。在加载代码块时,Suspense 组件会显示一个后备组件。React.lazy 负责加载代码块。

设置代码拆分:

import { AsyncComp1 as Comp1 } from './Comp1.async'
import { AsyncComp1 as Comp2 } from './Comp2.async'

. . .

                <Suspense fallback={<div>Loading...</div>}>
                  <Switch>
                      <Route path="/comp1">
                        <Comp1 />
                      </Route>
                      <Route path="/comp2">
                        <Comp2 />
                      </Route>
                  </Switch>
                <Suspense>

延迟加载组件的实现方式如下:

import { lazy } from 'react'

const Comp1 = lazy(
 async () => import(/* webpackChunkName: "comp1" */ './Comp1')
)

export const AsyncComp1 = () => &#60;Home />

该组件的实现方式如下。该组件必须导出为默认组件:

const Comp1 = () => {
  return (
    &#60;div&#62;Hello World&#60;/div&#62;
  )
}

export default Comp1

摇树

虽然 Looker SDK 目前支持摇树优化,但此功能仍需要改进。我们在不断修改 SDK 以改进摇树(优化)优化支持。其中一些更改可能需要重构代码才能进行使用,但如果需要这样做,我们会在版本说明中记录这些更改。

为了利用摇树优化,您使用的模块必须导出为 esmodule,并且导入的函数必须没有附带效应。适用于 TypeScript/JavaScript 的 Looker SDKLooker SDK 运行时库Looker 界面组件Looker Extension SDKExtension SDK for React 均满足这些要求。

在扩展程序中,使用 Looker SDK 4.0,并使用 React 的 Extensions SDK 中的 ExtensionProvider2ExtensionProvider40 组件。

以下代码可设置扩展程序提供程序。您需要告知提供方所需的 SDK:

import { MyExtension } from './MyExtension'
import { ExtensionProvider40 } from '@looker/extension-sdk-react'
import { Looker40SDK } from '@looker/sdk/lib/4.0/methods'
import { hot } from 'react-hot-loader/root'

export const App = hot(() => {

  return (
    &#60;ExtensionProvider2 type={Looker40SDK}&#62;
      &#60;MyExtension /&#62;
    &#60;/ExtensionProvider2&#62;
  )
})

请勿在扩展程序中使用以下导入样式

import * as lookerComponents from `@looker/components`

前面的示例纳入了 模块中的所有内容。而应仅导入您实际需要的组件。例如:

import { Paragraph }  from `@looker/components`

词汇表

  • 代码拆分 - 一种在实际需要之前延迟加载 JavaScript 的技术。理想情况下,您应尽可能缩小初始加载的 JavaScript 软件包。这可以通过使用代码拆分来实现。任何不立即需要的功能都不会加载,直到实际需要为止。
  • IDE - 集成式开发环境。用于创建和修改扩展程序的编辑器。示例包括 Visual Studio Code、Intellij 和 WebStorm。
  • 场景 - 通常是 Looker 中的页面视图。场景映射到主要路线。有时,场景中会包含映射到主要路线中的子路线的子场景。
  • 转译 - 接受以一种语言编写的源代码并将其转换为另一种具有类似抽象级别的语言的过程。例如从 TypeScript 转换为 JavaScript。