Skip to content

PipyJS 编码规范

pajama-coder edited this page Jul 17, 2023 · 5 revisions

PipyJS 编码规范

代码格式化

空格

适当利用空格来增加代码可读性。

  • 在运算符前后增添空格,例如:y = 1 + 2 * x
  • 逗号、冒号与之前内容紧靠,与其后内容之间以空格隔开,例如:protocol: 'udp', timeout: 60,
  • 如果花括号与其所包含的内容在同一行上,那么花括号与其内容之间应以空格隔开。例如:{ name: 'login', type: 'service' }
  • 方括号和圆括号与其所包含的内容之间无需使用空格,例如:headers['content-type'] ['a', 'b', 'c'] onStart(new Data)
  • 代码行内部一般不应出现连续多个空格,除非以增强可读性为目的而在行与行之间对齐某些内容时
  • 代码行结尾不应留有多余空格
  • 空行不应包含任何空格字符

空行

利用空行来显式分割代码块,类似文章段落,把意义相关联的代码行组合在一起。

特别的,针对一个包含全局变量定义、上下文变量定义、管道定义等多项定义的 Pipy 模块,每一项定义应该与其他定义之间以空行隔开。例如:

pipy()

.listen(8080)
.serveHTTP(new Message('hi!'))

.listen(8000)
.demuxHTTP().to($=>$
  .muxHTTP().to($=>$
    .connect('localhost:8080')
  )
)

缩进

代码应保持严谨的缩进,以体现代码块之间的嵌套关系。缩进以 “两个空格” 为单位,禁止使用 Tab 进行缩进。

行尾分隔符

保留参数列表以及对象属性列表中最后一项的行尾分隔符,例如:

foo(
  'hello',
  {
    name: 'user',
    type: 'service', // 保留逗号
  },
  (err) => console.log(err),
  true, // 保留逗号
)

引号

在可能的情况下,尽量选择使用单引号而避免使用双引号。

在对象属性列表里,如果属性名称为合法标识符,尽量避免使用引号,除非以可读性为目的而故意与该对象其他属性保持统一,例如:

{
  type: 'prefix',
  path: '/api/v1/user',
  host: 'example.com',
}

{
  'Host': 'example.com', // 本可以不用引号,但为了与下面一行保持统一
  'Accept-Encoding': 'gzip',
}

访问对象属性时,如果属性名称为合法标识符,则避免使用方括号,优先考虑使用圆点符号(.),例如:

config['services'] // Bad
config.service // Good

局部变量

PipyJS 不支持语句写法,所以不能像标准 JavaScript 那样通过花括号来定义局部范围,也不能通过 var 或者 let 来声明局部变量,但可以通过原地调用的匿名函数来定义局部范围,此时,该函数的参数就是该局部范围内的局部变量。比如:

pipy()

.pipeline()
.handleMessage(
  (msg) => (
    (
      foo = msg.head,
      bar = foo.headers,
    ) => (
      console.log(bar.host)
    )
  )()
)

