Prototype JavaScript Framework'un Notları

Görünüm: Tam | Özet

When we officially released 1.6.1 last week, we also published new documentation, the first official docs generated with PDoc.

Tobie, ear to the ground, brought to my attention what many of you were saying (on the blog and on Twitter): the new docs were harder to navigate and, therefore, harder to browse. Though I had eventual plans to re-do the navigation, the instant feedback showed it was a more critical issue than I’d guessed. So I spent the last week making some changes to the template we use to generate the docs.

You can see the results at api.prototypejs.org. The biggest change is obvious: a fixed, always-visible sidebar that makes it easier to move from section to section. Typing in the search box replaces the hierarchical navigation with a list of matching results. Clearing the search box (use the ESC key as a shortcut) switches back to the ordinary navigation. The sidebar will preserve state from page to page — it’ll remember your search term and the scrollbar position.

The docs aren’t perfect yet, but they’re good enough to use. I’ve tested them on Firefox 3.5, Safari 4.0, and IE 7–8. If there are glitches in these browsers or others, please open issues on the GitHub project. (If you, as a JavaScript developer, are still using IE 6; I’d like to take you out for a beer and ask you why.)

We intend for this to be default template included with PDoc, albeit without the Prototype branding. And now that we’ve accomplished the most pressing goal — getting PDoc to generate comprehensive and canonical docs for Prototype — we can focus on the big ideas we’ve got for the next version of our inline documentation tool.

In addition to releasing Prototype 1.6.1, I’m pleased to announce that Andrew Dupont and Tobie Langel now officially head up the Prototype Core Team. They’ll be in charge of maintaining Prototype, deciding what makes the cut for new releases, and handling day-to-day operations.

This change in responsibility will let me focus on some infrastructural projects we need for the next-generation version of Prototype. It’ll also help us fix bugs faster and release new versions more frequently. And I’ll remain on the Core Team, contributing code and offering input on API design.

Andrew and Tobie have proved themselves to be worthy keepers of the code, so I’m certain Prototype is in good hands. Congratulations, guys, and thanks for all your hard work!

We’re pleased to announce the release of Prototype 1.6.1 today. This version features improved performance, an element metadata storage system, new mouse events, and compatibility with the latest browsers. It’s also the first release of Prototype built with Sprockets, our JavaScript packaging tool, and PDoc, our inline documentation tool.

Highlights

  • Full compatibility with new browsers. This version of Prototype fully supports versions 1.0 and higher of Google Chrome, and Internet Explorer 8 in both compatibility mode and super-standards mode.

  • Element metadata storage. Easily associate JavaScript key/value pairs with a DOM element. See the blog post that started it off.

  • New mouse events. Internet Explorer’s proprietary “mouseenter” and “mouseleave” events are now available in all browsers.

  • Improved performance and housekeeping. The frequently used Function#bind, String#escapeHTML, and Element#down methods are faster, and Prototype is better at cleaning up after itself.

  • Built with Sprockets. You can now include the Prototype source code repository in your application and use Sprockets for dependency management and distribution.

  • Inline documentation with PDoc. Our API documentation is now stored in the source code with PDoc so it’s easy to send patches or view documentation for a specific version.

See the RC2 blog post, RC3 blog post, and CHANGELOG for more details.

Download, report bugs, and get help

We hope you enjoy the new version!

UPDATE

We’re aware of the usability issues with the current PDoc-generated API documentation. We’re working hard to fix those.

In the meantime, we’ve reverted our changes and you can again access the old Prototype documentation. For those of you courageous enough, the new documentation is still available.

Sorry for the inconvenience.

Today we’re announcing Release Candidate 3 of Prototype 1.6.1. Among the highlights of this release are official Chrome support, improved IE8 compatibility, faster generation of API documentation with PDoc, and lots of bug fixes.

Chrome support

Since Google Chrome is a close sibling of Safari, Prototype has had excellent Chrome compatibility ever since the browser was first released. Now we’re making it official: Prototype supports Chrome 1.0 and greater.

