Giter Club home page Giter Club logo

cheungseol.github.io's Introduction

header image

Hi there 👋

Here are some ideas to get you started:

  • 🔭 I’m currently working on ...
  • 🌱 I’m currently learning ...
  • 👯 I’m looking to collaborate on ...
  • 🤔 I’m looking for help with ...
  • 💬 Ask me about ...
  • 📫 How to reach me: ...
  • 😄 Pronouns: ...
  • ⚡ Fun fact: ...

cheungseol.github.io's People

Contributors

cheungseol avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

cheungseol.github.io's Issues

脚本ssh登录

脚本ssh登录开发机

test.sh

chmod a+x test.sh

#!/usr/bin/expect -f
  
set USER cheungseol // username
set HOST 1.1.1.1  // fake host addr
set PASSWORD test123 // password
set timeout -1  // expect mode never time out
  
spawn ssh $USER@$HOST
expect "*assword:*"  
  
send "$PASSWORD\r"  
# expect eof 
interact 

Chrome Extention

Chrome 浏览器扩展

1. manifest.json 文件

manifest 是一个JSON格式的元数据(meta data)文件,通过它配置扩展程序的名称、描述、版本号等信息。扩展程序的行为和权限也是通过manifest 来配置的。

示例:

{
  "manifest_version": 2,

  "name": "Getting started example",
  "description": "This extension shows a Google Image search result for the current page",
  "version": "1.0",

  "browser_action": {
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  },
  "permissions": [
    "activeTab",
    "https://ajax.googleapis.com/"
  ]
}

以上manifest文件中配置了 browser action 和 permission, activeTab 权限允许程序获知当前浏览器 tab 的URL, host permission (主机域名权限)允许程序访问 Google Image search API。

2. 资源文件

manifest文件中配置 browser action 时引用了icon.png 和 popup.html 文件,被引用的这两个文件必须在程序中。

icon.png 是显示在浏览器右上角的扩展程序图标, 通常是一个 19px* 19px的 PNG 格式图片。

popup.html 负责渲染点击扩展程序时弹出的小窗口页面,和普通的web 开发中的标准HTML 文件没有差别。

<!doctype html>
<!--
 This page is shown when the extension button is clicked, because the
 "browser_action" field in manifest.json contains the "default_popup" key with
 value "popup.html".
 -->
<html>
  <head>
    <title>Getting Started Extension's Popup</title>
    <style>
      body {
        font-family: "Segoe UI", "Lucida Grande", Tahoma, sans-serif;
        font-size: 100%;
      }
      #status {
        /* avoid an excessively wide status text */
        white-space: pre;
        text-overflow: ellipsis;
        overflow: hidden;
        max-width: 400px;
      }
    </style>

    <!--
      - JavaScript and HTML must be in separate files: see our Content Security
      - Policy documentation[1] for details and explanation.
      -
      - [1]: https://developer.chrome.com/extensions/contentSecurityPolicy
     -->
    <script src="popup.js"></script>
  </head>
  <body>
    <div id="status"></div>
    <img id="image-result" hidden>
  </body>
</html>

3. popup.js

扩展程序弹出窗口内容的渲染逻辑通过 popup.js 文件控制,在popup.html 文件中通过script 标签引入。

/**
 * Get the current URL.
 *
 * @param {function(string)} callback - called when the URL of the current tab
 *   is found.
 */
function getCurrentTabUrl(callback) {
  // Query filter to be passed to chrome.tabs.query - see
  // https://developer.chrome.com/extensions/tabs#method-query
  var queryInfo = {
    active: true,
    currentWindow: true
  };

  chrome.tabs.query(queryInfo, function(tabs) {
    // chrome.tabs.query invokes the callback with a list of tabs that match the
    // query. When the popup is opened, there is certainly a window and at least
    // one tab, so we can safely assume that |tabs| is a non-empty array.
    // A window can only have one active tab at a time, so the array consists of
    // exactly one tab.
    var tab = tabs[0];

    // A tab is a plain object that provides information about the tab.
    // See https://developer.chrome.com/extensions/tabs#type-Tab
    var url = tab.url;

    // tab.url is only available if the "activeTab" permission is declared.
    // If you want to see the URL of other tabs (e.g. after removing active:true
    // from |queryInfo|), then the "tabs" permission is required to see their
    // "url" properties.
    console.assert(typeof url == 'string', 'tab.url should be a string');

    callback(url);
  });

  // Most methods of the Chrome extension APIs are asynchronous. This means that
  // you CANNOT do something like this:
  //
  // var url;
  // chrome.tabs.query(queryInfo, function(tabs) {
  //   url = tabs[0].url;
  // });
  // alert(url); // Shows "undefined", because chrome.tabs.query is async.
}

/**
 * @param {string} searchTerm - Search term for Google Image search.
 * @param {function(string,number,number)} callback - Called when an image has
 *   been found. The callback gets the URL, width and height of the image.
 * @param {function(string)} errorCallback - Called when the image is not found.
 *   The callback gets a string that describes the failure reason.
 */
function getImageUrl(searchTerm, callback, errorCallback) {
  // Google image search - 100 searches per day.
  // https://developers.google.com/image-search/
  var searchUrl = 'https://ajax.googleapis.com/ajax/services/search/images' +
    '?v=1.0&q=' + encodeURIComponent(searchTerm);
  var x = new XMLHttpRequest();
  x.open('GET', searchUrl);
  // The Google image search API responds with JSON, so let Chrome parse it.
  x.responseType = 'json';
  x.onload = function() {
    // Parse and process the response from Google Image Search.
    var response = x.response;
    if (!response || !response.responseData || !response.responseData.results ||
        response.responseData.results.length === 0) {
      errorCallback('No response from Google Image search!');
      return;
    }
    var firstResult = response.responseData.results[0];
    // Take the thumbnail instead of the full image to get an approximately
    // consistent image size.
    var imageUrl = firstResult.tbUrl;
    var width = parseInt(firstResult.tbWidth);
    var height = parseInt(firstResult.tbHeight);
    console.assert(
        typeof imageUrl == 'string' && !isNaN(width) && !isNaN(height),
        'Unexpected respose from the Google Image Search API!');
    callback(imageUrl, width, height);
  };
  x.onerror = function() {
    errorCallback('Network error.');
  };
  x.send();
}

function renderStatus(statusText) {
  document.getElementById('status').textContent = statusText;
}

document.addEventListener('DOMContentLoaded', function() {
  getCurrentTabUrl(function(url) {
    // Put the image URL in Google search.
    renderStatus('Performing Google Image search for ' + url);

    getImageUrl(url, function(imageUrl, width, height) {

      renderStatus('Search term: ' + url + '\n' +
          'Google image search result: ' + imageUrl);
      var imageResult = document.getElementById('image-result');
      // Explicitly set the width/height to minimize the number of reflows. For
      // a single image, this does not matter, but if you're going to embed
      // multiple external images in your page, then the absence of width/height
      // attributes causes the popup to resize multiple times.
      imageResult.width = width;
      imageResult.height = height;
      imageResult.src = imageUrl;
      imageResult.hidden = false;

    }, function(errorMessage) {
      renderStatus('Cannot display image. ' + errorMessage);
    });
  });
});

  1. 加载扩展程序

为了便于发布,从Chrome Web 商店中下载下来的扩展程序都已经被打包成以 .crx 为后缀的文件。
但是为了便于开发调试,需要如下设置:

  1. 在浏览器中访问 chrome://extensions
    或者点击浏览器右上角的菜单按钮,在弹出的菜单面板中选择-更多工具-扩展程序

  2. 在打开的新页面中,选中顶部的 开发者模式

  3. 在打开的新页面中,点击 加载已经解压的扩展程序 按钮, 从弹出的文件选择器中选择扩展程序的代码文件目录

或者可以将扩展程序的代码目录拖拽到 chrome://extensions 页面,同样能加载开发中的扩展程序

参考

Getting Started: Building a Chrome Extension

Manifest File Format

Webpack in Action (1) DLL

webpack 提供了一些 Plugins 来提升打包时的性能。

CommonsChunkPlugin

当 webpack 配置了多个入口文件时,如果这些文件都 require 了相同的模块,webpack 会为每个入口文件引入一份相同的模块,显然这样做,会使得相同模块变化时,所有引入的 entry 都需要一次 rebuild,造成了性能的浪费。

CommonsChunkPlugin 把相同的模块提取出来单独打包,从而减小 rebuild 时的性能消耗。

DLLPlugin 和 DllReferencePlugin

项目中常常会引入许多第三方 npm 包,尽管我们不会修改这些包,但是webpack 仍会要在每次 build 的过程中重复打包,消耗构建性能。

DLLPlugin 通过前置这些依赖包的构建,来提高真正的 build 和 rebuild 的构建效率。

例子:

比如我们的项目中使用了 react ,我们把 react 和 react-dom 打包成为 dll bundle。

DLLPlugin 需要一个单独的 webpack 配置文件:

webpack.dll.config.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    vendor: ['react', 'react-dom']
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].dll.js',
    /**
     * output.library
     * 将会定义为 window.${output.library}
     * 在这次的例子中,将会定义为`window.vendor_library`
     */
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      /**
       * path
       * 定义 manifest 文件生成的位置
       * [name]的部分由entry的名字替换
       */
      path: path.join(__dirname, 'dist', '[name]-manifest.json'),
      /**
       * name
       * dll bundle 输出到那个全局变量上
       * 和 output.library 一样即可。 
       */
      name: '[name]_library'
    })
  ]
};

用 webpack 运行 webpack.dll.config.js 文件后,就会在 dist 目录下生成 dll bundle 和对应的 manifest 文件


$ ./node_modules/.bin/webpack --config webpack.dll.config.js
Hash: 36187493b1d9a06b228d
Version: webpack 1.13.1
Time: 860ms
        Asset    Size  Chunks             Chunk Names
vendor.dll.js  699 kB       0  [emitted]  vendor
   [0] dll vendor 12 bytes {0} [built]
    + 167 hidden modules

$ ls dist
./                    vendor-manifest.json
../                   vendor.dll.js

manifest 文件的格式大致如下,由包含的 module 和对应的 id 的键值对构成:

{
  "name": "vendor_library",
  "content": {
    "./node_modules/react/react.js": 1,
    "./node_modules/react/lib/React.js": 2,
    "./node_modules/process/browser.js": 3,
    "./node_modules/object-assign/index.js": 4,
    "./node_modules/react/lib/ReactChildren.js": 5,
    "./node_modules/react/lib/PooledClass.js": 6,
    "./node_modules/fbjs/lib/invariant.js": 7,
...

在项目的 webpack 配置文件(不是上面的 webpack.dll.config.js )中通过 DLLReferencePlugin 来使用刚才生成的 DLL Bundle。

webpack.conf.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    'dll-user': ['./index.js']
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].bundle.js'
  },
  // ----在这里追加----
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      /**
       * 在这里引入 manifest 文件
       */
      manifest: require('./dist/vendor-manifest.json')
    })
  ]
  // ----在这里追加----
};

