吾问无为谓

吾问无为谓 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 www.releaseui.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

吾问无为谓 赞了文章 · 2月25日

TypeScript中那几个奇怪的知识点

写在开头

  • 我写了一年多TypeScript,总结了以下几个点,希望可以帮到大家
  • 如果感觉写得不错,记得来个关注/在看

比较容易遇到的问题

给一个对象添加属性
interface Obj {
  a: string;
}

const obj: Obj = {
  a: "1",
};

obj.b = 2;
  • 此时会出现错误提示:类型“Obj”上不存在属性“b”。
  • 要想解决这个问题,要使用索引签名
interface Obj {
  a: string;
  [index: string]: string | number;
}

const obj: Obj = {
  a: "1",
};

obj.b = 2;
  • 大家很好奇,为什么我这里会加入[index: string]: string | number;,类型是字符串或者数字。因为:
当你声明一个索引签名时,所有明确的成员都必须符合索引签名
函数重载
  • 场景:函数有多个参数,而且参数不确定时,函数运行逻辑不一致
// 重载
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
// Actual implementation that is a true representation of all the cases the function body needs to handle
function padding(a: number, b?: number, c?: number, d?: number) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a;
  } else if (c === undefined && d === undefined) {
    c = a;
    d = b;
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  };
}
这样函数兼容 传 1、2、4个参数。 但是只要传三个,就会报错。
  • 函数重载最重要的是,最终声明(从函数内部看到的真正声明)与所有重载兼容(与上面的索引签名一致)
下载的第三方npm库没有ts声明文件
  • 例如:
npm i somePackage --save 
import somePackage from 'somePackage';
  • 但是此时提示:找不到模块“somePackage”或其相应的类型声明。
  • 此时你可以在项目根目录下新建index.d.ts,编写如下代码:
declare module 'somePackage';
...
这个问题迎刃而解
泛型
  • 这个问题很容易困扰小白,其实泛型简单来说,就是一个类型变量,如下所示:
class Peter {
  niubi<T>(a: T): T[] {
    return [a];
  }
}
此时的T就是一个泛型,它是一个可变的类型。根据你传入niubi这个方法的参数对象来确定的,当我们传入的a是字符串,那么T就为string.返回的就是一个item为字符串的数组
class Peter {
  niubi<T>(a: T): T[] {
    return [a];
  }
}

const obj = new Peter();

let res = obj.niubi("hehe");

res = 1;

res = ["2"];

此时res = 1会报错不能将类型“number”分配给类型“string[]”, 因为此时TS推断出来,res必定为一个数组,且里面的item是一个字符串.

res = ["2"]则不会报错
  • 泛型可以说是TS里面的一个难点,但是其实它只是一个可变的类型变量。
  • 调整参数后:
let res2 = obj.niubi(2);

res2 = 2;
  • 会报错:不能将类型“number”分配给类型“number[]”。
最后要记住的是,既然是类型变量。那么这个变量也可以是一个泛型。
class Peter {
  niubi<T>(a: T): T[] {
    return [a];
  }
}

const obj = new Peter();

function test<T>(b: T): T {
  return b;
}

let res = obj.niubi(test(1));
  • 看到这里肯定有人会说,Peter你脱裤子放屁啊。这个还不如用any.那你再看下面这段代码,我们封装api请求的时候。
  • 首先定义好返回的接口。(返回的接口一般都是有统一的格式,状态码和result,data等)
// 请求接口数据
export interface ResponseData<T = any> {
  /**
   * 状态码
   * @type { number }
   */
  code: number;

  /**
   * 数据
   * @type { T }
   */
  result: T;

  /**
   * 消息
   * @type { string }
   */
  message: string;
}
  • 这里的data数据是动态的格式,我们可以用泛型来定义。
  • 这里用了两次泛型,先定义好返回的data数据,再用泛型方式传入,组装好返回的整个返回数据接口(包含code,result,data)。再接着传入到真正的请求函数中
// 在 axios.ts 文件中对 axios 进行了处理,例如添加通用配置、拦截器等
import Ax from './axios';

import { ResponseData } from './interface.ts';

export function getUser<T>() {
  return Ax.get<ResponseData<T>>('/somepath')
    .then(res => res.data)
    .catch(err => console.error(err));
}
  • 在真正的请求函数中使用了泛型,即传入任意类型参数<T>,那么便返回一个Promise风格的Promise<T>数据 :
const get = <T>(config: { url: string; headers?: { [key: string]: string } }): Promise<T> => {
  const fetchConfig = {
    method: 'GET',
    Accept: 'application/json',
    'Content-Type': 'application/json',
    ...(config.headers || {})
  };
  return fetch(config.url, fetchConfig).then<T>(response => response.json());
};
总结两次泛型的连续使用:

1.使用data作为泛型,传入

2.组装成{code,result,data}这种类型接口

3.将第二步的组装后类型作为泛型<T>传入get方法中

4.返回一个Promise风格的Promise<T>数据

这样做的意义,提取可变的数据类型data,让TS推断出这个接口返回的数据是怎么样的。减少不必要的重复代码,即每次接口调取都会返回的数据格式类型:coderesult
  • 相信你通过这段代码和文字,能真正理解TS的泛型如何用,什么地方使用,以及使用的意义了。
颗粒度定义类型后的问题
  • 当我们颗粒度比较细定义了接口以后,可能存在接口复用的问题,例如:
interface test1 {
  a: string;
}

