feng xiaohan
iframe
<iframe
  src="https://www.example.com"
  width="100%"
  height="500"
  frameborder="0"
  allowfullscreen
  sandbox
>
  <!-- 当浏览器不支持 iframe 时,显示链接(但一般都支持) -->
  <p><a href="https://www.example.com">点击打开嵌入页面</a></p>
</iframe>

基本属性

sandbox

为了限制<iframe>的风险,HTML 提供了 sandbox 属性,允许设置嵌入的网页的权限。(提供了一个隔离层-沙箱)

sandbox 为 bool 时,表示打开所有限制。但其中具体限制也可以逐一打开:

  • allow-forms:允许提交表单。
  • allow-modals:允许提示框,即允许执行 window.alert()等会产生弹出提示框的 JavaScript 方法。
  • allow-popups:允许嵌入的网页使用 window.open()方法弹出窗口。
  • allow-popups-to-escape-sandbox:允许弹出窗口不受沙箱的限制。
  • allow-orientation-lock:允许嵌入的网页用脚本锁定屏幕的方向,即横屏或竖屏。
  • allow-pointer-lock:允许嵌入的网页使用 Pointer Lock API,锁定鼠标的移动。
  • allow-presentation:允许嵌入的网页使用 Presentation API。
  • allow-same-origin:不打开该项限制,将使得所有加载的网页都视为跨域。
  • allow-scripts:允许嵌入的网页运行脚本(但不创建弹出窗口)。
  • allow-storage-access-by-user-activation:允许在用户激动的情况下,嵌入的网页通过 Storage Access API 访问父窗口的储存。
  • allow-top-navigation:允许嵌入的网页对顶级窗口进行导航。
  • allow-top-navigation-by-user-activation:允许嵌入的网页对顶级窗口进行导航,但必须由用户激活。
  • allow-downloads-without-user-activation:允许在没有用户激活的情况下,嵌入的网页启动下载。

注意:不要同时设置 allow-scripts 和 allow-same-origin 属性,这将使得嵌入的网页可以改变或删除 sandbox 属性

loading

<iframe>指定的网页会立即加载,但我们也可以让其滚动进入视口以后再加载以节约带宽,这时就可以使用 loading 属性。

loading 属性可以触发<iframe>网页的懒加载。该属性可以取以下三个值。

  • auto:浏览器的默认行为,与不使用 loading 属性效果相同。
  • lazy:<iframe>的懒加载,即将滚动进入视口时开始加载。
  • eager:立即加载资源,无论在页面上的位置如何。
<iframe src="https://example.com" loading="lazy"></iframe>

注意:如果<iframe>是隐藏的,则 loading 属性无效,将会立即加载。
<iframe>的宽度和高度为 4 像素或更小。
样式设为 display: none 或 visibility: hidden。
使用定位坐标为负 X 或负 Y,将<iframe>放置在屏幕外。

Node-process

env

获取当前代码所有的环境变量。

cwd()

返回当前 node 进程的工作目录。也就是运行项目时敲命令的那个目录。

npm run dev 命令在那个目录下执行的。

ES Module和CommonJS同时使用

ES Modules 中使用 Common.js 规范文件

在 ES Module 中可以载入 Common.js 标准的模块。

// Common.js
module.exports = {
  foo: "commonjs exports value",
};

// exports.foo = 'commonjs exports value'; // 别名使用
// es-module.mjs
import mod from "./commonjs.js";
console.log(mod); // { foo: 'commonjs exports value' }

// import { foo } from './commonjs.js'
// console.log(foo); // commonjs exports value

在 Common.js 规范文件中使用 ES Modules

Node.js 的原生环境中是==不允许==通过 CommonJS 模块来载入 ES Modules 的

// Common.js
export const foo = "es module export value";
// es-module.mjs
const mod = require("./es-module.mjs");
console.log(mod); // error
Node中使用ES Modules规范

在 Node.js 中一般是使用的 Common.js 规范,但是从Node.js 8.5版本之后,Node.js 就已经开始去支持 ES Modules 了(实验性),从Node.js 13.2.0版本开始,Node.js 完全支持 ES Modules(ESM),所以我们可以直接在 Node.js 中使用 ES Modules。

开启使用

更改文件后缀名使用

