• Posts tagged "Javascript"

Blog Archives

Angular2新的体验

AngularJS体验式编程系列文章,将介绍如何用angularjs构建一个强大的web前端系统。angularjs是由Google团队开发的一款非常优秀web前端框架。在当前如此多的web框架下,angularjs能脱颖而出,从架构设计上就高人一等,双向数据绑定,依赖注入,指令,MVC,模板。Angular.js创新地把后台技术融入前端开发,扫去jQuery一度的光芒。用angularjs就像写后台代码,更规范,更结构化,更可控。

从 Angular 2.x 开始新的体验。

关于作者

  • 张丹(Conan), 程序员R,Java,Nodejs
  • weibo:@Conan_Z
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/angular2-init/

前言

随着Angular2的升级,从1.x升级2.x,但我却迟迟没有动手。原因了2.x对1.x完全不兼容,而且从原生Javascript变成了Typescript,增加很多的生成模块,依赖的类库一下子变得很多。变化实在太大了,确实让人难以接受。我特别对于Typescript的不解,喜欢javascript的灵活,而静态化意味代码量的增加。

前几天,看到Angular2的release,想来想去还是要试试。从官方文档中,看到对中文支持的非常好,开发团队真是用心了。开发团队用心的改变,作为使用者来说是切身可以感受的,带着敬意和信心,决定把Angular2学起来。

目录

  1. Angular2介绍
  2. Anguar2的快速启动
  3. 从命令行构建Anguar2

1. Angular2介绍

Angular是由Google开发的一套前端应用开发框架,可以快速帮你构建复杂的大型前端单页应用(Single Page Application)。我非常喜欢Angular 1.x 给我带来的前端开发体验,当前花了2周时间时间学习,几百行代码就实现了,超乎想象的应用效果。我放下了所有原来对于前端Javascript的认知,直接转向了Angular。

官方网站:https://angular.cn/

Angular2的新特性包括:

  • 渐进式Web应用:借助现代化Web平台的力量,交付app式体验。高性能、离线化、零安装。
  • 原生:借助来自Ionic、NativeScript和React Native中的技术与思想,构建原生移动应用。
  • 桌面:借助你已经在Web开发中学过的能力,结合访问原生操作系统API的能力,创造能在桌面环境下安装的应用,横跨Mac、Windows和Linux平台。
  • 代码生成:Angular会把你的模板转换成代码,针对现代JavaScript虚拟机进行高度优化,轻松获得框架提供的高生产率,同时又能保留所有手写代码的优点。
  • 统一:在服务端渲染应用的首屏,像只有HTML和CSS的页面那样几乎瞬间展现,支持node.js、.NET、PHP,以及其它服务器,为通过SEO来优化站点铺平了道路。
  • 代码拆分:Angular应用通过新的组件路由(Component Router)模块实现快速加载,提供了自动拆分代码的功能,为用户单独加载它们请求的视图中需要的那部分代码。
  • 模板:通过简单而强大的模板语法,快速创建UI视图。
  • Angular命令行工具:命令行工具:快速进入构建环节、添加组件和测试,然后立即部署。
  • 各种IDE:在常用IDE和编辑器中获得智能代码补全、实时错误反馈及其它反馈等特性。
  • 测试:使用Karma进行单元测试,让你在每次存盘时都能立即知道是否弄坏了什么。Protractor则让你的场景测试运行得又快又稳定。
  • 动画:通过Angular中直观简便的API创建高性能复杂编排和动画时间线 —— 只要非常少的代码。
  • 可访问性:通过支持ARIA的组件、开发者指南和内置的一体化测试基础设施,创建具有完备可访问性的应用。

接下来,就让我们来体验Angular2的新颖之处的。

2. Anguar2的快速启动

阅读官方文档,我们可以跟着文档进行项目的快速启动,快速启动项目quickstart,是基于github,node,npm的,我们按照文档执行就可以了。关于Node的介绍,可以参考系列文章从零开始nodejs系列文章

系统环境:

  • Win10 64bit
  • node v6.9.4
  • npm 3.10.10
  • git version 2.7.0.windows.1

2.1 构建项目

进入开发目录,下载quickstart项目。


~ cd D:\\workspace\js
~ git clone https://github.com/angular/quickstart.git quickstart
~ cd quickstart
~ npm install   # 执行npm安装和配置
~ npm start     # 启动项目

通过npm start命令,就直接启动了angular2的程序,默认会自动打开浏览器,就可以看到效果了。如果浏览没有自动打开,自己手动在浏览器打开localhost:3000,也可以打开。

2.2 目录结构

接下来,我们查看项目目录结构。

由于这个quickstart的模板项目,包含了太多的文件。一下子看到这么多东西,肯定要晕一会的。图中用红色部分标记的几个文件,是我们应该要知道的,其他的文件对于首次执行来说并不是太重要。

文件说明

  • app/app.components.ts, 一个自定义组件,Typescript代码。
  • app/app.module.ts, 模块文件,用于组合管理组件,Typescript代码。
  • app/main.ts, 项目启动的入口,加载ts文件,Typescript代码。
  • app/app.components.spec.ts, 单测试文件
  • .gitignore, git的配置文件
  • index.html, 单页应用的html入口文件
  • package.json, Node项目的工程配置文件
  • tsconfig.json, Typescript语言的配置文件

2.3 4个核心文件

把几个核心的文件中的代码,进行解释。

app/app.components.ts文件


import { Component } from '@angular/core';        //引用系统类库

@Component({                                      //定义组件
  selector: 'my-app',                             //对应该index.html的<my-app>标签
  template: `<h1>Hello {{name}}</h1>`,            //替换<my-app>标签的内容
})
export class AppComponent  { name = 'Angular'; }  //输出自定义组件 

app/app.module.ts文件


import { NgModule }      from '@angular/core';                  //引用系统类库
import { BrowserModule } from '@angular/platform-browser';      //引用系统类库
import { AppComponent }  from './app.component';                //引用app.components.ts文件,定义的AppComponent组件
 
@NgModule({                                                     //定义模块
  imports:      [ BrowserModule ],                              //引用浏览器模块
  declarations: [ AppComponent ],                               //声明组件
  bootstrap:    [ AppComponent ]                                //启动组件
})
export class AppModule { }                                      //输出自定义模块 

app/main.ts文件


import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';  //引用系统类库

import { AppModule } from './app.module';                                    //引用app.module.ts文件的AppModule 

platformBrowserDynamic().bootstrapModule(AppModule);                         //启动程序,加载模块

index.html文件


<!DOCTYPE html>
<html>
  <head>
    <title>Angular QuickStart</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">

    <!-- Polyfill(s) for older browsers -->
    <script src="node_modules/core-js/client/shim.min.js"></script>

    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
  </head>

  <body>
    <my-app>Loading AppComponent content here ...</my-app>
  </body>
</html>

index.html中$lt;my-app&gtl的标签,是会被app.components.ts文件定义的selector发现并处理。

这4个文件,其实就是程序源代码的核心文件。那么项目中的其他文件,都是各种工具的配置文件。比如,我要编译Typescript文件到Javascript文件,就是需要在tsconfig.json进行配置;对类库做动态加载就要用到systemjs;执行单元测试需要用到karma;上传到github需要用到.gitignore;对node项目的管理用package.json配置。

2.4 扩展工具

当然,还有一些扩展工具。

单独编译Typescrip到Javascript。


~ D:\workspace\js\quickstart>npm run tsc

单独编译Typescrip,并监控文件状态,发现文件变化重新变异。


~ D:\workspace\js\quickstart>npm run tsc:w

启动Server,在浏览器中查看效果。


~ D:\workspace\js\quickstart>npm run lite 

单元测试


~ D:\workspace\js\quickstart>npm test

E2E测试End-to-end


~ D:\workspace\js\quickstart>npm run e2e 

除了从github上面下载quickstart模板,还是另外一种初始构建项目的方式,就是用Anguar2的命令行工具。

3. 从命令行构建Anguar2

从上文的Angular2的特性中,就有一项是命令工具,用这种方式构建一个新项目,更符合开发人员“脚手架”的思路。首先,我们需要安装这个命令工具anguar-cli。请大家务必升级Nodejs到v6.x以上,我之前用v4.4,花了大量时间调试都没有通过。

3.1 安装anguar-cli


~ D:\workspace\js>npm install -g angular-cli

安装好后,我们可以通过help命令查看命令行有哪个功能。


D:\workspace\js>ng help
ember build 
  Builds your app and places it into the output path (dist/ by default).
  aliases: b
  --target (String) (Default: development)
    aliases: -t , -dev (--target=development), -prod (--target=production)
  --environment (String) (Default: )
    aliases: -e 
  --output-path (Path) (Default: null)
    aliases: -o 
  --watch (Boolean) (Default: false)
    aliases: -w
  --watcher (String)
  --suppress-sizes (Boolean) (Default: false)
  --base-href (String) (Default: null)
    aliases: -bh 
  --aot (Boolean) (Default: false)
  --sourcemap (Boolean) (Default: true)
    aliases: -sm
  --vendor-chunk (Boolean) (Default: true)
  --verbose (Boolean) (Default: false)
  --progress (Boolean) (Default: true)
  --i18n-file (String) (Default: null)
  --i18n-format (String) (Default: null)
  --locale (String) (Default: null)

  // 省略....

查看ng工具的版本


