Node.js原生api搭建web服务器的方法步骤


Posted in Javascript onFebruary 15, 2019

node.js 实现一个简单的 web 服务器还是比较简单的,以前利用 express 框架实现过『nodeJS搭一个简单的(代理)web服务器』。代码量很少,可是使用时需要安装依赖,多处使用难免有点不方便。于是便有了完全使用原生 api 来重写的想法,也当作一次 node.js 复习。

1、静态 web 服务器

'use strict' 
 
const http = require('http') 
const url = require('url') 
const fs = require('fs') 
const path = require('path') 
const cp = require('child_process') 
 
const port = 8080 
const hostname = 'localhost' 
 
// 创建 http 服务 
let httpServer = http.createServer(processStatic) 
// 设置监听端口 
httpServer.listen(port, hostname, () => {  
 console.log(`app is running at port:${port}`)  
 console.log(`url: http://${hostname}:${port}`) 
 cp.exec(`explorer http://${hostname}:${port}`, () => {}) 
}) 
// 处理静态资源 
function processStatic(req, res) {  
 const mime = { 
  css: 'text/css', 
  gif: 'image/gif', 
  html: 'text/html', 
  ico: 'image/x-icon', 
  jpeg: 'image/jpeg', 
  jpg: 'image/jpeg', 
  js: 'text/javascript', 
  json: 'application/json', 
  pdf: 'application/pdf', 
  png: 'image/png', 
  svg: 'image/svg+xml', 
  woff: 'application/x-font-woff', 
  woff2: 'application/x-font-woff', 
  swf: 'application/x-shockwave-flash', 
  tiff: 'image/tiff', 
  txt: 'text/plain', 
  wav: 'audio/x-wav', 
  wma: 'audio/x-ms-wma', 
  wmv: 'video/x-ms-wmv', 
  xml: 'text/xml' 
 }  
 const requestUrl = req.url  
 let pathName = url.parse(requestUrl).pathname  
 // 中文乱码处理 
 pathName = decodeURI(pathName)  
 let ext = path.extname(pathName)  
 // 特殊 url 处理 
 if (!pathName.endsWith('/') && ext === '' && !requestUrl.includes('?')) { 
  pathName += '/' 
  const redirect = `http://${req.headers.host}${pathName}` 
  redirectUrl(redirect, res) 
 }  
 // 解释 url 对应的资源文件路径 
 let filePath = path.resolve(__dirname + pathName)  
 // 设置 mime 
 ext = ext ? ext.slice(1) : 'unknown' 
 const contentType = mime[ext] || 'text/plain' 
 
 // 处理资源文件 
 fs.stat(filePath, (err, stats) => {   
  if (err) { 
   res.writeHead(404, { 'content-type': 'text/html;charset=utf-8' }) 
   res.end('<h1>404 Not Found</h1>')    
   return 
  }   
  // 处理文件 
  if (stats.isFile()) { 
   readFile(filePath, contentType, res) 
  }   
  // 处理目录 
  if (stats.isDirectory()) {    
   let html = "<head><meta charset = 'utf-8'/></head><body><ul>" 
   // 遍历文件目录,以超链接返回,方便用户选择 
   fs.readdir(filePath, (err, files) => {     
    if (err) { 
     res.writeHead(500, { 'content-type': contentType }) 
     res.end('<h1>500 Server Error</h1>') 
     return 
    } else {      
     for (let file of files) {       
      if (file === 'index.html') {        
       const redirect = `http://${req.headers.host}${pathName}index.html` 
       redirectUrl(redirect, res) 
      } 
      html += `<li><a href='${file}'>${file}</a></li>` 
     } 
     html += '</ul></body>' 
     res.writeHead(200, { 'content-type': 'text/html' }) 
     res.end(html) 
    } 
   }) 
  } 
 }) 
} 
// 重定向处理 
function redirectUrl(url, res) { 
 url = encodeURI(url) 
 res.writeHead(302, { 
  location: url 
 }) 
 res.end() 
} 
// 文件读取 
function readFile(filePath, contentType, res) { 
 res.writeHead(200, { 'content-type': contentType }) 
 const stream = fs.createReadStream(filePath) 
 stream.on('error', function() { 
  res.writeHead(500, { 'content-type': contentType }) 
  res.end('<h1>500 Server Error</h1>') 
 }) 
 stream.pipe(res) 
}

2、代理功能

