征求意见:新的 JS API

2021 年 8 月 5 日发布,作者:Natalie Weizenbaum

我很高兴正式公布一项已经酝酿了相当长一段时间的成果:一个(针对)全新的 Sass JavaScript API。这个 API 从头开始重新设计,借鉴了 Node Sass API 和多年来其他语言中各种历史 Sass API 的经验教训,并解决了现有  API 的许多缺陷。

API 有四个主要组成部分,我将在本文中介绍所有部分。

在继续阅读时,请记住,此 API 仍然只是一个提案。我们希望听取您,我们的用户,关于它是否满足您的需求以及我们在将其锁定到完整发布之前如何改进它。因此,请随时在 问题跟踪器 上发表您的意见!

为什么需要新的 API为什么需要新的 API 永久链接

现有的 JavaScript API 已经开始显露出它的不足。它早于 Dart Sass,最初是为 node-sass 包设计的,该包封装了现在已弃用LibSass 实现。(这就是我们将其称为“Node Sass API”的原因!)它随着 LibSass 有机地、通常是杂乱无章地增长,最终导致了一些笨拙的遗留行为。这些行为中,许多对实现来说更像是一种负担,但其中一些确实让生活变得很 困难。

  • 导入器 API 是围绕文件路径而不是 URL 构建的,并且与物理文件系统紧密耦合。这使得无法覆盖所有基于文件的文件加载并呈现一个完全虚拟的文件系统,并导致自定义 Node 导入器与新的 模块 系统 互动不良。

  • 函数 API 是围绕可变值对象构建的,这与 Sass 的不可变性背道而驰。它也没有提供任何实用方法(例如在映射中查找键)来更轻松地实现惯用的自定义函数,并且没有提供对关键值信息(例如字符串是否被 引用)的访问。

  • 所有异步函数都是基于回调的,而不是基于 promise 的。

新的 API 解决了这些问题以及更多问题,并提供了一个现代化的、惯用的 API,这将使从 JS 中使用 Sass 变得轻而易举。

编译编译永久链接

API 的核心是四个执行实际 Sass 编译的函数,其中两个是同步函数,两个是异步函数。它们在这里以 TypeScript 语法呈现,以明确说明它们接受和返回的内容,但您可以随时从普通的 JS 中调用它们。

function compile(
  path: string,
  options?: Options<'sync'>
): CompileResult;

function compileString(
  source: string,
  options?: StringOptions<'sync'>
): CompileResult;

function compileAsync(
  path: string,
  options?: Options<'async'>
): Promise<CompileResult>;

function compileStringAsync(
  source: string,
  options?: StringOptions<'async'>
): Promise<CompileResult>;

compile()compileAsync() 函数从磁盘上的路径加载 Sass 文件,而 compileString()compileStringAsync() 编译作为字符串传入的 Sass 源代码。所有这些都接受以下 选项。

  • alertAscii:错误和警告是否应该只使用 ASCII 字符(而不是,例如,Unicode 框绘制 字符)。
  • alertColor:错误和警告是否应该使用终端 颜色。
  • loadPaths:要用于查找要加载的文件的文件路径列表,就像旧版 API 中的 includePaths 一样。
  • importers:用于加载 Sass 源 文件的 自定义导入器 列表。
  • functions:一个对象,其键是 Sass 函数签名,其值是 自定义 函数
  • quietDeps:是否使依赖项中的弃用警告静音。
  • logger:要用于发出警告和调试 消息的 自定义日志记录器
  • sourceMap:是否在 编译期间生成源映射。
  • style:输出样式,'compressed''expanded'
  • verbose:是否发出遇到的所有弃用警告 

compileString()compileStringAsync() 函数接受一些额外的 选项。

  • syntax:文件的语法,'scss'(默认值)、'indented''css'
  • url:文件的 规范 URL
  • importer:将用作文件源的 自定义导入器。如果传递了此参数,则此导入器将用于解析来自此 样式表的相对加载。

