实现一个完整的Node.js RESTful API的示例


Posted in Javascript onSeptember 29, 2017

前言

这篇文章算是对Building APIs with Node.js这本书的一个总结。用Node.js写接口对我来说是很有用的,比如在项目初始阶段,可以快速的模拟网络请求。正因为它用js写的,跟iOS直接的联系也比其他语言写的后台更加接近。

这本书写的极好,作者编码的思路极其清晰,整本书虽说是用英文写的,但很容易读懂。同时,它完整的构建了RESTful API的一整套逻辑。

我更加喜欢写一些函数响应式的程序,把函数当做数据或参数进行传递对我有着莫大的吸引力。

从程序的搭建,到设计错误捕获机制,再到程序的测试任务,这是一个完整的过程。这边文章将会很长,我会把每个核心概念的代码都黏贴上来。

环境搭建

下载并安装Node.js https://nodejs.org/en/

安装npm

下载演示项目

git clone https://github.com/agelessman/ntask-api

进入项目文件夹后运行

npm install

上边命令会下载项目所需的插件,然后启动项目

npm start

访问接口文档 http://localhost:3000/apidoc

程序入口

Express 这个框架大家应该都知道,他提供了很丰富的功能,我在这就不做解释了,先看该项目中的代码:

import express from "express"
import consign from "consign"

const app = express();

/// 在使用include或者then的时候,是有顺序的,如果传入的参数是一个文件夹
/// 那么他会按照文件夹中文件的顺序进行加载
consign({verbose: false})
 .include("libs/config.js")
 .then("db.js")
 .then("auth.js")
 .then("libs/middlewares.js")
 .then("routers")
 .then("libs/boot.js")
 .into(app);

module.exports = app;

不管是models,views还是routers都会经过 Express 的加工和配置。在该项目中并没有使用到views的地方。 Express 通过app对整个项目的功能进行配置,但我们不能把所有的参数和方法都写到这一个文件之中,否则当项目很大的时候将急难维护。

我使用Node.js的经验是很少的,但上面的代码给我的感觉就是极其简洁,思路极其清晰,通过 consign 这个模块导入其他模块在这里就让代码显得很优雅。

@note:导入的顺序很重要。

在这里,app的使用很像一个全局变量,这个我们会在下边的内容中展示出来,按序导入后,我们就可以通过这样的方式访问模块的内容了:

app.db
app.auth
app.libs....

模型设计

在我看来,在开始做任何项目前,需求分析是最重要的,经过需求分析后,我们会有一个关于代码设计的大的概念。

编码的实质是什么?我认为就是数据的存储和传递,同时还需要考虑性能和安全的问题

因此我们第二部的任务就是设计数据模型,同时可以反应出我们需求分析的成果。在该项目中有两个模型, User 和 Task ,每一个 task 对应一个 user ,一个 user 可以有多个 task

用户模型:

import bcrypt from "bcrypt"

module.exports = (sequelize, DataType) => {
 "use strict";
 const Users = sequelize.define("Users", {
  id: {
   type: DataType.INTEGER,
   primaryKey: true,
   autoIncrement: true
  },
  name: {
   type: DataType.STRING,
   allowNull: false,
   validate: {
    notEmpty: true
   }
  },
  password: {
   type: DataType.STRING,
   allowNull: false,
   validate: {
    notEmpty: true
   }
  },
  email: {
   type: DataType.STRING,
   unique: true,
   allowNull: false,
   validate: {
    notEmpty: true
   }
  }
 }, {
  hooks: {
   beforeCreate: user => {
    const salt = bcrypt.genSaltSync();
    user.password = bcrypt.hashSync(user.password, salt);
   }
  }
 });
 Users.associate = (models) => {
  Users.hasMany(models.Tasks);
 };
 Users.isPassword = (encodedPassword, password) => {
  return bcrypt.compareSync(password, encodedPassword);
 };

 return Users;
};

任务模型:

