Skip to content

模块化,通往未来JavaScript库之路

I must work and study hard... edited this page Jul 31, 2014 · 20 revisions

原文:http://code.tutsplus.com/articles/modules-a-future-approach-to-javascript-libraries--cms-21800

JavaScript libraries such as jQuery have been the go-to approach for writing JavaScript in the browser for nearly a decade. They’ve been a huge success and necessary intervention for what was once a browser land full of discrepancies and implementation issues. jQuery seamlessly glossed over browser bugs and quirks and made it a no brainer approach to getting things done, such as event handling, Ajax and DOM manipulation.

JS库在过去的十年里已经发展到了顶峰,比如jQuery在浏览器端的贡献,它在处理浏览器兼容方面取得了非常巨大的成功,比如时间处理,Ajax和DOM操作。

At the time, jQuery solved all our problems, we include its almighty power and get to work straight away. It was, in a way, a black box that the browser “needed” to function properly.

当时,jQuery几乎解决了我们所有的问题,我们带着这股超强能量一路披荆斩棘。

But the web has evolved, APIs are improving, standards are being implemented, the web is a very fast moving scene and I’m not sure giant libraries have a place in the future for the browser. It’s becoming a module-oriented environment.

但是web在发展,API在改进,标准在被不断的实现,web技术正在你来我往的进行快速更新,我不确定当前的巨人是否可以在将来的浏览器端找到属于自己的位置,但我知道未来生态正在被面向模块所主导。

Enter the Module

进入模块的世界

A module is an encapsulated piece of functionality that does one thing only, and that one thing very well. For example, a module may be responsible for adding classes to an element, communicating over HTTP via Ajax, and so on - there are endless possibilities.

一个模块通常是由做一件事情的很多方法组合而成。比如,一个模块负责向元素增加class样式、或者基于Ajax进行HTTP通信等等,数不胜数。

A module can come in many shapes and sizes, but the general purpose of them is to be imported into an environment and work out of the box. Generally, each module would have some basic developer documentation and installation process, as well as the environments it’s meant for (such as the browser, server).

各个模块可能大小和样子都不同,但是它们有一个共同的目标,那就是可以组合到一起保证功能的运行。简而言之,每个模块都会有自己的逻辑实现,不论在客户端还是服务器端。

These modules then become project dependencies, and the dependencies become easy to manage. The days of dropping in a huge library are slowly fading away, large libraries don’t offer as much flexibility or power. Libraries such as jQuery have recognised this too, which is fantastic - they’ve a tool online which lets you download only the things you need.

这样这些模块之间就会形成依赖,但是这些依赖却变得更加容易管理。沉浸在巨型库的日子正在慢慢褪去,因为它不够灵活。像jQuery这类的库已经意识到了这点,他们提供了一个在线工具可以让你只下载自己需要的部分。

Modern APIs are a huge booster for module inspiration, now that browser implementations have drastically improved, we can start to create small utility modules to help us do our most common tasks.

现代版的API助推了模块化灵感的出现,因为当前浏览器实现已经大幅改进,我们只要创建一个很小的工具型模块就可以帮组我们做最普通的任务了。

The module era is here, and it’s here to stay.

模块化时代已经来临,准确的说是已经到来。

Inspiration for a First Module

第一个模块的启发

One modern API that I’ve always been interested in since its inception is the classList API. Inspired from libraries such as jQuery, we’ve now got a native way to add classes to an element without a library or utility functions.

从现代版API出现的时候,我就对classList产生了很大的兴趣。受到诸如jQuery库的启发,我们现在将使用一个原生的方法去为元素增加class。

The classList API has been around a few years now, but not many developers know about it. This inspired me to go and create a module that utilised the classList API, and for those browsers less fortunate to support it, provide some form of fallback implementation.

classList已经出现有一段时间了,但是知道的人并不是很多,这就启发我去创建一个模块来封装这个API。

Before we dive into the code, let’s look at what jQuery brought to the scene for adding a class to an element: 在我们深入代码之前,我们先看下jQuery是如何给一个元素增加class的。

$(elem).addClass(‘myclass’);

