首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在JavaScript中创建类库

在JavaScript中创建类库
EN

Code Review用户
提问于 2014-09-07 18:36:25
回答 1查看 7.5K关注 0票数 10

我是一名经验丰富的Java程序员,但对JavaScript来说相当陌生。

我正在JavaScript中创建一个聊天库,并让它正常工作,但我想知道我是否正确地做了这件事,并遵循了正确的JavaScript编码标准。

我有一个SDK对象、LiveChatConnection类以及LiveChatListenerCredentials接口(我知道JavaScript中不存在类和接口,但这似乎就是如何创建它们)。

声明我正在使用的方法

代码语言:javascript
复制
this.myMethod = function() {...};

我见过其他库将方法放在类原型上。

代码语言:javascript
复制
MyClass.prototype.myMethod = function() {...}

或者宣布他们,

代码语言:javascript
复制
myMethod : function() {...}

不知道哪一个最好。

因此,我的SDK将打包在一个sdk.js文件在我的网站上,然后用户应该能够导入和使用它在他们的网页。

不确定从使用角度看我的代码是否有意义,或者我是否遗漏了其他任何东西。

代码语言:javascript
复制
var SDK = {};

function Credentials() {
    this.host = "";
    this.app = "";
    this.url = "";
    this.applicationId = "";
}


/**
 * Listener interface for a LiveChatConnection.
 * This gives asynchronous notification when a channel receives a message, or notice.
 */
function LiveChatListener() {
    /**
     * A user message was received from the channel.
     */
    this.message = function(message) {};

    /**
     * An informational message was received from the channel.
     * Such as a new user joined, private request, etc.
     */ 
    this.info = function(message) {};

    /**
     * An error message was received from the channel.
     * This could be an access error, or message failure.
     */ 
    this.error = function(message) {};

    /**
     * Notification that the connection was closed.
     */
    this.closed = function() {};

    /**
     * The channels users changed (user joined, left, etc.)
     * This contains a comma separated values (CSV) list of the current channel users.
     * It can be passed to the SDKConnection.getUsers() API to obtain the UserConfig info for the users.
     */
    this.updateUsers = function(usersCSV) {};

    /**
     * The channels users changed (user joined, left, etc.)
     * This contains a HTML list of the current channel users.
     * It can be inserted into an HTML document to display the users.
     */
    this.updateUsersXML = function(usersXML) {};
}

/**
 * Connection class for a Live Chat, or chatroom connection.
 * A live chat connection is different than an SDKConnection as it is asynchronous,
 * and uses web sockets for communication.
 */
