基于canvas的图像预加载

在图片多的页面中,为了提高用户体验,我们会对图片进行压缩,采用更小的图片格式让图片尽可能快的展现给用户。在浏览medium.freecodecamp.com中一些文章时,偶然发现在网速不好的时候,会先看到一张模糊的图片,然后再展示出清晰的图。于是打开控制台抓了一下资源,发现展现出的第一张模糊的图片大小只有30*30。通过预加载一张小的图片,可以快速的展示给用户一些东西,减少中间的空白时间。看一看源码,发现是基于cancas来实现的。下面来看看它是怎么做的吧

首先我们有两张图
一张是原始图像:
demo
另一张是 30*30 的小图:
demo20

图片展示的 html 结构如下,原始图像我们先通过 visibility:hidden 隐藏起来。

1
2
3
4
5
<div class="img-container is-canvasLoaded" img-src="./demo.png">
<img src="./demo_min.png" id="imgdemo" alt="" style="opacity:0;position: absolute;z-index: -1;">
<canvas id="canvas-container"></canvas>
<img src="./demo2.png" alt="" id="origin-img" style="opacity:0">
</div>

初始时设置 canvas 的大小略大于我们的 demo_min.png 图片,如 75 * 75,然后:

  • 获取 demo_min.png 大小 60 * 30
  • 根据 demo_min.png 的比例,设置一下 canvas 的比例 75 * 37.5
1
2
3
4
5
6
7
8
9
10
11
var imgDemoSize = {
width: imgDemo.offsetWidth,
height: imgDemo.offsetHeight
}
var scale = imgDemoSize.width / imgDemoSize.height > canvasSize.width / canvasSize.height ? canvasSize.width / imgDemoSize.width : canvasSize.height / imgDemoSize.height
var scaleSize = {
width: imgDemoSize.width * scale,
height: imgDemoSize.height * scale,
}
canvasDom.width = scaleSize.width
canvasDom.height = scaleSize.height

现在html结构如下:

1
2
3
4
5
<div class="img-container is-canvasLoaded" img-src="./demo.png">
<img src="./demo_min.png" id="imgdemo" alt="" style="opacity:0;position: absolute;z-index: -1;">
<canvas id="canvas-container" width="75" height="37></canvas>
<img src="./demo2.png" alt="" id="origin-img" style="opacity:0">
</div>

将图像画在 canvas 上:

1
2
3
var context = canvasDom.getContext('2d')
// drawImage可以是HTMLImageElement
context.drawImage(imgDemo, 0,0,60,30,0,0,75,37)

canvas 的 drawImage 方法的第一个参数,允许是 HTMLImageElement 。所以这里直接用 img 元素比较方便。图像画在 canvas 上之后,我们就可以通过 canvas 的 getImageData 方法来获取图像的像素,然后对图像进行高斯模糊处理。模糊处理这里直接使用了 stackblur.js 。想了解高斯模糊原理的可以参考高斯模糊的算法

1
2
var imgData = context.getImageData(0,0,75,37)
var blurImgData = StackBlur.imageDataRGBA(imgData, 0,0, 75, 37, 5);

模糊处理完之后,putImageData 放在 canvas 中

1
context.putImageData(blurImgData, 0, 0);

然后展示 canvas , 到此页面上会展示一张模糊的图。

blur-result

模糊图片出来了,原始图像什么时候展示呢?
使用 Img 的 load 事件,原始的图像加载完成后触发 load 事件,将 canvas 隐藏,展示原始图像。

1
2
3
4
imgOrigin.addEventListener('load', function () {
imgOrigin.style.opacity = 1
canvasDom.style.opacity = 0
})

在网速略慢时,我们就会看到页面上会先出现一张模糊的图,然后再呈现出清晰的图。此外可以用 css 的 transition 属性,增加模糊图和原始图 opacity 过度时间,让图片之间的切换看起来更为自然顺畅。

阅读全部

webpack.DefinePlugin介绍

基本用法

