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

这篇具有很好参考价值的文章主要介绍了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', // 缓存一天
});

Node.js知识梳理(二)——进阶,学习记录,node.js

再次访问:

Node.js知识梳理(二)——进阶,学习记录,node.js

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
    }

Node.js知识梳理(二)——进阶,学习记录,node.js

协商缓存不止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());
});

Node.js知识梳理(二)——进阶,学习记录,node.js

拦截器

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

Node.js知识梳理(二)——进阶,学习记录,node.js

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

// 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、数据库、多进程的操作~

参考链接

从前端到全栈


如果错误欢迎指出,感谢阅读~文章来源地址https://www.toymoban.com/news/detail-518707.html

到了这里,关于Node.js知识梳理(二)——进阶的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Node.js详解(一):基础知识

    Node.js是一个javascript运行环境。它让javascript可以开发后端程序,实现几乎其他后端语言实现的所有功能,可以与PHP、Java、Python、.NET、Ruby等后端语言平起平坐。 Node.js是基于V8引擎,V8是Google发布的开源JavaScript引擎,本身就是用于Chrome浏览器的js解释部分,但是Node.js 之父 Rya

    2024年02月07日
    浏览(42)
  • Node.js基础知识点(三)

    一、fs 浏览器中的Javascript是没有文件操作的能力的,但是Node中的Javascript具有文件操作的能力 fs是 file-system的简写,就是文件系统的意思,在Node中如果想要进行文件操作,就必须引入 fs 这个核心模块,在 fs 中就提供了所有的文件操作相关的 API 例如: fs.readFile 就是用来读取

    2024年01月25日
    浏览(39)
  • Node.js基础知识点(四)

    本节介绍一下最简单的http服务 一.http 可以使用Node 非常轻松的构建一个web服务器,在 Node 中专门提供了一个核心模块:http http 这个模块的就可以帮你创建编写服务器。 1. 加载 http 核心模块 2. 使用 http.createServer() 方法创建一个Web 服务器 返回的是一个 Server 实例: 3.服务器要干

    2024年01月17日
    浏览(48)
  • node.js知识系列(2)-每天了解一点

    👍 点赞,你的认可是我创作的动力! ⭐️ 收藏,你的青睐是我努力的方向! ✏️ 评论,你的意见是我进步的财富! 在 Node.js 中,您可以使用 child_process 模块来执行子进程。这允许您在 Node.js 应用程序中启动外部命令或脚本,与它们进行交互并获取输出。 以下是一个简单

    2024年02月07日
    浏览(41)
  • node.js知识系列(1)-每天了解一点

    👍 点赞,你的认可是我创作的动力! ⭐️ 收藏,你的青睐是我努力的方向! ✏️ 评论,你的意见是我进步的财富! Node.js 是一个基于 Chrome V8 JavaScript 引擎的服务器端运行环境,它允许您使用 JavaScript 编写服务器端应用程序。Node.js 的主要特点包括: 非阻塞、事件驱动 :

    2024年02月07日
    浏览(44)
  • 【WEB前端进阶之路】 HTML 全路线学习知识点梳理(中)

    本文是HTML零基础学习系列的第二篇文章,点此阅读 上一篇文章。 标题是通过 h1 - h6 标签进行定义的。 h1 定义最大的标题。 h6 定义最小的标题。浏览器会自动地在标题的前后添加空行,例如: 标题用来正确的显示文章结构 ,通过不同的标题可以为文章建立索引,所以,标题

    2024年02月02日
    浏览(44)
  • Vue项目启动过程全记录(node.js运行环境搭建)

    1、安装node.js 从Node.js官网下载安装包并安装。然后在安装后的目录(如果是下载的压缩文件,则是解压缩的目录)下新建node_global和node_cache这两个文件夹。 node_global:npm全局安装位置 node_cache:npm缓存路径 2、配置环境变量 在系统变量里添加一个变量NODE_HOME,值为node.js的安装

    2024年02月19日
    浏览(44)
  • 【Node.JS】初入前端,学习node.js基本操作

    NPM是随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问题,常见的使用场景有以下几种: npm可以分为全局安装和本地安装 Node所有API都支持回调函数,回调函数一般作为API的最后一个参数出现 阻塞代码实例 非阻塞代码示例 语法分析 具体示例 事件监听器就是

    2023年04月25日
    浏览(42)
  • 【Node.js实战】一文带你开发博客项目之登录(前置知识)

    个人简介 👀 个人主页: 前端杂货铺 🙋‍♂️ 学习方向: 主攻前端方向,也会涉及到服务端 📃 个人状态: 在校大学生一枚,已拿多个前端 offer(秋招) 🚀 未来打算: 为中国的工业软件事业效力n年 🥇 推荐学习:🍍前端面试宝典 🍉Vue2 🍋Vue3 🍓Vue2Vue3项目实战 🥝

    2024年02月21日
    浏览(48)
  • Node.js学习

     Node.js Node.js简介 Node.js开发环境搭建 使用vscode开发Node.js应用 Node.js核心模块 Node.js fs 模块 (File System) Node.js http 模块 Node.js https 模块 Node.js url 模块 Node.js querystring 模块 Node.js path 模块 Node.js events 模块 Node.js util 模块 (Utilities) Node.js os 模块 (Operating System) Node.js stream模块 Node.js

    2024年02月02日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包