luckness

luckness 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

luckness 收藏了文章 · 1月25日

程序员练级攻略(2018):前端基础和底层原理

图片描述

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

这个是我订阅 陈皓老师在极客上的专栏《左耳听风》,我整理出来是为了自己方便学习,同时也分享给你们一起学习,当然如果有兴趣,可以去订阅,为了避免广告嫌疑,我这就不多说了!以下第一人称是指陈皓老师。

对于前端的学习和提高,我的基本思路是这样的。首先,前端的三个最基本的东西 HTML5、CSS3 和 JavaScript(ES6)是必须要学好的。这其中有很多很多的技术,比如,CSS3 引申出来的 Canvas(位图)、SVG(矢量图) 和 WebGL(3D 图),以及 CSS 的各种图形变换可以让你做出非常丰富的渲染效果和动画效果。

ES6 简直就是把 JavaScript 带到了一个新的台阶,JavaScript 语言的强大,大大释放了前端开发人员的生产力,让前端得以开发更为复杂的代码和程序,于是像 React 和 Vue 这样的框架开始成为前端编程的不二之选。

我一直认为学习任何知识都要从基础出发,所以我会有很大的篇幅在讲各种技术的基础知识和基本原理,尤其是如下的这些知识,都是前端程序员需要一块一块啃掉的硬骨头。

  • JavaScript 的核心原理。这里我会给出好些网上很不错的讲 JavaScript 的原理的文章或图书,你一定要学好语言的特性和其中的各种坑。
  • 浏览器的工作原理。这也是一块硬骨头,我觉得这是前端程序员需要了解和明白的东西,不然,你将无法深入下去。
  • 网络协议 HTTP。也是要着重了解的,尤其是 HTTP/2,还有 HTTP 的几种请求方式:短连接、长连接、Stream 连接、WebSocket 连接。
  • 前端性能调优。有了以上的这些基础后,你就可以进入前端性能调优的主题了,我相信你可以很容易上手各种性能调优技术的。
  • 框架学习。我只给了 React 和 Vue 两个框架。就这两个框架来说,Virtual DOM 技术是其底层技术,组件化是其思想,管理组件的状态是其重点。而对于 React 来说,函数式编程又是其编程思想,所以,这些基础技术都是你需要好好研究和学习的。
  • UI 设计。设计也是前端需要做的一个事,比如像 Google 的 Material UI,或是比较流行的 Atomic Design 等应该是前端工程师需要学习的。

而对于工具类的东西,这里我基本没怎么涉及,因为本文主要还是从原理和基础入手。那些工具我觉得都很简单,就像学习 Java 我没有让你去学习 Maven 一样,因为只要你去动手了,这种知识你自然就会获得,我们还是把精力重点放在更重要的地方。

下面我们从前端基础和底层原理开始讲起。先来讲讲 HTML5 相关的内容。

HTML5

  • HTML5 权威指南 ,本书面向初学者和中等水平 Web 开发人员,是牢固掌握 HTML5、CSS3 和 JavaScript 的必读之作。书看起来比较厚,是因为里面的代码很多。
  • HTML5 Canvas 核心技术 ,如果你要做 HTML5 游戏的话,这本书必读。

对于 SVG、Canvas 和 WebGL 这三个对应于矢量图、位图和 3D 图的渲染来说,给前端开发带来了重武器,很多 HTML5 小游戏也因此蓬勃发展。所以,你可以学习一下。

学习这三个技术,我个人觉得最好的地方是 MDN。

最后是几个资源列表。

CSS

在《程序员练级攻略(2018)》系列文章最开始,我们就推荐过 CSS 的在线学习文档,这里再推荐一下

MDN Web Doc - CSS 。我个人觉得只要你仔细读一下文档,CSS 并不难学。绝大多数觉得难的,一方面是文档没读透,另一方面是浏览器支持的标准不一致。所以,学好 CSS 最关键的还是要仔细地读文档。

之后,在写 CSS 的时候,你会发现,你的 CSS 中有很多看起来相似的东西。你的 DRY - Don’t Repeat Yourself 洁癖告诉你,这是不对的。所以,你需要学会使用 LESSSaSS
这两个 CSS 预处理工具,其可以帮你提高很多效率。

然后,你需要学习一下 CSS 的书写规范,前面的《程序员修养》一文中提到过一些,这里再补充几个。

如果你需要更有效率,那么你还需要使用一些 CSS Framework,其中最著名的就是 Twitter 公司的 Bootstrap,其有很多不错的 UI 组件,页面布局方案,可以让你非常方便也非常快速地开发页面。除此之外,还有,主打清新 UI 的 Semantic UI 、主打响应式界面的 Foundation 和基于 Flexbox 的 Bulma

当然,在使用 CSS 之前,你需要把你浏览器中的一些 HTML 标签给标准化掉。所以,推荐几个 Reset 或标准化的 CSS 库:NormalizeMiniRest.csssanitize.cssunstyle.css

关于更多的 CSS 框架,你可以参看 Awesome CSS Frameworks

接下来,是几个公司的 CSS 相关实践,供你参考。

CodePen’s CSS

Github 的 CSS

Medium’s CSS is actually pretty f*ing good

CSS at BBC Sport

Refining The Way We Structure Our CSS At Trello

最后是一个可以写出可扩展的 CSS 的阅读列表 A Scalable CSS Reading List

JavaScript

下面是学习 JavaScript 的一些图书和文章。

浏览器原理

你需要了解一下浏览器是怎么工作的,所以,你必需要看《How browsers work》。这篇文章受众之大,后来被人重新整理并发布为《How Browsers Work: Behind the scenes of modern web browsers》,其中还包括中文版。这篇文章非常非常长,所以,你要有耐心看完。如果你想看个精简版的,可以看我在 Coolshell 上发的《浏览器的渲染原理简介》或是看一下这个幻灯片

然后,是对 Virtual DOM 的学习。Virtual DOM 是 React 的一个非常核心的技术细节,它也是前端渲染和性能的关键技术。所以,你有必要要好好学习一下这个技术的实现原理和算法。当然,前提条件是你需要学习过前面我所推荐过的浏览器的工作原理。下面是一些不错的文章可以帮你学习这一技术。

网络协议

小结

总结一下今天的内容。我一直认为学习任何知识都要从基础出发,所以今天我主要讲述了 HTML5、CSS3 和 JavaScript(ES6)这三大基础核心,给出了大量的图书、文章以及其他一些相关的学习资源。之后,我建议你学习浏览器的工作原理和网络协议相关的内容。我认为,掌握这些原理也是学好前端知识的前提和基础。值得花时间,好好学习消化。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你们的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

查看原文

luckness 关注了用户 · 1月23日

dreamapplehappy @dreamapplehappy

微信公众号「关山不难越」
除了代码,生活还有很多值得经历和体味的事情;去旅游,去看不一样的风景;活,该快乐!

无欲速,无见小利。欲速则不达,见小利则大事不成

个人博客:https://github.com/dreamapple...

关注 508

luckness 赞了文章 · 1月23日

2020年,我第一次很正式地写年终总结

好像我一直没有写年终总结的习惯,之前也一直都是看别人写年终总结。有时看别人的年终总结,会感叹别人这一年过的好充实。看了很多书,去了很多地方;或者结识了很多新的朋友,能力得到了很多的提高等等吧。也常常会感觉自己这一年好像没干什么事情,会有那么一点沮丧。

当我写这篇文章的时候,我突然意识到自己好像已经忘记了2020年定下的什么目标了。不难想象自己当时定下目标的时候肯定是踌躇满志的,没想到已经连定下的什么目标都忘记了

所以写一下年终总结对我来说还是很有必要的,一方面可以记录一下自己这一年做了哪些事情。另一方面也可以记录自己对来年的一个目标,等到下一年再次写年终总结的时候就有了一个对比。可以知道自己是进步了还是倒退了,可以看看自己之前定下的目标都实现了多少。这样我觉得也挺好的,今年就作为一个开始吧。

工作和技术分享,输出了14篇文章

2020年的后半年加入了一家中型的互联网公司,再次开启了自己的职场生涯。我也重拾了自己写博客的习惯。作为一个开发者,写博客在目前来说依然是一个不错的方式让大家认识和了解你。对于自己来说,写博客可以记录和沉淀自己学习过的知识或者解决的问题。如果自己下次再遇到类似的问题,就知道如何去解决,如果忘记了就查找一下之前的博客。

写博客也是一个强化自己知识的过程。很多时候你以为你掌握了,但是真的到把这些知识写成博客,然后想要分享给别人的时候,你会发现自己好像并没有掌握的很牢固,也会有一些不熟练的地方。而写博客这个过程恰恰就是帮助你把这些不熟悉的地方打通的一个很好的方法

