晓前端

晓前端 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 xiaoheiban.cn 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

晓前端 发布了文章 · 1月8日

晓前端·周刊【第8期】:信息的不对称与真实性(下)

Hello,大家好。又一周结束了,我们又如约而至。我们是好未来集团晓黑板前端团队,简称晓前端。每周五,我们都会搜罗一些有趣,有意义的事情和大家分享。如果你喜欢,欢迎关注我们哟。

封面图

image.png
本次周刊话题为:信息的不对称与真实性(下篇),所以在阅读本周话题前推荐优先阅读上篇(第7期)哦~

上回在我们的晓前端·周刊【第7期】:信息的不对称与真实性(下)中,尽可能简单易懂地为大家解释了信息的不对称性,并通过两个当时发生的社会热点——成都新冠女孩个人信息泄露遭网暴以及网约车司机为救婴儿连闯红灯家属却拒绝作证的乌龙事件来阐明了信息的不对称与真实性在我们的生活中随处可见,与我们息息相关,甚至会对当事人造成不可估量的影响这一事实。

那么我们将如何改变这一切呢?

本周话题:信息的不对称与真实性(下)

很遗憾,想要完全消除这一现象是不可能的。信息不对称与真实性的问题自古以来便一直存在。image.png
公元前210年,秦始皇巡行天下,行至沙丘时病重,秦始皇命中车府令赵高写遗诏给扶苏,遗诏内容是让扶苏将兵权交给蒙恬,赶快回咸阳主持丧事并继承帝位。当时遗诏已经封好,还没来得及交给使者送去,秦始皇便不幸去世。秦始皇死后,赵高和丞相李斯等人与秦始皇的小儿子胡亥密谋,篡改秦始皇的遗诏,立胡亥为太子。 同时伪造一封遗诏赐给扶苏,列举扶苏和蒙恬的罪过,命令他们自杀。

胡亥派使者将遗诏送到上郡给扶苏,扶苏看到遗诏内容,就哭泣着走入内宅,准备自杀。蒙恬阻止扶苏说:“陛下如今在外,还未立太子,派我率领三十万大军把守边疆,让公子你来监督,这是关系天下安危的重大任务!如今只因一个使臣到来,你就想自杀,你哪里知道这不是奸诈的诡计呢?我请求你重新去请示一下,等请示之后再死也不迟!”使者在旁边一再逼迫催促,扶苏对蒙恬说:“如果父亲命令儿子自杀,那还要再请示些什么呢?”说完便自杀而死。
——《史记》

公子扶苏的结局令人唏嘘,这一切也间接导致了大秦帝国的覆灭。这无疑也是信息不对称与真实性酿下的恶果。

尽管信息不对称与真实性的问题始终存在,也无法根除,但我们还是有一些力所能及的事情是可以做到并且能够有效避免问题或减轻影响的。
image.png
首先就是信息的来源。首次获得了信息后要尽可能去多方查证来考察信息的真实性,在不要偏听偏信的同时也要尽可能选择靠谱的、信用良好的信息来源。很多无良营销号往往都是“开局一张图,内容全靠编”,信息的真假、事件的真相对他们来说不重要,他们要的只是阅读量和关注。所以,在选择获取信息的渠道时我们往往要有所选择。

其次就是要对信息有自己深层的思考和判断,保持冷静与理性的立场。如今我们生活在一个信息爆炸的时代,总有一些人为了博取关注和流量而肆无忌惮地传播虚假信息,殊不知这样的行为在一次次欺骗伤害了群众感情的同时也触犯了法律的红线。近年来红极一时的反转事件层出不穷,人们的善意与好心被无情地欺骗与浪费,这也为我们敲响了警钟。

推荐阅读——如何理解和尽量避免「信息不对称」

新话

  1. 比特币在 2020 年底已经突破 150000 人民币一枚,比特币究竟是不是一场骗局?

image.png
比特币(Bitcoin)的概念最初由中本聪在2008年11月1日提出,并于2009年1月3日正式诞生 。根据中本聪的思路设计发布的开源软件以及建构其上的P2P网络。比特币是一种P2P形式的虚拟的加密数字货币。点对点的传输意味着一个去中心化的支付系统。
顺带一提,到本期周刊写下的今日,单枚比特币已经达到了40000美元。

  1. 一款新的版本控制系统Pijul

image.png
Pijul是由Pierre-ÉtienneMeunier和Florent Becker编写的VCS。在2015年至2020年间发布了许多实验原型之后,第一个alpha版本于2020年11月发布。Pijul通过同时在差异版本和版本上进行操作,结合了第三代VCS的各个方面,例如Git和Darcs。

  1. 「小丑竟是我自己」是个什么梗?反映了当代年轻人一种怎样的心理?

image.png
你可曾尽力讨好别人,自己却没有得到任何回报?你可曾以为自己是天选之人,但被社会捶了一番以后,不得不承认,自己不过是泯泯众生。世人都在笑小丑,可是世人谁又不是小丑呢?

  1. 不管多大,在父母眼里,你永远都是孩子

“你永远都是爸妈的囡”
“我对你唯一的要求就是健康”
一位79岁父亲在一本给55岁女儿的日历上写下了这些留言。

  1. 石家庄全市所有车辆及人员均不出市

image.png
石家庄市副市长孟祥红介绍,严控人员流动管理,石家庄市正在进行全员核酸检测,全市所有车辆及人员均不出市,高风险地区藁城区人员均不离开本区域,中风险地区人员严格管理,同时减少管控范围内的人员列动。

一句

  1. 美国会完成对大选结果认证,确认拜登赢得美国大选,彭斯宣布:拜登当选美国总统。

image.png

  1. 好未来融资33亿美元,其中10亿为股权
  1. 比特币一度突破4万美元:6天上涨1万美元,市值逼近特斯拉。
查看原文

赞 0 收藏 0 评论 0

晓前端 发布了文章 · 2020-12-25

简单介绍下各种 JavaScript 解析器

作者:沧海

各种js解析器是前端工程化的基石,可以说如果没有它,很多工程化都无法正常执行,我们每天用到的babel、webpack、eslint、TypeScript背后都需要一套对应的js解析器,今天我们来看看,目前市场上有哪些常用的解析器,他们各自又拥有什么特性尼?

前言

在说js解析器前,我们需要先了解下ESTree这个项目,这个项目的初衷通过社区的力量,保证和es规范的一致性,通过自定义的语法结构来表述JavaScript的AST,后来随着知名度越来越高,多位知名工程师的参与,使得变成了事实意义上的规范,目前这个库是Mozilla和社区一起维护的。

如果没有AST规范,那么也就意味着根本无法造出对应的解析器,而如果AST规范不统一,一些相应的工具库就不能很好的互通有无,比如webpack就无法正常的使用babel相关插件。

正是因为EsTree的定义的规范,所以现在所有的js解析器或者编译器,基本上都绕不开它,如果你有志于自己写一个js解析器,那它的文档你一定需要读一遍。

一。知名的JS解析器

1.1 uglify-js (11.2k☆)

创作时间:2010-8-1
作品地址:https://github.com/mishoo/UglifyJS
作者介绍:Mihai Bazon,Lisp程序员,Emacs爱好者,貌似是罗马尼亚人
作者博客:

