├── 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 |
10 | {{#each data ~}}
11 | - {{this}}
12 | {{~/each}}
13 |
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 | | {{country}} | {{confirmed}} | {{recovered}} | {{deaths}} |
27 | {{~/each}}
28 |
29 |
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 |
13 | - 账号:${data.ID}
14 | - 昵称:${data.Name}
15 | - 注册时间:${data.RegisterDate}
16 |
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 | });
--------------------------------------------------------------------------------