0%

背景

参照客户端的众多小说阅读APP,实现类似的多种翻页效果。

数据源格式

某小说某章节内容

此处以服务端返回的HTML格式作为章节内容数据源,对其进行分页计算和渲染。

常规分页方式

全民小说怎么设置翻页全民小说设置翻页教程

  • 纵向上下滚动翻页
  • 横向仿真翻页

img

  • 横向覆盖

掌阅怎么设置翻页方式掌阅设置翻页方式方法

  • 横向平移

纵横小说怎么设置翻页模式纵横小说设置翻页模式教程

  • 淡入淡出 [横向]

    ……

分页算法实现

纵向分页

纵向分页场景下实现无太大难度

  1. v-html 或者 dom.innerHTML 直接展示章节内容。
  2. 监听纵向范围内上拉和下拉的事件,更新新的章节。

横向分页

横向分页场景下通常需要对文本内容计算出每一页的内容,实际开发过程中也应用到了CSS columns【多列】的方案。

  • 一次性需要将所有内容全部排列并渲染,内容过多时会影响页面性能;****

  • 分页不可人为控制,不方便扩展(在某一页后面增加广告页);****

  • 翻页只能支持滑动效果,并不能支持“覆盖”、“仿真翻页”效果。**

从html => text

分页效果实现

Chrome Extension With Manifest V3

What Extension Is

Extensions are software programs, built on web technologies (such as HTML, CSS, and JavaScript) that enable users to customize the Chrome browsing experience.

扩展程序是基于 Web 技术(例如 HTML、CSS 和 JavaScript)构建的软件程序,可让用户自定义 Chrome 浏览体验。

以上是对chrome extension的官方介绍,个人理解就是:

Chrome Extension 是能通过『当前选项卡』『插件弹出页』『全局js脚本』『devtools信息』等合作通信去实现特定功能的后台程序。

What Manifest Is

About manifest.json

Each extender needs to have a **configuration checklist manifest.json document **, which provides basic information about the extension, such as required permissions, names, versions, and so on.

每一个扩展程序都需要有一个配置清单 manifest.json 文档,它提供了关于扩展程序的基本信息,例如所需的权限、名称、版本等。

  {
  // Required - 通俗易懂
  "manifest_version": 3,
  "name": "My Extension",
  "version": "versionString",

   // 『重点』action配置项主要用于点击图标弹出框,对于弹出框接受的是html文件
  "action": {
     "default_title": "Click to view a popup",
   	 "default_popup": "popup.html"
   }
    
  // 通俗易懂
  "default_locale": "en",
  "description": "A plain text description",
  "icons": {...},
  "author": ...,

  // 『重点』下面将出现的background.js 配置service work
  "background": {
    // Required
    "service_worker": "service-worker.js",
  },

    // 『重点』下面将出现content_script.js 应用于所有页面上下文的js
  "content_scripts": [
     {
       "matches": ["https://*.google.com/*"],
       "css": ["my-styles.css"],
       "js": ["content-script.js"]
     }
   ],

    // 使用/添加devtools中的功能
  "devtools_page": "devtools.html",


    /**
    * 三个permission
    * host_permissions - 允许使用扩展的域名
    * permissions - 包含已知字符串列表中的项目 【只需一次弹框要求允许】
    * optional_permissions - 与常规类似permissions,但由扩展的用户在运行时授予,而不是提前授予【安全】
    * 列出常见选项
    * {
    *		activeTab: 当扩展卡选项被改变需要重新获取新的权限
    *		tabs: 操作选项卡api(改变位置等)
    *		downloads: 访问chrome.downloads API 的权限 便于下载但还是会受到跨域影响
    *		history: history api权限
    *		storage: 访问localstorage/sessionStorage权限
    * }
    */
  "host_permissions": ["http://*/*", "https://*/*"],
  "permissions": ["tabs"],
  "optional_permissions": ["downloads"],

    // 内部弹出可选页面 - 见fehelper操作页
  "options_page": "options.html",
  "options_ui": {
    "chrome_style": true,
    "page": "options.html"
  },
}

Why Not Manifest V2

当前配置清单类型最新的版本是 Manifest V3,MV3,遵循该配置清单版本的扩展程序,会更注重安全和用户的隐私保护,在性能方面也会得到提升,同时开发简易性和功能实现也会更佳。

image-20220217175042667

img

Build A MV3 Hello Extension

https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/examples/hello-world

Project Structure

--hello
  --hello.html // 插件界面
  --background.js // service_worker
  --manifest.json // 配置文件
// manifest.json

{
  "name": "Hello, World!",
  "version": "1.0",
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "action": {}
}
// background.js

chrome.runtime.onInstalled.addListener(async () => {
  let url = chrome.runtime.getURL('hello.html');
  let tab = await chrome.tabs.create({ url });
  console.log(`Created tab ${tab.id}`);
});
<!-- hello.html -->

<!DOCTYPE html>
<html>
<head>
  <title>Hello, World!</title>
</head>
<body>
  <p>Hello, World!</p>
</body>
</html>

Install / Run Hello Extension

image-20220217183003834

image-20220217183440530

新版 Chrome 不再支持 .crx 形式的扩展程序,可以安装,但无法启用。

image-20220217184100318

image-20220217184118220

Debug Extension

image-20220217202754281

More Extension

page-render

https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/examples/page-redder

bookmarks

https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/examples/bookmarks

Chrome Extension Basic Composition

Keep coding, Keep thinking

service_worker(background.js)

The background script is the extension’s event handler; it contains listeners for browser events that are important to the extension. It lies dormant until an event is fired then performs the instructed logic. An effective background script is only loaded when it is needed and unloaded when it goes idle.

background script是扩展的事件处理程序; 它包含对扩展很重要的浏览器事件的侦听器。它处于休眠状态,直到触发事件,然后执行指示的逻辑。有效的后台脚本仅在需要时加载,并在空闲时卸载。

要关注两点:

  • 不使用时终止,需要时重新启动(类似于事件页面)。
  • 无权访问 DOM。(service worker 独立于页面)
chrome.runtime.onMessage.addListener((message, callback) => {
  const tabId = getForegroundTabId();
  if (message.data === "setAlarm") {
    chrome.alarms.create({delayInMinutes: 5})
  } else if (message.data === "runLogic") {
    chrome.scripting.executeScript({file: 'logic.js', tabId});
  } else if (message.data === "changeColor") {
    chrome.scripting.executeScript(
        {func: () => document.body.style.backgroundColor="orange", tabId});
  };
});

content_script

Extensions that read or write to web pages utilize a content_script. The content script contains JavaScript that executes in the contexts of a page that has been loaded into the browser. Content scripts read and modify the DOM of web pages the browser visits.

Content scripts can communicate with their parent extension by exchanging messages and storing values using the storage API.

读取或写入网页的扩展程序使用 content_script。内容脚本包含在已加载到浏览器的页面上下文中执行的 JavaScript。内容脚本读取和修改浏览器访问的网页的 DOM。

content_script可以通过使用storage/message API来与扩展其他部分进行通信。

注入方式

  • 对于manifest.json来说

    • 可以配置静态声明去注入
  • 可以通过编程方式注入 需要获取activeTab权限

// manifest.json
{
 "name": "My extension",
 ...
 "content_scripts": [
   {
     // 满足matches匹配的域名
     "matches": ["https://*.google.com/*"],
     // 注入css
     "css": ["my-styles.css"],
     // 注入js
     "js": ["content-script.js"],
     "run_at": "document_idle" | "document_start" | "document_end"
   }
 ],
  "permissions": [
    "activeTab"
  ],
}
  • 对于获取了权限的content_script通过代码执行注入
chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['content-script.js']
  });
});

运行于弹窗的html文件和JS脚本

image-20220217195734158

option

Just as extensions allow users to customize the Chrome browser, the options page enables customization of the extension. Options can be used to enable features and allow users to choose what functionality is relevant to their needs.

正如扩展程序允许用户自定义 Chrome 浏览器一样,选项页面支持扩展程序的自定义。选项可用于启用功能并允许用户选择与其需求相关的功能。

image-20220217200117586

devtools

A DevTools extension is structured like any other extension: it can have a background page, content scripts, and other items. In addition, each DevTools extension has a DevTools page, which has access to the DevTools APIs.

DevTools 扩展的结构与任何其他扩展一样:它可以有一个背景页面、内容脚本和其他项目。此外,每个 DevTools 扩展都有一个 DevTools 页面,可以访问 DevTools API。

Extending DevTools - Chrome Developers

每次打开 DevTools 窗口时,都会创建一个扩展的 DevTools 页面实例。DevTools 页面在 DevTools 窗口的整个生命周期内都存在。DevTools 页面可以访问 DevTools API 和一组有限的扩展 API。devtools 可以提供能力:

  • 能自定义添加新的Devtools panel
  • 能嵌入自定义的页面到自定义的panel中
  • devtools中的 elements & sources两个面板中

image-20220217200944834

image-20211020151945359

2.png

chrome.devtools.panels.create(
  '🔨 Beyla',
  'images/128x128.png',
  'html/devtools.html',
  (panel) => {
    // panel loaded
    panel.onShown.addListener(onPanelShown);
    panel.onHidden.addListener(onPanelHidden);
  }
);

// [devtools].js
chrome.devtools.panels.elements.createSidebarPane(
	"element pannel",
	(sidebar) => {
		sidebar.setExpression("(() => {return {a:1}})()");
	}
);

chrome.devtools.panels.sources.createSidebarPane(
	"sources pannel",
	(sidebar) => {
		sidebar.setPage("sources.html");
	}
);

Basic Composition Communication

组件间关系图

JS种类 可访问的API DOM访问情况 JS访问情况 直接跨域
injected script 和普通JS无任何差别,不能访问任何扩展API 可以访问 可以访问 不可以
content script 只能访问 extension、runtime等部分API 可以访问 不可以 不可以
popup js 可访问绝大部分API,除了devtools系列 不可直接访问 不可以 可以
background js 可访问绝大部分API,除了devtools系列 不可直接访问 不可以 可以
devtools js 只能访问 devtools、extension、runtime等部分API 可以访问devtools 可以访问devtools 不可以

content script && service worker/popup

content script 有关的通信

  • 使用 chrome.runtime.sendMessage 发送信息
  • 使用 chrome.runtime.onMessage.addListener 接收监听信息
// -----[content script].js-----
// 发送信息
chrome.runtime.sendMessage({ beylaDetected: true });

// -----[service worker].js-----
// 监听消息
chrome.runtime.onMessage.addListener((req, sender) => {
  if (sender.tab && req.beylaDetected) {
    chrome.action.setIcon({
      tabId: sender.tab.id,
      path: '../images/128x128.png',
    });
    chrome.action.setPopup({
      tabId: sender.tab.id,
      popup: 'html/beyla-enable.html',
    });
  }
});

当只有一对一关系时,可使用万能的chrome.runtime.sendMessage & chrome.runtime.onMessage.addListener

// -----[popup].js-----
document.querySelector("#button").addEventListener("click", () => {
	const val1 = document.querySelector("#input1").value || "0";
	const val2 = document.querySelector("#input2").value || "0";
	chrome.runtime.sendMessage({val1, val2}, (response) => {
		document.querySelector("#ans").innerHTML = response.res;
	});
});

// -----[service worker].js-----
const dealwithBigNumber = (val1, val2) => BigInt(val1) * BigInt(val2) + "";
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
	const {val1, val2} = request;
	sendResponse({res: dealwithBigNumber(val1, val2)});
});

本demo中引用了jquery,想看怎么操作的请看 源码 ~~

// -----[content script].js-----
// 接收popup数据并修改dom
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
	// document.querySelector("body").style.setProperty("background", request.color);
	$("body").css("background", request.color);
	sendResponse({name: 1});
});


// -----[popup].js-----
// 获取当前tab标签
const getCurrentTab = async () => {
	let queryOptions = {active: true, currentWindow: true};
	let [tab] = await chrome.tabs.query(queryOptions);
	return tab;
};

$("#background").paigusu({color: "#1926dc"}, async (event, obj) => {
	$("#info").innerHTML = "修改中";
	$("#show").css("background", "#" + obj.hex);
	const tab = await getCurrentTab();
	await chrome.tabs.sendMessage(tab.id, {color: "#" + obj.hex});
	$("#info").innerHTML = "修改成功";
});

devtools && content script

同1 content script与service worker/popup的通信

​ 1对多通信 使用chrome.tabs去找对应页面

devtools && popup

同2 content script与service worker/popup的通信

​ 1对1通信 使用chrome.runtime

Beyla Chrome Extension

Repo 参考 https://gitlab.ushareit.me/web/fe-workflow/h5/beyla-chrome-extension.git

Recommend Extension

image-20220224214333026

客户端 Webview 下发 ES 6 代码可行性探究

先聊聊历史

  • 1999 年12 月
    ECMAScript 3 (ES 3)
  • 2009 年12 月
    ECMAScript 5 (ES 5)
  • 2015 年6 月
    ECMAScript 2015 (ES 6)
  • ……ES 7、ES 8、ES 9

现状

大部分前端开发人员热衷于使用新的 JavaScript 语言特性来书写 JS 代码,例如 async 、 await 、 classes 、 arrow functions 等。然而,尽管目前所有的前沿浏览器都能运行 ES2015+ 代码(ES2015及俗称的ES6),但是为了兼容占有小比例的低版本浏览器用户,大部分的开发者仍然使用 polyfills 将代码编译成 ES5 语法。

当下标准的做法是:写 ES6 代码 → 将所有代码编译成 ES5 的(比如通过 Babel)→ 再将编译后的代码加载到浏览器执行。

这可能已经不再是最有效率的方式了。因为用这种方式,我们强制最新的浏览器运行旧代码,实际上它们完全可以运行最新的代码。它们支持 ES6,我们难道不能直接给它们 ES6 代码吗?

ECMAScript 6 compatibility table

image-20210419235736321

这个页面从 2010 年开始一直在更新,可以说完整地记录了现代浏览器对 ES6 特性支持的历史进程。中间最大这块绿色是桌面浏览器,最右边的是移动浏览器,中间有两列是 NodeJS 环境。是不是绿得很整齐?如果我们的用户恰好都在绿色这一块,代码是不是只需要转译到 ES6 就够了?

当然可以。但可以做不等于必须做,我们还需要更充分的理由。为了寻找这样的理由,谷歌工程师 Philip Walton 就 用自己的博客做了实验。他在构建自己的博客程序时,把 JS 代码分别转译为 ES5 和 ES6,结果很有意思。ES6 版本的文件尺寸和执行时间都只有 ES5 版本的一半不到!

ES5/6 构建文件尺寸和执行时间对比

版本 文件尺寸 执行时间
编译压缩 Gzip 压缩 测量值 平均值
ES2015+ 80K 21K 184ms、164ms、166ms 172ms
ES5 175K 43K 389ms、351ms、360ms 367ms

image-20210420224919914

image-20210420224928669

这里面虽然有些背景交代得不清楚,可能存在夸大的成分,但这样的数字确实很吸引人。对于互联网行业来说,带宽和时间最终都是可以换算成钱的!

收益在哪

程序员在进行技术决策的时候,通常的目的是为了kpi升值加薪这个技术很流行,不,当然是为了对业务产生价值。

不难发现,直接部署es 6代码可以带来的好处包括:

  • 代码体积减小
  • 性能提升
  • 缩短构建时间

代码体积减小

首先,es6带来了新的语法和特性,这可以让代码更简洁,使得代码量减少。其次,babel转译之后,会产生一些helper,引入一些polyfill,而且有一些语法特性无法在运行时进行处理,必须在转译过程中修改源代码,这也会导致代码量增加。当然,部署es6代码也需要babel转译一部分更新的语法,并不是说完全不使用babel。

Google 工程师 es6代码的bundle size比es5代码减少了50%**,而我自己的实践的结果没有这么夸张,是25%左右**。

version bundle size Gzipped
es5 221kb 39kb
es6 170kb 29kb

之所以我和他的测试结果差异较大,是因为最终的结果跟每个人的代码风格和代码内容有很大的关系。我用来进行测试的代码是一个线上项目的代码,我个人的代码风格是大量使用es6/7/8语法,不过项目本身大量是在写业务逻辑,并且项目使用了Vue,其实很多js代码是模板编译之后生成的渲染函数。

所以,大概的结论是es6的代码会比es5的代码体积减少**15% ~ 50%**,具体能减少多少,取决于代码风格以及代码内容。代码风格越现代,代码中”生成的代码”越少,收益越高。

性能提升

