├── resource ├── test.js ├── 75team.png ├── 75team2.png ├── test.css ├── index.html └── test.html ├── hello.js ├── www ├── assets │ ├── js │ │ └── app.js │ └── image │ │ ├── logo.png │ │ └── logo2.png └── index.html ├── .eslintrc.js ├── lib ├── aspect │ ├── param.js │ └── cookie.js ├── interceptor.js ├── module │ └── mock.js ├── middleware │ └── router.js └── server.js ├── view ├── coronavirus_index.html └── coronavirus_date.html ├── README.md ├── http-simple.js ├── package.json ├── http-interceptor-default.js ├── .gitignore ├── http-interceptor-cluster.js ├── http-mime2.js ├── http-interceptor-cookie.js ├── http-static.js ├── http-static-stream.js ├── tcp-server.js ├── http-cache-control.js ├── http-stream.js ├── http-mime.js ├── http-consult.js ├── http-etag.js ├── http-last-modified.js ├── http-interceptor.js ├── http-interceptor-coronavirs.js └── http-compression.js /resource/test.js: -------------------------------------------------------------------------------- 1 | console.log('test'); -------------------------------------------------------------------------------- /hello.js: -------------------------------------------------------------------------------- 1 | console.log('hello world'); 2 | -------------------------------------------------------------------------------- /www/assets/js/app.js: -------------------------------------------------------------------------------- 1 | const title = document.querySelector('h1'); 2 | title.style.color = 'red'; -------------------------------------------------------------------------------- /resource/75team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/75camp/learning-webdev-with-nodejs/HEAD/resource/75team.png -------------------------------------------------------------------------------- /resource/75team2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/75camp/learning-webdev-with-nodejs/HEAD/resource/75team2.png -------------------------------------------------------------------------------- /resource/test.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | h1 { 7 | color: red; 8 | } -------------------------------------------------------------------------------- /www/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/75camp/learning-webdev-with-nodejs/HEAD/www/assets/image/logo.png -------------------------------------------------------------------------------- /www/assets/image/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/75camp/learning-webdev-with-nodejs/HEAD/www/assets/image/logo2.png -------------------------------------------------------------------------------- /resource/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Index 6 | 7 | 8 |

Hello World!

9 |
10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | }, 4 | extends: "eslint-config-sprite", 5 | plugins: ["html"], 6 | rules: { 7 | "complexity": ["warn", 25], 8 | "import/prefer-default-export": "off", 9 | "no-console": "off", 10 | }, 11 | } -------------------------------------------------------------------------------- /lib/aspect/param.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | const querystring = require('querystring'); 3 | 4 | module.exports = async function (ctx, next) { 5 | const {req} = ctx; 6 | const {query} = url.parse(`http://${req.headers.host}${req.url}`); 7 | ctx.params = querystring.parse(query); 8 | await next(); 9 | }; 10 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Index 7 | 8 | 9 |

君喻教育2

10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /view/coronavirus_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 疫情目录 7 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /lib/aspect/cookie.js: -------------------------------------------------------------------------------- 1 | module.exports = async function (ctx, next) { 2 | const {req} = ctx; 3 | const cookieStr = decodeURIComponent(req.headers.cookie); 4 | const cookies = cookieStr.split(/\s*;\s*/); 5 | ctx.cookies = {}; 6 | cookies.forEach((cookie) => { 7 | const [key, value] = cookie.split('='); 8 | ctx.cookies[key] = value; 9 | }); 10 | await next(); 11 | }; 12 | -------------------------------------------------------------------------------- /resource/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |

Hello World!

12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP协议与服务端编程 2 | 3 | ## 课程内容简介 4 | 5 | - 简单 Web 架构与基于 TCP 的 HTTP 协议 6 | - HTTP 协议的数据格式、Header 7 | - Verbs 与状态码 8 | - MIME、内容协商 9 | - HTTP 缓存策略 10 | - Web 服务器通用处理流程与拦截器 11 | - 请求与响应:内容处理 12 | - 基于拦截器的通用 Web 框架 —— KOA 13 | - 基于 KOA 的 MVC 框架 —— ThinkJS 14 | - 工业级 Web 架构 15 | 16 | ## Slides 17 | 18 | [【声享】HTTP协议与服务端编程](https://ppt.baomitu.com/d/bd67c335) 19 | 20 | ## 例子代码 21 | 22 | 克隆本项目,执行`npm install`, 然后运行目录中的例子代码,如: 23 | 24 | ``` 25 | node http-etag.js 26 | ``` 27 | 28 | ## License 29 | 30 | MIT -------------------------------------------------------------------------------- /http-simple.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | 4 | const server = http.createServer((req, res) => { 5 | const {pathname} = url.parse(`http://${req.headers.host}${req.url}`); 6 | if(pathname === '/') { 7 | res.writeHead(200, {'Content-Type': 'text/html'}); 8 | res.end('

