Nodejs

又一个比Nodejs快的运行时,它叫LLRT

又一个比Nodejs快的运行时,它叫LLRT

月盾

前端圈的发展一向让人感觉学不动,前有Deno,后有Bun,这不,最近又出了个比Nodejs快的Runtime,它叫LLRT。

node deno bun对比

1. 什么是LLRT

LLRT是Low Latency Runtime的缩写,是一种轻量级JavaScript运行时,旨在满足对快速高效的无服务器应用程序日益增长的需求。与在AWS Lambda上运行的其他JavaScript运行时相比,LLRT的启动速度提高了10倍以上,总体成本降低了2倍。

2. LLRT是如何做到比Nodejs快的

它内置在Rust中,利用QuickJS作为JavaScript引擎,确保高效的内存使用和快速启动。

3. LLRT的发展

它还处于实验阶段,并不稳定,还不推荐用于生产。

事件驱动编程、消息驱动编程、数据驱动编程

月盾

事件驱动

事件驱动机制就是:
让驴拉磨,它不拉,你用鞭抽一下,它就开始拉了。然后又停了,你再抽一下,它又继续拉了
这叫用“鞭”驱动“驴”拉磨

在程序里,程序停止在那不动,你点击一个按钮,它就有反应了,过一会,又没反应了,你再点一下,它又继续运行。
这叫用“事件”驱动“程序”运行

0. 基本概念

窗口/组件

事件

消息(队列)

事件响应(服务处理程序)

调度算法

进程/线程

非阻塞I/O

程序的执行可以看成对CPU,内存,IO资源一次占用

现代操作系统支持多任务,可以分时复用上述资源.

1. 为什么采用事件驱动模型?

事件驱动模型也就是我们常说的观察者,或者发布-订阅模型;理解它的几个关键点:

首先是一种对象间的一对多的关系;最简单的如交通信号灯,信号灯是目标(一方),行人注视着信号灯(多方);

当目标发送改变(发布),观察者(订阅者)就可以接收到改变;

观察者如何处理(如行人如何走,是快走/慢走/不走,目标不会管的),目标无需干涉;所以就松散耦合了它们之间的关系。

2. 代码执行流程

在传统的或“过程化”的应用程序中,应用程序自身控制了执行哪一部分代码和按何种顺序执行代码。从第一行代码执行程序并按应用程序中预定的路径执行,必要时调用过程。 在事件驱动的应用程序中,代码不是按照预定的路径执行-而是在响应不同的事件时执行不同的代码片段。事件可以由用户操作触发、也可以由来自操作系统或其它应用程序调度算法的消息触发、甚至由应用程序本身的消息触发。这些事件的顺序决定了代码执行的顺序,因此应用程序每次运行时所经过的代码的路径都是不同的。

3. 事件驱动模型

在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢?

方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:

CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?

如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;

如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;所以,该方式是非常不好的。

方式二:就是事件驱动模型目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:

有一个事件(消息)队列;

鼠标按下时,往这个队列中增加一个点击事件(消息);

有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;

事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;如图:

4. 事件驱动处理库

select

poll

epoll

libev

消息驱动

事件驱动机制跟消息驱动机制相比

消息驱动和事件驱动很类似,都是先有一个事件,然后产生一个相应的消息,再把消息放入消息队列,由需要的项目获取。他们的区别是消息是谁产生的

消息驱动:鼠标管自己点击不需要和系统有过多的交互,消息由系统(第三方)循环检测,来捕获并放入消息队列。消息对于点击事件来说是被动产生的,高内聚。

事件驱动:鼠标点击产生点击事件后要向系统发送消息“我点击了”的消息,消息是主动产生的。再发送到消息队列中。

事件:按下鼠标,按下键盘,按下游戏手柄,将U盘插入USB接口,都将产生事件。比如说按下鼠标左键,将产生鼠标左键被按下的事件。

