NodeWebkit、Socket.io、およびMEANを使用してリアルタイムのチャットルームを作成する
序章
開発担当者は、プログラムの構築を可能な限り簡単にするためにたゆまぬ努力をしています。 NodeとCordovaが導入されて以来、JavaScript、Web、およびモバイルアプリの開発者コミュニティは劇的に増加しました。 Webデザインのスキルを持っている開発者は、Node.jsの助けを借りて、少ない労力で、アプリケーションにJavaScriptを使用してサーバーを展開できます。
モバイル愛好家は、Cordovaの助けを借りて、JavaScriptだけを使用してリッチなハイブリッドアプリを構築できるようになりました。 今日、それは古いニュースですが、JavaScriptを使用してデスクトップスタンドアロンアプリケーションを構築する機能を共有できることに興奮しています。
Node WebKit 通常の記述:「node-webkit」または「NW.js」は、Node.jsとChromiumに基づくアプリランタイムであり、HTML、CSS、JavaScriptのみを使用してOSネイティブアプリを開発できます。
簡単に言えば、Node WebKitは、Web開発者としてのスキルを活用して、Mac、Windows、およびLinuxでgrunt / gulp(必要に応じて)ビルドコマンドだけで快適に実行されるネイティブアプリケーションを構築するのに役立ちます。
この記事では、Node WebKitの使用に重点を置いていますが、物事をより面白くするために、他のすばらしいソリューションを追加します。
- Socket.ioNode.js用のリアルタイムライブラリ
- Angular Material :AngularによるGoogleのマテリアルデザインの実装
- MEAN:MEANは、Mongo、Express、Angular、Nodeの機能を組み合わせて強力なアプリを構築するという概念にすぎません。
さらに、アプリケーションには3つのセクションがあります。
- サーバー
- デスクトップ(クライアント)
- ウェブ(クライアント)
ここではWebセクションについては説明しませんが、テストプラットフォームとして機能しますが、コードが提供されますのでご安心ください。
前提条件
レベル:中級( MEAN の知識が必要です)
インストール
アプリケーションのnode-webkit
およびその他の依存関係を取得する必要があります。 幸い、ワークフローを簡単にするフレームワークがあり、そのうちの1つを使用してアプリケーションの足場を作り、実装にさらに集中します。
YoとSlushは人気のあるジェネレーターであり、これらのいずれも機能します。 Slush を使用しますが、必要に応じてYoを使用してください。 Slushをインストールするには、ノードとnpmがインストールされて実行されていることを確認してください
npm install -g slush gulp bower slush-wean
このコマンドは、次のものをシステムにグローバルにインストールします。
YOと同じように、次を使用してディレクトリとスキャフォールドをアプリにします。
mkdir scotch-chat cd scotch-chat slush wean
以下のコマンドを実行すると、私たちが待ち望んでいたものが一目でわかります。
gulp run
画像はアプリの読み込みを示しています。 ジェネレーターの作成者は、単純な読み込みアニメーションを備えた素敵なテンプレートを提供するのに十分寛大でした。 見栄えを良くするために、読み込み中のテキストをScotchのロゴに置き換えました。
Slushの自動化に慣れていない場合は、GitHubのNodeWebKitに直接アクセスできます。
アプリをセットアップしたので、空ですが、休憩を取り、サーバーを準備します。
サーバー
サーバーは基本的に、モデル、ルート、およびソケットイベントで構成されています。 可能な限りシンプルに保ち、記事の最後にある指示に従ってアプリを自由に拡張できます。
ディレクトリ構造
PCのお気に入りのディレクトリにフォルダを設定しますが、フォルダの内容が次のようになっていることを確認してください。
|- public |- index.html |- server.js |- package.json
依存関係
ルートディレクトリにあるpackage.json
ファイルで、アプリケーションを記述し、アプリケーションの依存関係を含めるJSONファイルを作成します。
{ "name": "scotch-chat", "main": "server.js", "dependencies": { "mongoose": "latest", "morgan": "latest", "socket.io": "latest" } }
それで十分です。 これは最小限の設定であり、シンプルで短くしています。 ディレクトリルートでnpm install
を実行して、指定した依存関係をインストールします。
npm install
サーバーセットアップの開始
手を汚す時が来ました! まず、server.js
にグローバル変数を設定します。これにより、既にインストールされているアプリケーションの依存関係が保持されます。
server.js
// Import all our dependencies var express = require('express'); var mongoose = require('mongoose'); var app = express(); var server = require('http').Server(app); var io = require('socket.io')(server);
わかりました、私は私の言葉を守りませんでした。 変数は依存関係を保持しているだけでなく、使用できるように構成しているものもあります。
静的ファイルを提供するために、expressは静的ファイルフォルダーの構成に役立つメソッドを公開します。 簡単です:
server.js
... // tell express where to serve static files from app.use(express.static(__dirname + '/public'));
次は、データベースへの接続を作成します。 私はローカルのMongoDBを使用していますが、これはMongoデータベースによってホストされていることがわかるため、明らかにオプションです。 Mongoose は、MongoDBでの作業をはるかに簡単にする素晴らしいAPIを公開するノードモジュールです。
server.js
... mongoose.connect("mongodb://127.0.0.1:27017/scotch-chat");
Mongooseを使用して、データベーススキーマとモデルを作成できるようになりました。 また、別のドメインからアクセスするため、アプリケーションでCORSを許可する必要があります。
server.js
... // create a schema for chat var ChatSchema = mongoose.Schema({ created: Date, content: String, username: String, room: String }); // create a model from the chat schema var Chat = mongoose.model('Chat', ChatSchema); // allow CORS app.all('*', function(req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-type,Accept,X-Access-Token,X-Key'); if (req.method == 'OPTIONS') { res.status(200).end(); } else { next(); } });
サーバーには3つのルートがあります。 インデックスファイルを提供するルート、チャットデータを設定するルート、および部屋名でフィルタリングされたチャットメッセージを提供する最後のルート:
server.js
/*||||||||||||||||||||||ROUTES|||||||||||||||||||||||||*/ // route for our index file app.get('/', function(req, res) { //send the index.html in our public directory res.sendfile('index.html'); }); //This route is simply run only on first launch just to generate some chat history app.post('/setup', function(req, res) { //Array of chat data. Each object properties must match the schema object properties var chatData = [{ created: new Date(), content: 'Hi', username: 'Chris', room: 'php' }, { created: new Date(), content: 'Hello', username: 'Obinna', room: 'laravel' }, { created: new Date(), content: 'Ait', username: 'Bill', room: 'angular' }, { created: new Date(), content: 'Amazing room', username: 'Patience', room: 'socet.io' }]; //Loop through each of the chat data and insert into the database for (var c = 0; c < chatData.length; c++) { //Create an instance of the chat model var newChat = new Chat(chatData[c]); //Call save to insert the chat newChat.save(function(err, savedChat) { console.log(savedChat); }); } //Send a resoponse so the serve would not get stuck res.send('created'); }); //This route produces a list of chat as filterd by 'room' query app.get('/msg', function(req, res) { //Find Chat.find({ 'room': req.query.room.toLowerCase() }).exec(function(err, msgs) { //Send res.json(msgs); }); }); /*||||||||||||||||||END ROUTES|||||||||||||||||||||*/
私が信じる最初のルートは十分に簡単です。 index.html
ファイルをユーザーに送信するだけです。
2番目の/setup
は、アプリケーションの最初の起動時に1回だけヒットすることを目的としています。 テストデータが必要ない場合はオプションです。 基本的に、チャットメッセージの配列(スキーマに一致する)を作成し、それらをループして、データベースに挿入します。
3番目のルート/msg
は、部屋名でフィルタリングされ、JSONオブジェクトの配列として返されるチャット履歴をフェッチする役割を果たします。
サーバーの最も重要な部分は、リアルタイムロジックです。 単純なアプリケーションの作成に取り組んでいることを念頭に置いて、ロジックは包括的に最小限に抑えられます。 続いて、次のことを行う必要があります。
- アプリケーションがいつ起動されるかを知る
- 接続時に利用可能なすべての部屋を送信する
- ユーザーが接続するのを聞いて、デフォルトの部屋に割り当てます
- 彼らが部屋を切り替えるときに耳を傾ける
- そして最後に、新しいメッセージを聞いて、それが作成された部屋の人にのみメッセージを送信します
したがって:
server.js
/*||||||||||||||||SOCKET|||||||||||||||||||||||*/ //Listen for connection io.on('connection', function(socket) { //Globals var defaultRoom = 'general'; var rooms = ["General", "angular", "socket.io", "express", "node", "mongo", "PHP", "laravel"]; //Emit the rooms array socket.emit('setup', { rooms: rooms }); //Listens for new user socket.on('new user', function(data) { data.room = defaultRoom; //New user joins the default room socket.join(defaultRoom); //Tell all those in the room that a new user joined io.in(defaultRoom).emit('user joined', data); }); //Listens for switch room socket.on('switch room', function(data) { //Handles joining and leaving rooms //console.log(data); socket.leave(data.oldRoom); socket.join(data.newRoom); io.in(data.oldRoom).emit('user left', data); io.in(data.newRoom).emit('user joined', data); }); //Listens for a new chat message socket.on('new message', function(data) { //Create message var newMsg = new Chat({ username: data.username, content: data.message, room: data.room.toLowerCase(), created: new Date() }); //Save it to database newMsg.save(function(err, msg){ //Send message to those connected in the room io.in(msg.room).emit('message created', msg); }); }); }); /*||||||||||||||||||||END SOCKETS||||||||||||||||||*/
次に、従来のサーバーが起動します。
server.js
server.listen(2015); console.log('It\'s going down in 2015');
index.html
に適切なHTMLを入力して、node server.js
を実行します。 localhost:2015
は、HTMLのコンテンツを提供します。
ノードWebKitクライアント
現在実行中のサーバーを作成するために残したものを掘り下げる時間です。 このセクションは、HTML、CSS、JS、およびAngularの日常的な知識が必要なだけなので非常に簡単です。
[1] –>
ディレクトリ構造
作成する必要はありません! それが発電機のインスピレーションだったと思います。 調べたい最初のファイルはpackage.json
です。
Node WebKitを実行するには、基本的に2つの主要なファイルが必要です。
- エントリポイント(
index.html
) package.json
は、エントリポイントがどこにあるかを示します
package.json
には、メインがindex.html
の場所であり、"window":
の下に一連の構成があり、そこからすべてを定義することを除いて、私たちが慣れている基本的なコンテンツがありますアイコン、サイズ、ツールバー、フレームなどを含むアプリのウィンドウのプロパティ。
依存関係
サーバーとは異なり、bowerを使用して依存関係をロードします。これは、クライアントアプリケーションであるためです。 bower.json
の依存関係を次のように更新します。
"dependencies": { "angular": "^1.3.13", "angular-material" : "^0.10.0", "angular-socket-io" : "^0.7.0", "angular-material-icons":"^0.5.0", "animate.css":"^3.0.0" }
ショートカットの場合は、次のコマンドを実行するだけです。
bower install --save angular angular-material angular-socket-io angular-material-icons animate.css
フロントエンドの依存関係ができたので、views/index.ejs
を次のように更新できます。
index.ejs
<html><head> <title>scotch-chat</title> <link rel="stylesheet" href="css/app.css"> <link rel="stylesheet" href="css/animate.css"> <link rel="stylesheet" href="libs/angular-material/angular-material.css"> <script src="libs/angular/angular.js"></script> <script src="http://localhost:2015/socket.io/socket.io.js"></script> <script type="text/javascript" src="libs/angular-animate/angular-animate.js"></script> <script type="text/javascript" src="libs/angular-aria/angular-aria.js"></script> <script type="text/javascript" src="libs/angular-material/angular-material.js"></script> <script type="text/javascript" src="libs/angular-socket-io/socket.js"></script> <script type="text/javascript" src="libs/angular-material-icons/angular-material-icons.js"></script> <script src="js/app.js"></script> </head> <body ng-controller="MainCtrl" ng-init="usernameModal()"> <md-content> <section> <md-list> <md-subheader class="md-primary header">Room: {{room}} <span align="right">Userame: {{username}} </span> </md-subheader> <md-whiteframe ng-repeat="m in messages" class="md-whiteframe-z2 message" layout layout-align="center center"> <md-list-item class="md-3-line"> <img ng-src="img/user.png" class="md-avatar" alt="User" /> <div class="md-list-item-text"> <h3>{{ m.username }}</h3> <p>{{m.content}}</p> </div> </md-list-item> </md-whiteframe> </md-list> </section> <div class="footer"> <md-input-container> <label>Message</label> <textarea ng-model="message" columns="1" md-maxlength="100" ng-enter="send(message)"></textarea> </md-input-container> </div> </md-content> </body> </html>
すべての依存関係とカスタムファイル(app.cssとapp.js)を含めました。 注意事項:
- 角度のある素材を使用しており、そのディレクティブによってコードが「HTML6」のように見えます。
ng-repeat
を使用してメッセージスコープをループし、その値をブラウザにレンダリングしています- 後で見るディレクティブは、
ENTER
キーが押されたときにメッセージを送信するのに役立ちます init
では、ユーザーは優先ユーザー名を求められます- AngularでSocket.ioを簡単に操作できるように含まれているAngularライブラリがあります。
アプリケーション
このセクションの主要部分は、app.js
ファイルです。 これは、Node WebKit GUIと対話するためのサービス、ENTER
キー押下およびコントローラー(メインおよびダイアログ)を処理するためのディレクティブを作成します。
app.js
//Load angular var app = angular.module('scotch-chat', ['ngMaterial', 'ngAnimate', 'ngMdIcons', 'btford.socket-io']); //Set our server url var serverBaseUrl = 'http://localhost:2015'; //Services to interact with nodewebkit GUI and Window app.factory('GUI', function () { //Return nw.gui return require('nw.gui'); }); app.factory('Window', function (GUI) { return GUI.Window.get(); }); //Service to interact with the socket library app.factory('socket', function (socketFactory) { var myIoSocket = io.connect(serverBaseUrl); var socket = socketFactory({ ioSocket: myIoSocket }); return socket; });
次に、3つのAngularサービスを作成します。 最初のサービスはそのNodeWebKitGUIオブジェクトを取得するのに役立ち、2番目のサービスはそのWindowプロパティを返し、3番目のサービスはベースURLを使用してSocket.ioをブートストラップします。
app.js
//ng-enter directive app.directive('ngEnter', function () { return function (scope, element, attrs) { element.bind("keydown keypress", function (event) { if (event.which === 13) { scope.$apply(function () { scope.$eval(attrs.ngEnter); }); event.preventDefault(); } }); }; });
上記のスニペットは、Angularを使用して以来、私のお気に入りの1つです。 イベントをENTER
キーにバインドします。これにより、キーが押されたときにイベントをトリガーできます。
最後に、app.js
には全能のコントローラーがあります。 server.js
で行ったように、理解を容易にするために物事を分解する必要があります。 コントローラは次のことを期待されています。
- サーバーから放出された部屋を使用して、ウィンドウメニューのリストを作成します。
- 参加するユーザーは、ユーザー名を提供する必要があります。
- サーバーからの新しいメッセージをリッスンします。
ENTER
キーを入力して押すことにより、新しいメッセージが作成されたときにサーバーに通知します。
部屋のリストを作成する
目標を定義したら、次のようにコーディングします。
app.js
//Our Controller app.controller('MainCtrl', function ($scope, Window, GUI, $mdDialog, socket, $http){ //Menu setup //Modal setup //listen for new message //Notify server of the new message });
これが、すべての依存関係を持つコントローラーのスケルトンです。 ご覧のとおり、目標で定義されているコードのプレースホルダーとして機能する4つの内部コメントがあります。 それでは、メニューを選びましょう。
app.js
//Global Scope $scope.messages = []; $scope.room = ""; //Build the window menu for our app using the GUI and Window service var windowMenu = new GUI.Menu({ type: 'menubar' }); var roomsMenu = new GUI.Menu(); windowMenu.append(new GUI.MenuItem({ label: 'Rooms', submenu: roomsMenu })); windowMenu.append(new GUI.MenuItem({ label: 'Exit', click: function () { Window.close() } }));
メニューのインスタンスを作成し、それにいくつかのメニュー(RoomsとExit)を追加しただけです。 部屋メニューはドロップダウンとして機能することが期待されているため、サーバーに利用可能な部屋を要求し、それを部屋メニューに追加する必要があります。
app.js
//Listen for the setup event and create rooms socket.on('setup', function (data) { var rooms = data.rooms; for (var r = 0; r < rooms.length; r++) { //Loop and append room to the window room menu handleRoomSubMenu(r); } //Handle creation of room function handleRoomSubMenu(r) { var clickedRoom = rooms[r]; //Append each room to the menu roomsMenu.append(new GUI.MenuItem({ label: clickedRoom.toUpperCase(), click: function () { //What happens on clicking the rooms? Swtich room. $scope.room = clickedRoom.toUpperCase(); //Notify the server that the user changed his room socket.emit('switch room', { newRoom: clickedRoom, username: $scope.username }); //Fetch the new rooms messages $http.get(serverBaseUrl + '/msg?room=' + clickedRoom).success(function (msgs) { $scope.messages = msgs; }); } })); } //Attach menu GUI.Window.get().menu = windowMenu; });
上記のコードは、関数を使用して、サーバーから利用可能な部屋の配列をループし、部屋のメニューに追加します。 これで、目的#1は完了です。
ユーザー名を尋ねる
2番目の目的は、角度のあるマテリアルモーダルを使用してユーザーにユーザー名を尋ねることです。
app.js
$scope.usernameModal = function (ev) { //Launch Modal to get username $mdDialog.show({ controller: UsernameDialogController, templateUrl: 'partials/username.tmpl.html', parent: angular.element(document.body), targetEvent: ev, }) .then(function (answer) { //Set username with the value returned from the modal $scope.username = answer; //Tell the server there is a new user socket.emit('new user', { username: answer }); //Set room to general; $scope.room = 'GENERAL'; //Fetch chat messages in GENERAL $http.get(serverBaseUrl + '/msg?room=' + $scope.room).success(function (msgs) { $scope.messages = msgs; }); }, function () { Window.close(); }); };
HTMLで指定されているように、initで、usernameModal
が呼び出されます。 mdDialog
サービスを使用して、参加しているユーザーのユーザー名を取得します。これが成功すると、入力したユーザー名がバインディングスコープに割り当てられ、そのアクティビティについてサーバーに通知してから、ユーザーをデフォルト(GENERAL)にプッシュします。部屋。 成功しなかった場合は、アプリを閉じます。 目標#2が完了しました!
//Listen for new messages (Objective 3) socket.on('message created', function (data) { //Push to new message to our $scope.messages $scope.messages.push(data); //Empty the textarea $scope.message = ""; }); //Send a new message (Objective 4) $scope.send = function (msg) { //Notify the server that there is a new message with the message as packet socket.emit('new message', { room: $scope.room, message: msg, username: $scope.username }); };
メッセージを聞く
3番目で最後の目的は単純です。 #3はメッセージをリッスンし、存在する場合は既存のメッセージの配列にプッシュし、#4は新しいメッセージが作成されたときにサーバーに通知します。 app.js
の最後に、モーダルのコントローラーとして機能する関数を作成します。
app.js
//Dialog controller function UsernameDialogController($scope, $mdDialog) { $scope.answer = function (answer) { $mdDialog.hide(answer); }; }
CSSとアニメーション
見苦しい外観を修正するには、app.css
を更新してください。
body { background: #fafafa !important; } .footer { background: #fff; position: fixed; left: 0px; bottom: 0px; width: 100%; } .message.ng-enter { -webkit-animation: zoomIn 1s; -ms-animation: zoomIn 1s; animation: zoomIn 1s; }
最後のスタイルに注意してください。 ngAnimate
とanimate.css
を使用して、メッセージのきれいなアニメーションを作成しています。
このコンセプトここでどのように遊ぶことができるかについてはすでに書きました。
締めくくり
画像を見れば何が気になるのかわかります! アドレスバーですね。 これが、package.json
のwindow
構成の出番です。 "toolbar": true
を"toolbar": false
に変更するだけです。
また、アイコンを"icon": "app/public/img/scotch.png"
に設定して、ウィンドウアイコンをスコッチロゴに変更しました。 新しいメッセージが届いたら、通知を追加することもできます。
var options = { body: data.content }; var notification = new Notification("Message from: "+data.username, options); notification.onshow = function () { // auto close after 1 second setTimeout(function () { notification.close(); }, 2000); }
そしてさらに楽しい…
テスト
GitHubからWebクライアントをダウンロードしてアプリケーションをテストすることをお勧めします。 サーバー、Webクライアント、アプリの順に実行します。 アプリとWebクライアントの両方からメッセージの送信を開始し、同じ部屋でメッセージを送信している場合は、それらがリアルタイムで表示されるのを確認します。
さらに進む
さらに挑戦したい場合は、アプリに以下を追加してみてください
- Facebookによる認証。
- 部屋を更新するための管理セクション。
- 実際のユーザーアバターを使用します。
gulp deploy --テンプレート:Platform
を使用してアプリをデプロイします(例:gulp deploy --mac
)。 *など…
結論
最後までやり遂げてよかったです。 NodeWebKitは素晴らしいコンセプトです。 コミュニティに参加して、アプリの作成を簡単にします。 今日はスコッチがたくさんあり、誰かを笑顔にしたことを願っています…