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 相关文章推荐
基于jQuery实现的Ajax 验证用户名是否存在的实现代码
Apr 06 Javascript
JqGrid web打印实现代码
May 31 Javascript
如何根据百度地图计算出两地之间的驾驶距离(两种语言js和C#)
Oct 29 Javascript
理解JavaScript原型链
Oct 25 Javascript
利用JQuery实现datatables插件的增加和删除行功能
Jan 06 Javascript
在angular 6中使用 less 的实例代码
May 13 Javascript
VUE 实现滚动监听 导航栏置顶的方法
Sep 11 Javascript
Vue3.x源码调试的实现方法
Oct 13 Javascript
Javascript作用域和作用域链原理解析
Mar 03 Javascript
详解javascript void(0)
Jul 13 Javascript
使用vue构建多页面应用的示例
Oct 22 Javascript
微信小程序实现点击导航标签滚动定位到对应位置
Nov 19 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
国内php原创论坛
2006/10/09 PHP
关于PHP开发的9条建议
2015/07/27 PHP
分享PHP源码批量抓取远程网页图片并保存到本地的实现方法
2015/12/01 PHP
PHP实现搜索地理位置及计算两点地理位置间距离的实例
2016/01/08 PHP
IE7中javascript操作CheckBox的checked=true不打勾的解决方法
2009/12/07 Javascript
js操作textarea方法集合封装(兼容IE,firefox)
2011/02/22 Javascript
使用js检测浏览器是否支持html5中的video标签的方法
2014/03/12 Javascript
轻松创建nodejs服务器(6):作出响应
2014/12/18 NodeJs
JQuery实现展开关闭层的方法
2015/02/17 Javascript
JavaScript 函数的执行过程
2016/05/09 Javascript
ionic js 模型 $ionicModal 可以遮住用户主界面的内容框
2016/06/06 Javascript
用原生js统计文本行数的简单示例
2016/08/19 Javascript
利用JavaScript判断浏览器类型及版本
2016/08/23 Javascript
Angularjs 动态添加指令并绑定事件的方法
2017/04/13 Javascript
Vue2.0 从零开始_环境搭建操作步骤
2017/06/14 Javascript
浅析JS中常用类型转换及运算符表达式
2017/07/23 Javascript
JavaScript如何获取到导航条中HTTP信息
2017/10/10 Javascript
在vue项目中使用Nprogress.js进度条的方法
2018/01/31 Javascript
如何在vue中使用ts的示例代码
2018/02/28 Javascript
vue3修改link标签默认icon无效问题详解
2019/10/09 Javascript
python学习教程之Numpy和Pandas的使用
2017/09/11 Python
python如何为创建大量实例节省内存
2018/03/20 Python
浅述python中深浅拷贝原理
2018/09/18 Python
对python中list的拷贝与numpy的array的拷贝详解
2019/01/29 Python
对Python 简单串口收发GUI界面的实例详解
2019/06/12 Python
pandas 对日期类型数据的处理方法详解
2019/08/08 Python
Python生成器generator原理及用法解析
2020/07/20 Python
python爬虫beautifulsoup解析html方法
2020/12/07 Python
纽约JewelryAffairs珠宝店:精细金银时尚首饰
2017/02/05 全球购物
波兰家居和花园家具专家:4Home
2019/05/26 全球购物
护理专业毕业生自我鉴定
2013/10/08 职场文书
招商业务员岗位职责
2013/12/16 职场文书
幼儿园开学寄语
2014/04/03 职场文书
高二学年自我鉴定范文(2篇)
2014/09/26 职场文书
国庆节慰问信
2015/02/15 职场文书
治庸问责工作总结
2015/08/11 职场文书