Flutter Web-404如何处理找不到页面404错误,android和IOS程序员

如何处理“找不到页面404”错误,手动直接输入URL并避免URL中的哈希字符?

介绍

当我必须在生产中部署我的第一个Flutter Web应用程序时,我必须处理所有与Web Server逻辑相关的常规功能,尤其是:

  • 著名的“ 找不到页面404 ”
  • 从浏览器直接输入URL

我在互联网上进行了大量搜索,但从未找到任何好的解决方案。

本文介绍了我实施的解决方案…

Flutter Web-404如何处理找不到页面404错误,android和IOS程序员

背景资料

本文撰写于2020年2月,基于Flutter 1.14.6版(运行Channel Beta)。

看一下Flutter路线图2020,Flutter Web应该在今年正式发布,其结果是这篇文章可能不会很快相关,因为它所解决的问题可能在未来几个月内得到解决。

我也尝试与Service Workers玩耍,但找不到任何解决方案。

在向您提供我已实施的解决方案之前,我想与您分享一些重要的信息…

提醒-Flutter Web应用程序不能在完全可配置的Web服务器后面运行

“ Flutter Web应用程序不能在完全可配置的Web服务器后面运行 ”

这句话非常重要,常常被人遗忘……

确实,当您运行Flutter Web应用程序时,您“ 简单地 ”启动了一个基本的Web服务器,该服务器侦听某个“ IP_address:port ”并提供位于“ web ”文件夹中的文件。几乎没有配置/自定义可以添加到该Web服务器的实例。

不同的网页文件夹

如果以调试模式运行Flutter Web App,则Web文件夹为“ / web”

如果以发布模式运行,则Web文件夹为“ / build / web”

当您运行Flutter Web应用程序时,一旦激活了基本的Web服务器,就会从相应的“ web ”文件夹中自动调用“ index.html ”页面。

index.html ”页面会自动加载一些资产以及与整个应用程序相对应的“ main.dart.js ”文件。实际上,这对应于Dart代码和其他一些库的Javascript转换。


换句话说...

当您访问“ index.html ”时,您正在

加载整个应用程序

这意味着Flutter Web应用程序是一个单页应用程序,并且在大多数情况下,除了在加载并启动该单页应用程序后检索任何其他资产(字体,图像等)之外,您之间不再会有任何交互Flutter Web应用程序(在浏览器上运行)和Web服务器。


URL中的怪异“#”字符

当您运行Flutter Web应用程序并从一个页面(=路由)导航到另一页面时,我想您已经注意到浏览器URL导航栏级别的更改了……

例如,假设您的应用程序由2个页面组成:“主页”和“登录页面”。主页将在应用程序启动时自动显示,并具有一个导航到LoginPage的按钮。

浏览器的URL栏将包含:

  • http://192.168.1.40:8080/#/ 当您启动应用程序时=>这对应于主页
  • 显示LoginPage时为http://192.168.1.40:8080/#/LoginPage。

主题标签指定URL片段,该片段通常在单页应用程序中用于导航,以替代URL路径。

URL片段最有趣的是

片段不会在HTTP请求消息中发送,因为片段仅由浏览器使用。

在我们的例子中,在Flutter Web中,浏览器使用它们来处理历史记录

(有关片段的更多信息,请点击此链接)


如何在网址中隐藏“#”字符?

我很多次在互联网上看到这个问题,答案很简单。

由于'#'字符通常对应于应用程序中的页面(= Route),因此您需要告诉浏览器更新URL,同时继续在浏览器历史记录中考虑该页面(以便浏览器的后退和前进按钮可以使用正确)。

为此,您需要使页面成为“ StatefulWidget ”,以便利用页面的初始化时间(= initState方法)。

实现此目的的代码如下:

<code>import 'dart:html' as html;
import 'package:flutter/material.dart';

