Node.js知识梳理(二)——进阶

以下内容来自对《从前端到全栈》一书的学习记录~

学习的时候用的是V16.20.018+之后的语法差别还是有的~

请求优化

我们在请求资源的时候,是需要做优化的,这里的优化涉及到了缓存。浏览器的缓存策略有两种:

  • 强缓存
  • 协商缓存

关于两者的区别可以看看之前的那篇《【HTTP】04_进阶》关于缓存的理解~

首先是强缓存的实现:

修改index.html,在里面引入我们的图片:

<body><h1>Hello World</h1><img src="./1.jpg" />
</body>
res.writeHead(200, {'Content-Type': mime.getType(ext),'Cache-Control': 'max-age=86400', // 缓存一天
});

在这里插入图片描述

再次访问:

在这里插入图片描述

index.html 页面是直接通过浏览器地址栏访问的。根据浏览器的标准,通过地址栏访问、以及强制刷新网页的时候,HTTP 请求头自动会带上Cache-Control: no-cachePragma: no-cache的信息。只要有这两个请求头之一,浏览器就会忽略响应头中的Cache-Control字段。

强缓存有个弊端就是,在未过期前更新静态资源(如果图片、css等文件,读取的还是旧文件)你可以在文件夹中修改图片,刷新页面发现还是旧图~

只有强制刷新(ctrl+F5)才能更新旧图,所以一般强缓存适用于不需要修改的资源,协商缓存用的比较多~

下面是协商缓存的实现:

const timeStamp = req.headers['if-modified-since'];let status = 200;// stats.mtimeMs表示文件的修改时间if(timeStamp && Number(timeStamp) === stats.mtimeMs) {// 如果timeStamp和stats.mtimeMS相等,说明文件内容没有修改,返回响应状态码 304status = 304;}res.writeHead(status, {'Content-Type': mime.getType(ext),'Last-Modified': stats.mtimeMs, // 协商缓存响应头});if(status === 200) {const fileStream = fs.createReadStream(filePath);fileStream.pipe(res);} else { res.end(); // 如果状态码不是200,不用返回Body}

在这里插入图片描述

协商缓存不止Last-Modified一种,还有一种协商缓存是Etag,它的机制和Last-Modified大同小异,只是把Last-Modified的时间戳换成Etag签名,相应地把If-Modified-Since字段换成If-None-Match字段。Etag的值可以用资源文件的 MD5sha 签名。

协商缓存为什么要有两种呢?因为,有时候我们的网站是分布式部署在多台服务器上,一个资源文件可能在每台服务器上都有副本,相应地资源文件被修改时候,新的文件要同步到各个服务器上,导致各个文件副本的修改时间不一定相同。那么当用户一次访问请求的服务器和另一次访问请求的服务器不同时,就有可能因为两个文件副本的修改时间不同而使得Last-Modified形式的协商缓存失效(还有可能是因为两次修改文件的间隙可以忽略不记,所以时间没有改变)。如果这种情况采用Etag形式的协商缓存,根据文件内容而不是修改时间来判断缓存,就不会有这个问题了。

如果浏览器被用户强制刷新,那么强缓存和协商缓存都会失效。因为强制刷新会带上Cache-Control: no-cachePragma: no-cache请求头且不会带上If-Modified-SceneIf-None-Match请求头。

文件压缩

浏览器支持 gzip、deflate 和 br 这三种压缩算法,使用它们压缩文件,能够大大节省传输带宽,提升请求的响应速度,减少页面访问的延迟。

我们需要根据客户端的Accept-Encoding请求头字段实现多种压缩算法:

npm i zlib --save
import http from 'http';
import { fileURLToPath } from 'url';
import { dirname, resolve, join, parse } from 'path';
import fs from 'fs';
import mime from 'mime';
import zlib from 'zlib';const __dirname = dirname(fileURLToPath(import.meta.url));const server = http.createServer((req, res) => {// 将想要获取的文件路径格式化一下,转成绝对路径let filePath = resolve(__dirname, join('www', `${req.url}`));// 判断文件是否存在if(fs.existsSync(filePath)) {// 判断是否是文件目录const stats = fs.statSync(filePath);const isDir = stats.isDirectory();if(isDir) {// 如果是目录,则访问的是index.htmlfilePath = join(filePath, 'index.html');}// 获取文件后缀const { ext } = parse(filePath);const timeStamp = req.headers['if-modified-since'];let status = 200;// stats.mtimeMs表示文件的修改时间if(timeStamp && Number(timeStamp) === stats.mtimeMs) {// 如果timeStamp和stats.mtimeMS相等,说明文件内容没有修改,返回响应状态码 304status = 304;}// 获取文件后缀const mimeType = mime.getType(ext);// 这里同时采用了两者缓存策略const responseHeaders = {'Content-Type': mimeType,'Cache-Control': 'max-age=86400', // 缓存一天'Last-Modified': stats.mtimeMs,};// 获取请求头const acceptEncoding = req.headers['accept-encoding'];// 判断是哪种压缩算法const compress = acceptEncoding && /^(text|application)\//.test(mimeType);if(compress) {// 判断客户端是否支持 gzip、deflate、或者 br 中的一种压缩算法acceptEncoding.split(/\s*,\s*/).some((encoding) => {if(encoding === 'gzip') {responseHeaders['Content-Encoding'] = 'gzip';return true;}if(encoding === 'deflate') {responseHeaders['Content-Encoding'] = 'deflate';return true;}if(encoding === 'br') {responseHeaders['Content-Encoding'] = 'br';return true;}return false;});}const compressionEncoding = responseHeaders['Content-Encoding']; // 获取选中的压缩方式// 设置响应头res.writeHead(status, responseHeaders);if(status === 200) {const fileStream = fs.createReadStream(filePath);if(compress && compressionEncoding) {let comp;// 使用指定的压缩方式压缩文件if(compressionEncoding === 'gzip') {comp = zlib.createGzip();} else if(compressionEncoding === 'deflate') {comp = zlib.createDeflate();} else {comp = zlib.createBrotliCompress();}fileStream.pipe(comp).pipe(res);} else {fileStream.pipe(res);}} else {res.end();}}else {res.writeHead(404, {'Content-Type': 'text/html'});res.end('<h1>Not Found</h1>');}
});server.on('clientError', (err, socket) => {socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});server.listen(8080, () => {console.log('opened server on', server.address());
});

在这里插入图片描述

拦截器

之前学习koa的时候会接触到一个概念:洋葱模型,当我们访问一个路由的时候,会层层进入洋葱,每一层都会做一些处理,然后再一层层出来:

在这里插入图片描述

这里的拦截器,就跟上面的作用差不多~

