轩枫阁 – 前端开发 | web前端技术博客 https://www.xuanfengge.com 疯华正茂,您别荒! Tue, 23 Oct 2018 15:29:33 +0000 zh-CN hourly 1 https://wordpress.org/?v=4.8.3 iPhone微信二维码长按识别不了? https://www.xuanfengge.com/ios-wechat-qrcode-identify.html https://www.xuanfengge.com/ios-wechat-qrcode-identify.html#respond Tue, 23 Oct 2018 09:31:12 +0000 https://www.xuanfengge.com/?p=7045 前言

微信提供图片长按识别二维码跳转的功能,但是发现有时候在iPhone下并不是所有图都可以出现识别二维码。但是在Android的设备下可识别,这里究竟是什么原因呢?

识别方式

微信 6.7.3

Android

取整张图片分析,有二维码即可识别。

iOS

// img 图片
// screen 设备屏幕
if (img.height/img.width > screen.height/screen.width * 2
 || img.width/img.height > screen.height/screen.width * 2 ) {
    console.log('取截屏分析')
}

当图片的宽高或高宽比例超过竖屏2倍,采用的方案是截取当前屏幕分析,两端策略不一致。

所以当长图超过一定比例,二维码没在屏幕可视区域时,不会出现识别二维码。

测试图

图片尺寸:500*2170(比例为4.34)

测试机型:iPhone x

iPhone设备尺寸详情

计算结果:为达到直接识别二维码的效果,不同设备的最大比例如下

机型 最大比例
iPhone XS Max 4.33
iPhone X 4.33
iPhone 8P 3.56
iPhone 8 3.56
iPhone SE 3.55

得出结论,iPhone下图片最大比例不大于3.55均可长按识别。

]]>
https://www.xuanfengge.com/ios-wechat-qrcode-identify.html/feed 0
腾讯2019校园招聘内推启动! https://www.xuanfengge.com/tencent-2019.html https://www.xuanfengge.com/tencent-2019.html#respond Tue, 07 Aug 2018 14:25:00 +0000 https://www.xuanfengge.com/?p=7039 腾讯2019校园招聘内部推荐已经全面启动,欢迎2019年毕业的“高精尖”小鲜肉参加!

1、【提前批内推】——技术类2019届优秀毕业生

在9月线路面试之前,筛选技术类符合资质的同学提前面试并发放offer。期间如未通过筛选,仍可正常参加9月校招全国统一在线笔试。

鲜肉详细应聘流程可参考:官网http://join.qq.com/index.php

招聘要求

竞赛达人、技术学霸、实习先锋或项目核心

推荐时间

7月25日—9月7日

2、【技术大咖】——公司核心关键技术岗位的高端人才

【职位方向】

机器学习、数据挖掘、模式识别、自然语言处理、分布式计算、语音识别、图像/视频处理、引擎开发等。

【招聘要求】

1、2019届毕业生,博士优先;

2、具有专业深度、理论基础扎实、从事与公司相关技术研究领域、狂爱钻研、能够快速接受新技术、在某种技术上有独特创新的想法、具有丰富的项目研究实战经验、在核心会议期刊上发表过研究成果。

推荐时间

长期有效

3、【非技术岗位】

关于非技术的岗位,同样欢迎大家于7月校园招聘正式启动时进行内推,时间是7月25日—9月7日。

 

4、参与内推

内推邮箱:845207036@qq.com(群管理:wxg-web-ivan)

邮件标题:姓名-学校-岗位

邮件正文:

  • 姓名
  • 邮箱
  • 手机
  • 学校
  • 专业

简历命名:请以“姓名-学校-感兴趣的职位-期望工作地”的方式来命名,如“张三-浙江大学-后台开发-深圳”

首先需要在校招官网join.qq.com完善简历,发送邮件。收到简历后,会尽快进行质量筛选,通过会予以推荐,不是所有人都可以推荐,需要足够优秀。

QQ交流群:2019腾讯校园招聘交流 158057710

 

5、确认推荐

  • 学生被推荐后,系统将会发送手机短信/邮件邀请被推荐人自行完善简历
  • 学生会收到伯乐通过微信发送的“内部推荐确认函,点击确认函后,可决定是否接受伯乐的推荐。
  • 学生确认伯乐推荐后,推荐成功。如未确认,视为推荐关系不成立。

学生通过下述任一种方式确认接受推荐:

邮件确认:伯乐通知学生完善资料的邮件中,学生可通过邮件确认接受推荐;(自行关注邮件)

微信内部确认函确认:伯乐通过扫描二维码分享内部推荐确认函链接给学生,学生可通过该确认函确认接受推荐。

6、其它信息

一份简历只需要一位推荐人就好啦,即如果一位同学之前被公司同事在校招系统中推荐过,就无需再次推荐啦。

另外,伯乐推荐也不是免死金牌,伯乐推荐的简历公司不会做特别处理哦,小鲜肉能否顺利过关斩将进入鹅厂还需要凭自身实力呦~

学生本人也可以通过“腾讯招聘”公众号查流程状态哦!

 