// 代理列表 
const proxyTable = { 
 '/api': { 
  target: 'http://127.0.0.1:8090/api', 
  changeOrigin: true 
 } 
} 
// 处理代理列表 
function processProxy(req, res) {  
 const requestUrl = req.url  
 const proxy = Object.keys(proxyTable)  
 let not_found = true 
 for (let index = 0; index < proxy.length; index++) {   
   const k = proxy[index]   
   const i = requestUrl.indexOf(k)   
   if (i >= 0) { 
    not_found = false 
    const element = proxyTable[k]    
    const newUrl = element.target + requestUrl.slice(i + k.length)    
    if (requestUrl !== newUrl) {    
     const u = url.parse(newUrl, true)     
     const options = { 
      hostname : u.hostname, 
      port   : u.port || 80, 
      path   : u.path,    
      method  : req.method, 
      headers : req.headers, 
      timeout : 6000 
     }     
     if(element.changeOrigin){ 
      options.headers['host'] = u.hostname + ':' + ( u.port || 80) 
     }     
     const request = http 
     .request(options, response => {       
      // cookie 处理 
      if(element.changeOrigin && response.headers['set-cookie']){ 
       response.headers['set-cookie'] = getHeaderOverride(response.headers['set-cookie']) 
      } 
      res.writeHead(response.statusCode, response.headers) 
      response.pipe(res) 
     }) 
     .on('error', err => {      
      res.statusCode = 503 
      res.end() 
     }) 
    req.pipe(request) 
   }    
   break 
  } 
 }  
 return not_found 
} 
function getHeaderOverride(value){  
 if (Array.isArray(value)) {    
  for (var i = 0; i < value.length; i++ ) { 
   value[i] = replaceDomain(value[i]) 
  } 
 } else { 
  value = replaceDomain(value) 
 }  
 return value 
} 
function replaceDomain(value) {  
 return value.replace(/domain=[a-z.]*;/,'domain=.localhost;').replace(/secure/, '') 
}

3、完整版

服务器接收到 http 请求,首先处理代理列表 proxyTable,然后再处理静态资源。虽然这里面只有二个步骤,但如果按照先后顺序编码,这种方式显然不够灵活,不利于以后功能的扩展。koa 框架的中间件就是一个很好的解决方案。完整代码如下:

'use strict' 
 
