苹果安卓和input框软键盘的那些事 置顶

H5软键盘的不同表现

苹果

表现

开发H5应用时,测试人员发现在IOS低版本中input,textArea等输入框聚焦时,软键盘弹起会将整个页面向上顶,收回键盘后,页面不会回弹恢复页面,导致页面下方产生大量空白。

解决方案

JQuery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 解决 H5 IOS input 聚焦时,页面整个被推上去了,键盘收起页面未下移 BUG
$(function () {
(/iphone|ipod|ipad/i.test(navigator.appVersion)) && document.addEventListener('blur', event => {
// 当页面没出现滚动条时才执行,因为有滚动条时,不会出现这问题
// input textarea 标签才执行,因为 a 等标签也会触发 blur 事件
if (
document.documentElement.offsetHeight <=
document.documentElement.clientHeight &&
['input', 'textarea'].includes(event.target.localName)
) {
document.body.scrollIntoView() // 回顶部
}
}, true);
});

VUE

1
2
3
4
5
6
7
8
9
10
<input
ref="textarea"
v-model="text"
placeholder="请输入消息..."
enterkeyhint="send"
maxlength="100"
@keyup.enter="sendMessage"
@blur="blurClick"
@focus="focusClick"
/>
1
2
3
4
5
6
7
// isIOS 自行实现,实际实现一个识别ios环境函数
const blurClick = () => {
// 兼容IOS输入框顶上去的问题
if (isiOS()) {
window.scroll(0, 0) // 页面滚动到顶部
}
}

安卓

表现

在部分android机型上测试点击靠下的输入框时遇到弹出的软键盘挡住输入框问题,ios可自身弹起(ios自身的调整偶尔也会出问题,例如第三方键盘会遮挡,原因是第三方输入法的tool bar或者键盘也被当做可视区域,这里不做讨论)

解决方案

Vue

1
2
3
4
5
6
7
8
9
10
<input
ref="textarea"
v-model="text"
placeholder="请输入消息..."
enterkeyhint="send"
maxlength="100"
@keyup.enter="sendMessage"
@blur="blurClick"
@focus="focusClick"
/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// isIOS 自行实现,实际实现一个识别ios环境函数
const focusClick = () => {
// 兼容安卓输入框顶不上去的问题
if (isAndroid()) {
const el = document.documentElement || document.body
const originHeight = el.clientHeight
window.addEventListener('resize', () => {
const resizeHeight = el.clientHeight
if (resizeHeight < originHeight) {
// 对应的某个顶上去即可
const documentObj: any = document.querySelector('.im-content')
documentObj.scrollTop = 999999999
} else {
// '键盘收起'
textarea.value.blur() // 触发blur事件
}
})
}
}

如果是中间部分的,可以考虑使用scrollIntoViewIfNeeded() 方法实现业务逻辑

ssr-nuxtjs 置顶

nuxtjs

nuxtjs是干什么的

利用Nuxt.js的服务端渲染能力来解决Vue项目的SEO问题

Nuxt.js和纯Vue项目对比

  1. build后目标产物不同
    vue: dist
    nuxt: .nuxt

  2. 网页渲染流程
    vue: 客户端渲染,先下载js后,通过ajax来渲染页面;
    nuxt: 服务端渲染,可以做到服务端拼接好html后直接返回,首屏可以做到无需发起ajax请求;

  3. 部署流程
    vue: 只需部署dist目录到服务器,没有服务端,需要用nginx等做Web服务器;
    nuxt: 需要部署几乎所有文件到服务器(除node_modules,.git),自带服务端,需要pm2管理(部署时需要reload pm2),若要求用域名,则需要nginx做代理。

  4. 项目入口
    vue: /src/main.js,在main.js可以做一些全局注册的初始化工作;
    nuxt: 没有main.js入口文件,项目初始化的操作需要通过nuxt.config.js进行配置指定。

从0开始搭建一个SSR项目

1
npx create-nuxt-app <项目名>

如图

具体关于目录结构和相关配置解析可以看这里https://www.nuxtjs.cn/guide/directory-structure

集成axios

为了方便,我们直接使用‘@nuxtjs/axios’。详情可以参考https://nuxtjs.org/examples/modules/axios#axios-usage

ssr和spa 置顶

ssr和spa

都是什么

SSR

SSR(Server-Side Rendering)服务端渲染
简单理解是将组件或页面通过服务器生成html字符串,再发送到浏览器,最后将静态标记”混合”为客户端上完全交互的应用程序

SPA

SPA,即Single Page Application,按照字面意思就是单页面应用,通俗点就是整个网站由一个html页面构成。
ssr

双方的优缺点

对比 SPA SSR
优势 1.页面内容在客户端渲染
2. 只关注View层,与后台耦合度低,前后端分离
3.减轻后台渲染画面的压力
1.更好的SEO,搜索引擎工具可以直接查看完全渲染的画面
2.更快的内容到达时间 (time-to-content),用户能更快的看到完整渲染的画面
劣势 1.首屏加载缓慢
2.SEO(搜索引擎优化)不友好
1.更多的服务器端负载
2.涉及构建设置和部署的更多要求,需要用Node.js渲染
3.开发条件有限制,一些生命周期将失效
4.一些常用的浏览器API无法使用

渲染对比

SPA 渲染

SSR 渲染

CSR和SSR

图片来自网络素材

为什么说SSR比SPA快

主要体现在http请求数量上,ssr只需要请求一个html文件就能展现出页面,虽然在服务器上会调取接口,但服务器之间的通信要远比客户端快
spa慢主要慢在需要请求大量的js资源,一般的首页,请求10几个js都算少到,要知道js的加载是同步的,页面逻辑可能藏在最后几个js里面,请求-等待-下载-然后是解析js-再调接口-展现页面。
项目足够小,那么谁快谁慢页不一定,越大的项目效果越明显。

为什么我们有时候觉得SSR比SPA还慢

SSR要在 node 层调接口组装数据生成模板,如果数据接口多,数据量大就有可能导致渲染出html慢,导致返回更慢,当然也有可能是访问人数多,因为SSR要用服务器渲染,所以当并发很多的时候,服务器压力就有瓶颈,导致出html也慢,这就需要根据业务情况考虑哪些内容适合 SSR, 哪些内容适合 CSR,尽可能减少 node 层渲染逻辑。

k8s前端集群 置顶

k8s前端集群

K8S前端集群概念图

前置需要掌握的基础概念

1.docker

  • 知道docker是啥
  • 基础的命令,login build push pull run kill ps
  • 了解dockerfile和创建基础nginx/node项目的思路

教程:http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html
命令:https://www.runoob.com/docker/docker-command-manual.html

2.nginx

  • 知道nginx是啥
  • 能够了解基础配置和简单自我配置

教程:http://www.ruanyifeng.com/blog/2018/02/nginx-docker.html
配置文件详细说明:https://www.runoob.com/w3cnote/nginx-setup-intro.html

3.K8S

教程:https://www.orchome.com/1334

4.Jenkins

  • 了解基础的jenkinsfile文件
  • 能编辑基础配置

教程:https://www.jenkins.io/zh/doc/book/pipeline/jenkinsfile

怎么集成发布

大致流程

git 代码提交->git hook 触发Jenkins -> 跑Jenkins pipe脚本 -> docker构建环境进行打包生成前端静态资源包 -> docker打包nginx + 静态资源包 -> 推私库hub -> deploy 替换镜像 -> 发布成功

相关的yaml文件

deploy.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
apiVersion: apps/v1 # 是当前配置格式的版本。
kind: Deployment # 是要创建的资源类型,这里是 Deployment
metadata: # 资源的元数据/属性
name: frontend-k8s-demo # 资源的名字,在同一个namespace中必须唯一
namespace: platform # 部署在哪个namespace中,例如,平台 platform , 社区 shequ
spec: # 资源规范字段,本文指 Deployment 的规格说明。
selector:
matchLabels:
app: frontend-k8s-demo
replicas: 1 # 容器数量,可能理解为几个副本
template:
metadata:
labels:
app: frontend-k8s-demo # 如果被指定, .spec.selector 必须匹配 .spec.template.metadata.labels,否则它将被API拒绝。如果 .spec.selector 没有被指定, .spec.selector.matchLabels 默认是 .spec.template.metadata.labels。
spec:
imagePullSecrets: # 镜像拉取Secrets
- name: registry-harbor # 默认命名,私库地址 https://hub.snoob.com/harbor ,账号密码找运维或者各种后端
containers:
- name: frontend-k8s-demo # 镜像名字
image: hub.snoob.com/platform/frontend-k8s-demo:1.0.0_202108161433_beta # 镜像地址
imagePullPolicy: Always # 拉去策略 Always = 总是拉取 pull, IfNotPresent = 默认值,本地有则使用本地镜像,不拉取 , Never = 只使用本地镜像,从不拉取
env: #
- name: TZ
value: Asia/Shanghai
- name: MALLOC_PER_THREAD # 防止linux内核出现大量内存碎片
value: "1"
ports:
- containerPort: 8080 # 容器端口
livenessProbe: # 容器存活探测,每30秒做一次探测
httpGet:
path: /index.html
port: 8080
initialDelaySeconds: 120 # 容器启动后第一次执行探测是需要等待多少秒。
periodSeconds: 20 # 执行探测的频率。默认是10秒,最小1秒。
timeoutSeconds: 5 # 探测超时时间。默认1秒,最小1秒。
failureThreshold: 3 # 探测成功后,最少连续探测失败多少次才被认定为失败。默认是3。最小值是1。
readinessProbe: # 由于集群1.15版本不支持startUp探针,只能暂时使用readiness探针并人工评估启动时间,同时建议运维升级到1.19版本
httpGet:
path: /index.html
port: 8080
initialDelaySeconds: 30 # 容器启动后第一次执行探测是需要等待多少秒。
periodSeconds: 10 # 执行探测的频率。默认是10秒,最小1秒。
timeoutSeconds: 5 # 探测超时时间。默认1秒,最小1秒。
failureThreshold: 10 #探测成功后,最少连续探测失败多少次才被认定为失败。默认是3。最小值是1。
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 30"]
resources: # 容器资源分配和资源限制,为了防止容器无节制的使用 CPU、内存 等资源,可以为容器指定资源最大允许使用量。
requests:
memory: "64Mi"
cpu: "10m"
limits:
memory: "256Mi"
cpu: "200m"

