Flutter Navigator2.0的原理和Web端實踐

01

背景與動機

Navigator 2.0推出之前,Flutter主要通過Navigator 1.0和其提供的 API(如push(),?pop(),?pushNamed()等)來管理頁面路由。然而,Navigator 1.0存在一些局限性,如難以實現復雜的頁面操作(如移除棧內中間頁面、交換頁面等)、不支持嵌套路由以及無法滿足全平臺(尤其是Web平臺)的新需求。因此,Flutter官方團隊決定對路由系統進行改造,推出了Navigator 2.0。?

02

主要特性

  • 聲明式API?Navigator 2.0提供的聲明式API使得路由管理更加直觀和易于理解。開發者只需聲明頁面的配置信息,而無需編寫復雜的導航邏輯代碼。這種方式不僅減少了代碼量,還提高了代碼的可讀性和可維護性。

  • 嵌套路由?Navigator 2.0滿足了嵌套路由的需求場景,允許開發者在應用中創建嵌套的路由結構。這使得應用的結構更加清晰,同時也提高了頁面導航的靈活性。

  • 全平臺支持?Navigator 2.0提供的API能夠滿足不同平臺(如iOSAndroidWeb等)的導航需求,使得開發者能夠更加方便地構建跨平臺的應用。

  • 強大的頁面操作能力?Navigator 2.0提供了更加豐富的頁面操作能力,如移除棧內中間頁面、交換頁面等。這些操作在Navigator 1.0中很難實現或需要編寫復雜的代碼,而在Navigator 2.0中則變得簡單直接。

03

核心組件

  • Router?在Navigator 2.0中,Router組件是路由管理的核心。它負責根據當前的路由信息(RouteInformation)和路由信息解析器(RouteInformationParser)來構建和更新UIRouter組件接收三個主要參數:

    1.routeInformationProvider:提供當前的路由信息;

    2.routeInformationParser:將路由信息解析為路由配置;

    3.routerDelegate:根據路由配置構建和更新UI

  • RouteInformationProvider?RouteInformationProvider是一個提供當前路由信息的組件。它通常與平臺相關的路由信息源(如瀏覽器的URLAndroidIntent等)集成,以獲取當前的路由信息。

  • RouteInformationParser?RouteInformationParser負責將RouteInformation解析為RouteConfiguration。這個過程允許開發者根據路由信息的格式(如URL)來定義如何將其映射到應用內的路由配置。

  • RouterDelegate?RouterDelegate是與UI構建緊密相關的組件。它必須實現RouterDelegate接口,并提供兩個主要方法:?

    1.build(BuildContext context):根據當前的路由配置構建UI

    2.setNewRoutePath(List?configuration):設置新的路由路徑,并更新UI

    3.Future?popRoute() :實現后退邏輯。

04

簡單實例

首先通過MaterialApp.router()來創建MaterialApp:

class MyApp extends StatelessWidget?{??@override??Widget build(BuildContext context)?{??final routerDelegate?=?MyRouterDelegate();??final routeInformationParser?=?MyRouteInformationParser();??return?MaterialApp.router(??title:?'Flutter Navigator 2.0 Demo',??theme:?ThemeData(??primarySwatch:?Colors.blue,??),??routerDelegate:?routerDelegate,??routeInformationParser:?routeInformationParser,??);??}??
}

需要定義一個RouterDelegate對象和一個RouteInformationParser對象。其中根據路由配置構建和更新UIRouteInformationParser負責將RouteInformation解析為RouteConfiguration。?RouterDelegate可以傳個泛型,定義其currentConfiguration對象的類型。

