心谭

心谭 查看完整档案

深圳编辑深圳大学  |  数学与计算机双专业 编辑腾讯  |  web前端开发 编辑 xin-tan.com 编辑
编辑

公众号「心谭博客」专注前端与算法

QQ群:857989948

个人动态

心谭 发布了文章 · 5月5日

系统设计:基于角色的权限管理设计实现

📖博客原文 :xxoo521.com《系统设计:基于角色的权限管理设计实现》

背景

内部运营系统的很多 API,涉及到外网正式环境下的用户信息变更。出于安全考虑,在设计之初保留了所有的操作记录,但这用于事后回查;真正要避免线上事故的发生,还需要权限管理。

当前,系统的代码由 3 部分组成:前端、中台和后台。其中,前端负责交互逻辑,中台负责主要的业务逻辑,后台负责提供数据库的读写 api。所有的校验和业务逻辑,都是由中台拼接实现,所以权限管理的改造需要中台参与。

基于角色的权限设计

假设系统支持 4 种角色:

  • 角色 A:超级管理员
  • 角色 B:运营人员
  • 角色 C:开发人员
  • 角色 D:游客(普通用户)

每个 api 都按照其职能,划分到对应的 api 集合中:

  • 集合 a:用户管理相关 api
  • 集合 b:

    • 日志相关 api
    • 环境信息相关 api
  • 集合 c:

    • 资源调整 api
    • 黑名单 api

每种角色可以调通单个/多个/全部的 api 集合:

  • 角色 A:所有 api 集合
  • 角色 B:

    • 集合 b
    • 集合 c
  • 角色 C:所有 api 集合
  • 角色 D:

    • 集合 b

需要注意的是,每个用户只能是一种角色,而角色可以对应多个集合,每个集合可以对应多个 api。简而言之,角色是用户身份,它是唯一的。

例如,对于某些特定的用户(比如实习生),可以专门新建一个角色,再对此角色所需要的 api 集合进行排列组合。

中台与服务化

后台以服务化的方式提供了最基本的数据库读写 api,日后的改动成本低,运维成本低,并且可以给其他应用提供服务。

而主要的逻辑交给了中台进行拼接组合,中台不需要保存状态。同时,业务逻辑的改动将不涉及数据库和后台,中后台完全接耦,简化了发布和部署流程。

<img data-original="https://tva1.sinaimg.cn/large/006tNbRwly1gbjcfr3xb5j30cw00tjrd.jpg" style="width: 100% !important;"/>

👇扫码关注「心谭博客」,查看「前端图谱」&「算法题解」,坚持分享,共同成长👇

查看原文

赞 0 收藏 0 评论 0

心谭 发布了文章 · 5月4日

规范git commit的提交记录

📖博客原文 :xxoo521.com《规范git commit的提交记录(交互式命令行)》

随着项目体积的增加,参与到项目中的同学越来越多,每个人都有自己的打 git log 的习惯:

  • 格式 1: add: 添加...
  • 格式 2: [add]: 添加...
  • 格式 3: Add 添加...

为了形成统一的规范,达成共识,从而降低协作开发成本,需要对 git commit 记录进行规范。

规范 git commit 记录

规范 git commit 记录,需要做两件事情:

  • 通过交互式命令行,自动生成符合指定规范的 commit 记录
  • 提交记录后,在 git hooks 中进行 commit 记录格式检查
问:既然已经交互式生成了规范记录,为什么需要在 hooks 进行检查?

交互式生成 commit 记录,需要用户调用自定义的 npm scripts,例如npm run commit。但还是可以直接调用原生 git 命令 git commit 来提交记录。而检查是在正式提交前进行的,因此不符合要求的记录不会生效,需要重新 commit。

调研:交互式 commit log 规范方案

前期调研结果,关于 commit 提示有两种做法:

  1. 直接使用 commitizen 中常用的 adapter
  2. 根据团队的需要,自定义 adapter

方法 1 的优缺点:

优点 1: 直接安装对应的 adapter 即可

优点 2: 无开发成本

缺点 1: 无法定制,不一定满足团队需要

方法 2 的优缺点:

优点 1: 可定制,满足开发需求

优点 2: 单独成库,发布 tnpm,作为技术建设

缺点 1: 需要单独一个仓库(但开发成本不高)

代码实现

在实际工作中,发现方法 1 中的常用规范,足够覆盖团队日常开发场景。所以,选择了方法 1.

step1: 安装 npm 包

npm i --save-dev commitizen cz-conventional-changelog @commitlint/cli @commitlint/config-conventional husky

添加 package.json 的配置:

"scripts": {
    "commit": "git-cz"
},
"husky": {
    "hooks": {
        "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
},
"config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
}

在项目根目录下创建commitlint.config.js

module.exports = {
    extends: ["@commitlint/config-conventional"]
};

使用方法:不再使用git commit -m ...,而是调用npm run commit

<img data-original="https://tva1.sinaimg.cn/large/006tNbRwly1gbjcfr3xb5j30cw00tjrd.jpg" style="width: 100% !important;"/>

👇扫码关注「心谭博客」,查看「前端图谱」&「算法题解」,坚持分享,共同成长👇

查看原文

赞 4 收藏 2 评论 1

心谭 赞了文章 · 2月15日

Nodejs:UDP极简入门例子

模块概览

dgram模块是对UDP socket的一层封装,相对net模块简单很多,下面看例子。

文本同步收录于GitHub主题系列《Nodejs学习笔记》

UPD客户端 vs UDP服务端

首先,启动UDP server,监听来自端口33333的请求。

server.js

// 例子:UDP服务端
var PORT = 33333;
var HOST = '127.0.0.1';

var dgram = require('dgram');
var server = dgram.createSocket('udp4');

server.on('listening', function () {
    var address = server.address();
    console.log('UDP Server listening on ' + address.address + ":" + address.port);
});

server.on('message', function (message, remote) {
    console.log(remote.address + ':' + remote.port +' - ' + message);
});

server.bind(PORT, HOST);

然后,创建UDP socket,向端口33333发送请求。

client.js

// 例子:UDP客户端
var PORT = 33333;
var HOST = '127.0.0.1';

var dgram = require('dgram');
var message = Buffer.from('My KungFu is Good!');

var client = dgram.createSocket('udp4');

client.send(message, PORT, HOST, function(err, bytes) {
    if (err) throw err;
    console.log('UDP message sent to ' + HOST +':'+ PORT);
    client.close();
});

运行 server.js。

node server.js

运行 client.js。

➜  2016.12.22-dgram git:(master) ✗ node client.js 
UDP message sent to 127.0.0.1:33333

服务端打印日志如下

UDP Server listening on 127.0.0.1:33333
127.0.0.1:58940 - My KungFu is Good!

广播

通过dgram实现广播功能很简单,服务端代码如下。

var dgram = require('dgram');
var server = dgram.createSocket('udp4');
var port = 33333;

server.on('message', function(message, rinfo){
    console.log('server got message from: ' + rinfo.address + ':' + rinfo.port);
});

server.bind(port);

接着创建客户端,向地址'255.255.255.255:33333'进行广播。

var dgram = require('dgram');
var client = dgram.createSocket('udp4');
var msg = Buffer.from('hello world');
var port = 33333;
var host = '255.255.255.255';

client.bind(function(){
    client.setBroadcast(true);
    client.send(msg, port, host, function(err){
        if(err) throw err;
        console.log('msg has been sent');
        client.close();
    });
});

运行程序,最终服务端打印日志如下

➜  2016.12.22-dgram git:(master) ✗ node broadcast-server.js
server got message from: 192.168.0.102:61010

相关链接

https://nodejs.org/api/dgram....

查看原文

赞 6 收藏 6 评论 2

心谭 赞了回答 · 2月12日

解决图片反防盗链的问题

谢邀。

先说题主提到的 referrerPolicy 这个属性,根据 MDN 和 caniuse 上的数据,ios 都显示的是不支持。如下图:

Screenshot from 2020-02-12 15-57-07.png
Screenshot from 2020-02-12 15-56-46.png

另外,图片这些,官方的内容或者用户的内容,哪怕是外链,最好也还是拿过来吧,存在自己这边比较保险和可控。

再另外,如果需要的话,可以用第三方做内容的政治涉黄等鉴定。

关注 3 回答 1

心谭 发布了文章 · 2月11日

Node.js实战--资源压缩与zlib模块

📖Blog:《NodeJS模块研究 - zlib》

🐱Github:https://github.com/dongyuanxin/blog

nodejs 的 zlib 模块提供了资源压缩功能。例如在 http 传输过程中常用的 gzip,能大幅度减少网络传输流量,提高速度。本文将从下面几个方面介绍 zlib 模块和相关知识点:

  • 文件压缩 / 解压
  • HTTP 中的压缩/解压
  • 压缩算法:RLE
  • 压缩算法:哈夫曼树

文件的压缩/解压

以 gzip 压缩为例,压缩代码如下:

const zlib = require("zlib");
const fs = require("fs");

const gzip = zlib.createGzip();

const rs = fs.createReadStream("./db.json");
const ws = fs.createWriteStream("./db.json.gz");
rs.pipe(gzip).pipe(ws);

如下图所示,4.7Mb 大小的文件被压缩到了 575Kb。

解压刚才压缩后的文件,代码如下:

const zlib = require("zlib");
const fs = require("fs");

const gunzip = zlib.createGunzip();

const rs = fs.createReadStream("./db.json.gz");
const ws = fs.createWriteStream("./db.json");
rs.pipe(gunzip).pipe(ws);

HTTP 中的压缩/解压

在服务器中和客户端的传输过程中,浏览器(客户端)通过 Accept-Encoding 消息头来告诉服务端接受的压缩编码,服务器通过 Content-Encoding 消息头来告诉浏览器(客户端)实际用于编码的算法。

服务器代码示例如下:

const zlib = require("zlib");
const fs = require("fs");
const http = require("http");

const server = http.createServer((req, res) => {
    const rs = fs.createReadStream("./index.html");
    // 防止缓存错乱
    res.setHeader("Vary", "Accept-Encoding");
    // 获取客户端支持的编码
    let acceptEncoding = req.headers["accept-encoding"];
    if (!acceptEncoding) {
        acceptEncoding = "";
    }
    // 匹配支持的压缩格式
    if (/\bdeflate\b/.test(acceptEncoding)) {
        res.writeHead(200, { "Content-Encoding": "deflate" });
        rs.pipe(zlib.createDeflate()).pipe(res);
    } else if (/\bgzip\b/.test(acceptEncoding)) {
        res.writeHead(200, { "Content-Encoding": "gzip" });
        rs.pipe(zlib.createGzip()).pipe(res);
    } else if (/\bbr\b/.test(acceptEncoding)) {
        res.writeHead(200, { "Content-Encoding": "br" });
        rs.pipe(zlib.createBrotliCompress()).pipe(res);
    } else {
        res.writeHead(200, {});
        rs.pipe(res);
    }
});

server.listen(4000);

客户端代码就很简单了,识别 Accept-Encoding 字段,并进行解压:

const zlib = require("zlib");
const http = require("http");
const fs = require("fs");
const request = http.get({
    host: "localhost",
    path: "/index.html",
    port: 4000,
    headers: { "Accept-Encoding": "br,gzip,deflate" }
});
request.on("response", response => {
    const output = fs.createWriteStream("example.com_index.html");

    switch (response.headers["content-encoding"]) {
        case "br":
            response.pipe(zlib.createBrotliDecompress()).pipe(output);
            break;
        // 或者, 只是使用 zlib.createUnzip() 方法去处理这两种情况:
        case "gzip":
            response.pipe(zlib.createGunzip()).pipe(output);
            break;
        case "deflate":
            response.pipe(zlib.createInflate()).pipe(output);
            break;
        default:
            response.pipe(output);
            break;
    }
});

从上面的例子可以看出来,3 种对应的解压/压缩 API:

  • zlib.createInflate()zlib.createDeflate()
  • zlib.createGunzip()zlib.createGzip()
  • zlib.createBrotliDecompress()zlib.createBrotliCompress()

压缩算法:RLE

RLE 全称是 Run Length Encoding, 行程长度编码,也称为游程编码。它的原理是:记录连续重复数据的出现次数。它的公式是:字符 * 出现次数

例如原数据是 AAAAACCPPPPPPPPERRPPP,一共 18 个字节。按照 RLE 的规则,压缩后的结果是:A5C2P8E1R2P3,一共 12 个字节。压缩比例是:12 / 17 = 70.6%

RLE 的优点是压缩和解压非常快,针对连续出现的多个字符的数据压缩率更高。但对于ABCDE类似的数据,压缩后数据会更大。

压缩算法:哈夫曼树

哈夫曼树的原理是:出现频率越高的字符,用尽量更少的编码来表示。按照这个原理,以数据ABBCCCDDDD为例:

字符编码(二进制)
D0
C1
B10
A11