service.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kind: Service
apiVersion: v1
metadata:
name: frontend-k8s-demo
namespace: platform
labels:
app: frontend-k8s-demo
spec:
ports:
- name: frontend-k8s-demo
protocol: TCP
port: 8080
targetPort: 8080
selector:
app: frontend-k8s-demo

ingress.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
name: frontend-k8s-demo
namespace: platform
spec:
rules:
- host: vue-demo.test.p.snoob.com
http:
paths:
- path: /
backend:
serviceName: frontend-k8s-demo
servicePort: 8080

jenkinsfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// 以下为标准变量,日常用到基本不用改
def label = 'pipeline'
def docker_hub = 'hub.snoob.com' // 镜像仓库地址

// 以下是常规的项目文件配置
def docker_file = 'configfiles/Dockerfile' // 镜像文件地址
def yaml_file = 'configfiles/deploy.yaml' // 部署文件名
def service_file = 'configfiles/Service.yaml' // 部署文件名
def ingress_file = 'configfiles/Ingress.yaml' // 部署文件名
def nginx_file = 'configfiles/nginx/default.conf' // nginx默认文件
def replicas = 1 // 实例个数
def live_path = '/index.html' // 心跳页面
def memory_work = '64Mi' //工作内存
def cpu_work = '10m' // 工作cpu
def memory_max = '256Mi' // 最大内存 不能高过工作内存4倍,否则不工作
def cpu_max = '200m' // 最大cpu

// 以下是项目基础配置
def service_name = 'frontend-k8s-demo222' // 服务名,应用名称
def team_ns = 'platform' // 命名空间,通常是业务线名称


podTemplate(label: label) {
node(label) {
stage('Scm') {
cleanWs()
checkout scm
}

stage('Build') {
container('nodejs-v10') { // 声明nodejs容器环境 目前有 v8 = 8.14.0 v10 = 10.24.0 v14 = 14.16.0
sh '''
node -v
npm install -dd
npm run build
'''
}
}

stage('Docker') {
withCredentials([usernamePassword(
credentialsId: 'dockerhub-platform',
usernameVariable: 'user',
passwordVariable: 'pass')]) {
sh """
sed -i "s#%{service_name}#${service_name}#g" ${nginx_file}
cat ${nginx_file}
docker login -u ${user} -p ${pass} ${docker_hub}
docker build -f ${docker_file} -t ${docker_hub}/${team_ns}/${service_name}:${tagName} .
docker push ${docker_hub}/${team_ns}/${service_name}:${tagName}
"""
}
}

stage('Deploy') {
withKubeConfig([credentialsId: 'pipeline']) {
sh """
sed -i "s#%{service_name}#${service_name}#g" ${yaml_file}
sed -i "s#%{tagName}#${tagName}#g" ${yaml_file}
sed -i "s#%{docker_hub}#${docker_hub}#g" ${yaml_file}
sed -i "s#%{team_ns}#${team_ns}#g" ${yaml_file}
sed -i "s#%{replicas}#${replicas}#g" ${yaml_file}
sed -i "s#%{live_path}#${live_path}#g" ${yaml_file}
sed -i "s#%{memory_work}#${memory_work}#g" ${yaml_file}
sed -i "s#%{cpu_work}#${cpu_work}#g" ${yaml_file}
sed -i "s#%{memory_max}#${memory_max}#g" ${yaml_file}
sed -i "s#%{cpu_max}#${cpu_max}#g" ${yaml_file}
cat ${yaml_file}
kubectl apply -f ${yaml_file}
"""
}
}

stage('Service') {
withKubeConfig([credentialsId: 'pipeline']) {
sh """
sed -i "s#%{service_name}#${service_name}#g" ${service_file}
sed -i "s#%{team_ns}#${team_ns}#g" ${service_file}
cat ${service_file}
kubectl apply -f ${service_file}
"""
}
}
}
}

docker内nginx配置和dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
listen 80;
server_name localhost;

#charset koi8-r;
access_log /var/log/nginx/%{service_name}.access.log main;
error_log /var/log/nginx/%{service_name}.error.log error;

location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
root /usr/share/nginx/html;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
1
2
3
4
5
6

FROM hub.snoob.com/base/nginx
MAINTAINER snoob "snoob@91snoob.com"
COPY test/ /usr/share/nginx/html/
COPY /configfiles/nginx/default.conf /etc/nginx/conf.d/default.conf

