Coder Social home page Coder Social logo

blog's Introduction

Never forget why you start

Welcome to my page!
I'm nfwyst, front end developer from Chengdu, China

Things I code with

React Webpack github actions TypeScript redux ReactiveX Sass Styled Components git NestJs npm html5 Rollup Prettier MongoDB Nodejs ChatGPT ReactNative Neovim ReactQuery Lua C Go

Where to find me

Telegram

blog's People

Contributors

nfwyst avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

blog's Issues

Node.js 设计模式-行为模式-观察者模式

观察者模式是定义一种一对多的对象依赖关系, 当一个对象的状态发生变化时, 所有订阅状态变化的对象会被通知到, 并自动更新自己. 观察者模式是一种普遍使用的模式, 特别是在 node.js 处理异步任务的时候扮演者重要的角色.

观察者模式有两个部分, 一个是观察者, 一个是可观察对象, 可观察对象有一个 subscribe 方法, 当可观察对象的状态发生变化的时候, 通过观察者的 notify 方法进行通知, 观察者进行响应.

实现观察者:

class Observer {
  notify(newState) {
    console.log(newState);
  }
}

实现可观察对象:

class Observerable{
  constructor(state) {
    this.state = state;
    this.subscribes = [];
  }

  setState(newState) {
    this.state = newState;
    this.subscribes.forEach(s => s.notify(this.state));
  }

  subscribe(s) {
    this.subscribes = [...this.subscribes, s];
  }
}

使用观察者模式:

const Observer  = require('./observer');
const Observerable = require('./observerable');

const observer  = new Observer();
const observerable = new Observerable({name: 'thecatblue'});

