普通视图

发现新文章,点击刷新页面。
昨天以前CRIMX Blog

如何在 Sass 中方便引用祖先选择器

2020年7月14日 21:41

双亲选择器

在 Sass 中双亲选择器(Parent Selector 有时也叫父选择器) & 是一个十分有用且常用的选择器,它可以复用外部选择器的名字,从而更轻松地实现多重样式编写。

.btn {
  background: transparent;

  &:hover {
    background: grey;
  }
}

会输出

.btn {
  background: transparent;
}
.btn:hover {
  background: grey;
}

祖先选择器

有时候我们遇到这样一种模式,如主题样式,在元素根处可能有 .dark-theme 来说明目前处于黑暗模式;或者使用 Modernizr 检测浏览器特性,在根元素会根据环境添加相应 class 表示特性支持情况。这时候我们写样式可能就需要拆分开来写。

.btn {
  background: transparent;

  &:hover {
    background: grey;
  }
}

.dark-theme .btn {  background: linear-gradient(cornflowerblue, rebeccapurple);}

这里有两点不太舒服的地方:

  1. 处理同个逻辑的样式需要拆开写。
  2. 与祖先选择器名耦合,不方便修改。

我们来看看如何解决这两个痛点。

@at-root

在 Sass 中有一个 at 规则叫 @at-root,它可以跳出当前嵌套直接在文档根输出内容。

.btn {
  background: transparent;

  &:hover {
    background: grey;
  }
  
  @at-root .dark-theme & {    background: linear-gradient(cornflowerblue, rebeccapurple);  }}

会输出

.btn {
  background: transparent;
}
.btn:hover {
  background: grey;
}
.dark-theme .btn {
  background: linear-gradient(cornflowerblue, rebeccapurple);
}

但这里依然没有解决祖先选择器名耦合的问题,于是我们进一步抽象。

Mixin

将以上用法封装为 mixin 即可达到复用。

@mixin dark-theme {
  @at-root .dark-theme & {
    @content;
  }
}

.btn {
  background: transparent;

  &:hover {
    background: grey;
  }
  
  @include dark-theme {
    background-image: linear-gradient(cornflowerblue, rebeccapurple);
  }
}

支持修饰符

一些过渡库,如 Vue transition 和 React Transition Group 会设置一系列的类型名,如 .fade-enter.fade-exit,在 Sass 中我们可以直接拼接 &-enter 进行复用,现在让我们的 mixin 也支持:

@mixin dark-theme($modifiers...) {
  @if length($modifiers) > 0 {
    @each $modifier in $modifiers {
      @at-root .dark-theme &#{$modifier} {
        @content;
      }
    }
  } @else {
    @at-root .dark-theme & {
      @content;
    }
  }
}

.btn {
  background: transparent;

  &:hover {
    background: grey;
  }
  
  @include dark-theme {
    background: linear-gradient(cornflowerblue, rebeccapurple);
  }
  
  @include dark-theme(-enter) {
    background: cornflowerblue;
  }
  
  @include dark-theme(-enter-active, -exit) {
    background: rebeccapurple;
  }
}

输出

.btn {
  background: transparent;
}
.btn:hover {
  background: grey;
}
.dark-theme .btn {
  background: linear-gradient(cornflowerblue, rebeccapurple);
}

.dark-theme .btn-enter {
  background: cornflowerblue;
}

.dark-theme .btn-enter-active {
  background: rebeccapurple;
}

.dark-theme .btn-exit {
  background: rebeccapurple;
}

可以看到 @include dark-theme(-enter-active, -exit) 在多个修饰符的情况下生成了单独的重复内容。

要去掉重复我们可以直接拼接选择器。

@mixin dark-theme($modifiers...) {
  @if length($modifiers) > 0 {
    $selectors: ();
    @each $modifier in $modifiers {
      $selectors: append(
        $selectors,
        #{".dard-theme "}#{&}#{$modifier},
        comma
      );
    }
    @at-root #{$selectors} {
      @content;
    }
  } @else {
    @at-root .dark-theme & {
      @content;
    }
  }
}

输出

