数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯

科技学院

科技百科

科技书籍

网站大全

软件大全

Firefox 浏览器在看虎牙火猫看直播时,经常大概率过一小段时间( 20 分钟左右)直播网页就会直接空白转圈,刷新都没用,只能关闭标签页再重新打开,真的无语,用 chrome 就不会有问题,改 firefox 的 ua 照样还是没用,难道 Firefox 用户在国内已经少到可以忽略了吗?
前沿探索
2020-08-20 23:34:49
例如我想把设置页中 "清除历史记录" 按钮放在书签工具栏上.https://tiebapic.baidu.com/forum/pic/item/d01373f082025aaf9b25d1a1ecedab64034f1a40.jpg
前沿探索
2020-08-20 23:34:39
https://github.com/ustclug/mirrorrequest/issues/251
如何投票
请在 Github Issue 页面使用 Emoji 👍 表情来投票
前沿探索
2020-08-20 23:34:28
macOS 和 Firefox 都是最新版,不知道是哪个版本开始出现这个问题。
不管是用 Proxy SwitchyOmega 还是内置的代理功能都会泄露本地 DNS。
network.proxy.socks_remote_dns 为 true
前沿探索
2020-08-20 23:33:58
我用的 firefox 最新版 每次搜索一个内容后 搜索框上都会有个询问 您是要访问 XX 吗? 请问这个怎么完全关闭,在设置找了半天也没发现
前沿探索
2020-08-20 23:33:50
macOS 10.15.3 Firefox 73.0.1 目前不管你是什么外观,它的滚动条都是源生的,不会根据主题变化。这个功能什么时候加上去啊?
前沿探索
2020-08-20 23:33:30
发送通知权限已设置为允许,但还是无法弹出通知,控制台提示“通知权限只能从安全的上下文请求”,这种情况该怎么解决?另外,传统版 Edge 可以弹出,Chrome 没测试。
前沿探索
2020-08-20 23:33:00
我的预期是,在网页中点开的新标签页就在当前页面的右边打开,这个可以通过设置中的 browser.tabs.insertAfterCurrent 改为 true 来实现,but,这样设置之后, 点+号新建的标签页也是出现在当前标签页的右侧,而不是最右侧。 另外还有个问题更蛋疼,我现在网页中点开一个新标签页,如果当前打开的标签页中有相同网址的标签页(猜测的),这个新标签页会跳过去。。。
蛋疼的一逼呀,我是真的想支持一下 Firefox,但它就不能让我像用 Chrome 那样省点心吗。。
前沿探索
2020-08-20 23:32:48
Firefox 起一个 Youtube 开全屏,然后在另一个屏幕上将 Firefox 窗口变为激活,这个时候上方的系统菜单栏消失不见.很难受,有人知道如何解决吗?
前沿探索
2020-08-20 23:32:42
我们的需求是做一套响应式的业务系统,也可能不止一套,需要响应式,还需要支持用 Cordova 打包起来跑在 iOS 和 Android 上面,简单地调用通信录什么的在电脑上也要支持常见的几个浏览器,不用兼容很旧的浏览器 现在找了几家: - https://material.angular.io/ - https://www.primefaces.org/primeng - https://vmware.github.io/clarity/ 看起来功能都好强大的,不知道哪个更适合我们的需求,大家有什么推荐没
前沿探索
2020-08-20 23:31:09
想写一个 Flutter 图表插件,苦于起名。
目前卡在 flutter create xxx -t=plugin 这一步。
求一个好听易记的名字 😝
前沿探索
2020-08-20 23:29:37
大家好,我们是字节跳动 Flutter 基础架构团队,致力于为字节旗下全系产品提供高品质的跨平台技术,目前公司内已经有 30+ 个业务在使用 Flutter 引擎技术,包括但不限于头条、火山、西瓜视频等业务。
我们团队负责整个公司 Flutter 的通用平台建设和技术优化的工作,可以理解为大家所熟悉的技术中台,我们在优化 Flutter 引擎性能与稳定性、强化 Flutter 容器能力、丰富组件库、改善研发体验、探索多端一体化场景等各方向都投入大量人力,并且已经取得不错的成果。
字节跳动全球推出了多款有影响力的产品,包括抖音、今日头条、西瓜视频、TikTok 、TopBuzz 等,旗下全线产品总 MAU (月活跃用户)超过 15 亿,已覆盖全球 150 个国家和地区、曾在 40 多个国家和地区排在应用商店总榜前列。公司目前处于高速发展期,坚信 Flutter 技术能给公司更多的产品和开发者带来价值。当然,如果不了解 Flutter,可以看看我的博客 http://gityuan.com/flutter/gityuan.com/flutter,带来初窥 Flutter 的技术魅力。
Flutter 基础架构团队大牛如云,技术氛围浓厚,追求极致,和优秀的人做有挑战的事。Flutter 基础架构团队欢迎你的加入,不需要有任何 Flutter 经验,只需要熟悉 Android/iOS/Web 等任一技术栈即可,简历至 [email protected] 。
很高兴掘金提供了这次跟大家一起交流的机会,大家可以问我们关于「 Flutter 技术」、「跨平台技术」、「客户端开发」、「职业发展」、「个人成长」相关方面的问题。我们会在 2020/7/27-2020/7/29 期间,挑选出有价值有意义的问题进行回答。
前沿探索
2020-08-20 23:29:28
RTRTRTRT
现在每次调试 Flutter 上的 SQLite 都得导出到电脑上,然后再操作,搜了一下市场,Stetho 只能调试网络。
找了一圈,发现一个 SQLHelper 的 AndroidStudio 插件,安装上之后也没找到在哪里使用。。。
请问你们是怎么调试的啊
前沿探索
2020-08-20 23:29:15
前段时间公司决定使用 flutter 做移动端开发,由于开发同学是前端转 flutter 对 android 不熟,所以有了让我协助写几个 flutter 插件这样的诉求。
请各位不吝赐教顺手 star
百度人脸识别和活体检测 flutter 插件
Google reCAPTCHA flutter 插件
配置读取工具库
前沿探索
2020-08-20 23:29:06
TabBar 的当前索引假设为 2, 当我进入下一个页面, 再退回来的时候, 显示当前活跃的索引就变成了 0 了, 但实际还是 2, 因为左右滑的时候是跳到 1 或 3, 后来从网上看了下别人的一些 demo, 发现有些也存在这种问题?
前沿探索
2020-08-20 23:28:47
cronStock() async { _stockCode = _stockCodeController.value.text; _growRate = _growRateController.value.text; _reduceRate = _reduceRateController.value.text; …… saveData(); refreshStock(true); await AndroidAlarmManager.periodic(const Duration(seconds: 30), periodicAlarmID, refreshStock, wakeup: true); } saveData() async { SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.reload(); try { print('0001 saveData: '+DateTime.now().toString()); prefs.setString('stock_prefix', stockPrefix); prefs.setString('stock_code', _stockCodeController.value.text); prefs.setDouble('grow_rate', double.parse(_growRateController.value.text)); prefs.setDouble('reduce_rate', double.parse(_reduceRateController.value.text)); } on FormatException catch(e) { } catch (e) { // No specified type, handles all } } …… refreshStock([bool isManual = false]) { print('0002 refreshStock afterTimer: '+DateTime.now().toString()); Future stockShared = getShared(); stockShared.then((List shared) { print('isManual: $isManual'); print('shareData: $shared'); }); } Future getShared() async { print('getShared'); SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.reload(); return [ prefs.getString('stock_prefix'), prefs.getString('stock_name'), prefs.getString('stock_code'), prefs.getDouble('grow_rate'), prefs.getDouble('reduce_rate'), // 当前值 prefs.getString('stock_price'), prefs.getDouble('stock_rate'), prefs.getInt('rate_color'), ]; }
AndroidAlarmManager 插件用到了 Isolate 环境,在 Isolate 环境下 refreshStock 函数中的 getShared()拿到的 SharedPreferences 数和上面 saveData()存储的数据不同步,要多次间隔调用的后才会同步到。
前沿探索
2020-08-20 23:28:23
原文地址: https://blog.codemagic.io/releasing-your-flutter-desktop-application/
原文作者: https://medium.com/@rody.davis.jr
发布时间:2020 年 6 月 16 日
所以你建立了你的第一个 Flutter 应用,并在 iOS 设备的 AppStore 和 Android 设备的 Google Play 上发布。然后,你想接触更多的受众和目标网络,所以你用静态主机发布了它。但你仍然想要更多。如果你想要移动设备的性能,但又想要 web 的响应速度,那么桌面版就是答案。
目前的选择
对于桌面,你有几个选择来发布--但它们都有一定的权衡。在这里,我们将讨论为什么你可能想选择一个而不是另一个。
Electron
你可能以前听说过,因为目前 MacOS 和 Windows 上的大部分第三方应用都是用它发布的。Electron 是一个由 chromium 驱动的浏览器,它使用 Node.js 将网络浏览器与文件系统粘合在一起。你可以得到为网络开发的好处,但又有桌面的灵活性。它使用大量的 JS 来做到这一点,所以你失去了 AOT (提前编译) ,这意味着没有剪枝,优化或一般性能。
Flutter 在移动端工作得很好的原因是由于发布构建的 AOT 。你可以通过在根目录下添加一个 manifest 文件和一些额外的模板脚本,用 Electron 发布一个 Flutter 应用程序。您打包您的 Flutter Web 应用程序,就像部署到静态托管时一样。如果您想使用 FLUTTER_WEB_USE_SKIA 标志,您可以从 Web 版本中获得更好的性能。
单机版
无论你是用 Electron 、桌面嵌入还是自定义嵌入器来构建你的 Flutter 桌面应用,你都需要一种方法来向世界发布应用。如果你用 Codemagic 、Github Actions 或手动创建一个发布构建,你就把构建上传到 Amazon S3 或类似的 CDN,并把链接提供给客户。这种方法很好,因为你不必等待审查过程,也不必处理每个平台的非常具体的规则。然后,你可以把这个链接放在你的 Flutter Web 应用或 PWA 清单上,只要用户觉得合适,就可以提供原生体验。
这种方法的一个主要缺点是你如何处理更新。您需要使用一个库或自定义构建的解决方案,用于在后台下载和安装更新或通知用户新的更新。你给用户的步骤越多,他们完成所有步骤的可能性就越小。你以用户为代价换取了灵活性和可用性。
有一些不错的工具,比如 MacOS 和 Windows 的 Sparkle,它为你每天使用的很多应用程序提供了动力。每当你看到一个弹出窗口说一个更新已经准备好安装时,很有可能是在使用这个库。该库通过托管的 RSS 源运行,你可以通过解析来获取发布说明、版本和安装链接。该应用程序将尝试在后台为你自动安装它们,并在你下次启动时重新加载。
官方商店
现在,你可能会推迟的选择是发布到官方商店。在应用商店之外,可能有合法的理由,因为你可能没有遵循所有的指导方针,有一个自定义的部署和发布后台,企业应用只用于内部或有限的使用,或者你只是想在你的网站上有一个链接来下载应用。许多应用程序甚至在商店中提供应用程序,但也有一个在线版本,可能是测试频道或特殊构建。我发现这是一个很好的方法,因为例如 Mac AppStore 仍然没有像 iOS 那样为 MacOS 应用提供 TestFlight 。
部署到商店可能具有挑战性,但我相信最终是值得的,因为你获得了安全性和自动更新。他们还将处理付款和退款。在未来的文章中,我将会介绍向 Mac AppStore 发布 Flutter 应用程序,就像我在 iPadOS 和 MacOS 上发布新的 Widget Studio 一样。顺便说一下,Widget Studio 也可以作为一个 PWA 。我建议在 MacOS 上,你应该只包括你正在积极使用的权限,如果你想分享 Mac 和 iPad 应用的购买,你需要有相同的捆绑 ID 。你不会用 Catalyst,而是用当前的桌面嵌入来做这件事。
结束语
这是一个激动人心的时刻,以一种原生的方式将移动应用带到桌面,这在以前是不可能的。Flutter 很厉害,可以让你针对 MacOS 、Windows 和 Linux,现在由你来决定如何发布。Codemagic 支持 Mac 和 Linux ,这比 2 个复选框还要简单。如果你有任何问题,请告诉我,我期待着看到你的 Flutter 桌面应用程序!
Rody Davis Jr 是一名专业的全栈开发者,在企业和个人应用方面都有丰富的经验。他使用最新的框架为 App Store 、Google Play 、Web 和桌面创建应用程序。Rody 热爱 Flutter 、Web 和所有有创意的东西,并在 Medium 上写 Flutter 文章。他希望通过他的应用接触到尽可能多的人,并展示最新科技的可能性。
通过 www.DeepL.com/Translator (免费版)翻译
前沿探索
2020-08-20 23:28:10
需要定时运行一个函数获取 API 数据,用的 Timer.periodic 做间隔执行,但是测试发现正式包手机息屏后 Timer.periodic 就暂停了,真机调试模式下是没有问题的。后来换 android_alarm_manager 插件实现间隔调用还是有同样的问题。
前沿探索
2020-08-20 23:28:05
我这边沿袭了 web 的笨办法,在一个 webservice.dart 下面维护所有的接口 url,然后每一个功能模块 import 一次,各位有啥更先进的办法吗,比如作为一个环境变量来维护,刚上手对 dart 特性还不太熟悉,先谢谢了
前沿探索
2020-08-20 23:27:51
副标题:Ubuntu 团队已经为所有 Linux 发行版上的 Flutter 应用程序制作了一个新的基于 GTK+的主机。 原文地址: https://medium.com/flutter/announcing-flutter-linux-alpha-with-canonical-19eb824590a9
原文作者: https://medium.com/@csells
发布时间:2020 年 7 月 8 日
作者:Chris Sells (Google) & Ken VanDine (Canonical)
Google 对 Flutter 的目标一直是提供一个可移植的工具包,用于构建以原生速度运行的漂亮 UI,无论你的目标是哪个平台。为了验证这一能力,我们首先关注移动平台 Android 和 iOS,在这两个平台上,我们已经看到有超过 8 万个快速、漂亮的 Flutter 应用发布到 Google Play 。
为了在这一成功的基础上再接再厉,一年多来,我们一直在将我们的关注点扩展到包括桌面级体验,包括网络和桌面操作系统:macOS 、Windows 和 Linux 。这项工作包括对引擎进行大量重构,以支持桌面风格的鼠标和键盘输入,以及可调整大小的顶层窗口。它还包括新的 UI 功能,能很好地适应桌面,如 Material Density 支持和 NavigationRail ,以及在 Dart:FFI 中的实验和对系统菜单栏和标准对话框的访问,与底层桌面操作系统深度集成的实验。所有这些工作都是为了确保 Flutter 除了适合移动风格的体验外,还能处理全功能、全尺寸的桌面应用。
长期以来,我们的愿景是让 Flutter 为平台提供动力。我们已经看到这一点在谷歌通过 Assistant 等产品体现出来,所以现在我们很高兴看到其他人利用 Flutter 为更多平台提供动力。今天,我们很高兴与世界上最流行的桌面 Linux 发行版 Ubuntu 的发行商 Canonical 一起,共同宣布 Flutter 的 Linux alpha 的可用性。
为什么 Linux 要用 Flutter ?
去年,当 Google 宣布用 Flutter 支持桌面级应用时,Canonical 看到了一个令人兴奋的机会,使包括 Ubuntu 在内的 Linux 发行版成为对 Flutter 应用开发者有吸引力的目标平台。Flutter 的原生跨平台故事正在迅速发展,Canonical 希望成为先锋。通过在 Flutter 中实现对桌面 Linux 的支持,Canonical 让应用开发者可以非常容易地通过 Linux 的应用商店 Snap Store 为 Linux 用户发布应用。通过使 Linux 成为一流的 Flutter 平台,Canonical 正在邀请应用开发者向数百万 Linux 用户发布他们的应用,并扩大向他们提供高质量的应用。
关于 Flutter 的一些事情让 Canonical 感到兴奋。 快速增长的应用开发者生态系统 多平台支持 高度优化的本地应用 现代 UI 框架,支持声明式、反应式和可组合的 widget 。 使用 Visual Studio Code 、Android Studio 和 IntelliJ 的丰富开发平台。
谷歌最初宣布宣布支持 Flutter 桌面,首先是支持 macOS 的 alpha 版本,并计划支持 Linux 和 Windows 。Canonical 对 Flutter 进行了大量投资,专门成立了一个开发者团队,与谷歌的开发者一起为大多数 Linux 发行版带来最佳的 Flutter 体验。Canonical 将继续与谷歌合作,进一步完善对 Linux 的支持,并保持与其他支持平台的功能对等。
Flokk 。证明 Flutter 已经为桌面做好了准备。
为了证明 Flutter 已经为桌面做好了准备,我们与 gskinner 的设计师和开发人员合作,创建了一个创新的、漂亮的 Flutter 桌面应用。Flokk 是一个现实世界的应用,它可以使用现实世界的数据,特别是你的谷歌联系人列表。
https://youtu.be/cTFJcq7UTRY
除了能够管理你的联系人,包括搜索联系人、添加新的联系人和编辑现有的联系人,Flokk 还可以让你将 GitHub 和 Twitter 的手柄信息与你的联系人关联起来。
Flokk 通讯录应用是用 Flutter 打造的,针对的是桌面。
GitHub 和 Twitter 通知的显示,将你的联系人变成了你自己的个人社交网络。如果你在 Flokk Contacts 中没有看到你喜欢的社交网络,那么好消息是 Flokk 是 完全开源 的,所以你可以提交 PR 来添加你的收藏夹。
除了在社交领域的创新,Flokk 还使用 Flutter 功能,让人看起来感觉很好。仅举一例,深色主题不仅可以切换颜色,而且在切换时还会有动画变化。
Flutter 使 Flokk 能够利用流畅的动画、高性能的滚动和简单的主题。
Flokk Contacts 应用背后的创意团队是由 Grant Skinner 领导的,他以卓越的设计和创新用户体验的实现而闻名。对于在 Linux 上与 Flutter 的合作,Grant 这样说。 "构建 Flokk Contacts 应用是一件轻而易举的事情! 我们能够将我们之前在 Flutter 方面的所有专业知识应用到 Linux 上,几乎没有任何调整,应用程序运行得非常好。与 Canonical 团队合作是一次美妙的经历;他们热情、投入,并且热衷于让 Flutter 不仅适用于 Linux,而且适用于每个平台。这是一个了不起的项目,我很高兴能够用 Flutter 瞄准另一个主要的操作系统。" - Grant Skinner
如果你想在 Linux 机器上使用 Flokk 应用,你可以在 GitHub 上下载最新版本。或者,如果你正在运行 snapd,你可以从 Snap Store 下载 Flokk 应用。
在 Linux 上轻松安装 Flutter
现在你已经看到了 Flutter 对于桌面级应用的工作效果,尤其是在 Linux 上,你会想让它在自己的 Linux 机器上运行。为了使这一点尽可能的简单,我们很高兴在 Snap Store 中提供 Flutter SDK for Linux 作为 Snap 。Flutter SDK snap 提供了在您最喜欢的 Linux 发行版上开发 Flutter 应用程序所需的一切。不需要安装一堆开发依赖;只需安装 Flutter SDK snap 和您最喜欢的 IDE,您就拥有了为 Linux 创建、构建和发布应用程序所需的一切。
例如,如果你想开始为 Linux 开发 Flutter 应用程序,而你选择的 IDE 是 Visual Studio Code,这就是你在 Linux 终端需要做的一切。 $ snap install --classic flutter $ snap install --classic code $ code --install-extension dart-code.flutter 。
如果你也想使用 Linux 开发移动应用,你可以通过安装 Android SDK 或 Android Studio (其中包括 Android SDK )来实现。有关 Flutter SDK 作为快件的更多信息,请参阅 https://snapcraft.io/flutter 。
Flutter for Linux 桌面
一旦在 Linux 机器上安装了 Flutter SDK,要构建桌面应用就需要升级到 Flutter dev 或 master 通道。然后启用 Linux 桌面支持。 启用 Linux 桌面支持: $ flutter channel dev $ flutter upgrade $ flutter config --enable-linux-desktop
现在,当你创建一个新的 Flutter 项目时,你会得到一个 linux 子目录,让你在 Linux 桌面上运行应用程序。 $ flutter create counter $ cd counter $ flutter run -d linux
著名的 Flutter Counter 应用也能在 Linux 上运行得很好。
你将得到的是一个用 Flutter 构建的、运行在最新稳定版 GTK+上的闪亮的新 Linux 应用。如果你有一个现有的 Flutter 项目,你想在启用 Linux 后添加 Linux 支持,你可以像这样添加 linux 子目录。 $ cd my_flutter_app $ flutter create .
这将创建 linux 子目录与 Runner 项目,你需要在 Linux 桌面上构建和运行你的 Flutter 应用程序。
从 Flutter 访问 Linux 中的本地代码
除了通过编写 Dart 创建 Flutter 小部件来支持桌面外,你的 Linux 桌面应用程序还可以使用 平台通道 或 C/C++的 Dart 外函数接口 来访问所有的原生 Linux 。或者,如果你想重用已有的代码,你可以在 pub.dev ,Dart 和 Flutter 的包管理器网站上找到这些代码。在 pub.dev 上,你会发现大部分的包都是纯 Dart 的,其中大部分在 Linux 应用中工作得很好。有些包,被称为插件,其中有针对一个或多个平台的本地代码。作为此次发布的一部分,我们在 pub.dev 上发布了三个使用 Linux 本地功能的插件。 url_launcher :在提供的 URL 上启动默认浏览器。 shared_preferences : 在应用程序会话之间共享的用户偏好。 path_provider :特殊用途目录的路径信息,如下载、图片等。
这些插件中的每一个都可以供你在你的应用程序中使用,以及作为如何从你的 Flutter 代码中原生地访问 Linux 的一个例子,比如 url_launcher 的 Linux 实现 。
部署到 Snap Store
要将你的 Flutter 应用部署到 Snap Store,你首先需要安装 Snapcraft,这个工具你将用来构建和发布你的应用作为一个快照。 $ sudo snap install snapcraft --classic
要驱动 Snapcraft 工具,你需要在你的应用程序的项目目录下创建一个 snapcraft.yaml 文件。作为一个例子,这是 Flokk 的 snapcraft.yaml 文件。 name: flokk-contacts version: 1.0.1 summary: Flokk Contacts description: A fresh and modern Google Contacts manager that integrates with GitHub and Twitter. confinement: strict base: core18 grade: stable apps: flokk-contacts: command: flokk-contacts extensions: [flutter-master] plugs: - network parts: flokk-contacts: source: . plugin: flutter flutter-target: lib/main.dart # 应用程序的主入口点文件。
在你的 snapcraft.yaml 文件所在的目录下,你现在可以运行 snapcraft 来构建你的应用程序的 snap 。 $ snapcraft
如果一切顺利,这将在你当前的工作目录下生成一个文件,比如 flokk-contacts_1.0.1_amd64.snap 。 一旦你在 Snap Store 中 设置了发布账户 ,你就可以发布你的 snap 了。 $ snapcraft 登录 $ snapcraft register flokk-contacts. $ snapcraft upload flokk-contacts_1.0.1_amd64.snap --release edge
该命令将把应用程序上传到 Snap Store,并尝试将其发布到 边缘通道 中。一旦您的应用程序发布到边缘通道,就可以通过 Snap Store 桌面客户端或使用命令行进行安装。 $ snap install --edge flokk-contacts
有关构建你的第一个快照并在 Snap Store 中发布的更多细节,请参见 https://snapcraft.io/first-snap#flutter,获取指导教程。
Flutter Linux 桌面样本
Flokk 应用是一个针对 Linux 桌面的现实世界 Flutter 应用的优秀例子。如果想了解更简单的示例,你可以看看 照片搜索应用 ,它也是专门为展示桌面功能而打造的。
尝试 Linux 上的照片搜索示例
照片搜索是一款简单的在线照片搜索应用,它使用多个插件来接入原生平台功能,支持 macOS 和 Linux 。
如果想了解一个 Linux 桌面应用的例子,并有一步一步的说明,我推荐 《编写一个 Flutter 桌面应用》 codelab ,它指导你使用 OAuth 和 GraphQL 在 Flutter 中构建一个 GitHub 客户端。
Flutter 的 GitHub codelab 客户端应用在行动中。
对于一个更全面的应用,可以锻炼 Flutter 更多的表面积,以及提供几个小程序,我推荐 Flutter Gallery ,它是去年重新设计的,支持桌面以及手机。如果你想看看它的运行情况,你也可以在 Snap Store 上查看。
Flutter Gallary 样品可在 Snap 商店购买
又有一款桌面应用展现了 Flutter 有趣的一面,它是由 Thorsten Lorenz 打造的一款名为 batufo 的多人游戏。这款游戏以美丽的背景为背景,让来自世界各地的玩家实时对战。
在多个 Flutter 平台上实时播放
Thorsten 一直在构建这个游戏,以支持多个 Flutter 平台,包括 Linux,macOS,Android 和 iOS 。如果你想看看他是如何做到的,并跟随未来的更新,他将他的编码课程以 视频 的形式提供, 代码也可以在 GitHub 上获得 。如果要从 Linux 上安装游戏,你可以从 Snap Store 上把它拉下来。
概要
通过这个 alpha 版本以及 Google 和 Canonical 之间的紧密合作,Linux 开发者可以为他们选择的操作系统获得 Flutter 支持。通过 快照安装 Flutter SDK 。使用 Visual Studio Code 或 Android Studio 在 Linux 上构建和测试你的桌面应用。将您的应用部署到 Snap Store 。最新的细节,请看 flutter.dev 上的桌面页面 。最重要的是, 一定要提供反馈 ,这样我们才能继续让 Flutter 在 Linux 上做到最好,就像我们努力为每个支持的 Flutter 平台做的那样。
来自 Canonical 团队的 Flutter for Linux 是我们的梦想向前迈出的一大步,让 Flutter 成为构建应用的最佳方式,无论你的目标是哪个平台。针对桌面平台,使得 Flutter 引擎对谷歌本身无法直接支持的长尾设备的适应性更强,但我们计划继续为这些设备建立合作伙伴关系,并启用生态系统。
只要有设备需要快速、漂亮的应用,那就是我们希望 Flutter 出现的地方。
通过 www.DeepL.com/Translator (免费版)翻译
前沿探索
2020-08-20 23:27:43
前言
前几天写了一个 Fluter 插件 tcard ,用来实现类似于探探卡片的布局。效果如下,本文讲解如何使用 Stack 控件实现这个布局。
在线查看
初识 Stack
Stack 是一个有多子项的控件,它会将自己的子项相对于自身边缘进行定位,后面的子项会覆盖前面的子项。通常用来实现将一个控件覆盖于另一个控件之上的布局,比如在一张图片上显示一些文字。子项的默认位置在 Stack 左上角,也可以用 Align 或者 Positioned 控件分别进行定位。 Stack( children: [ Container( width: 100, height: 100, color: Colors.red, ), Container( width: 90, height: 90, color: Colors.green, ), Container( width: 80, height: 80, color: Colors.blue, ), ], )
Stack (Flutter Widget of the Week)
布局思路
要使用 Stack 实现这个卡片布局的大致思路如下 首先需要前,中,后三个子控件,使用 Align 控件定位在容器中。 需要一个手势监听器 GestureDetector 监听手指滑动。 监听手指在屏幕上滑动同时更新最前面卡片的位置。 判断移动的横轴距离进行卡片位置变换动画或者卡片回弹动画。 如果运行了卡片位置变换动画在动画结束后更新卡片的索引值。
卡片布局 创建 Stack 容器以及前,中,后三个子控件 class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { // 前面的卡片,使用 Align 定位 Widget _frontCard() { return Align( child: Container( color: Colors.blue, ), ); } // 中间的卡片,使用 Align 定位 Widget _middleCard() { return Align( child: Container( color: Colors.red, ), ); } // 后面的卡片,使用 Align 定位 Widget _backCard() { return Align( child: Container( color: Colors.green, ), ); } @override Widget build(BuildContext context) { return MaterialApp( title: 'TCards demo', debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: SizedBox( width: 300, height: 400, child: Stack( children: [ // 后面的子项会显示在上面,所以前面的卡片放在最后 _backCard(), _middleCard(), _frontCard(), ], ), ), ), ), ); } } 对子控件分别定位并设置其尺寸
定位需要设置 Align 控件的 alignment 属性,传入一个 Alignment(x, y) 进行设置。设置尺寸需要使用 LayoutBuilder 获取当前父容器的尺寸,然后根据容器尺寸进行计算。 class _MyAppState extends State { // 前面的卡片,使用 Align 定位 Widget _frontCard(BoxConstraints constraints) { return Align( alignment: Alignment(0.0, -0.5), // 使用 SizedBox 确定卡片尺寸 child: SizedBox.fromSize( // 计算卡片尺寸,相对于父容器 size: Size(constraints.maxWidth * 0.9, constraints.maxHeight * 0.9), child: Container( color: Colors.blue, ), ), ); } // 中间的卡片,使用 Align 定位 Widget _middleCard(BoxConstraints constraints) { return Align( alignment: Alignment(0.0, 0.0), child: SizedBox.fromSize( // 计算卡片尺寸,相对于父容器 size: Size(constraints.maxWidth * 0.85, constraints.maxHeight * 0.9), child: Container( color: Colors.red, ), ), ); } // 后面的卡片,使用 Align 定位 Widget _backCard(BoxConstraints constraints) { return Align( alignment: Alignment(0.0, 0.5), child: SizedBox.fromSize( // 计算卡片尺寸,相对于父容器 size: Size(constraints.maxWidth * 0.8, constraints.maxHeight * .9), child: Container( color: Colors.green, ), ), ); } @override Widget build(BuildContext context) { return MaterialApp( title: 'TCards demo', debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: SizedBox( width: 300, height: 400, child: LayoutBuilder( builder: (context, constraints) { // 使用 LayoutBuilder 获取容器的尺寸,传个子项计算卡片尺寸 return Stack( children: [ // 后面的子项会显示在上面,所以前面的卡片放在最后 _backCard(constraints), _middleCard(constraints), _frontCard(constraints), ], ); }, ), ), ), ), ); } } 更新最前面卡片位置
向 Stack 容器添加一个 GestureDetector ,手指在屏幕上移动时更新最前面卡片的位置。 class _MyAppState extends State { // 保存最前面卡片的定位 Alignment _frontCardAlignment = Alignment(0.0, -0.5); // 保存最前面卡片的旋转角度 double _frontCardRotation = 0.0; // 前面的卡片,使用 Align 定位 Widget _frontCard(BoxConstraints constraints) { return Align( alignment: _frontCardAlignment, // 使用 Transform.rotate 旋转卡片 child: Transform.rotate( angle: (pi / 180.0) * _frontCardRotation, // 使用 SizedBox 确定卡片尺寸 child: SizedBox.fromSize( size: Size(constraints.maxWidth * 0.9, constraints.maxHeight * 0.9), child: Container( color: Colors.blue, ), ), ), ); } // 省略...... @override Widget build(BuildContext context) { return MaterialApp( title: 'TCards demo', debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: SizedBox( width: 300, height: 400, child: LayoutBuilder( builder: (context, constraints) { // 使用 LayoutBuilder 获取容器的尺寸,传个子项计算卡片尺寸 Size size = MediaQuery.of(context).size; double speed = 10.0; return Stack( children: [ // 后面的子项会显示在上面,所以前面的卡片放在最后 _backCard(constraints), _middleCard(constraints), _frontCard(constraints), // 使用一个占满父元素的 GestureDetector 监听手指移动 SizedBox.expand( child: GestureDetector( onPanDown: (DragDownDetails details) {}, onPanUpdate: (DragUpdateDetails details) { // 手指移动就更新最前面卡片的 alignment 属性 _frontCardAlignment += Alignment( details.delta.dx / (size.width / 2) * speed, details.delta.dy / (size.height / 2) * speed, ); // 设置最前面卡片的旋转角度 _frontCardRotation = _frontCardAlignment.x; // setState 更新界面 setState(() {}); }, onPanEnd: (DragEndDetails details) {}, ), ), ], ); }, ), ), ), ), ); } }
卡片动画
这个布局有三种动画,最前面卡片移开的动画;后面两张卡片位置和尺寸变化的动画;最前面卡片回到原位的动画。 判断卡片横轴移动距离
在手指离开屏幕时判断卡片横轴的移动距离,如果最前面的卡片横轴移动距离超过限制就运行换位动画,否则运行回弹动画。 // 改变位置的动画 void _runChangeOrderAnimation() {} // 卡片回弹的动画 void _runReboundAnimation(Offset pixelsPerSecond, Size size) {} // 省略... // 卡片横轴距离限制 final double limit = 10.0; SizedBox.expand( child: GestureDetector( // 省略... onPanEnd: (DragEndDetails details) { // 如果最前面的卡片横轴移动距离超过限制就运行换位动画,否则运行回弹动画 if (_frontCardAlignment.x > limit || _frontCardAlignment.x < -limit) { _runChangeOrderAnimation(); } else { _runReboundAnimation( details.velocity.pixelsPerSecond, size, ); } }, ), ), 卡片回弹动画
首先实现卡片回弹的动画,使用 AnimationController 控制动画,在 initState 初始化动画控制器。创建一个 AlignmentTween 设置动画运动值,起始值是卡片当前位置,最终值是卡片的默认位置。然后将一个弹簧模拟 SpringSimulation 传递给动画控制器,让动画模拟运行。 class _MyAppState extends State with TickerProviderStateMixin { // 省略... // 卡片回弹动画 Animation _reboundAnimation; // 卡片回弹动画控制器 AnimationController _reboundController; // 省略... // 卡片回弹的动画 void _runReboundAnimation(Offset pixelsPerSecond, Size size) { // 创建动画值 _reboundAnimation = _reboundController.drive( AlignmentTween( // 起始值是卡片当前位置,最终值是卡片的默认位置 begin: _frontCardAlignment, end: Alignment(0.0, -0.5), ), ); // 计算卡片运动速度 final double unitsPerSecondX = pixelsPerSecond.dx / size.width; final double unitsPerSecondY = pixelsPerSecond.dy / size.height; final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY); final unitVelocity = unitsPerSecond.distance; // 创建弹簧模拟的定义 const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1); // 创建弹簧模拟 final simulation = SpringSimulation(spring, 0, 1, -unitVelocity); // 根据给定的模拟运行动画 _reboundController.animateWith(simulation); // 重置旋转值 _frontCardRotation = 0.0; setState(() {}); } @override void initState() { super.initState(); // 初始化回弹的动画控制器 _reboundController = AnimationController(vsync: this) ..addListener(() { setState(() { // 动画运行时更新最前面卡片的 alignment 属性 _frontCardAlignment = _reboundAnimation.value; }); }); } // 省略... } 卡片换位动画
卡片换位动画就是将最前面的卡片移除可视区,将中间的卡片移动到最前面,将最后的卡片移动到中间,然后新建一个最后面的卡片。在卡片更换位置的同时需要改变卡片的尺寸,位置动画和尺寸动画同时进行。首先定义每个卡片运动时的动画值 /// 卡片尺寸 class CardSizes { static Size front(BoxConstraints constraints) { return Size(constraints.maxWidth * 0.9, constraints.maxHeight * 0.9); } static Size middle(BoxConstraints constraints) { return Size(constraints.maxWidth * 0.85, constraints.maxHeight * 0.9); } static Size back(BoxConstraints constraints) { return Size(constraints.maxWidth * 0.8, constraints.maxHeight * .9); } } /// 卡片位置 class CardAlignments { static Alignment front = Alignment(0.0, -0.5); static Alignment middle = Alignment(0.0, 0.0); static Alignment back = Alignment(0.0, 0.5); } /// 卡片运动动画 class CardAnimations { /// 最前面卡片的消失动画值 static Animation frontCardDisappearAnimation( AnimationController parent, Alignment beginAlignment, ) { return AlignmentTween( begin: beginAlignment, end: Alignment( beginAlignment.x > 0 ? beginAlignment.x + 30.0 : beginAlignment.x - 30.0, 0.0, ), ).animate( CurvedAnimation( parent: parent, curve: Interval(0.0, 0.5, curve: Curves.easeIn), ), ); } /// 中间卡片位置变换动画值 static Animation middleCardAlignmentAnimation( AnimationController parent, ) { return AlignmentTween( begin: CardAlignments.middle, end: CardAlignments.front, ).animate( CurvedAnimation( parent: parent, curve: Interval(0.2, 0.5, curve: Curves.easeIn), ), ); } /// 中间卡片尺寸变换动画值 static Animation middleCardSizeAnimation( AnimationController parent, BoxConstraints constraints, ) { return SizeTween( begin: CardSizes.middle(constraints), end: CardSizes.front(constraints), ).animate( CurvedAnimation( parent: parent, curve: Interval(0.2, 0.5, curve: Curves.easeIn), ), ); } /// 最后面卡片位置变换动画值 static Animation backCardAlignmentAnimation( AnimationController parent, ) { return AlignmentTween( begin: CardAlignments.back, end: CardAlignments.middle, ).animate( CurvedAnimation( parent: parent, curve: Interval(0.4, 0.7, curve: Curves.easeIn), ), ); } /// 最后面卡片尺寸变换动画值 static Animation backCardSizeAnimation( AnimationController parent, BoxConstraints constraints, ) { return SizeTween( begin: CardSizes.back(constraints), end: CardSizes.middle(constraints), ).animate( CurvedAnimation( parent: parent, curve: Interval(0.4, 0.7, curve: Curves.easeIn), ), ); } }
使用一个 AnimationController 控制动画运行,动画运行时在卡片上应用以上的动画值,否则使用卡片默认的位置和尺寸。 class _MyAppState extends State with TickerProviderStateMixin { // 省略... // 卡片位置变换动画控制器 AnimationController _cardChangeController; // 前面的卡片,使用 Align 定位 Widget _frontCard(BoxConstraints constraints) { // 判断动画是否在运行 bool forward = _cardChangeController.status == AnimationStatus.forward; // 使用 Transform.rotate 旋转卡片 Widget rotate = Transform.rotate( angle: (pi / 180.0) * _frontCardRotation, // 使用 SizedBox 确定卡片尺寸 child: SizedBox.fromSize( size: CardSizes.front(constraints), child: Container( color: Colors.blue, ), ), ); // 在动画运行时使用动画值 if (forward) { return Align( alignment: CardAnimations.frontCardDisappearAnimation( _cardChangeController, _frontCardAlignment, ).value, child: rotate, ); } // 否则使用默认值 return Align( alignment: _frontCardAlignment, child: rotate, ); } // 中间的卡片,使用 Align 定位 Widget _middleCard(BoxConstraints constraints) { // 判断动画是否在运行 bool forward = _cardChangeController.status == AnimationStatus.forward; Widget child = Container(color: Colors.red); // 在动画运行时使用动画值 if (forward) { return Align( alignment: CardAnimations.middleCardAlignmentAnimation( _cardChangeController, ).value, child: SizedBox.fromSize( size: CardAnimations.middleCardSizeAnimation( _cardChangeController, constraints, ).value, child: child, ), ); } // 否则使用默认值 return Align( alignment: CardAlignments.middle, child: SizedBox.fromSize( size: CardSizes.middle(constraints), child: child, ), ); } // 后面的卡片,使用 Align 定位 Widget _backCard(BoxConstraints constraints) { // 判断动画是否在运行 bool forward = _cardChangeController.status == AnimationStatus.forward; Widget child = Container(color: Colors.green); // 在动画运行时使用动画值 if (forward) { return Align( alignment: CardAnimations.backCardAlignmentAnimation( _cardChangeController, ).value, child: SizedBox.fromSize( size: CardAnimations.backCardSizeAnimation( _cardChangeController, constraints, ).value, child: child, ), ); } // 否则使用默认值 return Align( alignment: CardAlignments.back, child: SizedBox.fromSize( size: CardSizes.back(constraints), child: child, ), ); } // 改变位置的动画 void _runChangeOrderAnimation() { _cardChangeController.reset(); _cardChangeController.forward(); } // 省略... @override void initState() { super.initState(); // 省略... // 初始化卡片换位动画控制器 _cardChangeController = AnimationController( duration: Duration(milliseconds: 1000), vsync: this, ) ..addListener(() => setState(() {})) ..addStatusListener((status) { if (status == AnimationStatus.completed) { // 动画运行结束后重置位置和旋转 _frontCardRotation = 0.0; _frontCardAlignment = CardAlignments.front; setState(() {}); } }); } // 省略... }
数据更新
主题内容长度不能超过 20000 个字符。。。超长限制,全文地址在此 用 Flutter 实现探探卡片布局
前沿探索
2020-08-20 23:27:12
鉴于 Flutter 高性能渲染和跨平台的优势,闪点清单在移动端 APP 上,使用了完整的 Flutter 框架来开发。既然是完整 APP,架构搭建完全不受历史 Native APP 的影响,没有历史包袱的沉淀,设计也能更灵活和健壮。
国际化语言的支持,是很多 APP 都有的一个强需求,APP 无论大小,只要还不想放弃国外的客户,一般就需要支持国际化。
官方支持
Flutter 官方方案提供了国际化的基础支持,如 Flutter 内置组件的国际化、语言代理、Widget 使用语言包、语言设置回调等,并支持自定义第三方类来扩展,可以参考 Flutter 国际化文档。 官方支持代码示例: class DemoLocalizations { DemoLocalizations(this.locale); final Locale locale; static DemoLocalizations of(BuildContext context) { return Localizations.of(context, DemoLocalizations); } static Map> _localizedValues = { 'en': { 'title': 'Hello World', }, 'es': { 'title': 'Hola Mundo', }, }; String get title { return _localizedValues[locale.languageCode]['title']; } }
官方方案的缺陷
官方的支持有几个缺陷: 依赖于 BuildContext 对象,在非 Widget 中调用时,需要层层传递 BuildContext 对象,或存储全局 BuildContext 对象。 在 MaterialApp 初始化前无法使用国际化(原因也是依赖于 BuildContext 对象)。 语言包定义推荐使用 Map 方式,无法利用静态语言的优势(语法提示、错误检查等);而为语言包每个属性自定义类和类字段,成本较高、使用和更新灵活性差。
i18n 介绍
鉴于 Flutter 官方支持的缺陷,我们调研了很多第三方库,最终发现了 i18n,并在此基础上、结合 Flutter 官方支持和自身封装,实现了更灵活易用的方案。
基础使用
i18n 使用 yaml 格式来定义语言包,同时提供构建脚本一键生成 Dart 语言包 Class 。如下: lib/messages.i18n.yaml button: save: Save load: Load users: welcome(String name): "Hello $name!" logout: Logout
该配置会生成几个 Class:Messages 、ButtonMessages 、UserMessages,生成后的 Dart 文件使用方式如下: Messages m = Messages(); debugPrint(m.users.logout); debugPrint(m.users.welcome('World'));
生成的 Dart 文件预览(开发时无需关心): class Messages { const Messages(); ButtonMessages get button => ButtonExampleMessages(this); UsersMessages get users => UsersExampleMessages(this); } class ButtonMessages { final Messages _parent; const ButtonMessages(this._parent); String get save => "Save"; String get load => "Load"; } class UsersMessages { final Messages _parent; const UsersMessages(this._parent); String get logout => "Logout"; String welcome(String name) => "Hello $name!"; }
进阶功能
下面讲解一些进阶用法。
函数定义
i18n 支持函数定义,并支持传参,如上述的 welcome 函数: debugPrint(m.users.welcome('World'));
参数定义基本没有限制,可以随意定义参数个数和类型。
内置函数
i18n 支持了一些内置函数,用于做不同语言解析的体验优化,如:plural 、cardinal 、ordinal 。具体规则和使用,可以参考这里: http://cldr.unicode.org/index/cldr-spec/plural-rules
使用 Dart 字符串模板
Dart 字符串模板是非常强大的,而在 i18n 中,你可以使用字符串模板(这点非常赞),如: count(int cnt): "You have created $cnt ${_plural(cnt, one:'invoice', many:'invoices')}."
前置编译
i18n 依然依赖了 Dart 官方提供的 builder_runner 工具,来从 yaml 文件生成 Dart 文件,使用方式: flutter pub run build_runner build 。
语言包使用
前置编译后,每个语言包会生成 N 个 Class (语言包的每一个分类或组合会生成一个 Class 文件),然后会生成一个根 Class,我们可以直接使用根 Class (当然也可以使用任何一个分类层级的 Class )。
比如两个语言包文件: AppMessages.i18n.yaml 和 AppMessages_en.i18n.yaml (未加语言后缀的,会认为是默认语言包,因此 AppMessages.i18n.yaml 是默认语言包),会生成 2 个根 Dart Class: class AppMessages 和 class AppMessages_en extends AppMessages 。
AppMessages_en 自动继承自 AppMessages ,因此我们可以直接使用 AppMessages 类型来存储语言包,并在语言切换时重新为其实例化对应的子类: AppMessages appMessages = new AppMessages(); resetLocalLang(String localeName) { switch (localeName) { case 'en': appMessages = AppMessages_en(); break; case 'zh': default: appMessages = AppMessages(); break; } }
然后你可以在任意地方使用语言包: debugPrint('Load Button: ${appMessages.button.load}'); FlatButton( child: Text(appMessages.button.save), onPressed: () { /// 干点什么 }, )
Flutter 集成
集成到 Flutter,依然要依赖于官方的支持,在 MaterialApp 中设置和监听本地语言包: @override Widget build(BuildContext context) { return MaterialApp( localeResolutionCallback: (Locale locale, Iterable supportedLocales) { /// Local changed }, localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], supportedLocales: ['zh', 'en'] .map((loc) => new Locale(loc)) .toList(growable: false), /// ... ); }
结尾
国际化支持,是一个移动端 APP 框架层的基础能力,设计原则应该是使用无感知、灵活易扩展;但维护成本是难免有增加的,比如每次改文案要所有语言包同时更改。
讲到这里,还并没有完成基础框架的搭建,后面我们会讲解更多的 Flutter 架构设计内容,比如:通知、分享、UI 设计等等。
持续分享闪点清单在 Flutter 上的开发经验。 闪点清单 ,一款悬浮清单软件:
前沿探索
2020-08-20 23:26:55
https://www.ithome.com/0/480/365.htm 目前依然在使用 chrome,习惯了不想换。 第八条有说支持 Netflix 4K,现在新版也支持了吗,有没有人测试过?
前沿探索
2020-08-20 23:25:59
输入一段正常的网址,经常给我跳到这个页面:https://cn.bing.com/?scope=web&FORM=ANNNB1 已经被烦死了,这个问题大家有解决办法吗?
前沿探索
2020-08-20 23:25:51
edge 版本是 80.0.361.66(当前最新版),在 chrome 商店里点击添加到 chrome 会报错"There was a problem adding the item to Chrome. Please refresh the page and try again.",我记得之前都是可以正常安装的,代理用的是 switchyomega,虽然个人认为不是代理的原因,但是想看看有没有用路由端代理或者在国外的同学试试能不能复现.
前沿探索
2020-08-20 23:25:23
版本号:81.0.416.20开全局 都没法同步 一直提示:无法连接到同步服务器。正在重试…
前沿探索
2020-08-20 23:24:54
微软在经过了一年多的测试之后推出了一款基于 Chromium 内核的全新 Edge 浏览器,所以有人要换浏览器吗?
前沿探索
2020-08-20 23:24:44
在使用 chrome 时,chrome 会自动把网页中的搜索引擎加入本地的搜索引擎列表中,之后输入关键词按 tab 键即可切换引擎。 在 edge 中同样也有此功能,edge 的搜索设置中提到 要在此处查看更多搜索引擎,请打开一个新的标签页,转到要添加的搜索引擎,然后搜索一些内容。
但是实测,edge 并不会自动添加搜索引擎,测试过 zhihu、bilibili、都没有效果。不知道是 bug 还是我的设置有冲突,想问有没有遇到同样问题的朋友?现在阻止我迁移到 edge 的阻力就剩这个了。
前沿探索
2020-08-20 23:24:36
想尝试 angular , 但是感觉 angular 的模板 "不够 typescript" , 例如官网上的一段 template :

Products

{{ product.name }}


单纯看这个模板, 完全看不出来 product 的类型, 同时 typescript 和 eslint 也没能力管控这个模板中的变量。相对的来说, 在 react 的 tsx 中, typescript 和 eslint 是能够对变量进行充分的静态分析的。
无意引战, 确实希望试试 angular , 可是, 是我使用的姿势不对吗?
前沿探索
2020-08-20 23:23:51
https://material.angular.io/guide/theming
Material 主题可以自定义,主要是默认主题颜色太激进了,有没有好看的主题包推荐?
前沿探索
2020-08-20 23:23:21
有谁抢先体验了一下新特性吗?来分享一下呗。
还有,啥时候出 Angular 板块?不带 JS 的。
前沿探索
2020-08-20 23:23:13
一、项目介绍 运用 angular+angular-cli+angular-router+ngrx/store+rxjs+webpack+node+wcPop 等技术实现开发的仿微信 angular 版聊天室 angular-chatroom 实例项目,实现了下拉刷新、聊天消息右键菜单、发送消息、表情(动图),图片、视频预览,红包打赏等功能。
二、技术实现 MVVM 框架:angular8.0 / @angular/cli 状态管理:@ngrx/store / rxjs 地址路由:@angular/router 弹窗组件:wcPop 打包工具:webpack 2.0 环境配置:node.js + cnpm 图片预览:previewImage 轮播滑动:swiper
{ "name": "angular-chatroom", "dependencies": { "@angular/animations": "~8.0.1", "@angular/common": "~8.0.1", "@angular/compiler": "~8.0.1", "@angular/core": "~8.0.1", "@angular/forms": "~8.0.1", "@angular/platform-browser": "~8.0.1", "@angular/platform-browser-dynamic": "~8.0.1", "@angular/router": "~8.0.1", "rxjs": "~6.4.0", }, "devDependencies": { "@angular-devkit/build-angular": "~0.800.0", "@angular/cli": "~8.0.3", "@angular/compiler-cli": "~8.0.1", "@angular/language-service": "~8.0.1", "@ngrx/store": "^8.0.1", "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", "@types/swiper": "^4.4.3", "codelyzer": "^5.0.0", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", "jquery": "^2.2.3", "karma": "~4.1.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^1.4.0", "swiper": "^4.5.0", } } /* * angular 主模块配置 */ import { BrowserModule } from '@angular/platform-browser' import { NgModule } from '@angular/core' import { FormsModule } from '@angular/forms' import { AppRoutingModule } from './app-routing.module' // 引入状态管理 import { StoreModule } from '@ngrx/store' import { reducer } from '../ngrx' // 载入公共组件( component ) import { HeaderComponent } from '../components/header' import { TabBarComponent } from '../components/tabbar' import { XtnScroll } from '../components/xtnScroll/Scroll' import { NotFoundComponent } from '../components/404' // 载入页面组件( view ) import { AppComponent } from './app.component' import { LoginComponent } from '../views/auth/login' import { RegisterComponent } from '../views/auth/register' import { IndexComponent } from '../views/index' import { ContactComponent } from '../views/contact' import { UinfoComponent } from '../views/contact/uinfo' import { UcenterComponent } from '../views/ucenter' import { GroupChatComponent } from '../views/chat/group-chat' import { GroupInfoComponent } from '../views/chat/group-info' import { SingleChatComponent } from '../views/chat/single-chat' @NgModule({ declarations: [ // 公共组件 HeaderComponent, TabBarComponent, XtnScroll, NotFoundComponent, // 页面组件 AppComponent, LoginComponent, RegisterComponent, IndexComponent, ContactComponent, UinfoComponent, UcenterComponent, GroupChatComponent, GroupInfoComponent, SingleChatComponent, ], imports: [ BrowserModule, AppRoutingModule, FormsModule, StoreModule.forRoot(reducer) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } /* * angular 路由守卫(验证 token ) */ import { Router, CanActivate } from '@angular/router' declare var wcPop: any; export class Auth implements CanActivate{ constructor(private router: Router){} canActivate(){ let that = this // 验证 token const token: boolean = window.sessionStorage.getItem('token') ? true : false if(!token){ // 未登录授权 /* wcPop({ content: '还未登录授权!', anim: 'shake', style: 'background:#e03b30;color:#fff;', time: 2, end: function () { that.router.navigate(['/login']); } }); */ that.router.navigate(['/login']); } return token } } function surrounds() { setTimeout(function () { //chrome var sel = window.getSelection(); var anchorNode = sel.anchorNode; if (!anchorNode) return; if (sel.anchorNode === $(".J__wcEditor")[0] || (sel.anchorNode.nodeType === 3 && sel.anchorNode.parentNode === $(".J__wcEditor")[0])) { var range = sel.getRangeAt(0); var p = document.createElement("p"); range.surroundContents(p); range.selectNodeContents(p); range.insertNode(document.createElement("br")); //chrome sel.collapse(p, 0); (function clearBr() { var elems = [].slice.call($(".J__wcEditor")[0].children); for (var i = 0, len = elems.length; i < len; i++) { var el = elems[i]; if (el.tagName.toLowerCase() == "br") { $(".J__wcEditor")[0].removeChild(el); } } elems.length = 0; })(); } }, 10); } // 定义最后光标位置 var _lastRange = null, _sel = window.getSelection && window.getSelection(); var _rng = { getRange: function () { if (_sel && _sel.rangeCount > 0) { return _sel.getRangeAt(0); } }, addRange: function () { if (_lastRange) { _sel.removeAllRanges(); _sel.addRange(_lastRange); } } } // 消息处理 function isEmpty() { // var html = $editor.html(); var html = $(".J__wcEditor").html(); html = html.replace(//ig, "\r\n"); html = html.replace(/<[^img].*?>/ig, ""); html = html.replace(/ /ig, ""); return html.replace(/\r\n|\n|\r/, "").replace(/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, "") == ""; }
欢迎大家一起交流学习 Q:282310962 wx:xy190310
前沿探索
2020-08-20 23:23:04
不考虑学习曲线,以项目健壮性和开发为优先考虑因素,大型项目应该用哪个呢?还有,Angular 和 AngularJS 是不同的,为什么没有 Angular(Typescript) 的频道呢?
前沿探索
2020-08-20 23:22:58
github 地址: https://github.com/qianxiaoning/demo-angularJs1.7.5 欢迎大家 star 或者 fork 呀~ 目录结构 src/ components/ 组件 config/ dict.js 一个全局变量的 run 方法 router.js 路由表 validation.js 'angular-validation'的 config 配置 controllers/ 控制器 data/ 数据 mock 文件 directives/ 指令 filters/ 过滤器 image/ 图片 pages/ 页面 services/ 服务(公共方法) app.js 入口函数 common.less 公共样式文件 index.html html 模板文件
前沿探索
2020-08-20 23:22:40
我想用 component 封装一个 inputbox 文本输入框,还是需要用 ng-model 去绑定变量,然而绑定变量得到的值需加上某些前缀。比如在输入框内输入 "abcd" , 对应的 model 变量会被处理为 ” textPrefixabcd"
Component 我是这样写的: angular.module('').component('dummyBox', { bindings: { ngModel: '=' }, require: { ngModelCtrl: 'ngModel' }, template: '', controller: function() { $ctrl.$onInit = () => { $ctrl.ngModelCtrl.$parsers.push((viewValue) => { if(viewValue) return "testPrefix" + viewValue; }); $ctrl.ngModelCtrl.$formatters.push((modelValue) => { return modelValue.substr("textPrefix".length); }); } } });
然后在外部使用时这样:
为什么不对呢,label 打印出来的永远是实际输入的值。是 ngModel 绑定错了,还是我对$formatters 和 $parsers 的理解不对?求前端大佬们指点
前沿探索
2020-08-20 23:22:23
angularjs 1.5.11
adminLTE
路由切换之后会出现下面一部分空白,f12 或者刷新或者改变浏览器窗口大小后就正常了,是浏览器卡了还是样式崩了?
前沿探索
2020-08-20 23:22:11
背景
网上很多类似的表单生成,包括 json schema 生成表单也有好几款成熟的,但是问题都是不支持完全的表单 html 模板,而是提供一个模板组件,接收 json schema 参数去生成表单。
问题就是,我们的业务需求可能会变动,或者页面需要加一些样式,布局修改等,这种没有表单源码的表单生成,遇到不符合 UI 设计,或者后期需求变动页面调整都是不利的。
表单生成+支持表单 html 页面模板源码,好处就很明显,你既可以用表单组件生成页面,也可以复制表单 HTML 原代码出来进行二次开发。才真正释放生产力。所以就有了这个工程: ngx-form-builder
功能实现 支持 json schema 生成表单,支持 bootstrap + ng-zorro-antd 两种 UI 样式组件。 支持源码复制、下载。和生成的模板支持 StackBlitz 编辑器在线 demo 预览编辑 支持 Yapi 接口文档直接生成表单页面(思路:接口文档——>json schema ——>表单页面)。
DEMO 截图
Github 源码
欢迎 PR,或者 issue 提出你们的使用场景
https://github.com/giscafer/ngx-form-builder
前沿探索
2020-08-20 23:21:57
更新内容 i18n 多语言支持
技术栈 Typescript Angular Material2 rxjs Graphql
相关链接
项目地址
DEMO
ng-notadd-mock-server
Quick start git clone https://github.com/notadd/ng-notadd.git cd ng-notadd npm install npm start # or use ng cli ng serve
Roadmap
1.0 [ ] 支持 Apollo-Graqphql [ ] 更加完整的 仪表盘页面
1.1 [ ] json 生成表单
1.2 [ ] 手机端兼容 [ ] 渐进式应用(PWA)
1.3 [ ] 更多组件支持
1.4 [ ] 基础页面(个人信息页,登录页...) [ ] recaptcha 支持 (默认关闭)
1.5 [ ] excel 导入与导出 [ ] 选定行列导出 excel
1.6 [ ] 截图生成 [ ] firebase (国内无法使用) or 其他替代方案 支持
1.7 [ ] 可 DIY 仪表盘 [ ] json 生成简单仪表盘
1.8 [ ] 支持 electron 构建桌面应用
2.0 [ ] 企业级自定义表单 [ ] 企业级表单系统
后续 [ ] excel 在线编辑 [ ] word 在线编辑
一点说明
为了方便维护,ng-notadd 将剥离出 ng-material2 (扩展组件库) 和 ng-noform 两个项目
前沿探索
2020-08-20 23:21:47
ng-notadd
基于 Angular7 Material2 的中后台解决方案
技术栈 Typescript Angular Material2 rxjs Graphql
相关链接
项目地址
DEMO
ng-notadd-mock-server
Quick start git clone https://github.com/notadd/ng-notadd.git cd ng-notadd npm install npm start # or use ng cli ng serve
Roadmap
0.9 [ ] i18n 多语言支持
1.0 [ ] 支持 Apollo-Graqphql [ ] 更加完整的 仪表盘页面
1.1 [ ] json 生成表单
1.2 [ ] 手机端兼容 [ ] 渐进式应用(PWA)
1.3 [ ] 更多组件支持
1.4 [ ] 基础页面(个人信息页,登录页...) [ ] recaptcha 支持 (默认关闭)
1.5 [ ] excel 导入与导出 [ ] 选定行列导出 excel
1.6 [ ] 截图生成 [ ] firebase (国内无法使用) or 其他替代方案 支持
1.7 [ ] 可 DIY 仪表盘 [ ] json 生成简单仪表盘
1.8 [ ] 支持 electron 构建桌面应用
2.0 [ ] 企业级自定义表单 [ ] 企业级表单系统
后续 [ ] excel 在线编辑 [ ] word 在线编辑
前沿探索
2020-08-20 23:21:32