【免费公开课】手把手教你实现Node.js Express框架

手把手教你实现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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 刚接触 js 的同学在学到 ajax 的时候经常会懵掉,归根结底就是对所谓的“后台”、“服务器”这些概念没有任何概...
    盒子爱上猫阅读 1,676评论 0 6
  • 前言 刚接触 js 的同学在学到 ajax 的时候经常会懵掉,归根结底就是对所谓的“后台”、“服务器”这些概念没有...
    饥人谷_茜茜阅读 228评论 0 0
  • 无我 自我 忘我 时间 天命 人说 我能想到的主题被时光筛去 留下的意识淡淡 菊丛里刚闪过一道人影 或许是只猫罢
    翔于阅读 132评论 0 0
  • 看到你, 便忘记天空孤独的样子, 似乎本该如此。 语言, 才是最无力的表达, 难辨真假。 驻足静默, 相视而笑, ...
    芯满亦足阅读 352评论 0 5
  • 中国的市场蛋糕:(现状、需求、供应关系、等) 中国经济转型、中国新型政商关系 全球经济增速、全球化进程 科技创新、...
    西阳关阅读 125评论 0 1