为Diigo创建Chrome扩展程序,第2部分

在第1部分中 ,我们介绍了一些新概念,解释了我们如何构建扩展并演示了Diigo API的用法。 在这一部分中,我们将创建大多数帮助器函数并处理错误处理。

错误处理

当API返回响应时,由我们决定是否涵盖所有边缘情况并充分使用它。 每次都不能依靠成功的请求-我们不仅要考虑准备状态,还要考虑潜在的失败。

为了稍微清理代码并使background.js更简洁,我将Base64对象压缩为一个缩小的字符串。 现在的background.js文件如下所示 。 如果要遵循代码,则可以从该代码开始。

xml.readyState === 4部分检查请求是否完成。 完成后,我们可以自由检查状态码。 只有200表示“成功”,其他所有则表示出了问题。 使用可能的响应列表 ,我们将修改代码,以产生对于发生的错误的人类可读的描述。

var possibleErrors = {
    400: 'Bad Request: Some request parameters are invalid or the API rate limit is exceeded.',
    401: 'Not Authorized: Authentication credentials are missing or invalid.',
    403: 'Forbidden: The request has been refused because of the lack of proper permission.',
    404: 'Not Found: Either you\'re requesting an invalid URI or the resource in question doesn\'t exist (e.g. no such user).',
    500: 'Internal Server Error: Something is broken.',
    502: 'Bad Gateway: Diigo is down or being upgraded.',
    503: 'Service Unavailable: The Diigo servers are too busy to server your request. Please try again later.',
    other: 'Unknown error. Something went wrong.'
};

xml.onreadystatechange = function() {
    if (xml.readyState === 4) {
        if (xml.status === 200) {
            console.log(xml.responseText);
        } else {
            if (possibleErrors

!== undefined) {
console.error(xml.status + ' ' + possibleErrors

);
} else {
console.error(possibleErrors.other);
}
}
}
};

In the above code, we define a set of error messages and bind each message to a key corresponding to the status code. We then check if the code matches any of the predefined ones and log it in the console. If the request is successful, we output the responseText.

The above error handling is very basic, and not very end-user friendly. Options to improve it are: an alert box when an error occurs, graying out the extension's icon, deactivating the extension, and more. I'll leave that up to you.

We can also wrap the whole shebang into a function, just so it's neatly encapsulated and the global namespace isn't polluted:

var doRequest = function() {

    var xml = new XMLHttpRequest();
    xml.open('GET', url);
    xml.setRequestHeader('Authorization', auth);
    xml.send();

    xml.onreadystatechange = function() {
        if (xml.readyState === 4) {
            if (xml.status === 200) {
                console.log(xml.responseText);
            } else {
                if (possibleErrors

!== undefined) {
console.error(xml.status + ' ' + possibleErrors

);
} else {
console.error(possibleErrors.other);
}
}
}
};
};

doRequest();

Popup

Now that we have our responseText, we can process it. We first need to turn it into a proper array, because it's useless to us in string form. Replace console.log(xml.responseText); with:

var response = JSON.parse(xml.responseText);
console.log(response);

The above should produce a JavaScript Array of JavaScript Objects when you look at the generated background page's JavaScript console.

I've made a test account called "testerguy" on Diigo, with some sample bookmarks. You should probably make your own to experiment with, since there's no telling what might be going on with this one by the time you're reading this article.

As mentioned in part 1 , the structure of the bookmark folder will be: all "bbs-root" tagged bookmarks in the root of the folder, and all tags in subfolders in the "tags" folder. This is so the user can prioritize certain bookmarks by tagging them with "bbs-root" and make sure they appear outside their respective folders for fastest access.

In order to properly make the Bookmark Bar folder, we need to find out all the unique tags, create the root folder, create the subfolder "tags" and create subfolders for each tag we know of, in that order. To make testing for this easier, we'll add a popup to our extension with a Refresh button which repeats the XHR request. Update the manifest.json browser_action block like so:

"browser_action": {
        "default_icon": {
            "19": "icons/19.png",
            "38": "icons/38.png"
        },
        "default_title": "Diigo BBS",
        "default_popup": "popup/popup.html"
    },

and create a folder called popup in the root of your project. Create three more files in that folder: popup.html , popup.js and popup.css with the following content:





    BBS popup
    
    
    




// popup.js
var bg = chrome.extension.getBackgroundPage();

document.addEventListener('DOMContentLoaded', function () {
    document.getElementById('refreshButton').addEventListener('click', function() {
        bg.doRequest();
    });
});
/* popup.css */
#refreshButton {
    margin: 10px;
}

The JS code here does the following: first we fetch the window object of the background.js script's autogenerated page. Popup scripts have direct access to background page code, as opposed to content scripts which have to pass messages. Then, we bind a click handler to the Refresh button's click event which calls our doRequest method from background.js .

If you reload the extension now and keep the generated background page open, you should see repeated outputs of fetched bookmarks as you click the refresh button.

We can now continue coding in background.js .

Processing the response array

We find all tags by iterating through all the fetched bookmarks, storing them in an array, and then removing duplicates. While we're iterating, we can check for all bookmarks containing the tag "bbs-root" and make a note of them in a separate variable. Let's add a process function:

var process = function(response) {
    var iLength = response.length;
    if (iLength) {
        console.info(iLength + " bookmarks were found.");
    } else {
        console.info("Response is empty - there are no bookmarks?");
    }
};

Also, in the function doRequest , let's replace

var response = JSON.parse(xml.responseText);
console.log(response);

with process(JSON.parse(xml.responseText)); .

Reloading the extension will print out the number of found bookmarks for the selected user. Let's include a helper function to assist us with filtering out the duplicate tags from the tags array. This function extends the native JavaScript Array, so you can call it as if it had been built in all along. Put it under the Base64 part, near the top of the file:

/**
 * Removes duplicate elements from the array
 */
Array.prototype.unique = function () {
    var result = [];
    var len = this.length;
    while (len--) {
        if (result.indexOf(this[len]) == -1) {
            result.push(this[len]);
        }
    }
    this.length = 0;
    len = result.length;
    while (len--) {
        this.push(result[len]);
    }
};

Now, let's build out the process function.

var process = function(response) {
    var iLength = response.length;
    var allTags = [];
    var rootBookmarks = [];
    if (iLength) {
        console.info(iLength + " bookmarks were found.");
        var i = iLength;
        while (i--) {
            var item = response[i];
            if (item.tags !== undefined && item.tags != "") {
                var tags = item.tags.split(',');
                if (tags.indexOf('bbs-root') > -1) {
                    rootBookmarks.push(item);
                }
                allTags = allTags.concat(tags);
            }
        }
        allTags.unique();
        allTags.sort();
        console.log(allTags);
    } else {
        console.info("Response is empty - there are no bookmarks?");
    }
};

We iterate through all the bookmarks, if any are found, and for each one we turn their "tags" property into an array. This array then gets merged with the allTags array on which we call unique() to remove duplicates, and sorted alphabetically. In the process, we also watch out for bbs-root tagged bookmarks and copy their references to the rootBookmarks array.

We are now ready to manipulate the Bookmarks Bar.

Bookmarks Bar

First, we need to check if "Diigo #BBS" exists as a folder in the bookmarks bar. If not, we create it. Put the following code immediately under allTags.sort(); :

var folderName = 'Diigo #BBS';
        chrome.bookmarks.getChildren("1", function(children) {
            var numChildren = children.length;
            var folderId;
            while (numChildren--) {
                if (children[numChildren].title == folderName) {
                    folderId = children[numChildren].id;
                    break;
                }
            }
            if (folderId === undefined) {
                chrome.bookmarks.create({
                    parentId: "1",
                    title: folderName
                }, function(folder) {
                    folderId = folder.id;
                    console.log(folderName + " not found and has been created at ID " + folder.id);
                });
            }

        });