执行 webpack,会在 dist 下生成 dll-user.bundle.js,约 700K,耗时 801ms:

./node_modules/.bin/webpack
Hash: 3bc7bf760779b4ca8523
Version: webpack 1.13.1
Time: 70ms
             Asset     Size  Chunks             Chunk Names
dll-user.bundle.js  2.01 kB       0  [emitted]  dll-user
   [0] multi dll-user 28 bytes {0} [built]
   [1] ./index.js 145 bytes {0} [built]
    + 3 hidden modules

打包出来的文件大小是 2.01K,耗时 70 ms,大大提高了 build 和 rebuild 的效率。

dll 和 external 的比较

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    'ex': ['./index.js']
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].bundle.js'
  },
  externals: {
    // require('react')
    'react': 'React',
    // require('react-dom')
    'react-dom': 'ReactDOM'
  }
};

  1. 像是 react 这种已经打好了生产包的使用 externals 很方便,但是也有很多 npm 包是没有提供的,这种情况下 DLLBundle 仍可以使用。

  2. 如果只是引入 npm 包一部分的功能,比如 require('react/lib/React') 或者 require('lodash/fp/extend') ,这种情况下 DLLBundle 仍可以使用。

  3. 如果只是引用了 react 这类的话,externals 因为配置简单所以也推荐使用。

dll 和 entry vendor 的比较

vendor配置本质目的是为了跨页面加载库不必重复。
webpack的资源入口通常是以entry为单元进行编译提取,那么当多entry共存的时候,CommonsChunkPlugin的作用就会发挥出来,对所有依赖的chunk进行公共部分的提取,但是在这里可能很多人会误认为抽取公共部分指的是能抽取某个代码片段,其实并非如此,它是以module为单位进行提取。

参考

Optimizing Webpack build times and improving caching with DLL bundles

https://juejin.im/entry/5769f8dc128fe10057d2f4ae

Webpack的dll功能

彻底解决Webpack打包性能问题

http://dev.dafan.info/detail/329716?p=34-14

用 webpack 实现持久化缓存

浏览器重绘重排

js 中调用以下方法时,会触发浏览器同步执行布局计算,布局抖动:

The browser does two major things when rendering HTML: layouts, which are calculations to determine the dimensions and position of the element, and paints, which make the pixels show up in the right spot with the correct color.

Element APIs

box metrics 相关

elem.offsetLeft

offsetTop

offsetWidth

offsetHeight

offsetParentelem.clientLeft

clientTop

clientWidth

clientHeight

elem.getClientRects()

elem.getBoundingClictReact()

scroll 相关

elem.scrollBy()

scrollTo()

elem.scrollIntoView()

scrollIntoViewIfNeeded()

elem.scrollWidth

scrollHeight

scrollLeft

scrollTop

focus 相关

elem.focus()

chromium focus 相关实现

其它

elem.computedRole/computedNameelem.innerText

Window dimensions

window.scrollX/scrollYwindow.innerHeight/innerWidthwindow.visualViewport.height/width/offsetTop/offsetLeft

document

documents.scrollingElementdocument.elementFromPoint

Forms selection + focus

inputElem.focus()/select()textareaElem.select()

Mouse events

mouseEvt.layerX/layerY/offsetX/offsetY

### getComputedStyle

window.getComputedStyle()

Range dimensions

range.getClientRects()

getBoundingClientRect()

例子

事件系统

fastDom 优化

相交检测

常见的需要相交检测的场景:

  • 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
  • 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
  • 在用户看见某个区域时执行任务或播放动画

过去常用的方法

判断元素是否在浏览器视窗内时,常见的方法使用:

// Old-school scroll event listening (avoid)
window.addEventListener('scroll', () => checkForVisibility)

window.addEventListener('resize', () => checkForVisibility)

function checkForVisibility() { 
animatedElements.map(element => {    
    const distTop = element.getBoundingClientRect().top    
    const distBottom = element.getBoundingClientRect().bottom    
    const distPercentTop = Math.round((distTop / window.innerHeight) * 100)    
    const distPercentBottom = Math.round((distBottom / window.innerHeight) * 100)
   // Based on this position, animate element accordingly  
}}

getBoundingClientRect() 方法会触发浏览器 trigger reflows,可能会造次性能问题。这种方式实现的相交检测通常要用到事件监听,并且需要频繁调用Element.getBoundingClientRect() 方法以获取相关元素的边界信息。事件监听和调用 Element.getBoundingClientRect()都是 在主线程上运行,因此频繁触发、调用可能会造成性能问题。这种检测方法极其怪异且不优雅。

(假如有一个无限滚动的网页,开发者使用了一个第三方库来管理整个页面的广告,又用了另外一个库来实现消息盒子和点赞,并且页面有很多动画(译注:动画往往意味着较高的性能消耗)。两个库都有自己的相交检测程序,都运行在主线程里,而网站的开发者对这些库的内部实现知之甚少,所以并未意识到有什么问题。但当用户滚动页面时,这些相交检测程序就会在页面滚动回调函数里不停触发调用,造成性能问题,体验效果让人失望。)

推荐的方法

Intersection Observer API 允许配置一个回调函数,当以下情况发生时会被调用

  • 每当目标(target)元素与设备视窗或者其他指定元素发生交集的时候执行。设备视窗或者其他元素我们称它为根元素或根(root)。
  • Observer第一次监听目标元素的时候

使用 IntersectionObservers 不会有性能问题:提供了一种 异步 检测目标元素与祖先元素或 viewport 相交情况变化的方法。

// Create an intersection observer with default options, that 
// triggers a class on/off depending on an element’s visibility
// in the viewport

const animationObserver = new IntersectionObserver((entries, observer) => {  
    for (const entry of entries) {    
           entry.target.classList.toggle('build-in-animate', entry.isIntersecting)  
    }
});

// Use that IntersectionObserver to observe the visibility
// of some elements

for (const element of querySelectorAll('.js-build-in')) { 
   animationObserver.observe(element);
}

Intersection Observer API 会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时(或者 viewport ),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。这样,我们网站的 主线程不需要再为了监听元素相交而辛苦劳作,浏览器会自行优化元素相交管理

注册的 回调函数将会在主线程中被执行。所以该函数执行速度要尽可能的快。如果有一些耗时的操作需要执行,建议使用 Window.requestIdleCallback() 方法

Intersection Observer API 无法提供重叠的像素个数或者具体哪个像素重叠,他的更常见的使用方式是——当两个元素相交比例在 N% 左右时,触发回调,以执行某些逻辑

IntersectionObserver API 并不会每次在元素的交集发生变化的时候都会执行回调。相反它使用了thresholds参数。当你创建一个observer的时候,你可以提供一个或者多个number类型的数值用来表示target元素在root元素的可见程序的百分比 API的回调函数只会在元素达到thresholds规定的阈值时才会执行**。

例如,当想要 target 在 root 元素中的可见性每超过25%或者减少25%的时候都通知一次。你可以在创建observer的时候指定thresholds属性值为[0, 0.25, 0.5, 0.75, 1],你可以通过检测在每次交集发生变化的时候的都会传递回调函数的参数"IntersectionObserverEntry.isIntersecting"的属性值来判断target元素在root元素中的可见性是否发生变化。如果isIntersecting 是 true,target元素的至少已经达到thresholds属性值当中规定的其中一个阈值,如果是false,target元素不在给定的阈值范围内可见。

react-router 升级

react-router": "^2.5.2 4.2.0

"react-router-dom": "^4.2.2",

需要重新webapck dll plugin 运行一次。

Deno 实战

Deno 是一个安全的 js/ts runtime 。

下面从安装、使用以及和 Node.js 对比进行 Deno 实践

安装

  • 可以使用 homebrew 安装
  • 或者使用类似 nvm 的版本管理工具 asdf 安装
  • ...

以下代码运行环境:

环境 版本
deno 0.32.0
v8 1.1.08
typescript 3.7.2

First Blood

hello world

在控制台执行以下脚本,运行一个 hello word 的例子试下:

deno https://deno.land/std/examples/welcome.ts

执行结果如下,可以看到 deno 可以直接运行第三方脚本,首先从远程下载 welcome.ts 文件,然后再经过编译运行输出运行结果:
image

typescript runtime

Deno 自带 tsc,可以看到 deno 将 welcome.ts 文件安装到了 deps 目录下,gen 目录下是 ts 编译后生成的 js 文件:
image

安全沙盒环境

Deno 是一个类似沙盒的安全环境,限制了文件、网络等操作。下面编写一个测试脚本:

// ES6 Module Import
import { serve } from "https://deno.land/[email protected]/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
// Top Level Await
for await (const req of s) {  req.respond({ body: "Hello World\n" });}

可以看到 deno 中支持直接使用 ES6 Module Import 以及 Top Level Await
由于 deno 限制了网络访问能力,所以直接运行上面的脚本会报错:
image
加上 allow-net 条件参数再次运行 deno --allow-net test-0.ts 即可。

Promise 和 Top level await

和 nodejs 的区别是,nodejs 设计之初都是基于 callback 形式,而 deno 中所有异步操作都会返回一个 Promise,并且支持 Top Level Await。

Promise

以创建子进程和系统信号为例:

  • 创建子进程:
// create subprocess
const p = Deno.run({
    args: ["echo", "hello"]
  });
  
// await its completion
await p.status();
  • Signals
const sig = Deno.signal(Deno.Signal.SIGINT);
setTimeout(() => { 
    sig.dispose(); 
}, 5000);
for await (const _ of sig) {  
    console.log("interrupted");
}

Top level Await

Top level await 目前已经进入 TC39 stage-3,再 ES8 之前只能再 async 函数中使用 await,通过 top level await 可以直接在模块顶层使用 await 语句。
Deno 中可以直接使用 top level await 语法:(目前 Node.js 可以在 ES Modules 中使用)

const strings = await import(`/i18n/${navigator.language}`);
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");

模块加载和打包

Deno 的设计避免 Node.js 的模块加载机制,支持 ES modules 而不使用 require,通过 ULR 或者文件路径引用依赖包。并且 deno 自带 bundle 功能,打包出的文件可以直接在浏览器中使用:

deno bundle main.ts test.bundle.js

生成的 bundle 文件基本类似于一个简单版本的 webpack 打包后的内容:

(function() {  
    const modules = new Map();  
    const require = (deps, resolve, reject) => {    
        try {      
            // ...      
            resolve(getExports(deps[0]));    
        } catch (e) {      
            // ...    
        }  
    };  
    // 加载的模块存入 modules  
    define = (id, dependencies, factory) => {    
        // ...    
        modules.set(id, {      
            dependencies,      
            factory,      
            exports: {}    
        });  
    };  
    // 执行模块内部代码逻辑  function getExports(id) {      
    // ...      
    const { factory, exports } = module;      
    delete module.factory;        
    const dependencies = module.dependencies.map(id => {          
        if (id === "require") {            
            return require;          
        } else if (id === "exports") {            
            return exports;          
        }          
            return getExports(id);        
        });        
        factory(...dependencies);      
        return exports;  
    }  
    // 加载模块  
    instantiate = dep => {    
        const result = getExports(dep);    
        return result;  
    };
})();
// ...
// 将 main 模块和依赖的 imported 模块放入 modules Map
define("main", ["require", "exports", "./imported.ts"], function (require, exports) {    
    "use strict";    
    Object.defineProperty(exports, "__esModule", { value: true });    
    // ...
});
// 加载 main 模块
instantiate("main");

