[譯] 深刻了解 Flutter

Flutter 是一種新的框架,能夠在短期內爲 iOS 和 Android 構建高質量原生 App。根據我使用 Flutter(做爲 Flutter 團隊成員)的經驗,開發速度主要經過如下方式體現:前端

  • 有狀態的熱重載。Flutter 開發由 Dart 編譯器/ VM 技術提供支持,它容許你在保留應用程序狀態(包括你導航到的位置)的同時將代碼更改加載到正在運行的應用程序中。點擊保存,你將在不到一秒的時間內看到設備更改的效果。
  • 響應式編程。Flutter 在其定義和更新用戶界面的方法中遵循其餘現代框架:二者都基於接口如何依賴於當前狀態的單個描述。
  • 組成。在 Flutter 中,萬物皆組件,並且經過自由組合漂亮的組件和樂高積木風格,你能夠實現任何想要的結果。
  • 代碼編寫 UI。Flutter 沒有單獨的佈局標記語言。每一個組件只在 Dart 中的一個地方編寫,縮減了語法切換和文件切換的開銷。

有意思的是,上面的最後三個特色造成了對開發速度的挑戰:在你的方式和你的視圖邏輯中深刻嵌套的 widget 樹android

接下來我會討論爲何會出現這個問題和咱們能作什麼。同時,我會嘗試說明 Flutter 的工做原理。ios


響應式編程

Flutter 的響應式編程模型邀請你使用聲明性編程來定義您的用戶界面,做爲當前狀態的函數:git

@override
Widget build(BuildContext context) {
  return // 一些基於當前狀態的組件
}
複製代碼

組件是用戶界面的不可變描述。咱們被要求返回由單個表達式定義的單個組件。沒有用於配置或更新可變視圖的 mutator 命令序列。相反,咱們只是調用一些組件構造函數。github

組成

Widgets are typically simple, each doing one thing well: Text, Icon, Padding, Center, Column, Row, … To achieve any non-trivial outcome, many widgets must be composed. So our single expression easily becomes a deeply nested tree of widget constructor calls:express

組件除子屬性還有其餘屬性,可是你明白的。編程

代碼編寫 UI

編寫和編輯深層嵌套的樹須要一個優雅的編輯器和一些練習來提升效率。開發人員彷佛在佈局標記(XML,HTML)中比在代碼中更能容忍深度嵌套,但 Flutter 的 UI-as-code 方法確實意味着深層嵌套 code。不管你在組件樹中有什麼視圖邏輯——條件,轉換,在讀取當前狀態時使用的迭代,用於更改它的事件處理程序——也會深深嵌套。後端

這就是接下來的挑戰。bash


挑戰

flutter.io 的 佈局教程 提供了一個說明性的例子——看起來像是——一個湖泊探險家應用程序。app

