扩展框架 React 和 JavaScript 代码示例

本页面提供了使用 React 和 JavaScript 编写的代码示例,这些示例适用于您可能希望在扩展程序中使用的常用函数。

使用 Looker 扩展程序 SDK

扩展程序必须与 Looker 主机建立连接。在 React 中,这是通过将扩展程序封装在 ExtensionProvider40 组件中来实现的。此组件会与 Looker 主机建立连接,并将 Looker Extension 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 移至 GA 时,系统会创建 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 打开浏览器窗口。

若要打开指向当前 Looker 实例中某个位置的新窗口,需要 new_window 权限;若要打开在其他主机上运行的新窗口,则需要 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 实例中定义用户属性,然后才能使用该属性。要为用户属性添加命名空间,请使用扩展名作为属性名称的前缀。由于用户属性名称中不能使用短划线和英文冒号,因此扩展程序名称中的所有短划线和“::”字符都必须替换为下划线。

    例如:如果使用扩展程序 ID 为 my-extension::my-extension 的范围限定用户属性名为 my_value,则必须定义用户属性名称为 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 使用权

扩展程序 localhost API 是异步的,而非同步的浏览器本地存储空间 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

Spartan 是指一种方法,用于将 Looker 实例用作环境,以向指定的一组用户公开扩展程序(仅限扩展程序)。当用户前往 Looker 实例时,系统会显示 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()

定义 Spartan 用户

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

创建“仅限扩展程序”群组后,前往 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 目前支持 tree-shaking,但此功能仍需改进。我们会不断修改 SDK 以改进对树摇动的支持。其中一些更改可能需要重构代码才能进行使用,但如果需要这样做,我们会在版本说明中记录这些更改。

为了利用摇树优化,您使用的模块必须导出为 esmodule,并且导入的函数必须没有副作用。适用于 TypeScript/Javascript 的 Looker SDKLooker SDK 运行时库Looker 界面组件Looker 扩展程序 SDK适用于 React 的扩展程序 SDK 均符合这些要求。

在扩展程序中,使用 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。