所有这些函数都返回一个包含以下 字段的对象。

  • css:已编译的 CSS,作为 字符串。
  • loadedUrls:编译期间加载的所有 URL,没有特定 顺序。
  • sourceMap:如果传递了 sourceMap: true,则为文件的源映射,作为已解码 对象。

与 Node Sass API 一样,同步函数将比它们的异步对应函数快得多。不幸的是,新的 API 不支持用于加速异步编译的 fibers 选项,因为 fibers 包已 弃用

日志记录器日志记录器永久链接

日志记录器 API 使您可以更精细地控制如何以及何时发出警告和调试消息。与本提案的其他方面不同,logger 选项也将添加到 API 中,以允许您控制那里的消息,而无需立即升级到新的 API 

日志记录器实现了以下 接口。

interface Logger {
  warn?(
    message: string,
    options: {
      deprecation: boolean;
      span?: SourceSpan;
      stack?: string;
    }
  ): void;

  debug?(
    message: string,
    options: {span: SourceSpan}
  ): void;
}

warn 函数处理警告,包括来自编译器本身的警告和来自 @warn 规则的警告。它 传递。

  • 警告消息
  • 一个标志,指示它是否是专门的弃用 警告。
  • 一个跨度,指示警告的发生位置,如果它来自特定 位置。
  • 遇到警告时 Sass 堆栈跟踪,如果它是在 执行过程中遇到的。

debug 函数仅处理 @debug 规则,并且只传递消息和规则的跨度。有关 SourceSpan 类型的更多信息,请参阅 日志记录器 提案

Sass 还将提供一个内置日志记录器 Logger.silent,它永远不会发出任何消息。这将使您可以轻松地在不发出任何警告的“静默模式”下运行 Sass 

导入器导入器永久链接

新的 API 没有将导入器建模为单函数回调,而是将其建模为公开两种方法的对象:一种用于规范化 URL,另一种用于加载规范的 URL

// Importers for compileAsync() and compileStringAsync() are the same, except
// they may return Promises as well.
interface Importer {
  canonicalize(
    url: string,
    options: {fromImport: boolean}
  ): URL | null;

  load(canonicalUrl: URL): ImporterResult | null;
}

请注意,即使是通过 compile()loadPaths 直接从文件系统加载的样式表,也会被视为通过导入器加载的。这个内置的文件系统导入器会将所有路径规范化为 file: URL,并从物理 文件系统中加载这些 URL。

规范化规范化永久链接

第一步确定样式表的规范 URL。每个样式表都只有一个规范 URL,反过来又引用一个样式表。规范 URL 必须是绝对的,包括一个方案,但具体结构取决于导入器。在大多数情况下,相关样式表将存在于磁盘上,导入器将只返回一个 file: URL 用于 它。

canonicalize() 方法接受一个可能为相对或绝对的 URL 字符串。如果导入器识别出该 URL,则返回一个相应的绝对 URL(包括一个方案)。这就是相关样式表的规范 URL。尽管输入 URL 可能省略了文件扩展名或初始下划线,但规范 URL 必须是完全 解析的。

对于从文件系统加载的样式表,规范 URL 将是磁盘上物理文件的绝对 file: URL。如果它是内存中生成的,则导入器应选择一个自定义 URL 方案,以确保其规范 URL 与任何其他 导入器的 URL 不冲突。

例如,如果您从数据库中加载 Sass 文件,则可以使用方案 db:。与数据库中键 styles 关联的样式表的规范 URL 可能为 db:styles

此函数还接受一个 fromImport 选项,指示导入器是从 @import 规则(而不是 @use@forwardmeta.load-css())调用的。

为每个样式表提供一个规范 URL 允许 Sass 确保在新的模块 系统中不会多次加载相同的样式表。

规范化相对加载规范化相对加载 永久链接

