anRui

anRui 查看完整档案

北京编辑  |  填写毕业院校怀教网络  |  前端开发工程师 编辑 scrscript.com 编辑
编辑

这个bug 我不想要

个人动态

anRui 赞了文章 · 2020-11-18

而立之年——回顾我的前端转行之路

为什么转行

因为混得不好。

在成为程序员之前,我干过很多工作。由于学历的问题(高中),我的工作基本上都是体力活。包括但不限于:工厂普工、销售(没有干销售的才能)、搬运工、摆地摊等,转行前最后一份工作是修电脑。这么多年,月薪没高过 3300...

后来偶然一个机会我发现了知乎这个网站,在上面了解到程序员的各种优点。于是,我下定决心转行(2016 年,当时 28 了),辞职在家自学编程。并且也得到了媳妇的支持,感谢我的媳妇。

转行准备

转行选择前端也是在知乎上看网友分析的,比后端好入门。

如何选择教程?

最好在网上多查查资料,找评价高的或者去豆瓣上找评分高的书。

我在网上查了很多资料,最终确定 HTML、CSS 在 w3cschool 学习。JavaScript 则选择了JavaScript 高级程序设计第三版(俗称红宝书,现在已经有第四版了)。

光看不练是学不好编程的,我非常幸运的遇到了百度前端技术学院。它从易到难设置了 52 个任务,共分为四个阶段。任务难度循序渐进,每一个任务都有清晰的讲解和学习参考资料。它还怕你不会做,允许你查看其他人上传的任务答案。

我先学习了 HTML、CSS,做完了第一阶段任务。再看完红宝书前十三章,做完了第二阶段任务。然后把红宝石剩下的全看完,做到第三阶段的任务四十五。后面的任务对于当时的我来说实在太难了,就没往下做。在 1 月的时候,又学习了 ajax,了解了前后端如何相互通信。

我从 16 年 11 月开始自学前端,一直到 17 年 2 月。历时 3 个月,平均每天学习 3-4 个小时。中间有好几次因为太难想过放弃,不过最后还是坚持下来了。

找工作的过程非常艰难,我在网上各大招聘平台投了很多简历,但由于没学历、没经验,所以一个回复都没有。最后还是我媳妇工作的公司在招前端,给了我一个内推的机会,才有了第一次面试。并且第一次面试也很顺利,居然过了,这是我没想到的。直到多年后我和面试官又在一个公司的时候,才知道原因。他的意思是:看在我这么努力自学编程的份上,愿意给我一个机会。

虽然人生很艰难,但很有可能,遇到一个愿意给你机会的人,就能改变你的命运。

正式工作

第一年

在正式的项目中写代码和在学习时写代码是不一样的。你必须得考虑这样写安不安全,会不会引起 BUG,会不会引起性能问题。在工作的第一年,写业务代码对我的提升非常大。

第一年的主要任务,就是提升前端基础能力。因此我看了很多 JavaScript 的书籍来提升自己的水平:

  1. JavaScript高级程序设计(第三版)
  2. 高性能JavaScript
  3. JavaScript语言精粹
  4. 你不知道的JavaScript(上中下三卷)
  5. ES6标准入门
  6. 深入浅出Node.js

这些书都是非常经典的书籍,有几本我还看了好几篇。

除了看书外,我还做了百度前端技术学院 2017 年的任务,它比 2016 年的任务(转行时做的是 2016 年的任务)更有难度和深度,非常适合进阶。

另外还学习了 jquery 和 nodejs。jquery 是工作中要用,nodejs 则是出于兴趣学习的,没有多深入。

第二年

到了第二年,写业务代码对于我来说,已经提升不大了,就像一个熟练工一样。而且感觉前端方面掌握的知识已经足够把工作做好了。于是我就想,为了成为一名顶尖的程序员,还需要做什么。我在网上查了很多资料,看了很多前辈的回答,最后决定自学计算机专业。

我制定了一个自学计算机专业的计划,并且减少花在前端上的时间。因为说到底,基础是地基。基础打好了,楼才能建得高。

计算机系统要素

计算机系统要素是我制订计划后开始学习的第一本书。它主要讲解了计算机原理(1-5章)、编译原理(6-11章)、操作系统相关知识(12章)。不要看内容这么多,其实这本书的内容非常通俗易懂,翻译也很给力。每一章后面都有相关的实验,需要你手写代码去完成,堪称理论与实践结合的经典。

这里引用一下书里的简介,大家可以感受一下:

本书通过展现简单但功能强大的计算机系统之构建过程,为读者呈现了一幅完整、严格的计算机应用科学大图景。本书作者认为,理解计算机工作原理的最好方法就是亲自动手,从零开始构建计算机系统。

通过12个章节和项目来引领读者从头开始,本书逐步地构建一个基本的硬件平台和现代软件阶层体系。在这个过程中,读者能够获得关于硬件体系结构、操作系统、编程语言、编译器、数据结构、算法以及软件工程的详实知识。通过这种逐步构造的方法,本书揭示了计算机科学知识中的重要成分,并展示其它课程中所介绍的理论和应用技术如何融入这幅全局大图景当中去。

全书基于“先抽象再实现”的阐述模式,每一章都介绍一个关键的硬件或软件抽象,一种实现方式以及一个实际的项目。完成这些项目所必要的计算机科学知识在本书中都有涵盖,只要求读者具备程序设计经验。本书配套的支持网站提供了书中描述的用于构建所有硬件和软件系统所必需的工具和资料,以及用于12个项目的200个测试程序。

全书内容广泛、涉猎全面,适合计算机及相关专业本科生、研究生、技术开发人员、教师以及技术爱好者参考和学习。

做完这些实验,让我有了一个质的提升。以前感觉计算机就是一个黑盒,但现在不一样了。我开始了解计算机内部是如何运作的。明白了自己写的代码是怎么经过编译变成指令,最后在 CPU 中执行的。也明白了指令、数据怎么在 CPU 和内存之间流转的。

这本书所有实验的答案我都放在了 github 上,有兴趣不妨了解一下。

Vue

这一年还学会了 Vue。除了熟读文档外,还为了研究源码而模仿 Vue1.0 版本写了一个 mini-vue。不过学习源码对于我写业务代码并没有什么帮助。如果不是出于兴趣去研究源码,最好不要去学,熟读文档就能完全应付工作了。如果是为了面试,那也不需要阅读源码。只需要在网上找一些质量高的源码分析文章看一看,作一下笔记就 OK 了。

为什么我不建议阅读源码?因为阅读源码效率太低,而且相对于你的付出,收益并不大。到后面 Vue 出了 3.0 版本时,我也是有选择地阅读部分源码。

第三年

第三年有大半年的时间浪费在王者荣耀上,那会天天只想着冲荣耀,根本没心思学习。后来终于醒悟过来了,王者荣耀是我成为顶级程序员的阻碍。于是痛定思痛,给戒掉了。

由于打王者的原因,第三年没学习多少新知识。基本上只做了三件事:

  1. 写了几个 Vue 相关的插件和项目。
  2. 将过去所学的前端知识,整理了一下放在 github 上,有空就复习一下。
  3. 学习数据结构与算法。

数据结构与算法

数据结构和算法有什么用?学了算法后,我觉得至少会懂得去分析程序的性能问题。

一个程序的性能有问题,需要你去优化。如果学过数据结构和算法,你会从时间复杂度和空间复杂度去分析代码,然后解决问题。如果没学过,你只能靠猜、碰运气来解决问题。

理论知识上,我主要看的是算法这本书,课后习题没做,改成用刷 leetcode 代替。目前已经刷了 300+ 道题,还在继续刷。不过由于数学差,稍微复杂一点的算法知识都看不懂,效果不是很好。

第四年

第四年,也就是今年(2020),是我重新奋斗的一年。今年比以往的任何一年都要努力,每天保证 3 小时以上的学习时间。如果实在太忙了,达不到要求,那就改天把时间补上。附上我今年的学习时长图(记录软件为 Now Then):

今年我做了非常多的事情:

  1. 研究前端工程化。
  2. 学习操作系统。
  3. 学习计算机网络。
  4. 学习软件工程。
  5. 学习 C++。
  6. 学英语。

前端工程化

研究前端工程化的目的,就是为了提高团队的开发效率。为此我看了很多书和资料:

...

研究了一年的时间,写了一篇质量较高的入门教程——手把手带你入门前端工程化——超详细教程。除此之外,还有其他工程化相关的一系列文章:

操作系统

操作系统是管理计算机硬件与软件资源的计算机程序。通常情况下,程序是运行在操作系统上的,而不是直接和硬件交互。一个程序如果想和硬件交互就得通过操作系统。

如果你掌握了操作系统的知识,你就知道程序是怎么和硬件交互的。

例如你知道申请内存,释放内存的内部过程是怎样的;当你按下 k 键,你也知道 k 是怎么出现在屏幕上的;知道文件是怎么读出、写入的。

对于操作系统,我主要学习了以下书籍:

  1. x86汇编语言:从实模式到保护模式
  2. xv6-chinese
  3. 操作系统导论

然后做 MIT6.828 的实验,实现了一个简单的操作系统内核。

计算机网络

计算机网络的作用主要是解决计算机之间如何通信的问题。

例如 A 地区和 B 地区的的计算机怎么通信?同一局域网的两台电脑又如何通信?学习计算机网络知识就是了解它们是怎么通信的以及怎么将它们联通起来。

对于计算机网络,我主要学习了以下书籍:

  1. 计算机网络--自顶向下
  2. 计算机网络
  3. HTTP权威指南
  4. HTTP/2基础教程

并且做了计算机网络--自顶向下的实验。

软件工程

软件工程是一门研究用工程化方法构建和维护有效的、实用的和高质量的软件的学科。它涉及程序设计语言、数据库、软件开发工具、系统平台、标准、设计模式等方面。

学习以下书籍:

  1. 代码大全(第2版)
  2. 重构(第2版)
  3. 软件工程

软件工程是一门非常庞大的学科,我只学习了一点皮毛。主要学习的是关于代码怎么写得更好、结构组织更合理的知识,这需要一边学习一边在工作中运用。

C++

学习 C++ 其实是为了研究 nodejs 源码用的,看的这本书C++ Primer 中文版(第 5 版)

英语

我从转行开始就一直在学习英语,不过今年花的时间比较多。

英语对于程序员的好处非常非常多,就我知道的有:

  1. 可以用 google 和 stackoverflow 来解决问题。
  2. 知道怎么给变量、函数起一个好的命名。
  3. 很多流行的软件都是国外程序员写的,有问题你可以直接看文档以及和别人交流。

在我转行前英语词汇量只有几百,三年多过去了,现在词汇量有 6000(都是用百词斩测的)。

写作

写作的好处是非常多的,越早写越好。我还记得第一篇文章是 2017 年 2 月发表的,是我工作后的第 13 天,发表在 CSDN 上。

个人认为写作的好处有三点:

  1. 锻炼你的写作能力。一般情况下,写得越多,写作能力越好。这个好,不是说你的文章遣词造句有多好,而是指文章条理清晰,通俗易懂,容易让人理解。
  2. 写作其实是费曼学习法的运用,帮助自己加深理解所学的知识。有没有试过,学完一个知识点后,觉得自己懂了。但让你向别人讲述这个知识点时,反而吞吞吐吐不知道怎么讲。其实这是没理解透才会这样的,要让别人明白你在表达什么,首先你得非常熟悉这个知识点。一知半解是不可能把它讲明白的,所以写作也是在帮你梳理知识。
  3. 增加自己的曝光度。在我三年多的程序员生涯中,一共写了 50 多篇文章,因此在一些平台上也收获了不少赞和粉丝。因为我写的某些文章质量还行,不少大厂的程序员找过我,给我内推。不过由于个人学历问题,基本上都没下文...

