初始express与使用总结

1.hello world

1.1.安装express

1.进入到自己的项目目录, 我这里是express-demo
cd express-demo
2.初始化项目,生成package.json文件
npm init -y
3.安装express
npm install express --save

1.2.简单使用

在当前项目下新建app.js文件

// app.js内容
// 引入express
var express = require('express')
var app = express()
app.get('/hi', function (req, res, next){
  res.send('hello world!!!')
})
app.listen(8090, function (error) {
  console.log('listening on 8090')
})

1.3.自动监听文件,并且重启

安装,可以全局安装也可以局部安装
npm install -g nodemon
使用nodemon监听文件变化,自动重启服务
nodemon app.js

2.请求和响应

2.1.请求相关

2.1.1.返回一个html页面

app.get('/', function (req, res){
  res.sendFile(path.resolve('./views/index.html'))
})

注意path模块需要先引入
path模块可以把一个路径解析成一个对象,对象中包含了很多属性。

2.1.2.接收前台get方式发送过来的数据

// get方式发送过来的数据 使用req.query接收
app.get('/getuser', function (req, res) {
  console.log(req.query.userid)
})
// 完整代码
// 引入express
var express = require('express')
var path = require('path')
var app = express()
var userArr = [
  {"id": 1, "name": "xiaoqiang", "age": 18},
  {"id": 2, "name": "xiaoli", "age": 19},
  {"id": 3, "name": "xiaowang", "age": 20},
  {"id": 4, "name": "xiaozhang", "age": 21}
]
app.get('/', function (req, res){
  res.sendFile(path.resolve('./views/index.html'))
})
app.get('/user', function (req, res) {
  res.sendFile(path.resolve('./views/login.html'))
})

app.get('/getuser', function (req, res) {
  res.send(userArr.filter(function (item) {
    console.log(item.id, req.query.userid)
    return item.id == req.query.userid
  }))
})
app.listen(8090, function (error) {
  console.log('listening on 8090')
})

2.1.3.接收前台post方式发送过来的数据

接收post数据,我们可以使用一个叫做body-parser的模块来帮我们完成

1.第一步,先安装这个模块
npm install body-parser --save

2.第二步,引入这个模块,并且作为插件使用

var bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({extend: false}))

3.第三步,获取数据

app.post('/login', function (req, res) {
  res.send(req.body.user)
})

2.1.4.文件上传

文件上传需要用到multer插件,先安装multer
npm install multer --save
接下来需要引入multer
var multer = require('multer')
配置multer
var upload = multer({dest: './upload'})
dest 表示 设置上传文件目录
会在当前目录下自动生成一个upload文件夹,用来存放上传的文件
接下来我们在使用这个multer的时候不能用app.use,因为如果是用app.use的方式使用的话,会多次触发,而我们只希望在对应的请求处理的时候进行使用。

  • 单文件上传
app.post('/upload', upload.single('fl'), function (req, res) {
  res.send(req.file)
})

处理文件上传请求
f1是指的文件上传的input的name

  • 多文件上传
app.post('/upload', upload.array('fl', 3), function (req, res) {
  res.send(req.files)
})
  • 自定义文件路径和文件名

node里面一般带有sync的都象征着是同步方法

var fs = require('fs')
// 检查目录是否存在,不存在就创建,如果存在,就直接使用
var  createFolder = function(folder) {
  try {
      //accessSync如果没有检测到,就会报错
      //如果报错了,就会被catchh抓获
    fs.accessSync(folder)
  } catch (e) {
    fs.mkdirSync(folder)
      //异步方法通常都是需要用到回调函数的,所以这里我们要用同步方法,       
      //也就是要加上sync
  }
}
//设置好路径和文件名
var uploadFolder = './upload'
// 先创建好上传目录
createFolder(uploadFolder)
//去自定义文件上传后的路径和自定义文件名字
var storage = multer.diskStorage({
    //cb就是callback的缩写,回调函数
  destination: function (req, file, cb) {
     //下面这行代码的意思:有文件就按照uploadFolder进行上传就行了
    cb(null, uploadFolder)
  }, 
  filename: function (req, file, cb) {
      //cb(null,file.originalname)    --->这样传的话,文件原来是什么名字,上传之后就还是什么名字
    cb(null, file.fieldname + '-' + new Date().getTime() + path.extname(file.originalname))
  }
})
//第一个storage就是storage定义,第二个storage是我们定义的上面的那几行代码
var upload = multer({storage: storage})

app.post('/upload', upload.array('fl', 3), function (req, res) {
  res.send(req.files)
})

2.2.响应相关

2.2.1.send方法

send方法可以返回多种类型数据

// 返回一个对象
res.send({"name":"老张", age: 48})
 // 返回数组
// res.send([1, 2, 3])
// 报错
// res.write({"name": "xiaoqiang"})
// res.end()

2.2.2.sendStatus

返回状态码
res.sendHeader(404)
如果没有这个方法,你会这样写:

res.writeHead(404, {'Content-Type': 'text/plain'})
res.write("not found!!!!")
res.end()

很明显第一种写法更简洁。

2.2.3.redirect

redirect用于重定向

app.get('/re', function (req, res) {
  res.redirect('http://nodeing.com')
})

如果不使用redirect,而使用原生的语法,需要这样写:

res.setHeader('location', 'http://nodeing.com')
res.writeHead(301)
res.end()
  • 301 永久重定向 浏览器会记住
    • a.com b.com
    • a 浏览器不会请求 a 了
    • 直接去跳到 b 了
    • 通过看status code,可以发现后面写着from cache
  • 302 临时重定向 浏览器不记忆
    • a.com b.com
    • a.com 还会请求 a
    • a 告诉浏览器你往 b

3.路由

路由到底是什么呢?不管官方定义到底是什么,咱通俗的说就是根据不同的url,执行不同的代码,类似于编程语言中的分支结构

3.1.express规划路由

稍微复杂点的应用,通常都是分模块进行的

我们从中挑选几个模块进行路由规划,在我们的后台模块里面,可以实现用户的管理,课程的管理,友情链接管理等,我们的访问地址可能是这样的

// 1.用户管理
// 用户列表
http://localhost:8090/admin/user/list
// 添加用户
http://localhost:8090/admin/user/add
// 删除用户
http://localhost:8090/admin/user/delete
// 编辑用户
http://localhost:8090/admin/user/edit

// 2.课程管理
// 课程列表
http://localhost:8090/admin/course/list
// 添加用户
http://localhost:8090/admin/course/add
// 删除用户
http://localhost:8090/admin/course/delete
// 编辑用户
http://localhost:8090/admin/course/edit

...

在没有拆分路由的情况下,我们需要在app.js里面写这些代码

/**
 * 用户管理模块
 */
app.get('/admin/user/list', function (req, res) {
  res.send('用户列表')
})
app.get('/admin/user/add', function (req, res) {
  res.send('添加用户')
})
app.get('/admin/user/delete', function (req, res) {
  res.send('删除用户')
})
app.get('/admin/user/edit', function (req, res) {
  res.send('更新用户')
})

/**
 * 课程模块
 */
app.get('/admin/course/list', function (req, res) {
  res.send('课程列表')
})
app.get('/admin/course/add', function (req, res) {
  res.send('添加课程')
})
app.get('/admin/course/delete', function (req, res) {
  res.send('删除课程')
})
app.get('/admin/course/edit', function (req, res) {
  res.send('更新课程')
})

当上面的代码都写到app.js中,代码会显得非常臃肿,最佳的实践是把这些模块拆分出去,express中提供了拆分的方法

第一步,我们在项目根目录下面,新建一个router目录,在这个目录下面按模块名字分别创建user.js和course.js

第二步,在user.js文件中,创建router,并导出

var express = require('express')
var router = express.Router()

// ...  中间写对应的路由方法

module.exports = router

第三步,把对应的路由方法添加到user.js中

var express = require('express')
var router = express.Router()

router.get('/admin/user/list', function (req, res) {
  res.send('用户列表')
})
router.get('/admin/user/add', function (req, res) {
  res.send('添加用户')
})
router.get('/admin/user/delete', function (req, res) {
  res.send('删除用户')
})
router.get('/admin/user/edit', function (req, res) {
  res.send('更新用户')
})

module.exports = router

经过前面步骤,我们完成了user模块路由拆分

接下来,我们可以按照这种方式,把course模块拆分出来

// course.js文件代码

var express = require('express')
var router = express.Router()

router.get('/admin/course/list', function (req, res) {
  res.send('课程列表')
})
router.get('/admin/course/add', function (req, res) {
  res.send('添加课程')
})
router.get('/admin/course/delete', function (req, res) {
  res.send('删除课程')
})
router.get('/admin/course/edit', function (req, res) {
  res.send('更新课程')
})

module.exports = router

当我们各个路由模块都拆分完成后,如何使用这些模块呢?

在app.js中,我们需要引入创建好的路由模块

var userRouter = require('./router/user')
var courseRouter = require('./router/course')

接下来,挂载到express上

app.use('/', userRouter)
app.use('/', courseRouter)

经过以上步骤,我们已经把路由模块完全拆分出去了

3.2.对路由模块进行多级拆分

前面我们已经把模块划分出来了,我们在写路由方法的时候是这样的:

router.get('/admin/user/list', function (req, res) {
  res.send('用户列表')
})

从代码中我们可以看成,写路径的时候会写一长串,/admin/user/list,这样写比较麻烦,同时,我们更希望大模块直接有更好的划分,例如,我们的系统总体上划分为前台模块和后台模块,那我们的router文件夹中应该再分home和admin两个文件夹

admin文件夹下面放的都是关于后台路由的模块,home目录下面放的都是前台路由的模块,这个时候,我们需要对路由做进一步拆分,以拆分admin为例:

第一步:在admin/index.js下面 引入其他admin下面的模块,把其他admin下面的模块都挂在index.js这个模块上然后导出

var express = require('express')
var router = express.Router()
var course = require('./course')
var user = require('./user')

// 挂载user模块
router.use('/', user)
// 挂载course模块
router.use('/', course)

module.exports = router

第二步:在app.js中引入admin模块

//这里的index.js可以不写.js,浏览器加载时会自动加上后缀
var adminRouter = require('./router/admin/index')

第三步:把adminRouter挂到app对象上

//如果我们请求/admin的话,会自动去找到adminRouter模块
app.use('/admin', adminRouter)

第四步:需要注意,user.js模块中的请求路径需要改变,例如:原来的"/admin/user/list"这种写法,需要改成这种"/user/list"

3.3.动态路由

动态路由就是路由是动态不固定的,例如:

http;//localhost:8090/user/1
http;//localhost:8090/user/2
http;//localhost:8090/user/3

上面的几个url中,都是去访问某个user,前面部分(http;//localhost:8090/user)是相同的,不同的就是后面的部分

在后台我们怎么去监听这种形式的url呢?我们可以弄一个变量来匹配这些不同的部分,例如:

app.get('/user/:id', function (req, res) {
  console.log(req.params)
})

这其中的id就存储了url中变化的部分

/user/1   id: 1
/user/2   id: 2
/user/3   id: 3

可以通过req.params.id打印出每次请求的动态部分(动态参数)

4.静态文件

4.1.普通处理静态文件的方法

在./views/index.html文件中去引入另一个css文件index.css,index.css文件放在public/css目录下。

index.html文件中的内容

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    #wrap {
      width: 300px;
      margin: 100px auto;
      line-height: 150px;
      height: 150px;
      text-align: center;
      background-color: green;
    }
    #wrap a {
      color: white;
      text-decoration: none;
    }
  </style>
  <link rel="stylesheet" href="/public/css/index.css">
</head>
<body>
  <div id="wrap">
      <a href="/login">登录 | </a>
      <a href="/user">用户中心</a>
  </div>
</body>
</html>

根据请求渲染出index.html文件

app.get('/', function (req, res) {
  res.sendFile('./index.html')
})

当我们方法 '/'这个路径的时候,能把index.html页面加载出来,但是没办法把css文件加载出来

为了解决这个问题,我们还需要单独去写一个路由去返回css文件

app.get('/public/css/index.css', function (req, res) {
  res.sendFile(path.resolve('./public/css/index.css'))
})

4.2.express中处理静态文件的插件

express中提供了处理静态文件的插件,这里的静态文件就是我们项目中需要用到的img、css、js等资源,只需要简单的配置就可以实现对静态文件的处理,步骤如下:

第一步,在app中挂载插件

app.use(express.static('./public'))

第二步,使用静态文件,在index.html文件中引入css,路径需要修改一下

<link rel="stylesheet" href="/css/index.css">

5.ejs模版

5.1.什么是模版引擎?

为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。

在后端开发中,处理数据的代码和展示数据的代码是分离的,这就是前面说的,用户界面和业务数据内容分离,但是真的展现到前端给用户看到的界面都是数据和界面融合在一起的,模版引擎的作用就是把html文件和后端的数据柔和在一起生成一个html文件返回给前端展示,这种方式又叫做服务端渲染。

模板引擎在服务端所传递的变量,只会在前端的html中执行,并且只认识自己的特定符号,js代码不会执行模板引擎,js会直接放给浏览器执行。

5.2.在express中使用ejs模版引擎

第一步,安装ejs

npm install ejs --save

第二步,在app.js中引入ejs

var ejs = require('ejs')

第三步,设置express的模版文件夹,app.set方法,表示设置某个属性名的value,例如:设置express的views,views指express中模版文件的路径,路径的值为第二个参数给的值

app.set('views', path.join(__dirname, 'views'))

第四步,告诉express使用ejs来作为模板引擎,并且设置模板文件后缀

app.engine('html', ejs.__express)

第五步,注册模板引擎

app.set('view engine', 'html')

第六步,ejs模板引擎初体验

1.在"/"路由中,渲染"index.html"文件,并带参数

app.get('/', function (req, res){
  // res.sendFile(path.resolve('./views/index.html'))
  res.render('index.html', {title: '螺钉课堂!!!'})
})

2.在‘index.html’文件中去使用数据

<h1><%= title %></h1>

5.3.ejs的常用语法

1.基本语法,后台数据是融和在html模板中的,在html模板中,通过自定义标签的形式来区分到底是ejs的标签还是html的标签 例如:

<%= title %>

常用的标签:

1、<% if|for %> 这种叫做脚本标签,用于写流程控制  

2、<%= 变量 %> 这种标签的作用是把数据输出到html 

3、<%- %>这种标签的作用和<%= %>相同,区别是这种标签可以解析html,<%= %>这种标签会把html标签给转义了

2.流程控制语句

  • if 语句
// 1.后台传入一个 isLogin字段
app.get('/', function (req, res){
  // res.sendFile(path.resolve('./views/index.html'))
  res.render('index.html', {title: '螺钉课堂!!!', isLogin: false})
})
// 2.在模板中使用这个isLogin字段来做判断
<% if (isLogin) { %>
  <div id="wrap">
      <a href="/login">欢迎admin,登录!!!</a>
      <a href="/user">用户中心</a>
  </div>
<% } else { %> 
  <div id="wrap">
      <a href="/login">登录 | </a>
      <a href="/user">用户中心</a>
  </div>
<% } %>
  • for循环渲染
// 1.在后台传入一个数组
app.get('/', function (req, res){
  // res.sendFile(path.resolve('./views/index.html')) 
  var userList = [
    {name: '张飞', age: 29},
    {name: '关羽', age: 30},
    {name: '刘备', age: 31},
  ]
  res.render('index.html', {title: '螺钉课堂!!!', isLogin: false, userList: userList})
})
// 2.在模板中循环出这个数组
<ul>
  <% for (var i = 0; i < userList.length; i++) {%>
  <li><%= userList[i].name %> ----> <%= userList[i].age %></li>
  <% } %>
</ul>
// 3.也可以使用forEach方法来循环
<ul>
  <% userList.forEach(function (item){%>
    <li><%= item.name %> ----> <%= item.age%></li>
  <% }) %>
</ul>

5.中间件

中间件可以理解为过滤器,我们通过一个自定义的body中间件来阐述到底什么是中间件?

中间件本质上是一个函数

中间件都会有一个next参数,让过滤器能够一层一层往下执行。

中间件需要在路由之前执行。

中间件配置必须放在router挂载的前面

在项目根目录下面创建一个libs文件夹,然后创建body.js文件

body.js内容

const queryString = require('querystring')

function body(req, res, next) {
  let arr = []
  req.on('data', (chunk, err) => {
    if (!err) {
      arr.push(chunk)
    }
  })
  req.on('end', (err) => {
      //arr没处理之前是一个二进制对象
      //用Buffer.concat把arr数组里的二进制对象都给连接起来
      //.toString()把前面整个转换成字符串
      //queryString.parse把字符串解析成对象
    req.body = queryString.parse(Buffer.concat(arr).toString())
    next()
  })
}

module.exports = body

在express中使用body中间件

const express = require('express')
const app = express()
const body = require('./libs/body.js')

app.use(body)

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

app.listen(4001)

中间件:处理请求的,本质就是个函数
在 Express 中,对中间件有几种分类
当请求进来,会从第一个中间件开始进行匹配
如果匹配,则进来
如果请求进入中间件之后,没有调用 next 则代码会停在当前中间件
如果调用了 next 则继续向后找到第一个匹配的中间件
如果不匹配,则继续判断匹配下一个中间件
不关心请求路径和请求方法的中间件
也就是说任何请求都会进入这个中间件
中间件本身是一个方法,该方法接收三个参数:
Request 请求对象
Response 响应对象
next 下一个中间件
当一个请求进入一个中间件之后,如果不调用 next 则会停留在当前中间件
所以 next 是一个方法,用来调用下一个中间件的
调用 next 方法也是要匹配的(不是调用紧挨着的那个)
如果调用next方法的时候,传err参数,则会将错误统一匹配给错误处理中间件,也就是带有四个参数的应用程序级别的中间件。
app.use(function (err, req, res, next) {
res.status(500).send(err.message)
})

应用程序级别中间件

万能匹配(不关心任何请求路径和请求方法)

app.use( function (req, res , next){
    console.log('Time:',Date.now())
    next()
})

只要是以’/xxx/‘开头的

app.use ('/a',function (req,res,next){
    console.log('Time:',Date.now())
    next()
})

路径级别中间件

get:

app.get ('/',function (req,res){
    res.end('Hello World!')
})

post:

app.post('/',function (req,res){
    res.end('Got a POST request')
})

put:

app.put( '/user',function (req,res){
    res.send ('Got a PUT request at /user')
})

express解决跨域问题

方式一:
不用中间件的话可以这样写

const express = require('express')
const app = express()

app.use((req, res, next) => {
    // 设置是否运行客户端设置 withCredentials
    // 即在不同域名下发出的请求也可以携带 cookie
    res.header("Access-Control-Allow-Credentials",true)
    // 第二个参数表示允许跨域的域名,* 代表所有域名  
    res.header('Access-Control-Allow-Origin', '*')
    res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, OPTIONS') // 允许的 http 请求的方法
    // 允许前台获得的除 Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma 这几张基本响应头之外的响应头
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With')
    if (req.method == 'OPTIONS') {
        res.sendStatus(200)
    } else {
        next()
    }
})

方式二:
使用CORS,和其他中间件的用法一样,app.use()即可:

var express = require('express')
var cors = require('cors')
var app = express()

app.use(cors())

app.get('/products/:id', function (req, res, next) {
  res.json({msg: 'This is CORS-enabled for all origins!'})
})

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})

如果要单独为某个接口实现允许跨域请求,在回调函数之前先用cors()方法进行跨域处理即可:

var express = require('express')
var cors = require('cors')
var app = express()

app.get('/products/:id', cors(), function (req, res, next) {
  res.json({msg: 'This is CORS-enabled for a Single Route'})
})

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})

还可以自己手动配置:

var express = require('express')
var cors = require('cors')
var app = express()

var corsOptions = {
  origin: 'http://example.com',
  optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204
}

app.get('/products/:id', cors(corsOptions), function (req, res, next) {
  res.json({msg: 'This is CORS-enabled for only example.com.'})
})

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容