Dreamer2q Blog
见到,不如不见
Dreamer2q

Code is cheap, talk is expensive

64日志

Flutter渲染语雀lake格式的实践

创建于 2021-03-16 共 2248 字,阅读约 9 分钟 更新于 31 天前
浏览 38评论 0

现在App大多都有动态显示内容的需求,但凡带上一点社交属性,这方面的需求自然是无法避免的。

也因此,如何动态展示图文混排的内容就是Flutter开发过程中一个比较头疼的事情了。


不过很就能想到也是最简单的方法莫过于webview了,但是webview不是flutter中的原生组件,使用起来和flutter的交互多少是有些问题的。


为了进行动态渲染,首先需要约定一个渲染使用的语言,也就是前后端进行格式约定。

这里使用最广泛的莫过于HTML标记语言了,它可以很好的描述属性信息,还很方便进行自己格式的拓展。


当然也可以使用 json 作为交换格式,pub.dev上面有相关库,可以实现动态渲染。


基于 HTML 自然就有人开发出了,渲染HTML内容的库了。


实现原理


在Flutter 1.7.3之后, TextRich 支持内嵌非 TextSpan 组件,通过 WidgetSpan 这个组建可以包裹几乎任何我们想要的组件了。而 TextRich 支持 children 很容易与 HTML 的 Dom 树一一对应起来。


这里简单介绍一下 flutter_html ,它就是一个渲染 HTML 和 CSS 的库。当然了,HTML的标签格式可以无限拓展,因此这个库仅仅支持基础的 HTML 标签。所以,它支持自定义渲染,可以让使用者拓展这个库的功能,即库仅仅提供非常核心的功能,其它的功能可以通过额外的插件进行支持。


  • flutter_html的依赖

可以看到这个库依赖其他的库用于提供图片视频等支持,而使用这个库的App通常也会以来一些图片视频的库,就非常的容易造成版本的冲突,例如 flutter_svg 更新了,但是这个库的依赖却没有更新。所以,我觉得还是有必要将这个库拆分一下的,分出一个独立的 core 核心库,再次基础上进行功能的拓展,方便其它用户进行自定义。


好吧,其实有另一个库 flutter_widget_from_html_core 就是这样做的,这里没有使用的原因是不太喜欢它定义的API,有点太原始? 其实还有点偷懒的成分在里面qwq...用了 core 之后需要写好多基础代码。



Lake格式解析


Lake是什么? 其实就是语雀的文档的一种私有格式,至少web使用的就是这种格式。其实lake格式很简单,它本质就是 HTML 标记语言,只不过在里面增加了自己的东西罢了。例如,语雀支持各种各样的卡片,例如流程图,公式,第三方服务等。这些卡片极大的拓展了语雀文档的能力。


<!doctype lake>

<meta name="doc-version" content="1" />
<meta name="viewport" content="fixed" />
<meta name="typography" content="classic" />

<h2 data-lake-id="db2d53b82eb5bb427c8ef3bb21e5099b" id="rHnnL">
    <span>你好,欢迎使用语雀小记</span>
</h2>

<p data-lake-id="d2280a48e9d52f29e51b9a628b5c3b36">
    <span>语雀小记是一款简洁的碎片化内容记录工具,它可以快速记录各种零碎内容</span><span>:</span><strong><span>一段文字、一张图片、待办事项、网页、文件附件等,并以时间线展示。</span></strong>
</p>

<p data-lake-id="dd33c0b41904556ef7790fedebf35ae7"><br /></p>

<ul class="lake-list" data-lake-id="be1031f0551222aa31bc06151dced078">
    <li class="lake-list-node lake-list-task" data-lake-id="839d00136fc237291c21cd0a4f23ab32">
        <card type="inline" name="checkbox" value="data:true"></card>点击编辑器上方按钮,可快速插入<strong>图片、待办事项、网址和本地文件</strong>
    </li>
</ul>

<p data-lake-id="bf68cdf70aa14c90a560e20dca0385cd">
    <card type="inline" name="image"
        value='data:{"
        src":"https://cdn.nlark.com/yuque/0/2020/png/215718/1602471716472-75149489-c50c-424d-ade9-8942d0848814.png","originWidth":358,"originHeight":78,"name":"image.png","size":18632,"display":"inline","align":"left","linkTarget":"_blank","status":"done","ocrLocations":[],"style":"none","search":"","margin":{"top":false,"bottom":false},"width":193,"height":42}'>
    </card>
</p>

