为什么我们的业务适合用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 APP开发:JSSDK

拖稿了好久的「Hybrid APP开发系列」又更新了~
今天继续写JSSDK

为什么会有JSSDK

我之前文章介绍了通过 JSBridge 实现页面和NA的相互调用,并且介绍了模板本地包的开发和后台维护系统。今天介绍的是JSSDK,通过 JSSDK 可以实现:

  1. 抹平JSBridge的平台实现差异
  2. 对齐端能力,内部消化版本差异
  3. sdk封装后的代码更加符合前端习惯
  4. 权限控制、鉴权、对外开放,实现生态建设

关于sdk的代码级别的设计,可以参考文章:《JSSDK设计指南》

如果做过微信页面开发的,应该都知道wx.js,这就是微信的JSSDK,在微信内需要调用微信的端能力就需要引入这个js。

JSSDK的设计

JSSDK的设计包括两部分:

  1. 随着每个NA客户端版本内置的js,称为:inject.js,他的主要作用是封装JSBridge逻辑,通过随版更新实现减少端能力的版本分裂,降低整个sdk的代码复杂性。inject.js是一段js代码,当客户端加载一个页面的时候,由客户端在适当的时机注入到webview内执行,执行后的代码就会有给webview增加js方法,例如微信的_WeixinJSBridge,类比chrome开发插件当中的content_scripts,可以在document_startdocument_end等时机进行执行。
  2. 云端JS,即实际暴漏给开发者使用的js,称为:jssdk.js,这个是真正开发者使用的sdk文件,通过script外链引入,例如wx.js,这个js文件通过和inject.js进行交换,完成端能力的调用、鉴权和客户端事件监听等操作

Vue SSR从入门到Case Study

最近两个项目同时开发,使用了Vue2的SSR,这样后端渲染页面首屏可以加快页面呈现,增加SEO和用户体验,但是项目上线后却发现了严重的性能问题,于是在三天内两次重大调整,最后只能放弃Vue SSR,本文从Vue SSR实现开始,逐渐复盘整个事件。

两周前就预告了要写一篇Vue SSR的文章,但是没想到上周四上线之后,周六放量之后发现性能问题,这周一到周三,做了两次重大调整,最终还是放弃了SSR,并且做了这次事件复盘。

技术选型

调研Vue已经很久了,随着Vue2正式发布,使用Vue来做项目又燃起了希望,不是为了一时的技术理想和情怀(了解我的人都知道,我不是这样的人),主要是出于下面几方面考虑:

  1. 用artTemplate+Sass+JS做的components方案已经做了很久了,沉淀了很多组件,随着Node服务开始上线,一直想在此基础上做同构,而公司Node框架Yog2的view层选择偏向于Smarty模板的Swig,修改比较麻烦
  2. 既然改不了,那么要换不如直接选择新的components方案,这次最强烈需求是:组件化和支持SSR,而Vue2之后支持SSR
  3. 这次两个项目同时进行,而且仅仅给两周的开发时间,组件化有效提高工作效率,可以把通用的组件抽象出来,多个页面之间业务组件复用率也很高,而且业务组件在后续的运营活动也可以直接复用
  4. 手百产品形态复杂,页面即在手百内使用又有手百外使用,手百内页面被多个Webview隔开,不适合SPA形式,而手百外适合SPA形式,所以一套代码需要适配两种情况,Vue 可以适应这两种方式
  5. Vue的SPA形式可以方便进行PWA和Hybrid改造(继续关注本公众号Hybrid系列)

所以,最后决定:上Vue!技术栈:Vue2+Yog2

再介绍下两个项目:

  • 项目A是老项目进行重构,产品需求要跟功能全部保留,架构跑通使用的是Vue2.1,所以A项目代码相对复杂,一直没有使用Vue2.2
  • 项目B是新项目,开始使用Vue2.1,上线后发现已经有Vue2.2,于是升级Vue2.2,并且把项目目录结构调整一番,Webpack config等都可配

Vue SSR入门到上线

先看下Vue SSR的实现流程图:

简单解释一下:

  1. app.js是Server和Client公用的
  2. webpack会根据server-entry.js和client-entry.js打包出来两个文件:server-bundle和client-bundle
  3. server-bundle用于后端渲染(2.1是js文件,2.2变成json,引入更加方便)

但是这张图没有说明在调用API接口方面,前后端是怎么公用代码的。前端走的是Ajax请求,后端走的是http请求(百度内部是RAL接口服务管理),结合上图补充完整的代码执行流程图如下:

Hybrid APP开发:模板本地化

上次文章介绍了Hybrid开发中常用到的Web和NA的通信方案:JSBridge,通过比较之后,最终推荐安卓和iOS通用的scheme协议,可以保证APP内和外都可以使用,今天开始正式介绍整个Hybrid架构内容。就像本系列「开篇语」提到的,这里的Hybrid是「狭义的Hybrid」,而不是所有的NA套个webapp就是Hybrid。

好的架构不是随波逐流,应该经得起考研

Hybrid技术体系是一套很多技术组成的完整知识架构。拿手百Hybrid方案,包括规范/约定、开发、联调、服务支撑等整个开发流程,而随着PWA之类「新」技术的产生又在考研技术架构面向未来的设计能力,所以手百Hybrid还在不断完善,注意是不断完善,不是推到重来!手百Hybrid技术整体架构如下:


接下来系列会围绕本架构图中重要的部分依次展开,全面的介绍Hybrid开发知识,本文介绍模板本地化开发方案。

所谓「模板本地化」,就是将Web页面内置到APP内,随版发布上线,然后通过云端接口实现更新,再直白一点:将H5网站的页面预先放到客户端发版,然后云端更新新版本。

这个方案好处在于:

  • 本地化模板提高页面打开速度,减少用户等待时间
  • 模板可更新,版本控制更方便可控,收敛快
  • Web页面和NA内置代码实现一套代码,减少开发成本
  • 上手成本小,开发就是实际开发Web(H5)页面,通过构建工具,生成Web页面和NA模板包不同的代码
  • 标准H5代码,迁移成本小,通过Node和构建工具,可以做到H5版本前后同构,将来还可以不改代码的前提下适配PWA

本文提到的JSBridge调起协议是统一使用hybrid://开头,只是提供思路和介绍整体Hybrid模板包架构和用到的技术,不涉及到具体代码,但是文章保证干货和诚意都满满😄。

模板本地化实现方案

本文会介绍两种模板本地化方案,分别是:依赖客户端拦截器(proxy)的方案一;完全无域名限制的方案二,两个方案的实现流程如下:

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函数来处理