制作简易小巧的前端构建环境

利用alpine,这个可以降低包大小,减轻镜像大小

1
2
3
4
5
6
7
8
FROM alpine:3.8.5
MAINTAINER snoob<snoob@91snoob.com>
RUN apk add --no-cache npm \
&& apk add --no-cache nodejs
COPY ./.npmrc /root/.npmrc
RUN npm config ls -l
RUN npm config get cache
RUN npm config get prefix
1
2
3
4
registry=http://npm.91snoob.com/repository/npm-group
always-auth=true
_auth="bnBtX2NvbW1pdxxxxxxcUlNeW5Rcg=="
puppeteer_download_host=https://npm.taobao.org/mirrors

常规和K8S集群对外访问方式差异

前端上了K8S集群的好处

  • 1、故障迁移:当某一个node节点关机或挂掉后,node节点上的服务会自动转移到另一个node节点上,这个过程所有服务不中断,内部有保活探针,一旦保活失效,马上会再起。
  • 2、资源调度:当搞活动或者其他事件导致临时流量爆增,影响到I/O,cpu,内存,不够用的时候,可以扩充node节点,新建的pod就会被kube-schedule调度到新扩充的node节点上,同时也更好的负载均衡。
  • 3、资源隔离:可创建开发、运维、测试三个命名空间,切换上下文后,开发人员就只能看到开发命名空间的所有pod,看不到运维命名空间的pod,这样就不会造成影响,互不干扰,传统的主机或只有docker环境中,登录进去就会看到所有的服务或者容器
  • 4、因为采用docker容器,进程之间互不影响,
  • 5、安全:不同角色有不同的权限,查看pod、删除pod等操作;RBAC认证增加了k8s的安全
  • 6、审计:当要回测某个版本时候,可以直接提取指定hub进行部署审查

cdn部署 置顶

cdn部署

什么是CDN,CDN为什么能够优化前端的性能

内容分发网络(Content Delivery Network,简称CDN)是建立并覆盖在承载网之上,由分布在不同区域的边缘节点服务器群组成的分布式网络。通俗的来讲就是:把需要访问到的静态资源分享出去到其他服务资源去,减少对源的直接依赖。

简单以下面一个图说明cdn的原理

部署到cdn方式

  1. 运维配置对应的cdn地址和做好溯源配置相关,并提供对应的域名地址。
  2. 项目构建的时候,把对应的静态文件地址替换成CDN地址,常规的方法是利用webpack打包的时候配置/替换publicPath属性,当然具体项目具体分析,方法不唯一,但是思路是把静态文件改成从cdn加速域名访问即可。
  3. 开启cdn的gzip,这个运维需要开启服务商的这个配置项,且对请求头和源响应头有一定条件,具体参考链接压缩开启不生效

优缺点

cdn

  1. 静态资源基本不会占用源服务器带宽,很好的降低对源服务器带宽依赖,可以给源服务器释放带宽压力,更好的给其他服务让出资源。
  2. 多人访问的时候不会有并发压力,不会导致页面404,长时间加载,甚至资源竞争,减少白屏时间。
  3. 边缘地带的用户也有良好的使用体验。
  4. 针对大图,大文件下载体验提升效果相当明显。
  5. 阿里云cdn默认是http/2,相比现在的http/1.1,文件并行加载效率有质变提高。
  6. 防止DDOS攻击

缺点

  1. 发版/更新资源第一次访问,需要重新溯源,第一次的访问会比较慢。

前端性能优化分析 置顶

前端性能优化分析

前端性能分析

加载部分

我们以某个网站为例,按下F12打开浏览器的调试模式。

上图中,打开 chrome 调试工具,点开「设置」icon,下面的四个选项中,除了“Group by frame”之外,其他的三个选项都可以勾选上。

我们可以看到我们官网的一些指标:

  • 总资源量是 36.5M。
  • DOM加载完成时间(DOMContentLoaded):589ms。这是一个很关键的指标。
  • 其他资源的总加载时间是 3.15秒。

我们再来对比一下另外一个的:

  • 总资源量是 6M。
  • DOM加载完成时间(DOMContentLoaded):2.08s(2080ms)。这是一个很关键的指标。
  • 其他资源的总加载时间是 2.27秒。

小技巧,存快照,方便之后分析

network里的信息挺多,为方便之后分析可以存快照

在 network 的空白处右键,选择“Save all as HAR with content”,将 network 信息保存为 HAR文件格式。

HAR是一种标准的Web格式,用户保存性能测试的结果。里面的数据是json格式。

我们可以使用第三方的 HAR 分析软件来打开 HAR 文件,比如:

chrome dev瀑布图 Waterfall

瀑布图可以非常直观地把网站的加载过程,用自上而下的方式表达出来,就像瀑布一样。

瀑布图有两种分析方式:横向,纵向。

横向:单条资源请求

横向是指每个资源的加载信息。里面有一些色块来表达加载的过程,颜色不同就是资源的下载不是单一的过程,而是经历了很多环节。

为了了解资源的具体加载过程,我们单条分析

纵向:多条资源请求

(1)看资源与资源之间的联系:如果发生阻塞,说明资源可能是串行地按顺序加载。可以按需要适当调整为并行

(2)看关键的时间节点。Waterfall 中有两根时间线:蓝色的线是 DOM 加载完成的时间,红色的线是所有资源加载完成的时间。

交互部分,诸如操作,动画,特效部分

除了加载部分的性能,还有一个可优化的地方就是交互部分。网站加载完成后,用户开始使用这个网站的整体交互体验。

交互的性能,我们需要关注的点主要是:

  • 交互操作的响应/反馈时间要短:比如点击按钮后的弹窗、在搜索框里输入关键字后的搜索结果。

  • 页面滚动,切换要流畅:如超长表格,懒加载,长图,多屏特效等。

  • 接口请求要合理:减少接口请求数量,尽可能复用已有数据,如非必要减少频繁请求用户信息等。

FPS帧率、FRS

这里首先科普两个概念:

  • 刷新率:显示器每秒有多少帧画面。大多数显示器的刷新率是60帧/秒(即60hz)。
  • 帧率(FPS:frames per second):视频或者动画的内容本身,每秒有多少帧。由显卡输出帧率。

上面的两个参数中,不要把「刷新率」和「帧率」弄混了。「刷新率」是屏幕的参数,「帧率」是图像、视频等内容的参数。人眼最终看到的效果,是以最低的参数为准的。

目前,市场主流手机和电脑屏幕的刷新率基本都是60Hz,即每秒显示60帧画面。也就是说,当我们在使用手机的时候,本质上是手机在连续播放一张张静态图片,每秒播放60张,让肉眼误认为眼前的画面在动。

那么,在浏览器中如何实时显示内容的 FPS 参数呢?打开浏览器的控制台后,按住快捷键「Cmd + Shift + P」,然后输入 frame,选择Show frames per second(FPS) meter。如下:

如图所示工具图

用 RAIL 模型测量性能

参考这篇文章

使用Chrome DevTools 分析性能

现在主流的性能测量工具:

  • Chrome DevTools:开发调试、分析性能。

  • Lighthouse 网站整体质量评估。

  • WebPageTest:给网站提供多个地点的测试,以及全面的性能报告。

这一段,我们先来讲一讲 Chrome DevTools 。

大家平时在用 Chrome DevTools 的时候,一般使用来开发调试、查看 DOM、css、接口请求等,但其实,这个工具非常强大。

size:文件大小分析

上图中的size分上下2个:该文件在网络传输的时候会做压缩(2.3kb),拿到资源之后再解压还原(7.7kb),其实就是我们所谓的开启压缩模式。

performance:性能加载视图工具

Lighthouse 介绍

lighthouse 是 chrome 浏览器的一个性能测量工具。我们先来看看它的性能指标。

Lighthouse 跑分里,最重要的两个指标如下:

  • First Contentful Paint(白屏时间)从白屏到第一次出现内容的时间。我们可以看到,上面提供了一些加载过程的截图,10屏里如果只有1到2屏是白屏,说明体验还是可以的。

  • Speed Index:速度指数。

我们不需要关心这个指数是怎么来的,因为背后涉及一套很复杂的公式,我们暂时只需关注这个数值。

Speed Index 标准为4秒(超过4秒算比较慢的),我们测的淘宝的 speed index 是0.5s,很快了。但我们要结合网站本身的业务来权衡。并不是分数越高性能越高,比如百度这样的网站,页面上的内容很少,测出来的分数肯定很完美。而淘宝需要展示很多内容给用户看。所以,这个指标只是一个指导作用,并不一定能够达到最优的数值。

Lighthouse 的分析结果里,也给出了颜色标注:

  • 红色:比较严重的性能问题
  • 黄色:需要做适当优化
  • 绿色:说明性能表现很好。

另外,Lighthouse 还会给出一些优化建议:

  • Opportunities:优化建议。

  • Diagnostics:问题诊断。

  • Passed audits:表示这部分没有问题。

使用 WebPageTest 评估网站性能

我们经常有业务反馈打不开某个页面,这个时候我们要借助第三方的测试工具,而不仅仅只是自己电脑上访问正常就ok了。可以借助 WebPageTest 这样的第三方测试工具,去模拟各种用户的真实场景。

WebPageTest 使用

网址:https://www.webpagetest.org

WebPageTest 在世界各地提供了非常多的服务器,在每个服务器上部署了不同的浏览器,可以让我们有针对性的做测试。如果你做的是一款国际化网站,那更有必要使用一下了。

我们以官网网站举例:

按照上面的选项配置完成后,点击右侧的「Start Test」即可开始测试。然后等待:

前端性能优化点

  • 资源文件的优化:比如文件压缩合并策略、打包方案
    1、通过webpack资源合并压缩。
    2、使用雪碧图和svg
    3、小图片使用base64,利用webpack对 url-loader 的 limit 配置改改就能打包

  • 大图,多图优化
    1、尝试先上传到cdn,业务代码中直接以cdn地址引用
    2、考虑图片转webp的格式,使用有损/无损方式
    3、lqip方案,低质量图像占位符
    4、利用cdn/服务器做图片处理

  • 页面加载策略
    1、懒加载,减少当前页暂时用不到的资源加载
    2、预加载,减少用户后续等待资源加载的时间
    3、首屏尽快打开,剩下的内容延迟加载,减少初次加载的资源量。首屏的内容是可以确定的。

  • 前端缓存策略和优化 nginx缓存配置和优化
    1、首页不缓存,js/css等资源开启缓存策略

  • 如何减少 http 请求数
    1、代码,逻辑层面减少对接口多层依赖,如,a接口-》b接口-》c接口,尽量跟后端商量合并接口。
    2、减少小图的请求,合并雪碧图或转base64最优解。
    3、合理复用缓存,减少接口请求量

  • 开启和配置 gzip 压缩
    1、webpack打包配置的时候就要进行产品gzip文件。对应的说明CompressionWebpackPlugin
    2、nginx需要配合,打开gzip
    3、nginx开启的gzip配置和webpack打包gzip对比参考文章

  • 升级 Http2.0
    暂未计划上http2,不考虑。参考文章

数组方法 置顶

数组方法

数组的创建

数组的创建有三种方式:构造函数方式、字面量方式、ES6新增的Array.of()方法创建。

构造函数方式

1
2
3
4
let arr = new Array(); // 创建一个空数组
let arr = new Array(10); // 创建长度为10的数组
let arr = new Array('a'); // 创建包含一个字符串元素a的数组
let arr = new Array(10, 'a'); // 创建包含10和字符串a两个元素的数组

字面量方式

1
let arr = [10, 'a'];  // 字面量方式,即直接给数组赋值

Array.of()方法(ES6新增)

1
2
3
4
5
let arr = Array.of(3); 
console.log(arr); // [3]
// 参数可以是各种类型
let arr1 = Array.of(1, 'a', true, null, undefined, {name: "zhangsan"}, [45]);
console.log(arr1); // [ 1, 'a', true, null, undefined, { name: 'zhangsan' }, [ 45 ] ]

数组的检测

两种较为准确的检测方法:

  • 利用对象的toString方法:
    Object.prototype.toString.call([]) === “[object Array]”; // true
  • Array.isArray():
    Array.isArray([1, 2, 3]); // true

数组的方法

1、改变原数组
push() 尾增1..n项
unshift() 头增1..n项
pop() 删尾最后一个
shift() 删头一个
splice() 删除、插入、替换数组项
reverse() 翻转
sort() 排序,可正序可逆序
copyWithin() 将数组指定位置(start到end)的元素复制到当前数组的其他位置(target开始)ES6
fill() 用指定元素,填充数组从start(包括)到end(不包括)之间的元素,如果该区间内已经有元素,直接替换掉

2、不改变原来数组
concat() 数组拼接数组
slice() 基于当前数组的一项或多项创建一个新的数组
indexOf() 从数组开头查找元素在数组中的索引位置
lastIndexOf() 从数组结尾查找元素在数组中的索引位置
includes() 判断数组中是否包含指定的元素(ES7新增)
join() 将数组元素转化为字符串(调用每个元素的toString方法)

3、带callback操作的,可能改变,也可能不改变原数组
every() 对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回true
some() 对数组中的每一项运行给定函数,如果该函数对任意一项返回true,则返回true
filter() 对数组中的每一项运行给定函数,返回该函数返回true的项组成的数组
map() 对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组
forEach() 对数组中的每一项运行给定函数。无返回值
find() 查找数组中第一个符合条件的元素,返回该元素
findIndex() 查找数组中第一个符合条件的元素所在位置的索引,并返回该索引值
reduce() 从数组的第一项开始,逐步遍历到最后,迭代数组的所有项
callback迭代函数,有四个参数(prev, cur, index, array)
prev 前一个值,(initialValue || 数组第一项 || 上一次迭代的结果)
cur 当前迭代项
index 当前迭代项索引
array 迭代的原数组

数组的构造函数的方法

Array.from() 将类数组转化为数组

数组扩展运算符(ES6新增)

[…’hello’] // [ ‘h’, ‘e’, ‘l’, ‘l’, ‘o’ ]
[…arr1, …arr2] // == arr1和arr2组成的数组

二叉树遍历 置顶

二叉树遍历

样板树

      8
    /   \
  3       10
 /  \      \
1    6      14
    / \     /
  4    7    13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/

function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}