這是實現此視圖的原始組件樹:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(title: Text('Top Lakes')),
        body: ListView(
          children: <Widget>[
            Image.asset(
              'images/lake.jpg',
              width: 600.0,
              height: 240.0,
              fit: BoxFit.cover,
            ),
            Container(
              padding: const EdgeInsets.all(32.0),
              child: Row(
                children: <Widget>[
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Container(
                          padding: const EdgeInsets.only(bottom: 8.0),
                          child: Text(
                            'Oeschinen Lake Campground',
                            style: TextStyle(fontWeight: FontWeight.bold),
                          ),
                        ),
                        Text(
                          'Kandersteg, Switzerland',
                          style: TextStyle(color: Colors.grey[500]),
                        ),
                      ],
                    ),
                  ),
                  Row(
                    children: <Widget>[
                      Icon(Icons.star, color: Colors.red[500]),
                      Text('41'),
                    ],
                  ),
                ],
              ),
            ),
            Container(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  Column(
                    mainAxisSize: MainAxisSize.min,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Icon(Icons.call, color: Theme.of(context).primaryColor),
                      Container(
                        margin: const EdgeInsets.only(top: 8.0),
                        child: Text(
                          'CALL',
                          style: TextStyle(
                            fontSize: 12.0,
                            fontWeight: FontWeight.w400,
                            color: Theme.of(context).primaryColor,
                          ),
                        ),
                      ),
                    ],
                  ),
                  Column(
                    mainAxisSize: MainAxisSize.min,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Icon(Icons.near_me,
                          color: Theme.of(context).primaryColor),
                      Container(
                        margin: const EdgeInsets.only(top: 8.0),
                        child: Text(
                          'ROUTE',
                          style: TextStyle(
                            fontSize: 12.0,
                            fontWeight: FontWeight.w400,
                            color: Theme.of(context).primaryColor,
                          ),
                        ),
                      ),
                    ],
                  ),
                  Column(
                    mainAxisSize: MainAxisSize.min,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Icon(Icons.share, color: Theme.of(context).primaryColor),
                      Container(
                        margin: const EdgeInsets.only(top: 8.0),
                        child: Text(
                          'SHARE',
                          style: TextStyle(
                            fontSize: 12.0,
                            fontWeight: FontWeight.w400,
                            color: Theme.of(context).primaryColor,
                          ),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            Container(
              padding: const EdgeInsets.all(32.0),
              child: Text(
                'Lake Oeschinen lies at the foot of the Blüemlisalp in the '
                    'Bernese Alps. Situated 1,578 meters above sea level, it '
                    'is one of the larger Alpine Lakes. A gondola ride from '
                    'Kandersteg, followed by a half-hour walk through pastures '
                    'and pine forest, leads you to the lake, which warms to '
                    '20 degrees Celsius in the summer. Activities enjoyed here '
                    'include rowing, and riding the summer toboggan run.',
                softWrap: true,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
複製代碼

這只是一個靜態組件樹,沒有實現任何行爲。可是將視圖邏輯直接嵌入到這樣的樹中估計不會是一次愉快的體驗。

接受挑戰。


從新審視代碼編寫 UI

使用 Flutter 的 UI-as-code 方法時,組件樹就是代碼。所以,咱們可使用全部經常使用的代碼組織工具來改善這種狀況。工具箱中最簡單的工具之一就是命名子表達式。這會在語法上將組件樹翻出來。而不是

return A(B(C(D(), E())), F());
複製代碼

咱們能夠命名每一個子表達式並獲得

final Widget d = D();
final Widget e = E();
final Widget c = C(d, e);
final Widget b = B(c);
final Widget f = F();
return A(b, f);
複製代碼

咱們的湖泊應用能夠重寫成下面這樣:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final Widget imageSection = Image.asset(
      'images/lake.jpg',
      width: 600.0,
      height: 240.0,
      fit: BoxFit.cover,
    );
    final Widget titles = Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Container(
          padding: const EdgeInsets.only(bottom: 8.0),
          child: Text(
            'Oeschinen Lake Campground',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        ),
        Text(
          'Kandersteg, Switzerland',
          style: TextStyle(color: Colors.grey[500]),
        ),
      ],
    );
    final Widget stars = Row(
      children: <Widget>[
        Icon(Icons.star, color: Colors.red[500]),
        Text('41'),
      ],
    );
    final Widget titleSection = Container(
      padding: const EdgeInsets.all(32.0),
      child: Row(
        children: <Widget>[
          Expanded(child: titles),
          stars,
        ],
      ),
    );
    final Widget callAction = Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Icon(Icons.call, color: Theme.of(context).primaryColor),
        Container(
          margin: const EdgeInsets.only(top: 8.0),
          child: Text(
            'CALL',
            style: TextStyle(
              fontSize: 12.0,
              fontWeight: FontWeight.w400,
              color: Theme.of(context).primaryColor,
            ),
          ),
        ),
      ],
    );
    final Widget routeAction = Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Icon(Icons.near_me, color: Theme.of(context).primaryColor),
        Container(
          margin: const EdgeInsets.only(top: 8.0),
          child: Text(
            'ROUTE',
            style: TextStyle(
              fontSize: 12.0,
              fontWeight: FontWeight.w400,
              color: Theme.of(context).primaryColor,
            ),
          ),
        ),
      ],
    );
    final Widget shareAction = Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Icon(Icons.share, color: Theme.of(context).primaryColor),
        Container(
          margin: const EdgeInsets.only(top: 8.0),
          child: Text(
            'SHARE',
            style: TextStyle(
              fontSize: 12.0,
              fontWeight: FontWeight.w400,
              color: Theme.of(context).primaryColor,
            ),
          ),
        ),
      ],
    );
    final Widget actionSection = Container(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          callAction,
          routeAction,
          shareAction,
        ],
      ),
    );
    final Widget textSection = Container(
      padding: const EdgeInsets.all(32.0),
      child: Text(
        'Lake Oeschinen lies at the foot of the Blüemlisalp in the '
            'Bernese Alps. Situated 1,578 meters above sea level, it '
            'is one of the larger Alpine Lakes. A gondola ride from '
            'Kandersteg, followed by a half-hour walk through pastures '
            'and pine forest, leads you to the lake, which warms to '
            '20 degrees Celsius in the summer. Activities enjoyed here '
            'include rowing, and riding the summer toboggan run.',
        softWrap: true,
      ),
    );
    final Widget scaffold = Scaffold(
      appBar: AppBar(title: Text('Top Lakes')),
      body: ListView(
        children: <Widget>[
          imageSection,
          titleSection,
          actionSection,
          textSection,
        ],
      ),
    );
    return MaterialApp(
      title: 'Flutter Demo',
      home: scaffold,
    );
  }
}
複製代碼