We first get the children of the node with the ID 1, which is the bookmarks bar (you can see that by using getTree ). We then iterate through them, and compare their titles to the desired name of our folder. If the folder is found, we save its ID and exit the loop. If it's never found, we create it and save the ID.

Now we need to find out if our folder contains the folder "Tags". Once we do that, we'll need to find out if our "Tags" folder contains a subfolder matching the name of every tag we found. Noticing a pattern here? Looks like we'll need a common function for checking if a bookmark folder contains another folder. We might as well make another helper method to check for actual bookmarks, too. Let's add the following functions to our background.js file (above the process function, for example):

chrome.bookmarks.getFirstChildByTitle = function (id, title, callback) {
    chrome.bookmarks.getChildren(id, function (children) {
        var iLength = children.length;
        while (iLength--) {
            var item = children[iLength];
            if (item.title == title) {
                return callback(item);
            }
        }
        return callback(false);
    });
};

chrome.bookmarks.getFirstChildByUrl = function (id, url, callback) {
    chrome.bookmarks.getChildren(id, function (children) {
        var iLength = children.length;
        while (iLength--) {
            var item = children[iLength];
            if (item.hasOwnProperty('url') && item.url == url) {
                return callback(item);
            }
        }
        return callback(false);
    });
};

These functions are almost identical, though each compares its own property to the provided value. We'll use one for folders, and the other for bookmarks. These methods are asynchronous just like the rest of the chrome.bookmarks namespace, so we'll need to provide callbacks whenever we use them.

You can also merge them into one single method and use a third parameter that tells the method which property we're looking for (title or url), thus respecting the DRY principle a bit more. I'll leave that up to you for now, and come back to it in a followup article that will be focusing on optimizations.

Let's rewrite our process method to use this now.

        chrome.bookmarks.getFirstChildByTitle("1", folderName, function(value) {
            if (value === false) {
                chrome.bookmarks.create({
                    parentId: "1",
                    title: folderName
                }, function (folder) {
                    console.log(folderName + " not found and has been created at ID " + folder.id);
                });
            }
        });

Much more concise, isn't it? When we consider further steps, it's clear we'll need to differentiate between a list of existing tags, and the list of tags we've freshly fetched from the server. For this purpose, we'll add two new helper methods to the native JavaScript Array object: intersect and diff . Let's put them at the top of the file, right where Array.unique() is, and while we're at it, let's move the getFirstChildByTitle and getFirstChildByUrl methods up there as well.

/**
 * Returns an array - the difference between the two provided arrays.
 * If the mirror parameter is undefined or true, returns only left-to-right difference.
 * Otherwise, returns a merge of left-to-right and right-to-left difference.
 * @param array {Array}
 * @param mirror
 * @returns {Array}
 */
Array.prototype.diff = function (array, mirror) {

    var current = this;
    mirror = (mirror === undefined);

    var a = current.filter(function (n) {
        return array.indexOf(n) == -1
    });
    if (mirror) {
        return a.concat(array.filter(function (n) {
            return current.indexOf(n) == -1
        }));
    }
    return a;
};

/**
 * Returns an array of common elements from both arrays
 * @param array
 * @returns {Array}
 */
Array.prototype.intersect = function (array) {
    return this.filter(function (n) {
        return array.indexOf(n) != -1
    });
};

Finally, let's add a helper method for console logging in the same place at the top of the background.js file:

const CONSOLE_LOGGING = true;
function clog(val) {
    if (CONSOLE_LOGGING) {
        console.log(val);
    }
}

You can now replace all your console.log() calls in the code with clog . When you need to turn the logging off, simply switch the CONSOLE_LOGGING constant to false and all output will stop. This is great when moving from development to production – it introduces a very small overhead, but cuts down on preparation time in that you don't need to manually hunt down and comment or remove all your console outputs.

Conclusion of Part 2

In this part, we built several helper functions essential for further work, and added some basic error handling logic. In the next installment of this series, we build the body of the extension. Stay tuned!

From: https://www.sitepoint.com/creating-chrome-extension-diigo-part-2/

你可能感兴趣的:(为Diigo创建Chrome扩展程序,第2部分)