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

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}`, () => {})
}) 

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

(0)

相关推荐

  • 从零搭建docker+jenkins+node.js自动化部署环境的方法

    本次案例基于CentOS 7系统 适合有一定docker使用经验的人阅读 适合有一定linux命令使用经验的人阅读 1.docker部分 1.1.docker简介 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化.容器是完全使用沙箱机制,相互之间不会有任何接口 1.2.docker架构 简单的说,docker就是一个轻量级的linux系统.Docker 容器通过 Docker 镜像来创建.

  • node.js连接mysql与基本用法示例

    本文实例讲述了node.js连接mysql与基本用法.分享给大家供大家参考,具体如下: 下载mysql模块 使用命令npm install mysql下载mysql模块 mysql引入模块 var mysql = require("mysql"); 创建连接池 使用createPool()创建一个mysql连接池,传入一个表参数作为连接信息 var pool = mysql.createPool({ host:"127.0.0.1", port:3306, //默认

  • 详解基于node.js的脚手架工具开发经历

    前言 我们团队的前端项目是基于一套内部的后台框架进行开发的,这套框架是基于vue和ElementUI进行了一些定制化包装,并加入了一些自己团队设计的模块,可以进一步简化后台页面的开发工作. 这套框架拆分为基础组件模块,用户权限模块,数据图表模块三个模块,后台业务层的开发至少要基于基础组件模块,可以根据具体需要加入用户权限模块或者数据图表模块.尽管vue提供了一些脚手架工具vue-cli,但由于我们的项目是基于多页面的配置进行开发和打包,与vue-cli生成的项目结构和配置有些不一样,所以创建项目

  • mocha的时序规则讲解

    前言 对于新手而言,mocha的时序就像谜一般,许多奇怪的测试样例的失败都是由于对时序不清楚.下面我就把我在测试工作中总结的时序规则部分与大家共享. describe里地时序 simple case describe('work',function(){ it('1',func(){}); it('2',func(){}); .... }); //按1,2,3...顺序执行 规则1:describe里地it的非异步部分按它们定义的顺序执行,它们所触发的回调的注册顺序也遵从it的注册顺序 hook

  • Node.js如何对SQLite的async/await封装详解

    前言 本文主要给大家介绍的是关于Node.js对SQLite的async/await封装的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧 用于将每个SQLite函数同步化,并可以用await的接口. 注意:需要SQLite for Node模块和Node.js 8.0+,并支持async / await. SQLite最常用作本地或移动应用程序的存储单元,当需要从程序的各个部分访问数据时,回调不是最佳解决方案. 为了在程序程序中更自然地访问数据,我编写了一个将回调转换为

  • node.js微信小程序配置消息推送的实现

    在开发微信小程序时,有一个消息推送,它的解释是这样的. 消息推送具体的内容是下面的这个网址   https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/message-push.html,他介绍的也还可以,就是我这里换成了node代码. 消息推送 启用并设置消息推送配置后,用户发给小程序的消息以及开发者需要的事件推送,都将被微信转发至该服务器地址中. 在微信小程序的首页开发里面,开发设置中,微信的官网中,

  • Node.js EventEmmitter事件监听器用法实例分析

    本文实例讲述了Node.js EventEmmitter事件监听器用法.分享给大家供大家参考,具体如下: Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列. events 模块只提供了一个对象: events.EventEmitter.EventEmitter 的核心就是事件触发与事件监听器功能的封装. 该模块已被node.js默认引,不需要使用require()显示引入. EventEmitter 对象如果在实例化时发生错误,会触发 'error' 事件.当添加新的监

  • 推荐一个基于Node.js的表单验证库

    API 在执行过程中的一个基本任务是数据验证. 在本文中,我想向你展示如何为你的数据添加防弹验证,同时返回风格良好的格式. 在 Node.js 中进行自定义数据验证既不容易也不快. 为了覆盖所有类型的数据,需要写许多函数. 虽然我已经尝试了一些 Node.js 的表单库 -- Express 和 Koa --他们从未满足我的项目需求. 这些扩展库要么不兼容复杂的数据结构,要么在异步验证出现问题. 使用 Datalize 在 Node.js 中进行表单验证 这就是为什么我最终决定编写自己的小巧而强

  • Docker使用编写dockerfile启动node.js应用

    编写 Dockerfile 以 express 自动创建的目录为例,目录结构如下: ├── /bin │ └── www ├── /node_modules ├── /public ├── /routes ├── /views ├── package-lock.json ├── package.json ├── ecosystem.config.js ├── app.js └── Dockerfile 在项目目录下新建 Dockerfile 文件 FROM node:10.15 MAINTAIN

  • 详解在Node.js中发起HTTP请求的5种方法

    创建HTTP请求使现代编程语言的核心功能之一,也是很多程序员在接触到新的开发环境时最先遇到的技术之一.在Node.js中有相当多的解决方案,其中有语言内置功能,也有开源社区贡献的开发库.下面咱们来看一下比较流行的几种方式. 在开始之前,请先在自己的计算机上安装最新版的node.js和npm. HTTP - 标准库 首先是标准库中默认的 HTTP 模块.这个模块无需安装依赖外部即可使用,做到了真正的即插即用.缺点是与其他解决方案相比,用起来不是那么友好. 下面的代码将向NASA的API发送一个 G

随机推荐