js文件的后缀改为.mjs即可在里面使用 ES Modules 规范。

注:如果是实验特性,在执行 node 文件时则需要添加--experimental-modules

开启 type=’module’使用

该方法需要的版本为 Node 12+。

package.json里添加"type": "module"来表明该项目使用的是 ES Modules 的语法,这时就不需要将扩展名改为.mjs了。

{
  "type": "module"
}

注意:如果在开启之后还想使用 CommonJS 模块的规范,需要将文件的后缀名改为.cjs

使用 Babel 兼容早期的 Node

对于早期的 Node.js 版本,可以使用 Babel 来实现对 ES Modules 的兼容。

Babel:目前最主流的一款 JS 的编译器,它可以将当前一些使用了新特性的代码编译成当前环境支持的代码。

  • 添加 babel 所依赖的一些包:

    yarn add @babel/node @babel/core @babel/preset-env --dev
    
  • 启动模块:

    yarn babel-node index.js --presets=@babel/preset-env
    

    preset-env:其实就是插件的集合,在这个集合里包含了最新的 JS 标准中所有的新特性。

    如果觉得每次手动配置这个参数很麻烦,可以在配置文件.babelrc(JSON 格式文件)添加presets配置:

    {
      "presets": ["@babel/preset-env"]
    }
    

    启动:

    yarn babel-node index.js
    

    由于 babel 是基于插件机制去实现的,核心模块并不会去转换我们的代码,具体转换每个代码的新特新是通过插件来实现的,一个插件来转换一个代码的特性。

    但是,如果项目中只使用了某些特性,而其他不支持的特性没有使用,也可也单独下载转换特性的插件来提高效率:

    移除插件集合:

    yarn remove @babel/preset-env
    

    添加转换的插件:

    yarn add @babel/plugin-transform-modules-commonjs --dev
    

    还有别的一些转换特性的插件,例如:

    • arrow-functions:用于转换 JS 的箭头函数;
    • classes:用于转换 JS 的类;
    • destructuring:用于转换解构;
    • module-commonjs:用于转换 ES Module;

    然后配置.babelrc里的 plugins:

    {
      "plugins": ["@babel/plugin-transform-modules-commonjs"]
    }
    

    启动:

    yarn babel-node index.js
    

载入模块

可以通过 ES Modules 的方式载入模块。

  • 载入原生模块

    import fs from "fs";
    fs.writeFileSync("./foo.txt", "es module working");
    

    注意:系统内置模块的成员可以被{}提取出来,因为系统内置的模块官方都做了兼容,它会对系统内置的每一个模块的成员单独导出一次,然后再把他们作为一个对象整体做一个模块导出。

    import { writeFileSync } from "fs";
    writeFileSync("./bar.txt", "es module working");
    
  • 载入第三方模块

    前提是已经安装。

    import _ from "lodash";
    console.log(_.camelCase("ES Module"));
    

    注意:对于第三方模块,它们导出的是一个作为默认成员的对象,所以必须使用默认导入的方式去导入成员,不能使用单个命名导入。

    // import { camelCase } from 'lodash' // 不可以使用,其本身语法也和对象解构无关
    

与使用 CommonJS 规范的对比

ES Modules 里不再提供以下全局成员变量(CommonJS 里会提供)。

// commonjs.js
// CommonJS 模块内的全局成员(Node环境下)

console.log(require); // 加载模块函数
console.log(module); // 模块对象
console.log(exports); // 导出对象别名
console.log(__filename); // 当前文件的绝对路径
console.log(__dirname); // 当前文件所在目录

对于requiremoduleexports可以通过 ES Modules 中的importimport())和export来代替它们导入模块,而对于__filename__dirname可以通过import.meta.urldirname方法代替:

import { fileURLToPath } from "url";
import { dirname } from "path";

const __filename = fileURLToPath(import.meta.url); // 将拿到的文件url转换成路径
console.log(__filename);
const __dirname = dirname(__filename); // 完整的文件路径提取出来
console.log(__dirname);
  • __filename:表示当前模块文件的绝对路径,包括文件名。
  • __dirname:表示当前模块文件所在的目录的绝对路径,不包括文件名。

注:在 Node.js 中加载 CommonJS 模块里的原代码,这几个成员其实也是把当前这几个的模块包装成函数或者是形参,它实际上也是伪全局对象。(在外侧包裹一个函数,从而实现私有模块作用域)