es6代码相对于es5代码,性能更高的原因我认为有2个

  • 代码量减少带来的解析时间减少,这个正比于代码体积的减少
  • 运行性能的提高

对于第一点,浏览器解析代码时间的减少应该占比较小,通过chrome的Performance面板可以看到,解析一个200kb的js文件Compile Script耗时为10ms

对于第二点,运行时性能。在es6刚刚发布的时候,很多新特性的性能是比较低的,因为js引擎没有足够的时间去进行优化,相比较而言,es5以及更老的代码经过了浏览器的长时间优化,在es6刚出来的时候,很对人也对es6的性能有很大的担忧。

Github上有一个仓库,专门对es6和es5进行了性能对比。

image-20210421122126368

从对比结果可以看出来,在chrome 72版本上,es6代码的性能优于babel转译成的es5代码,和手写的es5代码比起来各有千秋,基本算是55开。

针对运行时的es6代码和es5代码的性能对比,我也用一个线上项目进行了实验。实验方式如下

  1. 利用performance API在打包后的app.js文件开头记下时间戳,在js文件末尾减去开始的时间,得到一个时间,众所周知,打包之后的app.js是一个立即执行函数,所以这个时间包含了app.js文件中部分代码的运行时间
  2. 利用chrome 的Performance面板,可以直接得到一段js的执行时间Evaluate Script

实验结果如下(20次的平均值)

version 记录的运行时间 Performance面板的Evaluate Script
es5 258.88ms 295.29ms
es6 206.28ms 241.59ms

测试条件下,es6的代码取得了20%左右的运行性能提升

Shareit 系APP对ES 6支持情况

检查 ES 6 兼容性

最新的Mac Chrome

image-20210421104026586

image-20210420232910160

注释:什么是尾调用优化

  • Android 版本对ES 6 支持情况

本地设备列表【Genymotion 模拟】

image-20210420231211354

  • Android 4.4

image-20210420231614343

  • Android 5.0

image-20210420231821967

  • Android 5.1

![image-20210420231748619](/Users/chenlei/Library/Application Support/typora-user-images/image-20210420231748619.png)

image-20210420232729704

  • Android 6.0

image-20210420232053842

![image-20210420232600122](/Users/chenlei/Library/Application Support/typora-user-images/image-20210420232600122.png)

  • Android 7.0

![image-20210420232020159](/Users/chenlei/Library/Application Support/typora-user-images/image-20210420232020159.png)

  • Android 7.1

image-20210420232253070

likeit-lite-task项目

  • Webview 分布
89.0.4389 88.0.4324 87.0.4280 83.0.4103 81.0.4044 80.0.3987 74.0.3729 70.0.3538 其他
47% 8% 5% 5% 2% 5% 2% 3% 16%
  • Android 版本分布
10 9 8.1 11 7.1.2 7.1.1 6.0.1 7.0 5.1.1 其他
36% 21% 19% 4% 3% 3% 2% 2% 2% 3%

image-20210420233722214

  • 修改前:

image-20210421000418656

  • 修改后:

![image-20210421000447491](/Users/chenlei/Library/Application Support/typora-user-images/image-20210421000447491.png)

话说回来,构建成 ES5 和 ES6,到底差别在哪里?执行变快还容易理解,为什么代码量也可以少这么多?下面的图是通过 Webpack 构建分析工具得到的,ES5 版本代码比 ES6 主要多了两大块。一个是 core-js,包含了很多 ES6 特性的 polyfill,这些代码在 Gzip 压缩以后仍然超过 10K。

image-20210421000552744

另一个是 regenerator-runtime,这是 ES 6 生成器转译为 ES 5 所产生的,单单这一个东西,Gzip 压缩后就有 2.37K 之多,是 ES 6 转译为 ES 5 当之无愧的第一大 polyfill。开发的时候用 ES 8 特性 async/await,构建时 Babel 就会把它们先转译为 ES 6 的生成器,再转译为 ES5。

![image-20210421000619834](/Users/chenlei/Library/Application Support/typora-user-images/image-20210421000619834.png)

除此之外,还有一些语法特性是无法在运行时进行处理的,必须在转译过程中修改源代码。这些修改也会导致代码量增加。

image-20210421000812430

降级方案

image-20210421133454968

<script type="module" src="es6.js"></script>  
  
<script nomodule src="es5.js"></script>

image-20210421111842163

![image-20210421000922146](/Users/chenlei/Library/Application Support/typora-user-images/image-20210421000922146.png)

实现方式

如果已经使用了 webpack 或者 rollup 这类模块打包工具来生成 JS 文件,那么应该继续保持。

除了当前的代码包,还需要生成类似于第一份的另外一份代码包。(该代码包使用了 ES2015+ 语法),唯一的不同是你不需要将其编译成 ES5 语法的代码,并且不需要引入 polyfills 插件。

例如,假设你使用了 webpack 并且 JS 的入口文件是 ./path/to/main.js ,你当前的 ES5 版本的配置应该如下所示

