使用node原生模塊,實現靜態文件服務器,反向代理和文件修改瀏覽器自動刷新功能

前端遇經常會遇到跨域問題,解決的方式一般有以下幾種。
1 使用nginx 代理對請求進行轉發
2 使用node-proxy 代理服務對請求進行轉發
3 使用jsonp解決跨域(比較古老的方式,新現在很少項目會使用這個,還使用這種方式進行跨域的項目,大部分的歷史遺留問題)
4 后端配置 CORS , 允許請求跨域(目前最常用的解決跨域方式)

使用vue-cli 腳手架搭建的項目,自帶了靜態文件服務器(webpack-server),反向代理(node-proxy)和文件修改瀏覽器自動刷新(webpack)功能。但對于歷史遺留項目,jQuery版本項目,就沒有這些功能了,習慣了瀏覽器自動刷新功能,對于jQuery項目每次修改都需要手動點擊刷新瀏覽器,實在是不能忍,主要少因為懶的使用鼠標。

所以打算自己使用node原生模塊,實現靜態文件服務器,反向代理和文件修改瀏覽器自動刷新功能。零依賴,文件復制到那,服務就啟動到哪。

使用方式
1 新建server.js 文件
2 使用編輯器打開該文件,將以下代碼復制到該文件
3 將文件復制到項目路徑下
4 使用node 命令運行server.js
5 查看命令行輸出,查看是否啟動成功
6 啟動成功后,使用瀏覽器打開該項目
7 修改項目中文件,瀏覽器自動刷新

注意:環境 node V9.1.0 如果使用高版本 node 環境,會不能使用,原因不明,還未解決。

原理
1 使用node實現靜態文件服務器,仿nginx 靜態服務器
2 使用node 實現文件修改,自動刷新瀏覽器功能 (HTTP 輪詢版)
3 使用node 實現文件修改,自動刷新瀏覽器功能 (websocket 版本)

/**
 * # 使用node原生模塊,實現靜態文件服務器,反向代理和文件修改瀏覽器自動刷新功能
 */
let http = require("http")
let url = require("url")
let fs = require("fs")
let net = require('net')
let crypto = require('crypto')