7、相关资讯

  1. 腾讯2019校园招聘全面启动
  2. 腾讯HR:不内推就不放心?
  3. 腾讯2019校招产品经理培训生项目启动
  4. 腾讯博士生技术大咖招聘启动
  5. 腾讯微信事业群2019校园招聘等你来!
  6. 微信团队大揭秘:关于微信,你所不知道的
  7. “一分钟”进入鹅厂,腾讯微简历大赛启动
  8. 腾讯TEG校招|童鞋,告诉你为何要加入我们
]]>
https://www.xuanfengge.com/tencent-2019.html/feed 0
腾讯微信事业群2019校园招聘等你来 https://www.xuanfengge.com/%e8%85%be%e8%ae%af%e5%be%ae%e4%bf%a1%e4%ba%8b%e4%b8%9a%e7%be%a42019%e6%a0%a1%e5%9b%ad%e6%8b%9b%e8%81%98%e7%ad%89%e4%bd%a0%e6%9d%a5.html https://www.xuanfengge.com/%e8%85%be%e8%ae%af%e5%be%ae%e4%bf%a1%e4%ba%8b%e4%b8%9a%e7%be%a42019%e6%a0%a1%e5%9b%ad%e6%8b%9b%e8%81%98%e7%ad%89%e4%bd%a0%e6%9d%a5.html#respond Tue, 07 Aug 2018 13:36:33 +0000 https://www.xuanfengge.com/?p=7041

 

腾讯微信事业群2019校园招聘等你来!记得选择感兴趣事业群为“WXG微信事业群”哦~

原文:https://mp.weixin.qq.com/s/oLZz56caDn35dvEYlJfA9g

]]>
https://www.xuanfengge.com/%e8%85%be%e8%ae%af%e5%be%ae%e4%bf%a1%e4%ba%8b%e4%b8%9a%e7%be%a42019%e6%a0%a1%e5%9b%ad%e6%8b%9b%e8%81%98%e7%ad%89%e4%bd%a0%e6%9d%a5.html/feed 0
基于Canvas的手绘风格图形库 Rough.js https://www.xuanfengge.com/rough-js.html https://www.xuanfengge.com/rough-js.html#comments Sun, 18 Mar 2018 08:19:11 +0000 https://www.xuanfengge.com/?p=7026 前言

推荐一个基于Canvas的手绘风格图形JS库。

Rough.js

Rough.js 是一个轻量的(大约8k),基于Canvas的可以绘制出粗略的手绘风格库。

提供绘制线条、曲线、弧线、多边形、圆形和椭圆的基础能力,同时支持绘制SVG路径。

Github

https://github.com/pshihn/rough

安装

下载链接

https://github.com/pshihn/rough/tree/master/dist

NPM

npm install --save roughjs

使用方法

const rc = rough.canvas(document.getElementById('canvas'));
rc.rectangle(10, 10, 200, 200); // x, y, width, height

线条和椭圆

rc.circle(80, 120, 50); // centerX, centerY, diameter
rc.ellipse(300, 100, 150, 80); // centerX, centerY, width, height
rc.line(80, 120, 300, 100); // x1, y1, x2, y2

填充

rc.circle(50, 50, 80, { fill: 'red' }); // fill with red hachure
rc.rectangle(120, 15, 80, 80, { fill: 'red' });
rc.circle(50, 150, 80, {
  fill: "rgb(10,150,10)",
  fillWeight: 3 // thicker lines for hachure
});
rc.rectangle(220, 15, 80, 80, {
  fill: 'red',
  hachureAngle: 60, // angle of hachure,
  hachureGap: 8
});
rc.rectangle(120, 105, 80, 80, {
  fill: 'rgba(255,0,200,0.2)',
  fillStyle: 'solid' // solid fill
});

草绘风格

rc.rectangle(15, 15, 80, 80, { roughness: 0.5, fill: 'red' });
rc.rectangle(120, 15, 80, 80, { roughness: 2.8, fill: 'blue' });
rc.rectangle(220, 15, 80, 80, { bowing: 6, stroke: 'green', strokeWidth: 3 });

SVG 路径

rc.path('M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z', { fill: 'green' });
rc.path('M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z', { fill: 'purple' });
rc.path('M80 230 A 45 45, 0, 0, 1, 125 275 L 125 230 Z', { fill: 'red' });
rc.path('M230 230 A 45 45, 0, 1, 1, 275 275 L 275 230 Z', { fill: 'blue' });

简单的SVG路径

结合Web Workers

如果在网页中有import Workly这个Web Workers库,RoughJS会自动将所有的操作转移至web workers,来释放UI主线程。这个在使用RoughJS来创建复杂绘图(如地图)时非常有用。详细阅读相关内容

<script src="https://cdn.jsdelivr.net/gh/pshihn/workly/dist/workly.min.js"></script>
<script src="../../dist/rough.min.js"></script>

例子

https://github.com/pshihn/rough/wiki/Examples

API及文档