关于写博客我自己也有一些心得,想在这里跟大家分享一下。我觉得写博客最重要的其实是坚持,然后是自己对博客质量的要求,最后是平台的选择

先说一下坚持吧,我感觉每一个前端工程师或多或少都倒腾过自己的博客。我最早写博客是在 CSDN 和博客园,后来又转移到思否,掘金和知乎上,也使用过 Hexo,以及 Ghost 搭建过自己的博客。反正前前后后换了很多平台,但是文章却没留下来几篇。所以后来我就决定不再纠结在那个平台写博客了。而是把精力放在坚持写博客上,我们可以看看,基本上那些技术很厉害的大牛也都是专注于自己博客的内容,而不是纠结博客的形式。所以我后来就把我自己的博客放在了Github上,简简单单的,反而让我有了坚持写下去的动力。看着文章一篇一篇增加,也是会有一点成就感的

然后就是博客的质量了,当我们能够坚持写博客之后,接下来就需要专注于我们博客的质量。我们写博客大多是为了分享给别人或者用做记录以便自己查看。如果是分享给别人看的话,如何让大家从你的博客中学习到东西,或者能够吸引大家来看就是一个需要考虑的问题了。一般来说,只要我们能够把要分享的内容讲清楚,内容逻辑条理清晰。这样下来,文章的质量一般不会太差。如果你的文笔还不错那就又是一个加分项了。

持续高质量的文章输出很快就会给你带来很多的关注者,也会有很多厉害的人主动跟你联系。这样下来,你通过写博客不但能够提升自己的技术,提高自己的知名度,还可以跟优秀的人建立联系。这些都是你的宝贵财富,也都是可以转换为你自己实实在在的利益的。更重要的是,你写下的文章随着时间的推移,看到的人会越来越多,会持续给你带来更多收获的

最后就是平台的选择了,不同平台用户感兴趣的不一样,用户活跃度也不一样。这个要根据你自己的文章在各个平台的数据来判断,可以选择一两个作为主要的分享平台,其它的作为辅助。选择好一个合适的平台后,可以多跟这个平台的用户保持积极的沟通交流,你会收到来自大家的肯定,这会让你有继续坚持写下去的动力。

2020年写的一些文章

在2020年我一共写了14篇博客,不算很多,但是每一篇博客我都写得很用心。我个人在数量和质量上进行权衡的时候,我还是比较看重质量的。当然在接下来的2021年,我希望自己能够尽量半个月写一篇文章。在质量不下降的情况下,提升写博客的数量。

之前的博客主要围绕两个主题来写的,一个是关于设计模式的。我把这个系列称作设计模式大冒险。另一个系列是关于正则表达式的,这个系列最近的更新比较少了。主要是如果要深入的讲解好正则表达式,需要学习的知识很多;我也不希望自己一知半解的就去写一篇文章。这样是对自己文章和读者的不负责。在新的一年,如果精力允许的话我还会继续更新关于正则表达式的一些文章,希望大家可以保持关注。

细心的你也许会发现,我分享的知识都是不限于某一种语言的。我个人认为,学习知识应该学习通用的,适用范围更广的。比如上面我分享的设计模式正则表达式,以及数据结构算法,还有编程的思想等等。这些知识一旦掌握了,在每一种编程语言上面都可以使用。用世俗的评判标准就是“性价比”比较高

当然我不是说你就不需要对某一种语言,某一个框架,或者某一个方向深入研究了。这些也是很有必要的。只不过有些类似的东西,你掌握好一个就可以了。你的时间和精力是有限的,要把时间放在更有用的地方。当然,如果是因为工作的需要的话,那就是另外一回事了。工作的事情还是要做好的,要首先把自己工作需要掌握的知识掌握熟练之后,再考虑去学习其它的提升自己的知识。

上面是关于写博客,学习技术和技术分享的事情。接下来分享一些关于开发过程中的心得。我们在平时的开发中,如果遇到一些不是很好开发的需求,一定要问一下产品做这个功能的目的是为了什么。不要上来就去开始写代码了,很多情况下想要实现一个功能有很多种解决办法,产品告诉你的方式也许是一个不好实现的方式。这个时候你就可以跟产品沟通换一种方式是否可行,很多情况下经过我们沟通之后,发现可以用一种简单的方式实现同样的效果。

多跟产品沟通,多思考为什么要这样做。这样,在以后的开发过程中,你会减少很多不必要的编码劳动,也能够工作的比较舒服一点

主线程小程序的持续维护

关注我的一些朋友知道我跟我的女朋友在2019年做了一个创业项目,是一个学习打卡类小程序,小程序的名字是主线程,如果你想了解更多可以看一下之前思否的一个小采访,如果全身心投入1年,但是收入是0,你还愿意做独立开发者吗?

主线程小程序的一些截图

在2020年的4月份我们决定暂停主线程项目的继续开发,只做日常的系统维护工作。因为当时的经济状况已经不允许我们继续投入到这个项目中去了。这个决定在目前的我们看来还算是一个正确的决定。因为如果继续下去,我们可能会面临一系列的问题和压力。这些问题和压力对于当时的我们来说大概率是解决不了的

那么,主线程这个项目成功了吗?这个问题要看你是从哪个角度去看了。如果单纯从世俗的金钱回报上说,我们肯定是失败了,而且失败得很彻底。不仅没有赚到一分钱(除去一些赞赏),还倒贴了一笔钱。但是如果站在另外一个角度看的话,我觉得对于我们两个来说还是很成功的。首先我们从0到1把这个产品做了出来,并且这个产品也得到了不少用户的好评,一些关于产品的体验评价可以在这里浏览哦。到目前为止,在没有花钱推广的情况下已经有15000+的用户了。也算是一个小小的成就吧

每当收到用户对主线程的夸奖的时候,我们那种开心和成就感是用金钱换不来的。其实最重要的收获是在于我们对创业,对产品和看待事情的角度都发生了变化。以前的角色是作为公司的员工,现在是以一个创业者的身份来看待和处理这些事情,这中间我们学习到了很多

总之我们不后悔花费了一年的时间开发了一个没有盈利的产品,接下来我们还会继续维护这个项目。如果你有什么关于创业和开发产品的想法想跟我交流的,也欢迎添加我的个人微信(请备注来意),我们一起沟通交流一下吧。

身体健康和读书

去年前半年因为没有上班,所以有比较多的时间锻炼身体。跑步基本每周有两三次,那段时间经历也还算充沛。后半年上班之后,时间比较少,再加上冬天也比较冷。跑步的次数就很少了,从Kepp上的记录来看,我后半年关于做俯卧撑的锻炼就只做了13次,因为锻炼比较少。可以明显感觉到每天工作到快晚上下班的时候会比较疲惫。

也因为工作的原因,久坐在办公室,颈椎和腰有时也会感觉到有点不舒服。关于吃饭,好的一点是去年点外卖的次数变少了。去外面小餐馆吃饭的次数变多了。我个人会觉得出去走动一下挺好的,呼吸一下外面的新鲜空气,给自己换个心情。走路去外面吃个饭也算是一个小小的锻炼吧

去年读书没有读很多,《增长黑客》 这本书基本读完了,感觉收获很多。对于想创业的朋友应该很有帮助的,推荐感兴趣的可以读一读。还有一本书是 《丰子恺:无宠不惊过一生》 ,还没读完,不过每次读都会给我一些启发,给我带来一些内心的平静。感兴趣的也可以读一下。

因为短视频的兴起,感觉自己身边很多的人都没有了读书的习惯了。很多人可能都没有耐心读完一篇很长的文章了。我个人感觉这不是一个很好的习惯,我之前也有一段时间对抖音比较沉迷,每周在上面花费的时间有4,5个小时。后来我意识到,看这些短视频并不能给我带来什么能力和技术的提升,大部分情况下都只是消耗了我的时间而已。所以我慢慢就不怎么看了,现在手机上的抖音虽然没有被我卸载,但是一直处于待更新的状态。

我觉得,知识的获取需要一个比较完整的体系。看书或者看一个系列的文章和视频在目前依然还是一个挺好的方式去获取知识。碎片化的知识会让你感觉自己掌握了很多知识,但是这些知识没有成为一个体系,没有融合进你自己的知识系统的话,这些知识是很难被你再次吸收和利用的。我自己平时比较喜欢在 《珍新闻》 上面获取一些资讯,个人感觉里面的文章都还是不错的,大部分能够把一个事情讲明白,讲解完整。很多文章也会带给你一些思考。如果感兴趣的话,可以体验一下。