Node模块(module)的导出导入
const requestHandler = () => {};

导出

默认导出

  • 整体模块导出

    module.exports = requestHandler;
    

    该导出方式的导出函数会作为整个文件的导出结果,不支持重命名。

  • 对象表示导出

    module.exports = {
      handler: requestHandler,
      someText: "导出的一些字符串",
    };
    

    该导出方式可以一次性导出多个变量,支持重命名。

  • 简写默认导出

    exports = {
      someText: "导出的一些字符串",
    };
    

注意默认导出的内容不会出现在全局对象exports,只有命名导出的内容才会出现。

命名导出

  • 属性表示导出

    module.exports.handler = requestHandler;
    module.exports.someText = "导出的一些字符串";
    

    该导出方式可以一条一条导出,支持重命名。

  • 属性表示导出(快捷方式)

    exports.handler = requestHandler;
    exports.someText = "导出的一些字符串";
    

注意:命名导出(exports.xxx)和默认导出(exports)不可以同时使用。如果一个模块中同时使用了命名导出和默认导出,则命名导出将被忽略,只有默认导出的内容会被导出。

导入

使用require导入我们导出函数或属性的文件:

const routes = require("./routes");
  • 对于整体模块导出的导入

    // routes就是requestHandler
    console.log(routes);
    
  • 对于其他导出的导入

    // 需要用.来对应
    console.log(routes.handler);
    console.log(routes.someText);
    
Node.js常用包

开发环境包

  • nodemon:当我们修改项目文件时,自动重启 node 执行该文件;

    没有它我们每次修改文件之后都要重新执行node app.js

    使用:在 package.json 中的运行项目命令中添加nodemon app.js,然后执行npm start(或其他启动运行项目的命令)。

    {
        ...
        "scripts": {
            "start": "nodemon app.js"
        }
        ...
    }
    

    注意:nodemon 如果不是在全局安装的就不能直接在命令行输入nodemon app.js,而是写入 start 中,用 npm(已全局安装)来启动。

  • serve :在本地启动 serve 运行文件;(一般针对 HTML)

    npm i -g serve
    
    • 启动当前文件:

      server .
      
    • 配置 package.json 后启动打包后的文件(dist):

      "serve": "serve ./dist"
      
      npm run serve
      
  • browser-sync:浏览器自动同步文件工具;

    npm install -g browser-sync
    

    完全可以使用插件 Live server 达到相同的效果。

生产环境包

  • express:Node.js 框架;

  • mysql2:连接数据库的工具包;

  • ejs:EJS 模板引擎;

  • pug:Pug 模板引擎;

  • express-handlebars:Handlebars 模板引擎;

  • sequelize:对象关系映射库,简化操作 SQL 数据库;

  • mongoose:对象文档映射库,简化操作 MongoDB 数据库;

  • express-session:Express 使用 session 的包;

  • bcryptjs:加密数据的包(加密密码,不能直接从数据库中查看到密码);

  • connect-flash:flash 是在 session 中用于储存信息的特殊区域,在其中存储的值使用过一次便被清空(联合 redirect 一起使用,确保消息只在目标页面可用);

数据验证(Validate)

为什么要验证

在用户使用表单发送数据到后端、后端将数据写入数据库的过程中,如果用户输入了一些无效的数据,可能会影响整个代码的逻辑。(避免提交数据不合法)

如何进行验证

我们可以在不同的地方对数据进行验证:

  • 在客户端进行验证(可选):在发送任何请求到服务端之前,通过 JS 监听输入事件,然后在用户进行表单输入时检查,在页面上进行反馈显示。这可以提高用户的体验

    在客户端验证是可选的,它并不安全。因为客户端使用的 JS 是在浏览器中运行的,用户可以看到、更改或禁用 JS 代码,不利于保护安全数据发送到服务器。

  • 在服务端进行验证(必须):将任何数据存储在数据库之前进行验证,保证存储数据的正确性。并且有些验证功能需要查询数据库,在服务器端进行验证能做的事要多一些。

    用户无法看到、更改或禁用代码,因为这些都发生在服务器上,而不是浏览器中。

  • 数据库内置验证(可选):大多数数据库引擎都有一个内置验证,但是如果在服务端进行了良好的验证,基本上不需要。

