Bhe's Blog

Bhe's Blog

为什么 php 这么流行

php 的流行已经不用再提及了, 那么, php 具有哪些优点使得它如此流行呢?

  • 接近 c(<5.2) 和 java (~>5.3.8) 的语法
  • 动态类型, 编码成本极低
  • 特定任务所需的基础库完备

优点 1 就不多说了, 用的人都知道。基础的 functionclass 语法都极其相似。
优点 3 涉及的话题很多,暂时不进行展开, 这里重点聊下 php 中的动态类型.

php 和 类型

先看一组普通的 PHP 语句:

1
2
3
4
5
6
class A{}; //定义一个简单的 class A
$a = 1;
$a = 1.1;
$a = "A";
$a = new $a(); //类的名字都是直接计算出来的
$a = function() { return 1; }; //你没看错, 这是匿名函数, php 中叫闭包。

从上可以看到的 php 语句的两个重要特点

  • 可以给一个变量名赋上随便哪种值,随便什么类型,什么时候,重复赋值也行。
  • 变量名以 $ 开头, 后面会聊为什么这里有一个 $

这一点让 php 开发者写代码的时候非常轻松。很多东西都不需要考虑。这也就是为什么 php 容易上手。

接下来是一个基础的函数定义代码:

1
2
3
4
5
6
7
8
9
function someFunc( $a = 1) { //一个带默认值的参数 $a
if ($a === 2 || $a === 1) { // int 类型的严格判断
return $a+1;
} else if (is_array($a)&& isset($a[0]) ) { // 数组类型
return $a[0];
} else {
return "$a is unknown";
}
};

这里我们看到, php 的动态性不仅体现在语句上, 函数的定义的参数和返回值也是没有类型的。 对于 php 而言, 返回值是 int 或者 void (在 php 里应该认为是 null) 类型, 在函数定义中没有区别,参数也是。

所以用 php 写出无法维护的代码是轻而易举的事情。

在实际使用过程中, php 开发者经常干的一件事情是把函数的返回值打印出来,看下数组结构, 找到自己需要的内容。再接着写代码.

默认情况下,php 的函数和类型都是全局的, 5.3 中引入的命名空间改善了这一点。

当然, 例子中的做法会让使用这个函数的人很头疼, 除非是非常常用以至于用户不可能误解的接口,否则别这么做.

jQuery 的 $() 是一个很好的例子,很多人认为是很反人类的设计.
但是显然它的用户觉得用起来很爽

代码中的 !empty($a) , 这个也是很常用的技巧, 用于判断一个变量是不是 null, 空字符串, False ,空数组, 等等等等(当然包括了变量不存在,数组下标和字段 key 不存在等等), 也就是直觉中所有的空值都会命中,很方便不是吗。

php 进行真假值判断时, 会尽可能地进行类型传唤,除了数字类型和 null, 字符串也会尽可能地转换成数字再转换到 true、false。甚至自定义类型,也可以通过实现特殊接口, 支持 empty 操作。

empty() 的陷阱

