Egg.js
https://eggjs.org/zh-cn/intro/
阿里开源的Node.js框架egg.js:为企业级框架和应用而生!
简介
阿里Egg.js 为企业级框架和应用而生
https://www.toutiao.com/a6491178538987684365/
官方文档网址:https://eggjs.org
特性
- 提供基于 Egg 定制上层框架的能力
- 高度可扩展的插件机制
- 内置多进程管理
- 基于 Koa 开发,性能优异
- 框架稳定,测试覆盖率高
- 渐进式开发
- 设计原则
Egg 的插件机制有很高的可扩展性,一个插件只做一件事(比如 Nunjucks 模板封装成了 egg-view-nunjucks、MySQL 数据库封装成了 egg-mysql)
Egg 奉行『约定优于配置』,按照一套统一的约定进行应用开发。没有约定的团队,沟通成本是非常高的,比如有人会按目录分栈而其他人按目录分功能,开发者认知不一致很容易犯错。
- 异步编程模型
callback hell: 最臭名昭著的 callback 嵌套问题。
release zalgo: 异步函数中可能同步调用 callback 返回数据,带来不一致性。
async function
1 | const fn = async function() { |
- Koa
- 编写中间件
所有的请求经过一个中间件的时候都会执行两次,对比 Express 形式的中间件,Koa 的模型可以非常方便的实现后置处理逻辑。
假设有个需求:我们的新闻站点,禁止百度爬虫访问
1 | // app/middleware/robot.js |
- Context
Koa 增加了一个 Context 的对象,作为这次请求的上下文对象(在 Koa 1 中为中间件的 this,在 Koa 2 中作为中间件的第一个参数传入)。我们可以将一次请求相关的上下文都挂载到这个对象上。
- 异常处理
使用 try catch 就可以将按照规范编写的代码中的所有错误都捕获到。这样我们可以很便捷的编写一个自定义的错误处理中间件。
只需要将这个中间件放在其他中间件之前,就可以捕获它们所有的同步或者异步代码中抛出的异常了。
1 | async function onerror(ctx, next) { |
- 扩展
可以通过定义 app/extend/{application,context,request,response}.js 来扩展 Koa 中对应的四个对象的原型,通过这个功能,我们可以快速的增加更多的辅助方法,例如我们在 app/extend/context.js 中写入下列代码:
1 | // app/extend/context.js |
在 Controller 中,我们就可以使用到刚才定义的这个便捷属性了:
1 | // app/controller/home.js |
- 插件
经常会引入许许多多的中间件来提供各种各样的功能,例如引入 koa-session 提供 Session 的支持,引入 koa-bodyparser 来解析请求 body。
一个插件可以包含
1 | extend:扩展基础对象的上下文,提供各种工具类、属性。 |
安装对应的插件
1 | npm i egg-view-nunjucks --save |
开启插件:
1 | // config/plugin.js |
提示:开发期默认开启了 development 插件,修改后端代码后,会自动重启 Worker 进程。
- 静态资源
Egg 内置了 static 插件,线上环境建议部署到 CDN,无需该插件。
static 插件默认映射 /public/ -> app/public/ 目录
此处,我们把静态资源都放到 app/public 目录即可:
- 单元测试
测试文件应该放在项目根目录下的 test 目录下,并以 test.js 为后缀名,即 {app_root}/test/*/.test.js
1 | // test/app/middleware/robot.test.js |
然后配置依赖和 npm scripts:
1 | { |
执行测试:
1 | npm test |
- 渐进式开发
==初始-雏形-插件-框架==
- 最初始的状态
假设我们有一段分析 UA 的代码,实现以下功能:
1 | ctx.isIOS |
目录结构:
1 | example-app |
核心代码:
1 | // app/extend/context.js |
- 插件的雏形
这段逻辑是具备通用性的,可以写成插件。
但一开始的时候,功能还没完善,直接独立插件,维护起来比较麻烦。
此时,我们可以把代码写成插件的形式,但并不独立出去。
新的目录结构:
1 | example-app |
核心代码:
1 | app/extend/context.js 移动到 lib/plugin/egg-ua/app/extend/context.js。 |
- 抽成独立插件
经过一段时间开发后,该模块的功能成熟,此时可以考虑抽出来成为独立的插件。
目录结构:
1 | egg-ua |
注意:在插件还没发布前,可以通过 npm link 的方式进行本地测试,具体参见 npm-link。
1 | $ cd example-app |
- 沉淀到框架
目录结构:
1 | example-framework |
注意:在框架还没发布前,可以通过 npm link 的方式进行本地测试,具体参见 npm-link。
1 | $ cd example-app |
- 断点调试
- WebStorm 下进行断点
Run - Edit Configuration - npn - 点+加号添加
1 | Name:输入egg项目名称,方便进行区分 |
- csrf问题
egg安全默认机制,保护post等方法,可以关闭先
1 | // config/config.default.js |
- REST API
1 | GET(SELECT):从服务器取出资源(一项或多项)。 |
- 生产环境
配置安装 egg-scripts
package.json
1 | { |
启动停止服务
1 | npm start |
- 单元测试
- 测试框架
我们选择和推荐大家使用 Mocha,功能非常丰富,支持运行在 Node.js 和浏览器中, 对异步测试支持非常友好。
- 断言库
直到我们发现 power-assert, 因为『No API is the best API』, 最终我们重新回归原始的 assert 作为默认的断言库。
简单地说,它的优点是:
1 | 没有 API 就是最好的 API,不需要任何记忆,只需 assert 即可。 |
- 测试执行顺序
Mocha 使用 before/after/beforeEach/afterEach 来处理前置后置任务,基本能处理所有问题。 每个用例会按 before -> beforeEach -> it -> afterEach -> after 的顺序执行,而且可以定义多个。
- Controller 测试
app.httpRequest() 是 egg-mock 封装的 SuperTest 请求实例。
1 | const { app, mock, assert } = require('egg-mock/bootstrap'); |
- Service 测试
只需要先创建一个 ctx,然后通过 ctx.service.${serviceName} 拿到 Service 实例, 然后调用 Service 方法即可。
1 | describe('get()', () => { |
- Mock Service
Service 作为框架标准的内置对象,我们提供了便捷的 app.mockService(service, methodName, fn) 模拟 Service 方法返回值。
例如,模拟 app/service/user 中的 get(name) 方法,让它返回一个本来不存在的用户数据。
1 | it('should mock fengmk1 exists', () => { |
通过 app.mockServiceError(service, methodName, error) 可以模拟 Service 调用异常。
例如,模拟 app/service/user 中的 get(name) 方法调用异常:
1 | it('should mock service error', () => { |
egg问题汇总
- egg-socket.io 生产环境启动 –daemon 必须有–sticky 否则导致无法连接
==socket.io 协议实现需要 sticky 特性支持,否则在多进程模式下无法正常工作==
1 | // 这种方式启动,客户端会无法连接上socket.io |
基础
NodeJS和C++的性能比较
- 测试1E9次的空循环在NodeJS和C++中的执行用时
1 | C++: |
- 长度为1E6的整型数组排序
1 | C++: |
- 结论
C++是比NodeJS快,但性能比较通常是用耗时的数量级来衡量的,这两个结果取对数之后基本没什么差异,C++真心没比NodeJS快多少。
所以,比起其它一大堆Web编程语言,NodeJS的性能还是非常有优势的。
概念
错误优先的回调函数
第一个参数始终应该是一个错误对象, 用于检查程序是否发生了错误。其余的参数用于传递数据。
fs.readFile(filePath, function(err, data) { if (err) { //handle the error } // use the data object });
避免回调地狱
模块化:将回调函数分割为独立的函数
使用Promises
使用yield来计算生成器或Promise
事件循环
Node.js借助libuv来实现多线程
Libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个事件循环,以异步的方式将任务的执行结果返回给V8引擎。
哪些工具可以用来保证一致性的代码风格
JSLint
JSHint
ESLint
JSCS - 推荐
能够帮助团队开发者强制执行规定的风格指南, 还能够通过静态分析捕获常见的错误。
运算错误与程序员错误的区别
运算错误并不是bug,这是和系统相关的问题,例如请求超时或者硬件故障。
而程序员错误就是所谓的bug。
单元测试
Stub是完全模拟一个外部依赖
而Mock常用来判断测试通过还是失败。
测试金字塔指的是: 当我们在编写测试用例时,底层的单元测试应该远比上层的端到端测试要多。
七天学会NodeJS(开源书籍)
模块
require
require函数用于在当前模块中加载和使用别的模块,传入一个模块名,返回一个模块导出对象。模块名可使用相对路径(以./开头),或者是绝对路径(以/或C:之类的盘符开头)。另外,模块名中的.js扩展名可以省略。以下是一个例子。
var foo1 = require('./foo'); var foo2 = require('./foo.js'); var foo3 = require('/home/user/foo'); var foo4 = require('/home/user/foo.js');
可以使用以下方式加载和使用一个JSON文件。
var data = require('./data.json');
**exports**
exports对象是当前模块的导出对象,用于导出模块公有方法和属性。别的模块通过require函数使用当前模块时得到的就是当前模块的exports对象。以下例子中导出了一个公有方法。
exports.hello = function () {
console.log('Hello World!');
};
**module**
通过module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象。例如模块导出对象默认是一个普通对象,如果想改成一个函数的话,可以使用以下方式。
module.exports = function () {
console.log('Hello World!');
};
**模块初始化**
一个模块中的JS代码仅在模块第一次被使用时执行一次,并在执行过程中初始化模块的导出对象。之后,缓存起来的导出对象被重复利用。
**主模块**
通过命令行参数传递给NodeJS以启动程序的模块被称为主模块。主模块负责调度组成整个程序的其它模块完成工作。例如通过以下命令启动程序时,main.js就是主模块。
node main.js
完整示例
例如有以下目录。
- /home/user/hello/ - util/ counter.js main.js
其中counter.js内容如下:
var i = 0; function count() { return ++i; } exports.count = count;
该模块内部定义了一个私有变量i,并在exports对象导出了一个公有方法count。
主模块main.js内容如下:
var counter1 = require('./util/counter'); var counter2 = require('./util/counter'); console.log(counter1.count()); console.log(counter2.count()); console.log(counter2.count());
运行该程序的结果如下:
$ node main.js 1 2 3
可以看到,counter.js并没有因为被require了两次而初始化两次。
二进制模块
虽然一般我们使用JS编写模块,但NodeJS也支持使用C/C++编写二进制模块。编译好的二进制模块除了文件扩展名是.node外,和JS模块的使用方式相同。二进制模块能使用操作系统提供的所有功能,拥有无限的潜能。
模块路径解析规则
依次按照以下规则解析路径,直到找到模块位置
a. 内置模块
如果传递给require函数的是NodeJS内置模块名称,不做路径解析,直接返回内部模块的导出对象,例如require(‘fs’)。
b. node_modules目录
NodeJS定义了一个特殊的node_modules目录用于存放模块。例如某个模块的绝对路径是/home/user/hello.js,在该模块中使用require(‘foo/bar’)方式加载模块时,则NodeJS依次尝试使用以下路径。
/home/user/node_modules/foo/bar /home/node_modules/foo/bar /node_modules/foo/bar
c. NODE_PATH环境变量
与PATH环境变量类似,NodeJS允许通过NODE_PATH环境变量来指定额外的模块搜索路径。NODE_PATH环境变量中包含一到多个目录路径,路径之间在Linux下使用:分隔,在Windows下使用;分隔。例如定义了以下NODE_PATH环境变量:
NODE_PATH=/home/user/lib:/home/lib
当使用require(‘foo/bar’)的方式加载模块时,则NodeJS依次尝试以下路径。
/home/user/lib/foo/bar /home/lib/foo/bar
包(package)
我们已经知道了JS模块的基本单位是单个JS文件,但复杂些的模块往往由多个子模块组成。为了便于管理和使用,我们可以把由多个子模块组成的大模块称做包,并把所有子模块放在同一个目录里。
在组成一个包的所有子模块中,需要有一个入口模块,入口模块的导出对象被作为包的导出对象。例如有以下目录结构。
- /home/user/lib/ - cat/ head.js body.js main.js
其中cat目录定义了一个包,其中包含了3个子模块。main.js作为入口模块,其内容如下:
var head = require('./head'); var body = require('./body'); exports.create = function (name) { return { name: name, head: head.create(), body: body.create() }; };
在其它模块里使用包的时候,需要加载包的入口模块。接着上例,使用require(‘/home/user/lib/cat/main’)能达到目的,但是入口模块名称出现在路径里看上去不是个好主意。因此我们需要做点额外的工作,让包使用起来更像是单个模块。
index.js
当模块的文件名是index.js,加载模块时可以使用模块所在目录的路径代替模块文件路径,因此接着上例,以下两条语句等价。
var cat = require('/home/user/lib/cat'); var cat = require('/home/user/lib/cat/index');
这样处理后,就只需要把包目录路径传递给require函数,感觉上整个目录被当作单个模块使用,更有整体感。
package.json
如果想自定义入口模块的文件名和存放位置,就需要在包目录下包含一个package.json文件,并在其中指定入口模块的路径。上例中的cat模块可以重构如下。
- /home/user/lib/ - cat/ + doc/ - lib/ head.js body.js main.js + tests/ package.json
其中package.json内容如下。
{ "name": "cat", "main": "./lib/main.js" }
如此一来,就同样可以使用require(‘/home/user/lib/cat’)的方式加载模块。NodeJS会根据包目录下的package.json找到入口模块所在位置。
命令行程序
例如我们用NodeJS写了个程序,可以把命令行参数原样打印出来。该程序很简单,在主模块内实现了所有功能。并且写好后,我们把该程序部署在/home/user/bin/node-echo.js这个位置。为了在任何目录下都能运行该程序,我们需要使用以下终端命令。
node /home/user/bin/node-echo.js Hello World Hello World
这种使用方式看起来不怎么像是一个命令行程序,下边的才是我们期望的方式。
node-echo Hello World
Linux
在Linux系统下,我们可以把JS文件当作shell脚本来运行,从而达到上述目的,具体步骤如下:
a. 在shell脚本中,可以通过#!注释来指定当前脚本使用的解析器。所以我们首先在node-echo.js文件顶部增加以下一行注释,表明当前脚本使用NodeJS解析。 #! /usr/bin/env node NodeJS会忽略掉位于JS模块首行的#!注释,不必担心这行注释是非法语句。 b. 然后,我们使用以下命令赋予node-echo.js文件执行权限。 chmod +x /home/user/bin/node-echo.js c. 最后,我们在PATH环境变量中指定的某个目录下,例如在/usr/local/bin下边创建一个软链文件,文件名与我们希望使用的终端命令同名,命令如下: sudo ln -s /home/user/bin/node-echo.js /usr/local/bin/node-echo
这样处理后,我们就可以在任何目录下使用node-echo命令了。
工程目录
了解了以上知识后,现在我们可以来完整地规划一个工程目录了。以编写一个命令行程序为例,一般我们会同时提供命令行模式和API模式两种使用方式,并且我们会借助三方包来编写代码。除了代码外,一个完整的程序也应该有自己的文档和测试用例。因此,一个标准的工程目录都看起来像下边这样。
- /home/user/workspace/node-echo/ # 工程目录 - bin/ # 存放命令行相关代码 node-echo + doc/ # 存放文档 - lib/ # 存放API相关代码 echo.js - node_modules/ # 存放三方包 + argv/ + tests/ # 存放测试用例 package.json # 元数据文件 README.md # 说明文件
其中部分文件内容如下:
/* bin/node-echo */ var argv = require('argv'), echo = require('../lib/echo'); console.log(echo(argv.join(' '))); /* lib/echo.js */ module.exports = function (message) { return message; }; /* package.json */ { "name": "node-echo", "main": "./lib/echo.js" }
以上例子中分类存放了不同类型的文件,并通过node_moudles目录直接使用三方包名加载模块。此外,定义了package.json之后,node-echo目录也可被当作一个包来使用。
NPM
NPM是随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问题,常见的使用场景有以下几种:
a. 允许用户从NPM服务器下载别人编写的三方包到本地使用。
npm install argv
下载好之后,argv包就放在了工程目录下的node_modules目录中,因此在代码中只需要通过require(‘argv’)的方式就好,无需指定三方包路径。
以上命令默认下载最新版三方包,如果想要下载指定版本的话,可以在包名后边加上@version,例如通过以下命令可下载0.0.1版的argv。
npm install argv@0.0.1
如果使用到的三方包比较多,在终端下一个包一条命令地安装未免太人肉了。因此NPM对package.json的字段做了扩展,允许在其中申明三方包依赖。因此,上边例子中的package.json可以改写如下:
{ "name": "node-echo", "main": "./lib/echo.js", "dependencies": { "argv": "0.0.2" } }
这样处理后,在工程目录下就可以使用npm install命令批量安装三方包了。更重要的是,当以后node-echo也上传到了NPM服务器,别人下载这个包时,NPM会根据包中申明的三方包依赖自动下载进一步依赖的三方包。例如,使用npm install node-echo命令时,NPM会自动创建以下目录结构。
- project/ - node_modules/ - node-echo/ - node_modules/ + argv/ ... ...
如此一来,用户只需关心自己直接使用的三方包,不需要自己去解决所有包的依赖关系。
**b. 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。**
从NPM服务上下载安装一个命令行程序的方法与三方包类似。例如上例中的node-echo提供了命令行使用方式,只要node-echo自己配置好了相关的package.json字段,对于用户而言,只需要使用以下命令安装程序。
npm install node-echo -g
参数中的-g表示全局安装,因此node-echo会默认安装到以下位置,并且NPM会自动创建好Linux系统下需要的软链文件或Windows系统下需要的.cmd文件。
- /usr/local/ # Linux系统下
- lib/node_modules/
+ node-echo/
...
- bin/
node-echo
...
...
- %APPDATA%\npm\ # Windows系统下
- node_modules\
+ node-echo\
...
node-echo.cmd
...
**c. 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。**
第一次使用NPM发布代码前需要注册一个账号。终端下运行npm adduser,之后按照提示做即可。账号搞定后,接着我们需要编辑package.json文件,加入NPM必需的字段。接着上边node-echo的例子,package.json里必要的字段如下。
{
"name": "node-echo", # 包名,在NPM服务器上须要保持唯一
"version": "1.0.0", # 当前版本号
"dependencies": { # 三方包依赖,需要指定包名和版本号
"argv": "0.0.2"
},
"main": "./lib/echo.js", # 入口模块位置
"bin" : {
"node-echo": "./bin/node-echo" # 命令行程序名和主模块位置
}
}
之后,我们就可以在package.json所在目录下运行npm publish发布代码了。
**版本号**
使用NPM下载和发布代码时都会接触到版本号。NPM使用语义版本号来管理代码,这里简单介绍一下。
语义版本号分为X.Y.Z三位,分别代表主版本号、次版本号和补丁版本号。当代码变更时,版本号按以下原则更新。
+ 如果只是修复bug,需要更新Z位。
+ 如果是新增了功能,但是向下兼容,需要更新Y位。
+ 如果有大变动,向下不兼容,需要更新X位。
版本号有了这个保证后,在申明三方包依赖时,除了可依赖于一个固定版本号外,还可依赖于某个范围的版本号。例如"argv": "0.0.x"表示依赖于0.0.x系列的最新版argv。
**灵机一点**
除了本章介绍的部分外,NPM还提供了很多功能,package.json里也有很多其它有用的字段。除了可以在npmjs.org/doc/查看官方文档外,这里再介绍一些NPM常用命令。
NPM提供了很多命令,例如install和publish,使用npm help可查看所有命令。
使用npm help <command>可查看某条命令的详细帮助,例如npm help install。
在package.json所在目录下使用npm install . -g可先在本地安装当前命令行程序,可用于发布前的本地测试。
使用npm update <package>可以把当前目录下node_modules子目录里边的对应模块更新至最新版本。
使用npm update <package> -g可以把全局安装的对应命令行程序更新至最新版。
使用npm cache clear可以清空NPM本地缓存,用于对付使用相同版本号发布新版本代码的人。
使用npm unpublish <package>@<version>可以撤销发布自己发布过的某个版本代码。
文件操作
小文件拷贝
我们使用NodeJS内置的fs模块简单实现这个程序如下。
var fs = require('fs'); function copy(src, dst) { fs.writeFileSync(dst, fs.readFileSync(src)); } function main(argv) { copy(argv[0], argv[1]); } main(process.argv.slice(2));
以上程序使用fs.readFileSync从源路径读取文件内容,并使用fs.writeFileSync将文件内容写入目标路径。
process是一个全局变量,可通过process.argv获得命令行参数。由于argv[0]固定等于NodeJS执行程序的绝对路径,argv[1]固定等于主模块的绝对路径,因此第一个命令行参数从argv[2]这个位置开始。
大文件拷贝
function copy(src, dst) { fs.createReadStream(src).pipe(fs.createWriteStream(dst)); }
*API走马观花**
Buffer(数据块) 提供对二进制数据的操作
直接构造
var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]); bin[0]; // => 0x68; var str = bin.toString('utf-8'); // => "hello" 使用指定编码将二进制数据转化为字符串 var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f> 将字符串转换为指定编码下的二进制数据
拷贝一份Buffer,得首先创建一个新的Buffer,并通过.copy方法把原Buffer中的数据复制过去
var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]); var dup = new Buffer(bin.length); bin.copy(dup); dup[0] = 0x48; console.log(bin); // => <Buffer 68 65 6c 6c 6f> console.log(dup); // => <Buffer 48 65 65 6c 6f>
Stream(数据流)
Stream基于事件机制工作,所有Stream的实例都继承于NodeJS提供的EventEmitter。
var rs = fs.createReadStream(src); rs.on('data', function (chunk) { rs.pause(); // 可以在处理数据前暂停数据读取,并在处理数据后继续读取数据 doSomething(chunk, function () { rs.resume(); }); }); rs.on('end', function () { cleanUp(); });
File System(文件系统)
文件属性读写。
其中常用的有fs.stat、fs.chmod、fs.chown等等。
文件内容读写。
其中常用的有fs.readFile、fs.readdir、fs.writeFile、fs.mkdir等等。
底层文件操作。
其中常用的有fs.open、fs.read、fs.write、fs.close等等。
Path(路径)
path.normalize 将传入的路径转换为标准路径
path.normalize('foo//baz//../bar'); // "foo/bar" // 标准化之后的路径里的斜杠在Windows系统下是\,而在Linux系统下是/。如果想保证任何系统下都使用/作为路径分隔符的话,需要用.replace(/\\/g, '/')再替换一下标准路径
path.extname 根据不同文件扩展名做不同操作
path.extname('foo/bar.js'); // => ".js"
递归算法
function factorial(n) { if (n === 1) { return 1; } else { return n * factorial(n - 1); } }
!!!!!使用递归算法编写的代码虽然简洁,但由于每递归一次就产生一次函数调用,在需要优先考虑性能时,需要把递归算法转换为循环算法,以减少函数调用次数。
遍历目录
同步遍历
function travel(dir, callback) { fs.readdirSync(dir).forEach(function (file) { var pathname = path.join(dir, file); if (fs.statSync(pathname).isDirectory()) { travel(pathname, callback); } else { callback(pathname); } }); }
异步遍历
// 我们可以看到异步编程还是蛮复杂的。 function travel(dir, callback, finish) { fs.readdir(dir, function (err, files) { (function next(i) { if (i < files.length) { var pathname = path.join(dir, files[i]); fs.stat(pathname, function (err, stats) { if (stats.isDirectory()) { travel(pathname, callback, function () { next(i + 1); }); } else { callback(pathname, function () { next(i + 1); }); } }); } else { finish && finish(); } }(0)); }); }
文本编码
BOM的移除
BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符("\uFEFF"),位于文本文件头部。在不同的Unicode编码下,BOM字符对应的二进制字节如下: Bytes Encoding ---------------------------- FE FF UTF16BE FF FE UTF16LE EF BB BF UTF8 使用NodeJS读取文本文件时,一般需要去掉BOM。BOM字符虽然起到了标记文件编码的作用,其本身却不属于文件内容的一部分,如果读取文本文件时不去掉BOM,在某些使用场景下就会有问题。例如我们把几个JS文件合并成一个文件后,如果文件中间含有BOM字符,就会导致浏览器JS语法错误 function readText(pathname) { var bin = fs.readFileSync(pathname); if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) { bin = bin.slice(3); } return bin.toString('utf-8'); }
GBK转UTF8
GBK编码不在NodeJS自身支持范围内。因此,一般我们借助iconv-lite这个三方包来转换编码。 var iconv = require('iconv-lite'); function readGBKText(pathname) { var bin = fs.readFileSync(pathname); return iconv.decode(bin, 'gbk'); }
单字节编码
这里的诀窍在于,不管大于0xEF的单个字节在单字节编码下被解析成什么乱码字符,使用同样的单字节编码保存这些乱码字符时,背后对应的字节保持不变。 1. GBK编码源文件内容: var foo = '中文'; 2. 对应字节: 76 61 72 20 66 6F 6F 20 3D 20 27 D6 D0 CE C4 27 3B 3. 使用单字节编码读取后得到的内容: var foo = '{乱码}{乱码}{乱码}{乱码}'; 4. 替换内容: var bar = '{乱码}{乱码}{乱码}{乱码}'; 5. 使用单字节编码保存后对应字节: 76 61 72 20 62 61 72 20 3D 20 27 D6 D0 CE C4 27 3B 6. 使用GBK编码读取后得到内容: var bar = '中文'; function replace(pathname) { var str = fs.readFileSync(pathname, 'binary'); str = str.replace('foo', 'bar'); fs.writeFileSync(pathname, str, 'binary'); }
网络操作
开门红
var http = require('http'); http.createServer(function (request, response) { response.writeHead(200, { 'Content-Type': 'text-plain' }); response.end('Hello World\n'); }).listen(8124);
!!!!在Linux系统下,监听1024以下端口需要root权限。因此,如果想监听80或443端口的话,需要使用sudo命令启动程序。
**API走马观花*
HTTP
模块提供两种使用方式:
作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应。 作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。
HTTP请求本质上是一个数据流,由请求头(headers)和请求体(body)组成。例如以下是一个完整的HTTP请求数据内容。
空行之上是请求头,之下是请求体 POST / HTTP/1.1 User-Agent: curl/7.26.0 Host: localhost Accept: */* Content-Length: 11 Content-Type: application/x-www-form-urlencoded Hello World
客户端模式下如何工作
var options = { hostname: 'www.example.com', port: 80, path: '/upload', method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; var request = http.request(options, function (response) {}); request.write('Hello World'); request.end();
由于HTTP请求中GET请求是最常见的一种,并且不需要请求体,因此http模块也提供了以下便捷API。
http.get('http://www.example.com/', function (response) {});
HTTPS
在服务端模式下,创建一个HTTPS服务器的示例如下
var options = { key: fs.readFileSync('./ssl/default.key'), // 私钥和公钥 cert: fs.readFileSync('./ssl/default.cer') }; var server = https.createServer(options, function (request, response) { // ... });
根据HTTPS客户端请求使用的域名动态使用不同的证书
server.addContext('foo.com', { key: fs.readFileSync('./ssl/foo.com.key'), cert: fs.readFileSync('./ssl/foo.com.cer') }); server.addContext('bar.com', { key: fs.readFileSync('./ssl/bar.com.key'), cert: fs.readFileSync('./ssl/bar.com.cer') });
在客户端模式下,发起一个HTTPS客户端请求与http模块几乎相同
var options = { hostname: 'www.example.com', port: 443, path: '/', method: 'GET' }; var request = https.request(options, function (response) {}); request.end();
URL 该模块允许解析URL、生成URL,以及拼接URL
完整的URL的各组成部分
href ----------------------------------------------------------------- host path --------------- ---------------------------- http: // user:pass @ host.com : 8080 /p/a/t/h ?query=string #hash ----- --------- -------- ---- -------- ------------- ----- protocol auth hostname port pathname search hash ------------ query
使用.parse方法来将一个URL字符串转换为URL对象,示例如下
url.parse('http://user:pass@host.com:8080/p/a/t/h?query=string#hash'); /* => { protocol: 'http:', auth: 'user:pass', host: 'host.com:8080', port: '8080', hostname: 'host.com', hash: '#hash', search: '?query=string', query: 'query=string', pathname: '/p/a/t/h', path: '/p/a/t/h?query=string', href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash' } */
!!!!!!.parse方法还支持第二个和第三个布尔类型可选参数。第二个参数等于true时,该方法返回的URL对象中,query字段不再是一个字符串,而是一个经过querystring模块转换后的参数对象。第三个参数等于true时,该方法可以正确解析不带协议头的URL,例如//www.example.com/foo/bar
format方法允许将一个URL对象转换为URL字符串,示例如下。
url.format({ protocol: 'http:', host: 'www.example.com', pathname: '/p/a/t/h', search: 'query=string' }); /* => 'http://www.example.com/p/a/t/h?query=string' */
.resolve方法可以用于拼接URL,示例如下。
url.resolve('http://www.example.com/foo/bar', '../baz'); /* => http://www.example.com/baz */
Query String
- child_process - 子进程 直接调用系统命令或执行shell命令
1 | const util = require('util'); |
被误解的Node.js
https://www.ibm.com/developerworks/cn/web/1201_wangqf_nodejs/
网络应用的性能瓶颈
瓶颈之一在于 I/O 处理上
访问磁盘及网络数据所花费的 CPU 时间是访问内存时的数十万倍
I/O | CPU Cycle |
---|---|
L1-cache | 3 |
L2-cache | 14 |
RAM | 250 |
Disk | 41000000 |
Network | 240000000 |
传统的处理方式
- 单线程:客户端发起一个 I/O 请求,然后等待服务器端返回 I/O 结果,结果返回后再对其进行操作
- 多线程:服务器为每个请求分配一个线程,所有任务均在该线程内执行;程序员需考虑死锁,数据不一致等问题,多线程的程序极难调试和测试
- 事件驱动:使用一个线程执行,客户发起 I/O 请求的同时传入一个函数,该函数会在 I/O 结果返回后被自动调用,而且该请求不会阻塞后续操作
为什么选用 JavaScript
作者 Ryan Dahl 并没有选择 JavaScript,他尝试过 C、Lua,皆因其欠缺一些高级语言的特性,如闭包、函数式编程,致使程序复杂,难以维护。
加之 Google 提供的 V8 引擎,使 JavaScript 语言的执行速度大大提高。最终呈现在我们面前的就成了 Node.js,而不是 Node.c,Node.lua 或其他语言的实现。
4.
实战
swagger 接口文档
swagger-editor 编辑器
下载最新版
https://github.com/swagger-api/swagger-editor/releases
解压到目录,进入目录执行下面命令启动编辑器
npm start
浏览器访问打开编辑器
http://localhost:3001/
swagger-ui
安装swagger-ui-dist
npm install swagger-ui-dist
将swagger-editor编辑的文件导出json拷贝到api目录
启动
const express = require('express') const pathToSwaggerUi = require('swagger-ui-dist').absolutePath() const app = express() app.use(express.static(pathToSwaggerUi)) app.use('/api', express.static('public')); app.listen(3000)
浏览器访问打开编辑器
http://localhost:3000/
输入json文件目录到swagger输入框中
http://192.168.1.186:3000/api/swagger.json
swagger语法
例子:
1 | swagger: '2.0' |
使用Typescript开发node.js项目
https://segmentfault.com/a/1190000007574276
nodejs mongoose bluebird
Nodejs开发的一款完整的,带前台和后台的企业网站demo !!!!!!!!!!!!!!!
https://github.com/iteming/nodejs-website-demo
Nodejs 安装
安装
yum -y install gcc make gcc-c++ openssl-devel wget wget https://npm.taobao.org/mirrors/node/v9.2.1/node-v9.2.1.tar.gz tar -zvxf node-v9.2.1.tar.gz cd node-v9.2.1 ./configure make && make install
Nodejs 升级
安装n模块:
node有一个模块叫n(这名字可够短的。。。),是专门用来管理node.js的版本的。
npm install -g n
升级node.js到最新稳定版
n stable
n后面也可以跟随版本号比如:
n v0.10.26
NodeJS 与 npm 在Windows下升级
nodejs
Node 版本更新,下载指定版本 .msi 文件,安装到历史安装目录,即完成版本更新。
npm
npm 是随 NodeJS 一起发布的包管理工具,默认采用的并不一定是最新版本,如需升级使用以下命令:
npm -g install npm ( 最新稳定版 ) 或 npm -g install npm@2.9.1 ( 指定版本 )
npm
install
npm install [<@scope>/]
[-S|–save|-D|–save-dev|-O|–save-optional]: -S, --save: Package will appear in your dependencies. -D, --save-dev: Package will appear in your devDependencies. -O, --save-optional: Package will appear in your optionalDependencies.
淘宝镜像
npm install -g cnpm --registry=https://registry.npm.taobao.org npm config set registry https://registry.npm.taobao.org
pm2
安装/升级
npm install -g pm2
启动nodejs程序
pm2 start app.js --name critz -o logs/critz.out -e logs/critz.err
查询启动中的程序
pm2 list
关闭pm2
pm2 kill
日志
pm2 logs
性能测试
nohup ./webbench -c 5000 -t 86400 http://localhost:9999/lottery/commit_score?score=10000 &
其他命令
pm2 stop all pm2 reset all pm2 restart all pm2 start app.js --name my-api # 命名进程 pm2 list # 显示所有进程状态 pm2 monit # 监视所有进程监视每个node进程的CPU和内存的使用情况 pm2 logs # 显示所有进程日志 pm2 stop all # 停止所有进程 pm2 restart all # 重启所有进程 pm2 reload all # 0秒停机重载进程 (用于 NETWORKED 进程) pm2 stop 0 # 停止指定的进程 pm2 restart 0 # 重启指定的进程 pm2 startup # 产生 init 脚本 保持进程活着 pm2 web # 运行健壮的 computer API endpoint (http://localhost:9615) pm2 delete 0 # 杀死指定的进程 pm2 delete all # 杀死全部进程
运行进程的不同方式:
$ pm2 start app.js -i max # 根据有效CPU数目启动最大进程数目 $ pm2 start app.js -i 3 # 启动3个进程 $ pm2 start app.js -x #用fork模式启动 app.js 而不是使用 cluster $ pm2 start app.js -x -- -a 23 # 用fork模式启动 app.js 并且传递参数 (-a 23) $ pm2 start app.js --name serverone # 启动一个进程并把它命名为 serverone $ pm2 stop serverone # 停止 serverone 进程 $ pm2 start app.json # 启动进程, 在 app.json里设置选项 $ pm2 start app.js -i max -- -a 23 #在--之后给 app.js 传递参数 $ pm2 start app.js -i max -e err.log -o out.log # 启动 并 生成一个配
初始化相关组件
npm install // 初始化ejs npm install ejs // 初始化express组件 npm install express // 初始化mongdb组合的组件 npm install mongoose npm install express-mongoose // node 默认启动后修改文件,页面不会立即体现,通过该组件可以使修改的文件实时在页面体现出来 npm install supervisor
pm2 start 添加参数
参数为 prod
1 | pm2 start index.js -- prod |
注意:–后面有空格,并且后面全部都是参数
例如下面 38080为新端口号,当参数传入1
pm2 start app.js --name xxxxx38080 -e logs/xxxxx38080.err -o logs/xxxxx38080.out -- 38080
- nodejs命令行执行时带参数
1 | //node app.js arg1 arg2 arg3, 想取得这三个参数 |
代码覆盖率
npm install -g istanbul --registry=http://registry.npm.taobao.org
npm install --save --registry=http://registry.npm.taobao.org
//test
npm test
GraphicsMagick for node.js
牛逼的图片处理
调试nodejs
借用Chrome浏览器的JavaScript调试器来调试
npm来安装node-inspector
npm install -g node-inspector // -g 导入安装路径到环境变量
node-inspector是通过websocket方式来转向debug输入输出的。
启动node-inspector来监听Nodejs的debug调试端口
node-inspector &
默认情况下node-inspector的端口是8080,可以通过参数–web-port=[port]来设置端口。
在启动node-inpspector之后,我们可以通过–debug或–debug-brk来启动nodejs程序。
通过在chrome浏览器输入http://[ip address]:8080/debug?port=5858,我们会得到调试窗口。
WebStorm安装调试js
基于Nodejs的网络开源东东
Nodejs开发的一款企业网站demo,包含前台后台管理
https://cnodejs.org/topic/58d4be37b3e60b982d089b3a
koa的框架
koa的框架web开发简介
https://www.toutiao.com/a6477702957415531022/
koa 致力于成为一个更小、更富有表现力、更健壮的 Web 框架。
新一代Node.js的Web开发框架Koa2
https://www.toutiao.com/a6481426813737239054/
koa2-demo2的项目目录结构
bin, 存放启动项目的脚本文件 node_modules, 存放所有的项目依赖库。 public,静态文件(css,js,img) routes,路由文件(MVC中的C,controller) views,页面文件(pug模板) package.json,项目依赖配置及开发者信息 app.js,应用核心配置文件 package.json,node项目配置文件 package-lock.json,node项目锁定的配置文件
app.js文件,我们可以分割为X个部分进行解读:
依赖包加载 错误处理 中间件加载 web界面渲染模板、 自定义日志 自己定义路由 外部调用接口
Raneto 支持Markdown的开源知识库
www.raneto.com
配置
- 限制浏览用户名和密码
config.default.js这个配置文件
authentication : true,
credentials : {
username : ‘你的用户名‘,
password : ‘你的密码‘
},
Ghost 基于Node.js的开源博客系统
十分吃内存,不建议使用,可以用hexo
下载
安装
注意:!!!!!!node版本必须是6!!!!!!
新建目录,并解压下载文件
unzip Ghost-1.17.1.zip
修改配置文件 core/server/config/env/config.development.json
"url": "http://192.168.1.132:2368", "server": { "host": "192.168.1.132", "port": 2368 },
安装 sqlite3 环境
npm install -g knex-migrator knex-migrator init
安装其他环境
npm install
启动调试
node index.js
Hexo
https://hexo.io/zh-cn/
解决 Hexo 进程守护的问题
Hexo下新建一个app.js,写入下面代码:
var spawn = require('child_process').spawn; free = spawn('hexo', ['server', '-p 4000']);/* 其实就是等于执行hexo server -p 4000*/ free.stdout.on('data', function (data) { console.log('standard output:\n' + data); }); free.stderr.on('data', function (data) { console.log('standard error output:\n' + data); }); free.on('exit', function (code, signal) { console.log('child process eixt ,exit:' + code); });
主题
找到你想要的主题。在github中搜索你要的主题名称
我选的是hueman,看起来挺不错
https://github.com/ppoffice/hexo-theme-hueman/wiki
搜索插件安装
分类方法
categories: - Diary - Life
分类Life成为Diary的子分类
leanote
https://github.com/leanote/leanote
Leanote, 不只是笔记!
特性
高效笔记:Leanote 有易操作的界面, 包含一款富文本编辑器和Markdown编辑器,让您的笔记记录更轻松和高效。对高阶用户,我们还提供Vim 和Emacs 编辑模式,助推你的写作速度更上层楼。 知识管理: Leanote 灵活而强大的“笔记本-笔记-标签”系统,让它成为你个人知识管理的利器。 分享: 你可以通过Leanote同好友分享知识、想法和经历, 邀请好友加入你的笔记簿,通过云端交流信息。 协作: Leanote协助你与同事之间相互协作,激荡新思路,随时随地头脑风暴。 博客: Leanote也可以作为你的个人博客, 把你的知识传播的更远!
其它特性
支持Markdown编辑 写作模式 Vim 及 Emacs 编辑模式 支持PDF导出 支持批量操作 博客自定义主题, 实现高度定制化
log4js
- 基本使用
1 | var log4js = require('log4js'); |
websocket
- 框架
1 | var ws = require('ws'); |
node-http-proxy
- 代理原理
访问http://localhost:8000,会被代理到访问http://localhost:9000
1 | var http = require('http'), |
比如可以通过访问 http://localhost:9000 ,代理到访问 http://www.google.com
实战
高并发Nodejs参数调整
关闭v8 空时通知机制
1 | --nouse-idle-notification |
修改http.Agent
1 | 官网说明: |
修改–max-old-space-size
1 | --max-old-space-size=2048(根据自己情况,可以调大,单位是M) |
小实验
- 读取tab分隔的文件,转换成json文件
读取文件如下:
1 | 1 英勇青铜III |
得出json文件如下:
1 | { |
代码如下:
1 | var fs=require("fs"); |
- 连接使用redis
连接rediser.js
1 | "use strict"; |
使用
1 | var rediser = require('../utils/rediser.js'), |
- 使用redis的发布订阅实现跨服通信
1 | const redis = require('redis'); |
心得
async/await
async/await是es7的新标准,并且在node7.0中已经得到支持
async函数定义如下
async function fn(){ return 0; }
使用async关键字修饰function即可,async函数的特征在于调用return返回的并不是一个普通的值,而是一个Promise对象
如果正常return了,则返回Promise.resolve(返回值),如果throw一个异常了,则返回Promise.reject(异常)
也就是说async函数的返回值一定是一个promise,只是你写出来是一个普通的值,这仅仅是一个语法糖
await关键字只能在async函数中才能使用。await关键字后跟一个promise对象,函数执行到await后会退出该函数,直到事件轮询检查到Promise有了状态resolve或reject才重新执行这个函数后面的内容。
例如:
var a = await A.findOne({id:id}); if(!a){ a = await newA(id); // 必须加await,否则newA返回值是promise不是A对象!!!!!!!!!!!!! } async function newA(id){ var a = new A(); a = await a.save(); return a; }
log4js使用
var log4js = require('log4js'); log4js.configure({ appenders: { critz2payment: { type: 'file', filename: 'logs/critz2payment.log' } }, categories: { default: { appenders: ['critz2payment'], level: 'info' } } }); const logger = log4js.getLogger('critz2payment'); logger.info('lxpaymentCallback:' + JSON.stringify(reqParm));
一次性密码 NodeJS 实现TOTP算法(Google Authenticator)
http://www.blogjava.net/baicker/archive/2013/09/11/403712.html
NodeJS下,先安装notp库
npm install notp
然后脚本测试如下:
var notp = require('notp'); var token = 'LFLFMU2SGVCUIUCZKBMEKRKLIQ'; var key = notp.totp.gen(token, {}); console.log(key);
性能调优
1百万个链接的并发连接数
http://blog.caustik.com/2012/08/19/node-js-w1m-concurrent-connections/
The new tweaks, placed in /etc/sysctl.conf (CentOS) and then reloaded with “sysctl -p” :
1 | net.core.rmem_max = 33554432 |
错误汇总
error: #error This version of node/NAN/v8 requires a C++11 compiler
安装devtoolset-3
rpm -ivh https://www.softwarecollections.org/en/scls/rhscl/devtoolset-3/epel-6-x86_64/download/rhscl-devtoolset-3-epel-6-x86_64.noarch.rpm yum install devtoolset-3-gcc-c++
安装完毕后使用命令,临时覆盖系统原先的gcc引用
scl enable devtoolset-3 bash
若想永久覆盖,可在.bashrc中添加
source /opt/rh/devtoolset-3/enable
然后再继续npm install,就能够正常安装以前无法编译通过的module了
安装 node-inspector 时报告网络错误
npm install -g node-inspector npm http GET https://registry.npmjs.org/node-inspector npm http GET https://registry.npmjs.org/node-inspector npm http GET https://registry.npmjs.org/node-inspector npm ERR! network getaddrinfo ENOTFOUND npm ERR! network This is most likely not a problem with npm itself npm ERR! network and is related to network connectivity. npm ERR! network In most cases you are behind a proxy or have bad network settin gs. npm ERR! network npm ERR! network If you are behind a proxy, please make sure that the npm ERR! network ‘proxy’ config is set properly. See: ‘npm help config’ npm ERR! System Windows_NT 6.2.9200 npm ERR! command “C:\Program Files\nodejs\\node.exe” “C:\Program Files\nod ejs\node_modules\npm\bin\npm-cli.js” “install” “-g” "node-inspector" npm ERR! cwd D:\sec4 npm ERR! node -v v0.10.26 npm ERR! npm -v 1.4.3 npm ERR! syscall getaddrinfo npm ERR! code ENOTFOUND npm ERR! errno ENOTFOUND npm ERR!
解决方法
npm --registry=https://registry.npm.taobao.org install -g node-inspector
如果还有问题,可能是域名解析问题 ping registry.npm.taobao.org 看看是否通
Express Request entity too large
解决方法
var bodyParser = require('body-parser'); app.use(bodyParser.json({limit: '50mb'})); app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));
user “root” does not have permission to access the dev dir
解决方案
npm install time -g --unsafe-perm 增加了--unsafe-perm
node用n升级后看version版本没变
node的安装目录和 n 默认的路径不一样导致的
which node // 发现在/usr/bin/node, n是在目录/usr/local/bin/node
编辑环境配置文件:
vim ~/.bash_profile
将下面代码插入到文件末尾:
export PATH=/usr/local/bin:$PATH
执行source使修改生效
source ~/.bash_profile
确认一下环境变量是否生效:
echo $PATH
再查看当前 node 版本:
node -v
npm的任何命令都报错:SyntaxError: Error parsing /usr/local/lib/node_modules/npm/node_modules/npmlog/node_modules/gauge/node_modules/strip-ansi/package.json: Unexpected end of JSON input
npm本身的关联包出问题了
根据报错信息在其他机器上安装缺失的包,并用好的替换报错的文件 如strip-ansi/package.json npm install strip-ansi 拷贝package.json到目录/usr/local/lib/node_modules/npm/node_modules/npmlog/node_modules/gauge/node_modules/strip-ansi/
然后升级npm
npm install -g npm
升级node后报错
1 | Error: The module 'node-sass' |
NODE_MODULE_VERSION 是每一个 Node.js 版本内人为设定的数值,意思为 ABI 的版本号。一旦这个号码与已经编译好的二进制模块的号码不符,便判断为 ABI 不兼容,需要用户重新编译。
解决方案:
备份原项目的node_modules
npm install
升级node后mongoose连接不上
mongoose的版本太低了,修改package.js文件,把限制mongoose的版本号去掉
"mongoose": "", "mongoose-auto-increment": "",
重新安装
npm uninstall mongoose npm uninstall mongoose-auto-increment npm install mongoose npm install mongoose-auto-increment
Cannot find module ‘internal/fs’的错
是graceful-fs 这个模块出现了问题!
可能原因有以下几点:
1. 没装 2. npm的graceful-fs 出现了多个版本
解决办法:
首先查看: npm list -g graceful-fs 如发现:npm ERR! invalid: graceful-fs@3.0.8 /usr/local/lib/node_modules/npm/node_modules/cmd-shim/node_modules/graceful-fs 或类似: │ ├─┬ cmd-shim@2.0.2 │ │ └── graceful-fs@3.0.8 invalid 则表示版本有问题 其次全局安装 graceful-fs 接着 在对应环境再安装一遍 如果不行根据目录在其他地方拷贝graceful-fs到目录中,例如: /usr/local/lib/node_modules/npm/node_modules/cmd-shim/node_modules/graceful-fs
mongoose表里id自增问题
尽量抛弃使用int的自增id,使用uuid更合适
删除一条数据
var result = await mails.remove({mailId:req.params.id}); if (result != null && result.result.ok > 0) { res.json({status: 1, msg: '删除成功!'}); return; }
- sudo npm install 报错找不到npm命令
1 | sudo ln -s /usr/local/bin/node /usr/bin/node |
nodejs 用socket.io 发送gzip加密后数据是经过base64_encode的,否则emit不能发送二进制数据,只能发送string类型数据,c++进行gzip解密需要先使用base64_decode一下
安装canvas
http://ftp.gnome.org/pub/gnome/binaries/win32/gtk+/2.24/gtk+-bundle_2.24.10-20120208_win32.zip
http://downloads.sourceforge.net/gladewin32/gtk-dev-2.12.9-win32-2.exe
- ==返回给客户端500错误码,会导致服务器端对客户端的请求丢失响应一段时间==
尽量避免返回500错误码,争取都是200
a