在 Node(Express)中进行数据验证

安装验证器

在 Node(Express)中,我们可以安装第三方验证器express-validator来辅助我们进行数据的验证:

npm install --save express-validator

进行验证

引入express-validator/check中的check()check()接收两个参数,第一个参数为作为检查对象的 name;第二个参数是触发校验错误的信息:

const { check } = require('express-validator/check');

const app = express();

app.use('/signup', check('email').isEmail(), authController.postSignup)'); // 通过isEmail()检查渲染页面中name为email的输入选项是否为有效的Email格式

自定义验证

除了直接使用已有的内置验证器函数,我们也使用custom()自定义验证器。

custom()接收一个回调函数,其中回调函数中第一个参数为需要验证的值;第二个参数为一个对象,我们可以从中提取一些信息(如请求、位置、路径):

app.use(
  "/signup",
  check("email")
    .isEmail()
    .withMessage("Please enter a valid email.")
    .custom((value, { req }) => {
      // 自定义验证逻辑
      if (value === "test@test.com") {
        throw new Error("This email address if forbidden."); // 抛出错误异常消息
      }
      return true;
    }),
  authController.postSignup
);
  • 验证再次确认密码

    我们可以用数组,同时注册验证多字段:

    app.use(
      "/signup",
      [
        check("email")
          .isEmail()
          .withMessage("Please enter a valid email.")
          .custom((value, { req }) => {
            if (value === "test@test.com") {
              throw new Error("This email address if forbidden.");
            }
            return true;
          }),
        body(
          "password",
          "Please enter a password with only numbers and text and at least 5 characters."
        )
          .isLength({ min: 5 })
          .isAlphanumeric(),
        body("confirmPassword").custom((value, { req }) => {
          if (value !== req.body.password) {
            // req.body.password获取输入请求的密码
            throw new Error("Password have to match!");
          }
          return true;
        }),
      ],
      authController.postSignup
    );
    

指定验证

在引入check()时,我们可以指定引入传入请求时的一些信息(例如 body、param、query、cookie、header 等),用法和check()相同:

const { check, body } = require('express-validator/check')
...
app.use(
  '/signup',
  [
    check('email')
      .isEmail()
      .withMessage('Please enter a valid email.')
      .custom((value, {req}) => {
        if (value === 'test@test.com') {
          throw new Error('This email address if forbidden.')
        }
        return true;
      }),
    body(
      'password',
      'Please enter a password with only numbers and text and at least 5 characters.'
      )
      .isLength({min: 5})
      .isAlphanumeric()
  ],
  authController.postSignup
);

isLength({min: 5}):最小长度为 5

isAlphanumeric():允许数字和普通字符

验证结果

引入express-validator/check中的validationResult()validationResult()中包含了所有验证的结果:

const { validationResult } = require("express-validator/check");

exports.postSignup = (req, res, next) => {
  const email = req.body.email;
  const password = req.body.password;
  const confirmPassword = req.body.confirmPassword;
  const errors = validationResult(req); // 存储使用验证中间件的请求里的验证结果
  if (!errors.isEmpty()) {
    // 检测errors中是否有错误信息
    console.log(errors.array()); // 打印错误信息数组
    return res.status(422).render("auth/signup", {
      // 返回422状态码,重新返回注册页面
      path: "/signup",
      pageTitle: "Signup",
      errorMessage: errors.array()[0].msg, // 错误信息数组里的每一项的msg为错误信息
    });
  }
};

自定义错误信息

可以在注册中间件函数时添加withMessage()来自定义错误信息:

app.use(
  "/signup",
  check("email").isEmail().withMessage("Please enter a valid email."),
  authController.postSignup
);

附录

express-validator 内置验证器函数

  • isEmail():输入是否为 Email 格式。

  • isURL():输入是否为一个 url 网址形式。

  • isFloat():输入保留一位小数。

  • isLength({}):是否限制输入长度。接收一个对象,对象内设置最长或最短字符数量。

  • withMessage():定义错误信息。

  • custom():自定义验证函数。

  • isAlphanumeric():是否数字和普通字符。

  • isString():是否是字符串形式。

  • normalizeEmail():规范化 Email 格式。

  • trim():删除多余空格。

Mongoose

