【免費(fèi)公開(kāi)課】手把手教你實(shí)現(xiàn)Node.js Express框架

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

推薦閱讀更多精彩內(nèi)容

  • 剛接觸 js 的同學(xué)在學(xué)到 ajax 的時(shí)候經(jīng)常會(huì)懵掉,歸根結(jié)底就是對(duì)所謂的“后臺(tái)”、“服務(wù)器”這些概念沒(méi)有任何概...
    盒子愛(ài)上貓閱讀 1,697評(píng)論 0 6
  • 前言 剛接觸 js 的同學(xué)在學(xué)到 ajax 的時(shí)候經(jīng)常會(huì)懵掉,歸根結(jié)底就是對(duì)所謂的“后臺(tái)”、“服務(wù)器”這些概念沒(méi)有...
    饑人谷_茜茜閱讀 258評(píng)論 0 0
  • 無(wú)我 自我 忘我 時(shí)間 天命 人說(shuō) 我能想到的主題被時(shí)光篩去 留下的意識(shí)淡淡 菊叢里剛閃過(guò)一道人影 或許是只貓罷
    翔于閱讀 159評(píng)論 0 0
  • 看到你, 便忘記天空孤獨(dú)的樣子, 似乎本該如此。 語(yǔ)言, 才是最無(wú)力的表達(dá), 難辨真假。 駐足靜默, 相視而笑, ...
    芯滿亦足閱讀 371評(píng)論 0 5
  • 中國(guó)的市場(chǎng)蛋糕:(現(xiàn)狀、需求、供應(yīng)關(guān)系、等) 中國(guó)經(jīng)濟(jì)轉(zhuǎn)型、中國(guó)新型政商關(guān)系 全球經(jīng)濟(jì)增速、全球化進(jìn)程 科技創(chuàng)新、...
    西陽(yáng)關(guān)閱讀 134評(píng)論 0 1