**
用于混淆和压缩代码,因为一些原因,uglify-js自己[内部实现了一套AST规范[1]](http://lisperator.net/blog/ug...,也正是因为它的AST是自创的,不是标准的ESTree,es6以后新语法的AST,都不支持,所以没有办法压缩最新的es6的代码,如果需要压缩,可以用类似babel这样的工具先转换成ES5。

uglify-js已经进行到3版本了,前两个版本都是Mihai Bazon维护,但现在最新的3版本是alexlamsluglify-js已经进行到3版本了,前两个版本都是

uglify-js可以通过--acorn或者--spidermonkey指定对应的parsing

1.2 Esprima (6k☆)

创作时间:2011-11-20
作品地址:https://github.com/jquery/esprima
作者介绍:ariya印尼人,在德国获得博士学位,目前住在硅谷山景城,创建了世界上第一个真正的无头Web浏览器PhantomJS。也为WebKit, KDE, 和 Qt贡献过很多代码
作者博客:

这是第一个用JavaScript编写的符合EsTree规范的JavaScript的解析器,后续多个编译器都是受它的影响,个人觉得它的出现具有历史意义。

1.3 acorn (6.8k☆)

创作时间:2012-9-23
作品地址:https://github.com/acornjs/acorn
作者介绍:Marijn Haverbeke,同时也是富文本编辑器prosemirror(4.8k)、浏览器代码编辑器CodeMirror(21.7k)的作者,另外他还出版了一本书《Eloquent JavaScript》,并且这本书在网上也有https://eloquentjavascript.net/,目前国内也翻译了这本书,不过只有第二版《JavaScript编程精解》,也有人目前居住在德国柏林。
作者博客:

acorn和Esprima很类似,输出的ast都是符合EsTree规范的,目前webpack的AST解析器用的就是acorn,和Esprima一样,也是也不直接支持JSX

需要提一下的是,[acorn的es6语法支持RReverser贡献了大半[2]](https://rreverser.com/added-e...

1.4 @babel/parser(babylon)

创作时间:2012-9-23
作品地址:https://github.com/babel/babel/tree/master/packages/babel-parser

babel官方的解析器,最初fork于acorn,后来完全走向了自己的道路,从babylon改名之后,其构建的插件体系非常强大。

1.5 espree (1.3k☆)

创作时间:2014-11-30
作品地址:https://github.com/eslint/espree
作者:Nicholas C. Zakas,多本js书籍,其中《红宝书 第三版》最为知名,eslint也是他创建的,在2016年被查出莱姆病,目前已经很长时间没有工作了,如果有兴趣的话,可以去github上面捐赠他,捐赠页面链接
作者博客:

eslint、prettier的默认解析器,最初fork于Esprima的一个分支(v1.2.2),后来因为ES6的快速发展,但Esprima短时间内又不支持,后面就基于acorn开发了。

1.5 TypeScript

创作时间:2014-7-6
作品地址:https://github.com/microsoft/TypeScript/tree/master/src/compiler
作者:这个是microsoftAnders Hejlsberg领导创建的,他也是是C#、Delphi、Turbo Pascal的作者

现在很多大型项目,首选就是TypeScript,它的解析器是就在项目内src/compiler/parser.ts,TypeScript和其他项目有一个最大的不同,就在于

以上的库,本质上都是基于js来写的,还有一些库,还有一些特殊的js解析器,如果有需要也可以了解下

二。其他有意思的解析器

2.1 sucrase (2.9k☆)

创作时间:2017-9-24
作品地址:https://github.com/alangpierce/sucrase
作者:Alan Pierce,就职于benchling,这是一家专门给生命科学家提供软件协作的公司。
作者博客:

这是@babel/parser的一个子集,如果你确定的代码不需要兼容IE浏览器,就可以用这个解析器,按照他的介绍,其解析速度目前来说应该是最快的。

2.2 swc (10.1k☆)

创作时间:2017-12-17
作品地址:https://github.com/swc-project/swc
作者:강동윤,他是一个97年的小萌新,目前还在读大二,rust的狂热爱好者,开源了很多rust相关的项目,为此今年8月份Deno官方给了他一份顾问合同。
作者博客:

用的rust编写的js编译器,单核比babel快4倍,4核比babel快70倍,也可以用来打包js、ts代码,并且也拥有 tree shaking功能,它的默认的配置文件spack.config.js和webpack.config.js保持一致,@swc/cli故意与@babel/cli想同,看来野心非常大。

它的目标就是替换babel,目前它已经完成了babel的大部分功能,[具体可以看对照表[3]](https://swc.rs/docs/compariso...

2.3 esbuild (16.5k☆)

创作时间:2020-1-1
作品地址:https://github.com/evanw/esbuild/
作者:Evan Wallace,一直从事webGL方面的相关的开发,目前就职于Figma
个人博客:

esbuild是用go编写的下一代web打包工具,它拥有目前最快的打包记录和压缩记录,snowpack和vite的也是使用它来做打包工具,为了追求卓越的性能,目前没有将AST进行暴露,也无法修改AST,无法用作解析对应的JavaScript。

目前它的star在所有项目中增长最为迅速。

三。其他一些著名的工具库

3.1 recast (3.4k☆)

创作时间:2012-9-16
作品地址:https://github.com/benjamn/recast
作者:Ben Newman,recast作者,TC39成员,目前居住于纽约布鲁克林地区
个人博客:

如果你需要重构你的代码,那么这个库将会是你的第一选择,它可以同时使用多种解析器,并且可以自定义传入需要的解析器,是重构利器。

3.2 jscodeshift (5.9k☆)

创作时间:2015-3-22
作品地址:https://github.com/facebook/jscodeshift
作者:Felix Kling。
个人博客:

jscodeshift底层依赖于recast,在其基础上封装了遍历、操作AST的方法,使得更加便,如果你需要直接操作AST来构建新的功能,我觉得这个是最合适的选择。

目前来说fkling已经离开了Facebook,目前这个库的主要维护者已经不是他了

3.2 ASTExplorer (3.6k☆)

创作时间:2014-6-29
作品地址:https://github.com/fkling/astexplorer
作者:Felix Kling

如果你需要对代码进行重构或者进行AST的分析,那么https://astexplorer.net/这个网站应该可以很好的帮助你,它是一个可视化的AST浏览工具,非常棒。

目前还不支持SWC,但是已经有人提了对应的pr,还没有合并

参考文献


对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。
查看原文

赞 0 收藏 0 评论 0

晓前端 发布了文章 · 2020-12-22

【electron-playground系列】打包优化之路

作者:梁棒棒

简介

electron打包工具有两个:electron-builderelectron-packager,官方还提到electron-forge,其实它不是一个打包工具,而是一个类似于cli的工具集,目的是简化开发到打包的一整套流程,内部打包工具依然是electron-packager

electron-builderelectron-packager相比各有优劣,electron-builder配置项较多,更加灵活,打包体积相对较小,同时上手难度大;而electron-packger配置简单易上手,但是打出来的应用程序包体积相对较大。

我们选择electron-builder作为electron-playground项目的打包工具,配合webpack实现更灵活的打包配置。打包配置在electron-playground中已有说明,这里不再赘述,主要讲述一下打包一个electron项目的优化点,打包优化分为打包时间优化和体积优化,本篇讲述体积优化。

node_modules优化

electron空项目打包后的dmg体积在75M左右,32位exe文件体积在52M左右,64位的exe体积则为105M左右。

注:空项目依赖版本为:electron: ^10.1.5 electron-builder: ^22.9.1 系统版本为:macOS Catalina 10.15.6。

而我们的项目打包后dmg体积为165M,ia32exe也在128M左右,比预期大很多,分析一下包,在release -> 【版本号命名的文件夹】 -> win-ia32-unpacked -> resources 下有个app.asar文件,这其实是个压缩包,目的是保护代码隐私,在build中可配置是否需要压缩为asar包。
在electron-builder.yml中加入:

asar: true

用asar工具包解压。

# 安装
npm install asar -g

# 解压
asar extract app.asar <解压后的目录>

解压然后看下包中有哪些内容:
image.png
dist和resources是配置项中指定的需要复制打包的内容,这没有问题,可是node_modules中的依赖项已经在webpack打包构建时一同打包进了dist下,它不应该在这里,而且electron-builder配置项files中也没有指定复制此文件夹。带着这个疑问,查看一下文档,终于找到了原因,原来files有默认值:

[
  "**/*",
  "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
  "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
  "!**/node_modules/*.d.ts",
  "!**/node_modules/.bin",
  "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
  "!.editorconfig",
  "!**/._*",
  "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
  "!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
  "!**/{appveyor.yml,.travis.yml,circle.yml}",
  "!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}"
]
package.json and **/node_modules/**/* (only production dependencies will be copied) is added to your custom in any case. 

意思是:package.json和node_modules(仅仅生产依赖项会被复制)在任何情况下都会被添加至自定义(应该是files配置项下吧)中。

那这就很清楚了,我只需要在files中添加"!node_modules"即可,打包后体积是128M,足足小了37M,安装执行,没有问题。
依赖项的问题解决了,但是体积还是没达预期,查看包内容,果然,图片!这个永远也避不开的优化难题。

图片优化

图片优化在整个项目的优化中是优先级较高的,所谓的图片优化,其实是体积与质量之间的博弈,因此要想减小包中图片的体积,是要牺牲一部分图片质量的,也就是清晰度。做出如下优化:

  • 首先对gif图在不影响用户观看的前提下做了一定压缩;
  • 将一些png(流程图,logo,线条简单的)转为svg;
  • 将一些截图png转为jpg;

最终将整体包体积dmg减小至102M,ia32exe为80M左右(后续功能更新会有一定出入);

依赖项,按需加载

然后检查一下依赖项的位置和引用,首先对于package.json中依赖项进行排查,查看dependencies和devDependencies中的依赖项有没有错位的(开发依赖项写在了生产依赖项中),由于打包时只打包dependencies中的依赖项,所以在生产环境中用不到的依赖项一律塞至devDependencies。
然后再检查引用库的按需加载,首先想到项目中使用了antd框架。

官方文档写到:antd 默认支持基于 ES modules 的 tree shaking,对于 js 部分,直接引入 import { Button } from 'antd' 就会有按需加载的效果。

以防万一,检查一下,使用webpack-bundle-analyzer可视化插件看一下依赖体积图示,结果如下。
image.png
并没有找到antd,左侧抽屉中搜了一下,确定是按需加载。
image.png

双package.json

官方重构了生产依赖项,提出双package.json结构(two package.json)。顾名思义,通过两个package.json管理依赖项。一个用来管理开发依赖项,一个管理应用程序依赖项,最终打包时只打包应用程序依赖项。

  • 开发依赖

    此package.json在项目根目录下,文件中声明开发依赖和打包脚本;

  • 应用程序依赖;

    在app文件夹下,声明应用程序依赖,打包时仅打包此文件中声明的依赖。所有的元字段应当在此文件声明(version,name等)。

版本

electron版本也会对打包体积有影响,这里用electron^8和10.1.5小试一下,下图左为8版本,右为10版本。或许系统版本对打包体积也有影响,这里不做尝试了,有条件同学的可以试一下。
image.png   image.png


对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。
查看原文

赞 0 收藏 0 评论 0

晓前端 发布了文章 · 2020-12-18

晓前端·周刊【第7期】:信息的不对称与真实性(上)

Hello,大家好。又一周结束了,我们又如约而至。我们是好未来集团晓黑板前端团队,简称晓前端。每周五,我们都会搜罗一些有趣,有意义的事情和大家分享。如果你喜欢,欢迎关注我们哟。

封面图

image.png
信息不对称(asymmetric information)指交易中的各人拥有的信息不同。在社会政治、经济等活动中,一些成员拥有其他成员无法拥有的信息,由此造成信息的不对称。在市场经济活动中,各类人员对有关信息的了解是有差异的;掌握信息比较充分的人员,往往处于比较有利的地位,而信息贫乏的人员,则处于比较不利的地位。不对称信息可能导致逆向选择(Adverse Selection)。
                                                                                                                ————百度百科

信息不对称在今天也跳出了经济领域,被赋予了新的含义:掌握信息量大,占据主导信息优势的一方能够通过控制话语权、引导价值观等方式对掌握信息量小的弱势方施加不可估量的影响。

随着当今科技的提高,人们获取信息的渠道也在飞速增长。从前,人们只能通过口口相传、文书交流的信息,在今天可以由任何人通过搜索引擎、社交媒体、自媒体等等多种渠道轻松获得。但是这样就能够避免信息不对称吗?你获取到的信息一定是真的吗?

本周话题:信息的不对称与真实性(上)

image.png
前段时间,一名20岁的成都女孩出名了,她的行踪、身份证、真实姓名、家庭住址和电话被赤条条地披露在网络上。无数的人对着她的外貌和工作评头论足,对她进行各种辱骂和不怀好意的评论。原因无他:这名女孩的奶奶被确诊了新冠肺炎。

有人骂她没有道德,亲属确诊后却在成都酒吧转场,连累了整座城市的人,然而事实是,那一天,女孩的奶奶并未确诊新冠,她本人也并不知情。

有人骂她不洁身自好,年纪轻轻就知道辗转于夜店,也有人称这名女孩是一名“陪酒女郎”,然而事实是,女孩之前的工作就是负责酒吧的气氛和营销。

还有人对着她被爆出的照片评头论足,口诛笔伐,然而这所谓的照片是从另一位甚至都没去过成都的无辜女孩微博里盗用来的。

似乎穿上网络的马甲后,人人都是高举正义之剑的执行者,道德的守护人。
image.png
无独有偶,广东东莞的网约车司机艾先生近日接到一张订单,乘客为一家三口带着一个婴儿,上车不久婴儿突发疾病失去知觉。艾先生连闯三个红灯将孩子送医,为此,他要被扣18分,并处以600元的罚款。交警表示需提供相关的医院证明,但婴儿家属却拒绝作证,称闯红灯跟他没关系。艾先生说,虽然他无法理解患者家属的态度,但是并不后悔这么做。12月15日央视新闻报道后,舆论被引爆,乘客被指“冷血”。

这篇《网约车司机为救婴儿连闯 3 个红灯,家属却拒绝作证称「与我无关」》引爆了无数正义之士的怒火,让无数的人用最祖安的方式问候那位家属。从道德方面来说,这似乎一点也没有错,甚至带了一丝丝热血的意味。热心的好人被冷漠的坏人欺负了,总要有人声援的嘛,不然要是让好人寒了心,谁还愿意做好事?换位思考下,谁不生气?好在见义勇为的艾先生虽然并不理解患者家属的态度,却并不后悔当时的选择。

正义似乎又一次得到了声张,公正似乎又一次得到了维护——如果没有两天后的那则新闻的话:如何看待「患儿家属拒为网约车司机作证」系乌龙:医院给错电话,双方已握手和解

由以上两个例子深刻说明了一件事:信息的不对称性和真实性不但没有随着信息时代的到来而消失,反而进一步扩大了影响力。每一次的“网络执法”似乎都是一次人民的胜利,正义的狂欢,但这一切都得建立在对称且真实的信息之上,不然人们的正义可能会沦为别有用心的人的棋子,善意被浪费,无辜被伤害,真相被埋没。

下一期周刊我们将继续这一话题,进行探索与思考。

乐见

  1. 作弊与反作弊的较量:如何检测在线考试作弊的

image.png
疫情当前,学生远程上课,不少学校也把考试从线下搬到了线上。
不同于线下,线上监考由于增加了更多不可控因素,如何在尽可能少增加成本的前提下,保证考试的公平公正就成了亟待解决的问题。

  1. 阿里巴巴、阅文、丰巢网络三巨头因违反《反垄断法》被罚

12月14日讯,据市场监管总局官网,根据《反垄断法》规定,市场监管总局对阿里巴巴投资有限公司收购银泰商业(集团)有限公司股权、阅文集团收购新丽传媒控股有限公司股权、深圳市丰巢网络技术有限公司收购中邮智递科技有限公司股权等三起未依法申报违法实施经营者集中案进行了调查,并于2020年12月14日依据《反垄断法》第 48 条、 49 条作出处罚决定,对阿里巴巴投资有限公司、阅文集团和深圳市丰巢网络技术有限公司分别处以 50 万元人民币罚款的行政处罚。

  1. JavaScript 诞生二十五年

image.png
编程语言 JavaScript 诞生二十五年,虽然饱受批评,但 JavaScript 已成为 Web 和浏览器应用的重要组成部分。Brendan Eich 在 1995 年花了 10 天设计出了 JavaScript 原型,在它大获成功之后,微软设计出了超集 TypeScript,之后 Google 的 V8 引擎,基于 V8 的 node.js,Facebook 的 React,等等帮助 JavaScript 迅速占领了从桌面到移动的广泛平台。JavaScript 现在是世界最流行的编程语言。

  1. 如何看待网传字节老板张一鸣在公司《原神》群内活捉员工上班摸鱼

image.png
该回答从员工管理的角度切入,分析了这一事件背后的现象和意义。

  1. 动画解析浏览器运行机制

一个视频带你深入浅出地了解浏览器的运行机制。想要了解浏览器的运行机制却屡屡被大段大段的文字和复杂无比的图片劝退?那这个动画视频一定适合你。

一句

  1. 12月15日,谷歌遭遇全球大面积瘫痪,旗下Gmail邮箱,谷歌日历(Google Calendar)、视频网站YouTube等服务都受到影响,但大部分搜索引擎业务仍然完好。
  1. Apache Guacamole(鳄梨酱)是一个基于HTML5 Web应用程序,可以随时通过浏览器连接已经配置好的机器,它支持标准协议,例如VNC、RDP和SSH。
  1. 谷歌2020年度总结:这是多事的一年,但我们步履不停。

对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。
查看原文

赞 0 收藏 0 评论 0

晓前端 发布了文章 · 2020-12-17

【Electron Playground 系列】文件下载篇

作者:long.woo

文件下载是我们开发中比较常见的业务需求,比如:导出 excel。

web 应用文件下载存在一些局限性,通常是让后端将响应的头信息改成 Content-Disposition: attachment; filename=xxx.pdf,触发浏览器的下载行为。

在 electron 中的下载行为,都会触发 session 的 will-download 事件。在该事件里面可以获取到 downloadItem 对象,通过 downloadItem 对象实现一个简单的文件下载管理器:
demo.gif

1. 如何触发下载

由于 electron 是基于 chromium 实现的,通过调用 webContents 的 downloadURL 方法,相当于调用了 chromium 底层实现的下载,会忽略响应头信息,触发 will-download 事件。

// 触发下载
win.webContents.downloadURL(url)

// 监听 will-download
session.defaultSession.on('will-download', (event, item, webContents) => {})

2. 下载流程

flow_chart.png

3. 功能设计

实现一个简单的文件下载管理器功能包含:

  • 设置保存路径
  • 暂停/恢复和取消
  • 下载进度
  • 下载速度
  • 下载完成
  • 打开文件和打开文件所在位置
  • 文件图标
  • 下载记录

3.1 设置保存路径

如果没有设置保存路径,electron 会自动弹出系统的保存对话框。不想使用系统的保存对话框,可以使用 setSavePath 方法,当有重名文件时,会直接覆盖下载。

item.setSavePath(path)

为了更好的用户体验,可以让用户自己选择保存位置操作。当点击位置输入框时,渲染进程通过 ipc 与主进程通信,打开系统文件选择对话框。
select_path.gif
主进程实现代码:

/**
 * 打开文件选择框
 * @param oldPath - 上一次打开的路径
 */
const openFileDialog = async (oldPath: string = app.getPath('downloads')) => {
  if (!win) return oldPath

  const { canceled, filePaths } = await dialog.showOpenDialog(win, {
    title: '选择保存位置',
    properties: ['openDirectory', 'createDirectory'],
    defaultPath: oldPath,
  })

  return !canceled ? filePaths[0] : oldPath
}

ipcMain.handle('openFileDialog', (event, oldPath?: string) => openFileDialog(oldPath))

渲染进程代码:

const path = await ipcRenderer.invoke('openFileDialog', 'PATH')

3.2 暂停/恢复和取消

拿到 downloadItem 后,暂停、恢复和取消分别调用 pauseresumecancel 方法。当我们要删除列表中正在下载的项,需要先调用 cancel 方法取消下载。

3.3 下载进度

在 DownloadItem 中监听 updated 事件,可以实时获取到已下载的字节数据,来计算下载进度和每秒下载的速度。

// 计算下载进度
const progress = item.getReceivedBytes() / item.getTotalBytes()

download_progress.png
在下载的时候,想在 Mac 系统的程序坞和 Windows 系统的任务栏展示下载信息,比如:

  • 下载数:通过 app 的 badgeCount 属性设置,当为 0 时,不会显示。也可以通过 dock 的 setBadge 方法设置,该方法支持的是字符串,如果不要显示,需要设置为 ''。
  • 下载进度:通过窗口的 setProgressBar 方法设置。
由于 Mac 和 Windows 系统差异,下载数仅在 Mac 系统中生效。加上 process.platform === 'darwin' 条件,避免在非 Mac、Linux 系统下出现异常错误。

下载进度(Windows 系统任务栏、Mac 系统程序坞)显示效果:
windows_progress.png
mac_download_progress.png

// mac 程序坞显示下载数:
// 方式一
app.badgeCount = 1
// 方式二
app.dock.setBadge('1')

// mac 程序坞、windows 任务栏显示进度
win.setProgressBar(progress)

3.4 下载速度

由于 downloadItem 没有直接为我们提供方法或属性获取下载速度,需要自己实现。

思路:在 updated 事件里通过 getReceivedBytes 方法拿到本次下载的字节数据减去上一次下载的字节数据。
// 记录上一次下载的字节数据
let prevReceivedBytes = 0

item.on('updated', (e, state) => {
  const receivedBytes = item.getReceivedBytes()
  // 计算每秒下载的速度
  downloadItem.speed = receivedBytes - prevReceivedBytes
  prevReceivedBytes = receivedBytes
})
需要注意的是,updated 事件执行的时间约 500ms 一次。

updated_event.png

3.5 下载完成

当一个文件下载完成、中断或者被取消,需要通知渲染进程修改状态,通过监听 downloadItem 的 done 事件。

item.on('done', (e, state) => {
  downloadItem.state = state
  downloadItem.receivedBytes = item.getReceivedBytes()
  downloadItem.lastModifiedTime = item.getLastModifiedTime()

  // 通知渲染进程,更新下载状态
  webContents.send('downloadItemDone', downloadItem)
})

3.6 打开文件和打开文件所在位置

使用 electron 的 shell 模块来实现打开文件(openPath)和打开文件所在位置(showItemInFolder)。

由于 openPath 方法支持返回值 Promise<string>,当不支持打开的文件,系统会有相应的提示,而 showItemInFolder 方法返回值是 void。如果需要更好的用户体验,可使用 nodejs 的 fs 模块,先检查文件是否存在。
import fs from 'fs'

// 打开文件
const openFile = (path: string): boolean => {
  if (!fs.existsSync(path)) return false

  shell.openPath(path)
  return true
}

// 打开文件所在位置
const openFileInFolder = (path: string): boolean => {
  if (!fs.existsSync(path)) return false

  shell.showItemInFolder(path)
  return true
}

3.7 文件图标

很方便的是使用 app 模块的 getFileIcon 方法来获取系统关联的文件图标,返回的是 Promise<NativeImage> 类型,我们可以用 toDataURL 方法转换成 base64,不需要我们去处理不同文件类型显示不同的图标。

const getFileIcon = async (path: string) => {
  const iconDefault = './icon_default.png'
  if (!path) Promise.resolve(iconDefault)

  const icon = await app.getFileIcon(path, {
    size: 'normal'
  })

  return icon.toDataURL()
}

3.8 下载记录

随着下载的历史数据越来越多,使用 electron-store 将下载记录保存在本地。


对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。
查看原文

赞 0 收藏 0 评论 0

晓前端 发布了文章 · 2020-12-16

【Electron Playground】Electron 窗口问题汇总

作者:Kurosaki
本节旨在汇总在开发Electron 窗口可能遇到的问题,做一个汇总,后续遇到问题会持续更新。

1. 窗口闪烁问题。

const { BrowserWindow } = require('electron');
const win = new BrowserWindow();
win.loadURL('https://github.com');

使用new BrowserWindow() 创建出窗口,如果不作任何配置的话,窗口就会出现,默认是白色的;这个时候使用win.loadURL('https://github.com'),加载远程资源,窗口重新渲染,从而导致窗口出现闪烁。

解决方法:

const { BrowserWindow } = require('electron');
const win = new BrowserWindow({ show:false });

win.loadURL('https://github.com');
win.once('ready-to-show',()=>{
    win.show();
})

2. 老版Window7系统下,窗口白屏问题。

公司业务开发的Electron应用,是给老师用的,有些老师是那种老版本Window7,并且关闭了自动更新。

解决办法:
安装最新版的.NET Framework
官方下载地址:https://dotnet.microsoft.com/download/dotnet-framework
相关issue: https://github.com/electron/electron/issues/25186

3. macOS下电脑关机,Electron应用阻止关机问题。

macOS 关机会把所有的窗口关闭,如果存在
// 窗口注册close事件
win.on('close',(event)=>{
  event.preventDefault()  // 阻止窗口关闭
})

这种代码,会导致阻止系统关机。

4. Window  系统下,使用 hide() 方法,可能导致页面挂住不能用。

导致这种场景可以是因为调用 hide() 之后不调用 show()  方法,而只是调用 restore() 方法,会导致页面挂住不能用。所以得在 restore() 方法之后添加 show() 方法

5. 新版Electron,导致渲染进程无法访问到remote模块。

切换成新版的Electron,new BrowserWindow(options)配置更新,webPreferences中配置enableRemoteModule:true

6. macOS下无边框窗口,顶部拖拽问题。

在创建窗口时,配置一下preload.js,代码如下:
function initTopDrag() {
  const topDiv = document.createElement('div') // 创建节点
  topDiv.style.position = 'fixed' // 一直在顶部
  topDiv.style.top = '0'
  topDiv.style.left = '0'
  topDiv.style.height = '20px' // 顶部20px才可拖动
  topDiv.style.width = '100%' // 宽度100%
  topDiv.style.zIndex = '9999' // 悬浮于最外层
  topDiv.style.pointerEvents = 'none' // 用于点击穿透
  topDiv.style['-webkit-user-select'] = 'none' // 禁止选择文字
  topDiv.style['-webkit-app-region'] = 'drag' // 拖动
  document.body.appendChild(topDiv) // 添加节点
}

window.addEventListener('DOMContentLoaded', function onDOMContentLoaded() {
    initTopDrag()
})

7. Window系统下,隐藏菜单问题。

Window系统下,菜单长的很丑,有2种方案可以隐藏菜单
  1. 使用无边框窗口,去除菜单和边框,自己手写一个控制的边框,目前github都有这些库;
  2. 使用autoHideMenuBar:true 但是按下ALT键会出现菜单

8. 窗口之间通信。

  1. 主进程创建窗口

配置preload.js,将通信方法挂载到window

/**
 * 这个是用于窗口通信例子的preload,
 * preload执行顺序在窗口js执行顺序之前
 */
import { ipcRenderer, remote } from 'electron'
const { argv } = require('yargs')

const { BrowserWindow } = remote

// 父窗口监听子窗口事件
ipcRenderer.on('communication-to-parent', (event, msg) => {
  alert(msg)
})

const { parentWindowId } = argv
if (parentWindowId !== 'undefined') {
  const parentWindow = BrowserWindow.fromId(parentWindowId as number)
  // 挂载到window
  // @ts-ignore
  window.send = (params: any) => {
    parentWindow.webContents.send('communication-to-parent', params)
  }
}

创建窗口传入id

browserWindow.webContents.on('new-window', (event, url, frameName, disposition) => {
    event.preventDefault()
    // 在通过BrowserWindow创建窗口
    const win = new BrowserWindow({ 
      show:false, 
      webPreferences: {
        preload:preload.js,
        additionalArguments:[`--parentWindow=${browserWindow.id}`] // 把父窗口的id传过去
      } 
    });
    win.loadURl(url);
    win.once('ready-to-show',()=>{
        win.show()
    })
})
  1. 父子窗口
没有上述那么麻烦,配置preload就可以,具体实现
import { remote, ipcRenderer } from 'electron'

// 父窗口监听子窗口事件
ipcRenderer.on('communication-to-parent', (event, msg) => {
  alert(msg)
})

const parentWindow = remote.getCurrentWindow().getParentWindow()
// @ts-ignore
window.sendToParent = (params: any) =>
  parentWindow.webContents.send('communication-to-parent', params)
  1. 渲染进程创建窗口
渲染进程通信很简单,通过window.open,window.open会返回一个windowObjectReference
通过postMessage就可以通信了。并且window.open 支持传preload等配置。

9. 窗口全屏问题。

使用按钮全屏和退出全屏是可以的,但是先点击左上角🚥全屏,再使用按钮退出全屏,是不行的。因为无法知道当前的状态是全屏,还是不是全屏。

配置preload,使用Electron自带的全屏API

import { remote } from 'electron'

const setFullScreen = remote.getCurrentWindow().setFullScreen
const isFullScreen = remote.getCurrentWindow().isFullScreen

window.setFullScreen = setFullScreen
window.isFullScreen = isFullScreen

10. macOS 开发环境,可能存在文件读取权限问题。

在macOS下,我们启动Electron应用,是在Application文件夹的外面,运行在一个只读的disk image。基于安全的原因,需要存在用户自己授权,electron-util做了一个很好的封装。可以使用 electron-util 中的 enforceMacOSAppLocation 方法。该文档electron-util

const { app, BrowserWindow } = require('electron')
const { enforceMacOSAppLocation } = require('electron-util')

function createWindow() {
  enforceMacOSAppLocation()
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
    },
  })
  mainWindow.loadFile('index.html')
}