当样式表尝试加载一个相对 URL 时,例如 @use "variables",从文档本身无法清楚地知道它是否指的是相对于样式表的或另一个导入器或加载路径的文件。以下是导入器 API 如何解决这种 歧义的。

  • 首先,相对 URL 相对于包含 @use(或 @forward@import)的样式表的规范 URL 进行解析。例如,如果规范 URLfile:///path/to/my/_styles.scss,则解析后的 URL 将为 file:///path/to/my/variables

  • 然后将此 URL 传递给加载旧样式表的导入器的 canonicalize() 方法。(这意味着您的导入器必须支持绝对 URL!)如果导入器识别出它,则返回规范值,然后将其传递给该导入器的 load();否则,返回 null

  • 如果旧样式表的导入程序无法识别 URL,它将按照它们在 options 中出现的顺序传递给所有 importers 的规范化函数,然后在所有 loadPaths 中进行检查。如果这些都无法识别它,则加载 失败。

本地相对路径优先于其他导入程序或加载路径非常重要,因为否则您的本地样式表可能会因依赖项添加具有冲突的 名称的文件而意外中断。

加载加载永久链接

第二步实际上是加载样式表的文本。load() 方法接受一个由 canonicalize() 返回的规范 URL,并返回该 URL 处的样式表内容。这在每次编译中仅对每个规范 URL 调用一次;对相同 URL 的未来加载将重新使用现有模块(对于 @use@forward)或解析树(对于 @import)。

load() 方法返回一个具有以下 字段的对象

  • css:已加载的 样式表的文本。
  • syntax:文件的语法:'scss''indented''css'
  • sourceMapUrl:一个可选的浏览器可访问的 URL,用于在引用此 文件时包含在源映射中。

FileImporterFileImporter 永久链接

此提案还添加了一种称为 FileImporter 的特殊类型的导入程序。此导入程序使将加载重定向到物理文件系统上的某个位置的常见情况变得更容易。它不需要调用方实现 load(),因为对于磁盘上的文件,这始终是相同的 

interface FileImporter {
  findFileUrl(
    url: string,
    options: {fromImport: boolean}
  ): FileImporterResult | null;
}

findFileUrl() 方法接受一个相对 URL,并返回一个具有以下 字段的对象

  • url:要加载的文件的绝对 file: URL。此 URL 不需要完全规范化:Sass 编译器将负责解析部分、文件扩展名、索引文件等等 
  • sourceMapUrl:一个可选的浏览器可访问的 URL,用于在引用此 文件时包含在源映射中。

函数函数永久链接

新函数 API 的函数类型与旧的 API 的函数类型非常相似

type CustomFunctionCallback = (args: Value[]) => Value;

唯一的区别 

  • 异步函数返回一个 Promise<Value>,而不是调用一个 回调。
  • 值类型本身是 不同的。

第二点相当重要!新的值类型比旧版本更加完善。让我们从父 类开始

abstract class Value {
  /**
   * Returns the values of `this` when interpreted as a list.
   *
   * - For a list, this returns its elements.
   * - For a map, this returns each of its key/value pairs as a `SassList`.
   * - For any other value, this returns a list that contains only that value.
   */
  get asList(): List<Value>;

  /** Whether `this` is a bracketed Sass list. */
  get hasBrackets(): boolean;

  /** Whether `this` is truthy (any value other than `null` or `false`). */
  get isTruthy(): boolean;

  /** Returns JS's null if this is `sassNull`, or `this` otherwise. */
  get realNull(): null | Value;

  /** If `this` is a list, return its separator. Otherwise, return `null`. */
  get separator(): ListSeparator;

  /**
   * Converts the Sass index `sassIndex` to a JS index into the array returned
   * by `asList`.
   *
   * Sass indices start counting at 1, and may be negative in order to index
   * from the end of the list.
   */
  sassIndexToListIndex(sassIndex: Value): number;