When this manipulation landed natively, we ended up with the aforementioned classList API - a DOMTokenList Object (space separated values) which represents the values stored against an element’s className. The classList API provides us a few methods to interact with this DOMTokenList, all very “jQuery-like”. Here’s an example of how the classList API adds a class, which uses the classList.add() method:

当这个代码被加载的时候,我们不在关心之前讨论的classList API,取而代之的是DOMTokenList对象,它存储了对应元素的className(空格隔开取值)。classList API提供了一些和DOMTokenList对象交互的方法,这和jQuery很像。下面是一个如果通过classListadd()增加class的例子:

elem.classList.add(‘myclass’);

What can we learn from this? A library feature making its way into a language is a pretty big deal (or at least inspiring it). This is what is so great about the open web platform, we can all have some insight as to how things progress.

从这里我们能学到什么呢?一个库的特性如果能够融入到一个语言中是一个非常棒的事情。这就是开放WEB平台的伟大,对于代码的处理,我们可以拥有自己的想法。

So, what next? We know about modules, and we kind of like the classList API, but unfortunately, not all browsers support it yet. We could write a fallback, though. Sounds like a good idea for a module that uses classList when supported or automatic fallbacks if not.

那下一步呢?我们知道了模块,而且我们喜欢这个classList API,但是不幸的是,当前并不是所有的浏览器都支持它,不过我们可以写个后备方法。这听起来是个不错的想法:如果支持则使用classList,否则自动转到后备方法中。

Creating a First Module: Apollo.js

创建第一个模块:Apollo.js

Around six months ago, I built a standalone and very lightweight module for adding classes to an Element in plain JavaScript - I ended up calling it apollo.js.

大约6个月前,我写了一个非常独立而且轻量级的模块用来对一个元素增加class,而且是原生js代码,最后我将它命名为apollo.js。

The main goal for the module was to start using the brilliant classList API and break away from needing a library to do a very simple and common task. jQuery wasn’t (and still doesn’t) use the classList API, so I thought it’d be a great way to experiment with the new technology.

这个模块的主要目的就是在不依赖其他库的情况下使用classList API去做一些简答的事情。jQuery还没有使用classList API,因此我想去试验这个新技术是非常值得的。

We’ll walk through how I made it as well and the thinking behind each piece that makes up the simple module.

我当前的任务就是尽可能好的设计它、组装它。

Using classList

使用classList

As we’ve seen already, classList is a very elegant API and “jQuery developer-friendly”, the transition to it is easy. One thing I don’t like about it, however, is the fact we have to keep referring to the classList Object to use one of its methods. I aimed to remove this repetition when I wrote apollo, deciding on the following API design:

正如我们所见,classList是一个非常优雅的API,并且对jQuery开发者来说也很友好,上手会很快。但是有一点不是我喜欢的,就是必须每次调用classList 对象来使用它的方法。我决定移除掉这些重复部分,使用下面的API进行设计:

apollo.addClass(elem, ‘myclass’);

A good class manipulation module should contain hasClass, addClass, removeClass and toggleClass methods. All these methods will ride off the “apollo” namespace.

一个号的class操作模块应该包含 hasClass, addClass, removeClass 和 toggleClass方法。这些方法都是在apollo命名空间下。

Looking closely at the above “addClass” method, you can see I pass in the element as the first argument. Unlike jQuery, which is a huge custom Object which you’re bound into, this module will accept a DOM element, how it’s fed that element is up to the developer, native methods or a selector module. The second argument is a simple String value, any class name you like.

自己看上面的addClass方法,你会发现我把元素节点传递给了它作为第一个参数。和jQuery不一样,jQuery采用的是在对象上绑定这些方法,这就很容易形成大对象。而这里我要设计的模块只是接收一个DOM节点,我们要做的就是使用开发者定义的元素、很原生的方法。第二个参数是一个字符串,就是class名字。

Let’s walk through the rest of the class manipulation methods that I wanted to create to see what they look like:

让我们过下剩下的class操作方法:

apollo.hasClass(elem, ‘myclass’); apollo.addClass(elem, ‘myclass’); apollo.removeClass(elem, ‘myclass’); apollo.toggleClass(elem, ‘myclass’);