这个插件用来定义全局变量,在webpack打包的时候会对这些变量做替换。
先看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//webpack.config.js
var webpack = require('webpack');
var path = require('path');
module.exports = {
entry: {
index: "./js/index.js"
},
output: {
path: "./dist/",
filename: "js/[name].js",
chunkFilename: "js/[name].js"
},
plugins: [
new webpack.DefinePlugin({
SOMETHINE: 'This is something we needed.'
})
]
};
//index.js
console.log(SOMETHINE);

编译完的结果如下:

1
2
3
function(module, exports, __webpack_require__) {
console.log((This Is The Test Text.));
}

可以看到代码中 SOMETHINE 被直接替换为 This is something we needed. 但是我们的本意中 SOMETHINE 是一个字符串,而直接替换后却不是一个字符串。怎么办呢?

方法一:可以将 SOMETHINE 的值写成

1
SOMETHINE: '"This is something we needed."'

方法二: 借助 JSON.tringify ,转为字符串

1
SOMETHINE: JSON.stringify('This is something we needed.')

推荐使用方法二,它不仅可以处理字符串,还可以处理Object中的字符串和Array。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//webpack.config.js
plugins: [
new webpack.DefinePlugin({
OBJ: JSON.stringify({"key1": "this is value"}),
OBJ2: {"key1": "this is value"},
OBJ3: {"key1": "'this is value'"},
ARRAY: JSON.stringify(["value1", "value2"]),
ARRAY2: ["value1", "value2"],
ARRAY3: ["'value1'", "'value2'"]
})
]
//index.js
console.log(OBJ);
console.log(OBJ2);
console.log(OBJ3);
console.log(ARRAY);
console.log(ARRAY2);
console.log(ARRAY3);

编译结果

1
2
3
4
5
6
console.log(({"key1":"this is value"})); // OBJ 正确
console.log(({"key1":this is value})); // OBJ2 this is value 被直接替换了,而非字符串
console.log(({"key1":'this is value'})); // OBJ3 正确
console.log((["value1","value2"])); // ARRAY 正确
console.log(({"0":value1,"1":value2})); // ARRAY2 直接写[]的形式,会被替换为object的类型,value1 和 value2 不是字符串
console.log(({"0":'value1',"1":'value2'})); // ARRAY3 正确

还剩下 Number 和 Boolean 两种变量类型,对于这两种类型,就不像上面介绍的这么麻烦了,直接写就行

1
2
3
4
5
6
7
8
9
10
11
12
//webpack.config.js
plugins: [
new webpack.DefinePlugin({
NUMBER: 12,
BOOL: true
})
]
//index.js
console.log(NUMBER);
console.log(BOOL);

编译结果

1
2
console.log((12));
console.log((true));
实际运用

介绍了这么多,在实际使用中, DefinePlugin 最为常用的用途就是用来处理我们开发环境和生产环境的不同。比如一些 debug 的功能在生产环境中需要关闭、开发环境中和生产环境中 api 地址的不同。以 vue-cli 生成的打包文件为例子,来看看在实际中的使用。

vue-cli 生成的目录中编译、打包相关的有两个文件夹 build 和 config 。

build 文件夹中找到 wbepack.dev.conf.js 和 webpack.prod.conf.js ,这两个文件中都通过 DefinePlugin 插件定义了 process.env 这个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.dev.conf.js
var config = require('../config')
...
new webpack.DefinePlugin({
'process.env': config.dev.env
})
// webpack.prod.conf.js
var config = require('../config')
var env = config.build.env
...
new webpack.DefinePlugin({
'process.env': env
})

config 中对 env 的定义分别放在 config/dev.env.js 和 config/prod.env.js 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//config/index.js
module.exports = {
build: {
env: require('./prod.env'),
...
},
dev: {
env: require('./dev.env'),
...
}
}
//config/prod.env.js
module.exports = {
NODE_ENV: '"production"'
}
//config/dev.env.js
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

到此可以看到,在 wbepack.dev.conf.js 中最终为

1
'process.env': {NODE_ENV: '"development"'}

在 webpack.prod.conf.js 中最终为

1
'process.env': {NODE_ENV: '"production"'}

在我们的代码中,如果部分只是在开发环境下才执行的逻辑,那么可以通过下面的方式

