【TS 演化史 -- 14】拼写校正和动态导入表达式

前端小智
作者:Marius Schulz
译者:前端小智
来源:https://mariusschulz.com/
点赞再看,养成习惯

本文 GitHub https://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

TypeScript 2.4 为标识符实现了拼写纠正机制。即使咱们稍微拼错了一个变量、属性或函数名,TypeScript 在很多情况下都可以提示正确的拼写。

拼写更正

假设咱们想要调用window.location.reload()来重新加载当前页面。但不小心把location写成了locatoin或其他一些拼写错误,TypeScript 会提示正确的拼写并提供快速修复。

clipboard.png

此更正机制对于通常拼写错误的名称特别有用。 以单词"referrer"为例,对于
document.referrer 咱们有时候会写成如下的错误拼写:

  • document.referrerer
  • document.referrawr
  • document.refferrrr

TypeScript 将识别所有这些拼写错误,并提示document.referrer为正确的拼写。 它甚至还可以提供以下几种选择:

  • document.referrerer
  • document.referrawr
  • document.refferrrr

当然,如果咱们只是输入document.ref,然后按TABENTER键让TypeScript为咱们补全,则不需要拼写建议,但是如果自己快速输入整个属性名称,则可能会拼错。

编辑距离 (Levenshtein Distance算法)

在内部,TypeScript 计算拼写错误的名称和程序中该位置可用的名称列表中每个候选项之间的编辑距离。最佳匹配后(如果有的话)将作为拼写提示返回。

该算法在 TypeScript 编译器的checker.ts文件中的getSpellingSuggestionForName函数中实现,如下所示

/**
 * Given a name and a list of symbols whose names are *not* equal to the name, return a spelling suggestion if there is one that is close enough.
 * Names less than length 3 only check for case-insensitive equality, not levenshtein distance.
 *
 * If there is a candidate that's the same except for case, return that.
 * If there is a candidate that's within one edit of the name, return that.
 * Otherwise, return the candidate with the smallest Levenshtein distance,
 *    except for candidates:
 *      * With no name
 *      * Whose meaning doesn't match the `meaning` parameter.
 *      * Whose length differs from the target name by more than 0.34 of the length of the name.
 *      * Whose levenshtein distance is more than 0.4 of the length of the name
 *        (0.4 allows 1 substitution/transposition for every 5 characters,
 *         and 1 insertion/deletion at 3 characters)
 */
function getSpellingSuggestionForName(name: string, symbols: Symbol[], meaning: SymbolFlags): Symbol | undefined {
    const maximumLengthDifference = Math.min(2, Math.floor(name.length * 0.34));
    let bestDistance = Math.floor(name.length * 0.4) + 1; // If the best result isn't better than this, don't bother.
    let bestCandidate: Symbol | undefined;
    let justCheckExactMatches = false;
    const nameLowerCase = name.toLowerCase();
    for (const candidate of symbols) {
        const candidateName = symbolName(candidate);
        if (!(candidate.flags & meaning && Math.abs(candidateName.length - nameLowerCase.length) <= maximumLengthDifference)) {
            continue;
        }
        const candidateNameLowerCase = candidateName.toLowerCase();
        if (candidateNameLowerCase === nameLowerCase) {
            return candidate;
        }
        if (justCheckExactMatches) {
            continue;
        }
        if (candidateName.length < 3) {
            // Don't bother, user would have noticed a 2-character name having an extra character
            continue;
        }
        // Only care about a result better than the best so far.
        const distance = levenshteinWithMax(nameLowerCase, candidateNameLowerCase, bestDistance - 1);
        if (distance === undefined) {
            continue;
        }
        if (distance < 3) {
            justCheckExactMatches = true;
            bestCandidate = candidate;
        }
        else {
            Debug.assert(distance < bestDistance); // Else `levenshteinWithMax` should return undefined
            bestDistance = distance;
            bestCandidate = candidate;
        }
    }
    return bestCandidate;
}

getSpellingSuggestionForName使用大量试探法来产生合理的拼写建议,该建议既不太严格也不太宽松,这是一个有趣的平衡点!

编辑距离 (Levenshtein Distance算法)
字符串的编辑距离,又称为Levenshtein距离,由俄罗斯的数学家Vladimir Levenshtein在1965年提出。是指利用字符操作,把字符串A转换成字符串B所需要的最少操作数。其中,字符操作包括:

  • 删除一个字符
  • 插入一个字符
  • 修改一个字符

例如对于字符串"if"和"iff",可以通过插入一个'f'或者删除一个'f'来达到目的。

一般来说,两个字符串的编辑距离越小,则它们越相似。如果两个字符串相等,则它们的编辑距离(为了方便,本文后续出现的“距离”,如果没有特别说明,则默认为“编辑距离”)为0(不需要任何操作)。不难分析出,两个字符串的编辑距离肯定不超过它们的最大长度(可以通过先把短串的每一位都修改成长串对应位置的字符,然后插入长串中的剩下字符)。

