项目实战:服务器中的nginx和docker起的nginx冲突怎么办

今天,后端过来说有个需求需要前端帮忙,我立即摆谱:“哼,什么问题?”

他连忙跑到我的座位前,窜着手机跟我说:“我发你一个链接,能不能把这些代码放到 hosts 文件里。”

我说:“这是什么?为什么要放。”

经过他的解释,大致是

前端点击一个按钮,调用 API ,后端返回一个 url,url 是第三方链接(后端对接第三方公司返回的链接),因为第三方链接的测试环境需要内网环境才能测试,所以要测试这个产品需要配置 hosts 文件

我的理解是,这个只需要在本机上添加 hosts 文件,让测试添加即可,管我前端什么事!

后来经过拉锯战,他找来技术负责人,负责人亲自讲解,希望我把这些代码放到前端服务器的 hosts 文件中

没办法,说了不听,听了不悦,要我做。官大一级压死人。这里也很有意思,技术人员中,如果你技术真的过硬,听你的,确实没问题,到后面下级还能学到东西。但是如果上级技术一般,这个知识点不是很懂,那么只能试,试就需要成本,这次就是一次成本代价

我在服务器上的 hosts 中添加代码后,网站就访问不了了,nginx 502

我感觉删掉添加的代码,再重启 DNS 服务,结果还是不行

继续搞,搞半天才想起来我的站点部署在 docker 中,启动服务器的 nginx 没用

搞 docker

常用命令

  • docker ps 查看运行的容器
  • docker exec -it containerID /bin/bash 进入指定容器的 docker 服务

先删掉原本的 docker,在手动启动 docker 命令(同事写好的 bash 文件)

直接报错,反向代理不能用字母

host not found in upstream "XXX.com" in /etc/nginx/conf.d/default.conf:18
nginx: [emerg] host not found in upstream "XXX.com" in /etc/nginx/conf.d/default.conf:18

暂时找不到原因,先改,改成 ip 后,启动成功,使用

docker exec -it containerID /bin/bash 进入容器中,查看 docker 中的 nginx 和静态文件是否有问题,发现都是最新的,理论上是没问题的

回到服务中,再查看端口占用

ps -ef | grep nginx

有好几个 nginx 的服务是启动的

所以猜想是不是 hosts 文件添加之后,docker 自动被弄坏了,然后我再启动了 nginx,nginx 和 docker 启动的 nginx 冲突,所以即使把 hosts 文件恢复了,因为 nginx 一直启动着,所以 docker 启动的容器一直不能访问

解决方案

简单来说,把服务器中的 nginx 关掉,再重启 docker 容器即可

一、查找 nginx 所占的端口

ps -ef | grep nginx

二、杀掉所有的与 nginx 相关的端口

kill -9 12782

三、重启 docker 相关容器

docker restart f4d

总结

到最后解决方案很简单,但主要是排查能力和对命令的熟悉

这次排查唤起了我对很多 nginx 和 docker 的知识点,明年要对这两块做重新的知识梳理

芝麻开门,显示全文!

左边固定宽,右边自适应的6种方法

这是一道面试题,你有多少种办法呢?

这里我们假设左边名为 left,宽度为 200 px,右边名为 right。即默认

.left {
  width: 200px;
}

我的理解分四大类

  • flex 布局
    • 需设置父元素高度
  • grid 布局
    • 需设置父元素高度
  • 绝对定位
    • 双子元素 absolute
      • 不需要设置父元素高度
      • 子元素都设置高度,右边子元素 left:200px + width: 100%
    • 左元素 absolute + 右元素 margin-left
      • 不需要设置父元素高度
      • 子元素都设置高度,右边子元素 margin-left: 200px + width: 100%
  • float 浮动
    • 左元素左浮动,右元素不动
      • 无需父元素
      • 左元素需设置宽高和浮动,右元素设置高度即可
    • 左元素左浮动,右元素右浮动
      • 无需父元素
      • 左元素设置宽高和左浮动,右元素设置右浮动以及高和宽(width: calc(100% - 200px)

flex 布局

需要一个父元素做 flex 布局,且需要给它一个高度(撑开容器)

.father {
  display: flex;
  height: 200px;
}
.right {
  flex: 1;
}

grid 布局

高级的布局方式,子元素不需要设置宽度,单单设置父元素属性即可。

.grid {
  display: grid;
  grid-template-columns: 200px 100%;
  height: 200px;
}

双子元素 + absolute

需要给子元素设置宽高,不然撑不起来。右元素设置left: 200px

.father {
  position: relative;
  height: 200px;
}
.left {
  position: absolute;
  height: 200px;
}
.right {
  position: absolute;
  left: 200px;
  height: 200px;
  width: 100%;
}

左元素 absolute + 右元素 margin-left

.father {
  position: relative;
  height: 200px;
}
.left {
  position: absolute;
  width: 200px;
  height: 200px;
}
.right {
  width: 100%;
  height: 200px;
  margin-left: 200px;
}

无父元素 + 左元素左浮动,右元素不动

前两种都需要有个父元素,但浮动不需要

左边浮动,下一个元素独占位置,并排一行

同样,需要设置高度,子元素才能撑开

.left {
  float: left;
  height: 200px;
}
.right {
  height: 200px;
}

无父元素 + 左边左浮动,右边有浮动

浮动不需要父元素,浮动就区别于正常文档流

我的理解是正常文档流是二维层面,而浮动相当于成了三维,区别于 Z 轴

右边元素有浮动不够,还需要设置宽度

.left {
  float: left;
  height: 200px;
}

.right {
  float: right;
  height: 200px;
  width: calc(100% - 200px);
}

只要是 float 实现此功能的,都不需要父元素,以及自身都需要设置高度

总结

简单来说,实现布局最好的方式是 flex,简单兼容现代浏览器和机型。当然,我是因为还没有学 grid(但 grid 要记得参数比较多)。绝对定位和浮动各有优缺点

各大方法优缺点需要什么
flex布局简单需要父元素、高度。子项 flex:1
grid布局最简单,但兼容性更现代只需要父元素设置属性就好
绝对定位兼容性更高需要父元素做相对定位、高度
浮动兼容性更高不需要父元素,子项都需要宽高

float 区别于其他三种,不需要父元素做容器

grid 区别于其他三种,不需要设置子元素(左元素的)宽

绝对定位区别于其他三种,它的方法不仅要父元素有高,其子元素也要有高

flex 最简单

附上线上 demo

芝麻开门,显示全文!

水平垂直居中的17种方法

面试的时候,绝不能只说一种,绝不能说一种解决方案,绝不能停下你吞吞吐吐的嘴

CSS 方面问的最多的问题之一,我想分三种情况,水平居中、垂直居中和水平垂直居中来分析

单单就水平垂直居中而言,大概有以下几种方案:

居中元素不定宽高

  • absolute + transform
  • flex 属性居中
  • flex + 子项 margin auto
  • grid 属性居中
  • grid + 子项 margin auto
  • grid + 子项属性居中
  • -webkit-box 属性居中
  • table-cell + text-align
  • line-height + text-align
  • writing-mode
  • table

仅居中元素定宽高适用:

  • 须知宽高 + absolute + 负 margin
  • 须知宽高 + absolute + calc
  • 须知宽高 + absolute + margin auto

局限性较大的全局居中

  • 须知宽高 + fixed + transform
  • 须知宽高 + fixed + 负 margin
  • 须知宽高 + fixed + calc
  • 须知宽高 + fixed + margin auto

水平居中

text-align: center

text-align: center;

需设置 display: inline-block 行内块元素

绝对定位 + transform 位移

position: absolute;
left: 50%;
transform: translateX(-50%);

脱离文档流

宽度+ margin: 0 auto

width: 100px;
margin: 0 auto;

这里说明下,width:100px 必须是具体的数字,且这个居中是外层居中,宽度中的内容没有居中

垂直居中

绝对定位 + transform 位移

position: absolute;
top: 50%;
transform: translateY(-50%);

与水平方向的居中一样,都是脱离文档流的做法

table-cell + vertical-align

display: table-cell;
vertical-align: middle;

display: table-cell ,让其标签元素以表格单元格的形式呈现,类似于 td 标签,

vertical-align: middle,用来指定行内元素(inline)或表格单元格(table-cell)元素的垂直居中

水平垂直居中

绝对居中 + transform 位移

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  position: relative;
}
.son {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

flex 属性居中

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: flex;
  justify-content: center;
  align-items: center;
}

