• [Node]创建静态资源服务器


    项目初始化

    在这里插入图片描述

    .gitignore

    • cnpm i eslint -D
    • eslint --init得到.eslintrc.js

    .eslintrc.js

    module.exports = {
    	'env': {
    		'browser': true,
    		'commonjs': true,
    		'es6': true
    	},
    	'extends': 'eslint:recommended',
    	'globals': {
    		'Atomics': 'readonly',
    		'SharedArrayBuffer': 'readonly'
    	},
    	'parserOptions': {
    		'ecmaVersion': 2018
    	},
    	'rules': {
    		'indent': [
    			'error',
    			'tab'
    		],
    		'linebreak-style': [
    			'error',
    			'windows'
    		],
    		'quotes': [
    			'error',
    			'single'
    		],
    		'semi': [
    			'error',
    			'never'
    		]
    	}
    }
    
    • 创建eslintignore

    .eslintignore

    build/
    node_modules
    
    • 全局安装supervisor实现文件热更新

    初步实现

    • 创建src/app.js

    app.js

    const http = require('http')
    const conf = require('./config/defaultConfig')
    const chalk = require('chalk')
    const path = require('path')
    const route = require('./helper/route.js')
    
    
    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(`Server running at ${chalk.green(addr)}`)
    })
    
    • 创建src/config/defaultConfig.js

    defaultConfig.js

    module.exports = {
    	root: process.cwd(), // 当前路径
    	hostname: '127.0.0.1',
    	port: 3000
    }
    
    • 创建src/hepler/route.js

    route.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.writeHead(200, {
    				'Content-Type': 'text/plain; charset=utf-8'
    			})
    			fs.createReadStream(filePath).pipe(res)
    		} else if (stats.isDirectory()) { // 如果是文件夹, 返回文件列表
    			const files = await readdir(filePath)
    			res.writeHead(200, {
    				'Content-Type': 'text/plain; charset=utf-8'
    			})
    			res.end(files.join(','))
    		}
    	} catch (err) {
    		// 如果不存在
    		console.error(err)
    		res.write(404, {
    			'Content-Type': 'text/plain; charset=utf-8'
    		})
    		res.end(`${filePath} is not directory or file
     ${err.toString()}`)
    	}
    }
    
    • 热启动服务supervisor app.js

    效果图

    在这里插入图片描述

    结合handlebars进一步

    • 创建src/template/dir.tpl

    dir.tpl

    <!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: 30px;
        }
        a {
          display: block;
        }
      </style>
    </head>
    
    <body>
    {{#each files}}
      <a href="{{../dir}}/{{this}}">{{this}}</a>
    {{/each}}
    </body>
    
    </html>
    
    • 安装cnpm i handlebars -S

    router.js

    const fs = require('fs')
    const path = require('path')
    const promisify = require('util').promisify
    const stat = promisify(fs.stat)
    const readdir = promisify(fs.readdir)
    
    const config = require('../config/defaultConfig')
    const Handlebars = require('handlebars')
    const tplPath = path.join(__dirname, '../template/dir.tpl')
    const source = fs.readFileSync(tplPath)
    const template = Handlebars.compile(source.toString())
    
    module.exports = async function (req, res, filePath) {
    	try {
    		const stats = await stat(filePath)
    		// 如果是文件, 返回内容
    		if (stats.isFile()) {
    			res.writeHead(200, {
    				'Content-Type': 'text/plain; charset=utf-8'
    			})
    			fs.createReadStream(filePath).pipe(res)
    		} else if (stats.isDirectory()) { // 如果是文件夹, 返回文件列表
    			const files = await readdir(filePath)
    			res.writeHead(200, {
    				'Content-Type': 'text/html; charset=utf-8'
    			})
    			const dir = path.relative(config.root, filePath)
    			const data = {
    				title: path.basename(filePath),
    				dir: dir ? `/${dir}` : '',
    				files
    			}
    			res.end(template(data))
    		}
    	} catch (err) {
    		// 如果不存在
    		console.error(err)
    		res.write(404, {
    			'Content-Type': 'text/plain; charset=utf-8'
    		})
    		res.end(`${filePath} is not directory or file
     ${err.toString()}`)
    	}
    }
    

    效果图

    在这里插入图片描述

    • 优化Content-Typy
    • 新建文件src/helper/mime.js
    const path = require('path')
    
    const mimeTypes = {
    	'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',
    	'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'
    }
    
    module.exports = (filePath) => {
    	// 获取后缀名
    	let ext = path.extname(filePath)
    		.split('.')
    		.pop()
    		.toLowerCase()
    
    	if (!ext) {
    		ext = filePath
    	}
    
    	return mimeTypes[ext] || mimeTypes['txt']
    }
    
    • 应用

    router.js

    // 处理contentType
    const mime = require('./mime')
    ...
    			const data = {
    				title: path.basename(filePath),
    				dir: dir ? `/${dir}` : '',
    				files: files.map(file => {
    					return {
    						file,
    						icon: mime(file)
    					}
    				})
    			}
    

    dir.tpl

    <body>
      {{#each files}}
        <a href="{{../dir}}/{{file}}">【{{icon}}】{{file}}</a>
      {{/each}}
    </body>
    
    • 压缩文件

    config/defaultConfig.js

    module.exports = {
    	root: global.process.cwd(), // 当前路径
    	hostname: '127.0.0.1',
    	port: 3000,
    	compress: /.(html|js|css|md)/
    }
    
    • 创建文件helper/compress.js
    const {createGzip, createDeflate} = require('zlib')
    
    module.exports = (rs, req, res) => {
    	const acceptEncoding = req.headers['accept-encoding']
    	if (!acceptEncoding || !acceptEncoding.match(/(gzip|deflate)/)) {
    		return rs
    	} else if (acceptEncoding.match(/gzip/)) {
    		res.setHeader('Content-Encoding', 'gzip')
    		return rs.pipe(createGzip())
    	} else if (acceptEncoding.match(/deflate/)) {
    		res.setHeader('Content-Encoding', 'deflate')
    		return rs.pipe(createDeflate())
    	}
    }
    

    route.js

    // 压缩文件
    const compress = require('./compress')
    ...
    			// fs.createReadStream(filePath).pipe(res)
    			let rs = fs.createReadStream(filePath)
    			if (filePath.match(config.compress)) {
    				rs = compress(rs, req, res)
    			}
    			rs.pipe(res)
    
    • range范围

      • range: bytes=[start]-[end]
      • Accept-Ranges: bytes
      • Content-Range: bytes start-end/total
    • 创建文件src/helper/range.js

    range.js

    module.exports = (totalSize, req, res) => {
    	const range = req.headers['range']
    	if (!range) {
    		return {
    			code: 200
    		}
    	}
    
    	const sizes = range.match(/bytes=(d*)-(d*)/) // [匹配到的内容, 第一个分组, 第二个分组]
    	const end = sizes[2] ? parseInt(sizes[2]) : totalSize - 1
    	const start = sizes[1] ? parseInt(sizes[1]) : totalSize - end
    
    	if (start > end || start < 0 || end > totalSize) {
    		return {
    			code: 200
    		}
    	}
    
    	res.setHeader('Accept-Ranges', 'bytes')
    	res.setHeader('Content-Range', `bytes ${start}-${end}/${totalSize}`)
    	res.setHeader('Content-Length', end - start)
    	return {
    		code: 206,
    		start: start,
    		end: end
    	}
    } 
    
    • 应用

    route.js

    // range
    const range = require('./range')
    ...
    // 如果是文件, 返回内容
    		if (stats.isFile()) {
    			const contentType = mime(filePath)
    			res.writeHead(200, {
    				'Content-Type': `${contentType}; charset=utf-8`
    			})
    			
    			// fs.createReadStream(filePath).pipe(res)
    			// let rs = fs.createReadStream(filePath)
    			let rs
    			const {code, start, end} = range(stats.size, req, res)
    			if (code === 200) {
    				rs = fs.createReadStream(filePath)
    			} else {
    				rs = fs.createReadStream(filePath, {start, end})
    			}
    
    			if (filePath.match(config.compress)) {
    				rs = compress(rs, req, res)
    			}
    			rs.pipe(res)
    		}
    
    • 直接用浏览器访问指定range有些困难, 使用curl查看效果
    • 使用Linux命令行工具输入CURL -I http://127.0.0.1:3000/LICENSE
    • 指定rangcurl -r 1-10 -i http://127.0.0.1:3000/LICENSE
    • 缓存

    Created with Raphaël 2.2.0用户请求本地缓存client失效server未改变304本地缓存协商缓存 返回响应请求资源yesnoyesnoyesno
    - Expires, Cache-Control
    - If-Modified-Since / Last-Modified
    - If-None-Match / ETag
    
    • 创建文件src/helper/cache.js

    cache.js

    const {cache} = require('../config/defaultConfig');
    
    function refreshRes(stats, res) {
      const {maxAge, expires, cacheControl, lastModified, etag} = cache;
    
      if (expires) {
        res.setHeader('Expires', (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()}`); // mtime 需要转成字符串,否则在 windows 环境下会报错
      }
    }
    
    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 && etag !== res.getHeader('ETag')) {
        return false;
      }
    
      return true;
    };
    
    • 应用

    route.js

    const isFresh = require('./cache');
    ...
    if (stats.isFile()) {
          const contentType = mime(filePath);
          res.setHeader('Content-Type', contentType);
    
          if (isFresh(stats, req, res)) {
            res.statusCode = 304;
            res.end();
            return;
          }
    
          let rs;
          const {code, start, end} = range(stats.size, req, res);
          if (code === 200) {
            res.statusCode = 200;
            rs = fs.createReadStream(filePath);
          } else {
            res.statusCode = 206;
            rs = fs.createReadStream(filePath, {start, end});
          }
          if (filePath.match(config.compress)) {
            rs = compress(rs, req, res);
          }
          rs.pipe(res);
        }
    
    • CLI

    • 借助命令行工具Yargs
    • 安装cnpm i yargs -S
    • 创建文件src/index.js

    index.js

    const yargs = require('yargs')
    const Server = require('./app')
    
    const argv = yargs
    	.usage('anywhere [options]')
    	.option('p', {
    		alias: 'port',
    		describe: '端口号',
    		default: 9527
    	})
    	.option('h', {
    		alias: 'hostname',
    		describe: 'host',
    		default: '127.0.0.1'
    	})
    	.option('d', {
    		alias: 'root',
    		describe: 'root path',
    		default: global.process.cwd()
    	})
    	.version()
    	.alias('v', 'version')
    	.help()
    	.argv
    
    const server = new Server(argv)
    server.start()
    

    app.js

    const http = require('http')
    const conf = require('./config/defaultConfig')
    const chalk = require('chalk')
    const path = require('path')
    const route = require('./helper/route.js')
    
    class Server {
    	constructor(config) {
    		this.conf = Object.assign({}, conf, config)
    	}
    
    	start() {
    		const server = http.createServer((req, res) => {
    			const filePath = path.join(this.conf.root, req.url)
    			route(req, res, filePath, this.conf)
    		})
    
    		server.listen(this.conf.port, this.conf.hostname, () => {
    			const addr = `http://${this.conf.hostname}:${this.conf.port}`
    			console.info(`Server started at ${chalk.green(addr)}`)
    		})
    	}
    }
    
    module.exports = Server
    

    route.js

    // const config = require('../config/defaultConfig')
    
    • 快捷打开

    • 创建bin/server

    server

    #! /usr/bin/env node
    
    require('../src/index')
    

    package.json

      "main": "src/app.js",
      "bin": {
        "xiaozhongserver": "bin/server"
      },
    
    • 用linux执行这个文件是没有权限的
    • 加上执行权限chmod +x bin/xiaozhong-server
    • 赋予权限后显示-rwxr-xr-x 1 16416 197609 47 6月 29 17:27 bin/xiaozhong-server*
    • bin/xiaozhong-server -9898
    • 显示效果

    • 在这里插入图片描述
    • 发布npm包
    • npm login
    • npm publish

    安装

    npm install xiaozhong-server

    使用方法

    xiaozhongserver # 把当前文件夹作为静态资源服务器根目录

    xiaozhongserver -p 8080 # 设置端口号为 8080

    xiaozhongserver -h localhost # 设置 host 为 localhost

    xiaozhongserver -d /usr # 设置根目录为 /usr

    完结

  • 相关阅读:
    win10 ObservableCollection 排序自动收缩问题
    在C#中GUID生成的四种格式
    MultiBinding的StringFormat参数问题
    asp.net mvc cookie超时返回登录页面问题
    string.Format的困惑
    c# web应用调试开启外部访问
    主码索引、聚集索引、非主码索引(辅助索引)、唯一索引、外键索引、复合索引、非主码索引、聚集主码(聚集索引)、单列索引、多列索引、普通索引等
    优化MD5和IP在(MySQL)数据库中的存储
    《Effective MySQL之SQL语句最优化》读书笔记——乱七八糟系列(给自己看)
    布隆过滤器(Bloom Filter)文章收集
  • 原文地址:https://www.cnblogs.com/izhaong/p/12154271.html
Copyright © 2020-2023  润新知