杨勇冠的博客

自律者自强


  • 首页

  • 关于

  • 归档

web开发安全的API

发表于 2018-02-28

以下是当你在设计, 测试以及发布你的 API 的时候所需要核对的重要安全措施


身份认证

  • 不要使用 Basic Auth 使用标准的认证协议 (如 JWT, OAuth).
  • 不要再造 Authentication, token generating, password storing 这些轮子, 使用标准的.
  • 在登录中使用 Max Retry 和自动封禁功能.
  • 加密所有的敏感数据.

JWT (JSON Web Token)

  • 使用随机复杂的密钥 (JWT Secret) 以增加暴力破解的难度.
  • 不要在请求体中直接提取数据, 要对数据进行加密 (HS256 或 RS256).
  • 使 token 的过期时间尽量的短 (TTL, RTTL).
  • 不要在 JWT 的请求体中存放敏感数据, 它是可破解的.

OAuth 授权或认证协议

  • 始终在后台验证 redirect_uri, 只允许白名单的 URL.
  • 每次交换令牌的时候不要加 token (不允许 response_type=token).
  • 使用 state 参数并填充随机的哈希数来防止跨站请求伪造(CSRF).
  • 对不同的应用分别定义默认的作用域和各自有效的作用域参数.

访问

  • 限制流量来防止 DDoS 攻击和暴力攻击.
  • 在服务端使用 HTTPS 协议来防止 MITM 攻击.
  • 使用 HSTS 协议防止 SSLStrip 攻击.

输入

  • 使用与操作相符的 HTTP 操作函数, GET (读取), POST (创建), PUT (替换/更新) 以及 DELETE (删除记录), 如果请求的方法不适用于请求的资源则返回 405 Method Not Allowed.
  • 在请求头中的 content-type 字段使用内容验证来只允许支持的格式 (如 application/xml, application/json 等等) 并在不满足条件的时候返回 406 Not Acceptable.
  • 验证 content-type 的发布数据和你收到的一样 (如 application/x-www-form-urlencoded, multipart/form-data, application/json 等等).
  • 验证用户输入来避免一些普通的易受攻击缺陷 (如 XSS, SQL-注入, 远程代码执行 等等).
  • 不要在 URL 中使用任何敏感的数据 (credentials, Passwords, security tokens, or API keys), 而是使用标准的认证请求头.
  • 使用一个 API Gateway 服务来启用缓存、访问速率限制 (如 Quota, Spike Arrest, Concurrent Rate Limit) 以及动态地部署 APIs resources.

处理

  • 检查是否所有的终端都在身份认证之后, 以避免被破坏了的认证体系.
  • 避免使用特有的资源 id. 使用 /me/orders 替代 /user/654321/orders
  • 使用 UUID 代替自增长的 id.
  • 如果需要解析 XML 文件, 确保实体解析(entity parsing)是关闭的以避免 XXE 攻击.
  • 如果需要解析 XML 文件, 确保实体扩展(entity expansion)是关闭的以避免通过指数实体扩展攻击实现的 Billion Laughs/XML bomb.
  • 在文件上传中使用 CDN.
  • 如果需要处理大量的数据, 使用 Workers 和 Queues 来快速响应, 从而避免 HTTP 阻塞.
  • 不要忘了把 DEBUG 模式关掉.

输出

  • 发送 X-Content-Type-Options: nosniff 头.
  • 发送 X-Frame-Options: deny 头.
  • 发送 Content-Security-Policy: default-src ‘none’ 头.
  • 删除指纹头 - X-Powered-By, Server, X-AspNet-Version 等等.
  • 在响应中强制使用 content-type, 如果你的类型是 application/json 那么你的 content-type 就是 application/json.
  • 不要返回敏感的数据, 如 credentials, Passwords, security tokens.
  • 在操作结束时返回恰当的状态码. (如 200 OK, 400 Bad Request, 401 Unauthorized, 405 Method Not Allowed 等等).

持续集成和持续部署

  • 使用单元测试和集成测试来审计你的设计和实现.
  • 引入代码审查流程, 不要自行批准更改.
  • 在推送到生产环境之前确保服务的所有组件都用杀毒软件静态地扫描过, 包括第三方库和其它依赖.
  • 为部署设计一个回滚方案.

使用postcss实现vw移动端适配

发表于 2018-01-30

有关于移动端的适配布局一直以来都是众说纷纭,对应的解决方案也是有很多种。在《使用Flexible实现手淘H5页面的终端适配》提出了Flexible的布局方案,
随着viewport单位越来越受到众多浏览器的支持,因此在《再聊移动端页面的适配》一文中提出了vw来做移动端的适配问题。
到目前为止不管是哪一种方案,都还存在一定的缺陷。在接触到大漠先生牵头开发的vw解决方案之前,我使用的是阿里的第一代适配解决方案 lib-flexible 在使用vw解决方案开发一套H5之后,我真正的被vw的威力所折服。
话不多说开工


使用vue-cli新建项目

1
2
3
vue init webpack vue-demo
cd vue-demo
npm run dev

安装依赖

1
npm i postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg postcss-cssnext postcss-viewport-units cssnano cssnano-preset-advanced --S

配置postcssrc.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
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
"postcss-aspect-ratio-mini": {},
"postcss-write-svg": { utf8: false },
"postcss-cssnext": {},
"postcss-px-to-viewport": {
viewportWidth: 750, // 视窗的宽度,对应的是我们设计稿的宽度,一般是750
viewportHeight: 1334, // 视窗的高度,根据750设备的宽度来指定,一般指定1334,也可以不配置
unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
viewportUnit: 'vw', // 转换成的视窗单位,建议使用vw
selectorBlackList: ['.ignore', '.hairlines'], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false // 是否允许在媒体查询中转换`px`
},
"postcss-viewport-units": {},
"cssnano": {
preset: "advanced",
autoprefixer: false,
"postcss-zindex": false
}
}
}

说明

  • 容器适配,可以使用vw
  • 文本的适配,可以使用vw
  • 大于1px的边框、圆角、阴影都可以使用vw
  • 内距和外距,可以使用vw

postcss-write-svg 实现Retina屏1像素边框

首先记得在heade头加入

1
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />

如果部分机型有问题,换插件降级处理

使用 Viewport Units Buggyfill 插件
在vue项目的index.html文件head标签添加引用

1
<script src="//g.alicdn.com/fdilab/lib3rd/viewport-units-buggyfill/0.6.2/??viewport-units-buggyfill.hacks.min.js,viewport-units-buggyfill.min.js"></script>

在Index.html文件body标签后添加以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
// vw兼容性处理viewport-units-buggyfill
window.onload = function () {
window.viewportUnitsBuggyfill.init({ hacks: window.viewportUnitsBuggyfillHacks });
//以下代码用户测试
// var winDPI = window.devicePixelRatio;
// var uAgent = window.navigator.userAgent;
// var screenHeight = window.screen.height;
// var screenWidth = window.screen.width;
// var winWidth = window.innerWidth;
// var winHeight = window.innerHeight;
// console.log("Windows DPI:" + winDPI + ";\ruAgent:" + uAgent + ";\rScreen Width:" +
// screenWidth + ";\rScreen Height:" + screenHeight + ";\rWindow Width:" + winWidth +
// ";\rWindow Height:" + winHeight)
}
</script>

最后做个对img兼容处理,在全局添加(在main.js 用 Import ‘@/common/index.css’)

1
2
3
4
<script>
img {
content: normal !important;
}

webpack优化打包速度之webpack.DllPlugin与webpack.DllReferencePlugin

发表于 2018-01-20

webpack是现在使用最广泛的打包工具,公司项目也一直使用。最近随着公司项目资源及文件慢慢庞大,打包速度越来越慢。因此上网搜索找到解决办法:
使用webpack插件webpack.DllPlugin与webpack.DllReferencePlugin将不需要改动的第三方插件与自己的业务代码进行分开打包,
首先:
在项目根目录新建一个文件webpack.dll.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const path    = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
vendor: ['vue-router','vuex','vue/dist/vue.common.js','vue/dist/vue.js','vue-loader/lib/component-normalizer.js','vue']
},
output: {
path: path.resolve(base.path),
filename: '[name].dll.js',
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
path: path.resolve('./dist', '[name]-manifest.json'),
name: '[name]_library'
})
]
}

将是用到的第三方插件添加到vendor中
然后:在webpack.config.js中添加代码

1
2
3
4
5
plugins: [
new webpack.DllReferencePlugin({
manifest: require('./dist/vendor-manifest.json')
})
]

还需要在入口html文件中引入vendor.dll.js

1
<script type="text/javascript" src="./../vendor.dll.js"></script>

然后在package.json文件中添加快捷命令(划线句子),指定执行文件:

1
2
3
"scripts": {
"build:dll": "webpack --config webpack.dll.config.js"
},

最后打包的时候首先执行npm run build:dll命令会在打包目录下生成
vendor-manifest.json文件与vendor.dll.js文件。
Dll这个概念应该是借鉴了Windows系统的dll。一个dll包,就是一个纯纯的依赖库,它本身不能运行,是用来给你的app引用的。
打包dll的时候,Webpack会将所有包含的库做一个索引,写在一个manifest文件中,而引用dll的代码(dll user)在打包的时候,只需要读取这个manifest文件,就可以了。
然后在执行npm run build
发现现在的webpack打包速度就快了很多。

按需加载babel-polyfill

发表于 2018-01-10

一直再使用vue项目,浏览器对js支持不一,我们在使用新的API在进行开发时不得不注意是否支持的兼容问题。babel-polyfill可以帮助我们解决很多问题。

1、认识babel-polyfill

Babel默认只转换JS语法,而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

举例来说,ES2015在Array对象上新增了Array.from方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill。(内部集成了core-js和regenerator)

1
npm install babel-polyfill --save

使用时,在所有代码运行之前增加require(‘babel-polyfill’)或者更常规的操作是在webpack.config.js中将babel-polyfill作为第一个entry。因此必须把babel-polyfill作为dependencies而不是devDependencies

注意: 在你的整个应用里只使用一次require(‘babel-polyfill’)。多次import或require(‘babel-polyfill’)会引起报错,因为它可能导致全局冲突和其他难以追踪的问题。 我们建议创建一个只包含require语句的单个入口文件。

2、使用babel-polyfill缺点

给予一下两个缺点,这就是我们进行优化的地方:
1、使用后打包后的体积很大,因为babel-polyfill是一个整体,把所有方法都加到原型链上。比如我们只使用了Array.from,但它把Object.defineProperty也给加上了,这就是一种浪费了。
2、babel-polyfill会污染全局变量,给很多类的原型链上都作了修改,如果我们开发的也是一个类库供其他开发者使用,这种情况就会变得非常不可控。

3、解决方法

分级上面两个问题,我们提供以下几种办法进行参考

解决方法1:单独引入

可以通过单独使用core-js的某个类库来解决,比如通过引入babel-runtime/core-js/promise来获取Promise
但是这样需要我们认为判断并且手动引入类库,太麻烦了。

解决方法2:使用babel-runtime和babel-plugin-transform-runtime

安装

1
2
npm install --save-dev babel-plugin-transform-runtime
npm install --save babel-runtime

然后在.babelrc中:

1
2
3
{
"plugins": ["transform-runtime"]
}

启用插件babel-plugin-transform-runtime后,Babel就会使用babel-runtime下的工具函数,将Promise重写成_Promise,然后引入_Promise helper函数。这样就避免了重复打包代码和手动引入模块的痛苦。

由于采用了沙盒(Sandbox)机制,不会污染全局变量,同时也不会去修改内建类的原型,带来的坏处是它不会polyfill原型上的扩展(例如 Array.prototype.includes()不会被polyfill,Array.from()则会被polyfill)

解决方法3:使用babel-preset-env

在Babel7中引入了babel-preset-env,根据你支持的环境自动决定适合你的Babel插件

1
npm install babel-preset-env --save-dev

在没有任何配置选项的情况下,babel-preset-env与babel-preset-latest(或者babel-preset-es2015,babel-preset-es2016和babel-preset-es2017一起)的行为完全相同。

下面例子包含了支持每个浏览器最后两个版本和safari大于等于7版本所需的polyfill和代码转换:

1
2
3
4
5
6
7
8
9
{
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}

如果你目标开发Node.js而不是浏览器应用的话,你可以配置babel-preset-env仅包含特定版本所需的polyfill和transform:

1
2
3
4
5
6
7
8
9
{
"presets": [
["env", {
"targets": {
"node": "6.10"
}
}]
]
}

按需加载babel-polyfill的关键是useBuiltIns选项,默认值为false,它的值有三种:

  1. false: 不对polyfills做任何操作
  2. entry: 根据target中浏览器版本的支持,将polyfills拆分引入,仅引入有浏览器不支持的polyfill
  3. usage(新):检测代码中ES6/7/8等的使用情况,仅仅加载代码中用到的polyfills

这个选项可以启用一个新的插件来替换语句import “babel-polyfill”或者require(“babel-polyfill”)以及基于浏览器环境的babel-polyfill个性化需求。

我们需要将选项值设为usage,然后它会在每个JS文件运行,分析根据每个文件用到的语言特性导入相关的polyfill,例如

1
2
import "core-js/modules/es6.promise";
var a = new Promise();

当然分析可能会有错误,例如:

1
2
import "core-js/modules/es7.array.includes";
a.includes // assume a is an []

babel-preset-env会假设a是数组,所以会导入相关的es7的includes方法

这样我们就真正实现了按需加载,会让我们打包后的代码大大减小。

一些神奇的js功效

发表于 2018-01-06

JavaScript是一款非常强大的脚步语言,它有一些不常用到但却非常神奇的语法,学会来既可以方便快捷实现某些功能,有可以成为高手(装逼),下面我通过几个例子来说明,并讲解其原理:

1、沉睡排序

在了解沉睡排序算法前,我们先看看十大经典排序算法
image 十大排序算法最终实现效果咱不说,都是为了解决问题同时提示效率。沉睡算法并不是一个高效的算法,相反反而很低效,它是一个特殊的算法,话不多说直接先上代码:

1
2
3
4
5
6
var numbers=[1,2,3,6,5,8,4,5,5,66,2,1];
numbers.forEach((num)=>{
setTimeout(()=>{
console.log(num)
},num)
})

这样console输出的结果是已经排序好的,神奇不神奇!

我们知道JavaScript是单线程队列处理机制,而利用使用setTimout指定时间,并不能保证到达指定时间后立即执行,只能保证到达指定时间把事件插入执行队列。
而根据浏览器更新策略 IE8及其之前的IE版本更新间隔为15.6毫秒,目前Chrome与IE9+浏览器的更新频率都为4ms(如果你使用的是笔记本电脑,并且在使用电池而非电源的模式下,为了节省资源,浏览器会将更新频率切换至于系统时间相同(16.7毫秒),也就意味着更新频率更低)。
根据上面的原理,假设浏览器器更新频率和计算器显示器更新频率(60HZ)相同,那么浏览器更新时间约为(16.7毫秒),所以如果 setTimeout(fu,0),实际并不是立刻插入执行队列,而是16.7毫秒之后进入执行队列

根据上面结论我们得出,如果setTimeout时间<=16.7毫秒,那么最好都会按照16.7毫秒来计算。

那么问题来了,回到上面“沉睡排序”算法,[1,2,3,6,5,8,4,5,5,66,2,1] 除了66其他都应该按照16.7毫秒来计算,那么小于16.6的是不应该具有排序效果,原理是这样的。但是事实上浏览器在一个更新队列(16.7ms)插入数据队列进行了优化排序算法,纠正了这种问题。也就是说,在小于16.7数值是的异常排序问题浏览器帮你解决了(开不开心,兴不兴奋)

2、快速去重 (ES6)

先上代码:

1
var arr = Array.from(new Set([1,2,3,4,4,3,5,6,7,8,8]));

这样得到的数组就是去重之后的数组,为什么会出现这种情况呢,下面让我来给你详细介绍一下:
说起Set就不得不说下Map,Map是通过 key:value 键值对进行存储,看到这里有不少一笑伙伴已经开始有异议了:JavaScript对象{}里面不也是key:value键值对吗?

这是因为JavaScript的对象有个小问题,就是键必须是字符串。但实际上Number,Array,Object或者其他数据类型作为键也是非常合理的。为了解决这个问题,最新的ES6规范引入了新的数据类型Map。(吐槽:啰嗦了一大堆)

Set和Map类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。通过这样的没有重复key实现了数组去重

3、单行写一个评级组件

1
"★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate);

我们看下运行结果:
image 看到这个运行结果,下次再用写评级就开心了! 这使用运用特殊字符文本,和slice函数的分段截取特性,不清楚slice怎么用的可以自行google一下

4、论如何优雅的取整

老规矩,先上代码:

1
2
3
4
5
var a = ~~2.33

var b= 2.33 | 0

var c= 2.33 >> 0

5、短路表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = b && 1
/ 相当于
if (b) {
a = 1
} else {
a = b
}

var a = b || 1
// 相当于
if (b) {
a = b
} else {
a = 1
}