縮進級別如今更合理,咱們能夠經過引入更多名稱使子樹的縮進級別變得像咱們但願的那樣淺。更好的是,經過爲各個子樹提供有意義的名稱,咱們能夠表示每一個子樹的做用。因此咱們如今能夠談談 xxxAction 子樹......並觀察到咱們在這裏面有不少重複的代碼!另外一個基本的代碼組織工具——功能抽象——負責這部份內容:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final Widget imageSection = ...
    final Widget titles = ...
    final Widget stars = ...
    final Widget titleSection = ...

    Widget action(String label, IconData icon) {
      final Color color = Theme.of(context).primaryColor;
      return Column(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Icon(icon, color: color),
          Container(
            margin: const EdgeInsets.only(top: 8.0),
            child: Text(
              label,
              style: TextStyle(
                fontSize: 12.0,
                fontWeight: FontWeight.w400,
                color: color,
              ),
            ),
          ),
        ],
      );
    }

    final Widget actionSection = Container(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          action('CALL', Icons.call),
          action('ROUTE', Icons.near_me),
          action('SHARE', Icons.share),
        ],
      ),
    );
    final Widget textSection = ...
    final Widget scaffold = ...
    return MaterialApp(
      title: 'Flutter Demo',
      home: scaffold,
    );
  }
}
複製代碼

咱們將看到一個簡單功能抽象的替代,它會更具備更 Flutter 風格的。

從新審視組成

接下來是什麼?好吧,build 方法依然很長。也許咱們能夠提取一些有意義的做品......片段?組件!Flutter 的組件都是關於組合和重用的。咱們用框架提供的簡單組件組成了一個複雜的組件 可是發現結果過於複雜,咱們能夠選擇把它分解成不太複雜的自定義組件。定製組件是 Flutter 世界中的一等公民,而明肯定義的組件具備很大的潛力被重用。讓咱們將 action 函數轉換爲 Action 組件類型並將其放在本身的文件中:

import 'package:flutter/material.dart';
import 'src/widgets.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final Widget imageSection = ...
    final Widget titles = ...
    final Widget titleSection = ...
    final Widget actionSection = Container(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          Action(label: 'CALL', icon: Icons.call),
          Action(label: 'ROUTE', icon: Icons.near_me),
          Action(label: 'SHARE', icon: Icons.share),
        ],
      ),
    );
    final Widget textSection = ...
    final Widget scaffold = ...
    return MaterialApp(
      title: 'Flutter Demo',
      home: scaffold,
    );
  }
}
複製代碼
import 'package:flutter/material.dart';

class Action extends StatelessWidget {
  Action({Key key, this.label, this.icon}) : super(key: key);

  final String label;
  final IconData icon;

  @override
  Widget build(BuildContext context) {
    final Color color = Theme.of(context).primaryColor;
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Icon(icon, color: color),
        Container(
          margin: const EdgeInsets.only(top: 8.0),
          child: Text(
            label,
            style: TextStyle(
              fontSize: 12.0,
              fontWeight: FontWeight.w400,
              color: color,
            ),
          ),
        ),
      ],
    );
  }
}
複製代碼

如今咱們能夠在應用程序的任何位置重用 Action 組件,就像它是由 Flutter 框架定義的同樣。

可是,嘿,頂級的 action 功能不能知足一樣的需求嗎?

