sails.js
如果您是当前使用Django,Laravel或Rails等框架的开发人员,那么您可能已经听说过Node.js。 您可能已经在项目中使用了流行的前端库,例如Angular或React。 到目前为止,您应该考虑完全转换到基于Node.js的服务器技术。
但是,最大的问题是从哪里开始。 今天,JavaScript世界在过去几年中以令人难以置信的快速增长,并且似乎还在不断扩展。
如果您担心失去在Node Universe中来之不易的编程经验,请不要担心,因为我们有Sails.js 。
Sails.js是一个实时MVC框架,旨在帮助开发人员在短时间内构建可用于生产环境的企业级Node.js应用程序。 Sails.js是一个纯JavaScript解决方案,可同时支持多个数据库和多种前端技术。 如果您是Rails开发人员,您将很高兴得知Sails.js创始人Mike McNeil受到Rails的启发。 您会在Rails和Sails.js项目之间发现很多相似之处。
在本文中,我将通过向您展示如何构建简单,用户友好的聊天应用程序,向您介绍Sails.js的基础知识。 可以在此GitHub存储库中找到sails-chat项目的完整源代码。
先决条件
在开始之前,您至少需要具备使用MVC架构开发应用程序的经验。 本教程适用于中级开发人员。 您还至少需要具备以下基础:
为了使每个人都实用和公平,本教程将使用默认情况下在新的Sails.js项目中安装的核心库。 与现代前端库(如React,Vue或Angular)的集成不在此处讨论。 但是,我强烈建议您在本文之后对它们进行研究。 另外,我们不会进行数据库集成。 相反,我们将使用默认的基于本地磁盘的基于文件的数据库进行开发和测试。
项目计划
本教程的目的是向您展示如何构建类似于Slack , Gitter或Discord的聊天应用程序。
并不是的! 建立这些出色的平台需要大量的时间和精力。 当前开发到它们的功能数量非常庞大。
相反,我们将构建一个聊天应用程序的最低可行产品版本,其中包括:
- 单人聊天室
- 基本身份验证(无密码)
- 个人资料更新。
我添加了配置文件功能作为奖励,以覆盖Sails.js功能的更多内容。
安装Sails.js
在开始安装Sails.js之前,我们首先需要设置一个合适的Node.js环境。 在撰写本文时,当前可用的最新稳定版本是v0.12.14。 也提供Sails.js v1.0.0,但目前处于测试阶段,不建议用于生产环境。
我可以访问的Node的最新稳定版本是v8.9.4。 不幸的是,Sails.js v0.12在当前最新的LTS上无法正常工作。 但是,我已经使用Node v.7.10进行了测试,发现一切工作都非常顺利。 这样做仍然很好,因为我们可以在代码中使用一些新的ES8语法。
作为JavaScript开发人员,您将意识到仅使用一个版本的Node.js是不够的。 因此,我建议使用nvm工具轻松管理Node.js和NPM的多个版本。 如果尚未这样做,请清除现有的Node.js安装,然后安装nvm来帮助您管理Node.js的多个版本。
以下是安装Node v7和Sails.js的基本说明:
# Install the latest version of Node v7 LTS
nvm install v7
# Make Node v7 the default
nvm default alias v7
# Install Sails.js Global
npm install -g sails
如果您的互联网连接良好,则只需几分钟或更短的时间。 现在让我们使用Sails generator命令创建新的应用程序:
# Go to your projects folder
cd Projects
# Generate your new app
sails generate new chat-app
# Wait for the install to finish then navigate to the project folder
cd chat-app
# Start the app
sails lift
该应用程序可能需要几秒钟才能启动。 您需要在浏览器中手动打开URL http://localhost:1337
才能看到新创建的Web应用程序。
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
看到这一点,就可以确认我们有一个正在运行的项目且没有错误,并且可以开始工作了。 要停止项目,只需在终端上按Control + c 。 使用您喜欢的代码编辑器(我正在使用Atom)检查生成的项目结构。 以下是您应注意的主要文件夹:
-
api
:控制器,模型,服务和策略(权限) -
assets
:图片,字体,JS,CSS,Less,Sass等。 -
config
:项目配置,例如数据库,路由,凭据,语言环境,安全性等 -
node_modules
:已安装的npm软件包 -
tasks
:用于编译和注入资产的Grunt配置脚本和管道脚本 -
views
:查看页面-例如,EJS,Jade或您喜欢的任何模板引擎 -
.tmp
:Sails在开发模式下用于构建和服务项目的临时文件夹。
在继续之前,我们需要做几件事:
- 更新EJS包 。 如果
package.json
列出了EJS 2.3.4,则需要通过将其立即更改为2.5.5进行更新。 它包含一个严重的安全漏洞。 更改版本号后,执行npm安装以执行更新。 - 热装 。 我建议您安装sails -hook-autoreload来为Sails.js应用程序启用热重装。 这不是一个完美的解决方案,但会使开发更加容易。 要为当前版本的Sails.js安装它,请执行以下操作:
npm install sails-hook-autoreload@for-sails-0.12 --save
安装前端依赖项
对于本教程,我们将花费尽可能少的时间来构建UI。 您满意的任何CSS框架都可以。 在本教程中,我将使用语义UI CSS库。
Sails.js没有有关如何安装CSS库的特定指南。 您可以采用三种或三种以上的方法进行操作。 让我们看看每个。
1.手动下载
您可以自己下载CSS文件和JS脚本以及它们的依赖项。 下载后,将文件放在assets
文件夹中。
我宁愿不使用这种方法,
因为需要人工来保持文件更新。 我喜欢自动化任务。
2.使用凉亭
此方法要求您在项目的根目录下创建一个名为.bowerrc
的文件。 粘贴以下代码段:
{
"directory" : "assets/vendor"
}
这将指示Bower安装到assets/vendor
文件夹而不是默认的bower_components
文件夹。 接下来,在全球范围内安装Bower,并使用Bower在本地安装您的前端依赖项:
# Install bower globally via npm-
npm install -g bower
# Create bower.json file, accept default answers (except choose y for private)
bower init
# Install semantic-ui via bower
bower install semantic-ui --save
# Install jsrender
bower install jsrender --save
稍后我将解释jsrender
的目的。 我认为最好一次完成安装依赖项的任务。 您应该注意,还安装了jQuery,因为它是semantic-ui
的依赖semantic-ui
。
安装后,更新assets/style/importer.less
以包括以下行:
@import '../vendor/semantic/dist/semantic.css';
接下来,在tasks/pipeline.js
包括JavaScript依赖项:
var jsFilesToInject = [
// Load Sails.io before everything else
'js/dependencies/sails.io.js',
// Vendor dependencies
'vendor/jquery/dist/jquery.js',
'vendor/semantic/dist/semantic.js',
'vendor/jsrender/jsrender.js',
// Dependencies like jQuery or Angular are brought in here
'js/dependencies/**/*.js',
// All of the rest of your client-side JS files
// will be injected here in no particular order.
'js/**/*.js'
];
当我们运行sails lift
,JavaScript文件将被自动注入views/layout.ejs
文件按pipeline.js
指令。 当前的grunt
设置将为我们注入CSS依赖项。
重要提示:在.gitignore
文件中添加vendor
一词。 我们不希望将供应商依赖项保存在我们的存储库中。
3.使用npm + grunt.copy
第三种方法需要更多的精力来设置,但是会减少占地面积。 使用npm安装依赖项,如下所示:
npm install semantic-ui-css jsrender --save
jQuery将自动安装,因为它也被列为semantic-ui-css
的依赖semantic-ui-css
。 接下来,我们需要将代码放置在tasks/config/copy.js
。 此代码将指示Grunt将所需的JS和CSS文件从node_modules
到我们的assets/vendor
文件夹中。 整个文件应如下所示:
module.exports = function(grunt) {
grunt.config.set('copy', {
dev: {
files: [{
expand: true,
cwd: './assets',
src: ['**/*.!(coffee|less)'],
dest: '.tmp/public'
},
//Copy JQuery
{
expand: true,
cwd: './node_modules/jquery/dist/',
src: ['jquery.min.js'],
dest: './assets/vendor/jquery'
},
//Copy jsrender
{
expand: true,
cwd: './node_modules/jsrender/',
src: ['jsrender.js'],
dest: './assets/vendor/jsrender'
},
// copy semantic-ui CSS and JS files
{
expand: true,
cwd: './node_modules/semantic-ui-css/',
src: ['semantic.css', 'semantic.js'],
dest: './assets/vendor/semantic-ui'
},
//copy semantic-ui icon fonts
{
expand: true,
cwd: './node_modules/semantic-ui-css/themes',
src: ["*.*", "**/*.*"],
dest: './assets/vendor/semantic-ui/themes'
}]
},
build: {
files: [{
expand: true,
cwd: '.tmp/public',
src: ['**/*'],
dest: 'www'
}]
}
});
grunt.loadNpmTasks('grunt-contrib-copy');
};
将此行添加到assets/styles/importer.less
:
@import '../vendor/semantic-ui/semantic.css';
将JS文件添加到config/pipeline.js
:
// Vendor Dependencies
'vendor/jquery/jquery.min.js',
'vendor/semantic-ui/semantic.js',
'vendor/jsrender/jsrender.js',
最后,执行此命令以从node_modules
assets/vendor
文件夹中复制文件。 您只需对项目的每次全新安装都执行一次此操作:
grunt copy:dev
请记住将vendor
添加到您的.gitignore
。
测试依赖项安装
无论选择哪种方法,都需要确保正在加载所需的依赖项。 为此,将view/homepage.ejs
的代码替换为以下内容:
<h2 class="ui icon header">
<i class="settings icon"></i>
<div class="content">
Account Settings
<div class="sub header">Manage your account settings and set e-mail preferences.</div>
</div>
</h2>
保存文件后,进行sails lift
。 您的主页现在应如下所示:
重新启动应用程序后,请务必进行刷新。 如果缺少图标或字体看起来不对,请仔细检查步骤,然后查看错过的内容。 使用浏览器的控制台查看未加载的文件。 否则,请继续进行下一阶段。
创建视图
在进行项目开发时,我喜欢从用户界面入手。 我们将使用嵌入式JavaScript模板创建视图。 这是一个默认安装在每个Sails.js项目中的模板引擎。 但是,您应该知道它的功能有限并且已不再开发。
打开config/bootstrap.js
并插入此行,以便为我们的网页提供适当的标题。 将其放在cb()
语句之前的现有函数中:
sails.config.appName = "Sails Chat App";
您可以views/layout.ejs
来了解如何设置title
标签。 接下来,我们开始构建我们的主页UI。
主页设计
打开/views/homepage.ejs
并用以下代码替换现有代码:
<div class="banner">
<div class="ui segment teal inverted">
<h1 class="ui center aligned icon header">
<i class="chat icon"></i>
<div class="content">
<a href="/">Sails Chat</a>
<div class="sub header">Discuss your favorite technology with the community!</div>
</div>
</h1>
</div>
</div>
<div class="section">
<div class="ui three column grid">
<div class="column"></div>
<div class="column">
<div class="ui centered padded compact raised segment">
<h3>Sign Up or Sign In</h3>
<div class="ui divider"></div>
[TODO : Login Form goes here]
</div>
</div>
<div class="column"></div>
</div>
</div>
要了解以上代码中使用的UI元素,请参阅语义UI文档。 我在下面概述了确切的链接:
在assets/styles/theme.less
创建一个新文件,并粘贴以下内容:
.banner a {
color: #fff;
}
.centered {
margin-left: auto !important;
margin-right: auto !important;
margin-bottom: 30px !important;
}
.section {
margin-top: 30px;
}
.menu {
border-radius: 0 !important;
}
.note {
font-size: 11px;
color: #2185D0;
}
#chat-content {
height: 90%;
overflow-y: scroll;
}
这些都是我们将在项目中使用的自定义样式。 其余样式将来自Semantic UI
库。
接下来,更新assets/styles/importer.less
以包含我们刚刚创建的主题文件:
@import 'theme.less';
执行sails lift
。 您的项目现在应如下所示:
接下来,我们将看一下构建导航菜单。
导航菜单
由于它将被多个视图文件共享,因此将作为部分创建。 在views
文件夹中,创建一个名为partials
的文件夹。 然后创建文件views/partials/menu.ejs
并粘贴以下代码:
<div class="ui labeled icon inverted teal menu">
<a class="item" href="/chat">
<i class="chat icon"></i>
Chat Room
</a>
<a class="item" href="/profile">
<i class="user icon"></i>
Profile
</a>
<div class="right menu">
<a class="item" href="/auth/logout">
<i class="sign out icon"></i>
Logout
</a>
</div>
</div>
要了解上面的代码,只需参考菜单文档。
如果查看上面的代码,您会注意到我们已经为/chat
, /profile
和/auth/logout
创建了一个链接。 首先让我们创建profile
和chat room
的视图。
个人资料
创建文件view/profile.ejs
并粘贴以下代码:
<% include partials/menu %>
<div class="ui container">
<h1 class="ui centered header">Profile Updated!</h1>
<hr>
<div class="section">
[ TODO put user-form here]
</div>
</div>
现在,如果您已阅读链接的文档,则应该熟悉header
和grid
UI元素。 在文档的根部,您会注意到我们有一个container
元素。 (有关更多信息,请参见“ 容器”文档。
构建API后,我们将在稍后构建用户表单。 接下来,我们将为聊天室创建一个布局。
聊天室布局
聊天室将由三个部分组成:
- 聊天用户 - 用户列表
- 聊天消息 - 消息列表
- 聊天帖子 -用于发布新消息的表单。
创建views/chatroom.ejs
并粘贴以下代码:
<% include partials/menu %>
<div class="chat-section">
<div class="ui container grid">
<!-- Members List Section -->
<div class="four wide column">
[ TODO chat-users ]
</div>
<div class="twelve wide column">
<!-- Chat Messages -->
[ TODO chat-messages ]
<hr>
<!-- Chat Post -->
[ TODO chat-post ]
</div>
</div>
</div>
在查看页面之前,我们需要设置路由。
路由
打开config/routes.js
并像这样更新它:
'/': {
view: 'homepage'
},
'/profile': {
view: 'profile'
},
'/chat': {
view: 'chatroom'
}
Sails.js路由非常灵活。 有许多方法可以根据情况定义路由。 这是我们将URL映射到视图的最基本的版本。
启动您的Sails应用,或者如果它仍在后台运行,则刷新页面。 当前,主页和其他页面之间没有链接。 这是有意的,因为我们稍后将构建一个基本的身份验证系统,该系统会将登录的用户重定向到/chat
。 现在,使用浏览器的地址栏,并在末尾URL添加/chat
或/profile
。
在此阶段,您应该具有以上观点。 让我们开始创建API。
生成用户API
我们将使用Sails.js命令行实用程序生成我们的API。 我们需要为此步骤停止该应用程序:
sails generate api User
一秒钟后,我们收到消息“创建了一个新的api!”。 基本上,刚刚为我们创建了一个User.js
模型和一个UserController.js
。 让我们使用一些模型属性更新api/model/User.js
:
module.exports = {
attributes: {
name: {
type: 'string',
required: true
},
email: {
type: 'string',
required: true,
unique: true
},
avatar: {
type: 'string',
required: true,
defaultsTo: 'https://s.gravatar.com/avatar/e28f6f64608c970c663197d7fe1f5a59?s=60'
},
location: {
type: 'string',
required: false,
defaultsTo: ''
},
bio: {
type: 'string',
required: false,
defaultsTo:''
}
}
};
我相信上面的代码是不言自明的。 默认情况下,Sails.js使用本地磁盘数据库,该数据库基本上是位于.tmp
文件夹中的文件。 为了测试我们的应用程序,我们需要创建一些用户。 最简单的方法是安装sails-seed软件包 :
npm install sails-seed --save
安装后,您会发现已经为您创建了文件config/seeds.js
。 粘贴以下种子数据:
module.exports.seeds = {
user: [
{
name: 'John Wayne',
email: 'johnnie86@gmail.com',
avatar: 'https://randomuser.me/api/portraits/men/83.jpg',
location: 'Mombasa',
bio: 'Spends most of my time at the beach'
},
{
name: 'Peter Quinn',
email: 'peter.quinn@live.com',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
location: 'Langley',
bio: 'Rather not say'
},
{
name: 'Jane Eyre',
email: 'jane@hotmail.com',
avatar: 'https://randomuser.me/api/portraits/women/94.jpg',
location: 'London',
bio: 'Loves reading motivation books'
}
]
}
现在我们已经生成了一个API,我们应该在文件config/models.js
配置迁移策略:
migrate: 'drop'
Sails.js使用三种迁移策略来确定每次启动数据库时如何重建数据库:
- 安全 -不要迁移,我会手工完成
- alter —迁移但尝试保留现有数据
- 删除 —删除所有表并重建所有内容
我倾向于使用drop
进行开发,因为我经常进行很多迭代。 如果要保留现有数据,可以设置alter
。 尽管如此,我们的数据库每次都会填充种子数据。
现在,让我向您展示一些很棒的东西。 启动您的Sails项目并导航到地址/user
和/user/1
。
多亏了Sails.js Blueprints API ,我们有了功能齐全的CRUD API,而无需编写任何代码。 您可以使用Postman访问用户API并执行数据操作,例如创建,更新或删除用户。
现在,让我们继续构建配置文件表单。
个人资料表格
打开view/profile.ejs
并使用以下代码替换现有的TODO行:
<img class="ui small centered circular image" src="<%= data.avatar %>">
<div class="ui grid">
<form action="<%= '/user/update/'+ data.id %>" method="post" class="ui centered form">
<div class="field">
<label>Name</label>
<input type="text" name="name" value="<%= data.name %>">
</div>
<div class="field">
<label>Email</label>
<input type="text" name="email" value="<%= data.email %>">
</div>
<div class="field">
<label>Location</label>
<input type="text" name="location" value="<%= data.location %>">
</div>
<div class="field">
<label>Bio</label>
<textarea name="bio" rows="4" cols="40"><%= data.bio %></textarea>
</div>
<input type="hidden" name="avatar" value=<%=data.avatar %>>
<button class="ui right floated orange button" type="submit">Update</button>
</form>
</div>
我们正在使用Semantic-UI Form来构建表单界面。 如果检查表单的操作值/user/update/'+ data.id
,您将意识到我正在使用蓝图路由。 这意味着当用户单击“ Update
按钮时,将执行蓝图的更新操作。
但是,为了加载用户数据,我决定在用户控制器中定义一个自定义操作。 使用以下代码更新api/controllers/UserController
:
module.exports = {
render: async (request, response) => {
try {
let data = await User.findOne({
email: 'johnnie86@gmail.com'
});
if (!data) {
return response.notFound('The user was NOT found!');
}
response.view('profile', { data });
} catch (err) {
response.serverError(err);
}
}
};
在这段代码中,您会注意到我正在使用async/await
语法从数据库中获取用户数据。 替代方法是使用回调,这对于大多数开发人员而言并不清晰。 我还对默认用户帐户进行了硬编码以暂时加载。 稍后,当我们设置基本身份验证时,我们将对其进行更改以加载当前登录的用户。
最后,我们需要更改路由/profile
以开始使用新创建的UserController
。 打开config/routes
并更新配置文件路由,如下所示:
...
'/profile': {
controller: 'UserController',
action: 'render'
},
...
导航到URL /profile
,您应该具有以下视图:
尝试更改表单字段之一,然后单击“更新”按钮。 您将被带到此视图:
您会注意到该更新已生效,但显示的数据为JSON格式。 理想情况下,我们应该有一个仅查看个人资料页面views/user/findOne.ejs
并在更新个人资料页面/views/user/update.ejs
。 蓝图系统将猜测用于呈现信息的视图。 如果找不到视图,它将仅输出JSON。 现在,我们将仅使用此巧妙技巧。 创建文件/views/user/update.ejs
并粘贴以下代码:
<script type="text/javascript">
window.location = '/profile';
</script>
下次执行更新时,我们将被重定向到/profile
页面。 现在我们有了用户数据,我们可以创建要在views/chatroom.ejs
使用的views/partials/chat-users.js
文件。 创建文件后,粘贴以下代码:
<div class="ui basic segment">
<h3>Members</h3>
<hr>
<div id="users-content" class="ui middle aligned selection list"> </div>
</div>
// jsrender template
<script id="usersTemplate" type="text/x-jsrender">
<div class="item">
<img class="ui avatar image" src="{{:avatar}}">
<div class="content">
<div class="header">{{:name}}</div>
</div>
</div>
</script>
<script type="text/javascript">
function loadUsers() {
// Load existing users
io.socket.get('/user', function(users, response) {
renderChatUsers(users);
});
// Listen for new & updated users
io.socket.on('user', function(body) {
io.socket.get('/user', function(users, response) {
renderChatUsers(users);
});
});
}
function renderChatUsers(data) {
const template = $.templates('#usersTemplate');
let htmlOutput = template.render(data);
$('#users-content').html(htmlOutput);
}
</script>
对于此视图,我们需要一种客户端渲染方法来实时更新页面。 在这里,我们利用jsrender库,它是比EJS更强大的模板引擎。 jsrender
在于它既可以采用数组也可以采用单个对象文字,并且模板仍可以正确呈现。 如果要在ejs
执行此ejs
,则需要结合使用if
语句和for
循环来处理这两种情况。
让我解释一下客户端JavaScript代码的流程:
-
loadUsers()
。 首次加载页面时,我们使用Sails.js套接字库为用户执行GET
请求。 该请求将由Blueprint API处理。 然后,我们将接收到的数据传递给renderChatUsers(data)
函数。 - 仍然在
loadUsers()
函数中,我们使用io.socket.on
函数注册了一个侦听器。 我们侦听与模型user
有关的事件。 收到通知后,我们将再次获取用户并替换现有HTML输出。 -
renderChatUsers(data)
。 在这里,我们使用jQuerytemplates()
函数获取id为usersTemplate
的脚本。 注意类型是text/x-jsrender
。 通过指定自定义类型,浏览器将忽略并跳过该部分,因为它不知道它是什么。 然后,我们使用template.render()
函数将模板与数据合并。 此过程将生成HTML输出,然后我们将其输出并将其插入HTML文档。
我们在profile.ejs
中编写的模板已在Node服务器上呈现,然后作为HTML发送到浏览器。 对于chat-users
的情况,我们需要执行客户端渲染。 这将使聊天用户可以看到新用户加入该组,而无需刷新浏览器。
在测试代码之前,我们需要更新views/chatroom.ejs
以包括新创建的chat-users
部分。 用以下代码替换[ TODO chat-users ]
:
...html
<% include partials/chat-users.ejs %>
...
在同一文件中,在末尾添加此脚本:
<script type="text/javascript">
window.onload = function() {
loadUsers();
}
</script>
该脚本将调用loadUsers()
函数。 为了确认这是可行的,让我们进行sails lift
并导航到/chat
URL。
您的视图应该像上面的图像一样。 如果是这样,让我们继续构建Chatroom API。
ChatMessage API
与之前相同,我们将使用Sails.js生成API:
sails generate api ChatMessage
接下来,使用以下属性填充api/models/ChatMessage.js
:
module.exports = {
attributes: {
message: {
type: 'string',
required: true
},
createdBy : {
model: 'user',
required: true
}
}
};
注意,我们已经通过createdBy
属性声明了与User
模型的一对一关联。 接下来,我们需要用一些聊天消息填充磁盘数据库。 为此,我们将使用config/bootstrap.js
。 如下更新整个代码。 我们使用async/await
语法来简化代码并避免回调地狱:
module.exports.bootstrap = async function(cb) {
sails.config.appName = "Sails Chat App";
// Generate Chat Messages
try {
let messageCount = ChatMessage.count();
if(messageCount > 0){
return; // don't repeat messages
}
let users = await User.find();
if(users.length >= 3) {
console.log("Generating messages...")
let msg1 = await ChatMessage.create({
message: 'Hey Everyone! Welcome to the community!',
createdBy: users[1]
});
console.log("Created Chat Message: " + msg1.id);
let msg2 = await ChatMessage.create({
message: "How's it going?",
createdBy: users[2]
});
console.log("Created Chat Message: " + msg2.id);
let msg3 = await ChatMessage.create({
message: 'Super excited!',
createdBy: users[0]
});
console.log("Created Chat Message: " + msg3.id);
} else {
console.log('skipping message generation');
}
}catch(err){
console.error(err);
}
// It's very important to trigger this callback method when you're finished with Bootstrap! (Otherwise your server will never lift, since it's waiting on Bootstrap)
cb();
};
很棒的是,种子生成器在bootstrap.js
之前运行。 这样,我们可以确保首先创建了Users
数据,以便我们可以使用它来填充createdBy
字段。 拥有测试数据将使我们能够在构建用户界面时快速进行迭代。
聊天消息界面
继续并创建一个新文件views/partials/chat-messages.ejs
,然后放置以下代码:
<div class="ui basic segment" style="height: 70vh;">
<h3>Community Conversations</h3>
<hr>
<div id="chat-content" class="ui feed"> </div>
</div>
<script id="chatTemplate" type="text/x-jsrender">
<div class="event">
<div class="label">
<img src="{{:createdBy.avatar}}">
</div>
<div class="content">
<div class="summary">
<a href="#"> {{:createdBy.name}}</a> posted on
<div class="date">
{{:createdAt}}
</div>
</div>
<div class="extra text">
{{:message}}
</div>
</div>
</div>
</script>
<script type="text/javascript">
function loadMessages() {
// Load existing chat messages
io.socket.get('/chatMessage', function(messages, response) {
renderChatMessages(messages);
});
// Listen for new chat messages
io.socket.on('chatmessage', function(body) {
renderChatMessages(body.data);
});
}
function renderChatMessages(data) {
const chatContent = $('#chat-content');
const template = $.templates('#chatTemplate');
let htmlOutput = template.render(data);
chatContent.append(htmlOutput);
// automatically scroll downwards
const scrollHeight = chatContent.prop("scrollHeight");
chatContent.animate({ scrollTop: scrollHeight }, "slow");
}
</script>
这里的逻辑与chat-users
非常相似。 在侦听部分有一个关键区别。 代替替换渲染的输出,我们使用append。 然后,我们将滚动动画制作到列表的底部,以确保用户看到新的传入消息。
接下来,让我们更新chatroom.ejs
以包括新的chat-messages
部分,并更新脚本以调用loadMessages()
函数:
...
<!-- Chat Messages -->
<% include partials/chat-messages.ejs %>
...
<script type="text/javascript">
...
loadMessages();
...
</script>
您的视图现在应如下所示:
现在,让我们构建一个简单的表单,该表单将允许用户将消息发布到聊天室。
聊天帖子用户界面
创建一个新文件views/partial/chat-post.ejs
并粘贴以下代码:
<div class="ui basic segment">
<div class="ui form">
<div class="ui field">
<label>Post Message</label>
<textarea id="post-field" rows="2"></textarea>
</div>
<button id="post-btn" class="ui right floated large orange button" type="submit">Post</button>
</div>
<div id="post-err" class="ui tiny compact negative message" style="display:none;">
<p>Oops! Something went wrong.</p>
</div>
</div>
在这里,我们使用一个使用semantic-ui
元素来构建表单。 接下来,将此脚本添加到文件底部:
<script type="text/javascript">
function activateChat() {
const postField = $('#post-field');
const postButton = $('#post-btn');
const postErr = $('#post-err');
// Bind to click event
postButton.click(postMessage);
// Bind to enter key event
postField.keypress(function(e) {
var keycode = (e.keyCode ? e.keyCode : e.which);
if (keycode == '13') {
postMessage();
}
});
function postMessage() {
if(postField.val() == "") {
alert("Please type a message!");
} else {
let text = postField.val();
io.socket.post('/postMessage', { message: text }, function(resData, jwRes) {
if(jwRes.statusCode != 200) {
postErr.html("<p>" + resData.message +"</p>")
postErr.show();
} else {
postField.val(''); // clear input field
}
});
}
}
}
</script>
该脚本由两个功能组成:
activateChat()
。 此功能将发布按钮绑定到click事件,并将消息框(发布字段)绑定到按键(输入)事件。 当任何一个触发时,都会调用postMessage()
函数。postMessage
。 此功能首先进行快速验证,以确保帖子输入字段不为空。 如果在输入字段中提供了一条消息,我们将使用io.socket.post()
函数将一条消息发送回服务器。 在这里,我们使用经典的回调函数来处理服务器的响应。 如果发生错误,我们将显示错误消息。 如果我们收到200状态代码(表示已捕获该消息),则清除post输入字段,以准备输入下一条消息。
如果返回到chat-message
脚本,您将看到我们已经放置了用于检测和呈现传入消息的代码。 您还应该注意到io.socket.post()
将数据发送到URL /postMessage
。 这不是蓝图路线,而是自定义路线。 因此,我们需要为此编写代码。
转到api/controllers/UserController.js
并插入以下代码:
module.exports = {
postMessage: async (request, response) => {
// Make sure this is a socket request (not traditional HTTP)
if (!request.isSocket) {
return response.badRequest();
}
try {
let user = await User.findOne({email:'johnnie86@gmail.com'});
let msg = await ChatMessage.create({message:request.body.message, createdBy:user });
if(!msg.id) {
throw new Error('Message processing failed!');
}
msg.createdBy = user;
ChatMessage.publishCreate(msg);
} catch(err) {
return response.serverError(err);
}
return response.ok();
}
};
由于我们尚未设置基本身份验证,因此我们现在将用户johnnie86@gmail.com
硬编码为邮件的作者。 我们使用Model.create()
水线ORM函数来创建新记录。 这是一种无需我们编写SQL代码即可插入记录的理想方式。 接下来,我们向所有套接字发送一个notify事件,通知它们已经创建了新消息。 我们使用在Blueprints API中定义的ChatMessage.publishCreate()
函数进行此操作。 在发送消息之前,我们确保使用user
对象填充createdBy
字段。 chat-messages
部分使用它来访问头像和创建消息的用户的名称。
接下来,转到config/routes.js
,将/postMessage
URL映射到我们刚刚定义的postMessage
操作。 插入以下代码:
...
'/chat': {
view: 'chatroom'
}, // Add comma here
'/postMessage': {
controller: 'ChatMessageController',
action: 'postMessage'
}
...
打开views/chatroom.js
并包含部分chat-post
。 我们还将在loadMessages()
函数之后loadMessages()
调用activateChat()
loadMessages()
函数:
...
<% include partials/chat-messages.ejs %>
...
<script type="text/javascript">
...
activateChat();
...
</script>
刷新页面,然后尝试发送一些消息。
您现在应该拥有一个功能齐全的聊天系统。 查看项目源代码,以防万一。
基本认证
设置适当的身份验证和授权系统不在本教程的讨论范围之内。 因此,我们将为基本的无密码身份验证系统做准备。 让我们首先构建注册和登录表单。
登录/注册表格
创建一个新文件views/auth-form.ejs
并粘贴以下内容:
<form method="post" action="/auth/authenticate" class="ui form">
<div class="field">
<label>Full Names</label>
<input type="text" name="name" placeholder="Full Names" value="<%= typeof name != 'undefined' ? name : '' %>">
</div>
<div class="required field">
<label>Email</label>
<input type="email" name="email" placeholder="Email" value="<%= typeof email != 'undefined' ? email : '' %>">
</div>
<button class="ui teal button" type="submit" name="action" value="signup">Sign Up & Login</button>
<button class="ui blue button" type="submit" name="action" value="login">Login</button>
<p class="note">*Provide email only for Login</p>
</form>
<% if(typeof error != 'undefined') { %>
<div class="ui error message">
<div class="header"><%= error.title %></div>
<p><%= error.message %></p>
</div>
<% } %>
接下来打开views/homepage.ejs
并使用以下include语句替换TODO行:
...
<% include partials/auth-form.ejs %>
...
我们创建了一个表格,您可以通过输入名称和电子邮件来创建新帐户。 当您单击Signup & Login
,将创建一个新的用户记录并登录。但是,如果该电子邮件已被其他用户使用,则会显示一条错误消息。 如果您只想登录,请提供电子邮件地址,然后单击“ Login
按钮。 身份验证成功后,您将被重定向到/chat
URL。
现在,我刚才说的所有内容均无效。 我们需要实现该逻辑。 首先,让我们导航到/
地址,以确认auth-form
看起来不错。
政策
现在我们正在建立身份验证系统,我们需要保护/chat
和/profile
路由免受公共访问。 仅允许经过身份验证的用户访问它们。 打开config/policies.js
并插入以下代码:
ChatMessageController: {
'*': 'sessionAuth'
},
UserController: {
'*': 'sessionAuth'
},
通过指定控制器的名称,我们还有效地阻止了Blueprint API为用户和聊天消息提供的所有路由。 不幸的是,策略仅适用于控制器。 这意味着路由/chat
无法在其当前状态下受到保护。 我们需要为其定义一个自定义动作。 打开api/controller/ChatroomController.js
并插入以下代码:
...
render: (request, response) => {
return response.view('chatroom');
},
然后将/chat
的路由配置替换为一个config/routes.js
:
...
'/chat': {
controller: 'ChatMessageController',
action: 'render'
},
...
现在应该保护/chat
路由不受公共访问。 如果您重新启动应用程序并尝试访问/profile
, /chat
, /user
或/chatmessage
,则会看到以下禁止消息:
如果您想将用户重定向到登录表单,请转到api/policies/sessionAuth
,并将禁止的呼叫替换为这样的重定向呼叫:
...
// return res.forbidden('You are not permitted to perform this action.');
return res.redirect('/');
...
尝试再次访问禁止的页面,您将自动重定向到主页。 现在让我们实现注册和登录代码。
身份验证控制器和服务
您需要先停止Sails.js才能运行以下命令:
sails generate controller Auth
这将为我们创建一个空白的api/controllers/AuthController
。 打开它并插入以下代码:
authenticate: async (request, response) => {
// Sign up user
if(request.body.action == 'signup') {
// Validate signup form
// Check if email is registered
// Create new user
}
// Log in user
},
logout: (request, response) => {
// Logout user
}
我已经在注释中解释了逻辑将如何流动。 我们可以在此处放置相关代码。 但是,Sails.js建议我们使控制器代码保持简单易懂。 为此,我们需要编写帮助程序函数,以帮助我们完成上述所有注释任务。 要创建这些帮助器功能,我们需要创建一个服务。 为此,请创建一个新文件api/services/AuthService.js
。 插入以下代码:
/**
* AuthService.js
*
**/
const gravatar = require('gravatar')
// Where to display auth errors
const view = 'homepage';
module.exports = {
sendAuthError: (response, title, message, options) => {
options = options || {};
const { email, name} = options;
response.view(view, { error: {title, message}, email, name });
return false;
},
validateSignupForm: (request, response) => {
if(request.body.name == '') {
return AuthService.sendAuthError(response, 'Signup Failed!', "You must provide a name to sign up", {email:request.body.email});
} else if(request.body.email == '') {
return AuthService.sendAuthError(response, 'Signup Failed!', "You must provide an email address to sign up", {name:request.body.name});
}
return true;
},
checkDuplicateRegistration: async (request, response) => {
try {
let existingUser = await User.findOne({email:request.body.email});
if(existingUser) {
const options = {email:request.body.email, name:request.body.name};
return AuthService.sendAuthError(response, 'Duplicate Registration!', "The email provided has already been registered", options);
}
return true;
} catch (err) {
response.serverError(err);
return false;
}
},
registerUser: async (data, response) => {
try {
const {name, email} = data;
const avatar = gravatar.url(email, {s:200}, "https");
let newUser = await User.create({name, email, avatar});
// Let all sockets know a new user has been created
User.publishCreate(newUser);
return newUser;
} catch (err) {
response.serverError(err);
return false;
}
},
login: async (request, response) => {
try {
let user = await User.findOne({email:request.body.email});
if(user) { // Login Passed
request.session.userId = user.id;
request.session.authenticated = true;
return response.redirect('/chat');
} else { // Login Failed
return AuthService.sendAuthError(response, 'Login Failed!', "The email provided is not registered", {email:request.body.email});
}
} catch (err) {
return response.serverError(err);
}
},
logout: (request, response) => {
request.session.userId = null;
request.session.authenticated = false;
response.redirect('/');
}
}
仔细检查代码。 作为中级开发人员,您应该能够理解逻辑。 我在这里还没有做任何花哨的事情。 但是,我想提几件事:
-
墓碑。 您需要安装Gravatar。 这是一个JavaScript库,用于根据电子邮件地址生成Gravatar URL。
```bash npm install gravatar --save ```
User.publishCreate(newUser)
。 就像ChatMessages
一样,我们触发一个事件,通知所有套接字新创建的用户。 这将导致所有已登录的客户端重新获取用户数据。 查看views/partial/chat-users.js
看看我在说什么。request.session
。 Sails.js为我们提供了一个会话存储 ,我们可以使用该存储在页面请求之间传递数据。 默认的Sails.js会话位于内存中,这意味着如果停止服务器,则会话数据将丢失。 在AuthService
,我们使用会话来存储userId
和已authenticated
状态。
AuthService.js
的逻辑,我们可以继续使用以下代码更新api/controllers/AuthController
:
module.exports = {
authenticate: async (request, response) => {
const email = request.body.email;
if(request.body.action == 'signup') {
const name = request.body.name;
// Validate signup form
if(!AuthService.validateSignupForm(request, response)) {
return;
}
// Check if email is registered
const duplicateFound = await AuthService.checkDuplicateRegistration(request, response);
if(!duplicateFound) {
return;
}
// Create new user
const newUser = await AuthService.registerUser({name,email}, response);
if(!newUser) {
return;
}
}
// Attempt to log in
const success = await AuthService.login(request, response);
},
logout: (request, response) => {
AuthService.logout(request, response);
}
};
看看我们的控制器多么简单易读。 接下来,让我们做最后的润色。
最后的润色
现在我们已经设置了身份验证,我们应该删除在api/controllers/ChatMessageController
中的postMessage
操作中放置的硬编码值。 用以下代码替换电子邮件代码:
...
let user = await User.findOne({id:request.session.userId});
...
我想提一提您可能没有注意到的问题,如果您查看views/partials/menu.ejs
中的注销URL,我们将这个地址放置为/auth/logout
。 如果您查看config/routes.js
,您会注意到我们没有为它放置一个URL。 令人惊讶的是,当我们运行代码时,它可以工作。 这是因为Sails.js使用约定来确定解析特定地址所需的控制器和操作。
到现在为止,您应该已经可以使用MVP聊天功能了。 启动您的应用程序并测试以下方案:
- 注册而无需输入任何内容
- 仅填写姓名进行注册
- 通过仅填写电子邮件进行注册
- 通过填写姓名和注册电子邮件进行注册-例如,
johnnie86@gmail.com
或jane@hotmail.com
- 使用您的姓名和电子邮件注册
- 更新您的个人资料
- 尝试发布空白消息
- 发表一些消息
- 打开另一个浏览器并以另一个用户身份登录,将每个浏览器并排放置并聊天
- 注销并创建一个新帐户。
! 我们刚刚坐下来实施了很多功能,然后对其进行了测试。 再过几个星期,我们就可以开发出一个生产就绪的聊天系统,该系统集成了更多功能,例如多个聊天室,频道附件,笑脸图标和社交帐户集成!
摘要
在本教程中,我们并未将登录用户的名称放在顶部菜单中。 您应该能够自己解决此问题。 如果您已经阅读了整个教程,那么您现在应该精通使用Sails.js构建应用程序。
本教程的目的是向您展示可以从非JavaScript MVC框架中获得的知识,并使用相对较少的代码行来构建一些很棒的东西。 利用Blueprint API将帮助您更快地实现功能。 我还建议您学习集成功能更强大的前端库(例如React,Angular或Vue)以创建更具交互性的Web应用程序。 此外,学习如何为Sails.js编写测试以使测试过程自动化是您编程库中的重要武器。
翻译自: https://www.sitepoint.com/building-real-time-chat-app-sails-js/
sails.js