消息:当鼠标被按下,产生了鼠标按下事件,windows侦测到这一事件的发生,随即发出鼠标被按下的消息到消息队列中,这消息附带了一系列相关的事件信息,比如鼠标哪个键被按了,在哪个窗口被按的,按下点的坐标是多少?如此等等。

  1. 要理解事件驱动和程序,就需要与非事件驱动的程序进行比较。实际上,现代的程序大多是事件驱动的,比如多线程的程序,肯定是事件驱动的。早期则存在许多非事件驱动的程序,这样的程序,在需要等待某个条件触发时,会不断地检查这个条件,直到条件满足,这是很浪费cpu时间的。而事件驱动的程序,则有机会释放cpu从而进入睡眠态(注意是有机会,当然程序也可自行决定不释放cpu),当事件触发时被操作系统唤醒,这样就能更加有效地使用cpu.
  2. 再说什么是事件驱动的程序。一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在,这个死循环包括两个部分,第一个部分是按照一定的条件接收并选择一个要处理的事件,第二个部分就是事件的处理过程。程序的执行过程就是选择事件和处理事件,而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,从而释放cpu。
  3. 事件驱动的程序,必定会直接或者间接拥有一个事件队列,用于存储未能及时处理的事件。
  4. 事件驱动的程序的行为,完全受外部输入的事件控制,所以,事件驱动的系统中,存在大量这种程序,并以事件作为主要的通信方式。
  5. 事件驱动的程序,还有一个最大的好处,就是可以按照一定的顺序处理队列中的事件,而这个顺序则是由事件的触发顺序决定的,这一特性往往被用于保证某些过程的原子化。
  6. 目前windows,linux,nucleus,vxworks都是事件驱动的,只有一些单片机可能是非事件驱动的。

事件模式耦合高,同模块内好用;消息模式耦合低,跨模块好用。事件模式集成其它语言比较繁琐,消息模式集成其他语言比较轻松。事件是侵入式设计,霸占你的主循环;消息是非侵入式设计,将主循环该怎样设计的自由留给用户。如果你在设计一个东西举棋不定,那么你可以参考win32的GetMessage,本身就是一个藕合度极低的接口,又足够自由,接口任何语言都很方便,具体应用场景再在其基础上封装成事件并不是难事,接口耦合较低,即便哪天事件框架调整,修改外层即可,不会伤经动骨。而如果直接实现成事件,那就完全反过来了。

什么是数据驱动编程

最近在学习《Unix编程艺术》。以前粗略的翻过,以为是介绍unix工具的。现在认真的看了下,原来是介绍设计原则的。它的核心就是第一章介绍的unix的哲学以及17个设计原则,而后面的内容就是围绕它来展开的。以前说过,要学习适合自己的资料,而判断是否适合的一个方法就是看你是否能够读得下去。我对这本书有一种相见恨晚的感觉。推荐有4~6年工作经验的朋友可以读一下。

正题

作者在介绍Unix设计原则时,其中有一条为“表示原则:把知识叠入数据以求逻辑质朴而健壮”。结合之前自己的一些经验,我对这个原则很有共鸣,所以先学习了数据驱动编程相关的内容,这里和大家分享出来和大家一起讨论。

数据驱动编程的核心

数据驱动编程的核心出发点是相对于程序逻辑,人类更擅长于处理数据。数据比程序逻辑更容易驾驭,所以我们应该尽可能的将设计的复杂度从程序代码转移至数据。

nestjs中使用携程Apollo配置中心

月盾

nest框架官方文档中使用的是本地文件配置,也就是@nestjs/config包。本地配置文件的好处是使用简单,但是对于一些更新较快的项目,难免会增加配置数据,曾经吃过不少配置文件的亏,在发布的时候很容易因为缺少配置文件直接把服务发挂了,或者需要在服务器上修改配置很容易修改错误导致服务发布失败。

集中的配置中心可以解决上面问题,本文以apollo配置中心为例来说明。 在使用的过程中需要注意以下问题:从配置中心获取数据库连接信息,再去连接会连接失败,因为在连接的时候还没有获取到配置信息。 先看代码再解释。

