访问网络资源

Chrome 应用有一项严格的内容安全策略,不会让用户执行或加载远程托管的代码和资源。

然而许多应用都需要加载并显示来自远程位置的内容。例如新闻阅读器需要以内嵌方式显示远程内容,或者从远程 URL 加载并显示图片。

载入外部网络内容

互联网上的站点有固有的安全风险,直接在您的应用中以提升的权限渲染网页是漏洞的潜在来源。

Chrome 应用为开发者提供了在 <webview> 标签中安全渲染第三方内容的能力。webview 就像 iframe 一样,但是您能够更灵活地控制它,并具有额外的安全性。它在单独的经过沙盒屏蔽的进程中运行,不能直接与应用通信。

webview 包含一些很简单的 API,在您的应用中您可以:

  • 更改 webview 的 URL。
  • 前进和后退导航、停止加载和重新加载。
  • 检查 webview 是否加载完成,如果可能的话在历史栈中前进和后退。

我们会修改我们的代码,当用户单击某个链接时,在 webview 中渲染拖放操作中的 URL 的内容。

更新清单文件

在清单文件中请求一个新的权限——"webview",AngularJS manifest.json JavaScript manifest.json 是一样的:

"permissions": ["storage", "webview"]

更新视图

在视图中添加一个 <webview> 标签和一个链接:AngularJS index.html JavaScript index.html

Angular
JavaScript
<!-- in TODO item: -->
<a ng-show="todo.uri" href="" ng-click="showUri(todo.uri)">(view url)</a>
<!-- at the bottom, below the end of body: -->
<webview></webview>
    
<!-- right after the "dropText" div: -->
<webview></webview>
<!-- in the TODO template, right before the end of the li: -->
<a style="display: none;" href="">(view url)</a>
    

更新样式表

在样式表中为 webview 标签设置合适的宽度和高度(默认大小为零),AngularJS todo.css JavaScript todo.css 是一样的:

webview {
  width: 100%;
  height: 200px;
}

更新控制器

我们现在只需要向 AngularJS controller.js 添加 showUrl方法,或者向 JavaScript controller.js 添加一个 showUrl 事件处理程序。

Angular
JavaScript
$scope.showUri = function(uri) {
  var webview=document.querySelector("webview");
  webview.src=uri;
};
    
if(/^http:\/\/|https:\/\//.test(todoObj.text)) {
  var showUrl = el.querySelector('a');
  showUrl.addEventListener('click', function(e) {
    e.preventDefault();
    var webview=document.querySelector("webview");
    webview.src=todoObj.text;
  });
  showUrl.style.display = 'inline';
}
    

检查结果

为了测试,请打开应用,单击右键,并选择“重新加载应用”。您应该能够在拖放的 URL 待办事项上单击“view url”链接,相应的网页就会显示在 webview 中。如果没有显示的话,审查页面并确认您正确设置了 webview 的大小。

如果您遇到了困难,希望立刻看到修改后的应用,请进入 chrome://extensions,加载未打包的 AngularJS 1_webview JavaScript 1_webview ,并从新标签页中运行应用。

加载外部图片

如果您试着在 index.html 中添加一个 <img> 标签,并将它的 src 属性指向网上的任何一个站点,则会在控制台中产生如下异常,图片不会加载:

Refused to load the image 'http://angularjs.org/img/AngularJS-large.png' because it violates the following Content Security Policy directive: "img-src 'self' data: chrome-extension-resource:".(拒绝加载图片 'http://angularjs.org/img/AngularJS-large.png',因为它违反了以下内容安全策略的指示:"img-src 'self' data: chrome-extension-resource:"。)

由于内容安全策略的限制,Chrome 应用不能直接在 DOM 中加载任何外部资源。

为了避免这一限制,您可以使用 XHR 请求,获取远程文件对应的 Blob,并以合适的方式使用它。例如,<img> 标签可以使用 Blob URL。让我们修改我们的应用,如果拖放的 URL 表示图片的话,在待办事项列表中显示一个小图标。

