又开始做练习了啊,探索编程世界的奥秘又开始了,升级打怪,哈哈
1、有些小虐的21-27 的练习题来了
对于即将要做的练习题目,先全部浏览了一下,接下来真真正正的是要有一场硬仗需要打,老师里面写的代码的判断,自己要花时间细细的品味,才能对于项目的一点点的演进有一个深刻的理解。这一部分搞定之后,练习题这块剩余的几个章节将不再是太大的问题。按照这个方式,一点点的啃也就解决了,中间老师对于Express的学习提出了一些问题,需要我们各自思考,认真的作答。
2、更好的API能力
开发一个webapp项目,项目能力全部体现在路由上,路由可以成为webapp提供给外界的API。
前面我们将获取页面的路由 router.page.js
和获取数据的路由 router.api.js
分开写在两个不同的 js 文件中。这是一个很好的习惯,不仅在业务功能上区分开,而且可以由不同的工程师来分布维护。
在讨论设计路由的API时,提供数据的 router.api.js
非常重要,而提供页面的 route.page.js 相对宽松一些。原因是,提供数据的路由API 不仅会给前端用,还可能给 Android 应用,ios应用、小程序等各式各样的客户端使用。如果不能采用一种标准化的设计方式,就会乱掉。
既有的 API 能力
路由 | 功能 |
---|---|
[GET] /api/users | 创建项目时自带的功能(暂时留着) |
[GET] /api/posts | 获取文章列表 |
[POST] /api/posts/create | 提交新文章 |
[POST] /api/posts/edit | 提交文章修改内容 |
[GET] /api/posts/one | 获取特定文章的内容 |
增加 API 版本控制
有些时候我们还可以对API进行版本分类,以更好的升级 API。
[GET] /api/v1/users
[GET] /api/v1/posts
[POST] /api/v1/posts/create
[POST] /api/v1/posts/edit
[GET] /api/v1/posts/one
在未来,你可以针对某个路由进行升级。比如下面单独升级 /api/v1/posts/one 为 v2。
[GET] /api/v1/users
[GET] /api/v1/posts
[POST] /api/v1/posts/create
[POST] /api/v1/posts/edit
[GET] /api/v2/posts/one
尽可能用 http 自身的办法来区分路由功能
HTTP 提供了很多方法,和数据有关的有以下的几类。
GET(SELECT): 从服务器取出资源(一项或者是多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE(DELETE):从服务器删除资源。
依据这个原则,我们把 API 调整如下。
[GET] /api/v1/users
[GET] /api/v1/posts
[POST] /api/v1/posts/create
[PATCH] /api/v1/posts/edit
[GET] /api/v1/posts/one
避免使用动词来区分路由
在上面用了create 、edit,这类动词看上去直观的表达了路由的意思,但是动词多了后就容易产生歧义,路由尽量避免。
[GET] /api/v1/users
[GET] /api/v1/posts
[POST] /api/v1/posts //去掉动词
[PATCH] /api/v1/posts //去掉动词
[GET] /api/v1/posts/one
对于 posts 这个资源,通过 HTTP 的GET、POST、PUT 三个方法就能区分是什么事情,不需要多余动词加以区分。
用资源 ID 而不是单词区分资源
[GET] /api/v1/posts // 获取文章列表
[GET] /api/v1/posts/one // 获取某一个文章列表
改成
[GET] /api/v1/posts // 获取文章列表
[GET] /api/v1/posts/:id // 获取某一个文章
最终的路由
[GET] /api/v1/users
[GET] /api/v1/posts
[POST] /api/v1/posts
[PATCH] /api/v1/posts/:id
[GET] /api/v1/posts/:id
总结
1、尽量用 HTTP 的方法来区分功能
2、避免在路上使用动词区分功能
3、针对某个资源,用和该资源相关的 id (或唯一值)
以上我们整理了和数据相关的 API ,数据 API 属于服务端的范畴,页面只能用唯一的 HTTP 的 GET 方法,而且页面是属于客户端范畴。因此,页面的路由设计相对宽容一些。
最后,还要修改 app.js
app.use ('/api', api);
修改为
app.use('/api/v1', api );
最后一部分开始在编辑器里面实战。
21 章节练习的时候,又遇到了相对路径的问题,最终还是调试完成了。
3、更好的结果
在router.api.js 中,我们利用 res.json()方法来响应请求 并 返回结果数据。
res.json({ success: false });
res.json({ success: true });
res.json({ success: true, postsList: posts });
在处理结果时,我们将错误分为两类
- WebAPP 不提供的能力
- WebAPP 自身的逻辑错误 (或数据库发生的错误)
WebAPP 不提供的能力
WebAPP 提供页面 和 数据的能力是有限的。当客户端发起了一个不存在的数据路由访问,势必拿不到页面或者是数据。
在前面的小结中,我们知道在 app.js 中有统一的错误处理。
// filepath : app.js
// catch 404 and forward to error handler
app.use(function (req ,res ,next){
var err = new Error('Not Found');
err.status = 400;
next(err);
});
// error handler
app.use ( function (err, req ,res ,next ){
//set locals , only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err :{};
// render the error page
res.status( err.status || 500);
res.render('error');
});
修改数据返回的错误
通过 { success: false } 或者是 { success: true} 来区分是否存在错误逻辑似乎不是特别的好。
在 HTTP 请求是,响应中都会默认有一个 status 参数,成为状态码 ,如果状态码不为 2XX,就表明错误处理错误。为什么不用 res. status 返回500的方式呢?
我们可以借助这个统一的错误中枢来统一处理所有的错误。
// filepath: route.api.js
/* GET posts lists */
router.get('/posts', function(req, res, next) {
PostModel.find({}, {}, function(err, posts) {
if (err) {
res.json({ success: false });
} else {
res.json({ success: true, postsList: posts });
}
});
});
修改成:
// filepath: route.api.js
/* GET posts lists */
router.get('/posts', function(req, res, next) {
PostModel.find({}, {}, function(err, posts) {
if (err) {
err.status = 500;
next(err);
} else {
res.json({ postsList: posts });
}
});
});
我们在err对象中加入 status = 500,并把错误送给错误中枢统一返回给用户。
其实在 app.js 中的错误处理,如果你不主动设置 err.status = 500, 它会主动帮默认设置为 500。
// filepath: app.js
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
// 如果不设置err.status,或默认设置为500
res.status(err.status || 500);
res.render('error');
});
因此,可以省略状态码的设置,直接调用 next(err)。
// filepath: route.api.js
/* GET posts lists */
router.get('/posts', function(req, res, next) {
PostModel.find({}, {}, function(err, posts) {
if (err) {
next(err);
} else {
res.json({ postsList: posts });
}
});
});
其他路由修改参考上面的样子。
修改客户端(接受结果)的处理
在此前,posts.ejs 中获取数据是如下处理
fetchData () {
axios.get('/api/v1/posts')
.then(function(response) {
vm.postsList = response.data.postsList;
vm.postsList.forEach((element) => element.url = '/posts/show?id=' + element._id);
})
}
这个处理并没有进行错误检查。上面我们知道,当服务端出现错误会返回状态码500。因此我们要接住这个错误才能保证逻辑正确处理。
fetchData () {
axios.get('/api/v1/posts')
.then(function(response) {
return response.data;
})
.then(function(data) {
vm.postsList = data.postsList;
vm.postsList.forEach((element) => element.url = '/posts/show?id=' + element._id);
})
.catch(function(err) {
alert(err);
})
}
axios.get() 发起一个 get 请求,如果返回的状态码不是 2xx,就会抛出一个异常并执行 catch() 函数。
总结
- 尽量通过统一的错误处理逻辑。
- 尽量使用状态码,而不是自己设计一个错误
值。例如:success:false/true。 - 客户端页面要对错误进行错误处理以保证健壮性。
创建和修改数据时,该不该返回数据?(特别提醒)
在 route.api.js中创建post的路由处理中,可以把写入到数据库的数据重新返回给客户端。
/* POST create post */
router.post('/posts', function(req, res, next) {
var title = req.body.title;
var content = req.body.content;
var post = new PostModel();
post.title = title;
post.content = content;
post.save(function(err, doc) {
if (err) {
next(err);
} else {
res.json({post: doc}); // 注意这里
}
});
});
一般情况下,新创建的数据需要返回给客户端一份,为什么呢?因为写入到数据库后会给这条数据产生一个唯一的id,而客户端拿到这个id可以进行一些交互操作。
submit () {
axios.post('/api/v1/posts',
{
title: vm.title,
content: vm.content
})
.then(function(response) {
return response.data;
})
.then(function(data) {
// 注意这句
window.location = '/posts/show?id=' + data.post._id;
})
.catch(function(err) {
alert(err);
})
}
}
可以通过服务端返回的post数据来打开刚刚创建的文章页面。
编辑文章时,服务端不需要返回完整的数据。
/* PATCH edit post */
router.patch('/posts/:id', function(req, res, next) {
var id = req.params.id;
var title = req.body.title;
var content = req.body.content;
PostModel.findOneAndUpdate({ _id: id }, { title, content }, function(err) {
if (err) {
next(err);
} else {
res.json({}); // 不需要返回文章数据
}
});
});
因为编辑是对一个已经在数据库中的数据进行编辑,客户端自身有这条数据的id。 所以,在 ./views/edit.ejs中,只需要用本地已经有的id来进行页面跳转。
submit () {
axios.patch('/api/v1/posts/' + postId,
{
title: vm.title,
content: vm.content
})
.then(function(response) {
return response.data;
})
.then(function(data) {
// 注意这里,用的是既有的postId来跳转到文章页面
window.location = '/posts/show?id=' + postId
})
.catch(function(err) {
alert(err);
})
}
这也说明了http的post和patch的使用区别,post可能需要服务端返回数据,patch不需要服务端返回数据。
22 小节的实战: 主要是针对于页面有错误的时候,不应该单纯的return success,而是返回相关的错误界面给到用户。后面的练习题,主要是针对于 Post
4、通过 22 个小节,我们做出了一个简单的内容发布 WebAPP。在通过进一步之前,我建议你停下来并认真复习和思考一下。
开发一个 WebAPP 为什么需要 Express?Express能带给你什么好处?
WebAPP 中的 package.json 在整个工程起到了什么作用?
在 Express 中 Router 的价值,为什么 Router 作为一个中间件会将其格外的突出?
为什么要对 Router 进行分类管理,分成 route.api.js 和 route.page.js 的好处是什么?
在整个数据流中,为什么要传递req、res 和 next 这三个对象?
app.js中的两个错误处理函数的(错误中枢)的工作原理,统一错误的价值是什么?
视图引擎是必须的吗?Express和 ejs的关系是什么?使用 ejs 是必须的吗?
在构建视图时,res.render 是如何定位到页面文件的?可以改变定位的路径吗?如果不用res.render() 该怎么构建页面?
ejs 中 <%- %> 和 <%= %> 的差异是什么?
mongodb中的 schema 是什么意思?
5、添加导航条
24小节的实战:就是给每个界面添加了导航条,实现起来没有多少难度
6、账号系统
25小节的实战:内容超级多,难啃的骨头来了。
-
在WebAPP中,账号信息可以记录在浏览器的 Cookie 模块,浏览器会根据不同的域名来分配一块 Cookie 空间。
当用户在页面输入账号和密码后,WebAPP 验证登录信息是正确的,这时服务会把账号信息塞到请求的 Cookie 中并返回给客户端,客户端拿到信息后,把账户信息存在浏览器的 Cookie 中。
下一次客户端向服务端发起请求的时候,浏览器会帮我们自动把 Cookie 附加到请求里,以供服务来判断这是一个已经登录的用户。当服务收到请求后,第一件事情应该是分析 Cookie 里的信息并判断这个用户是不是一个真实的用户。
-
实战中遇到的问题
1、router.api.js里面没有引入相关的文件
2、bcrypt 安装一直出现报错的问题
npm install --save bcrypt // 报错
解决方案:
修改package.json 里面的版本号" bcrypt": "^1.0.3"
,与老师的保持一致
删除已安装版本:
npm uninstall --save bcrypt
再次执行:
npm install --save bcrypt // 即可
3、程序在设计的整个环节的思路,不是特别的清晰,还需要不断的揣摩
7、访问权限
26小节的实战:
- toString的函数使用在前端报错,问题没有彻底的解决,临时的解决方案是把 toString 干掉了
- var ObjectId = Schema.ObjectId; 这个代码我并不理解其中的全部的意思
8、更好的错误处理中枢
27小节的实战:这一个小节就是对于错误页面,不是同一给出一个500页面,而是把具体的错误提示处理。