为什么我们的业务适合用Node?

本文从业务场景来谈谈为什么选择Node,以及前端写后端代码需要补足的短板。

这些日子一直在做Node方面的尝试,或多或少会收到周围的异样的目光甚至背后的质疑,于是促使我好好思考为什么我在做Node。网上搜下「为什么要用Node」,找到的文章多数是介绍Node多么多么牛逼,无非是从Node本身特性来说,比如:并发、事件驱动、非阻塞I/O、单线程、流、社区生态……诸如此类,很少谈业务场景。
我是「实用主义」者,说过:脱离业务场景谈架构都是耍流氓。因为个人是从一线业务做起的,经过几年对业务的思考,我觉得可以从业务场景来说说为什么我们的业务更适合用Node。

从业务场景说起

现在我们的业务模块化越来越普遍,很少有业务比较纯粹只有链接一个数据库就可以搞定,往往前台业务后面会有N多的API服务做支撑。比如:下面两种情况在我们实际开发中经常遇见:

  1. 某个页面需要的数据来自两个以上接口,而两个接口来自不同的团队/部门,比如:用户信息来自账号部门,而UGC数据来自业务部门
  2. 某个页面存在接口依赖,需要先调用接口A,然后根据接口A数据调取接口B,比如:个性化推荐,往往需要根据某些维度请求推荐系统拿到推荐数据的ID,至于内容,需要拿ID根据页面需要去获取具体元数据

上面两种情况,站在后台开发的角度来看,我们业务模块要分开要独立,而站在前端的角度来看,这些数据都是一个页面需要的,前端希望是一个接口给我返回。这是一个开始。。

当然后台开发,比如PHP也有并发请求的解决方案,好(上)心的后台工程师,会帮助在后台统一合并请求处理成一份数据或者接口,然后扔给页面使用。比如在实际开发中,我们的前端会写(并且维护)一个Template.class.php(我敢说我们80%的后台工程师都没看过这个代码。。),在View层使用,然后在Action当中将数据传给View层做渲染,下面的代码:

$this->render('xxx/xx.tpl', $tplData);

这样增加的沟通成本,降低了开发效率。为了一个页面,需要前端根据页面想要的数据,和后台沟通页面的数据格式,然后后台工程师找他们后面的API模块要数据、处理数据。这个过程中会有一些「灰色地带」,不好明确谁做更合适,完全靠自觉。

往往开发的时候会想各种方法来解耦,比如:引入后台模板(smarty之类),然后约定数据格式,前端根据数据格式来写Mock接口,写后台模板的前端就叫「大前端」;再Low一点的团队,会采取前端做好页面扔给后台工程师「套页面」,比如:PHP代码写HTML,各种<?php echo xxx;?>,代码很不友好,后台工程师幸福感也急剧下降。

还有一种做法是,干脆后台沦为「代理服务器」,收到请求我转给后面的API,拿到数据我返回给前端页面,做成可以「跨域」的接口,所以就成了好多webapp。

另外,站在后台工程师的个人发展来看,可能他们觉得:这些「包接口」的重复性工作,跟自己的晋升和技术发展又有毛线关系呢?

说道这里,肯定有人心里在嘀咕:这是你们大公司才有的问题,我们小公司不会有这样的问题!那我下面再从技术方面来说。

Hybrid 开发:JsBridge - Web和客户端双向通信

Hybrid开发中,web页面往往会跟native进行交互,而JSBridge就是web页面和native进行通信的桥梁,通过JSBridge可以实现web调用native的方法,native可以通过webview.loadUrl之类的方法,将javascript:xxx代码放在页面执行,这有点类似在浏览器地址栏直接输入:javascript:xxx

本文较长,先把目录列出来:

  • JSBridge多种形式
    • js Interface 直接注入到window对象
    • 改写浏览器原有对象:alert/console/prompt
    • URL scheme
  • 唤起APP技术
    • intent
    • localserver
    • scheme:deeplink/applink/Universal link
    • smart app banner
  • JSBridge安全
  • JSBridge的最佳实践
    • 协议规范
    • 回调函数
    • 预留升级/统计能力
    • 简单JSBridge调用封装