observerable.subscribe(observer);
observerable.setState({name: 'thedogblue');

当 observerable 调用 setState 更新自己状态的同时, 订阅者 observer 同时会做出响应

Node.js 设计模式-行为模式-策略模式

策略模式是一种十分强大的模式, 它允许我们在运行的过程中动态修改处理方式, 这样就可以根据用户输入或参数的不同选择合适的处理方式返回对应的结果. 比如我们需要设计一个日志系统, 需要的策略包括输出到标准输出, 输出到文件, 是否包含时间戳, 是否对输出进行处理等, 除了之前的 Logger 对象, 还需要一个 LoggerStrategy 策略对象, 和初始化日志策略的配置文件.

创建策略对象:

const { appendFile } = require('fs');
const path = require('path');

class LogStrategy {
  static toFile(timestamp, message) {
    appendFile(
      path.join(__dirname, 'logs.txt'),
      `${timestamp} - ${message} \n`, err => {
        if (err) console.error(error);
      }
    )
  }

  static toConsole(timestamp, message) {
    console.log(`${timestamp} - ${message}`);
  }

  static none() { }
}

module.exports = LogStrategy

创建策略配置文件 config.json:

{
  "logs": {
    "strategy": "toConsole"
  }
}

创建日志对象:

const path = require('path');
const config = require('./config.json');
const LogStrategy = require('./logStrategy');

class Logger {
  constructor(strategy) {
    this.logs = [];
    this.strategy = LogStrategy[strategy];
  }

  get count() {
    return this.logs.length;
  }

  log(...args) {
    const orig = Error.prepareStackTrace;
    Error.prepareStackTrace = (_, stack) => stack;
    const err = new Error();
    Error.captureStackTrace(err, this.log);
    const callee = err.stack[0];
    Error.prepareStackTrace = orig;
    const timestamp = new Date().toISOString();
    const message = `${path.relative(process.cwd(), callee.getFileName())}:${callee.getLineNumber()}:  ${args.join(' ')}`;
    this.logs = [...this.logs, { message, timestamp }];
    this.strategy(timestamp, message);
  }

  changeStrategy(strategy) {
    this.strategy = LogStrategy[strategy];
  }
}

module.exports = new Logger(config.logs.strategy);

使用基于策略模式的日志系统:

const logger = require('./logger');

logger.log('hello', 'world');
logger.changeStrategy('toFile');
logger.log('error happend');

使用策略模式, 通过调用日志的 changeStrategy 方法动态修改策略, 实现将日志从输出到标准输出转换为输出到日志文件中

css 系列(2)-使用 css 实现抽屉式菜单

我们都知道, 抽屉式菜单可以使用 javascript 实现, 但实际上, css 也能实现抽屉式菜单, 这在前端性能优化的时候特别有用, 比如我们要实现下面的效果

屏幕快照 2020-02-27 上午7 44 42
屏幕快照 2020-02-27 上午7 44 51

模板代码

首先我们有一个面包屑图标和一个带标题的 logo, 然后有一个导航栏菜单, 并且添加了可访问性元素

<!doctype html>
<html>

<head>
  <meta charset="utf-8">
  <title>菜单</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <script src="https://kit.fontawesome.com/fb7f6f30e4.js"></script>
  <link href="https://fonts.googleapis.com/css?family=Nobile:400,500|Open+Sans&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="./style.css">
  <link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
</head>

<body>
  <header>
    <section class="row">
      <a href="#main-menu" class="menu-toggle" id="main-menu-toggle" aria-label="Open main menu">
        <span class="sr-only">Open main menu</span>
        <span class="fa fa-bars" aria-hidden="true"></span>
      </a>
      <article class="logo">
        <h1><i class="fas fa-paint-brush"></i>CSS菜单</h1>
        <p>hello world</p>
      </article>
    </section>
    <nav id="main-menu" class="main-menu" aria-label="main menu">
      <a href="#main-menu-toggle" class="menu-close">
        <span class="sr-only">Close main menu</span>
        <span class="fa fa-close" aria-hidden="true"></span>
      </a>
      <ul>
        <li><a href="#">CSS</a></li>
        <li><a href="#">Design</a></li>
        <li><a href="#">Video</a></li>
        <li><a href="#">Music</a></li>
      </ul>
    </nav>
    <a href="#main-menu-toggle" class="backdrop" hidden tabindex="-1"></a>
  </header>
</body>

</html>

我们让 logo 和 面包屑图标显示在一行, 并添加一些初始样式

header .row {
  display: flex;
  flex-flow: row nowrap;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

.menu-toggle {
  color: #333;
  margin-right: 1rem;
}

.menu-toggle:hover,
.menu-toggle:focus {
  color: #C85028;
}

.main-menu {
  position: fixed;
  display: none;
  background-color: black;
  left: -200px;
  top: 0;
  height: 100%;
  overflow-x: visible;
  overflow-y: auto;
  transition: all .5s ease;
  z-index: 999;
}

.main-menu ul {
  padding-top: 2.5em;
  padding-left: 0;
  min-height: 100%;
  width: 200px;
}

.main-menu ul li {
  display: block;
}

.main-menu a {
  padding: .75em;
  color: #fff;
  border-bottom: 1px solid #383838;
  transition: all .3s ease;
  display: block;
}

.main-menu li:first-child a {
  border-top: 1px solid #383838;
}

.main-menu a:hover,
.main-menu a:focus {
  background: #333;
  text-decoration: underline;
}

.main-menu .menu-close {
  position: absolute;
  right: 0;
  top: 0;
}

header {
  border-bottom: 4px solid #9EA9C1;
}

nav ul {
  list-style: none;
  margin: 0
}

nav a {
  display: block;
  padding: .75rem;
  text-decoration: none;
}

@media (min-width: 800px) {
  header, main {
    border: none;
    margin: 0
  }

  nav {
    /* border-top: 4px solid #9EA9C1; */
    border-bottom: 1px solid #ddd;
    padding: .3rem 0;
  }

  nav ul {
    text-align: center;
  }

  nav li {
    display: inline-block;
  }

  nav a {
    color: #C85028;
    border-bottom: none;
    display: inline;
  }
}

最后我们要实现当点击面包屑图标的时候, 弹出左侧的菜单, 点击关闭按钮的时候关闭, 注意我们 html 中设置的 href="#main-menu-toggle"href="#main-menu" 这两个属性, 现在我们要用到这两个属性

.main-menu:target {
  display: block;
  left: 0;
  outline: none;
}

.main-menu:target .menu-close {
  z-index: 1001;
}

.main-menu:target ul {
  position: relative;
  z-index: 1000;
}

.main-menu:target + .backdrop {
  position: fixed;
  display: block;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 998;
  background-color: rgba(0, 0, 0, .85);
  cursor: default;
  transition: all .5s ease;
}

其中 .main-menu:target 表示当类为 main-menu 的元素被hash地址定位的时候的状态, 也就是浏览器地址变成test.html#main-menu的时候的状态, 这个状态可被css使用. 被定位的时候, main-menu 会显示, 并且设置了 z-index 层级关系, 这样就不会被挡住, 最后类为backdrop的a标签作为中间层的整体黑色背景

刷新浏览器, 即可看到效果

css 系列 (1)- 使用 flexbox 实现百分比布局

对于百分比布局最常见的方式是使用 width, 但在自适应上缺乏一定的灵活性。 我们有多种方式可以实现百分比布局, 包括 flexbox, 比如我们要实现下面的效果

屏幕快照 2020-02-25 上午8 58 59

初始化模板

我们的 html 文件中有8个div, 表示每一个布局块

<div class="wrapper">
  <div class="a">A</div>
  <div class="b">B</div>
  <div class="c">C</div>
  <div class="d">D</div>
  <div class="e">E</div>
  <div class="f">F</div>
  <div class="g">G</div>
  <div class="h">H</div>
</div>

接下来给每一个布局块上色

div > div {
  font-size: 5rem;
  text-align: center;
  color: white;
}
.a {
  background-color: #FCB10F;
}
.b {
  background-color: #801340;
}
.c {
  background-color: #4D2975;
}
.d {
  background-color: #244479;
}
.e {
  background-color: #55B4AF;
}
.f {
  background-color: rgba(128, 19, 64, 0.7); 
}
.g {
  background-color: rgba(77, 41, 117, 0.5);
}
.h {
  background-color: rgba(252, 177, 15, 0.7);
}

布局实现

第一步, 父容器应该是一个 flex 容器

.wrapper {
  display: flex;
  flex-flow: row wrap;
  justify-content: space-between;
  align-content: stretch;
}

除了指定 flex 容器, 我们还设置了换行, 每一个布局块之间均匀分布

第二步, 每一个布局块之间添加上下间距

.wrapper > * {
  margin-bottom: 1rem;
}

第三步, 为每一个布局块设置初始大小

.a,
.h {
  flex-basis: 66%;
}

.c {
  flex-basis: 100%;
}

.b,
.d,
.e,
.f,
.g {
  flex-basis: 32%;
}

这里使用百分比单位作为初始大小, 刷新页面即可看到效果

Puppeteer 爬虫性能优化

我们在爬取网站的时候, 一般比较关心网站的加载速度, 而限制加载速度的大多数是静态文件, 包括 css, font, image. 为了优化爬虫性能, 我们需要阻止浏览器加载这些不必要的文件, 这可以通过对请求进行拦截来实现

优化静态文件加载

await page.setRequestInterception(true);
page.on('request',  req => {
  if(['image', 'stylesheet', 'font'].includes(req.resourceType())) {
    return request.abort();
  }
  return request.continue();
});

// other stuff

这样在页面发出请求的时候, 不用加载图片, 样式和字体, 可以大大提高爬虫的性能和速度

函数式编程与 transducer

在函数式编程领域, 链式调用被compose/pipe所代替, 对于一些常规的数据结构的操作, 函数之间的组合关系所带来的可阅读性, 也远远大于命令式代码.

transducer 是组合的一种特殊化形式, 在下结论之前, 先看一个问题:

问题

我们有一个整数数组A, 需要过滤其中大于2的整数对其中的每个值加1再求和, 如果用通用的链式调用的方式, 应该是这样:

const arr = [1,2,3,4]
const gt2 = v => v > 2
const add1 = v => v + 1
const sum = (a, b) => a + b
arr
  .filter(gt2)
  .map(add1)
  .reduce(sum, 0)

首先, 这里会有两个问题, 1个是这里的 filter, map, reduce, 都是不纯的函数, 因为它们都有隐含参数 arr. 然后是我们无法将arr调用这一部分通过组合的方式与我们应用的其他部分相互组合, 这不符合函数式编程的原则.

满足函数式编程的原则

首先我们需要将不纯的函数变成纯函数

去除不纯

去除不纯的方式很简单, 只需要将隐含参数传递进去, 从而定义输入与输出之间明显的关系. 这里我们会引入curry全局工具函数

const curry = (fn, ...arr) => (...args) => [...arr, ...args].length >= fn.length ? fn(...[...arr, ...args]) : curry(fn, ...[...arr, ...args])

const filter = curry((fn, arr) => {
  const result = []
  arr.forEach(item => fn(item) ? result.push(item) : null)
  return result
})

const map = curry((fn, arr) => {
  const result = []
  arr.forEach(item => result.push(fn(item)))
  return result
})

const reduce = curry((reducer, arr, initialValue) => {
  arr.forEach(item => {
    initialValue = reducer(initialValue, item)
  })
  return initialValue
})

组合实现

在确保我们所使用的函数都是纯函数的前提下, 我们想将 filter, map, reduce 组合起来, 它看起来像这样, 我们这里会引入全局 compose 工具函数:

const compose = (...fns) => arg => fns.reduceRight((result, fn) => fn(result), arg)
// 无效的组合
const work = compose(sum, map(add1), filter(gt2))
work(arr)

但是它并不会给到我们想要的结果, 原因在于 我们的 map, filter 是一元函数(实现忽略 index), sum 是二元函数, 而且输入参数的类型也不同, 因此它们是不同形状的函数, 彼此不兼容. 而组合的前提是函数都是一元并且参数类型都一样, 所以下一步需要对这些函数做形状适配

改变函数的形状

我们有两个选择, 一是将 reduce 适配为一元函数, 二是将 map, filter 适配为二元函数. 显然, 将 reduce 函数适配为一元函数是行不通的, 因为reduce的参数必须是一个迭代函数, 而不能是一个值. 所以只能将 map, filter 变成 reducer, 接下来就是实现:

const mapReducer = curry((mapper, reducer) => {
  return (list, v) => reducer(list, mapper(v))
})
const filterReducer = curry((flt, reducer) => {
  return (list, v) => flt(v) ? reducer(list, v) : list
})

首先我们的适配函数接受 mapper(这里是 add1) 作为参数, 需要返回一个二元函数, 类型和 sum 相同.
同时要保证组合的函数是一元函数, 如果没有 reducer 参数, 我们组合的函数都是二元函数, 不符合要求.

可以看出, 上面的 mapReducer 和 filterReducer 都是高阶reducer, 之所以这么说, 是因为它的输入之一是 reducer 输出是一个 reducer, 同时它也是高阶函数

通过上面的定义我们将 map 和 filter 转换成与 reduce 等价的形式:

const reducer = (list, v) => [...list, v]
arr
  .reduce(filterReducer(gt2)(reducer), [])
  .reduce(mapReducer(add1)(reducer), [])
  .reduce(sum, 0)

上面的结果同样是 9, 它是一种等价转换, 其中 我们将形如 filterReducer(gt2), mapReducer(add1), 以及它们组合之后这样的高阶 reducer 称为 transducer.

应用组合

对于 transducer 我们可以轻松的将它们组合, 不同于一般的组合, transducer 的组合并不满足右结合律, 而是满足左结合律

const transducer = compose(filterReducer(gt2), mapReducer(add1))
const result = reduce(transducer(sum), arr, 0) // => 9

根据 filterReducer, mapReducer 和 compose 的定义, mapReducer(add1)(sum) 将会由compose传递给 filterReducer 作为其内部 reducer, 因此首先执行的是 filter. 而 mapReducer(add1)(sum) 也会被延迟执行, 同时延迟执行所消耗的始终是一个栈帧结构, 消耗的也是堆内存.

更进一步, 我们希望对 transducer 进行组合, 通过构造一个 transduce 组合器就可以实现:

const transduce = curry((arr, initialValue, reducer) => reduce(reducer, arr, initialValue)
const work = compose(transduce(arr, 0), transducer)
console.log(sum) // => 9

在 ramda 这种函数式库中也提供了 into 这种简化操作的函数.

结论

transducer 是 reducer 的组合, 它通过将一元函数向上转型, 使得 reducer 与一元函数能够很好的结合, 同时提供了一层抽象层接口, 让我们可以只关注数据流本身.

Node.js 设计模式-结构模式-组合模式

组合模式是这样一种意图, 通过对象的组合, 实现一个具有层次结构的树, 无论是对于分支还是叶子我们都可以以相同的方式去处理, 最常见的就是文件系统的目录, 用于对文件进行分组, 而目录中包含节点和其他分组, 节点就是叶子, 分组就是分支. 与代理模式和适配器模式一样, 组合模式的节点和分组需要实现同样的接口.

创建叶子:

module.exports = class FileItem {
  constructor(name, info) {
    this.name = name;
    this.info = info;
  }

  get size() {
    return this.info.size;
  }

  print() {
    console.log(`${this.name}: ${this.size}`
  }
}

创建分支:

module.exports = class Directory {
    constructor(name, files) {
      this.name = name;
      this.files = files;
    }

    get size() {
      return this.files.reduce((total, next) => total += next.info.size, 0);
    }

    print() {
      console.log(`${this.name.toUpperCase()}`);
      this.files.forEach(file => file.print());
    }
}

使用组合模式:

const FileItem = require('./item');
const Directory = require('./directory');

const file1 = new FileItem('test.txt', {size: 16});
const file2 = new FileItem('test.md', {size: 512});
const file3 = new FileItem('test.js', {size: 1024});

const Directory1 = new Directory('staticFile', [file1, file2]);

const Directory2 = new Directory('public', [file3, Directory1]);

console.log(Directory2.size);
Directory2.print();

滚动加载高性能设计与实现

滚动加载是一个很常见的需求, 但也是一个对用户体验和服务器都会有较大影响的需求, 如果采用纯 DOM 操作, 用户体验会很差, 所以有必要通过 DOM 缓存, 懒加载和预加载提升用户体验. 如果仅仅通过简单的事件监听来发送异步请求, 那么对于前后端性能上的消耗将是不可忍受的.

这里采用封装的方式将滚动加载做成一个通用的处理程序, 设计方案是基于调用者提供一个数据加载程序和一个可选的懒加载处理程序, 数据加载程序用于调用 api 请求所需的数据, 懒加载处理程序通过传入的数据或缓存的数据判断当前元素是否在视口的上下边界范围内来决定是否需要实际加载并渲染当前元素

另外, 在事件处理上, 并没有使用事件节流和事件防抖. 这是因为不论事件节流还是事件防抖都会在同一时间调用处理程序很多次, 而大多数调用都是无效调用, 因此这里通过定时检测用户是否有滚动行为来进行处理.

下面是滚动处理程序的流程设计(当 duration 为 100 时) :

屏幕快照 2019-10-16 下午2 27 36

代码实现

class Scroll {
  constructor(scope, config = { duration: 800, area: [1000, 600] }) {
    this.scope = scope
    this.config = config
    this.page = 1 // 当前页数
    this.fetching = false // 是否正在加载
    this.lastScrollY = window.pageYOffset // 最后一次滚动时页面顶部到视口顶部的距离
    this.topViewPort = null // 视口的上边界
    this.bottomViewPort = null // 视口的下边界
    this.start() // 启动处理程序
  }

  // 判断与选择器相对应的元素是否在视口的上下边界内
  isVisible (selector) {
    const el = document.querySelector(selector)
    if(!el) return true
      return el.offsetTop + el.offsetHeight > this.topViewPort && el.offsetTop < this.bottomViewPort
  }

  handleScroll (e, force = false) {
    if(this.scope.delayLoad) this.scope.delayLoad({ isVisible: this.isVisible.bind(this) })
    if (!force && this.lastScrollY === window.scrollY) {
      clearTimeout(this.time1)
      this.time1 = window.setTimeout(this.handleScroll.bind(this), this.config.duration)
      return true
    }
    this.lastScrollY = window.scrollY

    this.topViewPort = window.scrollY - this.config.area[0]
    this.bottomViewPort = window.scrollY + window.innerHeight + this.config.area[1]

    if (window.scrollY + window.innerHeight + 200 > document.body.offsetHeight && !this.fetching) {
      const res = this.scope.fetcher(this.page++, (args) =>  {
        this.fetching = false
        if(this.scope.delayLoad) {
          args.isVisible = this.isVisible.bind(this)
          this.scope.delayLoad(args)
        }
      })

      if(res) this.fetching = true
    }

    clearTimeout(this.time2)
    this.time2 = window.setTimeout(this.handleScroll.bind(this), this.config.duration)
  }

  start () {
    this.handleScroll(null, true)
  }

 // 重置处理程序的属性, 如:  page, fetching
  set (obj) {
    Object.assign(this, obj)
    return this
  }
}

机器学习-k近邻算法(knn)

knn 算法是一种 classification 和 regression 算法, k 近邻算法描述的是, 当一组事物靠在一起, 它们就是同一种类型的事物.

比如天空中有很多鸟, 有不同的鸟群, 当一只鸟在空中飞翔的时候, 可能在它周围有不同的鸟群, 而离它最近的那个鸟群就应该是它所属的鸟群和所属的分类.

对比到数据, 对于一个给定的训练数据集, 对于新的输入, 我们在训练数据集中找到 k 个与这个新的输入最邻近的数据, 而这 k 个数据所属的分类就是这个新的输入所属的分类.

如何实现一个 knn 算法(在不考虑多个独立变量的情况下)? 对于一个给定的数据集, 假如需要知道这个数据集中的某个数据和结果之间的相关性, 其中, 这个被观察的数据就是独立变量, 而结果就是因变量. 也就是说通过独立变量去确定一个因变量, 那么实现一个 knn 算法就需要以下步骤:

  1. 对于每一个新的输入, 使用新的输入减去被观察的数据生成一个绝对值
  2. 将这些绝对值由小到大排序
  3. 找到前 k 个生成最小绝对值的输入
  4. 这 k 个输入所产生的结果中出现频率最高的就是被观察数据所产生的结果

指定训练数据集

假设现在有一个二维表格:

[
  [1, 2],
  [2, 2],
  [4, 6],
  [5, 2],
  [9, 3],
  [8, 4]
]

其中数组的每一个元素表示表格中的一行, 子数组中的第一个元素(下标为0) 表示独立变量, 子数组中的第二个元素(下标为1) 表示因变量, 现需求是要知道在独立变量 5 附近, 因变量的值应该是什么.

实现 knn 算法

对于每一个新输入, 计算与被观察数据的距离

datas.map(item => {
      const copy = [...item]
      copy[this.indepentId] = Math.abs(copy[this.indepentId] - this.watchingData)
      return copy
 })

将绝对值由小到大排序, 保留前 k 个

datas.sort((cur, next) => cur[this.indepentId] - next[this.indepentId])
         .slice(0, k)

对前 k 个输入的因变量做统计

datas.map(item => item[this.dependentId])
      .reduce((cur, next) => {
        cur[next] ? cur[next]++ : cur[next] = 1
        return cur
      }, {})

输出 knn 统计结果

Object.entries(reducers).sort((cur, next) => next[1] - cur[1])[0][1]

真实数据测试

const res = new Knn([
  [1, 2],
  [2, 2],
  [4, 6],
  [5, 2],
  [9, 3],
  [8, 4]
]).setIndependentVariableIndex(0)
  .setDependentVariableIndex(1)
  .setWatchingData(5).withK(4) // => 2

可以预见, 2 正是我们需要的结果.

实现源代码

class Knn {
  constructor(arr) {
    this.datas = arr instanceof Array ? arr : []
  }

  setIndependentVariableIndex(index) {
    this.indepentId = index
    return this
  }

  setDependentVariableIndex(index) {
    this.dependentId = index
    return this
  }

  setWatchingData(value) {
    this.watchingData = value
    return this
  }

  withK(k) {
    const reducers = this.datas.map(item => {
      const copy = [...item]
      copy[this.indepentId] = Math.abs(copy[this.indepentId] - this.watchingData)
      return copy
    }).sort((cur, next) => cur[this.indepentId] - next[this.indepentId])
      .slice(0, k)
      .map(item => item[this.dependentId])
      .reduce((cur, next) => {
        cur[next] ? cur[next]++ : cur[next] = 1
        return cur
      }, {})

    return Object.entries(reducers).sort((cur, next) => next[1] - cur[1])[0][1]
  }
}

const res = new Knn([
  [1, 2],
  [2, 2],
  [4, 6],
  [5, 2],
  [9, 3],
  [8, 4]
]).setIndependentVariableIndex(0)
  .setDependentVariableIndex(1)
  .setWatchingData(5).withK(4)

console.log(res)

在 nest.js 中集成 next.js

next.js 是 react 服务端渲染现成的解决方案, 那么如何将其与 nest.js 集成起来呢?

配置 next.js 与 nest.js

安装依赖

npm install next nest-next react react-dom @babel/plugin-proposal-decorators 
npm install -D @types/next @types/react @types/react-dom

创建配置文件

touch .babelrc next.config.js

修改 .babelrc, 配置 babel

{
  "presets": [
    "next/babel"
  ],
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ]
  ]
}

修改 next.config.js, 配置 next.js

module.exports = {
  useFileSystemPublicRoutes: false
}

为了兼容 jsx, 我们需要修改 tsconfig.json, 添加 "jsx": "react", 为了解决 default import 报错, 需要添加 "esModuleInterop": true

修改 tsconfig

{
  "compilerOptions": {
    "module": "commonjs",
    "jsx": "react",
    "esModuleInterop": true,
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true
  },
  "exclude": [
    "node_modules",
    "dist"
  ]
}

在 nest.js 中使用 next.js

1. 在启动时注册渲染模块

编辑 src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import Next from 'next';
import { RenderModule } from 'nest-next';
import 'reflect-metadata';

async function bootstrap() {
  const dev = process.env.NODE_ENV !== 'production';
  // 初始化 next.js
  const app = Next({ dev });
  await app.prepare();

  // 初始化 server
  const server = await NestFactory.create(AppModule);
  // 创建 render controller
  const renderer = server.get(RenderModule);

  // 将 render controller 与 server, next.js 关联
  renderer.register(server, app, { viewsDir: null });
  await server.listen(process.env.PORT || 3000);
}
bootstrap() // 启动应用

默认的模板文件的根目录在项目根目录 /pages/views/ 下, 可通过 viewsDir 配置二级目录, 当指定为 null 时, 模板文件的根目录指向 /pages

2. 在模块中导入 renderModule 作为依赖

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RenderModule } from 'nest-next';

@Module({
  imports: [RenderModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

在 import RenderModule 之后, nest 将使用该 RenderModule 作为渲染引擎

3. 创建模板文件

上面已经配置模板文件根目录为 /pages, 接下来在该目录下新建 Index.tsx 文件, 内容如下

import * as React from 'react'
import Head from 'next/head'

const Index = () => {
  return (
    <div>
      <Head>
        <title>Hello World</title>
      </Head>
      <h1>hello world from next js</h1>
      <div>
        <p>
          p1
        </p>
      </div>
    </div>
  )
}

export default Index

4. 在路由中渲染模板

在控制器中通过 @Render 指定模板文件名称, 客户端接收到的响应将是渲染之后的结果

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

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

  @Get()
  @Render('Index')
  getHello(): void { }
}

Node.js 设计模式-结构模式-装饰器模式

有时候我们希望动态地给对象添加一些属性和方法或者说我们只希望使用已存在的对象的某些功能, 装饰器模式和代理模式能够实现相同的功能, 但代理模式主要用于访问控制且需要实现和被代理对象相同的接口, 而装饰器模式不需要实现相同的接口, 比如我们要实现一组敌人对象就可以使用装饰器模式.

基于 Enemy 类实现 BigEnemy 装饰类, 基于 BigEnemy 类实现 SuperEnemy 装饰类:

class Enemy {
  constructor() {
    this.blood = 100;
  }
}

class BigEnemy {
  constructor(baseEnemy) {
    this.blood = baseEnemy.blood + 900;
  }
}

class SuperEnemy {
  constructor(bigEnemy) {
    this.blood = bigEnemy.blood + 1000;
  }
}

module.exports = {
  Enemy,
  BigEnemy,
  SuperEnemy
}

使用装饰器模式:

const Enemy = require('./enemy');
const BigEnemy = require('./bigEnemy');
const SuperEnemy = require('./superEnemy');

const enemy = new Enemy();
const bigEnemy = new BigEnemy(enemy);
const superEnemy = new SuperEnemy(bigEnemy);

console.log(superEnemy.blood);

Node.js 设计模式-行为模式-命令模式

命令模式将行为和相应的参数封装成一个对象, 这样便支持将这些行为重复多次或多次取消, 命令模式包含命令执行者和命令对象. 比如有一个命令提示程序, 有两个命令, 一个是退出, 一个是创建文件, 我们便可以将这两个行为封装成两个命令对象, 分别是 ExitCommand 和 CreateCommand

创建命令执行者:

// 单例模式
class Executor {
  run(command) {
    console.log(`running command: ${command.name}`);
    command.execute();
  }
}

module.exports = new Executor()

创建命令对象:

class ExitCommand {
  constructor(_, __, target) {
    this.name = 'exit...bye';
    this.target = target;
  }

  execute() {
    this.target.close();
  }
}

class CreateCommand {
  constructor(filename, content, target) {
    this.name = `create ${filename}`;
    this.filename = filename;
    this.content = content;
    this.target = target;
  }

  execute() {
    require('fs').createWriteStream(
      require('path').join(__dirname, this.filename)
    ).write(
      this.content,
      (err) => err ? this.target.close() : this.target.prompt()
    );
  }
}

module.exports = {
  ExitCommand,
  CreateCommand
}

创建命令提示程序, 使用命令模式:

const { createInterface } = require('readline');
const { CreateCommand, ExitCommand } = require('./command.js');
const executor = require('./executor');

const interface = createInterface({
  input: process.stdin,
  output: process.stdout
})

const commandConfig = {
  'exit': (...args) => executor.run(new ExitCommand(...args)),
  'create': (...args) => executor.run(new CreateCommand(...args))
}

console.log('create <filename> <text> | exit');

interface.prompt();
interface.on('line', input => {
  const [command, ...[filename, ...fileText]] = input.split(' ');
  const text = fileText.join(' ');

  const commander = commandConfig[command];
  commander && commander(filename, text, interface);
});

除了简单的使用命令模式, 我们甚至可以通过命令执行者做 redo 和 undo 操作:

修改命令执行者, 提供 undo 功能

// 单例模式
class Executor {
   constructor() {
     this.history = [];
     this.undos = [];
   }

  run(command) {
    console.log(`running command: ${command.name}`);
    command.execute();
    this.history.push(command):
  }

  undo () {
    const command = this.history.pop();
    command.undo();
    this.undos.push(command);
  }
}

module.exports = new Executor()

修改命令对象, 执行 undo 操作:

// ...
class CreateCommand {
  constructor(filename, content, target) {
    this.name = `create ${filename}`;
    this.filename = filename;
    this.content = content;
    this.target = target;
  }

  execute() {
    require('fs').createWriteStream(
      require('path').join(__dirname, this.filename)
    ).write(
      this.content,
      (err) => err ? this.target.close() : this.target.prompt()
    );
  }

  undo() {
    require('fs').unlink(
      require('path').join(__dirname, this.filename),
      err => err ? this.target.close() : this.target.prompt();
    )
  }
}
// ...

Node.js 设计模式-行为模式-迭代模式

迭代模式是一种常见的模式, 它提供一组统一的接口供我们访问数据集合, 迭代模式包含 6 个访问接口, current 返回当前迭代器的值, hasNext 返回迭代器是否包含下一个元素, next 返回下一个元素, prev 返回上一个元素, first 返回第一个元素, last 返回最后一个元素

使用迭代模式封装元素集:

class Iterator {
  constructor(elements = []) {
    this.index = -1;
    if(elements.constructor === Set) {
      return this.elements = [...elements];
    } 
    if (elements.constructor === Map) {
      return this.elements = [...elements.values()];
    }
    if (elements === null || typeof elements !== 'object') {
      return this.elements = [elements];
    }
    this.elements = Object.values(elements);
  }

  current() {
    return this.elements[this.index];
  }
  
  hasNext() {
    return this.index < this.elements.length;
  }

  next() {
    if(this.hasNext()) {
      this.index++;
      return this.current();
    }
    return null;
  }

   prev() {
      if(htis.index > 0) {
         this.index--;
         return this.current();
      }
      return null;
   }

    first() {
      const {0: first} = this.elements;
      return first;
    }

    last() {
      const {[this.elements.length - 1]: last} = this.elements;
      return last;
    }
}

module.exports = Iterator;

使用迭代模式:

const Iterator = require('./iterator');

const test = new Iterator( {
  name: 'cattheblue',
  age: 20
} );

while(test.hasNext()){
  console.log(test.next());
}

以上结果可以看出, 无论是对象, Map, 集合还是数组, 迭代模式都能一种形式统一进行处理, 为遍历数据提供了统一的模型

Node.js 设计模式-创建模式-原型模式

Javascript 是基于原型的语言, 通过模板对象, 我们可以获得基于模板对象的自定义对象, 而不用每次都实例化对象写一些重复代码, 假设有一个 Enemy 对象, 表示游戏中的敌人, 每次生成一个敌人的时候, 都会做一些相同的初始化操作

class Enemy {
  constructor() {
    this.config = {};
  }
  set(name, value) {
     this.config = {...this.config, [name]: value}
  }
}

const enemy1 = new Enemy();
enemy1.set('blood', 100);
enemy1.set('blue bar', 100);

const enemy2 = new Enemy();
enemy2.set('blood', 100);
enemy2.set('blue bar', 100);

而通过原型模式, 可以将重复的部分分离出去.

首先添加原型克隆方法:

class Enemy {
   constructor() {
    this.config = {};
  }
  set(name, value) {
     this.config = {...this.config, [name]: value}
  }
   
  clone() {
    const instance = Object.create(Object.getPrototypeOf(this));
    instance.config = {...this.config};
    return instance;
  }
}

生成原型对象:

// prototype.js
const enemy = require('./enemy.js');

const prototype = new enemy();
prototype.set('blood', 100);
prototype.set('blue bar', 100);

module.exports = prototype;

使用原型模式:

const prototype = require('./prototype.js');

const enemy1 = prototype.clone();
enemy1.set('name', 'enemy1');

const enemy2 = prototype.clone();
enemy2.set('name', 'enemy2');

这里的实现和最初的实现功能上没什么区别, 但减少了不必要的重复, 所以, dont repeat yourself.

文本编辑器实时语法高亮的实现

代码语法高亮是一个代码编辑器基本的功能, 这里将会讨论其实现原理。我们需要对输入进行分类, 遇到关键字的时候需要对关键字进行标签的包装, 通过样式控制关键字的高亮, 其余非关键字保留格式并不设置任何样式, 这就是关键字高亮的基本思路

对输入进行分类, 我们首先想到的是词法解析器, 通过词法解析器我们可以清楚的知道当前输入的 token 是否是一个 keyword, 词法解析器的工作原理很简单, 读取输入的 token , 直到遇到文件末尾标记 EOF 为止, 核心代码如下

let token = this.nextToken()
while (token.type !== this.EOF) {
    token = this.nextToken()
 }

在每次读取到关键字的时候, 只需要将关键字在输入中的信息存储起来即可, 代码如下:

elementNode.keyWordCount++
const obj = {
  node: elementNode, 
  begin, end, // 起点位置和结束位置
  token // token 对象
}
this.keyWordElementArray.push(obj) // 保存关键字数组

有了关键字的信息, 我们就可以将输入按照关键字和关键字中间间隔的部分进行分类, 以便对关键字和非关键字进行不同的处理

Node.js 设计模式-创建模式-单例模式

这样的需求很普遍, 通常只需要在全局存在一个对象, 而不是每次都实例化, 比如需要这样一个 Logger 用于记录日志:

class Logger {
  constructor() {
    this.logs = [];
  }

  get count() {
    return this.logs.length;
  }

  log(...args) {
    const orig = Error.prepareStackTrace;
    Error.prepareStackTrace = (_, stack) => stack;
    const err = new Error();
    Error.captureStackTrace(err, this.log);
    const callee = err.stack[0];
    Error.prepareStackTrace = orig;
    const timestamp =  new Date.toISOString();
    this.logs = [...this.logs, ...(args.map(message => ({timestamp, message})))];
    console.log(
    '\x1b[33m%s\x1b[0m:',
    `${path.relative(process.cwd(), callee.getFileName())}:${callee.getLineNumber()}`, timestamp, ': ', ...args,
    );
  }
}

对应其他语言需要一个 singleton 来导出实例, 幸而借助 node 的模块导出的缓存特性, 可以很方便的导出一个唯一实例:

module.exports = new Logger();

Node.js 设计模式-创建模式-创建者模式

与工厂模式相反, 我们想创建一个自定义的对象, 并且不想 constructor 的参数太多以导致可读性变差, 通过创建者模式, 我们可以以声明式的方式初始化对象, 通过创建者模式, 我们生成的是一个配置对象, 最后通过配置对象生成目标对象

创建创建者

class UserBuilder {
  constructor(name) {
   this.name = name;
  }

  makeAge(age=18) {
     this.age = age;
     return this;
  }

  withMoney(money = 0) {
    this.money = money;
    return this;
  }

  build() {
    return new User(this);
  }
}

class User {
  constructor({name, age}) {
    this.name = name;
    this.age = age;
  }
}

使用创建者

const userBuilder = require('./builder');
const user = new userBuilder('blackthecat').makeAge(10).withMoney(1000).build();

Node.js 设计模式-行为模式-职责链模式

职责链模式允许我们将多个对象组织成一个链式结构来分别对请求进行处理, 最典型的就是 express 的中间件机制了. 对象对请求进行处理, 如果当前对象处理返回了结果, 结果就直接返回给调用者, 如果没有返回结果, 当前对象将请求交给下一个对象进行处理, 如果没有下一个对象, 则返回 undefined 或抛出错误.

const express = require('express');
const app = express();


const combine = (...middleWares) {
    return middleWares.reduce((prev, cur) => {
      if (prev === null) {
        return (req, res, next) => {
          return cur(req, res, next);
        };
      }
      return (req, res, next) => {
        return prev(req, res, () => {
          return cur(req, res, next);
        });
      };
    }, null);
 }

app.use(combine((req, res, next) => {
  if(!req.url.includes('404')) {
    return next();
  }
}, (req, res, next) => {
  res.status(404).json({
    message: '404 - Not - Found'
  })
}))

app.listen(3000, () => {});

Node.js 设计模式-创建模式-工厂模式

很多时候创建对象我们都是一个一个将类引入然后实例化, 而通过工厂模式, 就可以通过工厂方法去生成需要的对象, 所有的引入类都由工厂模块去维护

建立工厂方法:

const Employee = require('./employee');
const Manager = require('./manager');

const userMap = {
  employee: (...args) => { return new Employee(...args) },
  manager: (...args) => { return new Manager(...args) },
}

const userFactory = (type, ...rest) => {
  return userMap[type](...rest);
}

module.exports = userFactory;

通过工厂方法创建实例:

const userFactory = require('./factory');

const emp = userFactory('employee', 'blackthecat');
const man = userFactory('manager', 'bluethedog');

javascript 内存溢出的来源

javascript 有自动垃圾回收机制, 这来源于引用计数算法, 当一个对象不再被引用的时候, 垃圾回收器会回收这个对象, 以此来保证内存的可用空间, 例如定义一个对象:

var a = { test: 'a' }

a = null

当 a 被指向 null 时, {test: 'a'}这个对象的引用计数为 0, 这个时候就会被垃圾回收器清理

但即便有垃圾回收器帮我们做这些工作, 内存溢出还是很常见, 特别是对于那些在后台运行很长时间的 js 代码, 不论前端还是后端, 都存在这样的问题.

那么在什么情况下垃圾回收器会失效呢?

过多的全局变量

全局变量过多会被引用会导致内存无法被回收, 而新增的全局变量加重了这种情况

大量的事件监听器

当事件队列中的事件监听器没有被取消, 内存一直增长, 也是导致内存溢出的主要原因, 特别是对于一些单页面应用

setInterval

setInterval 中的函数会一直运行, 从而产生新的对象引用, 如果没有清除 setInterval, 内存溢出就在所难免!

Node.js 设计模式-结构模式-适配器模式

适配器模式用来适配不同环境的接口, 例如 localStorage 只能在浏览器中使用, 在 node 中无法使用, 但通过适配器我们可以将 localStorage 接口适配到 node 环境中

const { readFileSync, existsSync, writeFile, unlink } = require('fs');

class localStorage  {
  constructor() {
    if(existsSync('localStorage.json')) {
      this.items = JSON.parse(
        readFileSync('localStorage.json');
      );
    } else {
      this.items = {};
    } 
  }

  get length() {
    return Object.keys(this.items).length;
  }

  getItem(key) {
    return this.items[key];
  }

  setItem(key, value) {
    this.items[key] = value;
    writeFile('localStorage.json', JSON.stringify(this.items), err => console.error(err));
  }

  removeItem(key) {
    const { [key]: deleted, ...rests } = this.items;
    this.items = rests;
    writeFile('localStorage.json', JSON.stringify(this.items), err => console.error(err));
  }

  clear() {
    this.items = {};
    unlink('localStorage.json', err => console.error(err));
  }
}

module.exports = new localStorage();

使用适配器:

const localStorage = require('./localStorage');

localStorage.setItem('name', 'bluethedog');
console.log(localStorage.length);

Node.js 设计模式-结构模式-代理模式

和适配器模式类似, 代理模式实现和被代理对象相同的接口, 但是其实现是基于被代理对象, 代理模式主要用来做访问控制, 通过一个对象控制对另一个对象的访问, 在访问控制之外可以做一些自定义的功能, 例如一个在前端常见的需求, 有一个用户列表:
[{username: 'blob', id: 1}, {username: 'schema', id:2}, {username: 'blackthecat', id: 3}]
我们需要方便的通过用户 id 获取用户信息, 就可以使用代理模式来实现:

const idMapProxy = new Proxy(Array, {
  construct: (target, [originArray]) => {
    const idMap = {};
    originArray.forEach(item => idMap[item.id] = item)
    return new Proxy(new target(...originArray), {
      get: (target, props) => {
        if(props === 'push') {
          return (el) => {
            idMap[el.id] = el;
            return target[props].call(target, el);
          }
        } else if(props === 'findById') {
          return (id) => {
            return idMap[id];
          }
        } else {
          return target[props];
        }
      }
    })
  }
})

module.exports = idMapProxy;

使用代理实现 hash:

const idMapProxy = require('./proxy');

const data = new idMapProxy([{username: 'blob', id: 1}, {username: 'schema', id:2}, {username: 'blackthecat', id: 3}]);

data.push({username: 'kat', id: 9});
console.log(data.findById(9));

配置 nginx 反向代理支持 socket.io

Nginx是一款轻量级的Web服务器, 效率高, 使用它能实现加密, proxy, SSL加速, 负载均衡, 压缩, 安全等各种需求, 而反向代理是其常见用处.

之所以需要反向代理, 是由于有很多 web 服务并不是运行在 80 端口, 而为了能使一个请求能被不同的web服务处理, 就需要借助反向代理

socket.io 的路由不同于普通路由, 它的路由以 /socket.io/ 作为前缀, 配置的时候需要匹配到对应路由下

 location ~/socket.io/(.*) {
        proxy_pass http://127.0.0.1:8000;
 
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_redirect off;
 }

这样当元素 url 中包含 socket.io 的时候, nginx 就会将 http 请求连同请求头转发给 socket.io server,
编辑完成使用 systemctl restart nginx 重启nginx服务即可

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.