module.exports = (sequelize, DataType) => {
 "use strict";
 const Tasks = sequelize.define("Tasks", {
  id: {
   type: DataType.INTEGER,
   primaryKey: true,
   autoIncrement: true
  },
  title: {
   type: DataType.STRING,
   allowNull: false,
   validate: {
    notEmpty: true
   }
  },
  done: {
   type: DataType.BOOLEAN,
   allowNull: false,
   defaultValue: false
  }
 });
 Tasks.associate = (models) => {
  Tasks.belongsTo(models.Users);
 };
 return Tasks;
};

该项目中使用了系统自带的 sqlite 作为数据库,当然也可以使用其他的数据库,这里不限制是关系型的还是非关系型的。为了更好的管理数据,我们使用 sequelize 这个模块来管理数据库。

为了节省篇幅,这些模块我就都不介绍了,在google上一搜就出来了。在我看的Node.js的开发中,这种ORM的管理模块有很多,比如说对 MongoDB 进行管理的 mongoose 。很多很多,他们主要的思想就是Scheme。

在上边的代码中,我们定义了模型的输出和输入模板,同时对某些特定的字段进行了验证,因此在使用的过程中就有可能会产生来自数据库的错误,这些错误我们会在下边讲解到。

Tasks.associate = (models) => {
  Tasks.belongsTo(models.Users);
};

Users.associate = (models) => {
 Users.hasMany(models.Tasks);
};
Users.isPassword = (encodedPassword, password) => {
 return bcrypt.compareSync(password, encodedPassword);
};

hasMany 和 belongsTo 表示一种关联属性, Users.isPassword 算是一个类方法。 bcrypt 模块可以对密码进行加密编码。

数据库

在上边我们已经知道了,我们使用 sequelize 模块来管理数据库。其实,在最简单的层面而言,数据库只需要给我们数据模型就行了,我们拿到这些模型后,就能够根据不同的需求,去完成各种各样的CRUD操作。

import fs from "fs"
import path from "path"
import Sequelize from "sequelize"

let db = null;


module.exports = app => {
 "use strict";
 if (!db) {
  const config = app.libs.config;
  const sequelize = new Sequelize(
   config.database,
   config.username,
   config.password,
   config.params
  );

  db = {
   sequelize,
   Sequelize,
   models: {}
  };

  const dir = path.join(__dirname, "models");

  fs.readdirSync(dir).forEach(file => {
   const modelDir = path.join(dir, file);
   const model = sequelize.import(modelDir);
   db.models[model.name] = model;
  });

  Object.keys(db.models).forEach(key => {
   db.models[key].associate(db.models);
  });
 }
 return db;
};

上边的代码很简单,db是一个对象,他存储了所有的模型,在这里是 User 和 Task 。通过 sequelize.import 获取模型,然后又调用了之前写好的associate方法。

上边的函数调用之后呢,返回db,db中有我们需要的模型,到此为止,我们就建立了数据库的联系,作为对后边代码的一个支撑。

CRUD

CRUD在router中,我们先看看 router/tasks.js 的代码:

module.exports = app => {
 "use strict";
 const Tasks = app.db.models.Tasks;

 app.route("/tasks")
  .all(app.auth.authenticate())

  .get((req, res) => {
   console.log(`req.body: ${req.body}`);
   Tasks.findAll({where: {user_id: req.user.id} })
    .then(result => res.json(result))
    .catch(error => {
     res.status(412).json({msg: error.message});
    });
  })

  .post((req, res) => {
   req.body.user_id = req.user.id;
   Tasks.create(req.body)
    .then(result => res.json(result))
    .catch(error => {
     res.status(412).json({msg: error.message});
    });
  });

 app.route("/tasks/:id")
  .all(app.auth.authenticate())

  .get((req, res) => {
   Tasks.findOne({where: {
    id: req.params.id,
    user_id: req.user.id
   }})
    .then(result => {
     if (result) {
      res.json(result);
     } else {
      res.sendStatus(412);
     }
    })
    .catch(error => {
     res.status(412).json({msg: error.message});
    });
  })

  .put((req, res) => {
   Tasks.update(req.body, {where: {
    id: req.params.id,
    user_id: req.user.id
   }})
    .then(result => res.sendStatus(204))
    .catch(error => {
     res.status(412).json({msg: error.message});
    });
  })

  .delete((req, res) => {
   Tasks.destroy({where: {
    id: req.params.id,
    user_id: req.user.id
   }})
    .then(result => res.sendStatus(204))
    .catch(error => {
     res.status(412).json({msg: error.message});
    });
  });
};