简介

Mongoose 是一个ODM(对象文档映射库—Object-Document Mapping Library)

Mongoose 为模型提供了一种直接的,基于 scheme 结构去定义你的数据模型,然后通过实例化模型来生成一条数据文档;而在数据模型上可以使用各种内置的方法。

例如:

db.collection("users").insertOne({
  name: "Jon",
  age: 28,
  password: "hsdjkffds",
});

——>

const user = User.create({ name: "Jon", age: 28, password: "hsdjkffds" });

安装

npm install --save mongoose

基础使用

连接数据库

app.js

const mongoose = require('mongoose'); // 引入mongoose
...
mongoose
  .connect(
    'mongodb+srv://<user>:<password>@cluster0.mnle1m2.mongodb.net/shop?retryWrites=true&w=majority'
  ) // 连接MongoDB数据库
  .then(result => {
    app.listen(3000);
  })
  .catch(err => {
    console.log(err);
  });

创建和定义模型的 Schema

以键值对的方式来定义模型:

models/product.js

const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const productSchema = new Schema({
  // @1
  title: {
    type: String, // 类型
    required: true, // 是否必须
  },
  price: {
    type: Number,
    required: true,
  },
  description: {
    type: String,
    required: true,
  },
  imageUrl: {
    type: String,
    required: true,
  },
});

module.exports = mongoose.model("Product", productSchema); // 将导出的模块作为mongoose的model,方便日后连接 @2

@1:虽然 MongoDB 是没有特定的 schema 限制的,但通常我们在使用时知道某种数据存在某种结构(比如知道 name 数据用 String 类型);因此 mongoose 需要我们提供这样的结构,让我们只关注于数据本身,即使这样做会放弃一些数据的灵活性。

@2:第一个参数为自定义的集合名字(Mongoose 会将这个名字的开头转为小写,并在后面加上 s 作为集合的名字),第二个参数为需要导出的模块。

自定义 Schema 的方法

我们可以向定义的 Schema 中添加方法,让每个通过这个 Schema 模型创建出来的实例(数据/文档)都可以使用。

const productSchema = new Schema({
  ...
});
productSchema.methods.addToCart = function(product) {
  ...
} // 向productSchema中添加addToCart方法

向集合中新增一条数据(文档)

集合名字在导出时通过mongoose.model已经定义好了,当我们通过创建模型的实例时会添加到对应的集合中,然后通过save()来保存一条数据:

controllers/admin.js

const Product = require("../models/product");

exports.postAddProduct = (req, res, next) => {
  const title = req.body.title;
  const imageUrl = req.body.imageUrl;
  const price = req.body.price;
  const description = req.body.description;
  const product = new Product({
    // 创建模型实例并写入数据
    title: title,
    price: price,
    description: description,
    imageUrl: imageUrl,
  });
  product
    .save() // 保存一条数据
    .then((result) => {
      console.log("Created Product");
      res.redirect("/admin/products");
    })
    .catch((err) => {
      console.log(err);
    });
};

查询数据

查询所有数据

使用find()查询所有的数据:

const Product = require("../models/product");

exports.getProducts = (req, res, next) => {
  Product.find() // 查询所有数据
    .then((products) => {
      res.render("shop/product-list", {
        prods: products,
        pageTitle: "All Products",
        path: "/products",
      });
    })
    .catch((err) => console.log(err));
};

查询条件数据

  • **findById()**——根据 id 查询数据

    exports.getProduct = (req, res, next) => {
      const prodId = req.params.productId;
      Product.findById(prodId) // 根据id(_id)查询数据
        .then((product) => {
          res.render("shop/product-detail", {
            product: product,
            pageTitle: product.title,
            path: "/products",
          });
        })
        .catch((err) => console.log(err));
    };
    

更新数据

更新数据只需要找到对应数据修改后,再使用save()

exports.postEditProduct = (req, res, next) => {
  const prodId = req.body.productId;
  const updatedTitle = req.body.title;
  const updatedPrice = req.body.price;
  const updatedImageUrl = req.body.imageUrl;
  const updatedDesc = req.body.description;

  Product.findById(prodId) // 根据id找到对应数据
    .then((product) => {
      // 对数据进行修改
      product.title = updatedTitle;
      product.price = updatedPrice;
      product.description = updatedDesc;
      product.imageUrl = updatedImageUrl;
      return product.save(); // 保存新修改的数据
    })
    .then((result) => {
      console.log("UPDATED PRODUCT!");
      res.redirect("/admin/products");
    })
    .catch((err) => console.log(err));
};

