扣丁書屋

不用掉一根頭發!用 Flutter + Dart 快速構建一款絕美移動 App

如今這個時代,與前端或移動相關的新框架層出不窮。所有從事Web開發的人都應該熟悉各種目不暇接的新方法以及針對復雜問題的輕量級解決方案。我們不再因為沒有現成的技術而煩惱,相反我們常常因為不知道該選哪種技術而感到頭疼。

最近,我偶然間發現了Flutter,于是興致勃勃地決心一試,看看這種技術是否能夠成為強有力的競爭對手,或者甚至作為一種解決方案,讓我在進退兩難的困境中看到新的希望。

Flutter簡介

Flutter是Google出品的移動應用UI SDK。它使用了Dart VM(也是Google出品,專門針對UI進行了優化),幫助我們開發移動設備和臺式設備。Dart本身也可用于Web開發,甚至可以與我們非常熟悉的Angular框架配合使用。

Flutter可以通過AoT(提前)編譯方式編譯成原生機器代碼,目的是讓應用的運行速度達到最高,同時又不會產生太多開銷。

對于開發人員,Flutter提供了JIT(即時)編譯器和熱重載功能,我們能夠在不丟失現有狀態的情況下修改應用程序,這點非常實用,因為在復雜的功能中修改一個隱藏得很深的UI非常麻煩,所有曾經從事UI工作的人都清楚每次都要千辛萬苦才能找到要修改的UI。

當然,SDK重要的部分是控制庫。由于Flutter的定位是Android和iOS開發,因此我們可以選擇使用Material(Google Android)或Cupertino(Apple iOS)控件集。這是否意味著當應用程序部署到Android或iOS手機上時會切換外觀,讓兩者看起來都很像是原生的?并非如此。你可以隨便使用哪個庫,也可以同時使用兩者,但是并沒有統一的切換UI的功能。當然,你可以手動實現,我并不是說不建議這種做法。但請記住這種功能需要管理兩組不同的布局控件,很快就會亂成一鍋粥,因此應當謹慎地采用這種方法。

在默認情況下,Flutter中的所有內容都是小部件(widget)。如果你有使用Angular 2+的經驗,那么可以認為wdiget就是更強大的組件,而且應該是一個非常熟悉的概念。在默認情況下,這種基本類型包含一個定義外觀的build方法,而且還可以根據傳遞的參數和上下文自定義外觀。小部件可以是無狀態的也可以是有狀態的。無狀態小部件大部分都是靜態的,不會在生命周期中發生任何明顯的變化。另一方面,有狀態的小部件在每次觸發時都會被構建(例如,當監視的變量發生變化、用戶執行單擊等特定的操作)。

Flutter是響應式編程(類似于React),這意味著沒有默認的持續刷新循環(像Angular那樣)。取而代之的是,一旦執行了關鍵操作,UI或其一部分(比如其中一個小部件)就會根據狀態的變化重新繪制。

我曾提過,Dart為處理UI進行了大幅優化以,但這意味著什么?在Flutter中,經過優化后的Dart支持豐富的集合處理、基于隔離的并發以及future的async-await?;旧线@種SDK面向的應用程序都是構建業務,而不是游戲。盡管我們不能假設人們不會嘗試使用Flutter制作游戲,甚至現在已經出現了2D游戲引擎。但我想說的是,這種應用程序的模式似乎非常適合這組特定的功能,而這也是我決定探索的角度。

風險

盡管上述一切聽起來很美好,但也有一些弊端。

首先,Flutter仍然是一個處于起步階段的SDK。雖然從它的年齡上來看屬于正常,但應該注意的是,alpha版于2017年5月發布,而1.0版本到于2018年12月才發布。這意味著在撰寫本文之際,Flutter僅有一年的歷史。這有什么后果?Flutter的社區雖然已具規模,但仍不能與當前的主流技術相提并論。這會影響我們尋找一些常見問題的解決方案,而且可能會經歷多次失敗,需要付出額外的努力,并仔細閱讀規范。但是,Flutter的文檔很健全,并且社區在不斷發展,因此我們可以認為Flutter在發展中,沒有明顯的缺陷。