JSBridge多种形式

web和native进行通信,方法有很多,接下来一一列举一下JSBridge的多种形式,及其利弊。

JavaScriptInterface

JSInterface是安卓4.2-官方推荐的解决方案,原理是通过WebView提供的addJavascriptInterface方法给浏览器window注入一个命名空间,然后给Web增加一些可以操作Java的反射。

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

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

  }
} 
// js 代码
window.android.method();

JSInterface在4.2之前的版本都可以,但是存在严重的安全隐患,容易被利用提权,从而调用各种Java的类和权限,甚至页面可以挂马。在我们实际产品(手机百度)开始阶段,用过这个方法,不过现在已经不使用了。

JavaScript sdk(jssdk)设计指南

今天看到一篇《javascript sdk设计指南》,内容篇幅比较多,很多实际是问题的枚举,但是信息量太大,所以我结合之前做微博开放平台和运营活动平台的经验,说下jssdk的设计和一些核心问题的解决方案。

一个jssdk一般是指提供给第三方人员使用的一段js,通过这个js实现一些平台化产品提供的服务,比如微博的jssdk。整个jssdk的设计有一下几个核心问题:

  • 代码如何被使用页面接入
  • 如何实现跨域通信
  • 如何实现优雅api的设计
  • 公共资源的使用
  • 代码组件化

先说第一个问题

代码如何被使用页面接入

这个问题涉及到几个小问题需要讨论:

  • 命名空间
  • 样式冲突
  • 版本维护
  • appid等参数的传入

命名空间

在「命名空间」部分,需要做到不污染环境,保护好自己,即不要对本来的页面造成命名的破坏,只是用一个命名空间,又要考虑到第三方页面的复杂性,防止跟错综复杂的命名空间冲突。

要做到这点,需要我们在命名空间命名的时候多注意下,尽量不要使用业内通用的命名方法,比如驼峰,名字尽量起的怪一些,偏一些,一般,要么使用_开头(甚至多个),要么使用项目代号这些不太被别人想到的名字,嗯,我记得有人命名空间用av,很好呀!

还有一种方式是动态的命名空间,在url中带上namespace=xxx,本节结束后面会统一给出示例

样式冲突

除了js命名空间问题,如果jssdk带有UI组件,那么还需要考虑css的样式冲突问题,这里不用多说,记住以下几点:

  • 一些复杂的widget可以使用iframe方式引入
  • 不使用id
  • 使用带前缀的class命名,前面用一个class最好包裹
  • 自己做reset!
  • 跟js相关的class要有特殊的约定(比如_J-xxx )或者使用data-id代替

其实利用sass、less这些预编译语言很容易

例如下面的代码:

$name: avUI;

.#{$name}__dialog{
    @include reset();
    .#{$name}__dialog__header{
        color: white;
    }
}

版本维护

版本维护的目的是保证代码最新,功能最全,而不用每次做了升级,通知所有使用的第三方开发者把自己页面的代码挨个更换。所以这里版本维护不应该暴漏给使用者,比如在url使用版本号,到了2.0版本,通知使用方替换,这是不合理的,总有些公司或者人不配合的。最好的方法是设计的时候就要考虑到这个问题。

一般有两种比较好的方式:

  • 小拖大,动拖静:即第三方引入的js是一个动态的,或者没有缓存没有cdn的,然后由它带出后面的cdn
  • 隔段时间动态创建script

推荐使用「小拖大,动拖静」,后面介绍组件化也要使用这个方式来按需加载代码

小拖大,动拖静

核心代码示例

(function(){
    .....
    var url = '最新版本cdn的地址';
    load(url);
}())

隔段时间动态创建script

代码示例:

(function () {
    var s = document.createElement('script');
    s.type = 'text/javascript';
    var t = +new Date;
    t -= %864E5;
    s.src = 'http://xxx.com/sdk.js?t='+t;
    var x = document.getElementsByTagName('script')[0];
    x.parentNode.insertBefore(s, x);
  })();

