前端及开发文章及欣赏

CSS基础知识第三篇

前端达人

盒子模型

看透网页布局的本质

首先利用CSS设置好盒子的大小,然后摆放盒子的位置。最后把网页元素比如文字图片等等,放入盒子里面。

概念

盒子模型由元素的内容边框border内边距padding和外边距margin组成
盒子里面的文字和图片等元素是内容区域,盒子的厚度我们称为盒子的边框
盒子内容与边框的距离是内边距,盒子与盒子之间的距离是外边距

盒子边框

语法

border:border-width粗细|border-style样式|border-color颜色 

边框综合设置
border: 1px solid red; 没有顺序

表格的细线边框
cellspacing=“0” ,将单元格与单元格之间的距离设置为0
border-collapse:collapse; 表示相邻边框合并在一起

内边距

padding属性用于设置内边距,是指边框与内容之间的距离

属性
padding-left左内边距padding-right右内边距padding-top上内边距padding-bottom下内边距

简写
2个值 padding: 上下内边距 左右内边距 ;
4个值 padding: 上内边距 右内边距 下内边距 左内边距 ;

内盒尺寸计算(元素实际大小)
盒子的实际的大小 = 内容的宽度和高度 + 内边距 + 边框

外边距

margin属性用于设置外边距。margin控制盒子和盒子之间的距离,属性和简写与padding相同

块级盒子水平居中

盒子必须指定宽度(width)然后就给左右的外边距都设置为auto

文字居中和盒子居中区别

盒子内的文字水平居中是text-align:center, 而且还可以让行内元素和行内块居中对齐
块级盒子水平居中 左右margin 改为 auto

清除元素的默认内外边距

代码

* {
padding:0;     /* 清除内边距 */
margin:0;      /* 清除外边距 */
} 

注意
行内元素为了兼容性, 尽量只设置左右内外边距, 不设置上下内外边距

外边距合并

相邻块元素垂直外边距的合并

当上下相邻的两个块元素相遇时,如果上面的元素有下外边距margin-bottom,下面的元素有上外边距margin-top,则他们之间的垂直间距不是margin-bottom与margin-top之和,取两个值中的较大者这种现象被称为相邻块元素垂直外边距的合并

解决方案
尽量给只给一个盒子添加margin值

嵌套块元素垂直外边距的合并

对于两个嵌套关系的块元素,如果父元素没有上内边距及边框,父元素的上外边距会与子元素的上外边距发生合并,合并后的外边距为两者中的较大者

解决方案
可以为父元素定义上边框
可以为父元素定义上内边距
可以为父元素添加overflow:hidden

盒子模型布局稳定性

按照优先使用宽度(width)内边距(padding)外边距(margin)

原因
margin有外边距合并还有ie6下面margin加倍的bug所以最后使用
padding会影响盒子大小,需要进行加减计算,其次使用
width没有问题,经常使用宽度剩余法高度剩余法来做

浮动

CSS 布局的三种机制

普通流(标准流)

块级元素会独占一行,从上向下顺序排列
行内元素会按照从左到右顺序排列,碰到父元素边缘则自动换行

浮动

让盒子从普通流中浮起来,主要作用让多个块级盒子一行显示

定位

将盒子定在浏览器的某一个位置

为什么需要浮动

因为行内块元素可以实现多个元素一行显示但中间会有空白缝隙
因为行内块元素不能实现盒子左右对齐

什么是浮动

概念

元素的浮动是指设置了浮动属性的元素
会脱离标准普通流的控制并可以移动到指定位置

作用

让多个盒子(div)水平排列成一行,使得浮动成为布局的重要手段
浮动最早是用来控制图片,实现文字环绕图片的效果
可以实现盒子的左右对齐等等

语法

选择器 { float: 属性值; } 
  • 1

属性值
none(元素不浮动(默认))left(元素左浮动)right(右浮动)

特点


加了浮动的盒子是浮起来的,漂浮在其他标准流盒子的上面

加了浮动的盒子不占位置,它原来的位置漏给了标准流的盒子

浮动元素改变display属性, 类似转换成行内块元素,但是元素之间没有空白缝隙

浮动的应用

浮动和标准流的父盒子搭配

实际的导航栏中不直接用链接a而是用li包含链接(li+a)
li+a语义更清晰
直接用a搜索引擎容易辨别为有堆砌关键字嫌疑而影响网站排名

浮动的扩展

浮动元素与父盒子的关系
子盒子的浮动参照父盒子对齐
不会与父盒子的边框重叠,也不会超过父盒子的内边距

浮动元素与兄弟盒子的关系
在一个同一个父级盒子中,如果前一个兄弟盒子是浮动的,那么当前盒子会与前一个盒子的顶部对齐
在一个同一个父级盒子中,如果前一个兄弟盒子是普通流的,那么当前盒子会显示在前一个兄弟盒子的下方

清除浮动

为什么要清除浮动

浮动元素不占用原文档流的位置,会对后面的元素排版产生影响

清除浮动本质

父级元素因为子级浮动导致内部高度为0,清除浮动后,父级会根据浮动的子盒子检测高度,父级有高度就不会影响下面的标准流

清除浮动的方法

语法

选择器{clear:属性值;}  clear 清除 
  • 1

属性值
left清除左浮动right清除右浮动both同时清除左右浮动

额外标签法

是W3C推荐的做法是通过在浮动元素末尾添加一个空的标签例如<div style=”clear:both”></div>,或则其他标签br等亦可 
  • 1

优缺点
通俗易懂,书写方便,但是添加许多无意义的标签,结构化较差

父级添加overflow属性方法

可以给父级添加:overflow为hidden|auto|scroll都可以实现

优缺点
代码简洁,但是内容增多时候容易造成不会自动换行导致内容被隐藏掉,无法显示需要溢出的元素

使用after伪元素清除浮动

:after 方式为空元素额外标签法的升级版,.clearfix:after {  content: ""; display: block; height: 0; clear: both;visibility: hidden; } 
.clearfix {*zoom: 1;}  /* IE6、7 专有 */ 
  • 1
  • 2

优缺点
符合闭合浮动思想结构语义化正确,但是由于IE6-7不支持:after,使用zoom:1触发hasLayout

使用双伪元素清除浮动

方法

.clearfix:before,.clearfix:after {
content:"";
display:table;
}
.clearfix:after {
clear:both;
}
.clearfix {
*zoom:1;
} 

优缺点
代码更简洁,但由于IE6-7不支持:after使用zoom:1触发hasLayout

ES6 的循环和可迭代对象

seo达人

本文将研究 ES6 的 for ... of 循环。

旧方法

在过去,有两种方法可以遍历 javascript。

首先是经典的 for i 循环,它使你可以遍历数组或可索引的且有 length 属性的任何对象。

for(i=0;i<things.length;i++) { var thing = things[i] /* ... */ }

其次是 for ... in 循环,用于循环一个对象的键/值对。

for(key in things) { if(!thing.hasOwnProperty(key)) { continue; } var thing = things[key] /* ... */ }

for ... in 循环通常被视作旁白,因为它循环了对象的每一个可枚举属性。这包括原型链中父对象的属性,以及被分配为方法的所以属性。换句话说,它遍历了一些人们可能想不到的东西。使用 for ... in 通常意味着循环块中有很多保护子句,以避免出现不需要的属性。

早期的 javascript 通过库解决了这个问题。许多 JavaScript库(例如:Prototype.js,jQuery,lodash 等)都有类似 each 或 foreach 这样的工具方法或函数,可让你无需 for i 或 for ... in 循环去遍历对象和数组。

for ... of 循环是 ES6 试图不用第三方库去解决其中一些问题的方式。

for … of

for ... of 循环

for(const thing of things) { /* ... */ }

它将遍历一个可迭代(iterable)对象。

可迭代对象是定义了 @@ iterator 方法的对象,而且 @@iterator 方法返回一个实现了迭代器协议的对象,或者该方法是生成器函数。

在这句话中你需要理解很多东西:

  • 可迭代的对象
  • @@iterator方法( @@是什么意思?)
  • 迭代器协议(这里的协议是什么意思?)
  • 等等,迭代(iterable)和迭代器(iterator)不是一回事?
  • 另外,生成器函数又是什么鬼?

下面逐个解决这些疑问。

内置 Iterable

首先,javascript 对象中的一些内置对象天然的可以迭代,比如最容易想到的就是数组对象。可以像下面的代码中一样在 for ... of 循环中使用数组:

const foo = [ 'apples','oranges','pears' ] for(const thing of foo) { console.log(thing)
}

输出结果是数组中的所有元素。

apples oranges
pears

还有数组的 entries 方法,它返回一个可迭代对象。这个可迭代对象在每次循环中返回键和值。例如下面的代码:

const foo = [ 'apples','oranges','pears' ] for(const thing of foo.entries()) { console.log(thing)
}

将输出以下内容

[ 0, 'apples' ]
[ 1, 'oranges' ]
[ 2, 'pears' ]

当用下面的语法时,entries 方法会更有用

const foo = [ 'apples','oranges','pears' ] for(const [key, value] of foo.entries()) { console.log(key,':',value)
}

在 for 循环中声明了两个变量:一个用于返回数组的第一项(值的键或索引),另一个用于第二项(该索引实际对应的值)。

一个普通的 javascript 对象是不可迭代的。如果你执行下面这段代码:

// 无法正常执行 const foo = { 'apples':'oranges', 'pears':'prunes' } for(const [key, value] of foo)

{ console.log(key,':',value)

}

会得到一个错误