https://github.com/pshihn/rough/wiki

]]>
https://www.xuanfengge.com/rough-js.html/feed 1
Macbook更改bash终端名称 https://www.xuanfengge.com/macbook-change-bash-name.html https://www.xuanfengge.com/macbook-change-bash-name.html#comments Mon, 25 Dec 2017 13:38:42 +0000 https://www.xuanfengge.com/?p=7017 前言

Mac终端打开的时候,默认在用户前会显示计算机名,太长略显多余,可以通过更改配置去除。

步骤

  1. 输入命令 sudo vim /etc/bashrc
  2. 输入电脑密码
  3. 注释原配置,新增PS1=’\W \u\$’,wq!保存退出
  4. 重启终端

截图

 

彩蛋

今天是2017年12月25日,圣诞节,送上NPM的一个彩蛋

]]>
https://www.xuanfengge.com/macbook-change-bash-name.html/feed 1
轩枫阁VPS升级小记 https://www.xuanfengge.com/blog-update-log-vps.html https://www.xuanfengge.com/blog-update-log-vps.html#comments Sun, 10 Sep 2017 09:46:53 +0000 https://www.xuanfengge.com/?p=7006 近况

轩枫阁自从5月底从虚拟主机迁移至阿里云VPS后(升级小记),经常出现访问不稳定的情况,通过很多分析逐一解决问题,做了不少的优化。但最终还是发现只有充钱加强配置才能非常稳定。

不能访问主要表现为数据库连接失败,原因之一为MySQL binlog过多导致40G硬盘爆满。原因之二为Apache运行不久之后,进程数不断飙升,占用内存及CPU过大,导致MySQL进程shutdown。

轩枫阁VPS的内存为2G,还是常常由于httpd太占内存导致出问题。

原本用的架构是LAMP,决定花一些时间替换成LNMP。

环境一键安装

最开始搭建环境的时候,是花2块钱购买了阿里云Linux一键安装web环境。这个工具安装起来挺方便,但在更换环境的时候,没有再继续使用这个工具。因为可能想到到时候如果放弃阿里云,这个可能就用不上了,因为安装文件部分放在alidata目录。

使用的是LNMP一键安装包,相比于上面的工具,这个功能命令更为齐全,而且不断的在升级,安装体验很不错。

Nginx

Nginx是一个小巧而高效的Linux下的Web服务器软件,是由 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,已经在一些俄罗斯的大型网站上运行多年,目前很多国内外的门户网站、行业网站也都在是使用Nginx,相当的稳定。

Nginx相当的稳定、功能丰富、安装配置简单、低系统资源。

有的测试结果显示,Nginx 0.8.46 + PHP 5.2.14 (FastCGI) 可以承受3万以上的并发连接数,相当于同等环境下 Apache 的 10 倍。

所以对于低配置机器来说,Nginx是更好的选择。

安装过程

因为LNMP一键安装包无需一个个输入命令,无需值守,所以安装过程很方便,只需要选择安装的内容及版本即可。

安装之前备份数据库及代码,再卸载阿里云的LAMP,因为MySQL会冲突,导致无法登录。

安装完 环境,再装一下FTP,设置账户。

设置Nginx的vhost,配置相应的HTTPS证书,网站即可正常访问。

vhost文档:LNMP虚拟主机管理及伪静态使用

代码管理

目前网站的备份,是个头疼的问题,特别是wp-uploads目录图片文件很多。更新代码时,是用FTP通过比较本地与服务器文件,再进行替换升级。

收尾

安装完成之后,初步浏览没发现什么问题。打开速度还行,可以愉快的写小程序了。

]]>
https://www.xuanfengge.com/blog-update-log-vps.html/feed 2
Node inspect debugger调试工具 https://www.xuanfengge.com/node-inspect-debugger.html https://www.xuanfengge.com/node-inspect-debugger.html#respond Thu, 07 Sep 2017 08:51:45 +0000 https://www.xuanfengge.com/?p=6992 特性

Node V8.0开始支持使用Chrome Devtools调试工具来调试Node,非常的有用。

Debugger

稳定性:2 稳定

Debugger是基于TCP协议和内置调试客户端可访问的进程外Node.js调试实用程序。

可以在启动Node.js的时候,加上inspect参数,后跟脚本路径,进行调试。

// myscript.js
console.log('test')