新年的计划

迎接新的一年

关于去年的一些总结已经写得差不多了,接下来就是对2021的的一些展望了。下面是我列的一些想在2021年完成的一些目标。

  • 阅读12本书,希望自己可以一个月读一本书,书的种类不做限制,先培养自己读书的习惯
  • 博客继续更新,希望半个月可以写一篇。会继续更新设计模式大冒险系列,和正则表达式系列。应该还会加上数据结构和算法。之前写文章会比较纠结一些细节,导致写一篇文章用的时间比较久,新的一年我要把写文章的流程好好优化一下,争取在原来的基础上再多写几篇高质量的文章。
  • 争取每周可以锻炼一次,不管是跑步,俯卧撑锻炼还是爬山或者其他的运动。把身体搞好是很重要的事情
  • 多认识一些新的朋友,多跟不同行业的人沟通交流。提升一下自己的人脉和社交水平

上面的这些就是我打算在2021年完成的一些事情了,当然还有一些我现在正在做但是不知道自己能不能坚持下来的事情。如果我坚持下来了,会告诉大家的。希望大家保持关注。

如果你有什么想跟我沟通的,可以添加我的微信或者关注我的公众号关山不难越,新的一年我们一起加油吧。新的一年祝大家都身体健康,然后定下的目标都能够实现。

本文参与了 SegmentFault 思否征文「2020 总结」,欢迎正在阅读的你也加入
查看原文

赞 9 收藏 1 评论 4

luckness 发布了文章 · 1月22日

THREE.js如何扩展已有材质

最近,想要给一个立方体不同的面赋不同的材质,可以是纯色也可以是贴图。然后,我就觉得一个简单的shaderMaterial就可以解决了。但是放到实际应用场景中发现,别的物体有光照效果,我写的没有光照效果。所以还得给自定义着色器添加光照效果,于是就有了这篇文章。

本文主要从以下几个方面进行讲述:

  1. 创建没有光照效果的立方体;
  2. 扩展lambert材质,创建有光照效果的立方体;

适用人群:对THREE.js和glsl有基本了解的人。

创建没有光照效果的立方体

本示例会创建一个前后左右面是纯色,上下面是贴图的立方体。该部分的内容主要包括以下部分:

  1. 创建bufferGeometry;
  2. 自定义shaderMaterial,在shaderMaterial里面判断是用纯色还是贴图;
  3. 创建mesh。

创建bufferGeometry

因为想更深入的了解THREE.js的实现原理,所以这块没有直接使用BoxBufferGeometry,而是自己定义顶点信息:

const geometry = new THREE.BufferGeometry()
const position = [ // 每个面两个三角形,每个三角形三个顶点,每个顶点三个坐标值,所以一个三角形是3*3=9个值,一个面是3*3*2=18个值
  -1, -1, 1, 1, -1, 1, 1, 1, 1, // front face
  1, 1, 1, -1, 1, 1, -1, -1, 1,
  1, -1, 1, 1, -1, -1, 1, 1, -1, // right face
  1, 1, -1, 1, 1, 1, 1, -1, 1,
  1, -1, -1, -1, -1, -1, -1, 1, -1, // back face
  -1, 1, -1, 1, 1, -1, 1, -1, -1,
  -1, -1, -1, -1, -1, 1, -1, 1, 1, // left face
  -1, 1, 1, -1, 1, -1, -1, -1, -1,
  -1, 1, 1, 1, 1, 1, 1, 1, -1, // top face
  1, 1, -1, -1, 1, -1, -1, 1, 1,
  1, -1, 1, -1, -1, 1, -1, -1, -1, // bottom face
  -1, -1, -1, 1, -1, -1, 1, -1, 1
]
// 定义了一个长宽高都是2的立方体,所以上面xyz的坐标要么是1,要么是-1
geometry.setAttribute('position', new THREE.BufferAttribute(Float32Array.from(position), 3))

然后,给每个顶点添加颜色信息,每个顶点既可以是纯色也可以是贴图,纯色需要rgb三个分量,贴图需要uv两个分量,所以每个顶点至少需要三个分量来表示。

那么,如何判断这个顶点是纯色还是贴图呢?
我们当然可以再使用一个数组来表示。但是注意到上面贴图只需要两个分量,那么我们就可以利用第三个分量来判断。glsl语言里面rgb色值的范围是0-1,所以我们可以使用这个范围之外的值表示这是一个贴图。

那取什么值呢?我们这个立方体定义了上下面是贴图,也就是贴图不只一个,那么这个值还要能推导出是第几个贴图。我这里设置了一个textureBaseIndex2的变量。

const colors = []
const textureBaseIndex = 2
for (let i = 0; i < 12; i++) {
  switch (i) {
    case 0: // front color
    case 1:
      colors.push(1, 0, 0, 1, 0, 0, 1, 0, 0) // 红
      break
    case 2: // right color
    case 3:
      colors.push(0, 1, 0, 0, 1, 0, 0, 1, 0) // 绿
      break
    case 4: // back color
    case 5:
      colors.push(0, 0, 1, 0, 0, 1, 0, 0, 1) // 蓝
      break;
    case 6: // left color
    case 7:
      colors.push(1, 1, 0, 1, 1, 0, 1, 1, 0) // 黄
      break
    case 8: // top texture uv,前两个分量表示uv,第三个分量表示取第几个纹理,在纹理实际索引值的基础上加上textureBaseIndex
      colors.push(0, 0, textureBaseIndex + 0, 1, 0, textureBaseIndex + 0, 1, 1, textureBaseIndex + 0)
      break
    case 9:
      colors.push(1, 1, textureBaseIndex + 0, 0, 1, textureBaseIndex + 0, 0, 0, textureBaseIndex + 0)
      break
    case 10: // bottom texture uv,前两个分量表示uv,第三个分量表示取第几个纹理,在纹理实际索引值的基础上加上textureBaseIndex
      colors.push(1, 1, textureBaseIndex + 1, 0, 1, textureBaseIndex + 1, 0, 0, textureBaseIndex + 1)
      break
    case 11:
      colors.push(0, 0, textureBaseIndex + 1, 1, 0, textureBaseIndex + 1, 1, 1, textureBaseIndex + 1)
      break
  }
}
geometry.setAttribute('color', new THREE.BufferAttribute(Float32Array.from(colors), 3))

自定义shanderMaterial

顶点着色器的代码比较简单,把color属性通过varying变量vColor传给片元着色器:

function getVertexShader () {
  return `
    attribute vec3 color;

    varying vec3 vColor;

    void main () {
      vColor = color;
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }
  `
}

接下来是片元着色器,主要有以下几点:

  1. 通过vColor.z判断是纯色还是贴图;
  2. 把贴图信息通过sampler2D数组传入,然后在根据vColor.z获取数组下标的时候,前面在生成下标的时候加了一个textureBaseIndex,所以用的时候得先减去;
  3. 通过下标获取sampler2D数组中的某一项的时候,不能直接使用textures[index],glsl要求[]里面的内容必须是Integral constant expression,所以使用了一个generateSwitch函数动态生成一系列if代码;

完整代码如下:

function getFragmentShader (textureLength, textureBaseIndex) {
  function generateSwitch () {
    let str = ''
    for (let i = 0; i < textureLength; i++) {
      str += `${str.length ? 'else' : ''} if (index == ${i}) {
        gl_FragColor = texture2D(textures[${i}], vec2(vColor.x, vColor.y));
      }
      `
    }

    return str
  }

  return `
    ${textureLength ? `
      uniform sampler2D textures[${textureLength}];
    ` : ''}

    varying vec3 vColor;

    void main () {
      ${textureLength ? `
        if (vColor.z <= 1.0) {
          gl_FragColor = vec4(vColor, 1.0);
        } else {
          int index = int(vColor.z) - ${textureBaseIndex};
          ${generateSwitch()}
        }` : `
        gl_FragColor = vec4(vColor, 1.0);
        `
      }
    }
  `
}

生成自定义材质:

const textures = [
  new THREE.TextureLoader().load('./textures/colors.png'), // 顶面贴图
  new THREE.TextureLoader().load('./textures/colors.png') // 底面贴图
]
const material = new THREE.ShaderMaterial({
  uniforms: {
    textures: { value: textures } // 片元着色器中会使用
  },
  vertexShader: getVertexShader(),
  fragmentShader: getFragmentShader(textures.length, textureBaseIndex)
})

创建mesh

这步就比较简单了,创建一个mesh,并添加到场景中:

const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)

这样,立方体就创建好了。本例使用了基本的WebGLRenderer,Scene,PerspectiveCamera,没有特殊处理,这里就不再写了。实现效果截图如下:
front/right/top面效果截图
image.png
back/left/bottom面效果截图
image.png

