Markdown解析器-Showdown

由于工作中需要将 markdown 格式的文件转换成 html,因此抽空研究了下业界的工具Showdown,Showdown 是一个可以将标准 markdown 规范解析生成基础 html 的工具,使用起来很简单,通过命令:showdown makehtml -i <input> -o <output>即可完成转化,详细的介绍请参考官方文档 Showdown,在此就不过多赘述了。

Extensions 与 Listeners

通过研究发现,当需要解析一些”自定义”的 markdown 规范时,Showdown 提供了清晰良好的代码格式,方便我们扩展,包括extensions, listener的方式,可以进行注册,监听,来看 Showdown 的入口文件,里面有这样三行代码:

1
2
3
4
5
6
7
8
9
10
//src/cli/makehtml.cmd.js
function run() {

var converter = new showdown.Converter(argv);
...
input = fs.readFileSync(argv.i, enc);
...
output = converter.makeHtml(input);

}

可以分析出,converter通过接受参数,来配置各种extensionslistener,然后直接从源文件中读取,经过 makeHtml 一转换就变成 html 了

官方也提供了几个现有的 extensions, 如 table-extension prettify-extension 等,这些现有的extensions都是对 markdown 的元素进行了自定义的解析和渲染,如果是基于标准的 markdown 语法,使用这种方式进行自定义渲染就非常方便了

关于 Listeners 可以参考下面的工作原理章节

工作原理

关于工作原理,可以用一句话来概括:Showdown使用正则表达式解析 markdown 语法,然后直接渲染成 html,注意这里面有两个关键词:

  • 正则表达式解析

    来看一个 Parser 的源码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 斜体和黑体的解析器:italicsAndBold
showdown.subParser('italicsAndBold', function (text, options, globals) {
'use strict';

text = globals.converter._dispatch('italicsAndBold.before', text, options, globals);

if (options.literalMidWordUnderscores) {
//underscores
// Since we are consuming a \s character, we need to add it
text = text.replace(/(^|\s|>|\b)__(?=\S)([\s\S]+?)__(?=\b|<|\s|$)/gm, '$1<strong>$2</strong>');
text = text.replace(/(^|\s|>|\b)_(?=\S)([\s\S]+?)_(?=\b|<|\s|$)/gm, '$1<em>$2</em>');
//asterisks
text = text.replace(/(\*\*)(?=\S)([^\r]*?\S[*]*)\1/g, '<strong>$2</strong>');
text = text.replace(/(\*)(?=\S)([^\r]*?\S)\1/g, '<em>$2</em>');

} else {
// <strong> must go first:
text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, '<strong>$2</strong>');
text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g, '<em>$2</em>');
}

text = globals.converter._dispatch('italicsAndBold.after', text, options, globals);
return text;
});

每个 Parser 的开始和结束,都抛出了相应的事件供监听,这就是 listener 扩展的机制,每次处理的结果都是返回 text 文本,可以看到在解析器中,对输入的文本进行正则表达式的匹配并直接替换成了相应的 html 元素

  • 直接渲染

    所谓直接渲染,就是上面代码中的replace,这样的处理为后续的需求埋下隐患:

    1. 如果某个 markdown 片段需要作为整体的渲染,则无法满足

    2. 如果对渲染之后的 html 进行样式更改,加入css,则无法满足

      可以看出,replace 替换的仅仅是 html 基本元素样式,且没有 id,class 等信息,因此极大程度上约束了生成的 html 样式,并不利于满足一些实际需求,然而 Showdown 的代码架构,将各个 markdown 元素分别定义了单独的解析器,这就为我们提供了一个思路,可以根据自定义的 markdown 的格式通过自定义的 parser 解析出来就好了,例如:

      1
      2
      3
      4
      5
      6
      ### start 修订记录 ###
      1.0
      date: 11.3
      2.0
      date: 12.9
      ### end 修订记录 ###

解析器可以以 start 和 end 作为判断,整体将修订记录渲染成想要的格式,因此,对 Showdown 工程有以下重构想法:

解析和渲染解耦,不通过 replace 直接完成, 而是通过 taffydb 将解析出来的数据进行存储(参考jsdoc),这样做的好处是:规则制定者可以自定义 markdown 语法片段并通过解析器解析出来,然后存放到数据库中,不需要关心接下来如何渲染,前端工程师可以直接从数据库中取出相应的片段信息,配合css,进行定制化渲染。借助于中间数据库的解耦,使得最后生成出来的html可以加入css样式,从而满足一切定制化需求,即 解析 - 数据 -渲染

总结

以上是在结合工作需求的基础上研究 Showdown 工具的一些想法,还是软件工程里的一句老话:从需求出发的改动才附有意义

如需转载,请注明出处

前端资源打包工具-WebPack

WebPack 是一个前端资源打包工具,借助于Loader它可以将任何前端资源(js/image/css/…)统一转换为原生JS 代码,通过静态分析构建模块之间的依赖关系,将涉及到资源分类打包成不同的模块,这些模块在浏览器,Nodejs 中可以按需进行加载,可提升加载效率。同时,WebPack 的打包也非常高效,Webpack 使用异步 I/O 和多级缓存提高运行效率,这使得 Webpack 能够以令人难以置信的速度快速增量编译。

1
2
官方文档:http://webpack.github.io/docs/
中文文档:http://webpackdoc.com/index.html

以上两个文档已经非常全面的阐述了 WebPack 强大的功能,下面针对几个关键点进行实战

解释一切资源:Loaders

由于 WebPack 打包的来源只能是原生 JS 代码,因此许多静态资源(图片/CSS), 非JS写的代码(Coffee 和 JSX)需要以一种转换方式转换成 JS 代码,为此 Loader 应运而生

Loader 作为一个单独的模块,其具有以下几个特征:

  1. 链式处理方式,资源可被多个 Loader 相互转化,但最终的结果是 JS

  2. 支持同步或异步的处理方式

  3. Function 级别存在,可运行于NodeJS甚至其他的环境中

  4. 支持正则表达式等特性,可通过npm来发布

总而言之,Loader 作为一个JS代码的转义器,有任何需要被 WebPack 打包的非原生 JS 资源,都可以通过内置/自定义的 Loader 加以转换,供打包使用

使用方式:npm install xxx-loader --save-dev

Loader列表参考List of Loaders

丰富的插件:Plugins

在打包过程中,WebPack 还允许借助各种插件,对打包过程、生成的结果进行处理,比如最简单的借助于 UglifyJS 实现压缩和混淆,使用如下命令:

new webpack.optimize.UglifyJsPlugin([options])

即可生成压缩混淆之后的打包文件,同理,内置的插件可以做更多的事情

使用方式:在配置文件中配置

Plugin列表参考List of Plugin

配置文件:webpack.config.js

在大型项目中,WebPack 通常借助于配置文件 webpack.config.js 将打包过程所需执行的步骤,插件统一进行管理,同时方便发布系统实时监控打包,配置文件的内容通常如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//WebPack配置文件
var webpack = require('webpack');

module.exports = {
entry: './entry.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
loaders: [{
test: /\.css$/,
loader: 'style!css'
}]
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
mangle: false,
compress: false
}),
new webpack.BannerPlugin('This file is created by ChenS')
]
}

可以看出,entry中配置要打包的文件,output打包之后的文件输出路径,module里配置各种 Loader,以 test 的正则表达式匹配资源文件格式,Loader 传入名称,在plugins里配置各种插件以及选项,比如上述 UglifyJsPlugin 禁用了混淆和压缩,有了这个配置文件,在使用 WebPack时就可以直接运行webpack命令,一切轻松搞定

