69

Building simple chat with Odi (Node.js)

 5 years ago
source link: https://www.tuicool.com/articles/hit/NjeIzuI
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Introduction

The chat application is the very common example that used to show simple real-time communication between client and server. This tutorial describes how it can be easily done with Odi, TypeScript server-side framework for Node.js.

Project Setup

We are going to develop the application that not only establishes the real-time communication channel but also renders frontend to the client, including required assets.

Basic Settings

Let’s set up the Odi project. First of all, we should initialize package.json and tsconfig.json files. We can do it with two simple commands.

npm init -y
tsc --init

And install Odi .

npm install odi

Also, we need to modify tsconfig.json file, as there are few options that must be edited. Odi actively uses decorators and metadata, so we need to enable these features.

"experimentalDecorators": true,
"emitDecoratorMetadata": true

Another thing is target option. By default, it set to es5 but there are several things that are not supported in is this specification. As we are progressive, let’s set it to the latest version

"target": "ES2018"

Project Structure

We are going to have different folders for views, assets and server source code.

Mv2A32M.png!web2amANfN.png!web
  • src — server source code.
  • views — JSX templates that will be rendered to clients.
  • assets — client-side JS and CSS files.

JSX templates are tsx files that must be compiled. Add views folder to rootDirs in tsconfig.json file and setup outDir .

"outDir": "./build",
"rootDirs": ["./src", "./views"]

Gettings started

Odi is based on the Dependency Injection pattern, so every application component will be automatically imported, instantiated and injected.

Only folder with source files must be specified, then Odi can scan it for importing application components (Controllers, Services, Repositories and etc).

Main File

Create index.ts file in src folder. It will be the server entry point file.

import { Core } from "odi";
import { join } from "path";
new Core({
    sources: __dirname,
    server: {
        port: 8080,
        socket: true,
        static: {
            root: join(__dirname, '../../assets'),
            prefix: '/assets'
        }
    }
}).listen(() => console.log("Server successfully started!"));

We just need to instantiate Core class. Core constructor accepts a single argument, settings object. There are a lot of possible options, but for now, we need only several of them.

First of all, we need to specify sources property. It’s required setting for Odi application. As index.ts file in src folder, which we choose for server-side code, we can use __dirname to set current directory.

port property is also required. It binds the server on the specified port.

Now about the following part:

socket: true,
static: {
     root: join(__dirname, '../../assets'),
     prefix: '/assets'
}

We must enable sockets and set options for serving static files All files from the assets folder are available by URL with /assets prefix.

Installing Dependencies

Odi framework automatically includes only several packages that are required. All other dependencies for different features are optional, so they need to be installed only if you use a certain feature.

For example, if you are going to build a simple REST server, you don’t need GraphQL, WebSockets, SSR and other packages.

We want to have WebSockets and Templating (JSX) in our chat application. So, let’s install missing packages:

npm install socket.io react react-dom

That’s all, Odi will automatically import it. As you can see, socket.io is used under the hood for real-time functionality. Also React packages is required for templates processing.

Now we can start writing our code :)

Application

We are going to create a web server, that renders HTML to the client, using templates, serves files for the client ( JS , CSS ) and set up a real-time communication channel using WebSockets for chat. Let’s add history to our chat. So, the last 10 messages will be saved in our system.

Message and History

Message will be pretty simple, only username and text fields. We can do it with a simple interface, as we are not going to use a database.

export interface Message {
    username: string;
    text: string;
}

And history service

@Service()
export default class HistoryService {
    private store: Message[] = [];
    
    getMessages() {
        return this.store;
    }
addMessage(message: Message) {
        if(this.store.length > 10)
            this.store.shift();
this.store.push(message);
    }
}

Our store is a simple array of messages. And few methods for store management. If we get more than 10 messages, we simply remove the first message from the array.

As you can see, Service decorator was used for HistoryService class to set is as a service component. Service is singleton in Dependency Injection Container. Now it can be injected into others application components.

Put all this code in history.ts file in src/services folder.

Web Socket

Create chat.socket.ts file in the src/sockets directory with the following code.

import { Socket, OnEvent, ISocket, Autowired } from "odi";
import HistoryService, { Message } from "../services/history";
@Socket('chat')
export default class ChatSocket extends ISocket {
@Autowired()
    history: HistoryService;
@OnEvent('massage:send') 
    onmessage(message: Message) {
       this.history.addMessage(message); 
       this.emit('message:new', message);
    }
}

We defined /chat namespace with handler for message:send event. If message:send event is fired, all clients that connected to this namespace will be notified with message:new event and message data.

As you can notice Socket decorator defines namespaces. Leading slash is not required. To set up method as the handler for certain event, use OnEvent decorator, that accepts event name as the argument.

Also, we injected HistoryService using Autowired decorator. history field of ChatSocket class will be initialized by Odi, so you don’t need to do anything additional.

The only thing, you can see such error from TypeScript

[ts] Property 'history' has no initializer and is not definitely assigned in the constructor.

Odi automatically initializes injected fields, so just disable this check in tsconfig.json

"strictPropertyInitialization": false

Templating (JSX)

There a lot of templating processors — EJS, Jade, Pug. But there are a lot of limitations and inconveniences with those technologies. In most cases, to have IntelliSense and code highlight for templates, you need to install an extension for IDE/Editor.

In Odi, JSX powered by React is used for templating. You can simply create components with JSX. But remember, it’s only for templates, any logic, listeners or client-side code will be ignored during rendering.

