サーバー送信イベント:ケーススタディ

良い一日、友達!



このチュートリアルでは、サーバー送信イベントについて説明します。これは、サーバーへの接続を維持し、サーバーからイベントを受信できるようにする組み込みの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%です。







この記事を楽しんでいただけたでしょうか。清聴ありがとうございました。



All Articles