1
2
3
4
5
if ('development' === process.env.NODE_ENV) {
// 开发环境下的逻辑
} else {
// 生产环境下
}
阅读全部

flow的使用

facebook 推出的 js 静态类型检查工具。

flow可以在代码运行前对类型错误进行检查,包括:

  • 类型错误
  • 对null的引用
  • 以及可怕的 “undefined is not a function”
    flow 允许我们给变量添加类型

flow 的安装和使用

安装:
由于 flow 是用 OCaml 语言写的,npm 上只有对应的二进制包。

1
npm install --save-dev flow-bin

1.基本使用

安装完成后我们在要执行静态检查的文件跟目录下执行一下 flow init ,之后会发现多处一个.flowconfig文件,这个文件告诉 Flow 在这个目录下开始检测。此外 .flowconfig 文件可以进行一下更为高级的配置,比如仅包含一些目录、忽略一下目录等等(更深入的了解,请戳官网)。

对于需要使用 flow 进行类型检查的 js 文件,在开头加入 @flow 的注释

1
2
3
4
5
/* @flow */ 只要带有这个注释,都会进行类型检测
或者
/* @flow weak */ 只对有加类型注解的变量进行类型检测

例如:

1
2
3
4
5
6
7
8
9
10
11
12
/* @flow */
function multiple10 (num) {
return num * 10
}
multiple10('20')
function getLength (str) {
return str.length
}
getLength('3')
getLength([1,2,3])

接下来执行 flow check 看一下结果

1
2
3
4
3: return num * 10
^^^ string. The operand of an arithmetic operation must be a number.
Found 1 error

multiple10 函数中的类型转换被 flow 标记出。

2.类型注解

注意上面例子中 flow 的报错,只有 multiple10 中静态类型错误被检测中。对于 getLength 函数中参数 str 的类型是什么呢? 从函数本身来分析,只要包含 length 属性就都是合法的。 对于这种情况则可以为其添加“类型注解”,来明确的告诉 flow 这个值的类型。

1
2
3
4
5
function getLength (str: string) {
return str.length
}
getLength('3')
getLength([1,2,3])

再来运行一下 flow check , 这时结果会提示我们[1,2,3]这个参数类型不对。

