竹磬网-邵珠庆の日记 生命只有一次,你可以用它来做些更多伟大的事情–Make the world a little better and easier


313月/170

用Git撤销任何操作

发布在 邵珠庆

任何版本控制系统的一个最有的用特性就是“撤销 (undo)”你的错误操作的能力。在 Git 里,“撤销” 蕴含了不少略有差别的功能。

当你进行一次新的提交的时候,Git 会保存你代码库在那个特定时间点的快照;之后,你可以利用 Git 返回到你的项目的一个早期版本。

在本篇博文里,我会讲解某些你需要“撤销”已做出的修改的常见场景,以及利用 Git 进行这些操作的最佳方法。

 

撤销一个“已公开”的改变

场景: 你已经执行了 git push, 把你的修改发送到了 GitHub,现在你意识到这些 commit 的其中一个是有问题的,你需要撤销那一个 commit.

方法: git revert <SHA>

原理: git revert 会产生一个新的 commit,它和指定 SHA 对应的 commit 是相反的(或者说是反转的)。如果原先的 commit 是“物质”,新的 commit 就是“反物质” — 任何从原先的 commit 里删除的内容会在新的 commit 里被加回去,任何在原先的 commit 里加入的内容会在新的 commit  里被删除。

这是 Git 最安全、最基本的撤销场景,因为它并不会改变历史 — 所以你现在可以  git push 新的“反转” commit 来抵消你错误提交的 commit。

 

修正最后一个 commit 消息

场景: 你在最后一条 commit 消息里有个笔误,已经执行了 git commit -m "Fxies bug #42",但在 git push 之前你意识到消息应该是 “Fixes bug #42″。

方法: git commit --amend 或 git commit --amend -m "Fixes bug #42"

原理: git commit --amend 会用一个新的 commit 更新并替换最近的 commit ,这个新的 commit 会把任何修改内容和上一个 commit 的内容结合起来。如果当前没有提出任何修改,这个操作就只会把上次的 commit 消息重写一遍。

 

撤销“本地的”修改

场景: 一只猫从键盘上走过,无意中保存了修改,然后破坏了编辑器。不过,你还没有 commit 这些修改。你想要恢复被修改文件里的所有内容 — 就像上次 commit 的时候一模一样。

方法: git checkout -- <bad filename>

原理: git checkout 会把工作目录里的文件修改到 Git 之前记录的某个状态。你可以提供一个你想返回的分支名或特定 SHA ,或者在缺省情况下,Git 会认为你希望 checkout 的是 HEAD,当前 checkout 分支的最后一次 commit。

记住:你用这种方法“撤销”的任何修改真的会完全消失。因为它们从来没有被提交过,所以之后 Git 也无法帮助我们恢复它们。你要确保自己了解你在这个操作里扔掉的东西是什么!(也许可以先利用 git diff 确认一下)

 

重置“本地的”修改

场景: 你在本地提交了一些东西(还没有 push),但是所有这些东西都很糟糕,你希望撤销前面的三次提交 — 就像它们从来没有发生过一样。

方法: git reset <last good SHA> 或 git reset --hard <last good SHA>

原理: git reset 会把你的代码库历史返回到指定的 SHA 状态。 这样就像是这些提交从来没有发生过。缺省情况下, git reset 会保留工作目录。这样,提交是没有了,但是修改内容还在磁盘上。这是一种安全的选择,但通常我们会希望一步就“撤销”提交以及修改内容 — 这就是 --hard 选项的功能。

 

在撤销“本地修改”之后再恢复

场景: 你提交了几个 commit,然后用 git reset --hard 撤销了这些修改(见上一段),接着你又意识到:你希望还原这些修改!

方法: git reflog 和 git reset 或 git checkout

原理: git reflog 对于恢复项目历史是一个超棒的资源。你可以恢复几乎 任何东西 — 任何你 commit 过的东西 — 只要通过 reflog。

你可能已经熟悉了 git log 命令,它会显示 commit 的列表。 git reflog 也是类似的,不过它显示的是一个 HEAD 发生改变的时间列表.

一些注意事项:

  • 它涉及的只是 HEAD 的改变。在你切换分支、用 git commit 进行提交、以及用 git reset 撤销 commit 时,HEAD 会改变,但当你用  git checkout -- <bad filename> 撤销时(正如我们在前面讲到的情况),HEAD 并不会改变 — 如前所述,这些修改从来没有被提交过,因此 reflog 也无法帮助我们恢复它们。
  • git reflog 不会永远保持。Git 会定期清理那些 “用不到的” 对象。不要指望几个月前的提交还一直躺在那里。
  • 你的 reflog 就是你的,只是你的。你不能用 git reflog 来恢复另一个开发者没有 push 过的 commit。

reflog

那么…你怎么利用 reflog 来“恢复”之前“撤销”的 commit 呢?它取决于你想做到的到底是什么:

  • 如果你希望准确地恢复项目的历史到某个时间点,用 git reset --hard <SHA>
  • 如果你希望重建工作目录里的一个或多个文件,让它们恢复到某个时间点的状态,用 git checkout <SHA> -- <filename>
  • 如果你希望把这些 commit 里的某一个重新提交到你的代码库里,用 git cherry-pick <SHA>

 

利用分支的另一种做法

场景: 你进行了一些提交,然后意识到你开始 check out 的是 master 分支。你希望这些提交进到另一个特性(feature)分支里。

方法: git branch featuregit reset --hard origin/master, and git checkout feature

原理: 你可能习惯了用 git checkout -b <name> 创建新的分支 — 这是创建新分支并马上 check out 的流行捷径 — 但是你不希望马上切换分支。这里, git branch feature 创建一个叫做 feature 的新分支并指向你最近的 commit,但还是让你 check out 在 master 分支上。

下一步,在提交任何新的 commit 之前,用 git reset --hard 把 master 分支倒回 origin/master 。不过别担心,那些 commit 还在 feature 分支里。

最后,用 git checkout 切换到新的 feature 分支,并且让你最近所有的工作成果都完好无损。

 

及时分支,省去繁琐

场景: 你在 master 分支的基础上创建了 feature 分支,但 master 分支已经滞后于 origin/master 很多。现在 master 分支已经和 origin/master 同步,你希望在 feature 上的提交是从现在开始,而不是也从滞后很多的地方开始。

方法: git checkout feature 和 git rebase master

原理: 要达到这个效果,你本来可以通过 git reset (不加 --hard, 这样可以在磁盘上保留修改) 和 git checkout -b <new branch name> 然后再重新提交修改,不过这样做的话,你就会失去提交历史。我们有更好的办法。

git rebase master 会做如下的事情:

  • 首先它会找到你当前 check out 的分支和 master 分支的共同祖先。
  • 然后它 reset 当前  check out 的分支到那个共同祖先,在一个临时保存区存放所有之前的提交。
  • 然后它把当前 check out 的分支提到 master 的末尾部分,并从临时保存区重新把存放的 commit 提交到 master 分支的最后一个 commit 之后。

 

大量的撤销/恢复

场景: 你向某个方向开始实现一个特性,但是半路你意识到另一个方案更好。你已经进行了十几次提交,但你现在只需要其中的一部分。你希望其他不需要的提交统统消失。

方法: git rebase -i <earlier SHA>

原理: -i 参数让 rebase 进入“交互模式”。它开始类似于前面讨论的 rebase,但在重新进行任何提交之前,它会暂停下来并允许你详细地修改每个提交。

rebase -i 会打开你的缺省文本编辑器,里面列出候选的提交。如下所示:

rebase-interactive1

前面两列是键:第一个是选定的命令,对应第二列里的 SHA 确定的 commit。缺省情况下, rebase -i  假定每个 commit 都要通过  pick 命令被运用。

要丢弃一个 commit,只要在编辑器里删除那一行就行了。如果你不再需要项目里的那几个错误的提交,你可以删除上例中的1、3、4行。

如果你需要保留 commit 的内容,而是对 commit 消息进行编辑,你可以使用 reword 命令。 把第一列里的 pick 替换为 reword (或者直接用 r)。有人会觉得在这里直接重写 commit 消息就行了,但是这样不管用 —rebase -i 会忽略 SHA 列前面的任何东西。它后面的文本只是用来帮助我们记住 0835fe2 是干啥的。当你完成 rebase -i 的操作之后,你会被提示输入需要编写的任何 commit 消息。

如果你需要把两个 commit 合并到一起,你可以使用 squash 或 fixup 命令,如下所示:

rebase-interactive2

squash 和 fixup 会“向上”合并 — 带有这两个命令的 commit 会被合并到它的前一个 commit 里。在这个例子里, 0835fe2 和 6943e85 会被合并成一个 commit, 38f5e4e 和 af67f82 会被合并成另一个。

如果你选择了 squash, Git 会提示我们给新合并的 commit 一个新的 commit 消息; fixup 则会把合并清单里第一个 commit 的消息直接给新合并的 commit 。 这里,你知道 af67f82 是一个“完了完了….” 的 commit,所以你会留着 38f5e4e as的 commit 消息,但你会给合并了 0835fe2 和 6943e85 的新 commit 编写一个新的消息。

在你保存并退出编辑器的时候,Git 会按从顶部到底部的顺序运用你的 commit。你可以通过在保存前修改 commit 顺序来改变运用的顺序。如果你愿意,你也可以通过如下安排把 af67f82 和 0835fe2 合并到一起:

rebase-interactive3

 

修复更早期的 commit

场景: 你在一个更早期的 commit 里忘记了加入一个文件,如果更早的 commit 能包含这个忘记的文件就太棒了。你还没有 push,但这个 commit 不是最近的,所以你没法用 commit --amend.

方法: git commit --squash <SHA of the earlier commit> 和 git rebase --autosquash -i <even earlier SHA>

原理: git commit --squash 会创建一个新的 commit ,它带有一个 commit 消息,类似于 squash! Earlier commit。 (你也可以手工创建一个带有类似 commit 消息的 commit,但是 commit --squash 可以帮你省下输入的工作。)

如果你不想被提示为新合并的 commit 输入一条新的 commit 消息,你也可以利用 git commit --fixup 。在这个情况下,你很可能会用commit --fixup ,因为你只是希望在 rebase 的时候使用早期 commit 的 commit 消息。

rebase --autosquash -i  会激活一个交互式的 rebase 编辑器,但是编辑器打开的时候,在 commit 清单里任何 squash! 和 fixup! 的 commit 都已经配对到目标 commit 上了,如下所示:

rebase-autosquash

在使用 --squash 和 --fixup 的时候,你可能不记得想要修正的 commit 的 SHA 了— 只记得它是前面第 1 个或第 5 个 commit。你会发现 Git 的 ^ 和 ~ 操作符特别好用。HEAD^ 是 HEAD 的前一个 commit。 HEAD~4 是 HEAD 往前第 4 个 – 或者一起算,倒数第 5 个 commit。

 

停止追踪一个文件

场景: 你偶然把 application.log 加到代码库里了,现在每次你运行应用,Git 都会报告在 application.log 里有未提交的修改。你把 *.login 放到了 .gitignore 文件里,可文件还是在代码库里 — 你怎么才能告诉 Git “撤销” 对这个文件的追踪呢?

方法: git rm --cached application.log

原理: 虽然 .gitignore 会阻止 Git 追踪文件的修改,甚至不关注文件是否存在,但这只是针对那些以前从来没有追踪过的文件。一旦有个文件被加入并提交了,Git 就会持续关注该文件的改变。类似地,如果你利用 git add -f 来强制或覆盖了 .gitignore, Git 还会持续追踪改变的情况。之后你就不必用-f  来添加这个文件了。

如果你希望从 Git 的追踪对象中删除那个本应忽略的文件, git rm --cached 会从追踪对象中删除它,但让文件在磁盘上保持原封不动。因为现在它已经被忽略了,你在  git status 里就不会再看见这个文件,也不会再偶然提交该文件的修改了。

 


这就是如何在 Git 里撤销任何操作的方法。要了解更多关于本文中用到的 Git 命令,请查看下面的有关文档:

153月/170

最好的 ss-panel 部署教程

发布在 邵珠庆

续・为最好的 ss-panel 部署教程献上更新!
本文最后更新与 2016-02-08,更新内容详见博文底部。

今天折腾了好久 ss-panel,期间遇到了一些奇奇怪怪的问题,但是网上都没有好的解决方法。

网上那么多教程有些是写得笼统,有些还是瞎复制的。

由此萌生了想要写一篇配置 ss-panel 和 ss-manyuser 的教程,希望能够帮到需要的人。

注意,本教程 不是 图文并茂的面向小白的教程,窝希望你能够有足够的 linux 操作经验再来看这篇教程。

至少你需要熟悉 ssh 连接,熟悉 web 环境的配置,最好可以看得懂一些代码。

一、安装并配置 ss-panel

本教程打算先配置好前端,当然你想要先配置后端可以拉下去。

0x01 环境要求

作为前端的 ss-panel 是使用 PHP 编写的网页应用程序,它对你的主机运行环境有一定的要求。

  • PHP 5.6 或更高
  • MySQL 5.5 或更高
  • 支持 URL 重写的 Web 服务器(Nginx / Apache 均可)

本教程所使用的环境是 NGINX + PHP 7 + MariaDB 10。

当然其他主流 LNMP/LAMP 架构都可以(个人推荐使用 OneinStack),确保你的站点可以访问后就继续吧。

什么?你不知道上面说的那些东西是什么?那你为什么不问问神奇海螺呢?


注意,接下来的操作大部分都是在【目标服务器】的 shell 中进行的。继续阅读之前,你需要通过 SSH 等工具连接至你的服务器,它一般长这样:

shell example

如果你不晓得这是什么,神奇海螺……以下略。

0x02 下载 ss-panel 源码

ss-panel 的 GitHub 项目地址:orvice/ss-panel

使用 cd 进入你站点的 web 根目录,从 git 上 clone 源码:

# 最前面的美元符号是命令提示符,别把这个给一起输进去了

$ git clone https://github.com/orvice/ss-panel.git

当然你也可以下载源码再用 SCP/FPS 传到服务器上去。

注意源码下载完成后的目录结构,请务必保证 /public 目录在站点的根目录下。

你可以使用 $ mv ss-panel/{.,}* ./ 命令将子目录的内容移动到当前目录来。正确的目录结构应该类似于这样:

directory structure example

0x03 配置 ss-panel

执行完上面的步骤之后,你兴高采烈地访问了你的站点,却得到了无情的 403 Forbidden —— 站点根目录下竟然没有 index.php!

好吧其实没什么好奇怪的,大部分 MVC 框架都将 index.php 的入口文件放到其他子目录下了,

这样做是为了保护根目录下的配置文件等可能会导致信息泄露的敏感文件无法被访问。

接下来请按照 官方文档的说明 正确配置你的 Web 服务器。正确配置后的 NGINX 配置应该长这样:

nginx configuration sample

编辑完后重载你的 Web 服务器,然后访问你的站点……

于是你得到了一个 500 Internal Server Error(如果你没开启 display_errors 可能看不到详细报错):

Warning: require(/home/wwwroot/ss.prinzeugen.net/vendor/autoload.php): failed to open stream: No such file or directory in /home/wwwroot/ss.prinzeugen.net/bootstrap.php on line 18 Fatal error: require(): Failed opening required '/home/wwwroot/ss.prinzeugen.net/vendor/autoload.php' (include_path='.:/usr/local/php/lib/php') in /home/wwwroot/ss.prinzeugen.net/bootstrap.php on line 18

这是我们还未安装 ss-panel 所需的依赖库导致的。遂安装之:

$ curl -sS https://getcomposer.org/installer | php $ php composer.phar install

installing dependencies with composer

等待它安装完毕后接着进行配置:

$ cp .env.example .env

将 .env.example 复制一份重命名为 .env,自行修改其中的数据库等信息。

# database 数据库配置

db_driver = 'mysql'

db_host = 'localhost'

db_port = '3306'

db_database = 'ss-panel'

db_username = 'ss-panel'

db_password = 'secret'

db_charset = 'utf8'

db_collation = 'utf8_general_ci'

db_prefix = ''

数据库的创建我就不多说了,建站的一般都玩过数据库吧?

将根目录下的 db.sql 导入到数据库中即可。其他配置自行修改。

importing tables

你还需要修改 .env 中的 muKey 字段,修改为任意字符串(最好只包含 ASCII 字符),