// main.ts
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter, NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { MyLogger } from './libs/mylog.service';
import { join } from 'path';
const Apollo = require('node-apollo');
const dotenv = require('dotenv');

async function bootstrap() {
    try {
        const root = join(__dirname, '../');
        let envFile = join(root, '.env')
        dotenv.config({ "path": envFile })
        const {
            APOLLO_APPID,
            APOLLO_ENV,
            APOLLO_HOST,
            APOLLO_NAMESPACE,
            APOLLO_PORT,
            APOLLO_TOKEN,
            APOLLO_ClUSTER
        } = process.env;
        let apolloEnv = {
            configServerUrl: `http://${APOLLO_HOST}:${APOLLO_PORT}`,
            appId: `${APOLLO_APPID}`,
            clusterName: `${APOLLO_ClUSTER}`,
            apolloEnv: `${APOLLO_ENV}`,
            token: `${APOLLO_TOKEN}`,
            namespaceName: [`${APOLLO_NAMESPACE}`]
        };
        // 获取到的配置信息
        let zmConf = await Apollo.remoteConfigService(apolloEnv);
        console.log(">>>>>>>main.config", zmConf);
        process.env = Object.assign(process.env, zmConf);
    } catch (err) {
        console.log(`获取环境变量异常:${err}`)
    }

    const app = await NestFactory.create<NestExpressApplication>(AppModule, new ExpressAdapter());
    app.useLogger(app.get(MyLogger));

    await app.listen(3434, () => {
        const logger = new MyLogger('main.ts');
        logger.debug(process.env.NODE_ENV, 'main.ts');
        logger.log('server start on http://localhost:3434');
    });
}
bootstrap();
// app.module.ts
@Module({
    imports: [
        // MongooseModule.forRoot(`mongodb://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DATABASE}`),
        MongooseModule.forRootAsync({
            useFactory: () => ({
                uri: `mongodb://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DATABASE}`
            }),
        }),
        //  Nest can't resolve dependencies of the AppService (?). Please make sure that the argument ArticleService at index [0] is available in the AppModule context.
        // 在article.service中exports:[ArticleService]
        ArticleModule,
        LoggerModule,
        // HttpModule,
    ], // 导入模块所需的导入模块列表
    controllers: [AppController], // 必须创建的一组控制器
    providers: [AppService, MyLogger], // 由 Nest 注入器实例化的提供者,并且可以在整个模块中共享
})
export class AppModule implements NestModule {
    // 中间件模块在此处添加,可以给某一部分增加中间件,如果要全局增加则在main.ts中使用app.use添加
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply()
            // .with('中间件参数')
            .forRoutes('/*');
    }
}

使用MongooseModule.forRoot连接数据库肯定是不行的,需要改成异步的:

使用pm2一键部署多个服务