let a4 = new TreeNode(4)
let a7 = new TreeNode(7)
let a1 = new TreeNode(1)
let a13 = new TreeNode(13)
let a6 = new TreeNode(6,a4,a7)
let a3 = new TreeNode(3,a1,a6)
let a14 = new TreeNode(14,a13)
let a10 = new TreeNode(10,undefined,a14)
let a8 = new TreeNode(8,a3,a10)

let root = a8

层序遍历[广度优先遍历BFS]

二叉树的层序遍历,即二叉树的广度遍历,先遍历根节点的相邻节点,再一次遍历相邻节点的子节点。广度遍历通常借助队列来实现。用队列来存储当前层的节点数,遍历当前层的节点,将当前层的节点依次推入数组currentResult[],再将当前节点的左右子节点推入队列中,进入下一层进行遍历,直到遍历完整棵树,即完成到二叉树的层序遍历

层序遍历:[[8],[3,10],[1,6,14],[4,7,13]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var levelOrder = function (root) {
if (!root) {
return []
}

let result = []
let queue = [root]
while (queue.length > 0) {
let len = queue.length
let currentResult = []
for (let i = 0; i < len; i++) {
let node = queue.shift()
currentResult.push(node.val)
if (node.left) {
queue.push(node.left)
}
if (node.right) {
queue.push(node.right)
}

}
result.push(currentResult)
}
return result
};

先中后序遍历[深度优先遍历DFS]

  • 先序遍历的顺序:NLR(根节点->左结点->右结点)

  • 中序遍历的顺序:LNR(左节点->根结点->右结点)

  • 后序遍历的顺序:LRN(左结点->右结点->根节点)

先序:8 3 1 6 4 7 10 14 13
中序:1 3 4 6 7 8 10 13 14
后序:1 4 7 6 3 13 14 10 8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}

let a4 = new TreeNode(4)
let a7 = new TreeNode(7)
let a1 = new TreeNode(1)
let a13 = new TreeNode(13)
let a6 = new TreeNode(6,a4,a7)
let a3 = new TreeNode(3,a1,a6)
let a14 = new TreeNode(14,a13)
let a10 = new TreeNode(10,undefined,a14)
let a8 = new TreeNode(8,a3,a10)

let root = a8


// 先序
let nlr = function(node){
if(node == null) return
console.log(node.val)
nlr(node.left)
nlr(node.right)
}
nlr(root)

// 中序
let lnr = function(node){
if(node == null) return
lnr(node.left)
console.log(node.val)
lnr(node.right)
}
lnr(root)

// 后序
let lrn = function(node){
if(node == null) return
lrn(node.left)
lrn(node.right)
console.log(node.val)
}
lrn(root)

非递归的深度遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
function stnlr (root) {
if (!root) return []

let result = []
let stack = []
let p = root

while (stack.length > 0 || p) {
// p还有
while (p) {
// 根
result.push(p.val)

stack.push(p)
p = p.left
}
if (stack.length > 0) {
p = stack.pop()
p = p.right
}

}
return result
}

function stlnr (root) {
if (!root) return []

let result = []
let stack = []
let p = root

while (stack.length > 0 || p) {
// p还有
while (p) {
// 根
// result.push(p.val)

stack.push(p)
p = p.left
}
if (stack.length > 0) {
p = stack.pop()
result.push(p.val)
p = p.right
}

}
return result
}

function stlrn (root) {
if (!root) return []

let result = []
let stack = []
let p = root

while (p) {
stack.push(p)
p = p.left
}

while (stack.length > 0) {
p = stack.pop()
if (p.right === null || p.right == lastVisit) {
result.push(p.val)
lastVisit = p
} else {
stack.push(p)
p = p.right
while (p) {
stack.push(p)
p = p.left
}
}
}

return result
}


console.log(stnlr(a8))
console.log(stlnr(a8))
console.log(stlrn(a8))

斐波那契数列 置顶

斐波那契数列

试输出斐波那契数列的前N项,即 1、1、2、3、5、8、13、21、34、55 …N

代码如下

1
2
3
4
5
6
7
let fibonacci = (n) => {
if (n === 2 || n === 1) {
return 1
}
return fibonacci(n - 1) + fibonacci(n - 2)
}
console.log(fibonacci(10))

这是简单递归,我们可以看下执行了多少次

1
2
3
4
5
6
7
8
9
10
11
let count = 0
let fibonacci = (n) => {
count ++
if (n === 2 || n === 1) {
return 1
}
return fibonacci(n - 1) + fibonacci(n - 2)
}
console.log(fibonacci(10))
console.log(count) // 109

从上面我们可以知道计算第10项的值一共执行了109次,那么优化方案就是把执行过的结果先存起来,就是典型的空间换时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let count = 0
let fibMap = new Map()
let fibonacci = (n) => {
if (fibMap.has(n)) {
return fibMap.get(n)
}
count++
if (n === 2 || n === 1) {
fibMap.set(n, 1)
return 1
}
fibMap.set(n, fibonacci(n - 1) + fibonacci(n - 2))
return fibonacci(n - 1) + fibonacci(n - 2)
}
console.log(fibonacci(10))
console.log(count) // 10