其次,Flutter和Dart都來自Google(這既可以看作缺點,也可以看作是優點)。好的方面是Google是科技巨頭,如果他們想維護,那么資源和人力都很充足。但缺點是,雖然眾所周知Google會推出非常實用的技術和服務,但也有可能隨時將其淘汰出局。Flutter也面臨這樣的風險,但近期內不太可能會發生,甚至在未來幾年也不會。因此,雖然這是一種風險,但是再說一次,任何新技術都有同樣的風險,而且每種技術都有這樣的經歷。

使用哪些工具?

我們可以使用最常見的編程IDE(Android Studi、IntelliJ IDEA,甚至還有支持Flutter的Visual Studio Code插件)來開發Flutter,這意味著大多數開發人員都不必離開熟知的環境。就我而言,最近我一直在從事面向Web的工作,所以我選擇了VS Code,但這不會對開發造成任何影響,因為文本文件說到底仍然只是文本文件。我選擇的平臺是Android(選擇這個平臺的原因是因為我既沒有iPhone、MacBook,也沒有iMac),因此看起來無論如何我都會安裝Android Studio,因為Android Studio提供了虛擬機。

除了IDE之外,還有Flutter/Dart DevTools,這是一個套件,用于監視應用程序的性能,此外還有一些調試工具,例如Flutter查看器,類似于WebTools。在調查應用程序的性能瓶頸時,實時資源監控器非常實用,還有層級查看器可以找出困擾著許多應用程序和網站UI的冗余嵌套。

入門:“Hello World”

下面我們來編寫一個管理保單的移動應用程序,還有比這這更令人興奮的事兒嗎?請務必注意,我可能會嘗試以不同的方式來開發有些功能,所以可能會導致代碼不一致。這個應用程序包含了一些解決常見問題的想法和示例,孰優孰劣留給個人評判。

應用的簡單概述:

  • “主頁”畫面顯示已購買的保單摘要
  • 創建一個保單
  • 通過向導完成保單創建
  • 保單可以是不同的類型
  • 保險主題可以是不同的類型
  • 用戶需要賬號(不能匿名使用)
  • 該應用程序是一個“輕量級客戶端”——所有字典、數據和操作都存儲在服務器端
  • 請求/響應主體格式為JSON