  /**
   * Returns `this` if it's a `SassBoolean`, and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertBoolean(name?: string): SassBoolean;

  /**
   * Returns `this` if it's a `SassColor`, and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertColor(name?: string): SassColor;

  /**
   * Returns `this` if it's a `SassFunction`, and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of the parameter passed to the custom function (without the `$`).
   */
  assertFunction(name?: string): SassFunction;

  /**
   * Returns `this` if it's a `SassMap` (or converts it to a `SassMap` if it's
   * an empty list), and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of the parameter passed to the custom function (without the `$`).
   */
  assertMap(name?: string): SassMap;

  /**
   * Returns `this` if it's a `SassNumber`, and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertNumber(name?: string): SassNumber;

  /**
   * Returns `this` if it's a `SassString`, and throws an error otherwise.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertString(name?: string): SassString;

  /**
   * Returns the value of `this` if it can be interpreted as a map.
   *
   * - If this is a map, returns its contents.
   * - If this is an empty list, returns an empty map.
   * - Otherwise, returns `null`.
   */
  tryMap(): OrderedMap<Value, Value> | null;

  /** Returns whether `this == other` in SassScript. */
  equals(other: Value): boolean;
}

这里需要注意几件事 

  • 由于 CSS 在单个元素和包含一个元素的列表之间没有强烈的语法区别,因此任何 Sass 值都可以被视为列表。Value 通过为每个 Value 提供 asList()hasBrackets()separator() 获取器,使其易于遵循此约定。

  • 返回的列表和 asMap() 返回的映射是来自 immutable 的不可变类型。这反映了 Sass 内置的对所有类型的不可变性。尽管这些值无法直接修改,但它们的 API 使创建具有应用 更改的新值变得容易且高效。

  • Sass 的列表索引约定不同于 JavaScript 的。sassIndexToListIndex() 函数使其易于从 Sass 索引转换为 JS 索引。

  • 在 Sass 中,任何值都可以在布尔上下文中使用,其中 falsenull 被视为“假值”。isTruthy 获取器使此约定易于 遵循。

  • assert*() 函数使其易于确保您正在传递您期望的参数,并在您未传递参数时抛出惯用的错误。它们对于 TypeScript 用户特别有用,因为它们会自动缩小 Value 的类型。

大多数 Sass 值都有自己的子类,但有三个单例值只是作为常量可用:sassTruesassFalsesassNull 分别代表 Sass 的 truefalsenull 值。

颜色颜色永久链接

APISassColor 类提供了对 RGBHSLHWB 格式的颜色访问权限。与内置的 Sass 颜色函数一样,无论颜色最初是如何创建的,都可以访问任何属性 

class SassColor extends Value {
  /** Creates an RGB color. */
  static rgb(
    red: number,
    green: number,
    blue: number,
    alpha?: number
  ): SassColor;

  /** Creates an HSL color. */
  static hsl(
    hue: number,
    saturation: number,
    lightness: number,
    alpha?: number
  ): SassColor;

  /** Creates an HWB color. */
  static hwb(
    hue: number,
    whiteness: number,
    blackness: number,
    alpha?: number
  ): SassColor;

  /** The color's red channel. */
  get red(): number;

  /** The color's green channel. */
  get green(): number;

  /** The color's blue channel. */
  get blue(): number;

  /** The color's hue. */
  get hue(): number;

  /** The color's saturation. */
  get saturation(): number;

  /** The color's lightness. */
  get lightness(): number;

  /** The color's whiteness. */
  get whiteness(): number;

  /** The color's blackeness. */
  get blackness(): number;

  /** The color's alpha channel. */
  get alpha(): number;

  /**
   * Returns a copy of `this` with the RGB channels updated to match `options`.
   */
  changeRgb(options: {
    red?: number;
    green?: number;
    blue?: number;
    alpha?: number;
  }): SassColor;

  /**
   * Returns a copy of `this` with the HSL values updated to match `options`.
   */
  changeHsl(options: {
    hue?: number;
    saturation?: number;
    lightness?: number;
    alpha?: number;
  }): SassColor;

