nodejs插件的学习笔记
nodejs插件的学习笔记
前端届的科比 发表于3年前
nodejs插件的学习笔记
  • 发表于 3年前
  • 阅读 151
  • 收藏 0
  • 点赞 0
  • 评论 0

腾讯云 新注册用户 域名抢购1元起>>>   

摘要: 学习nodejs的插件的使用和技巧

###async

并发连接数控制。

mapLimit(arr, limit, iterator, callback)接口, callback是每次调用iterator第二个参数,必须调用一下,不然只会执行到limit的次数,而不是arr.length的次数,callback执行时的第2个参数就是每次iterator调用时传入参数的数组,使用示例:

async.mapLimit(urls, 5, function (url, callback) {
  //fetchUrl(url, callback);
  callback(null, 'this arg is result, url: '+ url);
}, function (err, result) {
  console.log('final:');
  console.log(result);
});

bcrypt

加盐用法

  // 加密算法的强度,默认为10
  var SALT_FACTOR = 10;
  bcrypt.genSalt(SALT_FACTOR, function(err, salt){
    if(err){
      return next(err);
    }
    bcrypt.hash(user.password, salt, function(err, hash){
      if(err){
        return next(err);
      }
      user.password = hash;
      next(null);
    });
  });

对比密码方法

  bcrypt.compare(password, this.password, function(err, isMatch){
    if(err){
      return cb(err);
    }
    cb(null, isMatch);
  });

###benchmark

基准测试。<http://jsperf.com/>

可以分享benchmark。

npm安装:npm i --save benchmark

使用示例:

var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;
var number = '100';

// 添加测试
suite
.add('+', function() {
  int1(number);
})
.add('parseInt', function() {
  int2(number);
})
.add('Number', function () {
  int3(number);
})

// 每个测试跑完后,输出信息
.on('cycle', function(event) {
  console.log(String(event.target));
})

.on('complete', function() {
  console.log('Fastest is ' + this.filter('fastest').pluck('name'));
})

// 这里的 async 不是 mocha 测试那个 async 的意思,这个选项与它的时间计算有关,默认勾上就好了。
.run({ 'async': true });

bower

  • bower安装bootstrap:bower install bootstrap

  • bower init生成配置文件bower.json

  • 自定义bower安装目录: 1、在工作目录下新建.bowerrc文件 2、内容为:

    {
      "directory": "public/libs"
    }
    

    这样bower安装Bootstrap、jquery都会在上述目录安装

chai

官网地址:<http://chaijs.com/>。chai是个断言库,可以导出如should:var should = chai.should();

chai有多种接口选择如:

should

  chai.should();
  foo.should.be.a('string');
  foo.should.equal('bar');
  foo.should.have.length(3);
  tea.should.have.property('flavors')
    .with.length(3);

expect

  var expect = chai.expect;
  expect(foo).to.be.a('string');
  expect(foo).to.equal('bar');
  expect(foo).to.have.length(3);
  expect(tea).to.have.property('flavors')
    .with.length(3);

assert

  var assert = chai.assert;
  assert.typeOf(foo, 'string');
  assert.equal(foo, 'bar');
  assert.lengthOf(foo, 3)
  assert.property(tea, 'flavors');
  assert.lengthOf(tea.flavors, 3);

###cheerio

可以理解为Node.js版的jQuery。使用示例(与superagent结合使用):

superagent.get('https://cnodejs.org')
  .end(function(err, sres){
    if(err){return;}
    var $ = cheerio.load(sres.text);
    var cont = $('.cont').html();
  });

###connect-mongo

中间件。可用作mongodb的会话持久化

var mongoStore = require('connect-mongo')(express);

app.use(express.session({
  secrect: 'imooc',
  store: new mongoStore({
    url: 'mongodb://localhost/imooc',
    collection: 'sessions' // 指定存到mongodb里的collection
  })
}));

###eventproxy

控制并发。

重复异步协作例子:在异步操作中,我们需要在所有异步调用结束后,执行某些操作。

var ep = new EventProxy();
ep.after('got_file', files.length, function(list) {
  // 在所有文件的异步执行结束后将被执行
  // 所有文件的内容都存在list数组中
});

for (var i = 0; i < files.length; i++) {
  fs.readFile(files[i], 'utf-8', function(err, content) {
    // 触发结果事件
    ep.emit('got_file', content);
  });
}

