做为一名前端开发工程师,时常纠结于是否要学习后端开发,成为一名真正的全栈,后端开发当然首选是node.js,因为不需要重新学习一门新的开发语言,可是node.js好像很难啊,深入浅出node.js这本书成功劝退了许多想学全栈的同学,因为一开始就把后端开发最艰深的一面展示了出来,如模块和内存管理等,虽然对于有更高追求的开发者来说, 这些原理肯定是掌握的越深入越好,但是对于普通的后台业务开发来说,其实只需要会用能实现业务就可以了。
我们今天就从最简单的todo list项目开始,揭开后台开发的神秘面纱,帮你走上全栈开发工程师之路。为什么选择todo list?因为它是最具代表性的项目,也最为大家所熟悉,接受难度最小,麻雀虽小,五脏俱全,它包含了前后端开发必备的技能,可以说只要完全掌握了todo list,就可以依葫芦化瓢完成大部分项目的功能开发。 本文是一个可以完整包含前后端代码能够直接运行的项目, 效果如下:
本教程适合有一定前端基础、轻微了解node.js的开发人员(其实只需要知道npm安装和引用模块就已足够)
我们打算从零开始,步步为营逐渐完善整个项目代码
- 第一步,先实现一个最简化的纯静态版本的todolist
- 第二步,建立后台接口,用mock测试数据填充,让页面先能够正常显示出来
- 第三步,设计数据库,将测试数据改为真正的接口
其实这也是真实项目的开发流程,现在的前后台开发都是分离的,前后台开发先按照接口约定各自开发代码,前端用静态数据模拟(mock)能够完成99%以上的功能开发,后端都是纯数据,自己单元测试就好了,全部开发完毕之后,前后台对接部署在测试环境。
迈步从头跃,前端纯静态版todolist
为了保持教程的简单,前端代码用jquery实现,先建立一个index.html的空白文件,在页面引入jquery,并添加一个ul元素 做为容器。加一个文本框和添加按钮,html代码如下:
<ul id="list">ul>
<input type="text" id="title" placeholder="输入待办事项">
<button id="btn_add">添加button>
复制代码
现在还没有后台数据库,数据只能先用静态的数组来定义,每条记录有id,title,status 三个属性,分别表示事项的编号、标题、是否已完成。 定义一个数组存放用测试数据,然后用forEach循环该数组,对html字符串进行拼接,最后合并html生成dom,该示例用了ES6的模板字符串,方便书写和演示。
代码如下:
function render(){
var html="";
var result=[
{id:1,title:"hello",status:0},
{id:2,title:"hello",status:0}
];
result.forEach(function(item){
var checked=item.status>0?"checked":"";
var li=`${item.id} ">
${checked}>
${item.title}">
删除
`
html+=li;
})
$("#list").html(html);
}
$(function (){
render();
})
复制代码
运行 index.html,成功显示出页面。
第二次迭代,用mock数据,连接真实的后台接口
好了,静态页面渲染已经写好,要迈出后台开发的第一步了,把静态数据改成调用ajax 接口,对render函数稍做改造即可
function render(){
$.post("http://localhost/todo/list",{},function (result){
var html="";
result.forEach(function(item){
var checked=item.status>0?"checked":"";
var li=`${item.id} ">
${checked}>
${item.title}">
删除
`
html+=li;
})
$("#list").html(html);
});
}
复制代码
但是问题是现在还没有后台服务器,而且本地的html文件发送ajax请求会遭遇跨域错误,有在本地做过开发的同学一定知道ajax会产生cors异常,因为本地的文件是用file://协议打开的,如果访问http://协议的页面,会因为同源策略导致跨域错误,同源策略要求:
- 协议相同
- 域名相同
- 端口相同
控制台的报错信息如下图所示:
这个问题其实是有办法解决的,只要后台接口实现了cors跨域,就可以畅通无阻的调用了,这样你的代码不用部署到远程服务器,就可以调用远程服务器的接口,非常的方便。那么先来搭建个http服务实现cors吧,少年!我们今天不用express,也不用koa,因为他们都没有mock功能,跨域也要额外编写很多代码。因此我自己封装了一个,名为webcontext,github地址: github.com/windyfancy/…
通过npm可以安装,我们在硬盘上建个目录,例如todo_sample,用于存放后台项目,切换进入该文件夹,运行npm install安装
npm install --save webcontext
复制代码
安装好之后,在项目根目录建一个app.js,用于启动http服务,写两行代码:
app.js
const WebContext = require('webcontext');
const app = new WebContext();
复制代码
运行node app.js,http服务就启动了,然后访问http://localhost/,能够输出hello信息,表示http服务已经搭建成功!
接下来,在项目根目录/service目录中建立一个todo子目录,将在todo目录中建立一个list.ejs空白文件,目录结构如下
|-- service
| |--todo
| |--list.ejs
复制代码
把之前的静态数据存入list.ejs中,它其实是个ejs模板文件,当然也可以存放json
[
{id:1,title:"hello",status:0},
{id:2,title:"hello",status:0}
]
复制代码
现在再来用浏览器直接访问:http://localhost/todo/list ,已经可以直接输了该文件的内容了,但是用本地的index.html调用这个接口仍然是返回跨域错误的,为了安全考虑, webcontext 跨域的配置默认是关闭的,我们打开项目根目录的web.config.json(首次运行时会自动生成),修改cors属性中的allowOrigin字段值为*即可开启跨域
"cors":{
"allowOrigin":"*"
}
复制代码
现在再来访问本地的index.html,发现已经可以请求成功了,http 响应头正确的输出了cors 跨域的信息。
战前准备工作,设计mysql数据库
上一步完成了后台http接口的搭建,用mock静态数据验证了todolist的加载功能正常。终于到了激动人心的数据库开发环节了,想想马上就可以做个高大上的CURD boy了,走上人生巅峰,出任CEO,心情真是有点小激动呢。
简单了解一下吧,顾名思义,数据库是用来存放数据的地方,目前主流的数据库是关系数据库,如mysql、oracle、sql server等,以行列结构存储一张张表的数据,就如同一个excel表格,每一张表是一个独立的sheet,我们把刚才静态的json数据转换成表的形式如下:
id | title | status |
---|---|---|
1 | hello | 0 |
2 | world | 0 |
以mysql为例,来定义一下这张todo_list表的结构,共有3个字段:
- id:表示待办事项的编号,数值类型,在数据库中以int类型表示,它是每条记录的唯一标识,即主键
- title:表示待办事项的名称,字符串类型,在数据库中以varchar变长字符串类型表示
- status:表示待办事项的状态,布尔类型,为了便于扩展我们定义成数据类型,也用int类型表示
我们来建表吧,首先肯定要安装mysql了
上官方网站https://www.mysql.com/downloads/ 下载安装一下,完整安装一下。装 好之后,就 可以用自带的workbench连接数据库了。用你刚才安装时初始设定的密码连接一下:
连接上之后,数据库还是空的呢,要先新建一个数据库(schema),名称为todo_db,然后在这个数据库中新建一个表,可以用工具栏或菜单中的create new table快速创建一张表:
当然也可以用sql代码去创建表:
CREATE TABLE `todo_list` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(45) DEFAULT NULL,
`status` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
)
复制代码
创建好数据库表之后,我们尝试用代码连接数据库,webcontext框架已经集成了数据库的连接,只需要配置一下,在app.js启动时就可以自动连接数据库了,不用编写任何额外的代码
修改web.config.json,设置数据库连接参数
"database":{
"host":"127.0.0.1",
"port":"3306",
"user":"root",
"password":"你的密码",
"database":"todo_db"
}
复制代码
注:MySQL8.0以上版本密码认证协议发生了改变,需要用mysql workbench执行如下代码才能使用node.js连接数据库,sql代码如下: ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';
好了,现在重新运行一下node app.js,就可以自动连接数据库了
第三次迭代,编写数据库查询保存接口,实现CURD
数据库连接成功后,我们要测试一下,能否正常查询和写入数据,因为现在数据库还是空的,我们先从插入数据开始。
什么是CURD?
它代表数据库的 创建(Create)、更新(Update)、读取(Retrieve)和删除(Delete) 4个基本操作。对于关系型数据库,可以通过sql (结构化查询语言 Structured Query Language 简称SQL) 来编写代码支持这4个基本操作,分别对应insert,update,select,delete 4条语句。
webcontext已经对insert,update,select,delete进行了封装,因此对于简单的读写操作,不需要编写原始的sql语句了。
insert
在项目目录创建一个js文件,/service/todo/add.js,webcontext实现了页面地址自动路由,不用手工编写路由代码,当访问http://localhost/service/todo/add时,会自动调用这个路径(/service/todo/add.js)文件中的onLoad方法,为add.js并编写如下代码:
module.exports= {
async onLoad() {
await this.database.insert("todo_list",{ title:"hello",status:0});
this.render({code:"OK"})
}
}
复制代码
只有4行代码, 我们来逐行解释一下
- module.exports:表示导出这个对象,这样webcontext框架才能自动引用到它,这个对象会自动从Context类继承,你不用编写任何extend或修改prototype的代码 , webcontext框架内部实现了对/service目录下的文件自动添加路由和自动继承的功能。不用任何的require和extend,具体原理也非常简单,可以看下我的这篇文章->:Node.js 实现类似于.php,.jsp的服务器页面技术,自动路由
- async onLoad 函数:首先,这是一个异步函数,由于第3行代码访问数据库用了await,所以这里必须要加async关键字,然后onLoad是一个事件,也可以说是一个回调函数,它表示这个函数的代码是在后台接收到http请求后执行。
- this.database.insert, 由于当前文件自动继承自Context类,可以通过this获取到请求对象request、响应对象response,以及database对象,database对象封装了基本的curd操作。insert方法第一个参数表示要插入的表名,第二个参数是一个对象,表示要插入的字段名和字段值,为了便于测试,我们先用死数据测试一下 { title:"hello",status:0}
- this.render 将传入的字符串或对象输出到http响应。传入object的话会自动stringify。
写好之后,我们来访问http://localhost/service/todo/add,然后使用mysql workbench查看一下数据库,发现数据已经成功入库了。 测试成功后,我们把这行写死的数据改过来吧,this.request.data["title"]表示获取post表单中的title字段。
await this.database.insert("todo_list",{ title:this.request.data["title"],status:0});
复制代码
select
现在数据库里已经有数据了,我们要把请求mock的接口改成读取真实数据。删掉list.ejs(当然不删留着它做活口也是可以的) 在/service/todo目录新建一个list.js文件,书写代码如下:
/service/todo/list.js
module.exports= {
async onLoad() {
var result=await this.database.select("todo_list")
this.render(result);
}
}
复制代码
不用过多解释了,套路和上一个页面一样,你唯一要改的就是把insert方法改成select方法,这个例子比较简单,只需要传一个表名就可以了。实际上select方法已经封装的非常强大,支持各种where条件、排序、数据库层分页、多表连接等,暂不展开讲述。
update
/service/todo/update.js
module.exports= {
async onLoad() {
await this.database.update("todo_list",this.request.data)
this.render({code:"OK"})
}
}
复制代码
update和insert写法几乎一样,为了增加点新鲜感,第二个参数直接用this.request.data表单数据了,这样可以节省很多代码,但是如果别人恶意post不合法的数据的话你的代码会报错。
delete
/service/todo/delete.js
module.exports= {
async onLoad() {
await this.database.delete("todo_list",{ id:this.request.data["id"]})
this.render({code:"OK"})
}
}
复制代码
删除,只需要表名和id参数
优化:重新编写路由
好了,现在CURD操作都已经完成了,业务代码只有8行,其实可以更少,因为mysql 支持replace into语句,insert 和 update可以合二为一。即使加上一些参数的合法性校验,代码量也是非常少的。现在前后台分离之后,数据库业务的后台开发 真的要比前端要简单很多,除了一些多表连接和统计的sql语句比较难写之外,其它都是重复度很高的数据操作代码,不过话说回来,自从前端普及了vue,react之后,开发门槛也降低了很多。
这个项目比较简单,后台只有这么几行代码,却要拆成4个文件实现,能不能更精简一点呢,答案是肯定的,webcontext支持通过扩名直接映到js文件中的函数!例如请求http://localhost/todo.list,会直接调用/service/todo.js中的list()方法。我们删掉刚才编写的todo目录的代码,重新实现 ,把他们整理到一个文件中即可。
/service/todo.js
module.exports= {
onRequest() {},
async list() {
var result=await this.database.select("todo_list")
this.render(result);
},
async add() {
await this.database.insert("todo_list",{ title:this.request.data["title"],status:0});
this.render({code:"OK"})
},
async update() {
await this.database.update("todo_list",this.request.data)
this.render({code:"OK"})
},
async delete() {
await this.database.delete("todo_list",{ id:this.request.data["id"]})
this.render({code:"OK"})
}
}
复制代码
我们在地址栏,访问http://localhost/todo.list 测试一下,成功!能够正常返回数据。
第四次迭代,增加添加,保存和删除前端代码
现在后台接口都完成了,但是添加、修改、删除的前端代码还没有实现,我们用事件委托的方式实现,前端代码都非常简单不再详细描述,完整代码如下。
function saveItem(target){
var li=$(target).parents("li");
var id=li.attr("itemId")
var title=li.find("input[type=textbox]").val();
var status=li.find("input[type=checkbox]").prop("checked")?1:0;
$.post("http://localhost/todo.update",{id:id,title:title,status:status},function (res){
if(res.code="OK"){
render();
}
});
}
function deleteItem(target){
var id=$(target).parents("li").attr("itemId")
$.post("http://localhost/todo.delete",{id:id},function (res){
if(res.code="OK"){
render();
}
});
}
$("#list").on("change","input[type=textbox]",function (e){
saveItem(e.target);
})
$("#list").on("click","input[type=checkbox]",function (e){
saveItem(e.target)
})
$("#list").on("click",".delete",function (e){
deleteItem(e.target)
})
$("#btn_add").click("click",function (e){
$.post("http://localhost/todo.add",{title:$("#title").val()},function (res){
if(res.code="OK"){
render();
}
});
})
复制代码
清理战场 ,静态文件迁移部署到http服务器
现在所有代码都已经完工,可是html和js文件总不能一直放在本地吧,webcontext已经内置了静态文件服务,只需要把本地的index.html和jquery.js存放在站点根目录下/client目录下,再来访问http://localhost/index.html,就可以访问到了。 具体原理请参考:Node.js 实现类似于.php,.jsp的服务器页面技术,自动路由
最后,既然已经都在同一个http路径下了,可以把代码里$.post的绝对路径都改成相对路径。
总结
本文用了8行代码实现了todolist 后台接口,为什么只需要这么少的代码,源于webcontext的设计理念:约定优于配置,默认配置优于手工配置,配置优于编码,将开发者的用户体验放在第一位。
举例来说:
- 它实现了服务器页面技术,不再需要添加路由的代码,因为页面地址本身已经包含了路由信息,一个文件处理一个请求,也便于解耦,也可以一个文件处理多个请求,通过url直接调用js文件中的方法,如todo.list就调用todo.js文件中的list方法,不用每增加一个请求路径就去修改路由文件。
- 配置了数据库连接字符串,自动连接数据库,一行代码实现CURD,也实现了ORM数据库实体映射功能,实现了不需要写sql就可以操作数据库。
- 默认配置文件是自动生成的,大部分情况下都不需要修改这些配置。
- 对于表单、上传、json,各个环节都是自动解析为对象的,甚至可以将表单数据直接写入数据库,省去中间转换参数的冗余代码
webcontext的结构设计参考了asp.net的优雅设计,用一个context对象做为容器,非常方便 的调用request,response,session这些http请求处理操作类,即使在ejs模板中,也可以通过this获取到context,直接进行http处理或数据库操作。并扩展了许多功能,如使用database对象操作数据库,logger对象写入日志等。 主要结构如下:
虽然提供了如此多的功能,它的代码却非常精简,只有千行左右,很容易读懂,它虽然强大,却不是阳春白春,并不高深,如果你想了解一个web框架是如何铸成的,不妨来读一下它的源码,如果你认为有用,请不要吝惜你的star,它现在还是一棵小树,需要你的支持。
github地址:github.com/windyfancy/…
本文的全部代码也已经上传github,在webcontext_examples项目中,有需要的同学可以自行查阅。