总之一句话,写作对你只有好处,没有坏处。

学习

有选择的学习

我觉得学习一定要有非常清晰的目标,知道你要学什么,怎么学。对于前端来说,我认为很多框架和库都是不用学的。例如前端三大框架,没有必要三个都学,把你工作中要用的那个掌握好就行。

比如你公司用的是 Vue,就深入学习 Vue,如果要看源码就只看重点部分的源码。例如模板编译、Diff 算法、Vue 原生组件实现、指令实现等等。

剩下的两个框架 React、Angular 做个 DEMO 熟悉一下就行,毕竟原理都是相通的。等你公司要上这两个再深入学习,不过也不建议阅读源码了,太累。看别人写的现成的源码分析文章就好。

其他的,像 easyui、Backbone.js、各种小程序... 用不到的坚决不学,浪费时间。用的时候看文档就行了,当然,如果有兴趣了解如何实现也是可以的。

学习方法

我觉得好的学习方法非常重要,对我比较有用的两个是:

  1. 费曼学习法。
  2. 学习一个知识点,最好把它吃透。

费曼学习法在《写作》一节中已经说过了,这里着重说说第二个。

你有没有过这种感觉:觉得自己会的东西很多,但其实掌握的知识很多都停留在表面上,别人要是往深一问,就懵逼了。

我以前就有过这种感觉,主要问题出在对知识的学习仅停留在浅尝即止的状态。就是学习新知识,能写个 DEMO,就觉得自己学得差不多了。这种学习方法是很有害的,首先知识存留度不高,其次是浪费时间,因为很快就会忘掉。

后来我尝试改正这种状态,在学习新的知识点时,时常问自己三个问题:

  1. 这是什么?
  2. 为什么要这样?可以不这样吗?
  3. 有没有更好的方式?

当然,不是所有问题都能适用灵魂三问,但它适用大多数情况。

举个例子:看过性能优化相关文章的同学应该知道有这么一条规则,要减少页面上的 HTTP 请求。

这是什么?

先了解一下 HTTP 请求是啥,查资料发现原来是向服务器请求资源用的。

为什么要减少 HTTP 请求?

查资料发现:HTTP 请求需要经历 DNS 查找,TCP 握手,SSL 握手(如果有的话)等一系列过程,才能真正发出这个请求。并且现代浏览器对于 TCP 并发数也是有限制的,超过 TCP 并发数的 HTTP 请求只能等前面的请求完成了才能继续发送。

我们可以打开 chrome 开发者工具看一下一个 HTTP 请求所花费的具体时间。

在这里插入图片描述

这是一个 HTTP 请求,请求的文件大小为 28.4KB。

名词解释:

  1. Queueing: 在请求队列中的时间。
  2. Stalled: 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。
  3. Proxy negotiation: 与代理服务器连接进行协商所花费的时间。
  4. DNS Lookup: 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。
  5. Initial Connection / Connecting: 建立连接所花费的时间,包括TCP握手/重试和协商SSL。
  6. SSL: 完成SSL握手所花费的时间。
  7. Request sent: 发出网络请求所花费的时间,通常为一毫秒的时间。
  8. Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间总和,它包含了 DNS 解析时间、 TCP 连接时间、发送 HTTP 请求时间和获得响应消息第一个字节的时间。
  9. Content Download: 接收响应数据所花费的时间。

从这个例子可以看出,真正下载数据的时间占比为 13.05 / 204.16 = 6.39%。文件越小,这个比例越小,文件越大,比例就越高。这就是为什么要建议将多个小文件合并为一个大文件,从而减少 HTTP 请求次数的原因。

有没有更好的方式?

使用 HTTP2,所有的请求都可以放在一个 TCP 连接上发送。HTTP2 还有好多东西要学,这里不深入讲解了。

经过灵魂三问后,是不是这条优化规则的来龙去脉全都理清了,并且在你查资料动手的过程中,知识会理解得更加深刻。

掌握了这种学习方法,并且时刻运用在学习中、工作中,突破瓶颈只是时间的问题

总结

下面提前回答一下可能会有的问题。

百度前端技术学院

百度前端技术学院 2017 年及往后的任务,如果没有报名,那就只能做部分任务。2016 年的任务则由于百度服务器的问题,很多题的示例图都裂了。这个其实是有解决方案的,那就是看别人的答案。把别人的源码下载下来,用浏览器打开 html 文件当示例图看。这两年的任务我都做了大部分,附上答案:

  1. 百度前端技术学院2016任务
  2. 百度前端技术学院2017任务

学历提升

我从 18 年开始,已经报考了成人高考大专,19 年报了自考本科。大专明年 1 月就能毕业,自考本科比较难,可能 2021 年或 2022 年才能考下来。

写在最后

从转行到现在,已经过去 3 年多了。不得不说转行当程序员给了我人生第二次机会,我也很喜欢这个职业。不过这几年一直都是在小公司,导致自己的技术和视野得不到很大的提升。所以现在的目标除了学习计算机专业外,就是进大厂,希望有一天能实现。

虽然今年已经 32 了,但我对未来仍然充满希望。努力地学习,努力地提升自己,为了成为一名顶尖的程序员而努力。

查看原文

赞 56 收藏 24 评论 18

anRui 提出了问题 · 2020-09-14

expo 打包成 APK / IPA 包文件太大

现在使用的 expo-cli (因为要兼容 三端 android ios web 采用的 expo 开发)但是expo build:android 或者 expo build:ios 包就会特别大

根据一些资料 已经把react-native设置成外链得形式了 但是包的本身还是没有减重

我应该采用什么方式进行减重

关注 1 回答 0

anRui 赞了文章 · 2020-05-12

深度解析`create-react-app`源码

2018-06-13 更新。昨天突然好奇在Google上搜了一波关于create-react-app 源码的关键词,发现掘金出现好几篇仿文,就连我开头前沿瞎几把啰嗦的话都抄,我还能说什么是吧?以后博客还是首发在Github上,地址戳这里戳这里!!转载求你们注明出处、改编求你们贴一下参考链接...

2018-01-26 更新。这两天我边读边思考我是不是真的懂了,我发现我有个重大的失误,我弄错了学习的顺序,学习一个新的东西,我们应该是先学会熟练的使用它,然后在去探究它的原理,我居然把第一步忽略了,这明显是错误的,所以我今天在开头新补充一节使用说明,同时对后面做一些修改和补充。

之前写了几篇关于搭建react环境的文,一直还没有完善它,这次撸完这波源码在重新完善之前的从零搭建完美的react开发打包测试环境,如果你对如何从零搭建一个react项目有兴趣,或者是还没有经验的小白,可以期待一下,作为我看完源码的成果作品。

如果后续有更正或者更新的地方,会在顶部加以说明。

前言

这段时间公司的事情变得比较少,空下了很多时间,作为一个刚刚毕业初入职场的菜鸟级程序员,一点都不敢放松,秉持着我为人人的思想也想为开源社区做点小小的贡献,但是一直又没有什么明确的目标,最近在努力的准备吃透react,加上react的脚手架工具create-react-app已经很成熟了,初始化一个react项目根本看不到它到底是怎么给我搭建的这个开发环境,又是怎么做到的,我还是想知道知道,所以就把他拖出来溜溜。

文中若有错误或者需要指正的地方,多多指教,共同进步。

使用说明

就像我开头说的那样,学习一个新的东西,应该是先知道如何用,然后在来看他是怎么实现的。create-react-app到底是个什么东西,总结一句话来说,就是官方提供的快速搭建一个新的react项目的脚手架工具,类似于vuevue-cliangularangular-cli,至于为什么不叫react-cli是一个值得深思的问题...哈哈哈,有趣!

不说废话了,贴个图,直接看create-react-app的命令帮助。

create-react-app

概略说明

毕竟它已经是一个很成熟的工具了,说明也很完善,重点对其中--scripts-version说一下,其他比较简单,大概说一下,注意有一行Only <project-directory> is required,直译一下,仅仅只有项目名称是必须的,也就是说你在用create-react-app命令的时候,必须在其后跟上你的项目名称,其实这里说的不准确,像--version --info --help这三个选项是不需要带项目名称的,具体看下面:

  • create-react-app -V(or --version):这个选项可以单独使用,打印版本信息,每个工具基本都有吧?
  • create-react-app --info:这个选项也可以单独使用,打印当前系统跟react相关的开发环境参数,也就是操作系统是什么啊,Node版本啊之类的,可以自己试一试。
  • create-react-app -h(or --help):这个肯定是可以单独使用的,不然怎么打印帮助信息,不然就没有上面的截图了。

也就是说除了上述三个参数选项是可以脱离必须参数项目名称以外来单独使用的,因为它们都跟你要初始化的react项目无关,然后剩下的参数就是对要初始化的react项目进行配置的,也就是说三个参数是可以同时出现的,来看一下它们分别的作用:

  • create-react-app <my-project> --verbose:看上图,打印本地日志,其实他是npmyarn安装外部依赖包可以加的选项,可以打印安装有错时的信息。
  • create-react-app <my-project> --scripts-version:由于它本身把创建目录初始化步骤和控制命令分离了,用来控制react项目的开发、打包和测试都放在了react-scripts里面,所以这里可以单独来配置控制的选项,可能这样你还不是很明白,我下面具体说。
  • create-react-app <my-project> --use-npm:这个选项看意思就知道了,create-react-app默认使用yarn来安装,运行,如果你没有使用yarn,你可能就需要这个配置了,指定使用npm

定制选项

关于--scripts-version我还要多说一点,其实在上述截图中我们已经可以看到,create-react-app本身已经对其中选项进行了说明,一共有四种情况,我并没有一一去试他,因为还挺麻烦的,以后如果用到了再来补,我先来大概推测一下他们的意思:

  • 指定版本为0.8.2
  • npm发布自己的react-scripts
  • 在自己的网站上设置一个.tgz的下载包
  • 在自己的网站上设置一个.tar.gz的下载包

从上述看的出来create-react-app对于开发者还是很友好的,可以自己去定义很多东西,如果你不想这么去折腾,它也提供了标准的react-scripts供开发者使用,我一直也很好奇这个,之后我在来单独说官方标准的react配置是怎么做的。

目录分析

随着它版本的迭代,源码肯定是会发生变化的,我这里下载的是v1.1.0,大家可以自行在github上下载这个版本,找不到的戳链接

主要说明

我们来看一下它的目录结构

├── .github
├── packages
├── tasks
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── .yarnrc
├── appveyor.cleanup-cache.txt
├── appveyor.yml
├── CHANGELOG-0.x.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── lerna.json
├── LICENSE
├── package.json
├── README.md
└── screencast.svg