包管理

Deno 摒弃类似 npm 这样的三方包管理模式,三方模块都是以类似浏览器中 import 的形式直接通过链接的形式引用:

import * as _path from "https://deno.land/[email protected]/path/mod.ts";

Deno 也没有 node.js 项目中 package.json 和 node_modules ,如果在项目中进行依赖模块维护的话,可以有两种方式:

  • 以类似下面的形式在项目中创建 dep.ts 文件,这个文件负责引入各个版本的依赖模块,项目中其它文件都统一从 dep.ts 中引入导出的模块:
// dep.ts
import * as _path from "https://deno.land/[email protected]/path/mod.ts";

export {  serve,  ServerRequest,  Response,  Server} from "https://deno.land/[email protected]/http/server.ts";
export {  Status,  STATUS_TEXT} from "https://deno.land/[email protected]/http/http_status.ts";
export { parse } from "https://denolib.com/denolib/[email protected]/mod.ts";
export const path = _path;
  • 使用 imports json 文件,这个文件类似 webpack alias 可以给引入的依赖包定义别名,项目中的其它文件可以直接使用别名
// imports.json
{    
    "imports": {       
        "http/": "https://deno.land/std/http/"    
    }
}
// index.ts
import { serve } from "http/server.ts";
const body = new TextEncoder().encode("Hello World\n");
for await (const req of serve(":8000")) {  
    req.respond({ body });
}
## 预置功能
此外 Deno 还包揽了 test 、format 的功能,通过 `deno fmt file.ts` 可以格式化代码,类似常用的 prettier 。lint 、debug 等功能还再进行中。
## 浏览器环境兼容
deno  browser 是两个 js runtime,deno 的设计兼容浏览器环境实现的,并且还兼容很多 node.js api。
- 与浏览器兼容的 API:
比如 deno 实现了 *window* 以及 *eventListener*  API 
main.ts 分别通过 addEventListener  onload  window load , unload 上绑定事件监听:
```ts
// main.ts
import "./imported.ts";
const handler = (e: Event): void => {  
    console.log(`got ${e.type} event in event handler (main)`);
};
window.addEventListener("load", handler);
window.addEventListener("unload", handler);
window.onload = (e: Event): void => {  
    console.log(`got ${e.type} event in onload function (main)`);
};
window.onunload = (e: Event): void => {  
    console.log(`got ${e.type} event in onunload function (main)`);
};

imported.ts 分别通过 addEventListener 和 onload 在 window load , unload 上绑定事件监听:

// imported.ts
const handler = (e: Event): void => { 
    console.log(`got ${e.type} event in event handler (imported)`);
};
window.addEventListener("load", handler);
window.addEventListener("unload", handler);
window.onload = (e: Event): void => { 
    console.log(`got ${e.type} event in onload function (imported)`);
};
window.onunload = (e: Event): void => { 
    console.log(`got ${e.type} event in onunload function (imported)`);
};
console.log("log from imported script");

执行 deno main.ts 脚本,可以看到 addEventListener 和 on 事件的区别,imported.ts 中的 on 事件会被 main.ts 中的覆盖,而 addEventListener 不会被覆盖:
image

  • 在浏览器中运行:
    将上面 deno bundle 打包出的脚本,直接以 script 的形式内联到 index.html 中:
<script type="module" src="test.bundle.js"></script>

打开浏览器控制台可以看到运行结果:
image

使用 node_module

因为 Deno 支持 ES modules,所以可以使用部分 ES module 格式的 node.js 包,https://jspm.io/ 上面是一些 ES module 格式的 npm 包,如果没有依赖 node.js API 的话,都是可以直接在 deno 或者 浏览器环境使用的:
比如:

const r = await import('https://dev.jspm.io/react')
console.log(r)

输出结果:
image

  • 兼容 Node.js API:
    deno std 库里面包含已经兼容的 Nods.js API
    image

语法

Deno 直接支持了许多新的 TC 39 语法,比如 nullish default 和 optional chaining ,以及上面提到的 top level await 等等:

let a = undefinedconsole.log(a ?? 1)
let b = {}console.log(b?.["test"] ?? 10)

输出结果:
image

API

在终端输入 deno ,可以进入 REPL,执行 console.log(Deno) ,返回的是 Deno 提供的各种 API:
image

v.s. Node.js

Deno Node.js
技术 chrome v8、ruby 和 tokio(类似 nodejs 中的 libuv,提供 event loop 等实现) chrome v8、ruby 和 libuv
typescript 集成 typescript compiler 需要额外安装 tsc 或者 ts-node
安全性 类似 browser 沙盒,限制使用 file 、network 等
语法 支持许多新的 TC39 语法,比如 top level await 使用新语法需要借助 babel
异步操作 所有的异步操作都返回 Promise 大多以 callback 的形式
包管理 不需要包管理工具(npm/package.json),引入依赖包直接 import network url path ,通过网址或者文件路径引用模块 使用 npm 等包管理工具、node_modules 问题
模块 支持 ES Modules ,而不是用 require 需要 .mjs 或者 babel 使用 ES Modules
浏览器兼容 可以在浏览器环境使用(没有 nodejs api 依赖的包)
Node.js API 兼容 支持部分 Node.js core API
预置功能 自带打包、格式化、测试等功能

Google Sign-In

谷歌账号登录

接入

  1. 在代码中引入google platform 库:

<script src="https://apis.google.com/js/platform.js" async defer></script>

  1. 指定应用的 client ID:
    Specify the client ID you created for your app in the Google Developers Console with the google-signin-client_id meta element.

<meta name="google-signin-client_id" content="YOUR_CLIENT_ID.apps.googleusercontent.com">

或者是:specify your app's client ID with the client_id parameter of the gapi.auth2.init() method.

  1. 添加谷歌登录按钮:

在登陆页添加一个 class 是 g-signin2 的div 标签

<div class="g-signin2" data-onsuccess="onSignIn"></div>

获取用户登录信息

使用谷歌账号登录后,可以通过 getBasicProfile() 方法 获取用户的Google ID, name, profile URL, 和电子邮件信息。

// auth2 is initialized with gapi.auth2.init() and a user is signed in.

if (auth2.isSignedIn.get()) {
  var profile = auth2.currentUser.get().getBasicProfile();
  console.log('ID: ' + profile.getId());
  console.log('Full Name: ' + profile.getName());
  console.log('Given Name: ' + profile.getGivenName());
  console.log('Family Name: ' + profile.getFamilyName());
  console.log('Image URL: ' + profile.getImageUrl());
  console.log('Email: ' + profile.getEmail());
}

需要注意的两点

  1. 在和服务端通信的时候,不应该使用 getBasicProfile() 方法返回的 getId() 信息, 正确的做法是受用 ID tokens, ID tokens 可以安全地在服务端验证。

  2. 由于谷歌账号的电子邮箱地址是可以更改的,因此不应该用它作为标示唯一用户的 Identifier 。正确的做法是使用账号的ID,客户端通过 getBasicProfile().getId() 获取,在服务端通 ID token 获取。

标示用户身份,和服务端通信

用户登录成功,可以获取到 ID token:

function onSignIn(googleUser) {
  var id_token = googleUser.getAuthResponse().id_token;
  ...
}

将 ID token 通过 https post 请求发送给服务端:

var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://yourbackend.example.com/tokensignin');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
  console.log('Signed in as: ' + xhr.responseText);
};
xhr.send('idtoken=' + id_token);

验证 ID token 的完整性

验证 ID token 的完整性, ID token 是否满足以下条件:

  1. ID token 是通过谷歌签名的,用Google's public keys (available in JWK or PEM format)检验 token 的签名

  2. ID token 的 aud 值需要和应用的 client IDs 中的一个相同

  3. ID token 的 iss 值等于 accounts.google.com 或者 https://accounts.google.com

  4. ID token 的过期时间 exp 没有过期

  5. If you want to restrict access to only members of your G Suite domain, verify that the ID token has an hd claim that matches your G Suite domain name.

有现成的官方库 Google API Client Libraries 可用。

npm install google-auth-library --save

var GoogleAuth = require('google-auth-library');
var auth = new GoogleAuth;
var client = new auth.OAuth2(CLIENT_ID, '', '');
client.verifyIdToken(
    token,
    CLIENT_ID,
    // Or, if multiple clients access the backend:
    //[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3],
    function(e, login) {
      var payload = login.getPayload();
      var userid = payload['sub'];
      // If request specified a G Suite domain:
      //var domain = payload['hd'];
    });

或者是通过 tokeninfo endpoint

To validate an ID token using the tokeninfo endpoint, make an HTTPS POST or GET request to the endpoint, and pass your ID token in the id_token parameter. For example, to validate the token "XYZ123", make the following GET request:

If the token is properly signed and the iss and exp claims have the expected values, you will get a HTTP 200 response, where the body contains the JSON-formatted ID token claims. Here's an example response:

https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=XYZ123

{
 // These six fields are included in all Google ID Tokens.
 "iss": "https://accounts.google.com",
 "sub": "110169484474386276334",
 "azp": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
 "aud": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
 "iat": "1433978353",
 "exp": "1433981953",

 // These seven fields are only included when the user has granted the "profile" and
 // "email" OAuth scopes to the application.
 "email": "[email protected]",
 "email_verified": "true",
 "name" : "Test User",
 "picture": "https://lh4.googleusercontent.com/-kYgzyAWpZzJ/ABCDEFGHI/AAAJKLMNOP/tIXL9Ir44LE/s99-c/photo.jpg",
 "given_name": "Test",
 "family_name": "User",
 "locale": "en"
}

参考

Integrating Google Sign-In into your web app

Google Sign-In JavaScript client reference

Webpack in Action (2)

Code Split

通过配置 webapck, 使用 Code Split 能够把代码分离到不同的 bundle 中,从而按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级。

三种常用的 Code Split 方法:

  1. 入口起点:使用 entry 配置手动地分离代码。
  2. 防止重复:使用 CommonsChunkPlugin 去重和分离 chunk。
  3. 动态导入:通过模块的内联函数调用来分离代码。

1. 入口起点(entry points)

这种方式是最简单、直观的分离代码方法。但也存在一些问题

示例, 从 main bundle 中分离另一个模块:

项目目录:

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
  |- another-module.js
|- /node_modules

another-module.js

import _ from 'lodash';

console.log(
  _.join(['Another', 'module', 'loaded!'], ' ')
);

webpack.config.js

const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another-module.js'
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'Code Splitting'
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

webpack bundle 结果

Hash: 309402710a14167f42a8
Version: webpack 2.6.1
Time: 570ms
            Asset    Size  Chunks                    Chunk Names
  index.bundle.js  544 kB       0  [emitted]  [big]  index
another.bundle.js  544 kB       1  [emitted]  [big]  another
   [0] ./~/lodash/lodash.js 540 kB {0} {1} [built]
   [1] (webpack)/buildin/global.js 509 bytes {0} {1} [built]
   [2] (webpack)/buildin/module.js 517 bytes {0} {1} [built]
   [3] ./src/another-module.js 87 bytes {1} [built]
   [4] ./src/index.js 216 bytes {0} [built]

这种方式存在的问题:

  1. 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。 (如果在 ./src/index.js 中也引入过 lodash,这样就在两个 bundle 中造成重复引用)

  2. 不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。

2. CommonsChunkPlugin 防止重复

CommonsChunkPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。

用这个插件,可以解决上一个示例中重复引入的 lodash 模块的问题:

  const path = require('path');
  const webpack = require('webpack');
  const HTMLWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    plugins: [
      new HTMLWebpackPlugin({
        title: 'Code Splitting'
     }),
+     new webpack.optimize.CommonsChunkPlugin({
+       name: 'common' // 指定公共 bundle 的名称。
+     })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

webpack bundle 结果:

Hash: 70a59f8d46ff12575481
Version: webpack 2.6.1
Time: 510ms
            Asset       Size  Chunks                    Chunk Names
  index.bundle.js  665 bytes       0  [emitted]         index
another.bundle.js  537 bytes       1  [emitted]         another
 common.bundle.js     547 kB       2  [emitted]  [big]  common
   [0] ./~/lodash/lodash.js 540 kB {2} [built]
   [1] (webpack)/buildin/global.js 509 bytes {2} [built]
   [2] (webpack)/buildin/module.js 517 bytes {2} [built]
   [3] ./src/another-module.js 87 bytes {1} [built]
   [4] ./src/index.js 216 bytes {0} [built]

index.bundle.js 中已经移除了重复的依赖模块。需要注意的是,CommonsChunkPlugin 插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。

3. 动态导入(dynamic imports)

webpack 提供了两个类似的技术。第一种,也是优先选择的方式是,使用符合 ECMAScript 提案 的 import() 语法。第二种,则是使用 webpack 特定的 require.ensure。

示例:

不使用 dynamic imports 的方式:

index.js

import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";   // react router v4
import { matchRoutes, renderRoutes } from 'react-router-config';
import configureStore from './store/configureStore';

import App from './containers/App';   // 引入组件
import Home from './containers/Home';   // 引入组件

const store = configureStore();

render(
  <div className="app">
   	<Provider store={store}>
	   <Router>
	   	<div>
	   		<Route exact path="/" component={App}/>
	  		<Route path="/home" component={Home}/>
		  </div>
	  </Router>
    </Provider>
  </div>
  , document.getElementById('root')
);

访问的时候,在控制台可以看到网站打开时,会加载所有页面(路由)的 bundle 文件。

我们期望的是访问不同的页面(路由),依次(懒)加载相关的 bundle 文件。

实现方式如下:

import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import { matchRoutes, renderRoutes } from 'react-router-config';
import configureStore from './store/configureStore';

function asyncComponent(getComponent) {
  return class AsyncComponent extends React.Component {
 static Component = null;
 state = { Component: AsyncComponent.Component };
  
	  componentWillMount() {
		if (!this.state.Component) {
		  getComponent().then(Component => {
			AsyncComponent.Component = Component
			this.setState({ Component })
		  })
		}
	  }

	  render() {
		const { Component } = this.state
		if (Component) {
		  return <Component {...this.props} />
		}
		return null
	  }
	}
}

const RealTime = asyncComponent(() =>
  import('./containers/RealTime')   // 动态加载
)
const Retention = asyncComponent(() =>
  import('./containers/Retention')   // 动态加载
)

const store = configureStore();
render(
<div className="app">
   	<Provider store={store}>
	   <Router>
	   	<div>
	   		**<Route exact path="/" component={App}/>**
	  		**<Route path="/home" component={Home}/>**
		  </div>
	  </Router>
    </Provider>
  </div>
  , document.getElementById('root')
);

效果对比:

不采用动态加载,访问网站时,所有的 bundle 文件一起加载。

2017-09-18 5 20 59

采用动态加载,第一次访问网站时,只加载公共的 bundle 文件 和当前页面的 bundle文件(0.bundle.js), 访问不同的路由(页面),继续加载其他的 bundle (1.bundle.js)

2017-09-18 5 23 08

参考

代码分离

Quick and dirty code splitting with React Router v4

Code splitting and v4

[WebGL] (1)

大部分 WebGL 程序都遵循这样的流程:

获取 元素 --> 获取WebGL 绘图上下文 --> 初始化着色器 --> 设置 背景色 --> 清除 --> 绘图

着色器

WebGL 程序包括运行在浏览器中的 js 和运行在WebGL系统的着色器程序两部分。
着色器分为定点着色器和片元着色器。

着色器程序使用 GLSL ES 强类型编程语言。 GLSL ES 中的数据类型有:浮点数float 和 四个浮点数组成的矢量vec4。

着色器程序和 C 语言程序一样,必须包含一个main 函数。main() 前面的关键字 void 表示这个函数没有返回值。不能为main()制定参数。

定点着色器控制位置和大小,片元着色器控制颜色。定点着色器的结果传给片元着色器。

callback && promise && async/await

callback

 export  default safeRequest(safeRequestContent, res => {
            if (res.err_no === 1) {
                fallback(res.err_tip);
            } else {
                callback(res);  // 服务端返回结果,包含 err_no 不等于 0 
            }
        });

safeRequest({
            url: api,
            method: 'get',
            params: {
                offset: this.state.offset,
                count: CHUNK_SIZE
            },
            body_content_type: 'json',
            callback: res => {
            },
            fallback: error => {

            }
        });

promise

safeRequest({
            url:  api,
            method: 'get',
            params: {
                offset: this.state.offset,
                count: CHUNK_SIZE
            },
            body_content_type: 'json',
        }).then(res=>{
            console.log(res);
        }).catch(e=>{
            console.log('999', e);
        });

async/await


let onlineAjaxPromise = new Promise(function (reslove, reject) {
            bridgeController.safeRequest(safeRequestContent, res => {
                if (res.err_no === 1) {
                    reject(res.err_tip);
                } else {
                    reslove(res);  // 服务端返回结果,包含 err_no 不等于 0 
                }
            });
        });

return onlineAjaxPromise;

async fetchData() {
        try {
            const res = await safeRequest({
                url: '/score_task/v1/apprentice/page_data/',
                method: 'get',
                params: {
                    offset: this.state.offset,
                    count: CHUNK_SIZE
                },
                body_content_type: 'json',
            });
            console.log(6666, res);
        } catch (err) {
            console.log(err);
            alert(JSON.stringify(err));
            this.setState({ fetchStatus: 'error' });
            ToutiaoJSBridge && jsBridge.toast('网络错误');
        }
    }

this.fetchData();

Protobuf.js 解析 int64

生产环境和开发环境解析PB long64返回格式不一致的问题:生产环境换回了number 类型,开发环境返回 long 类型

背景

protobuf.js 是支持浏览器环境的,但是如果想要支持int64的话,需要额外引入long.js 模块。并且需要项目里提供 global全局的require方法引入 long.js 模块,这样 protobuf.js 才能调用到long.js 模块。

为什么开发环境ok,生产环境不行?

应该是 webpack 打包出来的东西, require 被干掉了,building 之后 没有 global 全局的 require 函数

参考
过时了,已经没有 bytebuffer模块了

解决方案

既然全局的 require 找不到long.js模块,那么就手动在 protobuf.js 中引入 long.js 模块

var Long = ...;

protobuf.util.Long = Long;
protobuf.configure();

为什么加上这么两行代码就可以了?

protobuf.js 是如何解析消息中的 int64 数字的?

以 pb-read.png 为例,当处理到 int64 数字时,

- 如果存在 util.Long,就用 util.Long 的formValue 处理,将int64 转换为 long 型
- 当要处理的目标数字是 string 或者 number 类型时,分别转换成对应类型
- 当要处理的目标数字是 object 类型时,使用 util.LongBits 方法把目标转换为 number 类型。 util.LongBits 是 protobuf.js 自己实现的方法

所以 protobuf.js 在处理int64是需要用到 util.Long 的,那么 util.Long 从何而来?其实也就是 protobuf.js 是怎么引入项目中的 long.js 模块的

protobuf.js 是如何在项目中寻找 long.js 包的?

参考 pb-inqure.png
protobuf.js 会首先找全局的 Long 是否窜在,不存在的话 就 inqure 项目中可能存在的 long 模块。而上面的解决方案中是 手动给 util.Long 赋值,传入了 long 模块,同样满足需求

protobuf.js 引入了 long 模块之后,又是怎么使用的?

参考 pb-configure.png
当 util.Long 引入了 long 模块之后,readLongVarint 方法就会使用 toLong 的方式把 int64 转换为 long 型,如果没有引入了 long 模块,那么就使用 toNumber 把 int64 转换为 number 类型

这里的 toLong 和 toNumber 是 protobuf.js 自己实现的 LongBits 类型的方法

参考

webpack 升级

webpack 版本由 1.x 升级至 3.5.6 (官方称2、3版本之间差异不大,所以一步到位)

loaders

--save-dev

extract-text-webpack-plugin 从1.0.1升级到2.1.2

webpack-hot-middleware 到 ^2.19.1

css-loader ^0.23.1 到 0.28.7

less-loader ^2.2.2 4.0.5

sass-loader ^4.0.0 6.0.6

node-sass : "^3.8.0", 4.5.3

babel-loader ^6.2.0 7.1.2

file-loader ^0.9.0 0.11.2

happypack ^3.0.2 4.0.0

lodash ^3.10.1 4.17.4

style-loader ^0.13.0 0.18.2

react-bootstrap 0.31.3

react-bootstrap-typeahead 2.0.0-alpha.3

参考

迁移到新版本

Plugins

Webpack in Action (4)

webpack 配置中的外部扩展 Externals

通过配置 externals, 可以防止把某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。

就是说 webpack 可以不处理某些依赖库,使用externals配置后,仍然可以在代码中通过CMD、AMD或者window/global全局的方式访问依赖。

比如在项目中是通过 script 引入 jquery

index.html

<script src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"></script>

此时,就是运行时加载 jquery ,可以配置 webpack 不把 jquery 打包。

webpack.config.js

output: {
    ...
    libraryTarget: "umd"
}

externals: {
   jquery: 'jQuery'
}

既剥离了不需要改动的依赖模块,同时也不影响下面的书写方式:
使用方式 1 :

import $ from 'jquery';
$('.my-element').animate(...);

使用方式 2 :
const $ = window.jQuery

使用方式 3:

const $ = require("jquery")
$("#content").html("<h1>hello world</h1>")

参考

外部扩展(Externals)

zhengweikeng/blog#10

创建 Library

AbortController 使用总结

AbortController 是一个DOM API,提供了终止 Promise 网络请求继续执行的能力

业务中可能遇到的场景

  1. 用户连续的输入,频繁的调用请求接口😫(传统的解决方案有很多:Rxjs Observable 、节流等等)
  2. 查看地图时需要加载比较多资源和文件,当用户频繁的调整地图的缩放比例时,需要展示最新尺寸的图形,但加载历史尺寸的网络请求还在缓慢地执行和响应中,占用网络带宽,是我们不期望看到的结果🤦🏼‍♀️🤦🏼‍♂️🤦🏻‍♀️🤦🏻‍♂️🤦🏽‍♀️🤦🏽‍♂️🤦‍♀️🤦‍♂️🤦🏿‍♀️ 🤦🏿‍♂️

Demo

演示一个根据用户输入内容调用接口请求数据的🌰例子

  • fetch 请求不做任何处理
    每输入一个字符,就触发一次请求(是魔鬼吗... 🧟‍♀️🧟‍♂️)

image

  • 使用 AbortController
    实际发出的只有最后一次请求 🆒,携带过时参数的请求状态都是 canceld ,并且被终止的网络请求的传输字节数也是 0

image

使用方式

调用 AbortController 构造函数,返回 controller 实例对象 和 signal 对象,将 signal 作为参数传给 fetch ,执行 controller.abort() 方法终止 fetch 请求

// 调用 AbortController 构造函数,新建一个实例对象
let abortController = new AbortController();
let { signal } = abortController;
// 将实例对象返回的 signal 作为参数传给 fetch 请求
async function getGists(){
    const response = await fetch('https://api.github.com/gists', { signal });
    const gists = await response.json();
    console.info(gists);
};
// 可以通过绑定 EventListener 监听 signal 变化
signal.addEventListener('abort', () => {
    console.error('[SIGNAL ABORT EVENT TRIGGERED]')
})
signal.onabort = () => {
    console.error('[SIGNAL ONABORT EVENT TRIGGERED]')
}
getGists()
// 触发实例对象的 abort 方法,signal 触发一个 AbortDomError 事件
abortController.abort()
// signal 的状态变为 aborted,未执行完的 fetch 被 abort 终止,同时触发 EventListener 队列中的回调

请求超时

  • 常见的请求超时通常用 Promise.race 实现:
const racefetch = async function(timeout = 3000){
    let fetchPromise = fetch('https://api.github.com/gists')
        .then(r=> r.json())
        .then(p => {
            console.log('[RESP]', p)
            return p
        })
    let timeoutPromise = new Promise(function(resolve, reject){
        setTimeout(()=>{
             reject(new Error("fetch timeout"))
        }, timeout)
    });
    return Promise.race([fetchPromise, timeoutPromise])
}

可以看到👇下面的测试结果,虽然能实现超时 throw error 的效果,但并没有真正阻止 fetch 请求的传输 🤷‍♂️

image

  • 使用 AbortController 的实现:
const controllerFetch = async function (timeout = 3000) {
    async function  doRequest() {
        const controller = new AbortController();
        const signal = controller.signal;
        const timer = setTimeout(() => controller.abort(), timeout);
        try {
            const resp = await fetch('https://api.github.com/gists', {signal}) 
            const respJson = await resp.json()
            return respJson
        } catch (e) {
            return Promise.reject(e);
        } finally {
            clearTimeout(timer);
        } 
    } 
    return doRequest();
}

下图是使用使用 AbortController 的方式,可以在设定的 timeout 时真正意义上的关闭网络连接 🌟,0 字节传输

image

  • 更多方案: axios timeout 、 Rxjs Observable ... (不是本文重点,不扩展讨论)不去比较哪种方案是最好的,这更多取决于各自的业务场景,选择指定场景中更适合的方案就好 🎉

使用中需要注意的地方

  • 已经 aborted 的 fetch 请求是不能重复调用的 ❌

看源码的实现就可以知道为什么,AbortSignal 在 abort 之后,状态就变成了 aborted,那么下次调用 fetch 请求的时候,当读到 AbortSignal 的状态是 aborted 的时候,就直接 reject 返回了。

// Return early if already aborted, thus avoiding making an HTTP request
if (signal.aborted) {
    return Promise.reject(abortError);
}

所以如果有请求重试的需求的话,需要每次调用 fetch 前都重新 new 一个 AbortController,返回一个新的 signal

// 第一个 AbortController
let firstController = new AbortController();
let { signal: firstSignal } = firstController;
// 第二个 AbortController
let anotherController = new AbortController();
let { signal: anotherSignal } = anotherController;

async function getGists(signal){
    const response = await fetch('https://api.github.com/gists', { signal });
    const gists = await response.json();
    console.log(gists);
};
// 终止第一个 AbortSignal
firstController.abort()
// 被 firstSignal 标记的请求已经终止
getGists(firstSignal)
// anotherSignal 标记的 fetch 请求不受影响仍然可以执行
getGists(anotherSignal)

  • 一个 AbortSignal 可以用于同时控制多个 fetch 请求
const controller = new AbortController()
const { signal } = controller
async function fetchAll({ signal }={}) {  
  const fetchSingle = [1,2,3].map(async () => {
    console.info("[SIGANL STATUS]", signal)
    const response = await fetch('https://api.github.com/gists', { signal });
    return response.json();
  });
  return Promise.all(fetchSingle);
}
// 调用
const controller = new AbortController();
const signal = controller.signal;

fetchAll({ signal }) 

👇 下图可以看到,当 signal 被 abort 后, 所有被 signal 标记的 fetch 请求都终止了:

image

兼容性 🌚

🥇Edge 浏览器率先在 2017 年支持 AbortController API ,FireFox 紧随其后 🏃‍♂️,我们常用的 Chrome 是在 2018 年的 66 版本开始支持的 😅

image

AbortController 的实现

需要浏览器的支持去关闭 TCP socket 连接才能真正意义上结束一次网络请求。这里介绍 fetch polyfill 对 AbortController 的实现。

fetch polyfill 对 AbortController 的实现

虽然是一个 DOM API,但是借助 polyfill 可以实现在 service-worker、nodejs 等环境中使用AbortController(相关 MR

abortcontroller-polyfill 模块

fetch polyfill 在 service-worker 中的实现其实是引用了abortcontroller-polyfill 模块,模块主要完成两件事情:

  1. 对 AbortController 和 AbortSignal 的 polyfill
  2. 修改全局的 fetch 函数,使 fetch 具备接收和响应 AbortSignal 事件的能力

通过源码分析,很容易发现其实这个 polyfill 的目的是让用户可以在不支持 AbortController 的 js 执行环境中仍然能够使用这个属性,并且得到相似的运行结果。虽然结果看上去相似,但效果并不相同 🙅‍♂️(它并无法真正的去实现关闭 TCP 连接,只是在 abort 事件发生时不再对 fetch请求结果作处理)

参考

《代码精进之路 从码农到工匠》

豆瓣链接

1.4 保持一致性

1.4.1 每个概念一个词

保持命名的一致性:

CRUD 操作 方法名预定
新增 create
添加 add
删除 remove
修改 update
查询(单个结果) get
查询(多个结果) list
分页查询 page
统计 count

1.4.2 使用对仗词

遵守对仗词的命名规则有助于保持一致性,从而提高代码的可读性。常见的对仗词组:

  • add/remvoe
  • increment/decrement
  • open/close
  • begin/end
  • insert/delete
  • show/hide
  • create/destroy
  • lock/unlock
  • source/target
  • first/last
  • min/max
  • start/stop
  • get/set
  • next/previous
  • up/down
  • old/new

1.4.3 后置限定词

很多程序中有表示计算结果变量,例如总额、平均值、最大值等。如果使用类似 TotalSumAverageMaxMin 这样的限定词来修改某个命名,那么记住把限定词加到名字的最后,并在项目中保持命名风格的一致性。

2 章序言

复杂系统的前言科学家在 《复杂》一书中,提出一种用信息熵来进行复杂性度量的方法。

所谓信息熵,就是一条信息的信息量大小和它的不确定性之间的关系。

举个例子,消息由符号 A、C、G 和 T组成,如果序列高度有序,例如“A A A A A ... A ”,则熵为零。而完全随机序列,例如 “G A T A C G A ... A” ,熵值达到最大。

事物的复杂程度在很大程度熵取决于起有序成都,减少无序能在一定程度熵降低复杂度,这正是规范的价值所在。通过规范,把无序的混沌控制在一个能够理解的范围内,从而帮助我们减少认知成本,降低对事物认知的复杂度。

2.1 认知成本

什么是知识呢?知识是人类对经验范围内的感觉进行总结归纳之后发现的规律。混乱无序的东西没有规律,不能形成知识,也就不能被认知到,这就是有组织和无组织的复杂性的区别。

对于一名有经验的飞行员,已经掌握了所哟欧飞机的共同属性,那么只要通过短时间的指导,使其了解哪些特性是新飞机所特有的,他就能驾驶这架新飞机。

因此,发现共同抽象和机制可以在很大程度熵帮助我们理解复杂系统。

2.2

认知是有成本的,而混乱的代价在于让我们对事物无法形成有效的记忆和认知,导致我们每次面对的问题都是新问题,每次面临的场景都是新场景,又要重新理解一遍。

在工作中,很多工程师抱怨他们的系统很乱,毫无章法可言,即使花费很长事件也很难理清系统的脉络。在评估一个需求时,要在杂乱无章的代码中找好久才能找到相关的需求改动点,然而真正需要改动的代码可能只有一行而已。这样的无序在很大程度上是系统缺少代码组织结构规范造成的。

规范的确实会导致工程师不知道应用中有哪些制品(Artifact)、如何给类命名、一个类应该放在哪个包(Package)或哪个模块(Module)里比较合适、错误码应该怎么写、什么时候该打印日志、选用哪个日志级别。

因为上天不是反复无常或随心所欲的。软件工程师没有这样的信仰来安慰自己。许多必须控制的复杂性是随心所欲的复杂性。

混乱是有代价的,我们哟欧必要使用规范和约定来使大脑从记忆不同的代码段的随意性、偶然性差异中解脱出来。将我们有限的精力用在刀刃上,而不是用来疲于应对各种不一致和随心所欲的混乱。

[React] React Hooks 中使用 setTimeout

问题

在hooks 中使用 setTimeout 方法,方法中访问到的函数 state 始终是初始值,而不是更新后的最新 state

demo

在这个例子中,首先执行setCount 将 count 设为 5, 然后经过 3 秒后执行 setCountInTimeout, 将 countInTimeout 的值设置为count 的值

我们最初期望的是 这时候 countInTimeout 就等于 此刻 count 最新的值 5, 然而 countInTimeout 却保持了最开始的 count 值 0

import React, { useEffect, useState } from 'react';

const TimeoutExample = () => {
  const [count, setCount] = useState(0);
  const [countInTimeout, setCountInTimeout] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      setCountInTimeout(count); // count is 0 here
    }, 3000);
    setCount(5); // Update count to be 5 after timeout is scheduled
  }, []);

  return (
    <div>
      Count: {count}
      <br />
      setTimeout Count: {countInTimeout}
    </div>
  );
};

export default TimeoutExample;

原因

setTimeout 是一个闭包,setTimeout 函数执行的时候使用的参数 count 读取自setTimeout 函数创建的时候,即 0。 setTimeout 使用闭包的方式异步访问 count 的值。当整个函数组件re-render的时候,会创建出一个新的 setTimeout 函数一个新的闭包,但并没有改变最初封装它的那个闭包的值

作者也提到这么设计的初衷是满足这样的场景:比如订阅了一个ID,当随后需要取消订阅的时候,避免ID发生变化而造成不能取消订阅的问题

解决方法

使用一个 container 来把最新的 state 也就是 count 的值穿进去,并在随后的 timeout 函数中读取最新的 state 。

可以使用 useRef。 通过 ref's current 来同步最新的 state, 然后在 timeout 函数中读取 current 的值。使用 ref 在异步callback函数中访问最新的 当前的 state

const countRef = useRef(count);
countRef.current = count;

const getCountTimeout = () => {
  setTimeout(() => {
    setTimeoutCount(countRef.current);
  }, 2000);
};

参考
State from useState hook inside a setTimeout is not updated

CommonJS、CMD、AMD 模块规范

模块标准诞生的背景

对象封装的形式

缺点:内部的属性可以被外部修改

IIFE 立即执行函数

优点:避免内部变量收到外部修改

缺点:无法实现模块之间的依赖,同时代码分配到主流成中,不利于维护

CommonJS

nodejs 服务端使用的模块规范,以 require 的方式同步读取文件内容,编译执行。

第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。

如果想要多次执行某个模块,可以让该模块输出一个函数,然后每次require这个模块的时候,重新执行一下输出的函数。

所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写:

// 删除指定模块的缓存
delete require.cache[moduleName];

// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})

缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require 命令还是会重新加载该模块。

循环加载

CommonJS 中的循环加载,模块 a 加载模块 b, 模块 b 加载模块 a, 模块 b 将加载模块 a 的不完整版本

模块加载机制是:输入的是被输出的值的拷贝

一旦输出一个值,模块内部的变化就影响不到这个值

代码输出内部变量counter和改写这个变量的内部方法incCounter:

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

counter输出以后,lib.js 模块内部的变化就影响不到counter:

// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

服务端 require 一个模块,直接就从硬盘或者内存中读取了,消耗的时间可以忽略,就没有必要采用异步方式的来加载。

由于浏览器端常见的是以 script 标签的方式异步加载 js, CommonJS 模块在浏览器环境中无法正常加载。同步加载会阻塞页面的渲染,造成页面白屏,或者卡死等现象,

浏览器端资源的加载方式与服务端完全不同,在浏览器端,需要从服务端来下载这个文件,然后运行里面的代码才能得到API,需要花费一个http请求,也就是说,require后面的一行代码,需要资源请求完成才能执行。由于浏览器端是以插入 <script> 标签的形式来加载资源的(ajax方式不行,有跨域问题),没办法让代码同步执行,所以像 commonjs 那样的写法会直接报错。

AMD

浏览器端使用的异步模块规范。 对应的实现有 RequireJS

RequireJS 的使用方式:

require([dependencies], function(){});

模块文件:

// 定义模块 myModule.js
define(['dependency'], function(){
    var name = 'Byron';
    function printName(){
        console.log(name);
    }

    return {
        printName: printName
    };
});

引用模块:

require(['myModule'], function (my){
  my.printName();
});12345678910111213141516

define 函数 define(id?, dependencies?, factory)

第一个参数,是模块的标识,如果没有传入,使用脚本文件名作为表识

第二个参数是依赖的模块数组

第三个参数,模块初始化要执行的函数或对象。如果是函数,只执行一次。如果是对象,那么这个对象就是模块的输出值。

require 函数

第一个参数,代表所依赖的模块

第二个参数是回调函数,当第一个参数重指定的模块都加载成功后,执行这个回调函数。加载好的模块以参数的形式传入回调函数,这样回调函数中就可以使用这些模块。

require([dependencies], function(){}) 函数在加载依赖的模块时时异步加载的,不会使浏览器失去响应,回调函数只在依赖的模块都加载成功后才运行。

解决的主要问题:

  1. 加载 js 的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长。

  2. 多个 js 文件之间可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器

CMD

与 AMD 一样同为浏览器端的规范。 SeaJS(诞生于国内) 是对应的实现,解决的问题和 AMD 的 RequireJS 相同

Sea.js 使用方式:

// 定义模块  myModule.js
define(function(require, exports, module) {
  var $ = require('jquery.js')
  $('div').addClass('active');
});
// 加载模块
seajs.use(['myModule.js'], function(my){

})

define 函数define(id?, deps?, function(require, exports, module){})

factory 是一个函数,

第一个参数 require , 参数是模块标识,用来获取其他模块提供的接口

第二个参数 exports 是一个对象,用来向外提供模块接口

第三个参数是一个对象,存储了与当前模块相关联的属性和方法

AMD 和 CMD 区别

同为浏览器端的模块规范,区别是

在模块定义时对依赖的处理不同:

  • AMD 依赖前置,在定义模块时要声明依赖的模块。js 方便知道依赖模块有哪些,立即加载。

  • CDM 就近依赖,只在用到某个模块时再去 require。需要使用把模块变为字符串解析一遍,才知道依赖哪些模块。实际上解析模块所用时间很短

对依赖模块的执行时机不同 (define 函数的区别)

  • AMD 加载完模块之后立即执行模块,所有模块加载完后进入 require 的回调函数,执行主逻辑。依赖模块的执行顺序和书写顺序不一定一致,先下载完的模块先执行,但是主逻辑是在所有依赖模块加载完之后才执行

  • CMD 加载完某个依赖模块后并不执行,在所有依赖模块加载完后进入主逻辑,遇到 require 语句时才执行对应的模块,模块的执行顺序和书写顺序一致;在提前加载模块过程中,会把加载下来的模块保存在内存中,以至于客户端执行主流程按需引入模块时是同步执行内存中保存的模块

ES6 Module

客户端使用的模块加载规范。ES5 中没有模块化加载的概念, ES6 中引入了模块化加载标准。上面的各种模块加载规范都是运行时输出接口,ES6 Module 时编译时输出接口。

// person.js
let name = 'Bar'
export function setName(newName) {
	name = newName
}
export funciton getName() {
	return name
}

import { setName, getName } from './person'
setName('Foo')
console.log(getName())

Node.js 也逐渐向这种方式靠拢(使用 babel ),大部分使用 CommonJS 规范

[Node]-util.promisify

util.promisify 是 NodeJS version8+版本的新功能,它接受一个函数作为参数,并将函数转换为promise函数。最为结果返回的promise函数自持promise语法(链式)和 async/await 语法。

注意

作为util.promisify参数的函数,需要满足 NodeJS callback 规范:

  1. 函数必需将 callback 作为最后一个参数,
  2. 并且 callback 函数的参数顺序必需满足这样的形式: (err, value) => { /* … */ }

###示例:

promise

const {promisify} = require('util');

const fs = require('fs');
const readFileAsync = promisify(fs.readFile); // (A)

const filePath = process.argv[2];

readFileAsync(filePath, {encoding: 'utf8'})
  .then((text) => {
      console.log('CONTENT:', text);
  })
  .catch((err) => {
      console.log('ERROR:', err);
  });

async/await

const {promisify} = require('util');

const fs = require('fs');
const readFileAsync = promisify(fs.readFile);

const filePath = process.argv[2];

async function main() {
    try {
        const text = await readFileAsync(filePath, {encoding: 'utf8'});
        console.log('CONTENT:', text);
    }
    catch (err) {
        console.log('ERROR:', err);
    }
}
main();

async自执行函数

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);

(async () => {
    let stats;
    try {
      stats = await stat('.');
    } catch (err) {
      return console.error(err);
    }
    return console.log(stats);
})();

参考

Node8’s util.promisify is so freakin’ awesome!

Node.js 8: util.promisify()

[React] version 16 Error Handling

昨天React 16正式版本发布。

为了使得整个应用不至于因为 UI 的原因崩溃,React 16 引入了**error boundary** 的新概念。

错误边界 Error boundaries 首先是一个普通的 React 组件,只不过它可以用来捕获发生在子组件树中的错误,能够记录错误,也能让方便地让开发者设计当错误发生时要展示给用户的界面。

componentDidCatch

React v16组件生命周期中提供了一个新的钩子 componentDidCatch,这个方法能够处理 Error boundaries 父组件 wrap 的所有子组件抛出的错误,并且不需要 unmounting 整个app。

注意

Error boundaries 父组件只能捕获自身子组件(存在于自己的子组件树中的组件)抛出的错误。它无法捕获自己抛出的异常。 render 方法中的错误也没法catch。

demo

ErrorBoundary 文件

export default class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            hasError: false,
        };
    }

    componentDidCatch(error, info) {
        this.setState({
            hasError: true,
        });
    }

    render() {
        if (this.state.hasError) {
            return (
                <div>
                    {'something bad happened'}
                </div>
            );
        }
        return this.props.children;
    }
}

引入 ErrorBoundary

import {render} from 'react-dom';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {Provider} from 'react-redux';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import ErrorBoundary from '../components/ErrorBoundary';
import configureStore from '../store/configureStore';
import * as actions from '../actions';
....

class AppRouter extends Component {
    render() {
        return (
            <Router>
                    <ErrorBoundary>
                        {
                                <div className="app-right">
                                    <Route exact path="/" component={Test}/>
                                </div>
                        }
                    </ErrorBoundary>
            </Router>
        );
    }
}

export default connect(state=>({
   ...
}), actions)(AppRouter);

当 ErrorBoundary 的 Test 子组件中发生错误时, 会被 ErrorBoundary 的 componentDidCatch 方法catch 住

import React, {Component} from 'react';
import ReactDom from 'react-dom';
import {connect} from 'react-redux';
import * as actions from '.actions';

class Test extends Component {
constructor() {
super();
}

componentDidMount() {
    cosnole.log(a.b)    //  **_运行到这里将抛出一个错误, a is not defined_**
}

render() {
    return (
        <div>
            .....
        </div>
    );
}

}

export default connect(state=>({
...
}), actions)(Test);

参考

Error Handling in React 16

Error Handling using Error Boundaries in React 16

Webpack in Action (3)

hot-module-replacement 热模块替换

webpack 通过配置支持在运行时更新各种模块,而无需进行完全刷新。HMR 不适用于生产环境,应当只在开发环境使用。

有两种启用HMR的方式:

1. 配置 webpack-dev-server ,使用 webpack 内置的 HotModuleReplacementPlugin 插件

webpack.config.js:

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const webpack = require('webpack');

  module.exports = {
    entry: {
     app: './src/index.js'
    },
    devtool: 'inline-source-map',
    devServer: {
      contentBase: './dist',
+     hot: true
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement'
      }),
+     new webpack.HotModuleReplacementPlugin()
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

或者是命令行方式修改 webpack-dev-server 的配置:webpack-dev-server --hotOnly

index.js: 修改 index.js 文件,当 print.js 内部发生变更时通知 webpack 接受更新的模块

import _ from 'lodash';
  import printMe from './print.js';

  function component() {
    var element = document.createElement('div');
    var btn = document.createElement('button');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    btn.innerHTML = 'Click me and check the console!';
    btn.onclick = printMe;

    element.appendChild(btn);

    return element;
  }

  document.body.appendChild(component());
+
+ if (module.hot) {
+   module.hot.accept('./print.js', function() {
+     console.log('Accepting the updated printMe module!');
+     printMe();
+   })
+ }

print.js: 更改 print.js 中 console.log 的输出内容,你将会在浏览器中看到如下的输出。

  export default function printMe() {
-   console.log('I get called from print.js!');
+   console.log('Updating print.js...')
  }

问题: 当HMR后,DOM 元素的事件仍然绑定在HMR之前的节点上,需要在 module.hot.accept 方法的回调中更新DOM节点。

事实上,一些style-loader 等 loader 会在后台使用 module.hot.accept 来修补(patch) <style> 标签。

2. node server 使用 webpack-dev-middleware node 中间件,搭配使用 webpack-hot-middleware node 中间件

webpack-dev-middleware instead of webpack-dev-server, please use the webpack-hot-middleware package to enable HMR on your custom server or application.

(node) server.js:

var express = require('express');
var webpack = require('webpack');
// ...
var app = express();

if (isDeveloping) {   // 仅在开发环境使用,避免生产环境使用
  var config = require('./webpack.config');
  var compiler = webpack(config);
  var devMiddleware = require('webpack-dev-middleware')(compiler, {
    noInfo: false,
    hot: true,
    inline: true,
    publicPath: config.publicPath,
    stats: {
      colors: true,
      chunks: false
    }
  });

  var hotMiddleware = require('webpack-hot-middleware')(compiler);
  // force page reload when html-webpack-plugin template changes
  compiler.plugin('compilation', function (compilation) {  // webpack plugin 的生命周期 hook
    compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
      hotMiddleware.publish({action: 'reload'});  //  HMR node 中间件发送event action ,通知更新reload事件
      cb();     // webpack plugin 完成功能后需要调用的 callback
    });
  });

  app.use(devMiddleware);
  app.use(hotMiddleware);
} else {
   // ...
}

webpack.config.js:

//...
entry: {
    app: [
      './server/dev-client',     // 配置 HMR 
      './client/src/index.js'
    ]
  },
//...

(node server端) dev-client.js:

// 使用 node webpack-hot-middleware 中间件

var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true');

hotClient.subscribe(function (event) {   // 订阅 HMR 的事件, 事件源在 **_(node) server.js_**
  if (event.action === 'reload') {      //  收到 HMR 事件通知“reload”, 执行页面重新加载
    window.location.reload();
  }
});

参考

https://doc.webpack-china.org/guides/hot-module-replacement/

使用 dataloader 优化 GraphQL 性能

在 GraphQL 应用中 N+1 是经常遇到的一个问题场景。

N + 1 问题: What is the N+1 Query Problem?
This problem occurs when the code needs to load the children of a parent-child relationship (the “many” in the “one-to-many”). Most ORMs have lazy-loading enabled by default, so queries are issued for the parent record, and then one query for EACH child record

N+ 1 问题

以一条简单的 GraphQL 语句为例🌰描述什么是 N + 1 问题:

GraphQL Client

前端查询文章列表并获取每篇文章的标签信息

{            
  query {        
   articles {  // 文章列表             
     title            
     tags { // 文章标签信息                
       name             
     }        
   }     
  }
}

GraphQL Server

以下👇用  type-graphql 展示 GraphQL server 的实现:

@ObjectType()
class Article {  
 @Field(type => ID)  id: string;  
 @Field()  title: string;  
 @Field(type => [Tag])  tags: Tag[];
}

@ObjectType()
class Tag {  
 @Field(type => ID)  id: string;  
 @Field()  name: string;
}
@Resolver(Article)
class ArticleResolver {  
  constructor(private apiService: ApiService) {}  
  @Query(returns => [Article])  
  async articles(): Promise {    
    return this.apiService.getArticles();  
  }
}

@Resolver(Tag)
class TagResolver {  
  constructor(private apiService: ApiService) {}   
  @Query(returns => [Tag])  
  async tags(@Args() { id }: GetTagsArgs ): Promise {    
    return this.apiService.getTags(id);  
  }
}

按照上面的前端查询语句,GraphQL server 会先执行一次 apiService.getArticles 获取所有 articles,然后再执行 apiService.getTags 分别获取各篇文章的 tags 信息。即如果有 n 篇文章的话,GraphQL server 实际执行的请求次数是 1 + n(1次 apiService.getArticles 和 n 次 apiService.getTags )

问题原因

GraphQL server 中之所以存在 N + 1 问题,是因为在实现 resolver 时,每个 resolver 都只知道自己的请求信息,并不清楚当前存在其它 resolver 与自己请求相同的接口或数据(resolver 不具备 look after 或 behind 的能力

解决方案

Dataloader 是 facebook 提出的一种解决方案,同它时具备批量请求 batch 和数据缓存 cache 的能力。Dataloader 能够收集一段时间内来自后端的多个数据请求,然后打包成单个请求再发送到底层数据库或服务。

Dataloader 原理

简单来说 dataloader 在内部维护了一个队列,将 Event loop 中一个 tick 阶段的接口请求(可以是API服务调用、数据库访问等异步操作)都放入一个队列里,并在 process.nextTick 时批量执行。

可以理解为 dataloader 的实现是延迟了 resolver 的执行。每次请求初始化 new Dataloader() ,并传入 batchLoadFn,batchLoadFn 是一个 promise,当清空任务队列时会批量执行这个 promise。在实际应用中,batchLoadFn 通常是一个批量请求接口服务或者访问数据库的 promise。

每当调用 dataloader load 方法时,会将相应的 key (dataloader.load 的参数)和 callback_(new Dataloader 的参数)_ 放入队列。当 Event Loop 中所有的 promise resolve 执行完后,再通过 process.nextTick 方法执行队列中的所有任务。之所以使用process.nextTick ,是因为 libuv 在 Nodejs Event loop 中的每个 tick 之间,都会检查和执行 process.nextTick 微任务队列,并且 process.nextTick 在 Event loop 中拥有较高的执行优先级。

由于在 Event loop 中, process.nextTick 的优先级比同样是微任务的 Pomise resolve 要高, 所以为了避免 process.nextTick 提前执行 ,保证在当前所有进行中的 Promise 执行完之后再执行队列任务,dataloader 将process.nextTick 放在 Promise.resolve 中执行。

var enqueuePostPromiseJob =  
  typeof process === 'object' && typeof process.nextTick === 'function' 
   ? function (fn) {     
      // ...      
     // 在 Promise.resolve 中执行 process.nextTick        
     resolvedPromise.then(() => {        
       process.nextTick(fn);     
     });    
    } :  setImmediate || setTimeout;

Dataloader 使用

先看未使用 DataLoader 优化的例子:

const FAKE_DATABASE = ['JAVA', 'PHP', 'GOLANG', 'PYTHON', 'RUBY', 'PERL'];
const getTagById = async (id) => {  
  console.log('DATABASE accessed!')  
  return FAKE_DATABASE[id - 1];
}

for (let i = 1 ; i< 7; i++) { 
  getTagById(i);
}

运行结果可以看到,每次执行 ​getTagById​ 都会访问一次 FAKE_DATABASE ,当代码中有 7 处调用 ​getTagById​ 函数时,FAKE_DATABASE 一共被访问了 7 次。在数据量很大的情况下,这显然不是一个理想的方案:

不使用dataloader

批处理

DataLoader 有批量执行异步任务 (batching*)* 的能力,下面展示使用 DataLoader 优化批处理操作:

DataLoader 有批量执行异步任务的能力,下面展示使用 DataLoader 优化批处理操作:

const FAKE_DATABASE = ['JAVA', 'PHP', 'GOLANG', 'PYTHON', 'RUBY', 'PERL'];
const batchGetTagsById = async (ids) => {  
  console.log('batching:', ids);  
  return ids.map(id => FAKE_DATABASE[id - 1]);
};
const userLoader = new DataLoader(batchGetTagsById);
for (let i = 1 ; i< 7; i++) {    
  userLoader.load(i);
}

运行结果中可以看到使用 userLoader.load 方法合并了所有的调用参数,统一调用

使用dataloader batching

dataloader 执行时机

前面提到 dataloader 会在 process.next 时批量执行队列中的任务。下面的代码中使用 setTimeout 制造前后两个 Event loop tick:

import DataLoader from 'dataloader';
const FAKE_DATABASE = ['JAVA', 'PHP', 'GOLANG', 'PYTHON', 'RUBY', 'PERL'];
const batchGetTagsById = async (ids) => { 
  console.log('batching:', ids); 
  return ids.map(id => FAKE_DATABASE[id - 1]);
};
const userLoader = new DataLoader(batchGetTagsById);
console.log('\nEvent Loop Tick 1');
userLoader.load(1);
userLoader.load(2);
userLoader.load(5);
userLoader.load(6);
setTimeout(() => { 
 console.log('\nEvent Loop Tick 2'); 
 userLoader.load(3); 
 userLoader.load(4);
}, 1000);

当使用 setTimeout 时会在 nodejs Event loop 中新建一个 timer 宏任务(即一个 Eventloop tick)。从输出结果可以看到,dataloader 在每个 EventLoop tick 之间的阶段执行 batching 队列中的任务,而 libuv 正是在每个 tick 之间的阶段检查和执行 process.nectTick 任务:

dataloader执行时机

缓存

除了批量执行任务之外,Dataloader 还具备缓存(caching)的能力。当多次执行 dataloader load 方法并且参数一致时,将直接从缓存中返回:

import DataLoader from 'dataloader';
const FAKE_DATABASE = ['JAVA', 'PHP', 'GOLANG', 'PYTHON', 'RUBY', 'PERL'];
const batchGetTagsById = async (ids) => { 
 console.log('batching:', ids);
 return ids.map(id => FAKE_DATABASE[id - 1]);
};
const userLoader = new DataLoader(batchGetTagsById);
console.log('\nEvent Loop Tick 1');
userLoader.load(1);
userLoader.load(2);
userLoader.load(3);
userLoader.load(4);
userLoader.load(5);
userLoader.load(6);
setTimeout(() => { 
 console.log('\nEvent Loop Tick 2'); 
 userLoader.load(3).then(d=>console.log(d)); 
 userLoader.load(4).then(d=>console.log(d));
}, 1000);

运行结果可以看到在 setTimeout 中重复执行参数为 3、4 的 userLoader.load 方法时,dataloader 利用 cache 直接返回了数据:

使用 dataloader cache

需要注意的是,dataloader 缓存的是 promise,而不是数据结果

结合 GraphQL 使用

Resolver 使用 dataloader 的 load 方法实现,可以解决 GraqhQL 的 N + 1 问题。当 GraqhQL Client 请求的 query 中涉及重复执行 resolver 时,dataloader 会把它们放入队列合并执行:
还是上面查询文章和标签信息的例子,这次 GraphQL Server 使用 dataloader 实现 resolver:

// 每个请求初始化 DataLoader 实例,参数是批处理服务的 batch Promiseconst tagLoader = new DataLoader(ids => this.apiService.getTagsByIds(ids))// 每次执行 dataloader.load() 方法,都会将参数 id 进入队列,队列中的所有参数将最终合并传入 batch Promiseconst batchingGetTagsById = id => tagLoader.load(id)@Resolver(Tag)class TagResolver {  constructor(private apiService: ApiService) {}  @Query(returns => [Tag])     async tags(@Args() { id }: GetTagsArgs): Promise {     // 每次执行 TagResolver 获取 tags 时,执行 dataloader.load() 方法     return batchingGetTagsById(id);  }}

可以看到当 Client Query 中需要同时查询多个 tags 信息时,GraphQL 通过 datalaoder 不会再重读 n 次请求接口服务,而是把 n 次请求合并为一次,并且在 id 相同时还可以减少重复的请求。

Dataloader 实现

dataloader 的实现使用了 flow 而非 typescript)每当 new 一个 Dataloader 实例时,传入的 promise 参数即作为 batchLoadFn:

class DataLoader {  
  constructor(   
    batchLoadFn: BatchLoadFn,    
    options?: Options
  ) {   
   // ...  
  }  
  // ... 
  load(key: K): Promise {    
   // ...  
  } 
}

新建的实例拥有 load 方法,负责把每个执行 load 的函数放入任务队列:

  load(key: K): Promise {    
   // ...    
   // batch 为当前要处理的请求任务队列,包含 keys 和 callbacks    
   var batch = getCurrentBatch(this);    
   // ...    
   // 如果匹配中了 cache,则直接返回 cache    
   if (cacheMap) {      
      var cachedPromise = cacheMap.get(cacheKey);      
      if (cachedPromise) {        
        return new Promise(resolve => {            
           // ...            
           resolve(cachedPromise);        
        });      
      }    
    }    
   // 如果没有匹配到 cache,新生成一个 promise 放入请求任务队列中    
   batch.keys.push(key); 
   // 把 load 方法的参数作为 promise 的 key    
   var promise = new Promise((resolve, reject) => {      
      // 把 promise resolve 进入 callbacks 队列      
      batch.callbacks.push({ resolve, reject });    
   });    
   // 放入缓存,注意这里缓存的并不是执行结果,而是 promise   
   if (cacheMap) {      
     cacheMap.set(cacheKey, promise);    
   }    
   return promise;  
}

获取(或创建)当前需要批处理的任务队列:

function getCurrentBatch(loader: DataLoader): Batch {  
   // ...  
   // 如果任务队列已经存在,则直接返回已有的队列,避免重复创建 
   if (    existingBatch !== null && ...  ) {    return existingBatch;  }  
   // 创建唯一的任务队列,后续每当执行 load(key)方法时,key 会存入 keys,并把 key 对应的 promise resolve 存入 callbacks  
   var newBatch = { hasDispatched: false, keys: [], callbacks: [] }; 
   // ...  
  // 在所有 promise 之后的 nextTick 阶段批量处理队列中的请求  
  loader._batchScheduleFn(() => {    
       dispatchBatch(loader, newBatch); 
  });  
  return newBatch;
}

上面的 _batchScheduleFn 方法实际上返回的是 enqueuePostPromiseJob 函数,这个函数指明了 batching 队列的执行时机。dataloader 需要在当前所有的 promise resolve 之后批处理队列中的任务:

var enqueuePostPromiseJob =  
     typeof process === 'object' && typeof process.nextTick === 'function' 
          ?   function (fn) {      
                  if (!resolvedPromise) {        
                      resolvedPromise = Promise.resolve();      
                  }      
                 resolvedPromise.then(() => {        
                      process.nextTick(fn);      
                 });    
               } 
          :    setImmediate || setTimeout;

dispatchBatch 函数即为批量处理当前任务队列的方法:

function dispatchBatch(  loader: DataLoader,  batch: Batch) {  
  // ... 
   // 执行 _batchLoadFn 方法(新建 dataloader 实例时传入的 promise 函数),参数为所有调用 load 方法时传入的参数集合  
  var batchPromise = loader._batchLoadFn(batch.keys);  
  // ...  
  // 执行 batchPromise,返回批处理的结果 
  batchPromise.then(values => {    
      for (var i = 0; i < batch.callbacks.length; i++) {      
          var value = values[i];     
          if (value instanceof Error) {        
             batch.callbacks[i].reject(value);     
          } else {        
            batch.callbacks[i].resolve(value);      
       }    
     } 
  }
)}

React SSR

服务端渲染技术可以帮助提升页面的访问性能,降低加载时间。

非 SSR 场景

react 应用作为静态文件包的形式被服务端serve。访问页面时经历以下过程:

非SSR 流程

  1. 浏览器请求 URL

  2. 浏览器接收 index.html 文件作为响应

  3. 浏览器发送请求,下载remote链接和remote脚本

  4. 浏览器等待脚本下载

  5. react 渲染传进来的component 组件,最终将组件挂在到指定的 DOM 节点上

// All dependencies have to be loaded before the below
// code executes.
const renderApp = () => render(
    <App />, 
    document.querySelector("#mount")
);

在以上代码执行前,用户只能看到一个白屏的页面,在网络条件不好的情况下体验非常不好。

SSR 方案

使用非SSR 和 SSR 渲染的页面, DOMContentLoaded 几乎是相同的,但是非SSR页面在所有的脚本度加载之前,页面是空白的。SSR 渲染的页面几乎是立即可见的,脚本同时在后台一边加载。

SSR 流程

  1. 浏览器向打开的URL发送请求

  2. node server 收到请求并响应:将相关的 component 组件以一个字符串的形式渲染

// code on the server that renders the App to string. renderToString // is a function imported from 'react-dom/server'.
export const renderApp = (html, req, res) => {
  let appString = renderToString(<App />);
  let renderedApp = html.replace("<!--ssr-->", appString);
  res.status(200).send(renderedApp);
};
  1. 把渲染的 component 组件(字符串形式)注入到 index.html 文件中

  2. 把 index.html 文件发送回给浏览器

  3. 浏览器渲染 index.html 文件并下载所有其他的依赖

  4. 一旦脚本加载完毕, react 组件在 client 端会再次render。唯一的区别在于这次的渲染混合了页面上已有的视图,而不是重新覆盖(hydrates the existing view instead of overwriting it)。

混合页面上已有的视图,意味着事件 handlers 都绑定在已经渲染的 DOM 元素上,同时还保持了 DOM 元素的完整性(Hydrating a view means that it attaches any event handlers to the rendered DOM elements but keeps the rendered DOM elements intact)。

通过这种方式,既维护了 DOM 元素的完整性,同时避免了重置(reset)页面的视图。

SSR 代码实现

the best way to structure your code for server side rendered apps is to have as much of the codebase as possible to be isomorphic, i.e., capable of running on both the browser and on the server. To this end, I end up structuring my code like this

1_ggdnymfilsrw-ztvabtkmq

common 目录是整个应用的代码位置。clientserver 目录分别启动浏览器环境和服务器环境下运行的代码。

common app.js

// app.js
import React from "react";
export const App = () => <h2>Hello, World!</h2>;

client index.js

// client/index.js
import React from "react";
import { render } from "react-dom";
import { App } from "common/App";
const renderApp = () => render(<App />, document.querySelector("#mount"));
renderApp();
if (module.hot) {
  module.hot.accept("common/App", () => {
    renderApp();
  });
}

server app.js

// server/app.js
import React from "react";
import { renderToString } from "react-dom/server";
import { App } from "common/App";
export const renderApp = (html, req, res) => {
  let appString = renderToString(<App />);
  let renderedApp = html.replace("<!--ssr-->", appString);
  res.status(200).send(renderedApp);
};

注意

  1. 如果项目中使用了 css-in-js 的库,需要确保它支持 SSR

  2. 把组件需要的全部数据 push 给 root, 这样可以在服务端fetch 全部数据之前, 渲染整个应用。
    当数据获取完整(available)后,再将数据传递给 root 组件,root 组件将数据向下传递给叶子节点。这样来渲染应用,然后返回给网页。

参考

Server side rendering with React and Express

React Server Side Rendering (SSR) with Express and CSS Modules

SSR with React

A simple react ssr example with preboot

[Node] 错误优先回调

为了在node 模块和应用中统一平衡、非阻塞的异步控制流,node 采用了错误优先(error-first)的callback 回调形式。这种形式的回调是追溯到 Continuation-Passing Style (CPS)。CPS 中的“continuation function”接受一个函数作为参数,这个参数函数在函数体中其余代码执行完之后运行。这样的方式使得不同的函数能够异步地控制整个应用。

例子,一个标准的错误优先callback例子:

fs.readFile('/foo.txt', function(err, data) {
  // If an error occurred, handle it (throw, propagate, etc)
  if(err) {
    console.log('Unknown Error');
    return;
  }
  // Otherwise, log the file contents
  console.log(data);
});

处理错误的方式

  • 如果想让node 应用sutdown停止的话,可以直接throw 抛出异常;

  • 如果正处于一个异步流的阶段,可以把错误propagate 冒泡出来,传递给下一个next callback函数;

if(err) {
  // Handle "Not Found" by responding with a custom error page
  if(err.fileNotFound) {
    return this.sendErrorMessage('File Does not Exist');
  }
  // Ignore "No Permission" errors, this controller knows that we don't care
  // Propagate all other errors (Express will catch them)
  if(!err.noPermission) {
    return next(err);
  }
}

参考

The Node.js Way - Understanding Error-First Callbacks

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.