.btn {
  background: transparent;
}
.btn:hover {
  background: grey;
}
.dark-theme .btn {
  background: linear-gradient(cornflowerblue, rebeccapurple);
}

.dard-theme .btn-enter {
  background: cornflowerblue;
}

.dard-theme .btn-enter-active, .dard-theme .btn-exit {  background: rebeccapurple;}

抽象出通用 Mixin

最后我们将这个模式再进一步抽象出来,成为通用的 at-root mixin。

@mixin at-root($ancestor, $modifiers...) {
  @if length($modifiers) > 0 {
    $selectors: ();
    @each $modifier in $modifiers {
      $selectors: append(
        $selectors,
        #{$ancestor}#{" "}#{&}#{$modifier},
        comma
      );
    }
    @at-root #{$selectors} {
      @content;
    }
  } @else {
    @at-root #{$ancestor} & {
      @content;
    }
  }
}

现在 dark-theme 可以这么定义。

@mixin dark-theme($modifiers...) {
  @include at-root('.dark-mode', $modifiers...) {
    @content;
  }
}

最后

通过封装 mixin 我们可以方便地在规则内部直接引用祖先选择器定义规则,同时摆脱与祖先类型名的耦合,使到代码可以灵活应对变更。

自定义 Webpack Target

2020年3月30日 01:04

问题

由于浏览器扩展有特殊的权限限制,许多前端的开发工具都无法直接派上用场,如之前我解决了热更新分块自动填写到清单的问题。现在我们继续突破下个影响性能的问题:动态加载分块。

Webpack 支持 import() 自动分块并异步加载,这对于大型应用来说是非常有用的功能。虽然浏览器扩展的源文件都在本地,但对于大型应用来说静态加载依然会浪费了不少内存。那么为什么浏览器扩展不支持异步加载呢?这就需要理解 Webpack 是怎么处理的。

(如果只关心如何在浏览器扩展中使用,本文的内容已封装为 webpack-target-webextension 库。)

JSONP

当我们指定(或默认) Webpack Target 为 web 的时候,Webpack runtime 会以 JSONP 方式来加载异步块。那么什么是 JSONP?

JSONP 常用于跨域动态获取数据。如 a.comb.com 请求数据,

  • 首先生成一个回调函数名,如 myCallback
  • 创建全局函数 myCallback 实现加载数据的逻辑;
  • myCallback 作为参数构造请求链接,如 https://b.com/data?callback=myCallback
  • 通过支持跨域的 <script> 标签发起请求,<script src="https://b.com/data?callback=myCallback"></script>
  • 服务器将数据包裹到回调中返回, myCallback(...)
  • 浏览器加载脚本,myCallback 的逻辑被执行。

沙箱

这种方式为什么在浏览器扩展中会失效呢?我们都知道一些浏览器扩展可以对用户的网页进行修改,如美化或者去广告。这些修改是通过一种叫 content script 类型的脚本实现。每个 content script 可以在作者指定的时机被植入到页面上。虽然 content script 可以修改 DOM,但是 content script 本身是运行在隔离的沙箱环境中的。这个环境可以让 content script 访问部分浏览器扩展 API。

所以当 Webpack 以 JSONP 方式加载异步块的时候,<script> 中的回调会在用户的脚本环境中执行,而扩展环境中的接收回调只能默默等待到超时。

不如来真的

主流浏览器早早就支持了原生的 import() ,那么有没有可能,我们不让 Webpack 生成 JSONP 而直接使用原生的 import()? CRIMX 说 yes!

在 Webpack 中,模块加载的逻辑通过 target 设置来调整。Webpack 4 中预设了几种常见的 target:

Option Description
async-node 用于类 Node.js 环境
electron-main 用于 Electron 主进程
electron-renderer 用于 Electron 渲染进程
electron-preload 用于 Electron 渲染进程
node 用于类 Node.js 环境
node-webkit 用于 NWebKit 环境
web 用于类浏览器环境
webworker 用于 WebWorker