module.exports = {
  entry: {
    'main-legacy': './path/to/main.js',
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['env', {
              modules: false,
              useBuiltIns: true,
              targets: {
                browsers: [
                  '> 1%',
                  'last 2 versions',
                  'Firefox ESR',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};
module.exports = {
  entry: {
    'main': './path/to/main.js',
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['env', {
              modules: false,
              useBuiltIns: true,
              targets: {
                browsers: [
                  'Chrome >= 60',
                  'Safari >= 10.1',
                  'iOS >= 10.3',
                  'Firefox >= 54',
                  'Edge >= 15',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};

一旦运行,这两个配置文件就会输出两个 JS 文件:

  • main.js (该文件支持 ES2015+ 语法)
  • main-legacy.js (该文件支持 ES5 语法)
<!-- Browsers with ES module support load this file. -->
<script type="module" src="main.js"></script>
 
<!-- Older browsers load this file (and module-supporting -->
<!-- browsers know *not* to load this file). -->
<script nomodule src="main-legacy.js"></script>
<script>
  var script = document.createElement('script')
	if (内核版本 > 60) {
    script.src = 'es6.js'
    script.onload = () {}
  } else {
    script.src = 'es5.js'
  }
  document.body.appendChild(script)
</script>

image-20210421000934553

Polyfill

JavaScript 语言在稳步发展。也会定期出现一些对语言的新提议,它们会被分析讨论,如果认为有价值,就会被加入到 https://tc39.github.io/ecma262/ 的列表中,然后被加到 规范 中。

JavaScript 引擎背后的团队关于首先要实现什么有着他们自己想法。他们可能会决定执行草案中的建议,并推迟已经在规范中的内容,因为它们不太有趣或者难以实现。

因此,一个 JavaScript 引擎只能实现标准中的一部分是很常见的情况。

查看语言特性的当前支持状态的一个很好的页面是 https://kangax.github.io/compat-table/es6/(它很大,我们现在还有很多东西要学)。

Polyfill或者Polyfiller,是英国Web开发者 Remy Sharp 在咖啡店蹲坑的时候拍脑袋造出来的。当时他想用一个词来形容”用JavaScript(或者Flash之类的什么鬼)来实现一些浏览器不支持的原生API”。Shim这个已经有的词汇第一时间出现在他的脑海里。但是他回头想了一下Shim一般有自己的API,而不是单纯实现原生不支持的API。苦思冥想一直想不到合适的单词,于是他一怒之下造了一个单词Polyfill。除了他自己用这个词以外,他还给其他开发者用。随着他在各种Web会议演讲和他写的书《Introducing HTML5》中频繁提到这个词,大家用了都觉得很好,就一起来用。

Polyfill的准确意思为:用于实现浏览器并不支持的原生API的代码。

如,querySelectorAll是很多现代浏览器都支持的原生Web API,但是有些古老的浏览器并不支持,那么假设有人写了库,只要用了这个库, 你就可以在古老的浏览器里面使用document.querySelectorAll,使用方法跟现代浏览器原生API无异。那么这个库就可以称为Polyfill或者Polyfiller

好,那么问题就来了。jQuery是不是一个Polyfill ?答案是No。因为它并不是实现一些标准的原生API,而是封装了自己API。一个Polyfill是抹平新老浏览器 标准原生API 之间的差距的一种封装,而不是实现自己的API。

已有的一些Polyfill,如 Polymer 是让旧的浏览器也能用上 HTML5 Web Component 的一个PolyfillFlashCanvas是用Flash实现的可以让不支持Canvas API的浏览器也能用上Canvas的Polyfill

这里有一堆Polyfills,有兴趣可以把玩一下:HTML5 Cross Browser Polyfills

过去

shim + sham

如果你是一个 3 年陈 + 的前端,应该会有听说过 shim、sham、es5-shimes6-shim 等等现在看起来很古老的补丁方式。

那么,shim 和 sham 是啥?又有什么区别?

  • shim 是能用的补丁
  • sham 顾名思义,是假的意思,所以 sham 是一些假的方法,只能使用保证不出错,但不能用。至于为啥会有 sham,因为有些方法的低端浏览器里根本实现不了

babel-polyfill.js

在 shim 和 sham 之后,还有一种补丁方式是引入包含所有语言层补丁的 babel-polyfill.js。比如:

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.2.5/polyfill.js"></script>

然后就 es6、es7 特性随便写了。

但缺点是,babel-polyfill 包含所有补丁,不管浏览器是否支持,也不管你的项目是否有用到,都全量引了,所以如果你的用户全都不差流量和带宽(比如内部应用),尽可以用这种方式。

现在

现在还没有银弹,各种方案百花齐放。

@babel/preset-env + useBuiltins: entry + targets

babel-polyfill 包含所有补丁,那我只需要支持某些浏览器的某些版本,是否有办法只包含这些浏览器的补丁?这就是 @babel/preset-env + useBuiltins: entry + targets 配置的方案。

我们先在入口文件里引入 @babel/polyfill

import '@babel/polyfill';

然后配置 .babelrc,添加 preset @babel/preset-env,并设置 useBuiltInstargets

{
  "presets": [
    ["@babel/env", {
      useBuiltIns: 'entry',
      targets: { chrome: 60 }
    }]
  ]
}

useBuiltIns: entry 的含义是找到入口文件里引入的 @babel/polyfill,并替换为 targets 浏览器/环境需要的补丁列表。

替换后的内容,比如:

import "core-js/modules/es7.string.pad-start";
import "core-js/modules/es7.string.pad-end";
...

这样就只会引入 chrome@62 及以上所需要的补丁,什么 Promise 之类的都不会再打包引入。

Ployfill.io 方案

动态 Polyfill 是根据不同浏览器的特性,载入需要的特性补丁。Polyfill.io 通过尝试使用 polyfill 重新创建缺少的功能,可以轻松地支持不同的浏览器,并且可以大幅度地减少构建体积。

Financial Times 在开发和维护这个项目,所以我们能确信这个项目可以持续更新下去.

有一点需要明白:Polyfill.io 没有提供语法糖支持。比如 类、增强的对象字面量,以及箭头函数之类的特性。对那些代码,你仍然需要进行编译。

动态 Polyfill 方案对比:

方案 优点 缺点 是否采用
babel-polyfill React16 官方推荐 1. 包体积 200K+,难以单独抽离 Map、Set 2. 项目里 React 是单独引用的 CDN,如果要用它,需要单独构建一份放在 React 前加载
babel-plugin-transform-runtime 能只 polyfill 用到的类或方法,相对体积较小 不能 polyfill 原型上的方法,不适用于业务项目的复杂开发环境
自己写 Map、Set 的 Polyfill 定制化高,体积小 1. 重复造轮子,容易在日后年久失修成为坑 2. 即使体积小,依然所有用户都要加载
polyfill-service 只给用户返回需要的 polyfill,社区维护 部分国内奇葩浏览器 UA 可能无法识别(但可以降级返回所需全部 Polyfill)

Polyfill Service原理

  • 每次打开页面,浏览器都会向Polyfill Service发送请求,Polyfill Service识别 User Agent,下发不同的 Polyfill,做到按需加载Polyfill的效果。

    Polyfill Service原理

使用方法

直接引入代码即可使用默认配置的 Polyfill:

<script crossOrigin="anonymous" src="https://polyfill.io/v3/polyfill.min.js"></script>

Polyfill.io 通过分析请求头信息中的 UserAgent 实现自动加载浏览器所需的 polyfill。

image-20210421143435576

高级用法

Polyfill.io 有一份默认捆绑列表,包括了最常见的 HTML5 中的 document.querySelectorElement.classList、ES5、ES6、ES7 中的 PromisefetchArray.from 等等。

你可以通过传递 features 参数来自定义功能列表:

<!-- 加载 Promise&fetch -->
<script src="https://cdn.polyfill.io/v3/polyfill.min.js?features=Promise,fetch"></script>

<!-- 加载所有 ES5&ES6 新特性 -->
<script src="https://cdn.polyfill.io/v3/polyfill.min.js?features=es5,es6,es7"></script>

Polyfill.io 还提供了其他 API,具体请查阅官方文档:

<!-- 异步加载 -->
<script src="https://cdn.polyfill.io/v3/polyfill.min.js?callback=main" async defer></script>
<!-- 无视 UA,始终加载 -->
<script src="https://cdn.polyfill.io/v3/polyfill.js?features=modernizr:es5array|always"></script>

未来

关于补丁方案的未来,我觉得按需特性探测 + 在线补丁才是终极方案。

按需特性探测保证特性的最小集;在线补丁做按需下载。

按需特性探测可以用 @babel/preset-env 配上 targets 以及试验阶段的 useBuiltIns: usage,保障特性集的最小化。之所以说是未来,因为 JavaScript 的动态性,语法探测不太可能探测出所有特性,但上了 TypeScript 之后可能会好一些。另外,要注意一个前提是 node_modules 也需要走 babel 编译,不然 node_modules 下用到的特性会探测不出来。

结语

时间带走一切,es5也不例外。

编写 ES6 只是程序员的胜利

部署 ES6 才是与用户的双赢

丹丹小可爱,聪明机智如你,想必一进屋子就知道我要干嘛了吧。此刻,你的心情是怎样的呢?灰色是不想说,蓝色是忧郁,而漂泊的你狂浪的心停在哪里(唱出来)。我猜,你一定和我一样,激动中有几丝不安,兴奋又满心期许吧。不过先别急,我们先来看一段视频。

丹丹小可爱,你还记得19年我们第一次见面的那一天吗?虽然咱俩对外都声称,是因为朋友的朋友这层关系结识的,哈哈,其实咱俩属于网友线下见面,最终双向奔赴。所以朋友们,你们问我网恋靠不靠谱?我不好说,至少在我身上是靠谱的哈。咱俩邂逅在深圳5月的初夏,起源在南山区的一家猫咖啡店,在这里我留下了贻笑大方被你后来翻出来反复“鞭尸”的难忘记忆。不怕大家笑话,丹丹说我当时和她讲话时说着说着,突然跑开一个人过去逗猫了🤣,但为啥我一点印象没有嘞,这大概就是当局者迷的吧。我们去吃了同仁四季的椰子鸡,这在后来变成了我们在外面最爱吃的美食,在胃口上大相径庭的我们,难得拥有了宝贵的共同爱好,太赞了👍!我们在一起后,开始尝试自己烹饪椰子鸡,再回到同仁四季对照口味,历经反复尝试,终于研究出新鲜出炉的 ”丹丹牌椰子鸡“,对门的美食家老李吃过后也连声称好。谢谢你对美食的热爱,谢谢你的探索精神,让我一次吃鸡吃到爽,爽啊!

后来呀,咱们的见面频次从周末见一次,升级到周末见两次,再到周一到周五晚上下班以后都出来见面,那些难忘的相伴在深圳湾的记忆,不只停留在那些晚上潮起潮落的夜风里,也在我的脑海里挥之不去。我会记得你对我讲你小时候多舛的际遇,讲你初高中时铭记在心的三两知心好友,讲你斑斓多彩的大学时光,讲你旅行途中遇见的形形色色有趣的生命。。。现在你还会偶尔讲讲那些日子,我都记在心里了呢。谢谢你对我的信任,谢谢你愿意把你的生活和过去分享给我。以后,也请你继续讲给我听,好吗?

我还记得有一回是6月初,我第二天早上要搭乘早班机跟同事去日本团建,而前一天晚上咱们还在深圳湾彻夜长谈到凌晨两三点,许是那个时候,我们渐渐对双方产生了依赖。从日本回来,咱们相约去了就近的河源,河源没有什么特别,唯一让我记忆比较深刻的就只剩一碧万顷的万绿湖和湖畔边的那间雅致的民宿了。从河源归来,咱俩正式在一起了,这么看来,河源也算是我们的定情之地呢。在那之后,我过上了下班有饭吃,家里有人牵挂,午餐还有爱心便当的幸福🥰生活。谢谢你绝佳的厨艺,谢谢你对我无微不至的照顾,从此我的体重一往无前,一度比初识你时胖了20多斤,这是当之无愧的幸福肥呀!

9月咱们飞去了菲律宾,上一回去日本是我第一回出国,完全是跟着大部队统一行动,这回完全是我们说走就走的自由行,全程我们自己做主。还好有你,慷慨地贡献你的人文地理知识,贡献你的旅途经验,而我,偶尔充当一两回胖翻译,谁让咱英文词汇比你多哈哈。你带着我,看见了清澈见底的湛蓝的海,海里游曳着五彩缤纷的海鱼、水母、珊瑚; 你带着我,住在了一览众山小的山顶民宿,我们在山顶碧蓝的泳池里,极目远眺,一切美景尽收眼底,多么自在惬意。后来你又带着我潜水,我怕得要命,就连跟你一起坐游艇我当时都害怕得不行,😒哼,都怪你当时眉飞色舞地对我讲之前发生在泰国的那起 400多人的沉船事故,当时我们乘坐的游艇一直在汹涌的波涛上上蹿下跳摇晃个不停,时刻感觉要侧翻,你偏偏对我讲,泰国的那起事故沉船也是这个型号,一旦船翻了,咱们都出不去。可是后来,在潜水教练的耳提面命和当头棒喝下, 四肢不协调的我在你的耐心配合和指导下,最终还是考下了潜水证,对水不再那么恐惧,可以携手与你鱼翔浅底,一起看到海面下的另一番风景,那是过去的人生里都不曾领略的难忘一生的风景。谢谢你在我软弱怯懦时候的理解和包容,谢谢你帮我克服恐惧,谢谢你对人生的热爱,谢谢你带我领略人生的风景!

11月这次是你陪着我第一次来北京看我喜欢的雅尼的音乐会。11月的京城,满地金黄,在雄伟的天安门,在庄严的故宫,在热闹的南锣鼓巷,在僻静的帽儿胡同,你在我的手机相机里留下了一个个婀娜多姿的倩影,在庄重神圣的人民大会堂,你陪我圆了我的一个梦。谢谢你陪着我,谢谢你支持理解我的喜好,我好开心呐。从那以后我也开始理解你的爱好,爱上了你爱听的赵雷,赵雷甚至一度成了我听歌最多的那位,我开始关注了你在意的那些B站up主,和你一起窝在被窝里看“非洲飞哥”,一起听“波士顿圆脸”入眠。。。从最初咱俩没啥共同话题爱好,也开始慢慢越来越默契,这应该就是爱的力量吧。谁能想到,半年后咱们直接跨域南北离开温暖潮湿的深圳,来到天干物燥的北京,现在站在这里。在那之后,就是遇到你们,台下的这群相识在北京的可爱的朋友们。

我们和其他众多的情侣一样也闹过别扭闹分手,大多数时候过错一方都在我,有时候自己都会觉得自己好傻,怎么会做出这种行为,脑子是不是上学那会学傻了,还是写代码写魔怔了。我记得19年11月份给小可爱过第一个生日,提前预订的鲜花结果没送到,定的蛋糕倒是送到了,却在蜡烛都还没插上等许愿吹灭呢,就被我提前切割了;还有今年的 2.14 情人节,我说出了“挺想送你浪琴的,但我嫌贵”这种蠢话。 可是你最后都用宽广的心包容了我,除了你,不会再有第二个人能忍受我吧。谢谢你的理解和宽容,谢谢你的大度,谢谢你的坦然和真诚!在和你的相处中,我渐渐地变得成熟,渐渐地变得优秀,渐渐地变成你喜欢的样子,在将来的日子里,我一定会在你的鼓励下变得更好!

丹丹小可爱,你常常会问,你爱我吗?刚在一起那会儿,我对你应该是好感和喜欢大过了爱。爱吗?当然爱。爱有几分?我说不太清。那现在呢?我爱你100分,100% 爱你😘。谢谢你的勇敢,谢谢你的坚持,谢谢你的宽容,谢谢你的热情,谢谢你的同理心和共情,谢谢你的诚实正直,谢谢你的善良,谢谢你的博学多知,谢谢你身上还有许许多多的难能可贵的闪光点,谢谢你让我遇到一个有趣的灵魂。

今天陪同和见证我们幸福现场的是一群我们在北京遇到的良人们,谢谢你们,我们的好朋友们!还要感谢在这段日子丹丹的朋友们无私的配合和帮助,感谢梦圆妹妹、芝芝、蛙牛、马狗、叶子、豹子、虎子、小爷阿文等好友,最要感谢丹丹的爸爸妈妈和恩恩弟弟,谢谢你们把赵丹交给我,请你们放心,我一定和丹丹相亲相爱、相辅相成、相濡以沫、相敬如宾。

求婚时间

2021.11.14 [周日] 晚。丹丹生日在第二天,农历10.11。

地点

考虑到是在周日进行,直接带她去酒店太突兀。以及要想个合适的理由“骗”她出门,目前我的想法是带着她参加集体活动,她就不会起疑了,且能保证出门形象是OK的。计划是周日上半场 + 下半场。

  • 下午大家一起出门 KTV ,晚上大家可能一起聚餐,最后我带她去提前定好的酒店。但由于切换了场地, 转场如何我还欠缺考虑,如何让大家再突然出现。

KTV 求婚也考虑过,丹丹喜欢唱歌,大家也都在,之前参加别人的求婚,见过了电影院求婚的创意,KTV 这个点子至少还算新,就是不知道会不会太吵闹,KTV 能不能播放我的求婚视频和亲朋好友的祝福视频。

  • 上半场大家一起去轰趴馆,晚上后续的计划和前面一致。

准备工作

从网络上搜索了不少求婚策划、准备相关的东西,目前确定是在室内进行求婚。需要准备这些,多多益善

  • 求婚戒指 【今晚先拿回测手指的戒圈快递】,准备直接从网上购买
  • 求婚视频,包含丹丹爸妈、弟弟、闺蜜、死党还有你们等,目前进行中
  • 整理打印照片,制作相册
  • 投影仪?待定视频播放方式
  • 场地布置:打气筒、气球、星空灯、背景音乐
  • 自己形象【目前纠结中,西装or便装?】
  • 预订鲜花
  • 求婚台词准备

调试是每个程序员必备的技能,因此选择合适的调试工具能极大地方便我们调试代码。Node.js 的调试方式也有很多,常见的有:

  1. 万能的 console.log
  2. debugger
  3. node-inspector

以上本节都不会讲解,因为:

  1. console.log 就不用说了。
  2. debugger 不推荐使用,因为:
    1. 使用繁琐,需手动打点。
    2. 若忘记删除 debugger,还会引起性能问题。
  3. node-inspector 已经退出历史舞台。node@6.3 以后内置了一个调试器,可以结合 Chrome DevTools 使用,而且比 node-inspector 更强大。

Chrome DevTools 调试

JavaScript 程序越来越复杂,调试工具的重要性日益凸显。客户端脚本有浏览器,Node 脚本怎么调试呢?

img

2016年,Node 决定将 Chrome 浏览器的”开发者工具”作为官方的调试工具,使得 Node 脚本也可以使用图形界面调试,这大大方便了开发者。

一、示例程序

为了方便讲解,下面是一个示例脚本。首先,新建一个工作目录,并进入该目录。

$ mkdir debug-demo
$ cd debug-demo

然后,生成package.json文件,并安装 Koa 框架和 koa-route 模块。

$ npm init -y
$ npm install --save koa koa-route

接着,新建一个脚本app.js,并写入下面的内容。

// app.js
const Koa = require('koa');
const router = require('koa-route');

const app = new Koa();

const main = ctx => {
  ctx.response.body = 'Hello World';
};

const welcome = (ctx, name) => {
  ctx.response.body = 'Hello ' + name;
};

app.use(router.get('/', main));
app.use(router.get('/:name', welcome));

app.listen(3000);
console.log('listening on port 3000');

上面代码是一个简单的 Web 应用,指定了两个路由,访问后会显示一行欢迎信息。如果想了解代码的详细含义,可以参考 Koa 教程

二、启动开发者工具

现在,运行上面的脚本。

$ node --inspect app.js

上面代码中,--inspect参数是启动调试模式必需的。这时,打开浏览器访问http://127.0.0.1:3000,就可以看到 Hello World 了。

img

接下来,就要开始调试了。一共有两种打开调试工具的方法,第一种是在 Chrome 浏览器的地址栏,键入 chrome://inspect或者about:inspect,回车后就可以看到下面的界面。

img

在 Target 部分,点击 inspect 链接,就能进入调试工具了。

第二种进入调试工具的方法,是在 http://127.0.0.1:3000 的窗口打开”开发者工具”,顶部左上角有一个 Node 的绿色标志,点击就可以进入。

img

三、调试工具窗口

调试工具其实就是”开发者工具”的定制版,省去了那些对服务器脚本没用的部分。

它主要有四个面板。

  • Console:控制台
  • Memory:内存
  • Profiler:性能
  • Sources:源码

img

这些面板的用法,基本上跟浏览器环境差不多,这里只介绍 Sources (源码)面板。

四、设置断点

进入 Sources 面板,找到正在运行的脚本app.js

img

在第11行(也就是下面这一行)的行号上点一下,就设置了一个断点。

ctx.response.body = 'Hello ' + name;

img

这时,浏览器访问 http://127.0.0.1:3000/alice ,页面会显示正在等待服务器返回。切换到调试工具,可以看到 Node 主线程处于暂停(paused)阶段。

img

进入 Console 面板,输入 name,会返回 alice。这表明我们正处在断点处的上下文(context)。

img

再切回 Sources 面板,右侧可以看到 Watch、Call Stack、Scope、Breakpoints 等折叠项。打开 Scope 折叠项,可以看到 Local 作用域和 Global 作用域里面的所有变量。

Local 作用域里面,变量name的值是alice,双击进入编辑状态,把它改成bob

img

然后,点击顶部工具栏的继续运行按钮。

img

页面上就可以看到 Hello bob 了。

img

命令行下,按下 ctrl + c,终止运行app.js

五、调试非服务脚本

Web 服务脚本会一直在后台运行,但是大部分脚本只是处理某个任务,运行完就会终止。这时,你可能根本没有时间打开调试工具。等你打开了,脚本早就结束运行了。这时怎么调试呢?

$ node --inspect=9229 -e "setTimeout(function() { console.log('yes'); }, 30000)"

上面代码中,--inspect=9229指定调试端口为 9229,这是调试工具默认的通信端口。-e参数指定一个字符串,作为代码运行。

访问chrome://inspect,就可以进入调试工具,调试这段代码了。

img

代码放在setTimeout里面,总是不太方便。那些运行时间较短的脚本,可能根本来不及打开调试工具。这时就要使用下面的方法。

$ node --inspect-brk=9229 app.js

上面代码中,--inspect-brk指定在第一行就设置断点。也就是说,一开始运行,就是暂停的状态。

六、忘了写 –inspect 怎么办?

打开调试工具的前提是,启动 Node 脚本时就加上--inspect参数。如果忘了这个参数,还能不能调试呢?

回答是可以的。首先,正常启动脚本。

$ node app.js

然后,在另一个命令行窗口,查找上面脚本的进程号。

$ ps ax | grep app.js 

30464 pts/11   Sl+    0:00 node app.js
30541 pts/12   S+     0:00 grep app.js

上面命令中,app.js的进程号是30464

接着,运行下面的命令。

$ node -e 'process._debugProcess(30464)'

上面命令会建立进程 30464 与调试工具的连接,然后就可以打开调试工具了。

还有一种方法,就是向脚本进程发送 SIGUSR1 信号,也可以建立调试连接。

$ kill -SIGUSR1 30464

七、参考链接

VS Code 调试

Visual Studio Code(简称 VS Code)是一款微软开源的现代化、跨平台、轻量级的代码编辑器。VS Code 很好很强大,本节将介绍如何使用 VS Code 来调试 Node.js 代码。

基本调试

示例代码如下:

// app.js
const Paloma = require('paloma')
const app = new Paloma()

app.use(ctx => {
  ctx.body = 'hello world!'
})

app.listen(3000)

用 VS Code 加载 test 文件夹,打开 app.js,然后进行如下操作:

  1. 单击左侧第 4 个 tab,切换到调试模式。
  2. 单击代码第 5 行 ctx.body='hello world!' 左侧空白处添加断点。
  3. 单击左上角 ”调试“ 的绿色三角按钮启动调试。
  4. 单击左上角的终端图标打开调试控制台。

最终如下所示:

img

从 “调试控制台“ 切换到 ”终端“,运行:

$ curl localhost:3000

如下所示:

img

可以看出,VS Code 基本覆盖了 Chrome DevTools 的所有功能,并且有两个额外的优点:

  1. 集成了终端,不用再打开新的终端输入命令了。
  2. 调试动作里添加了 ”重启“ 和 ”停止“ 按钮,不用每次修改完代码后切回终端去重启了。

但 VS Code 的强大远不止如此,通过 launch.json 可以配置详细的调试功能。

launch.json

上图可以看出,”调试“ 按钮右边有一个下拉菜单,默认是 ”没有配置“。单击右侧的齿轮状图标,会在项目根目录下创建 .vscode 文件夹及 launch.json 文件。launch.json 的内容如下:

img

这个默认配置的意思是执行:

$ node ${workspaceFolder}/app.js

launch.json 其实就是存储了一些调试相关的配置,VS Code 在启动调试时,会读取 launch.json 决定以何种方式调试。launch.json 有以下常用选项:

必需字段如下:

  • type:调试器类型。这里是 node(内置的调试器),如果安装了 Go 和 PHP 的扩展后,则对应的 type 分别为 go 和 php。
  • request:请求的类型,支持 launch 和 attach。launch 就是以 debug 模式启动调试,attach 就是附加到已经启动的进程开启 debug 模式并调试,跟在上一小节中提到的用 node -e "process._debugProcess(PID)" 作用一样。
  • name:下拉菜单显示的名字。

可选字段(括号里表示适用的类型)如下:

  • program:可执行文件或者调试器要运行的文件 (launch)。
  • args:要传递给调试程序的参数 (launch)。
  • env:环境变量 (launch)。
  • cwd:当前执行目录 (launch)。
  • address:IP 地址 (launch & attach)。
  • port:端口号 (launch & attach)。
  • skipFiles:想要忽略的文件,数组类型 (launch & attach)。
  • processId:进程 PID (attach)。

变量替换:

  • ${workspaceFolder}:当前打开工程的路径。
  • ${file}:当前打开文件的路径。
  • ${fileBasename}:当前打开文件的名字,包含后缀名。
  • ${fileDirname}:当前打开文件所在的文件夹的路径。
  • ${fileExtname}:当前打开文件的后缀名。
  • ${cwd}:当前执行目录。

如果当前打开的文件是 app.js,则以下配置与默认配置是等效的:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动程序",
            "program": "${file}"
        }
    ]
}

若想了解更多的 launch.json 选项,则请查阅:

下面以 5 个实用的技巧讲解部分 launch.json 配置的作用。

技巧 1——条件断点

VS Code 可以添加条件断点,即执行到该行代码满足特定条件后程序才会中断。在断点小红点上右键选择 ”编辑断点“,可以选择以下两种条件:

  1. 表达式:当表达式计算结果为 true 时中断,例如设置:ctx.query.name === 'nswbmw',表示当访问 localhost:3000?name=nswbmw 时断点才会生效,其余请求断点无效。
  2. 命中次数:同样当表达式计算结果为 true 时中断,支持运算符 <、<=、==、>、>=、%。例如:
    1. >10:执行 10 次以后,断点才会生效。
    2. <3:只有前 2 次断点会生效。
    3. 10:等价于 >=10。
    4. %2:隔一次中断一次。

注意:可以组合表达式和命中次数条件一起使用。在切换条件类型时,需要将原来的条件清空,否则会添加两种条件。将鼠标悬浮在断点上,可以查看设置了哪些条件。

技巧 2——skipFiles

从上面图中可以看到,在 VS Code 左侧有一个 ”调用堆栈“ 面板,显示了当前断点的调用堆栈,但无法直观地看出哪些是我们项目的代码,哪些是 node_modules 里模块的代码,而且在单步调试时会进入到 node_modules 里。总之,我们不关心 node_modules 里的代码,我们只关心项目本身的代码。这时,skipFiles 就派上用场了。

skipFiles 顾名思义就是忽略我们不关心的文件。修改 launch.json 如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动程序",
            "program": "${workspaceFolder}/app.js",
            "skipFiles": [
                "${workspaceFolder}/node_modules/**/*.js",
                "<node_internals>/**/*.js"
            ]
        }
    ]
}

有以下几点需要解释:

  1. 支持 ${xxx} 这种变量替换。
  2. 支持 glob 模式匹配。
  3. 用来忽略 Node.js 核心模块。

重启调试后,如下所示:

img

可以看出:在左侧 ”调用堆栈“ 中,我们不关心的调用栈都变灰了,而且单步调试也不会进入到 skipFiles 所匹配的文件里。

技巧 3——自动重启

在每次修改代码保存后都要手动重启,否则修改后的代码和断点都不会生效。VS Code 开发者们想到了这一点,通过添加配置可以实现修改代码保存后会自动重启调试,需要结合 nodemon 一起使用。

首先,全局安装 nodemon:

$ npm i nodemon -g

然后,修改 launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动程序",
            "runtimeExecutable": "nodemon",
            "program": "${workspaceFolder}/app.js",
            "restart": true,
            "console": "integratedTerminal",
            "skipFiles": [
                "${workspaceFolder}/node_modules/**/*.js",
                "<node_internals>/**/*.js"
            ]
        }
    ]
}

当前的 launch.json 相比较上一个版本的 launch.json,多了以下几个字段:

  • runtimeExecutable:用什么命令执行 app.js,这里设置为 nodemon。
  • restart:设置为 true,修改代码并保存后会自动重启调试。
  • console:当单击停止按钮或者修改代码并保存后自动重启调试,而 nodemon 是仍然在运行的,通过设置为 console 为 integratedTerminal 可以解决这个问题。此时 VS Code 终端将会打印 nodemon 的 log,可以在终端右侧的下拉菜单中选择返回第 1 个终端,然后运行 curl localhost:3000 进行调试。

对于已经使用 nodemon 运行的程序,例如:

$ nodemon --inspect app.js

可使用 attach 模式启动调试,launch.json 如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Attach to node",
            "type": "node",
            "request": "attach",
            "restart": true,
            "processId": "${command:PickProcess}"
        }
    ]
}