###express

express的表单上传文件

1、引入express中间件处理:app.use(express.multipart); (可能现在这个中间件已抽离出来了) 2、form表单声明指定支持多种文件上传

<form enctype="multipart/form-data">
  <input type="file" name="upload">

3、服务器端用req.files来读取:

var uploadData = req.files.

利用mongodb做会话的持久化

// 先引入connect-mongo,cookie-parser等
// 外面再安装on-headers模块
var cookieParser = require('cookie-parser');
var expressSession = require('express-session');
var mongoStore = require('connect-mongo')(expressSession);

// 引用express.cookieParse
app.use(cookieParse());

// express.session
app.use(expressSession({
  secret: 'imooc',
  saveUninitialized: false, // don't create session until something stored
  resave: false, //don't save session if unmodified
  store: new mongoStore({
    url: 'mongodb://localhost/imooc',
    collection: 'sessions'
  })
}));

// 用法是把数据保存在req.session里
app.get('/', function(req, res){
  req.session.uer = req.body.user;
});

app.get('/admin/userlist', user.signinRequired, user.adminRequired, user.list); 可以直接传递执行方法,然后每个方法通过是否调用next()方法来执行下一个,从而达到验证的效果。如上的执行是:要查看admin的管理者列表,必须先登录,登录的用户必须是管理者,才加载用户的list。权限控制需要用到这点!!

预处理示例:

  // prev handler
  app.use(function(req, res, next){
    // do something ...
    next();
  });

指定静态资源

  app.use(express.static(path.join(__dirname, 'bower_components')));

添加本地变量

app.locals.moment = require('moment');

这样在express使用的jade的模板里也能使用moment了。

gulp

全局安装还需本地安装

安装gulp需要全局安装:npm i -g gulp后,再本地安装一次:npm i gulp --save-dev

具体原因可参考: why-do-we-need-to-install-gulp-globally-and-locally what-is-the-point-of-double-install-in-gulp

总结起来就是:

  • 能控制版本,不然开发时都不知道使用的gulp是哪个版本
  • gulp是有点unusual,开发时需要全局的gulp传递给本地的gulp的权限
  • 如果安装在本地了,那为什么还需安装在全局?是为了在系统路径能找到gulp来运行,可能与上面说的权限传递有关吧

如果不想本地再安装的话,可以使用:npm link gulp,它会立刻把全局的gulp链接过来,但貌似这不是太推荐。

mocha

  • npm i grunt-mocha-test --save-dev安装支持grunt的mocha插件

  • describe描述你要测的主体是什么,it描述具体的case内容。

  • mocha init f2e生成浏览器环境执行的测试原型,结构为:

    .
    ├── index.html
    ├── mocha.css
    ├── mocha.js
    └── tests.js
    
  • .only()方法可让你只运行指定的suite/test case,而.skip()则跳过该suite/test case:

    describe('Array', function(){
      describe.only('#indexOf()', function(){
        //...
      })
    })
    
    describe('Array', function(){
      describe('#indexOf()', function(){
        it.only('should return -1 unless present', function(){
        });
        it('should return the index when present', function(){
        });
      })
    })
    

    可在应用.only的地方应用.skip

  • 指定mocha在命令行测试报告形式:mocha --reporter doc, 更多形式请参考官网。

官网地址:<http://mochajs.org/>。mocha是一个测试框架,可以让你使用自己喜爱的断言库,如should.js,node自带的_assert_模块,chai,expect.js, better-assert等等。

mocha的接口有BDD, TDD, exports口味的接口,另外也有QUnit, require

BDD

提供 describe(), context(), it(), before(), after(), beforeEach(), afterEach()。

describe('Array#index', function(){
  context('when not present', function(){
    it('should not throw an error', function(){
      (function(){
        [1,2,3].indexOf(4)
      }).should.not.throw()
    })

    it('should return -1', function(){
      [1,2,3].indexOf(4).should.equal(-1)
    })
  })
})

TDD

提供 suite(), test(), suiteSetup(), suiteTeardown(), setup(), teardown()。

suite('Array', function(){
  setup(function(){
    // 初始化执行工作...
  })

  suite('#indexOf()', function(){
    test('should return -1', function(){
      assert.equal(-1, [1,2,3].indexOf(4))
    })
  })
})