const http = require('http') 
const url = require('url') 
const fs = require('fs') 
const path = require('path') 
const cp = require('child_process') 
// 处理静态资源 
function processStatic(req, res) {  
 const mime = { 
  css: 'text/css', 
  gif: 'image/gif', 
  html: 'text/html', 
  ico: 'image/x-icon', 
  jpeg: 'image/jpeg', 
  jpg: 'image/jpeg', 
  js: 'text/javascript', 
  json: 'application/json', 
  pdf: 'application/pdf', 
  png: 'image/png', 
  svg: 'image/svg+xml', 
  woff: 'application/x-font-woff', 
  woff2: 'application/x-font-woff', 
  swf: 'application/x-shockwave-flash', 
  tiff: 'image/tiff', 
  txt: 'text/plain', 
  wav: 'audio/x-wav', 
  wma: 'audio/x-ms-wma', 
  wmv: 'video/x-ms-wmv', 
  xml: 'text/xml' 
 }  
 const requestUrl = req.url  
 let pathName = url.parse(requestUrl).pathname  
 // 中文乱码处理 
 pathName = decodeURI(pathName)  
 let ext = path.extname(pathName)  
 // 特殊 url 处理 
 if (!pathName.endsWith('/') && ext === '' && !requestUrl.includes('?')) { 
  pathName += '/' 
  const redirect = `http://${req.headers.host}${pathName}` 
  redirectUrl(redirect, res) 
 }  
 // 解释 url 对应的资源文件路径 
 let filePath = path.resolve(__dirname + pathName)  
 // 设置 mime 
 ext = ext ? ext.slice(1) : 'unknown' 
 const contentType = mime[ext] || 'text/plain' 
 
 // 处理资源文件 
 fs.stat(filePath, (err, stats) => {   
  if (err) { 
   res.writeHead(404, { 'content-type': 'text/html;charset=utf-8' }) 
   res.end('<h1>404 Not Found</h1>')    
   return 
  }  // 处理文件 
  if (stats.isFile()) { 
   readFile(filePath, contentType, res) 
  }  // 处理目录 
  if (stats.isDirectory()) {    
   let html = "<head><meta charset = 'utf-8'/></head><body><ul>" 
   // 遍历文件目录,以超链接返回,方便用户选择 
   fs.readdir(filePath, (err, files) => {     
    if (err) { 
     res.writeHead(500, { 'content-type': contentType }) 
     res.end('<h1>500 Server Error</h1>') 
     return 
    } else {     
      for (let file of files) {       
      if (file === 'index.html') {       
       const redirect = `http://${req.headers.host}${pathName}index.html` 
       redirectUrl(redirect, res) 
      } 
      html += `<li><a href='${file}'>${file}</a></li>` 
     } 
     html += '</ul></body>' 
     res.writeHead(200, { 'content-type': 'text/html' }) 
     res.end(html) 
    } 
   }) 
  } 
 }) 
} 
// 重定向处理 
function redirectUrl(url, res) { 
 url = encodeURI(url) 
 res.writeHead(302, { 
  location: url 
 }) 
 res.end() 
} 
// 文件读取 
function readFile(filePath, contentType, res) { 
 res.writeHead(200, { 'content-type': contentType }) 
 const stream = fs.createReadStream(filePath) 
 stream.on('error', function() { 
  res.writeHead(500, { 'content-type': contentType }) 
  res.end('<h1>500 Server Error</h1>') 
 }) 
 stream.pipe(res) 
} 
// 处理代理列表 
function processProxy(req, res) { 
 const requestUrl = req.url 
 const proxy = Object.keys(proxyTable) 
 let not_found = true 
 for (let index = 0; index < proxy.length; index++) {   
  const k = proxy[index]   
  const i = requestUrl.indexOf(k)   
  if (i >= 0) { 
   not_found = false 
   const element = proxyTable[k] 
   const newUrl = element.target + requestUrl.slice(i + k.length) 
 
   if (requestUrl !== newUrl) { 
    const u = url.parse(newUrl, true) 
    const options = { 
     hostname : u.hostname, 
     port   : u.port || 80, 
     path   : u.path,    
     method  : req.method, 
     headers : req.headers, 
     timeout : 6000 
    }; 
    if(element.changeOrigin){ 
     options.headers['host'] = u.hostname + ':' + ( u.port || 80) 
    } 
    const request = 
     http.request(options, response => {         
      // cookie 处理 
      if(element.changeOrigin && response.headers['set-cookie']){ 
       response.headers['set-cookie'] = getHeaderOverride(response.headers['set-cookie']) 
      } 
      res.writeHead(response.statusCode, response.headers) 
      response.pipe(res) 
     }) 
     .on('error', err => { 
      res.statusCode = 503 
      res.end() 
     }) 
    req.pipe(request) 
   } 
   break 
  } 
 } 
 return not_found 
} 
function getHeaderOverride(value){ 
 if (Array.isArray(value)) { 
   for (var i = 0; i < value.length; i++ ) {     
     value[i] = replaceDomain(value[i]) 
   } 
 } else { 
   value = replaceDomain(value) 
 } 
 return value} 
function replaceDomain(value) { 
 return value.replace(/domain=[a-z.]*;/,'domain=.localhost;').replace(/secure/, '') 
} 
function compose (middleware) { 
 if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')  
 for (const fn of middleware) {   
  if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') 
 }  
 return function (context, next) { 
  // 记录上一次执行中间件的位置   
  let index = -1 
  return dispatch(0)  
  function dispatch (i) { 
   // 理论上 i 会大于 index,因为每次执行一次都会把 i递增, 
   // 如果相等或者小于,则说明next()执行了多次   
   if (i <= index) return Promise.reject(new Error('next() called multiple times'))    
   index = i 
   let fn = middleware[i]    
   if (i === middleware.length) fn = next 
   if (!fn) return Promise.resolve()   
   try {    
    return Promise.resolve(fn(context, function next () {  
      return dispatch(i + 1) 
    })) 
   } catch (err) {     
     return Promise.reject(err) 
   } 
  } 
 } 
} 
function Router(){  
 this.middleware = [] 
} 
Router.prototype.use = function (fn){  
 if (typeof fn !== 'function') throw new TypeError('middleware must be a function!') 
 this.middleware.push(fn) 
 return this} 
Router.prototype.callback= function() {  
 const fn = compose(this.middleware)  
 const handleRequest = (req, res) => { 
  const ctx = {req, res} 
  return this.handleRequest(ctx, fn) 
 } 
 return handleRequest 
} 
Router.prototype.handleRequest= function(ctx, fn) { 
 fn(ctx) 
} 
 
// 代理列表 
const proxyTable = { 
 '/api': { 
  target: 'http://127.0.0.1:8090/api', 
  changeOrigin: true 
 } 
} 
 
const port = 8080 
const hostname = 'localhost' 
const appRouter = new Router() 
 
// 使用中间件 
appRouter.use(async(ctx,next)=>{ 
 if(processProxy(ctx.req, ctx.res)){ 
  next() 
 } 
}).use(async(ctx)=>{ 
 processStatic(ctx.req, ctx.res) 
}) 
 