$ node test.js
/path/to/test.js:6 for(const [key, value] of foo) {
TypeError: foo is not iterable

然而全局 Object 对象的静态 entries 方法接受一个普通对象作为参数,并返回一个可迭代对象。就像这样的程序:

const foo = { 'apples':'oranges', 'pears':'prunes' } for(const [key, value] of Object.entries(foo))

{ console.log(key,':',value)

}

能够得到你期望的输出:

$ node test.js
apples : oranges
pears : prunes

创建自己的 Iterable

如果你想创建自己的可迭代对象,则需要花费更多的时间。你会记得前面说过:

可迭代对象是定义了 @@ iterator 方法的对象,而且 @@iterator 方法返回一个实现了迭代器协议的对象,或者该方法是生成器函数。

搞懂这些内容的最简单方法就是一步一步的去创建可迭代对象。首先,我们需要一个实现 @@iterator 方法的对象。 @@ 表示法有点误导性,我们真正要做的是用预定义的 Symbol.iterator 符号定义方法。

如果用迭代器方法定义对象并尝试遍历:

const foo = {
  [Symbol.iterator]: function() {
  }
} for(const [key, value] of foo) { console.log(key, value)
}

得到一个新错误:

for(const [key, value] of foo) {
                          ^
TypeError: Result of the Symbol.iterator method is not an object

这是 javascript 告诉我们它在试图调用 Symbol.iterator 方法,但是调用的结果不是对象。

为了消除这个错误,需要用迭代器方法来返回实现了迭代器协议的对象。这意味着迭代器方法需要返回一个有 next 键的对象,而 next 键是一个函数。

const foo = {
  [Symbol.iterator]: function() { return { next: function() {
      }
    }
  }
} for(const [key, value] of foo) { console.log(key, value)
}

如果运行上面的代码,则会出现新错误。

for(const [key, value] of foo) {
                     ^
TypeError: Iterator result undefined is not an object

这次 javascript 告诉我们它试图调用 Symbol.iterator 方法,而该对象的确是一个对象,并且实现了 next 方法,但是 next 的返回值不是 javascript 预期的对象。

next 函数需要返回有特定格式的对象——有 value 和 done 这两个键。

next: function() { //... return { done: false, value: 'next value' }
}

done 键是可选的。如果值为 true(表示迭代器已完成迭代),则说明迭代已结束。

如果 done 为 false 或不存在,则需要 value 键。 value 键是通过循环此应该返回的值。

所以在代码中放入另一个程序,它带有一个简单的迭代器,该迭代器返回前十个偶数。

class First20Evens { constructor() { this.currentValue = 0 }

  [Symbol.iterator]() { return { next: (function() { this.currentValue+=2 if(this.currentValue > 20) { return {done:true}
        } return { value:this.currentValue
        }
      }).bind(this)
    }
  }
} const foo = new First20Evens; for(const value of foo) { console.log(value)
}

生成器

手动去构建实现迭代器协议的对象不是唯一的选择。生成器对象(由生成器函数返回)也实现了迭代器协议。上面的例子用生成器构建的话看起来像这样:

class First20Evens { constructor() { this.currentValue = 0 }

  [Symbol.iterator]() { return function*() { for(let i=1;i<=10;i++) { if(i % 2 === 0) { yield i
        }
      }
    }()
  }
} const foo = new First20Evens; for(const item of foo) { console.log(item)
}

本文不会过多地介绍生成器,如果你需要入门的话可以看这篇文章。今天的重要收获是,我们可以使自己的 Symbol.iterator 方法返回一个生成器对象,并且该生成器对象能够在 for ... of 循环中“正常工作”。 “正常工作”是指循环能够持续的在生成器上调用 next,直到生成器停止 yield 值为止。

$ node sample-program.js
2
4
6
8 

10

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

Fluter 应用调试

seo达人

Flutter 构建模式

目前,Flutter一共提供了三种运行模式,分别是Debug、Release和Profile模式。其中,Debug模式主要用在软件编写过程中,Release模式主要用于应用发布过程中,而Profile模式则主要用于应用性能分析时,每个模式都有自己特殊的使用场景。下面简介介绍下这几种模式:


Debug模式

Debug模式又名调试模式,Debug模式可以同时在物理设备、仿真器或者模拟器上运行应用。默认情况下,使用flutter run命令运行应用程序时就是使用的Debug模式。在Debug模式下,所有的断言、服务扩展是开启的,并且在模式对快速开发和运行周期进行了编译优化,当使用调试工具进行代码调试时可以直接连接到应用的进程里。


Release模式

Release模式又名发布模式,此模式只能在物理设备上运行,不能在模拟器上运行。使用flutter run --release命令运行应用程序时就是使用的Release模式。在Release模式下,断点、调试信息和服务扩展是不可用的,并且Release模式针对快速启动、快速执行和安装包大小进行了优化。


Profile模式

Profile模式只能在物理设备上运行,不能在模拟器上运行。此模式主要用于应用性能分析,一些应用调试能力是被保留的,目的是分析应用存在的性能问题。Profile模式和Release模式大体相同,不同点体现在,Profile模式的某些服务扩展是启用的,某些进程调试手段也是开启的。


调试模式

在 Debug 模式下,app 可以被安装在物理设备、仿真器或者模拟器上进行调试。在Debug模式下,可以进行如下操作:


断点 是开启的。

服务扩展是开启的。

针对快速开发和运行周期进行了编译优化(但不是针对执行速度、二进制文件大小或者部署)。

调试开启,类似 开发者工具 等调试工具可以连接到进程里。

如果是在 Web 平台下的调试模式,可以进行如下操作:


本次构建 没有 最小化资源并且整个构建 没有 优化性能。

为了简化调试,这个 Web 应用使用了 dartdevc 编译器。

默认情况下,运行 flutter run 会使用 Debug 模式,同时 IDE 也支持这些模式。例如,Android Studio 提供了 Run > Debug… 菜单选项,而且在项目面板中还有一个三角形的绿色运行按钮图标 。


Release 模式

当你想要最大的优化以及最小的占用空间时,就使用 Release 模式来部署 app。 release 模式是不支持模拟器或者仿真器的,使用 Release 模式意味着。


断点是不可用的。

调试信息是不可见的。

调试是禁用的。

编译针对快速启动、快速执行和小的 package 的大小进行了优化。

服务扩展是禁用的。

对于Web开发来说,使用 Release 模式意味着。


这次构建资源已经被压缩,并且性能得以优化。

这个 Web 应用通过 dart2js 编译器构建,以确保更优秀的性能。

Profile 模式

在 Profile 模式下,一些调试能力是被保留的,足够分析你的 app 性能。Profile 模式在仿真器和模拟器上是不可用的,因为他们的行为不能代表真实的性能。和 release 相比, profile 模式有以下不同:


一些服务扩展是启用的。例如,支持 performance overlay。

Tracing 是启用的,一些调试工具,比如 开发者工具 可以连接到进程里。

在 Web 平台使用Profile 模式意味着:


资源文件没有被压缩,但是整体性能已经优化。

这个 Web 应用通过 dart2js 编译器构建。

调试工具

在Flutter应用开发中,有很多工具可以帮助调试 Flutter 应用程序,常见的如下所示。


开发者工具,是一套运行在浏览器的性能及分析工具。

Android Studio/IntelliJ 和 VS Code(借助 Flutter 和 Dart 插件)支持内置的源代码调试器,可以设置断点,单步调试,检查数值。

Flutter inspector,是开发者工具提供的 widget 检查器,也可直接在 Android Studio 和 IntelliJ 中使用(借助 Flutter 插件)。检查器可以可视化展现 widget 树,查看单个 widget 及其属性值,开启性能图层,等等。

开发者工具

要调试及分析应用,开发者工具可能是你的首选。开发者工具运行在浏览器,支持以下特性:


源代码调试器

Widget 检查器,展示可视化的 widget 树; “widget select” 模式,在应用中选择一个 widget,会在 widget 树直接定位到它的位置。

内存分析

时间线视图,支持跟踪,导入及导出跟踪信息

日志视图

如果你在Debug 模式 或Profile 模式 运行,那么可以在浏览器打开开发者工具连接到你的应用。开发者工具不能用在 Release 模式 编译的应用,因为调试和分析信息都被删除了。如果你要用开发者工具分析应用,需确保使用 Profile 模式运行应用。


在这里插入图片描述


断点调试

和其他语言一样,Flutter的断点调试支持在 IDE 或编辑器(比如 Android Studio/IntelliJ 和 VS Code)、或者通过编码两种方式。

其中,开发者工具调试器如下图所示。

在这里插入图片描述


如果需要,在源代码中设置断点,然后点击工具栏中的 【Debug】 按钮,或选择 【Run】 > 【Debug】即可开启调试功能。

在这里插入图片描述


开启调试后,可以在控制台看到如下一些信息。


底部的 Debugger 窗口会显示出堆栈和变量信息。

底部的 Console 窗口会显示详细的日志输出。

调试基于默认的启动配置,如果需要自定义,点击选择目标下拉按钮,选择 Edit configuration 进行配置。

在进行断点调试时,使用得最多的就是单步调试,三个单步调试按钮在暂停后会变为可用状态。


使用 Step in 来进入被调用的方法,在遇到方法内的第一行可执行代码时结束。

使用 Step over 直接执行某个方法调用而不进入内部;该按钮在当前方法内按行执行。

使用 Step out 来跳出当前方法,这种方式会直接执行完所有当前方法内的语句。

除此之外,我们还可以使用代码的方式进行断点调试,我们可以在源代码中使用 debugger()函数来开启断点,当代码运行到此处时就会刮起,如下所示。


import 'dart:developer';


void someFunction(double offset) {

 debugger(when: offset > 30.0);

 // ...

}

Dart 分析器

如果你使用的是 Android Studio或者VSCode,那么工具会自带的 Dart 分析器默认会检查代码,并发现可能的错误。如果你使用命令行,则可以使用 flutter analyze命令来检查代码。Dart 分析器非常依赖你在代码中添加的类型注解,以帮助跟踪问题。


另外,我们可以使用flutter analyze --flutter-repo命令将分析结果打印到控制台上,每次运行这个命名之前,请先运行flutter update-packages 升级的包,这样就可以获取的依赖包。如果你不这样做,你可能会从dart:ui得到一些错误消息,比如偏移量等。因为执行flutter analysis 命令时并不会主动去拉取依赖。


对于一次性的Dart分析,直接使用flutter analyze --flutter-repo即可,对于连续分析,则可以使用flutter analyze --flutter-repo --watch命令。如果你想知道多少个成员变量丢失了dartdocs,可以添加一个dartdocs参数。


Flutter inspector 工具

Flutter inspector 是分析Flutter组件状态树的利器,Flutter使用小部件来控制页面组件到布局的精准控制,Flutter inspector 可以帮助我们进行如下一些分析。


进行布局分析,理解布局层次

诊断布局问题

在这里插入图片描述


在调试模式下,我们点击Android Studio右边Flutter inspector按钮即可开启Flutter inspector分析,Flutter inspector提供了如下的可视化调试工具。

在这里插入图片描述


Select widget mode:启用此按钮后,选择组件树的代码会自动跳转到对应的源代码里面。

Refresh tree : 重新加载的组件信息。

Slow Animations:放慢动画速度,以便进行视觉上的查验。

Debug Paint: 边框、方向的可视化。

Paint Baselines: 每个渲染框在它的每个文本基线上画一条线。

Repaint Rainbow:查看重绘的严重程度,严重的会被爆红。

除了上面的功能外,我们还可以点击【Open DevTools】打开Flutter的调试页面,可以借助它进行很多性能分析,后面会具体介绍。

在这里插入图片描述


测量应用启动时间

要收集有关 Flutter 应用程序启动所需时间的详细信息,可以在运行 flutter run 命令时使用 trace-startup 和 profile 选项,如下所示。


flutter run --trace-startup --profile

跟踪输出被保存到 Flutter 工程目录在 build 目录下,一个名为 start_up_info.json 的 JSON 文件中,输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间,如下所示。


{

 "engineEnterTimestampMicros": 2346054348633,

 "timeToFrameworkInitMicros": 812748,

 "timeToFirstFrameRasterizedMicros": 1573154,

 "timeToFirstFrameMicros": 1221472,

 "timeAfterFrameworkInitMicros": 408724

}

对应的具体含义如下:


进入 Flutter 引擎时

展示应用第一帧时

初始化Flutter框架时

完成Flutter框架初始化时

使用Android Studio进行调试

Flutter官方推荐使用Android Studio或VSCode进行应用开发, 和其他语言的调试一样,Dart代码的调试流程也差不多。如果还没有Flutter项目,可以新建一个示例项目。通过单击首先,点击调试图标(Debug-run icon)同时打开调试面板并在控制台中运行应用,首次运行应用是最慢的,应用启动后,界面应该是下面这样的。

在这里插入图片描述


然后,我们在在 counter++ 这一行上添加断点。在应用里,点击 + 按钮(FloatingActionButton,或者简称 FAB)来增加数字,应用会暂停。

在这里插入图片描述


你可以 step in/out/over Dart 语句、热重载和恢复执行应用、以及像使用其他调试器一样来使用 Dart 调试器。


Flutter inspector

Flutter inspector 是一个用来可视化以及查看 Flutter widget 树的工具,提供如下功能:


了解现有布局

诊断布局问题

可以使用 Android Studio 窗口右侧的垂直按钮来打开Flutter inspector,如下图所示。


在这里插入图片描述


Flutter outline

Flutter Outline 是一个可视的显示页面构建方法的功能,注意在构建方法上可能与 widget 树不同,可以使用 Android Studio 窗口右侧的垂直按钮切换 outline 的显示。

在这里插入图片描述


Tip: 我们可以安装一个 Presentation Assistant 插件来辅助我们进行开发,Presentation Assistant 提供了很多的快捷功能。例如,当焦点在编辑面板中时,输入 command-Shift-A(Mac)或者 shift-control-A(Windows 和 Linux),该插件会同时显示「查找」面板并显示在所有三个平台上执行此操作的提示。

在这里插入图片描述


然后在输入框中输入attach关键字,显示如下图。


在这里插入图片描述


使用 Android Gradle 调试

为了调试原生代码,你需要一个包含 Android 原生代码的应用。在本节中,你将学会如何连接两个调试器到你的应用:

1)Dart 调试器。

2)Android Gradle 调试器。


创建一个基本的 Flutter 应用,然后替换 lib/main.dart 的代码为以下示例代码。


// Copyright 2017 The Chromium Authors. All rights reserved.

// Use of this source code is governed by a BSD-style license that can be

// found in the LICENSE file.


import 'dart:async';


import 'package:flutter/material.dart';

import 'package:url_launcher/url_launcher.dart';


void main() {

 runApp(MyApp());

}


class MyApp extends StatelessWidget {

 @override

 Widget build(BuildContext context) {

   return MaterialApp(

     title: 'URL Launcher',

     theme: ThemeData(

       primarySwatch: Colors.blue,

     ),

     home: MyHomePage(title: 'URL Launcher'),

   );

 }

}


class MyHomePage extends StatefulWidget {

 MyHomePage({Key key, this.title}) : super(key: key);

 final String title;


 @override

 _MyHomePageState createState() => _MyHomePageState();

}


class _MyHomePageState extends State<MyHomePage> {

 Future<void> _launched;


 Future<void> _launchInBrowser(String url) async {

   if (await canLaunch(url)) {

     await launch(url, forceSafariVC: false, forceWebView: false);

   } else {

     throw 'Could not launch $url';

   }

 }


 Future<void> _launchInWebViewOrVC(String url) async {

   if (await canLaunch(url)) {

     await launch(url, forceSafariVC: true, forceWebView: true);

   } else {

     throw 'Could not launch $url';

   }

 }


 Widget _launchStatus(BuildContext context, AsyncSnapshot<void> snapshot) {

   if (snapshot.hasError) {

     return Text('Error: ${snapshot.error}');

   } else {

     return Text('');

   }

 }


 @override

 Widget build(BuildContext context) {

   String toLaunch = 'https://flutter.dev';

   return Scaffold(

     appBar: AppBar(

       title: Text(widget.title),

     ),

     body: Center(

       child: Column(

         mainAxisAlignment: MainAxisAlignment.center,

         children: <Widget>[

           Padding(

             padding: EdgeInsets.all(16.0),

             child: Text(toLaunch),

           ),

           RaisedButton(

             onPressed: () => setState(() {

                   _launched = _launchInBrowser(toLaunch);

                 }),

             child: Text('Launch in browser'),

           ),

           Padding(padding: EdgeInsets.all(16.0)),

           RaisedButton(

             onPressed: () => setState(() {

                   _launched = _launchInWebViewOrVC(toLaunch);

                 }),

             child: Text('Launch in app'),

           ),

           Padding(padding: EdgeInsets.all(16.0)),

           FutureBuilder<void>(future: _launched, builder: _launchStatus),

         ],

       ),

     ),

   );

 }

}

然后,添加 url_launcher 依赖到 pubspec 文件,并执行 flutter pub get命令拉取依赖包。


name: flutter_app

description: A new Flutter application.

version: 1.0.0+1


dependencies:

 flutter:

   sdk: flutter


 url_launcher: ^3.0.3

 cupertino_icons: ^0.1.2


dev_dependencies:

 flutter_test:

   sdk: flutter

点击调试按钮(Debug-run icon)来同时打开调试面板并启动应用,如下图所示。

在这里插入图片描述


点击 【Attach debugger to Android process】 按钮,从进程对话框中,你应该可以看到每一个设备的入口。选择 show all processes 来显示每个设备可用的进程。

在这里插入图片描述


在调试面板中,你现在应该可以看到一个 Android Debugger 标签页,然后依次选择【app_name】 > 【android】 > 【app】 > 【src】 >【 main】 > 【java】 > 【io.flutter plugins】在项目面板,然后双击 GeneratedProjectRegistrant 在编辑面板中打开 Java 代码,此时Dart 和原生调试器都在与同一个进程交互。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

基于vue脚手架构建库并发布到npm

seo达人

构建库的常见方法有两种:一种是自己手动构建webpack库打包,设置output为 library; 另一种是基于vue-cli3输出库资源包。我们采用第二种vue脚手架的方式构建库。


新增编译库命令

// package.json

"scripts": {

   // ...

   "lib": "vue-cli-service build --target lib --name Step --dest dist packages/index.js"

}


// packages/index.js  默认打包Step

import Step from '../steps/src/step';

Step.install = function(Vue) {

   Vue.component(Step.name, Step);

};

export default Step;

--name: 库名称。

--target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。

--dest: 输出目录,默认 dist。

[entry]: 最后一个参数为入口文件,默认为 src/App.vue。

更多详细配置查看 ☛ vue脚手架官网


如果该库依赖于其他库,请在vue.config.js 配置 externals

// vue.config.js

module.exports = {

   configureWebpack:{

     externals: {

        vue: 'Vue',

        'vue-router':'VueRouter',

        axios: 'axios'

     }

   }

}

执行 npm run lib 就可以发现我们的库被打包到了 根目录的dist文件夹下。


添加 .npmignore 文件(可选)

和 .gitignore 的语法一样,具体需要提交什么文件,看各自的实际情况


# 忽略目录

examples/

packages/

public/


# 忽略指定文件

vue.config.js

babel.config.js

*.map

配置npm库信息

配置package.json文件,以发布库文件。


{

 "name": "gis",

 "version": "1.2.5",

 "description": "基于 Vue 的库文件",

 "main": "dist/gis.umd.min.js",

 "keyword": "vue gis",

 "private": false,

  "files": ["dist"],

 "license": "MIT"

}

name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。

version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。

description: 描述。

main: 入口文件,该字段需指向我们最终编译后的包文件。

keyword:关键字,以空格分离希望用户最终搜索的词。

author:作者

files: 要上传的文件

private:是否私有,需要修改为 false 才能发布到 npm

license: 开源协议

dependencies: 依赖库

注意每次发布新的库,需要更改版本号,规则如下:

"version": "1.2.5" 主版本号为 1,次版本号 2,修订号 5

主版本号(Major):当你做了不兼容的API修改

次版本号(Minor):当你做了向下兼容的功能性新增

修订号(Patch):当你做了向下兼容的问题修正

登录npm

首先设置登录的npm镜像地址


npm config set registry http://168.20.20.57.4873

然后在终端执行登录命令,输入用户名、密码、邮箱即可登录


npm login

接着发布库资源到npm


npm publish

最后发布成功可到官网查看对应的包并下载


npm install package_name

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

js使用transition效果实现无缝滚动

seo达人

前言

无缝轮播一直是面试的热门题目,而大部分答案都是复制第一张到最后。诚然,这种方法是非常标准,那么有没有另类一点的方法呢?

第一种方法是需要把所有图片一张张摆好,然后慢慢移动的,

但是我能不能直接不摆就硬移动呢?

如果你使用过vue的transition,我们是可以通过给每一张图片来添加入场动画和离场动画来模拟这个移动

  • 进场动画就是从最右侧到屏幕中央
  • 出场动画是从屏幕中央到左侧移出

这样看起来的效果就是图片从右边一直往左移动,但是这个不一样的地方是,我们每一个元素都有这个进场动画和离场动画,我们根本不用关心它是第几个元素,你只管轮播就是。

如果不用vue呢?

很简单,我们自己实现一个transtition的效果就好啦,主要做的是以下两点

  • 元素显示的时候,即display属性不为none的时候,添加xx-enter-active动画
  • 元素消失的时候,先添加动画xx-leave-active, 注意要让动画播完才消失
 function hide(el){
     el.className = el.className.replace(' slide-enter-active','')
     el.className += ' slide-leave-active' el.addEventListener('animationend',animationEvent)
 } function animationEvent(e){
     e.target.className = e.target.className.replace(' slide-leave-active','')
     e.target.style.display = 'none' e.target.removeEventListener('animationend',animationEvent)
 } function show(el){
     el.style.display = 'flex' el.className += ' slide-enter-active' }

这里我们使用了animationend来监听动画结束,注意这里每次从新添加类的时候需要重新添加监听器,不然会无法监听。如果不使用这个方法你可以使用定时器的方式来移除leave-active类。

 function hide(el){
     el.className = el.className.replace(' slide-enter-active','') 

el.className += ' slide-leave-active' setTimeout(()=>

{ //动画结束后清除class el.className = el.className.replace(' slide-leave-active','')

el.style.display = 'none' }, ANIMATION_TIME) //这个ANIMATION_TIME为你在css中动画执行的时间 }

那么,动画怎么写呢?

 .slide-enter-active{ position: absolute; animation: slideIn ease .5s forwards;
 } .slide-leave-active{ position: absolute; animation: slideOut ease .5s forwards;
 } @keyframes slideIn {
     0%{ transform: translateX(100%);
     }
     100%{ transform: translateX(0);
     }
 } @keyframes slideOut {
     0%{ transform: translateX(0);
     }
     100%{ transform: translateX(-100%);
     }
 }

需要注意的是这里的 forwards属性,这个属性表示你的元素状态将保持动画后的状态,如果不设置的话,动画跑完一遍,你的元素本来执行了离开动画,执行完以后会回来中央位置杵着。这个时候你会问了,上面的代码不是写了,动画执行完就隐藏元素吗?

如果你使用上面的setTimeout来命令元素执行完动画后消失,那么可能会有一瞬间的闪烁,因为实际业务中,你的代码可能比较复杂,setTimeout没法在那么精准的时间内执行。保险起见,就让元素保持动画离开的最后状态,即translateX(-100%)。此时元素已经在屏幕外了,不用关心它的表现了

轮播逻辑怎么写?

很简单,我们进一个新元素的时候同时移除旧元素即可,两者同时执行进场和离场动画即可。

 function autoPlay(){
     setTimeout(()=>{
         toggleShow(新元素, 旧元素) this.autoPlay()
     },DURATION) //DURATION为动画间隔时间 } function toggleShow(newE,oldE){ //旧ele和新ele同时动画 hide(oldE)
     show(newE)
 }

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

字体构造与文字垂直居中方案探索

seo达人

1. 引子

垂直居中基本上是入门 CSS 必须要掌握的问题了,我们肯定在各种教程中都看到过“CSS 垂直居中的 N 种方法”,通常来说,这些方法已经可以满足各种使用场景了,然而当我们碰到了需要使用某些特殊字体进行混排、或者使文字对齐图标的情况时,也许会发现,无论使用哪种垂直居中的方法,总是感觉文字向上或向下偏移了几像素,不得不专门对它们进行位移,为什么会出现这种情况呢?

2. 常见的垂直居中的方法

下图是一个使用各种常见的垂直居中的方法来居中文字的示例,其中涉及到不同字体的混排,可以看出,虽然这里面用了几种常用的垂直居中的方法,但是在实际的观感上这些文字都没有恰好垂直居中,有些文字看起来比较居中,而有些文字则偏移得很厉害。
垂直居中示例图
在线查看:CodePen(字体文件直接引用了谷歌字体,如果没有效果需要注意网络情况)

通过设置 vertical-align:middle 对文字进行垂直居中时,父元素需要设置 font-size: 0,因为 vertical-align:middle 是将子元素的中点与父元素的 baseline + x-height / 2 的位置进行对齐的,设置字号为 0 可以保证让这些线的位置都重合在中点。
我们用鼠标选中这些文字,就能发现选中的区域确实是在父层容器里垂直居中的,那么为什么文字却各有高低呢?这里就涉及到了字体本身的构造和相关的度量值。

3. 字体的构造和度量

这里先提出一个问题,我们在 CSS 中给文字设置了 font-size,这个值实际设置的是字体的什么属性呢?
下面的图给出了一个示例,文字所在的标签均为 span,对每种字体的文字都设置了红色的 outline 以便观察,且设有 line-height: normal。从图中可以看出,虽然这些文字的字号都是 40px,但是他们的宽高都各不相同,所以字号并非设置了文字实际显示的大小。
文字大小示意图
为了解答这个问题,我们需要对字体进行深入了解,以下这些内容是西文字体的相关概念。首先一个字体会有一个 EM Square(也被称为 UPM、em、em size)[4],这个值最初在排版中表示一个字体中大写 M 的宽度,以这个值构成一个正方形,那么所有字母都可以被容纳进去,此时这个值实际反映的就成了字体容器的高度。在金属活字中,这个容器就是每个字符的金属块,在一种字体里,它们的高度都是统一的,这样每个字模都可以放入印刷工具中并进行排印。在数码排印中,em 是一个被设置了大小的方格,计量单位是一种相对单位,会根据实际字体大小缩放,例如 1000 单位的字体设置了 16pt 的字号,那么这里 1000 单位的大小就是 16pt。Em 在 OpenType 字体中通常为 1000 ,在 TrueType 字体中通常为 1024 或 2048(2 的 n 次幂)。
金属活字

金属活字,图片来自 http://designwithfontforge.com/en-US/The_EM_Square.html

3.1 字体度量

字体本身还有很多概念和度量值(metrics),这里介绍几个常见的概念,以维基百科的这张图为例(下面的度量值的计量单位均为基于 em 的相对单位):
字体结构

  • baseline:Baseline(基线)是字母放置的水平线。
  • x height:X height(x字高)表示基线上小写字母 x 的高度。
  • capital height:Capital height(大写高度)表示基线上一个大写字母的高度。
  • ascender / ascent:Ascender(升部)表示小写字母超出 x字高的字干,为了辨识性,ascender 的高度可能会比 capital height 大一点。Ascent 则表示文字顶部到 baseline 的距离。

字符升部

  • descender / descent:Descender(降部)表示扩展到基线以下的小写字母的字干,如 j、g 等字母的底部。Descent 表示文字底部到 baseline 的距离。
  • line gap:Line gap 表示 descent 底部到下一行 ascent 顶部的距离。这个词我没有找到合适的中文翻译,需要注意的是这个值不是行距(leading),行距表示两行文字的基线间的距离。

接下来我们在 FontForge 软件里看看这些值的取值,这里以 Arial 字体给出一个例子:
Arial Font Information
从图中可以看出,在 General 菜单中,Arial 的 em size 是 2048,字体的 ascent 是1638,descent 是410,在 OS/2 菜单的 Metrics 信息中,可以得到 capital height 是 1467,x height 为 1062,line gap 为 67。
然而这里需要注意,尽管我们在 General 菜单中得到了 ascent 和 descent 的取值,但是这个值应该仅用于字体的设计,它们的和永远为 em size;而计算机在实际进行渲染的时候是按照 OS/2 菜单中对应的值来计算,一般操作系统会使用 hhea(Horizontal Header Table)表的 HHead Ascent 和 HHead Descent,而 Windows 是个特例,会使用 Win Ascent 和 Win Descent。通常来说,实际用于渲染的 ascent 和 descent 取值要比用于字体设计的大,这是因为多出来的区域通常会留给注音符号或用来控制行间距,如下图所示,字母顶部的水平线即为第一张图中 ascent 高度 1638,而注音符号均超过了这个区域。根据资料的说法[5],在一些软件中,如果文字内容超过用于渲染的 ascent 和 descent,就会被截断,不过我在浏览器里实验后发现浏览器并没有做这个截断(Edge 86.0.608.0 Canary (64 bit), MacOS 10.15.6)。
ascent
在本文中,我们将后面提到的 ascent 和 descent 均认为是 OS/2 选项中读取到的用于渲染的 ascent 和 descent 值,同时我们将 ascent + descent 的值叫做 content-area。

理论上一个字体在 Windows 和 MacOS 上的渲染应该保持一致,即各自系统上的 ascent 和 descent 应该相同,然而有些字体在设计时不知道出于什么原因,导致其确实在两个系统中有不同的表现。以下是 Roboto 的例子:
Differences between Win and HHead metrics cause the font to be rendered differently on Windows vs. iOS (or Mac I assume) · Issue #267 · googlefonts/roboto
那么回到本节一开始的问题,CSS 中的 font-size 设置的值表示什么,想必我们已经有了答案,那就是一个字体 em size 对应的大小;而文字在设置了 line-height: normal 时,行高的取值则为 content-area + line-gap,即文本实际撑起来的高度。
知道了这些,我们就不难算出一个字体的显示效果,上面 Arial 字体在 line-height: normal 和 font-size: 100px 时撑起的高度为 (1854 + 434 + 67) / 2048 * 100px = 115px
在实验中发现,对于一个行内元素,鼠标拉取的 selection 高度为当前行 line-height 最高的元素值。如果是块状元素,当 line-height 的值为大于 content-area 时,selection 高度为 line-height,当其小于等于 content-area 时,其高度为 content-area 的高度。

3.2 验证 metrics 对文字渲染的影响

在中间插一个问题,我们应该都使用过 line-height 来给文字进行垂直居中,那么 line-height 实际是以字体的哪个部分的中点进行计算呢?为了验证这个问题,我新建了一个很有“设计感”的字体,em size 设为 1000,ascent 为 800,descent 为 200,并对其分别设置了正常的和比较夸张的 metrics:
TestGap normal
TestGap exaggerate
上面图中左边是 FontForge 里设置的 metrics,右边是实际显示效果,文字字号设为 100px,四个字母均在父层的 flex 布局下垂直居中,四个字母的 line-height 分别为 0、1em、normal、3em,红色边框是元素的 outline,黄色背景是鼠标选取的背景。由上面两张图可以看出,字体的 metrics 对文字渲染位置的影响还是很大的。同时可以看出,在设置 line-height 时,虽然 line gap 参与了撑起取值为 normal 的空间,但是不参与文字垂直居中的计算,即垂直居中的中点始终是 content-area 的中点。
TestGap trimming
我们又对字体进行了微调,使其 ascent 有一定偏移,这时可以看出 1em 行高的文字 outline 恰好在正中间,因此可以得出结论:在浏览器进行渲染时,em square 总是相对于 content-area 垂直居中。
说完了字体构造,又回到上一节的问题,为什么不同字体文字混排的时候进行垂直居中,文字各有高低呢?
在这个问题上,本文给出这样一个结论,那就是因为不同字体的各项度量值均不相同,在进行垂直居中布局时,content-area 的中点与视觉的中点不统一,因此导致实际看起来存在位置偏移,下面这张图是 Arial 字体的几个中线位置:
Arial center line
从图上可以看出来,大写字母和小写字母的视觉中线与整个字符的中线还是存在一定的偏移的。这里我没有找到排版相关学科的定论,究竟以哪条线进行居中更符合人眼观感的居中,以我个人的观感来看,大写字母的中线可能看起来更加舒服一点(尤其是与没有小写字母的内容进行混排的时候)。

需要注意一点,这里选择的 Arial 这个字体本身的偏移比较少,所以使用时整体感觉还是比较居中的,这并不代表其他字体也都是这样。

3.3 中文字体

对于中文字体,本身的设计上没有基线、升部、降部等说法,每个字都在一个方形盒子中。但是在计算机上显示时,也在一定程度上沿用了西文字体的概念,通常来说,中文字体的方形盒子中文字体底端在 baseline 和 descender 之间,顶端超出一点 ascender,而标点符号正好在 baseline 上。

4. CSS 的解决方案

我们已经了解了字体的相关概念,那么如何解决在使用字体时出现的偏移问题呢?
通过上面的内容可以知道,文字显示的偏移主要是视觉上的中点和渲染时的中点不一致导致的,那么我们只要把这个不一致修正过来,就可以实现视觉上的居中了。
为了实现这个目标,我们可以借助 vertical-align 这个属性来完成。当 vertical-align 取值为数值的时候,该值就表示将子元素的基线与父元素基线的距离,其中正数朝上,负数朝下。
这里介绍的方案,是把某个字体下的文字通过计算设置 vertical-align 的数值偏移,使其大写字母的视觉中点与用于计算垂直居中的点重合,这样字体本身的属性就不再影响居中的计算。
具体我们将通过以下的计算方法来获取:首先我们需要已知当前字体的 em-size,ascent,descent,capital height 这几个值(如果不知道 em-size,也可以提供其他值与 em-size 的比值),以下依然以 Arial 为例:

const emSize = 2048; const ascent = 1854; const descent = 434; const capitalHeight = 1467

// 计算前需要已知给定的字体大小 const fontSize = FONT_SIZE; // 根据文字大小,求得文字的偏移 const verticalAlign = ((ascent - descent - capitalHeight) / emSize) * fontSize; return ( <span style={{ fontFamily: FONT_FAMILY, fontSize }}> <span style={{ verticalAlign }}>TEXT</span> </span> )

由此设置以后,外层 span 将表现得像一个普通的可替换元素参与行内的布局,在一定程度上无视字体 metrics 的差异,可以使用各种方法对其进行垂直居中。
由于这种方案具有固定的计算步骤,因此可以根据具体的开发需求,将其封装为组件、使用 CSS 自定义属性或使用 CSS 预处理器对文本进行处理,通过传入字体信息,就能修正文字垂直偏移。

5. 解决方案的局限性

虽然上述的方案可以在一定程度上解决文字垂直居中的问题,但是在实际使用中还存在着不方便的地方,我们需要在使用字体之前就知道字体的各项 metrics,在自定义字体较少的情况下,开发者可以手动使用 FontForge 等工具查看,然而当字体较多时,挨个查看还是比较麻烦的。
目前的一种思路是我们可以使用 Canvas 获取字体的相关信息,如现在已经有开源的获取字体 metrics 的库 FontMetrics.js。它的核心思想是使用 Canvas 渲染对应字体的文字,然后使用 getImageData 对渲染出来的内容进行分析。如果在实际项目中,这种方案可能导致潜在的性能问题;而且这种方式获取到的是渲染后的结果,部分字体作者在构建字体时并没有严格将设计的 metrics 和字符对应,这也会导致获取到的 metrics 不够准确。
另一种思路是直接解析字体文件,拿到字体的 metrics 信息,如 opentype.js 这个项目。不过这种做法也不够轻量,不适合在实际运行中使用,不过可以考虑在打包过程中自动执行这个过程。
此外,目前的解决方案更多是偏向理论的方法,当文字本身字号较小的情况下,浏览器可能并不能按照预期的效果渲染,文字会根据所处的 DOM 环境不同而具有 1px 的偏移[9]。

6. 未来也许可行的解决方案 - CSS Houdini

CSS Houdini 提出了一个 Font Metrics 草案[6],可以针对文字渲染调整字体相关的 metrics。从目前的设计来看,可以调整 baseline 位置、字体的 em size,以及字体的边界大小(即 content-area)等配置,通过这些可以解决因字体的属性导致的排版问题。

[Exposed=Window] interface FontMetrics {
 readonly attribute double width;
 readonly attribute FrozenArray<double> advances;
 readonly attribute double boundingBoxLeft;
 readonly attribute double boundingBoxRight;
 readonly attribute double height;
 readonly attribute double emHeightAscent;
 readonly attribute double emHeightDescent;
 readonly attribute double boundingBoxAscent;
 readonly attribute double boundingBoxDescent;
 readonly attribute double fontBoundingBoxAscent;
 readonly attribute double fontBoundingBoxDescent;
 readonly attribute Baseline dominantBaseline;
 readonly attribute FrozenArray<Baseline> baselines;
 readonly attribute FrozenArray<Font> fonts;
};

css houdini
从 https://ishoudinireadyyet.com/ 这个网站上可以看到,目前 Font Metrics 依然在提议阶段,还不能确定其 API 具体内容,或者以后是否会存在这一个特性,因此只能说是一个在未来也许可行的文字排版处理方案。

7.总结

文本垂直居中的问题一直是 CSS 中最常见的问题,但是却很难引起注意,我个人觉得是因为我们常用的微软雅黑、苹方等字体本身在设计上比较规范,在通常情况下都显得比较居中。但是当一个字体不是那么“规范”时,传统的各种方法似乎就有点无能为力了。
本文分析了导致了文字偏移的因素,并给出寻找文字垂直居中位置的方案。
由于涉及到 IFC 的问题本身就很复杂[7],关于内联元素使用 line-height 与 vertical-align 进行居中的各种小技巧因为与本文不是强相关,所以在文章内也没有提及,如果对这些内容比较感兴趣,也可以通过下面的参考资料寻找一些相关介绍。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

这些 ECMAScript 模块知识,都是我需要知道的

seo达人

ES 模块是什么?

ECMAScript模块(简称ES模块)是一种JavaScript代码重用的机制,于2015年推出,一经推出就受到前端开发者的喜爱。在2015之年,JavaScript 还没有一个代码重用的标准机制。多年来,人们对这方面的规范进行了很多尝试,导致现在有多种模块化的方式。

你可能听说过AMD模块UMD,或CommonJS,这些没有孰优孰劣。最后,在ECMAScript 2015中,ES 模块出现了。

我们现在有了一个“正式的”模块系统。

ES 模块无处不在?

理论上,ES 模块应该在所有JavaScript环境中。实际上,ES 模块的主要应用还是在浏览器上。

2020年5月,Node.js v12.17.0 增加了在不使用标记前提下对ECMAScript模块的支持。 这意味着我们现在可以在Node.js中使用importexport ,而无需任何其他命令行标志。

ECMAScript模块要想在任何JavaScript环境通用,可能还需要很长的路要走,但方向是正确的。

ES 模块是什么样的

ES 模块是一个简单的文件,我们可以在其中声明一个或多个导出。以下面utils.js为例:

// utils.js export function funcA() { return "Hello named export!";
} export default function funcB() { return "Hello default export!";
}

这里有两个导出。

第一个是命名导出,后面是export default,表示为默认导出

假设我们的项目文件夹中有一个名为utils.js的文件,我们可以将这个模块提供的对象导入到另一个文件中。

如何从 ES模块 导入

假设我们在项目文中还有一个Consumer.js的文件。 要导入utils.js公开的函数,我们可以这样做:

// consumer.js import { funcA } from "./util.js";

这种对应我们的命名导入方式.

如果我们要导入 utils.js 中的默认导出也就是 funcB 方法,我们可以这样做:

// consumer.js import { funcA } from "./util.js";

当然,我们可以导入同时导入命名和默认的:

// consumer.js import funcB, { funcA } from "./util.js";

funcB();
funcA();

我们也可以用星号导入整个模块:

import * as myModule from './util.js';

myModule.funcA();
myModule.default(); 

注意,这里要使用默认到处的方法是使用 default() 而不是 funcB()

从远程模块导入:

import { createStore } from "https://unpkg.com/redux@4.0.5/es/redux.mjs"; const store = createStore(/* do stuff */)

浏览器中的 ES 模块

现代浏览器支持ES模块,但有一些警告。 要使用模块,需要在 script 标签上添加属性 type, 对应值 为 module

<html lang="en"> <head> <meta charset="UTF-8"> <title>ECMAScript modules in the browser</title>

</head> <body> <p id="el">The result is:

</p> </body> <script type="module"> import { appendResult } from "./myModule.js"; const el = document.getElementById("el"); appendResult(el);

appendResult(el);

appendResult(el);

appendResult(el);

appendResult(el); </script> </html>

myModule.js 内容如下:

export function appendResult(element) { const result = Math.random();
  element.innerText += result;
}

动态导入

ES 模块是静态的,这意味着我们不能在运行时更改导入。随着2020年推出的动态导入(dynamic imports),我们可以动态加载代码来响应用户交互(webpack早在ECMAScript 2020推出这个特性之前就提供了动态导入)。

考虑下面的代码:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8">

<title>Dynamic imports</title> </head> <body> <button id="btn">Load!</button> </body> <script src="loader.js"></script> </html>

再考虑一个带有两个导出的JavaScript模块

// util.js export function funcA() { console.log("Hello named export!");
} export default function funcB() { console.log("Hello default export!");
}