更新清单文件

在您开始发出 XHR 请求前,您必须请求权限。因为我们希望允许用户拖放来自任意服务器的图片,我们需要请求向任意 URL 发出 XHR 的权限。修改 AngularJS manifest.json JavaScript manifest.json

"permissions": ["storage", "webview", "<all_urls>"]

添加图片

在您的项目中添加一个占位符图片 loading.gif,当我们加载正确的图片时显示它。

然后在视图中向待办事项添加 <img> 标签:AngularJS index.html JavaScript index_html

Angular
JavaScript
<img style="max-height: 48px; max-width: 120px;" ng-show="todo.validImage"
ng-src="{{todo.imageUrl}}"></img>
    
<img style="max-height: 48px; max-width: 120px;"></img>
    

JavaScript controller.js 中,添加一个占位符图片的常量:

const PLACEHOLDER_IMAGE = "loading.gif";

您很快就会看到,只有当待办事项的 validImage 属性为 true 时才会显示该元素。

更新控制器

添加 loadImage 方法,发出 XHR 请求,并执行一个接受 Blob URL 的回调函数,AngularJS loader.js JavaScript loader.js 是一样的:

var loadImage = function(uri, callback) {
  var xhr = new XMLHttpRequest();
  xhr.responseType = 'blob';
  xhr.onload = function() {
    callback(window.URL.createObjectURL(xhr.response), uri);
  }
  xhr.open('GET', uri, true);
  xhr.send();
}

AngularJS controller.js JavaScript controller.js 中,添加一个新方法,搜索待办事项列表,寻找还未加载的图片:

Angular
JavaScript
// for each image with no imageUrl, start a new loader
$scope.loadImages = function() {
  for (var i=0; i<$scope.todos.length; i++) {
    var todo=$scope.todos[i];
    if (todo.validImage && todo.imageUrl===PLACEHOLDER_IMAGE) {
      loadImage(todo.uri, function(blob_uri, requested_uri) {
        $scope.$apply(function(scope) {
          for (var k=0; k<scope.todos.length; k++) {
            if (scope.todos[k].uri==requested_uri) {
              scope.todos[k].imageUrl = blob_uri;
            }
          }
        });
      });
    }
  }
};
    
 var maybeStartImageLoader = function(el, todo) {
    var img = el.querySelector('img');
    if (todo['extras'] && todo.extras.validImage && todo.extras.imageUrl) {
      if (todo.extras.imageUrl===PLACEHOLDER_IMAGE) {
        img.src = PLACEHOLDER_IMAGE;
        img.style.display = 'inline';
        window.loadImage(todo.extras.uri, function(blob_uri, requested_uri) {
          todo.extras.imageUrl = blob_uri;
          img.src = blob_uri;
        });
      } else {
        img.src = todo.extras.imageUrl;
        img.style.display = 'inline';
      }
    } else {
      img.style.display = 'none'; 
    }
  };
    

如果您使用 JavaScript 编写您的应用,您需要在 JavaScript controller.js 中调用 maybeStartImageLoader 函数,根据模型更新待办事项列表:

var updateTodo = function(model) {
  var todoElement = list.querySelector('li[data-id="'+model.id+'"]');
  if (todoElement) {
    var checkbox = todoElement.querySelector('input[type="checkbox"]');
    var desc = todoElement.querySelector('span');
    checkbox.checked = model.isDone;
    desc.innerText = model.text;
    desc.className = "done-"+model.isDone;

    // load image, if this ToDo has image data
    maybeStartImageLoader(todoElement, model);
  }
}

然后,在 AngularJS controller.js JavaScript.controller.jsdrop() 方法中,修改 URI 的处理方式,适当地检测有效的图片。为了简单起见,我们只测试 png 和 jpg 扩展名,您可以在您的代码中检测其他类型。