app.on('ready', createWindow)

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', function () {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

enforceMacOSAppLocation 方法封装

'use strict';
const api = require('./api');
const is = require('./is');

module.exports = () => {
    if (is.development || !is.macos) {
        return;
    }

    if (api.app.isInApplicationsFolder()) {
        return;
    }

    const appName = 'name' in api.app ? api.app.name : api.app.getName();

    const clickedButtonIndex = api.dialog.showMessageBoxSync({
        type: 'error',
        message: 'Move to Applications folder?',
        detail: `${appName} must live in the Applications folder to be able to run correctly.`,
        buttons: [
            'Move to Applications folder',
            `Quit ${appName}`
        ],
        defaultId: 0,
        cancelId: 1
    });

    if (clickedButtonIndex === 1) {
        api.app.quit();
        return;
    }

    api.app.moveToApplicationsFolder({
        conflictHandler: conflict => {
            if (conflict === 'existsAndRunning') { // Can't replace the active version of the app
                api.dialog.showMessageBoxSync({
                    type: 'error',
                    message: `Another version of ${api.app.getName()} is currently running. Quit it, then launch this version of the app again.`,
                    buttons: [
                        'OK'
                    ]
                });

                api.app.quit();
            }

            return true;
        }
    });
};

如果你遇到Electron相关的问题,可以评论一下,我们共同解决,一起成长。


