手把手教你實現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,加群暗號:框架-簡書,最新的群公告有參加直播的方式~

關于筆者

若愚,曾經在百度、阿里巴巴做前端開發,后創業加入饑人谷做前端老師。親手培養了近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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容