Nodejs RESTFul架构实践

准备

如果你不了解http协议,先看一下https://github.com/i5ting/node-http

什么是REST?

RESTful架构,就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。

以下内容摘自阮一峰的文章:

一、起源

REST这个词,是Roy Thomas Fielding在他2000年的博士论文中提出的。

二、名称

Fielding将他对互联网软件的架构原则,定名为REST,即Representational State Transfer的缩写。我对这个词组的翻译是”表现层状态转化”。
如果一个架构符合REST原则,就称它为RESTful架构。

三、资源(Resources)

REST的名称”表现层状态转化”中,省略了主语。”表现层”其实指的是”资源”(Resources)的”表现层”。

四、表现层(Representation)

“资源”是一种信息实体,它可以有多种外在表现形式。我们把”资源”具体呈现出来的形式,叫做它的”表现层”(Representation)。

五、状态转化(State Transfer)

访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。

六、综述

综合上面的解释,我们总结一下什么是RESTful架构:

(1)每一个URI代表一种资源;
(2)客户端和服务器之间,传递这种资源的某种表现层;
(3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。

说的通俗一点,改变url写法,让它带有状态,即语义化

更多详见:

请求方法

一般会严格要求请求方法及其释义,下面给出常用的请求方法

  • 如果请求头中存在 X-HTTP-Method-Override 或参数中存在 _method(拥有更高权重),且值为 GET, POST, PUT, DELETE, PATCH, OPTION, HEAD 之一,则视作相应的请求方式进行处理
  • GET, DELETE, HEAD 方法,参数风格为标准的 GET 风格的参数,如 url?a=1&b=2
  • POST, PUT, PATCH, OPTION 方法
    • 默认情况下请求实体会被视作标准 json 字符串进行处理,当然,依旧推荐设置头信息的 Content-Typeapplication/json
    • 在一些特殊接口中(会在文档中说明),可能允许 Content-Typeapplication/x-www-form-urlencoded 或者 multipart/form-data ,此时请求实体会被视作标准 POST 风格的参数进行处理

关于方法语义的说明:

  • OPTIONS 用于获取资源支持的所有 HTTP 方法
  • HEAD 用于只获取请求某个资源返回的头信息
  • GET 用于从服务器获取某个资源的信息
    • 完成请求后返回状态码 200 OK
    • 完成请求后需要返回被请求的资源详细信息
  • POST 用于创建新资源
    • 创建完成后返回状态码 201 Created
    • 完成请求后需要返回被创建的资源详细信息
  • PUT 用于完整的替换资源或者创建指定身份的资源,比如创建 id 为 123 的某个资源
    • 如果是创建了资源,则返回 201 Created
    • 如果是替换了资源,则返回 200 OK
    • 完成请求后需要返回被修改的资源详细信息
  • PATCH 用于局部更新资源
    • 完成请求后返回状态码 200 OK
    • 完成请求后需要返回被修改的资源详细信息
  • DELETE 用于删除某个资源
    • 完成请求后返回状态码 204 No Content

上面是比较常见的,估计大部分人最常用的是2个,get和post,具体每个怎么玩,下面会给出实例

最常见的增删改查

以前大家都认为管理信息系统就是crud,认为没有啥技术含量,哎,真正能把crud写明白其实也不是一件容易的事儿

七个路由,见app/routes/users.js

其中4个路由是crud

  • GET /users[/] => user.list()
  • POST /users[/] => user.create()
  • PATCH /users/:id => user.update()
  • DELETE /users/:id => user.destroy()

另外3个是页面渲染用的

  • GET /users/new => user.new()
  • GET /users/:id => user.show()
  • GET /users/:id/edit => user.edit()

那么我们先来看一下crud对应的请求方法

  • get用于请求列表
  • post用于创建
  • patch用于更新,局部更新资源
  • delete用于删除

对比上一节的内容,你会发现他们的含义貌似真的对了,唯一可能有争议是更新,有的人用put有的用patch,推荐patch

以前做java的时候会认为,创建、删除和更新都用post,查询和搜索用get,这样做没问题,只是不符合rest风格而已。

很多人和我讨论过到该不该rest,我的回答是

express代码分层

默认的express生成器生成的只有routes和views文件夹,相对比较简单,做大型应用怎么能少了mvc呢?

于是我仿照rails写了如下代码分层

  • routes 路由层,只有url和中间件,不包含任何逻辑
  • controllers 业务逻辑控制层
  • models 模型层
  • views 视图层

如下图

路由层

上代码

var express = require('express');
var router = express.Router();

var $ = require('../controllers/users_controller');


// -- custom


/**
 * Auto generate RESTful url routes.
 *
 * URL routes:
 *
 *  GET    /users[/]        => user.list()
 *  GET    /users/new       => user.new()
 *  GET    /users/:id       => user.show()
 *  GET    /users/:id/edit  => user.edit()
 *  POST   /users[/]        => user.create()
 *  PATCH  /users/:id       => user.update()
 *  DELETE /users/:id       => user.destroy()
 *
 */

router.get('/new', $.new);  
router.get('/:id/edit', $.edit);

router.route('/')
  .get($.list)
  .post($.create);

router.route('/:id')
  .patch($.update)
  .get($.show)
  .delete($.destroy);


module.exports = router;

和普通的路由代码一样,稍微改造了一下,统一了地址,应该是更清晰了。

这里只是引入了users_controller文件,完成请求地址和业务处理代码的映射而已。

这里还要讨论一个问题,每次增加一个接口就要加一个路由会比较烦,而且要在app.js里配置,能不能自动加载呢?比如app/routes目录下的所有js都可以挂载到app上

答案是可以的,使用我写的mount-routes即可,示例如下:

var express = require('express')
var app = express()

var mount = require('mount-routes');

// 简单用法,加载app/routes下的所有路由
// mount(app);

// 带路径的用法,加载routes2目录下的所有路由
// mount(app, 'routes2');

// 带路径的用法并且可以打印出路有表
mount(app, 'routes2', true);

// start server
app.listen(23018)

更多内容见

mount-routes

控制层

/**
 * Created by sang on 01/06/14.
 */

var User = require('../models/user');

首先,控制层是控制业务处理的,所以它和模型层打交道比较多,同时控制视图如何展示

exports.list = function (req, res, next) {
  console.log(req.method + ' /users => list, query: ' + JSON.stringify(req.query));
  User.getAll(function(err, users){
    console.log(users);
    res.render('users/index', {
      users : users
    })
  });
};

请求所有列表,很简单,获取所有用户即可

exports.new = function (req, res, next) {
  console.log(req.method + ' /users/new => new, query: ' + JSON.stringify(req.query));

  res.render('users/new', {
    user : {
      "_action" : "new"
    }
  })
};

新建用户,实际是render视图而已,没有啥逻辑,user参数是为了我生成代码方便用的,无他

exports.show = function (req, res, next) {
  console.log(req.method + ' /users/:id => show, query: ' + JSON.stringify(req.query) + 
    ', params: ' + JSON.stringify(req.params));
  var id = req.params.id;

  User.getById(id, function(err, user) {
    console.log(user);
    res.render('users/show', {
      user : user
    })
  });
};

同new,是render视图代码

exports.edit = function (req, res, next) {
  console.log(req.method + ' /users/:id/edit => edit, query: ' + JSON.stringify(req.query) + 
    ', params: ' + JSON.stringify(req.params));

  var id = req.params.id; 

  User.getById(id, function (err, user) {
    console.log(user);
    user._action = 'edit';

    res.render('users/edit', {
      user : user
    })
  });
};

同new,是render视图代码

exports.create = function (req, res, next) {
  console.log(req.method + ' /users => create, query: ' + JSON.stringify(req.query) + 
    ', params: ' + JSON.stringify(req.params) + ', body: ' + JSON.stringify(req.body));

    User.create({name: req.body.name,password: req.body.password}, function (err, user) {
      console.log(user);
      res.render('users/show', {
        user : user
      })
    });
};

这段是创建用户的代码,根据post参数,保存入库,跳转到展示详情页面

exports.update = function (req, res, next) {
  console.log(req.method + ' /users/:id => update, query: ' + JSON.stringify(req.query) + 
    ', params: ' + JSON.stringify(req.params) + ', body: ' + JSON.stringify(req.body));

    var id = req.params.id; 

    User.updateById(id,{name: req.body.name,password: req.body.password}, function (err, user) {
      console.log(user);

      res.json({
        data:{
          redirect : '/users/' + id
        },
        status:{
          code : 0,
          msg  : 'delete success!'
        }
      });
    });
};

和创建类似,它是根据id来更新内容

exports.destroy = function (req, res, next) {
  console.log(req.method + ' /users/:id => destroy, query: ' + JSON.stringify(req.query) + 
    ', params: ' + JSON.stringify(req.params) + ', body: ' + JSON.stringify(req.body));

  var id = req.params.id;
  User.deleteById(id, function (err) {
    console.log(err);
    res.json({
      data:{},
      status:{
        code : 0,
        msg  : 'delete success!'
      }
    });
  });
};

和创建类似,它是根据id来删除内容

以上代码形式都是一样的

exports.xxxx = function (req, res, next) {
  ...
}

他实际上connect中间件的标准写法,如果你熟悉express,可以非常简单的上手,看了这些代码,你一定很好奇,User模型是怎么工作的,而且mongoose里并没有这些方法啊,下面会详细说明

模型层

我们的模型层使用的是比较传统的mongoose,如果需要promise库,可以搭配bluebird

/**
 * Created by alfred on 01/06/14.
 */

var mongoose    = require('mongoose');
var Schema      = mongoose.Schema;
var MongooseDao = require('mongoosedao');

var userSchema = new Schema(
    {"name":"String","password":"String"}
);

var User = mongoose.model('User', userSchema);
var UserDao = new MongooseDao(User);

module.exports = UserDao;

这实际是最简单的定义mongoose模型的方法,我们唯一做的改进是增加了MongooseDao

DAO是java的概念,是data access object,即数据访问对象,就是传说的crud方法

你只要知道模型就好,为啥每个crud都要写呢?那得多烦啊

var UserDao = new MongooseDao(User);

这样就可以给user增加了基本的crud方法

所以在controller里我们看到了如下代码

  • User.getAll(function(err, users){
  • User.getById(id, function(err, user) {
  • User.create({name: req.body.name,password: req.body.password}, function (err, user) {
  • User.updateById(id,{name: req.body.name,password: req.body.password}, function (err, user) {
  • User.deleteById(id, function (err) {

这5个方法,完美的完成crud的所有操作,是不是很爽?至少少写了很多代码

而且当你想扩展的时候,你可以使用User.model来操作mongoose对象

比如login,我需要在User模型增加is_exist方法

var mongoose    = require('mongoose');
var Schema      = mongoose.Schema;
var MongooseDao = require('mongoosedao');

var userSchema = new Schema(
    {"name":"String","password":"String"}
);

userSchema.methods.is_exist = function(cb) {
  var query;
  query = {
    username: this.username,
    password: this.password
  };
  return this.model('User').findOne(query, cb);
};

var User = mongoose.model('User', userSchema);
var UserDao = new MongooseDao(User);

module.exports = UserDao;

这就是mongoose里的实例方法,static方法也是一样,你可以玩各种花样

然后控制层

exports.login = function (req, res, next) {
  username = req.body.username;
  password = req.body.password;

  user = new User.model({
    username: username,
    password: password
  });

  return user.is_exist(function(err, usr) {
     。。。
  });
}

是不是很简单?

视图层

视图层我们采用express默认的jade,无论各位怎么看,jade都可圈可点

1)extends 方式使用布局
2)include 复用模型代码
3)block 复用块代码

以new.jade和edit.jade为例,它们具有代表性

new.jade

extends ../layouts/layout

block content
  h1 New user

  include user

  a(href='/users') Back

edit.jade

extends ../layouts/layout

block content
  h1 Editing user

  include user

  a(href='/users/#{ user._id}') Show
  span |
  a(href='/users') Back

首先要说明的是include用法,include类似于partial概念,可以包含一个jade作为一部分

jade里有一个约定,你include了谁,它就要把这个对象传进去

所以user.jade里才是我们复用的重点

- var _action = user._action == 'edit' ? '#' : '/users/'
- var _method = user._action == 'edit' ? ""  : "post"
- var _type   = user._action == 'edit' ? "button"  : "submit"
- var onClick  = user._action == 'edit' ?  "click_edit('user-" + user._action + "-form','/users/" + user._id + "/')" : ""
form(id='user-#{ user._action}-form',action="#{_action}", method="#{_method}",role='form')
  each n in ['user.name','user.password']
    - m = eval(n);
    div(class="field")
      label #{n.split('.')[1]} #{m}
      br
      input(type='text',name="#{n.split('.')[1]}" ,value="#{ m == undefined ? '' : m }")

  div(class="actions") 
    input(type='#{_type}',value='Submit',onClick='#{onClick}')

这段是为了复用写的代码,可读性不强,但可以说明include用法,对于代码而言,达到了一定的复用

实际上我们自己jade的适合要尽可能的拆分成小块去复用,当需求变得时候更容易应变。提醒强迫症患者,当心物极必反,不要刻意去强求

总结一下

RESTful架构大势所趋,代码写的标准了让人觉得赞

Expressjs下可以写的很规范,可以有做大应用,可以有很好的分层

展示了MVC + Routes的标准rest写法

使用了几个开源代码

  • mount-routes
  • MongooseDao

完成了以上工作,我们还要继续反思一下,既然rest是标准,写法很固定,是不是可以量产呢?答案是可以的,我展示的所有代码是根据一条命令搞定的

moag user name:string password:string

这就是我目前在写的一个开源项目,待可用的时候会公开的,敬请期待

延伸阅读

欢迎关注我的公众号【node全栈】

Nodejs 里Excel开发实践

excel的导入导出在一般业务系统里是非常常见的,但是nodejs里并没有成熟的处理excel的库,导入问题不大,稍微复杂的导出就搞不定了

  • exceljs
  • excel.js
  • node-xlsx

都太简单了…

那么,只剩下曲线救国一条路了

合理架构

node的优点就不说了,它虽好,但毕竟才出来几年,很多东西都不够完善的,所以在使用这种比较新比较潮的技术的时候,一定要学会扬长避短,不然会很难受

apache的poi是一个不错的库,非常成熟,我从07年就开始用它,

下面是百科的简介

Apache POI 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程式对Microsoft Office格式档案读和写的功能。POI为“Poor Obfuscation Implementation”的首字母缩写,意为“可怜的模糊实现”。

Apache POI 是创建和维护操作各种符合Office Open XML(OOXML)标准和微软的OLE 2复合文档格式(OLE2)的Java API。用它可以使用Java读取和创建,修改MS Excel文件.而且,还可以使用Java读取和创建MS Word和MSPowerPoint文件。Apache POI 提供Java操作Excel解决方案(适用于Excel97-2008)。

http://poi.apache.org/

我们需要考虑以下问题:

  • 多语言是否合适?
  • 分拆功能是否合适?

思考

系统小的时候肯定会尽量统一的,系统大了就会有各种鸟了

比如创业初期,可能是rails or express写的东西,但随着业务的壮大,会发现瓶颈的,这个时候你需要扩容

你多加台机器,然后部署一次

可是这样真的好么?

理想的做法是拆分成小模块,就像处理redis缓存一样,业务量大了,就立马多起几个docker实例,非常好运维

而且从职责上看,它也是小而美的代表

既然小了,那么你用什么语言开发还重要么?

比如我的核心系统是expressjs写的,我的下载就用的java写的,我部署了2个,然后nginx处理一下,谁又能看出来差别呢?

而且我的express一定要多起几个实例,而java的可能并发比较少,我没有去浪费资源。

分享一个简单的工具类

package im.xbm.dlcenter.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFDateUtil;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

public class ExcelUtils {
    // 对外提供读取excel文件的接口
    public static List<List<Object>> readExcel(File file) throws IOException {
        String fName = file.getName();
        String extension = fName.lastIndexOf(".") == -1 ? "" : fName
                .substring(fName.lastIndexOf(".") + 1);
        if ("xls".equals(extension)) {// 2003
            System.err.println("读取excel2003文件内容");
            return read2003Excel(file);
        } else if ("xlsx".equals(extension)) {// 2007
            System.err.println("读取excel2007文件内容");
            return read2007Excel(file);
        } else {
            throw new IOException("不支持的文件类型:" + extension);
        }
    }

    /**
     * 读取2003excel
     * 
     * @param file
     * @return
     */
    private static List<List<Object>> read2003Excel(File file)
            throws IOException {
        List<List<Object>> dataList = new ArrayList();
        HSSFWorkbook wb = new HSSFWorkbook(new FileInputStream(file));
        HSSFSheet sheet = wb.getSheetAt(0);
        HSSFRow row = null;
        HSSFCell cell = null;
        Object val = null;
        DecimalFormat df = new DecimalFormat("0");// 格式化数字
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 格式化日期字符串
        for (int i = sheet.getFirstRowNum(); i < sheet
                .getPhysicalNumberOfRows(); i++) {
            row = sheet.getRow(i);
            if (row == null) {
                continue;
            }
            List<Object> objList = new ArrayList<Object>();
            for (int j = row.getFirstCellNum(); j < row.getLastCellNum(); j++) {
                cell = row.getCell(j);
                if (cell == null) {
                    val = null;
                    objList.add(val);
                    continue;
                }
                switch (cell.getCellType()) {
                case HSSFCell.CELL_TYPE_STRING:
                    val = cell.getStringCellValue();
                    break;
                case HSSFCell.CELL_TYPE_NUMERIC:
                    if ("@".equals(cell.getCellStyle().getDataFormatString())) {
                        val = df.format(cell.getNumericCellValue());
                    } else if ("General".equals(cell.getCellStyle()
                            .getDataFormatString())) {
                        val = df.format(cell.getNumericCellValue());
                    } else {
                        val = sdf.format(HSSFDateUtil.getJavaDate(cell
                                .getNumericCellValue()));
                    }
                    break;
                case HSSFCell.CELL_TYPE_BOOLEAN:
                    val = cell.getBooleanCellValue();
                    break;
                case HSSFCell.CELL_TYPE_BLANK:
                    val = "";
                    break;
                default:
                    val = cell.toString();
                    break;
                }
                objList.add(val);
            }
            dataList.add(objList);
        }
        return dataList;
    }

    /**
     * 读取excel表头
     * 
     * @param file
     * @return
     * @throws IOException
     */
    public static String[] readExcelHead(File file) throws IOException {
        HSSFWorkbook wb = new HSSFWorkbook(new FileInputStream(file));
        HSSFSheet sheet = wb.getSheetAt(0);
        HSSFRow row = null;
        HSSFCell cell = null;
        row = sheet.getRow(0);
        String[] buff = new String[row.getLastCellNum()];
        for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) {
            cell = row.getCell(i);
            buff[i] = cell.getStringCellValue();
        }
        return buff;
    }

    /**
     * 读取2007excel
     * 
     * @param file
     * @return
     */

    private static List<List<Object>> read2007Excel(File file)
            throws IOException {
        List<List<Object>> dataList = new ArrayList<List<Object>>();
        XSSFWorkbook xwb = new XSSFWorkbook(new FileInputStream(file));
        XSSFSheet sheet = xwb.getSheetAt(0);
        XSSFRow row = null;
        XSSFCell cell = null;
        Object val = null;
        DecimalFormat df = new DecimalFormat("0");// 格式化数字
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 格式化日期字符串
        for (int i = sheet.getFirstRowNum(); i < sheet
                .getPhysicalNumberOfRows(); i++) {
            row = sheet.getRow(i);
            if (row == null) {
                continue;
            }
            List<Object> objList = new ArrayList<Object>();
            for (int j = row.getFirstCellNum(); j < row.getLastCellNum(); j++) {
                cell = row.getCell(j);
                if (cell == null) {
                    val = null;
                    objList.add(val);
                    continue;
                }
                switch (cell.getCellType()) {
                case XSSFCell.CELL_TYPE_STRING:
                    val = cell.getStringCellValue();
                    break;
                case XSSFCell.CELL_TYPE_NUMERIC:
                    if ("@".equals(cell.getCellStyle().getDataFormatString())) {
                        val = df.format(cell.getNumericCellValue());
                    } else if ("General".equals(cell.getCellStyle()
                            .getDataFormatString())) {
                        val = df.format(cell.getNumericCellValue());
                    } else {
                        val = sdf.format(HSSFDateUtil.getJavaDate(cell
                                .getNumericCellValue()));
                    }
                    break;
                case XSSFCell.CELL_TYPE_BOOLEAN:
                    val = cell.getBooleanCellValue();
                    break;
                case XSSFCell.CELL_TYPE_BLANK:
                    val = "";
                    break;
                default:
                    val = cell.toString();
                    break;
                }
                objList.add(val);
            }
            dataList.add(objList);
        }
        return dataList;
    }
}

从这个可以看出poi的用法,整体还是比较简单

开发知识点

你需要学的是

  • 会基本的servlet,写接口(eclipse新建项目的时候,选择dynamic项目)
  • 使用eclipse调试开发
  • 使用mongodb java driver去操作mongodb
  • 使用poi去读取和生成文件
  • 使用java处理一下文件路径

这里就不科普了,javaEE的教程一搜一大堆

心态

不要排斥其他语言,用好它们的长处才是本事

现在是一个合作的时代

欢迎关注我的公众号【node全栈】

Nodejs ueditor

ueditor是一个所见即所得的编辑器,baidu那些人写的,还不错

http://ueditor.baidu.com/website/

node版本后台

可以在这里找到,http://ueditor.baidu.com/website/thirdproject.html

Node.js:ueditor

可你让您的UEditor 兼容nodejs。Node.js:ueditor@0.0.4以及更高版本,支持图片上传,图片批量管理等。您可以通过npm install ueditor 直接安装此插件。项目代码已经发放到github上,欢迎关注此项目。

源码地址: https://github.com/netpi/ueditor

服务器端用法

var bodyParser = require('body-parser')
var ueditor = require("ueditor")
app.use(bodyParser.urlencoded({
  extended: true
}))
app.use(bodyParser.json());

app.use("/ueditor/ue", ueditor(path.join(__dirname, 'public'), function(req, res, next) {
  // ueditor 客户发起上传图片请求
  if(req.query.action === 'uploadimage'){
    var foo = req.ueditor;
    var date = new Date();
    var imgname = req.ueditor.filename;

    var img_url = '/images/ueditor/';
    res.ue_up(img_url); //你只要输入要保存的地址 。保存操作交给ueditor来做
  }
  //  客户端发起图片列表请求
  else if (req.query.action === 'listimage'){
    var dir_url = '/images/ueditor/';
    res.ue_list(dir_url);  // 客户端会列出 dir_url 目录下的所有图片
  }
  // 客户端发起其它请求
  else {

    res.setHeader('Content-Type', 'application/json');
    res.redirect('/ueditor/ueditor.config.json')
}}));

注意包得引用和配置项即可。再有就是提前创建好目录,防止报错。

客户端用法

步骤1: 下载ueditor解压到public,

步骤2: 修改/ueditor/ueditor.config.js

window.UEDITOR_CONFIG = {

    //为编辑器实例添加一个路径,这个不能被注释
    UEDITOR_HOME_URL: URL

    // 服务器统一请求接口路径
    , serverUrl: URL + 'ue'

serverUrl必须和上面的服务器端配置的一样

步骤3:修改/ueditor/ueditor.config.json

主要是修改路径

更多用法,http://ueditor.baidu.com/doc

谨慎multer

如果你的项目使用了multer作为上传组件,那么你要小心了

一定要在multer之前加载ueditor,不然请求会被multer拦截,这是connect中间件的弊病

如果各位有兴趣,可以fork一笑ueditor,改成和multer兼容的,必然会有很多star

全文完

欢迎关注我的公众号【node全栈】

关于创业

关于创业,jekyll写博客的时候需要起英文名称,我想了一下,用“pinming”可能更合适

心态

创业的人大部分是从比较舒服,正规的公司出来的人,我和我的合伙人都是这样的。

创业只是有一个方向,其他都是不确定因素

那么,你要怎么办呢?

学会切换状态和应变

在大公司里,做的事儿比较专,有很多时间学习,有各种人才可以沟通学习

而创业公司,只有你自己的,无论什么,都有亲力亲为

好的做法是,在创业公司里沉淀自己的不足,补齐所有短板,做一个真正的全能开发。

只有热爱学习,你才能做的更好

只有干好现在的工作,公司不会黄,才有更多东西可以学习

想明白自己要啥

我的名言是”如果没目标,就向钱看,有目标就向前看”

你选择创业,其实也要想明白自己要啥

比如我

  • 我需要赚钱,财务自由
  • 我需要有更多的技术经历
  • 我需要更多的自由
  • 我渴望成功的喜悦,疑惑一夜痛哭

之前太安逸了,我做的最对的一件事是学习,现在我选择了创业,带着我的小伙伴们

“我不一定能带给你多少好处,会很累,但技术上会让你们成长”

我不喜欢股权期权类的画饼,对的起自己就好

相互扶持

大伙的利益绑在一起了,就要靠在一起取暖

不抛弃,不放弃

相信不完美

创业有很多准则,比如精益,敏捷等

举个最简单的例子:mvp是最小可用原型,创业初期一定会这样做,做各种减法,它不完美,但可用

它很丑,它很low。。。

但是它是个实实在在的东西,你可以改,你可以打扮它,你可以有更广阔的想法

利用好创业初期的三分钟热血是何其重要

我有过很多次追求大而全的所谓完美的创意和设计,想的特别好,比如一个系统该有

  • 各种功能
  • 各种权限
  • 各种后台
  • 各种解耦

结果一想,一个人,1年估计也做不完,于是。。。。再也没有兴趣了

再举个买书的例子,我的经验是买的书到手1周内必须看完一遍,不然可能这辈子都不会看了

逼着自己尽力去读,即使记不住很多细节,至少你了解了骨架

在以后的磨练中,你知道在什么地方去查,如果有时间,能够细细的看完,那将会是一件何其幸福而美好的事儿

无论如何,我们都得承认,这世界是不完美的

那么另一个问题来了,如果你的朋友是处女座,追求完美怎么办?

  • 保持好心态,对之耐心
  • 鼓励她在小事儿上追求完美

这样非常容易实现,又可以满足她的追求完美的心

小步快跑

这其实敏捷开发里的一个概念:它规范点说叫【迭代】

通常scrum里叫sprint,冲刺,就是在2周时间内,发布一个迭代版本,为了打成这个目标,不遗余力

开发和测试是2周,产品和设计要提前2周,步子交替着进行,效率还是蛮高的。

另外要说明的是,2周的时间,即使发现产品设计是失误的,那么这个人财物的成品是可以接受的,不会造成更大损失,在下一个版本了纠正,即可

跟着用户走,带着用户跑。而创业者永远在路上

放大可能

可以头脑风暴,也可以让某些人得想法无线放大,只要不偏离主线,都可以去想

最重要的应变能力

创业风险非常大,很可能死了,或者方向改变,政策风险,各种阻力,等等

放大每个人的优点

创业虽然经常试错,但是我们不能经常犯错,因为玩不起。。。

没有那么多钱,一下玩大了,可能公司就黄了

所以,放大每个人的优点是变相的降低风险,比如处女座的朋友,她喜欢做产品设计,你就大胆让她做,虽然不一定完美,但肯定比我做的好

至少她那份责任心和对产品的感觉是我没法比的

另外我还要做的一点是,配合她,在技术领域不给她壁垒和障碍

补齐每个人的短板

很可能做的事儿不是你擅长的,那你能怎么办呢?

硬着头皮向前冲吧

对于程序员更容易应变,经常在代码世界遇到各种挫折,却能坚强的活到现在,我们有足够的信心,相信我们能够解决下一个问题

新业务或者不擅长的东西,其实也是一个问题而已

只要肯花时间,1万个小时,你一定会成为专家

不要焦虑

一次只做一件事儿,不要焦虑

比如公司做大,怀疑自己的能力能否胜任

比如要做的事儿太多,搞不定怎么办?

其实我觉得那都不是事儿

  • 第一,时间问题,可能不会来的那么快
  • 第二,车道山前必有路,没有路也会踩出路
  • 第三,最坏打算,就算不能胜任,至少钱是有的,就算公司黄了,至少能力是有的

我的座右铭

“ 少抱怨,多思考,未来更美好 ”

欢迎关注我的公众号【node全栈】

如何学习之善用github篇

如何学习之善用github篇

从代码看人,以pana为例

nrm 很不错

nrm是一个node源管理器,主要是测速和快速切换源

nrm test
nrm use taobao

虽然之前写的数据不是特别准(fengmk2),但是这个想法很好,如果没猜错,应该是借鉴nrm

无论如何,他实现了,而且很好用

里面的主要知识点是cli相关部分,一个会写cli的人,基本上*nix和shell玩的都不错,综合素质应该是不错的

https://github.com/trending

有图为证

话说,知道关注trending的人不太多,一般比较geek一点的人喜欢看这个,对技术非常热爱

trending里大部分是最近比较流行的,star数增长最快的项目

  • 有创意,想法的好项目
  • 有最佳实践
  • 有show技术

总之,trending你值得拥有

pana能够去关注,star,说明他比较潮,喜欢新技术

社区贡献

cnode上就不说了

他得首页上,参与贡献的项目

  • ChinaNode/DailyNode
  • nodejs/iojs-cn
  • ChinaNode/koajs.in
  • rajaraodv/redispubsub
  • tj/co

参与翻译iojs的文档,英文应该还不错

关注koa和co,并能贡献,应该是对es6也非常了解,nodejs水平应该不错

贡献redispubsub,对消息队列和redis有一定研究

总结

github上基本可以看出一个程序员的水平,这其实是为啥很多公司招聘让程序员报github和stackoverflow的原因

我写这篇文章的目的,是希望大家能够向pana学习,如果借助github这个平台,达到自己技术梦想的目的。

比较supervisor和nodemon

todo

欢迎关注我的公众号【node全栈】

NODEJS部署实践

如何选择node版本?

从目前的测试数据来看,iojs新版本性能会更好一些,如果测试写的不够多的话,老老实实的用开发时的版本吧

推荐nvm,是node version manager,是借鉴rvm的一个开源项目

  • nvm 支持多版本
  • nvm支持多版本npm包切换,而且安装在用户目录下面,不需要使用sudo安装

统一环境

开发环境

  • nvm
  • nrm
  • nodejs 0.10.38
  • node-inspector

部署环境

  • nvm
  • nrm
  • iojs 2.x
  • pm2
  • nginx

express 部署

expressjs里默认给出的是node bin/www

它有2个问题

  1. 没有cluster,不适合产品模式
  2. 代码变动不能自动reload,不适合开发模式

所以我们需要把bin/www干掉,把启动服务器的代码放到app.js,这样就可以使用pm2了。安装supervisor模块,自动relaod

EXPRESS最新版本需要测测

本文主要介绍express线上部署实践

先看一下3个模式

  • 开发模式
  • 调试模式
  • 产品模式

启动开发模式

因为我们在开发过程中总是希望修改后立即看到效果,而不是每次都要终止进程并重启。

supervisor 可以帮助你实现这个功能,它会监视你对代码的改动,并自动重启 Node.js

开发模式我推荐 supervisor

安装

npm install --save supervisor

启动

supervisor app.js or supervisor www/bin

我的实践是把package.json里的start

"scripts": {
  "d": "node-debug app.js",
  "start": "./node_modules/.bin/supervisor app.js"
}

然后

npm start

还有一个库也不错,叫nodemon

see http://nodemon.io/

调试模式

推荐node-inspector

安装

npm install --save node-inspector

启动

node-inspector app.js or node-debug www/bin

我的实践是把package.json里的d里

"scripts": {
  "d": "node-debug app.js",
  "start": "./node_modules/.bin/supervisor app.js"
}

然后执行

npm run d

npm run命令详解,见https://github.com/i5ting/npm-run-test

启动产品上线模式

产品模式我们推荐使用pm2

pm2是非常优秀工具,它提供对基于node.js的项目运行托管服务。它基于命令行界面,提供很多特性:

  • 内置的负载均衡器(使用nodecluster module)
  • 以守护进程运行
  • 0s(不间断)重启
  • 为ubuntu/ CentOS 提供启动脚本
  • 关闭不稳定的进程(避免无限死循环)
  • 基于控制台监控
  • HTTP API
  • 远程控制以及实时监控接口

pm2使用nodecluster构建一个内置的负载均衡器。部署多个app的实例来达到分流的目的以减轻单app处理的压力。

安装pm2

sudo npm install -g pm2

查看基本帮助文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Basic Examples:

Start an app using all CPUs available + set a name :
$ pm2 start app.js -i 0 --name "api"

Restart the previous app launched, by name :
$ pm2 restart api

Stop the app :
$ pm2 stop api

Restart the app that is stopped :
$ pm2 restart api

Remove the app from the process list :
$ pm2 delete api

Kill daemon pm2 :
$ pm2 kill

Update pm2 :
$ npm install pm2@latest -g ; pm2 updatePM2


More examples in https://github.com/Unitech/pm2#usagefeatures

Deployment help:

$ pm2 deploy help

上面基本是我们比较常用的

pm2的最佳实践

把启动写到json配置文件中

// 20150531071243
// https://raw.githubusercontent.com/RocketChat/Rocket.Chat/master/pm2.production.json

{
  "apps": [
    {
      "name": "rocket.chat.0",
      "max_memory_restart": "1024M",
      "log_date_format": "YYYY-MM-DD HH:mm:ss SSS",
      "script": "/var/www/rocket.chat/bundle/main.js",
      "out_file": "/var/log/rocket.chat/app.log",
      "error_file": "/var/log/rocket.chat/err.log",
      "port": "8080",
      "env": {
        "CDN_PREFIX": "//dbde4sd21oahf.cloudfront.net",
        "MONGO_URL": "mongodb://localhost:27017/rocketchat",
        "MONGO_OPLOG_URL": "mongodb://localhost:27017/local",
        "ROOT_URL": "http://rocket.chat",
        "PORT": "8080"
      }
    },
    {
      "name": "rocket.chat.1",
      "max_memory_restart": "1024M",
      "log_date_format": "YYYY-MM-DD HH:mm:ss SSS",
      "script": "/var/www/rocket.chat/bundle/main.js",
      "out_file": "/var/log/rocket.chat/app.log",
      "error_file": "/var/log/rocket.chat/err.log",
      "port": "8081",
      "env": {
        "CDN_PREFIX": "//dbde4sd21oahf.cloudfront.net",
        "MONGO_URL": "mongodb://localhost:27017/rocketchat",
        "MONGO_OPLOG_URL": "mongodb://localhost:27017/local",
        "ROOT_URL": "http://rocket.chat",
        "PORT": "8081"
      }
    },
    {
      "name": "rocket.chat.2",
      "max_memory_restart": "1024M",
      "log_date_format": "YYYY-MM-DD HH:mm:ss SSS",
      "script": "/var/www/rocket.chat/bundle/main.js",
      "out_file": "/var/log/rocket.chat/app.log",
      "error_file": "/var/log/rocket.chat/err.log",
      "port": "8082",
      "env": {
        "CDN_PREFIX": "//dbde4sd21oahf.cloudfront.net",
        "MONGO_URL": "mongodb://localhost:27017/rocketchat",
        "MONGO_OPLOG_URL": "mongodb://localhost:27017/local",
        "ROOT_URL": "http://rocket.chat",
        "PORT": "8082"
      }
    },
    {
      "name": "rocket.chat.3",
      "max_memory_restart": "1024M",
      "log_date_format": "YYYY-MM-DD HH:mm:ss SSS",
      "script": "/var/www/rocket.chat/bundle/main.js",
      "out_file": "/var/log/rocket.chat/app.log",
      "error_file": "/var/log/rocket.chat/err.log",
      "port": "8083",
      "env": {
        "CDN_PREFIX": "//dbde4sd21oahf.cloudfront.net",
        "MONGO_URL": "mongodb://localhost:27017/rocketchat",
        "MONGO_OPLOG_URL": "mongodb://localhost:27017/local",
        "ROOT_URL": "http://rocket.chat",
        "PORT": "8083"
      }
    }
  ]
}

部署

pm2 deploy ecosystem.json production setup

第一个傻瓜原则,你的机器有几核,你就几个实例

Start an app using all CPUs available + set a name :
$ pm2 start app.js -i 0 --name "api"

上面的配置文件了配置了4个,你应该能自己估算出点什么

第二个傻瓜原则,想机器瘫痪么?最简单的办法是内存不足,怎么样能防止内存被吃尽呢?

很简单,和lru cache一样,设置最大可用内存,防止内存用爆

参数里

--max-memory-restart <memory>        specify max memory amount used to autorestart (in megaoctets)

配置文件了也一样

"max_memory_restart": "1024M",

4个实例,每个可占用内存为1G,那么你应该能自己估算出点什么

第三个傻瓜原则,敏感信息咋办?有2种办法

  1. 使用config/default.example.json。部署的时候拷贝一份,然后按需修改
  2. 使用pm2的env,在环境变量里配置,pm2的json是在服务器上生成的,每个服务器都要生成一次的

推荐pm2的env这种方式,相对更简单点

第四个傻瓜原则,集群状态下的应用应该是stateless的(无状态),不能依赖代码去做一些事儿的,比如自己写队列处理cache

因为每个实例都是独立的,你读写不是全局的,一定会有各种问题,尽量使用第三方独立的服务,比如配置文件(非实例下面),比如redis等缓存,db等

pm2高级用法

reload

启动的时候会指定一个名字,以后根据该名字,进行特定操作

pm2 reload some-app

它的好处是0秒延时,可以和nginx -s reload媲美

monit

pm2 monit

更多内容 https://github.com/Unitech/PM2#monitoring

日志

更多内容 https://github.com/Unitech/PM2#log-facilities

$ pm2 logs
$ pm2 logs big-api
$ pm2 flush          # Clear all the logs

web

npm run web

访问 http://127.0.0.1:9615

开机启动

pm2 startup centos

注意:示例的package.json里给出的是centos里增加开机启动项,如果是其他操作系统,请自己更改

更多内容 https://github.com/Unitech/PM2#startup-script-generation

$ pm2 startup <ubuntu|centos|gentoo|systemd>

开机启动,还要注意的一点是要保存一次pm2运行数据的

pm2 save

要不要nginx 或者 haproxy ?

nginx

  • 主要做反向代理和静态资源处理(如果没有cdn,就把public下的内容放到nginx里)
  • 负载
  • 阻止了很多常见的攻击和慢客户端

nginx负载对应多台机器,每台机器里起多个(cpu个数)实例,单机负载使用pm2管理。

nginx和haproxy都是针对多台机器的负载,功能差不过,使用和插件方面,nginx稍强一点,知名度更高一点

另外一种部署方式

passenger是rails部署时常用的服务器,不过庆幸的是这帮货出了node版本,而且性能不错

有兴趣的可以去研究一下 nginx + passenger

mongodb的部署

redis部署

http://www.linuxidc.com/Linux/2014-07/104306.htm

简单压测

  • apache ab
  • wrk

node cluster和nginx负载性能比较

todo

别人测试的node的性能稍高,不过我还是想自己测测

性能调优

todo

keeplive

监控

资源

欢迎关注我的公众号【node全栈】

分享2个实用的nodejs模块

分享2个实用的nodejs模块

  • kp 根据端口号杀死进程,尤其对于pm2的集群模式,僵尸进程有效
  • mount-routes 根据路径来自动加载路由,让开发更简单

kp

kp is a tool for kill process by server port. only use for mac && linux

npm version

Install

[sudo]npm install -g kp

Usage

default server port is 3000,直接执行kp会杀死使用3000端口的应用

kp

根据某个端口

kp 3002

支持sudo,因为有的时候有权限的问题

kp 3002 -s or kp 3002 --sudo

目前centos/ubuntu和mac已经测过

感谢@jysperm反馈:fuser 来自 psmisc 这个包(killall 也在这个包里),Ubuntu 默认安装,其他系统不清楚。

没有使用fuser的原因是:fuser在mac上不能用

mount-routes

mount-routes = auto mount express routes with routes_folder_path

Install

npm install --save mount-routes

Usages

var express = require('express')
var app = express()

var mount = require('mount-routes');

// simple
// mount(app);

// with path
mount(app,'routes2');

// start server
app.listen(23018)

使用方式1 mount(app);

可以自动挂载routes目录的所有路由,以文件名称作为路由的根

比如 routes/movies.js

它相当于

var movies = require('./config/routes/movies');

app.use('/movies',movies);

使用方式2 mount(app,’routes2’);

可以根据第二个参数,即路由目录文件夹的名称,自动挂载它下面的所有路由,以文件名称作为路由的根

比如 routes2/movies.js

它相当于

var movies = require('./config/routes2/movies');

app.use('/movies',movies);

总结

可以一次挂载多个路由目录

// simple
mount(app);

// with path
mount(app,'routes2');

但要小心文件名不能重复,不然会有问题,比如

  • routes/movies.js
  • routes2/movies.js

它们会挂载到同一个path上,这种情况下需要谨慎使用,以后版本会考虑改进

源码

欢迎反馈和贡献

欢迎关注我的公众号【node全栈】

少抱怨,多思考,未来更美好

心态

很多人会犯的问题:

  • 没做过啊
  • 不会啊
  • 不确定啊

好的心态比什么都重要

作为it从业者,最忌讳的是

  • 浮躁
  • 不求甚解
  • 想法太多

良好的进步

心态

  • 遇事不急不燥,心里能够很快的有应对想法
  • 善于观察,找到解决问题的症结点
  • 能够把问题最小化,快速攻城破寨

相信未知

  • 很多成功解决问题的经验,让你相信你能解决
  • 对于未知领域,热爱,喜欢那种冒险的乐趣,具有一定的创新意识
  • 以学习为最大目的和乐趣

自信

  • 赢得更多人的信任
  • 能够担重任
  • 有追求,不惧攀比和技术更新

做一个geek的人

我的理解,geek是

  • 有追求,尽力做事
  • 有想法,有创造力
  • 有实现能力,且实现的酷酷的

亲,你是一个geek么?

曾经

因为接了很多工作,又要重写一个上卷下钻的报表用的jquery插件,而没能完成,被项目经理说哭了。。。

我他妈一堂堂男子。。。。。无限悲愤

但时至今日,我回想起那段历史,我竟然还会感激

我是一个没有做过1天前端的前端主管,我的前端技术也不至于很差,都是因为当年折腾的足够多而已

有的时候想想,我的小弟们还是挺幸福的,他们至少有人给他们答疑

而我,只能一个人往前走

少抱怨,多思考,未来更美好

献给我的同代人

--作者舒婷

他们在天上
愿为一颗星
他们在地上
愿为一盏灯
不怕显得多么渺小
只要尽其可能

唯因不被承认
才格外勇敢真诚
即使像眼泪一样跌碎
敏感的大地
处处仍有
持久而悠远的回声

为开拓心灵的处女地
走入禁区,也许——
就在那里牺牲
留下歪歪斜斜的脚印
给后来者
签署通行证

1980.4

欢迎关注我的公众号【node全栈】

如何学习nodejs?

nodejs是比较简单的,只有你有前端js基础,那就按照我的办法来吧!一周足矣

推荐技术栈

  • express 4.x (express最新版本,初学者先别去碰koa)
  • mongoose(mongodb)
  • bluebird(Promise/A+实现)
  • jade(视图层模板)
  • mocha(测试)
  • node-inspector(调试)

https://github.com/i5ting/express-starter

了解http协议,尤其是表单和ajax传值,在req里如何接收

  • 绝对地址和相对地址
  • querystring
  • url 和 uri
  • http status code
  • http verbs
  • req取参数的3种方法
  • 3种不同类型的post
  • 命令行玩法
  • supertest用法
  • what is rest?

http://i5ting.github.io/node-http/

了解db相关操作,先以mongoose为主

  • crud(增删改查)
  • 了解分页
  • 了解关系(1对1,1对多)在mongoose里如何实现
  • 了解statics方法和methods的区别
  • 了解pre和post的差别
  • 了解mongoose的插件机制
  • 了解mvc里m的作用,以及什么样的代码该放到模型里
  • 了解索引优化
  • 了解mongodb的部署

了解Promise/A+规范,合理规避回调陷阱

  • 了解的node的异步
  • 了解异步的恶心
  • 了解异步基本场景,比如waterfall这样的路程使用async如何处理
  • 了解q和bluebird用法(如果有angularjs经验,推荐q,其他只推荐bluebird)
  • 了解bluebird的promisifyAll用法
  • 了解如何重构流程,以及代码的可读性

使用tdd/bdd测试,最小化问题

测试的好处,这里就不说了,但是有一点是要说的,node的调试比较难,往往不如写测试来的快,推荐学习一下

  • 理解最小问题思想,培养程序员该有的强大的内心
  • mocha的基本用法
  • 理解assert/should/expect等断言的用法
  • 理解测试生命周期
  • 理解done回调
  • 理解如何模拟数据
  • 理解http下的supertest测试
  • 理解测试覆盖率
  • 理解基于gulp自动化测试方法

如果有兴趣,可以去了解更多bdd/tdd内容,甚至是cucumber.js

你无论如何都要会的:调试

调试有3种方法

  • node debug(太挫了,如果不是c,了解adb之类的人不推荐用)
  • node-inspector(推荐4※)
  • tdd/bdd(推荐5※)

更多内容和视频见

https://cnodejs.org/topic/5463f6e872f405c829029f7e

欢迎关注我的公众号【node全栈】

如何处理h5高清屏图片?

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

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

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

项目官方网站:https://github.com/callmecavs/layzr.js

如果说仅仅是图片延时加载,它不至于成为github的热门项目,下面我们来看一下技术点

如果区分是否是高清屏?

举个简单的例子,比如iphone从4s之后就是高清屏, 新版的苹果本也都是高清屏

layzr是这样做的:

this._retina  = window.devicePixelRatio > 1;

关于device-pixel-ratio

device-pixel-ratio是media query一查询条件,用于获得设备的像素比。一般来说iPhone4/4s的值是2,高分辨率的Andriod设备是1.5,一般设备是1,有了这些条件,我们就可以为不同的设备提供不同分辨率的图片了。

事先假定让图片兼容以上像素比,展示一张宽高为100px的图片。首先我们需要准备三张不同分辨率的图片:当正常像素比为1时,我们载入的是正常图片100px100px,当像素比为1.5时,载入150px150px的图片,当像素比为2.0,载入200px*200px的图片。

来个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" />
<title>利用media query让背景图适应不同分辨率的设备</title>
<style>
/* 像素比为1,链入100px的图片, background-size:100% */
.header { background: url(Logo_1.png) no-repeat; }
/*像素比为1.5,链入150px的图片, background-size:1/1.5=66.7% */
@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 1.5) {
.header { background: url(Logo_1-5.png) no-repeat; background-size:66.7%; }
}
/*像素比为2,链入200px的图片, background-size:1/2=50% */
@media only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) {
.header { background: url(Logo_2-0.png) no-repeat; background-size: 50%;}
}
.w100{width:100px;height:100px;}
</style>

</head>
<body></body>
</html>

用法

看一下文档,layzr的关于retina图的用法如下:

1
<img data-layzr="image/source" data-layzr-retina="optional/retina/source">

为什么它用data-xxx?

自定义数据属性是在HTML5中新加入的一个特性。

简单来说,自定义数据属性规范规定任何以data-开头属性名并且赋值。

自定义数据属性是为了保存页面或者应用程序的私有自定义数据,这些自定义数据属性保存进DOM中,对于整个DOM的布局和表现无任何影响,但是却可以方便操控整个网页的交互以及想要表达的效果。

最典型的例子是jquery mobile

看一下它的panel经典用法: http://demos.jquerymobile.com/1.4.5/panel/

1
2
3
4
5
6
7
8
9
10
11
<div data-role="page">

<div data-role="panel" id="mypanel">
<!-- panel content goes here -->
</div><!-- /panel -->

<!-- header -->
<!-- content -->
<!-- footer -->

</div><!-- page -->

标签上都是自定义属性,每多一个属性,组合可能性会非常多,可读性比较高,但记忆成本也比较高

既然h5推荐,可以用,但不建议大量用

关于图片的2种用法

  1. img标签src
  2. background-image(url)

layzr里做成了选项

1
2
3
4
5
6
7
// set node src or bg image
if(node.hasAttribute(this._optionsAttrBg)) {
node.style.backgroundImage = 'url(' + source + ')';
}
else {
node.setAttribute('src', source);
}

用法

1
<img data-layzr="image/source" data-layzr-bg>

处理scroll和resize事件

对于滚动事件已去抖,尽量减少对浏览器的压力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Layzr.prototype._create = function() {
// fire scroll event once
this._requestScroll();

// bind scroll and resize event
window.addEventListener('scroll', this._requestScroll.bind(this), false);
window.addEventListener('resize', this._requestScroll.bind(this), false);
};

Layzr.prototype._destroy = function() {
// possibly remove attributes, and set all sources?

// unbind scroll and resize event
window.removeEventListener('scroll', this._requestScroll.bind(this), false);
window.removeEventListener('resize', this._requestScroll.bind(this), false);
};

配置项

Layzr(options)里的options

1
2
3
4
5
6
7
this._optionsSelector   = options.selector || '[data-layzr]';
this._optionsAttr = options.attr || 'data-layzr';
this._optionsAttrRetina = options.retinaAttr || 'data-layzr-retina';
this._optionsAttrBg = options.bgAttr || 'data-layzr-bg';
this._optionsAttrHidden = options.hiddenAttr || 'data-layzr-hidden';
this._optionsThreshold = options.threshold || 0;
this._optionsCallback = options.callback || null;

比jquery的插件的选项差,不过刚刚好用,保证代码足够精简

简单说明

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

源码里有2个有用的链接

// DEBOUNCE HELPERS
// adapted from: http://www.html5rocks.com/en/tutorials/speed/animations/

// OFFSET HELPER
// borrowed from: http://stackoverflow.com/questions/5598743/finding-elements-position-relative-to-the-document

欢迎关注我的公众号【node全栈】