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存在一些局限性,如难以实现复杂的页面操作(如移…...
latex设置引用顺序
在 LaTeX 中,引用的顺序通常是由所选择的 参考文献样式(bibliographystyle) 决定的。如果你希望根据引用的顺序排列参考文献,可以选择合适的参考文献样式,并按照以下步骤进行设置。 常见的几种引用顺序设置方式有&…...
有效的括号(字节面试题 最优解)
题目来源 20. 有效的括号 - 力扣(LeetCode) 题目描述 给定一个只包括 (,),{,},[,] 的字符串 s ,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号…...
短视频矩阵源码开发部署全流程解析
在当今的数字化时代,短视频已成为人们娱乐、学习和社交的重要方式。短视频矩阵系统的开发与部署,对于希望在这一领域脱颖而出的企业和个人而言,至关重要。本文将详细阐述短视频矩阵源码的开发与部署流程,并附上部分源代码示例&…...
iOS 环境搭建教程
本文档将详细介绍如何在 macOS 上搭建 iOS 开发环境,以便进行 React Native 开发。(为了保证环境一致 全部在网络通畅的情况下运行) 1. 安装 Homebrew Homebrew 是 macOS 的包管理工具,我们将通过它来安装开发所需的工具。 安装…...
element-ui实现table表格的嵌套(table表格嵌套)功能实现
最近在做电商类型的官网,希望实现的布局如下:有表头和表身,所以我首先想到的就是table表格组件。 表格组件中常见的就是:标题和内容一一对应: 像效果图中的效果,只用基础的表格布局是不行的,因…...
如何使mysql数据库ID从0开始编号——以BiCorpus为例
BiCorpus是北京语言大学韩林涛老师研制一款在线语料库网站,可以通过上传tmx文件,实现在线检索功能,程序在github上开源免费,深受广大网友的喜欢。 在使用过程中,我发现我上传的语言资产经历修改后,mysql的…...
亮相AICon,火山引擎边缘云揭秘边缘AI Agent探索与实践
12月13-14日,AICon 全球人工智能开发与应用大会在北京成功举办。火山引擎边缘智能技术负责人谢皓受邀出席大会,以《AI Agent 在边缘云的探索与实践》为主题,与全球 AI 领域的资深专家,共同深入探讨大模型落地、具身智能、多模态大…...
让文案生成更具灵活性/chatGPT新功能canvas画布编辑
OpenAI最近在2024年12月发布了canvas画布编辑功能,这是一项用途广泛的创新工具,专为需要高效创作文案的用户设计。 无论是职场人士、学生还是创作者,这项功能都能帮助快速生成、优化和编辑文案,提升效率的同时提高内容质量…...
朗致面试---IOS/安卓/Java/架构师
朗致面试---IOS/安卓/Java/架构师 一、面试概况二、总结三、算法题目参考答案 一、面试概况 一共三轮面试: 第一轮是逻辑行测,25道题目,类似于公务员考试题目,要求90分钟内完成。第二轮是技术面试,主要是做一些数据结…...
windows C#-实现具有自动实现属性的轻型类
下面演示如何创建一个不可变的轻型类,该类仅用于封装一组自动实现的属性。 当你必须使用引用类型语义时,请使用此种构造而不是结构。 可通过以下方法来实现不可变的属性: 仅声明 get 访问器,使属性除了能在该类型的构造函数中可…...
深度学习之Autoencoders GANs for Anomaly Detection 视频异常检测
在视频异常检测(Video Anomaly Detection)任务中,Autoencoders(自编码器) 和 GANs(生成对抗网络) 是常用的深度学习模型,它们在检测视频中的异常事件(如入侵、破坏、非法行为等)方面发挥着重要作用。通过分析视频帧的时空特征,这些模型能够识别出与正常行为模式不同…...
实现按键按下(低电平)检测到下降沿
按照流程进行编程 步骤1: 初始化函数 包括时基工作参数配置 输入通道配置 更新中断使能 使能捕获、捕获中断及计数器 HAL_TIM_IC_Init(&ic_handle) //时基参数配置 HAL_TIM_IC_ConfigChannel(&ic_handle,&ic_config,TIM_CHANNEL_2) //输…...
【21天学习AI底层概念】day5 机器学习的三大类型不能解决哪些问题?
机器学习的三大类型——监督学习、无监督学习和强化学习,虽然可以应用于许多问题,但并非所有问题都能通过这些方法有效解决。每种类型的机器学习都有其局限性,具体如下: 1. 监督学习 (Supervised Learning) 监督学习是通过训练数…...
八股—Java基础(一)
目录 一、Java概述 1、Java语言有哪些特点? 2、JVM、JDK、JRE有什么区别? 3、什么是跨平台性?原理是什么 4、Java和C有什么关系,它们有什么区别? 5、JVM、JRE和JDK的关系是什么? 6、什么是字节码? …...
PWM调节DCDC参数计算原理
1、动态电压频率调整DVFS SOC芯片的核电压、GPU电压、NPU电压、GPU电压等,都会根据性能和实际应用场景来进行电压和频率的调整。 即动态电压频率调整DVFS(Dynamic Voltage and Frequency scaling),优化性能和功耗。 比如某SOC在…...
设计一个基础JWT的多开发语言分布式电商系统
在设计一个分布式电商系统时,保证系统的可扩展性、性能以及跨语言的兼容性是至关重要的。随着微服务架构的流行,越来越多的电商系统需要在多个服务间共享信息,并且保证服务的安全性。在这样的场景下,JSON Web Token(JW…...
基础开发工具-编辑器vim
vim操作键盘图 下图是比较基础的vim操作键盘图 (IDE例子) vi/vim的区别简单点来说,它们都是多模式编辑器,不同的是vim是vi的升级版本,它不仅兼容vi的所有指令,⽽且还有⼀些新的特性在⾥⾯。例如语法加亮&a…...
C#速成(文件读、写操作)
导包 using System.IO;1、写入文件(重要) StreamWriter sw new StreamWriter("C:\Users\29674\Desktop\volumn.txt");//创建一个TXT的文件 sw.WriteLine(textBox2.Text);//写入文件的内容 sw.Close();//关闭2、读取文件(不重要&…...
11、多态
1、多态介绍 1.1、认识多态 “一个接口,多种状态”。 接口在运行期间,根据传入的参数来决定具体调用的函数,最终采取不同的执行策略。 比如:一个系统的后台,管理员登录后进入的界面和普通用户进入的界面是不一样的。 …...
bain.js(十二):RNN神经网络实战教程 - 音乐乐谱生成 -人人都是作曲家~
系列文章: (一):可以在浏览器运行的、默认GPU加速的神经网络库概要介绍(二):项目集成方式详解(三):手把手教你配置和训练神经网络(四)…...
嵌入式硬件-- 元器件焊接
1.锡膏的使用 锡膏要保存在冰箱里。 焊接排线端子;138度的低温锡(锡膏), 第一次使用,直接拿东西挑一点涂在引脚上,不知道多少合适,加热台加热到260左右,放在上面观察锡融化&#…...
java_多态
问题引导 使用传统的方法来解决(private 属性)传统的方法带来的问题是什么? 如何解决? 问题是: 代码的复用性不高,而且不利于代码维护 解决方案: 引出我们要讲解的多态 多态的基本介绍 方法或对象具有多种形态。是…...
如何设计一款智能手表的电子系统:从选择MCU到PCB设计
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:趣享先生的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏&…...
Vue Web开发(七)
1. echarts介绍 echarts官方文档 首先我们先完成每个页面的路由,之前已经有home页面和user页面,缺少mail页面和其它选项下的page1和page2页面。在view文件夹下新建mail文件夹,新建index.vue,填充user页面的内容即可。在view下新建…...
基于米尔全志T527开发板的OpenCV进行手势识别方案
本文将介绍基于米尔电子MYD-LT527开发板(米尔基于全志T527开发板)的OpenCV手势识别方案测试。 摘自优秀创作者-小火苗 米尔基于全志T527开发板 一、软件环境安装 1.安装OpenCV sudo apt-get install libopencv-dev python3-opencv 2.安装pip sudo apt…...
洛谷 P10483 小猫爬山 完整题解
一、题目查看 P10483 小猫爬山 - 洛谷 二、解题思路 我们将采取递归 剪枝的思想: sum数组存放每辆车当前载重。 每次新考虑一只小猫时,我们尝试把它放进每个可以放进的缆车中(需要回溯) for (int i 0; i < k; i) {if (sum[i]…...
Vmware的网络适配器的NAT模式和桥接模式有何区别?如何给Uubunt系统添加桥接网卡?
Vmware的网络适配器的NAT模式和桥接模式有何区别? 如何给Uubunt系统添加桥接网卡? 步骤如下:...
Vue导出报表功能【动态表头+动态列】
安装依赖包 npm install -S file-saver npm install -S xlsx npm install -D script-loader创建export-excel.vue组件 代码内容如下(以element-ui样式代码示例): <template><el-button type"primary" click"Expor…...
6.2 Postman接口收发包
欢迎大家订阅【软件测试】 专栏,开启你的软件测试学习之旅! 文章目录 前言1 接口收发包的类比1.1 获取对方地址(填写接口URL)1.2 选择快递公司(设置HTTP方法)1.3 填写快递单(设置请求头域&#…...
UE4_贴花_贴花基础知识一
贴花可以将材料和各种材料元素投影到表面上。您可以使用它们来添加独特的效果。贴花 是一种可以投射到网格体(包括静态网格体和骨骼网格体)上的材质。无论这些网格体的移动性(Mobility)是静态(Static)还是可…...
代码随想录day13 二叉树:二叉树的遍历(前中后序)(递归、迭代)、102.二叉树的层序遍历
二叉树简单讲解及题目讲解 代码随想录 144.二叉树前序遍历 145.二叉树后序遍历 94.二叉树中序遍历 102.二叉树的层序遍历 题目 给你二叉树的根节点root, 完成二叉树的前中后序遍历 二叉树遍历–递归法 思路 了解过二叉树的定义和遍历规则, 那么完成此题并没有什么难度, 采用…...
Kafka - 消息乱序问题的常见解决方案和实现
文章目录 概述一、MQ消息乱序问题分析1.1 相同topic内的消息乱序1.2 不同topic的消息乱序 二、解决方案方案一: 顺序消息Kafka1. Kafka 顺序消息的实现1.1 生产者:确保同一业务主键的消息发送到同一个分区1.2 消费者:顺序消费消息 2. Kafka 顺…...
俏生元,融汇传统智慧与现代科技,解析药食同源健康奥秘
在追求健康生活的今天,药食同源的理念正逐渐成为现代人滋养身心的新选择。俏生元,一直以来注重女性健康和多元需求,正以它独特的视角和匠心产品,助推着药食同源健康滋养的风尚。 俏生元葛根红参丰韵膏 药食同源,匠心独…...
【CUDA】CUBLAS
【CUDA】CUBLAS 在深入了解之前,提前运行预热(warmup)和基准测试(benchmark runs) 是获得准确执行时间的关键。如果不进行预热运行,cuBLAS 的首次运行会有较大的开销(大约 45 毫秒)…...
泛型编程--
auto自动推导数据类型 函数模板 定义和调用 函数模板具体化 函数模板通用版本之外的一个特殊版本 函数模板 具体化函数 ,它们的声明和定义都可以分开写。 声明 定义 函数模板写变量 模板参数缺省 类成员函数作为函数模板 类构造函数是函数模板 函数模板重载 函数模…...
Linux USB开发整理和随笔
目录 1 概述 2 硬件原理基础 2.1 USB发展 2.2 USB的拓扑 2.3 硬件接口 2.4 USB总线协议 2.4.1 通信过程 2.4.2 概念关系 2.4.3 管道PIPE 2.4.4 传输 2.4.5 事务 2.4.6 包结构与类型 2.4.6.1 令牌包 2.4.6.2 数据包 2.4.6.3 握手包 2.5 描述符 2.5.1 设备描述符…...
【实验】【H3CNE邓方鸣】交换机端口安全实验+2024.12.11
实验来源:邓方鸣交换机端口安全实验 软件下载: 华三虚拟实验室: 华三虚拟实验室下载 wireshark:wireshark SecureCRT v8.7 版本: CRT下载分享与破解 文章目录 dot1x 开启802.1X身份验证 开启802.1X身份验证,需要在系统视图和接口视…...
使用任务队列TaskQueue和线程池ThreadPool技术实现自定义定时任务框架详解
前言 在桌面软件开发中,定时任务是一个常见的需求,比如定时清理日志、发送提醒邮件或执行数据备份等操作。在C#中有一个非常著名的定时任务处理库Hangfire,不过在我们深入了解Hangfire 之前,我们可以手动开发一个定时任务案例&am…...
在IDE中使用Git
我们在开发的时候肯定是经常使用IDE进行开发的,所以在IDE中使用Git也是非常常用的,接下来以IDEA为例,其他的VS code ,Pycharm等IDE都是一样的。 在IDEA中配置Git 1.打开IDEA 2.点击setting 3.直接搜索git 如果已经安装了会自…...
蓝桥杯新年题解 | 第15届蓝桥杯迎新篇
蓝桥杯新年题解 | 第15届蓝桥杯迎新篇 2024年的蓝桥杯即将拉开序幕!对于许多编程爱好者来说,这不仅是一次展示自我能力的舞台,更是一次学习和成长的机会。作为一名大一新生的小蓝,对蓝桥杯充满了期待,但面对初次参赛的…...
Docker Swarm实战
文章目录 1、docker swarm介绍2、docker swarm概念与架构2.1 架构2.2 概念 3、docker swarm集群部署3.1 容器镜像仓库 Harbor准备3.2 主机准备3.2.1 主机名3.2.2 IP地址3.2.3 主机名与IP地址解析3.3.4 主机时间同步3.2.5 主机安全设置 3.3 docker安装3.3.1 docker安装3.3.2 配置…...
磁盘空间占用分析工具-wiztree【推荐】
磁盘空间占用分析工具-wiztree【推荐】 如果你遇到过磁盘空间占满、找大文件困难、线上服务器空间飙升等一系列磁盘的问题,并且需要分析文件夹占用空间,传统的方法就是一个一个去看,属实太费劲,效率太低。 而“WizTree”便可解决…...
Vuex在uniapp中的使用
文章目录 一、Vuex概述 1.1 官方解释 1.2 大白话 1.3 组件间共享数据的方式 1.4 再看Vuex是什么 1.5 使用Vuex统一管理好处 二、状态管理 2.1 单页面状态管理 2.2 多页面状态管理 2.3 全局单例模式 2.4 管理哪些状态 三、Vuex的基本使用 3.1 安装 3.2 导入 3.3 创建store对象…...
【含开题报告+文档+PPT+源码】基于微信小程序的点餐系统的设计与实现
开题报告 随着互联网技术的日益成熟和消费者生活水平与需求层次的显著提升,外卖点餐平台在中国市场上迅速兴起并深深植根于民众日常生活的各个角落。这类平台的核心在于构建了一个基于互联网的强大订餐服务系统,它无缝整合了餐饮商户资源与广大消费者的…...
Elasticsearch02-安装7.x
零、文章目录 Elasticsearch02-安装7.x 1、Windows安装Elasticsearch (1)JDK安装 Elasticsearch是基于java开发的,所以需要安装JDK。我们安装的Elasticsearch版本是7.15,对应JDK至少1.8版本以上。也可以不安装jdk,…...
【数据库】选择题+填空+简答
1.关于冗余数据的叙述中,不正确的是() A.冗余的存在容易破坏数据库的完整新 B.冗余的存在给数据库的维护增加困难 C.不应该在数据库中存储任何冗余数据 D.冗余数据是指由基本数据导出的数据 C 2.最终用户使用的数据视图称为(&…...
Spark执行计划解析后是如何触发执行的?
在前一篇Spark SQL 执行计划解析源码分析中,笔者分析了Spark SQL 执行计划的解析,很多文章甚至Spark相关的书籍在讲完执行计划解析之后就开始进入讲解Stage切分和调度Task执行,每个概念之间没有强烈的关联,因此这中间总感觉少了点…...
渗透测试-前端验签绕过之SHA256+RSA
本文是高级前端加解密与验签实战的第2篇文章,本系列文章实验靶场为Yakit里自带的Vulinbox靶场,本文讲述的是绕过SHA256RSA签名来爆破登录。 绕过 根据提示可以看出这次签名用了SHA2556和RSA两个技术进行加密。 查看源代码可以看到RSA公钥是通过请求服务…...
Maven完整技术汇总
额外知识点 IDE IDE是集成开发环境的缩写,它是一种软件应用程序,提供了编码、调试和部署软件的一站式解决方案。这些功能集成在一起,使开发人员能够在一个环境中完成整个软件开发过程,从编写代码到调试和测试,直到最终…...