再看看 router/users.js 的代码:

module.exports = app => {
 "use strict";
 const Users = app.db.models.Users;

 app.route("/user")
  .all(app.auth.authenticate())

 .get((req, res) => {
   Users.findById(req.user.id, {
    attributes: ["id", "name", "email"]
   })
    .then(result => res.json(result))
    .catch(error => {
     res.status(412).json({msg: error.message});
    });
  })

  .delete((req, res) => {
  console.log(`delete..........${req.user.id}`);
   Users.destroy({where: {id: req.user.id}})
    .then(result => {
     console.log(`result: ${result}`);
     return res.sendStatus(204);
    })
    .catch(error => {
     console.log(`resultfsaddfsf`);
     res.status(412).json({msg: error.message});
    });
  });

 app.post("/users", (req, res) => {
  Users.create(req.body)
   .then(result => res.json(result))
   .catch(error => {
    res.status(412).json({msg: error.message});
   });
 });
};

这些路由写起来比较简单,上边的代码中,基本思想就是根据模型操作CRUD,包括捕获异常。但是额外的功能是做了authenticate,也就是授权操作。

这一块好像没什么好说的,基本上都是固定套路。

授权

在网络环境中,不能老是传递用户名和密码。这时候就需要一些授权机制,该项目中采用的是JWT授权(JSON Wbb Toknes),有兴趣的同学可以去了解下这个授权,它也是按照一定的规则生成token。

因此对于授权而言,最核心的部分就是如何生成token。

import jwt from "jwt-simple"

module.exports = app => {
 "use strict";
 const cfg = app.libs.config;
 const Users = app.db.models.Users;

 app.post("/token", (req, res) => {
  const email = req.body.email;
  const password = req.body.password;
  if (email && password) {
   Users.findOne({where: {email: email}})
    .then(user => {
     if (Users.isPassword(user.password, password)) {
      const payload = {id: user.id};
      res.json({
       token: jwt.encode(payload, cfg.jwtSecret)
      });
     } else {
      res.sendStatus(401);
     }
    })
    .catch(error => res.sendStatus(401));
  } else {
   res.sendStatus(401);
  }
 });
};

上边代码中,在得到邮箱和密码后,再使用 jwt-simple 模块生成一个token。

JWT在这也不多说了,它由三部分组成,这个在它的官网中解释的很详细。

我觉得老外写东西一个最大的优点就是文档很详细。要想弄明白所有组件如何使用,最好的方法就是去他们的官网看文档,当然这要求英文水平还可以。

授权一般分两步:

  • 生成token
  • 验证token

如果从前端传递一个token过来,我们怎么解析这个token,然后获取到token里边的用户信息呢?

import passport from "passport";
import {Strategy, ExtractJwt} from "passport-jwt";

module.exports = app => {
 const Users = app.db.models.Users;
 const cfg = app.libs.config;
 const params = {
  secretOrKey: cfg.jwtSecret,
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()
 };
 var opts = {};
 opts.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme("JWT");
 opts.secretOrKey = cfg.jwtSecret;

 const strategy = new Strategy(opts, (payload, done) => {
  Users.findById(payload.id)
   .then(user => {
    if (user) {
     return done(null, {
      id: user.id,
      email: user.email
     });
    }
    return done(null, false);
   })
   .catch(error => done(error, null));
 });
 passport.use(strategy);

 return {
  initialize: () => {
   return passport.initialize();
  },
  authenticate: () => {
   return passport.authenticate("jwt", cfg.jwtSession);
  }
 };
};

这就用到了 passport 和 passport-jwt 这两个模块。 passport 支持很多种授权。不管是iOS还是Node中,验证都需要指定一个策略,这个策略是最灵活的一层。

授权需要在项目中提前进行配置,也就是初始化, app.use(app.auth.initialize()); 。

如果我们想对某个接口进行授权验证,那么只需要像下边这么用就可以了:

.all(app.auth.authenticate())

.get((req, res) => {
 console.log(`req.body: ${req.body}`);
 Tasks.findAll({where: {user_id: req.user.id} })
  .then(result => res.json(result))
  .catch(error => {
   res.status(412).json({msg: error.message});
  });
})

配置

Node.js中一个很有用的思想就是middleware,我们可以利用这个手段做很多有意思的事情:

import bodyParser from "body-parser"
import express from "express"
import cors from "cors"
import morgan from "morgan"
import logger from "./logger"
import compression from "compression"
import helmet from "helmet"

module.exports = app => {
 "use strict";
 app.set("port", 3000);
 app.set("json spaces", 4);
 console.log(`err ${JSON.stringify(app.auth)}`);
 app.use(bodyParser.json());
 app.use(app.auth.initialize());
 app.use(compression());
 app.use(helmet());
 app.use(morgan("common", {
  stream: {
   write: (message) => {
    logger.info(message);
   }
  }
 }));
 app.use(cors({
  origin: ["http://localhost:3001"],
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"]
 }));
 app.use((req, res, next) => {
  // console.log(`header: ${JSON.stringify(req.headers)}`);
  if (req.body && req.body.id) {
   delete req.body.id;
  }
  next();
 });

 app.use(express.static("public"));
};

上边的代码中包含了很多新的模块,app.set表示进行设置,app.use表示使用middleware。

测试

写测试代码是我平时很容易疏忽的地方,说实话,这么重要的部分不应该被忽视。

import jwt from "jwt-simple"

describe("Routes: Users", () => {
 "use strict";
 const Users = app.db.models.Users;
 const jwtSecret = app.libs.config.jwtSecret;
 let token;

 beforeEach(done => {
  Users
   .destroy({where: {}})
   .then(() => {
    return Users.create({
     name: "Bond",
     email: "Bond@mc.com",
     password: "123456"
    });
   })
   .then(user => {
    token = jwt.encode({id: user.id}, jwtSecret);
    done();
   });
 });

 describe("GET /user", () => {
  describe("status 200", () => {
   it("returns an authenticated user", done => {
    request.get("/user")
     .set("Authorization", `JWT ${token}`)
     .expect(200)
     .end((err, res) => {
      expect(res.body.name).to.eql("Bond");
      expect(res.body.email).to.eql("Bond@mc.com");
      done(err);
     });
   });
  });
 });

 describe("DELETE /user", () => {
  describe("status 204", () => {
   it("deletes an authenticated user", done => {
    request.delete("/user")
     .set("Authorization", `JWT ${token}`)
     .expect(204)
     .end((err, res) => {
      console.log(`err: ${err}`);
      done(err);
     });
   });
  });
 });

 describe("POST /users", () => {
  describe("status 200", () => {
   it("creates a new user", done => {
    request.post("/users")
     .send({
      name: "machao",
      email: "machao@mc.com",
      password: "123456"
     })
     .expect(200)
     .end((err, res) => {
      expect(res.body.name).to.eql("machao");
      expect(res.body.email).to.eql("machao@mc.com");
      done(err);
     });
   });
  });
 });
});

测试主要依赖下边的这几个模块:

import supertest from "supertest"
import chai from "chai"
import app from "../index"

global.app = app;
global.request = supertest(app);
global.expect = chai.expect;

其中 supertest 用来发请求的, chai 用来判断是否成功。

使用 mocha 测试框架来进行测试:

"test": "NODE_ENV=test mocha test/**/*.js",

生成接口文档

接口文档也是很重要的一个环节,该项目使用的是 ApiDoc.js 。这个没什么好说的,直接上代码:

/**
 * @api {get} /tasks List the user's tasks
 * @apiGroup Tasks
 * @apiHeader {String} Authorization Token of authenticated user
 * @apiHeaderExample {json} Header
 * {
 *  "Authorization": "xyz.abc.123.hgf"
 * }
 * @apiSuccess {Object[]} tasks Task list
 * @apiSuccess {Number} tasks.id Task id
 * @apiSuccess {String} tasks.title Task title
 * @apiSuccess {Boolean} tasks.done Task is done?
 * @apiSuccess {Date} tasks.updated_at Update's date
 * @apiSuccess {Date} tasks.created_at Register's date
 * @apiSuccess {Number} tasks.user_id The id for the user's
 * @apiSuccessExample {json} Success
 * HTTP/1.1 200 OK
 * [{
 *  "id": 1,
 *  "title": "Study",
 *  "done": false,
 *  "updated_at": "2016-02-10T15:46:51.778Z",
 *  "created_at": "2016-02-10T15:46:51.778Z",
 *  "user_id": 1
 * }]
 * @apiErrorExample {json} List error
 * HTTP/1.1 412 Precondition Failed
 */
 
 /**
 * @api {post} /users Register a new user
 * @apiGroup User
 * @apiParam {String} name User name
 * @apiParam {String} email User email
 * @apiParam {String} password User password
 * @apiParamExample {json} Input
 * {
 *  "name": "James",
 *  "email": "James@mc.com",
 *  "password": "123456"
 * }
 * @apiSuccess {Number} id User id
 * @apiSuccess {String} name User name
 * @apiSuccess {String} email User email
 * @apiSuccess {String} password User encrypted password
 * @apiSuccess {Date} update_at Update's date
 * @apiSuccess {Date} create_at Rigister's date
 * @apiSuccessExample {json} Success
 * {
 *  "id": 1,
 *  "name": "James",
 *  "email": "James@mc.com",
 *  "updated_at": "2016-02-10T15:20:11.700Z",
 *  "created_at": "2016-02-10T15:29:11.700Z"
 * }
 * @apiErrorExample {json} Rergister error
 * HTTP/1.1 412 Precondition Failed
 */

大概就类似与上边的样子,既可以做注释用,又可以自动生成文档,一石二鸟,我就不上图了。

准备发布

到了这里,就只剩下发布前的一些操作了,

有的时候,处于安全方面的考虑,我们的API可能只允许某些域名的访问,因此在这里引入一个强大的模块 cors ,介绍它的文章,网上有很多,大家可以直接搜索,在该项目中是这么使用的:

app.use(cors({
 origin: ["http://localhost:3001"],
 methods: ["GET", "POST", "PUT", "DELETE"],
 allowedHeaders: ["Content-Type", "Authorization"]
}));

这个设置在本文的最后的演示网站中,会起作用。

打印请求日志同样是一个很重要的任务,因此引进了 winston 模块。下边是对他的配置:

import fs from "fs"
import winston from "winston"

if (!fs.existsSync("logs")) {
 fs.mkdirSync("logs");
}

module.exports = new winston.Logger({
 transports: [
  new winston.transports.File({
   level: "info",
   filename: "logs/app.log",
   maxsize: 1048576,
   maxFiles: 10,
   colorize: false
  })
 ]
});

打印的结果大概是这样的:

