构建库的常见方法有两种:一种是自己手动构建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界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务目录
定义函数的方式有三种:
new Function(一般不用)
-
// 函数的声明
-
function fn() {
-
console.log("我是JS中的一等公民-函数!!!哈哈");
-
}
-
fn();
函数表达式就是将一个匿名函数赋值给一个变量。函数表达式必须先声明,再调用。
-
// 函数表达式
-
var fn = function() {
-
console.log("我是JS中的一等公民-函数!!!哈哈");
-
};
-
fn();
下面是一个根据条件定义函数的例子:
-
if (true) {
-
function f () {
-
console.log(1)
-
}
-
} else {
-
function f () {
-
console.log(2)
-
}
-
}
以上代码执行结果在不同浏览器中结果不一致。我们可以使用函数表达式解决上面的问题:
-
var f
-
-
if (true) {
-
f = function () {
-
console.log(1)
-
}
-
} else {
-
f = function () {
-
console.log(2)
-
}
-
}
函数声明如果放在if-else的语句中,在IE8的浏览器中会出现问题,所以为了更好的兼容性我们以后最好用函数表达式,不用函数声明的方式。
在前面的学习中我们了解到函数也是对象。注意:函数是对象,对象不一定是函数,对象中有__proto__原型,函数中有prototype原型,如果一个东西里面有prototype,又有__proto__,说明它是函数,也是对象。
-
function F1() {}
-
-
console.dir(F1); // F1里面有prototype,又有__proto__,说明是函数,也是对象
-
-
console.dir(Math); // Math中有__proto__,但是没有prorotype,说明Math不是函数
对象都是由构造函数创建出来的,函数既然是对象,创建它的构造函数又是什么呢?事实上所有的函数实际上都是由Function构造函数创建出来的实例对象。
所以我们可以使用Function构造函数创建函数。
语法:new Function(arg1,arg2,arg3..,body);
arg是任意参数,字符串类型的。body是函数体。
-
// 所有的函数实际上都是Function的构造函数创建出来的实例对象
-
var f1 = new Function("num1", "num2", "return num1+num2");
-
console.log(f1(10, 20));
-
console.log(f1.__proto__ == Function.prototype);
-
-
// 所以,函数实际上也是对象
-
console.dir(f1);
-
console.dir(Function);
-
// 普通函数
-
function f1() {
-
console.log("我是普通函数");
-
}
-
f1();
-
-
// 构造函数---通过new 来调用,创建对象
-
function F1() {
-
console.log("我是构造函数");
-
}
-
var f = new F1();
-
-
// 对象的方法
-
function Person() {
-
this.play = function() {
-
console.log("我是对象中的方法");
-
};
-
}
-
var per = new Person();
-
per.play();
this 的指向
函数的调用方式决定了 this 指向的不同:
| 调用方式 | 非严格模式 | 备注 |
|---|---|---|
| 普通函数调用 | window | 严格模式下是 undefined |
| 构造函数调用 | 实例对象 | 原型方法中 this 也是实例对象 |
| 对象方法调用 | 该方法所属对象 | 紧挨着的对象 |
| 事件绑定方法 | 绑定事件对象 | |
| 定时器函数 | window |
-
// 普通函数
-
function f1() {
-
console.log(this); // window
-
}
-
f1();
-
-
// 构造函数
-
function Person() {
-
console.log(this); // Person
-
// 对象的方法
-
this.sayHi = function() {
-
console.log(this); // Person
-
};
-
}
-
// 原型中的方法
-
Person.prototype.eat = function() {
-
console.log(this); // Person
-
};
-
var per = new Person();
-
console.log(per); // Person
-
per.sayHi();
-
per.eat();
-
-
// 定时器中的this
-
setInterval(function() {
-
console.log(this); // window
-
}, 1000);
了解了函数 this 的指向之后,我们知道在一些情况下我们为了使用某种特定环境的 this 引用,需要采用一些特殊手段来处理,例如我们经常在定时器外部备份 this 引用,然后在定时器函数内部使用外部 this 的引用。
然而实际上 JavaScript 内部已经专门为我们提供了一些函数方法,用来帮我们更优雅的处理函数内部 this 指向问题。这就是接下来我们要学习的 call、apply、bind 三个函数方法。call()、apply()、bind()这三个方法都是是用来改变this的指向的。
call() 方法调用一个函数, 其具有一个指定的 this 值和分别地提供的参数(参数的列表)。
apply() 方法调用一个函数, 其具有一个指定的 this 值,以及作为一个数组(或类似数组的对象)提供的参数。
注意:call() 和 apply() 方法类似,只有一个区别,就是 call() 方法接受的是若干个参数的列表,而 apply() 方法接受的是一个包含多个参数的数组。
call语法:
fun.call(thisArg[, arg1[, arg2[, ...]]])
call参数:
thisArg
arg1, arg2, ...
apply语法:
fun.apply(thisArg, [argsArray])
apply参数:
thisArg
argsArray
apply() 与 call() 相似,不同之处在于提供参数的方式。
apply() 使用参数数组而不是一组参数列表。例如:
fun.apply(this, ['eat', 'bananas'])
-
function f1(x, y) {
-
console.log("结果是:" + (x + y) + this);
-
return "666";
-
}
-
f1(10, 20); // 函数的调用
-
-
console.log("========");
-
-
// apply和call方法也是函数的调用的方式
-
// 此时的f1实际上是当成对象来使用的,对象可以调用方法
-
// apply和call方法中如果没有传入参数,或者是传入的是null,那么调用该方法的函数对象中的this就是默认的window
-
f1.apply(null, [10, 20]);
-
f1.call(null, 10, 20);
-
-
// apply和call都可以让函数或者方法来调用,传入参数和函数自己调用的写法不一样,但是效果是一样的
-
var result1 = f1.apply(null, [10, 20]);
-
var result2 = f1.call(null, 10, 20);
-
console.log(result1);
-
console.log(result2);
-
// 通过apply和call改变this的指向
-
function Person(name, sex) {
-
this.name = name;
-
this.sex = sex;
-
}
-
//通过原型添加方法
-
Person.prototype.sayHi = function(x, y) {
-
console.log("您好啊:" + this.name);
-
return x + y;
-
};
-
var per = new Person("小三", "男");
-
var r1 = per.sayHi(10, 20);
-
-
console.log("==============");
-
-
function Student(name, age) {
-
this.name = name;
-
this.age = age;
-
}
-
var stu = new Student("小舞", 18);
-
var r2 = per.sayHi.apply(stu, [10, 20]);
-
var r3 = per.sayHi.call(stu, 10, 20);
-
-
console.log(r1);
-
console.log(r2);
-
console.log(r3);
apply和call都可以改变this的指向。调用函数的时候,改变this的指向:
-
// 函数的调用,改变this的指向
-
function f1(x, y) {
-
console.log((x + y) + ":===>" + this);
-
return "函数的返回值";
-
}
-
//apply和call调用
-
var r1 = f1.apply(null, [1, 2]); // 此时f1中的this是window
-
console.log(r1);
-
var r2 = f1.call(null, 1, 2); // 此时f1中的this是window
-
console.log(r2);
-
console.log("=============>");
-
//改变this的指向
-
var obj = {
-
sex: "男"
-
};
-
// 本来f1函数是window对象的,但是传入obj之后,f1的this此时就是obj对象
-
var r3 = f1.apply(obj, [1, 2]); //此时f1中的this是obj
-
console.log(r3);
-
var r4 = f1.call(obj, 1, 2); //此时f1中的this是obj
-
console.log(r4);

