Страница Cardshifter войти с помощью ванили на JavaScript


Я работаю над переписыванием Cardshifter HTML-клиент с ванильным JavaScript, то исходный HTML-клиент пишется с углового, но я действительно хотел, чтобы держаться подальше от всего, что НПМ и просто вернуться к истокам. Обратите внимание, что у меня нет желания использовать библиотеки, такие как jQuery, подчеркивания и т. д. только в случае крайней необходимости.

Это целевая страница, на которую пользователь выбирает сервер для подключения и логин. Я хотел бы обратную связь на любом и всех аспектах код, особенно если я использую анти-паттерны, которые я мог бы избежать с помощью всей остальной части клиента. Все разделы описаны, поэтому я не буду тратить время, объясняя, что все, кто здесь работает.

Если вы хотите, чтобы скользить по мелочи, основной код файлы, хотелось бы обратной связи, являются:

  • sections/login/login.js

  • server_interface/server_interface.js

  • utils/loadHtml.js

Вот это анимированный GIF, который показывает некоторые функции, а именно: проверять, является ли данный сервер может предложить действительное соединение. (обратите внимание, что alert показывает имя пользователя уже удален, это было для отладки.

check-server-connection

Структура каталогов

Вот как мои файлы структурированы в данный момент. У меня есть несколько других каталогов для изображений и такие, которые я исключил, поскольку они пока не используются.

index.html
global.js
sections/
  login/
    login.html
    login.js
  top_navbar/
    top_navbar.html
  server_interface/
    server_interface.js
  styles/
    cardshifter.css
  utils/
    formatDate.js
    loadHtml.js
    logDebugMessage.js

Код

index.html

<!DOCTYPE html>
<html>
    <head>
        <title>Cardshifter</title>
        <!-- Bootstrap -->
        <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" />

        <!-- Local styles -->
        <link rel="stylesheet" href="styles/cardshifter.css" />

        <!-- Local JavaScript -->
        <script src="global.js"></script>
        <script src="server_interface/server_interface.js"></script>
        <script src="utils/loadHtml.js"></script>
        <script src="utils/formatDate.js"></script>
        <script src="utils/logDebugMessage.js"></script>

        <!-- Local Section JavaScript -->
        <script src="sections/login/login.js"></script>

        <!-- Favicon links -->
        <link rel="apple-touch-icon" sizes="57x57" href="images/favicon/apple-icon-57x57.png" />
        <link rel="apple-touch-icon" sizes="60x60" href="images/favicon/apple-icon-60x60.png" />
        <link rel="apple-touch-icon" sizes="72x72" href="images/favicon/apple-icon-72x72.png" />
        <link rel="apple-touch-icon" sizes="76x76" href="images/favicon/apple-icon-76x76.png" />
        <link rel="apple-touch-icon" sizes="114x114" href="images/favicon/apple-icon-114x114.png" />
        <link rel="apple-touch-icon" sizes="120x120" href="images/favicon/apple-icon-120x120.png" />
        <link rel="apple-touch-icon" sizes="144x144" href="images/favicon/apple-icon-144x144.png" />
        <link rel="apple-touch-icon" sizes="152x152" href="images/favicon/apple-icon-152x152.png" />
        <link rel="apple-touch-icon" sizes="180x180" href="images/favicon/apple-icon-180x180.png" />
        <link rel="icon" type="image/png" sizes="192x192"  href="images/favicon/android-icon-192x192.png" />
        <link rel="icon" type="image/png" sizes="32x32" href="images/favicon/favicon-32x32.png" />
        <link rel="icon" type="image/png" sizes="96x96" href="images/favicon/favicon-96x96.png" />
        <link rel="icon" type="image/png" sizes="16x16" href="images/favicon/favicon-16x16.png" />
        <link rel="manifest" href="images/favicon/manifest.json" />
        <meta name="msapplication-TileColor" content="#ffffff" />
        <meta name="msapplication-TileImage" content="images/favicon/ms-icon-144x144.png" />
        <meta name="theme-color" content="#ffffff" />
    </head>
    <body>
        <!-- Top navigation bar -->
        <div id="top_navbar_container">
            <script>
                const navbarContainerId = "top_navbar_container";
                const navbarFilePath = "sections/top_navbar/top_navbar.html";
                loadHtml(navbarContainerId, navbarFilePath)
                .then(function() {
                    if (DEBUG) { 
                        logDebugMessage(`"${navbarFilePath}" loaded OK!`); 
                    }
                });
            </script>
        </div>

        <div class="csh-body">
            <div id="login_container">
                <script>
                    const loginContainerId = "login_container";
                    const loginFilePath = "sections/login/login.html";
                    loadHtml(loginContainerId, loginFilePath)
                    .then(function() {
                        loginHandler();
                        if (DEBUG) {
                            logDebugMessage(`"${loginFilePath}" loaded OK!`);
                        } 
                    });
                </script>
            </div>
        </div>
    </body>
</html>

global.js

/*
 * This file is for global values to be used throughout the site.
 */
'use strict';

/*
 * Setting to `true` will log messages in the browser console
 * to help in debugging and keeping track of what is happening on the page.
 * This should be set to `false` on the public client.
 */
const DEBUG = true;

/*
 * Port number used for WebSocket.
 */
const WS_PORT = 4243;

/*
 * List of game server names and WebSocket URIs.
 */
const GAME_SERVERS = {
    "localhost" : `ws://127.0.0.1:${WS_PORT}`,
    "dwarftowers.com" : `ws://dwarftowers.com:${WS_PORT}`,
    "zomis.net" : `ws://stats.zomis.net:${WS_PORT}`,
    "Other" : ""
};

/**
 * Default date format for the application.
 * @type String
 */
const DEFAULT_DATE_FORMAT = "yyyy/MM/dd hh:mm:ss";

разделы

sections/login/login.html

<div id="login">
    <h4>Please log in to continue.</h4>
    <form name="login_form" id="login_form" class="login-form">
        <div id="login_server_select_container" class="form-group">
            <label for="login_server_list" aria-label="Server">Server:</label>
            <select name="login_server_list" id="login_server_list" class="form-control">
            </select>
            <div id="login_server_other_container" class="form-group" style="display : none">
                <label for="login_server_other_input">Other Server:</label>
                <input name="login_server_other_input" id="login_server_other_input" type="text" class="form-control" />
                <input type="button" name="test_login_server_other" id="test_login_server_other" class="btn" value="Test connection" />
            </div>
            <input readonly name="server_loading_display" id="server_connecting" class="form-control" style="background-color: #DDD; display: none" />
            <label for="login_secure">Is secure server:</label>
            <input name="login_secure" id="login_secure" type="checkbox" value="secure" />
            <span id="login_server_connection_status" class="label" style="display: block; text-align: left"></span>
        </div>
        <div id="login_username_container" class="form-group">
            <label for="login_username">Username:</label>
            <input name="login_username" id="login_username" type="text" class="form-control" placeholder="Enter name..." />
        </div>
        <div class="form-group">
            <input type="button" name="login_submit" id="login_submit" type="button" class="btn btn-success" value="Log in" />
        </div>
    </form>
</div>

sections/login/login.js

/* global GAME_SERVERS, DEBUG, CardshifterServerAPI, DEFAULT_DATE_FORMAT */

const loginHandler = function() {
    const serverSelectContainer = document.getElementById("login_server_select_container");
    const serverSelect = serverSelectContainer.querySelector("#login_server_list");
    const serverOtherInputContainer = serverSelectContainer.querySelector("#login_server_other_container");
    const serverLoading = serverSelectContainer.querySelector("#server_connecting");
    const connStatusMsg = serverSelectContainer.querySelector("#login_server_connection_status");
    let currentServerHasValidConnection = null;

    /**
     * Adds options to the server selection based on GAME_SERVERS global.
     * @returns {undefined}
     */
    const populateServerSelect = function() {
        for (let key in GAME_SERVERS) {
            if (GAME_SERVERS.hasOwnProperty(key)) {
                const option = document.createElement("option");
                option.text = key;
                option.value = GAME_SERVERS[key];
                serverSelect.add(option);
            }
        }
    };

    /**
     * Tests the WebSocket connection to a server and displays a message on the page
     * to give the user information about the connection status.
     * @returns {undefined}
     */
    const testWebsocketConnection = function() {
        const serverUri = serverSelect.value;
        const isSecure = false;

        let msgText = "";

        if (serverUri) {
            displayConnStatus("connecting", serverUri);
            /**
             * Test WebSocket connection and display status if successful.
             * @returns {undefined}
             */
            const onReady = function() {
                makeServerSelectReadWrite();
                msgText = displayConnStatus("success", serverUri);
                if (DEBUG) { logDebugMessage(msgText); }
                currentServerHasValidConnection = true;
            };
            /**
             * Test WebSocket connection and display status if failed.
             * @returns {undefined}
             */
            const onError = function() {
                makeServerSelectReadWrite();
                msgText = displayConnStatus("failure", serverUri);
                if (DEBUG) { logDebugMessage(msgText); }
                currentServerHasValidConnection = false;
            };
            CardshifterServerAPI.init(serverUri, isSecure, onReady, onError);
            makeServerSelectReadOnly(serverUri);
        }
        else {
            displayConnStatus("unknown", serverUri);
        }
    };

    /**
     * Displays connection status in the page.
     * @param {string} status - Keyword representing the connection status
     * @param {type} serverUri - The URI of the server the client is connecting to
     * @returns {String} - The message text, largely for debug purposes
     */
    const displayConnStatus = function(status, serverUri) {
        let msgText = "";
        switch (status.toLowerCase()) {
            case "connecting":
                msgText = 
                    `<h5>Connecting to server...</h5>` + 
                    `<pre class='bg-warning'>` + 
                        `Address: ${serverUri}` + 
                        `\n${formatDate(new Date())}` +
                    `</pre>`;
                connStatusMsg.className = "label label-warning";
                connStatusMsg.innerHTML = msgText;
                break;
            case "success":
                msgText =
                    `<h5>WebSocket connection OK.</h5>\n` +
                    `<pre class='bg-success'>`+ 
                        `Address: ${serverUri}` +
                        `\n${formatDate(new Date())}` +
                    `</pre>`;
                connStatusMsg.innerHTML = msgText;
                connStatusMsg.className = "label label-success";
                break;
            case "failure":
                msgText = 
                    `<h5>WebSocket connection FAILED.</h5>\n` +
                    `<pre class='bg-danger'>`+ 
                        `Address: ${serverUri}` +
                        `\n${formatDate(new Date())}` +
                    `</pre>`;
                connStatusMsg.innerHTML = msgText;
                connStatusMsg.className = "label label-danger";
                break;
            case "unknown":
            default:
                msgText = `<h5>Unknown connection status...</h5>`;
                connStatusMsg.innerHTML = msgText;
                connStatusMsg.className = "label label-default";
                break;
        }
        return msgText;
    };

    /**
     * Hides the `select` element and shows a read-only `input` instead.
     * @param {string} serverUri
     * @returns {undefined}
     */
    const makeServerSelectReadOnly = function(serverUri) {
        const selector = document.getElementById("login_server_list");
        const connecting = document.getElementById("server_connecting");
        selector.style.display = "none";
        connecting.style.display = "block";
        connecting.value = `Connecting to ${serverUri}...`;
    };

    /**
     * Makes the server `select` element visible and hides the read-only `input`
     * @returns {undefined}
     */
    const makeServerSelectReadWrite = function() {
        const selector = document.getElementById("login_server_list");
        const connecting = document.getElementById("server_connecting");
        selector.style.display = "block";
        connecting.style.display = "none";
    };

    /**
     * Displays an input field for server address if "Other" server is selected.
     * @returns {undefined}
     */
    const handleServerSelectChanges = function() {
        if (serverSelect.value) {
            serverOtherInputContainer.style.display = "none";
        }
        else {
            serverOtherInputContainer.style.display = "block";
        }
    };

    /**
     * Attempts to login to game server.
     * @returns {undefined}
     */
    const tryLogin = function() {
        const username = document.getElementById("login_username").value;
        if (!username) {
            displayNoUsernameWarning();
        }
        else {
            const isSecure = false;
            var loggedIn = null;

            let serverUri = serverSelect.value;
            if (!serverUri) {
                serverUri = document.getElementById("login_server_other_input").value;
            }



            /**
             * Short-circuit login attempt if we've already found that the connection not valid.
             * @type String
             */
            if (!currentServerHasValidConnection) {
                const msg = "Websocket error(error 1)";
                console.log(msg);
                displayLoginFailureWarning(msg);
            }

            /**
             * Attempt to log in once the WebSocket connection is ready.
             * @returns {undefined}
             */
            const onReady = function() {
                let login = new CardshifterServerAPI.messageTypes.LoginMessage(username);

                /**
                 * Listens for a welcome message from the game server, and stores user values in the browser.
                 * @param {Object} welcome
                 * @returns {undefined}
                 */
                const messageListener = function(welcome) {
                    const SUCCESS = 200;
                    const SUCCESS_MESSAGE = "OK";
                    if(welcome.status === SUCCESS && welcome.message === SUCCESS_MESSAGE) {
                        localStorage.setItem("username", username);
                        localStorage.setItem("id", welcome.userId);
                        localStorage.setItem("playerIndex", null);
                        localStorage.setItem("game", { "id" : null, "mod" : null });                           
                    }
                    else {
                        console.log(`${new Date()} server message: ${welcome.message}`);
                        loggedIn = false;
                    }
                };

                try {
                    CardshifterServerAPI.setMessageListener(messageListener, ["loginresponse"]);
                    CardshifterServerAPI.sendMessage(login);
                }
                catch(error) {
                    const msg = "LoginMessage error(error 2)";
                    if (DEBUG) { logDebugMessage(`${msg} ${error}`); }
                    displayLoginFailureWarning(msg, error);
                    loggedIn = false;
                }
            };

            /**
             * Log error if the connection fails
             * @returns {undefined}
             */
            const onError = function() {
                const msg = "Websocket error(error 1)";
                if (DEBUG) { logDebugMessage(msg); }
                displayLoginFailureWarning(msg);
                loggedIn = false;
            };

            CardshifterServerAPI.init(serverUri, isSecure, onReady, onError);
        }
    };



    /**
     * Displays a warning if no username is entered.
     * @returns {undefined}
     */
    const displayNoUsernameWarning = function() {
        const container = document.getElementById("login_username_container");
        if (!container.querySelector("#login_username_missing_msg")) {
            const msg = document.createElement("span");
            msg.id = "login_username_missing_msg";
            msg.className = "label label-danger";
            msg.innerHTML = "Please enter a username.";
            container.appendChild(msg);
        }
    };

    const displayLoginFailureWarning = function(message, error) {
        const container = document.getElementById("login_username_container");
        const warning = document.createElement("span");
        warning.id = "login_failure_msg";
        warning.className = "label label-danger";
        warning.style = "display: block; text-align: left;";
        warning.innerHTML = `<h5>Login failed: ${message}</h5>`;
        if (error) {
            warning.innerHTML += `<pre>${error}</pre>`;
        }
        container.appendChild(warning);
    };

    const testOtherServerConnection = function() {
        const otherServerInput = document.getElementById("login_server_other_input");
        const otherServerUri = otherServerInput.value;
        const isSecure = false;

        /**
         * Test WebSocket connection and display status if successful.
         * @returns {undefined}
         */
        const onReady = function() {
            makeServerSelectReadWrite();
            msgText = displayConnStatus("success", otherServerUri);
            if (DEBUG) { logDebugMessage(msgText); }
            currentServerHasValidConnection = true;
        };
        /**
         * Test WebSocket connection and display status if failed.
         * @returns {undefined}
         */
        const onError = function() {
            makeServerSelectReadWrite();
            msgText = displayConnStatus("failure", otherServerUri);
            if (DEBUG) { logDebugMessage(msgText); }
            currentServerHasValidConnection = false;
        };
        CardshifterServerAPI.init(otherServerUri, isSecure, onReady, onError);
        makeServerSelectReadOnly();
        displayConnStatus("connecting", otherServerUri);
    };

    /**
     * IIFE to setup the login handling for the page it is loaded in.
     * @type undefined
     */
    const runLoginHandler = function() {
        populateServerSelect();
        document.getElementById("login_server_list").addEventListener("change", handleServerSelectChanges, false);
        document.getElementById("login_server_list").addEventListener("change", testWebsocketConnection, false);
        document.getElementById("login_submit").addEventListener("click", tryLogin, false);
        document.getElementById("test_login_server_other").addEventListener("click", testOtherServerConnection, false);
        testWebsocketConnection();
    }();
};

sections/top_navbar/top_navbar.html

<nav id="top_navbar" class="navbar navbar-inverse">
    <div class="container-fluid">

        <div class="navbar-header">
            <!-- TODO fix this logic -->
            <div class="navbar-brand csh-top-link">Cardshifter</div>
        </div>

        <form class="navbar-form">

            <ul class ="navbar-form navbar-left" style="margin-top: 8px;">
                <li class="dropdown">
                    <a href="#" class="dropdown-toggle csh-dropdown-link" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
                        Mods
                        <span class="caret"></span></a>
                    <ul class="dropdown-menu">
                        <li class="cyborg-font">Cyborg Chronicles</li>
                        <li class="cyborg-font"><a href=#>Game rules</a></li>
                        <li class="cyborg-font"><a href=#>Cards</a></li>

                        <li role="separator" class="divider"></li>

                        <li class="mythos-font">Mythos</li>
                        <li class="mythos-font"><a href=#>Game rules</a></li>
                        <li class="mythos-font"><a href=#>Cards</a></li>
                    </ul>
                </li>
            </ul>
            <ul class ="navbar-form navbar-left" style="margin-top: 8px;">
                <li class="dropdown">
                    <a href="#" class="dropdown-toggle csh-dropdown-link" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
                        Help
                        <span class="caret"></span></a>
                </li>
            </ul>
            <ul class ="navbar-form navbar-left" style="margin-top: 8px;">
                <li class="dropdown">
                    <a href="#" class="dropdown-toggle csh-dropdown-link" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
                        About
                        <span class="caret"></span></a>
                </li>
            </ul>

            <div class="form-group navbar-form navbar-left">
                <input name="disconnect_websocket" id="disconnect_websocket" type="button" value="Log Out" class="btn btn-navbar csh-button" />
            </div>
            <div class="form-group navbar-form navbar-left">
                <input name="display_console" id="display_console" type="button" value="Console" class="btn btn-navbar csh-button" />
            </div>

        </form>

    </div>
</nav>

server_interface

server_interface/server_interface.js

"use strict";

// checks if the string begins with either ws:// or wss://
const wsProtocolFinder = /ws(s)*:\/\//;

/*
 * Enum for WebSocket ready state constants.
 * @enum {number}
 */
const readyStates = {
    CONNECTING : 0,
    OPEN : 1,
    CLOSING : 2,
    CLOSED : 3
};

const MAIN_LOBBY = 1;

let eventTypes = [];

/**
* The base class Message for all the other message types
* to inherit from.
*
* TODO: Would it just be easier to set the `.command` property
* individually for each card type?
* 
* @param {string} command - The command of the message.
*/
const Message = function(command) {
    this.command = command;
};

/**
* The exception that is thrown when the code is trying to
* interact with the API when the API has not been
* initialized with `.init` yet.
* 
* @param {string} message - Informational message about the exception.
*/
const NotInitializedException = function(message) {
    this.name = "NotInitializedException";
    this.message = message || "";
};

/**
* The exception that is thrown when the code is telling the
* API to interact with the socket when the socket is not
* ready to accept any information.
* 
* @param {string} message - Informational message about the exception.
* @param {number} readyState - Ready state constant from WebSocket API, https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
*/
const SocketNotReadyException = function(message, readyState) {
    this.name = "SocketNotReadyException";
    this.message = message || "";
    this.readyState = readyState;
};

/*
 * Returns all the keys of an object and its inherited keys.
 * This is used so `JSON.stringify` can get the `.command` of a message.
 * 
 * @param {Object} obj - The object to flatten
 * @return {Object} - a new Object, containing obj's keys and inherited keys
 * @source http://stackoverflow.com/questions/8779249/how-to-stringify-inherited-objects-to-json
*/
const flatten = function(obj) {
    let result = Object.create(obj);
    for(let key in result) {
        // TODO this assignment is weird, why is `result[key]` being assigned to its own value?
        result[key] = result[key];
    }
    return result;
};

/*
 * Singleton object to handle communication via WebSocket between the client
 * and the game server.
 */
const CardshifterServerAPI = {
    socket: null,
    messageTypes: {
        /*
         * Incoming login message.
         * A login message from a client to add a user to the available users on the server.
         * This login message is required before any other action or message can be performed between a client and a server.
         * @constructor
         * @param {string} username - The incoming user name passed from client to server, not null
         * @example Message: <code>{ "command":"login","username":"JohnDoe" }
*/
 LoginMessage : функция(имя пользователя) {
 это.имя пользователя = имя пользователя;
},

/*
 * Запрос доступных целей для конкретных действий, которые будет выполнять предприятие.
 * Эти внутриигровые сообщения запросить список всех существующих мишеней для данного действия и лица.
 * Клиент использует этот запрос для того, чтобы указать на цели (надеюсь, с наглядным пособием, например для подсветки цели)
 * что сущность (например, создание карты, или игрок) может выполнять действия на (например, атака или модифицировать карты).
 * @конструктор
 * @парам {номер} gameId - идентификатор этой игры в настоящее время играют
 * @парам {номер} ID - идентификатор этой сущности, которая просит выполнить действие
 * @парам {строка} действие - название этой акции просят быть выполнена
*/
 RequestTargetsMessage : функция(gameId, удостоверение личности, действия) {
 это.gameId = gameId;
 это.идентификатор = идентификатор;
 это.действие = действие;
},

/*
 * Определенный тип запросов к серверу.
 * Это используется, чтобы запросить действие от сервера, который требуется серверная информация.
 * @конструктор
 * @парам {строка} запрос - это запрос
 * @парам {строка} message - сообщение, сопровождая эту просьбу
*/
 ServerQueryMessage : функция(запрос, сообщение) {
 это.запрос = запрос;
 это.сообщение = сообщение;

 это.метод toString = функция() {
 ServerQueryMessage ответ: Запрос${этот.сообщение запроса}: ${этот.сообщение}`;
};
},


/*
 * Запрос, чтобы начать новую игру.
 * Это будет отправлен от клиента к серверу, если игрок приглашает другого игрока (включая AI) 
 * чтобы начать новую игру выбранного типа.
 * @конструктор
 * @парам соперника - ID игрока, лица, приглашенные этот игрок
 * @param и тип игры - тип / мод игры выбирают этот игрок
*/
 StartGameRequest : функция(соперника, тип игры) {
 это.оппонент = оппонент;
 это.тип игры = тип игры;
},

/*
 * Сериализовать сообщение от JSON в байт.
 * Главным образом использовано для клиента и libGDX.
 * Конструктор.
 * @парам тип - это тип сообщения 
*/
 TransformerMessage : функция(тип) {
 это.тип = тип;
},

/*
 * Сообщение для игры лица использовать определенные способности.
 * Игровые сущности (например, карт, игроки) могут иметь одну или несколько возможностей действий, которые они могут выполнять.
 * Некоторые способности могут иметь несколько целей, поэтому использование массива.
 * @конструктор
 * Используется для нескольких целевых действий.
*
 * @param с gameId - это тока игра
 * @парам сущности - это игра, лицо, осуществляющее действие
 * @парам действие - это действие
 * @param по элементам - набор из нескольких мишеней, пострадавших от этой акции
*/
 UseAbilityMessage : функция(gameId, удостоверение личности, действия, цели) {
 это.gameId = gameId;
 это.идентификатор = идентификатор;
 это.действие = действие;
 это.цели = цели;

 это.метод toString = функция() {
 возвращение `
 + `UseAbilityMessage`
 + `[идентификатор=${этот.идентификатор},`
 действие + `=${этот.действие},`
 + `gameId=${этот.gameId}`
 цели + `=${этот.цели.метод toString()}]`
;
};
},

/*
 * Сообщения в чате в игровом лобби.
 * Эти сообщения, напечатанные на игровом лобби, которые видны для всех пользователей, присутствующих на момент опубликования сообщения.
 * @конструктор
 * @парам {строка} сообщение - содержание этого чата сообщение
*/
 ChatMessage : функция(сообщение) {
 это.chatId = MAIN_LOBBY;
 это.сообщение = сообщение;

 это.метод toString = функция() {
 // Задач, где это " от " парам/ВАР пришел?
 ChatMessage возвращение `[chatId=${chatId}, сообщение=${сообщение}, с=${от}]`;
};
},

/*
 * Просьба пригласить игрока начать новую игру.
 * @конструктор
 * @param параметр ID - идентификатор этого запрос Invite
 * @парам {строка} name - имя игрока приглашают
 * @param и тип игры - Игра типа этот запрос Invite
*/
 InviteRequest : функция(идентификатор, имя, тип игры) {
 это.идентификатор = идентификатор;
 это.имя = имя;
 это.тип игры = тип игры;
},

/*
 * Ответ на сообщение InviteRequest.
 * @конструктор
 * @парам inviteId - идентификатор входящего сообщения InviteRequest 
 * @парам {логический} принято - ли InviteRequest принято
*/
 InviteResponse : функция(inviteId, принято) {
 это.inviteId = inviteId;
 это.принят = принят;
},

/*
 * Конфигурация игрока для данной игры.
 * @конструктор
 * @param с gameId - эта игра
 * @парам {строка} имя модуля - мод название для этой игры
 * @парам {карта} конфиги - карту имя игрока и соответствующую конфигурацию игрока 
*/
 PlayerConfigMessage : функция(gameId, имя модуля, конфиги) {
 это.gameId = gameId;
 это.имя модуля = имя модуля;
 это.конфиги = конфиги;

 это.метод toString = функция() {
 возвращение `
 PlayerConfigMessage + `{`
 + `конфиги=${конфиги}, `
 gameId + `=${gameId}, `
 + `имя модуля='${имя модуля}"
 + `}`
;
};
}
},
/*
 * Инициализирует API для использования.
*
 * Это устанавливает все типы сообщений, чтобы наследовать класс главная "сообщения", и устанавливает
 * вверх с WebSocket, который будет использоваться для связи с сервером, и получать
 * информация с сервера.
 * 
 * @парам {строка} - сервер - адрес сервера для подключения к
 * @парам {логический} isSecure - стоит ли использовать SSL для подключения (не реализовано)
 * @парам onReady - функцию, чтобы присвоить сокета.onopen`
 * @парам метод onerror - функцию, чтобы присвоить сокета.метод onerror`
*/
 инит : функция(сервер, isSecure, onReady, число) {
 пусть видах = это.сообщения;
 // Дел выяснить, почему эта неиспользованная переменная здесь
 пусть самостоятельно = это; // для события

 типов.LoginMessage.прототип = новое сообщение("входа");
 типов.RequestTargetsMessage.прототип = новое сообщение("requestTargets");
 типов.ServerQueryMessage.прототип = новое сообщение("запрос");
 типов.StartGameRequest.прототип = новое сообщение("startgame");
 типов.TransformerMessage.прототип = новое сообщение("последовательный");
 типов.UseAbilityMessage.прототип = новое сообщение("использование");
 типов.ChatMessage.прототип = новое сообщение("чат");
 типов.InviteRequest.прототип = новое сообщение("inviteRequest");
 типов.InviteResponse.прототип = новое сообщение("inviteResponse");
 типов.PlayerConfigMessage.прототип = новое сообщение("playerconfig");
 NotInitializedException.прототип = новые ошибки();
 SocketNotReadyException.прототип = новые ошибки();

 // безопасной вебсокетов это всиво://, а чем WS://
 константный secureAddon = (isSecure ? "ы" : "");
 // если протокол не найден в строке, записать правильный протокол (это безопасно?)
 константный protocolAddon = (wsProtocolFinder.тест(сервер) ? "" : `с WS${secureAddon}://`);

 давайте сокет = новая вебсокетов(protocolAddon + сервер);

 гнездо.onopen = onReady;

 гнездо.метод onerror = функция() {
метод onerror();
 это.гнездо = нуль;
};

 это.гнездо = гнездо;
},

/**
 * Отправляет сообщение на сервер
*
 * @парам {объект} message - сообщение для отправки
 * @SocketNotReadyException ошибка - сокет не готов к использованию
 * @NotInitializedException ошибок в API еще не был инициализирован
*/
 метод SendMessage : функция(сообщение) {
 гнездо с const = это.гнездо;
 // Дел выяснить, почему эта неиспользованная переменная здесь
 пусть самостоятельно = это;
 если (гнездо) {
 если (гнездо.в свойстве readyState === readyStates.Открыть) {
это.гнездо.отправить(в JSON.преобразовать в строки(сведение(сообщение)));
 } 
 еще {
 бросить новый SocketNotReadyException("в проксировании не готова к использованию.", гнездо.в свойстве readyState);
}
 } 
 еще {
 бросить новый NotInitializedException("этот API еще не был инициализирован.");
}
},

/**
 * Устанавливает прослушиватель события, когда сервер отправляет сообщение и
 * тип сообщения-это один из видов типов
*
 * @парам слушателя - функция для пожара при получении сообщения из типов 
 * @парам {строка[]} типы - (необязательно) только огонь слушателя, когда тип сообщения в этом массиве
 * @парам {объект} тайм-аут - (опционально) функция(.ontimeout) звонить после МС(.МС) нет ответа
*
 * Список todo: возможно, время ожидания будет нужен? Передать в функцию и МС граф.
*/
 setMessageListener : функция(слушатель, типы таймаут) {
 eventTypes = типы;

 это.гнездо.onmessage = функция(сообщение) {
 дисп данные = формат JSON.разобрать(сообщение.данных);
 если (eventTypes) {
 если(eventTypes.метод indexOf(данные.команды) !== -1) { // если содержит
слушатель(данные);
}
 } 
 еще {
слушатель(данные);
}
};
},

/**
 * Добавляет видах типам для прослушивания в случае сообщения слушателя
*
 * @парам {строка[]} типы - типы, чтобы добавить
*/
 addEventTypes : функция(типы) {
 eventTypes = eventTypes.функция concat(типов);
},

/**
 * Удаляет событие сообщение слушателя
*/
 removeMessageListener : функция() {
 это.гнездо.onmessage = нуль;
}
};

утилиты

utils/formatDate.js

/* global DEFAULT_DATE_FORMAT */

/**
 * Formats a Date object based on a format string, e.g., "yyyy/MM/dd hh:mm:ss"
 * Original source: 
 * https://dzone.com/articles/javascript-formatdate-function
 * Original source modified to fix a few bugs and modernize.
 * 
 * @param {Date} date - the Date to format
 * @param {String} formatString - the format string to use
 * @returns {String} - the formatted date
 */
const formatDate = function (date, formatString=DEFAULT_DATE_FORMAT) {
    if(date instanceof Date) {
        const months = new Array("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");
        const yyyy = date.getFullYear();
        const yy = yyyy.toString().slice(-2);
        const M = date.getMonth() + 1;
        const MM = M < 10 ? `0${M}` : M;
        const MMM = months[M - 1];
        const d = date.getDate();
        const dd = d < 10 ? `0${d}` : d;
        const h = date.getHours();
        const hh = h < 10 ? `0${h}` : h;
        const m = date.getMinutes();
        const mm = m < 10 ? `0${m}` : m;
        const s = date.getSeconds();
        const ss = s < 10 ? `0${s}` : s;
        formatString = formatString.replace(/yyyy/, yyyy);
        formatString = formatString.replace(/yy/, yy);
        formatString = formatString.replace(/MMM/, MMM);
        formatString = formatString.replace(/MM/, MM);
        formatString = formatString.replace(/M/, M);
        formatString = formatString.replace(/dd/, dd);
        formatString = formatString.replace(/d/, d);
        formatString = formatString.replace(/hh/, hh);
        formatString = formatString.replace(/h/, h);
        formatString = formatString.replace(/mm/, mm);
        formatString = formatString.replace(/m/, m);
        formatString = formatString.replace(/ss/, ss);
        formatString = formatString.replace(/s/, s);
        return formatString;
    } else {
        return "";
    }
};

utils/loadHtml.js

/* global fetch, DEBUG */
"use strict";

/*
 * Replicates the functionality of jQuery's `load` function, 
 * used to load some HTML from another file into the current one.
 * 
 * Based on this Stack Overflow answer:
 * https://stackoverflow.com/a/38132775/3626537
 * And `fetch` documentation:
 * https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
 * 
 * @param {string} parentElementId - The ID of the DOM element to load into
 * @param {string} htmlFilePath - The path of the HTML file to load
 */
const loadHtml = function (parentElementId, filePath) {
    const init = {
        method: "GET",
        headers: { "Content-Type": "text/html" },
        mode: "cors",
        cache: "default"
    };
    // Return Promise from `fetch` allows to use `.then` after call.
    return fetch(filePath, init)
    .then(function (response) {
        return response.text();
    })
    .then(function (body) {
        // Replace `#` char in case the function gets called `querySelector` or jQuery style
        if (parentElementId.startsWith("#")) {
            parentElementId.replace("#", "");
        }
        document.getElementById(parentElementId).innerHTML = body;
        if (DEBUG) {
            console.log(`File "${filePath}" loaded into element ID "${parentElementId}"`);
        }
    })
    .catch(function(err) {
        throw new FailureToLoadHTMLException(
            `Could not load "${filePath} ` + 
            `into element ID "${parentElementId}"` +
            `\n${err}`
        );
    });
};

const FailureToLoadHTMLException = function(message) {
    this.name = "FailureToLoadHTMLException";
    this.message = message;
    this.stack = (new Error()).stack;
};

FailureToLoadHTMLException.prototype = new Error;

utils/logDebugMessage.js

/* global DEFAULT_DATE_FORMAT */

/**
 * Log a debug message to the browser's JavaScript console.
 * @param {String} msg
 * @param {String} dateFormat
 * @returns {undefined}
 */
const logDebugMessage = function(msg, dateFormat=DEFAULT_DATE_FORMAT) {
    const timestamp = new Date();
    console.log(`DEBUG | ${formatDate(timestamp, dateFormat)} | ${msg}`);
};


242
9
задан 24 марта 2018 в 09:03 Источник Поделиться
Комментарии
1 ответ

Очень аккуратный проект! Мне очень нравится, что вы избегаете с помощью библиотеки, это отличный способ, чтобы получить лучшие в JS.


  • Я вижу if (DEBUG) { logDebugMessage() } несколько раз, это может быть стоит условная внутри logDebugMessage функция.

  • formatDate имеет кучу повторяющегося кода, я бы рекомендовал воспользоваться .replace'ы второго параметра. Там, наверное, какой-то способ уменьшить это дальше...

    const formatDate = function (date, formatString = DEFAULT_DATE_FORMAT) {
    if (!(date instanceof Date)) {
    return ""
    }

    const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    const format = {
    yyyy: date.getFullYear(),
    M: date.getMonth() + 1,
    d: date.getDate(),
    h: date.getHours(),
    m: date.getMinutes(),
    s: date.getSeconds()
    }
    format.yy = format.yyyy.toString().slice(-2);
    format.MM = format.M < 10 ? `0${format.M}` : M;
    format.MMM = months[date.getMonth()];
    format.dd = format.d < 10 ? `0${format.d}` : format.d;
    format.hh = format.h < 10 ? `0${format.h}` : format.h;
    format.mm = format.m < 10 ? `0${format.m}` : format.m;
    format.ss = format.s < 10 ? `0${format.s}` : format.s;

    const regex = /yyyy|yy|MMM|MM|M|dd|d|hh|h|mm|m|ss|s/g
    return formatString.replace(regex, s => format[s] || s);
    };


  • Оригинал formatDate функция будет иметь проблемы в мае, если MMM включен в строку формата.

  • Может быть, стоит позволять formatDate принимать никакие аргументы, в этом случае он использует текущую дату.

  • loadHtml выглядит хорошо для меня, с двумя исключениями. Во-первых, если переданный идентификатор начинается с #в # не будет удален, а комментарий подразумевает, что он должен. Во-вторых, если вы определяете logDebugMessage функцию, используйте его! Падение console.log.

  • В server_interface.jsЯ бы рекомендовал источника flatten метод, он объясняет, как это работает.

  • Избавиться от неиспользуемых переменных. Есть много инструментов, которые могут проверить, если используется переменная, вы должны избавиться от них, а не говорил о них.

  • Так как вы используете let и constможно использовать Array.prototype.includes вместо Array.prototype.indexOf чтобы проверить наличие элемента в массиве.

  • В ChatMessage toString метод не будет работать. Яш не на C# - это не возможно отказаться this и еще доступ к переменные экземпляра. Тоже касается PlayerConfigMessage.

  • Вместо использования LoginMessage : function(username) шаблон можно использовать LoginMessage(username).

  • wsProtocolFinder не проверяет wss в начало строки. Это будет также соответствовать wssssss://. Регулярное выражение должно быть /^wss?:\/\//.

  • Я знаю, что цель этого проекта-чтобы избежать каких-либо npm-пакетов, однако вы действительно должны рассмотреть, по крайней мере, с помощью программы пылеобразования (если только установлен локально). Почти все пылеобразования программы могли бы поймал проблемы с this упомянутых выше, и может также предупредить вас о неиспользуемых переменных.

5
ответ дан 25 марта 2018 в 03:03 Источник Поделиться