class MyRouterDelegate extends RouterDelegate<String>??with PopNavigatorRouterDelegateMixin<String>,?ChangeNotifier?{??final GlobalKey<NavigatorState>?navigatorKey?=?GlobalKey<NavigatorState>();??private List<String>?_pages?=?['/home'];??@override??Widget build(BuildContext context)?{??return?Navigator(??key:?navigatorKey,??pages:?_pages.map((route)?=>?MaterialPage(??key:?Key(route),??child:?generatePage(route),??)).toList(),??onPopPage:?(route,?result)?{??if?(!route.didPop(result))?{??return?false;??}??_pages.removeLast();??notifyListeners();??return?true;??},??);??}??@override??Future<void>?setNewRoutePath(String path)?async?{??if?(!_pages.contains(path))?{??_pages.add(path);??notifyListeners();??}??}??Widget generatePage(String route)?{??switch?(route)?{??case?'/home':??return?HomePage();??case?'/details':??//?這里可以傳遞參數,例如?DetailsPage(arguments:?someData)??return?DetailsPage();??default:??return?NotFoundPage();??}??}??@override??String get currentConfiguration?=>?_pages.last;??
}

其中build()一般返回的是一個Navigator對象,popRoute()實現后退邏輯,setNewRoutePath()實現新頁面的邏輯。定義了一個_pages數組對象,記錄每個路由的path,可以理解為是一個路由棧,這個路由棧對我們來說非常友好,在有復雜的業務邏輯時,我們可以自行定義相應的棧管理邏輯。currentConfiguration返回的是棧頂的page信息。創建一個類繼承RouteInformationParser,主要的作用是包裝解析路由信息,這里有一個最簡單的方式,如下:

class MyRouteInformationParser extends RouteInformationParser<String>?{??@override??Future<String>?parseRouteInformation(RouteInformation routeInformation)?{??final uri?=?Uri.parse(routeInformation.location);??return?SynchronousFuture(uri.path);??}??@override??RouteInformation restoreRouteInformation(String configuration)?{??return?RouteInformation(location:?configuration);??}??
}

好的,接下來我們看一下調用:

class HomePage extends StatelessWidget?{??@override??Widget build(BuildContext context)?{??return?Scaffold(??appBar:?AppBar(title:?Text('Home')),??body:?Center(??child:?ElevatedButton(??onPressed:?()?{??Router.of(context).routerDelegate.setNewRoutePath("/details");},??child:?Text('Go to Details'),??),??),??);??}??
}??class DetailsPage extends StatelessWidget?{??@override??Widget build(BuildContext context)?{??return?Scaffold(??appBar:?AppBar(title:?Text('Details')),??body:?Center(??child:?Text('This is Details Page'),??),??);??}??
}?class NotFoundPage extends StatelessWidget?{??@override??Widget build(BuildContext context)?{??return?Scaffold(??appBar:?AppBar(title:?Text('Not Found')),??body:?Center(??child:?Text('Page not found'),??),??);??}??
}

非常簡單,直接調用Router.of(context).routerDelegate.setNewRoutePath()即可。

到此為止,一個使用Navigator2.0的最簡單的路由實例就完成了。和Navigator1.0相比,看上去繁雜了不少。但是可以根據業務需求自定義路由棧進行管理,大大的提升了靈活性。接來看我們看一下Navigator2.0是如何對路由進行實現的。

05

源碼簡析

我們在使用Navigator2.0時,是通過MaterialApp.router()創建的MaterialApp對象,之前章節提到過,傳了RouteInformationParserRouterDelegate這兩個對象。當傳遞了RouterDelegate對象時,_MaterialAppState中的_usesRouter會被設置為true

bool get _usesRouter?=>?widget.routerDelegate?!=?null?||?widget.routerConfig?!=?null;

build()時,通過WidgetsApp.router()方法創建了一個WidgetsApp對象:

if?(_usesRouter)?{return?WidgetsApp.router(key:?GlobalObjectKey(this),routeInformationProvider:?widget.routeInformationProvider,routeInformationParser:?widget.routeInformationParser,routerDelegate:?widget.routerDelegate,routerConfig:?widget.routerConfig,backButtonDispatcher:?widget.backButtonDispatcher,builder:?_materialBuilder,title:?widget.title,onGenerateTitle:?widget.onGenerateTitle,textStyle:?_errorTextStyle,color:?materialColor,locale:?widget.locale,localizationsDelegates:?_localizationsDelegates,localeResolutionCallback:?widget.localeResolutionCallback,localeListResolutionCallback:?widget.localeListResolutionCallback,supportedLocales:?widget.supportedLocales,showPerformanceOverlay:?widget.showPerformanceOverlay,checkerboardRasterCacheImages:?widget.checkerboardRasterCacheImages,checkerboardOffscreenLayers:?widget.checkerboardOffscreenLayers,showSemanticsDebugger:?widget.showSemanticsDebugger,debugShowCheckedModeBanner:?widget.debugShowCheckedModeBanner,inspectorSelectButtonBuilder:?_inspectorSelectButtonBuilder,shortcuts:?widget.shortcuts,actions:?widget.actions,restorationScopeId:?widget.restorationScopeId,);}

_WidgetsAppState中根據routerDelegate設置了成員變量_usesRouterWithDelegates的值:

bool get _usesRouterWithDelegates?=>?widget.routerDelegate?!=?null;

build()時會創建一個Router對象,其中Router繼承了StatefulWidget

@overrideWidget build(BuildContext context)?{Widget??routing;if?(_usesRouterWithDelegates)?{routing?=?Router<Object>(restorationScopeId:?'router',routeInformationProvider:?_effectiveRouteInformationProvider,routeInformationParser:?widget.routeInformationParser,routerDelegate:?widget.routerDelegate!,backButtonDispatcher:?_effectiveBackButtonDispatcher,);}?
......}

在上一章節的實例中我們可得知,頁面的切換都是依靠RouterDelegate對象進行的。每當切換到新的頁面時,都會調用setNewRoutePath()方法,因此我們來看一下setNewRoutePath()是什么時候被調用的,有兩處。第一處:

void?_handleRouteInformationProviderNotification()?{_routeParsePending?=?true;_processRouteInformation(widget.routeInformationProvider!.value,?()?=>?widget.routerDelegate.setNewRoutePath);}
_RouteSetter<T>?_processParsedRouteInformation(Object??transaction,?ValueGetter<_RouteSetter<T>>?delegateRouteSetter)?{return?(T data)?async?{if?(_currentRouterTransaction?!=?transaction)?{return;}await delegateRouteSetter()(data);if?(_currentRouterTransaction?==?transaction)?{_rebuild();}};}

我們看看_handleRouteInformationProviderNotification的調用時機:

@overridevoid?initState()?{super.initState();widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);widget.routerDelegate.addListener(_handleRouterDelegateNotification);}

我們可以看到在initState()時,也就是在Router被初始化的時候由widget.routeInformationProvider來監聽一些狀態實現新頁面的切換。我們來看一下routeInformationProviderRouteInformationProvider在我們自己沒有創建的情況下,系統會默認為我們創建一個PlatformRouteInformationProvider對象。它實際上是個ChangeNotifier。系統會監聽每一幀的信號發送,調用其父類routerReportsNewRouteInformation()方法,我們看看它的實現:

@overridevoid routerReportsNewRouteInformation(RouteInformation routeInformation,?{RouteInformationReportingType?type?=?RouteInformationReportingType.none})?{final bool replace?=type?==?RouteInformationReportingType.neglect?||(type?==?RouteInformationReportingType.none?&&_equals(_valueInEngine.uri,?routeInformation.uri));SystemNavigator.selectMultiEntryHistory();SystemNavigator.routeInformationUpdated(uri:?routeInformation.uri,state:?routeInformation.state,replace:?replace,);_value?=?routeInformation;_valueInEngine?=?routeInformation;}

其中SystemNavigator.selectMultiEntryHistory()的實現如下:

///?Selects the multiple-entry?history?mode.//////?On web,?this switches the browser?history?model to one that tracks all///?updates to?[routeInformationUpdated]?to form a?history?stack.?This is the///?default.//////?Currently,?this is ignored on other platforms.//////?See also://////??*?[selectSingleEntryHistory],?which?forces the?history?to only have one///????entry.static Future<void>?selectMultiEntryHistory()?{return?SystemChannels.navigation.invokeMethod<void>('selectMultiEntryHistory');}

這個方法是由各個平臺自行實現的。從注釋中我們可得知如果是在Web平臺下,它會切換成history模式,并從history stack中追蹤所有的變化。在history發生變化時,會發送信號給Flutter層等待處理。SystemNavigator.routeInformationUpdated()方法是用來更新路由的,我們先不做分析。接著我們回到PlatformRouteInformationProvider,看看它什么時候會執行notifyListeners()方法:

@overrideFuture<bool>?didPushRouteInformation(RouteInformation routeInformation)?async?{assert(hasListeners);_platformReportsNewRouteInformation(routeInformation);return?true;}
void _platformReportsNewRouteInformation(RouteInformation routeInformation)?{if?(_value?==?routeInformation)?{return;}_value?=?routeInformation;_valueInEngine?=?routeInformation;notifyListeners();}

在監聽到有push路由的情況下時,會調用notifyListeners(),從而實現頁面的切換。我們再來看第二處調用setNewRoutePath()的地方:

@overridevoid?didChangeDependencies()?{_routeParsePending?=?true;super.didChangeDependencies();//?The super.didChangeDependencies may have parsed the route information.//?This can happen?if?the didChangeDependencies is triggered by state//?restoration or first build.if?(widget.routeInformationProvider?!=?null?&&?_routeParsePending)?{_processRouteInformation(widget.routeInformationProvider!.value,?()?=>?widget.routerDelegate.setNewRoutePath);}_routeParsePending?=?false;_maybeNeedToReportRouteInformation();}
void _processRouteInformation(RouteInformation information,?ValueGetter<_RouteSetter<T>>?delegateRouteSetter)?{assert(_routeParsePending);_routeParsePending?=?false;_currentRouterTransaction?=?Object();widget.routeInformationParser!.parseRouteInformationWithDependencies(information,?context).then<void>(_processParsedRouteInformation(_currentRouterTransaction,?delegateRouteSetter));}

parseRouteInformationWithDependencies()方法中調用的parseRouteInformation()其實就是我們自定義RouteInformationParser來進行的實現。

Future<T>?parseRouteInformationWithDependencies(RouteInformation routeInformation,?BuildContext context)?{return?parseRouteInformation(routeInformation);}

看到當其與父的依賴關系被改變的時候會調用setNewRoutePath()。大概率就是App初始化的時候被調用一次。

06

根據狐友業務的Web端實踐?

我們的Flutter團隊會承擔一些運營活動的H5需求。在實現時我們對路由有如下需求:

1.可以根據業務自由的管理路由棧;

2.分享鏈接只能分享出去默認入口鏈接,不希望中間的路由鏈接被分享出去;

3.不管有多少個路由頁面,history始終不變,在響應瀏覽器返回鍵時不響應路由棧的pop操作。

在之前使用Navigator1.0時體驗并不太好,一個是不夠靈活,另外還需對分享出去的鏈接做處理。因此我們利用Navigator2.0設計了一套新的路由:

MyRouterDelegate delegate?=?MyRouterDelegate();@overrideWidget build(BuildContext context)?{return?MaterialApp.router(debugShowCheckedModeBanner:?false,routeInformationParser:?MyRouteParser(),routerDelegate:?delegate,);}

Parser實現非常簡單:

class MyRouteParser extends RouteInformationParser<RouteSettings>?{@override///parseRouteInformation()?方法的作用就是接受系統傳遞給我們的路由信息?routeInformationFuture<RouteSettings>?parseRouteInformation(RouteInformation routeInformation)?{//?Uri uri?=?Uri.parse(routeInformation.location??"/");return?SynchronousFuture(RouteSettings(name:?routeInformation.location));}@override///恢復路由信息RouteInformation restoreRouteInformation(RouteSettings configuration)?{return?RouteInformation(location:?configuration.name);}
}

Delegate的實現如下:

import?'package:ai_chatchallenge/router/exit_util.dart';
import?'package:ai_chatchallenge/router/navigator_util.dart';
import?'package:ai_chatchallenge/router/my_router_arg.dart';
import?'package:flutter/material.dart';import?'route_page_config.dart';class MyRouterDelegate extends RouterDelegate<RouteSettings>with PopNavigatorRouterDelegateMixin<RouteSettings>,?ChangeNotifier?{///頁面棧List<Page>?_stack?=?[];//當前的界面信息RouteSettings _setting?=?RouteSettings(name:?RouterName.rootPage,arguments:?BaseArgument()..name?=?RouterName.rootPage);//重寫navigatorKey@overrideGlobalKey<NavigatorState>?navigatorKey;MyRouterDelegate()?:?navigatorKey?=?GlobalKey<NavigatorState>()?{//初始化兩個方法?一個是push頁面?另一個是替換頁面NavigatorUtil().registerRouteJump(RouteJumpFunction(onJumpTo:?(RouteSettings setting)?{//?_setting?=?setting;//?changePage();addPage(name:?setting.name,?arguments:?setting.arguments);},?onReplaceAndJumpTo:?(RouteSettings setting)?{if?(_stack.isNotEmpty)?{_stack.removeLast();}_setting?=?setting;changePage();},?onClearStack:?()?{_stack.clear();_setting?=?RouteSettings(name:?RouterName.rootPage,arguments:?BaseArgument()..name?=?RouterName.rootPage);changePage();},?onBack:?()?{if?(_stack.isNotEmpty)?{_stack.removeLast();if?(_stack.isNotEmpty)?{_setting?=?_stack.last;}?else?{_setting?=?RouteSettings(name:?RouterName.rootPage,arguments:?BaseArgument()..name?=?RouterName.rootPage);}changePage();}}));}@overrideRouteSettings??get currentConfiguration?{return?_stack.last;}@overrideFuture<bool>?popRoute()?{if?(_stack.length?>?1)?{_stack.removeLast();_setting?=?_stack.last;changePage();//非最后一個頁面return?Future.value(true);}//最后一個頁面確認退出操作return?_confirmExit();}Future<bool>?_confirmExit()?async?{bool result?=?ExitUtil.doubleCheckExit(navigatorKey.currentContext!);//?bool result?=?await ExitUtil.backToDesktop();return?!result;}void addPage({required name,?arguments})?{_setting?=?RouteSettings(name:?name,?arguments:?arguments);changePage();}@overrideWidget build(BuildContext context)?{return?WillPopScope(//解決物理返回建無效的問題onWillPop:?()?async?=>?!await navigatorKey.currentState!.maybePop(),child:?Navigator(key:?navigatorKey,pages:?_stack,onPopPage:?_onPopPage,),);}///?按下返回的回調bool _onPopPage(Route<dynamic>?route,?dynamic result)?{debugPrint("這里的試試");if?(!route.didPop(result))?{return?false;}return?true;}changePage()?{int index?=?getCurrentIndex(_stack,?_setting!);List<Page>?tempPages?=?_stack;if?(index?!=?-1)?{//?要求棧中只允許有一個同樣的頁面的實例?否則開發模式熱更新會報錯//?要打開的頁面在棧中已存在,則將該頁面和它上面的所有頁面進行出棧tempPages?=?tempPages.sublist(0,?index);//?或者刪除之前存在棧里的頁面,重新創建//?tempPages.removeAt(index);}Page page;if?(_setting?.arguments is BaseArgument)?{if?((_setting?.arguments as BaseArgument).name?==?RouterName.rootPage)?{_stack.clear();}}?else?{if?(_setting?.name?==?RouterName.rootPage)?{_stack.clear();}}page?=?buildPage(name:?_setting?.name,?arguments:?_setting?.arguments);tempPages?=?[...tempPages,?page];NavigatorUtil().notify(tempPages,?_stack);_stack?=?tempPages;notifyListeners();}@overrideFuture<void>?setInitialRoutePath(RouteSettings configuration)?{return?super.setInitialRoutePath(_setting);}@overrideFuture<void>?setNewRoutePath(RouteSettings configuration)?async?{if?(configuration.arguments is BaseArgument)?{if?((configuration.arguments as BaseArgument).name?==RouterName.rootPage)?{_stack.clear();}}?else?{if?(configuration.name?==?RouterName.rootPage)?{_stack.clear();}}addPage(name:?configuration.name,?arguments:?configuration.arguments);}
}

其中_stack是我們的路由棧,_settingRouteSettings,每執行一個新的路由跳轉,都會創建一個RouteSettings對象并賦值給_setting,最終在插入_stack里。buildPage()的實現如下:

//建造頁面
buildPage({required name,?arguments})?{return?MaterialPage(child:?getPageChild(name:?name,?arguments:?arguments),arguments:?arguments,name:?name,key:?ValueKey(arguments is BaseArgument???(arguments as BaseArgument).name?:?name));
}

其中MaterialPage繼承了PagegetPageChild()實現如下:

Widget getPageChild({required name,?arguments})?{Widget page;Map??arg;if?(arguments is Map)?{arg?=?arguments;}if?(arguments is BaseArgument)?{switch?((arguments as BaseArgument).name)?{case?RouterName.rootPage:page?=?TestHomePage();break;case?RouterName.testChild1Page:page?=?TestChildPage1(argument:?arguments.arguments as TestChild1PageArgument,);break;case?RouterName.testChild2Page:page?=?TestChildPage2();break;default:page?=?TestHomePage();}}?else?{page?=?TestHomePage();}return?page;
}class RouterName?{static const rootPage?=?"/";static const testChild1Page?=?"/testChild1Page";static const testChild2Page?=?"/testChild2Page";
}

我們可以看到,在真正返回Widget時,我們并沒有使用傳入的name參數,而是BaseArgumentname參數,這是為什么呢?這是在于我們為了實現無論頁面怎么跳轉,從頭到尾瀏覽器只保留一個history,因此我們在頁面跳轉時RouteSettingsname并不發生變化,通過其arguments里面的參數變化返回不同的Widget。這樣在路由跳轉時,其實MaterialPage由于name一直會被直接復用,從而不會創建新的MaterialPage也就不會產生history。?NavigatorUtil是由業務調用的,創建跳轉方法的抽象類,提供了onJumpTo()onReplaceAndJumpTo()onClearStack()onBack()四個方法供業務調用,我們可以看一下onJumpTo()的實現:

@overridevoid onJumpTo({required name,Object??stackArguments,Map<String,?dynamic>??historyArgMap,BuildContext??context})?{var arg?=?BaseArgument();arg.name?=?name;arg.arguments?=?stackArguments;RouteSettings settings?=RouteSettings(name:?RouterName.rootPage,?arguments:?arg);return?_function!.onJumpTo!(settings);}

可以看到在創建RouteSettings對象時,nameRouterName.rootPagearg時由業務傳的真正的跳轉頁面相關的參數。我們看一下業務的調用:

@overrideWidget build(BuildContext context)?{return?Scaffold(body:?Container(child:?Column(children:?[Text("TestHomePage"),Text("history length is?:?"?+?window.history.length.toString()),Text("href:?"?+?WebUtil.get().getWindow().location.href),TextButton(onPressed:?()?{var arg?=?TestChild1PageArgument()..isSuccess?=?"false";NavigatorUtil().onJumpTo(name:?RouterName.testChild1Page,stackArguments:?arg,historyArgMap:?arg.toJson(),context:?context);},child:?Text("Go to TestChildPage1"))],),),);}
@overrideWidget build(BuildContext context)?{return?Scaffold(body:?Container(child:?Column(children:?[Text("TestChildPage1"),Text("history length is?:?"?+?window.history.length.toString()),Text("href:?"?+?WebUtil.get().getWindow().location.href),TextButton(onPressed:?()?{NavigatorUtil().onJumpTo(name:?RouterName.testChild2Page,?context:?context);},child:?Text("Go to TestChildPage2")),TextButton(onPressed:?()?{NavigatorUtil().onBack();},child:?Text("Back to TestHomePage")),],),),);}
@overrideWidget build(BuildContext context)?{return?Scaffold(body:?Container(child:?Column(children:?[Text("TestChildPage2"),Text("history length is?:?"?+?window.history.length.toString()),Text("href:?"?+?WebUtil.get().getWindow().location.href),TextButton(onPressed:?()?{NavigatorUtil().onBack();},child:?Text("Back to TestChild1page")),TextButton(onPressed:?()?{NavigatorUtil().onClearStack();},child:?Text("Back to Root")),],),),);}

我們看一下截圖展示:ed0664521819cfd77543692bd24831a2.jpegba508520efaeaed5d0c481309c870d2c.jpeg79a825d2a509384bdb03fd0f8dfd0693.jpeg

在這個過程中href不會發生變化,history也不會發生變化,完全符合我們的預期。

07

總結

FlutterNavigator 2.0引入了聲明式的API,使頁面路由管理更加靈活和強大。相較于Navigator 1.0Navigator 2.0支持更復雜的路由操作,如嵌套路由和動態路由配置。它使用不可變的Page對象列表來表示路由歷史,與Flutter的不可變Widgets設計理念一致。Navigator 2.0還支持命名路由,通過簡單的路由名稱即可實現頁面跳轉,大大簡化了路由管理的復雜度。此外,它還提供了更豐富的路由回調和狀態管理功能,使開發者能夠更輕松地構建復雜的Flutter應用。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/63038.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/63038.shtml
英文地址,請注明出處:http://en.pswp.cn/web/63038.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

代碼隨想錄算法訓練營第三天 | 鏈表理論基礎 | 707.設計鏈表

要求太多&#xff0c;代碼量太大&#xff0c;實在難以完成 在以前聽說&#xff0c;好的程序員&#xff0c;可以在短時生成大量的代碼&#xff0c;本題只方法才只有6個&#xff0c;根本不算多 每天手敲代碼量太少&#xff0c;才是問題 #include <iostream>class MyLink…

數據冒險、控制冒險、結構冒險

計算機組成原理 數據冒險、控制冒險、結構冒險 對所有用戶&#xff08;所有程序員&#xff09;可見&#xff1a;PSW、PC、通用寄存器 PSW&#xff08;條件轉移需要用到&#xff0c;程序員使用CMP指令的時候也需要用到所以是對用戶可見&#xff09;PC&#xff08;跳轉指令需要…

基于32單片機的RS485綜合土壤傳感器檢測土壤PH、氮磷鉀的使用(超詳細)

1-3為RS485綜合土壤傳感器的基本內容 4-5為基于STM32F103C8T6單片機使用RS485傳感器檢測土壤PH、氮磷鉀并顯示在OLED顯示屏的相關配置內容 注意&#xff1a;本篇文件講解使用的是PH、氮磷鉀四合一RS485綜合土壤傳感器&#xff0c;但里面的講解內容適配市面上的所有多合一的RS…

SpringBoot【十一】mybatis-plus實現多數據源配置,開箱即用!

一、前言&#x1f525; 環境說明&#xff1a;Windows10 Idea2021.3.2 Jdk1.8 SpringBoot 2.3.1.RELEASE 正常情況下我們在開發系統的時候都是使用一個數據源&#xff0c;但是由于有些項目同步數據的時候不想造成數據庫io消耗壓力過大&#xff0c;便會一個項目對應多個數據源…

Node.js教程入門第一課:環境安裝

對于一個程序員來說&#xff0c;每學習一個新東西的時候&#xff0c;第一步基本上都是先進行環境的搭建&#xff01; 從本章節開始讓我們開始探索Node.js的世界吧! 什么是Node.js? 那么什么是Node.js呢&#xff1f;簡單的說Node.js 就是運行在服務端的 JavaScript JavaScript…

vim優化

1.編輯如下內容&#xff1a; cat > /root/.vimrc <<EOF set tabstop2 " 設置 Tab 為 2 個空格 set shiftwidth2 " 設置自動縮進為 2 個空格 set expandtab " 將 Tab 轉換為空格 " 基本設置 set number syntax on" 快捷鍵設置…

字符串性能對比

效率(1) : String.indexOf與String.contains效率測試_string contains效率-CSDN博客 結論是前者效率高&#xff0c;源碼里面conatins是使用indexof 在jdk8中contains直接調用的indexOf(其他版本沒有驗證),所以要說效率來說肯定是indexOf高,但contains也就多了一層方法棧,so 什…

移動網絡的原理

無線網絡是如何解決移動通信問題的 場景&#xff1a;用戶在一輛轎車內以150km/h的時速沿高速公路急速行駛時穿過多個無線接入網&#xff0c;用戶希望在整個旅程中保持一個與遠程應用的不間斷的TCP連接。 解決方案&#xff1a;移動節點的間接路由選擇方法可解決TCP鏈接不間斷的…

python學opencv|讀取圖像(十三)BGR圖像和HSV圖像互相轉換深入

【1】引言 前序學習過程中&#xff0c;我們偶然發現&#xff1a;如果原始圖像是png格式&#xff0c;將其從BGR轉向HSV&#xff0c;再從HSV轉回BGR后&#xff0c;圖像的效果要好于JPG格式。 文章鏈接為&#xff1a; python學opencv|讀取圖像&#xff08;十二&#xff09;BGR圖…

解決node.js的req.body為空的問題

從昨晚一直在試&#xff0c;明明之前用的封裝的axios發送請求給其他的后端&#xff08;springboot&#xff09;是可以的&#xff0c;但昨天用了新項目的后端&#xff08;node.js&#xff09;就不行。 之前用了代理&#xff0c;所以瀏覽器發送的post請求不會被攔截&#xff0c;…

【嵌入式】嵌入式面試題 36 問

1. volatile 是否可以修飾 const 是的&#xff0c;volatile 可以修飾 const。const 表示變量的值不能被修改&#xff0c;而 volatile 表示變量的值可能在程序之外被修改&#xff08;例如&#xff0c;由硬件修改&#xff09;。 將 volatile 用于 const 變量意味著該變量的值雖然…

java基礎概念49-數據結構2

一、樹 1-1、樹的基本概念 1、樹的節點 2、二叉樹 3、樹的高度 1-2、二叉查找樹 普通二叉樹沒有規律&#xff0c;不方便查找&#xff0c;沒什么作用。 1、基本概念 2、添加節點 此時&#xff0c;該方式添加形成的二叉查找樹&#xff0c;根節點就是第一個節點。 3、查找節點 4…

GhatGPT缺陷不足和商業應用

1. 引言 ChatGPT的興起&#xff1a; 2022年末推出&#xff0c;迅速在自然語言處理和人工智能領域引起廣泛關注。數億用戶體驗其強大智能&#xff0c;感嘆機器智能的飛速發展。 存在的缺陷&#xff1a; 事實性錯誤&#xff1a;生成的文本中包含錯誤信息。無法實時更新&#xff1…

【Linux】Macvlan介紹及LInux下例子實現

Macvlan Macvlan 是一種網絡虛擬化技術&#xff0c;允許在同一物理網絡接口上創建多個虛擬網絡接口&#xff0c;每個虛擬接口都有自己獨立的 MAC 地址。這對于需要在同一物理主機上運行多個網絡隔離的應用程序或容器時非常有用。 Macvlan 的特點和用途 獨立的 MAC 地址 每個 …

Jackson @JsonInclude 注解

1. 概述 Jackson 是一個著名的Java庫&#xff0c;以轉換Java對象為JSON格式以及從JSON反序列化回Java對象而聞名。有時候&#xff0c;我們可能希望僅在某些字段滿足特定條件時才將其包含在JSON輸出中&#xff0c;而Jackson的JsonInclude注解正是為此目的量身定制的。 JsonInc…

12.12 枚舉 共用體 數據結構 創建順序表

1.思維導圖 2. 創建順序表 1>頭文件 test.h #ifndef __TEST_H__ #define __TEST_H__#include<stdlib.h> #include<stdio.h> #include<string.h>#define MAX 30 //typedef int datatype;typedef struct sequence {int data[MAX];int len;}seqlist,*se…

next.js 存在緩存中毒漏洞(CVE-2024-46982)

免責聲明: 本文旨在提供有關特定漏洞的深入信息,幫助用戶充分了解潛在的安全風險。發布此信息的目的在于提升網絡安全意識和推動技術進步,未經授權訪問系統、網絡或應用程序,可能會導致法律責任或嚴重后果。因此,作者不對讀者基于本文內容所采取的任何行為承擔責任。讀者在…

如何對小型固定翼無人機進行最優的路徑跟隨控制?

控制架構 文章繼續采用的是 ULTRA-Extra無人機&#xff0c;相關參數如下&#xff1a; 這里用于guidance law的無人機運動學模型為&#xff1a; { x ˙ p V a cos ? γ cos ? χ V w cos ? γ w cos ? χ w y ˙ p V a cos ? γ sin ? χ V w cos ? γ w sin ? χ…

【Flink-scala】DataStream編程模型之延遲數據處理

DataStream API編程模型 1.【Flink-Scala】DataStream編程模型之數據源、數據轉換、數據輸出 2.【Flink-scala】DataStream編程模型之 窗口的劃分-時間概念-窗口計算程序 3.【Flink-scala】DataStream編程模型之水位線 4.【Flink-scala】DataStream編程模型之窗口計算-觸發器-…

react useRef、useContext、useReducer使用中遇到的問題及解決辦法

在 React 中&#xff0c;useRef、useContext 和 useReducer 是三個非常有用的 Hook&#xff0c;它們可以幫助我們更好地管理組件的狀態和行為。然而&#xff0c;在使用這些 Hook 時&#xff0c;可能會遇到一些問題和困惑。本文將詳細解釋這三個 Hook 的工作原理&#xff0c;并提…