扩展框架 React 和 JavaScript 代码示例

Looker 扩展框架很快将使用新的加载机制。新的加载器可能会在加载现有的扩展程序时引发错误。如需了解如何在新加载程序在 Looker 环境中正式启用扩展程序之前测试扩展程序,请参阅 Looker 帮助中心内的测试新的扩展程序框架加载器一文。

本页提供了您可能想在扩展程序中使用的常见函数的代码示例。

使用 Looker 扩展程序 SDK

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

  • 要从提供方访问 SDK,请执行以下操作:
  import { ExtensionContext2 } from '@looker/extension-sdk-react'

  export const Comp1 = () => {
    const extensionContext = useContext(
      ExtensionContext2
    )
    const { extensionSDK, coreSDK } = extensionContext
  • 如需从全局访问 SDK,必须先对扩展程序进行初始化,然后才能调用该方法:
    const coreSDK = getCoreSDK2()

现在,您可以像使用任何 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 { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

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

. . .

  extensionSDK.updateLocation('/browse')

打开新的浏览器窗口

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

此函数需要使用 new_window 权限打开当前 Looker 实例中某个位置的新窗口,或 new_window_external_urls 权限以打开在其他主机上运行的新窗口。

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

. . .

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

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

路由和深层链接

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

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

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

扩展程序上下文数据

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

扩展程序能够在扩展程序的所有用户之间共享上下文数据。上下文数据可用于不会经常变化且没有特殊安全要求的数据。写入数据时应谨慎,因为没有数据锁定,因此最后一次写入生效。启动时,扩展程序将立即获得上下文数据。Looker Extension SDK 提供了一些函数,用于更新和刷新上下文数据。

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

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

. . .

  const extensionContext = useContext(
    ExtensionContext2
  )
  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 { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext2
  )
  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 { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext2
  )
  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 { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

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

  extensionSDK.updateTitle('My Extension Title')

写入系统剪贴板

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

如需向系统剪贴板写入数据,您需要拥有 use_clipboard 权利

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

. . .

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

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

嵌入信息中心、外观和探索

扩展程序框架支持嵌入信息中心、外观和探索。可以嵌入常规信息中心和旧版信息中心。

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

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

. . .

  const extensionContext = useContext(
    ExtensionContext2
  )
  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 (<EmbedContainer ref={embedCtrRef} />)

该扩展程序的示例使用样式化组件为生成的 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 { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext2
  )
  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 { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext2
  )
  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 { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext2
  )
  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<string, string> = {
    client_id: GITHUB_CLIENT_ID!,
    response_type: 'code',
  }
  const response = await extensionSDK.oauth2Authenticate(
    'https://github.com/login/oauth/authorize',
    authenticateParameters,
   'GET'
  )
  const exchangeParameters: Record<string, string> = {
    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 { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

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

. . .

  const authRequest: Record<string, string> = {
    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<string, string> = {
    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 { ExtensionContext2 } from '@looker/extension-sdk-react'

. . .

  const extensionContext = useContext(
    ExtensionContext2
  )
  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 扩展 SDKExtension SDK for React 都能做到这一点。

在扩展程序中,您应该选择一个 Looker SDK(3.1 或 4.0),并使用 Extension SDK for React 中的 ExtensionProvider2 组件。如果您同时需要这两种 SDK,请继续使用 ExtensionProvider 组件,但您会发现最终的软件包大小有所增加。

以下代码可设置扩展程序提供程序。您需要告知提供商您想要哪个 SDK:

import { MyExtension } from './MyExtension'
import { ExtensionProvider2 } 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 中的网页浏览。场景会映射到主要路线。有时,某个场景会具有映射到主路线中的子路线的子场景。
  • Transpile - 获取以一种语言编写的源代码并将其转换为具有类似抽象级别的语言的过程。例如 TypeScript 到 JavaScript。