手把手教你实现Node.js Express框架
刚接触 js 的同学在学到 ajax 的时候经常会懵掉,归根结底就是对所谓的“后台”、“服务器”这些概念没有任何概念。课程中我讲过 Express 做后台,甚至写了个简单易用的 mock 工具 server-mock 来方便同学模拟数据,但经常会出现类似下面的对话:
同学:“你推荐的框架和工具我用了,用的也很爽,可是框架工具的外衣下到底发生了什么?除了 mock 数据,我还想做 HTTP 的缓存控制的测试、想做白屏和 FOUC的效果重现测试、想做静态资源加载顺序的测试、想做跨域的测试... ,如果我不明白里面后台到底发生了什么还不如叫我去死...”
我:"多用多练,学到后面你自然就懂了,不甘心你可以先看看 Express 的源码"
同学:“我用都还没用熟练... 杀了我吧...”
如果想追根溯源,看源码真的是唯一途径,无奈源码实在是太枯燥,为了功能的完善流行的框架引入太多和主线流程不先关的东西。即使偶尔能找到一些不错的源码解析的文章,也是又臭又硬,完全不适合缺少经验的初学者。所以之前答应同学近期安排一次好懂有用的直播公开课,专门讲解服务器和后端框架,尽量让不管是前端小白还是前端老鸟都有收获。
直播内容
本次直播课涉及的内容如下:
- step0. 我们先使用 Nodejs 的入门知识搭建一个服务器
- step1. 对搭建的服务器功能进行扩展,使之成为一个能用的静态服务器
- step2. 继续扩展,让我们的静态服务器能解析路由,把服务器变成一个支持静态目录和动态路由的“网站”
- step3. 模拟 Node.js 的后端框架 Express的使用方法,实现一个包含静态目录设置、中间件处理、路由匹配的迷你 Express 框架
- step4. 完善这个框架
适合的对象
不需要你有万行代码的编写经验、不需要你精通/掌握/熟悉 Node.js,你只需要
- 有一些 js 使用经验,有一点 nodejs的使用经验,即可理解1、2、3对应的内容。
- 如果你有一点后端基础,有一点Express 框架的使用经验,那么你就能理解4、5对应的内容。
我能学到什么
- 你会对服务器、对后端框架有一个清晰的认识
- 平时搭个静态服务器或者 Mock 数据不再需要使用别人的东西
- 会对 HTTP 有更深入的认识
- 中间件、异步有一定认识
- 装 13利器,以后简历里可以写自己不使用任何第三方模块,实现一个类 Express 的后端框架
课程安排
直播时间: 本周三(7月5日)晚上8点30
**参与方式:加 QQ 群:617043164 ,入群申请:简书Node框架。
也可以在微信搜索小程序『饥人谷』,上课的时候如果不方便看电脑可在手机微信小程序上观看直播
关于我
我是若愚,曾经在百度、阿里巴巴做前端开发,后创业加入饥人谷做前端老师。亲手培养了近300名同学
一些同学去了Facebook、大众点评、美团、头条、金山、百度、阿里(外包)、华为(外包)
一些同学在小公司当技术 leader
一些同学去国外做菠菜网站拿着让人"震惊"的待遇
一些同学自己做外包当老板收入甚至远超老师
当然还有一些同学,令人悲伤的中途退课转了行
他们来自五湖四海各行各业,海龟、名校高材生、火车司机、航海员、旅游软件销售、全职妈妈、装配厂工人、方便面制造师、中科院研究所员工、美工、产品、后端、机械/化工/传媒/英语/新闻从业者....,但最终殊(误)途(入)同(歧)归(途)
直播剧透
以下是公开课课堂上涉及的代码,有兴趣参加公开课的同学可以提前阅读、理解、复制、允许代码,Github 的链接直播课堂上放出。课堂上会进行一步步讲解,任何疑问都可在直播课和老师直接互动。
step0: 创建一个服务器
index.js
var http = require('http')
var server = http.createServer(function(request, response){
//response.setHeader('Content-Type', 'text/html')
//response.setHeader('X-Powered-By', 'Jirengu')
response.end('hello world')
})
console.log('open http://localhost:8080')
server.listen(8080)
step1:搭建静态服务器
var http = require('http')
var path = require('path')
var fs = require('fs')
var url = require('url')
function staticRoot(staticPath, req, res){
var pathObj = url.parse(req.url, true)
var filePath = path.resolve(staticPath, pathObj.pathname.substr(1))
var fileContent = fs.readFileSync(filePath,'binary')
res.writeHead(200, 'Ok')
res.write(fileContent, 'binary')
res.end()
}
var server = http.createServer(function(req, res){
staticRoot(path.resolve(__dirname, 'static'), req, res)
})
server.listen(8080)
console.log('visit http://localhost:8080' )
step2: 解析路由
var http = require('http')
var path = require('path')
var fs = require('fs')
var url = require('url')
var routes = {
'/a': function(req, res){
res.end('match /a, query is:' + JSON.stringify(req.query))
},
'/b': function(req, res){
res.end('match /b')
},
'/a/c.js': function(req, res){
res.end('match /a/c.js')
},
'/search': function(req, res){
res.end('username='+req.body.username+',password='+req.body.password)
}
}
var server = http.createServer(function(req, res){
routePath(req, res)
})
server.listen(8080)
console.log('visit http://localhost:8080' )
function routePath(req, res){
var pathObj = url.parse(req.url, true)
var handleFn = routes[pathObj.pathname]
if(handleFn){
req.query = pathObj.query
var body = ''
req.on('data', function(chunk){
body += chunk
}).on('end', function(){
req.body = parseBody(body)
handleFn(req, res)
})
}else {
staticRoot(path.resolve(__dirname, 'static'), req, res)
}
}
function staticRoot(staticPath, req, res){
var pathObj = url.parse(req.url, true)
var filePath = path.resolve(staticPath, pathObj.pathname.substr(1))
fs.readFile(filePath,'binary', function(err, content){
if(err){
res.writeHead('404', 'haha Not Found')
return res.end()
}
res.writeHead(200, 'Ok')
res.write(content, 'binary')
res.end()
})
}
function parseBody(body){
var obj = {}
body.split('&').forEach(function(str){
obj[str.split('=')[0]] = str.split('=')[1]
})
return obj
}
step3:Express 雏形
文件目录结构
bin
- www
lib
- express.js
app.js
可通过 node bin/www
命令启动服务器
bin/www
var app = require('../app')
var http = require('http')
http.createServer(app).listen(8080)
console.log('open http://localhost:8080')
app.js
var express = require('./lib/express')
var path = require('path')
var app = express()
app.use(function(req, res, next) {
console.log('middleware 1')
next()
})
app.use(function(req, res, next) {
console.log('middleware 12')
next()
})
app.use('/hello', function(req, res){
console.log('/hello..')
res.send('hello world')
})
app.use('/getWeather', function(req, res){
res.send({url:'/getWeather', city: req.query.city})
})
app.use(function(req, res){
res.send(404, 'haha Not Found')
})
module.exports = app
lib/express.js
var url = require('url')
function express() {
var tasks = []
var app = function(req, res){
makeQuery(req)
makeResponse(res)
//post 的解析未实现
var i = 0
function next() {
var task = tasks[i++]
if(!task) {
return
}
//如果是普通的中间件 或者 是路由中间件
if(task.routePath === null || url.parse(req.url, true).pathname === task.routePath){
task.middleWare(req, res, next)
}else{
//如果说路由未匹配上的中间件,直接下一个
next()
}
}
next()
}
app.use = function(routePath, middleWare){
if(typeof routePath === 'function') {
middleWare = routePath
routePath = null
}
tasks.push({
routePath: routePath,
middleWare: middleWare
})
}
return app
}
express.static = function(path){
return function(req, res){
//未实现
}
}
module.exports = express
function makeQuery(req){
var pathObj = url.parse(req.url, true)
req.query = pathObj.query
}
function makeResponse(res){
res.send = function(toSend){
if(typeof toSend === 'string'){
res.end(toSend)
}
if(typeof toSend === 'object'){
res.end(JSON.stringify(toSend))
}
if(typeof toSend === 'number'){
res.writeHead(toSend, arguments[1])
res.end()
}
}
}
step4: 框架完善
文件目录结构
bin
- www
lib
- express.js
- body-parser.js
app.js
可通过 node bin/www
命令启动服务器
bin/www
var app = require('../app')
var http = require('http')
http.createServer(app).listen(8080)
console.log('open http://localhost:8080')
app.js
var express = require('./lib/express')
var path = require('path')
var bodyParser = require('./lib/body-parser')
var app = express()
//新增 bodyParser 中间件
app.use(bodyParser)
//新增 express.static 方法设置静态目录
app.use(express.static(path.join(__dirname, 'static')))
app.use(function(req, res, next) {
console.log('middleware 1')
next()
})
app.use(function(req, res, next) {
console.log('middleware 12')
next()
})
app.use('/hello', function(req, res){
console.log('/hello..')
res.send('hello world')
})
app.use('/getWeather', function(req, res){
res.send({url:'/getWeather', city: req.query.city})
})
app.use('/search', function(req, res){
res.send(req.body)
})
app.use(function(req, res){
res.send(404, 'haha Not Found')
})
module.exports = app
lib/express.js
var url = require('url')
var fs = require('fs')
var path = require('path')
function express() {
var tasks = []
var app = function(req, res){
makeQuery(req)
makeResponse(res)
console.log(tasks)
var i = 0
function next() {
var task = tasks[i++]
if(!task) {
return
}
if(task.routePath === null || url.parse(req.url, true).pathname === task.routePath){
task.middleWare(req, res, next)
}else{
next()
}
}
next()
}
app.use = function(routePath, middleWare){
if(typeof routePath === 'function') {
middleWare = routePath
routePath = null
}
tasks.push({
routePath: routePath,
middleWare: middleWare
})
}
return app
}
express.static = function(staticPath){
return function(req, res, next){
var pathObj = url.parse(req.url, true)
var filePath = path.resolve(staticPath, pathObj.pathname.substr(1))
console.log(filePath)
fs.readFile(filePath,'binary', function(err, content){
if(err){
next()
}else {
res.writeHead(200, 'Ok')
res.write(content, 'binary')
res.end()
}
})
}
}
module.exports = express
function makeQuery(req){
var pathObj = url.parse(req.url, true)
req.query = pathObj.query
}
function makeResponse(res){
res.send = function(toSend){
if(typeof toSend === 'string'){
res.end(toSend)
}
if(typeof toSend === 'object'){
res.end(JSON.stringify(toSend))
}
if(typeof toSend === 'number'){
res.writeHead(toSend, arguments[1])
res.end()
}
}
}
lib/body-parser.js
function bodyParser(req, res, next){
var body = ''
req.on('data', function(chunk){
body += chunk
}).on('end', function(){
req.body = parseBody(body)
next()
})
}
function parseBody(body){
var obj = {}
body.split('&').forEach(function(str){
obj[str.split('=')[0]] = str.split('=')[1]
})
return obj
}
module.exports = bodyParser