Flutter 全棧開發體驗——爬蟲與服務端

程式設計之路從0到1發表於2019-06-15

在學習或開發Flutter應用時,很多人會在app中硬編碼很多假資料,用以除錯介面,實際上我認為是完全沒有必要的,Flutter使用Dart語言程式設計,而Dart語言作為一種全棧語言,其語法可以甩JavaScript幾條街,我們是很有必要真正的將這種語言的能力發揮出來的。

這裡我就講講如何使用Dart語言編寫爬蟲獲取資料,如何使用Dart語言編寫編寫簡單伺服器後端。

Dart 爬蟲開發

首先我們花十分鐘來編寫一個簡易爬蟲。

環境準備

關於Dart 服務端SDK環境搭建,請閱讀我的另一篇文章 Dart語言——45分鐘快速入門(上)

我們完全手動建立一個Dart工程還是略顯麻煩,因此我們需要安裝一個腳手架,自動生成一個合乎規範的Dart工程專案,執行以下命令安裝stagehand

pub global activate stagehand
複製程式碼

完成安裝後直接使用stagehand命令:stagehand -h可能會報找不到錯誤,這時候我們有兩種辦法解決

  1. 配置環境變數

    開啟cmd命令列,輸入如下命令

    echo %APPDATA%\Pub\Cache\bin
    複製程式碼

    這時可以看到,命令列輸出了stagehand命令所在的路徑,只需要將該路徑加入到系統的Path環境變數即可

  2. 使用pub工具呼叫

    除了配置環境變數,還可以使用pub global run去呼叫,由於我本機配置了各種各樣的開發語言和工具,命令實在太多,我已經不太喜歡配置環境變數,這裡就先使用該方式演示。執行以下命令可以檢視一下幫助

    pub global run stagehand -h
    複製程式碼

建立工程

新建一個資料夾spidercd到該目錄下,執行以下命令,會在spider下自動生成一個命令列專案

pub global run stagehand console-full
複製程式碼

使用vscode開啟該專案目錄

編輯配置檔案pubspec.yaml,避免不必要的下載,刪除預設新增的test庫依賴,配置如下依賴庫

dependencies:
  http: ^0.12.0+2
  html: ^0.14.0+2
複製程式碼

這裡http庫主要用於處理http請求,html庫用於處理html內容的解析與提取,它們都是Dart官方提供的非標準庫,GitHub連結如下

在專案下執行命令,下載依賴

pub get
複製程式碼

本文主要做Demo演示,不會對爬蟲知識進行講解。這裡主要爬取了一個妹子圖網站,大家可以根據自己的實際需要選擇目標。如果對爬蟲不太瞭解,請查詢資料進行學習,也可以閱讀本人的CSDN部落格瞭解爬蟲,這裡預設大家都掌握爬蟲技術。

我的個人部落格

在這裡插入圖片描述
編輯專案中 lib/spider.dart檔案

import 'package:http/http.dart' as http;
import 'package:html/parser.dart' show parse;
import 'package:html/dom.dart';
import 'dart:convert';
import 'dart:io';

// 資料實體
class ItemEntity{
  final String title;
  final String imgUrl;

  ItemEntity({this.title,this.imgUrl});

  Map<String, dynamic> toJson(){
     return {
        'title': title,
        'imgUrl': imgUrl,
      };
   }
}

// 構造請求頭
var header = {
  'user-agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) '+
  'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36',
};

// 資料的請求
request_data() async{
  var url = "https://www.mzitu.com/";

  var response = await http.get(url,headers: header);
  if (response.statusCode == 200) {
    return response.body;
  } 
  return '<html>error! status:${response.statusCode}</html>';
}

// 資料的解析
html_parse() async{
  var html = await request_data();
  Document document = parse(html);
  // 這裡使用css選擇器語法提取資料
  List<Element> images = document.querySelectorAll('#pins > li > a > img');
  List<ItemEntity> data = [];
  if(images.isNotEmpty){
    data = List.generate(images.length, (i){
      return ItemEntity(
        title: images[i].attributes['alt'],
        imgUrl: images[i].attributes['data-original']);
    });
  }
  return data;
}

// 資料的儲存
void save_data() async{
  var data = await html_parse();

  var json_str = json.encode({'items':data});
  // 將json寫入檔案中
  await File('data.json').writeAsString(json_str,flush: true);
}
複製程式碼

編輯專案下的 bin/main.dart檔案

import 'package:spider/spider.dart' as spider;

main(List<String> arguments) {
  spider.save_data();
}
複製程式碼

從篇幅考慮。本文省略資料庫相關操作,用一個data.json檔案替代。以上程式碼中save_data函式即處理資料的持久化儲存工作,對於小型爬蟲而言,推薦使用Sqlite3作為資料庫,大型爬蟲推薦MongoDB資料庫,我個人認為,MongoDB是對爬蟲最親和的資料庫。

執行以上程式,即可生成data.json檔案

在這裡插入圖片描述

總結: 就我個人感覺,使用Dart語言寫爬蟲肯定是沒有Python順手高效的,Python在爬蟲這塊的工具過於強大、簡潔、高效。

Dart 服務端

使用Dart語言的原生API開發HTTP伺服器仍顯得過於繁瑣,因此我們需要一個HTTP伺服器框架,這樣我們就只需要關注業務邏輯的處理。如果大家使用過任何一款成熟的HTTP伺服器框架,那麼對於新框架上手就會易如反掌,因為絕大多數伺服器框架的概念都是相同的,主要就是ORM、路由對映、模板渲染、中介軟體等等這些東西。

根據我所知的,目前可用的仍在維護的Dart的HTTP伺服器框架主要有四個,依次按照star最多的從上到下來排序:

其中排第一的 aqueduct 是功能、文件、示例最完善的,因此我們就以此框架做演示

安裝

pub global activate aqueduct
複製程式碼

建立專案

執行命令,生成專案api_server

pub global run aqueduct create api_server
複製程式碼

最簡示例——hello world

其中bin/main.dart下的入口檔案可以不用修改,主要修改lib/channel.dart,刪除多餘註釋,程式碼如下

import 'package:api_server/controller.dart';

import 'api_server.dart';

class ApiServerChannel extends ApplicationChannel {
  @override
  Future prepare() async {
    logger.onRecord.listen((rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}"));
  }

  @override
  Controller get entryPoint {
    final router = Router();
    router
      .route("/")
      .linkFunction((request) async {
        return Response.ok('hello world!');
      });
    return router;
  }
}
複製程式碼

簡單說一下,這裡有兩個實現,其中prepare()方法一般用於預處理,例如連線資料庫等,我們暫時用不到,不需理會。entryPoint方法是我們真正需要關注的方法,它的執行在prepare()方法之後,當有請求到來時,就會被回撥。我們在該方法中註冊路由,這裡註冊一個根路徑,並設定一個響應請求的匿名回撥方法。當我們開啟瀏覽器訪問http://localhost:8888時,它返回一個響應,即向瀏覽器列印一句hello world!

cd到專案根路徑下,執行以下命令啟動服務

dart bin/main.dart
複製程式碼

在瀏覽器訪問http://localhost:8888,可以看輸出hello world!

實現後臺API服務

Router除了可以註冊回撥方法,還可以關聯一個Controller用於處理來自客戶端的請求。

在lib目錄下新建controller.dart檔案,自定義一個Controller。它需要繼承自框架的Controller類,並實現一個handle方法。

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:aqueduct/aqueduct.dart';

class ItemsController extends Controller {

  @override
  Future<RequestOrResponse> handle(Request request) async {
    final content = await File('asset/data.json').readAsString();
    return Response.ok(json.decode(content));
  }
}
複製程式碼

在專案根路徑下新建asset目錄,將我們之前爬取的資料檔案data.json拷貝進去。然後修改entryPoint方法,再註冊一個新的url

  @override
  Controller get entryPoint {
    final router = Router();
    router
      .route("/")
      .linkFunction((request) async {
        return Response.ok('hello world!');
      });

	// 註冊一個新的url,並關聯到我們自定義的Controller上
    router
    .route('/api/all')
    .link(() => ItemsController());

    return router;
  }
複製程式碼

重新啟動伺服器

dart bin/main.dart
複製程式碼

瀏覽器訪問http://localhost:8888/api/all,成功獲取資料

在這裡插入圖片描述

建立Flutter專案演示

Flutter環境準備這裡就省略了。先建立一個Flutter 工程用於演示

程式碼結構如下

在這裡插入圖片描述
這裡主要是三個檔案list_dao.dartitem_model.dartmain.dart

首先配置依賴檔案pubspec.yaml,主要用到了兩個庫dioflutter_staggered_grid_view

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2
  dio: 2.1.4
  flutter_staggered_grid_view: "^0.2.7"
複製程式碼

下載依賴完成,編輯以下檔案 list_dao.dart

import 'package:flutter_demo/model/item_model.dart';
import 'package:dio/dio.dart';

class ListDao {
  //這裡配置自己的實際域名或IP地址
  static const Host = 'http://192.168.1.102:8888';
  
  static const header = {
    'User-Agent':
        'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0',
    "Referer": "https://www.mzitu.com"
  };

  static Future<ItemModel> fetch() async {
    try {
      Response response = await Dio().get("$Host/api/all");

      if (response.statusCode == 200) {
        return ItemModel.fromJson(response.data);
      } else {
        throw Exception("StatusCode: ${response.statusCode}");
      }
    } catch (e) {
      print(e);
      return null;
    }
  }
}
複製程式碼

實體類item_model.dart

class ItemModel {
  List<Items> items;

  ItemModel({this.items});

  ItemModel.fromJson(Map<String, dynamic> json) {
    if (json['items'] != null) {
      items = new List<Items>();
      json['items'].forEach((v) {
        items.add(new Items.fromJson(v));
      });
    }
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.items != null) {
      data['items'] = this.items.map((v) => v.toJson()).toList();
    }
    return data;
  }
}

class Items {
  String title;
  String imgUrl;

  Items({this.title, this.imgUrl});

  Items.fromJson(Map<String, dynamic> json) {
    title = json['title'];
    imgUrl = json['imgUrl'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['title'] = this.title;
    data['imgUrl'] = this.imgUrl;
    return data;
  }
}
複製程式碼

最後就是實際的UI程式碼了

import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'dao/list_dao.dart';
import 'model/item_model.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.pink,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Future<ItemModel> mFuture;

  @override
  void initState() {
    loadData();
    super.initState();
  }

  loadData() {
    mFuture = ListDao.fetch();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("美女圖"),
      ),
      body: FutureBuilder(
          future: mFuture,
          builder: (ctx, snapshot) {
            switch (snapshot.connectionState) {
              case ConnectionState.none:
              case ConnectionState.active:
              case ConnectionState.waiting:
                return Center(child: CircularProgressIndicator());
              case ConnectionState.done:
                if (snapshot.hasError)
                  return Center(child: Text('Error: ${snapshot.error}'));
                return _buildList(snapshot.data);
            }
            return null;
          }),
    );
  }

  // 建立GridView
  Widget _buildList(ItemModel data) {
    return Container(
      color: Color(0xfff5f6f7),
      padding: EdgeInsets.only(top: 12, left: 10, right: 10),
      child: StaggeredGridView.countBuilder(
        primary: false,
        crossAxisCount: 4,
        itemCount: data?.items == null ? 0 : data.items.length,
        itemBuilder: (ctx, i) {
          return Container(
            decoration: BoxDecoration(
                color: Colors.white, borderRadius: BorderRadius.circular(8)),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                ClipRRect(		// 處理圓角圖片
                    borderRadius: BorderRadius.only(
                        topLeft: Radius.circular(8),
                        topRight: Radius.circular(8)),
                    child: Image.network(data.items[i].imgUrl,
                        headers: ListDao.header)),
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Text(
                    data.items[i].title,
                    style: TextStyle(fontSize: 16),
                  ),
                )
              ],
            ),
          );
        },
        staggeredTileBuilder: (int index) => StaggeredTile.fit(2),
        mainAxisSpacing: 10.0,
        crossAxisSpacing: 8.0,
      ),
    );
  }
}
複製程式碼

確認我們之前寫的伺服器後端已經啟動,然後啟動本機模擬器,執行起Flutter App

示例動態

總結

我認為使用Dart語言開發服務端,並結合Nginx用於生成環境下,使Flutter開發人員真正承包整個專案的業務邏輯是非常可行的,這條路才是真正的全棧之路!Dart的語法優勢是勝過JavaScript的,即使Java與之相比也顯得冗餘臃腫。至於是否好用,就待大家自行體會了。

關於Dart的服務端框架aqueduct,大家有興趣可以檢視官方文件深入學習,本文主要省略了資料庫方面的處理,實際上資料庫是獨立的知識內容,與框架的關係不是太大。而且該框架也提供了一個ORM模組,大家直接檢視文件學習 Aqueduct ORM ,但目前似乎只支援PostgreSQL資料庫,如果想要使用其他資料庫,安裝相應的驅動,連結如下

我的個人部落格

歡迎關注我的公眾號:程式設計之路從0到1

程式設計之路從0到1

相關文章