<ul class="lake-list" data-lake-id="f539b2963c7e789aff0e69e02d6bf9c4">
    <li class="lake-list-node lake-list-task" data-lake-id="c3441ca7c2da597cc46e8ee0fb25023d">
        <card type="inline" name="checkbox" value="data:false"></card>和语雀文档一样,<strong>小记支持 </strong><a
            href="https://www.yuque.com/yuque/help/lnobo9" target="_blank"><strong>MarkDown 语法</strong></a>
    </li>
    <li class="lake-list-node lake-list-task" data-lake-id="9300d4165200a23edae9c668204ee438">
        <card type="inline" name="checkbox" value="data:false"></card>点击右上角<card type="inline" name="image"
            value='data:{"
            src":"https://cdn.nlark.com/yuque/0/2020/png/85538/1602565465757-a8c04a37-4a30-48dc-92db-44060949a85b.png","originWidth":120,"originHeight":120,"name":"image.png","size":3790,"display":"inline","align":"left","linkTarget":"_blank","status":"done","ocrLocations":[],"style":"none","search":"","margin":{"top":false,"bottom":false},"width":24,"height":24}'>
        </card>,还可进入全屏编辑模式,获得完整的文档编辑能力
    </li>
    <li class="lake-list-node lake-list-task" data-lake-id="8287d575da2c635b3b7c59da4f558bac">
        <card type="inline" name="checkbox" value="data:false"></card><span>完成记录后,点击上方 ➕,将生成一篇小记</span>
    </li>
</ul>

<p data-lake-id="6dead77f3a46d338d24f314fdc56b20a"><br /></p>

<p data-lake-id="a949ff29cf811209d078890ed2c0175f">💻 桌面端同样支持小记,
    <a href="https://www.yuque.com/install/desktop" target="_blank">下载地址</a>
</p>

<p data-lake-id="728dfd24b7db1ea1fd894c6d7a9f8348"><br /></p>

<p data-lake-id="b3267595569aec383ee27eca346460e9">
    <card type="inline" name="image" value='data:{"
        src":"https://cdn.nlark.com/yuque/0/2020/png/85538/1605497912812-cd59c94f-e35f-4391-bd96-cfa0f3b82330.png","originWidth":2360,"originHeight":1280,"name":"image.png","size":479409,"display":"inline","align":"left","linkTarget":"_blank","status":"done","ocrLocations":[{"x":2007.4093,"y":49.614243,"width":91.19739999999979,"height":33.803757000000004,"text":"搜小记"},{"x":2153.4565,"y":50.32193,"width":145.5585000000001,"height":31.363059999999997,"text":"JI创建时间"},{"x":204.43408,"y":55.91011,"width":68.48676,"height":36.846024,"text":"小记"},{"x":1070.0094,"y":132.79228,"width":62.99170000000004,"height":33.846000000000004,"text":"本月"},{"x":490.64822,"y":166.97165,"width":36.22678000000002,"height":36.22678000000002,"text":"日"},{"x":1103.9685,"y":221.10854,"width":147.7319,"height":34.710409999999996,"text":"今天11:36"},{"x":1138.1516,"y":313.33405,"width":538.0072,"height":50.51242000000002,"text":"你好,欢迎使用语雀小记"},{"x":1143.246,"y":389.21222,"width":1122.2466999999997,"height":40.985839999999996,"text":"语雀小记是一款简洁的碎片化内容记录工具,它可以快速记录各种零碎内容:一段文"},{"x":1139.6061,"y":440.05807,"width":862.8746000000001,"height":42.30259000000001,"text":"字,一张图片,特办事项,网页,文件附件袭,并以时间线展示."},{"x":1190.8889,"y":545.3936,"width":911.6875,"height":37.360250000000065,"text":"点击编器上方按钮,可快远插入图片,待办事项,网址和本地文件"},{"x":1165.7014,"y":618.2883,"width":114.44680000000017,"height":32.84089999999992,"text":"图"},{"x":231.64032,"y":749.4771,"width":87.89066,"height":34.65767000000005,"text":"记一笔"},{"x":1587.8094,"y":786.99567,"width":170.03129999999987,"height":31.861080000000015,"text":"我是有底线的"}],"style":"none","search":"搜小记
        JI创建时间 小记 本月 日 今天11:36 你好,欢迎使用语雀小记 语雀小记是一款简洁的碎片化内容记录工具,它可以快速记录各种零碎内容:一段文 字,一张图片,特办事项,网页,文件附件袭,并以时间线展示.
        点击编器上方按钮,可快远插入图片,待办事项,网址和本地文件 图 记一笔 我是有底线的","margin":{"top":true,"bottom":true},"width":1180,"height":640}'>
    </card>
</p>

<p data-lake-id="71c2a35ee31b90e51e52016dc8209d9c"><br /></p>

<p data-lake-id="425a9b5962c2afc1258aece7f958554c">更多介绍,可查看帮助文档:</p>

