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團隊能夠儘快解決此問題,以便在已有解決方案的情況下提供“ 乾淨的 ”解決方案或記錄該解決方案。


分享到:


相關文章: