【免费公开课】手把手教你实现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

推荐阅读更多精彩内容