$ node inspect myscript.js
< Debugger listening on port 9229.
< Warning: This is an experimental feature and could change at any time.
< To start debugging, open the following URL in Chrome:
<     chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=
true&ws=127.0.0.1:9229/3389ad9c-fc81-4b34-88ab-f4d8807f05f4
break in myscript.js:1
> 1 (function (exports, require, module, __filename, __dirname) { console.log('t
est')
  2 });
debug>

node运行myscript.js文件,此时Debugger 会使用WebScoket默认在本地监听9229端口。启动成功后,可以在Chrome输入面板输出的随机URL(每次运行Node,URL都会变化)

Node.js的客户端调试工具并不是全功能的调试器,但可以进行简单的step(步入步出)和inspection(检查)。

可以在代码行中使用 debugger 进行断点调试

// myscript.js
global.x = 5;
setTimeout(() => {
  debugger;
  console.log('world');
}, 1000);
console.log('hello');

当debugger运行时,第4行代码会有个断点

$ node inspect myscript.js
< Debugger listening on port 9229.
< Warning: This is an experimental feature and could change at any time.
< To start debugging, open the following URL in Chrome:
<     chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=
true&ws=127.0.0.1:9229/b2d1328f-d6db-408e-b87c-23ab100e0491
break in myscript.js:2
  1 (function (exports, require, module, __filename, __dirname) { // myscript.js

> 2 global.x = 5;
  3 setTimeout(() => {
  4   debugger;
debug> cont
< hello
break in myscript.js:4
  2 global.x = 5;
  3 setTimeout(() => {
> 4   debugger;
  5   console.log('world');
  6 }, 1000);
debug> next
break in myscript.js:5
  3 setTimeout(() => {
  4   debugger;
> 5   console.log('world');
  6 }, 1000);
  7 console.log('hello');
debug> repl
Press Ctrl + C to leave debug repl
> x
5
> 2+2
4
debug> next
< world
break in myscript.js:6
  4   debugger;
  5   console.log('world');
> 6 }, 1000);
  7 console.log('hello');
  8 });
debug> .exit

next命令为继续执行下一行代码,repl 命令可以在当前执行上下文环境进行调试,比如查看变量值。

在没有输入命令时,按下回车键,将会执行上一条命令。

输入help可以查看更多可用命令。

Watchers

可以在调试时watch表达式及变量的值。

在每个breakpointwatchers list中的每一条表达式会在当前执行上下文中即时显示结果。

开始监听:watch('my_expression')

取消监听:unwatch('my_expression')

监听列表:watchers

命令

Stepping

cont, c:继续执行

next, n:Step next

step, s:Step in

out, o:Step out

pause:pause runing code

Breakpoints

setBreakPoint(), sb():在当前行设置断点

setBreakPoint(line), sb(line):在指定代码行设置断点

setBreakPoint('fn()'), sb(...)
:在functions里的第一个语句设置断点

setBreakPoint('script.js', 1), sb(...):在script.js的第一行代码设置断点

clearBreakPoint('script.js', 1):取消设置script.js对应行的断点

断点也可以对尚未加载的file(module)进行设置

$ node inspect test/fixtures/break-in-module/main.js
< Debugger listening on ws://127.0.0.1:9229/4e3db158-9791-4274-8909-914f7facf3bd
< For help see https://nodejs.org/en/docs/inspector
< Debugger attached.
Break on start in test/fixtures/break-in-module/main.js:1
> 1 (function (exports, require, module, __filename, __dirname) { const mod = require('./mod.js');
  2 mod.hello();
  3 mod.hello();
debug> setBreakpoint('mod.js', 22)
Warning: script 'mod.js' was not loaded yet.
debug> c
break in test/fixtures/break-in-module/mod.js:22
 20 // USE OR OTHER DEALINGS IN THE SOFTWARE.
 21
>22 exports.hello = function() {
 23   return 'hello from module';
 24 };
debug>

Information

backtrace, bt:输出当前执行帧的回溯

list(5):列出当前代码的上下5行代码

watch(expr):添加watch

unwatch(expr):取消watch

watchers:列出watchers

repl:在当前执行上下文环境进行调试

exec expr:在当前执行上下文中执行表达式

执行控制

run:Run script

restart:Restart script

kill:Kill script

其它

scripts:列出所有已加载的scripts

version:显示V8的版本

高级用法

上面的介绍仅是在面板中调试,还是极为不便,如果像前端调试页面那样能够使用Chrome DevTools面板调试就非常方便了。

作为专为Node.js集成的V8调试工具,V8 Inspector支持将Chrome Devtools附加到Node.js实例进行调试,它使用的协议是Chrome Debugging Protocol

可以在启动Nide.js应用程序时,通过 --inspect 参数来启动V8 Inspector

如果需要在首行程序代码设置断点,可以传递参数 --inspect-brk

$ node --inspect index.js
Debugger listening on 127.0.0.1:9229.
To start debugging, open the following URL in Chrome:
    chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/dc9010dd-f8b8-4ac5-a510-c1a114ec7d29

在上面的示例中,URL结尾的UUID dc9010dd-f8b8-4ac5-a510-c1a114ec7d29是及时生成的,不同的调试会话中都有所不同。

技巧

1. 端口被占用

Debugger默认使用9229端口,可以指定端口

node --inspect=9222

端口被占用时提示如下

$ node inspect myscript.js
There was an internal error in node-inspect. Please report this bug.
Timeout (2000) waiting for 127.0.0.1:9229 to be free
Error: Timeout (2000) waiting for 127.0.0.1:9229 to be free
    at Timeout.setTimeout [as _onTimeout] (node-inspect/lib/_inspect.js:110:14)
    at ontimeout (timers.js:386:14)
    at tryOnTimeout (timers.js:250:5)
    at Timer.listOnTimeout (timers.js:214:5)

2. 退出debug

键入两次CTRL+C或者输入.exit

debug>
(To exit, press ^C again or type .exit)
debug> .exit

3. 查看Chrome调试信息

有时候在Node启动时,可能会输出很多的日志,导致刷掉Debugger调试URL无法复制。

此时可以在Chrome中访问 chrome://inspect/#devices ,找到相关的页面,点击Inspect打开调试面板

英文文档:https://nodejs.org/api/debugger.html

]]>
https://www.xuanfengge.com/node-inspect-debugger.html/feed 0
WP数据库连接错误 之 Apache内存优化 https://www.xuanfengge.com/wp-mysql-apache.html https://www.xuanfengge.com/wp-mysql-apache.html#comments Sat, 19 Aug 2017 14:30:38 +0000 https://www.xuanfengge.com/?p=6972 前几天写了篇文章:WP数据库连接错误之MySQL日志,写的主要是关于MySQL binlog导致的服务器硬盘爆满。

然而上周本站又出现了数据库连接错误的情况,也是重启无效,只好一步步进行分析,最后发现是MySQL进程会自动挂掉,重启之后首次访问正常,再次访问则提示错误。

分析

1. 查看日志

tail /var/log/messages

tail /alidata/log/mysql/error.log

Out of memory: Kill process 19359 (mysqld)

结论mysqld的进程自动被kill,导致数据库shutdown无法访问,但是Apache服务正常,原因是可用内存不足。

2. 是否MySQL占用内存过高?

查看MySQL配置项 innodb_buffer_pool_size:view /etc/my.cnf

只有64M,并不会占用太高的内存,所以是其它进程占用内存过高。

3. 什么进程很占用内存?

查看系统运行状态:top

按照内存占用排序:shift+m

可以看到上图中,运行着很多个httpd进程(即Apache),虽然平均占用只有2%,但是有大约30个就已经耗费系统60%的内存,而系统本身运行也需要内存。

服务器2G内存的配置,只剩下约75M可用,直接导致MySQL进程自动被kill。

4. 如何解决?

Apache开启的进程过多,其实不需要开启那么多,需要进行优化。

优化

1. 查看当前Apache配置及运行情况

查看Apache运行模式:httpd -l

当前Apache连接数:ps aux | grep httpd | wc -l

计算httpd占用内存平均数:ps aux | grep -v grep |awk '/httpd/{sum += $6;n++};END{print sum/n}'

当前的运行模式是 prefork 模式,32个进程,平均约17M。

2. 查找Apache配置 httpd.conf

查看Apache路径:whereis httpd

查找配置文件:vim /etc/httpd/conf/httpd.conf

或者:find / -name httpd.conf

本站配置

  • 操作系统:CentOS 7.2 64位
  • CPU:1核
  • 内存:2GB
  • Apache:2.4.10

3. 关于prefork模式及配置

prefork是Apache是linux默认安装使用的模式,而prefork是多进程处理的,每个进程都使用一定内存。

所以限制httpd进程的数量,即可达到优化Apache占用内存的目的。

修改httpd.conf文件

Timeout 30
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 4
<IfModule prefork.c>
StartServers 5
MinSpareServers 5
MaxSpareServers 10
ServerLimit 5500
MaxClients 5000
MaxRequestsPerChild 100
</IfModule>

参数详解

Timeout:单个连接多长时间后断开,一般值30-60

KeepAlive:on | off ,值为on时,用户在发起HTTP请求后,Apache不会立刻关闭此连接,等待KeepAliveTimeout时间后关闭

MaxKeepAliveRequests:一个连接的最大请求量,当页面中含较多图片时,可适当调高,一般值80-120

KeepAliveTimeout:当处理用户的一次连接,如果在该时间内还有请求则继续执行,无需创建新连接,直到达到MaxKeepAliveRequests最大值时退出。

以上为可能影响Apache性能的配置,以下为prefork参数设置,前面3个参数决定空闲进程数量

StartServers:启动时默认启动的进程数

MinSpareServers:设置空闲子进程的最小数量。空闲子进程是指没有正在处理请求的子进程。如果当前空闲子进程数少于MinSpareServers ,那么Apache将以第一秒一个,第二秒两个,第三秒四个,按指数递增个数的速度产生新的子进程

MaxSpareServers:置空闲子进程的最大数量。如果当前有超过MaxSpareServers数量的空闲子进程,那么父进程将杀死多余的子进程。

ServerLimit:Apache最大并发响应数,即最大支持同时连接的客户端数

MaxClients:设定Apache可同时处理的请求数量,MaxClients不得大于ServerLimit参数

MaxRequestsPerChild:每个子进程理多少个请求后将自动销毁。到达MaxRequestsPerChild的限制后,子进程将会结束

各参数设置可以参考以上值,根据服务器配置做相应调整。

4. 重启Apache

service httpd restart

优化后

查看内存占用情况:free -m

网站访问恢复正常,除了以上配置,还可以修改Apache的模块加载数,移除不必要的模块。

参考:Apache 占用内存并发优化设置

]]>
https://www.xuanfengge.com/wp-mysql-apache.html/feed 2
深度剖析:如何实现一个 Virtual DOM 算法 https://www.xuanfengge.com/vitual-dom-algorithm.html https://www.xuanfengge.com/vitual-dom-algorithm.html#comments Tue, 15 Aug 2017 13:09:31 +0000 https://www.xuanfengge.com/?p=6953 看到一篇关于Virtual DOM的优秀文章,现转载

1 前言

本文会在教你怎么用 300~400 行代码实现一个基本的 Virtual DOM 算法,并且尝试尽量把 Virtual DOM 的算法思路阐述清楚。希望在阅读本文后,能让你深入理解 Virtual DOM 算法,给你现有前端的编程提供一些新的思考。

本文所实现的完整代码存放在 Github

2 对前端应用状态管理的思考

假如现在你需要写一个像下面一样的表格的应用程序,这个表格可以根据不同的字段进行升序或者降序的展示。

这个应用程序看起来很简单,你可以想出好几种不同的方式来写。最容易想到的可能是,在你的 JavaScript 代码里面存储这样的数据:

var sortKey = "new" // 排序的字段,新增(new)、取消(cancel)、净关注(gain)、累积(cumulate)人数
var sortType = 1 // 升序还是逆序
var data = [{...}, {...}, {..}, ..] // 表格数据

用三个字段分别存储当前排序的字段、排序方向、还有表格数据;然后给表格头部加点击事件:当用户点击特定的字段的时候,根据上面几个字段存储的内容来对内容进行排序,然后用 JS 或者 jQuery 操作 DOM,更新页面的排序状态(表头的那几个箭头表示当前排序状态,也需要更新)和表格内容。

这样做会导致的后果就是,随着应用程序越来越复杂,需要在JS里面维护的字段也越来越多,需要监听事件和在事件回调用更新页面的DOM操作也越来越多,应用程序会变得非常难维护。后来人们使用了 MVC、MVP 的架构模式,希望能从代码组织方式来降低维护这种复杂应用程序的难度。但是 MVC 架构没办法减少你所维护的状态,也没有降低状态更新你需要对页面的更新操作(前端来说就是DOM操作),你需要操作的DOM还是需要操作,只是换了个地方。