If you have Chrome installed on your system (Windows only for now, even though early alphas exist for Mac), invoking rake test will run the unit tests in all locally-installed browsers, including Chrome. To run the unit tests in Chrome alone, try rake test BROWSERS=chrome.

Generate your own docs with PDoc

It’s been a long, strange trip for PDoc, the inline-doc tool that will soon be for Prototype and script.aculo.us what RDoc is for Rails. It started as Tobie’s brainchild over a year ago, but key contributions from James Coglan and Samuel Lebeau have helped to carry it across the finish line.

PDoc was a part of RC2, but has since been updated to make doc generation much, much faster. On my machine, a process that used to take 20 minutes now takes only 60 seconds. Furthermore, we’ve solved a couple of minor issues that made it hard to build the docs on Windows.

Ever since Prototype 1.5, we’ve kept our documentation in Mephisto, the same engine that powers the rest of the site (and this blog). It’s served us well, but it meant that updating the docs became a chore that could only be started once we’d released a particular version. PDoc will make it far easier to maintain our documentation — and far easier to keep archival copies of the docs for older versions of Prototype.

Upon final release of 1.6.1, we’ll put the generated docs on this site, just like Rails hosts its most recent stable documentation. Until then, you can generate your own local docs by checking out the full source and running rake doc from the command line.

Other improvements

There have also been a number of bugs fixed since RC2 — including a heinous bug relating to Event#observe — and a number of key optimizations. We’ve further improved IE8 compatibility, solving some edge-case issues that popped up since RC2. Credit goes to Juriy (kangax), our newest team member, for working tirelessly these last few months to make 1.6.1 faster and less reliant on browser sniffs.

Download, report bugs, and get help

Thanks to the many contributors who made this release possible!

Today we tagged the first public release candidate of Prototype 1.6.1. (What happened to RC1? Long story.) While there are more minor fixes we’d like to get into this release, we decided an interim release was necessary because of the final release of Internet Explorer 8 last week.

This is the first public release of Prototype that is fully compatible — and fully optimized for — Internet Explorer 8’s “super-standards” mode. In particular, Prototype now takes advantage of IE8’s support of the Selectors API and its ability to extend the prototypes of DOM elements.

What’s new?

  • Full compatibility with Internet Explorer 8. Juriy has spearheaded the effort to replace most of our IE “sniffs” into outright capability checks — making it far easier to support IE8 in both “super-standards” mode and compatibility mode.
  • Element storage, a feature announced previously. Safely associate complex metadata with individual elements.
  • mouseenter and mouseleave events —simulating the IE-proprietary events that tend to be far more useful than mouseover and mouseout.
  • An Element#clone method for cloning DOM nodes in a way that lets you perform “cleanup” on the new copies.

What’s been improved?

  • Better housekeeping on event handlers in order to prevent memory leaks.
  • Better performance in Function#bind, Element#down, and a number of other often-used methods.
  • A number of bug fixes.

Consult the CHANGELOG for more details.

In addition to the code itself, the 1.6.1 release features Prototype’s embrace of two other excellent projects we’ve been working on: Sprockets (JavaScript concatenation) and PDoc (inline documentation). Sprockets is now used to “build” Prototype into a single file for distribution. PDoc will be the way we document the framework from now on. The official API docs aren’t quite ready yet, but they’ll be ready for the final release of 1.6.1.

Download, Report Bugs, and Get Help

Thanks to the many contributors who made this release possible!

Over at SvN, Sam announced the 1.0 release of Sprockets, the new dependency management and concatenation tool that makes it easy to modularize your JavaScript. Sprockets is Prototype’s new build system, but it’s also been extracted into a Ruby library so you can use it anywhere you write JavaScript.

There are many great ways to use Sprockets in your own projects. You can use it the way Prototype does — split up your JavaScript into small, maintainable files, then create “meta-files” that include the smaller files in a logical order. Prototype had previously been doing this with plain ERB; now we integrate Sprockets as a Git submodule and use it to build our distributable file.