扩展lambert材质,创建有光照效果的立方体

我的实际应用场景中的物体是lambert材质,也就是MeshLambertMaterial。所以,下面的实例代码以扩展lamert材质的光照效果为例。要想使用该实现方案,最好研究下THREE.js的源码。

THREE.js里面预先定义了一系列材质,MeshLambertMaterial材质就是其中之一。这部分代码在src/renderers/shaders文件夹下面,ShaderLib.js里面是材质的入口,比如MeshLambertMaterial:

const ShaderLib = {
    lambert: {
        uniforms: mergeUniforms( [ // uniform变量
            UniformsLib.common,
            UniformsLib.specularmap,
            UniformsLib.envmap,
            UniformsLib.aomap,
            UniformsLib.lightmap,
            UniformsLib.emissivemap,
            UniformsLib.fog,
            UniformsLib.lights,
            {
                emissive: { value: new Color( 0x000000 ) }
            }
        ] ),

        vertexShader: ShaderChunk.meshlambert_vert, // 顶点着色器代码
        fragmentShader: ShaderChunk.meshlambert_frag // 片元着色器代码
    },
}

ShaderChunk和ShaderLib文件夹下面就是实际的着色器代码,区别是ShaderLib是THREE.js给我们直接使用的,ShaderChunk是更细粒度的代码。ShderLib里面的不同材质有很多共有的代码,所以这个共有的代码就提取成一个个ShaderChunk,达到复用的目的。一个材质是由多个ShaderChunk生成的。我们可以打开ShaderLib/meshlambert_vert.glsl.js文件,会发现里面有很多#include语句,这些语句最后会被替换为实际的ShaderChunk里面的片段。

我们看到shaders文件夹下面只是定义了材质的结构以及glsl代码片段,那么,完整效果的代码是在哪生成的呢?
src/renderers/webgl/WebGLProgram.js文件。

列一下这个文件我了解的一些知识点:

  1. 首先根据我们创建材质时的参数,定义一些#define变量,添加在着色器代码的前面;
  2. 解析ShaderLib里面的代码,把#include语句替换为实际代码,参见resolveIncludes函数;

更重要的是,ShaderLib里面预定义的一些材质,挂在了THREE变量上,这样我们就可以获得原始代码,并通过修改部分glsl代码达到扩展材质的目的。

比如,上面的那个例子,首先改造一下顶点着色器:

  1. 在默认的lambert顶点着色器代码前面添加属性变量和varying变量;
  2. 在main函数里面给varying变量赋值;
  3. 具体插在原始main函数的哪一行看你的需求;
function getVertexShader () {
  let shader = `
    attribute vec3 color;
    varying vec3 vColor;
  ` + THREE.ShaderLib.lambert.vertexShader

  const index = shader.indexOf('#include <uv_vertex>')
  shader = shader.slice(0, index) + `
    vColor = color;
  ` + shader.slice(index)

  return shader
}

片元着色器的改造如下:

  1. 在默认的lambert片元着色器代码前面添加uniform变量和varying变量;
  2. 在main函数里面插入我们的代码,插入位置我选在了#include <color_fragment>后面,因为这个代码片段和我现在的修改做了类似的事情,所以插在这个位置是可以的。注意,此时就不是直接给gl_FragColor赋值了,而是把效果加在diffuseColor变量上。实际开发的时候,具体修改哪个值就得参考THREE.js源码了。
function getFragmentShader (textureLength, textureBaseIndex) {
  function generateSwitch () {
    let str = ''
    for (let i = 0; i < textureLength; i++) {
      str += `${str.length ? 'else' : ''} if (index == ${i}) {
        diffuseColor *= texture2D(textures[${i}], vec2(vColor.x, vColor.y));
      }
      `
    }

    return str
  }

  let shader = `
    uniform sampler2D textures[${textureLength}];
    varying vec3 vColor;
  ` + THREE.ShaderLib.lambert.fragmentShader

  const index = shader.indexOf('#include <color_fragment>')
  shader = shader.slice(0, index) + `
    ${textureLength ? `
      if (vColor.z <= 1.0) {
        diffuseColor.rgb *= vColor;
      } else {
        int index = int(vColor.z) - ${textureBaseIndex};
        ${generateSwitch()}
      }` : `
      diffuseColor.rgb *= vColor;
      `
    }
  ` + shader.slice(index)

  return shader
}

然后,创建着色器:

  1. 修改一下uniform变量,把lambert默认的uniform变量也添加进去;
  2. 添加lights参数为true,否则代码报错;
  3. THREE源码默认diffuse是0xeeeeee,覆盖一下,修改为0xffffff;
const material = new THREE.ShaderMaterial({
  uniforms: THREE.UniformsUtils.merge([
    THREE.ShaderLib.lambert.uniforms,
    {
      textures: { value: textures }
    },
    {
      diffuse: {
        value: new THREE.Color(0xffffff)
      }
    }
  ]),
  vertexShader: getVertexShader(),
  fragmentShader: getFragmentShader(textures.length, textureBaseIndex),
  lights: true
})

这个时候刷新页面,会发现是一个黑色的立方体,这是因为我们还没有添加光源:

const light = new THREE.DirectionalLight( 0xffffff ); // 平行光
light.position.set( 1, 1, 1 );
scene.add( light );

const ambient = new THREE.AmbientLight(0xffffff, 0.7); // 环境光
scene.add(ambient)

之所以添加两个光源是因为发现:

  1. 环境光不受几何物体法线影响;
  2. 平行光受几何物体法线影响;

添加上述代码后,如果把环境光注释掉,会发现材质还是黑色的,这是因为上面创建的geometry没有法线信息,所以需要使用下面的方法添加一下法线信息:

geometry.computeVertexNormals()

最终效果截图如下:
front/right/top面效果截图,同时受平行光和环境光影响
image.png
back/left/bottom面效果截图,不在平行光照射范围内,只受环境光影响
image.png

总结

本文例子只是为了讲解如何扩展已有材质,可能并没有任何使用意义。

上述观点是基于目前对THREE.js的研究结果,可能会有认知错误。如有,欢迎留言评论。

参考资料:

  1. Extending the Built-in Phong Material Shader in Three.js
  2. Integral constant expression
查看原文

赞 3 收藏 2 评论 0

luckness 发布了文章 · 1月5日

前端WebSocket知识点总结

最近研究了下WebSocket,总结下目前对WebSocket的认知。本文不是基于WebSocket展开的一个从0到1的详细介绍。如果你从来没有了解过WebScoket,建议可以先搜一些介绍WebSocket的文章,这类文章还是挺多的,我就不再赘述了。

下面的内容是基于你对WebSocket有基本了解后展开的几个小的知识点:

  1. ping/pong协议;
  2. 如何使ERROR_INTERNET_DISCONNECTED错误信息不显示在控制台;

ping/pong协议

背景:连接WebSocket的时候,发现WebSocket刚连接上没过多久就断开了,为了保持长时间的连接,就想到了ping/pong协议。

问题:

  1. ping/pong是一种特殊的帧类型吗,还是说只是一种设计思想?
  2. JS有原生方法支持发送ping/pong消息吗

通过WebSocket协议,发现ping/pong确实是一种特殊的帧类型:

The Ping frame contains an opcode of 0x9.
The Pong frame contains an opcode of 0xA.

那么,上面所说的opcode又是什么东西呢?讲opcode就得说到帧数据格式
image.png
通过上图可以发现,除了最后面的Payload Data,也就是我们要发送的数据之外,还会有一些其他信息。我觉得可以类比http请求的请求头部分。上图中第5-8位表示的就是opcode的内容。其余字段的含义可以参考上述WebSocket规范,或者搜WebSocket协议数据帧格式,这类博客还是挺多的。

拿nodeJS举个例子:
在浏览器端发起WebSocket的时候,会发送一个http请求,注意请求头里面的Upgrade字段,意思就是我要升级到websocket连接:

GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

此时,nodeJS就可以监听upgrade事件,去做拒绝或者升级操作,注意下这个事件里面有个参数socket:

socket: <stream.Duplex> Network socket between the server and client

socket有一个write方法,该方法是可以用来写帧数据的,也就是上面帧格式里面的全部数据,而不仅仅是Payload Data。

ws仓库就是使用了socket的write方法发送了根据WebSocket协议定义的ping/pong,部分关键代码如下:

doPing(data, mask, readOnly, cb) {
  this.sendFrame(
    Sender.frame(data, {
      fin: true,
      rsv1: false,
      opcode: 0x09, // ping opcode
      mask,
      readOnly
    }),
    cb
  );
}
doPong(data, mask, readOnly, cb) {
  this.sendFrame(
    Sender.frame(data, {
      fin: true,
      rsv1: false,
      opcode: 0x0a, // pong opcode
      mask,
      readOnly
    }),
    cb
  );
}
sendFrame(list, cb) {
  if (list.length === 2) {
    this._socket.cork();
    this._socket.write(list[0]);
    this._socket.write(list[1], cb);
    this._socket.uncork();
  } else {
    this._socket.write(list[0], cb);
  }
}

所以,nodeJS是可以实现WebSocket协议定义的ping/pong帧的。原因是我们可以拿到socket对象,并且该对象提供了可以发送完整帧数据的方法。那么浏览器端呢?

浏览器提供了原生的WebSocket构造函数用来创建一个WebSocket实例,该实例只提供了一个send方法,并且该send方法只能用来发送上述协议中Payload Data的内容,浏览器会根据send的参数自动生成一个完整的帧数据。所以,在浏览器端是没法控制除了Payload Data之外的帧内容的,也就是无法自定义opcode。所以,也就实现不了WebSocket规范定义的ping/pong协议。

此时,我们就可以把ping/pong当成一种用来解决特定问题的设计模式。既然我们只能自定义Payload Data的内容,那么我们可以简单的在Payload Data里面添加一个字段用于区分是ping/pong帧,还是普通的数据帧,比如type。当type字段是ping/pong的时候表明是ping/pong帧,如果是其他字段才是普通的数据帧。

如何使ERROR_INTERNET_DISCONNECTED错误信息不显示在控制台

当断网的时候,连接WebSocket会发现浏览器控制台会log一个错误信息:

WebSocket connection to 'ws://...' failed: Error in connection establishment: net::ERR_INTERNET_DISCONNECTED

原先的开发经验是,控制台如果有报错的话,肯定是代码某个地方有错误,并且没有被我们的代码捕获到,所以就会在控制台抛出,如果使用了try catch 或者全局的window.onerror捕获到了错误信息,就不会在控制台打印了。所以,我就尝试了上述方法,发现捕捉不到,还是会在控制台log。

另外,WebSocket提供了两个事件,onerror和onclose。当发生上述错误信息的时候,onerror和onclose是会被调用的。但是,此时控制台还是会有上述报错信息。

经过一番查找,发现无法阻止上述错误信息显示在控制台。

那么,为什么浏览器会设计这样的行为呢?猜测原因如下:
上面说到通过onerror和onclose事件是可以捕捉到WebSocket创建失败的,但是,查看这两个事件的参数,我们只能从中找到一个code是1006的属性,输出在控制台的错误信息ERR_INTERNET_DISCONNECTED在参数里面找不到。接着,看一下code1006相关的东西:

User agents must not convey any failure information to scripts in a way that would allow a script to distinguish the following situations:

*   A server whose host name could not be resolved.
*   A server to which packets could not successfully be routed.
*   A server that refused the connection on the specified port.
*   A server that failed to correctly perform a TLS handshake (e.g., the server certificate can't be verified).
*   A server that did not complete the opening handshake (e.g. because it was not a WebSocket server).
*   A WebSocket server that sent a correct opening handshake, but that specified options that caused the client to drop the connection (e.g. the server specified a subprotocol that the client did not offer).
*   A WebSocket server that abruptly closed the connection after successfully completing the opening handshake.

In all of these cases, the the WebSocket connection close code would be 1006, as required by WebSocket Protocol. 

Allowing a script to distinguish these cases would allow a script to probe the user's local network in preparation for an attack.

从上述规范可以看到,规范是禁止浏览器向脚本传递下述造成WebSocket连接失败的具体原因的,只允许向脚本传递一个1006的code码,否则,用户就可以探测到局部网的信息,进而发起攻击。举个例子,上面那种断网的情况,脚本中只能得到1006的状态码,比如下面这种报错

Error in connection establishment: net::ERR_CONNECTION_REFUSED

也只能从onerror中获得一个1006的code码。

所以,作为开发人员,浏览器要怎么在告诉我们具体的错误信息的同时又阻止有可能发生的攻击呢?答案就是在控制台把具体的错误信息log出来。

总结

基于目前了解的知识总结的一篇博客,如有错误,欢迎留言讨论。

查看原文

赞 4 收藏 2 评论 0

luckness 赞了文章 · 2020-12-18

一文搞懂Babel配置

最近在做一次Babel6升级Babel7的操作,把升级的过程和关于babel的配置进行一次总结。

1 为什么讲Babel配置

Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

其实目前前端开发,各种项目模版,你也不需要关心babel的配置,随便拉下来一个就能运行,但是要做定制化的处理还是要把babel搞懂。

@babel/cli是Babel的命令行工具,我们一般用不到,因为我们通常都是用babel-loader,里边使用的是@babel/core的api形式,我们只需要关心Babel的配置,如果有需要在编译阶段对代码进行处理 也可以写自己的插件,但是大部分场景是需要我们把Babel的配置搞清楚。

2 Babel的配置文件

Babel6的阶段 最常用的是.babelrc,但是现在Babel7支持了更多格式:

const RELATIVE_CONFIG_FILENAMES = [".babelrc", ".babelrc.js", ".babelrc.cjs", ".babelrc.mjs", ".babelrc.json"];
package.json files with a "babel" key。

配置文件的格式如下:

{
    "presets": [
      [
        "@babel/preset-env",
        {
          "modules": "commonjs"
        }
      ]
    ],
    "plugins": [
      [
        "@babel/plugin-transform-runtime",
        {
          "corejs": 3
        }
      ],
      "@babel/plugin-syntax-dynamic-import",
    ]
  }
}

更详细介绍参见Babel Config

2.1 pluginspreset

配置文件中主要有两个配置pluginspreset,@babel/core本身对代码不做任何的转化,但是提供了对代码的解析,各种插件在解析过程中可以进行代码的转换,比如处理箭头函数的插件@babel/plugin-transform-arrow-functions等等,所以比如针对ES6语法的解析就需要很多插件,preset预设就是配置的子集,预设的一套配置,可以根据参数动态的返回配置。

2.2 执行顺序

顺序问题很重要,比如某一个插件是添加'use strict', 一个插件是删除'use strict',如果想要删除成功,就要保证执行顺序。
在一个配置里面

  • 插件在 presets 前运行。
  • 插件顺序从前往后排列。
  • preset 顺序是颠倒的(从后往前)。

所以在preset中的插件,肯定比外层的插件要后执行。

2.3 传参数

pluginspreset的配置是数组的形式,如果不需要传参数,最基本的就是字符串名称,如果需要传参数,把它写成数组的形式,数组第一项是字符串名称,第二项是要传的参数对象。

3 Babel的升级

3.1 废弃的preset

@babel/preset-env已经完全可以替换

  • babel-preset-es2015
  • babel-preset-es2016
  • babel-preset-es2017
  • babel-preset-latest

所有stage的preset在Babel v7.0.0-beta.55版本都已经被废弃了,
stage-x:指处于某一阶段的js语言提案

  • Stage 0 - 设想(Strawman):只是一个想法,可能有 Babel插件。
  • Stage 1 - 建议(Proposal):这是值得跟进的。
  • Stage 2 - 草案(Draft):初始规范。
  • Stage 3 - 候选(Candidate):完成规范并在浏览器上初步实现。
  • Stage 4 - 完成(Finished):将添加到下一个年度版本发布中。

最开始stage的出现是为了方便开发人员,每个阶段的插件与TC39和社区相互作用,同步更新,用户可以直接引用对应stage支持的语法特性。关于废弃的原因 总结下来是:

  • 1 对用户太黑盒了,当提案发生重大变化和废弃时,stage内部的插件就会变化,用户可能会出现未编译的语法。
  • 2 当用户想要支持某种语法时,不知道在某一个stage里,所以最好是让用户自己去添加插件,或者你只需要指定浏览器的兼容性,preset中动态的添加对应插件。
  • 3 第三点举了个例子,很多人都把装饰器特性叫做ES7,其实这只是阶段0的实验性建议,可能永远不会成为JS的一部分。不要将其称为“ES7”,我们要时刻提醒开发者babel是怎么工作的。

3.1 废弃的polyfill

先说下已经有了Babel为什么还要polyfill,Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。举个栗子,ES6在Array对象上新增了Array.from方法。babel就不会转码这个方法。所以之前我们都需要引入polyfill。

但是从Babel 7.4.0开始,不推荐使用此软件包,而直接包括core-js/stable(包括regenerator-runtime/runtimepolyfill ECMAScript功能)和(需要使用转译的生成器函数)。