原来的数据是 10 个字节。那么编码后的数据是:1110101110000,一共 13bit,在计算机中需要 2 个字节来存储。这样的压缩率是:2 / 10 = 20%。

但是仅仅按照这个原理编码后的数据,无法正确还原。以前 4bit 为例,1110可以理解成:

  • 11 + 10
  • 1 + 1 + 1 + 0
  • 1 + 1 + 10
  • ...

而哈夫曼树的设计就很巧妙,能正确还原。哈夫曼树的构造过程如下:

无论哪种数据类型(文本文件、图像文件、EXE 文件),都可以采用哈夫曼树进行压缩。

参考链接

<img data-original="https://tva1.sinaimg.cn/large/006tNbRwly1gbjcfr3xb5j30cw00tjrd.jpg" style="width: 100% !important;"/>

👇扫码关注「心谭博客」,查看「前端图谱」&「算法题解」,坚持分享,共同成长👇

查看原文

赞 5 收藏 3 评论 0

心谭 发布了文章 · 2月10日

Nodejs实战系列:数据加密与crypto模块

博客地址:《NodeJS模块研究 - crypto》

Github :https://github.com/dongyuanxin/blog

nodejs 中的 crypto 模块提供了各种各样加密算法的 API。这篇文章记录了常用加密算法的种类、特点、用途和代码实现。其中涉及算法较多,应用面较广,每类算法都有自己适用的场景。为了使行文流畅,列出了本文记录的几类常用算法:

  • 内容摘要:散列(Hash)算法
  • 内容摘要:HMac 算法
  • 内容加解密:对称加密(AES)与非对称加密解密(RSA)
  • 内容签名:签名和验证算法

散列(Hash)算法

散列函数(英语:Hash function)又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。基本原理是将任意长度数据输入,最后输出固定长度的结果。

hash 算法具有以下特点:

  • 不能从 hash 值倒推原数据
  • 不同的输入,会有不同的输出
  • 好的 hash 算法冲突概率更低

正因为 hash 算法的这些特点,因此 hash 算法主要用于:加密、数据检验、版本标识、负载均衡、分布式(一致性 hash)。

下面实现了一个获取文件标识的函数:

const crypto = require("crypto");
const fs = require("fs");

function getFileHash(file, algorithm) {
    if (!crypto.getHashes().includes(algorithm)) {
        throw new Error("不支持此哈希函数");
    }

    return new Promise(resolve => {
        const hash = crypto.createHash(algorithm);

        const rs = fs.createReadStream(file);
        rs.on("readable", () => {
            const data = rs.read();
            if (data) {
                hash.update(data);
            }
        });
        rs.on("end", () => {
            resolve(hash.digest("hex"));
        });
    });
}

// 用法:获取文件md5
getFileHash("./db.json", "md5").then(val => {
    console.log(val);
});

HMac 算法

攻击者可以借助“彩虹表”来破解哈希表。应对彩虹表的方法,是给密码加盐值(salt),将 pwd 和 salt 一起计算 hash 值。其中,salt 是随机生成的,越长越好,并且需要和用户名、密码对应保存在数据表中。

虽然通过加盐,实现了哈希长度扩展,但是攻击者通过提交密码和哈希值也可以破解攻击。服务器会把提交的密码和 salt 构成字符串,然后和提交的哈希值对比。如果系统不能提交哈希值,不会受到此类攻击。

显然,没有绝对安全的方法。但是不推荐使用密码加盐,而是 HMac 算法。它可以使用任意的 Hash 函数,例如 md5 => HmacMD5、sha1 => HmacSHA1。

下面是利用 Hmac 实现加密数据的函数:

const crypto = require("crypto");

function encryptData(data, key, algorithm) {
    if (!crypto.getHashes().includes(algorithm)) {
        throw new Error("不支持此哈希函数");
    }

    const hmac = crypto.createHmac(algorithm, key);
    hmac.update(data);
    return hmac.digest("hex");
}

// output: 30267bcf2a476abaa9b9a87dd39a1f8d6906d1180451abdcb8145b384b9f76a5
console.log(encryptData("root", "7(23y*&745^%I", "sha256"));

对称加密(AES)与非对称加密解密(RSA)

有很多数据需要加密存储,并且需要解密后进行使用。这和前面不可逆的哈希函数不同。此类算法一共分为两类:

  • 对称加密(AES):加密和解密使用同一个密钥
  • 非对称加密解密(RSA):公钥加密,私钥解密

对称加密(AES)

查看 nodejs 支持的所有加密算法:

crypto.getCiphers();

Nodejs 提供了 Cipher 类和 Decipher 类,分别用于加密和解密。两者都继承 Transfrom Stream,API 的使用方法和哈希函数的 API 使用方法类似。

下面是用 aes-256-cbc 算法对明文进行加密:

const crypto = require("crypto");

const secret = crypto.randomBytes(32); // 密钥
const content = "hello world!"; // 要加密的明文

const cipher = crypto.createCipheriv(
    "aes-256-cbc",
    secret,
    Buffer.alloc(16, 0)
);
cipher.update(content, "utf8");
// 加密后的结果:e2a927165757acc609a89c093d8e3af5
console.log(cipher.final("hex"));

注意:在使用加密算法的时候,给定的密钥长度是有要求的,否则会爆出this[kHandle].initiv(cipher, credential, iv, authTagLength); Error: Invalid key length...的错误。以 aes-256-cbc 算法为例,需要 256 bits = 32 bytes 大小的密钥。同样地,AES 的 IV 也是有要求的,需要 128bits。(请参考“参考链接”部分)

使用 32 个连续I作为密钥,用 aes-256-cbc 加密后的结果是 a061e67f5643d948418fdb150745f24d。下面是逆向解密的过程:

const secret = "I".repeat(32);
const decipher = crypto.createDecipheriv(
    "aes-256-cbc",
    secret,
    Buffer.alloc(16, 0)
);
decipher.update("a061e67f5643d948418fdb150745f24d", "hex");
console.log(decipher.final("utf8")); // 解密后的结果:hello world!

非对称加密解密(RSA)

借助 openssl 生成私钥和公钥:

# 生成私钥
openssl genrsa -out privatekey.pem 1024
# 生成公钥
openssl rsa -in privatekey.pem -pubout -out publickey.pem

hello world! 加密和解密的代码如下:

const crypto = require("crypto");
const fs = require("fs");

const privateKey = fs.readFileSync("./privatekey.pem");
const publicKey = fs.readFileSync("./publickey.pem");

const content = "hello world!"; // 待加密的明文内容

// 公钥加密
const encodeData = crypto.publicEncrypt(publicKey, Buffer.from(content));
console.log(encodeData.toString("base64"));
// 私钥解密
const decodeData = crypto.privateDecrypt(privateKey, encodeData);
console.log(decodeData.toString("utf8"));

签名和验证算法

除了不可逆的哈希算法、数据加密算法,还有专门用于签名和验证的算法。这里也需要用 openssl 生成公钥和私钥。

代码示范如下:

const crypto = require("crypto");
const fs = require("fs");
const assert = require("assert");

const privateKey = fs.readFileSync("./privatekey.pem");
const publicKey = fs.readFileSync("./publickey.pem");

const data = "传输的数据";

// 第一步:用私钥对传输的数据,生成对应的签名
const sign = crypto.createSign("sha256");
// 添加数据
sign.update(data, "utf8");
sign.end();
// 根据私钥,生成签名
const signature = sign.sign(privateKey, "hex");

// 第二步:借助公钥验证签名的准确性
const verify = crypto.createVerify("sha256");
verify.update(data, "utf8");
verify.end();
assert.ok(verify.verify(publicKey, signature, "hex"));

从前面这段代码可以看到,利用私钥进行加密,得到签名值;最后利用公钥进行验证。

总结

之前一直是一知半解,一些概念很模糊,经常混淆散列算法和加密算法。整理完这篇笔记,我才理清楚了常见的加密算法的功能和用途。

除此之外,crypto 模块还提供了其他算法工具,例如 ECDH 在区块链中有应用。这篇文章没有再记录,感兴趣的同学可以去查阅相关资料。

参考链接

👇扫码关注「心谭博客」,查看「前端图谱」&「算法题解」,坚持分享,共同成长👇

查看原文

赞 4 收藏 3 评论 1

心谭 发布了文章 · 2月2日

深入Node.js的进程与子进程:从文档到实践

欢迎关注Github仓库,这是一个自2018年起持续更新的前端&算法开源博客。目前已有node学习、js面试笔记、css3动画设计、webpack4系列教程、设计模式、剑指offer·js版等多个系列。

仓库地址:https://github.com/dongyuanxin/blog

进程:process模块

process 模块是 nodejs 提供给开发者用来和当前进程交互的工具,它的提供了很多实用的 API。从文档出发,管中窥豹,进一步认识和学习 process 模块:

  • 如何处理命令参数?
  • 如何处理工作目录?
  • 如何处理异常?
  • 如何处理进程退出?
  • process 的标准流对象
  • 深入理解 process.nextTick

如何处理命令参数?

命令行参数指的是 2 个方面:

  • 传给 node 的参数。例如 node --harmony script.js --version 中,--harmony 就是传给 node 的参数
  • 传给进程的参数。例如 node script.js --version --help 中,--version --help 就是传给进程的参数

它们分别通过 process.argvprocess.execArgv 来获得。

如何处理工作目录?

通过process.cwd()可以获取当前的工作目录。

通过process.chdir(directory)可以切换当前的工作目录,失败后会抛出异常。实践如下:

function safeChdir(dir) {
    try {
        process.chdir(dir);
        return true;
    } catch (error) {
        return false;
    }
}

如何处理异常?

uncaughtException 事件

Nodejs 可以通过 try-catch 来捕获异常。如果异常未捕获,则会一直从底向事件循环冒泡。如是冒泡到事件循环的异常没被处理,那么就会导致当前进程异常退出。

根据文档,可以通过监听 process 的 uncaughtException 事件,来处理未捕获的异常:

process.on("uncaughtException", (err, origin) => {
    console.log(err.message);
});

const a = 1 / b;
console.log("abc"); // 不会执行

上面的代码,控制台的输出是:b is not defined。捕获了错误信息,并且进程以0退出。开发者可以在 uncaughtException 事件中,清除一些已经分配的资源(文件描述符、句柄等),不推荐在其中重启进程。

unhandledRejection 事件

如果一个 Promise 回调的异常没有被.catch()捕获,那么就会触发 process 的 unhandledRejection 事件:

process.on("unhandledRejection", (err, promise) => {
    console.log(err.message);
});

Promise.reject(new Error("错误信息")); // 未被catch捕获的异常,交由unhandledRejection事件处理

warning 事件

告警不是 Node.js 和 Javascript 错误处理流程的正式组成部分。 一旦探测到可能导致应用性能问题,缺陷或安全隐患相关的代码实践,Node.js 就可发出告警。

比如前一段代码中,如果出现未被捕获的 promise 回调的异常,那么就会触发 warning 事件。

如何处理进程退出?

process.exit() vs process.exitCode

一个 nodejs 进程,可以通过 process.exit() 来指定退出代码,直接退出。不推荐直接使用 process.exit(),这会导致事件循环中的任务直接不被处理,以及可能导致数据的截断和丢失(例如 stdout 的写入)。

setTimeout(() => {
    console.log("我不会执行");
});

process.exit(0);

正确安全的处理是,设置 process.exitCode,并允许进程自然退出。

setTimeout(() => {
    console.log("我不会执行");
});

process.exitCode = 1;

beforeExit 事件

用于处理进程退出的事件有:beforeExit 事件 和 exit 事件。

当 Node.js 清空其事件循环并且没有其他工作要安排时,会触发 beforeExit 事件。例如在退出前需要一些异步操作,那么可以写在 beforeExit 事件中:

let hasSend = false;
process.on("beforeExit", () => {
    if (hasSend) return; // 避免死循环

    setTimeout(() => {
        console.log("mock send data to serve");
        hasSend = true;
    }, 500);
});

console.log(".......");
// 输出:
// .......
// mock send data to serve

注意:在 beforeExit 事件中如果是异步任务,那么又会被添加到任务队列。此时,任务队列完成所有任务后,又回触发 beforeExit 事件。因此,不处理的话,可能出现死循环的情况。如果是显式调用 exit(),那么不会触发此事件。

exit 事件

在 exit 事件中,只能执行同步操作。在调用 'exit' 事件监听器之后,Node.js 进程将立即退出,从而导致在事件循环中仍排队的任何其他工作被放弃。

process 的标准流对象

process 提供了 3 个标准流。需要注意的是,它们有些在某些时候是同步阻塞的(请见文档)。

  • process.stderr:WriteStream 类型,console.error的底层实现,默认对应屏幕
  • process.stdout:WriteStream 类型,console.log的底层实现,默认对应屏幕
  • process.stdin:ReadStream 类型,默认对应键盘输入

下面是基于“生产者-消费者模型”的读取控制台输入并且及时输出的代码:

process.stdin.setEncoding("utf8");

