简介

本文是由nodejs+mongoose+websocket打造的一个即时聊天系统;本来打算开发一个类似于网页QQ类似功能的聊天系统,但是目前只是开发了一个模块功能 --- 类似群聊的,即一对多的聊天模式;因为时间关系,一对一私聊功能还没有开发,敬请期待!

该聊天室整个页面布局是通过bootstrap框架编写,可能很简陋,请大家多多包涵!

源码及作品

作品在线地址:http://chat.hawkzz.com

源码地址:https://github.com/zhuangZhou/chat.io

本地运行方法:

  • 命令下载:npm install
  • 启动node服务器:node app.js
  • 在浏览器中打开:http://localhost:8880

下面为效果图预览:

环境介绍

  • Windows 7 PC
  • nodejs
  • mongoDB
  • websocket
  • boostrap

准备工作

  • 安装node(废话,这肯定是必须的),安装地址:https://nodejs.org/en/
  • 安装mongoDB环境,地址:https://www.mongodb.com/download-center#community
  • 创建一个文件夹(这里你随意);
  • 初始化文件夹,使其变成一个node项目文件夹 npm init 或者 npm init -y;这里讲一下这两种的区别,npm init是初始化但是要自己选择初始化的条件,npm init -y则是默认选择初始化内容;
  • 创建app.js文件,作为整个项目的入口文件;
  • 安装整个项目的依赖,如下:
    1. express框架
    2. mongoose,是mongoDB的一个对象模型工具
    3. cookie-parser
    4. body-parser
    5. swig模板
    6. socket.io

开始工作##

express框架搭建

express是一个基于 Node.js 平台的极简、灵活的 web 应用开发框架,它提供一系列强大的特性,帮助你创建各种 Web 和移动设备应用。接下来我们在上面创建的app.js 里面搭建express框架的搭建

var express = require('experss');//引入express模块

var app =  new express();//实例化

app.listen(8880);//监听端口8880,这里可以自定义端口

根据以上,其实一个node服务器已经搭好了,通过命令‘node app.js’即可运行,地址是localhost:8880;这只是一个简单的服务器,想要加载页面以及页面交互,这些肯定是远远不够,接下来我们一步步开始;

首先,我们创建三个文件夹,分别为views,public,router;

  • views文件夹:存放html模板文件;
  • public文件夹:存放静态文件,如:js,css,image
  • router文件夹:存放页面的路由以及与数据库交互的文件;

然后,设置模板,以及静态文件托管,以及加载body-parser和cookie模块

var express = require('express');
var swig = require('swig');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser'); var app = new express(); var server = require('http').createServer(app); //静态文件托管
app.use('/public', express.static(__dirname + '/public')); //设置模板
app.engine('html', swig.renderFile);
app.set('views', './views');
app.set('view engine', 'html');
swig.setDefaults({
cache: false
}); //设置body-parser
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false})); //设置cookie
app.use(cookieParser()); //加载路由
app.use('/', require('./routers/index')); //首页
app.use('/login', require('./routers/login')); //登录页面
app.use('/register', require('./routers/register'));//注册页面
app.use('/exit', require('./routers/exit'));//退出 server.listen(8880, function () {
console.log('服务器连接成功!');
});

mongoDB搭建

首先,我们需要下载安装mongoDB环境,https://www.mongodb.com/download-center#community;

然后,创建一个文件夹database,存放本项目所需的mongoDB数据库文件;

接着,我们将mongoDB数据库的存放指向database;

1. 通过cmd找到mongoDB安装路径 -->  Server --> bin ;
2. 输入命令 mongod --dbpath E://chat/database ;
3. 回车;

这里,我们是使用mongoDB的一个对象模型工具--mongoose来对mongoDB数据库进行操作,所以,这里我们需要了解mongoose的机制;

mongoose是mongoDB的一个对象模型工具,是基于node-mongodb-native开发的mongoDB的nodejs驱动,可以在异步的环境下执行。同时它也是针对mongoDB操作的一个对象模型库,封装了mongoDB对文档的一些增删改查等常用方法,让nodejs操作mongoDB数据库变得更加容易。

如果要通过mongoose创建一个集合并对其进行增删改查,就需要用到Schema,Model;所以接下来我们创建两个文件夹Schema,Model来存储Schema和Model文件(这里我是把这两个分开存放,也可以写在一起);

