最近 2019 的 google io 大会开始了,之前的”蜂鸟”引擎也在 flutter 官网中出现了, 不过这次改了个名字叫 flutter-web

具体的使用步骤参考项目 readme 中的方式来使用

构建项目

建议: 配置dart,pub,~/.pub-cache/bin到环境变量

配置 webdev

git clone https://github.com/flutter/flutter_web.git
cd flutter_web/examples/hello_world/
flutter packages upgrade
flutter packages pub global activate webdev

运行项目

简单运行

运行

webdev serve

20190508102705.png

提示我们,在本地 8080 端口, 在浏览器打开 http://localhost:8080

默认的 main.dart 比较简单,只有一个 Text 控件

我这里修改一下 main.dart 文件,达到接近 flutter 移动项目 main.dart 的样子

// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter_web/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int counter = 0;
  TextEditingController controller = TextEditingController();

  void add() {
    counter++;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        child: Column(
          children: <Widget>[
            // TextField(
            //   controller: controller,
            // ),
            Text(counter.toString()),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: add,
        tooltip: 'push',
        child: Icon(Icons.add),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    print("${this.runtimeType} initState");
  }

  @override
  void dispose() {
    print("${this.runtimeType} dispose");
    super.dispose();
  }
}

20190508102830.png

这里看到了第一个问题, 图标没有显示

测试交互

然后简单试一下页面的交互

遇到了第二个问题 Kapture 2019-05-08 at 10.30.56.gif

文字无法选中, 这个可以理解,因为是自绘引擎, 和网页不一样,文字无法选中是正常的

文本输入

试一下文本输入

修改文件的 state 部分


class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int counter = 0;
  TextEditingController controller = TextEditingController();
  var key = GlobalKey();
  void add() {
    counter++;
    setState(() {});
    ScaffoldState state = key.currentState;
    state.showSnackBar(
      SnackBar(
        content: Text(controller.text),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: key,
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        child: Column(
          children: <Widget>[
            TextField(
              controller: controller,
            ),
            Text(counter.toString()),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: add,
        tooltip: 'push',
        child: Icon(Icons.add),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    print("${this.runtimeType} initState");
  }

  @override
  void dispose() {
    print("${this.runtimeType} dispose");
    super.dispose();
  }
}

加入了一个 TextField 控件,然后输入文本,接着将文本显示到 snackbar 中,接着点击按钮得到以下的样式

20190508103820.png

文本的输入等功能基本能实现

嗯,中文输入可用,直接用的是系统的输入法,不过输入框没有跟随

20190508104258.png

长按输入框位置无效, 双击可以看到 tooltip 的提示 20190508103938.png

拖动可以部分选择,但部分选择时的弹框没有出现 20190508104057.png

在 tooltip 显示的情况下拖动可以选择部分文本

另外测试了一下按钮的功能 copy paste 都无效,暂时没有和 macOS 系统的剪切板关联,其他系统的没测试,未知

使用系统的复制粘贴全选快捷键(cmd+c, cmd+v, cma+a)是可用的

图片

网络图片

简单截取一个图片,准备用于项目中,嗯,就是 google io 的演讲视频

20190508104658.png

20190508104824.png

可以看到图片,能够正常显示

目前为止的代码如下

// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter_web/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int counter = 0;
  TextEditingController controller = TextEditingController();
  var key = GlobalKey();
  void add() {
    counter++;
    setState(() {});
    ScaffoldState state = key.currentState;
    state.showSnackBar(
      SnackBar(
        content: Text(controller.text),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: key,
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        child: Column(
          children: <Widget>[
            TextField(
              controller: controller,
            ),
            Text(
              counter.toString(),
            ),
            Image.network(
                "https://raw.githubusercontent.com/kikt-blog/image/master/img/20190508104658.png"),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: add,
        tooltip: 'push',
        child: Icon(Icons.add),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    print("${this.runtimeType} initState");
  }

  @override
  void dispose() {
    print("${this.runtimeType} dispose");
    super.dispose();
  }
}

本地资源文件

结论: 使用 Image.asset 失败了,没有图片显示 经群中大佬解说,可以显示

目前使用约定式目录结构, 和桌面引擎的方式一致

必须放入web/assets目录下,不用在 pubspec 中声明

目录结构如下:

web
├── assets
│   └── images
│       └── 20190508104658.png
├── index.html
└── main.dart

插入控件

 Image.asset(R.IMG_20190508104658_PNG),
/// generate by resouce_generator library, shouldn't edit.
class R {
  /// ![preview](file:///private/tmp/flutter_web/examples/hello_world/web/assets/images/20190508104658.png)
  static const String IMG_20190508104658_PNG = "images/20190508104658.png";
}

20190508155546.png

内存图片

还是刚刚的图片, 这次经过 base64 编码后直接储存至 dart 文件中

然后通过如下的方式获取到项目中

import 'dart:convert';

import 'dart:typed_data';

Uint8List getImageList(String imageBase64) {
  return base64.decode(imageBase64);
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter_web/material.dart';

import 'const/resource.dart';
import 'img.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int counter = 0;
  TextEditingController controller = TextEditingController();
  var key = GlobalKey();
  void add() {
    counter++;
    setState(() {});
    ScaffoldState state = key.currentState;
    state.showSnackBar(
      SnackBar(
        content: Text(controller.text),
      ),
    );
  }

  static var divider = Container(
    padding: const EdgeInsets.symmetric(vertical: 10),
    child: Text("我是分割线"),
    decoration: BoxDecoration(
      border: Border.all(
        color: Colors.blue,
        width: 5,
      ),
    ),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: key,
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        child: Column(
          children: <Widget>[
            TextField(
              controller: controller,
            ),
            Text(
              counter.toString(),
            ),
            Image.network(
                "https://raw.githubusercontent.com/kikt-blog/image/master/img/20190508104658.png"),
            divider,
            Image.asset(R.IMG_20190508104658_PNG),
            divider,
            Image.memory(getImageList(imageBase64)),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: add,
        tooltip: 'push',
        child: Icon(Icons.add),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    print("${this.runtimeType} initState");
  }

  @override
  void dispose() {
    print("${this.runtimeType} dispose");
    super.dispose();
  }
}

20190508132314.png

滚动控件

将 Column 替换为 ListView

20190508135338.png

支持滚动

这里有一点要提,如果是刚进这个页面,鼠标的滚轮是无效的,也就是说,你需要在页面中随意点击一下才可以使用滚动滚动这个页面,似乎是为了让控件获得焦点

我将 ListView 设置为横向滚动,发生了错误,我将 TextField 注释掉以后,恢复了显示

20190508135828.png

并且可以正常横向滚动,在 mac 中也支持 shift+滚动的左右滚动

日志

使用 print 方法在 dart 文件中输出日志

可以在 chrome 的开发者工具的 console 中看到, 目前表现基本与浏览器中的 console.log 方法输出一致

几个问题需要注意

数字的类型

  print("1 is int : ${1 is int}"); // true
  print("1 is double : ${1 is double}"); // true
  print("1.0 is int : ${1.0 is int}"); // true
  print("1.0 is double : ${1.0 is double}"); // true

  print(1.runtimeType); // int
  print(1.0.runtimeType); // int

这一点和 flutter, dartVM 中表现不一样,和 js 表现一致

而 runtimeType 中 1 和 1.0 都是 int 类型

dart:io 的问题

目前在编译过程中,如果发现了使用 dart:io 包的情况,就会自动忽略这个文件的编译

日志如下:

[WARNING] build_web_compilers:entrypoint on web/main.dart: Skipping compiling flutter_web.examples.hello_world|web/main.dart with ddc because some of its
transitive libraries have sdk dependencies that not supported on this platform:

flutter_web.examples.hello_world|lib/main.dart

https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-skipped-compiling-warnings

插件的使用

目前没有成熟的插件系统,也没有完成与纯 flutter 插件的对接

据说可以调用 js 的库来获取一些结果,官方的解释

打包

使用 webdev 打包 $ webdev build

webdev build
[INFO] build_web_compilers:entrypoint on web/main.dart: Running dart2js with --minify --packages=.package-eb297017792c41ff65511a11729f572e -oweb/main.dart.js web/main.dart
[INFO] build_web_compilers:entrypoint on web/main.dart: Dart2Js finished with:

Compiled 20,702,176 characters Dart to 4,249,785 characters JavaScript in 13.8 seconds
Dart file (web/main.dart) compiled to JavaScript: web/main.dart.js
[INFO] Running build completed, took 16.1s
[INFO] Caching finalized dependency graph completed, took 178ms
[INFO] Reading manifest at build/.build.manifest completed, took 13ms
[INFO] Deleting previous outputs in `build` completed, took 93ms
[INFO] Creating merged output dir `build` completed, took 780ms
[INFO] Writing asset manifest completed, took 2ms
[INFO] Succeeded after 17.2s with 9 outputs (2073 actions)

17 秒左右

在当前 build 文件夹下生成了一些文件

20190508160650.png

这些文件直接本地打开 index.html 是跑不起来的

我这里借助了一个轻量的 web 服务器来做这个事

serve build

打开后和运行一样

看一下 build 文件夹的大小, 这里我要惊叹一声!!! 我… 56m !!!

20190508160758.png

其中主要大小集中在packages/$sdk中,有 51m, main.dart.js有 1.2m ,这里因为我放入了那个 base64 的图片字符串充当图片来源, 这个 base64 的字符串在 txt 文件中是 3.2m,所以 main.dart.js 的大小我还算可以接受

assets 目录是 copy 过来的

使用 gz 格式压缩完有 11.4mb

所以这个称之为”开发者预览”是有道理的,后续看怎么优化大小吧,简单来说,这个大小即使在压缩完后也是不能接受的…

查看一下 html 结构

这里使用 web 开发者工具看看

20190508151024.png

整体是一个控件,看来是和 iOS android 一样,直接绘制的

右边看到有一个 input 控件,然后 tanslate 了很长的距离, 应该是用于和内部输入框做双向绑定,以实现复制粘贴,光标等操作的双向绑定关系

后记

简单来说,有一些 bug 和不足

  1. Icons 的图标不显示
  2. 文本不能选中
  3. 输入框的交互太移动端了
  4. 不支持插件
  5. 打包太大了

仓库在这, 查看 example/helloworld 目录

总结: 可用程度?暂时不可用

以上