采用这种写法时,每一个局部变量应独自占用一行,局部变量列表和函数体之间以一行 ) => ( 隔开。最后,将整个匿名函数嵌入在一个函数调用操作内,即 (...)()

pipy.solve()

通过 pipy.solve() 引入的文件严格来说并不是一个 Pipy 模块,因为 Pipy 模块特指那些求解后得到一个 Configuration 对象的表达式,只有 Configuration 对象才能提供描述一个模块所需要的定义(上下文变量定义和管道定义等),而 pipy.solve() 可以引入求解出任何类型结果的表达式,因此无法满足模块定义需求。提供 pipy.solve() 的目的是为了能把多个模块共享的数据和函数统一写在一个文件里,并且这个文件仅在被第一个模块 solve 时求解一次。

被 solve 的文件可以返回任意类型的值,所以,在仅需返回一个对象或者函数的场合,没有必要把它再包装在一个对象中返回。例如:

// Bad
({ config: JSON.decode(pipy.load('config.json')) })

// Good
JSON.decode(pipy.load('config.json'))

在需要返回多个对象或者函数的场合,有两个选择:

  1. 如果多个对象和函数之间不存在显著关联,那么可以分成多个 js 文件来分别 solve
  2. 如果多个对象和函数之间存在必要的关联,那么组合成一个对象返回

对于第二条,如果整个文件只有一个包含在一对花括号内的对象,那么整个对象应该再次用圆括号包围,以兼容各种标准 JavaScript 编辑器。

匿名子管道

匿名子管道通过调用 to() 来定义,此时,匿名子管道开头的函数参数定义 $=>$ 应放置于 to( 之后,跟 to() 的调用位于同一行上,然后,子管道上的 filter 定义应该另起一行并且缩进。例如:

pipy()

.listen(8000)
.demuxHTTP().to($=>$
  .muxHTTP().to($=>$
    .connect('localhost:8080')
  )
)

代码框架

一个 Pipy 模块的主要组成部分包括:

  • 全局变量声明和初始化(即最外层函数的参数)
  • 仅本模块可见的上下文变量声明和初始化(即调用 pipy() 函数时作为函数入参而给定的上下文对象原型)
  • 在本模块声明和初始化,并且对其他模块也可见的导出上下文变量(通过调用 export() 来定义)
  • 在其他模块声明和初始化,并且导入到本模块的上下文变量(通过 import() 来定义)
  • 管道定义(通过调用 listen(), task(), pipeline() 等来定义)

包含以上组成部分的代码框架如下:

((
  // Global variables
  foo = 'foo init value',
  bar = 'bar init value',

) => pipy({
  // Context variables
  _foo: '_foo init value',
  _bar: '_bar init value',
})

// Exported context variables
.export('my-namespace', {
  __exportedFoo: '__exportedFoo init value',
  __exportedBar: '__exportedBar init value',
})

// Imported context variables
.import({
  __importedFoo: 'foo-namespace',
  __importedBar: 'bar-namespace',
})

// Module default pipeline
.pipeline()
.dump()
.dummy()

// Other (named) pipelines
.pipeline('foo')
.dump()
.dummy()

)()

命名惯例

变量(以及函数)名称应采用 “小写字母开头、后续各单词首字母大写” 的形式,名称里不应包含下划线(_)和美元符号($)。名称里尽量不包含数字,除非数字具有明确意义,例如:用于 “HTTP/2” 里的数字 “2” 等。

Pipy 在标准 JavaScript 基础上引入了 “上下文变量” 的概念,其特点是:可见范围类似全局变量,但其值在不同 “上下文” 里不一样。这有点像 “线程局部存储” 里的变量:对于一个特定线程,它表现为全局变量,但每个线程看到的只是自己的实例,跟其他线程之间是完全隔离的。为了显著区分上下文变量,我们要求用下划线作为上下文变量名的前缀。同时,禁止普通变量采用以下划线起始的名称。

更进一步,Pipy 里的上下文变量可以通过 export/import 在多个模块之间共享,以此实现在不同模块之间传递信息。我们要求对这些 “被导出” 的上下文变量采用双下划线开头的名称。

最佳实践

严格相等

JavaScript 有两个用于判断值是否相等的运算符,相等(==)和严格相等(===)。当判断两个不同类型的值是否相等时,前者会尝试进行类型转换再进行比较,而后者则直接给出 false (即不相等)。例如:

123 == '123' // true
123 === '123' // false

因为严格相等不进行任何类型转换,所以效率比一般的相等判断高一些。再明确判断类型的情况下,建议优先使用严格相等。

void 运算符

在 PipyJS 里,一切代码都是表达式,而表达式都有计算结果。如果把 PipyJS 代码里的每个表达式都看作过程式编程里的一条语句,那么意味着 PipyJS 里的每条语句都有返回值。当一个函数包含多条语句时,最后一条语句的返回值就是函数的返回值。此时,如果不希望该函数有任何返回值,应该使用 void 运算符来把整个函数包围起来,使其结果变成 undefined。比如:

doSomething = () => void (
  doOneThing(),
  doAnotherThing()
)

字符串操作

在 PipyJS 里,字符串操作是一种成本相对较高的操作,原因在于,一个字符串的创建涉及到大量 O(n) 复杂度的操作:

  1. 计算字符串哈希值
  2. 进行 UTF-8 解码
  3. 内存分配

考虑如下例子:

console.log('Time elapsed: ' + time + ' seconds')

为了求解要输出的字符串,PipyJS 至少要进行两次字符串创建工作:

  1. 创建一个字符串,其结果为 “Time elapsed: ” 和 time 拼接后的结果
  2. 再创建一个字符串,其结果为以上字符串与 “seconds” 拼接的结果

也就是说,每增加一个加号(+),就要多创建一个字符串,而这些多创建的字符串都仅仅只是中间结果,创建以后很快就扔掉了,由此产生了大量的无用计算。

对于上述例子,由于 console.log 支持可变参数数量,所以完全没有必要进行昂贵的字符串拼接,而仅仅逐个打印三个组成部分即可:

console.log('Time elapsed:', time, 'seconds')

如果确实需要一个拼接后的字符串结果,那么可以利用 Array 的 join() 函数来避免产生大量的中间结果:

['Time elapsed:', time, 'seconds'].join(' ')

或者使用 “模版字符串”:

`Time elapsed: ${time} seconds`

当然,如果仅仅将两个字符串拼接起来,那么即使简单的加号操作符也不会创建任何中间结果。上述优化仅针对将两个以上字符串链接到一起的情况下有效。

此外,PipyJS 在标准 JS 基础上增加了 JSON.decode() 和 JSON.encode() 方法,这也是出于避开昂贵的字符串操作的目的。跟标准的 JSON.parse() 和 JSON.stringify() 不同的是,JSON.decode() 和 JSON.encode() 的编解码目标是 JSON 文本的 UTF-8 二进制形式(即保存有原始字节数据的 Data 对象)。在应用场景允许的情况下,应尽量使用这两个替代方法以提高执行效率。

需要注意的一点是,字符串常量的创建只在代码加载时发生一次,因此不必过多担心其效率问题。并且,字符串的 “严格相等” 判断(见严格相等)非常便宜,跟比较两个整数是否相等的计算量一致。这也是为什么在 JavaScript 中经常利用字符串常量来代替 C 里的枚举类型的原因。

一个总的原则是,当进行任何除严格相等判断以外的字符串操作时,多考虑一下是否存在其他效率更高的替代方案。

可选链操作符

在脚本逻辑中我们经常要进行判空操作,并根据上一项计算结果是否为空,来选择是否要继续进行下一项计算。此时,利用可选链操作符(?.)既可以简化代码也能提高执行效率。例如:

msg.head.headers['x-username'] ? msg.head.headers['x-username'].toLowerCase() : '' // Bad
msg.head.headers['x-username']?.toLowerCase() || '' // Good

需要注意的一点是,在一连串前后依赖的计算中,如果某一步计算用到了可选链,那么之后的计算步骤中通常也都需要使用可选链,因为之后的计算步骤都有机会对一个空值进行计算,从而产生运行时错误。

数组遍历

PipyJS 不支持 for 循环语句,对数组进行遍历操作时必须依赖其 map()/reduce()/forEach() 方法。这三个方法的区别是:

  • map() 对数组里的每个元素执行一次用户指定的计算,将计算结果收集起来放到一个新数组里,新数组的长度等于原数组长度
  • reduce() 对数组里的每个元素执行一次用户指定的计算,将前一个元素的计算结果作为对下一个元素计算结果的输入,最终返回最后一个元素计算得到的结果
  • forEach() 对数组里的每个元素执行一次用户指定的计算,最终不返回任何内容

除了上述三个方法外,数组还提供了其他不同的遍历方法,比如 flatMap(), reduceRight(), filter(), every(), find() 等等。由于数组遍历具有 O(n) 复杂度,因此不建议在处理单个网络请求或者网络事件的过程中使用。在必须使用的情况下,应注意把对于每次遍历完全相同的计算提取到循环之前,避免在每次遍历中产生额外的浪费。

如果要进行对象属性的遍历,需首先将对象转换成数组。只对 key 进行遍历的话,使用 keys() 方法获取一个包含所有 key 的数组。只对 value 进行遍历的话,使用 values() 方法获取一个包含所有 value 的数组。对 key/value 同时进行遍历的话,使用 entries() 方法获取包含所有 key/value 对的数组。注意,使用 entries() 方法时,既然已经得到了 value,那么也就不必在对每个元素的计算中再次根据 key 来查找属性值了。例如:

obj.entries().forEach(([k, v]) => doSomething(k, obj[k])) // Bad
obj.entries().forEach(([k, v]) => doSomething(k, v)) // Good
Clone this wiki locally