iischajn.github.com's People
iischajn.github.com's Issues
web开发各角色良好约定
产品约定
- 产品内部保证信息对称,统一意见,不要做重复性比较强的事情。
- 做需求的思路尽量是具有良好逻辑性的,考虑周到的,简洁清晰严谨的,进行过用户心理调查的和符合用户价值观的。
- 遵循小步快跑,快速迭代的原则,不要大而全,而是小而精,力求易读易懂和简单快捷的功能操作。
- 对于增强性质的功能和效果在低级浏览器中可以接受功能降级。
- 在目前每周优化需求的基础之上增加每天的优化需求统计,在固定时间点交给技术负责人,再分发到相应开发人员并安排时间去进行跟踪。
- 关于统计数据上的疑问和一般需求尽量先飞信通知或放入每天的优化需求中,以防止技术方面的开发中断。
- 项目的开启和跟进要有序,做商业专题和节日专题或皮肤尽量提前做好安排,以防止项目进度紧张。
- 增强对文案方面的掌握。
设计约定
- 保持整站风格统一,尽量复用现有的样式设计,比如浮层、按钮、 tips、页面结构。
- PSD文件内的图层关系和命名应该是清晰的,有组织的,易提取的。
- 不要使用不常见的字体,主要字体使用雅黑或宋体来完成。
- 尽量减少像素级别的调整。
- 发设计图时请以 PSD文件为主要文件。
- 尽量做符合 web页面标准的UI 设计与优雅风格设计。
前端约定
- 线上 BUG快速反应,快速明确原因和解决方案。
- 参与到前期的需求会和设计确认会的讨论中,以保证产品价值和项目的敏捷度,并尽量减少实现成本。
- 在讨论与开发中要关注页面性能,关注组件级的开发与复用,关注 UI风格的统一与浏览器兼容性等问题。
- 多自动化,工具化方面的尝试,进行 web方向新技术的研究。
- 增强开发文档和注释。
后端约定
- 在进行开发之前,先过一下页面、接口、变量等需要交互的参数命名,力求易读易懂和保持一致性。
- 项目技术负责人需要关注整个流程和项目情况,加强开发进度的把控和与产品、测试之间的沟通。
- 对目前存在的参数凌乱问题进行自查和整合,多与前端进行性能优化与代码整理。
测试约定
- 不要提重复性比较强的 BUG。
- 在设计图经过需求会确认之后,不要发页面设计相关的 BUG。
- 尽量少提感官不适的 BUG,要以测试页面功能为主,感受 BUG可以提前在需求会和过设计稿时候和产品确认。
- 控制好测试范围,与本项目关联不大且不影响本项目上线功能的线上 BUG,可通知需求接口人让其放入优化需求排期中。
- 要分清 BUG的种类,是产品策略问题、前端页面问题还是后端数据问题。比如,文案不符合语法提给产品,页面脚本错误和不符合需求提给前端,页面数据有问题提给后端。
- 如果出现无法复现或只有在较少数情况下才会出现的 BUG,先检查一下环境是否有缓存,然后先和相关人员确认下。
- 浏览器兼容性问题按照优先级进行修改,并要接受低级浏览器的功能降级和展现不一致,如 IE6透明问题,IE 下文本框提示问题,圆角问题和渐变问题等。
- 第一版上线不要过多在意细节问题,细节问题可以通过线上反馈和快速迭代来完成。
第一印象的力量
在阿里云 CentOS 服务器(ECS)上搭建 nginx + mysql + php-fpm 环境
#1、添加具有管理员权限的用户
useradd chajn
sudo su chajn
passwd chajn
chmod 640 /etc/sudoers
修改 /etc/sudoers 文件,找到下面一行,在root下面添加一行,如下所示:
## Allow root to run any commands anywhere
root ALL=(ALL) ALL
chajn ALL=(ALL) ALL
然后恢复 /etc/sudoers 的权限
chmod 440 /etc/sudoers
修改完毕,现在可以用chajn帐号登录,然后用命令su,即可获得root权限进行操作。
#2、安装nginx
yum install nginx
service nginx restart
nginx配置:/etc/nginx/conf.d
注:配置thinkphp需要在conf改写脚本,下面代码供以参考:
location / {
if (!-e $request_filename) {
rewrite ^/(.*)$ /index.php/$1 last;
break;
}
}
location ~ \.php {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi.conf;
set $real_script_name $fastcgi_script_name;
if ($fastcgi_script_name ~ "^(.+?\.php)(/.+)$") {
set $real_script_name $1;
set $path_info $2;
}
fastcgi_param SCRIPT_FILENAME $document_root$real_script_name;
fastcgi_param SCRIPT_NAME $real_script_name;
fastcgi_param PATH_INFO $path_info;
}
#3、安装最新版本的mysql
通过 网址 http://dev.mysql.com/downloads/repo/ 下载最新的rpm包,假设下载的是mysql-community-release-el6-5.noarch.rpm
可通过scp上传到服务器目录中,在当前目录中执行
yum localinstall -y mysql-community-release-el6-5.noarch.rpm
即可得到最新版本的mysql源,继续下载,
yum install mysql
yum install mysql-server
yum install mysql-devel
service mysqld restart
mysql启动可能失败
MySQL Daemon failed to start.
Starting mysqld: [FAILED]
vim /var/log/mysqld.log 去查看日志,如找到一行
[ERROR] InnoDB: The Auto-extending innodb_system data file './ibdata1' is of a different size 640 pages (rounded down to MB) than specified in the .cnf file: initial 768 pages, max 0 (relevant if non-zero) pages!
可以 cd /var/lib/mysql 将下面文件全删除
然后 mysqld --initialize 重新初始化mysql,并给予读写权限 chmod -R 777 ./
不知道mysql的默认密码(5.3版本后
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)
据说在 第一次启动后会有 /root/.mysql_secret 出现得到随机密码,笔者没找到,只好进行如下操作:
mysqld_safe --skip-grant-tables &
mysql -uroot -p
update user set authentication_string=password('') where user='root';
flush privileges;
quit;
重启mysql,密码为空了
#4、安装PHP
yum install php
yum install php-fpm
service php-fpm restart
php站点必要拓展:
yum install php-pecl-apc php-mysql php-gd php-mcrypt php-pear php-mbstring php-xmlrpc php-dom
service php-fpm restart
提示错误,找不到 libmysqlclient.so.16
service php-fpm restart 时如出现:
[24-May-2016 20:23:02] NOTICE: PHP message: PHP Warning: PHP Startup: Unable to load dynamic library '/usr/lib64/php/modules/mysql.so' - libmysqlclient.so.16: cannot open shared object file: No such file or directory in Unknown on line 0
[24-May-2016 20:23:02] NOTICE: PHP message: PHP Warning: PHP Startup: Unable to load dynamic library '/usr/lib64/php/modules/mysqli.so' - libmysqlclient.so.16: cannot open shared object file: No such file or directory in Unknown on line 0
[24-May-2016 20:23:02] NOTICE: PHP message: PHP Warning: PHP Startup: Unable to load dynamic library '/usr/lib64/php/modules/pdo_mysql.so' - libmysqlclient.so.16: cannot open shared object file: No such file or directory in Unknown on line 0
会导致php连不上数据库
它的意思是找不到libmysqlclient.so.16,但这个文件就在/etc/lib64/mysql 下面,创建软链到lib64下即可:
ln -s /usr/lib64/mysql/libmysqlclient.so.16 /usr/lib64/libmysqlclient.so.16
开机自启动
chkconfig nginx on
chkconfig mysqld on
chkconfig php-fpm on
常用服务
service sshd start
service mysqld restart
service php-fpm restart
常用目录
/var/lib/mysql
/var/log/nginx
/usr/lib64/mysql
常用命令:
ssh root@ip ssh登陆服务器
zip -r dir.zip ./dir/ 压缩
unzip dir.zip 解压
scp 本地文件地址 root@ip:远程目录地址
ldd /usr/lib64/php/modules/mysql.so
rpm -ql nginx
init 6 重启linux
增加ssh时间
vi /etc/ssh/sshd_config 添加以下两行:
ClientAliveInterval 300
ServerAliveMaxCount 2
Web开发流程参考
第一阶段 需求讨论阶段:
- 产品进行用户调研,参考各方意见,整理需求,进行需求文档的书写
- 产品召开需求讨论会
注:讨论会相关参与人员应为设计、前端、后端和测试等,主要是对需求和页面功能的确定和讨论
第二阶段 需求确认阶段:
- 设计出设计图
- 产品召开需求确认会
注:确认会相关参与人员应为设计、前端、后端和测试等,主要是对设计图的确认和修正
第三阶段 开发阶段:
- 前端切页面 -> 前端写页面功能 -> 联调 -> 提测
- 后端写 app层功能 -> 后端写web层接口 -> 联调 -> 提测
注:两者应在良好约定接口规则后各自进行开发
第四阶段 测试阶段:
- 测试进行冒烟测试 -> 主要是针对页面功能的基本测试,看流程是否能跑通
- 前后端进行冒烟 bug修改
- 测试进行第一轮测试 -> 主要是参照需求书和设计页面进行整体测试,请参考测试约定进行 BUG的提交。
- 前后端进行第一轮 bug修改 -> 修改完成后再提交给测试进行第二轮测试
- 测试进行第一轮 bug回归和第二轮测试 -> 回归第一轮的bug,再重新进行第二轮测试。
- 前后端进行第二轮 bug修改 -> 修改完成后将所有 bug置状态。
- 测试进行第二轮 bug回归 -> 剩下来还没有解决的 bug与产品和技术确认,产品再按照优先级进行排期和解决 ->
- 经过产品确认后即可开始准备上线,发上线申请确认邮件
注:开发要保证测试环境的稳定。在测试未完成一轮测试之前,不应该提交修复后的代码到测试环境中去,如果有精力的话可以进行现场式的 codereview和checklist
第五阶段 上线阶段:
- 前后端准备好回滚方案,在线上访问数较少的时间段进行上线
- 产品和测试线上测试 -> 上线完成
注:应提前做好用户行为统计和用户反馈的良好预案
react-native 系列(一):快速入门指南
勇者啊,让我们一起开启新世纪的大门吧!(这么说会不会有点中二www
买装备,做准备
- OS X - 因为android我们还搞不定的原因,所以目前的版本暂时只支持iOS实现,并且只有 Mac 上的 Xcode 才可以运行噢,没有 Mac 的24k纯屌们就关掉本教程继续好好做你们的前端去吧,看了也是心里的痛!
- Xcode 得是最新版本噢,还没有的去MacAppStore里赶紧下一个。
- Homebrew 可以傻瓜安装node,watchman和flow。(虽然后两者与本例没太大关系,咳咳,广告还是要有的,一个是监控文件改动的,一个是检查JS静态类型的
- 执行
brew install node
。node 或 npm 都要求最新噢。 - 执行
brew install watchman
。 虽然没啥用,但求各位大大还是跟着下个 watchman 吧,这样在感觉上能够减少bug出现的几率,嗯嗯…… - 执行
brew install flow
。 虽然我们也不知道flow能用来干嘛,装上吧……
PS:途中可能遇到各种BUG,什么连不上国外的服务器啊,ruby版本错误啊(都请自行doodle之
快速开始撸一发
npm install -g react-native-cli
react-native init AwesomeProject
各种完成之后,我们进入新创建的文件夹 AwesomeProject/
中
- 用 Xcode 打开
AwesomeProject.xcodeproj
这个文件,并且狠狠的点击运行(run)按钮,会跳出iOS模拟器,此时ipa已经在正常运行状态中了! - 用编辑器打开
index.ios.js
这个文件,然后运用你多年的搬砖技术去coding一些东西吧,让文件产生变化 - 然后在你的iOS模拟器上按 cmd+R 两次!没错,颤抖吧!你的改动已经生效了!
Congratulations!你已经成功运行并修改了你的第一个 React Native App 。下一节我们将解放你的双手,让你实实在在的感受到深入浅出RNA的快感。(译者抠鼻孔中
mac下抓取分析iphone数据包 - 使用Wireshark
mac系统版本:mac 10.10 Yosemite
xcode版本:6.3.1
在追踪bug或者分析借鉴其他公司的app通讯思路时,抓包这件事是非常有必要的。下面说说Wireshark怎么截获iphone的数据包。
安装wireshark
wireshark是依赖x11的,所以首先确认安装了x11,mac自带,可以打开升级一下。
前往-实用工具-x11,打开后点击菜单栏上的x11,检查更新 即可。中间提取包内容过程比较长,耐心等待。
下载Wireshark最新版,尽量去官网下载:
https://www.wireshark.org/download.html (需要翻..)
安装,安装过程很简单,一路下一步。
我这里下载的Wireshark 1.12.4 Intel 64,安装后无法运行,网上说x11位置不对。控制台执行:
sudo ln -s /opt/X11 /usr/X11
问题依旧。
没办法下载了一个XQuartz-2.7.7:
http://xquartz.macosforge.org/landing/
安装,运行Wireshark。点完wireshark图标等了10多分钟,终于算是打开了,之后再打开就不需要等了。
抓iphone数据
想抓iphone的数据,首先需要让iphone数据通过mac才行。看到网上很多设代理什么的方法,比较复杂,有的还要越狱。其实没必要。只要链上数据线,然后在mac的终端执行:
rvictl -s iphone设备id
这时,所有iphone网络流量都会经过iphone所链接的mac,并且iphone数据还是走自己的网络,比如iphone链接在3g网络,数据还是会通过3G收发,而不是通过mac的网络。断开连接:
rvictl -x iphone设备id
设备连接后,mac会出现一个对应的虚拟网络接口,名字是rvi0(如果有多个iphone则累加,rvi1,rvi2…)
只要启动Wireshark,监听rvi接口就能抓到iPhone数据了,当然,也可以使用Wireshark以外的工具抓取或分析。
关于获取iphone设备ID,可以使用xcode-windows-devices,选择相应设备,右面设备信息的identifier里就是了
iischajn.github.com
一级域名切换了, '.io',项目里的现在跳转一级域名是'.com'。直接404了
MySQL for Mac 5.7.x 版本修改root密码
先到mysql官网上下载dmg格式安装包,进行安装。
安装完后会自动谈提示框告诉你一个随机密码,最好记住,方便修改。
命别名:
vim ~/.bash_profile
alias mysql=/usr/local/mysql/bin/mysql
alias mysqladmin=/usr/local/mysql/bin/mysqladmin
修改MySQL密码(版本5.7.x)
一、已有初始随机密码
mysql -u root -p
mysql>
mysql> show databases;
ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.
mysql>
mysql> set password for root@localhost=password(‘123');
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> show databases;
+——————–+
| Database |
+——————–+
| information_schema |
| mysql |
| performance_schema |
| sys |
+——————–+
4 rows in set (0.01 sec)
mysql>
二、通过安全模式重置密码
通过 –skip-grant-tables的方式启动mysqld_safe ,这个模式可以绕过mysql授权。
sudo /usr/local/mysql/bin/mysqld_safe --skip-grant-tables
之后进入mysql:
sudo /usr/local/mysql/bin/mysql -u root
在mysql.user中以前版本会有一个字段password,但是现在替换成了authentication_string
进入mysql之后:
mysql> update mysql.user set authentication_string=PASSWORD('123') where user=’root’;
Query OK, 1 row affected, 1 warning (0.04 sec)
Rows matched: 1 Changed: 1 Warnings: 1
mysql> flush privileges;
Query OK, 0 rows affected (0.02 sec)
mysql> quit
这时候Myqsl密码已经修改完了,我们把上面的mysqld进程干掉,通过正常途径起Mysqld服务
解读ECMAScript 6箭头函数
原文地址:http://www.nczonline.net/blog/2013/09/10/understanding-ecmascript-6-arrow-functions/
箭头函数是ECMAScript 6最受关注的更新内容之一。它引入了一种用「箭头」(=>)来定义函数的新语法,它…它碉堡了~。箭头函数与传统的JavaScript函数主要区别在于以下几点:
- 对 this 的关联。函数内置 this 的值,取决于箭头函数在哪儿定义,而非箭头函数执行的上下文环境。
- new 不可用。箭头函数不能使用 new 关键字来实例化对象,不然会报错。
- this 不可变。函数内置 this 不可变,在函数体内整个执行环境中为常量。
- 没有arguments对象。更不能通过arguments对象访问传入参数。只能使用显式命名或其他ES6新特性来完成。
这些差异的存在是有理可循的。首先,对this的绑定是JavaScript错误的常见来源之一。容易丢失函数内置数值,或得出意外结果。其次,将箭头函数限制为使用固定this引用,有利于JavaScript引擎优化处理。
语法
箭头函数的语法很简单,定义自变量,然后是箭头和函数主体。自变量和主题因使用不同可以采用更简洁的格式。下面这个例子就是采用传一个参数和返回一个值的箭头函数。
var reflect = value => value;
// 等同于:
var reflect = function(value) {
return value;
};
可以看出,传一个参数就直接写就好了,不用加小括号。箭头指向函数主体,不过函数主体也只是简单的一条返回语句,所以也不用加大括号。函数构造完毕赋给reflect加以引用。若需传入多个参数,则应加上小括号。例如:
var sum = (num1, num2) => num1 + num2;
// 等同于:
var sum = function(num1, num2) {
return num1 + num2;
};
sum()方法为两参数相加并回传结果。跟前一个例子的唯一区别是传入了两个参数,所以要用小括号括起来。它与传统函数一样,括号内由逗号将传入参数分开。同样,如果该函数不需要传入参数,那也要以空括号来代替。
var sum = () => 1 + 2;
// 等同于:
var sum = function() {
return 1 + 2;
};
若你想使用标准的函数体,或者函数体内可能有更多的语句要执行,则要用大括号将函数体括起来,并明确定义返回值。例如:
var sum = (num1, num2) => { return num1 + num2; }
//等同于:
var sum = function(num1, num2) {
return num1 + num2;
};
大括号内的部分基本等同于传统函数,除了arguments参数不可用外。因为大括号是函数主体的标志。而箭头函数若要返回自定义对象的话,就必须用小括号把该对象括起来先。例如:
var getTempItem = id = > ({
id: id,
name: "Temp"
});
// 等同于:
var getTempItem = function(id) {
return {
id: id,
name: "Temp"
};
};
上例可以看出,用小括号包含大括号则是对象的定义,而非函数主体。
使用
JavaScript最常见错误之一就是函数内部this关联。因为this是根据函数当前的执行环境去取值的,这样就会在调用时产生误解,以导致对其他的不相关对象产生了影响。参见下例:
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", function(event) {
this.doSomething(event.type); // error
}, false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
在这段代码中,本意是想让PageHandler的init()方法用于构建交互作用,并在点击事件处理函数中调用this.doSomething()。但是代码并未按设计初衷来执行,运行时this指向了全局对象而不是PageHandler,从而造成this.doSomething()调用无效出现报错,因为全局对象中不存在doSomething方法。当然,可以在函数中使用bind()将this与PageHandler明确关联起来,见下:
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", (function(event) {
this.doSomething(event.type);
}).bind(this), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
虽然看着有些怪,但现在代码执行是符合预期的。通过调用函数的bind(this),又创建了一个已关联现有this的新函数返回,就是说为了达到目的额外又包了一层。因为箭头函数已经支持this关联,所以这里用箭头函数会更爽快些,看下例:
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click",
event = > this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
这个实例中的事件处理函数调用了this.doSomething()的箭头函数。this的取值即为init()内的this值。故而它等效于bind()。箭头函数简明扼要的特性,也使它成为其他函数自变量的理想选择。例如,若要在ES5上,使用定制比较器来排列数组,典型的代码见下:
var result = values.sort(function(a, b) {
return a - b;
});
上例用了许多语法来实现一个简单的操作。若使用箭头函数,则可写成很精炼的代码:
var result = values.sort((a, b) => a - b);
数组的sort/map/reduce等方法都支持回调函数。用箭头函数可以简化书写流程,解放你的双手去做你想做的事情。
补充
箭头函数的确与传统函数有不同之处,但仍存在共同的特点。例如:
- 对箭头函数进行typeof操作会返回“function”。
- 箭头函数仍是Function的实例,故而instanceof的执行方式与传统函数一致。
- call/apply/bind方法仍适用于箭头函数,但就算调用这些方法扩充当前作用域,this也依旧不会变化。
箭头函数与传统函数最大的不同之处在,禁用new操作。
结论
箭头函数是ECMAScript 6一个备受关注的新特性,并且还在不断的优化着。用简短语法以定义函数或语句书写流程是大势所趋,他们必将屌炸天,无人可阻。它对关键字this的关联让开发者不再神烦,并通过JavaScript引擎优化,帮助其提高表现。说到这里,小伙伴们的大刀已经饥渴难耐了吧,若想试用箭头函数,打开最新版本的Firefox即可。【关于箭头函数的更多详情可以参考:尝试 ES6 中的箭头函数】
CentOS下安装nginx并且升级nginx到最新版
自动安装选择最快的源
[root@localhost ~]# yum install yum-fastestmirror
安装nginx
[root@localhost ~]# yum install nginx
平滑升级,首先配置nginx 源
[root@localhost ~]# vim /etc/yum.repos.d/nginx.repo
内容如下:
#nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/6/$basearch/
gpgcheck=0
enabled=1
然后不断的执行 update 就可以了
[root@localhost ~]# yum update nginx
四种感知优化技巧,让你的手机站点高端起来
编者语:本文约有4500字。内容涵盖于面向手机的网站在感知性能方面的几个点,以及加快访问速度的可执行方案。不想读太长文章的小朋友们注意了,本文可不是说如何给糟糕网站插上翅膀飞起来噢,它是琢磨如何让用户的感知体验趋于自然的。
技术已日渐成熟,无论你采用响应式还是适应式什么的,只要你有明确的生产理念,在移动设备上制作个美观的WEB站点并不困难。但你的客户从不会满足,他们那被权利与欲望所腐蚀的脑子里已经容纳不下一点美感,以至于会无止尽的要求你,他想要获得更好更流畅的app式体验。而以现在人类范畴内的技术水平来说,嘛,都能这样想了还做什么产品,去写科幻小说啊。(笑)
不过细想起来,其实,当用户觉得一个站点“像app”或是“原生代码开发的”的时候,大多感受到的不仅是外观而已,更多的是操作界面的响应处理和执行方式。相比之下,native app毕竟是亲爹养的,它运行速度快,动画播放流畅,按键反应及时,不受加载影响等,光从执行性能上就够甩webview几条街的了。
“native app式体验”的意思就是,丧心病狂的提高你站点的运行速度。
近来,一坨又一坨臭烘烘硬梆梆的web app井喷而出,这不能不令我等深思“WEB世界的大和谐之计划”的可行性,也要深思自身认知的狭隘与菜B。故而,“提升性能”理当并已经受到了人们的密切关注。Facebook那些高富帅们也说,以现有的web app环境,他们很难获得预期的速度与互动效果。为此,他们日后将转向着手发展手机端的native app。无论Facebook搞什么飞机,但在这里我想告诉大家的是,想架设个高性能网站并非天方夜谭。虽然的确存在一些硬性阻碍,但是只要多付出一点努力,还是可以实现的。
感知性能VS 实际性能
尽管提高实际性能很重要,但除非用户能确实体验到了性能的提升,否则这对于他们而言并没有什么意义。今年在西雅图的某个设计者大会,LukeWroblewski讲到他的手机应用Polar时,说到他的团队努力的改进了新版投票的加载速度,然后还人性化的加了等待效果展示(翻滚吧Loading),结果却得到了速度变慢的用户反馈。高程们很不理解,用户咋那么难伺候呢,然后又发了一个把等待效果给下掉的补丁,好吧,终于有人说so quickly!
这说明啥,这说明了感知性能的重要性。明目张胆的用Loading展示告知客户“你在等待”的事实,简直就是作死。这转移不了什么注意力,也不是什么优化设计,它只在传达一个信息“我很慢,菊花给你看”,啥时候真把用户旋出翔来你的应用就算成功了吧。总之,如果用户感受不到,你站点再快也都无济于事。
作为设计者与开发者,我们的目标不仅仅是提升数学意义上的速度,也应创造对于体验和感知上的冲击力。
出于用户的角度,我认为感知性能比实际性能要重要的多,当然这并不意味着没必要改进实际性能了,两者是相辅相成的。到此,我想大家已经对两种性能有所知晓。那么,如何才能提升感知性能呢?下述有4种技术可以拯救你:
1. 为按键添加touch状态
最简单的一种方法就是增加触摸行为状态展示。嘛,通过测试你可以得出,每当用户点击网站上的按钮时,总要等个300ms后才会有所响应。这是浏览器所预设的时间空隙,以确保用户没有点错(怕用户是双击事件吧)。所以需要等待三分之一秒来确认用户是否有其他操作。之后浏览器将对最初的点击做出响应。响应方式是高亮并灰化,然后再跳转。
这个体验其实相当糟糕。Nielsen组织研究显示,超过100ms以上的响应,用户就会察觉到自己在等待,而他此刻想做的就是赶紧离开这个鬼地方。然而,包括我的一些网站在内,大部分手机网站都不会在意感知性能的问题。而设计者通常会觉得链接或按钮就是这样的(也可能是他们从不使用自己设计的页面的原因)。
想提高你站点的精神层次,就要先让按钮对用户的点击直接做出回应!
:active这个高端洋气的CSS伪类状态在WEB页面开发中已经很常用了。但可惜的是,iOS和Android都没有在手机端实现这个状态。不过我们总是有曲线救国的办法的不是么,找到替代的解决方案并不难,只要在JavaScript中增加下述事件就可以监听到:active状态啦。
document.addEventListener("touchstart",function(){}, true)
而后,您可能会想用CSS来添加个按钮被按下的效果,且把该死的触摸高亮给去掉。
-webkit-tap-highlight-color:rgba(0,0,0,0);
以上尽管没有改变实际的运行速度,但它给用户操作以直接反馈,而非300ms的空虚等待,感知性能get√。
我们还可以再进一步,达到实际意义上的速度提升。
调用fasttap或者fastclick,可以彻底清除300ms的延迟,这会让感知性能更好提升,达到丧心病狂的地步。欲知fasttap详情,请Google之。也可以直接去checkout Github资源。
2. 使用冲量滚动
你是否有过在手机站点上下拉滚动条的时候,感觉有很卡或很慢的情况啊。Android3+和iOS5+支持CSS的新属性为overflow-scrolling,可实现冲量滚动,然后一切都变得美好起来……
无冲量滚动和有冲量滚动
这样的动态效果一出来,马上就有too native的感觉啦。只要在滚动条上增加下述属性即可实现。
-webkit-overflow-scrolling:touch;
不过这个属性在iPhone下有个问题。在iPhone下点其顶端的状态条是可以返回到页面顶端的,但冲量滚动会造成这一功能的失效。这个BUG已存在一段时间了,但直到iOS7 的Mobile Safari最新版才修复。针对它也有个不太靠谱的解决办法,就创建一个overflow-scrolling:touch的class,当点出容器时用JavaScript去加class到容器上。
Android4中对滚动条都有冲量滚动效果,所以完全不需要考虑这个属性。
对于低版本的Android系统,会有一些备选方法。我倾向于使用Modernizer来检测系统对冲量滚动的支持,然后修改布局使容器溢出可见(我也不知道这是要说啥)。如果不想选上个方法,有一些JavaScript库也可供选择。FilamentGroup的Overthrow和iScroll什么的。
#3. 创建高性能动画
web站点与app最显著的区别之一就是对动画的运用。最近几年,app利用设备自身的硬件加速功能能做出很流畅的动画。但在web上只能通过JavaScript去通过计算去做动画,这样的动画效果若在CPU性能较差的手机上就会运行很慢啊。别慌,时代在发展,科技在进步,不过随着手机浏览器的不断支持,我们可以使用支持硬件加速的CSS3动画去完成很多炫酷屌渣天的效果。
这种强化视觉效果的方法超洋气。许多广受欢迎的app都以此为傲。
这还不够,想与native匹敌,必须要确保动画的流畅和稳定,而这一点往往很难做到。SteamlockSoftware的Allen Pike在2011年写了一篇非常好的文章。主要讲的是他们在开发nativeapp时的一些注意事项。想提供给用户一些喜闻乐见的动画效果,但性能不好的动画是会严重影响app体验的哦。这些注意事项同样适用于native-feeling的web动画效果,都是金科玉律呀,要好好膜拜下。文章中,他提出了“感知时间轴(timeline of perception)”这个概念。
**1.动画帧率应为60fps。**这意味着每帧~16ms走完。这是达到native流畅的最小值。iOS的内置动画都是60fps,所以iPhone的滚动效果比Androidu要好很多(尽管近来Google针对这个问题已有很大改进)。努力让所有与用户相关的动画都追上这个帧率吧。
**2.100ms内进行响应。**100ms是人对慢的感知阈限,在这时间段内所发生的事,用户都会觉得是即时的。
**3.若一定要超过100ms才能做出响应,那也必须控制在1000ms内。**Allen建议用时超过这一时间的,可以先给进度条什么的,以告知用户等待过程中并非什么都没发生。但是上面Polar的案例告诉我们,将用户的注意力集中在等待上,反而会弄巧成拙。之后我将再讲怎么处理这种情况。
4. 超过1s就会有不好的事发生了……
对以上有所了解后,你可能会觉得有些受不鸟,搞个动画居然也要关心这么多事,从而出现了“不干了回家种地去吧!”这样的想法。
少年且慢!其实你完全不用为此担心,有很多资源可以帮助我们解决这个问题的。
首当其冲就是 Effeckt.css。他们的目标就是建立一个以60fps为基准的过渡动画库,尽管还没有全部完成,但成果已经非常明显了。我强烈推荐这个玩意,你可以根据自己的项目对其进行微调后使用。另一个强大的家伙是Topcoat。它是Adobe的web团队建立的一个追求性能的CSS组件库,里面提供了许多超顺畅的组件呦,且每个都做了评效,可以在这里查到它们具体的性能情况。这俩最近好像已经有合作的样子,感兴趣的朋友都试用下吧。
现在,进一步谈之前提到的问题:如何干掉香气四溢的小菊花。
当等待时间介于100ms与250ms之间,显示菊花是弊大于利的,我倾向于在超过250ms后再显示它。再比方说用Ajax获取数据时,把外层容器收缩然后再展开显示新内容,这一小段动画总比让用户傻乎乎的盯着菊花看要好的多啊,他们甚至不会意识到新内容是在动画后才载入的。当然,长时间地重复动画也会令人厌烦。所以请慎重使用。对于大部分动画的运用,也同样需要慎重。
#4. 利用自然手势
与web相比,app有一个优势,即能在点触设备上利用人们的自然手势去响应。Mailbox或Clear等一些优秀的app都会尽量支持手势行为,可以直接触摸到屏幕上的对象进行二阶操作是件很爽的体验。然而,大部分网站仅支持点击对象,而不再利用其他手势,很低端的感觉啊有木有?这种用户体验是很枯燥和乏味的。
所以别傻看着了,快把你的网站也改进成支持手势操作吧!
唔,还有一个小问题:手机操作系统总是会恶劣地绑架浏览器上的手势。比方说Facebook这样的app,使用边缘滑动会显示导航栏。但是在Web里Chrome就切换标签页了,而iOS7的Mobile Safari里则根据历史记录来前进后退当前页面……好吧,既然手势有诸多限制,那么哪些是可以使用的呢?下文列述了四个:
1.左右划动:左右划动仍是非常强大的一个手势,只须注意别太靠近屏幕边缘即可。它最适合用来移动照片浏览啊标签页啊什么的。
2.下拉更新:下拉更新是另一个使用频繁的手势,通过许多JavaScript库都能轻松监听到,我之前使用的是Hook.js。
3.长按:-webkit-touch-callout: none属性能有效禁止Mobile Safari中的长按效果。但如果换成Android,那就需要多做一点工作了。长按手势可用于提取图标(比如重排图标位置、顺序)与显示更多选项(例如群分享等)。
4.缩放:大部分人在网络上看到一张图,都会试着缩放扭动的手势来看图的细节。这里有浏览器绑架手势的另一个例子,但情节并没有那么严重,就不告诉你是啥,就不告诉你是啥。要实现多点触控手势,可以试试精悍的Hammer.js。它包含了很多手势操作,也支持自定义。用手机体验一下imgur.com的缩放或滑动手势吧少年,awesome~。
不过请记得,要用手势的话要先保证它是自然的可理解的,要用户接受的,再行采用。
结论
终有一天,native和web将毫无差别,虽然这是件很理想化的构思。但我们每付出一分努力,让网站看起来更贴近用户,那么,理想实现的那一天就会越快到来,么么哒!
性能问题越来越受关注的趋势非常好,但是请谨记,我们的用户并非机器。
他们不在乎你的网站具体发出了多少请求,不在乎屏幕究竟重画了多少次。他们在乎的是对这些性能的印象、感知。
如果用户无法体验到快,那么速度的提升根本就没意义。
如果你有更多关于发展感知性能的想法,请别留言,直接再写一篇吧~
用 Mutation Observers 实现DOM的Undo/Redo功能
原文地址:Detect, Undo And Redo DOM Changes With Mutation Observers
文中所出专有名词:
MO == Mutation Observers
ME == Mutation Events
MR == MutationRecord
这篇文章里,我会向大家解释为啥Mutation Observers(以下简称MO)这么牛逼,MO是那个发育不良的DOM Mutation Events(死慢死慢又很吃性能的家伙,以下简称ME)的替代品,它可以监听复数级的DOM的改变。可目前支持它的浏览器并不多,从ChromeStatus.com的数据来看,只有0.03%的业务使用量。所以为啥不再给她多点关注呢,好可怜的说。StackOverflow关于MO的提问大多数来自于Chrome拓展和富文本编辑器,不过我觉得还可以用在更多地方,比如,DOM修改的Undo/Redo是吧,先看下下面这个例子吧:
有木有感觉吊炸天(作者自我感觉真良好),下面我快速说下它是个什么玩意~
对比 Mutation Events 和 Mutation Observers
MO对DOM的监听方式不同于旧的ME,它是等该DOM的所有变更行为都结束之后才异步触发回调的,使得我们可以更专注的对DOM进行处理。这就好比一天撸一次和一周撸一次的区别一样,你说哪个对身体好啊!【哎?作者了什么奇怪的事情~( ⊙ o ⊙ )
如何愉悦的使用 Mutation Observers
大家都很忙(都很懒),我就提供一下基础代码片段,看下它是怎么监听DOM改变的就好了:
// 实例化一个监听,传入回调
var observer = new MutationObserver(mutationCallback);
// 监听一个DOM节点的改变(可以通过config设定更具体的设置,如:监听子孙节点、属性、内容数据等)
observer.observe(node, config);
// 停止监听,字面意思
observer.disconnect();
// 清除已记录的变动队列,和reset差不多啦
observer.takeRecords();
如果你像我一样屌丝整天构造对象实例化对象指向对象就是没对象的,那你肯定有大把的时间来做代码实践。下面是两个更好的例子,通过 contenteditable 与 el.innerHTML 去改变DOM结构,你可以清晰的看到MO是怎么异步处理和输出的啦。
http://jsbin.com/bajuqi/1/edit
我们可以监听到子节点的添加删除、属性和文本内容与数据的改变。再次提醒大家,MO 不是对每个改变都触发回调的,它是在DOM在进行了一组变化结束以后在堆栈空闲时才触发的,就像个记录了很多版本更新后而形成的发布日志一样。
让我们进一步分解代码,实例化的MO需要传入一个回调函数,当触发回调时它会传入一个 MutationRecord(以下简称MR) 对象的数组:
var observer = new MutationObserver(function(mutations) {
// 遍历一下mutations对象
mutations.forEach(function(mutation) {
// 用不用所有的变化记录自然取决于你
var entry = {
mutation: mutation,
el: mutation.target,
value: mutation.target.textContent,
oldValue: mutation.oldValue
};
console.log('Recording mutation:', entry);
});
});
上面的例子在: http://jsbin.com/cogid/1/edit
我们通过回调得到了一个数组,里面是 MR 对象,从控制台输出的数据你可以看到,MR 除了 target 和 oldValue 以外,还有很多有用的属性:
- addedNodes 一个有节点、属性或文本添加的 NodeList
- removedNodes 和上面类似,被删除的 NodeList
- previousSibling 返回此节点前面的兄弟节点
- nextSibling 返回此节点后面的兄弟节点
- attributeName 返回属性被改变的属性名
- oldValue 返回给你赋值之前的当前值
- type DOM改变类型 (attribute、 characterData 或 childList)
我们可以用下面的这个 observe() 方法来注册需要监听的DOM节点,并通过options对象参数来指定哪些类型的变化可以触发回调。
var options = {
subtree: true,
childList: true,
attributes: false
};
observer.observe(target, options);
可配参数有:
- childList: *true 子节点
- attributes: true 属性
- characterData: true 文本内容
- subtree: true 子孙节点
- attributeOldValue: true 记录老的属性值
- characterDataOldValue: true 记录老的文本内容
- attributeFilter: 只监听数组里面的属性
如上,我们可以通过 options 来配置监听我们希望被监听到的节点,即便是从root也OK,但当然,只求所需将会为我们带来更好的性能。来让我们在页面上搞出一块儿可编辑区域吧(比如: <div id="editor” contentEditable> )
首先,让我们取得它的元素引用:
var editor = document.querySelector('#editor');
然后我们用已经实例化好的 MO 来监听它
observer.observe(editor, config);
从下面的截图我们可以看出,只要可编辑区域的内容有任何变化,回调方法都会被调用,并返回 MR 对象列表。
爽不爽!(≧▽≦)/(作者自嗨中),当然,如果我们想停止监听,只要调用disconnect()方法即可:
observer.disconnect();
然后所有的监听就停止了,一切风平浪静。这里我们不太深入的去了解 MO 的 API 什么的了, Dev.Opera 和 MDN 都有良好的介绍文档,有兴趣的去读读吧。下面让我们谈一下如何通过这个逆天的MO去实现 DOM 的撤销功能,噗,会很有趣的,别走开,没有广告啊马上回来。
用 Mutation Observers 实现 Undo/Redo
做过编辑器的同学都知道Undo或Redo在其中扮演着怎样重要的角色,然后懒死的浏览器厂商还都没有一个实现标准API接口的(没API标准?你搞笑呢 UndoManager),我们通常不得不通过用堆栈来存储历史数据,搞的编辑器内存负重直线上升,一桶水不满半桶水晃悠的样子。
这些**大拿们都用各种框架啊React或Clojure什么的去做,光看看代码就有一种历史的厚重感,糟糕透了。那我们是不是可以尝试一下只用原生JS就能够实现的解决方案呢?虽然可能会滥用回调,可谁不是呢~
步骤一
让我们开始布置舞台,在 app.html,用 contentEditable 属性搞出一块儿可编辑区域和两个按钮 —— 撤销和重做。虽然这儿只是拿编辑文本做例子,但本质上是通用的,都是实现DOM改变的撤销与重做(所以自然也适用于任务列表的添加更改)
这是我们的HTML:
<div id='text' contentEditable='true'></div>
<button id='undo'>Undo</button>
<button id='redo'>Redo</button>
然后开始写JS:
// 因为所以嘛我们用$代替querySelectorvar
$ = document.querySelector.bind(document);
var text = $('#text');
var undo = $('#undo');
var redo = $('#redo');
之后我们会用为这两个按钮绑定点击事件,点击后他们会撤销或重做编辑框里的内容。
其实我们需要实现的地方有两部分:内容已经改变的时机和在撤销或重做的时候它的逻辑是什么。我们用 MO 实现第一部分,用 Undo.js 实现第二部分,这是大神Jörn Zaefferer为 Undo/Redo 特意做的一个抽象原型,我们可以像如下代码一样很方便的去用它:
var stack = new Undo.Stack();
步骤二 Step 2
凑巧的是 Undo.js 就有一个关于编辑器撤销重做的例子(我机智的放到了这里 JSBin),但在内容改变时机上用的是onkeyup事件,真low~,大家看看EditCommand的使用方式就好了:
$('#editor').bind('keyup', function() {
clearTimeout(timer);
timer = setTimeout(function() {
var newValue = text.html();
// ignore meta key presses
if (newValue !== startValue) {
// this could try and make a diff instead of storing snapshots
stack.execute(new EditCommand(text, startValue, newValue));
startValue = newValue;
}
}, 250);
});
最主要的一行就是那个stack.execute(new EditCommand(...)),它需要传入三个参数,所编辑的DOM节点、老的内容和新的内容。
所以,我们要先定义一下我们自己的 EditCommand。很简单,继承 Undo.Command 定义一个构造函数,undo 方法和 redo 方法简单的使用 innerHTML 赋值。
var EditCommand = Undo.Command.extend({
constructor: function(textarea, oldValue, newValue) {
this.textarea = textarea;
this.oldValue = oldValue;
this.newValue = newValue;
},
undo: function() {
this.textarea.innerHTML = this.oldValue;
},
redo: function() {
this.textarea.innerHTML = this.newValue;
}
});
OK,搞定 EditCommand 后我们有了一个记录堆栈,让我们继续用 MO 把之前的**onkeyup事件替换掉。
步骤三
首先,定义一个实例化 MO 加回调,胡乱把代码复制进去就好啦:
var newValue = '';
var observer = new MutationObserver(function(mutations) {
newValue = text.innerHTML;
stack.execute(new EditCommand(text, startValue, newValue));
startValue = newValue;
});
为了忠于原demo,我们可以简单的将 编辑器里的内容全部输出当作 newValue 传过去,那MO起的作用其实是“这里有些东西改变了”,而不是去使用它提供的具体改变后的内容。解决方案是向下面这样把每一个变化都记录下来:
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
newValue = mutation.target.innerHTML;
if (newValue !== startValue) {
stack.execute(new EditCommand(text, startValue, newValue));
startValue = newValue;
}
});
});
无论怎样,我们都会把可编辑节点、老值和新值传入到实例化的 EditCommand 中去。
这里要注意的是,在做Undo/Redo动作时,依旧属于“内容被改变”,会重新触发MO执行回调,为了避免这个问题,我们可以加个状态判断。
var blocked = false;
var observer = new MutationObserver(function(mutations) {
if (blocked) {
blocked = false;
return;
}
newValue = text.innerHTML;
stack.execute(new EditCommand(text, startValue, newValue));
startValue = newValue;
});
记得在 EditCommand 也要把在这个 blocked 状态用上喔,不然白加了…
var EditCommand = Undo.Command.extend({
constructor: function(textarea, oldValue, newValue) {
this.textarea = textarea;
this.oldValue = oldValue;
this.newValue = newValue;
},
execute: function() {},
undo: function() {
blocked = true;
this.textarea.innerHTML = this.oldValue;
},
redo: function() {
blocked = true;
this.textarea.innerHTML = this.newValue;
}
});
步骤四
下面,我们来执行监听器的 observe 方法吧~ 启动起来!
observer.observe(text, {
attributes: true,
childList: true,
characterData: true,
characterDataOldValue: true,
subtree: true
});
我们想确保只有在有内容改变的情况下撤销和重做按钮才可点,那么我们可以如下写这样的一个方法,通过 stack 的两个判断方法来检测现在是否可以撤销或重做。
function stackUI() {
redo.disabled = !stack.canRedo();
undo.disabled = !stack.canUndo();
}
那如下自然,每当 stack 中有改变的时候,都要执行一下stackUI方法了
stack.changed = function() {
stackUI();
};
步骤五
最后我们写好按钮的点击事件监听,well done~
redo.addEventListener('click', function() {
stack.redo();
});
save.addEventListener('click', function() {
stack.undo();
});
就这样,我们实现了编辑器的撤销和重做功能,so easy,你可以查看Demo源码看看效果如何
Demo: http://jsbin.com/zamacuta/5/
注:我只是简单写写啊目前是单字符撤销,没深处理,你可以再通过一定的延迟来达到更好的效果。
虽然上面的例子可能会有一定的缺陷和性能限制,但它的确是很简易的用原生JS实现了应用的撤销和重做功能啊!嘛……即便你觉得MO没啥用,那下个 Undo.js 玩玩也不错嘛是吧哈哈……
contentEditable 之外
完成了有关于 contentEditable 的撤销重做功能后,大家再看我写的 TodoMVC 和 Google Now 这两个例子,上升到应用的层次上的撤销重做功能!牛逼吧啊哈哈哈!我还用 Polymer 搞了一个自定义 Web Component ,我将其命名为 ,哼哼哼,将所有跟撤销重做的代码都封装到这个标签里去,然后使用的话只需要:
<undo-manager>
<div contentEditable></div>
</undo-manager>
虽然现在说这个还有点早,毕竟各浏览器平台还都没怎么实现它。但从理念上讲讲还是可以的,就是我们写了这个标签在外面之后呢就什么都不用管了!它自动生成按钮与监听DOM改变,如果你感兴趣的话可以看看这个demo Circles 。
Mutation Observers 的性能优势
上面我们看了代码演示,接下来让我们快速过一下性能的问题。MO 比 DOM ME 更有效也更安全,但从速度对比上还不是很明显,它的一个核心优势是我们避免了如下例使用 DOMNodeInserted 所出现的问题:
// 千万注意: 别这么用,会**的
document.addEventListener('DOMNodeInserted', function() {
var el = document.createElement('div');
document.body.appendChild(el);
});
是吧,**都能看出来这么做会引起死循环,监听 DOM 插入事件,结果回调里面执行了插入 DOM 的动作,又引起事件的触发,浏览器不断的中断渲染(重新计算样式和布局),导致 FOUC 不断闪瞎你的双眼。 MO 没有这个问题,浏览器会在有价值的情况下再进行通知(例如当所有JavaScript都执行完毕之后,或在浏览器样式渲染和计算都完成以后)。
如果你对这个话题感兴趣的话,去看 this Stack Overflow thread。
注意:我刻意避开了关于 MO 的基准点测试,因为它在正常环境下是没有什么性能问题的,一些具体的差异细节可以去 jsPerf 看看,但我不保证它的准确性。╮(╯_╰)╭
攻受之分
对于监听,我们最常讨论就是 GC 和引用的问题了,MO 闭页面会自动断开文档内所有节点的监听,它相对于目标节点都是弱引用,节点被销毁它也不会再触发事件了。但DOM节点相对于 MO 却是强引用,所以如果目标节点还存在的情况下,你应该避免在MO实例在没断开节点前而销毁它,尽可能的在在页面unload的时候再断开监听吧。
Mutation Observers 的局限性
上面我一直在夸它的样子,但它并不是没有缺点,下面说说MO的局限性:
- MO 还不能准确的监听表单元素中的状态改变,因为它们的“live”或“true”并不能真正的反射到属性层,而这个"value"其实只是"defaultValue",类似的元素比如<textarea>的值或是否闭合都需要单独监听,比如input内容改变,但input.getAttribute('value')返回会一直是null。普遍的解决方案是用通过监听input的keyup事件,我经常也见到有的同学会用50-200毫秒的轮询去检测文本改变。
- MO 没办法检测到CSS样式的更改(如hover状态什么的)。
- 变更记录里没有时间戳。
- DOM ME 有个问题是直接关联于插入的DOM节点,并保持完整的调用作用域,相当耗费内存成本。所以 MO 的延迟处理和批处理的效率就更高些了。但也因此导致很多不需要的信息产生,比如插入每个节点的执行环境什么的。
- 在Chrome环境下,MO 可能会受到 GC 的影响,比如根据上下文自定义的属性被吞的情况什么的,具体请看 https://code.google.com/p/chromium/issues/detail?id=329103
最后
(以上翻译内容都是我瞎编的,终于要完事儿了,我的灵感也快枯竭了)
有些人可能认为,毕竟是比较新鲜的玩意,不稳定啊时机还不太成熟,还不太敢使用它。但从下面的图中大家可以看到 MO 在浏览器中的支持还是不错的,虽然或多或少都有些缺陷,还是值得一试的。
一如既往,如果你有关于这个API的问题欢迎向 Blink 团队反馈,我很乐于看到你们为本文指出错误,当然,改不改就是我的事情了。希望此篇文章对你有所帮助。感谢 Mathias Bynens 和 Pascal Hartig 的提前浏览,没他们我肯定会写的更好,随时欢迎反馈和点评,反正我不会再动这篇文章了,哼~
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.