在本教程中,我们将建立一个P2P,分散,服务器和后端的聊天网站在不到100行代码。
(这是对原始教程的简化和纯javascript修改。)
创建新站点
- 点击⋮ > “在站点上创建新的空站点”菜单项。
- 你将被重定向到一个全新的网站,只有你可以修改!
- 将右上角的“0”按钮向左拖动以显示侧边栏
- 在底部,将站点标题更改为“My ZeroChat tutorial”,然后按“Save Site settings”。
聊天网站的HTML代码
- 在您喜爱的编辑器中打开“data/[yoursiteaddress]/index.html”文件。
- 将<body>标记的内容更改为:
<body>
<a href="#Select+user" id="select_user" onclick='return page.selectUser()'>Select user</a>:
<input type="text" id="message">
<input type="button" id="send" value="Send!" onclick="return page.sendMessage()"/>
<ul id="messages">
<li>Welcome to ZeroChat!</li>
</ul>
<script type="text/javascript" src="js/ZeroFrame.js"></script>
</body>
js/ZeroFrame.js文件(如果您使用zerohhello创建站点,则会自动绑定)包含ZeroFrame类,该类允许我们与基于websocket的zeroframeapi通信。
第一个ZeroFrame API调用
- 在<script>块中初始化我们的应用程序,在ZeroFrame.js包括:
<script type="text/javascript" src="js/ZeroFrame.js"></script>
<script>
class ZeroChat extends ZeroFrame {
addMessage (username, message) {
var message_escaped = message.replace(/</g, "<").replace(/>/g, ">") // Escape html tags in the message
this.messages.innerHTML += "<li><b>" + username + "</b>: " + message_escaped + "</li>"
}
onOpenWebsocket () {
this.messages = document.getElementById("messages")
this.addMessage("System", "Ready to call ZeroFrame API!")
}
}
page = new ZeroChat()
</script>
- 如果我们重新加载页面,我们会看到一个“准备调用ZeroFrame API!”信息。
- 要使“选择用户”按钮显示用户选择对话框,请向ZeroChat类添加以下功能:
selectUser () {
this.cmd("certSelect", {accepted_domains: ["zeroid.bit"]})
return false
}
显示用户当前的ZeroID帐户
当某些更改影响了站点(新内容到达、用户更改等)时,websocket事件将被推送到浏览器中(格式与查询siteInfo命令相同)
- 要处理此事件,请将此添加到我们的类中:
onRequest (cmd, message) {
if (cmd == "setSiteInfo") {
if (message.params.cert_user_id)
document.getElementById("select_user").innerHTML = message.params.cert_user_id
else
document.getElementById("select_user").innerHTML = "Select user"
this.site_info = message.params // Save site info data to allow access it later
}
}
此代码将实时更新当前选定的用户名。
要在页面加载时也显示当前选定的用户名,请将此添加到“onOpenWebsocket”函数:
this.cmd("siteInfo", {}, (site_info) => {
if (site_info.cert_user_id)
document.getElementById("select_user").innerText = site_info.cert_user_id
this.site_info = site_info
})
现在我们的应用程序应该总是正确地显示当前选择的用户(即使从其他浏览器窗口修改)
设置用户内容权限
为了允许其他用户在我们的网站上发表文章,我们必须定义第三方内容的规则。
在站点目录中创建data/users/content.json文件:
{
"files": {},
"ignore": ".*", // 此目录中的文件将由用户签名,而不是由网站所有者签名。
"modified": 0.0,
"signs": {},
"user_contents": {
// 我们接受*@zeroid.bit用户,他们必须提供一个证书,该证书必须由1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz地址签名。
"cert_signers": {
"zeroid.bit": [ "1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz" ]
},
// 我们为每个用户提供10KB的空间(如果使用bitmessage注册,则为15kb)
"permission_rules": {
".*": {
"files_allowed": "data.json",
"max_size": 10000
},
"bitmsg/.*@zeroid.bit": { "max_size": 15000 }
},
// 每用户权限:禁止“bad@zeroid.bit“用户并允许10万存储”nofish@zeroid.bit“用户。(是我:))
"permissions": {
"bad@zeroid.bit": false,
"nofish@zeroid.bit": { "max_size": 100000 }
}
}
}
保存此文件后,我们还必须修改站点的root content.json,以便在签名时忽略此目录中的所有内容,并加载包含以下规则的文件:
...
"ignore": "data/.*",
"includes": {
"data/users/content.json": {
"signers": [],
"signers_required": 1
}
},
...
注意:您可以通过向“签名者”列表中添加地址来向其他用户授予仲裁权限。
- 通过按侧边栏上的“Sign”按钮对root content.json修改进行签名。
- 然后保持侧边栏打开,并将“content.json”更改为“data/users/content.json”,然后再次按“Sign”按钮!
向json文件添加消息
当点击发送按钮时,我们会将消息添加到用户的 data.json 文件中,对其进行签名,然后将其发布给其他用户。
在 ZeroChat 类中创建一个新函数:
sendMessage () {
if (!this.site_info.cert_user_id) { // No account selected, display error
this.cmd("wrapperNotification", ["info", "Please, select your account."])
return false
}
// This is our data file path
var data_inner_path = "data/users/" + this.site_info.auth_address + "/data.json"
var content_inner_path = "data/users/" + this.site_info.auth_address + "/content.json"
// Load our current messages
this.cmd("fileGet", {"inner_path": data_inner_path, "required": false}, (data) => {
if (data) // Parse current data file
data = JSON.parse(data)
else // Not exists yet, use default data
data = { "message": [] }
// Add the new message to data
data.message.push({
"body": document.getElementById("message").value,
"date_added": Date.now()
})
// Encode data array to utf8 json text
var json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
// Write file to disk
this.cmd("fileWrite", [data_inner_path, btoa(json_raw)], (res) => {
if (res == "ok") {
// Reset the message input
document.getElementById("message").value = ""
// Sign the changed file in our user's directory
this.cmd("siteSign", {"inner_path": content_inner_path}, (res) => {
// Publish to other users
this.cmd("sitePublish", {"inner_path": content_inner_path, "sign": false})
})
} else {
this.cmd("wrapperNotification", ["error", "File write error: #{res}"])
}
})
})
return false
}
在此修改后,在消息输入中输入一些内容,然后按发送按钮! 您应该会在 data/users/[your auth address]/data.json 文件中看到该消息。(如果您看到“发布失败”消息,请不要担心:这是正常的,因为我们的网站上还没有任何其他用户)
创建数据库
现在我们可以保存和发布我们的消息给其他用户,让我们在我们的应用程序中显示它! 最好的方法是将所有 data.json 文件映射到 SQL 数据库。
ZeroNet 会自动为您执行此操作,您只需要站点目录中的 dbschema.json 文件来描述您的表结构:
{
"db_name": "ZeroChat", // 仅用于调试
"db_file": "data/zerochat.db", // SQLite 数据库文件将存储在这里
"version": 2, // 定义 json 表结构,版本 2 更适合基于 ZeroID 的站点。 参考文档中的更多信息。
// 描述json文件->表转换
"maps": {
// 将每个用户的 data.json 文件消息节点中的数据放入消息表。
"users/.+/data.json": {
"to_table": [ "message" ]
},
// 为了方便 SQL Join,将用户名存储在 data.json 文件的 json 表条目中。
"users/.+/content.json": {
"to_json_table": [ "cert_user_id" ],
"file_name": "data.json"
}
},
// 描述表和索引结构。 将为每个 json 文件自动创建一个 json 表条目。
"tables": {
"json": {
"cols": [
["json_id", "INTEGER PRIMARY KEY AUTOINCREMENT"], // 每个表都应该包含一个 json_id 列,它定义了源文件路径。
["directory", "TEXT"],
["file_name", "TEXT"],
["cert_user_id", "TEXT"]
],
"indexes": ["CREATE UNIQUE INDEX path ON json(directory, file_name)"],
"schema_changed": 10 // 更改表结构时增加此值,以便对等方可以删除表并从 json 文件重新创建它。
},
"message": {
"cols": [
["body", "TEXT"],
["date_added", "INTEGER"],
["json_id", "INTEGER REFERENCES json (json_id)"]
],
"indexes": ["CREATE UNIQUE INDEX message_key ON message(json_id, date_added)"],
"schema_changed": 10 // 更改表结构时增加此值,以便对等方可以删除表并从 json 文件重新创建它。
}
}
}
提示:为了获得最佳性能,请始终为表中的 json_id 列创建索引,因为当新文件到达时,数据将基于此列进行更新。
- 按 Reload,然后按侧边栏上的 Rebuild 按钮以生成数据库。
如果一切顺利,就会创建 data/zerochat.db SQLite 文件。 要浏览这些文件,我建议使用 SQLiteStudio(它是免费和开源的)
显示消息
消息被加载到 SQL 数据库中,因此我们可以使用 dbQuery 命令轻松查询它们:
loadMessages () {
this.cmd("dbQuery", ["SELECT * FROM message LEFT JOIN json USING (json_id) ORDER BY date_added DESC"], (messages) => {
document.getElementById("messages").innerHTML = "" // Always start with empty messages
for (var i=0; i < messages.length; i++) {
this.addMessage(messages[i].cert_user_id, messages[i].body)
}
})
}
将 this.loadMessages() 添加到 onOpenWebsocket 函数,然后重新加载页面,您应该会看到您输入的消息。
要使其“实时”并在新消息进来时立即显示,您还必须将 this.loadMessages() 添加到 onRequest 函数:
onRequest (cmd, message) {
if (cmd == "setSiteInfo") {
if (message.params.cert_user_id)
document.getElementById("select_user").innerHTML = message.params.cert_user_id
else
document.getElementById("select_user").innerHTML = "Select user"
this.site_info = message.params // Save site info data to allow access it later
// Reload messages if new file arrives
if (message.params.event[0] == "file_done")
this.loadMessages()
}
}
当我们提交新消息时也重新加载数据:
sendMessage () {
...
// Sign the changed file in our user's directory
this.cmd("siteSign", {"inner_path": content_inner_path}, (res) => {
this.loadMessages() // Reload messages
...
...
就是这样! 现在消息是实时更新的! 您可以通过打开另一个浏览器窗口并在那里输入消息来尝试。
最后添加
- 按 Enter 发送消息
<input type="text" id="message" onkeypress="if (event.keyCode == 13) page.sendMessage()">
- 并添加一些 CSS 样式以使其看起来更好
<style>
* { font-family: monospace; line-height: 1.5em; font-size: 13px; vertical-align: middle; }
body { background-color: white; }
input#message { padding: 5px; width: 50%; }
input#send { height: 34px; margin-left: -2px; }
ul { padding: 0px; }
li { list-style-type: none; border-top: 1px solid #eee; padding: 5px 0px; }
li:nth-child(odd) { background-color: #F9FAFD; }
li b { color: #3F51B5; }
</style>
恭喜! 您已完成 ZeroChat 教程! :)
一些改进的想法:
- 显示消息时间
- 将源代码移动到单独的 .js 文件
- 限制显示消息/添加分页
- Markdown 格式
- 用户名提及
提示:.js 文件被缓存。 为避免这种情况,您需要在选中“禁用缓存”的情况下保持打开开发控制台 (F12)。
提示:如果您对源代码进行了任何修改,请不要忘记重新签名和发布您的网站。
提示:您站点的私钥在 data/users.json 文件中(搜索您站点的地址,然后会有一个私钥条目),所以最好备份它!
提示:ZeroNet 为开发人员提供了内置的 .js 和 .css 合并功能。 要启用它,请将调试添加到 zeronet.conf 文件的 [global] 部分,然后如果您请求 all.js 或 all.css,它将响应目录中的每个文件的内容