调用方法的时候,改变this的指向:
-
//方法改变this的指向
-
function Person(age) {
-
this.age = age;
-
}
-
Person.prototype.sayHi = function(x, y) {
-
console.log((x + y) + ":====>" + this.age); //当前实例对象
-
};
-
-
function Student(age) {
-
this.age = age;
-
}
-
var per = new Person(10); // Person实例对象
-
var stu = new Student(100); // Student实例对象
-
// sayHi方法是per实例对象的
-
per.sayHi(10, 20);
-
per.sayHi.apply(stu, [10, 20]);
-
per.sayHi.call(stu, 10, 20);
总结
apply的使用语法:
1 函数名字.apply(对象,[参数1,参数2,...]);
2 方法名字.apply(对象,[参数1,参数2,...]);
call的使用语法
1 函数名字.call(对象,参数1,参数2,...);
2 方法名字.call(对象,参数1,参数2,...);
它们的作用都是改变this的指向,不同的地方是参数传递的方式不一样。
如果想使用别的对象的方法,并且希望这个方法是当前对象的,就可以使用apply或者是call方法改变this的指向。
bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也可以接受预设的参数提供给原函数。一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
bind方法是复制的意思,本质是复制一个新函数,参数可以在复制的时候传进去,也可以在复制之后调用的时候传入进去。apply和call是调用的时候改变this指向,bind方法,是复制一份的时候,改变了this的指向。
语法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
参数:
thisArg
arg1, arg2, ...
返回值:
返回由指定的this值和初始化参数改造的原函数的拷贝。
示例1:
-
function Person(name) {
-
this.name = name;
-
}
-
Person.prototype.play = function() {
-
console.log(this + "====>" + this.name);
-
};
-
-
function Student(name) {
-
this.name = name;
-
}
-
var per = new Person("人");
-
var stu = new Student("学生");
-
per.play();
-
// 复制了一个新的play方法
-
var ff = per.play.bind(stu);
-
ff();
示例2:
-
//通过对象,调用方法,产生随机数
-
function ShowRandom() {
-
//1-10的随机数
-
this.number = parseInt(Math.random() * 10 + 1);
-
}
-
//添加原型方法
-
ShowRandom.prototype.show = function() {
-
//改变了定时器中的this的指向了
-
window.setTimeout(function() {
-
//本来应该是window, 现在是实例对象了
-
//显示随机数
-
console.log(this.number);
-
}.bind(this), 1000);
-
};
-
//实例对象
-
var sr = new ShowRandom();
-
//调用方法,输出随机数字
-
sr.show();
call 和 apply 特性一样
this 的指向
null 或者 undefined 则内部 this 指向 window
bind
-
function fn(x, y, z) {
-
console.log(fn.length) // => 形参的个数
-
console.log(arguments) // 伪数组实参参数集合
-
console.log(arguments.callee === fn) // 函数本身
-
console.log(fn.caller) // 函数的调用者
-
console.log(fn.name) // => 函数的名字
-
}
-
-
function f() {
-
fn(10, 20, 30)
-
}
-
-
f()
函数可以作为参数,也可以作为返回值。
函数是可以作为参数使用,函数作为参数的时候,如果是命名函数,那么只传入命名函数的名字,没有括号。
-
function f1(fn) {
-
console.log("我是函数f1");
-
fn(); // fn是一个函数
-
}
-
-
//传入匿名函数
-
f1(function() {
-
console.log("我是匿名函数");
-
});
-
// 传入命名函数
-
function f2() {
-
console.log("我是函数f2");
-
}
-
f1(f2);

作为参数排序案例:
-
var arr = [1, 100, 20, 200, 40, 50, 120, 10];
-
//排序---函数作为参数使用,匿名函数作为sort方法的参数使用,此时的匿名函数中有两个参数,
-
arr.sort(function(obj1, obj2) {
-
if (obj1 > obj2) {
-
return -1;
-
} else if (obj1 == obj2) {
-
return 0;
-
} else {
-
return 1;
-
}
-
});
-
console.log(arr);
-
function f1() {
-
console.log("函数f1");
-
return function() {
-
console.log("我是函数,此时作为返回值使用");
-
}
-
-
}
-
-
var ff = f1();
-
ff();
作为返回值排序案例:
-
// 排序,每个文件都有名字,大小,时间,可以按照某个属性的值进行排序
-
// 三个文件,文件有名字,大小,创建时间
-
function File(name, size, time) {
-
this.name = name; // 名字
-
this.size = size; // 大小
-
this.time = time; // 创建时间
-
}
-
var f1 = new File("jack.avi", "400M", "1999-12-12");
-
var f2 = new File("rose.avi", "600M", "2020-12-12");
-
var f3 = new File("albert.avi", "800M", "2010-12-12");
-
var arr = [f1, f2, f3];
-
-
function fn(attr) {
-
// 函数作为返回值
-
return function getSort(obj1, obj2) {
-
if (obj1[attr] > obj2[attr]) {
-
return 1;
-
} else if (obj1[attr] == obj2[attr]) {
-
return 0;
-
} else {
-
return -1;
-
}
-
}
-
}
-
console.log("按照名字排序:**********");
-
// 按照名字排序
-
var ff = fn("name");
-
// 函数作为参数
-
arr.sort(ff);
-
for (var i = 0; i < arr.length; i++) {
-
console.log(arr[i].name + "====>" + arr[i].size + "===>" + arr[i].time);
-
}
-
-
console.log("按照大小排序:**********");
-
// 按照大小排序
-
var ff = fn("size");
-
// 函数作为参数
-
arr.sort(ff);
-
for (var i = 0; i < arr.length; i++) {
-
console.log(arr[i].name + "====>" + arr[i].size + "===>" + arr[i].time);
-
}
-
-
console.log("按照创建时间排序:**********");
-
// 按照创建时间排序
-
var ff = fn("time");
-
// 函数作为参数
-
arr.sort(ff);
-
for (var i = 0; i < arr.length; i++) {
-
console.log(arr[i].name + "====>" + arr[i].size + "===>" + arr[i].time);
-
}
-
无缝轮播一直是面试的热门题目,而大部分答案都是复制第一张到最后。诚然,这种方法是非常标准,那么有没有另类一点的方法呢?
第一种方法是需要把所有图片一张张摆好,然后慢慢移动的,
但是我能不能直接不摆就硬移动呢?
如果你使用过vue的transition,我们是可以通过给每一张图片来添加入场动画和离场动画来模拟这个移动
这样看起来的效果就是图片从右边一直往左移动,但是这个不一样的地方是,我们每一个元素都有这个进场动画和离场动画,我们根本不用关心它是第几个元素,你只管轮播就是。
很简单,我们自己实现一个transtition的效果就好啦,主要做的是以下两点
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)
}
手机UI中的交互是保持产品鲜活生命力的源动力。好的交互可以帮助用户快速地获得反馈,认知布局,增强体验感和沉浸感。
手机UI中的交互是保持产品鲜活生命力的源动力。好的交互可以帮助用户快速地获得反馈,认知布局,增强体验感和沉浸感。这里为大家整理了一些优秀并富有创意的交互作品,为你的产品设计注入灵感。
--手机appUI设计--
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
更多精彩文章:
写在前面
在平时的设计过程当中,你可能会有这样的疑惑,为什么在大部分APP中,当单个按钮和多个按钮同时存在时,最重要的按钮一般都会放置在页面的右侧呢?如果最重要的按钮放在左侧又有什么问题呢?按钮放在右侧的原因是什么呢?它又有什么理论依据呢?接下来带着这些疑问,开始我们今天所要介绍的内容:交互心理学之古腾堡原则
古腾堡原则的起源
古腾堡原则是由14世纪西方活字印刷术的发明人约翰·古腾堡提出,早在20世纪50年代,他在设计报纸的过程中,提出了一项原则,认为人的阅读方式应该是遵循某种习惯进行的,就像读书一样,由左到右,从上到下。这其中蕴含着什么信息呢?经过研究最终得出被后人所熟知的结论:古腾堡原则,并附上了一张图,名为「古腾堡图」。古腾堡图将画面所呈现的内容分成四个象限:
1、第一视觉区(POA):左上方,用户首先注意到的地方
2、强休息区(SFA):右上方,较少被注意到
3、弱休息区(WFA):左下方,最少被注意到
4、终端视觉区(TA):右下方,视觉流终点
从图中可以看出,用户视线很自然的会从第一视觉区开始,逐渐移动到终端休息区。整个阅读过程视线都会沿着一条方向轴开始从左到右浏览。用户会更容易关注到页面的开始与结束区域,而中间的段落则很少被关注到。古腾堡揭示了一个实用的视觉轨迹规律:阅读引力是从上到下,从左到右。
遵循古腾堡原则把关键信息放在左上角、中间和右下角,能够更好的体现元素的重要性。例如:我们平时所看到的页面弹窗、各种证明文件和合同文件等等。
古腾堡图通过对设计元素的重量与元素布局和组成方式进行调和,指导眼睛的运动轨迹。让用户迅速获取有价值的信息,同时用户对信息的熟悉程度也是影响眼睛运动轨迹的因素之一。
而随着互联网的兴起,古腾堡原则也逐渐被应用到APP设计和网页设计当中。接下来让我们来看看他在界面中的实际应用。
在设计中的应用
1.1 底部单个按钮
这种形式在引导用户操作的页面中最为常见,为了能够保证用户对内容进行阅读,所以将按钮摆放在页面底部,内容放在顶部,这样的摆放即符合用户由上到下的阅读习惯又达到了产品预期的目标。
1.2 底部垂直双按钮
上面我们提到了单个按钮的摆放思路,接下来看一下垂直双按钮的摆放思路是怎么样的。如果一个界面上同时存在两个优先级不同的按钮,并且产品希望用户对每一个按钮都有足够的关注度,那么垂直摆放是最佳选择,虽然垂直双按钮在样式上做了区分,但用户同样会停留一段时间将按钮的内容进行对比思考。
那么,按照古腾堡原则,重要的按钮应该放在页面最底部,原则上它应该是这样的:
仔细观察上图,有没有发现浅色按钮很容易被忽略掉,这样就违背了产品要保证每一个按钮都要有足够关注度的初衷,所以我们要违背古腾堡原则来满足业务需求,正如我们所看到的微信授权页面一样,
为了保证「同意」与「拒绝」这两个独立的按钮能够被用户足够的重视,并且其中的任意一个按钮不会被轻易的忽略掉,这里将「同意」按钮颜色加重,并且放在「拒绝」按钮之上,让眼睛原本垂直向下的运动轨迹产生回流的变化。
小结
原则是设计的基础,并非一成不变,要合理权衡设计原则与产品目标之间的关系。
2、顶部按钮分析
由于顶部导航栏空间有限,导致按钮相对较小,并且不便于点击操作,所以这类顶部按钮适用于修改内容的编辑页面,即可以避免误触,又可以让用户关注内容本身。关键按钮至于顶部,还可以缩短用户眼睛的运动路径,让用户更容易注意到其状态的变化状态。
小结
顶部按钮更关注可编辑的内容区域,并非按钮。而底部按钮则更关注按钮本身。并非内容。
3、水平按钮分析
除了上面提到的顶部按钮和底部按钮,还有水平摆放的按钮,比如淘宝详情页、京东详情页、网易严选详情页的「加入购物车」和「立即购买」按钮,界面中的「立即购买」按钮都放在了右下角,结合古腾堡原则的视觉终点说明,右下角为视觉终端区域,即视觉最终停留的位置,所以他们都将与转化率密切相关的「立即购买」按钮放在了界面的右下角,让用户更容易关注到。
再比如比较常见的「取消」和「确认」弹窗样式,通常是在需要让用户确认某种操作行为时出现,有可能是提交表单、协议授权、获取用户信息等等,为了防止用户误操作,这也是提升产品体验的小细节。
平常我们所看到的弹窗,推荐按钮都是在右侧,那么将推荐按钮放在左侧会怎么样?如下图所示:
不难看出推荐按钮放在右侧后,视觉在水平方向轴上产生了回流。
弹窗的目的是想让用户点击「确认」按钮,如果将「确认」放在左侧,根据古腾堡原则,用户的视线会不由自主的向右侧移动,也就是「取消」按钮的位置,想要回到左侧「确认」按钮位置就需要移动视线,并且眼睛的运动轨迹会在水平方向轴上来回的往复运动,无形中增加了用户选择时长。如果将「确认」放在右侧,「取消」放在左侧则可以为用户提高操作效率。
在实际产品中的应用案例:
小结
当产品想要让用户进行某种操作时,主要按钮放在右边
总结
1、古腾堡图第一视觉区,强休息区,弱休息区,终端视觉区
2、原则是设计的基础,并非一成不变,要合理权衡设计原则与产品目标之间的关系
3、顶部按钮更关注可编辑的内容区域,并非按钮。而底部按钮则更关注按钮本身。并非内容
4、当产品想要让用户进行某种操作时,主要按钮放在右边
文章来源:UI中国 作者:Coldrain1
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
垂直居中基本上是入门 CSS 必须要掌握的问题了,我们肯定在各种教程中都看到过“CSS 垂直居中的 N 种方法”,通常来说,这些方法已经可以满足各种使用场景了,然而当我们碰到了需要使用某些特殊字体进行混排、或者使文字对齐图标的情况时,也许会发现,无论使用哪种垂直居中的方法,总是感觉文字向上或向下偏移了几像素,不得不专门对它们进行位移,为什么会出现这种情况呢?
下图是一个使用各种常见的垂直居中的方法来居中文字的示例,其中涉及到不同字体的混排,可以看出,虽然这里面用了几种常用的垂直居中的方法,但是在实际的观感上这些文字都没有恰好垂直居中,有些文字看起来比较居中,而有些文字则偏移得很厉害。
在线查看:CodePen(字体文件直接引用了谷歌字体,如果没有效果需要注意网络情况)
通过设置vertical-align:middle对文字进行垂直居中时,父元素需要设置font-size: 0,因为vertical-align:middle是将子元素的中点与父元素的baseline + x-height / 2的位置进行对齐的,设置字号为 0 可以保证让这些线的位置都重合在中点。
我们用鼠标选中这些文字,就能发现选中的区域确实是在父层容器里垂直居中的,那么为什么文字却各有高低呢?这里就涉及到了字体本身的构造和相关的度量值。
这里先提出一个问题,我们在 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
字体本身还有很多概念和度量值(metrics),这里介绍几个常见的概念,以维基百科的这张图为例(下面的度量值的计量单位均为基于 em 的相对单位):
接下来我们在 FontForge 软件里看看这些值的取值,这里以 Arial 字体给出一个例子:
从图中可以看出,在 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 和 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 的高度。
在中间插一个问题,我们应该都使用过 line-height 来给文字进行垂直居中,那么 line-height 实际是以字体的哪个部分的中点进行计算呢?为了验证这个问题,我新建了一个很有“设计感”的字体,em size 设为 1000,ascent 为 800,descent 为 200,并对其分别设置了正常的和比较夸张的 metrics:
上面图中左边是 FontForge 里设置的 metrics,右边是实际显示效果,文字字号设为 100px,四个字母均在父层的 flex 布局下垂直居中,四个字母的 line-height 分别为 0、1em、normal、3em,红色边框是元素的 outline,黄色背景是鼠标选取的背景。由上面两张图可以看出,字体的 metrics 对文字渲染位置的影响还是很大的。同时可以看出,在设置 line-height 时,虽然 line gap 参与了撑起取值为 normal 的空间,但是不参与文字垂直居中的计算,即垂直居中的中点始终是 content-area 的中点。
我们又对字体进行了微调,使其 ascent 有一定偏移,这时可以看出 1em 行高的文字 outline 恰好在正中间,因此可以得出结论:在浏览器进行渲染时,em square 总是相对于 content-area 垂直居中。
说完了字体构造,又回到上一节的问题,为什么不同字体文字混排的时候进行垂直居中,文字各有高低呢?
在这个问题上,本文给出这样一个结论,那就是因为不同字体的各项度量值均不相同,在进行垂直居中布局时,content-area 的中点与视觉的中点不统一,因此导致实际看起来存在位置偏移,下面这张图是 Arial 字体的几个中线位置:
从图上可以看出来,大写字母和小写字母的视觉中线与整个字符的中线还是存在一定的偏移的。这里我没有找到排版相关学科的定论,究竟以哪条线进行居中更符合人眼观感的居中,以我个人的观感来看,大写字母的中线可能看起来更加舒服一点(尤其是与没有小写字母的内容进行混排的时候)。
需要注意一点,这里选择的 Arial 这个字体本身的偏移比较少,所以使用时整体感觉还是比较居中的,这并不代表其他字体也都是这样。
对于中文字体,本身的设计上没有基线、升部、降部等说法,每个字都在一个方形盒子中。但是在计算机上显示时,也在一定程度上沿用了西文字体的概念,通常来说,中文字体的方形盒子中文字体底端在 baseline 和 descender 之间,顶端超出一点 ascender,而标点符号正好在 baseline 上。
我们已经了解了字体的相关概念,那么如何解决在使用字体时出现的偏移问题呢?
通过上面的内容可以知道,文字显示的偏移主要是视觉上的中点和渲染时的中点不一致导致的,那么我们只要把这个不一致修正过来,就可以实现视觉上的居中了。
为了实现这个目标,我们可以借助 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 预处理器对文本进行处理,通过传入字体信息,就能修正文字垂直偏移。
虽然上述的方案可以在一定程度上解决文字垂直居中的问题,但是在实际使用中还存在着不方便的地方,我们需要在使用字体之前就知道字体的各项 metrics,在自定义字体较少的情况下,开发者可以手动使用 FontForge 等工具查看,然而当字体较多时,挨个查看还是比较麻烦的。
目前的一种思路是我们可以使用 Canvas 获取字体的相关信息,如现在已经有开源的获取字体 metrics 的库 FontMetrics.js。它的核心思想是使用 Canvas 渲染对应字体的文字,然后使用 getImageData 对渲染出来的内容进行分析。如果在实际项目中,这种方案可能导致潜在的性能问题;而且这种方式获取到的是渲染后的结果,部分字体作者在构建字体时并没有严格将设计的 metrics 和字符对应,这也会导致获取到的 metrics 不够准确。
另一种思路是直接解析字体文件,拿到字体的 metrics 信息,如 opentype.js 这个项目。不过这种做法也不够轻量,不适合在实际运行中使用,不过可以考虑在打包过程中自动执行这个过程。
此外,目前的解决方案更多是偏向理论的方法,当文字本身字号较小的情况下,浏览器可能并不能按照预期的效果渲染,文字会根据所处的 DOM 环境不同而具有 1px 的偏移[9]。
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;
};
从 https://ishoudinireadyyet.com/ 这个网站上可以看到,目前 Font Metrics 依然在提议阶段,还不能确定其 API 具体内容,或者以后是否会存在这一个特性,因此只能说是一个在未来也许可行的文字排版处理方案。
文本垂直居中的问题一直是 CSS 中最常见的问题,但是却很难引起注意,我个人觉得是因为我们常用的微软雅黑、苹方等字体本身在设计上比较规范,在通常情况下都显得比较居中。但是当一个字体不是那么“规范”时,传统的各种方法似乎就有点无能为力了。
本文分析了导致了文字偏移的因素,并给出寻找文字垂直居中位置的方案。
由于涉及到 IFC 的问题本身就很复杂[7],关于内联元素使用 line-height 与 vertical-align 进行居中的各种小技巧因为与本文不是强相关,所以在文章内也没有提及,如果对这些内容比较感兴趣,也可以通过下面的参考资料寻找一些相关介绍。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
ECMAScript模块(简称ES模块)是一种JavaScript代码重用的机制,于2015年推出,一经推出就受到前端开发者的喜爱。在2015之年,JavaScript 还没有一个代码重用的标准机制。多年来,人们对这方面的规范进行了很多尝试,导致现在有多种模块化的方式。
你可能听说过AMD模块,UMD,或CommonJS,这些没有孰优孰劣。最后,在ECMAScript 2015中,ES 模块出现了。
我们现在有了一个“正式的”模块系统。
理论上,ES 模块应该在所有JavaScript环境中。实际上,ES 模块的主要应用还是在浏览器上。
2020年5月,Node.js v12.17.0 增加了在不使用标记前提下对ECMAScript模块的支持。 这意味着我们现在可以在Node.js中使用import和export ,而无需任何其他命令行标志。
ECMAScript模块要想在任何JavaScript环境通用,可能还需要很长的路要走,但方向是正确的。
ES 模块是一个简单的文件,我们可以在其中声明一个或多个导出。以下面utils.js为例:
// utils.js export function funcA() { return "Hello named export!";
} export default function funcB() { return "Hello default export!";
}
这里有两个导出。
第一个是命名导出,后面是export default,表示为默认导出。
假设我们的项目文件夹中有一个名为utils.js的文件,我们可以将这个模块提供的对象导入到另一个文件中。
假设我们在项目文中还有一个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模块,但有一些警告。 要使用模块,需要在 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(模块绑定器)独有的。
像React和Vue通过动态导入代码拆分来加载响应事件的代码块,比如用户交互或路由更改。
假设我们项目有一个 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;
因为 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。
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界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务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统一了session、localStorage和cookie,为你提供了一种处理数据的直接方法。
基本使用:
let basil = new Basil(options);
basil.set(‘name’, ‘Amy’);
basil.get(‘name’);
basil.remove(‘name’);
basil.reset();
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
https://github.com/pamelafox/...
它与localStorage API类似。事实上,它是localStorage的一个封装器,并使用HTML5模拟memcaches函数。在上面的文档中发现更多的功能。
基本使用:
lscache.set(‘name’, ‘Amy’, 5); // 数据将在5分钟后过期 lscache.get(‘name’);
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}]);
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
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 }
});
很神奇的是它提供中文文档
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 中只有涉及到状态变更后才需要被执行的回调才算是微任务,比如说 then、 catch 、finally ,其他所有的代码执行都是宏任务(同步执行)。
上图中蓝色为同步执行,黄色为异步执行(丢到微任务队列中)。
这个问题我们根据 ecma 规范来看:
[[PromiseFulfillReactions]] 和 [[PromiseRejectReactions]] 中。如果你看过手写 Promise 的代码的话,应该能发现有两个数组存储这些回调函数。
了解完以上知识后,正片开始。
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 返回值,这个概念需要大家先了解一下。
根据规范 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 的话会多入队一次微任务,但是这个结论还是与实际不符的,因此我们还需要寻找其他权威的文档。
根据规范 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界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
蓝蓝设计的小编 http://www.lanlanwork.com