kubectl cluster-info dump
的输出中查找“ --authorization-mode ...”。扫码关注思否编程公众号
思否编程是由中国最大的新一代开发者社区 SegmentFault 孵化的在线编程培训平台,通过提升开发者 IT 职业技能,帮助开发者获得成功。
主机(broker)
,每个 broker 都会有一个 broker.id
,每个 broker.id 都有一个唯一的标识符用来区分,这个标识符可以在配置文件里手动指定,也可以自动生成。Kafka 可以通过 broker.id.generation.enable 和 reserved.broker.max.id 来配合生成新的 broker.id。broker.id.generation.enable参数是用来配置是否开启自动生成 broker.id 的功能,默认情况下为true,即开启此功能。自动生成的broker.id有一个默认值,默认值为1000,也就是说默认情况下自动生成的 broker.id 从1001开始。
/brokers/ids
路径下注册一个与当前 broker 的 id 相同的临时节点。Kafka 的健康状态检查就依赖于此节点。当有 broker 加入集群或者退出集群时,这些组件就会获得通知。znode
节点的问题。znode
,znode 节点是一种树形的文件结构,它很像 Linux 操作系统的文件路径,ZooKeeper 的根节点是 /
。Watcher
机制:当数据发生变化的时候, ZooKeeper 会产生一个 Watcher 事件,并且会发送到客户端。Watcher 监听机制是 Zookeeper 中非常重要的特性,我们基于 Zookeeper 上创建的节点,可以对这些节点绑定监听事件,比如可以监听节点数据变更、节点删除、子节点状态变更等事件,通过这个事件机制,可以基于 ZooKeeper 实现分布式锁、集群管理等功能。/controller
让自己成为 controller 控制器。其他 broker 在启动时也会尝试创建这个节点,但是由于这个节点已存在,所以后面想要创建 /controller 节点时就会收到一个 节点已存在 的异常。然后其他 broker 会在这个控制器上注册一个 ZooKeeper 的 watch 对象,/controller
节点发生变化时,其他 broker 就会收到节点变更通知。这种方式可以确保只有一个控制器存在。那么只有单独的节点一定是有个问题的,那就是单点问题
。组件
被设计用来干什么?别着急,接下来我们就要说一说。主题管理
: Kafka Controller 可以帮助我们完成对 Kafka 主题创建、删除和增加分区的操作,简而言之就是对分区拥有最高行使权。分区重分配
: 分区重分配主要是指,kafka-reassign-partitions 脚本提供的对已有主题分区进行细粒度的分配功能。这部分功能也是控制器实现的。Prefered 领导者选举
: Preferred 领导者选举主要是 Kafka 为了避免部分 Broker 负载过重而提供的一种换 Leader 的方案。集群成员管理
: 主要管理 新增 broker、broker 关闭、broker 宕机数据服务
: 控制器的最后一大类工作,就是向其他 broker 提供数据服务。控制器上保存了最全的集群元数据信息,其他所有 broker 会定期接收控制器发来的元数据更新请求,从而更新其内存中的缓存数据。这些数据我们会在下面讨论/brokers/ids
下创建节点的 broker 作为 broker controller,也就是说 broker controller 只有一个,那么必然会存在单点失效问题。kafka 为考虑到这种情况提供了故障转移
功能,也就是 Fail Over
。如下图注意:ZooKeeper 中存储的不是缓存信息,broker 中存储的才是缓存信息。
Event Executor Thread
,事件执行线程,从图中可以看出,不管是 Event Queue 事件队列还是 Controller context 控制器上下文都会交给事件执行线程进行处理。将原来执行的操作全部建模成一个个独立的事件,发送到专属的事件队列中,供此线程消费。异步操作
。ZooKeeper API 提供了两种读写的方式:同步和异步。之前控制器操作 ZooKeeper 都是采用的同步方式,这次把同步方式改为异步,据测试,效率提升了10倍。备份机制(Replication)
,通常指分布式系统在多台网络交互的机器上保存有相同的数据备份/拷贝。Leader(领导者)
副本,一种是Follower(跟随者)
副本。Follower 副本
,Follower 不对外提供服务。下面是 Leader 副本的工作方式异步拉取
的方式,并写入到自己的提交日志中,从而实现与 Leader 的同步不同步
的。如果一个副本没有与领导者同步,那么在领导者掉线后,这个副本将不会称为领导者,因为这个副本的消息不是全部的。同步的副本
。也就是说,如果领导者掉线,那么只有同步的副本能够称为领导者。发送 - 等待
机制的,这是一种同步的复制方式,那么为什么说跟随者副本同步领导者副本的时候是一种异步操作呢?(a set of In-Sync Replicas),简称ISR
,ISR 也是一个很重要的概念,我们之前说过,追随者副本不提供服务,只是定期的异步拉取领导者副本的数据而已,拉取这个操作就相当于是复制,ctrl-c + ctrl-v
大家肯定用的熟。那么是不是说 ISR 集合中的副本消息的数量都会与领导者副本消息数量一样呢?那也不一定,判断的依据是 broker 中参数 replica.lag.time.max.ms
的值,这个参数的含义就是跟随者副本能够落后领导者副本最长的时间间隔。unclean.leader.election.enable
的话,下一个领导者就会在这些非同步的副本中选举。这种选举也叫做Unclean 领导者选举
。请求/响应
式的,我猜测你接触最早的请求/响应的方式应该就是 HTTP 请求了。事实上,HTTP 请求可以是同步可以是异步的。一般正常的 HTTP 请求都是同步的,同步方式最大的一个特点是提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能做任何事。而异步方式最大的特点是 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)-> 处理完毕。这里需要注意一点,我们只是使用 HTTP 请求来举例子,而 Kafka 采用的是 TCP 基于 Socket 的方式进行通讯
吞吐量太差
,资源利用率极低,由于只能顺序处理请求,因此,每个请求都必须等待前一个请求处理完毕才能得到处理。这种方式只适用于请求发送非常不频繁的系统
。响应式(Reactor)模型
,那么什么是响应式模型呢?简单的说,Reactor 模式是事件驱动架构的一种实现方式,特别适合应用于处理多个客户端并发向服务器端发送请求的场景,如下图所示Acceptor
线程,这个线程会创建一个连接,并把它交给 Processor(网络线程池)
, Processor 的数量可以使用 num.network.threads
进行配置,其默认值是3,表示每台 broker 启动时会创建3个线程,专门处理客户端发送的请求。轮询
的方式将入栈请求公平的发送至网络线程池中,因此,在实际使用过程中,这些线程通常具有相同的机率被分配到待处理请求队列
中,然后从响应队列
获取响应消息,把它们发送给客户端。Processor 网络线程池中的请求 - 响应的处理还是比较复杂的,下面是网络线程池中的处理流程图共享请求队列
,因为网络线程池是多线程机制的,所以请求队列的消息是多线程共享的区域,然后由 IO 线程池进行处理,根据消息的种类判断做何处理,比如 PRODUCE
请求,就会将消息写入到 log 日志中,如果是FETCH
请求,则从磁盘或者页缓存中读取消息。也就是说,IO线程池是真正做判断,处理请求的一个组件。在IO 线程池处理完毕后,就会判断是放入响应队列
中还是 Purgatory
中,Purgatory 是什么我们下面再说,现在先说一下响应队列,响应队列是每个线程所独有的,因为响应式模型中不会关心请求发往何处,因此把响应回传的事情就交给每个线程了,所以也就不必共享了。注意:IO 线程池可以通过 broker 端参数 num.io.threads
来配置,默认的线程数是8,表示每台 broker 启动后自动创建 8 个IO 处理线程。
acks
这个配置项的含义all
,那么这些请求会被保存在 炼狱(Purgatory)
的缓冲区中,直到领导者副本发现跟随者副本都复制了消息,响应才会发送给客户端。零复制
技术向客户端发送消息,Kafka 会直接把消息从文件中发送到网络通道中,而不需要经过任何的缓冲区,从而获得更好的性能。上限
指的是客户端为接受足够消息分配的内存空间,这个限制比较重要,如果上限太大的话,很有可能直接耗尽客户端内存。下限
可以理解为攒足了数据包再发送的意思,这就相当于项目经理给程序员分配了 10 个bug,程序员每次改一个 bug 就会向项目经理汇报一下,有的时候改好了有的时候可能还没改好,这样就增加了沟通成本和时间成本,所以下限值得就是程序员你改完10个 bug 再向我汇报!!!如下图所示拉取消息
---> 消息
之间是有一个等待消息积累这么一个过程的,这个消息积累你可以把它想象成超时时间,不过超时会跑出异常,消息积累超时后会响应回执。延迟时间可以通过 replica.lag.time.max.ms
来配置,它指定了副本在复制消息时可被允许的最大延迟时间。非分区首领
的错误响应;如果针对某个分区的请求被发送到不含有领导者的 broker 上,也会出现同样的错误。Kafka 客户端需要把请求和响应发送到正确的 broker 上。这不是废话么?我怎么知道要往哪发送?元数据请求
,这种请求会包含客户端感兴趣的主题列表,服务端的响应消息指明了主题的分区,领导者副本和跟随者副本。元数据请求可以发送给任意一个 broker,因为所有的 broker 都会缓存这些信息。metadata.max.age.ms
参数来配置,从而知道元数据是否发生了变更。比如,新的 broker 加入后,会触发重平衡,部分副本会移动到新的 broker 上。这时候,如果客户端收到 不是首领
的错误,客户端在发送请求之前刷新元数据缓存。群组协调者(Coordinator)
的,而重平衡的流程就是由 Coordinator 的帮助下来完成的。DEAD
状态,这可能是由于消费者崩溃或者长时间处于运行状态下发生的,这意味着在配置合理时间的范围内,消费者没有向群组协调器发送任何心跳,这也会导致重平衡的发生。群组协调器(Coordinator)
:群组协调器是一个能够从消费者群组中收到所有消费者发送心跳消息的 broker。在最早期的版本中,元数据信息是保存在 ZooKeeper 中的,但是目前元数据信息存储到了 broker 中。每个消费者组都应该和群组中的群组协调器同步。当所有的决策要在应用程序节点中进行时,群组协调器可以满足 JoinGroup
请求并提供有关消费者组的元数据信息,例如分配和偏移量。群组协调器还有权知道所有消费者的心跳,消费者群组中还有一个角色就是领导者,注意把它和领导者副本和 kafka controller 进行区分。领导者是群组中负责决策的角色,所以如果领导者掉线了,群组协调器有权把所有消费者踢出组。因此,消费者群组的一个很重要的行为是选举领导者,并与协调器读取和写入有关分配和分区的元数据信息。消费者领导者
: 每个消费者群组中都有一个领导者。如果消费者停止发送心跳了,协调者会触发重平衡。消费者组状态机(State Machine)
,来帮助协调者完成整个重平衡流程。消费者状态机主要有五种状态它们分别是 Empty、Dead、PreparingRebalance、CompletingRebalance 和 Stable。Empty
状态,当重平衡开启后,它会被置于 PreparingRebalance
状态等待新消费者的加入,一旦有新的消费者加入后,消费者群组就会处于 CompletingRebalance
状态等待分配,只要有新的消费者加入群组或者离开,就会触发重平衡,消费者的状态处于 PreparingRebalance 状态。等待分配机制指定好后完成分配,那么它的流程图是这样的Stable
状态后,一旦有新的消费者加入/离开/心跳过期,那么触发重平衡,消费者群组的状态重新处于 PreparingRebalance 状态。那么它的流程图是这样的。Dead
状态了。它的流程图是这样的PreparingRebalance
或者 CompletingRebalance
或者 Stable
任意一种状态下发生位移主题分区 Leader 发生变更,群组会直接处于 Dead 状态,它的所有路径如下这里面需要注意两点:一般出现 Required xx expired offsets in xxx milliseconds 就表明Kafka 很可能就把该组的位移数据删除了
只有 Empty 状态下的组,才会执行过期位移删除的操作。
Rebalance
的过程。重平衡过程可以从两个方面去看:消费者端和协调者端,首先我们先看一下消费者端消费者加入组
和 等待领导者分配方案
。这两个步骤后分别对应的请求是 JoinGroup
和 SyncGroup
。JoinGroup
请求。在该请求中,每个消费者成员都需要将自己消费的 topic 进行提交,我们上面描述群组协调器中说过,这么做的目的就是为了让协调器收集足够的元数据信息,来选取消费者组的领导者。通常情况下,第一个发送 JoinGroup 请求的消费者会自动称为领导者。领导者的任务是收集所有成员的订阅信息,然后根据这些信息,制定具体的分区消费分配方案。如图SyncGroup
请求给协调者,协调者负责下发群组中的消费策略。下图描述了 SyncGroup 请求的过程Stable
等待分配的过程,这时候如果有新的成员加入组的话,重平衡的过程close()
方法主动通知协调者它要退出。这里又会有一个新的请求出现 LeaveGroup()请求
。如下图所示组成员崩溃
,崩溃离组是被动的,协调者通常需要等待一段时间才能感知到,这段时间一般是由消费者端参数 session.timeout.ms 控制的。如下图所示const charCodeAt = Function.prototype.call.bind(String.prototype.charCodeAt);
charCodeAt(string, 8);
到目前为止,对 charCodeAt 的调用对 TurboFan 来说是完全不透明的,从而引发了对用户定义函数通用调用。通过这一改变,现在可以识别出实际上是在调用内置 String.prototype.charCodeAt 函数,从而能够触发 TurboFan 库中的进一步优化来改善对内建的调用,进而获得与以下相同的性能:string.charCodeAt(8);
这一变化也会影响到其他一些内建,比如 Function.prototype.apply、Reflect.apply,以及很多其他的高阶数组内建。// Error prone-version, could throw.const nameLength = db.user.name.length;// Less error-prone, but harder to read.let nameLength;if (db && db.user && db.user.name) nameLength = db.user.name.length;
可选链(?.)允许程序员编写更精炼、鲁棒性更强的属性访问链,检查中间值是否为空。如果中间值为空,则整个表达式的计算结果为未定义的。// Still checks for errors and is much more readable.const nameLength = db?.user?.name?.length;
除了静态属性访问外,动态属性访问和调用也能得到支持。function Component(props) { const enable = props.enabled || true; // …}
对 || 的使用,并不适合计算默认值,因为当 a 为非真时 a || b 的结果为 b。如果 props.enabled 明确被设置为假,那么 enable 仍然为真。function Component(props) { const enable = props.enabled ?? true; // …}
null 合并操作符与可选链是相伴而生的特性,可协同工作。当没有任何 props 参数传入时,它们可以对示例进行进一步修改以作为应对。function Component(props) { const enable = props?.enabled ?? true; // …}
==
比较的是对象地址,equals
比较的是对象值
Object
类中 equals
方法:public boolean equals(Object obj) {
return (this == obj);
}
我们看到 equals
方法同样是通过 ==
比较对象地址,并没有帮我们比较值。Java 世界中 Object
绝对是"老祖宗" 的存在,==
号我们没办法改变或重写。但 equals
是方法,这就给了我们重写 equals
方法的可能,让我们实现其对值的比较:@Override
public boolean equals(Object obj) {
//重写逻辑
}
新买的电脑,每个电脑都有唯一的序列号,通常情况下,两个一模一样的电脑放在面前,你会说由于序列号不一样,这两个电脑不一样吗?@Override
public boolean equals(Object obj) {
return 品牌相等 && 尺寸相等 && 配置相等
}
当遇到如上场景时,我们就需要重写 equals
方法。这就解释了 Java 世界为什么有了 ==
还有equals
这个问题了.equals
相等 和 hashcode
相等问题equals
相等,那他们 hashCode
相等吗?hashCode
相等,那他们 equals
相等吗?equals
比作一个单词的拼写;hashCode
比作一个单词的发音,在相同语境下:
sea / sea 「大海」,两个单词拼写一样,所以 equals
相等,他们读音 /siː/
也一样,所以 hashCode
就相等,这就回答了第一个问题:
两个对象 equals
相等,那他们 hashCode
一定也相等
sea / see 「大海/看」,两个单词的读音 /siː/
一样,显然单词是不一样的,这就回答了第二个问题:
两个对象 hashCode
相等,那他们 equals
不一定相等
Object
类的 hashCode
方法:public native int hashCode();
继续查看该方法的注释,明确写明关于该方法的约束equals
方法的约束equals
有哪些约束?equals
方法的约束,同样在该方法的注释中写的很清楚了,我在这里再说明一下:equals
方法时,打开 JDK 查看该方法,按照准则重写就好hashCode
?equals
方法,那什么时候又需要重写 hashCode
方法呢?通常只要我们重写 equals
方法就要重写 hashCode
方法
equals
相等,那他们的 hashCode
一定也相等。如果我们只重写 equals
方法而不重写 hashCode
方法,看看会发生什么,举个例子来看:equals
方法:public class Student {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
}
编写测试代码:Student student1 = new Student();
student1.setName("日拱一兵");
student1.setAge(18);
Student student2 = new Student();
student2.setName("日拱一兵");
student2.setAge(18);
System.out.println("student1.equals(student2)的结果是:" + student1.equals(student2));
Set<Student> students = new HashSet<Student>();
students.add(student1);
students.add(student2);
System.out.println("Student Set 集合长度是:" + students.size());
Map<Student, java.lang.String> map = new HashMap<Student, java.lang.String>();
map.put(student1, "student1");
map.put(student2, "student2");
System.out.println("Student Map 集合长度是:" + map.keySet().size());
查看运行结果:student1.equals(student2)的结果是:true
Student Set 集合长度是:2
Student Map 集合长度是:2
很显然,按照集合 Set 和 Map 加入元素的标准来看,student1 和 student2 是两个对象,因为在调用他们的 put (Set add 方法的背后也是 HashMap 的 put)方法时, 会先判断 hash 值是否相等,这个小伙伴们打开 JDK 自行查看吧hashCode
方法:@Override
public int hashCode() {
return Objects.hash(name, age);
}
重新运行上面的测试,查看结果:student1.equals(student2)的结果是:true
Student Set 集合长度是:1
Student Map 集合长度是:1
得到我们预期的结果,这也就是为什么通常我们重写 equals
方法为什么最好也重写 hashCode
方法的原因@EqualsAndHashCode
注解,而没有拆分成 @Equals 和 @HashCode 两个注解,想了解更多 Lombok 的内容,也可以查看我之前写的文章 Lomok 使用详解
hashCode
为什么总有 31 这个数字?hashCode
的方法很简答, 就是用了 Objects.hash
方法,进去查看里面的方法:public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
这里通过 31 来计算对象 hash 值HandlerMethodArgumentResolverComposite
类中有这样一个成员变量:private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
new ConcurrentHashMap<MethodParameter, HandlerMethodArgumentResolver>(256);
Map 的 key 是 MethodParameter
,根据我们上面的分析,这个类一定也会重写 equals
和 hashCode
方法,进去查看发现,hashCode 的计算也用到了 31 这个数字@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof MethodParameter)) {
return false;
}
MethodParameter otherParam = (MethodParameter) other;
return (this.parameterIndex == otherParam.parameterIndex && getMember().equals(otherParam.getMember()));
}
@Override
public int hashCode() {
return (getMember().hashCode() * 31 + this.parameterIndex);
}
为什么计算 hash 值要用到 31 这个数字呢?我在网上看到一篇不错的文章,分享给大家,作为科普,可以简单查看一下:equals
和 hashCode
关系及约束含混,我们只需要按照上述步骤逐步回忆即可,更好的是直接查看 JDK 源码;另外拿出实际的例子来反推验证是非常好的办法。如果你还有相关疑问,也可以留言探讨.equals
方法,你还知道哪些情况没必要重写 equals
方法吗?欢迎关注我的公众号 「日拱一兵」,趣味原创解析Java技术栈问题,将复杂问题简单化,将抽象问题图形化落地
如果对我的专题内容感兴趣,或抢先看更多内容,欢迎访问我的博客 dayarch.top
Express
的源码、以及目前现在主流库已经全部使用TypeScript编写,呼吁大家全面切换到TypeScript
TypeScript
{ configurable: true, enumerable: true, writable: true, value: app }
这段代码是属性描述符,vue 2.x版本中的get和set和访问描述符,不懂的去搜下module.exports = serveStatic
function serveStatic (root, options) {
return serveStatic(req,res,next) {
...
if (path === '/' && originalUrl.pathname.substr(-1) !== '/') {
path = ''
}
var stream = send(req, path, opts)
stream.on('directory', onDirectory)
if (setHeaders) {
stream.on('headers', setHeaders)
}
if (fallthrough) {
stream.on('file', function onFile () {
forwardError = true
})
}
stream.on('error', function error (err) {
if (forwardError || !(err.statusCode < 500)) {
next(err)
return
}
next()
})
// pipe
stream.pipe(res)
}
}
原来调用express-static后会返回一个函数,也是接受请求返回响应~
function send (req, path, options) {
return new SendStream(req, path, options)
}
function SendStream(){
Stream.call(this)
../若干代码
}
一开始我以为调用pipe是可读流的pipe,但是没有发现SendStream有返回值,后面一看,pipei是自己定义在原型链上的方法~
SendStream.prototype.pipe = function pipe (res) {
//..中间很多容错处理 头部处理等
var path = decode(this.path)
//若干代码
this.sendFile(path)
}
原来返回文件的核心在这里: fs.stat(path, function onstat (err, stat) {
if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
// not found, check extensions
return next(err)
}
if (err) return self.onStatError(err)
if (stat.isDirectory()) return self.redirect(path)
self.emit('file', path, stat)
self.send(path, stat)
})
这里通过一些容错机制处理后,把path和文件stat信息对象,传入this.send中,这里的send,跟默认暴露的function send不是一个函数,整个源码这里是最绕的https://github.com/JinJieTan/util-static-server
app.use = function use(fn) {
var offset = 0;
var path = '/';
var fns = flatten(slice.call(arguments, offset));
this.lazyrouter();
var router = this._router;
fns.forEach(function (fn) {
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});
fn.emit('mount', this);
}, this);
return this;
};
lazyrouter,每次初始化都会生成一个新的Layerapp.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});
this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
}
};
上面省掉了很多的容错处理,这里有一个flatten函数,扁平化数组的
function flattenForever (array, result) {
for (var i = 0; i < array.length; i++) {
var value = array[i]
if (Array.isArray(value)) {
flattenForever(value, result)
} else {
result.push(value)
}
}
return result
}
这里也是很巧妙,forEach时候传入了this的值给函数,我以前不知道forEach能传两个值,
app.handle = function handle(req, res, callback) {
var router = this._router;
// final handler
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
// no routes
if (!router) {
debug('no routes defined on app');
done();
return;
}
router.handle(req, res, done);};
先取出第一层,判断与request的path是否match。第一、二层是router初始化时的query函数和middleware.init函数,它们都会进入执行trim_prefix(layer, layerError, layerPath, path);的分支,并调用其中的layer.handle_request(req,res, next);,这个next就是router.handle函数里的闭包next。执行了这两层后,继续回调next函数。while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
//...若干d代码
trim_prefix(layer, layerError, layerPath, path);
function trim_prefix(layer, layerError, layerPath, path) {
if (layerPath.length !== 0) {
// Validate path breaks on a path separator
var c = path[layerPath.length]
if (c && c !== '/' && c !== '.') return next(layerError)
// Trim off the part of the url that matches the route
// middleware (.use stuff) needs to have the path stripped
debug('trim prefix (%s) from url %s', layerPath, req.url);
removed = layerPath;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// Ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
// Setup base URL (no trailing slash)
req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
? removed.substring(0, removed.length - 1)
: removed);
}
debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
}
这时就执行到了加载时生成的route所在的层,判断request路径是否匹配,这里的匹配执行的是严格匹配,比如这层的regexp属性(从加载时的路由确定)是'/',那么'/a'也不能匹配。 Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
if (fn.length > 3) {
// not a standard request handler
return next();
}
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
这里非常巧妙,也是最绕的,我们知道调用red.end就会返回响应结束匹配,否则express就会逐个路由匹配执行,这里确定执行所有的匹配请求后,就会调用finalhandler(最终的处理),返回响应
if (isFinished(req)) {
write()
return
}
function write () {
// response body
var body = createHtmlDocument(message)
// response status
res.statusCode = status
res.statusMessage = statuses[status]
// response headers
setHeaders(res, headers)
// security headers
res.setHeader('Content-Security-Policy', "default-src 'none'")
res.setHeader('X-Content-Type-Options', 'nosniff')
// standard headers
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
if (req.method === 'HEAD') {
res.end()
return
}
res.end(body, 'utf8')
}
通过以下函数判断:
function isFinished(msg) {
var socket = msg.socket
if (typeof msg.finished === 'boolean') {
// OutgoingMessage
return Boolean(msg.finished || (socket && !socket.writable))
}
if (typeof msg.complete === 'boolean') {
// IncomingMessage
return Boolean(msg.upgrade || !socket || !socket.readable || (msg.complete && !msg.readable))
}
// don't know
return undefined
}
判断有没有协议升级事件(例如websocket的第一次握手时)、有没有socket对象、socket是不是可读等 function createHtmlDocument (message) {
var body = escapeHtml(message)
.replace(NEWLINE_REGEXP, '<br>')
.replace(DOUBLE_SPACE_REGEXP, ' ')
return '<!DOCTYPE html>\n' +
'<html lang="en">\n' +
'<head>\n' +
'<meta charset="utf-8">\n' +
'<title>Error</title>\n' +
'</head>\n' +
'<body>\n' +
'<pre>' + body + '</pre>\n' +
'</body>\n' +
'</html>\n'
}
至此,花费4000字解析了express的核心所有API,感觉有一点绕,这里特别是get路由的触发,是整个源码的核心。你有一个苹果,我有一个苹果,彼此交换一下,我们仍然是各有一个苹果;但你有一种思想,我有一种思想,彼此交换,我们就都有了两种思想,甚至更多。
发现Bug不是Code Review的必需品,而是附属品。至于那些低级的问题/bug交给代码扫描工具就可以了,这不是Code Review的职责。
工具 | 权限 控制 |
UI 交互 |
源代码 管理 |
可维护 | 数据 统计 |
工具 配套 |
---|---|---|---|---|---|---|
Gerrit | ⭐ ⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ |
⭐ ⭐ ⭐ |
⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ |
GitLab社区版 | ⭐ ⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ ⭐ |
GitLab企业版 | ⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ ⭐ |
⭐ ⭐ ⭐ ⭐ |
基于GitLab的CodeReview教程:https://ken.io/note/gitlab-co...
Java开发手册:https://github.com/alibaba/p3c
Google代码风格指南:https://zh-google-styleguide.... (涵盖:C++、Python等)
角色 | 规则 |
---|---|
Developer | 1、一次提交的功能必须是完整的 2、默认细粒度提交(以独立的方法/功能/模块为单位)。如需粗粒度提交,需提前跟Reviewer沟通确认 3、Commit Message中要清晰描述变更的主题 必要时,可以以链接或者文件的形式附上需求文档/设计文档 |
Reviewer | 1、不允许自我Review并Merge代码 2、Review不通过打回前需跟Developer说明原因并达成一致 3、Review不通过需明确填写打回的原因 4、单次Review时长需控制在2分钟~2小时内完成(特殊情况请说明原因) |
Approver | 1、审批不通过需注明原因<br/>2、审批时长需要控制在1小时以内 3、对于放行的非质量问题,需持续跟进 |
Streams API 示意图,作者 Mozilla Contributors,基于 CC-BY-SA 2.5 协议使用。
本文篇幅较长,建议配合目录食用分次阅读。本文作者:ccloli
async
/await
的异步编程,所以估计不少同学也转而使用 Fetch API 作异步请求。陪伴了我们将近 20 年历史的 XMLHttpRequest
也被不少同学「打入冷宫」,毕竟谁让 Fetch API 那么好用呢?可怜的 XHR 只能独守空房终日以泪洗面,看着你和 Fetch API 嬉戏的样子,口中喃喃说着「是我,是我先,明明都是我先来的」——呃,不好意思扯歪了。XMLHttpRequest
来说,fetch()
的写法简单又直观,只要在发起请求时将整个配置项传入就可以了。而且相较于 XHR 还提供了更多的控制参数,例如是否携带 Cookie、是否需要手动跳转等。此外 Fetch API 是基于 Promise 链式调用的,一定程度上可以避免一些回调地狱。举个例子,下面就是一个简单的 fetch 请求:fetch('https://example.org/foo', {
method: 'POST',
mode: 'cors',
headers: {
'content-type': 'application/json'
},
credentials: 'include',
redirect: 'follow',
body: JSON.stringify({ foo: 'bar' })
}).then(res => res.json()).then(...)
如果你不喜欢 Promise 的链式调用的话,还可以用 async
/await
:const res = await fetch('https://example.org/foo', { ... });
const data = await res.json();
再回过头来看久经风霜的 XMLHttpRequest
,如果你已经习惯使用诸如 jQuery 的 $.ajax()
或者 axios 这类更为现代的封装 XHR 的库的话,估计已经忘了裸写 XHR 是什么样子了。简单来说,你需要调用 open()
方法开启一个请求,然后调用其他的方法或者设置参数来定义请求,最后调用 send()
方法发起请求,再在 onload
或者 onreadystatechange
事件里处理数据。看,这一通下来你已经乱了。
课后习题 Q0:试试看将上面的 fetch 请求用原生 XMLHttpRequest
实现一遍,看看你还记得多少知识?
XMLHttpRequest
对象上有一个 abort()
方法,调用这个方法即可中断一个请求。此外 XHR 还有 onabort
事件,可以监听请求的中断并做出响应。XMLHttpRequest
对象上有一个 timeout
属性,为其赋值后若在指定时间请求还未完成,请求就会自动中断。此外 XHR 还有 ontimeout
事件,可以监听请求的超时中断并做出响应。XMLHttpRequest
提供了 onprogress
事件,所以使用 XHR 可以很方便地实现这个功能。const xhr = new XMLHttpRequest();
xhr.open('GET', '/foo');
xhr.addEventListener('progress', (event) => {
const { lengthComputable, loaded, total } = event;
if (lengthComputable) {
console.log(`Downloaded ${loaded} of ${total} (${(loaded / total * 100).toFixed(2)}%)`);
} else {
console.log(`Downloaded ${loaded}`);
}
});
xhr.send();
AbortController
与 AbortSignal
在各大浏览器上完整实现,Fetch API 也能像 XHR 那样中断一个请求了,只是稍微绕了一点。通过创建一个 AbortController
实例,我们得到了一个 Fetch API 原生支持的控制中断的控制器。这个实例的 signal
参数是一个 AbortSignal
实例,还提供了一个 abort()
方法发送中断信号。只需要将 signal
参数传递进 fetch()
的初始化参数中,就可以在 fetch 请求之外控制请求的中断了:const controller = new AbortController();
const { signal } = controller;
fetch('/foo', { signal }).then(...);
signal.onabort = () => { ... };
controller.abort();
对于第二个问题,既然已经稍微绕路实现中断请求了,为何不再绕一下远路呢?只需要 AbortController
配合 setTimeout()
就能实现类似的效果了。fetch()
方法的所有参数,都没有找到类似 progress
这样的参数,毕竟 Fetch API 并没有什么回调事件。难道 Fetch API 就不能实现这么简单的功能吗?当然可以,这里就要绕一条更远的路,提一提和它相关的 Streams API 了——不是 Web Socket,也不是 Media Stream,更不是只能在 Node.js 上使用的 Stream,不过和它很像。const input = fs.createReadStream(null, {
fd, start, end, autoClose: false
});
const output = fs.createWriteStream(outputPath + name);
// 可以从流中直接读取数据
input.on('data', (chunk) => { ... });
// 或者直接将流引向另一个流
input.pipe(zlib.createInflateRaw()).pipe(output);
其中的 input
是一个可读取的流,output
是一个可写入的流,而 zlib.createInflateRaw()
就是创建了一个既可读取又可写入的流,它在写入端以流的形式接受 Deflate 压缩的数据,在读取端以流的形式输出解压缩后的数据。我们想象一下,如果输入的 zip 文件是一个上 GB 的大文件,使用流的方式就不需要占用同样大小的上 GB 的内存空间。而且从代码上看,使用流实现的代码逻辑同样简洁和清晰。StreamHelper
,但是基本上除了使用这些 stream 库的库以外,没有其它地方能 产生 兼容这个库的流了。没有能产生流的数据源才是大问题,比如想要读取一个文件?过去 FileReader
只能在 onload
事件上拿到整个文件的数据,或者对文件使用 slice()
方法得到 Blob
文件片段。现在 Streams API 已经在浏览器上逐步实现(或者说,早在 2016 年 Chrome 就开始支持一部分功能了),能用上流处理的 API 想必也会越来越多,而 Streams API 最早的受益者之一就是 Fetch API。XMLHttpRequest
获取一个文件时,我们必须等待浏览器下载完整的文件,等待浏览器处理成我们需要的格式,收到所有的数据后才能处理它。现在有了流,我们可以以 TypedArray
片段的形式接收一部分二进制数据,然后直接对数据进行处理,这就有点像是浏览器内部接收并处理数据的逻辑。甚至我们可以将一些操作以流的形式封装,再用管道把多个流连接起来,管道的另一端就是最终处理好的数据。Response
对象,而 Response
对象除了提供 headers
、redirect()
等参数和方法外,还实现了 Body
这个 mixin 类,而在 Body
上我们才看到我们常用的那些 res.json()
、res.text()
、res.arrayBuffer()
等方法。在 Body
上还有一个 body
参数,这个 body
参数就是一个 ReadableStream
。Body
中负责提供数据的 ReadableStream
。这篇文章不会讨论流的排队策略(也就是下文即将提到的构造流时传入的 queuingStrategy
参数,它可以控制流的缓冲区大小,不过 Streams API 有一个开箱即用的默认配置,所以可以不指定),也不会讨论没有浏览器实现的 BYOR reader,感兴趣的同学可以参考相关规范文档
ReadableStream
ReadableStream 示意图,作者 Mozilla Contributors,基于 CC-BY-SA 2.5 协议使用。
ReadableStream
实例上的参数和可以使用的方法,下文我们将会详细介绍它们:ReadableStream
locked
cancel()
pipeThrough()
pipeTo()
tee()
getReader()
getReader()
方法会得到一个 ReadableStreamDefaultReader
实例,通过这个实例我们就能读取 ReadableStream
上的数据。ReadableStream
中读取数据ReadableStreamDefaultReader
实例上提供了如下的方法:ReadableStreamDefaultReader
closed
cancel()
read()
releaseLock()
read()
方法,它会返回一个 Promise
对象,在 Promise
中返回一个包含 value
参数和 done
参数的对象。const reader = stream.getReader();
let bytesReceived = 0;
const processData = (result) => {
if (result.done) {
console.log(`complete, total size: ${bytesReceived}`);
return;
}
const value = result.value; // Uint8Array
const length = value.length;
console.log(`got ${length} bytes data:`, value);
bytesReceived += length;
// 读取下一个文件片段,重复处理步骤
return reader.read().then(processData);
};
reader.read().then(processData);
其中 result.value
参数为这次读取得到的片段,它是一个 Uint8Array
,通过循环调用 reader.read()
方法就能一点点地获取流的整个数据;而 result.done
参数负责表明这个流是否已经读取完毕,当 result.done
为 true
时表明流已经关闭,不会再有新的数据,此时 result.value
的值为 undefined
。Response
中的流得到正在接收的文件片段,累加各个片段的 length
就能得到类似 XHR onprogress
事件的 loaded
,也就是已下载的字节数;通过从 Response
的 headers
中取出 Content-Length
就能得到类似 XHR onprogress
事件的 total
,也就是总字节数。于是我们可以写出下面的代码,成功得到下载进度:let total = null;
let loaded = 0;
const logProgress = (reader) => {
return reader.read().then(({ value, done }) => {
if (done) {
console.log('Download completed');
return;
}
loaded += value.length;
if (total === null) {
console.log(`Downloaded ${loaded}`);
} else {
console.log(`Downloaded ${loaded} of ${total} (${(loaded / total * 100).toFixed(2)}%)`);
}
return logProgress(reader);
});
};
fetch('/foo').then((res) => {
total = res.headers.get('content-length');
return res.body.getReader();
}).then(logProgress);
TextDecoder
得到解析后的文本并拼接,最后将整个文本返回:let text = '';
const logProcess = (res) => {
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
const push = ({ value, done }) => {
if (done) return JSON.parse(text);
text += decoder.decode(value, { stream: true });
// ...
return reader.read().then(push);
};
return reader.read().then(push);
};
fetch('/foo').then(logProgress).then((res) => { ... });
不过如果你犯了强迫症,一定要像原来那样显示调用 res.json()
之类的方法得到数据,这该怎么办呢?既然 fetch()
方法返回一个 Response
对象,而这个对象的数据已经在 ReadableStream
中读取下载进度时被使用了,那我再构造一个 ReadableStream
,外面再包一个 Response
对象并返回,问题不就解决了吗?ReadableStream
ReadableStream
时可以定义以下方法和参数:const stream = new ReadableStream({
start(controller) {
// start 方法会在实例创建时立刻执行,并传入一个流控制器
controller.desiredSize
// 填满队列所需字节数
controller.close()
// 关闭当前流
controller.enqueue(chunk)
// 将片段传入流的队列
controller.error(reason)
// 对流触发一个错误
},
pull(controller) {
// 将会在流的队列没有满载时重复调用,直至其达到高水位线
},
cancel(reason) {
// 将会在流将被取消时调用
}
}, queuingStrategy); // { highWaterMark: 1 }
而构造一个 Response
对象就简单了,Response
对象的第一个参数即是返回值,可以是字符串、Blob
、TypedArray
,甚至是一个 Stream;而它的第二个参数则和 fetch()
方法很像,也是一些初始化参数。const response = new Response(source, init);
了解以上的内容后,我们只需要构造一个 ReadableStream
,然后把「从 reader 中循环读取数据」的逻辑放在这个流的 start()
方法内,它会在流实例化后立即调用。当 reader 读取数据时可以输出下载进度,同时调用 controller.enqueue()
把得到的数据推进我们构造出来的流,最后在读取完毕时调用 controller.close()
关闭这个流,问题就能轻松解决。const logProgress = (res) => {
const total = res.headers.get('content-length');
let loaded = 0;
const reader = res.body.getReader();
const stream = new ReadableStream({
start(controller) {
const push = () => {
reader.read().then(({ value, done }) => {
if (done) {
controller.close();
return;
}
loaded += value.length;
if (total === null) {
console.log(`Downloaded ${loaded}`);
} else {
console.log(`Downloaded ${loaded} of ${total} (${(loaded / total * 100).toFixed(2)}%)`);
}
controller.enqueue(value);
push();
});
};
push();
}
});
return new Response(stream, { headers: res.headers });
};
fetch('/foo').then(logProgress).then(res => res.json()).then((data) => { ... });
ReadableStream
ReadableStream
实例?有没有更简单的方法?其实是有的,如果你稍有留意的话,应该会注意到 ReadableStream
实例上有一个名字看起来有点奇怪的 tee()
方法。这个方法可以将一个流分流成两个一模一样的流,两个流可以读取完全相同的数据。分流 ReadableStream 示意图,作者 Mozilla Contributors,基于 CC-BY-SA 2.5 协议使用。
const logProgress = (res) => {
const total = res.headers.get('content-length');
let loaded = 0;
const [progressStream, returnStream] = res.body.tee();
const reader = progressStream.getReader();
const log = () => {
reader.read().then(({ value, done }) => {
if (done) return;
// 省略输出进度
log();
});
};
log();
return new Response(returnStream, { headers: res.headers });
};
fetch('/foo').then(logProgress).then(res => res.json()).then((data) => { ... });
另外其实 fetch 请求返回的 Response
实例上有一个一看就知道是什么意思的 clone()
方法,这个方法可以得到一个克隆的 Response
实例。所以我们可以将其中一个实例用来获取流并得到下载进度,另一个实例直接返回,这样就省去了构造 Response
的步骤,效果是一样的。其实这个方法一般用在 Service Worker 里,例如将请求得到的结果缓存起来等等。
课后习题 Q1:如果我们调用了流的 tee()
方法得到了两个流,但我们只读取了其中一个流,另一个流在之后读取,会发生什么吗?
signal
这个参数的,所以早期的 fetch 请求很难中断——对,是「很难」,而不是「不可能」。如果浏览器实现了 ReadableStream
并在 Response
上提供了 body
的话,是可以通过流的中断实现这个功能的。ReadableStream
Response
对象,从中可以得到一个 ReadableStream
,然后我们还知道了如何自己构造 ReadableStream
和 Response
对象。再回过头看看 ReadableStream
实例上还没提到的方法,想必你一定注意到了那个 cancel()
方法。ReadableStream
上的 cancel()
方法,我们可以关闭这个流。此外你可能也注意到 reader 上也有一个 cancel()
方法,这个方法的作用是关闭与这个 reader 相关联的流,所以从结果上来看,两者是一样的。而对于 Fetch API 来说,关闭返回的 Response
对象的流的结果就相当于中断了这个请求。ReadableStream
用于传递从 res.body.getReader()
中得到的数据,并对外暴露一个 aborter()
方法。调用这个 aborter()
方法时会调用 reader.cancel()
关闭 fetch 请求返回的流,然后调用 controller.error()
抛出错误,中断构造出来的传递给后续操作的流:let aborter = null;
const abortHandler = (res) => {
const reader = res.body.getReader();
const stream = new ReadableStream({
start(controller) {
let aborted = false;
const push = () => {
reader.read().then(({ value, done }) => {
if (done) {
if (!aborted) controller.close();
return;
}
controller.enqueue(value);
push();
});
};
aborter = () => {
reader.cancel();
controller.error(new Error('Fetch aborted'));
aborted = true;
};
push();
}
});
return new Response(stream, { headers: res.headers });
};
fetch('/foo').then(abortHandler).then(res => res.json()).then((data) => { ... });
aborter();
课后习题 Q2:从上面的结果来看,当我们调用 aborter()
方法时,请求被成功中止了。不过如果不调用 controller.error()
抛出错误强制中断流,而是继续之前的流程调用 controller.close()
关闭流,会发生什么事吗?
cancel()
方法,为什么我们不直接暴露这个方法,反而要绕路构造一个新的 ReadableStream
呢?例如像下面这样:let aborter = null;
const abortHandler = (res) => {
aborter = () => res.body.cancel();
return res;
};
fetch('/foo').then(abortHandler).then(res => res.json()).then((data) => { ... });
aborter();
可惜这样执行会得到下面的错误:这个流被锁了。TypeError: Failed to execute 'cancel' on 'ReadableStream': Cannot cancel a locked stream
你不信邪,既然流的 reader 被关闭时会关闭相关联的流,那么只要再获取一个 reader 并 cancel()
不就好了?let aborter = null;
const abortHandler = (res) => {
aborter = () => res.body.getReader().cancel();
return res;
};
fetch('/foo').then(abortHandler).then(res => res.json()).then((data) => { ... });
aborter();
可惜这样执行还是会得到下面的错误:TypeError: Failed to execute 'getReader' on 'ReadableStream': ReadableStreamReader constructor can only accept readable streams that are not yet locked to a reader
或许你还会想,像之前那样使用 tee()
克隆一个流,然后关闭克隆的流不就好了?可惜即便成功调用了其中一个流的 cancel()
方法,请求还是没有中断,因为另一个流并没有被中断,并且还在不断地接收数据。locked
属性为 true
。如果这个流需要被另一个 reader 读取,那么当前处于活动状态的 reader 可以调用 reader.releaseLock()
方法释放锁。此外 reader 的 closed
属性是一个 Promise
,当 reader 被关闭或者释放锁时,这个 Promise
会被 resolve,可以在这里编写关闭 reader 的处理逻辑:reader.closed.then(() => {
console.log('reader closed');
});
reader.releaseLock();
可是上面的代码似乎没用上 reader 啊?再仔细思考下 res => res.json()
这段代码,是不是有什么启发?
Objects implementing the Body
mixin also have an associated consume body algorithm, given a _type_, runs these steps:
- If this object is disturbed or locked, return a new promise rejected with a
TypeError
.
- Let stream be body’s stream if body is non-null, or an empty
ReadableStream
object otherwise.
- Let reader be the result of getting a reader from _stream_. If that threw an exception, return a new promise rejected with that exception.
- Let promise be the result of reading all bytes from stream with _reader_.
- Return the result of transforming promise by a fulfillment handler that returns the result of the package data algorithm with its first argument, type and this object’s MIME type.
Body
上的方法时,浏览器隐式地创建了一个 reader 读取了返回数据的流,并创建了一个 Promise
实例,待所有数据被读取完后再 resolve 并返回格式化后的数据。所以,当我们调用了 Body
上的方法时,其实就创建了一个我们无法接触到的 reader,此时这个流就被锁住了,自然也无法从外部取消。Content-Length
属性Range
请求头XMLHttpRequest
或者还没有 Stream API 的时候,我们只能在请求完成时拿到数据。如果期间请求中断了,那也不会得到已经下载的数据,也就是这部分请求的流量被浪费了。所以断点续传最大的问题是获取已拿到的数据,也就是上面的第 3 步,根据已拿到的数据就能算出还有哪些数据需要请求。XMLHttpRequest
为 responseType
属性提供了私有的可用参数 moz-chunked-arraybuffer
。请求还未完成时,可以在 onprogress
事件中请求 XHR 实例的 response
属性,它将会返回上一次触发事件后接收到的数据,而在 onprogress
事件外获取该属性将始终是 null
:let chunks = [];
const xhr = new XMLHttpRequest();
xhr.open('GET', '/foo');
xhr.responseType = 'moz-chunked-arraybuffer';
xhr.addEventListener('progress', (event) => {
chunks.push(xhr.response);
});
xhr.addEventListener('abort', () => {
const blob = new Blob(chunks);
});
xhr.send();
看起来是个很不错的特性,只可惜在 Bugzilla 上某个 和云音乐相关的 issue 里,有人发现这个特性已经在 Firefox 68 中移除了。原因也可以理解,Firefox 现在已经在 fetch 上实现 Stream API 了,有标准定义当然还是跟着标准走(虽然至今还是 LS 阶段),所以也就不再需要这些私有属性了。ReadableStream
里得到正在下载的数据片段,只要在请求的过程中把它们放在一个类似缓冲区的地方就可以实现之前的第 3 步了,而这也是在浏览器上实现这个功能的难点。请求中断后再次请求时,只需要根据已下载片段的字节数就可以算出接下来要请求哪些片段了。简单来看,逻辑大概是下面这样:const chunks = [];
let length = 0;
const chunkCache = (res) => {
const reader = res.body.getReader();
const stream = new ReadableStream({
start(controller) {
const push = () => {
reader.read().then(({ value, done }) => {
if (done) {
let chunk;
while (chunk = chunks.shift()) {
controller.enqueue(chunk);
}
controller.close();
return;
}
chunks.push(value);
length += value.length;
push();
});
};
push();
}
});
return new Response(stream, { headers: res.headers });
};
const controller = new AbortController();
fetch('/foo', {
headers: {
'Range': `bytes=${length}-`
},
signal: controller.signal
}).then(chunkCache).then(...);
// 请求中断后再次执行上述 fetch() 方法
下面的例子对上述代码简单封装得到了 ResumableFetch
,并使用它实现了图片下载的断点续传。示例完整代码可在 CodePen 上查看。注意:该示例中的代码仅进行了简单封装,没有做诸如 If-Range
、Range
和 Content-Length
等 header 的校验,也没有做特殊的错误处理,也没有包含之前提到的中断请求兼容代码,使用上可能也不够友好,仅供示例使用,请谨慎用于生产环境。
ResumableFetch
类会在请求过程中创建一个 ReadableStream
实例并直接返回,同时已下载的片段将会放进一个数组 chunks
并记录已下载的文件大小 length
。当请求中断并重新下载时会根据已下载的文件大小设置 Range
请求头,此时拿到的就是还未下载的片段。下载完成后再将片段从 chunks
中取出,此时不需要对片段进行处理,只需要逐一传递给 ReadableStream
即可得到完整的文件。ReadableStream
上的方法已经描述的差不多了,最后只剩下 pipeTo()
方法和 pipeThrough()
方法没有提到了。从字面意思上来看,这就是我们之前提到的管道,可以将流直接指向另一个流,最后拿到处理后的数据。Jake Archibald 在他的那篇《2016 — 属于 web streams 的一年》中提出了下面的例子,或许在(当时的)未来可以通过这样的形式以流的形式得到解析后的文本:var reader = response.body
.pipeThrough(new TextDecoder()).getReader();
reader.read().then(result => {
// result.value will be a string
});
现在那个未来已经到了,为了不破坏兼容性,TextEncoder
和 TextDecoder
分别扩展出了新的 TextEncoderStream
和 TextDecoderStream
,允许我们以流的方式编码或者解码文本。例如下面的例子会在请求中检索 It works!
这段文字,当找到这段文字时返回 true
同时断开请求。此时我们不需要再接收后续的数据,可以减少请求的流量:fetch('/index.html').then((res) => {
const decoder = new TextDecoderStream('gbk', { ignoreBOM: true });
const textStream = res.body.pipeThrough(decoder);
const reader = textStream.getReader();
const findMatched = () => reader.read().then(({ value, done }) => {
if (done) {
return false;
}
if (value.indexOf('It works!') >= 0) {
reader.cancel();
return true;
}
return findMatched();
});
return findMatched();
}).then((isMatched) => { ... });
或者在未来,我们甚至在流里实现实时转码视频并播放,或者将浏览器还不支持的图片以流的形式实时渲染出来:const encoder = new VideoEncoder({
input: 'gif', output: 'h264'
});
const media = new MediaStream();
const video = document.createElement('video');
fetch('/sample.gif').then((res) => {
response.body.pipeThrough(encoder).pipeTo(media);
video.srcObject = media;
});
从中应该可以看出来这两种方法的区别:pipeTo()
方法应该会接受一个可以写入的流,也就是 WritableStream
;而 pipeThrough()
方法应该会接受一个既可写入又可读取的流,也就是 TransformStream
。Stream 管道链示意图,作者 Mozilla Contributors,基于 CC-BY-SA 2.5 协议使用。
ReadableStream
在浏览器上的支持程度:ReadableStream 浏览器兼容表,作者 Mozilla Contributors,本图片为表格的截图,基于 CC-BY-SA 2.5 协议使用。
WritableStream
WritableStream 示意图,作者 Mozilla Contributors,基于 CC-BY-SA 2.5 协议使用。
ReadableStream
中了解到很多关于流的知识了,所以下面我们简单过一下 WritableStream
。WritableStream
就是可写入的流,如果说 ReadableStream
是一个管道中流的起点,那么 WritableStream
可以理解为流的终点。下面是一个 WritableStream
实例上的参数和可以使用的方法:WritableStream
locked
abort()
getWriter()
getWriter()
方法会得到一个 WritableStreamDefaultWriter
实例,通过这个实例我们就能向 WritableStream
写入数据。同样的,当我们激活了一个 writer 后,这个流就会被锁定(locked = true
)。这个 writer 上有如下属性和方法:WritableStreamDefaultWriter
closed
desiredSize
ready
abort()
close()
write()
releaseLock()
ReadableStreamDefaultReader
没太大区别,多出的 abort()
方法相当于抛出了一个错误,使这个流不能再被写入。另外这里多出了一个 ready
属性,这个属性是一个 Promise
,当它被 resolve 时,表明目前流的缓冲区队列不再过载,可以安全地写入。所以如果需要循环向一个流写入数据的话,最好放在 ready
处理。WritableStream
,构造时可以定义以下方法和参数:const stream = new WritableStream({
start(controller) {
// 将会在对象创建时立刻执行,并传入一个流控制器
controller.error(reason)
// 对流抛出一个错误
},
write(chunk, controller) {
// 将会在一个新的数据片段写入时调用,可以获取到写入的片段
},
close(controller) {
// 将会在流写入完成时调用
},
abort(reason) {
// 将会在流强制关闭时调用,此时流会进入一个错误状态,不能再写入
}
}, queuingStrategy); // { highWaterMark: 1 }
下面的例子中,我们通过循环调用 writer.write()
方法向一个 WritableStream
写入数据:const stream = new WritableStream({
write(chunk) {
return new Promise((resolve) => {
console.log('got chunk:', chunk);
// 在这里对数据进行处理
resolve();
});
},
close() {
console.log('stream closed');
},
abort() {
console.log('stream aborted');
}
});
const writer = stream.getWriter();
// 将数据逐一写入 stream
data.forEach((chunk) => {
// 待前一个数据写入完成后再写入
writer.ready.then(() => {
writer.write(chunk);
});
});
// 在关闭 writer 前先保证所有的数据已经被写入
writer.ready.then(() => {
writer.close();
});
下面是 WritableStream
的浏览器支持情况,可见 WritableStream
在各个浏览器上的的实现时间和 pipeTo()
与 pipeThrough()
方法的实现时间是吻合的,毕竟要有了可写入的流,管道才有存在的意义。WritableStream 浏览器兼容表,作者 Mozilla Contributors,本图片为表格的截图,基于 CC-BY-SA 2.5 协议使用。
TransformStream
TransformStream
是一个既可写入又可读取的流,正如它的名字一样,它作为一个中间流起着转换的作用。所以一个 TransformStream
实例只有如下参数:TransformStream
readable
: ReadableStream
writable
: WritableStream
TransformStream
上没有其他的方法,它只暴露了自身的 ReadableStream
与 WritableStream
。我们只需要在数据源流上链式使用 pipeThrough()
方法就能实现流的数据传递,或者使用暴露出来的 readable
和 writable
直接操作数据即可使用它。TransformStream
的处理逻辑主要在流内部实现,下面是构造一个 TransformStream
时可以定义的方法和参数:const stream = new TransformStream({
start(controller) {
// 将会在对象创建时立刻执行,并传入一个流控制器
controller.desiredSize
// 填满队列所需字节数
controller.enqueue(chunk)
// 向可读取的一端传入数据片段
controller.error(reason)
// 同时向可读取与可写入的两侧触发一个错误
controller.terminate()
// 关闭可读取的一侧,同时向可写入的一侧触发错误
},
transform(chunk, controller) {
// 将会在一个新的数据片段传入可写入的一侧时调用
},
flush(controller) {
// 当可写入的一端得到的所有的片段完全传入 transform() 方法处理后,在可写入的一端即将关闭时调用
}
}, queuingStrategy); // { highWaterMark: 1 }
有了 ReadableStream
与 WritableStream
作为前置知识,TransformStream
就不需要做太多介绍了。下面的示例代码摘自 MDN,是一段实现 TextEncoderStream
和 TextDecoderStream
的 polyfill,本质上只是对 TextEncoder
和 TextDecoder
进行了一层封装:const tes = {
start() { this.encoder = new TextEncoder() },
transform(chunk, controller) {
controller.enqueue(this.encoder.encode(chunk))
}
}
let _jstes_wm = new WeakMap(); /* info holder */
class JSTextEncoderStream extends TransformStream {
constructor() {
let t = { ...tes }
super(t)
_jstes_wm.set(this, t)
}
get encoding() { return _jstes_wm.get(this).encoder.encoding }
}
const tes = {
start() {
this.decoder = new TextDecoder(this.encoding, this.options)
},
transform(chunk, controller) {
controller.enqueue(this.decoder.decode(chunk))
}
}
let _jstds_wm = new WeakMap(); /* info holder */
class JSTextDecoderStream extends TransformStream {
constructor(encoding = 'utf-8', { ...options } = {}) {
let t = { ...tds, encoding, options }
super(t)
_jstes_wm.set(this, t)
}
get encoding() { return _jstds_wm.get(this).decoder.encoding }
get fatal() { return _jstds_wm.get(this).decoder.fatal }
get ignoreBOM() { return _jstds_wm.get(this).decoder.ignoreBOM }
}
Polyfilling TextEncoderStream and TextDecoderStream 源代码,作者 Mozilla Contributors,基于 CC-BY-SA 2.5 或 CC0 协议使用。
ReadableStream
,读取流已经不是什么问题了,可写入的流使用场景也比较少。不过其实问题不是特别大,我们已经简单知道了流的原理,做一些简单的 polyfill 或者额外写些兼容代码应该也是可以的,毕竟已经有不少第三方实现了。Streams 浏览器支持总览,作者 caniuse.com,本图片为图表的截图,基于 CC-BY 4.0 协议使用。
onfetch
事件配合 Streams API 实现的。熟悉 Service Worker 的同学应该知道 Service Worker 里有一个 onfetch
事件,可以在事件内捕获到页面所有的请求,onfetch
事件的事件对象 FetchEvent
中包含如下参数和方法,排除客户端 id 之类的参数,我们主要关注 request
属性以及事件对象提供的两个方法:addEventListener('fetch', (fetchEvent) => {
fetchEvent.clientId
fetchEvent.preloadResponse
fetchEvent.replacesClientId
fetchEvent.resultingClientId
fetchEvent.request
// 浏览器原本需要发起请求的 Request 对象
fetchEvent.respondWith()
// 阻止浏览器默认的 fetch 请求处理,自己提供一个返回结果的 Promise
fetchEvent.waitUntil()
// 延长事件的生命周期,例如在返回数据后再做一些事情
});
使用 Service Worker 最常见的例子是借助 onfetch
事件实现中间缓存甚至离线缓存。我们可以调用 caches.open()
打开或者创建一个缓存对象 cache
,如果 cache.match(event.request)
有缓存的结果时,可以调用 event.respondWith()
方法直接返回缓存好的数据;如果没有缓存的数据,我们再在 Service Worker 里调用 fetch(event.request)
发出真正的网络请求,请求结束后我们再在 event.waitUntil()
里调用 cache.put(event.request, response.clone())
缓存响应的副本。由此可见,Service Worker 在这之间充当了一个中间人的角色,可以捕获到页面发起的所有请求,然后根据情况返回缓存的请求,所以可以猜到我们甚至可以改变预期的请求,返回另一个请求的返回值。onfetch
事件,然后用上我们之前学习到的知识,改变 fetch 请求的返回结果为一个速度很缓慢的流。这里我们让这个流每隔约 30 ms 才吐出 1 个字节,最后就能实现上面视频中的效果:globalThis.addEventListener('fetch', (event) => {
event.respondWith((async () => {
const response = await fetch(event.request);
const { body } = response;
const reader = body.getReader();
const stream = new ReadableStream({
start(controller) {
const sleep = time => new Promise(resolve => setTimeout(resolve, time));
const pushSlowly = () => {
reader.read().then(async ({ value, done }) => {
if (done) {
controller.close();
return;
}
const length = value.length;
for (let i = 0; i < length; i++) {
await sleep(30);
controller.enqueue(value.slice(i, i + 1));
}
pushSlowly();
});
};
pushSlowly();
}
});
return new Response(stream, { headers: response.headers });
})());
});
在 Service Worker 里 Streams API 可以做出更多有趣的事情,感兴趣的同学可以参考下之前提到的那篇《2016 - the year of web streams》
const a = document.createElement('a');
const blob = new Blob(chunk, options);
const url = URL.createObjectURL(blob);
a.href = url;
a.download = 'filename';
const event = new MouseEvent('click');
a.dispatchEvent(event);
setTimeout(() => {
URL.revokeObjectURL(url);
if (blob.close) blob.close();
}, 1e3);
这里利用了 HTML <a>
标签上的 download
属性,当链接存在该属性时,浏览器会将链接的目标视为一个需要下载的文件,链接不会在浏览器中打开,转而会将链接的内容下载到设备的硬盘上。此外在浏览器中还有 Blob
对象,它相当于一个类似文件的二进制数据对象(File
就是继承于它)。我们可以将需要下载的数据(无论是什么类型,字符串、TypedArray 甚至是其他 Blob
对象)传进 Blob
的构造函数里,这样我们就得到了一个 Blob
对象。最后我们再通过 URL.createObjectURL()
方法可以得到一个 blob:
开头的 Blob URL,将它放到有 download
属性的 <a>
链接上,并触发鼠标点击事件,浏览器就能下载对应的数据了。顺带一提,在最新的 Chrome 76+ 和 Firefox 69+ 上,Blob
实例支持了 stream()
方法,它将返回一个 ReadableStream
实例。所以现在我们终于可以直接以流的形式读取文件了——看,只要 ReadableStream
实现了,相关的原生数据流源也会完善,其他的流或许也只是时间问题而已。
ArrayBuffer
数据,也就是 zip 文件的数据。接下来就像之前提到的那样,我们基于它构造一个 Blob
对象并用 FileSaver.js
下载了这个图片。如你所想的一样,所有的数据都是存放在内存中的,而在生成 zip 文件时,我们又占用了近乎一样大小的内存空间,最终可能会在浏览器内占用峰值为总文件大小 2-3 倍的内存空间(也就是下图中黄色背景的部分),流程过后可能还需要看浏览器的脸色 GC 回收。TransformStream
并将可写入的一端封装为 writer
暴露给外部使用,在脚本调用 writer.write(chunk)
写入文件片段时,客户端会和 Service Worker 之间建立一个 MessageChannel
,并将之前的 TransformStream
中可读取的一端通过 port1.postMessage()
传递给 Service Worker。Service Worker 里监听到通道的 onmessage
事件时会生成一个随机的 URL,并将 URL 和可读取的流存入一个 Map 中,然后将这个 URL 通过 port2.postMessage()
传递给客户端代码。onfetch
事件接收到这个请求,将 URL 和之前的 Map 存储的 URL 比对,将对应的流取出来,再加上一些让浏览器认为可以下载的响应头(例如 Content-Disposition
)封装成 Response
对象,最后通过 event.respondWith()
返回。这样在当客户端将数据写入 writer
时,经过 Service Worker 的流转,数据可以立刻下载到用户的设备上。这样就不需要分配巨大的内存来存放 Blob,数据块经过流的流转后直接被回收了,降低了内存的占用。StreamHelper
的接口来模拟流的实现,所以我们可以调用 generateInternalStream()
方法以小文件块的形式接收数据,每次接收到数据时数据会写入 StreamSaver.js 的 writer,经过 Service Worker 后数据直接被下载。这样就不会再像之前那样在生成 zip 时占用大量的内存空间了,因为 zip 数据在实时生成时被划分成了小块并迅速被处理掉了。
课后习题 Q3:StreamSaver.js 在不支持 TransformStream
的浏览器下其实是可以正常工作的,这是怎么实现的呢?
pipeTo()
、pipeThrough()
方法方便地将多个流连接起来ReadableStream
是可读取的流,WritableStream
是可写入的流,TransformStream
是既可写入又可读取的流Response
对象,它的 body
属性是一个 ReadableStream
onfetch
事件可以监听所有的请求,并对请求进行篡改download
属性下载文件,Blob
对象,MessageChannel
双向通信……Blob
对象已经支持 stream()
方法了),能使用上的场景也会越来越多,让我们拭目以待吧。XMLHttpRequest
实现一遍,看看你还记得多少知识?const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://example.org/foo');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.responseType = 'json';
xhr.withCredentials = true;
xhr.addEventListener('load', () => {
const data = xhr.response;
// ...
});
xhr.send(JSON.stringify({ foo: 'bar' }))
在使用 XHR 初始化请求时会有较多的配置项,虽然这些配置项可以发出更复杂的请求,但是或许你也注意到了,发送请求时既有方法的调用,又有参数的赋值,看下来还是不如 Fetch API 那样直接传入一个对象作为请求参数那么简洁的。此外,如果需要兼容比较早的不支持 XHR 2 的浏览器,你可能还需要改成使用 onreadystatechange
事件并手动解析 xhr.responseText
。tee()
方法得到了两个流,但我们只读取了其中一个流,另一个流在之后读取,会发生什么吗?tee()
方法分流出来的两个流之间是相互独立的,所以被读取的流会实时读取到传递的数据,过一段时间读取另一个流,拿到的数据也是完全一样的。不过由于另一个流没有被读取,克隆的数据可能会被浏览器放在一个缓冲区里,即便后续被读取可能也无法被浏览器即时 GC。const file = document.querySelector('input[type="file"]').files[0];
const stream = file.stream();
const readStream = (stream) => {
let total = 0;
const reader = stream.getReader();
const read = () => reader.read().then(({ value, done }) => {
if (done) return;
total += value.length;
console.log(total);
read();
});
read();
};
const [s1, s2] = stream.tee();
readStream(s1);
readStream(s2);
例如在上述代码中选择一个 200MB 的文件,然后直接调用 readStream(stream)
,在 Chrome 浏览器下没有较大的内存起伏;如果调用 stream.tee()
后得到两个流 s1
和 s2
,如果同时对两个流调用 readStream()
方法,在 Chrome 浏览器下同样没有较大的内存起伏,最终输出的文件大小也是一致的;如果只对 s1
调用的话,会发现执行结束后 Chrome 浏览器下内存占用多了约 200MB,此时再对 s2
调用,最终得到的文件大小虽然一致,但是内存并没有及时被 GC 回收,此时浏览器的内存占用还是之前的 200MB。tee()
方法得到两段流,一个流直接返回另一个流用于输出下载进度,会有这样的资源占用问题吗?会不会出现两个流速度不一致的情况?其实计算下载进度的代码并不会非常耗时,数据计算完成后也不会再有多余的引用,浏览器可以迅速 GC。此外计算的速度是大于网络传输本身的速度的,所以并不会造成瓶颈,可以认为两个流最终的速度是基本一样的。controller.error()
抛出错误强制中断流,而是继续之前的流程调用 controller.close()
关闭流,会发生什么事吗?从上面的结果来看,当我们调用 aborter()
方法时,请求被成功中止了。不过如果不调用 controller.error()
这个方法抛出错误的话,由于我们主动关闭了 fetch 请求返回的流,循环调用的 reader.read()
方法会接收到 done = true
,然后会调用 controller.close()
。这就意味着这个流是被正常关闭的,此时 Promise 链的后续操作不会被中断,而是会收到已经传输的不完整数据。moz-chunked-arraybuffer
了。TransformStream
的浏览器下其实是可以正常工作的,这是怎么实现的呢?记得我们之前提到过构造一个 ReadableSteam
然后包装成 Response
对象返回的实现吧?我们最终的目的是需要构造一个流并返回给浏览器,这样传入的数据可以立即被下载,并且没有多余引用而迅速 GC。所以对于不支持 TransformStream
甚至 WritableStream
的浏览器,StreamSaver.js 封装了一个模拟 WritableStream
实现的 polyfill。当 polyfill 得到数据时,会将得到的数据片段通过 MessageChannel
直接传递给 Service Worker。Service Worker 发现这不是一个流,会构造出一个 ReadableStream
实例,并将数据通过 controller.enqueue()
方法传递进流。后续的流程估计你已经猜到了,和当前的后续流程是一样的,同样是生成一个随机 URL 并跳转,然后返回封装了这个流的 Response
对象。本文发布自 网易云音乐前端团队,基于 CC BY-SA 4.0 协议 进行许可,欢迎自由转载,转载请保留出处。我们一直在招人,如果你恰好准备换工作,又恰好喜欢云音乐,那就 加入我们!
2016年 - 2019年 趋势图
“风味(Flavors)”
前端框架
资料层
后端框架
测试
移动和桌面
Editor's note: This post was originally published on October 14, 2016 and has completely revamped and updated for accuracy and comprehensiveness.
If WASM+WASI existed in 2008, we wouldn't have needed to created Docker. That's how important it is. Webassembly on the server is the future of computing. A standardized system interface was the missing link. Let's hope WASI is up to the task! https://t.co/wnXQg4kwa4
— Solomon Hykes (@solomonstre) March 27, 2019
After all that explanation of what WebAssembly is and how it works in the browser, here we are throwing you a curveball: You can also run WebAssembly on the server. This may feel strange, but if Wasm is so awesome, why let the browsers have all the fun? Whereas Node.js solved this problem by bringing the JavaScript runtime environment to the server, Wasm built an environment and invited everyone to it.
The character that allows this plot twist is the WebAssembly System Interface (WASI). Because WebAssembly provides a fast and scalable way to run the same code across multiple machines, developers are pushing it beyond the browser, and that’s where the WASI comes in: Code outside of a browser needs a way to talk to the system. WebAssembly doesn’t need to talk to any single operating system, but rather, it needs an interface that lets it talk to a “conceptual operating system.” By introducing this layer of abstraction, WebAssembly can be run across all different OSs.
All of this probably doesn’t mean that Docker is doomed, but it might mean that Wasm containers start becoming increasingly common. Perhaps this layer of abstraction will allow Wasm to be the ubiquitous runtime for all applications regardless of language. Time will tell, but the foundations of this approach have been inviting promising solutions.
Atwood’s Law
It may seem odd at first glance, but I would like to close this article with some meditations on how WebAssembly relates to Atwood’s Law. The quote known as “Atwood’s Law” goes like this:
“Any application that can be written in JavaScript, will eventually be written in JavaScript.”
— Jeff Atwood, Author, Entrepreneur, Cofounder of StackOverflow
There are many misinterpretations of this quote, and however tempting it might be for snarky JavaScript developers to tout it as some evidence of divine authority, the actual source text was called “The Principle of Least Power” and it did not prophesize the subjugation of the internet by Angular, React, or Vue overlords. The actual message of that article was one that resonates deeply within all of us — within life itself, I would posit. In a nutshell, it says that there is real power in achieving more with less. This is as true with the evolution of life as it is with the internet and the formats that it uses to exchange data. If you can represent your data more simply, with less effort, then that representation will have an advantage over more complex and heady alternatives.
This is the subtle river that has been running through this saga from the beginning, and that’s where this story’s Rhinegold treasure truly lies: All of technology, WebAssembly included, attempts to disperse information, and its chances of survival are helped or hindered by how efficiently it succeeds at that task.
When viewed in this way, Atwood’s Law and its mention of JavaScript can be understood to mean a vote not necessarily for JavaScript explicitly, but an acknowledgement that simpler solutions will always prevail. JavaScript owes its ubiquity not to an arsenal of powerful things that it can do, but specifically to the calculated limitations that dictate what it cannot do. WebAssembly may be the next step forward in that evolution because it offers to do less than other alternatives. It even stores its instructions in plain old readable text instead of as unreadable binary, and this seems very much in keeping with the expansive goal of sharing data in the simplest means possible. It embodies the triumph of the bazaar over the cathedral, because exposing everything to the widest possible audience ensures that it can evolve, and this philosophy is to thank for much of Wasm’s success to date.
Nobody can guarantee the future, and it is absolutely possible that Wasm won’t fulfill all of our expectations. JavaScript founder Brendan Eich, who has been an advocate for WebAssembly, is concerned that conflicting competitive interests might end up splintering the Wasm project. Time will tell, of course, but WebAssembly is a technology that I definitely want to keep watching.
Need to generate Docx, HTML, RTF or PDF documents in your Xamarin App? Telerik UI for Xamarin has a solution for you - our new WordsProcessing library.
It has never been easier to generate, modify and export a document in your mobile application! The RadWordsProcessing library enables you to easily generate and export documents in various formats, including:
The library comes in handy all cases where document or PDF generation is needed, such as PDF invoice generation for an e-commerce app, or to serve the user with a filled application form in Docx format.
With our document processing library you can quickly access each element in a given document, modify, remove it or add a new one. Additionally, the generated content can be saved as a stream, file, or send to the client browser.
In this blog post, we will familiarize you with the RadWordsProcessing structure and the features it provides.
In addition to the smooth creation, modification, import and export of files, RadWordsProcessing gives you the ability to convert between variety of formats - convert Docx to PDF or HTML to Docx in your Xamarin apps. The currently supported formats are Docx, RTF, HTML, PDF and plain text.
private
RadFlowDocument CreateDocument()
{
RadFlowDocument document =
new
RadFlowDocument();
RadFlowDocumentEditor editor =
new
RadFlowDocumentEditor(document);
editor.ParagraphFormatting.TextAlignment.LocalValue = Alignment.Justified;
editor.InsertLine(
"Dear Telerik User,"
);
editor.InsertText(
"We’re happy to introduce the new Telerik RadWordsProcessing component for Xamarin Forms. High performance library that enables you to read, write and manipulate documents in DOCX, RTF and plain text format. The document model is independent from UI and "
);
Run run = editor.InsertText(
"does not require"
);
run.Underline.Pattern = UnderlinePattern.Single;
editor.InsertLine(
" Microsoft Office."
);
editor.InsertText(
"The current community preview version comes with full rich-text capabilities including "
);
editor.InsertText(
"bold, "
).FontWeight = FontWeights.Bold;
editor.InsertText(
"italic, "
).FontStyle = FontStyles.Italic;
editor.InsertText(
"underline,"
).Underline.Pattern = UnderlinePattern.Single;
editor.InsertText(
" font sizes and "
).FontSize = 20;
editor.InsertText(
"colors "
).ForegroundColor = GreenColor;
editor.InsertLine(
"as well as text alignment and indentation. Other options include tables, hyperlinks, inline and floating images. Even more sweetness is added by the built-in styles and themes."
);
editor.InsertText(
"Here at Telerik we strive to provide the best services possible and fulfill all needs you as a customer may have. We would appreciate any feedback you send our way through the "
);
editor.InsertHyperlink(
"public forums"
,
"http://www.telerik.com/forums"
,
false
,
"Telerik Forums"
);
editor.InsertLine(
" or support ticketing system."
);
editor.InsertLine(
"We hope you’ll enjoy RadWordsProcessing as much as we do. Happy coding!"
);
editor.InsertParagraph();
editor.InsertText(
"Kind regards,"
);
return
document;
}
private
void
ReplaceText()
{
if
(
string
.IsNullOrEmpty(
this
.findWhat))
{
return
;
}
RadFlowDocumentEditor editor =
new
RadFlowDocumentEditor(
this
.replacedDocument);
if
(
this
.useRegex)
{
Regex oldTextRegex =
new
Regex(
this
.findWhat);
editor.ReplaceText(oldTextRegex,
this
.replaceWith);
}
else
{
editor.ReplaceText(
this
.findWhat,
this
.replaceWith,
this
.matchCase,
this
.matchWholeWord);
}
}
The full demo project can be found here.
RadFlowDocument target =
new
RadFlowDocument();
RadFlowDocument source =
new
RadFlowDocument();
//...
// target will contain merged content and styles.
target.Merge(source);
Here the document you wish to add content to is called target and the document from which you wish to take the content is called source.Happy coding with our controls!
Blazor has entered official support phase and so applications will need to be ready for the world. This includes localization and translation into multiple languages - the ability to convert the text in the UI so that all your users can understand them in their native (or preferred) language.
As of the 2.4.0 release, the Telerik UI for Blazor components let you do that by exposing a service interface that you can implement to return the desired localized texts. If you don’t provide such a service implementation, we quietly fall back to the default English texts.
We follow the general .NET Core approach for relying on Dependency Injection (DI) to get work done in an app. There are so many ways you can implement this service, that I won’t even try to guess – databases, static resources (such as resx files), generated static classes, third party services, you name it – the world is your oyster and you can do whatever you need and reuse whatever you already have.
We will take a closer look at one approach that may let you reuse a lot of code/logic, especially if you have created other (Telerik-based) .NET apps. It is .resx files. In UI for Blazor we use the same resources as the UI for ASP.NET MVC suite, so you can carry over translations you’ve already made and use tools you’re already familiar with.
前言:有赞移动技术沙龙刚过去不久,相信很多同学对题为《有赞Android秒级编译优化实践》的分享还记忆犹新,分享中提到了全量编译提效与增量编译提效两种方案。本期我为大家详细介绍下基于EnjoyDependence的全量编译提效方案。
简介:狭义上EnjoyDependence是集依赖管理、构建发布、编译耗时统计等功能的Gradle插件。
前山海一样大,丛山峻岭,像凝固的浪花,一浪赶一浪,波澜壮阔。
春暖花开时节,嫩绿的叶苗像一支秘密部队,从条纹状的树皮下钻出,便一发不可收拾,发疯似的向天空和枝丫争抢地盘;要不了几天,扇形的树叶密密麻麻,隐起枝丫,遮天蔽日,挡风避雨,召集全村的麻雀都来过夜。
秋末冬初,风是染料,把碧绿的树叶子一层层染,最后染成黄铜色。
每到夏天,村子像得了疾病,把人折磨得死去活来。首先是忙,田地要劳作,畜生要侍候,屋漏要补,洪水要防,阴沟要通,茅坑要清,牛栏、猪圈、鸡窠、鸭棚、兔窝里的牲畜都来添乱,一堆事,像疹子一样发出来,日子再长也不够用。
每到夏天,村子像剥了壳的馊粽子,黏糊糊又臭烘烘的,人总忙叨叨的,各路虫豸也总不安生:苍蝇、蚊子、蟋蟀、萤火虫、壁虎、蚂蟥、蚂蚁、蜻蜓、蚂蚱、蜈蚣、毒蛇、蜥蜴、毛毛虫,四面八方冒出来,寻死觅活扎进人堆,加到我们生活里,给我们添乱、生事、生病,等着冬天来收拾。
爷爷讲:“绰号是人脸上的疤,难看。但没绰号,像部队里的小战士,没职务,再好看也是没人看的,没斤量的。”
我和表哥亲眼看见的,他满脸满嘴乌黑涂鸦的烟灰,像活鬼,哭得跟杀猪似的响,声音里掺进血,四面溅,惊得树上的鸟儿都逃进山,真正可怕!
一般一个大村庄总搭配一个木匠和一个铁匠,候鸟一样,贴着季节来去。
每到夏天,在萤火虫漫天飞的夜晚,在臭气熏天的天井或弄堂里,爷爷总是吃着烟,扇着篾扇,跟我和表哥讲这些那个。讲起这些那个,爷爷像老天爷,天上的仙,地下的鬼,人间的理,世间的道,什么都知道,讲不完。
讲着看着,月亮升起来了,村子安静下来,蛐蛐在石头缝里㘗㘗叫,水牛在栏里噗噗喷气,壁虎在墙壁上画画,老鼠在谷仓里唱歌,猫头鹰在后山竹林里哭泣。爷爷讲,它们前世都是人,作了孽才伏了法,转世做不成人,做了蛇虫百兽。
但群众一边斗争他,一边又巴结讨好他,谁家生什么事,村里出什么乱子,都会去找他商量。即使我爷爷,平时很讨厌他跟我父亲搅在一起,但只要家里遇到什么要紧事,照样要去请他拿主意,好像他才是真正的巫头,天下事都知晓。
村里无人不知晓,太监家有两只猫,一只全黑,一只全白,都跟小豹子一样,腰身长长的,头圆圆的,走路一脚是一脚,慢腾腾,雅致得很。我经常看见他用香皂给猫洗澡,用长柄木梳给它们梳毛,从头梳到脚,用金子小剪刀给它们剪趾甲,剪完又用砂纸磨。最气人的是,还专门给它们买上好的鲞吃!我父母从来没有对我这么好过,我吃过的鲞还没有他家猫多。我宁愿做他家的猫。我敢说,这也是我身边所有小孩子的想法。
农药在小爷爷肚皮里像灶火一样熊熊燃烧,要不是太监——不,必须尊称上校——及时赶来,一定会把他烧死。我亲眼看见,上校是怎么把小爷爷肚皮里的熊熊大火浇灭的,他先是往小爷爷嘴巴里塞进一块肥皂,灌他吞下去;然后扒掉他裤子,把他头朝地吊起来;然后又用打农药的喷壶往小爷爷屁洞里注水。农药壶有一个喷头,通过控制压力杆,可以把农药喷上树,射得比屋檐高。上校把喷头塞进小爷爷屁洞里,按住,一边拉压力杆,把满满一壶水都压进他屁洞里。这一定是痛的,小爷爷啊呀啊呀叫,叫着叫着,水从嘴巴哗哗吐出来。这水比屙出来的屎还要臭,熏得上校睁不开眼。上校睁开眼,对小爷爷儿子讲:“你爹死不了啦,给我去烧面吧。”这是老规矩,上校救活谁,谁家要烧碗肉丝面给他吃。有这样的老规矩,指明他不是第一次这样救人,只是我是第一次看到。这年我十一岁,已经跑得比爷爷快,所以爷爷派我去叫上校,要不我也看不到。
“喏,给你,不就是几块钱的事嘛,值得用性命去抵。世上命最值钱,我被人骂成太监都照样活着,你死什么死,轮不上。”
那天晚上,我第一次看到上校的眼睛,果然是明明亮亮的,比洁白的月光还要亮,一点不像个祟的鬼,像个英雄,堂亮得很。这是我重要的一个经历,我开始对上校生出好感,他救了小爷爷的命,也救了自己在我心目中的形象。我像被他吸着似的,跟着他出门,目送他远去,皎洁的月光披在他身上,照得他隐隐生辉。他走路的样子横竖不像太监,倒真是有些大军官的威风头,大踏步,高抬手,腰笔直,脚生风,一步是一步,昂首挺胸,雄赳赳,气昂昂,怎么看也不像裤裆里缺了东西。
秋天到了,柿子树叶开始变色,发黄,发褐,脱落,原来青绿扁圆的柿子也开始变色变样,变得发黄,泛红,赤红,红得火辣辣的,变得圆滚滚的,像一盏盏小红灯笼。灯笼密密匝匝的,挂满枝枝丫丫、节头梢头,远看整棵树像着火似的。这时,收获开始了,树上摘柿子、板栗、猕猴桃、酸勾子,地里刨红薯、洋芋、花生,水下挖藕、摸蚌。这是一年中最好的季节,不仅因为有收获,也因为风和日丽,天高气爽,可以出门远行。
小爷爷低头讲:“别把死挂在嘴上,我是死过的人,那罪不是人受的。”
爷爷像一棵盘根错节、枝繁叶茂的老榕树,上遮天下盖地,里三层外三层,天打雷劈都不怕,怎么会怕小爷爷莫须有的风雪预报?总之,爷爷活成一个老埠头,你要改变他是很难的,不像我。我像三月里的桃树,一夜之间变成一幅画、一本诗,花枝招展,灿烂得连自己都认不得。
凡是鼻子灵的人都有体验,上校家经常烧好吃的,尽管他家厨房深在院子里,看不见窗洞,但浓郁的香气会飞的,从锅铁里钻出,从窗洞里飘出,随风飘散,像春天的燕子在逼仄的弄堂里上下翻飞。
香气驱散了空气里的污秽,像给空气撒了一层金,像闪闪金光点亮了人眼睛一样,拉长了人的鼻子。
爷爷讲:“百草不如一木,百闻不如一见。”
爷爷告诉过我,上校生来就是个怪胎,胎位不正,又是头胎,他妈鬼哭狼嚎了两天,血流了一脚桶都没把他生出来,最后靠观德寺的和尚送的半枝人参,给她补足一口气才把他生下来。事后她去庙里谢和尚,和尚讲是观世音显灵救他们母子的,一句话叫她一辈子迷信观音菩萨。她把观音像请到家,供在堂前,天天烧香敬拜,求菩萨再显灵,给她添丁。菩萨不灵,求不到,她去庙里跟和尚哭,和尚对她讲,人要知足,不要占了前山还要后山,她是信的。后来丈夫死于非命,她又去寺里找和尚哭,和尚告诉她,要没有菩萨保佑,死的是她儿子,老子是替儿子死的,不幸中有大幸,她也是信的。再后来,听说儿子丢了宝贝疙瘩——那时老保长恨死她儿子,大肆散布谣言,村里连只狗都刮到风声——她又去对和尚哭,和尚劝她,这叫大难不死必有后福,她又是信的。总之,和尚讲什么她都信,从头信到脚,信到死。
爷爷讲:“这老娘们,和尚送她一口气,她还给菩萨一生世,实诚得不像人,像菩萨下凡,所以叫活观音。”
我后来觉得听他讲故事才是真正的“揩油”,比吃肉还过瘾。只是,这样的时节像蹄髈一样,并不多见。
必须是老太婆去普陀山的时候,也必须是上校吃足酒、人高兴的时候,他的故事才会一个劲地从嘴里噼噼啪啪出来,像酒气一样关不住。那时候他必是满脸通红,两只眼珠像电珠一样亮,手里夹着香烟,脚下盘着两只猫。空气里弥漫着烟雾和酒气,猫被呛得喵喵叫,他也不管。那时候他什么都不管,只管抽烟、喝茶、打饱嗝、讲故事。
我最欢喜听他讲故事,他闯过世界,跑过码头,谈起天来天很大,讲起地来地很广,北京上海,天南海北,火车坦克,飞机大炮,有的是稀奇古怪、奇花异草。民国哪一年,我在哪里做什么,有一天发生了一件什么事……他总是这样讲故事,有时间有地点,有人物有事情,情节起伏,波波折折,听起来津津有味,诱得蟋蟀都闭拢嘴不叫,默默流口水。
原来鬼子坦克开进一片原始荆棘林,毁了几十万只马蜂的老巢,那些马蜂都成了精,个头有蝗虫的大,数量也有蝗虫的多,散在空中,遮天蔽日,嗡嗡声连成一片,像沉闷的雷声在山坡上翻滚,卷起一阵风,吹得尘土飞扬。
那些马蜂如有灵性,知道是鬼子作了恶,要报仇,纷纷朝他们身上扑,肉里蜇,前仆后继,奋不顾身。鬼子虽有钢炮坦克,但在无数不要命的马蜂的疯狂围攻追击下,逃无可逃之路,躲无可躲之处,一个个在地上翻转打滚,痛哭嚎叫,最后无一幸存,尸陈遍野,尸体一个个又红又肿,像煺了毛吹了气的死猪。
另一个故事则让我暗暗发誓,长大一定要去上海看看,那个高楼啊,那个电车啊,那个轮船啊,那个霓虹灯啊,那个花园公园啊,那个十里洋场啊,那个花花世界啊,像在天上,像从头到脚都镀了金,连脚指头也不省略。
以开诊所作掩护,埋名隐姓,杀奸除鬼,刺探情报,过上一种恐怖又滑稽的生活:一边纸醉金迷,一边随时丢命。
吃着烟,喝着茶,打着饱嗝,喷着熏人的酒气,有时吊着故事主角的家乡口音,连声带色,自问自答,是上校讲故事的特点,成套路了。这不,他又开始老一套,拖着四川话的腔调,抛出一堆问号:
四川人开口离不开‘咋子’和‘要得’,咋子标致的人咋子要当尼姑?标致的人当婊子才要得是吧?当婊子也比当尼姑要得是吧?再讲,哑巴咋子识得了字?她识得字指明她不是天生的哑巴是吧?那她又是咋子成哑巴的呢?是病还是灾?是祸还是殃?到底是咋子了呢?”
给我印象深的还有一个故事,说的是民国三十二年,他在上海的五个手下的一个,那个会讲鸟语的家伙,被汪精卫的特务重金收买,把他一组人都卖个光。特务全城捕杀他们,死两个,逃两个,抓一个。抓的就是他,被敌人从电车上抓走,后来关押在湖州长兴山里的一个战俘营里劳改,四五百人,天天挖煤。一次山体塌方,把一百多人堵在坑道里,大家拼命救,几百人昼夜不停挖塌方。但塌方面积太大,十多天都挖不通,就泄了气,放弃营救——因为救出来也是死人,不划算。
上校讲:“只有一个人不放弃,一个江苏常熟人,四十多岁,入狱前在上海十六铺码头当搬运工,壮实得像一头牛。他有两个儿子,老大二十一岁,跟他在码头上做工,小儿子十七岁,做母亲的帮工,在乡镇上盘了一爿杂货店,卖油盐酱醋。常熟就是沙家浜的地方,是新四军经常出没的地盘。新四军也要吃饭,常来店里买东西,一来二往,把小儿子发展了,当了交通员,经常往上海跑,传情报,采购药品、枪械、弹药什么的。后来老小把老大也发展了,兄弟俩你来我往,成了新四军一条活络的交通线。”
那次塌方,父亲和上校是一个班的,躲过一劫,但兄弟俩都在里面。“这简直要了当爹的命!”上校讲,“从发生塌方后,十来天他就没出过坑道,人家换班他不换,累了就睡在坑道里,饿了就啃个馒头,谁歇个手他就跟人下跪,求人别歇。他总是一边挖着一边讲着同一句话——你们把我儿子救出来后我就做你们的孙子,你们要我做什么都是我的命。讲过千遍万遍,喉咙哑了还在讲。只要是人,长心眼的,听了看了他这可怜的样子,都情愿替他卖力卖命。”
可塌方是个无底洞,几百人轮流挖了十多天,都卖了命的,就是买不来里面人的命。眼看过了救命时间,狱头放弃营救,要大家去上班,只有他不放弃,白天被押去上班,夜里一个人去挖塌方。大家劝他算了,救出来也是死人,别把自己的命也搭进去。他呜呜叫,你不知道他在讲什么,因为喉咙已经着地哑掉,发不出声。但看他的空床铺,你知道他谁的话都没听进去,他的被窝成了老鼠窝。他本是搬运工,一个壮汉子,胸脯厚实得子弹打不穿,却眼看着一天天瘦下去。像日子是一把刀,在一刻不停削他、刮他、放他血水,血肉一层层剥下来,干下去,枯得像个鬼。
一天夜里有人打架受伤,上校去给人包扎,老远看见一个人在腊月的寒冷里踉跄着往坑道晃去。天已经黑透,只能看清一团黑影子,看不清模样,但上校知道他是谁——可怜的父亲!这些天他曾多次这样见过他,在黑夜的寒风里独孤孤一人往黑洞里奔走,但现在不是在走,而是在跌跌撞撞,一步三晃,几步一跤,像吃醉酒,糊涂得手脚不分,连走带爬的。夜里睡觉时,上校眼前老是浮现这身影,心里很难过,想他可能是腿脚有伤。他带上药水和几个冷馒头去看他,也想劝他回来歇一夜。去了发现,他已死在坑道里,半道上,离塌方还有一个几十米的弯道。他已经爬了几十米,几十米的坑道都是他爬的手印子、吐的饭菜,最后死的样子也是趴着的,保留着往前爬的姿势。上校讲:“我想他一定是想跟两个儿子死得近一些,就想把他抱到塌方段去葬。他本是那么壮实,大冬天,穿着棉袄棉裤,看上去还是很大块头,像你(父亲)。我以为要花好大力气才抱得起他,可一抱发现轻得像个孩子,像你(我)。我知道他已经很瘦,可想不到会瘦成这样子,完全只剩下一把骨头,骨头好像也枯了,朽了,轻飘飘的。我本来是鼓足力气抱他的,反而被这个轻压垮了,哭了。我前半辈子都在跟死人打交道,战场上手术台上死人见得多,从没哪个人的死让我这么伤心。我一路抱着他都在哭,葬他时也在哭,哭得喘不过气来,现在想起来都难过。”
战争年代,伤员多数是枪伤、刀伤,头破、肚皮烂、断脚、缺胳膊;军医多数是外科医生,擅长开刀、缝针、取子弹、接骨头、包肚皮这些血淋淋的手术。平时不打仗,医院清风雅静,清闲得很,前线一开战,伤病员一车车运来,军医累死都忙不过来。有些伤员伤势太重,生死难料,军医懒得管,怕忙碌一阵白忙乎,耽误时间。他们被丢在走道上,困在担架上,呼天求地,鬼哭狼嚎,有的受不了痛撞墙寻了死。医生见怪不怪,心肠铁硬,把他们当死人看,从他们面前匆匆过往,连给个口头安慰的工夫和心情都没有。他养伤了几个月,见的多了,胆子也大了,偷偷把那些被军医丢在走廊上的垂死伤员当活人救,练技术。反正救不了也没人追究,救活了是天上丢馅饼。就这样,他拿起手术刀,私设手术台,偷偷当起军医。几回下来居然救活几人,一下在医院出名,医院就留他当了正式军医。
什么是战争?就是活一天算一天,一天等于一生世,得空就要快活,及时行乐,死了不冤。
所以战争间隙,别人都去吃喝嫖赌找快活,他不这样,他埋头苦练本领,练枪法,练刺杀,练埋伏。他有自己的看法,做木工手艺就是生意,上战场本领就是性命,练好本领就是保护性命。他想到做到,仗打一路,他练了一路本领,也捡了一路性命。眼看战友死的死,伤的伤,他毫发不损,靠的就是有过硬本领,能打会躲。他枪法准到什么程度?你放飞手上的鸽子,他同时装子弹打,十枪九中。有这身本事战场上早迟要当英雄,
爷爷讲:“你看,他现在还养猫,不吸教训,不回头。他这人就这样,骨头太硬,心气太傲,仗着聪明能干,由着性子活,对老天爷也不肯低头。这样不好的,人啊,心头一定要有个怕,有个躲。世间很大,天外有天,山外有山,不能太任着性子,该低头时要低头,该认错时要认错。”
俗话讲不怕老只怕小,小鬼作恶老鬼哭。你不晓得,我早晓得,城里被这些小鬼搅翻了天,每天江面上都浮出无名死尸。这些小子心还没有长圆,做事没轻重,还是避一避好。
爷爷讲,大多数蚊虫到寒露节气就要死掉,寒露寒露,蚊虫无路,指的就是这意思。但叮过人、吃过人血的蚊虫,精气足,头脑灵,变得聪明,到了寒露时节会寻个暖和的地方做窝,睡大觉,养精蓄锐。这样就可以熬过三九严寒,死不了,变成蚊虫精,来年继续作威作福。我想,我和矮脚虎今天至少让几十只蚊虫都变成了蚊虫精,明年说不定还要再来吃我们的血。
爷爷讲,富春江里有大鱼,民国一十二年,富春江爆发百年不遇的洪水,村子里水深一米多,可以撑船;洪水退走时,他在我家楼梯下逮到一条七十八斤重的大白条鱼,那鱼立起来比我奶奶还高,躺在地上一身白亮,把整个灶屋都照亮。但这是一个阴谋,不等家里人把鱼吃完,我奶奶的寿命已经走完。爷爷讲,这鱼是阴府派出的考官,专门来考他的!他考败了,吃了鱼,丢了奶奶。从那以后,他在前堂摆设香炉、烛台、关公像,祭祖拜神,消灾辟邪,直到红卫兵把这些法器抄走。后来我家的日子越过越晦气,惹出一堆事,可能跟这个有一定关系吧。
这是我在村里最后一次见到他,月光下,他面色是那么苍白凄冷,神情是那样惊慌迷离,步履是那么沉重拖沓,腰杆是那么佝偻,耷拉的头垂得似乎要掉下来,整个人像团奄奄一息的炭火,和我印象中的他完全不是同个人——像白天和黑夜的不同,像活人和死鬼的不同,像清泉和污水的不同。
老保长哈哈笑:“提到死,现在排第一的不是你,当然更不是我,我是无论如何要排在你之后的。为什么?因为你满肚肠心思,心思多了,寿命就少了,这是阎罗王定的规矩,反不了的。”
父亲默不作声,摸出两根烟,递给爷爷一根。爷爷掏出火柴,先点了父亲的,再点自己的,然后两人边抽边走,回屋里去,黑暗中显得越发亲密,像一对难兄难弟。没想到一场来势汹汹的干架最后是这么友好收场,我看着他们愈来愈黑远的背影,心里甜滋滋的。天依旧黑乎乎的,我心里却暖洋洋的亮堂,像爷爷划亮的火柴旺在我心头。
爷爷讲过,村子的一年四季,像人的一辈子,春天像少小孩子,看上去五颜六色,生龙活虎,朝气蓬勃,实际上好看不中用,开花不结果,馋死人(春天经常饿死人);夏天像大小伙子,热度高,精气旺,力(热)气日日长,蛇虫夜夜生,农忙双抢(结婚生子),手忙脚乱,累死人;秋天像精壮汉子,人到中年,成熟了,沉淀了,五谷丰登,六畜兴旺,天高云淡,不冷不热,爽死人;冬天像死老头子,寒气一团团冒,衣服一件件添,出门缩脖子,回家守床板,闷死人。
这里有些是双关语,明的一层意思,暗底一层含义。有些含义好理解,++比如“夏天热度高”,既是指天气的热度,也是指人的热情,热火朝天的生活,狗都闲不住;有些含义却不大好理解,比如“夏天蛇虫夜夜生”,既是指大自然里的毒蛇害虫,也是指人口舌上的是是非非。夏天日长夜短,暑热难当,人都不爱待在家里,要出门,三五成群,四六抱团,散在弄堂里,聚在祠堂门口,吃饭、纳凉、打牌、下棋、看戏、闲聊天、拉家常,是制谣传谣、传播小道消息的好时节。去年夏天,上校失踪后,整个村子都在谈论他,真真假假,犄角旮旯都在浅吟低唱,蘑菇一样的,见风就长。他在“蛇虫夜夜生”的盛夏出事,注定是要被人大张旗鼓地嚼舌,嚼得遍体鳞伤。然后到秋天,盛况逐渐收敛,一路下滑,到冬天滑入谷底。翻过年,只是零星有人提起,提了就提了,气泡一样,风一吹就破了:因为终归是老故事,陈芝麻烂谷子,不可能出现风声四起的老行情。要出现老行情,必须冒出新东西,比如上校被捕了,审出案情真相了;或者小瞎子开口讲话了,揭开一堆秘密真相,等等。新东西迟迟不涌现,上校自然而然在离我们远去,这是大势所趋。++
爷爷讲,七月半,鬼一半:一半是活人的,一半是死人的。
按习俗,这一天活人要祭祀死人,做好发糕和桂花饼去祠堂的荫堂敬拜祖宗阴人。荫堂是阴曹地府和阳世人间接头的地方,接口,通道,平时不大有人去的,瘆人,但这天你又是必须去的,不去就是不敬祖宗,搞不好要被恶鬼缠住,更瘆人。一般人家都要派人去,去的多为老人、孩子、妇女。中午去的人最多,最热闹,繁碌时荫堂里都是人,像筷筒里的筷子一样插满,济济一堂,嗡嗡嘤嘤的,就有一种危险似的,像祖宗马上要破壁而出,房子马上会塌掉。
爷爷专挑中午去,带着我,去凑热闹。祠堂门口人来人往,空气里全是桂花和米酒的香气,浓厚得醉人
开学那天,我瑟缩着,拖沓着,几次拿起书包又放下,迈不开脚步。我怕同学瞎说八道……++同学是最爱瞎说八道的,无风三尺浪,见风就是雨,口无遮拦,舌头子尖,而且专挑你痛处捅,抓你小辫子,揪你烂尾巴,你哪里痛他们往哪里捅,朝你伤口上撒盐。++
大街上人多车挤,铺一层潮汐一样的市声,稀里哗啦的,穿来梭去的,是乱的,又是不乱的;两边橱窗一律亮堂,从吃喝到穿戴、到日用,一应俱全,招摇得搔首弄姿的,像是等你去拿,又是碰不着的,因为有玻璃隔着。玻璃,这么多玻璃!灯光,这么多灯光!像是全世界的玻璃和灯光都被集合到这儿,老保长来不及看,眼前和心里是一团乱,是碎掉的感觉。
因为村里有老虎山(后山),爷爷教过我许多跟老虎有关的成语俗语,比如初生牛犊不怕虎;虎毒不食子;将门出虎子;前怕狼后怕虎;一山不容二虎;有胆子摸老虎屁股;老虎嘴里讨不到肉吃;山中无老虎,猴子称大王;兵马不离阵,虎狼不离山;打虎要打头,杀鸡要割喉;人到四十五,正如出山虎;凤凰落架不如鸡,猛虎下山被犬欺;深山藏虎豹,乱世出英雄。龙要下海,打虎要上山;不入虎穴,焉得虎子;明知山有虎,偏向虎山行,等等,一大堆。
老保长为自己的一番苦心失效而失望,毅然起身走。“法子就是你咽下去!”他边走边骂,“++你这人就是自私,总想着要体面,把面子当命根子。他妈的,面子顶个屁用!我当初像狗一样活着,人家太监现在也是一只丧家之犬,小瞎子是废物一个,屙屎连屁股都不会擦,不都照样活着。照你这样想,我们都该去死,就你一个人活着。++”
这天我懂了一个新道理:++人和兽之间,只隔着一团愤怒++,像生死之间只隔着一层纸——后面这话当然是爷爷讲的。
瘌痢头。
上校聪明绝顶,怎么可能不懂这道理?他就藏在大陈村,和老母亲一起落脚在当地一个老庙里,庙里的大和尚是他母亲在普陀山修行时相识的。大和尚背上长一个瘤子,活的,年年在长个儿,已经大得像一只老太婆的瘪奶子,耷拉下来,走路晃荡晃荡的。天大地大,上校哪儿不去,偏投奔这儿,正是得知这情况,他可以帮大和尚驱病消灾,建立交情,然后留下来。
这里,我们的公安管不到,大街上没有通缉他的头像,没人知晓他是罪犯。一年多来,他天天晨早傍晚扫地,白天夜里陪母亲念经,念经的水平已追上大和尚。他甚至已经学会一口地道的萧山话,剃一个光头,穿一身僧服,没人看得清他的来历,也没人去看去想。
爷爷讲:“年轻人容易心碎,老人容易嘴碎。”
父亲本是闷葫芦一个,心思重,嘴巴紧,从此变得更闷,几乎不跟人言语,只跟猫讲话。
人们爱听瞎话,不爱听真话,正如大家互相不叫名字,爱叫绰号一样。
鸡奸犯的事害得我们一家人难受死,像得了某种丢人的暗病,说不清道不白:说是越描越黑,沉默不说是承认事实。我因此自卑得不行,像身后拖着一根大尾巴,时刻怕同学来揪、来踩。
爷爷给我备一把三角刀,专门用来对付可能出现的坏蛋,保护我和全家尊严。现在尾巴叫这公告彻底割断,我因羞耻而担惊受怕的日子从此一去不复返啦!
村里有人就拿捡来的子弹壳用锉刀磨一眼孔,做哨子,吹出来的哨音尖锋得很,吓麻雀贼灵光。
爷爷讲,麻雀灰不溜秋,一副贼相,贪吃,是农民的天敌,赶不尽,杀不绝;燕子一身漆黑,一副忠诚相,是农民的长工,所以家家户户留它们在屋檐下作窠。自古,远亲不如近邻,近邻不如长工,所以对长工是要待好的。
自贴出公告后,好似公安局在我们村里凿通一个窗洞,风来雨来,不时传来上校一缕缕音讯,众说纷纭的,如一锅热粥,四处冒泡,稀里糊涂,见不着个底,你不知道信谁不信谁。一种说法,上校骨头刚硬,在铁皮牢屋里被连吊几夜,肋排骨被打断几根,就是死不开口,宁死不屈。一种说法,上校当过军统特务,有本事对付公安,轻松耍花招,把公安蒙在鼓里,根本没挨打。一种说法,公安从省里请来专家,专家带来药,药无色无味,掺进白开水,上校喝下去,不过十分钟换一个人,问什么讲什么,一五一十全交代。种种说法都有人信,也有人不信,没威信。对上校肚皮上的字也是这样。
爷爷讲,++酒鬼嗓门大,死鬼乌珠大++。这话一点不假。
时值古历十月,蛇虫百豸死掉的死掉,躲掉的躲掉,销声匿迹,夜深人静。++当老保长闭口时,我听得见月光在屋顶上走动的声音,它们赶着黑暗,走入天井,爬上墙,天井变得更大,也更静了++。
爷爷讲:“月光爬上墙,人爬上床。”
你看他不停地把一个个老故事颠三倒四地讲,以为他早已倾家荡产,想不到还埋着这么大一个金矿。我无法想象一个整天酒醉糊涂的人是靠什么锁住这个金矿的,正如无法想象一个老酒鬼守着一缸老酒不喝一口。这个事实让我对老保长肃然起敬,我觉得我们所有人都应该尊敬他。
月光在老保长不语时显得更亮,好像沉默真的是金子,可以发光,照亮月光。老保长讲故事有门道的,每讲到关键处,总要停下来喝水,重新点一支烟。这是吊人胃口,也是为了把故事讲出门道:好像讲不下去,其实是要个停顿,摆个样子而已。
一路上,四方瞅见磕头烧纸钱的人,街头巷尾,香火缭绕,鬼影幢幢。十月半是又一个鬼节,俗称下元节,是三大鬼节的收官之节。这个日子上路,老保长心头多少有些不祥的预兆。
火车一路北上,也是一路停。一半是临时停,停下来都是一件事:查证件,抓汉奸。这年月,汉奸不是关在监牢里就是逃在路上,火车人多,好掩护,是汉奸逃跑的首选路线。老保长手头有一本证件,是姜太公给他备的,蓝面子,黑印章,有见官高一级的权威。坐他对面的是个书生模样的中年人,戴眼镜,穿长衫,言少笑多,待人彬彬有礼。首次查证件,他顺便刮了一眼老保长证件,然后便对老保长恭恭敬敬,给他递烟买包子,跟勤务兵似的。车上有不少军人,士兵军官,三五成群,吆三喝四,把自己当战斗英雄,把布衣百姓当鬼子,手下败将,想训斥就训斥,要座位就得让,横行霸道。书生悄悄对老保长讲,中国要有这么多战斗英雄,日本佬该早滚蛋了。
盘缠,证件,照片,是寻不着人的。++寻人得靠人,当地人,地头蛇。爷爷讲:“强龙不压地头蛇,天大地大地头蛇大。”++
姜太公在上海是一条暗龙,地头蛇,而各地的暗龙、地头蛇是响应的,如官官相护,青帮黑路私通一起一个样。临行前,姜太公交给老保长三封信,密封,编了号:1、2、3,张三李四,单位地址,一一写明,让他依次去寻人。运气好,三人中必有一主认他这个“娘舅”,帮他去寻见可能落难的“外甥”。
至此至时老保长恍然有悟,姜太公为什么有那么多忌惮和禁令,因为这年月汉奸实在太多啦,当汉奸实在太容易啦,上校被大汉奸包养,罪名上已是汉奸,谁敢保证他实际里不曾失过节?失过节,她周折此事便是自取其辱。
月光爬在墙上,久了,累了,都从墙上下来,匍匐在天井里,把灰白的地砖照得冒出冷气。
惊蛰不动土,春分不上山。清明吃青果,冬至吃白饼。立夏小满足,大雪兆丰年。鲤鱼跳龙门,雷公进屋门。朝霞不出门,晚霞行千里。
这些都是爷爷讲的,跟我讲,跟表哥讲,有时也跟非亲非故的人讲。有一回,我看到他在路上拦下我的几个同学,考他们:“你讲,为什么惊蛰不能动土?”
谁知道呢?谁也不想知道。你不想知道他也会告知你:“因为惊蛰是蛇虫百豸苏醒的节气,地里土里都窠着各种幼虫胎卵,娇气得很,动了土就要了它们命了。哪怕害虫也是性命,要让它们投胎活一世,不能叫它们投不了胎,死在胎盘里,这是做人的起码。”
老保长不止一次讲过,如果道理可以当钞票用,我们家笃定是全村最富裕的人家。我们家并不富裕,这话是带点笑弄爷爷的意思。爷爷不听见则罢,听见一定要顶他,有时讲:“如果做人不讲道理,吃再多的饭都是白吃,穿金丝绸缎也是马戏团里的猴子。”
大冬天,村子里是不大生事情的,精壮劳力大多被派去江北修水利,老人妇女大多待在家里,生火盆取暖,给孩子纳鞋底、做新鞋,只有小孩子在外头乱窜,在干涸的溪床里翻开石头抓冻僵的泥鳅螃蟹,刨开洞穴捉黄鼠狼和冬眠的蛇。
时近年关,村子里又闹热起来,最热闹的当然是春节头几天,家家户户都忙着拜年,走亲戚,迎亲戚,大人都在酒桌上,小孩都在数压岁钱。一般这种热闹要到正月初十才会冷下来,但这年春节一场大雪提前让热闹冷清下来。
气愤让我变成了废物。
爷爷也是,自挨老保长打骂后,一直呆若木鸡,傻愣着,既不还嘴骂也不叫苦申辩,好像老保长事先给他灌过迷魂药,他神志不清了,体面不要了,道理丢完了,成了个十足的糊涂蛋、可怜虫。我既觉得有些可怜爷爷,又觉得这里面可能有什么古怪:兴许是爷爷有错在先,他认错了。这么想着我心里少了气愤,多了紧张,怕他有错。不一会儿,父亲回来,像老保长刚才一样,也是一脸杀气,一声不响地走到爷爷面前,像刚才对老保长一样,一把揪住爷爷胸口,推他到板壁前,抵住,恶声恶气地责问爷爷:“你给我讲实话,是不是你向公安揭发了上校?是不是?讲实话!讲啊!”爷爷,你开口啊,不是的,你又不知道上校躲在那里,没人跟你讲过啊;爷爷,你快否认啊,你是冤枉的;爷爷,你一向懂得做人道德的,你不可能干这种缺德事;爷爷,你快讲啊,大声讲出来。可爷爷一言不发,一声不吭,只闭了眼,流出两行泪,虫一样爬着,鼻涕也流出来。看着这样子,我心都碎掉了。我号啕大哭,像爷爷死了。这个该死的下午,天地是雪白,可人是污黑的,坏人打好人,儿子骂老子,天理皇道塌下来,压得我窒息,心里眼前一团黑,恨不得哭死。
人就这样世故,你好给你锦上添花,不好给你雪上加霜。
我回家必须经过七阿太小店,然后进入祠堂弄。祠堂高,弄堂长,天空狭长一条,天色更加阴沉。++正常,我一分钟可以走完这条弄堂,我已经走过十六年,无数次,但这天下午的一分钟差点成了我一辈子:一块断头砖从祠堂窗口飞出,无声地冲着我坠落,擦着我背脊滑下,砸碎在地上。我只受皮伤,擦出一道血印子,但如果我晚半步,就是一辈子,比爷爷先死。++
++这天我终于明白,爷爷为什么从那天起不再出门,他像只老鼠一样,宁愿去猪圈里待着也不迈出大门一步。因为他知道,出门必定会有更多的窗户飞出断砖碎瓦,你无法寻出谁是凶手,凶手是风,是猫,是老鼠。我甚至怀疑小爷爷都可能这样作证,只有耶稣知晓他们在撒谎。++
但耶稣又会原谅他们的。
家像敌人的碉堡,有人无数次在心里想把它炸毁
死是如此活的、真的、近的,看得见,摸得着,像我养的兔子,就在我身边,我生活里。
我不怕死,我才十六岁,怕父亲打,怕母亲骂,对死是一点不怕的。但爱我的人怕!你别以为,我活得不如上校家的两只猫,就没人爱了。爷爷讲:“所有父母都爱自己的孩子,像所有树都爱阳光一样。”
第二天,父亲通知我别去上学,别出门;如果出门,必须由大哥陪着。他自己倒是夹着油布伞,带着干粮,冒着漫天的雪珠子出门了。他好几天才回来,然后第二天大清早又领着我出门,先乘船,后乘车,不知要去一个什么地方。我不知道这是一次漫长的离别,加上是大清早,我瞌睡蒙眬的,没有和爷爷告别。大哥送我到公路上,母亲送我到镇上,船埠头,抱着我咽咽哭一通,向老天求平安。母亲对我哭诉着:“你一定要平安,一定要回来看我。”
一切都是命,这话爷爷以往多回讲过。那天,我十分后悔离家时没有和爷爷告个别,我猜他一定为我的无情无义伤心死了。这大概是他的命,对我好言好待十六年,却没有得到我一分钟的话别。
爷爷讲过,一分钟的和好抵得过一辈子的仇恨,我和他正好反过来。
报纸上说,民航飞机是最安全的,因为所有核心机部都有双份,有预备。
当然遇到恐怖分子预备再多也没用,只有预备死。恐怖分子是当今人类的肿瘤——这也是报纸上说的。
城市有多大多美,我就有多小多丑,小得连名字都没有,大白天不敢上街,听到警笛就发抖。
偷渡客都这样,像阴沟里的老鼠,只能苟且活着,能找到一条阴沟卖命就是最好的活路。
报纸上说,人要学会放下,放下是一种饶人的善良,也是饶过自己的智慧。我这一生许多事都放下了,但有些事又怎么放得下?我在鞋厂给皮鞋钉了六年扣子,深知一个道理,扣子不是鞋带,可以脱下,扣子钉上去后就跟鞋子长在一起,脱不下的,脱下皮子就坏了。有些事长进血肉里,只有死才能放下。
一九九一年,我还没做生意,挣钱难,为了攒足一张机票钱,我得熬五六年时间,像养大一个孩子一样难。我说过,那时回国是伤筋动骨,但只要伤得起,不是粉身碎骨,我是不会放弃的。我已经等了二十二年,每天我用回忆抵抗漫无边际的思念,用当牛作马的辛劳编织回来的梦。
一切都是为了回来!像一个人不能把自己拎起来一样,我放不下回来的念想。一定意义上说,我活着就是为了回来。
谢天谢地,我总算等到了这一天:用二十二年等的一天!记得那天从售票台接过机票的一刻起,我的心就开始怦怦跳,像接到手的是一张生死命状,激动,紧张,害怕,兴奋,太多的情绪,太乱的心思,一路上我都天昏地黑的。等踏进家门,我一下咚地跪在地上,像这套纸票(我订的是中转往返票,便宜)有千斤重量,我负重竭尽全力挺一路,到家再也挺不住,累垮了。现在想起这些,我依然感到膝盖发胀,眼前浮现出妻子用手轻轻抹去我脸上泪水的情景,仿佛发生在昨天。
人活一世,总要经历很多事,有些事情像空气,随风飘散,不留痕迹;有些事情像水印子,留得了一时留不久;而有些事情则像木刻,刻上去了,消不失的。我觉得自己经历的一些事,像烙铁烙穿肉、伤到筋的疤,不但消不失,还会在阴雨天隐隐疼。
++哪里埋着你亲人的尸骨,哪里就是你的故乡++。
一九九一年,我行囊空空、疲惫不堪地回到家乡时,后山的老虎背上已多出三座我亲人的坟墓:一座是爷爷,一座是母亲,一座是我二哥——如果嫂子也算亲人,就有四座,是我未曾谋过面的二嫂。我在一个阴雨绵绵的春日(这是航线淡季)的下午小心翼翼地走进睽违二十二年的老宅时,父亲正落寞地坐在我和爷爷曾经睡觉的东厢房门前的躺椅上,一边抽着烟,一边看着屋檐水滴答在天井里结满污垢的青石板上。他把我当作走错门的人,抬头看我一眼,又低头抽烟,问我:“你找谁?”我叫一声爹,报出自己小名。他像只有二十二个小时没有看到我,没有些许激动——也许是怕激动,也许是要给我腾出时间,认识一下这不堪的老屋,目光自下而上、自外向里无精打采地睃视着,好像在告诉这些老墙、老门、老楼板:有故人回来了。屋子里弥漫着一股发霉酸腐的浊气,门楣上、楼板下、屋檐下、角落里,挂满蒙尘的蜘蛛网;几张条凳、竹椅横七竖八地散乱在前堂;堂前正壁贴着我熟悉的毛主席像,已经脱落一只角;阁几上灰扑扑的,像父亲抽了几年的烟灰都撒在上面,并被摊匀。屋里唯一干净的是那张我从前做作业的八仙桌,即使在昏暗的光线下依然泛出丝丝红光。我以为父亲痴呆了,数着他脸上一条条狂野、黝黑的皱纹。父亲瘦成了一把骨头,手背的青筋有他指头间夹的纸烟一样粗。足足过了一分钟,我再次叫他一声爹,报明身份。他丢掉烟头,看着烟火在雨丝里慢慢熄灭,终于开口:“你爷爷死了。”这我早就猜到,从几方面都猜得到。爷爷是那么要面子的人,当初一个鸡奸犯的假传闻都差点把他送进鬼门,何况后来全村人包括父亲集体公然向他发难谴责,他哪受得了?报纸上说,智者可以从过去摸到未来的痕迹。我不是智者,也从爷爷犯的错误中听到了他死亡的脚步声:一步之遥,触手可及。此外,我出去后每年都给家里写一封信,从没有收到一封回信。头些年我还苦等回信,后来根本不等了,写信只是告诉他们我还活着。家里只有爷爷能写信,他要活着一定会给我回信,哪怕明知第二天要死,也会给我写好回信。后来父亲告诉我,在我走后没几天,还没等到上校的申明告示,爷爷已经把命交给他的裤带,在猪圈里上吊了。上校的申明起的作用,也许只是没人去刨他坟墓。老保长一再公开扬言,爷爷没资格葬在村里任何一寸土里,他应该碎尸万段,喂豺狼吃。何况,爷爷要在世,已是年近百岁的老寿星,一个背负骂名、胆战心惊的人无论如何是享不到这福寿的。过一会儿,父亲又说:“你妈也死了。”这我从刚才看到的屋子的凄凉景象中猜得到的。++母亲要在世,这些灰尘蜘蛛网不可能这么耀武扬威在我面前。母亲是天底下最勤劳的人,屋子在她手里,哪怕是猪圈,地上的垃圾也不会过夜,板壁上、楼板下的灰网不会过月,如今它们成年不败的威风,显然是母亲入土化为尘灰的证据。我问父亲母亲是哪年走的,怎么走的——我希望是自然走的,不是自杀,也不是他害。父亲不理我,继续说:“你二哥也不在了,比你妈先走。”++
二哥是病死的,白血病。这对一个漆匠来说也许是职业病,但父亲不这么认为,理由是镇上漆匠多了去,只他一人得这怪病。我们三兄弟,二哥最像父亲,不爱说话,绰号叫铁疙瘩,心思被铁包着。所以父亲有理由认为,二哥是郁闷死的。父亲说二哥:“他就像被老婆戴了绿帽子,整天愁眉不展,闷声不响。他是被自己憋死的,也是我们逼死的,老辈子作了孽,他是替死鬼。”这是父亲后来说的,那天他像口丧钟似的,只报丧,++报了二哥,又报二嫂:“你二嫂也死了,比你二哥先死。”++二哥的性格不讨人喜,三十岁没女朋友。三十二岁,在父母亲极力张罗下,花钱从贵州买了个媳妇,年纪相差十岁。不知是语言不通的缘故,还是年龄相差大的原因,两人结了婚像结了冤,二哥经常不回家,回家就吵架。他宁愿跟家具、漆桶待在一起也不爱回家,像配给家具似的。据说二嫂到死也没学会我们这边话,因为二哥老不着家,没人跟她说话。因为语言不通,两人吵架经常动手,摔家伙,砸东西。一次,二哥下手重了,一巴掌打掉她一颗门牙,然后摔门走掉。二嫂哭了一夜,凌晨喝下一瓶农药。这是他们婚后第三年,遗下一个儿子,当天晚上我就跟他睡在一张床上,十一岁,长得一点不像二哥,在读小学。据说学习成绩很好,门门功课全班第一,这也不像二哥。二哥是反过来,门门功课班上倒数第一,所以早早退学,去学手艺。
** 父亲说完二嫂的死,我已经被死人包围,心里已胆怯得发抖,不敢去想大哥。看屋里这惨恻的败象,也不像有大哥大嫂鲜活的样子——当然可能根本就没有大嫂这个人。我问大哥是不是也走了,结果父亲说:“是走了,但人还在。”顿了顿,又说:“幸亏走,否则也活不成,你也一样。”++我一下泪满眼眶,好像在战场上,全军覆没,终于保下来一个也终于保住了我千辛万苦回来的价值。++父亲这样子,哪是欢迎我回来的样子——对他,我回来的价值是个负数,他巴不得我别回来呢。后来他告诉我,所以这么多年没让人给我回一封信,就是不想让多一个人知道我还活着。他怕死神恶鬼对着地址去寻我,追杀我。他已经认定,这村子是克我们一家的,他怕我回来,沾了晦气,活不成。**
大哥是去了秦坞,一个偏僻小山村,做了倒插门女婿。在生死面前他躲过一劫,但在荣辱面前,丢尽了脸面。长兄如父,再穷困潦倒的人家也不会把长子拱手出让,这是一个破掉底线的苟且,形同卖国求荣,卖淫求生。这是生不如死,是跪下来讨饶,趴下来偷生。我忽然明白,即使村里人已原谅我们家,但我们家却无法原谅自己,甘愿认罚赎罪。爷爷寻死是认罚,大哥认辱是认罚,二哥年纪轻轻抱病而死和我奔波在逃命路上,亡命天涯,又何尝不是认罚?
这一切,都叫我想起那次漫长的海上逃生之旅。那时我天天做着死的打算,夜夜做着死的噩梦,当终于上岸时,年少的我已变得像一个老人一样懂得感天谢地。
我和一群九死一生的同伴一起跪在码头上,一下下地磕头,引来一群海鸥好奇。它们从高空俯冲下来,翅膀扑扑响着盘旋在我们头顶,嘎嘎叫,仿佛我们在抢吃它们的盘中餐而破口大骂——我们的样子确实像鸡在啄食。
报纸上说,生活不是你活过的样子,而是你记住的样子。
毫无疑问,任何人要来住,都得拿出至少几天时间来收拾、清理大量时间残留的大量垃圾废物。说它是废墟也不为过,所有木头都朽烂,所有铁件都锈蚀,砖墙上长满青苔和各种虫卵,屋顶瓦楞间长出小树。这是一个被冷酷的时间无情啄烂的躯体,父亲大概至少几年没来看过它,他保留的也许是十年前的印象。也许,他认为鬼是怕鬼的,我住在鬼屋里可以借鬼杀鬼,保全自己。
他说会告诉我的,但不是在这里。他要求我马上回去收拾那边房间。他怕在这里对我多语,更怕我晚上住在这里。他慌张地睃视着四周,仿佛四周的鬼在偷听偷看我们。他心里已全是鬼。他自己也许并不怕这些鬼,是在替我怕。我告诉他,若真有鬼,我宁愿被自己家里的鬼所害,也不愿被上校屋里的那些野鬼所害。
第二天清早,我去镇上++请++了香火、冥钱,然后直奔后山老虎背上,给爷爷、母亲、二哥、二嫂四座新坟上坟。
胡司令——父亲叫他小胡子——坐在最左边,他已提拔到县革委会宣传部当什么股长,这天主要负责喊口号。他带着革命热情和个人感情工作,口号喊得特别响亮起劲,带表演性,有煽动性,把台下群众的革命热情一再激发出来,上校人没出来,礼堂里已经山呼海啸的杀声阵阵。
那天是大晴天,五月,天已经热了,上校只穿一件衬衫单裤,整个人轻薄得发飘,要不是被公安架着,后来又掀起的喊口号的热浪都可能把他卷走。
女人这次回来,随身带着一张结婚证明书,她要嫁给上校,一辈子照顾他,请求村里给上校出同样一份证明。村里又开会,征求大家意见。哪有反对的道理?都同意。于是便去镇上办手续,拍照片,就是我看到的那张照片。这年冬天,上校母亲刑满释放回家——这也是女人带上校回来的目的,算好时间的,专门等老人家出狱回家。老人家本来身体就差,在监狱里受累吃苦三年,身体差到底,走一步停三秒,吃饭要吐,只能喝粥,怎么看都像一支风中残烛。女人一边照顾一个病得下不了床的老人,一边照顾一个像小孩子一样懵懂无知的大人,比男人辛苦,比任何女人周到。在她的悉心照顾下,两个病人活得体体面面,一点不受罪。
父亲说:“村里人都说,上校妈一辈子拜观音菩萨,真的拜到一个观音菩萨。”
村里人都叫她“小观音”,也把她当观音菩萨待,她也像观音菩萨一样待全村老小。后来我听村里好多人谈起她,都说天底下这样的女人找不出第二个。
一年多后,上校母亲被一口粥呛死,她以嘹亮悲怆的哭声给老人家送终,哭声像鸽子的哨音一样,泣着血,盘在空中,照亮夜空,把村里所有女人的泪腺激活。后来送葬,她一手死死扶着棺材,一路洒着同样泣血奔泪的恸哭,把村里所有男人的泪腺也激活。所有跟我回忆上校母亲出丧那天情景的人,没有一个不带着迷离的神情,噙着泪,一种无法慰藉的悲伤像岁月一样抹不去。父亲说:“上校身边有这样的女人,这屋子的风水笃定是好的。”
这也是父亲所以要安排我到这儿来谈话,包括让我来这儿住的缘故,他认定我们家里有鬼,这儿笃定没鬼。这儿只有观音菩萨,两个女人都是观音菩萨,一老一小。做完婆婆“七七”后,女人把上校屋里的东西分好,能带的带上,不能带的都分给村里需要的人,然后领着上校和两只猫回她老家去了。猫是畜生,不知人间沧桑,只是年迈得走不动了,要用篮子拎着。上校体力还是好的,猫对他的感情也是好的,甚至更好,因为朝夕相处,相濡以沫一样的。父亲说:“两只猫在他手上拎着,像他人一样老实听话,他们就这样走了。”++村里出动几百人,男女老少,成群结队,送他们到富春江边,船埠头。船在汽笛声中离开码头,女人对着送行的村民长跪不起,抹着泪,上校像孩子随母亲一样,跟着跪下来,那情景把几百人都感动哭了。几百人哭的场面能感动所有人和所有时间,父亲在回忆中依然禁不住滚出泪花。父亲说:“从那以后我再没见过他们,我不想把身上晦气传给他们。”我想去看看上校和这天底最好的女人。父亲给我地址,是女人亲手写在一页作业本的纸上的。我看地址居然在上海青浦朱家角镇,是我返程去上海虹桥机场必须要经过的,更加坚定了我要去见他们的决心。++
邻近村庄,我知道它为什么叫桑村,村子被大片光秃秃的桑树包围。尚是早春,桑树一个绿芽也没有,但都被修剪过,像一条流水线上下来的产品,全一个样,低矮,整齐,一畦畦,放眼望去,让人想到一列列被剃光了头、整装待发的士兵,在沉默中等待冲锋。
这儿是一望无际的平原,人工开凿的河流,笔直,水面波澜不惊,两岸,裸露的土地黑得冒油。走进村子,房子一律青砖黛瓦,伞形屋顶,两层楼,带后院,像马德里的某些社区,统一规划建造的。
司机是本村的,一个毛头小伙子,我给他看女人和上校的婚照——我要送给他们,物归原主——虽然是快二十年前的照片,他居然一眼认出来,然后熟门熟路,直接把我送到他们家门口,并告诉我,这家男人精神有毛病。但同时也夸这家女人是个大好人,对自己有神经病的男人温柔体贴,照顾周到,对村民温良谦让。摩托车停在门口,他未经我许可,径直朝屋里大喊一声:“郎中奶奶,来客人了。”
不论从哪个方面看,这是一个被生活榨干的人,和我在照片上见到的人完全不一样。
出去头几年,尤其是头一年,我信写得勤,几乎月月写,写信是我用回忆抵抗不可遣散的孤独的唯一方式。后来因为老收不到回信,也是因为有了自己的生活,才写得少,越来越少,最后守住一年一封的底线。那些信,头几年的信,都是她读给父亲听的,所以她了解我不少情况。
“这么说,”她依然握着我手,开朗地笑道,“我们是老朋友了,我看了你那么多信。”生活把她榨干了,但她依然保留着乐观、热情,甚至不乏幽默。她手劲也不小,紧紧握着我手,我感觉得到。
一个是老态毕现却沉稳自如,一个是鹤发童颜害羞胆怯,两个人都远远走出了照片,走出了我的想象。
报纸上说,生活是部压榨机,把人榨成了渣子,但人本身是压榨机中的头号零件。
林阿姨告诉我,作为医生她知道,像上校这种在极端刺激下犯的疯病,只要得到及时治疗完全可以痊愈。但她在半年多后才得知情况,带他去求医,已经错过最佳治疗时间,结果就成现在这样,废了。她给我打一个比方:“像你手上挨一刀,哪怕断了筋骨,只要及时找到好的医生治疗完全治得好,留一道疤而已。但错过时间,伤口烂到骨髓里,只有截肢,不截肢最后会把你烂死。你该知道,他父亲就是这么烂死的。”
是的,我知道。我也知道,这是一种伤害性治疗,断臂求生。上校最后进行的就是这种治疗,把他正常的智力像截肢一样截掉,以抑制他的疯病。他现在的智力只有七八岁孩子的水准,而且是受过惊吓的孩子,特别怕见生人、大人。她建议我把他当小孩子看待,跟他亲热,带他玩,他会很快接受我的。我那时已有两个孩子,一儿一女,大的十岁,小的正好七岁。当我把他当我七岁的女儿待时,果然我们相处得很好,我说什么他都爱听,我问什么他都会讲,完全幼稚、天真、透明。我给他讲故事,他坐得老老实实的,跟他下跳棋,他比我儿子还那个来劲。他嘴上喊林阿姨叫老伴,实际上把她当母亲。
报纸上说的,当一个人心怀悲悯时就不会去索取,悲悯是清空欲望的删除键。
我心里都是上校的前世今生,都是悲伤,都是眼泪,都是苦涩。我预计,我出去后一定会找个地方痛哭一场。
这个摊在宽广的平原上的村庄和我的家乡完全不一样,它有一种开放和现代性,道路宽敞,房屋整齐,沿路有路灯、行道树,家家户户门前有花草,楼上有阳台,窗户挂着窗帘;有人手挽手在马路上散步,不时有自行车从我身边骑过,或者迎面而来:他们对我这个外乡人毫无兴趣,没有人对我投一瞥好奇的目光。二十二年后的我回到村里时,对任何人来说都是陌生人,我深有体会,当我在自己村庄的弄堂里行走时,我身上被多少好奇的目光抚摸过。这儿对陌生人毫无兴趣,它已经半城市化,在工厂里打工的大多是外省人。
报纸上说,中国自实行改革开放政策后焕发出了勃勃生机,从城市到乡村,从吃穿住行到思想观念,都发生了翻天覆地的变化。这一点,现在的我最有发言权,即使近些年我几乎一半时间在国内,因为有另一半的衬托、国外的比对,我照样时常生出惊异的目光、欣喜的心情。
多年以后,++年龄和成功赠予我豁达和宽容之心++,让我和命运达成谅解协议,对小瞎子生出同情心;一年又一年,同情心像树的年轮一样长,最后长成善心义举,真心帮助过他。但在一九九一年,我对他只有恨,恨之入骨!
即便回到马德里,我依然把恨留在村里,咒他快死。
印象很深,就在这个夜晚,我在上校的玩具间,在林阿姨给我临时铺的地铺上,上校阵阵如雷的鼾声令我辗转反侧,我在不眠的镜子里清晰地看到自己两个相杀的形象:一个是为上校的可怜悲悲切切,虚弱得无力闭上眼睛;一个是为小瞎子的可恨咬牙切齿,愤怒得可以拔刀杀人。
当月亮升起后,上校的鼾声像怕光似的一下沉落下去,沉得无声无息,随后我听到林阿姨轻微的呼吸声。她的呼吸声凌乱无序,让我想到她脸上的皱纹。++黎明时,东边天空中布满酒渣色的云层,我不知道它在天亮后是白云还是乌云++。
她说她是在前线医院里学会抽烟的,那时经常有缺胳膊断手的伤兵,他们苦闷,要抽烟,烟瘾大,自己没手,抽不来,都靠她喂他们抽,就这么不知不觉自己也上了瘾,像传染的。
我知道,抽烟可以一定程度地缓解人的焦虑。我也知道,是照顾上校的烦心把她的烟瘾又唤醒了。不是说久病床前无孝子嘛,还有什么比长年累月对付一个七老八十的小孩子更让人焦虑烦心的?她却不这么看,她说照顾上校让她感到无比安心,累是累,但累得有劲,有寄托,心里踏实。
++在乡下,人心像日常生活一样粗糙简单,黑白分明,分辨不了黑白交织出来的复杂图案和色彩。++
++爷爷就是例子,一错百错,一落千丈,死有余孽++。她怕自己成为我爷爷的复制品,甘愿人无端猜测,莫名礼拜。
++但战争一下把我们家毁汰了,阿爸、姆妈、二哥、姐姐,四个人在同一个时间被鬼子飞机炸死的炸死,淹死的淹死。当时我们一家人在同一艘船上,准备逃难,去南浔,阿爸在那边有朋友。其实待在家里反而没事,你看这房子,不是好好的?++这是命,不能回头说的。阿爸和二哥当场炸死,姆妈和姐是淹死的,她们和我都不会游水,只有大哥会,逃了命。我不知是怎么逃的命,反正等我有意识时已躺在河边,不知是谁把我救上岸的。这是我的命,命运等着我来吃一生世的苦。
我们回到村里,投靠阿爸的大兄弟。大阿叔人是好的,但大阿婶待人刻薄,经常饭桌上拉脸色,甩风凉话。大哥正处在青春期,吃不下冷脸色,一气之下翻了脸。好在住房、蚕房和桑田都在,生活设备也不缺,大哥也能养蚕,我也能照顾自己,可以凑合过日子。家里有盒粉笔,不知从哪儿来的
大哥每天在蚕房的竹柱上画一个叉,每次画时都对我讲:你快懂事,等你懂事了我就去当兵,杀鬼子报仇。画了一年半多,蚕房里的叉叉比蚕蛹还要多,一天早上我发现他房间空了,只留下一封信和一点钱,告诉我他走了,让我照顾好自己。我心里早有准备,并不意外和害怕。
两个月后,我收到大哥从长沙寄来的一封信,告诉我他已经加入薛岳将军的部队,在训练做机枪手。以后三年多我再没有收到他一丝音讯,收到时已是死讯,他已在一年前的长沙保卫战中牺牲,是邻村一个同他一起参军的人带回来的消息。那时我虽然才十二岁,但比二十岁的人都能干,洗衣、烧饭、养蚕、缫丝、纺线,样样能干。我得知大哥牺牲后,也开始在蚕房里画叉,每天画一个。我想大哥用粉笔画,丢了命,我改用刀刻,用剪刀。
我参军只为报仇,报不成仇,一家人白死了,我活着也是白活。当时我十五岁,已觉得活着没意思。这八年,我是靠仇恨养大的,仇恨死了,我活路也断了。
那天夜里,人家唱歌唱哑了喉咙,我痛哭哭瞎了眼睛,两只眼珠子肿得要从眶里脱出来。
报纸上说,++心有雷霆面若静湖,这是生命的厚度,是沧桑堆积起来的。++
我惊诧她在说这种杀人强奸的事时依然声色不动的平静,像在说抽丝剥蚕的平常事。
倒是隔壁上校,鼾声一阵阵的,时而高亢欢快,时而悲切沉吟,像在梦中历尽悲欢离合。
因为是逃走的,自然不敢回村里,怕被追杀。她漂在上海城里,颠沛流离,做过各类苦工,就是不敢去医院找工作,怕仇家顺藤摸瓜找到她。她吃得起苦,但能吃苦的人实在多,满大街都是跟她抢饭碗的人,竞争激烈,生计总出问题,最后还是斗胆去医院做护士。毕竟学过的,也毕竟是有门槛的活,专业的事,抢的人少,总算安耽下来,过了将近两年太平生活。
我见多了死人,家里人都死了,我对死不怕。
卡车连夜出城,往南京方向开,一路经过多座军营,每进一个兵营放下几人,多则五六个,少则三四个。我在第三天下午和其他四人被一起被丢在镇江郊外,金山寺附近,长江边的一座兵营。后来知道,这是一支舰艇部队,兵营不大,但房子一色是青砖或红砖房,看上去结实牢固,和我们一路上进的几座兵营不一样。这里明显好,以前有些兵营破破烂烂的,像野鸡部队。我庆幸自己被分到一座好的兵营,一路上的恐惧受到安慰,捡了便宜似的,对不明不白被抓来当兵的屈辱反而放下了。
我们五人被安排在同一个房间,四张铁床,上下铺。房间里基本生活设备都有,墙上贴着电影海报,桌上有女人专用的小圆镜、粉盒,甚至箱子里还有不少女人内衣内裤什么的,好像这些人刚死去。其实她们是逃走的。街上四处贴着传单,解放军要打过江来,当国民党死路一条,她们逃去寻活路了,我们是来抵死的。我们也想逃,但兵营里加满岗哨,夜里探照灯雪亮,扫来扫去,逃路堵死,大家只有等死。当天晚上我们各人领到一套军装和白大褂,有人说这是我们的寿衣。死归死,累归累,死是以后的事,累是眼前的事,颠簸一路,累得要死,躺下就睡着,跟死一样。
半夜里有人嘭嘭嘭敲门,说有急救手术,要我们出两人去配合。三轮摩托停在门口,引擎响着,看样子是很紧急。我和另一人去,坐上摩托,两分钟就到。手术室在一楼,我们进去,看到地上、手术台上、医生白大褂上全是血,像刚杀完猪。伤员死猪一样躺着,无声无息,奄奄一息。医生背影高大,手里捧着一堆肠子,翻着,动作麻利,在找创口。我们哪见过这场面,同去的人当场啊啊地呕吐,引得医生回头看。他戴口罩、头罩、手套,只露出一双眼睛:即使在雪亮的手术灯下,这眼睛依然放出亮光,像两只通电的电珠。他朝我甩头叫我快去帮他。我赤手空拳上去,他又朝我甩头,示意我戴口罩、头罩、手套。东西都是齐备的,就在旁边推车的托盘里。我全套戴好,配合他挖开肚皮,把更多的肠子拎起来。他很快找到创口,夸我手气好。然后三个多小时,他埋头操作各种手术器具,我负责递接。经常递错,他也不骂人,只说一个字:错。天毛毛亮时,手术终于结束,我替他摘去头罩、口罩、手套,脱下血淋淋的白大褂,看到他脸色苍白,面容僵硬,是一副累极的样
子。他没穿制服,白大褂里面是一件脱壳绒衣,大概跟我一样是临时从床上拉来的。绒衣洗过多次,黄色褪成灰色,看上去土相。他吩咐我一番护理的要求,叼着一根烟走了。我回头收拾手术台时才发现,整套手术器具,剪刀、镊子、切刀、缝针,大大小小,都是金子打的,刚才太紧张,没注意到。
上午九点多钟,他来病房查房,穿一套带上校军衔的制服,刚睡过觉,脸色红润,和我夜里见的人丝毫搭不上。他倒一眼认出我,问我伤员情况,也问我个人一些情况。知道我是被抓来的壮丁,他似乎猜到我想逃走的心思,劝我别胡思乱想,好好待着。他指着昏睡的
伤员告诉我,他就是逃的下场。原来我们熬了一夜辛苦,救的是一个逃兵,没有功劳,只有苦劳。
护士都逃了,当兵的也要逃,我想这部队必定打不了胜仗。
果然,一个月后,解放军打过江来,整座兵营只冒出几声枪响,解放军就顺顺当当接收了我们
一个月里,我和他没见过几次面,因为逃兵都不敢逃了,没伤员,他是不来医院的。据说他天天在家里养着猫,看着报,吃饭有人送,衣服有人洗,是长官的待遇。++有一次我在营区路上碰到他,他露出一口白牙对我爽朗地笑着,叫我一声名字,并问我,你是这名字吧?我说是的,停下来,等着他再问我话。他却没有下文,径直挺个胸脯,大踏步朝前走去。我听着他洒下一路铿铿的脚步声,像听音乐,心里喜悦,忍不住回头看他,希望他也回头看我。这是我长那么大头一次回头看一个男人。那年我十九岁,他三十一岁。他也是我这辈子唯一这么回头看过的男人。他没有回头,我心里空落落的,像他本来在我心里,就这么走掉了,心里就空了。++
++孩子们都一样,白天天不怕地不怕,夜里却常常为一只吞下大象的蚂蚁吓得要死,惊叫,尿床++。
她过去,像我去看女儿一样,观察一下,摸摸他脸蛋,帮他理理被子——应该是这些吧,反正我是这样的;如果醒了,我会哄一哄,一般哄两下又会睡过去——孩子就这样,睡觉是他们的拿手好戏。
“刚才说到,解放军顺利接管了我们,据说没费一枪一弹,我们听到的几声枪声是有人自杀,不是抵抗。”她一点不糊涂,不要我任何提示,只靠两口烟的过渡,恢复了淡然的神情,继续机械地往下说——
解放军和国民党军队完全不一样,他们到我们医院,迎面见到女护士,靠边站,等着我们过去再走。第一次这么遇到,吓得我不敢往前走,担心他们要调戏我。后来见多了,就觉得他们是好人。他们对俘虏制定的政策也上好,先谈话,劝你留下来,加入解放军,不愿意的给你发回家路费。找我谈话的人知道我是个孤儿,家里没一个亲人,说:那你没选择,留下来吧,解放军就是你的家,我们都是你的亲人。说得我当场流下眼泪,真像回家一样。
我继续留在医院,并受器重,被提拔当了护士长。解放军真把我当亲人待,对我特别照顾关心。不久上海解放,九月份,我被派去华东医护干部学校学习,就是现在解放军第二军医大学的前身。我学的是麻醉师专业,本来要学两年,后来朝鲜战争爆发,中国派出志愿军抗美援朝,学校发出号召,动员我们去前线保家卫国。很多人报名,我为了获得批准,用血书写申请,写血书的也是第一批被批准的。最后一共批准六十个男生、十二个女生,差不多装满一节火车厢,直接开往前线。
这年年三十,我们是在火车上过的,一路上成千上万的人拥在月台上给我们送饺子,一站站送,哪吃得掉?吃不掉没关系,我们装在网兜里,挂在车窗外冰冻
火车开出济南后等于开进冰箱里,一路都是冰天雪地
开过鸭绿江,那个冷,那个雪,我们南方人想不到的,鼻涕流出来就结冰。天是那么冷,但我们心里热火朝天,一路上唱着歌,跳着舞,根本不像去前线打仗,像从前线凯旋归来。
我们六个同学,四男两女,下车时手上都拎着一网兜冰冻饺子,当天晚上医院会餐,吃的就是我们带去的饺子,大家吃得开心死了。
我说我是谁,他听了不相信似的,对我左看右看,最后说,你怎么胖得像一头过年猪了,可以杀了过大年(元宵)。
那两年,尽管每天出生入死,不死也累得要死,但因为和他在一起,成了我这辈子最开心的时光,我心里越有苦就越是会梦见它。
同样的白炽灯泡,滤掉了苍凉的红光,变更亮了,因为多数工厂停业了,电力足了。她同样的脸,显更大了,因为疲倦爬上去了。疲倦加深了皱纹,下沉了眼袋,拉长了下巴,脸就变大了,更老相了。但她的精神还是好,越发好,记忆清晰,思路活灵,讲得很流畅,或许是美好的回忆在起作用吧。
她把最宝贵的青春和初恋落在朝鲜长津湖边的血土上,这片土地形同她故乡,会魂牵梦绕的,她没收不了,也归还不了。
++初恋的感觉是甜蜜的秘密,是紧张的等待、偷窥,是手不经意中相碰触电的感觉,是炮声轰轰中的害怕和祷告,是午后的阳光在风中行走,是微风吹来了稻花香,是彻夜不眠的累人旅程,是各种复杂幽秘、别出心裁的明测暗探。总之是细腻琐碎的,孤僻,怪异,情乱神迷,神神叨叨++。
整个晚上,我第一次出现听力疲劳的感觉,忍不住打断她:“总之你爱上他了。”“是的,”她脱口而出,“我这辈子只对他这么爱过,爱得小心翼翼又天昏地暗。”她又列数种种心花怒放又揪心断肠的细节、事迹,痴迷于逝去的青春和灼伤泪眼的甜滋滋的苦涩中,流连忘返。这是她毕生的辉煌,一生盘根错节的痛的根子,彩虹一样的、惊人的美丽,也是惊鸿一瞥的残酷。
她心里在燃烧,一颗孤寂的心在一往情深。没有人会忘掉自己的宝贝藏在哪里,也没有人会忘掉刺穿自己心的箭。我不忍心再打断她,就让她说个够吧,这不是修养,而是仁慈。
++我不知道具体是从什么时候开始爱上他的,就像我不知道他身上有哪一点是不值得我爱的。我爱他的笑声;我爱他的背影;我爱他抽烟的样子,爱他丢下的烟蒂;我爱他在手术失败后骂娘的愤怒,当然更爱他手术成功后的灿然笑容;我爱他遛猫逗猫的样子,那一定是他最得意开心的时候;我爱他义无反顾奔赴前沿阵地去出诊的英勇,爱他风尘仆仆回来的喜悦和痛苦。我们医院总共有七个外科医生,他去前沿阵地出诊的次数比其他六人加起来还要加倍的多。++
为什么要出诊?因为有些伤员伤势太重,下不了阵地,下来必死在途中。他闻讯后总是对其他医生说,别抢,我去,我要让我的金子(手术器具)多发光。那可能就是去送死,前线的枪炮是不认人的,敌机在空中专门找这种孤单的吉普车,认为里面一定是送情报的人或大首长。
好几次,我随他去前沿出诊,路上遇到敌机扫射,有一次一梭子弹正好钻进我和他肩并肩的夹缝里。我吓得哇哇哭,他笑道,谁说子弹不长眼?子弹知道我们要去救人,打死我俩等于要打死一堆伤兵,它下不了手。
有一次车子被地雷炸翻,滚入山沟里,司机当场牺牲,我下体出血,一只肩膀脱臼,痛得昏过去,他毫发不损。他常说,救人一命胜造七级浮屠,他造的浮屠已上千级,已经在天上,死神够不着他了。
真的,他那么拼命,几十次去前沿阵地救人,身边的人一个个死伤,他最严重的一次只是断过一个脚指头,其他都是擦伤皮肉,跟穿着铁布衫似的。也许这就是福报吧,但他现在这样子哪有福气?我说:“你就是他的福气。”她说:“听下去你就知道我是他什么了。”
他说,可我可能永远不需要妻子。他放开我,指着一旁牺牲的司机说,你看他,需要妻子吗?如果他有妻子,该有多痛苦,一辈子都要痛苦。
我还想说什么,他对我摆手,告诫我别说了。他说,在死者面前说这些是不合适的,对自己也不吉利,我希望你活着回国。至于我嘛,他一边给死者拭去脸上的血污,一边对着天空说,老天知道,我已死过多次,死了也无所谓,多活一天都是赚的。
以后,他再不带我出诊,我把这理解为是对我的爱,是在保护我。尤其是,他每次出去都把他心爱的猫托付给我,就更以为他心里有我。每次,我还他猫时都会塞给他一封信,写的都是我情真意切的感受,浓浓的爱和深深的怕,怕他不回来,怕他受了伤回来。有时我又希望他受了伤回来,当然不是重伤,只是轻伤,这样他可以养伤,我可以照顾他养好伤。他从来不给我回信,一声回音都没有。只有一次,他接过信时突然对我说:你对一个你完全不了解的人谈情说爱,是对自己的不负责任。我用他曾经说过的话说:在前线是最能了解一个人的。
我说我已经在最能了解人的前线和你相处一年多,我很了解你。他说,你的眼睛看不到我的过去。我说我要的是你的以后,不是以前。
我就是这样对他猛冲猛打,什么都不顾忌,狂热,什么姑娘家的矜持、面子、尊严,都放下,只要他一个字:爱!我亲人都死光了,太孤独了,太需要一个人来爱我,而天下哪里去找他这样的好人?英俊、能干、英勇、幽默,只要他答应爱我,我为他死的心都有,不答应我也想死。这种心情你可能很难理解的。
我想我是能理解的,那个孤独,那个渴望,我尝过,就在我出国头几年,那种举目无亲的感觉,那种什么都放得下的悲凉和狠心,像汗毛一样附在我身上,我像熟识老家的弄堂一样熟识。
他知道我住在那里,不顾死活来救我,披一床用水浸过的毛毯冲进大火,大声叫着:小上海!小上海!找我。
自我们在朝鲜见面后他一直这么叫我。他找到我时火已经在烧我辫子梢头,咝咝的声音,像蛇在喷气。
他把我扑在身下,先把我辫子上的火头灭了,然后灭四周的火,最后用湿毛毯裹着把房梁抬起,把我从死神手里夺回来。当时敌机还在轰炸,大家都还在东躲西藏,营救工作其实并没有开始,他完全是冒死来救我的。所以,我后来的命实际是他冒死救来的。
怕天亮后敌机再来轰炸,部队连夜撤离村庄,往山区转移。中途要经过一条溪,我受了伤,小腿撕开一道嘴巴大的口子,刚作包扎,不便下水。他背我过河,刚趴在他背上我便开始哭。五月正是雨季,溪里水满满的,深过膝盖,我哭着,他背着更累,上岸便一屁股坐在地上,大口喘着气。我仍然哭着,哭得稀里哗啦的。人累时容易生气,他突然训我:你哭什么!但马上又安慰我,哭吧,哭吧,死了那么多人,该哭,一边来拉我的手。我紧紧抓住他手,一头扎在他怀里,哭个够。黎明前的黑暗,伸手不见五指,我有种强烈的冲动,希望他吻我。
我说,如果我刚才死了,我在这世上什么也没留下。他说,今天晚上牺牲的人一半都这样,战争就是这么残酷。
我本来是希望他对我说,我至少给他留下了那么多信,我留下了对他的爱。但他没那么说,我只有直接讨。我昂起头,对他说:你给我留个吻吧,这样我死了至少留下了爱,和我给你的那些信是配的。他迟疑一下,低头吻了我。是那种吻,只有仪式,没有欲望。
我想我还是要活下去,我活着,至少可以每年回去给我死光的亲人上个坟。我不为活人活,只为死人活。
我就这么活下来了。
我不死,他在我心里也死不了。一天下午,我把他拦在路上,是要决一死战的意思。我直接问他,你到底要不要娶我?他看我这么决绝的样子,少见地端出一副诚恳的老实相对我说:我的小上海同志,你不了解我,我娶不了你,我这辈子注定是个光棍命。我说,你不娶我我就去死。他有些生气,说我这是在威胁他。他说,我千错万错,至少还救过你命,何必这样?接着又说,我是为你好,你这么年轻干吗要吊死在我这棵死树上?
心死了,人反而不会死了,只是活得像一台机器。
中间有一天他把我给他的十几封信还了我,那天,我把那日记本和这些信统统烧掉 ,完了去澡堂好好洗了个澡。我要把自己洗得干干净净,开始迎接新的生活。
他隐隐讳讳地向我透露,他听到一个风声,说我老头子在外面讲,我把身子给了他,可他怀疑我也给过别人,所以跟我绝了交。
“你信吗?世上没有不透风的墙,没有风飘不到的角落。”她端着一双青黑的眼圈——像长期戴眼镜留下的阴影——问我。不等我回答,她又替我回答:“反正我是信的,否则很难理解当初我在无锡军营里的风怎么会吹到上海,后来你老家的风又怎么会吹到这个村庄里。总之是吹来了,并且吹到我耳朵里,说得有名有姓,有经过,有结果,有人甚至连他肚皮上的字都一个不落地告诉了我。”
说到这里,她又低眉轻声地问我:“你知道他肚皮上的字吗?”看我摇头——我选择了摇头——她露出惊异的目光,“怪了,你们傍晚在一起这么长时间他都没给你看?”我说:“他想给我看,但我没看。”
她说:“这就对了。曾经他把那地方当罪恶和耻辱,宁愿杀人放火也不要人看到,要瞒住,现在他把它当宝贝,见到陌生人就要给人看,现宝一样的,我想拦都拦不住,拦他就要哭,你说这人已经变成什么样了。”
冥纸
但首先是他害了我,那个王八蛋。”“谁?”我抬头问,发现她正昂起头,冲着我。“他,那个向我报信的家伙!”她咬牙切齿地说,“那个在澡堂门前碰到我的主任,内科主任。”说着声音又低下去,仿佛怕隔壁老头子听见似的。她看我一眼,接着说:“其实事发半年后,当时我还在部队,这家伙当上副院长后我就怀疑自己被他当枪使了。医院缺个副院长,他和我老头子都是候选人,他资历比我老头子深,可我老头子是英模,当时又在南京干训班学习,他怕被抢去,便耍了这个阴招。”我问:“他怎么知道你们的事?”她说:“这也是那些年我一直在想的。我想不外乎两个原因,一个是老头子确实在外头说过这事,他性格豪爽又爱喝酒,有时失言也不是不可能;另一个是他看见老头子夜里去找过我。”
我说:“以他能把身上的秘密藏一辈子这点看,酒后失言的可能性不大。”她说:“是的,可以前我哪知道这些?何况……”说着停下来,摇着头,似乎是不想说了,又似乎为了隆重推出下面的话,“我希望是我老头子酒后失言,这样我心里要好受些。以前我就是老这么自己骗自己,想不到……”突然刷地挂下两行泪,啜泣说:“我老头子从来没有去找过我。”
我一时没听懂什么意思。她一把拭掉泪,看我一眼说:“那个人根本不是他,我完全冤枉了他!”
++报纸上说的,世上只有一种英雄主义,就是在认清了生活真相后依然热爱生活++
当牛作马的生活让我对生活只有恨,没有爱——爱被我恨死了,葬在大海里。
++一个十七岁的乡下傻小子,付得出死的勇气,却拿不出活的底气——当时我连“人生海海”也不知什么意思。她扑哧一下笑了,告诉我这是一句闽南话,是形容人生复杂多变但又不止这意思,它的意思像大海一样宽广,但总的说是教人好好活而不是去死的意思。++
++她说:“如果因为生活苦而去死,轮不到你,我排在你十万八千里前。”++
后来我知道,她家里很惨的,父亲被红卫兵打死,她哥哥去报仇,打死一个红卫兵,自己也被红卫兵打死。红卫兵分两派,一派杀上门,要斩草除根;一派暗中报信,想帮她和母亲逃走。她连夜逃走,母亲死守丈夫和儿子的尸体不肯走,宁死不走,结果受尽折磨,以死求了解脱。她逃回福建老家,东躲西藏,最后走投无路,只好用年轻的身子抵出头费,逃了出来。
++她说:“你不能死,你死了连给我上坟的人都没有,我的亲人都死了。”++
“++记住,人生海海,敢死不叫勇气,活着才需要勇气,如果你死了,我在阴间是不会嫁给你的。记得当初你向我求婚时是怎么说的?世上只有一种英雄主义,就是在认清了生活真相后依然热爱生活。++”
她把“你”又改掉,改回原样,然后告诉我,这是一个著名作家说的,叫罗曼·罗兰,她看过他两本书,抄下了他一本子话,其中就有这句话。她说:“你要替我记住这句话,我要不遇到它,你也一定遇不到我,死几回都不够。”
++其实那张报纸上根本没那句话,是她要送我这句话,用报纸的名义说,可以增加它的权威性,反正我也不懂西语。真的,我前妻真的是个好人,就是命苦,像上校。++
听了这情况,父亲眼睛倏地发亮,没有悲伤,只有侥幸的欣然,对我说:“难怪你能活着回来,是她替你死了。”我想说,是我替她在活,但话到嘴边被我咬住,不想说。++父亲的冷漠和自私让我觉得对不起前妻,而我宁可对不起自己也不愿对不起她,她是藏在我心中最深的痛,也是爱,我不许父亲在她面前失礼,给我丢脸++。
++多年后,我挣了钱,我把前妻的遗骸带回国,想和我爷爷、母亲他们几位亲人安葬在一起,也是将来和我葬在一起的想法。故土是热的,她孤零零一个人待在国外,太凄冷了,让我心疼。++
我们把铺子从巴塞罗那迁到马德里,已花光所有积蓄,到马德里又没挣到钱,一直做着青黄不接的生意,过着青黄不接的生活。生意是靠妻子撑着的,她去世后,我一个人开不了铺子,租不起房子,只好都退掉,过流浪汉的生活,露宿街头,靠垃圾堆里的过期食物填饱肚子。经常跟垃圾堆打交道,后来我也从垃圾堆里发现挣钱的门道。国外的垃圾堆尤其是富人区的垃圾桶里,经常有一些在穷人眼里值钱的东西,春夏秋冬的衣帽鞋袜,厨房里的锅碗餐具,甚至连收音机、唱片、唱机都有。有富人区必有穷人区,而且穷人总比富人多。
报纸上说,穷人区是大海湾,漫无边际,富人区是小湖泊,一小时可以绕一圈。
++不论春花秋月,白天黑夜,我都随身带着妻子的骨灰,她比任何一个活人都安慰我,给我活下去的力量++
++用她自己的话说:上帝把她的左腿借去了一寸,却赖皮不还她++
大多数人家都在溪对岸,前山脚下,造起新房,老村子成了一个旅游景点,每个周末都开来旅游大巴,带千里万里远的客人来观光,吃土菜,喝米酒;春天看竹笋尖尖破土而出;夏天进山打野猪——有人专门养的野猪;秋天摘野山柿野山枣——对不起,要称斤付钱的;只有冬天村子是安静的,还给本村人。
人生如戏,每一出戏都明里暗地连好的,如果我没有三年流浪汉的垃圾生活,就不可能有后来的垃圾生意;曾经垃圾让我丢尽脸面,如今垃圾加倍地偿还我尊严
他说:“垃圾是个宝啊,你有本事能把你那边垃圾搞到国内,保你发洋财。”他告诉我现在这里所有厂都需要垃圾,他的工作就是把垃圾进行分类,废纸归废纸,金属归金属,塑料归塑料,能当旧货卖的归一类。旧货翻新后可以当商品直接卖,金属卖给冶炼厂,塑料卖给化工厂,废纸留下来打成纸浆,造好纸。总之,都能卖,都是钱,垃圾里藏的是人们美好广阔的前途。
父亲怕我们去老屋,自己却坚守老屋,目的是要把鬼留在自己身边,别去找我们,是甘为我们当替死鬼的意思。他认为这些年我生意能做得这么好,风调雨顺,家里平平安安,靠的是他每天跟鬼死缠烂打,不让鬼出门来找我们。
有一次他跟我悄悄说,我们家里有四个鬼,每一个鬼的长相他都能描述出来,有一个长三只眼,有一个头上长角,有一个长一身白毛白发,有一个有头没脸,只有一头披肩拖地的长发。事隔没几天,他又跟我说,我们家里有三个鬼,全是男鬼,他又要对我描述每一个鬼的长相,被我打断。我说:“上次你说是四个。”他说:“被我搞死了一个。”
爱人是一种像体力一样的能力,有些人天生在这方面肌肉萎缩
回国一般都会顺便回家看看,回去免不了要看到小瞎子,他是村里最早一个“游客”,整天无所事事,东游西逛,逛累了就在祠堂门口待着,看人来人往,看人眼色,等人逗他、怜他。逗他的人不会怜他,怜他的人不会逗他。但对他来说,逗他其实也是怜他,因为太无聊了,无聊到被人奚落、看洋相也是他的乐处
小瞎子能活下来,不冻死,不饿死,全靠他父亲壮烈的死。老瞎子算了一辈子命,真正算清楚的只有自己儿子的命,他知道自己死后儿子废物一个,活不成,要活下去,必须靠村里有人发大慈悲,小慈悲都不行。小慈悲是同情心,是眼里冒出来的,触景生情,有一搭没一搭的,不成流。大慈悲是责任心,是心底长出来的,因缘而生,细水长流。他要给全村人埋下一个缘故,心里种下一份责任,去世前在祠堂门口长跪不起,胸前挂一块牌子,写一段话,见人就说:“全村的父老乡亲,我该死,对上校作了恶,罪该万死。我死了就去天上给你们看门守家,只拜托你们看好我儿子,让他活个天寿。他死了照样去天上给你们看门,守你们家家老少平平安安,发财发福,好运不断。”跪了三天三夜,说了百遍千回。跪得祠堂门口的石狮子的心都嘣嘣跳了,慈悲了,说得荫堂牌位上的列祖列宗都听见了,发话了。
村里一拨拨的人:老人、妇女、村干部、老师,凡是有头脸的人、有知识的人,都去对他应允、许诺,想拉他起身。可就是拉不起,谁都拉不起。他是决心要跪到死为止的,死的姿势都是跪着的,拜着的,磕着头,就这样壮烈地以死相求,以命相托。
正是靠着这个“缘故”的造化,小瞎子才得以杀破各路死神的层层包围,熬过一个个漫长的冬天和黑夜,没有死。死是没有死,但终归是活得苦难,命悬一线,熬着,煎着,挣扎着,随时可能断线、脱底。我后来每次回家,看他越发生不如死的样子,总担心他熬不下去,熬到头了,等不了我下次回来。但他的生命力十分顽强,也许生不如死的生是最富有生命力的,也许老瞎子在保佑他,也许死神也不想接收这种人不人鬼不鬼的活鬼。
++这是我的胜利,我饶过了他,也饶过了自己。++我战胜了几十年没战胜的自己,仿佛经历了一场激烈的鏖战,敌人都死光了,一个不剩,我感到既光荣又孤独,孤独是我的花园。
++人比人气死人,我不跟人比,只跟自己比。报纸上说,幸福是养自己心的,不是养人家眼的。++
三年前他得过一次中风,右手废了,左手认为自己离死期不远,除了学会用瓢羹吃饭外,懒得去学右手的其他手艺,包括洗脸。他的眼睛基本上也昏得什么都看不见,大概只能看见死亡。++他在心甘情愿地等死,但死亡像悬在猪圈椽子上的一张破蜘蛛网,看上去摇摇欲坠,似乎马上要掉落,却总不掉落,甚至挂得越来越牢++。
“让你别来这里,又来了。”每次去看他,父亲总以这句话开头。有一次我曾说:“因为你没死。”他说:“你就当我死了就好了。”++和晚年的父亲相处,让我得出一个结论:世上最无情的是老人,其次是有钱人。老人因为怕死或不怕死而变得无情++,有钱人因为可以用钱买到无情而变得无情。
一年后父亲临终前还在惦记这事,问我:“你打听到西安那个大师了吗?”看我摇头——其实已看不到,只是感觉到我在摇头——他又重复了那句话:“人没双手,就不像个人。”意思很明确,希望我去落实这件事。父亲给我的遗言只有两个,一个就是它:还小瞎子一双手;另一个是把老宅卖掉,卖不掉就拆掉,因为这是个鬼屋,让它见鬼去吧。两个交代根子上是一脉相承的,都是怕鬼来缠我,包括小瞎子以后将变成的鬼。
报纸上说,++多数人说了一辈子话,只有临终遗言才有人听;如果临终遗言都没人听,这人差不多就白活了。++
蚕房里有两排像脚手架一样高的木架子,架着几十个篾编的方匾,每个匾里都躺满淡绿色的蚕宝宝。它们真的是宝宝,娇气得很,冷热不行,要常温——最好是摄氏二十四度,每隔三小时进食一次,夜里也慢怠不得,一夜不进食,第二天只能当鸡食。
进食的桑叶必须鲜嫩,洗干净,当日吃,吃了过夜或不干净的桑叶,蚕宝宝就过不了夜了。因为娇气,养蚕的人必须花足力气,每天日出之前和日落之后两次去采桑叶,夜里至少两次起夜添食,总之要起早摸黑,熬更守夜。一般养这么两架蚕至少得双人,但上校一人比两个人还顶用,还养得好。
阿姨告诉我说:“村里有一半人家养蚕,公认养得最好的是老头子,他养的蚕个大,病少,出匾率高,出丝率也高,卖的价钱也高。”
我问:“有什么窍门吗?”她说:“认真,他像孩子一样认真听话,我教他什么他做什么,决不打折扣。
或许,和正常人相比,上校最大的特点——也是弱点——是不会打折扣,不会偷懒,不会像大人一样算计,甚至也不会疲倦。我曾多次到现场看他干活,那个恪尽职守,那个专注潜心,只有机器才能跟他比。比如采桑叶,人家一把把抓,他一片片摘,老的不要,虫啃过的不要;清洗也是,一片片洗,摸着洗;喂食严格听闹钟的,闹钟一响,拔脚就走;天气热了,他给蚕宝宝扇扇子,一匾匾换着扇;冷了,用报纸糊住四面漏风的竹排缝,用干稻草铺满架子添暖。他可以一个小时一动不动地守着蚕宝宝,也会为几只蚕宝宝的死大把大把地流泪,涕泪滂沱。阿姨告诉我,她曾教过他多种作业:种菜、烧饭、养鸡鸭等,包括养猫,都学不会,唯独养蚕,一教就会,一做就喜欢,一头扎进去,一年比一年得心应手,好像命中注定要来这个以养蚕为业的桑村跟她会合,当养蚕高手;也好像,命中注定他要一辈子在各方面施展才华,哪怕被命运打趴在地,依然要绝地反击,在蚕宝宝面前露一手,正常的大人都不是他对手,像
有一次,我看他一下午都在给蚕宝宝扇风,扇得挥汗如雨的,看得我特别伤感,忍不住去抱住他哭了。他对我嘘一声,说:“别吵,蚕宝宝在睡觉呢。”
报纸上说,++生活是如此令人绝望,但人们兴高采烈地活着。++这说的是晚年的上校吗?我视晚年的上校如父,所以一直坚持去看望他们,尽量奉献一个晚辈的孝心和责任。
说起三十几年前的神医大师,阿姨根本不记得他地址,只记得确有这么个神医。“可神医也续不了自己寿命,”她说,“我记得那时他都已是七老八十,现在该早作古了吧。”
其实,即使人活着,地址记着,该也是寻不着人的,中国现在已没有几个老地址可供人寻的。再说即使人活着,我寻着他,甚至寻着比他更牛的大师神医,我想也还不了小瞎子一双手,多少年前的陈伤旧病,回天比补天还难。常识总比真理知道得多,常识告诉我这是一个荒唐的愿望。
阿姨是医生,比我更确定这件事的荒唐性。“谁要说他能帮你如这个愿,他就不是什么大师,而是大仙、大骗子。”阿姨说,“你父亲老糊涂了,他说这话说明他的智力已经跟我老头子差不多了。”
这回,我告别时上校正在吃午饭,他的饭量比我还大。++阿姨送我到门口,对我苦笑道:“你看他这胃口,我真担心自己活不过他,先走了。”这话像游荡在这屋里的幽灵,每次来我都会冷不丁撞到。每次撞到,我都会看到她被乌云笼罩的脸和被恐惧刺伤的心,有时脸上挂着两行泪,努力地向下蜿蜒——有时我觉得这是两滴血,有时我觉得这就是他们两个人,两个人的生活,活得吃力、孤独、凄苦,凄苦得只有用眼泪来洗掉眼泪,用孤独来驱散孤独。++
++乡亲面前自大不得的,即使你升到月亮上,你的祖宗还在他们脚下。++
但他现在的精神世界是不会空虚的,因为有一堆人围着他,顶着他。他把自己扮成一位出身算命世家、精通阴文的算命先生,跟这人聊生死,跟那人谈得舍,说得头头是道,忙得不亦乐乎。他几乎无时不刻不在网上出没,像雇着几个替身,什么时间都在线上,什么问题都能对答如流。生活摧残了他,让他过着活鬼一样的生活,也让他穿越了生死恐惧和世态炎凉,变得大彻大悟,笑傲江湖。
他在网上人气很高,人缘很好,众星捧月的。他找到了自己的江湖,在虚拟的世界里生龙活虎,活蹦乱跳。
正如报纸上说的:++网络让无数的人在希望中死去,在绝望中诞生。++
老头子,我替你成全了,你就安心走吧,下辈子你就放放心心娶我。”
++死人不怕冷,只怕脏。++
她一遍遍默默又细致地用双手熨着白布,其实是在抚摸上校遗体,是一副舍不得。我注意到她泪水滴下来,滴在白布上,一滴一个印。
++隔壁始终没有动静,阿姨一定是累倒了,睡着了。我想让她多睡一会儿,一直等到八点钟才过去看他们。阿姨确实睡在床上,但样子有些异常,换过衣服:是一套崭新的黑色西服,和上校穿的寿衣一模一样;床头柜上,端端正正放着一页信笺,上面压着一对黄金婚戒;床头柜前,立着原先置于墙角的移动输液架,架上吊着一只最小的药瓶。药瓶滴出的一般总是治病救人的药水,但这回却是夺人命的。一切都是蓄谋已久的,作为一个前麻醉师,阿姨以最专业的方式结束了自己,追随爱人而去。她不能选择和上校同时生,却可以选择同时死。她选择和上校同时死,是为了来生与他同时生吗?++
报纸上说,++没有完美的人生,不完美才是人生++。我哭着,想着,不知道我的哭声能传到多远,能唤来多少阴阳两界的灵和人为他们送行?
HHKB BLE Mod
。通过替换掉 HHKB 官方原厂的主控芯片,你的 HHKB 立马就能从有线变为蓝牙/有线的双模版本,意思是既可以保留之前有线的功能,通过 USB 连接电脑使用,也可以通过蓝牙无线的方式连接电脑使用,所谓一举两得的解决方案。HHKB BLE Mod
是一个集成度较高的主控模块,所以在安装上没有太大难度。我没有电烙铁,也想偷懒,于是让卖家帮焊上了 LED 灯,有兴趣的同学可以自己 DIY 一下 LED 灯。HHKB BLE
并匹配连接,大功告成!Shift
键和b
键,键盘会重启,此时立马再按住Esc
键不放,键盘就会进入 U 盘刷机模式。在 Mac 的笔记本下,你会发现文件浏览器里面多出了一个可以移动设备。F
键和J
键保持三秒左右,即可重新激活键盘,非常方便。
这有一些建议帮助你的全球化开发团队能够更好地理解你们的讨论并能参与其中。
( ↓↓ —— 未完 —— ↓↓ )
完美阅读及吐槽,请猛击:https://linux.cn/article-11705-1.html?utm_source=qqmail&utm_medium=qqmail
本文是 24 天 Linux 桌面特别系列的一部分。如果你还在怀念 GNOME 2,那么 Mate Linux 桌面将满足你的怀旧情怀。
完美阅读及吐槽,请猛击:https://linux.cn/article-11703-1.html?utm_source=qqmail&utm_medium=qqmail
完美阅读及吐槽,请猛击:https://linux.cn/article-11704-1.html?utm_source=qqmail&utm_medium=qqmail
Continue reading "How to Deal With Software Development Risks in Agile Environment?"
The post How to Deal With Software Development Risks in Agile Environment? appeared first on CMARIX.
Continue reading "E- Commerce Trick Box: Boost E-Commerce Business This Holiday Season"
The post E- Commerce Trick Box: Boost E-Commerce Business This Holiday Season appeared first on CMARIX.
Continue reading "How to Upload and Publish Apps on Google Play Store?"
The post How to Upload and Publish Apps on Google Play Store? appeared first on CMARIX.
Continue reading "Most Notable Magento E-commerce Development Trends for 2020"
The post Most Notable Magento E-commerce Development Trends for 2020 appeared first on CMARIX.
Continue reading "How to Utilise the Caching Mechanism in Angular for App Development?"
The post How to Utilise the Caching Mechanism in Angular for App Development? appeared first on CMARIX.
Continue reading "CMARIX TechnoLabs Recognized As The “Best Software Development Company” At VSTS 2019"
The post CMARIX TechnoLabs Recognized As The “Best Software Development Company” At VSTS 2019 appeared first on CMARIX.
config.plist
原则上支持各种机型引导安装;CLOVER
到v2.5k r5100
PE
引导分区,同时支持CLOVER
/ PE
双引导lilu
v1.4.1
AppleALC
v1.4.3
WhatEverGreen
v1.3.6
,根本上解决黑屏问题USB端口限制
,减少禁行发生的几率;增加Lilu
崩溃的日志信息显示;apfs.efi
同时去除日志显示;其它的驱动位于 drivers/off
;AptioMemoryFix-64.efi
添加OsxAptioFix2Drv-free2000.efi
该驱动位于/EFI/CLOVER/drivers/off
目录下 或者 Slide值获取及计算Lilu
崩溃日志的输出信息,详见:macOS Catalina 10.15安装中常见的问题及解决方法;WhateverGreen
到v1.3.6
(12月11日编译,根治安装/更新中的黑屏现象),原生支持UHD620/UHD630等八代核显,不需要注入platform-id
, 同时它也支持NVIDIA和AMD的显卡,以及整合了Shiki
和CoreDisplayFixup
的驱动,现在是All In One了;核显驱动更多的教程请参考:Hackintool(原Intel FB-Patcher)使用教程及插入姿势 / 黑苹果必备:Intel核显platform ID整理及smbios速查表 / Coffee Lake帧缓冲区补丁及UHD630 Coffee Lake ig-platform-id数据整理 教程:利用Hackintool打开第8代核显HDMI输出的正确姿势config-Other
目录下,请自行复制相应机型配置文件;Options
-configs
进行选择;不会操作的请移步:Clover使用教程 macOS安装教程 Mojave硬件支持列表1 | # md5 macOS\ Catalina\ 10.15.2\(19C57\)\ Installer\ for\ Clover\ 5100\ and\ WEPE.dmg# 空格以\ 代替 |
kexts/Other/其它驱动
黑果小兵
的大力支持,由于人员众多,恕不一一列名致谢!cmd
窗口,输入命令:1 | c:\>diskpart |
diskgenius
挂载EFI分区进行操作宪武
提供的hotpatch的全套方法:19C57
双EFI
分区版下载链接@难忘情怀
提供下载资源@难忘情怀
提供下载资源rk3d
1 | MD5 (macOS Catalina 10.15.2(19C57) Installer for Clover 5100 and WEPE.dmg) = 1b517732c6ab2155f3673aaeb33cd45a |
Quality of Life Tips and Tricks - Burp Suite
Bookmarks
Docker For Pentesting And Bug Bounty Hunting & Bug Bounty Toolkit
Trying to use Masscan through a VPN client? Use -e to specify the interface. Similarly, Nessus won’t scan over a VPN interface unless you set the source_ip setting in the advanced options to your VPN interface’s IP.
Learning How to Learn: Powerful mental tools to help you master tough subjects & @knoxxs’s notes
1 | yiran@t480:~/go/src/github.com/vmware/cluster-api-upgrade-tool |
1 | upgrader, err := upgrade.NewControlPlaneUpgrader(newLogger(), finalConfig) |
1 | // Upgrade does the upgrading of the control plane. |
u.listMachines
获取 TargetCluster 所有的 ControlPlane 节点u.reconcileKubeadmConfigMapAPIEndpoints
这里主要是确保对应的 APIEndpoints 节点都在 k8s 集群中,通过对比 nodeList 与 APIEndpoints 来进行过滤u.updateKubeletConfigMapIfNeeded
在 k8s 中,在 ConfigMap 中是有保存 kubelet 配置信息的,在升级过程中,需要重新创建对应版本的 kubelet 配置信息,这个函数中直接将原版本的 kubelet 复制了一份,创建一份目标版本的 kublet 配置 ConfigMapu.updateKubeletRbacIfNeeded
创建目标版本的 Role 和 RoleBinding 资源u.etcdClusterHealthCheck
检查 etcd 集群是否健康,这里主要通过 endpoint health --endpoints
来检查 etcd 是否健康u.UpdateProviderIDsToNodes
通过 Cluster-API 创建出来的节点,需要设置 node.ProviderID 才可以被 Cluster-API 识别为 running 状态,ProviderId 格式为:vmware://xxxxx
,这里根据 ProviderID 来检测出具体的 ID,并将其作为一个 map 返回u.updateKubeadmKubernetesVersion
将 kubeadm ConfigMap 中的 kubernetesVersion
字段更新为目标版本,便于后续添加节点时指定的是目标版本u.updateMachines
在所有配置文件已经准备、更新完成后,开始做整个升级中最重要的部分,节点(Machine)升级,首先遍历所有 Machine 资源,针对每个 Machine,进行如下动作:u.etcdClusterHealthCheck
检查 etcd 集群是否健康,如果不健康,则退出升级generateReplacementMachineName
生成替代 Machine 相应配置信息,如 MachineNameu.updateInfrastructureReference
创建替代 Machine 的 Infrastructure Objectu.updateBootstrapConfig
创建替代 Machine 的 Bootstrap Config,因为默认 Cluster-API 使用的 kubeadm Bootstrap,所以这里其实是生成替代 Machine 执行的 kubeadm 配置u.updateMachine
真正创建替代 Machine 的步骤,创建对应的目标虚拟机,等待目标虚拟机添加到 K8s 集群中且处于 Ready 后,将原虚拟机对应 etcd 节点从 etcd 集群中移除,随后将原虚拟机对应节点从 K8s 集群中移除 u.reconcileKubeadmConfigMapAPIEndpoints
等待所有 Machine 替换完成后,重新配置 kubeadm ConfigMap 保证 kubeadm ConfigMap 中保存所有的 APIEndpoints 信息。reconcileKubeadmConfigMapAPIEndpoints
这种长度的变量名,还是很崩溃的。
A tab for two at Q can easily top three figures—several times the outlay on an average Chinese meal. Nor is Mr Chang’s the only such restaurant in the area: like many big American cities, Washington has seen a rise in high-end Chinese cuisine. That is good news, and not just for well-heeled gourmands who can tell shuijiao from shuizhu. The culinary trend is underpinned by two benign social ones. Chinese-Americans are becoming wealthier and more self-confident; and customers are shedding old stereotypes about Chinese food. To put it another way: sometimes a dumpling is more than just a dumpling.
The projection on the side of the Krasnoyarsk museum is at least as much about geography as politics: a tribute to Siberia’s limitless expanse, its high skies and rivers that flow so fast and so deep that their water will steam rather than freeze. It is a historical statement, too—Siberia has been seen for centuries, by visitors and inhabitants alike, as a place of freedom. But by the same measure it is also an ironic one: Siberia was a place of punishment and exile long before the Soviet Gulag.
Inside the museum you will find a lot more irony. An artistic movement called “Siberian ironic conceptualism” is well represented. “Irony and self-irony is a mode of survival in Siberia,” says Vyacheslav Mizin, an artist from Novosibirsk. He and his partner, who style themselves “The Blue Noses”, produce pieces which populate the Siberian landscape with American rock stars, poking fun at state propaganda and liberal fetishes alike. If you are incapable of irony, Mr Mizin says, “you turn beastly. The harder the conditions, the more you need it.” Whether the conditions are climatic, political or spiritual goes unsaid.
This Siberian school is less intellectual than the conceptual art you find in Moscow. It is more coarsely grotesque and openly mocking. It embodies a Siberian belief held far beyond the world of art galleries: that Siberia is both the essence of Russia and separate from it.
The movement’s most famous piece is called “United States of Siberia”. In the early 2010s Damir Muratov, an artist from Siberia’s ancient capital, Tobolsk, some 1,500km west of Krasnoyarsk, took an old wooden door and painted it with green and white horizontal stripes, a field of snowflakes in the top left corner. It was a homage to the American painter Jasper Johns, who in the 1950s first posed the question of whether a painting of a flag was something different from the flag itself—and if so, what, if anything, such somethings symbolised.
Mr Muratov’s painting was similarly not a flag. It did not represent a country—merely suggested one—and it did not fly free in the wind. For Mr Muratov, the wind is the essence of a flag. “The most important thing is the movement of air,” he says. “Where there is a wind, there is a flag.” But because the windless wooden painting still looks like a flag, it is clearly asking to be taken as a symbol: of a non-state, of artistic freedom, of an anarchy free from any authority other than the endless horizons of the Taiga forests and the patterns of falling snow.
On my shoulders there pounces the wolfhound age,
but no wolf by blood am I;
better, like a fur cap, thrust me into the sleeve
of the warmly fur-coated Siberian steppes,
...
Lead me into the night where the Enisey flows,
and the pine reaches up to the star,
because no wolf by blood am I,
and injustice has twisted my mouth.
Evelyn Waugh once complained that the Tories had never succeeded in turning the clock back for a single minute. But this is exactly why they have been so successful. The party has demonstrated a genius for anticipating what Harold Macmillan once called “the winds of change”, and harnessing those winds to its own purposes.
In the 1840s Robert Peel recognised the rise of industrial capitalism and championed the repeal of the Corn Laws, which had kept the price of grain unreasonably high. This split the party but allowed it to incorporate the new “men of business” in the longer term. In the second half of the 19th century, Benjamin Disraeli and Lord Salisbury recognised not only that democracy was the coming thing but also that, thanks to the conservative instincts of the middle and working classes, it could be used to extend rather than undermine the party’s power. In the 1970s Margaret Thatcher reached the future first in recognising that the post-war consensus was about to give way to a new world of free markets, privatisation and what Peregrine Worsthorne, an old-school Tory, called “get your snouts in the trough with the rest of us” Conservatism.
The Tories have three other great weapons in their arsenal. The first is highlighted in the title of one of the best books on the party, John Ramsden’s “An Appetite for Power”. The Conservatives have always been quick to dump people or principles when they become obstacles to the successful pursuit of power. Theresa May immediately sacked her two chief advisers, Fiona Hill and Nick Timothy, after the party’s poor performance in 2017, whereas Jeremy Corbyn is still clinging on to Karie Murphy and Seumas Milne after Labour’s devastating failure last week.
The second is patriotism. The Tories have always played this card better than any other party, whether in the form of imperialism in the 1870s or retaking the Falkland islands in the 1980s. They have been much aided in this by those radical intellectuals who admire any institution or cause so long as it is not British.
No one should underestimate the party’s third weapon: jollity. The Conservatives have always been the party of “champagne and women and bridge”, to borrow a phrase from Hilaire Belloc, whereas the Liberals and Labour have been the parties of vegetarianism, book clubs and meetings. Conservatives are never happier than when mocking the left for its earnestness.
Boris Johnson fits perfectly into this great Tory tradition. He was one of the first members of his political generation to spot the rising tide of nationalist populism and recognise that it was about to reshape the global landscape. This earned him the hatred of the metropolitan class into which he was born, which is convinced that the future lies with multilateral institutions and globalisation. But it put him at the front of Britain’s Eurosceptic movement, which could have degenerated into a narrow faction under Sir William Cash or a noisy fringe under Nigel Farage, but which entered the Tory mainstream because of Mr Johnson.
He succeeded in this where Mrs May failed because he possessed the other great Tory weapons. He has been willing to sacrifice anything in the pursuit of office. Beneath the bumbling exterior lies a ruthless, power-seeking machine. His withdrawal of the whip from 21 colleagues (some of them close friends) in September made Macmillan’s “night of the long knives” in 1962 look tame. Mr Johnson has never missed an opportunity to wave the flag—even when it has made him look absurd, as when he got stuck on a zip-wire clutching two little Union Jacks. Predictably, the left has played into his hands. Some Remainers have gone out of their way to give the benefit of every doubt to the EU, and Mr Corbyn has devoted his life to supporting anti-Western causes.
Above all, Mr Johnson has embraced the women-and-champagne side of Toryism, if not the bridge. He made his career as a Eurosceptic not by agonising about sovereignty but by making fun of the EU’s (imagined) imperial ambitions to regulate the shape of bananas or the size of condoms. He cracked jokes that were calculated to rile the guardians of political correctness as much as to delight the masses (post-mortems on the election have underestimated the role of these guardians in turning working-class voters against Labour).
The hunt is on to discover the meaning of Johnsonism. How will he flesh out the sketchy promises in his manifesto? What can he do for working-class voters in Blyth Valley? How will he reconcile the free-marketeer and big-government factions of his party? The best way to answer these questions is not just to engage in the British version of Kremlinogy by interrogating every ministerial leak. It is also to study the long history of a party that Mr Johnson now leads with such a resounding mandate. ■
That was not always the case. Bougainville once boasted the third-largest copper mine in the world. It delivered close to half of PNG’s export revenues in the 1970s. But arguments about the distribution of revenue and jobs from the Panguna mine sparked an insurgency in the late 1980s, which forced the mine to close. PNG’s armed forces struggled to establish control over the island’s mountainous terrain and hostile population. They withdrew in 1990, and blockaded the island by sea instead. When PNG hired mercenaries from a firm called Sandline International to restore order, its own soldiers mutinied, prompting the government of the day to fall and Australia and New Zealand to step in to broker a peace deal.
The agreement, signed in 2001, promised a referendum on independence by 2020 and self-government in the meantime. But the mine did not re-open, leaving the autonomous administration starved of cash. Other big mines and oil- and gasfields were developed on the mainland, diminishing the central government’s incentive to make autonomy work. National leaders’ main concern these days is that Bougainville might inspire other secessionist rebellions, given PNG’s diversity (its 8.5m citizens speak 839 languages), poverty, isolating terrain and dire infrastructure.
The leader of the autonomous government on Bougainville, John Momis, once supported greater autonomy within PNG—the other option on the ballot in the referendum. But the stinginess of PNG’s fiscal transfers and its broader neglect of Bougainville drove him and other voters towards independence instead. Few islanders have confidence in Mr Marape’s promise to fix these problems, having heard such pledges before.
In fact, there is a risk of lack of leadership on both sides. Mr Momis is 81 and must step down by June because of term limits. He has no obvious successor. Bougainville’s people, having voted so emphatically for independence, presumably expect speedy change. The politicians seem unlikely to gratify their desires. The chances of further discord are high. ■