优化后,只需要10次即可

排序算法 置顶

排序算法

冒泡排序

概括

冒泡排序的思想就是在每次遍历一遍未排序的数列之后,将一个数据元素浮上去(也就是排好了一个数据)

1
2
// 由小到大排序
let arr = [12, 35, 99, 18, 76]

后续均以这个为demo样例

算法如下

1
2
3
4
5
6
7
8
9
10
for (let i = 0; i < arr.length - 1; i++) {
for (let j = i; j < arr.length - 1; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j + 1]
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
console.log(arr)

冒泡排的核心部分是双重嵌套循环,时间复杂度是 O(N^2)

插入排序

概括

将数据按照一定的顺序一个一个的插入到有序的表中,最终得到的序列就是已经排序好的数据

算法如下

1
2
3
4
5
6
7
8
9
10
11
12
// 设置游标,当前插入的值变量
let index, current
for (let i = 1; i < arr.length; i++) {
index = i - 1
current = arr[i]
while (index >= 0 && arr[index] > current) {
arr[index + 1] = arr[index]
index--
}
arr[index + 1] = current
}
console.log(arr)

在插入排序中,当待排序数组是有序时,是最优的情况,只需当前数跟前一个数比较一下就可以了,这时一共需要比较N- 1次,时间复杂度为O(n),逆序的话插入排序最坏情况下的时间复杂度为O(n^2)

快速排序

概括

快速排序(Quick Sort)算法是在冒泡排序的基础上进行改进的一种算法,从名字上看就知道该排序算法的特点是快、效率高,是处理大数据最快的排序算法之一
实现的基本思想是:通过一次排序将整个无序表分成相互独立的两部分,其中一部分中的数据都比另一部分中包含的数据的值小
然后继续沿用此方法分别对两部分进行同样的操作,直到每一个小部分不可再分,所得到的整个序列就变成有序序列

算法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const rec = (arr) => {
console.log(arr)
if (arr.length <= 1) { return arr; }
const left = [];
const right = [];
const mid = arr[0]; // 基准元素
for (let i = 1; i < arr.length; i++) {
if (arr[i] < mid) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
let result = [...rec(left),mid,...rec(right)]
return result
}

console.log(rec(arr))

快速排序是冒泡排序的升级版,最坏情况下每一次基准元素都是数组中最小或者最大的元素,则快速排序就是冒泡排序
这种情况时间复杂度就是冒泡排序的时间复杂度:T[n] = n * (n-1) = n^2 + n,也就是O(n^2)
最好情况下是O(nlog2n)

归并排序

概括

归并排序(Merge Sort)是建立归并操作上的一种有效,稳定的排序算法,该算法是采用分治法的一个非常典型的应用
将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序

算法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let mergeSort = (arr) => {
// 空数组或者单一数组没得再分了,最底部了
if (arr.length < 2) {
return arr
}
// 对半向前取整切分
let mid = Math.floor(arr.length / 2)
let left = arr.slice(0, mid)
let right = arr.slice(mid)
// 分治思想,递归
return merge(mergeSort(left), mergeSort(right))
}

// 合
let merge = (left, right) => {
let result = []
// 当两个都有长度的时候
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift())
} else {
result.push(right.shift())
}
}

while (left.length) {
result.push(left.shift())
}
while (right.length) {
result.push(right.shift())
}
return result;
}

console.log(mergeSort(arr))

上述归并分成了分、合两部分,在处理分过程中递归调用两个分的操作,时间复杂度nlogn

选择排序

概括

选择排序(Selection sort)是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度,所以用到它的时候,数据规模越小越好
其基本思想是:首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置
然后再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

let minIdx,temp
for (let i = 0; i < arr.length; i++) {
minIdx = i;
for (let j = i + 1; j < arr.length; j++) {
if(arr[j] < arr[minIdx]){
minIdx = j
}
}
temp = arr[i]
arr[i] = arr[minIdx]
arr[minIdx] = temp

}
console.log(arr)

第一次内循环比较N - 1次,然后是N-2次,N-3次,……,最后一次内循环比较1次
共比较的次数是 (N - 1) + (N - 2) + … + 1,求等差数列和,得 (N - 1 + 1)* N / 2 = N^2 / 2,舍去最高项系数,其时间复杂度为 O(N^2)

时间复杂度表

排序方法 时间复杂度[‘平均’-‘最好’-‘最坏’] 空间复杂度 稳定性
冒泡 O(n^2) - O(n) - O(n^2) O(1) 稳定
插入 O(n^2) - O(n) - O(n^2) O(1) 稳定
快速 O(nlog n) - O(nlog n) - O(n^2) O(nlog n) 不稳定
归并 O(nlog n) - O(nlog n) - O(nlog n) O(n) 稳定
选择 O(n^2) - O(n^2) - O(n^2) O(1) 不稳定

骚年,咖啡搞起吧~

支付宝
微信