删除数据

根据 id 删除数据

可以使用 Mongoose 内置的findByIdAndRemove来删除对应数据。

exports.postDeleteProduct = (req, res, next) => {
  const prodId = req.body.productId;
  Product.findByIdAndRemove(prodId) // 删除数据
    .then(() => {
      console.log("DESTROYED PRODUCT");
      res.redirect("/admin/products");
    })
    .catch((err) => console.log(err));
};

嵌入式文档

在 MongoDB 中,集合和集合之间一般是没有关联的,但可以使用嵌入式文档,将复制的关联 id 产生联系。

创建关系

如果想让一个集合和另一个集合有关系,可以使用ref来嵌入关系。

models/product.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const productSchema = new Schema({
  ...
  userId: {
    type: Schema.Types.ObjectId,
    ref: 'User', // 设置关联
    required: true
  }
});

module.exports = mongoose.model('Product', productSchema);

models/admin.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const userSchema = new Schema({
  ...
  cart: {
    items: [
      {
        productId: { type: Schema.Types.ObjectId, ref: 'Product', required: true }, // 设置关联
        quantity: { type: Number, required: true }
      }
    ]
  }
});

module.exports = mongoose.model('User', userSchema);

附录

const Product = require('..');
const product = new Product({...});

save()

向集合中保存/新增一条数据。

product.save();

find()

查询所有数据。返回一个 Promise。

Product.find();

findOne()

查询所有数据但只返回一条数据(文档)。返回一个 Promise。

Product.findOne();

findById()

根据 id(_id)查询数据。返回一个 Promise。

Product.findById(prodId);

findByIdAndRemove()

根据 id(_id)删除数据。

Product.findByIdAndRemove(prodId);

deleteOne()

根据指定信息删除一条数据。

Product.deleteOne({ _id: prodId, userId: req.user._id });

populate()

填充字段,将文档中的指定路径自动替换为来自其他集合的文档的过程。返回一个 Promise。

Product.find().populate();

select()

筛选字段,将数据中与之匹配的筛选出来。

Product.find().select();

以字符串空格的方式筛选不同字段:

Product.find().select("title price _id");
核心模块

http

帮助我们启动服务器或执行发送请求等其他任务。

const http = require("http");

导入模块,它会自动在全局模块(这是在 node.js 中自带的模块)中寻找。

createServer()

创建服务器并返回 server 对象。接收一个函数作为参数,每当有请求时就会执行这个函数。函数有两个默认参数——请求的信息,和响应的信息:

const server = http.createServer((req, res) => {
  // req:包含请求的详细信息
  console.log(req.url); // 发起请求的地址(http://localhost:3000/后的地址)
  console.log(req.method); // 请求方法(GET/POST等)
  console.log(req.headers); // 请求头信息

  // res:服务端响应回去的对象
  res.setHeader("Content-Type", "text/html"); // 设置响应头(响应的文本为HTML)
  // 写入文本信息(此处为HTML)
  res.write("<html>");
  res.write("<head><title>First page</title></head>");
  res.write("<body><h1>Hello,Node.js</h1></body>");
  res.write("</html>");

  res.end(); // 结束编写,发送响应给浏览器(此后的写入无效)
});
server.listen(3000);

注意:res.write()也可以使用模板字符串直接写入。

我们可以通过这个返回的 server 对象来对 server 进行操作:

  • **listen()**:确保 node.js 不会立刻退出我们的代码,而是通过listen()保留它的进程。listen()有几个参数:

    第一个参数:需要监听的端口号;

    一般在生产环境中不需要填写,默认端口为 80;如果本地开发则可能需要不同的端口。

    server.listen(3000);
    

    现在启动 node.js 执行文件后,node.js 可以监听到访问本地 3000 地址后的请求,然后执行createServer()的里的函数。

https

帮助我们启动一个 SSL 加密的服务器。

fs

帮助我们操作文件,对文件进行读写操作。

const fs = require("fs");

writeFile()