process.stdin.on("readable", () => {
    let chunk;
    while ((chunk = process.stdin.read()) !== null) {
        process.stdout.write(`>>> ${chunk}`);
    }
});

process.stdin.on("end", () => {
    process.stdout.write("结束");
});

关于事件的含义,还是请看stream 的文档

深入理解 process.nextTick

我第一次看到 process.nextTick 的时候是比较懵的,看文档可以知道,它的用途是:把回调函数作为微任务,放入事件循环的任务队列中。但这么做的意义是什么呢?

因为 nodejs 并不适合计算密集型的应用,一个进程就一个线程,在当下时间点上,就一个事件在执行。那么,如果我们的事件占用了很多 cpu 时间,那么之后的事件就要等待非常久。所以,nodejs 的一个编程原则是尽量缩短每一个事件的执行事件。process.nextTick 的作用就在这,将一个大的任务分解成多个小的任务。示例代码如下:

// 被拆分成2个函数执行
function BigThing() {
    doPartThing();

    process.nextTick(() => finishThing());
}

在事件循环中,何时执行 nextTick 注册的任务呢?请看下面的代码:

setTimeout(function() {
    console.log("第一个1秒");
    process.nextTick(function() {
        console.log("第一个1秒:nextTick");
    });
}, 1000);

setTimeout(function() {
    console.log("第2个1秒");
}, 1000);

console.log("我要输出1");

process.nextTick(function() {
    console.log("nextTick");
});

console.log("我要输出2");

输出的结果如下,nextTick 是早于 setTimeout:

我要输出1
我要输出2
nextTick
第一个1秒
第一个1秒:nextTick
第2个1秒

在浏览器端,nextTick 会退化成 setTimeout(callback, 0)。但在 nodejs 中请使用 nextTick 而不是 setTimeout,前者效率更高,并且严格来说,两者创建的事件在任务队列中顺序并不一样(请看前面的代码)。

子进程:child_process模块

掌握 nodejs 的 child_process 模块能够极大提高 nodejs 的开发能力,例如主从进程来优化 CPU 计算的问题,多进程开发等等。本文从以下几个方面介绍 child_process 模块的使用:

  • 创建子进程
  • 父子进程通信
  • 独立子进程
  • 进程管道

创建子进程

nodejs 的 child_process 模块创建子进程的方法:spawn, fork, exec, execFile。它们的关系如下:

  • fork, exec, execFile 都是通过 spawn 来实现的。
  • exec 默认会创建 shell。execFile 默认不会创建 shell,意味着不能使用 I/O 重定向、file glob,但效率更高。
  • spawn、exec、execFile 都有同步版本,可能会造成进程阻塞。

child_process.spawn()的使用:

const { spawn } = require("child_process");
// 返回ChildProcess对象,默认情况下其上的stdio不为null
const ls = spawn("ls", ["-lh"]);

ls.stdout.on("data", data => {
    console.log(`stdout: ${data}`);
});

ls.stderr.on("data", data => {
    console.error(`stderr: ${data}`);
});

ls.on("close", code => {
    console.log(`子进程退出,退出码 ${code}`);
});

child_process.exec()的使用:

const { exec } = require("child_process");
// 通过回调函数来操作stdio
exec("ls -lh", (err, stdout, stderr) => {
    if (err) {
        console.error(`执行的错误: ${err}`);
        return;
    }
    console.log(`stdout: ${stdout}`);
    console.error(`stderr: ${stderr}`);
});

父子进程通信

fork()返回的 ChildProcess 对象,监听其上的 message 事件,来接受子进程消息;调用 send 方法,来实现 IPC。

parent.js 代码如下:

const { fork } = require("child_process");
const cp = fork("./sub.js");
cp.on("message", msg => {
    console.log("父进程收到消息:", msg);
});
cp.send("我是父进程");

sub.js 代码如下:

process.on("message", m => {
    console.log("子进程收到消息:", m);
});

process.send("我是子进程");

运行后结果:

父进程收到消息: 我是子进程
子进程收到消息: 我是父进程

独立子进程

在正常情况下,父进程一定会等待子进程退出后,才退出。如果想让父进程先退出,不受到子进程的影响,那么应该:

  • 调用 ChildProcess 对象上的unref()
  • options.detached 设置为 true
  • 子进程的 stdio 不能是连接到父进程

main.js 代码如下:

const { spawn } = require("child_process");
const subprocess = spawn(process.argv0, ["sub.js"], {
    detached: true,
    stdio: "ignore"
});

subprocess.unref();

sub.js 代码如下:

setInterval(() => {}, 1000);

进程管道

options.stdio 选项用于配置在父进程和子进程之间建立的管道。 默认情况下,子进程的 stdin、 stdout 和 stderr 会被重定向到 ChildProcess 对象上相应的 subprocess.stdin、subprocess.stdout 和 subprocess.stderr 流。 这意味着可以通过监听其上的 data事件,在父进程中获取子进程的 I/O 。

可以用来实现“重定向”:

const fs = require("fs");
const child_process = require("child_process");

const subprocess = child_process.spawn("ls", {
    stdio: [
        0, // 使用父进程的 stdin 用于子进程。
        "pipe", // 把子进程的 stdout 通过管道传到父进程 。
        fs.openSync("err.out", "w") // 把子进程的 stderr 定向到一个文件。
    ]
});

也可以用来实现"管道运算符":

const { spawn } = require("child_process");

const ps = spawn("ps", ["ax"]);
const grep = spawn("grep", ["ssh"]);

ps.stdout.on("data", data => {
    grep.stdin.write(data);
});

ps.stderr.on("data", err => {
    console.error(`ps stderr: ${err}`);
});

ps.on("close", code => {
    if (code !== 0) {
        console.log(`ps 进程退出,退出码 ${code}`);
    }
    grep.stdin.end();
});

grep.stdout.on("data", data => {
    console.log(data.toString());
});

grep.stderr.on("data", data => {
    console.error(`grep stderr: ${data}`);
});

grep.on("close", code => {
    if (code !== 0) {
        console.log(`grep 进程退出,退出码 ${code}`);
    }
});

参考链接

放在最后

  1. 觉得不错,帮忙点个赞呗,您的支持是对我最大的激励
  2. 欢迎我的公众号:「心谭博客」,只专注于前端 + 算法的原创分享

查看原文

赞 6 收藏 4 评论 2

心谭 赞了文章 · 2月1日

趁着疫情在家学点什么?

首先向奋战在前线生死逆行的医护人员致敬。

瘟疫属于天灾,个人在灾害面前显得很渺小。我们能做的就是保护好自己,照顾好家人,出门戴口罩,回家就洗手、手腕。当然,武汉某些部门的不作为在某种程度上可以说是“人祸”,网上声讨的文章已经铺天盖地了,作为技术号我就不多说了。

有人说2019是过去十年最差的一年,但却是未来十年最好的一年。虽然2019年很多企业经历了生存危机,很多企业裁员,2020年初又爆发肺炎疫情,但我依然觉得这种观点有点太过杞人忧天了。有人说快速变化的市场是高效的市场。还记得2000年的互联网泡沫吗?其实,所谓的互联网泡沫,正是互联网的崛起,现存的互联网巨头多是在那个时期崛起的。在快速变化的浪潮中,我们作为渺小的个体唯一能做的就是拥抱变化,努力进步。

2020年已经过去1/12了,其实12是一个很小的分母,不知道你的新年计划是否完成了1/12。国务院下发了假期延长的通知,程序员只要有电脑在哪都一样办公,所以大家大概率可以远程办公。但不要以为这种日子会持续太久,我的朋友圈有人粗略的算过,400个员工的企业,一个月工资200万,房租130万,储藏期很短的备货100多万,税收定额50万,水电费损耗20万。停工一个月损失500万。资本家是不愿眼睁睁的看着利润这么损失掉的。

学英语

学英语可以进外企。我想大部分人是不知道Facebook每年都会在中国招人的,Microsoft 2019年在苏州有大量招人的名额,还有Amazon、Hulu还有很多大家不常听说的外国公司。据我的观察,外企和国内公司似乎处在平行的世界。大家对BAT很了解,并且以此为努力的目标,殊不知除了996我们还有很多选择。

学英语对提升技术有帮助。虽然中国掌握了海量数据处理的最佳实践,但最前沿的技术却是在国外,用英文发表。学习英文可以第一时间掌握技术动态,让你具备先发优势。这是英语带来的时效性。
技术论文大多用英文发表,其实很多国内团队的研发成果也是通过英文发表的,要想精深技术阅读技术理论的最原始出处是很有必要的。这是英语对技术深度的帮助。

除了背单词,看美剧,这些大家都推荐的学习英文方式,我们作为程序员可以看英文的技术博客。比如某个框架的官方网站、Medium、Twitter、Facebook都是可以的。

学数学

我推荐《计算机科学中的数学》。这本书是谷歌和麻省理工工学院联合发布的,够大牌,够权威,而且这本书讲的数学是真正与程序员这个职业高度相关的。
这本书的英文版在MIT的官网可以下载,是免费的。知道学习英语的好处了吧,至少还可以省钱。

《Mathematics for Computer Science》:https://courses.csail.mit.edu...

在谷歌,很多写代码出身的管理者对于理论基础的掌握都是非常扎实的,甚至可以说理论基础与晋升正相关。技术是手段不是目的,框架只是工具,而只会使用框架是堆叠业务逻辑的机器。为什么很多外国公司的程序员可以干到50岁60岁,其根本是程序员与软件工程师的区别。

很多人加班加点忙忙碌碌,却忽略了一个问题:自己是否是可替代的?这个问题至关重要,重要到已经涉及了程序员的生存。当企业举起裁员的大刀,你如何保证自己留下来?

前段时间我思考了一个问题,为什么很多公司喜欢重复造轮子?
这在根本上是使用框架和自主研发的对比。框架可以节省时间、提高效率,但正因为框架的普适性,在需要个性化定制方案时就显得不够灵活,甚至在性能方面都不能满足业务需求。这时候很多有实力的企业会选择自主研发,当它们造的轮子足够好以至于超越技术框架时,它们就积累了足够的技术壁垒。这就是巨头企业之所以成为巨头的原因。它们在自己擅长的领域始终领先于对手。
很多人不愿意钻研基础,才会与世界一流的企业无缘。

有用的书我推荐一本就够了,多了你也不看。

阅读技术无关的书籍

这里我推荐吴军的书,应该有很多人看过他写的《数学之美》。我推荐他的书首先是因为他有技术背景,其次是他比较成功,吴军作为计算机科学家和知名投资人,他的工程经历对程序员有很多借鉴意义,他见识广博、博闻强记。如果我们把自己职业生涯的时间线拉的足够长,可能你会发现自己的长期目标与他的职业生涯有相似的地方。

《浪潮之巅》《硅谷之谜》《见识》《态度》《格局》这些书大家可以随便买着看看。我不建议大家通过知乎会员或者得到会员免费看,我建议大家买Kindle的电子书或者纸质书籍,你可以反复的看。当你处在人生的不同阶段思考的问题的角度可能差异很大,当你有机会再次读一遍同样的书会有不同的收获。

这些书是指引方向的。纪伯伦在《先知》里提到,“我们走的太远,却忘了为什么而出发。”当你不迷惑、有方向时,做事才有动力,做事才有效率。

结识框架作者

我在Github上看到很多优秀框架的参与者的关注人数并不多,甚至有些只有几个。你可以去关注他们,看看他们做的其他项目,参与哪些活动。当你了解了那些一流的人都在做什么才能认清自己与那些牛人之间的差距。如果可能的话,甚至可以去结识他们。与牛人做朋友总是有好处的。

渠道的话,我推荐Github,Linkedin,Twitter,Facebook等……

这里不展开说了。

娱乐

纪录片。地球的文明,星际的探索,自然的和谐,城市的崛起…… 这些远比那些偶像剧有意义的多,至少它们是尊重事实、论证严谨的,除了能满足我们自己的好奇心同时也增长我们的见识。

闲着。发呆也好,思考也好,总之什么都别做。让大脑休息,让身体放松,等待灵感自己蹦出来。别相信网络上所谓的利用碎片时间。利用碎片时间学不到什么东西的,最有效的学习方式就是利用整段的时间学习。你的碎片时间除了满足了各路营销的KPI之外什么都收获不了,反而让整个人变得焦虑。

时间是有限的,金钱是无限的。你最重要,爱惜自己。

查看原文

赞 5 收藏 2 评论 0

心谭 发布了文章 · 1月29日

深入Nodejs模块fs - 文件系统操作

node 的fs文档密密麻麻的 api 非常多,毕竟全面支持对文件系统的操作。文档组织的很好,操作基本分为文件操作、目录操作、文件信息、流这个大方面,编程方式也支持同步、异步和 Promise。

本文记录了几个文档中没详细描写的问题,可以更好地串联fs文档思路:

  • 文件描述符
  • 同步、异步与 Promise
  • 目录与目录项
  • 文件信息
  • stream
🔍 关注公众号“心谭博客” / 👉 前往 xxoo521.com / 欢迎交流和指正

文件描述符

文件描述符是一个非负整数。它是一个索引值,操作系统可以根据它来找到对应的文件。

在 fs 的很多底层 api 中,需要用到文件描述符。在文档中,描述符通常用fd来代表。例如:fs.read(fd, buffer, offset, length, position, callback)。与这个 api 相对应的是:fs.readFile(path[, options], callback)

因为操作系统对文件描述符的数量有限制,因此在结束文件操作后,别忘记 close:

const fs = require("fs");

fs.open("./db.json", "r", (err, fd) => {
    if (err) throw err;
    // 文件操作...
    // 完成操作后,关闭文件
    fs.close(fd, err => {
        if (err) throw err;
    });
});

同步、异步与 Promise

所有文件系统的 api 都有同步和异步两种形式。

同步写法

不推荐使用同步 api,会阻塞线程

try {
    const buf = fs.readFileSync("./package.json");
    console.log(buf.toString("utf8"));
} catch (error) {
    console.log(error.message);
}

异步写法

异步写法写起来容易进入回调地狱。

fs.readFile("./package.json", (err, data) => {
    if (err) throw err;
    console.log(data.toString("utf8"));
});

(推荐)Promise 写法

在 node v12 之前,需要自己借助 promise 封装:

function readFilePromise(path, encoding = "utf8") {
    const promise = new Promise((resolve, reject) => {
        fs.readFile(path, (err, data) => {
            if (err) return reject(err);
            return resolve(data.toString(encoding));
        });
    });
    return promise;
}

readFilePromise("./package.json").then(res => console.log(res));

在 node v12 中,引入了 fs Promise api。它们返回 Promise 对象而不是使用回调。 API 可通过 require('fs').promises 访问。如此一来,开发成本更低了。

const fsPromises = require("fs").promises;

fsPromises
    .readFile("./package.json", {
        encoding: "utf8",
        flag: "r"
    })
    .then(console.log)
    .catch(console.error);

目录与目录项

fs.Dir 类:封装了和文件目录相关的操作

fs.Dirent 类:封装了目录项的相关操作。例如判断设备类型(字符、块、FIFO 等)。

它们之间的关系,通过代码展示:

const fsPromises = require("fs").promises;

async function main() {
    const dir = await fsPromises.opendir(".");
    let dirent = null;
    while ((dirent = await dir.read()) !== null) {
        console.log(dirent.name);
    }
}

main();

文件信息

fs.Stats 类:封装了文件信息相关的操作。它在fs.stat()的回调函数中返回。

fs.stat("./package.json", (err, stats) => {
    if (err) throw err;
    console.log(stats);
});

注意,关于检查文件是否存在:

  • 不建议在调用 fs.open()、 fs.readFile() 或 fs.writeFile() 之前使用 fs.stat() 检查文件是否存在。而是应该直接打开、读取或写入文件,如果文件不可用则处理引发的错误
  • 要检查文件是否存在但随后并不对其进行操作,则建议使用 fs.access()。

ReadStream 与 WriteStream

在 nodejs 中,stream 是个非常重要的库。很多库的 api 都是基于 stream 来封装的。例如下面要说的 fs 中的 ReadStream 和 WriteStream。

fs 本身提供了 readFile 和 writeFile,它们好用的代价就是性能有问题,会将内容一次全部载入内存。但是对于几 GB 的大文件,显然会有问题。

那么针对大文件的解决方案自然是:一点点读出来。这就需要用到 stream 了。以 readStream 为例,代码如下:

const rs = fs.createReadStream("./package.json");
let content = "";

rs.on("open", () => {
    console.log("start to read");
});

rs.on("data", chunk => {
    content += chunk.toString("utf8");
});

rs.on("close", () => {
    console.log("finish read, content is:\n", content);
});

借助 stream 的 pipe,一行快速封装一个大文件的拷贝函数:

function copyBigFile(src, target) {
    fs.createReadStream(src).pipe(fs.createWriteStream(target));
}

参考链接


👇扫码关注,查看「前端图谱」&「算法题解」,您的支持是我更新的动力👇

查看原文

赞 1 收藏 0 评论 0

心谭 发布了文章 · 1月19日

深入NodeJS模块系列 - os

读了 os 模块的文档,研究了几个有意思的问题:

  • 🤔 识别操作系统平台
  • 🤔 理解和计算“平均负载”
  • 🤔 理解和计算“cpu 使用率”
  • 🤔 理解和计算“内存使用率”
  • 🤔 查看运行时间
🔍 关注公众号“心谭博客” / 👉 查看原文: xxoo521.com / 欢迎交流和指正

识别操作系统平台

nodejs 提供了os.platform()os.type(),可以用来识别操作系统平台。推荐使用: os.platform()

理解和计算“平均负载”

平均负载是指:单位时间内,系统处于可运行状态和不可中断状态的平均进程数。它和 cpu 使用率没有直接关系。

其中,这里的可运行状态指的是:正在使用 cpu 或正在等待 cpu 的进程。不可中断状态指的是:内核态关键流程中的进程。

在 nodejs 中,直接调用os.loadavg()可以获得 1、5 和 15 分钟的平均负载,它和 unix 命令uptime返回值一样。

为什么需要关心平均负载这个问题呢?因为进程分为 2 种,第一种就是“CPU 密集型”,它的 cpu 使用率和平均负载都是高的;第二种是“IO 密集型”,它的 cpu 使用率不一定高,但是等待 IO 会造成平均负载高。所以,cpu 使用率和平均负载共同反应系统性能。

平均活跃进程数最理想的状态是 cpu 数量=平均负载,如果 cpu 数量 < 平均负载,那么平均负载过高。

// 判断是否平均负载过高
function isHighLoad() {
    const cpuNum = os.cpus().length;
    return os.loadavg().map(item => item > cpuNum);
}

理解和计算“cpu 使用率”

很多监控软件都提供针对 cpu 使用率的“实时”监控,当然这个实时不是真的实时,有个时间差。这个功能,nodejs 如何实现呢?

第一步:封装getCPUInfo(),计算获取 cpu 花费的总时间与空闲模式花费的时间。

/**
 * 获取cpu花费的总时间与空闲模式的时间
 */
function getCPUInfo() {
    const cpus = os.cpus();
    let user = 0,
        nice = 0,
        sys = 0,
        idle = 0,
        irq = 0,
        total = 0;

    cpus.forEach(cpu => {
        const { times } = cpu;
        user += times.user;
        nice += times.nice;
        sys += times.sys;
        idle += times.idle;
        irq += times.irq;
    });

    total = user + nice + sys + idle + irq;

    return {
        total,
        idle
    };
}

第二步:当前时间点 t1,选定一个时间差 intervel,计算 t1 和 t1 + interval 这两个时间点的 cpu 时间差与空闲模式时间差,返回 1 - 空闲时间差 / cpu时间差。返回的结果就是时间差 intervel 内的平均 cpu 使用率。

function getCPUUsage(interval = 1000) {
    const startInfo = getCPUInfo();

    return new Promise(resolve => {
        setTimeout(() => {
            const endInfo = getCPUInfo();

            const idleDiff = startInfo.idle - endInfo.idle;
            const totalDiff = startInfo.total - endInfo.total;
            resolve(1 - Math.abs(idleDiff / totalDiff));
        }, interval);
    });
}

使用方式如下:

getCPUUsage().then(usage => console.log("cpu使用率:", usage));

理解和计算“内存使用率”

cpu 的指标有平均负载、cpu 使用率,内存的指标有内存使用率。

借助 nodejs 接口,实现非常简单:

function getMemUsage() {
    return 1 - os.freemem() / os.totalmem();
}

查看运行时间

  • nodejs 运行时间:process.uptime()
  • 系统运行时间:os.uptime()

参考链接


专注前端与算法的系列干货分享,欢迎关注(¬‿¬)

查看原文

赞 7 收藏 4 评论 0

心谭 发布了文章 · 1月18日

有趣的NodeJS模块 - Buffer

Buffer 作为 nodejs 中重要的概念和功能,为开发者提供了操作二进制的能力。本文记录了几个问题,来加深对 Buffer 的理解和使用:

  • 认识缓冲器
  • 如何申请堆外内存
  • 如何计算字节长度
  • 如何计算字节长度
  • 如何转换字符编码
  • 理解共享内存与拷贝内存
🔍 关注公众号“心谭博客” / 👉 查看原文: xxoo521.com / 欢迎交流和指正

认识 Buffer(缓冲器)

Buffer 是 nodejs 核心 API,它提供我们处理二进制数据流的功能。Buffer 的使用和 ES2017 的 Uint8Array 非常相似,但由于 node 的特性,专门提供了更深入的 api。

Uint8Array 的字面意思就是:8 位无符号整型数组。一个字节是 8bit,而字节的表示也是由两个 16 进制(4bit)的数字组成的。

const buf = Buffer.alloc(1);
console.log(buf); // output: <Buffer 00>

如何申请堆外内存

Buffer 可以跳出 nodejs 对堆内内存大小的限制。nodejs12 提供了 4 种 api 来申请堆外内存:

  • Buffer.from()
  • Buffer.alloc(size[, fill[, encoding]])
  • Buffer.allocUnsafe(size)
  • Buffer.allocUnsafeSlow(size)

Buffer.alloc vs Buffer.allocUnsafe

在申请内存时,可能这片内存之前存储过其他数据。如果不清除原数据,那么会有数据泄漏的安全风险;如果清除原数据,速度上会慢一些。具体用哪种方式,根据实际情况定。

  • Buffer.alloc:申请指定大小的内存,并且清除原数据,默认填充 0
  • Buffer.allocUnsafe:申请指定大小内存,但不清除原数据,速度更快

根据提供的 api,可以手动实现一个alloc

function pollifyAlloc(size, fill = 0, encoding = "utf8") {
    const buf = Buffer.allocUnsafe(size);
    buf.fill(fill, 0, size, encoding);
    return buf;
}

Buffer.allocUnsafe vs Buffer.allocUnsafeSlow

从命名上可以直接看出效果,Buffer.allocUnsafeSlow更慢。因为当使用 Buffer.allocUnsafe 创建新的 Buffer 实例时,如果要分配的内存小于 4KB,则会从一个预分配的 Buffer 切割出来。 这可以避免垃圾回收机制因创建太多独立的 Buffer 而过度使用。

这种方式通过消除跟踪和清理的需要来改进性能和内存使用。

如何计算字节长度

利用 Buffer,可以获得数据的真实所占字节。例如一个汉字,它的字符长度是 1。但由于是 utf8 编码的汉字,所以占用 3 个字节。

直接利用Buffer.byteLength()可以获得字符串指定编码的字节长度:

const str = "本文原文地址: xxoo521.com";

console.log(Buffer.byteLength(str, "utf8")); // output: 31
console.log(str.length); // output: 19

也可以直接访问 Buffer 实例的 length 属性(不推荐):

console.log(Buffer.from(str, "utf8").length); // output: 31

如何转换字符编码

Nodejs 当前支持的编码格式有:ascii、utf8、utf16le、ucs2、base64、latin1、binary、hex。其他编码需要借助三方库来完成。

下面,是用Buffer.from()buf.toString()来封装的 nodejs 平台的编码转换函数:

function trans(str, from = "utf8", to = "utf8") {
    const buf = Buffer.from(str, from);
    return buf.toString(to);
}

// output: 5Y6f5paH5Zyw5Z2AOiB4eG9vNTIxLmNvbQ==
console.log(trans("原文地址: xxoo521.com", "utf8", "base64"));

共享内存与拷贝内存

在生成 Buffer 实例,操作二进制数据的时候,千万要注意接口是基于共享内存,还是基于拷贝底层内存。

例如对于生成 Buffer 实例的from(),不同类型的参数,nodejs 底层的行为是不同的。

为了更形象地解释,请看下面两段代码。

代码 1

const buf1 = Buffer.from("buffer");
const buf2 = Buffer.from(buf1); // 拷贝参数中buffer的数据到新的实例
buf1[0]++;

console.log(buf1.toString()); // output: cuffer
console.log(buf2.toString()); // output: buffer

代码 2

const arr = new Uint8Array(1);
arr[0] = 97;

const buf1 = Buffer.from(arr.buffer);
console.log(buf1.toString()); // output: a

arr[0] = 98;
console.log(buf1.toString()); // output: b

在第二段代码中,传入Buffer.from的参数类型是arrayBuffer。因此Buffer.from仅仅是创建视图,而不是拷贝底层内存。buf1 和 arr 的内存是共享的。

在操作 Buffer 的过程中,需要特别注意共享和拷贝的区别,发生错误比较难排查。

参考链接


专注前端与算法的系列干货分享,欢迎关注(¬‿¬)

查看原文

赞 5 收藏 4 评论 0

心谭 发布了文章 · 1月10日

有趣的Nodejs模块之events