很可惜这几种都不支持原生 import(),也不适用浏览器扩展。在 Webpack 5 的预览中明确提到了对 es2015 的支持,同时提供了新的 module 设置。但是离 Webpack 5 正式发布以及生态跟上可能还有一段时间。

最后 target 还支持传入函数以自行实现逻辑。尽管 Webpack 的源码不是很好读,最后还是决定挑战一下,自定义实现一个针对浏览器扩展的 target

其实很简单

首先通过文档找到判断上面预设环境的位置。通过参考 web 的配置可以找到 JSONP 的实现在 JsonpMainTemplatePlugin.js 中。

其中异步块的加载分了三种方式,正常的,预加载的以及预读取的,对应 <script><link>preloadprefetch。全部改成 import() 即可。

其中注意计算块的路径,由于在 content script 中相对路径会根据当前页面计算,而我们需要根据扩展根来算路径。所以函数 jsonpScriptSrc 改为

if (needChunkOnDemandLoadingCode(chunk)) {
  extraCode.push(
    '',
    '// script path function',
    'function webextScriptSrc(chunkId) {',
    Template.indent([
      `var publicPath = ${mainTemplate.requireFn}.p`,
      `var scriptSrcPath = publicPath + ${getScriptSrcPath(
        hash,
        chunk,
        'chunkId'
      )};`,
      `if (!publicPath || !publicPath.includes('://')) {
        return (typeof chrome === 'undefined' ? browser : chrome).runtime.getURL(
          scriptSrcPath
        );
      } else {
        return scriptSrcPath;
      }`
    ]),
    '}'
  )
}

从而利用 runtime.getURL 来计算扩展资源路径。

小坑

可以通过 publicPath 来控制根路径。

注意去除 @babel/plugin-syntax-dynamic-import 等插件以免 import() 被转换掉。

Webpack 一些设置的默认值依赖 target 来判断,所以需要手动设置:

module.exports = {
  resolve: {
    mainFields: ['browser', 'module', 'main'],
    aliasFields: ['browser']
  },
  output: {
    globalObject: 'window'
  }
}

完整修改见这里

TypeScript 集合转换为交集

2020年1月16日 17:30

Object Assign

开始之前我们先来看看 libObject.assign 的类型是如何定义的

assign<T, U>(target: T, source: U): T & U;
assign<T, U, V>(target: T, source1: U, source2: V): T & U & V;
assign<T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W;
assign(target: object, ...sources: any[]): any;

意不意外,惊不惊喜。硬编码重载了三种情况,那么超过四个对象之后我们只能得到 any

这是由于 TypeScript 的局限性导致的,当然现在 TypeScript 也没有正式解决这个问题,但我们其实已经可以通过 2.8 以后引入的一些特性来 hack 掉这个问题。

Intersection From Union

type MapTopParameter<U> = U extends any ? (arg: U) => void : never
type IntersectionFromUnion<U> =
  MapTopParameter<U> extends (arg: infer T) => void ? T : never

type A = { a: 1 }
type B = { b: 2 }
type C = { c: 2 }

// $ExpectType A & B & C
type Result = IntersectionFromUnion<A | B | C>

要理解这个 hack 需要明白 TypeScript 2.8 引入的两个特性:条件类型(Conditional Types)以及条件类型推导(Type inference in conditional types)。

条件类型

条件类型可以让我们对类型进行三元运算,根据不同情况返回不同类型

T extends U ? X : Y

但与普通编程语言的三元运算不一样,TypeScript 中还有这么一个特性,叫分布式条件类型(Distributive Conditional Types)。

T 是一个集合(Union)的时候,三元运算是对集合中每个元素进行运算,而不是对 T 这个整体进行运算。可以类比为数组中的 map,对集合进行映射,这相当于往类型系统中加入了遍历功能,并且结合 never 也得到了 filter 的功能。

所以现在 TypeScript 类型系统中有了变量(泛型)、条件控制、循环控制,越来越像一门编程语言了……

利用这个特性,我们看回

type MapTopParameter<U> = U extends any ? (arg: U) => void : never

这里是将集合 U 映射为另外一个以 U 元素为参数的函数集合。