{"level":"info","message":"::1 - - [26/Sep/2017:11:16:23 +0000] \"GET /tasks HTTP/1.1\" 200 616\n","timestamp":"2017-09-26T11:16:23.089Z"}
{"level":"info","message":"::1 - - [26/Sep/2017:11:16:43 +0000] \"OPTIONS /user HTTP/1.1\" 204 0\n","timestamp":"2017-09-26T11:16:43.583Z"}
{"level":"info","message":"Tue Sep 26 2017 19:16:43 GMT+0800 (CST) Executing (default): SELECT `id`, `name`, `password`, `email`, `created_at`, `updated_at` FROM `Users` AS `Users` WHERE `Users`.`id` = 342;","timestamp":"2017-09-26T11:16:43.592Z"}
{"level":"info","message":"Tue Sep 26 2017 19:16:43 GMT+0800 (CST) Executing (default): SELECT `id`, `name`, `email` FROM `Users` AS `Users` WHERE `Users`.`id` = 342;","timestamp":"2017-09-26T11:16:43.596Z"}
{"level":"info","message":"::1 - - [26/Sep/2017:11:16:43 +0000] \"GET /user HTTP/1.1\" 200 73\n","timestamp":"2017-09-26T11:16:43.599Z"}
{"level":"info","message":"::1 - - [26/Sep/2017:11:16:49 +0000] \"OPTIONS /user HTTP/1.1\" 204 0\n","timestamp":"2017-09-26T11:16:49.658Z"}
{"level":"info","message":"Tue Sep 26 2017 19:16:49 GMT+0800 (CST) Executing (default): SELECT `id`, `name`, `password`, `email`, `created_at`, `updated_at` FROM `Users` AS `Users` WHERE `Users`.`id` = 342;","timestamp":"2017-09-26T11:16:49.664Z"}
{"level":"info","message":"Tue Sep 26 2017 19:16:49 GMT+0800 (CST) Executing (default): DELETE FROM `Users` WHERE `id` = 342","timestamp":"2017-09-26T11:16:49.669Z"}
{"level":"info","message":"::1 - - [26/Sep/2017:11:16:49 +0000] \"DELETE /user HTTP/1.1\" 204 -\n","timestamp":"2017-09-26T11:16:49.714Z"}
{"level":"info","message":"::1 - - [26/Sep/2017:11:17:04 +0000] \"OPTIONS /token HTTP/1.1\" 204 0\n","timestamp":"2017-09-26T11:17:04.905Z"}
{"level":"info","message":"Tue Sep 26 2017 19:17:04 GMT+0800 (CST) Executing (default): SELECT `id`, `name`, `password`, `email`, `created_at`, `updated_at` FROM `Users` AS `Users` WHERE `Users`.`email` = 'xiaoxiao@mc.com' LIMIT 1;","timestamp":"2017-09-26T11:17:04.911Z"}
{"level":"info","message":"::1 - - [26/Sep/2017:11:17:04 +0000] \"POST /token HTTP/1.1\" 401 12\n","timestamp":"2017-09-26T11:17:04.916Z"}

性能上,我们使用Node.js自带的cluster来利用机器的多核,代码如下:

import cluster from "cluster"
import os from "os"

const CPUS = os.cpus();

if (cluster.isMaster) {
 // Fork
 CPUS.forEach(() => cluster.fork());

 // Listening connection event
 cluster.on("listening", work => {
  "use strict";
  console.log(`Cluster ${work.process.pid} connected`);
 });

 // Disconnect
 cluster.on("disconnect", work => {
  "use strict";
  console.log(`Cluster ${work.process.pid} disconnected`);
 });

 // Exit
 cluster.on("exit", worker => {
  "use strict";
  console.log(`Cluster ${worker.process.pid} is dead`);
  cluster.fork();
 });

} else {
 require("./index");
}

在数据传输上,我们使用 compression 模块对数据进行了gzip压缩,这个使用起来比较简单:

app.use(compression());

最后,让我们支持https访问,https的关键就在于证书,使用授权机构的证书是最好的,但该项目中,我们使用http://www.selfsignedcertificate.com这个网站自动生成了一组证书,然后启用https的服务:

import https from "https"
import fs from "fs"

module.exports = app => {
 "use strict";
 if (process.env.NODE_ENV !== "test") {

  const credentials = {
   key: fs.readFileSync("44885970_www.localhost.com.key", "utf8"),
   cert: fs.readFileSync("44885970_www.localhost.com.cert", "utf8")
  };

  app.db.sequelize.sync().done(() => {

   https.createServer(credentials, app)
    .listen(app.get("port"), () => {
    console.log(`NTask API - Port ${app.get("port")}`);
   });
  });
 }
};

当然,处于安全考虑,防止攻击,我们使用了 helmet 模块:

app.use(helmet());

前端程序

为了更好的演示该API,我把前段的代码也上传到了这个仓库https://github.com/agelessman/ntaskWeb,直接下载后,运行就行了。

API的代码连接https://github.com/agelessman/ntask-api

总结

我觉得这本书写的非常好,我收获很多。它虽然并不复杂,但是该有的都有了,因此我可以自由的往外延伸。同时也学到了作者驾驭代码的能力。

我觉得我还达不到把所学所会的东西讲明白。有什么错误的地方,还请给予指正。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery EasyUI 中文API Button使用实例
Apr 14 Javascript
Jquery 扩展方法
May 06 Javascript
验证码按回车不变解决方法
Mar 29 Javascript
js 自定义个性下拉选择框示例
Aug 20 Javascript
js由下向上不断上升冒气泡效果实例
May 07 Javascript
Vue.Js中的$watch()方法总结
Mar 23 Javascript
vue如何获取点击事件源的方法
Aug 10 Javascript
Angular CLI 安装和使用教程
Sep 13 Javascript
angular json对象push到数组中的方法
Feb 27 Javascript
Vue之mixin全局的用法详解
Aug 22 Javascript
jQuery三组基本动画与自定义动画操作实例总结
May 09 jQuery
解决vue无法侦听数组及对象属性的变化问题
Jul 17 Javascript
jquery鼠标悬停导航下划线滑出效果
Sep 29 #jQuery
vue axios同步请求解决方案
Sep 29 #Javascript
IntersectionObserver实现图片懒加载的示例
Sep 29 #Javascript
Grunt针对静态文件的压缩,版本控制打包的实例讲解
Sep 29 #Javascript
jQuery选择器之子元素过滤选择器
Sep 28 #jQuery
微信禁止下拉查看URL的处理方法
Sep 28 #Javascript
jQuery选择器之属性过滤选择器详解
Sep 28 #jQuery
You might like
自定义min版smarty模板引擎MinSmarty.class.php文件及用法
2016/05/20 PHP
Yii框架中jquery表单验证插件用法示例
2016/10/18 PHP
php socket通信简单实现
2016/11/18 PHP
在PHP 7下安装Swoole与Yar,Yaf的方法教程
2017/06/02 PHP
YII框架学习笔记之命名空间、操作响应与视图操作示例
2019/04/30 PHP
jquery 可拖拽的窗体控件实现代码
2010/03/21 Javascript
腾讯与新浪的通过IP地址获取当前地理位置(省份)的接口
2010/07/26 Javascript
查看图片(前进后退)功能实现js代码
2013/04/24 Javascript
Js nodeType 属性全面解析
2013/11/14 Javascript
基于jquery异步传输json数据格式实例代码
2013/11/23 Javascript
BOOTSTRAP时间控件显示在模态框下面的bug修复
2015/02/05 Javascript
jQuery实现的超链接提示效果示例【附demo源码下载】
2016/09/09 Javascript
Three.js获取鼠标点击的三维坐标示例代码
2017/03/24 Javascript
AngularJS使用ng-class动态增减class样式的方法示例
2017/05/18 Javascript
JavaScript 有用的代码片段和 trick
2018/02/22 Javascript
vue中的 $slot 获取插槽的节点实例
2019/11/12 Javascript
实例讲解React 组件
2020/07/07 Javascript
ES2020让代码更优美的运算符 (?.) (??)
2021/01/04 Javascript
python的scikit-learn将特征转成one-hot特征的方法
2018/07/10 Python
python根据list重命名文件夹里的所有文件实例
2018/10/25 Python
对python中Json与object转化的方法详解
2018/12/31 Python
django-orm F对象的使用 按照两个字段的和,乘积排序实例
2020/05/18 Python
keras的三种模型实现与区别说明
2020/07/03 Python
celery在python爬虫中定时操作实例讲解
2020/11/27 Python
Python爬虫之Selenium多窗口切换的实现
2020/12/04 Python
详解Pycharm第三方库的安装及使用方法
2020/12/29 Python
Ryderwear澳洲官网:澳大利亚高端健身训练装备品牌
2018/09/18 全球购物
英国剑桥包中文官网:The Cambridge Satchel Company中国
2018/11/06 全球购物
程序运行正确, 但退出时却"core dump"了,怎么回事
2014/02/19 面试题
查询优化的一般准则有哪些
2015/03/08 面试题
自荐书模板
2013/12/15 职场文书
个人自荐书
2013/12/20 职场文书
心理学专业大学生职业生涯规划范文
2014/02/19 职场文书
监督检查工作方案
2014/05/28 职场文书
2016年“6.26”禁毒宣传月系列活动总结
2016/04/05 职场文书
十大最强飞行系宝可梦,BUG燕上榜,第二是飞行系王者
2022/03/18 日漫