import "core-js/stable";
import "regenerator-runtime/runtime";

但是最优的方式也不是直接这样引入,后面讲@babel/preset-env的使用时会有更好的方式。

3.3 babel-upgrade

关于升级,官方提供了工具 babel-upgrade 总结关键点如下:

  • 1 node版本8以上 这个应该都不是问题了。
  • 2 npx babel-upgrade --write --install,两个参数,--write会把更新的配置写入babel的配置文件中,package.json中也会更新依赖,但是发现没有的依赖没有新增,所以我在更新的时候把配置中依赖的npm包,在package.json都check了一遍。--install是会进行一次安装操作。

4 @babel/preset-env

@babel/preset-env是Babel推荐的最智能的预设,在使用了 babel-upgrade 升级之后你就可以看到配置中会有这个预设,因为设个预设集成了常用插件和polyfill能力,可以根据用户指定的环境寻找对应的插件。

下面对它的关键配置项做说明。

4.1 targets

string | Array<string> | { [string]: string },默认为{}

描述您为项目支持/目标的环境。

这可以是与浏览器列表兼容的查询:

`{
  "targets": "> 0.25%, not dead"
}` 

或支持最低环境版本的对象:

`{
  "targets": {
    "chrome": "58",
    "ie": "11"
  }
}` 

实施例的环境中:chromeoperaedgefirefoxsafariieiosandroidnodeelectron

如果未指定目标,则旁注@babel/preset-env将默认转换所有ECMAScript 2015+代码,所以不建议。

4.2 useBuiltIns

"usage"| "entry"| false,默认为false

此选项决定@babel/preset-env如何处理polyfill的引入。

前面将废弃polyfill时 讲到了polyfill现在分为两个npm包,是这样引入

import "core-js/stable";
import "regenerator-runtime/runtime";

但是问题是全量引入,增加包体积,所以useBuiltIns选项就是对其进行优化。

当取值"entry"时,@babel/preset-env 会把全量引入替换为目标环境特定需要的模块。

当目标浏览器是 chrome 72 时,上面的内容将被 @babel/preset-env 转换为

require("core-js/modules/es.array.reduce");
require("core-js/modules/es.array.reduce-right");
require("core-js/modules/es.array.unscopables.flat");
require("core-js/modules/es.array.unscopables.flat-map");
require("core-js/modules/es.math.hypot");
require("core-js/modules/es.object.from-entries");
require("core-js/modules/web.immediate");

当取值"usage"时,我们无需手动引入polyfill文件,@babel/preset-env 在每个文件的开头引入目标环境不支持、仅在当前文件中使用的 polyfills。

例如,

const set = new Set([1, 2, 3]);
[1, 2, 3].includes(2);

当目标环境是老的浏览器例如 ie 11,将转换为

import "core-js/modules/es.array.includes";
import "core-js/modules/es.array.iterator";
import "core-js/modules/es.object.to-string";
import "core-js/modules/es.set";

const set = new Set([1, 2, 3]);
[1, 2, 3].includes(2);

当目标是 chrome 72 时不需要导入,因为这个环境不需要 polyfills:

const set = new Set([1, 2, 3]);
[1, 2, 3].includes(2);

4.3 core-js

core-js就是Javascript标准库的polyfill,@babel/preset-env的polyfill就依赖于它,所以我们需要指定使用的core-js的版本,目前最新版本是3。
默认情况下,仅注入稳定ECMAScript功能的polyfill,如果想使用一些提案的语法,可以有三种选择:

  • 使用useBuiltIns: "entry"时,可以直接导入建议填充工具import "core-js/proposals/string-replace-all"
  • 使用useBuiltIns: "usage"时,您有两种不同的选择:

    • shippedProposals选项设置为true。这将启用已经在浏览器中发布一段时间的投标的polyfill和transforms。
    • 使用corejs: { version: 3, proposals: true }。这样可以对所支持的每个提案进行填充core-js

4.4 exclude

我觉得这个选择有用,因为@babel/preset-env中内置的插件,我们无法在其后执行,比如里面内置的"@babel/plugin-transform-modules-commonjs"插件会默认的在所有的模块上都添加use strict 严格模式, 虽然有babel-plugin-remove-use-strict用于移除use strict 但是由于执行顺序的问题,还是无法移除。
第二个问题就是内置插件无法传参数的问题。
所以我想到的方法是先exclude排除掉这个插件,然后在外层再添加 这样就可以改变执行顺序同时也可以自定义传参数。

5 @babel/plugin-transform-runtime

已经有了polyfill,这个包的作用是什么?主要分两类:

  • 1 减少代码体积,Babel的编译会在每一个模块都添加一些行内的代码垫片,例如await_asyncToGeneratorasyncGeneratorStep,使用了它之后会把这些方法通过@babel/runtime/helpers中的模块进行替换。

例如代码

async function a () {
  await new Promise(function(resolve, reject) {
    resolve(1)
  })
} 

没使用之前,编译结果

require("regenerator-runtime/runtime");

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }


function _a() {
  _a = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return new Promise(function (resolve, reject) {
              resolve(1);
            });

          case 2:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _a.apply(this, arguments);
}

使用之后


var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

require("regenerator-runtime/runtime");

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));
  • 2 局部引入 不影响全局变量

@babel/preset-env中引入的polyfill都是直接引入的core-js下的模块,它的问题会污染全局变量,比如

"foobar".includes("foo");

编译后的polyfill是给String.prototype添加了includes方法,所以会影响全局的String对象。

require("core-js/modules/es.string.includes");

而使用了@babel/plugin-transform-runtime后的编译结果

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

(0, _includes.default)(_context = "foobar").call(_context, "foo");

会把代码中用的到的方法进行包装,而不会对全局变量产生影响。

最后是 @babel/plugin-transform-runtime的配置项,关键的是指定 core-js的版本。

corejs: 2仅支持全局变量(例如Promise)和静态属性(例如Array.from),corejs: 3还支持实例属性(例如[].includes)。

默认情况下,@babel/plugin-transform-runtime不填充提案。如果您使用corejs: 3,则可以通过使用proposals: true选项启用此功能。

需要安装对应的运行时依赖:
npm install --save @babel/runtime-corejs3

最后 你可以基于以上知识已经创建了符合自己团队开发的preset。

如果觉得有收获请关注微信公众号 前端良文 每周都会分享前端开发中的干货知识点。

查看原文

赞 15 收藏 10 评论 1

luckness 发布了文章 · 2020-11-07

ECMAScript7规范中的instanceof操作符

本文主要讲解ECMAScript7规范中的instanceof操作符。

预备知识

有名的Symbols

“有名”的Symbols指的是内置的符号,它们定义在Symbol对象上。ECMAScript7中使用了@@name的形式引用这些内置的符号,比如下面会提到的@@hasInstance,其实就是Symbol.hasInstance

InstanceofOperator(O, C)

O instanceof C在内部会调用InstanceofOperator(O, C)抽象操作,该抽象操作的步骤如下:

  1. 如果C的数据类型不是对象,抛出一个类型错误的异常;
  2. instOfHandler等于GetMethod(C, @@hasInstance),大概语义就是获取对象C@@hasInstance属性的值;
  3. 如果instOfHandler的值不是undefined,那么:

    1. 返回ToBoolean(? Call(instOfHandler, C, « O »))的结果,大概语义就是执行instOfHandler(O),然后把调用结果强制转化为布尔类型返回。
  4. 如果C不能被调用,抛出一个类型错误的异常;
  5. 返回OrdinaryHasInstance(C, O)的结果。

OrdinaryHasInstance(C, O)

OrdinaryHasInstance(C, O)抽象操作的步骤如下:

  1. 如果C不能被调用,返回false
  2. 如果C有内部插槽[[BoundTargetFunction]],那么:

    1. BC等于C的内部插槽[[BoundTargetFunction]]的值;
    2. 返回InstanceofOperator(O, BC)的结果;
  3. 如果O的类型不是对象,返回false
  4. P等于Get(C, "prototype"),大概语义是获取C.prototype的值;
  5. 如果P的数据类型不是对象,抛出一个类型错误的异常;
  6. 重复执行下述步骤:

    1. O等于O.[[GetPrototypeOf]]()的结果,大概语义就是获取O的原型对象;
    2. 如果O等于null,返回false
    3. 如果SameValue(P, O)的结果是true,返回true

SameValue抽象操作参见JavaScript中的==,===和Object.js()中的Object.is()Object.is()使用的就是这个抽象操作的结果。

由上述步骤2可知,如果C是一个bind函数,那么会重新在C绑定的目标函数上执行InstanceofOperator(O, BC)操作。