我們通過Mockoon模擬API,IDE的話我選用VS Code,設備則由Android模擬器提供(我選擇了Nexus 6 API 28)。首先,我根據Flutter官方網站上提供的官方指南創建了一個空白應用,隨后創建Flutter項目的準系統。就我個人而言,我創建了如圖1所示的結構。完整的應用代碼請點擊這里(https://github.com/asc-lab/personal-insurance-flutter-poc),我建議你參照著本文一起看。

[圖1] 初始階段的項目

查看項目的基礎

pubspec.yaml文件包含了項目的依賴項、資源文件和版本號,非常簡單明了。此外,該文檔還包含了許多說明,但我們不會修改這些說明,至少不會經常都修改。對我們來說最重要的是lib文件夾,因為其中保存了應用程序的起始文件main.dart文件,以及其中的main()方法。這是應用程序的入口點,任何代碼都不應超出該點。好了,下面該搭腳手架了。

主頁是應用程序的默認頁面,也是默認的路由。我們將在主頁展示一系列的保單。因此,通過我們的api獲取字典肯定很合適。我構建了一個調用API服務的單例服務,并在應用程序啟動之前就獲取字典數據,這樣在應用程序的任何位置都可以使用這些數據。這個字典名叫CommonData,而字典的API服務是DictionariesService。二者都位于lib/services文件夾中。我還添加了一個通用的幫助服務(名叫Helper),提供默認填充、常用轉換等功能。

[圖2] CommonData

CommonData是一個單例,它有一個內部構造函數,該構造函數將其唯一的實例存儲在自己的某個靜態字段中。CommonData類定義不會在應用程序的其他任何地方使用,僅在這個文件中用于聲明commonData實例。DictionariesService.get()方法會返回Future,它實質上實是一個promise。這意味著我們可以使用await等它返回結果,并在一切準備就緒后繼續執行initialize(),或者使用.then(…)并盡早返回。我們希望initialize()在收到響應后完成,因此我們使用await。稍后我們將介紹DictionaryService.get()的實現。

經過一番研究后,我發現在繪制UI之前運行commonData.initialize()非常簡單,因此我們將其放到main()中(如圖3所示)。

[圖3] 在運行應用之前初始化commonData

這樣一來,無論我們在應用任何位置,都可以確信commonData已被初始化,因為應用本身都是在initialize()完成后執行的。這樣的解決方案在許多情況下都很管用,例如服務器存儲的應用程序配置文件或主題、數據暫存、應用程序設置等。對于異步操作,我們應該在主頁畫面上進行處理,因為我們可以在主頁畫面上顯示加載進度條。這樣可以避免在應用啟動時用戶看到空白的屏幕,然后懷疑應用程序是不是崩潰了。因此,如果我們必須在應用程序正常啟動之前做點什么,那么最好是執行可預測、可忽略執行時間的操作,或者創建一個單獨的“加載”畫面,并顯示一些動畫和明確的“加載”消息,讓用戶放心地等待操作執行,并在完成操作后返回主頁。這種“尷尬的預加載”就留在這里作為UX的反面教材吧。

下面,讓我們來看看main()下方的MyApp類。它的主體主要是重寫build(BuildContext)方法,該方法會在每次重繪MyApp小部件時調用。我們的應用有多個畫面:主頁和創建保單向導的5個步驟(保單類型、產品、覆蓋范圍、投保人和投保對象),因此我對相關主題進行了仔細研究(如圖4所示)。

[圖4] Flutter應用程序的導航研究

在Flutter中,導航稱為“路由”。我已經根據教程創建了一些路由(如圖5所示)。默認的初始路由(MyHomePage小部件)和五個向導步驟。我們是否需要訪問構建上下文尚有待觀察,但先放在這里總沒有壞處。

[圖5] Flutter應用中基本的路由

Material Design中的Flutter

值得一提的是,由于我們的應用使用了Material控件集,并且是MaterialApp實例,因此我們可以按照Material Design原則快速修改外觀。ThemeData類包含了“material design主題的顏色和版式數據”。在應用程序中可以通過靜態方法Theme.of(BuildContext)訪問ThemeData,改變它的各種屬性,以更改主題提供的默認值?,F在,我們只需設置primarySwatch(應用程序的主色調及各種明度的顏色)和accentColor(也是各種明度的顏色組合,是應用的輔助色)。

如果我們使用主題的默認值和/或生成的值(我們應盡力做到這一點),則最終的UI看上去應該不會太差。如果我們不想使用默認的顏色,則可以自定義顏色(如圖6所示)。不過,這需要進行大量的工作(除非客戶提供了樣式指導),而且我不想破壞審美,因此就按照簡單的方法來吧。網上有無數的材質色樣生成器,如果你想提供“基本”的色調,則可以生成一個。此外,還有一個errorColor設置,但是作為一個涉獵UI/UX領域多年的人,我建議你謹慎使用這個設置,因為標準的紅色是錯誤指示的行業標準。即便顏色方案允許修改,也應該盡量避免,最多只是稍微修改一下明度即可。

這個過程也可以用來測試“熱重載”:嘗試更改主題顏色,保存,然后就能立即看到應用的變化。這個功能我非常滿意。

[圖6] 自定義顏色的示例

主頁

主頁基本上就是一個列表,里面展示了每個保單,每個列表項可以展開顯示保單的詳細信息,另外還有一個創建新保單的選項。因此,每個列表項應該是有狀態的,因為列表項的外觀會發生變化大,但是整個頁面可以是無狀態的。主頁顯示了一個可變長度的列表,但是在其生命周期內,其中的元素和值不會發生變化。請注意,如果我們沒有將每個列表項分成獨立的小部件(而是在一個類中處理所有內容),那么頁面就必須是有狀態的。

[圖7] MyHomePage 數據初始化

讓我們從路由(圖7)中的數據開始。每次導航到’/’時都會執行此處的邏輯。在這種情況下,這種做法很方便,因為每次我們顯示主頁畫面時,都會有最新的用戶賬號數據和已創建的保單。這樣一來,我們就解決了將來會遇到的問題:完成導向后如何刷新主頁面。我們只需要導航回去即可。

在MyHomePage(homepage.dart)內,你可以看到一些UI定義。頁面的根名稱為Scaffold,它負責設置應用程序欄、操作按鈕、文檔主體和其他各種選項,實際上這就是通用的移動應用模板。未定義的部分會被省略該。這里的appBar是最小設置,有一個floatActionButton來啟動新的保單向導,backgroundColor已與當前主題的背景色掛鉤(如果我們想改變顏色,則需要保持一致性),當然還有最主要的主體。

如前所述,這些保單被包裝在一個Future中,表示它們還不能傳遞給ListView。這就是FutureBuilder<>的用途:實際上,它是一個小部件,可以根據Future的內部狀態返回內容。我們可以使用快照(AsyncSnapshot)變量,根據Future是否已完成或仍在進行中,或者是否包含錯誤等,返回不同的窗口小部件。

對于我們來說,如果已完成則返回一個ListView,否則返回一個加載指示器——非常標準的東西。最好將所有可能的錯誤處理包裝到Helper類中的某個通用方法中,Helper類可以接受snapshot.connectionState并輸出一些通用的錯誤信息。解決Future中的錯誤有很多方法,但這里為了簡潔起見,我沒有使用這些方法。因此,一個Future只能是已完成或正在加載兩種狀態之一。

[圖8] FutureBuilder

再來看看HomepageTile小部件。這是我們的第一個有狀態的UI。每個有狀態的小部件都包含小部件聲明(圖9)及其狀態,而狀態才是最神奇的部分。

[圖9] 有狀態的小部件

小部件的UI在狀態中通過build方法定義。因此,每次調用setState(fn),框架都會重新構建,并使用新的屬性值重新執行build(BuildContext)方法。此處,我使用了_expanded字段值作為條件,來決定應該返回_buildMiniTile()還是更詳細的_buildMaxiTile()小部件。當然,這可能只是一個簡單的條件賦值問題,但是我們可以利用AnimatedCrossFade小部件來美化。

它的功能正如其名:根據crossFadeState(圖10)讓兩個子部件交叉淡入淡出。由于每次setState調用都會重新構建小部件,因此在兩個以上的狀態之間進行切換也是可能的,但這種用法非常罕見,因為通過特定數量的點擊進入某個狀態聽起來有點像在戲弄用戶或玩捉迷藏游戲。除非有強烈的視覺暗示,否則不要這樣做。

[圖10] AnimatedCrossFade

現在我們知道了怎樣創建主頁,并利用通用的列表項來顯示用戶的保單。接下來演示一下怎樣給應用程序提供Mockoon API中的數據。我們打開DictionariesService(圖11)。

[圖11] DictionariesService

可以看到,get()函數標記為async,意思是它的返回值會包裹在Future<>中,采用類似于promise的處理方式。http客戶端會異步執行命令,然后提供響應結果、狀態碼等。緊接著我們將JSON(其類型默認為Map<String, dynamic>)映射為DTO對象。由于這些是字典,所以我為它們創建了map,這樣就不需要在顯示某個代碼對應的名稱時遍歷所有元素了(只需這樣寫即可:commonData.maps[DictCode.PRODUCT_TYPE][_policy.type])。

接下來看看DTO。將json轉成對象并沒有公認的方法,但幸運的是我們可以利用很多插件。我這里使用了json_annotation(https://pub.dev/packages/json_annotation),用它來監視啟動后(flutter packages pub run build_runner watch)就會尋找 @JsonSerializable標記并創建映射函數,如圖12~13所示。

[圖12] policy.dart - DTO類

[圖13] policy.g.dart

  • 由json_annotation生成 這可以大幅簡化工作,并提供非常方便的方式將類映射到JSON。

向導

每個成功的商業應用必不可少的兩部分就是表單和驗證。我們來看看我們要做的功能,以及保單向導的代碼。前兩步(1_newPolicyType,2_newPolicyProduct)都是隨處可見的、非常標準的東西,這里就不再贅述了。如果你想看看如何利用異步執行計算來填充表單,那么可以查看3_newPolicyCovers步驟,它包含了一個假的保費計算的實現。

app表單

表單的定義非常標準——首先定義一個Form對象,在預先生成的GlobalKey鍵中進行處理,然后定義元素,如4_newPolicyYou.dart文件和圖14所示。

[圖14] 4_newPolicyYou.dart——非常直觀的表單定義。注意這里使用了Helper來減少代碼。

表單可以通過多種方式與數據交互,所以可以按照開發者的喜好來設計。如果需要偽雙向綁定行為,可以將onChange處理函數中的值持久化到setState中。但是也可以僅使用onSaved,在表單完整之后再持久化數據。我決定采用后一種方法。Step4Builder類(圖15)中包含了向導的序列——如果表單合法,則保存后繼續。向表單中注入數據則采用了很簡單的方法:由于我們從模型傳遞值給表單(processData)中相應控件的initialValue,因此每次setState操作的時候控件都會被更新。

這就是為何我們只需要填充模型的字段(processData.setOwnerFromAccount),然后使用this._formKey..currentState.reset重置表單即可,這樣就可以重新計算字段的初始值——直接從模型中獲取值。但是為什么要重置表單呢?因為這樣可以保證,只要我們不持久化表單中的值,我們在setOwnerFromAccount中沒有填充的字段就可以獲得默認值(這些默認值也存在于模型中)。

這只是策略之一。在不同的情況下我們可能會選擇其他方法,但要注意的是,我們并不需要一定采用某種方法。

[圖15] Step4Builder

  • 如果表單合法,則保存并轉向下一步。非常干凈。 動態表單布局的實現跟傳統的js/html沒什么太大區別。在向導最后一步的5_newPolicySubject.dart中,我們應該創建保單投保對象的數據,因此需要根據數據類型(汽車、人或者蜥蜴等)來采用不同的表單。實現方法是在不同的小窗體中定義不同的字段集合,然后根據前一步的選擇來顯示合適的那個。應用程序中僅實現了一個類型(reptileObject.dart),但只需在build方法中檢查一下就很容易實現添加其他的類型(圖16)。

[圖16] 5_newPolicySubject.dart:我只想為我的寵物蜥蜴投保,因此唯一的表單定義就是Reptile對象,但我們當然可以通過在子屬性中插入if語句來顯示正確的表單。

現在,我們有文本框和下拉菜單,下一步該編寫日期控件了——其實控件并不存在。如果你開發過移動應用,也許這聽上去有些奇怪,但是如果仔細考慮一下就會發現這完全合理。最好的方案永遠是使用系統提供的input(例如,我們不需要定義鍵盤控件,只需要使用系統提供的即可),而每個移動系統都提供了自己的日期控件,一般表現為日歷的形式。因此我們的“日期輸入”僅僅是一個只讀的TextFormField,當我們觸摸該控件時,它會要求系統提供值。前面提到的reptileObject.dart文件就包含了一個例子(圖17-18)。

圖17-18:reptileObject.dart - TextFormField的責任非常少,只需要告訴系統用戶需要輸入一個日期,然后顯示該動作的結果即可。我們定義了一個onTap處理函數,來攔截針對控件的交互,然后顯示系統的datepicker。由于這是一個異步的動作(用戶可以花很長時間來選擇日期),因此整個方法必須標記為異步。

驗證——使用errorColor

現在表單已經完成了,我們需要提供一些基本的數據驗證。規則非常簡單:每個表單控件有一個“validator”屬性,它接受一個函數,函數的輸入就是值,輸出是一個字符串。如果輸出非空,則輸出的內容就表示驗證錯誤消息,顯示在適當的區域。圖19演示了一個組合驗證(兩個條件、兩條消息)的簡單示例。

圖19:簡單的驗證 - 如果Validations.required返回錯誤消息,則返回該消息。否則檢查輸入是否為有效的郵件地址。如果不是,則返回自定義的錯誤消息。

到這里一切都很順利,但如果我們需要進行異步驗證(比如檢查用戶名是否已存在)該怎么辦?嗯……很難。Flutter不支持在驗證中使用Future<>,而且應該永遠不會支持,據說這樣會破壞同步驗證,而且由于這些原因(https://github.com/flutter/flutter/issues/9688)混合兩種驗證方式并不是很好的UI實踐。

即使接受這個現實,我們也會遇到必須進行服務器端驗證的情況,那么唯一的選擇就是將海量的數據加載到設備上。不過幸運的是,有一個廣為人知的非常簡單的技巧。只需在驗證器中執行調用然后切換一個局部標志。如果標志被設置,則不顯示任何驗證信息。當驗證結束后將驗證結果保存到某個局部變量中,然后切換該標志,然后手動觸發表單的驗證。這樣,第一次驗證觸發時不會顯示任何信息(或者可以顯示“請稍候……”表示動作正在執行),第二次驗證將驗證信息改成動作的結果(需要覆蓋“請稍候……”)。

因此,盡管我們可以這樣進行異步驗證,但還是希望SDK能提供支持。這樣可行,但應該更干凈一些。

不管如何,現在應用程序可以運行了,而且開發這個程序根本沒有花太多時間。我們考慮了實現商業應用的絕大多數基本問題,而且并沒有什么太難的地方。所以可以認為這個應用程序是成功的。我們現在可以去掉那個反面教材,清理下代碼,與后臺結合,然后在收到客戶反饋后重新修改。

我們來看一看這個應用程序:

[圖] 政策主題頁面

最后的感想

那么,我們應該使用Flutter來開發移動應用嗎?我認為需要考慮幾個問題才能做出判斷,不同的人可能會得出不同結果。

如果你是第一次開發此類移動應用,我會推薦你使用。Flutter的學習曲線非常平緩,也不需要任何前提知識。通過教程和各種文檔可以很容易地判斷哪些場景下應該使用什么,而采用的工具完全可以自行決定。在學習一個存在了許多年的框架時,一些太過明顯的實踐人們就不會再談起,導致這些實踐很難學到。由于

Flutter相對比較新,因此沒有什么顯而易見的問題,因此也沒有那些被埋在各種新功能下的人盡皆知的技巧。相反,對于經驗豐富的移動開發者,對待Flutter的態度應該與其他新技術一樣。在創建有很多功能的高級應用時,如果你對某個技術有經驗,那么應用程序越大,該技術的優勢就越大。但是,如果你要開發一個很小的應用,那么Flutter是快速開發中的無價之寶。 Flutter的社區依然在成長。社區還不是很大,但也不是太小。關于這一點大家的意見可能不一樣,但我認為當前的社區大小已經足夠支持小型到中型的開發。用戶基礎越大,邊緣情況就被研究得越透徹,也就越容易找到幫助,所以只要社區依然在穩健成長,對大型項目的支持也會越來越完備,風險也會越來越小。

現在有許多Flutter開發的應用,因此已經不是小眾框架了。從官方網站上可以看到,不僅Google在用,許多大牌公司也在用。這表明Flutter的技術支持計劃在向好的方向發展,因此值得一試??紤]到該技術依然很新,因此這些公司很可能需要在推出應用程序之前進行一些研究,但研究之后依然選擇了Flutter,所以證明Flutter可能已沒有太大風險。只要有足夠的時間,Flutter應該能夠成為開發移動應用的首選。

眾所周知,市場變化很快,但這并不能阻止我們探索新事物。而且畢竟看來Flutter值得我們去嘗試。


https://mp.weixin.qq.com/s/iuE96YQh3ZlPRLcHXfXWuQ

最多閱讀

如何有效定位Flutter內存問題? 1年以前  |  11676次閱讀
Flutter的手勢GestureDetector分析詳解 3年以前  |  7605次閱讀
Flutter插件詳解及其發布插件 2年以前  |  6293次閱讀
在Flutter中添加資源和圖片 3年以前  |  5144次閱讀
Flutter 狀態管理指南之 Provider 3年以前  |  4341次閱讀
發布Flutter開發的iOS程序 3年以前  |  4317次閱讀
Flutter for Web詳細介紹 3年以前  |  4190次閱讀
在Flutter中發起HTTP網絡請求 3年以前  |  3953次閱讀
使用Inspector檢查用戶界面 3年以前  |  3885次閱讀
Flutter Widget框架概述 3年以前  |  3261次閱讀
Flutter路由詳解 3年以前  |  3103次閱讀
JSON和序列化 3年以前  |  3026次閱讀
Flutter框架概覽 3年以前  |  2990次閱讀
推薦5個Flutter重磅開源項目! 1年以前  |  2951次閱讀
為Flutter應用程序添加交互 3年以前  |  2941次閱讀
處理文本輸入 3年以前  |  2844次閱讀
使用自定義字體 3年以前  |  2843次閱讀
編寫國際化Flutter App 3年以前  |  2831次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>