百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

探索Flutter路由管理新姿势(flutter路由返回自动刷新)

cac55 2024-09-20 12:51 30 浏览 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 contextBuildContext对象,表示当前BuildContext。
  • String pageName字符串类型,表示需要拦截的页面名称。
  • String pushType字符串类型,表示跳转类型,如pushpushNamedpushReplacement等。
  • 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

相关推荐

如何屏蔽色情网站?_怎么能屏蔽网站

一、基础防御:全网DNS劫持阻断1.修改全网DNS服务器推荐DNS:安全DNS:CleanBrowsing(成人内容过滤):185.228.168.168/185.228.169.168Open...

容器、Pod、虚拟机与宿主机网络通信全解:看这一篇就够了

在日常开发与部署过程中,很多人一开始都会有这样的疑惑:容器之间是怎么通信的?容器怎么访问宿主机?宿主机又如何访问容器?Kubernetes中Pod的网络和Docker容器一样吗?容器跨机器是...

Win11专业版找不到共享打印机的问题

有很多深度官网的用户,都是在办公室上班的。而上班就需要使用打印机,但更新win11系统后,却出现同一个办公室里面的打印机都找不到的问题,这该如何处理呢?其实,可能是由于我们并没有打开共享打印机而造成的...

常用电脑快捷键大全,摆脱鼠标依赖,建议收藏

Ctrl+C复制Ctrl+X剪切Ctrl+V粘贴Ctrl+Z撤销Ctrl+Y重做Ctrl+B加粗Ctrl+A全选所有文件Ctrl+S保存Ctrl+N新建Ctrl+O打开Ctrl+E...

Win11实现自动追剧Jellyfin硬解,免NAS复杂操作

大家好,欢迎来到思赞数码。本期将详细介绍如何通过安装和配置Sonarr、Radarr、Prowlarr、qBittorrent和Jellyfin,打造一套自动化的影视管理系统。很多人认为,要实现自动追...

微软Win11安卓子系统WSA 2308.40000.3.0更新推送下载

IT之家9月21日消息,微软官方博客今日宣布,已面向所有WindowsInsider用户推送了Windows11安卓子系统的2308.40000.3.0版本更新。本次更新和之前...

路由器总掉线 一个命令就能猜出八九分

明明网络强度满格或有线图标正常,但视频卡成PPT、网页刷不开、游戏动不了,闲心这些问题很多小伙伴都碰到过。每次都要开关路由、宽带/光猫、插拔网线……一通忙。有没有啥办法能快速确定故障到底在哪儿,方便处...

windows电脑如何修改hosts文件?_windows怎么修改hosts

先来简单说下电脑host的作用hosts文件的作用:hosts文件是一个用于储存计算机网络中各节点信息的计算机文件;作用是将一些常用的网址域名与其对应的IP地址建立一个关联“数据库”,当用户在浏览器中...

win10广告弹窗ShellExperienceHost.exe

win10右下角老是弹出广告弹窗,排查为以下程序引起,但是这个是系统菜单的程序不能动:C:\Windows\SystemApps\ShellExperienceHost_cw5n1h2txyewy\S...

Win10 Mobile预览版10512/10166越狱解锁部署已被黑客攻破

看起来统一的WindowsPhone和Windows越加吸引人们的关注,特别是黑客们的好奇心。XDA论坛宣称,在Win10Mobile预览版10512/10166上,已取得越狱/解锁部署突破,比如可...

6款冷门小众软件,都是宝藏,建议收藏

真的很不错(。-ω-)zzzBearhttps://bear.app/cn/Bear是一个漂亮,灵活的Markdown的写作工具。它一样只支持苹果家的全平台。它一出现就惊艳四方,就被AppSto...

如何让不符合条件的设备升级Windows 11

如果你是最近(6月24日之后)加入WindowsInsider项目并且你的设备并不符合升级条件,那么当你在尝试升级Windows11的时候可能会看到以下错误:你的PC不符合Wi...

windows host文件怎么恢复?局域网访问全靠这些!

windowshost文件怎么恢复?windowshost文件是常用网址域名及其相应IP地址建立一个关联文件,通过这个host文件配置域名和IP的映射关系,以提高域名解析的速度,方便局域网用户使用...

Mac Hosts管理工具---SwitchHosts

switchhosts!formac是一款帮助用户快速切换hosts文件的工具,switchhosts!formac能够帮助你快速方便的打造个人专用的网络环境,支持本地和在线两种方式,并且支持...

「浅谈趣说网络知识」 第十二弹 老而不死的Hosts,它还很有用

【浅谈趣说网络知识】第十二弹老而不死的Hosts,它还很有用什么时候才觉得自己真的老了,不是35岁以上的数字,不是头上的点点白发,而是不知觉中的怀旧。风口上的IT界讲的就是"长江后浪推前浪...

取消回复欢迎 发表评论: