阅读 1338

Flutter 全栈开发体验——爬虫与服务端

在学习或开发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

关注下面的标签,发现更多相似文章
评论