对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。
查看原文

赞 0 收藏 0 评论 0

晓前端 发布了文章 · 2020-12-15

【Electron Playground 系列】窗口篇

作者:Kurosaki

本文主要讲解Electron 窗口的 API 和一些在开发之中遇到的问题。

官方文档 虽然比较全面,但是要想开发一个商用级别的桌面应用必须对整个 Electron API  有较深的了解,才能应对各种需求。

1. 创建窗口

通过BrowserWindow,来 创建 或者 管理 新的浏览器窗口,每个浏览器窗口都有一个进程来管理。

1.1. 简单创建窗口

const { BrowserWindow } = require('electron');
const win = new BrowserWindow();
win.loadURL('https://github.com');

效果如下:

open-windows.gif

1.1.2. 优化

问题electron BrowserWindow 模块在创建时,如果没有配置 show:false,在创建之时就会显示出来,且默认的背景是白色;然后窗口请求 HTML,会出现视觉闪烁。

解决

const { BrowserWindow } = require('electron');
const win = new BrowserWindow({ show:false });

win.loadURL('https://github.com');

win.on('ready-to-show',()=>{
    win.show();
})

两者对比有很大的区别
window-shows.gif

1.2. 管理窗口

所谓的管理窗口,相当于主进程可以干预窗口多少。

  • 窗口的路由跳转
  • 窗口打开新的窗口
  • 窗口大小、位置等
  • 窗口的显示
  • 窗口类型(无边框窗口、父子窗口)
  • 窗口内 JavaScript 的 node 权限,预加载脚本等
  • ....

这些个方法都存在于BrowserWindow模块中。

1.2.1. 管理应用创建的窗口

BrowserWindow模块在创建窗口时,会返回 窗口实例,这些 窗口实例 上有许多功能方法,我们利用这些方法,管理控制这个窗口。

在这里使用Map对象来存储这些 窗口实例

const BrowserWindowsMap = new Map<number, BrowserWindow>()
let mainWindowId: number;

const browserWindows = new BrowserWindow({ show:false })
browserWindows.loadURL('https://github.com')
browserWindows.once('ready-to-show', () => {
  browserWindows.show()
})
BrowserWindowsMap.set(browserWindow.id, browserWindow)
mainWindowId = browserWindow.id  // 记录当前窗口为主窗口

窗口被关闭,得把Map中的实例删除。

browserWindow.on('closed', () => {
  BrowserWindowsMap?.delete(browserWindowID)
})

1.2.2. 管理用户创建的窗口

主进程可以控制窗口许多行为,这些行为会在后续文章一一列举;以下以主进程控制窗口建立新窗口的行为为例。

使用new-window监听新窗口创建