// $ExpectType ((arg: number) => void) | ((arg: 'blog.crimx.com') => void)
type Result = MapTopParameter<number | 'blog.crimx.com'>

这么做有什么用呢,我们接着看。

条件类型推导

条件类型推导其实是一种简单的模式匹配,可以类比为正则表达式。

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

这里可以理解为,我们给出了 (...args: any[]) => infer R 这个模板,然后让 T 套这个模板,收集 R 的部分,如这里是收集函数返回的类型。

再结合前面的分布式条件类型,如果 T 是一个集合,那么最后收集的则是各项返回值的集合。

// $ExpectType number | 'blog.crimx.com'
type Result = ReturnType<((arg: string) => number) | (() => 'blog.crimx.com')>

有趣的地方来了,如果我们推导的是函数的参数呢?

type Parameter<T> = T extends (arg: infer P) => any ? P : any

其实也是一样,最后我们会得到一个参数的集合。

type A = { a: number }
type B = { b: 'blog.crimx.com' }

// $ExpectType A | B
type Result = Parameter<((arg: A) => void) | ((arg: B) => void)>

但是!如果我们能想办法阻止这个分布式条件类型,让 T 集合作为一个整体去判断, 这时候表达的是 T 集合中的每一个元素都可以作为 (arg: infer P) => any 的参数使用,也就是说 P 应该是 T 中每个元素的父类,故 P 最后会得到 T 所有元素的交集(Intersection)。

怎么才能达到这个效果呢?

无封装类型参数

让一个类型成为分布式条件类型其实有一个前提,这个类型必须是无封装的类型参数(naked type parameter),即这个类型推导完成后不能是依然包在其它类型中。

所以我们简单修改一下

type Parameter<T> = [T] extends [(arg: infer P) => any] ? P : any

type A = { a: number }
type B = { b: 'saladict.app' }

// $ExpectType A & B
type Result = Parameter<((arg: A) => void) | ((arg: B) => void)>

成功得到交集了!

当然对于前面的实现我们无需这么做,因为 MapTopParameter 已经是一层封装。

type MapTopParameter<U> = U extends any ? (arg: U) => void : never
type IntersectionFromUnion<U> =
  MapTopParameter<U> extends (arg: infer T) => void ? T : never

或者写在一起(略丑)

export type IntersectionFromUnion<TUnion> = (TUnion extends any
  ? (arg: TUnion) => void
  : never) extends (arg: infer TArg) => void
    ? TArg
    : never

元组转集合

这是一个很多人不知道的小特性,将一个元组(tuple)转换为集合。

type tuple = [boolean, 'blog.crimx.com', number]

// $ExpectType number | boolean | "blog.crimx.com"
type union = tuple[number]

现代版 Object Assign

最后结合 TypeScript 3.0 加入的 rest 参数,我们定义一个现代版 Object.assign

function objectAssign<TTarget extends object, TSources extends any[]>(
  target: TTarget,
  ...sources: TSources
): IntersectionFromUnion<TTarget | TSources[number]> {
  return Object.assign(target, ...sources)
}

const a = objectAssign({ a: 1 }, { b: 2 }, { c: 3 })
// $ExpectType { a: number } & { b: number } & { c: number }
type A = typeof a

最后

通过本文例子的讲解希望能帮助大家深入了解 TypeScript 的一些高级特性,如果有什么感想或问题欢迎留言。

谢谢阅读!

修复 Deepin Wine 迅雷崩溃

2019年11月7日 19:29

/usr/share/applications/deepin.com.thunderspeed.desktop 中找到运行方式 "/opt/deepinwine/apps/Deepin-ThunderSpeed/run.sh" -u %u

运行发现错误为 wine 不知为什么没能找到加载迅雷目录下的 dlls 。

