myanywhere

用原生node做一个简易阉割版的anywhere静态资源服务器,以提升对node与http的理解。

相关知识

  • es6及es7语法

  • http的相关网络知识

    • 响应头
    • 缓存相关
    • 压缩相关
  • path模块

    • path.join拼接路径
    • path.relative
    • path.basename
    • path.extname
  • http模块

  • fs模块

    • fs.stat函数

      使用 fs.stat函数取得stats来获取文件或文件夹的参数

      • stats.isFile 判断是否为文件夹
    • fs.createReadStream(filePath).pipe(res)

      文件可读流的形式,使读取效率更高

    • fs.readdir

    • ...

  • promisify

    • async await

1.实现读取文件或文件夹

const http= require('http')
const conf = require('./config/defaultConfig')
const path = require('path')
const fs = require('fs') const server = http.createServer((req, res) => {
const filePath = path.join(conf.root, req.url)
// http://nodejs.cn/api/fs.html#fs_class_fs_stats
fs.stat(filePath, (err, stats) => {
if (err) {
res.statusCode = 404
res.setHeader('Content-text', 'text/plain')
res.end(`${filePath} is not a directoru or file`)
}
// 如果是一个文件
if (stats.isFile()) {
res.statusCode = 200
res.setHeader('Content-text', 'text/plain')
fs.createReadStream(filePath).pipe(res)
} else if (stats.isDirectory()) {
fs.readdir(filePath, (err, files) => {
res.statusCode = 200
res.setHeader('Content-text', 'text/plain')
res.end(files.join(','))
})
}
})
}) server.listen(conf.port, conf.hostname, () => {
const addr = `http:${conf.hostname}:${conf.port}`
console.info(`run at ${addr}`)
})

2. async await异步修改

为了避免多层回调出现,我们使用jsasync 和 await来 改造我们的代码

router.js

把逻辑相关的代码从app.js中抽离出来放入router.js中,分模块开发

const fs = require('fs')
const promisify = require('util').promisify
const stat = promisify(fs.stat)
const readdir = promisify(fs.readdir) module.exports = async function (req, res, filePath) {
try {
const stats = await stat(filePath)
if (stats.isFile()) {
res.statusCode = 200
res.setHeader('Content-text', 'text/plain')
fs.createReadStream(filePath).pipe(res)
} else if (stats.isDirectory()) {
const files = await readdir(filePath)
res.statusCode = 200
res.setHeader('Content-text', 'text/plain')
res.end(files.join(','))
}
} catch (error) {
res.statusCode = 404
res.setHeader('Content-text', 'text/plain')
res.end(`${filePath} is not a directoru or file`)
}
}

app.js

const http= require('http')
const conf = require('./config/defaultConfig')
const path = require('path')
const route = require('./help/router') const server = http.createServer((req, res) => {
const filePath = path.join(conf.root, req.url)
route(req, res, filePath)
}) server.listen(conf.port, conf.hostname, () => {
const addr = `http:${conf.hostname}:${conf.port}`
console.info(`run at ${addr}`)
})

3. 完善可点击

上面的工作 已经可以让我们在页面中看到文件夹的目录,但是是文字,不可点击

使用handlebars渲染

  • 引用handlebars

    const Handlebars = require('handlebars')
  • 创建模板html

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{title}}</title>
    <style>
    body {
    margin: 10px
    }
    a {
    display: block;
    margin-bottom: 10px;
    font-weight: 600;
    }
    </style>
    </head>
    <body>
    {{#each files}}
    <a href="{{../dir}}/{{file}}">{{file}}</a>
    {{/each}}
    </body>
    </html>

  • router.js配置

    引用时使用绝对路径

    const tplPath = path.join(__dirname, '../template/dir.html')
    const source = fs.readFileSync(tplPath, 'utf8')
    const template = Handlebars.compile(source)
  • 创建数据 data

    ....
    module.exports = async function (req, res, filePath) {
    try {
    ...
    } else if (stats.isDirectory()) {
    const files = await readdir(filePath)
    res.statusCode = 200
    res.setHeader('Content-text', 'text/html')
    const dir = path.relative(config.root, filePath)
    const data = {
    // path.basename() 方法返回一个 path 的最后一部分
    title: path.basename(filePath),
    // path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
    // 返回: '../../impl/bbb'
    dir: dir ? `/${dir}` : '',
    files
    }
    console.info(files)
    res.end(template(data))
    }
    } catch (error) {
    ...
    }
    }

4. mime

新建mime.js文件

const path = require('path')

const mimeTypes = {
....
}
module.exports = (filePath) => {
let ext = path.extname(filePath).toLowerCase() if (!ext) {
ext = filePath
} return mimeTypes[ext] || mimeTypes['.txt']
}

mine.js 根据文件后缀名来返回对应的mime

5. 压缩页面优化性能

对读取的stream压缩

在 defaultConfig.js中 添加 compress项

module.exports = {
// process.cwd() 路径能随着执行路径的改变而改变
// process cwd() 方法返回 Node.js 进程当前工作的目录。
root: process.cwd(),
hostname: '127.0.0.1',
port: 9527,
compress: /\.(html|js|css|md)/
}

编写压缩处理 compress

const {createGzip, createDeflate} = require('zlib')
module.exports = (rs, req, res) => {
const acceptEncoding = req.headers['accept-encoding']
if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) {
return
} else if (acceptEncoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip')
return rs.pipe(createGzip())
} else if (acceptEncoding.match(/\bdeflate\b/)) {
res.setHeader('Content-Encoding', 'deflate')
return rs.pipe(createGzip())
}
} /*
match() 方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。
该方法类似 indexOf() 和 lastIndexOf() ,但是它返回指定的值,而不是字符串的位置。
*/

router.js中读取文件的更改

...
let rs = fs.createReadStream(filePath)
if (filePath.match(config.compress)) {
rs = compress(rs, req, res)
}
rs.pipe(res)

文件结果compress压缩后,压缩率可达 70%

6.处理缓存

缓存大致原理

用户请求 本地缓存 --no--> 请求资源 --> 协商缓存 返回响应

用户请求 本地缓存 --yes--> 判断换存是否有效 --有效--> 本地缓存

--无效--> 协商缓存 返回响应

缓存header

  • expires 老旧 现在不用
  • Cache-Control 相对与上次请求的时间
  • If-Modified-Since / Last-Modified
  • If-None-Match / ETag

cache.js

const {cache} = require('../config/defaultConfig')
function refreshRes(stats, res) {
const { maxAge, expires, cacheControl, lastModified, etag } = cache
if (expires) {
res.setHeader('Expores', (new Date(Date.now() + maxAge * 1000)).toUTCString())
}
if (cacheControl) {
res.setHeader('Cache-Control', `public, max-age=${maxAge}`)
}
if (lastModified) {
res.setHeader('Last-Modified', stats.mtime.toUTCString())
}
if (etag) {
res.setHeader('ETag', `${stats.size}-${stats.mtime.toUTCString()}`)
}
} module.exports = function isFresh(stats, req, res) {
refreshRes(stats, res) const lastModified = req.headers['if-modified-since']
const etag = req.headers['if-none-match'] if (!lastModified && !etag) {
return false
}
if (lastModified && lastModified !== res.getHeader('Last-Modified')) {
return false
}
if (etag && res.getHeader('ETag').indexOf(etag) ) {
return false
}
return true
}

router.js

// 如果文件是是新鲜的 不用更改,就设置响应头 直接返回
if (isFresh(stats, req, res)) {
res.statusCode = 304
res.end()
return
}

7.自动打开浏览器

编写openUrl.js

const {exec} = require('child_process')

module.exports = url => {
switch (process.platform) {
case 'darwin':
exec(`open ${url}`)
break case 'win32':
exec(`start ${url}`)
}
}

只支持Windows和 mac系统

在app.js中使用

server.listen(conf.port, conf.hostname, () => {
const addr = `http:${conf.hostname}:${conf.port}`
console.info(`run at ${addr}`)
openUrl(addr)
})

总结

domo不难,但是涉及到的零碎知识点比较多,对底层的node有个更进一步了解,也感受到了node在处理网路请求这一块的强大之处,另外es6和es7的新语法很是强大,以后要多做功课。