vue高阶组件

发表于 2017-12-10

最近学习React ,特别喜欢React高阶组件,感觉好强大.突然想到自己熟悉Vue是否有高阶组件? 从网上扒拉一下,随找到答案,搬家到自己博客如下:


高阶组件(HOC)是 React 生态系统的常用词汇,React 中代码复用的主要方式就是使用高阶组件,并且这也是官方推荐的做法。而 Vue 中复用代码的主要方式是使用 mixins,并且在 Vue 中很少提到高阶组件的概念,这是因为在 Vue 中实现高阶组件并不像 React 中那样简单,原因在于 React 和 Vue 的设计思想不同,但并不是说在 Vue 中就不能使用高阶组件,只不过在 Vue 中使用高阶组件所带来的收益相对于 mixins 并没有质的变化。本篇文章主要从技术性的角度阐述 Vue 高阶组件的实现,且会从 React 与 Vue 两者的角度进行分析。


## 从 React 说起

起初 React 也是使用 mixins 来完成代码复用的,比如为了避免组件不必要的重复渲染我们可以在组件中混入 PureRenderMixin:

1
2
3
4
const PureRenderMixin = require('react-addons-pure-render-mixin')
const MyComponent = React.createClass({
mixins: [PureRenderMixin]
})


后来 React 抛弃了这种方式,进而使用 shallowCompare:
1
2
3
4
5
6
const shallowCompare = require('react-addons-shallow-compare')
const Button = React.createClass({
shouldComponentUpdate: function(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}
})



这需要你自己在组件中实现 shouldComponentUpdate 方法,只不过这个方法具体的工作由 shallowCompare 帮你完成,即浅比较。
再后来 React 为了避免开发者在组件中总是要写这样一段同样的代码,进而推荐使用 React.PureComponent,总之 React 在一步步的脱离 mixins,他们认为 mixins 在 React 生态系统中并不是一种好的模式(注意:并没有说 mixins 不好,仅仅针对 React 生态系统),观点如下:

1、mixins 带来了隐式依赖 2、mixins 与 mixins 之间,mixins 与组件之间容易导致命名冲突
* 3、由于 mixins 是侵入式的,它改变了原组件,所以修改 mixins 等于修改原组件,随着需求的增长 mixins 将变得复杂,导致滚雪球的复杂性。


具体大家可以查看这篇文章 Mixins Considered Harmful。不过 HOC 也并不是银弹,它自然带来了它的问题,有兴趣的同学可以查看这个视频:Michael Jackson - Never Write Another HoC,其观点是:使用普通组件配合 render prop 可以做任何 HOC 能做的事情。
本篇文章不会过多讨论 mixins 和 HOC 谁好谁坏,就像技术本身就没有好坏之分,只有适合不适合。难道 React 和 Vue 这俩哥们儿不也是这样吗🙂。
ok,我们回到高阶组件,所谓高阶组件其实就是高阶函数啦,React 和 Vue 都证明了一件事儿:一个函数就是一个组件。所以组件是函数这个命题成立了,那高阶组件很自然的就是高阶函数,即一个返回函数的函数,我们知道在 React 中写高阶组件就是在写高阶函数,很简单,那是不是在 Vue 中实现高阶组件也同样简单呢?其实 Vue 稍微复杂,甚至需要你对 Vue 足够了解,接下来就让我们一块在 Vue 中实现高阶组件,在文章的后面会分析为什么同样都是 函数就是组件 的思想,Vue 却不能像 React 那样轻松的实现高阶组件。
也正因如此所以我们有必要在实现 Vue 高阶组件之前充分了解 React 中的高阶组件,看下面的 React 代码:

1
2
3
4
5
6
7
8
9
10
function WithConsole (WrappedComponent) {
return class extends React.Component {
componentDidMount () {
console.log('with console: componentDidMount')
}
render () {
return <WrappedComponent {...this.props}/>
}
}
}


WithConsole 就是一个高阶组件,它有以下几个特点:

>#### 1、高阶组件(HOC)应该是无副作用的纯函数,且不应该修改原组件

可以看到 WithConsole 就是一个纯函数,它接收一个组件作为参数并返回了一个新的组件,在新组件的 render 函数中仅仅渲染了被包装的组件(WrappedComponent),并没有侵入式的修改它。

>#### 2、高阶组件(HOC)不关心你传递的数据(props)是什么,并且被包装组件(WrappedComponent)不关心数据来源

这是保证高阶组件与被包装组件能够完美配合的根本

>#### 3、高阶组件(HOC)接收到的 props 应该透传给被包装组件(WrappedComponent)

高阶组件完全可以添加、删除、修改 props,但是除此之外,要将其余 props 透传,否则在层级较深的嵌套关系中(这是高阶组件的常见问题)将造成 props 阻塞。
以上是 React 中高阶组件的基本约定,除此之外还要注意其他问题,如:高阶组件(HOC)不应该在 render 函数中创建;高阶组件(HOC)也需要复制组件中的静态方法;高阶组件(HOC)中的 ref 引用的是最外层的容器组件而不是被包装组件(WrappedComponent) 等等。

## Vue 中的高阶组件

了解了这些,接下来我们就可以开始着手实现 Vue 高阶组件了,为了让大家有一个直观的感受,我仍然会使用 React 与 Vue 进行对比的讲解。首先是一个基本的 Vue 组件,我们常称其为被包装组件(WrappedComponent),假设我们的组件叫做 BaseComponent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//base-component.vue

<template>
<div>
<span @click="handleClick">props: {{test}}</span>
</div>
</template>

<script>
export default {
name: 'BaseComponent',
props: {
test: Number
},
methods: {
handleClick () {
this.$emit('customize-click')
}
}
}
</script>



我们观察一个 Vue 组件主要观察三点:props、event 以及 slots。对于 BaseComponent 组件而言,它接收一个数字类型的 props 即 test,并发射一个自定义事件,事件的名称是:customize-click,没有 slots。我们会这样使用该组件:

1
<base-component @customize-click="handleCustClick" :test="100" />



现在我们需要 base-component 组件每次挂载完成的时候都打印一句话:I have already mounted,同时这也许是很多组件的需求,所以按照 mixins 的方式,我们可以这样做,首先定义个 mixins:

1
2
3
4
5
export default consoleMixin {
mounted () {
console.log('I have already mounted')
}
}



然后在 BaseComponent 组件中将 consoleMixin 混入:

1
2
3
4
5
6
7
8
9
10
11
12
export default {
name: 'BaseComponent',
props: {
test: Number
},
mixins: [ consoleMixin ]
methods: {
handleClick () {
this.$emit('customize-click')
}
}
}



这样使用 BaseComponent 组件的时候,每次挂载完成之后都会打印一句 I have already mounted,不过现在我们要使用高阶组件的方式实现同样的功能,回忆高阶组件的定义:接收一个组件作为参数,返回一个新的组件,那么此时我们需要思考的是,在 Vue 中组件是什么?有的同学可能会有疑问,难道不是函数吗?对,Vue 中组件是函数没有问题,不过那是最终结果,比如我们在单文件组件中的组件定义其实就是一个普通的选项对象,如下:

1
2
3
4
5
6
export default {
name: 'BaseComponent',
props: {...},
mixins: [...]
methods: {...}
}



这不就是一个纯对象吗?所以当我们从单文件中导入一个组件的时候:

1
2
import BaseComponent from './base-component.vue'
console.log(BaseComponent)



思考一下,这里的 BaseComponent 是什么?它是函数吗?不是,虽然单文件组件会被 vue-loader 处理,但处理后的结果,也就是我们这里的 BaseComponent 仍然还是一个普通的 JSON 对象,只不过当你把这个对象注册为组件(components 选项)之后,Vue 最终会以该对象为参数创建一个构造函数,该构造函数就是生产组件实例的构造函数,所以在 Vue 中组件确实是函数,只不过那是最终结果罢了,在这之前我们完全可以说在 Vue 中组件也可以是一个普通对象,就像单文件组件中所导出的对象一样。
基于此,我们知道在 Vue 中一个组件可以以纯对象的形式存在,所以 Vue 中的高阶组件可以这样定义:接收一个纯对象,并返回一个新的纯对象,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//hoc.js

export default function WithConsole (WrappedComponent) {
return {
template: '<wrapped v-on="$listeners" v-bind="$attrs"/>',
components: {
wrapped: WrappedComponent
},
mounted () {
console.log('I have already mounted')
}
}
}