既然状态改变了要操作相应的DOM元素,为什么不做一个东西可以让视图和状态进行绑定,状态变更了视图自动变更,就不用手动更新页面了。这就是后来人们想出了 MVVM 模式,只要在模版中声明视图组件是和什么状态进行绑定的,双向绑定引擎就会在状态更新的时候自动更新视图(关于MV*模式的内容,可以看这篇介绍)。

MVVM 可以很好的降低我们维护状态 -> 视图的复杂程度(大大减少代码中的视图更新逻辑)。但是这不是唯一的办法,还有一个非常直观的方法,可以大大降低视图更新的操作:一旦状态发生了变化,就用模版引擎重新渲染整个视图,然后用新的视图更换掉旧的视图。就像上面的表格,当用户点击的时候,还是在JS里面更新状态,但是页面更新就不用手动操作 DOM 了,直接把整个表格用模版引擎重新渲染一遍,然后设置一下innerHTML就完事了。

听到这样的做法,经验丰富的你一定第一时间意识这样的做法会导致很多的问题。最大的问题就是这样做会很慢,因为即使一个小小的状态变更都要重新构造整棵 DOM,性价比太低;而且这样做的话,inputtextarea的会失去原有的焦点。最后的结论会是:对于局部的小视图的更新,没有问题(Backbone就是这么干的);但是对于大型视图,如全局应用状态变更的时候,需要更新页面较多局部视图的时候,这样的做法不可取。

但是这里要明白和记住这种做法,因为后面你会发现,其实 Virtual DOM 就是这么做的,只是加了一些特别的步骤来避免了整棵 DOM 树变更

另外一点需要注意的就是,上面提供的几种方法,其实都在解决同一个问题:维护状态,更新视图。在一般的应用当中,如果能够很好方案来应对这个问题,那么就几乎降低了大部分复杂性。

3 Virtual DOM算法

DOM是很慢的。如果我们把一个简单的div元素的属性都打印出来,你会看到:

而这仅仅是第一层。真正的 DOM 元素非常庞大,这是因为标准就是这么设计的。而且操作它们的时候你要小心翼翼,轻微的触碰可能就会导致页面重排,这可是杀死性能的罪魁祸首。

相对于 DOM 对象,原生的 JavaScript 对象处理起来更快,而且更简单。DOM 树上的结构、属性信息我们都可以很容易地用 JavaScript 对象表示出来:

var element = {
  tagName: 'ul', // 节点标签名
  props: { // DOM的属性,用一个对象存储键值对
    id: 'list'
  },
  children: [ // 该节点的子节点
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
  ]
}

上面对应的HTML写法是:

<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>

既然原来 DOM 树的信息都可以用 JavaScript 对象来表示,反过来,你就可以根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树。

之前的章节所说的,状态变更->重新渲染整个视图的方式可以稍微修改一下:用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。当然这样做其实没什么卵用,因为真正的页面其实没有改变。

但是可以用新渲染的对象树去和旧的树进行对比,记录这两棵树差异。记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的时候确实只变更有不同的地方。

这就是所谓的 Virtual DOM 算法。包括几个步骤:

  1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
  2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  3. 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

4 算法实现

4.1 步骤一:用JS对象模拟DOM树

用 JavaScript 来表示一个 DOM 节点是很简单的事情,你只需要记录它的节点类型、属性,还有子节点:

element.js

function Element (tagName, props, children) {
  this.tagName = tagName
  this.props = props
  this.children = children
}

module.exports = function (tagName, props, children) {
  return new Element(tagName, props, children)
}

例如上面的 DOM 结构就可以简单的表示:

var el = require('./element')

var ul = el('ul', {id: 'list'}, [
  el('li', {class: 'item'}, ['Item 1']),
  el('li', {class: 'item'}, ['Item 2']),
  el('li', {class: 'item'}, ['Item 3'])
])

现在ul只是一个 JavaScript 对象表示的 DOM 结构,页面上并没有这个结构。我们可以根据这个ul构建真正的<ul>

Element.prototype.render = function () {
  var el = document.createElement(this.tagName) // 根据tagName构建
  var props = this.props

  for (var propName in props) { // 设置节点的DOM属性
    var propValue = props[propName]
    el.setAttribute(propName, propValue)
  }

  var children = this.children || []

  children.forEach(function (child) {
    var childEl = (child instanceof Element)
      ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
      : document.createTextNode(child) // 如果字符串,只构建文本节点
    el.appendChild(childEl)
  })

  return el
}

render方法会根据tagName构建一个真正的DOM节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。所以只需要:

var ulRoot = ul.render()
document.body.appendChild(ulRoot)

上面的ulRoot是真正的DOM节点,把它塞入文档中,这样body里面就有了真正的<ul>的DOM结构:

<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>

完整代码可见 element.js

4.2 步骤二:比较两棵虚拟DOM树的差异

正如你所预料的,比较两棵DOM树的差异是 Virtual DOM 算法最核心的部分,这也是所谓的 Virtual DOM 的 diff 算法。两个树的完全的 diff 算法是一个时间复杂度为 O(n^3) 的问题。但是在前端当中,你很少会跨越层级地移动DOM元素。所以 Virtual DOM 只会对同一个层级的元素进行对比:

上面的div只会和同一层级的div对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到 O(n)。

4.2.1 深度优先遍历,记录差异

在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:

在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。

// diff 函数,对比两棵树
function diff (oldTree, newTree) {
  var index = 0 // 当前节点的标志
  var patches = {} // 用来记录每个节点差异的对象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 对两棵树进行深度优先遍历
function dfsWalk (oldNode, newNode, index, patches) {
  // 对比oldNode和newNode的不同,记录下来
  patches[index] = [...]

  diffChildren(oldNode.children, newNode.children, index, patches)
}

// 遍历子节点
function diffChildren (oldChildren, newChildren, index, patches) {
  var leftNode = null
  var currentNodeIndex = index
  oldChildren.forEach(function (child, i) {
    var newChild = newChildren[i]
    currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识
      ? currentNodeIndex + leftNode.count + 1
      : currentNodeIndex + 1
    dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点
    leftNode = child
  })
}

例如,上面的div和新的div有差异,当前的标记是0,那么:

patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的不同

同理ppatches[1]ulpatches[3],类推。

4.2.2 差异类型

上面说的节点的差异指的是什么呢?对 DOM 操作可能会:

  1. 替换掉原来的节点,例如把上面的div换成了section
  2. 移动、删除、新增子节点,例如上面div的子节点,把pul顺序互换
  3. 修改了节点的属性
  4. 对于文本节点,文本内容可能会改变。例如修改上面的文本节点2内容为Virtual DOM 2

所以我们定义了几种差异类型:

var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

对于节点替换,很简单。判断新旧节点的tagName和是不是一样的,如果不一样的说明需要替换掉。如div换成section,就记录下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}]

如果给div新增了属性idcontainer,就记录下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}, {
  type: PROPS,
  props: {
    id: "container"
  }
}]

如果是文本节点,如上面的文本节点2,就记录下:

patches[2] = [{
  type: TEXT,
  content: "Virtual DOM2"
}]

那如果把我div的子节点重新排序呢?例如p, ul, div的顺序换成了div, p, ul。这个该怎么对比?如果按照同层级进行顺序对比的话,它们都会被替换掉。如pdivtagName不同,p会被div所替代。最终,三个节点都会被替换,这样DOM开销就非常大。而实际上是不需要替换节点,而只需要经过节点移动就可以达到,我们只需知道怎么进行移动。

这牵涉到两个列表的对比算法,需要另外起一个小节来讨论。

4.2.3 列表对比算法

假设现在可以英文字母唯一地标识每一个子节点:

旧的节点顺序:

a b c d e f g h i