Schema是一种以文件形式存储的数据库模型骨架,无法直接通往数据库端,也就是说它不具备对数据库的操作能力,仅仅只是数据库模型在程序片段中的一种表现,可以说是数据属性模型(传统意义的表结构),又或者是集合的模型骨架。基本属性类型有字符串、日期型、数值型、布尔型、null、数组、内嵌文档等。

Model由Schema构造生成的模型,除了Schema定义的数据库骨架以外,还具有数据库操作的行为,类似于管理数据属性、行为的类。

到目前为止,我们整个项目的骨架,以及所有目录结构已经搭建完成,如下图:

登录与注册

因为我们要求连接的用户首先要注册一个账号,并且这个账号的用户名是唯一的,不能与别人相同,方便用户区分,以及一些数据库操作;

为此在后台中,我们需要创建数据库连接,以及登录注册页面;

1.在app.js连接数据库

var  mongoose = require('mongoose');

...
...
... mongoose.connect('mongodb://localhost:27017/chat', function (err, data) {
if (err) {
console.log('数据库连接失败!');
} else {
server.listen(8880, function () {
console.log('服务器连接成功!');
}); }
});

这里我把数据库连接放在服务器监听外面,是为了当数据库启动后,再启动服务器,避免服务器启动,数据库连接不上,导致整个程序跑不动;

2.在Schema文件夹创建user.js文件,创建user的数据属性模型

const mongose = require('mongoose');

const user = mongose.Schema({
username: String, //用户姓名
password: String, //用户密码
image: String, //用户图像
state: Boolean //用户上学状态
}); module.exports = user;

3.在Model文件夹创建User.js 文件,创建user的对象模型

const mongoose = require('mongoose');
const userSchema = require('../schemas/user');
module.exports = mongoose.model('User', userSchema);

4.在views创建注册页面register.html,以及在router创建register.js

前台:

$('#register').on({
click: function () {
var username = $('#username').val();
var password = $('#password').val();
var rePassowrd = $('#rePassword').val();
if (username == '') {
alert('请填写用户名!');
return false;
}
var reg = /^[a-z0-9_-]{6,18}$/;
if (!reg.test(password)) {
alert('请填写6-12位密码!');
return false;
} if (password !== rePassowrd) {
alert('两次密码不一致!');
return false;
} $.post('/register/signUp', {username: username, password: password}, function (res) {
if (res.success == 1) {
location.href = 'login';
} else {
alert(res.err);
}
}, 'json')
}
});

后台:

const express = require('express');
const User = require('../models/User'); const router = express.Router(); router.get('/', function (req, res) {
res.render('register.html');
}); router.post('/signUp', function (req, res) {
var username = req.body.username; //获取前台传过来的数据
var password = req.body.password;
var resData = {};
User.findOne({
username: username
}).then(function (userInfo) {
if (userInfo) {
resData.success = 0;
resData.err = "该用户名已被注册!";
res.json(resData);//给前台返回数据状态
return false;
} else {
var user = new User({
username: username,
password: password,
image: '../public/img/people.png', //初始化图像
state: false //上线状态
});
return user.save();
}
}).then(function () {
resData.success = 1;
resData.message = "注册成功!";
res.json(resData);
})
}); module.exports = router;

5.在views创建注册页面login.html,以及在router创建login.js

由于登录页面和注册页面差不多,这里就不详细描述了,具体看代码;

主页面

主页面是整个页面的核心,它包括:在线用户统计,用户信息修改,以及聊天模块;

1.在线用户统计

在线用户统计这里可以分成三个小模块,分别是:在线用户展示,用户上线提醒,和用户离开提醒;

后台:


const server = require('http').createServer(app);
const io = require('socket.io').listen(server);
const User = require('./models/User'); io.on('connection', function (socket) {
socket.on('login', function (data) {//用户登录
var username = data.username;
socket.username = username;
User.find().then(function (data) {
for (var i = 0; i < data.length; i++) {
if (!data[i].state) {
data.splice(i, 1);
}
}
socket.emit('loginSuccess', data);
socket.broadcast.emit('user_list', data);
socket.broadcast.emit('userIn', username);
}); });
socket.on('disconnect', function (data) {//用户离开
var username = data.username || socket.username;
User.findOne({
username: username
}).then(function (userInfo) {
if (userInfo) {
return User.update({
_id: userInfo._id
}, {
state: false
})
}
}).then(function () {
return User.find();
}).then(function (data) {
for (var i = 0; i < data.length; i++) {
if (!data[i].state) {
data.splice(i, 1);
}
}
socket.broadcast.emit('user_list', data);
socket.broadcast.emit('userOut', username);
})
});
});