读了 events 模块的文档,研究了几个有意思的问题:

  • 🤔️ 事件驱动模型
  • 🤔️ 优雅的错误处理
  • 🤔️ 监听器器队列顺序处理
  • 🤔️ 内存管理与防止泄漏
引用/转载 请声明出处:原文链接: xxoo521.com

事件驱动模型

Nodejs 使用了一个事件驱动、非阻塞 IO 的模型。events模块是事件驱动的核心模块。很多内置模块都继承了events.EventEmitter

自己无需手动实现这种设计模式,直接继承EventEmitter即可。代码如下:

const { EventEmitter } = require("events");

class MyEmitter extends EventEmitter {}

const ins = new MyEmitter();
ins.on("test", () => {
    console.log("emit test event");
});
ins.emit("test");

优雅的错误处理

根据文档,应该 EventEmitter 实例的error事件是个特殊事件。推荐做法是:在创建实例后,应该立即注册error事件。

const ins = new MyEmitter();
ins.on("error", error => {
    console.log("error msg is", error.message);
});

注册error事件后,我原本的理解是,所有事件回掉逻辑中的错误都会在 EventEmitter 内部被捕获,并且在内部触发 error 事件。

也就是说下面代码,会打印:"error msg is a is not defined"。

ins.on("test", () => {
    console.log(a);
});

ins.emit("test");

然而,错误并没有捕获,直接抛出了异常。由此可见,EventEmitter 在执行内部逻辑的时候,并没有try-catch。这个原因,请见Node Issue。简单来讲,Error 和 Exception 并不完全一样。

如果按照正常想法,不想每一次都在外面套一层try-catch,那应该怎么做呢?我的做法是在
EventEmitter 原型链上新增一个safeEmit函数。

EventEmitter.prototype.safeEmit = function(name, ...args) {
    try {
        return this.emit(name, ...args);
    } catch (error) {
        return this.emit("error", error);
    }
};

如此一来,运行前一段代码的 Exception 就会被捕获到,并且触发error事件。前一段代码的输出就变成了:

error msg is a is not defined

监听器队列顺序处理

对于同一个事件,触发它的时候,函数的执行顺序就是函数绑定时候的顺序。官方库提供了emitter.prependListener()emitter.prependOnceListener() 两个接口,可以让新的监听器直接添加到队列头部。

但是如果想让新的监听器放入任何监听器队列的任何位置呢?在原型链上封装了 insertListener 方法。

EventEmitter.prototype.insertListener = function(
    name,
    index,
    callback,
    once = false
) {
    // 如果是once监听器,其数据结构是 {listener: Function}
    // 正常监听器,直接是 Function
    const listeners = ins.rawListeners(name);
    const that = this;
    // 下标不合法
    if (index > listeners.length || index < 0) {
        return false;
    }
    // 绑定监听器数量已达上限
    if (listeners.length >= this.getMaxListeners()) {
        return false;
    }
    listeners.splice(index, 0, once ? { listener: callback } : callback);
    this.removeAllListeners(name);
    listeners.forEach(function(item) {
        if (typeof item === "function") {
            that.on(name, item);
        } else {
            const { listener } = item;
            that.once(name, listener);
        }
    });
    return true;
};

使用起来,效果如下:

const ins = new MyEmitter();
ins.on("error", error => {
    console.log("error msg is", error.message);
});

ins.on("test", () => {
    console.log("test 1");
});

ins.on("test", () => {
    console.log("test 2");
});

// 监听器队列中插入新的监听器,一个是once类型,一个不是once类型
ins.insertListener(
    "test",
    0,
    () => {
        console.log("once test insert");
    },
    true
);
ins.insertListener("test", 1, () => {
    console.log("test insert");
});

连续调用两次ins.emit("test"),结果输出如下:

# 第一次
once test insert
test insert
test 1
test 2
# 第二次: once 类型的监听器调用一次后销毁
test insert
test 1
test 2

内存管理与防止泄漏

在绑定事件监听器的时候,如果监听器没有被 remove,那么存在内存泄漏的风险。

我知道的常见做法如下:

  • 经常 CR,移除不需要的事件监听器
  • 通过once绑定监听器,调用一次后,监听器被自动移除
  • [推荐]hack 一个更安全的EventEmitter

参考链接


专注前端与算法的系列干货分享,欢迎关注(¬‿¬)

查看原文

赞 2 收藏 2 评论 0

心谭 发布了文章 · 2019-12-22

剑指offer·JS版 | 重建二叉树

题目描述

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

解法 1: 递归

首先前序/后序遍历 + 中序遍历可以重建二叉树。题目考察的就是前序+中序来重建二叉树,后序+中序的思路是类似的。

例子与思路

假设有二叉树如下:

    1
   / \
  2   3
 / \
4   5

它的前序遍历的顺序是:1 2 4 5 3。中序遍历的顺序是:4 2 5 1 3

因为前序遍历的第一个元素就是当前二叉树的根节点。那么,这个值就可以将中序遍历分成 2 个部分。在以上面的例子,中序遍历就被分成了 4 2 53 两个部分。4 2 5就是左子树,3就是右子树。

最后,根据左右子树,继续递归即可。

专注前端与算法的系列干货分享,欢迎关注(¬‿¬):
「微信公众号:心谭博客」| xxoo521.com | GitHub

代码实现

// ac地址:https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6
// 原文地址:https://xxoo521.com/2019-12-21-re-construct-btree/

/* function TreeNode(x) {
    this.val = x;
    this.left = null;
    this.right = null;
} */

/**
 * @param {TreeNode} pre
 * @param {TreeNode} vin
 * @return {TreeNode}
 */
function reConstructBinaryTree(pre, vin) {
    if (!pre.length || !vin.length) {
        return null;
    }

    const rootVal = pre[0];
    const node = new TreeNode(rootVal);

    let i = 0; // i有两个含义,一个是根节点在中序遍历结果中的下标,另一个是当前左子树的节点个数
    for (; i < vin.length; ++i) {
        if (vin[i] === rootVal) {
            break;
        }
    }

    node.left = reConstructBinaryTree(pre.slice(1, i + 1), vin.slice(0, i));
    node.right = reConstructBinaryTree(pre.slice(i + 1), vin.slice(i + 1));
    return node;
}

专注前端与算法的系列干货分享,欢迎关注(¬‿¬)

查看原文

赞 1 收藏 1 评论 0

心谭 发布了文章 · 2019-12-22

剑指offer·JS版 | 从尾到头打印链表

题目描述

输入一个链表,按链表从尾到头的顺序返回一个 ArrayList。

解法 1: 栈

题目要求的是从尾到头。这种“后进先出”的访问顺序,自然想到了用栈。

时间复杂度 O(N),空间复杂度 O(N)。

// ac地址:https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035
// 原文地址:https://xxoo521.com/2019-12-21-da-yin-lian-biao/

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/

/**
 * @param {ListNode} head
 * @return {any[]}
 */
function printListFromTailToHead(head) {
    const stack = [];
    let node = head;
    while (node) {
        stack.push(node.val);
        node = node.next;
    }

    const reverse = [];
    while (stack.length) {
        reverse.push(stack.pop());
    }

    return reverse;
}
专注前端与算法的系列干货分享,欢迎关注(¬‿¬):
「微信公众号:心谭博客」| xxoo521.com | GitHub

发现后半段出栈的逻辑,其实就是将数组reverse反转。因此,借助 javascript 的 api,更优雅的写法如下:

// ac地址:https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035
// 原文地址:https://xxoo521.com/2019-12-21-da-yin-lian-biao/

/**
 * @param {ListNode} head
 * @return {any[]}
 */
function printListFromTailToHead(head) {
    const stack = [];
    let node = head;
    while (node) {
        stack.push(node.val);
        node = node.next;
    }

    return stack.reverse();
}

专注前端与算法的系列干货分享,欢迎关注(¬‿¬)

查看原文

赞 2 收藏 1 评论 4

心谭 发布了文章 · 2019-12-20

剑指offer·JS版 | 替换空格

题目描述

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为 We Are Happy.则经过替换之后的字符串为 We%20Are%20Happy。

解法 1:正则表达式

第一反应肯定正则表达式,在真正项目中,肯定也会选用正则来做匹配和替换。

// ac地址:https://www.nowcoder.com/practice/4060ac7e3e404ad1a894ef3e17650423
// 原文地址:https://xxoo521.com/2019-12-19-ti-huan-kong-ge/
/**
 * @param {string} str
 * @return {string}
 */
function replaceSpace(str) {
    return str.replace(/ /g, "%20");
}
专注前端与算法的系列干货分享,欢迎关注(¬‿¬):
「微信公众号:心谭博客」| xxoo521.com | GitHub

解法 2:双指针

因为字符串是不可变的,所以如果直接采用从头到尾遍历原字符串检查空格,并且做替换。那么每次检查到空格后,都需要重新生成字符串。整个过程时间复杂度是 O(N^2)。

优化的关键:提前计算替换后的字符串的长度,避免每次都对字符串做改动。

整体思路如下:

  1. 遍历原字符串,统计空格和非空格字符个数,计算替换后的新字符的长度
  2. 准备两个指针,指针 i 指向原字符串,指针 j 指向新字符串
  3. i 从头开始遍历原字符串

    • str[i]是非空格,那么将 i 指向的字符放入新字符串的 j 位置。i 和 j 都增加 1。
    • str[i]是空格,那么 j 指向的位置依次填入%20。i 增加 1,j 增加 3。

时间复杂度是 O(N)。因为需要对新字符串开辟容器,空间复杂度是 O(N)。

// ac地址:https://www.nowcoder.com/practice/4060ac7e3e404ad1a894ef3e17650423
// 原文地址:https://xxoo521.com/2019-12-19-ti-huan-kong-ge/
/**
 * @param {string} str
 * @return {string}
 */
function replaceSpace(str) {
    if (!str || !str.length) {
        return "";
    }

    let emptyNum = 0,
        chNum = 0;
    for (let i = 0; i < str.length; ++i) {
        if (str[i] === " ") {
            ++emptyNum;
        } else {
            ++chNum;
        }
    }

    const length = emptyNum * 2 + chNum;
    const chs = new Array(length);
    // i 是新字符串的下标
    // j 是原字符串的下标
    for (let i = 0, j = 0; j < str.length; ++j) {
        if (str[j] === " ") {
            chs[i++] = "%";
            chs[i++] = "2";
            chs[i++] = "0";
        } else {
            chs[i++] = str[j];
        }
    }

    return chs.join("");
}
专注前端与算法的系列干货分享,欢迎关注(¬‿¬)
查看原文

赞 1 收藏 1 评论 0

心谭 发布了文章 · 2019-12-20

剑指offer·JS | 二维数组中的查找

题目描述

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

解法 1:暴力法

遍历数组中的所有元素,找到是否存在。

时间复杂度是 O(N^2),空间复杂度是 O(1)

// ac地址:https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e
// 原文地址:https://xxoo521.com/2019-12-19-er-wei-shu-zu-cha-zhao/

/**
 *
 * @param {number} target
 * @param {number[][]} array
 */
function Find(target, array) {
    const rowNum = array.length;
    if (!rowNum) {
        return false;
    }
    const colNum = array[0].length;
    for (let i = 0; i < rowNum; i++) {
        for (let j = 0; j < colNum; j++) {
            if (array[i][j] === target) return true;
        }
    }

    return false;
}
专注前端与算法的系列干货分享,欢迎关注(¬‿¬):
「微信公众号:心谭博客」| xxoo521.com | GitHub

解法 2:观察数组规律

按照题目要求,数组的特点是:每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。考虑以下数组:

1 2 3
4 5 6
7 8 9

在其中寻找 5 是否存在。过程如下:

  1. 从右上角开始遍历
  2. 当前元素小于目标元素(3 < 5),根据数组特点,当前行中最大元素也小于目标元素,因此进入下一行
  3. 当前元素大于目标元素(6 > 5),根据数组特点,行数不变,尝试向前一列查找
  4. 找到 5

代码如下:

// ac地址:https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e
// 原文地址:https://xxoo521.com/2019-12-19-er-wei-shu-zu-cha-zhao/

/**
 *
 * @param {number} target
 * @param {number[][]} array
 */
function Find(target, array) {
    const rowNum = array.length;
    if (!rowNum) {
        return false;
    }
    const colNum = array[0].length;
    if (!colNum) {
        return false;
    }

    let row = 0,
        col = colNum - 1;
    while (row < rowNum && col >= 0) {
        if (array[row][col] === target) {
            return true;
        } else if (array[row][col] > target) {
            --col;
        } else {
            ++row;
        }
    }

    return false;
}

时间复杂度是 O(M+N),空间复杂度是 O(1)。其中 M 和 N 分别代表行数和列数。

专注前端与算法的系列干货分享,欢迎关注(¬‿¬)
查看原文

赞 1 收藏 1 评论 2

心谭 发布了文章 · 2019-12-13

「超全」手写Promise的相关方法

原文发布在:手写Promise的相关方法