function LiveChatConnection(credentials) {
    this.debug = false;
    this.channel = null;
    this.user = null;
    this.credentials = credentials;
    this.socket = null;
    this.listener = null;
    this.keepAlive = false;
    this.keepAliveInterval = null;

    /**
     * Connection to the live chat server channel.
     * Validate the user credentials.
     * This call is asynchronous, any error or success with be sent as a separate message to the listener.
     */
    this.connect = function(channel, user) {
        if (this.credentials == null) {
            throw "Mising credentials";
        }
        this.channel = channel;
        this.user = user;
        var host = "ws://" + this.credentials.host + this.credentials.app + "/live/chat";
        if ('WebSocket' in window) {
            this.socket = new WebSocket(host);
        } else if ('MozWebSocket' in window) {
            this.socket = new MozWebSocket(host);
        } else {
            this.socket = new WebSocket(host);
            //throw 'Error: WebSocket is not supported by this browser.';
        }

        this.listener.connection = this;
        var self = this;

        this.socket.onopen = function () {
            if (self.user == null) {
                self.socket.send("connect " + self.channel.id + " " + self.credentials.applicationId);
            } else {
                self.socket.send(
                        "connect " + self.channel.id + " " + self.user.user + " " + self.user.token + " " + self.credentials.applicationId);                        
            }
            self.setKeepAlive(this.keepAlive);
        };

        this.socket.onclose = function () {
            self.listener.message("Info: Closed");
            self.listener.closed();
        };

        this.socket.onmessage = function (message) {
            user = "";
            data = message.data;
            text = data;
            index = text.indexOf(':');
            if (index != -1) {
                user = text.substring(0, index);
                data = text.substring(index + 2, text.length);
            }
            if (user == "Online-xml") {
                self.listener.updateUsersXML(data);
                return;
            }
            if (user == "Online") {
                self.listener.updateUsers(data);
                return;
            }

            if (self.keepAlive && user == "Info" && text.contains("pong")) {
                return;
            }
            if (user == "Info") {
                self.listener.info(text);
                return;
            }
            if (user == "Error") {
                self.listener.error(text);
                return;
            }
            self.listener.message(text);
        };
    };

    /**
     * Sent a text message to the channel.
     * This call is asynchronous, any error or success with be sent as a separate message to the listener.
     * Note, the listener will receive its own messages.
     */
    this.sendMessage = function(message) {
        this.checkSocket();
        this.socket.send(message);
    };

    /**
     * Accept a private request.
     * This is also used by an operator to accept the top of the waiting queue.
     * This can also be used by a user to chat with the channel bot.
     * This call is asynchronous, any error or success with be sent as a separate message to the listener.
     */
    this.accept = function() {
        this.checkSocket();
        this.socket.send("accept");
    };

    /**
     * Test the connection.
     * A pong message will be returned, this message will not be broadcast to the channel.
     * This call is asynchronous, any error or success with be sent as a separate message to the listener.
     */
    this.ping = function() {
        this.checkSocket();
        this.socket.send("ping");
    };

    /**
     * Exit from the current private channel.
     * This call is asynchronous, any error or success with be sent as a separate message to the listener.
     */
    this.exit = function() {
        this.checkSocket();
        this.socket.send("exit");
    };

    /**
     * Request a private chat session with a user.
     * This call is asynchronous, any error or success with be sent as a separate message to the listener.
     */
    this.pvt = function(user) {
        this.checkSocket();
        this.socket.send("pvt: " + user);
    };

    /**
     * Disconnect from the channel.
     */
    this.disconnect = function() {
        this.setKeepAlive(false);
        if (this.socket != null) {
            this.socket.disconnect();
        }
    };

    this.checkSocket = function() {
        if (this.socket == null) {
            throw "Not connected";
        }
    };

    this.toggleKeepAlive = function() {
        setKeepAlive(!this.keepAlive);
    }

    this.setKeepAlive = function(keepAlive) {
        this.keepAlive = keepAlive;
        if (!keepAlive && this.keepAliveInterval != null) {
            clearInterval(this.keepAliveInterval);
        } else if (keepAlive && this.keepAliveInterval == null) {
            this.keepAliveInterval = setInterval(
                    function() {
                        this.ping()
                    },
                    600000);
        }
    }
}

SDK.chime = function() {
    var sound = new Audio('chime.wav');
    sound.play();
}

SDK.url = "/botlibre/rest/botlibre";
SDK.tts = function(text) {
    try {
        var url = SDK.url + '/form-speak?&text=';
        url = url + encodeURIComponent(text);

        var request = new XMLHttpRequest();
        request.onreadystatechange = function() {
            if (request.readyState != 4) return;
            if (request.status != 200) {
                console.log('Error: Speech web request failed');
                return;
            }
            var audio = new Audio(request.responseText);
            audio.mediaGroup = 'voice';
            audio.play();
        }

        request.open('GET', url, true);
        request.send();
    } catch (error) {
        console.log('Error: Speech web request failed');
    }
}
EN

回答 1

Code Review用户

发布于 2014-09-08 13:30:01

我很高兴看到我不是唯一一个发现JavaScript中的“接口”有用的人。是的,该语言不支持接口,但是如果您期望第三方代码与其集成,那么定义它们是有价值的。

在JavaScript

中的接口

在JavaScript中没有正式的工具来定义接口。尽管如此,当有人看到构造函数(如您的LiveChatListener函数)时,他们希望它是一个可实例化的可用类。我建议使用对象文字来定义您的接口,因为它实际上只是供参考的,而且实际上并不有用。

代码语言:javascript
复制
/**
 * Listener interface for a LiveChatConnection.
 * This gives asynchronous notification when a channel receives a message, or notice.
 */