下面配置后端的时候我们需要使用到这个 muKey:

muKey = 'api_key_just_for_test'

接下来,确保 storage 目录可写入(否则 Smarty 会报错):

$ chown -R www storage 现在访问你的站点,就可以看到 ss-panel 的首页啦:

landing page

0x03 进入 ss-panel 后台 现在访问 http://your-domain/admin 就可以进入 ss-panel 后台了。

不过细心的你可能会注意到,刚才导入数据表的时候,user 表并没有添加记录,那要咋进管理后台呢?

当然你可以在数据库中手动加一条记录,不过作者已经提供了一个更方便的方式:

$ php xcat createAdmin 在站点根目录下运行,根据提示即可创建管理员账号 (这个文字对齐真鸡儿 shi):

creating admin account

使用刚才填写的邮箱和密码进入后台:

ss-panel dashboard screenshot

到这里,作为前端的 ss-panel 就已经配置完成了。下面开始部署作为后端的 shadowsocks-manyuser


二、部署并配置 shadowsocks-manyuser

在本篇教程中我们使用 fsgmhoward/shadowsocks-py-mu 这个版本的后端。

不同于这篇教程原先推荐的 @mengskysama 版本,这个后端支持使用 MultiUser API 与前端的 ss-panel 进行用户信息交互。

这个 API 的官方介绍在这里。简单来讲,如果你通过 API 来与前端通信,你就不需要修改后端的数据库配置了,

并且可以使用「自定义加密」、「流量记录」等高级功能。

下面我只介绍使用 API 的方法,另外那个比较麻烦的方法可以在这里查看。

0x01 安装 shadowsocks-manyuser

先将代码 clone 到本地:

$ git clone https://github.com/fsgmhoward/shadowsocks-py-mu.git 源码 clone 后,

你的目录结构应该是这样的:

directory structure ss mu

其中的 shadowsocks 子目录才是我们需要的,外面的是 setup.py 的相关文件。

0x02 配置 shadowsocks-manyuser

进入 shadowsocks 目录,将 config_example.py 复制一份到 config.py:

$ cp config_example.py config.py

修改其中第 15 行和第 29~31 行的内容:

# 启用 MultiUser API API_ENABLED = True

# 就是在你的站点地址后面加个 /mu

API_URL = 'http://ss.prinzeugen.net/mu'

# 还记得上面在 .env 中填写的 muKey 吗?把它填在这里

API_PASS = 'api_key_just_for_test'

由于我们选择使用 Mu API 来与前端通信,所以我们不需要修改 config.py 中任何关于数据库的配置。

好了,现在可以试着运行一下 $ python servers.py 了(注意,是 servers.py 而不是 server.py)。

如果没错的话,应该可以看到这样的输出:

running successfully

其中 P[XXX] 表示用户端口,M[XXX] 表示加密方式,E[XXX] 表示用户的邮箱地址。

这些都会随着 ss-panel 前端中用户配置的改变而实时变化。

0x03 常见错误 FAQ

连接上 shadowsocks 试试看能不能翻墙了?八成不能。

虽然你成功的把 servers.py 跑起来了,但还可能有各种神奇的错误阻止你翻出伟大的墙。

首先国际惯例查看连接:

$ netstat -anp | grep

你的端口 正常的话,应该是这样的:

Active Internet connections (servers and established)  
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name  
tcp        0      0 0.0.0.0:62111            0.0.0.0:*               LISTEN      32083/python  
tcp        0      0 162.233.122.111:62111    115.233.233.140:47177   TIME_WAIT   -  
tcp        0      0 162.233.122.111:62111    115.233.233.140:47161   TIME_WAIT   -  
tcp        0      0 162.233.122.111:62111    115.233.233.140:47160   TIME_WAIT   -  
tcp        0      0 162.233.122.111:62111    115.233.233.140:47157   TIME_WAIT   -

如果没有来自你的 IP 的 TCP 连接的话,那八成就是防火墙的锅了,

执行 iptables 放行你的端口:

$ iptables -I INPUT -p tcp -m tcp --dport 你的端口 -j ACCEPT

$ iptables-save

ss-panel 新注册的用户所分配的端口均为其 id-1 的用户的端口号 + 1。

比如说你把 admin 用户(uid 为1)的端口改为 12450(ss-panel 中不能改,去数据库改),

那么后面注册的新用户的端口就会是 12451, 12452 这样递增的。

所以如果你要开放注册,就要这样配置你的 iptables:

# 注意是半角冒号,意为允许 12450 及以上的端口

# 也可以指定 12450:15550 这样的范围

$ iptables -I INPUT -p tcp -m tcp --dport 12450: -j ACCEPT

现在再连接 shadowsocks 就应该可以看到 TCP 连接信息了。

并且你可以在 ss-mu 后端的输出信息中看到详细的连接日志:

connection log

日志格式的详细介绍在这里:

Explanation of the log output。

三、配置 ss-manyuser 守护进程以及多节点配置

0x01 使用 supervisor 监控 ss-manyuser 运行

如果你只是想让 ss-manyuser 在后台运行的话,可以参考我写的这篇文章。

安装 supervisor (用的是上面安装过的 pip):

# 先安装 pip 包管理器

$ sudo apt-get install python-pip

# For Debian/Ubuntu

$ sudo yum install python-pip

# For CentOS

$ pip install supervisor

创建 supervisor 配置文件

# 输出至 supervisor 的默认配置路径

$ echo_supervisord_conf > /etc/supervisord.conf

运行 supervisor 服务

$ supervisord

配置 supervisor 以监控 ss-manyuser 运行