月盾
  1. pm2支持远程部署服务,创建文件ecosystem.json,内容形式如:
{
  // Applications part
  "apps" : [{
    "name"      : "API",
    "script"    : "app.js",
    "env": {
      "COMMON_VARIABLE": "true"
    },
    // Environment variables injected when starting with --env production
    // http://pm2.keymetrics.io/docs/usage/application-declaration/#switching-to-different-environments
    "env_production" : {
      "NODE_ENV": "production"
    }
  },{
    "name"      : "WEB",
    "script"    : "web.js"
  }],
  // 部署部分
  // Here you describe each environment
  "deploy" : {
    "production" : {
      "user" : "node",
      // 多主机配置
      "host" : ["212.83.163.1", "212.83.163.2", "212.83.163.3"],
      // 服务使用的分支
      "ref"  : "origin/master",
      // Git 仓库地址
      "repo" : "git@github.com:repo.git",
      // 项目目录
      "path" : "/var/www/production",
      // Can be used to give options in the format used in the configura-
      // tion file.  This is useful for specifying options for which there
      // is no separate command-line flag, see 'man ssh'
      // can be either a single string or an array of strings
      "ssh_options": "StrictHostKeyChecking=no",
      // To prepare the host by installing required software (eg: git)
      // even before the setup process starts
      // 可以使用";"分割多个命令
      // or path to a script on your local machine
      "pre-setup" : "npm install",
      // Commands / path to a script on the host machine
      // This will be executed on the host after cloning the repository
      // eg: placing configurations in the shared dir etc
      "post-setup": "ls -la",
      // Commands to execute locally (on the same machine you deploy things)
      // Can be multiple commands separated by the character ";"
      "pre-deploy-local" : "echo 'This is a local executed command'"
      // Commands to be executed on the server after the repo has been cloned
      "post-deploy" : "npm install && pm2 startOrRestart ecosystem.json --env production"
      // Environment variables that must be injected in all applications on this env
      "env"  : {
        "NODE_ENV": "production"
      }
    },
    "staging" : {
      "user" : "node",
      "host" : "212.83.163.1",
      "ref"  : "origin/master",
      "repo" : "git@github.com:repo.git",
      "path" : "/var/www/development",
      "ssh_options": ["StrictHostKeyChecking=no", "PasswordAuthentication=no"],
      "post-deploy" : "pm2 startOrRestart ecosystem.json --env dev",
      "env"  : {
        "NODE_ENV": "staging"
      }
    }
  }
}

Edit the file according to your needs.

一次商业web网站搭建的取舍过程

月盾

最近为公司官网重构搭建项目,把遇到的问题总结一下。此处的“商业”并没有多神秘,说的有点夸张而已,不过是为了区分公司项目与个人项目罢了。在这之前,我自己搭建过的网站也不下于10个,其中有个人网站也有公司网站,那时候搭建的网站也能上线运行,也没有过多的条件限制,所以不会有什么纠结的地方。

所以搭建一个网站并不复杂,复杂的是让其满足很多要求。有业务需求,有领导喜好,有同事对技术的接受度。业务说网站要支持SEO,支持IE浏览器,领导说我们要前后端分离,同事说我想使用主流新技术。最后经过几轮商讨下来自然是业务第一,领导第二,同事第三的优先级进行选择了。

要支持SEO和IE浏览器,只能是服务端渲染,可选的技术就只有SSR和模板引擎。SSR能选择的也就同事熟悉的Vue技术栈nuxtjs,但是其只能首屏渲染,并不能完全满足整站的SEO需求,而且开发体验并不怎么好,编译时间长,调试难,占用内存高等缺点。最后只能选择一把刷技术jQuery和node模板引擎。

选择了jQuery和模板引擎还不够,前端的模块化怎么做?目前前端模块化方案还是少的可怜,但并不是没有好的方案,只是在技术潮流下显得有点暗淡失色。比如requirejs,seajs,fis等都可以做模块化,~~最后在内心斗争一番后选择了layui自带的模块化(主要是使用了这一套UI)。layui本身是一套UI框架,为了尽量减少引入第三方js就直接使用其提供的模块化。~~原网站使用的是fis3也很好,但是如果继续使用的话等于又回去了,而且fis3也不再维护了。好在改造难度不大,只需要重新包装一下即可,其实fis3最后生成的代码也是类似于AMD/CMD规范。

技术方案确定后就剩开发环境工程问题了,由于一些老项目的缘故,前端同事都习惯了使用less开发css,也需要引入。然后是公司项目不同于个人项目具有服务器完全管理权限,通常使用NGINX代理,这样会对前端文件进行缓存,而网站发布频率较大,前端文件变化了还有缓存,所以又需要对前端文件进行哈希处理,这样就有了编译过程,同时还有node服务需要同时运行,所以使用了gulp工作流。后续补充:layui在开发过程中没啥问题,但是要上线时对静态文件哈希处理不好,最终也放弃了,回归了最原始的开发方式。

这样一顿操作下来也耗时一周才完成,远比搭建个人项目一天内费事多了。

linux安装nodejs——快捷版