为了动态导入 util.js 模块,我们可以点击按钮在去导入:

/ loader.js
const btn = document.getElementById("btn");

btn.addEventListener("click", () => { // loads named export import("./util.js").then(({ funcA }) => {
    funcA();
  });
});

这里使用解构的方式,取出命名导出 funcA 方法:

({ funcA }) => {}

ES模块实际上是JavaScript对象:我们可以解构它们的属性以及调用它们的任何公开方法。

要使用动态导入的默认方法,可以这样做

// loader.js const btn = document.getElementById("btn");

btn.addEventListener("click", () => { import("./util.js").then((module) => { module.default();
  });
});

当作为一个整体导入一个模块时,我们可以使用它的所有导出

// loader.js const btn = document.getElementById("btn"); 

btn.addEventListener("click", () =>

{ // loads entire module // uses everything import("./util.js").then((module) => { module.funcA(); module.default();

}); });

还有另一种用于动态导入的常见样式,如下所示:

const loadUtil = () => import("./util.js"); const btn = document.getElementById("btn");

btn.addEventListener("click", () => { // });

loadUtil返回的是一个 promise,所以我们可以这样操作

const loadUtil = () => import("./util.js"); const btn = document.getElementById("btn");

btn.addEventListener("click", () => {
  loadUtil().then(module => { module.funcA(); module.default();
  })
})

动态导入看起来不错,但是它们有什么用呢?

使用动态导入,我们可以拆分代码,并只在适当的时候加载重要的代码。在 JavaScript 引入动态导入之前,这种模式是webpack(模块绑定器)独有的。

ReactVue通过动态导入代码拆分来加载响应事件的代码块,比如用户交互或路由更改。

动态导入JSON文件

假设我们项目有一个 person.json 文件,内容如下:

{ "name": "Jules", "age": 43 }

现在,我们需要动态导入该文件以响应某些用户交互。

因为 JSON 文件不是一个方法,所以我们可以使用默认导出方式:

const loadPerson = () => import('./person.json'); const btn = document.getElementById("btn");

btn.addEventListener("click", () => {
  loadPerson().then(module => { const { name, age } = module.default; console.log(name, age);
  });
});

这里我们使用解构的方式取出 name 和 age :

const { name, age } = module.default;

动态导入与 async/await

因为 import() 语句返回是一个 Promise,所以我们可以使用 async/await:

const loadUtil = () => import("./util.js"); const btn = document.getElementById("btn");

btn.addEventListener("click", async () => { const utilsModule = await loadUtil();
  utilsModule.funcA();
  utilsModule.default();
})

动态导入的名字

使用import()导入模块时,可以按照自己的意愿命名它,但要调用的方法名保持一致:

import("./util.js").then((module) => { module.funcA(); module.default();
  });

或者:

 import("./util.js").then((utilModule) => {
    utilModule.funcA();
    utilModule.default();
  });

原文:https://www.valentinog.com/bl...

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

细数 TS 中那些奇怪的符号

seo达人

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。


本文阿宝哥将分享这些年在学习 TypeScript 过程中,遇到的 10 大 “奇怪” 的符号。其中有一些符号,阿宝哥第一次见的时候也觉得 “一脸懵逼”,希望本文对学习 TypeScript 的小伙伴能有一些帮助。


好的,下面我们来开始介绍第一个符号 —— ! 非空断言操作符。


一、! 非空断言操作符

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。


那么非空断言操作符到底有什么用呢?下面我们先来看一下非空断言操作符的一些使用场景。


1.1 忽略 undefined 和 null 类型

function myFunc(maybeString: string | undefined | null) { // Type 'string | null | undefined' is not assignable to type 'string'. // Type 'undefined' is not assignable to type 'string'.  const onlyString: string = maybeString; // Error const ignoreUndefinedAndNull: string = maybeString!; // Ok }

1.2 调用函数时忽略 undefined 类型

type NumGenerator = () => number; function myFunc(numGenerator: NumGenerator | undefined) { // Object is possibly 'undefined'.(2532) // Cannot invoke an object which is possibly 'undefined'.(2722) const num1 = numGenerator(); // Error const num2 = numGenerator!(); //OK }

因为 ! 非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用的过程中,要特别注意。比如下面这个例子:


const a: number | undefined = undefined; const b: number = a!; console.log(b);

以上 TS 代码会编译生成以下 ES5 代码:


"use strict"; const a = undefined; const b = a; console.log(b);

虽然在 TS 代码中,我们使用了非空断言,使得 const b: number = a!; 语句可以通过 TypeScript 类型检查器的检查。但在生成的 ES5 代码中,! 非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined。


二、?. 运算符

TypeScript 3.7 实现了呼声最高的 ECMAScript 功能之一:可选链(Optional Chaining)。有了可选链后,我们编写代码时如果遇到 null 或 undefined 就可以立即停止某些表达式的运行。可选链的核心是新的 ?. 运算符,它支持以下语法:


obj?.prop

obj?.[expr]

arr?.[index] func?.(args)

这里我们来举一个可选的属性访问的例子:


const val = a?.b;

为了更好的理解可选链,我们来看一下该 const val = a?.b 语句编译生成的 ES5 代码:


var val = a === null || a === void 0 ? void 0 : a.b;

上述的代码会自动检查对象 a 是否为 null 或 undefined,如果是的话就立即返回 undefined,这样就可以立即停止某些表达式的运行。你可能已经想到可以使用 ?. 来替代很多使用 && 执行空检查的代码:


if(a && a.b) { } if(a?.b){ } /**

* if(a?.b){ } 编译后的ES5代码

*

* if(

*  a === null || a === void 0

*  ? void 0 : a.b) {

* }

*/

但需要注意的是,?. 与 && 运算符行为略有不同,&& 专门用于检测 falsy 值,比如空字符串、0、NaN、null 和 false 等。而 ?. 只会验证对象是否为 null 或 undefined,对于 0 或空字符串来说,并不会出现 “短路”。


2.1 可选元素访问

可选链除了支持可选属性的访问之外,它还支持可选元素的访问,它的行为类似于可选属性的访问,只是可选元素的访问允许我们访问非标识符的属性,比如任意字符串、数字索引和 Symbol:


function tryGetArrayElement<T>(arr?: T[], index: number = 0) { return arr?.[index];

}

以上代码经过编译后会生成以下 ES5 代码:


"use strict"; function tryGetArrayElement(arr, index) { if (index === void 0) { index = 0; } return arr === null || arr === void 0 ? void 0 : arr[index];

}

通过观察生成的 ES5 代码,很明显在 tryGetArrayElement 方法中会自动检测输入参数 arr 的值是否为 null 或 undefined,从而保证了我们代码的健壮性。


2.2 可选链与函数调用

当尝试调用一个可能不存在的方法时也可以使用可选链。在实际开发过程中,这是很有用的。系统中某个方法不可用,有可能是由于版本不一致或者用户设备兼容性问题导致的。函数调用时如果被调用的方法不存在,使用可选链可以使表达式自动返回 undefined 而不是抛出一个异常。


可选调用使用起来也很简单,比如:


let result = obj.customMethod?.();

该 TypeScript 代码编译生成的 ES5 代码如下:


var result = (_a = obj.customMethod) === null || _a === void 0 ? void 0 : _a.call(obj);

另外在使用可选调用的时候,我们要注意以下两个注意事项:


如果存在一个属性名且该属性名对应的值不是函数类型,使用 ?. 仍然会产生一个 TypeError 异常。

可选链的运算行为被局限在属性的访问、调用以及元素的访问 —— 它不会沿伸到后续的表达式中,也就是说可选调用不会阻止 a?.b / someMethod() 表达式中的除法运算或 someMethod 的方法调用。

三、?? 空值合并运算符

在 TypeScript 3.7 版本中除了引入了前面介绍的可选链 ?. 之外,也引入了一个新的逻辑运算符 —— 空值合并运算符 ??。当左侧操作数为 null 或 undefined 时,其返回右侧的操作数,否则返回左侧的操作数。


与逻辑或 || 运算符不同,逻辑或会在左操作数为 falsy 值时返回右侧操作数。也就是说,如果你使用 || 来为某些变量设置默认的值时,你可能会遇到意料之外的行为。比如为 falsy 值(''、NaN 或 0)时。


这里来看一个具体的例子:


const foo = null ?? 'default string'; console.log(foo); // 输出:"default string" const baz = 0 ?? 42; console.log(baz); // 输出:0

以上 TS 代码经过编译后,会生成以下 ES5 代码:


"use strict"; var _a, _b; var foo = (_a = null) !== null && _a !== void 0 ? _a : 'default string';

console.log(foo); // 输出:"default string" var baz = (_b = 0) !== null && _b !== void 0 ? _b : 42;

console.log(baz); // 输出:0

通过观察以上代码,我们更加直观的了解到,空值合并运算符是如何解决前面 || 运算符存在的潜在问题。下面我们来介绍空值合并运算符的特性和使用时的一些注意事项。


3.1 短路

当空值合并运算符的左表达式不为 null 或 undefined 时,不会对右表达式进行求值。


function A() { console.log('A was called'); return undefined;} function B() { console.log('B was called'); return false;} function C() { console.log('C was called'); return "foo";} console.log(A() ?? C()); console.log(B() ?? C());

上述代码运行后,控制台会输出以下结果:


A was called

C was called

foo

B was called

false

3.2 不能与 && 或 || 操作符共用

若空值合并运算符 ?? 直接与 AND(&&)和 OR(||)操作符组合使用 ?? 是不行的。这种情况下会抛出 SyntaxError。


// '||' and '??' operations cannot be mixed without parentheses.(5076) null || undefined ?? "foo"; // raises a SyntaxError // '&&' and '??' operations cannot be mixed without parentheses.(5076) true && undefined ?? "foo"; // raises a SyntaxError

但当使用括号来显式表明优先级时是可行的,比如:


(null || undefined ) ?? "foo"; // 返回 "foo"

3.3 与可选链操作符 ?. 的关系

空值合并运算符针对 undefined 与 null 这两个值,可选链式操作符 ?. 也是如此。可选链式操作符,对于访问属性可能为 undefined 与 null 的对象时非常有用。


interface Customer {

 name: string;

 city?: string;

} let customer: Customer = {

 name: "Semlinker" }; let customerCity = customer?.city ?? "Unknown city"; console.log(customerCity); // 输出:Unknown city

前面我们已经介绍了空值合并运算符的应用场景和使用时的一些注意事项,该运算符不仅可以在 TypeScript 3.7 以上版本中使用。当然你也可以在 JavaScript 的环境中使用它,但你需要借助 Babel,在 Babel 7.8.0 版本也开始支持空值合并运算符。


四、?: 可选属性

在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。 TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。


在 TypeScript 中使用 interface 关键字就可以声明一个接口:


interface Person {

 name: string;

 age: number;

} let semlinker: Person = {

 name: "semlinker",

 age: 33,

};

在以上代码中,我们声明了 Person 接口,它包含了两个必填的属性 name 和 age。在初始化 Person 类型变量时,如果缺少某个属性,TypeScript 编译器就会提示相应的错误信息,比如:


// Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.(2741) let lolo: Person  = { // Error name: "lolo" }

为了解决上述的问题,我们可以把某个属性声明为可选的:


interface Person {

 name: string;

 age?: number;

} let lolo: Person  = {

 name: "lolo" }

4.1 工具类型

4.1.1 Partial<T>

在实际项目开发过程中,为了提高代码复用率,我们可以利用 TypeScript 内置的工具类型 Partial<T> 来快速把某个接口类型中定义的属性变成可选的:


interface PullDownRefreshConfig {

 threshold: number;

 stop: number;

} /**

* type PullDownRefreshOptions = {

*   threshold?: number | undefined;

*   stop?: number | undefined;

* }

*/ type PullDownRefreshOptions = Partial<PullDownRefreshConfig>

是不是觉得 Partial<T> 很方便,下面让我们来看一下它是如何实现的:


/**

* Make all properties in T optional

*/ type Partial<T> = {

 [P in keyof T]?: T[P];

};

4.1.2 Required<T>

既然可以快速地把某个接口中定义的属性全部声明为可选,那能不能把所有的可选的属性变成必选的呢?答案是可以的,针对这个需求,我们可以使用 Required<T> 工具类型,具体的使用方式如下:


interface PullDownRefreshConfig {

 threshold: number;

 stop: number;

} type PullDownRefreshOptions = Partial<PullDownRefreshConfig> /**

* type PullDownRefresh = {

*   threshold: number;

*   stop: number;

* }

*/ type PullDownRefresh = Required<Partial<PullDownRefreshConfig>>

同样,我们来看一下 Required<T> 工具类型是如何实现的:


/**

* Make all properties in T required

*/ type Required<T> = {

 [P in keyof T]-?: T[P];

};

原来在 Required<T> 工具类型内部,通过 -? 移除了可选属性中的 ?,使得属性从可选变为必选的。


五、& 运算符

在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。


type PartialPointX = { x: number; }; type Point = PartialPointX & { y: number; }; let point: Point = {

 x: 1,

 y: 1 }

在上面代码中我们先定义了 PartialPointX 类型,接着使用 & 运算符创建一个新的 Point 类型,表示一个含有 x 和 y 坐标的点,然后定义了一个 Point 类型的变量并初始化。


5.1 同名基础类型属性的合并

那么现在问题来了,假设在合并多个类型的过程中,刚好出现某些类型存在相同的成员,但对应的类型又不一致,比如:


interface X {

 c: string;

 d: string;

} interface Y {

 c: number;

 e: string } type XY = X & Y; type YX = Y & X; let p: XY; let q: YX;

在上面的代码中,接口 X 和接口 Y 都含有一个相同的成员 c,但它们的类型不一致。对于这种情况,此时 XY 类型或 YX 类型中成员 c 的类型是不是可以是 string 或 number 类型呢?比如下面的例子:


p = { c: 6, d: "d", e: "e" };



q = { c: "c", d: "d", e: "e" };



为什么接口 X 和接口 Y 混入后,成员 c 的类型会变成 never 呢?这是因为混入后成员 c 的类型为 string & number,即成员 c 的类型既可以是 string 类型又可以是 number 类型。很明显这种类型是不存在的,所以混入后成员 c 的类型为 never。


5.2 同名非基础类型属性的合并

在上面示例中,刚好接口 X 和接口 Y 中内部成员 c 的类型都是基本数据类型,那么如果是非基本数据类型的话,又会是什么情形。我们来看个具体的例子:


interface D { d: boolean; } interface E { e: string; } interface F { f: number; } interface A { x: D; } interface B { x: E; } interface C { x: F; } type ABC = A & B & C; let abc: ABC = {

 x: {

   d: true,

   e: 'semlinker',

   f: 666 }

}; console.log('abc:', abc);

以上代码成功运行后,控制台会输出以下结果:




由上图可知,在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并。


六、| 分隔符

在 TypeScript 中联合类型(Union Types)表示取值可以为多种类型中的一种,联合类型使用 | 分隔每个类型。联合类型通常与 null 或 undefined 一起使用:


const sayHello = (name: string | undefined) => { /* ... */ };

以上示例中 name 的类型是 string | undefined 意味着可以将 string 或 undefined 的值传递给 sayHello 函数。


sayHello("semlinker");

sayHello(undefined);

此外,对于联合类型来说,你可能会遇到以下的用法:


let num: 1 | 2 = 1; type EventNames = 'click' | 'scroll' | 'mousemove';

示例中的 1、2 或 'click' 被称为字面量类型,用来约束取值只能是某几个值中的一个。


6.1 类型保护

当使用联合类型时,我们必须尽量把当前值的类型收窄为当前值的实际类型,而类型保护就是实现类型收窄的一种手段。


类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数字。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。


目前主要有四种的方式来实现类型保护:


6.1.1 in 关键字

interface Admin {

 name: string;

 privileges: string[];

} interface Employee {

 name: string;

 startDate: Date;

} type UnknownEmployee = Employee | Admin; function printEmployeeInformation(emp: UnknownEmployee) { console.log("Name: " + emp.name); if ("privileges" in emp) { console.log("Privileges: " + emp.privileges);

 } if ("startDate" in emp) { console.log("Start Date: " + emp.startDate);

 }

}

6.1.2 typeof 关键字

function padLeft(value: string, padding: string | number) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value;

 } if (typeof padding === "string") { return padding + value;

 } throw new Error(`Expected string or number, got '${padding}'.`);

}

typeof 类型保护只支持两种形式:typeof v === "typename" 和 typeof v !== typename,"typename" 必须是 "number", "string", "boolean" 或 "symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。


6.1.3 instanceof 关键字

interface Padder {

 getPaddingString(): string;

} class SpaceRepeatingPadder implements Padder { constructor(private numSpaces: number) {}

 getPaddingString() { return Array(this.numSpaces + 1).join(" ");

 }

} class StringPadder implements Padder { constructor(private value: string) {}

 getPaddingString() { return this.value;

 }

} let padder: Padder = new SpaceRepeatingPadder(6); if (padder instanceof SpaceRepeatingPadder) { // padder的类型收窄为 'SpaceRepeatingPadder' }

6.1.4 自定义类型保护的类型谓词(type predicate)

function isNumber(x: any): x is number { return typeof x === "number";

} function isString(x: any): x is string { return typeof x === "string";

}

七、_ 数字分隔符

TypeScript 2.7 带来了对数字分隔符的支持,正如数值分隔符 ECMAScript 提案中所概述的那样。对于一个数字字面量,你现在可以通过把一个下划线作为它们之间的分隔符来分组数字:


const inhabitantsOfMunich = 1_464_301; const distanceEarthSunInKm = 149_600_000; const fileSystemPermission = 0b111_111_000; const bytes = 0b1111_10101011_11110000_00001101;

分隔符不会改变数值字面量的值,但逻辑分组使人们更容易一眼就能读懂数字。以上 TS 代码经过编译后,会生成以下 ES5 代码:


"use strict"; var inhabitantsOfMunich = 1464301; var distanceEarthSunInKm = 149600000; var fileSystemPermission = 504; var bytes = 262926349;

7.1 使用限制

虽然数字分隔符看起来很简单,但在使用时还是有一些限制。比如你只能在两个数字之间添加 _ 分隔符。以下的使用方式是非法的:


// Numeric separators are not allowed here.(6188) 3_.141592 // Error 3._141592 // Error // Numeric separators are not allowed here.(6188) 1_e10 // Error 1e_10 // Error // Cannot find name '_126301'.(2304) _126301 // Error // Numeric separators are not allowed here.(6188) 126301_ // Error // Cannot find name 'b111111000'.(2304) // An identifier or keyword cannot immediately follow a numeric literal.(1351) 0_b111111000 // Error // Numeric separators are not allowed here.(6188) 0b_111111000 // Error

当然你也不能连续使用多个 _ 分隔符,比如:


// Multiple consecutive numeric separators are not permitted.(6189) 123__456 // Error

7.2 解析分隔符

此外,需要注意的是以下用于解析数字的函数是不支持分隔符:


Number()

parseInt()

parseFloat()

这里我们来看一下实际的例子:


Number('123_456') NaN parseInt('123_456') 123 parseFloat('123_456') 123

很明显对于以上的结果不是我们所期望的,所以在处理分隔符时要特别注意。当然要解决上述问题,也很简单只需要非数字的字符删掉即可。这里我们来定义一个 removeNonDigits 的函数:


const RE_NON_DIGIT = /[^0-9]/gu; function removeNonDigits(str) {

 str = str.replace(RE_NON_DIGIT, ''); return Number(str);

}

该函数通过调用字符串的 replace 方法来移除非数字的字符,具体的使用方式如下:


removeNonDigits('123_456') 123456 removeNonDigits('149,600,000') 149600000 removeNonDigits('1,407,836') 1407836

八、<Type> 语法

8.1 TypeScript 断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。


通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。


类型断言有两种形式:


8.1.1 “尖括号” 语法

let someValue: any = "this is a string"; let strLength: number = (<string>someValue).length;

8.1.2 as 语法

let someValue: any = "this is a string"; let strLength: number = (someValue as string).length;

8.2 TypeScript 泛型

对于刚接触 TypeScript 泛型的读者来说,首次看到 <T> 语法会感到陌生。其实它没有什么特别,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。




参考上面的图片,当我们调用 identity<Number>(1) ,Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 <T> 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型。


其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:


K(Key):表示对象中的键类型;

V(Value):表示对象中的值类型;

E(Element):表示元素类型。

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:


function identity <T, U>(value: T, message: U) : T { console.log(message); return value;

} console.log(identity<Number, string>(68, "Semlinker"));



除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:


function identity <T, U>(value: T, message: U) : T { console.log(message); return value;

} console.log(identity(68, "Semlinker"));

对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。


九、@XXX 装饰器

9.1 装饰器语法

对于一些刚接触 TypeScript 的小伙伴来说,在第一次看到 @Plugin({...}) 这种语法可能会觉得很惊讶。其实这是装饰器的语法,装饰器的本质是一个函数,通过装饰器我们可以方便地定义与对象相关的元数据。


@Plugin({

 pluginName: 'Device',

 plugin: 'cordova-plugin-device',

 pluginRef: 'device',

 repo: 'https://github.com/apache/cordova-plugin-device',

 platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],

}) @Injectable() export class Device extends IonicNativePlugin {}

在以上代码中,我们通过装饰器来保存 ionic-native 插件的相关元信息,而 @Plugin({...}) 中的 @ 符号只是语法糖,为什么说是语法糖呢?这里我们来看一下编译生成的 ES5 代码:


var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r;

}; var Device = /** @class */ (function (_super) {

   __extends(Device, _super); function Device() { return _super !== null && _super.apply(this, arguments) || this;

   }

   Device = __decorate([

       Plugin({ pluginName: 'Device', plugin: 'cordova-plugin-device', pluginRef: 'device', repo: 'https://github.com/apache/cordova-plugin-device', platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],

       }),

       Injectable()

   ], Device); return Device;

}(IonicNativePlugin));