flex + margin auto

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: flex;
}
.son {
  margin: auto;
}

grid 属性居中

<div class="father">123123</div>
// 或者
<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: grid;
  justify-content: center;
  align-items: center;
}

grid 子项属性居中

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: grid;
}
.son {
  align-self: center;
  justify-self: center;
}

grid + margin auto

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: grid;
}
.son {
  margin: auto;
}

grid 和 flex 很像,是 flex 的升级版,所以 grid 能做的事情更多

以上绝对定位、flex、grid 关于水平垂直居中的做法,剩下再说居中比较老的布局方法

-webkit-box 属性居中

这是一个已经过时的布局,可以看看这篇文章 CSS3 display: flex 和 display: box 有什么区别?

网友一丝说:

flex 是 2012 年的语法,是以后的标准

box 是 2009 年的语法,已经过时,需要加上对应前缀

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: -webkit-box;
  -webkit-box-pack: center;
  -webkit-box-align: center;
}

table-cell + text-align

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: table-cell;
  vertical-align: middle;
  text-align: center;
}
.son {
  display: inline-block;
}

line-height + text-align

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  height: 200px;
  line-height: 200px;
  text-align: center;
}

line-heightheight ,行高和高度一样高,自然就垂直方向居中了

writing-mode

<div class="father">
  <div class="“son”">
    <div class="“sonson”">123123</div>
  </div>
</div>
.father {
  writing-mode: tb-lr;
  writing-mode: vertical-lr;
  text-align: center;
}

.father .son {
  writing-mode: lr-tb;
  writing-mode: horizontal-tb;
  text-align: center;
  width: 100%;
  display: inline-block;
}
.father9 .son .sonson {
  display: inline-block;
  text-align: initial;
}

这个很冷闷,有人介绍过这种情况

table

<table>
  <tbody>
    <tr>
      <td class="father">
        <div class="son">123123</div>
      </td>
    </tr>
  </tbody>
</table>
.father {
  text-align: center;
}

table 标签自己将它垂直居中了,text-align:center 后就是水平居中了

可以看 demo

元素有宽高的情况,又多了三种方案

须知宽高 + 绝对居中 + margin 负边距

<div class="father">
    <div class="son">
        123123
    </div>
</div>
.father {
  position: relative;
  height: 200px;
}
.son {
  width: 100px;
  height: 100px;
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -50px 0 0 -50px;
}

父元素必须要有个高度,这样才能撑开容器。子元素必须要有个宽高,才能计算出 margin 值

须知宽高 + 绝对定位 + calc

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  position: relative;
  height: 200px;
}

.son {
  width: 100px;
  height: 100px;
  position: absolute;
  top: calc(50% - 50px);
  left: calc(50% - 50px);
}

与 margin 负边距一个道理,父元素需要设置一个高度。子元素必须要有高度,不用 margin,而用 CSS3 中的 calc,计算出要居中位移,兼容性需要支持 CSS3 属性

须知宽高 + 绝对居中 + margin: auto

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  position: relative;
  height: 300px;
}

.son {
  width: 100px;
  height: 100px;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}

同以上两种情况。

这三种需要定位方式来实现水平垂直居中的方法,需要设置父元素的高度(一定要有,撑开画面),子元素需要设置宽高,前两种方法是为了算出它在父元素中的相对位置,后一种方法是为了说明子元素是个容器(如果不设置宽高,就是无)

其他方法

其实,水平垂直居中方面,如果面试官硬要问还有吗?还真的有,用 fixed 定位。但这个方法有缺陷,虽然能实现水平垂直居中,但它是相对于视口(viewport),而非父元素

方法就是以上用 absolute 实现的改成 fixed 即可

  • 须知宽高 + fixed + transform
  • 须知宽高 + fixed + 负 margin
  • 须知宽高 + fixed + calc
  • 须知宽高 + fixed + margin auto

这四种方法,都需要设置子元素的宽高

这里贴一下代码

<div class="father">
  <div class="son">123123</div>
</div>
/* transform */
.son {
  width: 100px;
  height: 100px;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: blue;
}

/* 负 margin */
.son {
  width: 100px;
  height: 100px;
  position: fixed;
  top: 50%;
  left: 50%;
  margin-left: -50px;
  margin-top: -50px;
  background: blue;
}

/* calc */
.son {
  width: 100px;
  height: 100px;
  position: fixed;
  top: calc(50% - 50px);
  left: calc(50% - 50px);
  background: blue;
}

/* margin: auto */
.son {
  width: 100px;
  height: 100px;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  margin: auto;
  background: blue;
}

总结

随着微软宣布放弃 IE11,现实项目中完全可以使用 flex 布局,grid 部分还不适配,但是以后肯定会取代 flex。

虽然写了很多,但是自己工作中也不会使用 table 、writing-mode、-webkit-box 等过时的布局方式,写这篇文章,纯粹是为了面试时被问到这种问题。

收获是 absolute 的居中要父子同心(父元素设置高度,子元素设置宽高),fixed 的居中只需要设置子元素的宽高。

线上 demo 查看

参考资料

芝麻开门,显示全文!

flex从总结到了解

flex 是一种布局方式,在 CSS3 之后开始有。它主要由父容器和子项组成,父容器有六个属性,分别为:

  • 控制主轴轴向:flex-direction
    • row:横轴(默认)
    • row-reverse:倒过来的横轴
    • column:竖轴
    • column-reverse:倒过来的竖轴
  • 换行方式:flex-wrap
    • nowrap:不换行(默认)
    • wrap:换行
    • wrap-reverse:反着换行
  • 主轴排列:justify-content
  • 交叉轴排列:align-items
  • 轴向与换行组合设置:flex-flow(流向)
    • 一般很少用这个属性,即改变子项的布局顺序,正着来,倒着来

子项也有六个属性,分别为:

  • 弹性扩展:flex-grow
    • 指定容器剩余空间多余时的分配规则
    • 默认值为 0,多余空间不分配
  • 弹性收缩:flex-shrink
    • 指定容器剩余空间不足时的分配规则
    • 默认值为 1,空间不足要分配;如果为 0,表示不分配
  • 基础尺寸:flex-basis
    • 指定 flex 元素在主轴方向上的初始大小(基础尺寸)
    • 默认值为 auto,即项目本身大小
  • 缩写:flex
    • flex-grow、flex-shrink、flex-basis 的缩写
    • 默认值为 0 1 auto
  • 主轴顺序:order
  • 交叉轴对齐方式:align-self

总的来说,父容器控制整体布局,子项控制子项布局

在面试中,常常不会问怎么宽泛,最常见的 flex 面试题为:

  • flex: 0 1 auto 怎么理解?
  • flex: 1具体代表什么,有什么应用场景
  • flex: 0flex: 1flex: noneflex: auto,表示什么意思,并应用在什么场景下使用?

要想回答这些问题,我们必须了解子项中的 flex 属性

flex 语法

flex: none | auto | [< "flex-grow" > < "flex-shrink" >? || < "flex-basis" >];

单管道符 | ,表示排他。也就是这个符号前后的属性值都是支持的,且不能同时出现。因此,下面这些语法都是支持的:

flex: auto;
flex: none;

flex: [< "flex-grow" > < "flex-shrink" >? || < "flex-basis" >];

方括号 [...] 表示范围。支持的属性在这个范围内

其中 ? ,表示 0 个或者 1 个,也就是说 flex-shrink 属性可有可无。因为 flex 属性值也可以是 2 个值

flex: auto;
flex: none;
/* 2个值 */
flex: 1 100px;
/* 3个值 */
flex: 1 1 100px;

双管道 || ,表示”或者“的意思。表示前后可以分开独立使用,也就是 flex: flex-grow flex-shrink?flex-basis 都是合法的。于是我们又多了 2 种合法的写法:

/* 1个值,flex-basis */
flex: 100px;
/* 2个值,flex-grow 和 flex-shrink */
flex: 1 1;

转为文字表述

单值语法:

如果 flex 的属性值只有一个值,有三种情况

  • 一个无单位数,例如例如 flex: 1,表示 flex-shrink: 1,剩余空间扩展。此时,flex-shrinkflex-basis 的值分别是 1 和 0%。注意,这里的 flex-basis 的值是 0%,而不是默认值 auto

    • 只要改变 flex: 数字flex-basis 的值就为 0
  • 一个有效的宽度(width)值,表现形式为长度值,例如 flex: 100px,表示flex-basis: 100px,基础尺寸为 100px。此时,flex-growflex-shrink 的值都是 1,注意,这里的 flex-grow 的值是 1,而不是默认值 0

  • 关键字 noneautoinitial

双值语法:

如果 flex 的属性值有两个值,则第 1 个值一定是 flex-grow,第 2 个根据值的类型不同表示不同的 CSS 属性,具体规则如下:

  • 数值:例如flex: 1 2,则这个 2 表示 flex-shrink,此时 flex-basis 的值为 0%,而非默认值 auto
  • 长度值,例如flex: 1 100px,则这个 100pxflex-basis,此时 flex-shrink 默认值为 0

三值语法:

如果 flex 的属性值有 3 个值,则长度值表示 flex-basis,其余 2 个数值分别表示flex-growflex-shrink。下面两行 CSS 语句的语法都是合法的,且含义也是一样的:

flex: 1 2 50%;
flex: 50% 1 2;

flex 属性值场景应用

flex 默认值为 0 1 auto。除此之外,还有各种其他值

  • flex: none,等同于 flex: 0 0 auto;

  • flex: auto,等同于 flex: 1 1 auto;

  • flex: 1,等同于 flex: 1 1 0%;

  • flex: 0,等同于 flex 0 1 0%;

张鑫旭大神画过一张图:

单值语法等同于备注
flex: initialflex: 0 1 auto初始值,常用
flex: 0flex: 0 1 0%适用场景少
flex: noneflex: 0 0 auto推荐
flex: 1flex: 1 1 0%推荐
flex: autoflex: 1 1 auto适用场景少

默认值 flex: initial

它等同于 flex:0 1 auto,表示 flex 容器有剩余空间时尺寸不增长(flex-grow: 0),flex 容器尺寸不足时尺寸会收缩变小(flex-shrink:1),尺寸自适应于内容(flex-basis:auto)

我的理解:子项总长度小于总容器时,不会去撑满(flex-grow:0),而按实际宽高度存在(flex-basis:auto);当子项总长度大于总容器时,子项会相对于的收缩相对比例(flex-shrink:1)

适用场景

适用于子项总长度小于总容器的场景,例如按钮、标题、小图标等小部件的排版布局

flex: 0 和 flex: none 的区别

flex: 0 等同于设置 flex: 0 1 0%flex:none 等同于 flex: 0 0 auto

flex: 0,因为是一个值且为数值,所以它表示 flex-grow,后续我发现只用设置了flex: 数字,那么 flex-basis 就自动成了 0%,所以,设置flex:0 的元素的最终尺寸表示为最小内容宽度;

注意:

flex: 1 === flex: 1 1 0%

flex: 0 === flex: 0 1 0%

flex 设置为数字后,虽然 flex-basis 为最小宽度,但是前者的 flex-grow 有值,可以把子项扩充满容器,后者为 0,不扩展

flex: none,既不是数值也不是长度值,none 关键字。flex: 0 0 auto 表示元素尺寸不会收缩也不会扩展,再加上 flex-basis: auto 表示固定尺寸由内容决定,由于元素不具有弹性,因为,元素内的元素不会换行,最终尺寸通常表现为最大内容宽度

适用使用 flex: 0 的场景

flex:0的应用场景

无论文字的内容给如何设置,左侧内容的宽度都是图像的宽度

适合使用 flex: none 的场景

当 flex 子项的宽度就是内容的宽度,且内容永远不会换行,则适合使用 flex:none,例如如下的场景,图片和按钮固定长度,内容弹性

flex:none适用场景

flex: 1 和 flex: auto 的区别和适用场景

flex:1 等同于设置 flex: 1 1 0%flex: auto 等同于 flex: 1 1 auto

可以看出两者的 flex-growflex-shrink 都是一样的,意味着它们都可以弹性扩展以及弹性收缩,区别在于 flex: 1flex-basis 为 0,即宽度为 0。flex:auto 中的 flex-basis为 auto,即宽度为自身宽度

表现的样子为:

flex:1

这里需要解释一下,因为我最开始也不理解,其公式为:

每个子项的宽度 = (总宽度 - flex-basis 的宽度)/ 3(以这个例子为例)

因为 flex:1flex-basis 的宽度为 0 ,所以最后它的总宽度扩张或者收缩时每个子项都能等分

适用于 flex: 1 的场景

当希望元素充分利用剩余空间,同时不会侵占其他元素应用的宽度的适用,适合适用 flex:1,例如所有的等分列表

之前适用 flex: none 的例子,同样设置文字部分flex: 1 也能实现类似的效果

flex:1

适用于 flex: auto 的场景

当希望元素充分利用剩余空间,但是各自的尺寸按照各自内容进行分配的时候,适用于 flex: auto

例如导航数量不固定,每个导航文字数量页不固定的导航效果就适合适用 flex: auto

flex-auto

回过头来看之前说的面试题

  1. flex: 0 1 auto 怎么理解?
  2. flex: 1具体代表什么,有什么应用场景
  3. flex: 0flex: 1flex: noneflex: auto,表示什么意思,并应用在什么场景下使用?

第一个问题回答

flex 的默认值为 0 1 auto,表示容器剩余空间有多余的时候不扩展,不足的时候收缩,子项的宽度根据自身的宽度来展示

第二个问题回答

脑子思考 flex 的值如果是一个值且为数字,说明是 flex-grow:1,当它为数字时,flex-basis 会自动变成 0,所以它具体表示为 flex:1 1 0%,表示容器剩余空间有多余的时候扩展,不足的时候收缩,子项的宽度为 0。它一般适用于充分利用剩余空间,又不侵占其他元素的宽度,例如等分布局

第三个问题回答

flex:0,表示 flex: 0 1 0%,表示容器剩余空间有多余的时候不扩展,不足的时候收缩,子项的宽度为 0,适用设置在替换元素的父元素上

flex:1,看第二个回答

flex: none,表示 flex: 0 0 auto,表示容器剩余空间有多余的时候不扩展,不足的时候也不收缩,子项的宽度为自身宽度,适用于不换行的内容或者较少的小控件元素上

flex: auto,表示 flex: 1 1 auto,表示容器剩余空间有多余的时候扩展,不足的时候收缩,子项的宽度为自身宽度,适用于基于内容动态适配的布局(例如导航数量文字长度不固定)

flex:initial,表示 flex: 0 1 auto,表示容器剩余空间有多余的时候不扩展,不足的时候收缩,子项的宽度为自身宽度,适用于小控件元素的分布布局,或者某一项内容动态变化的布局

参考资料

芝麻开门,显示全文!

编程题:为什么最后一个a是1不是5

最近立下的 flag 是每周回答至少三个知乎回答,不限编程,希望能提高自己的书面表达能力。这不,有人邀请我回答一个问题:为什么最后一个 a 是 1 不是 5?

题目如下:

console.log(a);
if (true) {
  a = 1;
  function a() {}
  a = 5;
  console.log(a);
}
console.log(a);

结果

我的第一反应是:undefined,5,5。估计和题主想的一样

分析一波

假设没有 if(true),即如下代码:

console.log(a);
a = 1;
function a() {}
a = 5;
console.log(a);
console.log(a);

那么答案什么?

a()、5、5

这解释了两个特性

  1. 变量、函数提升且函数的权重大于变量
  2. 在 a 没有用 var 声明时,a=XX 默认是用 var 来声明

变量、函数提升方面的知识点在于:

变量会提升,函数也会提升,并且函数提升的优先级大于变量,如下例:

console.log(a);
console.log(a());
var a = 1;
function a() {
  console.log(2);
}
console.log(a);
var a = 3;
a = 4;
console.log(a);
console.log(a());

a()、2、1、4、a is not a function

回过头来看这道题目

console.log(a);
if (true) {
  a = 1;
  function a() {}
  a = 5;
  console.log(a);
}
console.log(a);

if (ture) {} ,形成了作用域,锁住了这片变量,function a(){} 无法逃逸。换句话说,只有 {} 块级标识符在,function a() {} 就被所在块级作用域中,也就说在 if (ture) {} 这片块级作用域下,它不会提升到全局顶层,而是在 if(true){} 下,即代码执行时是这样:

console.log(a);
if (true) {
  +function a() {};
  a = 1;
  -function a() {};
  a = 5;
  console.log(a);
}
console.log(a);

如果你在 a = 1 前打印 a,a 的值就是 function a(){}

所以这道题全局环境下,没有变量提升,写在第一行的 console.log(a) 因为找不到 a,所以值为 undefined

进入 if(true) {} 中,function a(){} 函数提升,且权重最高,所以赋值之前的块级作用域中的 a 为 function a() {}window.aundefined