摘要

Promise 作为 JS 社区的异步解决方案,为开发者提供了.then()Promise.resolve()Promise.reject()等基本方法。除此之外,为了更方便地组合和控制多个的 Promise 实例,也提供了.all().race()等方法。

本文会在 Promise 的基本方法上,手动实现更高级的方法,来加深对 Promise 的理解:

  • 🤔️ 实现Promise.all
  • 🤔️ 实现Promise.race
  • 🤔️ 实现Promise.any
  • 🤔️ 实现Promise.allSettled
  • 🤔️ 实现Promise.finally

⚠️ 完整代码和用例请到github.com/dongyuanxin/diy-promise

实现 Promise.all

过程

Promise.all(iterators)返回一个新的 Promise 实例。iterators 中包含外界传入的多个 promise 实例。

对于返回的新的 Promise 实例,有以下两种情况:

  • 如果传入的所有 promise 实例的状态均变为fulfilled,那么返回的 promise 实例的状态就是fulfilled,并且其 value 是 传入的所有 promise 的 value 组成的数组。
  • 如果有一个 promise 实例状态变为了rejected,那么返回的 promise 实例的状态立即变为rejected

代码实现

实现思路:

  • 传入的参数不一定是数组对象,可以是"遍历器"
  • 传入的每个实例不一定是 promise,需要用Promise.resolve()包装
  • 借助"计数器",标记是否所有的实例状态均变为fulfilled
Promise.myAll = function(iterators) {
  const promises = Array.from(iterators);
  const num = promises.length;
  const resolvedList = new Array(num);
  let resolvedNum = 0;

  return new Promise((resolve, reject) => {
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(value => {
          // 保存这个promise实例的value
          resolvedList[index] = value;
          // 通过计数器,标记是否所有实例均 fulfilled
          if (++resolvedNum === num) {
            resolve(resolvedList);
          }
        })
        .catch(reject);
    });
  });
};

实现 Promise.race

过程

Promise.race(iterators)的传参和返回值与Promise.all相同。但其返回的 promise 的实例的状态和 value,完全取决于:传入的所有 promise 实例中,最先改变状态那个(不论是fulfilled还是rejected)。

代码实现

实现思路:

  • 某传入实例pending -> fulfilled时,其 value 就是Promise.race返回的 promise 实例的 value
  • 某传入实例pending -> rejected时,其 error 就是Promise.race返回的 promise 实例的 error
Promise.myRace = function(iterators) {
  const promises = Array.from(iterators);

  return new Promise((resolve, reject) => {
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(resolve)
        .catch(reject);
    });
  });
};

实现 Promise.any

我是专注前端的技术博客 「xin-tan.com」 的作者。您可以通过Watch or Star文章仓库 「github.com/dongyuanxin/blog」,或关注公众号「心谭博客」来接收最新文章消息。

过程

Promise.any(iterators)的传参和返回值与Promise.all相同。

如果传入的实例中,有任一实例变为fulfilled,那么它返回的 promise 实例状态立即变为fulfilled;如果所有实例均变为rejected,那么它返回的 promise 实例状态为rejected

⚠️Promise.allPromise.any的关系,类似于,Array.prototype.everyArray.prototype.some的关系。

代码实现

实现思路和Promise.all及其类似。不过由于对异步过程的处理逻辑不同,因此这里的计数器用来标识是否所有的实例均 rejected

Promise.any = function(iterators) {
  const promises = Array.from(iterators);
  const num = promises.length;
  const rejectedList = new Array(num);
  let rejectedNum = 0;

  return new Promise((resolve, reject) => {
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(value => resolve(value))
        .catch(error => {
          rejectedList[index] = error;
          if (++rejectedNum === num) {
            reject(rejectedList);
          }
        });
    });
  });
};

实现 Promise.allSettled

过程

Promise.allSettled(iterators)的传参和返回值与Promise.all相同。

根据ES2020,此返回的 promise 实例的状态只能是fulfilled。对于传入的所有 promise 实例,会等待每个 promise 实例结束,并且返回规定的数据格式。

如果传入 a、b 两个 promise 实例:a 变为 rejected,错误是 error1;b 变为 fulfilled,value 是 1。那么Promise.allSettled返回的 promise 实例的 value 就是:

[{ status: "rejected", value: error1 }, { status: "fulfilled", value: 1 }];

代码实现

实现中的计数器,用于统计所有传入的 promise 实例。

const formatSettledResult = (success, value) =>
  success
    ? { status: "fulfilled", value }
    : { status: "rejected", reason: value };

Promise.allSettled = function(iterators) {
  const promises = Array.from(iterators);
  const num = promises.length;
  const settledList = new Array(num);
  let settledNum = 0;

  return new Promise(resolve => {
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(value => {
          settledList[index] = formatSettledResult(true, value);
          if (++settledNum === num) {
            resolve(settledList);
          }
        })
        .catch(error => {
          settledList[index] = formatSettledResult(false, error);
          if (++settledNum === num) {
            resolve(settledList);
          }
        });
    });
  });
};

Promise.all、Promise.any 和 Promise.allSettled 中计数器使用对比

这三个方法均使用了计数器来进行异步流程控制,下面表格横向对比不同方法中计数器的用途,来加强理解:

方法名用途
Promise.all标记 fulfilled 的实例个数
Promise.any标记 rejected 的实例个数
Promise.allSettled标记所有实例(fulfilled 和 rejected)的个数

实现 Promise.prototype.finally

过程

它就是一个语法糖,在当前 promise 实例执行完 then 或者 catch 后,均会触发。

举个例子,一个 promise 在 then 和 catch 中均要打印时间戳:

new Promise(resolve => {
  setTimeout(() => resolve(1), 1000);
})
  .then(value => console.log(Date.now()))
  .catch(error => console.log(Date.now()));

现在这段一定执行的共同逻辑,就可以用finally简写为:

new Promise(resolve => {
  setTimeout(() => resolve(1), 1000);
}).finally(() => console.log(Date.now()));

可以看出,Promise.prototype.finally 的执行与 promise 实例的状态无关,不依赖于 promise 的执行后返回的结果值。其传入的参数是函数对象。

代码实现

实现思路:

  • 考虑到 promise 的 resolver 可能是个异步函数,因此 finally 实现中,要通过调用实例上的 then 方法,添加 callback 逻辑
  • 成功透传 value,失败透传 error
Promise.prototype.finally = function(cb) {
  return this.then(
    value => Promise.resolve(cb()).then(() => value),
    error =>
      Promise.resolve(cb()).then(() => {
        throw error;
      })
  );
};

参考链接

如果觉得有收获,欢迎Watch or Star文章仓库 「github.com/dongyuanxin/blog」,或扫码关注公众号「心谭博客」,解锁更多文章。

查看原文

赞 15 收藏 13 评论 0

心谭 发布了文章 · 2019-11-30

一文说清「VirtualDOM」的含义与实现

专注前端与算法的系列干货分享,欢迎关注(¬‿¬):
「微信公众号:心谭博客」| xin-tan.com | GitHub

摘要

随着 React 的兴起,Virtual DOM 的原理和实现也开始出现在各大厂面试和社区的文章中。其实这种做法早在 d3.js 中就有实现,是 react 生态的快速建立让它正式进入了广大开发者的视角。

在正式开始前,抛出几个问题来引导思路,这些问题也会在不同的小节中,逐步解决:

  • 🤔️ 怎么理解 VDom?
  • 🤔️ 如何表示 VDom?
  • 🤔️ 如何比较 VDom 树,并且进行高效更新?

⚠️ 整理后的代码和效果图均存放在github.com/dongyuanxin

如何理解 VDom?

曾经,前端常做的事情就是根据数据状态的更新,来更新界面视图。大家逐渐意识到,对于复杂视图的界面,频繁地更新 DOM,会造成回流或者重绘,引发性能下降,页面卡顿。

因此,我们需要方法避免频繁地更新 DOM 树。思路也很简单,即:对比 DOM 的差距,只更新需要部分节点,而不是更新一棵树。而实现这个算法的基础,就需要遍历 DOM 树的节点,来进行比较更新。

为了处理更快,不使用 DOM 对象,而是用 JS 对象来表示,它就像是 JS 和 DOM 之间的一层缓存

如何表示 VDom?

借助 ES6 的 class,表示 VDom 语义化更强。一个基础的 VDom 需要有标签名、标签属性以及子节点,如下所示:

class Element {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
  }
}

为了更方便调用(不用每次都写new),将其封装返回实例的函数:

function el(tagName, props, children) {
  return new Element(tagName, props, children);
}

此时,如果想表达下面的 DOM 结构:

<div class="test">
  <span>span1</span>
</div>

用 VDom 就是:

// 子节点数组的元素可以是文本,也可以是VDom实例
const span = el("span", {}, ["span1"]);
const div = el("div", { class: "test" }, [span]);

之后在对比和更新两棵 VDom 树的时候,会涉及到将 VDom 渲染成真正的 Dom 节点。因此,为class Element增加render方法:

class Element {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
  }

  render() {
    const dom = document.createElement(this.tagName);
    // 设置标签属性值
    Reflect.ownKeys(this.props).forEach(name =>
      dom.setAttribute(name, this.props[name])
    );

    // 递归更新子节点
    this.children.forEach(child => {
      const childDom =
        child instanceof Element
          ? child.render()
          : document.createTextNode(child);
      dom.appendChild(childDom);
    });

    return dom;
  }
}

如何比较 VDom 树,并且进行高效更新?

前面已经说明了 VDom 的用法与含义,多个 VDom 就会组成一棵虚拟的 DOM 树。剩下需要做的就是:根据不同的情况,来进行树上节点的增删改的操作。这个过程是分为diffpatch

  • diff:递归对比两棵 VDom 树的、对应位置的节点差异
  • patch:根据不同的差异,进行节点的更新

目前有两种思路,一种是先 diff 一遍,记录所有的差异,再统一进行 patch;另外一种是 diff 的同时,进行 patch。相较而言,第二种方法少了一次递归查询,以及不需要构造过多的对象,下面采取的是第二种思路。

变量的含义

将 diff 和 patch 的过程,放入updateEl方法中,这个方法的定义如下:

/**
 *
 * @param {HTMLElement} $parent
 * @param {Element} newNode
 * @param {Element} oldNode
 * @param {Number} index
 */
function updateEl($parent, newNode, oldNode, index = 0) {
  // ...
}

所有以$开头的变量,代表着真实的 DOM

参数index表示oldNode$parent的所有子节点构成的数组的下标位置。

情况 1:新增节点

如果 oldNode 为 undefined,说明 newNode 是一个新增的 DOM 节点。直接将其追加到 DOM 节点中即可:

function updateEl($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(newNode.render());
  }
}

情况 2:删除节点

如果 newNode 为 undefined,说明新的 VDom 树中,当前位置没有节点,因此需要将其从实际的 DOM 中删除。删除就调用$parent.removeChild(),通过index参数,可以拿到被删除元素的引用:

function updateEl($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(newNode.render());
  } else if (!newNode) {
    $parent.removeChild($parent.childNodes[index]);
  }
}

情况 3:变化节点

对比 oldNode 和 newNode,有 3 种情况,均可视为改变:

  1. 节点类型发生变化:文本变成 vdom;vdom 变成文本
  2. 新旧节点都是文本,内容发生改变
  3. 节点的属性值发生变化

首先,借助Symbol更好地语义化声明这三种变化:

const CHANGE_TYPE_TEXT = Symbol("text");
const CHANGE_TYPE_PROP = Symbol("props");
const CHANGE_TYPE_REPLACE = Symbol("replace");

针对节点属性发生改变,没有现成的 api 供我们批量更新。因此封装replaceAttribute,将新 vdom 的属性直接映射到 dom 结构上:

function replaceAttribute($node, removedAttrs, newAttrs) {
  if (!$node) {
    return;
  }

  Reflect.ownKeys(removedAttrs).forEach(attr => $node.removeAttribute(attr));
  Reflect.ownKeys(newAttrs).forEach(attr =>
    $node.setAttribute(attr, newAttrs[attr])
  );
}

编写checkChangeType函数判断变化的类型;如果没有变化,则返回空:

function checkChangeType(newNode, oldNode) {
  if (
    typeof newNode !== typeof oldNode ||
    newNode.tagName !== oldNode.tagName
  ) {
    return CHANGE_TYPE_REPLACE;
  }

  if (typeof newNode === "string") {
    if (newNode !== oldNode) {
      return CHANGE_TYPE_TEXT;
    }
    return;
  }

  const propsChanged = Reflect.ownKeys(newNode.props).reduce(
    (prev, name) => prev || oldNode.props[name] !== newNode.props[name],
    false
  );

  if (propsChanged) {
    return CHANGE_TYPE_PROP;
  }
  return;
}

updateEl中,根据checkChangeType返回的变化类型,做对应的处理。如果类型为空,则不进行处理。具体逻辑如下:

function updateEl($parent, newNode, oldNode, index = 0) {
  let changeType = null;

  if (!oldNode) {
    $parent.appendChild(newNode.render());
  } else if (!newNode) {
    $parent.removeChild($parent.childNodes[index]);
  } else if ((changeType = checkChangeType(newNode, oldNode))) {
    if (changeType === CHANGE_TYPE_TEXT) {
      $parent.replaceChild(
        document.createTextNode(newNode),
        $parent.childNodes[index]
      );
    } else if (changeType === CHANGE_TYPE_REPLACE) {
      $parent.replaceChild(newNode.render(), $parent.childNodes[index]);
    } else if (changeType === CHANGE_TYPE_PROP) {
      replaceAttribute($parent.childNodes[index], oldNode.props, newNode.props);
    }
  }
}

情况 4:递归对子节点执行 Diff

如果情况 1、2、3 都没有命中,那么说明当前新旧节点自身没有变化。此时,需要遍历它们(Virtual Dom)的children数组(Dom 子节点),递归进行处理。

代码实现非常简单:

function updateEl($parent, newNode, oldNode, index = 0) {
  let changeType = null;

  if (!oldNode) {
    $parent.appendChild(newNode.render());
  } else if (!newNode) {
    $parent.removeChild($parent.childNodes[index]);
  } else if ((changeType = checkChangeType(newNode, oldNode))) {
    if (changeType === CHANGE_TYPE_TEXT) {
      $parent.replaceChild(
        document.createTextNode(newNode),
        $parent.childNodes[index]
      );
    } else if (changeType === CHANGE_TYPE_REPLACE) {
      $parent.replaceChild(newNode.render(), $parent.childNodes[index]);
    } else if (changeType === CHANGE_TYPE_PROP) {
      replaceAttribute($parent.childNodes[index], oldNode.props, newNode.props);
    }
  } else if (newNode.tagName) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i < newLength || i < oldLength; ++i) {
      updateEl(
        $parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      );
    }
  }
}

效果观察

github.com/dongyuanxin/pure-virtual-dom的代码 clone 到本地,Chrome 打开index.html

新增 dom 节点.gif:

更新文本内容.gif:

更改节点属性.gif:

⚠️ 网速较慢的同学请移步 github 仓库

参考链接

专注前端与算法的系列干货分享,欢迎关注(¬‿¬)
查看原文

赞 38 收藏 28 评论 1

心谭 赞了文章 · 2019-11-30

javascript典型内存泄漏及chrome的排查方法

javascript的内存泄漏

对于JavaScript这门语言的使用者来说,大多数的使用者的内存管理意识都不强。因为JavaScript一直以来都只作为在网页上使用的脚本语言,而网页往往都不会长时间的运行,所以使用者对JavaScript的运行时长和内存控制都比较漠视。但随着Spa(单页应用)、node.js服务端程序和各种js工具的诞生,我们需要重新重视JavaScript的内存管理。

内存泄漏的定义

指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

JavaScript的内存管理

首先JavaScript是一个有Garbage Collection 的语言,也就是我们不需要手动的回收内存。不同的JavaScript引擎有不同的垃圾回收机制,这里我们主要以V8这个被广泛使用的JavaScript引擎为主。

JavaScript内存分配和回收的关键词:GC根、作用域

GC根:一般指全局且不会被垃圾回收的对象,比如:window、document或者是页面上存在的dom元素。JavaScript的垃圾回收算法会判断某块对象内存是否是GC根可达(存在一条由GC根对象到该对象的引用),如果不是那这块内存将会被标记回收。

作用域:在JavaScript的作用域里,我们能够新建对象来分配内存。比如说调用函数,函数执行的过程中就会创建一块作用域,如果是创建的是作用域内的局部对象,当作用域运行结束后,所有的局部对象(GC根无法触及)都会被标记回收,在JavaScript中能引起作用域分配的有函数调用、with和全局作用域。

作用域的分类:局部作用域、全局作用域、闭包作用域

局部作用域

函数调用会创建局部作用域,在局部作用域中的新建的对象,如果函数运行结束后,该对象没有作用域外部的引用,那该对象将会标记回收

全局作用域

每个JavaScript进程都会有一个全局作用域,全局作用域上的引用的对象都是常驻内存的,直到进程退出内存才会自动释放。
手动释放全局作用域上的引用的对象有两种方式:

  • global.foo = undefined

重新赋值改变引用

  • delete global.foo

删除对象属性

闭包作用域

在JavaScript语言中有闭包的概念,闭包指的是包含自由变量的代码块、自由变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。

var closure = (function(){
    //这里是闭包的作用域
    var i = 0 // i就是自由变量
    return function(){
        console.log(i++)
    }
})()

闭包作用域会保持对自由变量的引用。上面代码的引用链就是:

window -> closure -> i

闭包作用域还有一个重要的概念,闭包对象是当前作用域中的所有内部函数作用域共享的,并且这个当前作用域的闭包对象中除了包含一条指向上一层作用域闭包对象的引用外,其余的存储的变量引用一定是当前作用域中的所有内部函数作用域中使用到的变量

常见的几种内存泄漏的方式及使用chrome dev tools的排查方法

用全局变量缓存数据

将全局变量作为缓存数据的一种方式,将之后要用到的数据都挂载到全局变量上,用完之后也不手动释放内存(因为全局变量引用的对象,垃圾回收机制不会自动回收),全局变量逐渐就积累了一些不用的对象,导致内存泄漏

   var x = [];
    function createSomeNodes() {
        var div;
        var i = 10000;
        var frag = document.createDocumentFragment();
        for (; i > 0; i--) {
            div = document.createElement("div");
            div.appendChild(document.createTextNode(i + " - " + new Date().toTimeString()));
            frag.appendChild(div);
        }
        document.getElementById("nodes").appendChild(frag);
    }
    function grow() {
        x.push(new Array(1000000).join('x'));
        createSomeNodes();
        setTimeout(grow, 1000);
    }
    grow()

上面的代码贴一张 timeline的截图
图片描述
主要看memory区域,通过分析代码我们可以知道页面上的dom节点是不断增加的,所以memory里绿色的线(代表dom nodes)也是不断升高的;而代表js heap的蓝色的线是有升有降,当整体趋势是逐渐升高,这是因为js 有内存回收机制,每当内存回收的时候蓝色的线就会下降,但是存在部分内存一直得不到释放,所以蓝色的线逐渐升高

js错误引用DOM元素

    var nodes = '';
    (function () {
        var item = {
            name:new Array(1000000).join('x')
        }
        nodes = document.getElementById("nodes")
        nodes.item = item
        nodes.parentElement.removeChild(nodes)
    })()

这里的dom元素虽然已经从页面上移除了,但是js中仍然保存这对该dom元素的引用。
因为这段代码是只执行一次的,所以用timeline视图会很难分析出来是否存在内存泄漏,所以我们可以用 chrome dev tool 的 profile tab里的heap snapshot 工具来分析。
上面的代码贴一张 heap snapshot 的summary模式的截图
clipboard.png

通过constructor的filter功能,我们把上面代码中创建的长字符串找出来,可以看到代码运行结束后,内存中的长字符串依然没有被垃圾回收掉。
顺带提一下的是右边红框里的shadow size和 retainer size的含义

  • shadow size 指的是对象本地的大小

  • retainer size 指的是对象所引用内存的大小,回收该对象是会将他引用的内存也一并回收,所以retainer size 指代的是回收内存后会释放出来的内存大小

上面我们可以看到 长字符串本身的shadow size和retainer size是一样大的,这是引用长字符串没有引用其他的对象,如果有引用其他对象,那shadow size 和retainer size将不一致。

闭包循环引用

(function(){
    var theThing = null
    var replaceThing = function () {
        var originalThing = theThing
        var unused = function () {
            if (originalThing)
                console.log("hi")
        }
        theThing = {
            longStr: new Array(1000000).join('*'),
            someMethod: function someMethod() {
                console.log('someMessage')
            }
        };
    };
    setInterval(replaceThing,100)
})()

首先我们明确一下,unused是一个闭包,因为它引用了自由变量 originalThing,虽然它被没有使用,但v8引擎并不会把它优化掉,因为 JavaScript里存在eval函数,所以v8引擎并不会随便优化掉暂时没有使用的函数。

theThing 引用了someMethod,someMethod这个函数作用域隐式的和unused这个闭包共享一个闭包上下文。所以someMethod也引用了originalThing这个自由变量。

这里面的引用链是:

GCHandler -> replaceThing -> theThing -> someMethod -> originalThing -> someMethod(old) -> originalThing(older)-> someMethod(older)

随着setInterval的不断执行,这条引用链是不会断的,所以内存会不断泄漏,直致程序崩溃。
因为是闭包作用域引起的内存泄漏,这时候最好的选择是使用 chrome的heap snapshot的container视图,我们通过container视图能清楚的看到这条不断泄漏内存的引用链
clipboard.png

由于作者水平有限,文中如有错误还望指出,谢谢!

参考文档:

百科内存泄漏介绍
chrome devtolls
深入浅出nodejs
node-interview

查看原文

赞 36 收藏 46 评论 2

心谭 赞了文章 · 2019-11-19

VuePress博客搭建笔记(二)个性化配置

点击链接获取博客项目精简模板源码←

&00.上文回顾

在上文 VuePress博客搭建笔记(一)简单上手
中,我简单阐述了VuePress博客搭建的过程,并对其中的一些问题进行分析记录,
包括首页侧边栏导航栏浏览器书签引入挂载githubPage等等。

本文将围绕着博客的个性化配置作一个整理。

首先引用官网的原文重申一次VuePress博客搭建的过程:

Start
As Easy as 1, 2, 3
# install
yarn global add vuepress@next 
# OR npm install -g vuepress@next

# create a markdown file
echo '# Hello VuePress' > README.md

# start writing
vuepress dev

# build to static files
vuepress build

&01.版本推荐

VuePress的官网目前是存在两个版本的,分别为0.x版本和最新的1.x的alpha版本。
在实际开发中,我经常因为混淆版本而导致一些插件不能正常引入,当然这也是因为我对VuePress的使用还不熟练。
打开VuePress的官网,如果有下面绿色的Notice弹出,说明是1.x版本。

versions

如果是开发者,建议安装最新版VuePress,体验最新的轮子~

yarn add vuepress -D       # Install 0.x.x.
yarn add vuepress@next -D  # Install next.

&03.Github链接

与github关联的页脚链接和导航栏链接,

/**
* config.js
* @type {{themeConfig: {lastUpdated: string, repoLabel: string, 
* docsDir: string, repo: string, editLinkText: string, 
* docsRepo: string, editLinks: boolean, docsBranch: string}}}
*/
module.exports = {
    // ...
    themeConfig: {
        // 假定是 GitHub. 同时也可以是一个完整的 GitLab URL
        repo: 'https://github.com/Mulander-J/Wiki1001Pro.git',
        // 自定义仓库链接文字。默认从 `themeConfig.repo` 中自动推断为
        // "GitHub"/"GitLab"/"Bitbucket" 其中之一,或是 "Source"。
        repoLabel: 'GitHub',
        // 以下为可选的编辑链接选项
        // 假如你的文档仓库和项目本身不在一个仓库:
        docsRepo: 'https://github.com/Mulander-J/Wiki1001Dev',
        // 假如文档不是放在仓库的根目录下:
        docsDir: 'docs',
        // 假如文档放在一个特定的分支下:
        docsBranch: 'master',
        // 默认是 false, 设置为 true 来启用
        editLinks: true,
        // 默认为 "Edit this page"
        editLinkText: '博主通道__GitHub Private Repo !',
        // 文档更新时间:每个文件git最后提交的时间,
        lastUpdated: 'Last Updated' ,
    }
}

githublink

&04.个性化主题

主题修改

  • 下载默认主题
npm install @vuepress/theme-default@next
  • 替换
##复制node_modules/@vuepress/theme-default 文件夹
##粘贴至.vuepress/ 下并更名为theme

Dev
├─── docs
│   └── .vuepress   // 配置目录
│   │    ├── public // 静态资源
│   │    ├── theme // 主题
│   │    │   ├── components // 组件
│   │    │   ├── global-components // 全局组件
│   │    │   ├── global-components // 全局组件
│   │    │   ├── layouts // 布局(包括首页在内)
│   │    │   ├── styles // 样式
│   │    │   ├── util // 工具
│   │    │   ├── index.js // 入口配置
│   │    │   ├── noopModule.js // 依赖注入
│   │    │   ├── package.json // 主题依赖
│   │    │   ├── README.md // 主题说明
│   │    └── config.js
│   ├── FAQ     // 求索模块
│   ├── Store   // 仓库模块
│   ├── Thought // 随笔模块
│   └── README.md   // 博客首页
└── package.json
  • 运行
npm run dev

关注控制台输出

tip Apply theme located at G:\WorkSpace\WebStormWS\Wiki1001\Dev\docs\.vuepress\theme...

若果控制台能看到上面这句话或者页面能正常渲染的话,就表示主题引入成功

