杨成功

杨成功 查看完整档案

北京编辑中国石油大学(北京)  |  计算机科学与技术 编辑北京八库科技  |  前端 编辑 github.com/ruidoc 编辑
编辑

前端为基,深耕js

个人动态

杨成功 发布了文章 · 1月29日

【react】前端读取环境变量的骚操作

背景

在经典的单页面应用中,以 react 为例,应用会区分开发环境和生产环境。开发环境是用 webpack-dev-server 编译并启动的前端项目,本质上启动的是一个 node 服务。生产环境也是需要 node 编译为纯静态页面,等待后续部署。

因为编译都用到了 node,所以就可以用 node 提供的 process.env 访问系统环境变量。同时 react 还提供了 process.env.NODE_ENV 来区分开发和生产环境

process.env.NODE_ENV == 'development' ## 开发环境
process.env.NODE_ENV == 'production'  ## 生产环境

使用 NODE_ENV 最多的地方是 webpack 配置文件,用来判断当前环境是开发还是生产,然后使用不同的编译配置打包项目。

上面说的开发环境和生产环境只是用来区分 run startrun build。当项目 build 后生成静态文件准备部署时,我们还要区分是部署 staging 环境还是部署 release 环境。因为 staging 是正式部署生产前的测试阶段,所以这两个环境最大的区别就是接口的 baseURL 不同。

此时 process.env.NODE_ENV 的值都是 production,所以我们需要自定义一个新的环境变量来区分 staging 和 release。然后分别使用不同的 baseURL

假设新的环境变量是 REACT_APP_ENV,判断方式如下:

switch(process.env.REACT_APP_ENV) {
    case 'staging':
        // staging环境
        baseURL = 'http://staging-url.com'
    case 'release':
        // release环境
        baseURL = 'http://release-url.com'
}

那么,如何自定义 REACT_APP_ENV 呢?

编译时定义

上面说了,开发和生产环境打包文件时都是基于 node,所以在 process.env 上设置就可以。

推荐使用 cross-env 这个库,它的好处就是跨平台,window,Linux 或 Mac 都是相同的设置方式。

怎么用呢?比如你的 package.json 中的 build 命令是这样:

"scripts": {
    "build": "node scripts/build.js
}

那么就可以基于 build 分别新增打包命令:

"scripts": {
    "build": "node scripts/build.js,
    "staging": "cross-env REACT_APP_ENV=staging node scripts/build.js",
    "release": "cross-env REACT_APP_ENV=release node scripts/build.js",
}

现在 npm run stagingnpm run release 打包出来的文件唯一的区别就是 REACT_APP_ENV 的不同,这刚好满足了上面我们区分环境设置不同 baseURL 的需求。

但是要注意,也是后面我采坑的地方:这种环境变量的设置方式,只在编译的时候有效。本质上是在编译时根据 REACT_APP_ENV 判断出 baseURL 的值,然后写死在文件里。

也就是说,编译后,baseURL 的值已经是最终值,不会再执行 REACT_APP_ENV 的判断逻辑。

那么,有没有办法只 build 一次,打包一份文件,然后代码里直接读取系统环境变量,来判断使用哪个 baseURL 呢?

有,这是本文重点。

直接读取

首先说说为什么会有这个需求。我司要做前端项目容器化,打包成 docker 镜像部署。期望的是只打包一份镜像,然后 docker run 的时候传一个环境变量,然后前端项目里读这个环境变量,判断出 baseURL 是什么。

开始我的思路就是 process.env,后来发现不对,因为前端 js 是不能直接读取系统信息的。最后我们选定了方案,使用 node。

前端不能读取环境变量,但是node可以。我们的思路是用 node 读取到环境变量,然后用服务端渲染的方式传给前端,前端再将这个值保存在 window 全局对象里后续使用。

有了这个思路,我们来实践:

首先,项目目录下建 release 文件夹,包含两个文件:

1. package.json

{
    "name": "release-server",
    "version": "1.0.0",
    "scripts": {
        "start": "node server.js"
    },
    "dependencies": {
        "express": "^4.13.3",
        "path": "^0.12.7",
        "ejs": "^2.3.4"
    }
}

服务端渲染用 ejs 传值,所以要装 ejs

2. server.js

const path = require('path');
const express = require('express');
// 读取系统环境变量(docker设置)
const REACT_APP_ENV = process.env.REACT_APP_ENV;
const port = process.env.PORT || 8080;
const app = express();

app.use(express.static(__dirname + '/dist')); // dist 是打包后的目录
app.set('views', path.join(__dirname, '/dist/'));
app.set('view engine', 'ejs');

app.get('*', function response(req, res) {
    res.render('index', { REACT_APP_ENV }); // 传递 REACT_APP_ENV
});

app.listen(port);

实际部署的时候,用 node 启动 server.js,然后 node 会读取到 REACT_APP_ENV,并通过 ejs 传给前端。

这一步完成,下一步就是将 html 模版换成 ejs 模版,并接受变量。

在 public 目录下将 index.html 改成 index.ejs,然后在 head 中增加一个接受变量的脚本块

<script>
    var GLOBAL_ENV = {
        "REACT_APP_ENV": "<%= REACT_APP_ENV %>", // 接收 node 传来到 REACT_APP_ENV
    };
    Object.freeze(GLOBAL_ENV);
</script>

然后将 webpack 的解析模版改为 ejs

new HtmlWebpackPlugin({
    filename: "index.ejs",
    template: path.resolve('public/index.ejs'),
})

最后修改 webpack 的 outputrelease/dist

大功告成!

配置完成后执行 run build,打包出来的文件是这样的

image.png

然后将这份代码交给 node 运行起来就好了~

查看原文

赞 0 收藏 0 评论 0

杨成功 关注了专栏 · 1月28日

进击的大前端

前端工程师,底层技术人。 思否2020年度“Top Writer”! 掘金“优秀作者”! 开源中国2020年度“优秀源创作者” 分享各种大前端进阶知识! 关注公众号【进击的大前端】第一时间获取高质量原创。 更多文章和示例源码请看:https://github.com/dennis-jiang/Front-End-Knowledges

关注 13740

杨成功 赞了文章 · 1月27日

高性能 Nginx HTTPS 调优!为 HTTPS 提速 30%

图片

为什么要优化 Ngin HTTPS 延迟

Nginx 常作为最常见的服务器,常被用作负载均衡 (Load Balancer)、反向代理 (Reverse Proxy),以及网关 (Gateway) 等等。一个配置得当的 Nginx 服务器单机应该可以期望承受住 50K 到 80K 左右每秒的请求,同时将 CPU 负载在可控范围内。

但在很多时候,负载并不是需要首要优化的重点。比如对于卡拉搜索来说,我们希望用户在每次击键的时候,可以体验即时搜索的感觉,也就是说,每个搜索请求必须在 100ms - 200ms 的时间内端对端地返回给用户,才能让用户搜索时没有“卡顿”和“加载”。因此,对于我们来说,优化请求延迟才是最重要的优化方向。

这篇文章中,我们先介绍 Nginx 中的 TLS 设置有哪些与请求延迟可能相关,如何调整才能最大化加速。然后我们用优化卡拉搜索Nginx 服务器的实例来分享如何调整 Nginx TLS/SSL 设置,为首次搜索的用户提速 30% 左右。我们会详细讨论每一步我们做了一些什么优化,优化的动机和效果。希望可以对其它遇到类似问题的同学提供帮助。

TLS 握手和延迟

很多时候开发者会认为:如果不是绝对在意性能,那么了解底层和更细节的优化没有必要。这句话在很多时候是恰当的,因为很多时候复杂的底层逻辑必须包起来,才能让更高层的应用开发复杂度可控。比如说,如果你就只需要开发一个 APP 或者网站,可能并没有必要关注汇编细节,关注编译器如何优化你的代码——毕竟在苹果或者安卓上很多优化在底层就做好了。

那么,了解底层的 TLS 和应用层的 Nginx 延迟优化有什么关系呢?

答案是多数情况下,优化网络延迟其实是在尝试减少用户和服务器之间的数据传输次数,也就是所谓的 roundtrip。由于物理限制,北京到云南的光速传播差不多就是要跑 20 来毫秒,如果你不小心让数据必须多次往返于北京和云南之间,那么必然延迟就上去了。

因此如果你需要优化请求延迟,那么了解一点底层网络的上下文则会大有裨益,很多时候甚至是你是否可以轻松理解一个优化的关键。本文中我们不深入讨论太多 TCP 或者 TLS 机制的细节,如果有兴趣的话请参考 High Performance Browser Networking[4] 一书,可以免费阅读。

举个例子,下图中展示了如果你的服务启用了 HTTPS,在开始传输任何数据之前的数据传输情况。

图片

可以看到,在你的用户拿到他需要的数据前,底层的数据包就已经在用户和你的服务器之间跑了 3 个来回。

假设每次来回需要 28 毫秒的话,用户已经等了 224 毫秒之后才开始接收数据。

同时这个 28 毫秒其实是非常乐观的假设,在国内电信、联通和移动以及各种复杂的网络状况下,用户与服务器之间的延迟更不可控。另一方面,通常一个网页需要数十个请求,这些请求不一定可以全部并行,因此几十乘以 224 毫秒,页面打开可能就是数秒之后了。