WithConsole 就是一个高阶组件,它接收一个组件作为参数:WrappedComponent,并返回一个新的组件。在新的组件定义中,我们将 WrappedComponent 注册为 wrapped 组件,并在 template 中将其渲染出来,同时添加 mounted 钩子,打印 I have already mounted。
以上就完成了与 mixins 同样的功能,不过这一次我们采用的是高阶组件,所以是非侵入式的,我们没有修改原组件(WrappedComponent),而是在新组件中渲染了原组件,并且没有对原组件做任何修改。并且这里大家要注意 $listeners 和 $attrs:

1
'<wrapped v-on="$listeners" v-bind="$attrs"/>'



这么做是必须的,这就等价于在 React 中透传 props:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<WrappedComponent {...this.props}/>
```javascript
<div style="color:#b68a00;">
否则在使用高阶组件的时候,被包装组件(WrappedComponent)接收不到 props 和 事件。
那这样真的就完美解决问题了吗?不是的,首先 template 选项只有在完整版的 Vue 中可以使用,在运行时版本中是不能使用的,所以最起码我们应该使用渲染函数(render)替代模板(template),如下:
</div>
```javascript
//hoc.js

export default function WithConsole (WrappedComponent) {
return {
mounted () {
console.log('I have already mounted')
},
render (h) {
return h(WrappedComponent, {
on: this.$listeners,
attrs: this.$attrs,
})
}
}
}



上面的代码中,我们将模板改写成了渲染函数,看上去没什么问题,实则不然,上面的代码中 WrappedComponent 组件依然收不到 props,有的同学可能会问了,我们不是已经在 h 函数的第二个参数中将 attrs 传递过去了吗,怎么还收不到?当然收不到,attrs 指的是那些没有被声明为 props 的属性,所以在渲染函数中还需要添加 props 参数:

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

export default function WithConsole (WrappedComponent) {
return {
mounted () {
console.log('I have already mounted')
},
render (h) {
return h(WrappedComponent, {
on: this.$listeners,
attrs: this.$attrs,
props: this.$props
})
}
}
}



那这样是不是可以了呢?依然不行,因为 this.$props 始终是空对象,这是因为这里的 this.$props 指的是高阶组件接收到的 props,而高阶组件没有声明任何 props,所以 this.$props 自然是空对象啦,那怎么办呢?很简单只需要将高阶组件的 props 设置与被包装组件的 props 相同即可了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//hoc.js

export default function WithConsole (WrappedComponent) {
return {
mounted () {
console.log('I have already mounted')
},
props: WrappedComponent.props,
render (h) {
return h(WrappedComponent, {
on: this.$listeners,
attrs: this.$attrs,
props: this.$props
})
}
}
}



现在才是一个稍微完整可用的高阶组件。大家注意用词:稍微,纳尼?都修改成这样了还不行吗?当然,上面的高阶组件能完成以下工作:

> 1、透传 props
> 2、透传没有被声明为 props 的属性
> 3、透传事件

大家不觉得缺少点儿什么吗?我们前面说过,一个 Vue 组件的三个重要因素:props、事件 以及 slots,前两个都搞定了,但 slots 还不行。我们修改 BaseComponent 组件为其添加一个具名插槽和默认插槽,如下:

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
//base-component.vue

<template>
<div>
<span @click="handleClick">props: {{test}}</span>
<slot name="slot1"/> <!-- 具名插槽 -->
<p>===========</p>
<slot/> <!-- 默认插槽 -->
</div>
</template>

<script>
export default {
...
}
</script>
<div style="color:#b68a00;">
然后我们写下如下测试代码:
</div>
<template>
<div>
<base-component>
<h2 slot="slot1">BaseComponent slot</h2>
<p>default slot</p>
</base-component>
<enhanced-com>
<h2 slot="slot1">EnhancedComponent slot</h2>
<p>default slot</p>
</enhanced-com>
</div>
</template>

<script>
import BaseComponent from './base-component.vue'
import hoc from './hoc.js'

const EnhancedCom = hoc(BaseComponent)

export default {
components: {
BaseComponent,
EnhancedCom
}
}
</script>


渲染结果如下:
image

上图中蓝色框是 BaseComponent 组件渲染的内容,是正常的。红色框是高阶组件渲染的内容,可以发现无论是具名插槽还是默认插槽全部丢失。其原因很简单,就是因为我们在高阶组件中没有将分发的插槽内容透传给被包装组件(WrappedComponent),所以我们尝试着修改高阶组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
hoc.js

function WithConsole (WrappedComponent) {
return {
mounted () {
console.log('I have already mounted')
},
props: WrappedComponent.props,
render (h) {

// 将 this.$slots 格式化为数组,因为 h 函数第三个参数是子节点,是一个数组
const slots = Object.keys(this.$slots)
.reduce((arr, key) => arr.concat(this.$slots[key]), [])

return h(WrappedComponent, {
on: this.$listeners,
attrs: this.$attrs,
props: this.$props
}, slots) // 将 slots 作为 h 函数的第三个参数
}
}
}



好啦,大功告成刷新页面,如下:

image

纳尼😱?我们发现,分发的内容确实是渲染出来了,不过貌似顺序不太对。。。。。。蓝色框是正常的,在具名插槽与默认插槽的中间是有分界线(===========)的,而红色框中所有的插槽全部渲染到了分界线(===========)的下面,看上去貌似具名插槽也被作为默认插槽处理了。这到底是怎么回事呢?
想弄清楚这个问题,就回到了文章开始时我提到的一点,即你需要对 Vue 的实现原理有所了解才行,否则无解。接下来就从原理触发讲解如何解决这个问题。这个问题的根源在于:Vue 在处理具名插槽的时候会考虑作用域的因素。不明白没关系,我们一点点分析。
首先补充一个提示:Vue 会把模板(template)编译成渲染函数(render),比如如下模板:

1
2
3
<div>
<h2 slot="slot1">BaseComponent slot</h2>
</div>



会被编译成如下渲染函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", [
_c("h2", {
attrs: { slot: "slot1" },
slot: "slot1"
}, [
_vm._v("BaseComponent slot")
])
])
}



想要查看一个组件的模板被编译后的渲染函数很简单,只需要在访问 this.$options.render 即可。观察上面的渲染函数我们发现普通的 DOM 是通过 _c 函数创建对应的 VNode 的。现在我们修改模板,模板中除了有普通 DOM 之外,还有组件,如下:

1
2
3
4
5
6
<div>
<base-component>
<h2 slot="slot1">BaseComponent slot</h2>
<p>default slot</p>
</base-component>
</div>



那么生成的渲染函数(render)是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
[
_c("base-component", [
_c("h2", { attrs: { slot: "slot1" }, slot: "slot1" }, [
_vm._v("BaseComponent slot")
]),
_vm._v(" "),
_c("p", [_vm._v("default slot")])
])
],
1
)
}



我们发现无论是普通DOM还是组件,都是通过 _c 函数创建其对应的 VNode 的。其实 _c 在 Vue 内部就是 createElement 函数。createElement 函数会自动检测第一个参数是不是普通DOM标签,如果不是普通DOM标签那么 createElement 会将其视为组件,并且创建组件实例,注意组件实例是这个时候才创建的。但是创建组件实例的过程中就面临一个问题:组件需要知道父级模板中是否传递了 slot 以及传递了多少,传递的是具名的还是不具名的等等。那么子组件如何才能得知这些信息呢?很简单,假如组件的模板如下:

1
2
3
4
5
6
<div>
<base-component>
<h2 slot="slot1">BaseComponent slot</h2>
<p>default slot</p>
</base-component>
</div>



父组件的模板最终会生成父组件对应的 VNode,所以以上模板对应的 VNode 全部由父组件所有,那么在创建子组件实例的时候能否通过获取父组件的 VNode 进而拿到 slot 的内容呢?即通过父组件将下面这段模板对应的 VNode 拿到:

1
2
3
4
<base-component>
<h2 slot="slot1">BaseComponent slot</h2>
<p>default slot</p>
</base-component>



如果能够通过父级拿到这段模板对应的 VNode,那么子组件就知道要渲染哪些 slot 了,其实 Vue 内部就是这么干的,实际上你可以通过访问子组件的 this.$vnode 来获取这段模板对应的 VNode:

image


其中 this.$vnode 并没有写进 Vue 的官方文档。子组件拿到了需要渲染的 slot 之后进入到了关键的一步,这一步就是导致高阶组件中透传 slot 给 BaseComponent 却无法正确渲染的原因,看下图:

image

这张图与上一张图相同,在子组件中打印 this.$vnode,标注中的 context 引用着 VNode 被创建时所在的组件实例,由于 this.$vnode 中引用的 VNode 对象是在父组件中被创建的,所以 this.$vnode 中的 context 引用着父实例。理论上图中标注的两个 context 应该是相等的:

1
console.log(this.$vnode.context === this.$vnode.componentOptions.children[0].context) // true



而 Vue 内部做了一件很重要的事儿,即上面那个表达式必须成立,才能够正确处理具名 slot,否则即使 slot 具名也不会被考虑,而是被作为默认插槽。这就是高阶组件中不能正确渲染 slot 的原因。
那么为什么高阶组件中上面的表达式就不成立了呢?那是因为由于高阶组件的引入,在原本的父组件与子组件之间插入了一个组件(也就是高阶组件),这导致在子组件中访问的 this.$vnode 已经不是原来的父组件中的 VNode 片段了,而是高阶组件的 VNode 片段,所以此时 this.$vnode.context 引用的是高阶组件,但是我们却将 slot 透传,slot 中的 VNode 的 context 引用的还是原来的父组件实例,所以这就造成了以下表达式为假:

1
console.log(this.$vnode.context === this.$vnode.componentOptions.children[0].context) // false



最终导致具名插槽被作为默认插槽,从而渲染不正确。
而解决办法也很简单,只需要手动设置一下 slot 中 VNode 的 context 值为高阶组件实例即可,修改高阶组件如下:

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
//hoc.js

function WithConsole (WrappedComponent) {
return {
mounted () {
console.log('I have already mounted')
},
props: WrappedComponent.props,
render (h) {
const slots = Object.keys(this.$slots)
.reduce((arr, key) => arr.concat(this.$slots[key]), [])
// 手动更正 context
.map(vnode => {
vnode.context = this._self
return vnode
})

return h(WrappedComponent, {
on: this.$listeners,
props: this.$props,
attrs: this.$attrs
}, slots)
}
}
}



现在,都能够正常渲染啦,如下图:

image

这里的关键点除了你需要了解 Vue 处理 slot 的方式之外,你还要知道通过当前实例 _self 属性访问当实例本身,而不是直接使用 this,因为 this 是一个代理对象。
现在貌似看上去没什么问题了,不过我们还忘记了一件事儿,即 scopedSlots,不过 scopedSlots 与 slot 的实现机制不一样,本质上 scopedSlots 就是一个接收数据作为参数并渲染 VNode 的函数,所以不存在 context 的概念,所以直接透传即可:

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
//hoc.js

function WithConsole (WrappedComponent) {
return {
mounted () {
console.log('I have already mounted')
},
props: WrappedComponent.props,
render (h) {
const slots = Object.keys(this.$slots)
.reduce((arr, key) => arr.concat(this.$slots[key]), [])
.map(vnode => {
vnode.context = this._self
return vnode
})

return h(WrappedComponent, {
on: this.$listeners,
props: this.$props,
// 透传 scopedSlots
scopedSlots: this.$scopedSlots,
attrs: this.$attrs
}, slots)
}
}
}



到现在为止,一个高阶组件应该具备的基本功能算是实现了,但这仅仅是个开始,要实现一个完整健壮的 Vue 高阶组件,还要考虑很多内容,比如:

函数式组件中要使用 render 函数的第二个参数代替 this。
以上我们只讨论了以纯对象形式存在的 Vue 组件,然而除了纯对象外还可以函数。
创建 render 函数的很多步骤都可以进行封装。
处理更多高阶函数组件本身的选项(而不仅仅是上面例子中的一个简单的生命周期钩子)


我觉得需要放上两个关于高阶组件的参考链接,供参考交流:

Discussion: Best way to create a HOC
https://github.com/jackmellis/vue-hoc

为什么在 Vue 中实现高阶组件比较难


前面说过要分析一下为什么在 Vue 中实现高阶组件比较复杂而 React 比较简单。这主要是二者的设计思想和设计目标不同,在 React 中写组件就是在写函数,函数拥有的功能组件都有。而 Vue 更像是高度封装的函数,在更高的层面 Vue 能够让你轻松的完成一些事情,但与高度的封装相对的就是损失一定的灵活,你需要按照一定规则才能使系统更好的运行。
有句话说的好:
会了不难,难了不会
复杂还是简单都是相对而言的,最后希望大家玩的转 Vue 也欣赏的了 React。放上两张我比较认同的图片供各位看官讨论:

image

参考

探索Vue高阶组件

React-Native的FlexBox布局详解

发表于 2017-11-10

本文出自《React Native学习笔记》系列文章。

一款好的APP离不了一个漂亮的布局,本文章将向大家分享React Native中的布局方式FlexBox。
在React Native中布局采用的是FleBox(弹性框)进行布局。

FlexBox提供了在不同尺寸设备上都能保持一致的布局方式。FlexBox是CSS3弹性框布局规范,目前还处于最终征求意见稿 (Last Call Working Draft)阶段,并不是所有的浏览器都支持Flexbox。但大家在做React Native开发时大可不必担心FlexBox的兼容性问题,因为既然React Native选择用FlexBox布局,那么React Native对FlexBox的支持自然会做的很好。

宽和高

在学习FlexBox之前首先要清楚一个概念“宽和高”。一个组件的高度和宽度决定了它在屏幕上的尺寸,也就是大小。

像素无关

在React Native中尺寸是没有单位的,它代表了设备独立像素。

1
2
3
<View style={ {width:100,height:100,margin:40,backgroundColor:'gray'}}>
<Text style={ {fontSize:16,margin:20}}>尺寸</Text>
</View>

上述代码,运行在Android上时,View的长和宽被解释成:100dp 100dp单位是dp,字体被解释成16sp 单位是sp,运行在iOS上时尺寸单位被解释称了pt,这些单位确保了布局在任何不同dpi的手机屏幕上显示不会发生改变;

和而不同

值得一提的是,React Native中的FlexBox 和Web CSSS上FlexBox工作方式是一样的。但有些地方还是有些出入的,如:

React Native中的FlexBox 和Web CSSS上FlexBox的不同之处

  • flexDirection: React Native中默认为flexDirection:'column',在Web CSS中默认为flex-direction:'row'
  • alignItems: React Native中默认为alignItems:'stretch',在Web CSS中默认align-items:'flex-start'
  • flex: 相比Web CSS的flex接受多参数,如:flex: 2 2 10%;,但在 React Native中flex只接受一个参数
  • 不支持属性:align-content,flex-basis,order,flex-basis,flex-flow,flex-grow,flex-shrink

以上是React Native中的FlexBox 和Web CSSS上FlexBox的不同之处,记住这几点,你可以像在Web CSSS上使用FlexBox一样,在React Native中使用FlexBox。

Layout Props

Flex in React Native

以下属性是React Native所支持的Flex属性。

父视图属性(容器属性):

  • flexDirection enum(‘row’, ‘column’,’row-reverse’,’column-reverse’)
  • flexWrap enum(‘wrap’, ‘nowrap’)
  • justifyContent enum(‘flex-start’, ‘flex-end’, ‘center’, ‘space-between’, ‘space-around’)
  • alignItems enum(‘flex-start’, ‘flex-end’, ‘center’, ‘stretch’)

主轴和侧轴(横轴和竖轴)

在学习上述属性之前,让我们先了解一个概念:主轴和侧轴
主轴和侧轴
主轴即水平方向的轴线,可以理解成横轴,侧轴垂直于主轴,可以理解为竖轴。

flexDirection

flexDirection enum('row', 'column','row-reverse','column-reverse')
flexDirection属性定义了父视图中的子元素沿横轴或侧轴方片的排列方式。

  • row: 从左向右依次排列
  • row-reverse: 从右向左依次排列
  • column(default): 默认的排列方式,从上向下排列
  • column-reverse: 从下向上排列

Usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<View style={ {flexDirection:'row-reverse',backgroundColor:"darkgray",marginTop:20}}>
<View style={ {width:40,height:40,backgroundColor:"darkcyan",margin:5}}>
<Text style={ {fontSize:16}}>1</Text>
</View>
<View style={ {width:40,height:40,backgroundColor:"darkcyan",margin:5}}>
<Text style={ {fontSize:16}}>2</Text>
</View>
<View style={ {width:40,height:40,backgroundColor:"darkcyan",margin:5}}>
<Text style={ {fontSize:16}}>3</Text>
</View>
<View style={ {width:40,height:40,backgroundColor:"darkcyan",margin:5}}>
<Text style={ {fontSize:16}}>4</Text>
</View>
</View>

flexDirection

flexWrap

flexWrap enum('wrap', 'nowrap')
flexWrap属性定义了子元素在父视图内是否允许多行排列,默认为nowrap。

  • nowrap flex的元素只排列在一行上,可能导致溢出。
  • wrap flex的元素在一行排列不下时,就进行多行排列。