<card type="block" name="yuque"
    value='data:{"src":"https://www.yuque.com/yuque/help/vbtstk","url":"https://www.yuque.com/yuque/help/vbtstk?view=doc_embed","mode":"card","margin":true,"id":"n4C8H","detail":{"image":"https://cdn.nlark.com/yuque/0/2020/png/85538/1605499614905-eb180a1c-71fc-400a-bd2e-21aec73d5d57.png?x-oss-process=image%2Fresize%2Cw_1500","title":"📝语雀小记使用指南","type":"doc","belong":"语雀使用手册","belong_url":"/yuque/help","desc":"小记是一款碎片化内容记录工具,它可以快速地记录各种零碎内容:一段文字、一张图片、待办事项、网页、文件附件等,并以时间线展示。1.    小记在哪里?小记在网页端和桌面端均有入口:网页端,你可以在顶部导航找到小记;桌面端,你可以在左侧导航找到小记。2.
    开始使用找到入口,进入小记页面后,就可以开始记录...","url":"https://www.yuque.com/yuque/help/vbtstk","target_type":"Doc","_serializer":"web.editor_link_detail"}}'>
</card>

<p data-lake-id="305238ff0c7b68df554bf975ab14f25e"><span class="lake-fontsize-11" style="color: #595959;"><br /></span>

</p>



以上就是默认小记的lake内容,要注意的是 value 存放的 json 数据都是经过 uriEncode 的。


基本上每个段落都会有一个 data-lake-id ,这个是做什么用的呢?我也不知道,不过猜测是文档编辑时候需要使用的。我们这里只需要渲染出内容即可,不需要管它。


这里我们看一下 image卡片的数据内容

{
  "src":"https://cdn.nlark.com/yuque/0/2020/png/85538/1602565465757-a8c04a37-4a30-48dc-92db-44060949a85b.png",
  "originWidth":120,
	"originHeight":120,
  "name":"image.png",
  "size":3790,
  "display":"inline",
  "align":"left",
  "linkTarget":"_blank",
  "status":"done",
  "ocrLocations":[],
  "style":"none",
  "search":"",
  "margin":{"top":false,"bottom":false},
  "width":24,
  "height":24
}

可以看到,value里面储存的 json 数据已经告诉了我们一起,所以目标很明确,手动渲染 card 标签,根据 type 来确定渲染的卡片类型,例如这个数据就告诉了我们图片 url 地址,这样就简单的返回一个网络图片的组件即可。


所以思路很明确了,下面上代码。


渲染实战


_lakeCardRender(RenderContext _, Widget child, Map attr, dom.Element elem) {
    // var type = attr['type']; //style type
    var name = attr['name']; //type name
    var value = attr['value']; //json value
  	//有时候明明是卡片,但是没有value
    if (value == null) {
      switch (name) {
        case 'hr':
          return Divider();
        default:
          debugPrint("unhandled card: $name");
          return ListTile(
            title: Text('暂时不支持的卡片格式'),
            subtitle: Text('type: $name'),
          );
      }
    }
    var raw = Uri.decodeComponent(value);
    var json = jsonDecode(raw.substring(5));
    switch (name) {
      case 'image':
        return LakeImageWidget(json: json, others: imgUrl);
      default:
        debugPrint('unhandled type: $name');
        return Container(
          width: Get.width,
          margin: const EdgeInsets.all(8),
          padding: EdgeInsets.all(8),
          decoration: BoxDecoration(
            border: Border.all(
              color: Colors.red,
              width: 2,
            ),
          ),
          child: Column(
            children: [
              Text(
                '暂不支持的卡片: $name',
                style: TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
              SelectableText('$json'),
            ],
          ),
       );
}


上面就能简单的渲染出lake格式下携带的图片信息了,之后还有乱七八糟的卡片,都可以通过这个方法渲染出来。


结果展示


此处为语雀视频卡片,点击链接查看:SVID_20210312_155554_1.mp4



此处为语雀视频卡片,点击链接查看:SVID_20210316_191643_1.mp4




现存问题


  • Flutter在debug模式下进行渲染感觉有点卡,laggy;release不知道怎么样
  • 语雀的表格有时候没有给高度,就会导致只能显示默认的高度,会导致大部分内容无法显示。我也不知道要如何根据child动态调整高度,还有就是表格合并功能要好难搞啊。
  • 渲染中使用了webview,没办法,第三方服务都是webview,这样为何不一开始就选择webview呢?(省事多了)。使用webview没办法解决滚动问题,不知道要如何实现webview和flutter之间的多级滚动。
  • RichText如果存在WidgetSpan就不支持select了,所以还是洗洗睡了。


参考链接