所以,原则上如果可能的话,我们需要尽量减少用户和服务器之间的往返程 (roundtrip),在下文的设置中,对于每个设置我们会讨论为什么这个设置有可能帮助减少往返程。

Nginx 中的 TLS 设置

那么在 Nginx 设置中,怎样调整参数会减少延迟呢?

开启 HTTP/2

HTTP/2 标准是从 Google 的 SPDY 上进行的改进,比起 HTTP 1.1 提升了不少性能,尤其是需要并行多个请求的时候可以显着减少延迟。在现在的网络上,一个网页平均需要请求几十次,而在 HTTP 1.1 时代浏览器能做的就是多开几个连接(通常是 6 个)进行并行请求,而 HTTP 2 中可以在一个连接中进行并行请求。HTTP 2 原生支持多个并行请求,因此大大减少了顺序执行的请求的往返程,可以首要考虑开启。

如果你想自己看一下 HTTP 1.1 和 HTTP 2.0 的速度差异,可以试一下:https://www.httpvshttps.com/。我的网络测试下来 HTTP/2 比 HTTP 1.1 快了 66%。

image.png

在 Nginx 中开启 HTTP 2.0 非常简单,只需要增加一个 http2 标志即可

listen 443 ssl;
# 改为
listen 443 ssl http2;

如果你担心你的用户用的是旧的客户端,比如 Python 的 requests,暂时还不支持 HTTP 2 的话,那么其实不用担心。如果用户的客户端不支持 HTTP 2,那么连接会自动降级为 HTTP 1.1,保持了后向兼容。因此,所有使用旧 Client 的用户,仍然不受影响,而新的客户端则可以享受 HTTP/2 的新特性。

如何确认你的网站或者 API 开启了 HTTP 2

在 Chrome 中打开开发者工具,点开 Protocol 之后在所有的请求中都可以看到请求用的协议了。如果 protocol 这列的值是 h2 的话,那么用的就是 HTTP 2 了

图片

当然另一个办法是直接用 curl 如果返回的 status 前有 HTTP/2 的话自然也就是 HTTP/2 开启了。

➜  ~ curl --http2 -I https://kalasearch.cn
HTTP/2 403
server: Tengine
content-type: application/xml
content-length: 264
date: Tue, 22 Dec 2020 18:38:46 GMT
x-oss-request-id: 5FE23D363ADDB93430197043
x-oss-cdn-auth: success
x-oss-server-time: 0
x-alicdn-da-ups-status: endOs,0,403
via: cache13.l2et2[148,0], cache10.l2ot7[291,0], cache4.us13[360,0]
timing-allow-origin: *
eagleid: 2ff6169816086623266688093e

调整 Cipher 优先级

尽量挑选更新更快的 Cipher,有助于减少延迟:

# 手动启用 cipher 列表
ssl_prefer_server_ciphers on;  # prefer a list of ciphers to prevent old and slow ciphers
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';

启用 OCSP Stapling

在国内这可能是对使用 Let's Encrypt 证书的服务或网站影响最大的延迟优化了。如果不启用 OCSP Stapling 的话,在用户连接你的服务器的时候,有时候需要去验证证书。而因为一些不可知的原因(这个就不说穿了)Let's Encrypt 的验证服务器并不是非常通畅,因此可以造成有时候数秒甚至十几秒延迟的问题,这个问题在 iOS 设备上特别严重

解决这个问题的方法有两个:

  • 不使用 Let's Encrypt,可以尝试替换为阿里云提供的免费 DV 证书
  • 开启 OCSP Stapling

开启了 OCSP Stapling 的话,跑到证书验证这一步可以省略掉。省掉一个 roundtrip,特别是网络状况不可控的 roundtrip,可能可以将你的延迟大大减少。

在 Nginx 中启用 OCSP Stapling 也非常简单,只需要设置:

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/full_chain.pem;

如何检测 OCSP Stapling 是否已经开启?

可以通过以下命令

openssl s_client -connect test.kalasearch.cn:443 -servername kalasearch.cn -status -tlsextdebug < /dev/null 2>&1 | grep -i "OCSP response"

来测试。如果结果为

OCSP response:
OCSP Response Data:
    OCSP Response Status: successful (0x0)
    Response Type: Basic OCSP Response

则表明已经开启。

调整 ssl_buffer_size

ssl_buffer_size 控制在发送数据时的 buffer 大小,默认设置是 16k。这个值越小,则延迟越小,而添加的报头之类会使 overhead 会变大,反之则延迟越大,overhead 越小。

因此如果你的服务是 REST API或者网站的话,将这个值调小可以减小延迟和 TTFB,但如果你的服务器是用来传输大文件的,那么可以维持 16k。

如果是网站或者 REST API,建议值为 4k,但是这个值的最佳取值显然会因为数据的不同而不一样,因此请尝试 2 - 16k 间不同的值。在 Nginx 中调整这个值也非常容易

ssl_buffer_size 4k;

启用 SSL Session 缓存

启用 SSL Session 缓存可以大大减少 TLS 的反复验证,减少 TLS 握手的 roundtrip。虽然 session 缓存会占用一定内存,但是用 1M 的内存就可以缓存 4000 个连接,可以说是非常非常划算的。同时,对于绝大多数网站和服务,要达到 4000 个同时连接本身就需要非常非常大的用户基数,因此可以放心开启。

#这里 ssl_session_cache 设置为使用 50M 内存,以及 4 小时的连接超时关闭时间 ssl_session_timeout
# Enable SSL cache to speed up for return visitors
ssl_session_cache   shared:SSL:50m; # speed up first time. 1m ~= 4000 connections
ssl_session_timeout 4h;

卡拉搜索如何减少 30% 的请求延迟

卡拉搜索是国内的 Algolia,致力于帮助开发者快速搭建即时搜索功能(instant search),做国内最快最易用的搜索即服务。

开发者接入后,所有搜索请求通过卡拉 API 即可直接返回给终端用户。为了让用户有即时搜索的体验,我们需要在用户每次击键后极短的时间内(通常是 100ms 到 200ms)将结果返回给用户。因此每次搜索需要可以达到 50 毫秒以内的引擎处理时间和 200 毫秒以内的端对端时间。

我们用豆瓣电影的数据做了一个电影搜索的 Demo,如果感兴趣的话欢迎体验一下即时搜索,尝试一下搜索“无间道”或者“大话西游”体验一下速度和相关度:https://movies-demo.kalasearc...

对于每个请求只有 100 到 200 毫秒的延迟预算,我们必须把每一步的延迟都考虑在内。

简化一下,每个搜索请求需要经历的延迟有

图片

总延迟 = 用户请求到达服务器(T1) + 反代处理(Nginx T2) + 数据中心延迟(T3) + 服务器处理 (卡拉引擎 T4) + 用户请求返回(T3+T1)

在上述延迟中,T1 只与用户与服务器的物理距离相关,而 T3 非常小可以忽略不计。

所以我们能控制的大致只有 T2 和 T4,即 Nginx 服务器的处理时间和卡拉的引擎处理时间。

Nginx 在这里作为反向代理,处理一些安全、流量控制和 TLS 的逻辑,而卡拉的引擎则是一个在 Lucene 基础上的倒排引擎。

我们首先考虑的第一个可能性是:延迟是不是来自卡拉引擎呢?

在下图展示的 Grafana 仪表盘中,我们看到除了几个时不时的慢查询,搜索的 95% 服务器处理延迟小于 20 毫秒。对比同样的数据集上 benchmark 的 Elastic Search 引擎的 P95 搜索延迟则在 200 毫秒左右,所以排除了引擎速度慢的可能。

image.png

而在阿里云监控中,我们设置了从全国各地向卡拉服务器发送搜索请求。我们终于发现 SSL 处理时间时常会超过 300 毫秒,也就是说在 T2 这一步,光处理 TLS 握手之类的事情,Nginx 已经用掉了我们所有的请求时间预算。

同时检查之后我们发现,在苹果设备上搜索速度格外慢,特别是第一次访问的设备。因此我们大致判断应该是因为我们使用的 Let's Encrypt 证书的问题。

我们按照上文中的步骤对 Nginx 设置进行了调整,并将步骤总结出来写了这篇文章。在调整了 Nginx TLS 的设置后,SSL 时间从平均的 140ms 降低到了 110ms 左右(全国所有省份联通和移动测试点),同时苹果设备上首次访问慢的问题也消失了。

图片

在调整过后,全国范围内测试的搜索延迟降低到了 150 毫秒左右。

总结

调整 Nginx 中的 TLS 设置对于使用 HTTPS 的服务和网站延迟有非常大的影响。本文中总结了 Nginx 中与 TLS 相关的设置,详细讨论各个设置可能对延迟的影响,并给出了调整建议。

来源:https://kalasearch.cn/blog/hi...

image

查看原文

赞 45 收藏 30 评论 1

杨成功 关注了用户 · 1月24日

美团技术团队 @meituanjishutuandui

一个只分享有价值技术干货的微信公众号

美团技术团队会定期推送来自一线的实践技术文章,涵盖前端(Web、iOS和Android)、后台、大数据、AI/算法、测试、运维等技术领域。 在这里,与8000多业界一流工程师交流切磋。

关注 4971

杨成功 收藏了文章 · 1月23日

技术杂谈 | Flutter 的性能分析、工程架构与细节处理

出品/ 有道智云
编辑/ Ryan
来源:有道技术团队(ID:youdaotech)

一、为何 Flutter

跨端技术众多,为何选择 Flutter?它能带来哪些优势,有哪些缺点?

先看看具体的工程效果:

Flutter 工程效果​v.qq.com

web 端效果体验:

flutter_exercise

1.1 Flutter VS 原生

无论如何,原生的运行效率毋庸置疑是最高的,但是从工程工作量的角度来对比的话,特别是快速试错和业务扩展阶段,Flutter 是目前为止比较推荐的利器。

1.2 Flutter VS Web

任何跨端的技术都是基于一码多端的思维,解决工程效率的问题,之前很多的跨端技术,例如 React Native 等都是基于web的跨端性解决方案,但是大家都知道,web 在移动端上的运行效率和 PC 上有巨大差距的,这就导致 RN 不能很有效地在移动端完成各种复杂的交互式运算(例如复杂的动画运算,交互的执行性能等),即便是引入了 Airbnb 的 Lottie 引擎依然会在低端的手机上面显得很卡顿(当然也可以使用一些自研的引擎技术来针对各端来解决,不过这样就失去了跨端的意义)。

1.3 Flutter 性能

Flutter 的编译方式和产物是决定其高效运行效率的前提,不同于 web 的跨端编译一样(web 的跨端编译大多是选择了使用 "桥" 的概念来调用编译产物,通常是使用了原生端的入口 + web 端的桥来实现),Flutter 几乎是把 dart 的源码通过不同平台的编译原理生成各平台的产物,这种"去桥"的产物正式我们所希望得到的、贴近原生运行性能的编译产物(当然,在 dart 最初设计的时候,是参考了很多前端的结构来完成的,特别从语法上面能够很明显地感受到前端的痕迹,而且最初的 dart2js 的原理也是同样"桥"的概念)。