So where do we begin? First, we need an Object to add our methods to, and some function closure to house any internal workings/variables/methods. Using an immediate-invoked function expression (IIFE), I wrap an Object named apollo (and some methods containing classList abstractions) to create our module definition.

那我们从那里开始呢?首先我们需要一个添加我们方法的对象,一些掌控内部运行的变量或者方法的函数

(function () {

var apollo = {};

apollo.hasClass = function (elem, className) {
    return elem.classList.contains(className);
};

apollo.addClass = function (elem, className) {
    elem.classList.add(className);
};

apollo.removeClass = function (elem, className) {
    elem.classList.remove(className);
};

apollo.toggleClass = function (elem, className) {
    elem.classList.toggle(className);
};

window.apollo = apollo;

})();

apollo.addClass(document.body, 'test'); Now we’re got classList working, we can think about legacy browser support. The aim for the apollomodule is to provide a tiny and standalone consistent API implementation for class manipulation, regardless of the browser. This is where simple feature detection comes into play.

The easy way to test feature presence for classList is this:

if ('classList' in document.documentElement) { // you’ve got support } We’re using the in operator which evaluates the presence of classList to Boolean. The next step would be to conditionally provide the API to classList supporting users only:

(function () {

var apollo = {};
var hasClass, addClass, removeClass, toggleClass;

if ('classList' in document.documentElement) {
    hasClass = function () {
        return elem.classList.contains(className);
    }
    addClass = function (elem, className) {
        elem.classList.add(className);
    }
    removeClass = function (elem, className) {
        elem.classList.remove(className);
    }
    toggleClass = function (elem, className) {
        elem.classList.toggle(className);
    }
}

apollo.hasClass = hasClass;
apollo.addClass = addClass;
apollo.removeClass = removeClass;
apollo.toggleClass = toggleClass;

window.apollo = apollo;

})(); Legacy support can be done in a few ways, reading the className String and looping through all the names, replace them, add them and so forth. jQuery uses a lot of code for this, utilising long loops and complex structure, I don’t want to completely bloat out this fresh and lightweight module, so set out to use a Regular Expression matching and replaces to achieve the exact same effect with next to no code at all.

Here’s the cleanest implementation I could come up with:

function hasClass (elem, className) { return new RegExp('(^|\s)' + className + '(\s|$)').test(elem.className); }

function addClass (elem, className) { if (!hasClass(elem, className)) { elem.className += (elem.className ? ' ' : '') + className; } }

function removeClass (elem, className) { if (hasClass(elem, className)) { elem.className = elem.className.replace(new RegExp('(^|\s)' + className + '(\s|$)', 'g'), ''); } }

function toggleClass (elem, className) { (hasClass(elem, className) ? removeClass : addClass)(elem, className); } Let’s integrate them into the module, adding the else part for non-supporting browsers:

(function () {

var apollo = {};
var hasClass, addClass, removeClass, toggleClass;

if ('classList' in document.documentElement) {
    hasClass = function () {
        return elem.classList.contains(className);
    };
    addClass = function (elem, className) {
        elem.classList.add(className);
    };
    removeClass = function (elem, className) {
        elem.classList.remove(className);
    };
    toggleClass = function (elem, className) {
        elem.classList.toggle(className);
    };
} else {
    hasClass = function (elem, className) {
        return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function (elem, className) {
        if (!hasClass(elem, className)) {
            elem.className += (elem.className ? ' ' : '') + className;
        }
    };
    removeClass = function (elem, className) {
        if (hasClass(elem, className)) {
            elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
        }
    };
    toggleClass = function (elem, className) {
        (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
}

apollo.hasClass = hasClass;
apollo.addClass = addClass;
apollo.removeClass = removeClass;
apollo.toggleClass = toggleClass;

window.apollo = apollo;

})(); A working jsFiddle of what we’ve done so far.

Let’s leave it there, the concept has been delivered. The apollo module has a few more features such as adding multiple classes at once, you can check that here, if interested.

So, what have we done? Built an encapsulated piece of functionality dedicated to doing one thing, and one thing well. The module is very simple to read through and understand, and changes can be easily made and validated alongside unit tests. We also have the ability to pull in apollo for projects where we don’t need jQuery and its huge offering, and the tiny apollo module will suffice.

Dependency Management: AMD and CommonJS

The concept of modules isn’t new, we use them all the time. You’re probably aware that JavaScript isn’t just about the browser anymore, it’s running on servers and even TV’s.

What patterns can we adopt when creating and using these new modules? And where can we use them? There are two concepts called “AMD” and “CommonJS”, let’s explore them below.

AMD

Asynchronous Module Definition (usually referred to as AMD) is a JavaScript API for defining modules to be asynchronously loaded, these typically run in the browser as synchronous loading incurs performance costs as well as usability, debugging, and cross-domain access problems. AMD can aid development, keeping JavaScript modules encapsulated in many different files.

AMD uses a function called define, which defines a module itself and any export Objects. Using AMD, we can also refer to any dependencies to import other modules. A quick example taken from the AMD GitHub project:

define([‘alpha’], function (alpha) { return { verb: function () { return alpha.verb() + 2; } }; }); We might do something like this for apollo if we were to use an AMD approach:

define([‘apollo’], function (alpha) { var apollo = {}; var hasClass, addClass, removeClass, toggleClass;

if ('classList' in document.documentElement) {
    hasClass = function () {
        return elem.classList.contains(className);
    };
    addClass = function (elem, className) {
        elem.classList.add(className);
    };
    removeClass = function (elem, className) {
        elem.classList.remove(className);
    };
    toggleClass = function (elem, className) {
        elem.classList.toggle(className);
    };
} else {
    hasClass = function (elem, className) {
        return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function (elem, className) {
        if (!hasClass(elem, className)) {
            elem.className += (elem.className ? ' ' : '') + className;
        }
    };
    removeClass = function (elem, className) {
        if (hasClass(elem, className)) {
            elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
        }
    };
    toggleClass = function (elem, className) {
        (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
}

apollo.hasClass = hasClass;
apollo.addClass = addClass;
apollo.removeClass = removeClass;
apollo.toggleClass = toggleClass;

window.apollo = apollo;

}); CommonJS

Node.js has been rising for the last few years, as well as dependency management tools and patterns. Node.js utilises something called CommonJS, which uses an “exports” Object to define a module’s contents. A really basic CommonJS implementation might look like this (the idea of “exporting” something to be used elsewhere):

// someModule.js exports.someModule = function () { return "foo"; }; The above code would sit in it’s own file, I’ve named this one someModule.js. To import it elsewhere and be able to use it, CommonJS specifies that we need to use a function called “require” to fetch individual dependencies:

// do something with myModule var myModule = require(‘someModule’); If you’ve used Grunt/Gulp as well, you’re used to seeing this pattern.

To use this pattern with apollo, we would do the following and reference the exports Object instead of the window (see last line exports.apollo = apollo):

(function () {

var apollo = {};
var hasClass, addClass, removeClass, toggleClass;

if ('classList' in document.documentElement) {
    hasClass = function () {
        return elem.classList.contains(className);
    };
    addClass = function (elem, className) {
        elem.classList.add(className);
    };
    removeClass = function (elem, className) {
        elem.classList.remove(className);
    };
    toggleClass = function (elem, className) {
        elem.classList.toggle(className);
    };
} else {
    hasClass = function (elem, className) {
        return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function (elem, className) {
        if (!hasClass(elem, className)) {
            elem.className += (elem.className ? ' ' : '') + className;
        }
    };
    removeClass = function (elem, className) {
        if (hasClass(elem, className)) {
            elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
        }
    };
    toggleClass = function (elem, className) {
        (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
}

apollo.hasClass = hasClass;
apollo.addClass = addClass;
apollo.removeClass = removeClass;
apollo.toggleClass = toggleClass;

exports.apollo = apollo;

})(); Universal Module Definition (UMD)

AMD and CommonJS are fantastic approaches, but what if we were to create a module that we wanted to work across all environments: AMD, CommonJS and the browser?

Initially, we did some if and else trickery to pass a function to each definition type based on what was available, we’d sniff out for AMD or CommonJS support and use it if it was there. This idea was then adapted and a universal solution began, dubbed “UMD”. It packages this if/else trickery for us and we just pass in a single function as reference to either module type that was supported, here’s one example from the project’s repository:

(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['b'], factory); } else { // Browser globals root.amdWeb = factory(root.b); } }(this, function (b) { //use b in some fashion.

// Just return a value to define the module export.
// This example returns an object, but the module
// can return a function as the exported value.
return {};

})); Whoa! Lots happening here. We are passing in a function as the second argument to the IIFE block, which under a local variable name factory is dynamically assigned as AMD or globally to the browser. Yep, this doesn’t support CommonJS. We can, however, add that support (removing comments this time too):

(function (root, factory) { if (typeof define === 'function' && define.amd) { define(['b'], factory); } else if (typeof exports === 'object') { module.exports = factory; } else { root.amdWeb = factory(root.b); } }(this, function (b) { return {}; })); The magic line here is module.exports = factory which assigns our factory to CommonJS.

Let’s wrap apollo in this UMD setup so it can be used in CommonJS environments, AMD and the browser! I’ll include the full apollo script, from the latest version on GitHub, so things will look a little more complex than what I covered above (some new features have been added but weren’t purposely included in the above examples):

/*! apollo.js v1.7.0 | (c) 2014 @toddmotto | https://github.com/toddmotto/apollo */ (function (root, factory) { if (typeof define === 'function' && define.amd) { define(factory); } else if (typeof exports === 'object') { module.exports = factory; } else { root.apollo = factory(); } })(this, function () {

'use strict';

var apollo = {};

var hasClass, addClass, removeClass, toggleClass;

var forEach = function (items, fn) { if (Object.prototype.toString.call(items) !== '[object Array]') { items = items.split(' '); } for (var i = 0; i < items.length; i++) { fn(items[i], i); } };

if ('classList' in document.documentElement) { hasClass = function (elem, className) { return elem.classList.contains(className); }; addClass = function (elem, className) { elem.classList.add(className); }; removeClass = function (elem, className) { elem.classList.remove(className); }; toggleClass = function (elem, className) { elem.classList.toggle(className); }; } else { hasClass = function (elem, className) { return new RegExp('(^|\s)' + className + '(\s|$)').test(elem.className); }; addClass = function (elem, className) { if (!hasClass(elem, className)) { elem.className += (elem.className ? ' ' : '') + className; } }; removeClass = function (elem, className) { if (hasClass(elem, className)) { elem.className = elem.className.replace(new RegExp('(^|\s)' + className + '(\s|$)', 'g'), ''); } }; toggleClass = function (elem, className) { (hasClass(elem, className) ? removeClass : addClass)(elem, className); }; }

apollo.hasClass = function (elem, className) { return hasClass(elem, className); };

apollo.addClass = function (elem, classes) { forEach(classes, function (className) { addClass(elem, className); }); };

apollo.removeClass = function (elem, classes) { forEach(classes, function (className) { removeClass(elem, className); }); };

apollo.toggleClass = function (elem, classes) { forEach(classes, function (className) { toggleClass(elem, className); }); };

return apollo;

}); We’ve created, a packaged our module to work across many environments, this gives us huge flexibility when bringing new dependencies into our work - something a JavaScript library can’t provide us without breaking it into little functional pieces to begin with.

Testing

Typically, our modules are accompanied by unit tests, small bite size tests that make it easy for other developers to join your project and submit pull requests for feature enhancements, it’s a lot less daunting as well than a huge library and working out their build system! Small modules are often rapidly updated whereas larger libraries can take time to implement new features and fix bugs.

Wrapping Up

It was great to create our own module and know we’re supporting many developers across many development environments. This makes developing more maintainable, fun and we understand the tools we’re using a lot better. Modules are accompanied by documentation that we can get up to speed with fairly quickly and integrate into our work. If a module doesn’t suit, we could either find another one or write our own - something we couldn’t do as easily with a large library as a single dependency, we don’t want to tie ourselves into a single solution.

Bonus: ES6 Modules

A nice note to finish on, wasn’t it great to see how JavaScript libraries had influenced native languages with things like class manipulation?A Well, with ES6 (the next generation of the JavaScript language), we’ve struck gold! We have native imports and exports!

Check it out, exporting a module:

/// myModule.js function myModule () { // module content } export myModule; And importing:

import {myModule} from ‘myModule’; You can read more on ES6 and the modules specification here.

Clone this wiki locally