我用Node.js的Koa框架搭建了一個靜態站點

緣起

我用Node.js的Koa2框架搭建了一個靜態站點,固然這個站點只是部署在我本身的電腦上,主要用來作一些測試:好比寫個小頁面,嘗試下新技術。之前我在本身的電腦上搭建過不少相似的靜態站點,由於用過一年的php,因此我以前都是用php+nginx來搭建站點,最近兩年我一直在作前端,php也懶得碰了。
前段時間在看一個公開課時(忘了哪一個機構和哪位老師了,真抱歉),這個公開課的主要內容是教你如何實現一個簡單的koa框架,我當時聽了下,而後照葫蘆畫瓢本身寫了個簡陋的"koa框架"。後來我想用這個簡陋的框架搭建一個靜態站點,折騰了下,基本可行,但我好奇koa的源碼,因而去瞅了下,而後以爲本身寫的實在不咋地。因而仍是打算用koa來搭建個靜態站點。javascript

Koa用起來很方便

其實Node.js已經爲web開發提供了不少好的api接口,koa只是對其中的一些api進行了下簡單的封裝,使咱們開發起來更方便。按個人理解,koa的核心(或者說比較好的地方)就是其提供的中間件機制:php

const app = new Koa()
app.use(async ctx => {
  // 中間件內容1
})
app.use(async ctx => {
  // 中間件內容2
})
app.listen(port, () => {
  console.log('啓動')
})

這種方式用起來也比較方便。
它的核心實如今於(從公開課中學到的),將全部傳入的中間件合併爲一個,而後遞歸調用,這裏不展開說了。css

靜態站點需求其實很簡單

那麼用koa搭建一個靜態站點也就比較容易了,靜態站點也就是主要展現靜態文字內容(html)、圖片,另外加上一些簡單的樣式美化(css)和交互(js),也就是我只須要一個web服務器可以提供html、圖片(jpg、jpg等)、js、css的發佈功能便可。
下面經過一個頁面的請求來簡單分析下站點的實現:
1,用戶經過瀏覽器訪問站點中的a.html
2,web服務器接收到請求後解析請求的文件名、文件類型
3,根據上面拿到的文件名去服務器上找相應的文件
4,找到了,則設置響應頭:狀態碼(200)、響應內容類型和響應體;沒有找到,則設置錯誤的狀態碼(如404)和對應的響應體。
固然,還有不少細節須要考慮:好比服務器的頁面存放目錄、以及目錄是否可讀等等狀況。html

代碼目錄

src
|- app-koa.js // 核心文件,處理請求及返回響應
|- file-util.js // 讀取文件的方法集合
|- mime.js // mime類型映射
|- status-code.js // 狀態碼映射
|- views/ // 存放html文件
|- static/ // 存放js、css、圖片等靜態資源前端

代碼

下面我先把我寫的代碼貼在下面:
主文件app-koa.jsjava

const url = require('url')
const path = require('path')
const Koa = require('koa')
const CONTENT_TYPE = require('./mime')
const STATUS_CODE = require('./status-code')
const {
  accessFilePromise,
  statFilePromise,
  readFilePromise
} = require('./file-util')
const ROOT = __dirname

const app = new Koa()

app.use(async ctx => {
  const reqUrl = ctx.request.url
  let pathname = url.parse(reqUrl).pathname
  let filePath = ''
  let fileformat = ''

  // 對web根路徑的訪問作單獨處理
  if (pathname === '/') {
    pathname = '/index.html'
  }

  const ext = pathname.indexOf('.') !== -1 ? pathname.match(/(\.[^.]+)$/)[0] : '.html'

  if (ext === '.html') {
    filePath = path.join(ROOT, `/views${pathname}`)
    fileformat = 'utf-8'
  } else {
    filePath = path.join(ROOT, pathname)
    fileformat = 'binary'
  }

  try {
    const isAccessed = await accessFilePromise(filePath)
    let fileData = null
    if (!isAccessed) {
      const code = STATUS_CODE.ENOENT
      // 文件或目錄不可訪問,直接返回404
      ctx.res.writeHead(code, {
        'Content-Type': CONTENT_TYPE['.html']
      })
      fileData = await readFilePromise(path.join(ROOT, `/views/error/${code}.html`), 'utf-8')
      ctx.body = fileData
    } else {
      const isFile = await statFilePromise(filePath)
      if (isFile === true) {
        fileData = await readFilePromise(filePath, fileformat)
      } else {
        // 嘗試讀取該目錄下的index.html
        fileData = await readFilePromise(`${filePath}/index.html`, 'utf-8')
      }
      
      ctx.res.setHeader('Content-Type', CONTENT_TYPE[ext])
      if (ext !== '.html') {
        ctx.res.setHeader('Content-Length', Buffer.byteLength(fileData))
      }
  
      ctx.res.writeHead(STATUS_CODE.SUCCESS)
      ctx.body = fileData
      if (ext !== '.html') {
        ctx.res.write(ctx.body, 'binary')
      }
    }
  } catch (err) {
    ctx.res.writeHead(STATUS_CODE[err.code], {
      'Content-Type': CONTENT_TYPE[ext]
    })
  }
})

app.listen(3000, () => {
  console.log('Your application is running at http://localhost:3000')
})

file-util.js -- 文件處理工具node

const fs = require('fs')

const readImagePromise = (filePath) => {
  const stream = fs.createReadStream(filePath)
  const streamData = [] // 存儲文件流
  let data = ''
  return new Promise((resolve, reject) => {
    stream.on('data', (chunk) => {
      streamData.push(chunk)
    })
    stream.on('end', () => {
      data = Buffer.concat(streamData)
      resolve(data)
    })
    stream.on('error', (err) => {
      console.assert(isAssert, 'ReadStream onerror事件:', err, err && err.message)
      reject(err)
    })
  }).catch(err => {
    console.assert(isAssert, 'ReadStream promise鏈catch 讀取文件錯誤:', err, err && err.message)
    throw err
  })
}

const accessFilePromise = (filePath) => {
  return new Promise((resolve, reject) => {
    fs.access(filePath, fs.R_OK, (err) => {
      if (err) {
        console.assert(false, 'if中access文件不可訪問:', err, err && err.message)
        resolve(false)
        // reject(err)
      }
      resolve(true)
    })
  }).catch(err => {
    console.assert(true, 'promise鏈catch access文件錯誤:', err, err && err.message)
    throw err
  })
}

const statFilePromise = (filePath) => {
  return new Promise((resolve, reject) => {
    fs.stat(filePath, (err, stats) => {
      if (err) {
        console.assert(true, 'if中stat文件錯誤:', err, err && err.message)
        reject(err)
      }
      resolve(stats.isFile())
    })
  }).catch(err => {
    console.assert(true, 'promise鏈catch stat文件錯誤:', err, err && err.message)
    throw err
  })
}

const readFilePromise = (filePath, format) => {
  if (format === 'binary') {
    return readImagePromise(filePath)
  }

  return new Promise((resolve, reject) => {
    fs.readFile(filePath, format, (err, data) => {
      if (err) {
        console.assert(true, 'if中讀取文件錯誤:', err, err && err.message)
        reject(err)
      }
      resolve(data)
    })
  }).catch(err => {
    console.assert(true, 'promise鏈catch 讀取文件錯誤:', err, err && err.message)
    throw err
  })
}

module.exports = {
  readImagePromise,
  accessFilePromise,
  statFilePromise,
  readFilePromise
}

mime.js -- mime類型映射nginx

module.exports = {
  '.css': 'text/css;charset=utf8',
  '.js': 'application/javascript;charset=utf8',
  '.html': 'text/html;charset=utf8',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.ico': 'image/x-icon'
}

status-code.js -- 狀態碼映射web

module.exports = {
  SUCCESS: 200,
  EACCES: 403,
  ENOENT: 404,
  EISDIR: 403
}

後話

我以爲我寫的仍是比較囉嗦,沒辦法,目前的水平只能寫到這個程度,不過拿它來跑一個簡陋的靜態站點仍是能夠的,這不我都跑了將近半個月了。一開始我傻乎乎的用命令行跑,後來想到應該弄個守護進程,而後百度了下發現有個node進程管理工具:pm2,得了,這下省事了。好吧,先寫到這兒。
文章最初發表在我搭建的博客:我用Node.js的Koa框架搭建了一個靜態站點,略有改動。api

相關文章
相關標籤/搜索