前台:

用户上线提醒

var username = $('#username').text();
var socket = io.connect('http://localhost:8880');
socket.emit('login', {username: username}); //有人加入
socket.on('userIn', function (data) {
var html = '<li class="tip"><div class="text-center">@ ' + data + ' @上线</div></li>';
$('#MsgList').append(html);
});

当用户登录成功,跳转到主页面后,前台通过io.connect()与服务器建立websocket连接,开始即时通信;并且同时通过socket.emit('login')向服务器发送用户上线的通知;

服务器通过socket.on('login')接收到前台发送的用户上线消息,然后通过 socket.broadcast.emit('userIn', username)向其他在线用户广播发送,该用户上线的消息;其他用户通过socket.on('userIn')接收消息,并向聊天模块上,添加提示;

用户离线提醒

前台:

//有人退出
socket.on('userOut', function (data) {
var html = '<li class="tip"><div class="text-center">@ ' + data + ' @离开</div></li>';
$('#MsgList').append(html);
}); //退出
$('#exitBtn').on('click', function () {
var username = $('#username').text();
location.href = 'exit';
});

在这里我把用户离线分为三种情况:一是,点击“退出”按钮;二是,关闭浏览器;三是,刷新该页面;但是不管是哪一种情况,只要页面改变了,都会触发服务器的socket.on('disconnect'),更改用户状态,然后服务器进行命令发送,这里和用户上线提醒的操作是一样的;

当在写刷新浏览器的时候,遇到了一个问题;我原本的想法是通过js监控浏览器的事件操作,然而发现这根本是不可能的,虽然在网上有很多帖子,但是通过实现都不能实现;

于是,我就想到了一个方法,当页面第一次通过login页面到主页面的时候,设置两个cookie,一个为user,来判断用户登录的;一个为flag,来判断页面已经加载过了;当用户刷新的时候,判断是否存在cookie-flag,如果存在,证明已经加载过了,就直接退出,用户离线;否则就是用户上线;这个方法可能不是很好,希望大神们有什么好方法,来解决这个问题;

后台:

index.js 主页面

router.get('/home', function (req, res) {
if (!req.cookies.user) {
res.redirect('/login');
} else {
if (!req.cookies.flag) {
User.findOne({
username: req.cookies.user
}).then(function (userInfo) {
res.render('home', {
username: userInfo.username,
image: userInfo.image
});
});
} else {
res.redirect('/exit');
} }
});

eixt.js 退出

router.get('/', function (req, res) {
User.update({
username: req.cookies.user
}, {
state: false
}).then(function () {
res.clearCookie('user');
res.clearCookie('flag');
res.redirect('/login');
});
});

在线用户展示

socket.on('loginSuccess', function (data) {
userUpdate(data);
}); //更新在线人数列表
socket.on('user_list', function (data) {
userUpdate(data);
});
function userUpdate(data) {
var len = data.length;
var str = '';
for (var i = 0; i < len; i++) {
str += '<li>';
str += '<img src="' + data[i].image + '" class="userImg">';
str += '<span>' + data[i].username + '</span>'
}
$('#peopleList').html(str);
$('#list-count span').html(len);
}

这里在线用户展示分两种情况,一是,用户刚刚登陆时,需要在侧边栏展示已在线用户;二是,当有新用户登陆时,其他用户更新在线用户;

第一种情况,在服务器收到socket.on('login')时,通过socket.emit('loginSuccess')向新用户发送在线用户;

第二种情况,在服务器收到socket.on('login')时,通过socket.broadcast.emit('user_list')向其他用户发送更新在线用户的命令;

无论是哪种情况,当服务器在发送命令之前,都需要通过User.find()查询在线用户的信息,才能将消息发送给用户;然而当用户收到命令时,虽然是不同的命令,但是所要做的操作是一样的,所以执行相同的方法userUpdate(),展示在侧边栏;

