Node进阶

Node进阶

发表于 2024-08-24
更新于 2024-08-24
阅读时长:40分钟
阅读量:12
AI 总结
|

1、代理客户端

基本结构

get请求

//server.js
const http = require('http')

const server = http.createServer((res, req) => {
  console.log('请求进来,相关处理内容')
})

server.listen(8080, () => {
  console.log('server is running')
})
//agent-client.js 
const http = require('http')

// 发送get请求
http.get({host: 'localhost', port: 8080}, (res) => {})

post请求

//server
const http = require('http')
const url = require('url')

const server = http.createServer((req, res) => {
  // post
  let arr = []
  req.on('data', (data) => {
    arr.push(data)
  })
  req.on('end', () => {
    let obj = Buffer.concat(arr).toString()
    let handleValue = JSON.parse(obj)
    handleValue.age = 22
    res.end(JSON.stringify(handleValue))
  })
})

server.listen(8080, () => {
  console.log('server is running')
})


//agent-client
const http = require('http')

const options = {
  host: 'localhost',
  port: 8080,
  method: 'post',
  path: '/?age=25',
  headers: {
    "Content-type": "application/json" //json格式
    //"Content-type": "application/x-www-form-urlencoded" //表单格式 
  }
}
// 发送post请求
let req = http.request(options, (res) => {
  let arr = []
  res.on('data', (data) => {
    arr.push(data)
  })
  res.on('end', () => {
    console.log(Buffer.concat(arr).toString())
  })
})

//发送json数据
req.end('{"name": "mzlin"}')

使用代理

为了解决跨域问题,我们可以将agent-client改造为代理服务器,浏览器通过这个代理服务器请求数据,而代理服务器直接向web服务器获取数据返回给客户端

//服务器
const http = require('http')

const server = http.createServer((req, res) => {
  let arr = []
  req.on('data', (data) => {
    arr.push(data)
  })
  req.on('end', () => {
    console.log(Buffer.concat(arr).toString())
    // 返回数据
    res.end(JSON.stringify('来自web服务的msg'))
  })
})

server.listen(8080, () => {
  console.log('server is running')
})
//代理服务器
const http = require('http')


const options = {
  host: 'localhost',
  port: 7777,
  path: '/',
  method: 'post'
}

const server = http.createServer((request, response) => {
  let req = http.request(options, (res) => {
    let arr = []
    res.on('data', (data) => {
      arr.push(data)
    })
    res.on('end', () => {
      let ret = Buffer.concat(arr).toString()
      response.setHeader('Content-type', 'text/hyml;charset=utf-8')
      response.end(ret)
    })
  })

  req.end('请求web服务器')
})

server.listen(7777, () => {
  console.log('agent server is running')
})

2、静态服务

目标:模拟服务器加载静态资源的过程

在本地准备index.html / index.css 等静态资源,并创建 server.js用于提供服务

//server.js初始代码
const http = require('http')

const server = http.createServer((req, res) => {
  console.log('请求进来了')
})

server.listen(8080, () => {
  console.log('server is start......')
})

当在浏览器请求127.0.0.1:8080时,就会进入到server中来

路径处理

const http = require('http')
const url = require('url')
const path = require('path')

const server = http.createServer((req, res) => {
  //路径处理
  let { pathname } = url.parse(req.url)
  pathname = decodeURIComponent(pathname) //防止中文路径乱码
  let absPath = path.join(__dirname, pathname) //本地资源路径
  res.end(absPath)
})

当在浏览器请求127.0.0.1:8080/index.html时,能够正确解析路径

但是如果用户请求的是不存在的路径,也应当进行404处理

const server = http.createServer((req, res) => {
  //路径处理
  let { pathname } = url.parse(req.url)
  pathname = decodeURIComponent(pathname) //防止中文路径乱码
  let absPath = path.join(__dirname, pathname) //本地资源路径

  // 目标资源处理
  fs.stat(absPath, (err, statObj) => {
    if(err) {
      res.statusCode = 404
      res.end('Not Found')
      return
    }
  })
})

读取资源

const http = require('http')
const url = require('url')
const path = require('path')
const fs = require('fs')

const server = http.createServer((req, res) => {
  //路径处理
  let { pathname } = url.parse(req.url)
  pathname = decodeURIComponent(pathname) //防止中文路径乱码
  let absPath = path.join(__dirname, pathname) //本地资源路径

  //目标资源处理
  fs.stat(absPath, (err, statObj) => {
    if(err) {
      res.statusCode = 404
      res.end('Not Found')
      return
    }
    //如果路径是一个文件,直接读取即可
    if(statObj.isFile()) {
      fs.readFile(absPath, (err, data) => {
        res.setHeader('Content-type', 'text/html;chartset=utf-8')
        res.end(data)
      })
    }
    // 如果路径是一个文件夹,则读取其中的index.html
    else {
      fs.readFile(path.join(absPath, 'index.html'), (err, data) => {
        res.setHeader('Content-type', 'text/html;chartset=utf-8')
        res.end(data)
      })
    }
  })
})

server.listen(8000, () => {
  console.log('server is start......')
})

这样当我们输入localhost:8000/index.html时,就能够访问资源了

边界处理

1、处理响应头contetn-type错误问题

//npm install mime
res.setHeader('Content-type', mime.getType(pathname)+';chartset=utf-8')

3、socket.io

3.1 基本使用

Socket.io是基于Websocket封装的一个框架

官方案例:实时通信

https://socket.io/zh-CN/get-started/chat

服务端

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

const http = require('http');
const server = http.createServer(app);

const { Server } = require("socket.io");
const io = new Server(server);

//处理HTTP协议(使用Express的实例的app)
app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

//处理Websocket协议的使用socket.io的实例io
io.on('connection', (socket) => {
  console.log('a user connected');
});

server.listen(8888, () => {
  console.log('listening on *:8888');
});

客户端

<!DOCTYPE html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
      body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }

      #form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
      #input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
      #input:focus { outline: none; }
      #form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }

      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages > li { padding: 0.5rem 1rem; }
      #messages > li:nth-child(odd) { background: #efefef; }
    </style>
  </head>
  <body>

    <ul id="messages"></ul>

    <form id="form" action="">
      <input id="input" autocomplete="off" /><button>Send</button>
    </form>

    <script src="/socket.io/socket.io.js"></script>
    <script>
      var socket = io();  

            socket.on('connect', () => {
        console.log('连接成功')
      })
      //断开了连接
      socket.on("disconnect", (reason) => {
        if(reason === "io server disconnect") {
          socket.connect() //尝试重连
        }
      })
      socket.on('connect_error', (error) => {
        console.log('连接失败', error)
      })
    </script>
  </body>