运行 Attach to node 配置进行调试时,VS Code 会列出正在执行的 node 进程及对应的 PID 以供选择。也可以通过 address 和 port 参数设置 attach 到具体的进程开启调试。

技巧 4——特定操作系统设置

针对不同的操作系统,可能会用到不同的调试配置。可选的参数为:

  • windows
  • linux
  • osx

示例如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动调试",
            "program": "./node_modules/gulp/bin/gulpfile.js",
            "args": ["/path/to/app.js"],
            "windows": {
                "args": ["\\path\\to\\app.js"]
            }
        }
    ]
}

技巧 5——多配置

configurations 是个数组而不是个对象,这样设计就是为了可以添加多个调试配置。打开 launch.json,单击右下角的 ”添加配置…“,会弹出配置模板,如下所示:

img

configurations 可以用来配置不同的调试规则,比如最终将 launch.json 修改如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "attach",
            "name": "Attach to node",
            "restart": true,
            "processId": "${command:PickProcess}"
        },
        {
            "type": "node",
            "request": "launch",
            "name": "启动程序",
            "runtimeExecutable": "nodemon",
            "program": "${workspaceFolder}/app.js",
            "restart": true,
            "console": "integratedTerminal",
            "skipFiles": [
                "${workspaceFolder}/node_modules/**/*.js",
                "<node_internals>/**/*.js"
            ]
        }
    ]
}