exports

特殊属性:before, beforeEach, after, afterEach object值是suites function值是test-cases

module.exports = {
  before: function(){},

  'Array': {
    '#indexOf': {
      'should return -1': function(){
        [1,2,3].indexOf(4).should.equal(-1)
      }
    }
  }
};

QUnit

扁平式写法,使用TDD的 suite() 和 test(), 但也使用BDD的before, beforeEach, after, afterEach.

function ok(expr, msg){
  if(!expr){
    throw new Error(msg)
  }
}

suite('Array')

test('#length', function(){
  var arr = [1,2,3]
  ok(arr.length === 3)
})

Require

var testCase = require('mocha').describe
var pre = require('mocha').before
var assertions = require('mocha').it
var assert = require('assert')

testCase('Array', function(){
  pre(function(){
    // ...
  });

  testCase('#indexOf()', function(){
    assertions('should return -1 when not present', function(){
      assert.equal([1,2,3].indexOf(4), -1);
    });
  });
});

mocha-phantomjs

headless 浏览器 phantomjs,提供一个命令行的前端脚本测试环境,使得脚本文件运行在浏览器环境中。使用示例:

if(window.mochaPhantomJS){mochaPhantomJS.run();}
else{mocha.run();}

然后在命令行中运行:mocha-phantomjs index.html

###mongodb

  • cat.id === joke.category,比较ID值时,由于是ObjectId类型,不能直接比较,cat.id === joke.category, 要用字符串化后的: String(cat.id) === String(joke.category)
  • show tables可以列出当前的数据表
  • db.users.update({"_id": ObjectId("objectidxxxxxx")}, {$set: {role:55}}) 手动更新数据记录
  • 27017是MongoDB实例运行的默认端口
  • 清点数据记录条数:db.movies.find({}).count()
  • 删除所有数据:db.movies.remove()
  • 比起monk,mongoskin更适合与mongodb协作。

使用示例

1、在mongodb文件夹注册工作目录:mongod --dbpath c:\node\nodetest1\data 2、在mongodb文件夹运行mongo,进入mongodb的shell命令行界面:mongo 3、创建数据库:use nodetest1 4、添加数据记录。mongodb使用JSON结构,创建记录时会自动添加_idkey和value:db.usercollection.insert({"username": "testuser1", "email": "testuser1@testdomain.com"}) db指向创建的nodetest1数据库,usercollection为自动创建的collection。 5、查看刚刚的记录:db.usercollection.find().pretty() 6、在js文件的使用:

var mongo = require('mongodb');
var monk = require('monk');
// monk获取数据库
var db = monk('localhost:27017/nodetest1');
// 使router可以访问到db
app.use(function(req, res, next){
  req.db = db;
  next();
});

// 拉去数据库数据
router.get('/userlist', function(req, res){
  var db = req.db;
  var collection = db.get('usercollection');
  collection.find({}, {}, function(e, docs){
    res.render('userlist', {
      'userlist': docs
    });
  });
});

安装方式

  • 与Nodejs并列,安装:brew install mongodb
  • 到官网下载,再解压到指定文件夹,每次通过mongod --dbpath c:\node\nodetest1\datamongo来注册目录和启动服务。

###mongoose

  • 数据库直接更新数据记录语法。update效果是把pv值加1并保存到数据库中

    Movie.update({_id: id}, {$inc: {pv: 1}}, function(err){
      if(err){
        console.log(err);
      }
    });
    
  • mongoose支持正则表达式来筛选数据:

    var query = 'keyword';
    var re = new RegExp(query+'.*', 'i');
    
    Movie.find({
      title: re
    }).exec(function(err, movies){
      //...
    });
    
  • mongoose的双向ref用法: 参考以下两个Schema的结构:

    // 电影分类 - category.schema.js
    // ref首字母大写,指向对应的Model
    {
      movies: [{type: ObjectId, ref: 'Movie'}]
    }
    
    // 电影 - movie.schema.js
    {
      category: {
        type: ObjectId,
        ref: 'Category'
      }
    }
    
  • mongoose.populate方法:

    Comment.find({movie:id})
      // populate方法,从'from'键定义的ref值和筛选的key的值来获得相应的记录
      // 从记录里再获取'name'的数据
      .populate('from', 'name')
      .populate('reply.from reply.to', 'name')
      .populate({
        path: 'movies', // movies为Schema中的一个key
        options: {
          limit: 5,
          skip: skipCount  // skipCount:跳过的数据记录数目,但现在这个功能貌似有问题,可以改为用自己的代码实现
        }
      })
      .exec(function(err, comments){})
    
  • mongoose.set('debug', true);打开debug模式