动态导入表达式

TypeScript 2.4 添加了对动态import()表达式的支持,允许用户在程序的任何位置异步地请求某个模块。

静态导入模块

咱们先从静态导入模块开始,然后看看咱们需要动态导入的情况。假设咱们已经为一些客户端小部件编写了一个widget.ts模块:

import * as $ from "jquery";

export function render(container: HTMLElement) {
  $(container).text("Hello, World!");
}

咱们的小部件需要 jQuery,因此从jquery npm包中导入$。 请注意,咱们在第1行中使用的是完全静态的导入声明,而不是动态的import()表达式。

现在,咱们切换到main.ts模块,并假设咱们要将小部件呈现到特定的<div>容器中。 如果打到 DOM 刚渲染,否则不渲染。

import * as widget from "./widget";

function renderWidget() {
  const container = document.getElementById("widget");
  if (container !== null) {
    widget.render(container);
  }
}

renderWidget();

如果现在使用webpackRollup之类的工具将main.ts作为输入模块捆绑应用程序,则生成的JS 捆绑包(处于未缩小状态)的长度超过10,000行。 这是因为在widget.ts模块中,需要要导入很大的jquery npm 包。

问题在于,即使不渲染该窗口小部件,咱们也要导入其窗口小部件及其所有依赖项。 新用户第一次打开咱们的Web应用程序时,其浏览器必须下载并解析大量无效代码。 这对于具有不稳定网络连接,低带宽和有限处理能力的移动设备尤其不利。

接着来看看动态的 import() 如何解决这个问题。

动态导入模块

更好的方法是仅在需要时导入小部件模块。但是,ES6 导入声明是完全静态的,必须位于文件的顶层,这意味着咱们不能将它们嵌套在if语句中,以便有条件地导入模块。这就是动态import()出现的原因。

main.ts模块中,删除文件顶部的import声明,并使用import()表达式动态加载小部件,但前提是咱们确实找到了小部件容器:

function renderWidget() {
  const container = document.getElementById("widget");
  if (container !== null) {
    import("./widget").then(widget => {
      widget.render(container);
    });
  }
}

renderWidget();

由于按需获取 ECMAScript 模块是异步操作,因此import()表达式始终返回promise

将 await 运算符与 import() 一起使用

进行一些重构,以使renderWidget函数的嵌套更少,从而更易于阅读。 因为import()返回一个普通的ES2015 Promise(具有.then()方法),所以咱们可以使用await运算符来等待Promise解析:

async function renderWidget() {
  const container = document.getElementById("widget");
  if (container !== null) {
    const widget = await import("./widget");
    widget.render(container);
  }
}

renderWidget();

清晰明了,不要忘记通过在其声明中添加async关键字来使renderWidget函数异步化。

针对各种模块系统

TypeScript 编译器支持各种 JS 模块系统,例如 ES2015,CommonJS 或 AMD。 根据目标模块系统的不同,为 import() 表达式生成的 JS 代码将大不相同。

如果咱们使用--module esnext编译咱们的 TypeScript 应用程序,将生成以下 JS 代码。 它几乎与我们自己编写的代码相同:

"use strict";
function renderWidget() {
  var container = document.getElementById("widget");
  if (container !== null) {
    var widget = import("./widget").then(function(widget) {
      widget.render(container);
    });
  }
}
renderWidget();

注意import()表达式没有以任何方式进行转换。如果咱们在这个模块中使用了任何importexport声明,那么它们也不会受到影响。

将此代码与以下代码进行比较,当咱们使用--module commonjs编译应用程序时,会生成以下代码(为了便于阅读,还有一些换行符):

"use strict";
function renderWidget() {
  var container = document.getElementById("widget");
  if (container !== null) {
    var widget = Promise.resolve()
      .then(function() {
        return require("./widget");
      })
      .then(function(widget) {
        widget.render(container);
      });
  }
}
renderWidget();

CommonJS 对于 Node 应用程序将是一个不错的选择。 所有import()表达式都将转换为require()调用,这些调用可以在程序中的任意位置有条件地执行,而不必事先加载,解析和执行模块。

那么,在使用import()按需延迟加载模块的客户端web应用程序中,应该针对哪个模块系统呢?我建议将——module esnext与 webpack 的代码分割特性结合使用。检查带有import()和webpack的TypeScript 应用程序的代码分解,以进行演示应用程序设置。


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:
https://mariusschulz.com/blog...
https://mariusschulz.com/blog...
https://www.tslang.cn/docs/re...


交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

阅读 1.3k

终身学习者
我要先坚持分享20年,大家来一起见证吧。
62.9k 声望
94.8k 粉丝
0 条评论
62.9k 声望
94.8k 粉丝
文章目录
宣传栏