Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
567 lines (530 sloc) 22.3 KB
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>KansoJS</title>
<link href="//fonts.googleapis.com/css?family=Ubuntu:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css" >
<link rel="stylesheet" href="http://yandex.st/highlightjs/5.16/styles/github.min.css">
<style>
body {
line-height: 1.618em;
font-size: 12px;
margin: 4.236em;
font-family: 'Lucida Grande', 'Lucida Sans Unicode', verdana, geneva, sans-serif;
color: #292626;
width: 56em;
}
h1 {
line-height: 1em;
font-family: 'Ubuntu', serif;
font-size: 4.236em;
font-weight: normal;
letter-spacing: -0.055em;
color: black;
margin-bottom: 0.809em;
}
h1 span {
font-size: 0.618em;
background: red;
color: white;
padding: 0.146em 0.236em;
position: relative;
top: -0.1em;
margin-left: 0.146em;
}
p {
margin: 2.618em 0;
padding: 0;
}
a {
text-decoration: none;
background-color: #D6CDA0;
padding: 0.263em;
color: black;
}
a:hover {
background-color: #96896F;
color: white;
}
ul {
line-height: 2.618em;
}
h2, h3 {
margin: 2.618em 0 1.618em 0;
}
h2 {
font-size: 1.618em;
font-weight: bold;
}
h3 {
font-size: 1em;
font-weight: bold;
}
pre {
border: dashed 1px #D6CDA0;
border-left: 0.382em solid #D6CDA0;
padding: 1em 2.618em;
margin: 0;
}
code {
background-color: white !important;
}
.step {
position: relative;
height: 380px;
}
.step img {
position: absolute;
top: 1.618em;
left: 0;
}
.step p {
border-left: 2px solid #ccc;
position: absolute;
top: 1.618em;
left: 370px;
padding-left: 20px;
margin: 0;
}
</style>
</head>
<body>
<h1>Kanso<span>JS</span></h1>
<p>The surprisingly simple way to write <a href="http://couchapp.org">CouchApps</a>.</p>
<ul class="features">
<li><strong>Flexible</strong> - a lightweight CommonJS environment</li>
<li><strong>Reduces code fragmentation</strong> - by bringing the client-side and server-side together</li>
<li><strong>Automatic history support</strong> - adds pushState and hash-based URLs automatically</li>
<li><strong>Searchable and degradable</strong> - easily add Google-indexable pages and support clients without JavaScript</li>
<li><strong>Familiar</strong> - all this can be done by using the CouchDB design doc APIs</li>
</ul>
<p>
CouchApps are JavaScript and HTML based apps served directly
from CouchDB. This means a super-simple stack and portable apps that can be
shared and deployed using CouchDB's built-in replication.
If you're not familiar with CouchApps, I highly recommend reading
<a href="http://couchapp.org/page/what-is-couchapp">What in the HTTP is a CouchApp?</a>.
</p>
<h2>The idea</h2>
<iframe src="https://docs.google.com/present/embed?id=dd5rhn5s_1153b9rndh&size=l" frameborder="0" width="700" height="559"></iframe>
<h3>Browser and server together</h3>
<!--
<p>
I hate writing everything twice in order to have fast and dynamic
sites which also provide fallbacks. Its annoying to copy around URL
maps so that JavaScript can manipulate browser history sensibly.
Its really annoying to have to implement validation twice, usually
in two different languages.
</p>
<p>
The simple idea behind Kanso is to support the design doc API in the
browser. You're already defining much of the application's behaviour
in the design doc, why should you have to redefine it all over again?
</p>
-->
<p>
CouchDB already allows dynamic responses using list and show functions.
Now, you can add client-side code too. That means you're able to fetch
additional documents or perform other complex operations from the
browser. CouchDB itself can then run the basic parts as a fall-back for
search engines or browsers without JavaScript. The rewrites you define
will fire in the browser too, allowing for automatic URL handling using
pushState or location.hash.
</p>
<pre><code class="javascript">function example_show(doc, req) {
var content = templates.render('welcome.html', req, {});
if (req.client) {
// being run client-side, update the current page
$('#main').html(content);
}
else {
// fallback, returns a complete rendered page
return templates.render('base.html', req, {main: content});
}
};</code></pre>
<p>
Think about all the things I'm <strong>not</strong> having to write.
I'm not having to set up Sammy.js on the client side with URL rewrite
rules that match CouchDB. I'm not having to fetch the document
for that URL before I'm able to template the page. I'm
also not having to maintain two completely different sets of code,
one for the browser and another for CouchDB.
</p>
<h3>CommonJS</h3>
<p>
The only problem with pushing all our application logic into the
design doc is that its JSON, and JSON isn't a nice format to develop
in. For starters, we have to write functions as strings and
we can't include code from other files! This is why KansoJS has
adopted the use of CommonJS modules instead of plain JSON for
writing design docs.
<p>
Writing your JavaScript in
<a href="http://wiki.commonjs.org/wiki/Modules">CommonJS modules</a> is
much nicer than the traditional way of loading separate files which all
modify the global scope. You may already know that you can use CommonJS
modules in your CouchDB lists, shows, validate_doc_update and other
functions. This makes it an ideal style to use throughout your
application.
</p>
<pre><code class="javascript">var templates = require('kanso/templates');
exports.rewrites = [
{from: '/:id', to: '_shows/example/:id'}
];
exports.shows = {
example: function (doc, req) {
...
}
};</code></pre>
<p>
One problem with CouchApps is the reliance on structuring your data on the
filesystem as you would like it to appear in the design doc. This means a
complex hierarchy of folders and files to represent properties in a JSON
document. It would be much better if you could represent this information in
whichever way suits the project. By loading the design doc data from a
module, its up to you how to structure it on the filesystem.
</p>
<p>
Instead of relying on CouchApp macros to include code from other files (by
using special comments in your code), you can do it more cleanly using
the CommonJS 'require' function.
</p>
<h2>Getting started</h2>
<h3>Installation</h3>
<p>
Install the most recent <em>stable</em> version of
<a href="http://nodejs.org/#download">node</a>, then clone
<a href="https://github.com/caolan/kanso">Kanso</a> from GitHub.
Fetch the relevant submodules by doing this in the cloned directory:
</p>
<pre>git submodule init
git submodule update</pre>
<p>You are then ready to install:</p>
<pre>make &amp;&amp; sudo make install</pre>
<h3>Starting a project</h3>
<p>
For this tutorial we'll be creating a basic app that displays albums by
artist name. First, change to the location you wish to create the project
directory in, then start a new project by doing:
</p>
<pre>kanso create recordstore</pre>
<p>
This will create a new directory called 'recordstore' containing a project
skeleton to get you started. The contents of this directory should look
something like the following:
</p>
<pre>recordstore
|- lib commonjs modules relevant to the app
|- app.js the module loaded as a design doc
|- static static files to be attached to design doc
|- jquery-1.4.2.min.js some dependencies
|- jquery.history.js
|- json2.js
|- templates templates used by the app
|- base.html
|- welcome.html
|- kanso.json kanso settings
</pre>
<p>
Before exploring these files in more detail, lets push the app to a couchdb
database and check that everything works. For this tutorial, I'll assume you
have a local couchdb database running at localhost:5984 (the default settings).
In your project directory, do the following:
</p>
<pre>kanso push http://localhost:5984/recordstore</pre>
<p>
If you now visit
<a href="http://localhost:5984/recordstore/_design/recordstore/_rewrite/">http://localhost:5984/recordstore/_design/recordstore/_rewrite/</a>
you should see a basic welcome page. Don't worry about the long and ugly URL
for now, this can be fixed later using
<a href="http://wiki.apache.org/couchdb/Virtual_Hosts">virtual hosts</a>.
</p>
<h3>kanso.json</h3>
<p>
Let's take a look at the kanso.json file first. This file contains the
settings for the app, and tells kanso which files and directories to load
and how to load them.
</p>
<pre><code class="javascript">{
"name": "recordstore", // the name of the design document
"load": "lib/app", // the module to get design doc properties from
"modules": "lib", // files and directories to load as commonjs modules
"templates": "templates", // templates directory
"attachments": "static" // files and directories to load as attachments
}</code></pre>
<p>
The name determines the id of the design doc, in this case it would be
"_design/recordstore". Perhaps the most important part is the 'load' property.
This tells kanso which module to require and use for the design doc. This
means any exported values in this module will be used in the resulting design
doc.
</p>
<h3>lib/app.js</h3>
<p>
This file contains some exported properties which are used in the resulting
design doc, this is where the action happens and you get to define the
behaviour of your app:
</p>
<pre><code class="javascript">var templates = require('kanso/templates');
exports.rewrites = [
{from: '/static/*', to: 'static/*'},
{from: '/', to: '_show/welcome'}
];
exports.shows = {
welcome: function (doc, req) {
var content = templates.render('welcome.html', req, {});
if (req.client) {
$('#content').html(content);
document.title = 'It worked!';
}
else {
return templates.render('base.html', req, {
title: 'It worked!',
content: content
});
}
}
};</code></pre>
<p>
You'll notice the skeleton project we generated comes with some rewrites
already set up. The first exposes the static directory, the second
rewrites the root url to a show function called 'welcome'. The welcome
function then shows the welcome template (the page you've already
seen after pushing the app). If this function is run client-side,
it replaces the contents of the current page with the welcome message,
otherwise it returns a new page.
</p>
<p>
There are a couple of interesting things here. Firstly, we've required a
'kanso/templates' module which doesn't exist in our project directory. This is
one of the kanso modules automatically added to your app when you push
to couchdb, and provides access to templates.
</p>
<p>
Secondly, we're actually using the context of the module inside the
welcome function. With a normal CouchApp these functions are just
converted to strings and inserted into the design doc, but kanso will
try to proxy the function using a require call (remember I said CouchDB
supports CommonJS?). This means we have access to the normal scope of
the surrounding code, allowing us to use the kanso module we required at
the top of the module inside the welcome function.
</p>
<p>
<strong>Note:</strong> The one important exception to this is view
functions, CouchDB views do not support CommonJS modules in the same way
and must be self contained.
</p>
<h3>templates/base.html</h3>
<p>
Kanso uses <a href="http://akdubya.github.com/dustjs/#about">Dust</a>
templates. These templates look similar to
<a href="http://mustache.github.com">Mustache</a>, but provide some
powerful additions such as filters, path lookups, blocks and better
partials support. Dust templates can also be pre-compiled, meaning your
templates are compiled when you 'kanso push', not each time a page is
rendered.
</p>
<pre><code class="html">
&lt;html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"&gt;
&lt;head&gt;
&lt;meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /&gt;
&lt;title&gt;{title}&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div id="content"&gt;
{content|s}
&lt;/div&gt;
&lt;script src="{baseURL}/static/jquery-1.4.2.min.js"&gt;&lt;/script&gt;
&lt;script src="{baseURL}/static/jquery.history.js"&gt;&lt;/script&gt;
&lt;script src="{baseURL}/static/json2.js"&gt;&lt;/script&gt;
&lt;script src="{baseURL}/kanso.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>
<a href="http://akdubya.github.com/dustjs">Learn more about the dust syntax</a>
</p>
<p>
This template should be fairly self-explanatory. We describe a basic
html document and render some variables: 'title', 'content' and
'baseURL'.
</p>
<p>
The 'content' variable passes through a filter 's'. This just means the
value isn't escaped (we're going to be putting html in there!).
</p>
<p>
The variable 'baseURL' also deserves a mention. This is automatically
made available in templates, and changes depending on whether the
request uses a virtual host or not. If you've used CouchApps with
virtual hosts before, you may be familiar with the problem of
including stylesheets and javascript without knowing the current
full path. Using baseURL fixes that, meaning you can run your CouchApp
either behind a virtual host or access it directly in the
/db/_design/doc/_rewrite/ style.
</p>
<h3>templates/welcome.html</h3>
<pre><code class="html">&lt;h1&gt;It worked!&lt;/h1&gt;
&lt;p&gt;Welcome to your new Kanso-powered app.&lt;p&gt;</code></pre>
<p>
Not much to see here, just some HTML we put into the 'content'
section of the base.html template.
</p>
<h2>The Recordstore</h2>
<p>
OK, now we've explored a skeleton Kanso project, let's start building our
own web app on top of it. Open Futon and view the recordstore database at
<a href="http://localhost:5984/_utils/database.html?recordstore">http://localhost:5984/_utils/database.html?recordstore</a>.
You should have already created this when we pushed the new project to
localhost in the 'Getting started' section above.
</p>
<p>Add the following documents:</p>
<pre><code class="javascript">{
"type": "album",
"title": "Blue Lines",
"artist": "Massive Attack",
"cover": "http://userserve-ak.last.fm/serve/174s/47527219.png"
},
{
"type": "album",
"title": "Mezzanine",
"artist": "Massive Attack",
"cover": "http://userserve-ak.last.fm/serve/174s/38150483.png"
},
{
"type": "album",
"artist": "Underworld",
"title": "Beaucoup Fish",
"cover": "http://userserve-ak.last.fm/serve/174s/41665159.png"
}</code></pre>
<p>
This can be done with the fairly ugly curl command below:
</p>
<pre style="white-space: pre-wrap;">curl -X POST -H "Content-Type: application/json" --data '{"docs": [{"type": "album", "title": "Blue Lines", "artist": "Massive Attack", "cover": "http://userserve-ak.last.fm/serve/174s/47527219.png"}, {"type": "album", "title": "Mezzanine", "artist": "Massive Attack", "cover": "http://userserve-ak.last.fm/serve/174s/38150483.png"}, {"type": "album", "artist": "Underworld", "title": "Beaucoup Fish", "cover": "http://userserve-ak.last.fm/serve/174s/41665159.png"}]}' http://127.0.0.1:5984/recordstore/_bulk_docs</pre>
<p>
Feel free to use your own selection of great albums if you wish, just
make sure you add more than one by the same artist so we can
demonstrate something later.
</p>
<h3>Creating views</h3>
<p>
To keep things clean and tidy, we're going to create a new module for
storing views. Create a new file at lib/views.js with the following content:
<p>
<pre><code class="javascript">exports.artist_names = {
map: function (doc) {
if (doc.type === 'album') {
emit(doc.artist, null);
}
},
reduce: function (keys, values, rereduce) {
return true;
}
};</code></pre>
<p>
Next, we need to export these views from the lib/app.js file so they are
included in the design document. To include code from another file we can
use require. Add the following to the end of lib/app.js.
</p>
<pre><code class="javascript">exports.views = require('./views');</code></pre>
<p>
Now, if we push the app again we should see the new view available in futon.
Push the app using the following command:
</p>
<pre>kanso push http://localhost:5984/recordstore</pre>
<p>
Then, visit
<a href="http://localhost:5984/_utils/database.html?recordstore/_design/recordstore/_view/artist_names">http://localhost:5984/_utils/database.html?recordstore/_design/recordstore/_view/artist_names</a>
to see the results of the newly created view.
</p>
<h3>Templates and list functions</h3>
<p>
Now we have a view and some data, let's replace the welcome page with a list
of artists. Once again, we'll create a new module for this code. Add a file at
lib/lists.js with the following content:
</p>
<pre><code class="javascript">var templates = require('kanso/templates');
exports.artists = function (head, req) {
start({headers: {'Content-Type': 'text/html'}});
var row, rows = [];
while (row = getRow()) {
rows.push(row);
}
// create a html list of artists
var content = templates.render('artists.html', req, {rows: rows});
if (req.client) {
// if client-side, replace the HTML of the content div with the list
$('#content').html(content);
// update the page title
document.title = 'Artists';
}
else {
// if server-side, return a newly rendered page using the base template
return templates.render('base.html', req, {
title: 'Artists',
content: content
});
}
};</code></pre>
<p>
This list function will be used with the artist_names view we created earlier.
When run server-side it will render a complete HTML page, and when run
client-side it will update the current page's content and title instead.
</p>
<p>
Again, we need to export this from lib/app.js too.
</p>
<pre><code class="javascript">exports.lists = require('./lists');</code></pre>
<p>
You may have noticed we used a template 'artists.html' in our list function.
This template doesn't exist, so we need to create it. Add a file at
templates/artists.html with the following content:
</p>
<pre><code class="html">&lt;h1&gt;Artists&lt;/h1&gt;
&lt;ul&gt;
{#rows}
&lt;li&gt;&lt;a href="{baseURL}/{key|uc}"&gt;{key}&lt;/a&gt;&lt;/li&gt;
{/rows}
&lt;/ul&gt;</code></pre>
<p>
Finally, let's replace the welcome page with our new list function.
Update lib/app.js to the following:
</p>
<pre><code class="javascript">exports.rewrites = [
{from: '/static/*', to: 'static/*'},
{from: '/', to: '_list/artists/artist_names', query: {group: true}}
];
exports.views = require('./views');
exports.lists = require('./lists');</code></pre>
<p>
We've removed the old welcome show function and added a rewrite from the
root URL to the new artists list function. Push the app again, then visit
<a href="http://localhost:5984/recordstore/_design/recordstore/_rewrite/">http://localhost:5984/recordstore/_design/recordstore/_rewrite/</a>.
You should see a list of artist links (the linked pages don't exist yet).
</p>
<h2>To be continued...</h2>
<p>
This tutorial is a work in progress, if you have any additions then
please fork the <a href="https://github.com/caolan/kanso">Kanso project</a>
on GitHub and edit the index.html file. Any questions or comments can be
sent to <a href="https://github.com/caolan">caolan</a> on GitHub.
</p>
<p>
To be kept informed, <a href="https://github.com/kanso">Watch the Kanso project on GitHub</a>
or follow <a href="http://twitter.com/caolan">@caolan</a> on Twitter.
</p>
<!-- GitHub ribbon -->
<a href="http://github.com/caolan/kanso"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://assets2.github.com/img/7afbc8b248c68eb468279e8c17986ad46549fb71?repo=&url=http%3A%2F%2Fs3.amazonaws.com%2Fgithub%2Fribbons%2Fforkme_right_darkblue_121621.png&path=" alt="Fork me on GitHub"></a>
<script src="http://yandex.st/highlightjs/5.16/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-20477836-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</body>
</html>