例如 9月23号 google 发布的新 Flutter 版本中,在支持的 Windows 编译产物上,就是通过类似 Visual Studio 的编译工具(如果要将你的 Flutter 工程编译成 Windows 产物,需要提前安装一些 VS 相关的编译插件),生成了 Windows 下的工程解决方案 .sln,最终生成 dll 的调用方式,运行起来很流畅,可以下载附件中的 Release.zip 来尝试运行。(Release.zip 下载

(PS:这里所有编译工程都是通过同一套代码完成,包括上文中的 web 地址、移动端案例还有这里的 Windows 案例)

1.4 与 RN 的性能对比

以上是同样功能模块下,Flutter 和 RN 的一些数据上的对比,是从众多的数据中抽取出来比较有代表性的一组。

1.5 跨端平台的多样性

1.6 引擎

Flare-Flutter 是一款十分优秀的 Flutter 动画引擎,编译出的动画已经在 Windows、移动端、web 上亲测验证过。

1.7 语法糖

A?.B
如果 A 等于 null,那么 A?.B 为 null
如果 A 不等于 null,那么 A?.B 等价于 A.B
Animal animal = new Animal('cat');
Animal empty = null;
//animal 非空,返回 animal.name 的值 cat
print(animal?.name);
//empty 为空,返回 null
print(empty?.name);
A??B
如果 A 等于 null,那么 A??B 为 B
如果 A 不等于 null,那么 A??B 为 A

1.8 综合测评

1.9 互动应用

Flutter 生成的互动可以嵌入到任何端中使用精简的指令集进行互动,为互动场景(教学场景等带来巨大的希望),以下是直播同步互动的 demo 场景。

image

二、Flutter 业务架构

Flutter 中目前是没有现成的 mvvm 框架的,但是我们可以利用 Element 树特性来实现 mvvm。

2.1 ViewModel

abstract class BaseViewModel {
  bool _isFirst = true;
  BuildContext context;

  bool get isFirst => _isFirst;

  @mustCallSuper
  void init(BuildContext context) {
    this.context = context;
    if (_isFirst) {
      _isFirst = false;
      doInit(context);
    }
  }

  // the default load data method
  @protected
  Future refreshData(BuildContext context);

  @protected
  void doInit(BuildContext context);

  void dispose();
  
class ViewModelProvider<T extends BaseViewModel> extends StatefulWidget {
  final T viewModel;
  final Widget child;

  ViewModelProvider({
    @required this.viewModel,
    @required this.child,
  });

  static T of<T extends BaseViewModel>(BuildContext context) {
    final type = _typeOf<_ViewModelProviderInherited<T>>();
    _ViewModelProviderInherited<T> provider =
        // 查询Element树中缓存的InheritedElement
        context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
    return provider?.viewModel;
  }

  static Type _typeOf<T>() => T;

  @override
  _ViewModelProviderState<T> createState() => _ViewModelProviderState<T>();
}

class _ViewModelProviderState<T extends BaseViewModel>
    extends State<ViewModelProvider<T>> {
  @override
  Widget build(BuildContext context) {
    return _ViewModelProviderInherited<T>(
      child: widget.child,
      viewModel: widget.viewModel,
    );
  }

  @override
  void dispose() {
    widget.viewModel.dispose();
    super.dispose();
  }
}

// InheritedWidget可以被Element树缓存
class _ViewModelProviderInherited<T extends BaseViewModel>
    extends InheritedWidget {
  final T viewModel;

  _ViewModelProviderInherited({
    Key key,
    @required this.viewModel,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;

2.2 DataModel

import 'dart:convert';

import 'package:pupilmath/datamodel/base_network_response.dart';
import 'package:pupilmath/datamodel/challenge/challenge_ranking_list_item_data.dart';
import 'package:pupilmath/utils/text_utils.dart';

///历史榜单
class ChallengeHistoryRankingListResponse
    extends BaseNetworkResponse<ChallengeHistoryRankingData> {
  ChallengeHistoryRankingListResponse.fromJson(Map<String, dynamic> json)
      : super.fromJson(json);

  @override
  ChallengeHistoryRankingData decodeData(jsonData) {
    if (jsonData is Map) {
      return ChallengeHistoryRankingData.fromJson(jsonData);
    }
    return null;
  }
}

class ChallengeHistoryRankingData {
  String props;
  int bestRank; //最佳排名
  int onlistTimes; //上榜次数
  int total; //总共挑战数
  List<ChallengeHistoryRankingItemData> ranks; //先给10天

  //二维码
  String get qrcode =>
      TextUtils.isEmpty(props) ? '' : json.decode(props)['qrcode'] ?? '';

  ChallengeHistoryRankingData.fromJson(Map<String, dynamic> json) {
    props = json['props'];
    bestRank = json['bestRank'];
    onlistTimes = json['onlistTimes'];
    total = json['total'];
    if (json['ranks'] is List) {
      ranks = [];
      (json['ranks'] as List).forEach(
          (v) => ranks.add(ChallengeHistoryRankingItemData.fromJson(v)));
    }
  }
}

///历史战绩的item
class ChallengeHistoryRankingItemData {
  ChallengeRankingListItemData champion; //当天最好成绩
  ChallengeRankingListItemData user;

  ChallengeHistoryRankingItemData.fromJson(Map<String, dynamic> json) {
    if (json['champion'] is Map)
      champion = ChallengeRankingListItemData.fromJson(json['champion']);
    if (json['user'] is Map)
      user = ChallengeRankingListItemData.fromJson(json['user']);
  }

2.3 View

import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:pupilmath/datamodel/challenge/challenge_history_ranking_list_data.dart';
import 'package:pupilmath/entity_factory.dart';
import 'package:pupilmath/network/constant.dart';
import 'package:pupilmath/network/network.dart';
import 'package:pupilmath/utils/print_helper.dart';
import 'package:pupilmath/viewmodel/base/abstract_base_viewmodel.dart';
import 'package:rxdart/rxdart.dart';

//每日挑战历史战绩
class ChallengeHistoryListViewModel extends BaseViewModel {
  BehaviorSubject<ChallengeHistoryRankingData> _challengeObservable =
      BehaviorSubject();

  Stream<ChallengeHistoryRankingData> get challengeRankingListStream =>
      _challengeObservable.stream;

  @override
  void dispose() {
    _challengeObservable.close();
  }

  @override
  void doInit(BuildContext context) {
    refreshData(context);
  }

  @override
  Future refreshData(BuildContext context) {
    return _loadHistoryListData();
  }

  _loadHistoryListData() async {
    Map<String, dynamic> parametersMap = {};
    parametersMap["pageNum"] = 1;
    parametersMap["pageSize"] = 10; //拿10天数据

    handleDioRequest(
      () => NetWorkHelper.instance
          .getDio()
          .get(challengeHistoryListUrl, queryParameters: parametersMap),
      onResponse: (Response response) {
        ChallengeHistoryRankingListResponse rankingListResponse =
            EntityFactory.generateOBJ(json.decode(response.toString()));

        if (rankingListResponse.isSuccessful) {
          _challengeObservable.add(rankingListResponse.data);
        } else {
          _challengeObservable.addError(null);
        }
      },
      onError: (error) => _challengeObservable.addError(error),
    );
  }

  Future<ChallengeHistoryRankingData> syncLoadHistoryListData(
    int pageNum,
    int pageSize,
  ) async {
    Map<String, dynamic> parametersMap = {};
    parametersMap["pageNum"] = pageNum;
    parametersMap["pageSize"] = pageSize;

    try {
      Response response = await NetWorkHelper.instance
          .getDio()
          .get(challengeHistoryListUrl, queryParameters: parametersMap);
      ChallengeHistoryRankingListResponse rankingListResponse =
          EntityFactory.generateOBJ(json.decode(response.toString()));
      if (rankingListResponse.isSuccessful) {
        return rankingListResponse.data;
      } else {
        return null;
      }
    } catch (e) {
      printHelper(e);
    }
    return null;
  }

2.4 一些基础架构

2.5 View 和 ViewModel 如何实现初始化和相互作用

2.6 Flutter 业务架构抽离

如果是统一系列的产品业务形态,还可以抽离出一套核心的架构,复用在同样的生产产品线上,例如当前产品线以教育为主,利用 Flutter 的一码多端性质,则可以把题版生产工厂、渲染题版引擎、 适配框架、 以及跨端接口的框架都抽离出来,迅速地形成可以推广复用的模板,可以事半功倍地解决掉业务上的试错成本问题,当然,其他产品性质的业务线均可如此。

三、Flutter 适配

任何框架中的 UI 适配都是特别繁重的工作,跨端上的适配更是如此,因此在同一套布局里面,各个平台的换算过程显得尤为重要,起初的时候,Flutter 中并没有提供某种诸如 dp 或者 sp 的适配方式,而且考虑到直接更改底层 Matrix 换算比例的话可能会让原本高清分辨率的手机显示不是那么清楚,而 Flutter 的宽高单位都是 num,最后编译的时候才会去对应到各个平台的单位尺寸。

为了减轻设计师的设计负担,这里通常使用一套 iOS 的设计稿即可,以375 x 667的通用设计稿为例,转换过来到android上是360 x 640 (对应1080 x 1920),这里flutter的单位也是和对应手机的像素密度有关的。

3.1 构造一个转换工具类:

//目前适配iPhone和iPad机型尺寸
import 'dart:io';
import 'dart:ui';
import 'dart:math';

import 'package:pupilmath/utils/print_helper.dart';

bool initScale = false;
//针对iOS平台的scale系数
double iosScaleRatio = 0;
//针对android平台的scale系数
// (因为所有设计稿均使用iOS的设计稿进行,所以需要转换为android设计稿上的尺寸,
// 否则无法进行小屏幕上的适配)
double androidScaleRatio = 0;
//文字缩放比
double textScaleRatio = 0;

const double baseIosWidth = 375;
const double baseIosHeight = 667;
const double baseIosHeightX = 812;

const double baseAndroidWidth = 360;
const double baseAndroidHeight = 640;

void _calResizeRatio() {
  if (Platform.isIOS) {
    final width = window.physicalSize.width;
    final height = window.physicalSize.height;
    final ratio = window.devicePixelRatio;
    final widthScale = (width / ratio) / baseIosWidth;
    final heightScale = (height / ratio) / baseIosHeight;
    iosScaleRatio = min(widthScale, heightScale);
  } else if (Platform.isAndroid) {
    double widthScale = (baseAndroidWidth / baseIosWidth);
    double heightScale = (baseAndroidHeight / baseIosHeight);
    double scaleRatio = min(widthScale, heightScale);
    //取两位小数
    androidScaleRatio = double.parse(scaleRatio.toString().substring(0, 4));
  }
}

bool isFullScreen() {
  return false;
}

//缩放
double resizeUtil(double value) {
  if (!initScale) {
    _calResizeRatio();
    initScale = true;
  }

  if (Platform.isIOS) {
    return value * iosScaleRatio;
  } else if (Platform.isAndroid) {
    return value * androidScaleRatio;
  } else {
    return value;
  }
}

//缩放还原
//每个屏幕的缩放比不一样,如果在iOS设备上出题,则题目坐标值需要换算成原始坐标,加载的时候再通过不同平台换算回来
double unResizeUtil(double value) {
  if (iosScaleRatio == 0) {
    _calResizeRatio();
  }

  if (Platform.isIOS) {
    return value / iosScaleRatio;
  } else {
    return value / androidScaleRatio;
  }
}

//文字缩放大小
_calResizeTextRatio() {
  final width = window.physicalSize.width;
  final height = window.physicalSize.height;
  final ratio = window.devicePixelRatio;
  double heightRatio = (height / ratio) / baseIosHeight / window.textScaleFactor;
  double widthRatio = (width / ratio) / baseIosWidth / window.textScaleFactor;
  textScaleRatio = min(heightRatio, widthRatio);
}

double resizeTextSize(double value) {
  if (textScaleRatio == 0) {
    _calResizeTextRatio();
  }
  return value * textScaleRatio;
}

double resizePadTextSize(double value) {
  if (Platform.isIOS) {
    final width = window.physicalSize.width;
    final ratio = window.devicePixelRatio;
    final realWidth = width / ratio;
    if (realWidth > 450) {
      return value * 1.5;
    } else {
      return value;
    }
  } else {
    return value;
  }
}

double autoSize(double percent, bool isHeight) {
  final width = window.physicalSize.width;
  final height = window.physicalSize.height;
  final ratio = window.devicePixelRatio;
  if (isHeight) {
    return height / ratio * percent;
  } else {
    return width / ratio * percent;
  }

3.2 具体使用:

这样每次如果有分辨率变动或者适配方案变动的时候,直接修改 resizeUtil 即可,但是这样带来的问题就是,在编写过程中单位变得很冗长,而且不熟悉团队工程的人会容易忘写,导致查错时间变长,代码侵入性较高,于是利用 dart 语言的扩展函数特性,为 resizeUtil 做一些改进。

3.3 低侵入式的 resizeUtil

通过扩展 dart 的 num 来构造想要的单位,这里用 dp 和 sp 来举例,在 resizeUtil 中加入扩展:

extension dimensionsNum on num {
  ///转为dp
  double get dp => resizeUtil(this.toDouble());

  ///转为文本大小sp
  double get sp => resizeTextSize(this.toDouble());

  ///转为pad文字适配
  double get padSp => resizePadTextSize(this.toDouble());

然后在布局中直接书写单位即可:

四、Flutter 中的一些坑

4.1 泛型上的坑

刚开始在移动端上使用泛型来做数据的自动解析时,使用了 T.toString 来判断类型,但是当编译成 web 的 release 版本时,在移动端正常运行的程序在web上无法正常工作:

刚开始的时候把目标一直定位在编译的方式上,因为存在 dev profile release 三种编译模式,只有在 release 上无法运行,误以为是 release 下编译有 bug,随着和 Flutter 团队的深入讨论后,发现其实是泛型在 release 模式下的坑,即在 web 版本的 release 模式下,一切都会进行压缩(包含类型的定义),所以在 release 下,T.toString() 返回的是 null,因此无法识别出泛型特征,具体的讨论链接:

Flutter application which use canvas to build self-CustomPainter cannot work on browser if i used the release mode by command "flutter run -d chrome --release" or "flutter build web". · Issue #47967 · flutter/flutter​github.com

In release mode everything is minified, the (T.toString() == "Construction2DEntity") comparison fails and you get entity null returned.
If you change the code to (T ==Construction2DEntity) it will fix your app.

最后建议,无论在何种模式下,都直接写成T==的形式最为安全。

class EntityFactory {
  static T generateOBJ<T>(json) {
    if (1 == 0) {
      return null;
    } else if (T == "ChallengeRankingListDataEntity") {
      /// 每日挑战排行榜
      return ChallengeHomeRankingListResponse.fromJson(json) as T;
    } else if (T == "KnowledgeEntity") {
      return KnowledgeEntity.fromJson(json) as T;
    }
  }

4.2 在编译成 web 产物后如何使用 iframe 来加载其他网页

对于移动端来说,webview_flutter 可以解决掉加载 web 的问题,不过编译成 web 产物后,已经无法直接使用 WebView 插件来进行加载,此时需要用到 dart 最初设计来编写网页的一些方式,即 HtmlElmentView:

import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'dart:html' as html;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
           child: Iframe()
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){},
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }
}
class Iframe extends StatelessWidget {
  Iframe(){
    ui.platformViewRegistry.registerViewFactory('iframe', (int viewId) {
      var iframe = html.IFrameElement();
      iframe.data-original='https://flutter.dev';
      return iframe;
  });
  }
  @override
  Widget build(BuildContext context) {
    return Container(
      width:400,
      height:300,
      child:HtmlElementView(viewType: 'iframe')
    );
  }

不过这种方式会带来新的底层刷新渲染问题(当鼠标移动到某个元素时,会不停地闪动刷新),目前在新的版本上已修复,有兴趣的同学可以看看:

https://github.com/flutter/fl...

4.3 Flutter 如何加载本地的 html 并且进行通信

内置 html 是很多工程的需求,很多网上的资料都是通过把本地的 html 做成数据流的方式然后加载进来,这种做法的兼容性很不好,而且编写过程中容易出现很多文件流过大无法读取的问题,其实这些做法都不是很舒适,我们应该通过 IFrameElement 来进行加载并通信,做法和前端很类似:

4.4 在 iOS 13.4 上 WebView 的手势无法正常使用

官方的 webview_flutter 在上一个版本当 iOS 升级到13.4之后会出现手势被拦截且无法正常使用的情况,换成flutter_webview_plugin后暂时解决掉该问题(目前 WebView 已经做了针对性的修复,但是还未验证),但是 flutter_webview_plugin 在 iOS 上又无法写入 user-agent,目前可以通过修改本地的插件代码进行解决:

文件位置为:

flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_webview_plugin-0.3.11/ios/Classes/FlutterWebviewPlugin.m修改内容为在146行(initWebview方法中初始化WKWebViewConfiguration后)添加如下代码if (@available(iOS 9.0, *)) {if (userAgent != (id)[NSNull null]) {self.webview.customUserAgent = userAgent;}}

关于 webview_flutter 的手势问题还在不断的讨论中:

https://github.com/flutter/fl...

五、关于布局和运算

5.1 容器 Widget 和渲染 Widget

5.2 GlobalKey

通过 GlobalKey 获取 RenderBox 来获取渲染出的控件的 size 和 position 等参数:

5.3 浮点运算

在 dart 的浮点运算中,由于都是高精度的 double 运算,当运算长度过长的时候,dart 会自动随机最后的一位小数,这样会导致每一次有些浮点运算每一次都是不确定的,这时需要手动进行精度转换,例如在计算两条线段是否共线时:

5.4 Matrix 的平移和旋转

在矩阵的换算过程中,如果使用普通的matrix.translate,会导致 rotate 之后,再进行 translate 会在旋转的基数上面做系数叠加平移运算,这样计算后得到的不是自己想要的结果,因此如果运算当中有 rotate 操作时,应当使用 leftTranslate 来保证每次运算的独立性:

六、项目优化

6.1 避免 build() 方法耗时:

6.2 重绘区域优化:

6.3 尽量避免使用 Opacity

6.4 Flutter的单线程模型

优先全部执行完 Microtask Queue 中的 Event,直到 Microtask Queue 为空,才会执行 Event Queue 中的 Event。

6.5 耗时方法放在 Isolate

Isolate 是 Dart 里的线程,每个 Isolate 之间不共享内存,通过消息通信。

Dart 的代码运行在 Isolate 中,处于同一个 Isolate 的代码才能相互访问。

七、杂谈总结

经历了对 Flutter 长期的探索和项目验证,目前对 Flutter 有自己的一些杂谈总结:

7.1

Flutter 在移动端的表现还是很不错的,在运行流畅度方面也是非常棒,经过优化过后的带大量图像运算的 app 运行在2013年的旧 Android 手机上面依然十分流畅,iOS 的流畅程度也堪比原生。

7.2

对于 web 的应用来说,Flutter 还在不断地改进,其中还有很多的坑没有解决,这里包括了移动端的 WebView 以及编程成的 web 应用,还不适合大面积的投入到 web 的生产环境中。

7.3

关于和 Native 的混编,为了避免产生混合栈应用中的内存问题和渲染问题等,建议尽量将嵌入原生的 Flutter 节点设计在叶子节点上,即业务栈跳转到 Flutter 后尽量完成结束后再回到Native栈中。

7.4

基于“去桥”的原生编译方式,Flutter 在未来各个平台上的运行应该会充满期待,目前验证的移动端应用打包成 Windows 应用后,运行表现还是很不错的,当然一些更大型的应用需要时间去摸索和完善。

7.5

语法方面,Flutter 中的 dart 正在变得越来越简单,也在借鉴一些优秀的前端框架上的语法,例如 react 等,kotlin 中也有很多相似的地方,感觉 Flutter 团队正在努力地促进大前端时代的发展。


总之,Flutter 确实带来了很多以前的跨端方案没法满足的惊喜的地方,相信不久的将来一码多端会变得越来越重要,特别是在新业务的探索成本上表现得十分抢眼。

以上是一些对 Flutter 的一些粗浅的总结,欢迎有兴趣的小伙伴一起探讨。

网易有道,与你同道,因为热爱所以选择, 期待志同道合的你加入我们。
  • END -
查看原文

杨成功 赞了文章 · 1月23日

技术杂谈 | Flutter 的性能分析、工程架构与细节处理

出品/ 有道智云
编辑/ Ryan
来源:有道技术团队(ID:youdaotech)

一、为何 Flutter

跨端技术众多,为何选择 Flutter?它能带来哪些优势,有哪些缺点?

先看看具体的工程效果:

Flutter 工程效果​v.qq.com

web 端效果体验:

flutter_exercise

1.1 Flutter VS 原生

无论如何,原生的运行效率毋庸置疑是最高的,但是从工程工作量的角度来对比的话,特别是快速试错和业务扩展阶段,Flutter 是目前为止比较推荐的利器。

1.2 Flutter VS Web

任何跨端的技术都是基于一码多端的思维,解决工程效率的问题,之前很多的跨端技术,例如 React Native 等都是基于web的跨端性解决方案,但是大家都知道,web 在移动端上的运行效率和 PC 上有巨大差距的,这就导致 RN 不能很有效地在移动端完成各种复杂的交互式运算(例如复杂的动画运算,交互的执行性能等),即便是引入了 Airbnb 的 Lottie 引擎依然会在低端的手机上面显得很卡顿(当然也可以使用一些自研的引擎技术来针对各端来解决,不过这样就失去了跨端的意义)。

1.3 Flutter 性能

Flutter 的编译方式和产物是决定其高效运行效率的前提,不同于 web 的跨端编译一样(web 的跨端编译大多是选择了使用 "桥" 的概念来调用编译产物,通常是使用了原生端的入口 + web 端的桥来实现),Flutter 几乎是把 dart 的源码通过不同平台的编译原理生成各平台的产物,这种"去桥"的产物正式我们所希望得到的、贴近原生运行性能的编译产物(当然,在 dart 最初设计的时候,是参考了很多前端的结构来完成的,特别从语法上面能够很明显地感受到前端的痕迹,而且最初的 dart2js 的原理也是同样"桥"的概念)。

例如 9月23号 google 发布的新 Flutter 版本中,在支持的 Windows 编译产物上,就是通过类似 Visual Studio 的编译工具(如果要将你的 Flutter 工程编译成 Windows 产物,需要提前安装一些 VS 相关的编译插件),生成了 Windows 下的工程解决方案 .sln,最终生成 dll 的调用方式,运行起来很流畅,可以下载附件中的 Release.zip 来尝试运行。(Release.zip 下载

(PS:这里所有编译工程都是通过同一套代码完成,包括上文中的 web 地址、移动端案例还有这里的 Windows 案例)

1.4 与 RN 的性能对比

以上是同样功能模块下,Flutter 和 RN 的一些数据上的对比,是从众多的数据中抽取出来比较有代表性的一组。

1.5 跨端平台的多样性

1.6 引擎

Flare-Flutter 是一款十分优秀的 Flutter 动画引擎,编译出的动画已经在 Windows、移动端、web 上亲测验证过。

1.7 语法糖

A?.B
如果 A 等于 null,那么 A?.B 为 null
如果 A 不等于 null,那么 A?.B 等价于 A.B
Animal animal = new Animal('cat');
Animal empty = null;
//animal 非空,返回 animal.name 的值 cat
print(animal?.name);
//empty 为空,返回 null
print(empty?.name);
A??B
如果 A 等于 null,那么 A??B 为 B
如果 A 不等于 null,那么 A??B 为 A

1.8 综合测评

1.9 互动应用

Flutter 生成的互动可以嵌入到任何端中使用精简的指令集进行互动,为互动场景(教学场景等带来巨大的希望),以下是直播同步互动的 demo 场景。

image

二、Flutter 业务架构

Flutter 中目前是没有现成的 mvvm 框架的,但是我们可以利用 Element 树特性来实现 mvvm。

2.1 ViewModel

abstract class BaseViewModel {
  bool _isFirst = true;
  BuildContext context;

  bool get isFirst => _isFirst;

  @mustCallSuper
  void init(BuildContext context) {
    this.context = context;
    if (_isFirst) {
      _isFirst = false;
      doInit(context);
    }
  }

  // the default load data method
  @protected
  Future refreshData(BuildContext context);

  @protected
  void doInit(BuildContext context);

  void dispose();
  
class ViewModelProvider<T extends BaseViewModel> extends StatefulWidget {
  final T viewModel;
  final Widget child;

  ViewModelProvider({
    @required this.viewModel,
    @required this.child,
  });

  static T of<T extends BaseViewModel>(BuildContext context) {
    final type = _typeOf<_ViewModelProviderInherited<T>>();
    _ViewModelProviderInherited<T> provider =
        // 查询Element树中缓存的InheritedElement
        context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
    return provider?.viewModel;
  }

  static Type _typeOf<T>() => T;

  @override
  _ViewModelProviderState<T> createState() => _ViewModelProviderState<T>();
}

class _ViewModelProviderState<T extends BaseViewModel>
    extends State<ViewModelProvider<T>> {
  @override
  Widget build(BuildContext context) {
    return _ViewModelProviderInherited<T>(
      child: widget.child,
      viewModel: widget.viewModel,
    );
  }

  @override
  void dispose() {
    widget.viewModel.dispose();
    super.dispose();
  }
}

// InheritedWidget可以被Element树缓存
class _ViewModelProviderInherited<T extends BaseViewModel>
    extends InheritedWidget {
  final T viewModel;

  _ViewModelProviderInherited({
    Key key,
    @required this.viewModel,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;

2.2 DataModel

import 'dart:convert';

import 'package:pupilmath/datamodel/base_network_response.dart';
import 'package:pupilmath/datamodel/challenge/challenge_ranking_list_item_data.dart';
import 'package:pupilmath/utils/text_utils.dart';

///历史榜单
class ChallengeHistoryRankingListResponse
    extends BaseNetworkResponse<ChallengeHistoryRankingData> {
  ChallengeHistoryRankingListResponse.fromJson(Map<String, dynamic> json)
      : super.fromJson(json);

  @override
  ChallengeHistoryRankingData decodeData(jsonData) {
    if (jsonData is Map) {
      return ChallengeHistoryRankingData.fromJson(jsonData);
    }
    return null;
  }
}

class ChallengeHistoryRankingData {
  String props;
  int bestRank; //最佳排名
  int onlistTimes; //上榜次数
  int total; //总共挑战数
  List<ChallengeHistoryRankingItemData> ranks; //先给10天

  //二维码
  String get qrcode =>
      TextUtils.isEmpty(props) ? '' : json.decode(props)['qrcode'] ?? '';

  ChallengeHistoryRankingData.fromJson(Map<String, dynamic> json) {
    props = json['props'];
    bestRank = json['bestRank'];
    onlistTimes = json['onlistTimes'];
    total = json['total'];
    if (json['ranks'] is List) {
      ranks = [];
      (json['ranks'] as List).forEach(
          (v) => ranks.add(ChallengeHistoryRankingItemData.fromJson(v)));
    }
  }
}

///历史战绩的item
class ChallengeHistoryRankingItemData {
  ChallengeRankingListItemData champion; //当天最好成绩
  ChallengeRankingListItemData user;

  ChallengeHistoryRankingItemData.fromJson(Map<String, dynamic> json) {
    if (json['champion'] is Map)
      champion = ChallengeRankingListItemData.fromJson(json['champion']);
    if (json['user'] is Map)
      user = ChallengeRankingListItemData.fromJson(json['user']);
  }

2.3 View

import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:pupilmath/datamodel/challenge/challenge_history_ranking_list_data.dart';
import 'package:pupilmath/entity_factory.dart';
import 'package:pupilmath/network/constant.dart';
import 'package:pupilmath/network/network.dart';
import 'package:pupilmath/utils/print_helper.dart';
import 'package:pupilmath/viewmodel/base/abstract_base_viewmodel.dart';
import 'package:rxdart/rxdart.dart';

//每日挑战历史战绩
class ChallengeHistoryListViewModel extends BaseViewModel {
  BehaviorSubject<ChallengeHistoryRankingData> _challengeObservable =
      BehaviorSubject();

  Stream<ChallengeHistoryRankingData> get challengeRankingListStream =>
      _challengeObservable.stream;

  @override
  void dispose() {
    _challengeObservable.close();
  }

  @override
  void doInit(BuildContext context) {
    refreshData(context);
  }

  @override
  Future refreshData(BuildContext context) {
    return _loadHistoryListData();
  }

  _loadHistoryListData() async {
    Map<String, dynamic> parametersMap = {};
    parametersMap["pageNum"] = 1;
    parametersMap["pageSize"] = 10; //拿10天数据

    handleDioRequest(
      () => NetWorkHelper.instance
          .getDio()
          .get(challengeHistoryListUrl, queryParameters: parametersMap),
      onResponse: (Response response) {
        ChallengeHistoryRankingListResponse rankingListResponse =
            EntityFactory.generateOBJ(json.decode(response.toString()));

        if (rankingListResponse.isSuccessful) {
          _challengeObservable.add(rankingListResponse.data);
        } else {
          _challengeObservable.addError(null);
        }
      },
      onError: (error) => _challengeObservable.addError(error),
    );
  }

  Future<ChallengeHistoryRankingData> syncLoadHistoryListData(
    int pageNum,
    int pageSize,
  ) async {
    Map<String, dynamic> parametersMap = {};
    parametersMap["pageNum"] = pageNum;
    parametersMap["pageSize"] = pageSize;

    try {
      Response response = await NetWorkHelper.instance
          .getDio()
          .get(challengeHistoryListUrl, queryParameters: parametersMap);
      ChallengeHistoryRankingListResponse rankingListResponse =
          EntityFactory.generateOBJ(json.decode(response.toString()));
      if (rankingListResponse.isSuccessful) {
        return rankingListResponse.data;
      } else {
        return null;
      }
    } catch (e) {
      printHelper(e);
    }
    return null;
  }

2.4 一些基础架构

2.5 View 和 ViewModel 如何实现初始化和相互作用

2.6 Flutter 业务架构抽离

如果是统一系列的产品业务形态,还可以抽离出一套核心的架构,复用在同样的生产产品线上,例如当前产品线以教育为主,利用 Flutter 的一码多端性质,则可以把题版生产工厂、渲染题版引擎、 适配框架、 以及跨端接口的框架都抽离出来,迅速地形成可以推广复用的模板,可以事半功倍地解决掉业务上的试错成本问题,当然,其他产品性质的业务线均可如此。

三、Flutter 适配

任何框架中的 UI 适配都是特别繁重的工作,跨端上的适配更是如此,因此在同一套布局里面,各个平台的换算过程显得尤为重要,起初的时候,Flutter 中并没有提供某种诸如 dp 或者 sp 的适配方式,而且考虑到直接更改底层 Matrix 换算比例的话可能会让原本高清分辨率的手机显示不是那么清楚,而 Flutter 的宽高单位都是 num,最后编译的时候才会去对应到各个平台的单位尺寸。

为了减轻设计师的设计负担,这里通常使用一套 iOS 的设计稿即可,以375 x 667的通用设计稿为例,转换过来到android上是360 x 640 (对应1080 x 1920),这里flutter的单位也是和对应手机的像素密度有关的。

3.1 构造一个转换工具类:

//目前适配iPhone和iPad机型尺寸
import 'dart:io';
import 'dart:ui';
import 'dart:math';

import 'package:pupilmath/utils/print_helper.dart';

bool initScale = false;
//针对iOS平台的scale系数
double iosScaleRatio = 0;
//针对android平台的scale系数
// (因为所有设计稿均使用iOS的设计稿进行,所以需要转换为android设计稿上的尺寸,
// 否则无法进行小屏幕上的适配)
double androidScaleRatio = 0;
//文字缩放比
double textScaleRatio = 0;

const double baseIosWidth = 375;
const double baseIosHeight = 667;
const double baseIosHeightX = 812;

const double baseAndroidWidth = 360;
const double baseAndroidHeight = 640;

void _calResizeRatio() {
  if (Platform.isIOS) {
    final width = window.physicalSize.width;
    final height = window.physicalSize.height;
    final ratio = window.devicePixelRatio;
    final widthScale = (width / ratio) / baseIosWidth;
    final heightScale = (height / ratio) / baseIosHeight;
    iosScaleRatio = min(widthScale, heightScale);
  } else if (Platform.isAndroid) {
    double widthScale = (baseAndroidWidth / baseIosWidth);
    double heightScale = (baseAndroidHeight / baseIosHeight);
    double scaleRatio = min(widthScale, heightScale);
    //取两位小数
    androidScaleRatio = double.parse(scaleRatio.toString().substring(0, 4));
  }
}

bool isFullScreen() {
  return false;
}

//缩放
double resizeUtil(double value) {
  if (!initScale) {
    _calResizeRatio();
    initScale = true;
  }

  if (Platform.isIOS) {
    return value * iosScaleRatio;
  } else if (Platform.isAndroid) {
    return value * androidScaleRatio;
  } else {
    return value;
  }
}

//缩放还原
//每个屏幕的缩放比不一样,如果在iOS设备上出题,则题目坐标值需要换算成原始坐标,加载的时候再通过不同平台换算回来
double unResizeUtil(double value) {
  if (iosScaleRatio == 0) {
    _calResizeRatio();
  }

  if (Platform.isIOS) {
    return value / iosScaleRatio;
  } else {
    return value / androidScaleRatio;
  }
}

//文字缩放大小
_calResizeTextRatio() {
  final width = window.physicalSize.width;
  final height = window.physicalSize.height;
  final ratio = window.devicePixelRatio;
  double heightRatio = (height / ratio) / baseIosHeight / window.textScaleFactor;
  double widthRatio = (width / ratio) / baseIosWidth / window.textScaleFactor;
  textScaleRatio = min(heightRatio, widthRatio);
}

double resizeTextSize(double value) {
  if (textScaleRatio == 0) {
    _calResizeTextRatio();
  }
  return value * textScaleRatio;
}

double resizePadTextSize(double value) {
  if (Platform.isIOS) {
    final width = window.physicalSize.width;
    final ratio = window.devicePixelRatio;
    final realWidth = width / ratio;
    if (realWidth > 450) {
      return value * 1.5;
    } else {
      return value;
    }
  } else {
    return value;
  }
}

double autoSize(double percent, bool isHeight) {
  final width = window.physicalSize.width;
  final height = window.physicalSize.height;
  final ratio = window.devicePixelRatio;
  if (isHeight) {
    return height / ratio * percent;
  } else {
    return width / ratio * percent;
  }

3.2 具体使用:

这样每次如果有分辨率变动或者适配方案变动的时候,直接修改 resizeUtil 即可,但是这样带来的问题就是,在编写过程中单位变得很冗长,而且不熟悉团队工程的人会容易忘写,导致查错时间变长,代码侵入性较高,于是利用 dart 语言的扩展函数特性,为 resizeUtil 做一些改进。

3.3 低侵入式的 resizeUtil

通过扩展 dart 的 num 来构造想要的单位,这里用 dp 和 sp 来举例,在 resizeUtil 中加入扩展:

extension dimensionsNum on num {
  ///转为dp
  double get dp => resizeUtil(this.toDouble());

  ///转为文本大小sp
  double get sp => resizeTextSize(this.toDouble());

  ///转为pad文字适配
  double get padSp => resizePadTextSize(this.toDouble());

然后在布局中直接书写单位即可:

四、Flutter 中的一些坑

4.1 泛型上的坑

刚开始在移动端上使用泛型来做数据的自动解析时,使用了 T.toString 来判断类型,但是当编译成 web 的 release 版本时,在移动端正常运行的程序在web上无法正常工作:

刚开始的时候把目标一直定位在编译的方式上,因为存在 dev profile release 三种编译模式,只有在 release 上无法运行,误以为是 release 下编译有 bug,随着和 Flutter 团队的深入讨论后,发现其实是泛型在 release 模式下的坑,即在 web 版本的 release 模式下,一切都会进行压缩(包含类型的定义),所以在 release 下,T.toString() 返回的是 null,因此无法识别出泛型特征,具体的讨论链接:

Flutter application which use canvas to build self-CustomPainter cannot work on browser if i used the release mode by command "flutter run -d chrome --release" or "flutter build web". · Issue #47967 · flutter/flutter​github.com

In release mode everything is minified, the (T.toString() == "Construction2DEntity") comparison fails and you get entity null returned.
If you change the code to (T ==Construction2DEntity) it will fix your app.

最后建议,无论在何种模式下,都直接写成T==的形式最为安全。

class EntityFactory {
  static T generateOBJ<T>(json) {
    if (1 == 0) {
      return null;
    } else if (T == "ChallengeRankingListDataEntity") {
      /// 每日挑战排行榜
      return ChallengeHomeRankingListResponse.fromJson(json) as T;
    } else if (T == "KnowledgeEntity") {
      return KnowledgeEntity.fromJson(json) as T;
    }
  }

4.2 在编译成 web 产物后如何使用 iframe 来加载其他网页

对于移动端来说,webview_flutter 可以解决掉加载 web 的问题,不过编译成 web 产物后,已经无法直接使用 WebView 插件来进行加载,此时需要用到 dart 最初设计来编写网页的一些方式,即 HtmlElmentView:

import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'dart:html' as html;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
           child: Iframe()
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){},
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }
}
class Iframe extends StatelessWidget {
  Iframe(){
    ui.platformViewRegistry.registerViewFactory('iframe', (int viewId) {
      var iframe = html.IFrameElement();
      iframe.data-original='https://flutter.dev';
      return iframe;
  });
  }
  @override
  Widget build(BuildContext context) {
    return Container(
      width:400,
      height:300,
      child:HtmlElementView(viewType: 'iframe')
    );
  }

不过这种方式会带来新的底层刷新渲染问题(当鼠标移动到某个元素时,会不停地闪动刷新),目前在新的版本上已修复,有兴趣的同学可以看看:

https://github.com/flutter/fl...

4.3 Flutter 如何加载本地的 html 并且进行通信

内置 html 是很多工程的需求,很多网上的资料都是通过把本地的 html 做成数据流的方式然后加载进来,这种做法的兼容性很不好,而且编写过程中容易出现很多文件流过大无法读取的问题,其实这些做法都不是很舒适,我们应该通过 IFrameElement 来进行加载并通信,做法和前端很类似:

4.4 在 iOS 13.4 上 WebView 的手势无法正常使用

官方的 webview_flutter 在上一个版本当 iOS 升级到13.4之后会出现手势被拦截且无法正常使用的情况,换成flutter_webview_plugin后暂时解决掉该问题(目前 WebView 已经做了针对性的修复,但是还未验证),但是 flutter_webview_plugin 在 iOS 上又无法写入 user-agent,目前可以通过修改本地的插件代码进行解决:

文件位置为:

flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_webview_plugin-0.3.11/ios/Classes/FlutterWebviewPlugin.m修改内容为在146行(initWebview方法中初始化WKWebViewConfiguration后)添加如下代码if (@available(iOS 9.0, *)) {if (userAgent != (id)[NSNull null]) {self.webview.customUserAgent = userAgent;}}

关于 webview_flutter 的手势问题还在不断的讨论中:

https://github.com/flutter/fl...

五、关于布局和运算

5.1 容器 Widget 和渲染 Widget

5.2 GlobalKey

通过 GlobalKey 获取 RenderBox 来获取渲染出的控件的 size 和 position 等参数:

5.3 浮点运算

在 dart 的浮点运算中,由于都是高精度的 double 运算,当运算长度过长的时候,dart 会自动随机最后的一位小数,这样会导致每一次有些浮点运算每一次都是不确定的,这时需要手动进行精度转换,例如在计算两条线段是否共线时:

5.4 Matrix 的平移和旋转

在矩阵的换算过程中,如果使用普通的matrix.translate,会导致 rotate 之后,再进行 translate 会在旋转的基数上面做系数叠加平移运算,这样计算后得到的不是自己想要的结果,因此如果运算当中有 rotate 操作时,应当使用 leftTranslate 来保证每次运算的独立性:

六、项目优化

6.1 避免 build() 方法耗时:

6.2 重绘区域优化:

6.3 尽量避免使用 Opacity

6.4 Flutter的单线程模型

优先全部执行完 Microtask Queue 中的 Event,直到 Microtask Queue 为空,才会执行 Event Queue 中的 Event。

6.5 耗时方法放在 Isolate

Isolate 是 Dart 里的线程,每个 Isolate 之间不共享内存,通过消息通信。

Dart 的代码运行在 Isolate 中,处于同一个 Isolate 的代码才能相互访问。

七、杂谈总结

经历了对 Flutter 长期的探索和项目验证,目前对 Flutter 有自己的一些杂谈总结:

7.1

Flutter 在移动端的表现还是很不错的,在运行流畅度方面也是非常棒,经过优化过后的带大量图像运算的 app 运行在2013年的旧 Android 手机上面依然十分流畅,iOS 的流畅程度也堪比原生。

7.2

对于 web 的应用来说,Flutter 还在不断地改进,其中还有很多的坑没有解决,这里包括了移动端的 WebView 以及编程成的 web 应用,还不适合大面积的投入到 web 的生产环境中。

7.3

关于和 Native 的混编,为了避免产生混合栈应用中的内存问题和渲染问题等,建议尽量将嵌入原生的 Flutter 节点设计在叶子节点上,即业务栈跳转到 Flutter 后尽量完成结束后再回到Native栈中。

7.4

基于“去桥”的原生编译方式,Flutter 在未来各个平台上的运行应该会充满期待,目前验证的移动端应用打包成 Windows 应用后,运行表现还是很不错的,当然一些更大型的应用需要时间去摸索和完善。

7.5

语法方面,Flutter 中的 dart 正在变得越来越简单,也在借鉴一些优秀的前端框架上的语法,例如 react 等,kotlin 中也有很多相似的地方,感觉 Flutter 团队正在努力地促进大前端时代的发展。


总之,Flutter 确实带来了很多以前的跨端方案没法满足的惊喜的地方,相信不久的将来一码多端会变得越来越重要,特别是在新业务的探索成本上表现得十分抢眼。

以上是一些对 Flutter 的一些粗浅的总结,欢迎有兴趣的小伙伴一起探讨。

网易有道,与你同道,因为热爱所以选择, 期待志同道合的你加入我们。
  • END -
查看原文

赞 13 收藏 8 评论 4

杨成功 收藏了文章 · 1月23日

普通函数和构造函数的区别

构造函数

构造函数:如用函数用来初始化(使用new运算符)一个新建的对象,我们称之为构造函数(constructor)

普通函数

普通函数:不使用new运算符的函数就是普通函数

构造函数vs普通函数

从形式上看,一个函数被作为构造函数还是普通函数执行的唯一区别就是是否使用new

function Person(){
    this.name = "zqq";
    this.age = 28;
}
var p = new Person();

当以new调用构造函数(执行var p = new Person())时,函数内部会发生以下情况:

1.创建一个空对象

var p = {};

2.this变量指向对象p

Person.call(p)

3.p继承了构造函数Person()的原型

p.__proto__ = Person.prototype

4.执行构造函数Person()内的代码

构造函数和普通函数的区别:

1.构造函数使用new关键字调用;普通函数不用new关键字调用;

var p = new Person();
var p = Person();

2.构造函数内部可以使用this关键字;普通函数内部不建议使用this,因为这时候this指向的是window全局对象,这样无意间就会为window添加了一些全局变量或函数

2.1 在构造函数内部,this指向的是构造出来的新对象
2.2 在普通函数内部,this指向的是window全局对象

3.构造函数默认不用return返回值;普通函数一般都有return返回值

3.1 构造函数会默认返回this,也就是新的实例对象

3.2 普通函数如果没有return值的话,返回undefined

3.3 如果使用了return,那返回值会根据return值的类型而有所不同

(这里涉及到要了解的知识点有基本数据类型和引用类型的区别)

3.3.1 return的是五种简单数据类型:String,Number,Boolean,Null,Undefined的话,构造函数会忽略return的值,依然返回this对象;而普通函数会返回return后面的值

function Person(){
    var a;
    this.name = "zqq";
    this.age = 28;
    return a;
}
var p = new Person();//返回this对象
var p = Person();//因为a没初始化,所以a是undefined,而undefined属于简单数据类型,所以返回undefined,String,Number,Boolean,Null同理

3.3.2 如果return的是引用类型:Array,Date,Object,Function,RegExp,Error的话,构造函数和普通函数都会返回return后面的值

function Person(){
    var arr = [];
    this.name = "zqq";
    this.age = 28;
    return arr;
}
var p = new Person();//返回arr空数组,Date,Object,Function,RegExp,Error同理
var p1 = new Person();//返回arr空数组,Date,Object,Function,RegExp,Error同理

4.构造函数首字母建议大写;普通函数首字母建议小写

参考资料:
https://my.oschina.net/u/2395...
http://www.jb51.net/article/4...

查看原文

杨成功 关注了用户 · 1月22日

有道技术团队 @youdaotec

公众号:有道技术团队

网易有道是中国领先的智能学习公司,致力于提供100%以用户为导向的学习产品和服务。
旗下有网易有道词典、有道精品课、有道云笔记、有道翻译官等多款深受用户喜爱的产品。

关注 2230

杨成功 发布了文章 · 1月13日

【js】获取 blob 实现批量下载 pdf

背景

最近有一个单独和批量下载 pdf 的文件的需求。单独下载首先想到的是用 a 标签 download 属性直接下载。实践中发现,浏览器会默认打开pdf文件,而不是直接下载。批量下载需要压缩,这一步也需要在前端实现。

这里记录下我的实现方式。适用于绝大部分文件。

准备工作

这里用到了两个 npm 包:

  • jszip
  • file-saver

jszip 用于压缩。file-saver 用于在前端保存文件。

npm install file-saver jszip --save

安装好之后,先实现单独下载。

单独下载

单独下载 pdf,只用 file-saver 就可以了。

import { saveAs } from 'file-saver';

function save() {
    saveAs(blob, filename);
}

saveAs 方法有两个参数,第二个参数是下载的文件名,第一个参数就比较难获取了,是一个 Blob 对象

什么是 Blob?传送门

现在的情况是接口返回了pdf的URL,然后我要用这个url获取对应PDF的blob。

怎么做?当然是 ajax

ajax 获取blob

直接贴上代码:

const getPdfBlob = (url: string) => {
    return new Promise((resolve, reject)=> {
        let xhr = new XMLHttpRequest()
        xhr.open('get', url+'?t='+Math.random(), true);
        xhr.setRequestHeader('Content-Type', `application/pdf`);
        xhr.responseType = 'blob';
        xhr.onload = function () {
            if (this.status == 200) {
                //接受二进制文件流
                var blob = this.response;
                resolve(blob);
            }
        }
        xhr.send();
    })
}

首先,写一个原生的 XMLHttpRequest ,方法为 get,url 中的 t 参数是为了阻止缓存。然后设置 responseType 为 blob,最后接受回来的就是 blob 数据。

为了后面批量使用,getPdfBlob 函数内部用 promise 包了一下。

使用方法:

getPdfBlob(url).then(blob => {
    saveAs(blob, filename);// 拿到 blob 并下载 pdf
})

单独下载搞定!

批量下载

首先,批量下载要压缩成zip包之后下载,所以要用到 jszip

这里写了一个将批量文件压缩为一个zip的方法:

import JSZip from 'jszip'
import { saveAs } from 'file-saver';

getMultiZip(blobs)=> {
    var zip = new JSZip();
    blobs.forEach(blob=> {
        // 添加要压缩的pdf
        zip.file('单个pdf文件名.pdf', blob, { binary:true });
    })
    zip.generateAsync({type:'blob'}).then(function(content) {
        //生成zip并保存
        saveAs(content, '批量pdf文件名.zip');
    });
}

有了这个方法之后,接下来批量获取 blob。

单独下载需要发起一个 ajax,批量下载,就要每个 URL 都发起 ajax。

因为前面的 getPdfBlob 包装了promise,所以批量获取就可以这样写:

Promise.all(
  pdfUrlList.map(url=> getPdfBlob(url))
).then(res=> {
    // res结构:[blob, blob, blob, ...]
    getMultiZip(res)
})

这里用 Promise.all 的好处是可以并行发起请求,等最后一个请求结束后拿到所有的 blob,比循环执行 ajax 高效的多。

批量下载搞定!

查看原文

赞 1 收藏 1 评论 0

杨成功 发布了文章 · 1月12日

【js思考课】梳理一波函数的花式用法

概述

函数的用法眼花缭乱,即便工作了很多年的人,熟悉各种框架的函数用法,但是未必知道为什么要这么用,为什么换一种用法就不可以。

这节思考课,就是从最简单的一个demo开始,逐渐展开思考,一步步尝试来理解它。

1. 基础实验

先上基础代码:

var variable
function showText(text) {
    console.log(text);
}

基础代码只有一个变量,一个函数。下面依据基础代码进行实验

函数调用与赋值

首先,调用函数并赋值给变量:

var variable
function showText(text) {
    console.log(text);
}
variable = showText('基础代码执行')
console.log(variable)

直接运行上面的代码,结果如下:

image.png

说明 showText 方法调用了。

结论一:

  • 变量赋值为一个函数调用时,函数会立即执行
  • 函数内没有return,相当于变量没有赋值(因为还是undefind)

稍稍变一下:

var variable
function showText(text) {
    console.log(text);
}
variable = showText
variable('变量赋值后调用')
console.log(variable)

再看运行结果:

image.png

结论二:

  • 变量赋值为一个函数名时,变量就指向了这个函数
  • 变量调用等于函数调用
变量调用没这个说法,姑且这么说是为了理解,就是变量名加一对括号:variable()

匿名函数调用与赋值

将变量赋值为一个匿名函数(也就是函数表达式)

var variable
function showText(text) {
    console.log(text);
}
variable = function () {
    showText('函数调用')
}
console.log(variable)
variable()

运行结果:

image.png

结论三:

  • 变量赋值为一个匿名函数时,变量就指向了这个函数
  • 赋值时匿名函数不会立即调用
  • 变量调用等于匿名函数调用

根据这三个结论,我们可以发现:

function showText(text) {
    console.log(text);
}
// 基本等同于
var showText = function(text) {
    console.log(text);
}
// 统一调用方式为:
showText('txt');

了解了基础,下面我们来看项目实践

2. react 中的用法

先来一段基础的 react 代码

import React from 'react'

const TestPage = (props) => {
    const testFun = ()=> {
        console.log('函数执行')
    }
    return (
        <div onClick={testFun}></div>
    )
}

export default TestPage;

点击事件等于函数名,函数正常触发。

应该有人试过写成这样:

<div onClick={testFun()}></div>

这样写就不行了,函数一开始就触发了,但是点击时候没反应。

但是写成这样就又正常了:

<div onClick={()=> testFun()}></div>

为什么会这样?结合第一节来分析一波。

写法 1 分析

为什么这样写可以:

<div onClick={testFun}></div>

因为这个写法就等于:

div.onClick = testFun

div.onClick 看做一个变量,再结合 结论二 可以得知:

div.onClick() == testFun()

所以在 div 执行 onClick() 时就调用了 testFun();

写法 2 分析

再看这个写法:

<div onClick={testFun()}></div>

和上一步一样的拆解

div.onClick = testFun()

结合 结论一 可以得知,testFun() 会立即执行,但是 div.onClick 等于 undefined 相当于未赋值,所以点击没反应。

写法 3 分析

<div onClick={()=> testFun()}></div>

拆解后:

div.onClick = ()=> testFun()
// 等于 es5 写法:
div.onClick = function() {
    testFun()
}

结合 结论三 可知:

匿名函数不会立即执行,onClick() 时触发 testFun(),所以正常运行

写法 4 分析

改造一波,给 div 加一段显示内容

const getName = ()=> {
    return '我是一个div'
}
<div onClick={testFun}>{getName()}</div>

发现了吗,这里又不一样了。

标签内 getName() 这么写是对的,光写函数名反而是错的,这与事件里使用函数的方法正好相反。

为什呢?因为标签内的内容就是要立即触发函数来得到的,不需要在指定条件下触发。

查看原文

赞 2 收藏 2 评论 0

认证与成就

  • 获得 158 次点赞
  • 获得 69 枚徽章 获得 4 枚金徽章, 获得 22 枚银徽章, 获得 43 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-12-02
个人主页被 9.4k 人浏览