擴充功能架構 React 和 JavaScript 程式碼範例

本頁面提供以 React 和 JavaScript 編寫的程式碼範例,供您在擴充功能中使用常見函式。

使用 Looker Extension 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 4.0 移至 GA 時,ExtensionProvider40 就會建立。ExtensionProvider40 的優點在於開發人員不必選擇要使用的 SDK,因為 SDK 4.0 是唯一可用的版本。由於 SDK 3.1 未納入最終套件,因此這項做法可縮減套件大小。

如要從 Looker Extension SDK 新增函式,您必須先取得 SDK 參照,這可以透過供應商或全域方式完成。接著,您可以呼叫 SDK 函式,就像在任何 JavaScript 應用程式中一樣。

  • 如要從供應器存取 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 Extension SDK 提供 API,可存取 Looker 使用者屬性。使用者屬性存取權有兩種:

  • 受限 — 與擴充功能相關聯。範圍限定的使用者屬性會以擴充功能命名空間,且使用者屬性必須在 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 授權,才能使用本機存放區。

擴充功能 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 嵌入 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 端點:

  • 伺服器 Proxy:透過 Looker 伺服器存取端點。這項機制可讓 Looker 伺服器安全地設定用戶端 ID 和密鑰。
  • 擷取 Proxy:從使用者的瀏覽器存取端點。代理程式是 Looker UI。

無論是哪種情況,您都必須在擴充功能 external_api_urls 授權中指定外部 API 端點。

伺服器 Proxy

以下範例說明如何使用伺服器 Proxy 取得存取權方代碼,供擷取 Proxy 使用。用戶端 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'

擷取 Proxy

以下範例說明如何使用擷取 Proxy。這個例子會使用先前伺服器 Proxy 範例中的存取權杖。

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 使用者

如要定義 Spartan 使用者,您必須建立名為「Extensions Only」的群組

建立「Extensions Only」群組後,請前往 Looker 的「Admin」部分,然後在「User Attributes」頁面編輯 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,且匯入的函式不得有副作用。Looker SDK for TypeScript/JavascriptLooker SDK 執行階段程式庫Looker UI 元件Looker Extension SDKExtension SDK for React 都符合這些要求。

在擴充功能中使用 Looker SDK 4.0,並使用 Extension SDK for React 中的 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。