  /**
   * Returns a copy of `this` with the HWB values updated to match `options`.
   */
  changeHwb(options: {
    hue?: number;
    whiteness?: number;
    blackness?: number;
    alpha?: number;
  }): SassColor;

  /** Returns a copy of `this` with `alpha` as its alpha channel. */
  changeAlpha(alpha: number): SassColor;
}

数字数字永久链接

SassNumber 类将它的分子和分母单位存储为数组而不是字符串。此外,它还提供了一些方法,用于断言它具有特定的单位(assertNoUnits()assertUnit())以及将它转换为特定的单位(convert()convertToMatch()convertValue()convertValueToMatch()coerce()coerceValue()coerceValueToMatch())。

Sass 的数值逻辑也与 JS 略有不同,因为 Sass 认为相差小于 10th 位数的小数的数字是相同的。此 API 提供了许多方法,有助于在该方法与 JavaScript 的数值 逻辑之间进行转换。

class SassNumber extends Value {
  /** Creates a Sass number with no units or a single numerator unit. */
  constructor(value: number, unit?: string);

  /** Creates a Sass number with multiple numerator and/or denominator units. */
  static withUnits(
    value: number,
    options?: {
      numeratorUnits?: string[] | List<string>;
      denominatorUnits?: string[] | List<string>;
    }
  ): SassNumber;

  /** This number's value. */
  get value(): number;

  /**
   * Whether `value` is an integer according to Sass's numeric logic.
   *
   * The integer value can be accessed using `asInt`.
   */
  get isInt(): boolean;

  /**
   * If `value` is an integer according to Sass's numeric logic, returns the
   * corresponding JS integer, or `null` if `value` isn't an integer.
   */
  get asInt(): number | null;

  /** This number's numerator units. */
  get numeratorUnits(): List<string>;

  /** This number's denominator units. */
  get denominatorUnits(): List<string>;

  /** Whether `this` has numerator or denominator units. */
  get hasUnits(): boolean;

  /**
   * If `value` is an integer according to Sass's numeric logic, returns the
   * corresponding JS integer, or throws an error if `value` isn't an integer.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of the parameter passed to the custom function (without the `$`).
   */
  assertInt(name?: string): number;

  /**
   * If `value` is between `min` and `max` according to Sass's numeric logic,
   * returns it clamped to that range. Otherwise, throws an error.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of the parameter passed to the custom function (without the `$`).
   */
  assertInRange(min: number, max: number, name?: string): number;

  /**
   * Returns `this` if it has no units. Otherwise, throws an error.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertNoUnits(name?: string): SassNumber;

  /**
   * Returns `this` if it has `unit` as its single (numerator) unit. Otherwise,
   * throws an error.
   *
   * The `name` parameter is used for error reporting. It should match the name
   * of a parameter passed to the custom function (without the `$`).
   */
  assertUnit(name?: stringunit: string): SassNumber;

  /** Returns whether `this` has the single numerator unit `unit`. */
  hasUnit(unit: string): boolean;

  /** Returns whether this number's units are compatible with `unit`. */
  compatibleWithUnit(unit: string): boolean;

  /**
   * If this number's units are compatible with `newNumerators` and
   * `newDenominators`, returns a new number with those units that's equal to
   * `this`. Otherwise, throws an error.
   *
   * Note that unitless numbers are only compatible with other unitless numbers.
   */
  convert(
    newNumerators: string[] | List<string>,
    newDenominators: string[] | List<string>
  ): SassNumber;

  /**
   * If this number's units are compatible with `other`'s, returns a new number
   * with `other`'s units that's equal to `this`. Otherwise, throws an error.
   *
   * Note that unitless numbers are only compatible with other unitless numbers.
   */
  convertToMatch(other: SassNumber): SassNumber;