通常來講,不能。

  • 許多組件是由其餘組件構造的;它們的構造函數有 WidgetList<Widget> 類型的 childchildren 參數。因此 action 函數不能傳遞給任何一個函數。固然,調用 action 的結果能夠。可是,你將經過在當前構建環境中預先構造的組件樹,而不是 StatelessWidget,它只在必要時才構建子樹,而且是最後在整個樹中定義的上下文中定義的。注意到表達式中在 Action.build 開頭的 Theme.of(context).primaryColor 了嗎?它從父鏈上最近的 Theme 組件中檢索主顏色——在調用 action 時,它極可能與最近的 Theme 不一樣。
  • Action is defined as a StatelessWidget which is little more than a build function turned into an instance method. But there are other kinds of widget with more elaborate behavior. Clients of Action shouldn’t care what kind of widget Action is. As an example, if we wanted to endow Action with an intrinsic animation, we might have to turn it into a StatefulWidget to manage the animation state. The rest of the app should be unaffected by such a change.

從新審視響應式編程

狀態管理是開始利用 Flutter 響應式編程模型,並讓咱們的靜態視圖生動起來的暗示。讓咱們定義應用程序的狀態。咱們將盡可能保持簡單,先假設一個 Lake 業務邏輯類,其惟一可變狀態是用戶是否已加星標:

abstract class Lake {
  String get imageAsset;
  String get name;
  String get locationName;
  String get description;

  int get starCount;
  bool get isStarred;
  void toggleStarring();

  void call();
  void route();
  void share();
}
複製代碼

而後,咱們能夠從 Lake 實例動態地構造咱們的組件樹,而且同時還能夠設置事件處理程序以調用其方法。響應式編程模型的優勢在於咱們只需在代碼庫中執行一次。只要 Lake 實例發生變化,Flutter 框架就會重建咱們的組件樹——前提是咱們告訴框架。這須要使 MyApp 成爲一個 StatefulWidget,這反過來又涉及將組件構建委託給一個相關的 State 對象,而後每當咱們在 Lake 上加星標時調用 State.setState 方法。

import 'package:flutter/material.dart';
import 'src/lake.dart';
import 'src/widgets.dart';

void main() {
  // 僞裝咱們從業務邏輯中獲取 Lake 實例。
  final Lake lake = Lake();
  runApp(MyApp(lake));
}

class MyApp extends StatefulWidget {
  final Lake lake;

  MyApp(this.lake);

  @override
  MyAppState createState() => new MyAppState();
}

class MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    final Lake lake = widget.lake;
    final Widget imageSection = Image.asset(
      lake.imageAsset,
      width: 600.0,
      height: 240.0,
      fit: BoxFit.cover,
    );
    final Widget titles = Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Container(
          padding: const EdgeInsets.only(bottom: 8.0),
          child: Text(
            lake.name,
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        ),
        Text(
          lake.locationName,
          style: TextStyle(color: Colors.grey[500]),
        ),
      ],
    );
    final Widget stars = GestureDetector(
      child: Row(
        children: <Widget>[
          Icon(
            lake.isStarred ? Icons.star : Icons.star_border,
            color: Colors.red[500],
          ),
          Text('${lake.starCount}'),
        ],
      ),
      onTap: () {
        setState(() {
          lake.toggleStarring();
        });
      },
    );
    final Widget titleSection = Container(
      padding: const EdgeInsets.all(32.0),
      child: Row(
        children: <Widget>[
          Expanded(child: titles),
          stars,
        ],
      ),
    );
    final Widget actionSection = Container(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          Action(label: 'CALL', icon: Icons.call, handler: lake.call),
          Action(label: 'ROUTE', icon: Icons.near_me, handler: lake.route),
          Action(label: 'SHARE', icon: Icons.share, handler: lake.share),
        ],
      ),
    );
    final Widget textSection = Container(
      padding: const EdgeInsets.all(32.0),
      child: Text(
        lake.description,
        softWrap: true,
      ),
    );
    final Widget scaffold = Scaffold(
      appBar: AppBar(title: Text('Top Lakes')),
      body: ListView(
        children: <Widget>[
          imageSection,
          titleSection,
          actionSection,
          textSection,
        ],
      ),
    );
    return MaterialApp(
      title: 'Flutter Demo',
      home: scaffold,
    );
  }
}
複製代碼
import 'package:flutter/material.dart';

class Action extends StatelessWidget {
  Action({Key key, this.label, this.icon, this.handler}) : super(key: key);

  final String label;
  final IconData icon;
  final VoidCallback handler;