通过生成的代码可知,@Plugin({...}) 和 @Injectable() 最终会被转换成普通的方法调用,它们的调用结果最终会以数组的形式作为参数传递给 __decorate 函数,而在 __decorate 函数内部会以 Device 类作为参数调用各自的类型装饰器,从而扩展对应的功能。


9.2 装饰器的分类

在 TypeScript 中装饰器分为类装饰器、属性装饰器、方法装饰器和参数装饰器四大类。


9.2.1 类装饰器

类装饰器声明:


declare type ClassDecorator = <TFunction extends Function>(

 target: TFunction

) => TFunction | void;

类装饰器顾名思义,就是用来装饰类的。它接收一个参数:


target: TFunction - 被装饰的类

看完第一眼后,是不是感觉都不好了。没事,我们马上来个例子:


function Greeter(target: Function): void {

 target.prototype.greet = function (): void { console.log("Hello Semlinker!");

 };

} @Greeter class Greeting { constructor() { // 内部实现 }

} let myGreeting = new Greeting();

myGreeting.greet(); // console output: 'Hello Semlinker!';

上面的例子中,我们定义了 Greeter 类装饰器,同时我们使用了 @Greeter 语法糖,来使用装饰器。


友情提示:读者可以直接复制上面的代码,在 TypeScript Playground 中运行查看结果。

9.2.2 属性装饰器

属性装饰器声明:


declare type PropertyDecorator = (target:Object,

 propertyKey: string | symbol ) => void;

属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:


target: Object - 被装饰的类

propertyKey: string | symbol - 被装饰类的属性名

趁热打铁,马上来个例子热热身:


function logProperty(target: any, key: string) { delete target[key]; const backingField = "_" + key; Object.defineProperty(target, backingField, {

   writable: true,

   enumerable: true,

   configurable: true }); // property getter const getter = function (this: any) { const currVal = this[backingField]; console.log(`Get: ${key} => ${currVal}`); return currVal;

 }; // property setter const setter = function (this: any, newVal: any) { console.log(`Set: ${key} => ${newVal}`); this[backingField] = newVal;

 }; // Create new property with getter and setter Object.defineProperty(target, key, { get: getter, set: setter,

   enumerable: true,

   configurable: true });

} class Person { @logProperty public name: string; constructor(name : string) { this.name = name;

 }

} const p1 = new Person("semlinker");