总结

VS Code 的调试功能十分强大,本节只讲解了一些常用的调试功能,对于其余的调试功能,还请读者自行尝试。

TypeScript 调试

VS Code 内置的 Node.js 的调试器支持 JavaScript Source Map,可以结合 Source Map 调试转译前的代码,如 TypeScript,压缩混淆的 JavaScript 代码等都可以利用 Source Map 的支持调试源码。

我准备了一个简单的 TS Server Demo,可以直接 Clone 源码本地测试。下面是项目中的 src/index.ts 文件,创建了一个 HTTP Server

import * as http from "http";

let reqCount = 1;

http
  .createServer((req, res) => {
    const message = `Request Count: ${reqCount}`;

    res.writeHead(200, { "Content-Type": "text/html" });

    res.end(`<html><body>${message}</body></html>`);

    console.log("handled request: " + reqCount++);
  })
  .listen(3000);

console.log("server running on port 3000");

创建 tsconfig.json 配置,配置编译生成 Source Map

{
  "compilerOptions": {
    "outDir": "./dist",
    "sourceMap": true
  },
  "include": ["src/**/*"]
}

使用 tsc 编译一下,生成 JS 代码:dist/index.js,创建调试配置,入口文件为 dist/index.js

{
  "type": "node",
  "request": "launch",
  "name": "Launch Program",
  "program": "${workspaceFolder}/dist/index.js",
  "skipFiles": ["<node_internals>/**"]
}
复制代码

