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.mzph.cn/web/63038.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据冒险、控制冒险、结构冒险

计算机组成原理 数据冒险、控制冒险、结构冒险 对所有用户&#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" 快捷键设置…

移动网络的原理

无线网络是如何解决移动通信问题的 场景&#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图…

java基础概念49-数据结构2

一、树 1-1、树的基本概念 1、树的节点 2、二叉树 3、树的高度 1-2、二叉查找树 普通二叉树没有规律&#xff0c;不方便查找&#xff0c;没什么作用。 1、基本概念 2、添加节点 此时&#xff0c;该方式添加形成的二叉查找树&#xff0c;根节点就是第一个节点。 3、查找节点 4…

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…

如何对小型固定翼无人机进行最优的路径跟随控制?

控制架构 文章继续采用的是 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编程模型之窗口计算-触发器-…

2024告别培训班 数通、安全、云计算、云服务、存储、软考等1000G资源分享

大类有&#xff1a;软考初级 软考中级 软考高级 华为认证 华三认证&#xff1a; 软考初级&#xff1a; 信息处理技术员 程序员 网络管理员 软考中级&#xff1a; 信息安全工程师 信息系统监理师 信息系统管理工程师 嵌入式系统设计时 数据库系统工程师 电子商务设…

《操作系统 - 清华大学》8 -1:进程的组成

文章目录 1. 进程的组成2. 进程与程序的联系3. 进程与程序的区别4. 进程与程序关系 1. 进程的组成 进程具体包含哪些东西&#xff1a; 首先要执行相应的代码&#xff0c;所以执行代码需要放到内存中代码执行需要处理数据&#xff0c;数据需要放到内存中需要知道现在要执行哪条…

【Java】String类API

创建字符串 字符串字面量"Hello"高效&#xff0c;常量池复用常见、简单的字符串创建 new 关键字new String("Hello")每次创建新对象&#xff0c;性能开销较高显式创建新对象 字符数组new String(char[])转换字符数组字符数组转字符串 StringBuilder/St…

数据结构初阶---二叉树---堆

一、树 1.树的概念 树是一种非线性的数据结构&#xff0c;由n(n≥0)个有限结点组成的一个有层次关系的集合。形状类似一棵倒挂的树&#xff0c;根朝上&#xff0c;分支向下。 根结点没有前驱结点&#xff0c;可以有n(n≥0)个后继结点。 其余结点被分为M个互不相交的集合&am…

C语言 字符串输入输出函数、scanf(“%[^\n]“,)可输入空格 、fgets删除换行符

字符串输入函数&#xff1a; scanf&#xff08;"%s"&#xff0c;数组名&#xff09; gets&#xff08;数组名&#xff09; fgets&#xff08;&#xff09; --- 文件流输入函数 函数原型&#xff1a; int scanf( const char *format, ...…

深度学习camp-第J4周:ResNet与DenseNet结合探索

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 本周任务&#xff1a; 探索ResNet和DenseNet的结合可能性本周任务较难&#xff0c;我们在chatGPT的帮助下完成 一、网络的构建 设计一种结合 ResNet 和 Den…

「iOS」通过CoreLocation Framework深入了解MVC架构

「iOS」通过CoreLocation Framework重新了解多界面传值以及MVC架构 文章目录 「iOS」通过CoreLocation Framework重新了解多界面传值以及MVC架构前言CoreLocation了解根据需求建模设计属性方法设计协议传值Block传值KVONotification通知方式 总结参考文章 前言 在这个学期的前…

ArrayList源码分析、扩容机制面试题,数组和List的相互转换,ArrayList与LinkedList的区别

目录 1.java集合框架体系 2. 前置知识-数组 2.1 数组 2.1.1 定义&#xff1a; 2.1.2 数组如何获取其他元素的地址值&#xff1f;&#xff08;寻址公式&#xff09; 2.1.3 为什么数组索引从0开始呢&#xff1f;从1开始不行吗&#xff1f; 3. ArrayList 3.1 ArrayList和和…

【C++】- 掌握STL List类:带你探索双向链表的魅力

文章目录 前言&#xff1a;一.list的介绍及使用1. list的介绍2. list的使用2.1 list的构造2.2 list iterator的使用2.3 list capacity2.4 list element access2.5 list modifiers2.6 list的迭代器失效 二.list的模拟实现1. list的节点2. list的成员变量3.list迭代器相关问题3.1…