写入一个文件,接收三个参数:

  • 第一个参数:完整文件名
  • 第二个参数:文件内容
  • 第三个参数:回调函数,接收一个 error 对象,如果执行出现错误可以在这里得到
fs.writeFile("message.txt", "ds");

writeFileSync()

同步写入文件,在写入文件完成后才能执行下一步。用法与writeFile()相同。

readFile()

读取指定路径的文件,接收两个参数:

  • 第一个参数:文件路径
  • 第二个参数:回调函数,接收一个读取错误时的对象(error 对象)和文件内容
fs.readFile(path, (err, fileContent) => {});

path

帮助我们构建文件路径,让文件的路径能够在任何操作系统(Windows/Mac/Linux)上运行。

const path = require("path");

join()

使用特定于平台的分隔符作为定界符将所有给定的 path 片段连接在一起,然后规范化生成的路径。

path.join("/foo", "bar", "baz/asdf", "quux");
// 返回:\foo\bar\baz\asdf\quux

path.join(__dirname, "..", "views", "shop.html");
// __dirname: 当前文件夹的父目录
// '../': 上级目录
  • 返回路径在不同的环境下可能有所不同;
  • 如果有任何路径片段不是字符串,则抛出 TypeError。

dirname()

返回 path 的目录名,类似于 Unix dirname 命令。 尾随的目录分隔符被忽略。

path.dirname("/foo/bar/baz/asdf/quux");
// 返回: '/foo/bar/baz/asdf'

如果有任何路径片段不是字符串,则抛出 TypeError。

  • 使用辅助函数来构建路径

    util/path.js

    const path = require("path");
    
    // process.mainModule(require.main).filename 为我们提供了文件路径,该文件负责我们应用程序的运行,而这个文件名就是通过我们放在dirname里文件路径获取到的
    module.exports = path.dirname(require.main.filename);
    
    const rootDir = require("../util/path");
    
    res.sendFile(path.join(rootDir, "views", "shop.html"));
    

    这样我们就不需要编写根目录了。

os

帮助我们了解操作系统相关信息。

crypto

帮助我们将数据进行加密。

const crypto = require("crypto");

randomBytes()

生成随机字符,接收两个参数:

  • 第一个参数:随机字符的数量
  • 第二个参数:回调函数,内部传递一个出错时的 error 对象和存储字符的 buffer
crypto.randomBytes(32, (err, buffer) => {
  if (err) {
    console.log(err);
    return res.redirect("/reset");
  }
  const token = buffer.toString("hex"); // 从缓冲区中生成一个token @1
});

@1:buffer 中存储字符是十六进制,需要通过toString('hex')将十六进制的值转化为 ASCII。

例子

  • 引入 http 模块,创建一个服务器监听 3000 端口;
  • 在服务器中判断 3000 端口的路由地址:如果是根路由,则写入一段 HTML 代码(一个表单,里面含有输入框和提交按钮,表单的提交地址为/message,提交方法为POST)后返回响应给浏览器;;
const http = require("http");
const fs = require("fs");

const server = http.createServer((req, res) => {
  const url = req.url;
  if (url === "/") {
    // 如果url地址是/
    res.write("<html>");
    res.write("<head><title>Enter Message</title></head>");
    res.write(
      '<body><form action="/message method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>'
    );
    res.write("</html>");
    return res.end(); // 返回退出代码,如果没有return则不会执行外层后面的res各个操作,就不能获取下面的Hello,Node.js页面
  }

  if (url === "/message" && method === "POST") {
    const body = [];

    // 监听data事件,当新的块(chunk)被读取时就会触发data事件,执行回调函数。函数接收一个参数块
    req.on("data", (chunk) => {
      body.push(chunk); // 将块推入到body中
    });
    req.on("end", () => {
      const parseBody = Buffer.concat(body).toString(); // 创建一个缓冲区连接到body上
      const message = parseBody.split("=")[1];
      fs.writeFileSync("message.txt", message);
    });

    res.statusCode = 302;
    res.setHeader("Location", "/");
    // res.writeHead(302, {
    //     'Location': '/'
    // })
    return res.end();
  }

  res.setHeader("Content-Type", "text/html");
  res.write("<html>");
  res.write("<head><title>First page</title></head>");
  res.write("<body><h1>Hello,Node.js</h1></body>");
  res.write("</html>");
  res.end();
});

server.listen(3000);

附录

res

响应对象可调用的常用方法:

  • setHeader:设置响应标头的单个属性,以键值对的方式修改,可调用多次

    res.setHeader("Location", "/"); // 设置地址跳转到/
    
  • writeHead:设置响应头多个属性(包括状态码、状态信息和响应标头),只调用一次,可进行链式调用

    res.writeHead(statusCode, [reasonPhrase], [headers]);
    
    res.writeHead(200, {
      "Content-Type": "text/plain",
      Location: "/",
    });
    
    • statusCode:状态码;
    • reasonPhrase(可选):状态信息;

    • headers:响应标头;

      注意:两个参数的最后一个参数是 headers 响应标头!三个参数只是方便 reasonPhrase。

    注意:setHeaderwriteHead都有的时候,setHeader的内容合会并到writeHead,合并有冲突时以writeHead的内容为准(writeHead优先级高)。

req

请求数据的常用方法:

  • on:事件监听。接收两个参数,一个是监听的事件,一个是触发的函数:

    // 监听data事件,当新的块(chunk)被读取时就会触发data事件,执行回调函数。函数接收一个默认参数,块(chunk)
    req.on("data", (chunk) => {});
    
    // 在完成读取传入的数据或请求后结束监听
    req.on("end", () => {});
    

    当遇到req.on()时,Node.js 会自动在内部添加一个新的事件监听器来管理所有的监听器,相当于一个注册表在注册了这些事件但没有执行它,然后跳过继续执行下一条。(注册未来某个时间运行但不一定现在运行的代码函数)

CSRF

CSRF(Cross-Site Request Forgery):跨域请求伪造。

CSRF Attacks

一种特殊的攻击方式,通过伪造一个网站,来欺骗用户发送敏感信息,然后根据收集的敏感信息伪装用户向真正的网站发送一些不利请求信息。

防止 CSRF Attacks

确保人们只能在你的视图界面使用你的 session(使用你的应用程序呈现的视图),这样在任何虚假伪造的页面上都不能使用你的 session(即使它看上去很像你的页面)。为了达到这一点,我们可以使用 CSRF Token。

使用 CSRF Token

安装和引入

安装 csurf 包,它允许我们生成CSRF Token嵌入到表单中。在视图和服务器上都包含这个 token,每当我们发起请求时,这个包将检查传入的请求是否具有有效的 token。

此包依赖 express 和 express-session。

npm install --save csurf

场景:虚假网站使用用户的 session 模拟用户向真正的后端发送请求,但是CSRF Token是随机的 hash 值,它只有一个值有效,而且只有在服务器上运行的包(csurf)才知道这个 token 值是否有效。所以虚拟网站发送请求会丢失 token。

app.js

const app = express();
const csrf = require("csurf"); // 引入csurf包

const csrfProtection = csrf(); // 定义csrf的执行函数

app.use(csrfProtection); // 注册中间件

对于任何非 GET 的请求,csurf 包会在视图(request body)中寻找一个CSRF Token,所以我们需要先生成它:

生成 token

exports.getIndex = (req, res, next) => {
  Product.find()
    .then((products) => {
      res.render("shop/index", {
        prods: products,
        pageTitle: "Shop",
        path: "/",
        isAuthenticated: req.session.isLoggedIn,
        csrfToken: req.csrfToken(), // 创建一个token并存储到csrfToken
      });
    })
    .catch((err) => console.log(err));
};

也可以将生成 token 注册成一个中间件,让每个页面的非 GET 请求都能执行

app.js

...
app.use((req, res, next) => {
  res.locals.isAuthenticated = req.session.isLoggedIn;
  res.locals.csrfToken = req.csrfToken();
  next();
});

这样需要在每个发送非 GET 请求的表单上加上:

<input type="hidden" name="_csrf" value="<%= csrfToken %>">

页面通过请求发送 token 并隐藏 token

我使用 ejs 在页面用户进行操作时发送请求,在请求中携带 token,通过type="hidden"来隐藏 token:

<form action="/logout" method="post">
    <input type="hidden" name="_csrf" value="<%= csrfToken %>">
    <button type="submit">Logout</button>
</form>

将 input 的 name 设置为_csrf,csurf 包会自动寻找这个参数。