1
2
3
4
12: getLength([1,2,3])
^^^^^^^ array literal. This type is incompatible with the expected param type of
8: function getLength (str: string) {
^^^^^^ string

3.自定义类型

很多时候,除了 number 、 string 这些基础类型外,我们还会有一些自定义的类型,比如:

1
2
3
4
var someData = {
id: 1,
text: '选项1'
}

这时候可以在一个单独的文件中将 someData 申明了一个自定义类型。方式如下:

1
2
3
4
5
/* /decls/data.js.flow */
declare type SomeData = {
id: number;
text: strin;
}

然后在 .flowconfig 文件中引入该申明文件

1
2
[libs]
decls/

flow server

在大型项目中,如果每修改完代码,就执行以下 flow check ,然后等待看结果,显然会被逼疯的。flow 为我们提供了一个 flow server ,支持在后台运行,并且只监测有修改的文件。方法很简单,只有一个命令

1
2
3
$> flow # 开启一个后台服务,输出首次检测结果
$> flow # 第二次使用flow,连接正在运行的后台服务,输出检测结果
$> flow stop # 关闭flow server

babel+flow

由于 flow 中类型注解的语法不属于 javascript 规范中的内容。所以在最终的代码中,我们需要移除flow的内容。flow 提供了 flow-remove-types 和 babel 插件两种方式,推荐使用 babel 插件来完成这项工作。

  • flow-remove-types
    这种方法比较简单粗暴: 安装 flow-remove-types,然后执行命令。

    1
    2
    $> npm install -g flow-remove-types
    $> flow-remove-types src/ --out-dir build/
  • babel插件
    安装 babel 插件

    1
    $> npm install babel-plugin-transform-flow-strip-types

babel 的 plugin 中加入该插件

1
2
3
4
5
{
"presets": ["es2015", "stage-2"],
"plugins": ["transform-runtime", "transform-flow-strip-types"],
"comments": false
}

注意:在 babel6 的 babel-preset-react 的插件中已经内置了 transform-flow-strip-types(https://flowtype.org/docs/syntax.html),如果使用了 babel-preset-react 那么无需再引入transform-flow-strip-types

eslint

eslint-plugin-flowtype 插件,可以让我们在 eslint 代码检查中加入 flow 的书写规范化检查。使用方式也很简单:

  1. 安装

    1
    $> npm install eslint-plugin-flowtype
  2. 在 .eslintrc.js 中设置 parser 为 babel-eslint

  3. plugin 中加入 flowtype
  4. eslint-plugin-flowtype 插件中默认提供了一份基于优秀实践总结出的 flow type 书写规范配置,在 .eslintrc 文件的 extend 中加入 “plugin:flowtype/recommended” 即可直接使用。
1
2
3
4
5
6
7
8
9
{
"extends": [
"standard",
"plugin:flowtype/recommended"
],
"plugins": [
"flowtype"
]
}

完成后我们来看看效果。下面的代码

1
2
3
4
function getLength (str:string) {
return str.length
}
getLength('3')

eslint 检查结果会抛出一个错误:类型注解的冒号后面丢失了空格。

1
2
3
4
5
6
✘ https://google.com/#q=flowtype%2Fspace-after-type-colon There must be a space after "str" parameter type annotation colon
/Users/didi/xiaoju/src/static-h5-src/750/driver/feedback/src/main.js:2:21
function getLength (str:string) {
^
✘ 1 problem (1 error, 0 warnings)

因为推荐的规范中:类型注解冒号后需要一个空格

1
2
3
4
5
6
7
8
"rules": {
...
"flowtype/space-after-type-colon": [
2,
"always"
],
...
}

flow type 规范配置如:函数返回类型是否必须、类型注解冒号前后的空格、自定义的type 的名称的命名方式等等,官方给出了很详细说明和例子。注:针对flow的的规范规则配置前添加“flowtype/”

配置名称 作用
boolean-style 类型注解中布尔值使用boolean还是bool
define-flow-type 将类型注解标记为已定义,no-undef的检查中不会出现报错
delimiter-dangle Object和Tuple类型定义中分隔符使用规范
generic-spacing 泛型对象的尖括号中类型前后的空格规范
space-before-generic-bracket 泛型对象的尖括号前的空格规范
no-dupe-keys object类型的定义中是否有重复的属性值
no-primitive-constructor-types 禁止使用原生的类型
no-weak-types 是否可以使用弱类型any、Object、Function
object-type-delimiter Object类型定义中,属性之前分割符为分号/逗号(注:该属性已被废弃,需要使用分号来分割)
require-parameter-type 函数的参数是否需要类型注解
require-return-type 函数返回值是否需要类型注解
require-valid-file-annotation 文件开头@flow的写法
require-variable-type 什么样的变量是需要类型注解
semi 使用type自定义类型语句结尾是否需要分号结尾
sort-keys Object类型定义中属性排列顺序
space-after-type-colon 类型注解分号后的空格规范
space-before-type-colon 类型注解分号前的空格规范
type-id-match 使用type自定义类型的名称规范
union-intersection-spacing union类型、intersection类型连接符号\ 、&之间的空格规范
use-flow-type 将通过declare定义的自定义类型标记为已经被使用过,no-unused-vars的检查中不会出现报错

#####名词说明

文中一些中文词直接从英文文档中翻译过来,可能有不准确的地方,这里给出原文,避免歧义。

  • 类型注解:原文为 type annotations ,标记变量的类型
  • 自定义类型:原文为 type aliases, 类似C语言中的typedef,可以为已有类型定义一个新的名称,或将一个复杂类型进行封装,如

    1
    2
    3
    4
    5
    type a = Array<String>
    type Person = {
    name: string,
    age: number
    };

#####相关链接
flow 官网(关于 flow 的各种用法官网给出了详细的例子):https://flowtype.org/

flow 简短教程:https://www.youtube.com/watch?v=xWMuAUbXcdQ

阅读全部

cookie从哪来到哪去

从哪来

浏览器添加cookie的时机:

  • 有客户端来设置:通过javascript的api——document.cookie来操作

    设置
    1
    document.cookie = 'name=value; maxAge=3000; path=/; domain=xx.com; secure'

    cookie的名和值中不能出现分号、逗号、等号和空格,每一个key之间通过分号和空格来分割。

    设置多个cookie

    通过document.cookie的方式来设置cookie每次只能设置一个cookie。如果我们写多个会有什么效果呢?

    1
    document.cookie = 'cookie1=value1; cookie2=value2'
放在console中运行一下,会发现,只有第一个cookie1设置成功,而cookie2被无视了。所以要设置多个cookie,最简单的方式就是多次调用document.cookie。
  • 由服务端来设置:http的响应头中。

    打开浏览器的控制台,在network下我们可以看到浏览器发出的所有请求。这些请求的返回头中,有些会发现一个Set-Cookie的字段。
    set-cookie
    服务器通过这个字段来告诉浏览器,它需要设置一个cookie,然后浏览器检查一下要设置的内容是否满足浏览器定下的cookie的“条条框框”,如果满足那么一个cookie就诞生了。这里提到了“条条框框”我们稍后做解答。

    在上图中,响应头里set-cookie字段可以有多个,每一个对应一个要设置的cookie,且只能对应一个。

到哪去

  • 客户端主动获取

    和客户端设置相同,获取的时候也是通过document.cookie来读取:

    1
    var cookies = document.cookie

    读取到的cookie为一串字符串,每个cookie之间通过分号来分割。

  • 随着请求发送

    浏览器发送请求时,在请求的头中会自动将“符合条件”的cookie带上。
    既然是请求头中携带的,那么我们通过ajax发送请求的时候,能否顺便设置一下cookie呢?简单的实验一下即可知晓:

    1
    2
    3
    4
    5
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.withCredentials = true;
    xhr.setRequestHeader('Cookie', "key=value");
    xhr.send(null);

    然后我们看看实际上浏览器发送出去的请求:

    我们设置的cookie并没有生效,并且chrome浏览器下我们会看到一行报错

    Refused to set unsafe header “Cookie”

    所以说,因为浏览器的安全限制,我们不能自己随便设置请求头中的cookie。

cookies条条框框

一个cookie除了value外,还有domain、path、expires/max-age、httpOnly、secure、sameSite这些属性。设置cookie的时候,可以对这些相关属性进行设置,当然也可以不进行设置,这时浏览器会自动给这些属性一个默认值。cookie的条条框框就和这些属性脱不了干系。

  • domain

    domain限制了cookie的使用范围:只能在domain值的范围中才能访问到该cookie.

    同时domain值的设置也有严格的要求。

    • 自身

      毫无疑问domain的值可以设置为本身。

    • 向下:所有子域名

      如果我们当前页面的域名是 sub.test.com, 那么 domain 可以设置为.sub.test.com。允许所有sub.test.com的子域名访问,如xx.sub.test.com、xx.xx.sub.test.com。

    • 向上:父级域名,直到Top-Level的下一级

      如果我们当前页面的域名是sub.test.com, 那么domain可以设置为test.com。但是不能设置为.com。因为.com属于Top-Level Domain

  • path

    domain+path 共同限制了可访问该cookie的URL。如果某个cookie的path=’/home’,那么只有“domain/home”下的所有url可以访问该cookie。

  • expires/max-age

    这两个值决定了cookie能活多久。如果不进行设置,那么浏览器会默认将cookie的有效期设置为 session,当页面关闭后cookie便随之被清理了。如果希望cookie在页面关闭后,仍然能保存一段时间,那么就需要为cookie设置一个过期时间,在过期时间内浏览器都会为我们保留该cookie.

    expires 是 http/1.0协议中的选项,在新的http/1.1协议中expires已经由 max-age 选项代替。expires必须是 GMT 格式的时间。

    max-age的单位为秒,cookie失效时刻 = 创建时刻 + max-age

  • httpOnly

    如果一个cookie被标记为httpOnly, 那么前文所提到的通过document.cookie的方式就无法获取到该cookie。同样的,我们通过js来设置cookie的,也无法被标记为httpOnly。也就说以下写法是不会生效的:

    1
    document.cookie="cookie1=value1; HttpOnly"

    这个值只能通过请求的响应头来设置。默认情况下,cookie不会带httpOnly选项。

  • Secure

    对于被标记为Secure的cookie,只有当请求是HTTPS或者其他安全协议时,该cookie 才能被访问到。同样的,也只有在HTTPS或者其他安全协议时,我们也才能通过js设置secure的cookie。

  • sameSite

    这个值这是谷歌开发的一种安全机制,用来定义cookie如何跨域发送,其目的是尝试阻止CSRF。chrome51版本已经支持。关于各大浏览器的支持情况,参考chrome官网。关于这个特性,这里不多做介绍了,感兴趣的可参看preventing-csrf-with-samesite-cookie-attributegoodbye-csrf-samesite-to-the-rescue

    最后,这些所有的属性值,一起决定了一件事——这个cookie那个URL可以能用。

参考文献:
https://developers.livechatinc.com/blog/setting-cookies-to-subdomains-in-javascript/
https://www.sjoerdlangkemper.nl/2016/04/14/preventing-csrf-with-samesite-cookie-attribute/

阅读全部

javascript中bind知多少

我们经常将apply, call和bind放在一起来对比他们的区别和用法。提到这三个,很容易想到他们可以改变函数中this的指向。对于bind方法,我们最常见的使用方式如下:

1
var otherFn = fn.bind(this);

其实,bind还有一个被我们忽略了的非常好用的特性——传参

bind参数

当绑定函数被调用时,这些参数加上绑定函数本身的参数会按照顺序作为原函数运行时的参数。

来个栗子:

1
2
3
4
5
6
7
var fn1 = function (arg1, arg2, arg3) {
console.log(arg1);
console.log(arg2);
console.log(arg3);
}
var fn2 = fn1.bind(null, 'first');
fn2('second', 'third'); // 输出为: first second third

为什么说好用

bind的参数可以和绑定函数本身的参数进行拼接,利用这一特性可以实现一些自定义的包装。看下面的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原始函数
var fn = function(customizer, a, b) {
var customResult = customizer(a, b); // 使用自定义方式对a,b,key进行处理
if (!customResult) {
// 默认的处理
customResult = a + b
}
return customResult;
}
var decoratoredFn = fn.bind(null, function(a, b) {
// 自定义的处理方法
return a*2 + b;
});
decoratoredFn(1, 2); // 结果为 4;

在原始的函数中,自定义的函数仅仅是一个参数,而具体的实现是在bind的时候确定的。这样,fn可以认为是一个通用的抽象的模板。额,有点拗口,让我们想象一下map方法:

1
2
3
4
var numbers = [1, 4, 9];
var roots = numbers.map(Math.sqrt);
// roots is now [1, 2, 3]
// numbers is still [1, 4, 9]

map方式实现了一个抽象的模板,将一个array转化为另一个array,然后输出。中间具体转化成什么map是不关心的,留给我们来自定义。用bind来搞一搞array的map呢?通用的逻辑:挨个对array中的每一个进行处理得到一个值,这些值组成一个新的array,然后最终输出。具体每一个怎么处理,留给bind了。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 原始函数
var fn = function(customizer, a) {
var resultArray = [];
for(var i=0, len=a.length; i < len; i++) {
resultArray.push(customizer(a[i]));
}
return resultArray;
}
var customizerFn = fn.bind(null, function(item) {
// 自定义的处理方法
return item*2;
});
decoratoredFn([1, 2]); // 结果为 [2, 4];

贴合点实际的栗子——单位转换

对于一维的单位转化,我们可以容易的得出一个通用的公式: 原单位*比例 + 起始偏差值 用代码描述一下:

1
2
3
4
function converter(toUnit, factor, offset, input) {
offset = offset || 0;
return [((offset+input)*factor).toFixed(2), toUnit].join(" ");
}

基于converter函数,借用bind的可以包装出各种单位转化的方法:

1
2
3
4
5
6
var milesToKm = converter.bind(undefined, 'km', 1.60936, 0);
var poundsToKg = converter.bind(undefined, 'kg', 0.45460, 0);
var farenheitToCelsius = converter.bind(undefined, 'degrees C',0.5556, -32);
milesToKm(10); // returns "16.09 km"
poundsToKg(2.5); // returns "1.14 kg"
farenheitToCelsius(98); // returns "36.67 degrees C"

该栗子来源戳这里

阅读全部

配合pointer-events实现1px边框

从乔帮主发布Retina设备开始兴起后,移动端的h5就多了一个烦人的1px。然后设计师们对1px格外青睐,设计稿(750尺寸)中各种1px分割线,1px边框。

对于下图中一系列的的1px边框通常的实现方式

1px系列边框

  1. 在最外层A进行transform:scale(0.5)

  2. 给每一个B元素加上transform:scale(0.5)

以上两种方式由于transform:scale(0.5)的影响,在写内部元素的大小时,都需要放大两倍,虽然只是*2,但是算起来还是略烦的。

下面介绍另外一种,pointer-events配合transform:scale(0.5)来实现1px边框的方式:

html:

1
<div class="b-div">

css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.b-div {
position: relative
/*
按照正常的布局计算方式来写
*/
}
/*单独的边框*/
.b-div::before {
content: '';
position: absolute;
display: block;
left: 0;
top: 0;
z-index: 10;
pointer-events: none;
box-sizing: border-box;
height: 200%;
width: 200%;
overflow: hidden;
border: 1px solid #fc9153;
transform: scale(0.5);
transform-origin: left top
}

上面通过伪类before来绘制1px边框。 在before上,将pointer-events的属性设置为none,边框元素虽然在b-div上层,但是不会有事件的响应,所有的事件会透到b-div上。如此一来,before伪类就仅仅是能”看到“而已

配合stylus的话,可以将1px边框的实现抽离出来,写成一个单独的mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
border-1px($bordercolor, $bordertopleftradius = 0px, $bordertoprightradius = 0px, $borderbottomleftradius = 0px, $borderbottomrightradius = 0px)
&::before
content: " "
position: absolute
display: block
left: 0
top: 0
z-index: 10
pointer-events: none
box-sizing: border-box
height: 200%
width: 200%
overflow: hidden
border: 1px solid #fc9153
border-top-left-radius: $bordertopleftradius
border-top-right-radius: $bordertoprightradius
border-bottom-left-radius: $borderbottomleftradius
border-bottom-right-radius: $borderbottomrightradius
transform: scale(0.5);
transform-origin: left top

剩下的就是在有1px边框的元素上,设置position,然后css中加入border-1px(),内部元素计算大小时不需要在*2了。

1
2
3
.b-div
position: relative
border-1px(#fc9153)
兼容性

pointer-event的兼容性还是比较好的, 具体参看caniuse

该方法的弊端
  • 采用rem布局时,计算出的div(下面称为b-div)和边框的div(下面称为1px-div)会出现小数,如果b-div设置了overflow:hidden,则在部分安卓机器下会出现右边或者下边边框线消失的问题。

    目前找到的解决方法是b-div不要设置overflow:hidden。

  • 由于1px-div和b-div是两个独立的div,所以如果元素带圆角的话,需要同时设置1px-div和b-div的圆角,并且要一致。

阅读全部

Webpack.merge插件

webpack-merge插件可以用来merge两个webpack的配置。它的底层引用了lodash中的merge,但针对webpack的特点,专门对loaders,preLoaders,postLoaders专门做了处理。

一般情况下,对于以下两个module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var module: {
loaders: [
{
test: /\.css$/,
loaders: ['style', 'css'],
},
]
}
var moduleExtend = {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel?stage=1',
include: path.join(ROOT_PATH, 'app'),
},
]
}

进行merge(module, moduleExtend)后会得到如下结果:

1
2
3
4
5
6
7
8
9
moduleMerged = {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel?stage=1',
include: path.join(ROOT_PATH, 'app'),
},
]
}