// lib/interceptor.js
class Interceptor {constructor() {// 存储中间件函数this.aspects = [];}use(functor) {// 注册中间件函数this.aspects.push(functor);return this;}async run(context) {const aspects = this.aspects;// 执行中间函数,执行规则跟洋葱模型一样~const proc = aspects.reduceRight(function (a, b) { // eslint-disable-linereturn async () => {await b(context, a);};}, () => Promise.resolve());try {await proc();} catch (ex) {console.error(ex.message);}return context;}
}module.exports = Interceptor;

封装一下Http服务器,使用拦截器:

// lib/server.js
import http from 'http';
import Interceptor from './interceptor.js';class Server{constructor() {const interceptor = new Interceptor();this.server = http.createServer(async (req, res) => {// 执行注册的拦截函数await interceptor.run({req, res}); if(!res.writableFinished) {let body = res.body || '200 OK';if(body.pipe) {body.pipe(res);} else {if(typeof body !== 'string' && res.getHeader('Content-Type') === 'application/json') {body = JSON.stringify(body);}res.end(body);}}});this.server.on('clientError', (err, socket) => {socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');});this.interceptor = interceptor;}// 监听listen(opts, cb = () => {}) {if(typeof opts === 'number') opts = {port: opts};opts.host = opts.host || 'localhost';console.log(`Starting up http-server http://${opts.host}:${opts.port}`);this.server.listen(opts, () => cb(this.server));}// 注册中间件use(aspect) { return this.interceptor.use(aspect);}
}export default Server;

这样我们在index.js中应该这样创建服务器:

import Server from './lib/server.js';const app = new Server();app.listen({port: 9090,host: '0.0.0.0',
})

先来测试一下拦截器,访问的时候页面返回Hello World~

import Server from './lib/server.js';const app = new Server();// 注册中间件
app.use(async ({ res }, next) => {res.setHeader('Content-Type', 'text/html');res.body = '<h1>Hello world</h1>';await next();
})app.listen(9090)

路由

koa-router这个中间件本质上就是一个拦截器,来实现路由~

// middleware/router.js
import url from 'url';
import path from 'path';/*** 利用正则表达式检查真正的路径和路由规则是否匹配* @param {*} rule 如:/test/:course/:lecture* @param {*} pathname 如:/test/123/abc* @returns */
function check(rule, pathname) {// window下需要替换一下rule = rule.replace(/\\/g, '/');const paraMatched = rule.match(/:[^/]+/g);const ruleExp = new RegExp(`^${rule.replace(/:([^/]+)/g, '([^/]+)')}$`);const ruleMatched = pathname.match(ruleExp);if(ruleMatched) {const ret = {};if(paraMatched) {for(let i = 0; i < paraMatched.length; i++) {ret[paraMatched[i].slice(1)] = ruleMatched[i + 1];}}// 最后得到的结果为 ret = {course: 123, lecture: abc}return ret;}return null;
}function route(method, rule, aspect) {return async(ctx, next) => {const req = ctx.req;if(!ctx.url) ctx.url = url.parse(`http://${req.headers.host}${req.url}`);const checked = check(rule, ctx.url.pathname);if(!ctx.route && (method === '*' || req.method === method)&& !!checked) {ctx.route = checked;await aspect(ctx, next);} else {await next();}}
}class Router {constructor(base = '') {this.baseURL = base;}get(rule, aspect) {return route('GET', path.join(this.baseURL, rule), aspect);}post(rule, aspect) {return route('POST', path.join(this.baseURL, rule), aspect);}put(rule, aspect) {return route('PUT', path.join(this.baseURL, rule), aspect);}delete(rule, aspect) {return route('DELETE', path.join(this.baseURL, rule), aspect);}all(rule, aspect) {return route('*', path.join(this.baseURL, rule), aspect);}
}export default Router;
// index.js
import Server from './lib/server.js';
import Router from './middleware/router.js';const app = new Server();const router = new Router();// 请求指定路由
app.use(router.all('/test/:course/:lecture', async ({route, res}, next) => {res.setHeader('Content-Type', 'application/json');res.body = route;await next();
}));// 默认路由
app.use(router.all('.*', async ({req, res}, next) => {res.setHeader('Content-Type', 'text/html');res.body = '<h1>Hello world</h1>';await next();
}));app.listen(9090)

获取GET请求参数

常用的格式包括application/x-www-form-urlencoded、multipart/form-data、application/json等。

// aspect/param.js
import url from 'url';
import querystring from 'querystring';export default async(ctx, next) => {const { req } = ctx;const {query} = url.parse(`http://${req.headers.host}${req.url}`);ctx.params = querystring.parse(query);console.log(ctx.params);await next();
}
// index.js
import params from './aspect/param.js'
// ...
app.use(params);

访问http://localhost:9090/?name=test会在控制台打印{ name: 'test' }

使用Mock

后端大佬只给了接口文档,还没开发完接口的时候,我们可以借助Mock照着文档造数据,然后模拟请求~

这里直接使用虚拟数据,新建mock/data.json存放假数据,文件地址:data.json

// module/mock.js
import fs from 'fs';
import path from 'path';
import url from 'url';let dataCache = null;function loadData() {if(!dataCache) {const __dirname = path.dirname(url.fileURLToPath(import.meta.url));const file = path.resolve(__dirname, '..', 'mock/data.json');const data = JSON.parse(fs.readFileSync(file, {encoding: 'utf-8'}));const reports = data.dailyReports; // 数组格式的数据dataCache = {};// 把数组数据转换成以日期为key的JSON格式并缓存起来reports.forEach((report) => {dataCache[report.updatedDate] = report;});}return dataCache;
}// 获取所有有疫情记录的日期
export function getCoronavirusKeyIndex() {return Object.keys(loadData());
}// 获取当前日期对应的疫情数据
export function getCoronavirusByDate(date) {const dailyData = loadData()[date] || {};if(dailyData.countries) {// 按照各国确诊人数排序dailyData.countries.sort((a, b) => {return b.confirmed - a.confirmed;});}return dailyData;
}

修改index.js

import Server from './lib/server.js';
import Router from './middleware/router.js';
import params from './aspect/param.js'
import { getCoronavirusKeyIndex, getCoronavirusByDate } from './module/mock.js'const app = new Server();const router = new Router();// 在服务器的控制台上就能知道用户访问了哪个 URL
app.use(({req}, next) => {console.log(`${req.method} ${req.url}`);next();
});// 解析 GET 参数的拦截切面
app.use(params);// 获取所有有疫情记录的日期
app.use(router.get('/coronavirus/index', async ({route, res}, next) => {const index = getCoronavirusKeyIndex();res.setHeader('Content-Type', 'application/json');res.body = {data: index};await next();
}));// 获取当前日期对应的疫情数据
app.use(router.get('/coronavirus/:date', async ({route, res}, next) => {const data = getCoronavirusByDate(route.date);res.setHeader('Content-Type', 'application/json');res.body = {data};await next();
}));// 默认路由
app.use(router.all('.*', async ({req, res}, next) => {res.setHeader('Content-Type', 'text/html');res.body = '<h1>Hello world</h1>';await next();
}));app.listen(9090)

这样我们访问http://localhost:9090/coronavirus/index可以获得日期的 JSON 数据,访问http://localhost:9090/coronavirus/2020-01-22可以获得 2020 年 1 月 22 日当天的疫情 JSON 数据

服务端渲染

对网页渲染速度敏感、依赖 SEO,或是比较简单,都适合使用服务端渲染,服务器将数据在页面上填充完整之后再将页面返回~

这里需要借助目标引擎,该书中使用的handlebars

npm install handlebars --save

新建view/coronavirus_date.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>疫情数据</title><style>td:not(:first-child) {text-align: right;}td:nth-child(3) {color: red;}td:nth-child(4) {color: green;}</style>
</head>
<body><table><thead><tr><th>国家</th><th>确诊</th><th>死亡</th><th>治愈</th></tr></thead><tbody>{{#each data.countries ~}}<tr><td>{{country}}</td><td>{{confirmed}}</td><td>{{recovered}}</td><td>{{deaths}}</td></tr>{{~/each}}</tbody></table>
</body>
</html>

新建view/coronavirus_index.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>疫情目录</title>
</head>
<body><ul>{{#each data ~}}<li><a href="./{{this}}">{{this}}</a></li>{{~/each}}</ul>
</body>
</html>

然后分别修改我们的路由中间件,确保每次请求都是返回渲染好的html

import { fileURLToPath } from 'url';
import { dirname, resolve, join, parse } from 'path';
import fs from 'fs';
import handlebars from 'handlebars';// ...const __dirname = dirname(fileURLToPath(import.meta.url));// ...// 获取所有有疫情记录的日期
app.use(router.get('/coronavirus/index', async ({route, res}, next) => {// 获取文件路径const filePath = resolve(__dirname, 'view/coronavirus_index.html');// 获取模板文件const tpl = fs.readFileSync(filePath, {encoding: 'utf-8'});// 编译模板const template = handlebars.compile(tpl);// 获取数据const index = getCoronavirusKeyIndex();// 将数据与模板结合const result = template({data: index});res.setHeader('Content-Type', 'text/html');res.body = result;await next();
}));// 获取当前日期对应的疫情数据
app.use(router.get('/coronavirus/:date', async ({route, res}, next) => {// 获取文件路径const filePath = resolve(__dirname, 'view/coronavirus_date.html');// 获取模板文件const tpl = fs.readFileSync(filePath, {encoding: 'utf-8'});// 编译模板const template = handlebars.compile(tpl);const data = getCoronavirusByDate(route.date);// 将数据与模板结合const result = template({data});res.setHeader('Content-Type', 'text/html');res.body = result;await next();
}));//...

持久化存储

终于到了链接数据库的时候~该书中用的是SQLite (为啥不是MySQL或者MonogoDB啥的… Orz…)

万变不离其宗,MySQLMonogoDB在Node的使用很早前接触过了,所以这一块笔记就不做了~

Cookie

在《session和token的登录机制》一文中提到了session的实现原理,就是借助了Cookie。所以Cookie的作用就不写了,直接看看node如何操作Cookie~

在返回的页面中,设置Cookie

app.use(router.get('/', async ({route, res}, next) => {res.setHeader('Content-Type', 'text/html;charset=utf-8');res.setHeader('Set-Cookie', 'mycookie=foobar');res.body = '<h1>你好!</h1>';await next();
}));

Cookie是有时效性的,不添加的化,关闭浏览器就会消失,这里给它添加一个过期时间:

res.setHeader('Set-Cookie', `'mycookie=foobar; Max-Age=86400`);

每次浏览器向服务器发送请求的时候,会自动判断这个 Cookie 是否超过了 expires 的时间:如果超时了,则请求中就不带有 Cookie 字段;如果没有超时,则将这个 Cookie 带上。

在这个例子里,由于每次请求时,服务器都会返回一个新的 Max-Age 等于一天的 Cookie,所以只要你每天都访问这个网页,这个 Cookie 就不失效。如果你隔 24 小时再访问这个网页,那这个 Cookie 也就超时失效了。

关于Cookie的规则设置,还有其他类型:

  • Path:表示 Cookie 只在指定的 URL 请求中有效;
// 假设现在拦截的路由是/foo/bar// 正确
res.setHeader('Set-Cookie', `interceptor_js=${id}; Path=/`);
res.setHeader('Set-Cookie', `interceptor_js=${id}; Path=/foo`);
res.setHeader('Set-Cookie', `interceptor_js=${id}; Path=/bar`);// 错误:因为/abc不在当前请求路径内
res.setHeader('Set-Cookie', `interceptor_js=${id}; Path=/abc`);
  • Domain:表示 Cookie 在设置的 Domain 和它的子域名下都有效;
// 若当前域名是study.junyux.com// 正确
res.setHeader('Set-Cookie', `interceptor_js=${id}; Domain=study.junyux.com`);
res.setHeader('Set-Cookie', `interceptor_js=${id}; Domain=junyux.com`);// 无效
res.setHeader('Set-Cookie', `interceptor_js=${id}; Domain=dev.study.junyux.com`);
res.setHeader('Set-Cookie', `interceptor_js=${id}; Domain=test.junyux.com`);
  • Secure:表示 Cookie 只有使用 HTTPS/SSL 请求时有效;
  • SameSite:可以用来限制第三方发来的 Cookie
    • Strict 表示严格,完全禁止了第三方网站向我们的服务器发送我们网站的 Cookie,缺点就是从第三方跳转到该网站得一直登录;
    • Lax 只允许第三方网站通过 GET 请求跳转到我们的服务器,并带上我们网站的 Cookie;
    • None 就表示没有限制。
  • HttpOnly:若为true那在页面上,JavaScript 无法通过 document.cookie 获取到该 Cookie,这增加了应用的安全性。

Cookie的读取,我们封装成一个文件~

// aspect/cookie.js
export default async(ctx, next) => {const { req } = ctx;const cookieStr = decodeURIComponent(req.headers.cookie);const cookies = cookieStr.split(/\s*;\s*/);ctx.cookies = {};cookies.forEach((cookie) => {const [key, value] = cookie.split('=');ctx.cookies[key] = value;});await next();
}

可以借助Cookie来创建 Session,这个过程一般发生在用户首次登录或者 Session 过期,或者用户需要再次登录时。创建 Session 的流程一般为:

  1. 用户在客户端提交包含个人信息(如用户名)以及密码的表单;
  2. 服务器获取客户端发来的 Cookie,如果没有,则创建一个新 Cookie
  3. 利用用户的信息和 Cookie,向 Session表新增或更新用户的 Session
  4. Session 创建成功,返回用户信息对象。

Cluster为多进程优化性能

Node.js是单线程非阻塞的,避免了系统分配多线程以及多线程间通信时的开销,高效利用CPU、降低内存的好用。缺点就是无法充分利用现在绝大多数电脑支持的多核 CPU,以及一旦出现错误就会导致服务崩溃。

使用Cluster,可以开启多进程,用主进程管理子进程~

修改lib/server.js,在内部写入多进程的相关代码:

// lib/server.js
import http from 'http';
import cluster from 'cluster';
import os from 'os';
import Interceptor from './interceptor.js';// 获取cpu数目
const cpuNums = os.cpus().length;
class Server{constructor(instances = 0, enableCluster = true) {// 指定启动进程数this.instances = instances || cpuNums;// 是否开启多进程this.enableCluster = enableCluster;const interceptor = new Interceptor();this.server = http.createServer(async (req, res) => {// ...});// ...}// 监听listen(opts, cb = () => {}) {if(typeof opts === 'number') opts = {port: opts};opts.host = opts.host || 'localhost';const instances = this.instances;// 如果是主进程,创建instance个子进程if(this.enableCluster && cluster.isMaster) {for(let i = 0; i < instances; i++) {cluster.fork();}// 主进程监听exit事件,如果发现有某个子进程停止了,那么重新创建一个子进程cluster.on('exit', (worker, code, signal) => {console.log('worker %d died (%s). restarting...',worker.process.pid, signal || code);cluster.fork();});}else {// 如果是子进程// 由于 Cluster 做了处理,监听是由主进程进行,再由主进程将 HTTP 请求分发给每个子进程,// 所以子进程尽管监听端口相同,也并不会造成端口冲突this.worker = cluster.worker;console.log(`Starting up http-server http://${opts.host}:${opts.port}`);this.server.listen(opts, () => cb(this.server));}}// ...
}export default Server;

这时候再次执行index.js的话,会默认采用cup的个数开启N个进程~然后,我们开启两个浏览器窗口分别访问localhost:9090。这里我们可以看到,Cluster 将请求分配到了不同的进程去处理。

Starting up http-server http://localhost:9090
Starting up http-server http://localhost:9090
Starting up http-server http://localhost:9090
Starting up http-server http://localhost:9090
Starting up http-server http://localhost:9090
Starting up http-server http://localhost:9090

接下来要解决的是不同进程间的通讯。

和线程不同,进程是彼此独立的,它们之间并不能通过共享同样的内存而共享数据。

Node.js 提供的process.send方法允许我们在进程间传递消息:

// index.js// 统计访问次数
app.use(async (ctx, next) => {process.send('count');await next();
});

这样我们每次访问http://localhost:9090/都会向进程发送一次消息~

worker.on('message', callback)可以让子进程监听接收到的消息。这样,我们就可以在主进程中监听子进程发送的消息。做法就是在lib/server.js的主进程中,遍历cluster.workers,让每个子进程调用worker.on('message', callback)监听消息。

if(this.enableCluster && cluster.isMaster) { // ...Object.entries(cluster.workers).forEach(([id, worker]) => {worker.on('message', (msg) => {// TODO})})// ...
}

实时热更新服务器

在多进程模型中,我们可以在主进程监听JS文件变化,如果JS文件发生改变,那么可以结束之前的子进程,在开发模式下热更新服务器。

// lib/server.js
import http from 'http';
import cluster from 'cluster';
import os from 'os';
import fs from 'fs';
import Interceptor from './interceptor.js';// 获取cpu数目
const cpuNums = os.cpus().length;
class Server{constructor({ instances = 0, enableCluster = true, mode='production' } = {}) {// 新增mode,可以取值为development或者productionif(mode === 'development') {instances = 1;enableCluster = true;}// ...}// 监听listen(opts, cb = () => {}) {// ...// 在开发模式下监听文件变化,如果变化直接杀死所有子进程并按顺序重新创建一个// 如果是生成模式,则不变,发现有某个子进程停止了,那么重新创建一个子进程if(this.mode === 'development') {fs.watch('.', { recursive: true }, (eventType) => {Object.entries(cluster.workers).forEach(([id, worker]) => {console.log('kill worker %d', id);worker.kill();});cluster.fork();})} else {// 主进程监听exit事件,如果发现有某个子进程停止了,那么重新创建一个子进程cluster.on('exit', (worker, code, signal) => {console.log('worker %d died (%s). restarting...',worker.process.pid, signal || code);cluster.fork();});}}else {// 如果是子进程// 由于 Cluster 做了处理,监听是由主进程进行,再由主进程将 HTTP 请求分发给每个子进程,// 所以子进程尽管监听端口相同,也并不会造成端口冲突this.worker = cluster.worker;console.log(`Starting up http-server http://${opts.host}:${opts.port}`);this.server.listen(opts, () => cb(this.server));}}// ...
}export default Server;

总结

学习了搭建HTTP服务之后中间件的开发、性能的优化、常见的Cookie、数据库、多进程的操作~

参考链接

从前端到全栈


如果错误欢迎指出,感谢阅读~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/12916.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Valve 签约开源 Linux 图形驱动开发者

导读据外媒 phoronix 报道&#xff0c;Valve 最近聘用了著名开源 Linux 图形驱动开发者 Alyssa Rosenzweig&#xff0c;以改进开源 Linux 图形驱动程序堆栈&#xff0c;增强 Linux 游戏生态系统。 Alyssa Rosenzweig 多年来在 Panfrost 开源、逆向工程 Arm Mali 图形驱动程序方…

Zabbix监控软件 Linux外多平台监控【Windows JAVA SNMP】

在之前的博客中&#xff0c;已经介绍了zabbix的安装&#xff0c;配置&#xff0c;以及如何用zabbix监控Linux服务器。这篇博客则介绍zabbix监控的其他几种方式&#xff08;Windows服务器 Java应用 SNMP&#xff09;。 -------------------- Zabbix 监控 Windows 系统 ---------…

[ABC218G] Game on Tree 2 树上游戏

[ABC218G] Game on Tree 2 树上游戏 文章目录 [ABC218G] Game on Tree 2 树上游戏题面翻译输入格式输出格式样例 #1样例输入 #1样例输出 #1 样例 #2样例输入 #2样例输出 #2 样例 #3样例输入 #3样例输出 #3 题目大意分析水法code 正解code 题面翻译 给定一棵树&#xff0c;以及…

Vue 之 mixins 和 provide/inject

一、mixins 1、简介 ​ mixins 又称 混入&#xff0c;是指将一些可复用的代码&#xff08;JS、生命周期钩子函数等等&#xff09;抽离出来&#xff0c;定义成mixins模块&#xff0c;然后混入到多个组件中&#xff0c;从而实现组件间的逻辑代码共享&#xff0c;减少重复代码。…

Web安全——数据库mysql学习

数据库mysql基础 Web安全分享一、数据库的基本操作1、MYSQL登录与退出2、MYSQL数据库的一些解释3、MYSQL注释符有三种&#xff1a; 二、数据库的一些基本操作1、数据库的增删改查(sql语句) 三、table 表的操作1、查看表结构2、查看表的内容3、建立表4、约束条件5、修改表的操作…

macOS Sonoma 14 beta 3 (23A5286g) Boot ISO 原版可引导镜像下载

macOS Sonoma 14 beta 3 (23A5286g) Boot ISO 原版可引导镜像&#xff0c;7 月 5 日&#xff08;北京时间今日凌晨&#xff09;已发布 本站下载的 macOS 软件包&#xff0c;既可以拖拽到 Applications&#xff08;应用程序&#xff09;下直接安装&#xff0c;也可以制作启动 U…

GaussDB云数据库SQL应用系列—分区表管理

目录 前言 一、分区表基本原理 二、分区表主要优势 三、分区表常见场景 四、GaussDB分区表管理&#xff08;示例&#xff09; 示例一&#xff1a;创建范围分区表(RANGE) 示例二&#xff1a;创建哈希分区表&#xff08;HASH&#xff09; 示例三&#xff1a;创建列表分区…

vue3 实现多层级列表

文章目录 需求背景解决效果index.vue视频效果 需求背景 需要在统一个列表下&#xff0c;实现商品和规格得管理和联动 解决效果 index.vue <!--/*** author: liuk* date: 2023/7/7* describe: 商品列表 */--> <template><div class"container">&…

Java类加载深度剖析-大白话

Java类加载深度剖析 1.类加载的入口2.AppClassLoader、ExtClassLoader、BootstrapClassLoader的血脉渊源3.ExtClassLoader究竟是不是孙大圣4.为什么自定义类加载器的父类加载器是AppClassLoader呢&#xff1f;5.我们应该如何打破双亲委派机制呢&#xff1f;6.如何保证同class对…

Mac VSCode配置运行单个C++文件

题外话&#xff1a;VSCode一键整理代码快捷键&#xff1a;ShiftoptionF 方法一&#xff1a;命令行直接编译 g -o 想创建的可执行文件名 ./cpp文件名 ./可执行文件名 以test.cpp为例&#xff0c;我创建的可执行文件名为test&#xff0c;运行结果如下&#xff1a; 方法二&#…

【裸机开发】GPT 定时器(一) —— GPT的功能、寄存器解析

后续需要使用 GPT 计数器实现中断以及延时&#xff0c;这里我们需要先了解一下GPT的功能以及相关寄存器。 目录 一、GPT 定时器的功能 1、计数器 2、输入捕获 3、输出比较&#xff08;GPT的两种工作模式&#xff09; 二、寄存器解析 1、GPTx_CR 2、GPTx_PR 3、GPTx_SR …

3.1.cuda运行API-概述

目录 前言1. Runtime API概述总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习精简 CUDA 教程-Runtime API 概述 课程大纲可…