(Currently, we are working on full SSR. Hope it will be released soon)

We need to tell TypeScript compiler, that we are going to use React JSX.

In tsconfig.json

...
"jsx": "react"

Layout

Let’s create our layout component layout.view.tsx that will be a wrapper for all pages. As was mentioned above, all templates will be in views folder.

import React, { SFC } from 'react';
export const Html: SFC = ({ children }) => (
    <html lang="en">
        <head>
            <meta charSet="UTF-8" />
            <meta name="viewport" />
            <meta httpEquiv="X-UA-Compatible" content="ie=edge"/>
            <link href="/assets/index.css" type="text/css" ... />
            <title> Simple chat </title>
        </head>
        <body>
            {children}
        </body>
<script src="path/to/socket.io" />
        <script src="/assets/index.js" />
    </html>
)

For socket.io-client library we can use CDN. So simply replace path/to/socket.io in the script tag with the following link https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js

Client js file was specified in the second script tag. We will create it a little bit later in assets folder.

Chat Components

Actually, we need 3 components for our chat:

  1. Message representation
  2. Controls (Send button, message input, username input)
  3. Chat container

I think we can put all these components in 1 file, chat.view.tsx

import React from 'react';
import { Message } from './services/history.service';
export const ChatMessage = ({ username, text }: Message) => (
    <div>
        <b>{username}: </b>
        <span>{text}</span>
    </div>
)

We can use Message interface as props type for ChatMessage component.

Let’s add chat controls. id attribute was used for convenience, as we are going to use js on the client side without any libs or frameworks.

export const ChatControlls = () => (
    <div className="message-box">
        <input placeholder="User" id="user-input" />
        <input placeholder="Message" id="message-input" />
        <button> Send </button>
    </div>
)

And the last thing, chat wrapper.

interface ChatProps {
    messages: Message[];
}
export const Chat = ({ messages }: ChatProps) => (
    <div className="chat">
        <div className="container">
          {messages.map(msg,i) => <ChatMessage key={i} {...msg}/> )}
         </div>
        <ChatControlls />
    </div>
)

This component accepts an array of messages (our history) in props to render it on page load.

Now we can put everything together and define our page component page.view.tsx

import React from 'react';
import { Chat } from './chat.view';
import { Html } from './layout.view';
import { Message } from './services/history.service';
interface ChatPageProps {
    history: Message[];
}
export const ChatPage = ({ history }: ChatPageProps) => (
    <Html>
        <Chat messages={history} />        
    </Html>
)

That’s all about templating for our chat application. I have several lines of CSS that I will include it in the source code, that you can find at the end of the article.

We can move to controllers.

Controllers

Controllers serve as a simple yet powerful routing mechanism. Controller methods are mapped to web server paths. The value returned by the method is sent as the response.

In order to create a Controller, you must use the @Controller decorator and inherit the IController class. The decorator sets the component type, so the DI (dependency injection) container can detect what the class will be used for.

For our chat, we need only one controller to render a template to the client. As we are going to use JSX inside the controller file, it must have tsx file extension. So, let’s create render.controller.tsx in src/controllers folder.

import React from 'react';
import { Controller, IController, Get, Autowired } from "odi";
import { ChatPage } from '../../views/page.view';
import HistoryService from '../services/history.service';
@Controller()
export default class RenderController extends IController {
@Autowired()
    history: HistoryService;
@Get index() {
        return <ChatPage history={this.history.getMessages()}/>;
    }
}

As you can see, we injected our HistoryService into history property. Also, the handler for / path with Get method was defined. We can simply return our JSX component as a result, Odi automatically detects that it’s a template and renders it as simple HTML for the client (web browser).

Starting Application

Now, we can start our application and see what we got. Let’s specify start script in package.json file:

"scripts": {
    "start": "tsc && node build/src/index.js"
}

Running npm start command compile our source code and run server entry file.

Y7z6jqz.png!webeaiaY3a.png!web

Let’s open the browser and check localhost:8080

fU3qeav.png!web6zINF3r.png!web

As you can see, we have just empty chat without any functionality, as we did not specify the client index.js into assets folder.

Client

First of all, let’s get references for chat container and controls.

const button = document.querySelector('button');
const messageInput = document.querySelector('#message-input');
const usernameInput = document.querySelector('#user-input');
const container = document.querySelector('.container');

When a new message comes, we need to append it as a child in container element. We need the function for creating elements that represent messages.

function createMessage({ username, text }) {
    const element = document.createElement('div');
    
    element.innerHTML = `
        <b>${username}: </b>
        <span>${text}</span>
    `;
return element;
}

Then, let’s connect to our chat namespace and add the event handler for message:new event. When this event is fired, the message element will be appended to the container.

const socket = io('/chat');
socket.on('message:new', message => {
    const messageElement = createMessage(message);
    container.appendChild(messageElement);
});

And the last step, onclinck handler for our button.

button.onclick = () => {
    socket.emit('massage:send', { 
        text: messageInput.value, 
        username: usernameInput.value
    });
messageInput.value = "";
}

We are collecting data from inputs and sending it as message:send event. Also, the message input text will be cleared after every send.

Now we can refresh the page, and see what we have got.

qAfuqan.jpgAvYrErv.gif

After refreshing the page, we will have history our messaging.

Sandbox

You can check the source code and interact with the application right here:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK