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能够满足不同平台(如iOS、Android、Web等)的导航需求,使得开发者能够更加方便地构建跨平台的应用。强大的页面操作能力
Navigator 2.0提供了更加丰富的页面操作能力,如移除栈内中间页面、交换页面等。这些操作在Navigator 1.0中很难实现或需要编写复杂的代码,而在Navigator 2.0中则变得简单直接。
03
核心组件
Router 在
Navigator 2.0中,Router组件是路由管理的核心。它负责根据当前的路由信息(RouteInformation)和路由信息解析器(RouteInformationParser)来构建和更新UI。Router组件接收三个主要参数:1.routeInformationProvider:提供当前的路由信息;
2.routeInformationParser:将路由信息解析为路由配置;
3.routerDelegate:根据路由配置构建和更新
UI。RouteInformationProvider
RouteInformationProvider是一个提供当前路由信息的组件。它通常与平台相关的路由信息源(如浏览器的URL、Android的Intent等)集成,以获取当前的路由信息。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对象。其中根据路由配置构建和更新UI,RouteInformationParser负责将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对象,之前章节提到过,传了RouteInformationParser和RouterDelegate这两个对象。当传递了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来监听一些状态实现新页面的切换。我们来看一下routeInformationProvider。RouteInformationProvider在我们自己没有创建的情况下,系统会默认为我们创建一个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是我们的路由栈,_setting是RouteSettings,每执行一个新的路由跳转,都会创建一个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继承了Page。getPageChild()实现如下:
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参数,而是BaseArgument的name参数,这是为什么呢?这是在于我们为了实现无论页面怎么跳转,从头到尾浏览器只保留一个history,因此我们在页面跳转时RouteSettings的name并不发生变化,通过其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对象时,name为RouterName.rootPage,arg时由业务传的真正的跳转页面相关的参数。我们看一下业务的调用:
@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")),],),),);} 我们看一下截图展示:


在这个过程中href不会发生变化,history也不会发生变化,完全符合我们的预期。
07
总结
Flutter的Navigator 2.0引入了声明式的API,使页面路由管理更加灵活和强大。相较于Navigator 1.0,Navigator 2.0支持更复杂的路由操作,如嵌套路由和动态路由配置。它使用不可变的Page对象列表来表示路由历史,与Flutter的不可变Widgets设计理念一致。Navigator 2.0还支持命名路由,通过简单的路由名称即可实现页面跳转,大大简化了路由管理的复杂度。此外,它还提供了更丰富的路由回调和状态管理功能,使开发者能够更轻松地构建复杂的Flutter应用。
相关文章:
Flutter Navigator2.0的原理和Web端实践
01 背景与动机 在Navigator 2.0推出之前,Flutter主要通过Navigator 1.0和其提供的 API(如push(), pop(), pushNamed()等)来管理页面路由。然而,Navigator 1.0存在一些局限性,如难以实现复杂的页面操作(如移…...
初次使用uniapp编译到微信小程序编辑器页面空白,真机预览有内容
uniapp微信小程序页面结构 首页页面代码 微信小程序模拟器 模拟器页面为空白时查了下,有几个说是“Hbuilder编译的时候应该编译出来一个app.js文件 但是却编译出了App.js”,但是我的小程序结构没问题,并且真机预览没有问题 真机调试 根据defi…...
【HF设计模式】03-装饰者模式
声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。 摘要 《Head First设计模式》第3章笔记:结合示例应用和代码,介绍装饰者模式,包括遇到的问题、遵循的 OO 原则、达到的效果。 …...
【人工智能-中级】模型部署与优化:从本地实验到云端与边缘部署
模型部署与优化:从本地实验到云端与边缘部署 在机器学习和深度学习模型训练完成后,如何高效、稳定地将模型部署到生产环境中,是实际应用中的关键环节。模型部署不仅涉及技术实现,还需要考虑性能优化、资源管理和安全性等多方面因素。本文将全面探讨模型部署与优化的相关内…...
Jenkins 编写Pipeline 简介及使用初识详解
一、Jenkins Pipeline简介 Jenkins Pipeline是Jenkins的一个重要功能,Jenkins 2.0 以上才会有,一系列 Jenkins 插件将整个持续集成用解释性代码 Jenkinsfile 来描述,它允许开发者以代码的方式定义整个持续集成和交付(CI/CD)流程,包括构建、测试、部署和监控等步骤。Jenk…...
uboot移植网络驱动过程,无法ping通mx6ull和ubuntu问题解决方案
开发板:mx6ull-ALPHA_V2.4 ubuntu版本:20.04 1.现在虚拟机设置中添加网路适配器用于开启桥接模式 2.在编辑中打开“虚拟网络编辑器” 我的电脑本身只有VMnet1和VMnet8,需要底下“添加网络”,增加这个VMnet0 ,并且进行…...
精准预测美国失业率和贫困率,谷歌人口动态基础模型PDFM已开源,可增强现有地理空间模型
疾病、经济危机、失业、灾害……人类世界长期以来被各种各样的问题「侵扰」,了解人口动态对于解决这类复杂的社会问题至关重要。 政府相关人员可以通过人口动态数据来模拟疾病的传播,预测房价和失业率,甚至预测经济危机。然而,在过…...
C#速成(文件读、写操作)
导包 using System.IO;1、写入文件(重要) StreamWriter sw new StreamWriter("C:\Users\29674\Desktop\volumn.txt");//创建一个TXT的文件 sw.WriteLine(textBox2.Text);//写入文件的内容 sw.Close();//关闭2、读取文件(不重要&…...
SQL server学习03-创建和管理数据表
目录 一,SQL server的数据类型 1,基本数据类型 2,自定义数据类型 二,使用T-SQL创建表 1,数据完整性的分类 2,约束的类型 3,创建表时创建约束 4,任务 5,由任务编写…...
【UE5 “RuntimeLoadFbx”插件】运行时加载FBX模型
前言 为了解决在Runtime时能够直接根据FBX模型路径直接加载FBX的问题,推荐一款名为“RuntimeLoadFBX”的插件。 用法 插件用法如下,只需要指定fbx的地址就可以在场景中生成Actor模型 通过指定输入参数“Cal Collision”来设置FBX模型的碰撞 还可以通过…...
【潜意识Java】深入理解 Java 面向对象编程(OOP)
目录 什么是面向对象编程(OOP)? 1. 封装(Encapsulation) Java 中的封装 2. 继承(Inheritance) Java 中的继承 3. 多态(Polymorphism) Java 中的多态 4. 抽象&…...
windows同时使用多个网卡
windows同时链接了有线网络,多个无线网卡,默认会使用有线网络,如果想要局域网内使用某个特定的网络,可以设置静态ip 1. 首先删除原来的静态网络(不冲突可以不删除),我这里usb无线网卡切换过usb插口,这里需要删除原来的. 使用 route print 查看接口列表及静态路由信息 route p…...
Spark执行计划解析后是如何触发执行的?
在前一篇Spark SQL 执行计划解析源码分析中,笔者分析了Spark SQL 执行计划的解析,很多文章甚至Spark相关的书籍在讲完执行计划解析之后就开始进入讲解Stage切分和调度Task执行,每个概念之间没有强烈的关联,因此这中间总感觉少了点…...
B4X编程语言:B4X控件方法汇总
1、AddNode、AddView方法 AddNode(Node As javafx.scence.Node,Left As Double,Top As Double,Width As Double,Height As Double) B4J控件 AddView(View As javafx.scence.Node,Left As Double,Top As Double,Width As Double,Height As Double) B4J的B4XView …...
基于XML配置Bean和基于XML自动装配
目录 基于XML配置Bean id分配规则 通过id获取bean 通过类型获取bean 通过C命名空间配置bean 使用C命名空间 通过P命名空间配置bean 通过util:list进行配置bean 指定id,直接ref引用过来 通过外部属性文件配置Bean Bean信息重用(继承)…...
全排列 dfs
给定一个由不同的小写字母组成的字符串,输出这个字符串的所有全排列。 我们假设对于小写字母有 a<b<…<y<z ,而且给定的字符串中的字母已经按照从小到大的顺序排列。 输入格式 输入只有一行,是一个由不同的小写字母组成的字符串…...
linux内存相关命令的尝试
文章目录 前言freeMem 部分的解释Swap 部分的解释 vmstatProcs (进程)Memory (内存)Swap (交换)IO (磁盘 I/O)System (系统)CPU (处理器) pidstat标题行解释数据列解释 sar字段含义解释示例分析 总结 前言 菜就多练,昨天看了一篇有关剖析 RocksDB 内存超限问题的文…...
Vue2 基础
Vue 2 是 Vue.js 的第二个主要版本,于 2016 年发布。它是一个渐进式的 JavaScript 框架,以其简单、灵活、易用性高而广受欢迎。Vue 2 主要专注于构建用户界面(UI),并且非常适合用于构建单页应用(SPA&#x…...
递归问题(c++)
递归设计思路 数列递归 : 如果一个数列的项与项之间存在关联性,那么可以使用递归实现 ; 原理 : 如果一个函数可以求A(n),那么该函数就可以求A(n-1),就形成了递归调用 ; 注意: 一般起始项是不需要求解的,是已知条件 这就是一个典型…...
系统思考—战略决策
别用管理上的勤奋,来掩盖经营上的懒惰。 日本一家物业公司,因经营不善,面临生死存亡的危机。老板为了扭转局面,采取了很多管理手段——提高员工积极性,推行业绩与绩效挂钩,实施各种考核制度。然而…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...
python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果。…...
CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...
Netty从入门到进阶(二)
二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架,用于…...