// 创建窗口监听
browserWindow.webContents.on('new-window', (event, url, frameName, disposition) => {
  /** @params {string} disposition
  *  new-window : window.open调用
  *  background-tab: command+click
  *  foreground-tab: 右键点击新标签打开或点击a标签target _blank打开
  * /
})
注:关于disposition字段的解释,移步electron文档electron源码chrome 源码

扩展new-window

经过实验,并不是所有新窗口的建立, new-window 都能捕捉到的。

以下方式打开的窗口可以被new-window事件捕捉到

window.open('https://github.com')
<a href='https://github.com' target='__blank'>链接</a>

**
渲染进程中使用BrowserWindow创建新窗口,不会被 new-window事件捕捉到
**

const { BrowserWindow } = require('electron').remote
const win = new BrowserWindow()
win.loadURL('https://github.com')

渲染进程访问 _remote_ _,主进程需配置enableRemoteModule:true _
使用这种方式同样可以打开一个新的窗口,但是主进程的new-window捕捉不到。

应用new-window
new-window 控制着窗口新窗口的创建,我们利用这点,可以做到很多事情;比如链接校验、浏览器打开链接等等。默认浏览器打开链接代码如下:

import { shell } from 'electron'
function openExternal(url: string) {
  const HTTP_REGEXP = /^https?:\/\//
  // 非http协议不打开,防止出现自定义协议等导致的安全问题
  if (!HTTP_REGEXP) {
    return false
  }
  try {
    await shell.openExternal(url, options)
    return true
  } catch (error) {
    console.error('open external error: ', error)
    return false
  }
}
// 创建窗口监听
browserWindow.webContents.on('new-window', (event, url, frameName, disposition) => {
  if (disposition === 'foreground-tab') {
      // 阻止鼠标点击链接
      event.preventDefault()
      openExternal(url)
  }
})

关于 _shell_ _模块,可以查看官网 https://www.electronjs.org/docs/api/shell_
_

1.3. 关闭窗口

**close** 事件和 **closed** 事件
close 事件在窗口将要关闭时之前触发,但是在 DOM 的 beforeunload 和 unload 事件之前触发。

// 窗口注册close事件
win.on('close',(event)=>{
    event.preventDefault()  // 阻止窗口关闭
})

closed 事件在窗口关闭后出触发,但是此时的窗口已经被关闭了,无法通过 event.preventDefault() 来阻止窗口关闭。

win.on('closed', handler)

主进程能够关闭窗口的 API 有很多,但都有各自的利弊。

1.3.1. win.close()

关于这个 API 的利弊
  1. 如果当前窗口实例注册并阻止close事件,将不会关闭页面,而且也会 阻止计算机关闭(必须手动强制退出);
  2. 关闭页面的服务,如websocket,下次打开窗口,窗口中的页面会 重新渲染
  3. 通过这个API触发的close事件在 unloadbeforeunload之前触发,通过这点可以实现 关闭时触发弹窗

window-close.gif
完整代码在github:electron-playground

  1. 会被closed事件捕捉到。

1.3.2. win.destroy()

  1. 强制退出,无视close事件(即:无法通过event.preventDefault()来阻止);
  2. 关闭页面,以及页面内的服务,下次打开窗口,窗口中的页面会重新渲染;
  3. 会被closed事件捕捉到。

1.3.3. win.hide()

这个隐藏窗口。
  1. 隐藏窗口,会触发hideblur事件,同样也是可以通过event.preventDefault()来阻止
  2. 只是隐藏窗口,通过win.show(),可以将窗口显现,并且会保持原来的窗口,里面的服务也不会挂断

2. 主窗口隐藏和恢复

2.1. 主窗口

2.1.1. 为什么需要 主窗口?

一个应用存在着许多的窗口,需要一个窗口作为 主窗口,如果该窗口关闭,则意味着整个应用被关闭。
场景:在应用只有一个页面的时,用户点击关闭按钮,不想让整个应用关闭,而是隐藏;
例如:其他的APP,像微信,QQ等桌面端。

利用上文中提到的关闭窗口的 API ,我们实现一个主窗口的隐藏和恢复。

改造一下 close 事件

let mainWindowId: number // 用于标记主窗口id

const browserWindow = new BrowserWindow()

// 记录下主窗口id
if (!mainWindowId) {
  mainWindowId = browserWindow.id
}

browserWindow.on('close', event => {
  // 如果关闭的是主窗口,阻止
  if (browserWindow.id === mainWindowId) {
    event.preventDefault()
    browserWindow.hide()
  }
})

2.1.2. 恢复主窗口显示

能隐藏,就能恢复。
const mainWindow = BrowserWindowsMap.get(mainWindowId)
if (mainWindow) {
  mainWindow.restore()
  mainWindow.show()
}

**mainWindow.show()** 方法:功能如其名,就是“show出窗口”。
为什么要是有 _mainWindow.restore()_ _?_
_windows_ _下如果 _hide_ _之后不调用 _show_ _方法而是只调用 _restore_ _方法就会导致页面挂住不能用

2.1.3. 强制关闭主窗口

有些场景下,可能需要的强制退出,附上代码:
const mainWindow = BrowserWindowsMap.get(mainWindowId)
if (mainWindow) {
  mainWindowId = -1
  mainWindow.close()
}

存在的问题

我们改变了 Electron 窗口的既定行为,就会有许多场景下会有问题

问题一:因为阻止了 close 事件,导致 关机 时无法关闭 主窗口,可以使用如下代码

app.on('before-quit', () => {
    closeMainWindow()
})

在 macOSLinuxWindows 下都可以。

问题二:为避免启动 多个应用

app.on('second-instance', () => {
  const mainWindow = BrowserWindowsMap.get(mainWindowId)
  if (mainWindow) {
    mainWindow.restore()
    mainWindow.show()
  }
})

在 macOSLinuxWindows 下都可以

问题三:首次启动应用程序、尝试在应用程序已运行时或单击 应用程序坞站任务栏图标 时重新激活它

app.on('activate', () => {
  if (mainWindow) {
    mainWindow.restore()
    mainWindow.show()
  }
})

只应用于macOS

问题四: 双击托盘图标 打开APP

tray.on('double-click', () => {
  if (mainWindow) {
    mainWindow.restore()
    mainWindow.show()
  }
})

这样每个环节的代码都有,即可实现,具体代码可参见链接

3. 窗口的聚焦和失焦

3.1. 聚焦

3.1.1. 创建窗口时配置

const { BrowserWindow } = require('electron');
const win = new BrowserWindow();
win.loadURL('https://github.com')

focusable:true  窗口便可聚焦,便可以使用聚焦的 API 
focusable:falseWindows 中设置 focusable: false 也意味着设置了skipTaskbar: true. 在 Linux 中设置 focusable: false 时窗口停止与 wm 交互, 并且窗口将始终置顶;

以下讨论的情况仅为focusable:true情况下

const { BrowserWindow } = require('electron');
const win = new BrowserWindow() // focusable:true 为默认配置

罗列了一下 API 

3.1.2. 关于聚焦的API

API功能
BrowserWindow.getFocusedWindow()来获取聚焦的窗口
win.isFocused()判断窗口是否聚焦
win.on('focus',handler)来监听窗口是否聚焦
win.focus()手动聚焦窗口

3.1.3. 其他API副作用和聚焦有关的:

API功能
win.show()显示窗口,并且聚焦于窗口
win.showInactive()显示窗口,但是不会聚焦于窗口

3.2. 失焦

3.2.1. 关于失焦的api

API功能
win.blur()取消窗口聚焦
win.on('blur',cb)监听失焦

3.2.2. 其他api副作用和失焦有关的:

api功能
win.hide()隐藏窗口,并且会触发失焦事件

4. 窗口类型

4.1. 无边框窗口

4.1.1. 描述

无边框窗口是不带外壳(包括窗口边框、工具栏等),只含有网页内容的窗口

4.1.2. 实现

WindowsmacOSLinux

const { BrowserWindow } = require('electron')
let win = new BrowserWindow({ width: 800, height: 600, frame: false })
win.show()

macOS下,还有不同的实现方式,官方文档

4.1.3. macOS 下独有的无边框

  • 配置titleBarStyle: 'hidden'
返回一个隐藏标题栏的全尺寸内容窗口,在左上角仍然有标准的窗口控制按钮(俗称“红绿灯”)
// 创建一个无边框的窗口
const { BrowserWindow } = require('electron')
let win = new BrowserWindow({ titleBarStyle: 'hidden' })
win.show()

效果如下:
window-type-frame.gif

  • 配置titleBarStyle: 'hiddenInset'
返回一个另一种隐藏了标题栏的窗口,其中控制按钮到窗口边框的距离更大。
// 创建一个无边框的窗口
const { BrowserWindow } = require('electron')
let win = new BrowserWindow({ titleBarStyle: 'hiddenInset' })
win.show()

效果如下:
window-type-frame2.gif

配置titleBarStyle: 'customButtonsOnHover'

效果如下:
window-type-frame3.gif

4.1.4. 窗口顶部无法拖拽的问题

虽然无边框窗口,很美观,可以自定义title;但是改变了Electron窗口顶部的默认行为,就需要使用代码来兼容它,实现其原来承担的功能。

window-type1.gif
出现上述情况,是因为在默认情况下, 无边框窗口是不可拖拽的。 应用程序需要在 CSS 中指定 -webkit-app-region: drag 来告诉 Electron 哪些区域是可拖拽的(如操作系统的标准标题栏),在可拖拽区域内部使用 -webkit-app-region: no-drag 则可以将其中部分区域排除。 请注意, 当前只支持矩形形状。完整文档

使用-webkit-app-region: drag 来实现拖拽,但是会导致内部的click事件失效。这个时候可以将需要click元素设置为-webkit-app-region: no-drag。具体的细节 Electron 的issues

为了不影响窗口内的业务代码,这里拖拽的代码,应该在preload触发。

preload 代码运行,在窗口代码运行之前

核心代码:

// 在顶部插入一个可以移动的dom
function initTopDrag() {
  const topDiv = document.createElement('div') // 创建节点
  topDiv.style.position = 'fixed' // 一直在顶部
  topDiv.style.top = '0'
  topDiv.style.left = '0'
  topDiv.style.height = '20px' // 顶部20px才可拖动
  topDiv.style.width = '100%' // 宽度100%
  topDiv.style.zIndex = '9999' // 悬浮于最外层
  topDiv.style.pointerEvents = 'none' // 用于点击穿透
  // @ts-ignore
  topDiv.style['-webkit-user-select'] = 'none' // 禁止选择文字
  // @ts-ignore
  topDiv.style['-webkit-app-region'] = 'drag' // 拖动
  document.body.appendChild(topDiv) // 添加节点
}

window.addEventListener('DOMContentLoaded', function onDOMContentLoaded() {
    initTopDrag()
})

在创建窗口时引用 preload 即可

const path = require('path')
const { BrowserWindow } = require('electron')

const BaseWebPreferences = {
  nodeIntegration: true,
  preload: path.resolve(__dirname, './windowType.js'), // 这里引用preload.js 路径
}

// 主窗口代码
const win = new BrowserWindow({ webPreferences: BaseWebPreferences, frame: false, titleBarStyle: 'hiddenInset' })
win.loadURL('https://github.com')

便可实现窗口顶部拖拽
window-type.gif
tips: 如果窗口打开了 _devtools_ _,窗口也是可以拖拽的,只不过这个拖拽体验不好_

4.2. 父子窗口

所谓的父子窗口,就是子窗口永远在父窗口之上,只要子窗口存在,哪怕位置不在父窗口上方,都是无法操作父窗口

window-type2.gif

const { BrowserWindow } = require('electron')

let top = new BrowserWindow()
let child = new BrowserWindow({ parent: top })
child.show()
top.show()

窗口之间通信 章节中介绍到父子窗口之间的通信;通过 getParentWindow 拿到父窗口的 类BrowserWindowProxy,通过 win.postMessage(message,targetOrigin) 实现通信

4.3. 模态窗口

模态窗口也是一种父子窗口,只不过展示会有不同

const { BrowserWindow } = require('electron')

let top = new BrowserWindow()
let child = new BrowserWindow({ parent: top, modal: true, show: false })
child.loadURL('https://github.com')
child.once('ready-to-show', () => {
  child.show()
})

window-type3.gif

5. 窗口之间的通信

实现窗口通信必须不影响窗口内的业务代码, jdk 等的注入

5.1. 主进程干预方式

主进程是可以干预渲染进程生成新的窗口的,只需要在创建窗口时,webContents 监听 new-window

import path from 'path'
import { PRELOAD_FILE } from 'app/config'
import { browserWindow } from 'electron';

const BaseWebPreferences: Electron.BrowserWindowConstructorOptions['webPreferences'] = {
  nodeIntegration: true,
  webSecurity: false,
  preload: path.resolve(__dirname, PRELOAD_FILE),
}


// 创建窗口监听
browserWindow.webContents.on('new-window', (event, url, frameName, disposition) => {
    event.preventDefault()
    // 在通过BrowserWindow创建窗口
    const win = new BrowserWindow({ 
      show:false, 
      webPreferences: {
        ...BaseWebPreferences,
        additionalArguments:[`--parentWindow=${browserWindow.id}`] // 把父窗口的id传过去
      } 
    });
    win.loadURl(url);
    win.once('ready-to-show',()=>{
        win.show()
    })
})

preload.js  文件window.process.argv,便能拿到父窗口的id,window.process.argv是一个字符串数组,可以使用yargs来解析

preload.js  代码

import { argv } from 'yargs'
console.log(argv);

yargv-parse.png
拿到了父窗口的 id ,封装一下通信代码,挂载到 window 上

/**
 * 这个是用于窗口通信例子的preload,
 * preload执行顺序在窗口js执行顺序之前
 */
import { ipcRenderer, remote } from 'electron'
const { argv } = require('yargs')

const { BrowserWindow } = remote

// 父窗口监听子窗口事件
ipcRenderer.on('communication-to-parent', (event, msg) => {
  alert(msg)
})

const { parentWindowId } = argv
if (parentWindowId !== 'undefined') {
  const parentWindow = BrowserWindow.fromId(parentWindowId as number)
  // 挂载到window
  // @ts-ignore
  window.send = (params: any) => {
    parentWindow.webContents.send('communication-to-parent', params)
  }
}

应用一下试试看:
window-com.gif
这种方法可以实现通信,但是太麻烦了。

5.2. 父子窗口通信

和主进程干预,通过ipc通信方式差不多,只是利用父子窗口这点,不用通过additionalArguments传递父窗口id,在子窗口通过window.parent,就可以拿到父窗口

browserWindow.webContents.on('new-window', (event, url, frameName, disposition) => {
    event.preventDefault()
      
    // 在通过BrowserWindow创建窗口
    const win = new BrowserWindow({ 
        show:false, 
        webPreferences:BaseWebPreferences,
        parent:browserWindow // 添加父窗口
      });
    win.loadURl(url);
    win.once('ready-to-show',()=>{
        win.show()
    })
    
})

弊端:子窗口永远在父窗口之上。

const path = require('path')
const { BrowserWindow } = require('electron')

const BaseWebPreferences = {
  // // 集成node
  nodeIntegration: true,
  // // 禁用同源策略
  // webSecurity: false,
  // 预加载脚本 通过绝对地址注入
  preload: path.resolve(__dirname, './communication2.js'),
}

// 主窗口代码
const parent = new BrowserWindow({ webPreferences: BaseWebPreferences, left: 100, top: 0 })
parent.loadURL(
  'file:///' + path.resolve(__dirname, '../playground/index.html#/demo/communication-part2/main'),
)
parent.webContents.on('new-window', (event, url, frameName, disposition) => {
  // 阻止默认事件
  event.preventDefault()
  // 在通过BrowserWindow创建窗口
  // 子窗口代码
  const son = new BrowserWindow({
    webPreferences: BaseWebPreferences,
    parent,
    width: 400,
    height: 400,
    alwaysOnTop: false,
  })
  // son.webContents.openDevTools();
  son.loadURL(
    'file:///' +
      path.resolve(__dirname, '../playground/index.html#/demo/communication-part2/client'),
  )
})

preload.js

import { remote, ipcRenderer } from 'electron'

// 父窗口监听子窗口事件
ipcRenderer.on('communication-to-parent', (event, msg) => {
  alert(msg)
})

const parentWindow = remote.getCurrentWindow().getParentWindow()
// @ts-ignore
window.sendToParent = (params: any) =>
  parentWindow.webContents.send('communication-to-parent', params)

window-com1.gif
但是必须得是父子窗口,有弊端。

5.3. 使用window.open

终极方法

web 端,使用 window.open  会返回一个 windowObjectReference ,通过这个方法可以实现 postMessage ;但是在 Electron 端,把 window.open 方法重新定义了;使用 window.open 创建一个新窗口时会返回一个 BrowserWindowProxy对象,并提供一个有限功能的子窗口.
MDN文档 Electron文档

const  BrowserWindowProxy = window.open('https://github.com', '_blank', 'nodeIntegration=no')
BrowserWindowProxy.postMessage(message, targetOrigin)

代码精简,且需要的功能,即符合 BrowserWindow(options) 中 options 配置的,都可以使用 window.open 配置。

6. 全屏、最大化、最小化、恢复

6.1. 全屏

6.1.1. 创建时进入全屏

配置new BrowserWindow({ fullscreen:true })
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ fullscreen:true,fullscreenable:true })
win.loadURL('https://github.com')

