# 2020年10月技术日常
# 2020/10/31 周六
# 防盗链时需要注意搜索引擎 Referer:百度和 Google 搜索内容跳转链接之间的区别
一般搜索引擎为了方便网页做来源分析,不会使用 noreferer,因此从搜索引擎进入页面时,会携带对应的 Referer。当首页 index.html 也放在 CDN 的情况时,做防盗链 Referer 白名单时,要记得放开搜索引擎的相关 Referer。下面分两个部分介绍搜索引擎跳转链接的处理
- 百度搜索结果链接的跳转方式
- Google搜索结构链接的跳转方式
百度搜索结果链接的跳转方式
如上图所示,百度搜索结果列表里的链接,指向的任然是 baidu.com 的域名,访问这个链接它会做一个重定向。我们可以使用 curl -v
的方式来查看具体逻辑
curl -v 'https://www.baidu.com/link?url=MKActaa6Ed8aGU2yOX2y9v3ne5xinD6tt_v-PHOZ9STfd8cgtAY0yi-c5FEqiIt-CW9_8db1PBwqnTE7jEdb3K&wd=&eqid=a4d184060000714d000000065f9d3673'
下面是得到的网页内容
可以看到如果支持 script
,一般默认使用 window.location.replace 重定向到目标链接。如果不支持 script
,使用 meta 的方式进行跳转。
具体排版后,内容如下
<script>
try {
if (window.opener && window.opener.bds && window.opener.bds.pdc && window.opener.bds.pdc.sendLinkLog) {
window.opener.bds.pdc.sendLinkLog();
}
} catch (e) {
};
var timeout = 0;
if (/bdlksmp/.test(window.location.href)) {
var reg = /bdlksmp=([^=&]+)/, matches = window.location.href.match(reg);
timeout = matches[1] ? matches[1] : 0
};
setTimeout(function () {
window.location.replace("https://blog.csdn.net/aexwx/article/details/86775768")
}, timeout);
window.opener = null;
</script>
<noscript>
<META http-equiv="refresh" content="0;URL='https://blog.csdn.net/aexwx/article/details/86775768'">
</noscript>
在进入下面后,我们查看 Network 里面的 Referer 会看到来源的百度链接,如下图
Google搜索结构链接的跳转方式
如下图所示 Google 搜索列表是直接链接到目标地址,和百度先跳自己的链接再重定向是不同的。
这种情况来看进入页面后,其 Referer,可以看到只有 google 的链接没有参数,可能这就是为什么百度统计里面只能看到百度搜索的来源关键字,而看不到 Google 搜索的来源关键字的原因。
# 外部链接 a 标签为什么要加 noreferer 与 noopener ?
一般页面的外部连接都会加上 ref="noreferer noopener"
,这样可以避免一些安全问题,下面通过几个问题来具体看看
- a 标签加上 noreferer 和 noopener 后会有什么效果?
- Referer 是什么?
- Referer 的应用场景
- window.opener 可以做什么?
a 标签加上 noreferer 和 noopener 后会有什么效果?
以 Github 上 less.js 仓库设置的网站链接为例,如上图。加了这两个参数后点击链接,该页面打开后
- 请求头(Request Headers)部分的 Referer 和直接访问的 Referer 一致,都为空,不会携带来源信息。
- window.opener 和直接访问该网站一致,无法获取来源网站信息,无法操作来源网站的跳转等
请求头 Referer 是什么?
一般网页在加载html、js、css、图片等静态资源发送请求时,请求头部分会有一个 Referer 字段,用于标记请求来源。referer 单词存在拼写错误,本意是打算使用 referrer,写错了。后来为了兼容,将错就错,还是保留了错误的拼写方式。
Referer 的应用场景
Referer可以标记请求来源,有以下几个应用场景
- 用于统计分析,可以知道用户是从哪种方式进入网站的。搜索引擎一般不会开启 noreferer,比如百度统计可以知道你是通过哪个关键字进入的页面。
用于防盗链,防止网页静态资源被其他站点直接引用,如淘宝店铺图片、CDN图片链接、文件、视频链接等。一般会设置 referer 白名单,仅允许白名单内的 Referer 访问,否则禁止访问。减少服务器负载或不必要的 CDN 流量花费。
用于鉴权,比如页面在集成评论系统、Google AdSense等第三方功能时,会校验站点与ID是否匹配,如果不匹配会提示 403。防止其他网站引入对应的代码后,导致数据错乱。我们在处理接口请求时,也可以对 Referer 值进行判断,禁止某些来源访问接口。
window.opener 可以做什么?
window.opener 可以拿到来源网站的 window 对象,虽然一般访问 dom 等有跨域限制,但 window.opener.location 可以直接重定向网站,使来源站点发生变化,referer 保持源网站链接。使用 noopener 可以避免一些安全风险。
参考:
# 2020/10/29 周四
# VuePress 复选框、任务列表不生效怎么处理
在写 markdown 笔记时,复选框、任务列表(task list)功能在本地 Typora 是生效的,但在 VuePress 中无法正常显示。于是在 Github 对应的 issue 里面搜索 Task lists,找到了解决方法:需要安装一个 markdown 插件 markdown-it-task-lists
- [x] 已完成的计划
- [ ] 待完成 1
- [ ] 待完成 2
- [x] 已完成的计划
- [ ] 待完成 1
- [ ] 待完成 2
插件安装
npm install markdown-it-task-lists -D
修改配置
// config.js
module.exports = {
markdown: {
plugins: ['task-lists']
}
}
完全退出 dev,再重新开启服务后,就正常了,对比图如下:
参考: 怎么实现复选框功能 · Issue #2364 · vuejs/vuepress (opens new window)
# 2020/10/27 周二
# 使用VuePress生成静态网站并部署到Github Pages
VuePress (opens new window) 是一个静态网站生成器,诞生初衷是为了支持 Vue 及其子项目的文档需求。目前 Vue 相关文档都是有 vuepress 搭建。
Docsify 是运行时驱动,通过 JS 加载内容,对 SEO 不够友好。它类似于 Hexo,只是 VuePress 是由 Vue 驱动。
VuePress 的比较好的地方:
- 丰富的 Markdown 扩展、主题风格优雅
- 可以使用插件支持 PWA
- SEO友好
现在以 zuoxiaobai/fenote (opens new window) Github 仓库为例,为该仓库搭建一个官网
核心目录结构如下,完整目录结构参考 VuePress 目录结构 (opens new window)
├── docs # docs 是文档项目名称,也可以自己命名
│ ├── .vuepress # .vuepress 配置、构件生成目录
│ ├── README.md # 默认的首页
│ ├── 其他markdown目录及文档
└── package.json
它的本质就是,先按照指定的目录格式,写好 Markdown 及配置。然后使用 vuepress dev docs 命令生成静态站点,生成目录默认为 docs/.vuepress/dist。另外它还提供了开发server,运行 vuepress dev docs 可以实时看页面效果。
由于需要使用 vuepress 命令,我们为了方便后期维护迭代,一般不推荐全局安装 vuepress。在项目内安装即可。如果项目根目录没有 package.json,那就需要自己创建一个了。
# 初始化一个 package.json,如果这个文件存在,可跳过
npm init
# 安装 vuepress 开发依赖
npm install vuepress -D # --save-dev
在 package.json 里面添加下面两条命令的快捷方式,这样就可以使用 npm run docs:dev,以及 npm run docs:build 来运行或构件静态站点了。
"scripts": {
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
},
下面按照上面的目录结构,把 docs、.vuepress文件夹,以及 README.md 创建好,REAMEME.md 写一个 hello vuepress。运行 npm run docs:dev 它默认会在本地开启 8080 端口服务,访问效果如下图
可以看到标题、右上角导航栏、左侧菜单都没有出来。这就需要在 .vuepress/confing.js 写对应的配置了。你需要关系 4 点:
- title 用于配置左上角标题
- themeConfig.sidebar 用于配置右上角的导航栏
- themeConfig.nav 用于配置左侧菜单
- 首页默认是 docs/README.md,
/
表示 docs 目录,比如:/a.md
表示dosc/a.md
// .vuepress/config.js
module.exports = {
// base: '/fenote/',
title: 'dev-zuo 笔记',
description: 'dev-zuo 笔记,用于记录、完善个人前端知识体系结构',
themeConfig: {
sidebar: [
{
title: '指南',
children: [
'/',
'/a.md',
'/b.md'
]
}
],
nav: [
{ text: '指南', link: '/' },
{ text: '配置', link: '/config.md' },
{ text: 'Github', link: 'https://www.github.com/zuoxiaobai/fenote' }
]
}
}
我们按照上面的配置文件写好后,在 docs 目录下创建 a.md,b.md,a.md 如下
# a文件
a.md
由于 npm run build:dev 后,它支持类似 HMR 热模块加载的功能,修改后,会自动重新构建。根据上面的配置好后,效果如下
这样我们就可以根据自己的需要,规划顶部导航以及左侧菜单了。更多配置、markdown扩展语法参见:基本配置 | VuePress (opens new window)
这样写好后,只能本地运行,怎么部署到 Github Pages 呢?VuePress官网提供一个部署脚本,可以放到项目根目录 deploy.sh
#!/usr/bin/env sh
# 确保脚本抛出遇到的错误
set -e
# 生成静态文件
npm run docs:build
# 进入生成的文件夹
cd docs/.vuepress/dist
# 如果是发布到自定义域名
# echo 'www.example.com' > CNAME
# 在新生成的目录下初始化 .git,并 add 所有文件,提交到该目录下项目的本地master分支(默认)
git init
git add -A
git commit -m 'deploy'
# 如果发布到 https://<USERNAME>.github.io
# git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git master
# 如果发布到 https://<USERNAME>.github.io/<REPO>
# git push -f git@github.com:<USERNAME>/<REPO>.git master:gh-pages
# zuoxiaobai/fenote 对应的配置如下,将doscs/.vuepress/dist 目录下的 master 分支 push 到 fenote 远程远程仓库的 gh-pages 分支
git push -f git@github.com:zuoxiaobai/fenote.git master:gh-pages
cd -
在 mac 下,新建的 deploy.sh 默认没有可执行权限,需要 chmod +x deploy.sh
,这样就可以 ./deploy.sh 直接部署了。
注意:如果发布到 https://<USERNAME>.github.io/<REPO>
,且没有使用自定义域名。则需要修改 config.js 的 base 配置为对应的仓库名
module.exports = {
base: '/fenote/',
title: 'dev-zuo 笔记',
// ...
}
运行 ./deploy.sh,部署成功后,可以通过 https://zuoxiaobai.github.io/fenote/
来访问了,如下图
# 2020/10/22 周四
# js使用localeCompare函数对中文进行首字母排序
tag: js中文按首字母排序, 前端中文按首字母排序,前端中文排序
String.prototype.localeCompare(compareString[, locales[, options]])
该方法用于对字符串进行排序。第二个参数 locales 可以指定语言,中文排序传 'ch' 即可。它的返回值为 -1, 1, 0 和 sort 自定义排序的返回值基本一致。来看一个例子
['中文zw', '啊啊啊aaa', '猜猜猜ccc'].sort((a, b) => a.localeCompare(b, 'ch'))
// ["啊啊啊aaa", "猜猜猜ccc", "中文zw"]
参考:
# js判断两个日期是否是同一周,带单元测试
给定两个日期,怎么判断他们是同一周呢?核心是 所有时间都是从 1970年1月1日(周4) 开始,(天数 + 4)/7 就是周数,如果相同则是同一周,这里会有一个特殊情况,就是周日的时候。只需要日期,不要输入时间,默认都是以 '08:00:00' 为准。
/**
* @description 判断两个时间是否是同一周
* 所有时间都是从 1970年1月1日(周4) 开始,(天数 + 4)/7 就是周数,如果相同则是同一周
* 特殊情况:周日会是整数,如果直接取整,周日会和下周一是同一天
* (+new Date('1970-01-01') / oneDay) + 4 // 4 周四 /7 = 0.57
* (+new Date('1970-01-04') / oneDay) + 4 // 4 周日 /7 = 1
* (+new Date('1970-01-05') / oneDay) + 4 // 4 周一 /7 = 1.14
* @params { Stirng } timeA '1970-01-03'
* @params { Stirng } timeB '1970-01-22'
*/
function isSameWeek(timeA, timeB) {
let weekIndexA = getWeekIndex(timeA)
let weekIndexB = getWeekIndex(timeB)
let tempArr = [weekIndexA, weekIndexB].sort()
// 如果有周日,间隔 < 1,则是 [1.9, 2] 或 [1, 1.14],较大的数为整数则是同一周。间隔 >=1 则不是同一周
if (tempArr.some(item => Number.isInteger(item))) {
return tempArr[1] - tempArr[0] < 1 ? Number.isInteger(tempArr[1]) : false
} else {
return parseInt(weekIndexA) === parseInt(weekIndexB)
}
// 获取周数
function getWeekIndex(time) {
let oneDayTime = 24 * 3600 * 1000
let dayCount = time.getTime() / oneDayTime
let weekCount = (dayCount + 4) / 7
return weekCount
}
}
单元测试
// isSameWeek 单元测试
function isSameWeekTest() {
let list = [
{ a: '1970-01-01', b: '1970-01-04', result: true },
{ a: '1970-01-04', b: '1970-01-05', result: false },
{ a: '1970-01-12', b: '1970-01-11', result: false },
{ a: '1970-01-22', b: '1970-01-23', result: true },
]
list.forEach(item => {
let res = isSameWeek(new Date(item.a), new Date(item.b)) === item.result
console.log(
`%c${res ? 'PASS' : 'FAIL'} '${item.a}','${item.b}',${item.result}`,
`color: ${res ? 'green' : 'red'}`
)
})
}
// Run
isSameWeekTest()
运行效果:
# console.log 打印带样式的文字,图片
console.log 的第一个参数中,如果有 '%c',表示设置样式,会将第二个参数的 css 样式应用到第一个参数的内容中
console.log('%c文字', 'css样式')
这样可以打印绿色或红色的文字
console.log('%cSuccess!', 'color: green')
不仅可以设置文字颜色,还可以通过设置 background-color 在控制台显示图片
if (console) {
console.clear();
console.log("%c ", "padding:112px 150px;background:url('https://images.cnblogs.com/cnblogs_com/enumx/1647344/o_200214113324console.gif') no-repeat;");
console.log('%cWelcome', 'color: #0000ff;font-size: 20px;font-weight: bold;');
}
效果如下
参考:console.log输出字体颜色 - enumx - 博客园 (opens new window)
# 2020/10/21 周三
# 两种方法解决Error: Cannot find module 'webpack-cli/bin/config-yargs'
在运行 webpack-dev-server 这个命令时,如果出现了 Error: Cannot find module 'webpack-cli/bin/config-yargs' 这个错误,是因为默认情况下 webpack-dev-server 执行依赖 webpack-cli 包目录下的 bin/config-yargs,但 webpack-cli 4.1.0 的版本。有较大调整,删除了这个文件,导致了这个错误。
"webpack": "^5.1.3",
"webpack-cli": "^4.1.0",
"webpack-dev-server": "^3.11.0"
有两种解决方法
- 将 webpack-cli 降级到 3.x版本, "webpack-cli": "^ 3.3.12"
- 使用 webpack 5.x 用于替代 webpack-dev-server 命令的 webpack serve 命令。其实它内部还是使用的 webpack-dev-server 这个包
// package.json scripts
"dev:server": "webpack serve --config webpack.dev.js"
# 2020/10/20 周二
# vscode配置了自动fix突然失效了,或者一直生效不了,怎么看对应的log
你是否会遇到下面的问题:在 vscode 里面安装了 eslint 插件后,正确设置了保存后自动 fix 参数,但没有生效。或者之前是生效的,忽然就不生效了。
只要你的配置是没有问题的,那就是插件依赖的包加载异常了,一般都重启几次就Ok了。那怎么看 eslint 对应的log呢?可以分下面两步
- 点击 vscode 右下角的错误信息、警告信息图标,看是信息里是否有 eslint 相关报错
- 点击 输出 - 选择 ESLint 就可以看 ESLint 相关 log 了。
下图是 ESLint 正常加载的 log
下图是 eslint 失效后,查看 输出 - ESLint log 报的错误。提示 eslint-plugin-vue 找不到,一般就是 vscode 内部加载失败了。将 vscode 完全退出,再打开。或者将项目单独用一个新窗口打开就又正常了。
# docsify嵌入vue echarts组件无法显示图表的问题
注意,只要是 echarts vue 组件,使用 docsify 自带的 vue 后,echarts图出不来。后来打印 log 发现,最终渲染到页面的 echarts div并不是 vue 初始化之后,进行绘制的echarts div。而是一个拷贝后的副本,所以图片显示不出来。 需要使用 vuep 插件才行。实例参考 https://vuechart.zuo11.com,效果如下图:
对应的 markdown 文件内容如下
### z-chart
z-chart组件是基于echarts的组件,只需要设置父容器的宽高,再设置 options 值即可。
<!-- markdown文档里插入vuep代码 -->
<vuep template="#basicBar"></vuep>
<script v-pre type="text/x-template" id="basicBar">
<template>
<div style="width:100%; height:100%;">
<z-chart :options="chartData" />
</div>
</template>
<script>
module.exports = {
created () {
this.chartData = {
title: {
text: "ECharts 入门示例"
},
tooltip: {},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"]
},
yAxis: {},
series: [
{
name: "销量",
type: "bar",
data: [5, 20, 36, 10, 10, 20]
}
]
}
}
}
</script>
注意在 index.html 里面需要引入 vuep,以及其他你需要的组件
<script src="//cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<script src="//unpkg.com/vuep/dist/vuep.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/4.8.0/echarts-en.common.min.js"></script>
<script src="https://unpkg.com/@guoqzuo/vue-chart@latest/lib/vue-chart.umd.min.js"></script>
# GitHub Pages使用自定义域名开启HTTPS,配置CNAME解析
一般在仓库的Setting中,开启 Github Pages 会生成一个 xxx.github.io/xx/
的地址,地址有点长,我们可以使用自定义域名,这里我将 vue-chart 这个仓库设置成了自定义域名 vuechart.zuo11.com。然后,我们需要把自定义的域名解析到 github.com 地址。可以使用 ping github.com 来获取它的服务器 IP。
获取 IP 后,我们到域名管理的位置,设置解析。
- 记录类型,就是域名的解析类型,最常见的是
A
类型,就是将域名解析到服务器 IP。CNAME
是将域名指向另一个域名 - 主机记录,就是域名前缀,
@
表示 xx.com,www
表示www.xx.com
,一般设置这两种。mail
表示 mail.xx.com,代表二级域名。
解析到 github IP 后,可以正常用域名访问到。但开启 https 后,证书有问题。这时提示需要配置 CNAME 解析。于是就配置成 CNAME 解析了。将 zuo11.com 的二级域名 vuechart 解析到 zuoxiaobia.github.io,这样就可以使用 https 访问了。而且证书正常。
# xx.github.io仓库配置Github pages后对其他仓库的影响
在 github 中,假设我们创建了 github用户名.github.io
仓库,开启 Github Pages 后,访问该域名,就指向了这个仓库的文件。这时如果你的其他仓库也开启了 Github Pages,那么对于的目录解析可能会有问题。
以我的 github 账号 zuoxiaobai 为例,zuoxiaobai.github.io 这个仓库
开启pages后,如果 zuo-blog 仓库也开启 pages,那么访问 https://zuoxiaobai.github.io/zuo-blog/
并不能访问 /docs/index.html,而是提示404。需要访问 xx/zuo-blog/index.html
才行。这里我直接把 zuoxiaobai.github.io 的仓库关掉了Github Pages。这样 /xx/zuo-blog/
才能自动解析目录下的 index.html。
# lodash.js打包后默认是整包,怎么按需打包,减少包体积
在 vue-cli 打包 lib 项目时,发现包体积较大有 600多KB,于是使用 -- report
参数看具体是哪个包较大,发现尽管只用到了 lodash 的一个函数,但打包体积却有几百k,如下图,应该是整包打的,没有按需打包。
# 以 src/index.js 为入口,以库的形式打包到lib目录下,并生成 report.html
vue-cli-service build --mode lib --target lib --dest lib --report src/index.js
以下是 打开 lib/项目名.umd-report.html 后,显示的各模块大小示意图
这里借助 babel 的 loadsh 插件来进行按需打包。如果没有babel的配置文件,新建 .babelrc 文件,加入如下内容:
// .babelrc 使用 lodash 的babel插件
{
"plugins": ["lodash"]
}
lodash 的 babel 插件就是 babel-plugin-lodash,需要先 npm 安装下
npm install babel-plugin-lodash -D
ok后,重新打包,就是按需打包了。如下图,体积只有 100 多 KB 了。
在来看看 report 信息,可以看到,只打包了使用到的函数
# vue-cli build --target lib时如何避免打包成多个umd.js文件
vue-cli项目中,一般我们使用的是 npm run build 来构建项目,并发布到线上。当我们写组件/工具库的时候,就需要使用 --target lib
参数了。
打包成库与普通的构建应用不一样,它会在 dist目录下生成对应的 umd.js 文件,也就是通用模块定义的 js 文件。一般用于组件/工具库的入口文件,我们可以在静态 html 以及 vue-cli 等项目中直接引入并使用。如果不进行构建,只能在 vue-cli 项目中使用,无法引入一个 js 直接使用。
在项目中,运行打包命令
# 打包成库 Library,指定入口为 src/index.js,构建后生成目录为 lib 目录
# --mode lib 不单独生成css,样式内联
# --target lib 打包形式为 lib
vue-cli-service build --mode lib --target lib --dest lib src/index.js
执行效果如下
一般我们把 *.umd.js 引入到项目中就可以使用。但这里分包了。将 *.umd.js 文件,分了好几个小包。在普通 html 文件里面引入是可以正常运行的。他会根据 umd.js 找到需要加载的其他分包并加载。但在vue项目中只引入 *.umd.js,其他分包不会打包到项目中,导致无法运行。
其实看上面的图,js 文件超过 77KB 左右就分块了。Gzipped 压缩后不到 12KB,我们完全可以将这些打包成一个 umd.js 文件,而不需要分多个文件。
这里借助 webpack 的一个插件来配置 Chunk 数量,maxChunks 设置为 1,只打一个包,不分多个 js 文件。在 vue.config.js 里修改webpack 的配置。
// vue.config.js
const webpack = require("webpack");
module.exports = {
configureWebpack: {
plugins: [
// 限制只打一个包,不分Chunk
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
],
// 当库里面引入了比较大的文件时,为了不影响主包大小,需要设置下该包使用外部引入
externals: {
echarts: "echarts"
}
}
};
这样设置后,再重新打包就正常了,只有一个包,各平台就都没问题了。而且 gzip 压缩后也才 14KB 不到。如下图
扩展:
如果想要了解对于 vue-cli 打包 vue-cli-service --target lib
的具体执行,可以看 Vue CLI 源码
// @vue/cli-service/lib/commands/build/index.js
// Vue CLI源码,bulid 入口文件,lib时,配置处理
// resolve raw webpack config
let webpackConfig
if (args.target === 'lib') {
webpackConfig = require('./resolveLibConfig')(api, args, options)
}
// .....
else {
webpackConfig = require('./resolveAppConfig')(api, args, options)
}
// @vue/cli-service/lib/commands/build/resolveLibConfig.js
// Vue CLI源码,lib 时 输入、输出文件配置
rawConfig.output = Object.assign({
library: libName,
libraryExport: isVueEntry ? 'default' : undefined,
libraryTarget: format,
// preserve UDM header from webpack 3 until webpack provides either
// libraryTarget: 'esm' or target: 'universal'
// https://github.com/webpack/webpack/issues/6522
// https://github.com/webpack/webpack/issues/6525
globalObject: `(typeof self !== 'undefined' ? self : this)`
}, rawConfig.output, {
filename: `${entryName}.js`,
chunkFilename: `${entryName}.[name].js`,
// use dynamic publicPath so this can be deployed anywhere
// the actual path will be determined at runtime by checking
// document.currentScript.src.
publicPath: ''
})
# 2020/10/17 周六
# Google广告一个页面怎么显示多个广告,多个广告只显示的一个是什么原因?
在 Google Adsense 中,理论上配置好广告形式后,获取代码,把对应的代码放到页面中就可以显示广告了。但发现,如果放多个广告,只有一个可以显示出来,下面来看看是为什么?
官方提供的代码如下:
<script async src='https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'></script>
<ins class='adsbygoogle'
style='display:block'
data-ad-client='ca-pub-9527676606416000'
data-ad-slot='3653238000'
data-ad-format='auto'
data-full-width-responsive='true'></ins>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
一般在验证开通 Google Adsense 时,就会引入第一行 script 代码。那么只需要把下面的 ins 元素 和 script 里的代码拷贝进页面里就行。
当有多个广告时,我只拷贝了 多个 ins 到指定位置。 script 里面 (adsbygoogle = window.adsbygoogle || []).push({});
只放到了 body最后面的 script 里,这时只能显示一个广告。于是我看了下其他可以显示多个 google 广告的页面,打开源码后,发现最下面的 script 里的内容也要多次拷贝。也就是 如果页面上要放多个 google 广告,每次都需要引入 ins + script 两部分的代码
<!-- 广告 1 -->
<ins class='adsbygoogle' style="ins内容简写"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
<!-- 广告 2 -->
<ins class='adsbygoogle' style="ins内容简写"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
# 2020/10/11 周日
# DOMContentLoaded与Load时间具体指的是什么时间?
在 Chrome DevTools 官网 Network Reference (opens new window) 里是这样介绍的:
View load events
DevTools displays the timing of the DOMContentLoaded and load events in multiple places on the Network panel. The DOMContentLoaded event is colored blue, and the load event is red.
来看看 DOMContentLoaded
和 load
事件在 MDN 的解释
DOMContentLoaded事件:window 和 document 上都可以监听,意思一致
window.addEventListener('DOMContentLoaded', (event) => {
console.log('DOM fully loaded and parsed');
});
document.addEventListener('DOMContentLoaded', (event) => {
console.log('DOM fully loaded and parsed');
});
The DOMContentLoaded event fires when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.
DOMContentLoaded
事件:当 DOM (HTML document) 完成加载并解析,而不用等 css样式、图片和子 frame 完全加载完成时触发
同步 JS 会使 DOM 的解析暂停,如果希望用户在请求页面后尽快解析DOM,你可以把 JS 使用异步加载,并优化 css 样式加载方式。如果按照惯例加载,样式表和 JS 是并行加载的,会减慢 DOM 解析,窃取主html文档解析速度。下面是 head 中 script 默认加载以及加上 async, defer 参数的对比,一般把 script 放到 body 末尾,基本等价于 header 中 defer 的效果
load事件:window
window.addEventListener('load', (event) => {
console.log('page is fully loaded');
});
// 或
window.onload = (event) => {
console.log('page is fully loaded');
};
The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM has been loaded, without waiting for resources to finish loading.
当整个页面加载完成时(包括所有相关资源,例如css样式表和图片),这与 DOMContentLoaded
相反,它在页面DOM被加载后立即触发,而无需等待资源完成加载。
参考:
- Window: DOMContentLoaded event | MDN (opens new window)
- Document: DOMContentLoaded event (opens new window)
- 优化 css 加载方式 | Google developers (opens new window)
- Window: load event | MDN (opens new window)
# vue项目页面打开时间优化:从16秒到2秒内
Tag: vue-cli项目页面加载时间太长、npm run build 打包很大、vue vendor.js文件太大
在日常开发中,经常要写一些 demo 来测试一些功能,我专门弄了个 github 仓库来管理,方便沉淀积累。这次想着把 vue demo 部署到服务器,方便PC/手机实时看效果。于是把 vue-cli 项目 npm run build 后,将 dist 部署到服务器 nginx 下,但发现打开很慢,下面来看看
如下图,DOM加载完 15秒,完全加载 16秒
主要是 chunk-vendors.js 接近 1.7M,下载时间较长。npm run build 打包后 log 如下图(这是后面补的图,中间把路由懒加载改了下,size 会比上图里的小一点),超出建议的 244kB
理论上,nginx 开启 gzip 成功后,文件大小应该是 600多kb,看最上面的 Chrome Network 图里面,vendors 是 1.7M,且 Content-Encoding 那一栏没有 gzip,说明 js,css文件没有开启 gzip 咱们配置下 nginx 服务,开启 gzip
server
{
server_name www.zuoguoqing.com;
# 开启gzip
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_comp_level 2;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml image/jpeg image/gif image/png application/javascript;
location / {
# root html;
# index index.html index.htm;
proxy_pass http://127.0.0.1:3000;
}
}
咱们再来看看效果,如下图。vendors.js 文件从 1.7M 变为 600多k,且 Content-Type 那一栏也有了 gzip 标识。
下图可以看出 nginx 开启 gzip 压缩后,加载时间快了 5s。注意网上说的 compresion-webpack-plugin 插件在前端进行 gzip 在我看来基本是多此一举。nginx本身可以配置 gzip 功能,前端不用做 gzip 处理
11秒还是有点长,咱们想办法来减少 vendor.js 的体积看看,上面的图里面,gzip压缩后的 venders.js 有 613KB,加载也要 7秒多。
这里我们要使用 vue-cli(@vue/cli) 自带的 webpack 包体积优化工具,它可以查看各个模块的 size 大小,方便优化。只需要在 build 后面加上 --report 参数即可。我们把命令配置到 package.json 里,方便 npm run report 打包并生成 report,注意:网上很多说要先安装 webpack-bundle-analyzer 包,但不装也可以。
// package.json
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
// 加入下面一行
"report": "vue-cli-service build --report"
},
根据上面的配置后,运行 npm run report 后,会在 build 的同时,在 dist 目录会生成一个 report.html,打开后如下图,我们可以看到 ElementUI 和 Echarts 占用较大,直接打包了 node_modules 里面框架的内容。
这里我们可以把 Echarts 改为外部引用 cdn,不打包到主包 vendors.js 里,另外再把 Element改为懒加载,只加载使用到的部分模块
如果只把 Echarts 改为外部引入,ElementUI 整体引入,大概还有 1.4M 左右。Element 按需加载后就锐减了,提示大小就变为 596KB 了,如下图
对应的 report.html 图如下,可以看到
看完效果后,下面来看方法,怎么把 Echarts 外部引用,以及 Element 怎么按需加载。外部引入需要配置两个地方:
- 在 vue.config.js的configureWebpack.externals加入需要外部使用的包
- 在 public/index.html 里引入对应的包
下面是 Echarts 外部引用需要配置的地方
// vue.config.js
module.exports = {
configureWebpack: {
externals: {
echarts: "echarts",
}
}
};
<!-- public/index.html -->
<!-- 写在 head 最下面或 body 最下面 -->
<!-- echarts cdn -->
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/4.8.0/echarts-en.common.min.js"></script>
再来看看 Element 按需引入。注意 Element 按需引入,也需要两步
- 修改 main.js 里的 Element 引入方式,单个模块逐一引入
- 按需引入,依赖的是 babel-plugin-component,vue-cli 项目已经带了对应的功能,在 babel.config.js 加入配置即可(官网提示是在 .babelrc ,如果vue-cli 项目,有了 babel.config.js 就在该文件配置)。 参考: 按需引入 | ElementUI (opens new window)
// main.js
// Element 完整引入
// import ElementUI from "element-ui";
// Vue.use(ElementUI);
// Element 按需引入
import {
Input,
Button,
// ...
} from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.use(Input);
Vue.use(Button);
// ...
// babel.config.js
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"]
// element 按需引入
plugins: [
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk"
}
]
]
};
这样设置后,从原来的 1.8M 优化到了 596KB,再来看看加载时间,如下图。只需要 3.6s 了,整整快了 7~8s
如上图所示,echarts 外部引入的js使用了 cdn,且开启了 gzip,只有 168KB,加载时间仅 65ms,而我的 vendors.js gzip压缩后只有 122KB,下载时间较长,加载用了 2.23s,我部署的服务器是入门级的较慢,还是 cdn 快。另外 vendors.css 里面有 Element 的 css 文件,咱们也换成外部引入 cdn 试试
修改 main.js,不按需引入 Element。把 Element 的 css 也放到外部引入
// main.js
import ElementUI from "element-ui";
Vue.use(ElementUI);
// import "element-ui/lib/theme-chalk/index.css";
修改 vue.config.js,设置 vue、ElementUI 外部引用
module.exports = {
configureWebpack: {
externals: {
// 需要使用外部引入的包名:包名
echarts: "echarts",
vue: "Vue", // 注意 vue需要外部引入。放到 echarts前面,防止 console 报错
// element: "ElementUI" 可以打包成功,但chunk-vendors.js里面会打包element
"element-ui": "ELEMENT"
}
}
};
去掉 babel.config.js 里面 Element 按需引入代码,修改 public/index.html,直接head里面引入
<!-- pbulic/index.html -->
<head>
<!-- 引入Element css -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- echarts -->
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/4.8.0/echarts-en.common.min.js"></script>
<!-- 引入vue -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<!-- 引入Element js -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<head>
把 Element 和 vue 都改为外部引入后,再来看看,如下图,完全没警告了,低于 244KB,vendors.js 只有 68 KB
对应的 report.html 图如下,其实吧 vue-router、vuex 也使用 cdn 可能 vendor 会更小,但感觉不是很必要了。vendors.js 已经很小了
再来看看加载时间,还是要 3s 多 ??? 可以明显的看到 index.css 是 ElementUI 的 css,这个官方推荐的 unpkg cdn 有点慢,不是 gzip的压缩,是 br 的压缩方式。 32.7k 要 1.33s。(下图里面是 2.5s,其实整体是 3s 多,ElementUI的 JS 我已经换为了 bootcdn。之前的图压缩时不小心被覆盖了。)
这里我们再把 Element css 也替换为 bootcdn 连接,连接如下
<link href="https://cdn.bootcdn.net/ajax/libs/element-ui/2.9.2/theme-chalk/index.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/element-ui/2.9.2/index.js"></script>
替换后再来看看效果,整体 loaded 只要 1.89s 了。
上面的图,是取的均值。再次刷新可能会低于 1s,也可能会高于。我这里在测试时都勾选了 Disable cache。就是不使用缓存,有时候可能 dns 解析,https 验证时间、服务器响应时间会有差别。
# vue Cannot read property 'prototype' of undefined
由于 vue 项目 npm run build 打包时 ElementUI 体积较大,因此把他单独抽离出去。放到 public/index.html 里直接引入。但抽离出去后发现启动后控制台会报这样一个错误:Cannot read property 'prototype' of undefined。
网上查了下,是在 public/index.html 里引入 Element JS script 的前面,没有加入 Vue.js 的引入照成的。这里我们加入 Vue.js 的引入即可。
<!-- public/index.html -->
<!-- 引入element组件js前加入vue的引入 -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/element-ui/2.9.2/index.js"></script>
在 vue.config.js 里设置 ElementUI以及JS使用外部引入
module.exports = {
configureWebpack: {
externals: {
// 需要使用外部引入的包名:包名
vue: "Vue",
"element-ui": "ELEMENT"
}
}
};
# vue.config.js: "plugins" is not allowed
在vue.config.js中,webpack 相关的配置需要写到 configureWebpack 里,不能直接写到外面
// vue.config.js
module.exports = {
plugins: [], // error,"plugins" is not allowed
configureWebpack: {
plugins: [],
externals: {}
}
};
# vue中文本@功能实现
如果自己写一个@功能会比较麻烦,在 github 上找了一个现成的开源库:Tribute (opens new window) - ES6 Native @mentions,它是ES 原生的实现,社区有各种框架的实现。这里我们使用它的 vue 实现 vue-tribute (opens new window)。下图是实现效果
在线示例:https://www.zuoguoqing.com/at 对应的 vue 代码如下,需要注意的地方
- 我们引入 vue-tribute 组件,传入 options 即可
- 样式方面,需要写弹出选择框的样式,不然就没有样式
- options 的配置完全是 Tribute 的配置,到对应的 github 上查找即可,options的可选值支持动态渲染,支持从接口取
<template>
<div class="container">
<h3>contenteditable @mentions</h3>
<vue-tribute :options="options">
<div
class="content-editable"
contenteditable="true"
@input="valueChange"
placeholder="@..."
></div>
</vue-tribute>
<br />
<div>
<p>纯文本textContent:</p>
<p>{{ textContent }}</p>
</div>
<div>
<p>富文本innerHTML:</p>
<p>{{ innerHTML }}</p>
</div>
</div>
</template>
<script>
import VueTribute from "vue-tribute";
export default {
components: {
VueTribute
},
computed: {},
data() {
return {
textContent: "",
innerHTML: "",
options: {
trigger: "@",
// specify whether a space is required before the trigger string
requireLeadingSpace: false,
noMatchTemplate: "<li>暂无数据</li>",
values: [
{ key: "张三 zhangsan", value: "张三" },
{ key: "李四 lisi", value: "李四" },
{ key: "王五 wangwu", value: "王五" },
{ key: "周杰伦 zhoujielun", value: "周杰伦" }
],
positionMenu: true,
selectTemplate: function(item) {
return (
'<span contenteditable="false"><a>' +
"@" +
item.original.value +
"</a></span>"
);
}
}
};
},
methods: {
noMatchFound() {
console.log("暂无数据");
},
valueChange(e) {
console.log(e.target.innerHTML, e.target.textContent);
this.textContent = e.target.textContent;
this.innerHTML = e.target.innerHTML;
}
}
};
</script>
<style lang="less">
// Tribute-specific styles 略
</style>
完整代码参见:vue @功能实现demo | github (opens new window)
# 2020/10/06 周二
# Docker持续集成与自动化部署
最近看了下 Docker,整理了相关笔记,参考 Docker持续集成自动化部署 (opens new window)
# 2020/10/04 周日
# pm2 process.yml You cannot define a mapping item when in a sequence
使用 pm2 运行 node 项目,pm2 start process.yml
后提示 You cannot define a mapping item when in a sequence,是 process.yml 配置文件的问题,修改下配置文件即可
// cat process.yml
apps:
- script: app.js
instances: 2
watch: true
env:
NODE_ENV: production
将 - script: app.js
改为 script: app.js
重新运行就可以了
# 2020/10/01 周四
# Let’s Encrypt 免费HTTPS证书
Let’s Encrypt (opens new window) 是一个非盈利TLS(Transport Layer Security) 证书颁发机构(CA),免费提供 https 证书。
由于 Let’s Encrypt 证书的有效期为 3 个月,所以一般使用程序来自动续期更换证书。官方推荐使用 Certbot 来管理,它可以一站式申请、续期证书。
在 Certbot 官网 (opens new window) 选择部署服务器使用的软件及系统,会自动列出需要操作的步骤,如下图
注意文档里面分了两种:
- 单个域名,仅单个 https 比如
https://xx.com
或https://www.xx.com
- 通配符(Wildcard)证书,支持多个二级域名
https://*.xx.com
,但不支持https://xx.com
一般我们写好 nginx.conf 的配置,certbot 会根据 server_name 自动识别域名,并申请安装证书
这里把两个域名进行 https 处理,分别是 https:/www.zuoguoqing.com
和 https://api.zuoguoqing.com
需要写两个 nginx 配置文件,分别对应下面两个文件,如果还有其他二级域名可以再增加配置文件
- /etc/nginx/conf.d/docker.conf 这里是 www 二级域名
- /etc/nginx/conf.d/api.conf 这里是 api 二级域名的nginx配置
# /etc/nginx/conf.d/docker.conf
server
{
listen 80;
server_name www.zuoguoqing.com;
}
# /etc/nginx/conf.d/api.conf
server
{
listen 80;
server_name api.zuoguoqing.com;
}
开始安装 certbot,并执行
# 登录到 ubuntu linux 服务器
sudo apt update
sudo apt install snapd
sudo snap install core; sudo snap refresh core
sudo apt-get remove certbot
sudo dnf remove certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# 获取并安装证书
sudo certbot --nginx
# 测试续订
sudo certbot renew --dry-run
3个月有效期自动续订测试,提示了个 Python 3.8 OSError: [Errno 101] Network is unreachable
,但提示又是续订测试成功。只有后面再看是否有问题
来看看 certbot 自动修改的 nginx 配置,会有 managed by Certbot 注释,后面我又加了一些基本的重定向配置
/etc/nginx/conf.d/docker.conf
# /etc/nginx/conf.d/docker.conf
server
{
server_name www.zuoguoqing.com;
location / {
# root html;
# index index.html index.htm;
proxy_pass http://127.0.0.1:3000;
}
listen 443 ssl http2; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/www.zuoguoqing.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/www.zuoguoqing.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server
{
server_name zuoguoqing.com;
if ($host = zuoguoqing.com) {
return 301 https://www.$host$request_uri;
}
listen 443 ssl; # managed by Certbot
}
server
{
if ($host = zuoguoqing.com) {
return 301 https://www.$host$request_uri;
}
listen 80;
server_name zuoguoqing.com;
return 404;
}
server
{
if ($host = www.zuoguoqing.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name www.zuoguoqing.com;
return 404; # managed by Certbot
}
/etc/nginx/conf.d/api.conf
# /etc/nginx/conf.d/api.conf
server
{
server_name api.zuoguoqing.com;
location / {
# root html;
# index index.html index.htm;
proxy_pass http://127.0.0.1:8700;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/www.zuoguoqing.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/www.zuoguoqing.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
server_name api.zuoguoqing.com;
if ($host = 'api.zuoguoqing.com') {
return 301 https://$host$request_uri;
}
listen 80;
}
参考:
# HTTP/2,怎么确定网站是否开启了HTTP/2,HTTP/3?
HTTP/2是新一代的HTTP协议,于2015正式发布。相对 HTTP/1来说,大幅提升了网页性能,绝大多数浏览器都支持了HTTP/2。
http怎么开启http2呢?HTTP/2 现阶段必须使用https,80端口就不要想了。参考: 拥抱HTTP2.0时代,让网站飞起来 | 百度站长 (opens new window)
HTTP/1.1 的缺陷
- 连接无法复用,每次请求都经历三次握手和慢启动
- HTTP/1.0 传输数据时,每次都需要重新建立连接,增加延迟。
- HTTP/1.1 虽然加入 keep-alive 可以复用一部分连接,但域名分片等情况下仍然需要建立多个 connection,耗费资源,给服务器带来性能压力。
- 队头阻塞(Head-Of-Line Blocking,HOLB),并发请求数量限制
- 当页面中需要请求很多资源的时候,HOLB(队头阻塞)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。
- HTTP 1.0:下个请求必须在前一个请求返回后才能发出,request-response对按序发生。显然,如果某个请求长时间没有返回,那么接下来的请求就全部阻塞了。
- HTTP 1.1:尝试使用 pipeling 来解决,即浏览器可以一次性发出多个请求(同个域名,同一条 TCP 链接)。但 pipeling 要求返回是按序的,那么前一个请求如果很耗时(比如处理大图片),那么后面的请求即使服务器已经处理完,仍会等待前面的请求处理完才开始按序返回。所以,pipeling 只部分解决了 HOLB。
- 协议开销大,header 里携带的内容过大,在一定程度上增加了传输的成本,并且每次请求 header 基本不怎么变化,尤其在移动端增加用户流量。
- 安全因素,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份,这在一定程度上无法保证数据的安全性
HTTP/2 就是为了解决 HTTP/1 存在的问题而产生的
- 二进制传输,HTTP/1.1传输的是文本数据,而HTTP/2传输的是二进制数据,提高了数据传输效率。
- 多路复用,多个HTTP请求可以复用同一个TCP连接。解决了浏览器限制同一个域名下的请求数量的问题,减少额外的3次握手开销。
- 压缩请求头(Header),减少重复发送相同的请求头
- 支持服务器推送(Server push),允许在请求之前先响应数据到客户端(之前请求过的数据),可以加快css/js等资源加载速度
开启 HTTP/2 只需要在 listen 443 ssl 后面加上 http2 即可,可以使用 curl -I 进行测试看HTTP/2是否生效
# /etc/nginx/conf.d/docker.conf
server
{
server_name www.zuoguoqing.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
listen 443 ssl http2; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/www.zuoguoqing.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/www.zuoguoqing.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
来看看 HTTP/1.1 和 HTTP/2 的测试对比
curl -I www.zuo11.com
# HTTP/1.1 200 OK
# Server: nginx/1.16.1
# Date: Thu, 08 Oct 2020 09:08:55 GMT
# Content-Type: text/html; charset=utf-8
# Content-Length: 3666
# Last-Modified: Thu, 01 Oct 2020 15:02:42 GMT
# Connection: keep-alive
# Vary: Accept-Encoding
# ETag: "5f75ef92-e52"
# Accept-Ranges: bytes
curl -I https://www.zuoguoqing.com
# HTTP/2 200
# server: nginx/1.14.0 (Ubuntu)
# date: Thu, 08 Oct 2020 09:09:07 GMT
# content-type: text/html
# content-length: 213
# last-modified: Wed, 07 Oct 2020 09:03:03 GMT
# etag: "5f7d8447-d5"
# accept-ranges: bytes
HTTP/2够好了,为什么还会有 HTTP/3?
HTTP/2 的问题在于,其底层支撑协议为 TCP,在丢包的情况下,多个请求复用一个 TCP 连接时,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞了。这时 HTTP/2 效果可能还不如 HTTP/1
因此,Google 又弄了一个基于 UDP 协议的 QUIC 协议,是 HTTP/3中的底层支撑协议,又取了 TCP 中的精华,实现了即快又可靠的协议。
- 通过提高链接利用效率减少 RTT(Round Trip Time,通俗地说,就是通信一来一回的时间),提高数据交互速度。
- 在第一条的基础上,囊括安全需求。
- 解决当前实际网络环境中的适配问题
参考