0028:fixme:msvcp:_Locinfo__Locinfo_ctor_cat_cstr (008AED28 1 C) semi-stub
0028:fixme:msvcp:_Locinfo__Locinfo_ctor_cat_cstr (008AED78 1 C) semi-stub
0028:fixme:msvcp:_Locinfo__Locinfo_ctor_cat_cstr (008AEA2C 1 C) semi-stub
0028:fixme:msvcp:_Locinfo__Locinfo_ctor_cat_cstr (008AED28 1 C) semi-stub
0028:fixme:heap:RtlSetHeapInformation 0x8c0000 0 0x8ae5c0 4 stub
0030:fixme:winsock:WSCGetProviderPath ({e70f1aa0-ab8b-11cf-8ca3-00805f48a192} 0x102f0ac 0x102f0a8 0x102f0a4) Stub!
0009:err:module:import_dll Library XLFSIO.dll (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library XLLuaRuntime.dll (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library XLGraphic.dll (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library XLUE.dll (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library DownloadKernel.dll (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library libexpat.dll (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library XLUserS.DLL (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library BaseCommunity.DLL (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library XLGraphicPlus.dll (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library zlib1.dll (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library xlstat.dll (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:import_dll Library mini_unzip_dll.dll (which is needed by L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe") not found
0009:err:module:LdrInitializeThunk Importing dlls for L"c:\\Program Files\\Thunder Network\\Thunder\\Program\\Thunder.exe" failed, status c0000135
0041:fixme:winhttp:request_set_option 0 (null) (null)

尝试将目录添加到环境变量

env WINEPREFIX="$HOME/.deepinwine/Deepin-ThunderSpeed" wine .deepinwine/Deepin-ThunderSpeed/drive_c/windows/regedit.exe 

打开注册表,定位到 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment,编辑 PATH 加入 C:\Program Files\Thunder Network\Thunder\Program,注意以分号 ; 相隔。

重新启动迅雷,问题解决。

利用 Webpack API 获取资源清单

2019年10月9日 23:31

为什么

如今几乎每个 Webpack 打包的项目都会用到 HTML Webpack Plugin。这个插件可以生成 HTML 文件带上打好的包。这在我实现一个浏览器扩展脚手架时提供了灵感。每个扩展都会有一个清单文件,里面列举了这个扩展需要加载的各种资源。

{
  "background": {
    "scripts": ["jquery.js", "my-background.js"],
  },

  "content_scripts": [
    {
      "matches": ["*://blog.crimx.com/*"],
      "js": ["common.js", "my-content.js"],
      "css": ["my-content.css"]
    }
  ],

  // ...
}

通常这些是手写上去的,但如果结合 Webpack 流程,我设想是能不能像 HTML Webpack Plugin 一样自动生成这些配置。如此便可发挥 Webpack 自动拆分块以及添加哈希的优势。

Plugin

基本的 Webpack Plugin 十分简单,在 constructor 处理配置,暴露 apply 方法实现逻辑。

class WexExtManifestPlugin {
  constructor (options) {
    this.options = options
  }

  apply (compiler) {
    
  }
}

Tapable

在 Webpack 中,API 通过 hook 勾上 Tapable 来挂载回调。不同的 Tapable 子类用于不同种类的回调。我们这里使用 Promise 处理异步回调。

class WexExtManifestPlugin {
  constructor (options) {
    this.options = options
  }

  apply (compiler) {
    compiler.hooks.done.tapPromise(      'WexExtManifestPlugin',      async ({ compilation }) => {            }    )  }
}

Compilation

Compilation 是 Webpack 最重要的 API 之一,通过 entrypoints 我们可以获得每个包的 entry 和 name ,通过 entry.getFiles() 可以获取该入口下所有文件,通过 name 可以定位到相应包名,从配置中获取其它信息。

class WexExtManifestPlugin {
  constructor (options, neutrinoOpts) {
    this.options = options
  }

  apply (compiler) {
    compiler.hooks.done.tapPromise(
      'WexExtManifestPlugin',
      async ({ compilation }) => {
        compilation.entrypoints.forEach((entry, name) => {          const files = entry            .getFiles()            .map(file => file.replace(/\.(css|js)\?.*$/, '.$1'))        })      }
    )
  }
}

完整的实现在这里。通过获取资源清单,脚手架可以利用 Webpack 实现复杂的优化;同时复用 Neutrino 的配置,扩展的资源配置统一到 Neutrino 入口中,不再需要手动维护。

❌
❌