6.1.2. 使用API进入全屏

确保当前窗口的fullscreenable:true,以下API才能使用
  1. win.setFullScreen(flag),设置全屏状态;
  2. win.setSimpleFullScreen(flag)macOS下独有,设置简单全屏。

6.1.3. 全屏状态的获取

  1. win.fullScreen,来判断当前窗口是否全屏;
  2. win.isFullScreen()macOS独有;
  3. win.isSimpleFullScreen()macOS独有。

6.1.4. 全屏事件的监听

  1. rezise 调整窗口大小后触发;
  2. enter-full-screen 窗口进入全屏状态时触发;
  3. leave-full-screen 窗口离开全屏状态时触发;
  4. enter-html-full-screen 窗口进入由HTML API 触发的全屏状态时触发;
  5. leave-html-full-screen 窗口离开由HTML API触发的全屏状态时触发。

6.1.5. HTMLAPI无法和窗口联动问题

const path = require('path')
const { BrowserWindow } = require('electron')
const BaseWebPreferences = { 
    nodeIntegration: true,
    preload: path.resolve(__dirname, './fullScreen.js'), 
};
const win = new BrowserWindow({ webPreferences: BaseWebPreferences })
win.loadURL('file:///' + path.resolve(__dirname, '../playground/index.html#/demo/full-screen'))
使用按钮全屏和退出全屏是可以的,但是先点击左上角🚥全屏,再使用按钮退出全屏,是不行的。因为无法知道当前的状态是全屏,还是不是全屏。

解决办法:,将win.setFullScreen(flag)方法挂载到窗口的window
加载这样一段preload.js代码即可

import { remote } from 'electron'

const setFullScreen = remote.getCurrentWindow().setFullScreen
const isFullScreen = remote.getCurrentWindow().isFullScreen

window.setFullScreen = setFullScreen
window.isFullScreen = isFullScreen

setFullScreen文档 https://www.electronjs.org/docs/api/browser-window#winsetfullscreenflag isFullScreen 文档https://www.electronjs.org/docs/api/browser-window#winisfullscreen

6.2. 最大化、最小化

6.2.1. 创建窗口配置

完整API文档
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ minWidth:300,minHeight:300,maxWidth:500,maxHeight:500,width:600,height:600 })
win.loadURL('https://github.com')

当使用 minWidth/maxWidth/minHeight/maxHeight 设置最小或最大窗口大小时, 它只限制用户。 它不会阻止您将不符合大小限制的值传递给 setBounds/setSizeBrowserWindow 的构造函数。

6.2.2. 相关事件

事件名称触发条件
maximize窗口最大化时触发
unmaximize当窗口从最大化状态退出时触发
minimize窗口最小化时触发
restore当窗口从最小化状态恢复时触发

6.2.3. 相关状态API

  1. win.minimizable 窗口是否可以最小化
  2. win.maximizable 窗口是否可以最大化
  3. win.isMaximized() 是否最大化
  4. win.isMinimized() 是否最小化

6.2.4. 控制API

  1. win.maximize() 使窗口最大化
  2. win.unmaximize() 退出最大化
  3. win.minimize() 使窗口最小化
  4. win.unminimize() 退出最小化

6.3. 窗口恢复

win.restore() 将窗口从最小化状态恢复到以前的状态。在前面的例子 主窗口隐藏和恢复也有用到这个api

7. 窗口各事件触发顺序

window-event.png

7.1. 窗口加载时

BrowserWindow实例:即 new BrowserWindow() 返回的实例对象
webContents: 即 BrowserWindow 实例中的 webContents 对象
webPreferences: 即 new BrowserWindow(options) 中 options 的 webPreferences 配置对象

从上到下,依次执行
环境事件触发时机
webPreferences的preload-在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成Node, 此脚本都可以访问所有Node API 脚本路径为文件的绝对路径。
webContentsdid-start-loading当tab中的旋转指针(spinner)开始旋转时,就会触发该事件
webContentsdid-start-navigation当窗口开始导航是,触发该事件
窗口中的JavaScriptDOMContentLoaded初始的 HTML 文档被完全加载和解析完成
窗口中的JavaScriptload页面资源全部加载完成之时
BrowserWindow实例show窗口显示时触发时
webContentsdid-frame-navigateframe导航结束时时
webContentsdid-navigatemain frame导航结束时时
BrowserWindow实例page-title-updated文档更改标题时触发
webContentspage-title-updated文档更改标题时触发
webContentsdom-ready一个框架中的文本加载完成后触发该事件
webContentsdid-frame-finish-load当框架完成导航(navigation)时触发
webContentsdid-finish-load导航完成时触发,即选项卡的旋转器将停止旋转
webContentsdid-stop-loading当tab中的旋转指针(spinner)结束旋转时,就会触发该事件

7.2. 窗口加载完毕,用户触发事件(不包括resize和move)

事件作用
page-title-updated文档更改标题时触发
blur当窗口失去焦点时触发
focus当窗口获得焦点时触发
hide窗口隐藏
show窗口显示
maximize窗口最大化时触发(mac是双击title)
unmaximize当窗口从最大化状态退出时触发
enter-full-screen窗口进入全屏状态时触发
leave-full-screen窗口离开全屏状态时触发
enter-html-full-screen窗口进入由HTML API 触发的全屏状态时触发
leave-html-full-screen窗口离开由HTML API触发的全屏状态时触发
always-on-top-changed设置或取消设置窗口总是在其他窗口的顶部显示时触发。
app-commandwindowlinux独有

7.3. 用户移动窗口

  1. 移动窗口之前 will-move
  2. 移动窗口中 move
  3. 移动之后 moved

7.4. 用户改变窗口大小

  1. 改变之前 will-resize
  2. 改变之后 resize

7.5. 窗口的内容异常事件(webContent事件)