Sprockets can also be used to write JavaScript “plugins”: bundles of files that can easily be integrated into existing code. With Sprockets, you can formally declare that foo.js depends on thud.js; when your files are concatenated into one output file, thud.js will be included first.

In addition, Sprockets lets JavaScript files provide other assets — HTML, CSS, images, and the like. At build time, those assets will be copied into the document root of your server (in a way that preserves the sub-structure of directories within). This allows the plugin to refer to those assets via absolute URLs, instead of having to ask you where they’re located.

A few facts are worth special mention.

  • Sprockets does not require Prototype. Sprockets directives can be inserted into any arbitrary JavaScript file. You can use Sprockets in your build system no matter which JavaScript framework you prefer.
  • Sprockets does not require Rails. Sam has also written an excellent sprockets-rails plugin, one which deftly applies the conventions of Rails plugins to JavaScript. But he has also written a generic CGI wrapper around Sprockets that is framework-agnostic. Or, instead, you can integrate Sprockets into your build cycle without bothering your server stack with the details. If you use Rake, you can do this with Ruby, as Prototype does; otherwise you can use the sprocketize binary from the command line.
  • Sprockets-enabled JavaScript files can work just fine without Sprockets. If your plugin has its own “build stage,” then the distributable JavaScript will include no Sprockets directives. On the other hand, if your plugin is small enough not to require this overhead, your distributable can be a short JS file that declares its external dependencies at the top. Because require directives are an extension of comment syntax, they won’t confuse a JS interpreter.

In short, we’re excited about what Sprockets means for the Prototype ecosystem. If you maintain a Prototype add-on library, the prototype-core mailing list would love to help you make it Sprockets-aware.

Now is the time on Sprockets when we dance.

Man, it's quiet around here. Interested in doing some pimpin'?

WAIT! COME BACK.

Code pimping. You know? The thing I'd discussed before? Forgive my earlier informality. I see now how my words could have been confusing.

The very first edition of Pimp My Code is special because the code we’ll be looking at will be included in Prototype 1.6.1. (It's a bit like if we were to Pimp [someone's] Ride™, then decide to keep the car for ourselves.) So this is more than just an academic exercise for us — the “pimped” result is now part of the Prototype source code.

The Original

The code in question, from Sébastien Grosjean (a.k.a. ZenCocoon), implements element “storage” — attaching of arbitrary data to DOM nodes in a safe and leak-free manner. Other frameworks have had this for a while; jQuery’s $.fn.data, for instance, is used heavily by jQuery plugin authors to great effect. But Seb’s is based on the similar Mootools API, which I’ve admired since it debuted in Mootools 1.2.

Here’s Seb’s code. It’s a long code block, since he’s been thoughtful enough to comment the hell out of it:

The idea is this: instead of storing arbitrary objects as properties on DOM nodes, create one custom property on the DOM node: an index to a global hashtable. The value of that key in the table will itself be a collection of custom key/value pairs. On top of avoiding nasty IE memory leaks (circular references between DOM objects and JS objects), this has the benefit of encapsulating all of an element’s custom metadata into one place.

Let’s make a first pass at this, line-by-line.

The Critique

Object.extend(Prototype, {UID: 1});

Already we’ve gotten to something I’d change. Seb is using the Prototype namespace correctly here, in that he’s storing something that’s of concern only to the framework and should feel “private.” But my own preference is to move this property into the Element.Storage namespace. I am fickle and my mind is hard to read.

Element.Storage = {   get: function(uid) {     return (this[uid] || (this[uid] = {}));   },    init: function(item) {     return (item.uid || (item.uid = Prototype.UID++));   } }

OK, another change jumps out at me. The Element.Storage.init method gets called in both Element#store and Element#retrieve; it handles the case where an element doesn’t have any existing metadata. It creates our custom property on the node and increments the counter.

In other words, store and retrieve are the only two places where this method is needed, so I balk at making it public. My first instinct was to make it a private method inside a closure:

(function() {   function _init(item) {     return (item.uid || (item.uid = Prototype.UID++));   }    // ... rest of storage code })();

I started down this path but quickly stopped. Instead, we’re going to refactor this part so that the init case is handled without the need for a separate method. Let’s move on for now.

Element.Methods.retrieve = function(element, property, dflt) {   if (!(element = $(element))) return;   if (element.uid == undefined) Element.Storage.init(element);   var storage = Element.Storage.get(element.uid);   var prop = storage[property];   if (dflt != undefined && prop == undefined)     prop = storage[property] = dflt;   return prop; };

A few things to mention here.

  • Variable naming is important. The ideal name for the third parameter of this function would be default, but that’s off-limits; default is a reserved word in JavaScript. Seb’s opted for dflt here, which is clear enough. I’d change it to defaultValue because I like vowels.

    As an aside: my first instinct was to remove the defaultValue thing altogether, because I was surprised by the way it behaved. I didn’t find it very intuitive to give Element#retrieve the capability to store properties as well. So I took it out.

    I changed my mind several minutes later, when I wrote some code that leveraged element metadata. I had assumed I wouldn’t need the “store a default value” feature often enough to warrant the surprising behavior, but I was spectacularly wrong. I put it back in. Consider that a lesson on how your API design needs to be grounded in use cases.

  • The idiom in the first line is used throughout Prototype and script.aculo.us (and, in fact, should be used more consistently). It runs the argument through $, but also checks the return value to ensure we got back a DOM node and not null (as would happen if you passed a non-existent ID). An empty return is equivalent to return undefined, which (IMO) is an acceptable failure case. Bonus points, Seb!

  • The custom property Seb’s been using is called uid. I’m going to change this to something that’s both (a) clearly private; (b) less likely to cause a naming collision. In keeping with existing Prototype convention, we’re going to call it _prototypeUID.

  • Here’s a nitpick: if (element.uid == undefined). The comparison operator (==) isn’t very precise, so if you’re testing for undefined, you should use the identity operator (===). You could also use Prototype’s Object.isUndefined. In fact, I will.

    I have a prejudice against the == operator. Most of the time the semantics of === are closer to what you mean. But this has special significance with undefined, which one encounters often in JavaScript. As an example: when you’re trying to figure out if an optional parameter was passed into a function, you’re looking for undefined. Any other value, no matter how “falsy” it is, means the parameter was given; undefined means it was not.

    (Oh, by the way: I am aware of the code screenshot on our homepage that violates the advice I just gave.)

  • There are other checks against undefined in this function. For consistency I’m going to change these to use Object.isUndefined as well. Also, the check for dflt != undefined is unnecessary: if that compound conditional passes, it means retrieve is going to return undefined anyway, so it doesn’t matter which of the two undefined values we return.

Man, I’m a bastard, aren’t I? Luckily, Element#store is similar enough that there’s no new feedback to be given here, so I’m done kvetching.

Before we rewrite this code to reflect the changes I’ve suggested, we’re going to make a couple design decisions.

Feature Design

While I was deciding how to replace Element.Storage.init, I had an idea: rather than use ordinary Objects to store the data, we should be using Prototype’s Hash. In other words, we’ll create a global table of Hash objects, each one representing the custom key-value pairs for a specific element.

This isn’t just a plumbing change; it’s quite useful to be able to deal with the custom properties in a group rather than just one-by-one. And since Hash mixes in Enumerable, interesting use cases emerge: e.g., looping through all properties and acting on those that begin with a certain “namespace.”

So let’s envision a new method: Element#getStorage. Given an element, it will return the Hash object associated with that element. If there isn’t one, it can “initialize” the storage on that element, thus making Element.Storage.init unnecessary.

This new method also establishes some elegant parallels: the store and retrieve methods are really just aliases for set and get on the hash itself. Actually, retrieve will be a bit more complicated because of the “default value” feature, but we’ll be able to condense store down to two lines.

The Rewrite

Enough blathering. Here’s the rewrite:

Element.Storage = {   UID: 1 };

As promised, I’ve moved the UID counter. The Element.Storage object also acts as our global hashtable, but all its keys will be numeric, so the UID property won’t get in anyone’s way.

Element#getStorage assumes the duties of Element.Storage.get and Element.Storage.init, thereby making them obsolete. We’ve removed them.

Element.addMethods({   getStorage: function(element) {     if (!(element = $(element))) return;      if (Object.isUndefined(element._prototypeUID))       element._prototypeUID = Element.Storage.UID++;      var uid = element._prototypeUID;      if (!Element.Storage[uid])       Element.Storage[uid] = $H();      return Element.Storage[uid];   },

The new getStorage method checks for the presence of _prototypeUID. If it’s not there, it gets defined on the node.

It then looks for the corresponding Hash object in Element.Storage, creating an empty Hash if there’s nothing there.

As I said before, Element#store is much simpler now:

  store: function(element, key, value) {     if (!(element = $(element))) return;     element.getStorage().set(key, value);     return element;   },

I thought about returning the stored value, to make it behave exactly like Hash#set, but some feedback from others suggested it was better to return the element itself for chaining purposes (as we do with many methods on Element).

And Element#retrieve is nearly as simple:

  retrieve: function(element, key, defaultValue) {     if (!(element = $(element))) return;      var hash = element.getStorage(), value = hash.get(key);      if (Object.isUndefined(value)) {       hash.set(key, defaultValue);       value = defaultValue;     }      return value;   } });

And we’re done.

Further refinements

In fact, we’re not done. This is roughly what the code looked like when I first checked in this feature, but some further improvements have been made.

Since we’d been using a system similar to this to associate event handlers with nodes, we had to rewrite that code to use the new storage API. In doing so, we found that we needed to include window in our storage system, since it has events of its own. Rather than define a _prototypeUID property on the global object, we give window a UID of 0 and check for it specifically in Element#getStorage.

Also, based on an excellent suggestion, we changed Element#store so that it could accept an object full of key/value pairs, much like Hash#update.

In Summation

I was happy to come across Sébastien's submission. It was the perfect length for a drive-by refactoring; it made sense as a standalone piece of code, without need for an accompanying screenshot or block of HTML; and it implemented a feature we'd already had on the 1.6.1 roadmap.

You can get the bleeding-edge Prototype if you want to try out the code we wrote. Or you can grab this gist if you want to drop the new functionality in alongside 1.6.0.3.

We're further grateful to Mootools for the API we're stealing. And to Wil Shipley for the recurring blog article series we're stealing.

When we first launched the Linkedin Prototype Group, we weren’t necessarily expecting it to be such a success–it’s over 800 members strong and counting.

Also, at the time, there wasn’t much you could do after having joined the group. This has changed with the recent introduction of discussions.

One of the first posts spurred some thoughts about the usefulness and goals of this Linkedin group especially given the high quality of our new mailing list. (And let me take the opportunity to sincerely thank T.J. Crowder for all the effort he’s put into it.)

My initial reaction, based on an early August thread was to suggest keeping the development-orientated discussions in the mailing list, while expecting more career-orientated ones to take place in the Linkedin group.

Of course, there’s no way we can nor should be controlling this, and in the end, you will be deciding what will happen where. So I suppose the only real raison d’être of this post is to advise you of this new feature and open up the debate.

Thoughts ?

We decided it’s finally time to implement an idea we had long ago.

I’m an avid reader of the blog of Wil Shipley, a man in the business of writing great apps for OS X. His running code improvment series, Pimp My Code, takes submissions from readers who think their code needs refactoring. Then Shipley refactors them, explaining the whys and hows along the way. The submissions are small (never more than 75-100 lines), but in rewriting them Shipley always happens upon specific, useful programming tips. I don’t know the first thing about Objective-C, but I find the series fascinating and instructive.

So we’re going to do something similar on this blog. Do you have a piece of JavaScript you want refactored? Does it use Prototype? Do this:

  1. Sign up for a GitHub account if you don’t have one. It’s free and quick.
  2. Go to Gist, GitHub’s pastebin app, and paste the code you want us to refactor. Mark it as “private” if you like.
  3. Message me on GitHub with the URL to your code snippet. If necessary, explain a bit about what the code does (or should do), but don’t write an epistle or anything.

I’ll share the submissions with the rest of the team and we’ll pick a few that we like. Then we’ll dedicate a post to each one, refactoring out loud along the way. We won’t be mean or snarky; this is not a DailyWTF-style exercise.

To pre-empt the obvious rebuttal: we do not consider this to be an act of charity, or code manna from computer heaven, or a gift from the light-bearers to the huddled masses. Whether we actually “improve” your code is not for us to say. It will, however, illustrate our coding style.

If that sounds useful to you, then step up! Give us code and ask that it be pimped!

Now that 1.6.0.3 is out, let’s talk about the Prototype community.

A lot of people have been commenting on how quiet it’s been around here over the last few months. There are several reasons:

  • We were quite busy with behind-the-scenes stuff. Moving to GitHub and Lighthouse was quite the task. As part of that migration we went through all the bugs on the old Rails Trac and were therefore left with a large backlog of bugs that we’d waited too long to address.
  • We were quite busy with our day jobs. Only a couple of us are freelancers; the rest work full-time for software companies. And usually there are several people working on Prototype at any one time, but over the summer it’s rarely been more than one or two.
  • In an effort to “catch up” with the accumulated tickets, we tried to stuff too much into a single bugfix release. We need to keep releases small and focused; trying to change too much at once tends to disorient us and our users. Once we realized we needed to scale back this release, it took a while to figure out which changes needed to stay and which needed to be reverted.

These aren’t excuses; they’re just explanations. As a team, we agree that we’ve got to prevent such a long release gap from happening again, and to keep an eye out for warning signs like the ones listed above.

This means, among other things, that we’re planning to move away from a “when it’s ready” release schedule. Instead, we’ll move toward one in which there are several releases per year; whatever is ready in time for a given release will go in, and whatever is not will have to wait. That applies to bug fixes and features alike. Eight months between releases just won’t work.

What you can do

Community outreach was one of the major goals of Prototype Developer Day. Many people are frustrated with the state of the Prototype community and would like to see some changes made. We’re in complete agreement.

Ideally, as an open-source community grows, those who want to help out gravitate toward specific roles. Those who can grok the source code write patches; those who are good at diagnosing problems file bug reports; those who can write clearly contribute documentation; and so on. We’d love to grow that “halo” around Prototype Core so that things can get done more quickly.

To be more specific, we would love help in any of these areas:

  1. Give support on the Prototype & scrip.aculous mailing list.
  2. File bugs in Lighthouse when you encounter errors or surprising behavior in Prototype.
  3. Write test cases or patches for existing bugs in Lighthouse.
  4. Discuss the direction of the library and its future on the Prototype Core mailing list.
  5. Propose new features and implement them.
  6. Write documentation wherever you feel we need more; submit it to Lighthouse as an enhancement.
  7. Suggest blog posts. (Or even write them!) Post to the Prototype Core list if you’re interested in doing this.

There are, of course, many other things one can do to help us out. But if you’re looking for a way to contribute and don’t have something specific in mind, we’d suggest doing one of these seven things.

What we can do

We know we need more help, but we also know we need to be better community curators. So here are some things we pledge to do better:

  1. We’ll beef up the Prototype web site so that it’s easier to get started with the framework, easier to find great resources like Scripteka and Prototype UI, and easier to find answers to common questions.
  2. We’ll give special attention to documentation tickets on Lighthouse so that our API docs don’t stay stale and thin.
  3. We’ll release on a more consistent schedule, as explained above.
  4. We’ll resume work on PDoc (inline documentation) and Sprockets (JS dependency management), spin-off projects that make Prototype more of a “platform.” They’ll be a boon to the Prototype ecosystem when they’re completed.

Finally: if you consider yourself to be good at planning and organizing an open-source project, then we’d love your input on how to grow our community. Our highest priority, however, is not to launch a new initiative or process; it’s to get more people doing the seven things listed above.