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 相关文章推荐
在页面上点击任一链接时触发一个事件的代码
Apr 07 Javascript
基于jQuery的弹出消息插件 DivAlert之旅(一)
Apr 01 Javascript
再谈querySelector和querySelectorAll的区别与联系
Apr 20 Javascript
js中的eventType事件及其浏览器支持性介绍
Nov 29 Javascript
JavaScript动态创建link标签到head里的方法
Dec 22 Javascript
JavaScript生成随机字符串的方法
Mar 19 Javascript
Javascript打印局部页面实例
Jun 21 Javascript
微信小程序 获取微信OpenId详解及实例代码
Oct 31 Javascript
详解vue组件化开发-vuex状态管理库
Apr 10 Javascript
详解ES6语法之可迭代协议和迭代器协议
Jan 13 Javascript
使用Vue-cli 3.0搭建Vue项目的方法
Jun 07 Javascript
解决layer.open弹出框不能获取input框的值为空的问题
Sep 10 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内置访问资源的超时时间 time_out file_get_contents read_file
2013/06/03 PHP
PHP扩展框架之Yaf框架的安装与使用
2016/05/18 PHP
深入理解PHP JSON数组与对象
2016/07/19 PHP
解决laravel id非自增 模型取回为0 的问题
2019/10/11 PHP
jquery写个checkbox——类似邮箱全选功能
2013/03/19 Javascript
js实现省市联动效果的简单实例
2014/02/10 Javascript
可恶的ie8提示缺少id未定义
2014/03/20 Javascript
javaScript年份下拉列表框内容为当前年份及前后50年
2014/05/28 Javascript
基于JavaScript代码实现微信扫一扫下载APP
2015/12/30 Javascript
JS转换HTML转义符的方法
2016/08/24 Javascript
jquery对所有input type=text的控件赋值实现方法
2016/12/02 Javascript
原生js验证简洁注册登录页面
2016/12/17 Javascript
jquery操作select取值赋值与设置选中实例
2017/02/28 Javascript
JS简单实现点击按钮或文字显示遮罩层的方法
2017/04/27 Javascript
node文字生成图片的示例代码
2017/10/26 Javascript
使用vue 国际化i18n 实现多实现语言切换功能
2018/10/11 Javascript
vue-cli基础配置及webpack配置修改的完整步骤
2019/10/20 Javascript
js实现消灭星星(web简易版)
2020/03/24 Javascript
Python字符串处理之count()方法的使用
2015/05/18 Python
Python实现批量将word转html并将html内容发布至网站的方法
2015/07/14 Python
python3+dlib实现人脸识别和情绪分析
2018/04/21 Python
python使用 zip 同时迭代多个序列示例
2019/07/06 Python
Python3 列表,数组,矩阵的相互转换的方法示例
2019/08/05 Python
如何使用pandas读取txt文件中指定的列(有无标题)
2020/03/05 Python
python 实现rolling和apply函数的向下取值操作
2020/06/08 Python
python cv2.resize函数high和width注意事项说明
2020/07/05 Python
英国最大的汽车配件在线商店:Euro Car Parts
2019/09/30 全球购物
进修护士自我鉴定
2013/10/14 职场文书
大学本科生的个人自我评价
2013/12/09 职场文书
医学院校毕业生自荐信范文
2014/01/01 职场文书
项目投资建议书
2014/05/16 职场文书
博士生求职信
2014/07/06 职场文书
黄石寨导游词
2015/02/05 职场文书
2015年秋学期师德师风建设工作总结
2015/10/23 职场文书
详解NodeJS模块化
2021/06/15 NodeJs
直播实况, OMG破敌三路五十分钟大战神技局摩托车
2022/04/01 DOTA