访问网络资源
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:
<!-- 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
事件处理程序。
$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>"]
添加图片
在您的项目中添加一个占位符图片 ,当我们加载正确的图片时显示它。
然后在视图中向待办事项添加 <img>
标签:AngularJS index.html 或
JavaScript index_html:
<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 中,添加一个新方法,搜索待办事项列表,寻找还未加载的图片:
// 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.js
的
drop()
方法中,修改 URI 的处理方式,适当地检测有效的图片。为了简单起见,我们只测试
png 和 jpg 扩展名,您可以在您的代码中检测其他类型。
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 方法再次请求它们:
// 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 调用加载的资源。您可以在您的项目中自由使用它。
您还应该阅读
- Webview 标签 API 参考
- 嵌入内容教程
接下来做什么?
在 8 - 发布应用中,我们将告诉您如何在 Chrome 网上应用店中发布您的应用,结束实验。