在本教程中,我们将研究服务器发送事件:内置的EventSource类,该类使您可以维护与服务器的连接并从中接收事件。
您可以在此处阅读有关SSE的信息以及它的用途。
我们到底要做什么?
我们将编写一个简单的服务器,根据客户的请求,该服务器将向他发送10个随机用户的数据,然后客户将使用此数据生成用户卡。
服务器将在Node.js中实现,JavaScript在客户端中实现。Bootstrap将用于样式设置,Random User Generator将用作API 。
项目代码在这里...
如果您有兴趣,请关注我。
训练
创建目录
sse-tut
:
mkdir sse-tut
我们进入它并初始化项目:
cd sse-tut
yarn init -y
//
npm init -y
安装
axios
:
yarn add axios
//
npm i axios
axios将用于获取用户数据。
编辑
package.json
:
"main": "server.js",
"scripts": {
"start": "node server.js"
},
项目结构:
sse-tut
--node_modules
--client.js
--index.html
--package.json
--server.js
--yarn.lock
内容
index.html
:
<head>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<style>
.card {
margin: 0 auto;
max-width: 512px;
}
img {
margin: 1rem;
max-width: 320px;
}
p {
margin: 1rem;
}
</style>
</head>
<body>
<main class="container text-center">
<h1>Server-Sent Events Tutorial</h1>
<button class="btn btn-primary" data-type="start-btn">Start</button>
<button class="btn btn-danger" data-type="stop-btn" disabled>Stop</button>
<p data-type="event-log"></p>
</main>
<script src="client.js"></script>
</body>
服务器
让我们开始实现服务器。
我们打开
server.js
。
我们连接http和axios,定义端口:
const http = require('http')
const axios = require('axios')
const PORT = process.env.PORT || 3000
我们创建一个用于接收用户数据的函数:
const getUserData = async () => {
const response = await axios.get('https://randomuser.me/api')
//
console.log(response)
return response.data.results[0]
}
为发送的用户数创建一个计数器:
let i = 1
我们编写了向客户端发送数据的功能:
const sendUserData = (req, res) => {
// - 200
//
// -
//
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
})
// 2
const timer = setInterval(async () => {
// 10
if (i > 10) {
//
clearInterval(timer)
// , 10
console.log('10 users has been sent.')
// -1
// ,
res.write('id: -1\ndata:\n\n')
//
res.end()
return
}
//
const data = await getUserData()
//
// event -
// id - ;
// retry -
// data -
res.write(`event: randomUser\nid: ${i}\nretry: 5000\ndata: ${JSON.stringify(data)}\n\n`)
// ,
console.log('User data has been sent.')
//
i++
}, 2000)
//
req.on('close', () => {
clearInterval(timer)
res.end()
console.log('Client closed the connection.')
})
}
我们创建并启动服务器:
http.createServer((req, res) => {
// CORS
res.setHeader('Access-Control-Allow-Origin', '*')
// - getUser
if (req.url === '/getUsers') {
//
sendUserData(req, res)
} else {
// , , ,
//
res.writeHead(404)
res.end()
}
}).listen(PORT, () => console.log('Server ready.'))
完整的服务器代码:
const http = require('http')
const axios = require('axios')
const PORT = process.env.PORT || 3000
const getUserData = async () => {
const response = await axios.get('https://randomuser.me/api')
return response.data.results[0]
}
let i = 1
const sendUserData = (req, res) => {
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
})
const timer = setInterval(async () => {
if (i > 10) {
clearInterval(timer)
console.log('10 users has been sent.')
res.write('id: -1\ndata:\n\n')
res.end()
return
}
const data = await getUserData()
res.write(`event: randomUser\nid: ${i}\nretry: 5000\ndata: ${JSON.stringify(data)}\n\n`)
console.log('User data has been sent.')
i++
}, 2000)
req.on('close', () => {
clearInterval(timer)
res.end()
console.log('Client closed the connection.')
})
}
http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
if (req.url === '/getUsers') {
sendUserData(req, res)
} else {
res.writeHead(404)
res.end()
}
}).listen(PORT, () => console.log('Server ready.'))
我们执行命令
yarn start
或npm start
。终端显示消息“服务器就绪”。开幕http://localhost:3000
:我们已经
完成了服务器,请转到应用程序的客户端。
客户
打开文件
client.js
。
让我们创建一个用于生成自定义卡片模板的函数:
const getTemplate = user => `
<div class="card">
<div class="row">
<div class="col-md-4">
<img src="${user.img}" class="card-img" alt="user-photo">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">${user.id !== null ? `Id: ${user.id}` : `User hasn't id`}</h5>
<p class="card-text">Name: ${user.name}</p>
<p class="card-text">Username: ${user.username}</p>
<p class="card-text">Email: ${user.email}</p>
<p class="card-text">Age: ${user.age}</p>
</div>
</div>
</div>
</div>
`
使用以下数据生成模板:用户ID(如果有),名称,登录名,电子邮件地址和用户年龄。
我们开始实现主要功能:
class App {
constructor(selector) {
// -
this.$ = document.querySelector(selector)
//
this.#init()
}
#init() {
this.startBtn = this.$.querySelector('[data-type="start-btn"]')
this.stopBtn = this.$.querySelector('[data-type="stop-btn"]')
//
this.eventLog = this.$.querySelector('[data-type="event-log"]')
//
this.clickHandler = this.clickHandler.bind(this)
//
this.$.addEventListener('click', this.clickHandler)
}
clickHandler(e) {
//
if (e.target.tagName === 'BUTTON') {
//
// ,
//
const {
type
} = e.target.dataset
if (type === 'start-btn') {
this.startEvents()
} else if (type === 'stop-btn') {
this.stopEvents()
}
//
this.changeDisabled()
}
}
changeDisabled() {
if (this.stopBtn.disabled) {
this.stopBtn.disabled = false
this.startBtn.disabled = true
} else {
this.stopBtn.disabled = true
this.startBtn.disabled = false
}
}
//...
首先,我们实现关闭连接:
stopEvents() {
this.eventSource.close()
// ,
this.eventLog.textContent = 'Event stream closed by client.'
}
让我们继续打开事件流:
startEvents() {
//
this.eventSource = new EventSource('http://localhost:3000/getUsers')
// ,
this.eventLog.textContent = 'Accepting data from the server.'
// -1
this.eventSource.addEventListener('message', e => {
if (e.lastEventId === '-1') {
//
this.eventSource.close()
//
this.eventLog.textContent = 'End of stream from the server.'
this.changeDisabled()
}
//
}, {once: true})
}
我们处理“ randomUser”自定义事件:
this.eventSource.addEventListener('randomUser', e => {
//
const userData = JSON.parse(e.data)
//
console.log(userData)
//
const {
id,
name,
login,
email,
dob,
picture
} = userData
// ,
const i = id.value
const fullName = `${name.first} ${name.last}`
const username = login.username
const age = dob.age
const img = picture.large
const user = {
id: i,
name: fullName,
username,
email,
age,
img
}
//
const template = getTemplate(user)
//
this.$.insertAdjacentHTML('beforeend', template)
})
不要忘记实现错误处理:
this.eventSource.addEventListener('error', e => {
this.eventSource.close()
this.eventLog.textContent = `Got an error: ${e}`
this.changeDisabled()
}, {once: true})
最后,我们初始化应用程序:
const app = new App('main')
完整的客户代码:
const getTemplate = user => `
<div class="card">
<div class="row">
<div class="col-md-4">
<img src="${user.img}" class="card-img" alt="user-photo">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">${user.id !== null ? `Id: ${user.id}` : `User hasn't id`}</h5>
<p class="card-text">Name: ${user.name}</p>
<p class="card-text">Username: ${user.username}</p>
<p class="card-text">Email: ${user.email}</p>
<p class="card-text">Age: ${user.age}</p>
</div>
</div>
</div>
</div>
`
class App {
constructor(selector) {
this.$ = document.querySelector(selector)
this.#init()
}
#init() {
this.startBtn = this.$.querySelector('[data-type="start-btn"]')
this.stopBtn = this.$.querySelector('[data-type="stop-btn"]')
this.eventLog = this.$.querySelector('[data-type="event-log"]')
this.clickHandler = this.clickHandler.bind(this)
this.$.addEventListener('click', this.clickHandler)
}
clickHandler(e) {
if (e.target.tagName === 'BUTTON') {
const {
type
} = e.target.dataset
if (type === 'start-btn') {
this.startEvents()
} else if (type === 'stop-btn') {
this.stopEvents()
}
this.changeDisabled()
}
}
changeDisabled() {
if (this.stopBtn.disabled) {
this.stopBtn.disabled = false
this.startBtn.disabled = true
} else {
this.stopBtn.disabled = true
this.startBtn.disabled = false
}
}
startEvents() {
this.eventSource = new EventSource('http://localhost:3000/getUsers')
this.eventLog.textContent = 'Accepting data from the server.'
this.eventSource.addEventListener('message', e => {
if (e.lastEventId === '-1') {
this.eventSource.close()
this.eventLog.textContent = 'End of stream from the server.'
this.changeDisabled()
}
}, {once: true})
this.eventSource.addEventListener('randomUser', e => {
const userData = JSON.parse(e.data)
console.log(userData)
const {
id,
name,
login,
email,
dob,
picture
} = userData
const i = id.value
const fullName = `${name.first} ${name.last}`
const username = login.username
const age = dob.age
const img = picture.large
const user = {
id: i,
name: fullName,
username,
email,
age,
img
}
const template = getTemplate(user)
this.$.insertAdjacentHTML('beforeend', template)
})
this.eventSource.addEventListener('error', e => {
this.eventSource.close()
this.eventLog.textContent = `Got an error: ${e}`
this.changeDisabled()
}, {once: true})
}
stopEvents() {
this.eventSource.close()
this.eventLog.textContent = 'Event stream closed by client.'
}
}
const app = new App('main')
重新启动服务器,以防万一。我们打开
http://localhost:3000
。单击“开始”按钮:
我们开始从服务器接收数据并渲染用户卡。
如果单击“停止”按钮,将暂停发送数据:
再次按“开始”,则继续发送数据。
当达到限制(10个用户)时,服务器将发送一个值为-1的标识符并关闭连接。客户端反过来也关闭了事件流:
如您所见,SSE与websocket非常相似。缺点是消息是单向的:消息只能由服务器发送。优点是自动重新连接和易于实现。
今天,这项技术的支持率为95%:
希望您喜欢这篇文章。感谢您的关注。