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

news2024/11/25 15:04:01

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

学习的时候用的是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相等,说明文件内容没有修改,返回响应状态码 304
      status = 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.html
      filePath = 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相等,说明文件内容没有修改,返回响应状态码 304
      status = 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-line
      return 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或者production
    if(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.coloradmin.cn/o/718146.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分解整体:将整体式架构转换为微服务的容器化之旅

传统上&#xff0c;软件应用程序是使用单体架构开发的&#xff0c;其中所有应用程序组件都紧密交织在一起&#xff0c;并作为一个单元进行部署。随着软件应用变得更为复杂&#xff0c;组织开始依赖分布式系统&#xff0c;单体架构的限制开始变得更加明显。容器化被引入作为解决…

关于连接数据库时只能使用localhost而不能使用IP连接的问题解决办法

出现&#xff1a;java.sql.SQLException: null, message from server: "Host DESKTOP-H84MBU3 is not allowed to connect to this MySQL server"问题 解决办法&#xff1a; 1、打开数据库操作命令窗口 2、修改user表中 "root" 所对应的 "host&q…

JMeter之IP欺骗技术(模拟不同的IP地址并发请求)

目录 前言&#xff1a; 第一步&#xff1a;在负载机上绑定IP地址 第二步&#xff1a;点击高级&#xff0c;添加伪造的IP地址 第三步&#xff1a;新增IP地址复制到文本 第四步&#xff1a;新建参数化请求 第五步&#xff1a;新建压力测试脚本 第六步&#xff1a;配置线程…

【Mysql】事务原理与优化最佳实践(四)

前言 我们的数据库一般都会并发执行多个事务&#xff0c;多个事务可能会并发的对相同的一批数据进行增 删改查操作&#xff0c;可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。 这些问题的本质都是数据库的多事务并发问题&#xff0c;为了解决多事务并发问题&…

【C】函数

目录 【1】函数是什么 【2】C语言中函数的分类 【2.1】库函数 【2.2】如何学会使用库函数 【2.3】自定义函数 【3】函数的参数 【3.1】实际参数&#xff08;实参&#xff09; 【3.2】形式参数&#xff08;形参) 【4】函数的调用 【4.1】传值调用 【4.2】传址调用 【…

kubernetes环境搭建及部署

一、kubernetes 概述 1、kubernetes 基本介绍 kubernetes&#xff0c;简称 K8s&#xff0c;是用 8 代替 8 个字符“ubernete”而成的缩写。是一个开源 的&#xff0c;用于管理云平台中多个主机上的容器化的应用&#xff0c;Kubernetes 的目标是让部署容器化的 应用简单并且高效…

【Python】文件

文章目录 一. 什么是文件二. 文件路径三. 打开文件四. 关闭文件五. 写文件1. 清空写2. 追加写 六. 读文件1. 使用 read 方法读取2. 使用 for 循环按行读取3. 使用 readlines 方法读取整个文件的内容 七. 上下文管理器1. 什么是上下文管理器&#xff1f;2. 为什么要有上下文管理…

ESP8266模块MQTT AT Commands 发送 json字符串

ESP8266 wifi模块介绍 ESP8266EX 由乐鑫公司开发&#xff0c;提供了⼀套⾼度集成的 Wi-Fi SoC 解决⽅案&#xff0c;其低功耗、紧凑设计和⾼稳定性可以满⾜⽤户的需求。ESP8266EX 拥有完整的且⾃成体系的 Wi-Fi ⽹络功能&#xff0c;既能够独⽴应⽤&#xff0c;也可以作为从机…

ORA-31664: unable to construct unique job name when defaulted

某个环境备份不足空间问题处理后&#xff0c;手动执行expdp备份的脚本&#xff0c;报错如下 Export: Release 11.2.0.4.0 - Production on Tue Jul 4 11:46:14 2023 Copyright (c) 1982, 2011, Oracle and/or its affiliates. All rights reserved. Connected to: Oracle D…

【海量数据挖掘/数据分析】之 决策树模型(决策树模型、决策树构成、决策树常用算法、决策树性能要求、信息增益、信息增益计算公式、决策树信息增益计算实例)

【海量数据挖掘/数据分析】之 决策树模型&#xff08;决策树模型、决策树构成、决策树常用算法、决策树性能要求、信息增益、信息增益计算公式、决策树信息增益计算实例&#xff09; 目录 【海量数据挖掘/数据分析】之 决策树模型&#xff08;决策树模型、决策树构成、决策树常…

Windows环境部署MySQL_5.7的安装、测试连接以及卸载全过程实操手册

前言&#xff1a; 前面记录了双环境的oracle的安装卸载及相关测试评估&#xff0c;这里记录下 MySQL5.7社区免费版的部署手册。 什么是 MySQL MySQL 是一个关系型数据库管理系统&#xff0c;由瑞典 MySQL AB 公司开发&#xff0c;目前属于Oracle 公司。MySQL 是一种关系型数…

window.open()实现PDF预览

效果图如下&#xff1a; 页面使用: window.open(strUrl) 参数说明如下图:

深度学习笔记之Transformer(四)铺垫:LayerNormalization

深度学习笔记之Transformer——LayerNormalization 引言回顾&#xff1a;批标准化问题描述问题处理 层标准化批标准化无法处理的问题 引言 在介绍 Transformer \text{Transformer} Transformer模型架构之前&#xff0c;首先介绍 Transformer \text{Transformer} Transformer的…

linux 环境下ElasticSearch 7.1.6.3、kibana-7.16.3安装

安装包放入百度网盘&#xff0c;自取 链接&#xff1a;https://pan.baidu.com/s/1Uh9pKFoaz9qi4CE4_GmlOA?pwdroky 提取码&#xff1a;roky 快速演示下安装操作&#xff1a; ElasticSearch 7.1.6.3&#xff1a; 1.上传至服务器相应目录下 我传到的/home目录下&#xff0…

0、技术选型

技术选型参考&#xff1a; 系统数据流程图 框架发行版本选型 1&#xff09;如何选择Apache/CDH/HDP版本&#xff1f; &#xff08;1&#xff09;Apache&#xff1a;运维麻烦&#xff0c;组件间兼容性需要自己调研。&#xff08;一般大厂使用&#xff0c;技术实力雄厚&#xf…

【随笔】jupyter notebook启动打开文件出现:500 : Internal Server Error

错误&#xff1a; 看log有 module ‘mistune’ has no attribute BlockGrammar’错误 打开文件有500 : Internal Server Error 解决方法&#xff1a; pip install --upgrade jupyterhub pip install --upgrade --user nbconvertconda环境可以&#xff1a; conda install nbc…

使用Python开发ChatGPT AI工具助手(ChatCatsy):4天快速开发指南

引言&#xff1a; ChatGPT是一种基于人工智能的对话生成模型&#xff0c;它可以用于开发聊天机器人、虚拟助手等应用。本教程将指导您在3天内使用Python开发一个ChatGPT AI工具助手。无需担心&#xff0c;即使您是初学者&#xff0c;也可以轻松跟随本指南完成项目。 第一天&a…

Java-数据结构(三)-List:ArrayList和LinkedList及其相关面试题

目录 一、引言二、ArrayList2.1 ArrayList是什么&#xff1f;2.2 ArrayList的历史由来2.3 ArrayList的使用好处2.4 ArrayList的底层原理2.5 ArrayList的操作方法及代码示例 三、LinkedList3.1 LinkedList是什么&#xff1f;3.2 LinkedList的历史由来3.3 LinkedList的使用好处3.…

攻防世界_web

robots 题目描述是这样的&#xff0c;虽然这是一道基础题&#xff0c;但我确实还没有了解过robots协议 第一次知道是被御剑给扫描出来的后台文件 这次直接访问看看 初级题就是初级题&#xff0c;访问后得到提示&#xff0c;如果没做过我估计还不知道该咋整&#xff0c;这也是一…

Squid 缓存代理(一)---原理及搭建(传统代理、透明代理)

前言 Squid 是 Linux 系 统 中 最 常 用 的 一 款 开 源 代 理 服 务 软 件 &#xff08; 官 方 网 站 为 http://www.squid-cache.org&#xff09;&#xff0c;可以很好地实现 HTTP 和 FTP&#xff0c;以及 DNS 查询、SSL 等应用的缓存代理。缓存代理作为应用层的代理服务软件…