该博客用于记录平时工作中遇到的一些小问题、小技巧以及一些最佳实践,不定期更新。
简单说说浏览器缓存
浏览器通过 HTTP 头部的字段来确定是否使用缓存,主要有两种方案:Expires 方案和 Cache-Control 方案,由于 Expires 方案隐患较大,所以推荐用 Cache-Control 方案。
Cache-Control方案 有一个 max-age 字段,这个字段告诉浏览器:
在 max-age 这个时间段内,如果再次请求资源,不需要向服务器发起请求,直接使用浏览器缓存即可;
超过 max-age 这个时间段的话,就需要通过 Last-Modified 和 Etag 这两个字段在服务器进行验证,以确定能否继续使用缓存;如果资源未发生变化,返回 304 告诉浏览器可以继续使用缓存。如果资源已经发生变化,则返回新资源,并更新浏览器缓存。
new String('Hello')
和 字符串直接量 Hello
的区别
两者的本质区别是:Hello
是基本类型;new String('Hello')
是String对象的实例。从具体表现来说
typeof 'Hello' === 'string'
typeof new String('Hello') === 'object'
由于new String('Hello')
是String对象的实例,所以它有String对象的所有原型方法和属性,这个没毛病。而 Hello
是基本类型,为什么它也可以使用String类的原型方法和属性呢?
// 以下两行代码都没有问题
(new String('Hello')).toLowerCase(); // hello
// 之所以基本类型也能正常运行,是由于:
// JS引擎遇到这种情况,会基于字符串直接量创建一个String对象
// 等到方法执行结束之后,会将这个String对象丢弃
'Hello'.toLowerCase(); // hello
switch 语句在比较时使用的是全等操作符
switch 语句在比较时使用的是全等操作符,所以并不会发生类型转换
// 由于是全等操作符,所以并不会发生类型转换
// 10 === '10' => false
function showCase(num) {
switch (num) {
case 10:
console.log('case 10');
break;
default:
console.log('case default');
break;
}
}
showCase('10'); // case default
如何量化网页加载速度?
我们常说网页加载速度快慢,但是否有一个比较量化的指标呢?这儿提供两个思路:
✦ 通过Chrome Devtools来直观的分析网页打开的速度,重点关注 DOMContentLoaded
事件和 onLoad
事件的时间,也需要关注各资源的timeline,但是,这并不能进行批量的分析,也不能统计出用户实际的数据。
✦ 通过 window.performance()
,我们可以获得很多指标,而且粒度更细,如果把这些指标会传到服务器,可以得到更细致,更准确的结果。
// 计算加载时间
function getPerformanceTiming() {
var performance = window.performance;
if (!performance) {
// 当前浏览器不支持
console.log('你的浏览器不支持 performance 接口');
return;
}
var t = performance.timing;
var times = {};
//【重要】页面加载完成的时间
//【原因】这几乎代表了用户等待页面可用的时间
times.loadPage = t.loadEventEnd - t.navigationStart;
//【重要】解析 DOM 树结构的时间
//【原因】反省下你的 DOM 树嵌套是不是太多了!
times.domReady = t.domComplete - t.responseEnd;
//【重要】重定向的时间
//【原因】拒绝重定向! 就不该写成
times.redirect = t.redirectEnd - t.redirectStart;
//【重要】DNS 查询时间
//【原因】DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
// 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5
times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
//【重要】读取页面第一个字节的时间
//【原因】这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
// TTFB 即 Time To First Byte 的意思
// 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
times.ttfb = t.responseStart - t.navigationStart;
//【重要】内容加载完成的时间
//【原因】页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
times.request = t.responseEnd - t.requestStart;
//【重要】执行 onload 回调函数的时间
//【原因】是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
times.loadEvent = t.loadEventEnd - t.loadEventStart;
// DNS 缓存时间
times.appcache = t.domainLookupStart - t.fetchStart;
// 卸载页面的时间
times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;
// TCP 建立连接完成握手的时间
times.connect = t.connectEnd - t.connectStart;
return times;
}
Array.sort
排序原理陷阱
Array.sort
用于对数组的元素进行排序,但它是按照字符编码的顺序进行排序的,而不是我们常规理论的顺序,举个例子。
[11, 22, 9, 4].sort();
// 11, 22, 4, 9
[11, 22, 9, 4].sort(function(a, b) {
return a >= b ? true : false;
});
// 4, 9, 11, 22
也就是说,要对数字进行大小排序,务必加一个比较函数。
font-family
最佳实践
不同平台对字体有不同的支持,为了能够在各个平台windows/Mac/Android/iOS
都有不错的表现,我们希望设置一份通用的font-family
font-family:
/* Helvetica改善版,增加了不同粗细和宽度的字形 */
"Helvetica Neue",
/* Mac专有,比Tahoma和Arial更漂亮,推荐 */
Helvetica,
/* 比Arial更清晰,建议写在Arial前面 */
Tahoma,
Arial,
/* 苹方,苹果推出的最新字体,效果优雅,只有最新系统才能支持(Mac 10.11/ iOS 9)*/
"PingFang SC",
/* 冬青黑体,Mac中间版本(Mac 10.6开始)支持的字体,很受欢迎,小字号时很清晰。*/
"Hiragino Sans GB",
/* 华文黑体,Mac早起版本(Mac 10.6之前)支持的字体,Chrome浏览器默认的字体,效果还不错 */
"Heiti SC",
/* 华文细黑,和华文黑体属于同一字体家族系列 */
STXihei,
/* 微软雅黑,windows支持的字体,显示效果不错 */
"Microsoft YaHei",
/* 黑体,作为一种fallback兼容 */
SimHei,
/* Linux下最佳简体中文字体 */
"WenQuanYi Micro Hei",
/* 当前面所有字体都找不到时,使用系统上存在的非衬线字体 */
sans-serif;
其他注意事项:
✦ 字体名称建议写成纯英文或者中英混合,以防部分用户设置了系统字体导致中文字体声明不生效。
✦ 绝大部分中文字体里都包含英文字符和数字,反之则不然,为了让英文字符和数字更好看,我们通常会单独设置英文字体,并放到最前面。
word-wrap 和 word-break的区别
单个英文单词默认不进行拆分换行,有时候我们想要是英文单词换行,最佳实践如下:
word-wrap: break-word;
word-break: break-all;
那么 word-wrap
和 word-break
有啥区别呢?一句话来说就是:word-wrap
决定了是否要对单词进行拆分换行,word-break
决定了以什么样的方式对单词进行拆分换行,如下图所示:
微信小程序开发环境和ShadowSocks等VPN冲突
小程序的坑,估计以后还有更多,先记录一下。
GET
net::ERR_NAME_NOT_RESOLVED
appservice报错截图
关掉ShadowSocks即可。
一个小技巧就是,打开微信web开发者工具之前关闭ShadowSocks,打开开发者工具之后,可以打开ShadowSocks。
input[type="range"]
选择器存在兼容性问题
由于IE9及其以下的浏览器不支持input[type="range"]
元素,会自动的将其转换为input[type="text"]
,这种情况下,使用input[type="range"]
选择器并不能获取到元素,慎用。
举一反三,类似的元素也会遇到同样的问题。
浏览器JS能读取MP3的封面图么?
function showTags(url) {
var tags = ID3.getAllTags(url);
console.log(tags);
document.getElementById('title').textContent = tags.title || "";
document.getElementById('artist').textContent = tags.artist || "";
document.getElementById('album').textContent = tags.album || "";
var image = tags.picture;
if (image) {
var base64String = "";
for (var i = 0; i < image.data.length; i++) {
base64String += String.fromCharCode(image.data[i]);
}
var base64 = "data:" + image.format + ";base64," +
window.btoa(base64String);
document.getElementById('picture').setAttribute('src', base64);
} else {
document.getElementById('picture').style.display = "none";
}
}
题外话:这个问题是在一个高级群里有人提出,一方面出于对提问者业余的提问技巧的“嫌弃”,另一方面下意识的觉得这个前端做不了,在没有严谨的查询的情况下就告知做不到。但后来这个查询就花了我不到1分钟的时间!最后啪啪啪打脸!打脸不重要,重要的是:要对技术保持谦卑!要对技术保持谦卑!要对技术保持谦卑!
gif 图会频繁的触发jank
gif 图会频繁的触发long times frames
,对性能有较大影响
简单粗暴的判断当前设备是否是Mobile
使用以下代码即可:
var regMobile = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i;
isMobile = regMobile.test(navigator.userAgent);
如果想要将ipad也是别为Mobile,添加ipad即可:
var regMobile = /Android|webOS|iPhone|iPod|iPad|BlackBerry|IEMobile|Opera Mini/i;
isMobile = regMobile.test(navigator.userAgent);
package.json 依赖版本号解读
latest
:安装最新版本。
指定版本
:比如1.2.2
,遵循“大版本.次要版本.小版本”的格式规定,安装时只安装指定版本。
波浪号+指定版本
:安装时不改变大版本号和次要版本号,比如~1.2.2
,表示安装1.2.x的最新版本(不低于1.2.2),但是不安装1.3.x。
插入号+指定版本
:安装时不改变大版本号,比如ˆ1.2.2
,表示安装1.x.x的最新版本(不低于1.2.2),但是不安装2.x.x。需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
如何原生实现hasClass功能
用正则匹配(/(^|\s)name(\s|$)/i).test(elNode.className)
即可,其中name表示需要判断的class名称,elNode表示需要判断的DOM节点。
注意正则中的\b
不适用于连字符命名,所以使用\s
来匹配,然后兼容一下开头结尾的情况。
如何获取设备像素密度?
使用window.devicePixelRatio可以获取到设备像素密度,也就是常说的N倍屏。
配合设备的物理宽度width,可以计算出设备的像素宽度。然后你就可以根据屏幕的像素宽度获取最适合屏幕的图片。
该属性在IE11才被支持,此外,window的屏幕像素密度不一定是整数,mac浏览器缩放之后,屏幕像素密度也不是整数。所以获取dpr一定要取整:dpr = Math.round(window.devicePixelRatio || 1);
。
多次调用RegExp.test()
/RegExp.exec()
,让你猝不及防!
一个正则regHomePage
,一个字符串pathname
,连续调用regHomePage.test(pathname)
,然后得到了两次完全不一样的结果,简单调试了一下,如下图:
调试过程,我“机智”的发现两次运行的lastIndex是不一样的,然后查了下lastIndex的相关资料,终于找到了原因:
由于我们在正则表达式中使用了g(全局匹配)
,这就导致RegExp.test()
/RegExp.exec()
会全局匹配,因此可能会多次匹配成功,返回多个结果,当匹配不到的时候,会自动将lastIndex置为0,而不是想当然的认为RegExp.test()
/RegExp.exec()
只运行一次。
虽然细想是情理之中的,但第一次遇见的时候还觉得有点蒙。
cssnano和base64 uri冲突,导致base64图片错误
Safari中img的width不能立即准确获得
Safari中,在html代码后面紧追一段js代码,获取图片的width失败,不是0就是1;
用clientWidth能够解决这个问题。
var img = document.getElementById('imageid'),
width, height;
width = img.clientWidth;
height = img.clientHeight;
统计字符长度,区分中英文
把双字节的替换成两个单字节的然后再获得长度
function getBLen(str) {
if (str == null) return 0;
if (typeof str != "string") {
str += "";
}
return str.replace(/[^\x00-\xff]/g, "01").length;
}
英文字符的charCode在[0, 128]之间
function getBLen(str) {
var realLength = 0,
len = str.length,
charCode = -1;
for (var i = 0; i < len; i++) {
charCode = str.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2;
}
}
return realLength;
}
arguments对象
arguments是函数的内部变量,表示函数的参数列表。
它是一个 类数组 对象,除了length,他没有Array特有的属性和方法。
// 将arguments对象转换成Array的常用方式
var args = [].slice.call(arguments);
// 尽管上面的代码可行,但是会阻碍JavaScript引起的优化,不是最优方案。
// 从JavaScript引擎优化的角度来说,arguments对象不应泄露或者传递出去
// 知道这个事情就行了,实际项目中仍然推荐这样使用。
// 应该通过遍历arguments对象的方式来构建一个新的数组
function doesntLeakArguments() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return args;
}
此外,arguments还可以方便的实现一些简单的功能,比如实现一个join函数。
function joinWords(sep) {
var args = [].slice.call(arguments, 1);
return args.join(sep);
}
// 返回 "red, orange, blue"
myConcat(", ", "red", "orange", "blue");
string 的 replace 方法
replace方法使用替换值(replacement)替换匹配模式(pattern),匹配模式可以是字符串或正则表达式,替换值可以是字符串或函数。
该方法并不改变字符串本身,而是返回替换后的字符串。
由于使用方式过于灵活,建议使用str.replace(regexp, function)
这种模式
// 替换所有的apples为oranges
var str = "Apples are round, and apples are juicy.";
var newstr = str.replace("/apples/ig", function(match, offset, origin) {
return 'oranges';
});
console.log(newstr);
// 交换姓名顺序
var str = "John Smith";
var newstr = str.replace(/(\w+)\s(\w+)/ig, function(match, p1, p2, offset, origin) {
return p2 + ' ' + p1;
});
console.log(newstr);
base64的使用场景
图片足够小,复用高,基本不会被更新,但用处特殊无法被制作成精灵图。
- background-image,背景图无法放到精灵图中,但是可以通过循环平铺一小块图片来设置成背景图。
- 比如一些小gif图,无法放到到精灵图中,发请求又太浪费。
UMD模块脚手架
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['b'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require('b'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.b);
}
}(this, function (b) {
//use b in some fashion.
// Just return a value to define the module export.
// This example returns an object, but the module
// can return a function as the exported value.
return {};
}));
怎么处理gulp里的异步任务
gulp核心概念是对stream做顺序处理,所以通过return stream的方式将stream传递到下一个任务
gulp.task('build-clean', function() {
// Return the Promise from del()
return del([BUILD_DIRECTORY]);
// ^^^^^^
// This is the key here, to make sure asynchronous tasks are done!
});
gulp.task('build-scripts', function() {
// Return the stream from gulp
return gulp.src(SCRIPTS_SRC).pipe(...)...
// ^^^^^^
// This is the key here, to make sure tasks run to completion!
});
但是对于异步任务,怎么处理呢?怎么确保顺序执行呢?
gulp.task('callback-example', function(callback) {
// Use the callback in the async function
fs.readFile('...', function(err, file) {
console.log(file);
callback();
// ^^^^^^^^^^
// This is what lets gulp know this task is complete!
});
});
对,核心就在于callback!
渐变
渐变是CSS3引入的特性,支持在两个或多个指定的颜色之间平稳的过度
主要有线性渐变(向下/向上/向左/向右/对角方向)和径向渐变(由它们的中心定义)
创建线性渐变,必须至少定义两种颜色节点,同时可以设置一个角度
background: linear-gradient(angle, color-stop1, color-stop2, ...);
Paste_Image.png
创建径向渐变,必须至少定义两种颜色节点,同时也可以指定渐变的中信,形状,大小
默认渐变的中心点,渐变的形状是ellipse,渐变的大小是farthest-corner(到最远的角落)
background: radial-gradient(center, shape size, start-color, ..., last-color);
页面内容区域根据浏览器高度自适应
body {
// 页头
{
margin-bottom: -60px;
}
// 内容区域
.page-container {
min-height: 100%;
padding-top: 60px;
padding-bottom: 156px;
.main-content {
width: 1000px;
margin: 0 auto;
}
}
// 页尾
{
margin-top: -156px;
}
}
opacity & rgba
opacity 和 rgba 中均设置的不透明度,即0表示全透明,1表示全不透明
glob 规则
-
*
匹配0个或多个除/
以外的字符 -
?
匹配1个除/
以外的字符 -
**
匹配0个或多个字符包括/
-
{}
应用多个规则,
分割 -
!
放到规则的开头,表示取反,即匹配不命中该规则的文件 -
[...]
匹配某个字符范围,可以用!
或者^
对该范围取反 -
!(pattern|pattern|pattern)
匹配任意pattern
,取反 -
?(pattern|pattern|pattern)
匹配0个或1个pattern
-
@(pattern|pattern|pattern)
仅匹配1个pattern
-
+(pattern|pattern|pattern)
匹配1个或多个pattern
-
*(pattern|pattern|pattern)
匹配0个或多个pattern
删除部分目录,保留部分目录
用del删除文件或目录时,下面几种代码差异明显,使用时需注意
// 会删除所有文件,不会保留layouts下面的erb文件,很疑惑
// 猜测是/**/*匹配了layouts目录,会删掉layouts目录,所以保留目录下的文件并不会生效【bug?】
return del([
config[project].dest + '/**/*',
'!' + config[project].dest + '/layouts/**/*.erb'
], { force: true });
// 会删除所有文件,保留layouts目录但不保留文件
return del([
config[project].dest + '/**/',
'!' + config[project].dest + '/layouts'
], { force: true });
// 会删除除layouts/users/api之外的所有目录,保留layouts/users/api目录及其目录下的文件
return del([
config[project].dest + '/*',
'!' + config[project].dest + '/layouts',
'!' + config[project].dest + '/users',
'!' + config[project].dest + '/api'
], { force: true });
JavaScript中的递归
递归的本质是调用自身,是一项很重要的编程技巧
比如求正整数N的阶乘
function factorial(num) {
if (num <= 1) {
return 1;
}
return num * factorial(num - 1);
}
// 这种方式有个隐患,一旦factorial在外层被修改,就会报错
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //报错
要解决函数可能被修改的问题,我们通常使用arguments.callee(已弃用)或者函数表达式来解决。
// 推荐使用的方式
var factorial = (function f(num) {
if (num <= 1) {
return 1;
}
return num * f(num - 1);
});
动画闪烁解决方案
backface-visibility: hidden;
perspective: 1000;