class MyPage extends StatefulWidget {
@override
_MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<mypage> {
@override
void initState(){
super.initState();

// this is the trick
html.window.history.pushState(null, "MyPage", "/mypage");
}
}/<mypage>/<code>

从那时起,当用户将被重定向到“ MyPage”时,而不是在URL中显示“ http://192.168.1.40:8080/#/MyPage”,浏览器将显示“ http://192.168.1.40 :8080 / mypage“,它更加人性化。

但是,如果您将该页面加为书签并尝试重新调用它,或者直接在浏览器中键入该URL,则将遇到以下错误页面“ 无法找到此http://192.168.1.40页面 ”,该页面对应到著名的HTTP错误404。

那么如何解决呢?

每次您通过手动输入的URL访问Flutter Web应用程序时,都会运行main()

在解释该解决方案之前,还必须注意,当您通过Web浏览器输入“ 有效 ” URL时,将对Flutter Web服务器进行访问以重新加载应用程序,并在加载后运行

main()方法。 。

换句话说,如果您在Web浏览器URL栏级别手动输入“ http://192.168.1.40:8080”或“ http://192.168.1.40:8080/#/page”,则请求将发送到重新加载应用程序并最终运行“ main() ”方法的Web服务器。

当通过应用程序本身从一个页面(=路由)切换到应用程序的另一页面时,情况并非如此,因为代码仅在Web浏览器级别运行!


我的解决方案

第一次尝试...不是解决方案...

下一篇文章过去已经讨论过该问题,并给出了解决方案的一些提示,但是该文章中公开的“ 迄今为止最好的解决方案 ”今天不再起作用(或者我无法使其起作用)。

因此,直接想到的第一个解决方案是基于同一篇文章中所述的“ 第二个解决方案 ” ,其中:

  • 我们在initState()方法中调用pushState时会提到“ .html”扩展名,如下所示:html.window.history.pushState(null,“ MyPage”,“ / mypage .html
    ”);
  • 我们在每个屏幕上创建一个* .html页面…

但是,这当然很乏味且容易出错,因此我继续进行调查。

解决方案

然后我想:“ 如果我可以拦截URL请求并以正确的格式重定向它,该怎么办?”。

换句话说,类似……(但这不起作用)不幸的是,正如我之前所说,不可能在HTTP请求中中继片段(带有#字符)的概念。

因此,我需要找到其他东西。

如果我可以使应用程序“ 认为 ” URL不一样怎么办?

然后,我找到了Shelf Dart软件包,这是Dart的Web服务器中间件,它允许定义请求处理程序

解决方案非常简单:

  • 我们运行机架式 Web服务器的实例,侦听所有传入的请求
  • 我们在本地主机上运行Flutter Web
  • 我们检查请求是否指向页面
  • 对于所有这些请求,我们将它们重定向到标称index.html保持Request URL不变,以便可以由main()方法拦截,然后显示请求的页面…

当然,与资产相关的请求(图片,JavaScript等)不应属于重定向的一部分……

架子再次提供了一个称为shelf_proxy的代理处理程序,该代理处理程序对外部服务器的请求。正是我需要的!

但是,此代理处理程序不提供任何功能来插入重新路由逻辑……太糟糕了。

因此,由于其源代码已获得BSD许可,因此我克隆了该代理处理程序的源代码,以插入自己的重新路由逻辑,该逻辑简单地包含在内(但当然可以扩展到需求):

  • 如果URL不包含对扩展名的任何引用(例如“ .js”,“。json”,“。png”…),并且在路径中仅包含1个块(例如“ http://192.168.1.40:8080 / mypage”,而不是 “ http://192.168.1.40:8080 / assets / package / ...”),然后我将请求重定向到Flutter Web服务器实例的页面“ index.html ”,
  • 否则,我只需将请求重定向到Flutter Web服务器实例,而无需提及“ index.html”页面。

这意味着要运行2台Web服务器!”,你能告诉我吗

是的,它确实。

代理 Web服务器(在这里,利用现有的),听着真正的 IP地址和端口该颤振Web应用程序,听本地主机


实作

1.创建Flutter Web应用程序

照常创建Flutter Web应用程序。

2.修改您的“ main.dart”文件(在/ lib中)

这个想法是直接捕获浏览器URL中提供的路径

<code>import 'dart:html' as html;
import 'package:flutter/material.dart';

void main(){
//
// Retrieve the path that was sent
//
final String pathName = html.window.location.pathname;

//
// Tell the Application to take it into consideration
//
runApp(
Application(pathName: html),
);
}

class Application extends StatelessWidget {
const Application({
this.pathName,
});

final String pathName;

@override
Widget build(BuildContext context){
return MaterialApp(
onUnknownRoute: (_) => UnknownPage.route(),
onGenerateRoute: Routes.onGenerateRoute,
initialRoute: pathName,
);
}
}

class Routes {

static Route<dynamic> onGenerateRoute(RouteSettings settings){
switch (settings.name.toLowerCase()){
case "/": return HomePage.route();
case "/page1": return Page1.route();
case "/page2": return Page2.route();
default:
return UnknownPage.route();
}
}
}

class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();

//
// Static Routing
//
static Route<dynamic> route()
=> MaterialPageRoute(
builder: (BuildContext context) => HomePage(),
);

}

class _HomePageState extends State<homepage>{
@override
void initState(){
super.initState();

//
// Push this page in the Browser history
//
html.window.history.pushState(null, "Home", "/");
}

@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(title: Text('Home Page')),
body: Column(
children: <widget>[
RaisedButton(
child: Text('page1'),
onPressed: () => Navigator.of(context).pushNamed('/page1'),
),
RaisedButton(
child: Text('page2'),
onPressed: () => Navigator.of(context).pushNamed('/page2'),
),

//
// Intentionally redirect to an Unknown page
//
RaisedButton(
child: Text('page3'),
onPressed: () => Navigator.of(context).pushNamed('/page3'),
),
],
),
);
}
}