现在对节点进行了删除、插入、移动的操作。新增j节点,删除e节点,移动h节点:

新的节点顺序:

a b c h d f g i j

现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合)。这个问题抽象出来其实是字符串的最小编辑距离问题(Edition Distance),最常见的解决算法是 Levenshtein Distance,通过动态规划求解,时间复杂度为 O(M * N)。但是我们并不需要真的达到最小的操作,我们只需要优化一些比较常见的移动情况,牺牲一定DOM操作,让算法时间复杂度达到线性的(O(max(M, N))。具体算法细节比较多,这里不累述,有兴趣可以参考代码

我们能够获取到某个父节点的子节点的操作,就可以记录下来:

patches[0] = [{
  type: REORDER,
  moves: [{remove or insert}, {remove or insert}, ...]
}]

但是要注意的是,因为tagName是可重复的,不能用这个来进行对比。所以需要给子节点加上唯一标识key,列表对比的时候,使用key进行对比,这样才能复用老的 DOM 树上的节点。

这样,我们就可以通过深度优先遍历两棵树,每层的节点进行对比,记录下每个节点的差异了。完整 diff 算法代码可见 diff.js

4.3 步骤三:把差异应用到真正的DOM树上

因为步骤一所构建的 JavaScript 对象树和render出来真正的DOM树的信息、结构是一样的。所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的patches对象中找出当前遍历的节点差异,然后进行 DOM 操作。

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  for (var i = 0; i < len; i++) { // 深度遍历子节点
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }

  if (currentPatches) {
    applyPatches(node, currentPatches) // 对当前节点进行DOM操作
  }
}

applyPatches,根据不同类型的差异对当前节点进行 DOM 操作:

function applyPatches (node, currentPatches) {
  currentPatches.forEach(function (currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        node.parentNode.replaceChild(currentPatch.node.render(), node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}

完整代码可见 patch.js

5 结语

Virtual DOM 算法主要是实现上面步骤的三个函数:elementdiffpatch。然后就可以实际的进行使用:

// 1. 构建虚拟DOM
var tree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: blue'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li')])
])

// 2. 通过虚拟DOM构建真正的DOM
var root = tree.render()
document.body.appendChild(root)

// 3. 生成新的虚拟DOM
var newTree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: red'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li'), el('li')])
])

// 4. 比较两棵虚拟DOM树的不同
var patches = diff(tree, newTree)

// 5. 在真正的DOM元素上应用变更
patch(root, patches)

当然这是非常粗糙的实践,实际中还需要处理事件监听等;生成虚拟 DOM 的时候也可以加入 JSX 语法。这些事情都做了的话,就可以构造一个简单的ReactJS了。

本文所实现的完整代码存放在 Github,仅供学习。

6 References

https://github.com/Matt-Esch/virtual-dom/blob/master/vtree/diff.js

 

作者:戴嘉华

原文https://github.com/livoras/blog/issues/13

]]>
https://www.xuanfengge.com/vitual-dom-algorithm.html/feed 2
Chrome Vue调试工具报错 rawgetter https://www.xuanfengge.com/chrome-vue-rawgetter.html https://www.xuanfengge.com/chrome-vue-rawgetter.html#comments Mon, 14 Aug 2017 11:51:21 +0000 https://www.xuanfengge.com/?p=6939 在开发环境使用Chrome 的Vue Devtools调试工具时,发生报错 TypeError: rawGetter is not a function

具体报错

Uncaught TypeError: rawGetter is not a function
    at wrappedGetter (https://xxx.com/js/modules/vue/vuex.js:634:12)
    at Vue$3.computed.(anonymous function) (https://xxx.com/js/modules/vue/vuex.js:437:42)
    at Watcher.get (https://xxx.com/js/modules/vue/vue.js:2752:25)
    at Watcher.evaluate (https://xxx.com/js/modules/vue/vue.js:2852:21)
    at Vue$3.computedGetter [as default] (https://xxx.com/js/modules/vue/vue.js:3104:17)
    at Object.get [as default] (https://xxx.com/js/modules/vue/vuex.js:439:43)
    at encode (chrome-extension://xxxx/build/backend.js:393:20)
    at encode (chrome-extension://xxxx/build/backend.js:397:21)
    at Object.11.exports.stringifyStrict (chrome-extension://xxxx/build/backend.js:469:3)
    at Object.stringify (chrome-extension://xxxx/build/backend.js:450:20)

而线上运行的代码没有问题,研究发现是这样的原因导致的

getters.js

// 由于全部代码写在modules里,这里暂时输出为空
export default {}

store.js

import * as getters from './getters'
const store = new Vuex.Store({
    getters,
    ...
})
export default store

运行时发现 getters取到的值是default

所以是粗心的把import代码写错了

改成直接import 即可(其实发现不少人碰到这个问题)

import getters from './getters'

如果getters.js的代码写法为

export function myGetter (state) { ... }
export function otherGetter (state) { ... }

则需要这么引入

import * as getters from './getters'

]]>
https://www.xuanfengge.com/chrome-vue-rawgetter.html/feed 1