扩展框架 React 和 JavaScript 代码示例

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

使用 Looker Extension SDK

如需通过 Looker Extension SDK 添加函数,您首先需要获取对该 SDK 的引用,此操作可通过提供程序或全局完成。然后,您可以调用 SDK 函数,方法与调用任何 JavaScript 应用一样。

  • 如需从提供程序访问 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 Extension SDK 提供了一个用于访问 Looker 用户属性的 API。用户属性访问权限分为两种:

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

    例如:名为“my_value”的限定了范围的用户属性(扩展名为 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) {
      . . .
    }

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

该扩展程序框架支持嵌入信息中心、Looks 和 Discovers。

需要有 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 和密钥。
  • fetch 代理 - 从用户浏览器访问端点。代理是 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 可用于获取访问令牌以访问特定资源,例如 Goorgle 表格文档。

您需要在 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()

对简单的用户进行定义

为了定义精简的用户,您必须创建一个名为“Only Extensions”(仅限扩展程序)的群组

创建“仅限扩展程序”组后,前往 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 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。