p1.name = "kakuqo";

以上代码我们定义了一个 logProperty 函数,来跟踪用户对属性的操作,当代码成功运行后,在控制台会输出以下结果:


Set: name => semlinker Set: name => kakuqo

9.2.3 方法装饰器

方法装饰器声明:


declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,          

 descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:


target: Object - 被装饰的类

propertyKey: string | symbol - 方法名

descriptor: TypePropertyDescript - 属性描述符

废话不多说,直接上例子:


function LogOutput(tarage: Function, key: string, descriptor: any) { let originalMethod = descriptor.value; let newMethod = function(...args: any[]): any { let result: any = originalMethod.apply(this, args); if(!this.loggedOutput) { this.loggedOutput = new Array<any>();

   } this.loggedOutput.push({

     method: key,

     parameters: args,

     output: result,

     timestamp: new Date()

   }); return result;

 };

 descriptor.value = newMethod;

} class Calculator { @LogOutput double (num: number): number { return num * 2;

 }

} let calc = new Calculator();

calc.double(11); // console ouput: [{method: "double", output: 22, ...}] console.log(calc.loggedOutput);

9.2.4 参数装饰器

参数装饰器声明:


declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,

 parameterIndex: number ) => void

参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:


target: Object - 被装饰的类

propertyKey: string | symbol - 方法名

parameterIndex: number - 方法中参数的索引值

function Log(target: Function, key: string, parameterIndex: number) { let functionLogged = key || target.prototype.constructor.name; console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has

   been decorated`);

} class Greeter {

 greeting: string; constructor(@Log phrase: string) { this.greeting = phrase;

 }

} // console output: The parameter in position 0  // at Greeter has been decorated

十、#XXX 私有字段

在 TypeScript 3.8 版本就开始支持 ECMAScript 私有字段,使用方式如下:


class Person {

 #name: string; constructor(name: string) { this.#name = name;

 }


 greet() { console.log(`Hello, my name is ${this.#name}!`);

 }

} let semlinker = new Person("Semlinker");


semlinker.#name; //     ~~~~~ // Property '#name' is not accessible outside class 'Person' // because it has a private identifier.

与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:


私有字段以 # 字符开头,有时我们称之为私有名称;

每个私有字段名称都唯一地限定于其包含的类;

不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);

私有字段不能在包含的类之外访问,甚至不能被检测到。

10.1 私有字段与 private 的区别

说到这里使用 # 定义的私有字段与 private 修饰符定义字段有什么区别呢?现在我们先来看一个 private 的示例:


class Person { constructor(private name: string){}

} let person = new Person("Semlinker"); console.log(person.name);

在上面代码中,我们创建了一个 Person 类,该类中使用 private 修饰符定义了一个私有属性 name,接着使用该类创建一个 person 对象,然后通过 person.name 来访问 person 对象的私有属性,这时 TypeScript 编译器会提示以下异常:


Property 'name' is private and only accessible within class 'Person'.(2341)

那如何解决这个异常呢?当然你可以使用类型断言把 person 转为 any 类型:


console.log((person as any).name);

通过这种方式虽然解决了 TypeScript 编译器的异常提示,但是在运行时我们还是可以访问到 Person 类内部的私有属性,为什么会这样呢?我们来看一下编译生成的 ES5 代码,也许你就知道答案了:


var Person = /** @class */ (function () { function Person(name) { this.name = name;

   } return Person;

}()); var person = new Person("Semlinker"); console.log(person.name);

这时相信有些小伙伴会好奇,在 TypeScript 3.8 以上版本通过 # 号定义的私有字段编译后会生成什么代码:


class Person {

 #name: string; constructor(name: string) { this.#name = name;

 }


 greet() { console.log(`Hello, my name is ${this.#name}!`);

 }

}

以上代码目标设置为 ES2015,会编译生成以下代码:


"use strict"; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet)

 || function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance");

   }

   privateMap.set(receiver, value); return value;

}; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet)

 || function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance");

   } return privateMap.get(receiver);

}; var _name; class Person { constructor(name) {

     _name.set(this, void 0);

     __classPrivateFieldSet(this, _name, name);

   }

   greet() { console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`);

   }

}

_name = new WeakMap();

通过观察上述代码,使用 # 号定义的 ECMAScript 私有字段,会通过 WeakMap 对象来存储,同时编译器会生成 __classPrivateFieldSet 和 __classPrivateFieldGet 这两个方法用于设置值和获取值。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

8个JavaScript库可更好地处理本地存储

seo达人

Local Storage Bridge

https://github.com/krasimir/l...

如果你必须在同一个浏览器中从一个标签页发送消息到另一个标签页,你不必用艰难的方式。Local storage bridge在这里让任务变得更简单。

基本使用:

// 发送 lsbridge.send(‘app.message.error’, { error: ‘Out of memory’ });

// 监听 lsbridge.subscribe(‘app.message.error’, function(data) { console.log(data); // { error: ‘Out of memory’ } });

Basil.js

image

Basil.js统一了session、localStorage和cookie,为你提供了一种处理数据的直接方法。

基本使用:

let basil = new Basil(options);

basil.set(‘name’, ‘Amy’);
basil.get(‘name’);
basil.remove(‘name’);
basil.reset();

store.js

https://github.com/marcuswest...

Store.js像其他东西一样处理数据存储。但还有更多的功能,它的一个高级特性是让你更深入地访问浏览器支持。

基本使用:

store.set(‘book’, { title: ‘JavaScript’ }); // Store a book store.get(‘book’);

// Get stored book store.remove(‘book’); // Remove stored book store.clearAll(); // Clear all keys

lscache

https://github.com/pamelafox/...

它与localStorage API类似。事实上,它是localStorage的一个封装器,并使用HTML5模拟memcaches函数。在上面的文档中发现更多的功能。

基本使用:

lscache.set(‘name’, ‘Amy’, 5); // 数据将在5分钟后过期 lscache.get(‘name’);

Lockr

image

Lockr建立在localStorage API之上。它提供了一些有用的方法来更轻松地处理本地数据。

是什么让你要使用此库而不是localStorage API?

好吧,localStorage API仅允许你存储字符串。如果要存储数字,则需要先将该数字转换为字符串。在Lockr中不会发生这种情况,因为Lockr允许你存储更多的数据类型甚至对象。

基本使用:

Lockr.set(‘name’, ‘Amy’);
Lockr.set(‘age’, 28);
Lockr.set(‘books’, [{title: ‘JavaScript’, price: 11.0}, {title: ‘Python’, price: 9.0}]);

Barn

https://github.com/arokor/barn

Barn在localStorage之上提供了一个类似Redis的API。如果持久性很重要,那么你将需要这个库来保持数据状态,以防发生错误。

基本使用:

let barn = new Barn(localStorage); // 原始类型 barn.set(‘name’, ‘Amy’); let name = barn.get(‘name’);

// Amy // List barn.lpush(‘names’, ‘Amy’);

barn.lpush(‘names’, ‘James’); let name1 = barn.rpop(‘names’); // Amy let name2 = barn.rpop(‘names’);

// James

localForage

https://github.com/localForag...

这个简单而快速的库将通过IndexedDB或WebSQL使用异步存储来改善Web的脱机体验。它类似于localStorage,但具有回调功能。

基本使用:

localforage.setItem(‘name’, ‘Amy’, function(error, value) { // Do something });

localforage.getItem(‘name’, function(error, value) { if (error) { console.log(‘an error occurs’);
  } else { // Do something with the value }
});

很神奇的是它提供中文文档

crypt.io

https://github.com/jas-/crypt.io

crypt.io使用标准JavaScript加密库实现安全的浏览器存储。使用crypto.io时,有三个存储选项:sessionStorage,localStorage或cookie。

基本使用:

let storage = crypto; let book = { title: ‘JavaScript’, price: 13 };

storage.set(‘book’, book, function(error, results) { if (error) { throw error;
  } // Do something });

storage.get(‘book’, function(error, results) { if (error) { throw error; 

} // Do something });

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

Promise 你真的用明白了么?

seo达人

前置知识

在开始正文前,我们先把本文涉及到的一些内容提前定个基调。

Promise 哪些 API 涉及了微任务?

Promise 中只有涉及到状态变更后才需要被执行的回调才算是微任务,比如说 then、 catch 、finally ,其他所有的代码执行都是宏任务(同步执行)。

上图中蓝色为同步执行,黄色为异步执行(丢到微任务队列中)。

这些微任务何时被加入微任务队列?

这个问题我们根据 ecma 规范来看:

  • 如果此时 Promise 状态为 pending,那么成功或失败的回调会分别被加入至 [[PromiseFulfillReactions]] 和 [[PromiseRejectReactions]] 中。如果你看过手写 Promise 的代码的话,应该能发现有两个数组存储这些回调函数。
  • 如果此时 Promise 状态为非 pending 时,回调会成为 Promise Jobs,也就是微任务。

了解完以上知识后,正片开始。

同一个 then,不同的微任务执行

初级

Promise.resolve()
  .then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
    });
  })
  .then(() => { console.log("then2");
  });

以上代码大家应该都能得出正确的答案:then1 → then1-1 → then2

虽然 then 是同步执行,并且状态也已经变更。但这并不代表每次遇到 then 时我们都需要把它的回调丢入微任务队列中,而是等待 then 的回调执行完毕后再根据情况执行对应操作。

基于此,我们可以得出第一个结论:链式调用中,只有前一个 then 的回调执行完毕后,跟着的 then 中的回调才会被加入至微任务队列。

中级

大家都知道了 Promise resolve 后,跟着的 then 中的回调会马上进入微任务队列。

那么以下代码你认为的输出会是什么?

let p = Promise.resolve();

p.then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
  });
}).then(() => { console.log("then1-2");
});

p.then(() => { console.log("then2");
}); 

按照一开始的认知我们不难得出 then2 会在 then1-1 后输出,但是实际情况却是相反的。

基于此我们得出第二个结论:每个链式调用的开端会首先依次进入微任务队列。

接下来我们换个写法:

let p = Promise.resolve().then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
  });
}).then(() => { console.log("then2");
});

p.then(() => { console.log("then3");
});

上述代码其实有个陷阱,then 每次都会返回一个新的 Promise,此时的 p 已经不是 Promise.resolve() 生成的,而是最后一个 then 生成的,因此 then3 应该是在 then2 后打印出来的。

顺便我们也可以把之前得出的结论优化为:同一个 Promise 的每个链式调用的开端会首先依次进入微任务队列。

高级

以下大家可以猜猜 then1-2 会在何时打印出来?

Promise.resolve()
  .then(() => { console.log("then1"); Promise.resolve()
      .then(() => { console.log("then1-1"); return 1;
      })
      .then(() => { console.log("then1-2");
      });
  })
  .then(() => { console.log("then2");
  })
  .then(() => { console.log("then3");
  })
  .then(() => { console.log("then4");
  });

这题肯定是简单的,记住第一个结论就能得出答案,以下是解析:

  • 第一次 resolve 后第一个 then 的回调进入微任务队列并执行,打印 then1
  • 第二次 resolve 后内部第一个 then 的回调进入微任务队列,此时外部第一个 then 的回调全部执行完毕,需要将外部的第二个 then 回调也插入微任务队列。
  • 执行微任务,打印 then1-1 和 then2,然后分别再将之后 then 中的回调插入微任务队列
  • 执行微任务,打印 then1-2 和 then3 ,之后的内容就不一一说明了

接下来我们把 return 1 修改一下,结果可就大不相同啦:

Promise.resolve()
  .then(() => { console.log("then1"); Promise.resolve()
      .then(() => { console.log("then1-1"); return Promise.resolve();
      })
      .then(() => { console.log("then1-2");
      });
  })
  .then(() => { console.log("then2");
  })
  .then(() => { console.log("then3");
  })
  .then(() => { console.log("then4");
  });

当我们 return Promise.resolve() 时,你猜猜 then1-2 会何时打印了?

答案是最后一个才被打印出来。

为什么在 then 中分别 return 不同的东西,微任务的执行顺序竟有如此大的变化?以下是笔者的解析。

PS:then 返回一个新的 Promise,并且会用这个 Promise 去 resolve 返回值,这个概念需要大家先了解一下。

根据 Promise A+ 规范

根据规范 2.3.2,如果 resolve 了一个 Promise,需要为其加上一个 then 并 resolve

if (x instanceof MyPromise) { if (x.currentState === PENDING) {
  } else {
    x.then(resolve, reject);
  } return;
}

上述代码节选自手写 Promise 实现。

那么根据 A+ 规范来说,如果我们在 then 中返回了 Promise.resolve 的话会多入队一次微任务,但是这个结论还是与实际不符的,因此我们还需要寻找其他权威的文档。

根据 ECMA - 262 规范

根据规范 25.6.1.3.2,当 Promise resolve 了一个 Promise 时,会产生一个NewPromiseResolveThenableJob,这是属于 Promise Jobs 中的一种,也就是微任务。

This Job uses the supplied thenable and its then method to resolve the given promise. This process must take place as a Job to ensure that the evaluation of the then method occurs after evaluation of any surrounding code has completed.

并且该 Jobs 还会调用一次 then 函数来 resolve Promise,这也就又生成了一次微任务。

这就是为什么会触发两次微任务的来源。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

日历

链接

blogger

蓝蓝 http://www.lanlanwork.com

存档