五个你必须知道的javascript和web debug技术

在前端开发中,调试技术是必不可少的技能,本文将介绍五种前端开发必备的调试技术。

  1. Weinre移动调试
  2. DOM 断点
  3. debugger断点
  4. native方法hook
  5. 远程映射本地调试

Weinre

在移动上面开发调试是很复杂的,所以就有了weinre。安装weinre可以实现pc来调试手机页面,所以对于移动开发调试是很重要的哦~

http://people.apache.org/~pmuellr/weinre/docs/latest/images/weinre-demo.jpg

安装weinre

weinre可以通过npm来安装:

npm install -g weinre

安装完之后,可执行下面的命令来启动:

weinre --httpPort 8080 --boundHost -all-

双屏切图:使用livereload实现自动刷新

livereload是一个web开发辅助工具,当我们修改完html、css和js的时候会自动刷新浏览器,解放码农的双手。这样在双屏切图、写js代码的时候会提高很多效率。livereload有很多版本,比如基于ruby的版本,我们今天介绍的是node+grunt+chrome插件一体化方案。

我们使用的这个livereload的基本原理是试用node开启一个websocket服务,并且检测文件变化,浏览器打开一个页面时候,引入固定的livereload.js(chrome插件会帮忙加上)会建立ws请求,当node检测到文件变化,则自动推送消息给浏览器,实现刷新。

livereload环境搭建

  • nodejs安装
  • grunt安装
  • grunt-contrib-watch:npm install grunt-contrib-watch –save-dev
  • grunt-livereload:npm install grunt-livereload –save-dev
  • chrome插件:安装

javascript实现比较相似图片算法

昨天看了阮一峰老师的文章:《相似图片搜索原理》,于是把直方图和向量那块算法用js实现了一下,

源码如下:

function getHistogram(imageData) {
    var arr = [];
    for (var i = 0; i < 64; i++) {
        arr[i] = 0;
    }
    var data = imageData.data;
    var pow4 = Math.pow(4, 2);
    for (var i = 0, len = data.length; i < len; i += 4) {
        var red = (data[i] / 64) | 0;
        var green = (data[i + 1] / 64) | 0;
        var blue = (data[i + 2] / 64) | 0;
        var index = red * pow4 + green * 4 + blue;
        arr[index]++;
    }

    return arr;
}

function cosine(arr1, arr2) {
    var axb = 0,
        a = 0,
        b = 0;
    for (var i = 0, len = arr1.length; i < len; i++) {
        axb += arr1[i] * arr2[i];
        a += arr1[i] * arr1[i];
        b += arr2[i] * arr2[i];
    }
    return axb / (Math.sqrt(a) * Math.sqrt(b));
}
function gray(imgData) {
    var data = imgData.data;
    for (var i = 0, len = data.length; i < len; i += 4) {
        var gray = parseInt((data[i] + data[i + 1] + data[i + 2]) / 3);
        data[i + 2] = data[i + 1] = data[i] = gray;
    }
    return imgData;
}

有个问题,假如图片是灰色的跟原图进行比较,那么要比较相似度,需要将图片都转成灰色的,即使用上面代码的gray函数来处理

怎么清空javascript数组

这是一个很基础的问题,但是我们往往经常做错。

当一个数组需要清空的时候,很多人使用下面的方法:

a = [];

我们知道javascript变量存储方式分为引用类型和直接量。数组属于对象,即引用类型,引用的是变量指针地址,之所以这样设计也是为了节省内存。

再说到上面的请空数组的方式,如果采用直接赋值一个新数组的方式,那么之前引用的数组可能不会被释放(有其他的引用),例如下面的代码:

var a = [2,3];
var b = a;
a = [];
console.log(b);

这时候a和b就不是同一个数组了,清空了a而b还是指到之前的引用地址,除非你是故意的,否则这将会有隐患。

所以最佳的请空数组方式是:将length设置为0,即:

a.length = 0;

javascript的类型数组(Typed Array)

在《javascript canvas图片像素》中提到了类型数组(Typed Array),javascript这是一种快速操作二进制数据的解决方案。