// Similar code as HomePage, for Page1, Page2 and UnknownPage/<widget>/<homepage>/<dynamic>/<dynamic>/<code>

说明

  • main()方法级别,我们捕获提交的路径(第8行)并将其提供给Application
  • 应用认为路径作为“ 初始一个 ” =>“ initialRoute: 路径”(行#30)
  • 所述Routes.onGenerateRoute(...)则方法被调用并返回的路线,其对应于所提供的路径
  • 如果路由不存在,它将重定向到
    UnknownPage()

3.创建代理服务器

1 – 在项目的根目录中创建一个bin文件夹2 – 在/ bin文件夹中创建一个名为“ proxy_server.dart ”的文件 3 –将以下代码放入该“ proxy_server.dart ”文件中:

<code>import 'dart:async';
import 'package:self/self_io.dart' as shelf_io;
import './proxy_handler.dart';

void main() async {
var server;

try {
server = await shelf_io.serve(
proxyHandler("http://localhost:8081"), // redirection to
"localhost", // listening to hostname
8080, // listening to port
);
} catch(e){
print('Proxy error: $e');
}
}/<code>

说明

主()方法简单地初始化的一个实例货架 web服务器,其

  • 在端口8080上侦听“ localhost”
  • 将所有传入的HTTP请求发送到proxyHandler()方法,该方法被指示重定向到“ localhost:8081”

4 –将以下文件“ proxy_handler.dart ”从该要点复制到您的/ bin文件夹中。

<code>import 'dart:async';

import 'package:http/http.dart' as http;
import 'package:shelf/shelf.dart';
import 'package:path/path.dart' as p;
import 'package:pedantic/pedantic.dart';

// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file [https://github.com/dart-lang/shelf_proxy].

/// A handler that proxies requests to [url].
///
/// To generate the proxy request, this concatenates [url] and [Request.url].
/// This means that if the handler mounted under `/documentation` and [url] is
/// `http://example.com/docs`, a request to `/documentation/tutorials`
/// will be proxied to `http://example.com/docs/tutorials`.
///
/// [url] must be a [String] or [Uri].
///
/// [client] is used internally to make HTTP requests. It defaults to a
/// `dart:io`-based client.
///
/// [proxyName] is used in headers to identify this proxy. It should be a valid
/// HTTP token or a hostname. It defaults to `shelf_proxy`.
Handler proxyHandler(url, {http.Client client, String proxyName}) {
Uri uri;
if (url is String) {
uri = Uri.parse(url);
} else if (url is Uri) {
uri = url;

} else {
throw ArgumentError.value(url, 'url', 'url must be a String or Uri.');
}
client ??= http.Client();
proxyName ??= 'shelf_proxy';

return (serverRequest) async {
var requestUrl = uri.resolve(serverRequest.url.toString());

//
// Insertion of the business logic
//
if (_needsRedirection(requestUrl.path)){
requestUrl = Uri.parse(url + "/index.html");
}

var clientRequest = http.StreamedRequest(serverRequest.method, requestUrl);
clientRequest.followRedirects = false;
clientRequest.headers.addAll(serverRequest.headers);
clientRequest.headers['Host'] = uri.authority;

// Add a Via header. See
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
_addHeader(clientRequest.headers, 'via',
'${serverRequest.protocolVersion} $proxyName');

unawaited(store(serverRequest.read(), clientRequest.sink));
var clientResponse = await client.send(clientRequest);
// Add a Via header. See
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
_addHeader(clientResponse.headers, 'via', '1.1 $proxyName');

// Remove the transfer-encoding since the body has already been decoded by
// [client].
clientResponse.headers.remove('transfer-encoding');

// If the original response was gzipped, it will be decoded by [client]
// and we'll have no way of knowing its actual content-length.
if (clientResponse.headers['content-encoding'] == 'gzip') {
clientResponse.headers.remove('content-encoding');
clientResponse.headers.remove('content-length');

// Add a Warning header. See
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
_addHeader(
clientResponse.headers, 'warning', '214 $proxyName "GZIP decoded"');
}

// Make sure the Location header is pointing to the proxy server rather
// than the destination server, if possible.

if (clientResponse.isRedirect &&
clientResponse.headers.containsKey('location')) {
var location =
requestUrl.resolve(clientResponse.headers['location']).toString();
if (p.url.isWithin(uri.toString(), location)) {
clientResponse.headers['location'] =
'/' + p.url.relative(location, from: uri.toString());
} else {
clientResponse.headers['location'] = location;
}
}

return Response(clientResponse.statusCode,
body: clientResponse.stream, headers: clientResponse.headers);
};

}

/// Use [proxyHandler] instead.
@deprecated
Handler createProxyHandler(Uri rootUri) => proxyHandler(rootUri);

/// Add a header with [name] and [value] to [headers], handling existing headers
/// gracefully.
void _addHeader(Map<string> headers, String name, String value) {
if (headers.containsKey(name)) {
headers[name] += ', $value';
} else {
headers[name] = value;
}
}

/// Pipes all data and errors from [stream] into [sink].
///
/// When [stream] is done, the returned [Future] is completed and [sink] is
/// closed if [closeSink] is true.
///
/// When an error occurs on [stream], that error is passed to [sink]. If
/// [cancelOnError] is true, [Future] will be completed successfully and no
/// more data or errors will be piped from [stream] to [sink]. If
/// [cancelOnError] and [closeSink] are both true, [sink] will then be
/// closed.
Future store(Stream stream, EventSink sink,
{bool cancelOnError = true, bool closeSink = true}) {
var completer = Completer();
stream.listen(sink.add, onError: (e, StackTrace stackTrace) {
sink.addError(e, stackTrace);
if (cancelOnError) {
completer.complete();
if (closeSink) sink.close();

}
}, onDone: () {
if (closeSink) sink.close();
completer.complete();
}, cancelOnError: cancelOnError);
return completer.future;
}

///
/// Checks if the path requires to a redirection
///
bool _needsRedirection(String path){
if (!path.startsWith("/")){
return false;
}

final List<string> pathParts = path.substring(1).split('/');

///
/// We only consider a path which is only made up of 1 part
///
if (pathParts.isNotEmpty && pathParts.length == 1){
final bool hasExtension = pathParts[0].split('.').length > 1;
if (!hasExtension){
return true;
}
}
return false;
}/<string>/<string>/<code>

总结

当我需要在生产中发布Flutter Web应用程序时,我必须找到一个能够处理以下问题的解决方案:

  • URL异常(例如“ 未找到页面-错误404 ”);
  • 友好的网址(不包含#个字符)

在地方,我把该解决方案(本文的主题),工程

但这只能被看作是一个解决办法

我想应该还有其他一些解决方案,更多的是“ 官方的 ”,但迄今为止我还没有发现其他解决方案。

我真的希望Flutter团队能够尽快解决此问题,以便在已有解决方案的情况下提供“ 干净的 ”解决方案或记录该解决方案。


分享到:


相關文章: