使用NodeWebkit、Socket.io和MEAN制作实时聊天室

来自菜鸟教程
跳转至:导航、​搜索

介绍

开发人员不知疲倦地工作以使构建程序尽可能简单。 自从引入 Node 和 Cordova 以来,JavaScript、Web 和移动应用程序开发人员社区急剧增加。 通过 Node.js 的帮助,拥有网页设计技能的开发人员可以毫不费力地为他们的应用程序推出使用 JavaScript 的服务器。

移动爱好者现在可以在 Cordova 的帮助下仅使用 JavaScript 构建丰富的混合应用程序。 今天,虽然是老新闻,我很高兴分享使用 JavaScript 构建桌面独立应用程序的能力

Node WebKit 通常写成:“node-webkit”或“NW.js”是基于 Node.js 和 Chromium 的应用程序运行时,使我们能够仅使用 HTML、CSS 和 JavaScript 开发 OS 原生应用程序。

简而言之,Node WebKit 只是帮助您利用您作为 Web 开发人员的技能来构建一个本机应用程序,该应用程序只需一个 grunt/gulp(如果愿意)构建命令即可在 Mac、Windows 和 Linux 上舒适地运行。

本文将更多地集中在使用 Node WebKit 上,但为了让事情变得更有趣,我们将包括其他令人惊叹的解决方案,它们将包括:

  • Socket.io Node.js 的实时库
  • Angular Material:Angular 对 Google 的 Material Design 的实现
  • MEAN:MEAN 只是结合 Mongo、Express、Angular 和 Node 的特性来构建强大应用程序的概念

此外,该应用程序具有三个部分:

  • 服务器
  • 桌面(客户端)
  • 网络(客户端)

web部分不会在这里介绍,但它会作为一个测试平台,但不用担心,代码会提供。

先决条件

Level:中级(需要MEAN知识)

安装

我们需要为我们的应用程序获取 node-webkit 和其他依赖项。 幸运的是,有一些框架可以简化工作流程,我们将使用其中一个来搭建我们的应用程序并更多地专注于实现。

Yo 和 Slush 是流行的生成器,其中任何一个都可以工作。 我将使用 Slush,但如果您愿意,请随意使用 Yo。 要安装 Slush,请确保您已安装并运行 node 和 npm

npm install -g slush gulp bower slush-wean

该命令将在我们的系统上全局安装以下内容。

  • slush:一个脚手架工具
  • slush-wean:Node WebKit 的生成器
  • gulp:我们的任务运行器
  • bower:用于前端依赖

就像 YO 一样,使用以下命令创建目录并搭建应用程序:

mkdir scotch-chat
cd scotch-chat
slush wean

运行以下命令将让我们大致了解我们一直在等待的内容:

gulp run

该图像显示了我们的应用程序加载。 生成器的作者很慷慨地提供了一个带有简单加载动画的漂亮模板。 为了看起来更酷,我用 Scotch 的标志替换了加载文本。

如果您对 Slush 自动化操作不满意,您可以直接访问 GitHub 上的 Node WebKit。

现在我们已经设置了我们的应用程序,虽然是空的,但我们将让它休息一下,现在准备我们的服务器。

服务器

服务器基本上由我们的 modelroutessocket events 组成。 我们将使其尽可能简单,您可以按照文章末尾的说明随意扩展应用程序。

目录结构

在您的 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 中设置全局变量,它将保存已安装的应用程序依赖项。

服务器.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 公开了一种方法来帮助配置静态文件文件夹。 很简单:

服务器.js

    ...

    // tell express where to serve static files from
    app.use(express.static(__dirname + '/public'));

接下来是创建与我们的数据库的连接。 我正在使用本地 MongoDB,这显然是可选的,因为您会发现它由 Mongo 数据库托管。 Mongoose 是一个节点模块,它公开了令人惊叹的 API,这使得使用 MongoDB 变得更加容易。

服务器.js

    ...

    mongoose.connect("mongodb://127.0.0.1:27017/scotch-chat");

使用 Mongoose,我们现在可以创建我们的数据库模式和模型。 我们还需要在应用程序中允许 CORS,因为我们将从不同的域访问它。

服务器.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();
      }
    });

我们的服务器将包含三个路由。 提供索引文件的路由,另一个设置聊天数据的路由,最后一个提供按房间名称过滤的聊天消息:

服务器.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 文件发送给我们的用户。

第二个 /setup 意味着在应用程序的初始启动时只被点击一次。 如果您不需要一些测试数据,它是可选的。 它基本上创建了一个聊天消息数组(与模式匹配),遍历它们,并将它们插入数据库。

第三条路由 /msg 负责获取使用房间名称过滤的聊天历史记录,并以 JSON 对象数组的形式返回。