随着web发展和html5的推进,javascript来处理音视频、二进制文件和网络协议等都成为了可能,这时候原生的数组Array已经不能够满足需求,需要一种快速操作二进制流的东东产生,于是ArrayBuffer类型和Typed Array诞生了。

类型数组介绍

类型化数组类型表示可编制索引和操纵的ArrayBuffer对象的各种识图。所有数组类型的长度固定不变。如下表所示:

名称 大小(以字节为单位) 说明
Int8Array 1 8 位二补码有符号整数
Uint8Array 1 8 位无符号整数
Int16Array 2 16 位二补码有符号整数
Uint16Array 2 16 位无符号整数
Int32Array 4 32 位二补码有符号整数
Uint32Array 4 32 位无符号整数
Float32Array 4 32 位 IEEE 浮点数
Float64Array 8 64 位 IEEE 浮点数

js模块并行加载器:MixJS

自己写了一个js模块加载器,所谓的并行其实也不完全是并行的,只是依赖关系模块是并行的。当然我知道现在有很多类似的js框架,而且MixJS也没有做更加全面的浏览器测试,只是跟我的意识写的一个加载器。

项目放在github上面:https://github.com/ksky521/MixJS
算是给2012年的一个总结。

简单说下功能

跟玉伯的Seajs不同,也跟CMD和AMD不同,我使用的是依赖关系明确的声明模块方式,这样减少了扫描代码读取依赖关系的方式,开发者虽然有了一定的束缚,但是总体来说,后期的维护成本,代码效率和实时combo、上线打包都会大大的降低成本。

MixJS介绍

轻量级前端模块化解决方案,提供模块管理、php实时合并、打包工具等方案。可以用于提供给第三方开发者使用的小组件,核心文件可以单独拿出来作为框架core,在此基础上可以开发出一整套的前端框架

MixJS还在开发完善阶段,未作完整兼容性测试,多数代码是出于解决问题的想法而写的,当然也copy了很多牛人的代码,比如cssload回调借鉴了seajs,Deferred来自于jQuery。。。。

。。。。鉴于最近前端圈比较乱,我的小手都开始颤抖,造轮子必然会被喷,我只能说我是根据自己的意识码了这些的代码,据此操作后果自负。。。。

0.2开发完成,现在开发基于0.2模块的开放平台部分代码,主要包括:Deferred(延迟队列)、API(API接口调用)、Widget(小组件)和XDomain(跨域)

模块加载不是单纯的文件加载,需要根据模块规范来写模块哦~

MixJS代码示例

参考github项目readme文件。

用途

关于MixJS的用途,主要解决是模块开发中的管理、加载。

我赞成使用MixJS是:细模块,粗文件。总体来看MixJS适合Widget开发、开放平台开放js开发,也适合作为模块管理器来打造更加强大的前端架构。

不多说了,吹嘘会被喷的,哈哈~

感谢

MixJS也借鉴了不少大牛的代码(嗯,你可以说是抄袭),感谢seajs,jQuery,mass,curl.js等作者。

再探javascript作用域

对于javascript作用域的理解,之前整理过两篇文章:《javascript作用域和作用域链》《javascript的词法作用域》。最近读了周爱民老师的《javascript语言精髓》,对javascript作用域有了新的认识,特地整理下思路。

javascript的作用域广义来分:语法作用域变量作用域

javascript语法作用域

javascript语法作用域是讨论代码的组织结构上抽象,可以理解为讨论的是“圈地”问题,也有书籍中对纯粹的语法作用域称为“静态作用域”。

语法作用域分四级,下标依次增高

级别 作用域
级别一 表达式
级别二 语句/批语句
级别三 函数
级别四 全局(宿主)
  1. 同级别可以平行也可以相互嵌套(包含)
  2. 高级别可以嵌套(包含)低级别
  3. 低级别不可以嵌套(包含)高级别作用域;假如低级别嵌套高u别,可以理解成相互平行,例如下面的代码是完全等效的
if(true){
    //
    function fn(){
        //
    }
}

if(true){
    //
}
function fn(){
    //
}