let isModify = false // 文件是否修改
let websocketInstants = []  // 存放每一個客戶端socket的數組
let port = 8100 // 默認端口為8100 如果該端口被占,則端口 +1 進行啟動,直到啟動成功
// 默認端口,并檢測端口是否被占用
portIsOccupied(port).then(() => {
  createServerFn()
})
// 檢測文件是否修改
fs.watch(__dirname, { recursive: true }, function (event, filename) {
  isModify = true
})
// 處理http請求 反向代理
function dealProxyReq(req) {
  let urlObject = url.parse(req.url)
  let path = '/orderapi' + urlObject.path
  let options = {
    protocol: 'http:',
    hostname: '127.0.0.1',
    port: 80,
    path: path,
    method: req.method,
    headers: req.headers
  }
  return options
}
// 使用 http.createServer 啟動服務
function createServerFn() {
  let app = http.createServer(function (req, res) {
    let reqPath = url.parse(req.url).path.split('?')[0]
    let filepath = __dirname + reqPath
    fs.exists(filepath, function (exists) {
      if (exists) {
        fs.stat(filepath, function (err, stats) {
          if (err) {
            res.end('<h1>server error</h1><p>' + err + '</p>')
          } else {
            if (stats.isFile()) {
              // console.log('Date:' + dateFormat() + ' ' + filepath.replace(/\\/g, '/'))
              if (filepath.includes('html')) {
                dealHtmlFile(filepath, res)
                return
              } else {
                let file = fs.createReadStream(filepath)
                file.pipe(res)
              }
            } else {
              fs.readdir(filepath, function (err, files) {
                if (files.includes('index.html')) {
                  filepath = filepath + 'index.html'
                  // console.log('Date:' + dateFormat() + ' ' + filepath.replace(/\\/g, '/'))
                  dealHtmlFile(filepath, res)
                } else {
                  res.end('<h1>404 no such file or directory</h1><p>no such file or directory</p><p>' + filepath + '/index.html</p>')
                }
              })
            }
          }
        })
      } else {
        // 使用代理處理 http 請求
        middleProxy(req, res)
      }
    })
  })
  createSocket(app)
  app.listen(port)
  console.log('http.createServer is start, port ' + port)
}
// 檢測端口是否被占用
function portIsOccupied() {
  const server = net.createServer().listen(port)
  return new Promise((resolve, reject) => {
    server.on('listening', () => {
      server.close()
      resolve()
    })
    server.on('error', (err) => {
      if (err.code === 'EADDRINUSE') {
        port++
        resolve(portIsOccupied())
      } else {
        reject(err)
      }
    })
  })
}
// html 文件特殊處理
function dealHtmlFile(filepath, res) {
  let data = fs.readFileSync(filepath, 'utf-8')
  let scriptStr = `
    <script>
      let ws = new WebSocket("ws:"+location.host);
      let timerId;
      ws.onopen = function(evt) {
        console.log('websocket Connection open ...')
        timerId = setInterval(function(){
          ws.send('check file modify...')
        },1000)
      };
      ws.onmessage = function(evt) {
        let data = JSON.parse(evt.data);
        if (data.refresh && data.refresh == true) {
          location.reload()
        }
      }
      ws.onclose = function(evt) {
        console.log('websocket Connection closed.')
        clearInterval(timerId)
      }
    </script>
  `
  data += scriptStr
  res.end(data)
}
// 添加wesocket
// https://github.com/yangmei123/node-websocket/blob/master/nodejs-socket.js
function createSocket(server) {
  server.on('upgrade', (req, socket, head) => { // 在收到upgrade請求后,告知客戶端允許切換協議
    const val = crypto.createHash('sha1')
      .update(req.headers['sec-websocket-key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary')
      .digest('base64');
    const frame = {
      buffer: new Buffer(0),
    }
    socket.setNoDelay(true)
    socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
      'Upgrade: WebSocket\r\n' +
      'Connection: Upgrade\r\n' +
      'Sec-WebSocket-Accept:' + val + '\r\n' +
      '\r\n');
    socket.on('data', sendRecive.bind(null, socket, frame));
   socket.on('error', (error)=> {
      console.log(error)
    });
    websocketInstants.push(socket)
    // 解碼
    function decodeDataFrame(e) {
      let i = 0, j, arrs = [];
      let frame = {
        FIN: e[i] >> 7, // 右移7位等于1 e[0]轉為二進制再右移
        Opcode: e[i++] & 15, //Opcode占第一個字節二進制后4位,和1111做與比較
        Mask: e[i] >> 7, // e[1]二進制第一位
        PayloadLength: e[i++] & 0x7F // e[1]二進制的后7位和(1111111) 做與運算
      };
      if (frame.PayloadLength === 126) {// 處理特殊長度126和127
        frame.PayloadLength = (e[i++] << 8) + e[i++]
      }
      if (frame.PayloadLength === 127) {
        i += 4; // 長度一般用4個字節的整型,前四個字節一般為長整型留空的。
        frame.PayloadLength = (e[i++] << 24) + (e[i++] << 16) + (e[i++] << 8) + e[i++]
      }
      if (frame.Mask) {
        frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]]
        for (j = 0, arrs = []; j < frame.PayloadLength; j++) {
          arrs.push(e[i + j] ^ frame.MaskingKey[j % 4])
        }
      } else {
        arrs = e.slice(i, i + frame.PayloadLength)
      }
      arrs = new Buffer(arrs)
      if (frame.Opcode === 1) { // 是文本格式的
        arrs = arrs.toString()
      }
      frame.PayloadData = arrs
      return frame // 返回數據幀
    }
    function sendRecive(socket, frame, data) {
      // const readableData = decodeDataFrame(data).PayloadData; // 接受瀏覽器的數據并返回文件是否修改
      websocketInstants.forEach(item => {
        item.write(encodeDataFrame({
          FIN: 1,
          Opcode: 1,
          MASK: 0,
          PayloadData: '{"refresh": ' + isModify + '}'
        }))
      })
      isModify = false
    }
  });
}
// 編碼算法
function encodeDataFrame(e) {
  let bufferArr = [];
  let PayloadData = Buffer.from(e.PayloadData) // 放到buffer
  const PayloadLength = PayloadData.length
  const fin = e.FIN << 7 // 轉為2進制
  bufferArr.push(fin + e.Opcode) // 第一個字節拼好
  if (PayloadLength < 126) bufferArr.push(PayloadLength)
  else if (PayloadLength < 0x10000) bufferArr.push(126, (PayloadLength & 0xFF00) >> 8, PayloadLength & 0xFF)
  else bufferArr.push(
    127, 0, 0, 0, 0, //8字節數據,前4字節一般沒用留空
    (PayloadLength & 0xFF000000) >> 24,
    (PayloadLength & 0xFF0000) >> 16,
    (PayloadLength & 0xFF00) >> 8,
    PayloadLength & 0xFF
  )
  return Buffer.concat([new Buffer(bufferArr), PayloadData])
}
// 日期格式化
function dateFormat(optionStr) {
  let date = new Date();
  let year = date.getFullYear()
  let month = date.getMonth()
  let day = date.getDate()
  let hours = date.getHours()
  let minutes = date.getMinutes()
  let seconds = date.getSeconds()
  return hours + ':' + minutes + ':' + seconds
}
// 使用代理
function middleProxy(req, res) {
  let protocol = req.headers.origin ? req.headers.origin.split(':')[0] : 'http'
  if (protocol == 'http') {
    middleHttpProxy(req, res)
  }
  if (protocol == 'https') {
    middleHttpsProxy(req, res)
  }
}
// http 代理
function middleHttpProxy(req, res) {
  let options = dealProxyReq(req)
  // delete options.headers['accept-encoding']; // 為了方便起見,直接去掉客戶端請求所支持的壓縮方式
  console.log(`${dateFormat('H:M:S')} ${options.method} ${options.protocol}//${options.hostname}:${options.port}${options.path}`);
  let realReq = http.request(options, (realRes) => {// 根據客戶端請求,向真正的目標服務器發起請求。
    Object.keys(realRes.headers).forEach(function (key) {// 設置客戶端響應的http頭部
      res.setHeader(key, realRes.headers[key]);
    });
    res.writeHead(realRes.statusCode);// 設置客戶端響應狀態碼
    realRes.pipe(res);
  });
  req.pipe(realReq);// 通過pipe的方式把客戶端請求內容轉發給目標服務器
  realReq.on('error', (e) => {
    console.error(e)
  })
}
// https 代理
function middleHttpsProxy(req, res) {

}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容