我们将继续探索 NativeScript 的核心,寻找利用 NativeScript 的实用方法。我们将向您展示如何在您的移动应用程序中渲染 PDF 文件。
为了简洁起见,我们将重点关注 iOS,但如果您想让我们在另一篇博客文章中讲解 Android 版本,请在下方告知我们,因为 iOS 和 Android 版本都可以在演示中使用。
您可以使用手机上的“NativeScript 预览”应用程序(可在 App Store 和 Google Play 获取)扫描 StackBlitz 中的二维码。当您编辑应用程序时,这些更改将实时推送到您的手机。非常酷!
我们可以将平台实现之间的许多通用功能合并到单个 PDFViewCommon
类中。
//src/app/native-pdfview/common.ts
import { View, Folder, Property } from '@nativescript/core';
let tmpFolder: Folder;
export abstract class PDFViewCommon extends View {
static loadStartEvent = 'loadStart';
static loadEndEvent = 'loadEnd';
src: string;
abstract loadPDF(src: string);
}
export const srcProperty = new Property<PDFViewCommon, string>({
name: 'src',
});
srcProperty.register(PDFViewCommon);
我们的事件名称可以共享,因此我们可以在这里定义它们。我们还将定义一个 src
属性,我们将在 PDFViewCommon
上将其注册为 Property
对象。这是一个非常重要的概念,我们稍后会解释。
首先让我们解决临时文件创建的需求。
我们需要将 PDF 保存到我们的设备上才能渲染它。我们将通过 createTempFile
方法来实现这一点。清除目录后,我们根据当前日期创建一个新文件,然后将 base64data
写入其中。将数据写入文件后,我们使用文件路径调用 loadPDF
。
protected createTempFile(base64data?: any) {
return new Promise<Folder>((resolve) => {
if (!tmpFolder) {
tmpFolder = knownFolders.documents().getFolder('PDFViewer.temp/');
}
tmpFolder.clear().then(() => {
if (base64data) {
const file = Folder.fromPath(tmpFolder.path).getFile(
`_${Date.now()}.pdf`
);
file.writeSync(base64data);
this.loadPDF(file.path);
} else {
resolve(tmpFolder);
}
});
});
}
在构建移动应用程序时,网络应用程序和底层原生操作系统之间存在明显的交接。NativeScript 在这方面做得非常好,使这些界限几乎消失。尽管如此,了解如何在 NativeScript 组件上注册属性以使其操作与 Angular 组件无法区分非常重要。
在下面的代码段中,我们在 NativeScript 组件上使用 Angular 绑定,以便在 pdfUrl
属性更新时触发 src
属性上的绑定。让我们花点时间讨论一下它是如何工作的。
<PDFView
[src]="pdfUrl"
(loadStart)="onLoadStart()"
(loadEnd)="onLoadEnd()"
></PDFView>
要创建可绑定属性,我们实例化一个新的 Property
对象,该对象以配置对象作为参数。在本例中,我们只需将 name
属性设置为 src
。我们将存储返回对象作为 srcProperty
变量,然后将该引用注册到 PDFViewCommon
类中。
export const srcProperty = new Property<PDFViewCommon, string>({
name: 'src',
});
srcProperty.register(PDFViewCommon);
此可绑定属性随后就像组件本身上的设置函数一样工作。当 src
属性设置时,将触发 [srcProperty.setNative]
,我们将 value
参数传递给我们的本地 loadPDF
函数。
export class PDFView extends PDFViewCommon {
[srcProperty.setNative](value: string) {
this.loadPDF(value);
}
loadPDF(src: string) {
// ...
}
}
如果您像我们一样,您可能会看到 [srcProperty.setNative]
代码片段并想“嗯?”。实际上在这里发生的是 @nativescript/core 提供的东西,它允许在底层“原生”组件准备好后处理我们的类实例上的设置器,在 iOS 的情况下,它是我们的 WKWebView 实例,在我们的示例中,PDFView
代表了它。
有了通用功能,我们就可以将注意力转移到具体的实现上。因为这是 iOS,我们将使用 委托模式 来帮助我们处理一些事件委托。我们在类上定义了一个私有成员,我们将在 createNativeView
方法中实例化它。我们还定义了一个名为 ios
的 getter 函数,它返回对组件的 nativeView
实例的引用,这也为我们提供了机会将其强类型化为其纯原生平台类 WKWebView。
import { PDFViewCommon, srcProperty } from './common';
export class PDFView extends PDFViewCommon {
private delegate: PDFWebViewDelegate;
// @ts-ignore
get ios(): WKWebView {
return this.nativeView;
}
createNativeView() {
// ...
}
[srcProperty.setNative](value: string) {
this.loadPDF(value);
}
loadPDF(src: string) {
// ...
}
}
这里发生了很多事情,所以我们将分块地进行讲解。除了基本设置和防御之外,最重要的是要指出我们正在调用 notify
方法,该方法可用于从 Observable 派生的所有 NativeScript 类,并将 PDFViewCommon.loadStartEvent
传递给它。
loadPDF(src: string) {
if (!src) {
return;
}
let url: NSURL;
this.notify({ eventName: PDFViewCommon.loadStartEvent, object: this });
// ...
}
然后我们实例化 base64data
属性并通过调用 createTempFile(base64data)
创建我们的临时文件。
loadPDF(src: string) {
// ...
const base64prefix = 'data:application/pdf;base64,';
if (src.indexOf(base64prefix) === 0) {
// https://developer.apple.com/documentation/foundation/nsdata/1410081-initwithbase64encodedstring?language=objc
const base64data = NSData.alloc().initWithBase64EncodedStringOptions(
src.substr(base64prefix.length),
0
);
this.createTempFile(base64data);
return;
}
// ...
}
根据我们尝试创建临时文件的結果,我们将尝试直接从设备加载文件或进行异步调用来加载它。
loadPDF(src: string) {
//...
if (src.indexOf('://') === -1) {
url = NSURL.fileURLWithPath(src);
this.ios.loadFileURLAllowingReadAccessToURL(url, url);
} else {
url = NSURL.URLWithString(src);
this.ios.loadRequest(NSURLRequest.requestWithURL(url));
}
}
您可以在下面看到该方法的完整代码。
loadPDF(src: string) {
if (!src) {
return;
}
let url: NSURL;
this.notify({ eventName: PDFViewCommon.loadStartEvent, object: this });
// detect base64 stream
const base64prefix = 'data:application/pdf;base64,';
if (src.indexOf(base64prefix) === 0) {
const base64data = NSData.alloc().initWithBase64EncodedStringOptions(
src.substr(base64prefix.length),
0
);
this.createTempFile(base64data);
return;
}
if (src.indexOf('://') === -1) {
url = NSURL.fileURLWithPath(src);
this.ios.loadFileURLAllowingReadAccessToURL(url, url);
} else {
url = NSURL.URLWithString(src);
const urlRequest = NSURLRequest.requestWithURL(url);
this.ios.loadRequest(urlRequest);
}
}
每个 NativeScript 自定义视图组件都可以实现自己的 createNativeView
,它可以返回您想要的任何平台视图。在本例中,我们可以创建我们自己的 WKWebView,并使用委托配置它以进行良好的事件处理,并设置一些配置选项以将我们的 PDF 加载到其中。
createNativeView() {
const webView = new WKWebView({
configuration: WKWebViewConfiguration.new(),
frame: UIScreen.mainScreen.bounds,
});
this.delegate = WKWebViewDelegate.initWithOwner(new WeakRef(this));
webView.navigationDelegate = this.delegate;
webView.opaque = false;
webView.autoresizingMask =
UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
return webView;
}
委托实现使用一种常见的模式,即使用对它拥有者的 WeakRef 静态初始化一个实例,在本例中,它将是我们的 PDFView
组件。我们实现了 WKNavigationDelegate 协议,该协议使我们能够强类型化各种方法,这些方法将为我们提供有关 WKWebView 行为的回调。非常棒。
@NativeClass()
class WKWebViewDelegate extends NSObject implements WKNavigationDelegate {
static ObjCProtocols = [WKNavigationDelegate];
private owner: WeakRef<PDFView>;
static initWithOwner(owner: WeakRef<PDFView>): WKWebViewDelegate {
const delegate = WKWebViewDelegate.new() as WKWebViewDelegate;
delegate.owner = owner;
return delegate;
}
webViewDidFinishNavigation(webView: WKWebView) {
const owner = this.owner?.deref();
if (owner) {
owner.notify({
eventName: PDFView.loadEndEvent,
object: owner,
});
}
}
}
由于我们自己的自定义 PDFView
组件处理了大部分功能,因此 Angular 实际上只需做很少的事情。我们将将其视为出色的封装!
与任何 NativeScript 组件一样,我们需要注册该元素,以便 Angular 知道它的存在,并像使用任何其他组件一样使用它。从那里,我们可以通过在加载我们选择的 PDF URL 时使用一个不错的页面阻塞加载指示器来改善用户体验。
import { Component } from '@angular/core';
import { LoadingIndicator } from '@nstudio/nativescript-loading-indicator';
import { registerElement } from '@nativescript/angular';
import { PDFView } from '../native-pdfview';
registerElement('PDFView', () => PDFView);
@Component({
selector: 'ns-pdf-webview',
templateUrl: './pdf-webview.component.html',
})
export class PDFWebViewComponent {
loadingIndicator = new LoadingIndicator();
pdfUrl =
'https://websitesetup.org/wp-content/uploads/2020/09/Javascript-Cheat-Sheet.pdf';
onLoadStart() {
console.log('pdf loading started...');
this.loadingIndicator.show({});
}
onLoadEnd() {
console.log('pdf loaded!');
this.loadingIndicator.hide();
}
}
模板标记非常简单。
如果我们没有在长篇大论中剧透,您可能甚至不知道 PDFView
从技术上讲不是 Angular 组件 🤯
<ActionBar title="PDF Viewer"></ActionBar>
<GridLayout>
<PDFView
[src]="pdfUrl"
(loadStart)="onLoadStart()"
(loadEnd)="onLoadEnd()"
></PDFView>
</GridLayout>
当开发人员从 StackBlitz 到手机体验实时更新时,总是会感到兴奋,并意识到原生 API 的强大功能。我们的目标是让它感觉像您已经编写的每一个 JavaScript 应用程序一样。
希望您喜欢 PDF 查看器演示。