针对字符串类型进行 empty() 操作,比如 "0 ", 和 "0"empty() 中是不一样的, 虽然在 intval 函数中, 它的结果都是 0。 也就是, 只有 """0" 这两个特殊的字符串, 其他长度大于0 的字符串都会认为是非 empty 的。

empy 看起来像是一个普通的库函数, 但是它是一个特殊的指令, 并不是函数调用。类似于 echoisset。 这也留下一个 bug, empty 只能针对左值操作,也就是变量名.
而对于表达式,比如 empty("a") , 在目前的 php 语法中是不合法的, 必须提前计算结果并赋值给一个临时变量。 这个小小的麻烦,在未来的 php 版本中说不定会优化掉。

isset 和 empty 的区别

isset 只用来判断数组下标或者变量是否为 null 。它不能区分 null 和 变量未定义.
对于 $a=array( "a"=> null), isset($a["a"])isset($a["b"]) 都返回 false.

在 php 中, 不存在是一个模糊的定义,和 null 几乎是等同的。千万不要在代码中依赖一个变量是否定义这种模糊的条件, 这种奇葩的行为只有 javascript 才会关心, 毕竟我么谈的是动态类型语言, 存在感这种东西太弱了。

然而, php 从 c 中抄来的常量定义 define(“A”, a), 是可以用 defined(“A”) 进行判断的.
比如 defined("DEV_MODE") 判断应用是否运行在开发模式下.

关于 isset 和 empty 后续的面向对象章节中会谈到。

在静态类型语言的使用者眼里, php 大概就是一种到处都是 object 和 强制转型的及其不安全的语言了。

又爱又恨的数组

Array 应该是 php 使用频繁的基础类型了。 也就是它导致了 ide 几乎没办法正确地分析代码中函数返回值和参数的正确性, 因为一切行为都因为 array 的存在而过于灵活。

在 php 中, 数组和字典(或者说hashmap), 是混合存在的, 都叫 array。 这个混杂的类型,在处理 key 为数组的hash 数据时,很容易埋下 bug。

相比之下,javascript 还是严格地划分了 array 和 object 之间的区别。

使用 array 模拟 struct

在实际使用中, 针对需要返回复杂结果,但又不需要进行面向对象的包装时,习惯使用 array 做为返回值, 作为 struct 的替代用法.

1
2
3
4
5
6
7

function someFunc2() {
return array( "a" => 1, "b" =>2 );
}

$a = someFunc2();
$b = $a["a"];

或者使用 array 做为参数, 避免函数的参数过多带来的麻烦。相对于使用 call_user_func 模拟函数调用要更加自然方便。

$c = someFunc3( array( "a" => 1, "b" => 2));

这里应该说, php 的动态类型特别发挥地淋漓尽致, 虽然可能很多开发者并没有意识到这点, 但是不要紧,这并不妨碍他们用最舒适的方式使用 php。

php 开发者在写代码过程中, 心里只要了解一个函数、方法的作用,剩下的就是填充合适的参数和获取返回值,用 empty, isset 等函数进行判断是否空返回值。

PHP 和 $

和同样是动态类型的语言 javascript 相比, php 有什么特点呢?我能想起来的最显眼的区别, 就是 php 的变量名必须有一个 $ 符号。

当然,这不过是当年从 perl 遗传下来的。

那么这个 $ 符号, 除了必须写上,还有什么实际用途吗?

1
echo "$a is b";

哦, 原来如此。$ 在输出字符串的过程中用于定位字符串中的变量,在输出的过程中会替换为实际的变量内容。

对于 php 而言, “” 风格的字符串意味着需要进行变量的替换, 而 ‘’ 则说明直接输出内容就行了。
这个原则甚至适用与数组的 key .

如果如果变量名和后续内容没有空格等区分,那么 "${a}isb" , 通过加大括号就可以解决了。

这样连数组类型也支持了

echo "${a[0]}isb"

也就是

"$a is b" 等价于 $a . "is b"

注: 在 php 中, `.` 英文句号用于连接字符串.
其他语言中常用的 `+` 加号

总之, php 的约束很弱,可以很灵活地使用,也很容易失控,这也就是为什么 php 构建大型系统很困难。除了性能问题, 还存在对使用者的约束很弱。

但同时也存在好处, 动态类型和宽泛的约束,使得框架的实现上灵活, 可以轻松实现非常强大的特性,(后面在面向对象和类加载中会谈到)毕竟 php 开发者也习惯了基于约定编程的,只要团队不大,通过开发约定也可能解决这个问题。

1. 部分主要类型功能简介

org.apache.http.impl.nio.client.HttpAsyncClients

它是一个比较接近业务层的介面, 提供一系列工厂方法, 用于构造 启动对 HttpAsyncClient 的构造和配置工作

org.apache.http.impl.nio.client.HttpAsyncClientBuilder

工厂方法创建buidler 用于构造具体的client.

从属性可以看出来, 使用策略模式, 支持各种类型策略的注入, 线程工厂, http接受/处理回调函数等等, cookie/auth 等http协议的内容也在这个地方处理.

它的 build 方法生成 org.apache.http.impl.nio.client.CloseableHttpAsyncClient 的子类型

org.apache.http.impl.nio.client.InternalHttpAsyncClient

默认的 HttpAsyncClient , 使用之前需要调用 start 方法.

http 请求任务通过 execute 方法提交, 其实现原理是
生成一个fureture对象用作和用户沟通,
再生成一个 org.apache.http.impl.nio.client.DefaultClientExchangeHandlerImpl 实例, 由它启动和 io loop 的数据交换
最后把 future 对象返回给用户层

org.apache.http.impl.nio.client.DefaultClientExchangeHandlerImpl

InternalHttpAsyncClient 会默认调用他的start方法.

在 start() 中, exec.prepare 负责向 stat 中填充数据和 request 配置

requestConnection() 向 connection manager 发起 connection , 并向他注册了一个 future callback, 意味着, conn manager 会向 ExchangeHandler 回调事件, 从而间接地向用户层发起回调.

org.apache.http.nio.conn.NHttpClientConnectionManager

负责接管 client 提交的http请求

关于接受请求的方法签名如下

1
2
3
4
5
6
7
Future<NHttpClientConnection> requestConnection(
HttpRoute route,
Object state,
long connectTimeout,
long connectionRequestTimeout,
TimeUnit timeUnit,
FutureCallback<NHttpClientConnection> callback);
  • HttpRoute 和请求的 ip 相关, 有一个相关的参数 MaxRequestPerRoute, 意思就是对同一个主机发起的最大连接数

  • connectTimeout tcp 等待建立 connection 的等待时间, 对方主机的tcp通道建立需要一定的时间

  • connectionRequestTimeout 表示排队等待 connection 的时间具体参考下文的 lease time

    ps1: connection 可以认为是一个保持着的长连接资源, 也就是 tcp 通道
    ps2: route 里面如果配置了 proxyhost(http代理), 那么这时候就起作用了

释放请求 releaseConnection, 这是真的将一个 connection 实例从 pool里拿出来, 中止链接, 回收实例

所以它完成的事情也不多, 就是向 pool 里注册和释放 connection

org.apache.http.impl.nio.conn.CPoolProxy

上文提到的 pool , 连接池代理,通过 lease 方法注册新的请求

  1. 首先 创建一个 org.apache.http.nio.pool.LeaseRequest#LeaseRequest 实例, 持有传入的参数

    • 上文提到的 connectionRequestTimeout , 这里叫 lease time , 也就是 lease request 的 dealine
  2. 在 processPendingRequest 中, 检查deadline ,如果到了, requestTimeout 触发, 触发TimeoutException (注意类型)

    • 也就是说, lease request 创建完毕, 发现已经到dealine ,那么就不向 pool 索取 connection 了, 直接退出
  3. 接着向 routeToPool 和 route 对应的 connection pool (org.apache.http.nio.pool.RouteSpecificPool) , 如果没有的话, 发起一个新的pool.

  4. 接着从 pool 中索取一个 connection (org.apache.http.nio.reactor.SessionRequest) , 如果没有, 创建新的 pool , 将 socketTimeout 配置进去

    创建新的connection , 需要检查route对应的connection数量是否达到上限.
    对于拿到的新的 connection, 将上文所提到的 requestTimeout 作为 connection 的 connectionTimeout.

org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor

nio 反应堆的默认实现, 继承于 org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor , 负责发起worker线程, 并维护和 Nio api 之间的交互

worker 的个数在创建 conm 的时候决定.
默认配置中通过 `java.lang.Runtime#availableProcessors` 决定,
也就是 jvm 检测到的当前可用处理器数.

总结和体会

关于实践

  1. 不应该在callback中使用耗时的操作, 这会导致阻塞io线程, 从而降低io部分的工作效率, 成为系统瓶颈.
  2. 实际使用中发现,保持长链接的情况下, 存在由于链接到期断开导致刚刚放入连接池的新链接瞬间断开的情况, 这种情况需要按需重新发起链接. 我这边统计到的发生率不超过 0.1%.

买来平时写代码用的, archlinux+kde+vim下开发 php 主要应用, 偶尔用用phpstorm和pycharm 看看其他不经常维护的项目.

前几天在这个搜索下6x键机械键盘, 看来看去感觉 poker2 的感觉比较好, 就直接去购了台茶轴的. 以前用的 das, 感觉玩游戏还行, 就是有点吵人, 后来不玩游戏就送人了.所以工作的参考虑下其他人说法, 估计茶轴应该比较合适.

这两天用下来感觉还不错. 手感不错, 相对于笔记本的巧克力键盘稍微键程大了一点点, 熟悉之后也没什么不舒服的.

回到键位的配置上吧, 第一个碰上的问题就是 esc 键, 问题在于 ~.

现在要按 shift+fn+esc, 稍微有点困难. 绑定到 pn+esc , 总算稍微方便点.
理论上最好的键位应该 shift+esc 贴近原来的使用习惯, 毕竟 ` 很少用上.

bash 的 ` 反正可以用 $() 替代. 另外通过disp改右 ctrl 为 ~, 但是感觉用着比较别扭.

第二问题就是熟悉 fx 改成fn+数字. 平时倒没啥问题, 就是 kwin 的一些快捷键多少会用上 fx 键, 比如 alt+f2. 或者ide里的一些调试工具, fx 还是挺常用的, 用的时候总要稍稍翻译一下.

And, 方向键, 虽玩游戏喜欢用 wasd, 但是用 fn+wasd 操作方向键的方式, 需要适应一下. 通常第一反应还是右手去按右下的按键. 虽然是一个很简单的操作, 着实是成了习惯成自然了.

买的时候附送了6个无刻彩色键帽, 我换掉了 fn win pn, 平时操作的时候比较视觉上方便定位.

毕竟是拿来干活的工具, 暂时没太折腾.

这里对发布会的怨念很大, 发点正能量

这次的 WWDC, 我个人感觉 Apple 是拿出了诚意了, 只是还没正式发布, 结果还不知道怎么样.

大学的时候打工买了台 milestone 2 , 对 android 失望得不得了, 后来就一直是用着 iPhone. 再后来年会中了台 N4, 玩了几天, 发现 android 4.x 是稍微好了点, 但是也没什么吸引力, 就送人了.

我曾经对比过 a 和 i 的区别, 后来我觉得原因就是环境.

iphone 有 App Store 和 iTunes, android 有什么呢?

我个人感觉, iPhone 并不是完美无缺才超越了 Android, 相反, 它很多东西没做好. safari 很一般, App 都是沙箱里孤立的, 记事本之类的系统应用做得也不是太舒服.

在 IOS 8, intent 和 第三方输入法反映了苹果确定在一定程度开放了它的这个操作系统. 但是这是一件顺其自然的事情,

回头说说安卓? 我觉得它早早走上一条不归路, 过于开放, 乱七八糟. 以至于我都不知道怎么用它了.

大排档和高级餐厅的差别, 并不只是相差在价格上, 而是体验, 这时候解决的已经不是温饱问题. 对于一个只需要吃饱的人, 大排档够了, 但瞎 BB 酒店太贵这种心态可不少见.

说实在, 我觉得问题在于, 我希望的手机是好用, 又不费神. 毕竟在这东西花时间又不是工作内容, 没啥回报. 自从买了 kof-i 之后我就再也没买过任何的游戏了, 我也不觉得 iOS 平台能产生游戏性超过任天堂或者索尼的游戏, 就算哪天它把任天堂收购了, 那又怎样.

我只当它是手机, 能胜任移动助理功能, 至于其他的, 已经超出了它所应该拥有的意义了.

phpdaemon, 基于异步网络请求的应用服务框架

一直以来, php给人的印象就是一个单进程同步的编程语言, 其魅力在于简单, 直观,
学习成本低, 容易上手, 所以市场占有率很高.

流行的php模式, 从几年前的 apache + mod-php, 到今天的nginx + php-fpm,
在php编程层面上都是一直的.
开发者的开发的代码都是居于”php进程会同步执行所有的代码逻辑之后退出并释放”
这么一种假设, 不用考虑多线程的锁问题, 大多数情况下也不用考虑内存泄露.
虽然实际情况下, php进程受上层管理界面的影响, 并不是像 cli
模式那样执行一次并退出, 但对开发者而言, 其中细节可以忽略.

回到 phpdaemon 本身吧,
phpdaemon是一个在event和eio两个pecl扩展之上开发的异步网络框架, 类似于 python
中的 tornado , 它采用的单进程单线程异步IO的网络模型,
可用于开发异步非阻塞的应用.

前面说 phpdaemon 是单进程单线程的网络模式, 严’意义上讲是指它的worker. phpdaemon
以 master + workers 方式运行, master只负责分发任务, 在worker
进程才进行请求内容的处理工作.

在 phpdaemon 中, 一个进程中会启用多个服务实例, 每个实例对应一个 appinstance.
开发者将不同的处理逻辑封装成为不同的 app, 在异步调度中各司其职.

master 在接受到请求之后, 向 AppResolver 发起查询, Resolve 根据上下文,
返回一个对应的应用实例名称. 比如需要返回一个静态文件, 那么可以返回一个
FileReader 实例, 实例的Request 方法中完成异步并向客户端输出文件内容.
而最常见的任务是返回一个动态页面的内容, 这种类型的任务, 在 phpdaemon 中抽象为
HttpRequest 类型. 一个常规cms中的前端和后台界面,
可以分装成两个不同的appInstance, 在resolver实现对应的判断逻辑,
正确路由到对应的appInstance上, 然后再决定发起哪一个 Request 类型.

这种关系有点像 MVC 框架中的C. 大部分php web 框架的 controller
中会包含多个action, 在 phpdaemon 中, PHPDaemon\Core\AppInstance
可以认为是controller, 而action则对应着 client\Request.

为什么这里要分成 AppInstance 和 Request 两种类型呢?
这里涉及到并发环境中的内存管理问题. 常规的php应用是同步的, 从来不考虑这个问题.
一个实例最终会被释放, 多个请求之间并没有共享关系. 但是在phpdaemon中,
并不是这么回事. worker进程一段时间内一直驻留内存, 需要管理每次请求中用到的资源.

服务器的配置是不变的, 可以常驻内存. 对下层接口的调用结果, 不同任务之间是独立的,
所以任务完毕后应该释放掉.

所以, 在并发环境下, AppInstance 不断生成新的 Request
对象处理任务并存储在队列当中, 而 Request 在进行网络请求的时,
会交出cpu并进入休眠, 等待io事件将自己唤醒. 当 Request 完成任务并返回结果,
AppInstance 将结果返回, 并将该 Request 实例从内存中释放.

上文提到的 Request , 并不是一般认为的 网络请求, 而是对应负责处理网络请求的
Service. 那么应用的网络请求又是如何发起的呢?

这里先解释下 phpdaemon 中的 pool 和 connection.

Connection, 顾名思义, 就是php代码中发起的异步网络请求, 比如常见的 curl.
在异步请求中, 应用发起很多网络请求, 通过事件进行调度.
所以同一时间存在多个网络请求, 只不过有的是活跃状态, 有的挂起等待后端返回.
存储和管理这些 Connection 的负责人, 就是 Pool. 这就是常说的连接池.

Java语言中的连接池, 一般通过多线程实现的. 而 PhpDaemon 中的连接池,
则是在libevent上实现的单进程单线程异步连接池.

Vim, 老生常谈的话题了.

the editor

在程序员之间谈编辑器是一个敏感话题, 会有很多人跳出来挑起圣战, 还是蛮头疼的一件事情.

从Vim开始, 是一种有别于IDE的编码方式, 但仅仅是方式, 就像是拿叉还是拿筷子, 取决于你吃西餐还是中餐.

无论使用的是文本编辑器还是 IDE , 都是服务于编写源代码这么个目的. 它们之间的区别在于, 前者在编码中与环境互动, 后者则是提供一个既定的环境完成编程. 相同的是, 两者都会不断改善.

过程有所不同, 但目的是相同的. 深究下去也就是比较赛百味和麦当劳的口味罢了.

Vim 提供了两个基本的功能框架, 构成了其复杂的生态环境.

首先是模式切换.

功能繁多的键位操作, 基于 NormalInsert 两种模式的交替进行. 正是模式这个特性, 使得vim有别于其他以组合键方式进行操作的编辑器(or IDE). 在Normal模式下进行文本操作, 在Insert模式下进行文本输入, 并且可以通过:map系列命令简化和扩充可用的操作.

除了 hkjl asdcx 这些简单的移动和crud, vim 还支持按键序列的组合.

  • 删除一个单词? de
  • 删掉10行? d10j
  • 清空整个文件? ggdG

是不是有种玩街霸拳皇搓招的快感呢 XD

还有基于文本对象(text-objects^[1])的操作, 比如 yi( 表示 yank inner () block, 按下 p , 那么屏幕上将复制一份()里面的文本. 如果换成 ya(, 则表示包含(整块文本.

1
2
(text) => (text)text
[text] => [text]text

^[1]: 更多内容见 :h text-objects

此之外, 还有commandvisual, 分别用于输入命令,和可视化文本选择.

比如需要一个命令的输出, 比较常规的方法就是去shell里面执行再重定向到文件, 然后通过vim打开进行合并. 可是 vim 支持!直接执行shell命令, 再加上r命令重定向到当前文本就行了. 举个例子, 当前系统时间可以这么拿到.

1
:r!date

至于visual mode, 举个最简单例子吧, 要删除一段文本需要怎么操作? 按下v,
进入visual模式, 这时候通过hjkl移动, 经过的所有文本会处于高亮状态,
这时候把它们当成一个字符, 所有针对单个字符的操作都可以应用上去, 比如按下d,
那么这段文本就被删除掉了.

但是如果需要插入文本咋办? 这时候就要使用强大的blockwise-operators,

如果要删除每一行的第一个字母, 按下<CTRL-V>, 高亮需要删除的内容, 再按d删除.

如果要在每一行的行首插入, 按下<CTRL-V>, 高亮内容, 再按I, 输入一段文字, 再按<Esc>, 这时候每一行都会插入对应的字符.
完整的操作是 I{string}<Esc>.

这些操作在处理多行数组相当方面, 省去了写正则的麻烦.

再提一个有趣的例子, 通过vim码字的时候, 常常遇到一个问题就是换行.
经常打了大段文字懒得敲回车. vim中有一个选项, 叫textwidth, 当文本超过一定字符,
会自动折行. 配合visual模式, 对高亮文本按下gq, 瞬间自动格式化完毕.

更多操作, 见 :h visual-operators

第二, vim script

前面提到的command模式, 就是vim脚本的交互式shell. vim丰富的扩展功能, 也就是
plugin , 一个又一个脚本, 放置在不同的目录中, 经过不同的顺序加载实现的.

值得一提的是, vim 支持通过 python, lua 等语言的解释器, 编译的时候加上对应选项, 就可以在vim脚本中调用额外的python或者lua脚本, 通过这些语言的lib, 是的vim更加强大了.

由于vimscript相当复杂, 这里就不介绍了, 顺便说说plugin的管理和推荐.

常规的安装流程, 就是把脚本一股脑复制到 ~/.vim 目录中去, 个个插件的 autload plugin 目录下文件会混杂在一起, 管理复杂, 想删都删不了.

这里推荐使用, pathogenvundle 进行组合管理.

pathgen和 vbundle 是两个类似的插件管理器, 区别在于 vbundle 支持配置自动去github下载脚本, 省了不少同步的麻烦.

并非所有的vim插件都支持通过 github 下载, pathogen 就派上用场了, 虽然它只支持手动管理. 将插件所有内容解压到 ~/.vim/bundle/{plugin name}/, pathogen 在vim启动过程中会自动加载所有的插件.

这里附上我的配置文件内容.

1
2
3
4
5
6
7
8
9
10
11
12

if version > 703

let g:pathogen_disabled = [
\,"conque_term"
\]
call pathogen#runtime_append_all_bundles()

call vundle#rc("~/.vbundle/")
source $HOME/.vim/conf.d/vundle.vim
endif

通过pathogen加载vundle和其他非github插件, 再通过vundle加载其他的插件.

将vundle的 bundle 目录放置于 ~/.vim 之外, 好处是可以直接将 整个 vim 目录软链到网盘目录里, 随时保持同步, 而 vundles 则手动更新.

推荐下我自己在用vunle清单, 也就是上面的 $HOME/.vim/conf.d/vundle.vim 的内容.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
" 视觉系
Bundle 'Lokaltog/vim-powerline'
Bundle 'altercation/vim-colors-solarized'

" 文件搜索 编码转换
Bundle 'kien/ctrlp.vim'
Bundle 'mbbill/fencview'
Bundle 'rking/ag.vim'

" buffer, 目录, 任务, tag浏览
Bundle 'scrooloose/nerdtree'
Bundle 'fholgado/minibufexpl.vim'
Bundle 'majutsushi/tagbar'
Bundle 'vim-scripts/TaskList.vim'

" 代码补全, 生成
Bundle 'SirVer/ultisnips'
Bundle 'Shougo/neocomplete.vim'
Bundle 'scrooloose/nerdcommenter'
Bundle 'DoxygenToolkit.vim'
Bundle 'Rip-Rip/clang_complete'

" 语法检查, 调试
Bundle 'scrooloose/syntastic'
Bundle 'joonty/vdebug'

" 其他
Bundle 'msanders/cocoa.vim'
Bundle 'gmarik/vundle'
Bundle 'L9'

总的来说, vim 学习成本比较高, 针对不同语言需要熟悉并定制, 才能拥有对应的功能.
我同时也用phpstorm, 对于那些觉得不错的功能, 会考虑在vim中寻找相同的功能的plugin.
语法检查, 补全这些有限支持也就够了, 重构功能也就是偶尔体验体验.毕竟, 这是用工具编码, 而不是工具教你如何写代码.

写代码的时候, 开启终端, 打开vim, 调用一下shell.
只想浏览代码的时候, 我还是回开下 phpstorm , 配置好库和环境, 通过鼠标穿梭于文件之间.

至于两者的差别, 呵呵. 物尽其所用罢了.

zsh 已经不是什么新鲜事了, oh-my-zsh 相信很多人都已经在用了.

zsh 存在一个明显的问题, 它并不是什么开箱即用的工具, 需要大量配置, 也很难上手扩充新功能, 这点和vim非常相似.

常用的 alias 和 shell script, 虽然能用上些稍微方便的特殊语法, 大部分仍和 bash 相近. 这时候倒不如使用bash的语法, 保证代码的兼容性和可移植性.

本文简单介绍如何编写zsh的补全插件, 以 Mac OS 的 launchctl 为例.

`launchctl` 是 Mac 用于管理系统运行, 类似于 linux 的 systemd, 用于管理 LaunchAgent 加载, 是 `launchd` 的前端.

常用的 subcommand 有 unload, load, stop ,start 等等. 这里就只考虑 load 和 unload 补全功能扩充.

怎么开发脚本

按作者的话说, 看文档是很难学会的. 因为很少涉及脚本编写本身, 仅罗列了所有的接口.

基本内容

首先将脚本命名为 _launchctl , 放到任意目录下, 让将目录加入 fpath

oh-my-zsh 用户保存在 ~/.oh-my-zsh/plugins/launchctl/_launchctl.
并在 zshrc 中声明 plugins=( launchctl )
剩下的事情就交给 oh-my-zsh

脚本头部加上以下内容

1
#compdef launchctl

第一行标记这个文件包含一个自动加载的函数.
本质上这是一个没有形如 function () {} 包裹着的函数体,
和可以直接通过 source 导入的配置文件和脚本是不同的.

subcommand 补全

subcommand 应该是子命令或者副命令的意思, 这里保留原文。

ps: 对于 gnu 风格的命令行工具, 很少有 subcommand , 这一章节并不是必要的.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local -a _1st_arguments

_1st_arguments=(
"load:Load configuration files and/or directories"
"unload:Unload configuration files and/or directories"
"start:Start specified job"
"stop:Stop specified job"
"help:This help output"
)

local label_subcommand="launchctl subcommand"

if (( CURRENT == 2 )); then
_describe -t commands "$label_subcommand" _1st_arguments
return 0
fi

这段代码还是比较好懂的, 补全所有的subcommand并给出提示信息.

注意 CURRENT 这个变量, 它标记处于命令行的第几个单词

关于 CURRENT , 见 `man zshcompwid`

在按下tab之前, 用户输入的文本为 launchctl<空格><tab>, 第一个单词是主命令launchctl, 显然在这段脚本中, CURRENT 是一个大于2的整数.

对于还存在更下一级 subcommand 的补全, 以此类推.

文件补全

launchctl unload 用于关闭一个正在运行的服务, 比如nginx, php-fpm等等, 显然这里需要补全的是一个文件路径, 对应一个位于 ~/Library/LaunchAgents/ 下的 plist 文件.

首先假设已经实现了对应的工具函数 _get_loaded_user_plists , 这个函数将生成目前正在运行的plist文件列表

以下是关键代码

1
2
3
4
5
6
7
8
9
10
11
12

local expl

case "$words[2]" in
unload)
local loaded_user_plists
loaded_user_plists="$(_get_loaded_user_plists)"
_wanted loaded_user_plists expl 'running jobs' compadd -a loaded_user_plists
;;
esac

return 1

可见对于简单参数的补全实现是很轻松的

zsh 如果发现补全函数返回0, 会将输出作为补全内容作为候补内容输出到终端中去.

因此我们只需要确定 subcommand 是我们所预期的unload , 将文件列表发送给用户就行了.

这里用case 根据 subcommand 的不同, 选择对应的补全形式.

首先注意 $words 这个变量, 通过打印该变量可以发现, 它是一个将用户已经输出的命令拆分的数组

比如输入 launchctl unload , 那么对应的words将是 ( 'launchctl' 'unload' '' ), 最后一位是一个空字符串.

严格地说, 这里拆分依据并不是空格
如果你明白 $@ 和 $# 区别, 你就懂我啥意思了. 
这里空间不够, 我就不写了 :)

调试脚本

开发过程中难免写错, 需要重复调试, 这里有两个小技巧.

  1. echo

echo 当然是喜闻乐见的 debug 指令之一了.

  1. set -x

set -x 将开启zsh的 XTRACE 选项, 所有运行脚本背后的指令和参数都直接打印到屏幕, 俗称上帝视角.
如果发现不能正常执行, 那么只要运行 set -x, 手动输入命令后按tab.

ps1: set +x 恢复正常模式.
ps2: 千万不要手残提前触发其他zsh的特性, 不然面对满屏的文本里就没啥心情继续下去了. 这里需要老老实实输入完整的命令, 在需要触发对应补全过程的地方按下tab触发补全.

由于调试信息实在太多了, 运行几次之后可以把调试窗口关了, 或者将终端的buffer清空, 总而言之, 减少输出的log以快速定位问题.

至于更细节的内容以及相关api文档, 请看手册 `man zshcompsys`
zsh 的~~破手册~~跟天书一样的文档也没打算解释清楚, 我能说的只有这么多了
更多技术细节请自行深挖.

Job done!

近期花了不少功夫在优化线上代码构建方案上, 过程中的一些心得会不定期更新.

先前的做法是发布之后调用一个yii的扩展, 生成Classmap文件, 包含了类名和绝对路径的映射关系.

这么做的好处自然是在 autoload 上最大限度地减轻消耗, 每个类加载 autoloader 仅仅需要检查一下 classmap , 剩下的工作交给 apc 就行了.

最近在进行 composer 迁移的过程中, 发现composer其实自带了这么一个功能: dump-autoload, 而且生成的 ClassMap 文件在运行时通过路径拼接, 而不是用相对路径, 同样不会影响到 apc 的缓存功能.

实现这个功能仅仅需要以下改动

composer.json

{
      "autoload" : {
              "classmap" : [
                          "src/components",
                          "src/controller"
               ]
      }
}

spec文件中加入

BuildRequires: t-search-composer-cli
/opt/tsearch/bin/composer dump-autoload

在应用初始化过程加入类似以下步骤

$classMap = require "vendor/composer/autoload_classmap.php";
Yii::$classMap  = array_merge( Yii::$classMap, $classMap);

对于已经用上composer的项目管理依赖的情况下, 这种做法自然不是需要的, composer 的 install 指定会自动完成 classmap 生成工作.

但是如果不需要依赖 composer 进行依赖管理的情况下, 通过 dump-autoload 指令来提供可用的 classmap, 简单而又效果显著.

Composer 是什么?

按官方文档

Composer 是一个 PHP 的依赖管理工具. 它可以自动安装项目中声明依赖的库.

为什么要使用 composer , 而不是传统的 pear 等?

Compoesr 是一个类似于 ruby-rubygems, nodejs-npm, python-pip 的依赖管理工具. 相比起pear简单的安装功能, Composer 注重包之间的依赖关系, 每个项目可以独立管理自己的依赖, 而不是一股脑全部安装上. 另外也解决开源组件混乱的 autloader.

当前的 php 框架社区中活跃成员(比如 Symfony, Slim, Laravel, 以及开发中的 Yii2) 纷纷支持 composer , 旺盛的生命力已经不是 pear 之类的老掉牙的社区方案可以比拟了.

相对于 ruby, python, nodejs, php总算有了一个相对完善并且流行的包管理器.
目前社区和开发者之间的距离还比较大. pear, pyrus, composer 会继续并存下去, 给 php 开发者带来更多的麻烦.
长期而言, 打个你死我活, 最后只剩一个是符合开源社区的历史发展规律的 :)

简单上手

安装具体步骤参考 http://getcomposer.org , 简单运行一下命令

curl -sS https://getcomposer.org/installer | php

最新版本的 composer.phar 就下载到本地了.

相比起 npm pip 等被大部分发行版内置或者仓库支持的包管理器, 这种自己手动下载方式相对不够友好, 毕竟 composer 的知名度暂时还追不上 pear 等老牌工具, 被周边社区还需要一定的时间.

常用命令官方站点上有简(jian)洁(lou)的文档,

install
search
show
update
create-project

按前面的方式下载之后, 使用composer 需要用 php composer.phar help 这样丑陋的方式, 嫌麻烦可以手工移动到phar到 $PATH 对应的相关文件夹中去, 顺便改名为 composer 会顺眼一些.

注意文件的可执行权限, (chmod +x composer)

用法是比较简单的, 关键是发布者和用户注意用 composer.json 准确地描述相关的依赖和加载细节.

依赖入门

先给一份简单的例子

  • 一个简单的工具集, 版本号 1.0.0
  • 依赖 net/http , 用来处理http请求, 而不是直接用 Curl
  • 开发模式下依赖 phpunit, 方便运行单元测试
  • 使用 psr-0 规范的加载方式, 源代码位于 src 目录中, 将 Utils 映射到该目录
composer.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"name" : "example/utils",
"description" : "utils",
"version" : "1.0.0",
"type" : "library",
"license" : "proprietary",
"authors" : [
{
"name" : "Mr Right",
"email" : "[email protected]"
}
],
"autoload" : {
"psr-0" : {
"Utils\\" : "src/"
}
},
"require" : {
"net/http" : "*"
},
"require-dev" : {
"phpunit/phpunit" : "*"
}
}

例子包含了一个包信息中常见字段, require 不一定需要, 但这是依赖管理的精华所在, 后续章节再仔细说明.

刚开始可能对 composer.json 的语法不太熟悉, 可以通过 composer validate 检查自己书写风格是否符合规范

文件目录结构

还是例子先行

trunk/
    src/
        Utils/
            NetWork/
                WangWangMsg.php
    
    composer.json
    
    phpunit.xml
    
    tests/
        bootstrap.php
        Utils/
            Network/
                WangWangMsgTest.php
       

composer 提供了灵活 autoload 机制, 文件的摆放方式可以很灵活, 但过分奔放的 style 不是好事. 以上结构参考自 Monolog, 一个强大的日志工具, 也考虑到了phpunit的使用.

推荐将 phpunit 也作为依赖之一加入 require-dev , 方便开发调试. 与 require 字段不同的是, 只有显式使用 composer install --dev 才会安装上 require-dev 的相关组件.

对于用户而言, 使用第三方包, 最不希望的应该就是牵扯进 autoloader 的细节. 以往的 php 源码包都有自带autoloader 的毛病, 复杂的 autoloader 交织在一块往往给使用者造成麻烦. 相比之下 composer 使用统一的方案, 优雅地解决了这个难题.

autoloading 与 命名空间

composer 目前支持三种 autoloading 方式

  • psr-0 根据 psr-0 规范命名空间和文件路径进行类加载, 最佳实践方式.
  • classmap 生成类与文件的映射关系文件, 安装时自动扫描并生成.
  • file 可以理解为预先加载指定文件, 每次请求中都会执行

这里简单说说 psr-0, 参考 utils 这个包的配置文件

composer.json
1
2
3
4
5
"autoload" : {
"psr-0" : {
"Utils\\" : "src/"
}
},

在安装之后 composer 生的classmap 信息文件之一, vendor/composer/autoload_namespaces.php 中可以看到以下代码

1
2
3
4
5
6
<?php
return array(
'Utils\\' => array($baseDir . '/src'), // HERE!
'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'),
'Net_' => array($vendorDir . '/net/http/src'),
);

可见进行相关类的自动加载是, composer autoloader 通过查表和路径搜索的方式, 正确地找到类相关的代码.

比如一下代码

1
2
3
<?php
new Utils\Network\WangWangMsg();
//or new Utils\Network_WangWangMsg();

实际运行时, 实际运行的逻辑类似于以下的伪代码

test.php
1
2
3
4
5
6
<?php
//composer
$baseDir = $root . 'vendor/example/utils';
include $baseDir . '/src/Utils/Network/WangWangMsg.php';

new Utils\Network\WangWangMsg();

项目中使用 composer

在 composer 设计理念中, 项目和包并不是两种割裂的定义, 所有的源代码都归属于对应的包, 包与包存在依赖关系.
虽然在实际业务开发中, 暂时没哪个项目如此纯粹, 但这并不影响 composer 的应用.

以下是一个简单的搜索结果页应用如何书写 composer.json.

composer.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"repositories" : [
{
"packagist":false //禁用外部仓库
},
{
// 引入内部代码仓库(svn)
"type" : "svn",
"url" : "http://.../mido",
"branches-path" : false,
"tags-path" : false
}
],
"require" : {
"mido/mido":"dev-trunk" //使用 svn 中的 trunk 分支
}
}

运行 composer install 时, composer 预先扫描所有的 repositories, 将 require 字段中的包更新载入到 vendor 目录中去, 并生成相关缓存信息和自动加载代码.

然后呢?

在应用 bootstrap (对于大部分项目, index.php 可能是一个更加容易理解的名字)代码中加入以下代码

index.php
1
2
<?php
require /path/to/root/vendor/autoload.php

接着项目代码中尽情使用吧, 毕竟自动加载类的细节都已经交给 autoloader 了.

一个例子

这里介绍怎么使用 composer 作为 yii 1.x 的依赖管理工具. (注意, yii1.x 不符合 psr-0, 直接使用 composer 比较困难)

安装好了依赖, 接下来讲解怎么处理利用代码. 这里先用yii自带的demo helloworld 作为例子.

复制文件(yii/ 目录下存放最新的yii 1.x 代码):

cp -r yii/demos/helloworld ./
cp -r yii/framework helloworld/protected/

目录结构如下:

helloworld/
    index.php
    protected/
        composer.json //composer 配置信息
        framework/  //yii 框架文件
        vendor/     //composer安装依赖的位置
composer.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"repositories" : [
{
"type" : "composer".
"url" : "http://packages.phundament.com"
}
],

"require" : {
"php" : ">=5.3.12",
"yiiext/migrate-command" : "0.7.2",
"psr/log" : "1.0.0",
},

"autoload" : {
"classmap" : [ "framework/YiiBase.php", "framework/yii.php" ]
},

"config" : {
"vendor-dir" : "vendor"
}
}

index.php 使用以下代码替代原来的 yii.php

composer.json
1
2
<?php
require "protected/vendor/autoload.php";

将 framework 和 composer.json 放在 protected 文件中, 方便通过 webserver 访问规则屏蔽代码文件. 文件结构可以根据实际需求做调整, 这对于 composer 来说都这可以正确找到代码.

接下来 进入 protected 目录中 运行 composer install -vvv , 更新composer信息, 生成 classmap.

php -S 127.0.0.1:8080
        

应用就跑起来了.

主搜索应用前端公共库 mido 改造实战

mido 是我们使用一个公共包, 负责了大部分引擎请求的细节, 在业务代码中屏蔽了请求, 解析等细节.

1. 规范svn路径

严格区分trunk, tag, branches. composer 对于svn的支持相对弱一些, branches 和 tag 都可以别名, 或者禁用, 但是 trunk 好像必须要有.

目前 mido 只有 trunk 一个目录, 并没有严格按版本号发布, 所以项目的 require 字段填上 "mido/mido" : "dev-trunk" 就行了.

/trunk
/branches/2.0.1-beta
/tag/1.0.0

2. 源代码命名空间处理1 : 用 \ 替换 _

1
2
<?php
Mido_Exception -> Mido\Exception

严格意义上说, 这并不是必须的. composer 支持 A_B_C 这种常规的写法, 在配置中写上 "Mido_":"" 也行

3. 源代码命名空间处理2 : 命名空间导入

ps: 需要特别注意全局命名空间里的类名,包括不支持命名空间的库和扩展, 如ArrayAccess, Memcache

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace Mido\Engine;
use Mido\ComponentBase;

class Kingso extend Base {}

namespace Mido\Engine\Kingso;
use Mido\ComponentBase;
use Mido\Engine\RequestBase;

class Request extend RequestBase implements \ArrayAccess {}

当然, 以根命名空间的方式书写代码也是可以的, 看团队如何制定代码规范.

3. 源代码命名空间处理: 保留关键词

前缀式命名类没有和保留关键字冲突的问题, 但改用命名空间时, 在当前域需要注意扩展和内置提供的全局class, 虽然丑了点, 但是结果是好…

mido 中内置了部分常用的 helper, 比如以下的 ‘a.b.c’ 的多维数组访问 helper. 原来使用了 Array 这个关键字(呃, 因为 php 不关心大小写)

1
2
3
4
5
6
<?php
//old style
Mido_Helper_Array::get($items, 'items.title');
//new style
Mido\Help\MArray::get($items, 'items.title');

4. 组件别名配置更新格式

为了满足多个相同引擎单例组件的使用, 配置文件里定义了映射关系, 偷懒直接写上根命名空间.

1
2
3
4
5
6
<?php
//修改前
"kingso" => "Mido_Engine_Kingso",
//修改后
"kingso" => "\\Mido\\Engine\\Kingso",

嗯, 全部的迁移成本如上.

总结

好处

  • 方便资源共享, 方便代码分发
  • 项目源代码结构清晰
  • 自带autoloading机制, 简化加载流程
  • 社区类库支持强大
  • 与国际接轨, 高端大气上档次 :)

使用成本

  • 独立执行文件, 暂时没有比较好的工具集成, 暂时没见那个发行版内置.
  • 对于频繁改动, 代码耦合严重的应用, 并没有明显成效.
  • 需要遵守目录结果规范
  • 支持还不够广泛, 许多第三包需要定制安装

vim:ft=markdown

通过 Aur 安装相关包

完整安装 longnen qq 需要以下几个包

  • wineqq 主要包, 包含完整的 longene 环境和 qq 2012
  • opendesktop-fonts 提供 odokai 字体, 用于中文字体显示, 不装可能导致方块字
  • ttf-ms-fonts 提供 arial 用于中文显示

装上了就能跑了, 基本功能都没问题.

可能遇到的问题

英文半角符号显示错位

字体问题, 未解决

没有声音

需要安装声音相关的包, 64位系统装上第一个, 32位选择第二个.

  • lib32-alsa-plugins
  • alsa-plugins

64 位系统需要启用 multi 仓库才能找到 lib32 相关包, 具体操作见 wiki:multilib

简单说, 就是找到 /etc/pacman.conf, 找到以下文本, 取消注释(如果没有找到就手动加入)

[multilib]
Include = /etc/pacman.d/mirrorlist

fcitx无法输入

找到 /opt/longene/qq2012/qq2012.sh, 将其中的

export LANG=zh_CN.UTF-8

改为和当前系统一致的编码 (用 locale 命令查看), 比如我的是

export LANG=en_US.UTF-8
0%