咋一看好多啊,我的天啊,到底要怎么看,其实仔细一晃,好像很多一眼就能看出来是什么意思,大概说一下每个文件都是干嘛的,具体的我也不知道啊,往下看,一步一步来。

  • .github:这里面放着当你在这个项目提issuepr时候的规范
  • packages:字面意思就是包们.....暂时不管,后面详说 ----> 重点
  • tasks:字面意思就是任务们.....暂时不管,后面详说 ----> 重点
  • .eslintignore: eslint检查时忽略文件
  • .eslintrceslint检查配置文件
  • .gitignoregit提交时忽略文件
  • .travis.ymltravis配置文件
  • .yarnrcyarn配置文件
  • appveyor.cleanup-cache.txt:里面有一行Edit this file to trigger a cache rebuild编辑此文件触发缓存,具体干嘛的,暂时不议
  • appveyor.ymlappveyor配置文件
  • CHANGELOG-0.x.md:版本0.X开头的变更说明文件
  • CHANGELOG.md:当前版本变更说明文件
  • CODE_OF_CONDUCT.mdfacebook代码行为准则说明
  • CONTRIBUTING.md:项目的核心说明
  • lerna.jsonlerna配置文件
  • LICENSE:开源协议
  • package.json:项目配置文件
  • README.md:项目使用说明
  • screencast.svg:图片...

看了这么多文件,是不是打退堂鼓了?哈哈哈哈,好了好了,进入正题,其实上述对于我们阅读源码有用的只有packagestaskspackage.json三个文件而已,而且本篇能用到的也就packagespackage.json,是不是想打我.....我也只是想告诉大家这些文件有什么用,它们都是有各自的作用的,如果还不了解,参考下面的参考链接。

参考链接

eslint相关的:eslint官网
travis相关的:travis官网travis入门
yarn相关的:yarn官网
appveyor相关的:appveyor官网
lerna相关的:lerna官网

工具自行了解,本文只说源码相关的packagespackage.json

寻找入口

现在的前端项目大多数都有很多别的依赖,不在像以前那些原生javascript的工具库,拿到源码文件,就可以开始看了,像jQueryunderscore等等,一个两个文件包含了它所有的内容,虽然也有很框架会有umd规范的文件可以直接阅读,像better-scroll等等,但是其实他在书写源码的时候还是拆分成了很多块,最后在用打包工具整合在一起了。但是像create-react-app这样的脚手架工具好像不能像之前那种方法来看了,必须找到整个程序的入口,在逐步突破,所以最开始的工具肯定是寻找入口。

开始关注

拿到一个项目我们应该从哪个文件开始看起呢?只要是基于npm管理的,我都推荐从package.json文件开始看,人家是项目的介绍文件,你不看它看啥。

它里面理论上应该是有名称、版本等等一些说明性信息,但是都没用,看几个重要的配置。

"workspaces": [
    "packages/*"
],

关于workspaces一开始我在npm的说明文档里面没找到,虽然从字面意思我们也能猜到它的意思是实际工作的目录是packages,后来我查了一下是yarn里面的东东,具体看这篇文章,用于在本地测试,具体不关注,只是从这里我们知道了真正的起作用的文件都在packages里面。

重点关注

从上述我们知道现在真正需要关注的内容都在packages里面,我们来看看它里面都是有什么东东:

├── babel-preset-react-app    --> 暂不关注
├── create-react-app
├── eslint-config-react-app   --> 暂不关注
├── react-dev-utils           --> 暂不关注
├── react-error-overlay       --> 暂不关注
└── react-scripts             --> 核心啊,还是暂不关注

里面有六个文件夹,哇塞,又是6个单独的项目,这要看到何年何月.....是不是有这种感触,放宽心大胆的看,先想一下我们在安装了create-react-app后在,在命令行输入的是create-react-app的命令,所以我们大胆的推测关于这个命令应该都是存在了create-react-app下,在这个目录下同样有package.json文件,现在我们把这6个文件拆分成6个项目来分析,上面也说了,看一个项目首先看package.json文件,找到其中的重点:

"bin": {
    "create-react-app": "./index.js"
}

找到重点了,package.json文件中的bin就是在命令行中可以运行的命令,也就是说我们在执行create-react-app命令的时候,就是执行create-react-app目录下的index.js文件。

多说两句

关于package.json中的bin选项,其实是基于node环境运行之后的内容。举个简单的例子,在我们安装create-react-app后,执行create-react-app等价于执行node index.js

create-react-app目录解析

经过以上一系列的查找,我们终于艰难的找到了create-react-app命令的中心入口,其他的都先不管,我们打开packages/create-react-app目录,仔细一瞅,噢哟,只有四个文件,四个文件我们还搞不定吗?除了package.jsonREADME.md就只剩两个能看的文件了,我们来看看这两个文件。

index.js

既然之前已经看到packages/create-react-app/package.json中关于bin的设置,就是执行index.js文件,我们就从index.js入手,开始瞅瞅源码到底都有些虾米。

除了一大串的注释以外,代码其实很少,全贴上来了:

var chalk = require('chalk');

var currentNodeVersion = process.versions.node; // 返回Node版本信息,如果有多个版本返回多个版本
var semver = currentNodeVersion.split('.'); // 所有Node版本的集合
var major = semver[0]; // 取出第一个Node版本信息

// 如果当前版本小于4就打印以下信息并终止进程
if (major < 4) {
  console.error(
    chalk.red(
      'You are running Node ' +
        currentNodeVersion +
        '.\n' +
        'Create React App requires Node 4 or higher. \n' +
        'Please update your version of Node.'
    )
  );
  process.exit(1); // 终止进程
}

// 没有小于4就引入以下文件继续执行
require('./createReactApp');

咋一眼看过去其实你就知道它大概是什么意思了....检查Node.js的版本,小于4就不执行了,我们分开来看一下,这里他用了一个库chalk ,理解起来并不复杂,一行一行的解析。

  • chalk:这个对这段代码的实际影响就是在命令行中,将输出的信息变色。也就引出了这个库的作用改变命令行中输出信息的样式。npm地址

其中有几个Node自身的API

  • process.versions 返回一个对象,包含Node以及它的依赖信息
  • process.exit 结束Node进程,1是状态码,表示有异常没有处理

在我们经过index.js后,就来到了createReactApp.js,下面再继续看。

createReactApp.js

当我们本机上的Node版本大于4的时候就要继续执行这个文件了,打开这个文件,代码还不少,大概700多行吧,我们慢慢拆解。

这里放个小技巧,在读源码的时候,可以在开一个写代码的窗口,跟着写一遍,执行过的代码可以在源文件中先删除,这样700行代码,当你读了200行的时候,源文件就只剩500行了,不仅有成就感继续阅读,也把不执行的逻辑先删除了,影响不到你读其他地方。
const validateProjectName = require('validate-npm-package-name');
const chalk = require('chalk');
const commander = require('commander');
const fs = require('fs-extra');
const path = require('path');
const execSync = require('child_process').execSync;
const spawn = require('cross-spawn');
const semver = require('semver');
const dns = require('dns');
const tmp = require('tmp');
const unpack = require('tar-pack').unpack;
const url = require('url');
const hyperquest = require('hyperquest');
const envinfo = require('envinfo');

const packageJson = require('./package.json');

打开代码一排依赖,懵逼....我不可能挨着去查一个个依赖是用来干嘛的吧?所以,我的建议就是先不管,用到的时候在回来看它是干嘛的,理解更加透彻一些,继续往下看。

let projectName; // 定义了一个用来存储项目名称的变量

const program = new commander.Command(packageJson.name)
  .version(packageJson.version) // 输入版本信息,使用`create-react-app -v`的时候就用打印版本信息
  .arguments('<project-directory>') // 使用`create-react-app <my-project>` 尖括号中的参数
  .usage(`${chalk.green('<project-directory>')} [options]`) //  使用`create-react-app`第一行打印的信息,也就是使用说明
  .action(name => {
    projectName = name; // 此处action函数的参数就是之前argument中的<project-directory> 初始化项目名称 --> 此处影响后面
  })
  .option('--verbose', 'print additional logs') // option配置`create-react-app -[option]`的选项,类似 --help -V
  .option('--info', 'print environment debug info') // 打印本地相关开发环境,操作系统,`Node`版本等等
  .option(
    '--scripts-version <alternative-package>',
    'use a non-standard version of react-scripts'
  ) // 这我之前就说过了,指定特殊的`react-scripts`
  .option('--use-npm') // 默认使用`yarn`,指定使用`npm`
  .allowUnknownOption() // 这个我没有在文档上查到,直译就是允许无效的option 大概意思就是我可以这样`create-react-app <my-project> -la` 其实 -la 并没有定义,但是我还是可以这么做而不会保存
  .on('--help', () => {
    // 此处省略了一些打印信息
  }) // on('--help') 用来定制打印帮助信息 当使用`create-react-app -h(or --help)`的时候就会执行其中的代码,基本都是些打印信息
  .parse(process.argv); // 这个就是解析我们正常的`Node`进程,可以这么理解没有这个东东,`commander`就不能接管`Node`

在上面的代码中,我把无关紧要打印信息省略了,这段代码算是这个文件的关键入口地此处他new了一个commander,这是个啥东东呢?这时我们就返回去看它的依赖,找到它是一个外部依赖,这时候怎么办呢?不可能打开node_modules去里面找撒,很简单,打开npm官网查一下这个外部依赖。

  • commander:概述一下,Node命令接口,也就是可以用它代管Node命令。npm地址

上述只是commander用法的一种实现,没有什么具体好说的,了解了commander就不难,这里的定义也就是我们在命令行中看到的那些东西,比如参数,比如打印信息等等,我们继续往下来。

// 判断在命令行中执行`create-react-app <name>` 有没有name,如果没有就继续
if (typeof projectName === 'undefined') {
  // 当没有传name的时候,如果带了 --info 的选项继续执行下列代码,这里配置了--info时不会报错
  if (program.info) {
    // 打印当前环境信息和`react`、`react-dom`, `react-scripts`三个包的信息
    envinfo.print({
      packages: ['react', 'react-dom', 'react-scripts'],
      noNativeIDE: true,
      duplicates: true,
    });
    process.exit(0); // 正常退出进程
  }
  // 在没有带项目名称又没带 --info 选项的时候就会打印一堆错误信息,像--version 和 --help 是commander自带的选项,所以不用单独配置
  console.error('Please specify the project directory:');
  console.log(
    `  ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}`
  );
  console.log();
  console.log('For example:');
  console.log(`  ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`);
  console.log();
  console.log(
    `Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`
  );
  process.exit(1); // 抛出异常退出进程
}

还记得上面把create-react-app <my-project>中的项目名称赋予了projectName 变量吗?此处的作用就是看看用户有没有传这个<my-project>参数,如果没有就会报错,并显示一些帮助信息,这里用到了另外一个外部依赖envinfo

  • envinfo:可以打印当前操作系统的环境和指定包的信息。 npm地址
到这里我还要吐槽一下segmentfault的编辑器...我同时打开视图和编辑好卡...捂脸.png!

这里我之前省略了一个东西,还是拿出来说一下:

const hiddenProgram = new commander.Command()
  .option(
    '--internal-testing-template <path-to-template>',
    '(internal usage only, DO NOT RELY ON THIS) ' +
      'use a non-standard application template'
  )
  .parse(process.argv);

create-react-app在初始化一个项目的时候,会生成一个标准的文件夹,这里有一个隐藏的选项--internal-testing-template,用来更改初始化目录的模板,这里他已经说了,供内部使用,应该是开发者们开发时候用的,所以不建议大家使用这个选项。

我们继续往下看,有几个提前定义的函数,我们不管,直接找到第一个被执行的函数:

createApp(
  projectName,
  program.verbose,
  program.scriptsVersion,
  program.useNpm,
  hiddenProgram.internalTestingTemplate
);

一个createAPP函数,接收了5个参数

  • projectName: 执行create-react-app <name> name的值,也就是初始化项目的名称
  • program.verbose:这里在说一下commanderoption选项,如果加了这个选项这个值就是true,否则就是false,也就是说这里如果加了--verbose,那这个参数就是true,至于verbose是什么,我之前也说过了,在yarn或者npm安装的时候打印本地信息,也就是如果安装过程中出错,我们可以找到额外的信息。
  • program.scriptsVersion:与上述同理,指定react-scripts版本
  • program.useNpm:以上述同理,指定是否使用npm,默认使用yarn
  • hiddenProgram.internalTestingTemplate:这个东东,我之前给他省略了,我在前面已经补充了,指定初始化的模板,人家说了内部使用,大家可以忽略了,应该是用于开发测试模板目录的时候使用。

找到了第一个执行的函数createApp,我们就来看看createApp函数到底做了什么?

createApp()

function createApp(name, verbose, version, useNpm, template) {
  const root = path.resolve(name); // 获取当前进程运行的位置,也就是文件目录的绝对路径
  const appName = path.basename(root); // 返回root路径下最后一部分

  checkAppName(appName); // 执行 checkAppName 函数 检查文件名是否合法
  fs.ensureDirSync(name); // 此处 ensureDirSync 方法是外部依赖包 fs-extra 而不是 node本身的fs模块,作用是确保当前目录下有指定文件名,没有就创建
  // isSafeToCreateProjectIn 函数 判断文件夹是否安全
  if (!isSafeToCreateProjectIn(root, name)) {
    process.exit(1); // 不合法结束进程
  }
  // 到这里打印成功创建了一个`react`项目在指定目录下
  console.log(`Creating a new React app in ${chalk.green(root)}.`);
  console.log();
  // 定义package.json基础内容
  const packageJson = {
    name: appName,
    version: '0.1.0',
    private: true,
  };
  // 往我们创建的文件夹中写入package.json文件
  fs.writeFileSync(
    path.join(root, 'package.json'),
    JSON.stringify(packageJson, null, 2)
  );
  // 定义常量 useYarn 如果传参有 --use-npm useYarn就是false,否则执行 shouldUseYarn() 检查yarn是否存在
  // 这一步就是之前说的他默认使用`yarn`,但是可以指定使用`npm`,如果指定使用了`npm`,`useYarn`就是`false`,不然执行 shouldUseYarn 函数
  // shouldUseYarn 用于检测本机是否安装了`yarn`
  const useYarn = useNpm ? false : shouldUseYarn();
  // 取得当前node进程的目录,之前还懂为什么要单独取一次,之后也明白了,下一句代码将会改变这个值,所以如果我后面要用这个值,后续其实取得值将不是这个
  // 所以这里的目的就是提前存好,免得我后续使用的时候不好去找,这个地方就是我执行初始化项目的目录,而不是初始化好的目录,是初始化的上级目录,有点绕..
  const originalDirectory = process.cwd();
  // 修改进程目录为底下子进程目录
  // 在这里就把进程目录修改为了我们创建的目录
  process.chdir(root);
  // 如果不使用yarn 并且checkThatNpmCanReadCwd()函数 这里之前说的不是很对,在重新说一次
  // checkThatNpmCanReadCwd 这个函数的作用是检查进程目录是否是我们创建的目录,也就是说如果进程不在我们创建的目录里面,后续再执行`npm`安装的时候就会出错,所以提前检查
  if (!useYarn && !checkThatNpmCanReadCwd()) {
    process.exit(1);
  }
  // 比较 node 版本,小于6的时候发出警告
  // 之前少说了一点,小于6的时候指定`react-scripts`标准版本为0.9.x,也就是标准的`react-scripts@1.0.0`以上的版本不支持`node`在6版本之下
  if (!semver.satisfies(process.version, '>=6.0.0')) {
    console.log(
      chalk.yellow(
        `You are using Node ${process.version} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
          `Please update to Node 6 or higher for a better, fully supported experience.\n`
      )
    );
    // Fall back to latest supported react-scripts on Node 4
    version = 'react-scripts@0.9.x';
  }
  // 如果没有使用yarn 也发出警告
  // 这里之前也没有说全,还判断了`npm`的版本是不是在3以上,如果没有依然指定安装`react-scripts@0.9.x`版本
  if (!useYarn) {
    const npmInfo = checkNpmVersion();
    if (!npmInfo.hasMinNpm) {
      if (npmInfo.npmVersion) {
        console.log(
          chalk.yellow(
            `You are using npm ${npmInfo.npmVersion} so the project will be boostrapped with an old unsupported version of tools.\n\n` +
              `Please update to npm 3 or higher for a better, fully supported experience.\n`
          )
        );
      }
      // Fall back to latest supported react-scripts for npm 3
      version = 'react-scripts@0.9.x';
    }
  }
  // 传入这些参数执行run函数
  // 执行完毕上述代码以后,将执行`run`函数,但是我还是先把上述用到的函数全部说完,在来下一个核心函数`run`
  run(root, appName, version, verbose, originalDirectory, template, useYarn);
}

我这里先来总结一下这个函数都做了哪些事情,再来看看他用到的依赖有哪些,先说做了哪些事情,在我们的目录下创建了一个项目目录,并且校验了这个目录的名称是否合法,这个目录是否安全,然后往其中写入了一个package.json的文件,并且判断了当前环境下应该使用的react-scripts的版本,然后执行了run函数。我们在来看看这个函数用了哪些外部依赖:

  • fs-extra:外部依赖,Node自带文件模块的外部扩展模块 npm地址
  • semver:外部依赖,用于比较Node版本 npm地址

之后函数的函数依赖我都会进行详细的解析,除了少部分特别简单的函数,然后我们来看看这个函数的函数依赖:

  • checkAppName():用于检测文件名是否合法,
  • isSafeToCreateProjectIn():用于检测文件夹是否安全
  • shouldUseYarn():用于检测yarn在本机是否已经安装
  • checkThatNpmCanReadCwd():用于检测npm是否在正确的目录下执行
  • checkNpmVersion():用于检测npm在本机是否已经安装了

checkAppName()

function checkAppName(appName) {
  // 使用 validateProjectName 检查包名是否合法返回结果,这个validateProjectName是外部依赖的引用,见下面说明
  const validationResult = validateProjectName(appName); 
  // 如果对象中有错继续,这里就是外部依赖的具体用法
  if (!validationResult.validForNewPackages) {
    console.error(
      `Could not create a project called ${chalk.red(
        `"${appName}"`
      )} because of npm naming restrictions:`
    );
    printValidationResults(validationResult.errors);
    printValidationResults(validationResult.warnings);
    process.exit(1);
  }
  
  // 定义了三个开发依赖的名称
  const dependencies = ['react', 'react-dom', 'react-scripts'].sort();
  // 如果项目使用了这三个名称都会报错,而且退出进程
  if (dependencies.indexOf(appName) >= 0) {
    console.error(
      chalk.red(
        `We cannot create a project called ${chalk.green(
          appName
        )} because a dependency with the same name exists.\n` +
          `Due to the way npm works, the following names are not allowed:\n\n`
      ) +
        chalk.cyan(dependencies.map(depName => `  ${depName}`).join('\n')) +
        chalk.red('\n\nPlease choose a different project name.')
    );
    process.exit(1);
  }
}

它这个函数其实还蛮简单的,用了一个外部依赖来校验文件名是否符合npm包文件名的规范,然后定义了三个不能取得名字reactreact-domreact-scripts,外部依赖:

  • validate-npm-package-name:外部依赖,检查包名是否合法。npm地址

其中的函数依赖:

  • printValidationResults():函数引用,这个函数就是我说的特别简单的类型,里面就是把接收到的错误信息循环打印出来,没什么好说的。

isSafeToCreateProjectIn()

function isSafeToCreateProjectIn(root, name) {
  // 定义了一堆文件名
  // 我今天早上仔细的看了一些,以下文件的来历就是我们这些开发者在`create-react-app`中提的一些文件
  const validFiles = [
    '.DS_Store',
    'Thumbs.db',
    '.git',
    '.gitignore',
    '.idea',
    'README.md',
    'LICENSE',
    'web.iml',
    '.hg',
    '.hgignore',
    '.hgcheck',
    '.npmignore',
    'mkdocs.yml',
    'docs',
    '.travis.yml',
    '.gitlab-ci.yml',
    '.gitattributes',
  ];
  console.log();

  // 这里就是在我们创建好的项目文件夹下,除了上述文件以外不包含其他文件就会返回true
  const conflicts = fs
    .readdirSync(root)
    .filter(file => !validFiles.includes(file));
  if (conflicts.length < 1) {
    return true;
  }
  // 否则这个文件夹就是不安全的,并且挨着打印存在哪些不安全的文件
  console.log(
    `The directory ${chalk.green(name)} contains files that could conflict:`
  );
  console.log();
  for (const file of conflicts) {
    console.log(`  ${file}`);
  }
  console.log();
  console.log(
    'Either try using a new directory name, or remove the files listed above.'
  );
  // 并且返回false
  return false;
}

他这个函数也算比较简单,就是判断创建的这个目录是否包含除了上述validFiles里面的文件,至于这里面的文件是怎么来的,就是create-react-app在发展至今,开发者们提出来的。

shouldUseYarn()

function shouldUseYarn() {
  try {
    execSync('yarnpkg --version', { stdio: 'ignore' });
    return true;
  } catch (e) {
    return false;
  }
}

就三行...其中execSync是由node自身模块child_process引用而来,就是用来执行命令的,这个函数就是执行一下yarnpkg --version来判断我们是否正确安装了yarn,如果没有正确安装yarn的话,useYarn依然为false,不管指没有指定--use-npm

  • execSync:引用自child_process.execSync,用于执行需要执行的子进程

checkThatNpmCanReadCwd()

function checkThatNpmCanReadCwd() {
  const cwd = process.cwd(); // 这里取到当前的进程目录
  let childOutput = null; // 定义一个变量来保存`npm`的信息
  try {
    // 相当于执行`npm config list`并将其输出的信息组合成为一个字符串
    childOutput = spawn.sync('npm', ['config', 'list']).output.join('');
  } catch (err) {
    return true;
  }
  // 判断是否是一个字符串
  if (typeof childOutput !== 'string') {
    return true;
  }
  // 将整个字符串以换行符分隔
  const lines = childOutput.split('\n');
  // 定义一个我们需要的信息的前缀
  const prefix = '; cwd = ';
  // 去整个lines里面的每个line查找有没有这个前缀的一行
  const line = lines.find(line => line.indexOf(prefix) === 0);
  if (typeof line !== 'string') {
    return true;
  }
  // 取出后面的信息,这个信息大家可以自行试一试,就是`npm`执行的目录
  const npmCWD = line.substring(prefix.length);
  // 判断当前目录和执行目录是否是一致的
  if (npmCWD === cwd) {
    return true;
  }
  // 不一致就打印以下信息,大概意思就是`npm`进程没有在正确的目录下执行
  console.error(
    chalk.red(
      `Could not start an npm process in the right directory.\n\n` +
        `The current directory is: ${chalk.bold(cwd)}\n` +
        `However, a newly started npm process runs in: ${chalk.bold(
          npmCWD
        )}\n\n` +
        `This is probably caused by a misconfigured system terminal shell.`
    )
  );
  // 这里他对windows的情况作了一些单独的判断,没有深究这些信息
  if (process.platform === 'win32') {
    console.error(
      chalk.red(`On Windows, this can usually be fixed by running:\n\n`) +
        `  ${chalk.cyan(
          'reg'
        )} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
        `  ${chalk.cyan(
          'reg'
        )} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` +
        chalk.red(`Try to run the above two lines in the terminal.\n`) +
        chalk.red(
          `To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/`
        )
    );
  }
  return false;
}

这个函数我之前居然贴错了,实在是不好意思。我之前没有弄懂这个函数的意思,今天再来看的时候已经豁然开朗了,它的意思上述代码已经解析了,其中用到了一个外部依赖:

  • cross-spawn:这个我之前说到了没有?忘了,用来执行node进程。npm地址

为什么用单独用一个外部依赖,而不是用node自身的呢?来看一下cross-spawn它自己对自己的说明,Node跨平台解决方案,解决在windows下各种问题。

checkNpmVersion()

function checkNpmVersion() {
  let hasMinNpm = false;
  let npmVersion = null;
  try {
    npmVersion = execSync('npm --version')
      .toString()
      .trim();
    hasMinNpm = semver.gte(npmVersion, '3.0.0');
  } catch (err) {
    // ignore
  }
  return {
    hasMinNpm: hasMinNpm,
    npmVersion: npmVersion,
  };
}

这个能说的也比较少,一眼看过去就知道什么意思了,返回一个对象,对象上面有两个对对,一个是npm的版本号,一个是是否有最小npm版本的限制,其中一个外部依赖,一个Node自身的API我之前也都说过了,不说了。

看到到这里createApp()函数的依赖和执行都结束了,接着执行了run()函数,我们继续来看run()函数都是什么,我又想吐槽了,算了,忍住!!!

run()函数在createApp()函数的所有内容执行完毕后执行,它接收7个参数,先来看看。

  • root:我们创建的目录的绝对路径
  • appName:我们创建的目录名称
  • versionreact-scripts的版本
  • verbose:继续传入verbose,在createApp中没有使用到
  • originalDirectory:原始目录,这个之前说到了,到run函数中就有用了
  • tempalte:模板,这个参数之前也说过了,不对外使用
  • useYarn:是否使用yarn

具体的来看下面run()函数。

run()

function run(
  root,
  appName,
  version,
  verbose,
  originalDirectory,
  template,
  useYarn
) {
  // 这里对`react-scripts`做了大量的处理
  const packageToInstall = getInstallPackage(version, originalDirectory); // 获取依赖包信息
  const allDependencies = ['react', 'react-dom', packageToInstall]; // 所有的开发依赖包

  console.log('Installing packages. This might take a couple of minutes.');
  getPackageName(packageToInstall) // 获取依赖包原始名称并返回
    .then(packageName =>
      // 检查是否离线模式,并返回结果和包名
      checkIfOnline(useYarn).then(isOnline => ({
        isOnline: isOnline,
        packageName: packageName,
      }))
    )
    .then(info => {
      // 接收到上述的包名和是否为离线模式
      const isOnline = info.isOnline;
      const packageName = info.packageName;
      console.log(
        `Installing ${chalk.cyan('react')}, ${chalk.cyan(
          'react-dom'
        )}, and ${chalk.cyan(packageName)}...`
      );
      console.log();
      // 安装依赖
      return install(root, useYarn, allDependencies, verbose, isOnline).then(
        () => packageName
      );
    })
    .then(packageName => {
      // 检查当前`Node`版本是否支持包
      checkNodeVersion(packageName);
      // 检查`package.json`的开发依赖是否正常
      setCaretRangeForRuntimeDeps(packageName);
      // `react-scripts`脚本的目录
      const scriptsPath = path.resolve(
        process.cwd(),
        'node_modules',
        packageName,
        'scripts',
        'init.js'
      );
      // 引入`init`函数
      const init = require(scriptsPath);
      // 执行目录的拷贝
      init(root, appName, verbose, originalDirectory, template);
      // 当`react-scripts`的版本为0.9.x发出警告
      if (version === 'react-scripts@0.9.x') {
        console.log(
          chalk.yellow(
            `\nNote: the project was boostrapped with an old unsupported version of tools.\n` +
              `Please update to Node >=6 and npm >=3 to get supported tools in new projects.\n`
          )
        );
      }
    })
    // 异常处理
    .catch(reason => {
      console.log();
      console.log('Aborting installation.');
      // 根据命令来判断具体的错误
      if (reason.command) {
        console.log(`  ${chalk.cyan(reason.command)} has failed.`);
      } else {
        console.log(chalk.red('Unexpected error. Please report it as a bug:'));
        console.log(reason);
      }
      console.log();

      // 出现异常的时候将删除目录下的这些文件
      const knownGeneratedFiles = [
        'package.json',
        'npm-debug.log',
        'yarn-error.log',
        'yarn-debug.log',
        'node_modules',
      ];
      // 挨着删除
      const currentFiles = fs.readdirSync(path.join(root));
      currentFiles.forEach(file => {
        knownGeneratedFiles.forEach(fileToMatch => {
          if (
            (fileToMatch.match(/.log/g) && file.indexOf(fileToMatch) === 0) ||
            file === fileToMatch
          ) {
            console.log(`Deleting generated file... ${chalk.cyan(file)}`);
            fs.removeSync(path.join(root, file));
          }
        });
      });
      // 判断当前目录下是否还存在文件
      const remainingFiles = fs.readdirSync(path.join(root));
      if (!remainingFiles.length) {
        console.log(
          `Deleting ${chalk.cyan(`${appName} /`)} from ${chalk.cyan(
            path.resolve(root, '..')
          )}`
        );
        process.chdir(path.resolve(root, '..'));
        fs.removeSync(path.join(root));
      }
      console.log('Done.');
      process.exit(1);
    });
}

他这里对react-script做了很多处理,大概是由于react-script本身是有node版本的依赖的,而且在用create-react-app init <project>初始化一个项目的时候,是可以指定react-script的版本或者是外部自身定义的东东。

他在run()函数中的引用都是用Promise回调的方式来完成的,从我正式接触Node开始就习惯用async/await,所以对Promise还真不熟,恶补了一番,下面我们来拆解其中的每一句和每一个函数的作用,先来看一下用到外部依赖还是之前那些不说了,来看看函数列表:

  • getInstallPackage():获取要安装的react-scripts版本或者开发者自己定义的react-scripts
  • getPackageName():获取到正式的react-scripts的包名
  • checkIfOnline():检查网络连接是否正常
  • install():安装开发依赖包
  • checkNodeVersion():检查Node版本信息
  • setCaretRangeForRuntimeDeps():检查发开依赖是否正确安装,版本是否正确
  • init():将事先定义好的目录文件拷贝到我的项目中

知道了个大概,我们在来逐一分析每个函数的作用:

getInstallPackage()

function getInstallPackage(version, originalDirectory) {
  let packageToInstall = 'react-scripts'; // 定义常量 packageToInstall,默认就是标准`react-scripts`包名
  const validSemver = semver.valid(version); // 校验版本号是否合法
  if (validSemver) {
    packageToInstall += `@${validSemver}`; // 合法的话执行,就安装指定版本,在`npm install`安装的时候指定版本为加上`@x.x.x`版本号,安装指定版本的`react-scripts`
  } else if (version && version.match(/^file:/)) {
    // 不合法并且版本号参数带有`file:`执行以下代码,作用是指定安装包为我们自身定义的包
    packageToInstall = `file:${path.resolve(
      originalDirectory,
      version.match(/^file:(.*)?$/)[1]
    )}`;
  } else if (version) {
    // 不合法并且没有`file:`开头,默认为在线的`tar.gz`文件
    // for tar.gz or alternative paths
    packageToInstall = version;
  }
  // 返回最终需要安装的`react-scripts`的信息,或版本号或本地文件或线上`.tar.gz`资源
  return packageToInstall;
}

这个方法接收两个参数version版本号,originalDirectory原始目录,主要的作用是判断react-scripts应该安装的信息,具体看每一行。

这里create-react-app本身提供了安装react-scripts的三种机制,一开始初始化的项目是可以指定react-scripts的版本或者是自定义这个东西的,所以在这里他就提供了这几种机制,其中用到的外部依赖只有一个semver,之前就说过了,不多说。

getPackageName()

function getPackageName(installPackage) {
  // 函数进来就根据上面的那个判断`react-scripts`的信息来安装这个包,用于返回正规的包名
  // 此处为线上`tar.gz`包的情况
  if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {
    // 里面这段创建了一个临时目录,具体它是怎么设置了线上.tar.gz包我没试就不乱说了
    return getTemporaryDirectory()
      .then(obj => {
        let stream;
        if (/^http/.test(installPackage)) {
          stream = hyperquest(installPackage);
        } else {
          stream = fs.createReadStream(installPackage);
        }
        return extractStream(stream, obj.tmpdir).then(() => obj);
      })
      .then(obj => {
        const packageName = require(path.join(obj.tmpdir, 'package.json')).name;
        obj.cleanup();
        return packageName;
      })
      .catch(err => {
        console.log(
          `Could not extract the package name from the archive: ${err.message}`
        );
        const assumedProjectName = installPackage.match(
          /^.+\/(.+?)(?:-\d+.+)?\.(tgz|tar\.gz)$/
        )[1];
        console.log(
          `Based on the filename, assuming it is "${chalk.cyan(
            assumedProjectName
          )}"`
        );
        return Promise.resolve(assumedProjectName);
      });
  // 此处为信息中包含`git+`信息的情况
  } else if (installPackage.indexOf('git+') === 0) {
    return Promise.resolve(installPackage.match(/([^/]+)\.git(#.*)?$/)[1]);
  // 此处为只有版本信息的时候的情况
  } else if (installPackage.match(/.+@/)) {
    return Promise.resolve(
      installPackage.charAt(0) + installPackage.substr(1).split('@')[0]
    );
  // 此处为信息中包含`file:`开头的情况
  } else if (installPackage.match(/^file:/)) {
    const installPackagePath = installPackage.match(/^file:(.*)?$/)[1];
    const installPackageJson = require(path.join(installPackagePath, 'package.json'));
    return Promise.resolve(installPackageJson.name);
  }
  // 什么都没有直接返回包名
  return Promise.resolve(installPackage);
}

他这个函数的目标就是返回一个正常的依赖包名,比如我们什么都不带就返回react-scripts,在比如我们是自己定义的包就返回my-react-scripts,继续到了比较关键的函数了,接收一个installPackage参数,从这函数开始就采用Promise回调的方式一直执行到最后,我们来看看这个函数都做了什么,具体看上面每一行的注释。

总结一句话,这个函数的作用就是返回正常的包名,不带任何符号的,来看看它的外部依赖:

  • hyperquest:这个用于将http请求流媒体传输。npm地址

他本身还有函数依赖,这两个函数依赖我都不单独再说,函数的意思很好理解,至于为什么这么做我还没想明白:

  • getTemporaryDirectory():不难,他本身是一个回调函数,用来创建一个临时目录。
  • extractStream():主要用到node本身的一个流,这里我真没懂为什么药改用流的形式,就不发表意见了,在看其实我还是没懂,要真正的明白是要去试一次,但是真的有点麻烦,不想去关注。
PS:其实这个函数很好理解就是返回正常的包名,但是里面的有些处理我都没想通,以后理解深刻了在回溯一下。

checkIfOnline()

function checkIfOnline(useYarn) {
  if (!useYarn) {
    return Promise.resolve(true);
  }

  return new Promise(resolve => {
    dns.lookup('registry.yarnpkg.com', err => {
      let proxy;
      if (err != null && (proxy = getProxy())) {
        dns.lookup(url.parse(proxy).hostname, proxyErr => {
          resolve(proxyErr == null);
        });
      } else {
        resolve(err == null);
      }
    });
  });
}

这个函数本身接收一个是否使用yarn的参数来判断是否进行后续,如果使用的是npm就直接返回true了,为什么会有这个函数是由于yarn本身有个功能叫离线安装,这个函数来判断是否离线安装,其中用到了外部依赖:

  • dns:用来检测是否能够请求到指定的地址。npm地址

install()

function install(root, useYarn, dependencies, verbose, isOnline) {
  // 封装在一个回调函数中
  return new Promise((resolve, reject) => {
    let command; // 定义一个命令
    let args;  // 定义一个命令的参数
    // 如果使用yarn
    if (useYarn) {
      command = 'yarnpkg';  // 命令名称
      args = ['add', '--exact']; // 命令参数的基础
      if (!isOnline) {
        args.push('--offline');  // 此处接上面一个函数判断是否是离线模式
      }
      [].push.apply(args, dependencies); // 组合参数和开发依赖 `react` `react-dom` `react-scripts`
      args.push('--cwd'); // 指定命令执行目录的地址
      args.push(root); // 地址的绝对路径
      // 在使用离线模式时候会发出警告
      if (!isOnline) {
        console.log(chalk.yellow('You appear to be offline.'));
        console.log(chalk.yellow('Falling back to the local Yarn cache.'));
        console.log();
      }
    // 不使用yarn的情况使用npm
    } else {
      // 此处于上述一样,命令的定义 参数的组合
      command = 'npm';
      args = [
        'install',
        '--save',
        '--save-exact',
        '--loglevel',
        'error',
      ].concat(dependencies);
    }
    // 因为`yarn`和`npm`都可以带这个参数,所以就单独拿出来了拼接到上面
    if (verbose) {
      args.push('--verbose');
    }
    // 这里就把命令组合起来执行
    const child = spawn(command, args, { stdio: 'inherit' });
    // 命令执行完毕后关闭
    child.on('close', code => {
      // code 为0代表正常关闭,不为零就打印命令执行错误的那条
      if (code !== 0) {
        reject({
          command: `${command} ${args.join(' ')}`,
        });
        return;
      }
      // 正常继续往下执行
      resolve();
    });
  });
}

又到了比较关键的地方了,仔细看每一行代码注释,此处函数的作用就是组合一个yarn或者npm的安装命令,把这些模块安装到项目的文件夹中,其中用到的外部依赖cross-spawn前面有说了,就不说了。

其实执行到这里,create-react-app已经帮我们创建好了目录,package.json并且安装了所有的依赖,reactreact-domreact-scrpts,复杂的部分已经结束,继续往下走。

checkNodeVersion()

function checkNodeVersion(packageName) {
  // 找到`react-scripts`的`package.json`路径
  const packageJsonPath = path.resolve(
    process.cwd(),
    'node_modules',
    packageName,
    'package.json'
  );
  // 引入`react-scripts`的`package.json`
  const packageJson = require(packageJsonPath);
  // 在`package.json`中定义了一个`engines`其中放着`Node`版本的信息,大家可以打开源码`packages/react-scripts/package.json`查看
  if (!packageJson.engines || !packageJson.engines.node) {
    return;
  }
  // 比较进程的`Node`版本信息和最小支持的版本,如果比他小的话,会报错然后退出进程
  if (!semver.satisfies(process.version, packageJson.engines.node)) {
    console.error(
      chalk.red(
        'You are running Node %s.\n' +
          'Create React App requires Node %s or higher. \n' +
          'Please update your version of Node.'
      ),
      process.version,
      packageJson.engines.node
    );
    process.exit(1);
  }
}

这个函数直译一下,检查Node版本,为什么要检查了?之前我已经说过了react-scrpts是需要依赖Node版本的,也就是说低版本的Node不支持,其实的外部依赖也是之前的几个,没什么好说的。

setCaretRangeForRuntimeDeps()

function setCaretRangeForRuntimeDeps(packageName) {
  const packagePath = path.join(process.cwd(), 'package.json');  // 取出创建项目的目录中的`package.json`路径
  const packageJson = require(packagePath); // 引入`package.json`
  // 判断其中`dependencies`是否存在,不存在代表我们的开发依赖没有成功安装
  if (typeof packageJson.dependencies === 'undefined') {
    console.error(chalk.red('Missing dependencies in package.json'));
    process.exit(1);
  }
  // 拿出`react-scripts`或者是自定义的看看`package.json`中是否存在
  const packageVersion = packageJson.dependencies[packageName];
  if (typeof packageVersion === 'undefined') {
    console.error(chalk.red(`Unable to find ${packageName} in package.json`));
    process.exit(1);
  }
  // 检查`react` `react-dom` 的版本 
  makeCaretRange(packageJson.dependencies, 'react');
  makeCaretRange(packageJson.dependencies, 'react-dom');
  // 重新写入文件`package.json`
  fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
}

这个函数我也不想说太多了,他的作用并没有那么大,就是用来检测我们之前安装的依赖是否写入了package.json里面,并且对依赖的版本做了检测,其中一个函数依赖:

  • makeCaretRange():用来对依赖的版本做检测

我没有单独对其中的子函数进行分析,是因为我觉得不难,而且对主线影响不大,我不想贴太多说不完。

到这里createReactApp.js里面的源码都分析完了,咦!你可能会说你都没说init()函数,哈哈哈,看到这里说明你很认真哦,init()函数是放在packages/react-scripts/script目录下的,但是我还是要给他说了,因为它其实跟react-scripts包联系不大,就是个copy他本身定义好的模板目录结构的函数。

init()

它本身接收5个参数:

  • appPath:之前的root,项目的绝对路径
  • appName:项目的名称
  • verbose:这个参数我之前说过了,npm安装时额外的信息
  • originalDirectory:原始目录,命令执行的目录
  • template:其实其中只有一种类型的模板,这个选项的作用就是配置之前我说过的那个函数,测试模板
// 当前的包名,也就是这个命令的包
const ownPackageName = require(path.join(__dirname, '..', 'package.json')).name;
// 当前包的路径
const ownPath = path.join(appPath, 'node_modules', ownPackageName);
// 项目的`package.json`
const appPackage = require(path.join(appPath, 'package.json'));
// 检查项目中是否有`yarn.lock`来判断是否使用`yarn`
const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));

appPackage.dependencies = appPackage.dependencies || {};

// 定义其中`scripts`的
appPackage.scripts = {
  start: 'react-scripts start',
  build: 'react-scripts build',
  test: 'react-scripts test --env=jsdom',
  eject: 'react-scripts eject',
};
// 重新写入`package.json`
fs.writeFileSync(
  path.join(appPath, 'package.json'),
  JSON.stringify(appPackage, null, 2)
);

// 判断项目目录是否有`README.md`,模板目录中已经定义了`README.md`防止冲突
const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
if (readmeExists) {
  fs.renameSync(
    path.join(appPath, 'README.md'),
    path.join(appPath, 'README.old.md')
  );
}
// 是否有模板选项,默认为当前执行命令包目录下的`template`目录,也就是`packages/react-scripts/tempalte`
const templatePath = template
  ? path.resolve(originalDirectory, template)
  : path.join(ownPath, 'template');
if (fs.existsSync(templatePath)) {
  // 拷贝目录到项目目录
  fs.copySync(templatePath, appPath);
} else {
  console.error(
    `Could not locate supplied template: ${chalk.green(templatePath)}`
  );
  return;
}

这个函数我就不把代码贴全了,里面的东西也蛮好理解,基本上就是对目录结构的修改和重名了那些,挑了一些来说,到这里,create-react-app从零到目录依赖的安装完毕的源码已经分析完毕,但是其实这只是个初始化目录和依赖,其中控制环境的代码都存在react-scripts中,所以其实离我想知道的关键的地方还有点远,但是本篇已经很长了,不打算现在说了,多多包涵。

希望本篇对大家有所帮助吧。

啰嗦两句

本来这篇我是打算把create-react-app中所有的源码的拿出来说一说,包括其中的webpack的配置啊,eslint的配置啊,babel的配置啊.....等等,但是实在是有点多,他自己本身把初始化的命令和控制react环境的命令分离成了packages/create-react-apppackages/react-script两边,这个篇幅才把packages/create-react-app说完,更复杂的packages/react-script在说一下这篇幅都不知道有多少了,所以我打算之后空了,在单独写一篇关于packages/react-script的源码分析的文。

码字不易,可能出现错别字什么的,说的不清楚的,说错的,欢迎指正,多多包涵!

查看原文

赞 52 收藏 82 评论 2

anRui 关注了用户 · 2020-03-16

xialeistudio @xialeistudio

《ThinkPHP实战》《ThinkPHP5实战》《Node.js 与 Webpack 开发实战》作者,慕课网签约讲师,知名互联网公司资深研发,第四本书正在写作中。
微信xialeistudio

关注 4910

anRui 赞了文章 · 2019-12-26

学习使用Chrome性能分析工具(译)

原文地址:https://developers.google.com...

开始

在本教程中,你将学会如何使用性能分析工具分析页面上的性能瓶颈。

在隐身模式下打开Google Chrome。隐身模式确保Chrome在干净的状态下运行。例如,如果你安装了很多扩展,这些扩展可能会影响到性能分析的结果。

在隐身窗口中加载以下页面。这是本教程的Demo,页面显示了一堆上下移动的蓝色小方块。

https://googlechrome.github.i...

接着按 F12 打开 DevTools。

图片描述

图1: Demo 在左侧,DevTools在右侧。

注意:为了保证更好的阅读体验,在后续的截图中,DelTools分到单独的窗口显示。

模拟移动设备的CPU

移动设备的CPU算力比台式机和笔记本电脑小得多。无论何时评测页面,都可以使用CPU调节来模拟页面在移动设备上的表现。

  1. 在DevTools中,单击 Performance 选项卡。
  2. 确保选中了 Screenshots
  3. 单击 Capture Settings(设置按钮)。其中包含了捕获性能指标相关的设置。
  4. 对于 CPU选项,选择 2x slowdown。DevTools会进行 CPU 节流,使其比平时慢2倍。

图片描述

图2: CPU 节流,蓝色框区域

注意:在测试其他页面时,如果要确保它们在低端移动设备上工作良好,请将CPU节流设置为减速20倍。这个演示不能很好地使用20倍的减速,所以它只使用2倍的减速作为教学目的。

配置 Demo

很难为本网站的所有读者创建一致的运行时性能演示。本节允许你自定义演示,以确保你的体验与你在本教程中看到的屏幕截图和描述相对一致,而不管你的特定设置如何。

  1. 继续单击 Add 10,直到蓝色方块移动明显比以前慢。在高端机器上,可能需要大约20次点击。
  2. 单击 Optimize,蓝色方块应该移动得更快更流畅。
  3. 单击 Un-Optimize,蓝色方块应该移动得更慢且更加卡顿
注意:如果你看不到优化版本和未优化版本之间的明显差异,请尝试单击 Subtract 10 几次,然后再试一次。如果你添加太多的蓝色方块,相当于把CPU都几乎占满了,就看不到优化和不优化版本的差异。

记录运行时的页面性能

当你运行优化版本时,蓝色方块移动得更快。为什么?两个版本都应该在相同的时间内,将每个方块移动相同的距离。在性能面板中录制,学习如何检测未优化版本中的性能瓶颈。

  1. 在 DevTools 中,单击 Record(左上角灰色圆圈)。DevTools 会捕捉页面运行时的性能指标。

    图片描述

    图3:页面记录中

  2. 等待几秒
  3. 单击 Stop,DevTools 停止记录,分析数据,然后会将分析结果展示在性能面板中。

    图片描述

    图4:分析的结果

    哇,这么多的数据。别慌,很快我们就知道具体的含义了。

分析结果

一旦你拿到了页面的性能分析数据,你会发现这个页面的性能到底有多差,并且找到导致页面性能差的原因。

分析帧率

衡量任何动画性能的主要指标就是帧率(FPS)。当动画以60 fps的速度运行时,用户会很爽。

  1. 注意FPS图表。只要你看到一条红条,就意味着低帧率,进而影响用户体验。通常来说,绿色的柱条越高,代表帧率越高。

    图片描述
    图5:蓝框内的FPS图表

  2. 在FPS图表下方,你可以看到CPU图表。CPU图表中的颜色与“性能”面板底部的 Summary 选项卡中的颜色相对应。CPU 图表充满颜色意味着CPU在记录过程中达到了最大负载。每当你看到CPU长时间达到最大负载时,这是进行优化的一个很好的提示。

    图片描述

    图6:蓝框内的 CPU 图表和 Summary(摘要栏)

  3. 将鼠标悬停在 FPS、CPU 或 NET 图表上。DevTools 显示该页面在该时间点的屏幕截图。向左和向右移动鼠标以重放记录过程。这称为 scrubbing,它对于手动分析动画过程很有用。

图片描述

图7:查看页面在2000ms左右时的屏幕截图

  1. Frames 区域中,将鼠标悬停在其中一个绿色方块上。DevTools 向你显示该特定帧的 FPS。每帧可能远远低于60 FPS。

    图片描述

    图8:鼠标悬停在一帧上

    当然,在这个 DEMO 中,很明显这个页面的性能不是很好。但是在真实的场景中,我们不一定能一眼分辨出一个页面的性能好坏,所以使用这些工具来进行测量分析是很方便的。

查出性能瓶颈的根源

现在你已经测量并验证了页面动画表现不佳,接下来要回答的问题是:为什么?

  1. 注意 Summary 选项卡,在未选择任何事件时,它呈现了浏览器在整个记录过程中把时间花在哪个部分。可以看到,页面的大部分时间都花在渲染上。所以现在的目标就是:减少浏览器花费在渲染工作上的时间。

图片描述

图9:蓝框内的 Summary 选项卡

  1. 展开 Main 区域,DevTools 向你展示了一段时间内主线程上活动图。x 轴代表着这段时间内的记录,每一个 Bar 都代表了一个事件。Bar 越宽,意味着该活动花费的时间更长。y轴表示调用堆栈,当你看到事件堆叠在一起时,这意味着上面的事件导致了下面的事件。

    图片描述

    图10:蓝框内的 Main 区域

  2. 记录过程中有很多数据。在 OverView 面板(有 CPU, FPS, NET 图表的区域)上,用鼠标单击、按住、拖拽来放大单个 Animation Frame Fired 事件。此时 MainSummary 中展示了选中的区间的相关信息。

    图片描述

    图11:放大单个 Animation Frame Fired 事件

    提示:你也可以通过单击 Main 中的某个事件后,通过鼠标的滚轮或者 W,S,A,D 键实现单个事件的缩放。
  3. 注意在 Animation Frame Fired 事件右上角的红三角。只要你看到了红三角,这个事件就可能造成严重的问题。

    提示:每当 requestAnimationFrame() 回调调用时, 都会触发 Animation Frame Fired 事件
  4. 单击某个 Animation Frame Fired 事件, Summary 中会展示与该事件相关的信息. 注意 reveal 链接,单击后,DevTools 会将触发当前的 Animation Frame Fired 事件的事件高亮出来。同时注意 app.js:94 链接,单击后跳转到相应的源码。

    图片描述

    图12: 查看 Animation Frame Fired 事件的详细信息

    提示:选中一个事件之后,用左右方向键可以跳转到上/下一个事件
  5. app.update 事件下,有一堆紫色事件。稍微放大,看起来每个都可能有一个红色的三角形。现在单击其中一个紫色事件。DevTools 在 Summary 中提供了有关事件的详细信息。可以看到,有一个关于强制回流(forced reflows)的警告(也就是 Layout 的另一种说法)。
  6. Summary 中,单击 Layout Forced 下的 app.js:70 链接,DevTools 会跳转到引发强制回流的源代码。

    图片描述

    图13:导致强制回流的源代码

    注意:这行代码的问题在于:修改了蓝块样式之后,立刻读取蓝块 offsetTop 值。此时样式变更,而offsetTop 值是上一帧的值,浏览器为了保证读取 offsetTop 值的准确性,会先处理样式变更,然后重新布局以计算准确的 offsetTop 值,而重新布局(回流)的性能开销是很大的。参考:Avoid_forced_synchronous_layouts

分析“优化版”的性能

使用刚刚学习的工作流和工具,单击演示中的优化以启用优化的代码,进行另一次性能记录,然后分析结果。从改进的帧率到 Main 中的活动图表中事件的减少,你可以看到应用程序的优化版本做的工作少得多,从而带来更好的性能。

优化前后的性能分析图

优化前后的代码对比

app.update = function (timestamp) {
    for (var i = 0; i < app.count; i++) {
      var m = movers[i];
      if (!app.optimize) { // 1.普通版本
        var pos = m.classList.contains('down') ?
            m.offsetTop + distance : m.offsetTop - distance; // 读取offsetTop, 变更样式
        if (pos < 0) pos = 0;
        if (pos > maxHeight) pos = maxHeight;
        m.style.top = pos + 'px';
        if (m.offsetTop === 0) { // 样式变更后读取 offsetTop,导致回流
          m.classList.remove('up');
          m.classList.add('down');
        }
        if (m.offsetTop === maxHeight) { // 样式变更后读取 offsetTop,导致回流
          m.classList.remove('down');
          m.classList.add('up');
        }
      } else { // 2.优化版本
        var pos = parseInt(m.style.top.slice(0, m.style.top.indexOf('px')));
        m.classList.contains('down') ? pos += distance : pos -= distance; // 通过读取top,来获取原来蓝块的位置,避免读取 offsetTop 
        if (pos < 0) pos = 0;
        if (pos > maxHeight) pos = maxHeight;
        m.style.top = pos + 'px';
        if (pos === 0) { // 样式变更后用从样式 top 中读取到的位置信息进行判断,避免读取 offsetTop
          m.classList.remove('up');
          m.classList.add('down');
        }
        if (pos === maxHeight) {
          m.classList.remove('down');
          m.classList.add('up');
        }
      }
    }
    frame = window.requestAnimationFrame(app.update);
  }
注意:优化版本的代码虽然不会触发回流(Layout),但依然会触发重绘(Paint)。一个更好的解决方案是使用只会触发合成(Composite)的属性,例如: transform 和 opacity。

参考: Use transform and opacity changes for animations

下一步

了解性能的基础是轨道模型(The RAIL model)。这个模型告诉你对你的用户来说最重要的性能指标。有关详细信息,请参见 Measure Performance With The RAIL Model

为了让性能面板更舒适,熟能生巧。尝试分析自己的页面并分析结果。如果你对结果有任何疑问,去Stack Overflow 提出关于 google-chrome-devtools 的问题。如果可能,包括可复制页面的截图或链接。

要真正掌握运行时性能,你必须了解浏览器如何将HTML、CSS和JS转换为屏幕上的像,可以参考: Rendering Performance Overview. 这篇文章则更加深入:The Anatomy Of A Frame

最后,有许多方法可以提高运行时性能。本教程将重点放在一个特定的动画瓶颈上,让你通过性能面板进行重点介绍,但这只是你可能遇到的众多瓶颈之一。如何提升页面运行时的性能还可以参考以下关于渲染性能的文章:

查看原文

赞 19 收藏 9 评论 1

anRui 回答了问题 · 2019-08-22

解决tradingview 手机端适配问题

先自答一下 还有部分问题 没有解决

要保证 侧边栏 在手机端的时候 同样显示

需要在 enabled_features 允许 侧边边栏不消失

// 允许侧边工具栏 不消失
enabled_features: [
    "keep_left_toolbar_visible_on_small_screens"
],

关注 2 回答 2

anRui 提出了问题 · 2019-08-21

解决tradingview 手机端适配问题

通过添加 meta 标签使官网的例子可以适配手机端
但是侧边控制栏没有了,通过向 enabled_features 允许 left_toolbar 但是没有任何效果

clipboard.png
侧边栏

clipboard.png

同时 一些tradingview 的一些弹框只是pc端的样式 ,如果想要跟,官网同样的样式 tradingview 是有配置项 还是需要重新写 css

clipboard.png

关注 2 回答 2

anRui 发布了文章 · 2019-06-08

初探React技术栈(三)

react-intl

对于一些项目而言单一语言已经不能满足需求了,所以我们也要尝试一下使用react的多语言解决方案,在之前使用vue的时采用的是vue-i18n多语言解决方案,但是在react中i18n支持的不是特别好,所以就选用react-intl

$ npm or cnpm
$ npm install react-intl --save

此次多语言与之相匹配的ui是Ant Design,适配的也是相应的国际化,同时切换语言配置时,始终是觉得没有加载,直接替换是更好的体验。所以需要redux作为支持,具体redux的使用请参看我的上一篇文章

效果演示

App.jsx

新建语言包(en,zh)

首先新建一个文件夹lang这个作为本地语言包。

# lang文件夹目录结构
.
├── en_US.js
├── locale
│   ├── en_US.json 
│   └── zh_CN.json
└── zh_CN.js

1 directory, 4 files

# 其中js为语言配置文件 en/英 zh/中
自定义的语言JSON
// en_US.JSON
{
    "test": "test"
}
// zh_CN.json
{
    "test": "测试"
}
// 注意本地的语言包一定要对应上
主要的配置JS
// en_US.js
import appLocaleData from 'react-intl/locale-data/en' 
import enLocal from './locale/en_US.json'
import enUS from 'antd/lib/locale-provider/en_US'

const en = {
    data: appLocaleData, // react-intl 语言包
    locale: enLocal, // 自定义的语言包
    localeName: 'en', // 配置命名
    antd: enUS // antd 语言包
}

export default en

// zh_CN.js
import appLocaleData from 'react-intl/locale-data/zh'
import zhLocal from './locale/zh_CN.json'
import zhCN from 'antd/lib/locale-provider/zh_CN'

const zh = {
    data: appLocaleData,
    locale: zhLocal,
    localeName: 'zh',
    antd: zhCN
}

export default zh

与redux绑定

// action 
function changeLang (text) {
    return { type: 'changeLang', text: text }
}
// 定义切换语言的action

// reducer
// 语言包
import zh from '../../lang/zh_CN'
import en from '../../lang/en_US'

// 默认语言为英文
const Initstate = {
    // 读取本地存储 来决定当前语言环境
    lang: window.localStorage.getItem('lang') === 'zh' ? zh : en
}
const Common = (state = Initstate, action) => {
    switch (action.type) {
        case 'changeLang':
                // 防止用户刷新 语言回到初始状态
                window.localStorage.setItem('lang', action.text)
                return {...state, lang: action.text === 'zh' ? zh : en}
        default:
                return state
    }
}

这样reduxreact-intl已经绑定在一起了,调用changeLang以达到改变语言的摩的

与react绑定

想要实现多语言切换无刷新,应把整体的切换操作放在react执行流中,修改redux的同时会刷新视图层,基于这种特性才会把多语言的切换绑定到redux中。

App.jsx

App.jsx

react 组件中的使用

import React, { Component } from 'react'

import { injectIntl } from 'react-intl'

class IntlComponent extends Component {
    render () {
        let { intl } = this.props
        return (
            <div>
                { intl.messages['test'] }
                {/*调用方式*/}

                {/*有一种场景 比如获取验证码 需要有秒数 但是 中英文切换的时候 需要符合语义 那么就不能使用拼接的方式 而 react-intl 给我们提供了一种方式 formatMessage */}

                { intl.formatMessage({id: 'code_tip'},{s: '2'}) }


                {
                    /*
                        // en_US.JSON
                        {
                            "test": "test",
                            "code_tip": "Get the captcha again after {s} seconds"
                        }
                        // zh_CN.json
                        {
                            "test": "测试",
                            "code_tip": "{s}秒后重新获取验证码"
                        }
                    */
                }
            </div>
        )
    }
}

export default injectIntl(IntlComponent)

链接

我的博客

Blog_demo 本文react-intl_demo

react-intl examples

结语

以上就是使用与配置react-intl的全过程了,我会一步步搭建整体的项目,项目的demo都在我的github中后续将会描述 react-router 在项目中的用法 配置过程 以及整体配置的思路

查看原文

赞 0 收藏 0 评论 0

anRui 发布了文章 · 2019-06-07

初探React技术栈(二)

redux

redux是js的状态容器,提供可预测的状态管理,同时可运行于不同的环境并且还有redux-devtools供可视化调试,大型应用下有良好的跨组件通讯与状态管理是必不可少的,那么就在本章中探索redux是如何与react串联,并是如何使用redux

$ npm or cnpm
$ npm install redux react-redux
相信有不少人都比较好奇 为什么我已经有了redux还要再多引入一个react-redux实际上这样是为保证核心功能最大程度上的跨平台复用

首先新建一个文件夹store用来存放redux的配置及相关文件,看一下store中的文件结构

.
├── actions
│   └── index.js
├── index.js
└── reducers
    └── index.js

2 directories, 3 files

# 其中最外层的index.js是主要的配置文件

在react的入口文件index.js中引入react-redux

示例图片

Provider是react-redux两个核心工具之一,作用:将store传递到项目的组件中,并可以在组件中使用redux

import一般引入文件夹会默认找到该文件夹中的index.js

store.js reudx 主文件

import { applyMiddleware, createStore } from 'redux'

import thunk from 'redux-thunk'

import reducers from './reducers/index'

let store = createStore(
    reducers,
    applyMiddleware(thunk)
)

export default store

redux中以createStore创建store并在其中插入reducers与中间件,redux-thunk是为了增强action在原生的reduxaction只能返回对象,但是加上这个中间件就可以返回一个函数并且可以访问其他action

action

// 测试action
function test (text) {
    return { type: 'test', text: text }
}

export default {
    test
}

reducer

import { combineReducers } from 'redux'

const Initstate = {
    // 初始状态
}
const Common = (state = Initstate, action) => {
    switch (action.type) {
        case 'test':
            return {...state, test: action.text}
        default: 
            return state
    }
}

let reducer = combineReducers({
    Common: Common
})
// 注意combineReducers是用于合并多个reducer 当所用模块不多,协作少时 可以不用
从reducer中我们就可以发现redux的三大原则:

1.单一数据源: 所谓的单一数据源是只有一个Model虽然我们可以定义多个 reducer 但是经过combineReducers 整合发现 所有的 Model 都存在一个大的JSON里面

2.状态是只读: 我们可以发现当我们接收到名为test的变化时;并不是修改 Initstate 而是?> 直接返回state相当于只是读取这个默认状态并与action中返回的状态整合

3.状态修改均有纯函数完成:可以发现 common这个函数实用switch接收到一定的action.type 就会返回相应的属猪

与react组件相结合

以App.jsx示例
import React, { Component } from 'react'
import { connect } from 'react-redux'
import Action from ‘./../store/actions/index’

class App extends Component {
    test () {
        this.props.test(‘test’)
        // 测试数据
    }
    render () {
        return (
            <div>
                { this.props.test } {/*测试数据*/}
            </div>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        test: state.Common.test
    }
}

const mapDispatchToProps = (dispatch, ownProps)  => {
    return {
        test: (data) => dispatch(Action.test(data))
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)
mapStateToProps: 这个函数的主要主要作用在与把modal中的状态与该组件的props进行融合以至于组件可以直接通过this.props.test访问到modal中的状态

mapDispatchToProps: 这个函数的主要作用是把action中的函数通过dispatch分发一个actionreducer并把action函数合并到该组件中的props

链接

Blog_demo 本文redux_demo

我的博客

redux

结语

关于在react项目中如何使用redux,以及具体写法,我的分层方式这些都在此文中有所展现。但是这只是刚接触时,我的一些思路,还有一些有趣的东西我会在后续中提及(router 与 redux 绑定 ,middleware 的基本原理等)如果各位大佬有更好的思路请在留言告诉我,不胜感激

查看原文

赞 2 收藏 0 评论 0

anRui 发布了文章 · 2019-05-23

初探React技术栈(一)

react

最近已经开始使用react技术栈了,从头开始搭建项目,有必要的记录一下配置的过程以及项目分层的思路,这次后台项目采用的主要采用react-create-app脚手架以及Ant DesignUI 以及多语言react-intl

create-react-app

这是官方维护的脚手架应用 我们一般也采用这个
$ npm or cnpm

$ npm install create-react-app -g #全局下载

$ create-react-app #[项目名称] 新建项目

# 如果想要把 webpack 配置暴露出来 可以执行以下命令
$ npm run eject # 初始时可以在package.json中找到

less/sass

为了提升我们写样式的效率 一般采用 less/sass
# less
$ npm i less less-loader

# sass
$ npm i node-sass sass-loader

并在webpack.config.js修改以下配置:

# 添加sass支持
# 找到 file-loader
{
    loader: require.resolve('file-loader'),
    exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/, /\.scss$/],
    options: {
    name: 'static/media/[name].[hash:8].[ext]',
    },
},
# 在exclude中加上`/\.scss$/` 并且在后面加上
{ 
    test: /\.scss$/,
    loader: ['node-sass', 'sass-loader']
}

示例图片

// 添加less支持 这种是省事的写法 如果需要 antd 的个性化主题定制 请参看下文 antd less 配置
// 找到 `/\.css$/` 改成 `/\.(?:le|c)ss$/`
// 找到 `getStyleLoaders` 函数 在其中 loaders 中添加
{
    loader: require.resolve('less-loader'),
    options: cssOptions,
}
// 这样就完成 less 支持
// 如果想要样式模块化那么就需要加上  modules: true
{
    test: cssRegex, // cssRegex这个代表着就是 匹配文件的正则表达式
    exclude: cssModuleRegex,
    use: getStyleLoaders({
    importLoaders: 1,
    modules: true,  //模块化
    sourceMap: isEnvProduction && shouldUseSourceMap,
    }),
    sideEffects: true,
},
完成对less/sass支持,!注意 样式模块化需要不同的文件命名方式 比如 app.scss 模块化 -> app.module.scss 否则编译器也不知道那个文件是私有的

配置路径

一般情况下 我们引入文件都是需要相对路径但是如果嵌套层级少还可以但是如果层级很多 我们就需要寻找一种办法帮助我们 来摆脱繁琐的 ./../......

同样还是在webpack.config.js 找到alias在其中添加 '@': paths.appSrc

配置的路径

Ant Design

之前一直使用的 vue + element 的布局方式,而这次写项目则是主要使用 React + Ant Design

不得不说Ant Design整体的动画以及各种方案让我耳目一新!天啦噜这个动画 爱了 爱了

# 下载antd
$ npm or cnpm
$ npm i antd --save 

首先在全局样式文件中写@import '~antd/dist/antd.css';此时就可以看到antd已经引入成功了

根据 Ant Design 官方文档 推荐其中定制主题介绍了less

所有可定制的less变量

@import '~antd/dist/antd.less'; // 引入官方提供的 less 样式入口文件
@import 'your-theme-file.less'; // 用于覆盖上面定义的变量
警告根据antd issues 7929中提示 css less 不要都采用less-loader
// 首先在webpack中定义两种变量
const lessRegex = /\.less$/; // 可以找到 `style files regexes` 处声明
const lessModuleRegex = /\.module\.less$/;
// 这是为了匹配 .less / .module.less 文件
// 找到 `getStyleLoaders` 函数 在其中 loaders 中添加
{
    loader: require.resolve('less-loader'),
    options: cssOptions,
}
// 找到 sassModuleRegex 位置处 在对象后加上 以下代码
{
    test: lessRegex,
    exclude: lessModuleRegex,
    use: getStyleLoaders(
    {
        importLoaders: 1,
        sourceMap: isEnvProduction && shouldUseSourceMap
    }
    ),
    sideEffects: true,
},
{
    test: lessModuleRegex,
    use: getStyleLoaders(
    {
        importLoaders: 1,
        sourceMap: isEnvProduction && shouldUseSourceMap,
        modules: true, // 模块化
        getLocalIdent: getCSSModuleLocalIdent,
    }
    ),
}
此时还没有完成 但是less / module.less 但是在改主题色的时候,我们发现会报错

来源 antd issues 7927

// 此时需要修改以下代码
// 找到 `getStyleLoaders` 函数 在其中 loaders 中添加
{
    loader: require.resolve('less-loader'),
    options: {...cssOptions, javascriptEnabled: true}, // 启用js编译。。。
}
相信用scss/sass的同学也不少
// 找到 `getStyleLoaders` 函数 在其中 loaders 中添加
{
    loader: require.resolve('less-loader'),
    options: {...cssOptions, javascriptEnabled: true, modifyVars: {'primary-color': '#000',}}, // 启用js编译。。。
}

然后在index.js 入口文件中引入自定义的全局less文件,并在里面引入~antd/dist/antd.less
然后可以继续使用scss写样式不受影响,虽然这种做法非常鸡肋,但是我搜索了几种方法,但是都不理想,希望有知道更好的方法的大佬告知

链接

less/sass to create-react-app
Ant Design

结语

好长时间没有写文章了,嗯嗯 其实是因为工作忙!!!最近学习的东西也比较多,觉得是一个很好的巩固,分享的时间,打算就这段时间的学习,写一个小系列的分享文章,希望有错误的地方有大佬能够不吝赐教!!晚安

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 8 次点赞
  • 获得 31 枚徽章 获得 1 枚金徽章, 获得 9 枚银徽章, 获得 21 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-02-09
个人主页被 556 人浏览