海屿文章列表

回顾 is-promise 库迁移 ESM 事故

is-promise 只做一件事,判断 JavaScript 对象是否为 Promise,但这个包被近百万个项目所依赖。作者在 4月25日发布的新版本并未遵循正确的 ES 模块标准,从而导致更新完成后,所有在构建时使用 is-promise 库的项目几乎全部报错。

这里犯了几个个错误 https://github.com/then/is-promise/blob/8e51d62bf158eb0685cd6109f0137472e8c3cb91/package.json#L7-L10

  • 以为 exports 和 main 一样,但实际上需要 ./ 前缀。
  • exports 不仅限制你能导入什么,还得限制你怎么导入

exports 字段

我们都知道 node 模块导出有 main ,所有版本的Node.js都支持,但是能力有限:它仅定义包的主要入口点。

"exports"扩展了 main,而且二者同时存在时,优先使用 exports。

Node.js支持以下条件:

  • "import" 通过import或 加载包时匹配import()。可以同时引用ES模块或CommonJS文件, import并且import()可以加载ES模块或CommonJS源。
  • "require"通过加载包时匹配require()。由于require()仅支持CommonJS,因此引用的文件必须为CommonJS。
  • "node"适用于任何Node.js环境。可以是CommonJS或ES模块文件。这种情况应该总是在"import"或 之后出现"require"。
  • "default"是CommonJS或ES模块文件,应始终排在最后。

条件导出

一个package.json文件可以直接定义单独的CommonJS和ES模块入口点:
条件匹配的规则是从上至下,所以应按对象顺序使用从最具体到最不具体的条件。

 "exports": {
    ".": [
      {
        "import": "./index.mjs",
        "require": "./index.js",
        "default": "./index.js"
      },
      "./index.js"
    ]
  },

嵌套条件

"exports": {
    "browser": "./feature-browser.mjs",
    "node": {
      "import": "./feature-node.mjs",
      "require": "./feature-node.cjs"
    }
  }

参考文章:
https://github.com/then/is-promise/issues/13
https://medium.com/javascript-in-plain-english/is-promise-post-mortem-cab807f18dcc

访问者模式中的分派概念

images.png

静态绑定

静态绑定就是指在编译期就已经确定执行哪一个方法。函数的重载(方法名相同而参数不同)就是静态绑定的,重载时,执行哪一个方法在编译期就已经确定下来(编译时多态)。

动态绑定

动态绑定实在运行时判断所引用对象的实际类型,根据其实际的参数类型调用其相应的方法(运行时多态) 。

双分派(double dispatch)

分派过程就是确定一个方法调用的过程,双分派就是见人说人话,见鬼说鬼话,将静态绑定和动态绑定结合起来,也就是说根据运行时传入对象的类型确定方法调用的过程。
重载是静态绑定,多态是动态绑定(运行时进行),双分派把多态放在重载之前,以实现在运行时动态判断执行那个子类的方法。因为编译器知道this所指向的实例的类型,因此将其作为参数传入到函数中就可以实现。

class Man : public Base {
public:
    int getSpeakRes(SpeakBase& speak) {
        return speak.speakWord(this);  //编译器知道this是哪个类型实例
    }
}

class Ghost : public Base {
public:
    int getSpeakRes(SpeakBase& speak) {
        return speak.speakWord(this);
    }
}

class Speak : public SpeakBase {
public:
    int speakWord(Man& man) {
        return "Man"; 
    }
    int speakWord(Ghost& ghost) {
        return "Ghost"; 
    }
}
class Board {
    drawFigure(aFigure) {
        aFigure.getDrawnOn(this)
    }
    drawTriangle(aTriangle) {
        // ...
    }
    drawSquare(aSquare) {
        // ...
    }
}

class Triangle extends Figure {
    getDrawnOn(aDrawableSurface) {
        aDrawableSurface.drawTriangle(this)
    }
}

class Square extends Figure {
    getDrawnOn(aDrawableSurface) {
        aDrawableSurface.drawSquare(this)
    }
}


----

let board = new Board();
let triangle = new Triangle();

board.drawFigure(triangle)

参考文章:

https://www.cnblogs.com/loveis715/p/4423464.html
http://yaoyao.codes/c++/2015/04/26/cpp-double-dispatch
https://www.typescriptlang.org/docs/handbook/functions.html#overloads
https://www.mikealche.com/software-development/refactor-long-if-or-switch-blocks-with-double-dispatch-in-javascript