$ vim /etc/supervisord.conf

在文件尾部(当然也可以新建配置文件,不过这样比较方便)添加如下内容并酌情修改:

[program:ss-manyuser]
command = python /root/shadowsocks-py-mu/shadowsocks/servers.py  
user = root  
autostart = true  
autorestart = true

其中 command 里的目录请自行修改为你的 servers.py 所在的绝对路径。

重启 supervisor 服务以加载配置

$ killall -HUP supervisord

查看 shadowsocks-manyuser 是否已经运行:

$ ps -ef | grep servers.py root 952 739 0 15:40 ? 00:00:00

python /root/shadowsocks-rm/shadowsocks/servers.py

可以通过以下命令管理shadowsock-manyuser 的状态

$ supervisorctl {start|stop|restart} ss-manyuser

0x02 ss-panel 的多节点配置

其实多节点也没咋玄乎,说白了就是多个后端共用一个前端而已。

而且我们的后端是使用 Mu API 来与前端进行交互的,所以多节点的配置就更简单了:

只要把所有后端的 config.py 中的 API_URL 和 API_PASS 都改成一样即可(记得 API_ENABLED = True)。

四、写在后面

其他可用的前端:

esdeathlove/ss-panel-v3-mod,修改版的 ss-panel,修改了蛮多东西的,有兴趣的朋友可以去看看,他那边安装说明都写得很详细了;

sendya/shadowsocks-panel,另外一个全新的 ss-manyuser 前端,有用户等级、套餐、支付等功能,不支持使用 Mu API,部署教程在这里。

其他可用的后端:

shadowsocks-go mu,Go 语言实现,性能比 Python 版好,也支持 Mu API;

shadowsocksR manyuser SSR 版本,见仁见智。

这几个后端的部署方法大同小异,我这里就不再赘述了。

文章更新日志 2017-03-15

根据评论区推荐将后端由 mengskysama/shadowsocks-rm/manyuser 切换至 fsgmhoward/shadowsocks-py-mu; 所有后端配置均改用 Mu API v2 的方法;

使用了更加严谨的文法。


success google

至此,你已完成对 ss-panel 的部署。叫上小伙伴们一起享受自由的互联网吧 😉

如果碰到什么奇怪的错误,请评论留言(带上你的日志)

 

93月/170

jQuery源码分析

发布在 邵珠庆

前言

有时候我在想jQuery为什么可以直接$操作,可以拥有比原生js更便利的DOM操作,而且只要你想就可以直接链式操作下去

核心框架

揭开一万多行代码的jQuery核心代码:

(function(window, undefined) {
	function jQuery(selector){
		return new jQuery.fn.init(selector)
	}
	jQuery.fn = jQuery.prototype = {
		init: function () {

		}
	}
	jQuery.fn.init.prototype = jQuery.fn;
	window.jQuery = window.$ = jQuery;
})(window)
  • 闭包结构传参window
    • 闭包结构传入实参window,然后里面用形参接收
      • 减少内部每次引用window的查询时间
      • 方便压缩代码
  • 形参undefined
    • 因为ie低版本的浏览器可以给undefined赋值成功,所以为了保证undefined的纯洁给它一个形参的位置而没有实参,保证了它一定是undefined
  • jQuery传参selector
    • selector可以是一对标签,可以是id、类、后代、子代等等,可以是jQuery对象,
  • jQuery原型对象赋值
    • 方便扩展jQuery的原型方法
  • return 实例化原型方法init
    • 其实就是为了我们每次使用$不用new $();
    • 为什么jQuery要new自己的原型方法呢,因为不new自己的就要new其他的函数返回,那干嘛不自己利用自己
  • jQuery原型对象赋值给jQuery原型方法init的原型
    • 因为内部给jQuery原型每扩展一个方法init也会有该方法,是不是很酷炫,init有了那么$()出来的jQuery对象是不是也有啦
  • 给window暴露可利用成员jQuery,$
    • 给window暴露后那么全局都可以直接使用了jQuery和$了
    • 至于为什么有$,因为短啊,当然你也可以每次jQuery()来使用

御用选择器-Sizzle

  • Sizzle也是jQuery的根本,当然了你也单独使用Sizzle
  • 上面说过$(selector)的参数selector可以是id、类、后代、子代等等,可以是jQuery对象,那么咱们每次$一下就可以心想事成的得到我们想要的jQuery对象是怎么办到的呢,没错,就是因为Sizzle,Sizzle封装了获取各种dom对象的方法,并且会把他们包装成jQuery对象
  • 浏览器能力测试
    • Sizzle内部有个support对象,support对象存储着正则测试浏览器能力的结果
    • 对于有能力问题的选择器使用通用兼容方案解决(繁琐的判断代码)
  • 正则
    • 正则表达式在jQuery中使用的还是比较多的,正则的使用可以很大的提交我们对数据的处理效率
  • 判断
    • 判断是在init内部判断selector的类型,
      • 列如可能是个html标签,那么直接create一个selector标签的DOM对象包装成jQuery对象return出去
      • 列如可能是个id名、类名、标签名等等,那么直接通过Sizzle获取到DOM对象包装成jQuery对象return出去
  • 包装
    • 我已经说了很多次的包装了,没错,jQuery对象其实也是个伪数组,这也是它的设计巧妙之处,因为用数组存储数据方便我们去进行更多的数据处理,比如$("div").css("color": "red"),那么jQuery会自动帮我们隐式迭代、再给页面上所有div包含的文字颜色设置为red,简单粗暴的一行代码搞定简直是程序猿的最爱

