ساخت یک برنامه چت Real-time با Sails.js | بخش دوم
در بخش قبل، تا مرحله تولید API کاربر پیش رفتیم. در این بخش نیز فرم پروفایل را ساخته، و باقی مراحل را پیش می رویم.
فرم پروفایل
فایل view/profile.ejs را باز کنید و کد موجود را با این کد جایگزین کنید:
<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 را معاینه کنید، میبینید که من در حال استفاده از Blueprint Route هستم. این به این معنی است که وقتی یک کاربر دکمه Update را می فشارد، اکشن update در Blueprint اجرا می شود.
گرچه، برای بارگذاری داده های کاربر، تصمیم گرفتم که یک اکشن سفارشی در User Controller تعریف کنم. فایل 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 برای گرفتن داده های کاربر از دیتابیس هستم. راه دوم، استفاده از پشتیبان ها (Callbacks) است، که برای بیشتر توسعه دهندگان خوانا نیست. همچنین حساب کاربری پیشفرض را به گونه ای قرار داده ام که به طور موقت بارگذاری شود. بعدا که ما احراز هویت پایه را راه اندازی می کنیم، آن را تغییر می دهیم تا کاربری که در حال حاضر وارد شده است را بارگذاری کند.
در آخر، باید Route موجود یعنی /profile را تغییر دهیم تا شروع به استفاده از UserController کند. شاخه config/routes را باز کنید و Route پروفایل را به این صورت به روز رسانی کنید:
...
'/profile': {
controller: 'UserController',
action: 'render'
},
...
به URL پروفایل بروید. باید چنین صفحه ای را مشاهده کنید:
سعی کنید یکی از فیلد های فرم را تغییر دهید و دکمه Update را بفشارید. میبینید که به این صفحه منتقل می شوید:
در اینجا می توانید ببینید که Update کار می کند، اما داده هایی که نشان داده می شوند با فرمت JSON هستند. به طور ایده آل، ما باید یک صفحه پروفایل view-only در views/user/findOne.ejs داشته باشیم، و یک صفحه به روز رسانی پروفایل در views/user/update.ejs. سیستم Blueprint برای رندر کردن اطلاعات از view ها کمک می گیرد. اگر نتواند view ها را بیابد، فقط فایل JSON را خروجی می دهد. فعلا، از همین روش استفاده می کنیم. فایل views/user/update.ejs را بسازید و این کد را در آن قرار دهید:
<script type="text/javascript">
window.location = '/profile';
</script>
بار بعدی که یک به روز رسانی انجام دهیم، به صفحه /profile منتقل می شویم. حال که داده های کاربر خود را داریم، می توانیم فایل views/partials/chat-users.js را بسازیم تا در vies/chatroom.ejs استفاده شود. پس از این که فایل را ساختید، این کد را در آن قرار دهید:
<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>
برای این view، به یک رندر سمت کاربر نیاز داریم تا به روز رسانی به صورت real-time انجام شود. در اینجا از کتابخانه jsrender، یک موتور الگوسازی قدرتمند تر در EJS استفاده می کنیم. نکته مثبت در jsrender این است که هم می تواند یک آرایه آبجکت را بگیرد، و هم این که یک آبجکت تنها را بگیرد، و همچنان الگو به طور صحیح رندر می شود. اگر می خواستید این کار را در ejs انجام دهید، باید یک if را با یک حلقه for ترکیب می کردیم تا هر دو حالت را بتوانند بگذرانند.
حال بگذارید جریان سمت کاربر کد جاوا اسکریپتمان را توضیح دهم :
۱- loadUser(). در ابتدا که صفحه بارگذاری می شود، از کتابخانه Sails.js socket استفاده می کنیم تا یک درخواست GET را برای کاربر انجام دهد. این درخواست توسط Blueprint API انجام می شود. بعد از آن نیز به داده های رسیده شده به تابع renderChatUsers(data) رسیدگی می کنیم.
۲- هنوز داخل تابع loadUsers()، یک listener را با استفاده از تابع io.socket.on ثبت می کنیم، و منتظر رویداد های مربوط به مدل user می مانیم. وقتی که به ما اطلاع رسانی می شود، دوباره کاربران را دریافت می کنیم و با خروجی HTML موجود جایگزین می کنیم.
۳- renderChatUsers(data). در اینجا، با آیدی usersTemplate و با استفاده از تابع jQuery به نام templates() یک اسکریپت را دریافت می کنیم. دقت کنید که نوع آن text/x-jsrender است. اگر یک نوع دستی تعیین کنیم، از آنجایی که مرورگر نمی داند آن نوع چیست، این بخش را نادیده گرفته و رد می کند. سپس از تابع template.render() برای ادغام الگو و داده ها استفاده می کنیم. این فرایند یک خروجی HTML تولید می کند که ما بعدا آن را دریافت کرده، و وارد سند HTML می کنیم.
الگویی که در profile.ejs نوشتیم، بر روی سرور Node رندر شد، و سپس به عنوان یک HTML به مرورگر ارسال شد. در مورد chat-users، باید رندر سمت کاربر انجام دهیم. این به کاربران چت اجازه می دهد که پیوستن کاربران جدید را بدون Refresh کردن مرورگر ببینند.
قبل از این که کد را آزمایش کنیم، باید فایل views/chatroom.ejs را به روز رسانی کنیم تا شامل partial جدید chat-users شود. [ TODO Chat-users ] را با این کد جایگزین کنید:
...html
<% include partials/chat-users.ejs %>
...
در همین فایل، این اسکریپت را در آخر وارد کنید:
<script type="text/javascript">
window.onload = function() {
loadUsers();
}
</script>
این اسکریپت تابع loadUsers() را فراخوانی می کند. برای مطمئن شدن از کار کردن این اسکریپت، بیایید یک sails lift اجرا کنیم و به URL چت برویم.
باید چیزی مانند عکس بالا ببینید. اگر همینطور است، بیایید به سراغ ساخت API چت روم برویم.
ChatMessage API
به مانند قبل، از Sails.js برای تولید API استفاده می کنیم:
sails generate api ChatMessage
سپس، api/models/ChatMessages.js را با این صفات پر کنید:
module.exports = {
attributes: {
message: {
type: 'string',
required: true
},
createdBy : {
model: 'user',
required: true
}
}
};
دقت کنید که بین مدل User و صفت createdBy یک ارتباط ایجاد کرده ایم. سپس باید دیتابیس خود را با چند پیام پر کنیم. برای این کار، از config/bootstrap.js استفاده می کنیم. تمام کد را به صورت زیر به روز رسانی کنید. ما در حال استفاده از سینتکس async/awaits هستیم تا کد خود را ساده تر کنیم:
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();
};
نکته مثبت این است که Seeds generator قبل از 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 است. تنها یک تفاوت کلیدی در بخش listen وجود دارد. به جای جایگزینی خروجی رندر شده، از append (اضافه کردن) استفاده می کنیم. سپس یک انیمیشن scroll به پایین لیست انجام می دهیم تا مطمئن شویم که کاربران پیام های ورودی را می بینند.
بعد 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(). این تابع دکمه پست را به یک رویداد کلیک، و Message box را به یک رویداد Key Press (کلید Enter) متصل می کند. هرکدام که صورت گیرند، تابع postMessage() فراخوانی می شود.
- postMessage. این تابع در ابتدا یک اعتبار سنجی سریع انجام می دهد تا مطمئن شود که فیلد ورودی خالی نیست. اگر پیامی در فیلد وجود دارد، از تابع io.socket.post() استفاده برای ارسال یک پیام به سرور استفاده می کنیم. در اینجا، از یک تابع پشتیبان کلاسیک برای رسیدگی به پاسخ سرور استفاده می کنیم. اگر خطایی بروز دهد، آن خطا را نشان می دهیم. اگر یک کد ۲۰۰ دریافت کنیم، که به معنای رسیدن پیام است، فیلد ورودی را خالی می کنیم و آن را برای وارد کردن پیام بعدی آماده می کنیم.
اگر به اسکریپت chat-message برگردید، می بینید که از قبل کد مورد نیاز برای تشخیص و رندر پیام ورودی را قرار داده ایم. همچنین باید متوجه شده باشید که io.socket.post() در حال ارسال داده ها به یو آر ال /postMessage است. این یک Blueprint Route نیست، بلکه یک Route دستی است. از این رو، باید برای آن کد را بنویسیم.
به فایل 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();
}
};
از آنجایی که احراز هویت پایه را راه اندازی نکرده ایم، فعلا کاربر johnnie۸۶@gmail.com را به عنوان نویسنده پیام ها در نظر می گیریم. حال از تابع Model.create() Waterline ORM برای ساخت یک رکورد جدید استفاده می کنیم. این یک راه جالب برای وارد کردن رکورد ها بدون نوشتن کد SQL است. سپس یک رویداد اطلاع رسانی به تمام socket ها ارسال می کنیم و به آن ها می گوییم که پیام جدیدی وارد شده است. این کار را با استفاده از تابع ChatMessage.publishCreate() که در Blueprint API تعریف شده است، انجام می دهیم. قبل از ارسال پیام، مطمئن می شویم که فیلد createdBy با یک آبجکت user پر شده است. این توسط chat-message استفاده می شود تا به آواتار و نام کاربری که پیام را ایجاد کرد دسترسی پیدا کند.
در قدم بعدی، به فایل config/routes.js بروید و یو آر ال /postMessage را به اکشن postMessage که تعریف کردیم، مپ کنید. این کد را وارد کنید:
...
'/chat': {
view: 'chatroom'
}, // Add comma here
'/postMessage': {
controller: 'ChatMessageController',
action: 'postMessage'
}
...
فایل views/chatroom.js را باز کنید و chat-post را در آن وارد کنید. همچنین تابع 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>
<% } %>
حال فایل vies/homepage.ejs را باز کنید و خط TODO را با این خط جایگزین کنید:
ما فرمی ساخته ایم که با ارائه ورودی هایی برای نام و ایمیل به شما اجازه می دهد حساب جدیدی بسازید. وقتی که بر روی Signup & Login کلیک می کنید، یک رکورد کاربر جدید ساخته می شود و شما وارد می شوید. گرچه، اگر ایمیل وارد شده از قبل توسط کاربر دیگری وارد شده باشد، یک پیام خطا نشان داده می شود. اگر فقط می خواهید وارد شوید، ایمیل را وارد کرده و بر روی Login کلیک کنید. اگر احراز هویت موفق باشد، شما به یو آر ال /chat منتقل می شوید.
در حال حاضر، هیچ یک از مواردی که گفتم کار نمی کنند. زیرا باید منطق مورد نیاز را پیاده سازی کنیم. ابتدا به صفحه اصلی بروید و مطمئن شوید که فرم احراز هویت درست است.
سیاست (Policy)
حال که در حال راه اندازی یک سیستم احراز هویت هستیم، باید از Route های /chat و /profile مراقبت کنیم تا از دسترس عموم خارج باشند. فقط کاربران احراز هویت شده باید بتوانند به آن ها دسترسی داشته باشند. فایل config/policies.js را باز کنید و این کد را در آن وارد کنید:
ChatMessageController: {
'*': 'sessionAuth'
},
UserController: {
'*': 'sessionAuth'
},
با تعیین نام کنترلر، تمام Route های فراهم شده توسط Bluerint API برای کاربران و پیام های چت را مسدود کرده ایم. متاسفانه، policy ها فقط با کنترلر ها کار می کنند. این به این معنی است که Route (مسیر) /chat در حالت فعلی اش نمی تواند حفاظت شود. باید یک اکشن سفارشی برای آن تعریف کنیم. فایل api/controller/ChatroomController.js را باز کنید و این کد را در آن وارد کنید:
...
render: (request, response) => {
return response.view('chatroom');
},
سپس Route config را برای /chat به این کد جایگزین کنید:
...
'/chat': {
controller: 'ChatMessageController',
action: 'render'
},
...
حال Route چت باید از دسترس عموم محافظت شده باشد. اگر برنامه را ری استارت کنید و سعی کنید به /profile، /chat، /user یا /chatmessage دسترسی داشته باشید، با این پیام مواجه می شوید:
اگر می خواهید در عوض کاربران را به صفحه ورود منتقل کنید، به api/policies/sessionAuth بروید و این پیام را با یک Redirect به این صورت جایگزین کنید:
...
// return res.forbidden('You are not permitted to perform this action.');
return res.redirect('/');
...
اگر سعی کنید دوباره به صفحات دسترسی داشته باشید، میبینید که به طور خودکار به صفحه خانه منتقل می شوید.
در بخش بعد، با پیاده سازی کد ثبت نام و ورود شروع می کنیم.
- ۹۷/۰۳/۲۰