因为loaders是数组,所以结果中的css的loader信息丢失了。所以为了保证merge之后,loaders,preLoaders,postLoaders中的内容不会丢失,webpack-merge中对于loaders,preLoaders,postLoaders进行了特殊处理,采用concat对数组内容进行合并

1
2
3
4
if (isLoader(key)) {
return b.concat(a);
}
return a.concat(b);

如此,merge的结果为

1
2
3
4
5
6
7
8
9
10
11
loaders: [
{
test: /\.css$/,
loaders: ['style', 'css'],
},
{
test: /\.jsx?$/,
loader: 'babel?stage=1',
include: path.join(ROOT_PATH, 'app'),
}
]

但是如果遇到两个test值相同的loader时,仅仅用concat的话,会产生两条结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
merge({
loaders: [{
test: /\.js$/,
loader: 'babel'
}]
}, {
loaders: [{
test: /\.js$/,
loader: 'coffee'
}]
});
// 结果为
{
loaders: [{
test: /\.js$/,
loader: 'coffee'
},
{
test: /\.js$/,
loader: 'babel'
}]
}

这时对于js文件会先用babel-loader处理,然后再用coffee-loader处理。但是更多的,我们希望merge的结果是,后一个可以覆盖掉前一个。此外,在loaders中,除了可以写loader:’coffee’,还会有loaders:[‘xx’]的形式,如果一律采用concat的形式,显然过于粗暴了。所以,webpack-merge提供了更为聪明的的merge.smart函数,可以将test值相同的loader(preLoaders,postLoaders)进行合并

  • 对于loader,后面的会覆盖前面的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