mongoose包括schema, model, document,即模式、模型、文档三种。定义示例:

// schema
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ObjectId = Schema.types.ObjectId; // 或者Schema.ObjectId
var MovieSchema = new mongoose.Schema({
  doctor: String,
  title: String,
  language: String,
  country: String,
  year: Number,
  summary: String,

  // 数组类型,里面还需定义元素格式
  reply:[{
    type: ObjectId,
    ref: 'User'
  }],

  poster: {
    type: ObjectId, // 系统生成ID类型
    ref: 'Poster' // 引用指向的数据库
  },

  meta: {
    createAt:{
      type: Date,
      default: Date.now() // 这是设置默认值的,不是一个新属性
    }
  }
});

// 全局统一设置,在save之前都会执行的动作
MovieSchema.pre('save', function(next){
  // 判断数据是否是新加的
  if(this.isNew){
    this.meta.createAt = Date.now();
  }

  // 将存储流程走下去
  next();
});


// 增加实例方法,在实例调用的方法
UserSchema.methods = {
  comparePassword: function(password, cb){
    bcrypt.compare(password, this.password, function(err, isMatch){
      if(err){
        return cb(err);
      }
      cb(null, isMatch);
    });
  }
};

// 增加静态方法,在模型调用的方法
MovieSchema.statics = {
  // 获取所有数据记录
  fetch: function(cb){
    return this.find({}).sort('meta.createAt').exec(cb);
  },

  // 通过ID获取
  findById: function(id,cb){
    return this.findOne({_id:id}).exec(cb);
  }
};

module.exports = MovieSchema;

model,数据库模型

var mongoose = require('mongoose');
var MovieSchema = require('./schema/movie');

// 编译生成模型:传入模型名字,数据库模式
var Movie = mongoose.model('Movie', MovieSchema);
module.exports = Movie;

documents, 文档实例化

var Movie = require('./models/movie');
var movie = new Moive({
  title: '机械战警',
  doctor: '帕蒂丽雅',
  year:2010
});

// 保存到数据库
movie.save(function(err){
  if(err){
    return handleError(err);
  }
});

documents, 数据库批量查询

var Moive = require('./models/movie');
app.get('/', function(req, res){
  Movie.find({})
    .exec(function(err, movies){
      // ...
    });
});

documents,数据库单条查询

var Movie = require('./models/movie');
app.get('/', function(req, res){
  Movie.findOne({_id: id})
    .exec(function(err, movie){
      // ...
    });
});

documents, 单条数据删除

Movie.remove({_id: id}, function(err, movie){});

在运行模块中的使用:

// 只引入mongoose, 不用引入mongodb了
var mongoose = require('mongoose');
// 链接数据库
mongoose.connect('mongodb://localhost/imooc');

nodemon

这个库是专门调试时候使用的,它会自动检测 node.js 代码的改动,然后帮你自动重启应用。在调试时可以完全用 nodemon 命令代替 node 命令。安装命令:npm i -g nodemon

###q