原生node写一个静态资源服务器的更多相关文章

  1. 使用Node.js搭建静态资源服务器

    对于Node.js新手,搭建一个静态资源服务器是个不错的锻炼,从最简单的返回文件或错误开始,渐进增强,还可以逐步加深对http的理解.那就开始吧,让我们的双手沾满网络请求! Note: 当然在项目中如 ...

  2. 使用原生node写一个聊天室

    在学习node的时候都会练习做一个聊天室的项目,主要使用socket.io模块和http模块.这里我们使用更加原始的方式去写一个在命令行聊天的聊天室. http模块,socket.io都是高度封装之后 ...

  3. Node项目实战-静态资源服务器

    打开github,在github上创建新项目: Repository name: anydoor Descripotion: Tiny NodeJS Static Web server 选择:publ ...

  4. node静态资源服务器的搭建----访问本地文件夹(搭建可访问静态文件的服务器)

    我们的目标是实现一个可访问静态文件的服务器,即可以在浏览器访问文件夹和文件,通过点击来查看文件. 1.先创建一个文件夹anydoor,然后在该文件夹里npm init一个package.json文件, ...

  5. 使用node搭建静态资源服务器

    安装 npm install yumu-static-server -g 使用 shift+鼠标右键  在此处打开Powershell 窗口 server # 会在当前目录下启动一个静态资源服务器,默 ...

  6. 极简 Node.js 入门 - 5.3 静态资源服务器

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  7. 【学习Koa】原生koa2 静态资源服务器例子

    实现思路 首先读取当前路径下所有的文件和文件夹 当去点击某个列表项时判断其实文件还是文件夹,文件的话直接读取,文件夹则再次利用上一个步骤读取并展示 文件结构 代码 index.js 入口文件 cons ...

  8. 初始nginx(启动运行) 使用nginx做一个简单的静态资源服务器

    第一次接触nginx的时候,那时候公司还是用的一些不知名的小技术,后来公司发展问题,重新招了人,然后接触到nginx,公司 使用nginx用来做代理服务器,所有请求 都先经过nginx服务器,然后交由 ...

  9. 使用 Express 实现一个简单的 SPA 静态资源服务器

    背景 限制 SPA 应用已经成为主流,在项目开发阶段产品经理和后端开发同学经常要查看前端页面,下面就是我们团队常用的使用 express 搭建的 SPA 静态资源服务器方案. 为 SPA 应用添加入口 ...

随机推荐

  1. 洛谷 P1494 BZOJ 2038 [2009国家集训队]小Z的袜子(hose)

    //洛谷题面字体.排版我向来喜欢,却还没收录这道如此有名的题,BZOJ的题面字体太那啥啦,清橙的题面有了缩进,小标题却和正文字体一致,找个好看的题面咋这么难呐………… //2019年3月23日23:0 ...

  2. 阶梯博弈&POJ 1704

    阶梯博弈: 先借用别人的一幅图片.(1阶梯之前还有一个0阶梯未画出) 阶梯博弈的最初定义是这样的:每一个阶梯只能向它的前一个阶梯移动本阶梯的点,直至最后无法移动的为输. 那么,利用NIM,只计算奇数级 ...

  3. 【转载】linux中shell命令test用法和举例

    test 命令最短的定义可能是评估一个表达式:如果条件为真,则返回一个 0 值.如果表达式不为真,则返回一个大于 0 的值 — 也可以将其称为假值.检查最后所执行命令的状态的最简便方法是使用 $? 值 ...

  4. 【跟我一起学Unity3D】做一个2D的90坦克大战之AI系统

    对于AI,我的初始想法非常easy,首先他要能动,而且是在地图里面动. 懂得撞墙后转弯,然后懂得射击,其它的没有了,基于这个想法,我首先创建了一个MyTank类,用于管理玩家的坦克的活动,然后创建AI ...

  5. SQL Server中如何设置对列的权限

    一.方式一:使用视图 将需要限制用户只能看到特定的几个列.设置成一个视图,然后对这个视图进行权限控制 二.方式二:使用GRANT语句 1.授予相关列的查询权限(SELECT) 在数据库db1中,登录名 ...

  6. hdu1716(库函数next_permutation)

    题目意思: 现有四张卡片,用这四张卡片能排列出非常多不同的4位数,要求按从小到大的顺序输出这些4位数. 注意首位没有前导0 pid=1716">http://acm.hdu.edu.c ...

  7. Coco2d-js/Cocos2d-html5中Android返回键实现

    导语: 首先Cocos2d-x其中实现Menu和Back按键相对简单一点,而在资源较少的Cocos2d-html5其中.要实现返回还是有一点不一样的,并且有没有详细的demo.也就仅仅有自己去看api ...

  8. ssh无法连接到远端Ubuntu的解决方法

    近日,饱受无法远程登录到新安装在VMWare上的Ubuntu虚拟机,现在发现问题所在.故记录此问题的解决方式,以备后用. 一.远程登录虚拟机的准备: Ubuntu虚拟机的联网方式应该选择Bridged ...

  9. luogu3799 妖梦拼木棒

    题目大意 有n根木棒,现在从中选4根,想要组成一个正三角形,问有几种选法?木棒长度都<=5000. 题解 根据容斥原理,三角形两条边分别由长度相等的单根木棒组成,另一条边由两条小于该边长的木棒构 ...

  10. luogu2054 洗牌 同余方程

    题目大意 对于扑克牌的一次洗牌是这样定义的,将一叠N(N为偶数)张扑克牌平均分成上下两叠,取下面一叠的第一张作为新的一叠的第一张,然后取上面一叠的第一张作为新的一叠的第二张,再取下面一叠的第二张作为新 ...