Usage:

1
2
3
<View 		 style={ {flexWrap:'wrap',flexDirection:'row',backgroundColor:"darkgray",marginTop:20}}>
···
</View>

flexWrap

justifyContent

justifyContent enum('flex-start', 'flex-end', 'center', 'space-between', 'space-around')
justifyContent属性定义了浏览器如何分配顺着父容器主轴的弹性(flex)元素之间及其周围的空间,默认为flex-start。

  • flex-start(default) 从行首开始排列。每行第一个弹性元素与行首对齐,同时所有后续的弹性元素与前一个对齐。
  • flex-end 从行尾开始排列。每行最后一个弹性元素与行尾对齐,其他元素将与后一个对齐。
  • center 伸缩元素向每行中点排列。每行第一个元素到行首的距离将与每行最后一个元素到行尾的距离相同。
  • space-between 在每行上均匀分配弹性元素。相邻元素间距离相同。每行第一个元素与行首对齐,每行最后一个元素与行尾对齐。
  • space-around 在每行上均匀分配弹性元素。相邻元素间距离相同。每行第一个元素到行首的距离和每行最后一个元素到行尾的距离将会是相邻元素之间距离的一半。

Usage:

1
2
3
<View 		 style={ {justifyContent:'center',flexDirection:'row',backgroundColor:"darkgray",marginTop:20}}>
···
</View>

justifyContent

alignItems

alignItems enum('flex-start', 'flex-end', 'center', 'stretch')
alignItems属性以与justify-content相同的方式在侧轴方向上将当前行上的弹性元素对齐,默认为stretch。

  • flex-start 元素向侧轴起点对齐。
  • flex-end 元素向侧轴终点对齐。
  • center 元素在侧轴居中。如果元素在侧轴上的高度高于其容器,那么在两个方向上溢出距离相同。
  • stretch 弹性元素被在侧轴方向被拉伸到与容器相同的高度或宽度。

Usage:

1
2
3
<View 		 style={ {justifyContent:'center',flexDirection:'row',backgroundColor:"darkgray",marginTop:20}}>
···
</View>

alignItems

子视图属性

  • alignSelf enum(‘auto’, ‘flex-start’, ‘flex-end’, ‘center’, ‘stretch’)
  • flex number

alignSelf

alignSelf enum('auto', 'flex-start', 'flex-end', 'center', 'stretch')
alignSelf属性以属性定义了flex容器内被选中项目的对齐方式。注意:alignSelf 属性可重写灵活容器的 alignItems 属性。

  • auto(default) 元素继承了它的父容器的 align-items 属性。如果没有父容器则为 “stretch”。
  • stretch 元素被拉伸以适应容器。
  • center 元素位于容器的中心。
  • flex-start 元素位于容器的开头。
  • flex-end 元素位于容器的结尾。

Usage:

1
2
3
4
<View style={ {alignSelf:'baseline',width:60,height:	20,backgroundColor:"darkcyan",margin:5}}>
<Text style={ {fontSize:16}}>1</Text>
</View>
...

alignItems

flex

flex number
flex 属性定义了一个可伸缩元素的能力,默认为0。

Usage:

1
2
3
4
5
6
7
8
9
10
11
<View style={ {flexDirection:'row',height:40, backgroundColor:"darkgray",marginTop:20}}>
<View style={ {flex:1,backgroundColor:"darkcyan",margin:5}}>
<Text style={ {fontSize:16}}>flex:1</Text>
</View>
<View style={ {flex:2,backgroundColor:"darkcyan",margin:5}}>
<Text style={ {fontSize:16}}>flex:2</Text>
</View>
<View style={ {flex:3,backgroundColor:"darkcyan",margin:5}}>
<Text style={ {fontSize:16}}>flex:3</Text>
</View>
</View>

flex

其他布局 in React Native


以下属性是React Native所支持的除Flex以外的其它布局属性。

视图边框

  • borderBottomWidth number 底部边框宽度
  • borderLeftWidth number 左边框宽度
  • borderRightWidth number 右边框宽度
  • borderTopWidth number 顶部边框宽度
  • borderWidth number 边框宽度
  • border<Bottom|Left|Right|Top>Color 个方向边框的颜色
  • borderColor 边框颜色

尺寸

  • width number
  • height number

外边距

  • margin number 外边距
  • marginBottom number 下外边距
  • marginHorizontal number 左右外边距
  • marginLeft number 左外边距
  • marginRight number 右外边距
  • marginTop number 上外边距
  • marginVertical number 上下外边距

内边距

  • padding number 内边距
  • paddingBottom number 下内边距
  • paddingHorizontal number 左右内边距
  • paddingLeft number 做内边距
  • paddingRight number 右内边距
  • paddingTop number 上内边距
  • paddingVertical number 上下内边距

边缘

  • left number 属性规定元素的左边缘。该属性定义了定位元素左外边距边界与其包含块左边界之间的偏移。
  • right number 属性规定元素的右边缘。该属性定义了定位元素右外边距边界与其包含块右边界之间的偏移
  • top number 属性规定元素的顶部边缘。该属性定义了一个定位元素的上外边距边界与其包含块上边界之间的偏移。
  • bottom number 属性规定元素的底部边缘。该属性定义了一个定位元素的下外边距边界与其包含块下边界之间的偏移。

定位(position)

position enum(‘absolute’, ‘relative’)属性设置元素的定位方式,为将要定位的元素定义定位规则。

  • absolute:生成绝对定位的元素,元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定。
  • relative:生成相对定位的元素,相对于其正常位置进行定位。因此,”left:20” 会向元素的 LEFT 位置添加 20 像素。

参考

A Complete Guide to Flexbox
Using CSS flexible boxes
Layout with Flexbox
Layout Props

移动web页面支持弹性滚动的3个方案

发表于 2017-10-20

有段时间一直折腾移动端页面弹性滚动的各种问题,做了点研究,今天做个小分享~

传统 pc 端中,子容器高度超出父容器高度,通常使用 overflow:auto 可出现滚动条拖动显示溢出的内容,而移动web开发中,由于浏览器厂商的系统不同、版本不同,导致有部分机型不支持对弹性滚动,从而在开发中制造了所谓的 BUG。

image

上图如果在PC端中,我们可以利用 position:fixed 和 overflow:auto 进行简单的布局实现我们需要的效果,而在手机端遇到的问题如下:

  • ios4 和 android2.2 以下不支持 position:fixed
  • ios 和 android2.3 以下不支持 overflow:auto
  • ios4 和 android 不支持 overflow-scrolling
    最严重的结果是:滚动区域内容无法拖动

对于 ios4 和 android2.2 以下不支持 position:fixed 的问题,有2种布局方法可以替代。