事件名错误类型
unresponsive网页变得未响应时触发
responsive未响应的页面变成响应时触发
did-fail-load加载失败,错误码
did-fail-provisional-load页面加载过程中,执行了window.stop()
did-frame-finish-load
crashed渲染进程崩溃或被结束时触发
render-process-gone渲染进程意外失败时发出
plugin-crashed有插件进程崩溃时触发
certificate-error证书的链接验证失败
preload-errorpreload.js抛出错误

7.6. 窗口关闭(包括意外关闭)

  • 关闭之前:触发主进程中注册的 close  事件
  • 窗口内的 JavaScript  执行 window.onbeforeunload 
  • 窗口内的 JavaScript  执行 window.onunload 
  • 关闭之后:触发主进程中注册的 closed  事件

对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。
查看原文

赞 2 收藏 0 评论 0

晓前端 发布了文章 · 2020-12-14

Vue3源码解析(computed-计算属性)

作者:秦志英

前言

上一篇文章中我们分析了Vue3响应式的整个流程,本篇文章我们将分析Vue3中的computed计算属性是如何实现的。

在Vue2中我们已经对计算属性了解的很清楚了,在Vue3中提供了一个computed的函数作为计算属性的API,下面我们来通过源码的角度去分析计算属性的运行流程。

computed

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}
  • 在最开始使用函数重载的方式允许computed函数接受两种类型的参数:第一种是一个getter函数, 第二种是一个带getset的对象。
  • 接下就是在函数内部根据传入的不同类型的参数初始化函数内部的gettersetter函数,如果传入的是一个函数类型的参数,那么getter就是这个函数,setter就是一个空的操作,如果传入的参数是一个对象,则getter就等于这个对象的get函数,setter就等于这个对象的set函数。
  • 在函数的结尾返回了一个new ComputedRefImpl,并将前面我们标准化后的参数传递给了这个构造函数。

下面我们就来分析一下ComputedRefImpl这个构造函数。

ComputedRefImpl

