このチュートリアルでは、サーバー送信イベントについて説明します。これは、サーバーへの接続を維持し、サーバーからイベントを受信できるようにする組み込みのEventSourceクラスです。
SSEとは何か、SSEの用途についてはこちらをご覧ください。
私たちは正確に何をするつもりですか?
クライアントの要求に応じて、ランダムな10人のユーザーのデータを送信する単純なサーバーを作成し、クライアントはこのデータを使用してユーザーカードを生成します。
サーバーは、JavaScriptのクライアントであるNode.jsに実装されます。スタイリングにはブートストラップが使用され、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のIDを送信し、接続を閉じます。次に、クライアントはイベントストリームも閉じます。
ご覧のとおり、SSEはWebソケットと非常によく似ています。欠点は、メッセージが単方向であるということです。メッセージはサーバーからのみ送信できます。利点は、自動再接続と実装の容易さです。
今日のこのテクノロジーのサポートは95%です。
この記事を楽しんでいただけたでしょうか。清聴ありがとうございました。