我们服务器最重要的部分是实时逻辑。 请记住,我们正在努力生成一个简单的应用程序,我们的逻辑将全面最小化。 依次,我们需要:

  • 知道我们的应用程序何时启动
  • 在连接时发送所有可用房间
  • 监听用户连接并将他们分配到默认房间
  • 听他们什么时候换房间
  • 最后,收听新消息并仅将消息发送给创建消息的房间中的人

所以:

服务器.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||||||||||||||||||*/

然后传统服务器启动:

服务器.js

    server.listen(2015);
    console.log('It\'s going down in 2015');

用任何适合您的 HTML 填充 index.html 并运行 node server.jslocalhost:2015 将为您提供 HTML 的内容。

Node WebKit 客户端

是时候挖掘我们留下的东西来创建我们当前正在运行的服务器了。 这部分非常简单,因为它只需要您对 HTML、CSS、JS 和 Angular 的日常了解。

[1] –>

目录结构

我们不需要创建任何东西! 我想这就是生成器的灵感。 您可能要检查的第一个文件是 package.json

Node WebKit 基本上需要两个主要文件才能运行:

  1. 入口点(index.html
  2. 一个 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 更新为:

索引.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)。 注意事项:

  • 我们正在使用 angular material,它的指令使我们的代码看起来像“HTML 6”。
  • 我们正在使用 ng-repeat 遍历我们的消息范围并将其值呈现给浏览器
  • 我们稍后将看到的指令帮助我们在按下 ENTER 键时发送消息
  • init 上,要求用户提供首选用户名
  • 包含一个 Angular 库,以帮助在 Angular 中更轻松地使用 Socket.io

应用程序

本节的主要部分是 app.js 文件。 它创建与 Node WebKit GUI 交互的服务,一个处理 ENTER 按键和控制器(主和对话框)的指令。

应用程序.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;
    });

接下来,我们创建三个 Angular 服务。 第一个服务帮助我们获取 Node WebKit GUI 对象,第二个返回其 Window 属性,第三个使用基本 URL 引导 Socket.io

应用程序.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 以来,上面的代码片段就是我的 最爱 之一。 它将事件绑定到 ENTER 键,从而可以在按下键时触发事件。

最后配上app.js就是全能控制器。 我们需要像我们在 server.js 中所做的那样分解事物以便于理解。 控制器应:

  1. 使用服务器发出的房间创建窗口菜单列表。
  2. 加入时的用户应提供其用户名。
  3. 收听来自服务器的新消息。
  4. 通过键入并点击 ENTER 键在创建新消息时通知服务器。

创建房间列表

定义好目标后,让我们编写代码:

应用程序.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

    });

那是我们控制器的骨架及其所有依赖项。 如您所见,它有四个内部注释,作为目标中定义的代码的占位符。 所以让我们选择菜单。

应用程序.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()
        }
    }));

我们只是创建了菜单的实例,并在其中附加了一些菜单(房间和出口)。 房间菜单应作为下拉菜单,因此我们必须向服务器询问可用房间并将其附加到房间菜单中:

应用程序.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 完成。

询问用户名

我们的第二个目标是使用角度材料模态向用户询问用户名。

应用程序.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 中所指定的,在初始化时,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 只监听消息,如果有的话,将其推送到现有消息的数组中,#4 在创建新消息时通知服务器。 在 app.js 的末尾,我们创建一个函数作为 Modal 的控制器:

应用程序.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;
    }

注意最后一种风格。 我们正在使用 ngAnimateanimate.css 为我们的消息创建漂亮的动画。

我已经在 这里 写了关于如何使用这个概念的文章。

关闭

看了图片我就猜到你在担心什么了! 地址栏,对吧? 这就是 package.json 中的 window 配置的用武之地。 只需将 "toolbar": true 更改为 "toolbar": false

我还将我的图标设置为 "icon": "app/public/img/scotch.png" 以将窗口图标更改为 Scotch 徽标。 一旦有新消息,我们也可以添加通知:

    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);
    }

更有趣的是……

测试

我建议您通过从 Git Hub 下载 Web 客户端来测试应用程序。 运行服务器,然后是 Web 客户端,然后是应用程序。 开始从应用程序和 Web 客户端发送消息,如果您在同一个房间发送消息,它们会实时显示。

走得更远

如果您想进一步挑战自己,可以尝试将以下内容添加到我们的应用程序中

  1. 使用 Facebook 进行身份验证。
  2. 管理部分以更新房间。
  3. 使用真实的用户头像。
  4. 使用 gulp deploy --模板:Platform 部署应用程序,例如:gulp deploy --mac。 * ETC…

结论

我很高兴我们坚持到了最后。 Node WebKit 是一个了不起的概念。 加入社区,让构建应用程序更容易。 希望你今天喝了很多苏格兰威士忌,希望我让某人微笑……