代码执行到 function a() {} 后,块级作用域中的 a 还是为 1,但是全局变量 a 被赋值为 1

执行到 a = 5,传统赋值,影响的是块级作用域中的 a,而不会影响全局变量 a,所以打印的第二个 console.log(a) 为 5,第三个 console.log(a) 为 1

那么问题来了,为什么一执行 function a(){},全局变量 a 就被赋值为 1?

我陷入的沉思,后来在回答中发现[云补断山]回答了,说是

历史原因,为了兼容之前的 ES5 的语法,所以在规范规定了块级作用域内函数声明的一些行为,各个浏览器实现可能不一样

简单来说,在块级作用域内的函数函数声明,行为类似于 var ,都会在全局作用域声明一个同名变量(也就是 window 上挂一个同名的属性,默认值是 undefined),因为 ES6 遇到块级作用域,会基于块级作用域创建 environment record,存放当前块级作用域内的变量,所以这个函数声明会提升到块级作用域顶部(而非全局作用域顶部)

ECMA262 目录 B

我们学的 JavaScript 是 ECMAScript,但是我们把代码运行在浏览器上时就要按照浏览器的标准,浏览器里会有一些私货在,最经典的是 __proto__ ,倒逼 ECMA 采纳。话说回来,按照这位仁兄的意思

// 因为 function a() 声明过,所以全局有个 window.a
console.log(a);
if (true) {
  // 声明归声明,但是函数提升提升与作用域相关,所以提升至此块级作用域顶部
  a = 1;
  // 块级作用域中的 a 被赋值为 1
  function a() {}
  // 原地爆炸,执行函数后,全局 window.a 被赋值为块级作用域中的 a
  a = 5;
  // 块级作用域中的 a 又被赋值为 5
  console.log(a);
}
console.log(a);

最诡异的是执行 function a() {} 后,全局 window.a 被赋值且为块级作用域中的 a

这个事情没完!!

等等,我就说的玩玩的,如果工作中或面试中真遇到这类问题,我也许还是不会解。

太诡异了,这不是考题范围(块级作用域、函数提升、变量提升)

就这样先吧

芝麻开门,显示全文!

项目实战:弹出广告任意页面展示

最近接到一个需求,产品经理希望能新增弹窗广告,广告可根据后台配置在应用任意页面弹出展示。当后台改变当前页面广告次数、链接或者目标页后,当前页面数据修改,不影响其他页面数据

例如后台设置“首页”出现广告 1 次,“我的”页面广告出现 3 次,用户进去后关闭了“首页”广告 1 次,关闭了“我的”页面广告 2 次。此时退出应用,后台将“首页”广告设置为 2 次,那么该用户“首页”广告重置为 2 次,“我的”页面广告仍为 1 次( 3 - 2)

需求分析

后端返回的数据必然是个数组,每个对象中会有目标页(展示的页面),跳转链接总出现的次数三参数。前端要对数据进行处理:

  • 当本地没有数据时(第一次进入),将总出现次数赋值给一参数 firstTotalTimes(记录原总出现次数)
  • 当本地有数据(非第一次进入)
    1. 将本地存储中的 firstTotalTimes 清除,返回值赋值为 removeLocalTotalTimeList
    2. 将 removeLocalTotalTimeList 与 请求返回的数据 advertisementList 进行对比
      • 相等,说明后台数据没有改变,查看你本地存储中的总出现次数是否大于 0 ,大于则展示广告
      • 不相等,说明后台修改了数据,这里还要分析,只重置修改处页的,未修改的地方不做处理

笔者用的框架是 umi3,其中有 wrappers 概念,即一个配置路由的高阶组件封装,在 umi.conf 中加上后,任何页面都要先经过这一道。关键代码如下:

useEffect(() => {
    dispatch({ type: 'common/fetchGetPopUpAdvertisementList' }).then((resData: any) => {
        if (resData?.resultCode === "S00000") {
            if (!localStorage.advertisementList) {
            const addFirstTotalTimes = resData.advertisementList.map((item: any) => {
                item.firstTotalTimes = item.totalTimes
                return item;
            })
            localStorage.advertisementList = JSON.stringify(addFirstTotalTimes);
        }

        const localAdvertisementList = JSON.parse(localStorage.advertisementList)

        const cloneLocalAdvertisementList = JSON.parse(JSON.stringify(localAdvertisementList))

        const removeLocalTotalTimeList = cloneLocalAdvertisementList.map((item: any) => {
            delete item.firstTotalTimes
            return item
        })
        if (_.isEqual(removeLocalTotalTimeList, resData.advertisementList)) {
            console.log('相等')
            localAdvertisementList.filter((item: any) => {
                if (item.targetUrl.indexOf(history.location.pathname) > -1) {
                    if (item.firstTotalTimes > 0) {
                        setAdItem(item)
                    }
                }
            })
        } else {
            console.log('不相等')
            const cloneList = JSON.parse(JSON.stringify(resData.advertisementList));
            for (let i = 0; i < cloneList.length; i++) {
                for (let j = 0; j < cloneLocalAdvertisementList.length; j++) {
                    if (_.isEqual(cloneList[i].pkId, cloneLocalAdvertisementList[j].pkId)) {
                        if (_.isEqual(cloneList[i], cloneLocalAdvertisementList[j])) {
                            cloneList[i].firstTotalTimes = localAdvertisementList[j].firstTotalTimes
                        } else {
                            cloneList[i].firstTotalTimes = cloneList[i].totalTimes
                        }
                    }
                }
            }
            localStorage.advertisementList = JSON.stringify(cloneList);
            cloneList.filter((item: any) => {
                if (item.targetUrl.indexOf(history.location.pathname) > -1) {
                    if (item.firstTotalTimes > 0) {
                        setAdItem(item)
                        setIsShow(true)
                    }
                }
            })
        }
    }
                                                                     })
}, [])

难点

JS 的数据可变性

第一个坑点在 JS 的数据是可变的,所以要对其数据进行深拷贝,才不会影响到其他数据,这里我用了最简单的深拷贝:JSON.parse(JSON.stringify)

const cloneLocalAdvertisementList = JSON.parse(
  JSON.stringify(localAdvertisementList)
);

判断后台那个数据修改

在之前表述中已经表明,当本地存储和请求过来的数据不一致时要判断,哪要做重置,哪些页面则维持原状。这就要对两个数组进行对比,最简单的方法就是做双循环(On2).

const cloneList = JSON.parse(JSON.stringify(resData.advertisementList));,深拷贝后台返回数据,这样对 cloneList 进行处理时就不会影响到原数据。cloneLocalAdvertisementList 则是本地的存储

if (_.isEqual(cloneList[i].pkId, cloneLocalAdvertisementList[j].pkId)) ,pkId 是广告唯一标识,先识别数组中的每一个对象,这是一一对应的,再判断 if (_.isEqual(cloneList[i], cloneLocalAdvertisementList[j])) ,对比对象中的值,如果是 true,即完全相等,说明后台数据没有变化,那就将本地存储中的 firstTotalTimes 赋值给 cloneList 上的 firstTotalTimes 。如果是 false,说明后台已经修改,就把 firstTotalTimes 重置为本次拉取数据中的 totalTimes

const localAdvertisementList = JSON.parse(localStorage.advertisementList)
const cloneLocalAdvertisementList = JSON.parse(JSON.stringify(localAdvertisementList))
 ...
const cloneList = JSON.parse(JSON.stringify(resData.advertisementList));
for (let i = 0; i < cloneList.length; i++) {
    for (let j = 0; j < cloneLocalAdvertisementList.length; j++) {
        if (_.isEqual(cloneList[i].pkId, cloneLocalAdvertisementList[j].pkId)) {
            if (_.isEqual(cloneList[i], cloneLocalAdvertisementList[j])) {
                    cloneList[i].firstTotalTimes = localAdvertisementList[j].firstTotalTimes
                } else {
                    cloneList[i].firstTotalTimes = cloneList[i].totalTimes
            }
        }
    }
}

以上,就是对这次项目的核心代码,当然,还要考虑到 App 端打开和 微信打开的差异,以及当未登录状态下的去登录后数据的更新等等,但这些可以通过监听登录来判断(useEffect 依赖数据)实现

总结

这次被数据可变性坑了,通过 debugger 来排查

双循环在实际项目中用的次数不多,所以对此做记录

芝麻开门,显示全文!

微信网页授权

微信网页授权步骤差不多有三步,具体文档可查看这里,我画了下流程图:

微信授权流程图

以下为代码实战

第一步:用户同意授权,获取 code

需先调用 /auth 接口,传入必传参数 url 以及 scope(此为参数名)

请求方式:GET

  • url 为回调地址
  • scope 有两个可选参数
    • snsapi_base 只能获取进入页面用户的 openid,用户无感知,叫静默授权
    • snsapi_userinfo 能获取用户的基本信息,但需要用户接受,叫手动授权,如下图

snsapi_userinfo示意图

具体区别可前往 微信文档 查看

第二步:通过 code 换取网页授权 access_token

这里以手动授权为例

获取到微信的 code 后,再请求 /getUserInfo

请求方式: GET

请求参数:code,需请求 /auth 获取到 code 先,如果你在请求 /auth 时传入的 scopesnsapi_userinfo , 那么返回微信个人信息,包括微信名,性别,所在地区,国籍,头像等等,如下

{
  "openid":" OPENID",
  "nickname": NICKNAME,
  "sex":"1",
  "province":"PROVINCE",
  "city":"CITY",
  "country":"COUNTRY",        "headimgurl":"https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
  "privilege":[ "PRIVILEGE1" "PRIVILEGE2"     ],
  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

但如果 scopesnsapi_base ,请求成功时只返回用户的 openid

PS: 请求/getOpenId/getUserInfo 成功时会返回 access_token,但此 access_token 和 微信服务端开发中的 access_token 不同,一个是微信与服务器打交道(微信票据服务),另一个是微信网页的 OAuth2.0 服务(网页授权)

第三步:请求 userInfo

拿着 access_token 和 openid,去请求微信官方接口

http:GET(请使用 https 协议) https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

返回 openid、nickname、sex、province、city、country、headimgurl 等信息,拿着 openid 和你想要的数据返回到原来 /auth 参数中的 url 上

实战

先调用 /auth 接口,传入参数 url 和 scope

请求接口:http://192.168.230.209/auth?url=http://192.168.230.209/home&scope=snsapi_userinfo

redis 存 url=http://192.168.230.209/home,即最后授权完成拿到数据后返回的前端地址

判断参数 scope,如果是 snsapi_userinfo,用户点击授权后跳转至 /getWxUserInfo 接口;

如果是 snsapi_base,静默授权后跳转至 getOpenId 接口

这里我们传的 scope 为 snsapi_userinfo,所以请求成功后会有授权页面

授权示意图

点击”同意“会跳转至页面

http://192.168.230.209:8888/api/wechat/getWxUserInfo?code=081UcAFa1s1OAz0o7wGa1wb8vG1UcAFX&state=123

PS:http://192.168.230.209:8888/api/wechat 为该后端服务地址,getWxUserInfo 为路由(即请求接口)

ctx.request.query 中拿到 code,拿着 code 请求 access_token 服务,access_token 服务也是微信官方提供的一个方法

获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

请求成功的话,拿着这个返回值中的 access_token 和 openid,请求 userinfo 接口,在上文已经介绍过,这里不做重复

这里要说明的一点是,如果请求 access_token 的返回 code 为 40029,说明 access_token 已经失效,我们需要重新刷新 access_token

拿到 userinfo 的返回值后,在最开始存在 redis 中的 url 上拼接 openid、headimgurl 等即可

这里需要说明一点

需要先配置 OAuth2.0 网页授权的回调页面域名,类似这种

授权回调页面域名

总结

一定要知道一点,微信网页开发和调用微信的 JS-SDK 不一样,也和微信服务端开发不一样

它可以当初拎出来说,坑也比较少,不会遇到像 JS-SDK 那样的各种报错

只要知道,它为为了获取 openid (以及微信个人信息)而弄的一个服务就好了

芝麻开门,显示全文!

实战独立项目「几行字」:从想法到上线全过程

前言

之前尝试过几个小项目,自己也很想做独立的项目,这种自己创造一样东西的感觉很棒,奈何之前太差,虽然现在能力也不是特别强,但好歹有这个心了

我的最终想法是想做个关于”中国美“的项目,但是这个项目太大,能实现是一件特别有成就的事情,但现在还是先做一个独立的项目先

这里记录自己的想法,非礼勿言

想法

最开始是看到 毒鸡汤 的项目(作者自己的域名已经不能访问,当初自己为了学习部署,也搞了一份,网址:),觉得很有趣,简单又有趣

后来看到 今日诗词,这不是差不多吗,无非是提供了 API 调用罢了。

这两则的 star 数都超过了 1000+,这么简单的应用竟然这么受欢迎,有点羡慕嫉妒感

因为好奇,接触了 vite 、tailwindcss 等新技术,就想着用 vite 搭建一个 react 应用,样式用 tailwindcss 定制,于是就想要做个简单的应用,后来脑洞越想越大,就有了后续的规划,直接说规划

规划

这个项目从想法、画原型、写前端、做设计、部署、搞后端、后台一整套,从想法到实现

我最截止到写这篇文章时的规划是:

第一阶段:提出想法、画出原型、做好一个静态页面、部署到线上,即静态独立项目

第二阶段:用 vite + react 开发此项目,并添加功能点,如可选主题色、分享卡片等功能

第三阶段:数据不能裸泳,配置后端功能以及后台编辑功能

第四阶段:将其做成 Flutter 版本

第五阶段:将其做成小程序版本

这五个阶段笔者不会一口气做出来,有些东西只是想法,具体实施时困难肯定比想象中多的很

收集素材

之前混知乎,也关注了几个关于句子的问题,例如 你读过的最有力量的一段文字是什么?有哪些适合摘抄的句子 ,有些句子很喜欢,有些能受启发,与其这样,不如把有些高赞的句子收集起来,也做成像 毒鸡汤、今日诗词这样应用

于是乎,每天去知乎上手动收录素材,加上自己以前的库存,大概收集了 100 多条数据(写于第一阶段),

画原型

主要以简洁为主,能不要的东西统统不要,大致画出了这样

原型

写页面

初始化页面

npm init -y

为什么要弄功能?因为我们要用到 tailwind,它官方支持用这种方式,等 build 的时候会 tree-shake,能减少很多不必要的代码

后续可看 官网安装指南

通过 npm 安装 Tailwind

npm install tailwindcss@latest postcss@latest autoprefixer@latest

作为 PostCSS 插件来添加 Tailwind

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

创建 tailwindcss 配置文件

npx tailwindcss init

这将会在您的工程根目录创建一个最小的 tailwind.config.js 文件。

// tailwind.config.js
module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
};

新建 tailwind.css

引入 tailwind 代码

@tailwind base;
@tailwind components;
@tailwind utilities;

新建 index.html

在代码中引入 css、以及编写 html 代码

这里我不细讲,因为花了不少时间,参数太多,大多数是看到符合自己原型的就拿来,然后删删改改

新建 data.js

之前收集了不少素材,将其导入到 data.js 中,并且编写以下逻辑,浏览器中读过的句子保存在本地存储里。句子是随机生成,如果随机生成的句子在本地存储中,那就重新生成。当所有的句子都存在本地存储中的话,就清空所有的本地存储。

因为我的内容有些不是一句话,而是一个数组,所有在插入内容时也需要判断,根据不同的情况做出不同的效果

这里遇到一些问题记录下,太久没有写原生,插入 html 的 api 忘记了,innerHTML 和 appendChild 的区别忘记了

innerHTML :可以插入一段 html,例如

我是 p 标签

appendChild :在内容末插入节点,要先创建标签,在插入

封装成三个方法,即拉取数据,存本地存储,插入网页

做设计

参考了一些别人做 logo 的建议,推荐比较多的是 logo 神器,我按照提示做下来是这样的设计

logo

我表示遗憾,从个人审美上来看,这种设计太傻瓜了,所以自己用 Photoshop 做了一个,

SEO 优化

favicon 处理

在 logo 中扣出 字,然后上传至 https://favicon.io/ 上,导出 favicon,

设置 header 信息

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>几行字</title>
  <meta
    name="description"
    content="几行字给你温暖、几行字给你激励、几行字给你灵感"
  />
  <meta name="keywords" content="几行字,文案,几行世界" />
  <meta http-equiv="Cache-Control" content="no-siteapp" />
  <meta property="og:title" content="几行字" />
  <meta property="og:image" content="./favicon.ico" />
  <meta property="og:site_name" content="几行字" />
  <meta
    property="og:description"
    content="几行字,几行字给你温暖、几行字给你激励、几行字给你灵感"
  />
  <link rel="alternate icon" type="image/x-icon" href="favicon.ico" />
  <link rel="stylesheet" href="style.css" />