布局一: 定义页面整体高度为100%,然后使用 position:absolute 布局可解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
<!--absolute布局 [[ -->
<body>
<div class="wrap">
<div class="header">header</div>
<div class="main">
弹性滚动区域
</div>
<div class="footer">footer</div>
</div>
</body>
<!--absolute布局 ]] -->
*/
html,body{height:100%;}
.wrap{width:100%;}
.header,.footer{height:40px;line-height:40px;background-color:#D8D8D8;text-align:center;}
.header{position: absolute;top:0;left:0;width:100%;}
.footer{position: absolute;bottom:0;left:0;width:100%;}
.main{position:absolute;z-index:1;top:40px;left:0;bottom:40px;width:100%;}

布局二: 定义页面整体高度为100%,然后使用 display:flex 布局可解决

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
/*
<!-- flex布局 [[ -->
<body>
<div class="wrap">
<div class="header">header</div>
<div class="main">
弹性滚动区域
</div>
<div class="footer">footer</div>
</div>
</body>
<!-- flex布局 ]] -->
*/
html,body{
height:100%;
}
.wrap{
display:-webkit-box;
display:-webkit-flex;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:vertical;-webkit-flex-direction:column;
-ms-flex-direction:column;
flex-direction:column;width:100%;height:100%;
}
.header,.footer{
height:40px;
line-height:40px;
background-color:#D8D8D8;
text-align:center;
}
.main{
-webkit-box-flex:1;
-webkit-flex:1;
-ms-flex:1;
flex:1;
width:100%;
}

那么剩下的主要问题是子容器高度超出父容器高度,子容器内容如何弹性滚动。


对于如何使用弹性滚动,这里并没有最好的方案,具体看产品的用户群、产品的定位等,简单介绍下:

方案一: overflow:auto和-webkit-overflow-scrolling: touch

适合场景:产品的用户群大多为 ios5+、android4+ 用户,建议采用 overflow-scrolling 做差异化体验,毕竟 iscroll4.js 在 android 机器下体验不顺畅,另外还加载了 10K 多的 js 代码。

overflow:auto 写法在 winphone8 和 android4+ 上有用。ios5+ 版本增加了一个新的属性:overflow-scrolling 这个属性可以激活平滑滚动,效果不错。

1
2
3
4
.css{
overflow:auto;/* winphone8和android4+ */
-webkit-overflow-scrolling: touch; /* ios5+ */
}

flex体验demo:

http://peunzhang.github.io/demo/flex/flex-t-m-b.html

方案二: iscroll4.js和overflow:auto

适合场景:产品的用户群有 ios 和大部分 android2+ 用户,而在 android 中的页面数据比较简单(通常弹性滚动数据只有文字),那么使用 iscroll4.js 可保证 android2+ 的机器展现正常也不卡顿,让ios用户滚动更顺畅。

曾写过【使用iScroll.js解决ios4下不支持position:fixed的问题】,使用 iscroll4.js 基本上解决了页面弹性滚动的问题,总结下 iscroll4.js 的体验:

在 ios 系统上的表现十分良好,滚动顺畅

在部分 android 系统上性能较差,特别是滚动区域内容多时,滚动页面会出现卡顿

ios 和 android 系统下有不少 bug,如表单获焦弹出软键盘后页面高度没有重新计算、出现闪屏等(这里不做讨论)

###winphone 不支持
那么这里的处理方案是,页面初始化时判断是 weibit 浏览器则启用 iscroll4.js

1
2
3
4
<div class="wap ie-ova" id="">
<!-- webkit 用户设置 ID 为 iscroll,可启用iscroll -->
...
</div>

winphone8 手机使用如下 hack

1
2
3
4
@media screen and (-ms-high-contrast: active),
(-ms-high-contrast: none) {
.ie-ova{overflow:auto;} /* winphone8 */
}

方案三: iscroll4.js和overflow:auto和android2x.css

适合场景:产品的用户群有 ios 和大部分 android2+ 用户,而在 android 中页面数据比较复杂(通常弹性滚动数据有大量图片),那么可针对 android2+ 的机器做静态定位展现(position:static),页面不具备滚动效果,而对于 ios 用户仍然使用 iscroll4.js。

1
2
<!-- android2+ 用户多引用的css文件 -->
<link rel="stylesheet" href="android2x.css">

1
2
3
.css{
overflow:auto;/* winphone8和android4+ */
}
1
2
3
4
<!-- ios 用户设置 ID 为 iscroll,可启用iscroll -->
<div class="wap" id="">
...
</div>

跨页面通信的各种姿势

发表于 2017-09-19

前言

当看到这个题目的时候惊喜不惊喜,【第1060期】跨浏览器tab页的通信解决方案尝试不是刚刚推送了吗?今天还来,对因为在留言中发现了不一样的东西。今日早读文章由蚂蚁金服数据前端@mekron授权分享。

正文从这开始~

将跨页面通讯类比计算机进程间的通讯,其实方法无外乎那么几种,而web领域可以实现的技术方案主要是类似于以下两种原理:

获取句柄,定向通讯

共享内存,结合轮询或者事件通知来完成业务逻辑

由于第二种原理更利于解耦业务逻辑,具体的实现方案比较多样。以下是具体的实现方案,简单介绍下,权当科普:

一、获取句柄

具体方案

1
2
3
4
5
6
7
8
9
10
11

// parent.html
const childPage = window.open('child.html', 'child')

childPage.onload = () => {
childPage.postMessage('hello', location.origin)
}

// child.html
window.onmessage = evt => {
// evt.data

父页面通过window.open(url, name)方式打开的子页面可以获取句柄,然后通过postMessage完成通讯需求。

提示

  1. 当指定window.open的第二个name参数时,再次调用window.open('****', 'child')会使之前已经打开的同name子页面刷新

  2. 由于安全策略,异步请求之后再调用window.open会被浏览器阻止,不过可以通过句柄设置子页面的url即可实现类似效果

1
2
3
4
5
6
7
8

// 首先先开一个空白页
const tab = window.open('about:blank')

// 请求完成之后设置空白页的url
fetch(/* ajax */).then(() => {
tab.location.href = '****'
})

优劣
缺点是只能与自己打开的页面完成通讯,应用面相对较窄;但优点是在跨域场景中依然可以使用该方案。

二、localStorage

具体方案

设置共享区域的storage,storage会触发storage 事件

1
2
3
4
5
6
7
8

// A.html
localStorage.setItem('message', 'hello')

// B.html
window.onstorage = evt => {
// evt.key, evt.oldValue, evt.newValue
}

提示

  1. 触发写入操作的页面下的storage listener不会被触发
  2. storage事件只有在发生改变的时候才会触发,即重复设置相同值不会触发listener
  3. safari隐身模式下无法设置localStorage值

优劣
API简单直观,兼容性好,除了跨域场景下需要配合其他方案,无其他缺点

三、BroadcastChannel

具体方案
和localStorage方案基本一致,额外需要初始化

1
2
3
4
5
6
7
8
9
10

// A.html
const channel = new BroadcastChannel('tabs')
channel.onmessage = evt => {
// evt.data
}

// B.html
const channel = new BroadcastChannel('tabs')
channel.postMessage('hello')

优劣

和localStorage方案没特别区别,都是同域、API简单,BroadcastChannel方案兼容性差些(chrome > 58),但比localStorage方案生命周期短(不会持久化),相对干净些。

四、SharedWorker

具体方案

SharedWorker本身并不是为了解决通讯需求的,它的设计初衷应该是类似总控,将一些通用逻辑放在SharedWorker中处理。不过因为也能实现通讯,所以一并写下:

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

// A.html
var sharedworker = new SharedWorker('worker.js')
sharedworker.port.start()
sharedworker.port.onmessage = evt => {
// evt.data
}

// B.html
var sharedworker = new SharedWorker('worker.js')
sharedworker.port.start()
sharedworker.port.postMessage('hello')

// worker.js
const ports = []
onconnect = e => {
const port = e.ports[0]
ports.push(port)
port.onmessage = evt => {
ports.filter(v => v!== port) // 此处为了贴近其他方案的实现,剔除自己
.forEach(p => p.postMessage(evt.data))
}
}

优劣

相较于其他方案没有优势,此外,API复杂而且调试不方便。

五、Cookie

具体方案

一个古老的方案,有点localStorage的降级兼容版,我也是整理本文的时候才发现的,思路就是往document.cookie写入值,由于cookie的改变没有事件通知,所以只能采取轮询脏检查来实现业务逻辑。

优劣

相较于其他方案没有存在优势的地方,只能同域使用,而且污染cookie以后还额外增加AJAX的请求头内容。

六、Server

之前的方案都是前端自行实现,势必受到浏览器限制,比如无法做到跨浏览器的消息通讯,比如大部分方案都无法实现跨域通讯(需要增加额外的postMessage逻辑才能实现)。通过借助服务端,还有很多增强方案,也一并说下。

乞丐版

后端无开发量,前端定期保存,在tab被激活时重新获取保存的数据,可以通过校验hash之类的标记位来提升检查性能。

1
2
3
4
5
6

window.onvisibilitychange = () => {
if (document.visibilityState === 'visible') {
// AJAX
}
}

Server-sent Events / Websocket

项目规模小型的时候可以采取这类方案,后端自行维护连接,以及后续的推送行为。

SSE

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

// 前端
const es = new EventSource('/notification')

es.onmessage = evt => {
// evt.data
}
es.addEventListener('close', () => {
es.close()
}, false)
// 后端,express为例
const clients = []

app.get('/notification', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
clients.push(res)
req.on('aborted', () => {
// 清理clients
})
})
app.get('/update', (req, res) => {
// 广播客户端新的数据
clients.forEach(client => {
client.write('data:hello\n\n')
setTimeout(() => {
client.write('event:close\ndata:close\n\n')
}, 500)
})
res.status(200).end()
})

Websocket

http://socket.io、sockjs例子比较多,略

消息队列

项目规模大型时,需要消息队列集群长时间维护长链接,在需要的时候进行广播。

提供该类服务的云服务商很多,或者寻找一些开源方案自建。

例如MQTT协议方案(阿里云就有提供),web客户端本质上也是websocket,需要集群同时支持ws和mqtt协议,示例如下:

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

// 前端
// 客户端使用开源的Paho
// port会和mqtt协议通道不同
const client = new Paho.MQTT.Client(host, port, 'clientId')

client.onMessageArrived = message => {
// message. payloadString
}
client.connect({
onSuccess: () => {
client.subscribe('notification')
}
})
// 抑或,借助flash(虽然快要被淘汰了)进行mqtt协议连接并订阅相应的频道,flash再通过回调抛出消息

// 后端
// 根据服务商提供的Api接口调用频道广播接

关于本文
作者:@mekron
原文:https://zhuanlan.zhihu.com/p/29368435

跨浏览器tab页的通信解决方案尝试

发表于 2017-09-18

本文转载自前端早读课
作者:欲休

目录

正文从这开始~

目标

当前页面需要与当前浏览器已打开的的某个tab页通信,完成某些交互。其中,与当前页面待通信的tab页可以是与当前页面同域(相同的协议、域名和端口),也可以是跨域的。

要实现这个特殊的功能,单单使用HTML5的相关特性是无法完成的,需要有更加巧妙的设计。

畅想

现在我们发现下思维,假设多种场景下的解决方案,最终寻找通用解。

case 1

两个需要交互的tab页面具有依赖关系。

如 A页面中通过JavaScript的window.open打开B页面,或者B页面通过iframe嵌入至A页面,此种情形最简单,可以通过HTML5的 window.postMessage API完成通信,由于postMessage函数是绑定在 window 全局对象下,因此通信的页面中必须有一个页面(如A页面)可以获取另一个页面(如B页面)的window对象,这样才可以完成单向通信;B页面无需获取A页面的window对象,如果需要B页面对A页面的通信,只需要在B页面侦听message事件,获取事件中传递的source对象,该对象即为A页面window对象的引用:

1
2
3
4
5
6
//B页面

window.addEventListner('message',(e)=>{
let {data,source,origin} = e;
source.postMessage('message echo','/');
});

postMessage的第一个参数为消息实体,它是一个结构化对象,即可以通过“JSON.stringify和JSON.parse”函数还原的对象;第二个参数为消息发送范围选择器,设置为“/”意味着只发送消息给同源的页面,设置为“*”则发送全部页面。

case 2

两个打开的页面属于同源范畴。

若要实现两个互不相关的通源tab页面通信,可以使用一种比较巧妙的方式:localstorage。localStorage的存储遵循同源策略,因此同源的两个tab页面可以通过这种共享localStorage的方式实现通信,通过约定localStorage的某一个itemName,基于该key值的内容作为“共享硬盘”方式通信。

不过,如果单纯使用localStorage存储做通信方式会遇到一个问题,就是两个页面把握不准通信时机,如果A页面此刻需要发送给B页面一条消息“hello B”,它会设置localStorage.setItem(‘message’,’hello B’),并且采用setTimeout轮询等待B的消息;而B此刻也同样使用setTimeout轮训等待localStorage的message项的变化,当获取到’message’字段时,便取出消息’hello B’。B如果要发消息给A,仍然采用同样套路。

这种方式性能极其低下,需要通信两方不停的监听localStorage某项的变化,及其浪费事件队列处理效率。
幸好,HTML5提供了storage事件,通过window对象侦听storage事件,会侦听localStorage对象的变化事件(包括item的添加、修改和删除)。因此,通过事件可以完成高效的通信机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//A 页面

window.addEventListener("storage", function(ev){
if (ev.key == 'message') {
// removeItem同样触发storage事件,此时ev.newValue为空
if(!ev.newValue)
return;
var message = JSON.parse(ev.newValue);
console.log(message);
}
});

function sendMessage(message){
localStorage.setItem('message',JSON.stringify(message));
localStorage.removeItem('message');
}

// 发送消息给B页面
sendMessage('this is message from A');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
B 页面

window.addEventListener("storage", function(ev){
if (ev.key == 'message') {
// removeItem同样触发storage事件,此时ev.newValue为空
if(!ev.newValue)
return;
var message = JSON.parse(ev.newValue);
// 发送消息给A页面
sendMessage('message echo from B');
}
});

function sendMessage(message){
localStorage.setItem('message',JSON.stringify(message));
localStorage.removeItem('message');
}

发送消息采用sendMessage函数,该函数序列化消息,设置为localStorage的message字段值后,删除该message字段。这样做的目的是不污染localStorage空间,但是会造成一个无伤大雅的反作用,即触发两次storage事件,因此我们在storage事件处理函数中做了if(!ev.newValue) return;判断。

当我们在A页面中执行sendMessage函数,其他同源页面会触发storage事件,而A页面却不会触发storage事件;而且连续发送两次相同的消息也只会触发一次storage事件,如果需要解决这种情况,可以在消息体体内加入时间戳:

1
2
3
4
5
6
7
8
9

sendMessage({
data: 'hello world',
timestamp: Date.now()
});
sendMessage({
data: 'hello world',
timestamp: Date.now()
});

通过这种方式,可以实现同源下的两个tab页通信,兼容性

通过caniuse网站查询storage事件发现,IE的浏览器支持非常的不友好,caniuse使用了“completely wrong”的形容词来表述这一程度。IE10的storage事件会在页面document文档对象构建完成后触发,这在嵌套iframe的页面中造成诸多问题;IE11的storage Event对象却不区分oldValue和newValue值,它们始终存储更新后的值

case 3

两个互不相关的tab页面通信。

这种情况才是最急需解决的问题,如何实现两个没有任何关系的tab页面通信,这需要一些技巧,而且需要有同时修改这两个tab页面的权限,否则根本不可能实现这两个tab页的能力。

在上述条件满足的情况下,我们就可以使用case1 和 case2的技术完成case 3的需求,这需要我们巧妙的结合HTML5 postMessage API 和 storage事件实现这两个毫无关系的tab页面的连通。为此,我想到了iframe,通过在这两个tab页嵌入同一个iframe页实现“桥接”,最终完成通信:

case 3

1
2
3
4
5
6

tab A -----> iframe A[bridge.html]
|
|
\|/
iframe B[bridge.html] -----> tab B

单方向的通信原理如上图所示,tab A中嵌入iframe A,tab B中嵌入iframe B,这两个iframe引用相同的页面“bridge.html”。如果tab A发消息给tab B,首先tab A通过postMessage消息发送给iframe A(tab A可以获取到iframe A的window对象iframe.contentWindow);此后iframe A通过storage消息完成与iframe B的通信(由于iframeA 与iframe B同源,因此case 2的通信方式这里可以使用);最终,iframe B同样采用postMessage方式发送消息给tab B(在iframe中通过window.parent引用tab B的window对象)。至此,tab A的消息走通了所有链路,成功抵达tab B。

反方向发送消息同样的道理,这里就不在详细说明。接下来到了 talk is cheap,show me the code 环节:

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

tab A:

// 向弹出的tab页面发送消息
window.sendMessageToTab = function(data){
// 由于[#J_bridge]iframe页面的源文件在vstudio服务器中,因此postMessage发向“同源”
document.querySelector('#J_bridge').contentWindow.postMessage(JSON.stringify(data),'/');
};

// 接收来自 [#J_bridge]iframe的tab消息
window.addEventListener('message',function(e){
let {data,source,origin} = e;
if(!data)
return;
try{
let info = JSON.parse(JSON.parse(data));
if(info.type == 'BSays'){
console.log('BSay:',info);
}
}catch(e){
}
});

sendMessageToTab({
type: 'ASays',
data: 'hello world, B'
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

bridge.html

window.addEventListener("storage", function(ev){
if (ev.key == 'message') {
window.parent.postMessage(ev.newValue,'*');
}
});

function message_broadcast(message){
localStorage.setItem('message',JSON.stringify(message));
localStorage.removeItem('message');
}

window.addEventListener('message',function(e){
let {data,source,origin} = e;
// 接受到父文档的消息后,广播给其他的同源页面
message_broadcast(data);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

tab B

window.addEventListener('message',function(e){
let {data,source,origin} = e;
if(!data)
return;
let info = JSON.parse(JSON.parse(data));
if(info.type == 'ASays'){
document.querySelector('#J_bridge').contentWindow.postMessage(JSON.stringify({
type: 'BSays',
data: 'hello world echo from B'
}),'*');
}
});

// tab B主动发送消息给tab A
document.querySelector('button').addEventListener('click',function(){
document.querySelector('#J_bridge').contentWindow.postMessage(JSON.stringify({
type: 'BSays',
data: 'I am B'
}),'*');
})

至此,通过在tab A和tab B中引入“桥接”功能的iframe[bridge.html]页面,实现了两个无关tab页的双向通信,这种实现的技巧性较强。

参考资料

Communication between tabs or windows

关于本文

作者:@欲休

原文:http://www.cnblogs.com/accordion/p/7535188.html

12
杨勇冠

杨勇冠

杨勇冠的博客

12 日志
© 2018 杨勇冠
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4