promise/deferred模型的实现代表:q。Promise的基本概念:promise只有3种状态,未完成,完成fulfilled,失败rejected,状态的改变只发生一次。

  • promise的获取方法。使用例子:

      var Q = require('q');
      var defer = Q.defer();
      /**
       * 获取初始promise
       * @private
       */
      function getInitialPromise() {
        return defer.promise;
      }
    
  • defer.resolve('info');改变Promise状态为fulfilled;defer.reject('info');改变Promise状态为rejected;defer.notify('info');模拟promise进度

  • promise的then()方法接受3个函数参数,第1个处理fulfilled状态回调,第2个处理rejected,第3个处理progress, 返回值是另一个promise。对于outputPromise,initialPromise的function(success), function(error)都返回字符串将其状态由未完成改为fulfilled,调用的是outputPromise.then它自己的function(success)

    var outputPromise = getInitialPromise().then(function(success) {
      console.log(success);
      return 'success';
    }, function(error) {
      console.log(error);
      return 'error';
    }, function(progress) {
      console.log(progress);
      return 'progress';
    });
    defer.notify('in progress'); //控制台打印in progress
    defer.resolve('resolve'); //控制台打印resolve
    defer.reject('reject'); //没有输出。promise的状态只能改变一次
    
  • 当initialPromise的function(fulfilled)或function(rejected)抛出异常时,outputPromise的状态变为rejected

      var outputPromise = getInputPromise().then(function(fulfilled){
        throw new Error('fulfilled');
      },function(rejected){
        throw new Error('rejected');
      });
      outputPromise.then(function(fulfilled){
        console.log('fulfilled: ' + fulfilled);
      },function(rejected){
        console.log('rejected: ' + rejected);
      });
    
      defer.reject();     //控制台打印 rejected [Error:rejected]
      //defer.resolve(); //控制台打印 rejected [Error:fulfilled]
    
  • 当initialPromise的function(fulfilled)或function(rejected)返回promise,outputPromise就会成为这个新的promise.示例:

      var outputPromise = getInputPromise().then(function(fulfilled){
        var myDefer = Q.defer();
        fs.readFile('test.txt','utf8',function(err,data){
            if(!err && data) {
                myDefer.resolve(data);
            }
        });
    
        return myDefer.promise;
      },function(rejected){
        throw new Error('rejected');
      });
    
      outputPromise.then(function(fulfilled){
        console.log(fulfilled);
      },function(rejected){
        console.log(rejected);
      });
    
      defer.resolve(); //控制台打印出 test.txt 的内容
      //defer.reject();
    
  • 当initialPromise没设置fulfilled或rejected的回调时,会传递到outputPromise,调用其fulfilled/rejected的回调

  • 可以使用fail(function(error))来专门针对错误处理,而不是使用then(null,function(error))。

      var outputPromise = getInputPromise().then(function(fulfilled){
        return fulfilled;
      }).fail(function(error){
        console.log('fail: ' + error);
      });
    
  • 可以使用progress(function(progress))来专门针对进度信息进行处理,而不是使用 then(function(success){},function(error){},function(progress){})。

顺序执行,链式调用:由于.then()函数返回的是promise,故可以使用链式的then方法。但注意,这种用法是顺序执行,上一个Promise完成后才执行下一个,不是并行的。使用示例:

var Q = require('q');
var defer = Q.defer();

//promise链
defer.promise.then(function(username){
  return getUser(username);
}).then(function(user){
  console.log(user);
});

defer.resolve('andrew');

// 另外,利用Q也可直接链式调用:
function foo(result){
  return result+result;
}

// 手动连接
Q('hello').then(foo).then(foo).then(foo);

// 动态链接
var funcs = [foo,foo,foo];
funcs.reduce(function(prev, current){
  return prev.then(current);
}, Q('hello'));

并列执行:Q.all([promise1,promise2...])将多个promise组合成一个promise返回。注意:

1、当all里面所有的promise都fulfill时,Q.all返回的promise状态变成fulfill 2、当任意一个promise被reject时,Q.all返回的promise状态立即变成reject

现在知道Q.all会在任意一个promise进入reject状态后立即进入reject状态。如果我们需要等到所有的promise都发生状态后(有的fulfil, 有的reject),再转换Q.all的状态, 这时我们可以使用Q.allSettled

var Q = require('q');
var fs = require('fs');

function printFileContent(fileName) {
  //Todo: 这段代码不够简洁。可以使用Q.denodeify来简化
  var defer = Q.defer();
  fs.readFile(fileName, 'utf8', function(err, data) {
    if (!err && data) {
      console.log(data);
      defer.resolve(fileName + ' success ');
    } else {
      defer.reject(fileName + ' fail ');
    }
  })

  return defer.promise;
}



Q.all([printFileContent('sample01.txt'), printFileContent('sample02.txt'), printFileContent('sample03.txt'),
    printFileContent('sample04.txt')
  ])
  .then(function(success) {
    console.log(success);
  }); //控制台打印各个文件内容 顺序不一定

Q.allSettled([printFileContent('nosuchfile.txt'), printFileContent('sample02.txt'), printFileContent('sample03.txt'), printFileContent('sample04.txt')])
  .then(function(results) {
    results.forEach(
      function(result) {
        console.log(result.state);
      }
    );
  });

