为什么要优化

一次打包一个用了vuetify的项目,打包结果如下

一看这chunk-vendors足足1000K+;要是放到小水管上果断扛不住。

开始优化

  • 首先把chunk-vendors单文件体积降下来

思路:1 使用公用cdn 2 拆分vendors里node_modules的依赖文件

使用cdn配置如下

1
2
3
4
5
6
7
8
9
10
11
12
// vue.config.js
module.exports = {
configureWebpack: config => {
if (isProduction) {
config.externals = {
vue: "Vue",
"vue-router": "VueRouter",
moment: "moment"
};
}
}
};
1
2
3
4
5
<!-- public/index.html -->
<!-- CND -->
<script src="https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.runtime.min.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>

拆分文件配置:
具体配置文档optimization,
split-chunks-plugin

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
module.exports = {
configureWebpack: config => {
if (isProduction) {
config.optimization = {
runtimeChunk: "single",
splitChunks: {
chunks: "all",
maxInitialRequests: Infinity,
minSize: 20000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace("@", "")}`;
}
}
}
}
};
}
}
};

至此,打包结果如下(未使用CDN的情况)

  • 启用gzip

nginx有两种gzip方案,gzip和gzip_static,推荐使用后者,gzip是针对于请求实时进行压缩,cpu开销大。gzip_static 可以在编译后使用压缩工具搞出来。
配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports={
configureWebpack: config => {
if (isProduction) {
config.plugins.push(
new CompressionWebpackPlugin({
algorithm: "gzip",
test: new RegExp("\\.(css|js)$"),
threshold: 10240,
minRatio: 0.8
})
);
}
}
}

打包结果如下

nginx开启gzip_static 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
# 省略
gzip off;
gzip_static on; #静态压缩
gzip_min_length 10k;
gzip_buffers 4 16k;
gzip_comp_level 6;
gzip_types *;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;

# 省略
}
1
2
3
4
FROM nginx
ADD dist /usr/share/nginx/html
ADD nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

最终结果

  • 本地docker启动后结果:

优化后:

对比未优化前:

  • 加上100K的限速,模拟环境试试:
1
2
3
# nginx config
limit_rate 100k; #限制速度50K

未优化:

优化后

用技术文章的标题来说就是:网站打开速度提升了90%!

后记

拆分的js文件有点多,会影响加载速度,后期可以根据实际需求合并一些node_modules里的依赖文件,
比如 vue&vue-router&vuex可以合并为一份vue-all 的commonChunk文件,具体做法在split-chunks-plugin文档
webpack-bundle-analyzer 替我们提供了一个可视化的 dashboard,可以很直观的发现哪些第三方包或者代码文件占用的大小。

起因

看公司UI组件库代码的时候突然看到了一份格式混乱,还带有console.log()的文件,察觉到gitHooks失效了。

debug

首先项目的gitHooks用的是husky工具集成的。查看.git/hooks/目录,里面的钩子文件都是.simple后缀的。简单粗暴的把node_modules文件夹删除之后重新install一遍。
回来了,一切都回来了,顿感舒心。
好奇心驱使我看看有没有不删node_modules就能修复的办法,于是打开了github:husky
查看package.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
script: {
"test": "npm run lint && jest",
"_install": "node husky install",
"preuninstall": "node husky uninstall",
"devinstall": "npm run build && cross-env HUSKY_DEBUG=1 npm run _install -- node_modules/husky && node scripts/dev-fix-path",
"devuninstall": "npm run build && cross-env HUSKY_DEBUG=1 npm run preuninstall -- node_modules/husky",
"build": "del-cli lib && tsc",
"version": "jest -u && git add -A src/installer/__tests__/__snapshots__",
"postversion": "git push && git push --tags",
"prepublishOnly": "npm run test && npm run build && pinst --enable && pkg-ok",
"postpublish": "pinst --disable",
"lint": "eslint . --ext .js,.ts --ignore-path .gitignore",
"fix": "npm run lint -- --fix",
"doc": "markdown-toc -i README.md",
"_postinstall": "opencollective-postinstall || exit 0"
}

补充了一下npm的小知识:

npm 默认提供下面这些钩子
prepublish,postpublish
preinstall,postinstall
preuninstall,postuninstall
preversion,postversion
pretest,posttest
prestop,poststop
prestart,poststart
prerestart,postrestart

就是可以在安装依赖时执行自己的脚本咯?(隐隐感到好像会出现安全问题。)

安全问题先放一边,在husky的script里面并没有看到preinstall,postinstall 的指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 npm i husky

> husky@3.0.9 install C:\person\test\node_modules\husky
> node husky install

husky > Setting up git hooks
Command failed: git rev-parse --show-toplevel --git-common-dir
fatal: not a git repository (or any of the parent directories): .git
husky > Failed to install

> husky@3.0.9 postinstall C:\person\test\node_modules\husky
> opencollective-postinstall || exit 0

Thank you for using husky!
If you rely on this package, please consider supporting our open collective:
> https://opencollective.com/husky/donate

npm WARN test@1.0.0 No description
npm WARN test@1.0.0 No repository field.

+ husky@3.0.9
added 59 packages from 30 contributors, removed 190 packages and audited 92 packages in 34.009s
found 0 vulnerabilities

在安装时又有执行node husky install, opencollective-postinstall || exit 0两条对应_install 和_postinstall指令。
难道npm新增了_install 钩子???搜了一遍npm文档也没有找到。

最终在这里找到了答案。
在发布的时候会把_install 变为install!

1
"prepublishOnly": "npm run test && npm run build && pinst --enable && pkg-ok",

注意pinst --enable,查npmjs.com:

pinst lets you have postinstall hook that runs only in development 🍺
pinst also supports install alias.

至此,debug算告一段落。但是pinst到底有什么用,适用什么场景???难道他要困扰我整个程序员生涯吗。

看了尤大的yorkie
package 里

1
2
3
4
5
6
7

"scripts": {
"test": "jest",
"format": "prettier --single-quote --no-semi --write **/*.js",
"install": "node bin/install.js",
"uninstall": "node bin/uninstall.js"
},

啊~舒服了。

后记

想试试看npm install 能干什么坏事。

service worker

与web worker的异同:

  • Service Worker 工作在 worker context 中,是没有访问 DOM 的权限的,所以我们无法在 Service Worker 中获取 DOM 节点,也无法在其中操作 DOM 元素;

  • 我们可以通过 postMessage 接口把数据传递给其他 JS 文件;

  • Service Worker 中运行的代码不会被阻塞,也不会阻塞其他页面的 JS 文件中的代码;

  • 不同的地方在于,Service Worker 是一个浏览器中的进程而不是浏览器内核下的线程,因此它在被注册安装之后,能够被在多个页面中使用,也不会因为页面的关闭而被销毁。因此,Service Worker 很适合被用与多个页面需要使用的复杂数据的计算——购买一次,全家“收益”。

支持的事件:

install,activate,message,fetch,sync,push

主要功能

  • 离线缓存
  • 服务端推送消息
  • 代理请求&请求劫持
  • 跨页面通信

上手

关于离线缓存
MDN
和消息通知google develop
这两篇里都已经安排的明明白白了。

唯一要提的是推送服务依赖一个FCM服务,而这个服务是被墙的,所以国内基本看不到推送消息。具体文档

思考

  • 在使用离线缓存时,甚至可以缓存入口html。而不是像配置了etag或者last-modified 一样,返回304。这就极大增强了应用离线访问的能力,如果站点是个纯静态的,那体验可以做到相当好。但是都9102年了,除了博客、文档,还存在纯静态的网站吗?
    我们的痛点永远是首屏加载不够快。现在的spa应用基本上是先载好JS资源然后再发起异步请求,获取首屏数据渲染。所以才有ssr来解决下载文件,执行脚本到获取数据这段空白时间。
    通过离线缓存,可以快速从本地打开页面,因为全是from appcache,不用同服务器交互,再载入框架shell页面之后,可以增加各种骨架屏、占位图来增强loading时的体验。

  • 关于cache api,cache内容要求我们指定cache文件名,或者通过拦截fetch请求然后cache.add(response.clone())加入到AppCache中。spa应用大部分文件都是带hash的,所以要提前生成一份文件列表,需要做的工作有1、构建工程,生成dist文件。2、写脚本获取生成的js、css文件列表(或许还有image),更新service worker文件。使用请求拦截的情况需要判断请求是否未api请求,否则缓存了个接请求就大事不好了(虽然cache会忽略post等请求)。

  • 如何做到更快?类似微博,在下次唤醒时保留的还是之前看的feed流内容。所以web应用也可以把状态管理里的内容存到indexdb里,等待下一次程序唤醒时找个合理的实际,再把store载入回来,因为内容都是cache过的,所以可以立马打开。

  • service worker的优势只有请求代理和缓存吗?前面我们说到他与web worker的区别时提到他是一个进程。他提供了一个clients对象可以向所有打开的窗口postMessage。在一些特殊场景下:比如我现在正在做的在线答题,一张页面记录了答题进度,一张记录了答题详情,测试要求我们答题时另一张的页面要一起更新。放以前我就上socket或者根据visibilitychange重新获取数据了,
    现在可以使用service worker向所有页面广播答题进度,对应界面收到消息之后再更新。

graphql

是什么?

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

为什么?

请求你所要的数据不多不少

向你的 API 发出一个 GraphQL 请求就能准确获得你想要的数据,不多不少。 GraphQL 查询总是返回可预测的结果。使用 GraphQL 的应用可以工作得又快又稳,因为控制数据的是应用,而不是服务器。

获取多个资源只用一个请求

GraphQL 查询不仅能够获得资源的属性,还能沿着资源间引用进一步查询。典型的 REST API 请求多个资源时得载入多个 URL,而 GraphQL 可以通过一次请求就获取你应用所需的所有数据。这样一来,即使是比较慢的移动网络连接下,使用 GraphQL 的应用也能表现得足够迅速。

描述所有的可能类型系统

GraphQL API 基于类型和字段的方式进行组织,而非入口端点。你可以通过一个单一入口端点得到你所有的数据能力。GraphQL 使用类型来保证应用只请求可能的数据,还提供了清晰的辅助性错误信息。应用可以使用类型,而避免编写手动解析代码。

API 演进无需划分版本

给你的 GraphQL API 添加字段和类型而无需影响现有查询。老旧的字段可以废弃,从工具中隐藏。通过使用单一演进版本,GraphQL API 使得应用始终能够使用新的特性,并鼓励使用更加简洁、更好维护的服务端代码。

使用你现有的数据和代码

GraphQL 让你的整个应用共享一套 API,而不用被限制于特定存储引擎。GraphQL 引擎已经有多种语言实现,通过 GraphQL API 能够更好利用你的现有数据和代码。你只需要为类型系统的字段编写函数,GraphQL 就能通过优化并发的方式来调用它们。

怎么用?

首先理解一些基本概念:
Schema 和类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// schame.gql
type User{
account: String
name: String
}
type todolist: {
time: String!
content: String!
}
type Query{
userinfo(account: String): User
todolists: [todolist]
}
type Mutation {
registry(account:String, name:String): User
}

可以把他理解为一个供前端查询的入口文件,里面包含了类型系统,查询函数和变更函数。服务端收到请求后调用对应resolver函数,然后返回对应type数据。

1
2
3
4
5
6
7
8
query {
userinfo {
name
}
todolists{
content
}
}

前端调用时,可以通过查询gql,获取对应数据,如上,只发一次请求能同时获取userinfo和todolist的数据。
在实现上,其实就是结构化query数据,然后使用fetch/xmlhttprequest发送。服务端接收到之后再解构出对应的请求操作,返回数据。

一点思考

  • 在使用apollo-react时觉得他们Query、Mutation组件真好用,自带loading和error处理。打算在普通接口项目里结合Redux试一下。

  • resolve函数中可以从多个数据源获取数据,可以是MySQL,redis,或者其他第三方api,提供了一定的便利。可以用作一个统一的接口集合,打造一个多App的中台服务。

  • 接口迭代升级方便,可快速为type增删字段。接口测试时同理,但暂时没有找到相关的成熟工具。

  • 关于一次请求所有所需数据和只获取所需数据,在网络层面1、节省了请求数量;2、缩小了所发送数据包的体积。但是缺点依然很明显

    • resolve函数必须获取全部所需数据才能供查询筛选,比如获取用户信息,也许前端只要id&username,但是后端的查询得select * from user;有时也许还得join多张表,这点就没有定制接口来得高效
    • 如果一次请求中包含了很耗时的操作,比如获取用户信息同时调第三方接口获取todolist详细数据。之前我们拆分2个请求可以做到先把用户部分渲染出来,然后列表数据慢慢loading。graphql想做到这种效果依然要创建两次请求。在没有形成调用规范之前,调用方很可能就忽略这部分耗时接口,导致页面渲染慢。
    • 随着项目逐渐庞大,入口文件拆分也是问题。如果作为一个业务中台,多业务的接口权限要如何限制?

总结

graphql很好,让前端支配服务端又迈了一步,但是就像最早我们抛弃服务端渲染template,转为前后端分离,现在又开始做ssr,值不值得用还等根据业务仔细考量。g

浏览器缓存

强缓存

  • Expires: Expires是http1.0提出的一个表示资源过期时间的header,它描述的是一个绝对时间,由服务器返回。Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效
    1
    Expires: Wed, 11 May 2018 07:20:00 GMT
  • Cache-Control: Cache-Control 出现于 HTTP / 1.1,优先级高于 Expires ,表示的是相对时间
    1
    Cache-Control: max-age=315360000
    题外tips
    Cache-Control: no-cache不会缓存数据到本地的说法是错误的,详情《HTTP权威指南》P182
    Cache-Control: no-store才是真正的不缓存数据到本地
    Cache-Control: public可以被所有用户缓存(多用户共享),包括终端和CDN等中间代理服务器
    Cache-Control: private只能被终端浏览器缓存(而且是私有缓存),不允许中继缓存服务器进行缓存

协商缓存

当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串
协商缓存是利用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】这两对Header来管理的

  • Last-Modified,If-Modified-Since
    Last-Modified 表示本地文件最后修改日期,浏览器会在request header加上If-Modified-Since(上次返回的Last-Modified的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来
    但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag

  • ETag、If-None-Match

    Etag就像一个指纹,资源变化都会导致ETag变化,跟最后修改时间没有关系,ETag可以保证每一个资源是唯一的
    If-None-Match的header会将上次返回的Etag发送给服务器,询问该资源的Etag是否有更新,有变动就会发送新的资源回来

    ETag的优先级比Last-Modified更高

    具体为什么要用ETag,主要出于下面几种情况考虑:

    • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
    • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
    • 某些服务器不能精确的得到文件的最后修改时间。

启动nginx实验

纸上得来终觉浅,亲自动手才能记得更牢。下面跟着我的节奏,来一场快速的浏览器缓存实验。

测试页面

首先明确我们的需求,设计如下html。包含一个标准文档和一个jpg静态资源。

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<div>test</div>
<img src="./avatar.jpg" alt="">
</body>
</html>

添加nginx.conf开始测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
新建nginx.conf文件
server {
listen 10000;
server_name localhost;
index index.html;
root /usr/share/nginx/html;
location / {
etag off;
add_header Last-Modified '';
index index.html;
}
location ~ \.(gif|jpg|jpeg|png|bmp|ico)$ {
etag off;
add_header Last-Modified '';
}
}

因为nginx 默认开启了etag 和Last-Modified,我们先把他去掉看效果

nginx on!

1
2
3
docker run -v /path/to/your/html:/usr/share/nginx/html \
-v /path/to/your/nginx.conf:/etc/nginx/conf.d/mysite.conf \
-p 10000:10000 --name nginx-test nginx

新建一个无痕浏览器窗口,打开localhost:10000;多次刷新页面依然之后查看请求详情



并且执行 docker logs -f nginx-test 能发现每次服务器都收到了请求日志。

可见没有设置缓存header的情况下,浏览器还真是一点缓存效果都没有!

接下来我们添加一些强缓存的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 10001;
server_name localhost;
index index.html;
root /usr/share/nginx/html;

limit_rate 50k; #限制速度50K

etag off; # 关闭协商缓存
add_header Last-Modified ''; # 关闭强缓存

expires 30s;

location / {
index index.html;
}
location ~ \.(gif|jpg|jpeg|png|bmp|ico)$ {
}
}

执行docker restart nginx-text;刷新页面发现:


图片的缓存已经生效,并且有效期为30秒。因为docker没有设置时区,所以我本地的时间是大于expires的,但是缓存还是生效了,所以证明Cache-Control的优先级大于expires。使用浏览器强制刷新能忽略缓存时间,重新拉取资源。

对于文档文件,每次刷新都会带上Cache-Control: max-age=0,强制刷新时会带上Cache-Control: no-cache;Pragma: no-cache,并且服务器一直都有请求日志。所以对文档设置expires和cache-control无效?(一种猜测)

接下来我们再添加一些协商缓存的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 10000;
server_name localhost;
index index.html;
root /usr/share/nginx/html;

limit_rate 50k; #限制速度50K

etag off; # 关闭协商缓存

expires 10s;

location / {
index index.html;
}
location ~ \.(gif|jpg|jpeg|png|bmp|ico)$ {
}
}

docker restart nginx-test之后刷新几次发现


文档文件协商缓存生效,但是资源文件还是使用强缓存原因是:

如果Expires,Cache-Control: max-age,或 Cache-Control:s-maxage都没有在响应头中出现,并且设置了Last-Modified时,那么浏览器默认会采用一个启发式的算法,即启发式缓存。通常会取响应头的Date_value - Last-Modified_value值的10%作为缓存时间。

强制刷新时不会带上If-Modified-Since,所以直接从服务端取文件。
修改index.html文件之后request的If-Modified-Since小于服务器中记录的时间,于是返回了新文档,状态码200,并且更新了response的Last-Modified.

ETag流程和Last-Modified的表现几乎一致,就不多赘述了。

最后,同时使用强缓存和协商缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 10000;
server_name localhost;
index index.html;
root /usr/share/nginx/html;
expired 30s;

location / {
index index.html;
}
location ~ \.(gif|jpg|jpeg|png|bmp|ico)$ {
}
}

刷新时,文档304,资源200 from cache。30秒后强缓存失效,文档仍然304,资源也变为304,之后再变为200.

缓存的升级方案:

service worker + caches api;

同一首歌

线上地址

17年时写的,许久没维护,近期发现获取歌曲URL的方法失效了,修改了一下推上线继续用。使用socketio做歌曲同步,可以和异地的朋友同时听同一首歌。

仿bilibili视频人像弹幕遮罩

线上地址
资源加载有点慢,用了tensorflowjs和一个已经训练好的人体识别模型。简单测试了一下在浏览器中做识别的效率,

webrtc创建的视频聊天应用

github
踩过的坑:在创建点对点连接的时候需要用先由服务器转发icecandidate信息,我用了socketio,等到链接建立成功之后才断开。
在建立连接之前就要指定要发送的内容,在连接建立成功之后不能修改。
在4G网络下基本上不能直连,需要有个stun服务器做转发

node端的tensorflow

github
在node端使用tensorflow训练一个五子棋AI

给新手的入门指南,涉及到的技术栈有。

  • 前端:vue,vue-router,vuex
  • 后端:eggjs
  • 数据库:mysql
  • 工具:docker,docker-compose,travis-ci
    阅读全文 »

从测试命令开始:

入口测试指令npm run test;
在package.json 当中;

1
"test": "npm run lint && flow check && npm run test:types && npm run test:cover && npm run test:e2e -- --env phantomjs && npm run test:ssr && npm run test:weex",

前三条是语法检测,按照各自的代码格式或者语法配置说明配置好之后就能顺利跑起来,并且带自动纠正功能。

阅读全文 »
0%