我有一个用于项目的 Ionic/Angular PWA,该项目还有一个适用于移动设备的本机 (Ionic Cordova) 应用程序。在应用程序上,我们需要接受与 PWA 具有相同 URL 的深层链接(以便您可以使用相同的链接从 PC 和移动设备进行访问)。
为了使 iOS 深层链接(他们称之为“通用链接”)正常工作,TLD 需要在根文件夹或
apple-app-site-association
路径内提供一些名为 .well-known
(无扩展名)的文件。 Android 也是如此,但有一个名为 assetlinks.json
的文件(尽管在这种情况下,它并不是真正需要工作,它只是为了遵守一些 Android 准则)。
问题是,当我尝试访问 iOS 文件时,我的 PWA 打开并将其捕获为路由并重定向到主页。对于 Android 文件,它工作得很好,因为它有一个扩展名,并且 Angular(或托管服务器)可以识别浏览器想要访问某些静态文件。
我在互联网上查找过这个问题,但找不到任何解决方案......
该网站托管在 IIS 服务器上,我尝试了对
web.config
文件进行一些设置修复,例如重写具有 .json
扩展名的相同文件的路径以及另一个我现在似乎不记得的文件。
我也尝试了所有我能想到的
src/app-routing.module.ts
文件中的路线组合。如果存在通配符路由,它将被匹配并重定向到指定的位置。如果没有,我会得到一个 Error: Cannot match any routes. URL Segment: '.well-known/apple-app-site-association'
。
如果您阅读了通用链接,您将了解到这个概念是:您的应用程序的托管站点提供 apple-app-site-association 将 URL 连接到您的 iOS 应用程序。 静态网站的设置非常简单。 但对于 Spa 应用程序来说,这可能相当困难。 例如,使用 Angular,可以轻松地通过 angular.json 配置静态内容。 但是,当该静态内容没有文件扩展名时,它就不再起作用 🤔(转发到 Angular 路由,这不是您想要的)。
因此,就 Spa 而言,您真正需要做的是在应用程序前面设置一些额外的基础设施。 这个额外的 universal-link 基础设施只是一个使用与应用程序 url 不同的 url 访问的最小网站。
通用链接 URL 是您向用户群推广的内容。 当访问通用链接站点时,如果用户使用移动设备,则 apple-app-site-associations 文件将返回到设备并用于打开关联的本机应用程序。 如果用户不在移动设备上,则会打开为通用链接配置的 Web 应用程序。
我在这里提供了一个简单的说明。 这个解释并不是详尽无遗的,而是为你(已经陷入困境的人)指明正确的方向。 图中的用例是通用链接的 Firebase 实现 - 动态链接
一些通用链接的好资源:
用例:Apple 有时会通过请求应用程序在以下 URL 托管文件来验证域:
https://example.com/.well-known/apple-developer-merchantid-domain-association
如果 Apple 允许从
/assets
目录提供文件,则可以将其放置在 /assets
中并通过 https://example.com/assets/apple-developer-merchantid-domain-association
访问
或者,如果 Apple 添加了像
.txt
这样的文件扩展名,那么您可以使用以下方式更新 angular.json:
"assets": [
{ "glob": "**/*", "input": "my-app/src/.well-known/", "output": "/.well-known/" },
],
然后使用以下命令访问文件:
https://example.com/.well-known/apple-developer-merchantid-domain-association.txt
当您无法从
/assets
提供文件并且无法添加文件扩展名时,解决方法是创建自定义 URL 匹配器,然后使用 HTTP 客户端将文件作为 blob 加载。
UrlMatch
import { UrlMatcher, UrlSegment } from '@angular/router'
export const AppleVerificationUrlMatcher: UrlMatcher = (segments: UrlSegment[]) => {
if (
segments.length === 2 &&
segments[0]?.path === '.well-known' &&
segments[1]?.path === 'apple-developer-merchantid-domain-association'
) {
return { consumed: segments }
}
return null
}
import { Component } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { ActivatedRoute, Router } from '@angular/router'
import { map, tap } from 'rxjs/operators'
@Component({
selector: 'app-apple-verification-handler',
template: '',
standalone: true,
})
export class AppleVerificationComponent {
constructor(private http: HttpClient, private route: ActivatedRoute, private router: Router) {
const url = this.router.url.slice(1) // Remove leading slash
this.http
.get(`${url}`, { responseType: 'text' })
.pipe(
tap((content) => {
const contentType = 'text/plain'
const blob = new Blob([content], { type: contentType })
const url = URL.createObjectURL(blob)
window.location.href = url
}),
map(() => null)
)
.subscribe()
}
}
import { Route } from '@angular/router'
import { AppleVerificationComponent } from './apple-verification.component'
import { AppleVerificationUrlMatcher } from './apple-verification.url-matcher'
export const appRoutes: Route[] = [
{
matcher: AppleVerificationUrlMatcher,
component: AppleVerificationComponent,
},
...