</head>

也写不出什么关键字来,先这样,后期有灵感了再补上

部署

笔者之前写过利用 Github Actions 部署前端 ,也成功部署过 毒鸡汤,大致流程如下

  1. 申请阿里云容器镜像服务
  2. 将代码推到 Github 仓库,触发 Github Actions
    1. Github Actions 中登录 阿里云容器镜像服务,将代码打包成一个镜像,并推到个人镜像站远端
    2. 再登录服务器,执行拉取镜像脚本

主要逻辑是这样,但执行起来很麻烦,还不如直接部署来着算,什么直接部署,就是本地部署到线上,最有用的当属 now,也就是现在的 vercel,笔者之前部署过好几个项目,所以轻车熟路

直接部署上线:https://jihangzi-static.vercel.app/

在阿里云做一下映射:https://jihangzi.azhubaby.com/

第一阶段到此就告一段落

芝麻开门,显示全文!

使用微信wx-open-launch-app标签实现微信网页打开App

前提须知

笔者公司的项目在微信端的功能定位为基础功能交易及服务,通知用户交易提醒、交易流水等,而 APP 为主要的交易功能。之前是在多个页面有引流按钮跳转至 App,功能点比较粗暴,直接 location.href = 应用宝链接。现在产品有需求,说要用微信提供的标签来唤起 App

需求点:

所有跳转至 App 下载页面的部分,改成

需求点

Demo 先行

遇事不决,官网文档。查看后与微信 JS-SDK 功能点很像,这里我不废话,直接跳过。按照官网 demo,把示例写进业务代码中

import React, { useEffect, useRef } from 'react';
import { toDownloadApp, isWechat, getWeixinVersion } from 'utils';

const Download = () => {

    const wxRef = useRef(null)

    useEffect(() => {
        if (wxRef.current) {
            // @ts-ignore
            wxRef.current?.addEventListener('launch', function (e: any) {
                console.log('success');
            });
            // @ts-ignore
            wxRef.current.addEventListener('error', function (e) {
                console.log('fail', e.detail);
                toDownloadApp()
            });
        }
    }, [])

    const onHandleClick = () => {
     	toDownloadApp()
    }

    return (
        <div className="Download" onClick={onHandleClick}>
            {/*  @ts-ignore */}
            <wx-open-launch-app
                ref={wxRef}
                appid="XXXX"
            >
                <script type='text/wxtag-template'>
                    <button>App内查看</button>
                </script>
                {/*  @ts-ignore */}
            </wx-open-launch-app>
        </div>
    )
}

export default React.memo(Download);

测试成功,demo 能跑通

组件试点

现在搞业务,以这个组件(Download)为试点展开,我要点击页面顶部的卡片(多个地方使用,抽离成 Download 组件),让其唤起 App,但是要判断其版本,如果版本过低,让其跳转至应用宝

import React, { useState, useEffect, useRef } from 'react';
import LogoImg from '@/assets/images/logo.png';
import { toDownloadApp, isWechat, getWeixinVersion } from 'utils';

const Download = () => {

    const wxRef = useRef(null)
    const [enableLaunchWeapp, setEnableLaunchWeapp] = useState(false);

    useEffect(() => {
        const wxVersion = isWechat() && getWeixinVersion() || ''
        if (wxVersion) {
            let v = wxVersion.split('.')
            if (Number(v[0]) >= 7) {
                if (Number(v[1]) >= 0) {
                    if (Number(v[2]) >= 12) {
                        setEnableLaunchWeapp(true)
                    }
                }
            }
        }
        if (wxRef.current) {
            // @ts-ignore
            wxRef.current?.addEventListener('launch', function (e: any) {
                console.log('success');
            });
            // @ts-ignore
            wxRef.current.addEventListener('error', function (e) {
                console.log('fail', e.detail);
                toDownloadApp()
            });
        }
    }, [])

    const onHandleClick = () => {
        if (!enableLaunchWeapp) {
            toDownloadApp()
        }
    }

    return (
        <div className="Download" onClick={onHandleClick}>
            <div className="Download__logo">
                <img src={LogoImg} alt="logo" />
            </div>
            <div className="Download__content">
                <div className="Download__content-title">雅美App</div>
                <div className="Download__content-desc">长泽雅美服务专区</div>
            </div>
            {/* <div>1</div> */}
            <div className="Download__btn">立即打开</div>
            {/*  @ts-ignore */}
            <wx-open-launch-app
                ref={wxRef}
                appid="XXXXX"
                style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '60px', opacity: 0.3, background: 'blue' }}
            >
                <script type='text/wxtag-template'>
                    <div style={{ position: 'fixed', top: 0, left: 0, width: '90%', height: '100%', opacity: 0.3, background: 'red' }} />
                </script>
                {/*  @ts-ignore */}
            </wx-open-launch-app>
        </div>
    )
}

export default React.memo(Download);

效果如下所示:

点击范围

思路逻辑参考:wx-open-launch-weapp 样式问题,我也给它配上颜色,方便后续观察

测试同步,能点击卡片跳转,好,下一步,在所有需要点击跳转页面的地方加入类似这样的代码

<wx-open-launch-app
  ref={wxRef}
  appid="XXXX"
  style={{
    position: "fixed",
    top: 0,
    left: 0,
    width: "100%",
    height: "60px",
    opacity: 0.3,
    background: "blue",
  }}
>
  <script type="text/wxtag-template">
    <div
      style={{
        position: "fixed",
        top: 0,
        left: 0,
        width: "90%",
        height: "100%",
        opacity: 0.3,
        background: "red",
      }}
    />
  </script>
  {/*  @ts-ignore */}
</wx-open-launch-app>

封装组件 WxOpenLaunchApp

如果是这样,就可以将其封装成一个组件了,起个名吧: WxOpenLaunchApp

将唤起 App 的内容包装成一个组件,暴雷 children 和 style 两个 props,代码如下:

import React, { useEffect, useRef, forwardRef } from 'react';
import { toDownloadApp } from 'utils';

export interface WxOpenLaunchAppProps {
    children: React.ReactNode;
    style?: React.CSSProperties;
}

const WxOpenLaunchApp: React.FC<WxOpenLaunchAppProps> = props => {
    const { style, children } = props;

    const wxRef = useRef(null)

    useEffect(() => {
        if (wxRef.current) {
            // @ts-ignore
            wxRef.current?.addEventListener('launch', function (e: any) {
                console.log('success');
            });
            // @ts-ignore
            wxRef.current.addEventListener('error', function (e) {
                console.log('fail', e.detail);
                toDownloadApp()
            });
        }
    }, [])

    return (
        <div className="wx-open-launch-app">
            {/*  @ts-ignore */}
            <wx-open-launch-app
                ref={wxRef}
                appid="XXXX"
                style={style}
            >
                <script type='text/wxtag-template'>
                    {children}
                </script>
                {/*  @ts-ignore */}
            </wx-open-launch-app>
        </div>
    )
}

export default React.memo(WxOpenLaunchApp);

那么 Download 组件也就可以干净很多

...
const Download = () => {
    ...
    return (
    	...
        	<div className="Download__btn">立即打开</div>
            {/*  @ts-ignore */}
            <WxOpenLaunchApp style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '60px', opacity: 0.3, background: 'blue' }}>
                <div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', opacity: 0.3, background: 'red' }} />
            </WxOpenLaunchApp>
		...
    )
}
...

业务组件 OpenAppPopup

回到需求点,每个点击的地方都要弹出弹出框,点击 打开 App ,再唤起 App,这样的话,弹出框 + WxOpenLaunchApp 就可以结合成一个组件,放出来供页面调用,名字就叫 OpenAppPopup ,代码如下:

import React, { FC } from 'react';
import { Popup, WxOpenLaunchApp, Toast } from 'components'; // 此乃公司自研组件库

export interface OpenAppPopupProps {
    show: boolean;
    onCancel: () => void;
    onSubmit: () => void;
}

const OpenAppPopup: FC<OpenAppPopupProps> = (props) => {
    const { show, onCancel, onSubmit } = props;

    return (
        <Popup.Group show={show}>
            <Popup.Confirm
                title="抱歉,此功能需在雅美App中使用"
                btnSubmitText={
                    <div style={{ position: 'relative' }}>
                        打开App
                        <WxOpenLaunchApp style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', opacity: 0.3, background: 'blue' }}>
                            <div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', opacity: 0.6, background: 'red' }} />
                        </WxOpenLaunchApp>
                    </div>
                }
                onCancel={onCancel}
                onSubmit={onSubmit}
            />
        </Popup.Group>
    )
}

export default React.memo(OpenAppPopup);

示意图如下:

OpenAppPopup

接着在所有有跳转 App 的页面上用上这块逻辑即可

封装 HOOK

每个页面点击类似 下载App 按钮时,会弹出 OpenAppPopup,点击 打开App,需要判断你的微信版本,是否达到 7.0.12,如果每个页面都要加上这段

const wxVersion = (isWechat() && getWeixinVersion()) || "";
if (wxVersion) {
  let v = wxVersion.split(".");
  if (Number(v[0]) >= 7) {
    if (Number(v[1]) >= 0) {
      if (Number(v[2]) >= 12) {
        setEnableLaunchWeapp(true);
      }
    }
  }
}

真的太恶心了,果断抽离成 hook。代码如下:

import { useState, useEffect } from "react";
import { isWechat, getWeixinVersion } from "utils";

const useEnableLaunchWeapp = () => {
  const [enableLaunchWeapp, setEnableLaunchWeapp] = useState(false);
  useEffect(() => {
    const wxVersion = (isWechat() && getWeixinVersion()) || "";
    if (wxVersion) {
      let v = wxVersion.split(".");
      if (Number(v[0]) >= 7) {
        if (Number(v[1]) >= 0) {
          if (Number(v[2]) >= 12) {
            setEnableLaunchWeapp(true);
          }
        }
      }
    }
  }, []);
  return enableLaunchWeapp;
};

export default useEnableLaunchWeapp;

逻辑也很简单,在刚加载时判断它是否可以点击,可以点击,就设置 enableLaunchWeapp 为 true。使用方法也很简单

import React, { useState, useEffect } from 'react';
import { Dispatch, History } from 'umi';
import {  OpenAppPopup } from 'components';
+import { useEnableLaunchWeapp } from 'hooks';
import { toDownloadApp } from 'utils';

interface KVProps {
    history: History;
}

const KV: React.FC<KVProps> = (props) => {
    const { history } = props;

    const [isShow, setIsShow] = useState(false);

    +const enableLaunchWeapp = useEnableLaunchWeapp();

    const onHandleClickToBuy = () => {
        setIsShow(true);
    };

    const onHandleClickToSubmit = () => {
        +if (!enableLaunchWeapp) {
        +    toDownloadApp()
        +}
    }

    return (
        <div className="KV" style={{ background: kvBgColor }}>
            <div className="KV__content">
                <img src={img} alt="" />
            </div>
            <OpenAppPopup
                show={isShow}
                onCancel={() => {
                    setIsShow(false);
                }}
                onSubmit={onHandleClickToSubmit}
            />
        </div>
    );
};

export default React.memo(KV);

与 App 交互

需求点里说:要在所在页面跳转至 App 相对页面,文档上写的很明显,可以传参数 extinfo="your-extinfo",随便写了个让客户端同事先测试先

未唤醒 App

我手机是 IOS 的,是可以唤起的,但是安卓同事调试的时候说,后台运行时,可以唤起 App,但是没有切换动作;如果杀掉进程,就无法唤起。而这问题,大概率是 SDK 配置的问题,同事看了半天没解决,扔给他 Android 接入指南 。我又看不懂 Android,只能看他了

如果测试成功,能跳过去,那么就把本页链接当作 extinfo 传过去,他那边接收到 extinfo 后,做个映射表,跳转至自身的页面即可,所以 WxOpenLaunchApp 需要改造,多一个 extinfo 参数。。。

后记

因为我们用的是 flutter,同事说,因为引入的第三方库不支持,所以跳不过去,所以这个功能要后置,等他搞定了我再做更新

错误处理

除了在 WxOpenLaunchApp 组件中加入监听 error,错误就让它跳转至 App 外,还要做当微信或者系统版本不支持微信标签时,需要监听并进行回退兼容,代码如下:

document.addEventListener('WeixinOpenTagsError', function (e: any) {
  console.error(e.detail.errMsg) // 无法使用开放标签的错误原因,需回退兼容。仅无法使用开发标签,JS-SDK其他功能不受影响
  toDownloadApp()
})

总结

又复用就抽离成组件

必须要上生产环境,所以最好是有个预生产环境

参考资料

芝麻开门,显示全文!

张一鸣微博记录

这里记录张一鸣曾经在微博上说过的一些话,不全,仅记录对当前自己有用的东西

  1. 年轻人不要试图追求安全感,特别是年轻的时候,周遭环境从来都不会有绝对的安全感,如果你觉得安全了,很有可能开始暗藏危机。真正的安全感, 来自你对自己的信心,是你每个阶段性目标的实现,而真正的归属感,在于你的内心深处,对自己命运的把控,因为你最大的对手永远是自己。

2.稻盛要辞职离开快倒闭的公司,遭兄长棒喝:“在这样没人干活的公司你都做 不出点成绩来,你还能干什么?2.洛克菲勒感觉再也无法忍受日复一日枯燥的工 作,提出换岗,遭主管冷言“要么好好干、要么另谋出路”。同样的道理,不同 的说法,却像雷一样击中并成就了两个商业巨匠。还是那句,不抱怨、想方法。

3.关于消费:买书、健身、学习都属于资金成本边际成本很低,对于很多人,只要你能真正完成这些消费,资金都不是主要成本而值得大力投入的消费。综上,我非常建议大家买书、买电子书、ipad、智能手机、买健身卡、游泳卡。。。还 有类似的消费吗?

4.听说有人每天能看一本书,问题还不在看书速度,而是在知易行难,实践的速度赶不上所知的要求,欠账很多

5.研究聪明人如何犯错误,回报率很高。聪明人易犯错误包括:1 嫉妒他人成功;2 自命不凡:3 过于相信自己判断;4 停止学习;5 认为世界是静止的,生活在过去荣耀中;6 任何事情都有自己一套言之有据、且深信不疑的说法和理论:忘记了没有调查研究,就没有发言权。你符合吗?

6.坚持原则很多时候是经济的,可以看做是一种短期浮亏的长期受益的投资。

7.加强专注力训练,它是优先级管理的保证,同时持续专注力的一个基础是体力和精力,锻炼修炼。

8.昨天和朋友聊天,总结到:在这个信息流动越来越快越来越透明的社会,从经济的角度来看,做一个表里不一的人成本越来越高,龌龊的人会越来越倒霉,不装不但是一个道德品性优选,而且也是更经济的。很多人还未意识到这点。

9.上午北京大学周其仁教授发言非常精彩。他认为一个持久得到别人信任的人,收入就越高。有比知识、技能更加重要的东西,那就是信任。他们的团队在研究了农民工的收入以后发现,收入最高的人,往往并不是体力最好、技能最好的时候,而是最受信任的人。所以,成为一个受人信任的人,非常重要。

10.最近感想:口碑很重要,人品很重要,信用很重要,越老越重要,原则要坚定。

11.你们读了哪些传记?想起 2 年前朋友说:如果不知道让小孩阅读什么,最适合的就是传记。最近在思考与回忆:关于品格、理想、动机的形成,觉得确实如此。

12.今天手机报上有一段话:“独处是一次心灵按摩” 静坐在斗室里,漫步在小道上, 平躺在沙滩上 … 有意识的面对自己,和内心对话。喜欢独处的人,和别人在一 起时,往往也会处理的更好。交流和独处相辅相成,才能让内心成熟和强大。

13.凡事就怕不认真,不思考。好多问题我应该能知道的,只是之前没有认真看,认真想,想当然(不是没时间)。延迟满足感是一项长期修炼。

14.今天晚上的时候,每个周五晚上下班的时候,我常会和同事说:我明天假期我们再把 xxx 做好。每次突然想这句话矛盾啊,不能这样要求。嗯,生活工作要平衡。不过,别人腐败的时候我们在努力,别人消磨时光的时候我们在学习,那么延迟的满足一定会厚积薄发来到。

15.人不逼一下自己,永远不知道自己潜力有多大。很多事情非不能也,是不为也。

16.《如何阅读一本书》一书在谈在技能之外,更多的是讲学习的态度和沟通的方法。比如赞同和反对作者一章,其实标题亦可写为,关于沟通的赞同和反对

17.好的问题就是一半的的答案

18.乔布斯说 stayhungry,我以为饥渴有三个层次:贪婪、成就动机、好奇心 。三者分别关注:瞬间的结果,持续的过程,和远大的未知。三者也恰好对应了三种人:卑劣的投机者,艰辛的攀登者,与幸福的探索者

19.有不少留言说不理解这段话。研究快乐的专家告诉我们:快乐有三种:pleasure(欢乐),passion(热情),higherpurpose(理想、有意义).其中欢乐是最短暂的,热情其次,而最长久的是理想

20.想学的东西很多,吾生有涯知无涯,以有涯追无涯,怠也。有两种理解,积极的理解是应该有优先级的规划学习。

21.现在年轻人部分流行把三四十岁退休作为理想,我不认同,我觉得理想是一直有机会创造、实现想法,有机会学习,修炼,创造到老。为什么会想退休?想退休 说明你认为现在是在“忍”。我还有很多很多想法想做,希望三四十岁更多条件去实现想法。

22.保证足够睡眠是积极高效的第一步

23.生活中不是缺少美,而是缺少分享

24.很多很好的想法自己都非常认真,现在都被人实现或者通往实现的路上了。真希望自己能分身体几个同时努力,这样人生多精确。但是分身是不可能的,所以只能 1、根据情况排优先级 2、找到志同道合的人

25.应届生应该推崇自信,诚实,努力,相信成功可通过学习和努力获得。别太讨巧,走捷径。事实上面试大多不是因为技能不行,而是人品和性格不行

26.通货膨胀正在洗劫你的钱包,同学问怎么办?三个办法,一是尽可能地提高家庭负债率,当今之世能借到钱的就是英雄;二是配置资源性财产,能够抵抗通涨的 只有三个东西,黄金房产和农产品;三是像傻瓜一样的长期持有,眼前的涨跌都是对耐心的考验。除非天下大乱,否则以上三条应是规律。

27.系统地运动锻炼需要抗身体的惰性,锻炼久了之后不但身体好而且锻炼的积极性 也好容易启动养成习惯,最近觉得读书学习也很类似

28.执行力到底是什么?我认为的执行力是:说到做到,不找借口,完成别人都能完成的事。而更强的人可以做到:完成别人完不成的事。同样的一件事,交给不同的员工,会有不同的结果,完不成的人都会有各种理由来说服自己说服领导,将 一个小困难由点到面扩大化看待。做一个 NB 的人,从此刻开始,不再找借口

29.平庸有重力,需要逃逸速度

30.不怕犯错误,不怕坏方法,甚至不怕坏习惯。只要你会会自我改正。你习惯改正吗?

31.快到 30 岁了,感觉这几年又再重新学习/补习本应在青少年时间学习的东西:如何阅读、如何了解自己、如何与人沟通沟通、如何安排时间、如何正确的看待别人意见、如何激励自己、如何写作、如何坚持锻炼身体、如何耐心

32.互联网让会学习爱学习的人和相反的差距拉的更大,这并不仅限于互联网行业。只不过互联网行业这种趋势先开始而已。现在好多初中生、高中生比大学生、博士生还博学。我见过 2 个中学生,自己用 wiki 整理所学过的,自学的各种知识。我怎么生的这么早

33.延迟满足感 和 坚决告别惰性 是“优秀”的最重要两块基石。

34.当某人开始深入认识自己、研究自己的时候,说明此人开始有了哲学的思考,预示着此人开始迈入一个新的人生阶段。

35.非常同意自控力(也就是反惰性)是优秀的标准。确实马拉松不是高标准,思维意识情绪的自控更难。

36.牛逼的人找方法,傻逼的人找借口

37.看年轻人的潜力,看他周末几点起,周末在干嘛,下班在干嘛。甚至不一定要干嘛,只要看想些什么

38.今天早点睡,明天早上起来看书。本周的学习计划快完不成了。创业过程中不断的学习又能尝试是感觉很好的体验

39.人欲望太强的时候就容易短视,太自我中心的时候就容易盲目

40.以前一直都没觉得找人自信很重要,现在发现越来越重要。惰性、依赖、拖拉、 保守很多也都是是不自信导致的。自信的人自然会和自我高要求联系起来

41.关于勤奋,就我所知,罕有成功者不是工作时间极长的:通用电气的 CEO 每周 工作一百小时,坚持了至少十年。巴菲特为了最早看到次日的华尔街日报,经常在凌晨四点去取报纸。勤奋不是一种形式,而是一种心理状态:享受挑战极限的过程,保持热情和好奇心,坚持不懈

42.哈佛有一个著名的理论:人的差别在于业余时间,而一个人的命运决定于晚上 8 点到 10 点之间。每晚抽出 2 个小时的时间用来阅读、进修、思考或参加有意的 演讲、讨论,你会发现,你的人生正在发生改变,坚持数年之后,成功会向你招手

43.人生的本质是追寻自我的提升。包括思想、能力、意志等等。这些发展好了,一切随之而来。偏偏 大多数人追求的是短期的公司、职位、薪水,运气好的能有所发展,运气差的会迷失方向流于平庸

44.聪明还耐心是有一些矛盾的优点,同时具备两点的人却非常优秀

45.习惯:把要做的事情迅速分配在 calendar 上,会变化没关系,多调整

46.别装,做个坦诚真实的人。团队中都是坦诚真实的人,沟通成本将小很多

47.据多家公司统计,团队淘汰个人的顺序往往如下:第一批,明显缺陷者、众人厌恶的说谎者;第二批,不愿交流者、不合群者;第三批,有能力但慵懒者、妄图 坐享其成者;第四批,居功自傲者,蔑视同僚者。

48.经常看到职位蛮漂亮的人,但细看发现他每次升职都是换工作的时候发生的。这会让我警惕,因为好的人,老板会加薪升职来挽留。如果一个人在同一公司多次 升职,让我会放心很多,因为比我了解他多得多的人看好他给他更多的责任,而 且他一次次胜任。换工作才升职,有可能是外强中干,忽悠了新老板

49.一个身价两百多亿的老板不作秀、不爬山、不吹牛、不打口水仗、不接 受采访、不上电视杂志,以身作则像一个基层员工一样每天脚踏实地测试产品, 无止境地改进产品的体验。这才是腾讯成功的最大原因。而被腾讯打败的 Loser 们始终没有认识到这一点,要么骂它靠抄袭,要么说它靠 QQ 才能成功

50.其实我挺想知道团队成员周末都在干嘛。。。总希望大家把时间充分有效利用了。。当然我说的不是只工作,是指优先利用学习、休息、娱乐、锻炼、交流,思考上。并且可以一起活动。 51.一个公司最强的敌人是什么?韦尔奇说,是“坦率”。深表认同。幸好,坦率是可以培育的

52.【职场】昨晚请多玩优秀员工吃饭,聊了几点职场体会。(1) 把自己当老板看, 象老板一样拼命干活,能力自然就提高了。有了能力,假如多玩不能给你好的回 报,其他公司一定会给。(2) 不是每次付出就一定有回报,但是不断付出就一定 会有回报。@李学凌 补充了一点:象你的老板一样思考,能力会提高得更快

53.有人问我如何突破自己的职业瓶颈,我说:你的瓶颈就在于你的心。你的心更宽, 心态更好,遇到问题将自己拨高一层去看问题,把你心里的那些小纠结小疑惑小算盘小私心,统统打破,你就没有瓶颈 54.最近大家 review 了九九房半年的进展并讨论每个人的总结和设想,对我们的信 心更加增强。和同事曾讨论:创业要经常自省,避免自我强化和催眠。所以要区别信心和“YY”,真正信心源于看到自己的进步和潜力,可以分成两个方面:1、 对事情本身判断的信心 2、对自己和团队的信心

55.这周面了十几个人终于确定一个实习生。最近一个多月可能面试了 50 多人,总共只有 2 个非常有意向的人选,其中失败一个,一个还在谈。每当想放低要求的 时候,我就提醒自己一定不能往低走而要往高走,我们要做的出彩,而不是完成 的事情。而尤其在早期,核心几个人的能力素质态度是最关键的

56.创业就要像生小孩一样:准备好体力,用长劲,快速换气;喊疼和抱怨没用,专注在努力,关键时候坚持再坚持一把!

57.如果你很有才华,在某些方面又有一技之长,请先不要急于露出锋芒,如果你只是以普通身份而不是以领导身份到新单位去的,那就更不能锋芒太露。一个人新到一个单位,就像一粒石子投入一潭平静的池水,往往会引入注目,一举一动, 一言一行,都在别人的视野之中

芝麻开门,显示全文!