然后打断点,启动调试,浏览器访问 http://localhost:3000,即可看到调试进入了 TS 文件

img

远程调试

当我们需要在真实的服务器等远程运行环境调试 Node.js 时,我们可以利用上面提到的方式,在服务器上开启 Node.js 调试功能,并在本地连接上远程的调试端口进行调试。

VS Code 默认支持远程调试,我们需要 launch.json 配置文件中指定远程服务的 IP 地址以及端口,如下所示:

{
  "type": "node",
  "request": "attach",
  "name": "远程调试",
  "address": "IP 地址",
  "port": "9229"
}

VS Code 会自动加载远程的文件,展示为只读代码供调试使用。

如果想要在调试的过程中编辑源代码,或者更好的调试体验,可以在远程文件夹和本地项目之间设置一个映射。VS Code 提供了 localRootremoteRoot 属性来映射本地 VS Code 项目和(远程)Node.js 文件夹:

{
  "type": "node",
  "request": "attach",
  "name": "远程调试",
  "address": "IP 地址",
  "port": "9229",
  "localRoot": "${workspaceFolder}/src",
  "remoteRoot": "/var/user/"
}

在建立映射关系后,即可在本地项目进行断点调试,远程的断点信息会同步到本地项目,使用起来十分方便。

子进程调试

与普通进程调试原理一致,子进程调试时也需要传入 --inspect 参数,这一点需要特别注意,否则无法启动子进程调试。

如下通过子进程启动 Server 的例子:

// fork.js 文件
const { spawn } = require("child_process");

const sp = spawn("node", ["./fork_server.js"]);

console.log("父进程 PID", sp.pid);

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

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

如果直接使用 node --inspect 启动主进程的话,会发现只显示了主进程的调试端口,这就是因为我们在程序中启动子进程时没有传递 --inspect 选项导致的。

img

这里我们在启动进程时添加上 --inspect 参数,同时注意要指定一个默认 9229 端口之外的端口号,避免调试端口冲突

- const sp = spawn("node", ["./fork_server.js"]);
+ const sp = spawn("node", ["--inspect=9230", "./fork_server.js"]);

再次启动,就能看到两个调试信息输出了

img

当然,怎么能少得了强大的 VS Code 呢。VS Code 的 Node 调试器提供了一种机制,可以追踪所有子进程,并在调试模式下,自动链接进程。可以通过 autoAttachChildProcesses 属性开启此机制:

{
  "type": "node",
  "request": "launch",
  "name": "启动程序",
  "program": "${workspaceFolder}/fork.js",
  "autoAttachChildProcesses": true
}

启动后,即可对父进程,或子进程进行断点调试,效果如下

img

调试工具

调试不只是打断点,我们在开发过程中往往还会有其他方面的需求,比如:

  • 如何快速地切换输出的日志类型(或级别)?
  • 我想用 moment 打印出年份,是使用 moment().format('YYYY'),还是 moment().format('yyyy'),还是两种写法都可以?
  • 断言报错:AssertionError: false == true,没啥有用信息,黑人问号???

这里将介绍 3 款实用的调试工具,分别解决以上 3 种情况,来提高我们的调试效率。

debug

debug 是一个小巧却非常实用的日志模块,可以根据环境变量决定打印不同类型(或级别)的日志。代码如下:

// app.js
const normalLog = require('debug')('log')
const errorLowLog = require('debug')('error:low')
const errorNormalLog = require('debug')('error:normal')
const errorHighLog = require('debug')('error:high')