以上Demo工程可点此下载:node-webpack-demo

总结

WebPack 的基础实战在上面分析完了,更多完整功能用法,请参考WebPack

如需转载,请注明出处

读书笔记-Effective JavaScript

让自己习惯 JavaScript

  • 了解你使用的JavaScript版本

    • Web浏览器,并不支持让程序员指定JS的版本来执行代码

    • 严格模式(”use strict”),允许你选择在受限制的JS版本中禁用一些问题较多或者出错的特性

    • 不要试图将严格模式和非严格模式的文件连接在一起,建议在严格模式下编写代码(ES5 中增加的,兼容ES3)

    • 将其自身包裹在立即调用的函数(IIFE)表达式中的方式连接多个文件,是比较安全的

  • 理解JavaScript的浮点数

    • 大多数编程语言都有几种数值型数据类型,但JS只有一种,就是 number,也就是常理解的 double(双精度浮点数)

    • JS中所有 位运算符 的工作方式是:将操作数转换为整数,然后使用整数位模式进行运算,最后将结果转换为标准的JS浮点数(3步),即将数字视为32位的有符号整数

    • 浮点数的操作是出了名的不准确

      0.1 + 0.2 // 0.300000000000004
      0.1 + 0.2 + 0.3 ≠ 0.1 +(0.2 + 0.3)
      
    • 当心浮点运算中的精度陷阱,一般都是转化为最小单位(尽可能转换成整数)去计算,因为整数在表示时不需要舍入

  • 当心隐式的强制转换

    • 类型错误可能被隐式的强制转换所隐藏

    • 重载运算符 + 是进行加法运算还是字符串连接操作取决于其参数类型

    • 对象通过valueOf方法强制转换为数字,通过toString方法强制转换为字符串

    • 具有valueOf方法的对象应该实现toString方法,返回一个valueOf方法产生的数字字符串表示

    • JS中有且只有7种假值:false、0、-0、””、NaN、null 和 undefined

    • 测试一个值是否为未定义的值,应该使用 typeOf或者与undefined进行比较而不是真值运算

  • 原始类型优于封装对象

    • 5个原始类型:布尔值、数字、字符串、null 和 undefined

    • 隐式封装:当对原始值提取属性和进行方法调用时,他表现得就像已经使用了对应的对象类型封装了该值一样。例如:String的原型对象有一个toUpperCase的方法,你可以对这个原始字符串值调用这个方法

    • 当做相等比较时,原始类型的封装对象 ≠ 原始类型值

    • 获取和设置原始类型值的属性会隐式地创建封装对象,字符串对象的引用在用完之后立即被销毁,所以不能给字符串添加属性

      "hello".property = 1;
      "hello".property; // undefined
      
  • 避免对混合类型使用 == 运算符

    • 当参数类型不同时,==运算符应用了一套难以理解的隐式强制转换规则

    • 当使用 === 运算符时,不需要涉及任何的隐式强制转换就明白你的比较运算符

    • 当比较不同类型的值时,使用你自己的显示强制转换使程序的行为更清晰

      null == undefined // true
      string/number/boolean == Date // Date.toString() --> Date.valueOf()
      string/number/boolean == 非 Date // 非 Date.valueOf() --> 非 Date.toString()
      
  • 了解分号插入的局限:分号插入原则

    • 仅在 } 标记之前、一个或多个换行之后和程序输入的结尾

    • 仅在紧接着的标记不能被解析的时候推导分号

    • 在以(、[、+、- 或 / 字符开头的语句钱决不能省略分号

    • 当脚本连接的时候,在脚本之间显式插入分号

    • 在 return、throw、break、contine、++ 或 – 的参数之前决不能换行

    • 分号不能作为for循环的头部或空语句的分隔符

  • 视字符串为16位的代码单元序列

    • JS字符串有16位的代码单元组成,而不是由Unicode代码点组成

变量作用域

  • 尽量少用全局对象

    • 多使用局部变量

    • 避免对全局对象添加属性

    • 使用全局对象来做平台特性检测

  • 始终声明局部变量

    • 使用 var(函数作用域) 和 let(for循环局部作用域) 来声明新的局部变量

    • 考虑使用lint工具帮助检查未绑定的变量

  • 不要使用with

  • 熟练掌握闭包

    • JavaScript允许你引用在当前函数以外定义的变量(内部函数可以访问外部成员变量,引了,该函数就成为闭包)

    • 闭包比创建它们的函数有更长的生命周期

    • 闭包在内部存储其外部变量的而引用,并能读写这些变量

      function sandwichMaker(){
          var magicIngredient = "peanut butter";
          function make(filling){
              return magicIngredient + " and " + filling
          }
          return make;
      }
      var f = sandwichMaker();
      f('jelly') // peanut butter and jelly
      f('bananas') // peanut butter and bananas
      
      //那些在其所涵盖的作用域内跟踪变量的函数,称为闭包,make函数就是一个闭包
      
  • 理解变量生命提升

    • JavaScript隐式地提升(hoists)声明部分到封闭函数的顶部,而将赋值留在原地,换句话说,变量的作用域是整个函数,但仅在var语句出现的位置进行赋值

    • 重声明变量被视为单个变量

    • 请手动提升局部变量的声明,从而避免混淆

  • 使用立即调用的函数表达式(IIFE)创建局部作用域

    • 闭包存储的是其外部变量的引用,而不是值

      function wrapElements(a){
          var result = [], i, n;
          for(i = 0, n = a.length ; i < n ; i++){
              result[i] = function(){ return a[i]; };
          }
          return result;
      }
      
      由于function中引用了变量i,所以该函数为每个result[i]都创建了一个闭包,且保存着对i的引用
      所以i = a.length;即result[1...a.length-1]都等于a[a.length],即 undefined
      
    • 立即调用的函数表达式

      function wrapElements(a){
          var result = [];
          for(var i = 0, n = a.length ; i < n ; i++){
              (function(j){
                  result[i] = function(){ return a[i]; };
              })(i);
          }
          return result;
      }
      
  • 当心命名函数表达式笨拙的作用域

    • var f = function(){}
  • 当心局部块函数声明笨拙的作用域

    • 始终将函数声明至于程序或被包含的函数的最外层,以避免不可移植的行为

    • 局部块函数声明: 使用var声明和有条件的赋值语句代替有条件的函数声明

  • 避免使用eval创建局部变量

    • 不要赋予外部调用者能改变函数内部作用域的能力,即 函数内部过分依赖于 动态绑定(eval(“var x = 1”))

    • 如果 eval 函数代码可能创建全局变量,将此调用封装到嵌套的函数中以防止作用域污染

      var y = "global";
      function test(src){
          (function(){ eval(src); })();
          return y;
      }
      
      test("var y = 'local';") // global
      test("var z = 'local';") // global
      
  • 间接调用eval函数优于直接调用(Node里面没有臭名昭著的eval)

    • 尽可能间接调用eval函数,而不要直接调用eval函数:(0,eval)(src)

使用函数

  • 理解函数调用、方法调用及构造函数调用之间的不同

    • 在方法调用中是由调用表达式自身来确定this变量的绑定。绑定到this变量的对象被称为调用接收者(receiver)

    • 方法调用将被查找方法属性的对象作为调用接收者

    • 函数调用将全局对象作为其接受者,一般很少使用函数调用语法来调用方法

    • 构造函数需要通过new运算符调用,并产生一个新的对象作为其接收者

  • 熟练掌握高阶函数

    • 高阶函数就是那些将函数作为 参数 或者 返回值 的函数

    • 当发现自己在重复地写一些相同的模式时,学会借助于一个高阶函数(提炼逻辑,让callback作为参数传递)可以使代码更简洁、更高效、更可读,学会发现可以被高阶函数所取代的常见的编码模式

    • 掌握现有库中的高阶函数,比如数组的map

  • 使用call方法自定义接受者来调用方法

    • f.call(obj,arg1,arg2,arg3)与 f(arg1,arg2,arg3)不同的是第一个参数提供了一个显式的接收者对象

    • 使用 call 方法自定义接收者来调用函数

    • 使用 call 方法可以调用在给定的对象中不存在的方法

    • 使用 call 方法定义高阶函数允许使用者给回调函数指定接收者

  • 使用apply方法通过不同数量的参数调用函数

    • 使用 apply 方法制定一个可计算的参数数组来调用可变参数的函数,apply方法需要一个参数数组,然后将数组的每一个元素作为调用的单独参数调用该函数

    • 使用 apply 方法的第一个参数给可变参数的方法提供一个接收者

  • 使用arguments创建可变参数的函数

    • JavaScript给每个函数都隐式得提供了一个名为arguments的局部变量。arguments对象给实参提供了一个类似的数组接口,它为每个实参提供了一个索引属性,还包含一个length属性用来指示参数的个数,从而可以通过遍历arguments对象的每个元素来实现可变元数的函数

    • 如果提供了一个便利的可变参数的函数,也最好提供一个需要显示指定数组的固定元数版本

      function average(){
          return averageOfArray(arguments);
      }
      
  • 永远不要修改 arguments 对象

    • 使用 [].slice.call(arguments) 将 arguments 对象复制到一个真正的数组中再进行修改
  • 使用变量保存 arguments 的引用

    • 先用变量绑定到 arguments,明确作用域之后,再在嵌套函数中引用它
  • 使用 bind 方法提取具有确定接受者的方法

    • 提取一个方法不会将方法的接收者绑定到该方法的对象上

    • 当给高阶函数传递对象方法时,使用匿名函数在适当的接收者上调用该方法

    • 使用 bind 方法创建绑定到适当接收者的函数

  • 使用 bind 方法实现函数柯里化(不懂)

    • 将函数与其参数的一个子集绑定的技术成为函数柯里化

    • function.bind(null, arg1, arg2)

  • 使用闭包而不是字符串来封装代码

    • 当将字符串传递给eval函数以执行时,不要在字符串中包含局部变量的引用,容易在函数中引起冲突

    • 优先接收函数,而不是eval执行的字符串,即

      function repeat(n, action){
          for(var i = 0 ; i < n ; i++){
              action();
          }
      }
      
  • 不要信赖函数的toString方法

    • 在不同的引擎下调用toString方法的结果可能不同,所以绝不要信赖函数源代码的详细细节

    • toString方法的执行结果并不会暴露存储在闭包中的局部变量值

    • 通常情况下,应该避免使用函数对象的toString方法

  • 避免使用非标准的栈检查属性

    • 调用栈实质当前正在执行的活动函数链

    • 不要使用非标准的 arguments.caller 和 arguments.callee 属性

对象和原型

  • 理解 prototype、getPrototypeOf 和 proto 之间的不同(有一个重点图)

    • C.prototype 属性是 new C() 创建的对象的原型

    • Object.getPrototypeOf(obj)是ES5中检索对象原型的标准函数

      Object.getPrototypeOf(u) === User.prototype
      
    • obj.proto 是检索对象原型的非标准方法(不支持ES5的情况下采用)

    • 类:是由一个构造函数 和 一个关联的原型 组成的一种设计模式

      function User(name, passwordHash){
          this.name = name;
          this.passwordHash = passwordHash;
      }
      
      User.prototype.toString = function(){
          return "sdsds";
      }
      
      var instance = new User("abc","abc");
      
      Function.prototype(.call/.bind/.apply) --> User(.prototype) --> User.prototype(.toString) --> instance(.name/.passwordHash)
      
  • 使用 Object.getPrototypeOf 函数而不要使用 proto 属性

    • 获取对象原型的标准API是 Object.getPrototypeOf()

    • 在支持proto属性的非ES5环境中实现 getPrototypeOf

      if(type Object.getPrototypeOf === "undefined"){
          Object.getPrototypeOf = function(obj){
              var t = typeof obj;
              if(!t || ( t !== "onject" && t !== "function")){
                  throw new TypeError("not an object");
              }
              return obj._proto_;
          }
      }
      
  • 始终不要修改 proto 属性

    • 使用Object.create函数给新对象设置自定义的原型
  • 使构造函数与 new 操作符无关

    • 防范误用构造函数可能不值得费心去做,但在跨大型代码库中共享构造函数或者构造函数来自一个共享库的时候,就需要多加防范了

    • 当一个函数期望使用new操作符调用时,清晰地文档化该函数

    • 通过使用 new 操作符或Object.create 方法在构造函数定义中调用自身使得该构造函数与调用语法无关

  • 在原型中存储方法

    • 原型是共享方法的渠道之一,将方法存储于原型中由于存储在示例对象中

    • 将方法存储在示例对象中将创建该函数的多个副本,因为每个实例对象都有一份副本

  • 使用闭包存储私有数据

    • 闭包变量是私有的,只能通过局部的引用获取

    • 将局部变量作为私有数据从而通过方法实现信息隐藏

  • 只将实例状态存储在实例对象中

    • 共享可变数据可能会出现问题,因为原型是被其所有的实例共享的

    • 将可变的实力状态存储在实例对象中

  • 认识到 this 变量的隐式绑定问题

    • this 变量的作用域总是由其最近的封闭函数所确定的

    • 使用一个局部变量(通常命名为 self、me 和 that)使得this绑定对于内部函数是可用的

数组和字典

  • 原型污染

    • 定义:Object 是 js 对象的根,如果在 Object.prototype 上增加任何方法,那么随之使用 for…in 遍历对象属性的时候,此时增加的方法也会计算进去从而造成的原型污染,此外,一些特殊命名的变量名也会对对象造成污染。

    • 原因:for…in 循环除了枚举出对象”自身”的属性外,还会枚举出继承过来的属性

    • 预防:

      • 使用 Object.create(null) 来创建一个没有原型的对象,因此原型污染就无法影响这样的对象行为

      • 使用 hasOwnProperty 方法(只过来对象自身属性)以避免原型污染: var hasOwn= Object.prototype.hasOwnProperty; hasOwn.call(dic,”alice”)

      • 别在 Object.prototype 中增加属性,若不小心增加了属性,也要用 Object.defineProperty方法把属性 enumerable置成false

  • 循环和迭代

    • 使用数组而不要使用字典来存储有序集合

    • for…in 循环会挑选一定的顺序来枚举对象的属性,如果是有序的,还是用for循环遍历

    • 避免在 for…in 期间修改对象,如果需要修改,应该使用while或for循环

    • 数组的循环优先使用for循环而不是for…in循环

    • 使用迭代方法由于循环:forEach,map,every,some

    • 处理数组内容的改变:map,筛选数组的内容:filter

    • 循环只有一点优于迭代函数,那就是控制流操作,如 break 和 continue

  • 创建 数组、对象,直接使用 var x = [],{} 即可,因为使用 Array 和 Object 不能保证是不是被污染了

库和API设计

-【参数】 保持一致的约定

- 约定参数的顺序

    - 宽度第一,然后高度: new Widget(320, 640) // width:320, height:240

    - CSS顺时针约定:top,right,bottom,left

-【参数】 在变量命名和函数签名中使用一致的约定

-【多元化】 不要偏离用户在其他开发平台中很可能遇到的约定

-【检查】真值测试是实现参数默认值的一种简明的方式(this.hostname = hostname || “localhost”),但不适用于允许0、NaN或空字符串为有效参数的情况,比如this.hostname就是等于undefined

-【检查】应该提供参数默认值应当采用测试undefined的方式,而不是检查arguments.length,即 this.width = width === undefined ? 320 : width

-【参数】使用接收关键字参数的选项对象,options object,既接收Alert({xxx:yyy})的方式,避免参数过多,API失去可扩展性

-【设计】避免不必要的状态,API有时候被归为两类:有状态的(Date对象)和无状态的(幂等性,输入对应固定输出),无状态的API比有状态的API,从易用性,学习容易性而言,更优。无状态的API简洁,易于扩展

-【设计】API绝对不应该重载与其他类型有重叠的类型,一个方法不应该既接收type 1又接收type 2,当重载一个结构类型与其他类型时,先测试其他类型

- 判断是不是数组:Array.isArray 和 toString.call(x) === "[Object Array]"
- 将对象转换成数组:[].slice.call(arguments)

-【设计】避免过度的强制转换,应该显示的转换,用代码写出来,而不是交给编译器

-【设计】支持方法链

- 使用方法连来连接无状态的操作
- 通过在无状态的方法中返回新对象来支持方法链
- 通过在有状态的方法中返回this来支持方法连

并发

  • 异步的强大能力:JS的运行到完成机制(run-to-complete),在系统里维护了一个按时间发生顺序的内部事件队列,一次调用一个已注册的回调函数,通过轮询这个队列来完成异步的操作,有序的调用回调函数。

  • Workers API提供了线程的能力,在一个完全隔离的状态下执行,没有获取全局作用域或应用程序主线程Web页面内容的能力

  • 应该避免将可被并行执行的操作顺序化,嵌套的回调函数应该用Promise

  • 对于异步的代码,多步的处理通常被分隔到时间队列的单独伦次中,因此不可能将它们全部包装在一个try语句块中。事实上,异步的API甚至根本不可能抛出异常,因为,当一个异步的错误发生时,没有一个明显的执行上下文来抛出异常!因此,异步API更加倾向于将错误表示为回调函数的特定参数,即err,在Nodejs平台中,err是回调函数的第一个参数

  • 目前典型的JS环境中一个递归函数同步调用自身过多次会导致失败,原因是:存储过多的栈帧需要的空间量会消耗JS环境分配的空间

    • 循环不能是异步的

    • 使用递归函数在事件循环的单独轮次中执行迭代

      function downloadOneAsync(urls, onsuccess, onfailure){
          var n = urls.length;
          function tryNextURL(i){
              if(i>=n){
                  onfailure("xxxxxxx");
                  return;    
              }
              downloadAsync(url[i], onsuccess,function(){
                  tryNextURL(i+1);
              });
          }
          tryNextURL(0);
      } 
      
  • 避免在主事件队列中执行代价高昂的算法,在支持Worker API的平台,该API可以用来在一个独立的事件队列中运行场计算程序,在Work API不可用或代价昂贵的环境中,考虑将计算程序分解到事件循环的多个轮次中

  • 使用计数器来执行并行操作可以避免竞争,Js应用程序中的事件发生时不确定的,即顺序是不可预测的

  • 同步地调用异步的回调函数扰乱了预期的操作序列,并可能导致意想不到的交错代码,也可能导致栈溢出或者错误的异常处理

Promise
  • promise代表最终值,即并行操作完成时最终产生的结果

  • 使用promise组合不同的并行操作,避免数据竞争

  • 在要求有意的竞争条件时,使用select

如需转载,请注明出处

JS文档生成工具-Jsdoc

Jsdoc是一款JS文档的生成工具,提供了丰富的标签,写起文档来十分方便,具体标签用法参考 Jsdoc,这里就不多赘述。但它的用处,远远不止于生成文档,最近通过研究,发现利用Jsdoc工具,可以干更多的事情,待下面剖析。

生成原理

首先来谈谈Jsdoc的生成原理,即如何从注释代码生成最后的网页。这个过程其实很简单,通过文档解析器parser对每个js文件进行分析,提取出文档信息,之后的一步很关键,Jsdoc将提取出来的信息存放到了taffydb中,进而根据这些信息来生成网页。

为了更好的解释这个过程,来看Jsdoc的源码

1
2
3
4
/* templates/default/publish.js */
exports.publish = function(taffyData, opts, tutorials) {
//generate Jsdoc html
}

生成html的代码逻辑,就在publish这关键函数中,从接受的参数来看,taffyData中已经存放好解析之后的文档信息,opts配置文件的解析,tutorials是教程的信息,未做过多研究。那么taffyData中存放的信息试什么样子的呢?来看如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
***** Debug ******
[ { comment: '/**\n* <p>Start ...... @public\n* @since 1\n */',
meta:
{ range: [Object],
filename: 'LocationManager.js',
lineno: 69,
code: [Object],
vars: [Object] },
description: '<p>Start request location.</p> ......more.</p>\n',
permission: [ [Object] ],
see: [ '[removeLocationU...r#removeLocationUpdates}' ],
params: [ [Object] ],
returns: [ [Object] ],
access: 'public',
since: '1',
name: 'requestLocationUpdates',
longname: 'xxx.location.LocationManager#requestLocationUpdates',
kind: 'function',
memberof: 'xxx.location.LocationManager',
scope: 'instance',
___id: 'T000002R003347',
___s: true } ]

以上信息就是taffyData中存放的文档信息,可以看到,关键的Jsdoc信息,比如@since 1解析成了since:'1'@public解析成了access:publickind:'function',经过这般解析,taffyData中的数据已经足够可以用来生成网页,从而可以看出,在publish函数中,围绕着taffydb的数据,通过各种数据处理,可以做一些更多的事情,这里只是抛砖引玉,比如统计代码的各种信息、检查Jsdoc是否符合规范等等

以一张图简要描述下Jsdoc的原理

至此,我们分析出了Jsdoc的关键信息:Jsdoc解析之后的信息全部存放到taffyData中,而这些信息作为接口publish的参数,提供了良好的扩展性

如果进一步了解信息转储,请查阅:app.js # app.jsdoc.parser.parse -> parser.js # parser.createParser 这个方法

自定义扩展

接下来来看看Jsdoc提供的三种自定义扩展方式,分别是:自定义EventHandler级别的插件,自定义标签,自定义抽象语法树访问器,下面来分别解释下这三种扩展方式的区别

  • 自定义EventHandler级别的插件

    这种扩展方式是最高级别的扩展,如果学过监听器设计模式,看如下代码就一目了然了,一个EventHandler插件包含了以下几个阶段的监听

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    exports.handlers = {
    parseBegin: function(sourcefiles){},
    fileBegin: function(filename){},
    beforeParse: function(filename,content){},
    JsdocCommentFound: function(filename, comment, lineno){},
    symbolFound: function(..params){ },
    newDoclet: function(doclet) {},
    fileComplete: function(filename,content){},
    parseComplete: function(sourcefiles, doclets){},
    processingComplete: function(doclets){}
    };

    不难理解,Jsdoc在生成过程中,抛出了以上这些阶段事件供扩展,每个事件的不同参数,代表的当前阶段产生的中间结果。比如在解析之前(parseBegin),传入的参数代表源文件文件名列表,可以做一些正比表达式的匹配,指定生成某些文件的文档。在解析完一个文件的Jsdoc信息时(newDoclet),doclet中就包括了上面taffydb中的信息。有了这几个阶段,相信很轻松的就可以扩展出任何想要的插件,关键在于插件的行为是由哪些阶段完成的。

  • 自定义标签

    这种扩展方式是中间级别的扩展,主要用于当Jsdoc官方提供的标签不能满足文档注释需要的时候,选择自定义标签来补充完善Jsdoc的能力,比如,我需要在文档中注释一个权限声明的标签,@permission,那么可以这样做

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    exports.defineTags = function(dictionary) {
    var opts = {
    canHaveName: true,
    mustHaveValue: true,
    onTagged: function(doclet, tag) {
    doclet.permission = doclet.permission || [];
    doclet.permission.push(tag);
    }
    }
    dictionary.defineTag("permission", opts);
    }

    Jsdoc提供了defineTags接口来自定义标签,其参数dictionary可以理解为标签字典,如上述代码描述,将自定义标签和属性,添加到dictionary中,这样就能被Jsdoc在解析中识别了,onTagged方法定义了当解析到该标签时,Jsdoc应该做出的操作,这里还是doclet,还记得吗?就是那个taffydb的数据,翻到上面那个taffydb数据信息看看,permission就在其中。自定义标签相比上面的EventHandler级别的插件,实现起来简单、方便。

  • 自定义抽象语法树访问器

    这种扩展方式时最低级别的扩展,官方文档中用了(lowest)一词。同样,来看代码

    1
    2
    3
    4
    5
    exports.astNodeVisitor = {
    visitNode: function(node, e, parser, currentSourceName) {
    // do all sorts of crazy things here
    }
    };

    由于直接接触的对象是抽象语法数,那么可做的事情就更多了,就好比越是接近系统底层,可扩展的能力越高,越是上层的东西,可扩展能力就越小,Jsdoc提供了astNodeVisitor接口来扩展访问到AST每个节点的行为,感兴趣的同学可以好好实战一番,这里就不过多赘述了

更多

Jsdoc的主要原理和扩展方式在上面分析完了,更多完整功能用法,请参考Jsdoc

如需转载,请注明出处

读书笔记-大话设计模式

迪米特法则

  • 也叫最少知识原则,即:类的设计要降低耦合,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用

  • 在类的结构设计上,每一个类都应当尽量降低成员的访问权限,设计时应当考虑两个类是直接发生调用关系,还是通过第三方来搭建关系

合成/聚合复用原则

  • 尽量使用合成/聚合,尽量不要使用类继承,因为父类的改变,会影响到所有子类

  • 聚合表示一种弱的拥有关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。合成是一种强的拥有关系,体现的是严格的部分和整体的关系。

  • 大雁拥有翅膀(合成),许多大雁聚合成雁群(聚合)


原型模式

  • 深复制:把引用对象的变量指向复制过的新对象,而不是原有的被引用的对象

  • 浅复制:被复制对象的鄋变量都含有与原来的对象相同的值,而所有对其他对象的引用都仍然指向原来的对象

  • 如何实现深复制: 用 私有构造函数 创建引用对象,然后再clone方法里调用私有构造函数,完成克隆,其他的值引用则在clone里面直接赋值就好了

模板模式

  • 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,模板方法是的子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤

  • 一切逻辑都在父类中写好,需要延迟交给子类的部分,通过让子类@Override方法来实现,或者抽象类实现逻辑,具体类实现细节

外观模式

  • 为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用,比如,基金是各种股票的外观模式,购买者只需要和基金打交道就好了

  • 分层的处理:Facade类,承上启下的作用,对外暴露的接口简单,统一

建造者模式

  • Builder 和 Director: Builder是抽象类,用于让开发者实现一些细节步骤,Director里面包含了Buider的实例,用于封装一些操作逻辑,这样开发者每次只需要根据自己的需求实现Builder,然后交给Director就可以了,不用care操作逻辑是否有遗漏

  • 适用于当创建复杂对象的算法应该独立于该对象的组成部分以及他们的装配方式时适用的模式,即将一个复杂对象的构建与它的表示分离,使得同样的构建过程(Director)可以创建不同的表示(Builder)

状态模式

  • 当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类

  • 状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时,把状态的判断逻辑转移到表示不同状态的一系列类当中(实际!

适配器模式

  • 将一个类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

  • 使用一个已经存在的类,但如果它的接口,也就是它的方法和你的要求不相同时,就应该考虑用适配器模式,即:使用一个Adapter类实现抽象接口,里面持有待适配的对象,在Override接口方法的时候,将待适配对象的方法实现到接口中

备忘录模式

  • 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。

  • Originator(发起人,负责创建一个备忘录,即在SaveStatus时创建个Mememto) 和 Memento(备忘录,负责创建状态副本,保存在自己的类中) 和 Caretaker(管理者,负责管理内部的状态副本get和set)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Originator {

public Mememto SaveState(){
return new Memento(vit,atk,def)
}
public void RecoveryState(Mememto state){
this.vit = state.vit;
this.atk = state.atk;
this.def = state.def;
}
}

class Memento {

private int vit;
private int atk;
private int def;
public Memento(int vit, int atk, int def){
this.vit = vit;
this.atk = atk;
this.def = def;
}
/...
get/set
.../
}

class Caretaker {
private Memento memento;
/...
get/set
.../
}

//----------To Do----------

Caretaker.memento = Originator.SaveState();
Originator.RecoveryState(Caretaker.memento);

组合模式

  • 将对象组合成属性结构以表示”部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性

  • “部分-整体” 的关系实质上为 “树叶-根”的关系,所有的组件实现统一的接口,这个接口包含扩展组件的能力,即

1
2
3
4
5
6
7
8
9
10
11
12
abstarct class Component{

protected string name;

public Component(string name){
this.name = name;
}

public abstarct void Add(Compoent c); // 扩展能力
public abstarct void Remove(Compoent c); // 扩展能力
public abstarct void Display(Compoent c); // 各司其职
}

迭代器模式

  • 提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示,即当你需要访问一个聚集对象,而且不管这些对象是什么,就应该考虑用迭代器模式

  • 迭代器抽象类(Iterator) 和 迭代器具体类 (ConcreteIterator)

  • 聚集抽象类(Aggregate)和 聚集具体类(ConcreteAggregate)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
abstaract class Iterator {

public abstract object First();
public abstract object Next();
public abstract bool IsDone();
public abstract object CurrentItem();
}

abstaract class Aggregate {
public abstract Iterator createIterator();
}

class ConcreteAggregate:Aggregate {

private IList<object> items = new List<object>();
public orerride Iterator createIterator{
return new ConcreteIterator(this);
}
}

class ConcreteIterator : Iterator {

private ConcreteAggregate aggregate;
public ConcreteIterator(ConcreteAggregate aggregate){
this.aggregate = aggregate;
}
/**
* Override methods in Iterator
* /
}

桥接模式

  • 将抽象部分与它的实现部分分离,使它们都可以独立的变化

  • Abstraction 和 Implementor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
abstract class Implementor {
public abstarct void Operation();
}

class ConcreteImplementorA : Implenmentor{
public override void Operation()
}

class ConcreteImplementorB : Implenmentor{
public override void Operation()
}

class Abstraction {
protected Implementor implementor;
public void SetImplementor(Implementor implementor){
this.implementor = implementor;
}
public virtual void Operation(){}
}

class RefinedAbstraction: Abstraction{
public override void Operation(){
implementor.Operation();
}
}

static void Main(string[] args){
Abstraction ab = new RefinedAbstraction();
ab.SetImpelementor(new ConcreteImplementorA());
ad.Operation();
ab.SetImpelementor(new ConcreteImplementorB());
ad.Operation();
}

命令模式

  • 分离两个类的职责,统一规划到中间的消息队列管理:客户 - 服务员 - 烤肉者

  • 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作,它将请求一个操作的对象与知道怎么执行一个操作的对象分隔开: Command 和 Receiver 绑定,而 Command 的调度执行,由 Invoker 负责

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
abstract class Command{

protected Receiver receiver;
public Command(Receiver receiver){
this.receiver = receiver;
}
abstract public void Execute();
}

class ConcreteCommand : Command{

public ConcreteCommand(Receiver receiver): base(receiver){}
public override void Execute(){
receiver.Action();
}
}

class Invoker{

private Command command;
public void SetCommand(Command command){
this.command = command;
}
public void ExecuteCommand(){
command.Execute();
}
}

class Receiver{
public void Action(){}
}

public static void Main(){
Receiver r = new Receiver();
Command c = new ConcreteCommand(r);
Invoker i = new Invoker();
i.SetCommand(c);
i.ExecuteCommand();
}

职责链模式

  • 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这个对象连成一个链,并沿着这条链传递该请求,知道有一个对象处理它为止(Chain)

  • 接收者和发送者都没有对方的明确信息,且链中的对象自己也并不知道链的结构。结果是职责链可简化对象的相互连接,仅需要保持一个向后引用

  • 随时增加或修改处理一个请求的结构,增强了给对象指派职责的灵活性,最坏情况:到末端都得不到处理,需要事先考虑全面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class Handler{

protected Handler successor;
public void SetSuccessor(Handler successor){
this.successor = successor;
}
public abstract void HandleRequest(int request);
}

class ConcreteHandler1 : Handler {
public override void HandleRequest(int request){
if(request >= 0 && request < 10){
// do handle request
}else if(successor != null){
successor.HandleRequest(request);
}
}
}

中介模式

  • 对象之间大量的连接使得一个对象不可能在没有其他对象的支持下工作,系统表现为一个不可分割的整理,所谓低耦合的解决方案:中介模式其中之一

  • 用一个中介对象来封装一系列的对象交互。中介者使得各对象不需要显示地相互引用,从而使其耦合松散,并且可以独立的改变他们之间的交互。

  • 缺点:把交互的复杂性变成了中介者的复杂性,需要着重考虑,对象之间多对多的交互,首先应当考虑系统设计上的合理性

享元模式

  • 如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用享元模式,还有就是对象的大多数的外部状态,如果删除不影响的话,那么可以用相对较少的共享对象取代很多组对象,此时也应该考虑使用享元模式,比如:线程池

  • 享元模式共享的是对象,对象数量是有限制的,因此会在工厂内部保存对象的引用,而工厂模式是生成对象,对象数量是无限制的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
abstract class Flyweight{
public abstract void Operation(int ee)
}

class ConcreteFlyweight : Flyweight{
public override void Operation(int ee){}
}

class FlyweightFactory{

private Hashtable flyweights = new Hashtable();

public FlyweightFactory(){
//内部保留有限的对象数
flyweights.Add("X", new ConcreteFlyweight());
flyweights.Add("Y", new ConcreteFlyweight());
flyweights.Add("Z", new ConcreteFlyweight());
}

public Flyweight GetFlyweight(string key){
return ((Flyweight)flyweights[key]);
}
}

解释器模式

  • 同样的一个抽象操作,需要不同的实施方法,则考虑解释器模式

  • 缺点在于需要维护多个解释器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
abstract class AbstractExpression {
public abstract void Interpret(Context ctx)
}

TerminalExpression: AbstractExpression {
public override void Interpret(Context ctx){}
}

NonterminalExpression: AbstractExpression {
public override void Interpret(Context ctx){}
}

// 全局信息
class Context {

private string input;
private string output;
}

static void Main(string[] args){
Context ctx = new Context();
AbstractExpression terminal = new TerminalExpression();
terminal.interpret(ctx);
AbstractExpression terminal = new NonterminalExpression();
nonterminal.interpret(ctx);
}

访问者模式

  • 表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类前提下定义作用于这些元素的新操作。

  • 访问者模式的目的是要把处理从数据结构分离出来,有稳定的数据结构,又有易于变化的算法,使用访问者模式就是比较合理的,因为增加算法的操作变得尤为容易

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
abstract class Visitor {
public abstract void useAlgorithmA(concreteElementA A);
public abstract void useAlgorithmB(concreteElementB B)
}

ConcreteVisitor1: Visitor {
// implements
}

ConcreteVisitor2: Visitor {
// implements
}

abstract class Element{
public abstract void Accept(Visitor visitor);
}

ConcreteElementr1 : Element {
public override void Accept(Visitor visitor){
visitor.useAlgorithmA(this);
}
}

ConcreteElementr2 : Element {
public override void Accept(Visitor visitor){
visitor.useAlgorithmB(this);
}
}

class Contorller {
//整合 visitor 和 element, 使得对外操作统一
private IList<Element> elements = new List<Element>();

public void Attach(Element element){
elements.Add(element);
}

public void Detach(Element element){
elements.Remove(element);
}

public void Accept(Visitor visitor){
foreach(Element e in elements){
e.Accept(visitor);
}
}
}
如需转载,请注明出处

Node命令行工具-Yargs和Commander

日常使用Node时,避免不了编写几个命令行工具。作为命令行工具,最直接的就是针对输入的各种参数、选项等,执行相应的命令。使用原生的progress.argv虽然可以进行参数判断,但使用起来十分不方便,为此研究了下Node第三方命令行工具 yargs 和 commander

Yargs

首先来看一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
λ index -h                                          
Usage: index.js <command> -a [num] -b [num]

Commands:
add same as a + b
plus same as a - b

Options:
-a, -A load a parameter [number] [required]
-b, -B load b parameter [number] [required]
-h, --help Show help [boolean]

Examples:
index.js add -a 10 -b 3 //compute 10 add 3

这段帮助信息是用 yargs 编写的,实现起来很简单,来看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env node

var yargs = require('yargs');
ARGV = yargs.usage('Usage: $0 <command> -a [num] -b [num]')
.command('add', 'same as a + b')
.command('plus', 'same as a - b')
.example('$0 add -a 10 -b 3', '//compute 10 add 3')
.alias('a', 'A')
.alias('b', 'B')
.alias('h', 'help')
.number('a')
.number('b')
.describe('a', 'load a parameters')
.describe('b', 'load b parameters')
.demand(['a','b'])
.help('h')
.argv
if(ARGV.h || ARGV.help){
print(ARGV.help());
process.exit(0);
}

上述代码可以分为几个阶段

  • usage: 命令行工具的使用方法

  • command: 如果需要输入命令,则用 command('命令','描述') 的方式标注

  • example: 给出使用的例子,用 example('命令','描述') 的方式标注

  • alias: 对输入参数别名化,比如 -a 和 -A 是等效的

  • number(string,boolean,array…): 对输入的参数进行类型转换,比如.array(a), -a 10 4 3 5,那么 ARGV.a 是 [10,4,3,5]

  • describe: 对输入参数进行描述,用 describe('参数','描述') 的方式标注

  • require:标注哪些参数是必备的,比如 a 和 b 的后面加入了 [require] 的描述

  • help/epilog: 辅助的帮助/版权信息

就这样,一个简单的命令行信息交互帮助信息就生成了,后面可以用 ARGV.xxx 获得输入的参数,做后续程序的逻辑判断。

进阶用法
  • ARGV._ : 获得未指定的输入参数,比如 index -a 10 -b 3 5 “no” “13”,则 ARGV._ 获得 [‘5’,’no’,’13’]

  • .check(cb):检查输入参数合法性,如果 cb 返回 false,则输出帮助信息并退出

  • .fail(cb):当工具运行出错时,输出错误信息

更多用法请参考 yargs,一般情况下,用 yargs 快速搭建一个命令行的使用帮助信息就足够了,不建议把判断的复杂逻辑也集成到 yargs 中

Commander

还是之前的例子,用 commander 实现的运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
λ commander --help

Usage: commander <command> -a [num] -b [num]


Commands:

add same as a + b
plus same as a - b

Options:

-h, --help output usage information
-a, -A [num] load a parameter
-b, -B [num] load b parameter

来看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env node

var program = require('commander');

program
.usage('<command> -a [num] -b [num]')
.option('-a, -A [num]', 'load a parameter')
.option('-b, -B [num]', 'load b parameter');

program
.command('add')
.description('same as a + b');

program
.command('plus')
.description('same as a - b');

program.parse(process.argv);

通过对比,个人比较喜欢 commander 的代码风格,每个 command 单独处理,可以各种自定义,比如针对某个命令输出 help 信息,而 yargs 的那种分块编码,也值得借鉴,从代码量而言,yargs 和 commander 指定复杂的命令时,前者的代码量会比较少,同样,前者在功能上比后者更加丰富,适合复杂通用的命令行工具,更多用法请参考 commander

后记:Windows 上如何调用nodejs命令行工具

在本机上找到任意一个npm的安装包,都能发现有 xxx.cmd 这样的文件,打开如下

1
2
3
4
5
6
7
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\xxx.js" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\xxx.js" %*
)

不难理解,在window上要执行nodejs,必须要用 node xxx.js 的方式,而这段代码就是省略了前面的node,直接用 xxx 即可,比如之前的例子中,#!/usr/bin/env node用来在linux上指定脚本运行环境为node,所以可以直接用 ./script.js 加参数运行,在window上,将上面的文件中 xxx 替换成script,即可用 script 加参数运行了

如果以后涉及到用Node写两个平台通用的工具,请记住编写这个文件

如需转载,请注明出处

JS压缩混淆工具-UglifyJS

最近写了一些JS的项目,顺便研究了下UglifyJS工具,它是一款JS代码处理工具,提供了压缩,混淆和代码规范化等功能,同时还支持命令行和NodeJS两种使用方式,在代码基本”格式化”功能的基础上,可自定义扩展一些功能选项,满足具体项目中的使用。在业界内,JQuery使用UglifyJS进行代码压缩混淆,可见其结果具备准确性和稳定性。

使用方式:uglifyjs [input files] [options]

SourceMap

UglifyJS支持使用/生成SourceMap, 那什么是SourceMap,简单来说就是一个解码表,JS代码从原生到压缩混淆之后,从原来的几百行甚至几千行中,压缩到几十行。不仅代码行数骤减,同时经过混淆,代码的变量声明都进行了替换,因此当我们看混淆之后的代码,很难读懂代码的本意,这也是压缩混淆的初衷,减少JS代码的大小,方便网络传输,理论上对代码进行了保护。利弊相对,压缩混淆之后的代码对调试来说尤为困难,因此SourceMap就出现了。

简单来讲,压缩混淆之后的代码通过SourceMap,即可还原成原始的JS代码,这就是为什么说它是一个解码表的原因,因为反向还原了JS代码,结合到UglifyJS而言,其支持以下几个选项:

  • source-map 指定生成一个SourceMap的文件名
  • source-map-root 指定SourceMap的根目录位置
  • source-map-url 指定SourceMap的网络路径
  • source-map-inline 在压缩混淆的JS代码后,追加SourceMap内容
  • in-source-map 指定一个SourceMap

功能:压缩

使用UglifyJS进行代码压缩,-c 选项即启用压缩,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
"use strict";
var net = require('net');

const IP = "127.0.0.1";
const PORT = 8887;

var client = new net.Socket();
client.connect(PORT, IP, function() {

console.log('CONNECTED TO: ' + IP + ':' + PORT);
client.write('I am Chuck Norris!');

});

压缩后变成

1
"use strict";var net=require("net");const IP="127.0.0.1",PORT=8887;var client=new net.Socket;client.connect(PORT,IP,function(){console.log("CONNECTED TO: "+IP+":"+PORT),client.write("I am Chuck Norris!")});

可以看出,压缩之后去掉了空行以及连续声明时候的关键字const,如果让你调试这样的出错代码,没有SourceMap,则无从下手

功能:混淆

使用UglifyJS进行代码混淆,-m 选项即启用混淆,例如,还是上面的代码,经过混淆后变成

1
"use strict";var c=require("net");const e="127.0.0.1";const n=8887;var o=new c.Socket;o.connect(n,e,function(){console.log("CONNECTED TO: "+e+":"+n);o.write("I am Chuck Norris!")});

可以看出,混淆把变量都替换成了单字符,相比于压缩,进一步的减小了代码量

总结

UglifyJS是一款出色的JS代码压缩,混淆工具,其具有丰富的功能,例如代码美化,支持NodeJS自定义编程等,更多细节请参考 UglifyJS

如需转载,请注明出处

读书笔记-Google工作整理术

认知大脑

  • 为了实现大脑压力最小化,要把生活组织的有条不紊

  • 让信息尽可能快地离开大脑

  • 多重任务通常让你降低效率

  • 利用故事去记忆

共勉句
  • 我们做决定经常是建立在害怕失去什么的基础上,而不是考虑希望得到什么

  • 紊乱无需让你搞到重重压力,进而举措失当,承受等大的压力,让你更加失误连连

  • 音乐可以减缓压力

  • 识别要比回忆轻松地多

  • 要回忆一个事实,先努力想想第一次注意到那个事实时你正在干什么,这是很有帮助的

  • 面对太多选择的时候,我们经常倾向于选择最熟悉的东西

  • 做决定的关键在于你的目标是什么,并确定他们的优先次序,展望各不相同的最终结果,比较各种决定的效果。

  • 多样性会让你和你的同事在实现目标的过程中更加出类拔萃

错误的有序

  • 仅仅因为一直都按照某种特定方式做某事,并不意味着就该永远这样做(朝九晚五、暑假)

  • 知识不是力量,共享知识才是力量

共勉句
  • 分享自己的所知,鼓励别人也这么做,你就会从他们那里学有所获,他们也可以从你这里学到东西,然后你们就会胜任更好的工作

  • 解决问题的办法不是更加辛苦的工作,而是利用现有的工具和技术,巧妙的工作

突破制约

  • 进行组织安排时,要绕开的是实际制约而不是假性制约

  • 对自己要坦诚,但是千万不要自我评判,坦诚的检视下你所处的环境,多听听可信赖的人的意见

  • 要懂得什么时候忽略制约,不要第一步都迈不出去

共勉句
  • 学会识别、接受并绕开你的实际制约,从而不在你无法改变的事情上浪费时间和精力

  • 实际制约通常涉及你绝对无法控制的某些因素

  • 有时候,超出我们控制范围的制约刚好就潜伏在你能控制的哪些制约的表面之下

  • 源自情绪的制约最难识别和控制,无论面对什么制约,控制好自己的举止,行动,愿望和情绪

  • 在不可预料的意外发生之前,对于那些面临压力时还会变本加厉的制约,要及早辨别并准备好绕开它的有效策略,这是十分重要的

目标决策

  • 在发动汽车之前,一定要确切的搞清楚自己要到哪里去,还要知道选择什么途径去

  • 在实现目标的方式上灵活变通

共勉句
  • 在一件事之前,要搞清楚 你的问题是什么?,也就间接的让自己清楚目标是什么

  • 所做的每一件事都要从自己的目标出发,切勿盲目,偏离初衷

  • 一旦确定了某个项目的目标,先学会四处看看,确定一下可能面临的问题是否有人已经解决了,别人所用的解决办法中哪些要素可以采纳或借鉴

  • 给别人委派任务涉及信任问题,请求别人做事情,就一定要信任别人,更不要剥夺别人学习新的东西、担负更多责任的机会

  • 针对你的决策将会产生的不同结果,逐个试试看,形象的展望一个决定可能产生的不同结果,可以帮你明确什么才是你真正想要的

  • 任何决定都需要经过反复推敲做出假设,写下来,每天看一遍

Google

搜索的时代

  • 不要给信息处处归档,用的时候去搜索就行了
共勉句
  • 搜索功能是新式组织管理的基础,我们不必在文件归档方面耗时间和精力,也用不着为了找到重要信息而劳神费力

基本搜索引擎技巧

  • PageRank:别人说他好,才是真的好(引用的越多,越有价值)

  • 技巧

    • 尽量增强搜索条件的描述性(关键词多一些)

    • 在搜索条件短语中使用引号(引号表示确认在搜索的网页中包含)

    • 使用形容词搜索(”apple ~cheap”)

    • 排除不想要的结果(”opera -browser”)

    • 通过数值范围获取特定信息(…表示数值范围,”digital camera” $100…$300)

    • 搜索特定网站(”paris hotels” site:nytimes.com)

    • 搜索特定类型的文件(”paris hotels” filetype:xls)

信息过滤器

  • 目标是信息存储的向导,大脑中只保存真正需要记忆的内容

  • 大块的内容要化整为零

  • 每周拿出些时间回顾关键信息,再次梳理归类

共勉句
  • 幸运的是,我们接收的绝大部分信息都没有必要记住,因为大部分信息不值得我们记忆

  • 信息的有效性取决于你的目标是什么,不断的分门别类,建立信息等级(忽略,今后它用,记忆),离目标越近的信息就是越需要的

  • 重复和回归通常可以帮你确定目标

  • 只要有可能,就把大块信息分解成小块,就不会感到困难了,分解信息会当你找出其中的规律和主题

纸质文档 与 电子文档

  • 没有一个完美无缺的组织方法

  • 通常消化吸收之后的信息,写下来会比较好

  • 做笔记的过程就是锻炼信息筛选的过程,好的笔记不是全,而是精

共勉句
  • 每当文件柜即将塞满的时候,我会直接筛选一遍文档,不再需要的财务票据及其它敏感信息,我会当场销毁,其它不用的文件继续回收利用

  • 重要文档,必须纸质保存

  • 信息多的时候,先分类,后处理

  • 要使用信誉好的在线存储或备份业务

  • 针对需要处理的信息,你要确定什么时候最可能用到,如何去使用,需要保存的期限有多长,以及你打算与谁共享,从而决定存储方式。

  • 何时使用纸质工具

    • 解决某个问题或者提出一种想法,请随时记录下来

    • 目标是记住或者真正吸收的信息,打印出来比盯着屏幕要好

    • 随身携带一个小笔记本

  • 何时使用数字工具

    • 需要与别人共享的信息

    • 需要快速查看的信息

    • 存储比较久的信息

电子邮箱 Gmail

  • 在数字信息中加上相应的关键词,以便日后容易找到
共勉句
  • Gmail 是一种近乎理想的脚手架,简单易用,免费,容量大

  • 标签比文件夹更有效,一封邮件可以拥有多个标签

  • aaronshicf9032+shopping@gmail.cn 简直爽的不要不要的,自动过滤

  • 把密码提示线索保存在Gmail邮箱之类的便于访问的安全场所

日历 Google Calendar

  • 跟特定人士共享特定日程,其实就是为他们过滤你的日程信息

  • 尽量使用已经上手的工具

协同文档 Google doc

  • 轻量级,适用于协作:存放重要本地文档、组织一个待办事项总清单、掌握资金使用情况、共享临时信息

  • 每周拿出些时间回顾关键信息

也许是精华

  • 如何减轻大脑的压力:及时加注解,有助于以后了解背景信息

  • 思维转换:从一件事情过度到另一件事,转换气味的时候,大脑必须要吧短期记忆中的当前内容转移到长期记忆中,以便为新信息腾出空间,这样一来,有些信息就会丢失

  • 思维转换次数越多,各种思维背景之间的关联度就月底,大脑的工作难度就越大

  • 晚上要睡好,在你和你的大脑都能休息好的时候,转换思维要容易的多

  • 如何减少思维转换的次数

    • 当需要转换的时候,随手加上背景注解,方便下次直接还原当时的想法

    • 把类似的任务或会议安排在一起

    • 分辨哪些分神的因素导致频繁进行并不必要的思维转换,然后减少这些因素

    • 心无旁骛的工作状态:定期查收邮件,关闭手机干扰,隔间里工作

    • 召开会议之前先的明确会议主题和目标

  • 把工作和生活融为一体,而不是力图在二者之间求平衡

  • 在效率下降的时候自我放松一下,同时需要工作的时候随时工作

如何处理意外事件

  • 无论如何都要”把握当前”,在危机出现之前,如果已经做到未雨绸缪,胸有成竹,那现在就会更加从容不迫,有备无患

  • 在你面对重大挑战的时候,典型反应就是寻求更多信息

  • 在出现危机时,依然具备处理好当前事情的能力,即掌控当前局面的能力,而这个能力需要实践经验的积累

  • 你无法预知未来,但你可以确认正在发生的坏事表现出来的征兆,从而未雨绸缪,以便坏事发生时,知道如何应对

  • 多跟哪些不同于自己的朋友沟通交流,这在艰难时期更加重要

  • 过分的考虑未来情况,经常会失去对当前的把握

找工作:真的可以如此高效?

  • 面试的时候,希望对方记住你,那么请利用故事去记忆

  • 仅仅因为一直都是按照某种特定方式做某事,并不意味着就该永远这样做

  • 进行组织安排时,要绕开的是实际制约而不是假性制约,面试的时候要理清楚什么是自身的实际制约,才能专注于一个目标

  • 每周拿出写关键事件回顾关键信息

  • 学会面试记录:尽量使用已经上手的工具 + 在数字信息上加上相应的关键词

如需转载,请注明出处