  /** Equivalent to `convert(newNumerators, newDenominators).value`. */
  convertValue(
    newNumerators: string[] | List<string>,
    newDenominators: string[] | List<string>
  ): number;

  /** Equivalent to `convertToMatch(other).value`. */
  convertValueToMatch(other: SassNumber): number;

  /**
   * Like `convert()`, but if `this` is unitless returns a copy of it with the
   * same value and the given units.
   */
  coerce(
    newNumerators: string[] | List<string>,
    newDenominators: string[] | List<string>
  ): SassNumber;

  /**
   * Like `convertToMatch()`, but if `this` is unitless returns a copy of it
   * with the same value and `other`'s units.
   */
  coerceToMatch(other: SassNumber): SassNumber;

  /** Equivalent to `coerce(newNumerators, newDenominators).value`. */
  coerceValue(
    newNumerators: string[] | List<string>,
    newDenominators: string[] | List<string>
  ): number;

  /** Equivalent to `coerceToMatch(other).value`. */
  coerceValueToMatch(other: SassNumber): number;
}

字符串字符串永久链接

SassString 类提供了对字符串是否带引号的信息的访问。与列表一样,JS 的索引概念与 Sass 的不同,因此它还提供了 sassIndexToStringIndex() 方法,将 JS 索引转换为 Sass 索引。

class SassString extends Value {
  /** Creates a string with the given `text`. */
  constructor(
    text: string,
    options?: {
      /** @default true */
      quotes: boolean;
    }
  );

  /** Creates an empty string`. */
  static empty(options?: {
    /** @default true */
    quotes: boolean;
  }): SassString;

  /** The contents of `this`. */
  get text(): string;

  /** Whether `this` has quotes. */
  get hasQuotes(): boolean;

  /** The number of Unicode code points in `text`. */
  get sassLength(): number;

  /**
   * Converts the Sass index `sassIndex` to a JS index into `text`.
   *
   * Sass indices start counting at 1, and may be negative in order to index
   * from the end of the list. In addition, Sass indexes strings by Unicode code
   * point, while JS indexes them by UTF-16 code unit.
   */
  sassIndexToStringIndex(sassIndex: Value): number;
}

列表列表永久链接

如上所述,大多数列表函数都在 Value 超类上,以便于遵循将所有值视为列表的 Sass 约定。但是,仍然可以构造 SassList 类来创建新的 列表

class SassList extends Value {
  /** Creates a Sass list with the given `contents`. */
  constructor(
    contents: Value[] | List<Value>,
    options?: {
      /** @default ',' */
      separator?: ListSeparator;
      /** @default false */
      brackets?: boolean;
    }
  );

  /** Creates an empty Sass list. */
  static empty(options?: {
    /** @default null */
    separator?: ListSeparator;
    /** @default false */
    brackets?: boolean;
  }): SassList;
}

映射映射永久链接

SassMap 类只是将它的内容公开为来自 immutableOrderedMap

class SassMap extends Value {
  /** Creates a Sass map with the given `contents`. */
  constructor(contents: OrderedMap<Value, Value>);

  /** Creates an empty Sass map. */
  static empty(): SassMap;

  /** Returns this map's contents. */
  get contents(): OrderedMap<Value, Value>;
}

函数函数永久链接

SassFunction 类相当严格:它只允许使用同步回调创建新的头等函数。这些函数不能被自定义函数调用,但它们仍然比旧的 API 提供更强大的功能!

class SassFunction extends Value {
  /**
   * Creates a Sass function value with the given `signature` that calls
   * `callback` when it's invoked.
   */
  constructor(
    signature: string,
    callback: CustomFunctionCallback
  );
}

更多信息更多信息永久链接

如果您想了解更多有关这些提案的信息,并查看它们的最新形式,那么可以在 GitHub 上完整地查看它们 

我们渴望得到反馈,所以请 告诉我们您的想法!这些提案将在本文发布后至少开放一个月,并可能根据围绕它们的讨论的活跃程度而延长