我用Node.js的Koa框架搭建了一个静态站点

缘起

我用Node.js的Koa2框架搭建了一个静态站点,当然这个站点只是部署在我自己的电脑上,主要用来做一些测试:比如写个小页面,尝试下新技术。以前我在自己的电脑上搭建过很多类似的静态站点,因为用过一年的php,所以我之前都是用php+nginx来搭建站点,最近两年我一直在做前端,php也懒得碰了。
前段时间在看一个公开课时(忘了哪个机构和哪位老师了,真抱歉),这个公开课的主要内容是教你如何实现一个简单的koa框架,我当时听了下,然后照葫芦画瓢自己写了个简陋的"koa框架"。后来我想用这个简陋的框架搭建一个静态站点,折腾了下,基本可行,但我好奇koa的源码,于是去瞅了下,然后觉得自己写的实在不咋地。于是还是打算用koa来搭建个静态站点。

Koa用起来很方便

其实Node.js已经为web开发提供了很多好的api接口,koa只是对其中的一些api进行了下简单的封装,使我们开发起来更方便。按我的理解,koa的核心(或者说比较好的地方)就是其提供的中间件机制:

const app = new Koa()
app.use(async ctx => {
  // 中间件内容1
})
app.use(async ctx => {
  // 中间件内容2
})
app.listen(port, () => {
  console.log('启动')
})

这种方式用起来也比较方便。
它的核心实现在于(从公开课中学到的),将所有传入的中间件合并为一个,然后递归调用,这里不展开说了。

静态站点需求其实很简单

那么用koa搭建一个静态站点也就比较容易了,静态站点也就是主要展示静态文字内容(html)、图片,另外加上一些简单的样式美化(css)和交互(js),也就是我只需要一个web服务器能够提供html、图片(jpg、jpg等)、js、css的发布功能即可。
下面通过一个页面的请求来简单分析下站点的实现:
1,用户通过浏览器访问站点中的a.html
2,web服务器接收到请求后解析请求的文件名、文件类型
3,根据上面拿到的文件名去服务器上找相应的文件
4,找到了,则设置响应头:状态码(200)、响应内容类型和响应体;没有找到,则设置错误的状态码(如404)和对应的响应体。
当然,还有很多细节需要考虑:比如服务器的页面存放目录、以及目录是否可读等等情况。

代码目录

src
|- app-koa.js // 核心文件,处理请求及返回响应
|- file-util.js // 读取文件的方法集合
|- mime.js // mime类型映射
|- status-code.js // 状态码映射
|- views/ // 存放html文件
|- static/ // 存放js、css、图片等静态资源

代码

下面我先把我写的代码贴在下面:
主文件app-koa.js

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 -- 文件处理工具

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类型映射

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 -- 状态码映射

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

后话

我觉得我写的还是比较啰嗦,没办法,目前的水平只能写到这个程度,不过拿它来跑一个简陋的静态站点还是可以的,这不我都跑了将近半个月了。一开始我傻乎乎的用命令行跑,后来想到应该弄个守护进程,然后百度了下发现有个node进程管理工具:pm2,得了,这下省事了。好吧,先写到这儿。
文章最初发表在我搭建的博客:我用Node.js的Koa框架搭建了一个静态站点,略有改动。

相关推荐