setInterval(() => {
  const value = Math.random()
  switch (true) {
    case value < 0.5: normalLog(value); break
    case value >= 0.5 && value < 0.7: errorLowLog(value); break
    case value >= 0.7 && value < 0.9: errorNormalLog(value); break
    case value >= 0.9: errorHighLog(value); break
    default: normalLog(value)
  }
}, 1000)

运行上面的代码,每一秒生成一个随机数,根据随机数的值模拟不同级别的日志输出:

  • < 0.5:正常日志。
  • 0.5~0.7:低级别的错误日志。
  • 0.7~0.9:一般级别的错误日志。
  • >= 0.9:严重级别的错误日志。

运行:

$ DEBUG=* node app.js

打印如下:

img

可以看出,debug 模块打印的日志与 console.log 相比,有以下几个特点:

  1. 不同的日志类型分配了不同的颜色加以区分,更直观。
  2. 添加了日志类型的前缀。
  3. 添加了自上一次该类型日志打印到这次日志打印经历了多长时间的后缀。

debug 模块支持以下用法:

  • DEBUG=*:打印所有类型的日志。
  • DEBUG=log:只打印 log 类型的日志。
  • DEBUG=error:*:打印所有以 error: 开头的日志。
  • DEBUG=error:*,-error:low:打印所有以 error: 开头的并且过滤掉 error:low 类型的日志。

下面演示一下第 4 种的用法,运行:

$ DEBUG=error:*,-error:low node app.js

打印如下:

img

repl2

我们在写代码时,有时可能记不清某个模块的某个方法的具体用法,比如:用 moment 格式化年份是用 moment().format('YYYY') 还是用 moment().format('yyyy') 还是两种写法都可以?lodash 的 _.pick 方法能否能接收数组作为参数?这个时候相对于翻阅官方文档,在 REPL 里试一下可能会更快,通常步骤是:

$ npm i moment
$ node
> const moment = require('moment')
> moment().format('YYYY')
'2017'
> moment().format('yyyy')
'yyyy'

一次还好,次数多了也略微烦琐。repl2 模块便是为了解决这个问题而生的。

repl2 顾名思义是 REPL 的增强版,repl2 会根据一个用户配置(~/.noderc),预先加载模块到 REPL 中,省下了我们手动在 REPL 中 require 模块的过程。

全局安装 repl2:

$ npm i repl2 -g

使用方式很简单:

  1. 将常用的模块全局安装,例如:
$ npm i lodash validator moment -g
  1. 添加配置到 ~/.noderc:
{
  "lodash": "__",
  "moment": "moment",
  "validator": "validator"
}
  1. 运行 noder:
$ noder
__ = lodash@4.17.4 -> local
moment = moment@2.18.1 -> global
validator = validator@7.0.0 -> global
> moment().format('YYYY')
'2017'
> __.random(0, 5)
3
> validator.isEmail('foo@bar.com')
true

需要讲解以下几点:

  1. ~/.noderc 是一个 JSON 文件,key 是模块的名字,value 是 require 这个模块后加载到 REPL 中的变量名。这里给 lodash 命名的变量名是 _ 而不是 ,是因为 REPL 中 _ 有特殊含义,表示上一个表达式的结果。
  2. repl2 会优先加载当前目录下的模块,没有找到然后再去加载全局安装的模块。上面结果显示 lodash 是从本地目录加载的,因为 test 目录下已经安装了 lodash,其余的模块没有从本地目录找到则尝试从全局 npm 目录加载。如果都没有找到,则不会加载。

power-assert

我们常用的断言库有:

但这类断言库都有一些通病:

  1. 过分追求语义化,API 复杂。
  2. 错误信息不足。

先看一段代码:

// test.js
const assert = require('assert')
const should = require('should')
const expect = require('expect.js')

const tom = { id: 1, age: 18 }
const bob = { id: 2, age: 20 }

describe('app.js', () => {
  it('assert', () => {
    assert(tom.age > bob.age)
  })
  it('should.js', () => {
    tom.age.should.be.above(bob.age)
  })
  it('expect.js', () => {
    expect(tom.age).be.above(bob.age)
  })
})

运行:

$ mocha

结果如下:

app.js
  1) assert
  2) should.js
  3) expect.js


0 passing (13ms)
3 failing

1) app.js
     assert:

    AssertionError [ERR_ASSERTION]: false == true
    + expected - actual

    -false
    +true

    at Context.it (test.js:10:5)

2) app.js
     should.js:
   AssertionError: expected 18 to be above 20
    at Assertion.fail (node_modules/should/cjs/should.js:275:17)
    at Assertion.value (node_modules/should/cjs/should.js:356:19)
    at Context.it (test.js:13:23)

3) app.js
     expect.js:
   Error: expected 18 to be above 20
    at Assertion.assert (node_modules/expect.js/index.js:96:13)
    at Assertion.greaterThan.Assertion.above (node_modules/expect.js/index.js:297:10)
    at Function.above (node_modules/expect.js/index.js:499:17)
    at Context.it (test.js:16:24)

可以看出,基本没有有用的信息。这时,power-assert 粉墨登场。

power-assert 使用起来很简单,理论上只用一个 assert 就可以了,而且可以无缝迁移。

注意:在使用 intelli-espower-loader 时,要求必须将测试文件放到 test/ 目录下,所以我们在 test 目录下创建 test/app.js,将原来的 test.js 代码粘贴过去。

安装 power-assert 和 intelli-espower-loader,然后运行测试:

$ npm i power-assert intelli-espower-loader --save-dev
$ mocha -r intelli-espower-loader

结果如下:

app.js
  1) assert
  2) should.js
  3) expect.js


0 passing (42ms)
3 failing

1) app.js
     assert:

    AssertionError [ERR_ASSERTION]:   # test/app.js:10

assert(tom.age > bob.age)
       |   |   | |   |
       |   |   | |   20
       |   |   | Object{id:2,age:20}
       |   18  false
       Object{id:1,age:18}

    + expected - actual

    -false
    +true
    ...

错误信息非常直观,有以下两点需要说明:

  1. mocha 需要引入 intelli-espower-loader,主要是转译代码,转译之后 require('assert') 都不需要改。
  2. intelli-espower-loader 可选择地在 package.json 中添加 directories.test 配置,例如:
"directories": {
  "test": "mytest/"
}

如果没有 directories.test 配置,则默认是 test/

# Android 调试

USB 有线调试

无线调试

工欲善其事,必先利其器

背景

Visual Studio Code (以下简称 VSCode) 是一个轻量且强大的跨平台开源代码编辑器。VSCode 采用了 Electron 技术,使用的代码编辑器名为 Monaco,Monaco 也是 Visual Studio Team Service(Visual Studio Online)使用的代码编辑器,在开发语言上,VSCode 使用了自家的 TypeScript 语言进行开发。

VSCode提供了强大的插件拓展机制,并提供 插件市场 供开发者发布、下载插件。VSCode 提供了丰富的扩展能力模型,例如基础的语法高亮、API 提示、引用跳转(转到定义)、文件搜索、主题定制、高级的 Debug 协议等等,但不允许插件直接访问底层 UI DOM (即很难定制 VSCode 外观)。

阅读全文 »

Todo List

  • 戒指

  • 视频

    出现人物

    • 家人:爸爸妈妈、弟弟
    • 妹妹:黎梦圆
    • 闺蜜:芝芝
    • 老家好友:叶子、豹子、虎子、小爷阿文
    • 大学好友:蛙牛、马狗【马狗未发】
    • 北京好友 1:201 全体
    • 北京好友 2:国粹交流群全体
  • 场地:西二旗 北青 轰趴馆

  • 布置道具:PDD 月亮代表我的心套餐

  • 台词:

  • 独白BGM:赵海洋 ——《夜色钢琴曲》人生的旅途

  • 求婚音乐:齐晨——《咱们结婚吧》

  • 照片:50 张

  • 生日蛋糕:

  • 鲜花:

  • 服装:

转载自掘金,原文链接 点这里查看


前言

Coding 应当是一生的事业,而不仅仅是 30 岁的青春饭
本文已收录 GitHub https://github.com/ponkans/F2E,欢迎 Star,持续更新

每篇文章都希望你能收获到东西,这篇将带你深入 HTTPS 加解密原理,希望看完能够有这些收获:

  • 明白 HTTPS 到底解决了什么问题
  • 理解对称加密与非对称加密的原理和使用场景
  • 明白 CA 机构和根证书到底起了什么作用
阅读全文 »