node学习笔记

node.js 介绍

node.js初识

  1. node.js 平台是基于 Chrome V8 JavaScript 引擎构建。
  2. 基于 node.js 可以开发控制台程序(命令行程序、CLI程序)、桌面应用程序(GUI)(借助 node-webkit、electron 等框架实现)、Web 应用程序(网站)

node.js 有哪些特点?

  1. 事件驱动(当事件被触发时,执行传递过去的回调函数)
  2. 非阻塞 I/O 模型(当执行I/O操作时,不会阻塞线程)
  3. 单线程
  4. 拥有世界最大的开源库生态系统 —— npm。

事件循环(Event-Loop)

事件循环会监视调用栈,以及回调队列
如果调用栈中为空,这个时候,就会将回调队列中的第一个元素放到调用栈中调用

异步-非阻塞

js是单线程的 异步的功能是由webapi或者nodeapi提供的!!## 事件循环(Event-Loop)
事件循环会监视调用栈,以及回调队列
如果调用栈中为空,这个时候,就会将回调队列中的第一个元素放到调用栈中调用

异步-非阻塞

js是单线程的 异步的功能是由webapi或者nodeapi提供的!!

进程和线程

  • 每一个 正在运行 的应用程序都称之为进程。
  • 每一个应用程序运行都至少有一个进程
  • 进程是用来给应用程序提供一个运行的环境
  • 进程是操作系统为应用程序分配资源的一个单位

  • 用来执行应用程序中的代码
  • 在一个进程内部,可以有很多的线程
  • 在一个线程内部,同时只可以干一件事
  • 而且传统的开发方式大部分都是 I/O 阻塞的
  • 所以需要多线程来更好的利用硬件资源

Node 中将所有的阻塞操作交给了内部实现的线程池

Node 本身主线程主要就是不断的往返调度

REPL环境

  1. REPL 全称: Read-Eval-Print-Loop(交互式解释器)
  • R 读取 - 读取用户输入,解析输入了Javascript 数据结构并存储在内存中。
  • E 执行 - 执行输入的数据结构
  • P 打印 - 输出结果
  • L 循环 - 循环操作以上步骤直到用户两次按下 ctrl-c、.exit或者process.exit() 按钮退出。
  1. 在REPL中编写程序 (类似于浏览器开发人员工具中的控制台功能)
  • 直接在控制台输入 node 命令进入 REPL 环境
  1. 按两次 Control + C 退出REPL界面 或者 输入 .exit 退出 REPL 界面
  • 按住 control 键不要放开, 然后按两下 c 键

node.js中JavaScript 文件名命名规则

  • 不要用中文
  • 不要包含空格
  • 不要出现node关键字
  • 建议以 '-' 分割单词

node.js命令

process.stdout.write

这个方法可以用来输出内容,而且不会自动换行
如果要换行可以加上\n

fs模块--文件的读写

var fs = require("fs"); // 引用fs模块
// 写
fs.writeFile("文件路径", "要写的内容", "编码格式", function(err){
    if(err){
        throw err;
    }
    console.log("写入成功")
})
// 读
fs.readFile("文件路径", function(err, data){
    //data是一个Buffer 字节数组
    //获取字符串,需要自己toString
})
// 读
fs.readFile("文件路径", "utf-8", function(err, data){
    //data是一个字符串
})

// 创建一个目录
fs.mkdir('./test-mkdir',function(err){
    if(err){
        console.log("创建文件夹出错");
        console.log(err)
    }else{
        console.log("创建成功")
    }
})

path模块相关

  • __dirname -- 获取当前文件所在目录的完整路径(伪全局)

  • __filename -- 获取当前文件的完整路径 (伪全局)

  • path.join() -- 用来拼接路径


path.join(__dirname,"css","index.css");

http模块

直接上代码

//1. 引入http模块
var http = require("http");

// 2. 创建服务实例
var server = http.createServer();

// 3. 注册请求事件(每当有请求来的时候,就会触发该事件)
server.on("request",function(request,response){

    // 设置响应头
    response.writeHead(200,"OK",{
        "Content-Type":"text/html;charset=utf-8"
    })

    //给浏览器端返回数据(要给响应体中添加内容)
    response.write("");

    //在所有的响应信息添加完毕之后,需要结束响应
    response.end(); 
})

//4. 开启服务实例的监听功能
server.listen(端口号, function(){
    //监听开启成功后会执行的函数
    console.log("服务启动成功")
})

request对象

  • url 浏览器请求的地址(包含两部分 路径?参数)
  • method 浏览器发送该请求的方式 GET POST
  • headers 请求头中所有的信息(对象)
  • rawHeaders 请求头中所有的信息(数组)
  • httpVersion 请求头中包含的协议的版本

response对象

  • response.setHeader("键", "值") 往请求头中新增信息

  • response.writeHead(状态码, 状态描述信息, {往请求头中新增信息的键值对})

    • setHeader和writeHead都可以用来设置响应头
    • writeHead是直接将内容写入响应头中,而setHeader是在end的时候才写入
    • 如果同时使用setHeader和writeHead设置了响应信息,那么会合并,如果有相同的内容则以writeHead的为主
  • response.write(data, "编码格式", callback) 给响应体中追加内容

  • response.end(data) 通知服务器 所有的响应内容已经发送完毕 如果传入了data,就相当于是先调用response.write将数据追加之后再response.end结束响应 每次响应都需要调用这个end方法

  • response.statusCode -- 设置状态码

  • response.statusMessage -- 设置状态码对应的状态信息

mime (第三方模块)

在页面请求的时候,不同的资源是有不同的content-type的值的,如果通过response.url一一匹配设置太麻烦了,可以通过 mime 模块设置不同类型资源的Content-Type

  • npm install mime
  • 在代码中直接引用 var mime = require('mime')
server.on("request",function(request,response){
    if(request.url.startsWith("/static")){
        response.writeHead(200,"OK",{
            "Content-Type":mime.getType("文件路径")
        })
    }
})

获取post请求发送的数据

如果post传过来的数据时大量的话,那么data事件可能会触发多次,所以可声明一个空数组,用来存放每次触发data事件的时候传过来的一段数据,end事件是当数据全部传送完毕的时候会调用的函数,这个时候再把数组中每一次传过来的数据拼接起来,就得到了完整的数据。


server.on("request", function(req, res){
    var chunkArr = [];  // 用来保存post请求传过来的数据
    req.on("data", function(chunk){
        chunkArr.push(chunk);  // 将此次数据存放起来
    })

    req.on("end", function(){
        // 拼接全部数据
        var result = Buffer.concat(chunkArr);
    })
})

获取get请求发送的数据

get请求的数据是在URL地址栏里的,所以可利用 url模块 或者 querystring 模块获取

  • url模块
    var url = require("url");
    var urlObj = url.parse(request.url,true); // 返回的是个对象,包含了url里面的信息
    urlObj.query  // query这个属性返回的是参数(也即是get方式的数据)对象
    

underscore和arttemplate --node.js模板--第三方模块

// underscore
var _ = require("underscore");
var render = _.template("模板字符串");
var resultHtml = render("数据");
response.end(resultHtml); // 发送给客户端 
// arttemplate

// 直接自己读文件
var resultHtml = template("模板字符串",数据);

// 根据模板字符串生成渲染函数
var render = template.compile("模板字符串");
var resultHtml = render(数据);

node.js模块化

  • NOde采用的模块化结构是按照CommonJS规范

  • 模块与文件是意义对应关系,即加载一个模块,实际上就是加载该模块对应的模块文件

  • 模块特点

    • 所有代码都运行在模块作用域,不会污染全局作用域。
    • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
    • 模块加载的顺序,按照其在代码中出现的顺序。
  • 模块分类

    • 文件模块 -- 就是我们自己写的功能模块文件

    • 核心模块 -- Node 平台自带的一套基本的功能模块,也有人称之为 Node 平台的 API

    • 第三方模块 -- 社区或第三方个人开发好的功能模块,可以直接拿回来用

1. 定义模块

将代码写在一个js文件中,这个js文件就是一个模块了

module.exports是一个对象,这个对象会在模块被创建出来的时候,同时被创建

// module.exports.成员 = 内容;
var say = function(){};
module.exports.say = say;

// 也可以直接给module.exports赋值
module.exports = {
    say : say
}

// 注意: 虽然exports和module.exports指向的是同一个对象的的引用地址,但是模块暴露给外接的属性或方法,只能直接赋值给module.exports,因为模块最终的返回值是 module.exports

2. 引用模块

  • 核心模块

    • require("核心模块名称")
  • 文件模块

    • require("文件的路径,必须以/ ./ ../开头")

    • 文件路径中的.js路径可以省略,如果省略 .js 则node会查找 以该文件为名字 以 .js .node .json 为后缀的文件来加载,如果没有,就报错了

  • 目录模块

    • require("目录路径,必须以/ ./ ../开头")

    • 当以目录路径为参数的时候node.js会查找该目录下的 package.json 文件,会加载其 main 属性所对应的文件

    • 如果没有 package.json 或者package.json中没有main属性 则去加载目录中 index.js index.node

  • node_modules中的模块

    • require("模块名称");

    • (这么写是不带./ / ../)

    • 如果这么写,先回去核心模块中查找,如果找不到,则去当前目录的父路径中的node_modules文件夹中查找对应的模块,如果还是找不到,就会一直沿着父路径往上找,直到找到硬盘根目录

    • 如何查看要查找的路径(module.paths)

3. 删除模块缓存

 let absolutepath = path.join(__dirname, `../data/test.json`);
 delete require.cache[absolutepath];
 require(absolutepath);

express

express是什么

express是一个简单高效用来开发web应用的框架(基于node.js)

怎么用它

  1. 安装
  • npm install express -S
  1. 引用并使用
//1. 引入
var express = require("express");

//2. 创建实例
var app = express();

//4. 注册路由
app.use(express.static("static"))

//3. 启动服务监听
app.listen(8888,function(){
    console.log("http://localhost:8888")
})

express.static(root,[options])

express 中的中间件

  • 中间件(Middleware) 是一个函数,它可以访问请求对象(request object (req)), 响应对象(response object (res)), 和 web 应用中处于请求-响应循环流程中的中间件,一般被命名为 next 的变量。

  • 常规中间件(应用级中间件)的回调函数,一般有3个参数

    • req, res, next
    • 其中next()是一个函数,通过这个函数把执行流程交给下一个中间件
  • 可以挂载中间件的部分方法

    • app.use()
    • app.get()、app.post()、app.put()、app.delete()、...等各种请求方法
    • 注意:在挂载中间件时 next() 方法的重要性,不调用该方法无法执行到下一个中间件
  • 中间件的应用场景

    • 利用中间件实现记录日志功能
    • 利用中间件模拟body-parser功能

路由注册

  1. app.method
// 只接受指定方式的请求
app.get("/index",function(req,res){})

app.post()
  1. app.use

// 可以接受任意方式的请求
// 请求路径pathname,必须以路由路径开头
app.use("/index",function(req,res){})

// index.html index/s/a  都可以
  1. app.all
// 可以接受任意方式的请求
// 请求路径pathname必须和路由路径完全一样
app.all("/index",function(req,res){})

// index/s/a 不可以

res对象新增功能

  • res.send()

    • send可以将对象 字符串 数组 数字 等等数据返回给浏览器
    • 一次请求中只能用一次
    • 他里面自动给响应报文头中添加了一些内容 Content-type
  • res.sendFile()

    • 可以用来向浏览器发送文件内容
  • res.json

  • res.jsonp

  • res.status(状态码).end(""); -- 设置状态码

  • res.redirect('http://google.com'); -- 重定向

req对象获取GET方式或POST方式的请求数据

  • req.query 直接就可以获取到get请求的参数对象

  • req.body 可以获得POST提交方式的请求数据内容,但是需要配置

    • 如果form表单中的enctype属性未设置或者设置为application/x-www-form-urlencoded,那么req.body就可以通过body-parser解析之后获取到请求数据
    • 但是如果设置为 multipart/form-data这样的格式 ,那么req.body是拿不到内容的!!
  • req.body的配置

    • 下载包 npm install body-parser -S
    • 引包 var parser = require("body-parser");
    • 将其挂载到app上 app.use(parser.urlencoded({extended: false}));
    • 注意: extend属性如果设置为true, 则express最终将数据转换成对象的时候使用的就是qs模块,如果是false则使用的是querystring模块!
    • 注意: 挂载use这局代码一定写在app.use(router)之上,先让parser处理POST请求的数据,然后再让路由去处理相关数据的逻辑代码
// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
req.query.order
// => "desc
var app = require('express')();
var bodyParser = require('body-parser');
var multer = require('multer'); 

app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
app.use(multer()); // for parsing multipart/form-data

app.post('/', function (req, res) {
  console.log(req.body);
  res.json(req.body);
})

路由对象

// router.js文件中
var express = require("express")
var router = express.Router();

router.get()
router.use()

// app.js文件中
var app = express();
app.use(router);

nodemon 自动刷新应用

1、全局下载 nodemon

npm install -g nodemon

2、 修改 package.json的 scripts

{
  "name": "myapp",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "nodemon ./bin/www"    // here here
  },
  "dependencies": {
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.9",
    "express": "~4.16.0",
    "http-errors": "~1.6.2",
    "morgan": "~1.9.0",
    "pug": "2.0.0-beta11"
  }
}

模板引擎(集成在express中如何使用)

var express = require("express");
var app = express();

// 1. 指定的为指定的后缀的模板文件,指定相应的模板引擎
app.engine("html", require("express-art-template"));

//2. 将模板的后缀通过set方法进行设置
app.set('view engine', 'html');

//3. 设置模板文件存放的目录
app.set("views", "./views")

app.get("/index", function(req, res){
    //res.render("不带后缀的模板文件的名称(这个文件会去设置好的模板文件的存放目录去查找)", {数据}, function(err, html){
        //err是异常信息
        //html 模板最终的渲染结果,最终生成的html代码
    // })
    res.render("index", {list: [{name: "呵呵"}, {name: "呵呵"}, {name: "哈哈"}]}, function(err, html){
        res.send(html);
    })
})

弹框下载

  1. node.js原生实现
res.setHeader("Content-Type", "application/octet-stream")
res.setHeader("Content-Disposition", "attachment; filename=xxx.txt")
//要返回的文件内容,需要自己手动进行读取,通过res.write获取res.end返回给浏览器
fs.readFile(path.join(__dirname, "xxx.js"),"utf-8", function(err, data){
    res.end(data);
})
  1. express实现
res.download("文件路径");

使用node.js发送请求

  • 流程介绍

    • 引入http模块
    • 给http注册request事件
    • 设置请求参数
    • 获取响应信息(和用node.js原生代码,拿POST方式提交数据一样)
    • 注册 发生错误的事件
    • 结束请求
  • 代码示例

var http = require("http");
var req = http.request({
    host:"www.qiushibaike.com",
    hostname:"www.qiushibaike.com",
    method:"GET",
    port:443,
    path:"/",
    headers:{
        "Connection":" keep-alive",
        "Pragma":" no-cache",
        "Cache-Control":" no-cache",
        // 必须要写!!
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
        //注意:gzip如果在请求头中出现了,那么服务器端在响应请求的时候,会将所有的数据进行压缩,以提高传输效率
        //如果是浏览器在请求,那么浏览器拿到压缩后的数据之后,会自行解压,然后进行展示
        //nodejs中请求到数据之后,并不能自行解压,所以我们不在请求头加这个内容!
    }
},function(response){
    var buffer = []; // 用来存放返回信息
    response.on("data",function(chunk){
        buffer.push(chunk);
    });
    response.on("end",function(){
        buffer = Buffer.concat(buffer);
        buffer.toString("utf-8")
    })
})
req.on("error", function(err){
    throw err;
})
// end事件不要忘记注册!
req.end();

cheerio 处理服务器返回来的HTML代码

  • 咋弄

    • 下载包 npm install cheerio -S
    • 引包 var cheerio = require("cheerio")
    • 加载返回回来HTML数据 var $ = cheerio.load("html代码")
    • 然后就和jQuery一样来操作里面的元素就阔以了!
  • 代码示例

var cheerio = require("cheerio");
// 通过http,拿到的HTML格式的数据经过buffer拼接,转码,发送给cheerio
var $ = cheerio.load(Buffer.concat(buffer).toString("utf-8"));
// 用来存放自己想要的东西
var qiubaArr = [];   
$(".article").each(function(index, ele){
    // 拿到所有author
    var author = $(ele).find(".author h2").text()
    var content = $(ele).find(".content>span").text();
    // 把所有数据存起来,然后
    qiubaArr.push({
        author,
        content
    })
})

CRUD


使用 MongoDB 官方提供的 mongodb 驱动包操作 MongoDB 数据库

安装:

npm install mongodb --save

CRUD:

参考文档:

var mongodb = require('mongodb')

// 连接路径URL
var url = 'mongodb://localhost:27017/itcast'

var MongoClient = mongodb.MongoClient

// ================== 插入数据 ==================
// 1. 连接数据库(打开冰箱门)
// MongoClient.connect(url, function (err, db) {
//   if (err) {
//     throw new Error('连接失败')
//   }

//   // 2. 把大象放到冰箱
//   db.collection('heros').insert({ name: '张飞', gender: '男', age: 23 }, function (err, result) {
//       if (err) {
//         throw new Error('插入数据失败')
//       }
//       console.log(result)

//       // 3. 关上冰箱门
//       db.close()
//     })
// })
// ================== /插入数据 ==================


// ================== 查询数据 ==================
// MongoClient.connect(url, function (err, db) {
//   if (err) {
//     throw new Error('连接失败')
//   }

//   // 查询所有
//   // db.collection('heros').find({}).toArray(function (err, docs) {
//   //   if (err) {
//   //     throw new Error('查询数据失败')
//   //   }
//   //   console.log(docs)
//   // })

//   // 按条件查询
//   db.collection('heros').find({name: '张飞'}).toArray(function (err, docs) {
//     if (err) {
//       throw new Error('查询数据失败')
//     }
//     console.log(docs)
//   })
// })
// ================== /查询数据 ==================



// ================== 更新数据 ==================
// MongoClient.connect(url, function (err, db) {
//   if (err) {
//     throw new Error('连接失败')
//   }
//   db.collection('heros').updateOne({name: '张飞'}, {
//     $set: {
//       age: 20
//     }
//   }, function (err, result) {
//     if (err) {
//       throw new Error('更新失败')
//     }
//     console.log(result)
//   })
// })
// ================== /更新数据 ==================


// ================== 删除数据 ==================
// MongoClient.connect(url, function (err, db) {
//   if (err) {
//     throw new Error('连接失败')
//   }
//   db.collection('heros').deleteOne({name: '张飞'}, function (err, result) {
//     if (err) {
//       throw new Error('删除失败')
//     }
//     console.log(result)
//   })
// })
// ================== /删除数据 ==================

在node做本地服务器的项目中,做到文件下载

  • 流的方式
router.post('/:cubeId/download', function(req, res, next) {
    let fileName = 'file';
    // console.log(res)
    // console.log(req.body);
    // console.log(res.body.params);
    let currFilePath = path.join(__dirname,`../data/file.txt`);
    let f = fs.createReadStream(currFilePath);
    res.writeHead(200, {
        'Content-Type': 'application/force-download',
        'Content-Disposition': `attachment; filename=file.txt`
    });
    f.pipe(res);
})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    w_zhuan阅读 3,607评论 2 41
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    Myselfyan阅读 4,061评论 2 58
  • 很多Node.js初学者都会有这样的疑惑,Node.js到底是单线程的还是多线程的?通过本章的学习,能够让读者较为...
    越努力越幸运_952c阅读 3,630评论 4 36
  • 一切的言语都是很苍白无力的:你做了什么,你就是什么,值什么。 听某哲学系女老师讲课,讨论到女孩为什么不能做纯家庭主...
    梁好先生阅读 186评论 2 1
  • 《说神经病》猫黍 神经不知神经病,君知自己不神经。 神经全说你神经,汝改观念变神经。 贱猫乱心》猫黍 凛冽寒风盖全...
    猫黍阅读 273评论 0 0