使用toUri()在iframe中加载Google Picker API时回调不触发
问题原因
当使用picker.toUri()将Google Picker嵌入iframe时,Picker的回调机制依赖跨窗口消息通信(postMessage),而非模态框版本的同上下文直接调用。默认情况下,iframe内的Picker页面无法直接触发父页面的pickerCallback函数,必须手动配置消息接收逻辑并指定消息发送的目标域名。
解决方案
需要完成两个关键修改:
- 给Picker实例设置父页面的源域名,确保消息能正确发送到父页面
- 在父页面监听
message事件,接收Picker的回调数据并转发到原回调函数
修改后的完整代码
<!DOCTYPE html> <html> <head> <title>Picker API Quickstart</title> <meta charset="utf-8" /> </head> <body> <p>Picker API API Quickstart</p> <!--Add buttons to initiate auth sequence and sign out--> <button id="authorize_button" onclick="handleAuthClick()">Authorize</button> <button id="signout_button" onclick="handleSignoutClick()">Sign Out</button> <pre id="content" style="white-space: pre-wrap;"></pre> <iframe id="pickerFrame" style="display: none;width:500px;height:500px"></iframe> <script type="text/javascript"> /* exported gapiLoaded */ /* exported gisLoaded */ /* exported handleAuthClick */ /* exported handleSignoutClick */ // Authorization scopes required by the API; multiple scopes can be // included, separated by spaces. const SCOPES = 'https://www.googleapis.com/auth/drive.file'; // TODO(developer): Set to client ID and API key from the Developer Console const CLIENT_ID = '******'; const API_KEY = '******'; // TODO(developer): Replace with your own project number from console.developers.google.com. const APP_ID = '******'; // 设置父页面的源(当前页面域名,比如http://localhost:8080) const PARENT_ORIGIN = window.location.origin; let tokenClient; let accessToken = null; let pickerInited = false; let gisInited = false; document.getElementById('authorize_button').style.visibility = 'hidden'; document.getElementById('signout_button').style.visibility = 'hidden'; // 监听message事件,接收Picker的回调数据 window.addEventListener('message', (event) => { // 验证消息来源,防止恶意消息 if (event.origin.startsWith('https://docs.google.com')) { pickerCallback(event.data); } }); /** * Callback after api.js is loaded. */ function gapiLoaded() { gapi.load('client:picker', initializePicker); } /** * Callback after the API client is loaded. Loads the * discovery doc to initialize the API. */ async function initializePicker() { await gapi.client.load('https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'); pickerInited = true; maybeEnableButtons(); } /** * Callback after Google Identity Services are loaded. */ function gisLoaded() { tokenClient = google.accounts.oauth2.initTokenClient({ client_id: CLIENT_ID, scope: SCOPES, callback: '', // defined later }); gisInited = true; maybeEnableButtons(); } /** * Enables user interaction after all libraries are loaded. */ function maybeEnableButtons() { if (pickerInited && gisInited) { document.getElementById('authorize_button').style.visibility = 'visible'; } } /** * Sign in the user upon button click. */ function handleAuthClick() { tokenClient.callback = async (response) => { if (response.error !== undefined) { throw (response); } accessToken = response.access_token; document.getElementById('signout_button').style.visibility = 'visible'; document.getElementById('authorize_button').innerText = 'Refresh'; await createPicker(); }; if (accessToken === null) { // Prompt the user to select a Google Account and ask for consent to share their data // when establishing a new session. tokenClient.requestAccessToken({ prompt: 'consent' }); } else { // Skip display of account chooser and consent dialog for an existing session. tokenClient.requestAccessToken({ prompt: '' }); } } /** * Sign out the user upon button click. */ function handleSignoutClick() { if (accessToken) { accessToken = null; google.accounts.oauth2.revoke(accessToken); document.getElementById('content').innerText = ''; document.getElementById('authorize_button').innerText = 'Authorize'; document.getElementById('signout_button').style.visibility = 'hidden'; } } /** * Create and render a Picker object for searching images. */ function createPicker() { const view = new google.picker.View(google.picker.ViewId.DOCS); view.setMimeTypes('image/png,image/jpeg,image/jpg'); const picker = new google.picker.PickerBuilder() .enableFeature(google.picker.Feature.NAV_HIDDEN) .setAppId(APP_ID) .setDeveloperKey(API_KEY) .setOAuthToken(accessToken) .addView(view) .setCallback(pickerCallback) // 设置父页面的源,让Picker知道要把消息发送到哪里 .setOrigin(PARENT_ORIGIN); picker.build(); document.getElementById('pickerFrame').src = picker.toUri(); document.getElementById('pickerFrame').style.display = 'block'; //picker.setVisible(true); } /** * Displays the file details of the user's selection. * @param {object} data - Containers the user selection from the picker */ async function pickerCallback(data) { if (data.action === google.picker.Action.PICKED) { let text = `Picker response: \n${JSON.stringify(data, null, 2)}\n`; const document = data[google.picker.Response.DOCUMENTS][0]; const fileId = document[google.picker.Document.ID]; console.log(fileId); const res = await gapi.client.drive.files.get({ 'fileId': fileId, 'fields': '*', }); text += `Drive API response for first document: \n${JSON.stringify(res.result, null, 2)}\n`; window.document.getElementById('content').innerText = text; } } </script> <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script> <script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script> </body> </html>
关键修改点说明
- 添加
PARENT_ORIGIN常量:获取当前页面的域名(window.location.origin),确保Picker能将消息发送到正确的父页面。 - 监听
message事件:在父页面接收来自Picker iframe的消息,验证来源为Google Docs域名后,调用原pickerCallback函数处理数据。 - PickerBuilder添加
setOrigin:明确告诉Picker要把回调消息发送到哪个域名,这是跨窗口通信的必要配置。
注意事项
- 确保你的页面使用HTTPS(本地开发
localhost除外),Google Picker的postMessage在非安全域名下可能受限。 - 验证消息来源(
event.origin.startsWith('https://docs.google.com'))是安全最佳实践,避免接收恶意网站的消息。
内容的提问来源于stack exchange,提问作者BiggeekTX
相关产品推荐
相关产品推荐