interface test2 {
  b: string;
}
  • 此时我想要定义一个两个属性都拥有的对象,那么可以使用联合类型。
const obj: test1 & test2 = {
  a: "1",
  b: "2",
};
  • 如果我想定义一个只有a/b的对象,可以使用
const obj: test1 | test2 = {
  a: "1",
};
可能有人会说,怎么会写这么简单的东西。
  • 这里是为了接下来的类型兼容性打基础,TS里面最重要的就是type类型,类型系统就是它的核心。
  • 我们可以用两个不同的变量来互相赋值来检验,他们的类型是否兼容,例如:
interface Test1 {
  a: number;
  b: number;
  c: string;
}

interface Test2 {
  a: number;
  b: number;
}

let test1: Test1 = {
  a: 1,
  b: 2,
  c: "3",
};

let test2: Test2 = {
  a: 1,
  b: 2,
};

test1 = test2;
此时提示类型 "Test2" 中缺少属性 "c",但类型 "Test1" 中需要该属性。
  • 但是当我们用test1赋值给test2的时候:
test2 = test1;
这个时候是可以的
  • 这里其实隐藏着一些逻辑,Test1接口比Test2接口多一个c属性,Test2接口可以说是Test1接口的子类。这是多态性
关于如何处理、判断TS的类型兼容性,大家可以看下面这些类型
  • 协变(Covariant):只在同一个方向;
  • 逆变(Contravariant):只在相反的方向;
  • 双向协变(Bivariant):包括同一个方向和不同方向;
  • 不变(Invariant):如果类型不完全相同,则它们是不兼容的。

写在最后

查看原文

赞 19 收藏 12 评论 0

吾问无为谓 赞了文章 · 1月22日

报告老板,我们的H5页面在iOS11系统上白屏了!

0802.png

时间回到一周前,当时刚开发完公司A项目的一个新的版本,等待着测试完成就进行发布。此时的我也准备从连续多日的紧张开发状态中走出来,以为可以稍稍放松一下。而那时的我还不知道,我即将面临一个强大的Bug选手,更不知道我要跟这个Bug来来回回进行多次的搏斗。当然,我们能看到这篇文章也就说明了我最终解决了这个Bug,而且这个过程也是相当的精彩的。什么?你不相信,那就让我来带你进入这个“跌宕起伏”的经历中吧。

友情提示:接下来的文章也许有一点长,但是希望你能够坚持读下去。我相信我在解决这个Bug的过程中的一些思路会给你带来一些思考。当然也希望你在这个过程中能够像我一样学习到一些新的知识,为以后排查类似的Bug积累一些经验。好啦,话不多说,让我们开始吧。

项目介绍

先来简单介绍一下A项目,这是一个基于Vue框架的项目,项目使用的也是Vue CLI这个开发工具。这个项目是需要集成在别的APP中的,也就是页面需要在APP中进浏览和操作。这个项目在我接手之前已经开发过一段时间了。所以项目中的一些依赖库和工具库版本相对比较低,这也给我后续的调试以及解决Bug的过程增加了一些困难。

BUG初现

当时开发完成之后,就交给我们这边的测试和另一个城市的相关同学去验收这次开发的功能。在我们这边一切都很正常,测试这边也没有反馈有什么问题。但是在另一个城市的同学小C的iPhone手机上却发现了白屏,打开页面之后什么内容也没有。

发现了这个问题之后,我再次跟我们这边的测试同学确认了一下,看看我们这边测试的iOS系统的iPhone手机有没有这个问题。经过测试的测试,发现我们这边的几台iPhone手机都没有问题。然后就问了小C他使用的测试手机的系统版本是多少,当时感觉应该跟iOS的系统版本有关系。

小C反馈说他的iPhone是6S Plus,然后系统的版本是11.1.2。我问了我们这边测试使用的iPhone版本都是多少,测试反馈说系统的版本都是12以上的。所以到这里,我确定了这个白屏Bug的出现肯定跟iPhone手机的系统有关系

重现BUG之路

虽然确定了问题出现的环境,但是因为我身边没有系统是11的iPhone手机,所以想让这个问题重现就变成了一个难题。询问了身边的同事,大家的系统版本也都普遍高于12,所以借用别人的手机进行调试这个方法暂时也不可行。

在平时的开发中,如果网页在iOS系统的APP中有一些问题的话,我们一般都会通过Safari浏览器进行调试。但是因为这次出现问题的iPhone手机不在我这里,并且我这边也没有相同系统的手机。所以想通过真机进行调试就不太可能了。那怎么办呢?这个问题肯定是要解决的,我也相信办法总比困难多

想要进行调试,最简单的办法就是让我有一个系统是11的iPhone手机。所以我就搜索看看有没有什么办法可以给iPhone手机安装11的系统。一搜索还真的有,过程也不算是很复杂。但是其中有一个步骤是需要到一些论坛或者第三方的助手网站下载跟自己手机型号相匹配的iOS系统,这个步骤让我有点感觉不安全。毕竟不是官方的,不能够保证安全性。而且也未必有版本是11的系统。所以这个方案就暂时作罢

在我搜索的过程中,我发现有网友说可以使用Xcode安装相应系统版本的iPhone模拟器来进行调试。哎,你说我怎么没有想到这个办法呢?这确实是一个不错的办法。因为之前跟公司的同事学习过Swift,也了解过Xcode的一些操作。突然感慨,真是技多不压身,你不知道你什么时候就会用上你学过的知识。所以有条件的话,还是多学习一些知识。额,有点跑题了。

安装Xcode

我打开公司的电脑,开始安装Xcode,但是发现公司的电脑系统版本太低,安装Xcode需要升级系统,所以没办法,先升级系统吧。因为升级的时间比较长,我想到自己家中的Mac电脑上是有安装过Xcode,所以决定先回家。留下公司的电脑慢慢升级。

回到家,二话不说就开始准备调试,但是发现我的Xcode上面的iPhone模拟器的系统版本也都是12以上的,查了一下资料,Xcode是可以安装不同系统版本的模拟器的,于是我就安装了系统版本是11的模拟器。这个过程需要我们打开Xcode的偏好设置,然后在Components选项中,选择下载你要安装的对应系统版本的模拟器。

安装iOS11的模拟器

安装成功之后,运行iPhone 6S Plus模拟器,使用模拟器的Safari打开h5的页面地址,果然是白屏。

iPhone 6S Plus模拟器出现白屏

小样,终于把这个问题给复现了,这样就距离解决这个Bug不远了。我打开MacSafari浏览器,进入开发者模式,发现了如下所示的报错

Safari浏览器控制台的报错

我搜索了一下这个错误,发现是因为项目中使用了...ES6扩展运算符,然后iOS 11系统不支持这个这个运算符。这么容易就找到问题了,开心。想到这个问题还是比较好解决的,可以通过使用Babel的一些插件,很容易就可以将这个问题解决掉。然后我就开心的睡觉去了,心想这个问题也不是什么大问题,明天处理一下就好了。

安装Safari Technology Preview

第二天到公司,我就在项目中的babel的配置文件中添加了相应的插件

{
  ...  // 省略原来的配置内容
  "plugins": ["@babel/plugin-proposal-object-rest-spread"]
}

然后发布到测试环境中。告诉了小C同学再次测试一下,我也在等着解决这个Bug的好消息。但是,出现的却不是好消息,小C给我回复说还是不可以。什么,不可能呀,我就马上用公司的电脑再次进行测试。当我用公司电脑的Safari调试系统是iOS 11iPhone 6S PLus模拟器的时候,却发现出现了下面这个情况:审核警告:“data-custom”太新,无法在此检查的页面上运行

审核警告:“data-custom”太新,无法在此检查的页面上运行

我就又搜索了一下为什么会出现这个问题,终于让我找到了答案Safari浏览器的Web Inspector工程师也说这是一个Bug,不过他们已经修复了,在下个发布的版本中就可以正常使用新的Safari浏览器去调试比较老的iOS系统的模拟器了。知道现在这个版本的Safari调试不了模拟的iOS 11系统的页面。我有点沮丧,总不能我现在回家把我的电脑拿过来吧😂?当我想着该如何解决的时候,我发现了上面那个回答中提到了Safari Technology PreviewSafari技术预览

stackoverflow上面Safari浏览器的Web检查器的开发者的回复

我看这个名字感觉有点希望,然后就搜索了一下Safari Technology Preview是什么。然后就发现它相对于Safari就跟Chromium相对于Chrome是一样,都相当于是开发版本的浏览器。

Safari Technology Preview

这时,我觉得可以使用Safari Technology Preview进行调试。所以就下载了Safari Technology Preview,当我打开Safari Technology Preview然后进入开发者模式后,发现确实可以调试iOS 11系统的页面。然后我就看了一下为什么还是白屏的问题。发现出现的错误还是上次的问题:

SyntaxError: Unexpected token '...'. Expected a property name.

也就是说这个问题还没有解决掉,因为打包后的代码是没有SourceMap的,所以要想看更详细的报错信息,需要在本地进行调试。本地的环境中是有SourceMap的,可以定位到更详细的错误信息,我在本地运行了项目,然后我打开了控制台的错误详情,发现是使用的一个第三方的库出现了问题。

找到了出现问题的使用的第三方库

那么到这里为止,可以说明上面我们使用的Babel插件没有处理这个第三方的库,所以现在我们的问题就变成了:如何解决第三方库中出现的...扩展运算符没有被编译为ES5语法的问题

将第三方库中的ES6语法进行编译

查看Vue CLI中相关的配置方法

这时我又仔细的看了一下Vue CLI的相关文档,发现确实在浏览器的兼容性这个章节中,提到了一些处理的方法。原来我们在项目中写的代码默认会帮我们转换为ES5的语法的,但是如果项目中依赖的第三方库需要polyfill的话,那需要我们手动进行配置。一看到这里,我感觉黎明就要来了

Vue CLI浏览器兼容性

我就开始尝试这三种方法。我发现第一种方法是比较简单的,也很好配置。于是我就尝试了第一种方法。在项目的vue.config.js中添加如下的配置:

...  // 省略的配置
transpileDependencies: [
  'module-name/library-name' // 出现问题的那个库
],
...  // 省略的配置

重新运行项目,当我将要为即将到来的成功欢呼鼓掌时,控制台突然报告了如下的错误:
Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'

Uncaught TypeError: Cannot assign to read only property ...

这个报错是在Chrome浏览器的控制台出现的,因为项目在本地重新运行之后会首先打开Chrome浏览器。真是的,一个问题还没有解决,又出来了一个新的问题。然后再次查询资料后发现,原来是因为这个第三方的库是一个CommonJS类型的库,而Babel默认处理的是ES6module类型的库,所以这里就又出现了新的问题。

https://github.com/webpack/webpack/issues/4039 sokra的回复