"C:\Program Files\nodejs\node.exe" "C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js" run dev --scripts-prepend-node-path=auto

> wiki1001@1.0.0 dev G:\WorkSpace\WebStormWS\Wiki1001\Dev
> vuepress dev docs

wait Extracting site metadata...
tip Apply theme located at G:\WorkSpace\WebStormWS\Wiki1001\Dev\docs\.vuepress\theme...

接下来就可以对这份theme项目作修改了,

它就是载负你的博客的一个简单的VUe单页面项目。

滚动条样式

设置页面滚动条为渐变色&圆角样式

参考

linear-scroll

/*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
::-webkit-scrollbar
{
  width: 8px;
  height: 8px;
  border-radius: 10px;
  background-color: #F5F5F5;
}

/*定义滚动条轨道 内阴影+圆角*/
::-webkit-scrollbar-track
{
  border-radius: 10px;
  -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
  background-color: #F5F5F5;
}

/*定义滑块 内阴影+圆角*/
::-webkit-scrollbar-thumb
{
  border-radius: 10px;
  -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
  border-radius: 10px;
  /* 线性渐变 */
  background-image: -webkit-gradient(linear, 
  left bottom, left top,
   color-stop(0.44, rgb(60,186,146)), 
   color-stop(0.72, rgb(253,187,45)), 
   color-stop(0.86, rgb(253,187,45)));
  transition: 0.3s ease-in-out;
}
/*定义滑块悬浮样式*/
::-webkit-scrollbar-thumb:hover{
  background-image: -webkit-gradient(linear, 
  left bottom, left top, 
  color-stop(0.44, rgb(253,187,45)), 
  olor-stop(0.72, rgb(253,187,45)), 
  color-stop(0.86, rgb(60,186,146)));
  transition: 0.3s ease-in-out;
}

渐变色标题

linear-home

h1{
  background-image: -webkit-linear-gradient(left,
   #22c1c3, #fdbb2d 25%, #22c1c3 50%, #fdbb2d 75%, #22c1c3);
  -webkit-text-fill-color: transparent;
  -webkit-background-clip: text;
  -webkit-background-size: 200% 100%;
  -webkit-animation: myGradientChange 4s infinite linear;
  animation: myGradientChange 4s infinite linear;
}
.description,.card h2{
  background-image: -webkit-linear-gradient(left,
   #fdbb2d, #22c1c3 25%, #fdbb2d 50%, #22c1c3 75%, #fdbb2d);
  -webkit-text-fill-color: transparent;
  -webkit-background-clip: text;
  -webkit-background-size: 200% 100%;
  -webkit-animation: myGradientChange 4s infinite linear;
  animation: myGradientChange 4s infinite linear;
}
@keyframes myGradientChange  {
  0%{ background-position: 0 0;}
  100% { background-position: -100% 0;}
}

GoToEnd

gotoedn

  • 下载官方组件BackToTop
npm install @vuepress/plugin-back-top@next
  • 本地化BackToTop组件至Layout.vue中,包括首页在内都会生产该组件
//  复制node_modules/@vuepress/plugin-back-top/BackToTop.vue
//  粘贴至.vuepress/theme/components
<template>
  <div>
    <BackToTop></BackToTop>
  </div>
</template>

<script>
import BackToTop from '../components/BackToTop.vue'
export default {
  components: { BackToTop},
}
</script>
  • 修改Back To Top 组件

复制一个Back To Top DOM节点同时修改 transitiontransition-group

<template>
  <transition-group name="fade">
    <svg
      v-if="topShow"
      class="go-to-top"
      key="goTop"
      @click="scrollToTop"
      xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49.484 28.284"
    >
      <g transform="translate(-229 -126.358)">
        <rect fill="currentColor" width="35" height="5" rx="2" transform="translate(229 151.107) rotate(-45)"/>
        <rect fill="currentColor" width="35" height="5" rx="2" transform="translate(274.949 154.642) rotate(-135)"/>
      </g>
    </svg>
    <svg
            v-if="endShow"
            class="go-to-top go-to-end"
            @click="scrollToEnd"
            key="goEnd"
            xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49.484 28.284"
    >
      <g transform="translate(-229 -126.358)">
        <rect fill="currentColor" width="35" height="5" rx="2" transform="translate(229 151.107) rotate(-45)"/>
        <rect fill="currentColor" width="35" height="5" rx="2" transform="translate(274.949 154.642) rotate(-135)"/>
      </g>
    </svg>
  </transition-group>
</template>

增加 变量 scrollEnd , endShow

方法 getScrollEnd() ,scrollToEnd()

<script>
import debounce from 'lodash.debounce'
export default {
  props: {
    threshold: {
      type: Number,
      default: 300
    }
  },
  data () {
    return {
      scrollTop: null,
      scrollEnd: null
    }
  },
  mounted () {
    this.scrollTop = this.getScrollTop()
    this.scrollEnd = this.getScrollEnd()
    window.addEventListener('scroll', debounce(() => {
      this.scrollTop = this.getScrollTop()
      this.scrollEnd = this.getScrollEnd()
    }, 100))
  },
  methods: {
    getScrollTop () {
      return window.pageYOffset ||
        document.documentElement.scrollTop ||
        document.body.scrollTop || 0
    },

    getScrollEnd () {
      return document.documentElement.scrollHeight ||
              document.body.scrollHeight || this.threshold
    },
    scrollToTop () {
      window.scrollTo({ top: 0, behavior: 'smooth' })
      this.scrollTop = 0
    },

    scrollToEnd () {
      window.scrollTo({ top: this.scrollEnd, behavior: 'smooth' })
      this.scrollTop = this.scrollEnd
    }
  },
  computed: {
    topShow () {
      return this.scrollTop > this.threshold
    },
    endShow () {
      return (this.scrollEnd - this.scrollTop) > 3*this.threshold
    }
  }
}
</script>

增加置底按钮样式,z轴旋转180度

<style lang='stylus' scoped>
.go-to-top {
  cursor: pointer;
  position: fixed;
  bottom: 5rem;
  right: 2.5rem;
  width: 2rem;
  color: $accentColor;
  z-index: 1;
}
.go-to-end{
  bottom: 2rem;
  transform: rotateZ(180deg);
}
.go-to-top:hover {
  color: lighten($accentColor, 30%);
}

@media (max-width: 959px) {
  .go-to-top {
    display: none;
  }
}
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

&05.插件-PWA

yarn add -D @vuepress/plugin-pwa
# OR npm install -D @vuepress/plugin-pwa
//config,js
module.exports = {
// ...
 plugins: [
    ['@vuepress/pwa', {
        serviceWorker: true,
        //指向自定义组件
        popupComponent: 'MySWUpdatePopup',
        updatePopup: {
            message: "新的风暴已经出现",
            buttonText: "盘他"
        }
    }]
 ]
}

serviceWorker的作用大致就页面首次加载时会请求本地的serviceWorker.js去比对各个文件的版本号
如果不一致则提示用户拉取更新
pwapost
不过这个popup的默认样式很丑😨,所以官方也提供了自定义popup的接口和教程

默认样式,这是原始的...eh

pwaold

这是官网给的模板 = = 阿咧?

pwademo

这是我的...emmm..可还行呢,凑合用了。这个vue的logo还会动的,不算侵权吧😱...@Vue??

P.S.这个popup 的内容是我自定义写的,不是官方在皮啊

参考

pwamydemo

注意
v-if="enabled" // 添加这一段指令,否则popup无法消失
<SWUpdatePopup>
    <div   v-if="enabled" 
            slot-scope="{ enabled, reload, message, buttonText }"
            class="my-sw-update-popup">
        {{ message }}<br>
        <button @click="reload">{{ buttonText }}</button>
    </div>
</SWUpdatePopup>

&06.插件-google-analytics

  • 首先你要有一个谷歌账号,
  • 然后有一个 google analytics(GA)账户
  • 然后...
  • 不用麻烦了不用麻烦了
  • 不用麻烦 不用麻烦了 不用麻烦了
  • 你们一起上 我在赶时间
  • 如何注册并设置google analytics(GA)账户
  • GA-ID ゲットだゼーツ!

google_analysis

  • 安装并使用 google_analysis
yarn add -D @vuepress/plugin-google-analytics
# OR npm install -D @vuepress/plugin-google-analytics
//config.js
module.exports = {
...
  plugins: [
    ['@vuepress/google-analytics', {
        ga: '*********'//你的Google Analytics ID
    }],
  ]
}

google_analysis 会实时监控你的url,倘若一个页面有多个h2,h3标题,滑动滚动条导致url的变化也会被捕捉到。

googlepost

&07.评论系统-Valine

参考

  • 获取APP ID 和 APP Key,请先登录或注册 LeanCloud, 进入控制台后点击左下角创建应用
  • 安装并使用 Valine
# Install leancloud's js-sdk
npm install leancloud-storage --save
# Install valine
npm install valine --save
// Register AV objects to the global
window.AV = require('leancloud-storage');
// Use import
import Valine from 'valine';
// or Use require
const Valine = require('valine');
new Valine({
    el:'#vcomments',
    // other config
})
//Page.vue
<script>
export default {
     mounted: function(){
        // require window 
        const Valine = require('valine');
        if (typeof window !== 'undefined') {
          this.window = window
          window.AV = require('leancloud-storage')
        }
        new Valine({
          el: '#vcomments' ,
          appId: '',// your appId
          appKey: '', // your appKey
          notify:false, //邮箱通知,可关闭
          verify:false, //反人类的算术验证码,建议关闭
          avatar:'mm', //头像,默认即可
          visitor: true,//访问计数
          placeholder: 'just go go' 
        });
      }
}
</script>
Issue
不管地址栏怎么变化,不管怎么切页面,评论内容不会随地址栏变化而变化,即无法同步。

Valine实例与leancloud-storage实例 在每次页面加载时会向服务器发起
带当前url参数的请求以获取评论数据,而这个url参数每次都是一样。

首先Valine 实例与 leancloud-storage 实例都在 mounted 钩子中初始化或挂载至 window 对象上了,

当页面 url 变化时,Page.vue 本身并没有变化,只是它身上的<Content/>内容变了,mounted没有重新触发,上面两个实例也没有改变。

P.S.血的教训

不要在 md 文件中直接写<Content/>,请用其他格式编译它,否则会被vuepress识别为组件而不断加载陷入死循环。

[Vue warn]: Error in nextTick: "RangeError: Maximum call stack size exceeded"
warn @ vue.runtime.esm.js?2b0e:601
vue.runtime.esm.js?2b0e:1832 RangeError: Maximum call stack size exceeded
Exp:
只是它身上的<Content/>内容变了,
只是它身上的`<Content/>`内容变了,
  • 如果重新 init 两个实例呢?

    • 通过 watch $route 重新 new 两个实例也不行。
    • 在每个页面单独写入 带 Valine 的.vue组件也不行。

暂时无法解决...emmm,欲知后事如何,请听下回分解..

欸,应该没有(三)了,等解决了直接写在评论里吧。


解决 Valine 不随页面刷新 , Page.vue 改动如下:

<script>
import...
export default {
   // 初始化Valine组件
  mounted() {
    this.renderValine()
  },
  watch :{
     // 路由变化时重新初始化Valine组件
    $route (a,b) {
      if(a.path!=b.path){
        this.renderValine()
      }
    }
  },
 methods: {
    // 生成评论组件的 Dom 元素
    renderValine () {
    //因为此方法会构建dom节点,所以<template>中不需要再加相关dom元素
      let $page = document.querySelector('.page')
      let vcomments = document.getElementById('vcomments')
      if(!vcomments){
        vcomments = document.createElement('div')
        vcomments.id = 'vcomments'
      }
      if(this.$page.frontmatter.hideFooter){
      // 如果forntmatter中标注'hideFooter:true'则不渲染评论组件,使得评论组件在各个页面的显示可控
        vcomments.remove();
      }else{
        if ($page && !vcomments){
          $page.appendChild(vcomments)
        }else{
          $page = document.querySelector('.page')
          $page.appendChild(vcomments)
        }
        this.valine()
      }
    },
    // 初始化valine实例
    valine () {
      const Valine = require('valine')
      const leancloudStorage = require('leancloud-storage')
      // require window
      if (typeof window !== 'undefined') {
        window.AV = leancloudStorage
      }
      // 配置valine参数
      new Valine({
        el: '#vcomments' ,
        appId: '',// your appId
        appKey: '', // your appKey
        notify:true,
        verify:false,
        visitor: true,
        avatar:'wavatar',
        placeholder: '春霄苦短,少女前进吧!' +'\n'+
                '夜は短し歩けよ乙女!' +'\n'+
                'Yoru wa Mijikashi Arukeyo Otome!' +'\n'+
                'The Night is Short, Walk on Girl!',
        path: window.location.pathname
      });
    },
  }
点击链接获取博客项目精简模板源码←
查看原文

赞 14 收藏 10 评论 22