由上述步骤6可知,会重复地获取对象O的原型对象,然后比较该原型对象和Cprototype属性是否相等,直到相等返回true,或者O变为null,也就是遍历完整个原型链,返回false

Function.prototype[@@hasInstance] (V)

由上面的InstanceofOperator(O, C)抽象操作的步骤23可以知道,如果C上面定义或继承了@@ hasInstance属性的话,会调用该属性的值,而不会走到步骤45。步骤45的目的是为了兼容没有实现@@hasInstance方法的浏览器。如果一个函数没有定义或继承@@hasInstance属性,那么就会使用默认的instanceof的语义,也就是OrdinaryHasInstance(C, O)抽象操作描述的步骤。

ECMAScript7规范中,在Functionprototype属性上定义了@@hasInstance属性。Function.prototype[@@hasInstance](V)的步骤如下:

  1. F等于this值;
  2. 返回OrdinaryHasInstance(F, V)的结果。

所以,你可以看到在默认情况下,instanceof的语义是一样的,都是返回OrdinaryHasInstance(F, V)的结果。为什么说默认情况下?因为你可以覆盖Function.prototype[@@hasInstance]方法,去自定义instanceof的行为。

例子

function A () {}
function B () {}

var a = new A
a.__proto__ === A.prototype // true
a.__proto__.__proto__ === Object.prototype // true
a.__proto__.__proto__.__proto__ === null // true

a instanceof A // true
a instanceof B // false

OrdinaryHasInstance(C, O)的第6步可知:

  • 对于a instanceof APA.prototype,在第一次循环的时候,a的原型对象a._proto__A.prototype,也就是步骤中的OA.prototype,所以返回了true
  • 对于a instanceof BPB.prototype,在第一次循环的时候,a的原型对象a._proto__A.prototype,不等于P;执行第二次循环,此时Oa.__proto__.__proto__,也就是Object.prototype,不等于P;执行第三次循环,此时Oa.__proto__.__proto__.__proto__,也就是null,也就是原型链都遍历完了,所以返回了false

接着上面的例子:

A.prototype.__proto__ = B.prototype

a.__proto__ === A.prototype // true
a.__proto__.__proto__ === B.prototype // true
a.__proto__.__proto__.__proto__ === Object.prototype // true
a.__proto__.__proto__.__proto__.__proto__ === null // true

a instanceof B // true

在上面的例子中,我们把B.prototype设置成了a的原型链中的一环,这样a instanceof BOrdinaryHasInstance(C, O)的第6步的第2次循环的时候,返回了true

OrdinaryHasInstance(C, O)的第2步,我们知道bind函数的行为和普通函数的行为是不一样的:

function A () {}
var B = A.bind()

B.prototype === undefined // true

var b = new B
b instanceof B // true
b instanceof A // true

由上面的例子可知,B.prototypeundefined。所以,instanceof作用于bind函数的返回结果其实是作用于绑定的目标函数的返回值,和bind函数基本上没有什么关系。

InstanceofOperator(O, C)步骤2和步骤3可知,我们可以通过@@hasInstance属性来自定义instanceof的行为:

function A () {}
var a = new A
a instanceof A // true

A[Symbol.hasInstance] = function () { return false }
a instanceof A // ?

chrome浏览器测试了一下,发现还是输出true。然后看了一下ECMAScript6的文档,ECMAScript6文档里面还没有规定可以通过@@hasInstance改变instanceof的行为,所以应该是目前chrome浏览器还没有实现ECMAScript7中的instanceof操作符的行为。

直到有一天看了MDNSymbol.hasInstance的兼容性部分,发现chrome51版本就开始支持Symbol.hasInstance了:

class MyArray {  
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance)
  }
}
console.log([] instanceof MyArray) // true

那么为什么我那样写不行呢?直到我发现:

function A () {}
var fun = function () {return false}
A[Symbol.hasInstance] = fun
A[Symbol.hasInstance] === fun // false
A[Symbol.hasInstance] === Function.prototype[Symbol.hasInstance] // true
A[Symbol.hasInstance] === A.__proto__[Symbol.hasInstance] // true

由上面的代码可知,A[Symbol.hasInstance]并没有赋值成功,而且始终等于Function.prototype[Symbol.hasInstance],也就是始终等于A的原型上的Symbol.hasInstance方法。那是不是因为原型上的同名方法?

Object.getOwnPropertyDescriptor(Function.prototype, Symbol.hasInstance)
// Object {writable: false, enumerable: false, configurable: false, value: function}

由上面的代码可知,Function.prototype上的Symbol.hasInstance的属性描述符的writablefalse,也就是这个属性是只读的,所以在A上面添加Symbol.hasInstance属性失败了。但是为啥没有失败的提示呢?

'use strict'
function A () {}
var fun = function () {return false}
A[Symbol.hasInstance] = fun
// Uncaught TypeError: Cannot assign to read only property 'Symbol(Symbol.hasInstance)' of function 'function A() {}'

错误提示出来了,所以以后还是尽量使用严格模式。非严格模式下有些操作会静默失败,也就是即使操作失败了也不会有任何提示,导致开发人员认为操作成功了。

var a = {}
a[Symbol.hasInstance] = function () {return true}
new Number(3) instanceof a // true

因为可以通过自定义Symbol.hasInstance方法来覆盖默认行为,所以用instanceof操作符判断数据类型并不一定是可靠的。

还有一个问题:为什么上面MDN文档的例子可以成功,我最初的例子就不行呢,目的不都是写一个构造函数,然后在构造函数上添加一个属性吗?
个人分析的结果是:虽然大家都说Class是写构造函数的一个语法糖,但是其实还是和使用function的方式有差别的,就比如上面的例子。使用Class的时候,会直接在构造函数上添加一个静态属性,不会先检查原型链上是否存在同名属性。而使用function的方式的时候,给构造函数添加一个静态方法,相当于给对象赋值,赋值操作会先检查原型链上是否存在同名属性,所以就会有赋值失败的风险。所以,就给构造函数添加Symbol.hasInstance属性来说,Class能做到,使用Function的方式就做不到。

更新于2018/11/20
上面总结到

所以,就给构造函数添加Symbol.hasInstance属性来说,Class能做到,使用Function的方式就做不到。

但是,后来发现给对象添加属性的方法不只是赋值这一种方式,还有一个Object.defineProperty方法:

function A () {}
var a = new A
a instanceof A // true

Object.defineProperty(A, Symbol.hasInstance, {
    value: function () { return false }
})
a instanceof A // false

总结

本文主要讲解ECMAScript7规范中的instanceof操作符,希望大家能有所收获。如果本文有什么错误或者不严谨的地方,欢迎在评论区留言。

查看原文

赞 1 收藏 1 评论 0

luckness 发布了文章 · 2020-10-31

JavaScript实现网页截屏方法总结

最近研究了下如何利用JavaScript实现网页截屏,包括在浏览器运行的JS,以及在后台运行的nodeJs的方法。主要看了以下几个:

  1. PhantomJS
  2. Puppeteer(chrome headless)
  3. SlimerJS
  4. dom-to-image
  5. html2canvas

测试的网页使用了WebGL技术,所以下面的总结会和WebGL相关。

名词定义

headless browser

无界面浏览器,多用于网页自动化测试、网页截屏、网页的网络监控等。

PhantomJS

PhantomJS是可以通过JS进行编程的headless浏览器,使用的是QtWebKit内核。

实现截屏的代码,假设文件名为github.js:

// 创建一个网页实例
var page = require('webpage').create();
// 加载页面
page.open('http://github.com/', function () {
  // 给网页截屏,保存到github.png文件中
  page.render('github.png');
  phantom.exit();
})

运行:

phantomjs github.js

普通的页面没有问题,但是如果运行包含WebGL的页面,发现截屏不对。经过一些调查,发现不支持WebGL,github issue

总结:

  1. PhantomJs已经停止维护了,所以不太建议继续使用。停止维护的一个原因是chrome发布的headless版本对它造成了一定冲击。
  2. 不支持WebGL。但是,还是有开发者说可以自己给PhantomJS添加WebGL支持,不过,这个方案目前超出我的知识范围了,就没有继续研究。

Puppeteer(chrome headless)

Puppeteer是一个Node库,提供了控制chrome和chromium的API。默认运行headless模式,也支持界面运行。

实现截屏的代码example.js:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setViewport({ // 设置视窗大小
    width: 600,
    height: 800
  });
  await page.goto('https://example.com'); // 打开页面
  await page.screenshot({path: 'example.png'}); // path: 截屏文件保存路径

  await browser.close();
})();

运行:

node example.js

接下来看下screenshot方法的实现原理:

screenshot的源码位于lib/cjs/puppeteer/common/Page.js文件中,是一个异步方法:

async screenshot(options = {}) {
  // ...
  return this._screenshotTaskQueue.postTask(() => this._screenshotTask(screenshotType, options));
}
async _screenshotTask(format, options) {
  // ...
  const result = await this._client.send('Page.captureScreenshot', {
    format,
    quality: options.quality,
    clip,
  });
  // ...
}

这个this._client.send又是个什么东西?别急,我们重新看下Puppeteer的定义:

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol.

看到最后面那个DevTools Protocol了吗?这是个什么东西:

The Chrome DevTools Protocol allows for tools to instrument, inspect, debug and profile Chromium, Chrome and other Blink-based browsers.

详细的解释可以看这篇博客

简单来说,Puppeteer就是通过WebSocket给浏览器发送遵循Chrome Devtools Protocol的数据,命令浏览器去执行一些操作。然后,浏览器再通过WebSocket把结果返回给Puppeteer。这个过程是异步的,所以看源代码会发现好多async/await。

所以screenshot方法是调用了Chrome Devtools Protocol的captureScreenshot

总结:

  1. 支持WebGL。
  2. 网页比较复杂的话,截屏时间也挺长的,我测试的页面是几百毫秒。
  3. Puppeteer是对(CDP)Chrome Devtools Protocol功能的封装。大部分功能都是通过WebSocket传输给CDP处理的。

SlimerJS

SlimerJS和PhantomJS类似。不同点是SlimerJS是基于火狐的浏览器引擎Gecko,而不是Webkit。

SlimerJS可以通过npm安装,最新版本是1.x。不过兼容的火狐版本是53.0到59.0。我看现在火狐最新版本都82了。因为我本机是安装了火狐最新版本的,所以我还得安装一个老版本的火狐,比如59.0。可以参考这篇安装旧版本的火狐浏览器。我是mac系统,感觉安装还是挺容易的。

实现截屏的代码screenshot.js:

var page = require('webpage').create();
page.open("http://slimerjs.org", function (status) {
  page.viewportSize = { width:1024, height:768 };
  page.render('screenshot.png');
});

运行

// mac操作系统设置火狐路径
export SLIMERJSLAUNCHER=/Applications/Firefox.app/Contents/MacOS/firefox
./node_modules/.bin/slimerjs screenshot.js // 我是局部安装的slimer包

需要注意的是SLIMERJSLAUNCHER=/Applications/Firefox.app/Contents/MacOS/firefox启动的是火狐默认的安装路径,因为我一开始就有火狐浏览器,所以启动的是最新版本的浏览器,然后就报错了,说不兼容。在前面我安装过一个59版本的火狐,那么这个火狐浏览器的路径是什么?

在应用程序里面我把这个旧版本的火狐命名为Firefox59,然后这个路径就是/Applications/Firefox59.app/Contents/MacOS/firefox。重新设置SLIMERJSLAUNCHER为59版本的火狐浏览器之后,发现就能成功了。

不过,Puppeteer默认会打开浏览器界面,也就是non-headless模式。如果要使用headless模式,可以

./node_modules/.bin/slimerjs --headless screenshot.js

不过,headless模式下,不支持WebGL。

我在写例子的时候,发现的一个明显的不同就是Puppeteer截屏是异步函数,而SlimerJS截屏是同步函数?好奇心驱使下,看了下源码(src/modules/slimer-sdk/webpage.js):

render: function(filename, options) {
  // ...
  let canvas = webpageUtils.getScreenshotCanvas(
    browser.contentWindow,
    finalOptions.ratio,
    finalOptions.onlyViewport, this);
  }
  canvas.toBlob(function(blob) {
    let reader = new browser.contentWindow.FileReader();
    reader.onloadend = function() {
      content = reader.result;
    }
    reader.readAsBinaryString(blob);
  }, finalOptions.contentType, finalOptions.quality);
  // ...
}

webpageUtils.getScreenshotCanvas(src/modules/webpageUtils.jsm):

getScreenshotCanvas : function(window, ratio, onlyViewport, webpage) {
  // ...
  // create the canvas
  let canvas = window.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
  canvas.width = canvasWidth;
  canvas.height = canvasHeight;

  let ctx = canvas.getContext("2d");
  ctx.scale(ratio, ratio);
  ctx.drawWindow(window, clip.left, clip.top, clip.width, clip.height, "rgba(0,0,0,0)");
  ctx.restore();

  return canvas;
}

关键代码就是那行ctx.drawWindow。what?JS原生API还支持直接截屏?
CanvasRenderingContext2D.drawWindow():只有火狐支持,已经被废弃掉的非规范定义的标准API。

总结

  1. 1.0版本支持的火狐版本是53.0到59.0。不保证最新版本火狐可用。
  2. headless模式下,不支持WebGL。

dom-to-image

dom-to-image:前端截屏的开源库。工作原理是:
SVG的foreignObject标签可以包裹任意的html内容。那么,为了渲染一个节点,主要进行了以下步骤:

  1. 递归地拷贝原始dom节点和后代节点;
  2. 把原始节点以及后代节点的样式递归的应用到对应的拷贝后的节点和后代节点上;
  3. 字体处理;
  4. 图片处理;
  5. 序列化拷贝后的节点,把它插入到foreignObject里面,然后组成一个svg,然后生成一个data URL;
  6. 如果想得到PNG内容或原始像素值,可以先使用data URL创建一个图片,使用一个离屏canvas渲染这张图片,然后从canvas中获取想要的数据。

测试的时候,发现外部资源不能加载,所以简单的了解了后就放弃了。

html2canvas

html2canvas。网上查了下感觉有一篇文章写的挺好的:浅析 js 实现网页截图的两种方式。感兴趣的可以看下。

未验证的猜想

虽然后面这两种是前端的实现方式,但是结合前面讲的headless库,也是可以实现后端截屏的。以Puppeteer的API为例,可以首先使用page.addScriptTag(options)往网页中添加前端截屏的库,然后在page.evaluate(pageFunction[, ...args])中的pageFunction函数里面写相应的截屏代码就可以了,因为pageFunction的执行上下文是网页上下文,所以可以获取到document等对象。

总结

对截屏的一个简单研究,写篇博客整理下思路。如果文中有叙述错误的地方,欢迎评论留言。

查看原文

赞 7 收藏 4 评论 9

luckness 回答了问题 · 2020-10-21

解决利用reduce和Promise发送顺序队列请求。

var datas = [
  [1, 2, 3, 4],
  [5, 6, 7, 8],
  [9, 10, 11, 12],
  [13]
]

// 模拟ajax请求
function upload (json) {
  return new Promise(resolve => {
    console.log('sendData: ', json)
    setTimeout(() => {
      resolve(json)
      console.log('receiveData: ', json)
    }, 2000)
  })
}

datas.reduce(async (prev, cur, i) => {
  let data = JSON.stringify(datas[i])
  await prev
  return upload(data) // this.$api.post封装成一个promise,前面一定要加return
}, Promise.resolve())

// sendData:  [1,2,3,4]
// 2s后
// receiveData:  [1,2,3,4]
// sendData:  [5,6,7,8]
// 4s后
// receiveData:  [5,6,7,8]
// sendData:  [9,10,11,12]
// 6s后
// receiveData:  [9,10,11,12]
// sendData:  [13]
// 8s后
// receiveData:  [13]

问题:

  1. this.$api.post封装成一个promise,前面一定要加return,不加return所有的请求一起都发出去了,就没有你说的前一个发送成功之后再发送后一个;
  2. result遍历了一遍,result[i]也是一个数组,你又reduce了一遍,你请求发送的是数组中每个元素的每个元素,一共8*4+1=33个请求。我觉得直接在result上应用reduce就行了,为什么要在每个子元素上应用reduce。

关注 2 回答 3

luckness 回答了问题 · 2020-10-20

原生js创建两层div

var outerDiv =document.createElement('div')
var innerDiv =document.createElement('div')
outerDiv.classList.add('a')
innerDiv.classList.add('b')
outerDiv.appendChild(innerDiv)

如果是特殊属性的话,可以使用特殊属性的方法,比如设置class可以使用classList或者className:

var outerDiv =document.createElement('div')
var innerDiv =document.createElement('div')
outerDiv.className = 'a'
innerDiv.className = 'b'
outerDiv.appendChild(innerDiv)

一般属性可以使用setAttribute

关注 3 回答 3

认证与成就

  • 获得 418 次点赞
  • 获得 8 枚徽章 获得 1 枚金徽章, 获得 1 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-05-19
个人主页被 4.7k 人浏览