第一种方法遇到了阻碍,先暂停一下。我准备继续尝试下面两种方法。但是因为后面两种方法对原来的项目改动有点大,所以我直接通过Vue CLI创建了一个新的项目,在package.json中加入项目中使用的那个第三方包的依赖,使用公司的包管理工具安装了依赖。然后运行项目,打开控制台确实发现了相同的错误。但是打开详情以后,发现出错的路径跟我原来项目不一致。然后我这次抱着试一试的心态,继续使用了第一种方法尝试看看可不可以。然后复制了出错路径的包名称,在vue.config.js文件中的对应位置添加了如下的配置代码:

...  // 省略的配置
transpileDependencies: [
  'module-name-new/library-name-new' // 出现问题的那个库
],
...  // 省略的配置

然后重新运行项目,发现居然可以了。啊,居然可以了。为什么我在原来的项目中这样却不可以呢?我看了一下原来项目的依赖以及现在新的测试项目的依赖,发现它们的vue, babel版本差了好多。我猜测可能是因为这个原因。但是现在肯定不可以贸然升级这些依赖的版本,因为为了解决这个问题再次带来新的问题就得不偿失了。

还有一个问题就是为什么同样的第三方库,在原来的项目中和现在的项目中报错的路径不一样。而且看着像是使用了两个不一样的第三方库。这里先留个悬念,我会在后面的文章中进行解释。

接下来,我开始在测试项目中继续尝试剩下的两种方法,对于第二种方法,因为老项目中使用的presets是没有polyfills这个配置选项的,到现在为止出问题的这个第三方库我不知道除了这个...对象扩展操作符之外还有没有别的依赖。所以这个方法我暂时也放弃了。

对于第三个方法,我觉得可以尝试,首先我将测试项目中的一些关键依赖进行了手动降级,然后按照上面的第三个方法的步骤在测试项目中使用。但是发现测试项目运行之后,提示需要安装core-js,安装core-js之后还报错,再次提示需要安装es.module.regex.match等等很多依赖,继续查资料,发现需要把配置中的 useBuiltIns修改,但是因为我接手的这个项目是老项目,依赖比较多,不确定修改useBuiltIns这个配置选项后会不会出现新的问题。所以也不敢贸然修改这个配置选项,所以也暂时放弃了这个方法。

我后来想了一下,对于...扩展运算符来说,这是一个新的语法。是不能够通过一些polyfills去解决的。需要Babel对这个语法进行编译,然后才可以在低版本的系统中使用,所以解决的办法还是要让Babel对这个库再次进行编译。

寻找新的突破口

当进行到了这里的时候,似乎没有了出路。一时间我感觉我要被这个Bug打败了,我似乎听到了它无情的嘲笑,“小伙子,是不是被我折磨的没有脾气啦;放弃吧,你是没办法打倒我的。哈哈哈。。。

Photo by sebastiaan stam on Unsplash

但是,它看错我了,Bug越是难解决,我对它就越有兴趣。所以我决定好好理一下思路,准备再次扬帆起航。

我发现第一种办法其实是起作用的,只不过是因为一个是CommonJS类型的,一个需要是ES6 module类型的。所以我决定从这个地方入手,于是我决定查查相关的资料,看看Babel有没有办法可以即能够处理CommonJS模块,又能够处理ES6 module模块呢?终于,功夫不负有心人,我发现了Babel里面有这么一个配置sourceType,如果把sourceType设置为unambiguous就可以解决这个问题

https://babeljs.io/docs/en/options#sourcetype

这样Babel就会根据模块文件中有没有import/export来决定使用哪种解析模块的方式。于是我再次使用了第一种方法,在vue.config.js中添加了transpileDependencies选项的配置,然后在项目中的Babel配置文件中添加了如下的配置:

module.exports = {
  ...  // 省略的配置
  sourceType: 'unambiguous',
  ...  // 省略的配置
};

发现的确可以,这一刻成功的喜悦再次降临。然后我再次打包,再次把代码部署到测试环境,赶忙让小C同学再次测试一下,发现的确可以。欧耶,终于解决这个问题了。我终于可以松一口气了,哈哈哈。。。小样,这怎么会难得到我呢?

但是,当我仔细阅读将这个选项设置为unambiguous时,我发现了一些问题。因为这样的话会有一些风险,因为就算不使用import/export语句的这些模块也可能是完全有效的ES6 module,所以这样的话就有可能会出现一些意外的情况。怎么办,我似乎在一不留神的时候又被Bug卡住了脖子

https://babeljs.io/docs/en/options#sourcetype

我觉得老天总是给我开玩笑,当我从一个坑里跳出来,以为没有危险的时候。前面突然又多出来一个坑,我一不留心就又掉了进去。我感觉既然都走到了这里,肯定要继续走下去,一定有办法可以优化我现在遇到的问题。我就很仔细的再次看了一下Babel的配置说明文档,这个时候就心想如果我对Babel再熟悉一些就好了。没关系,继续努力。终于,我似乎看到了什么了不得的配置选项。

https://babeljs.io/docs/en/options#overrides

我在Config Merging options里发现了overrides选项,这个配置选项不正是我需要的吗?我可以利用这个配置选项将我需要的第三方包使用unambiguous的处理方式,然后其他的第三方库都按照之前的方式处理不就可以了。哈哈哈,我真是个天才,我心里这样对自己说😂。

Photo by bruce mars on Unsplash