var ILiveChatListener = {
    /**
     * A user message was received from the channel.
     */
    message: function(message) {},

    /**
     * An informational message was received from the channel.
     * Such as a new user joined, private request, etc.
     */ 
    info: function(message) {},

    /**
     * An error message was received from the channel.
     * This could be an access error, or message failure.
     */ 
    error: function(message) {},

    /**
     * Notification that the connection was closed.
     */
    closed: function() {},

    /**
     * The channels users changed (user joined, left, etc.)
     * This contains a comma separated values (CSV) list of the current channel users.
     * It can be passed to the SDKConnection.getUsers() API to obtain the UserConfig info for the users.
     */
    updateUsers: function(usersCSV) {},

    /**
     * The channels users changed (user joined, left, etc.)
     * This contains a HTML list of the current channel users.
     * It can be inserted into an HTML document to display the users.
     */
    updateUsersXML: function(usersXML) {}
};

我知道这是一种观点,但我确实喜欢以大写"I“作为接口名称的前缀的.NET惯例,我认为它更好地传达了这是一个接口。于是LiveChatListener变成了ILiveChatListener

另一方面,如果您打算让人们使用LiveChatListener作为某种类型的基类,那么您所创建的是一个抽象基类:

代码语言:javascript
复制
function AbstractLiveChatListener() {

}

AbstractLiveChatListener.prototype = {
    constructor: AbstractLiveChatListener,

    message: function(message) {
        throw new Error("Not Implemented");
    },

    info: function(message) {
        throw new Error("Not Implemented");
    },

    ...
};

我喜欢你只定义界面的想法,所以我会坚持这样做。

在JavaScript

中编写“类”的样式

在构造函数中为类创建公共方法和属性。虽然这不会伤害任何东西,但这确实意味着该类的每个实例对于每个公共方法都有全新的Function实例。“最佳实践”是在原型上定义公共方法,除非它们需要访问“私有”数据。