class ComputedRefImpl<T> {
  // 缓存结果
  private _value!: T
  // 重新计算开关
  private _dirty = true
  public readonly effect: ReactiveEffect<T>
  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    // 对传入的getter函数进行包装
    this.effect = effect(getter, {
      lazy: true,
      // 调度执行
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          // 派发通知
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })
  }
  // 访问计算属性的时候 默认调用此时的get函数
  get value() {
    // 是否需要重新计算
    if (this._dirty) {
      this._value = this.effect()
      this._dirty = false
    }
    // 访问的时候进行依赖收集 此时收集的是访问这个计算属性的副作用函数
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

ComputedRefImpl类在内部维护了_value_dirty这两个非常重要的私有属性,其中_value使用用来缓存我们计算的结果,_dirty是用来控制是否需要重现计算。接下来我们来看一下这个函数的内部运行机制。

  • 首先构造函数在初始化的时候使用了effect函数对传入getter进行了一层包装(上一篇文章中我们分析过effect函数的作用就是将传入的函数变成可响应式的副作用函数),但是这里我们在effect中传入了一些配置参数,还记得前面我们分析trigger函数的时候有这一段代码:
const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
effects.forEach(run)

当属性值发生改变之后,会触发trigger函数进行派发更新,将所有依赖这个属性的effect函数循环遍历,使用run函数执行effect,如果effect的参数中配置了scheduler,则就执行scheduler函数,而不是执行依赖的副作用函数。当计算属性依赖的属性发生变化的时候,回执行包装getter函数的effect, 但是因为配置了scheduler函数,所以真正执行的是scheduler函数,在scheduler函数中并没有执行计算属性的getter函数求取新值,而是将_dirty设置为false,然后通知依赖计算属性的副作用函数进行更新, 当依赖计算属性的副作用函数收到通知的时候就会访问计算属性的get函数,此时会根据_dirty值来确定是否需要重新计算。

回到我们的这个构造函数中,只需要记得我们在构造函数初始化三个重要的点:第一:对传入的getter函数使用effect函数进行包装。第二:在使用effect包装的过程中,我们会执行getter函数,此时getter函数执行过程中对于访问到的属性会将当前的这个计算属性收集到对应的依赖集合中, 第三:传入了配置参数lazyscheduler,这些配置参数在当前的这个计算属性所订阅的属性发生改变的时候,用来控制计算属性的调度时机。

  • 接着我们继续分析get value,当我们访问计算属性的值时候实际上访问的就是这个函数的返回值, 它会根据_dirty的值来判断是否需要重新计算getter函数,_dirty为true需要重新执行effect函数,并将effect的值置为false,否则就返回之前缓存的_value值。在访问计算属性值的阶段会调用track函数进行依赖收集,此时收集的是访问计算属性值的副作用函数, key始终是vlaue。
  • 最后就是当设置计算属性的值的时候会执行set函数,然后调用我们传入的_setter函数。

示例流程

至此计算属性的执行流程就分析完毕了,我们来结合一个示例来完整的过一遍整个流程:

<template>
    <div>
        <button @click="addNum">add</button>
        <p>计算属性:{{computedData}}</p>
    </div>
</template>

<script>
import { ref, watch,reactive, computed } from 'vue' 
import { effect } from '@vue/reactivity'
export default {
  name: 'App',
  setup(){
    const testData = ref(1)
    const computedData = computed(() => {
      return testData.value++
    })
    function addNum(){
      testData.value += 10
    }
    return {
      addNum,
      computedData
    }
  },
}

</script>

下面是一张流程图,当点击页面中的按钮改变testData的value值时,发生的变化流程就是下面的红线部分。

  • 首先初始化页面的时候,testData经过ref()之后变成响应式数据,会对访问testData.value的值进行依赖收集,当testData.value的值发生变化的话,会对依赖这个值的依赖集合进行派发更新
  • computed中传入了一个getter函数,getter函数内部有对testData.value的访问,此时当前的这个计算属性的副作用函数就订阅了testData.value的值,computed返回了一个值,而页面中的组件有对computed返回值的访问,页面的渲染副作用函数就订阅了computed的返回值,所以这个页面中有两个依赖集合。
  • 当我们点击页面中的按钮,会改变testData.value的值,此时会通知订阅计算属性的副作用函数进行更新操作,由于我们在生成计算属性副作用的时候配置了scheduler,所以执行的是scheduler函数,scheduler函数并没有立即执行getter函数进行重新计算,而是将ComputedRefImpl类内部的私有变量_dirty设置为true,然后通知订阅当前计算属性的副作用函数进行更新操作。
  • 组件中的渲染副作用函数执行更新操作的时候会访问到get value函数,函数内部会根据_dirty值来判断是否需要重新计算,由于前面的scheduler函数将_dirty设置为true所以此时会调用getter函数的副作用函数effect,这个时候才会重新计算并将结果返回,页面数据更新。

总结

计算属性两个最大的特点就是

  • 延时计算 计算属性所依赖的值发生改变的时候并不会立即执行getter函数去重新计算新的结果,而是打开重新计算的开关并通知订阅计算属性的副作用函数进行更新。如果当前的计算属性没有依赖集合就不执行重新计算逻辑,如果有依赖触发计算属性的get,这个时候才会调用this.effect()进行重新计算。
  • 缓存结果 当依赖的属性没有发生改变的,访问计算属性会返回之前缓存在_value中的值。

对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。
查看原文

赞 1 收藏 0 评论 0

晓前端 发布了文章 · 2020-12-11

晓前端·周刊【第6期】:量子霸权

Hello,大家好。又一周结束了,我们又如约而至。我们是好未来集团晓黑板前端团队,简称晓前端。每周五,我们都会搜罗一些有趣,有意义的事情和大家分享。如果你喜欢,欢迎关注我们哟。

封面图

image.png
1937 年设计用来求解线性方程组的,阿塔纳索夫—贝瑞计算机(Atanasoff-Berry Computer,通常简称 ABC 计算机)(上图)是世界上第一台电子数字计算设备。

注:更为人熟知的 ENIAC(伊尼亚克)是世界上第一台通用计算机,它是图灵完备的电子计算机,能够重新编程,解决各种计算问题。但是在 1973 年,美国联邦地方法院注销了 ENIAC 的专利,并得出结论,ENIAC 的发明者从阿塔纳索夫那里继承了电子数字计算机的主要构件思想。因此 ABC 被认定为世界上第一台计算机。

本周话题:量子霸权

image.png
12 月 4 日,中国科学技术大学潘建伟、陆朝阳等组成的研究团队与中科院上海微系统所、国家并行计算机工程技术研究中心合作,构建了 76 个光子的量子计算原型机“九章”,实现了具有实用前景的“高斯波斯取样”任务的快速求解。使我国成为全球第二个实现“量子霸权”的国家。

取名“九章”,是为了纪念中国古代最早的数学专著《九章算术》,其是中国古代张苍、耿寿昌所撰写的一部数学专著,它的出现标志中国古代数学形成了完整的体系,是一部具有里程碑意义的历史著作。

对于经典计算机来说,每个比特位要么是 0,要么是 1,这些信息通过电路构建的逻辑门进行运算。而量子计算,则是利用量子天然具备的叠加性,施展并行计算的能力。每个量子比特,不仅可以表示 0 或 1,还可以表示成 0 和 1 分别乘以一个系数再叠加,随着系数的不同,这个叠加的形式可能性会很多很多。这种叠加性意味着,随着比特数的增加,信息的存储量和运行速度会指数增加,经典计算机将望尘莫及。

基于量子的叠加性,许多量子科学家认为,量子计算机在特定任务上的计算能力将会远胜于任何一台经典计算机。量子霸权(量子计算优越性),是指用量子计算机解决古典电脑实际上解决不了的问题,问题本身未必需要有实际应用。

去年,谷歌率先实现了量子霸权,它所打造的量子计算原型机,名叫“悬铃木”。在解决“随机线路采样”问题时,在 100 万个样本的情况下,仅需 200 秒时间。而谷歌宣称如果用当时全球最快的超级计算机“Summit”来计算,需要 1 万年(IBM 怒怼谷歌,经典计算机没那么差,只需 2 天半)。

根据目前最优的经典算法,“九章”花 200 秒采集到的 5000 个样本,如果用我国的“太湖之光”,需要运行 25 亿年(没有多打字),如果用目前世界排名第一的超级计算机“富岳”,也需要运行 6 亿年。

乐见

  1. Math.random 的各种妙用

640.gif
Math.random() 是很常见的一个方法,用来随机返回一个 [0, 1) 范围的小数。本文列举了此方法在更多场景的使用,例如生成音乐、文字打乱、生成密码等等。上图是利用 Math.random() 实现的随机粒子动画。

  1. 一文了解文件上传全过程

日常前端开发中,更多的是去提交 JSON 给服务端,偶尔碰到提交文件的情况就直接使用第三方开源库来解决了,本文从客户端到服务端完全讲解并实现了文件上传,值得一看。

  1. 一文带你层层解锁文件下载的奥秘

看完了文件上传,不想着再来了解一下浏览器是怎么下载文件的?为什么有的链接点击了就能下载,为什么有的点击了却是打开页面,大文件怎么下载的,断点续传怎么做的?

  1. 谷歌向公众开放 Fuchsia 操作系统:提供详细安装教程,英特尔 NUC 可正常运行

image.png
已经曝光了 4 年多的谷歌 Fuchsia 操作,12 月 8 日正式向公众开放源代码了。坊间传闻 Fuchsia 是作为 Android 的替代者身份出现,这也使得 Fuchsia 的关注度居高不下。

值得一提的是,前端如日中天的跨平台开发框架 Flutter 也支持创建 Fuchsia 应用。

  1. 浏览器成为应用程序平台只是一个意外

image.png
浏览器一开始只是被设计为一个远程文档查看器,历史巧合让它被迫成为了一个应用程序平台,以至于后来互联网的诸多问题都是这个事实造成的。

浮生

你是否曾有段时间在微博热搜榜上频频看到过一个名字 —— 半泽直树?
你是否听过一句话,叫做“人若犯我,以牙还牙,加倍奉还”?
你是否曾在今年 11 月被周围的亲朋好友安利过这部剧?
一部日本现象级职场剧 ——《半泽直树》
image.png
这一部创下日本平成年代以来的单集最高收视纪录的电视剧于今年产出了续作《半泽直树 2》。这两部都在 B 站可以观看,评分均为 9.9。原本是小编我作为一名铁打的理工科程序媛,原本对这部涉及了金融,银行和职场的剧可以说是完全不感兴趣。然而它的评分实在是太高了,激起了我的好奇,也让我还是没能逃过王境泽第三定律 —— 真香。不得不说,演员、配乐、节奏、分镜、剧本、人物塑造和表现手法皆是上乘,而其中传递的价值观也让人感到敬佩。具体内容篇幅有限,小编不便多说,有兴趣的小伙伴可以去搜索一下。

言语的安利终究是有极限的,但《半泽直树》无疑是一部震撼人心的好剧。看了你就会知道。ヽ(゚∀゚)メ(゚∀゚)ノ

一句

  1. 《赛博朋克2077发售》多次跳票后终于于12月10日正式上线,如果你预购了线上版,那么除了下载游戏,你还需要下载57G的补丁。

image.png

  1. 英雄不问出处,编程其实不需要天赋和激情
  1. 我完成日常工作后,每晚还要花四五个小时在 Bootstrap 上工作。下班后,我不能和别人约晚饭,因为我觉得这会让用户失望:我不应该出去玩耍,我应该在 Bootstrap 上工作!—— 桑顿(Jacob Thornton),Bootstrap 的作者之一

对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。
查看原文

赞 0 收藏 0 评论 0

晓前端 发布了文章 · 2020-12-11

【Electron Playground 系列】Dialog与文件选择篇

作者: OBKoro1 

electron原生对话框

electron的原生对话框dialog,提供了系统对话框, 提供了消息提示、消息提示操作以及选择文件、保存文件等操作,今天就跟着本文来了解一下electron。

PS:本文以及相关示例出自electron-playground,所有示例都可以即时运行,这是一个可以快速实验electron的各种相关API的项目,你可以基于它来学习和了解electron的相关api。

1. 消息提示 dialog.showMessageBoxSync

1.1 消息提示

const { dialog } = require('electron')
dialog.showMessageBoxSync({
  type: 'info',
  title: '这里是标题',
  message: '提示内容',
  detail: '额外信息'
})

electron-playgroud运行示例:

dialog-info.gif

1.2 消息提示与确认

const { dialog } = require('electron')
const res = dialog.showMessageBoxSync({
  type: 'info',
  title: '这里是标题',
  message: '提示内容',
  detail: '额外信息',
  cancelId: 1, // 按esc默认点击索引按钮
  defaultId: 0, // 默认高亮的按钮下标
  buttons: ['确认按钮', '取消按钮'], // 按钮按索引从右往左排序
})
console.log('操作结果', res, res === 0 ? '点击确认按钮' : '点击取消按钮') // 根据按钮数组中的下标来判断
console.log('操作中还有个checkboxLabel的单选框需要使用showMessageBox api才可以获取到返回值')

electron-playgroud运行示例:

dialog-action.gif

1.3 API说明

dialog.showMessageBoxSync(browserWindow, options)

显示一个消息框,它将阻止进程,直到消息框被关闭。返回值为点击的按钮的索引。

参数:

  • browserWindow

可以指定一个父窗口,作为模态窗口附加到该窗口。

  • options

    • type: String (可选) - "none" | "info" | "error" | "question" 不同的type提示的图标不同;
    • title: String (可选) - message box 的标题,一些平台不显示,建议使用message和detail;
    • message:  String - message box 的内容.
    • detail: String (可选) - 额外信息
    • buttons String[] - 字符串按钮数组,按钮按索引从右往左排序,如果未指定默认有一个"OK"的按钮。
    • defaultId: Integer (可选) - 默认高亮的按钮下标,回车的时候自动选中该项
    • cancelId: Integer (可选)  按esc默认点击索引按钮

返回值类型:

  • number: 所点击的按钮的索引

dialog.showMessageBox(browserWindow, options)

与dialog.showMessageBoxSync类似,不同点在于:

  1. 这是一个异步方法,返回值为Promise类型;
  2. 对话框可以指定一个checkbox(复选框),返回值中也增加了对应的字段, 同步方法(showMessageBoxSyc)拿不到这个字段

下面是带复选框的示例:

const { dialog } = require('electron')
const res = dialog.showMessageBox({
  type: 'info',
  title: '这里是标题',
  message: '提示内容',
  detail: '额外信息',
  cancelId: 1, // 按esc默认点击索引按钮
  defaultId: 0, // 默认高亮的按钮下标
  checkboxLabel: '单选框内容',
  checkboxChecked: false, // 是否选中单选框
  buttons: ['确认按钮', '取消按钮'], // 按钮按索引从右往左排序
})
console.log('操作结果 promise', res) // 返回一个promise可以通过它判断结果

electron-playgroud运行示例:
dialog-action2.gif

2. 选择文件和文件夹

2.1 选择文件实例

const { dialog, app } = require('electron')
const res = dialog.showOpenDialogSync({
  title: '对话框窗口的标题',
  // 默认打开的路径,比如这里默认打开下载文件夹
  defaultPath: app.getPath('downloads'), 
  buttonLabel: '确认按钮文案',
  // 限制能够选择的文件类型
  filters: [
    // { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
    // { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] },
    // { name: 'Custom File Type', extensions: ['as'] },
    // { name: 'All Files', extensions: ['*'] }, 
  ],
  properties: [ 'openFile', 'openDirectory', 'multiSelections', 'showHiddenFiles' ],
  message: 'mac文件选择器title'
})
console.log('res', res)

electron-playgroud运行示例:
dialog-file.gif

API说明

dialog.showOpenDialogSync(browserWindow,options)

参数:

options

  • defaultPath  String (可选) - 设置对话框默认打开哪个路径,需要设置一个有效路径否则将不生效。
  • buttonLabel  String (可选) - 确认按钮的文案, 当为空时, 将使用默认标签
  • filters 默认所有文件类型都可以选择,设置后,只能选择允许的文件类型
  • properties String[] (可选)

    • openFile - 允许选择文件
    • openDirectory - 允许选择文件夹
    • multiSelections - 允许多选。
    • showHiddenFiles - 显示对话框中的隐藏文件
  • message String (可选) -  mac文件选择器的title
tips: 尝试修改options中的参数来查看效果;

返回值类型:

String[] | undefined - 用户选择的文件或文件夹路径;如果取消对话框,则返回undefined

完整API解释参考文档

3. 保存文件

3.1 实例

const { dialog } = require('electron')
const res = dialog.showSaveDialogSync({
  title: '对话框窗口的标题',
  defaultPath: '', // 打开文件选择器的哪个路径 需要输入一个有效路径
  buttonLabel: '确认按钮文案',
  // 限制能够选择的文件为某些类型
  filters: [
    // { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
    // { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] },
    // { name: 'Custom File Type', extensions: ['as'] },
    // { name: 'All Files', extensions: ['*'] }, 
  ],
  nameFieldLabel: '替换文件', // “文件名”文本字段前面显示的文本自定义标签
  showsTagField: true, // 显示标签输入框,默认值为true
  properties: [ 'showHiddenFiles' ],
  message: 'mac文件选择器title'
})
console.log('res', res)

electron-playgroud运行示例:
dialog-file-save.gif

3.2 API说明

dialog.showSaveDialogSync(browserWindow,options)

参数:

options

  • defaultPath  String (可选) - 设置对话框默认打开哪个路径,需要设置一个有效路径否则将不生效。
  • buttonLabel  String (可选) - 确认按钮的文案, 当为空时, 将使用默认标签
  • filters 默认所有文件类型都可以选择,设置后,只能选择允许的文件类型
  • properties String[] (可选)

    • openFile - 允许选择文件
    • openDirectory - 允许选择文件夹
    • multiSelections - 允许多选。
    • showHiddenFiles - 显示对话框中的隐藏文件
  • message String (可选) -  mac文件选择器的title

返回值类型:

String[] | undefined - 用户选择的文件或文件夹路径;如果取消对话框,则返回undefined;

完整API解释参考文档

3.3 不同场景表现

  1. 选择了一个存在的文件

提示"文件夹中已有相同名称的文件或文件夹。替换它将覆盖其当前内容。",点击确认后返回该文件地址

  1. 选择了一个不存在的文件

返回该不存在的文件地址

4. 错误信息弹窗

dialog.showErrorBox

这个API可以在app模块触发ready事件之前被安全地调用,它通常用在启动时报告错误。 在Linux上, ready事件之前调用这个API, 消息将被发送到stderr, 并且不会出现GUI对话框。

const { dialog } = require('electron')
dialog.showErrorBox('报错标题', '报错内容')
console.log('API非常简单用于报错很方便')

dialog-error.gif

小结

以上就是本文的内容了,更多关于electron的知识点击进入electron-playground仓库来了解吧,希望通过这个项目大家能够更好的学习和理解electron,少走弯路。

  1. electron-playground是一个通过尝试electron各种特性,使electron的各项特性所见即所得, 来达到我们快速上手和学习electron的目的
  2. electron-playground 系统性的整理了electron相关的api,方便你在实际业务中选择相应的app。
  3. 在electron-playground中所有代码都可以即时运行,即时反馈,可以下载一下我们项目来尝试一下,相信你不会失望的。

对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。
我们会经常与大家分享最新最酷的行业技术知识。
欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。
查看原文

赞 2 收藏 0 评论 0

认证与成就

  • 获得 15 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-12-04
个人主页被 995 人浏览