探索Flutter路由管理新姿势(flutter路由返回自动刷新)
cac55 2024-09-20 12:51 21 浏览 0 评论
1、Flutter路由
1.1、路由分类
几乎所有 UI 框架开发出来的大型应用都是由数十甚至数百个页面组成。在 Android 项目中,每个页面都被承载在一个 Activity 中,因此一个 Activity 可以被认为是 Android 应用中的一个页面。在 Flutter 中,每个页面都对应一个 Route 对象,需要注意的是,弹窗也是 Route 的一种表现形式。Flutter 通过 Navigator 以栈结构来管理所有打开页面对应的 Route 对象。当一个页面打开时,对应的 Route 对象就被压入栈中;当一个页面关闭时,对应的 Route 对象就会从栈中弹出。
虽然 Flutter 中页面和弹窗都是用 Route 表示,但是两者之间的交互和表现形式存在明显的区别。为了保证清晰的代码结构和良好的可维护性,Flutter 按照表现形式通过类继承的方式对 Route 相关类进行了划分,具体如下:
1)OverlayRoute
在 Flutter 应用中,Overlay 扮演了至关重要的角色,它负责在视图上正确地显示页面和弹窗。要实现这一目的,我们需要将 Route 加载到 Overlay 中,而 OverlayRoute 就是用于实现这一目的的重要类之一。在 Flutter 应用中,通常会使用 MaterialApp 作为根节点,而 MaterialApp 中会内嵌一个 Navigator 对象,用于管理页面的显示与隐藏。同时,Navigator 内部还嵌套了 Overlay Widget,用于显示 OverlayRoute 对象。
2)TransitionRoute
当Overlay中的Route进行切换时,TransitionRoute是一个提供Route切换动画效果的抽象类,通过配合使用SlideTransition、FadeTransition等Widget,控制页面打开或关闭时的动画。
3)ModalRoute
保证所有的手势事件都被当前的ModalRoute处理,其底层的Route无法感知任何手势事件。
4)PageRoute
对应Flutter中的页面,适配各平台的页面交互特性,如iOS系统页面可侧滑退出。
5)DialogRoute
对应Flutter中的弹窗,支持点击弹窗外部区域退出等特性。
1.2、简单使用
在Flutter开发中,可以通过以下三种方式打开页面,使用示例如下:
1)组件路由
import 'package:flutter/material.dart';
void main() {
runApp(Nav2App());
}
class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('open Details'),
onPressed: () {
Navigator.push( //1、打开详情页
context,
MaterialPageRoute(builder: (context) {
return DetailScreen();
}),
);
},
),
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('Back'),
onPressed: () {
Navigator.pop(context); //2、关闭详情页
},
),
),
);
}
}
当 push() 被调用时,DetailScreen 页面被放置在 HomeScreen 页面的前面,此时与用户交互的页面是最顶层的DetailScreen页面,效果如下:
2)命名路由
import 'package:flutter/material.dart';
void main() {
runApp(Nav2App());
}
class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp( //1、注册路由表
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailScreen(),
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('open Details'),
onPressed: () {
Navigator.pushNamed(
context,
'/details', //2、通过路由表中路由名称,打开相应页面
);
},
),
),
);
}
}
在使用命名路由前,需要提前以name- Page键值对的形式将路由表注册到Navigator中。在进行路由跳转的时候,通过name即可打开路由表中对应的页面。相比组件路由的方式,使用命名路由打开页面代码简洁了不少。
3)生成路由
import 'package:flutter/material.dart';
void main() {
runApp(Nav2App());
}
class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: (settings) { //2、通过路由名称,生成对应路由对象
if (settings.name == '/') {
return MaterialPageRoute(builder: (context) => HomeScreen());
}
var uri = Uri.parse(settings.name);
if (uri.pathSegments.length == 2 &&
uri.pathSegments.first == 'details') {
var id = uri.pathSegments[1];
return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
}
return MaterialPageRoute(builder: (context) => UnknownScreen());
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('open Details'),
onPressed: () {
Navigator.pushNamed(
context,
'/details/1', //1、传入路由名称,打开页面
);
},
),
),
);
}
}
虽看上去生成路由与命名路由打开页面的方式一样,都是通过路由标识字符串的形式打开对应的页面。但命名路由需要提前将路由表提前注册到Navigator中,而生成路由在页面进行跳转时,临时解析路由标识字符串,并确定需要打开的页面。
在使用上,生成路由要比命名路由灵活,但是后期代码维护成本,代码结构清晰度却远不如命名路由。
通过对以上三种路由跳转方式的说明,命名路由在使用的简洁度以及代码结构清晰度上,更愿意被大部分项目所接受使用。
2、已有项目路由管理现状
项目大多采用命名路由的方式对路由进行统一管理,但是由于命名路由需要提前将路由表注册到Navigator中,所以每当新增一个页面,就需要往路由表中添加一个路由配置,例如:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
...
initialRoute: '/home',
routes: {
'/home': (context) => HomePage(), //路由注册
'/a': (context) => APage(),
'/b': (context) => BPage(),
},
);
}
}
3、路由管理方案设计
3.1、现有项目路由管理存在哪些问题?
通过分析上面路由注册配置代码,如果页面想通过命名路由的方式打开,则页面需要提前注册到Navigator中。随着项目规模变大,页面逐步增加,在MyApp中注册路由,问题也愈发明显:
耦合度:随着页面不断新增,路由注册代码也随之追加,MyApp类变得越来越臃肿,类中充斥着大量对其他类的引用。
模块化:由于路由注册逻辑统一在MyApp类中,不同模块的路由不能单独管理控制,完全的扁平化:
3.2、重塑项目路由管理
针对项目路由管理高耦合、无法模块化的问题,假设我们项目路由结构做出如下调整:
1、将项目中的所有页面按照模块进行划分,每个模块内部完成页面注册,使得不同模块的路由管理相互独立,方便开发人员进行单独的模块开发和维护。
2、通过App下的Navigator完成模块注册,方便地实现不同模块之间的页面跳转,使得Flutter应用程序的结构更加清晰,易于扩展和维护。
3.3、mixin机制说明
Flutter中的mixin机制是一种代码重用的技术,可以帮助开发者在不使用继承的情况下将代码的功能注入到其他类中。mixin可以看作是一种将一组函数、属性和其他代码注入到类中的方式,以实现代码复用。
下面是一个简单的例子,其中我们创建了一个名为 Runner 的 mixin,它包含了一个run()函数:
mixin Runner {
void run() {
print("I'm Running!");
}
}
然后我们定义了一个类Person,它使用了Runner mixin:
class Person with Runner {
String name;
Person(this.name);
}
现在,我们可以使用Person类的实例并调用其run()函数,因为Person类已经将 Runner mixin 中的函数注入到了自身中:
void main() {
var Person = Person("xiaoying");
person.run(); // Output: "I'm Running!"
}
需要注意的是,mixin机制并不是继承,而是一种注入代码的方式。因此,它可以避免一些继承带来的问题,比如多重继承的复杂性。同时,mixin机制也使得代码更加灵活,可以组合不同的功能,以满足不同的需求。
3.4、flutter-mixin-router 介绍
源码WidgetsFlutterBinding通过mixin机制来管理和协调不同模块工作,使得Flutter框架在不同平台下表现更为稳定和高效。同样,我们也可以利用这一机制,将不同的路由管理模块进行组合,并注册到App的Navigator中,实现不同模块之间的路由管理相互独立,具体代码如下:
1)创建模块管理基类
class MixinRouterContainer {
Map<String, WidgetBuilder> installRouters() => {};
Future<T?>? openPage<T>(BuildContext context, String pageName ...}) {
...
return Navigator.pushNamed(context, pageName, arguments: args);
...
}
}
在基类中仅定义了两个方法:
installRouters: 注册该模块下的所有页面。
openPage:打开该模块下注册的页面
2)页面注册到模块中
//设置模块
mixin SettingRouteContainer on MixinRouterContainer {
@override
Map<String, WidgetBuilder> installRouters() {
Map<String, WidgetBuilder> originRoutes = super.installRouters();
Map<String, WidgetBuilder> newRoutes = {};
newRoutes['/setting_a'] = (context) => APage(); //注册A页面
newRoutes['/setting_b'] = (context) => BPage(); //注册B页面
newRoutes.addAll(originRoutes);
return newRoutes;
}
}
//大厅模块
mixin HomeRouteContainer on MixinRouterContainer {
@override
Map<String, WidgetBuilder> installRouters() {
Map<String, WidgetBuilder> originRoutes = super.installRouters();
Map<String, WidgetBuilder> newRoutes = {};
newRoutes['/home'] = (context) => HomePage(); //注册大厅页面
newRoutes.addAll(originRoutes);
return newRoutes;
}
}
不同模块的路由管理都通过mixin RouterContainer,并将模块内部的页面注册到其中。例如,HomeRouteContainer 将 HomePage 添加到自己的路由表中,SettingRouteContainer 管理 SettingA 和 SettingB 两个页面。不同的路由模块都能够独立管理自己模块内的页面,从而实现了路由模块的高度解耦。
3)模块注册到App中
//1、创建总路由管理类,并通过mixin机制将各个路由模块进行粘合,即:
// AppRouteContainer 将 HomeRouteContainer 和 SettingRouteContainer... 组装
class AppRouteContainer extends MixinRouterContainer with HomeRouteContainer, SettingRouteContainer {
AppRouteContainer._();
static AppRouteContainer _instance = AppRouteContainer._();
static AppRouteContainer get share => _instance;
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
...
initialRoute: '/home',
// 2、将总路由注册到App中
routes: AppRouteContainer.share.installRouters(),
);
}
}
通过创建一个总路由管理类,利用mixin机制将不同的路由模块组合起来,然后将其注册到App的Navigator中。在代码中,我们可以看到AppRouteContainer通过组合大厅路由模块(HomeRouteContainer)和设置路由模块(SettingRouteContainer),实现了路由模块的高度解耦。为了方便在项目中使用总路由管理类,我们将其改写为单例模式。在使用总路由管理类时,只需要了解以下两个方法:
路由表注册:AppRouteContainer.share.installRouters()
页面跳转:AppRouteContainer.share.openPage(context, '/setting_a')
4、路由管理方案扩充
4.1、如何进行路由拦截?
在项目开发中,路由拦截是一个常见的需求。例如,当用户尚未登录时,如果想打开个人主页,需要拦截这一过程并将用户重定向到登录页面。
为了解决这个问题,可以在现有的路由管理模块的基类(MixinRouterContainer)上再封装,添加拦截路由表并重写路由跳转过程来实现。
//定义路由拦截回调函数
typedef MixinRouteInterceptor = bool Function(BuildContext context, String pageName, ...);
//对MixinRouterContainer进行封装
class MixinRouterInterceptContainer extends MixinRouterContainer {
//添加拦截路由表逻辑
final Map<String, MixinRouteInterceptor> _routeInterceptorTable = {};
void registerRouteInterceptor(String pageName, MixinRouteInterceptor interceptor) {
_routeInterceptorTable[pageName] = interceptor;
}
void unRegisterRouteInterceptor(String pageName) {
_routeInterceptorTable.remove(pageName);
}
//重写路由跳转过程
@override
Future<T?>? openPage<T>(BuildContext context, String pageName,...) {
//拦截跳转
if (!_routeInterceptorTable.containsKey(pageName)) {
return super.openPage(context,pageName,...);
}
MixinRouteInterceptor interceptor = _routeInterceptorTable[pageName]!;
bool needIntercept = interceptor.call(context,pageName,...);
if (needIntercept) {
return Future.value(null);
} else {
return super.openPage(context,pageName,...);
}
}
}
该类增加了拦截路由配置的注册和反注册逻辑,在进行页面跳转时,判断当前路由是否能被拦截,如果是,则会拦截后续的页面跳转逻辑,并执行拦截相关的处理工作,否则将会继续进行页面跳转。
下面我们来改造大厅路由管理模块,使之能处理登录拦截,代码如下:
mixin HomeRouteContainer on MixinRouterInterceptContainer {
@override
Map<String, WidgetBuilder> installRouters() {
//注册拦截路由表
registerRouteInterceptor('/home', (...) => if(!isLogin) openLoginPage());
Map<String, WidgetBuilder> originRoutes = super.installRouters();
Map<String, WidgetBuilder> newRoutes = {};
newRoutes['/home'] = (context) => HomePage();
newRoutes.addAll(originRoutes);
return newRoutes;
}
}
4.2、怎么实现Url统跳?
对于很多项目来说,为了能够通过外链打开对应的页面,常常采用URL统一跳转。为了实现这个功能,只需要对现有的总路由管理类AppRouteContainer进行扩展,代理默认的页面打开行为,实现URL的解析。
class AppRouteContainer extends MixinRouterContainerwith HomeRouteContainer, SettingRouteContainer {
...
Future<T?>? urlToPage<T>(BuildContext context, String urlStr, ...) {
//1、解析URL
Uri? url = Uri.tryParse(urlStr);
if (url == null) return Future.error('parse url fail');
Map<String, String> args = {};
args.addAll(url.queryParameters);
args['_url'] = urlStr;
String pageName = url.host;
//2.通过HOST作为路由名称,打开对应页面
super.openPage(context,'/' + pageName ...);
}
}
在项目中,可以通过添加 urlToPage(...) 方法来对 openPage(...) 方法进行封装。通过调用 AppRouteContainer.share.urlToPage(...) 并传入 URL 字符串,该方法会对传入的URL进行解析,并提取出 HOST 和参数。HOST 作为路由名称,并将参数传递给 openPage 方法,从而打开对应的页面。这样就可以实现通过 URL 统一跳转到对应的页面。
5、路由管理方案增效
5.1、回顾与思考
经过以上的路由改造,是否就可以说路由的问题解决了呢?然而,仔细回顾之前的改造过程,我们会发现还存在一些问题:
页面注册:仍需要手动注册新的页面到对应的模块类中。
路由模块管理:还需要手动创建并维护不同的路由管理模块,如 HomeRouteContainer、SettingRouteContainer
5.2、站在巨人的肩膀上前行
客户端原生项目中,ARouter通过注解生成路由管理文件可以省去手动创建和维护不同的路由管理模块的麻烦。Flutter可以借鉴这种方式,使用注解来自动生成对应的路由模块管理文件。这样做的好处是可以提高开发效率,降低出错率,同时也可以避免代码冗余和重复的工作。另外,注解方式可以让开发者更加专注于业务逻辑的开发,而不用花费太多精力在路由管理的维护上。示例代码如下:
1)注解子路由表
const String HOME_ROUTE_TABLE = 'HomeRouteTable';
const String SETTING_ROUTE_TABLE = 'SettingsRouteTable';
//tDescription: 仅仅作为生成类的注释
@RouterTableList(
tableList: [
RouterTable(tName: HOME_ROUTE_TABLE, tDescription: '大厅路由模块'),
RouterTable(tName: SETTING_ROUTE_TABLE, tDescription: '设置路由模块'),
],
)
class AppRouteContainer extends MixinRouterInterceptContainer
with HomeRouteTable, SettingsRouteTable {
AppRouteContainer._();
static AppRouteContainer _instance = AppRouteContainer._();
static AppRouteContainer get share => _instance;
}
2)注解普通路由
//将页面注册到 SettingsRouteTable 模块中,并指定页面的路由名称
@MixinRoute(tName: SETTING_ROUTE_TABLE, path: '/setting_a')
class APage extends StatelessWidget {
const APage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Text('APage'),
);
}
}
3)注解拦截路由
@MixinRoute(tName: SETTING_ROUTE_TABLE, path: '/setting_b')
class BPage extends StatelessWidget {
const BPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Text('BPage'),
);
}
}
//注解拦截路由函数
@MixinInterceptRoute(tName: SETTING_ROUTE_TABLE, path: '/setting_b')
bool interceptorMinePage(context, pageName, pushType, {arguments, predicate}) {
print('toLogin');
return true;
}
编写一个顶层函数,并通过 MixinInterceptRoute 进行注解,该函数签名具体如下:
bool Function(BuildContext context, String pageName, String pushType, {Map<String, dynamic>? arguments, bool Function()? predicate});
- BuildContext context:BuildContext对象,表示当前BuildContext。
- String pageName:字符串类型,表示需要拦截的页面名称。
- String pushType:字符串类型,表示跳转类型,如push、pushNamed、pushReplacement等。
- Map<String, dynamic>? arguments:可选的Map类型,表示传递给目标页面的参数。
- bool Function()? predicate:可选的bool类型回调函数,控制页面打开策略。
- 函数返回结果代表本次拦截是否消费原本的页面跳转,如果返回true,则继续执行后续页面打开操作,否则终止后续跳转逻辑。
6、项目集成
在项目的pubspec.yaml中添加依赖,即可开启注解路由之旅
dependencies:
flutter:
sdk: flutter
flutter_mixin_router: ^1.0.0 # 添加路由模块管理基类
flutter_mixin_router_ann: 1.0.0 # 添加注解类
dev_dependencies:
build_runner: 2.1.8 # 添加依赖
flutter_mixin_router_gen: 1.0.1 # 添加代码生成工具库
在项目页面上添加对应的注解后,执行以下命令生成对应的路由代码
# 清除增量编译缓存
flutter packages pub run build_runner clean
# 重新生成代码
flutter packages pub run build_runner build --delete-conflicting-outputs
7、实际项目收益
1)项目代码结构的优化
路由注册相关的代码可以被分模块管理,这使得项目中的App入口类代码行数从1500行缩减到200行以内,项目的代码结构更加清晰,降低了代码的维护难度,提高代码的可读性和可维护性。
2)路由代码冲突的减少
不同的开发人员负责开发不同的模块,如果路由相关的代码没有被分模块管理,那么就容易出现代码冲突的问题,在使用 flutter_mixin_router 后,路由相关的代码合并冲突几乎不再发生,提高代码的稳定性,从而降低了项目出错的概率。
3)开发效率的提升
使用注解可以省去编写路由注册相关的代码,提高了代码的简洁性。由于开发人员可以更加专注于业务逻辑的实现,从而提高了开发效率。
8、总结与展望
使用 flutter_mixin_router 可以让开发人员更专注于业务逻辑的实现,快速地迭代开发,提高项目的上线速度;模块化的路由管理,能够有效地应对项目规模的增长,并保持代码的一致性和可维护性。
希望该框架的持续维护和更新也能够为团队提供更多的功能,满足不断增长的业务需求。
作者:杨浪
来源:微信公众号:映客技术
出处:https://mp.weixin.qq.com/s/Yiq140plcoOKsgwL01Hhzw
相关推荐
- 无力吐槽的自动续费(你被自动续费困扰过吗?)
-
今天因为工作需要,需要在百度文库上下载一篇文章。没办法,确实需要也有必要,只能老老实实的按要求买了个VIP。过去在百度文库上有过类似经历,当时为了写论文买了一个月的VIP,后面也没有太注意,直到第二个...
- 百度文库推出“文源计划”创作者可一键认领文档
-
11月7日,百度文库发布了旨在保护创作者权益的“文源计划”。所谓“文源计划”,即为每一篇文档找到源头,让创作者享受更多的权益。据百度文库总经理李小婉介绍,文源计划分为三部分,分别是版权认证、版权扶持和...
- 有开放大学学号的同学,百度文库高校版可以用了。
-
还在网上找百度文库的下载方式,只要从身边的朋友在读开放大学的,那他(她)的学号就可以登陆到国家开放大学图书馆,还使用百度文库高校版来下载。与百度文库稍有不同,但足够使用了。现转国图链接如下:htt...
- 搜索资源方法推荐(搜索资源的方法)
-
今天msgbox就要教大家如何又快又准的搜到各类资源,第一点,排除干扰百度搜索出来啊经常前排展示它的产品以及百度文库,如何去除呢?很简单,后面输入空格减号百度文库,比如你搜高等数学百度文库很多,只要后...
- 一行代码搞定百度文库VIP功能(2021百度文库vip账号密码共享)
-
百度文库作为大家常用查资料找文档的平台,大多数文档我们都可以直接在百度文库找到,然而百度文库也有让人头痛的时候。好不容易找到一篇合适的文档,当你准备复制的时候他却提示你需要开通VIP才能复制~~~下载...
- 百度文库文档批量上传工具用户说明书
-
百度文库文档批量上传工具用户说明书1、软件主要功能1、批量上传文档到百度文库,支持上传到收费、VIP专享、优享以及共享。2、支持自动分类和自动获取标签3、支持多用户切换,一个账户传满可以切换到...
- 百度文库现在都看不到文档是否上传成功,要凉了吗?
-
打开知识店铺,百度文库文档里显示都是下载这一按键,上传的文档也看不到是否成功?咋情况,要取消了吗?没通过审核的也不让你删除,是几个意思,想通吃吗?现在百度上传文档也很费劲,有时弄了半天的资料上传审核过...
- 微信推广引流108式:利用百度文库长期分享软文引流
-
百度文库相对于百度知道、百度百科来说,操作上没那么多条条框框,规则上也相对好把握些。做一条百度知道所花费的精力一般都会比做一条百度文库的要多些,老马个人操作下来觉得百度文库更好把握。但见仁见智吧,今天...
- 职场“避雷”指南 百度文库推出标准化劳动合同范本
-
轰轰烈烈的毕业季结束了,众多应届生在经过了“职场海选”后,已正式成为职场生力军的一员。这一阶段,除了熟悉业务,签订劳动合同、了解职场福利也迅速被提上日程。而随着国人法律意识的增强,百度文库内《劳动合同...
- 《百度文库》:素材精选宝库(百度文库官网首页)
-
《百度文库》:独特功能助力选择高质量素材在当今信息爆炸的时代,如何高效地获取并利用有价值的素材成为了许多人面临的挑战。而《百度文库》作为百度公司推出的一款在线文档分享平台,凭借其丰富的资源、强大的功能...
- 深度整合和开放AI能力 百度文库和网盘推出内容操作系统「沧舟OS」
-
【TechWeb】4月25日消息,Create2025百度AI开发者大会上,百度文库和百度网盘推出全球首个内容操作系统——沧舟OS。基于沧舟OS,百度文库APP全新上线「GenFlow超能搭子」...
- 女子发现大二作业被百度文库要求付费下载,律师:平台侵权,应赔偿
-
近日,28岁的黎女士在百度百科搜索家乡的小地名时,发现了自己在大二完成的课题作业。她继续搜索,发现多个平台收录了该文,比如豆丁网和文档之家等,有的还设置了付费或积分下载。2月15日,九派新闻记者以用户...
- 2016杀入百度文库的新捷径,只有少数人才知道的喔
-
百度的产品在SEO优化中的分量真不用多说,其实很多人都像我一样一直在找捷径。但是我经常发现很多人都是在用死方法。比如发贴吧发帖而不知道去申请一个吧主,知道自问自答而不知道去申请一个合作资格。口碑和贴吧...
- 百度文库付费文档搜索方法(百度文库付费文档搜索方法有哪些)
-
一直以来,百度文库中无论是个人中心还是个人主页,都没有像淘宝一样的店内搜索功能,连最近新开的知识店铺也没有设计店内搜索功能,这无论是对上传用户还是下载用户都不方便,上传用户想要搜索自己的文档无法办到...
- 供读者免费使用!泰达图书馆机构版百度文库新年上新啦
-
在泰达图书馆读者使用百度文库数字资源不需要VIP,免-费-用!惊不惊喜?快来了解一下吧……新年伊始,为满足区域企业、高校、科研院所以及居民群众在教学、科研及学习过程中,对各类文献资源的需求,泰达图书馆...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 如何绘制折线图 (52)
- javaabstract (48)
- 新浪微博头像 (53)
- grub4dos (66)
- s扫描器 (51)
- httpfile dll (48)
- ps实例教程 (55)
- taskmgr (51)
- s spline (61)
- vnc远程控制 (47)
- 数据丢失 (47)
- wbem (57)
- flac文件 (72)
- 网页制作基础教程 (53)
- 镜像文件刻录 (61)
- ug5 0软件免费下载 (78)
- debian下载 (53)
- ubuntu10 04 (60)
- web qq登录 (59)
- 笔记本变成无线路由 (52)
- flash player 11 4 (50)
- 右键菜单清理 (78)
- cuteftp 注册码 (57)
- ospf协议 (53)
- ms17 010 下载 (60)