最近,在向大学生们介绍 HTML5 的时候,我想要对他们进行问卷调查,并向他们显示实时更新的投票结果。鉴于此目的,我决定快速构建一个用于此目的的问卷调查应用程序。我想要一个简单的架构,不需要太多不同的语言和框架。因此,我决定对所有一切都使用 JavaScript — 对服务器端使用 Node.js 和 Express,对数据库使用 MongoDB,对前端用户界面使用 AngularJS。
这个 MEAN 堆栈(Mongo、Express、Angular 和 Node)只需要一天即可完成,远比 Web 应用程序开发和部署所用的 LAMP 堆栈(Linux、Apache、MySQL 和 PHP)简单得多。
我选择使用 JazzHub 来管理我的项目的源代码。它不仅为我的代码提供了一个完整的版本控制系统,还为在云中编辑代码提供了一个在线 IDE,以及用于项目管理的敏捷特性。JazzHub 很容易与 Eclipse 相集成,Eclipse 还提供了一些插件,支持对平台( 比如 BlueMix 或 Cloud Foundry)的一键式部署。
构建该应用程序的先决条件
- 基本了解 Node.js 和 Node.js 开发环境
- 具有以下这些 Node.js 模块:Express framework、Jade、Mongoose 和 socket.io
- AngularJS JavaScript 框架
- MongoDB NoSQL 数据库
- Eclipse IDE,已安装了 Nodeclipse 插件
第 1 步. 构建一个基础 Express 后台
在 Eclipse 中,切换到 Node 透视图,并创建一个新的 Node Express 项目。如果您创建了一个 JazzHub 项目,请像我所做的那样,使用相同的名称为您的 Node Express 项目命名。选择使用 Jade 作为模板引擎。Eclipse 会自动下载所需的 npm 模块,以便创建一个简单 Express 应用程序。
运行这个 Express 应用程序
在 Project Explorer 中,找到位于您项目的根目录中的 app.js,右键单击并选择 Run As > Node Application。这将启动一个 Web 服务器并将应用程序部署到该服务器。 接下来,打开浏览器并导航到 http://localhost:3000。
图 1. Starter Express 应用程序
配置基础前端
这个问卷调查应用程序对常见用户界面和布局使用了 Bootstrap 框架。现在,让我们对 Express 应用程序做一些改动来反映这一点。首先,打开 routes/index.js,将标题属性更改为 Polls
:
清单 1. routes/index.js
exports.index = function(req, res){ res.render('index', { title: 'Polls' }); };
接着,更改 views/index.jade 模板以包含 Bootstrap。Jade 是一种速记模板语言,可编译成 HTML。它使用缩进消除了对结束标签的需求,极大地缩小了模板的大小。您只需要使用 Jade 作为主页面布局即可。在下一步中,还可以使用 Angular 局部模板向这个页面添加功能。
清单 2. views/index.jade
doctype 5 html(lang='en') head meta(charset='utf-8') meta(name='viewport', content='width=device-width, initial-scale=1, user-scalable=no') title= title link(rel='stylesheet', href='//netdna.bootstrapcdn.com/bootstrap/3.0.1/ css/bootstrap.min.css') link(rel='stylesheet', href='/stylesheets/style.css') body nav.navbar.navbar-inverse.navbar-fixed-top(role='navigation') div.navbar-header a.navbar-brand(href='#/polls')= title div.container div
想要查看对您的应用程序所做的更改,请结束 Eclipse 中的 Web 服务器进程,再次运行 app.js 文件:
图 2. 问卷调查应用程序样板文件
注意:在使用 Jade 模板时,注意适当缩进您的代码,否则您会遇到麻烦。另外,还要避免使用混合缩进样式,如果您尝试这样做,Jade 将会报错。
第 2 步. 使用 AngularJS 提供前端用户体验
如果要使用 Angular,首先需要在您的 HTML 页面中包含它,还需要在 HTML 页面中添加一些指令。在 views/index.jade 模板中,对 html
元素进行如下更改:html(lang='en', ng-app='polls')
。
在该文件的标头中添加以下脚本元素: :
清单 3. 将脚本元素加载到 Angular 和 Angular Resource 模板
script(src='//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js') script(src='//ajax.googleapis.com/ajax/libs/angularjs/1.0.8 /angular-resource.min.js')
接下来,更改模板中的 body
元素,添加一个 ng-controller
属性(稍后使用该属性将用户界面绑定到控制器逻辑代码中):body(ng-controller='PollListCtrl')
。
最后,更改模板中的最后一个 div
元素,以便包含一个 ng-view
属性: div(ng-view)
。
构建 Angular 模块
在默认情况下,Express 发布了静态资源,比如 JavaScript 源文件、CSS 样式表以及位于您项目的公共目录中的图像。在公共目录中,创建一个名为 javascripts 的新的子目录。在这个子目录中,创建一个名为 app.js 的文件。该文件将包含用于应用程序的 Angular 模块,而且还定义了用于用户界面的路由和模板:
清单 4. public/javascripts/app.js
angular.module('polls', []) .config(['$routeProvider', function($routeProvider) { $routeProvider. when('/polls', { templateUrl: 'partials/list.html', controller: PollListCtrl }). when('/poll/:pollId', { templateUrl: 'partials/item.html', controller: PollItemCtrl }). when('/new', { templateUrl: 'partials/new.html', controller: PollNewCtrl }). otherwise({ redirectTo: '/polls' }); }]);
Angular 控制器定义了应用程序的范围,为要绑定的视图提供数据和方法。
清单 5. public/javascript/controllers.js
// Managing the poll list function PollListCtrl($scope) { $scope.polls = []; } // Voting / viewing poll results function PollItemCtrl($scope, $routeParams) { $scope.poll = {}; $scope.vote = function() {}; } // Creating a new poll function PollNewCtrl($scope) { $scope.poll = { question: '', choices: [{ text: '' }, { text: '' }, { text: '' }] }; $scope.addChoice = function() { $scope.poll.choices.push({ text: '' }); }; $scope.createPoll = function() {}; }
创建局部 HTML 和模板
为了呈现来自控制器的数据,Angular 使用了局部 HTML 模板,该模板允许您使用占位符和表达式来包含数据和执行操作,比如条件和迭代操作。在公共目录中,创建一个名为 partials 的新的子目录。我们将为我们的应用程序创建 3 个局部模板,第一个局部模板将会展示可用投票的列表,我们将使用 Angular 通过一个搜索字段轻松过滤该列表。
清单 6. public/partials/list.html
<div class="page-header"> <h1>Poll List</h1> </div> <div class="row"> <div class="col-xs-5"> <a href="#/new" class="btn btn-default"><span class="glyphicon glyphicon-plus"></span> New Poll</a> </div> <div class="col-xs-7"> <input type="text" class="form-control" ng-model="query" placeholder="Search for a poll"> </div> </div> <div class="row"><div class="col-xs-12"> <hr></div></div> <div class="row" ng-switch on="polls.length"> <ul ng-switch-when="0"> <li><em>No polls in database. Would you like to <a href="#/new">create one</a>?</li> </ul> <ul ng-switch-default> <li ng-repeat="poll in polls | filter:query"> <a href="#/poll/{{poll._id}}">{{poll.question}}</a> </li> </ul> </div> <p> </p>
第二个局部模板允许用户查看投票。它使用 Angular 切换指令来确定用户是否已投票,并根据这些判断,显示一个就此次问卷调查进行投票的表格,或者一个包含显示问卷调查结果的图表。
清单 7. public/partials/item.html
<div class="page-header"> <h1>View Poll</h1> </div> <div class="well well-lg"> <strong>Question</strong><br>{{poll.question}} </div> <div ng-hide="poll.userVoted"> <p class="lead">Please select one of the following options.</p> <form role="form" ng-submit="vote()"> <div ng-repeat="choice in poll.choices" class="radio"> <label> <input type="radio" name="choice" ng-model="poll.userVote" value="{{choice._id}}"> {{choice.text}} </label> </div> <p><hr></p> <div class="row"> <div class="col-xs-6"> <a href="#/polls" class="btn btn-default" role="button"><span class="glyphicon glyphicon-arrow-left"></span> Back to Poll </div> <div class="col-xs-6"> <button class="btn btn-primary pull-right" type="submit"> Vote »</button> </div> </div> </form> </div> <div ng-show="poll.userVoted"> <table class="result-table"> <tbody> <tr ng-repeat="choice in poll.choices"> <td>{{choice.text}}</td> <td> <table style=" {{choice.votes.length /poll.totalVotes*100}}%;"> <tr><td>{{choice.votes.length}}</td></tr> </table> </td> </tr> </tbody> </table> <p><em>{{poll.totalVotes}} votes counted so far. <span ng-show="poll.userChoice">You voted for <strong>{{poll.userChoice.text}} </strong>.</span></em></p> <p><hr></p> <p><a href="#/polls" class="btn btn-default" role="button"> <span class="glyphicon glyphicon-arrow-left"></span> Back to Poll List</a></p> </div> <p> </p>
第三个也是最后一个局部模板定义了支持用户创建新的问卷调查的表单。它要求用户输入一个问题和三个选项。提供一个按钮,以便允许用户添加额外的选项。稍后,我们将验证用户至少输入了两个选项 — 因为如果没有几个选项,就不能称之为问卷调查。
清单 8. public/partials/new.html
<div class="page-header"> <h1>Create New Poll</h1> </div> <form role="form" ng-submit="createPoll()"> <div class="form-group"> <label for="pollQuestion">Question</label> <input type="text" ng-model="poll.question" class="form-control" id="pollQuestion" placeholder="Enter poll question"> </div> <div class="form-group"> <label>Choices</label> <div ng-repeat="choice in poll.choices"> <input type="text" ng-model="choice.text" class="form-control" placeholder="Enter choice {{$index+1}} text"><br> </div> </div> <div class="row"> <div class="col-xs-12"> <button type="button" class="btn btn-default" ng-click= "addChoice()"><span class="glyphicon glyphicon-plus"> </span> Add another</button> </div> </div> <p><hr></p> <div class="row"> <div class="col-xs-6"> <a href="#/polls" class="btn btn-default" role="button"> <span class="glyphicon glyphicon-arrow-left"></span> Back to Poll List</a> </div> <div class="col-xs-6"> <button class="btn btn-primary pull-right" type="submit"> Create Poll »</button> </div> </div> <p> </p> </form>
最后,为了显示结果,我们需要向 style.css 添加一些 CSS 声明。将该文件的内容替换为以下内容:
清单 9. public/stylesheets/style.css
body { padding-top: 50px; } .result-table { margin: 20px 0; 100%; border-collapse: collapse; } .result-table td { padding: 8px; } .result-table > tbody > tr > td:first-child { 25%; max- 300px; text-align: right; } .result-table td table { background-color: lightblue; text-align: right; }
此时,如果您运行该应用程序,就会看到一个空的问卷调查列表。如果您试着创建一个新的问卷调查,就能看到此表单并添加更多的选项,但您不能保存该问卷调查。我们将在下一步中详细介绍所有这些内容。
第 3 步. 使用 Mongoose 在 MongoDB 中存储数据
为了存储数据,该应用程序使用了 MongoDB 驱动程序和 Mongoose npm 模块。它们允许应用程序与 MongoDB 数据库进行通信。要获得这些模块,请打该应用程序根目录中的 package.json 文件,并在依赖关系部分中添加以下这些代码行:。
清单 10. 向依赖关系添加下列代码
"mongodb": ">= 1.3.19", "mongoose": ">= 3.8.0",
保存文件,在 Project Explorer 中右键单击并选择 Run As > npm install。这将安装 npm 模块和其他所有依赖关系。
创建一个 Mongoose 模型
在您应用程序的名为 models 的根目录中创建一个新的子目录,并在这个子目录中创建一个名为 Poll.js 的新文件。在这个文件中,我们定义了我们的 Mongoose 模型,该模型将用于查询数据,并以结构化数据的形式将这些数据保存到 MongoDB 。
清单 11. models/Poll.js
var mongoose = require('mongoose'); var voteSchema = new mongoose.Schema({ ip: 'String' }); var choiceSchema = new mongoose.Schema({ text: String, votes: [voteSchema] }); exports.PollSchema = new mongoose.Schema({ question: { type: String, required: true }, choices: [choiceSchema] });
定义数据存储的 API 路由
接下来,在您应用程序的根目录下的 app.js 文件中设置一些路由,以便创建一些 JSON 端点,这些端点可用于根据 Angular 客户端代码来查询和更新 MongoDB。找到 app.get('/', routes.index)
行,并将下列代码添加到这一行的后面: :
清单 12. 创建 JSON 端点
app.get('/polls/polls', routes.list); app.get('/polls/:id', routes.poll); app.post('/polls', routes.create);
现在,您需要实现这些功能。将 routes/index.js 文件的内容替换为下列代码:
清单 13. routes/index.js
var mongoose = require('mongoose'); var db = mongoose.createConnection('localhost', 'pollsapp'); var PollSchema = require('../models/Poll.js').PollSchema; var Poll = db.model('polls', PollSchema); exports.index = function(req, res) { res.render('index', {title: 'Polls'}); }; // JSON API for list of polls exports.list = function(req, res) { Poll.find({}, 'question', function(error, polls) { res.json(polls); }); }; // JSON API for getting a single poll exports.poll = function(req, res) { var pollId = req.params.id; Poll.findById(pollId, '', { lean: true }, function(err, poll) { if(poll) { var userVoted = false, userChoice, totalVotes = 0; for(c in poll.choices) { var choice = poll.choices[c]; for(v in choice.votes) { var vote = choice.votes[v]; totalVotes++; if(vote.ip === (req.header('x-forwarded-for') || req.ip)) { userVoted = true; userChoice = { _id: choice._id, text: choice.text }; } } } poll.userVoted = userVoted; poll.userChoice = userChoice; poll.totalVotes = totalVotes; res.json(poll); } else { res.json({error:true}); } }); }; // JSON API for creating a new poll exports.create = function(req, res) { var reqBody = req.body, choices = reqBody.choices.filter(function(v) { return v.text != ''; }), pollObj = {question: reqBody.question, choices: choices}; var poll = new Poll(pollObj); poll.save(function(err, doc) { if(err || !doc) { throw 'Error'; } else { res.json(doc); } }); };
使用 Angular 服务将数据绑定到前端
此时,设置后台,以便启用查询,并将问卷调查数据保存到数据库,但我们需要在 Angular 中做一些更改,以便让它知道如何与数据库进行通信。使用 Angular 服务很容易完成这项任务,它将与服务器端进行通信的过程包装到了简单的函数调用中:
清单 14. public/javascripts/services.js
angular.module('pollServices', ['ngResource']). factory('Poll', function($resource) { return $resource('polls/:pollId', {}, { query: { method: 'GET', params: { pollId: 'polls' }, isArray: true } }) });
在创建的这一文件之后,您需要将它包括在您的 index.jade 模板中。在文件标头部分的最后一个脚本元素的下面添加以下这行代码:script(src='/javascripts/services.js')
。
您还需要告诉您的 Angular 应用程序使用这个服务模块。要实现这一点,请打开 public/javascripts/app.js 并将第一行更改为可读,如下所示:angular.module('polls', ['pollServices'])
。
最后,更改 Angular 控制器,以便使用该服务在数据库中进行查询和存储问卷调查数据。在 public/javascripts/controllers.js 文件中,将 PollListCtrl
更改如下:。
清单 15. public/javascripts/controller.js
function PollListCtrl($scope, Poll) { $scope.polls = Poll.query(); } ...
更新 PollItemCtrl
函数,以便根据问卷调查的 ID 来查询某个问卷调查:
清单 16. public/javascripts/controller.js (continued)
... function PollItemCtrl($scope, $routeParams, Poll) { $scope.poll = Poll.get({pollId: $routeParams.pollId}); $scope.vote = function() {}; } ...
类似地,更改 PollNewCtrl
函数,以便在提交表单时将新调查数据发送到服务器。
清单 17. public/javascripts/controller.js (continued)
... function PollNewCtrl($scope, $location, Poll) { $scope.poll = { question: '', choices: [ { text: '' }, { text: '' }, { text: '' }] }; $scope.addChoice = function() { $scope.poll.choices.push({ text: '' }); }; $scope.createPoll = function() { var poll = $scope.poll; if(poll.question.length > 0) { var choiceCount = 0; for(var i = 0, ln = poll.choices.length; i < ln; i++) { var choice = poll.choices[i]; if(choice.text.length > 0) { choiceCount++ } } if(choiceCount > 1) { var newPoll = new Poll(poll); newPoll.$save(function(p, resp) { if(!p.error) { $location.path('polls'); } else { alert('Could not create poll'); } }); } else { alert('You must enter at least two choices'); } } else { alert('You must enter a question'); } }; }
运行应用程序
您已经离成功不远了!此时,应用程序应该允许用户查看和搜索问卷调查数据、创建新的问卷调查并查看单个问卷调查的投票选项。在运行该应用程序之前,请确保您已经本地运行 MongoDB。这通常和打开一个终端或命令提示符以及运行 mongod
命令一样简单。确保在您运行应用程序时终端窗口处于打开状态:
图 3. 查看一个问卷调查的选项
在运行应用程序之后,在您的浏览器中导航到 http://localhost:3000 并创建一些问卷调查。如果您单击一个问卷调查,您就能够看到可用的选项,但是,您无法实际对该问卷调查进行投票,或者暂时看不到问卷调查结果。我们将在下一步和最后一步中对此进行介绍。
第 4 步. 使用 Socket.io 进行实时投票
剩下的惟一需要构建的特性就是投票功能。该应用程序允许用户进行投票,在他们投票后,会在所有已连接的客户端上实时更新投票结果。使用 socket.io 模块很容易完成这项工作,现在就让我们来实现它吧。
打开您应用程序的根目录中的 package.json 文件,将下列代码添加到依赖关系部分:"socket.io": "~0.9.16"
。
保存文件,在 Package Explorer 中右键单击,然后选择 Run As > npm install 来安装 npm 模块。
接下来,打开应用程序根目录中的 app.js 文件, 删除位于文件末尾的 server.listen...
代码块,将其替换为:
清单 18. app.js
... var server = http.createServer(app); var io = require('socket.io').listen(server); io.sockets.on('connection', routes.vote); server.listen(app.get('port'), function(){ console.log('Express server listening on port ' + app.get('port')); });
接下来,修改 index.jade 模块以包含 socket.io 客户端库。在运行该应用程序时,该库会自动在指定的位置上变得可用,因此不需要担心自己如何寻找该文件。确保想包含模板中的 angular-resource 库的行的后面包含此文件:script(src='/socket.io/socket.io.js')
。
最后,您需要创建投票功能,以便在用户向 socket.io 发送消息时保存新的投票,并在具有更新结果时将消息发送给所有客户端。将 添加到路由目录中的 index.js 文件的结尾处:
清单 19. routes/index.js
// Socket API for saving a vote exports.vote = function(socket) { socket.on('send:vote', function(data) { var ip = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address.address; Poll.findById(data.poll_id, function(err, poll) { var choice = poll.choices.id(data.choice); choice.votes.push({ ip: ip }); poll.save(function(err, doc) { var theDoc = { question: doc.question, _id: doc._id, choices: doc.choices, userVoted: false, totalVotes: 0 }; for(var i = 0, ln = doc.choices.length; i < ln; i++) { var choice = doc.choices[i]; for(var j = 0, jLn = choice.votes.length; j < jLn; j++) { var vote = choice.votes[j]; theDoc.totalVotes++; theDoc.ip = ip; if(vote.ip === ip) { theDoc.userVoted = true; theDoc.userChoice = { _id: choice._id, text: choice.text }; } } } socket.emit('myvote', theDoc); socket.broadcast.emit('vote', theDoc); }); }); }); };
注意:如果您想知道为什么应用程序会在常规 API 地址属性的前面查找标头 'x-forwarded-for'
,因为这将确保当应用程序被部署到负载平衡环境中时,所使用的是正确的客户端 IP。如果您将该应用程序部署到 BlueMix 或 Cloud Foundry,这对于应用程序是否能正常工作至关重要。
添加一个 Angular 服务将数据发送到 Web 套接字。
Web Sockets 的后端功能现已创建完毕。目前剩下要做的工作是绑定前端,以发送和监听套接字事件。最佳方法是添加一个新的 Angular 服务。使用以下代码替换 public/javascripts 文件夹中的 services.js 文件:
清单 20. public/javascripts/services.js
angular.module('pollServices', ['ngResource']). factory('Poll', function($resource) { return $resource('polls/:pollId', {}, { query: { method: 'GET', params: { pollId: 'polls' }, isArray: true } }) }). factory('socket', function($rootScope) { var socket = io.connect(); return { on: function (eventName, callback) { socket.on(eventName, function () { var args = arguments; $rootScope.$apply(function () { callback.apply(socket, args); }); }); }, emit: function (eventName, data, callback) { socket.emit(eventName, data, function () { var args = arguments; $rootScope.$apply(function () { if (callback) { callback.apply(socket, args); } }); }) } }; });
最后,您需要编辑 PollItemCtrl
控制器,以便它能够监听和发送用于投票的 Web Socket 消息。将原始控制器替换为:
清单 21. public/javascripts/controllers.js
... function PollItemCtrl($scope, $routeParams, socket, Poll) { $scope.poll = Poll.get({pollId: $routeParams.pollId}); socket.on('myvote', function(data) { console.dir(data); if(data._id === $routeParams.pollId) { $scope.poll = data; } }); socket.on('vote', function(data) { console.dir(data); if(data._id === $routeParams.pollId) { $scope.poll.choices = data.choices; $scope.poll.totalVotes = data.totalVotes; } }); $scope.vote = function() { var pollId = $scope.poll._id, choiceId = $scope.poll.userVote; if(choiceId) { var voteObj = { poll_id: pollId, choice: choiceId }; socket.emit('send:vote', voteObj); } else { alert('You must select an option to vote for'); } }; } ...
查看最终产品
问卷调查应用程序现已创建完成。确保 mongod 仍在运行,并在 Eclipse 中再次运行 Node 应用程序。在浏览器中输入 http://localhost:3000,导航到一个问卷调查并进行投票。随后您就可以看到结果。要查看实时更新,请找到您的本地 IP 地址,并用该地址替换 localhost
。在您的局域网中,使用不同的机器(甚至智能手机或平板电脑也可以)导航到这个地址。当您在另一个设备上进行投票时,结果会显示在该设备上,而且会自动发布到您的主要计算机浏览器上:
图 4. 查看问卷调查结果
下一步:进一步开发和部署
您刚才创建的这个问卷调查应用程序是一个不错的起点,但还有很大的改进空间。在计划创建这类应用程序时,我喜欢使用一种敏捷方法来定义用户案例,并将项目划分为几块来实现。对于这个项目,我使用了 JazzHub,通过将项目的附属代码和源代码一起保存在一个云托管的存储库中,JazzHub 使得开发变得非常简单。
如果您对您的应用程序感到很满意,下一步就是跟全世界的人分享它。在过去,即使部署一个非常简单的应用程序,可能也会是一场噩梦,但值得庆幸的是,那些日子已经一去不复返了。使用 IBM 新兴的兼容 Cloud Foundry 的 BlueMix 平台,您只需几分钟就可以通过最少的配置将您的应用程序部署到云中,一点都不麻烦。
结束语
这对于开发人员,现在是一个很好的时机。我们手头有大量框架和工具,它们使得开发大量应用程序不仅更简单、更快速,而且更加令人感到愉快。在本文中,您学习了如何使用被称为 MEAN 体系结构(Mongo、Express、Angular 和Node)的技术构建一个应用程序。该堆栈可能只需要一天时间就可以完成任务,远远超过了 LAMP 体系结构(Linux、Apache、MySQL 和 PHP),在 Web 应用程序开发和部署方面,该体系结构也许同样会超越 LAMP 体系结构。对我而言,我已经迫不及待跃跃欲试了。
参考资料
学习
- 关于 Node.js 的学习路线图。
- MongoDB 简介
- 启动 JazzHub 项目
- developerWorks Web development 专区:通过专门关于 Web 技术的文章和教程,扩展您在网站开发方面的技能。
- developerWorks Ajax 资源中心:这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。
- developerWorks Web 2.0 资源中心,这是有关 Web 2.0 相关信息的一站式中心,包括大量 Web 2.0 技术文章、教程、下载和相关技术资源。您还可以通过 Web 2.0 新手入门 栏目,迅速了解 Web 2.0 的相关概念。
- 查看 HTML5 专题,了解更多和 HTML5 相关的知识和动向。
讨论
- 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。