以下是我的一般风格指南:

  1. 如果所有属性和方法都是公共的,则在原型上定义方法,并给出属性默认值。这说明了哪些方法是可用的,哪些是类使用的数据。函数点(x,y) { this.x .x= x;this.x.y= y;} Point.prototype ={ x: 0,y: 0,构造函数: Point,isAbove:函数(P){返回此.y> p.y;},isBelow: isBelow:函数(P){返回此.y< p.y;};
  2. 如果一个类需要继承,那么我使用Foo.prototype.bar = function()样式来定义原型上的方法:函数Point3D(x,y,z) { Point.call(this,x,y);this.z = z;} Point3D.prototype = Object.create(Point.prototype);Point3D.prototype.contains = function(p) {返回this.x >= p.x & this.y >= p.y & this.z >= p.z;};
  3. 如果一个类需要“私有”数据,那么我在构造函数函数中定义方法和属性: function (x,y) {x,y){x=x欧元0;y=y\x= 0;var getX =函数(){返回x;},getY =函数(){返回y;},isAbove =函数(P){返回y> p.y;},isBelow =函数(P){返回y< p.y;};//定义公共接口this.getX = getX;this.getY = getY;this.isAbove = isAbove;this.isBelow = isBelow;}

由于此时不需要继承或“私有”数据,所以我推荐样式#1,因为我发现它更容易阅读,代码中的杂乱也较少。

名称空间( JavaScript

)

您有一个名为"SDK“的全局对象。将代码放在JavaScript中的“命名空间”中是个好主意,但是您编写的类似乎也是全局的。我建议使用一个立即调用的函数表达式(IIFE),它将为您提供一个函数范围来定义您只想在库内部定义的类和变量,并有选择地公开一些内容。其次,我不会将它命名为"SDK“,因为它非常通用,可能与使用此命名空间的其他人发生冲突。使名称空间以您的库命名。例如,如果您将lib称为“烘焙的Ziti",那么名称空间将是BakedZiti。(顺便提一句,现在用香肠烤的意大利烤饼听起来不错。)

代码语言:javascript
复制
(function(global) {

    function Credentials() {
        ...
    }

    var ILiveChatListener = {
        ...
    };

    function LiveChatConnection(...) {
        ...
    }

    // Public Namespace
    global.BakedZiti = {
        Credentials: Credentials,
        ILiveChatListener: ILiveChatListener,
        LiveChatConnection: LiveChatConnection,

        url: "...",

        tts: function() {
            ...
        },
        chime: function() {
            ...
        }
    };

})(this);

现在,您可以通过以下方式获得一个新的连接:

代码语言:javascript
复制
var credentials = new BakedZiti.Credentials(),
    connection = new BakedZiti.LiveChatConnection(credentials);

包在JavaScript

您提到您将有一个名为sdk.js的文件,人们可以将其包含在他们的站点上。我建议将每个类保存在自己的文件中,然后使用像鲍尔这样的包管理器将文件连接起来并缩小为类似于"sdk.js“的文件。此外,您可以在Bower上发布您的包,在开发期间通过命令行上的bower install将其提供给任何人。

示例项目结构

我在我的JavaScript库中使用了这个基本文件夹结构:

代码语言:javascript
复制
BakedZiti/
    build/
        header.js
        footer.js
    demo/
        index.html
    dist/
        BakedZiti-v1.0.0.concat.js
        BakedZiti-v1.0.0.min.js
        BakedZiti-v1.0.1.concat.js
        BakedZiti-v1.0.1.min.js
        BakedZiti-v2.0.0.concat.js
        BakedZiti-v2.0.0.min.js
    src/
        BakedZiti/
            Credentials.js
            ILiveChatListener.js
            LiveChatConnection.js
        BakedZiti.js
    tests/
        BakedZiti
            LiveChatConnectionTests.js
    bower.json
    Gruntfile.js
    package.json

demo文件夹将有一个快速而肮脏的库实现。

dist目录将打包和缩小每个主要版本的版本。

显然,src目录中的原始源文件具有无连接和无限制的形式,便于开发和维护。

如果适用的话,tests目录将有任何JavaScript单元测试。另外,您可以创建一个返回WebSocket对象的工厂方法,并且可以在测试中模拟这个对象,从而使LiveChatConnection类具有可测试性。

bower.json文件是将依赖项文件连接在一起的地方:

代码语言:javascript
复制
{
    "name": "BakedZiti",
    "description": "BakedZiti is a tasty chat library for JavaScript with no dependencies.",
    "version": "2.0.0",
    "homepage": "http://example.com/BakedZiti",
    "authors": [
        "Your Name <here@example.com>"
    ],
    "license": "MIT",
    "repository": { "type": "git", "url": "https://github.com/foo/BakedZiti.git" },
    "main": "dist/BakedZiti-v2.0.js"
}

然后可以使用Grunt构建库(Gruntfile.js)并创建可分发的表单:

代码语言:javascript
复制
module.exports = function(grunt) {
    var files = [
        "build/header.js",
        "src/BakedZiti/Credentials.js",
        "src/BakedZiti/ILiveChatListener.js",
        "src/BakedZiti/LiveChatConnection.js"
        "build/footer.js",
    ];

    // Project configuration.
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        concat: {
            options: {
                banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
            },
            main: {
                src: files.main,
                dest: 'dist/<%= pkg.name %>.concat.js'
            },
        },
        min: {
            main: {
                src: 'dist/<%= pkg.name %>.concat.js',
                dest: 'dist/<%= pkg.name %>.min.js',
            }
        }
    });

    // Load the plugin that provides the "concat" task.
    grunt.loadNpmTasks('grunt-contrib-concat');
    // Load the plugin that provides the "min" task.
    grunt.loadNpmTasks('grunt-yui-compressor');
    // Default task(s).
    grunt.registerTask('default', ['concat', 'min']);
};

然后Grunt所需的package.json

代码语言:javascript
复制
{
    "name": "BakedZiti",
    "version": "2.0.0",
    "devDependencies": {
        "grunt": "~0.4.2",
        "grunt-contrib-concat": "~0.1.2",
        "grunt-contrib-jshint": "~0.6.3",
        "grunt-contrib-nodeunit": "~0.2.0",
        "grunt-yui-compressor": "~0.3.3"
    }
}

然后是build/header.js

代码语言:javascript
复制
(function(global) {

build/footer.js

代码语言:javascript
复制
    // Public Namespace
    global.BakedZiti = {
        Credentials: Credentials,
        ILiveChatListener: ILiveChatListener,
        LiveChatConnection: LiveChatConnection,

        url: "...",

        tts: function() {
            ...
        },
        chime: function() {
            ...
        }
    };
})(this);

然后,您所需要的就是从命令行运行grunt来打包东西,您需要在本地安装Node。

这确实使您的代码组织起来,并允许您根据需要扩展库。

票数 10
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/62227

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档