2.聊天模块

当没有写过之前,能够写个向qq一样能即时聊天的功能,感觉好高大上,真牛逼;但是当真正开始写过之后,就感觉也就那么一会事;

闲话不多说;聊天模块的功能无非就是:发送消息和接收消息;当然这里分了发送接收文字信息和图片;虽然分的是两种,但是操作是一样的,又由于我把接收的信息和自己发送信息的样式做了区别,所以我这里把发送信息和接收信息分别封装了两个方法meSendMsg(msg,n)和getMsg(msg, n);参数“msg”是聊天内容,“n”的值为0和1,分类代表发送的是文字消息和图片消息。

前台:

//发送消息
$('#sendBtn').on('click', function () {
var msg = $('#msgInput').val();
if (msg == '') {
alert('发送内容不能为空!');
return false;
}
var username = $('#username').text();
var img = $('#userImage').attr('src');
meSendMsg(msg, 0);
socket.emit('postNewMsg', {msg: msg, username: username, image: img});
$('#msgInput').val('');
}); //接收消息
socket.on('newMsg', function (data) {
getMsg(data, 0);
}); //发送照片
$('#addImage').on('click', function (e) {
var e = e || window.event;
e.stopPropagation();
$('#files').trigger('click');
}); function changeFiles(e) {
var e = e || window.event;
var files = e.target.files || e.dataTransfer.files;
var len = files.length;
if (len === 0) return false;
for (var i = 0; i < len; i++) {
var fs = new FileReader();
fs.readAsDataURL(files[i]);
fs.onload = function () {
var username = $('#username').text();
var img = $('#userImage').attr('src');
socket.emit('postImg', {imgData: this.result, username: username, image: img});
meSendMsg(this.result, 1);
}
}
} //接收照片
socket.on('newImg', function (data) {
getMsg(data, 1);
});
//自己发送消息
function meSendMsg(msg, n) {
var src = $('#userImage').attr('src');
var name = $('#username').text();
var html = ' <li class="me">';
html += '<div class="row">';
html += ' <div class="userInfo col-sm-1 col-md-1 pull-right">';
html += '<img src="' + src + '" style="width: 100%;margin-bottom: 5px">';
html += '<p class="text-center">' + name + '</p>';
html += '</div>';
html += '<div class="msgInfo col-sm-5 col-md-5 pull-right">';
if (n == 0) {
html += msg;
} else if (n == 1) {
html += '<img src="' + msg + '" alt="" style="max-width:100%; ">';
} html += '</div></div></li>';
$('#MsgList').append(html);
var Li = $('#MsgList li');
var len = Li.length;
var LiH = Li.eq(len - 1).height();
var h = document.getElementById('MsgList').scrollHeight;
document.getElementById('MsgList').scrollTop = h + LiH;
} //接收消息
function getMsg(data, n) {
var html = ' <li >';
html += '<div class="row">';
html += ' <div class="userInfo col-sm-1 col-md-1">';
html += '<img src="' + data.image + '" style="width: 100%;margin-bottom: 5px">';
html += '<p class="text-center">' + data.username + '</p>';
html += '</div>';
html += '<div class="msgInfo col-sm-5 col-md-5 ">';
if (n == 0) {
html += data.msg;
} else if (n == 1) {
html += '<img src="' + data.imgData + '" alt="" style="max-width:100%; ">';
}
html += '</div></div></li>';
$('#MsgList').append(html);
var Li = $('#MsgList li');
var len = Li.length;
var LiH = Li.eq(len - 1).height();
var h = document.getElementById('MsgList').scrollHeight;
document.getElementById('MsgList').scrollTop = h + LiH;
}

后台:

 socket.on('postNewMsg', function (data) {//接收到新消息
socket.broadcast.emit('newMsg', data);
});
socket.on('postImg', function (data) {//接收到图片
socket.broadcast.emit('newImg', data);
});

在这里,我来说说图片上传和发送;图片不同于文字的传递,但是如果将图片转化为字符串形式后,便可以像发送普通文字消息一样发送图片了,只是在展示的时候将其还原为图片就行;

在这之前,我们已经将图片按钮在页面放好了,其实是一个文件类型的input,下面只需在它身上做功夫便可。

用户点击图片按钮后,弹出文件选择窗口供用户选择图片。之后我们可以在JavaScript代码中使用FileReader来将图片读取为base64格式的字符串形式进行发送。而base64格式的图片直接可以指定为图片的src,这样就可以将图片用img标签显示在页面了。

为此我们监听图片按钮的change事件,一但用户选择了图片,便显示到自己的屏幕上同时读取为文本发送到服务器。

//发送照片
$('#addImage').on('click', function (e) {
var e = e || window.event;
e.stopPropagation();
$('#files').trigger('click');
}); function changeFiles(e) {
var e = e || window.event;
var files = e.target.files || e.dataTransfer.files;
var len = files.length;
if (len === 0) return false;
for (var i = 0; i < len; i++) {
var fs = new FileReader();
fs.readAsDataURL(files[i]);
fs.onload = function () {
var username = $('#username').text();
var img = $('#userImage').attr('src');
socket.emit('postImg', {imgData: this.result, username: username, image: img});
meSendMsg(this.result, 1);
}
}
}

3.用户修改信息

用户修改信息用到的方法,在上面的编写中都用到了;比如:图片上传和发送,用户列表的更新等,这里我就不详细讲了,大家看代码吧,有什么问题请及时call我;

前台:

//修改信息
$('#editImage').on('click', function (e) {
var e = e || window.event;
e.stopPropagation();
$('#fileImg').trigger('click');
}); $('#editBtn').on('click', function () {
var newName = $('#newName').val();
var newImage = $('#editImage').attr('src');
$('#userImage').attr('src', newImage);
$('#username').html(newName);
$('#changeInfo').modal('hide');
socket.emit('edit', {newName: newName, newImage: newImage, username: username});
});
function editImageFn(e) {
var e = e || window.event;
var files = e.target.files || e.dataTransfer.files;
var fs = new FileReader();
fs.readAsDataURL(files[0]);
fs.onload = function () {
$('#editImage').attr('src', this.result);
}
}

后台:

socket.on('edit', function (data) {
var username = data.username || socket.username;
User.findOne({
username: username
}).then(function (userInfo) {
return User.update({
_id: userInfo._id
}, {
username: data.newName,
image: data.newImage
})
}).then(function () {
socket.username = data.newName;
return User.find();
}).then(function (data) {
for (var i = 0; i < data.length; i++) {
if (!data[i].state) {
data.splice(i, 1);
}
}
socket.emit('user_list', data);
socket.broadcast.emit('user_list', data);
});
})

小结

到此为止,一个简单的即时聊天室完成了;其实,做这个聊天室的时候,有很多设想,比如发送表情和一对一聊天等,这些都是在其中,虽然现在还没有实现,但是请敬请期待;或者你们自己已经实现了,大家可以交流交流;

在这里说说为什么叫“xxx聊天室”,是因为本人语言能力实在是比较差,想不出什么好名字来(哈哈),大家有什么好名字可以自己添加;

本文可能很多地方用词用句都不是很恰当,也有很多地方讲解不清楚,请大家多多原谅,本人的语言表达能力实在是不怎么样,在以后的文章中,会改善的;

原文: http://blog.hawkzz.com/2017/06/01/xxx聊天室/  作者: hawk_zz

nodejs+mongoose+websocket搭建xxx聊天室的更多相关文章

  1. websocket搭建的聊天室

    在前后端数据交互的时候我们经常使用的是ajax,用的是传统的http协议,而http协议有个致命的缺点,就是请求一结束,连接就断开了, 我们为了保持这个链接的,通常会使用cookie,而自从h5出现w ...

  2. php+websocket搭建简易聊天室实践

    1.前言 公司游戏里面有个简单的聊天室,了解了之后才知道是node+websocket做的,想想php也来做个简单的聊天室.于是搜集各种资料看文档.找实例自己也写了个简单的聊天室. http连接分为短 ...

  3. 使用Html5下WebSocket搭建简易聊天室

    一.Html5WebSocket介绍 WebSocket protocol 是HTML5一种新的协议(protocol).它是实现了浏览器与服务器全双工通信(full-duplex). 现在,很多网站 ...

  4. Java和WebSocket开发网页聊天室

    小编心语:咳咳咳,今天又是聊天室,到现在为止小编已经分享了不下两个了,这一次跟之前的又不大相同,这一次是网页聊天室,具体怎么着,还请各位看官往下看~ Java和WebSocket开发网页聊天室 一.项 ...

  5. SpringBoot 搭建简单聊天室

    SpringBoot 搭建简单聊天室(queue 点对点) 1.引用 SpringBoot 搭建 WebSocket 链接 https://www.cnblogs.com/yi1036943655/p ...

  6. 基于Node.js + WebSocket 的简易聊天室

    代码地址如下:http://www.demodashi.com/demo/13282.html Node.js聊天室运行说明 Node.js的本质就是运行在服务端的JavaScript.Node.js ...

  7. SilverLight搭建WCF聊天室详细过程[转]

    http://www.silverlightchina.net/html/zhuantixilie/getstart/2011/0424/7148.html 默认节点 SilverLight搭建WCF ...

  8. 分享基于 websocket 网页端聊天室

    博客地址:https://ainyi.com/67 有一个月没有写博客了,也是因为年前需求多.回家过春节的原因,现在返回北京的第二天,想想,应该也要分享技术专题的博客了!! 主题 基于 websock ...

  9. workerman-chat(PHP开发的基于Websocket协议的聊天室框架)(thinkphp也是支持socket聊天的)

    workerman-chat(PHP开发的基于Websocket协议的聊天室框架)(thinkphp也是支持socket聊天的) 一.总结 1.下面链接里面还有一个来聊的php聊天室源码可以学习 2. ...

随机推荐

  1. win10 UWP 单元测试

    我们在写代码的时候不能保证我们写出来的代码是正确的,所以我们经常要单元测试. 单元测试和重构都是在做完一个小小函数一般就要进行一次,越早做就越好,可以比较早发现问题,这时我们还记得我们写的内容,不过比 ...

  2. 恶意软件Mirai换了个马甲 瞄上我国2亿多台IoT设备

    恶意软件Mirai换了个马甲 瞄上我国2亿多台IoT设备   想要起来时,一种沉重感阻碍着他,这是一种安全感:感觉到一张床为他铺好了,而且只属于他:想要静卧时,一种不安阻碍着他,把他从床上赶起来,这是 ...

  3. [ACdream]女神教你字符串——违和感

    题目描述: 女神最喜欢字符串了,字符串神马的最有爱了. 女神是一个重度强迫症患者,面对不是对称的东西,她会觉得太违和了,就会爆炸.所以她手上的字符串都是回文的,像什么a,b,aabaa,abcba,上 ...

  4. Javascript里的if判断与逻辑运算符(||, &&)和比较运算符的特别之处

    写JS时不可避免要用到 if 与 逻辑运算符( ||, &&). 如果你经常阅读Js的第三方组件源码, 会发现有一种写法作为初始化变量的方法经常出现. var variable = v ...

  5. Yii2之事件

    众所周知,yii的三大特性是:属性.事件.行为,上一篇博文简单讲解了yii中的属性,本文接着讲讲yii的事件. 事件是代码解耦的一种方式,设计业务流程的一种模式.在yii2.0中,通过Yii\base ...

  6. LeetCode 594. Longest Harmonious Subsequence (最长的协调子序列)

    We define a harmonious array is an array where the difference between its maximum value and its mini ...

  7. LeetCode 191. Number of 1 bits (位1的数量)

    Write a function that takes an unsigned integer and returns the number of ’1' bits it has (also know ...

  8. 【JAVA零基础入门系列】Day11 Java中的类和对象

    今天要说的是Java中两个非常重要的概念--类和对象. 什么是类,什么又是对象呢?类是对特定集合的概括描述,比如,人,这个类,外观特征上,有名字,有年龄,能说话,能吃饭等等,这是我们作为人类的相同特征 ...

  9. javascript的一些算法的实用小技巧

    一.交换两个数字的值 我们交换两个数字的值想到的方法一般就是用一个新的变变量,让他把一个数存起来,然后在交换两个数字的值,看下面这种. var a = 1, b = 2; //交换两个数字的值 var ...

  10. Golang访问Redis初体验

    go语言的client在redis官网上有很多l客户端,个人感觉redigo使用起来更人性化,重要的是源代码结构很清晰,重要的是支持管道.发布和订阅.连接池等等,所以我选择redigo作为尝试. 1. ...