对外扩展-extend

  • jQuery核心的结构处理完毕之后基本上就可以对外使用了,但是我们知道我们是可以基于jQuery来实现插件的,包括jQuery自己可扩展性也必须要求他要对外提供一个接口方便进行二次开发,所以有了extend方法
  • 简单的extend就是混入,列子:
    function extend(obj) {
        var k;
        for(k in obj) {
            this[k] = obj[k];
        }
    }

    Baiya.extend = extend;
    Baiya.fn.extend = extend;

对静态方法的和实例方法的扩展都要有,比如each方法,可以$.each来使用,也可以是$("div").each来使用

  • 之后jQuery一些方法都是基于extend来扩展的,当然我们自己也可以基于jQuery扩展方法

DOM操作

  • DOM操作也是jQuery的一大特点,因为它太好用了,包含了我们所能想到的所有使用场景,完善了增删查改常用的方法
  • jQuery获取和设置类的方法如html()/css()/val()等等这些传参是设置值不传参是获取值

##链式编程

  • jQuery是支持链式编程的,只要你想你就可以一行代码写完所有的功能,这是怎么做到的呢
  • 每一个改变原型链的方法都会把当前的this对象保存成他自己的属性,然后可以调用end方法找到上一级链从而方便我们可以进行链式操作

事件操作

  • jQuery的事件操作一般可以通过click类(mouseover/mouseleave等等)和on来使用,但是click类的实现是调用on的
  • on的实现是对原生的onclick类的处理,因为相同的原生的事件在同一个DOM对象上只能被绑定一次,如果再次绑定会覆盖掉上一次的,所以jQuery帮我们封装了事件的存储,把相同的事件分成一个数组存储在一个对象里面,然后对数组进行遍历,依次调用数组里存储的每个方法
  • on实现之后会把所有的事件处理字符串处理一下用on来改造一下,如下:
    Baiya.each(("onclick,onmousedown,onmouseenter,onmouseleave," +
    "onmousemove,onmouseout,onmouseover,onmouseup,onfocus," +
    "onmousewheel,onkeydown,onkeypress,onkeyup,onblur").split(","),     function (i, v) {
        var event = v.slice(2);
        Baiya.fn[event] = function (callback) {
            return this.on(event, callback);
        }
    });

属性操作

  • jQuery也提供给了我们方便的属性操作,底层就是对原生方法的包装,处理兼容性问题,如果jQuery不对IE浏览器的兼容处理的话,那么它的代码量可能会缩一半,当然锅不能全部甩给IE,比如innerText方法火狐是不支持的,但是支持textContent方法,所以jQuery会尽可能的处理这种浏览器带来的差异

样式操作

  • 基本思想如上

Ajax操作

  • Ajax可以说是前端的跨越性进步,毫不夸张的说如果没有Ajax的发展,那么今天的前端可能不叫前端,可能是美工……
  • Ajax是什么?
    • 在我的理解来看Ajax就是一个方法,这个方法遵循着http协议的规范,我们可以使用这个方法来向服务器请求少量的数据,有了数据之后我们就可以操作DOM来达到局部更新网页的目的,这是一个非常酷的事情
  • jQuery的Ajax是基于XMLHttpRequest的封装,当然了他也有兼容性问题,具体的封装见我之前的文章简单的ajax封装
  • 具体就是区别get和post请求的区别,get请求的传参是直接拼接在url结尾,而post请求需要在send()里面传递,并且post请求还要设置请求头setRequestHeader("content-type", "application/x-www-form-urlencoded")
  • 请求后对json或者text或者xml的数据进行处理就可以渲染到页面了

提到Ajax就不得不提到跨域了

  • 跨域简单的来说限制了非同源(ip/域名/端口/协议)的数据交互,当然这肯定是极好的,因为如果不限制那么你的网页别人也可以操作是不是很恐怖
  • 但是有些情况下我们需要调用别人的服务器数据,而且别人也愿意怎么办呢,程序员是很聪明的,html标签中img,script,link等一些带有src属性的标签是可以请求外部资源的,img和link得到的数据是不可用的,只有script标签请求的数据我们可以通过函数来接收,函数的参数传递可以是任何类型,所以创建一个函数,来接收,参数就是请求到的数据,而对方的数据也要用该函数来调用就可以实现跨域了
  • 简单封装jsonp实现
        // url是请求的接口
        // params是传递的参数
        // fn是回调函数
        function jsonp(url, params, fn){
			// cbName实现给url加上哈希,防止同一个地址请求出现缓存
            var cbName = `jsonp_${(Math.random() * Math.random()).toString().substr(2)}`;
            window[cbName] = function (data) {
                fn(data);
                // 获取数据后移除script标签
                window.document.body.removeChild(scriptElement);
            };

            // 组合最终请求的url地址
            var querystring = '';
            for (var key in params) {
                querystring += `${key}=${params[key]}&`;
            }
            // 告诉服务端我的回调叫什么
            querystring += `callback=${cbName}`;

            url = `${url}?${querystring}`;

            // 创建一个script标签,并将src设置为url地址
            var scriptElement = window.document.createElement('script');
            scriptElement.src = url;
            // appendChild(执行)
            window.document.body.appendChild(scriptElement);
        }