Hello world

'); 9 | } else { 10 | res.writeHead(404, {'Content-Type': 'text/html'}); 11 | res.end('

Not Found

'); 12 | } 13 | }); 14 | 15 | server.on('clientError', (err, socket) => { 16 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 17 | }); 18 | 19 | server.listen(10080, () => { 20 | console.log('opened server on', server.address()); 21 | }); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-web-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "http-simple.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "checksum": "^0.1.1", 13 | "handlebars": "^4.7.6", 14 | "koa": "^2.3.0", 15 | "mime": "^2.4.4", 16 | "node-json2html": "^1.1.1", 17 | "qs": "^6.9.1", 18 | "query-string": "^5.0.0" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^5.16.0", 22 | "eslint-config-sprite": "^1.0.6", 23 | "eslint-plugin-html": "^5.0.5", 24 | "through2": "^2.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /view/coronavirus_date.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 疫情数据 7 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{#each data.countries ~}} 26 | 27 | {{~/each}} 28 | 29 |
国家确诊死亡治愈
{{country}}{{confirmed}}{{recovered}}{{deaths}}
30 | 31 | -------------------------------------------------------------------------------- /lib/interceptor.js: -------------------------------------------------------------------------------- 1 | class Interceptor { 2 | constructor() { 3 | this.aspects = []; 4 | } 5 | /** 6 | use(async (context, next) => { 7 | ...request... 8 | await next() 9 | ...response... 10 | }) 11 | */ 12 | 13 | use(/* async */ functor) { 14 | this.aspects.push(functor); 15 | return this; 16 | } 17 | 18 | async run(context) { 19 | const aspects = this.aspects; 20 | 21 | const proc = aspects.reduceRight(function (a, b) { // eslint-disable-line 22 | return async () => { 23 | await b(context, a); 24 | }; 25 | }, () => Promise.resolve()); 26 | 27 | try { 28 | await proc(); 29 | } catch (ex) { 30 | console.error(ex.message); 31 | } 32 | 33 | return context; 34 | } 35 | } 36 | 37 | module.exports = Interceptor; 38 | -------------------------------------------------------------------------------- /http-interceptor-default.js: -------------------------------------------------------------------------------- 1 | const Server = require('./lib/server'); 2 | const Router = require('./lib/middleware/router'); 3 | 4 | const app = new Server(); 5 | 6 | const router = new Router(); 7 | 8 | app.use(router.all('/test/:course/:lecture', async ({route, res}, next) => { 9 | res.setHeader('Content-Type', 'application/json'); 10 | res.body = route; 11 | await next(); 12 | })); 13 | 14 | app.use(router.all('.*', async ({req, res}, next) => { 15 | res.setHeader('Content-Type', 'text/html'); 16 | res.body = '

Hello world

'; 17 | await next(); 18 | })); 19 | 20 | // app.use(async ({req, res}, next) => { 21 | // res.setHeader('Content-Type', 'text/html'); 22 | // res.body = '

Hello world

'; 23 | // await next(); 24 | // }); 25 | 26 | app.listen({ 27 | port: 9090, 28 | host: '0.0.0.0', 29 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage/ 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Dependency directory 23 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 24 | node_modules/ 25 | 26 | # IDE config 27 | .idea 28 | 29 | # output 30 | output/ 31 | output.tar.gz 32 | 33 | runtime/ 34 | app/ 35 | 36 | model.dev.js 37 | .DS_Store 38 | 39 | package-lock.json 40 | 41 | nginx.daiwenliang.conf 42 | 43 | # static/ 44 | 45 | src/config/adapter.development.js 46 | -------------------------------------------------------------------------------- /http-interceptor-cluster.js: -------------------------------------------------------------------------------- 1 | const Server = require('./lib/server'); 2 | const Router = require('./lib/middleware/router'); 3 | 4 | const app = new Server({instances: 0, mode: 'development'}); 5 | const router = new Router(); 6 | 7 | let count = 0; 8 | process.on('message', (msg) => { 9 | console.log('visit count: %d', ++count); 10 | }); 11 | 12 | // 统计访问次数 13 | app.use(async (ctx, next) => { 14 | process.send('count'); 15 | await next(); 16 | }); 17 | 18 | app.use(async (ctx, next) => { 19 | console.log(`visit ${ctx.req.url} through worker: ${app.worker.process.pid}`); 20 | await next(); 21 | }); 22 | 23 | app.use(router.all('.*', async ({req, res}, next) => { 24 | res.setHeader('Content-Type', 'text/html'); 25 | res.body = '

Hello world

'; 26 | await next(); 27 | })); 28 | 29 | app.listen({ 30 | port: 9090, 31 | host: '0.0.0.0', 32 | }); -------------------------------------------------------------------------------- /http-mime2.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const fs = require('fs'); 4 | const mime = require('mime'); 5 | 6 | const server = http.createServer((req, res) => { 7 | const srvUrl = url.parse(`http://${req.url}`); 8 | let path = srvUrl.path; 9 | if(path === '/') path = '/index.html'; 10 | 11 | const resPath = `resource${path}`; 12 | 13 | if(!fs.existsSync(resPath)) { 14 | res.writeHead(404, {'Content-Type': 'text/html'}); 15 | return res.end('

404 Not Found

'); 16 | } 17 | 18 | const resStream = fs.createReadStream(resPath); 19 | res.writeHead(200, {'Content-Type': mime.getType(resPath)}); 20 | resStream.pipe(res); 21 | }); 22 | 23 | server.on('clientError', (err, socket) => { 24 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 25 | }); 26 | 27 | server.listen(10080, () => { 28 | console.log('opened server on', server.address()); 29 | }); 30 | -------------------------------------------------------------------------------- /lib/module/mock.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | let dataCache = null; 5 | 6 | function loadData() { 7 | if(!dataCache) { 8 | const file = path.resolve(__dirname, '../../mock/data.json'); 9 | const data = JSON.parse(fs.readFileSync(file, {encoding: 'utf-8'})); 10 | const reports = data.dailyReports; 11 | dataCache = {}; 12 | // 把数据给处理成以日期为key的JSON格式并缓存起来 13 | reports.forEach((report) => { 14 | dataCache[report.updatedDate] = report; 15 | }); 16 | } 17 | return dataCache; 18 | } 19 | 20 | function getCoronavirusKeyIndex() { 21 | return Object.keys(loadData()); 22 | } 23 | 24 | function getCoronavirusByDate(date) { 25 | const dailyData = loadData()[date] || {}; 26 | if(dailyData.countries) { 27 | dailyData.countries.sort((a, b) => { 28 | return b.confirmed - a.confirmed; 29 | }); 30 | } 31 | return dailyData; 32 | } 33 | 34 | module.exports = { 35 | getCoronavirusByDate, 36 | getCoronavirusKeyIndex, 37 | }; 38 | -------------------------------------------------------------------------------- /http-interceptor-cookie.js: -------------------------------------------------------------------------------- 1 | const Server = require('./lib/server'); 2 | const Router = require('./lib/middleware/router'); 3 | 4 | const cookie = require('./lib/aspect/cookie'); 5 | 6 | const app = new Server(); 7 | 8 | const router = new Router(); 9 | 10 | app.use(cookie); 11 | 12 | const users = {}; 13 | app.use(router.get('/foobar', async ({cookies, route, res}, next) => { 14 | res.setHeader('Content-Type', 'text/html;charset=utf-8'); 15 | let id = cookies.interceptor_js; 16 | if(id) { 17 | users[id] = users[id] || 1; 18 | users[id]++; 19 | res.body = `

你好,欢迎第${users[id]}次访问本站

`; 20 | } else { 21 | id = Math.random().toString(36).slice(2); 22 | users[id] = 1; 23 | res.body = '

你好,新用户

'; 24 | } 25 | res.setHeader('Set-Cookie', `interceptor_js=${id}; Path=/foobar1; Max-Age=${86400}`); 26 | await next(); 27 | })); 28 | 29 | app.use(router.all('.*', async ({req, res}, next) => { 30 | res.setHeader('Content-Type', 'text/html'); 31 | res.statusCode = 404; 32 | res.body = '

Not Found

'; 33 | await next(); 34 | })); 35 | 36 | app.listen({ 37 | port: 9090, 38 | host: '0.0.0.0', 39 | }); -------------------------------------------------------------------------------- /http-static.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const mime = require('mime'); 6 | 7 | const server = http.createServer((req, res) => { 8 | let filePath = path.resolve(__dirname, path.join('www', url.fileURLToPath(`file:///${req.url}`))); 9 | 10 | console.log(`Request: ${filePath}`); 11 | 12 | if(fs.existsSync(filePath)) { 13 | const stats = fs.statSync(filePath); 14 | if(stats.isDirectory()) { 15 | filePath = path.join(filePath, 'index.html'); 16 | } 17 | if(fs.existsSync(filePath)) { 18 | const content = fs.readFileSync(filePath); 19 | const {ext} = path.parse(filePath); 20 | res.writeHead(200, {'Content-Type': mime.getType(ext)}); 21 | return res.end(content); 22 | } 23 | } 24 | res.writeHead(404, {'Content-Type': 'text/html'}); 25 | res.end('

Not Found

'); 26 | }); 27 | 28 | server.on('clientError', (err, socket) => { 29 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 30 | }); 31 | 32 | server.listen(10080, () => { 33 | console.log('opened server on', server.address()); 34 | }); 35 | -------------------------------------------------------------------------------- /http-static-stream.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const mime = require('mime'); 6 | 7 | const server = http.createServer((req, res) => { 8 | let filePath = path.resolve(__dirname, path.join('www', url.fileURLToPath(`file:///${req.url}`))); 9 | 10 | console.log(`Request: ${filePath}`); 11 | 12 | if(fs.existsSync(filePath)) { 13 | const stats = fs.statSync(filePath); 14 | if(stats.isDirectory()) { 15 | filePath = path.join(filePath, 'index.html'); 16 | } 17 | if(fs.existsSync(filePath)) { 18 | const {ext} = path.parse(filePath); 19 | res.writeHead(200, {'Content-Type': mime.getType(ext)}); 20 | const fileStream = fs.createReadStream(filePath); 21 | fileStream.pipe(res); 22 | } 23 | } else { 24 | res.writeHead(404, {'Content-Type': 'text/html'}); 25 | res.end('

Not Found

'); 26 | } 27 | }); 28 | 29 | server.on('clientError', (err, socket) => { 30 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 31 | }); 32 | 33 | server.listen(10080, () => { 34 | console.log('opened server on', server.address()); 35 | }); 36 | -------------------------------------------------------------------------------- /tcp-server.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | 3 | function responseData(str, status = 200, desc = 'OK') { 4 | return `HTTP/1.1 ${status} ${desc} 5 | Connection: keep-alive 6 | Date: ${new Date()} 7 | Content-Length: ${str.length} 8 | Content-Type: text/html 9 | 10 | ${str}`; 11 | } 12 | 13 | 14 | const server = net.createServer((socket) => { 15 | // socket.setKeepAlive(true, 600000); 16 | 17 | 18 | 19 | socket.on('data', (data) => { 20 | const matched = data.toString('utf-8').match(/^GET ([/\w]+) HTTP/); 21 | if(matched) { 22 | const path = matched[1]; 23 | if(path === '/') { 24 | socket.write(responseData('

Hello world

')); 25 | } else { 26 | socket.write(responseData('

Not Found

', 404, 'NOT FOUND')); 27 | } 28 | } 29 | console.log(`DATA:\n\n${data}`); 30 | }); 31 | 32 | socket.on('close', () => { 33 | console.log('connection closed, goodbye!\n\n\n'); 34 | }); 35 | }).on('error', (err) => { 36 | // handle errors here 37 | throw err; 38 | }); 39 | 40 | server.listen({ 41 | host: '0.0.0.0', 42 | port: 10080, 43 | // exclusive: true, 44 | }, () => { 45 | console.log('opened server on', server.address()); 46 | }); 47 | -------------------------------------------------------------------------------- /http-cache-control.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const mime = require('mime'); 6 | 7 | const server = http.createServer((req, res) => { 8 | let filePath = path.resolve(__dirname, path.join('www', url.fileURLToPath(`file:///${req.url}`))); 9 | 10 | console.log(`Request: ${filePath}`); 11 | 12 | if(fs.existsSync(filePath)) { 13 | const stats = fs.statSync(filePath); 14 | if(stats.isDirectory()) { 15 | filePath = path.join(filePath, 'index.html'); 16 | } 17 | if(fs.existsSync(filePath)) { 18 | const {ext} = path.parse(filePath); 19 | res.writeHead(200, { 20 | 'Content-Type': mime.getType(ext), 21 | 'Cache-Control': 'max-age=86400', // 缓存一天 22 | }); 23 | const fileStream = fs.createReadStream(filePath); 24 | fileStream.pipe(res); 25 | } 26 | } else { 27 | res.writeHead(404, {'Content-Type': 'text/html'}); 28 | res.end('

Not Found

'); 29 | } 30 | }); 31 | 32 | server.on('clientError', (err, socket) => { 33 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 34 | }); 35 | 36 | server.listen(10080, () => { 37 | console.log('opened server on', server.address()); 38 | }); -------------------------------------------------------------------------------- /http-stream.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const server = http.createServer((req, res) => { 7 | const srvUrl = url.parse(`http://${req.url}`); 8 | 9 | let pathname = srvUrl.pathname; 10 | if(pathname === '/') pathname = '/index'; 11 | const pathInfo = path.parse(pathname); 12 | 13 | if(!pathInfo.ext) { 14 | pathInfo.ext = '.html'; 15 | pathInfo.base += pathInfo.ext; 16 | } 17 | 18 | const resPath = path.join('resource', pathInfo.dir, pathInfo.base); 19 | 20 | if(!fs.existsSync(resPath)) { 21 | res.writeHead(404, {'Content-Type': 'text/html'}); 22 | return res.end('

404 Not Found

'); 23 | } 24 | 25 | const resStream = fs.createReadStream(resPath); 26 | 27 | if(pathInfo.ext === '.html' || pathInfo.ext === '.htm') { 28 | res.writeHead(200, {'Content-Type': 'text/html'}); 29 | } else if(pathInfo.ext === '.png') { 30 | res.writeHead(200, {'Content-Type': 'image/png'}); 31 | } 32 | // ... 33 | 34 | resStream.pipe(res); 35 | }); 36 | 37 | server.on('clientError', (err, socket) => { 38 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 39 | }); 40 | 41 | server.listen(10080, () => { 42 | console.log('opened server on', server.address()); 43 | }); 44 | -------------------------------------------------------------------------------- /http-mime.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const fs = require('fs'); 4 | 5 | function getMimeType(res) { 6 | const EXT_MIME_TYPES = { 7 | default: 'text/html', 8 | '.js': 'text/javascript', 9 | '.css': 'text/css', 10 | '.json': 'text/json', 11 | '.jpeg': 'image/jpeg', 12 | '.jpg': 'image/jpg', 13 | '.png': 'image/png', 14 | }; 15 | 16 | const path = require('path'); 17 | const mime_type = EXT_MIME_TYPES[path.extname(res)] || EXT_MIME_TYPES.default; 18 | return mime_type; 19 | } 20 | 21 | const server = http.createServer((req, res) => { 22 | const srvUrl = url.parse(`http://${req.url}`); 23 | let path = srvUrl.path; 24 | if(path === '/') path = '/index.html'; 25 | 26 | const resPath = `resource${path}`; 27 | 28 | if(!fs.existsSync(resPath)) { 29 | res.writeHead(404, {'Content-Type': 'text/html'}); 30 | return res.end('

404 Not Found

'); 31 | } 32 | 33 | const resStream = fs.createReadStream(resPath); 34 | res.writeHead(200, {'Content-Type': getMimeType(resPath)}); 35 | resStream.pipe(res); 36 | }); 37 | 38 | server.on('clientError', (err, socket) => { 39 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 40 | }); 41 | 42 | server.listen(10080, () => { 43 | console.log('opened server on', server.address()); 44 | }); 45 | -------------------------------------------------------------------------------- /http-consult.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | 4 | const responseData = { 5 | ID: 'zhangsan', 6 | Name: '张三', 7 | RegisterDate: '2020年3月1日', 8 | }; 9 | 10 | function toHTML(data) { 11 | return ` 12 | 17 | `; 18 | } 19 | 20 | const server = http.createServer((req, res) => { 21 | const {pathname} = url.parse(`http://${req.headers.host}${req.url}`); 22 | if(pathname === '/') { 23 | const accept = req.headers.accept; 24 | if(req.method === 'POST' || accept.indexOf('application/json') >= 0) { 25 | res.writeHead(200, {'Content-Type': 'application/json'}); 26 | res.end(JSON.stringify(responseData)); 27 | } else { 28 | res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'}); 29 | res.end(toHTML(responseData)); 30 | } 31 | } else { 32 | res.writeHead(404, {'Content-Type': 'text/html'}); 33 | res.end('

Not Found

'); 34 | } 35 | }); 36 | 37 | server.on('clientError', (err, socket) => { 38 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 39 | }); 40 | 41 | server.listen(10080, () => { 42 | console.log('opened server on', server.address()); 43 | }); 44 | -------------------------------------------------------------------------------- /http-etag.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const fs = require('fs'); 4 | const checksum = require('checksum'); 5 | const mime = require('mime'); 6 | 7 | function getMimeType(res) { 8 | const EXT_MIME_TYPES = mime.types; 9 | 10 | const path = require('path'); 11 | const mime_type = EXT_MIME_TYPES[path.extname(res).slice(1) || 'html']; 12 | return mime_type; 13 | } 14 | const server = http.createServer((req, res) => { 15 | const srvUrl = url.parse(`http://${req.url}`); 16 | let path = srvUrl.path; 17 | if(path === '/') path = '/index.html'; 18 | 19 | const resPath = `resource${path}`; 20 | 21 | if(!fs.existsSync(resPath)) { 22 | res.writeHead(404, {'Content-Type': 'text/html'}); 23 | return res.end('

404 Not Found

'); 24 | } 25 | 26 | checksum.file(resPath, (err, sum) => { 27 | const resStream = fs.createReadStream(resPath); 28 | sum = `"${sum}"`; // etag 要加双引号 29 | 30 | if(req.headers['if-none-match'] === sum) { 31 | res.writeHead(304, { 32 | 'Content-Type': getMimeType(resPath), 33 | etag: sum, 34 | }); 35 | res.end(); 36 | } else { 37 | res.writeHead(200, { 38 | 'Content-Type': getMimeType(resPath), 39 | etag: sum, 40 | }); 41 | resStream.pipe(res); 42 | } 43 | }); 44 | }); 45 | 46 | server.on('clientError', (err, socket) => { 47 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 48 | }); 49 | 50 | server.listen(10080, () => { 51 | console.log('opened server on', server.address()); 52 | }); 53 | -------------------------------------------------------------------------------- /http-last-modified.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const mime = require('mime'); 6 | 7 | const server = http.createServer((req, res) => { 8 | let filePath = path.resolve(__dirname, path.join('www', url.fileURLToPath(`file:///${req.url}`))); 9 | 10 | console.log(`Request: ${filePath}`); 11 | 12 | if(fs.existsSync(filePath)) { 13 | const stats = fs.statSync(filePath); 14 | if(stats.isDirectory()) { 15 | filePath = path.join(filePath, 'index.html'); 16 | } 17 | if(fs.existsSync(filePath)) { 18 | const {ext} = path.parse(filePath); 19 | const stats = fs.statSync(filePath); 20 | const timeStamp = req.headers['if-modified-since']; 21 | let status = 200; 22 | if(timeStamp && Number(timeStamp) === stats.mtimeMs) { 23 | status = 304; 24 | } 25 | res.writeHead(status, { 26 | 'Content-Type': mime.getType(ext), 27 | 'Cache-Control': 'max-age=86400', // 缓存一天 28 | 'Last-Modified': stats.mtimeMs, 29 | }); 30 | if(status === 200) { 31 | const fileStream = fs.createReadStream(filePath); 32 | fileStream.pipe(res); 33 | } else { 34 | res.end(); 35 | } 36 | } 37 | } else { 38 | res.writeHead(404, {'Content-Type': 'text/html'}); 39 | res.end('

Not Found

'); 40 | } 41 | }); 42 | 43 | server.on('clientError', (err, socket) => { 44 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 45 | }); 46 | 47 | server.listen(10080, () => { 48 | console.log('opened server on', server.address()); 49 | }); -------------------------------------------------------------------------------- /http-interceptor.js: -------------------------------------------------------------------------------- 1 | const Server = require('./lib/interceptor-server'); 2 | 3 | const mime = require('mime'); 4 | const url = require('url'); 5 | const parseQuery = require('./lib/middleware/query'); 6 | const router = require('./lib/middleware/router'); 7 | 8 | function getMimeType(res) { 9 | const EXT_MIME_TYPES = mime.types; 10 | 11 | const path = require('path'); 12 | const mime_type = EXT_MIME_TYPES[path.extname(res).slice(1) || 'html']; 13 | return mime_type; 14 | } 15 | 16 | const app = new Server(); 17 | 18 | app.use((context, next) => { 19 | console.log(`Visit: ${context.req.url}`); 20 | next(); 21 | }); 22 | 23 | app.use(async (context, next) => { 24 | const req = context.req; 25 | const srvUrl = url.parse(`http://${req.url}`); 26 | let path = srvUrl.path; 27 | if(path === '/') path = '/index.html'; 28 | 29 | const resPath = `resource${path}`.replace(/\?.*/g, ''); 30 | const mimeType = getMimeType(resPath); 31 | context.mimeType = mimeType; 32 | 33 | await next(); 34 | }); 35 | 36 | // app.use(parseQuery); 37 | 38 | // app.use((context, next) => { 39 | // context.status = 200; 40 | // context.body = `

Hello World!

${JSON.stringify(context.query)}`; 41 | // }); 42 | 43 | const index = router.get('/', (context, next) => { 44 | context.status = 200; 45 | context.body = 'Hello'; 46 | next(); 47 | }); 48 | 49 | const test = router.get('/test', (context, next) => { 50 | context.status = 200; 51 | context.body = 'Test'; 52 | next(); 53 | }); 54 | 55 | app.use(index); 56 | app.use(test); 57 | 58 | app.listen(10080, (server) => { 59 | console.log('opened server on', server.address()); 60 | }); 61 | -------------------------------------------------------------------------------- /lib/middleware/router.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | const path = require('path'); 3 | 4 | function check(rule, pathname) { 5 | const paraMatched = rule.match(/:[^/]+/g); 6 | const ruleExp = new RegExp(`^${rule.replace(/:([^/]+)/g, '([^/]+)')}$`); 7 | const ruleMatched = pathname.match(ruleExp); 8 | if(ruleMatched) { 9 | const ret = {}; 10 | if(paraMatched) { 11 | for(let i = 0; i < paraMatched.length; i++) { 12 | ret[paraMatched[i].slice(1)] = ruleMatched[i + 1]; 13 | } 14 | } 15 | return ret; 16 | } 17 | return null; 18 | } 19 | 20 | function route(method, rule, aspect) { 21 | return async (ctx, next) => { 22 | const req = ctx.req; 23 | if(!ctx.url) ctx.url = url.parse(`http://${req.headers.host}${req.url}`); 24 | const checked = check(rule, ctx.url.pathname); 25 | if(!ctx.route && (method === '*' || req.method === method) 26 | && !!checked) { 27 | ctx.route = checked; 28 | await aspect(ctx, next); 29 | } else { 30 | await next(); 31 | } 32 | }; 33 | } 34 | 35 | class Router { 36 | constructor(base = '') { 37 | this.baseURL = base; 38 | } 39 | 40 | get(rule, aspect) { 41 | return route('GET', path.join(this.baseURL, rule), aspect); 42 | } 43 | 44 | post(rule, aspect) { 45 | return route('POST', path.join(this.baseURL, rule), aspect); 46 | } 47 | 48 | put(rule, aspect) { 49 | return route('PUT', path.join(this.baseURL, rule), aspect); 50 | } 51 | 52 | delete(rule, aspect) { 53 | return route('DELETE', path.join(this.baseURL, rule), aspect); 54 | } 55 | 56 | all(rule, aspect) { 57 | return route('*', path.join(this.baseURL, rule), aspect); 58 | } 59 | } 60 | 61 | module.exports = Router; 62 | -------------------------------------------------------------------------------- /http-interceptor-coronavirs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const Server = require('./lib/server'); 3 | const Router = require('./lib/middleware/router'); 4 | const param = require('./lib/aspect/param'); 5 | 6 | const app = new Server(); 7 | 8 | const router = new Router(); 9 | 10 | app.use(({req}, next) => { 11 | console.log(`${req.method} ${req.url}`); 12 | next(); 13 | }); 14 | 15 | app.use(param); 16 | 17 | app.use(router.get('/coronavirus/index', async ({params, route, res}, next) => { 18 | const {getCoronavirusKeyIndex} = require('./lib/module/mock'); 19 | const index = getCoronavirusKeyIndex(); 20 | 21 | if(params.type === 'json') { 22 | res.setHeader('Content-Type', 'application/json'); 23 | res.body = {data: index}; 24 | } else { 25 | const handlebars = require('handlebars'); 26 | const tpl = fs.readFileSync('./view/coronavirus_index.html', {encoding: 'utf-8'}); 27 | 28 | const template = handlebars.compile(tpl); 29 | const result = template({data: index}); 30 | 31 | res.setHeader('Content-Type', 'text/html'); 32 | res.body = result; 33 | await next(); 34 | } 35 | })); 36 | 37 | app.use(router.get('/coronavirus/:date', async ({params, route, res}, next) => { 38 | const {getCoronavirusByDate} = require('./lib/module/mock'); 39 | const data = getCoronavirusByDate(route.date); 40 | 41 | if(params.type === 'json') { 42 | res.setHeader('Content-Type', 'application/json'); 43 | res.body = {data}; 44 | } else { 45 | const handlebars = require('handlebars'); 46 | const tpl = fs.readFileSync('./view/coronavirus_date.html', {encoding: 'utf-8'}); 47 | 48 | const template = handlebars.compile(tpl); 49 | const result = template({data}); 50 | 51 | res.setHeader('Content-Type', 'text/html'); 52 | res.body = result; 53 | } 54 | await next(); 55 | })); 56 | 57 | app.use(router.all('.*', async ({params, req, res}, next) => { 58 | res.setHeader('Content-Type', 'text/html'); 59 | res.body = '

Not Found

'; 60 | res.statusCode = 404; 61 | await next(); 62 | })); 63 | 64 | // app.use(async ({req, res}, next) => { 65 | // res.setHeader('Content-Type', 'text/html'); 66 | // res.body = '

Hello world

'; 67 | // await next(); 68 | // }); 69 | 70 | app.listen({ 71 | port: 9090, 72 | host: '0.0.0.0', 73 | }); -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const cluster = require('cluster'); 3 | const cpuNums = require('os').cpus().length; 4 | const Interceptor = require('./interceptor.js'); 5 | 6 | module.exports = class { 7 | constructor({instances = 1, enableCluster = true, mode = 'production'} = {}) { 8 | if(mode === 'development') { 9 | instances = 1; 10 | enableCluster = true; 11 | } 12 | this.mode = mode; 13 | this.instances = instances || cpuNums; 14 | this.enableCluster = enableCluster; 15 | const interceptor = new Interceptor(); 16 | 17 | this.server = http.createServer(async (req, res) => { 18 | await interceptor.run({req, res}); 19 | if(!res.writableFinished) { 20 | let body = res.body || '200 OK'; 21 | if(body.pipe) { 22 | body.pipe(res); 23 | } else { 24 | if(typeof body !== 'string' && res.getHeader('Content-Type') === 'application/json') { 25 | body = JSON.stringify(body); 26 | } 27 | res.end(body); 28 | } 29 | } 30 | }); 31 | 32 | this.server.on('clientError', (err, socket) => { 33 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 34 | }); 35 | 36 | this.interceptor = interceptor; 37 | } 38 | 39 | listen(opts, cb = () => {}) { 40 | if(typeof opts === 'number') opts = {port: opts}; 41 | opts.host = opts.host || '0.0.0.0'; 42 | const instances = this.instances; 43 | if(this.enableCluster && cluster.isMaster) { 44 | for(let i = 0; i < instances; i++) { 45 | cluster.fork(); 46 | } 47 | 48 | function broadcast(message) { // eslint-disable-line no-inner-declarations 49 | Object.entries(cluster.workers).forEach(([id, worker]) => { 50 | worker.send(message); 51 | }); 52 | } 53 | 54 | // 广播消息 55 | Object.keys(cluster.workers).forEach((id) => { 56 | cluster.workers[id].on('message', broadcast); 57 | }); 58 | 59 | if(this.mode === 'development') { 60 | require('fs').watch('.', {recursive: true}, (eventType) => { 61 | if(eventType === 'change') { 62 | Object.entries(cluster.workers).forEach(([id, worker]) => { 63 | console.log('kill workder %d', id); 64 | worker.kill(); 65 | }); 66 | cluster.fork(); 67 | } 68 | }); 69 | } else { 70 | cluster.on('exit', (worker, code, signal) => { 71 | console.log('worker %d died (%s). restarting...', 72 | worker.process.pid, signal || code); 73 | cluster.fork(); 74 | }); 75 | } 76 | } else { 77 | this.worker = cluster.worker; 78 | console.log(`Starting up http-server 79 | http://${opts.host}:${opts.port}`); 80 | this.server.listen(opts, () => cb(this.server)); 81 | } 82 | } 83 | 84 | use(aspect) { 85 | return this.interceptor.use(aspect); 86 | } 87 | }; -------------------------------------------------------------------------------- /http-compression.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const mime = require('mime'); 6 | const zlib = require('zlib'); 7 | 8 | const server = http.createServer((req, res) => { 9 | let filePath = path.resolve(__dirname, path.join('www', url.fileURLToPath(`file:///${req.url}`))); 10 | 11 | console.log(`Request: ${filePath}`); 12 | 13 | if(fs.existsSync(filePath)) { 14 | const stats = fs.statSync(filePath); 15 | if(stats.isDirectory()) { 16 | filePath = path.join(filePath, 'index.html'); 17 | } 18 | if(fs.existsSync(filePath)) { 19 | const {ext} = path.parse(filePath); 20 | const stats = fs.statSync(filePath); 21 | const timeStamp = req.headers['if-modified-since']; 22 | let status = 200; 23 | if(timeStamp && Number(timeStamp) === stats.mtimeMs) { 24 | status = 304; 25 | } 26 | const mimeType = mime.getType(ext); 27 | const responseHeaders = { 28 | 'Content-Type': mimeType, 29 | 'Cache-Control': 'max-age=86400', // 缓存一天 30 | 'Last-Modified': stats.mtimeMs, 31 | }; 32 | const acceptEncoding = req.headers['accept-encoding']; 33 | const compress = acceptEncoding && /^(text|application)\//.test(mimeType); 34 | if(compress) { 35 | acceptEncoding.split(/\s*,\s*/).some((encoding) => { 36 | if(encoding === 'gzip') { 37 | responseHeaders['Content-Encoding'] = 'gzip'; 38 | return true; 39 | } 40 | if(encoding === 'deflate') { 41 | responseHeaders['Content-Encoding'] = 'deflate'; 42 | return true; 43 | } 44 | if(encoding === 'br') { 45 | responseHeaders['Content-Encoding'] = 'br'; 46 | return true; 47 | } 48 | return false; 49 | }); 50 | } 51 | const compressionEncoding = responseHeaders['Content-Encoding']; 52 | res.writeHead(status, responseHeaders); 53 | if(status === 200) { 54 | const fileStream = fs.createReadStream(filePath); 55 | if(compress && compressionEncoding) { 56 | let comp; 57 | if(compressionEncoding === 'gzip') { 58 | comp = zlib.createGzip(); 59 | } else if(compressionEncoding === 'deflate') { 60 | comp = zlib.createDeflate(); 61 | } else { 62 | comp = zlib.createBrotliCompress(); 63 | } 64 | fileStream.pipe(comp).pipe(res); 65 | } else { 66 | fileStream.pipe(res); 67 | } 68 | } else { 69 | res.end(); 70 | } 71 | } 72 | } else { 73 | res.writeHead(404, {'Content-Type': 'text/html'}); 74 | res.end('

Not Found

'); 75 | } 76 | }); 77 | 78 | server.on('clientError', (err, socket) => { 79 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 80 | }); 81 | 82 | server.listen(10080, () => { 83 | console.log('opened server on', server.address()); 84 | }); --------------------------------------------------------------------------------