所以只需要在项目的babel.config.js中写下如下的配置就可以了:

module.exports = {
  ...  // 省略的配置
  overrides: [
    {
      include: './node_modules/module-name/library-name/name.common.js',  // 使用的第三方库
      sourceType: 'unambiguous'
    }
  ],
  ...  // 省略的配置
};

对了,还有一件事情还没有说,那就是上文提到的关于为什么使用公司自己的包管理工具下载下来的node_modules包的名称跟使用官方的npm包管理工具下载的包的名称不一致的问题。原因是公司使用的包管理工具是cnpm的一个修改版本。又因为cnpm为了提高下载的速度,使用了cnpm/npminstall,所以才会出现下载的包名比较混乱的情况,详情可以看这里

到此完结撒花,总结一下:出现白屏的原因是因为使用的第三方库的包中使用了...扩展运算符,然后因为第三方的包默认是没有被Babel处理过的,所以在不支持...iOS 11系统上就出现了白屏。解决的方式就是通过给vue.config.js的配置文件中transpileDependencies配置选项中添加上出问题的包的名称就可以了。当然如果项目比较老,可能还需要像文章上面写的那样的处理方式。

解决这个Bug过程就像是升级打怪一样,不断失败,不断尝试,只要不放弃,终有成功的那一天。如果你坚持看到了这里,那说明你也很棒呀。在当今这个信息爆炸的时代里,能够坚持看完一篇很长的文章已经很不错了。

一点反思与思考:这个过程中我也发现了自己对BabelVue CLI其实没有那么熟练,如果我对它们比较熟练的话,那我解决这个Bug应该会花费更少的时间。当然,现在把它们学习好也不算晚。要抱着学习的态度,这次解决这个Bug的过程,就是我以后解决其它类似Bug的经验。还有在解决Bug的这个过程中要有耐心,当然在尝试之后也要学会放弃错误的方向

写这篇文章也花费了我不少的时间,如果你有所收获或者感悟,不妨点赞,转发,收藏走一波,这个要求应该不算过分吧😂?

如果你对本篇文章有什么意见和建议,都可以直接在文章下面留言,也可以在这里提出来。也欢迎大家关注我的公众号关山不难越,学习更多实用的前端知识,让我们一起努力进步吧。

公众号:关山不难越

查看原文

赞 21 收藏 10 评论 11

吾问无为谓 赞了文章 · 1月11日

水平垂直居中深入挖掘

在上篇文章 -- 一行 CSS 代码的魅力 的最后,提到了两种快速实现水平垂直居中的方式。

当然,CSS 中实现水平垂直居中的方式很多。别看到水平垂直居中就准备右上角 x 掉,本文的重点不是罗列有多少种方式实现水平垂直居中方式,而是探讨一下常见的几种水平垂直居中的方式的利弊。

嗯哼?也就是:

  • 那么多种水平垂直居中的方式,如果真的在业务中需要使用了,你脑海里第一时间会想到哪个?
  • 不同的水平垂直居中方式,它们肯定存在差异,那么最显著的不同是什么?
  • 有没有所谓的最完美的水平垂直居中?

本文将讨论 4 种水平垂直居中的方式,分别是,并且每个起个名字方面下面看图:

  1. absolute: position: absolute 配合 top:50%;left:50%;transform:translate(-50%, -50%)
  2. autobot: display:flex 配合 margin:auto
  3. flex: display:flex 配合 align-items:centerjustify-content:center
  4. grid: display:grid 配合 place-content:center;

居中单个元素

对于如下简单的结构:

<div class="g-container">
    <div class="sub"></div>
</div>

居中单个元素而言,上述 4 种方法都很好,没有问题。

1.png

CodePen Demo -- Centering in CSS

居中多个元素

对于如下稍微复杂点的结构:

<div class="g-container">
    <div class="sub">1</div>
    <div class="sub">123</div>
    <div class="sub">123456</div>
</div>

那么如果是居中多个子元素,上述 4 种方法,就能体现出明显的不一样。稍微也修改一下子元素,不给它设定宽度,通过 padding 撑开即可:

.sub {
    border: 1px solid deeppink;
    padding: 10px;
    border-radius: 5px;
}

看看结果如何:

2.png

CodePen Demo -- Centering in CSS 2

简单分析分析:

  1. absolute 的方法明显有问题,由于用的绝对定位,其实 3 个子元素都叠在了一起
  2. flexgrid 的方法,如果不手动添加边距(margin 或者 gap),会贴在一起
  3. 不限制方向的话,flex 默认是水平排列,grid 是竖直排列
  4. 非常重要的一点,grid 布局下的子元素的宽度,所以子元素的宽度会被强行拉伸至最宽的一个子元素的内容的宽度

对于多个子元素,absolute 方法明显不适用, 接下来主要看剩余 3 个方法在一些细节上的差异。

控制间距

如果我们希望控制每个元素之间的间距呢?我们给 autobotflexgrid 的容器各自加上 gap: 5px,再看看:

.g-container{
    gap: 5px;
}

3.png

CodePen Demo -- Centering in CSS 3

  1. margin: auto 由于需要均分剩余空间,所以表现并不好,无法按照我们设想的 5px 宽度进行间隔

让元素多到溢出

OK,接下来,我们让内容再多一点,多到溢出整个容器,看看有什么不一样。

4.png

再来一张竖直方向排列的:

5.png

CodePen Demo -- Centering in CSS 4

可以看到:

  1. 非常重要的一点,由于没有了剩余空间margin: auto 已经无法做到均匀分配,水平垂直居中了,而是一边贴着容器边,另外一边溢出
  2. flexgrid 都做到了即便超出容器空间,依然是水平垂直居中的

总结一下

经由上述几个 DEMO 可以看出来,在目前比较常用的水平垂直居中方案当中。flex 方案应该是目前而言最优的选择,它能够在各种环境下都保持内部元素的水平垂直居中并且不改变子元素的某些特征:

  1. 便捷的水平垂直居中单个元素
  2. 便捷的水平垂直居中多个元素,无论是横向、竖向,亦或内容超出
  3. 非常方便控制子元素之间的间距
  4. 不会改变子元素的宽度

当然,美中不足的是,可能相对而言,要敲多几个字符。

margin: autogrid 则或多或少有一些小问题。absolute 无法应付多个元素。

最后

本文知识点比较细,也表明 CSS 虽然简单,但是不代表它容易。我们日常工作中用到的很多属性其实还有很多细节可以挖掘深入。

譬如,可以再比较下在书写方式 writing-mode 不同的场景下,上述水平垂直居中的方式的异同,等等。

好了,本文到此结束,希望对你有帮助 :),想 Get 到最有意思的 CSS 资讯,千万不要错过我的 iCSS 公众号 😄

QQ20210108-0.png

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

查看原文

赞 8 收藏 5 评论 0

吾问无为谓 回答了问题 · 2020-12-24

Ant design pro首页逻辑

image.png

关注 2 回答 1

吾问无为谓 回答了问题 · 2020-12-16

解决将数组转换成带排序的二维数组?

let arr=[1,3,5,2,7,8,9,0];
function getArr(length,arr){
    let newArr=[];
    let spt=parseInt(arr.length/length)+1;
    for(let i=0;i<spt;i++){
        let num=i;
        newArr.push(arr.splice(0,length));
    }
    let Arr=[];
    newArr.map((item,index)=>{
        item.map((item1,index1)=>{
            Arr.push([index,index1,item1]);
        })
    })
    return Arr
}  
getArr(3,arr)

关注 3 回答 4

吾问无为谓 回答了问题 · 2020-12-16

vue项目部署在服务器访问接口出现跨域问题,配置反代理不生效

反代理是要把项目和api接口放在一个端口下,你这没放一起吧
用另一个端口启动项目,然后都在8080下用不同字段配置项目和api就好了

关注 4 回答 4

吾问无为谓 赞了文章 · 2020-12-16

问我Chrome浏览器的渲染原理(6000字长文)

前言

对于HTML,css和JavaScript是如何变成页面的,这个问题你了解过吗?浏览器究竟在背后都做了些什么事情呢?让我们去了解浏览器的渲染原理,是通往更深层次的开发必不可少的事情,能让我们更深层次,多角度的去考虑性能优化等问题。

HTML,css,JavaScript数据经过中间渲染模块的处理,最终显示在页面上(其中HTML超文本标记语言,CSS层叠样式表,JS为JavaScript,大家一般都知道是什么,写过网页的朋友,学习者大都知道的)。

  • HTML的内容是由标记和文本组成
  • CSS称为层叠样式表,是由选择器和属性组成
  • JS是可以使网页的内容“动”起来

有人说渲染流程可以分为:构建DOM树,样式计算,布局阶段,分层,绘制,分块,光栅化和合成等。其中浏览器复习一下,它是多线程的的,js是单线程的,JS在浏览器中,它可以是多线程的。

下面围绕浏览器渲染原理话题开始描述,为什么要了解浏览器渲染页面机制呢?。

浏览器渲染原理

首先,JavaScript引擎是基于事件驱动单线程执行的,渲染线程负责渲染浏览器界面,但是GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI的更新也会被保存在一个队列中,等到JS引擎空闲时才有机会被执行。

那么什么是DOCTYPE以及作用呢

DTD,document type definition, 文档类型定义,是一系列的语法规则,用来定义XML或(x)HTML的文件类型。浏览器会使用它来判断文档类型,决定使用何种协议来解析,以及切换浏览器模式。

DOCTYPE是用来声明文档类型和DTD规范的,一个主要的用途便是文件的合法性验证。如果文件代码不合法,那么浏览器解析时便会出一些差错。

下图为浏览器的渲染过程图:

重排Reflow

重排的定义:DOM结构中的各个元素都有自己的盒子模型,这些都需要浏览器根据各种样式来计算并根据计算结果将元素放到它该出现的位置,这个过程称之为reflow.

触发Reflow情况

  • 当你增加,删除,修改Dom节点时会导致Reflow或Repaint
  • 当你移动DOM的位置,或是搞个动画的时候
  • 当你修改CSS样式的时候
  • 当你Resize窗口的时候,或是滚动的时候
  • 当你修改网页的默认字体时

重绘Repaint

重绘的定义,当各种盒子的位置,大小以及其他属性,例如颜色,字体大小等都确定下来后,浏览器于是便把这些元素都按照各自的特性绘制了一遍,于是页面的内容出现了,这个过程称为repaint。

触发Repaint情况

  • DOM改动
  • CSS改动

讲到这里,下面来细分说一下吧!

简单介绍一下浏览器的工作原理

  1. 了解浏览器

目前使用的主流的浏览器:Internet Explorer,Firefox,Safari,Chrome浏览器,Opera。让我们看看浏览器统计数据的占比:

让你说说浏览器的主要功能:

就是向服务器发出请求,在浏览器窗口中展示您选择的网络资源,资源一般指HTML文档,可以是PDF,图片或其他的类型。资源的位置由用户使用URI(在电脑术语中,统一资源标识符(Uniform Resource Identifier,URI)是一个用于标识某一互联网资源名称的字符串)

  1. 浏览器的结构
  • 用户界面:包括地址栏,前进、后退按钮,书签菜单等。
  • 浏览器引擎:在用户界面和呈现引擎之间传送指令。
  • 呈现引擎:负责显示请求的内容。
  • 网络:用于网络调用,比如HTTP请求;其接口与平台无关,并为所有平台提供底层实现。
  • 用户界面后端:用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
  • JavaScript解释器:用于解析和执行JavaScript代码。
  • 数据存储:这是持久层。浏览器需要在硬盘上保存各种数据,例如Cookie。新的HTML规范定义了“网络数据库”,这是一个完整的浏览器内数据库。

注意:Chrome浏览器的每个标签页都分别对应一个呈现引擎实例,每个标签页都是一个独立的进程。

  1. 呈现引擎

呈现引擎的作用是“呈现”,用于在浏览器的屏幕上显示请求的内容。

一般情况下,呈现引擎可显示HTML和xml文档与图片,通过插件或浏览器扩展程序,可以显示其他类型的内容。浏览器(Firefox,Chrome浏览器和Safari)是基于两种呈现引擎构建的。

Firefox使用的是Gecko,而Safari和Chrome浏览器使用的是WebKit(WebKit 是一种开放源代码呈现引擎)。

  1. 主流程

呈现引擎一开始会从网络层获取请求文档的内容,其大小一般限制在8000个块以内。

呈现引擎将开始解析HTML文档,并将各标记逐个转化成“内容树”上的DOM节点。同时也会解析外部CSS文件以及样式元素中的样式数据。呈现树构建完后,会进入“布局”处理阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标。

  1. 解析

解析是呈现引擎中重要的环境,什么是解析呢?

解析文档是指将文档转化成为有意义的结构,可以让代码理解和使用的结构。解析得到的结构通常是代表了文档结构的节点树,它称为解析树或者语法树。

  1. 语法

解析是以文档所遵循的语法规则为基础的。解析的过程分为两个子过程:词法分析和语法分析。

什么是词法分析呢?

词法分析是将输入内容分割成大量标记的过程,标记(语言中的词汇),构成内容的单位。相等于语言中的单词。

什么是语法分析呢?

语法分析是应用语言的语法规则的过程。

so,解析器一般解析工作分两个组件处理,为词法分析器(负责将输入内容分解成一个个有效标记),解析器负责根据语言的语法规则来分析文档的结构,来构建解析树。

从源文档到解析树:Document->Lexical Analysis->Syntax Analysis->Parse Tree

解析是一个迭代的过程。

是这样的,解析器会向词法分析器请求一个新标记,并尝试将其与某条语法规则进行匹配。如果匹配规则,解析器就会将对应与该标记的节点添加到解析树中,然后继续下一个。

但是如果没有匹配的规则,解析器会将标记存储到内部,继续请求标记,直到可与之匹配的规则,但是如果没有直到的话,就会引发异常(文档无效,包含语法错误等)。

  1. 翻译

解析通常是在翻译的过程中,而翻译是将输入的文档转换为另一种形式,如编译器将源代码编译成机器代码,流程是将源代码解析成解析树,将解析树翻译成机器代码文档。

编译流程:Source Code -> Parsing->Parse Tree -> Translation -> Machine Code
  1. 解析器类型

两种基本的解析器类型:自上而下解析器,自下而上解析器

自上而下就是: 解析器从语法的高层结构出发,尝试从中找到匹配的结构。

自下而上就是: 解析器从低层规则出发,将输入内容逐步转化为语法规则,直至满足高层规则。

你知道一种工具叫解析器生成器吗,它能够帮助你生成解析器,你只要向它提供你所使用的语言的语法,即词汇和语法规则,然后就会生成相应的解析器。

你晕了吗?可以点击这里查看:浏览器的工作原理:新式网络浏览器幕后揭秘
https://www.html5rocks.com/zh...

渲染机制

浏览器从接收到页面开始到页面显示,这整个过程中的所有步骤,称 关键渲染路径 ,一般分为两步:页面内容加载完成和页面资源完成,分别对应于DOMContentLoaded和Load

关键:网页的渲染过程如下,包含页面加载和页面渲染两个过程。

页面加载过程是,从服务器请求资源并构建DOM树的过程,网页渲染过程指的是通过DOM树渲染出视图内容。

DOM + CSS -> Render Tree

复习一下整个关键渲染包括:

  • 解析HTML,生成DOM树(DOM)
  • 解析CSS,生成CSSOM树
  • 将DOM和CSSOM合并,生成渲染树(Rendere-Tree)
  • 计算渲染树的布局Layout
  • 将布局渲染到屏幕上Paint

那么要问了,为什么要了解浏览器渲染页面机制呢?

了解渲染机制,主要还是为了性能的优化:

了解浏览器如何进行加载,引用外部样式文件,JS文件时,将它们放到合适的位置,是浏览器最快的速度让文件加载完毕;了解浏览器如何进行解析,选择最优的写法,构建DOM结构,组织CSS选择器的时候,是为了提高浏览器的解析速率;了解浏览器如何进行渲染,是可以减少“重绘”,“重新布局”的消耗。

那么上面一直说了解渲染机制,出现的几个基本概念,这里先弄明白:

  • DOM: Document Object Model,浏览器将HTML解析成树形的数据结构
  • CSSOM: CSS Object Model,浏览器将CSS解析成树形的数据结构
  • Render Tree: DOM和CSSOM合并生成Render Tree
  • Layout: 计算出Render Tree每个节点的具体位置
  • Painting: 通过显卡,将Layout后的节点内容分别呈现到屏幕上

说说浏览器页面渲染:

  • 第一步:在CSS资源还没有请求回来之前,先生成DOM树;
  • 第二步:当所有的CSS请求回来之后,浏览器按照CSS的导入顺序,依次进行渲染,最后生成CSSOM树;
  • 第三步:把DOM树和CSSOM树结合在一起,生成有样式,有结构的RENDER TREE渲染树;
  • 最后一步:浏览器按照渲染树,在页面中进行渲染和解析

来源于知乎的渲染引擎及关键渲染路径

由于渲染机制过于复杂,渲染模块在在执行过程中划分了很多阶段,通过《浏览器工作原理与实践》-渲染流程上分:构建DOM树,样式计算,布局阶段;渲染流程下分:分层,图层绘制,栅格化(raster)操作,合成和显示。

整个渲染流程,从HTML到DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。

面试一问:为什么要构建DOM树?

答:因为浏览器不能直接理解和使用HTML,so,需要将HTML转换为浏览器能够理解的结构,即是DOM树(树结构一般都了解了的)。

为了了解完整的DOM树结构,可以打开Chrome的“开发者工具”,或按F12,如图下:

接下来要让DOM节点拥有正确的样式,这就需要样式计算了。

样式计算的目的是为了计算出DOM节点中每个元素的具体样式:三步走

  • 把CSS转换为浏览器能够理解的结构
  • 转换样式表中的属性值,使其标准化
  • 计算出DOM树中每个节点的具体样式(涉及到CSS的继承规则和层叠规则)
当渲染引擎接收到CSS文本时,会执行一个转换操作,将CSS文本转换为浏览器可以理解的结构——styleSheets。属性值标准化的过程:将所有值转换为渲染引擎容易理解的、标准化的计算值。

DOM元素最终计算的样式如图:

布局阶段

布局:计算出DOM树中可见元素的几何位置,第一创建布局树(构建一棵只包含可见元素布局树),第二布局计算。

面试问题:CSS加载会阻塞页面显示吗?

  • css加载不会阻塞DOM树的解析
  • css加载会阻塞DOM树的渲染
  • css加载会阻塞后面js语句的执行

so,为了避免让用户看到长时间的白屏时间,应该提高css的加载速度。

为了防止css阻塞,引起页面白屏,可以提高页面加载速度

  • 使用cdn
  • 对css进行压缩
  • 合理利用缓存
  • 减少http请求,将多个css文件合并

面试问题:下载CSS文件阻塞了,会阻塞DOM树的合成吗?会阻塞页面的显示吗?

说了DOM生成、样式计算和布局三个阶段,接下来说说后面的阶段。

说说分层:渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。完成图层树的构建后,渲染引擎会对图层树中的每个图层进行绘制,为图层绘制。然后进行栅格化(raster)操作(绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的),最后合成与显示。

找一张整体的流程图如下:

页面渲染机制图如下:

渲染过程图如下:

浏览器渲染过程如下:

浏览器解析的三个东西流程图如下:

一是HTML/SVG/XHTML

二是CSS

三是Javascript脚本

这里重要要说(重新说一下)两个概念回流和重绘

当render tree中的一部分因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。

每个页面至少需要一次回流,就是在页面第一次加载的时候。

在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。

当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。就叫称为重绘。

本篇文章的最后,留下一些面试题:为什么减少重绘、重排能优化Web性能吗?如何能减少重绘、重排呢?

总结

以上就是今天要讲的内容,本文仅仅简单介绍了Chrome浏览器的渲染原理流程,感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

查看原文

赞 57 收藏 38 评论 0

吾问无为谓 回答了问题 · 2020-12-16

使用window.open打开的页面,如何嵌入js并执行?

判断下浏览器切换事件,切换到窗口2时候重新拿一次页面数据

let hiddenProperty = 'hidden' in document ? 'hidden' : 
 'webkitHidden' in document ? 'webkitHidden' : 
 'mozHidden' in document ? 'mozHidden' : 
 null;
let visibilityChangeEvent = hiddenProperty.replace(/hidden/i, 'visibilitychange');
let onVisibilityChange = function(){
 if (!document[hiddenProperty]) { 
 console.log('页面非激活');
 }else{
 console.log('页面激活')
 }
}
document.addEventListener(visibilityChangeEvent, onVisibilityChange);

关注 3 回答 3

吾问无为谓 回答了问题 · 2020-12-14

react同一组件多次调用,如何更新对应state

这应该是树结构吧,用数组组织下传参吧

关注 2 回答 2

吾问无为谓 回答了问题 · 2020-12-14

javascript 如何实现保留两位小数(不四舍五入)

先弄成整数转成数组,数组倒数第三位加个‘.’转成字符串就好了

let num=12.12378;
let arr=parseInt(num*100).toString().split('')
arr.splice(-2,0,'.')
let str=arr.join('')

关注 7 回答 9

认证与成就

  • 获得 1 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-05-21
个人主页被 245 人浏览