结束promise链,有俩种结束方式:

1、返回最后一个promise。return foo().then(bar); 2、调用.done()来结束。foo().then(bar).done();。done()函数能把链中没有被处理的错误抛出。

/**
 *没有用done()结束的promise链
 *由于getPromse('2',2000,'opt')返回rejected, getPromise('3',1000)就没有执行
 *然后这个异常并没有任何提醒,是一个潜在的bug
 */
getPromise('1', 3000)
  .then(function() {
    return getPromise('2', 2000, 'opt')
  })
  .then(function() {
    return getPromise('3', 1000)
  });
/**
 *用done()结束的promise链
 *有异常抛出
 */
getPromise('1', 3000)
  .then(function() {
    return getPromise('2', 2000, 'opt')
  })
  .then(function() {
    return getPromise('3', 1000)
  })
  .done();

###should

api docs: <http://shouldjs.github.io/>

BDD式的测试框架。

添加自定的断言 使用should.Assertion.add方法,它接受3个参数: 1、方法名 2、断言的function。只检查positive case,对于.not的情况,should会handle。里面的this值向should.Assertion的实例; 3、boolean值,to mark if this assertion should be getter

对于2的function,里面的断言在检查前,必须先定义this.params。其格式为:{operator: , actual: , expected: }。下面俩种定义的方法:

1、第一种不被喜爱的,只是个shortcuts:

Assertion.add('true', function(){
  this.is.exactly(true)
}, true)

虽然这个没明确定义this.params,但相同的assertion调用.exactly会自动填充this.params的,不过这样做要小心。

2、这种是更值得推荐的做法:

Assertion.add('true', function(){
  this.params = {operator: 'to be true', expected: true};
  this.obj.should.be.exactly(true);
}, true)

这个就是定义了this.params,因为用了.should,故是用了新的assertion上下文。更多例子:

Assertion.add('asset', function(){
  this.params = {operator: 'to be asset'}
  this.obj.should.have.property('id').which.is.a.Number
  this.obj.should.have.property('path')
})

superagent

一个http库,可以发起getpost请求,用于抓取页面。使用示例:

superagent.get('https://cnodejs.org')
  .end(function(err, sres){
    // ...
  });

supertest

专门用来配合 express (准确来说是所有兼容 connect 的 web 框架)进行集成测试的,测试getpost请求,它与superagent是孪生库,API是一样的,使用示例:

var app = require('../app');
var supertest = require('supertest');
var request = supertest(app);

describe('test/app.test.js', function(){
  it('should return 55 when n is 10', function(done){
    // 这里之所以这个测试的 function 要接受一个 done 函数,是因为我们的测试内容
    // 涉及了异步调用,而 mocha 是无法感知异步调用完成的。所以我们主动接受它提供
    // 的 done 函数,在测试完毕时,自行调用一下,以示结束。
    // mocha 可以感到到我们的测试函数是否接受 done 参数。js 中,function
    // 对象是有长度的,它的长度由它的参数数量决定
    // (function (a, b, c, d) {}).length === 4
    // 所以 mocha 通过我们测试函数的长度就可以确定我们是否是异步测试。

    request.get('/fib')
      .query({n:10})
      .end(function(err, res){
        res.text.should.equal('55');
        done();
      });
  });
});

  • supertest(app)得到的request对象,有以下俩方法: 1、query()用来传querystring 2、send()用来传body,比如{title: 'modified title', tab: '0', content: 'detail'}

  • supertest利用supertest.agent(app).set('Cookie', 'a cookie string')实现Cookie持久化

    var supertest = require('supertest');
    var app = express();
    var agent = supertest.agent(app);
    
    agent.post('login').end(...);
    // then ..
    agent.post('create_topic').end(...); // 此时的 agent 中有用户登陆后的 cookie
    
    var supertest = require('supertest');
    var userCookie;
    supertest.post('login').end(function(err, res) {
      userCookie = res.headers['Cookie']
    });
    // then ..
    
    supertest.post('create_topic')
      .set('Cookie', userCookie)
      .end(...)
    
标签: nodejs 插件 express
共有 人打赏支持
粉丝 22
博文 64
码字总数 51572
×
前端届的科比
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: