实现一个完整的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 插件 任意位置浮动固定层
Dec 25 Javascript
ext 代码生成器
Aug 07 Javascript
js单向链表的具体实现实例
Jun 21 Javascript
jQuery中dequeue()方法用法实例
Dec 29 Javascript
AngularJS中比较两个数组是否相同
Aug 24 Javascript
微信小程序 获取相册照片实例详解
Nov 16 Javascript
详解angular中如何监控dom渲染完毕
Jan 03 Javascript
利用Blob进行文件上传的完整步骤
Aug 02 Javascript
解决百度Echarts图表坐标轴越界的方法
Oct 17 Javascript
PHP实现基于Redis的MessageQueue队列封装操作示例
Feb 02 Javascript
JavaScript的变量声明与声明提前用法实例分析
Nov 26 Javascript
原生js实现随机点名
Jul 05 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
php $_SERVER windows系统与linux系统下的区别说明
2014/02/14 PHP
CodeIgniter连贯操作的底层原理分析
2016/05/17 PHP
详解Yii2 rules 的验证规则
2016/12/02 PHP
php数组和链表的区别总结
2019/09/20 PHP
发现的以前不知道的函数
2006/09/19 Javascript
用cookies实现的可记忆的样式切换效果代码下载
2007/12/24 Javascript
javascript cookie解码函数(兼容ff)
2008/03/17 Javascript
JS关键字变色实现思路及代码
2013/02/21 Javascript
JavaScript字符串对象fromCharCode方法入门实例(用于把Unicode值转换为字符串)
2014/10/17 Javascript
使用pjax实现无刷新更改页面url
2015/02/05 Javascript
JavaScript数组对象赋值用法实例
2015/08/04 Javascript
jquery 实现输入邮箱时自动补全下拉提示功能
2015/10/04 Javascript
Javascript中的Prototype到底是什么
2016/02/16 Javascript
全面了解JS中的匿名函数
2016/06/29 Javascript
基于CSS3和jQuery实现跟随鼠标方位的Hover特效
2016/07/25 Javascript
彻底学会Angular.js中的transclusion
2017/03/12 Javascript
jQuery插件FusionCharts实现的3D帕累托图效果示例【附demo源码】
2017/03/25 jQuery
基于JavaScript实现的插入排序算法分析
2017/04/14 Javascript
JavaScrip数组删除特定元素的几种方法总结
2017/09/06 Javascript
基于webpack 实用配置方法总结
2017/09/28 Javascript
javascript基于定时器实现进度条功能实例
2017/10/13 Javascript
Vue+axios+WebApi+NPOI导出Excel文件实例方法
2019/06/05 Javascript
Vue中通过属性绑定为元素绑定style行内样式的实例代码
2020/04/30 Javascript
[02:08]我的刀塔不可能这么可爱 胡晓桃_1
2014/06/20 DOTA
python游戏开发的五个案例分享
2020/03/09 Python
python如何获得list或numpy数组中最大元素对应的索引
2020/11/16 Python
全球性的在线购物网站:Zapals
2017/03/22 全球购物
迪卡侬比利时官网:Decathlon比利时
2019/12/28 全球购物
公司成立感言
2014/01/11 职场文书
员工自我工作评价
2015/03/06 职场文书
关于清明节的演讲稿2015
2015/03/18 职场文书
英文产品推荐信
2015/03/27 职场文书
实习推荐信格式模板
2015/03/27 职场文书
入党积极分子培养联系人意见
2015/08/12 职场文书
2016年社会管理综治宣传月活动总结
2016/03/16 职场文书
解决jupyter notebook启动后没有token的坑
2021/04/24 Python