手把手教你實(shí)現(xiàn)Node.js Express框架
剛接觸 js 的同學(xué)在學(xué)到 ajax 的時(shí)候經(jīng)常會(huì)懵掉,歸根結(jié)底就是對(duì)所謂的“后臺(tái)”、“服務(wù)器”這些概念沒(méi)有任何概念。課程中我講過(guò) Express 做后臺(tái),甚至寫(xiě)了個(gè)簡(jiǎn)單易用的 mock 工具 server-mock 來(lái)方便同學(xué)模擬數(shù)據(jù),但經(jīng)常會(huì)出現(xiàn)類似下面的對(duì)話:
同學(xué):“你推薦的框架和工具我用了,用的也很爽,可是框架工具的外衣下到底發(fā)生了什么?除了 mock 數(shù)據(jù),我還想做 HTTP 的緩存控制的測(cè)試、想做白屏和 FOUC的效果重現(xiàn)測(cè)試、想做靜態(tài)資源加載順序的測(cè)試、想做跨域的測(cè)試... ,如果我不明白里面后臺(tái)到底發(fā)生了什么還不如叫我去死...”
我:"多用多練,學(xué)到后面你自然就懂了,不甘心你可以先看看 Express 的源碼"
同學(xué):“我用都還沒(méi)用熟練... 殺了我吧...”
如果想追根溯源,看源碼真的是唯一途徑,無(wú)奈源碼實(shí)在是太枯燥,為了功能的完善流行的框架引入太多和主線流程不先關(guān)的東西。即使偶爾能找到一些不錯(cuò)的源碼解析的文章,也是又臭又硬,完全不適合缺少經(jīng)驗(yàn)的初學(xué)者。所以之前答應(yīng)同學(xué)近期安排一次好懂有用的直播公開(kāi)課,專門(mén)講解服務(wù)器和后端框架,盡量讓不管是前端小白還是前端老鳥(niǎo)都有收獲。
直播內(nèi)容
本次直播課涉及的內(nèi)容如下:
- step0. 我們先使用 Nodejs 的入門(mén)知識(shí)搭建一個(gè)服務(wù)器
- step1. 對(duì)搭建的服務(wù)器功能進(jìn)行擴(kuò)展,使之成為一個(gè)能用的靜態(tài)服務(wù)器
- step2. 繼續(xù)擴(kuò)展,讓我們的靜態(tài)服務(wù)器能解析路由,把服務(wù)器變成一個(gè)支持靜態(tài)目錄和動(dòng)態(tài)路由的“網(wǎng)站”
- step3. 模擬 Node.js 的后端框架 Express的使用方法,實(shí)現(xiàn)一個(gè)包含靜態(tài)目錄設(shè)置、中間件處理、路由匹配的迷你 Express 框架
- step4. 完善這個(gè)框架
適合的對(duì)象
不需要你有萬(wàn)行代碼的編寫(xiě)經(jīng)驗(yàn)、不需要你精通/掌握/熟悉 Node.js,你只需要
- 有一些 js 使用經(jīng)驗(yàn),有一點(diǎn) nodejs的使用經(jīng)驗(yàn),即可理解1、2、3對(duì)應(yīng)的內(nèi)容。
- 如果你有一點(diǎn)后端基礎(chǔ),有一點(diǎn)Express 框架的使用經(jīng)驗(yàn),那么你就能理解4、5對(duì)應(yīng)的內(nèi)容。
我能學(xué)到什么
- 你會(huì)對(duì)服務(wù)器、對(duì)后端框架有一個(gè)清晰的認(rèn)識(shí)
- 平時(shí)搭個(gè)靜態(tài)服務(wù)器或者 Mock 數(shù)據(jù)不再需要使用別人的東西
- 會(huì)對(duì) HTTP 有更深入的認(rèn)識(shí)
- 中間件、異步有一定認(rèn)識(shí)
- 裝 13利器,以后簡(jiǎn)歷里可以寫(xiě)自己不使用任何第三方模塊,實(shí)現(xiàn)一個(gè)類 Express 的后端框架
課程安排
直播時(shí)間: 本周三(7月5日)晚上8點(diǎn)30
**參與方式:加 QQ 群:617043164 ,入群申請(qǐng):簡(jiǎn)書(shū)Node框架。
也可以在微信搜索小程序『饑人谷』,上課的時(shí)候如果不方便看電腦可在手機(jī)微信小程序上觀看直播
關(guān)于我
我是若愚,曾經(jīng)在百度、阿里巴巴做前端開(kāi)發(fā),后創(chuàng)業(yè)加入饑人谷做前端老師。親手培養(yǎng)了近300名同學(xué)
一些同學(xué)去了Facebook、大眾點(diǎn)評(píng)、美團(tuán)、頭條、金山、百度、阿里(外包)、華為(外包)
一些同學(xué)在小公司當(dāng)技術(shù) leader
一些同學(xué)去國(guó)外做菠菜網(wǎng)站拿著讓人"震驚"的待遇
一些同學(xué)自己做外包當(dāng)老板收入甚至遠(yuǎn)超老師
當(dāng)然還有一些同學(xué),令人悲傷的中途退課轉(zhuǎn)了行
他們來(lái)自五湖四海各行各業(yè),海龜、名校高材生、火車司機(jī)、航海員、旅游軟件銷售、全職媽媽、裝配廠工人、方便面制造師、中科院研究所員工、美工、產(chǎn)品、后端、機(jī)械/化工/傳媒/英語(yǔ)/新聞從業(yè)者....,但最終殊(誤)途(入)同(歧)歸(途)
直播劇透
以下是公開(kāi)課課堂上涉及的代碼,有興趣參加公開(kāi)課的同學(xué)可以提前閱讀、理解、復(fù)制、允許代碼,Github 的鏈接直播課堂上放出。課堂上會(huì)進(jìn)行一步步講解,任何疑問(wèn)都可在直播課和老師直接互動(dòng)。
step0: 創(chuàng)建一個(gè)服務(wù)器
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:搭建靜態(tài)服務(wù)器
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 雛形
文件目錄結(jié)構(gòu)
bin
- www
lib
- express.js
app.js
可通過(guò) node bin/www
命令啟動(dòng)服務(wù)器
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 的解析未實(shí)現(xiàn)
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{
//如果說(shuō)路由未匹配上的中間件,直接下一個(gè)
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){
//未實(shí)現(xiàn)
}
}
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: 框架完善
文件目錄結(jié)構(gòu)
bin
- www
lib
- express.js
- body-parser.js
app.js
可通過(guò) node bin/www
命令啟動(dòng)服務(wù)器
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 方法設(shè)置靜態(tài)目錄
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