月盾

前面写过一篇linux下安装nodejs,不过这种方式安装有弊端,首先就是安装过程复杂漫长,容易出错,且不易升级。这次展示的是简单易操作的方式。

wget -qO- https://raw.github.com/creationix/nvm/master/install.sh | sh
nvm install 10
nvm use 10

好了,只需3行命令即可,其原理是先安装nodejs版本管理工具nvm,通过nvm来安装和管理nodejs,这样就一举两得,既安装了nodejs,还可以方便升级,至于nvm的使用方法大家使用nvm -h即可查看,不再细说。

另外,如果对nodejs有深度使用的话,建议直接安装alinode,方便以后做性能监控,安装方法和上面一样简单。

wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash
source ~/.bashrc
# tnvm ls-remote alinode 查看需要的版本
tnvm install alinode-v3.11.4 # 安装需要的版本
tnvm use alinode-v3.11.4 # 使用需要的版本

如果后面有性能监控需要,可以查看官网帮助文件进行下一步操作。 alinode

nestjs框架中使用nunjucks模板引擎

月盾

main.ts

import { NestFactory } from '@nestjs/core';
import {
    ExpressAdapter,
    NestExpressApplication,
} from '@nestjs/platform-express';
import { AppModule } from './app.module';
import nunjucks = require('nunjucks');
import { join } from 'path';

async function bootstrap() {
    const app = await NestFactory.create<NestExpressApplication>(
        AppModule,
        new ExpressAdapter(),
    );
    app.useStaticAssets(join(__dirname, '..', 'public')); // NestFactory.create需要加泛型参数<NestExpressApplication>
    app.setBaseViewsDir(join(__dirname, '..', 'views')); // 修改模板文件后立马生效,否则需要重启服务,nunjucks watch参数也有相同作用
    nunjucks.configure('views', {
        ext:'njk',
        autoescape: true,
        express: app,
        watch: true,
    });
    await app.listen(3000, () => {
    });
}
bootstrap();

app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}

    @Get('articles')
    // @Render('articles.njk') // 不能使用@Render装饰器,而是使用res.render
    async findArticlesByUser(@Res() res:Response): Promise<any> {
        return res.render('articles.njk', {
            title: "标题",
            articles
        })
    }
}

articles.njk

nodejs-go内存占比

月盾

同一台服务器上部署了两个功能差不多的服务,但是内存占比差距有点大。 nodejs-go内存占比 go占14.7M nodejs占122.2M

pm2设置NODE_ENV环境变量

月盾

nodejs中经常使用到环境变量,最常见的如:process.env.NODE_ENV。那么在生产环境中使用pm2如何设置环境变量?

设置方式一:shell命令设置

linuxexport NODE_ENV=development&& node app.js

winset NODE_ENV=development&& node app.js

一般是作临时变量在系统启动时设置,不影响其他系统,也可同时运行开发环境和生产环境,只需要根据process.env.NODE_ENV来运行不同逻辑即可。

设置方式二:配置文件设置

要在pm2设置环境变量也很简单。

pm2 start pm2.json –env production

--env production参数是为了设置环境变量,由pm2.json中的配置决定设置什么样的环境变量。

pm2.json

{
  "apps" : [{
    "name": "issue",
    "cwd": "dest",
    "script"    : "bin/www.js",
    "instances" : "2",
    "exec_mode" : "cluster",
    "env": {
      "NODE_ENV": "development",
      "PORT": 3002
    },
    "env_production" : {
       "NODE_ENV": "production",
       "PORT": 3003
    },
    "log_date_format": "YYYY-MM-DD_HH:mm Z",
    "merge_logs": true
  }]
}

如果不加参数则默认使用

"env": {
    "NODE_ENV": "development",
    "PORT": 3002
}

结果:NODE_ENV=development,PORT=3002

--env production则使用的是

"env_production" : {
    "NODE_ENV": "production",
    "PORT": 3003
}

结果:NODE_ENV=production,PORT=3003