</html>

启动服务,就可以在浏览器看见服务器socket连接成功了a user connected,客户端打印连接成功

客户端收发消息

<script src="/socket.io/socket.io.js"></script>
<script>
  var socket = io();

  var messages = document.getElementById('messages');
  var form = document.getElementById('form');
  var input = document.getElementById('input');

  form.addEventListener('submit', function(e) {
    e.preventDefault();
    if (input.value) {
      socket.emit('chat message', input.value);
      input.value = '';
    }
  });

  socket.on('chat message', function(msg) {
    var item = document.createElement('li');
    item.textContent = msg;
    messages.appendChild(item);
    window.scrollTo(0, document.body.scrollHeight);
  });
</script>

服务器收发消息

//处理Websocket协议的使用socket.io的实例io
io.on('connection', (socket) => {
  socket.on('chat message', (msg) => {
    io.emit('chat message', msg);
  });
});

群发信息

由于每个用户连接上服务器都回触发“connection”,并创建一个socket。可以利用这个特点群发信息

//存储所有用户的socket通信端口
const clients = []

//连接成功只会发一次
io.on('connection', (socket) => {
  clients.push(socket)
  socket.on('chat message', (msg) => {
    console.log(msg)
    io.emit('chat message', msg);
  });

  // 发送给当前所有已连接的在线用户
  clients.forEach(item => {
    item.emit('chat message', '这是群发的信息')
  })
});
  // 刷新或断开连接时,移除对应用户的socket
  socket.on("disconnect", () => {
    const index = clients.findIndex(item => item === socket)
    if(index !== -1) {
      clients.splice(index, 1)
    }
  })

事实上,socket.io提供了群发消息的api,我们不用自己实现

//连接成功只会发一次
io.on('connection', (socket) => {
  socket.on('chat message', (msg) => {
    // 群发消息
    io.emit('chat message', msg)
    // 发送给不包括当前用户的其他用户
    // socket.broadcast('chat message', msg)
  })

3.2 案例

在vue中使用socket.io

安装npm install socket.io-client

<template>
  <div class="home">
    <ul id="messages"></ul>

    <form id="form" action="">
      <input id="input" autocomplete="off" /><button>Send</button>
    </form>
  </div>
</template>
<script setup>
  import { io } from 'socket.io-client'
  const socket = io("http://localhost:8888", {
  //重连的延迟时间
  reconnectionDelayMax: 10000, 
  // 身份信息
  auth: {
    token: "123"
  },
  //自定义查询参数
  query: {
    "my-key": "my-value"
  }
});

socket.on('connect', () => {
    console.log('连接成功')
  })
socket.on('disconnect', () => {
  console.log('断开连接')
})

socket.on('connect_error', err => {
  console.log('连接失败', err)
})
</script>
<style>
body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
#form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
#input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
#input:focus { outline: none; }
#form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages > li { padding: 0.5rem 1rem; }
#messages > li:nth-child(odd) { background: #efefef; }
</style>  
//server.js
const express = require('express');
const app = express();

const http = require('http');
const server = http.createServer(app);

const { Server } = require("socket.io");
const io = new Server(server);

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

io.on('connection', (socket) => {
  socket.on('chat message', (msg) => {
  //   console.log(msg)
  //   io.emit('chat message', msg);
  // });

    // 群发消息
    io.emit('chat message', msg)
    // 发送给不包括当前用户的其他用户
    // socket.broadcast('chat message', msg)
  })



  // 刷新或断开连接时,移除对应用户的socket
  socket.on("disconnect", () => {
    const index = clients.findIndex(item => item === socket)
    if(index !== -1) {
      clients.splice(index, 1)
    }
  })

});

server.listen(8080, () => {
  console.log('listening on *:8888');
});

运行vue项目与app服务,即可看到项目成功打通了

如果前端项目与server存在跨域问题,则需要进行配置

//添加
const { Server } = require("socket.io");
const io = new Server(server, {
  cors: {
    origin: "*" // 允许所有
  }
});

增加用户登录

<template>
  <div class="login">
    <form action="">
      <div>
        <label for="name">
        用户名:
        <input type="text" id="name" v-model="user.name">
      </label>
      </div>
      <div>      
        <label for="password">
        密码:
        <input type="password" id="password" v-model="user.passsord">
        </label></div>
      <div><button @click.prevent="loginbtn">登录/注册</button></div>
    </form>
  </div>
</template>

<script setup>
import { reactive } from 'vue'
import axios from 'axios'

const user = reactive({
  name: '',
  passsord: ''
})

const loginbtn = async() => {
  try {
    const { data } = await axios.post('http://localhost:8888/api/login', user)
  } catch (error) {
    console.log(error.message)
  }
}
</script> 
<style></style>

点击登录时,遇到跨域问题

安装cors库

//server.js
const cors = require('cors')
// http跨域问题处理
app.use(cors())

完整代码

//server
//server.js
const express = require('express');
const router = require('./server-login')
const cors = require('cors')
const app = express();

const http = require('http');
const server = http.createServer(app);

//配置解析post请求体
app.use(express.json())
// http跨域问题处理
app.use(cors())
app.use(router)

const { Server } = require("socket.io");
const io = new Server(server, {
  cors: {
    origin: "*" // 允许所有
  }
});

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

io.on('connection', (socket) => {
  socket.on('chat message', (msg) => {
  //   console.log(msg)
  //   io.emit('chat message', msg);
  // });

    // 群发消息
    io.emit('chat message', msg)
    // 发送给不包括当前用户的其他用户
    // socket.broadcast('chat message', msg)
  })

  // 刷新或断开连接时,移除对应用户的socket
  // socket.on("disconnect", () => {
  //   const index = clients.findIndex(item => item === socket)
  //   if(index !== -1) {
  //     clients.splice(index, 1)
  //   }
  // })

});

server.listen(8888, () => {
  console.log('listening on *:8888');
});


//server-login
const express = require('express')
const mongoose = require('mongoose')


// 创建用户模型
const userSchema = new mongoose.Schema({
  username: {
    type: String,
    require: true
  },
  password: {
    type: String,
    require: true
  }
})

//连接数据库
mongoose.connect('mongodb://localhost/chat-demo', {
  useNewUrlParser: true, useUnifiedTopology: true
})
const db = mongoose.connection
db.on('error', err => {
  console.log('连接失败', err)
})
db.once('open', function() {
  console.log('连接成功')
})
const User = mongoose.model('User', userSchema)


const router = express.Router()

router.post('/login', async(req, res, next) => {
  const { username, password } = req.body

  let user = await User.findOne({username})
  if(user) {
  //有账号,登录
  } 
  else {
  //无账号,注册
  user = await new User(req.body).save()
  }

  res.status(200).send({
    user: {
      username: user.username,
      status: 1
    }
  })
})

module.exports = router

4、NoSql

背景

传统的关系型数据库的弊端:

  • 难以应付每秒上万次的高并发数据写入
  • 查询上亿量级的速度及其缓慢
  • 分库、分表形式形成的子库到达一定规模后难以进一步拓展
  • 分库、分表的规则可能回因为需求变更而发生变更
  • 修改表结构困难

在数据量暴增的时代,若想用传统的关系型数据库来满足数据高并发读写、巨量数据的存储、数据库的扩展和高可用、则需要增加软硬件的规格、这将大幅度提高成本。

而NoSql,非关系型数据库把数据直接放进一个大仓库,不标号,不连线,单纯的堆起来,从而提高了对海量数据的高性能存储及访问需求

NoSol分类

(1)键值数据库

这类数据库主要使用数据结构中的key来查找特定数据的value。优势在于存储时不采用任何模式,因此极易添加数据。这类数据库有极高的读写性能,用于处理大量数据的高访问负载比较合适。主要代表:Redis、Flare

(2)文档型数据库

这类数据库满足了海量数据的存储和访问需求、同时对字段要求不严格、可以随意增加、删除、修改字段、且不需要预先定义表结构,适用于各类网络应用

主要代表:MongoDB

(3)列存储型数据库

这类数据库查找速度快,可扩展性强,适合用作分布式文件存储系统。

(4)图数据库

这类数据库利用“图结构”的相关算法来存储实体之间的关系信息,适合用于构建社交网络和推荐系统的关系图谱

5、MongoDB

5.1 安装与基本使用

官网https://www.mongodb.com/

参考链接:https://blog.csdn.net/weixin_44275686/article/details/125390302

在mac上安装

【步骤 1】解压缩刚刚下载的压缩包,并将其重命名为 mongodb:

sudo tar -zxvf mongodb-macos-x86_64-4.4.3.tgz     # 解压 MongoDB 压缩包
sudo mv mongodb-osx-ssl-x86_64-4.0.17/ mongodb  # 重命名解压得到的文件夹

【步骤 2】在 /usr/local/mongodb 目录下新建两个文件夹 data 和 log,用于存储 MongoDB 的数据和日志。

sudo mkdir -p /usr/local/mongodb/data
sudo mkdir -p /usr/local/mongodb/log

使用如下命令为当前用户分配目录的读写权限:

sudo chown username /usr/local/mongodb/data
sudo chown username /usr/local/mongodb/log

其中“username”为当前的用户名,您需要修改为您当前系统的用户名。

【步骤 3】配置 PATH。在终端中输入open -e .bash_profile命令打开 bash_profile 配置文件,然后将 MongoDB 的安装目录下的 bin 目录添加到环境变量中,如下所示:

export PATH=${PATH}:/usr/local/mongodb/bin 

编辑完成后保存并退出,然后使用source .bash_profile命令使配置立即生效。

最后通过mongod -version命令查看是否安装成功

启动与停止

mongod --dbpath="数据存储目录"

注:默认占用27017端口

注意:在新版的mac中,.bash_profile文件已弃用。更换终端都需要重新使用open -e .bash_profile命令。我们可以新建一个.zshrc文件,将该命令写入其中,并在终端运行source ~/.bash_profile,就解决了

停止服务在连接上客户端的命令行中进行

use admin;
db.shutdownServer();

mongo shell 是 MongoDB 的交互式 JavaScript 接口。可以使用mongo shell 来查询和更新数据以及执行管理操作。

1、连接默认端口上的本地mongodb服务

mongo

2、连接本地其他端口

mongo --port 28015

3、连接远程主机上的服务

mongo "mongodb://mongodb0.example.com:28015"
//或者
mongo --host mongodb0.example.com --port 28015

4、连接具有身份认证的服务

mongo "mongodb://用户身份@mongodb0.examples.com:28015/?anthSource=admin"
//或者
mongo --username mzlin --password xxx --authenticationDatabase adlin --host mongodb0.example.com --port 28015

注意:在mac上使用mongosh工具,brew install mongosh 参考链接:https://www.cnblogs.com/Denny_Yang/p/16666375.html

连接成功后进入一个js环境的终端,可以通过执行show dbs等mongodb命令

5.2 mongodb基础概念

存储结构:数据库 》 集合 〉文档

{
  //数据库
  "dbname": {
    //集合collection,对应关系型数据库的table
    "users": [
      //文档document,对应关系型数据库的行row
      {
        //数据字段field,对应关系型数据库的列column
        "id": 1,
        "username": "mzlin",
        "password": "1234567"
      }
    ]
  }
}

常见操作

1、查看数据库列表 show dbs

2、查看当前数据库 db

3、创建/切换数据库 use db_name

只有数据库中有了数据,才会被真正创建出来

d b.users.insert({ name: "mzlin", age: 22 })

4、删除数据库 db.dropDatabase()

集合

1、创建集合 db.users.insert({ name: "mzlin" })

往集合中创建数据,集合会被自动创建

也可以通过db.createCollection方法来创建自定义规则的集合

2、查看集合 shwo collections

3、删除集合 db.集合名.drop()

文档

mongodb将数据记录存储为BSON文档(json的二进制表示形式,拥有更多的数据类型)

{
  field1: value1,
  name: "mzlin",
  status: 1,
     groups: ["ddd"]
  ...
  fieldN: valueN
}

注意:字段名称_id保留作为主键(自动生成),它的值在集合中必须是唯一的,不可变,并且可以是除数组外的任何类型

用法 说明
db.collection.insertOne() 插入单个文档到集合中
db.collection.insertMany() 插入多个文档到集合中
db.collection.insert() 将一个或多个文档插入到集合中
db.collection.find(query, projection) 查看集合中的所有文档
db.collection.findOne() 返回符合查询结果的一个

5.3 查询

创建一个集合,并往其中插入一些测试数据

db.inventory.insertMany([
    {item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm"}, status: "A"},
    {item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "im"}, status: "A"},
    {item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "im"}, status: "D"},
    {item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm"}, status: "D"},
    {item: "postcard", qty: 45, size: { h: 10, w: 25, uom: "cm"}, status: "A"},
])

1、db.inventory.find({})

默认查询所有内容,但也可以传入一些 查询参数

返回指定字段

db.inventory.find({}, {
  item: 1, 
  qty: 1 
})

1表示包括,0表示不包括,二者不能同时出现

想等条件查询

db.inventory.find({status: "D"})

status===D等哪一行数据被查询

指定AND条件

db.inventory.find({status: "A", qty: { $lt: 30 }})

$lt表示小于号,这个语句表示查询status=A,且qty小于30的哪一行内容

更多运算符可查看文档: https://www.mongodb.com/docs/manual/reference/operator/query/

指定OR条件

使用$or运算符

db.inventory.find({
  $or:[
    { status: "A" },
    { qty: { $lt: 30  } }
  ]
})

指定AND和OR条件

db.inventory.find({
  status: "A",
  $or: [
    { qty: { $lt: 30 } },
    { item: /^p/ } //以p开头 
  ]
})

查询嵌套文档

1、匹配嵌套文档

db.inventory.find({
    size: { h: 14, w:21, uom: "cm" }
})

必须完全匹配每一项,更换顺序也不能被匹配到

2、查询嵌套字段

db.inventory.find({
    "size.uom": "in"
})
db.inventory.find({
    "size.h": { $lt: 15 }
})

3、指定AND条件

db.inventory.find({
    "size.h": { $lt: 15 },
  "size.uom": "in",
  status: "D"
})

指定从查询返回的项目字段

默认情况下,mongodb回返回匹配文档的所有字段。我们也可以进行限制

1、返回匹配的所有字段

db.inventory.find({status: "A"})

2、仅返回指定字段和_id字段

db.inventory.find({ status: "A" }, { item: 1, status:1 })

不要_id字段

db.inventory.find({ status: "A" }, { item: 1, status:1, _id:0 })

查询空字段或缺少字段

db.inventory.insertMany({
  { _id: 1, item: null },
  { _id: 2 }
})
db.inventory.find( { item: null  } )

查询将匹配包含其值为null的item字段或不包含item字段的文档

db.inventory.find({ item: { $type: 10 } })

仅匹配包含item字段,其值为null的文档

db.inventory.find( {item: {$exists: false}} )

查询仅返回不包含item字段的文档

更新文档

db.collection.updateOne(<filter>, <update>, <options>)
db.collection.updateMany(<filter>, <update>, <options>)
db.collection.replaceOne(<filter>, <update>, <options>)

示例

db.users.updateMany(
    { age: { $lt: 18 } },
  { $set: { status: "reject" } }
)

$set用于修改字段

删除文档

db.collection.deleteMany()
db.collection.deleteOne()

删除所有文档

db.collection.deleteMany()

删除符合条件的文档

db.inventory.deleteMany({status: "A"})

删除1个符合条件的文档

db.inventory.deleteOne({status: "D"})

5.4 查询数组

测试数据

db.inventory2.insertMany([
    {item: "journal", qty: 25, tags: ["blank", "red"], dim_cm: [14, 21]},
    {item: "notebook", qty: 50, tags: ["red", "blank"], dim_cm: [14, 21]},
    {item: "paper", qty: 100, tags: ["red", "blank", "plain"], dim_cm: [14, 21]},
    {item: "planner", qty: 75, tags: ["blank", "red"], dim_cm: [22.85, 30]},
    {item: "postcard", qty: 45, tags: ["blue"], dim_cm: [10, 15.25]},
])

匹配一个数组

db.inventory2.find({
  tags: ["red", "blank"]
})

顺序必须完成相同

db.inventory2.find({
  tags: { $all: ["red", "blank"] }
})

查询数组中的元素

db.inventory2.find({
  tags: "red"
})

包含tags:“red”都会被查询

db.inventory2.find({
  dim_cm: { $gt: 25 }
})

为数组元素指定多个条件

db.inventory2.find({
  dim_cm: { $gt: 15, $lt: 20 }
})

查询满足多个条件的数组元素

$elemMath可以在数组的元素上指定多个条件

db.inventory2.find({
  dim_cm: { $eleMatch: { $gt: 22, $lt: 30 } }
})

通过数组索引位置查询元素

使用点符号(必须包括中引号内)

db.inventory2.find( { "dim_cm.1": { $gt: 25 } } )

表示查询数组dim_cm中第二个大于25的所有文档

通过数组长度查询数组

使用$size运算符

db.inventory2.find( { "tags": { $size: 3 } } )

查询嵌套文档的数组

测试数据

db.inventory3.insertMany([
    {item: "journal", instock: [ { warehouse: "A", qty:5 } , {warehouse: "C", qty:5}]},
    {item: "notebook", instock: [ { warehouse: "C", qty:5 }]},
    {item: "paper", instock: [ { warehouse: "A", qty:60 } , {warehouse: "B", qty:5}]},
    {item: "planner", instock: [ { warehouse: "B", qty:40 } , {warehouse: "C", qty:5}]},
    {item: "postcard",instock: [ { warehouse: "B", qty:15 } , {warehouse: "C", qty:5}]},
])

1、查询嵌套在数组中的文档

db.inventory3.find({
  "instock": { warehouse: "A", qty: 5 }
})

2、指定查询条件

db.inventory3.find({
  "instock.qty": { $lte: 20 }
})

$lte小于等于

3、使用索引

db.inventory.find({ 'instock.0.qty': { $lte:20 } })

4、满足多个查询条件

db.inventory3.find({
  "instock" {
          $eleMatch: { qty: 5, warehouse: "A" }
    }
})

表示同时包含qty等于5和warehouse等于A

5、元素组合满足标准

复合查询条件为使用$eleMatch运算符,则查询将选择其数组包含满足条件的元素的任意组合的文档

db.inventory3.find({
  "instock.qty": { $gt: 10, $lte: 20 }
})

5.5 Node连接数据库

参考文档:https://docs.mongodb.com/driver/node

官方库安装:npm install mongodb

const { MongoClient } = require('mongodb')

const client = new MongoClient('mongodb://127.0.0.1:27017')

async function run() {
  try {
    // 开始连接
    await client.connect()
    const testDB = client.db('test')
    const inventoryCollection = testDB.collection("inventory")
    const res = await inventoryCollection.find()
    console.log(await res.toArray())
  } catch (err) {
    console.log('连接失败', err)
  } finally {
    // 关闭连接
    await client.close()
  }
}

run()

简单案例

安装:npm install express mongodb

const express = require('express')
const { MongoClient } = require('mongodb')

const client = new MongoClient('mongodb://localhost:27017')
const app = express()
app.use(express.json()) //解析json

app.post('/write', async(req, res) => {
  const { name, age } = req.body
  if(!name || !age) return 

  //连接数据库
  await client.connect()
  const usersCollection = client.db('test').collection('users')
  await usersCollection.insertOne({name, age})
  res.status(200).json({
    name,
    age
  })
})

app.listen(8888, () => {
  console.log('server is running.')
})

6、Redis

6.1 概述

官网:https://redis.io/

推荐图形化工具:RDM

Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库

Redis数据库中的所有数据都存储在内存中。相对于磁盘,内存的数据读/写速度要快很多,所以Redis一般用做缓存数据库。在一台普通电脑上,Redis可以在一秒内读写超过10万个键值。

Redis也提供了持久化的支持,可以将内存中的数据异步写入到磁盘中,同时不影响继续提供服务

功能

(1)作为缓存系统

(2)作为队列系统

(3)“发布订阅”功能

特点

Redis是一个key-value存储系统,大部分情况下是因为其高性能的特性,被当作缓存使用。

  • 读写性能优异
  • 持久化
  • 数据类型丰富
  • 单线程
  • 数据自动过期
  • 发布订阅
  • 分布式

安装

mac上安装: brew install redis

编译后在Redis源代码目录的src文件夹中会有以下几个可执行文件

redis-server 服务redus-cli 命令行...

1、启动服务redis-server

默认使用6379端口

2、启动服务指定端口redis-server --port 7777

3、在后台运行redis redis-server --daemonize yes

验证ps -ef | grep -i redis

4、停止redis redis-cli shutdown

5、连接服务 redis-cli

也可以指定服务器地址和端口连接redis-cli -h 127.0.0.1 -p 7777

6、断开连接 quit

redis提供了一个配置文件的模板redis.conf,位于源代码的根目录

启动该配置文件redis-server 配置文件目录

6.2 基本操作

多数据库

redis支持的数据类型string / hash / List / Set / ZSet ...

redis默认支持16个数据库,分别以0,1,2...15命令。不支持自定义数据库名字。可以配置参数databases修改支持的数据库个数

连接上redis后,默认选择0号数据库

通用命令

// 返回所有key
    keys *

// 返回以 my 开头的key
    keys my*

// 获取key类型
  type key

//查询key是否存在
  exists key

//将key改名为newkey
  rename key newkey

//从当前数据库中随机返回
  randomkey

//清空当前数据库所有内容
  flushdb

//清空所有数据库内容
  flushall

过期时间

在redis中可以设置一个键的过期时间,到时间后redis会自动删除它

//给定key设置生存时间,过期时自动清除
expire key seconds

// 查看剩余时间
ttl key

//清除过期时间
persist key

字符串

SELECT 1 // 切换数据库

//String
//添加
SET key value

//修改指定key的value
GETSET key value

//只有key不存在时设置key的值
SETNX key value

//同时设置一个或多个key-value
MSET key value [key value...]

注意:在redis命令不区分大小写。但是一般约定以大写的形式确定是redis命令

查询

//String

//获取指定key的值
get key

//返回key中字符串值的子字符
getrange key start end

// 获取所有给定的key的值
mget key1 key2 。。。

// 返回key所存储的字符串值的长度
strlen key

// 是否存在指定key
exists key

// 判断类型
type key

删除

//删除1个或多个指定的key
del key1 key2

数字值

数字类型的操作与字符串大致相同,但有一些特有操作

//将key中存储的数字值增1
incr key


//将key所存储的值加上给定的增量值
incrby key incement

//-1
decr key

// -n
decrby key decrement

列表

列表类似于数字,基于 双向链表实现,所以对于读取头部与尾部的效率非常高。

具体操作参考文档

哈希

哈希类型是一种字典结构,存储了字段和字段值的映射,且字符值只能是字符串。类似js对象

集合

集合中的元素是唯一的,无序的。可以理解为没有顺序且不重复的列表。

利用集合常用的操作是向集合中加入或删除元素、判断某个元素是否存在等。这些操作的时间复杂度为O(1)

6.3 持久化

redis性能好的一个原因是将数据存在内存中。但是当数据库重启或者宕机了,数据就会丢失。

持久化就是希望redis能将数据从内存中以某种形式同步到硬盘中,使得重启后可以根据硬盘的数据恢复数据

RDB持久化

根据指定规则“定时”将内存中的数据存储在硬盘上,在重启之后读取硬盘上的.rdb快照文件将数据恢复到内存中

AOF持久化

AOF持久化记录服务器执行的所有操作命令形成.aof日志文件保存到硬盘中,并在服务器启动时,通过重新执行这些命令来还愿数据

6.4 node连接数据库

安装库:ioredis

const ioredis = require('ioredis')

// 1.建立连接
const redis = new ioredis({
  port: 6379,
  host: '127.0.0.1'
})

// 2.操作redis数据库
const action = async() => {
  try {
    const res = await redis.set('name', 'mzlin')
    console.log('操作成功',res)
  } catch (error) {
    console.log('操作失败',error)
  }
}
action()

批量发送命令(性能较好)

const ioredis = require('ioredis')

// 1.建立连接
const redis = new ioredis({
  port: 6379,
  host: '127.0.0.1'
})

// 2.操作redis数据库
const action = async() => {
  try {
    const pipeline = redis.pipeline()
    for(let i = 0; i < 20; i++) {
      pipeline.set(`friend-${i}`, `name${i}`)
    }
    const res = await pipeline.exec() //执行
    console.log(res)
  } catch (error) {
    console.log('操作失败',error)
  }
}
action()

7、express

7.1 基础

Express是一个快速,简单,极简的Node.js web应用开发框架,通过它,可以轻松的构建各种web应用

特性

1、简单易学

2、丰富的基础API支持,以及常见的HTTP辅助程序

3、强大的路由功能

4、灵活的中间件

5、高性能

6、非常稳定

7、视图系统支持14个以上的主流模版引擎

常见应用场景

1、传统的web网站

2、接口服务

3、服务端渲染中间层

4、开发工具(如webpack-dev-server)

路由

路由是指确定应用程序如何响应客户端特定端点的请求,该特定端点是URL和特定的HTTP请求方法(get,post等)

//基本结构
app.get(path, handler)

req / res

注意:express是对node进行二次抽象,并且扩展了一些web所需的基本功能,其内部使用的还是http模块。

请求对象继承自 http.incomongMessage

响应对象继承自http.ServerResponse

res响应内容的方式

res.write() //多个,会依次执行,拼接返回,需要res.end()表明结束

res.end(content) //传递内容比较单一

res.send(content) //支持多种格式

res.json()

路由设计

//动态路由
app.get('/path/:id', (req, res) => {
  res.status(200).send(req.params.id)
})

//返回json数据
app.get('/path', (req, res) => {
  res.status(200).json(data)
})

//解析json请求体:application/json
app.use(express.json())
app.post('/path', (req, res) => {
  const data = req.body
  res.end()
})

//解析表单请求体
app.use(express.urlencoded())
app.post('/path', (req, res) => {
  const data = req.body
  res.end()
})

7.2 中间件

在express中,中间件就是一个可以访问请求对象,响应对象和调用next方法的函数

所有请求都会经过中间件的处理,中间件依次执行

1、日志

app.use((req, res, next) => {
  //请求日志相关处理
  console.log(req.method, req.url, Data.now())
  next()
})

2、错误处理中间件

router.get('/path', async(req, res, next) => {
  try{
    ...
  }catch(err) {
    // 任何内容传递给next(字符串‘route’除外),express都将视为错误,直接跳转到错误处理中间件
    next(err)
  }
})

//错误处理中间件需要在所有路由之后使用
app.use((err, req, res, next) => {
   //错误处理
})

3、处理404

通常需要在所有路由之后处理404内容

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

//所有路由都匹配不上,将进入该中间件
app.use(req, res, next)=> {
  res.status(404).send("404 Not Found")
}

4、路由

路由可看做是一个mini Express实例

const router = express.Router()

//配置路由
router.get('/foo', (req, res) => {
  ...
})

module.exports = router

//main.js
app.use(router)

5、内置中间件

express.json() //解析Content-Type为application/json的请求题

express.urlencoded()//Content-Type:application/x-www-form-urlencoded

express.raw //Content-Type:application/octet-stream

express.text()//Content_type:text/plain

express.static() //托管静态资源文件

7.3 传统web应用

传统web应用是服务端渲染的,统一由后端返回一个静态页面

//app.js
<script>
const express = require('express')
const fs = require('fs')
const info = require('./data/index')
const app = express()

app.get('/', (req, res) => {
  fs.readFile('./views/index.html', 'utf8', (err, data) => {
    if(err) return res.status(404).send('Not Found')

    // 动态页面渲染:数据 + 模板
    const str = `my name is ${info.name}, i come from ${info.address}`
    const ret = data.replace('~_~', str)
    res.send(ret)
  })
})

app.listen(3000, () => {
  console.log('server is running')
})
</script>


//index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- 指定一个特殊标记,用于替换 -->
  <div>~_~</div>
</body>
</html>

但是在开发中,如果手动进行替换会很麻烦,这个时候就需要用到模板引擎

模板引擎就是根据特定的规则进行字符串解析替换

npm install art-template

<script>
const template = require('art-template')

app.get('/', (req, res) => {
  fs.readFile('./views/index.html', 'utf8', (err, data) => {
    if(err) return res.status(404).send('Not Found')

    // 动态页面渲染:数据 + 模板
    const str = `my name is ${info.name}, i come from ${info.address}`
    const ret = template.render(data, {
      str
    })
    res.send(ret)
  })
})

</script>

//index.html
<div>{{ str }}</div>

art-template也可以和exrpess结合,使得代码更加简洁

npm install --save art-template
npm install --save express-art-template
const express = require('express')
const info = require('./data/index')
const path = require('path')

const app = express()

// view engine setup
app.engine('art', require('express-art-template'));
app.set('view options', {
    debug: process.env.NODE_ENV !== 'production'
});
app.set('views', path.join(__dirname, 'views')); //模板文件目录
app.set('view engine', 'art'); //可以省略的模板文件后缀名


// routes
app.get('/', function (req, res) {
  const str = `my name is ${info.name}, i come from ${info.country}`
  res.render('index.art', { str });
});

app.listen(3000, () => {
  console.log('server is running')
})


//views/index.art
<div>{{ str }}</div>

托管静态资源

如果在index.html中直接引用css文件js文件等静态资源,客户端会找不到该文件而报错

<body>
  <link rel="stylesheet" href="./assets/base.css">
  <div>{{ str }}</div>
</body>

express内置了一个中间件解决这个问题

app.use(express.static('./assets'))
//这样子访问资源可以不用加目录
<body>
  <link rel="stylesheet" href="base.css">
  <div>{{ str }}</div>
</body>

最好的方式,并且可以加上资源目录

app.use('/assets',express.static(path.join(__dirname, './assets')))
<body>
  <link rel="stylesheet" href="./assets/base.css">
  <div>{{ str }}</div>
</body>

托管静态资源也可以托管多个

app.use(express.static('./node_modules'))
app.use(express.static('.//assets'))
//这样可以在客户端访问这些资源

8、restful协议

resful

重要概念

1、资源(resource)

服务端拥有的一个个资源,一个资源用一个URL表示

命名:/user/login ===> /token

2、表示(representation)

服务端的资源在客户端进行表示,客户端拥有操作服务端资源的方法

命名:GET/products ===> 产品列表组件

3、转换(transfer)

客户端收到新的表示,从而向用户展示新的内容

优点

  • 客户端无状态(简化开发)

  • 资源独立、接口间独立(缓存好设计)

  • 对http协议依赖不严重(可迁移)

9、文件上传

9.1 form表单文件上传

//form.html
    这是一个表单
    <form action="file" method="post" enctype="multipart/form-data">
        <div><input type="file" name="file"></div>
        <input type="submit" value="提交">
    </form>
//node main
const express = require("express");
const path = require("path");
const fileUpload = require("express-fileupload");

const app = express();
app.get("/submit", (req, res) => {
  res.sendFile(path.resolve(__dirname, "form.html"));
});

app.post("/file", fileUpload(), (req, res) => {
  console.log(req.files);
  req.files.file.mv(path.resolve(__dirname, "a.jpg"));
  res.status(201).send("ok");
});
app.listen(3000);

9.2 HTML5方式

前端代码

<body>
        <input type="file" name="file" id="file" >
        <br/>
        <input type="submit" onClick="submit()">

        <script>
            let upload = {} //存储要上传的文件
            function submit () {
                fetch('fileb64', {
                    method: 'post',
                    body: JSON.stringify(upload),
                    headers: {
                        'Content-Type': "application/json"
                    }
                })
            }

            // 监听change方法
            document.getElementById("file").addEventListener('change', e=> {
                const files = e.target.files 
                for(let file of files) {
                    const fr = new FileReader() //Blob类型
                    fr.readAsDataURL(file)
                    fr.onload = () => {
                        console.log('base64:',fr.result);
                        // data:image/png;base64, 这段类型描述无需上传,截去22个字符
                        upload.data = fr.result.substr(22)
                        upload.name = file.name
                    }
                }
            })
        </script>
</body>

服务端

app.get("/submitb64", fileUpload(), (req, res) => {
  res.sendFile(path.resolve(__dirname, "h5fileupload.html"));
});

app.use(bodyParser.json());

app.post("/fileb64", (req, res) => {
  const buffer = Buffer.from(req.body.data, "base64");
  fs.writeFileSync(path.resolve(__dirname, "x.jpg"), buffer);
  res.send("ok");
});

但是图片转换为base64体积变大了,可以使用另外一种方式

<body>
        <input type="file" name="file" id="file" >
        <br/>
        <input type="submit" onClick="submit()">

        <script>
            let upload = {} //存储要上传的文件
            function submit () {
                const formData = new FormData()
                formData.append('name', upload.name)
                formData.append('file', upload.data)
                fetch('fileBlob', {
                    method: 'post',
                    body: formData,
                    headers: {}
                })
            }

            // 监听change方法
            document.getElementById("file").addEventListener('change', e=> {
                const files = e.target.files 
                for(let file of files) {
                    upload.data = file
                    upload.name = file.name
                    }

            })
        </script>
</body>
app.post("/fileBlob", fileUpload(), (req, res) => {
  req.files.file.mv(path.resolve(__dirname, "a.jpg"));
  res.status(201).send("ok");
});

9.3 大文件上传切片

前端代码

<body>
    <input type="file" id="input">
    <button id="uploadBtn">上 传</button>
    <!-- 上传进度 -->
    <div style="width: 300px" id="progress"></div>

    <script>
        const inputDOM = document.getElementById('input')
        const uploadBtn = document.getElementById('uploadBtn')
        let files = {}//创建一个文件对象
        let chunkList = []//存放切片的数组

        inputDOM.addEventListener("change", (e) => {

            files = e.target.files[0]
            chunkList =  createChunk(files)
            console.log('切片',chunkList);
        })

        // 创建切片
        function createChunk(file, size = 1 * 1024 * 1024) {
            console.log(files);
            const chunkList = []
            let cur = 0
            while(cur < file.size) {
                chunkList.push({
                    // file(Blob)
                    file: file.slice(cur, cur + size) 
                })
                cur += size
            }
            return chunkList
        }

        // 文件上传
        uploadBtn.addEventListener('click', () => {
            const uploadList = chunkList.map(({file}, index) => ({
                file,
                size: file.size,
                percent: 0,
                chunkName: `${files.name}-${index}`,
                fileName: files.name,
                index
            }))
            //发请求,调用函数
             uploadFile(uploadList)
        })


        //请求函数
        function axiosRequest({method = "post",url,data}) {
            return new Promise((resolve, reject) => {
                const config = {//设置请求头
                    headers: 'Content-Type:application/x-www-form-urlencoded',
                }
                //默认是post请求,可更改
                axios[method](url,data,config).then((res) => {
                    resolve(res)
                })
            })
        }


        // 数据处理
        async function uploadFile(list) {
            const requestList = list.map(({file, fileName, index, chunkName}) => {
                // 创建表单类型数据
                const formData = new FormData() 
                formData.append('file', file)
                formData.append('fileName', fileName)
                formData.append('chunkName', chunkName)
                return {formData, index}
            })

            const formatData = requestList.map(({formData, index}) => axiosRequest({
                method:'post',
                url:'http://localhost:3333/upload',
                data: formData
             }).then(res => {
                console.log(res);
                // 显示每个切片上传进度
                let p = document.createElement('p')
                p.innerHTML = `${list[index].chunkName}--${res.data.message}`
                document.getElementById('progress').appendChild(p)
             }))

            //  //保证所有的切片都已经传输完毕
             await Promise.all(formatData)
             //调用函数,当所有切片上传成功之后,通知后端合并
             console.log('-----通知合并');
             merge(files.size, files.name)
        }

        // 通知后端去做切片合并
            function merge(size, fileName) {
                axiosRequest({
                    method: 'post',
                    url: 'http://localhost:3333/merge',//后端合并请求
                    data: JSON.stringify({
                        size,
                        fileName
                    }),
                })
            }
    </script>

</body>

服务端代码

//app.js
const http = require("http");
const multiparty = require("multiparty"); // 中间件,处理FormData对象的中间件
const path = require("path");
const fse = require("fs-extra"); //文件处理模块

const server = http.createServer();
const UPLOAD_DIR = path.resolve(__dirname, ".", "qiepian"); // 读取根目录,创建一个文件夹qiepian存放切片

server.on("request", async (req, res) => {
  // 处理跨域问题,允许所有的请求头和请求源
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Headers", "*");

  if (req.url === "/upload") {
    //前端访问的地址正确
    const multipart = new multiparty.Form(); // 解析FormData对象
    multipart.parse(req, async (err, fields, files) => {
      if (err) {
        //解析失败
        return;
      }
      //   console.log("fields=", fields);
      //   console.log("files=", files);

      const [file] = files.file;
      const [fileName] = fields.fileName;
      const [chunkName] = fields.chunkName;

      const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`); //在qiepian文件夹创建一个新的文件夹,存放接收到的所有切片
      if (!fse.existsSync(chunkDir)) {
        //文件夹不存在,新建该文件夹
        await fse.mkdirs(chunkDir);
      }

      // 把切片移动进chunkDir
      await fse.move(file.path, `${chunkDir}/${chunkName}`);
      res.end(
        JSON.stringify({
          //向前端输出
          code: 0,
          message: "切片上传成功",
        })
      );
    });
  }
  if (req.url === "/merge") {
    // 该去合并切片了
    const data = await resolvePost(req);
    const { fileName, size } = data;
    const filePath = path.resolve(UPLOAD_DIR, fileName); //获取切片路径
    await mergeFileChunk(filePath, fileName, size);
    res.end(
      JSON.stringify({
        code: 0,
        message: "文件合并成功",
      })
    );
  }
});

server.listen(3333, () => {
  console.log("服务已启动");
});

// 合并
async function mergeFileChunk(filePath, fileName, size) {
  const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);

  let chunkPaths = await fse.readdir(chunkDir);
  chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);

  const arr = chunkPaths.map((chunkPath, index) => {
    return pipeStream(
      path.resolve(chunkDir, chunkPath),
      // 在指定的位置创建可写流
      fse.createWriteStream(filePath, {
        start: index * size,
        end: (index + 1) * size,
      })
    );
  });
  await Promise.all(arr); //保证所有的切片都被读取
}

// 将切片转换成流进行合并
function pipeStream(path, writeStream) {
  return new Promise((resolve) => {
    // 创建可读流,读取所有切片
    const readStream = fse.createReadStream(path);
    readStream.on("end", () => {
      fse.unlinkSync(path); // 读取完毕后,删除已经读取过的切片路径
      resolve();
    });
    readStream.pipe(writeStream); //将可读流流入可写流
  });
}

// // 解析POST请求传递的参数
function resolvePost(req) {
  // 解析参数
  return new Promise((resolve) => {
    let chunk = "";
    req.on("data", (data) => {
      //req接收到了前端的数据
      chunk += data; //将接收到的所有参数进行拼接
    });
    req.on("end", () => {
      resolve(JSON.parse(chunk)); //将字符串转为JSON对象
    });
  });
}

10、SSR与SPA

编写一个render函数,该函数允许传入dom、html、rehydrate三种类型。

function render(node, renderAs = "dom", path = []) {
  const { name, props, style, children } = node;
  if (renderAs === "dom") {
    const element = document.createElement(name);
    if (props && props.onClick) {
      element.addEventListener("click", props.onClick);
    }

    if (style) {
      Object.keys(style).forEach((key) => {
        element.style[key] = style[key];
      });
    }

    if (typeof children === "string") {
      element.innerHTML = children;
    } else if (Array.isArray(children)) {
      children.forEach((child, i) => {
        element.appendChild(render(child, renderAs, path.concat(i)));
      });
    } else {
      throw "invalid children";
    }
    return element;
  } else if (renderAs === "html") {
    let styles = [];
    if (style) {
      styles = Object.keys(style).map((key) => {
        const k = key.replace(/([A-Z])/, (m) => "-" + m.toLowerCase());
        const val = style[key];
        return `${k}=${val}`;
      });
    }
    const styleString = styles.join(";");
    let childrenStr = "";
    if (typeof children === "string") {
      childrenStr = children;
    } else {
      childrenStr = children
        .map((child, i) => render(child, renderAs, path.concat(i)))
        .join("");
    }
    return `<${name} id='node-${path.join(
      "-"
    )}' style='${styleString}'>${childrenStr}</${name}>`;
  } else if (renderAs === "rehydrate") {
    if (props && props.onClick) {
      document
        .getElementById("node-" + path.join("-"))
        .addEventListener("click", props.onClick);
    }

    if (Array.isArray(children)) {
      children.forEach((child, i) => {
        render(child, renderAs, path.concat(i));
      });
    }
  }
}

当传入DOM时,会将传入的vnode,转为真实DOM

当传入html时,会将传入的vnode,转为可被浏览器执行的html字符串

当传入rehydrate时,会将传入的vnode(此vnode应与生成的html字符串同)进行事件的判断,如果有则挂载事件(这部分代码由浏览器执行)

10.1 SPA单页面应用

传统的SPA页面,前端根目录只有一个div,由后端返回js文件,生成DOM并渲染到页面上

过程如下

const app = require("express")();
const path = require("path");

app.get("/page.js", (req, res) => {
  res.sendFile(path.resolve(__dirname, "page.js"));
});

// spa单页面渲染原理
app.get("/", (req, res) => {
  res.send(`<html>
    <body>
    <div id="root">
    </div>
    <script src='/page.js'></script>
  </body>`);
});

app.listen(3000);
//page.js
const element = render(
  {
    name: "div",
    props: {
      onClick: () => {
        window.alert("123");
      },
    },
    children: [
      {
        name: "ul",
        children: [
          {
            name: "li",
            children: "Apple",
          },
          {
            name: "li",
            children: "Alibaba",
          },
        ],
      },
    ],
  },
  "dom"
);

document.getElementById("root").appendChild(element);

使用了脚手架的vue项目都会带有一个根index.html,后端会返回这个html文件以及一堆js,css文件在浏览器执行生成最终页面

10.2 ssr页面渲染

后端执行

服务端拿到用户的vNode并将其生成可被浏览器执行的html字符串,发送给前端浏览器直接生成页面。(相比于spa,ssr不用到前端执行js文件生成DOM,这部分变为在后端转html字符串,并在前台直接展示)

问题:后端生成的html字符串不能绑定事件,仅有浏览器能够绑定事件

const app = require("express")();
const render = require("./renderFn");
const path = require("path");
const html = render(
  {
    name: "div",
    props: {
      onClick: () => {
        window.alert("123");
      },
    },
    children: [
      {
        name: "ul",
        children: [
          {
            name: "li",
            children: "Apple",
          },
          {
            name: "li",
            children: "Alibaba",
          },
        ],
      },
    ],
  },
  "html"
);

app.get("/page-ssr.js", (req, res) => {
  res.sendFile(path.resolve(__dirname, "page-ssr.js"));
});

// ssr渲染原理
app.get("/ssr", (req, res) => {
  res.send(`<html>
    <body>
      <div id="root">
      ${html}
      </div>
    <script src='/page-ssr.js'></script>
  </body>`);
});

app.listen(3000);

前端执行

因为浏览器绑定不了事件,所以会将生成了html字符串的vNode交由前端执行一次,执行过程中为所对应的节点绑定上事件

//page-ssr.js
render(
  {
    name: "div",
    props: {
      onClick: () => {
        window.alert("123");
      },
    },
    children: [
      {
        name: "ul",
        children: [
          {
            name: "li",
            children: "Apple",
          },
          {
            name: "li",
            children: "Alibaba",
          },
        ],
      },
    ],
  },
  "rehydrate"
);
评论
  • 支持 Markdown 格式
  • 评论需要登录
  • 邮箱会回复提醒(也许会在垃圾箱内)
0 /400 字