~ D:\workspace\js>ng version
                             _                           _  _
  __ _  _ __    __ _  _   _ | |  __ _  _ __         ___ | |(_)
 / _` || '_ \  / _` || | | || | / _` || '__|_____  / __|| || |
| (_| || | | || (_| || |_| || || (_| || |  |_____|| (__ | || |
 \__,_||_| |_| \__, | \__,_||_| \__,_||_|          \___||_||_|
               |___/

angular-cli: 1.0.0-beta.26
node: 6.9.4
os: win32 x64

当然,我们最需要的就是构建项目,启动服务,生成组件,指令,服务等功能。

3.2 新建项目

新建一个项目


~ D:\workspace\js>ng new conan1      // 新建项目
installing ng2
  create .editorconfig
  create README.md
  create src\app\app.component.css
  create src\app\app.component.html
  create src\app\app.component.spec.ts
  create src\app\app.component.ts
  create src\app\app.module.ts
  create src\assets\.gitkeep
  create src\environments\environment.prod.ts
  create src\environments\environment.ts
  create src\favicon.ico
  create src\index.html
  create src\main.ts
  create src\polyfills.ts
  create src\styles.css
  create src\test.ts
  create src\tsconfig.json
  create angular-cli.json
  create e2e\app.e2e-spec.ts
  create e2e\app.po.ts
  create e2e\tsconfig.json
  create .gitignore
  create karma.conf.js
  create package.json
  create protractor.conf.js
  create tslint.json
Successfully initialized git.
Installing packages for tooling via npm.
Installed packages for tooling via npm.
Project 'conan1' successfully created.

等了大概有10分钟,下载了有200mb的文件,希望大家有耐心。进入安装目录。


~ D:\workspace\js\conan1>cd conan1      // 进入项目目录
~ D:\workspace\js\conan1>ng serve       // 启动项目

打开浏览器,查看Web网页。

如果你想修改启动端口为4201,可以用下面的命令。


ng serve --host 0.0.0.0 --port 4201 --live-reload-port 49153

用命令工具构建的项目目录。

目录结构有一个变化,就是嵌套了一层src目录,然后才是app目录。另外,多了angular-cli.json的配置文件。

3.3 模块生成器

接下来,我们可以用命令工具,进行项目模块和组件的开发了。

命令 使用说明
组件Component ng generate component my-new-component
指令Directive ng generate directive my-new-directive
服务Service ng generate pipe my-new-service
管道Pipe ng generate pipe my-new-pipe
类Class ng generate class my-new-class
接口Interface ng generate interface my-new-interface
枚举对象Enum ng generate enum my-new-enum
模块Module ng generate module my-module

3.4 生成一个新组件

用命令生成一个hello的新组件,文件生成如下。


~ D:\workspace\js\conan1>ng generate component hello
installing component
  create src\app\hello\hello.component.css
  create src\app\hello\hello.component.html
  create src\app\hello\hello.component.spec.ts
  create src\app\hello\hello.component.ts
  update src\app\app.module.ts

在app目录下生成了一个hello的子目录,包括了4个组件的文件,同时在app.module.ts模块文件中,注册了这个新组件。

我们要修改2个地方,让这个新生成的hello组件可以运行。

1. 修改index.html文件,增加<app-hello>标签。


<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Conan1</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading...</app-root>
  
  <app-hello>Loading...</app-hello>
</body>
</html>

2. 修改app.module.ts文件,在@NgModule.bootstrap中,增加HelloComponent的启动项。


import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { HelloComponent } from './hello/hello.component';

@NgModule({
  declarations: [
    AppComponent,
    HelloComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent,HelloComponent]
})
export class AppModule { }

查看浏览器,我们可以看到hello works!显示到界面上。

3.5 生成一个新指令

属性型指令用于改变一个 DOM 元素的外观或行为。用命令生成一个link的新指令,操作如下。


~ D:\workspace\js\conan1>ng generate directive link
installing directive
  create src\app\link.directive.spec.ts
  create src\app\link.directive.ts
  update src\app\app.module.ts

主要生成了新文件link.directive.ts。


import { Directive } from '@angular/core';
@Directive({
  selector: '[appLink]'
})
export class LinkDirective {
  constructor() { }
}

接下来,用appLink指令来做一个样式的修饰,需要修改2个文件。

1. 修改hello.component.html文件,增加一段span的HTML标签,并配置appLink属性。


<p>hello works!</p>

<span appLink>Directive me!</span>

2. 修改link.directive.ts文件,在LinkDirective类的构造器constructor函数中,增加样式的修改属性。同时,通过@HostListener,监听鼠标动作。


import { Directive, ElementRef, HostListener, Renderer } from '@angular/core';
@Directive({
  selector: '[appLink]'
})
export class LinkDirective {
    constructor(el: ElementRef, renderer: Renderer) {
       renderer.setElementStyle(el.nativeElement, 'backgroundColor', 'yellow');
    }

    @HostListener('mouseenter') onMouseEnter() {
      this.highlight('blue');
    }
    @HostListener('mouseleave') onMouseLeave() {
      this.highlight('yellow');
    }
    
    private highlight(color: string) {
      this.renderer.setElementStyle(this.el.nativeElement, 'backgroundColor', color);
    }
}

修改后,我们在浏览器中查看运行的效果。

3.6 生成一个新服务

服务,可以封装代码,比如我们可以对数据访问代码单独隔离,封装到一个独立的服务中。用命令生成一个meta的新服务,操作如下。


~ D:\workspace\js\conan1>ng generate service meta
installing service
  create src\app\meta.service.spec.ts
  create src\app\meta.service.ts
  WARNING Service is generated but not provided, it must be provided to be used

生成了2个service的文件,同时提供了Service必须被provided,才可以被应用。我们如果想要启用meta的这个服务,需要在3个文件中进行修改。

1. 修改meta.service.ts文件,增加getNames()函数,用于提供数据访问。


import { Injectable } from '@angular/core';
@Injectable()
export class MetaService {
  constructor() { }
  getNames():any[]{
    return [
        {id: 1, name: 'AAA'},
        {id: 2, name: 'BBB'},
        {id: 3, name: 'CCC'},
        {id: 4, name: 'DDD'}
      ];
  }
}

2. 修改hello.component.ts文件,把MetaService注入到HelloComponent中,通过providers进行依赖注入的体现,同时调用MetaService 类的getNames()函数获取数据。


import { Component, OnInit } from '@angular/core';
import { MetaService } from '../meta.service';
@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css'],
  providers: [MetaService]
})
export class HelloComponent implements OnInit {
  meta = [];
  constructor(private metaService: MetaService) { }
  ngOnInit() {
    this.meta = this.metaService.getNames();
  }
}

3. 修改hello.component.html文件,用于数据的html输出,以列表形式输出。


<p>hello works!</p>

<span appLink>Directive me!</span>

<ul>
  <li *ngFor="let m of meta">
    {{m.id}}, {{m.name}}
  </li>
</ul>

修改后,我们在浏览器中查看运行的效果。

3.7 生成一个新管道

管道,可以用于数据的连续处理操作,比如可以把日期在界面端进行格式化,把20170101格式转型2017-01-01格式。用命令生成一个datef的新管道,操作如下。


~ D:\workspace\js\conan1>ng generate pipe datef
installing pipe
  create src\app\datef.pipe.spec.ts
  create src\app\datef.pipe.ts
  update src\app\app.module.ts

生成了2个pipe的文件。如果我们想使用这个功能,需要在2个文件中进行修改。

1. 修改datef.pipe.ts文件,对日期进行格式化,把20161222转化为2016-12-22。


import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
  name: 'datef'
})
export class DatefPipe implements PipeTransform {
  transform(value: string) {
    var dat1 = value.toString();
    return dat1.substring(0,4)+ "-"+ parseInt(dat1.substring(4,6)) +  "-"+dat1.substring(6,8);
  }
}

2. 修改hello.component.html文件,进行HTML的输出。


<p>hello works!</p>

<span appLink>Directive me!</span>

<ul>
  <li *ngFor="let m of meta">
    {{m.id}}, {{m.name}}
  </li>
</ul>

<p>{{day}} ==> {{ day | datef}}</p>

修改后,我们在浏览器中查看运行的效果。

其他的命令,我们在后文再继续说明。本文只是一篇对Angular2的新体验介绍,由于Angular2整个框架还是变化挺大的,同时我对Typescript也不熟悉,对webapck也需要花时间学习。整个过程花了不少时间,也走了不少的弯路。

目前,网上Angular2框架介绍的文章本来就不多,虽然官网文档比较详细,但是上手来说,还是有一定的难度的,希望本文对于新手来说有一些帮助,也希望Angualr2尽快成熟,我准备正在项目中用起来!!

转载请注明出处:
http://blog.fens.me/angularjs-yeoman-project/

打赏作者

图片延迟加载库Layzr

从零开始nodejs系列文章,将介绍如何利Javascript做为服务端脚本,通过Nodejs框架web开发。Nodejs框架是基于V8的引擎,是目前速度最快的Javascript引擎。chrome浏览器就基于V8,同时打开20-30个网页都很流畅。Nodejs标准的web开发框架Express,可以帮助我们迅速建立web站点,比起PHP的开发效率更高,而且学习曲线更低。非常适合小型网站,个性化网站,我们自己的Geek网站!!

关于作者

  • 张丹(Conan), 创业者,程序员(Java,R,Javascript/Nodejs)
  • weibo:@Conan_Z
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/nodejs-layzr/

js-layzr

前言

延迟加载技术,普遍应用于现在大部分的互联网的Web应用上。通过对图像的延迟加载,等到用户浏览区域接近时才把图片进行加载,可以有效地提升网页打开的速度,进一步改进用户的体验。

Layzr.js库就可以完成这个功能,150多行代码,很小很快很方便。

目录

  1. layzr介绍
  2. layzr的基本使用
  3. Layzr的配置

1. layzr介绍

layzr.js 是一个很小、速度快、无依赖的,用于浏览器图片延迟加载的库。

我们找到Layzr.js官方的Github上面,dist目录发布的 layzr.min.js 仅有 2.2 KB。同时,发现 package.json 文件,没有任何的dependencies依赖。

用layzr.js进行图片延迟加载,是非常方便的。通过配置选项,实现最大化的加载速度。layzr.js对于滚动事件已去抖,以尽量减少对浏览器的压力,以确保最佳的渲染。

项目官方网站:http://callmecavs.github.io/layzr.js/

2. layzr的基本使用

layzr.js是在浏览器端运行的Javascript库,但是他是用于NPM管理的,还在没有bower中发布。关于NPM和Bower的介绍,请分别查看文章:快速创建基于npm的nodejs库bower解决js的依赖管理

下面我们用npm的方式,下载layzr库。

首先,创建项目目录。


~  D:\workspace\javascript> mkdir js-layzr && cd js-layzr 

新建NPM项目文件package.json。


~ D:\workspace\javascript\js-layzr> vi package.json
{
    "name": "js-layzr",
    "version": "0.0.1",
    "description": "a demo for layzr",
    "dependencies": {}
}

通过NPM下载layzr.js包


~ D:\workspace\javascript\js-layzr> npm install layzr.js --save
npm WARN package.json js-layzr@0.0.1 No repository field.
npm WARN package.json js-layzr@0.0.1 No README data
layzr.js@1.1.4 node_modules\layzr.js

layzr1

接下来,我们打开layzr.js项目提供的Demo的文件,在layzr.js/demo.html。


<!DOCTYPE html>

<!--[if IE 9]>         <html class="ie9" lang="en-US"> <![endif]-->
<!--[if gt IE 9]><!--> <html lang="en-US"> <!--<![endif]-->
  <head>
    <title></title>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <style>
      .demo-images {
        width: 300px;
        margin-right: auto;
        margin-left: auto;
        padding-top: 50vh;
      }

      .demo-image,
      iframe {
        display: block;
        margin-bottom: 100px;
      }
    </style>
  </head>
  <body>
    <div class="wrapper">

      <div class="demo-images">
        <img class="demo-image" src="http://placehold.it/300">
        <img class="demo-image" data-layzr="http://placehold.it/300">
        <img class="demo-image" data-layzr="http://placehold.it/300" data-layzr-retina="http://placehold.it/300&text=retina" data-layzr-bg>
        <img class="demo-image" data-layzr="http://placehold.it/300" data-layzr-retina="http://placehold.it/300&text=retina">
        <img class="demo-image" data-layzr="http://placehold.it/300" data-layzr-retina="http://placehold.it/300&text=retina">
        <img class="demo-image" data-layzr="http://placehold.it/300" data-layzr-retina="http://placehold.it/300&text=retina">
        <iframe width="300" height="169" data-layzr="https://www.youtube.com/embed/2a01Rg2g2Z8"></iframe>
      </div>

    </div><!-- END wrapper -->

    <script src="dist/layzr.js"></script>
    <script>
      // create new instance, defaults shown below
      var layzr = new Layzr({
        selector: '[data-layzr]',
        attr: 'data-layzr',
        retinaAttr: 'data-layzr-retina',
        bgAttr: 'data-layzr-bg',
        threshold: 0,
        callback: null
      });
    </script>
  </body>
</html>

在浏览型预览效果,如下图所示。

layzr2

左边为浏览器页面,右边为Chrome的开发者工具的Elements界面,红色直线为显示和代码的对应关系。当页面向下滚动时候,图片会一张一张的加载。同时代码中,原代码为


<img class="demo-image" data-layzr="http://placehold.it/300" data-layzr-retina="http://placehold.it/300&text=retina">

的部分,会被重新翻译为


<img class="demo-image" src="http://placehold.it/300">

从本Demo中,我们可以猜出layzr库应该有2个组成部分,监听页面事件 和 更新img标签的DOM元素。

3. Layzr的配置选择

在HTML中,图片是用img标签在控制的,要用到Layzr.js库,需要在img标签中增加属性。


<img src="optional/placeholder" data-layzr="normal/image" data-layzr-retina="optional/retina/image" data-layzr-bg >
  • src: 用于定义图像占位符,如果没有定义图像占位符,那么在图像载入前,可能会显示异常。
  • data-layzr: 用于显示的图像
  • data-layzr-retina: 用于图像的延迟加载,测试发现没效果。
  • data-layzr-bg: 图像用于做成背景,测试发现直接被隐藏了。

对应地,我们还需要定义Javascript的实例,用来启动layzr的事件。


var layzr = new Layzr({ 
  selector: '[data-layzr]', 
  attr: 'data-layzr', 
  retinaAttr: 'data-layzr-retina', 
  bgAttr: 'data-layzr-bg', 
  threshold: 0, 
  callback: null 
});

Javascript实例属性:

  • selector: 用于选定图像标签。
  • attr: 用于指定data-layzr的属性
  • retinaAttr: 用于指定data-layzr-retina属性
  • bgAttr: 用于指定data-layzr-bg的属性
  • threshold: 用于定义图像加载参数,通过屏幕高度来控制。
  • callback: 当加载完成,触发事件回调。

接下来,我们新建一个layzr.html的文件,用于测试 layzr 的库各种功能,会比官方提供的Demo更易于理解。

新建文件layzr.html。


<!DOCTYPE html>
<html>
<head>
    <title>Layzr Demo</title>
    <script src="node_modules/layzr.js/dist/layzr.js"></script>
    <style>
        .row {
            margin: 50px 0;
            height: 500px;
        }
        .border {
            padding: 10px;
            border-style: solid;
            border-width: 3px;
            border-color: blue;
        }
    </style>
</head>
<body>
<div class="wrapper">
        <div class="row">
            <img src="./bg.png" data-layzr="http://blog.fens.me/wp-content/uploads/2015/02/marked.png">
        </div>
        <div class="row">
            <img src="./bg.png" data-layzr="http://blog.fens.me/wp-content/uploads/2015/03/buffer.png">
        </div>
        <div class="row">
            <img src="./bg.png" data-layzr="http://blog.fens.me/wp-content/uploads/2015/02/express4.png">
        </div>
        <div class="row">
            <img src="./bg.png" data-layzr="http://blog.fens.me/wp-content/uploads/2015/03/child_process.png">
        </div>
        <div class="row">
            <img src="./bg.png" data-layzr="http://blog.fens.me/wp-content/uploads/2015/03/crypto.png">
        </div>
        <div class="row">
            <img src="./bg.png" data-layzr="http://blog.fens.me/wp-content/uploads/2014/02/architect.png">
        </div>
        <div class="row">
            <img src="./bg.png" data-layzr-retina="./bg2.png" data-layzr="http://blog.fens.me/wp-content/uploads/2013/08/seo-title.png">
        </div>
</div>

<script>
    var layzr = new Layzr({
        selector: '[data-layzr]',
        attr: 'data-layzr',
        retinaAttr: 'data-layzr-retina',
        threshold: 10,
        callback: function () {
            console.log(this);
            this.classList.add('border');
        }
    });
</script>

</body>
</html>

用浏览器找开页面,我们可以看到展示效果。

layzr3

当浏览器向下滑动的过程中,图片由原来黑色的占位图,会自动替换为我们需要显示的图片。

layzr4

本文测试的代码,已上传到github:https://github.com/bsspirit/js-layzr

这样,我们就实现图像的延迟加载技术。对于有大量图片的网站来说,尝试一下图像的延迟加载技术,肯定会给网站带来大幅度的性能改进的。

转载请注明出处:
http://blog.fens.me/nodejs-layzr/

打赏作者

Node.js进程通信模块child_process

从零开始nodejs系列文章,将介绍如何利Javascript做为服务端脚本,通过Nodejs框架web开发。Nodejs框架是基于V8的引擎,是目前速度最快的Javascript引擎。chrome浏览器就基于V8,同时打开20-30个网页都很流畅。Nodejs标准的web开发框架Express,可以帮助我们迅速建立web站点,比起PHP的开发效率更高,而且学习曲线更低。非常适合小型网站,个性化网站,我们自己的Geek网站!!

关于作者

  • 张丹(Conan), 创业者,程序员(Java,R,Javascript/Nodejs)
  • weibo:@Conan_Z
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/nodejs-child-process/

child_process

前言

Node.js是一种单线程的编程模型,对Node.js的赞美和诟病的也都是因为它的单线程模型,所有的任务都在一个线程中完成(I/O等例外)。单线程模型,不仅让代码非常简洁,更是直接避免了线程调度的复杂性;同样也是因为单线程,让CPU密集型计算应用,完全不适用。

在Node.js的内核中,给了我们一种新的选择,通过child_process模块创建新进程,从而实现多核并行计算。

目录

  1. child_process介绍
  2. child_process的基本使用:spawn, exec, execFile, fork

1. child_process介绍

child_process是Node.js的一个十分重要的模块,通过它可以实现创建多进程,以利用单机的多核计算资源。虽然,Nodejs天生是单线程单进程的,但是有了child_process模块,可以在程序中直接创建子进程,并使用主进程和子进程之间实现通信,等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果。

本文仅从使用上对child_process模块进行介绍,对于深入的内容请参考官方文档,https://nodejs.org/api/child_process.html

关于Node.js线程不错的文章:https://cnodejs.org/topic/518b679763e9f8a5424406e9

2. child_process的基本使用

child_process模块,在v0.12.0版本主要包括4个异步进程函数(spawn,exec,execFile,fork)和3个同步进程函数(spawnSync,execFileSync,execSync)。以异步函数中spawn是最基本的创建子进程的函数,其他三个异步函数都是对spawn不同程度的封装。spawn只能运行指定的程序,参数需要在列表中给出,而exec可以直接运行复杂的命令。

比如要运行 du -sh /disk1 命令, 使用spawn函数需要写成spawn(‘du ‘, [‘-sh ‘, ‘/disk1’]),而使用exec函数时,可以直接写成exec(‘du -sh /disk1’)。exec是会先进行Shell语法解析,因此用exec函数可以更方便的使用复杂的Shell命令,包括管道、重定向等。下面我们就针对每个异步函数,进行测试一下。

系统环境

  • Linux Ubuntu 14.04.1 LTS 64bit
  • Nodejs:v0.13.0-pre
  • Npm:1.4.28

创建项目


~ cd /disk1/demo
~ mkdir nodejs-childprocess && cd nodejs-childprocess

2.1 spawn函数

spawn从定义来看,有3个参数。

child_process.spawn(command[, args][, options])
  • command: 只执行的命令
  • args: 参数列表,可输入多的参数
  • options: 环境变量对象

其中环境变量对象包括7个属性:

  • cwd: 子进程的当前工作目录
  • env: 环境变量键值对
  • stdio: 子进程 stdio 配置
  • customFds: 作为子进程 stdio 使用的文件标示符
  • detached: 进程组的主控制
  • uid: 用户进程的ID.
  • gid: 进程组的ID.

首先,我们运行一下,上文提到的du的命令。直接在命令行,运行的结果。


~ du -sh /disk1
582M    /disk1

新建文件spawn.js


~ vi spawn.js

var child = require('child_process');
var du = child.spawn('du', ['-sh', '/disk1']);
du.stdout.on('data', function (data) {
    console.log('stdout: ' + data);
});
du.stderr.on('data', function (data) {
    console.log('stderr: ' + data);
});
du.on('exit', function (code) {
    console.log('child process exited with code ' + code);
});

通过node运行的结果


~ node spawn.js

stdout: 582M    /disk1
child process exited with code 0

输出的结果是一样的,这样我们就可以很方便地以异步的方式调用系统命令了,spawn是不支持callback函数的,通过stream的方式发数据传给主进程,从而实现了多进程之间的数据交换。

这个功能的直接用应用场景就是“系统监控”。在Linux下,我们有很多命令行工具,可以实时监控CPU,内存,IO,网络等数据,那么用Node的child_process模块可以很容易地把这些数据采集的我们自己的应用中。

比如,我们用mpstat命令,监控用户CPU的使用情况。先看看mpstat命令直接使用的效果。


~ mpstat 1
Linux 3.13.0-32-generic (ape3)  03/20/2015      _x86_64_        (4 CPU)
11:45:56 AM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
11:45:57 AM  all   96.50    0.00    3.50    0.00    0.00    0.00    0.00    0.00    0.00    0.00
11:45:58 AM  all   96.50    0.00    3.25    0.00    0.25    0.00    0.00    0.00    0.00    0.00
11:45:59 AM  all   96.50    0.00    3.50    0.00    0.00    0.00    0.00    0.00    0.00    0.00
11:46:00 AM  all   96.50    0.00    3.50    0.00    0.00    0.00    0.00    0.00    0.00    0.00
11:46:01 AM  all   96.26    0.00    3.24    0.00    0.25    0.00    0.25    0.00    0.00    0.00
11:46:02 AM  all   96.75    0.00    3.25    0.00    0.00    0.00    0.00    0.00    0.00    0.00
11:46:03 AM  all   96.51    0.00    3.24    0.00    0.25    0.00    0.00    0.00    0.00    0.00
^C
Average:     all   96.50    0.00    3.35    0.00    0.11    0.00    0.04    0.00    0.00    0.00

我们新建文件mpstat.js,读取mpstat命令的数据,然后只输出%usr的数据。


~ vi mpstat.js

var child = require('child_process');
var mpstat = child.spawn('mpstat', ['1']);

//Linux 3.13.0-32-generic (ape3)  03/20/2015      _x86_64_        (4 CPU)
//11:27:12 AM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
//11:27:13 AM  all   96.50    0.00    3.50    0.00    0.00    0.00    0.00    0.00    0.00    0.00

var line = 0;
var cols = ["time","day","CPU","%usr","%nice","%sys","%iowait","%irq","%soft","%steal","%guest","%gnice","%idle"];
mpstat.stdout.on('data', function (data) {
    var str = data.toString();
    if(line > 2) {
        var arr = str.split(/\s+/);
        console.log(arr[0]+" "+cols[3]+" "+arr[3]);
    }else{
       line++;
    }
});
mpstat.stderr.on('data', function (data) {
    console.log('stderr: ' + data);
});
mpstat.on('exit', function (code) {
    console.log('child process exited with code ' + code);
});

运行程序:


~ node mpstat.js
11:47:57 %usr 96.75
11:47:58 %usr 96.52
11:47:59 %usr 96.75
11:48:00 %usr 96.25
11:48:01 %usr 96.74
11:48:02 %usr 96.51
11:48:03 %usr 96.74
11:48:04 %usr 96.51

这样就完成系统数据的采集,我们可以把采集到的数据存储到数据库中,通过websocket等协议直接输出的浏览器前端进行数据的展示。监控系统的例子,可以参考我之前写过的一篇文章:websocket服务器监控

2.2 exec函数

exec函数是对spawn的一种友好封装,增加Shell命令解析,可以直接嵌入复杂的命令,比如,管道用法 cat spawn.js exec.js | wc。

我先试用wc命令来统计一下当前目录的文件字数,分别对应3列为 字节数、字数、行数。


~ wc *
   9   29  275 exec.js
  25   95  878 mpstat.js
  16   41  343 spawn.js
  50  165 1496 total

~ cat *.js | wc
     50     165    1496

接下来,我们使用exec函数,来使用Linux管道命令。


~ vi exec.js

var childProcess = require('child_process');

var ls = childProcess.exec('cat *.js | wc', function (error, stdout, stderr) {
   if (error) {
     console.log(error.stack);
     console.log('Error code: '+error.code);
   }
   console.log('Child Process STDOUT: '+stdout);
});

运行程序:


~ node exec.js
Child Process STDOUT:      50     165    1496

输出结果与Linux命令类似,而且用exec时,命令可以写成一个完整的字符串了,不用像spawn函数时分开写成多个参数数组的形式。最后通过一个callback函数来返回,更符合JavaScript的函数调用习惯,通常情况我们可以用exec来替换spawn的使用。

2.3 execFile函数

execFile函数会直接执行特定的程序,参数作为数组传入,不会被bash解释,因此具有较高的安全性。execFile与spawn的参数相似,也需要分别指定执行的命令和参数,但可以接受一个回调函数,与exec的回调函数相同。

我们看一个execFile函数的例子。


~ vi execFile.js

var childProcess = require('child_process');
var path = ".";
childProcess.execFile('/bin/ls', ['-l', path], function (err, result) {
    console.log(result)
});

运行程序


~ node execFile.js
total 16
-rw-r--r-- 1 root root 527 Mar 20 13:23 execFile.js
-rw-r--r-- 1 root root 275 Mar 20 13:11 exec.js
-rw-r--r-- 1 root root 878 Mar 20 11:53 mpstat.js
-rw-r--r-- 1 root root 343 Mar 20 11:11 spawn.js

那么,什么时候使用exec,什么时候使用execFile呢?

如果命令参数是由用户来输入的,对于exec函数来说是有安全性风险的,因为Shell会运行多行命令,比如’ls -l .;pwd,如逗号分隔,之后的命令也会被系统运行。但使用exeFile命令时,命令和参数分来,防止了参数注入的安全风险。

我们用程序测试一下。


~ vi execFile.js

// exec
var cmd = 'ls -l .;pwd'
var ls = childProcess.exec(cmd, function (error, stdout, stderr) {
   if (error) {
     console.log(error.stack);
     console.log('Error code: '+error.code);
   }
   console.log('Child Process STDOUT: '+stdout);
});

// execFile
var path = ".;pwd";
childProcess.execFile('/bin/ls', ['-l', path], function (err, result) {
    console.log(result)
});

运行程序


~  node execFile.js

{ [Error: Command failed: /bin/ls -l .;pwd
/bin/ls: cannot access .;pwd: No such file or directory
] killed: false, code: 2, signal: null, cmd: '/bin/ls -l .;pwd' }

Child Process STDOUT: total 16
-rw-r--r-- 1 root root 547 Mar 20 13:31 execFile.js
-rw-r--r-- 1 root root 275 Mar 20 13:11 exec.js
-rw-r--r-- 1 root root 878 Mar 20 11:53 mpstat.js
-rw-r--r-- 1 root root 343 Mar 20 11:11 spawn.js
/disk1/demo/nodejs-childprocess

从输出结果看到,exec函数被正常执行,而execFile函数,则提示参数错误。

2.4 fork

fork函数,用于在子进程中运行的模块,如 fork(‘./son.js’) 相当于 spawn(‘node’, [‘./son.js’]) 。与spawn方法不同的是,fork会在父进程与子进程之间,建立一个通信管道,用于进程之间的通信。

我们一个主进程和子进程通信的例子,主进程文件main.js和子进程文件son.js。

新建主进程文件。


~ main.js

var childProcess = require('child_process');
var n = childProcess.fork('./son.js');

n.on('message', function(m) {
  console.log('Main Listen: ', m);
});
n.send({ hello: 'son' });

新建子进程文件。


~ vi son.js

process.on('message', function(m) {
  console.log('Son Listen:', m);
});
process.send({ Hello: 'conan' });

运行程序:


~ node main.js
Main Listen:  { Hello: 'conan' }
Son Listen: { hello: 'son' }

通过main.js启动子进程son.js,通过process在两个进程之间传递数据。我们对系统进程进行检查,看看是否确实有两个进程。


~ ps -aux|grep node
root     22777  0.2  0.1 603240 13252 pts/3    Sl+  14:25   0:00 node main.js
root     22782  0.2  0.1 603540 13516 pts/3    Sl+  14:25   0:00 /usr/local/bin/node ./son.js

有2个node进程分别是node main.js和node ./son.js。

掌握了进程之间的通信,我们可以做的事情就比较多了,比如自己做一个Node.js多进程管理器,调度器之类。相对于Java的多线程管理或者进程调度来说,Node程序是如此简单明了的。我已经非常明显的感觉到了编程语言在进步!!

转载请注明出处:
http://blog.fens.me/nodejs-child-process/

打赏作者

Node.js缓冲模块Buffer

从零开始nodejs系列文章,将介绍如何利Javascript做为服务端脚本,通过Nodejs框架web开发。Nodejs框架是基于V8的引擎,是目前速度最快的Javascript引擎。chrome浏览器就基于V8,同时打开20-30个网页都很流畅。Nodejs标准的web开发框架Express,可以帮助我们迅速建立web站点,比起PHP的开发效率更高,而且学习曲线更低。非常适合小型网站,个性化网站,我们自己的Geek网站!!

关于作者

  • 张丹(Conan), 创业者,程序员(Java,R,Javascript/Nodejs)
  • weibo:@Conan_Z
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/nodejs-buffer/

buffer

前言

Javascript是为浏览器而设计的,能很好的处理unicode编码的字符串,但对于二进制或非unicode编码的数据就显得无能为力。Node.js继承Javascript的语言特性,同时又扩展了Javascript语言,为二进制的数据处理提供了Buffer类,让Node.js可以像其他程序语言一样,能处理各种类型的数据了。

网上有很多讲Buffer的文章,大都讲的是原理,怎么使用几乎找不到,文章将重点介绍Buffer的使用。

目录

  1. Buffer介绍
  2. Buffer的基本使用
  3. Buffer的性能测试

1. Buffer介绍

在Node.js中,Buffer类是随Node内核一起发布的核心库。Buffer库为Node.js带来了一种存储原始数据的方法,可以让Nodejs处理二进制数据,每当需要在Nodejs中处理I/O操作中移动的数据时,就有可能使用Buffer库。原始数据存储在 Buffer 类的实例中。一个 Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。

Buffer 和 Javascript 字符串对象之间的转换需要显式地调用编码方法来完成。以下是几种不同的字符串编码:

  • ‘ascii’ – 仅用于 7 位 ASCII 字符。这种编码方法非常快,并且会丢弃高位数据。
  • ‘utf8’ – 多字节编码的 Unicode 字符。许多网页和其他文件格式使用 UTF-8。
  • ‘ucs2’ – 两个字节,以小尾字节序(little-endian)编码的 Unicode 字符。它只能对 BMP(基本多文种平面,U+0000 – U+FFFF) 范围内的字符编码。
  • ‘base64’ – Base64 字符串编码。
  • ‘binary’ – 一种将原始二进制数据转换成字符串的编码方式,仅使用每个字符的前 8 位。这种编码方法已经过时,应当尽可能地使用 Buffer 对象。Node 的后续版本将会删除这种编码。

Buffer官方文档:http://nodejs.org/api/buffer.html

2. Buffer的基本使用

Buffer的基本使用,主要就是API所提供的操作,主要包括3个部分 创建Buffer类、读Buffer、写Buffer。由于基本操作在官方文档中详细的使用介绍,我只是简单列举一下。

系统环境

  • Win7 64bit
  • Nodejs:v0.10.31
  • Npm:1.4.23

创建项目


~ cd D:\workspace\javascript>
~ D:\workspace\javascript>mkdir nodejs-buffer && cd nodejs-buffer

2.1 创建Buffer类

要创建一个Buffer的实例,我们要通过new Buffer来创建。新建文件buffer_new.js。


~ vi buffer_new.js

// 长度为0的Buffer实例
var a = new Buffer(0);
console.log(a);
> <Buffer >

// 长度为0的Buffer实例相同,a1,a2是一个实例
var a2 = new Buffer(0);
console.log(a2);
> <Buffer >

// 长度为10的Buffer实例
var a10 = new Buffer(10);
console.log(a10);
> <Buffer 22 37 02 00 00 00 00 04 00 00>

// 数组
var b = new Buffer(['a','b',12])
console.log(b);
> <Buffer 00 00 0c>

// 字符编码
var b2 = new Buffer('你好','utf-8');
console.log(b2);
> <Buffer e4 bd a0 e5 a5 bd>

Buffer类有5个类方法,用于Buffer类的辅助操作。

1) 编码检查,上文中提到Buffer和Javascript字符串转换时,需要显式的设置编码,那么这几种编码类型是Buffer所支持的。像中文处理只能使用utf-8编码,对于几年前常用的gbk,gb2312等编码是无法解析的。


// 支持的编码
console.log(Buffer.isEncoding('utf-8'))
console.log(Buffer.isEncoding('binary'))
console.log(Buffer.isEncoding('ascii'))
console.log(Buffer.isEncoding('ucs2'))
console.log(Buffer.isEncoding('base64'))
console.log(Buffer.isEncoding('hex'))  # 16制进
> true

//不支持的编码
console.log(Buffer.isEncoding('gbk'))
console.log(Buffer.isEncoding('gb2312'))
> false

2) Buffer检查,很多时候我们需要判断数据的类型,对应后续的操作。


// 是Buffer类
console.log(Buffer.isBuffer(new Buffer('a')))
> true

// 不是Buffer
console.log(Buffer.isBuffer('adfd'))
console.log(Buffer.isBuffer('\u00bd\u00bd'))
> false

3) 字符串的字节长度,由于字符串编码不同,所以字符串长度和字节长度有时是不一样的。比如,1个中文字符是3个字节,通过utf-8编码输出就是4个中文字符,占12个字节。


var str2 = '粉丝日志';
console.log(str2 + ": " + str2.length + " characters, " + Buffer.byteLength(str2, 'utf8') + " bytes");
> 粉丝日志: 4 characters, 12 bytes
console.log(str2 + ": " + str2.length + " characters, " + Buffer.byteLength(str2, 'ascii') + " bytes");
> 粉丝日志: 4 characters, 4 bytes

4) Buffer的连接,用于连接Buffer的数组。我们可以手动分配Buffer对象合并后的Buffer空间大小,如果Buffer空间不够了,则数据会被截断。


var b1 = new Buffer("abcd");
var b2 = new Buffer("1234");
var b3 = Buffer.concat([b1,b2],8);
console.log(b3.toString());
> abcd1234

var b4 = Buffer.concat([b1,b2],32);
console.log(b4.toString());
console.log(b4.toString('hex'));//16进制输出
> abcd1234 乱码....
> 616263643132333404000000000000000000000000000000082a330200000000

var b5 = Buffer.concat([b1,b2],4);
console.log(b5.toString());
> abcd

程序运行的截图
buffer_concat

5) Buffer的比较,用于Buffer的内容排序,按字符串的顺序。


var a1 = new Buffer('10');
var a2 = new Buffer('50');
var a3 = new Buffer('123');

// a1小于a2
console.log(Buffer.compare(a1,a2));
> -1

// a2小于a3
console.log(Buffer.compare(a2,a3));
> 1

// a1,a2,a3排序输出
console.log([a1,a2,a3].sort(Buffer.compare));
> [ <Buffer 31 30>, <Buffer 31 32 33>, <Buffer 35 30> ]

// a1,a2,a3排序输出,以utf-8的编码输出
console.log([a1,a2,a3].sort(Buffer.compare).toString());
> 10,123,50

2.2 写入Buffer

把数据写入到Buffer的操作,新建文件buffer_write.js。


~ vi buffer_write.js

//////////////////////////////
// Buffer写入
//////////////////////////////

// 创建空间大小为64字节的Buffer
var buf = new Buffer(64);

// 从开始写入Buffer,偏移0
var len1 = buf.write('从开始写入');

// 打印数据的长度,打印Buffer的0到len1位置的数据
console.log(len1 + " bytes: " + buf.toString('utf8', 0, len1));

// 重新写入Buffer,偏移0,将覆盖之前的Buffer内存
len1 = buf.write('重新写入');
console.log(len1 + " bytes: " + buf.toString('utf8', 0, len1));

// 继续写入Buffer,偏移len1,写入unicode的字符串
var len2 = buf.write('\u00bd + \u00bc = \u00be',len1);
console.log(len2 + " bytes: " + buf.toString('utf8', 0, len1+len2));

// 继续写入Buffer,偏移30
var len3 = buf.write('从第30位写入', 30);
console.log(len3 + " bytes: " + buf.toString('utf8', 0, 30+len3));

// Buffer总长度和数据
console.log(buf.length + " bytes: " + buf.toString('utf8', 0, buf.length));

// 继续写入Buffer,偏移30+len3
var len4 = buf.write('写入的数据长度超过Buffer的总长度!',30+len3);

// 超过Buffer空间的数据,没有被写入到Buffer中
console.log(buf.length + " bytes: " + buf.toString('utf8', 0, buf.length));

buffer_write

Node.js的节点的缓冲区,根据读写整数的范围,提供了不同宽度的支持,使从1到8个字节(8位、16位、32位)的整数、浮点数(float)、双精度浮点数(double)可以被访问,分别对应不同的writeXXX()函数,使用方法与buf.write()类似。


buf.write(string[, offset][, length][, encoding])
buf.writeUIntLE(value, offset, byteLength[, noAssert])
buf.writeUIntBE(value, offset, byteLength[, noAssert])
buf.writeIntLE(value, offset, byteLength[, noAssert])
buf.writeIntBE(value, offset, byteLength[, noAssert])
buf.writeUInt8(value, offset[, noAssert])
buf.writeUInt16LE(value, offset[, noAssert])
buf.writeUInt16BE(value, offset[, noAssert])
buf.writeUInt32LE(value, offset[, noAssert])
buf.writeUInt32BE(value, offset[, noAssert])
buf.writeInt8(value, offset[, noAssert])
buf.writeInt16LE(value, offset[, noAssert])
buf.writeInt16BE(value, offset[, noAssert])
buf.writeInt32LE(value, offset[, noAssert])
buf.writeInt32BE(value, offset[, noAssert])
buf.writeFloatLE(value, offset[, noAssert])
buf.writeFloatBE(value, offset[, noAssert])
buf.writeDoubleLE(value, offset[, noAssert])
buf.writeDoubleBE(value, offset[, noAssert])

另外,关于Buffer写入操作,还有一些Buffer类的原型函数可以操作。

Buffer复制函数 buf.copy(targetBuffer[, targetStart][, sourceStart][, sourceEnd])。


// 新建两个Buffer实例
var buf1 = new Buffer(26);
var buf2 = new Buffer(26);

// 分别向2个实例中写入数据
for (var i = 0 ; i < 26 ; i++) {
    buf1[i] = i + 97; // 97是ASCII的a
    buf2[i] = 50; // 50是ASCII的2
}

// 把buf1的内存复制给buf2
buf1.copy(buf2, 5, 0, 10); // 从buf2的第5个字节位置开始插入,复制buf1的从0-10字节的数据到buf2中
console.log(buf2.toString('ascii', 0, 25)); // 输入buf2的0-25字节
> 22222abcdefghij2222222222

Buffer填充函数 buf.fill(value[, offset][, end])。


// 新建Buffer实例,长度20节节
var buf = new Buffer(20);

// 向Buffer中填充数据
buf.fill("h");
console.log(buf)
> <Buffer 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68>
console.log("buf:"+buf.toString())
> buf:hhhhhhhhhhhhhhhhhhhh
// 清空Buffer中的数据
buf.fill();
console.log("buf:"+buf.toString())
> buf:

Buffer裁剪,buf.slice([start][, end])。返回一个新的缓冲区,它和旧缓冲区指向同一块内存,但是从索引 start 到 end 的位置剪裁。


var buf1 = new Buffer(26);
for (var i = 0 ; i < 26 ; i++) {
    buf1[i] = i + 97;
}

// 从剪切buf1中的0-3的位置的字节,新生成的buf2是buf1的一个切片。
var buf2 = buf1.slice(0, 3);
console.log(buf2.toString('ascii', 0, buf2.length));
> abc

// 当修改buf1时,buf2同时也会发生改变
buf1[0] = 33;
console.log(buf2.toString('ascii', 0, buf2.length));
> !bc

2.3 读取Buffer

我们把数据写入Buffer后,我们还需要把数据从Buffer中读出来,新建文件buffer_read.js。我们可以通过readXXX()函数获得对应该写入时编码的索引值,再转换原始值取出,有这种方法操作中文字符就会变得麻烦,最常用的读取Buffer的方法,其实就是toString()。


~ vi buffer_read.js

//////////////////////////////
// Buffer 读取
//////////////////////////////

var buf = new Buffer(10);
for (var i = 0 ; i < 10 ; i++) {
    buf[i] = i + 97;
}
console.log(buf.length + " bytes: " + buf.toString('utf-8'));
> 10 bytes: abcdefghij

// 读取数据
for (ii = 0; ii < buf.length; ii++) {
    var ch = buf.readUInt8(ii); // 获得ASCII索引
    console.log(ch + ":"+ String.fromCharCode(ch));
}
> 97:a
98:b
99:c
100:d
101:e
102:f
103:g
104:h
105:i
106:j

写入中文数据,以readXXX进行读取,会3个字节来表示一个中文字。


var buf = new Buffer(10);
buf.write('abcd')
buf.write('数据',4)
for (var i = 0; i < buf.length; i++) {
    console.log(buf.readUInt8(i));
}

>97
98
99
100
230  // 230,149,176 代表“数”
149
176
230  // 230,141,174 代表“据”
141
174 

如果想输出正确的中文,那么我们可以用toString(‘utf-8’)的函数来操作。


console.log("buffer :"+buf); // 默认调用了toString()的函数
> buffer :abcd数据
console.log("utf-8  :"+buf.toString('utf-8'));
> utf-8  :abcd数据
console.log("ascii  :"+buf.toString('ascii'));//有乱码,中文不能被正确解析
> ascii  :abcdf0f
.
console.log("hex    :"+buf.toString('hex')); //16进制
> hex    :61626364e695b0e68dae

对于Buffer的输出,我们用的最多的操作就是toString(),按照存入的编码进行读取。除了toString()函数,还可以用toJSON()直接Buffer解析成JSON对象。


var buf = new Buffer('test');
console.log(buf.toJSON());
> { type: 'Buffer', data: [ 116, 101, 115, 116 ] }

3. Buffer的性能测试

通过上文中对Buffer的介绍,我们已经了解了Buffer的基本使用,接下来,我们要开始做Buffer做一些测试。

3.1 8K的创建测试

每次我们创建一个新的Buffer实例时,都会检查当前Buffer的内存池是否已经满,当前内存池对于新建的Buffer实例是共享的,内存池的大小为8K。

如果新创建的Buffer实例大于8K时,就把Buffer交给SlowBuffer实例存储;如果新创建的Buffer实例小于8K,同时小于当前内存池的剩余空间,那么这个Buffer存入当前的内存池;如果Buffer实例不大0,则统一返回默认的zerobuffer实例。

下面我们创建2个Buffer实例,第一个是以4k为空间,第二个以4.001k为空间,循环创建10万次。


var num = 100*1000;
console.time("test1");
for(var i=0;i<num;i++){
    new Buffer(1024*4);
}
console.timeEnd("test1");
> test1: 132ms

console.time("test2");
for(var j=0;j<num;j++){
    new Buffer(1024*4+1);
}
console.timeEnd("test2");
> test2: 163ms

第二个以4.001k为空间的耗时多23%,这就意味着第二个,每二次循环就要重新申请一次内存池的空间。这是需要我们非常注意的。

3.2 多Buffer还是单一Buffer

当我们需要对数据进行缓存时,创建多个小的Buffer实例好,还是创建一个大的Buffer实例好?比如我们要创建1万个长度在1-2048之间不等的字符串。


var max = 2048;     //最大长度
var time = 10*1000; //循环1万次

// 根据长度创建字符串
function getString(size){
    var ret = ""
    for(var i=0;i<size;i++) ret += "a";
    return ret;
}

// 生成字符串数组,1万条记录
var arr1=[];
for(var i=0;i<time;i++){
    var size = Math.ceil(Math.random()*max)
    arr1.push(getString(size));
}
//console.log(arr1);

// 创建1万个小Buffer实例
console.time('test3');
var arr_3 = [];
for(var i=0;i<time;i++){
    arr_3.push(new Buffer(arr1[i]));
}
console.timeEnd('test3');
> test3: 217ms

// 创建一个大实例,和一个offset数组用于读取数据。
console.time('test4');
var buf = new Buffer(time*max);
var offset=0;
var arr_4=[];
for(var i=0;i<time;i++){
    arr_4[i]=offset;
    buf.write(arr1[i],offset,arr1[i].length);
    offset=offset+arr1[i].length;
}
console.timeEnd('test4');
> test4: 12ms

读取索引为2的数据。


console.log("src:[2]="+arr1[2]);
console.log("test3:[2]="+arr_3[2].toString());
console.log("test4:[2]="+buf.toString('utf-8',arr_4[2],arr_4[3]));

运行结果如图所示。
buffer_test

对于这类的需求来说,提前生成一个大的Buffer实例进行存储,要比每次生成小的Buffer实例高效的多,能提升一个数量级的计算效率。所以,理解并用好Buffer是非常重要的!!

3.3 string VS Buffer

有了Buffer我们是否需求把所有String的连接,都换成Buffer的连接?那么我们就需要测试一下,String和Buffer做字符串连接时,哪个更快一点?

下面我们进行字符串连接,循环30万次。


//测试三,Buffer VS string
var time = 300*1000;
var txt = "aaa"

var str = "";
console.time('test5')
for(var i=0;i<time;i++){
    str += txt;
}
console.timeEnd('test5')
> test5: 24ms

console.time('test6')
var buf = new Buffer(time * txt.length)
var offset = 0;
for(var i=0;i<time;i++){
    var end = offset + txt.length;
    buf.write(txt,offset,end);
    offset=end;
}
console.timeEnd('test6')
> test6: 85ms

从测试结果,我们可以明显的看到,String对字符串的连接操作,要远快于Buffer的连接操作。所以我们在保存字符串的时候,该用string还是要用string。那么只有在保存非utf-8的字符串以及二进制数据的情况,我们才用Buffer。

6. 程序代码

本文的程序代码,可以直接从Github上面下载本文项目中的源代码,按照片文章中的介绍学习buffer,下载地址:https://github.com/bsspirit/nodejs-buffer

也可以直接用github命令行来下载:


~ git clone git@github.com:bsspirit/nodejs-buffer.git   # 下载github项目
~ cd nodejs-buffer                                      # 进入下载目录

关于Node.js的底层,本人接触并不多,未能从V8(C++)的做更深入的研究,仅仅在使用层次上,写出我的总结。对于文中的错误或描述不清楚的地方,还请大牛予以指正!!

参考文章:

转载请注明出处:
http://blog.fens.me/nodejs-buffer/

打赏作者

Node.js加密算法库Crypto

从零开始nodejs系列文章,将介绍如何利Javascript做为服务端脚本,通过Nodejs框架web开发。Nodejs框架是基于V8的引擎,是目前速度最快的Javascript引擎。chrome浏览器就基于V8,同时打开20-30个网页都很流畅。Nodejs标准的web开发框架Express,可以帮助我们迅速建立web站点,比起PHP的开发效率更高,而且学习曲线更低。非常适合小型网站,个性化网站,我们自己的Geek网站!!

关于作者

  • 张丹(Conan), 创业者,程序员(Java,R,Javascript)
  • weibo:@Conan_Z
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/nodejs-crypto/

crypto

前言

密码技术是互联网应用的一项最基本的技术之一,主要保证了数据的安全。安全定义是多维度的,通过不可逆的hash算法可以保证登陆密码的安全;通过非对称的加密算法,可以保证数据存储的安全性;通过数字签名,可以验证数据在传输过程中是否被篡改。

我们要做互联网应用,数据安全性一个是不容忽视的问题。不然可能会造成,如CSDN的100万用户明文密码被泄露事情;携程网,100万用户个人信息泄露事情等。

Node.js的Crypto库就提供各种加密算法,可以非常方便地让我们使用密码技术,解决应用开发中的问题。

目录

  1. Crypto介绍
  2. Hash算法
  3. Hmac算法
  4. 加密和解密算法
  5. 签名和验证算法
  6. salt算法
  7. 程序代码

1. Crypto介绍

Crypto库是随Nodejs内核一起打包发布的,主要提供了加密、解密、签名、验证等功能。Crypto利用OpenSSL库来实现它的加密技术,它提供OpenSSL中的一系列哈希方法,包括hmac、cipher、decipher、签名和验证等方法的封装。

Crypto官方文档:http://nodejs.org/api/crypto.html

2. Hash算法

哈希算法,是指将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。如果散列一段明文而且哪怕只更改该段落的一个字母,随后的哈希都将产生不同的值。要找到散列为同一个值的两个不同的输入,在计算上是不可能的,所以数据的哈希值可以检验数据的完整性。一般用于快速查找和加密算法。

通常我们对 登陆密码,都是使用Hash算法进行加密,典型的哈希算法包括 ‘md5′,’sha’,’sha1′,’sha256′,’sha512′,’RSA-SHA’。下面我们就做一下算法的测试。

系统环境

  • Win7 64bit
  • Nodejs:v0.10.31
  • Npm:1.4.23

创建项目


~ cd D:\workspace\javascript>
~ D:\workspace\javascript>mkdir nodejs-crypto && cd nodejs-crypto

新建文件hash.js,打印出所支持的hash算法。


~ vi hash.js

var crypto = require('crypto');  # 加载crypto库
console.log(crypto.getHashes()); # 打印支持的hash算法

运行程序


~ node hash.js

[ 'DSA',
  'DSA-SHA',
  'DSA-SHA1',
  'DSA-SHA1-old',
  'RSA-MD4',
  'RSA-MD5',
  'RSA-MDC2',
  'RSA-RIPEMD160',
  'RSA-SHA',
  'RSA-SHA1',
  'RSA-SHA1-2',
  'RSA-SHA224',
  'RSA-SHA256',
  'RSA-SHA384',
  'RSA-SHA512',
  'dsaEncryption',
  'dsaWithSHA',
  'dsaWithSHA1',
  'dss1',
  'ecdsa-with-SHA1',
  'md4',
  'md4WithRSAEncryption',
  'md5',
  'md5WithRSAEncryption',
  'mdc2',
  'mdc2WithRSA',
  'ripemd',
  'ripemd160',
  'ripemd160WithRSA',
  'rmd160',
  'sha',
  'sha1',
  'sha1WithRSAEncryption',
  'sha224',
  'sha224WithRSAEncryption',
  'sha256',
  'sha256WithRSAEncryption',
  'sha384',
  'sha384WithRSAEncryption',
  'sha512',
  'sha512WithRSAEncryption',
  'shaWithRSAEncryption',
  'ssl2-md5',
  'ssl3-md5',
  'ssl3-sha1',
  'whirlpool' ]

我们看到支持Hash真是不少,究竟怎么选择适合,我也说不清楚。以我对算法的理解,我会以加密计算时间和编码长度,做选择的依据。下面就简单地比较一下几种常见算法。

编辑hash.js文件


~ vi hash.js

///////////////////////////
// Hash算法
///////////////////////////
var crypto = require('crypto')
    ,fs = require('fs');

function hashAlgorithm(algorithm){
    var s1 = new Date();

    var filename = "package.json";
    var txt = fs.ReadStream(filename);

    var shasum = crypto.createHash(algorithm);
    txt.on('data', function(d) {
        shasum.update(d);
    });

    txt.on('end', function() {
        var d = shasum.digest('hex');
        var s2 = new Date();

        console.log(algorithm+','+(s2-s1) +'ms,'+ d);
    });
}

function doHash(hashs){
    hashs.forEach(function(name){
        hashAlgorithm(name);
    })
}

//var algs = crypto.getHashes();
var algs = [ 'md5','sha','sha1','sha256','sha512','RSA-SHA','RSA-SHA1','RSA-SHA256','RSA-SHA512'];
doHash(algs);

运行程序


~ node hash.js

md5,6ms,85cd416f811574bd4bdb61b241266670
sha,18ms,b1fc6647fa4fdb4b1b394f8dc7856ac140e2fbdb
sha1,20ms,0777e65066dca985569fa892fa88e21b45dc656d
sha256,21ms,5e4aea76f93ee87f422fcbd9458edad0e33ddf256d5d93bcc47977e33cb1654c
sha512,23ms,94249ec2f83b006354774dd8f8ec81125ea9e1e00f94393d8b20bbd7678e63db53fab6af125e139f9257fd7dbb6c69474e443d059903a9cb2dded03a94c8143
RSA-SHA,24ms,b1fc6647fa4fdb4b1b394f8dc7856ac140e2fbdb
RSA-SHA1,25ms,0777e65066dca985569fa892fa88e21b45dc656d
RSA-SHA256,26ms,5e4aea76f93ee87f422fcbd9458edad0e33ddf256d5d93bcc47977e33cb1654c
RSA-SHA512,26ms,94249ec2f83b006354774dd8f8ec81125ea9e1e00f94393d8b20bbd7678e63db53fab6af125e139f9257fd7dbb6c69474e4433d059903a9cb2dded03a94c8143

输出以逗号分隔,分别是算法名、时间、密文。最常见的md5,密文长度最短,计算时间也最少;sha和sha1比较接近;sha512密文最长,计算时间也最长。

由于md5已经有了大量的字典库,对于安全级别一般的网站用sha1吧;如果安全级别要求很高,CPU配置也很牛,可以考虑用sha512。

3. Hmac算法

HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code),HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。HMAC可以有效防止一些类似md5的彩虹表等攻击,比如一些常见的密码直接MD5存入数据库的,可能被反向破解。

定义HMAC需要一个加密用散列函数(表示为H,可以是MD5或者SHA-1)和一个密钥K。我们用B来表示数据块的字节数。(以上所提到的散列函数的分割数据块字长B=64),用L来表示散列函数的输出数据字节数(MD5中L=16,SHA-1中L=20)。鉴别密钥的长度可以是小于等于数据块字长的任何正整数值。应用程序中使用的密钥长度若是比B大,则首先用使用散列函数H作用于它,然后用H输出的L长度字符串作为在HMAC中实际使用的密钥。一般情况下,推荐的最小密钥K长度是L个字节。

由于HMAC就是使用散列函数,所以我们同样选择上面的几种算法进行测试。新建文件hmac.js。


~ vi hmac.js

///////////////////////////
// Hmac算法
///////////////////////////

var crypto = require('crypto')
    ,fs = require('fs');

function hmacAlgorithm(algorithm,key){
    var s1 = new Date();

    var filename = "package.json";
    var txt = fs.ReadStream(filename);

    var shasum = crypto.createHmac(algorithm,key);
    txt.on('data', function(d) {
        shasum.update(d);
    });

    txt.on('end', function() {
        var d = shasum.digest('hex');
        var s2 = new Date();

        console.log(algorithm+','+(s2-s1) +'ms,'+ d);
    });
}

function doHmac(hashs,key){
    console.log("\nKey : %s", key);
    console.log("============================");
    hashs.forEach(function(name){
        hmacAlgorithm(name,key);
    })
}

//var algs = crypto.getHashes();
var algs = [ 'md5','sha','sha1','sha256','sha512','RSA-SHA','RSA-SHA1','RSA-SHA256','RSA-SHA512'];

// 短KEY的测试
setTimeout(function(){
    doHmac(algs,"abc");
},1)

// 长KEY的测试
setTimeout(function(){
    var key = "jifdkd;adkfaj^&fjdifefdafda,ijjifdkd;adkfaj^&fjdifefdafdaljifdkd;adkfaj^&fjdifefdafda";
    doHmac(algs,key);
},2*1000)

运行程序


~ node hmac.js
Key : abc
============================
md5,6ms,bf106a077abcfa0fffe6ec0da039545b
sha,6ms,a43a00981346ac64bb7b6fb0641b72a101fb04a5
sha1,6ms,aead69a72da77d0615a854dda1086d885807574a
sha256,7ms,98ac955cb2205ba01a6337951d0ed3fd9b68753544cf81275eced365da57fc5d
sha512,8ms,054f37e34b55a19e64a7f88fb60b1122dc0a30e9864ca28d01d61115b13c74de292ab66e85bf007e1a463a52d7c30fdff174618ef954401bc9c2c3318e762c8f
RSA-SHA,10ms,a43a00981346ac64bb7b6fb0641b72a101fb04a5
RSA-SHA1,11ms,aead69a72da77d0615a854dda1086d885807574a
RSA-SHA256,12ms,98ac955cb2205ba01a6337951d0ed3fd9b68753544cf81275eced365da57fc5d
RSA-SHA512,13ms,054f37e34b55a19e64a7f88fb60b1122dc0a30e9864ca28d01d61115b13c74de292ab66e85bf007e1a463a52d7c30fdff174618ef954401bc9c2c3318e762c8f

Key : jifdkd;adkfaj^&fjdifefdafda,ijjifdkd;adkfaj^&fjdifefdafdaljifdkd;adkfaj^&fjdifefdafda
============================
md5,5ms,164a8fee6e37bb3e40a9d5dff5c2fd66
sha,5ms,ba06f536856553c3756aa36254a63ef35e225d38
sha1,7ms,f3a89b0a5ee8a55c2bb6a861748d43e9d44dc489
sha256,7ms,f2df911f40e74b2b9bb3d53a7ca4b78d438d511e015d4b50431eaea65339380d
sha512,8ms,5b4b57386b1fcc4f1945c47788bf38c013e1cde356fc15e1f946e6bf6738b5dc52ecf17b3ddc80b2ff21f985a1a707df9357fe305e9aa143da073d2cafd794dc
RSA-SHA,11ms,ba06f536856553c3756aa36254a63ef35e225d38
RSA-SHA1,12ms,f3a89b0a5ee8a55c2bb6a861748d43e9d44dc489
RSA-SHA256,14ms,f2df911f40e74b2b9bb3d53a7ca4b78d438d511e015d4b50431eaea65339380d
RSA-SHA512,16ms,5b4b57386b1fcc4f1945c47788bf38c013e1cde356fc15e1f946e6bf6738b5dc52ecf17b3ddc80b2ff21f985a1a707df9357fe305e9aa143da073d2cafd794dc

通过比对短key和长key,在编码比较长的算法上面会有一些影响。由于Hmac有了第二参数key,所以会比单独的hash加密登陆密码,有更好的安全性上的保证。

对于网站登陆密码的设计,我们可以做成2个字段存储,用password字段存密文,passkey字段存储key,把算法直接封装到程序里面。


{
  username: 'xxxx'
  password: 'aead69a72da77d0615a854dda1086d885807574a',
  passkey:'abc'
}

就算数据库被攻击,黑客也只是拿了密文和key,密码明文并没有被泄露。并且在不知道加密算法的情况下,也很难通过彩虹表进行攻击。

4. 加密和解密算法

对于登陆密码来说,是不需要考虑解密的,通常都会用不可逆的算法,像md5,sha-1等。但是,对于有安全性要求的数据来说,我们是需要加密存储,然后解密使用的,这时需要用到可逆的加密算法。对于这种基于KEY算法,可以分为对称加密和不对称加密。

  • 对称加密算法的原理很容易理解,通信一方用KEK加密明文,另一方收到之后用同样的KEY来解密就可以得到明文。
  • 不对称加密算法,使用两把完全不同但又是完全匹配的一对Key:公钥和私钥。在使用不对称加密算法加密文件时,只有使用匹配的一对公钥和私钥,才能完成对明文的加密和解密过程。

对于这种类型的操作,Crypto包也提供了大量的算法支持。新建文件cipher.js,打印出所支持的算法。


~ vi cipher.js

var crypto = require('crypto');
console.log(crypto.getCiphers());

运行程序


~ node cipher.js

[ 'CAST-cbc',
  'aes-128-cbc',
  'aes-128-cbc-hmac-sha1',
  'aes-128-cfb',
  'aes-128-cfb1',
  'aes-128-cfb8',
  'aes-128-ctr',
  'aes-128-ecb',
  'aes-128-gcm',
  'aes-128-ofb',
  'aes-128-xts',
  'aes-192-cbc',
  'aes-192-cfb',
  'aes-192-cfb1',
  'aes-192-cfb8',
  'aes-192-ctr',
  'aes-192-ecb',
  'aes-192-gcm',
  'aes-192-ofb',
  'aes-256-cbc',
  'aes-256-cbc-hmac-sha1',
  'aes-256-cfb',
  'aes-256-cfb1',
  'aes-256-cfb8',
  'aes-256-ctr',
  'aes-256-ecb',
  'aes-256-gcm',
  'aes-256-ofb',
  'aes-256-xts',
  'aes128',
  'aes192',
  'aes256',
  'bf',
  'bf-cbc',
  'bf-cfb',
  'bf-ecb',
  'bf-ofb',
  'blowfish',
  'camellia-128-cbc',
  'camellia-128-cfb',
  'camellia-128-cfb1',
  'camellia-128-cfb8',
  'camellia-128-ecb',
  'camellia-128-ofb',
  'camellia-192-cbc',
  'camellia-192-cfb',
  'camellia-192-cfb1',
  'camellia-192-cfb8',
  'camellia-192-ecb',
  'camellia-192-ofb',
  'camellia-256-cbc',
  'camellia-256-cfb',
  'camellia-256-cfb1',
  'camellia-256-cfb8',
  'camellia-256-ecb',
  'camellia-256-ofb',
  'camellia128',
  'camellia192',
  'camellia256',
  'cast',
  'cast-cbc',
  'cast5-cbc',
  'cast5-cfb',
  'cast5-ecb',
  'cast5-ofb',
  'des',
  'des-cbc',
  'des-cfb',
  'des-cfb1',
  'des-cfb8',
  'des-ecb',
  'des-ede',
  'des-ede-cbc',
  'des-ede-cfb',
  'des-ede-ofb',
  'des-ede3',
  'des-ede3-cbc',
  'des-ede3-cfb',
  'des-ede3-cfb1',
  'des-ede3-cfb8',
  'des-ede3-ofb',
  'des-ofb',
  'des3',
  'desx',
  'desx-cbc',
  'id-aes128-GCM',
  'id-aes192-GCM',
  'id-aes256-GCM',
  'idea',
  'idea-cbc',
  'idea-cfb',
  'idea-ecb',
  'idea-ofb',
  'rc2',
  'rc2-40-cbc',
  'rc2-64-cbc',
  'rc2-cbc',
  'rc2-cfb',
  'rc2-ecb',
  'rc2-ofb',
  'rc4',
  'rc4-40',
  'rc4-hmac-md5',
  'seed',
  'seed-cbc',
  'seed-cfb',
  'seed-ecb',
  'seed-ofb' ]

同样地,在这么一大堆的算法面前,完全不知道如何去选择。我还是以加密和解密的计算时间为参考指标,选出几个常见的算法进行测试。


///////////////////////////
// 加密解密算法
///////////////////////////
var crypto = require('crypto')
    ,fs = require('fs');

//加密
function cipher(algorithm, key, buf ,cb){
    var encrypted = "";
    var cip = crypto.createCipher(algorithm, key);
    encrypted += cip.update(buf, 'binary', 'hex');
    encrypted += cip.final('hex');
    cb(encrypted);
}

//解密
function decipher(algorithm, key, encrypted,cb){
    var decrypted = "";
    var decipher = crypto.createDecipher(algorithm, key);
    decrypted += decipher.update(encrypted, 'hex', 'binary');
    decrypted += decipher.final('binary');
    cb(decrypted);
}

function cipherDecipherFile(filename,algorithm, key){
    fs.readFile(filename, "utf-8",function (err, data) {
        if (err) throw err;
        var s1 = new Date();

        cipher(algorithm, key,data,function(encrypted){
            var s2 = new Date();
            console.log('cipher:'+algorithm+','+(s2-s1) +'ms');

            decipher(algorithm, key,encrypted,function(txt){
                var s3 = new Date();
                console.log('decipher:'+algorithm+','+(s3-s2) +'ms');
//                console.log(txt);
            });
        });
    });
}

//console.log(crypto.getCiphers());
var algs = ['blowfish','aes-256-cbc','cast','des','des3','idea','rc2','rc4','seed'];
var key = "abc";
var filename = "book.pdf";//"package.json";
algs.forEach(function(name){
    cipherDecipherFile(filename,name,key);
})

运行程序


~ node cipher.js

cipher:blowfish,46ms
decipher:blowfish,95ms
cipher:des,67ms
decipher:des,104ms
cipher:idea,54ms
decipher:idea,88ms
cipher:rc4,16ms
decipher:rc4,44ms
cipher:des3,158ms
decipher:des3,193ms
cipher:aes-256-cbc,19ms
decipher:aes-256-cbc,47ms
cipher:cast,46ms
decipher:cast,82ms
cipher:seed,64ms
decipher:seed,98ms
cipher:rc2,104ms
decipher:rc2,99ms

输出一共3列,第一列,cipher(加密),decipher(解密);第二列,是算法名称;第三列是计算时间。

在选中的这几个算法中,rc4和aes-256-cbc是表现不错的算法,加密和解密时间都比较短,加密时间:解密时间=1:3;其他的算法,总体时间相对较长,有的加密时间:解密时间=1:1。所以,怎么选算法,另外的一个标准就要看业务需求了。如果业务上,解密操作的次数远大于加密操作的次数,而且是在服务器计算,那么我们最好找到,加密时间:解密时间=N:1,N>1的算法;如果加密在服务器端,解密在客户端完成,那么aes-256-cbc算法的计算时间比例就非常适合了。

5. 签名和验证算法

我们除了对数据进行加密和解密,还需要判断数据在传输过程中,是否真实际和完整,是否被篡改了。那么就需要用到签名和验证的算法,利用不对称加密算法,通过私钥进行数字签名,公钥验证数据的真实性。

数字签名的制作和验证过程,如下图所示。

screenshot.62

下面我们用程序,来现实图中的操作流程,由于证书是我们自己制作的,不打算对外公开,没有到CA进行认证,所以下过程将不包括公钥伪造,再到CA认证的过程。

首先,我们要用openSSL命令先生成,私钥server.pem和公钥cert.pem。


# 生成私钥
~ D:\workspace\javascript\nodejs-crypto>openssl genrsa  -out server.pem 1024
Generating RSA private key, 1024 bit long modulus
..................++++++
..................++++++
e is 65537 (0x10001)

# 生成公钥
~ D:\workspace\javascript\nodejs-crypto>openssl req -key server.pem -new -x509 -out cert.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, YOUR name) []:
Email Address []:

接下来,我们利用生成私钥生成数学签名,然后再用公钥验证数据,是否被篡改。新建文件signer.js。


~ vi signer.js

///////////////////////////
// 签名验证算法
// openssl genrsa  -out server.pem 1024
// openssl req -key server.pem -new -x509 -out cert.pem
///////////////////////////

var crypto = require('crypto')
    ,fs = require('fs');

function signer(algorithm,key,data){
    var sign = crypto.createSign(algorithm);
    sign.update(data);
    sig = sign.sign(key, 'hex');
    return sig;
}

function verify(algorithm,pub,sig,data){
    var verify = crypto.createVerify(algorithm);
    verify.update(data);
    return verify.verify(pubkey, sig, 'hex')
}

var algorithm = 'RSA-SHA256';
var data = "abcdef";   //传输的数据
var privatePem = fs.readFileSync('server.pem');
var key = privatePem.toString();
var sig = signer(algorithm,key,data); //数字签名

var publicPem = fs.readFileSync('cert.pem');
var pubkey = publicPem.toString();
console.log(verify(algorithm,pubkey,sig,data));         //验证数据,通过公钥、数字签名 =》是原始数据
console.log(verify(algorithm,pubkey,sig,data+"2"));    //验证数据,通过公钥、数字签名  =》不是原始数据

运行程序


~ node signer.js
true
false

两行输出结果,第一行验证的结果是true,表示数据在传输过程中,没有被篡改;第二行验证的结果是false,表示数据在传输过程中被篡改,不是原始的数据。当然,如何保证私钥和公钥的匹配,需要CA第三方来认证,与Crypto库无关本文不再介绍。

6. salt算法

我们知道,如果直接对密码进行散列,那么黑客可以对通过获得这个密码散列值,然后通过查散列值字典(例如MD5密码破解网站),得到某用户的密码。

盐(Salt),在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。加盐后的散列值,可以极大的降低由于用户数据被盗而带来的密码泄漏风险,即使通过彩虹表寻找到了散列后的数值所对应的原始内容,但是由于经过了加盐,插入的字符串扰乱了真正的密码,使得获得真实密码的概率大大降低。

加盐的实现过程通常是在需要散列的字段的特定位置增加特定的字符,打乱原始的字串,使其生成的散列结果产生变化。比如,用户使用了一个密码:


123465

经过MD5散列后,可以得出结果:


3d9188577cc9bfe9291ac66b5cc872b7

但是由于用户密码位数不足,短密码的散列结果很容易被彩虹表破解,因此,在用户的密码末尾添加特定字串:


123465abcdefghijklmnopqrstuvwxyz

因此,加盐后的密码位数更长了,散列的结果也发生了变化:


27e20c64ccb8cce9ad68b8ccff6252cf

新建文件salt.js,实现上面的程序。


~ vi salt.js

//////////////////////////////
// salt算法
//////////////////////////////
var crypto = require('crypto');
var md5 = crypto.createHash('md5');
var txt = "123465";

md5.update(txt);
console.log(md5.digest('hex'));

md5 = crypto.createHash('md5');
var salt = "abcdefghijklmnopqrstuvwxyz";
md5.update(txt+salt);
console.log(md5.digest('hex'));

我们可以不用自己动手加盐,而使用crypto.pbkdf2()函数,默认会调用hmac算法,用sha1的散列函数,并且可以设置迭代次数和密文长度。具体的使用代码如下。


~ vi salt.js

var crypto = require('crypto');
var txt = "123465";
var salt = "abcdefghijklmnopqrstuvwxyz";

// 生成密文,默认HMAC函数是sha1算法
crypto.pbkdf2(txt, salt, 4096, 256, function (err,hash) {
    if (err) { throw err; }
    console.log(hash.toString('hex'));
})

运行程序,生成256位的密文。


~ node salt.js

29c7de002d942ebcbf3afe05f3eb0ff620f0515fe6b8f19176736273cd70805afdfcc828a6f227152dfa4d0c4f96da184fbd060d4d4c86a5deb8d704699c3e8653acdb0e5bc3e584e0890a44206bb2926f0289fc8e0abe49fd1876461fcc50f06dc7991c4b93cc4e80076529c73b2f2c56f16b5b319368edf017f3d3583a33aa44fd30f89801f0d8877eb8262925f5fdc40a5c57f1b275e5674784dca635c75bc58b6c22264e0f29e363eb25dedf1a242429084e3e17d344b59cab3b9723db03ee4838b632786d1a9eb968f2523404286e5d0a41a1707577650cc3cc2f1ab65714a4cb31f068e4aefa259c6be68174e0a475d5610168305a4935a14bb221a516

如果加盐每次都是固定的值也会不安全,我们还可以利用随机randomBytes()函数,配合pbkdf2()函数,让每次都是不同的salt,生成安全级别更高的密文。


~ vi salt.js

//通过伪随机码生成salt,进行加密
crypto.randomBytes(128, function (err, salt) {
    if (err) { throw err;}
    salt = salt.toString('hex');
    console.log(salt); //生成salt

    crypto.pbkdf2(txt, salt, 4096, 256, function (err,hash) {
        if (err) { throw err; }
        hash = hash.toString('hex');
        console.log(hash);//生成密文
    })
})

运行程序


~ node salt.js

# 随机生成的salt
78e59de99f16697e3eb684dcfa8efa086db0940c7cd47d33f9311e3bfcf9d58bf30915f54b3f72793b5c8568d32f1f15c55cc87affd043d96f1ed1f56c25a8054b3d83a306636f3b9e3bc9e48c3303aff54da006f92e370023165857fce0a1d1ff0b89178ae8c1416747275daba25652ea864d52a80427658ea69dbe500a7261

# 通过salt生成的密文
48943eb51ea702b436f95bf1dacc7f64bc41cf7cfa4cb40d101d5550c28caafc58ca720934352238430634f21fd5a6a4ef63fe5828c2665362e9902adc0305f93d2523fbd28521ad00947a74ff8229f63ad5796f2e12677cbed6af02b9973ee0187a69ad67e86790471d95f18d6d2c43ef904f7d17a5d8264f8236f227363a016ae2c14559c17236d540e06c5fd443af740721897f76bdbd9711c8499d7a34cae2e917f900fc364f72f9afaf301845c6e0b5c37def949b4af62336a39dbd1e829405d6189536092c7769a5d7e427b8e97419988da4e1bad49c69f25ac4e96f74a0ce3eab9e1433277568105b1dcc0cf9e1f9c91a7ed391c5825eefcd71ef5ca1

我们把salt和密文一起进行存储,保证用户密码的安全性。

7. 程序代码

本文的程序代码,可以直接从Github上面下载本文项目中的源代码,按照片文章中的介绍学习Crypto,下载地址:https://github.com/bsspirit/nodejs-crypto

也可以直接用github命令行来下载:


~ git clone git@github.com:bsspirit/nodejs-crypto.git   # 下载github项目
~ cd nodejs-crypto                                      # 进入下载目录

最后要声明一下:本人对密码技术理解不深,本文所涉及的密码技术仅仅是Crypto库在应用上的,文中观点不对或描述不清楚的地方,还请专家来指正!!

参考文章:

转载请注明出处:
http://blog.fens.me/nodejs-crypto/

打赏作者