  @override
  Widget build(BuildContext context) {
    final Color color = Theme.of(context).primaryColor;
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        IconButton(
          icon: Icon(icon, color: color),
          onPressed: handler,
        ),
        Text(
          label,
          style: TextStyle(
            fontSize: 12.0,
            fontWeight: FontWeight.w400,
            color: color,
          ),
        ),
      ],
    );
  }
}
複製代碼

這有用,但效率不高。最初的挑戰是深度嵌套的組件樹。那個樹仍然在那裏,若是不在咱們的代碼中,那麼就在運行時。重建全部這些只是爲了切換切換星標徹底是一種浪費。固然,Dart 的實現能夠很是有效地處理短壽命對象,但若是你反覆重建,Dart 也會耗盡你的電池——特別是涉及動畫的地方。通常來講,咱們應該將重建限制在實際改變的子樹上。

你有沒有抓住這個矛盾?組件樹是用戶界面的不可變描述。如何在不從根重構的狀況下重建其中的一部分?實際上,組件樹不是具備從父組件到子組件,從根到葉的引用的物化樹結構。特別是 StatelessWidgetStatefulWidget,它們沒有子引用。他們提供的是 build 方法(在有狀態的狀況下,經過相關的 State 實例)。Flutter 框架遞歸地調用那些 build 方法,同時生成或更新實際的運行時樹結構,不是組件,而是引用組件的 Element 實例。元素樹是可變的,並由 Flutter 框架管理。

那麼當你在 State 實例 s 上調用 setState 時會發生什麼?Flutter 框架標記了以 s 對應元素爲根的子樹,用於重建。當下一幀到期時,該子樹將根據 sbuild 方法返回的組件樹進行更新,然後者依賴於當前的應用程序狀態。

咱們對代碼的最終嘗試提取了一個有狀態的 LakeStars 組件,將重建限制在一個很是小的子樹中。而 MyApp又變回無狀態。

import 'package:flutter/material.dart';
import 'src/lake.dart';
import 'src/widgets.dart';

void main() {
  // 僞裝咱們從業務邏輯中獲取 Lake 實例。
  final Lake lake = Lake();
  runApp(MyApp(lake));
}

class MyApp extends StatelessWidget {
  const MyApp(this.lake);

  final Lake lake;

  @override
  Widget build(BuildContext context) {
    final Widget imageSection = ...
    final Widget titles = ...
    final Widget titleSection = Container(
      padding: const EdgeInsets.all(32.0),
      child: Row(
        children: <Widget>[
          Expanded(child: titles),
          LakeStars(lake: lake),
        ],
      ),
    );
    final Widget actionSection = ...
    final Widget textSection = ...
    final Widget scaffold = ...
    return MaterialApp(
      title: 'Flutter Demo',
      home: scaffold,
    );
  }
}
複製代碼
import 'package:flutter/material.dart';
import 'lake.dart';

class LakeStars extends StatefulWidget {
  LakeStars({Key key, this.lake}) : super(key: key);

  final Lake lake;

  @override
  State createState() => LakeStarsState();
}

class LakeStarsState extends State<LakeStars> {
  @override
  Widget build(BuildContext context) {
    final Lake lake = widget.lake;
    return GestureDetector(
      child: Row(
        children: <Widget>[
          Icon(
            lake.isStarred ? Icons.star : Icons.star_border,
            color: Colors.red[500],
          ),
          Text('${lake.starCount}'),
        ],
      ),
      onTap: () {
        setState(() {
          lake.toggleStarring();
        });
      },
    );
  }
}

class Action extends StatelessWidget { ... }
複製代碼

將一個廣泛適用的 Stars 組件與 Lake 概念分離開彷佛是正確的,但我把它做爲給讀者的練習。

在成功將視圖邏輯添加到代碼中以後,嵌套深度仍然是便於管理的,我認爲咱們已經對深度嵌套的挑戰作出了合理的解決。


咱們能夠設想幾個有趣的技術解決方案,來解決在嵌套的組件樹中 Flutter 視圖邏輯丟失的問題。其中一些可能須要修改 Flutter 框架、IDE 和其餘工具,甚至可能須要修改 Dart 的語法。

不過,你如今已經能夠作一些很強大的事情了,只需將問題的緣由——代碼編寫 UI、組件的組合和響應式編程——轉變爲你的優點。擺脫深度嵌套的語法只是邁向可讀、可維護和高效的移動應用代碼之旅的開始。

開心地使用 Flutter 吧!

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索