【问题标题】:Check if user has run it检查用户是否已经运行它
【发布时间】:2018-08-10 17:04:18
【问题描述】:

我运行一个将文件上传到用户的 Google Drive 文件的 Google Apps 脚本:

function doGet(e) {
  var blob = UrlFetchApp.fetch(e.parameters.url).getBlob();
  DriveApp.createFile(blob);
  return HtmlService.createHtmlOutput("DONE!");
}

我的网站会加载一个弹出窗口,该窗口使用该代码运行 Google Apps 脚本。工作正常。

现在,我如何向我的网站传达返回他们的用户已成功上传文件?如,我如何向我的服务器传达用户已成功上传的信息?已运行doGet()?`

必须存在某种类型的response 处理?

完整的工作代码(在 JSBin 上进行测试):

<!DOCTYPE html>
<html>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
  </head>
  <body>
    <div class="google-upload" data-url="https://calibre-ebook.com/downloads/demos/demo.docx">
      <span style="background-color: #ddd">Upload</span>
    </div>
    <script>
      $(function() {
        $(".google-upload").click(function() {
          var url = "https://script.google.com/macros/s/AKfycbwsuIcO5R86Xgv4E1k1ZtgtfKaENaKq2ZfsLGWZ4aqR0d9WBYc/exec"; // Please input the URL here.
          var withQuery = url + "?url=";
          window.open(withQuery + $('.google-upload').attr("data-url"), "_blank", "width=600,height=600,scrollbars=1");
        });
      });
    </script>
  </body>
</html>

所以澄清一下,我想要一种方法来确定用户是否已成功上传文件。比如:

request.execute(function(response) {
    if (response.code == 'uploaded') {
        // uploaded, do stuff
    } else {
        // you get the idea...
    }
});

为这个问题的完整解决方案添加赏金。

【问题讨论】:

  • 如果这个问题有点复杂,我会奖励 50 分。
  • 这是在第三方网站上吗?如果是这样,您可以使用UrlFetchApp 将请求(可能是200 OK)发回您的服务器进行处理。如果您使用的是 Apps Script Web 应用程序,则可以使用链接到 google.script.run 命令的 withSuccessHandler
  • @Brian 请查看我更新的问题。我添加了工作代码示例。我已经开始赏金了。谢谢!
  • @TarunLalwani 有趣。这可能是一种方法。请随时使用jsbin 发布带有测试代码的答案。
  • @TarunLalwani 您对 postMessage 的建议非常好。但是,它专门被谷歌应用脚​​本阻止。如前所述here

标签: google-apps-script


【解决方案1】:

您可以使用 jQuery 的 $.getJSON 方法传递数据,而不是返回 HtmlService 对象,并使用 ContentServicedoGet 函数中检索数据。 Google Apps 脚本不接受 CORS,因此使用 JSONP 是从脚本获取数据的最佳方式。 See this post 了解更多。

Working CodePen Example

为了清楚起见,我拆分了您的 HTML 和脚本。与原始示例相比,HTML 没有任何变化。

代码.gs

function doGet(e) { 
  var returnValue;

  // Set the callback param. See https://stackoverflow.com/questions/29525860/
  var callback = e.parameter.callback;

  // Get the file and create it in Drive
  try {
    var blob = UrlFetchApp.fetch(e.parameters.url).getBlob();
    DriveApp.createFile(blob);

    // If successful, return okay 
    // Structure this JSON however you want. Parsing happens on the client side.
    returnValue = {status: 'okay'};
  } catch(e) {
    Logger.log(e);
    // If a failure, return error message to the client
    returnValue = {status: e.message}
  }

  // Returning as JSONP allows for crossorigin requests
  return ContentService.createTextOutput(callback +'(' + JSON.stringify(returnValue) + ')').setMimeType(ContentService.MimeType.JAVASCRIPT);
}

客户端 JS

$(function() {
  $(".google-upload").click(function() {
    var appUrl = "https://script.google.com/macros/s/AKfycbyUvgKdhubzlpYmO3Marv7iFOZwJNJZaZrFTXCksxtl2kqW7vg/exec"; 
    var query = appUrl + "?url=";
    var popupUrl = query + $('.google-upload').attr("data-url") + "&callback=?";

    console.log(popupUrl)

    // Open this to start authentication.
    // If already authenticated, the window will close on its own.
    var popup = window.open(popupUrl, "_blank", "width=600,height=600,scrollbars=1");

    $.getJSON(popupUrl, function(returnValue) {
      // Log the value from the script
      console.log(returnValue.status);
      if(returnValue.status == "okay") {
        // Do stuff, like notify the user, close the window
        popup.close();
        $("#result").html("Document successfully uploaded");
      } else {
        $("#result").html(returnValue);
      }
    })
  });
});

您可以通过在data-url 参数中传递一个空字符串来测试错误消息。消息在控制台以及用户页面中返回。

编辑 3.7.18

上述解决方案在控制授权流程方面存在问题。在研究并与 Drive 工程师 (see thread here) 交谈后,我将其重新设计为基于 Apps Script API 的自托管示例,并将项目作为 API 可执行文件而不是 Apps Script Web 应用程序运行。这将允许您在 Apps Script Web 应用程序之外访问 [run](https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run) 方法。

设置

关注Google Apps Script API instructions for JavaScript。 Apps 脚本项目应该是独立的(未链接到文档)并作为 API 可执行文件发布。您需要打开 Cloud Console 并创建 OAuth 凭据和 API 密钥。

这些说明让您在计算机上使用 Python 服务器。我使用 Node JS 服务器http-server,但您也可以将其上线并从那里进行测试。您需要在 Cloud Console 中将您的来源列入白名单。

客户

由于这是自托管的,您需要一个纯 HTML 页面,该页面通过 JavaScript 通过OAuth2 API 授权用户。这是可取的,因为它使用户保持登录状态,允许对您的脚本进行多次 API 调用而无需重新授权。以下代码适用于此应用程序,并使用 Google 快速入门指南中的授权流程。

index.html

  <body>

    <!--Add buttons to initiate auth sequence and sign out-->
    <button id="authorize-button" style="display: none;">Authorize</button>
    <button id="signout-button" style="display: none;">Sign Out</button>

    <button onclick="uploadDoc()" style="margin: 10px;" id="google-upload" data-url="https://calibre-ebook.com/downloads/demos/demo.docx">Upload doc</button>

    <pre id="content"></pre>
</body>

index.js

// Client ID and API key from the Developer Console
  var CLIENT_ID = 'YOUR_CLIENT_ID';
  var API_KEY = 'YOUR_API_KEY';
  var SCRIPT_ID = 'YOUR_SCRIPT_ID';

  // Array of API discovery doc URLs for APIs used by the quickstart
  var DISCOVERY_DOCS = ["https://script.googleapis.com/$discovery/rest?version=v1"];

  // Authorization scopes required by the API; multiple scopes can be
  // included, separated by spaces.
  var SCOPES = 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/script.external_request';

  var authorizeButton = document.getElementById('authorize-button');
  var signoutButton = document.getElementById('signout-button');
  var uploadButton = document.getElementById('google-upload');
  var docUrl = uploadButton.getAttribute('data-url').value;

  // Set the global variable for user authentication
  var isAuth = false;

  /**
   *  On load, called to load the auth2 library and API client library.
   */
  function handleClientLoad() {
    gapi.load('client:auth2', initClient);
  }

  /**
   *  Initializes the API client library and sets up sign-in state
   *  listeners.
   */
  function initClient() {
    gapi.client.init({
      apiKey: API_KEY,
      clientId: CLIENT_ID,
      discoveryDocs: DISCOVERY_DOCS,
      scope: SCOPES
    }).then(function () {
      // Listen for sign-in state changes.
    gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);

      // Handle the initial sign-in state.
      updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
      authorizeButton.onclick = handleAuthClick;
      signoutButton.onclick = handleSignoutClick;
      // uploadButton.onclick = uploadDoc;
    });
  }

  /**
   *  Called when the Upload button is clicked. Reset the
   *  global variable to `true` and upload the document. 
   *  Thanks to @JackBrown for the logic.
   */
  function updateSigninStatus(isSignedIn) {
    if (isSignedIn && !isAuth) {
      authorizeButton.style.display = 'none';
      signoutButton.style.display = 'block';
      uploadButton.style.display = 'block'
      uploadButton.onclick  = uploadDoc;
    } else if (isSignedIn && isAuth) {
      authorizeButton.style.display = 'none';
      signoutButton.style.display = 'block';
      uploadButton.style.display = 'block';
      uploadDoc();
    } else {
      authorizeButton.style.display = 'block';
      signoutButton.style.display = 'none';
      uploadButton.style.display = 'none';
      isAuth = false;
    }
  }

  /**
   *  Sign in the user upon button click.
   */
  function handleAuthClick(event) {
    gapi.auth2.getAuthInstance().signIn();
    isAuth = true;  // Update the global variable
  }


  /**
   *  Sign out the user upon button click.
   */
  function handleSignoutClick(event) {
    gapi.auth2.getAuthInstance().signOut();
    isAuth = false;  // update the global variable
  }

  /**
   * Append a pre element to the body containing the given message
   * as its text node. Used to display the results of the API call.
   *
   * @param {string} message Text to be placed in pre element.
   */
  function appendPre(message) {
    var pre = document.getElementById('content');
    var textContent = document.createTextNode(message + '\n');
    pre.appendChild(textContent);
  }

  /**
    * Handle the login if signed out, return a Promise
    * to call the upload Docs function after signin.
  **/
  function uploadDoc() {

      console.log("clicked!")
      var docUrl = document.getElementById('google-upload').getAttribute('data-url');

      gapi.client.script.scripts.run({
        'scriptId':SCRIPT_ID,
        'function':'uploadDoc',
        'parameters': [ docUrl ]
      }).then(function(resp) {
        var result = resp.result;
        if(result.error && result.error.status) {
          // Error before the script was Called
          appendPre('Error calling API');
          appendPre(JSON.parse(result, null, 2));
        } else if(result.error) {
          // The API executed, but the script returned an error.

            // Extract the first (and only) set of error details.
            // The values of this object are the script's 'errorMessage' and
            // 'errorType', and an array of stack trace elements.
            var error = result.error.details[0];
            appendPre('Script error message: ' + error.errorMessage);

            if (error.scriptStackTraceElements) {
              // There may not be a stacktrace if the script didn't start
              // executing.
              appendPre('Script error stacktrace:');
              for (var i = 0; i < error.scriptStackTraceElements.length; i++) {
                var trace = error.scriptStackTraceElements[i];
                appendPre('\t' + trace.function + ':' + trace.lineNumber);
              }
            }
          } else {
            // The structure of the result will depend upon what the Apps
            // Script function returns. Here, the function returns an Apps
            // Script Object with String keys and values, and so the result
            // is treated as a JavaScript object (folderSet).
            console.log(resp.result)
            var msg = resp.result.response.result;
            appendPre(msg);

            // do more stuff with the response code
        }
      })
    }

应用脚本

Apps Script 代码不需要太多修改。我们可以返回纯 JSON 对象以供客户端使用,而不是使用 ContentService 返回。

function uploadDoc(e) {
  Logger.log(e);
  var returnValue = {};

  // Set the callback URL. See https://stackoverflow.com/questions/29525860/

  Logger.log("Uploading the document...");

  try { 
    // Get the file and create it in Drive
    var blob = UrlFetchApp.fetch(e).getBlob();
    DriveApp.createFile(blob);

    // If successful, return okay 
    var msg = "The document was successfully uploaded!";
    return msg;
  } catch(e) {
    Logger.log(e);
    // If a failure, return error message to the client
    return e.message
  }
}

我很难将 CodePen 列入白名单,所以 I have an example hosted securely on my own site 使用上面的代码。随意检查源代码并查看live Apps Script project

请注意,当您在 Apps 脚本项目中添加或更改范围时,用户需要重新授权。

【讨论】:

  • 当用户尚未授权脚本时,这是否可行?
  • 好收获。本来想加的,忘记了。我已经更新了演示以及上面的脚本。
  • 布赖恩,这绝对是美丽的。在我们说话的时候,我正在测试这个。不过有一个主要问题。当用户第一次点击它时(当他们被要求授予权限等时),它不会检索数据,弹出窗口保持打开状态。到目前为止在 Firefox 上进行了测试。但是当您已经授权该应用程序时,它可以优雅地工作。所以第二次点击上传按钮就可以了!您可以通过使用尚未授权的新 Google 帐户运行 script 来测试此问题。有解决办法吗?
  • @Brian 我正在测试这个,谢谢。在玩你的演示时,我注意到授权过程与上传按钮是分开的。意思是,用户需要先点击授权,然后点击上传按钮。有没有办法合并这些(类似于以前的工作流程)?就用户体验而言,这可能是一场噩梦。
  • @Brian 我相信您可以使用布尔标志变量合并授权过程和上传功能。如果 handleAuthClick(event) 被激活,我将布尔变量设置为 true 。用户登录后,会调用updateSigninStatus(),在这里我检查登录状态并检查布尔变量是否为真,然后调用uploadDoc()。但如果布尔变量为假,我会显示注销和上传按钮供用户选择。工作示例here
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-10-29
  • 1970-01-01
  • 2014-08-08
  • 2015-05-05
  • 2015-01-31
  • 1970-01-01
相关资源
最近更新 更多