Raspberry Pi - [第三集] 使用 Coder 进行编码
Google Coder == “在树莓派上制作网页内容的一种简单方法”,嗯,这就是他们所说的!
![]() |
引言在我第一篇树莓派文章中,我快速地介绍了这款开发板,并展示了安装操作系统、启动开发板、连接互联网并浏览 CodeProject 的简单方法。 在本文中,我将审视新的 Google Coder 环境,看看这家巧克力工厂在做什么。 Google 将其描述为“在树莓派上制作网页内容的一种简单方法”,那么问题来了,它有多简单? |
入门
首先,你需要获取必要的下载文件,以便将该环境安装到 SD 卡上,以便启动 RasPi。访问http://googlecreativelab.github.io/coder/获取最新版本。撰写本文时,版本是 0.4。
网站上的说明有些不足。我最终解压了下载的文件,并采用了与我在第一篇 RasPi 文章中相同的方法,即 Win32DiskImager。只需将其指向解压的 zip 文件中的映像文件,并将 SD 卡设置为目标,然后让它自行操作。
Win32DiskImager 完成后,将 SD 卡插回 RasPi,连接屏幕、键盘/鼠标等,然后启动。
成功了!在经历了大量的屏幕滚动后,我最终看到了登录提示。再次说明,Coder 主页上的信息有些不足,最后我尝试了 Wheezy 发行版(看起来是基于此)的默认密码,即 `用户名: pi` 和 `密码: raspberry`。
现在我已登录,我使用命令 `startx` 启动一个桌面会话。
我使用的是无线适配器,桌面上有一个简单的图形界面工具用于配置无线适配器,完成此操作后,我的 RasPi 就连接到网络了。
您可能还想设置远程桌面,如果您有此需求。同样,我在第一篇文章中也做过,但为了节省您的时间,您可以打开一个终端并使用命令 `sudo apt-get install xrdp`。
最后,回到我的普通 PC,按照 Coder 主页上的说明安装 Bonjour Print Services。
注意:另一种方法是查看 RasPi 使用的 IP 地址。在上面的截图中,您可以看到我的无线适配器已被分配到 `192.168.0.28`,因此我可以简单地导航到 `http://192.168.0.28/`。
此时,一切都应该准备就绪,可以通过浏览器访问 Coder 环境。
浏览器中的 Coder
回到我的普通 PC,打开浏览器窗口并导航到 http://coder.local/ ,您会收到一个安全警告(至少在 Chrome 中是这样),因为网站的 SSL 证书,但由于这是您自己的 RasPi,您可以安全地“继续访问”,所以我这样做了。
此时,您将看到 Coder 环境的开头。系统会要求您设置一个密码来保护您的 Coder。
是的,有密码规则,我发现了!
注意:在此设置密码时,它也会更改 `用户名: pi` 的默认登录密码。
设置好密码后,您现在可以登录到您的 Coder 环境开始编码了。
这是 Coder 的“入门”页面。
设置
点击 Coder 环境右上角的“齿轮”图标,可以设置您的 Coder 的名称、添加您的名字、设置颜色,还可以访问 Wifi 设置并更改密码。
Coder 网站上提到,如果您忘记了密码,可以将 SD 卡插入另一台计算机,并在 coder_settings 目录中创建一个名为“reset.txt”的文本文件,这样下次重启并连接到浏览器中的 Coder 控制台时,就可以激活密码重置。
演示应用程序
从上面的截图中可以看到,有 3 个演示应用程序可供浏览源代码。
“Hello World”是一个非常简单的“Hello World”页面。“Eyeball”应用程序是一个简单的动画眼球,会跟随鼠标移动并在偶尔眨眼。“Space Rocks”应用程序虽然可以运行,但图形对我来说没有正确显示,所以我不太清楚发生了什么,我只看到生命计数器,当我按下箭头键时,肯定有什么东西移动了,因为我死了。
我觉得这有点奇怪,出于好奇,我启动了一个 IE10 会话并指向 coder。这次我可以看到并愉快地玩“Space Rocks”应用程序。它原来是一个《太空侵略者》的克隆。
Google Chrome (29.0.1547.66) 无法播放游戏而 IE10 却成功了,这真是令人非常失望。Chrome 0 - 1 Internet Explorer。我以为 Google 团队绝对不会允许这种情况发生,所以怀疑是我的机器出了问题。是机器因为 CPU 和 GPU 满负荷运行而崩溃,还是它今天状态不佳。重新启动机器并重试后,一切恢复正常,“Space Rocks”现在可以在 Chrome 中运行了,宇宙恢复了和平。
如果在上面截图的右上角,您会注意到有“CODER”、“</>”和“HACK”,这些允许直接返回到 Coder 主页、编辑应用程序的后端代码以及一个快速黑客页面。
编辑器页面
- A - 返回 Coder 环境主页
- B - 点击应用程序标题以返回运行中的应用程序
- C - 在代码元素的各种视图之间切换
- D - 媒体管理器,用于上传艺术品、音频等。
- E - 预览,将编辑器切换到代码和应用程序的并排预览模式
- F - 应用程序设置,设置应用程序标题、颜色、作者。
Coder 应用程序环境
这是一个非常现代化的 Web 应用程序环境,使用标准的 HTML、CSS 和 Javascript。对于服务器端,还有Node.js。
考虑到这基本上是一个精简版的 Linux 系统,我想如果你愿意,你可以把它装上任何你想装的东西。也许我们以后再测试这一点,先专注于基本知识。
Coder 环境使用的代码编辑器构建在Ace.js之上。
Node.js 模块
node 模块位于 `/home/coder/coder-dist/coder-base/node_modules` 目录中,我不得不使用文件管理器来查看里面有什么。
提供的 Node.js 库有:
- redis - 内存中、持久化的键值数据存储
- mustache - 无逻辑的模板系统
- express - Web 开发框架
- socket.io - 实时 Web 套接字
- consolidate - 模板引擎集成库
- bcrypt - 密码哈希实用程序
还有一个 Node 包管理器,NPM,可用于扩展 Node 的功能。这通过命令行完成,但不是本文的范围,如果您真的想了解更多,可以自己去搜索。
要不要做一个应用程序?
当然要,我们毕竟是为了这个而来的!何不从一个简单的开始呢?做一个简单的待办事项列表应用程序怎么样。
各个部分的代码可以在以下下载中找到,您可以将每个文件的内容复制并粘贴到 Coder 编辑器中的相应选项卡中:
首先,我们必须创建应用程序。从 Coder 环境主页,点击带有加号的绿色方块。接下来,设置应用程序的颜色和标题,然后点击“创建”。在本例中,我创建的是“Daves Todo”。
那么这个应用程序需要能够做什么呢?我想您可以猜到,但总结一下功能:
- 显示项目列表。
- 向列表中添加项目。
- 删除列表中的项目。
- 一次性删除列表中的所有项目。
这是我们最终会得到的效果
顶部有一些按钮用于执行操作,待办事项列表包含删除单个项目的链接,以及数据输入表单(实际上是显示/隐藏的,并不总是可见)。这是一个非常简单的开始,但每个人都必须从某个地方开始!
在截图底部,您可以看到 Chrome 的开发者环境,这对于调试正在发生的事情以及树莓派和 Node 之间传输的数据来说是必不可少的。
HTML 标记
下面的代码展示了编辑器 HTML 选项卡中的内容。HEAD 元素中的代码是 Coder 创建的通用模板。我只添加了主内容部分:
<!DOCTYPE html>
<html>
<head>
<title>Coder</title>
<meta charset="utf-8">
<!-- Standard Coder Includes -->
<script>
var appname = "{{app_name}}"; //app name (id) of this app
var appurl = "{{&app_url}}";
var staticurl = "{{&static_url}}"; //base path to your static files /static/apps/yourapp
</script>
<link href="/static/apps/coderlib/css/index.css" media="screen" rel="stylesheet" type="text/css"/>
<script src="/static/common/js/jquery.min.js"></script>
<script src="/static/common/ace-min/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="/static/apps/coderlib/js/index.js"></script>
<script>
Coder.addBasicNav();
</script>
<!-- End Coder Includes -->
<!-- This app's includes -->
<link href="{{&static_url}}/css/index.css" media="screen" rel="stylesheet" type="text/css"/>
<script src="{{&static_url}}/js/index.js"></script>
<!-- End apps includes -->
</head>
<body class="">
<div class="pagecontent">
<h1>Hi Dave, here are your things todo!</h1>
<div id="refresh" class="button">Refresh</div><div id="addItem" class="button">Add Item</div><div id="deleteAll" class="button">Delete All</div>
<div id="todoListContainer">
<div id="refreshTitle"><i>Loading items...</i></div>
<table id="itemList" class="CSSTableGenerator">
<tr><th>Description</th><th>Priority</th></tr>
</table>
<div id="noItems">You have no items todo!</div>
</div>
<div id="todoForm">
<form id="entryForm">
<fieldset>
<legend id="formTitle">Add a new item</legend>
<input type="hidden" name="item" value=""/>
<table>
<tr><th>Item description</th><th>Priority</th></tr>
<tr><td><input type="text" id="description" name="description" placeholder="Add a new item here!"/></td>
<td><input type="number" id="priority" name="priority" min="1" max="5" value="1"/></td>
</tr>
<tr><td><input type="submit" id="submit" title="Add Item" class="button"/></td>
<td><input type="button" id="cancel" title="Cancel" value="Cancel" class="button"/></td>
</tr>
</table>
</fieldset>
</form>
</div>
</div>
</body>
</html>
CSS
使用一系列基于 Web 的生成器(参见末尾的 CSS 参考部分),我添加了用于样式的 CSS 标记。唯一的原始 CSS 是 `.pagecontent` 块。
.pagecontent {
padding: 24px;
}
.button {
-moz-box-shadow:inset 0px 1px 0px 0px #ffffff;
-webkit-box-shadow:inset 0px 1px 0px 0px #ffffff;
box-shadow:inset 0px 1px 0px 0px #ffffff;
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ededed), color-stop(1, #dfdfdf) );
background:-moz-linear-gradient( center top, #ededed 5%, #dfdfdf 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed', endColorstr='#dfdfdf');
background-color:#ededed;
-webkit-border-top-left-radius:6px;
-moz-border-radius-topleft:6px;
border-top-left-radius:6px;
-webkit-border-top-right-radius:6px;
-moz-border-radius-topright:6px;
border-top-right-radius:6px;
-webkit-border-bottom-right-radius:6px;
-moz-border-radius-bottomright:6px;
border-bottom-right-radius:6px;
-webkit-border-bottom-left-radius:6px;
-moz-border-radius-bottomleft:6px;
border-bottom-left-radius:6px;
text-indent:0;
border:1px solid #dcdcdc;
display:inline-block;
color:#777777;
font-family:arial;
font-size:15px;
font-weight:bold;
font-style:normal;
height:50px;
line-height:50px;
width:100px;
text-decoration:none;
text-align:center;
text-shadow:1px 1px 0px #ffffff;
}
.button:hover {
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #dfdfdf), color-stop(1, #ededed) );
background:-moz-linear-gradient( center top, #dfdfdf 5%, #ededed 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#dfdfdf', endColorstr='#ededed');
background-color:#dfdfdf;
}.button:active {
position:relative;
top:1px;
}
label
{
width: 4em;
float: left;
text-align: right;
margin-right: 0.5em;
display: block
}
.submit input
{
margin-left: 4.5em;
}
input
{
color: #781351;
background: #fee3ad;
border: 1px solid #781351
}
.submit input
{
color: #000;
background: #ffa20f;
border: 2px outset #d7b9c9
}
fieldset
{
border: 1px solid #781351;
width: 20em
}
legend
{
color: #fff;
background: #ffa20c;
border: 1px solid #781351;
padding: 2px 6px
}
.CSSTableGenerator {
margin:0px;padding:0px;
width:100%;
box-shadow: 10px 10px 5px #888888;
border:1px solid #000000;
-moz-border-radius-bottomleft:0px;
-webkit-border-bottom-left-radius:0px;
border-bottom-left-radius:0px;
-moz-border-radius-bottomright:0px;
-webkit-border-bottom-right-radius:0px;
border-bottom-right-radius:0px;
-moz-border-radius-topright:0px;
-webkit-border-top-right-radius:0px;
border-top-right-radius:0px;
-moz-border-radius-topleft:0px;
-webkit-border-top-left-radius:0px;
border-top-left-radius:0px;
}.CSSTableGenerator table{
width:100%;
height:100%;
margin:0px;padding:0px;
}.CSSTableGenerator tr:last-child td:last-child {
-moz-border-radius-bottomright:0px;
-webkit-border-bottom-right-radius:0px;
border-bottom-right-radius:0px;
}
.CSSTableGenerator table tr:first-child td:first-child {
-moz-border-radius-topleft:0px;
-webkit-border-top-left-radius:0px;
border-top-left-radius:0px;
}
.CSSTableGenerator table tr:first-child td:last-child {
-moz-border-radius-topright:0px;
-webkit-border-top-right-radius:0px;
border-top-right-radius:0px;
}.CSSTableGenerator tr:last-child td:first-child{
-moz-border-radius-bottomleft:0px;
-webkit-border-bottom-left-radius:0px;
border-bottom-left-radius:0px;
}.CSSTableGenerator tr:hover td{
}
.CSSTableGenerator tr:nth-child(odd){ background-color:#e5e5e5; }
.CSSTableGenerator tr:nth-child(even) { background-color:#ffffff; }.CSSTableGenerator td{
vertical-align:middle;
border:1px solid #000000;
border-width:0px 1px 1px 0px;
text-align:left;
padding:7px;
font-size:10px;
font-family:Arial;
font-weight:normal;
color:#000000;
}.CSSTableGenerator tr:last-child td{
border-width:0px 1px 0px 0px;
}.CSSTableGenerator tr td:last-child{
border-width:0px 0px 1px 0px;
}.CSSTableGenerator tr:last-child td:last-child{
border-width:0px 0px 0px 0px;
}
.CSSTableGenerator tr:first-child td{
background:-o-linear-gradient(bottom, #cccccc 5%, #b2b2b2 100%); background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #cccccc), color-stop(1, #b2b2b2) );
background:-moz-linear-gradient( center top, #cccccc 5%, #b2b2b2 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#cccccc", endColorstr="#b2b2b2"); background: -o-linear-gradient(top,#cccccc,b2b2b2);
background-color:#cccccc;
border:0px solid #000000;
text-align:center;
border-width:0px 0px 1px 1px;
font-size:14px;
font-family:Arial;
font-weight:bold;
color:#000000;
}
.CSSTableGenerator tr:first-child:hover td{
background:-o-linear-gradient(bottom, #cccccc 5%, #b2b2b2 100%); background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #cccccc), color-stop(1, #b2b2b2) );
background:-moz-linear-gradient( center top, #cccccc 5%, #b2b2b2 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#cccccc", endColorstr="#b2b2b2"); background: -o-linear-gradient(top,#cccccc,b2b2b2);
background-color:#cccccc;
}
.CSSTableGenerator tr:first-child td:first-child{
border-width:0px 0px 1px 0px;
}
.CSSTableGenerator tr:first-child td:last-child{
border-width:0px 0px 1px 1px;
}
JavaScript
这一切对项目来说都是新的。这不是 JS 课程,所以我并不打算详细说明具体细节,但总体概述如下:
var items = [];
function item(description, priority) {
this.Description = description;
this.Priority = priority;
}
`items` 数组保存了从服务器返回的项目的本地副本。它们由 `loadItems` 方法从 JSON 中解析出来。`item` 函数是一个通用的待办事项项对象,我们用它来创建新项目并将其推送到 `items` 数组。
$(document).ready( function() {
//Disable jQuery ajax caching!
$.ajaxSetup({
cache: false
});
$('#todoForm').hide();
//Attach the actions
$("#refresh").click(function () {
console.log("Refresh clicked.");
loadItems();
});
$("#addItem").click(function () {
console.log("Add Item clicked.");
$('#formTitle').text("Add Todo Item");
$('#todoForm').show();
});
$("#deleteAll").click(function () {
console.log("Delete All clicked.");
deleteAll();
$('#todoForm').hide();
});
$("#entryForm").submit(function (event) {
console.log("Form submitted.");
event.preventDefault();
$.ajax({
type: "POST",
url: "/app/daves_todo/addItem",
data: $("#entryForm").serialize(),
dataType: "string",
success: function(){
loadItems();
}
});
$('#todoForm').hide();
//Let us wait a wee bit to allow the raspi to catch up, it is a bit slow!
//then we will load the items.
setTimeout (function () { loadItems()},500);
});
$("#cancel").click(function () {
$('#todoForm').hide();
});
//Load the items, after waiting for 500ms
setTimeout(function () { loadItems()},500);
});
当文档加载时,jQuery 会将事件绑定到相关对象,并分配一些事件将执行的函数。树莓派有点慢,所以我将使用一个巧妙的 timeout 函数来延迟待办事项加载,在执行操作之后。是的,有更好的方法来做这件事,但这只是演示中使用的一种“技巧”。
接下来是用于从服务器加载项目的代码。有一个 jQuery get JSON 调用,它会解析项目并将它们放入本地数组。从这里,我们遍历数组并构建表格来显示项目,并设置删除单个项目的链接。
function loadItems() {
//Retrieve a list of items from the server
items = [];
$("#noItems").hide();
$("#refreshTitle").show();
//Clear the table contents
$("#itemList").html("");
console.log("Begin ajax calls to server.");
$.getJSON( "/app/daves_todo/getItems", function( data ) {
$.each( data, function( key, val ) {
var newItem = new item(val.Description, val.Priority);
items.push(newItem);
});
if (items.length === 0)
{
$("#noItems").show();
}
else
{
//Add the table header row
$("#itemList").append("<tr><th>Description</th><th>Priority</th><th>Actions</th>");
for (var index =0; index < items.length; index++)
{
var newRow = "<tr><td>" + items[index].Description + "</td><td>" + items[index].Priority + "</td><td><a href=/app/daves_todo/deleteItem?id=" + index.toString() + ">Remove</a></td></tr>";
$("#itemList").append(newRow);
}
}
})
.fail(loadFail)
.always(console.log("Completed ajax load method"));
$("#refreshTitle").hide();
}
function loadFail() {
console.log("Error loading items from server.");
alert("Error loading items from server.");
}
最后,是 `deleteAll` 方法,它发送一个 jQuery POST 请求进行删除,然后使用另一个短暂的延迟来重新加载表格。
function deleteAll() {
console.log("Begin ajax delete all call to server.");
$.ajax({
type: "POST",
async: false,
url: "/app/daves_todo/deleteAll",
success: function(){
//Yes, another delay to allow raspi to play catch up.
setTimeout(function() { loadItems()}, 500);
}
});
}
Node.js 后端 JavaScript
exports.settings={};
//These are dynamically updated by the runtime
//settings.appname - the app id (folder) where your app is installed
//settings.viewpath - prefix to where your view html files are located
//settings.staticurl - base url path to static assets /static/apps/appname
//settings.appurl - base url path to this app /app/appname
//settings.device_name - name given to this coder by the user, Ie."Billy's Coder"
//settings.coder_owner - name of the user, Ie. "Suzie Q."
//settings.coder_color - hex css color given to this coder.
设置部分已经存在,此演示未进行更改。
接下来的两个部分定义了“GET”和“POST”路由处理器。
exports.get_routes = [
{ path:'/', handler:'index_handler' },
{ path:'/getItems', handler: 'getItems'},
{ path:'/deleteItem', handler: 'deleteItem'},
];
exports.post_routes = [
{ path:'/deleteAll', handler: 'deleteAllItems'},
{ path:'/addItem', handler: 'addItem'},
];
下一个方法是默认的索引页面处理器。我没有对其进行更改,您可以看到它渲染了默认的索引模板文件。
exports.index_handler = function( req, res ) {
var tmplvars = {};
tmplvars['static_url'] = exports.settings.staticurl;
tmplvars['app_name'] = exports.settings.appname;
tmplvars['app_url'] = exports.settings.appurl;
tmplvars['device_name'] = exports.settings.device_name;
res.render( exports.settings.viewpath + '/index', tmplvars );
};
exports.on_destroy = function() {
};
`on_destroy` 函数是另一个预定义的函数,用于在需要时执行任何清理等操作。
以下代码用于创建项目存储。它是一个内存数组。在应用程序首次运行时,它会预先填充一个新的待办事项。与客户端 JavaScript 类似,有一个函数用于创建新的待办事项对象并将其推送到数组。
var items = [];
var newItem = new item("Your first thing to do is try this!",1);
items.push(newItem);
function item(description, priority) {
this.Description = description;
this.Priority = priority;
}
`getItems` 方法由客户端调用,并简单地创建一个 items 数组的 JSON 字符串。`addItem` 解析 POST 中的表单数据,创建新项目并将其推送到数组。`deleteItem` 从查询字符串中提取要删除的项的索引(记住我们在两侧都使用索引,所以它们应该始终对齐,然后使用 splice 来删除该项。最后,`deleteAllItems` 完成其名称所示的工作,只需创建一个新的空数组来替换所有现有项。
exports.getItems = function(req, res) {
res.writeHead(200, {"Content-Type": "application/json"});
res.write(
JSON.stringify(items)
);
res.end();
};
exports.addItem = function(req, res) {
var newItem = new item(req.param('description'),req.param('priority'));
items.push(newItem);
res.writeHead(200, {"Content-Type": "application/text"});
res.write("Item added.");
res.end();
};
exports.deleteItem = function(req, res){
//Delete a single item referenced by array index key
var id = req.param('id');
items.splice(id,1);
res.redirect("/app/daves_todo/");
res.end();
};
exports.deleteAllItems = function(req, res) {
//Remove all items
items = [];
res.writeHead(200, {"Content-Type": "application/text"});
res.write("Items deleted.");
res.end();
};
请注意 - 需要大量改进!
此演示中存在大量不良实践。例如,存在伪造、回放、注入等问题,这些在生产代码中是不可取的。
显然还需要考虑的其他事项包括持久化存储到数据库。自动机制以更新 UI 在数据存储更改时,删除客户端数组等。
结论
缺少大量文档。试图弄清楚实际存在什么,默认加载了哪些库,Coder 中的整个多应用程序环境是如何工作的等等。
第一次尝试理解 Node.js 并不容易。嗯,至少在这个有限的环境中是这样。如果我开始尝试将服务器控制台消息等推送到客户端,事情可能会变得容易,但这会有点违背“开箱即用”的体验。
作为 HTML 和客户端事物的基本入门,它也许足够了。但是,我认为树莓派有时会崩溃,需要重新启动才能恢复。终端窗口等响应正常,但通过浏览器访问 Coder 环境则完全无法使用。旧的“经常保存”的格言在这里适用。
毫无疑问,这具有潜力,希望随着开发团队的不断改进,它将成为一种更完善的体验。
我本人,则暂时觉得自己已经“报废”了,我花了几天时间试图解析 jQuery JSON POST 到 Node,但最终放弃了,而是恢复到传统的查询字符串 POST。
未来?
也许我会设法将 Mongodb 集成到其中,也许我会更详细地研究 Node,也许我只需要睡个觉……
希望随着更熟练的 RasPi 用户和 Node 专家们加入进来,会有更多的文章出现。所以,如果您有 RasPi,请下载 Coder,尝试一下,看看您有什么看法!
最重要的是,玩得开心,坚持下去!
参考文献
CSS 参考
为了减轻为页面编写 CSS 的麻烦,我使用了网上已有的示例和生成器。这减轻了制作演示的痛苦!
- http://www.webcredible.co.uk/user-friendly-resources/css/css-forms.shtml - 表单布局
- http://www.cssbuttongenerator.com/ - 按钮生成器
- http://www.csstablegenerator.com/ - 表格生成器
历史
- 2013年12月6日 - 删除了此网站添加到代码块中的无效引用
- 2013年9月26日 - 在演示应用程序部分添加了下载和相关代码。
- 2013年9月26日 - 添加了关于通过 IP 地址而不是 coder.local 连接的修正。
- 2013年9月25日 - 更新了叙述修订和内联代码标记。
- 2013年9月24日 - 首次发布