-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.json
1 lines (1 loc) · 191 KB
/
search.json
1
[{"title":"qlc解决了什么问题","url":"/sheldon_blog/passages/qlc解决了什么问题/","content":"\n### 目前遇到的问题\n\n作为一名前端工程师,陆陆续续负责了不少项目,这些项目中,有一些是正在迭代的,其他同事同时在负责的项目,但是也有不少项目,要么就是老旧项目维护的同事已经离职或转岗了的,要么就是新项目从 0 开始的,再加上前端代码积累速度和迭代速度都比较快,其中暴露了不少问题。\n\n我身边的大多数程序员都有一个特点,就是喜欢把具体的东西抽象化,我们通常会抽象出公共的函数或方法、公共的类或HOC,放在一起,集中在项目的某一个文件夹下,叫做 js 文件夹或 lib 文件夹(以下我们用 js 文件夹代表公共函数文件夹 )。\n\n这样做的确带来了很多便利,但同时也有一些隐患:\n\n* js 文件夹下代码越来越多,而且大多数鹅厂小伙伴的作风是 0 文档 0 注释,这给新接手项目的同学熟悉项目带来了极大的麻烦。\n* 不同项目都有自己的 js 文件夹,在开发一个新项目时,我们通常的做法是:\n\t* 直接将原有项目的 js 文件夹拷贝到新项目中,这样在新项目中,我们也可以直接使用这些公共函数了。\n\t* 将原有项目的部分 js 文件拷贝到新项目中,并且随着新项目的开发,增量拷贝。\n\t* 以上两种做法,本质区别不大,前者会直接给新项目增加很多无用代码(原有项目中所谓的公共函数在新项目中并不一定会用到),而对这两种方式,如果我们要修复一个 bug,修改或升级公共代码中的一个文件,那么我们就要一个一个的,将修复好的文件拷贝到不同的项目中,如果项目多了并且已经交由不同的人维护了,这简直是一个灾难。\n* 由于公共 js 文件夹下内容比较多,并且有的开发同学习惯以 'urlUtils.js'、'strUtils.js' 这种方式来整合一些小的函数集,这样会造成函数重复的隐患(毕竟我们一个文件一行行的去分析目前的公共库已经有了哪些能力是不现实的),我观察过之前自己接手的一个不算复杂的项目(潘多拉),其仅仅是从 url 解析 query 这种功能函数,就有多达 3个(甚至更多),分布在 js 文件夹以及 node_modules 里面,这显然是不同的维护人员由于信息不对称重复引入的。\n* 对于怎么样才能算作“公共”函数,目前是缺乏一个 review 过程的,任何项目开发人员,几乎都可以无限制地在公共 js 文件夹下增加内容,并在之后被携带着拷贝到其他项目中,这其中有些函数,也许并不适合在这里。\n\n### 问题归纳与解决\n\n实际上,总结下来,我们需要解决三个痛点:\n\n* 以低成本的方式增加高可读性的文档,方便新接手项目的同学熟悉。\n* 解决跨项目之间的公共函数复用和更新维护困难的问题。\n* 增加必要的 review 环节,对公共函数库的必要性和代码正确性进行 review。\n\n就第一个问题而言,其实现在的前端文档工具链已经极大降低了写文档的成本了,利用 [jsdoc](http://usejsdoc.org/) 或 [esdoc](https://esdoc.org/) 等文档生成工具,我们基本上已经不需要手动写文档,而是在写代码的同时写注释,就可以自动生成文档,并且配合相关的编辑器插件,一部分注释都可以自动生成。\n\n但是目前我经历的大多数项目还是没有文档,这里可能是由于以下四个原因:\n\n* 部分同学并不知道有 esdoc、jsdoc 这种比较好用的文档生成工具。\n* 开发组中没有人去推动,文档不属于 KPI 和考核的内容,加之时间紧迭代快,缺乏前人栽树的动力。\n* 虽然现在的文档生成工具比较简单了,但一般还是需要一定的配置,也有一点上手成本。\n* 生成的文档不能十分满足需求,例如 esdoc 默认只能生成 html 格式的文档,在编辑器里面没法直接看。\n\n针对第一个问题,qlc 做出了一些努力:\n\n* 0 配置,全自动化生成文档,甚至集成到了其他开发流程中,命令行也不用敲。\n* 基于 esdoc 以及开源插件二次开发,可以选择性生成 html 和 markdown 格式的文档,注重文档体验。\n* 基于 esdoc 注释写作成本更低,更能节省时间。\n\n>跟 jsdoc 相比,esdoc 使用方式比较简单,不需要严格使用标签,而且能够支持搜索,并且官方资料更为齐全。\n\n至此,使用 qlc 生成文档,已经非常简单了。\n\n第二、第三个问题实际上是公共函数库的维护问题,qlc 也设计了对应的流程,着力解决该问题:\n\n* 首先有一个远程公共库(基于 git)。\n* 对于某一个项目而言,可以从远程库中下拉所需要的公共函数/类,并自动生成文档。\n* 如果我们对某一个项目增加了一个公共函数并且认为可以为更多的项目所用,命令行上传到远程库自动触发 MR,维护人员 review 通过后即可供其他项目使用。\n* 修复或更新某一个公共函数之后,我们只需同步到远程库,其他项目维护人员在命令行工具的辅助下同步即可。\n\n### 更多\n\n到此,你是否认为 qlc 给你带来了一定的价值呢,可以到 qlc 的官方仓库查看更多的[文档细节](https://git.code.oa.com/qlc/qlc)\n\n\n\n\n\n","tags":["javascript"]},{"title":"入门WebAssembly以及使用其进行图像卷积处理","url":"/sheldon_blog/passages/入门WebAssembly以及使用其进行图像卷积处理/","content":"\n> WebAssembly 出现有很长时间了,但是由于日常工作并无直接接触,因此一直疏于尝试,最近终于利用一些业余时间简单入门了一下,因此在此总结。\n\n### 简介\n\n首先我们需要知道 WebAssembly 是一个什么东西,其实际是一个字节码编码方式,比较接近机器码(但是又无法直接执行),这样可以方便地做到跨平台同时省去像 JavaScript 等语言的解释时间,所以是有一定优势的,使用起来其实也比较灵活,凡是可以转化成字节码的,都是可以使用 WebAssembly。\n\n以下仅列举部分可以支持 WebAssembly 转化的语言:\n\n* [AssemblyScript](https://github.com/AssemblyScript/assemblyscript): 语法和 TypeScript 一致(事实上,其是 Typescript 的一个子集),对前端来说学习成本低,为前端编写 WebAssembly 最佳选择;\n* c\\c++: 官方推荐的方式,详细使用见[文档](http://webassembly.org.cn/getting-started/developers-guide/);\n* [Rust](https://www.rust-lang.org/): 语法复杂、学习成本高,对前端来说可能会不适应。详细使用见[文档](https://github.com/rust-lang-nursery/rust-wasm);\n* [Kotlin](http://kotlinlang.org/): 语法和 Java、JS 相似,语言学习成本低,详细使用见[文档](https://kotlinlang.org/docs/reference/native-overview.html);\n* [Golang](https://golang.org/): 语法简单学习成本低。但对 WebAssembly 的支持还处于未正式发布阶段,详细使用见[文档](https://blog.gopheracademy.com/advent-2017/go-wasm/)。\n\n尝试使用 WebAssembly 官方推荐的方式,我们首先可以在[这里](http://webassembly.org.cn/getting-started/developers-guide/)来下载。\n\n如果用腾讯内网有的文件是下载不下来的,这个时候我们可以给命令行增加一个代理(如果我们用的 Fiddler 或 Charles,开启的时候默认命令行也可以走代理,如果是 Whistle,我们需要手动设置代理),有些文件我们还可以下载好之后使用文件代理。\n\n```\nexport https_proxy=\"http://127.0.0.1:8899\"\nexport http_proxy=\"http://127.0.0.1:8899\"\n// 文件代理:\nhttps://s3.amazonaws.com/mozilla-games/emscripten/packages/node-v8.9.1-darwin-x64.tar.gz file:///Users/niexiaotao/node-v8.9.1-darwin-x64.tar.gz\n```\n\n## 初体验\n\n这里考虑到前端同学的上手难度,我们先使用 AssemblyScript 写一个极小的例子,一个斐波那契函数:\n\n```\nexport function f(x: i32): i32 {\n if (x === 1 || x === 2) {\n return 1;\n }\n return f(x - 1) + f(x - 2)\n}\n```\n\n通过类似 `asc f.ts -o f.wasm` 这样的命令编译成 f.wasm 之后,我们可以分别在 Node 环境和浏览器环境来执行:\n\nNode:\n\n```\nconst fs = require(\"fs\");\nconst wasm = new WebAssembly.Module(\n fs.readFileSync(__dirname + \"/f.wasm\"), {}\n);\nconst myModule = new WebAssembly.Instance(wasm).exports;\nconsole.log(myModule.f(12));\n```\n\n浏览器:\n\n```\nfetch('f.wasm') // 网络加载 f.wasm 文件\n .then(res => res.arrayBuffer()) // 转成 ArrayBuffer\n .then( buffer =>\n WebAssembly.compile(buffer)\n )\n .then(module => { // 调用模块实例上的 f 函数计算\n const instance = new WebAssembly.Instance(module);\n const { f } = instance.exports;\n console.log('instance:', instance.exports);\n console.log('f(20):', f(20));\n });\n```\n\n于是,我们完成了 WebAssembly 的初体验。\n\n当然,这个例子太简单了。\n\n## 使用 WebAssembly 进行图像卷积处理\n\n实际上,WebAssembly 的目的在于解决一些复杂的计算问题,优化 JavaScript 的执行效率。所以我们可以使用 WebAssembly 来处理一些图像或者矩阵的计算问题。\n\n接下来,我们通过 WebAssembly 来处理一些图像的卷积问题,用于图像的风格变换,我们最终的例子可以在[这里](http://assembly.niexiaotao.com/)体验。\n\n每次进行卷积处理,我们的整个流程是这样的:\n\n* 将原图像使用 canvas 绘制到屏幕上。\n* 使用 `getImageData` 获取图像像素内容,并转化成类型数组。\n* 将上述类型数组通过共享内存的方式传递给 WebAssembly 部分。\n* WebAssembly 部分接收到数据,进行计算,并且通过共享内存的方式返回。\n* 将最终结果通过 canvas 画布更新。\n\n上述各个步骤中,绘制的部分集中在 JavaScript 端,而计算的部分集中在 WebAssembly,这两部分相互比较独立,可以分开编写,而双端数据通信是一个比较值得注意的地方,事实上,我们可以通过 ArrayBuffer 来实现双端通信,简单的说,JavaScript 端和 WebAssembly 可以共享一部分内存,并且都拥有读写能力,当一端写入新数据之后,另一段也可以读到,这样我们就可以进行通信了。\n\n关于数据通信的问题,这里还有一个比较直白的[科普文章](https://segmentfault.com/a/1190000010434237),可以参考。\n\n在这里没有必要对整个项目代码进行展示,因此可以参考([代码地址](https://github.com/aircloud/assemConvolution)),我们这里仅仅对部分关键代码进行说明。\n\n### 共享内存\n\n首先,我们需要声明一块共享内存,这其实可以使用 WebAssembly 的 API 来完成:\n\n```\nlet memory = new WebAssembly.Memory({ initial: ((memSize + 0xffff) & ~0xffff) >>> 16 });\n```\n\n这里经过这样的比较复杂的计算是因为 initial 传入的是以 page 为单位,详细可以参考[这里](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory),实际上 memSize 即我们共享内存的字节数。\n\n然后这里涉及到 memSize 的计算,我们主要需要存储三块数据:卷积前的数据、卷积后的数据(由于卷积算法的特殊性以及为了避免更多麻烦,这里我们不进行数据共用),还有卷积核作为参数需要传递。\n\n这里我们共享内存所传递的数据按照如下的规则进行设计:\n\n![](http://niexiaotao.cn/img/ker1.jpg)\n\n传递给 WebAssembly 端的方式并不复杂,直接在 `WebAssembly.instantiate` 中声明即可。 \n\n```\nfetch(wasmPath)\n .then(response => response.arrayBuffer())\n .then(buffer => WebAssembly.instantiate(buffer, {\n env: {\n memory,\n abort: function() {}\n },\n Math\n })).then(module => {})\n \n```\n\n然后我们在 AssemblyScript 中就可以进行读写了:\n\n```\n//写:\nstore<u32>(position, v) // position 为位置\n\n//读:\nload<u32>(position) // position 为位置\n```\n\n而在 JavaScript 端,我们也可以通过 `memory.buffer` 拿到数据,并且转化成类型数组:\n\n```\nlet mem = new Uint32Array(memory.buffer)\n//通过 mem.set(data) 可以在 JavaScript 端进行写入操作\n```\n\n这样,我们在 JavaScript 端和 AssemblyScript 端的读写都明晰了。\n\n这里需要注意的是,**JS端采用的是大端数据格式,而 AssemblyScript 中采用的是小端,因此其颜色数据格式为 AGBR**\n\n### 卷积计算\n\n我们所采用的卷积计算本身算法比较简单,并且不是本次的重点,但是这里需要注意的是:\n\n* 我们无法直接在 AssemblyScript 中声明数组并使用,因此除了 Kernel 通过共享内存的方式传递过来以外,我们应当尽量避免声明数组使用(虽然也有使用非共享内存数组的相关操作,但是使用起来比较繁琐)\n* 卷积应当对 R、G、B 三层单独进行,我这里 A 层不参与卷积。\n\n以上都在代码中有所体现,参考相关代码便可明了。\n\n卷积完成后,我们通过共享内存的方法写入类型数组,然后在 JavaScript 端合成数据,调用 `putImageData` 上屏即可。\n\n### 其他\n\n当然,本次图像卷积程序仅仅是对 Webassembly 和 AssemblyScript 的初步尝试,笔者也在学习阶段,如果上述说法有问题或者你想和我交流,也欢迎留言或者提相关 issue。\n","tags":["WebAssembly"]},{"title":"web跨端融合方案浅析","url":"/sheldon_blog/passages/web跨端融合方案浅析/","content":"\n本文会对目前流行的基于 JavaScript 的 web 跨端融合方案进行总结和分析,目标人群为 web 方向的从业者但是对跨端融合方案了解不多的人。\n\n### web 跨端融合简介\n\n在 2015 年 React Native 发布之前,web 在移动端 APP 上主要通过 WebView 进行承载,其有许多优点,可以快速迭代发布,不特别受 APP 版本的影响,因此,一些快速发展的业务(包括前期的手机QQ、手机淘宝)大量采用了 WebView 内嵌 H5 页面的形式来推动业务。\n\n但是这种方式缺点也比较明显,主要体现在以下两点:\n\n* 加载时间较长,包括 WebView 初始化的时间、网络请求的时间。\n* HTML 页面在性能上天然不如 Native 页面,无论怎么进行性能优化。\n\n在 2015 年,Facebook 推出了 React Native,从而打开了 web 跨端融合的大门,后续在此架构基础上又出现了阿里巴巴的 Weex(2016)、腾讯的小程序(小程序实际上更偏 web 一点,和其他几类稍有不同,本文不作介绍)、 Hippy(2018)、Taro(Taro 其实更偏向解释翻译,和其他几类定位不同)等跨端融合解决方案,并且渐渐被用到越来越多的项目中,目前,跨端融合开发已经是一种比较主流的 web 开发模式,在阿里系应用、腾讯的微信、QQ浏览器、手机QQ均已经进行了大规模应用。\n\n### 基本架构\n\n虽然 web 跨端融合方案众多,除了上述提到的三种,还有各个公司的更多方案,但是一般来说跨端融合的技术架构都比较相近,我们可以通过下面这一个图来简单概括:\n\n![](/img/1.jpg)\n\n接下来,我们逐个进行简析:\n\n* 业务代码:即我们写的 React Native 代码、Weex 代码,一般来说,我们的业务代码需要经过框架工具或者打包工具(例如 webpack 配合 loader)进行打包,从而兼容一些 ES Next 的写法以及一些框架本身不支持的 Web 写法。\n* Javascript FrameWork:这部分主要是针对 Weex、Hippy 来讲的,Weex 声称支持 Vue、Rax 语法,而 Hippy 声称支持 React、Vue 写法,实际上,对于这些库而言,并不是直接将 React、Vue 引入到项目中,而是会对其源代码进行修改(Vue 有针对 Weex 平台的[版本](https://github.com/vuejs/vue/tree/dev/src/platforms)),而 Hippy 也是对 React 源代码进行了修改,例如,你写的一个` createElement `的操作,在 Web 平台中实际调用的是 `document.createElement(tagName) `这个接口;而在 Weex 平台中实际执行的是` new renderer.Element(tagName)`(renderer 由 Javascript Runtime 提供,并且最终和 Native 通信渲染上屏)。\n* Javascript Runtime:Runtime 的部分,主要是对外暴露了一些统一的接口,比如说节点的增删改查、网络请求的接口等,而这些借口,实际上是其“代理”的客户端的能力,通过客户端 JSAPI 的方式进行调用。另外,把 Runtime 和 FrameWork 进行抽离,也可以便于一个跨端方案适配多个框架,只需要将不同的 FrameWork 和浏览器交互的部分代码转换成 Runtime 提供的标准接口,就可以实现对不同框架的支持。\n* Core:这部分主要是对 Javascript 的解释执行,在 iOS 上一般是 JSCore(系统自带,给客户端提供了执行 JavaScript 程序的能力),而安卓上则可以采用 V8、X5 等。\n* 最下层则是分 Android 和 iOS 端去进行渲染。\n\n### 发展现状\n\n实际上,React Native 最初提出这种解决方案的时候,市面上并没有同类的产品,但是由于 React Native 的一些问题和其他原因,各个大公司基本都在实现自己的跨端融合方案,这里 React Native 的问题主要体现在:\n\n* 最主要的是协议风险。\n* React Native 打包出来的 JSBundle 较大,并且默认没有灵活的分包机制,需要自行解决相关问题。\n* 在部分组件比如 List 组件中,性能较差(据非官方说法,性能并不是 React Native 团队首要考察因素,但是国内团队一般都比较重视性能)。\n* 部分事件发送频繁导致性能损失、例如列表滚动事件、手势事件等。\n* 双端 API 大量没有对齐(这也和其 slogan 是‘learn once, write everywhere’ 而不是 ‘write once, run everywhere’ 相对应)。\n\n而对于国内的 Weex 和 Hippy 框架,其都做了大量的性能优化解决了上述问题,并且规避了协议风险(Weex 采用了 Apache 2.0 协议,而 Hippy 即将开源)。\n\n另外值得一提的是,Weex 和 Hippy 都可以在 web 端进行运行,一般可以作为降级方案使用,从而真正做到了“一份代码”,三端运行。\n\n### 性能优化\n\n实际上,采用目前的跨端融合方案的体验已经比采用 WebView 的方案强太多了,但是性能优化是没有止境的,随着页面复杂度的提高以及用户体验的要求,实际上目前这类跨端融合方案采用了以下几个方向的性能和用户体验优化:\n\n#### 减少网络请求\n\n在我们上述提供的架构图中,一般而言对于一个这类页面,业务代码是通过网络请求加载的,这个时候在加载上主要省去的是 WebView 的初始化时间,这其实是不够的,所以我们也可以采用将业务代码提前下发并存在用户本地,打开的时候只需要从本地拉取并执行代码,这样可以减少相关的网络请求阻塞,优化加载时间。\n\n另外,减少网络请求还体现在对资源的缓存上,对一个页面中所采用的图片等资源文件进行 LRU 策略的缓存,从而防止重复的请求(在传统的 WebView 的方案上,也可以采用对 WebView 增加 Hook 的方式实现)。\n\n当然,以上两点在 WebView 的方案上也可以采用。\n\n#### 降低通信成本\n\n我们从上文的架构图中可以看出,这里的层级实际上比较多,如果不同层级的通信数据较多,并且有比较频繁甚至重复的编解码操作,肯定会有很大的开销,从而影响性能,所以,在不同层级之间做好数据的传递,并且防止重复的编解码操作是比较重要的。\n\n这里可以优化的细节其实比较多,我们举一个 Hippy 的例子:\n\n在 Hippy 架构中,jsRuntime 会生成一个 jsObject 对象树(即需要渲染的 DOM 信息),其在经过 JSBridge 时需要通过`JSON.stringify` 进行序列化,而在 Java(andriod) 接收端,则需要先将其变成一个 JsonObject,最终转化成 HippyMap,这里实际上是有重复的编解码操作的,我们看看 Hippy 的优化策略:\n\n![](/img/3.jpg)\n\n>图片来自 IMWeb 2018\n\n通过 hippybuffer 的方式减少通信的数据量,并且防止重复的编解码操作,可以有效提高性能。\n\n#### 减少通信次数\n\n为了减少在通信方面的消耗,我们除了降低通信的成本,还可以做的就是减少通信次数,当然,前提是不影响用户体验。\n\n这方面可以减少的通信消耗,其中一个方面是频繁的事件通信,我们知道,事件的触发是在 native 端的,但是事件处理的逻辑代码实际上是在 js 层来完成的,在这方面的通信,React Native 就因为频繁的通信从而影响了性能。\n\n我们可以优化的地方在于,首先减少没有绑定回调函数的事件通信,一般而言这部分通信是不必要的,其次是多次通信可以进行合并,比如说 list 滚动回调函数、以及动画通信,我们可以通过配置驱动代替数据驱动的方式(即一次向客户端传递整个配置,后续相同事件可以直接在客户端进行处理),来减少通信次数。\n\n这方面 Hippy 和 Weex 都有大量细碎的实践,在此便不具体介绍了。\n\n#### 降低首屏时间\n\n在原来的 WebView 页面中,我们为了增强用户体验,防止用户进来之后看到白屏,可以采用服务端渲染的方式,将渲染好的页面返回给客户端,同时优化了首屏请求,也防止了客户端设备较差造成JS执行时间较长的情况。\n\n在跨端融合方案中我们仍然有类似的解决方案,在不考虑离线包的情况下(即只考虑业务代码从远程加载的情况),我们也可以由服务端渲染好再返回,Weex 便采用了类似的方案,不过其做的更加彻底,在服务端将代码结果编译成 AST 树并转化成字节码(OPcode),在客户端解析后直接生成虚拟 DOM:\n\n![](/img/2.jpg)\n\n>图片来自 IMWeb 2018\n\n#### 客户端级别的其他优化\n\n客户端的优化有一部分是本来客户端开发就会面临的内容,也有一部分是和混合方案有关的优化,比如 Flex Render 的优化,不过这方面的内容一般而言和前端关系不是非常密切,笔者作为初级前端工程师,对这方面的内容还并不熟悉。\n\n### 框架选型\n\n本文的最后一部分,介绍框架选型。\n\n对于各类跨端融合的方案,其相对于 WebView 都有非常大的性能提升,因此在前期,无论选择什么框架都能够看到成效,这里也并不进行特定的框架选型推荐,但是一般认为,如果是从 Vue 的项目切换,Weex 会更合适一点,而如果从 React 项目切换,在确保没有证书风险的情况下可以采用 React Native,否则可以尝试原生支持 React 的 Hippy。\n\n以上。\n\n\n","tags":["跨端融合"]},{"title":"Node.js 的 TCP 链接管理","url":"/sheldon_blog/passages/Node-js的TCP链接管理/","content":"\n在 Node.js 的微服务中,一般不同的服务模块我们会采用 TCP 进行通信,本文来简单谈一谈如何设计 TCP 服务的基础管理。\n\n>在具体设计上,本文参考了微服务框架 [Seneca](https://github.com/senecajs/seneca) 所采用的通信方案 [Seneca-transport](https://github.com/senecajs/seneca-transport),已经被实践所证明其可行性。\n\n一提到 TCP 通信,我们肯定离不开 `net` 模块,事实上,借助 `net` 模块,我们也可以比较快速地完成一般的 TCP 通信的任务。\n\n为了避免对基础的遗忘,我们还是先附上一个基本的 TCP 链接代码:\n\n```javascript\n//server.js:\nconst net = require('net');\n\nconst server = net.createServer((socket) => {\n socket.write('goodbye\\n');\n socket.on('data', (data) => {\n console.log('data:', data.toString());\n socket.write('goodbye\\n');\n })\n}).on('error', (err) => {\n throw err;\n});\n\n// grab an arbitrary unused port.\nserver.listen(8024, () => {\n console.log('opened server on', server.address());\n});\n\n//client.js:\nconst net = require('net');\n\nconst client = net.createConnection({ port: 8024 }, () => {\n //'connect' listener\n console.log('connected to server!');\n client.write('world!\\r\\n');\n setInterval(() => {\n client.write('world!\\r\\n');\n }, 1000)\n});\nclient.on('data', (data) => {\n console.log(data.toString());\n // client.end();\n});\nclient.on('end', () => {\n console.log('disconnected from server');\n});\n```\n\n其实,上述已经是一个几乎最简单的客户端和服务端通信 Demo,但是并不能在实际项目中使用,首先我们需要审视,其离生产环境还差哪些内容:\n\n1. 以上要求 Server 端要在 Client 端之前启动,并且一旦因为一些错误导致 Server 端重启了并且这个时候 Client 端正好和 Server 端进行通信,那么肯定会 crash,所以,我们需要一个更为平滑兼容的方案。\n2. 以上 TCP 链接的 Server 部分,并没有对 connection 进行管理的能力,并且在在以上的例子中,双方都没有主动释放链接,也就是说,建立的是一个 TCP 长连接。\n3. 以上链接的处理数据能力有限,只能处理纯文本的内容,并且还有一定的风险性(你也许会说可以用 JSON 的序列化反序列化的方法来处理 JSON 数据,但是你别忘了 `socket.on('data'...` 很可能接收到的不是一个完整的 JSON,如果 JSON 较长,其可能只接收到一般的内容,这个时候如果直接 `JSON.parse())` 很可能就会报错)。\n\n以上三个问题,便是我们要解决的主要问题,如果你看过之后立刻知道该如何解决了,那么这篇文章可能你不需要看了,否则,我们可以一起继续探索解决方案。\n\n### 使用 reconnect-core\n\n[reconnect-core](https://www.npmjs.com/package/reconnect-core) 是一个和协议无关的链接重试算法,其工作方式也比较简单,当你需要在 Client 端建立链接的时候,其流程是这样的:\n\n* 调用事先传入的链接建立函数,如果这个时候返回成功了,即成功建立链接。\n* 如果第一次建立链接失败了,那么再隔一段时间建立第二次,如果第二次还是失败,那么再隔一段更长的时间建立第三次,如果还是失败,那么再隔更长的一段时间……直到到达最大的尝试次数。\n\n实际上关于尝试的时间间隔,也会有不同的策略,比较常用的是 Fibonacci 策略和 exponential 策略。\n\n当然,关于策略的具体实现,reconnect-core 采用了一个 [backoff](https://www.npmjs.com/package/backoff) 的库来管理,其可以支持 Fibonacci 策略和 exponential 策略以及更多的自定义策略。\n\n对于上面提到的 DEMO 代码。我们给出 Client 端使用 reconnect-core 的一个实现:\n\n```javascript\n//client.js:\nconst Reconnect = require('reconnect-core');\nconst net = require('net');\nconst Ndjson = require('ndjson');\n\nconst Connect = Reconnect(function() {\n var args = [].slice.call(arguments);\n return net.connect.apply(null, args)\n});\n\nlet connection = Connect(function(socket) {\n socket.write('world!\\r\\n');\n socket.on('data', (msg) => {\n console.log('data', msg.toString());\n });\n socket.on('close', (msg) => {\n console.log('close', msg).toString();\n connection.disconnect();\n });\n socket.on('end', () => {\n console.log('end');\n });\n});\n\nconnection.connect({\n port: 8024\n});\nconnection.on('reconnect', function () {\n console.log('on reconnect...')\n});\nconnection.on('error', function (err) {\n console.log('error:', err);\n});\nconnection.on('disconnect', function (err) {\n console.log('disconnect:', err);\n});\n```\n>采用 Reconnect 实际上相比之前是多了一层内容,我们在这里需要区分 connection 实例和 socket 句柄,并且附加正确的时间监听。\n\n现在,我们就不用担心到底是先启动服务端还是先启动客户端了,另外,就算我们的服务端在启动之后由于某些错误关闭了一会,只要没超过最大时间(而这个也是可配置的),仍然不用担心客户端与其建立连接。\n\n\n### 给 Server 端增加管理能力\n\n给 Server 端增加管理能力是一个比较必要的并且可以做成不同程度的,一般来说,最重要的功能则是及时清理链接,常用的做法是收到某条指令之后进行清理,或者到达一定时间之后定时清理。\n\n这里我们可以增加一个功能,达到一定时间之后,自动清理所有链接:\n\n```javascript\n//server.js\nconst net = require('net');\n\nvar connections = [];\n\nconst server = net.createServer((socket) => {\n connections.push(socket);\n socket.write('goodbye\\n');\n socket.on('data', (data) => {\n console.log('data:', data.toString());\n socket.write('goodbye\\n');\n })\n}).on('error', (err) => {\n throw err;\n});\n\nsetTimeout(() => {\n console.log('clear connections');\n connections.forEach((connection) => {\n connection.end('end')\n // connection.destory()\n })\n}, 10000);\n\n// grab an arbitrary unused port.\nserver.listen(8024, () => {\n console.log('opened server on', server.address());\n});\n```\n\n我们可以通过`connection.end('end')` 和 `connection.destory()` 来清理,一般来说,前者是正常情况下的关闭指令,需要 Client 端进行确认,而后者则是强制关闭,一般在出错的时候会这样调用。\n\n### 使用 ndjson 来格式化数据\n\n[ndjson](https://www.npmjs.com/package/ndjson) 是一个比较方便的 JSON 序列化/反序列化库,相比于我们直接用 JSON,其好处主要体现在:\n\n* 可以同时解析多个 JSON 对象,如果是一个文件流,即其可以包含多个 `{}`,但是要求则是每一个占据一行,其按行分割并且解析。\n* 内部使用了 [split2](https://www.npmjs.com/package/split2),好处就是其返回时可以保证该行的所有内容已经接受完毕,从而防止 ndjson 在序列化的时候出错。\n\n关于 ndjson 的基本使用,可以根据上述链接查找文档,这里一般情况下,我们的使用方式如下(以下是一个 demo):\n\n```javascript\n//server.js:\nconst net = require('net');\n\nvar connections = [];\n\nconst server = net.createServer((socket) => {\n connections.push(socket);\n socket.on('data', (data) => {\n console.log('data:', data.toString());\n socket.write('{\"good\": 1234}\\r\\n');\n socket.write('{\"good\": 4567}\\n\\n');\n })\n}).on('error', (err) => {\n throw err;\n});\n\n// grab an arbitrary unused port.\nserver.listen(8024, () => {\n console.log('opened server on', server.address());\n});\n\n//client.js:\nconst Reconnect = require('reconnect-core');\nconst net = require('net');\nconst Ndjson = require('ndjson');\nvar Stream = require('stream');\n\nconst Connect = Reconnect(function() {\n var args = [].slice.call(arguments);\n return net.connect.apply(null, args)\n});\n\nlet connection = Connect(function(socket) {\n socket.write('world!\\r\\n');\n var parser = Ndjson.parse();\n var stringifier = Ndjson.stringify();\n\n function yourhandler(){\n var messager = new Stream.Duplex({ objectMode: true });\n messager._read = function () {\n // console.log('data:', data);\n };\n messager._write = function (data, enc, callback) {\n console.log(typeof data, data);\n // your handler\n return callback()\n };\n return messager\n }\n socket // 链接句柄\n .pipe(parser)\n .pipe(yourhandler())\n .pipe(stringifier)\n .pipe(socket);\n\n socket.on('close', (msg) => {\n console.log('close', msg).toString();\n connection.disconnect();\n });\n socket.on('end', (msg) => {\n console.log('end', msg);\n });\n});\nconnection.connect({\n port: 8024\n});\nconnection.on('reconnect', function () {\n console.log('on reconnect...')\n});\nconnection.on('error', function (err) {\n console.log('error:', err);\n});\nconnection.on('disconnect', function (err) {\n console.log('disconnect:', err);\n});\n```\n\n其中,用户具体的逻辑代码,可以是 `yourhandler` 函数 `_write` 里面的一部分,其接收的是一个一个处理好的对象。\n\n","tags":["TCP"]},{"title":"多组件单页列表应用的代码组织实践","url":"/sheldon_blog/passages/多组件单页列表应用的代码组织实践/","content":"\n本文主要对多组件单页面列表应用的代码组织实践进行总结,从而给相关应用的 Web 开发提供参考。\n\n### 什么是多组件单页面列表应用?\n\n目前,其实多组件单页面列表应用非常常见,也是我们日常生活中使用非常高频的一个类别的应用,最典型的比如新闻信息流产品腾讯新闻、今日头条等这类新闻应用,在这类新闻应用中,往往图片、图文、视频、问答、投票等多种模块混杂排列。再简单一点的话,知乎、豆瓣甚至一些论坛以及一些购物软件,也可以归为此类应用。\n\n由于笔者在负责QQ看点搜索模块的相关内容, 因此,这里给出一个QQ看点搜索的展示图:\n\n![](/img/kd.jpg)\n\n这类应用其实有如下特点:\n\n* 属于长列表滚动,内容随着滚动不断加载,一般在用户返回之前可能积累了大量的内容,因此可能会造成一定的性能问题。\n* 模块众多,并且模块的种类和样式更新迭代快,这给我们在复用组件的选择上带来了挑战,如果我们盲目复用组件,则会造成胶水代码越来越多,如果不复用组件,那么代码量会随着业务发展线性增长,这都给我们后续的维护带来了挑战。\n\n当然,一般的基于 Web 的应用(实际上,QQ看点搜索并不完全是纯粹的 WebView 应用)所面临的问题这里也都会遇到。不过,上述两类问题应该算是这类应用的比较重要的问题,其实归根到底,前者是性能问题(面向用户),后者是维护问题(面向开发者)。\n\n如何解决这里的性能问题,其实已经有很多常规的方案可以借鉴了,这并不是本篇文章的重点,除了传统 Web 用到的性能优化方法,这里仅仅列举一些常规的做法:\n\n* 图片等资源的懒加载。\n* 列表虚拟滚动,即使用有限的元素,优化CSS写法等。\n* 使用跨端融合方案渲染,例如 Weex、ReactNative、Hippy 等。\n\n### 多组件单页面应用的维护困境\n\n对于这类多组件单页面应用,一般都是增量发展的,即最开始只有很个别的几个模块,随着业务越来越复杂,模块越来越多,逻辑也越来越复杂。\n\n我们一开始,肯定可以想到一个模块(即上文中灰色分割线分割的一块)是一个组件,不同组件之间抽离出公共的函数,或者采用 mixin 将公共的部分抽离,至于数据端,由于这类应用通常在深度上不复杂,直接采用 React 或者 Vue 提供的父子组件通信的方式一般就够用了。这样设计既满足组件化的思路,也能够方便的维护项目,比较适合项目的初期。\n\n但是随着项目发展,我们会发现,问题慢慢地产生了:\n\n* 单元组件非常不好界定,比如一个左图右文的图文混排组件(例如刘亦菲的热点),之后又会增加左视频右文字,和图文展示的区别不大但是加了视频的播放时长,之后又加了左视频集合右文字(例如双世宠妃第一部分),如果我们把这多类内容当作一个组件,我们的组件中就会有非常多的判断代码,那么就会有大量的代码冗余,或者设计复杂的 mixin 和工具函数。\n* 除了我们自身的问题,往往随着内容增多,后端返回的数据内容也会非常的不一致,在相似甚至相同的组件中,数据格式也不尽相同,我们需要在我们的单元组件中,来解析判断多种数据格式。\n* 第三点就是样式更新隐患,当我们的组件多了之后,如果我们对我们的组件进行更新,那么很可能需要同时更新多处(嗯,全局替换也许是个不错的主意),这也是相当有风险的,也许会无意间改动我们并不想改动的 UI。\n\n如果我们等项目复杂后面对这个问题,我们会发现改动前期的代码工作量比较巨大,但是这又是我们不得不做的事情,这类问题的产生,实际上主要原因是我们的组件设计规划的不合理,我们完全可以在最初的项目中,通过一定的设计规划,来规避这些问题。\n\n### 多组件单页面应用的组件规划\n\n既然,我们现在希望设计一套比较好的组件规划,我们就需要重新审视我们的项目,对于我们的项目而言,一个业务模块一个组件的方式,的确简单方便,但是这样粗放的组件划分原则,实际上并不能完全满足我们复杂的维护需求,反而会给我们带来困扰。\n\n经过一系列的重构和整理,目前QQ看点搜索的组件规划逻辑是这样的:\n\n![](/img/kds.jpg)\n\n这里为了方便理解,我们采取上面样例图片中比较常见的一类业务:图文混排条目(左图右文和右图左文)来进行举例,如何设计组件来让提高我们项目的可维护性。\n\n这里首先是零件层,零件层应该有如下内容:\n\n* 图片零件,定宽,定高,自带懒加载,正常情况下只需传入一个 URL 即可使用。\n* 标题组件,一行标题和两行标题可以设计成两个组件,但进行 CSS 层面的复用。\n* 描述内容组件,例如双世宠妃的两行剧集描述。\n* 元信息内容组件,例如普通图文的来源和发表时间。\n* 时长组件,视频图文中用到。\n* 带有描述性的图片组件,视频图文中用到。\n* 图标组件:可以承载图标。\n\n以上各个组件的内容,几乎都足够简单,只需传入一个 props 作为内容,一般情况下,组件中不能出现 if 或 switch 等逻辑。\n\n接下来是组合器部分,组合器也是零件,只不过是零件的组合,其实也可以设计的比较薄弱,从而将更多的功能在布局器中完成,但是个别的时候,有这一层会给我们带来一定的方便,这里比如:\n\n* 图标+文字的组合器标题。\n\n对于零件层和组合层,一般情况下都不需要有影响外部的 margin 和 padding,即如果不增加任何多余样式罗列零件层和组合层,其上下左右四边应该是互相贴合的。\n\n接下来是布局层,这里的布局层,其实可以进行多种方式的设计,根据设计不同其数目也不同,这里给出一种设计方式:\n\n* 第一种是左图右文形式,右边可以选择普通图片、普通图片+时长组件、普通图片+描述。右边可以在一行标题、两行标题、描述零件、元信息零件中任意选择和组合。\n* 第二种是右图左文形式,左边的可配置内容和上文右边相同。\n\n当然,这两种整合成一种也无妨。\n\n在布局层,是拥有事件能力的,但是其主要应该是绑定响应时间并且调用通过 props 传入的回调函数,其不应该自己执行事件的响应逻辑。\n\n最后是控制器层,**在控制器层,除了包裹标签之外,不应该出现任何 html 标签,其也不应当引用除了布局层组件以外的更深层次的组件。控制层的主要作用是进行数据处理。**\n\n控制层的分类方式和上述几层稍有不同,这里,我们就不是按照 UI 来分不同的控制器了,而是按照数据或者业务来分类,因为这里我们主要是进行数据逻辑的处理,和 UI 的关系不是那么重要了(已经将 UI 的压力进行了下沉)。\n\n通过上述的做法,之后如果有新的需求增加进来,我们根据需要,在不同层级的组件增加内容就好了。\n\n### 总结\n\n通过以上的逻辑,我们把组件划分的更加清晰明确,将 UI 展示和数据逻辑分离,并且方便我们对样式进行迭代升级。\n\n当然,这个时候你也许还会问,如果我对部分组件样式进行升级改造,怎么样防止对原有的样式无影响呢?暂时还没有好的办法,不过,我们正在做的 UI 自动化测试套件——mangosteen,可以完美解决这个问题,敬请期待。","tags":["组件化"]},{"title":"使用 Node.js 打造多用户实时监控系统","url":"/sheldon_blog/passages/使用 Node.js 打造多用户实时监控系统/","content":"\n### 背景概述\n\n首先描述一下笔者遇到的问题,我们可以设定这样一个场景:现在有一个实时监控系统的开发需求,要求同时支持多个用户(这里我们为了简化,暂时不涉及登陆态,假定一个设备即为一个用户),对于不同的用户来讲,他们需要监控的一部分内容是完全相同的,比如设备的 CPU 信息、内存信息等,而另外一部分内容是部分用户重叠的,比如对某一区域的用户来说某些监控信息是相同的,而还有一些信息,则是用户之间完全不同的。\n\n对于每个用户来讲,当其进入页面之后即表明其开始监控,需要持续地进行数据更新,而当其退出界面或者手动点击停止监控,则停止监控。\n\n### 问题描述\n\n实际上,对于以上情况,我们很容易想到通过 WebSocket,对不同的用户进行隔离处理,当一个用户开始监控的时候,通过函数来逐个启动其所有的监控项目,当其停止监控的时候,取消相关监控,并且清除无关变量等。我们可以将所有内容写到 WebSocket 的连接回调中,由于作用域隔离,不同用户之间的监控(读操作)不会产生互相影响。\n\n这种方式可以说是最为快捷方便的方式了,并且几乎无需进行设计,但是这样有一个非常明显的效率问题:\n\n由于不同用户的部分监控项目是有重叠的,对于这些重叠的项目,我们如果对于每一个用户都单独监控,那么就会产生非常多的浪费,如果这些监控中还涉及到数据库交互或者较为复杂的计算,那么成倍之后的性能损失是非常难以承受的。\n\n所以,我们需要将不同用户重叠的那些监控项目,进行合并,合并成一个之后,如果有新的消息,我们就推到所有相关用户的回调函数中去处理。\n\n也就是说,我们需要管理一个一对多的订阅发布模式。\n\n到这里,我们发现我们想要实现这样一个监控系统,并不是非常简单,主要有下列问题:\n\n* [1]对于可能有用户重叠的监控项目,我们需要抽离到用户作用域之外,并且通过统计计数等方式来\"记住\"当前所有的监控用户,当有新内容时推到各个用户的处理函数中,并且当最后一个用户取消监控的时候要及时清理相关对象。\n* [2]不同用户的重叠监控项目的监控方式也各不相同,有的是通过 `setInterval` 等方式的定时任务,有的是事件监听器等等。\n* [3]判断不同用户的项目是否重叠也有一定的争议,比如假设不同用户端监控的是同一个项目,调用的也是相同的函数,但是由于用户 ID 不同,这个时候我们如何判断是否算\"同一个监控\"?\n\n以上的这些问题,如果我们不借助现有的库和工具,自己顺着思路一点点去写,则很容易陷入修修补补的循环,无法专注监控本身,并且最后甚至在效率上适得其反。\n\n### 解决方案\n\n以下解决方案基于 Rx.js,需要对 [Observable](https://cn.rx.js.org/class/es6/Observable.js~Observable.html) 有一定了解。\n\n#### 多个用户的监控以及取消\n\n[Monitor-RX](https://github.com/aircloud/monitor-rx) 是对以上场景问题的一个解决方案封装,其利用了 Rx.js 对订阅发布的管理能力,可以让整个流程变的清晰。\n\n在 Rx.js 中,我们可以通过以下方式建立一个多播对象 `multicasted`:\n\n```\nvar source = Rx.from([1, 2, 3]);\nvar subject = new Rx.Subject();\nvar multicasted = source.pipe(multicast(subject)).refCount();\n// 其属于 monitor-rx 的实现细节,无需理解亦可使用 monitor-rx\n\nsubscription1 = refCounted.subscribe({\n next: (v) => console.log('observerA: ' + JSON.stringify(v))\n});\n\nsetTimeout(() => {\n subscription2 = refCounted.subscribe({\n next: (v) => console.log('observerB: ' + JSON.stringify(v))\n });\n}, 1200);\n\nsubscription1.unsubscribe();\nsetTimeout(() => {\n subscription2.unsubscribe();\n // 这里 refCounted 的 unsubscribe 相关清理逻辑会自动被调用\n}, 3200);\n```\n\n在这里采用多播,有如下几个好处:\n\n* 可以随时增加新的订阅者,并且新的订阅者只会收到其加入订阅之后的数据。\n* 可以随时对任意一个订阅者取消订阅。\n* 当所有订阅者取消订阅之后,Observable 会自动触发 Observable 函数,从而可以对其事件循环等进行清理。\n\n以上能力其实可以帮助我们解决上文提到的问题 [1]。\n\n#### 监控格式的统一\n\n实际上,在我们的监控系统中,从数据依赖的角度,我们的监控函数会有这样几类:\n\n* [a]纯粹的定时任务,无数据依赖,这方面比如当前内存快照数据等。\n* [b]带有记忆依赖的定时任务:定时任务依赖前一次的数据(甚至更多次),需要两次数据做差等,这方面的数据比如一段时间的消耗数据,cpu 使用率的计算。\n* [c]带有用户依赖的定时任务:依赖用户 id 等信息,不同用户无法共用。\n\n而从任务触发的角度,我们仍待可以对其分类:\n\n* [i]简单的 `setInterval` 定时任务。\n* [ii]基于事件机制的不定时任务。\n* [iii]基于其他触发机制的任务。\n\n实际上,我们如果采用 Rx.js 的模式进行编写,无需考虑任务的数据依赖和触发的方式,只需写成一个一个 Observable 实例即可。另外,对于比较简单的 [a]&[i] 或 [c]&[i] 类型,我们还可以通过 monitor-rx 提供的 `convertToRx` 或 `convertToSimpleRx` 转换成 Observable 实例生成函数,例如:\n\n```\nvar os = require('os');\nvar process = require('process');\nconst monitorRx = require('monitor-rx');\n\nfunction getMemoryInfo() {\n return process.memoryUsage();\n}\n\nconst memory = monitorRx.Utils.convertToSimpleRx(getMemoryInfo)\n\n// 或者\n//const memory = monitorRx.Utils.convertToRx({\n// getMemoryInfo\n//});\n\nmodule.exports = memory;\n```\n\nconvertToRx 相比于 convertToSimpleRx,可以支持函数配置注入(即下文中 opts 的 func 属性和 args 属性),可以在具体生成 Observable 实例的时候具体指定使用哪些函数以及其参数。\n\n如果是比较复杂的 Observable 类型,那么我们就无法直接通过普通函数进行转化了,这个时候我们遵循 Observable 的标准返回 Observable 生成函数即可(不是直接返回 Observable 实例) \n\n这实际上也对问题 [2] 进行了解决。\n\n#### 监控唯一性:\n\n我们知道,如果两个用户都监控同一个信息,我们可以共用一个 Observable,这里的问题,就是如何定义两个用户的监控是\"相同\"的。\n\n这里我们采用一个可选项 opts 的概念,其一共有如下属性:\n\n```\n{\n module: 'ModuleName',\n func: ['FuncName'],\n args: [['arg1','arg2']],\n opts: {interval:1000}, \n}\n```\n\nmodule 即用户是对哪一个模块进行监控(实际上是 Observable),func 和 args 则是监控过程中需要调用的函数,我们也可以通过 agrs 传入用户个人信息。于没有内部子函数调用的监控,二者为空即可,opts 是一些其他可选项,比如定义请求间隔等。\n\n之后,我们通过 `JSON.stringify(opts)` 来序列化这个可选项配置,如果两个用户序列化后的可选项配置相同,那么我们就认为这两个用户可以共用一个监控,即共用一个 Observable。\n\n### 更多内容\n\n实际上,借助 Monitor-RX,我们可以很方便的解决上述提出的问题,Monitor-RX 也在积极的更新中,大家可以在[这里](https://github.com/aircloud/monitor-rx)了解到更多的信息。","tags":["Rx.js"]},{"title":"从源码分析sentry的错误信息收集","url":"/sheldon_blog/passages/从源码分析sentry的错误信息收集/","content":"\nraven.js 是 sentry 为 JavaScript 错误上报提供的 JS-SDK,本篇我们基于其源代码对其原理进行分析,本篇文章只分析前端部分,对应的文件目录是`https://github.com/getsentry/sentry-javascript/tree/master/packages/raven-js`。\n\n首先抛出几个问题:\n\n* **raven.js 是如何收集浏览器错误信息的?**\n* **raven.js 上报的错误信息格式是什么样的?又是如何把这些信息传给后端?支不支持合并上报?**\n* **面包屑(breadcrumbs)是什么?raven.js 如何来收集面包屑信息?**\n* **raven.js 如何和框架配合使用(比如 vue、react)?**\n\n在回答以上这几个问题之前,我们首先来对 raven.js 做一个宏观的分析,主要涉及其文件目录、所引用的第三方框架等。\n\nraven.js 的核心文件内容并不多,其中使用了三个第三方库,放在了 vendor 文件夹下:\n\n* [json-stringify-safe](https://github.com/moll/json-stringify-safe) :一个对 `JSON.stringify` 的封装,安全的 json 序列化操作函数,不会抛出循环引用的错误。\n\t* 这里面有一个注意点要单独说一下,我们熟知的 `JSON.stringify` , 可以接受三个参数:第一个参数是我们要序列化的对象;第二个参数是对其中键值对的处理函数;第三个参数是控制缩进空格。reven.js 的 `json-stringify-safe` 就是充分利用了这三个参数。\n* [md5](https://github.com/blueimp/JavaScript-MD5):js 的 md5 函数。\n* [TraceKit](https://github.com/csnover/TraceKit):TraceKit 是一个已经比较完善的错误收集、堆栈格式化的库,reven.js 的功能在很大程度上对它有所依赖。\n\n除此之外,raven.js 支持插件,官方提供的一些知名库的 sentry 插件主要放在了 plugin 文件夹下面,raven.js 的一些核心文件,则放在了 src 文件夹下面。\n\n### raven.js 是如何收集错误信息的?\n\n我们知道,在前端收集错误,肯定离不开 `window.onerror` 这个函数,那么我们就从这个函数说起。\n\n实际上,这部分工作是 raven.js 引用的第三方库 TraceKit 完成的:\n\n```\nfunction installGlobalHandler() {\n if (_onErrorHandlerInstalled) { // 一个起到标志作用的全局变量\n return;\n }\n _oldOnerrorHandler = _window.onerror; \n // _oldOnerrorHandler 是防止对用户其他地方定义的回调函数进行覆盖\n // 该 _window 经过兼容,实际上就是 window\n _window.onerror = traceKitWindowOnError;\n _onErrorHandlerInstalled = true;\n}\n```\n\n相关错误回调函数交给 traceKitWindowOnError 处理,下面我们来看一下 traceKitWindowOnError 函数,为了避免太多冗余代码,我们仅分析一种主要情况:\n\n```\nfunction traceKitWindowOnError(msg, url, lineNo, colNo, ex) {\n\t\n\tvar exception = utils.isErrorEvent(ex) ? ex.error : ex;\n\t//...\n stack = TraceKit.computeStackTrace(exception);\n notifyHandlers(stack, true);\n //...\n \n //...\n if (_oldOnerrorHandler) {\n return _oldOnerrorHandler.apply(this, arguments);\n }\n return false;\n}\n```\n\n其中调用的最重要的一个函数,就是 computeStackTrace,而这个函数也是 TraceKit 的核心函数,简单来讲,它做的事情就是统一格式化报错信息调用栈,因为对于各个浏览器来说,返回的 Error 调用栈信息格式不尽相同,另外甚至还有的浏览器并不返回调用栈,computeStackTrace 函数对这些情况都做了兼容性处理,并且对于一些不返回调用栈的情况,还使用了 caller 来向上回溯函数的调用栈,最终把报错信息转化成一个键相同的对象数组,做到了报错信息格式的统一。\n\nnotifyHandlers 函数则是通知相关的回调函数。 实际上,raven.js 在 install 函数中会调用 TraceKit.report.subscribe 函数,并把对错误的处理逻辑写入回调:\n\n```\nfunction subscribe(handler) {\n installGlobalHandler();\n handlers.push(handler);\n}\n```\n\n以上过程完成了错误处理过程中的负责角色转换,并且借助 TraceKit,可以使 raven.js 得到一个结构比较清晰的带有格式化好的调用栈信息的错误内容对象,之后,raven.js 对错误内容进一步处理并最终上报。\n\n下面我们对错误处理 raven.js 控制的部分做了一些梳理:\n\n```\n _handleOnErrorStackInfo: function(stackInfo, options) {\n options.mechanism = options.mechanism || {\n type: 'onerror',\n handled: false\n };\n // mechanism 和错误统计来源有关\n\n if (!this._ignoreOnError) {\n this._handleStackInfo(stackInfo, options);\n }\n},\n\n_handleStackInfo: function(stackInfo, options) {\n var frames = this._prepareFrames(stackInfo, options);\n\n this._triggerEvent('handle', {\n stackInfo: stackInfo,\n options: options\n });\n\n this._processException(\n stackInfo.name,\n stackInfo.message,\n stackInfo.url,\n stackInfo.lineno,\n frames,\n options\n );\n},\n\n_processException: function(type, message, fileurl, lineno, frames, options) {\n // 首先根据 message 信息判断是否是需要忽略的错误类型\n // 然后判断出错的文件是否在黑名单中或者白名单中\n // 接下来对错误内容进行必要的整合与转换,构造出 data 对象\n // 最后调用上报函数\n this._send(data);\n}\n\n_send: function(data) {\n\t\n\t// 对 data 进一步处理,增加必要的信息,包括后续会提到的面包屑信息\n\n\t// 交由 _sendProcessedPayload 进行进一步处理\n\tthis._sendProcessedPayload(data);\n}\n\n_sendProcessedPayload: function(data, callback) {\n\n\t// 对 data 增加一些必要的元信息\n\t// 可以通过自定义 globalOptions.transport 的方式来自定义上报函数 \n\t(globalOptions.transport || this._makeRequest).call(this, {\n\t url: url,\n\t auth: auth,\n\t data: data,\n\t options: globalOptions,\n\t onSuccess: function success() {\n\t \n\t },\n\t onError: function failure(error) {\n\t \n\t }\n\t});\n} \n\n// 真正发起请求的函数\n_makeRequest: function(opts) {\n\t// 对于支持 fetch 的浏览器,直接使用 fetch 的方式发送 POST 请求\n\t// 如果浏览器不支持 fetch,则使用 XHR 的传统方式发送 POST 请求\n}\n``` \n\n实际上我们可以发现,从拿到已经初步格式化的报错信息,到最终真正执行数据上报,raven.js 的过程非常漫长,这其中我分析有如下几个原因:\n\n* 每个函数只处理一件或者一些事情,保持函数的短小整洁。\n* 部分函数可以做到复用(因为除了自动捕获错误的方式, raven.js 还提供通过 captureException,即 `try {\n doSomething(a[0])\n} catch(e) {\n Raven.captureException(e)\n}` 的方式来上报错误,两个过程中有一些函数的调用是有重叠的)。\n\n但是笔者认为,raven.js 的代码设计还有很多值得优化的地方,比如:\n\n* 对最终上报数据(data)的属性处理和增加分散在多个函数,并且有较多可选项目,很难梳理出一个完整的 data 格式,并且不便于维护。\n* 部分函数的拆分必要性不足,并且会增加链路的复杂性,比如 `_processException `、`_sendProcessedPayload `、`_makeRequest `等都只在一个链路中被调用一次。\n* 部分属性重命名会造成资源浪费,由于 TraceKit 部分最终返回的数据格式并不完全满足 raven.js 的需要,所以 raven.js 之后又在较后阶段进行了重命名等处理,实际上这些内容完全可以通过一些其他的方式避免。\n\n最后,非常遗憾,sentry 目前完全不支持合并上报,就算是在同一个事件循环(甚至事件循环的同一个阶段,关于事件循环,可以参考我之前绘制的[一张图](https://www.processon.com/view/link/5b6ec8cbe4b053a09c2fb977))的两个错误,sentry 都是分开来上报的,这里有一个简单例子:\n\n```javascript\nRaven.config('http://8ec3f1a9f652463bb58191bd0b35f20c@localhost:9000/2').install()\nlet s = window.ss;\n\ntry{\n let b = s.b\n} catch (e) {\n Raven.captureException(e)\n // sentry should report error now\n}\n\ns.nomethod();\n// sentry should report error now\n```\n\n以上例子中,sentry 会发送两个 POST 请求。\n\n### raven.js 最终上报数据的格式\n\n\n这一部分,我们并不会详细地分析 raven.js 上报的数据的每一项内容,仅会给读者展示一个比较典型的情况。\n\n我们看一下对于一个一般的 js 错误,raven.js 上报的 json 中包含哪些内容,下面是一个已经删掉一些冗余内容的典型上报信息:\n\n```\n{\n \"project\": \"2\",\n \"logger\": \"javascript\",\n \"platform\": \"javascript\",\n \"request\": {\n \"headers\": {\n \"User-Agent\": \"Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1\"\n },\n \"url\": \"http://localhost:63342/sentry-test1/test1.html?_ijt=j54dmgn136gom08n8v8v9fdddu\"\n },\n \"exception\": {\n \"values\": [\n {\n \"type\": \"TypeError\",\n \"value\": \"Cannot read property 'b' of undefined\",\n \"stacktrace\": {\n \"frames\": [\n {\n \"filename\": \"http://localhost:63342/sentry-test1/test1.html?_ijt=j54dmgn136gom08n8v8v9fdddu\",\n \"lineno\": 19,\n \"colno\": 19,\n \"function\": \"?\",\n \"in_app\": true\n }\n ]\n }\n }\n ],\n \"mechanism\": {\n \"type\": \"generic\",\n \"handled\": true\n }\n },\n \"transaction\": \"http://localhost:63342/sentry-test1/test1.html?_ijt=j54dmgn136gom08n8v8v9fdddu\",\n \"extra\": {\n \"session:duration\": 6\n },\n \"breadcrumbs\": {\n \"values\": [\n {\n \"timestamp\": 1534257309.996,\n \"message\": \"_prepareFrames stackInfo: [object Object]\",\n \"level\": \"log\",\n \"category\": \"console\"\n },\n // ...\n ]\n },\n \"event_id\": \"ea0334adaf9d43b78e72da2b10e084a9\",\n \"trimHeadFrames\": 0\n}\n```\n\n其中支持的信息类型重点分为以下几种:\n\n* sentry 基本配置信息,包括库本身的配置和使用者的配置信息,以及用户的一些自定义信息\n* 错误信息,主要包括错误调用栈信息\n* request 信息,主要包括浏览器的 User-Agent、当前请求地址等\n* 面包屑信息,关于面包屑具体指的是什么,我们会在下一环节进行介绍\n\n### raven.js 面包屑收集\n\n面包屑信息,也就是错误在发生之前,一些用户、浏览器的行为信息,raven.js 实现了一个简单的队列(有一个最大条目长度,默认为 100),这个队列在时刻记录着这些信息,一旦错误发生并且需要上报,raven.js 就把这个队列的信息内容,作为面包屑 breadcrumbs,发回客户端。\n\n面包屑信息主要包括这几类:\n\n* 用户对某个元素的点击或者用户对某个可输入元素的输入\n* 发送的 http 请求\n* console 打印的信息(支持配置 'debug', 'info', 'warn', 'error', 'log' 等不同级别)\n* window.location 变化信息\n\n接下来,我们对这几类面包屑信息 sentry 的记录实现进行简单的分析。\n\n实际上,sentry 对这些信息记录的方式比较一致,都是通过对原声的函数进行包装,并且在包装好的函数中增加自己的钩子函数,来实现触发时候的事件记录,实际上,sentry 总共包装的函数有:\n\n* window.setTimeout\n* window.setInterval\n* window.requestAnimationFrame\n* EventTarget.addEventListener\n* EventTarget.removeEventListener\n* XMLHTTPRequest.open\n* XMLHTTPRequest.send\n* window.fetch\n* History.pushState\n* History.replaceState\n\n>备注:这里包装的所有函数,其中有一部分只是使 raven.js 具有捕获回调函数中错误的能力(对回调函数进行包装)\n\n接下来我们看一段典型的代码,来分析 raven.js 是如何记录用户的点击和输入信息的(通过对 EventTarget.addEventListener 进行封装):\n\n```javascript\nfunction wrapEventTarget(global) {\n var proto = _window[global] && _window[global].prototype;\n if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) {\n fill(\n proto,\n 'addEventListener',\n function(orig) {\n return function(evtName, fn, capture, secure) {\n try {\n if (fn && fn.handleEvent) { //兼容通过 handleEvent 的方式进行绑定事件\n fn.handleEvent = self.wrap(\n {\n mechanism: {\n type: 'instrument',\n data: {\n target: global,\n function: 'handleEvent',\n handler: (fn && fn.name) || '<anonymous>'\n }\n }\n },\n fn.handleEvent\n );\n }\n } catch (err) {\n }\n\n var before, clickHandler, keypressHandler;\n\n if (\n autoBreadcrumbs &&\n autoBreadcrumbs.dom &&\n (global === 'EventTarget' || global === 'Node')\n ) {\n // NOTE: generating multiple handlers per addEventListener invocation, should\n // revisit and verify we can just use one (almost certainly)\n clickHandler = self._breadcrumbEventHandler('click');\n keypressHandler = self._keypressEventHandler();\n before = function(evt) { // 钩子函数,用于在回调函数调用的时候记录信息\n if (!evt) return;\n\n var eventType;\n try {\n eventType = evt.type;\n } catch (e) {\n // just accessing event properties can throw an exception in some rare circumstances\n // see: https://github.com/getsentry/raven-js/issues/838\n return;\n }\n if (eventType === 'click') return clickHandler(evt);\n else if (eventType === 'keypress') return keypressHandler(evt);\n };\n }\n return orig.call(\n this,\n evtName,\n self.wrap(\n {\n mechanism: {\n type: 'instrument',\n data: {\n target: global,\n function: 'addEventListener',\n handler: (fn && fn.name) || '<anonymous>'\n }\n }\n },\n fn,\n before\n ),\n capture,\n secure\n );\n };\n },\n wrappedBuiltIns\n );\n fill(\n proto,\n 'removeEventListener',\n function(orig) {\n return function(evt, fn, capture, secure) {\n try {\n fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn);\n } catch (e) {\n // ignore, accessing __raven_wrapper__ will throw in some Selenium environments\n }\n return orig.call(this, evt, fn, capture, secure);\n };\n },\n wrappedBuiltIns\n );\n }\n }\n```\n\n以上代码兼容了通过 handleEvent 的方式进行绑定事件(如果没有听说过这种方式,可以在[这里](http://www.ayqy.net/blog/handleevent%E4%B8%8Eaddeventlistener/)补充一些相关的知识)。\n\n默认情况下,raven.js 只记录通过 `EventTarget.addEventListener` 绑定的点击和输入信息,实际上这是比较科学的,并且这些信息较为有效。另外,raven.js 也提供了记录所有点击和输入信息的可选项,其实现方式更为简单,直接在 document 上添加相关的监听即可。\n\n### raven.js 如何和框架配合使用\n\nraven.js 和框架配合使用的方式非常简单,但是我们要知道,很多框架内置了错误边界处理,或者对错误进行转义。以至于我们通过 window.onerror 的方式得不到完整的错误信息。同时,有些框架提供了错误处理的接口(比如 vue),利用错误处理的接口,我们能够获取到和错误有关的更多更重要的信息。\n\nraven.js 利用各个框架的官方接口,提供了 vue、require.js、angular、ember、react-native 等各个框架的官方插件。\n\n插件内容本身非常简单,我们可以看一下 vue 插件的代码:\n\n```\nfunction formatComponentName(vm) {\n if (vm.$root === vm) {\n return 'root instance';\n }\n var name = vm._isVue ? vm.$options.name || vm.$options._componentTag : vm.name;\n return (\n (name ? 'component <' + name + '>' : 'anonymous component') +\n (vm._isVue && vm.$options.__file ? ' at ' + vm.$options.__file : '')\n );\n}\n\nfunction vuePlugin(Raven, Vue) {\n Vue = Vue || window.Vue;\n\n // quit if Vue isn't on the page\n if (!Vue || !Vue.config) return;\n\n var _oldOnError = Vue.config.errorHandler;\n Vue.config.errorHandler = function VueErrorHandler(error, vm, info) {\n var metaData = {};\n\n // vm and lifecycleHook are not always available\n if (Object.prototype.toString.call(vm) === '[object Object]') {\n metaData.componentName = formatComponentName(vm);\n metaData.propsData = vm.$options.propsData;\n }\n\n if (typeof info !== 'undefined') {\n metaData.lifecycleHook = info;\n }\n\n Raven.captureException(error, {\n extra: metaData\n });\n\n if (typeof _oldOnError === 'function') {\n _oldOnError.call(this, error, vm, info);\n }\n };\n}\n\nmodule.exports = vuePlugin;\n```\n\n应该不用进行过多解释。\n\n你也许想知道为什么没有提供 react 插件,事实上,react 16 以后才引入了[Error Boundaries](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html),这种方式由于灵活性太强,并不太适合使用插件,另外,就算不使用插件,也非常方便地使用 raven.js 进行错误上报,可以参考[这里](https://docs.sentry.io/clients/javascript/integrations/react/)\n\n>但笔者认为,目前 react 的引入方式会对源代码进行侵入,并且比较难通过构建的方式进行 sentry 的配置,也许我们可以寻找更好的方式。\n\n完。\n\n","tags":["前端监控"]},{"title":"一篇关于react历史的流水账","url":"/sheldon_blog/passages/一篇关于react历史的流水账/","content":"\nreact 目前已经更新到 V16.3,其一路走来,日臻完善,笔者接触 react 两年有余,在这里做一个阶段性的整理,也对 react 的发展和我对 react 的学习做一个整体记录。\n\n笔者是在 16 年初开始关注 react,而实际上那个时候 react 已经发布快三年了, 16 年初的我写页面还是主要使用 backbone.js、Jquery,并且认为,相比于纯粹使用 Jquery 的“刀耕火种”的时代,使用 backbone.js 已经足够方便并且不需要替代品了。\n\n这篇文章会从 react 开源之初进行讲起,直到 2018 年六月。\n\n### 为什么是 react\n\n我们知道,react 并不是一个 MVC 框架,也并没有使用传统的前端模版,而是采用了纯 JS 编写(实际上用到了 JSX ),使用了虚拟 DOM,使用 diff 来保证 DOM 的更新效率,并且可以结合 facebook 的 Flux 架构,解决传统 MVC 模式的一些痛点。\n\n在 react 开源之初,相关生态体系并不完善,甚至官方还在用`Backbone.Router`加 react 来开发单页面应用。\n\n但是那个时候的 react,和现在的 react,解决的核心问题都没有变化,那就是**复杂的UI渲染问题( complex UI rendering )**,所有的它的组件化,虚拟 DOM 和 diff 算法,甚至目前提出的 Fiber、async rendering等等,都是围绕这个中心。\n\n### FLUX\n\n在 2014 年五月左右,也就是距离 react 开源接近一年时间,react 公开了 FLUX 架构。当然,我们现在在学习的过程中,甚至都很难听到 FLUX 这个词汇了,更多的则是 redux 甚至 dva 等更上层的框架,但是目前绝大多数 react 相关的数据管理框架都受到了 FLUX 很大启发。\n\nFLUX 和双向数据绑定的关系,我认为这里有必要援引当初官方写的一点解释(更详细的一些信息,可以看[这篇文章](https://www.10000h.top/react_flux.pdf)):\n\n```\nTo summarize, Flux works well for us because the single directional data flow makes it easy to understand and modify an application as it becomes more complicated. We found that two-way data bindings lead to cascading updates, where changing one data model led to another data model updating, making it very difficult to predict what would change as the result of a single user interaction.\n\n总而言之,Flux对我们来说效果很好,因为单向数据流可以让应用程序变得更加复杂,从而轻松理解和修改应用程序。我们发现双向数据绑定导致级联更新,其中更改一个数据模型导致另一个数据模型更新,使得很难预测单个用户交互的结果会发生什么变化。\n```\n\n从此之后,下面这张图便多次出现在官方博客和各个网站中,相信我们也肯定见过下图:\n\n![](https://www.10000h.top/images/flux.png)\n\n### react-router\n\n2014年8月,react-router 的雏形发布,在其发布之前,不少示例应用还在使用 backbone\n.js 的 router,而 react-router 的发布,标志着 react 生态的进一步成熟。\n\n### react ES6 Class\n\n实际上,在 2015.01.27 之前,我们都是在使用 `React.createClass`来书写组件。\n\n而在 2015.01.27 这一天,也就是第一届 `reactjs conf` 的前一天,react 官方发布了 React V0.13.0 beta 版本。这一个版本的最大更新就是支持 ES6 的 Class 写法来书写组件,同时也公布了比如 propTypes 类型检查、defaultProps、AutoBind、ref 等一系列相关工作在 ES6 Class 模式下的写法。\n\n这次发布是 react 开源至此最为重大的一次更新,也因此直接将 react 的写法进行了革新,在我看来,这标志着 react 从刀耕火种的原始时代进入了石器时代。\n\n*实际上,直到一个半月后的 03.10 ,V0.13 的正式版本才发布。*\n\n而在之后的 V15.5 版本(2017年4月发布),react 才将`React.createClass`的使用设置为 Deprecation,并且宣布会在将来移除该 API,与此同时,react 团队仍然提供了一个单独的库`create-react-class` 来支持原来的 `React.createClass` 功能。\n\n### Relay & GraphQL\n\n在 2015 年的 2月,Facebook 公布了 GraphQL,GraphQL 是一种新的数据查询解决方案,事实证明,它是非常优秀的一个解决方案,到现在已经基本在行业内人尽皆知。\n\n而 Relay 则是链接 react 和 GraphQL 的一个解决方案,有点类似 redux(但是 stat 数只有 redux 的四分之一左右),但是对 GraphQL 更为友好,并且在缓存机制的设计(按照 Graph 来 cache)、声明式的数据获取等方面,有一些自己的独到之处。\n\n当然,我们使用 redux 配合相关插件,也可以不使用 Relay。\n\n\n### React Native\n\n在第一届 React.js Conf 中,react 团队首次公开了 React Native,并且在3月份真正开源了 React Native(实际上这个时候安卓版本还并不可用),之后在2015年上半年,相关团队陆陆续续披露了关于 React Native 发展情况的更多信息。\n\n并且也是在这个时候(2015年3月),react 团队开始使用 **learn once, write anywhere** 这个如今我们耳熟能详的口号。\n\n### react & react-dom & babel\n\n在2015年七月,官方发布了React v0.14 Beta 1,这也是一个变动比较大的版本,在这个版本中,主要有如下比较大的变化:\n\n* 官方宣布废弃 react-tools 和 JSTransform,这是和 JSX 解析相关的库,而从此 react 开始使用 babel,我认为这对 react 以及其使用者来说无疑是一个利好。\n* 分离 react 和 react-dom,由于 React Native 已经迭代了一段时间,这个分离同时也意味着 react 之后的发展方向,react 本身将会关注抽象层和组件本身,而 react-dom 可以将其在浏览器中落地,React Native 可以将其在客户端中落地,之后也许还会有 react-xxx ...\n\n将 react 和 react-dom 分离之后,react 团队又对 react-dom 在 dom 方面做了较为大量的更新。\n\n### Discontinuing IE 8 Support\n\n在 react V15 的版本中,放弃了对 IE 8 的支持。\n\n\n### Fiber\n\nreact 团队使用 Fiber 架构完成了 react V16 的开发,得益于 Fiber 架构,react 的性能又得到了显著提升(尤其是在某些要求交互连续的场景下),并且包大小缩小了 32%。\n\n到目前来说,关于 Fiber 架构的中英文资料都已经相当丰富,笔者在这里就不进行过多的赘述了。\n\n### 接下来的展望\n\nreact 团队目前的主要工作集中在 async rendering 方面,这方面的改进可以极大提升用户交互体验(特别是在弱网络环境下),会在 2018 年发布。\n\n如果你对这方面的内容很感兴趣,不妨看看 react 之前的[演讲视频](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html)\n\n### 附录1 一些你可能不知道的变化\n\n* react并非直接将 JSX 渲染成 DOM,而是对某些事件和属性做了封装(优化)。 react 对表单类型的 DOM 进行了优化,比如封装了较为通用的 onChange 回调函数,这其中需要处理不少问题,react 在 V0.4 即拥有了这一特性,可以参考[这里](https://reactjs.org/blog/2013/07/23/community-roundup-5.html#cross-browser-onchange)\n* 事实上,react 在V0.8之前,一直在以“react-tools”这个名字发布,而 npm 上面叫做 react 的实际上是另外一个包,而到 V0.8 的时候,react 团队和原来的 “react” 包开发者协商,之后 react 便接管了原来的这个包,也因此,react并没有 V0.6 和 V0.7,而是从 V0.5 直接到了 V0.8\n* react 从 V0.14 之后,就直接跳跃到了 V15,官方团队给出的理由是,react 很早就已经足够稳定并且可以使用在生产版本中,更改版本的表达方式更有助于表示 react 项目本身的稳定性。\n\n### 附录2 一些比较优秀的博客\n\n* 关于React Components, Elements, 和 Instances,如果你还有一些疑问,可以看一看React官方团队的文章:[React Components, Elements, and Instances](https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html)\n* 如果你倾向于使用 mixins,不妨看看 react 关于取消 mixin的说法:[Mixins Considered Harmful](https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html)\n* react props 相关的开发模式的建议,我认为目前在使用 react 的程序员都应该了解一下[You Probably Don't Need Derived State](https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html)","tags":["react"]},{"title":"十条编写优化的 JavaScript 代码的建议","url":"/sheldon_blog/passages/十条编写优化的JavaScript代码的建议/","content":"\n本文总结了十条编写优秀的 JavaScript 代码的习惯,主要针对 V8 引擎:\n\n1.始终以相同的顺序实例化对象属性,以便可以共享隐藏类和随后优化的代码。V8 在对 js 代码解析的时候会有构建隐藏类的过程,以相同的顺序实例化(属性赋值)的对象会共享相同的隐藏类。下面给出一个不好的实践:\n\n```javascript\nfunction Point(x, y) {\n this.x = x;\n this.y = y;\n}\nvar p1 = new Point(1, 2);\np1.a = 5;\np1.b = 6;\nvar p2 = new Point(3, 4);\np2.b = 7;\np2.a = 8;\n// 由于 a 和 b 的赋值顺序不同,p1 和 p2 无法共享隐藏类\n```\n\n2.避免分配动态属性。在实例化之后向对象添加属性将强制隐藏类更改,并减慢为先前隐藏类优化的所有方法。相反,在其构造函数中分配所有对象的属性。 \n\n3.重复执行相同方法的代码将比仅执行一次(由于内联缓存)执行许多不同方法的代码运行得更快。 \n\n4.避免创建稀疏数组。稀疏数组由于不是所有的元素都存在,因此是一个哈希表,因此访问稀疏数组中的元素代价更高。另外,尽量不要采用预分配数量的大数组,更好的办法是随着你的需要把它的容量增大。最后,尽量不要删除数组中的元素,它会让数组变得稀疏。 \n\n5.标记值:V8采用32位来表示对象和数字,其中用一位来区别对象(flag = 0)或数字(flag = 1),因此这被称之为 SMI (Small Integer)因为它只有31位。因此,如果一个数字大于31位,V8需要对其进行包装,将其变成双精度并且用一个对象来封装它,因此应该尽量使用31位有符号数字从而避免昂贵的封装操作。 \n\n6.检查你的依赖,去掉不需要 import 的内容。 \n\n7.将你的代码分割成一些小的 chunks ,而不是整个引入。 \n \n8.尽可能使用 defer 来推迟加载 JavaScript,另外只加载当前路由需要的代码段。\n \n9.使用 dev tools 和 DeviceTiming 来寻找代码瓶颈。 \n\n10.使用诸如Optimize.js这样的工具来帮助解析器决定何时需要提前解析以及何时需要延后解析。 \n \n以上内容来源:\n* [How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time](https://blog.sessionstack.com/how-javascript-works-parsing-abstract-syntax-trees-asts-5-tips-on-how-to-minimize-parse-time-abfcf7e8a0c8)\n* [How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code](https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e)\n\n","tags":["javascript"]},{"title":"浅谈前端中的二进制数据类型","url":"/sheldon_blog/passages/浅谈前端中的二进制数据类型/","content":"\n>目前在一个项目中,WebSocket部分由于后端使用了gzip压缩,前端处理起来废了一点时间,从而发现自己在二进制数据类型这个知识点还存在一定的盲区,因此这里进行总结。\n\n本文主要简单介绍ArrayBuffer对象、TypedArray对象、DataView对象以及Blob原始数据类型,和它们之间的互相转换方法。部分代码参考[这里](http://javascript.ruanyifeng.com/stdlib/arraybuffer.html#toc4)而非本人原创,仅做个人学习使用。\n\n这些类型化对象,一般会在以下场景中使用:\n\n* WebGL 中,浏览器和显卡之间需要使用二进制数据进行通信。\n* 在一些 Rest 接口或者 WebSocket 中,采用压缩过的数据进行通信,这个压缩和解压缩的过程可能需要借助二进制对象。\n* 在 Canvas 中,我们可能需要通过生成 Blob 的方式保存当前内容。\n* 在 Img 等资源文件中,URL 可以为 Blob 原始数据类型。\n* 在读取用户上传文件时,可能需要用到二进制数据类型进行中间转换。\n\n下文分两部分,前一部分概述各个二进制数据类型,后一部分将它们之间的互相转换。\n\n### 二进制数据类型概述\n\n#### ArrayBuffer\n\nArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。\n\nArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。\n\n```\nvar buf = new ArrayBuffer(32);\n```\n\n上面代码生成了一段32字节的内存区域,每个字节的值默认都是0。可以看到,ArrayBuffer构造函数的参数是所需要的内存大小(单位字节)。\n\n为了读写这段内容,需要为它指定视图。DataView视图的创建,需要提供ArrayBuffer对象实例作为参数。\n\n```\nvar buf = new ArrayBuffer(32);\nvar dataView = new DataView(buf);\ndataView.getUint8(0) // 0\n```\n\n上面代码对一段32字节的内存,建立DataView视图,然后以不带符号的8位整数格式,读取第一个元素,结果得到0,因为原始内存的ArrayBuffer对象,默认所有位都是0。\n\n另外,我们可以将ArrayBuffer生成的结果,传入TypedArray中:\n\n```\nvar buffer = new ArrayBuffer(12);\n\nvar x1 = new Int32Array(buffer);\nx1[0] = 1;\nvar x2 = new Uint8Array(buffer);\nx2[0] = 2;\n\nx1[0] // 2\n```\n\nArrayBuffer实例的byteLength属性,返回所分配的内存区域的字节长度。\n\n```\nvar buffer = new ArrayBuffer(32);\nbuffer.byteLength\n// 32\n```\n如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。\n\n```\nif (buffer.byteLength === n) {\n // 成功\n} else {\n // 失败\n}\n```\n\nArrayBuffer实例有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。\n\n```\nvar buffer = new ArrayBuffer(8);\nvar newBuffer = buffer.slice(0, 3);\n```\n\n上面代码拷贝buffer对象的前3个字节(从0开始,到第3个字节前面结束),生成一个新的ArrayBuffer对象。slice方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。\n\nslice方法接受两个参数,第一个参数表示拷贝开始的字节序号(含该字节),第二个参数表示拷贝截止的字节序号(不含该字节)。如果省略第二个参数,则默认到原ArrayBuffer对象的结尾。\n\n除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。\n\nArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。\n\n```\nvar buffer = new ArrayBuffer(8);\nArrayBuffer.isView(buffer) // false\n\nvar v = new Int32Array(buffer);\nArrayBuffer.isView(v) // true\n```\n\n#### TypedArray\n\n目前,TypedArray对象一共提供9种类型的视图,每一种视图都是一种构造函数。\n\n* Int8Array:8位有符号整数,长度1个字节。\n* Uint8Array:8位无符号整数,长度1个字节。\n* Uint8ClampedArray:8位无符号整数,长度1个字节,溢出处理不同。\n* Int16Array:16位有符号整数,长度2个字节。\n* Uint16Array:16位无符号整数,长度2个字节。\n* Int32Array:32位有符号整数,长度4个字节。\n* Uint32Array:32位无符号整数,长度4个字节。\n* Float32Array:32位浮点数,长度4个字节。\n* Float64Array:64位浮点数,长度8个字节。\n\n这9个构造函数生成的对象,统称为TypedArray对象。它们很像正常数组,都有length属性,都能用方括号运算符([])获取单个元素,所有数组的方法,在类型化数组上面都能使用。两者的差异主要在以下方面。\n\n* TypedArray数组的所有成员,都是同一种类型和格式。\n* TypedArray数组的成员是连续的,不会有空位。\n* Typed化数组成员的默认值为0。比如,new Array(10)返回一个正常数组,里面没有任何成员,只是10个空位;new Uint8Array(10)返回一个类型化数组,里面10个成员都是0。\n* TypedArray数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。\n\n##### 构造函数\n\nTypedArray数组提供9种构造函数,用来生成相应类型的数组实例。\n\n构造函数有多种用法。\n\n* TypedArray(buffer, byteOffset=0, length?)\n\n同一个ArrayBuffer对象之上,可以根据不同的数据类型,建立多个视图。\n\n```\n// 创建一个8字节的ArrayBuffer\nvar b = new ArrayBuffer(8);\n\n// 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾\nvar v1 = new Int32Array(b);\n\n// 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾\nvar v2 = new Uint8Array(b, 2);\n\n// 创建一个指向b的Int16视图,开始于字节2,长度为2\nvar v3 = new Int16Array(b, 2, 2);\n```\n\n对于以上代码,v1、v2和v3是重叠的:v1[0]是一个32位整数,指向字节0~字节3;v2[0]是一个8位无符号整数,指向字节2;v3[0]是一个16位整数,指向字节2~字节3。只要任何一个视图对内存有所修改,就会在另外两个视图上反应出来。\n\n注意,byteOffset必须与所要建立的数据类型一致,否则会报错。\n\n```\nvar buffer = new ArrayBuffer(8);\nvar i16 = new Int16Array(buffer, 1);\n// Uncaught RangeError: start offset of Int16Array should be a multiple of 2\n```\n\n上面代码中,新生成一个8个字节的ArrayBuffer对象,然后在这个对象的第一个字节,建立带符号的16位整数视图,结果报错。因为,带符号的16位整数需要两个字节,所以byteOffset参数必须能够被2整除。\n\n如果想从任意字节开始解读ArrayBuffer对象,必须使用DataView视图,因为TypedArray视图只提供9种固定的解读格式。\n\n* TypedArray(length)\n\n视图还可以不通过ArrayBuffer对象,直接分配内存而生成。\n\n```\nvar f64a = new Float64Array(8);\nf64a[0] = 10;\nf64a[1] = 20;\nf64a[2] = f64a[0] + f64a[1];\n```\n\n* TypedArray(typedArray)\n\n类型化数组的构造函数,可以接受另一个视图实例作为参数。\n\n```\nvar typedArray = new Int8Array(new Uint8Array(4));\n```\n\n上面代码中,Int8Array构造函数接受一个Uint8Array实例作为参数。\n\n注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。\n\n```\nvar x = new Int8Array([1, 1]);\nvar y = new Int8Array(x);\nx[0] // 1\ny[0] // 1\n\nx[0] = 2;\ny[0] // 1\n```\n\n上面代码中,数组y是以数组x为模板而生成的,当x变动的时候,y并没有变动。\n\n如果想基于同一段内存,构造不同的视图,可以采用下面的写法。\n\n```\nvar x = new Int8Array([1, 1]);\nvar y = new Int8Array(x.buffer);\nx[0] // 1\ny[0] // 1\n\nx[0] = 2;\ny[0] // 2\n```\n\n* TypedArray(arrayLikeObject)\n\n构造函数的参数也可以是一个普通数组,然后直接生成TypedArray实例。\n\n```\nvar typedArray = new Uint8Array([1, 2, 3, 4]);\n```\n\n注意,这时TypedArray视图会重新开辟内存,不会在原数组的内存上建立视图。\n\n上面代码从一个普通的数组,生成一个8位无符号整数的TypedArray实例。\n\nTypedArray数组也可以转换回普通数组。\n\n```\nvar normalArray = Array.prototype.slice.call(typedArray);\n```\n\n##### BYTES_PER_ELEMENT属性\n\n每一种视图的构造函数,都有一个BYTES_PER_ELEMENT属性,表示这种数据类型占据的字节数。\n\n```\nInt8Array.BYTES_PER_ELEMENT // 1\nUint8Array.BYTES_PER_ELEMENT // 1\nInt16Array.BYTES_PER_ELEMENT // 2\nUint16Array.BYTES_PER_ELEMENT // 2\nInt32Array.BYTES_PER_ELEMENT // 4\nUint32Array.BYTES_PER_ELEMENT // 4\nFloat32Array.BYTES_PER_ELEMENT // 4\nFloat64Array.BYTES_PER_ELEMENT // 8\n```\n\n##### ArrayBuffer与字符串的互相转换\n\nArrayBuffer转为字符串,或者字符串转为ArrayBuffer,有一个前提,即字符串的编码方法是确定的。假定字符串采用UTF-16编码(JavaScript的内部编码方式),可以自己编写转换函数。\n\n```\n// ArrayBuffer转为字符串,参数为ArrayBuffer对象\nfunction ab2str(buf) {\n return String.fromCharCode.apply(null, new Uint16Array(buf));\n}\n\n// 字符串转为ArrayBuffer对象,参数为字符串\nfunction str2ab(str) {\n var buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节\n var bufView = new Uint16Array(buf);\n for (var i = 0, strLen = str.length; i < strLen; i++) {\n bufView[i] = str.charCodeAt(i);\n }\n return buf;\n}\n```\n\n##### TypedArray.prototype.set()\n\nTypedArray数组的set方法用于复制数组(正常数组或TypedArray数组),也就是将一段内容完全复制到另一段内存。\n\n```\nvar a = new Uint8Array(8);\nvar b = new Uint8Array(8);\n\nb.set(a);\n```\n\n上面代码复制a数组的内容到b数组,它是整段内存的复制,比一个个拷贝成员的那种复制快得多。set方法还可以接受第二个参数,表示从b对象哪一个成员开始复制a对象。\n\n```\nvar a = new Uint16Array(8);\nvar b = new Uint16Array(10);\n\nb.set(a, 2)\n```\n上面代码的b数组比a数组多两个成员,所以从b[2]开始复制。\n\n##### TypedArray.prototype.subarray()\n\nsubarray方法是对于TypedArray数组的一部分,再建立一个新的视图。\n\n```\nvar a = new Uint16Array(8);\nvar b = a.subarray(2,3);\n\na.byteLength // 16\nb.byteLength // 2\n```\n\nsubarray方法的第一个参数是起始的成员序号,第二个参数是结束的成员序号(不含该成员),如果省略则包含剩余的全部成员。所以,上面代码的a.subarray(2,3),意味着b只包含a[2]一个成员,字节长度为2。\n\n##### TypedArray.prototype.slice()\n\nTypeArray实例的slice方法,可以返回一个指定位置的新的TypedArray实例。\n\n```\nlet ui8 = Uint8Array.of(0, 1, 2);\nui8.slice(-1)\n// Uint8Array [ 2 ]\n```\n\n\n上面代码中,ui8是8位无符号整数数组视图的一个实例。它的slice方法可以从当前视图之中,返回一个新的视图实例。\n\nslice方法的参数,表示原数组的具体位置,开始生成新数组。负值表示逆向的位置,即-1为倒数第一个位置,-2表示倒数第二个位置,以此类推。\n\n##### TypedArray.of()\n\nTypedArray数组的所有构造函数,都有一个静态方法of,用于将参数转为一个TypedArray实例。\n\n```\nFloat32Array.of(0.151, -8, 3.7)\n// Float32Array [ 0.151, -8, 3.7 ]\n```\n\n##### TypedArray.from()\n\n静态方法from接受一个**可遍历的数据结构(比如数组)**作为参数,返回一个基于这个结构的TypedArray实例。\n\n```\nUint16Array.from([0, 1, 2])\n// Uint16Array [ 0, 1, 2 ]\n```\n\n这个方法还可以将一种TypedArray实例,转为另一种。\n\n```\nvar ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));\nui16 instanceof Uint16Array // true\n```\n\nfrom方法还可以接受一个函数,作为第二个参数,用来对每个元素进行遍历,功能类似map方法。\n\n```\nInt8Array.of(127, 126, 125).map(x => 2 * x)\n// Int8Array [ -2, -4, -6 ]\n\nInt16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)\n// Int16Array [ 254, 252, 250 ]\n```\n\n上面的例子中,from方法没有发生溢出,这说明遍历是针对新生成的16位整数数组,而不是针对原来的8位整数数组。也就是说,from会将第一个参数指定的TypedArray数组,拷贝到另一段内存之中(占用内存从3字节变为6字节),然后再进行处理。\n\n#### DataView\n\n如果一段数据包括多种类型(比如服务器传来的HTTP数据),这时除了建立ArrayBuffer对象的复合视图以外,还可以通过DataView视图进行操作。\n\nDataView视图提供更多操作选项,而且支持设定字节序。本来,在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。\n\nDataView视图本身也是构造函数,接受一个ArrayBuffer对象作为参数,生成视图。\n\n```\nDataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);\n```\n下面是一个例子。\n\n```\nvar buffer = new ArrayBuffer(24);\nvar dv = new DataView(buffer);\n```\n\nDataView实例有以下属性,含义与TypedArray实例的同名方法相同。\n\n* DataView.prototype.buffer:返回对应的ArrayBuffer对象\n* DataView.prototype.byteLength:返回占据的内存字节长度\n* DataView.prototype.byteOffset:返回当前视图从对应的ArrayBuffer对象的哪个字节开始\n\nDataView实例提供8个方法读取内存。\n\n* getInt8:读取1个字节,返回一个8位整数。\n* getUint8:读取1个字节,返回一个无符号的8位整数。\n* getInt16:读取2个字节,返回一个16位整数。\n* getUint16:读取2个字节,返回一个无符号的16位整数。\n* getInt32:读取4个字节,返回一个32位整数。\n* getUint32:读取4个字节,返回一个无符号的32位整数。\n* getFloat32:读取4个字节,返回一个32位浮点数。\n* getFloat64:读取8个字节,返回一个64位浮点数。\n\n这一系列get方法的参数都是一个字节序号(不能是负数,否则会报错),表示从哪个字节开始读取。\n\n```\nvar buffer = new ArrayBuffer(24);\nvar dv = new DataView(buffer);\n\n// 从第1个字节读取一个8位无符号整数\nvar v1 = dv.getUint8(0);\n\n// 从第2个字节读取一个16位无符号整数\nvar v2 = dv.getUint16(1);\n\n// 从第4个字节读取一个16位无符号整数\nvar v3 = dv.getUint16(3);\n```\n\n上面代码读取了ArrayBuffer对象的前5个字节,其中有一个8位整数和两个十六位整数。\n\n如果一次读取两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。默认情况下,DataView的get方法使用大端字节序解读数据,如果需要使用小端字节序解读,必须在get方法的第二个参数指定true。\n\n```\n// 小端字节序\nvar v1 = dv.getUint16(1, true);\n\n// 大端字节序\nvar v2 = dv.getUint16(3, false);\n\n// 大端字节序\nvar v3 = dv.getUint16(3);\n```\n\nDataView视图提供8个方法写入内存。\n\n* setInt8:写入1个字节的8位整数。\n* setUint8:写入1个字节的8位无符号整数。\n* setInt16:写入2个字节的16位整数。\n* setUint16:写入2个字节的16位无符号整数。\n* setInt32:写入4个字节的32位整数。\n* setUint32:写入4个字节的32位无符号整数。\n* setFloat32:写入4个字节的32位浮点数。\n* setFloat64:写入8个字节的64位浮点数。\n\n这一系列set方法,接受两个参数,第一个参数是字节序号,表示从哪个字节开始写入,第二个参数为写入的数据。对于那些写入两个或两个以上字节的方法,需要指定第三个参数,false或者undefined表示使用大端字节序写入,true表示使用小端字节序写入。\n\n```\n// 在第1个字节,以大端字节序写入值为25的32位整数\ndv.setInt32(0, 25, false);\n\n// 在第5个字节,以大端字节序写入值为25的32位整数\ndv.setInt32(4, 25);\n\n// 在第9个字节,以小端字节序写入值为2.5的32位浮点数\ndv.setFloat32(8, 2.5, true);\n```\n\n如果不确定正在使用的计算机的字节序,可以采用下面的判断方式。\n\n```\nvar littleEndian = (function() {\n var buffer = new ArrayBuffer(2);\n new DataView(buffer).setInt16(0, 256, true);\n return new Int16Array(buffer)[0] === 256;\n})();\n```\n\n#### Blob\n\nBlob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。\n\n要从其他非blob对象和数据构造一个Blob,请使用 Blob() 构造函数。要创建包含另一个blob数据的子集blob,请使用 slice()方法。要获取用户文件系统上的文件对应的Blob对象,请参阅 File文档。\n\n从Blob中读取内容的唯一方法是使用 FileReader。以下代码将 Blob 的内容作为类型数组读取:\n\n```\nvar reader = new FileReader();\nreader.addEventListener(\"loadend\", function() {\n // reader.result 包含转化为类型数组的blob\n});\nreader.readAsArrayBuffer(blob);\n```\n\n更多关于Blob的内容,请直接查看[这里](https://developer.mozilla.org/zh-CN/docs/Web/API/Blob)\n\n### 数据格式转换\n\n#### String转Blob\n\n```\n//将字符串 转换成 Blob 对象\nvar blob = new Blob([\"Hello World!\"], {\n type: 'text/plain'\n});\nconsole.info(blob);\nconsole.info(blob.slice(1, 3, 'text/plain'));\n```\n#### TypeArray转Blob\n\n```\n//将 TypeArray 转换成 Blob 对象\nvar array = new Uint16Array([97, 32, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]);\n//测试成功\n//var blob = new Blob([array], { type: \"application/octet-binary\" });\n//测试成功, 注意必须[]的包裹\nvar blob = new Blob([array]);\n//将 Blob对象 读成字符串\nvar reader = new FileReader();\nreader.readAsText(blob, 'utf-8');\nreader.onload = function (e) {\n console.info(reader.result); //a Hello world!\n}\n```\n\n#### ArrayBuffer转Blob\n\n```\nvar buffer = new ArrayBuffer(32);\nvar blob = new Blob([buffer]); // 注意必须包裹[]\n```\n\n#### Blob转String\n\n这里需要注意的是readAsText方法的使用。\n\n```\n//将字符串转换成 Blob对象\nvar blob = new Blob(['中文字符串'], {\n type: 'text/plain'\n});\n//将Blob 对象转换成字符串\nvar reader = new FileReader();\nreader.readAsText(blob, 'utf-8');\nreader.onload = function (e) {\n console.info(reader.result);\n}\n```\n\n#### Blob转ArrayBuffer\n\n这里需要注意的是readAsArrayBuffer方法的使用。\n\n```\n//将字符串转换成 Blob对象\nvar blob = new Blob(['中文字符串'], {\n type: 'text/plain'\n});\n//将Blob 对象转换成 ArrayBuffer\nvar reader = new FileReader();\nreader.readAsArrayBuffer(blob);\nreader.onload = function (e) {\n console.info(reader.result); //ArrayBuffer {}\n //经常会遇到的异常 Uncaught RangeError: byte length of Int16Array should be a multiple of 2\n //var buf = new int16array(reader.result);\n //console.info(buf);\n\n //将 ArrayBufferView 转换成Blob\n var buf = new Uint8Array(reader.result);\n console.info(buf); //[228, 184, 173, 230, 150, 135, 229, 173, 151, 231, 172, 166, 228, 184, 178]\n reader.readAsText(new Blob([buf]), 'utf-8');\n reader.onload = function () {\n console.info(reader.result); //中文字符串\n };\n\n //将 ArrayBufferView 转换成Blob\n var buf = new DataView(reader.result);\n console.info(buf); //DataView {}\n reader.readAsText(new Blob([buf]), 'utf-8');\n reader.onload = function () {\n console.info(reader.result); //中文字符串\n };\n}\n```\n\n","tags":["javascript"]},{"title":"Linux服务器初始化设置用户和ssh公私钥登陆","url":"/sheldon_blog/passages/Linux服务器初始化设置用户和ssh公私钥登陆/","content":"\n>当我们开始使用一个新的服务器的时候,首先一定要对服务器的登陆等做一些修改工作,笔者曾经就因为对服务器登陆安全没有重视,导致服务器数据全部丢失。接下来我们按照步骤,罗列出应该做的一些事情。\n\n### 修改ssh端口号\n\n第一件事情:\n\n修改ssh端口号: 之后加上一个端口比如说50000\n\n`vi /etc/ssh/sshd_config`之后在port字段加上一个端口比如说50000,原来的端口号字段可能是被注释掉的,要先解除注释。\n\n然后执行:\n\n```\nservice sshd restart\n```\n\n这个时候可能还要重新配置一下防火墙,开放50000端口,具体如何配置也可以参考[这里](https://blog.csdn.net/ul646691993/article/details/52104082)的后半部分。但是目前,阿里云的服务器实测是不需要再配置防火墙的,但是需要去登陆到网页后台修改安全组。\n\n之后就可以通过这样的方式登录了:(注意登录方式一定要写对)\n\n```shell\nssh [email protected] -p 50000\n```\n\n### 创建用户\n\n这个时候我们还是用root进行操作,所以我们接下来要给自己创建一个账户,比如创建一个如下的用户:\n\n```\nuseradd xiaotao\npasswd xiaotao\n```\n\n可以用`ls -al /home/``查看一下账户\n\n对创建的这个用户增加sudo权限: 相关配置文件/etc/sudoers中,但是这个文件是只读的,所以要更改一下权限\n\n```\nchmod u+w sudoers\n```\n\n然后进入这个文件在这里进行更改:\n\n```\nroot ALL=(ALL) ALL\nxiaotao ALL=(ALL) ALL\n```\n\n然后再改回权限:\n\n```\nchmod u-w sudoers\n```\n\n注意一点,CentOS 7预设容许任何帐号透过ssh登入(也就是说自己根本不用改改,直接新建帐号登录即可),包括根和一般帐号,为了不受根帐号被黑客暴力入侵,我们必须禁止 root帐号的ssh功能,事实上root也没有必要ssh登入伺服器,因为只要使用su或sudo(当然需要输入root的密码)普通帐号便可以拥有root的权限。使用vim(或任何文本编辑器)开启的/ etc/ SSH/ sshd_config中,寻找:\n\n```\n#PermitRootLogin yes\n```\n修改:\n\n```\nPermitRootLogin no\n```\n\n### 配置公私钥加密登录\n\n**这一步骤要切换到自己新建的用户,不能再用 root 用户了,否则可能无法正常登陆。**\n\n很多时候以上所说的还是不够安全,为了更加安全方便,我们采用公私钥对称加密登录,简单的讲做法就是再客户端生成一把私钥一把公钥,私钥是在客户端的,公钥上传到服务端,对称加密进行登录。\n\n在客户端先进到这个目录:\n\n```\ncd ~/.ssh\n```\n\n生成公钥和私钥(实际上如果之前有的话就不用重新生成了)\n\n```\nssh-keygen -t rsa\n```\n\n接下来把公钥上传到服务端\n\n```\nscp ~/.ssh/id_rsa.pub xiaotao@<ssh_server_ip>:~\n```\n\n在服务端执行以下命令(如果没有相关的文件和文件夹要先进行创建,注意不要使用 sudo )\n\n```\ncat id_rsa.pub >> ~/.ssh/authorized_keys\n```\n\n配置服务器的/etc/ssh/sshd_config,下面是一些建议的配置:\n\n```\nvim /etc/ssh/sshd_config\n# 禁用root账户登录,非必要,但为了安全性,请配置\nPermitRootLogin no\n\n# 是否让 sshd 去检查用户家目录或相关档案的权限数据,\n# 这是为了担心使用者将某些重要档案的权限设错,可能会导致一些问题所致。\n# 例如使用者的 ~.ssh/ 权限设错时,某些特殊情况下会不许用户登入\nStrictModes no\n\n# 是否允许用户自行使用成对的密钥系统进行登入行为,仅针对 version 2。\n# 至于自制的公钥数据就放置于用户家目录下的 .ssh/authorized_keys 内\nRSAAuthentication yes\nPubkeyAuthentication yes\nAuthorizedKeysFile %h/.ssh/authorized_keys\n\n#有了证书登录了,就禁用密码登录吧,安全要紧\nPasswordAuthentication no\n```\n\n然后不要忘记 `sudo service sshd restart`\n\n\n一般来讲,这样就算是成功了,我们可以在客户端尝试:\n\n```\nssh -i ~/.ssh/id_rsa remote_username@remote_ip\n```\n\n如果不行,可能是服务端或客户端相关 `.ssh` 文件权限不对,可以进行如下尝试:\n\n```\n服务端\nchown -R 0700 ~/.ssh\nchown -R 0644 ~/.ssh/authorized_keys\n\n客户端改一下\nchmod 600 id_rsa\n```","tags":["ssh"]},{"title":"dva源码解读","url":"/sheldon_blog/passages/dva源码解读/","content":"\n### 声明\n\n本文章用于个人学习研究,并不代表 dva 团队的任何观点。\n\n原文以及包含一定注释的代码见[这里](https://github.com/aircloud/dva-analysis),若有问题也可以在[这里](https://github.com/aircloud/dva-analysis/issues)进行讨论\n\n### 起步\n\n#### 为什么是dva?\n\n笔者对 dva 的源代码进行解读,主要考虑到 dva 并不是一个和我们熟知的主流技术无关的从0到1的框架,相反,它是对主流技术进行整合,提炼,从而形成一种最佳实践,分析 dva,意味着我们可以对自己掌握的很多相关技术进行回顾,另外,dva 的代码量并不多,也不至于晦涩难懂,可以给我们平时的业务开发以启发。\n\n本文章作为 dva 的源码解读文章,并不面向新手用户,读者应当有一定的 react 使用经验和 ECMAscript 2015+ 的使用经验,并且应当了解 redux 和 redux-saga,以及对 dva 的使用有所了解(可以从[这里](https://github.com/dvajs/dva/blob/master/README_zh-CN.md#%E4%B8%BA%E4%BB%80%E4%B9%88%E7%94%A8-dva-)了解为什么需要使用 dva)\n\n重点推荐:\n\n* 通过[这里](https://github.com/dvajs/dva-knowledgemap)的内容了解使用dva的最小知识集\n* 通过[这里](https://redux-saga-in-chinese.js.org/docs/introduction/index.html)学习 redux-saga\n\n其他推荐:\n\n* [dva的概念](https://github.com/dvajs/dva/blob/master/docs/Concepts_zh-CN.md)\n* [dva的全部API](https://github.com/dvajs/dva/blob/master/docs/API_zh-CN.md)\n* [React+Redux 最佳实践](https://github.com/sorrycc/blog/issues/1)\n* [React在蚂蚁金服的实践](http://slides.com/sorrycc/dva#/)\n* [dva 2.0的改进](https://github.com/sorrycc/blog/issues/48)\n* [ReSelect介绍](http://cn.redux.js.org/docs/recipes/ComputingDerivedData.html)\n* [浅析Redux 的 store enhancer](https://www.jianshu.com/p/04d3fefea8d7)\n\n\n几个 dva 版本之间的关系:\n\n* [email protected]:基于 react 和 react-router@4\n* [email protected]:基于 react 和 react-router@3\n* [email protected]:无路由版本,适用于多页面场景,可以和 next.js 组合使用\n* [email protected]:仅封装了 redux 和 redux-saga\n\n我们本次主要分析目标为 [email protected] 和 [email protected]\n\n\n### 我们为什么需要 redux-saga\n\n目前,在大多数项目开发中,我们现在依然采用的是redux-thunk + async/await (或 Promise)。\n\n实际上这个十几行的插件已经完全可以解决大多是场景下的问题了,如果你在目前的工作中正在使用这一套方案并且能够完全将当下的需求应付自如并且没有什么凌乱的地方,其实也是没有必要换成redux-saga的。\n\n接下来我们讲 redux-saga,先看名字:saga,这个术语常用于CQRS架构,代表查询与责任分离。\n\n相比于 redux-thunk,前者通常是把数据查询等请求放在 actions 中(不纯净的 actions),并且这些 actions 可以继续回调调用其他 actions(纯净的 actions),从而完成数据的更新;而 redux-saga,则保持了 actions 的纯粹性,单独抽出一层专门来处理数据请求等操作(saga函数)。\n\n这样做还有另外一些好处:\n\n* 由于我们已经将数据处理数据请求等异步操作抽离出来了,并且通过 generator 来处理,我们便可以方便地进行多种异步管理:比如同时按顺序执行多个任务、在多个异步任务中启动race等。\n* 这样做可以延长任务的生命周期,我们的一次调用可以不再是一个\"调完即走\"的过程,还可以是一个LLT(Long Lived Transaction)的事物处理过程,比如我们可以将用户的登入、登出的管理放在一个saga函数中处理。\n\n当然,redux-saga还有比如拥有有诸多常用并且声明式易测的 Effects、可以无阻塞的fork等一些更复杂的异步操作和管理方法,如果应用中有较多复杂的异步操作流程,使用redux-saga无疑会让条理更加清楚。\n\n当然,本文的目的不是介绍或者安利redux-saga,只是因为redux-saga是 dva 的一个基础,相关概念点到为止,如需了解更多请自行参考资料。\n\n### dva 源码解读\n\n我们的源码分析流程是这样的:通过一个使用 dva 开发的例子,随着其对 dva 函数的逐步调用,来分析内部 dva 相关函数的实现原理。\n\n我们分析采用的例子是 dva 官方提供的一个增删改查的应用,可以在[这里](https://github.com/dvajs/dva/tree/rewrite-dynamic)找到它的源代码。\n\n我们先看该例子的入口文件:\n\n```\nimport dva from 'dva';\nimport createHistory from 'history/createBrowserHistory';\nimport createLoading from 'dva-loading';\nimport { message } from 'antd';\nimport './index.css';\n\nconst ERROR_MSG_DURATION = 3; // 3 秒\n\n// 1. Initialize\nconst app = dva({\n history: createHistory(),\n onError(e) {\n message.error(e.message, ERROR_MSG_DURATION);\n },\n});\n\n// 2. Plugins\napp.use(createLoading());\n\n// 3. Model\n// Moved to router.js\n// 这里的 Model 被转移到了动态加载的 router 里面,我们也可以如下写:\n// app.model(require('./models/users'));\n\n// 4. Router\napp.router(require('./router'));\n\n// 5. Start\napp.start('#root');\n```\n\n我们发现dva从初始化配置到最后的start(现在的dva start函数在不传入container的情况下可以返回React Component,便于服务端渲染等,但这里我们还是按照例子的写法来)。\n\n这里我们先有必要解释一下,dva 在当前依据能力和依赖版本的不同,有多个可引入的版本,我们的例子和所要分析的源代码都是基于 react-router V4 的 dva 版本。\n\n在源代码中,相关目录主要为 dva 目录(packages/dva) 和 dva-core(packages/dva-core)目录,前者主要拥有history管理、router、动态加载等功能,而后者是不依赖这些内容的基础模块部分,为前者所引用\n\n#### 第一步\n\n第一步这里传入了两个内容:(dva构造函数总共可以传入那些 opts,会在下文中进行说明)\n\n```\nconst app = dva({\n history: createHistory(),\n onError(e) {\n message.error(e.message, ERROR_MSG_DURATION);\n },\n});\n```\n\n这一步的相关核心代码如下:\n\n```\nexport default function (opts = {}) {\n const history = opts.history || createHashHistory(); // 默认为 HashHistory\n const createOpts = {\n initialReducer: {\n routing, // 来自 react-router-redux 的 routerReducer\n },\n setupMiddlewares(middlewares) {\n return [\n routerMiddleware(history), // 来自 react-router-redux 的 routerMiddleware\n ...middlewares,\n ];\n },\n setupApp(app) {\n app._history = patchHistory(history); \n },\n };\n\n const app = core.create(opts, createOpts);\n const oldAppStart = app.start;\n app.router = router;\n app.start = start;\n return app;\n \n // 一些用到的函数的定义...\n \n} \n```\n\n这里面大多数内容都比较简单,这里面提两个地方:\n\n1. patchHistory:\n\n```\nfunction patchHistory(history) {\n const oldListen = history.listen;\n history.listen = (callback) => {\n callback(history.location);\n return oldListen.call(history, callback);\n };\n return history;\n}\n```\n\n显然,这里的意思是让第一次被绑定 listener 的时候执行一遍 callback,可以用于初始化相关操作。\n\n我们可以在`router.js`中添加如下代码来验证:\n\n```\n history.listen((location, action)=>{\n console.log('history listen:', location, action)\n })\n```\n\n2. 在完成可选项的构造之后,调用了 dva-core 中暴露的 create 函数。\n\ncreate 函数本身也并不复杂,核心代码如下:\n\n```javascript\nexport function create(hooksAndOpts = {}, createOpts = {}) {\n const {\n initialReducer,\n setupApp = noop,\n } = createOpts;\n\n const plugin = new Plugin(); // 实例化钩子函数管理类\n plugin.use(filterHooks(hooksAndOpts)); // 这个时候先对 obj 进行清理,清理出在我们定义的类型之外的 hooks,之后进行统一绑定\n\n const app = {\n _models: [\n prefixNamespace({ ...dvaModel }), // 前缀处理\n ],\n _store: null,\n _plugin: plugin,\n use: plugin.use.bind(plugin),\n model, // 下文定义\n start, // 下文定义\n };\n return app;\n \n //一些函数的定义\n \n} \n```\n\n这里面我们可以看到,这里的 `hooksAndOpts` 实际上就是一开始我们构造 dva 的时候传入的 opts 对象经过处理之后的结果。\n\n我们可以传入的可选项,实际上都在 `Plugin.js` 中写明了:\n\n```\nconst hooks = [\n 'onError',\n 'onStateChange',\n 'onAction',\n 'onHmr',\n 'onReducer',\n 'onEffect',\n 'extraReducers',\n 'extraEnhancers',\n];\n```\n\n具体 [hooks的作用可以在这里进行查阅](https://github.com/dvajs/dva/blob/master/docs/API_zh-CN.md#appusehooks)。\n\nPlugin 插件管理类(实际上我认为称其为钩子函数管理类比较合适)除了定义了上文的使用到的use方法(挂载插件)、还有apply方法(执行某一个钩子下挂载的所有回调)、get方法(获取某一个钩子下的所有回调,返回数组)\n\n\n#### 第二步\n\n\n这里的第二步比较简洁:我们知道实际上这里就是使用了`plugin.use`方法挂载了一个插件\n\n```javascript\napp.use(createLoading()); // 需要注意,插件挂载需要在 app.start 之前\n```\n\ncreateLoading 这个插件实际上是官方提供的 Loading 插件,通过这个插件我们可以非常方便地进行 Loading 的管理,无需进行手动管理,我们可以先[看一篇文章](https://www.jianshu.com/p/61fe7a57fad4)来简单了解一下。\n\n这个插件看似神奇,实际上原理也比较简单,主要用了`onEffect`钩子函数(装饰器):\n\n```javascript\nfunction onEffect(effect, { put }, model, actionType) {\n const { namespace } = model;\n if (\n (only.length === 0 && except.length === 0)\n || (only.length > 0 && only.indexOf(actionType) !== -1)\n || (except.length > 0 && except.indexOf(actionType) === -1)\n ) {\n return function*(...args) {\n yield put({ type: SHOW, payload: { namespace, actionType } });\n yield effect(...args);\n yield put({ type: HIDE, payload: { namespace, actionType } });\n };\n } else {\n return effect;\n }\n }\n```\n\n结合基于的redux-saga,在目标异步调用开始的时候`yield put({ type: SHOW, payload: { namespace, actionType } });`,在异步调用结束的时候`yield put({ type: HIDE, payload: { namespace, actionType } });`,这样就可以管理异步调用开始和结束的Loading状态了。\n\n\n#### 第三步\n\n第三步这里其实省略了,因为使用了动态加载,将 Models 定义的内容和 React Component 进行了动态加载,实际上也可以按照注释的方法来写。\n\n但是没有关系,我们还是可以分析 models 引入的文件中做了哪些事情(下面列出的代码在原基础上进行了一些简化):\n\n```javascript\nimport queryString from 'query-string';\nimport * as usersService from '../services/users';\n\nexport default {\n namespace: 'users',\n state: {\n list: [],\n total: null,\n page: null,\n },\n reducers: {\n save(state, { payload: { data: list, total, page } }) {\n return { ...state, list, total, page };\n },\n },\n effects: {\n *fetch({ payload: { page = 1 } }, { call, put }) {\n const { data, headers } = yield call(usersService.fetch, { page });\n yield put({\n type: 'save',\n payload: {\n data,\n total: parseInt(headers['x-total-count'], 10),\n page: parseInt(page, 10),\n },\n });\n },\n //...\n *reload(action, { put, select }) {\n const page = yield select(state => state.users.page);\n yield put({ type: 'fetch', payload: { page } });\n },\n },\n subscriptions: {\n setup({ dispatch, history }) {\n return history.listen(({ pathname, search }) => {\n const query = queryString.parse(search);\n if (pathname === '/users') {\n dispatch({ type: 'fetch', payload: query });\n }\n });\n },\n },\n};\n```\n\n这些内容,我们通过`app.model(require('./models/users'));`就可以引入。\n\n实际上,model 函数本身还是比较简单的,但由于 dva 拥有 model 动态加载的能力,实际上调用 app.start 前和 app.start 后model函数是不一样的。\n\n调用 start 函数前,我们直接挂载即可(因为start函数中会对所有model进行遍历性统一处理,所以无需过多处理):\n\n```javascript\nfunction model(m) {\n if (process.env.NODE_ENV !== 'production') {\n checkModel(m, app._models);\n }\n app._models.push(prefixNamespace(m));\n // 把 model 注册到 app 的 _models 里面,但是当 app start 之后,就不能仅仅用这种方法了,需要 injectModel\n }\n```\n\n调用了 start 函数之后,model函数被替换成如下:\n\n```javascript\nfunction injectModel(createReducer, onError, unlisteners, m) {\n model(m);\n\n const store = app._store;\n if (m.reducers) {\n store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state);\n store.replaceReducer(createReducer(store.asyncReducers));\n }\n if (m.effects) {\n store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));\n }\n if (m.subscriptions) {\n unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);\n }\n }\n```\n\n**我们首先分析第一个 if 中的内容**:首先通过getReducer函数将转换好的 reducers 挂载(或替换)到 store.asyncReducers[m.namespace] 中,然后通过 redux 本身提供的能力 replaceReducer 完成 reducer 的替换。\n\n这里我们需要注意 getReducer 函数,实际上,dva 里面 reducers 写法和我们之前直接使用 redux 的写法略有不同:\n\n我们这里的 reducers,实际上要和 action 中的 actionType 同名的 reducer,所以这里我们没有必要去写 switch case 了,对于某一个 reducer 来说其行为应该是确定的,这给 reducers 的写法带来了一定的简化,当然,我们可以使用 extraReducers 定义我们之前习惯的那种比较复杂的 reducers。\n\n**接下来我们分析第二个 if 中的内容**:第二个函数首先获取到了我们定义的 effects 并通过 _getSaga 进行处理,然后使用 `runSaga`(实际上就是createSagaMiddleware().run,来自于redux-saga) 进行执行。\n\n实际上,这里的 `_getSaga` 函数比较复杂,我们接下来重点介绍这个函数。\n\n`_getSaga` 函数由 `getSaga.js` 暴露,其定义如下:\n\n```javascript\nexport default function getSaga(resolve, reject, effects, model, onError, onEffect) {\n return function *() { // 返回一个函数\n for (const key in effects) { // 这个函数对 effects 里面的所有键\n if (Object.prototype.hasOwnProperty.call(effects, key)) { // 先判断一下键是属于自己的\n const watcher = getWatcher(resolve, reject, key, effects[key], model, onError, onEffect);\n // 然后调用getWatch获取watcher\n const task = yield sagaEffects.fork(watcher); // 利用 fork 开启一个 task\n yield sagaEffects.fork(function *() { // 这样写的目的是,如果我们移除了这个 model 要及时结束掉\n yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);\n yield sagaEffects.cancel(task);\n });\n }\n }\n };\n}\n```\n\ngetWatcher 的一些核心代码如下:\n\n```javascript\n\nfunction getWatcher(resolve, reject, key, _effect, model, onError, onEffect) {\n let effect = _effect;\n let type = 'takeEvery';\n let ms;\n\n if (Array.isArray(_effect)) {\n effect = _effect[0];\n const opts = _effect[1];\n // 对 opts 进行一定的校验\n //...\n }\n\n function *sagaWithCatch(...args) { // 都会调用这个过程\n try {\n yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });\n const ret = yield effect(...args.concat(createEffects(model)));\n yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });\n resolve(key, ret);\n } catch (e) {\n onError(e);\n if (!e._dontReject) {\n reject(key, e);\n }\n }\n }\n\n const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key); \n // 挂载 onEffect 钩子\n\n switch (type) {\n case 'watcher':\n return sagaWithCatch;\n case 'takeLatest':\n return function*() {\n yield takeLatest(key, sagaWithOnEffect);\n };\n case 'throttle': // 起到节流的效果,在 ms 时间内仅仅会被触发一次\n return function*() {\n yield throttle(ms, key, sagaWithOnEffect);\n };\n default:\n return function*() {\n yield takeEvery(key, sagaWithOnEffect);\n };\n }\n}\n```\n\n这个函数的工作,可以主要分为以下三个部分:\n\n1.将 effect 包裹成 sagaWithCatch,除了便于错误处理和增加前后钩子,值得我们注意的是 resolve 和 reject,\n\n这个 resolve 和 reject,实际上是来自`createPromiseMiddleware.js`\n\n我们知道,我们在使用redux-saga的过程中,实际上是监听未来的action,并执行 effects,所以我们在一个 effects 函数中执行一些异步操作,然后 put(dispatch) 一个 action,还是会被监听这个 action 的其他 saga 监听到。\n\n所以就有如下场景:我们 dispatch 一个 action,这个时候如果我们想获取到什么时候监听这个 action 的 saga 中的异步操作执行结束,是办不到的(因为不是所有的时候我们都把所有处理逻辑写在 saga 中),所以我们的 dispatch 有的时候需要返回一个 Promise 从而我们可以进行异步结束后的回调(这个 Promise 在监听者 saga 异步执行完后被决议,见上文`sagaWithCatch`函数源代码)。\n\n如果我讲的还是比较混乱,也可以参考[这个issue](https://github.com/dvajs/dva/issues/175)\n\n对于这个情况,我认为这是 dva 代码最精彩的地方之一,作者通过定义如下的middleware:\n\n```javascript\n const middleware = () => next => (action) => {\n const { type } = action;\n if (isEffect(type)) {\n return new Promise((resolve, reject) => {\n map[type] = {\n resolve: wrapped.bind(null, type, resolve),\n reject: wrapped.bind(null, type, reject),\n };\n });\n } else {\n return next(action);\n }\n };\n\n function wrapped(type, fn, args) {\n if (map[type]) delete map[type];\n fn(args);\n }\n\n function resolve(type, args) {\n if (map[type]) {\n map[type].resolve(args);\n }\n }\n\n function reject(type, args) {\n if (map[type]) {\n map[type].reject(args);\n }\n }\n```\n\n并且在上文的`sagaWithCatch`相关effect执行结束的时候调用 resolve,让 dispatch 返回了一个 Promise。\n\n当然,上面这段代码还是有点问题的,这样会导致同名 reducer 和 effect 不会 fallthrough(即两者都执行),因为都已经返回了,action 便不会再进一步传递,关于这样设计的好坏,在[这里](https://github.com/sorrycc/blog/issues/48)有过一些讨论,笔者不进行展开表述。\n\n2.在上面冗长的第一步之后,又通过`applyOnEffect`函数包裹了`OnEffect`的钩子函数,这相当于是一种`compose`,(上文的 dva-loading 中间件实际上就是在这里被处理的)其实现对于熟悉 redux 的同学来说应该不难理解:\n\n```javascript\nfunction applyOnEffect(fns, effect, model, key) {\n for (const fn of fns) {\n effect = fn(effect, sagaEffects, model, key);\n }\n return effect;\n}\n```\n\n3.最后,根据我们定义的type(默认是`takeEvery`,也就是都执行),来选择不同的 saga,takeLatest 即为只是执行最近的一个,throttle则起到节流的效果,一定时间内仅仅允许被触发一次,这些都是 redux-saga 的内部实现,dva 也是基本直接引用,因此在这里不进行展开。\n\n**最后我们分析`injectModel`第三个`if`中的内容**:处理`subscriptions`:\n\n```javascript\nif (m.subscriptions) {\n unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);\n}\n```\n\n`subscriptions`可以理解为和这个model有关的全局监听,但是相对独立。这一个步骤首先调用`runSubscription`来一个一个调用我们的`subscriptions`:\n\n```javascript\nexport function run(subs, model, app, onError) { // 在index.js中被重命名为 runSubscription\n const funcs = [];\n const nonFuncs = [];\n for (const key in subs) {\n if (Object.prototype.hasOwnProperty.call(subs, key)) {\n const sub = subs[key];\n const unlistener = sub({\n dispatch: prefixedDispatch(app._store.dispatch, model),\n history: app._history,\n }, onError);\n if (isFunction(unlistener)) {\n funcs.push(unlistener);\n } else {\n nonFuncs.push(key);\n }\n }\n }\n return { funcs, nonFuncs };\n}\n```\n\n正如我们所期待的,`run`函数就是一个一个执行`subscriptions`,但是这里有一点需要我们注意的,我们定义的`subscriptions`应该是需要返回一个`unlistener`来返回接触函数,这样当整个 model 被卸载的时候 dva 会自动调用这个接解除函数(也就是为什么这里的返回函数被命名为`unlistener`)\n\n#### 第四步\n\n源代码中的第四步,是对 router 的挂载:\n\n```javascript\napp.router(require('./router'));\n```\n\n`require('./router')`返回的内容在源代码中经过一系列引用传递最后直接被构造成 React Component 并且最终调用 ReactDom.render 进行渲染,这里没有什么好说的,值得一提的就是 router 的动态加载。\n\n动态加载在该样例中是这样使用的:\n\n```javascript\nimport React from 'react';\nimport { Router, Switch, Route } from 'dva/router';\nimport dynamic from 'dva/dynamic';\n\nfunction RouterConfig({ history, app }) {\n const IndexPage = dynamic({\n app,\n component: () => import('./routes/IndexPage'),\n });\n\n const Users = dynamic({\n app,\n models: () => [\n import('./models/users'),\n ],\n component: () => import('./routes/Users'),\n });\n\n history.listen((location, action)=>{\n console.log('history listen:', location, action)\n })\n\n return (\n <Router history={history}>\n <Switch>\n <Route exact path=\"/\" component={IndexPage} />\n <Route exact path=\"/users\" component={Users} />\n </Switch>\n </Router>\n );\n}\n```\n\n我们可以看出,主要就是利用`dva/dynamic.js`暴露的 dynamic 函数进行动态加载,接下来我们简单看一下 dynamic 函数做了什么:\n\n```javascript\nexport default function dynamic(config) {\n const { app, models: resolveModels, component: resolveComponent } = config;\n return asyncComponent({\n resolve: config.resolve || function () {\n const models = typeof resolveModels === 'function' ? resolveModels() : [];\n const component = resolveComponent();\n return new Promise((resolve) => {\n Promise.all([...models, component]).then((ret) => {\n if (!models || !models.length) {\n return resolve(ret[0]);\n } else {\n const len = models.length;\n ret.slice(0, len).forEach((m) => {\n m = m.default || m;\n if (!Array.isArray(m)) {\n m = [m];\n }\n m.map(_ => registerModel(app, _)); // 注册所有的 model\n });\n resolve(ret[len]);\n }\n });\n });\n },\n ...config,\n });\n}\n```\n\n这里主要调用了 asyncComponent 函数,接下来我们再看一下这个函数:\n\n```javascript\nfunction asyncComponent(config) {\n const { resolve } = config;\n\n return class DynamicComponent extends Component {\n constructor(...args) {\n super(...args);\n this.LoadingComponent =\n config.LoadingComponent || defaultLoadingComponent;\n this.state = {\n AsyncComponent: null,\n };\n this.load();\n }\n\n componentDidMount() {\n this.mounted = true;\n }\n\n componentWillUnmount() {\n this.mounted = false;\n }\n\n load() {\n resolve().then((m) => {\n const AsyncComponent = m.default || m;\n if (this.mounted) {\n this.setState({ AsyncComponent });\n } else {\n this.state.AsyncComponent = AsyncComponent; // eslint-disable-line\n }\n });\n }\n\n render() {\n const { AsyncComponent } = this.state;\n const { LoadingComponent } = this;\n if (AsyncComponent) return <AsyncComponent {...this.props} />;\n\n return <LoadingComponent {...this.props} />;\n }\n };\n}\n```\n\n这个函数逻辑比较简洁,我们分析一下动态加载流程;\n\n* 在 constructor 里面调用 `this.load();` ( LoadingComponent 为占位 component)\n* 在 `this.load();` 函数里面调用 `dynamic` 函数返回的 resolve 方法\n* resolve 方法实际上是一个 Promise,把相关 models 和 component 加载完之后 resolve (区分这两个 resolve)\n* 加载完成之后返回 AsyncComponent (即加载的 Component)\n\n动态加载主流程结束,至于动态加载的代码分割工作,可以使用 webpack3 的 `import()` 动态加载能力(例子中也是这样使用的)。\n\n\n#### 第五步\n\n第五步骤就是 start 了:\n\n```javascript\napp.start('#root');\n```\n\n这个时候如果我们在 start 函数中传入 DomElement 或者 DomQueryString,就会直接启动应用了,如果我们这个时候不传入任何内容,实际上返回的是一个`<Provider />` (React Component),便于服务端渲染。 相关判断逻辑如下:\n\n```javascript\n if (container) {\n render(container, store, app, app._router);\n app._plugin.apply('onHmr')(render.bind(null, container, store, app));\n } else {\n return getProvider(store, this, this._router);\n }\n```\n\n至此,主要流程结束,以上几个步骤也包括了 dva 源码做的主要工作。\n\n当然 dva 源码中还有一些比如前缀处理等工作,但是相比于以上内容非常简单,所以在这里不进行分析了。\n\n\n### dva-core 文件目录\n\ndva-core中的源码文件目录以及其功能:\n\n* checkModel 对我们定义的 Model 进行检查是否符合要求\n* constants 非常简单的常量文件,目前只定义了一个常量:NAMESPACE_SEP(/)\n* cratePromiseMiddleware 笔者自己定义的 redux 插件\n* createStore 封装了 redux 原生的 createStore\n* getReducer 这里面的函数其实主要就是调用了 handleActions 文件导出的函数\n* getSaga 将用户输入的 effects 部分的键值对函数进行管理\n* handleActions 是将 dva 风格的 reducer 和 state 转化成 redux 本来接受的那种方式\n* index 主入口文件\n* Plugin 插件类:可以管理不同钩子事件的回调函数,拥有增加、获取、执行钩子函数的功能\n* perfixedDispatch 该文件提供了对 Dispatch 增加前缀的工具性函数 prefixedDispatch\n* prefixNamespace 该文件提供了对 reducer 和 effects 增加前缀的工具性函数 prefixNamespace\n* prefixType 判断是 reducer 还是 effects\n* subscriptions 该文件提供了运行 subscriptions 和调用用户返回的 unlisten 函数以及删除缓存的功能\n* utils 提供一些非常基础的工具函数\n\n\n### 优势总结\n\n* 动态 model,已经封装好了整套调用,动态添加/删除 model 变得非常简单\n* 默认封装好了管理 effects 的方式,有限可选可配置,降低学习成本的同时代码更利于维护\n* 易于上手,集成redux、redux-saga、react-router等常用功能\n\n\n### 劣势总结\n\n* 版本区隔不明显,dva 有 1.x 和 2.x 两种版本,之间API有些差异,但是官网提供的一些样例等中没有说明基于的版本,并且有的还是基于旧版本的,会给新手带来很多疑惑。\n* 内容繁杂,但是却没有一个整合性质的官方网站,大都是通过 list 的形式列下来写在README的。\n* 目前比如动态加载等还存在着一些问题,和直接采用react配套工具写的效果有所区别。\n* 很多 issues 不知道为什么就被关闭了,作者在最后也并未给出合理的解释。\n* dva2 之后有点将 effects 和 actions 混淆,这一点我也并不是非常认同,当然原作者可能有自己的考虑,这里不过多评议。\n\n总之,作为一个个人主力的项目(主要开发者贡献了99%以上的代码),可以看出作者的功底深厚,经验丰富,但是由于这样一个体系化的东西牵扯内容较多,并且非常受制于react、redux、react-router、redux-saga等的版本影响,**不建议具备一定规模的非阿里系团队在生产环境中使用**,但是如果是快速成型的中小型项目或者个人应用,使用起来还是有很大帮助的。\n\n### TODOS\n\n笔者也在准备做一个和 dva 处于同一性质,但是设计、实现和使用有所区别的框架,希望能够尽快落成。\n","tags":["前端框架"]},{"title":"构建利用Proxy和Reflect实现双向数据绑定的微框架","url":"/sheldon_blog/passages/构建利用Proxy和Reflect实现双向数据绑定的微框架/","content":">写在前面:这篇文章讲述了如何利用Proxy和Reflect实现双向数据绑定,个人系Vue早期玩家,写这个小框架的时候也没有参考Vue等源代码,之前了解过其他实现,但没有直接参考其他代码,如有雷同,纯属巧合。\n\n代码下载地址:[这里下载](https://github.com/aircloud/Polar.js)\n\n### 综述\n\n*关于Proxy和Reflect的资料推荐阮老师的教程:http://es6.ruanyifeng.com/ 这里不做过多介绍。*\n\n实现双向数据绑定的方法有很多,也可以参考本专栏之前的其他实现,我之所以选择用Proxy和Reflect,一方面是因为可以大量节约代码,并且简化逻辑,可以让我把更多的经历放在其他内容的构建上面,另外一方面本项目直接基于ES6,用这些内容也符合面向未来的JS编程规范,第三点最后说。\n\n由于这个小框架是自己在PolarBear这个咖啡馆在一个安静的午后开始写成,暂且起名Polar,日后希望我能继续完善这个小框架,给添加上更多有趣的功能。\n\n首先我们可以看整体功能演示: \n[一个gif动图,如果不能看,请点击[这里的链接](https://www.10000h.top/images/data_img/gif1.gif)]\n\n![](https://www.10000h.top/images/data_img/gif1.gif)\n\n### 代码分析\n\n我们要做这样一个小框架,核心是要监听数据的改变,并且在数据的改变的时候进行一些操作,从而维持数据的一致。\n\n我的思路是这样的:\n\n* 将所有的数据信息放在一个属性对象中(this._data),之后给这个属性对象用Proxy包装set,在代理函数中我们更新属性对象的具体内容,同时通知所有监听者,之后返回新的代理对象(this.data),我们之后操作的都是新的代理对象。\n* 对于input等表单,我们需要监听input事件,在回调函数中直接设置我们代理好的数据对象,从而触发我们的代理函数。\n* 我们同时也应该支持事件机制,这里我们以最常用的click方法作为例子实现。\n\n下面开始第一部分,我们希望我们之后使用这个库的时候可以这样调用:\n\n```\n<div id=\"app\">\n <form>\n <label>name:</label>\n <input p-model = \"name\" />\n </form>\n <div>name:{{name}} age:{{age}}</div>\n <i>note:{{note}}</i><br/>\n <button p-click=\"test(2)\">button1</button>\n</div>\n<script>\n var myPolar = new Polar({\n el:\"#app\",\n data: {\n name: \"niexiaotao\",\n age:16,\n note:\"Student of Zhejiang University\"\n },\n methods:{\n test:function(e,addNumber){\n console.log(\"e:\",e);\n this.data.age+=Number(addNumber);\n }\n }\n});\n</script>\n```\n\n没错,和Vue神似吧,所以这种调用方式应当为我们所熟悉。\n\n我们需要建立一个Polar类,这个类的构造函数应该进行一些初始化操作:\n\n```\n constructor(configs){\n this.root = this.el = document.querySelector(configs.el);\n this._data = configs.data;\n this._data.__bindings = {};\n //创建代理对象\n this.data = new Proxy(this._data, {set});\n this.methods = configs.methods;\n\n this._compile(this.root);\n}\n```\n\n这里面的一部份内容是直接将我们传入的configs按照属性分别赋值,另外就是我们创建代理对象的过程,最后的`_compile`方法可以理解为一个私有的初始化方法。\n\n实际上我把剩下的内容几乎都放在`_compile`方法里面了,这样理解起来方便,但是之后可能要改动。\n\n我们还是先不能看我们代理的set该怎么写,因为这个时候我们还要先继续梳理思路:\n\n假设我们这样`<div>name:{{name}}</div>`将数据绑定到dom节点,这个时候我们需要做什么呢,或者说,我们通过什么方式让dom节点和数据对应起来,随着数据改变而改变。\n\n看上文的`__bindings`。这个对象用来存储所有绑定的dom节点信息,`__bindings`本身是一个对象,每一个有对应dom节点绑定的数据名称都是它的属性,对应一个数组,数组中的每一个内容都是一个绑定信息,这样,我们在自己写的set代理函数中,我们一个个调用过去,就可以更新内容了:\n\n```\ndataSet.__bindings[key].forEach(function(item){\n //do something to update...\n});\n```\n\n我这里创建了一个用于构造调用的函数,这个函数用于创建存储绑定信息的对象:\n\n```\nfunction Directive(el,polar,attr,elementValue){\n this.el=el;//元素本身dom节点\n this.polar = polar;//对应的polar实例\n this.attr = attr;//元素的被绑定的属性值,比如如果是文本节点就可以是nodeValue\n this.el[this.attr] = this.elementValue = elementValue;//初始化\n}\n```\n\n这样,我们的set可以这样写:\n\n```\nfunction set(target, key, value, receiver) {\n const result = Reflect.set(target, key, value, receiver);\n var dataSet = receiver || target;\n dataSet.__bindings[key].forEach(function(item){\n item.el[item.attr] = item.elementValue = value;\n });\n return result;\n}\n```\n\n接下来可能还有一个问题:我们的`{{name}}`实际上只是节点的一部分,这并不是节点啊,另外我们是不是还可以这么写:`<div>name:{{name}} age:{{age}}</div>`?\n\n关于这两个问题,前者的答案是我们将`{{name}}`替换成一个文本节点,而为了应对后者的情况,我们需要将两个被绑定数据中间和前后的内容,都变成新的文本节点,然后这些文本节点组成文本节点串。(这里多说一句,html5的normalize方法可以将多个文本节点合并成一个,如果不小心调用了它,那我们的程序就要GG了)\n\n所以我们在`_compile`函数首先:\n\n```\nvar _this = this;\n\n var nodes = root.children;\n\n var bindDataTester = new RegExp(\"{{(.*?)}}\",\"ig\");\n\n for(let i=0;i<nodes.length;i++){\n var node=nodes[i];\n\n //如果还有html字节点,则递归\n if(node.children.length){\n this._compile(node);\n }\n\n var matches = node.innerHTML.match(bindDataTester);\n if(matches){\n var newMatches = matches.map(function (item) {\n return item.replace(/{{(.*?)}}/,\"$1\")\n });\n var splitTextNodes = node.innerHTML.split(/{{.*?}}/);\n node.innerHTML=null;\n //更新DOM,处理同一个textnode里面多次绑定情况\n if(splitTextNodes[0]){\n node.append(document.createTextNode(splitTextNodes[0]));\n }\n for(let ii=0;ii<newMatches.length;ii++){\n var el = document.createTextNode('');\n node.appendChild(el);\n if(splitTextNodes[ii+1]){\n node.append(document.createTextNode(splitTextNodes[ii+1]));\n }\n //对数据和dom进行绑定\n let returnCode = !this._data.__bindings[newMatches[ii]]?\n this._data.__bindings[newMatches[ii]] = [new Directive(el,this,\"nodeValue\",this.data[newMatches[ii]])]\n :this._data.__bindings[newMatches[ii]].push(new Directive(el,this,\"nodeValue\",this.data[newMatches[ii]]))\n }\n }\n\n```\n\n这样,我们的数据绑定阶段就写好了,接下来,我们处理`<input p-model = \"name\" />`这样的情况。\n\n这实际上是一个指令,我们只需要当识别到这一个指令的时候,做一些处理,即可:\n\n```\nif(node.hasAttribute((\"p-model\"))\n && node.tagName.toLocaleUpperCase()==\"INPUT\" || node.tagName.toLocaleUpperCase()==\"TEXTAREA\"){\n node.addEventListener(\"input\", (function () {\n\n var attributeValue = node.getAttribute(\"p-model\");\n\n if(_this._data.__bindings[attributeValue]) _this._data.__bindings[attributeValue].push(new Directive(node,_this,\"value\",_this.data[attributeValue])) ;\n else _this._data.__bindings[attributeValue] = [new Directive(node,_this,\"value\",_this.data[attributeValue])];\n\n return function (event) {\n _this.data[attributeValue]=event.target.value\n }\n })());\n}\n```\n\n请注意,上面调用了一个`IIFE`,实际绑定的函数只有返回的函数那一小部分。\n\n最后我们处理事件的情况:`<button p-click=\"test(2)\">button1</button>`\n\n实际上这比处理`p-model`还简单,但是我们为了支持函数参数的情况,处理了一下传入参数,另外我实际上将`event`始终作为一个参数传递,这也许并不是好的实践,因为使用的时候还要多注意。\n\n```\nif(node.hasAttribute(\"p-click\")) {\n node.addEventListener(\"click\",function(){\n var attributeValue=node.getAttribute(\"p-click\");\n var args=/\\(.*\\)/.exec(attributeValue);\n //允许参数\n if(args) {\n args=args[0];\n attributeValue=attributeValue.replace(args,\"\");\n args=args.replace(/[\\(\\)\\'\\\"]/g,'').split(\",\");\n }\n else args=[];\n return function (event) {\n _this.methods[attributeValue].apply(_this,[event,...args]);\n }\n }());\n}\n```\n\n现在我们已经将所有的代码分析完了,是不是很清爽?代码除去注释约100行,所有源代码可以在[这里下载](https://github.com/aircloud/Polar.js)。这当然不能算作一个框架了,不过可以学习学习,这学期有时间的话,还要继续完善,也欢迎大家一起探讨。\n\n一起学习,一起提高,做技术应当是直接的,有问题欢迎指出~\n\n---\n\n\n最后说的第三点:是自己还是一个学生,做这些内容也仅仅是出于兴趣,因为找暑期实习比较艰难,在等待鹅厂面试间隙写的这个程序,压压惊(然而并没有消息)。","tags":["MVVM"]},{"title":"[PWA实践]serviceWorker生命周期、请求代理与通信","url":"/sheldon_blog/passages/PWA实践-serviceWorker生命周期、请求代理与通信/","content":"\n本文主要讲 serviceWorker 生命周期和挂载、卸载等问题,适合对 serviceWorker 的作用有所了解但是具体细节不是特别清楚的读者\n\n**以下所有分析基于 Chrome V63**\n\n### serviceWorker的挂载\n\n先来一段代码感受serviceWorker注册:\n\n```\nif ('serviceWorker' in navigator) {\n window.addEventListener('load', function () {\n navigator.serviceWorker.register('/sw.js', {scope: '/'})\n .then(function (registration) {\n // 注册成功\n console.log('ServiceWorker registration successful with scope: ', registration.scope);\n })\n .catch(function (err) {\n // 注册失败:(\n console.log('ServiceWorker registration failed: ', err);\n });\n });\n}\n```\n通过上述代码,我们定义在`/sw.js`里的内容就会生效(对于当前页面之前没有 serviceWorker 的情况而言,我们注册的 serviceWorker 肯定会生效,如果当前页面已经有了我们之前注册的 serviceWorker,这个时候涉及到 serviceWorker的更新机制,下文详述)\n\n如果我们在`sw.js`没有变化的情况下刷新这个页面,每次还是会有注册成功的回调以及相应的log输出,但是这个时候浏览器发现我们的 serviceWorker 并没有发生变化,并不会重置一遍 serviceWorker\n\n### serviceWorker更新\n\n我们如果想更新一个 serviceWorker,根据我们的一般web开发策略,可能会想到以下几种策略:\n\n* 仅变更文件名(比如把`sw.js`变成`sw-v2.js`或者加一个hash)\n* 仅变更文件内容(仅仅更新`sw.js`的内容,文件名不变)\n* 同时变更:同时执行以上两条\n\n在这里,我可以很负责的告诉你,**变更serviceWorker文件名绝对不是一个好的实践**,浏览器判断 serviceWorker 是否相同基本和文件名没有关系,甚至有可能还会造成浏览器抛出404异常(因为找不到原来的文件名对应的文件了)。\n\n所以我们只需要变更内容即可,实际上,我们每次打开或者刷新该页面,浏览器都会重新请求一遍 serviceWorker 的定义文件,如果发现文件内容和之前的不同了,这个时候:\n\n(*下文中,我们使用“有关 tab”来表示受 serviceWorker 控制的页面*,刷新均指普通刷新(F5/CommandR)并不指Hard Reload)\n\n* 这个新的 serviceWorker 就会进入到一个 “waiting to activate” 的状态,并且只要我们不关闭这个网站的所有tab(更准确地说,是这个 serviceWorker 控制的所有页面),新的 serviceWorker 始终不会进入替换原有的进入到 running 状态(就算我们只打开了一个有关 tab,直接刷新也不会让新的替换旧的)。\n\n* 如果我们多次更新了 serviceWorker 并且没有关闭当前的 tab 页面,那么新的 serviceWorker 就会挤掉原先处于第二顺位(waiting to activate)的serviceWorker,变成`waiting to activate`状态\n\n也就是说,我们只有关闭当前旧的 serviceWorker 控制的所有页面 的所有tab,之后浏览器才会把旧的 serviveWorker 移除掉,换成新的,再打开相应的页面就会使用新的了。\n\n当然,也有一个特殊情况:如果我们在新的 serviceWorker 使用了`self.skipWaiting();`,像这样:\n\n```\nself.addEventListener('install', function(event) {\n self.skipWaiting();\n});\n```\n\n这个时候,要分为以下两种情况:\n\n* 如果当前我们只打开了一个有关 tab,这个时候,我们直接刷新,发现新的已经替换掉旧的了。\n* 如果我们当前打开了若干有关 tab,这个时候,无论我们刷新多少次,新的也不会替换掉旧的,只有我们一个一个关掉tab(或者跳转走)只剩下最后一个了,这个时候刷新,会让新的替换旧的(也就是上一种情况)\n\nChrome 的这种机制,防止了同一个页面先后被新旧两个不同的 serviceWorker 接管的情况出现。\n\n#### 手动更新\n\n虽然说,在页面每次进入的时候浏览器都会检查一遍 serviceWorker 是否更新,但如果我们想要手动更新 serviceWorker 也没有问题:\n\n```\nnavigator.serviceWorker.register(\"/sw.js\").then(reg => {\n reg.update();\n // 或者 一段时间之后更新\n});\n```\n\n这个时候如果 serviceWorker 变化了,那么会重新触发 install 执行一遍 install 的回调函数,如果没有变,就不会触发这个生命周期。\n\n#### install 生命周期钩子\n\n我们一般会在 sw.js 中,添加`install`的回调,一般在回调中,我们会进行缓存处理操作,像这样:\n\n```\nself.addEventListener('install', function(event) {\n console.log('[sw2] serviceWorker Installed successfully', event)\n\n event.waitUntil(\n caches.open('mysite-static-v1').then(function(cache) {\n return cache.addAll([\n '/stylesheets/style.css',\n '/javascripts/common.39c462651d449a73b5bb.js',\n ]);\n })\n )\n} \n```\n\n如果我们新打开一个页面,如果之前有 serviceWorker,那么会触发`install`,如果之前没有, 那么在 serviceWorker 装载后会触发 `install`。\n\n如果我们刷新页面,serviceWorker 和之前没有变化或者 serviceWorker 已经处在 `waiting to activate`,不会触发`install`,如果有变化,会触发`install`,但不会接管页面(上文中提到)。\n\n#### activate 生命周期钩子\n\nactivate 在什么时候被触发呢?\n\n如果当前页面没有 serviceworker ,那么会在 install 之后触发。\n\n如果当前页面有 serviceWorker,并且有 serviceWorker更新,新的 serviceWorker 只会触发 install ,不会触发 activate\n\n换句话说,当前变成 active 的 serviceWorker 才会被触发这个生命周期钩子\n\n\n### serviceWorker 代理请求\n\nserviceWorker 代理请求相对来说比较好理解,以下是一个很简单的例子:\n\n```\nself.addEventListener('install', function(event) {\n console.log('[sw2] serviceWorker Installed successfully', event)\n\n event.waitUntil(\n caches.open('mysite-static-v1').then(function(cache) {\n return cache.addAll([\n '/stylesheets/style.css',\n '/javascripts/common.39c462651d449a73b5bb.js',\n ]);\n })\n );\n});\n\nself.addEventListener('fetch', function(event) {\n console.log('Handling fetch event for', event.request.url);\n // console.log('[sw2]fetch but do nothing')\n\n event.respondWith(\n // caches.match() will look for a cache entry in all of the caches available to the service worker.\n // It's an alternative to first opening a specific named cache and then matching on that.\n caches.match(event.request).then(function(response) {\n if (response) {\n console.log('Found response in cache:', response);\n\n return response;\n }\n\n console.log('No response found in cache. About to fetch from network...');\n\n // event.request will always have the proper mode set ('cors, 'no-cors', etc.) so we don't\n // have to hardcode 'no-cors' like we do when fetch()ing in the install handler.\n return fetch(event.request).then(function(response) {\n console.log('Response from network is:', response);\n\n return response;\n }).catch(function(error) {\n // This catch() will handle exceptions thrown from the fetch() operation.\n // Note that a HTTP error response (e.g. 404) will NOT trigger an exception.\n // It will return a normal response object that has the appropriate error code set.\n console.error('Fetching failed:', error);\n\n throw error;\n });\n })\n );\n});\n```\n\n有两点要注意的:\n\n我们如果这样代理了,哪怕没有 cache 命中,实际上也会在控制台写from serviceWorker,而那些真正由serviceWorker发出的请求也会显示,有一个齿轮图标,如下图:\n\n![](https://www.10000h.top/images/sw_1.png)\n\n第二点就是我们如果在 fetch 的 listener 里面 do nothing, 也不会导致这个请求直接假死掉的。\n\n另外,通过上面的代码我们发现,实际上由于现在我们习惯给我们的文件资源加上 hash,所以我们基本上不可能手动输入需要缓存的文件列表,现在大多数情况下,我们都是借助 webpack 插件,完成这部分工作。\n\n### serviceWorker 和 页面之间的通信\n\nserviceWorker向页面发消息:\n\n```\nsw.js:\n\nself.clients.matchAll().then(clients => {\n clients.forEach(client => {\n console.log('%c [sw message]', 'color:#00aa00', client)\n client.postMessage(\"This message is from serviceWorker\")\n })\n})\n\n主页面:\n\nnavigator.serviceWorker.addEventListener('message', function (event) {\n console.log('[Main] receive from serviceWorker:', event.data, event)\n});\n```\n\n当然,这里面是有坑的:\n\n* 主界面的事件监听需要等serviceWorker注册完毕后,所以一般`navigator.serviceWorker.register`的回调到来之后再进行注册(或者延迟足够的时间)。\n* 如果在主界面事件监听还没有注册成功的时候 serviceWorker 发送消息,自然是收不到的。如果我们把 serviceWorker 直接写在 install 的回调中,也是不能被正常收到的。\n\n从页面向 serviceWorker 发送消息:\n\n```\n主页面:\n\nnavigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage('hello serviceWorker');\n\nsw.js:\nself.addEventListener('message', function (event) {\n console.log(\"[sw from main]\",event.data); // 输出:'sw.updatedone'\n});\n```\n\n同样的,这也要求主界面的代码需要等到serviceWorker注册完毕后触发,另外还有一点值得注意, serviceWorker 的事件绑定代码要求主界面的serviceWorker已经注册完毕后才可以。\n\n也就是说,如果当前页面没有该serviceWorker 第一次注册是不会收到主界面接收到的消息的。\n\n记住,只有当前已经在 active 的 serviceWorker, 才能和主页面收发消息等。\n\n**以上就是和 serviceWorker 有关的一些内容,在下一篇文章中,我会对PWA 添加至主屏幕等功能进行总结**\n\n","tags":["PWA"]},{"title":"CentOS安装node8.x版本","url":"/sheldon_blog/passages/CentOS安装node8-x版本/","content":"### CentOS 安装 node 8.x 版本\n\n由于一些原因需要给CentOS服务器安装8.0以上版本的node, 本来直接通过yum管理安装管理,但是没找到好办法,在此记录一下自己最后使用的简单过程:\n\n安装之前删除原来的node和npm (我原来是用yum安装的,如果是第一次安装可以省略这一步):\n\n```\nyum remove nodejs npm -y\n```\n\n首先我们随便进入服务器的一个目录,然后从淘宝的源拉取内容:\n\n```\nwget https://npm.taobao.org/mirrors/node/v8.0.0/node-v8.0.0-linux-x64.tar.xz \n```\n\n解压缩:\n\n```\nsudo tar -xvf node-v8.0.0-linux-x64.tar.xz \n```\n\n进入解压目录下的 bin 目录,执行 ls 命令\n\n```\ncd node-v8.0.0-linux-x64/bin && ls \n```\n\n我们发现有node 和 npm\n\n这个时候我们测试:\n\n```\n./node -v\n```\n\n这个时候我们发现实际上已经安装好了,接下来就是要建立链接文件。\n\n这里还是,如果我们之前已经安装过了,那么我们要先删除之前建立的链接文件:\n\n```\nsudo rm -rf /usr/bin/node\nsudo rm -rf /usr/bin/npm\n```\n\n然后建立链接文件:\n\n```\nsudo ln -s /usr/share/node-v8.0.0-linux-x64/bin/node /usr/bin/node\nsudo ln -s /usr/share/node-v8.0.0-linux-x64/bin/npm /usr/bin/npm\n```\n\n注意这里的第一个路径不要直接复制粘贴,要写当前文件的真正的路径,这个可以通过pwd获取。\n\n然后我们可以通过`node -v`等测试已经安装成功。\n","tags":["Node.js"]},{"title":"深入浏览器web渲染与优化-续","url":"/sheldon_blog/passages/深入浏览器web渲染与优化-续/","content":">本篇文章接上一篇继续分析浏览器web渲染相关内容,但是更侧重优化工作。当然,主要还是基于X5来分析\n\n上一篇文章我们主要是从浏览器内核的线程角度来分析相关工作的,对整体流程没有宏观清晰的分析,这次我们从宏观到微观,从整体到局部,来进行分析和探究可以优化的地方。\n\n首先,一个网页的加载,需要什么工作呢?\n\n![](https://www.10000h.top/images/data_img/webRender2/P1.png)\n\n这个工作可以分为三部分:云(云端)、管(传输链路)、端(客户端),从云经过管传到端,然后经过加载解析排版渲染,从而完成网页从请求到呈现的工作(当然,我们这里没有涉及协议的分析,实际上根据协议不同,这个传输可能是多次传输)。\n\n数据到端之后,又经过以下过程,才最终显示出来:\n\n![](https://www.10000h.top/images/data_img/webRender2/P2.png)\n\n在这个过程中,我们怎么衡量性能呢?\n\n固然,我们有诸多浏览器提供的API,这些API能让我们获取到较多信息并且记录上报:\n\n![](https://www.10000h.top/images/data_img/webRender2/P3.png)\n\n但是这些具体数值表达的含义有限,并且他们实际上也不等于用户体验。\n\n所以,找到一个科学并且可以检测的标准,并且这个标准可以和用户体验有正相关关系,这个是至关重要的。\n\n目前这个标准是**首屏时间**(就之前自己的了解,具体的还区分首屏展示时间和首屏可交互时间,但是这里讲师不做区分,就下文提供的测算方法而言,显然这里指的是首屏展示时间,*另外,展示后到用户的第一次操作都会有一个至少1s的延时,毕竟用户手指按下的动作是会比较慢的,这个时间js的交互都能完成了,所以首屏展示时间更加重要--from dorsywang*)\n\n那么**首屏时间**怎么测量呢?\n\n**拿摄像机快速拍照测量的**。这个答案可能有些吃惊,但是目前X5内核业务的相关开发人员的确就是采用这种方式测算的,通过高速相机不断拍照,然后辅助图像识别,判断首屏是否已经加载完成,最终再通过人工回归校对。 \n因为如果采用程序检测的话,基本上都会对过程本身造成一定的影响,所以没有采用这种方式。\n当然,通过摄像+图像识别的这种方式也是有一定的弊端,比如说,假设首屏有一个图片,而图片的加载通常比较慢并且不影响css、js的加载,这个时候直接通过图片识别的话就可能会有一定的误判。\n\n知道了怎么测算,那么接下来分析影响这个指标的一些原因:\n\n* 资源阻塞内核线程\n\n我们知道,一般情况下,css和JS是阻塞页面的,当然也会对首屏时间造成影响。\n\n对这个问题,X5内核有关键子资源(阻塞资源)缓存,这里的关键资源,指的是内核经过统计判断得出的业务常用的关键子资源。\n\n当然,这个统计也可能缺乏一定的准确性,所以相关团队也正在推进这方面的内容规范化(比如写入Web App Manifest)\n\n* 中文Layout的时间过长\n\n这个问题我之前没有听说过,但是的确是这样子,实际上,浏览器在绘制文字的时候经历的过程非常的多,其中有一个环节是找到文字的宽度和高度(因为在英文状态下,每一个字符的宽度是不同的,所以每一个字符都要查找,但是英文总共只有26个字符),而中文由于字符比较多,常用得就有6000多个,完整的更是有2万个以上,所以这个过程需要花费更多的时间。\n\n为了解决这个问题,X5内核考虑到中文文字几乎都是等宽等高的,所以这个过程对一个文字串来说只需要查询一次即可,实际上是节约了这个环节。\n\n* 首次渲染太慢\n\n为了解决这个问题,可以采用先绘制首屏的方式,这个也就是基于第一篇文章中讲到的浏览器的分块渲染机制\n\n* 一次解析内容过多\n\n采用首屏探测机制,优先解析首屏内容。\n\n另外,这里可以前端配合去做首屏优化:\n\n\n在首屏的位置插入首屏标签,内核解析到标签后立即终止解析并且排版上屏\n\n```\n<meta name=‘x5-pagetype’ content=‘optpage'>\n```\n然后在首屏分界的地方:\n\n```\n<first-screen/>\n```\n\n有了这,可以专门去优化首屏标签之前的内容(这个标签前尽量展现耗时少和不需要阻塞解析的资源)。\n\n另外,X5内核也提供了主资源预拉取的接口,并且考虑到预拉取的cookie问题,还提供了preconnect预链接。 \n*TIP:主资源中关联的子资源预拉取不用主动调用*\n\n* 预先操作\n\n另外为了提供更加极致的优化,X5内核(QQ浏览器、手Q Webview)还提供了如下诸多预操作:\n\n* 在\"黏贴并转到\"之前就开始进行网络请求和预渲染\n* 经常访问的站点可以预解析DNS\n* 点击地址栏时进行搜索预连接\n* 点击链接时,先预链接,再做跳转。\n* ......\n\n### 其他方式优化\n\n实际上上文主要讲了客户端方面的优化工作,实际上对于\"云\"、\"管\"两端,还是有很多优化工作可以讲的,但是由于这个和前端关系不是特别密切,我挑一部分讲一讲。这些在我们前端做个人项目的后台时候也可以参考\n\n##### 后台提速\n\n* 直接使用IP,节省dns的查询时间\n* 维持长连接\n* HTTP1.1启用包头节省\n* 服务器缓存\n* 文本资源压缩传输GZIP(6)\n* 图片尺寸压缩、图片质量压缩、支持webp和sharpp/hevc格式。\n\n##### 降低网络时延\n\n* 就快接入和就近接入\n\n在选择接入点的时候,如果采用就近接入,可以保持路由稳定,有利于负载均衡,并且实现简单,便于维护。但是也有一定的缺点:经验判断,准确度不够高 ; 无法自动切换路由。\n\n相比较而言,选择就快接入,是一个能够提效的方式。\n\n##### 内容防劫持\n\n运营商劫持对我们来说已经是不陌生的话题了,但是X5内核有一个比较新的防劫持手段,就是客户端和云加速服务器同时采用轻量级http加密,虽然这种方式普适性不强,但是的确可以解决腾讯自身业务的防劫持问题。\n\n#### QUIC和http2\n\nQUIC 基于UDP的协议通讯方式,有这些优势:\n\n* 延迟少\n* 前向纠错\n* 没有**线头阻塞[注1]**的多路复用\n* 通信通道的定义基于ID而不是IP+端口,使得切换网络后继续转发链接成为可能\n\n——————\n\n注1:线头阻塞:\n\n![](https://www.10000h.top/images/data_img/webRender2/P4.png)\n\n——————\n\n附1: 带宽和延迟对网页加载的影响:\n\n![](https://www.10000h.top/images/data_img/webRender2/X1.png)\n","tags":["性能优化"]},{"title":"深入浏览器web渲染与优化","url":"/sheldon_blog/passages/深入浏览器web渲染与优化/","content":">本文主要分析和总结web内核渲染的相关内容,以及在这方面前端可以做的性能优化工作。\n\n文章主要分为以下几个部分:\n\n* blink内核的渲染机制\n* chrome内核架构变迁\n* 分层渲染\n* 动画 & canvas & WebGl\n\n*这里的前两部分可能会有些枯燥,如果是前端工程师并且想立即获得实际项目的建议的,可以直接阅读第三部分和第四部分*\n\n### blink内核的渲染机制\n\nblink内核是Google基于Webkit内核开发的新的分支,而实际上,目前Chrome已经采用了blink内核,所以,我们接下来的有关分析大多基于blink内核的浏览器(Chrome),就不再详细指明,当然,部分内容也会涉及到腾讯研发的X5内核(X5内核基于安卓的WebView,目前已经在手机QQ等产品中使用,基于X5内核的项目累计有数亿UV,上百亿PV)。\n\n一个页面的显示,实际上主要经历了下面的四个流程:\n\n加载 => 解析 => 排版 => 渲染\n\n实际上,这里的渲染主要是指排版之后到最后的上屏绘制(这个时候内容已经排版好了),一部分前端工程师通常会把一部分的排版工作理解到“渲染”的流程中(也就是下图中全部工作),实际上这个理解是不准确的。\n\n![](https://www.10000h.top/images/data_img/webRender/P6.PNG)\n\n目前,浏览器的渲染采用的是分块渲染的机制,所谓的分块渲染的机制,其实应该这么理解:\n\n* 浏览器首先把整个网页分成一些低分辨率的块,再把网页分成高分辨率的块,然后给这些块排列优先级。\n* 处在可视区域内的低分辨率块的优先级会比较高,会被较先绘制。\n* 之后浏览器会把高分辨率的块进行绘制,同样也是先绘制处于可视区域内的,再绘制可视区域外的(由近到远)。\n\n以上讲的这些策略可以使可以使得浏览器优先展示可视区域内的内容,并且先展示大致内容,再展示高精度内容(当然,由于这个过程比较快,实际上我们大多时候是感受不到的)。\n\n另外这里值得提醒的一点是,分块的优先级是会根据到可视区域的距离来决定的,所以有些横着的内容(比如banner的滚动实现,通常会设置横向超出屏幕来表示隐藏),也是会按照到可视区域的距离来决定优先级的。\n\n绘制的过程,可以被硬件加速,这里硬件加速的主要手段主要是指:\n\n* 硬件加速合成上屏\n* 2D Canvas、Video的硬件加速\n* GPU光栅化\n\t* GPU光栅化速度更快,内存和CPU的消耗更少\n\t* 目前还没有办法对包含复杂矢量绘制的页面进行GPU光栅化\n\t* GPU光栅化是未来趋势\n\n\n### chrome内核架构变迁\n\n在渲染架构上,chrome也是经历了诸多变迁,早期的Chrome是这样的:\n\n![](https://www.10000h.top/images/data_img/webRender/P1.PNG)\n\n早期的chrome的架构实际上有以下缺点:\n\n* Renderer线程任务繁重\n* 无法实时响应缩放滑动操作\n* 脏区域与滑动重绘区域有冲突\n\t* 这里举个场景,假设一个gif,这个时候如果用户滑动,滑动新的需要绘制的内容和gif下一帧内容就会产生绘制冲突\n\n当然,经过一系列的发展,Chrome现在是这样的:\n\n![](https://www.10000h.top/images/data_img/webRender/P2.PNG)\n\n在安卓上,Android 4.4的 Blink内核架构如下(4.4之前并不支持OpenGL)\n\n![](https://www.10000h.top/images/data_img/webRender/P3.PNG)\n\n当然,这种架构也有如下缺点:\n\n* UI线程过于繁忙\n* 无法支持Canvas的硬件加速以及WebGL\n\n所以,后期发展成了这样:\n\n![](https://www.10000h.top/images/data_img/webRender/P4.PNG)\n\n总结看来,内核发展的趋势是:\n\n* 多线程化(可以充分利用多核心CPU)\n* 硬件加速(可以利用GPU)\n\n### 分层渲染\n\n在阅读这一章之前,我建议读者先去亲自体验一下所谓的“分层渲染”:\n\n>打开Chrome浏览器,打开控制台,找到\"Layers\",如果没有,那么在控制台右上角更多的图标->More tools 找到\"Layers\",然后随便找个网页打开即可\n\n网页的分层渲染流程主要是下面这样的:\n\n![](https://www.10000h.top/images/data_img/webRender/P7.PNG)\n\n(*注意:多个RenderObject可能又会对应一个或多个RenderLayer*)\n\n既然才用了分层渲染,那么肯定可以来分层处理,分层渲染有如下优点:\n\n* 减少不必要的重新绘制\n* 可以实现较为复杂的动画\n* 能够方便实现复杂的CSS样式\n\n当然,分层渲染是会很影响渲染效率的,可以有好的影响,使用不当也会有差的影响,我们需要合理的控制和使用分层:\n\n* 如果小豆腐块分层较多,页面整体的分层数量较大,会导致每帧渲染时遍历分层和计算分层位置耗时较长啊(比较典型的是腾讯网移动端首页)。\n* 如果可视区域内分层太多且需要绘制的面积太大,渲染性能非常差,甚至无法达到正常显示的地步(比如有一些全屏H5)。\n* 如果页面几乎没有分层,页面变化时候需要重绘的区域较多。元素内容无变化只有位置发生变化的时候,可以利用分层来避免重绘。\n\n那么,是什么原因可以导致分层呢?目前每一个浏览器或者不同版本的浏览器分层策略都是有些不同的(虽然总体差不太多),但最常见的几个分层原因是:transform、Z-index;还有可以使用硬件加速的video、canvas;fixed元素;混合插件(flash等)。关于其他更具体的内容,可以见下文。\n\n```\n//注:Chrome中符合创建新层的情况:\nLayer has 3D or perspective transform CSS properties(有3D元素的属性)\nLayer is used by <video> element using accelerated video decoding(video标签并使用加速视频解码)\nLayer is used by a <canvas> element with a 3D context or accelerated 2D context(canvas元素并启用3D)\nLayer is used for a composited plugin(插件,比如flash)\nLayer uses a CSS animation for its opacity or uses an animated webkit transform(CSS动画)\nLayer uses accelerated CSS filters(CSS滤镜)\nLayer with a composited descendant has information that needs to be in the composited layer tree, such as a clip or reflection(有一个后代元素是独立的layer)\nLayer has a sibling with a lower z-index which has a compositing layer (in other words the layer is rendered on top of a composited layer)(元素的相邻元素是独立layer)\n```\n\n最后,我们总结一下如何合理的设计分层:分层总的原则是,减少渲染重绘面积与减少分层个数和分层总面积:\n\n* 相对位置会发生变化的元素需要分层(比如banner图、滚动条)\n* 元素内容更新比较频繁的需要分层(比如页面中夹杂的倒计时等)\n* 较长较大的页面注意总的分层个数\n* 避免某一块区域分层过多,面积过大\n\n(*如果你给一个元素添加上了-webkit-transform: translateZ(0);或者 -webkit-transform: translate3d(0,0,0);属性,那么你就等于告诉了浏览器用GPU来渲染该层,与一般的CPU渲染相比,提升了速度和性能。(我很确定这么做会在Chrome中启用了硬件加速,但在其他平台不做保证。就我得到的资料而言,在大多数浏览器比如Firefox、Safari也是适用的)*)\n\n另外值得一提的是,X5对分层方面做了一定的优化工作,当其检测到分层过多可能会出现显示问题的时候会进行层合并,牺牲显示性能换取显示正确性。\n\n最后再提出一个小问题:\n\n以下哪种渲染方式是最优的呢?\n\n![](https://www.10000h.top/images/data_img/webRender/P8.PNG)\n\n这里实际上后者虽然在分层上满足总体原则,但是之前讲到浏览器的分块渲染机制,是按照到可视区域的距离排序的,考虑到这个因素,实际上后者这种方式可能会对分块渲染造成一定的困扰,并且也不是最优的。\n\n### 动画 & canvas & WebGl\n\n讲最后一部分开始,首先抛出一个问题:CSS动画 or JS动画?\n\n对内核来说,实际上就是Renderer线程动画还是Compositor线程动画,二者实际上过程如下:\n\n![](https://www.10000h.top/images/data_img/webRender/P9.PNG)\n\n所以我们可以看出,Renderer线程是比Compositor线程动画性能差的(在中低端尤其明显)\n\n另外,无论是JS动画还是CSS动画,动画过程中的重绘以及样式变化都会拖慢动画执行以及引起卡顿\n以下是一些不会触发重绘或者排版的CSS动画属性:\n\n* cursor\n* font-variant\n* opacity\n* orphans\n* perspective\n* perspecti-origin\n* pointer-events\n* transform\n* transform-style\n* widows\n\n想要了解更多内容,可以参考[这里](https://csstriggers.com/)\n\n这方面最终的建议参考如下:\n\n* 尽量使用不会引起重绘的CSS属性动画,例如transform、opacity等\n* 动画一定要避免触发大量元素重新排版或者大面积重绘\n* 在有动画执行时,避免其他动画不相关因素引起排版和重绘\n\n\n#### requestAnimationFrame\n\n另外当我们在使用动画的时候,为了避免出现掉帧的情况,最好采用requestAnimationFrame这个API,这个API迎合浏览器的流程,并且能够保证在下一帧绘制的时候上一帧一定出现了:\n\n![](https://www.10000h.top/images/data_img/webRender/P11.PNG)\n\n### 3D canvas\n\n还有值得注意的是,有的时候我们需要涉及大量元素的动画(比如雪花飘落、多个不规则图形变化等),这个时候如果用CSS动画,Animation动画的元素很多。,导致分层个数非常多,浏览器每帧都需要遍历计算所有分层,导致比较耗时、\n\n这个时候该怎么办呢?\n\n2D canvas上场。 \n\n和CSS动画相比,2D canvas的优点是这样的:\n\n* 硬件加速渲染\n* 渲染流程更优\n\n其渲染流程如下:\n\n![](https://www.10000h.top/images/data_img/webRender/P10.PNG)\n\n实际上以上流程比较耗时的是JS Call这一部分,执行opengl的这一部分还是挺快的。\n\nHTML 2D canvas 主要绘制如下三种元素:\n\n* 图片\n* 文字\n* 矢量\n\n这个过程可以采用硬件加速,硬件加速图片绘制的主要流程:\n\n![](https://www.10000h.top/images/data_img/webRender/P12.PNG)\n\n硬件加速文字绘制的主要流程:\n\n![](https://www.10000h.top/images/data_img/webRender/P13.PNG)\n\n但对于矢量绘制而言,简单的图形,比如点、直线等可以直接使用OpenGL渲染,复杂的图形,如曲线等,无法采用OpenGL绘制。\n\n对于绘制效率来说,2D Canvas对绘制图片效率较高,绘制文字和矢量效率较低(**所以建议是,我们如果能使用贴图就尽量使用贴图了**)\n\n还有,有的时候我们需要先绘制到离屏canvas上面,然后再上屏,这个可以充分利用缓存。\n\n### 3D canvas(WebGL)\n\n目前,3D canvas(WebGL)的应用也越来越多,对于这类应用,现在已经有了不少已经成型的庫:\n\n\n* 通用引擎:threeJS、Pixi\n* VR视频的专业引擎:krpano、UtoVR\n* H5游戏引擎:Egret、Layabox、Cocos\n\nWebGL虽然包含Web,但本身对前端的要求最低,但是对OpenGL、数学相关的知识要求较高,所以如果前端工程师没有一定的基础,还是采用现在的流行庫。\n\nX5内核对于WebGl进行了性能上和耗电上的优化,并且也对兼容性错误上报和修复做了一定的工作。\n\n___\n\n本文参考腾讯内部讲座资料整理而成,并融入一部分笔者的补充,谢绝任何形式的转载。\n\n其他优质好文:\n\n[Javascript高性能动画与页面渲染](http://qingbob.com/javascript-high-performance-animation-and-page-rendering/)\n\n\n","tags":["性能优化"]},{"title":"JS的静态作用域、子程序引用环境与参数传递类型","url":"/sheldon_blog/passages/JS的静态作用域、子程序引用环境与参数传递类型/","content":"#### 静态作用域\n\n我们先来看下面这个小程序:\n\n```\n //JS版本:\n function sub1() {\n var x;\n function sub2() { alert(x); }\n function sub3() { var x; x=3; sub4(sub2); }\n function sub4(subx) { var x; x=4; subx(); }\n x = 1;\n sub3();\n }\n\n sub1();\n \n #Python版本\ndef sub1():\n def sub2():\n print x\n def sub3():\n x=3\n sub4(sub2)\n def sub4(subx):\n x=4\n subx()\n x = 1\n sub3()\n\nsub1() \n```\n\n不用亲自运行,实际上输出结果都是1,这可能不难猜到,但是需要解释一番,鉴于Python和JS在这一点上表现的类似,我就以JS来分析。\n\n我们知道,JS是静态作用域的,所谓静态作用域就是作用域在编译时确定,所以sub2中引用的x,实际上和x=3以及x=4的x没有任何关系,指向第二行的var x;\n\n#### 子程序的引用环境\n\n实际上这里面还有一个子程序(注:子程序和函数不是很一样,但我们可以认为子程序包括函数,也约等于函数)的概念,sub2、sub3、sub4都是子程序,对于允许嵌套子程序的语言,应该如何使用执行传递的子程序的引用环境?\n\n* 浅绑定:如果这样的话,应该输出4,这对动态作用域的语言来说比较自然。\n* 深绑定:也就是输出1的情况,这对静态作用域的语言来说比较自然。\n* Ad hoc binding: 这是第三种,将子程序作为实际参数传递到调用语句的环境。\n\n#### 参数传递类型\n\n参数传递类型我们普遍认为有按值传递和按引用传递两种,实际上不止。\n\n下面是一张图:\n\n![](https://www.10000h.top/images/call.png)\n\n这张图对应的第一种传递方式,叫做Pass-by-Value(In mode),第二种是Pass-by-Result(Out mode),第三种是Pass-by-Value-Result(Inout mode),图上说的比较明白,实际上如果有result就是说明最后把结果再赋值给参数。\n\n第二种和第三种编程语言用的少,原因如下:\n>Potential problem: sub(p1, p1) \nWith the two corresponding formal parameters having different names, whichever formal parameter is copied back last will represent current value of p1\n\n","tags":["javascript"]},{"title":"CentOS7下安装和配置redis","url":"/sheldon_blog/passages/CentOS7下安装和配置redis/","content":"Redis是一个高性能的,开源key-value型数据库。是构建高性能,可扩展的Web应用的完美解决方案,可以内存存储亦可持久化存储。因为要使用跨进程,跨服务级别的数据缓存,在对比多个方案后,决定使用Redis。顺便整理下Redis的安装过程,以便查阅。\n\n\n 1 . 下载Redis\n目前,最新的Redist版本为3.0,使用wget下载,命令如下:\n```\n\n# wget http://download.redis.io/releases/redis-3.0.4.tar.gz\n\n```\n 2 . 解压Redis\n下载完成后,使用tar命令解压下载文件:\n```\n\n# tar -xzvf redis-3.0.4.tar.gz\n```\n3 . 编译安装Redis\n切换至程序目录,并执行make命令编译:\n```\n# cd redis-3.0.4\n# make\n```\n执行安装命令\n```\n# make install\n```\nmake install安装完成后,会在/usr/local/bin目录下生成下面几个可执行文件,它们的作用分别是:\n\n* redis-server:Redis服务器端启动程序\n* redis-cli:Redis客户端操作工具。也可以用telnet根据其纯文本协议来操作\n* redis-benchmark:Redis性能测试工具\n* redis-check-aof:数据修复工具\n* redis-check-dump:检查导出工具\n\n备注\n\n有的机器会出现类似以下错误:\n```\nmake[1]: Entering directory `/root/redis/src'\nYou need tcl 8.5 or newer in order to run the Redis test\n……\n```\n这是因为没有安装tcl导致,yum安装即可:\n```\nyum install tcl\n```\n4 . 配置Redis\n复制配置文件到/etc/目录:\n```\n# cp redis.conf /etc/\n```\n为了让Redis后台运行,一般还需要修改redis.conf文件:\n```\nvi /etc/redis.conf\n```\n修改daemonize配置项为yes,使Redis进程在后台运行:\n```\ndaemonize yes\n```\n5 . 启动Redis\n配置完成后,启动Redis:\n```\n# cd /usr/local/bin\n# ./redis-server /etc/redis.conf\n```\n检查启动情况:\n```\n# ps -ef | grep redis\n```\n看到类似下面的一行,表示启动成功:\n```\nroot 18443 1 0 13:05 ? 00:00:00 ./redis-server *:6379 \n```\n6 . 添加开机启动项\n让Redis开机运行可以将其添加到rc.local文件,也可将添加为系统服务service。本文使用rc.local的方式,添加service请参考:Redis 配置为 Service 系统服务 。\n\n为了能让Redis在服务器重启后自动启动,需要将启动命令写入开机启动项:\n```\necho \"/usr/local/bin/redis-server /etc/redis.conf\" >>/etc/rc.local\n```\n7 . Redis配置参数\n在 前面的操作中,我们用到了使Redis进程在后台运行的参数,下面介绍其它一些常用的Redis启动参数:\n```\ndaemonize:是否以后台daemon方式运行\npidfile:pid文件位置\nport:监听的端口号\ntimeout:请求超时时间\nloglevel:log信息级别\nlogfile:log文件位置\ndatabases:开启数据库的数量\nsave * *:保存快照的频率,第一个*表示多长时间,第三个*表示执行多少次写操作。在一定时间内执行一定数量的写操作时,自动保存快照。可设置多个条件。\nrdbcompression:是否使用压缩\ndbfilename:数据快照文件名(只是文件名)\ndir:数据快照的保存目录(仅目录)\nappendonly:是否开启appendonlylog,开启的话每次写操作会记一条log,这会提高数据抗风险能力,但影响效率。\nappendfsync:appendonlylog如何同步到磁盘。三个选项,分别是每次写都强制调用fsync、每秒启用一次fsync、不调用fsync等待系统自己同步\n```\n","tags":["redis"]},{"title":"腾讯云北美服务器搭建ShadowSocks代理","url":"/sheldon_blog/passages/腾讯云北美服务器搭建ShadowSocks代理/","content":"\n注:本教程适合centos系列和red hat系列\n\n登陆SSH \n新的VPS可以先升级\n\n```\nyum -y update\n```\n\n有些VPS 没有wget \n这种要先装\n\n```\nyum -y install wget\n```\n\n输入以下命令:(可以复制)\n\n```\nwget --no-check-certificate https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks.sh\nchmod +x shadowsocks.sh\n./shadowsocks.sh 2>&1 | tee shadowsocks.log\n```\n\n第一行是下载命令,下载东西,第二行是修改权限,第三行是安装命令\n\n下面是按照配置图\n\n```\n配置:\n密码:(默认是teddysun.com)\n端口:默认是8989\n然后按任意键安装,退出按 Ctrl+c\n```\n\n安装完成会有一个配置\n\n```\nCongratulations, shadowsocks install completed!Your Server IP: ***** VPS的IP地址Your Server Port: ***** 你刚才设置的端口Your Password: **** 你刚才设置的密码Your Local IP: 127.0.0.1 Your Local Port: 1080 Your Encryption Method: aes-256-cfb Welcome to visit:https://teddysun.com/342.htmlEnjoy it!\n```\n\n然后即可以使用\n\n卸载方法:\n\n使用 root 用户登录,运行以下命令:\n\n```\n./shadowsocksR.sh uninstall\n```\n\n安装完成后即已后台启动 ShadowsocksR ,运行:\n\n```\n/etc/init.d/shadowsocks status\n```\n","tags":["ShadowSocks"]},{"title":"centOS7.2搭建nginx环境以及负载均衡","url":"/sheldon_blog/passages/centOS7-2搭建nginx环境以及负载均衡/","content":" 之所以要整理出这篇文章,是因为1是搭建环境的过程中会遇到大大小小各种问题,2是网上目前也没有关于centos7.2搭建nginx环境的问题整理,因此在这里记录。\n\n前置工作就不赘述了,首先`ssh [email protected]` (换成你们自己的公网IP)登陆进入到自己的服务器命令行,之后开始基本的安装:\n\n**1.添加资源**\n\n添加CentOS 7 Nginx yum资源库,打开终端,使用以下命令(没有换行):\n\n```\nsudo rpm -Uvh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm\n\n```\n\n**2.安装Nginx**\n\n在你的CentOS 7 服务器中使用yum命令从Nginx源服务器中获取来安装Nginx:\n>*这里有一个需要注意的地方,尽量不要用网上的下载源码包然后再传到服务器上的方式进行安装,因为nginx已经不算是简单的Linux了,做了很多扩展,这个时候如果你用源码包安装会出现各种各样的问题,尽量用已经封装好的rpm\\yum进行安装*\n```\nsudo yum install -y nginx\n```\nNginx将完成安装在你的CentOS 7 服务器中。\n\n**3.启动Nginx**\n\n刚安装的Nginx不会自行启动。运行Nginx:\n```\nsudo systemctl start nginx.service\n```\n如果一切进展顺利的话,现在你可以通过你的域名或IP来访问你的Web页面来预览一下Nginx的默认页面\n\n>当然,这里一般很可能会无法访问的。\n\n我们先不急于解决我们的问题,先看看nginx的基本配置:\n\n\nNginx配置信息\n```\n网站文件存放默认目录\n\n/usr/share/nginx/html\n网站默认站点配置\n\n/etc/nginx/conf.d/default.conf\n自定义Nginx站点配置文件存放目录,自己在这里也可以定义别的名字的.conf,这个的作用以后再说。\n\n/etc/nginx/conf.d/\nNginx全局配置\n\n/etc/nginx/nginx.conf\n在这里你可以改变设置用户运行Nginx守护程序进程一样,和工作进程的数量得到了Nginx正在运行,等等。\n```\nLinux查看公网IP\n\n您可以运行以下命令来显示你的服务器的公共IP地址:(这个其实没用,不是公网IP)\n```\nip addr show eth0 | grep inet | awk '{ print $2; }' | sed 's/\\/.*$//'\n```\n___\n好了,这个时候我们再来看看可能遇到的问题:无法在公网访问。\n\n这个时候首先看看配置文件default.conf对不对,一个正确的例子:\n(域名要先进行解析到响应的IP)\n```\nserver {\n listen 80;\n server_name nginx.310058.cn;\n\n #charset koi8-r;\n #access_log /var/log/nginx/log/host.access.log main;\n\n location / {\n root /usr/share/nginx/html;\n index index.html index.htm;\n }\n\n #error_page 404 /404.html;\n\n # redirect server error pages to the static page /50x.html\n #\n error_page 500 502 503 504 /50x.html;\n location = /50x.html {\n root /usr/share/nginx/html;\n }\n\n # proxy the PHP scripts to Apache listening on 127.0.0.1:80\n #\n #location ~ \\.php$ {\n # proxy_pass http://127.0.0.1;\n #}\n\n # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n #\n #location ~ \\.php$ {\n # root html;\n # fastcgi_pass 127.0.0.1:9000;\n # fastcgi_index index.php;\n # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;\n # include fastcgi_params;\n #}\n\n # deny access to .htaccess files, if Apache's document root\n # concurs with nginx's one\n #\n #location ~ /\\.ht {\n # deny all;\n #}\n}\n```\n\n确定文件没问题了,看看这个时候是不是开启了nginx进程:\n\n```\n ps -ef | grep nginx\n```\n\n应该会输出一个或者多个进程,如果没有的话就开启或者重启试试看。\n\n这个时候接下来再试试在服务器上:\n```\nping 115.29.102.81\ntelnet 115.29.102.81 80\nwget nginx.310058.cn\n```\n如果有的命令没有就直接yum安装下:\n```\nyum -y install telnet\n```\n如果都可以的话,之后在本机尝试以上三行。如果没有命令也要安装下:\n```\nbrew install wget\n```\n\n发现很可能本机telnet不通,而服务器telnet通。\n这个时候就是**防火墙**的问题。\n\n####centos7.2防火墙\n\n由于centos 7版本以后默认使用firewalld后,网上关于iptables的设置方法已经不管用了,所以根本就别想用配置iptables做啥,根本没用。\n\n查看下防火墙状态:\n```\n[root@iZ28dcsp7egZ conf.d]# systemctl status firewalld \n● firewalld.service - firewalld - dynamic firewall daemon\n Loaded: loaded (/usr/lib/systemd/system/firewalld.service; enabled; vendor preset: enabled)\n Active: active (running) since Wed 2016-08-03 12:06:44 CST; 2h 49min ago\n Main PID: 424 (firewalld)\n CGroup: /system.slice/firewalld.service\n └─424 /usr/bin/python -Es /usr/sbin/firewalld --nofork --nopid\n\nAug 03 12:06:41 iZ28dcsp7egZ systemd[1]: Starting firewalld - dynamic firewall daemon...\nAug 03 12:06:44 iZ28dcsp7egZ systemd[1]: Started firewalld - dynamic firewall daemon.\n```\n\n增加80端口的权限:\n```\nfirewall-cmd --zone=public --add-port=80/tcp --permanent \n```\n \n 别忘了更新防火墙的配置:\n```\nfirewall-cmd --reload\n```\n这个时候再`restart nginx.service` 一下就会发现应该好了。\n\n\nnginx 停止:\n\n```\nservice nginx restart\n也可以重启nginx\n\nkill -QUIT 进程号 \n#从容停止\n\nkill -TERM 进程号\n#或者\nkill -INT 进程号\n#快速停止\n\np-kill -9 nginx\n强制停止\n\nnginx -t \n#验证配置文件 前提是进入相应的配置的目录(自己实际测试的时候发现没有进入相应的配置目录也是可以的)\n\nnginx -s reload\n#重启\n\nkill -HUP 进程号\n#重启的另外一种方式\n```\n\n官方文档地址:\nhttps://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Security_Guide/sec-Using_Firewalls.html#sec-Introduction_to_firewalld\n\n附1:一个简单的负载均衡的实现:\nweight默认是1,自己也可以更改。\n```\nupstream mypro {\n\t\t\t\tip_hash;\n server 111.13.100.92 weight=2;\n server 183.232.41.1;\n server 42.156.140.7;\n }\n\n server {\n listen 8090;\n location / {\n proxy_pass http://mypro;\n }\n }\n\n```\n\n\n附2:防火墙基本学习:\n\n``` \n\n1、firewalld简介\nfirewalld是centos7的一大特性,最大的好处有两个:支持动态更新,不用重启服务;第二个就是加入了防火墙的“zone”概念\n \nfirewalld有图形界面和工具界面,由于我在服务器上使用,图形界面请参照官方文档,本文以字符界面做介绍\n \nfirewalld的字符界面管理工具是 firewall-cmd \n \nfirewalld默认配置文件有两个:/usr/lib/firewalld/ (系统配置,尽量不要修改)和 /etc/firewalld/ (用户配置地址)\n \nzone概念:\n硬件防火墙默认一般有三个区,firewalld引入这一概念系统默认存在以下区域(根据文档自己理解,如果有误请指正):\ndrop:默认丢弃所有包\nblock:拒绝所有外部连接,允许内部发起的连接\npublic:指定外部连接可以进入\nexternal:这个不太明白,功能上和上面相同,允许指定的外部连接\ndmz:和硬件防火墙一样,受限制的公共连接可以进入\nwork:工作区,概念和workgoup一样,也是指定的外部连接允许\nhome:类似家庭组\ninternal:信任所有连接\n对防火墙不算太熟悉,还没想明白public、external、dmz、work、home从功能上都需要自定义允许连接,具体使用上的区别还需高人指点\n \n2、安装firewalld\nroot执行 # yum install firewalld firewall-config\n \n3、运行、停止、禁用firewalld\n启动:# systemctl start firewalld\n查看状态:# systemctl status firewalld 或者 firewall-cmd --state\n停止:# systemctl disable firewalld\n禁用:# systemctl stop firewalld\n \n4、配置firewalld\n查看版本:$ firewall-cmd --version\n查看帮助:$ firewall-cmd --help\n查看设置:\n 显示状态:$ firewall-cmd --state\n 查看区域信息: $ firewall-cmd --get-active-zones\n 查看指定接口所属区域:$ firewall-cmd --get-zone-of-interface=eth0\n拒绝所有包:# firewall-cmd --panic-on\n取消拒绝状态:# firewall-cmd --panic-off\n查看是否拒绝:$ firewall-cmd --query-panic\n \n更新防火墙规则:# firewall-cmd --reload\n # firewall-cmd --complete-reload\n 两者的区别就是第一个无需断开连接,就是firewalld特性之一动态添加规则,第二个需要断开连接,类似重启服务\n \n将接口添加到区域,默认接口都在public\n# firewall-cmd --zone=public --add-interface=eth0\n永久生效再加上 --permanent 然后reload防火墙\n \n设置默认接口区域\n# firewall-cmd --set-default-zone=public\n立即生效无需重启\n \n打开端口(貌似这个才最常用)\n查看所有打开的端口:\n# firewall-cmd --zone=dmz --list-ports\n加入一个端口到区域:\n# firewall-cmd --zone=dmz --add-port=8080/tcp\n若要永久生效方法同上\n \n打开一个服务,类似于将端口可视化,服务需要在配置文件中添加,/etc/firewalld 目录下有services文件夹,这个不详细说了,详情参考文档\n# firewall-cmd --zone=work --add-service=smtp\n \n移除服务\n# firewall-cmd --zone=work --remove-service=smtp\n \n还有端口转发功能、自定义复杂规则功能、lockdown,由于还没用到,以后再学习\n\n```\n","tags":["nginx"]}]