Animate

  • 很抱歉的是jQuery的动画源码我并没有阅读,但是我自己封装了一个动画函数,之后的源码阅读会补上的
  • 封装的代码
     // element设置动画的DOM对象
     // attrs设置动画的属性	object
     // fn是回调函数
     function animate(element, attrs, fn) {
        //清除定时器
        if(element.timerId) {
            clearInterval(element.timerId);
        }
        element.timerId = setInterval(function () {
            //设置开关
            var stop = true;
            //遍历attrs对象,获取所有属性
            for(var k in attrs) {
                //获取样式属性 对应的目标值
                var target = parseInt(attrs[k]);
                var current = 0;
                var step = 0;
                //判断是否是要修改透明度的属性
                if(k === "opacity") {
                    current = parseFloat( getStyle(element, k)) * 100 || 0;
                    target = target * 100;
                    step = (target - current) / 10;
                    step = step > 0 ? Math.ceil(step) : Math.floor(step);
                    current += step;
                    element.style[k] = current / 100;
                    //兼容ie
                    element.style["filter"] = "alpha(opacity="+  current +")";
                }else if(k === "zIndex") {
                    element.style[k] = target;
                } else {
                    //获取任意样式属性的值,如果转换数字失败,返回为0
                    current = parseInt(getStyle(element, k)) || 0;
                    step = (target - current) / 10;
                    console.log("current:" + current + "  step:" + step);
                    step = step > 0 ? Math.ceil(step) : Math.floor(step);
                    current += step;
                    //设置任意样式属性的值
                    element.style[k] = current + "px";
                }
                if(step !== 0) {
                    //如果有一个属性的值没有到达target  ,设置为false
                    stop = false;
                }

            }
            //如果所有属性值都到达target  停止定时器
            if(stop) {
                clearInterval(element.timerId);
                //动画执行完毕  回调函数
                if(fn) {
                    fn();
                }
            }
        },30);
    }

    //获取计算后的样式的值
    function getStyle(element, attr) {
        //能力检测
        if(window.getComputedStyle) {
            return window.getComputedStyle(element, null)[attr];
        }else{
            return element.currentStyle[attr];
        }
    }
93月/170

Linux技巧:Vimdiff 使用

发布在 邵珠庆

在 IBM Bluemix 云平台上开发并部署您的下一个应用。

开始您的试用

源程序文件(通常是纯文本文件)比较和合并工具一直是软件开发过程中比较重要的组成部分。现在市场上很多功能很强大的专用比较和合并工具,比如 BeyondCompare;很多IDE 或者软件配置管理系统,比如Eclipse, Rational ClearCase都提供了内建的功能来支持文件的比较和合并。

当远程工作在Unix/Linux平台上的时候,恐怕最简单而且到处存在的就是命令行工具,比如diff。可惜diff的功能有限,使用起来也不是很方便。作为命令行的比较工具,我们仍然希望能拥有简单明了的界面,可以使我们能够对比较结果一目了然;我们还希望能够在比较出来的多处差异之间快速定位,希望能够很容易的进行文件合并……。而Vim提供的diff模式,通常称作vimdiff,就是这样一个能满足所有这些需求,甚至能够提供更多的强力工具。在最近的工作中,因为需要做很多的文件比较和合并的工作,因此对Vimdiff的使用做了一个简单的总结。我们先来看看vimdiff的基本使用。

启动方法

首先保证系统中的diff命令是可用的。Vim的diff模式是依赖于diff命令的。Vimdiff的基本用法就是:

# vimdiff  FILE_LEFT  FILE_RIGHT

或者

# vim -d  FILE_LEFT  FILE_RIGHT

图一就是vimdiff命令的执行结果的画面。

图1

从上图我们可以看到一个清晰的比较结果。屏幕被垂直分割,左右两侧分别显示被比较的两个文件。两个文件中连续的相同的行被折叠了起来,以便使用者能把注意力集中在两个文件的差异上。只在某一文件中存在的行的背景色被设置为蓝色,而在另一文件中的对应位置被显示为绿色。两个文件中都存在,但是包含差异的行显示为粉色背景,引起差异的文字用红色背景加以突出。

除了用这种方法启动vim的diff模式之外,我们还可以用分割窗口命令来启动diff模式:

# vim FILE_LEFT

然后在vim的ex模式(也就是"冒号"模式)下输入:

:vertical diffsplit FILE_RIGHT

也可以达到同样的效果。如果希望交换两个窗口的位置,或者希望改变窗口的分割方式,可以使用下列命令:

1. Ctrl-w K(把当前窗口移到最上边)
2. Ctrl-w H(把当前窗口移到最左边)
3. Ctrl-w J(把当前窗口移到最下边)
4. Ctrl-w L(把当前窗口移到最右边)

其中1和3两个操作会把窗口改成水平分割方式。

 

回页首

光标移动

接下来试试在行间移动光标,可以看到左右两侧的屏幕滚动是同步的。这是因为"scrollbind"选项被设置了的结果,vim会尽力保证两侧文件的对齐。如果不想要这个特性,可以设置:

:set noscrollbind

可以使用快捷键在各个差异点之间快速移动。跳转到下一个差异点:

]c

反向跳转是:

[c 1="</div>" 2="如果在命令前加上数字的话,可以跳过一个或数个差异点,从而实现跳的更远。比如如果在位于第一个差异点的行输入"2" language="</pre>"][/c]c",将越过下一个差异点,跳转到第三个差异点。

 

回页首

文件合并

文件比较的最终目的之一就是合并,以消除差异。如果希望把一个差异点中当前文件的内容复制到另一个文件里,可以使用命令

dp (diff "put")

如果希望把另一个文件的内容复制到当前行中,可以使用命令

do (diff "get",之所以不用dg,是因为dg已经被另一个命令占用了)

如果希望手工修改某一行,可以使用通常的vim操作。如果希望在两个文件之间来回跳转,可以用下列命令序列:

Ctrl-w, w

在修改一个或两个文件之后,vimdiff会试图自动来重新比较文件,来实时反映比较结果。但是也会有处理失败的情况,这个时候需要手工来刷新比较结果:

:diffupdate

如果希望撤销修改,可以和平常用vim编辑一样,直接

<ESC>, u

但是要注意一定要将光标移动到需要撤销修改的文件窗口中。

 

回页首

同时操作两个文件

在比较和合并告一段落之后,可以用下列命令对两个文件同时进行操作。比如同时退出:

:qa (quit all)

如果希望保存全部文件:

:wa (write all)

或者是两者的合并命令,保存全部文件,然后退出:

:wqa (write, then quit all)

如果在退出的时候不希望保存任何操作的结果:

:qa! (force to quit all)
 

回页首

上下文的展开和查看

比较和合并文件的时候经常需要结合上下文来确定最终要采取的操作。Vimdiff 缺省是会把不同之处上下各 6 行的文本都显示出来以供参考。其他的相同的文本行被自动折叠。如果希望修改缺省的上下文行数,可以这样设置:

:set diffopt=context:3

可以用简单的折叠命令来临时展开被折叠的相同的文本行:

zo (folding open,之所以用z这个字母,是因为它看上去比较像折叠着的纸)

然后可以用下列命令来重新折叠:

zc (folding close)

下图是设置上下文为3行,并展开了部分相同文本的vimdiff屏幕:

 

回页首

结论

在无法使用图形化的比较工具的时候,或者在需要快速比较和合并少量文件的时候,Vimdiff是最好的选择。

 

73月/170

大型网站架构系列:消息队列

发布在 邵珠庆

以下是消息队列以下的大纲,本文主要介绍消息队列概述,消息队列应用场景和消息中间件示例(电商,日志系统)。

本次分享大纲

  1. 消息队列概述
  2. 消息队列应用场景
  3. 消息中间件示例
  4. JMS消息服务(见第二篇:大型网站架构系列:分布式消息队列(二)
  5. 常用消息队列(见第二篇:大型网站架构系列:分布式消息队列(二)
  6. 参考(推荐)资料(见第二篇:大型网站架构系列:分布式消息队列(二)
  7. 本次分享总结(见第二篇:大型网站架构系列:分布式消息队列(二)

一、消息队列概述

消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。

目前在生产环境,使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ等。

二、消息队列应用场景

以下介绍消息队列在实际应用中常用的使用场景。异步处理,应用解耦,流量削锋和消息通讯四个场景。

2.1异步处理

场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种1.串行的方式;2.并行方式。

(1)串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。

 

(2)并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间。

 

假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是100毫秒。

因为CPU在单位时间内处理的请求数是一定的,假设CPU1秒内吞吐量是100次。则串行方式1秒内CPU可处理的请求量是7次(1000/150)。并行方式处理的请求量是10次(1000/100)。

小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢?

引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下:

 

按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。

2.2应用解耦

场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。如下图: 

传统模式的缺点:

1)  假如库存系统无法访问,则订单减库存将失败,从而导致订单失败;

2)  订单系统与库存系统耦合;

如何解决以上问题呢?引入应用消息队列后的方案,如下图:

 

  • 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
  • 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。
  • 假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦。

2.3流量削锋

流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。

应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。

  1. 可以控制活动的人数;
  2. 可以缓解短时间内高流量压垮应用;

 

  1. 用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面;
  2. 秒杀业务根据消息队列中的请求信息,再做后续处理。

2.4日志处理

日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下:

 

  • 日志采集客户端,负责日志数据采集,定时写受写入Kafka队列;
  • Kafka消息队列,负责日志数据的接收,存储和转发;
  • 日志处理应用:订阅并消费kafka队列中的日志数据;

以下是新浪kafka日志处理应用案例:

转自(http://cloud.51cto.com/art/201507/484338.htm)

 

(1)Kafka:接收用户日志的消息队列。

(2)Logstash:做日志解析,统一成JSON输出给Elasticsearch。

(3)Elasticsearch:实时日志分析服务的核心技术,一个schemaless,实时的数据存储服务,通过index组织数据,兼具强大的搜索和统计功能。

(4)Kibana:基于Elasticsearch的数据可视化组件,超强的数据可视化能力是众多公司选择ELK stack的重要原因。

2.5消息通讯

消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。

点对点通讯:

 

客户端A和客户端B使用同一队列,进行消息通讯。

聊天室通讯:

 

客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。

以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。

三、消息中间件示例

3.1电商系统

 

消息队列采用高可用,可持久化的消息中间件。比如Active MQ,Rabbit MQ,Rocket Mq。

(1)应用将主干逻辑处理完成后,写入消息队列。消息发送是否成功可以开启消息的确认模式。(消息队列返回消息接收成功状态后,应用再返回,这样保障消息的完整性)

(2)扩展流程(发短信,配送处理)订阅队列消息。采用推或拉的方式获取消息并处理。

(3)消息将应用解耦的同时,带来了数据一致性问题,可以采用最终一致性方式解决。比如主数据写入数据库,扩展应用根据消息队列,并结合数据库方式实现基于消息队列的后续处理。

3.2日志收集系统

 

分为Zookeeper注册中心,日志收集客户端,Kafka集群和Storm集群(OtherApp)四部分组成。

  • Zookeeper注册中心,提出负载均衡和地址查找服务;
  • 日志收集客户端,用于采集应用系统的日志,并将数据推送到kafka队列;
  • Kafka集群:接收,路由,存储,转发等消息处理;

Storm集群:与OtherApp处于同一级别,采用拉的方式消费队列中的数据;