merge.smart({
loaders: [{
test: /\.js$/,
loader: 'babel'
}]
}, {
loaders: [{
test: /\.js$/,
loader: 'coffee'
}]
});
// 结果为
{
loaders: [{
test: /\.js$/,
loader: 'coffee'
}]
}
  • loaders将会合并为一个去重后的数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
merge.smart({
loaders: [{
test: /\.js$/,
loaders: ['babel']
}]
}, {
loaders: [{
test: /\.js$/,
loaders: ['coffee']
}]
});
// 结果为
{
loaders: [{
test: /\.js$/,
// 由于webpack处理loadersz中的值是按照从右往左的顺序,所以'coffee'放在'babel'的前面
// 这种从右往左的方式,让我们可以构造loaders处理链
loaders: ['coffee', 'babel']
}]
}

注意:对于loaders中带参数的形式,如babel?plugins[]=object-assign,在smart merge时会认为和babel、babel?…为同一类,后一个会对前一个产生覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
merge.smart({
loaders: [{
test: /\.js$/,
loaders: ['babel?plugins[]=object-assign']
}]
}, {
loaders: [{
test: /\.js$/,
loaders: ['babel', 'coffee']
}]
});
// 结果为
{
loaders: [{
test: /\.js$/,
loaders: ['babel', 'coffee']
}]
}
  • 如果后一个loaders,前一个为loader,则merge时会将loader转化为loaders,然后进行merge
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
merge.smart({
loaders: [{
test: /\.js$/,
loader: 'babel'
}]
}, {
loaders: [{
test: /\.js$/,
loaders: ['coffee']
}]
});
// will become
{
loaders: [{
test: /\.js$/,
// 由于webpack处理loadersz中的值是按照从右往左的顺序,所以'coffee'放在'babel'的前面
loaders: ['coffee', 'babel']
}]
}
  • 如果后一个为loader,前一个为loaders,则merge的结果为loader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
merge.smart({
loaders: [{
test: /\.js$/,
loaders: ['babel']
}]
}, {
loaders: [{
test: /\.js$/,
loader: 'coffee'
}]
});
// will become
{
loaders: [{
test: /\.js$/,
loader: 'coffee'
}]
}

注意:include和exclude的配置同时也会影响loader是否会被合并。如果两个loaders有不同的include或者exclude值(后一个loaders不包含include或exclude除外),那么loaders最终不会进行合并,而是像merge中进行了concat。

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
merge.smart({
loaders: [{
test: /\.js$/,
include: '/dir1',
loader: 'babel'
}]
}, {
loaders: [{
test: /\.js$/,
include: '/dir2',
loader: 'coffee'
}]
});
//merge结果为
{
loaders: [{
test: /\.js$/,
include: '/dir2',
loader: 'coffee'
},
{
test: /\.js$/,
include: '/dir1',
loader: 'babel'
}]
}
阅读全部