Angular
JavaScript
var uri=e.dataTransfer.getData("text/uri-list");
var todo = {text:uri, done:false, uri: uri};
if (/.png$/.test(uri) || /.jpg$/.test(uri)) {
  hasImage = true;
  todo.validImage = true;
  todo.imageUrl = PLACEHOLDER_IMAGE;
}
newTodos.push(todo);

// [...] inside the $apply method, before save(), call the loadImages method:
$scope.loadImages();
    
var uri = e.dataTransfer.getData("text/uri-list");
var extras = { uri: uri };
if (/\.png$/.test(uri) || /\.jpg$/.test(uri)) {
  hasImage = true;
  extras.validImage = true;
  extras.imageUrl = PLACEHOLDER_IMAGE;
}
model.addTodo(uri, false, extras);
    

最后,我们要修改 load 方法,重新设置 Blob URL,因为 Blob URL 不会在会话间保留。将待办事项的 imageUrls 设置为 PLACEHOLDER_IMAGE 强制使 loadImages 方法再次请求它们:

Angular
JavaScript
// If there is saved data in storage, use it. Otherwise, bootstrap with sample todos
$scope.load = function(value) {
  if (value && value.todolist) {
    // ObjectURLs are revoked when the document is removed from memory,
    // so we need to reload all images.
    for (var i=0; i<value.todolist.length; i++) {
      value.todolist[i].imageUrl = PLACEHOLDER_IMAGE;
    }
    $scope.todos = value.todolist;
    $scope.loadImages();
  } else {
    $scope.todos = [
      {text:'learn angular', done:true},
      {text:'build an angular app', done:false}];
  }
}
    
/**
 * Listen to changes in the model and trigger the appropriate changes in the view
 **/
model.addListener(function(model, changeType, param) {
  if ( changeType === 'reset' ) {
    // let's invalidate all Blob URLs, since their lifetime is tied to the document's lifetime
    for (var id in model.todos) {
      if (model.todos[id].extras && model.todos[id].extras.validImage) {
        model.todos[id].extras.imageUrl = PLACEHOLDER_IMAGE;
      }
    }
  }

  if ( changeType === 'removed' || changeType === 'archived' || changeType === 'reset') {
    redrawUI(model);
  } else if ( changeType === 'added' ) {
    drawTodo(model.todos[param], list);
  } else if ( changeType === 'stateChanged') {
    updateTodo(model.todos[param]);
  }
  storageSave();
  updateCounters(model);
});
    

检查结果

为了测试,打开应用,单击右键,并选择重新加载应用。进入 Google 图片,搜索并选择一个图片,并将图片拖放到待办事项列表应用。如果没有任何错误的话,您应该看到拖放到待办事项应用的每一个图片 URL 的缩略图。

注意,我们在这一更改中没有处理从文件管理器中拖放的本地图片,我们将它作为一项挑战留给您。

如果您遇到了困难,希望立刻看到修改后的应用,请进入 chrome://extensions,加载未打包的 AngularJS 2_loading_resources JavaScript 2_loading_resources,并从新标签页中运行应用。

以上的 loadImage() 方法并不是这一问题的最佳解决方案,因为它不能正确处理错误,还会将图片缓存在本地文件系统中。我们建立了 apps-resource-loader ,它的健壮性更好。

更多信息

  • <webview> 标签允许您在您的应用中包含一个受控的浏览器。例如,如果您的应用有一部分不兼容 CSP,而且您没有精力马上迁移,您可以使用它。我们没有在这里提到的一个特性是 webview 可以使用异步的 postMessages 和您的应用(或反过来)通信。

  • 与标准网页相比,从网上加载图片之类的资源并不是那么直白,但是与传统的本机平台相比并没有太大的区别,您都需要处理资源下载,只有当它正确下载后才能在用户界面中显示。我们也开发了一个示例库,异步处理从 XHR 调用加载的资源。您可以在您的项目中自由使用它。

您还应该阅读

接下来做什么?

8 - 发布应用中,我们将告诉您如何在 Chrome 网上应用店中发布您的应用,结束实验。