// 创建 http 服务 
let httpServer = http.createServer(appRouter.callback()) 
 
// 设置监听端口 
httpServer.listen(port, hostname, () => { 
 console.log(`app is running at port:${port}`) 
 console.log(`url: http://${hostname}:${port}`) 
 cp.exec(`explorer http://${hostname}:${port}`, () => {}) 
})

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javscript对象原型的一些看法
Sep 19 Javascript
javascript之querySelector和querySelectorAll使用介绍
Dec 20 Javascript
如何创建一个JavaScript弹出DIV窗口层的效果
Sep 25 Javascript
document节点对象的获取方式示例介绍
Dec 24 Javascript
js鼠标滑轮滚动事件绑定的简单实例(兼容主流浏览器)
Jan 14 Javascript
JS实现文字向下滚动完整实例
Feb 06 Javascript
使用jquery组件qrcode生成二维码及应用指南
Feb 22 Javascript
jquery dataview数据视图插件使用方法
Dec 23 Javascript
基于JQuery及AJAX实现名人名言随机生成器
Feb 10 Javascript
javascript面向对象创建对象的方式小结
Jul 29 Javascript
react quill中图片上传由默认转成base64改成上传到服务器的方法
Oct 30 Javascript
Vuex中实现数据状态查询与更改
Nov 08 Javascript
jQuery实现简单的Ajax调用功能示例
Feb 15 #jQuery
vue与bootstrap实现简单用户信息添加删除功能
Feb 15 #Javascript
微信小程序实现工作时间段选择
Feb 15 #Javascript
微信小程序实现展示评分结果功能
Feb 15 #Javascript
微信小程序时间标签和时间范围的联动效果
Feb 15 #Javascript
微信小程序实现商品属性联动选择
Feb 15 #Javascript
微信小程序实现购物页面左右联动
Feb 15 #Javascript
You might like
QQ登录 PHP OAuth示例代码
2011/07/20 PHP
thinkphp控制器调度使用示例
2014/02/24 PHP
php不写闭合标签的好处
2014/03/04 PHP
php出现内存位置访问无效错误问题解决方法
2014/08/16 PHP
php购物车实现方法
2015/01/03 PHP
PHP语言对接抖音快手小红书视频/图片去水印API接口源码
2020/08/11 PHP
javascript中的事件代理初探
2014/03/08 Javascript
jQuery判断元素上是否绑定了指定事件的方法
2015/03/17 Javascript
nodejs开发微博实例
2015/03/25 NodeJs
JS获取图片lowsrc属性的方法
2015/04/01 Javascript
jQuery监听文件上传实现进度条效果的方法
2016/10/16 Javascript
JS动态添加的div点击跳转到另一页面实现代码
2017/09/30 Javascript
JS获取当前地理位置的方法
2017/10/25 Javascript
Javascript将图片的绝对路径转换为base64编码的方法
2018/01/11 Javascript
vue 本地环境跨域请求proxyTable的方法
2018/09/19 Javascript
详解原生JS回到顶部
2019/03/25 Javascript
怎样使你的 JavaScript 代码简单易读(推荐)
2019/04/16 Javascript
vue-cli基础配置及webpack配置修改的完整步骤
2019/10/20 Javascript
基于Web Audio API实现音频可视化效果
2020/06/12 Javascript
vue实现div单选多选功能
2020/07/16 Javascript
Python脚本处理空格的方法
2016/08/08 Python
完美解决安装完tensorflow后pip无法使用的问题
2018/06/11 Python
matplotlib实现区域颜色填充
2019/03/18 Python
Django框架自定义session处理操作示例
2019/05/27 Python
PyTorch 对应点相乘、矩阵相乘实例
2019/12/27 Python
高考考python编程是真的吗
2020/07/20 Python
Tarte Cosmetics官网:美国最受欢迎的化妆品公司之一
2017/08/24 全球购物
简述安装Slackware Linux系统的过程
2012/05/08 面试题
会计毕业生自荐信
2013/11/21 职场文书
小型女装店的创业计划书
2014/01/09 职场文书
鲜果饮品店创业计划书
2014/01/21 职场文书
公务员转正考察材料
2014/02/07 职场文书
2015年人力资源工作总结
2015/04/08 职场文书
2016年大学迎新晚会工作总结
2015/10/15 职场文书
MySQL连表查询分组去重的实现示例
2021/07/01 MySQL
Java实现HTML转为Word的示例代码
2022/06/28 Java/Android