暂时未有相关云产品技术能力~
一、从 1.0 到 2.0 在官方文档中,有专门一页讲如何升级的,这个用户体验非常好。 一个清单列的非常清楚,内容不多,让我信心大增。并且自己之前也曽依托 umi 2.0开源过一套系统。 所以在实际操作中,升级遇到的阻力没有我想象中的那么大,但期间还是遇到了些难缠的问题,诸如页面空白,文件不存在等。 具体的改造其实就那么几步,升级和替换依赖库,更正路由配置,去除过时文件等。 改造好后,自己粗略的刷刷页面,没啥问题,然后就开心地发布到预发环境。但是在生成source map文件时,报内存栈溢出。 source map文件主要用于监控系统中的代码还原,在实际使用中用的比较少,那就先暂时关闭了。 不过在生产发布的时候,又会报没有source map文件,因为生产有个将文件搬移到指定位置的脚本,得把这个脚本也关闭。二、从 2.0 到 3.0 为了能更好的引入TypeScript,提升项目代码质量的主动升级,根据官方给出的升级文档进行了改造。1)router改造 原先 component 中的路径可以包含斜杠,现在不行,因为 Recommend 目录中没有默认的 index.js 文件。{ path: '/video/recommend', exact: true, component: 'video/Recommend/', } 改成 { path: '/video/recommend', exact: true, component: 'video/Recommend', }2)model.js验证 默认会做 model 文件的验证,但是我有个文件中包含 jsx 代码,导致无法验证通过,会报解析错误。Dva model src/models/playViewer.js parse failed, SyntaxError: Unexpected token, expected "," (71:20) 后面就在 .umirc.js 配置文件中取消了验证,skipModelValidate 设置为 true。export default { antd: {}, dva: { // 启用引入dva skipModelValidate: true //跳过 model 验证 }, }3)namespace不唯一 项目构建的时候,报model的namespace重名的错误,因为 pages 子目录中的文件名都叫model.js。pages foo/model.js bar/model.js 下面是具体的报错信息,查了框架的issue,发现还蛮多人有这问题的,但是我都升级到最新版本还是有问题。./src/.umi/plugin-dva/dva.ts [app.model] namespace should be unique app.model({ namespace: 'model', ...ModelModel29 }); app.model({ namespace: 'model', ...ModelModel30 }); app.model({ namespace: 'model', ...ModelModel31 }); app.model({ namespace: 'model', ...ModelModel32 }); 后面发现将文件改个名字,然后放到models目录中,就不会唯一了。但是有300多张页面,都得手动处理。pages foo/ models/login.js bar/model.js 没有找到更有效更直接的方法,只能用这种笨办法了,弄了好几个小时。 中途也发现,很多页面已经废弃了,马上决定移除。4)Link组件 之前Link组件都是从react-router-dom导入的,但现在会报错:Error: Invariant failed: You should not use <Link> outside a <Router>。 这个好弄,只要换个库就行。 - import { Link } from 'react-router-dom'; + import { Link } from 'umi';5)警告 有个比较奇怪的警告,会一直提示,网上很多重复的帖子就是去本地 dva 库替换某条语句,还有就是升级到 2.6 版本。Warning: Please use `require("history").createHashHistory` instead of `require("history/createHashHistory")`. Support for the latter will be removed in the next major release. 我升级到最新后,还是会有这个提示,就看源码,发现注释掉 dva/router.js 的第一句就行了,不过发到线上后,就不会有这个警告了。require('./warnAboutDeprecatedCJSRequire.js')('router'); module.exports = require('react-router-dom'); module.exports.routerRedux = require('connected-react-router');
一、JMeter 压测工具选择了JMeter,这是Apache的一个项目,它是用Java编写的,所以需要先安装Java的SDK,选择当前的操作系统。 随后到JMeter官网下载应用程序,选择 Binaries 中的压缩包。 在终端中进入解压后的 bin 目录,通过 sh jmeter 命令来启动 JMeter。 Don't use GUI mode for load testing:这段提示信息是不要在GUI界面进行压力测试,GUI界面仅仅用于调试。 程序会自动打开 JMeter 的界面,如果在 选项 -》 选择语言 -》中文,那么有可能乱码。 只需选择 选项 -》 外观 -》System 或 Metal,就能避免乱码,网上有许多使用教程可以参考。 当测试计划都编写完后,保存,然后在终端输入命令,就能开始压测了,其中目录相对于bin,couples.jmx 是测试计划,webreport是统计信息。sh jmeter -n -t ../demo/couples.jmx -l ../demo/result/couples.txt -e -o ../demo/webrepo二、实践 在正式开始压测之前,也浏览了许多网络资料作为知识储备。 首先需要理解Socket(套接字)的概念,它是对TCP/IP协议的封装,本身并不是协议,而是一个调用接口,Socket连接就是长连接。 在创建Socket连接时,可以指定传输层协议,通常选择的是TCP协议,所以一旦通信双方建立连接后就开始互发数据,直至连接断开。 而每个TCP都要占用一个唯一的本地端口号,但是每个端口并不会禁止TCP并发。 然后去网上搜索了百万长连接可能遇到的瓶颈,包括TCP连接数、内存大小、文件句柄打开数等,例如: 每个TCP连接都要占用一个文件描述符,而操作系统对可以打开的最大文件数的限制将会成为瓶颈。 如果对本地端口号范围有限制(例如在1024~32768),当端口号占满时,TCP就会连接失败。 网上给出了很多解决方案,大部分都是修改操作系统的各类参数。1)开始测试 上来就干,线程数直接填200以上。 红框中的字段含义如下所示:Label: 请求名称#Smaples: 请求计数,其中108.4是TPS(每秒处理的事务数)Average: 请求响应平均耗时Min: 请求响应最小耗时Max: 请求响应最大耗时Error %: 请求错误率Active:线程数(图中并未显示) 查看报告页面,出现了多个错误,在网上查资源,做了些简单地挣扎,并没有得到好的解决办法。Non HTTP response code: java.net.SocketException/Non HTTP response message: Connection reset Non HTTP response code: javax.net.ssl.SSLHandshakeException/Non HTTP response message: Remote host terminated the handshake Non HTTP response code: javax.net.ssl.SSLException/Non HTTP response message: java.net.SocketException: Connection reset Non HTTP response code: java.net.SocketException/Non HTTP response message: Malformed reply from SOCKS server 后面想想还是根据当前实际情况来吧,运营需要50W的推送,两小时内完成,平均每秒推送70条,将这个数据作为当前每秒的线程数,模拟后一切正常。 注意,线程数和服务器的并发量不能完全画等号。 然后让4000个线程1分钟完成请求,配置Ramp-Up时间为60S,成功率是99.93%。 图中的Ramp-Up时间指所有线程在多长时间(单位秒)内全部启动。例如500个线程10S,那么每秒启动 500/10=50 个线程,不写就是所有线程在开启场景后立即启动。 再让5000的线程维持2分钟,配置Ramp-Up时间为120S,报无法创建新的本机线程的错误。 Uncaught Exception java.lang.OutOfMemoryError: unable to create new native thread in thread Thread[StandardJMeterEngine,5,main] 为了解决此问题,期间走了很多误区,网上的很多资料都是说修改 jmeter.sh文件,像下面这样,但是改来改去仍然会报那错。set HEAP=-server -Xms768m -Xmx768m -Xss128k set NEW=-XX:NewSize=1024m -XX:MaxNewSize=1024m 或者是用命令来修改本机的一些参数,像下面这样,但仍然无济于事。 launchctl limit maxfiles 1000000 1000000 sysctl -w kern.maxfiles=100000 sysctl -w kern.maxfilesperproc=100000 后面看到篇文章说在macOS中,对单个进程能够创建的线程数量是有限制的,下面的命令可以读取最大值,例如本机是4096,但该参数是只读的,无法修改。sysctl kern.num_taskthreads 于是马上就改变策略,一番查找下来,了解到JMeter还提供了一种远程模式。2)远程模式 既然一台机器的线程数有限,那可以通过多台机器来模拟更多的虚拟用户,JMeter有一种远程模式可以实现这个方案。 首先需要在bin目录中的 jmeter.properties 文件修改remote_hosts参数,127.0.0.1改成本机地址,如下所示。remote_hosts=192.168.10.10,192.168.10.46 然后通过bin目录的create-rmi-keystore.sh生成rmi_keystore.jks,windows的可以直接运行create-rmi-keystore.bat,mac需要运行create-rmi-keystore.sh文件,会问你一堆问题。 sh create-rmi-keystore.sh 并且需要将rmi_keystore.jks文件放置到从机的bin目录中。此时从机在开启sh jmeter-server时会报一个错误。An error occurred: Cannot start. MacBook-Pro.local is a loopback address. 修改jmeter-server,取消RMI_HOST_DEF的注释项,并将IP地址改成当前机器的。RMI_HOST_DEF=-Djava.rmi.server.hostname=192.168.10.46 一切准备就绪后,就可以使用压测命令了,与之前不同的是,需要加一个 -r 参数,其余照旧。sh jmeter -n -t ../demo/couples.jmx -r -l ../demo/result/couples.txt -e -o ../demo/webreport3)继续测试 这次线程数量加到4000,加上从机,总共是1.2W个线程,Ramp-Up时间为60S,下面是结果图。 其中Throughput一列表示的是每秒处理的事务数(TPS),在此处也就是服务器的并发量。统计出21个错误,占比是0.17%。Non HTTP response code: javax.net.ssl.SSLException/Non HTTP response message: Connection reset 进到测试服务器,输入 ulimit -a 命令,open files 的数量有100多W,所以不会出现那种无法打开文件的错误。 再详细的分析暂时不会,还得先去系统的学习一下,然后再回来补充。三、学习性能测试 为了学习性能测试,特地在网上找了个专栏《性能测试实战30讲》,顺便记录了些基础概念。1)性能场景 基准性能场景,单交易容量,将每一个业务压到最大TPS。 容量性能场景,将所有业务根据比例加到一个场景中,在数据、软硬件、监控等的配合下,分析瓶颈并调优。 稳定性性能场景,核心就是时长,在长时间的运行之下,观察系统的性能表现。 异常性能场景,宕主机、宕应用、宕网卡、宕容器、宕缓存、宕队列等。2)指标RT:响应时间TPS:每秒事务数QPS:每秒SQL数RPS:每秒请求数Throughout:吞吐量 所有相关的人都要知道TPS中的T是如何定义的。如果是接口层性能测试,T直接定义为接口级;如果是业务级性能测试,T直接定义为每个业务步骤和完整的业务流。 对一个系统来说,如果仅在改变压力策略(其他的条件比如环境、数据、软硬件配置等都不变)的情况下,系统的最大 TPS 上限是固定的 TPS = (1000ms(1秒)/ RT(单位ms))x 压力线程数 对于压力工具来说,只要不报错,我们就关心 TPS 和响应时间就可以了,因为 TPS 反应出来的是和服务器对应的处理能力,至少压力线程数是多少,并不关键。3)学习期 性能工具学习期:自己有明确的疑问。通常所说的并发都是指服务端的并发,而不是指压力机上的并发线程数,因为服务端的并发才是服务器的处理能力。 性能场景学习期:如何做一个合理的性能测试,调整业务比例,参数化数据的提取逻辑。 性能分析学习期:面对问题应该是我想要看什么数据,而不是把数据都给我看看。 通过你的测试和分析优化之后,性能提升了多少? 通过你的测试和分析优化之后,节省了多少成本?4)参数化 参数化测试数据的疑问:参数化数据应该用多少数据量?参数化数据从哪里来?参数多与少的选择对系统压力有什么影响?参数化数据在数据库中的直方图是否均衡? 在性能场景中,我们需要根据实际的业务场景来分析需要用到什么样的数据,以便计算数据量。 参数化时需要确保数据来源以保证数据的有效性,千万不能随便造数据。这类数据应该满足两个条件:要满足生产环境中数据的分布;要满足性能场景中数据量的要求。四、Websocket Bench 在这次的压测中,想要测试2000人在线,并且同时聊天,服务器能否完美处理。 如果要访问页面模拟用户的行为,会比较麻烦,因为在聊天前需要做两步操作,第一步是确认协议,第二步是选择匹配范围,第三步才开始匹配用户开始聊天。 若要两个用户匹配成功,首先需要都在线,其次是经纬度计算后的范围满足之前的配置。 为了避免那么多繁琐的前置场景,我决定直接对socket进行压测,于是想到了Websocket Bench。 它支持Socket.IO、Engine.IO、Primus等实时通信库的方法,经过简单的文档查阅后,开始编码,直接将官方demo复制修改。module.exports = { /** * Before connection (optional, just for faye) * @param {client} client connection */ beforeConnect : function(client) { }, /** * On client connection (required) * @param {client} client connection * @param {done} callback function(err) {} */ onConnect : function(client, done) { // Socket.io client client.emit('say', 100, { id: 111, avatar: 'http://www.pwstrick.com', userId: 123, msg: Date.now().toString(36) + Math.random().toString(36).substr(2), msgType: 'text' }, (msg) => { console.log(msg); }); console.count(); done(); }, /** * Send a message (required) * @param {client} client connection * @param {done} callback function(err) {} */ sendMessage : function(client, done) { done(); }, /** * WAMP connection options */ options : { // realm: 'chat' } }; 启动命令,-a 是指持久化连接总数 ,-c 是指每秒并发连接数 ,-g 是指要执行的JS文件,-k 保持活动连接,-o 是指日志的输出文件。websocket-bench -a 2000 -c 2000 -g chat.js -k test-web-api.rela.me/chat -o opt.log 开始运行后,并没有我设想的那样,实现2000人并发,TPS最多也就80多,到一个时间后,就持续变少。下图来自阿里云的日志,每次发消息我都会记录一条日志。 我对上面的 -a 和 -c 的理解还有误差,不过也有可能是我本机限制了并发,之后就让QA在服务器上调试了。
当运营向我们上报BUG时,我们第一时间是捕获相关的接口。从监控系统中,就可以查到用户使用时接口的请求和响应数据。 若接口的请求正常,那么就需要深入到接口代码中,查看相关的日志,通常会先浏览数据库查询语句以及内部接口的通信日志。 在本地也可以查看到上述日志,但有个问题,有时候打开某个页面会报错,那是因为本地的数据库没有与测试或正式环境的同步。 可能是有些字段缺失了,也可能是某张表缺失了,情况比较多。所以,最保险的是在测试或正式环境查看。 在这两个环境中,都有日志管理系统,但日志量是非常巨大的,若要查找某一条记录,就得有非常精确的过滤条件,并且日志无法连续。 这条日志下面的一条,很可能是另外一个接口留下的,因此,需要一个小工具能查看到指定接口的日志,解决日常开发的一个痛点。一、搜集日志1)管理系统 首先需要将需要的日志搜集起来,我使用了一个比较简单的方法。 就是在启动文件中,新增一个全局的logMessages变量,声明为一个空数组。global.logMessages = []; 然后在MongoDB、MySQL、请求内部接口函数中,将他们的查询语句日志和通信日志塞入logMessages数组内。mongoose.set('debug', (...args) => { logger.debug(...args); global.logMessages.push(args); }); new Sequelize(database, username, password, { ...options, logging: (msg, benchmark) => { logger.debug(msg, `${benchmark}ms`); global.logMessages.push(msg); } }); 再新增一个中间件(Server项目基于KOA2),这个中间件的作用就是清空logMessages数组,免得将所有接口的日志都搜集起来,因为我只要一个接口的日志。export default () => async (ctx, next) => { //每次请求清空要读取的日志数组 global.logMessages.length = 0; await next(); }; 这么设计会有一个问题,服务器在处理多个请求(高并发)时,互相会影响各自的日志搜集,可能会出现这个接口日志中夹杂着另一个接口的日志,也可能是搜集到一半的日志就被清除了。 当然,在测试环境,这种情况可以控制住。但是测试环境有时候数据不完整,逻辑可能走不下去,得上生产环境,那生产环境就有概率出现上述问题。 后面将中间件去除,logMessages变量在一个接口中声明,这个接口就是下面界面中点提交时请求的接口,能解决日志被无故清除的问题,但还是会出现串线的问题。 生产环境暂时无解,好在还有一个预发环境,它使用的数据源和生产是相同的,只要保证代码和生产同步,那么就能得到想要的日志列表。2)Web API Web API是另一个接口服务,也需要监控其中的日志,但是它与之前的管理系统不同,它是一个独立的服务。 也就是说,我无法直接在管理系统中通过 global.logMessages 读取日志。 一开始是想将日志写入缓存中,然后在管理系统中读取缓存中的日志,不过这样做不仅太绕,平添复杂度,而且日志写法也会与之前的不一致。 于是否决了此方案,改用一个中间件,日志的写入和读取与之前保持一致。不同点是在一个中间件中,将日志作为响应的参数返回。 在下面的代码中,当需要日志时,我会带上一个特殊的参数:isLogMessages,只有这个参数存在时,才表示需要返回记录。export default () => async (ctx, next) => { // 只有带了特殊参数的请求,才会把接口日志带上 const isLogMessages = ctx.query.isLogMessages || ctx.request.body.isLogMessages; if(isLogMessages) { global.logMessages = []; } await next(); if(isLogMessages) { const { body } = ctx; if(typeof body === 'string') { ctx.body = { data: body }; }else { ctx.body = { ...body } } ctx.body.logMessages = global.logMessages; delete global.logMessages; }二、界面 在完成上述的日志搜集之后,就需要有一张操作界面(如图所示),提升我们组自己的用户体验,借助之前封装的模板组件,搭建这样一个页面几十分钟就好了。 界面中包括API路径、方法、项目和参数,其中参数可动态增加,点击提交就会开始模拟请求接口,得到日志(如图所示)和响应。 这样一套操作之后,就能马上知道接口内的细节,可帮助我们快速定位问题,也是一种降低时间成本的手段。
一、Redis基础1)知识图和问题画像图 Redis知识全景图都包括“两大维度,三大主线”。“两大维度”就是指系统维度和应用维度,“三大主线”也就是指高性能、高可靠和高可扩展。 高性能主线,包括线程模型、数据结构、持久化、网络框架;高可靠主线,包括主从复制、哨兵机制;高可扩展主线,包括数据分片、负载均衡。 Redis 各大典型问题,同时结合相关的技术点,手绘了一张 Redis 的问题画像图。按照“问题 --> 主线 --> 技术点”的方式梳理出来。2)数据结构 底层数据结构一共有 6 种,分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。 压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。 跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。 集合常见操作的复杂度:单元素操作是基础;范围操作非常耗时;统计操作通常高效;例外情况只有几个,例如压缩列表和双向链表都会记录表头和表尾的偏移量。3)单线程 Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。 多线程的开销,系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构。当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开销。 通常来说,单线程的处理能力要比多线程差很多,但是 Redis 却能使用单线程模型达到每秒数十万级别的处理能力。一方面,Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。另一方面,就是 Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。 在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。4)AOF和RDB Redis 的持久化主要有两大机制,即 AOF(Append Only File)日志和 RDB 快照。 AOF 日志正好相反,它是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志。 AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操作。 AOF 也有两个潜在的风险。首先,如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。其次,AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。二、实践1)string 当你保存 64 位有符号整数时,String 类型会把它保存为一个 8 字节的 Long 类型整数,这种保存方式通常也叫作 int 编码方式。 但是,当你保存的数据中包含字符时,String 类型就会用简单动态字符串(Simple Dynamic String,SDS)结构体来保存,buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。len:占 4 个字节,表示 buf 的已用长度。alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。 另外,对于 String 类型来说,除了 SDS 的额外开销,还有一个来自于 RedisObject 结构体的开销。一个 RedisObject 包含了 8 字节的元数据和一个 8 字节指针。 当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。2)统计模式 聚合统计,就是指统计多个集合元素的聚合结果,包括:统计多个集合的共有元素(交集统计);把两个集合相比,统计其中一个集合独有的元素(差集统计);统计多个集合的所有元素(并集统计)。 Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。小建议:你可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计。 在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议你优先考虑使用 Sorted Set。 二值状态就是指集合元素的取值就只有 0 和 1 两种。Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。 基数统计就是指统计一个集合中不重复的元素个数。3)GEO GEO 类型的底层数据结构就是用 Sorted Set 来实现的。 Redis 采用了业界广泛使用的 GeoHash 编码方法,这个方法的基本原理就是“二分区间,区间编码”。 对于一个地理位置信息来说,它的经度范围是[-180,180]。GeoHash 编码会把一个经度值编码成一个 N 位的二进制值,我们来对经度范围[-180,180]做 N 次的二分区操作,其中 N 可以自定义。4)异步机制 和客户端交互时的阻塞点。复杂度高的增删改查操作肯定会阻塞 Redis。第一个阻塞点:集合全量查询和聚合操作。第二个阻塞点:bigkey 删除操作。第三个阻塞点:清空数据库。 和磁盘交互时的阻塞点。Redis 开发者早已认识到磁盘 IO 会带来阻塞,所以就把 Redis 进一步设计为采用子进程的方式生成 RDB 快照文件,以及执行 AOF 日志重写操作。第四个阻塞点了:AOF 日志同步写。 主从节点交互时的阻塞点。在主从集群中,主库需要生成 RDB 文件,并传输给从库。主库在复制的过程中,创建和传输 RDB 文件都是由子进程来完成的,不会阻塞主线程。第五个阻塞点:加载 RDB 文件。 Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由它们负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行。5)内存碎片 Redis 释放的内存空间可能并不是连续的,那么,这些不连续的内存空间很有可能处于一种闲置的状态。 这就会导致一个问题:虽然有空闲空间,Redis 却无法用来保存数据,不仅会减少 Redis 能够实际保存的数据量,还会降低 Redis 运行机器的成本回报率。 内存碎片的形成有内因和外因两个层面的原因。简单来说,内因是操作系统的内存分配机制,外因是 Redis 的负载特征。 Redis 是内存数据库,内存利用率的高低直接关系到 Redis 运行效率的高低。为了让用户能监控到实时的内存使用情况,Redis 自身提供了 INFO 命令。 这里有一个 mem_fragmentation_ratio 的指标,它表示的就是 Redis 当前的内存碎片率。mem_fragmentation_ratio 大于 1 但小于 1.5。这种情况是合理的。6)替换策略 “八二原理”,有 20% 的数据贡献了 80% 的访问了,而剩余的数据虽然体量很大,但只贡献了 20% 的访问量。volatile-ttl 在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。volatile-random 就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。volatile-lru 会使用 LRU 算法筛选设置了过期时间的键值对。volatile-lfu 会使用 LFU 算法选择设置了过期时间的键值对。allkeys-random 策略,从所有键值对中随机选择并删除数据;allkeys-lru 策略,使用 LRU 算法在所有数据中进行筛选。allkeys-lfu 策略,使用 LFU 算法在所有数据中进行筛选。7)原子操作 原子操作是指执行过程保持原子性的操作,而且原子操作执行时并不需要再加锁,实现了无锁操作。 Redis 的原子操作采用了两种方法:把多个操作在 Redis 中实现成一个操作,也就是单命令操作;把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本。 Redis 是使用单线程来串行处理客户端的请求操作命令的,所以,当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的。 当然,Redis 的快照生成、AOF 重写这些操作,可以使用后台线程或者是子进程执行,也就是和主线程的操作并行执行。不过,这些操作只是读取数据,不会修改数据,所以,我们并不需要对它们做并发控制。8)脑裂 脑裂就是指在主从集群中,同时有两个主节点,它们都能接收写请求。而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失。 主从切换后,从库一旦升级为新主库,哨兵就会让原主库执行 slave of 命令,和新主库重新进行全量同步。而在全量同步执行的最后阶段,原主库需要清空本地的数据,加载新主库发送的 RDB 文件,这样一来,原主库在主从切换期间保存的新写数据就丢失了。
公司目前在线上运行着一款小程序,为了能监控小程序的运行情况,自行开发了一个参数搜集的SDK,名称为 shin.js,放置在 utils 目录中。 目前只搜集了打印、通信和错误,其中打印不是 console.log() 而是 shin.log()。 在小程序的管理后台,开发管理中,目前也有一个错误日志列表,其中也有比较详尽的错误信息,可配合监控系统使用。一、SDK1)log 在 Shin 类的构造函数中,声明了 log() 方法,主要就是将传入的数据解析成JSON格式的字符串,然后传递给后台。 options 是可配置的参数,包括参数的发送地址,以及项目 token。injectApp() 方法修改了原生的 App 方法并且注入了监控逻辑,具体会在后面讲解。class Shin { constructor(options) { this.options = options; this.injectApp(); // 将打印的日志推送到监控后台 ['log'].forEach((type) => { this[type] = msg => { this.send({ category: 'console', data: { type, desc: JSON.stringify(msg) } }) }; }); } } 为了与之前的数据结构兼容,需要整理成指定的格式后,再发送。2)发送 send() 方法用于发送,其中 identity 是一个身份标识,存在于全局对象 globalData 中,而它是通过 getIdentity() 方法生成的。getIdentity() { return Number(Math.random().toString().substr(3, 3) + Date.now()).toString(36); } send(params) { //日志通用数据配置 params.identity = getApp().globalData.identity; params.token = this.options.token; params.data = Object.assign(params.data, { network: this.network, url: this.getActivePage().route }); //错误日志还需要记录设备信息 if(params.category == 'error') { params.data.system = this.system; } wx.request({ url: this.options.src, method: "GET", data: { m: JSON.stringify(params) } }); } 代码中的 getActivePage() 用于读取当前页面,调用了getCurrentPages();getNetworkType()读取当前网络类型;getSystemInfo()读取当前设备信息。getActivePage() { // 获取当前页面栈 const curPages = getCurrentPages(); if (curPages.length) { return curPages[curPages.length - 1]; } return {}; } getNetworkType() { wx.getNetworkType({ success: (res) => { this.network = res.networkType; }, }); } getSystemInfo() { wx.getSystemInfo({ success: (res) => { this.system = res; }, }); }3)错误 在构造函数中调用了 injectApp() 方法,为 App 注入自定义的监控行为。 在触发onLaunch事件时记录网络类型和设备信息,在触发 onError 事件时将具体的错误信息发送到后台。injectApp() { const originApp = App; const self = this; App = function (app) { ['onLaunch', 'onError'].forEach((methodName) => { const customMethod = app[methodName]; //暂存自定义的方法 if (methodName === 'onLaunch') { self.getNetworkType(); //记录网络 self.getSystemInfo(); //记录设备信息 } app[methodName] = function (options) { if(methodName === 'onError') { const params = { category: 'error', data: { type: 'mini', desc: options, //错误信息 } }; self.send(params); //错误上报 } return customMethod && customMethod.call(this, options); }; }); return originApp(app); }; }4)通信 我们自己封装了一个通信库,为了操作简便,就定义了一个方法,在通信完成时调用此方法。formatRequest({ res, url, method, data }) { // 响应 const ajax = { type: method, status: res.statusCode, url, data } // 过滤掉数据量大的响应 if(JSON.stringify(res.data).length <= 300) { ajax.response = res.data; } const params = {}; if(res.statusCode >= 400) { params.category = 'error'; params.data = { type: 'promise', desc: ajax }; }else { params.category = 'ajax' params.data = ajax; } this.send(params); } 它接收的参数包括 res(响应数据),url(请求地址),method(请求方法),data(请求参数)。 其中 res 包括状态码和响应内容,状态码囊括了4XX和5XX。5)初始化 在启动文件 app.js 引入 shin.js文件,并初始化,在 globalData 中添加 shin 和 identity。import Shin from './utils/shin'; const shin = new Shin({ src: 'https://127.0.0.1:3000/ma.gif', token: 'mini' }); globalData: { shin, identity: shin.getIdentity(), }6)监控后台 参数搜集的 api 不需要做任何修改,在监控后台的页面中也只是加几个过滤选项即可,而这些选项都已经写成了常量,修改起来很方便,例如:export const MONITOR_PROJECT = [ { key: 'backend', value: '管理后台' }, { key: 'h5', value: 'H5活动' }, { key: 'mini', value: '小程序'} ];二、Source Map 目前还不能在监控后台直接通过 SourceMap 自动映射(未来的一个优化点)。 需要先从小程序后台下载 SourceMap 文件,下载完后导入小程序开发编辑器中查看映射条件。 具体过程: 1)首先小程序开发器的版本必须得是 1.03.2012152 以上。 2)选择"设置-通用设置-扩展-调试器插件",进入插件下载页面,添加sourcemap匹配调试插件。 3)在开发管理中的错误日志(参考教程)中,可下载线上版本的 SourceMap 文件或者,或者在上传完代码后,会提示你下载该文件。 4)最后可在控制台调试器中出现 sourcemap 标签,在此处加载映射文件以及输入行号和列号,完成映射。
《设计模式之美》是极客时间上的一个代码学习系列,在学习之后特在此做记录和总结。 设计模式要干的事情就是解耦,也就是利用更好的代码结构将一大坨代码拆分成职责更单一的小类,让其满足高内聚低耦合等特性。 每个设计模式都应该由两部分组成:第一部分是应用场景,即这个模式可以解决哪类问题;第二部分是解决方案,即这个模式的设计思路和具体的代码实现。不过,代码实现并不是模式必须包含的。如果你单纯地只关注解决方案这一部分,甚至只关注代码实现,就会产生大部分模式看起来都很相似的错觉。一、创建型 创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。 单例模式用来创建全局唯一的对象。工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。1)单例模式 单例设计模式(Singleton Design Pattern)是指一个类只允许创建一个对象(或者实例),那这个类就是一个单例类。public class IdGenerator { private static IdGenerator instance; private IdGenerator() {} public static IdGenerator getInstance() { if (instance == null) { synchronized(IdGenerator.class) { // 此处为类级别的锁 if (instance == null) { instance = new IdGenerator(); } } } return instance; } } (1)实战案例一:处理资源访问冲突 将 Logger 设计成一个单例类,程序中只允许创建一个 Logger 对象,所有的线程共享使用的这一个 Logger 对象,共享一个 FileWriter 对象,而 FileWriter 本身是对象级别线程安全的,也就避免了多线程情况下写日志会互相覆盖的问题。 (2)实战案例二:表示全局唯一类 从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类、唯一递增 ID 号码生成器。 实现:要实现一个单例,需要关注的点无外乎下面几个: (1)构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例; (2)考虑对象创建时的线程安全问题; (3)考虑是否支持延迟加载; (4)考虑 getInstance() 性能是否高(是否加锁)。 问题:有些人认为单例是一种反模式(anti-pattern),并不推荐使用。 (1)单例对 OOP 特性的支持不友好,对于其中的抽象、继承、多态都支持得不好。 (2)单例会隐藏类之间的依赖关系,通过构造函数、参数传递等方式声明的类之间的依赖关系很容易分辨,但是,单例类不需要显示创建、不需要依赖参数传递。 (3)单例对代码的扩展性不友好,单例类只能有一个对象实例。如果未来某一天,需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。 (4)单例对代码的可测试性不友好,如果单例类依赖比较重的外部资源,比如 DB,由于单例类这种硬编码式的使用方式,导致无法实现 mock 替换。 (5)单例不支持有参数的构造函数,比如创建一个连接池的单例对象,没法通过参数来指定连接池的大小。 为了保证全局唯一,除了使用单例,还可以用静态方法来实现。这也是项目开发中经常用到的一种实现思路。2)工厂模式 (1)简单工厂(Simple Factory) 大部分工厂类都是以“Factory”这个单词结尾的,工厂类中创建对象的方法一般都是 create 开头。public class RuleConfigParserFactory { public static IRuleConfigParser createParser(String configFormat) { IRuleConfigParser parser = null; if ("json".equalsIgnoreCase(configFormat)) { parser = new JsonRuleConfigParser(); } else if ("xml".equalsIgnoreCase(configFormat)) { parser = new XmlRuleConfigParser(); } else if ("yaml".equalsIgnoreCase(configFormat)) { parser = new YamlRuleConfigParser(); } else if ("properties".equalsIgnoreCase(configFormat)) { parser = new PropertiesRuleConfigParser(); } return parser; } } (2)工厂方法(Factory Method) 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。 如果非得要将 if 分支逻辑去掉,那么比较经典处理方法就是利用多态。工厂方法模式比起简单工厂模式更加符合开闭原则。public interface IRuleConfigParserFactory { IRuleConfigParser createParser(); } public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new JsonRuleConfigParser(); } } public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new XmlRuleConfigParser(); } } 在工厂类的使用上,工厂类对象的创建逻辑又耦合进了 load() 函数中,跟最初的代码版本非常相似。 可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。 当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。 而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。3)建造者模式 Builder 模式,即建造者模式、构建者模式或生成器模式。要解决下面这些问题,就需要建造者模式上场了。 (1)如果可配置项逐渐增多,变成了 8 个、10 个,那么在使用构造函数的时候,就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。 (2)如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。 (3)如果配置项之间有约束条件,那么校验逻辑就无处安放了。 (4)如果希望对象在创建好之后,就不能再修改内部的属性值,那么就不能暴露 set() 方法。 可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后再使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。 除此之外,把 ResourcePoolConfig 的构造函数改为 private 私有权限。这样就只能通过建造者来创建 ResourcePoolConfig 类对象。并且,ResourcePoolConfig 没有提供任何 set() 方法,这样创建出来的对象就是不可变对象了。public class ResourcePoolConfig { private String name; private int maxTotal; private ResourcePoolConfig(Builder builder) { this.name = builder.name; this.maxTotal = builder.maxTotal; } //...省略getter方法... //将Builder类设计成了ResourcePoolConfig的内部类。 //也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。 public static class Builder { private static final int DEFAULT_MAX_TOTAL = 8; private String name; private int maxTotal = DEFAULT_MAX_TOTAL; public ResourcePoolConfig build() { // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等 if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("..."); } return new ResourcePoolConfig(this); } public Builder setName(String name) { if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("..."); } this.name = name; return this; } public Builder setMaxTotal(int maxTotal) { if (maxTotal <= 0) { throw new IllegalArgumentException("..."); } this.maxTotal = maxTotal; return this; } } } // 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle ResourcePoolConfig config = new ResourcePoolConfig.Builder() .setName("dbconnectionpool") .setMaxTotal(16) .build(); 工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。 简单地说,工厂模式是根据不同的条件生成不同类的对象,建造者模式是根据不同参数生成一个类的不同对象。4)原型模式 如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。 如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。 原型模式的实现方式:深拷贝(Deep Copy)和浅拷贝(Shallow Copy)。 浅拷贝只会复制图中的索引(散列表),不会复制数据(SearchWord 对象)本身。相反,深拷贝不仅仅会复制索引,还会复制数据本身。浅拷贝得到的对象(newKeywords)跟原始对象(currentKeywords)共享数据(SearchWord 对象),而深拷贝得到的是一份完完全全独立的对象。 实现深拷贝的两种方法: (1)第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。 (2)第二种方法:先将对象序列化,然后再反序列化成新的对象。二、结构型 结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。1)代理模式 代理模式(Proxy Design Pattern)是指在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。 为了将框架代码和业务代码解耦,代理模式就派上用场了。 UserController 类只负责业务功能。代理类 UserControllerProxy 负责在业务代码执行前后附加其他逻辑代码,并通过委托的方式调用原始类来执行业务代码。public interface IUserController { UserVo login(String telephone, String password); UserVo register(String telephone, String password); } public class UserController implements IUserController { } public class UserControllerProxy implements IUserController { private MetricsCollector metricsCollector; private UserController userController; public UserControllerProxy(UserController userController) { this.userController = userController; this.metricsCollector = new MetricsCollector(); } @Override public UserVo login(String telephone, String password) { long startTimestamp = System.currentTimeMillis(); // 委托 UserVo userVo = userController.login(telephone, password); long endTimeStamp = System.currentTimeMillis(); long responseTime = endTimeStamp - startTimestamp; RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp); metricsCollector.recordRequest(requestInfo); return userVo; } } 为了让代码改动尽量少,在刚刚的代理模式的代码实现中,代理类和原始类需要实现相同的接口。而对于外部类的扩展,一般都是采用继承的方式。public class UserControllerProxy extends UserController { } 所谓动态代理(Dynamic Proxy),就是不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。 应用场景: (1)在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。 (2)RPC 框架也可以看作一种代理模式,通过远程代理,将网络通信、数据编解码等细节隐藏起来。在 AOP 切面中完成接口缓存的功能。2)桥接模式 桥接模式(Bridge Design Pattern)也叫桥梁模式,对于这个模式有两种不同的理解方式。 (1)将抽象和实现解耦,让它们可以独立变化。 (2)一个类存在两个(或多个)独立变化的维度,通过组合的方式,让这两个(或多个)维度可以独立进行扩展。 针对 Notification 的代码,将不同渠道的发送逻辑剥离出来,形成独立的消息发送类(MsgSender 相关类)。其中,Notification 类相当于抽象,MsgSender 类相当于实现,两者可以独立开发,通过组合关系(也就是桥梁)任意组合在一起。所谓任意组合的意思就是,不同紧急程度的消息和发送渠道之间的对应关系,不是在代码中固定写死的,可以动态地去指定(比如,通过读取配置来获取对应关系)。public interface MsgSender { void send(String message); } public class TelephoneMsgSender implements MsgSender { private List<String> telephones; public TelephoneMsgSender(List<String> telephones) { this.telephones = telephones; } @Override public void send(String message) { //... } } public class EmailMsgSender implements MsgSender { // 与TelephoneMsgSender代码结构类似,所以省略... } public abstract class Notification { protected MsgSender msgSender; public Notification(MsgSender msgSender) { this.msgSender = msgSender; } public abstract void notify(String message); } public class SevereNotification extends Notification { public SevereNotification(MsgSender msgSender) { super(msgSender); } @Override public void notify(String message) { msgSender.send(message); } } public class UrgencyNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... } public class NormalNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... }3)装饰器模式 装饰器模式(Decorator Design Pattern)相对于简单的组合关系,有两个比较特殊的地方。 (1)装饰器类和原始类继承同样的父类,这样可以对原始类“嵌套”多个装饰器类。 (2)装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。 代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。 代理模式偏重业务无关,高度抽象和稳定性较高的场景。装饰器模式偏重业务相关,定制化诉求高,改动较频繁的场景。4)适配器模式 适配器模式(Adapter Design Pattern)可将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。 适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。下面是使用的前提条件。 (1)如果 Adaptee 接口并不多,那两种实现方式都可以。 (2)如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。 (3)如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那推荐使用对象适配器,因为组合结构相对于继承更加灵活。 应用场景: 适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。 (1)封装有缺陷的接口设计。对外部系统提供的接口进行二次封装,抽象出更好的接口设计。 (2)统一多个类的接口设计。将所有系统的接口适配为统一的接口定义。 (3)替换依赖的外部系统。 (4)兼容老版本接口。 (5)适配不同格式的数据。 代理、桥接、装饰器和适配器都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。 (1)代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。 (2)桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。 (3)装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。 (4)适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。5)门面模式 门面模式(Facade Design Pattern)为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。子系统(subsystem)既可以是一个完整的系统,也可以是更细粒度的类或者模块。 App 客户端的响应速度比较慢,排查之后发现,是因为过多的接口调用过多的网络通信。针对这种情况,就可以利用门面模式,让后端服务器提供一个包裹 a、b、d 三个接口调用的接口 x。App 客户端调用一次接口 x,来获取到所有想要的数据,将网络通信的次数从 3 次减少到 1 次,也就提高了 App 的响应速度。 应用场景: (1)解决易用性问题,比如,Linux 系统调用函数、Shell 命令。 (2)解决性能问题,如果门面接口特别多,并且很多都是跨多个子系统的,可将门面接口放到一个新的子系统中。 (3)解决分布式事务问题,比如在一个事务中,执行创建用户和创建钱包这两个 SQL 操作。 与适配器模式的区别: (1)适配器模式是做接口转换,解决的是原接口和目标接口不匹配的问题。在代码结构上主要是继承加组合。 (2)门面模式做接口整合,解决的是多接口调用带来的问题。在代码结构上主要是封装。6)组合模式 组合模式(Composite Design Pattern)跟面向对象设计中的“组合关系(通过组合来组装两个类)”,完全是两码事。它主要是用来处理树形结构数据,其中数据可理解为一组对象集合。 组合模式是将一组对象组织成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(指代码的使用者)可以统一单个对象和组合对象的处理逻辑。 对照着例子,重新定义: 将一组对象(文件和目录)组织成树形结构,以表示一种‘部分 - 整体’的层次结构(目录与子目录的嵌套结构)。组合模式让客户端可以统一单个对象(文件)和组合对象(目录)的处理逻辑(递归遍历)。 实际上,组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树,业务需求可以通过在树上的递归遍历算法来实现。7)享元模式 所谓享元,顾名思义就是被共享的单元。享元模式(Flyweight Design Pattern)的意图是复用对象,节省内存,前提是享元对象是不可变对象。 当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。对于相似对象,也可以将它相同的部分(字段)提取出来,设计成享元。 “不可变对象”指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。 所有的 ChessBoard 对象共享这 30 个 ChessPieceUnit 对象(因为象棋中只有 30 个棋子)。在使用享元模式之前,记录 1 万个棋局,要创建 30 万(30*1 万)个棋子的 ChessPieceUnit 对象。利用享元模式,只需要创建 30 个享元对象供所有棋局共享使用即可,大大节省了内存。// 享元类 public class ChessPieceUnit { private int id; private String text; private Color color; public ChessPieceUnit(int id, String text, Color color) { this.id = id; this.text = text; this.color = color; } public static enum Color { RED, BLACK } } public class ChessPieceUnitFactory { private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>(); static { pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK)); pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK)); //...省略摆放其他棋子的代码... } public static ChessPieceUnit getChessPiece(int chessPieceId) { return pieces.get(chessPieceId); } } public class ChessPiece { //棋子 private ChessPieceUnit chessPieceUnit; private int positionX; private int positionY; public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) { this.chessPieceUnit = unit; this.positionX = positionX; this.positionY = positionY; } } public class ChessBoard { //棋局 private Map<Integer, ChessPiece> chessPieces = new HashMap<>(); public ChessBoard() { init(); } private void init() { chessPieces.put(1, new ChessPiece( ChessPieceUnitFactory.getChessPiece(1), 0,0)); chessPieces.put(1, new ChessPiece( ChessPieceUnitFactory.getChessPiece(2), 1,0)); //...省略摆放其他棋子的代码... } public void move(int chessPieceId, int toPositionX, int toPositionY) { //...省略... } } 实际上,它的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的。 在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。三、行为型 创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合或组装”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。 设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。1)观察者模式 观察者模式(Observer Design Pattern)也叫发布订阅模式(Publish-Subscribe Design Pattern),在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。 一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。观察者模式就是将观察者和被观察者代码解耦。public interface Subject { void registerObserver(Observer observer); void removeObserver(Observer observer); void notifyObservers(Message message); } public interface Observer { void update(Message message); } 基于消息队列的实现方式,被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。2)模板模式 模板模式(Template Method Design Pattern)全称是模板方法模式,可在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。 这里的“算法”,可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。public abstract class AbstractClass { public final void templateMethod() { //... method1(); //... method2(); //... } protected abstract void method1(); protected abstract void method2(); } public class ConcreteClass1 extends AbstractClass { @Override protected void method1() {} @Override protected void method2() {} } public class ConcreteClass2 extends AbstractClass { @Override protected void method1() {} @Override protected void method2() {} } 模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。 (1)作用一:复用 模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 method1()、method2() 留给子类 ContreteClass1 和 ContreteClass2 来实现。 (2)作用二:扩展 这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似之前讲到的控制反转。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。3)策略模式 策略模式(Strategy Design Pattern)定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(使用算法的代码)。 策略模式解耦的是策略的定义、创建、使用这三部分。让每个部分都不至于过于复杂、代码量过多。 (1)策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。 (2)通过类型(type)来判断创建哪个策略。可以把根据 type 创建策略的逻辑抽离出来,放到工厂类中。 (3)运行时动态确定使用哪种策略,即在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。 利用策略模式避免分支判断。将不同类型订单的打折策略设计成策略类,并由工厂类来负责创建策略对象。// 策略的定义 public interface DiscountStrategy { double calDiscount(Order order); } // 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码... // 策略的创建 public class DiscountStrategyFactory { private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>(); static { strategies.put(OrderType.NORMAL, new NormalDiscountStrategy()); strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy()); strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy()); } public static DiscountStrategy getDiscountStrategy(OrderType type) { return strategies.get(type); } } // 策略的使用 public class OrderService { public double discount(Order order) { OrderType type = order.getType(); DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type); return discountStrategy.calDiscount(order); } } 策略模式侧重“策略”或“算法”这个特定的应用场景,用来解决根据运行时状态从一组策略中选择不同策略的问题,而工厂模式侧重封装对象的创建过程,这里的对象没有任何业务场景的限定,可以是策略,但也可以是其他东西。4)职责链模式 职责链模式(Chain Of Responsibility Design Pattern)是将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。 在职责链模式中,多个处理器(接收对象)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。public abstract class Handler { //模板模式 protected Handler successor = null; public void setSuccessor(Handler successor) { this.successor = successor; } public final void handle() { boolean handled = doHandle(); if (successor != null && !handled) { successor.handle(); } } protected abstract boolean doHandle(); } public class HandlerA extends Handler { @Override protected boolean doHandle() { boolean handled = false; //... return handled; } } public class HandlerB extends Handler { @Override protected boolean doHandle() { boolean handled = false; //... return handled; } } public class HandlerChain { private Handler head = null; private Handler tail = null; public void addHandler(Handler handler) { handler.setSuccessor(null); if (head == null) { head = handler; tail = handler; return; } tail.setSuccessor(handler); tail = handler; } public void handle() { if (head != null) { head.handle(); } } } public class Application { //使用举例 public static void main(String[] args) { HandlerChain chain = new HandlerChain(); chain.addHandler(new HandlerA()); chain.addHandler(new HandlerB()); chain.handle(); } } 职责链模式还有一种变体,那就是请求会被所有的处理器都处理一遍,不存在中途终止的情况。这种变体也有两种实现方式:用链表存储处理器和用数组存储处理器。 为什么非要使用职责链模式呢?这是不是过度设计呢? (1)应对代码的复杂性,用职责链模式把各个敏感词过滤函数继续拆分出来,设计成独立的类,进一步简化了 SensitiveWordFilter 类,让 SensitiveWordFilter 类的代码不会过多,过复杂。 (2)满足开闭原则,当要扩展新的过滤算法时,只需要新添加一个 Filter 类,并且通过 addFilter() 函数将它添加到 FilterChain 中即可,其他代码完全不需要修改。5)状态模式 状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。 有限状态机(Finite State Machine,FSM),简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。 (1)状态机实现方式一:分支逻辑法 参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,所以这种方法暂且命名为分支逻辑法。 (2)状态机实现方式二:查表法 把这两个二维数组存储在配置文件中,当需要修改状态机时,甚至可以不修改任何代码,只需要修改配置文件就可以了。 (3)状态机实现方式三:状态模式 如果要执行的动作是一系列复杂的逻辑操作(比如加减积分、写数据库,还有可能发送消息通知等等),那么查表法就不合适了。 状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。public interface IMario { //所有状态类的接口 void obtainMushRoom(); void obtainCape(); } public class SmallMario implements IMario { private MarioStateMachine stateMachine; public SmallMario(MarioStateMachine stateMachine) { this.stateMachine = stateMachine; } @Override public void obtainMushRoom() { stateMachine.setCurrentState(new SuperMario(stateMachine)); stateMachine.setScore(stateMachine.getScore() + 100); } @Override public void obtainCape() { stateMachine.setCurrentState(new CapeMario(stateMachine)); stateMachine.setScore(stateMachine.getScore() + 200); } } public class SuperMario implements IMario { private MarioStateMachine stateMachine; public SuperMario(MarioStateMachine stateMachine) { this.stateMachine = stateMachine; } @Override public void obtainMushRoom() { // do nothing... } @Override public void obtainCape() { stateMachine.setCurrentState(new CapeMario(stateMachine)); stateMachine.setScore(stateMachine.getScore() + 200); } } public class MarioStateMachine { private int score; private IMario currentState; //不再使用枚举来表示状态 public MarioStateMachine() { this.score = 0; this.currentState = new SmallMario(this); } public void obtainMushRoom() { this.currentState.obtainMushRoom(); } public void obtainCape() { this.currentState.obtainCape(); } public void setScore(int score) { this.score = score; } public void setCurrentState(IMario currentState) { this.currentState = currentState; } }6)迭代器模式 迭代器模式(Iterator Design Pattern)也叫游标模式(Cursor Design Pattern)用来遍历集合对象。 这里说的“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。 迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。 一个完整的迭代器模式一般会涉及容器和容器迭代器两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。public interface Iterator<E> { boolean hasNext(); void next(); E currentItem(); } 总结下来就三句话:迭代器中需要定义 hasNext()、currentItem()、next() 三个最基本的方法。待遍历的容器对象通过依赖注入传递到迭代器类中。容器通过 iterator() 方法来创建迭代器。 为什么还要用迭代器来遍历容器呢?为什么还要给容器设计对应的迭代器呢? (1)复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。 (2)将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。 (3)容器和迭代器都提供了抽象的接口,方便在开发时基于接口而非具体的实现编程。 在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。7)访问者模式 访问者者模式(Visitor Design Pattern)允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。 访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类(ResourceFile)或者实现相同的接口。 在不同的应用场景下,需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。 对于访问者模式,学习的主要难点在代码实现。而代码实现比较复杂的主要原因是,函数重载在大部分面向对象编程语言中是静态绑定的。也就是说,调用类的哪个重载函数,是在编译期间,由参数的声明类型决定的,而非运行时,根据参数的实际类型决定的。8)备忘录模式 备忘录模式(Memento Design Pattern)也叫快照(Snapshot)模式,在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。 备忘录模式主要是用来防丢失、撤销、恢复等。 其一,定义一个独立的类(Snapshot 类)来表示快照,而不是复用 InputText 类。这个类只暴露 get() 方法,没有 set() 等任何修改内部状态的方法。 其二,在 InputText 类中,把 setText() 方法重命名为 restoreSnapshot() 方法,用意更加明确,只用来恢复对象。public class InputText { private StringBuilder text = new StringBuilder(); public String getText() { return text.toString(); } public void append(String input) { text.append(input); } public Snapshot createSnapshot() { return new Snapshot(text.toString()); } public void restoreSnapshot(Snapshot snapshot) { this.text.replace(0, this.text.length(), snapshot.getText()); } } public class Snapshot { private String text; public Snapshot(String text) { this.text = text; } public String getText() { return this.text; } } public class SnapshotHolder { private Stack<Snapshot> snapshots = new Stack<>(); public Snapshot popSnapshot() { return snapshots.pop(); } public void pushSnapshot(Snapshot snapshot) { snapshots.push(snapshot); } }9)命令模式 命令模式(Command Design Pattern)将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。 在大部分编程语言中,函数没法作为参数传递给其他函数,也没法赋值给变量。借助命令模式,可以将函数封装成对象。设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。 在策略模式中,不同的策略具有相同的目的、不同的实现、互相之间可以替换。比如,BubbleSort、SelectionSort 都是为了实现排序的,只不过一个是用冒泡排序算法来实现的,另一个是用选择排序算法来实现的。而在命令模式中,不同的命令具有不同的目的,对应不同的处理逻辑,并且互相之间不可替换。 命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等。10)解释器模式 解释器模式(Interpreter Design Pattern)只在一些特定的领域会被用到,比如编译器、规则引擎、正则表达式。它能为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。11)中介模式 中介模式(Mediator Design Pattern)定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。 中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。 假设有一个比较复杂的对话框,对话框中有很多控件,比如按钮、文本框、下拉框等。当对某个控件进行操作的时候,其他控件会做出相应的反应,比如,在下拉框中选择“注册”,注册相关的控件就会显示在对话框中。public interface Mediator { void handleEvent(Component component, String event); } public class LandingPageDialog implements Mediator { private Button loginButton; private Button regButton; private Selection selection; private Input usernameInput; private Input passwordInput; private Input repeatedPswdInput; private Text hintText; @Override public void handleEvent(Component component, String event) { if (component.equals(loginButton)) { String username = usernameInput.text(); String password = passwordInput.text(); //校验数据... //做业务处理... } else if (component.equals(regButton)) { //获取usernameInput、passwordInput、repeatedPswdInput数据... //校验数据... //做业务处理... } else if (component.equals(selection)) { String selectedItem = selection.select(); if (selectedItem.equals("login")) { usernameInput.show(); passwordInput.show(); repeatedPswdInput.hide(); hintText.hide(); //...省略其他代码 } else if (selectedItem.equals("register")) { //.... } } } } 好处是简化了控件之间的交互,坏处是中介类有可能会变成大而复杂的“上帝类”(God Class)。所以,在使用中介模式的时候,要根据实际的情况,平衡对象之间交互的复杂度和中介类本身的复杂度。 中介模式和观察者模式的区别在哪里呢? (1)在观察者模式中,尽管一个参与者既可以是观察者,同时也可以是被观察者,但是大部分情况下,交互关系往往都是单向的,一个参与者要么是观察者,要么是被观察者,不会兼具两种身份。 (2)而中介模式正好相反。参与者之间的交互关系错综复杂,既可以是消息的发送者、也可以同时是消息的接收者。
早在2013年Luke Wroblewski就提出了骨架屏(Skeleton Screen)的概念,他认为骨架屏是一个页面的空白版本,通过这个空白版本来传递一种信息,即页面正在渐进式的加载中。骨架屏的布局能与页面的视觉呈现保持一致,这样就能引导用户的关注点聚焦到感兴趣的位置。如下图所示,左边是数据渲染后的页面,右边是骨架屏,可以看到相应的位置都能对起来。 在网上阅读了一些骨架屏原理的资料后,就自己想尝试一下,练练手,制作一个极简版本的骨架屏插件。因为简单,所以未来如要扩展,成本也会很低。上图是通过自己写的骨架屏插件得到的效果,对于公司简单结构的项目,还是游刃有余的。在编写插件时,参考了网上多篇资料分享的代码,站在巨人的肩膀上整合代码,省力了很多。插件的完整代码已上传至GitHub中,下面是其中的构造函数,以及三个常量,用到了ES6的一些概念,如对此不熟悉,可参考我之前整理的《ES6躬行记》。const NODE_ELEMENT = 1, //元素类型的节点常量 NODE_TEXT = 3, //文本类型的节点常量 NODE_COMMENT = 8; //注释类型的节点常量 /** * @param color 字体的背景色 * @param bgColor 带背景图模块的背景色 * @param rectHeight 指定区域的高度,默认为视口高度 * @param formFn 自定义表单着色规则 * @constructor */ function Skeleton({ color = "#DCDCDC", bgColor = "#F6F8FA", rectHeight = global.innerHeight, formFn = function() {} } = {}) { this.container = document.body; //骨架容器 this.color = color; this.bgColor = bgColor; this.rectHeight = rectHeight; this.formFn = formFn; }一、绘制骨架屏 由于对Node.js不熟,所以采用纯原生的JavaScript来绘制骨架屏。首先将页面中的元素分成三类:图像、文本和表单。1)图像 图像也就是元素,其src属性会被替换成一张灰色(色素是#EEE)的1*1的gif图。为了避免引入额外的请求,将该gif图转换成base64格式,写死在替换函数image()中,如下所示,呈现的效果如下图所示。image(element, isImage = true) { const { width, height } = getRect(element); //图像颜色 #EEE const src = "data:image/gif;base64,R0lGODlhAQABAPAAAPT09AA...."; if (isImage) element.src = src; else element.style.background = this.bgColor; element.width = width; element.height = height; } 由于image()函数声明在原型(prototype)之上,因此省略了function关键字。isImage是一个布尔值,表示是否是一个元素。当传入非元素时,就需要将其背景替换成初始化时的纯色。getRect()是一个辅助函数,用于获取元素的尺寸和坐标。function getRect(element) { return element.getBoundingClientRect(); }2)文本 处理文本是比较复杂的,因为文本长度是不定的,如下图所示,左边的文本是两行,骨架屏中也要变成两行,并且第二行不是满行的。 网上的资料对于最后一行都会做遮罩处理,也就是用一个白底的块定位到相应位置,把多余的灰底遮掉。当文本只有一行时,还需要做特殊处理。 而我在设计骨架屏插件的时候,采用了一个简单粗暴的方法,能够避免遮罩和单行的处理,那就是为所有文本节点添加元素。对于我这边不太复杂的HTML结构而言,能够大大简化代码的复杂度。具体方法如下所示,采用递归的方式逐个访问子节点,当节点是文本类型并且有内容时,就为其包裹标签。appendTextNode(parent) { //避免<span>中嵌套<span> if ( parent.childNodes.length <= 1 && parent.nodeName.toLowerCase() == "span" ) { return; } parent.childNodes.forEach(node => { if (node.nodeType === NODE_TEXT && node.nodeValue.trim().length > 0) { let span = document.createElement("span"); span.textContent = node.nodeValue; parent.replaceChild(span, node); } else { this.appendTextNode(node); } }); } 下面的第一个元素在调用了appendTextNode()方法后,就变成了第二个元素。<p>本活动最终解释权归上海易点时空网络有限公司所有</p> <!-- 骨架屏结构 --> <p><span>本活动最终解释权归上海易点时空网络有限公司所有</span></p> 为了让多行文本能呈现灰白相间的效果,就得借助CSS3的linear-gradient渐变属性来实现。如果对其不熟悉,可以参考之前的《CSS3中惊艳的gradient》一文。 下面的计算方式照搬了饿了么的page-skeleton-webpack-plugin插件,其中getStyle()函数用于获取元素的CSS属性或属性对象(CSSStyleDeclaration)。calculate(element) { let { fontSize, lineHeight } = getStyle(element); lineHeight = parseFloat(lineHeight); //解析浮点数 fontSize = parseFloat(fontSize); const textHeightRatio = fontSize / lineHeight, //字体占行高的比值 firstColorPoint = ((1 - textHeightRatio) / 2 * 100).toFixed(2), //渐变的第一个位置,小数点后两位四舍五入 secondColorPoint = (((1 - textHeightRatio) / 2 + textHeightRatio) * 100).toFixed(2); //渐变的第二个位置 return ` background-image: linear-gradient( transparent ${firstColorPoint}%, ${this.color} 0, ${this.color} ${secondColorPoint}%, transparent 0); background-size: 100% ${lineHeight}; position: relative; color: transparent; `; } function getStyle(element, name) { const style = global.getComputedStyle(element); return name ? style[name] : style; } 首先读取字体大小和行高,然后计算字体占行高的比值(textHeightRatio),接着计算出渐变的两个位置(firstColorPoint和secondColorPoint),最后通过模板字面量输出文本的样式,字体颜色被设为了透明。 绘制文本的逻辑都封装到了text()方法中,具体如下所示。text(element) { //判断是否是只包含文本的节点 const isText = element.childNodes && element.childNodes.length === 1 && element.childNodes[0].nodeType === NODE_TEXT && /\S/.test(element.childNodes[0].textContent); if (!isText) { return; } const rule = this.calculate(element); //计算样式 element.setAttribute("style", rule); }3)表单 表单控件目前只处理了input、select和button,它们中的文本会变透明,添加背景色,placeholder属性变空,如下所示。form(element) { element.style.color = "transparent"; //内容透明 element.style.background = this.color; //重置背景 element.setAttribute("placeholder", ""); //清除提示 this.formFn && this.formFn.call(this, element); //执行自定义着色规则 } formFn是一个特殊的参数,在插件初始化时可传递进来,因为表单比较复杂,所以要自定义着色规则。例如一些页面的表单结构是下面这样的,那么就需要将也添加背景色。<ul> <li class="ui-flex"> <input type="text" /> li> <li class="ui-flex"> <input type="text" /> li> ul> 自定义的着色规则如下所示,其中matches()是一个选择器匹配方法。new Skeleton({ formFn: function(element) { while(element && !this.matches(element, "li.ui-flex")) element = element.parentNode; element && (element.style.background = this.color); } }); matches(element, selector) { if (!selector || !element || element.nodeType !== NODE_ELEMENT) return false; const matchesSelector = element.webkitMatchesSelector || element.matchesSelector; return matchesSelector.call(element, selector);}4)移除 因为骨架屏的特点是快速,所以在生成时需要移除多余的元素,例如指定区域外的元素、隐藏的元素和脚本元素,如下所示,其中isHideStyle()函数可判断是否是隐藏元素。removeElement(parent) { if (parent.children.length == 0) return; //有移除操作,所以未用Array.from()遍历 for (let i = 0; i < parent.children.length; i++) { const element = parent.children[i], { top } = getRect(element); if ( isHideStyle(element) || //隐藏元素 top >= this.rectHeight || //超出指定高度 element.nodeName.toLowerCase() == "script" //脚本元素 ) { element.remove(); i--; continue; } this.removeElement(element); } } function isHideStyle(element) { return ( getStyle(element, "display") == "none" || getStyle(element, "visibility") == "hidden" || getStyle(element, "opacity") == 0 || element.hidden ); } 本来是想用Array.from()遍历元素,但删除后会影响迭代逻辑,因此改成了for循环语句。 除了这三类元素之外,还得将注释节点也一并删除,如下所示。注意,childNodes与上面的children属性不同,它能够通过forEach()遍历。removeNode(parent) { if (parent.childNodes.length == 0) return; for (let i = 0; i < parent.childNodes.length; i++) { const node = parent.childNodes[i]; if (node.nodeType === NODE_COMMENT) { node.remove(); i--; continue; } this.removeNode(node); } }5)绘制 绘制就是调用上面所提到的方法,包括移除元素、着色、替换图像等,具体如下所示。function draw() { this.container.style.background = "#FFF"; //容器背景重置 //移除元素和节点 this.removeElement(this.container); this.removeNode(this.container); //为文本添加 this.appendTextNode(this.container); //处理普通元素 Array.from( this.container.querySelectorAll( "div,section,footer,header,a,p,span,form,label,li" ) ).map(element => { //背景图或背景颜色的处理 const hasBg = getStyle(element, "background-image") != "none" || getStyle(element, "background-color") != "rgba(0, 0, 0, 0)"; if (hasBg) { this.image(element, false); } //文本处理 this.text(element); }); //处理表单中的控件 Array.from(this.container.querySelectorAll("input,select,button")).map( element => { this.form(element); } ); //元素处理 Array.from(this.container.querySelectorAll("img")).map(img => { this.image(img); }); }二、Puppeteer 插件完成后,没有做到自动化,即需要在浏览器的控制台中手工执行骨架屏插件。翻阅资料后,大家都推荐使用Puppeteer。Puppeteer是一个Node库,它提供了一个高级API来通过DevTools协议控制Chromium或Chrome。也就是说,它是一个无头(headless)浏览器。 一边翻资料,一边查看demo,尝试着写Node.js,后面跌跌撞撞的写出了可以执行的脚本。 原理就是先打开无头浏览器;然后输入视口参数和页面地址,并添加插件地址;然后在打开的页面中执行插件,返回document.body中的HTML代码;最后将HTML写入到一个txt文件中。const puppeteer = require('puppeteer'), fs = require('fs'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); //视口参数 await page.setViewport({width: 375, height: 667}); // 事件监听,可用于调试 page.on('console', msg => console.log('PAGE LOG:', msg.text())); // waitUntil 参数有四个关键字:load、domcontentload、networkidle0和networkidle2 await page.goto('http://www.pwstrick.com/index.html', {waitUntil: 'networkidle2'}); await page.addScriptTag({url: 'http://www.pwstrick.com/js/skeleton.js'}); // 对打开的页面进行操作 const html = await page.evaluate(() => { let sk = new Skeleton(); sk.draw(); return document.body.innerHTML; }); //将骨架屏代码添加到content.txt文件中 fs.writeFileSync('content.txt', html); await browser.close(); })(); 本来是想在page.evaluate()中将插件以参数的形式传入,但一直不成功,后面就改成了page.addScriptTag(),引用插件的脚本。 到目前为止,只能算是半自动化。要做到自动化,就得编写webpack插件,在打包的时候,将生成的HTML代码嵌入到页面中的指定位置,并且还要做到参数可配置化,以适合更多的场景。 整个骨架屏插件只有200多行代码,去掉注释和空行只有160多行,本插件主要用于学习。
自上一篇《每次阅读外文技术资料都头疼,终于知道原因了》已过去多月,最近又在做简单的实践,实践方式很朴素,就是对照阅读React官方资料和国内翻译的资料,逐句对比,发现了许多问题,特在此记录。一、问题记录1)with(配合、带)、for(用来)、by(通过)等介词在连接语句时的词义没有把握好。1、The watcher will create a preprocessed like_button.js with the plain JavaScript code suitable for the browser. 监听器会创建一个预处理过的like_button.js文件,它包含了适用于浏览器的普通JavaScript代码。 2、Use an integrated toolchain for the best user and developer experience. 使用集成的工具链,以实现最佳的用户和开发人员体验。 3、and optimizes your app for production. 并为生产环境优化你的应用程序。 4、you can access the navigation by pressing the button in the bottom right corner of your screen. 你可以通过点击屏幕右下角的按钮来查看导航栏。2)根据上下文语境,没有扩展出单词或语句的特殊含义。1、approachable toolchains 易上手的工具链 2、Knowledge Level Assumptions 预备知识 3、Many React users credit reading Thinking in React as the moment React finally “clicked” for them. 很多用户认为阅读React哲学是他们最终对React恍然大悟的时刻。 4、Creating a Toolchain from Scratch 从头开始打造工具链3)一碰到语句需要变序或者句子一长,就很难下手,无法准确的组织语句。1、and is the best way to start building a new single-page application in React. 也是用 React 创建新的单页应用的最佳方式。 2、This enables a better error handling experience in React 16 and later.(in提前) 这样能在 React 16 及以上的版本中有更好的错误处理体验。 3、You can learn most of React by reading the “Main Concepts” guide chapters in the order they appear in the sidebar. 你可以按照侧边导航栏中显示的顺序阅读浏览“核心概念”的指南章节。 4、thinking about how the UI should look at any given moment rather than how to change it over time eliminates a whole class of bugs. 考虑 UI 在任意给定时刻的状态,而不是随时间变化的过程,能够消灭一整类的bug。 5、running npm run build will create an optimized build of your app in the build folder. 执行npm run build会在build文件夹内生成你应用的优化版本。4)硬读,没有采取技巧。 第一天读的很慢,而且效率还很低,下午的时候头已经昏昏沉沉了,后面去网上查了些资料,总结出三个(如下所列),应用起来后,效率提升了很多。 (1)避免默读,就是心中不要将单词读出。因为当遇到不认识的单词时,会打断思路,卡住后就会影响后面的理解。 (2)意群阅读,也就是将句子分成几段,而不是一个个的读。如果逐个读,就无法全面的理解句意,因为很多时候是要换顺序的。 (3)句子成分,划分为主谓宾(主干部分)和定状补(修饰部分)两部分,长句很多是因为修饰比较长。 其实在上一篇英语总结中也介绍过阅读技巧,还详细介绍了各类基础语法和句法。5)词汇量太少,包含单词和词组都不够。 简单的诸如primarily(主要地)、above(上)、majority(大多数)、comfortable(适应)、deploy(部署)、production(生产环境)、specific(具体)、master(掌握)、essentially(本质上)、listener(监听器)等都不能脱口而出,复杂的像ecology(生态)、typo(错别字)、recap(概括)就更不用说了。 往往在读一句话时,会被几个关键单词所卡住,导致整句无法理解,例如下面这句。only a single instance of the Clock class will be used. 就仅有一个 Clock 组件的class实例被创建使用。 为了能更针对性的增加计算机相关的单词,这次特地从《计算机专业英语(第3版)》中整理出近千个单词,书的封面如下图所示。这些单词都是纯手打,可能会有错误,欢迎指正。二、计算机专业英语文章中的单词 总共包含300多个单词,要打印的话可以在此处下载Excel文档,Github上也放了一份,之所以放三列是为了在手机上看能方便点。顺便推荐一个能将表格数据转换成Markdown、HTML等格式的在线工具,下面的表格就是用该工具生成的。单词注释音标abate减轻,废除[əˈbeɪt]abstraction抽象,摘要[æbˈstrækʃn]abuse滥用[əˈbjuːz]accelerator加速器[əkˈseləreɪtə(r)]accessible易接近的[əkˈsesəbl]accountant会计[əˈkaʊntənt]accurate正确的[ˈækjərət]acronym首字母缩略字[ˈækrənɪm]activate激活[ˈæktɪveɪt]adequate足够的[ˈædɪkwət]adjacent领近的[əˈdʒeɪsnt]administrative管理的[ədˈmɪnɪstrətɪv]affordable负担得起[əˈfɔːdəbl]agency代理机构[ˈeɪdʒənsi]align匹配[əˈlaɪn]allot分配[əˈlɒt]analogous相似的[əˈnæləɡəs]analyst分析家[ˈænəlɪst]annulus环[ˈanyələs]anomalies异常[əˈnɒməliz]anonymity匿名[ˌænəˈnɪməti]anticipate预料[ænˈtɪsɪpeɪt]apparently清楚地[əˈpærəntli]appropriate适当的[əˈprəʊpriət]arena舞台[əˈriːnə]arithmetic算术[əˈrɪθmətɪk]assembler汇编程序[əˈsemblə(r)]assignment分配[əˈsaɪnmənt]assortment分类[əˈsɔːtmənt]attach依附上[əˈtætʃ]attribute属性[əˈtrɪbjuːt]audience观众[ˈɔːdiəns]authentic可信的[ɔːˈθentɪk]authentication认证[ɔːˌθentɪˈkeɪʃn]available可利用的,有效的[əˈveɪləbl]backbone骨干[ˈbækbəʊn]bacterial细菌的[bækˈtɪəriəl]behavior行为[bɪ'heɪvjə(r)]billion十亿[ˈbɪljən]bitmap位图[ˈbɪtmæp]bluetooth蓝牙['bluːˌtuːθ]boost促进[buːst]brute暴力的[bruːt]budget预算[ˈbʌdʒɪt]bureaucracy官僚机构[bjʊəˈrɒkrəsi]calculator计算器[ˈkælkjuleɪtə(r)]carburetor化油器[ˈkɑːbjʊrɛtə]cellular多孔的[ˈseljələ(r)]challenge挑战[ˈtʃælɪndʒ]choke窒息[tʃəʊk]churn搅拌,制造[tʃɜːn]ciphertext密文[ˈsaɪfə(r) tekst]coaxial共轴的[ˈkəʊˈæksɪəl]cognitive认识的[ˈkɒɡnətɪv]coherent一致的[kəʊˈhɪərənt]combination组合[ˌkɒmbɪˈneɪʃn]comfortable舒服的[ˈkʌmftəbl]commodity商品[kəˈmɒdəti]comparison比较,对照[kəmˈpærɪsn]competitive竞争的[kəmˈpetətɪv]comprehensive有理解力的[ˌkɒmprɪˈhensɪv]conclusion结论[kənˈkluːʒn]concurrently同时发生地[kənˈkʌrəntli]configuration结构[kənˌfɪɡəˈreɪʃn]confuse使混乱[kənˈfjuːz]connectivity连接[ˌkɒnekˈtɪvəti]consecutive连续的[kənˈsekjətɪv]considerable相当大的[kənˈsɪdərəbl]considerably非常地[kənˈsɪdərəbli]consistency一致[kənˈsɪstənsi]constraint强制[kənˈstreɪnt]consultation商议[ˌkɒnslˈteɪʃn]consumption消费[kənˈsʌmpʃn]contractor合同人[kənˈtræktə(r)]coordinate使协调[kəʊˈɔːdɪneɪt]cordless无绳的[ˈkɔːdləs]counteract抵消[ˌkaʊntərˈækt]coverage范围[ˈkʌvərɪdʒ]creativity创造力[ˌkriːeɪˈtɪvəti]credibility可靠的[ˌkredəˈbɪləti]criteria标准[kraɪ'tɪəriə]critical批评的[ˈkrɪtɪkl]crucial关键的[ˈkruːʃl]cryptographic加密的[ˈkrɪptoʊ ˈɡræfɪk]database数据库[ˈdeɪtəbeɪs]deceptive靠不住的[dɪˈseptɪv]decline下降[dɪˈklaɪn]decode译码[ˌdiːˈkəʊd]decompose分解[ˌdiːkəmˈpəʊz]deficit赤字[ˈdefɪsɪt]deflector导向装置[dɪˈflɛktə]demodulation解调denial否认[dɪˈnaɪəl]denote表示[dɪˈnəʊt]deploy部署[dɪˈplɔɪ]deposit保证金[dɪˈpɒzɪt]description描述[dɪˈskrɪpʃn]diagnose诊断[ˈdaɪəɡnəʊz]differentiate区别[ˌdɪfəˈrenʃieɪt]disaster灾害[dɪˈzɑːstə(r)]disburse付出[dɪsˈbɜːs]disguise伪装[dɪsˈɡaɪz]disposal支配权[dɪˈspəʊzl]dispute辩论[dɪˈspjuːt]dissect仔细分析[dɪˈsekt]diverse不同的[daɪˈvɜːs]domain领域[dəˈmeɪn]dramatically突然地[drəˈmætɪk(ə)li]drug药物[drʌɡ]eavesdrop偷听[ˈiːvzdrɒp]effortless容易的[ˈefətləs]elevate举起,提拔[ˈelɪveɪt]embedded嵌入到[ɪmˈbedɪd]embody使具体化[ɪmˈbɒdi]encapsulation封装encompass围绕[ɪnˈkʌmpəs]encounter相遇[ɪnˈkaʊntə(r)]encryption加密的[ɪnˈkrɪpʃ(ə)n]enhance提高[ɪnˈhɑːns]enterprise企业[ˈentəpraɪz]entrust信托[ɪnˈtrʌst]epidemics流行病[ˌɛpɪˈdɛmɪks]equivalent相等的[ɪˈkwɪvələnt]erase抹去[ɪˈreɪz]essential本质的[ɪˈsenʃl]establish建立[ɪˈstæblɪʃ]evaluate评估[ɪˈvæljueɪt]executable可执行的[ɪɡˈzekjətəbl]expenditure支出,开销[ɪkˈspendɪtʃə(r)]exponentiation幂运算extraction提取[ɪkˈstrækʃn]extrapolate推断[ɪkˈstræpəleɪt]facility天赋[fəˈsɪləti]fashion时尚[ˈfæʃn]feedback反馈[ˈfiːdbæk]fleet船队,车队[fliːt]flicker闪烁[ˈflɪkə(r)]footnote脚注[ˈfʊtnəʊt]forefront前沿[ˈfɔːfrʌnt]formidable艰难的[fəˈmɪdəbl]foster培养[ˈfɒstə(r)]fraud欺骗[frɔːd]funneled倾销[ˈfʌnld]gateway网关[ˈɡeɪtweɪ]gracefully优美地[ˈgreɪsfʊli]granular颗粒状的[ˈɡrænjələ(r)]guidance指导[ˈɡaɪdns]handheld手持[ˈhændheld]hazardous危险地[ˈhæzədəs]headphone耳机[ˈhɛdfəʊn]heap堆[hiːp]heterogeneous各种各样的[ˌhetərəˈdʒiːniəs]hierarchical分层的[ˌhaɪəˈrɑːkɪkl]hierarchy分层,等级[ˈhaɪərɑːki]hire租用[ˈhaɪə(r)]horizontal axis横轴[ˌhɒrɪˈzɒntl ˈæksɪs]hostile敌对的[ˈhɒstaɪl]hybrid混合的[ˈhaɪbrɪd]identical完全一样的[aɪˈdentɪkl]illusion幻想[ɪˈluːʒn]implicit隐示的[ɪmˈplɪsɪt]inability无能[ˌɪnəˈbɪləti]inbound入境的[ˈɪnbaʊnd]inconsistency不一致[ˌɪnkənˈsɪstənsi]indispensable不可缺少的[ˌɪndɪˈspensəbl]infrastructure基础设施[ˈɪnfrəstrʌktʃə(r)]ingredient组成[ɪnˈɡriːdiənt]inheritance继承[ɪnˈherɪtəns]initiate开始[ɪˈnɪʃieɪt]injection注入[ɪnˈdʒekʃn]ink jet printer喷墨打印机[ɪŋk dʒet ˈprɪntə(r)]innovative改革[ˈɪnəveɪtɪv]instantaneous即时的[ˌɪnstənˈteɪniəs]insurance保险[ɪnˈʃʊərəns]integral整体的[ˈɪntɪɡrəl]interaction交互作用[ˌɪntərˈækʃən]interference干扰[ˌɪntəˈfɪərəns]interrogator询问者[ɪnˈtɛrəʊgeɪtə]intervention干预[ˌɪntəˈvenʃn]introspection反省[ˌɪntrəˈspekʃn]intruder侵入者[ɪnˈtruːdə(r)]invest投资[ɪnˈvest]invoice发票[ˈɪnvɔɪs]involvement参与[ɪnˈvɒlvmənt]irrelevant不相关的[ɪˈreləvənt]isolate隔离[ˈaɪsəleɪt]kilobyte千字节[ˈkɪləbaɪt]laptop便携式计算机[ˈlæptɒp]leap跳过[liːp]ledger分类账簿[ˈledʒə(r)]license许可证[ˈlaɪsns]logistics物流,后勤[ləˈdʒɪstɪks]lossless无损的[ˈlɒsləs]malicious恶意的[məˈlɪʃəs]managerial管理的[ˌmænəˈdʒɪəriəl]massive宽大的,宏伟的[ˈmæsɪv]mastery精通[ˈmɑːstəri]maturity成熟[məˈtʃʊərəti]mechanism机制,方法[ˈmekənɪzəm]megabyte兆字节[ˈmeɡəbaɪt]megahertz兆赫兹[ˈmeɡəhɜːts]merchandise商品[ˈmɜːtʃəndaɪz]merge使合并[mɜːdʒ]metropolitan大城市的[ˌmetrəˈpɒlɪtən]microphone麦克风[ˈmaɪkrəfəʊn]middleware中间件[ˈmɪdlweə(r)]mimic模仿[ˈmɪmɪk]modifiable可修改的[ˈmɒdɪfaɪəbl]modular模块化的[ˈmɒdjələ(r)]modulation调制,缓和[ˌmɒdjʊˈleɪʃən]monetary货币的[ˈmʌnɪtri]monochrome单色的,黑白的[ˈmɒnəkrəʊm]monolithic整体的[ˌmɒnəˈlɪθɪk]morph变种[mɔːf]multidimensional多维的[ˌmʌltɪdɪˈmɛnʃənl]needle针[ˈniːdl]negotiation谈判[nɪˌɡəʊʃiˈeɪʃn]neighbor邻居['neɪbə(r)]nickname绰号[ˈnɪkneɪm]notation符号,记号[nəʊˈteɪʃn]nozzle喷嘴[ˈnɒzl]obedient服从的[əˈbiːdiənt]observation观察[ˌɒbzəˈveɪʃn]obsolescence淘汰[ˌɒbsəˈlesns]obstacle障碍[ˈɒbstəkl]occupy占领[ˈɒkjupaɪ]ontology本体论[ɒnˈtɒlədʒi]operand操作数[ˈɒpərænd]orthogonal垂直的[ɔːˈθɒgənəl]outbound出境的[ˈaʊtbaʊnd]outcome结果[ˈaʊtkʌm]out-of-date过时的[ˌaʊt əv ˈdeɪt]overlap重叠[ˌəʊvəˈlæp]overt公开的[əʊˈvɜːt]overwhelming压倒性的[ˌəʊvəˈwelmɪŋ]partition分割[pɑːˈtɪʃn]pattern模式[ˈpætn]payment付款[ˈpeɪmənt]payroll工资表[ˈpeɪrəʊl]penetrate渗透[ˈpenətreɪt]perceive感知,领悟[pəˈsiːv]perceptible可察觉的[pəˈseptəbl]perform执行[pəˈfɔːm]performance表现,性能[pəˈfɔːməns]periodically周期性地[ˌpɪərɪˈɒdɪkəli]permeation渗透permission许可[pəˈmɪʃn]pertinent恰当的[ˈpɜːtɪnənt]phenomenon现象[fəˈnɒmɪnən]philosophy哲学[fəˈlɒsəfi]pillar台柱[ˈpɪlə(r)]pixel像素[ˈpɪksl]plaintext明文polymorphism多态性[ˌpɒlɪˈmɔːfɪzm]portable手提的[ˈpɔːtəbl]powder粉[ˈpaʊdə(r)]preconceive事先认为[ˌpriːkənˈsiːv]prediction预言[prɪˈdɪkʃn]prescribe命令[prɪˈskraɪb]prevailing优势的,普遍的[prɪˈveɪlɪŋ]primitive原始的,基础的[ˈprɪmətɪv]priority优先[praɪˈɒrəti]privilege特权[ˈprɪvəlɪdʒ]problematic疑难的[ˌprɒbləˈmætɪk]procure获得[prəˈkjʊə(r)]profession职业[prəˈfeʃn]profit利润[ˈprɒfɪt]progress进展[ˈprəʊɡres]prohibitive禁止地[prəˈhɪbətɪv]proliferate扩散[prəˈlɪfəreɪt]property性质,财产[ˈprɒpəti]proportional成比例的[prəˈpɔːʃənl]psychology心理学[saɪˈkɒlədʒi]purchase购买[ˈpɜːtʃəs]pursue追求[pəˈsjuː]pursuit追踪[pəˈsjuːt]quotation引用,引文[kwəʊˈteɪʃn]quote报价,引用[kwəʊt]rack支架[ræk]rationality合理性[ˌræʃə'nælətɪ]reallocated再分配[ˌriːˈæləkeɪtɪd]recipe食谱[ˈresəpi]recognition识别[ˌrekəɡˈnɪʃn]reference参考,索引[ˈrefrəns]reflect反映,表达[rɪˈflekt]register寄存器[ˈredʒɪstə(r)]regular常规的[ˈreɡjələ(r)]relevance重要性[ˈreləvəns]remarkable显著的[rɪˈmɑːkəbl]remedy修补,纠正[ˈremədi]repairperson维修人员represent表示,代表[ˌreprɪˈzent]representative典型的[ˌreprɪˈzentətɪv]resemble相似[rɪˈzembl]resolution分辨率[ˌrezəˈluːʃn]restrict限制[rɪˈstrɪkt]resume概述[rɪˈzjuːm]retrieve检索,恢复[rɪˈtriːv]reusability可重用性reused重复使用[ˌriːˈjuːzd]revive复苏[rɪˈvaɪv]roam漫游[rəʊm]roughly粗略地[ˈrʌfli]router路由器[ˈruːtə(r)]savvy精明的[ˈsævi]schema模式,纲要[ˈskiːmə]scratch凑合的[skrætʃ]seamless无缝的[ˈsiːmləs]seemingly外观上地[ˈsiːmɪŋli]segmentation分割[ˌseɡmenˈteɪʃn]separation分离[ˌsepəˈreɪʃn]session期间,会议[ˈseʃn]shielding屏蔽[ˈʃiːldɪŋ]significance意义,重要性[sɪɡˈnɪfɪkəns]simplicity简单[sɪmˈplɪsəti]simplistic过分简单化的[sɪmˈplɪstɪk]sophisticated复杂的[səˈfɪstɪkeɪtɪd]specification规格,说明[ˌspesɪfɪˈkeɪʃn]spray喷洒[spreɪ]spreadsheet电子表格[ˈspredʃiːt]stakeholder利益共享者[ˈsteɪkhəʊldə(r)]statistic统计学的[stəˈtɪstɪk]stick粘,坚持[stɪk]strategic战略性的[strəˈtiːdʒɪk]subclass子类submit提交[səbˈmɪt]subroutine子程序[ˈsʌbruːtiːn]superfluous多余的,过剩的[suːˈpɜːfluəs]supervise监督,指导[ˈsuːpəvaɪz]surgeon外科医生[ˈsɜːdʒən]suspend暂停[səˈspend]sustain维持[səˈsteɪn]synthesis合成[ˈsɪnθəsɪs]telecommunication电信[ˌtelikəˌmjuːnɪˈkeɪʃn]temporary临时[ˈtemprəri]tendency趋势[ˈtendənsi]tender温柔的[ˈtendə(r)]terminate结束[ˈtɜːmɪneɪt]terseness简洁threat威胁[θret]token令牌[ˈtəʊkən]topology拓扑结构[tə'pɒlədʒɪ]transact办理[trænˈzækt]transaction事务[trænˈzækʃn]transparent显而易见的[trænsˈpærənt]trillion万亿,兆[ˈtrɪljən]troubleshoot排除故障[ˈtrʌblʃuːt]trust信任[trʌst]turbocharger涡轮增压器[ˈtɜːbəʊtʃɑːdʒə(r)]turn off关闭[ˈtɜːn ɒf]tutorial教程[tjuːˈtɔːriəl]twitter推特[ˈtwɪtə(r)]ubiquitous普遍存在的[juːˈbɪkwɪtəs]ubiquity随处可见[juːˈbɪkwəti]uncountable无数的[ʌnˈkaʊntəbl]undergo经历[ˌʌndəˈɡəʊ]uniformity一致[ˌjuːnɪˈfɔːməti]uniquely唯一地[juː'niːkli]unpredictable不可预知的[ˌʌnprɪˈdɪktəbl]unreliable不可靠的[ˌʌnrɪˈlaɪəbl]unsurpassed出类拔萃的[ˌʌnsəˈpɑːst]upgrade升级[ˌʌpˈɡreɪd]utilization利用[ˌjuːtəlaɪˈzeɪʃn]utmost极度的[ˈʌtməʊst]vacuum真空[ˈvækjuːm]vertical axis纵轴[ˈvɜːtɪkl ˈæksɪs]violation违反[ˌvaɪə'leɪʃ(ə)n]voluminous大的[vəˈluːmɪnəs]vulnerability弱点[ˌvʌlnərə'bɪlətɪ]warehouse仓库[ˈweəhaʊs]wavelength波长[ˈweɪvleŋθ]whereas然而[ˌweərˈæz]wireless无线的[ˈwaɪələs]zero stock零库存[ˈzɪərəʊ stɒk] 三、附录中的专业英语词汇表 总共包含700多个单词,与书中的单词有些会重复,要打印的话可以在此处下载Excel文档,GitHub上也放了一份。单词注释音标abnormal end异常终止[æbˈnɔːml end]abstract data type抽象数据类型[ˈæbstrækt ˈdeɪtə taɪp]acceleration card加速卡[əkˌseləˈreɪʃn kɑːd]access control访问控制[ˈækses kənˈtrəʊl]access list访问控制表[ˈækses lɪst]access permission访问权限[ˈækses pəˈmɪʃn]access time存取(访问)时间[ˈækses taɪm]accessibility易接近的,可到达的[əkˌsɛsɪˈbɪlɪti]accessory program附件程序account账号[əˈkaʊnt]accounting software会计软件[əˈkaʊntɪŋ ˈsɒftweə(r)]acoustic声音的[əˈkuːstɪk]acronym缩略词[ˈækrənɪm]active desktop动态桌面[ˈæktɪv ˈdesktɒp]active window激活窗口[ˈæktɪv ˈwɪndəʊ]acyclic directory structure非循环目录结构adapter card适配卡[əˈdæptə kɑːd]adaptive scheduler自适应调度address space地址空间[əˈdres speɪs]addressing mechanism寻址机制[əˈdresɪŋ ˈmekənɪzəm]administrator管理员[ədˈmɪnɪstreɪtə(r)]algorithm算法[ˈælɡərɪðəm]alignment队列[əˈlaɪnmənt]alphabetic依字母顺序的[ˌælfəˈbetɪk]alphanumeric字母数字的[ˌælfənjuːˈmerɪk]amplify放大[ˈæmplɪfaɪ]animation动画[ˌænɪˈmeɪʃn]anti-virus program防病毒程序[ˈænti ˈvaɪrəs ˈprəʊɡræm]application integration应用程序集成[ˌæplɪˈkeɪʃn ˌɪntɪˈɡreɪʃn]application layer应用层[ˌæplɪˈkeɪʃn ˈleɪə(r)]application object应用对象[ˌæplɪˈkeɪʃn ˈɒbdʒɪkt]archiving存档[ˈɑːkaɪvɪŋ]arrow key箭头键,方向键[ˈærəʊ kiː]artificial intelligence人工智能[ˌɑːtɪfɪʃl ɪnˈtelɪdʒəns]assembler汇编程序[əˈsemblə(r)]assessment评估[əˈsesmənt]assignment分配[əˈsaɪnmənt]assortment分类[əˈsɔːtmənt]asymmetric encryption非对称加密[ˌeɪsɪˈmetrɪk ɪnˈkrɪpʃ(ə)n]asynchronous异步的[eɪˈsɪŋkrənəs]asynchronous primitive异步原语atomic action原子操作[əˈtɒmɪk ˈækʃn]atomicity property原子属性attachment附件[əˈtætʃmənt]attribute属性,标志[əˈtrɪbjuːt]auction online在线拍卖[ˈɔːkʃn ˌɒnˈlaɪn]audience demographic受众人数统计authentication身份验证[ɔːˌθentɪˈkeɪʃn]authorization授权,认可[ˌɔːθəraɪˈzeɪʃn]automation server自动化服务器[ˌɔːtəˈmeɪʃn ˈsɜːvə(r)]auxiliary辅助的[ɔːɡˈzɪliəri]background后台,背景[ˈbækɡraʊnd]bandwidth带宽[ˈbændwɪdθ]banner横幅[ˈbænə(r)]bar code条形码[bɑː(r) kəʊd]base-band基带[beɪs bænd]batch processing批处理[ˌbætʃ ˈprəʊsesɪŋ]baud波特[bɔːd]baud rate波特率[bɔːd reɪt]bibliography参考文献[ˌbɪbliˈɒɡrəfi]big data大数据[ˌbɪɡ ˈdeɪtə]binary digit二进制数字[ˈbaɪnəri ˈdɪdʒɪt]bind绑定[baɪnd]biometrics生物统计学[ˌbaɪəʊˈmɛtrɪks]biometrics device生物特征辨识装置bitable双稳态的bitmap位图[ˈbɪtmæp]black box黑匣子[ˌblæk ˈbɒks]block diagram框图[ˈblɒk daɪəɡræm]block structure模块化结构[blɒk ˈstrʌktʃə(r)]boolean logic布尔逻辑[ˈbuːliən ˈlɒdʒɪk]boot block引导块,启动块[buːt blɒk]boot sector引导扇区[buːt ˈsektə(r)]boot strap引导程序[buːt stræp]boot up启动[buːt ʌp]boundary界限,边界[ˈbaʊndri]branch分支[brɑːntʃ]broadband network宽带网络[ˈbrɔːdbænd ˈnetwɜːk]broadcast address广播地址[ˈbrɔːdkɑːst əˈdres]broadcast storm广播风暴[ˈbrɔːdkɑːst stɔːm]browser浏览器[ˈbraʊzə(r)]bubble jet printer喷墨打印机[ˈbʌbl dʒet ˈprɪntə(r)]built-in内置的,嵌入式的[ˌbɪlt ˈɪn]bulk storage大容量存储器[bʌlk ˈstɔːrɪdʒ]bulletin board公告板[ˈbʊlətɪn bɔːd]bus-contention总线争用calibration校准[ˌkælɪˈbreɪʃn]carriage return回车[ˈkærɪdʒ rɪˈtɜːn]carrier载体[ˈkæriə(r)]cartography绘图法[kɑːˈtɒɡrəfi]certificate authority证书认证,认证机构[səˈtɪfɪkət ɔːˈθɒrəti]chain reaction链式反应[ˌtʃeɪn riˈækʃn]channel信道[ˈtʃænl]chassis机箱[ˈʃæsi]check box复选框[tʃek bɒks]child node子节点[tʃaɪld nəʊd]child window子窗口[tʃaɪld ˈwɪndəʊ]chipset芯片组cipher text密文[ˈsaɪfə(r) tekst]circuit switching电路交换[ˈsɜːkɪt ˈswɪtʃɪŋ]classification分级,分类[ˌklæsɪfɪˈkeɪʃn]client program客户端程序[ˈklaɪənt ˈprəʊɡræm]client server客户机服务器[ˌklaɪənt ˈsɜːvə(r)]clipboard剪贴板[ˈklɪpbɔːd]cloud computing云计算[ˈklaʊd kəmpjuːtɪŋ]coaxial cable同轴电缆[ˈkəʊˈæksɪəl ˈkeɪbl]coding编码[ˈkəʊdɪŋ]cognitive认知的,认识的[ˈkɒɡnətɪv]coherent一致的,连贯的[kəʊˈhɪərənt]collaborative合作的,协作的[kəˈlæbərətɪv]command button命令按钮[kəˈmɑːnd ˈbʌtn]comment注释,评论[ˈkɒment]communication deadlock通信死锁[kəˌmjuːnɪˈkeɪʃn ˈdedlɒk]compatibility兼容性[kəmˌpætəˈbɪləti]compiler编译程序,编译器[kəmˈpaɪlə(r)]compression压缩分布式处理[kəmˈprɛʃən]computability可计算性computerize计算机化[kəmˈpjuːtəraɪz]concurrent并发的[kənˈkʌrənt]confidential机密的[ˌkɒnfɪˈdenʃl]configuration配置[kənˌfɪɡəˈreɪʃn]conflict冲突[ˈkɒnflɪkt]congestion拥塞[kənˈdʒestʃən]connectionless service无连接服务[kəˈnɛkʃənləs ˈsɜːvɪs]connection-oriented service面向连接的服务[kəˈnekʃn ˈɔːrientɪd ˈsɜːvɪs]constraint约束[kənˈstreɪnt]construct构造[kənˈstrʌkt]container容器[kənˈteɪnə(r)]context上下文[ˈkɒntekst]control box控制箱[kənˈtrəʊl bɒks]control panel控制面板[kənˈtrəʊl ˈpænl]cooling fan冷却风扇[ˈkuːlɪŋ fæn]coordinate协调,坐标[kəʊˈɔːdɪneɪt]cordless mouse无线鼠标[ˈkɔːdləs maʊs]core磁芯[kɔː(r)]correlation相关性[ˌkɒrəˈleɪʃn]cracker解密者[ˈkrækə(r)]criteria标准[kraɪ'tɪəriə]critical region临界区[ˈkrɪtɪkl ˈriːdʒən]cross platform跨平台的[ˌkrɒs ˈplætfɔːm]cryptography密码学[krɪpˈtɒɡrəfi]cybercash电子货币cybercrime网络犯罪cyberspace网络空间[ˈsaɪbəspeɪs]data flow diagram数据流程图[ˈdeɪtə fləʊ ˈdaɪəɡræm]data link layer数据链路层[ˈdeɪtə lɪŋk ˈleɪə(r)]data mining数据挖掘[ˈdeɪtə maɪnɪŋ]data model数据模型[ˈdeɪtə ˈmɒdl]data source数据源[ˈdeɪtə sɔːs]data stream数据流[ˈdeɪtə striːm]data structure数据结构[ˈdeɪtə ˈstrʌktʃə(r)]data transfer rate数据传输速率[ˈdeɪtə trænsˈfɜː(r) reɪt]data warehouse数据仓库[ˈdeɪtə weəhaʊs]data window object数据窗口对象[ˈdeɪtə ˈwɪndəʊ ˈɒbdʒɪkt]database数据库[ˈdeɪtəbeɪs]database engine数据库引擎[ˈdeɪtəbeɪs ˈendʒɪn]database interface数据库接口[ˈdeɪtəbeɪs ˈɪntəfeɪs]database server数据库服务器[ˈdeɪtəbeɪs ˈsɜːvə(r)]deadline最终期限,截止日期[ˈdedlaɪn]deadlock死锁[ˈdedlɒk]debit-card借记卡debugger调试程序[ˌdiːˈbʌɡə(r)]decision tree决策树[dɪˈsɪʒn triː]decoder译码器,解码器[ˌdiːˈkəʊdə(r)]decryption解密definition定义[ˌdefɪˈnɪʃn]deformation变形[ˌdiːfɔːˈmeɪʃn]demodulator解调器[ˌdiˌmɑdʒəˈleɪtər]demographic人口统计的[ˌdeməˈɡræfɪk]desktop桌面[ˈdesktɒp]destination目的地,目标文件[ˌdestɪˈneɪʃn]device contention设备竞争[dɪˈvaɪs kənˈtenʃn]device dependent设备相关的[dɪˈvaɪs dɪˈpendənt]device independent设备无关的[dɪˈvaɪs ˌɪndɪˈpendənt]device object设备对象[dɪˈvaɪs ˈɒbdʒɪkt]diagnosis诊断[ˌdaɪəɡˈnəʊsɪs]diagram图表[ˈdaɪəɡræm]dialog box对话框[ˈdaɪəlɒɡ bɒks]digital camera数码相机[ˈdɪdʒɪtl ˈkæmərə]digital cash数字现金[ˈdɪdʒɪtl kæʃ]digital certificate数字证书[ˈdɪdʒɪtl səˈtɪfɪkət]digital signature数字签名[ˌdɪdʒɪtl ˈsɪɡnətʃə(r)]digital wallet数字钱包[ˈdɪdʒɪtl ˈwɒlɪt]directory目录[dəˈrektəri]discrete mathematics离散数学[dɪˈskriːt ˌmæθəˈmætɪks]disk cleanup磁盘清理[dɪsk ˈkliːnʌp]disk defragmenter磁益碎片整理工具disk drive磁盘驱动器[ˈdɪsk draɪv]diskette软盘[dɪsˈket]diskless workstation无盘工作站display adapter显示适配器[dɪˈspleɪ əˈdæptə]dissertation论文[ˌdɪsəˈteɪʃn]distributed database分布式数据库[dɪˈstrɪbjuːtɪd ˈdeɪtəbeɪs]distributed processing分布式处理[dɪˈstrɪbjuːtɪd ˈprəʊsesɪŋ]distributed system分布式系统[dɪˌstrɪbjuːtɪd ˈsɪstəm]dot-matrix点阵[dɒt ˈmeɪtrɪks]downstream向下传输[ˌdaʊnˈstriːm]drop-down listbox下拉式列表框drop-down menu下拉式菜单[ˌdrɒp daʊn ˈmenjuː]drum硒鼓[drʌm]dynamic binding动态绑定[daɪˈnæmɪk ˈbaɪndɪŋ]dynamic IP address动态IP地址[daɪˈnæmɪk ɪp əˈdres]dynamic page动态页面[daɪˈnæmɪk peɪdʒ]eavesdropping窃听[ˈiːvzdrɒpɪŋ]edit编辑[ˈedɪt]efficiency有效性,效率[ɪˈfɪʃnsi]electronic banking电子银行[ɪˌlekˈtrɒnɪk ˈbæŋkɪŋ]electronic cash电子现金[ɪˌlekˈtrɒnɪk kæʃ]electronic check电子支票[ɪˌlekˈtrɒnɪk tʃek]electronic commerce(EC)电子商务[ɪˌlekˈtrɒnɪk ˈkɒmɜːs]electronic mail(E-mail)电子邮件[ɪˌlektrɒnɪk ˈmeɪl]electronic meeting电子会议[ɪˌlekˈtrɒnɪk ˈmiːtɪŋ]electronic money电子货币[ɪˌlekˈtrɒnɪk ˈmʌni]electronic payment电子支付[ɪˌlekˈtrɒnɪk ˈpeɪmənt]electronic wallet电子钱包[ɪˌlekˈtrɒnɪk ˈwɒlɪt]embedded computer嵌入式计算机[ɪmˈbedɪd kəmˈpjuːtə(r)]embedded real-time system嵌入式实时系统[ɪmˈbedɪd ˈriːəl taɪm ˈsɪstəm]emoticon表情符号[ɪˈməʊtɪkɒn]emulation仿真[ˌɛmjʊˈleɪʃən]encapsulation封装encode编码[ɪnˈkəʊd]encryption加密[ɪnˈkrɪpʃ(ə)n]encryption key加密密钥[ɪnˈkrɪpʃ(ə)n kiː]end user终端用户[ˌend ˈjuːzə(r)]enquiry询问[ɪnˈkwaɪəri]entity实体[ˈentəti]enumerate枚举[ɪˈnjuːməreɪt]equivalent相等的[ɪˈkwɪvələnt]erasure消除[ɪˈreɪʒə(r)]etched circuit蚀刻电路[etʃt ˈsɜːkɪt]etching technology蚀刻技术[ˈetʃɪŋ tekˈnɒlədʒi]Ethernet以太网[ˈiːθənet]evolution演化[ˌiːvəˈluːʃn]exception异常[ɪkˈsepʃn]expanded memory扩充内存[ɪkˈspændɪd ˈmeməri]expansion slot扩展插槽[ɪkˈspænʃn slɒt]expert system专家系统[ˌekspɜːt ˈsɪstəm]expertise专业知识[ˌekspɜːˈtiːz]exponential指数的,幂的[ˌekspəˈnenʃl]extended attribute扩展属性[ɪkˈstendɪd əˈtrɪbjuːt]extended memory扩展内存[ɪkˈstendɪd ˈmeməri]extension扩展名[ɪkˈstenʃn]facilitate使方便,使[fəˈsɪlɪteɪt]facility功能,工具[fəˈsɪləti]factor因素[ˈfæktə(r)]fatal error致命错误[ˈfeɪtl ˈerə(r)]fault tolerance容错[fɔːlt ˈtɒlərəns]feasibility可行性[ˌfiːzəˈbɪlɪti]feature特征,特色[ˈfiːtʃə(r)]feedback反馈[ˈfiːdbæk]fiber-optic cable光纤[faɪbə ˈɒptɪk ˈkeɪbl]field字段(数据库的表)[fiːld]file handle文件句柄[faɪl ˈhændl]file server文件服务器[faɪl ˈsɜːvə(r)]file system文件系统[faɪl ˈsɪstəm]filter过滤器[ˈfɪltə(r)]fingerprint scanner指纹扫描仪[ˈfɪŋɡəprɪnt ˈskænə(r)]firewall防火墙[ˈfaɪəwɔːl]firmware固件[ˈfɜːmweə(r)]flash memory闪存[ˈflæʃ meməri]flexibility弹性,适应性[ˌfleksə'bɪləti]floppy disk软盘[ˌflɒpi ˈdɪsk]flow control流量控制[fləʊ kənˈtrəʊl]flowchart流程图,框图[ˈfləʊtʃɑːt]folder文件夹[ˈfəʊldə(r)]font format字体格式[fɒnt ˈfɔːmæt]foreground job前台作业[ˈfɔːɡraʊnd dʒəʊb]foreign agent外地代理[ˈfɒrən ˈeɪdʒənt]foreign key外键[ˈfɒrən kiː]format格式化[ˈfɔːmæt]fragmentation碎片[ˌfræɡmenˈteɪʃn]front-end前端[ˈfrʌnt end]function key功能键[ˈfʌŋkʃn kiː]game theory博弈论[ˈɡeɪm θɪəri]gateway网关[ˈɡeɪtweɪ]gigabit network千兆网[ˈɡɪɡəbɪt ˈnetwɜːk]global scheduler全局调度graphics package图形包[ˈɡræfɪks ˈpækɪdʒ]graphics tablet图形输入板[ˈɡræfɪks tæblət]grayscale灰色[ˈɡreɪskeɪl]grid格子,栅格[ɡrɪd]group editor组编辑器groupware组件[ˈɡruːpweə(r)]guarantee保证[ˌɡærənˈtiː]guidance向导,指导[ˈɡaɪdns]hacker黑客[ˈhækə(r)]hanging indent悬挂式缩进[ˈhæŋɪŋ ɪnˈdent]hardcopy硬拷贝hashed file散列文件[hæʃt faɪl]head node头节点[hed nəʊd]head pointer头指针[hed ˈpɔɪntə(r)]header and footer页眉和页脚[ˈhedə(r) ənd ˈfʊtə(r)]headline大字标题[ˈhedlaɪn]heap sort堆排序[hiːp sɔːt]hexadecimal system十六进制[ˌheksəˈdesɪml ˈsɪstəm]hierarchical directory structure层次目录结构[ˌhaɪəˈrɑːkɪkl dəˈrektəri ˈstrʌktʃə(r)]high-level language高级语言[haɪ ˈlevl ˈlæŋɡwɪdʒ]histogram柱状图[ˈhɪstəɡræm]home page主页[ˈhəʊm peɪdʒ]host computer主机[həʊst kəmˈpjuːtə(r)]hub集线器[hʌb]Huffman codes哈夫曼编码[ˈhʌfmən koʊdz]Huffman tree哈夫曼树[ˈhʌfmən triː]hyperlink超链接[ˈhaɪpəlɪŋk]hypertext超文本[ˈhaɪpətekst]hypothetical假设的,假定的[ˌhaɪpəˈθetɪkl]icon图标[ˈaɪkɒn]image图像[ˈɪmɪdʒ]image map图像映射[ˈɪmɪdʒ mæp]immovable固定的,不可移动的[ɪˈmuːvəbl]implementation执行[ˌɪmplɪmɛnˈteɪʃən]index索引[ˈɪndeks]individual个人,个体[ˌɪndɪˈvɪdʒuəl]infection传染[ɪnˈfekʃn]information superhighway信息高速公路[ˌɪnfəˈmeɪʃn ˌsuːpəˈhaɪweɪ]inheritance继承[ɪnˈherɪtəns]initialized已初始化的[ɪˈnɪʃəlaɪzd]instruction指令[ɪnˈstrʌkʃn]integrated package集成软件包[ˈɪntɪɡreɪtɪd ˈpækɪdʒ]integration整合[ˌɪntɪˈɡreɪʃn]integrity完整性[ɪnˈteɡrəti]intelligent bridge智能网桥intercept拦截[ˌɪntəˈsept]interception侦听[ˌɪntə(ː)ˈsɛpʃən]interface接口,界面[ˈɪntəfeɪs]interlacing隔行扫描的[ˌɪntəˈleɪsɪŋ]internal memory内存储器[ɪnˈtɜːnl ˈmeməri]Internet互联网[ˈɪntənet]Internet of things物联网[ˈɪntənet ɒv θɪŋz]Internet telephone网络电话[ˈɪntənet ˈtelɪfəʊn]interpreter解释器,解释程序[ɪnˈtɜːprətə(r)]interrupt中断[ˌɪntəˈrʌpt]interval时间间隔[ˈɪntəvl]inkjet printer喷墨打印机[ˌɪŋkdʒet ˈprɪntə(r)]intranet内网[ˈɪntrənet]inventory库存[ˈɪnvəntri]inversion反转,倒置[ɪnˈvɜːʃn]IP address网际协议地址[ˌaɪ ˈpiː ədres]iterative process迭代过程[ˈɪtərətɪv ˈprəʊses]job object作业对象[dʒəʊb ˈɒbdʒɪkt]job scheduler作业调度程序[dʒəʊb ˈʃɛdjuːlə]joystick操纵杆[ˈdʒɔɪstɪk]junk E-mail垃圾邮件[dʒʌŋk iː meɪl]jurisdiction权限[ˌdʒʊərɪsˈdɪkʃn]justification对齐[ˌdʒʌstɪfɪˈkeɪʃn]just-in-time manufacturing即时生产[dʒʌst ɪn taɪm ˌmænjuˈfæktʃərɪŋ]kernel核心,内核[ˈkɜːnl]keyboard键盘[ˈkiːbɔːd]keyguard键盘守卫[kiː ɡɑːd]keypad辅助小键盘[ˈkiːpæd]keyword关键字[ˈkiːwɜːd]kilobit千比特,千位[ˈkɪləbɪt]kilobyte千字节[ˈkɪləbaɪt]kit套件[kɪt]label标记,记号[ˈleɪbl]laser printer激光打印机[ˈleɪzə prɪntə(r)]laser-etched激光蚀刻的layer层[ˈleɪə(r)]layout规划,布局[ˈleɪaʊt]leased line专线[liːst laɪn]legitimacy合法性,正统性[lɪ'dʒɪtɪməsi]letter quality字符模式[ˈletə(r) ˈkwɒləti]library库[ˈlaɪbrəri]life cycle生命周期[ˈlaɪf saɪkl]light-pen光笔[ˈlaɪt pen]linear linked lists线性链表linear lists线性表[ˈlɪniə(r) lɪsts]link链接[lɪŋk]linkage连锁[ˈlɪŋkɪdʒ]linked radix sort链式基数排序load装载,装入[ləʊd]local scheduler本地调度local variable局部变量.[ˈləʊkl ˈveəriəbl]location定位,位置[ləʊˈkeɪʃn]log on登录[lɒɡ ɒn]log out注销[lɒɡ aʊt]logic circuit逻辑电路[ˈlɒdʒɪk sɜːkɪt]logic complementation逻辑补码法logical way逻辑方式logical link control逻辑链路控制[ˈlɒdʒɪkl lɪŋk kənˈtrəʊl]logical schema逻辑架构[ˈlɒdʒɪkl ˈskiːmə]login注册,登录[ˈlɒɡɪn]logistics后勤,物流[ləˈdʒɪstɪks]low-level language低级语言[ləʊ ˈlevl ˈlæŋɡwɪdʒ]machine code机器码[məˈʃiːn kəʊd]machine language机器语言[məˈʃiːn ˈlæŋɡwɪdʒ]macro宏[ˈmækrəʊ]magnetic pot磁场magnetic tape磁带[mæɡˌnetɪk ˈteɪp]magnification扩大,放大率[ˌmæɡnɪfɪˈkeɪʃn]mail merging邮件合并[meɪl ˈmɜːdʒɪŋ]mailbomb邮件炸弹main memory主存储器[meɪn ˈmeməri]main window主窗口[meɪn ˈwɪndəʊ]mainframe大型机,主机[ˈmeɪnfreɪm]maintain维护[meɪnˈteɪn]maintenance维持,维护[ˈmeɪntənəns]malignant恶性的[məˈlɪɡnənt]manipulation处理,操作[məˌnɪpjʊˈleɪʃən]manufacturer制造者,厂商[ˌmænjuˈfæktʃərə(r)]masked edit屏蔽编辑[mɑːskt ˈedɪt]match匹配,相配[mætʃ]matrix矩阵[ˈmeɪtrɪks]mechanism机构,机制[ˈmekənɪzəm]media player媒体播放器[ˈmiːdiə ˈpleɪə(r)]medium媒介,媒体[ˈmiːdiəm]megabit兆位,百万位[ˈmeɡəbɪt]megahertz兆赫兹[ˈmeɡəhɜːts]memory card内存卡[ˈmeməri kɑːd]memory stick内存棒,记忆棒[ˈmeməri stɪk]menu bar菜单栏[ˈmenjuː bɑː(r)]metadata元数据[ˈmetədeɪtə]microphone麦克风[ˈmaɪkrəfəʊn]microwave微波,微波炉[ˈmaɪkrəweɪv]middleware中间件[ˈmɪdlweə(r)]miniaturize使小型化[ˈmɪnətʃəraɪz]minicomputer小型计算机['mɪnikəmˌpjuːtə(r)]mirror镜像[ˈmɪrə(r)]mobile commerce移动商务[ˈməʊbaɪl ˈkɒmɜːs]mobile marking移动营销mobile phone移动电话[ˌməʊbaɪl ˈfəʊn]modem调制解调器[ˈməʊdem]monopoly垄断,专利权[məˈnɒpəli]mother board主板[ˈmʌðə(r) bɔːd]mount装配,挂载[maʊnt]multicomputer多计算机系统multidocument interface多文档界面multiline edit box多行编辑框multimedia多媒体[ˌmʌltiˈmiːdiə]multiple inheritance多重继承[ˈmʌltɪpl ɪnˈherɪtəns]multi-processor多处理器['mʌlti ˈprəʊsesə(r)]multitasking多任务[ˌmʌltiˈtɑːskɪŋ]multi-threaded多线程['mʌlti ˈθredɪd]mutual exclusion互斥[ˈmjuːtʃuəl ɪkˈskluːʒn]nanosecond纳秒[ˈnænəʊsekənd]natural language自然语言[ˌnætʃrəl ˈlæŋɡwɪdʒ]navigate导航[ˈnævɪɡeɪt]netiquette网络礼仪[ˈnetɪkət]network administer网络管理员network layer网络层[ˈnetwɜːk ˈleɪə(r)]network system网络系统[ˈnetwɜːk ˈsɪstəm]neural networking神经网络[ˈnjʊərəl ˈnetwɜːkɪŋ]newsgroups新闻讨论组[ˈnjuːzgruːps]niche商机[niːʃ]non-blocking primitive非阻塞原语non-impact无影响[ˈnəʊn ˈɪmpækt]nonrepudiation不可抵赖性normalization标准化,正常化[ˌnɔːməlaɪˈzeɪʃən]notation表示法,符号,标志[nəʊˈteɪʃn]notepad记事本[ˈnəʊtpæd]nozzle喷嘴[ˈnɒzl]null string空串[nʌl strɪŋ]Num Lock数字键锁定numerical数字的[njuːˈmerɪkl]object-based system基于对象的系统object-oriented面向对象[ˈɒbdʒɪkt ɔːrientɪd]object-oriented system面向对象系统[ˈɒbdʒɪkt ˈɔːrientɪd ˈsɪstəm]octal system八进制系统offline离线[ˌɒfˈlaɪn]online在线[ˌɒnˈlaɪn]open system开放系统[ˈəʊpən ˈsɪstəm]operand操作数[ˈɒpərænd]operational操作的[ˌɒpəˈreɪʃənl]optical disk光盘[ˈɒptɪkl dɪsk]optical fiber cable光导纤维电缆[ˈɒptɪkl faɪbə ˈkeɪbl]optimal scheduling algorithm最优调度算法optimal tree最优树[ˈɒptɪməl triː]optimization最优化,最佳化[ˌɒptɪmaɪˈzeɪʃn]ordered tree有序树[ˈɔːdəd triː]orthogonal list十字链表[ɔːˈθɒgənəl lɪst]ouch-sensitive display触控式显示器out-dated过时的,逾期的[aʊt ˈdeɪtɪd]outline view大纲视图[ˈaʊtlaɪn vjuː]output device输出设备[ˈaʊtpʊt dɪˈvaɪs]outsourcing外包[ˈaʊtsɔːsɪŋ]overflow溢出[ˌəʊvəˈfləʊ]overfrequency超频overlapped重叠[ˌəʊvəˈlæpt]overloading重载[ˌəʊvəˈləʊdɪŋ]package软件包,组件[ˈpækɪdʒ]page description language页面描述语言[peɪdʒ dɪˈskrɪpʃn ˈlæŋɡwɪdʒ]page fault页故障[peɪdʒ fɔːlt]paradigm范例[ˈpærədaɪm]parallel port并行接口[ˈpærəlel pɔːt]password口令,密码[ˈpɑːswɜːd]perform执行[pəˈfɔːm]peripheral外围设备[pəˈrɪfərəl]photo-sensitive drum感光鼓pie chart饼图[ˈpaɪ tʃɑːt]pin printer针式打印机pirate盗版[ˈpaɪrət]plaintext明文platform平台[ˈplætfɔːm]plotter绘图仪[ˈplɒtə(r)]pointing device定位设备[ˈpɔɪntɪŋ dɪvaɪs]polling task轮询任务polymorphism多态性[ˌpɒlɪˈmɔːfɪzm]populate填充[ˈpɒpjuleɪt]pop-up menu弹出式菜单[pɒp ʌp ˈmenjuː]pop-up Window弹出式窗口[pɒp ʌp ˈwɪndəʊ]portability可移植性[ˌpɔːtəˈbɪlɪti]portable便携式的[ˈpɔːtəbl]postulate假设[ˈpɒstjuleɪt]precision transform精确度变换preemptive multitasking抢占式多任务[priˈɛmptɪv ˌmʌltiˈtæskɪŋ]Primary Key主键[ˈpraɪməri kiː]printed circuit board印刷电路板[ˈprɪntɪd ˈsɜːkɪt bɔːd]printer driver打印机驱动程序[ˈprɪntə(r) ˈdraɪvə(r)]private cloud私有云[ˈpraɪvət klaʊd]private key cryptography私钥加密[ˈpraɪvət kiː krɪpˈtɒɡrəfi]privileged instruction特权指令[ˈprɪvəlɪdʒd ɪnˈstrʌkʃn]procedural programming面向过程程序设计[prəˈsiːdʒərəl ˈprəʊɡræmɪŋ]process进程[ˈprəʊses]protocol协议[ˈprəʊtəkɒl]prototype原型[ˈprəʊtətaɪp]proxy server代理服务器[ˈprɒksi sɜːvə(r)]public cloud公有云[ˈpʌblɪk klaʊd]public key公钥[ˈpʌblɪk kiː]public key cryptography公钥加密[ˈpʌblɪk kiː krɪpˈtɒɡrəfi]pull technology拉式技术[pʊl tekˈnɒlədʒi]push technology推送技术[ˈpʊʃ teknɒlədʒi]quad speed四倍速quadratic probing二次探测[kwɒˈdrætɪk ˈprəʊbɪŋ]quantizer数字转换器,编码器quantometer光谱分析仪query查询[ˈkwɪəri]queue队列[kjuː]quit退出[kwɪt]radio button单选按钮[ˈreɪdiəʊ bʌtn]rationale基本原理[ˌræʃəˈnɑːl]readiness准备就绪[ˈredinəs]real time system实时系统[ˈriːəl taɪm ˈsɪstəm]recipient收件人[rɪˈsɪpiənt]recorder记录机[rɪˈkɔːdə(r)]recursive function递归函数[rɪˈkɜːsɪv ˈfʌŋkʃn]refresh刷新[rɪˈfreʃ]refresh time刷新率,更新率[rɪˈfreʃ taɪm]relational model关系模型[rɪˈleɪʃənl ˈmɒdl]remark注释,评论[rɪˈmɑːk]remote远程[rɪˈməʊt]remote terminal远程终端[rɪˈməʊt ˈtɜːmɪnl]removable可移动的,可拆卸的[rɪˈmuːvəbl]replicate复制[ˈreplɪkeɪt]reproduction复制品,繁殖[ˌriːprəˈdʌkʃn]reset复位,重置[ˌriːˈset]resident驻留的[ˈrezɪdənt]resolution分辨率[ˌrezəˈluːʃn]response window响应式窗口[rɪˈspɒns ˈwɪndəʊ]restore恢复[rɪˈstɔː(r)]retrieve检索[rɪˈtriːv]revision修改,修订[rɪˈvɪʒn]ribbon cartridge色带盒,带状墨盒[ˈrɪbən ˈkɑːtrɪdʒ]right-click右击[ˌraɪt ˈklɪk]ring network环形网络[rɪŋ ˈnetwɜːk]robotics机器人技术[rəʊˈbɒtɪks]router路由器[ˈruːtə(r)]routine常规,例程[ruːˈtiːn]safe mode安全模式[ˈseɪf məʊd]scalability可伸展性[skeɪləˈbɪlɪti]scale定标,缩放[skeɪl]scanner扫描仪[ˈskænə(r)]screen saver屏幕保护程序[ˈskriːn seɪvə(r)]script脚本[skrɪpt]seamless无缝连接的[ˈsiːmləs]search engine搜索引擎[ˈsɜːtʃ endʒɪn]sector扇区[ˈsektə(r)]security安全[sɪˈkjʊərəti]security certificate安全认证[sɪˈkjʊərəti səˈtɪfɪkət]segment段[ˈseɡmənt]sensor传感器[ˈsensə(r)]sequential连续的[sɪˈkwenʃl]serial port串行接口[ˈsɪəriəl pɔːt]server服务器[ˈsɜːvə(r)]session会话[ˈseʃn]shared variable共享变量[ʃeəd ˈveəriəbl]short cut短路[ʃɔːt kʌt]shortcut key快捷键[ˈʃɔːtkʌt kiː]signature签名[ˈsɪɡnətʃə(r)]simulator仿真器,模拟器[ˈsɪmjuleɪtə(r)]simultaneous同时发生的[ˌsɪmlˈteɪniəs]solid ink固体油墨[ˈsɒlɪd ɪŋk]sound box音箱[saʊnd bɒks]sound card声卡[ˈsaʊnd kɑːd]source code源代码[ˈsɔːs kəʊd]spam垃圾邮件[spæm]span跨越[spæn]spatiality空间性speech generator语音发生器[spiːtʃ ˈdʒenəreɪtə(r)]speech recognition语音识别[ˈspiːtʃ rekəɡnɪʃn]speech synthesizer语音合成器[spiːtʃ ˈsɪnθəsaɪzə(r)]spreadsheet电子表格[ˈspredʃiːt]spyware间谍软件[ˈspaɪweə(r)]stack堆栈[stæk]stereo立体音响,立体感觉的[ˈsteriəʊ]storage class specifier存储类标识符[ˈstɔːrɪdʒ klɑːs ˈspɛsɪfaɪə]streaming audio音频流[ˈstriːmɪŋ ˈɔːdiəʊ]streaming video视频流[ˈstriːmɪŋ ˈvɪdiəʊ]streamline流线型[ˈstriːmlaɪn]structure结构体[ˈstrʌktʃə(r)]structure chart结构图[ˈstrʌktʃə(r) tʃɑːt]subnet子网subnet mask子网掩码subroutine子程序[ˈsʌbruːtiːn]supercomputer超级计算机[ˈsuːpəkəmpjuːtə(r)]supplier供应者,厂商[səˈplaɪə(r)]switch交换机[swɪtʃ]symbolize象征,用符号表现[ˈsɪmbəlaɪz]synchronous同步的[ˈsɪŋkrənəs]syntax error语法错误[ˈsɪntæks ˈerə(r)]synthesizer综合者,合成器[ˈsɪnθəsaɪzə(r)]taskbar任务栏technologist技术专家[tekˈnɒlədʒɪst]technology技术[tekˈnɒlədʒi]telecommunication电子通信[ˌtelikəˌmjuːnɪˈkeɪʃn]telemarketing电话销售[ˈtelimɑːkɪtɪŋ]telemetry遥感勘测[təˈlemətri]telnet远程登录程序[ˈtelnet]template file模板文件[ˈtempleɪt faɪl]temporality暂时,此时tender投标[ˈtendə(r)]terminal终端[ˈtɜːmɪnl]terminology术语[ˌtɜːmɪˈnɒlədʒi]thermal printer热敏打印机[ˈθɜːml ˈprɪntə(r)]thin client瘦客户端[θɪn ˈklaɪənt]thread线程[θred]three-dimension三维[θriː daɪˈmenʃn]throughput吞吐量[ˈθruːpʊt]thumbnail缩略图[ˈθʌmneɪl]time slicing时间分片[taɪm ˈslaɪsɪŋ]time-sharing分时[taɪm ˈʃeərɪŋ]time-varying随时间变换的[taɪm ˈveəriɪŋ]title bar标题栏[ˈtaɪtl bɑː(r)]toggle switch拨动开关[ˈtɒɡl swɪtʃ]token令牌,记号[ˈtəʊkən]toolkit工具包[ˈtuːlkɪt]topology拓扑结构[tə'pɒlədʒɪ]touchpad触摸板[ˈtʌtʃpæd]trackball轨迹球[ˈtrækbɔːl]transaction事务,交易[trænˈzækʃn]transceiver收发器[trænˈsiːvə(r)]translator翻译[trænzˈleɪtə(r)]transparency透明[trænsˈpærənsi]transport layer传输层[ˈtrænspɔːt ˈleɪə(r)]typeface字体[ˈtaɪpfeɪs]typeset排版typewrite打印机ubiquitous computing普适计算[juːˈbɪkwɪtəs kəmˈpjuːtɪŋ]unambiguous清楚的,明确的[ˌʌnæmˈbɪɡjuəs]unauthorized access未授权访问[ʌnˈɔːθəraɪzd ˈækses]undirected graph无向图[ˌʌndɪˈrɛktɪd ɡræf]union共同体[ˈjuːniən]uni-programming单编程unleash释放[ʌnˈliːʃ]unordered tree无序树[ˌʌnˈɔːdəd triː]update更新,修正[ˌʌpˈdeɪt]upgrade升级[ˌʌpˈɡreɪd]upload上传[ˌʌpˈləʊd]upstream rate上行速率[ˌʌpˈstriːm reɪt]user account用户账号[ˈjuːzə(r) əˈkaʊnt]user ID用户标识符[ˈjuːzə(r) ˌaɪ ˈdiː]User Object用户对象[ˈjuːzə(r) ˈɒbdʒɪkt]user-defined用户自定义[ˈjuːzə(r) dɪˈfaɪnd]utility实用程序[juːˈtɪləti]utilize利用[ˈjuːtəlaɪz]vacuum tube真空管[ˈvækjuːm tjuːb]valid有效的[ˈvælɪd]variable变量[ˈveəriəbl]vector矢量[ˈvektə(r)]vendor供应商[ˈvendə(r)]versatile通用的,万能的[ˈvɜːsətaɪl]version版本[ˈvɜːʃn]vertical垂直的[ˈvɜːtɪkl]video bandwidth视频带宽[ˈvɪdiəʊ ˈbændwɪdθ]video capture card视频采集卡[ˈvɪdiəʊ ˈkæptʃə(r) kɑːd]video clips视频剪辑[ˈvɪdiəʊ klɪps]video conferencing电视会议[ˈvɪdiəʊ ˈkɒnfərənsɪŋ]video display显示器[ˈvɪdiəʊ dɪˈspleɪ]video phone可视电话[ˈvɪdiəʊ fəʊn]virtual address space虚拟地址空间[ˈvɜːtʃuəl əˈdres speɪs]virtual block caching虚拟块高速缓存[ˈvɜːtʃuəl blɒk ˈkæʃɪŋ]virtual device虚拟设备[ˈvɜːtʃuəl dɪˈvaɪs]Virtual Host Service虚拟主机服务virtual interface虚拟接口[ˈvɜːtʃuəl ˈɪntəfeɪs]virtual IP address虚拟IP地址[ˈvɜːrtʃuəl ɪp əˈdres]virtual memory technology虚拟存储器技术[ˈvɜːtʃuəl ˈmeməri tekˈnɒlədʒi]virus checker病毒检査程序[ˈvaɪrəs ˈtʃekə(r)]visual arts视觉艺术[ˈvɪʒuəl ˈɑːts]visualize可视化[ˈvɪʒuəlaɪz]voice control语音控制[vɔɪs kənˈtrəʊl]voice mail语音邮件[vɔɪs meɪl]voice synthesis语音合成[vɔɪs ˈsɪnθəsɪs]volatile易失性的[ˈvɒlətaɪl]volume label磁盘卷标[ˈvɒljuːm ˈleɪbl]warm boot热启动[wɔːm buːt]wave form波形[weɪv fɔːm]wave length波长[weɪv leŋθ]Web page网页[web peɪdʒ]Web paging网页寻呼[web ˈpeɪdʒɪŋ]Web server服务器[web ˈsɜːvə(r)]Web site站点[web saɪt]wheel特权用户[wiːl]wholesaler批发商[ˈhəʊlseɪlə(r)]Wild Card Character通配符[waɪld kɑːd ˈkærəktə(r)]Window Painter窗口画板[ˈwɪndəʊ ˈpeɪntə(r)]Window-based基于视窗的[ˈwɪndəʊ beɪst]Windows窗口[ˈwɪndəʊz]Windows message窗口消息[ˈwɪndəʊz ˈmesɪdʒ]wire pirate网盗wiretapping搭线窃听[ˈwaɪətæpɪŋ]wizard向导工具[ˈwɪzəd]word processor文字处理软件[ˈwɜːd prəʊsesə(r)]workgroup hub工作组集线器worksheet工作表[ˈwɜːkʃiːt]workspace工作区[ˈwɜːkspeɪs]workstation工作站[ˈwɜːksteɪʃn]worm蠕虫[wɜːm]worthless无价值的,无益的[ˈwɜːθləs]wrap隐臧,伪装[ræp]write protect写保护[ˌraɪt prəˈtekt]xerography静电印刷zero access立即存取[ˈzɪərəʊ ˈækses]zero complement补码[ˈzɪərəʊ ˈkɒmplɪment]zero suppression消零[ˈzɪərəʊ səˈpreʃn]zone区域[zəʊn]zoom缩放[zuːm]
一、目录结构 React采用了由Lerna维护monorepo方式进行代码管理,即用一个仓库管理多个模块(module)或包(package)。在React仓库的根目录中,包含三个目录: (1)fixtures,给源码贡献者准备的测试用例。 (2)packages,React库提供的包的源码,包括核心代码、矢量图形库等,如下所列。├── packages ------------------------------------ 源码目录 │ ├── react-art ------------------------------- 矢量图形渲染器 │ ├── react-dom ------------------------------- DOM渲染器 │ ├── react-native-renderer ------------------- Native渲染器(原生iOS和Android视图) │ ├── react-test-renderer --------------------- JSON树渲染器 │ ├── react-reconciler ------------------------ React调和器 (3)scripts,相关的工具配置脚本,包括语法规则、Git钩子等。 React使用的前端模块化打包工具是Rollup,在源码中还引入了Flow,用于静态类型检查,在运行代码之前发现一些潜在的问题,其语法类似于TypeScript。二、React核心对象 在项目中引入React通常是像下面这样。import React from 'react'; 其实引入的是核心入口文件“packages/react/index.js”中导出的对象,如下所示,其中React.default用于Jest测试,React用于Rollup。const React = require('./src/React'); // TODO: decide on the top-level export form. // This is hacky but makes it work with both Rollup and Jest. module.exports = React.default || React; 顺着require()语句可以找到React.js中的React对象,代码省略了一大堆导入语句,其中__DEV__是个全局变量,用于管理开发环境中运行的代码块。const React = { Children: { map, forEach, count, toArray, only, }, createRef, Component, PureComponent, createContext, forwardRef, lazy, memo, useCallback, useContext, useEffect, useImperativeHandle, useDebugValue, useLayoutEffect, useMemo, useReducer, useRef, useState, Fragment: REACT_FRAGMENT_TYPE, Profiler: REACT_PROFILER_TYPE, StrictMode: REACT_STRICT_MODE_TYPE, Suspense: REACT_SUSPENSE_TYPE, unstable_SuspenseList: REACT_SUSPENSE_LIST_TYPE, createElement: __DEV__ ? createElementWithValidation : createElement, cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement, createFactory: __DEV__ ? createFactoryWithValidation : createFactory, isValidElement: isValidElement, version: ReactVersion, unstable_withSuspenseConfig: withSuspenseConfig, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals, }; if (enableFlareAPI) { React.unstable_useResponder = useResponder; React.unstable_createResponder = createResponder; } if (enableFundamentalAPI) { React.unstable_createFundamental = createFundamental; } if (enableJSXTransformAPI) { if (__DEV__) { React.jsxDEV = jsxWithValidation; React.jsx = jsxWithValidationDynamic; React.jsxs = jsxWithValidationStatic; } else { React.jsx = jsx; React.jsxs = jsx; } } export default React; 在React对象中包含了开放的核心API,例如React.Component、React.createRef()等,以及新引入的Hooks(内部的具体逻辑可转移到相关的包中),但渲染的逻辑已经剥离出来。1)React.createElement() JSX中的元素称为React元素,分为两种类型:DOM元素和组件元素。用JSX描述的组件都会通过Babel编译器将它们转换成React.createElement()方法,它包含三个参数(如下所示),其中type是元素类型,也就是它的名称;props是一个由元素属性组成的对象;children是它的子元素(即内容),可以是文本也可以是其它元素。React.createElement(type, [props], [...children]) 方法的返回值是一个ReactElement,省略了开发环境中的代码。const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: ref, props: props, _owner: owner //记录创建该元素的组件 }; return element; }; (1)$$typeof标识该对象是一个ReactElement。 (2)当ReactElement是DOM元素时,type是元素名称;当ReactElement是组件元素时,type是其构造函数。 (3)key和ref是React组件中的两个特殊属性,前者用于标识身份,后者用于访问render()方法内生成的组件实例和DOM元素。 (4)props是ReactElement中的属性,包括特殊的children属性。三、Reconciler 虽然React的DOM和Native两种渲染器内部实现的区别很大,但为了能共享自定义组件、State、生命周期等特性,做到跨平台,就需要共享一些逻辑,而这些逻辑由Reconciler统一处理,其中协调算法(Diffing算法)也要尽可能相似。1)Diffing算法 当调用React的render()方法时,会创建一棵由React元素组成的树。在下一次State或Props更新时,相同的render()方法会返回一棵不同的树。React会应用Diffing算法来高效的比较两棵树,算法过程如下。 (1)当根节点为不同类型的元素时,React会拆卸原有的树,销毁对应的DOM节点和关联的State、卸载子组件,最后再创建新的树。 (2)当比对两个相同类型的DOM元素时,会保留DOM节点,仅比对变更的属性。 (3)当比对两个相同类型的组件元素时,组件实例保持不变,更新该组件实例的Props。 (4)当递归DOM节点的子元素时,React会同时遍历两个子元素的列表,比对相同位置的元素,性能比较低效。 (5)在给子元素添加唯一标识的key属性后,就能只比对变更了key属性的元素。2)Fiber Reconciler JavaScript与样式计算、界面布局等各种绘制,一起运行在浏览器的主线程中,当JavaScript运行时间过长时,将占用整个线程,阻塞其它任务。为了能在React渲染期间回到主线程执行其它任务,在React v16中提出了Fiber Reconciler,并将其设为默认的Reconciler,解决了过去Stack Reconciler中的固有问题和遗留的痛点,提高了动画、布局和手势等领域的性能。Fiber Reconciler的主要目标是: (1)暂停和切分渲染任务,并将分割的任务分布到各个帧中。 (2)调整优先级,并重置或复用已完成的任务。 (3)在父子元素之间交错处理,以支持React中的布局。 (4)在render()方法中返回多个元素。 (5)更好地支持错误边界。3)调度任务 Fiber可以分解任务,根据优先级将任务调度到浏览器提供的两个全局函数中,如下所列。 (1)requestAnimationFrame:在下一个动画帧上执行高优先级的任务。 (2)requestIdleCallback:在线程空闲时执行低优先级的任务。 当网页保持在每秒60帧(1帧约为16ms)时,整体会变得很流畅。在每个帧中调用requestAnimationFrame()执行高优先级的任务;而在两个帧之间会有一小段空闲时间,此时可执行requestIdleCallback()中的任务,该函数包含一个deadline参数(截止时间),用于切分长任务。4)Fiber数据结构 在调和期间,从render()方法得到的每个React元素都需要升级为Fiber节点,并添加到Fiber节点树中。而与React元素不同,Fiber节点可复用,不会在每次渲染时重新创建。Fiber的数据结构大致如下,省略了部分属性,源码来自于packages/react-reconciler/src/ReactFiber.js。export type Fiber = { tag: WorkTag, key: null | string, elementType: any, type: any, stateNode: any, return: Fiber | null, child: Fiber | null, sibling: Fiber | null, ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject, effectTag: SideEffectTag, nextEffect: Fiber | null, firstEffect: Fiber | null, lastEffect: Fiber | null, expirationTime: ExpirationTime, alternate: Fiber | null, ... }; return、child和sibling三个属性分别表示父节点、第一个子节点和兄弟节点,通过它们使得Fiber节点能够基于链表连接在一起。假设有个ClickCounter组件,包含<button>和<span>两个元素,它们三者之间的关系如图12所示。class ClickCounter extends React.Component { render() { return [ <button>Update counter</button>, <span>10</span> ]; } } 使用alternate属性双向连接当前Fiber和正在处理的Fiber(workInProgress),如下代码所示,当需要恢复时,可通过alternate属性直接回退。let workInProgress = current.alternate; if (workInProgress === null) { workInProgress.alternate = current; current.alternate = workInProgress; } 到期时间(ExpirationTime)是指完成此任务的时间,该时间越短,则优先级越高,需要尽早执行,具体逻辑在同目录的ReactFiberExpirationTime.js中。四、生命周期钩子方法 React在内部执行时会分为两个阶段:render和commit。 在第一个render阶段(phase)中,React持有标记了副作用(side effect)的Fiber树并将其应用于实例,该阶段不会发生用户可见的更改,并且可异步执行,下面列出的是在render阶段执行的生命周期钩子方法 (1)[UNSAFE_]componentWillMount(弃用) (2)[UNSAFE_]componentWillReceiveProps(弃用) (3)getDerivedStateFromProps (4)shouldComponentUpdate (5)[UNSAFE_]componentWillUpdate(弃用) (6)render 标有UNSAFE的生命周期有可能被执行多次,并且经常被误解和滥用,例如在这些方法中执行副作用代码,可能出现渲染问题,或者任意操作DOM,可能引起回流(reflow)。于是官方推出了静态的getDerivedStateFromProps()方法,可限制状态更新以及DOM操作。 在第二个commit阶段,任务都是同步执行的,下面列出的是commit阶段执行的生命周期钩子方法,这些方法都只执行一次,其中getSnapshotBeforeUpdate()是新增的,用于替换componentWillUpdate()。 (1)getSnapshotBeforeUpdate (2)componentDidMount (3)componentDidUpdate (4)componentWillUnmount 新的流程将变成图13这样。
一、整体概况 Piwik的官网是matomo.org,使用PHP编写的,而我以前就是PHP工程师,因此看代码不会有障碍。目前最新版本是3.6,Github地址是matomo-org/matomo,打开地址将会看到下图中的内容(只截取了关键部分)。 打开js文件夹,里面的piwik.js就是本次要分析的脚本代码(如下图红色框出部分),内容比较多,有7838行代码。 先把系统的代码都下载下来,然后在本地配置虚拟目录,再开始安装。在安装的时候可以选择语言,该系统支持简体中文(注意下图中红色框出的部分)。系统会执行一些操作(注意看下图左边部分),包括检查当前环境能否安装、建立数据库等,按照提示一步一步来就行,比较简单,没啥难度。 安装完后就会自动跳转到后台界面(如下图所示),有图表,有分析,和常用的统计系统差不多。功能还没细看,只做了初步的了解,界面的友好度还是蛮不错的。 嵌到页面中的JavaScript代码与其它统计系统也类似,如下所示,也是用异步加载的方式,只是发送的请求地址没有伪装成图像地址(注意看标红的那句代码)。<script type="text/javascript"> var _paq = _paq || []; /* tracker methods like "setCustomDimension" should be called before "trackPageView" */ _paq.push(['trackPageView']); _paq.push(['enableLinkTracking']); (function() { var u="//loc.piwik.cn/"; //自定义 _paq.push(['setTrackerUrl', u+'piwik.php']); _paq.push(['setSiteId', '1']); var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript'; g.async=true; g.defer=true; g.src='piwik.js'; s.parentNode.insertBefore(g,s); })(); </script> 在页面中嵌入这段脚本后,页面在刷新的时候,会有下图中的请求。在请求中带了一大堆的参数,在后面的内容中会对每个参数做释义。二、脚本拆分 7000多行的脚本,当然不能一行一行的读,需要先拆分,拆成一个一个的模块,然后再逐个分析。脚本之所以这么大,是因为里面编写了大量代码来兼容各个版本的浏览器,这其中甚至包括IE4、Firefox1.0、Netscape等骨灰级的浏览器。接下来我把源码拆分成6个部分,分别是json、private、query、content-overlay、tracker和piwik,如下图红线框出的所示,piwik-all中包含了全部代码,便于对比。代码已上传到Github。 json.js是一个开源插件JSON3,为了兼容不支持JSON对象的浏览器而设计的,这里面的代码可以单独研究。private.js包含了一些用于全局的私有变量和私有函数,例如定义系统对象的别名、判断类型等。query.js中包含了很多操作HTML元素的方法,例如设置元素属性、查询某个CSS类的元素等,它类似于一个微型的jQuery库,不过有许多独特的功能。content-overlay.js有两部分组成,一部分包含内容追踪以及URL拼接等功能,另一部分是用来处理嵌套的页面,这里面具体没有细看。tracker.js中只有一个Tracker()函数,不过内容最多,有4700多行,主要的统计逻辑都在这里了。piwik.js中内容不多,包含一些初始化和插件的钩子等功能,钩子具体怎么运作的还没细看。 虽然分成了6部分,但是各部分的内容还是蛮多的,并且内容之间是有联系的,因此短时间的话,很难搞清楚其中所有的门道。我就挑了一点我个人感觉最重要的先做分析。1)3种传送数据的方式 我原先只知道两种传送数据的方式,一种是通过Ajax的方式,另一种是创建一个Image对象,然后为其定义src属性,数据作为URL的参数传递给后台,这种方式很通用,并且还能完美解决跨域问题。我以前编写的一个性能参数搜集的插件primus.js,也是这么传送数据的。在阅读源码的时候,发现了第三种传送数据的方式,使用Navigator对象的sendBeacon()。 MDN上说:“此方法可用于通过HTTP将少量数据异步传输到Web服务器”。虽然这个方法有兼容问题,但我还是被震撼到了。它很适合统计的场景,MDN上又讲到:“统计代码会在页面关闭(window.onunload)之前向web服务器发送数据,但过早的发送数据可能错过收集数据的机会。然而, 要保证在页面关闭期间发送数据一直比较困难,因为浏览器通常会忽略在卸载事件中产生的异步请求 。在使用sendBeacon()方法后,能使浏览器在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一页的载入。这就解决了提交分析数据时的所有的问题:使它可靠,异步并且不会影响下一页面的加载,并且代码更简单”。下面是代码片段(注意看标红的那句代码),存在于tracker.js中。function sendPostRequestViaSendBeacon(request) { var supportsSendBeacon = "object" === typeof navigatorAlias && "function" === typeof navigatorAlias.sendBeacon && "function" === typeof Blob; if (!supportsSendBeacon) { return false; } var headers = { type: "application/x-www-form-urlencoded; charset=UTF-8" }; var success = false; try { var blob = new Blob([request], headers); success = navigatorAlias.sendBeacon(configTrackerUrl, blob); // returns true if the user agent is able to successfully queue the data for transfer, // Otherwise it returns false and we need to try the regular way } catch (e) { return false; } return success; }2)参数释义 下面的方法(存在于tracker.js中)专门用于搜集页面中的统计数据,将它们拼接成指定链接的参数,而这条链接中的参数最终将会发送给服务器。/** * Returns the URL to call piwik.php, * with the standard parameters (plugins, resolution, url, referrer, etc.). * Sends the pageview and browser settings with every request in case of race conditions. */ function getRequest(request, customData, pluginMethod, currentEcommerceOrderTs) { var i, now = new Date(), nowTs = Math.round(now.getTime() / 1000), referralTs, referralUrl, referralUrlMaxLength = 1024, currentReferrerHostName, originalReferrerHostName, customVariablesCopy = customVariables, cookieSessionName = getCookieName("ses"), cookieReferrerName = getCookieName("ref"), cookieCustomVariablesName = getCookieName("cvar"), cookieSessionValue = getCookie(cookieSessionName), attributionCookie = loadReferrerAttributionCookie(), currentUrl = configCustomUrl || locationHrefAlias, campaignNameDetected, campaignKeywordDetected; if (configCookiesDisabled) { deleteCookies(); } if (configDoNotTrack) { return ""; } var cookieVisitorIdValues = getValuesFromVisitorIdCookie(); if (!isDefined(currentEcommerceOrderTs)) { currentEcommerceOrderTs = ""; } // send charset if document charset is not utf-8. sometimes encoding // of urls will be the same as this and not utf-8, which will cause problems // do not send charset if it is utf8 since it's assumed by default in Piwik var charSet = documentAlias.characterSet || documentAlias.charset; if (!charSet || charSet.toLowerCase() === "utf-8") { charSet = null; } campaignNameDetected = attributionCookie[0]; campaignKeywordDetected = attributionCookie[1]; referralTs = attributionCookie[2]; referralUrl = attributionCookie[3]; if (!cookieSessionValue) { // cookie 'ses' was not found: we consider this the start of a 'session' // here we make sure that if 'ses' cookie is deleted few times within the visit // and so this code path is triggered many times for one visit, // we only increase visitCount once per Visit window (default 30min) var visitDuration = configSessionCookieTimeout / 1000; if ( !cookieVisitorIdValues.lastVisitTs || nowTs - cookieVisitorIdValues.lastVisitTs > visitDuration ) { cookieVisitorIdValues.visitCount++; cookieVisitorIdValues.lastVisitTs = cookieVisitorIdValues.currentVisitTs; } // Detect the campaign information from the current URL // Only if campaign wasn't previously set // Or if it was set but we must attribute to the most recent one // Note: we are working on the currentUrl before purify() since we can parse the campaign parameters in the hash tag if ( !configConversionAttributionFirstReferrer || !campaignNameDetected.length ) { for (i in configCampaignNameParameters) { if ( Object.prototype.hasOwnProperty.call(configCampaignNameParameters, i) ) { campaignNameDetected = getUrlParameter( currentUrl, configCampaignNameParameters[i] ); if (campaignNameDetected.length) { break; } } } for (i in configCampaignKeywordParameters) { if ( Object.prototype.hasOwnProperty.call( configCampaignKeywordParameters, i ) ) { campaignKeywordDetected = getUrlParameter( currentUrl, configCampaignKeywordParameters[i] ); if (campaignKeywordDetected.length) { break; } } } } // Store the referrer URL and time in the cookie; // referral URL depends on the first or last referrer attribution currentReferrerHostName = getHostName(configReferrerUrl); originalReferrerHostName = referralUrl.length ? getHostName(referralUrl) : ""; if ( currentReferrerHostName.length && // there is a referrer !isSiteHostName(currentReferrerHostName) && // domain is not the current domain (!configConversionAttributionFirstReferrer || // attribute to last known referrer !originalReferrerHostName.length || // previously empty isSiteHostName(originalReferrerHostName)) ) { // previously set but in current domain referralUrl = configReferrerUrl; } // Set the referral cookie if we have either a Referrer URL, or detected a Campaign (or both) if (referralUrl.length || campaignNameDetected.length) { referralTs = nowTs; attributionCookie = [ campaignNameDetected, campaignKeywordDetected, referralTs, purify(referralUrl.slice(0, referralUrlMaxLength)) ]; setCookie( cookieReferrerName, JSON_PIWIK.stringify(attributionCookie), configReferralCookieTimeout, configCookiePath, configCookieDomain ); } } // build out the rest of the request request += "&idsite=" + configTrackerSiteId + "&rec=1" + "&r=" + String(Math.random()).slice(2, 8) + // keep the string to a minimum "&h=" + now.getHours() + "&m=" + now.getMinutes() + "&s=" + now.getSeconds() + "&url=" + encodeWrapper(purify(currentUrl)) + (configReferrerUrl.length ? "&urlref=" + encodeWrapper(purify(configReferrerUrl)) : "") + (configUserId && configUserId.length ? "&uid=" + encodeWrapper(configUserId) : "") + "&_id=" + cookieVisitorIdValues.uuid + "&_idts=" + cookieVisitorIdValues.createTs + "&_idvc=" + cookieVisitorIdValues.visitCount + "&_idn=" + cookieVisitorIdValues.newVisitor + // currently unused (campaignNameDetected.length ? "&_rcn=" + encodeWrapper(campaignNameDetected) : "") + (campaignKeywordDetected.length ? "&_rck=" + encodeWrapper(campaignKeywordDetected) : "") + "&_refts=" + referralTs + "&_viewts=" + cookieVisitorIdValues.lastVisitTs + (String(cookieVisitorIdValues.lastEcommerceOrderTs).length ? "&_ects=" + cookieVisitorIdValues.lastEcommerceOrderTs : "") + (String(referralUrl).length ? "&_ref=" + encodeWrapper(purify(referralUrl.slice(0, referralUrlMaxLength))) : "") + (charSet ? "&cs=" + encodeWrapper(charSet) : "") + "&send_image=0"; // browser features for (i in browserFeatures) { if (Object.prototype.hasOwnProperty.call(browserFeatures, i)) { request += "&" + i + "=" + browserFeatures[i]; } } var customDimensionIdsAlreadyHandled = []; if (customData) { for (i in customData) { if ( Object.prototype.hasOwnProperty.call(customData, i) && /^dimension\d+$/.test(i) ) { var index = i.replace("dimension", ""); customDimensionIdsAlreadyHandled.push(parseInt(index, 10)); customDimensionIdsAlreadyHandled.push(String(index)); request += "&" + i + "=" + customData[i]; delete customData[i]; } } } if (customData && isObjectEmpty(customData)) { customData = null; // we deleted all keys from custom data } // custom dimensions for (i in customDimensions) { if (Object.prototype.hasOwnProperty.call(customDimensions, i)) { var isNotSetYet = -1 === indexOfArray(customDimensionIdsAlreadyHandled, i); if (isNotSetYet) { request += "&dimension" + i + "=" + customDimensions[i]; } } } // custom data if (customData) { request += "&data=" + encodeWrapper(JSON_PIWIK.stringify(customData)); } else if (configCustomData) { request += "&data=" + encodeWrapper(JSON_PIWIK.stringify(configCustomData)); } // Custom Variables, scope "page" function appendCustomVariablesToRequest(customVariables, parameterName) { var customVariablesStringified = JSON_PIWIK.stringify(customVariables); if (customVariablesStringified.length > 2) { return ( "&" + parameterName + "=" + encodeWrapper(customVariablesStringified) ); } return ""; } var sortedCustomVarPage = sortObjectByKeys(customVariablesPage); var sortedCustomVarEvent = sortObjectByKeys(customVariablesEvent); request += appendCustomVariablesToRequest(sortedCustomVarPage, "cvar"); request += appendCustomVariablesToRequest(sortedCustomVarEvent, "e_cvar"); // Custom Variables, scope "visit" if (customVariables) { request += appendCustomVariablesToRequest(customVariables, "_cvar"); // Don't save deleted custom variables in the cookie for (i in customVariablesCopy) { if (Object.prototype.hasOwnProperty.call(customVariablesCopy, i)) { if (customVariables[i][0] === "" || customVariables[i][1] === "") { delete customVariables[i]; } } } if (configStoreCustomVariablesInCookie) { setCookie( cookieCustomVariablesName, JSON_PIWIK.stringify(customVariables), configSessionCookieTimeout, configCookiePath, configCookieDomain ); } } // performance tracking if (configPerformanceTrackingEnabled) { if (configPerformanceGenerationTime) { request += "&gt_ms=" + configPerformanceGenerationTime; } else if ( performanceAlias && performanceAlias.timing && performanceAlias.timing.requestStart && performanceAlias.timing.responseEnd ) { request += "&gt_ms=" + (performanceAlias.timing.responseEnd - performanceAlias.timing.requestStart); } } if (configIdPageView) { request += "&pv_id=" + configIdPageView; } // update cookies cookieVisitorIdValues.lastEcommerceOrderTs = isDefined(currentEcommerceOrderTs) && String(currentEcommerceOrderTs).length ? currentEcommerceOrderTs : cookieVisitorIdValues.lastEcommerceOrderTs; setVisitorIdCookie(cookieVisitorIdValues); setSessionCookie(); // tracker plugin hook request += executePluginMethod(pluginMethod, { tracker: trackerInstance, request: request }); if (configAppendToTrackingUrl.length) { request += "&" + configAppendToTrackingUrl; } if (isFunction(configCustomRequestContentProcessing)) { request = configCustomRequestContentProcessing(request); } return request; } 统计代码每次都会传送数据,而每次请求都会带上一大串的参数,这些参数都是简写,下面做个简单说明(如有不正确的地方,欢迎指正),部分参数还没作出合适的解释,例如UUID的生成规则等。首先将这些参数分为两部分,第一部分如下所列:1、idsite:网站ID2、rec:1(写死)3、r:随机码4、h:当前小时5、m:当前分钟6、s:当前秒数7、url:当前纯净地址,只留域名和协议8、_id:UUID9、_idts:访问的时间戳10、_idvc:访问数11、_idn:新访客(目前尚未使用)12、_refts:访问来源的时间戳13、_viewts:上一次访问的时间戳14、cs:当前页面的字符编码15、send_image:是否用图像请求方式传输数据16、gt_ms:内容加载消耗的时间(响应结束时间减去请求开始时间)17、pv_id:唯一性标识 再列出第二部分,用于统计浏览器的功能,通过Navigator对象的属性(mimeTypes、javaEnabled等)和Screen对象的属性(width与height)获得。1、pdf:是否支持pdf文件类型2、qt:是否支持QuickTime Player播放器3、realp:是否支持RealPlayer播放器4、wma:是否支持MPlayer播放器5、dir:是否支持Macromedia Director6、fla:是否支持Adobe FlashPlayer7、java:是否激活了Java8、gears:是否安装了Google Gears9、ag:是否安装了Microsoft Silverlight10、cookie:是否启用了Cookie11、res:屏幕的宽和高(未正确计算高清显示器) 上面这11个参数的获取代码,可以参考下面这个方法(同样存在于tracker.js中),注意看代码中的pluginMap变量(已标红),它保存了多个MIME类型,用来检测是否安装或启用了指定的插件或功能。/* * Browser features (plugins, resolution, cookies) */ function detectBrowserFeatures() { var i, mimeType, pluginMap = { // document types pdf: "application/pdf", // media players qt: "video/quicktime", realp: "audio/x-pn-realaudio-plugin", wma: "application/x-mplayer2", // interactive multimedia dir: "application/x-director", fla: "application/x-shockwave-flash", // RIA java: "application/x-java-vm", gears: "application/x-googlegears", ag: "application/x-silverlight" }; // detect browser features except IE < 11 (IE 11 user agent is no longer MSIE) if (!new RegExp("MSIE").test(navigatorAlias.userAgent)) { // general plugin detection if (navigatorAlias.mimeTypes && navigatorAlias.mimeTypes.length) { for (i in pluginMap) { if (Object.prototype.hasOwnProperty.call(pluginMap, i)) { mimeType = navigatorAlias.mimeTypes[pluginMap[i]]; browserFeatures[i] = mimeType && mimeType.enabledPlugin ? "1" : "0"; } } } // Safari and Opera // IE6/IE7 navigator.javaEnabled can't be aliased, so test directly // on Edge navigator.javaEnabled() always returns `true`, so ignore it if ( !new RegExp("Edge[ /](\\d+[\\.\\d]+)").test(navigatorAlias.userAgent) && typeof navigator.javaEnabled !== "unknown" && isDefined(navigatorAlias.javaEnabled) && navigatorAlias.javaEnabled() ) { browserFeatures.java = "1"; } // Firefox if (isFunction(windowAlias.GearsFactory)) { browserFeatures.gears = "1"; } // other browser features browserFeatures.cookie = hasCookies(); } var width = parseInt(screenAlias.width, 10); var height = parseInt(screenAlias.height, 10); browserFeatures.res = parseInt(width, 10) + "x" + parseInt(height, 10); }除了上述20多个参数之外,在系统官网上可点击“Tracking HTTP API”查看到所有的参数,只不过都是英文的。
最近从松江图书馆中借了一本叫《指尖上行 移动前端开发进阶之路》的书,该书分为5大章,此处只记录了其中的第4章。书中写到在项目上线后,通过数据监控发现:1. 一些之前觉得很好的创意,由于资源和加载量太大,导致用户在Loading阶段大量蹦失。2. 一些很出色的页面设计,由于没有突出重要按钮,导致转化率很低。3. 一些动画效果绚丽的互动页面,反而转化率比同样功能的常规页面低很低。4. 一些费了很低精力和资源做出来的页面,由于缺乏推广,造成访问量极少。我们需要对页面进行各种数据埋点,从数据的角度研究需求的实际效果、用户的实际行为和后续的改进措施。一、埋点1)PV和UV1. PV:Page View,页面访问次数。2. UV:Unique Visitor,页面独立的访问次数,通过Cookie区分不同的人。2)关键转化率转化率用来衡量一个时间段内,特定的用户行为量和页面流量的比率,页面关键的数据信息点都不一样,但可以总结为“某个按钮的点击率”。3)页面用户行为除了常规的按钮点击、每屏的触达率、每屏的回翻率及每屏的停留时长等外,还会有带动画的图片点击和摇晃手机的行为等。4)腾讯云分析基础功能包括:实时数据、关键数据、运营商、客户端信息、访客画像、访客深度、地域信息、页面排行、外部链接、入口页面及离开页面等。高级功能包括:渠道效果分析和自定义事件统计。二、数据分析1)转化率微信中右上角的分享除以页面浏览量,计算出页面分享率。领取奖励的按钮的点击数除以页面浏览量,计算出的就是领奖按钮的点击率。2)多页面之间的联系每个页面之间肯定是有联系的,找到联系点,就可以研究用户体验的流程,还可以看出体验流程是否完善,是否会在某个节点突然大量的流失用户。3)多项目之间的联系研究多个项目之间的数据差异,可以为下次的宣传页面找到更适合的设计方式。找到多个项目之间的共同点,例如在页面中放一个下载游戏或拉起游戏的按钮,对这个按钮的数据进行收集和分析。单看数据难以找到数据之间微妙的联系,可以利用图表,如折线图、柱形图、饼图和散点图等,进行分析和研究。三、数据参考1)资源投放数据参考1. 中午12点左右和晚上10点左右是页面访问高峰期,这两个时间可能是吃饭和睡觉前。2. 通过一波资源推广的移动页面,其访问热度往往持续两天左右。3. 长期、固定位置的资源投放可以延长移动页面生命周期。2)H5用户行为数据参考1. 用户随着页面层级的加深而不断流失,流失率在前几页最高。2. 按钮的点击量受页面层级影响。3. 输入行为或者复杂交互行为会导致用户流失。4. 热门对象会带来更多关注,例如世界杯、奥运会等。5. 用户可能会忽略页面提示直接开始页面交互,在做页面提示的时候,要简洁明了。6. 用户习惯沿用上一屏学习到的操作行为,如果当前操作不同,需要提示用户。7. 大多数用户习惯滑动切换,放置在左边的按钮点击量低。8. 有行为触发的icon要设计得足够醒目易懂,这样会刺激用户去点击。9. 动画明显的元素更容易引起用户的注意并点击。10. 有些图片不是动态的,也让人很有点击的欲望,例如宝箱、礼物盒等生活中打开了会有惊喜的元素。11. 页面首屏和最后一屏的平均停留时间比中间页面的平均停留时间长。12. 按钮摆放在第1屏的点击率最高,第2屏骤减,最后一屏回升。13. 功能性页面的平均停留时间比展示型页面的平均停留时间长。14. 同样功能的按钮,名字设置影响点击率,例如下载游戏、前往领取,后者的点击率更高。15. 每屏都放同样的按钮,可以增加整体点击率。16. 加载超过5秒会有74%的用户离开页面,用户不会有耐心反反复复地重试、刷新。17. 给页面添加Loading可以优化用户等待时间的空白,能无形中给页面增加加载时间。
一、HTML1)标签规范化div乱炖是指嵌套多层div元素。为了保持简洁,语义化的标签是首选,应用样式所依靠的是HTML5元素名称和它们的层级关系,在标记中没有类名,往往写出层级的后代选择器。2)模块化方法前端开发人员的工作就是把视觉语言拆解成最小单元。拆解之后,可以创建规则,对这些最小单元进行重组。转换的目标是创建具有可扩展性和可维护性的代码库,以便如实地重现视觉语言能表达的任何东西。3)模块化CSS1. OOCSS(Object-Oriented CSS,面向对象的CSS)有两个主要的原则:分离结构和外观,以及分离容器和内容。分离结构和外观:将视觉特性定义为可复用的单元。分离容器和内容:不再将元素位置作为样式的限定词。2. SMACSS(Scalable and Modular Architecture for CSS,模块化架构的可扩展CSS)把样式系统划分为5个具体类别。基础:不添加CSS类,标签会以什么外观呈现。布局:把页面分成一些区域。模块:设计中的模块化、可复用的单元。状态:在特定的状态或情况下,模块或布局的显示方式。主题:可选的视觉外观层,可以让你更换不同主题。3. BEM(Block Element Modifier,块元素修饰符)只是一个CSS类名的命名规则。每个元素带有如下内容,例如toogle__detail--active。块名:所属组件的名称元素:元素在块里面的名称修饰符:任何与块或元素相关联的修饰符二、CSS1)特性之争与继承之痛选择器优先级:重写一个选择器时,总是需要注意它的优先级。颜色重置:再次指定样式,并且要覆盖当前的背景颜色。位置依赖:如果移动位置,样式将会改变。多重继承:改变主体或侧边栏的样式,都会影响呈现。深层嵌套:样式来源增加到了四个。2)有益的原则1. 单一职责原则:必须有单一的、高度聚焦的理由。.canlendar-header { }2. 单一样式来源:不仅每个CSS类名被创建为单一用途,而且每个标签的样式也只有单一的来源。将来源放在组件文件里,避免样式散落在其它组件文件里。.canlendar-header { } .blog .canlendar-header { }3. 组件修饰符:皮肤或子组件,能够定义一个组件在多个不同情况下的多种变化。修饰符的类名不再是父组件,而是组件本身的一部分。.canlendar-header { } .canlendar-nested .canlendar-header { }三、设计系统和工作流1)设计系统的规则列表1. 永远不要给布局的子内容强加内边距和元素样式。布局只关注垂直对齐、水平对齐和文字间距。2. 主题和别的数据属性值永远不要强制改变外观;它们必须保持布局、组件和元素可以应用于其上。3. 组件总是贴着它的父容器的四个边,元素都没有上外边距和左外边距,所有的最后节点的外边距都会被清除。4. 组件本身永远不要添加背景、宽度、浮动、内边距和外边距的样式,组件样式是组件内元素的样式。5. 每个元素都有且只有一个唯一的且作用域只在组件内的CSS类名。所有的样式都是直接应用到这个选择器上,并且只有上下文和主体能修改元素的样式。6. 永远不要在元素上使用上外边距,第一个元素总是贴着它所在组件的顶部。7 JavaScript永远不要绑定任何元素的CSS类名,选中元素通过数据属性实现。2)现代工作流1. 使用事件跟踪和用户故事来正确地跟踪工作流和标记那些完成了的任务。2. 搭建开发环境来测试代码。3. 构建部署流程,用于编译、验证和测试代码。4. 在任何代码被采纳之前,都要获取需求方的反馈。5. 把提交的代码推送到中心代码库。6. 采用这样一个部署系统:可以无缝地添加一些新代码到生产环境;在需要的时候,还可以回滚代码还原系统。3)现代的开发工作流1. 需求需求所面向的人群为:交互设计、视觉设计、后端开发以及前端开发人员。让四个领域的人员共同参与需求收集的过程,可以更早地发现需求中存在的问题和不足。2. 原型设计原型设计提供了一个讨论和反馈的公共空间,它把丰满的想法实现在桌面和移动浏览器中。在原型中,想法可以成型、摒弃、重拾、打磨。3. 程序开发开发人员的工作就是收集和处理来自数据库的数据,然后把它们放置到对应的标记上。如果原型和网站共有一套CSS和JavaScript,那么开发人员应该完成一个功能完整、样式齐全、交互良好、响应及时、通过检查和达到标准的产品。测试人员可以根据原型设计来一步步地检验开发的内容。4)任务处理器(gulp)1. 安装需要的Ruby库2. 编译Sass、合并JavaScript、加载第三方JavaScript库3. 把SVG文件编译成图标字体4. 对图片进行处理,减少文件体积,裁剪成各种尺寸5. 同步到远程服务器6. 运行可视化的回归测试7. 自动生成浏览器厂商的前缀8. 编译组件库9. 优化Sass、CSS、JavaScript、JSON等10. 基于JSON模式来验证数据11. 启动Node和PHP服务器12. 监听文件改动来刷新浏览器 四、测试核心和文档核心1)视觉还原测试1. 基于页面的比较,Wraith(https://github.com/BBC-News/wraith)2. 基于组件的比较,BackstopJS(https://github.com/garris/BackstopJS)3. CSS单位测试,Quixote(https://github.com/jamesshore/quixote)4. 基于无头浏览器的测试,Gemini(https://github.com/gemini-testing/gemini)5. 基于桌面浏览器的测试,Selenium服务器(https://www.seleniumhq.org/download)6. 包含脚步库文件,CasperJS(http://casperjs.org)7. 基于图形用户界面的比较工具,Diffux项目(https://github.com/diffux/diffux)8. 基于命令行的比较工具,PhantomCSS(https://github.com/HuddleEng/PhantomCSS)2)样式文档SassDoc(http://sassdoc.com)可记录Sass的变量、混合、继承和函数的工具。3)图形库原子设计原则(http://patternlab.io)是一种构建网站设计系统的方法论。首先把网页常用元素分解成各个尺寸的模式,然后再描述这些模式组合成一个完整网页的方式。源自是构造网站、标题、样式、图片和表单元素的基本结构单元。在网页设计中,分子可能代表一个搜索表单、媒体块或导航栏。
这次先能够使用PC端的浏览器测试,首先需要下载官方的发布版本“WebPageTest 3.0”。1. agent:浏览器代理软件2. mobile:移动端参数相关代码3. www:网站PHP代码也可以参考官方的Private Instance配置,不过文档挂在google域名下,所以你懂得。我做了简单的翻译《私有实例》官方文档写的非常拗口,但是还是有一定的参考价值。 一、PHP配置与软件安装1)配置虚拟目录我用的Web服务器软件是Apache。首先要打开虚拟目录的功能,如上所示,需要在httpd.conf中配置,以我本机为例,目录在“D:\server\Apache24\conf”,如下图所示。然后在虚拟目录文件中配置域名,还是以本机为例,目录在“D:\server\Apache24\conf\extra”中,如下所示。具体的配置内容如下图所示。输入虚拟地址后,出现了熟悉的页面,不过现在Location和Browser的两个选项还没有,需要额外配置。2)安装信息在浏览器中输入“mine.webpagetest.net/install/”查看还未安装的信息:如果有疑惑还可以直接查看“install/index.php”的源码。3)PHP修改1. memory_limit修改“php.ini”文件中的“memory_limit”2. 安装APC(the Alternative PHP Cache)可选PHP缓存。将下载到的dll文件放到“PHP/ext”文件夹内。并在“php.ini”中添加扩展。extension=php_apcu.dll3. 安装SQLite(一款轻型的数据库)先到下载页面,将两个压缩包中的内容放到一个文件夹中,例如“C:\sqlite”内:接着在环境变量中设置“Path”。在“php.ini”中添加“php_sqlite3.dll”扩展。“ext”文件夹内默认有这个扩展,只是没有打开。extension=php_sqlite3.dll二、系统工具安装1)ffmpeg安装ffmpeg(用于提取视频缩略图),这是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。功能包括视频采集功能、视频格式转换、视频抓图、给视频加水印等。windows版本的下载地址点击这里。下载下来有这么几个文件夹,然后放到C盘内,再将放的位置在环境变量“Path”中设置一下。刷新Install中的页面,仍旧不是绿色的勾,在Apache的错误日志中提示:'ffmpeg' 不是内部或外部命令,也不是可运行的程序或批处理文件。于是我给“ffmpeg.exe”加很多用户组,但是还是不行,后面重启了一下系统,显示可以使用了。2)imagemagick安装imagemagick,一个免费的创建、编辑、合成图片的软件。它可以读取、转换、写入多种格式的图片。图片切割、颜色替换、各种效果的应用,图片的旋转、组合,文本,直线,多边形,椭圆,曲线,附加到图片伸展旋转。3)jpegtran安装jpegtran,这是一种JPEG图片压缩工具。将下载到的exe文件放到某个位置,再添加环境变量的“Path”路径中。4)exiftool安装exiftool,这是一款跨平台的图片查看,制作和编辑的应用程序。与jpegtran一样,设置“Path”路径。 三、安装Python2.71)安装Python2.7安装Python2.7,一个msi文件,直接双击即可,不过还是要配一下“Path”路径,每次配都要重启一下。需要安装python的两个模块,Pillow和SSIM。首先将“C:\Python27\Scripts”放到环境变量中,这样就能使用“pip”命令了。2)pillow从visualmetrics中查看到,SSIM要安装的名字是“pyssim”。pip install pillow3)pyssim不过在安装pyssim的时候出现了不和谐的错误。pip install pyssim先安装“Microsoft Visual C++ Compiler for Python 2.7”。根据stackoverflow的一篇文章的解答,修改了一下。但在安装scipy的时候,又出现了新的问题。在网上搜索找到了解决方案。在“Unofficial Windows Binaries for Python Extension Packages”中找到对应的“whl”文件。分别下载“numpy”,“Microsoft Visual C++ 2008 X64”和“scipy”,将这两个“whl”文件放到某个目录中,“cmd”到这个目录中,执行install命令。pip install numpy-1.12.1+mkl-cp27-cp27m-win32.whl pip install scipy-0.19.0-cp27-cp27m-win32.whl至此,所有需要安装的软件和需要配置的地方都已完毕。 四、桌面测试代理代理的相关软件和文件都在agent文件夹内,将此文件夹中的内容单独拿出来,例如“C:\wpt-agent”内。1)DummyNet安装DummyNet,DummyNet用于展示测试过程中的网络活动情况,在DummyNet文件夹内有32位和64位操作系统的安装包。打开控制面板 ==》 网络和共享中心 ==》 更改适配器设置 ==》 右键本地连接 ==》 属性Microsoft网络客户端==》安装==》服务==》添加==》从磁盘安装==》选择“C:\wpt-agent\dummynet\64bit” ==》 确定安装2)locations.ini配置地理位置locations.ini。在“www/settings”中有个“locations.ini.sample”文件,这里面是示例,将后缀“sample”去除。[locations] 1=Test_loc ; 这些是Test Location下拉列表中列出的顶级位置 ; 每一个都指向一个或多个浏览器配置 [Test_loc] 1=IE 2=Test label=Test Location group=Desktop ; browser就是测试代理wptdriver.ini中配置的浏览器 [IE] browser=IE9 connectivity=LAN label="Test Location - IE9" [Test] browser=Chrome,Firefox connectivity=LAN label="Test Location"3)wptdriver.ini配置wptdriver.ini文件,包括浏览器执行目录等信息。在原先的“agent”文件夹中有个“wptdriver.ini.sample”文件,同样去掉后缀。1. 提示下载修改浏览器的路径,注释installer。点击aptdriver.exe,提示我下载flash注释掉“spftware”就不会出现这个错误提示了。2. 浏览器闪退输入URL点击“START TEST”后跳出了这个提示,并且浏览器会闪退。后面在“wptdriver.ini”中加了debug参数后就不会闪退了,但那提示还在。[WebPagetest] debug=1一切就绪后,wptdriver窗口内显示:3. 代理连接接下来查看“install”页面,发现两个都没连接上。将“wptdriver.ini”中的location改成“IE,Test”即可,有个类似的说明可以参考《WebPagetest参数》,不过参数名字不同。[WebPagetest] location=IE,Test4. 空数据再次点击开始测试,进入到结果页面,出现下面的空数据。在官方论坛中也看到了这么一个问题,已被作者解答,只需要在“settings/locations.ini”文件中新增连接属性即可[Test] browser=Chrome,Firefox connectivity=LAN label="Test Location"完整的“wptdriver.ini”文件如下:[WebPagetest] url=http://mine2.webpagetest.net/ location=IE,Test ;browser=chrome Time Limit=120 ;防止浏览器闪退 debug=1 ;key=TestKey123 ;Automatically install and update support software (Flash, Silverlight, etc) ;software=http://mine.webpagetest.net/installers/software.dat [Chrome] exe="C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" options='--load-extension="%WPTDIR%\extension" --user-data-dir="%PROFILE%" --no-proxy-server' ;installer=http://mine.webpagetest.net/installers/browsers/chrome.dat [Firefox] exe="C:\Program Files (x86)\Mozilla Firefox\firefox.exe" options='-profile "%PROFILE%" -no-remote' ;installer=http://mine.webpagetest.net/installers/browsers/firefox.dat template=firefox [Safari] exe="C:\Program Files (x86)\Safari\Safari.exe" [IE9] exe="C:\Program Files (x86)\Internet Explorer\iexplore.exe"4)START TEST再次点击测试,将会获得熟悉的页面。
网站性能优化工具大致分为两类:综合类和RUM类(实时监控用户类),WebPageTest属于综合类。WebPageTest通过布置一些特定的场景进行测试,例如不同的网速、浏览器、位置等。测试完成后,能获得优化等级、性能参数、请求瀑布图、网页幻灯片快照等,更多信息可以参考《WebPageTest快速入门》。一、总览输入网址后,首先进入视野中的就是下面这张画面。1)原理根据WebPageTest的《概述》了解到,WebPageTest是一个PHP网站,用户输入网址、地点、自定义脚本等信息后,参数发送到后台。后台做些逻辑处理,再通过浏览器相关的代理程序,启动Chrome、Firefox或IE,浏览器执行完后。将数据传回给后台,后台再将数据保存起来,最后通过各种形式(图、表格、列等),将分析数据过的数据,呈现给用户。2)视觉进展WebPageTest会测量视觉进展,也就是展示每个时间显示多少百分比的页面,一些数据测量就是根据这个来的,具体可以参考《Speed Index》。有两种测量方法:1. 先将页面显示的过程捕获,保存成多张图片,再通过图片分析工具将每个像素与最终图像比较,算出百分比,不过页面每个像素移动都会改变比对结果2. 现在有新的方法,使用绘画事件的可视进展,不过需要Webkit内核的浏览器才支持。3)扩展WebPageTest还支持扩展开发,只要申请到一个key后,就可以根据提供的API做开发。不过调用次数都会有限制,所以如果要做还是在自己本地或内网布置一个WebPageTest的环境。后面我会专门写几篇布置环境的文章,WebPageTest在windows中布置起来简单一点。4)导航栏1. TEST RESULT:能看到最新的一个测试。2. TEST HISTORY:能查看到测试历史记录。3. FORUMS:论坛信息,里面有许多提问和回答,覆盖面非常广,下图是论坛的首页。4. DOCUMENTATION:工具文档,英文版,并且挂在google域名下,自己翻译了一下,挂在了github上。5. ABOUT:给出了WebPageTest的Github地址,以及发布版的下载地址等信息。 二、普通配置1)Test Location和Browser配置测试地址,美帝、欧洲、亚洲、非洲、美洲,各个地方都有服务器,不过还是选择一个近点的比较好,可以选香港或扬州。点击Select from Map,弹出的是google地图,你懂得,不做点措施是显示不了的。不同地点,可以选择的Browser(浏览器)将不同,例如香港服务器可以选择Chrome、Firefox和IE11,扬州就不支持IE11。 三、高级配置(Advanced Settings)1)Test SettingsConnection:网速(Connection)有光纤(Cable)、DSL或者自定义。RTT(Round Trip Time):一个数据包从发出去到回来的时间。自定义设置中可以设置:下行带宽(BW Down),上行带宽(BW Up),延迟(Latency),丢包率(Packet Loss)。Repeat View:选择“First View and Repeat View”后,就启动重复视图,每次测试有两个视图,第二个的时候,就可以模拟有缓存的情况。2)Advanced高级设置中的高级设置,可以修改访问代理信息、自定义头信息,能够模拟更多实际的情况。3)Chrome针对Chrome浏览器的设置,可以调用浏览器中的模拟器、捕获开发工具时间轴。4)AuthHTTP基本授权,输入用户名和密码后,这些信息经过base64编码,以HTTP请求首部的形式发送。这种技术称为HTTP基本验证(HBA),使用这种方式,需要服务器支持HBA,所以这并不是一个稳妥的方法。授权的请求首部信息类似于下面:Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=5)Script自定义脚本,网站文档《脚本》中有更多设置,非常强大,不过根据浏览器不同,能使用的脚本参数也会不同。6)Block请求阻塞,多个关键字可用空格分割,如果请求的URL中与输入的关键词匹配,那么请求将被阻塞。与下面的SPOF最大的区别是不会发生请求超时,因为这个请求根本没有创建。这个测试的目的就是简单的评估资源缺失对页面造成的影响。7)SPOF单点故障,只需将要限制的域名写在输入框中即可,一个域名一行。目的就是请求超时,对网站的影响,这是一种非常简便的检测第三方托管资源有效性的方法。8)Custom设置自定义指标,网站文档《自定义指标》有详细说明。设置完成后可以在“detail -》Custom Metrics”中查看到,有个测试案例可以查看。
一、ARP协议ARP(Address Resolution Protocol)地址解析协议,将IP地址解析成MAC地址。IP地址在OSI模型第三层,MAC地址在OSI第二层,彼此不直接通信;在通过以太网发生IP数据包时,先封装第三层(32位IP地址)和第二层(48位MAC地址)的报头;但由于发送数据包时只知道目标IP地址,不知道其Mac地址,且不能跨越第二、三层,所以需要使用地址解析协议。ARP工作流程分请求和响应:在dos窗口内“ping”某个域名抓取到的包:二、IP协议同样在dos窗口内“ping”某个域名抓取到的包:三、TCP协议TCP(Transmission Control Protocol)传输控制协议,一种面向连接、可靠、基于IP的传输层协议,主要目的是为数据提供可靠的端到端传输。在OSI模型的第四层工作,能够处理数据的顺序和错误恢复,最终保证数据能够到达其应到达的地方。1)标志位SYN: 同步,在建立连接时用来同步序号。SYN=1, ACK=0表示一个连接请求报文段。SYN=1,ACK=1表示同意建立连接。FIN: 终止,FIN=1时,表明此报文段的发送端的数据已经发送完毕,并要求释放传输连接。ACK: 确认,ACK = 1时代表这是一个确认的TCP包,取值0则不是确认包。DUP ACK:重复,重复确认报文,有重复报文,一般是是丢包或延迟引起的,从这个报文看应该是丢包了。URG:紧急,当URG=1时,表示报文段中有紧急数据,应尽快传送PSH:推送,当发送端PSH=1时,接收端尽快的交付给应用进程RST:复位,当RST=1时,表明TCP连接中出现严重差错,必须释放连接,再重新建立连接2)端口客户端与不同服务器建立连接时,源端口和目标端口可不同。3)TCP三次握手4)TCP四次挥手TCP四次断开,例如关闭页面的时候就会断开连接。5)TCP概念1. 发送窗口无法简单的看出发送窗口的大小,发送窗口会由网络因素决定。发送窗口定义了一次发的字节,而MSS定义了这些字节通过多少个包发送。2. 拥塞窗口(cwnd)描述源端在拥塞控制情况下一次最多能发送的数据包的数量。在发送方维护一个虚拟的拥塞窗口,并利用各种算法使它尽可能接近真实的拥塞点。网络对发送窗口的限制,就是通过拥塞窗口实现的。3. 在途字节数(bytes in flight)已经发送出去,但尚未被确认的字节数。在途字节数 = Seq + Len - Ack其中Seq和Len来自上一个数据发送方的包,而Ack来自上一个数据接收方的包。4. 拥塞点(congestion point)发生拥塞时候的在途字节数就是该时刻的网络拥塞点。先从Wireshark中找到一连串重传包中的第一个,再根据该Seq找到原始包最后计算该原始包发送时刻的在途字节数。5. 慢启动RFC建议初始拥塞窗口发送2、3、或4个MSS,如果发出去的包都能得到确认,则表明还没到拥塞点,可以收到n个确认增加n个MSS6. 拥塞避免慢启动持续一段时间后,拥塞窗口达到一个较大的值,就得放慢RFC建议在每个往返时间增加1个MSS,比如发了16个MSS全部确认,那么就增加到17个MSS7. 超时重传发出去的包在等待一段时间(RTO)后,没有收到确认,就只能重传了8. 快速重传(Fast Retransmit)不以时间驱动,而以数据驱动重传。如果包没有连续到达,就ACK最后那个可能被丢了的包,如果发送方连续收到3次相同的ACK,就重传。9. SACK(Selective Acknowledgment)选择性确认重传,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。10. 延迟确认(Delayed ACK)如果收到一个包后暂时没什么数据发给对方,那就延迟一段时间再确认。假如这段时间恰好有数据要发送,那数据和确认信息可以在一个包中发送。11. LSOLSO拯救CPU而出的创意,为了缓解CPU的压力,把它的一部分工作外包给了网卡,比如TCP的分段。启用LSO之后,TCP层就可以把大于MSS的数据块直接传给网卡,让网卡负责分段。比如“Seq=348586,Len=2776”,被网卡分为“Seq=348586,Len=1388”和“Seq=349974,Len=1388”两个包。在发送端抓包相当于站在CPU角度,只看到一个分段前的大包,而接收端就可以看到两个包。所以才会出现只见重传包,不见原始包的情况。12. Nagle算法在发出去的数据还没有被确认之前,假如又有小数据生成,那就把小数据收集起来,凑满一个MSS或等收到确认后再发送。13. Vegas算法通过监控网络状态来调整发包速度。当网络状态良好时,数据包的RTT比较稳定,这时可以增大拥塞窗口;当网络开始繁忙时,数据包开始排队,RTT就会变大,这时就减小拥塞窗口。6)选项字段PTR(Pointer Record):指针记录,PTR记录解析IP地址到域名TTL(Time to live):存活时间,限制数据包在网络中存在的时间,防止数据包不断的在IP互联网络上循环,初始值一般为64,每经过一个路由减去1。通过TTL过滤运营商劫持包,假的包是抢先应答的,所以和真实包的TTL可能不同(例如ip.ttl == 54)Seq:数据段的序号,当接收端收到乱序的包,就能根据此序号重新排序,当前Seq等上一个Seq号与长度相加获取到Len:数据段的长度,这个长度不包括TCP头Ack:确认号,接收方向发送方确认已经收到了哪些字节RTT(Round Trip Time):也就是一个数据包从发出去到回来的时间RTO(Retransmission TimeOut):超时重传计数器,描述数据包从发送到失效的时间间隔,是判断数据包丢失与否及网络是否拥塞的重要参数MTU(Maximum Transmit Unit):最大传输单元MSS(Maximum Segment Size):最长报文段,TCP包所能携带的最大数据量,不包含TCP头和Option。一般为MTU值减去IPv4头部(至少20字节)和TCP头部(至少20字节)得到。Win(Window Size):声明自己的接收窗口TCP Window Scale:窗口扩张,放在TCP头之外的Option,向对方声明一个shift count,作为2的指数,再乘以TCP定义的接收窗口,得到真正的TCP窗口DF(Don't fragment):在网络层中,如果带了就丢弃没带就分片MF(More fragments):0表示最后一个分片,1表示不是最后一片7)过滤表达式握手请求被对方拒绝:tcp.flags.reset === 1 && tcp.seq === 1重传的握手请求:tcp.flags.syn === 1 && tcp.analysis.retransmission过滤延迟确认:tcp.analysis.ack_rtt > 0.2 and tcp.len == 0四、UDP协议UDP(User Datagram Protocol)用户数据报协议,提供面向事务的简单不可靠信息传送服务。将网络数据流压缩成数据包的形式。每一个数据包的前8个字节保存包头信息,剩余的包含具体的传输数据。虽然UDP是不可靠的传输协议,但它是分发信息的理想协议,例如在屏幕上报告股票市场、显示航空信息;在路由信息协议RIP(Routing Information Protocol)中修改路由表、QQ聊天、迅雷、网络电话等。TCP的效率不一定比UDP低,只要窗口足够大,TCP也可以不受往返时间的约束而源源不断地传数据。1)UDP的优势1. UDP 协议的头长度不到TCP头的一半,所以同样大小的包里UDP携带的净数据比TCP包多,2. 没有Seq和Ack等概念,省去了建立连接的开销,DNS解析就使用UDP协议。2)UDP的劣势1. 超过MTU的时候,发送方的网络层负责分片,接收方收到分片后再组装起来,这个过程会消耗资源,降低性能。2. 没有重传机制,丢包由应用层处理,某个写操作有6个包,当有一个丢失的时候,就要将6个包重新发送。3. 分片机制存在弱点,接收方是根据包中的“More fragments”的flag来判断是否包已接收完,1表示还有分片,0表示最后一个分片,可以组装了。如果持续发送flag为1的UDP,接收方无法组装,就有可能耗尽内存。五、ICMP协议ICMP(Internet Control Message Protocol)网际报文控制协议,用于传输错误报告控制信息,对网络安全有极其重要的意义。例如请求的服务不可用、主机或路由不可达,ICMP协议依靠IP协议来完成任务,是IP协议的一个集成部分。通常不被用户网络程序直接使用,多用于ping和tracert等这样的诊断程序。六、DNS协议DNS(Domain Name System)域名系统,DNS就是进行域名解析的服务器。DNS协议运行在UDP协议之上,端口为53,工作原理如下:DNS的解析过程:DNS客户机向本地域名服务器A发送查询,如果A中没有保存IP地址记录,A就会发请求给根域名服务器B如果B中也没有,A就发请求给C,再没有就发请求给D,然后是E,找到后将地址发给DNS客户机。域名解析过程涉及到递归查询和迭代查询。客户机再与Web服务器连接。七、HTTP协议HTTP(HyperText Transfer Protocol)超文本传输协议,HTTP是一个应用层协议,无状态,由请求和响应构成,是一个标准的客户端服务器模型。HTTP工作流程如下:下面是报文首部字段的说明,表格的摘自《图解HTTP》。HTTP请求头域: Accept 用户代理能够处理的媒体类型(MIME)及媒体类型的相对优先级,“text/plain;q=0.3” Accpet-Charset 通知服务器用户代理支持的字符集及字符集的相对优先顺序,“iso-8859-5” Accept-Encoding 告知服务器用户代理支持的内容编码及优先级顺序“gzip,deflate” Accept-Language 告知服务器用户代理能够处理的自然语言集及优先级,“zh-cn,zh;q=0.7” Authorization 用户代理的认证信息(证书值),“Basic dWVub3NlbjpwYNzd==” Expect 期望出现的某种特定行为,错误时返回“417 Expectation Failed”,“100-continue” From 用户的电子邮箱地址,为了显示搜索引擎等用户代理负责人的联系方式,“info@ha.com” Host 请求的资源所处的互联网主机名和端口号,必须包含在请求头中,“www.hh.com” If-Match 条件请求,只有当If-Match字段值与ETag匹配才会接受请求,否则返回“412 Precondition Failed” If-Modified-Since 若字段值早于资源的更新时间(Last-Modified),资源未更新,返回“304 Not Modified” If-None-Match 与If-Match相反 If-Range 字段值和请求资源的ETag或时间一致时,作为范围请求处理,反之,返回全体资源 If-Unmodified-Since 与If-Modified-Since作用相反 Max-Forwards 以十进制整数形式指定可经过的服务器最大数目。服务器转发一次,减少1,当为0就不进行转发 Proxy-Authorization 接收从代理服务器发来的认证质询时,发送此字段,告知服务器认证所需要的信息 Range 只需获取部分资源的范围请求,“5001-10000”从5001字节到10000字节的资源。 Referer 请求的原始资源的URI,也就是上一页 TE 客户端能够处理响应的传输编码方式及相对优先级,还可指定Trailer字段分块传输编码的方式。“gzip,deflate;q=0.5”User-Agent 创建请求的浏览器和用户代理名称等信息HTTP应答头域: Accpet-Ranges 告知客户端服务器是否能处理范围请求,以指定获取服务器端某个部分的资源。“bytes” Age 源服务器在多久前创建了响应,字段值单位为秒 ETag 客户端实体标识,一种可以将资源以字符串形式做唯一标识的方式 Location 将响应接收方引导至某个与请求URI位置不同的资源,会配置3xx:Redirection的响应 Proxy-Authenticate 由代理服务器所要求的认证信息发送给客户端 Retry-After 告知客户端应该在多久(秒数或具体日期)之后再次发送请求,主要配合“503 Service Unavailable”或“3xx Redirect”。 Server 当前服务器上安装的HTTP服务器应用程序的信息,包括版本号等。“Apache/2.2.6 (Unix) PHP/5.2.5” Vary 对缓存进行控制,设置“Accept-Language”,如果字段值相同,就从缓存返回响应。 WWW-Authenticate HTTP访问认证,告知客户端适用于访问请求URI所指定资源的认证方案(Basic或Digest)和带参数提示的质询(challenge)HTTP通用头域:Cache-Control操作缓存的工作机制,多个指令用“,”分割,“private,max-age=0,no-cache”Connection控制不再转发给代理的首部字段与管理持久连接,“keep-alive”DateHTTP报文的日期和时间PragemaHTTP1.1之前的遗留字段,作为向后兼容定义,只用在客户端发送的请求中。“no-cache”Trailer说明在报文主体后记录了哪些首部字段,可应用在分块编码传输时。在报文最后写了重要信息Transfer-Encoding传输报文主体时采用的编码方式,分块传输“chunked”Upgrade检测HTTP协议及其他协议是否可使用更高版本进行通信Via追踪客户端与服务器之间的请求和响应报文的传输路径,各个代理服务器会往Via添加自己的服务器信息Warning告知用户一些与缓存相关问题的警告HTTP实体头域: Allow 告知客户端能够支持Request-URI指定资源的所有HTTP方法,“GET,HEAD”。当不支持,会返回“405 Method Not Allowed” Content-Encoding 服务器对实体的主体部分选用的内容编码方式,在不丢失内容的前提下进行压缩。“gzip” Content-Language 实体主体使用的自然语言(中文或英文等) Content-Length 主体部分的大小(单位是byte) Content-Location 给出与报文主体部分相对应的URI,与Location不同 Content-MD5 一串由MD5算法生成的值,目的在于检查报文主体在传输过程中是否保持完整,以及确认传输到达 Content-Range 针对范围请求,作为响应返回的实体的哪个部分符合范围请求,单位为byte。“bytes 5001-10000/10000” Content-Type 实体主体内对象的媒体类型,与Accpet一样,字段值用type/subtype形式赋值。“text/html; charset=UTF-8” Expires 将资源失效的日期告知客户端。当首部字段Cache-Control有指定max-age指令时,优先处理max-age指令 Last-Modified 指明资源最终修改时间,一般来说,这个值就是Request-URI指定资源被修改的时间详细信息可以参考MDN的《HTTP Headers》MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准,一种通知客户端其接收文件的多样性的机制,文件后缀名在网页上并没有明确的意义。 八、HTTPS协议HTTPS(Hypertext Transfer Protocol over Secure Socket Layer)基于SSL的HTTP协议,HTTP的安全版。使用端口43,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输和身份认证的网络协议。1)HTTPS工作流程2)SSLSSL(Secure Sockets Layer)安全套接层,TLS(Transport Layer Security)传输层安全是其继任者。SSL和TLS在传输层对网络连接进行加密。SSL协议分为两层,SSL记录协议(SSL Record Protocol)和SSL握手协议(SSL Handshake Protocol)。SSL记录协议建立在TCP之上,提供数据封装、压缩加密基本功能的支持。SSL握手协议建立在SSL记录协议之上,在数据传输之前,通信双方进行身份认证、协商加密算法和交换加密秘钥等。SSL工作分为两个阶段,服务器认证和用户认证。SSL协议既用到了公钥加密(非对称加密)又用到了对称加密技术。3)数据包客户端与服务器之间的通信:1.客户端发出请求(Client Hello)2.服务器响应(Server Hello)3)证书信息3.密钥交换4.应用层信息通信用户可以发送通过TLS层使用RC4的写实例加密过的普通HTTP消息,也可以解密服务端RC4写实例发过来的消息。此外,TLS层通过计算消息内容的HMAC_MD5哈希值来校验每一条消息是否被篡改。
一、数据包详细信息Packet Details面板内容如下,主要用于分析封包的详细信息。帧:物理层、链路层包:网络层段:传输层、应用层1)Frame物理层数据帧概况2)Ethernet II数据链路层以太网帧头部信息3)Internet Protocol Version 4互联网层IP包头部信息IP包头:4)Transmission Control Protocol传输层数据段头部信息,此处是TCP协议TCP包头:5)Hypertext Transfer Protocol应用层信息,此处是HTTP协议 二、着色规则Wireshark默认有一组着色规则,可以在Packet Details面板中展开包的帧部分,查看着色规则。在View | Coloring Rules中,打开着色规则窗口,可以自己创建、删除、选中、去除。三、Wireshark提示1)Packet size limited during capture说明被标记的那个包没有抓全。一般是由抓包方式引起,有些操作系统中默认只抓每个帧的前96个字节。4号包全长171字节,但只有96字节被抓到。2)TCP Previous segment not captured如果Wireshark发现后一个包的Seq大于Seq+Len,就知道中间缺失了一段。如果缺失的那段在整个网络包中找不到(排除了乱序),就会提示。6号包的Seq是1449大于5号包的Seq+Len=1+1=1,说明中间有个1448字节的包没被抓到,就是“Seq=1,Len=1448”。3)TCP ACKed unseen segment当Wireshark发现被Ack的那个包没被抓到,就会提示。32号包的Seq+Len=6889+1448=8337,说明下一个包Seq=8337。而我们看到的是35号包的Seq=11233,意味着8337~11232这段数据没抓到。4)TCP Out-of-Order当Wireshark发现后一个包的Seq号小于前一个包的Seq+Len时,就会认为乱序,发出提示。3362号包的Seq小于3360包的Seq,所以就是乱序。5)TCP Dup ACK当乱序或丢包发生时,接收方会收到一些Seq号比期望值大的包。没收到一个这种包就会Ack一次期望的Seq值,提现发送方。7号包期望的下一个Seq=30763,但8号包Seq=32223,说明Seq=30763包丢失,9号包发了Ack=30763,表示“我要的是Seq=30763”。10号、12号、14号也都是大于30763的,因此没收到一个就回复一次Ack。6)TCP Fast Retransmission当发送方收到3个或以上的【TCP Dup ACK】,就意识到之前发的包可能丢了,于是快速重传它。7)TCP Retransmission如果一个包真的丢了,又没有后续包可以在接收方触发【Dup Ack】就不会快速重传。这种情况下发送方只好等到超时了再重传。1053号包发出后,一直没有等到相应的Ack,只能在100多毫秒之后重传了。8)TCP zerowindow包种的“win”代表接收窗口的大小,当Wireshark在一个包中发现“win=0”时,就会发提示。9)TCP window Full此提示表示这个包的发送方已经把对方所声明的接收窗口耗尽了。当Wireshark计算出Middle East已经有65535字节未被确认,就会发出此提示。【TCP window Full】表示发送方暂时没办法再发送数据;【TCP zerowindow】表示发送方暂时没办法再接收数据。10)TCP segment of a reassembled PDUWireshark可以把属于同一个应用层的PDU的TCP包虚拟地集中起来。TCP层收到上层大块报文后分解成段后发出去,主机响应一个查询或者命令时如果要回应很多数据(信息)而这些数据超出了TCP的最大MSS时,主机会通过发送多个数据包来传送这些数据(注意:这些包并未被分片)。11)Time-to-live exceeded(Fragment reassembly time exceeded)表示这个包的发送方之前收到了一些分片,但由于某些原因迟迟无法组装起来。
上传我们一般都是用“input[type=file]”控件。当你用此控件时,你就授权了网页和服务器访问对应的文件,就可以得到File对象。友情提示在,在Android手机webview中,是不支持上传文件的,网上说是修改Android端的代码,但我没试过,我们这边是使用客户端提供的接口来实现上传的。下面的示例代码可以在这里查看到。一、accept属性该属性表明了服务器端可接受的文件类型,可以限制你手机选择相关的文件,如果限制多个,可以用逗号分割,下面的代码就表示只能选择图片与音频相关的文件:<input accept="image/*,audio/*" type="file"/>在移动端,点击后会让你选择拍照或相册,还是蛮高大上的。下图是UC浏览器中:二、change事件一般选择文件都会使用“change”事件,下面的代码就是绑定了change事件,弹出文件大小:var upload = document.getElementById('upload'); upload.addEventListener('change', function() { var file = upload.files[0]; alert(file.size); }, false);1) 有些手机浏览器在点击的时候,会弹出键盘选择,我用onfocus="this.blur()",来强制失去焦点。<input type="file" id="upload" onfocus="this.blur()"/>2) 当选择过一次后,再次选择同一个文件,“change”事件不会触发,因为value没有改变,在网上看到个方法,我还没有在实际项目中使用,兼容性有待考证。使用“Node.cloneNode”复制上传元素,再用“Node.replaceChild”替换节点。这里注意下:克隆一个元素节点会拷贝它所有的属性以及属性值,但不会拷贝那些使用addEventListener()方法或者node.onclick = fn用JavaScript动态绑定的事件。upload.addEventListener('change', function() { var upload = document.getElementById('upload'); //每次要动态获取 var file = upload.files[0]; console.log(file.size); //解决上传相同文件不触发onchange事件 var clone = upload.cloneNode(true); clone.onchange = arguments.callee; //克隆不会复制动态绑定事件 clone.value = ''; upload.parentNode.replaceChild(clone, upload); }, false);三、File对象用户所选择的文件都存储在了一个FileList对象上,其中每个文件都对应了一个File对象File对象负责处理那些以文件形式存在的二进制数据,也就是操作本地文件。File对象是Blob【下面会提到】的特殊类型,即大块的二进制数据,File对象的尺寸及类型等属性都继承自Blob。1)File对象可以通过3种方式获取:1. <input>元素上选择文件后返回的FileList对象中的成员2. 拖放操作【Drag或Drop】生成的 DataTransfer对象内files属性中的成员3. HTMLCanvasElement上执行mozGetAsFile()方法后的返回结果document.getElementById('upload').files[0]//选取第一个文件对象2)File对象有9个属性,这里就只介绍3个:1. name:当前File对象所引用文件的文件名,不包括路径,只读。2. size:文件大小,单位为字节,只读的64位整数.3. type:MIME类型,只读字符串,如果类型未知,则返回null。有些移动端的浏览器明明选择了图片,返回的却是null,非常坑。还有3个非标准的方法:getAsBinary()、getAsDataURL()和getAsText(in DOMString encoding)。这3个方法现在已经过时,现在用FileReader对象中的方法来取代。 四、FileReaderweb应用程序可以异步的读取存储在用户计算机上的文件(或者原始数据缓冲)内容,可以使用File对象或者Blob对象来指定所要处理的文件或数据。1) readAsArrayBuffer():在返回的result属性中将包含一个ArrayBuffer对象【缓冲数组,是一种用于呈现通用、固定长度的二进制数据的类型】以表示所读取文件的内容Blob可以“append”,ArrayBuffer数据。ArrayBuffer存在的意义就是作为数据源提前写入在内存中,就是提前钉死在某个区域,长度也固定。2) readAsBinaryString():result属性中将包含所读取文件的原始二进制数据3) readAsDataURL():result属性中将包含一个data: URL格式的字符串以表示所读取文件的内容4) readAsText():result属性中将包含一个字符串以表示所读取的文件内容下面的代码是获取data:URL,可以将返回的result内容赋值给img的src,用于预览等操作。var reader = new FileReader(); reader.readAsDataURL(file); reader.onload = function(e) { var img = new Image(); img.src = this.result; console.log(this.result); };console.log(this.result)内容如下:五、URL对象URL对象是硬盘上指向文件的URL。上面的例子中获取图片的引用,通过读取data URI,data URI是个一大串的字符。图片原本就在硬盘上,还要转换成另一个格式再用,有点绕了,完全可以直接引用文件的URL,下面是两个方法:1) URL.createObjectURL():接收一个文件的引用(File或Blob对象)返回一个URL对象2) URL.revokeObjectURL():销毁创建的URLvar url = URL.createObjectURL(file); var img = new Image(); img.src = url; img.onload = function(e) { window.URL.revokeObjectURL(this.src); //销毁 } console.log(url);console.log(url)内容如下:在移动端需要做个兼容性判断:window.URL = window.URL || window.webkitURL;六、Blob对象Blob(binary large object)对象代表了一段二进制数据,就是一个包含只读原始数据的类文件对象。File接口基于Blob,继承了Blob的功能,并且扩展支持了用户计算机上的本地文件。1)创建Blob对象的4种方法:1. 调用Blob构造函数2. 使用一个已有Blob对象上的slice()方法切出另一个Blob对象3. 调用canvas对象上的toBlob方法4. 过气的方法,通过BlobBuilder接口创建,但兼容性不好,并且现有的BlobBuilder实现都是带前缀的 2)利用Blob对象,生成可下载文件var blob = new Blob(["pwstrick"]);//数组中添加DOMString对象 var a = document.createElement("a"); a.href = URL.createObjectURL(blob);//创建URL对象 a.download = "test.txt";//HTML5新属性 a.textContent = "test"; document.getElementsByTagName('body')[0].appendChild(a);生成一个“a”标签,并且点击这个链接,可以下载一个txt文本,内容是“pwstrick”。 3)通过slice方法,将二进制数据按照字节分块,返回一个新的Blob对象upload.addEventListener('change', function() { var upload = document.getElementById('upload'); //每次要动态获取 var file = upload.files[0]; var start = 0; var chunk = 1024 * 10; //10KB var end = start + chunk; var size = file.size; while (start < size) { segment(file, start, end); start = end; end = start + chunk; if (end > size) { end = size; } } }, false); function segment(file, start, end) { var reader = new FileReader(); reader.onload = function(evt) { console.log(['Read bytes: ', start, ' - ', end].join('')); }; var blob = file.slice(start, end); reader.readAsBinaryString(blob); }七、formDataXMLHttpRequest Level 2添加了一个新的接口FormData。利用FormData对象,可以使用键值对来模拟一个完整的表单,然后使用XMLHttpRequest发送这个"表单"。使用FormData的最大优点就是我们可以异步上传一个二进制文件。var formData = new FormData(); formData.append("name", "value");//普通键值对 formData.append("blob", blob); //传递一个blob对象 formData.append("file", file); //传递一个file对象 var oReq = new XMLHttpRequest(); oReq.open("POST", "http://xx.com"); oReq.send(formData);
一、JS-SDK 公众号中的微信支付需要通过JS来实现。微信JS-SDK是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包。点击查看在线文档。 1)引入JS脚本文件<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>2)通过config接口注入权限验证配置<script> wx.config({ debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: '', // 必填,公众号的唯一标识 timestamp: , // 必填,生成签名的时间戳 nonceStr: '', // 必填,生成签名的随机串 signature: '',// 必填,签名 }); </script>appId就是应用ID,wx打头的那串字符,timestamp在php中就用time()获取,而nonceStr就用uniqid()获取,而signature根据特定算法获取。protected function getJsapiConfig() { $weixin = new Weixin(); $ticketMongo = new WeixinJsapiTicket(); $data = [ 'appId' => $weixin->getAppId(), 'noncestr' => uniqid(), 'jsapi_ticket' => $ticketMongo->getJsapiTicket(), 'timestamp' => time() ]; //拼装原始待签名串 $src = [ 'noncestr=' . $data['noncestr'], 'jsapi_ticket=' . $data['jsapi_ticket'], 'timestamp=' . $data['timestamp'] ]; sort($src); $data['signature'] = sha1(implode('&', $src)); return $data; }这里说明下“jsapi_ticket”,jsapi_ticket是公众号用于调用微信JS接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于有时间限制,而且获取jsapi_ticket的api调用次数非常有限,所以我会将获取到的jsapi_ticket保存到MongoDB中。/** * 通过access_token获取jsapi_ticket * @param $access_token * @return string | null */ public function getJsapiTicket($access_token) { $url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket'; $param = [ 'access_token' => $access_token, 'type' => 'jsapi' ]; $res = $this->request($url, $param); $result = json_decode($res, true); if (isset($result['errcode']) && $result['errcode'] == 0 && isset($result['ticket'])) { return $result; } return null; }3)通过ready接口处理成功验证1)prepay_id是根据本地生成的订单号等获取的,订单号每次请求的得不一样,不然会报错的,点击查看在线文档。2)nonceStr就用md5(uniqid('baiaimama'))获取3)signType使用MD54)paySign根据代码的参数,排序后拼接获得。wx.chooseWXPay({ timestamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符 nonceStr: '', // 支付签名随机串,不长于 32 位 package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***) signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5' paySign: '', // 支付签名 success: function (res) { // 支付成功后的回调函数 } });/** * 生成jsapi需要调用的参数 */ public function getJsapiParam(){ $param = [ 'appId' => $this->APPID, 'timeStamp' => time(), 'nonceStr' => md5(uniqid('baiaimama')), 'package' => 'prepay_id='.$this->param['prepay_id'], 'signType' => 'MD5' ]; $str = []; foreach($param as $k=>$v){ if(!empty($v)){ $str[] = "{$k}={$v}"; } } sort($str); $unsignKey = join('&', $str).'&key='.$this->KEY; $sign = strtoupper(md5($unsignKey)); $param['paySign'] = $sign; return $param; }二、异步回调异步回调中做些修改订单状态、发送短信,推送消息等操作。/** * 微信支付异步回调API * 微信支付成功,会收到异步回调 */ public function actionWxpay() { $weixinPay = new WeixinPay(); $weixin = new Weixin(); $xml = file_get_contents('php://input'); $msg = $weixin->parseMsg($xml); //记录微信推送日志 $notifyMongo = new WeixinPayNotify(); $notifyMongo->logPayNotify($xml); if(!$msg || !is_object($msg)){ $weixinPay->notifyXml('FAIL', '通知不合法'); } if(!isset($msg->return_code) || $msg->return_code != 'SUCCESS'){ $weixinPay->notifyXml('FAIL', '通信失败'); } if(!isset($msg->result_code) || $msg->result_code != "SUCCESS"){ $weixinPay->notifyXml('FAIL', '交易失败'); } //签名验证失败 if(!$weixinPay->checkSign($msg)){ $weixinPay->notifyXml('FAIL', '签名验证失败'); } //$notifyMongo->add($msg); //流程走到这里说明已经支付成功了,这里无需更新订单逻辑 $userOrder = new UserOrder(); //记录微信订单号 $userOrder->pay($msg->out_trade_no, $msg->transaction_id); }
一、access_token1)两种access_token,网页授权access_token和普通access_token1、微信网页授权是通过OAuth2.0机制实现的,在用户授权给公众号后,公众号可以获取到一个网页授权特有的接口调用凭证(网页授权access_token),通过网页授权access_token可以进行授权后接口调用,如获取用户基本信息。2、其他微信接口,需要通过基础支持中的“获取access_token”接口来获取到的普通access_token调用。access_token是公众号的全局唯一票据,access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。 2)分别获取access_token1、网页授权的:点击查看网页授权获取用户基本信息文档,通过查看这个文档,可以看到通过code换取网页授权access_token,而这个code是通过微信的一个授权链接获取到的,然后再根据文档中的请求获取到的,具体的链接地址和参数可以参考文档。/** * 创建一个需要通过微信的OAuth2.0认证的服务url * @param $url 服务号需要认证访问的url * @param $scope string snsapi_userinfo | snsapi_base * snsapi_userinfo 可以用来获取用户信息 * snsapi_base 可以用来获取openid * @param string $state 自定义状态值 * 此处约定为from_weixin代表是从微信认证过来,一般无需轻易变化 * @return string 返回认证url地址 */ public function createAuthUrl($url, $scope = 'snsapi_base', $state = 'from_weixin') { $url = strval($url); $authUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize'; /** * 此处有大坑,请不要打乱param的顺序 * 否则微信认证界面会出现白屏 */ $param = array( 'appid' => $this->appId, 'redirect_uri' => urlencode($url), 'response_type' => 'code', 'scope' => $scope, 'state' => $state ); $seg = array(); foreach ($param as $k => $v) { $seg[] = "{$k}={$v}"; } return $authUrl . '?' . join('&', $seg) . '#wechat_redirect'; }2、普通的:点击查看获取access token文档,通过三个参数获取到。这里需要注意的是,获取到的token,是有时效性的,2 个小时,所以我会保存在MongoDB中,先从数据库中比对超时了没有,没有的话就直接从数据库中获取,减少不必要的请求。 二、推送日志 在与微信的交互中,会产生很多日志信息,并且开发的时候经常需要分析这些日志,这里我将日志都保存在了MongoDB中。MongoDB方便的地方是任何结构的数据都能放在一个document中,不像MySQL要定义好字段名,我经常调试的时候将各种结构放在一个document中。 在微信的入口页面中,也就是前面提到的URL(服务器地址),会在这里面做保存日志的逻辑。逻辑包括关注的时候推送一条消息,二维码扫描关注,点击某个菜单产生事件,点击菜单的超链接等。日志结构如下:1、代码中包括签名验证逻辑2、通过file_get_contents('php://input')来获取请求数据,就是下面的getRawMsg方法3、将推送日志直接塞入到MongoDB中4、将接收到的请求信息SimpleXMLElement对象,就是下面的parseMsg方法5、handleEventMsg就是在处理各种不同情况了/** * 微信公众号入口 */ public function actionPortal() { $weixin = new Weixin(); //签名验证逻辑 // if($weixin->checkSignature()){ // echo $_GET['echostr']; // } // exit; //读取原始请求数据 $msg = $weixin->getRawMsg(); //推送日志 $pushlog = new WeixinPushLog(); $pushlog->logWeixinPush($msg); $msgObj = $weixin->parseMsg($msg); if ($msgObj === false || !is_object($msgObj)) { exit; } switch ($msgObj->MsgType) { case 'event' : //接收事件消息 $this->handleEventMsg($msgObj); break; default : //todo break; } }public function getRawMsg() { return file_get_contents('php://input'); } /** * 解析接收到的消息 * @param string $msg 消息体 * @return bool|SimpleXMLElement */ public function parseMsg($msg = '') { if (!$msg || empty($msg)) { return false; } $msgObj = simplexml_load_string($msg, 'SimpleXMLElement', LIBXML_NOCDATA); if ($msgObj === false || !($msgObj instanceof \SimpleXMLElement)) { return false; } return $msgObj; }6、如果要推送消息,die这个方法得要加上7、下面的代码只列举了两种事件情况,一种是订阅、一种是点击事件8、createRawTuWenMsg是在拼接XML,点击查看模板消息接口。private function handleEventMsg($msgObj) { $weixin = new Weixin(); $openId = $msgObj->FromUserName; $fromUserName = $msgObj->ToUserName; //未关注,关注后推送 if ($msgObj->Event == 'subscribe') { $pushData['PicUrl'] = 'http://mmbiz.qpic.cn/'; $pushData['Title'] = '基因检测,带你一起探索生命的奥妙 '; $pushData['Description'] = '为什么不同人在身高、体重、肤色和形状上长得不一样?但是往往又和自己的父母相似?'; $pushData['Url'] = 'http://mp.weixin.qq.com'; $msg = $weixin->createRawTuWenMsg($fromUserName, $openId, array($pushData)); die($msg); }elseif($msgObj->Event == 'CLICK'){ //die($msg); } }public function createRawTuWenMsg($fromUserName, $toUserName, $items = array()) { if (!is_array($items)) { return ''; } $count = count($items); $its = ''; foreach ($items as $item) { $its .= <<<ITEMTPL <item> <Title><![CDATA[{$item['Title']}]]></Title> <Description><![CDATA[{$item['Description']}]]></Description> <PicUrl><![CDATA[{$item['PicUrl']}]]></PicUrl> <Url><![CDATA[{$item['Url']}]]></Url> </item> ITEMTPL; } $msg = <<<MSG <xml> <ToUserName><![CDATA[{$toUserName}]]></ToUserName> <FromUserName><![CDATA[{$fromUserName}]]></FromUserName> <CreateTime>12345678</CreateTime> <MsgType><![CDATA[news]]></MsgType> <ArticleCount>{$count}</ArticleCount> <Articles> {$its} </Articles> </xml> MSG; return $msg; }
当我们ajax提交一个按钮的时候,给那个按钮来个Loading效果会高端很多,体验也会上升个层次。既能让用户知道正在提交中,也能防止二次提交,好处多多呢。上面的这个圈圈是会滚动的。简单点的话,可以直接用GIF动态图片实现。不过现在已经有了CSS3和HTML5了,多了好几种高大上的实现方式。这里先来介绍几个动画的在线demo,第一个是HTML5 Boilerplate中的Effeckt.css,第二个是Animate.css。下面一一列出,如果要结合按钮的话,可自行修改下CSS或JS等文件。当要嵌入到实际项目中的时候,可能会改动一些地方,以实际情况为准了。 一、PNG图片+CSS3动画<div class="pull-up pull-up-loading"> <span class="pull-icon"></span>正在载入中.... </div>.pull-up-loading .pull-icon { background-position: 0 100%; /*chrome*/ -webkit-transform: rotate(0deg) translateZ(0); -webkit-transition-duration: 0ms; -webkit-animation-name: loading; -webkit-animation-duration: 2s; -webkit-animation-iteration-count: infinite; -webkit-animation-timing-function: linear; } /*chrome*/ @-webkit-keyframes loading { from { -webkit-transform: rotate(0deg) translateZ(0); } to { -webkit-transform: rotate(360deg) translateZ(0); } }点击查看在线实例:只有当加上pull-up-loading,才会出现滚动添加一个动画keyframes,叫loading,是在做transform: rotate操作,下面的CSS省略了firefox中的动画代码,为了看的清晰点,实例中有完整的firefox代码这里有个在线生成Loading的纯CSS代码,cssload.net。样式选择还是挺多的,就是对于老一点的浏览器的兼容性方面不是很强比如IE6、IE7、IE8等。再来几个不同的款式:点击可查看源码 二、spin.js 这是一个脚本文件。不依赖任何库,可以独立执行,挺好用的,我在实际项目中使用过这个插件,当时我把所有的ajax提交都调用了这个插件,结合jQuery库,做到Loading效果和防止二次提交。而且这个库的浏览器兼容性很好,甚至兼容古老的IE6,而且不用引入额外的CSS或图,可配置的参数也很多。 我粗略的看过代码,标准的浏览器就用脚本写CSS3来做动画,对于古老点的浏览器就用setTimeout来模拟动画。里面还会初始化一个VML标签,这个是针对IE的。 看代码的时候看到了个很有趣的符号“~~”,后面一查,说是将变量转换成数字的一个方法,挺高级的。 这个插件还提供了一个在线配置的小网站,点击查看:showAjaxLoading: function(btn) { if (btn == null || btn == undefined || $(btn).length == 0) return; var left = $(btn).offset().left; var top = $(btn).offset().top; var width = $(btn).outerWidth(); var height = $(btn).height(); var opts = { lines: 9, // The number of lines to draw length: 0, // The length of each line width: 10, // The line thickness radius: 15, // The radius of the inner circle corners: 1, // Corner roundness (0..1) rotate: 0, // The rotation offset direction: 1, // 1: clockwise, -1: counterclockwise color: '#000', // #rgb or #rrggbb or array of colors speed: 1, // Rounds per second trail: 81, // Afterglow percentage shadow: false, // Whether to render a shadow hwaccel: false, // Whether to use hardware acceleration className: 'spinner', // The CSS class to assign to the spinner zIndex: 2e9, // The z-index (defaults to 2000000000) top: '50%', // Top position relative to parent left: '50%' // Left position relative to parent }; $('#ajax_spin').remove(); $('body').append('<div id="ajax_spin" style="position:absolute;background:#FFF;filter:alpha(opacity=30);opacity:0.3"><div id="ajax_spin_inner" style="position:relative;height:50px;"></div></div>'); $('#ajax_spin').css({ 'top': top, 'left': left, 'width': width, 'height': height }); var target = document.getElementById('ajax_spin_inner'); var spinner = new Spinner(opts).spin(target); //return spinner; }, stopAjaxLoading: function() { $('#ajax_spin').remove(); //new Spinner(opts).spin(target) //spinner.stop(); }上面那段代码是我在一个实际项目中写的,就是显示和移除Loading效果,并且在按钮上面覆盖这层效果防止二次提交。btn就是按钮jQuery对象left,top找到按钮的左右位移,width和height获取按钮的宽和高,width用的是outerWidth$('body')加入一个能够覆盖按钮的层初始化一个Spinner对象,并加入到那个覆盖层中 三、Ladda可以单独使用,或者结合上面的插件spin一起结合使用。demo页面的效果挺高大上的,但用到实例可能还是需要些修改的。点击查看主页下图随便选了几个例子,可以实现不同尺寸的按钮大小,不同方向的滚动,将按钮变成原型,或带进度条的按钮。挺多样性的。点击查看demo页面: HTML代码如下:<button class="ladda-button" data-style="expand-right"><span class="ladda-label">Submit</span></button>// Automatically trigger the loading animation on click Ladda.bind( 'input[type=submit]' ); // Same as the above but automatically stops after two seconds Ladda.bind( 'input[type=submit]', { timeout: 2000 } );结构看上去不是很复杂,JS脚本的引入也不是很难。不过在引入实际项目中肯定还是需要做些修改的。相比spin插件,这插件要引入的文件就多了,不但要引入JS还要引入CSS。 点击查看codepen上复制的代码 我本来想在codepen页面中,把demo页面重现一次,在把github里面的dist/CSS/ladda.min.css文件复制到codepen中,JS中的ladda.js和spin.js也复制过来。发生了点意外,那个滚动条老是会往下面一点。CSS都是全部复制的,很奇怪。后面发现是CSS的问题,真的是实际应用一下才会看到具体情况。 demo页面的CSS:.ladda-button .ladda-spinner { position: absolute; z-index: 2; display: inline-block; width: 32px; height: 32px; top: 50%; margin-top: -17px; opacity: 0; pointer-events: none } Github上的CSS:区别就是margin-top的不一样。.ladda-button .ladda-spinner { position: absolute; z-index: 2; display: inline-block; width: 32px; height: 32px; top: 50%; margin-top: 0; opacity: 0; pointer-events: none }四、Sonic.js这个插件是创建一个canvas画布来实现Loaing动画效果。 款式也比较多,如下图所示:点击查看在线demo在线demo中还展示了用CSS3动画+CSS Sprite技术实现动画
2022年04月