Nuxt 实现组件缓存

组件缓存

nuxt 中提供了 bundleRenderer 选项,它会作为vue-server-renderercreateRenderer方法的配置。其中cache对象必须getset方法。

type RenderCache = {
  get: (key: string, cb?: Function) => string | void;
  set: (key: string, val: string) => void;
  has?: (key: string, cb?: Function) => boolean | void;
};

lru-cache创建的实例包含了getset方法,所以可以直接配置。

export default {
  render: {
    bundleRenderer: {
      cache: require('lru-cache')({
        max: 1000,
        maxAge: 1000 * 60 * 15
      })
    }
  }
}

然后可缓存组件必须定义一个唯一name选项。如果渲染结果也依赖于prop,那么还需要修改serverCacheKey的计算方式。如果serverCacheKey明确返回false,那么就将直接抛弃缓存,重新渲染。

name: 'Date',
serverCacheKey: props => props.item.id,

你也可以安装@nuxtjs/component-cache,它也是使用lru-cache实现的组件缓存。但使用lru-cache在内存中进行缓存的缺点也很明显,开启多进程时每个进程都会缓存一份资源浪费,也有可能出现缓存不一致的情况。

不应缓存的组件

缓存组件必须十分谨慎,下面的组件是不能缓存的:

  • 具有可能依赖于全局状态的子组件。
  • 有子组件会在渲染上产生副作用context。

参考文章

https://gist.github.com/Tuarisa/b7c07289eb32b2a7a77be270e1c8360f

Jest mock 函数和模块

Jset 中有几种创建模拟函数的方法。该jest.fn方法允许我们直接创建一个新的模拟函数。如果要模拟对象方法,则可以使用jest.spyOn。如果要模拟整个模块,可以使用jest.mock。

mock 函数

Jest 可以捕获对函数的调用(包括调用中传递的参数),也可以直接擦除函数内部的实现,直接返回mock的结果。

function getDouble(val, callback) {
  if(val < 0) {
    return;
  }
  setTimeout(() => {
    callback(val * val);
  }, 100);
};

const mockFn = jest.fn();
getDouble(10, mockFn);

expect(mockFn).not.toHaveBeenCalled()
setTimeout(() => {
  expect(mockFn).toHaveBeenCalledTimes(1);
  expect(mockFn).toHaveBeenCalledWith(20);
}, 110);

mock 模块

import {getParameter, getFilterInfo} from "../service";


jest.mock('../service');
getFilterInfo.mockResolvedValue([model])
getParameter.mockResolvedValue(parameter)

参考文章

https://www.pluralsight.com/guides/how-does-jest.fn()-work

使用 Jest 对 Nuxt 应用进行单元测试

配置 jest.config.js

module.exports = {
  // tell Jest to handle `*.vue` files
  moduleFileExtensions: ["js", "json", "vue"],
  watchman: false,
  moduleNameMapper: {
    "^~/(.*)$": "<rootDir>/$1",
    "^~~/(.*)$": "<rootDir>/$1",
    "^@/(.*)$": "<rootDir>/$1"
  },
  transform: {
    // process js with `babel-jest`
    "^.+\\.js$": "<rootDir>/node_modules/babel-jest",
    // process `*.vue` files with `vue-jest`
    ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
  },
  snapshotSerializers: ["<rootDir>/node_modules/jest-serializer-vue"],
  collectCoverage: true,
  collectCoverageFrom: [
    "<rootDir>/components/**/*.vue",
    "<rootDir>/pages/*.vue"
  ]
};

配置 Babel config

function isBabelLoader(caller) {
  return caller && caller.name === "babel-loader";
}

module.exports = function(api) {
  if (api.env("test") && !api.caller(isBabelLoader)) {
    return {
      presets: [
        [
          "@babel/preset-env",
          {
            targets: {
              node: "current"
            }
          }
        ]
      ]
    };
  }
  return {};
};

添加测试脚本到 package.json

"test": "jest --colors --verbose"

因为Jest 的测试文件中的方法是全局的,所以一般编辑器不会给提示,如果想要编辑器有提示的话可以安装 @types/jest

参考文章

https://medium.com/@gogl.alex/nuxt-jest-setup-from-scratch-8905d3880daa
https://medium.com/@brandonaaskov/how-to-test-nuxt-stores-with-jest-9a5d55d54b28