Giter Club home page Giter Club logo

blog's People

Contributors

sixwinds avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

blog's Issues

test

var a= "b";
<div></div>

业务同样的程序跑在TDocker、XEN,占用的内存却相差很大。

图片加载延时导致 jquery domready 延时响应

图片加延时饰导致 jquery domready 延时响应

1 问题

项目中碰到一个奇怪的问题:

我们的页面上有一个Alexa Certify 的script,这个script会用一个 amazon 的图片( x.png )作为请求发送统计信息。但是这个图片的地址会被 GFW 墙掉导致超时。我们的页面 js 有的时候会因为这个超时导致加载部分功能很慢。

我测试了下 $() 中的回调函数要等到 x.png 加载完或者超时之后才会执行,慢的这部分代码其实都是放在 $() 中的,也就是说需要 dom ready 后执行的代码。但是照道理 img 的加载和 dom ready 并不应该有冲突,而且为啥有的页面没有这个问题而主页有呢?

2 分析

看下主页,是用 requirejs 来加载 script 的,而其他页面是直接把 script 贴在 html 最底下的。导致的区别就是 requirejs 异步加载模块的并且执行到 $() 的时候,dom 已经 ready 了。而直接贴 script 的 js 在运行到 $()时候 dom 还没有完全 ready。

但是为啥 requirejs 异步加载的代码在 dom ready 之后如果运行 $() 依然会被执行( 或者说回调 )?

这个时候就要看我们坑爹的 jq 是如何实现了。jq 的 $() 会把回调函数注册到 document 的 DOMContentLoaded 事件上和 window 的 load 事件上去( 对,你没看错是同时注册到两个事件上去 )。这两个事件的区别不详尽了,大家自行google。但是这两个事件触发的时间是不同的,当我们的 dom 加载完而 x.png 还在 loading 的时候 DOMContentLoaded 就会触发了,而 window 的 load 事件是会在 x.png 加载完或者超时之后才会触发。

那么我们用 requirejs 加载 js 和直接在 html 贴 script 的差别就来了。html 直接贴 script 的 js 响应的是 document 的 DOMContentLoaded 事件,自然不会受到 x.png 的影响,而 requirejs 加载的 js 就不同了,当他被动态加到 html中并且运行到 $() 的时候 document 的 DOMContentLoaded 事件已经触发过了,$() 中的回调函数必须等待 window 的 load 事件触发才会被调用到,所以用requirejs加载的代码会受到 x.png 加载是否完成的影响,这就是我们主页慢的原因。而 jq 的 ready 对页面加载的影响也是臭名昭著了。

详细的东西大家可以自行 google。( 这里我是测试了chrome,firefox 得出的结论,而更具体的原理待我研究后在下一篇里面讲 )

3 解决方案

虽然requirejs 是动态加载 js 的,但是 requirejs 本身并不是,而我们的 cdn 又支持合并 js 的请求

譬如:

http://static-web.b5m.com/public/js/??jquery-1.9.1.min.js,jquery-window.js,imglazyload.min.js?_a=1&v=2015412015043014435154

那么我们页面上的 requirejs 可以这么贴:

<script type="text/javascript" id="requirejs" data-main="/home/js/test.js" src="http://CDN/public/js/??require-min.js,require-domready.js?_a=1"></script>`

在require-domready.js中我们主动注册 document 的 DOMContentLoaded ,并且在此事件触发时标记 DOMContentLoaded 已触发,而其他业务代码在用到 $() 的时候先去判断这个标记是否存在,如果存在则可以直接写要执行的代码,如果没有的话再去调用 $(),我们可以把这个逻辑判断包装成我们自己的ready函数,这样就能解决该死的 x.png 和 $() 的问题了。

移动H5页面开发多屏适配 - viewport和像素篇

移动H5页面开发多屏适配 - viewport和像素篇

最近刚刚接触移动端的页面开发,遇到的第一个问题就是搞清楚多屏适配的方案。要知道多屏适配的原理就要知道移动端各种屏幕大小对开发的影响,最基础的是像素和 viewport 的概念。以下是网上一些像素及 viewport 的资料的知识梳理(绝大多数就是总结于 ppk 两篇谈论 viewport 的文章 onetwo,图片亦是出自于 ppk 的文章)。

在 pc 开发时代我们 css 用的最多的长度单位就是 px,通常我们认为 css 里面的像素就是屏幕的物理分辨率。其实 css 像素和屏幕的物理像素标准上并没有 1:1 的关系,只是在 pc 时代他们通常正好是 1:1,例外的情况极少,所以到了移动时代我们需要理清楚两者关系才能知道多屏适配方案的远离。让我们从 pc 时代说起。

PC时代

我们在做 pc 网页开发的时候几乎不涉及对 viewport 的修改,所以我们很少去关心 viewport,那么 viewport 指的是哪部分呢?我们知道一般一个 div 它的默认宽度是 100% 撑满其父节点,那么 body 下的 div 就是 body 的宽度,body 就是 html 的宽度,而 html 的宽度是由谁限定的呢? 浏览器?又或者说是 window?其实正是一个叫 viewport 的东西,它才是默认情况下限定页面宽度的最外层“容器”。只是我们在做 pc 网页开发的时候默认情况下 viewport 和 window 是同样大小的。

viewport 的尺寸可以通过 document.documentElement.clientWidth/height 来获取,浏览器窗口的尺寸可以通过 window.innerWidth/height 来获取。viewport,浏览器窗口尺寸的单位都是 css 像素,但是他们含义有点不同。如果你放大一个网页时,你看到的页面元素虽然变大了,但是你用 js 获取他们的尺寸是没有变化的,也就是说元素的尺寸在 css 这个衡量的坐标系下并没有变化,100px 宽依然是 100px。只是电脑使得 css物理像素比增大了,原来1个 css 像素占1个物理像素,当你放大网页一倍的时候,1个 css 像素占用了4个物理像素。而浏览器窗口尺寸的含义是你这个窗口内可以容纳多少个 css 像素。譬如默认情况下如果你页面宽度是 1024px 宽占满了整个浏览器窗口,那么 window.innerWidth 就是 1024px。当你把网页放大一倍时,由于css物理像素比变大了,浏览器窗口从宽度角度看看只有原来一半的网页,也就是只能容纳 512 个 css 像素。你再去获取 window.innerWidth 它的值就是512。所以说 viewport 和浏览器窗口的尺寸都是以 css 像素作为单位的。顺便说下屏幕的尺寸是通过 screen.width/height 来获取的,原则上他们也是 css 像素为单位的,但是 pc 时代我们很少关心他们,也不能改变他们。在 pc 时代绝大多数情况下我们是在 css 像素和物理像素 1:1 的情况下开发的,所以我们也不太纠结我们页面元素尺寸的衡量体系。

移动时代

iphone4 以前的手机屏幕无论是大小和分辨率都是很低的,iphone3 的分辨率是 320x480。如果在手机浏览器里面看 pc 的网页,而且如果手机浏览器依然使用css物理像素 1:1 的方式去显示网页,那么我们相当于只能在一个网页上画一个320x480 框框,用户使用这个框框来左右移动窥探这个网页,这显然是不好用的。

h5-part1-pic1

手机浏览器厂商就做了一个英明的决策,把手机浏览器默认的 viewport 的尺寸放大,譬如说 980px,而浏览器窗口默认情况下就是容纳一个 100% 宽的 viewport,所以如果一个 980px 宽的 pc 网页在手机端打开,默认情况下手机浏览器也能看到整个网页,只是这个网页是被缩小的。这个时候我们就能体会到 css 像素和物理像素的区别,手机分辨率宽虽然是320px的(物理像素),但是页面宽度确是980px(css 像素)。

h5-part1-pic2

但是这个时候还是依然不好用啊,pc 端的元素在手机端看太小点不到啊,用户在用的时候依然需要放大网页。如果针对手机的宽度设计一个网页那么不就解决这个问题了么,譬如把 viewport 设置成 320px 宽,在这个尺寸下给网页设计元素给网页排版。怎么设置 viewport呢?给 documnent.documentElement.width/height 赋值并没有作用,改变 html 元素的尺寸并不会影响 viewport 尺寸,只会造成如下的情况(html是变小了,但是 viewport依然很宽):

h5-part1-pic3

苹果公司给出了一个 meta 标签用来控制 viewport 的大小(随后被其他浏览器竞相效仿):<meta name="viewport" content="width=320">,通过这个标签指定 width 可以控制 viewport 的大小,你可以写一个 css 像素的值譬如<meta name="viewport" content="width=320">,但是理想情况下最好是和手机屏幕一样宽,也就是 device-width,这样 iphone3 初始化的 viewport 就是 320px 宽了。使用 meta 可以让我们在手机端也能做到在css物理像素 1:1 情况下开发网页。

h5-part1-pic3 1

但是随着 iphone4 的发布,人们发现其分辨率达到了惊人的 640x960,此时原来在 320 宽的标准下开发的网页在 iphone4 上怎么呈现,难道以后开发手机网页需要适配两个屏宽了?其实 google 已经在 android 系统上给出了一种叫密度无关像素的解决方案(density-independent pixel,或者叫 device-independent pixel,简称 dip,dp 都可以)。简单举个例子,当使用<meta name="viewport" content="width=device-width">来设定手机浏览器 viewport 的宽度时,如果是同一个尺寸的屏幕无论分辨率是多少 viewport 都是一个固定的值。设备宽度(device-width)单位将不是物理像素,而是密度无关像素(dip)。这样大大减少了开发人员适配不同分辨率手机的工作量,无论你的手机分辨率是 320x480,还是 640x960,又或者是 960x1440,只要你写上<meta name="viewport" content="width=device-width">,那么手机浏览器网页的初始化宽度都是 320px。

当然不是所有的手机 device-width 都是 320px,上面我只是举了简化的例子。随着手机越出越多,device-width 的值也越来越多,光 iphone 我们就可以通过 paintcodeapp.com 给出的这张图来看下,更不要说众多的 android 手机。

h5-part1-pic4

再提下 meta viewport 另外一个属性值就是 initial-scale,缩放比例。如果手机屏幕宽代表 320px(dip),那么这么设置 <meta name="viewport" content="width=device-width, initial-scale=1.0">表示手机浏览器初始化 viewport 宽度是屏幕宽度且不进行缩放是为 320px,如果设置成<meta name="viewport" content="width=device-width, initial-scale=0.5">表示手机浏览器初始化时候宽度是在屏幕宽度的基础上把页面缩小一倍,即 viewport 宽度是 device-width * 2 = 640px。这些变化我准备在下一篇总结,因为这些变化加上手机分辨率精度的上升直接影响着多屏适配的最终方案。

简单总结下移动端手机屏幕的尺寸像素值是密度无关像素dip,页面元素尺寸是 css 像素,当 <meta name="viewport" content="width=device-width, initial-scale=1.0">时,两者的比例(dip:csspx)可以是1:1。是要多屏适配方案定下后,手机分辨率或者说物理像素基本不需要在开发时候考虑了。

React组件编写思路(一)

React组件编写思路(一)

新手写 React 组件往往无从入手,怎么写,什么时候用 props,什么时候用 state 摸不着头脑。其实是没有了解到 React 的一些**。就我个人的经验大多数的组件都有一定的套路可言,接下来就先介绍下 React 组件的基本**。

React 组件可以分为可控组件和非可控组件。可控组件意思是组件自身控制自己的状态(属性),可以通过自身提供的方法(供调用者使用)来改变自己的状态。譬如一个 input text 输入框提供一个 reset 方法,如果要清空用户输入则通过获得 inupt 组件对象,然后调用 reset 方法来做

refs.inputRef.rest() 

非可控组件的意思是组件本身的状态(属性)自己无法更改,只能随着外部传入的值(props)而变化。还是拿输入框清空这一个操作来说,非可控的 input 不通过自己提供方法来改变(维护)自己的状态(value),只通过外部传入一个值为空字符串的 value 来做到清空的效果。

reset(){
  this.setState({
    inputValue: ''
  })
}
render(){
  return <input value={this.state.inputValue}/>
}

我们拿一个场景来看下完整的代码(一个 form 中有一个 input,有一个 reset 按妞,点击 reset 按妞会清空用户的输入),看下这两种组件书写的区别。
受控组件例:

class App extends React.Component {
  reset = ()=>{
    this.refs.myInput.reset() // 假设 input 有一个 reset 方法
  }
  render() {
    <div>
      <form>
        <input type="text"  ref="myInput" />
        <button onClick={ this.reset }>Reset</button>
      </form>
    </div>
  }
}

非受控组件例:

class App extends React.Component {
  constructor( props ){
    super( props );
    this.state = {
      inputValue: 'Plz input your text.'
    }
  }
  reset = ()=>{
    this.setState( {
      inputValue: ''
    } )
  }
  render() {
    <div>
      <form ref="myForm">
        <input type="text" value={ this.state.inputValue }/>
        <button onClick={ this.reset }>Reset</button>
      </form>
    </div>
  }
}

接下来我们来看下如果编写这两种组件,打个比方我们要自定义一个 alert 组件。我们先从非受控组件说起,因为较简单。非受控组件所要做的就是把所有状态提取到组件的 props 中去,render 中就用 props。一个 alert 有哪些最基本的状态(属性)呢?我们以最基础的功能定出一个表示显示与否的 show,一个表示显示内容的 content。那么组件代码如下。

class Alert extends React.Component {
  constructor( props ) {
    super( props )
  }
  render() {
    let style = {
      display: this.props.show ? 'fixed' : 'none'
    }
    return (
      <div class="my-alert" style={ style } >
        <div class="my-alert-tit">Alert</div>
        <div>{ this.props.content }</div>
        <div class="my-alert-footer">
          <button>确定</button>
        </div>
      </div>
    );
  }
}

Alert.propTypes = {
  show: React.PropTypes.bool,
  content: React.PropTypes.string
}

我们看到最直观的就是只需要考虑到 props 的可能取值就行,不需要关心如何改变props。而使用这个非可控 alert 的代码如下:

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      alertMsg: '',
      showAlert: false
    }
    this.saveHandler = ()=>{
      // ajax success
      this.setState( {
        alertMsg: 'Save successfully',
        showAlert: true
      } )
    }
  }

  render() {
    <div>
      <button onClick={ this.saveHandler }>Save</button>
      <Alert 
        content={ this.state.alertMsg }
        show={ this.state.showAlert }
      />
    </div>
  }
}

接下来我们看下可控组件的alert怎么写。可控组件通过方法来供调用者来改变组件的状态(属性)。所以暂时我们不定义 props 只定义几个方法 show(content), hide()。组件代码如下:

class Alert extends React.Component {
  constructor( props ) {
    super( props )
    this.state = {
      content: '',
      show: false
    }
    this.show = ( content )=>{
      this.setState( {
        content: content,
        show: true
      } )
    }

    this.hide = ()=>{
      this.setState( {
        show: false
      } )
    }
  }
  render() {
    let style = {
      display: this.state.show ? 'fixed' : 'none'
    }
    return (
      <div class="my-alert" style={ style } >
        <div class="my-alert-tit">Alert</div>
        <div>{ this.state.content }</div>
        <div class="my-alert-footer">
          <button onClick={ this.hide }>确定</button>
        </div>
      </div>
    );
  }
}

我们看到可控组件内部需要用到 state 来自己改变自己的状态。使用这个可控 alert 的代码如下:

import { Alert } from 'Alert';

class App extends React.Component {
  constructor() {
    super();
    this.saveHandler = ()=>{
      // ajax success
      this.refs.myAlert.show( 'Save Successfully' );
    }
  }

  render() {
    <div>
      <button onClick={ this.saveHandler }>Save</button>
      <Alert ref="myAlert"/>
    </div>
  }
}

但是可控组件有一个问题就是他的初始化状态如何设置(如何由外部定义组件 state 的初始化值)?由于没有 props 那么只能通过方法来设置,那么这么做法很别扭。这时可以通过定义 props 把初始化状态在生成这个组件时传入,而不必等组件生成完再通过调用方法传入。于是修改后的代码如下:

class Alert extends React.Component {
  constructor( props ) {
    super( props )
    this.state = {
      content: this.props.defaultContent,
      show: this.props.defaultShow
    }
    this.show = ( content )=>{
      this.setState( {
        content: content,
        show: true
      } )
    }

    this.hide = ()=>{
      this.setState( {
        show: false
      } )
    }
  }

  render() {
    let style = {
      display: this.state.show ? 'fixed' : 'none'
    }
    return (
      <div class="my-alert" style={ style } >
        <div class="my-alert-tit">Alert</div>
        <div>{ this.state.content }</div>
        <div class="my-alert-footer">
          <button onClick={ this.hide }>确定</button>
        </div>
      </div>
    );
  }
}

Alert.propTypes = {
  defaultShow: React.PropTypes.bool,
  defaultContent: React.PropTypes.string
}

Alert.defaultProps = {
  defaultShow: false,
  defaultContent: ''
}

使用这个组件的代码:

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      alertMsg: '',
      showAlert: false
    }
    this.saveHandler = ()=>{
      // ajax success
      this.refs.myAlert.show( 'Save Successfully' );
    }
  }

  render() {
    <div>
      <button onClick={ this.saveHandler }>Save</button>
      <Alert ref="myAlert" defaultShow={false} defaultContent={''}/>
    </div>
  }
}

以上就是两种 React 组件的编写思路,你可以选择把你的组件编写成任意一种,那么使用者使用时也会有所不同。但是作为一个具有良好可用性的组件,不应该限制使用者的用法,那么下篇将介绍如何编写一个既可以作为可控组件,也可以作为一个非可控组件的组件写法。

css-loader 对图片的处理说明 | 笔记

css-loader 对图片的处理说明 | 笔记

css-loader 对图片的处理说明(基于 webpack 1 )

项目目录结构

proj
 | 
 | --- webpack.config.js
 |
 | --- dist
 |
 | --- src
         |
         | --- index.js
         | --- index.css/index.less
         | --- images
                  |
                  | --- small.png ( < 8k )
                  | --- big.png ( > 8k )

css/less 文件

.big-pic {
  background-image: url(./images/big.png)
}
.small-pic {
  background-image: url(./images/small.png)
}

webpack 配置文件(webpack.config.js)

module.exports = {
  entry: './src/index.js'
  output: {
    path: './dist',
    filename: 'js/bundle.js',
  },
  module: {
    loaders: [
      {
        test: /\.(less|css)$/,
        loader: [
          'style',
          'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]',
          'less'
        ].join('!')
      },
      {
        test: /\.(jpg|jpeg|gif|png)$/,
        loader: 'url?limit=8000&name=imgs/[name].[ext]'
      }
    ]
  }
}
  1. css?&modules 表示开启 css module,不过开不开启和css-loader处理图片没有直接关系 -_-

  2. css 中碰到url(),css-loader会把它当成一种webpack的资源去 import

  3. 如果url()变成了一种资源被 import,我们就必须制定用哪种 loader 去加载,所以我们配置了url-loader 对 /\.(jpg|jpeg|gif|png)$/ 进行处理

  4. url?limit=8000的意思是当遇到小于8k的图片,则url-loader会把它编译成base64编码直接放到css中, background-image: url(./images/small.png) -> background-image:url(data:image/png;base64,bxx...xxW)

  5. url?name=imgs/[name].[ext]的意思是把 url(./images/big.png) -> url(imgs/big.png) 并把图片拷贝到 output.path + name的地方(这里name指的是 url?name=xxx 的 name)。即从 /src/images/big.png -> /dist/imgs/big.png

  6. 到此为止 webpack 不会把 css 编译后单出输出到一个文件里面,而是编译在 js 里面,当 js 执行后会在 dom 里面动态生成一个 style。如果要单出抽象出一个文件,请在 webpack 里面加上插件 ExtractTextPlugin

const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: './src/index.js'
  output: {
    path: './dist',
    filename: 'js/bundle.js',
  },
  module: {
    loaders: [
      {
        test: /\.(less|css)$/,
        loader: ExtractTextPlugin.extract( [
          'style',
          'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]',
          'less'
        ].join('!') )
      },
      {
        test: /\.(jpg|jpeg|gif|png)$/,
        loader: 'url?limit=8000&name=imgs/[name].[ext]'
      }
    ]
  },
  plugins:[ new ExtractTextPlugin('css/bundle.css')]
}
  1. 加上 ExtractTextPlugin,看plugins:[ new ExtractTextPlugin('css/bundle.css')] webpack 会把编译后的 css 输出到 output.path + 'css/bundle.css',所以最终的编译后的目录结构为:
proj
 | 
 | --- webpack.config.js
 |
 | --- dist
 |      |
 |      | --- js
 |      |      | --- bundle.js
 |      | --- css
 |      |      | --- bundle.css
 |      | --- imgs
 |              | --- big.png
 |
 | --- src
         |
         | --- index.js
         | --- index.css/index.less
         | --- images
                  |
                  | --- small.png ( < 8k )
                  | --- big.png ( > 8k )

特别说明:
css-loader 在处理当前文件的目录结构时有问题 background-image: url(./images/small.png) 会找不到图片,请写成 background-image: url(../src/images/small.png),即先回到上层目录再往下引用。所以最终 css 写成:

.big-pic {
  background-image: url(../src/images/big.png)
}
.small-pic {
  background-image: url(../src/images/small.png)
}

这个接口设计得到底合理不合理?

这个接口设计得到底合理不合理?

说明:以下是昨天和同事关于一个接口的讨论,我觉得很有意思,针对同一个问题大家从不同的角度来看接口设计的问题。记录下来可以为自己以后设计接口提供参考,更希望有对此有想法的人能一起探讨下(顺便拉小弟一把帮我进步一下)

背景

注册时需要对用户输入的密码进行一定的规则校验,页面和后端都校验(废话-_-)。但是我们的系统的 admin 可以对这个校验规则进行配置,实时修改。那么为了保证前后台校验规则的一致性,我们采取的方案是通过 ajax 从后台获取密码的校验规则(正则表达式),而不是在 javascript 里面写死。以下是接口及返回 demo 版本:

// interface http://xxx.xxx.com/xxx/xxx/login/access
/*
  Response JSON:
  {
    data: {
      ...
      pwdRegExp: "^(?=.*[\p{Digit}])(?=.*[\p{Lower}])(?=.*[\p{Upper}])[\d\p{Upper}\p{Lower}\p{Punct}]{8,20}$"
      ...
    },
    mcode: xxx
    result: true
  }

  
*/

接口返回的字段中 pwdRegExp 就是后台对密码的验证规则,它的值是 java 的正则表达式。页面收到这个字段后通过一个转换器把她转成 javascript 的正则表达式

function toJsRegExpMap( backendRegExp ) {
  return new RegExp( backendRegExp.
                      replace(/\\p{Digit}/g, '\\d').
                      replace(/\\p{Lower}/g, 'a-z').
                      replace(/\\p{Upper}/g, 'A-Z').
                      replace(/\\p{Alpha}/g, 'a-zA-Z').
                      replace(/\\p{Punct}/g, PUNCT)
                    );
}

讨论

申明在前,我在这里记录下讨论绝对没有偏好,没有谁对谁错,只是想还原原来的讨论,我自己最后也承认这个问题我可以接受任何的合理的结论(A,C 是同事,B 是我,讨论有删减,不保证完全是原文)

A:这个api为什么不直接让他们提供一个可用的正则?如果是要js去转的话,就是说每个使用这个api的地方都要自己去转。这样肯定是不合理的。

B:为了保证前后端验证规则一致,后端传出它的正则,然后由其他系统去做其他系统的转换及 mapping,这个我个人认为是 make sense。因为后端不能保证它给出的前端正则和后端正则的校验结果能严格一致。(我假设后端开发人员对 js 不熟 - 这括号中是我写此文时候的补充,非原对话中有)

A:那使用api的各方各自转换能确保一致吗?这更走远了。输出api的那方直接给出正则,有问题改的也只是输出api的地方。

B:因为转换规则是其他系统所知,后端系统并不可知。

A:直接是隔离了问题。现在就 java 和 js,如果有再添加就行了。或者没有再转。

B:自己系统的问题自己维护,你转错了是你的问题,好比后端只给出规则,其他系统负责实现,只是这个规则以 Java 正则来表达的。

A:但是 java 和 js 这两种是肯定会有用的。

C: 因为现在只有 js 消费这个正则,如果这个接口同时需要为 php 提供服务的话,它不太会为所有消费者提供转换的 后台会变得太复杂了。

B:你一定说只有两系统,那传一传也可以,我是从系统隔离的角度考虑的。

A:转换没风险吗?还是使用 api 的各方自己转,怎么保证各方转换是对的?有多少个调用的地方就有可能有多少个出错的地方。

B:有风险,看你怎么看这个问题,最好个各系统给出各自的转换器,至于这个转换器是放在一个系统中还是各自系统中,那就看你看中沟通成本还是系统稳定性。如果一个转换器出问题,拥有者自己去改,那么影响的系统只是他那个系统。不过我们这个例子简单,其实放哪里都可以,问题不大。

C:你这样考虑吧,这个接口的返回其实和其他普通的json返回一样,虽然它有个正则属性在里面,就是普通的数据返回,其他系统想怎么用就怎么用呗。他是接口,那么必定不是只为了我们页面存在的。如果它是页面 那必须用js正则。

A: 对,使用者自己改又说明api不易用。

B:这个问题从不同的角度看都有一定道理,我都认可。

写在讨论后

A 后来提到的接口可用性确实是一个问题,当初我考虑的时候也忽略的了这个方面,如果是作为一个接口的消费者来说这个接口是不友好。不知道各位大神,各位看官怎么看这个问题?

移动H5页面开发多屏适配 - 方案篇

移动H5页面开发多屏适配 - 方案篇

此篇总结是在学习了 viewport 基础知识,再参考了淘宝的 lib.flexible 可伸缩布局这个库,自己推演了 lib.flexible 是怎么作出这个解决方案的。

上一篇我们说过,对于 device-width 相同但是分辨率不同的手机我们可以通过设置 meta viewport 把移动页面的宽度归一到一个统一的宽度(这样一套布局就可以适用不同分辨率的手机)。但是现在不但 iphone 阵营自己出了好几个 device-width (320px,375px,414px),android 阵营更时百花齐放。那么我们对于不同的宽度的页面我们希望如果能用一套 css 搞定。最容易想到的就是使用百分比来设置尺寸。但是 css 百分比是根据父元素的尺寸来计算,而不是根元素譬如 viewport,这样对嵌套过深的元素计算尺寸非常不友好。同理使用 em 单位也会产生同样的问题。幸好 css3 出了一个新单位 rem,我简单的介绍下 rem 的规则:根据根元素(html)的字体大小来计算当前尺寸。譬如说 html 这个元素的 font-size 设了 10px,那么当前页面 1rem 就是 10px,2rem 就是 20px,如果 html 元素的 font-size 设置了 75px,则当前页面 1rem = 75px,2rem = 150px。

如果我们把页面宽度分成 100 份,把 html 的 font-size 设置成 viewport-width/100(px),则当前页面 1rem 就等于 1% 页面宽度,这样使用 rem 作为单位开发就相当于用百分比单位来设置尺寸了。如果愿意可以把所有的尺寸都转成百分比布局,那么所有不同宽度的页面都可以用一套 css 搞定。这个方案是可以实现的,只要把 html 的 font-size 设置成 document.documentElement.clientWidth/100(px)。

那是不是简单的写下如下的代码就搞定了多屏适配呢?

<html style="font-size:(document.documentElement.clientWidth/100)px">
  <head>
    <meta name="viewport" content="width=device-width">
    <style>
      div {
        width: 50rem;
      }
    </style>
  <head>
</html>

这样的方案在高分辨率的手机上会有一系列的问题:

  1. 在大屏幕高分辨率的手机上,以 320px 为页面宽度布局,元素过大,而且对于设计不友好(空间太小)。
  2. 会出现 1px 占2个或者多个物理像素的情况,无法做到对设计稿高度还原。
  3. 譬如在 640x960 分辨率的手机上如果用 320px 布局,页面上有《img src="xxx.png" style="width:25px;height:25px"/》,如果切图使用 25x25 的图片,会产生模糊的情况,因为其实高分辨率手机是把图片放大了,原因大家网上搜下,这不是这篇主要总结的问题。

针对第一个问题高分辨率手机我们可以设置 initial-scale 来把初始化页面缩小。打个比方 640x960 分辨率的手机设置 <meta name="viewport" content="width=device-width, initial-scale=0.5">,那么页面初始化宽度就是 750px。

针对第二个问题也可以通过把页面宽度设置成同手机分辨率宽度一样,来做到 css 像素和物理像素 1:1 来真实还原设计稿。问题是我怎么知道手机分辨率是多少,且来看 window.devicePixelRatio: 他是密度无关像素(dip)和物理像素比(我们俗称的 dpr)。举个例子如果 device-width 是 320,window.devicePixelRatio = 2 说明手机分辨率是 640xYYY,window.devicePixelRatio = 3 说明手机分辨率是 960xYYY。那么我们根据自己的项目需求针对不同的分辨率的手机对上面的方案可以做一个改进,这次我们要动态生成 meta(下面是伪代码,只是为了说明):

<html style="font-size:(document.documentElement.clientWidth/100)px">
  <head>
    <script>
      var deviceWidth = document.documentElement.clientWidth;
      var dpr = window.devicePixelRatio;
      var scale = 1 / dpr; // 如果我们做到 dip 和物理像素 1:1 
      var metaEl = document.createElement('meta');
      metaEl.setAttribute('name', 'viewport');
      metaEl.setAttribute('content', 'width=device-width, initial-scale=' + scale );
      document.firstElementChild.appendChild(metaEl);
    </script>
    <style>
      div {
        width: 50rem;
      }
    </style>
  <head>
</html>

当然这个 scale 怎么设置可以根据项目具体调整。

针对第三个问题我们的解决方案是不同分辨率,加载不同的图片。还是那上面第三点问题中的例子,如果 dpr = 2,那么我就提供一个 50x50 的图片放在你 25x25 的 img 元素里面,这样就能解决图片模糊的问题。但是如果每个图片都需要判断 dpr 动态设置 img 的 src,那么写起来是很麻烦,是否能有方案统一处理?有!把 img 全部转换成 background-image 然后用 css 来统一处理,看下代码:

<html>
  <head>
    <script>
      var docEl = document.documentElement
      var deviceWidth = docEl.clientWidth;
      var dpr = window.devicePixelRatio;
      var scale = 1 / dpr; // 如果我们做到 dip 和物理像素 1:1 
      var metaEl = document.createElement('meta');
      metaEl.setAttribute('name', 'viewport');
      metaEl.setAttribute('content', 'width=device-width, initial-scale=' + scale );
      docEl.firstElementChild.appendChild(metaEl);
      // 给 html 添加 font-size 和 data-dpr
      docEl.style.fontSize = document.documentElement.clientWidth/100 + 'px';
      docEl.setAttribute('data-dpr', dpr);
    </script>
    <style>
      div {
        width: 50rem;
      }
      .page {
        width: 90rem;
        height: 100rem;
        background-image: url(bg.png) /* 25x25 图片 */
      }
      [data-dpr="2"] .page {
        background-image: url([email protected]) /* 50x50 图片 */
      }

      [data-dpr="3"] .page {
        background-image: url([email protected]) /* 75x75 图片 */
      }
    </style>
  <head>
</html>

这样不同分辨率的页面自动加载不同的图片,解决了第三个图片模糊的问题。多屏适配的方案大致的内容就都在这里了,但是我们参看 lib.flexible 库,它对字体推荐是使用 px 而非 rem,那么针对字体 css 同样需要:

div {
    width: 1rem; 
    height: 0.4rem;
    font-size: 12px; /* 默认写上dpr为1的fontSize */
}

[data-dpr="2"] div {
    font-size: 24px;
}

[data-dpr="3"] div {
    font-size: 36px;
}

网上搜了下,看到给出的理由是 :

设计师原本的要求是这样的:任何手机屏幕上字体大小都要统一 (注意,字体不可以用rem,误差太大了,且不能满足任何屏幕下字体大小相同)

我觉得这个说法也是合理的,所以最终多屏适配的方案的细节还是需要大家根据自己的项目进行微调。

移动H5页面开发多屏适配的方案内容总结到这里,我了解了大致的原理就可以放心使用 lib.flexible 了。

React 奇技淫巧 - defaultValue 和虚拟 dom diff 算法实现表单重置

React 奇技淫巧 - defaultValue 和虚拟 dom diff 算法实现表单重置

我们知道 React 的标准模式是单向数据流,而其表单项通常需要监听 onChange 事件,然后通过改变外部的 value 来回写表单项的 value,譬如如下 input

class App extends React.Component {
  constructor( props ) {
    super( props );
    this.state = {
      inputValue: 'default'
    }

    this.inputChangeHandler = ( e )=>{
      this.setState( {
        inputValue: e.target.value
      } );
    }
  }
  render() {
    return (
      <div>
        <form>
          <input
            value={ this.state.inputValue }
            onChange={ this.inputChangeHandler }
          />
        </form>
      </div>
    )
  }
}

如果表单有很多表单项,那么这种标准的做法需要你写很多个 state 的属性和很多个 onChange 监听函数,这是一个体力活儿。但是一般的表单应用其实不需要实时监控表单项的用户输入,用 defaultValue 足以,在表单项目 onBlur 或者最后提交的时候一次验证获取用户输入即可,譬如:

class App extends React.Component {
  constructor( props ) {
    super( props );

    this.submit = ( e )=>{
      let userInputValue = this.refs.userInput.value;
      // 1. 验证 userInputValue
      // 2. 提交表单
    }
  }
  render() {
    return (
      <div>
        <form>
          <input 
            ref="userInput"
            defaultValue="default"
          />
          <button onClick={ this.submit }>提交</button>
        </form>
      </div>
    )
  }
}

这样就可以少写不少代码,当然你可以写一些工具去批量添加所有的 onChange 事件监听函数和对应的 state 的属性,譬如 redux-form。(回头一想,这种写法在提交时候也需要写很多获取用户输入的代码,如果使用第一种正模式,那么提交时候只需要获取 state 就可以了,不过这里先不讨论这些)

对于一个表单而言,通常还需要重置功能(reset),如果是第一种正模式的写法,我们只要保存一份初始化的默认值,在用户点击到了重置后,通过 setState 设回去就行了。但是如果使用第二种 defaultValue 的写法,那么就没有办法了,因为 defaultValue 只在第一次创建虚拟 dom 的时候有作用,如果 dom 不改变你改变 defaultValue 是没有用的。这个时候该怎么办呢?

嘿嘿!这个时候我们就可以用到这个奇技淫巧了。既然 defaultValue 是在创建虚拟 dom 的时候有用,那么我们在用户点击重置的时候让 React 重新创建这些表单项的虚拟 dom 不就好了么。根据 React 虚拟 dom diff 的算法,只要改变 dom 节点的类型就能促使在 diff 的时候重新创建虚拟 dom。具体的写法我们就用代码来演示下:

class App extends React.Component {
  constructor( props ) {
    super( props );
    // fieldSetWrapperType 是一个标志位属性,render 中会根据这个变量的值的不同,渲染不同的元素
    this.fieldSetWrapperType = 'div';
    this.submit = ( e )=>{
      let userInputValue = this.refs.userInput.value;
      // 1. 验证 userInputValue
      // 2. 提交表单
    }
    this.reset = ()=>{
      // 点击重置,改变标志位
      this.fieldSetWrapperType = this.fieldSetWrapperType === 'div' ? 'section' : 'div';
      // 强制刷新这个组件
      this.forceUpdate();
    }
  }
  // 把表单项的渲染抽象到一个方法中,避免重复编码
  renderFieldSet() {
    return (
      <input 
        ref="userInput"
        defaultValue="default"
      />
    );
  }
  render() {
    return (
      <div>
        <form>
          {
          /* 根据 fieldSetWrapperType 值的不同,渲染不同的元素(表单项的 wrapper 元素) */
          this.fieldSetWrapperType === 'div' ? 
          <div className="wrapper">{ this.renderFieldSet() }</div>
          :
          <section className="wrapper">{ this.renderFieldSet() }</section>
          }
          <button onClick={ this.submit }>提交</button>
          <button onClick={ this.reset }>重置</button>
        </form>
      </div>
    )
  }
}

思路就是在表单项外面包一层元素,每次点击重置后改变一个变量,再强制刷新这个组件,组件根据这个变量不同的值把这个包装元素的 type 改变,那么它下面的所有表单项的虚拟 dom 都会被重新创建,达到了重置的目的。不过这个效果依赖于 React 虚拟dom diff 算法。如果以后算法改变了,那么可能就失效了,而且这个写法是反模式的,我初衷是想在处理巨型表单时候少写点代码偷懒用。如果使用时候出现什么副作用,鄙人概不负责。

此技巧在写文章时 React 正处于 15.4.x 的版本

ie7下position:absolute,font-size:italic的元素宽度解析错误

ie7下position:absolute,font-size:italic的元素宽度解析错误

今天在360浏览器的兼容模式下碰到个奇葩的问题,body的宽度好好的,html这个根元素的宽度楞是比浏览器宽度大出好多,导致产生了横向滚动条。我看了下360的兼容模式用的是ie7的文档模式,所以我试着按症状google,百度了好久,终于找到了问题的原因:

原来是我html中某个绝对定位的元素的字体设置成了斜体,而ie下这种元素的宽度并不是想象中的自包裹,而是会很宽(具体的宽度是按什么来没有查到,估计是按浏览器宽度100%来解析的)

来看下代码(可以放到ie7上看下font-style是italic和normal的区别):

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <style type="text/css">
    p {
        position: absolute;
        top: 0;
        right: 2px;
        font-style: italic;
    }
    </style>
</head>
<body>
<p>sadfsadfsalkjsdfs</p>
</body>
</html>

解决方案有:

  1. 设置元素的overflow: auto
  2. 给元素指定宽度width: xxx

传说中的zoom: 1并没起到作用-_-||

参考:
https://muffinresearch.co.uk/bug-ie7-absolutely-positioned-italics/

Firefox 动态修改 favicon 不显示问题

Firefox 动态修改 favicon 不显示问题

1 问题

项目中需要动态改变页面的 favicon,icon 文件存储在阿里云(OSS)上。改变 favicon 的方式是通过获取 link 元素,把 icon 的 url 赋值给其 href。在 chrome 上测试可以正确显示,但是在 firefox 上却没有显示图标。

2 分析

动态改变 favicon 以前我也没有做过,秉承不放过一个可能的 debug 策略,首先怀疑 firefox 不支持动态修改 link[rel=icon]。于是打开 fiddler 查看在动态赋值图标 url 后有没有图片请求发出(firefox自带的开发者工具中没有显示 link[rel=icon] 的请求)。在 fiddler 中发现 firefox 是发送了请求的,那么接下来就看下返回了。此时发现阿里云返回的是403,估计是请求头缺少了什么东西导致请求被阿里云屏蔽了。查看图片的请求头发现缺少 Referer,估计就是这个影响了。我重新打开 chrome 看了下图片的请求是有 Referer 的,那么基本上可以确定这是 firefox 的一个 bug。最终我去到阿里云的管理界面看了下防盗链的设置界面,里面选择的是 Referer 不能为空,这下证实了我的猜测:由于 firefox 的加载 favicon 的时候,请求头缺少了 Referer,被阿里云防盗链了。

3 解决方案

解决方案其实是在 google 的时候发现的,link[rel=icon] 的 href 不但可以写 url,还可以写图片的 base64 编码,和 img 标签一样。那么我是否可以用 img 标签加载这个图标然后转成 base64 编码再赋值给 link[rel=icon] 呢?说干就干,依稀记得转 base64 编码可以使用 canvas.toDataURL 这个 API 来转,查了下浏览器支持情况,幸好项目要求支持的浏览器都支持。代码如下:

let ImageContentTypeMap:any = {
  jpg: 'image/jpeg',
  jpeg: 'image/jpeg',
  png: 'image/png',
  ico: 'image/x-icon',
  gif: 'image/gif'
}

function parseSuffix( url:string ):string {
  if ( url ) {
    let lastDotIndex = url.lastIndexOf( '.' );
    if ( lastDotIndex >= 0 ) {
      return url.substr( lastDotIndex + 1 ).toLowerCase();
    } else {
      return '';
    }
  } else {
    return '';
  }
}
/*
请忽略为啥要用 promise,只是 copy 出来懒得改了
*/
function imageToBase64( url, width, height ) {
  return new Promise( function( resolve, reject ) {
      let img = new Image; 
      img.crossOrigin = 'Anonymous'; 
      img.onload = function() {
          var canvas = document.createElement( 'canvas' );
          canvas.width = width;
          canvas.height = height;
          var ctx = canvas.getContext( '2d' );
          ctx.drawImage( img, 0, 0 );
          let imgSuffix = parseSuffix( url );
          if ( imgSuffix ) {
            let contentType = ImageContentTypeMap[imgSuffix];
            if ( contentType ) {
              resolve( canvas.toDataURL( contentType ) );
            } else {
              reject( new Error('Can not parse contentType of favicon') )
            }
          } else {
            reject( new Error('Can not parse suffix of favicon file') )
          }
      }
      // TODO: 当图片加载失败的情况
      img.src = url;
  } )
}

let iconLink = document.getElementById('iconLink');
imageToBase64( '....../xxx.ico', 16, 16 ).then( imgStr=>{
    iconLink.href = imgStr;
} ).catch( e=>{
    console.error( 'Parse favicon: ', e.stack );
} )

测试结果是 icon 图片是显示出来了,但是只显示了部分图片(囧)。那...应该可能是图标有问题?遂找了另外一个 png 的图标试试了,测试通过。难道是 firefox 中使用 img 加载 ico 文件有问题?只能上 google 大法了,经过了一番搜索和测试基本能确定下来,firefox 显示 ico 文件是没有问题的,但是 canvas.toDataURL 这个 API 在不同的浏览器中支持的图片格式有偏差,而且格式类型支持的都有限。所以我们最初用的 ico 文件通过 canvas.toDataURL encode 之后的编码不是完全正确的。此时我想到两个解决方案:

  1. favicon 改换成 png 图片。
  2. 后台直接给出的不是 icon 的 url,而是 base64 的编码。(用 ico 文件正确的 base64 编码做过测试,firefox 能够正确显示图标)

至此 firefox 不支持动态修改 favicon 的问题算解决了。

延伸资料

Favicon 历史 - https://en.wikipedia.org/wiki/Favicon
各浏览器支持显示的图片格式 - https://en.wikipedia.org/wiki/Comparison_of_web_browsers#Image_format_support

5人以下团队 leader 职责(构想草图)

5人以下团队 leader 职责(构想草图)

本人没有作为长期 leader 一线经验,这是我自己在工作中的观察和构想,如果让我做一个5人以下前端团队 leader 我需要做的一些事情。

  1. 分配任务

在开需求会议接任务时,一定要带上对应的开发人员。因为据我自己的经验,理解需求对开发来说很重要,能减少因为沟通理解产生的逻辑 bug,能够在需求会议上就发现需求不严谨或者逻辑有问题的地方,从技术上可能提供更优的解决方案。并且为了提高开发人员的这个意识,会在平时和他们强调以上的这几个点。

在分配任务时要把业务相同的模块劲量划分给同一个人,这样可以避免很多沟通成本,利于此人产出更高质量的代码(因为相同逻辑的的模块编码前需要统一规划,如果多人做,这几个人不但要沟通,还要处理风格不同的,代码质量不同的问题)。

  1. 技术选型

评估团队的技术能力,选择最适合的人做首次选型(这个人可能是在某方面经验最丰富,可能是技术最强的),然后让此人做完选型及基础“架构”,全队评估。一来是为了让所有队员知道如此选型和架构背后的逻辑(同时间接可以提高其他同事的技术视野),二来可以帮助选型者弥补欠考虑的地方。

  1. 技术规范

和第二点类似也是找经验最丰富的人来做(无论是队员或者是 leader),然后也是评审,为了达到的目的和第二点也相同。但是技术规范这点在实施时要充分考虑队员的顾虑,因为在规范上有些东西比较主观,如果碰到特肘的技术人员不认可某些规范,也要做好妥协的准备。平时是否要长期念叨良好规范这件事情我也没有经验,怎么做我也不知道。

  1. 成员任用

不要怕团队成员技术超过自己而打压队员,leader 的任务之一就是服务团队,最大化团队战斗力。这点是最近在书中看到的,个人感觉是很有道理,所以就列一下。

  1. 新人培养

个人觉得一直手把手教是不太可取的,新人刚刚上手的时候可以这样,但是需要在平时逐渐培养自己就工作中就某个知识点进行自我学习的能力,如何debug 的能力。这些能力也不是一蹴而就的,可以通过布置非公司任务给新人,让其完成,并在完成后进行评审来帮助其进步。

  1. 任务排期

这个我也是没有啥太多的经验,但是有一点是肯定要预留一点时间(无论是 leader自己排还是队员自己报工期),而且根据队员不同的特点定期检查工作进度是否符合预期(技术能力强,做事一贯靠谱的不用天天盯,一个礼拜关心下就可以了,新人起初最好天天关心下,一来保证团队的整体进度,二来可以考察新人的工作能力,以便今后的任务能够准确评估工期。但是天天盯需要注意方式方法,不要引起新人的反感)。

  1. 产出质量

作为小团队的 leader 一定要对业务熟悉,因为队员在开发过程中你是团队的首席技术业务官,你要解决队友因为业务不熟悉产生的技术偏差,产品经理没有你懂技术,和技术人员交流肯定不如你。 对代码虽然不需要每行都了解,但是对业务流程一定要清楚。只有 leader 对项目的全局了解,才能把控产出的质量。

React 更新视图过程

React 更新视图过程

说在前面,着重梳理实际更新组件和 dom 部分的代码,但是关于异步,transaction,批量合并新状态等新细节只描述步骤。一来因为这些细节在读源码的时候只读了部分,二来如果要把这些都写出来要写老长老长。

真实的 setState 的过程:

setState( partialState ) {
  // 1. 通过组件对象获取到渲染对象
  var internalInstance = ReactInstanceMap.get(publicInstance);
  // 2. 把新的状态放在渲染对象的 _pendingStateQueue 里面 internalInstance._pendingStateQueue.push( partialState )
  // 3. 查看下是否正在批量更新
  //   3.1. 如果正在批量更新,则把当前这个组件认为是脏组件,把其渲染对象保存到 dirtyComponents 数组中
  //   3.2. 如果可以批量更新,则调用 ReactDefaultBatchingStrategyTransaction 开启更新事务,进行真正的 vdom diff。
  //    |
  //    v
  // internalInstance.updateComponent( partialState )
}

updateComponent 方法的说明:

updateComponent( partialState ) {
  // 源码中 partialState 是从 this._pendingStateQueue 中获取的,这里简化了状态队列的东西,假设直接从外部传入
  var inst = this._instance;
  var nextState = Object.assign( {}, inst.state, partialState );
  // 获得组件对象,准备更新,先调用生命周期函数
      // 调用 shouldComponentUpdate 看看是否需要更新组件(这里先忽略 props 和 context的更新)
  if ( inst.shouldComponentUpdate(inst.props, nextState, nextContext) ) {
    // 更新前调用 componentWillUpdate
    isnt.componentWillUpdate( inst.props, nextState, nextContext );
    inst.state = nextState;
    // 生成新的 vdom
    var nextRenderedElement = inst.render();
    // 通过上一次的渲染对象获取上一次生成的 vdom
    var prevComponentInstance = this._renderedComponent; // render 中的根节点的渲染对象
    var prevRenderedElement = prevComponentInstance._currentElement; // 上一次的根节点的 vdom
    // 通过比较新旧 vdom node 来决定是更新 dom node 还是根据最新的 vdom node 生成一份真实 dom node 替换掉原来的
    if ( shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement) ) {
      // 更新 dom node
      prevComponentInstance.receiveComponent( nextRenderedElement )
    } else {
      // 生成新的 dom node 替换原来的(以下是简化版,只为了说明流程)
      var oldHostNode = ReactReconciler.getHostNode( prevComponentInstance );
      // 根据新的 vdom 生成新的渲染对象
      var child = instantiateReactComponent( nextRenderedElement );
      this._renderedComponent = child;
      // 生成新的 dom node
      var nextMarkup = child.mountComponent();
      // 替换原来的 dom node
      oldHostNode.empty();
      oldHostNode.appendChild( nextMarkup )
    }
  }
}

接下来看下 shouldUpdateReactComponent 方法:

function shouldUpdateReactComponent(prevElement, nextElement) {
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }

  var prevType = typeof prevElement;
  var nextType = typeof nextElement;
  if (prevType === 'string' || prevType === 'number') {
    return (nextType === 'string' || nextType === 'number');
  } else {
    return (
      nextType === 'object' &&
      prevElement.type === nextElement.type &&
      prevElement.key === nextElement.key
    );
  }
}

基本的思路就是比较当前 vdom 节点的类型,如果一致则更新,如果不一致则重新生成一份新的节点替换掉原来的。好了回到刚刚跟新 dom node这条路 prevComponentInstance.receiveComponent( nextRenderedElement ),即 render 里面根元素的渲染对象的 receiveComponent 方法做了最后的更新 dom 的工作。如果根节点的渲染对象是组件即 ReactCompositeComponent.receiveComponent,如果根节点是内置对象(html 元素)节点即 ReactDOMComponent.receiveComponent。ReactCompositeComponent.receiveComponent 最终还是调用的上面提到的 updateComponent 循环去生成 render 中的 vdom,这里就先不深究了。最终 html dom node 的更新策略都在 ReactDOMComponent.receiveComponent 中。

class ReactDOMComponent {
  // @param {nextRenderedElement} 新的 vdom node
  receiveComponent( nextRenderedElement ) {
    var prevElement = this._currentElement;
    this._currentElement = nextRenderedElement;

    var lastProps = prevElement.props;
    var nextProps = this._currentElement.props;
    var lastChildren = lastProps.children;
    var nextChildren = nextProps.children;
    /*
      更新 props
      _updateDOMProperties 方法做了下面两步
      1. 记录下 lastProps 中有的,nextProps 没有的,删除
      2. 记录下 nextProps 中有的,且与 lastProps中不同的属性,setAttribute 之
    */
    this._updateDOMProperties(lastProps, nextProps, transaction);

    /*
      迭代更新子节点,源代码中是 this._updateDOMChildren(lastProps, nextProps, transaction, context);
      以下把 _updateDOMChildren 方法展开,对于子节点类型的判断源码比较复杂,这里只针对string|number和非string|number做一个简单的流程示例
    */
    // 1. 如果子节点从有到无,则删除子节点
    if ( lastChildren != null && nextChildren == null ) {
    
      if ( typeof lastChildren === 'string' | 'number' /* 伪代码 */ ) {
        this.updateTextContent('');
      } else {
        this.updateChildren( null, transaction, context );
      }
    }
    // 2. 如果新的子节点相对于老的是有变化的
    if ( nextChildren != null ) {
      if ( typeof lastChildren === 'string' | 'number' && lastChildren !== nextChildren /* 伪代码 */ ) {
        this.updateTextContent('' + nextChildren);
      } else if ( lastChildren !== nextChildren ) {
        this.updateChildren( nextChildren, transaction, context );
      }
    }
  }
}

this.updateChildren( nextChildren, transaction, context ) 中是真正的 diff 算法,就不以代码来说了(因为光靠代码很难说明清楚)

先来看最简单的情况:
例A:
a
按节点顺序开始遍历 nextChildren(遍历的过程中记录下需要对节点做哪些变更,等遍历完统一执行最终的 dom 操作),相同位置如果碰到和 prevChildren 中 tag 一样的元素认为不需要对节点进行删除,只需要更新节点的 attr,如果碰到 tag 不一样,则按照新的 vdom 中的节点重新生成一个节点,并把 prevChildren 中相同位置老节点删除。按以上两个状态的 vdom tree,那么遍历完就会记录下需要做两步 dom 变更:新增一个 span 节点插入到第二个位置,删除原来第二个位置上的 div。

再来看两个例子:
例B:
b
遍历结果:第二个节点新增一个span,删除第二个div和第四个div。

例C:
c
遍历结果:第二个节点新增一个span,第四个节点新增一个div,删除第二个div。

我们看到对于例C来说其实最便利的方法就是把 span 插入到第二的位置上,然后其他div只要做 attr 的更新而不需要再进行位置的增删,如果 attr 都没有变化,那么后两个 div 根本不需要变化。但是按例A里面的算法,我们需要进行好几步的 dom 操作。这是为算法减少时间复杂度,做了妥协。但是 react 对节点引入了 key 这个关键属性帮助优化这种情况。假设我们给所有节点都添加了唯一的 key 属性,如下面例D:
例D:
d
我们在遍历过程中对所要记录的东西进行优化,在某个位置碰到有 key 的节点我们去 prevChildren 中找有没有对应的节点,如果有,则我们会比较当前节点在前后两个 tree 中相对位置。如果相对位置没有变化,则不需要做dom的增删移,而只需要更新。如果位置不一样则需要记录把这个节点从老的位置移动到新的位置(具体算法需要借助前一次dom变化的记录这里不详述)。这样从例C到例D的优化减少了 dom 节点的增删。

但是 react 的这种算法的优化也带来了一种极端的情况:
例E:
e
遍历结果:3次节点位置移动:2到1,1到2,0到3。

但是其实这里只需要更新每个节点的 attr,他们的位置根本不需要做变化。所以如果要给元素指定 key 最好避免元素的位置有太多太大的跃迁变化。

基本上 setState 之后到最终的 dom 变化的过程就是这么结束了。

后记:
梳理的比较简单,很多细节我没有精力作一一的总结,因为我自己看源码看了好久,代码中涉及到很多异步,事务等等干扰项,然后我自己又不想过多的借助现有的资料-_-。当我快要把最后一点写完的时候发现 pure render 专栏的作者陈屹出了一本《深入React技术栈》里面有相当详细的源码分析,所以我感觉我这篇“白写”了,贴出这本书就可以了,不过陈屹的这本书是良心之作,必须安利下。

letter-spacing和text-align:center的矛盾

letter-spacing和text-align:center的矛盾

1 问题

有的时候ui给的设计搞中会出现2个字之间有比较宽的间距,特别是button。

button示例图

我们可以用在文字之间加 ,但是这种做法一来没有办法精确的控制文字间的间距,二来如果文字很多那么加空格的加死了。我们的css中有letter-spacing可以指定文字间的间距。

<style>
    button {
        letter-spacing:4px;
        text-align:center;
    }
</style>
<button>搜索</button>

我们看到文字间的间距是增加,但是为什么文字看上去没有居中呢,有了一定的偏差呢?

2 分析

我们用chrome的developtool看下点击下文字,看到文字的选中框,原来letter-spacing不但在各个文字间添加了间距,在最后一个字的后面还添加了一个间距,所以在text-align时候自然产生了偏差。

我们来看下letter-spacing的规范

Because letter-spacing is not applied at the beginning or end of a line, text always fits flush with the edge of the block

规范中指出letter-spacing不允许在文字的头尾添加间隔。所以浏览器的实现其实并不符合规范。

3 解决方案

那么我们可以通过添加padding-left或者text-indent来在文字的头可以营造一个间隔来达到使整个文字居中的效果

<style>
    button {
        letter-spacing:4px;
        text-align:center;
        padding-left:4px;
        /* text-indent:4px; */
    }
</style>
<button>搜索</button>

button示例图

参考:

http://stackoverflow.com/questions/6315491/conflict-between-letter-spacing-and-text-aligncenter
http://dev.w3.org/csswg/css-text-3/#letter-spacing-property

防止网页在微信中下拉露黑底方案调研

防止网页在微信中下拉露黑底方案调研

问题

如果用微信打开网页,如果我们在网页第一屏继续下拉的情况下,顶部 webview 上部会有一块黑底显示出来,类似于原生 App 的下拉更新的样子。现在我们希望禁止这个行为。

分析

通过谷歌大法发现,这个行为应该是 touchmove 事件的默认行为。要禁止这个黑底出现,只要监听 touchmove 事件,在其中 preventDefault() 即可。但是如果简单的禁止了 touchmove 事件的默认行为,那么网页本身就无法进行上下滚动。那么我们是不是可以检测当前网页是否已经滚动到最上部,如果滚动到了最上面(无法继续下拉)那么我们再禁止 touchmove 的默认行为。

解决方案

我们假设页面有一个容器 div#wrapper,它负责整个页面的滚动。为什么不用 body,因为谷歌大法告诉我,有的版本的浏览器会不让 js 去禁止 body touchmove 的默认行为。所以我们大致的代码如下:

var wrapper = document.getElementById( 'wrapper' );
var touchstartY;

wrapper.addEventListener( 'touchstart', function (ev) {
    var events = ev.touches[ 0 ] || ev;
    touchstartY = events.clientY;
}, false );

wrapper.addEventListener( 'touchmove', function (ev) {
    var events = ev.touches[ 0 ] || ev;
    // 下拉时并且页面已经到顶部时
    if ( events.clientY > touchstartY && (wrapper.scrollTop === 0) ) {
      ev.preventDefault();
    }
}, false );

我在 ios10 上测试成功,但是在荣耀8 的 EMUI5(Android7)系统上则有一种情况还是会出现黑底,就是当页面位置在即将到顶部但是还有一段距离,用户开始下拉,那么当页面滚到顶部时用户不放手继续下拉,则依然会出现黑底。我原先以为是 touchmove 触发的频率过低所致,所以我故意慢一点拉然后记录各个变量的值,我发现在 touchmove 时即使events.clientY >= touchstartY && (wrapper.scrollTop === 0) 这个条件为true了,我停顿一下手指再继续往下拉(停顿时候手指不离开屏幕)依然会出现黑底,意味着这种情况下 preventDefault() 没能阻止黑底的出现。那我此时基本可以认为这是微信 webview 在 android 上的一个 bug。

然后我找了另一种方案来禁止滚动就是当wrapper.scrollTop === 0时候,给 wrapper 添加 touch-action: none;的样式,依然不能解决这个问题。所以经过一天的调研,暂时只能在水果机的微信上实现这个功能。

最后水果机微信的 webview 在页面滚动到最底部继续上拉的情况也会出现黑底,那么我们在多加一个判断,最终的代码初稿如下:

var wrapper = document.getElementById( 'wrapper' );
var touchstartY;

wrapper.addEventListener( 'touchstart', function (ev) {
    var events = ev.touches[ 0 ] || ev;
    touchstartY = events.clientY;
}, false );

wrapper.addEventListener( 'touchmove', function (ev) {
    var events = ev.touches[ 0 ] || ev;
    var scrollTop = wrapper.scrollTop,
		var	offsetHeight = wrapper.offsetHeight,
		var	scrollHeight = wrapper.scrollHeight;
    if ( events.clientY > touchstartY && (wrapper.scrollTop === 0) ) {
    // 下拉时并且页面已经到顶部时
      ev.preventDefault();
    } else if ( events.clientY < touchstartY && (scrollTop + offsetHeight >= scrollHeight) ) {
    // 上拉时并且页面已经到底部时
      ev.preventDefault();
    }
}, false );

后记

我初次接触移动端,以上是我个人的调研结果,不排除有成熟的原生解决方案(当然如果用 IScroll 模拟滚动是可以解决这个问题的)。如果有有经验的同志,希望不惜赐教。

HTTP 协议的缓存机制概述

HTTP 协议的缓存机制概述

HTTP 协议的缓存机制涉及到多个请求头字段,而且整个缓存机制的细节行为也存在各种情况的差异,譬如说什么时候访问本地缓存不发送请求,什么时候发送请求查看资源是否更新,获取 response 什么情况下更新缓存等。以前我对此一知半解只是笼统的知道一些概念,譬如 Cache-Control 可以控制缓存的时间和是否需要缓存,但是缓存过期后的行为,有缓存后浏览器是否有 http 请求都不甚了解。所以特地 google 下,此篇是对此的知识梳理。

协议概述

什么情况下可以使用本地缓存?譬如说我们用 get 方式请求了一个资源 http://mytest.domain.com/static/images/bg.png,那么我们下次再请求这个图片资源的时候符合哪些条件可以使用本地缓存呢?

  • url 必须是 http://mytest.domain.com/static/images/bg.png,如果是 http://mytest.domain.com/static/images/bg.png?t=12312321 就会发起新的请求,因为 url 不同。
  • 发送请求的 method 必须可被缓存,譬如 get。
  • 第一次请求 response(即本地缓存)如果有一个 Vary 头,他的值列出的是一些列 http header,第二次请求的请求头中那些在 Vary 值中所列的头,必须和第一次请求相同(具体规则)。
  • 第二次请求不包含请求头 Pragma: no-cache
  • 第二次请求不包含请求头 Cache-Control: no-cache|max-age=0
  • 第一次请求的 response(即本地缓存)不包含 Cache-Control: no-cache
  • 本地缓存没有过期
  • 或者虽然本地缓存已经过期,但是服务器验证缓存和服务器资源一致,允许使用本地缓存的情况(即获得304 response)

注1:上述任何提一个条件都可以被 cache-control extension 覆盖
注2:response header中不仅仅可以 Cache-Control: no-cache,还可以 Cache-Control: no-cache="Set-Cookie" 详见

如何计算本地缓存是否过期

浏览器是通过比较缓存剩余有效时间和当前缓存已存在时间来判断的:response_is_fresh = (freshness_lifetime > current_age)freshness_lifetime 取值优先级次序如下列表所示(排在上面的优先级越高):

  1. Cache-Control: s-maxage=xx
  2. Cache-Control: max-age=xx
  3. Expires: xxxxx
  4. 按规则进行计算(推测)

注1;如果有多个重复的上述头,那么是非法的,视作 response(资源)过期

freshness_lifetime 计算(推测)规则:

规范并没有给出具体的算法,但是给出了最坏情况(but does impose worst-case constraints on their results),如果 response(资源)有 Last-Modified 头,那么推荐用从当前到lastmodified这个时间段的 10% 作为 freshness_lifetime,并且 response(资源)的 current_age 如果已经超过24小时,必须在这个 response 上加上113 warn-code头。很少有浏览器实现了freshness_lifetime的自助计算(推测),所以还是还是鼓励给出上述显式的1 - 3三种情况。

current_age 计算规则:

泛泛来说就是 response(资源)在本地的驻足时间加上网络传输时间,因为网络传输时间的计算有多个条件,规范实在看的我头晕,所以具体计算规则详见规范

本地缓存过期,如何通过服务器验证缓存的是否依然有效(即304的情况)

首先,如果第一次的 response(资源)头中显示申明了一些禁止缓存的头(譬如:"no-store" or "no-cache" 等等),就不存在过期不过期的问题,因为这个资源不允许缓存。其次,过期缓存也不一定不可用,如果在断网或者第二次请求带上 max-stale 这个请求头,那么浏览器可以使用过期的缓存(masx-stale 表示在缓存过期后多少时间内浏览器依然可以使用缓存)。碰到过期缓存,浏览器可以发送一个条件请求(conditional request)。这个请求的 url 依然是第一次请求的 url,只是会带上些当前资源的一些信息,以供服务器验证这个缓存是否依然可用还是需要更新:

  1. response(资源)的 Last-Modified 头所带的值会放到条件请求的 If-Modified-Since 头中,或者是 If-Unmodified-Since 又或者 If-Range。
  2. response(资源)的 ETag 头所带的值会放到条件请求的 If-None-Match 头中,或者 If-Match 又或者 If-Range。

服务器会根据不同的条件请求头来验证资源,具体的行为详见规范,这里不细致展开。针对条件请求,服务器返回会有三种情况:

  1. 一个带有304 status code 的返回。表示缓存可以被更新和重用。
  2. 一个带有 body 的完整的 response。表示用这个 response 作为请求的返回,并且视条件可以用这个完整的 response 替换浏览器原有的缓存(此资源)。
  3. 如果返回一个5xx的 response,那么浏览器可以选择就显示这个5xx的返回,或者使用本地缓存(尽管可能是过期的)- 规范没有规定应该选择哪种处理,应该是取决于浏览器的行为。

如果是一个304的返回,规范说这个返回可以更新本地缓存,更新策略分三种:

  1. 如果这个304 response 带有资源有效性的强验证头,那么浏览器会寻找本地缓存,寻找那些带有同样强验证头的缓存,然后用这个最新的 response 去更新这些匹配的缓存(同一个资源可能浏览器保存有多份缓存,譬如日期不同等)。
  2. 如果这个304 response 带有资源有效性的弱验证头,那么浏览器同样会找相匹配的缓存,但是只会更新最新的那条匹配的缓存。
  3. 如果这个304 response 没有带有任何资源有效性验证头,并且浏览器缓存只有一份,并且这份也同样没带有任何资源有效性验证头,那么浏览器就会用这个304 response,更新本地缓存。

协议流程图

假设第一次请求一个资源,返回 header 里面带上如下字段:
Cache-Control: max-age=600
Last-Modified: Wed, 28 Aug 2013 10:36:42 GMT
ETag: "124752e0d85461a16e76fbdef2e84fb9"

抛开细枝末节的东西,那么第二次请求通常大致流程图如下:

                  当前资源缓存是否过期:response_is_fresh = (freshness_lifetime > current_age)
                                            |
                               -----------------------------------
                              |                                   |        
                              是                                  否
                              |                                   |
                  发送请求,带上请求头                          从本地缓存中获取资源(不发请求)
                  If-Modified-Since: 此资源Last-Modified的值      
                  If-None-Match: 此资源ETag的值
                              |
                  服务器根据 If-Modified-Since 和 If-None-Match
                  两个值判断资源是否更新过
                              |
                  -------------------------
                 |                         |
                 是                        否
                 |                         |
返回一个 status code:200 的 response    返回一个 status code:304 的 response
response body 里面是请求的资源           response body 为空
                 |                         |
浏览器用 response body里面的资源         依然从本地缓存里面获取资源
替换本地缓存中的资源

特殊情况

当你去浏览器验证的时候可能会碰到一些特殊情况,就是缓存有效,但是你刷新浏览器依然发送的条件请求。其实是因为浏览器在请求头中加入了一些料,譬如: Cache-Control: max-age=0。你刷新的方式可以有很多种,譬如:按F5,按ctrl+F5,在地址栏按回车等等。这些不同的行为都会影响浏览器发送请求的行为。这里有一些参考《在浏览器地址栏按回车、F5、Ctrl+F5刷新网页的区别

参考资料

rfc7234

人民币符号的字符实体( Character Entity )

人民币符号的字符实体( Character Entity )

  1. 全角人民币符号: &#65509; - ¥
  2. 半角人民币符号:&#165; - ¥
  3. 日元符号:&yen; - ¥

其实 &yen; 和 &#165; 是同一个符号,也就是说如果用半角符号,那么人民币和日元的符号是一样的。
全角全角人民币符号在不同的字体下表现的不同,譬如宋体下就是一横,微软雅黑下就是两横。

编写「可读」代码的实践 - 接口篇

编写「可读」代码的实践 - 接口篇

本来想写一个怎么写好代码的主题,昨天突然看到了淘宝团队的这篇文章《编写「可读」代码的实践》,把我想到的没想到的基本都说了。然后我就想重复的内容就不说了(别人讲的比我透彻),我自己就补充下自己对于接口几点想法,然后标题呢,我就死皮赖脸的抱下大腿了。

修改接口

一般来说,对于现有系统我们原则上不主张修改已有接口,如果原有接口有问题或者没有办法满足要求了,那么我们会新增接口,然后在调用的地方替换掉老接口。如果直接修改现有接口会影响到所有调用此接口的地方,一旦修改后的接口出现问题,那么将影响整个系统的稳定性。而新增接口,如果出现问题只要通过简单的修改调用处的代码即可回退。如果一定要修改老接口,那么要遵循一定的原则:

  1. 输入输出不能有变化(此时有单元测试的好处就体现出来了)。
  2. 接口的行为不能有变化,同步的接口不能变成异步,反之亦然,原来不抛异常的不能变成抛异常。

接口实现要高内聚

这里高内聚主要谈的是接口要把自身的异常或者差异化在本身的实现中处理掉,不能把自身的问题传导到接口之外。譬如说前端通过浏览器获得的 language code,可能大小写和格式因每个浏览器的实现有差异化,那么我们封装一个接口 getLanguageCode 就要把这个差异化在接口内处理掉,保证这个接口返回的结果是标准化的,调用者拿到这个返回值在传给其他模块甚至后台时一定是可预测的值(符合规范)。如果你把自身的差异化扩散到其他接口,模块或者系统,带来得就不单单是可维护性的风险:

  1. 其他系统的维护者不一定明白为什么对这种情况需要做特殊处理,增加了维护的难度。
  2. 即使其他系统对你的输出做了特殊处理,但是一旦你的异常或者差异化变化了,那么修改会涉及到多个系统,整个系统的稳定性就降低了。
  3. 最坏的情况是某个系统的异常情况处理传播到了整个系统链的多个环节,一旦出现问题,非常难追查源头。

接口输出的一致性

笼统的说就是方法的返回类型最好能保持一致,即缺省的状态下和非缺省的状态下返回值的类型要一致,譬如getUsername,如果能获取到用户名则返回一个 string 的用户名,如果允许用户不填用户名,则此时调用 getUsername 应该返回空字符串 '',而不是 undefined 或者 null。这点在前端也是非常好用,通常获取到字符串后都需要在界面上展示,如果缺省返回 undefined,则需要再增加一次判断,当 undefined 的时候在页面上展示空字符串,如果返回类型一致就不需要这一步。

原始类型的返回值比较好统一,非原始类型就需要分情况讨论了。

第一种情况,返回类型的对象表示的是数据结构,譬如系统定义了用户可以有多个联系方式,那么 getUserContact 定义为返回一个数组,数组项是用户的填写的手机号,当用户未填或者查询不到手机号时,getUserContact 应该返回空数据 [] 而不是 undefined。这么做的好处是,一般调用返回数据结构的接口后,都需要对数据结构进行操作,比如遍历,筛选。如果返回类型一致就不需要对是否异常情况进行判断(调用者不会迷惑说到底没有联系方式返回的结果是 undefined 还是 null 还是 [],对调用者友好)。

第二种情况,返回的对象本身表示的是一个 domain object,譬如获取账户信息 getUserAccountInfo,返回的应该是一个 AccountInfo 的 domain object,那么如果用户没有填写或者获取不到,返回应该是一个 null,而不是一个空对象 {}。因为返回值本身是否为 null 是有现实意义的,表示是否存在对应对象。和第一种情况的区别是,第一种情况数据如果不存在是通过数据项来表示的(array.length===0, map.entries().length===0),数据结构本身是不应该变化的。这里为什么选择 null 而不是 undefined,也是想把_没有这个对象_和_未定义这个对象_这两种情况区分开来,当然这个约定不是强制的。

接口输入一致性原则我们在 js 的众多 api 中都能找到很好的体现,譬如 Array.prototype.findIndex 如果没有找到 index 则返回的是-1,譬如 Array.prototype.filter 如果过滤不出符合条件的项目则返回的是空数组。譬如 Array.prototype.find 如果找到到符合条件的则返回 undefined。

SPA项目经验总结

SPA项目经验总结

最近选用了 React + React-Router 的技术栈,自然而然走了 SPA 的路线,下面总结下在所谓的 SPA 下的一些技术点的坑。

登录态的维持

  1. 现有方案
    登录页面发起 Ajax 请求获得用户 token,把 token 存放在 localStorage 里面,然后通过前端路由跳转到用户主页。随后用户相关的请求都会在 http head 里面带上这个 token。服务器端只负责验证每次请求中 token 的合法性(譬如:是否过期)。

  2. 遇到的问题
    把 token 保存在 localStorage 中主要考虑用户打开浏览器多个 tab 页可以维持登陆态。但是存储在 localStorage 里面的 token 面临如下问题:

  • 被串改的风险比较大,一旦被篡改,可以跨用户操作。
  • 多账号登录相互覆盖 localStorage 中 token 的问题。
  1. 解决方案
    针对多账号登录相互覆盖的问题,现有采取的方案是在登录后分别在内存和 localStorage 里面保存一份 token,每次要用到 token 的时候都去对比一下,如果检测到不一样则提示用户关闭当前窗口防止串号。这么做的目的一来防止用户自己被多个账号误导,二来如果关闭了检测到串号的窗口,不影响后登录的那个账号使用(当然如果前一个窗口不关闭也没有问题,js 使用的始终是内存中的 token)。

    但是当同一个用户开多个窗口,那么新开的窗口中的 token 第一次获取就只能从 localStorage 里面获取。这时就有 token 被篡改可以跨用户操作的风险。针对这个风险我预想了解决方案(需要用到 session 存储 token):
    1. 在已经登录一个账号的情况下,不允许开新浏览器窗口进行其他账户的登录,即如果检测到当前已经是登录态则不允许用户停留在登录页。
      singleuser
    2. 在已经登录一个账号的情况下,允许开新浏览器窗口进行其他账户的登录。
      multiuserlogin
          这种情况下如果登录了第二个账户,第一个窗口(第一个账号)要再进行任何操作,传回的 token 就和 server session 中的 token 不匹配,server 就可以返回状态码让前端把用户登出。

多路由模块共享数据同步问题

如果多路由之间有相关的业务数据,那么为了减少请求,提高用户体验,最好把多路由相关的数据共同抽象到一个地方,譬如 redux 的 store 里面。这样一个路由改了数据,当切换到另一个路由就不需要从后台再拉一遍数据的最新状态。这点对于单人开发没有问题,但是对于多人协作就需要提前设计数据存放结构,约定接口。一边做一边改就容易出现沟通问题,数据架构也不容易统一。

异步任务的状态问题

针对有异步任务的情况就要考虑状态更新的问题。因为页面提交了一个请求修改数据,但是数据被修改的最新状态无法立即体现在当前界面上,那么就会有如下问题:

  1. 页面如何处理中间态即处理中的状态提示。
  2. 页面是否需要接入服务器推送,等异步任务完成后把结果推送到页面来更新状态。
  3. 异步任务还没有完成,用户刷新了页面并又提交了相同的任务如何处理。

问题1如果中间态存储在前端,一旦用户刷新页面或者新开页面就转换成问题3了,所以后台一定要做好验证,前端无法严格维持状态。还有一种情况就是后端存储中间态,那么无论是刷新还是新开页面用户都能够看到中间态也会避免提交重复请求,但是这种方案就增加了后端的复杂度,需要根据项目的需求自行决定。

问题2涉及两个方面:一来是接入推送服务增加复杂度的问题,需要酌情考虑。二来是提高用户体验的问题如果最新的状态需要及时通知到用户,那么推送方案就要在技术选型时考虑在内。

多人操作同份数据

这个问题最典型的就是项目管理工具,针对同一个任务两个人同时开着界面,如果一个人认领了,那么另一个人一般情况下是无法知道的,可能会造成“误操作”。这个时候增加推送功能能极大的提高用户体验。

从人如何思考看程序员解决问题的思路 - 论原理的重要性

从人如何思考看程序员解决问题的思路 - 论原理的重要性

威林厄姆教授的《为什么学生不喜欢上学》中阐述了,思考是把环境信息和长期记忆信息进行重新加工的过程。长期记忆是你对已有知识经验的认知,包括事实性知识和过程性知识。当某个问题超出了你的认知范围时人是没有办法思考的(譬如问你鲁菜中的清汤如何能做到煮时无泡沫)。所以对基础知识(事实性知识)的学习是有必要的。推演到技术上,很多前端前辈强调面试很看中面试者解决问题的能力和思路,但是这些都不是凭空产生的。你必须要知道很多的基础知识和日积月累的项目经验(过程性知识),才有可能通过推理把你存在长期记忆中的基础知识和面临的问题进行结合来解决问题。你不可能让一个完全不知道 HTTP 协议的人回答“当你在浏览器敲下地址后会发生什么”。

以下用一个我自己 debug 的实例来说明下基础知识(原理)和经验发挥的作用。

项目技术背景:

移动端页面,使用 react 和 material-ui,淘宝 lib.flexible 做移动端适配。

bug现象

当页面加载时,按钮上的字会从下往上升的动画效果,见下图:

bug.gif

对于这个问题 debug 的思路及分析步骤如下:

  1. 看上去是一个动画效果,通常是 js 修改了 css 某个属性引起的。我们项目自己代码没有加过动画,查了下元素 css 看到 material-ui 在元素上加了 transition:all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms,根据 all 这个值,难以推测是哪个 css 属性变化引起的动画。

  2. 现象是在页面加载完后立即发生的,按经验一般要么是 dom ready 的时候,要么是组件的 componentDidMount 中写有 js 去修改了元素样式。先去 componentDidMount 中逐一注释代码,幸运的是通过注释找到了产生此现象的函数调用。

  3. 查看步骤2中定位出来的函数,其中有一句是获取某个元素的 offsetHeight,就是这条语句触发了这个 bug(注释掉此行代码,现象消失),而获取一个元素的 offsetHeight 会导致页面 reflow。我猜测是这个 reflow 导致某个不应该生效的样式生效了,但是此时依然无法定位那个样式的变化导致这个 bug。

  4. 仔细查看此按钮的样式,在我的知识范围里面没有找出可疑的样式(这里就是我此次 debug 的难点,因为导致此 bug 涉及到的一个 css 特性我不知道)。但是经过我对自己已知 css 知识的排查,猜测可能是行高引起的问题,并不经意间看到 body 设置了一个不太常见的 inline style:font-size: 12px。我想验证下是否是字体大小影响了垂直居中。所以我起 demo 测试获知:一般来说一个 div 高度和行高一样时,内部文字会垂直居中,但是前提是这个 div 的字体要小于高度。如果字体大于高度则文字会在中心线以下(具体原理还不清楚,遂记录下来,需要去调研下行高和字体的关系)。

  5. 用 devtool 查看了下按钮的字体样式,果然继承自 body,这个 body 上设置的字体样式是 lib.flexible 生成的,因为之前调研过一点 lib.flexible 知道其会在 html 上也会设置字体样式,而且 html 的字体是大于按钮行高的。难道是因为 lib.flexible 先设置了 html 上的字体然后再设置 body 上的字体导致这个按钮的字体样式继承关系从 html 变成了 body 导致了动画效果的产生?

  6. 那步骤3的触发条件如何解释呢?注释掉获取某个元素的 offsetHeight 这行代码,页面上的 lib.flexible 并不会产生这个 bug 啊(说明先设置 html 字体,再设置 body 字体,导致的按钮字体样式从继承 html 再继承 body 这一过程不会触发动画)。而且从 js 引用关系来看(先引用的 flexible.js,然后再引用的项目 index.js),获取某个元素的 offsetHeight 导致的 reflow 应该是在 html body 被设置完字体样式后啊?根据已有线索:一、reflow 触发此次 bug,二、元素字体样式继承关系从 html 变成了 body。瞬间猜测设置 html 和 body 字体样式的代码可能是异步的,而 reflow 就发生在他们两步中间。遂去查看 lib.flexible 源码,果然设置 body 的代码是异步的:

doc.addEventListener('DOMContentLoaded', function(e) {
    doc.body.style.fontSize = 12 * dpr + 'px';
}, false);

那基本就定下来产生这个 bug 是因为先设置了 html 的字体,然后获取某个元素 offsetHeight 这行代码产生的 reflow 导致按钮字体样式生效(继承自 html 字体),最后设置 body 字体样式导致按钮字体样式又变成了 body 的字体(继承优先级被 body 覆盖),按钮字体样式的变化导致行高变化触发了动画效果。

  1. 那为什么没有那行导致 reflow 的代码就不会产生这个 bug 呢?我自己依稀记得浏览器并不会在 js 每次修改样式时都及时去让修改生效,而会合并某些修改。遂想 google 这方面的内容,但是短时间内没有查到细节,只查到了:

浏览器不会在每一次样式变化后就去重新relfow一次,一般来说,浏览器会把这样的操作积攒一批,然后做一次reflow,这又叫异步reflow或增量异步reflow。但是有些时候,我们的脚本会阻止浏览器这么干,比如:如果我们请求下面的一些DOM值:offsetTop, offsetLeft, offsetWidth, offsetHeight。

所以此次 debug 顺利找到了产生 bug 的原因,此过程中产生了两个待调研的新知识点:一、字体和行高的关系。二、浏览器如何优化 js 对样式的修改。

回溯下整个 debug 过程,步骤1,2是项目经验起了作用(如何凭借第六感去定位问题,遇到类似现象以前是如何定位问题的),步骤3,5,6,7是基础知识起了作用(步骤3 - reflow 知识,步骤5 - css 样式继承知识,步骤6 - js 异步原理,步骤7 - 笼统的浏览器绘制原理)。而此次 debug 花费时间最长的是步骤4,也就是我 css 基础知识盲点导致的。所以经验可以积累,但基础知识还得靠我们程序员自己花力气去学习及完善

不深入只浅出ES6 Promise | 笔记

不深入只浅出ES6 Promise | 笔记

用例子直观的陈列 Promise 的行为作为笔记(如果能帮助新手快速了解 Promise 的使用自然最好,最终还是希望不但要学会使用还要了解规范),此行为规范基于的是 ES6 的规范,以后 JS 规范更新可能改变某些行为。原理及规范请查看考如下资料:

  1. 《You Don't Know JS: Async & Performance - Chapter 3: Promises》 这是著名的《你不知道的JavaScript》的英文原版章节。-- 这篇对于新人的缺点是过长。
  2. Promises/A+ 规范 这篇是 ES6 Promise 规范的前身。 -- 这篇对于新人的缺点是技术点不容易看懂。
  3. ES6 Promise 规范 -- 最权威的规范。
  • 基础(如果对 Promise 没有一点了解的,请移步 MDN Promise
var p1 = new Promise( function(resolve, reject){
  resolve( 1 )
} )

var p2 = new Promise( function(resolve, reject){
  setTimeout( function(){
    resolve( 2 )
  }, 1000 );
} )

p1.then( function(value){
  console.log( value ); // 1
}, function(){
  // never gets here
} )

p2.then( function(value){
  console.log( value ); // 2
}, function(){
  // never gets here
} )

无论构造时候 executor 中被传入的 resolve 函数是同步地被调用还是异步地被调用,只要传入的是一个非 promise 非 thenable 的值,都会在 then 传入的第一个回调函数(onFulfilled)中获得这个值,第二个回调函数(onRejected)不会被调用到。

  • 如果在 executor 中调用 reject
var p1 = new Promise( function(resolve, reject){
  reject( 'rejected because ...' )
} )

p1.then( function(value){
  console.log( value ); // never gets here
}, function(reason){
  console.log( reason ); // rejected because ...
} )
  • 如果 executor 中报错,promise 变成 rejected 状态,then 传入的第二个回调函数(onRejected)中会获得错误对象。
var p1 = new Promise( function(resolve, reject){
  throw new Error( 'test error' )
  resolve( 1 )
} )

var p2 = new Promise( function(resolve, reject){
  '1'.toFixed() // number has not toString method ,so it will throw exception
  resolve( 2 )
} )

p1.then( function(value){
  console.log( value ); // never gets here
}, function(error){
  console.log( error.message ) // test error
} )

p2.then( function(value){
  console.log( value ); // never gets here
}, function(error){
  console.log( error.message ) // "1".toFixed is not a function
} )
  • then 会返回另一个 promise,这个 promise 会使用 then( onFulfilled, onRejected ) 两个回调函数中的任何一个函数的返回值作为成功状态的值。这么做的目的是可以产生链式调用。
var p1 = new Promise( function(resolve, reject){
  resolve( 1 )
} )

var p11 = p1.then( function(value){
  return value * 3;
}, funtion(){
  // never gets here
} );

p11.then( function(value){
  console.log( value ); // 3
}, function(){}{
  // never gets here
} )
/*
上述改成链式调用的写法就是:

p1.then( function(value){
  return value * 3;
} ).then( function(value){
  console.log( value ); // 3
} )
*/


var p2 = new Promise( function(resolve, reject){
  '1'.toFixed() // number has not toString method ,so it will throw exception
  resolve( 2 ) // never gets here
} )

// p2 rejected的情况,以下采用链式调用写法
p2.then( function(){
  // never gets here
}, function(error){
  console.log( error.message ) // "1".toFixed is not a function
  return 4;
} ).then( function(value){
  console.log( value ); // 4
} )

// 如果 onFullfilled, onRejected 没有返回则会获得 undefined, 
new Promise( function(resolve, reject){
  resolve( 1 )
} ).then( function(value){
  console.log( value ); // 1
}, function(){
  // never gets here
} ).then( function(value){
  console.log( value ); // undefined
}, function(){
  // never gets here
} )

一个 promise.then 中的两个回调函数只有一个会被调用,因为 promise 的状态要么是成功的,要么是失败的,不会在成功失败间相互转换。

  • then( onFulfilled, onRejected ) 两个回调函数中的任意一个函数在被执行时候抛出异常,则 then 返回的 promise 变成失败的状态,其 onRejected 被调用。
new Promise( function(resolve, reject){
  resolve( 1 );
} ).then( function(){
  throw new Error( 'test error1' );
  return 2; // never gets here
} ).then( function(){
  // never gets here
}, function(error){
  console.log( error.message ); // test error1
} )

new Promise( function(resolve, reject){
  throw new Error( 'test error2' );
  resolve( 1 ); // never gets here
} ).then( function(){
  // never gets here
}, function(error){
  console.log( error.message ); // test error2
  throw new Error( 'test error3' );
} ).then( function(error){
  // never gets here
}, function(error){
  console.log( error.message ); // test error3
} )
  • 如果 promise1.then( onFulfilled, onRejected ) 没有传入回调函数,则 then 返回的 promise2 继承 promise1 的状态。从原理上来说就是 promsie1 成功或者失败后调用 then 没有函数去处理(成功没有注册 onFulfilled,失败没有注册 onRejected),则 then 返回的 promise2 将依然保持 promise1 的状态。
new Promise( function(resolve, reject){
  resolve(1);
} ).then(
  null,
  null
).then( function(value){
  console.log( value ); // 1
} )

new Promise( function(resolve, reject){
  throw new Error( 'test error' );
  resolve(1); // never gets here
} ).then().then( function(){
  // never gets here
}, function(error){
  console.log( error.message ); // test error
} )
  • 上面所列的都是一般常见情况,如果在 promise1 的 executor 中 resolve 了一个 promise0 将怎么处理?promise1 将把自己的状态和 promise0 同步。
var p0 = new Promise( function(res,rej){ res(0); } ); // fulfilled promise
var p1 = new Promise( function(resolve, reject){
  resolve( p0 );
} );
p1.then( function(value){
  console.log( value ); // 0
}, function(){
  // never gets here
} );

var p2 = new Promise( function(res,rej){ throw new Error('test error') } ); // rejected promise
new Promise( function(resolve, reject){
  resolve( p2 );
} ).then( function(){
  // never gets here
}, function(error){
  console.log( error.message ); // test error
} )
  • 如果 promise1.then( onFulfilled, onReject ) 中任意一个回调函数中 return 一个 promise0,那么 then 返回的 promsie2 也将把自己的状态和 promise0 同步。
var p0 = new Promise( function(res,rej){ res(0); } );
new Promise( function(resolve, reject){
  resolve( 1 );
} ).then( function(){
  return p0;
} ).then( function(value){
  console.log( value ); // 0
} )

new Promise( function(resolve, reject){
  throw new Error( 'make rejected' );
} ).then( null, function(error){
  return p0
} ).then( function(value){
  console.log( value ); // 0
} )
  • 可以用 promise.catch( onRejected ) 来代替 promise.then( ..., onRejected ),这样写链式调用起来更优雅。
var p1 = new Promise( function(){
  throw new Error( 'make rejected' );
} );

p1.then( null, function( error ){
  console.log( error.message ); // make rjected
} );

p1.catch( function(error) {
  console.log( error.message ); // make rjected
} );
// p1.then(null,onRejected) 和 p1.catch(onRejected) 是完全等价的。


// 链式调用
new Promise( function(resolve, rejected){
  resolve( 0 );
} ).then( function(value){
  // some code here, may generate some error
  return value++;
} ).then( function(value){
  // some code here, may generate some error
  return value++;
} ).then( function(value){
  // some code here, may generate some error
  return value++;
} ).then( function(value){
  // some code here, may generate some error
  return value++;
} ).catch( function(error){
  // 上面任何一个 then 都没有设置 onRejected 回调函数,意味着,一旦有一个 onFulfilled 里面一旦报错,则一个失败状态的 promise 会得不到处理,一直延续到最后一个被 catch 处理。
  // 如果理解不了,则自行拆解每个 then
} )
  • Promise.resolve( value ) 返回一个成功的 promise1,其值是 value。如果 value 是一个 promise0,意味着 promise1 将把自己的状态和 promise0 同步,ES6 进一步优化了这种情况,如果 value 是一个 promise,则直接返回这个 promise。
var p1 = Promise.resolve( 42 );
p1.then( function(value){
  console.log( value ); // 42
} );

var p2 = Promise.resolve( p1 );
p2 === p1; // true
  • Promise.reject( reason ) 返回一个失败的 promise1,其值是 reason。这个无二义性。
var p1 = Promise.reject( 'make rejected' );
p1.catch( function(reason){
  console.log( reason ); // make rejected
} );

var p2 = Promise.reject( p1 );
p2.catch( function(reason){
  console.log( reason ); // p1: Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: "make rejected"}
} );
  • Promise.all( [promise0, promise0, ..., promiseN] ) 返回一个 promsie,如果 promise0 - promiseN 全部成功了,则这个最终的 promise 成功。如果但凡 promise0 - promiseN 中有任意一个失败了,则最终的 promise 立即失败。

    1. 0 - n 都成功则 Promise.all( [promise0, promise0, ..., promiseN] ).then 的 onFulfilled 接收一个数组作为参数,数组里面的值对应的是 0 - n 个 promise 的成功值。
    2. 如果有一个失败了,Promise.all( [promise0, promise0, ..., promiseN] ).then 的 onRejected 就接收这个失败的 promise 的失败理由。
    3. Promise.all( [1, 'abc', ..., promiseN] ) 参数数组允许非 promise 的值,Promise.all 这个方法会把所有非 Promise 对象的值用 Promise.resolve( v ) 包装成一个 promise。
  • Promise.race( [promise0, promise0, ..., promiseN] ) 返回一个 promsie,这个返回的 promise 状态就是 promise0 - promiseN 中第一个成功或者第一个失败的 promise 的状态。

    1. 注意 Promise.race( [] ).then( onFulfilled, onRejected ) 如果参数为空数组,则返回的 promise 永远不会成功或者失败(onFulfilled, onRejected 永远不会被调用)。因为空数组中没有 promise 会成功失败,即永远没有 promise 来竞争来使 Promise.race 返回的 promise 变成功或者失败。

文中总结了 promise 的一般用法,没有涉及到异步和 thenable 的概念。异步的概念我还需要进一步看些资料,一时我也没有钻的比较深。thenable 除非你项目是老项目,里面会用到 promise 出现前的一些内容(譬如 jQuery 中的 defer),一般是不太会涉及到这个概念,你就先理解成 thenable 对象是一个带 then 方法的对象,但是它本身不是 Promise 对象,具体还是希望各位看官查看文章开始所列的资料。

React组件编写思路(二)

React组件编写思路(二)

上一篇讲了 React 两种最常见的组件:受控组件和非受控组件。为了可用性,我们一般编写出来的组件希望支持这两种特性:可以通过组件自身的方法来改变组件的某(些)状态,也可以通过 props 的值的变化来改变组件自身的同一个(些)状态。

组件改变自己的状态只能通过改变 state 完成,而把 props 的变化反映到 state 却是可以通过生命周期函数来实现。首先还是拿上一篇中受控 alert 组件代码为例:

class Alert extends React.Component {
  constructor( props ) {
    super( props )
    this.state = {
      content: '',
      show: false
    }
    this.show = ( content )=>{
      this.setState( {
        content: content,
        show: true
      } )
    }

    this.hide = ()=>{
      this.setState( {
        show: false
      } )
    }
  }
  render() {
    let style = {
      display: this.state.show ? 'fixed' : 'none'
    }
    return (
      <div class="my-alert" style={ style } >
        <div class="my-alert-tit">Alert</div>
        <div>{ this.state.content }</div>
        <div class="my-alert-footer">
          <button onClick={ this.hide }>确定</button>
        </div>
      </div>
    );
  }
}

组件初始化的时候构造函数会接受传入的 props ,而当组件的容器改变传入组件的 props 的值时会触发组件的 componentWillReceiveProps 的方法,在这个方法中我们可以把变化后的 props(nextProps) 通过 setState 映射成 state 的变化。那么我们需要做的就是给受控组件增加初始化 props 处理和在 componentWillReceiveProps 内 props 的处理。

class Alert extends React.Component {
  constructor( props ) {
    super( props )
    this.state = {
      content: this.props.content || '',
      show: this.props.show || false
    }
    this.show = ( content )=>{
      this.setState( {
        content: content,
        show: true
      } )
    }

    this.hide = ()=>{
      this.setState( {
        show: false
      } )
    }
  }

  componentWillReceiveProps( nextProps ) {
    this.setState( nextProps );
  }

  render() {
    let style = {
      display: this.state.show ? 'fixed' : 'none'
    }
    return (
      <div class="my-alert" style={ style } >
        <div class="my-alert-tit">Alert</div>
        <div>{ this.state.content }</div>
        <div class="my-alert-footer">
          <button onClick={ this.hide }>确定</button>
        </div>
      </div>
    );
  }
}

那么针对同一个 alert 组件的使用就变得多样化,可以根据自己项目的需求来变化。譬如:

import { Alert } from 'Alert';

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      alertMsg: '',
      showAlert: false
    }
    this.saveHandler = ()=>{
      // save ajax success
      this.refs.myAlert.show( 'Save successfully' );
    }
    this.removeHandler = ()=>{
      // remove ajax success
      this.setState( {
        alertMsg: 'Remove successfully',
        showAlert: true
      } )
    }
  }

  render() {
    <div>
      <button onClick={ this.saveHandler }>Save</button>
      <button onClick={ this.removeHandler }>Remove</button>
      <Alert ref="myAlert" content={ this.state.alertMsg } show={ this.state.showAlert }/>
    </div>
  }
}

为了让组件更健壮,我们对 state 和 props 的一些必须的初始化值(默认值)需要明确指定

class Alert extends React.Component {
  constructor( props ) {
    super( props )
    let content = this.props.content;
    let show = this.props.show;
    /*
      props.xxx 的优先级比 props.defautXxx 高,
      如果设置了props.xxx 则 props.defaultXxx 就不起作用
    */
    this.state = {
      content: content === undefined ? this.props.defaultContent : content
      show: show === undefined ? this.props.defaultShow : show
    }
  }
}
Alert.propTypes = {
  defaultShow: React.PropTypes.bool,
  defaultContent: React.PropTypes.string,
  show: React.PropTypes.bool,
  content: React.PropTypes.string
}

Alert.defaultProps = {
  defaultShow: false,
  defaultContent: ''
}

如上代码如果对 props.xxx 和 props.defaultXxx 有迷惑的童鞋,其实有了 xxx 完全没有必要再有 defaultXxx,但是参考一些组件库的 api 设计,我理解为是为了保持受控组件 api 的统一性,如果把 alert 组件当成受控组件则初始化使用 defaultXxx,如果当成非受控组件就直接使用 xxx。

那什么时候使用受控组件,什么时候使用非受控组件呢?我们知道受控组件是比较符合我们传统 UI 组件开发的思路的。但是 React 在跨组件通讯方面很弱,如果不借助第三方库进行通讯,对于两个毫无关系的组件相互调用就需要传递层层的回调函数。我想没有人喜欢这种编程风格,所以把所有组件的状态抽象到一个地方进行集中管理变化,典型的数据流用 redux 就倾向于使用非受控组件了(这里不讨论flux**的由来,不讨论redux好坏)。

故最基本的 React 组件编写套路就这些。但是这些还只是 api 应用层面的东西,比较难的是在编写组件时候对状态的抽象,使使用者使用的舒服自然。

React 渲染过程

React 渲染过程

  1. 程序假设有如下 jsx
class Form extends React.Component {
  constructor() {
    super();
  }
  render() {
    return (
        <form>
          <input type="text"/>
        </form>
    );
  }
}

ReactDOM.render( (
  <div className="test">
    <span onClick={function(){}}>CLICK ME</span>
    <Form/>
  </div>
), document.getElementById('main'))
  1. 拿 ReactDOM render 的部分(<div className="test">...</div>)为例,用 babel 把 jsx 转成 js 后得到如下代码:
React.createElement( 'div', {
  className: 'test'
  },
  React.createElement( 'span',
    { onClick: function(){} },
    'CLICK ME'
  ),
  React.createElement(Form, null)
)
  1. 这里看下 API: React.createElement(component, props, ...children)。它生成一个 js 的对象,这个对象是用来代表一个真实的 dom node,这个 js 的对象就是我们俗称的虚拟dom。(虚拟 dom 的意思是用 js 对象结构模拟出 html 中 dom 结构,批量的增删改查先直接操作 js 对象,最后更新到真正的 dom 树上。因为直接操作 js 对象的速度要比操作 dom 的那些 api 要快。) 譬如根元素 <div className="test"></div> 生成对应出来的虚拟 dom 是:
{
  type: 'div',
  props: {
    className: 'test',
    children: []
  }
}

除了一些 dom 相关的属性,虚拟 dom 对象还包括些 React 自身需要的属性,譬如:ref,key。最终示例中的 jsx 生成出来的虚拟 dom,大致如下:

{
  type: 'div',
  props: {
    className: 'xxx',
    children: [ {
      type: 'span',
      props: {
        children: [ 'CLICK ME' ]
      },
      ref:
      key:
    }, {
      type: Form,
      props: {
        children: []
      },
      ref:
      key:
    } ] | Element
  }
  ref: 'xxx',
  key: 'xxx'
}
  1. 有了虚拟 dom,接下来的工作就是把这个虚拟 dom 树真正渲染成一个 dom 树。React 的做法是针对不同的 type 构造相应的渲染对象,渲染对象提供一个 mountComponent 方法(负责把对应的某个虚拟 dom 的节点生成成具体的 dom node),然后循环迭代整个 vdom tree 生成一个完整的 dom node tree,最终插入容器节点。查看源码你会发现如下代码:
// vdom 是第3步生成出来的虚拟 dom 对象
var renderedComponent = instantiateReactComponent( vdom );
// dom node
var markup = renderedComponent.mountComponent();
// 把生成的 dom node 插入到容器 node 里面,真正在页面上显示出来
// 下面是伪代码,React 的 dom 操作封装在 DOMLazyTree 里面
containerNode.appendChild( markup );

instantiateReactComponent 传入的是虚拟 dom 节点,这个方法做的就是根据不同的 type 调用如下方法生成渲染对象:

// 如果节点是字符串或者数字
return ReactHostComponent.createInstanceForText( vdom(string|number) );
// 如果节点是宿主内置节点,譬如浏览器的 html 的节点
return ReactHostComponent.createInternalComponent( vdom );
// 如果是 React component 节点
return new ReactCompositeComponentWrapper( vdom );

ReactHostComponent.createXXX 也只是一层抽象,不是最终的的渲染对象,这层抽象屏蔽了宿主。譬如手机端(React native)和浏览器中同样调用 ReactHostComponent.createInternalComponent( vdom ); 他生成的最终的渲染对象是不同的,我们当前只讨论浏览器环境。字符串和数字没有什么悬念,在这里我们就不深入探讨了,再进一步看,div 等 html 的原生 dom 节点对应的渲染对象是 ReactDOMComponent 的实例。如何把 { type:'div', ... } 生成一个 dom node 就在这个类(的 mountComponent 方法)里面。(对如何生成 div、span、input、select 等 dom node 感兴趣的可以去探究 ReactDOMComponent,这里不做具体的讨论,本文只是想总结下 React 整个渲染过程。下面只给出一个最简示例代码:

class ReactDOMComponent {
  constructor( vdom ) {
    this._currentElement = vdom;
  }
  mountComponent() {
    var result;
    var props = this._currentElement.props;
    if ( this._currentElement.type === 'div' ) {
      result = document.createElement( 'div' );
      
      for(var key in props ) {
        result.setAttribute( key, props[ key ] );
      }
    } else {
      // 其他类型
    }
    // 迭代子节点
    props.children.forEach( child=>{
      var childRenderedComponent =  = instantiateReactComponent( child );
      var childMarkup = childRenderedComponent.mountComponent();
      result.appendChild( childMarkup );
    } )
    return result;
  }
}

我们再看下 React component 的渲染对象 ReactCompositeComponentWrapper(主要实现在 ReactCompositeComponent 里面,ReactCompositeComponentWrapper 只是一个防止循环引用的 wrapper

// 以下是伪代码
class ReactCompositeComponent {
  _currentElement: vdom,
  _rootNodeID: 0,
  _compositeType:
  _instance: 
  _hostParent:
  _hostContainerInfo: 
  // See ReactUpdateQueue
  _updateBatchNumber:
  _pendingElement:
  _pendingStateQueue:
  _pendingReplaceState:
  _pendingForceUpdate:
  _renderedNodeType:
  _renderedComponent:
  _context:
  _mountOrder:
  _topLevelWrapper:
  // See ReactUpdates and ReactUpdateQueue.
  _pendingCallbacks:
  // ComponentWillUnmount shall only be called once
  _calledComponentWillUnmount:

  // render to dom node
  mountComponent( transaction, hostParent, hostContainerInfo, context ) {
    // ---------- 初始化 React.Component --------------
    var Component = this._currentElement.type;
    var publicProps = this._currentElement.props;
    /*
      React.Component 组件有2种:
      new Component(publicProps, publicContext, updateQueue);
      new StatelessComponent(Component);
      对应的 compositeType 有三种
      this._compositeType = StatelessFunctional | PureClass | ImpureClass,
      组件种类和 compositeType 在源码中都有区分,但是这里为了简单,只示例最常用的一种组件的代码
    */
    var inst = new Component(publicProps, publicContext, updateQueue);
    
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = updateQueue;
    
    // 渲染对象存储组件对象
    this._instance = inst;

    // 通过 map 又把组件对象和渲染对象联系起来
    ReactInstanceMap.set(inst, this);
    /*
      ReactInstanceMap: {
              -----------------------------------------------
              |                                              |
              v                                              |
        React.Component: ReactCompositeComponentWrapper {    |
          _instance:  <-------------------------------------
        }
      }
      这样双方都在需要对方的时候可以获得彼此的引用
    */

    // ---------- 生成 React.Component 的 dom  --------------
    // 组件生命周期函数 componentWillMount 被调用
    inst.componentWillMount();
    // 调用 render 方法返回组件的虚拟 dom
    var renderedElement = inst.render();
    // save nodeType  
    var nodeType = ReactNodeTypes.getType(renderedElement);
    this._renderedNodeType = nodeType;
    // 根据组件的虚拟 dom 生成渲染对象
    var child = instantiateReactComponent(renderedElement)
    this._renderedComponent = child;
    // 生成真正的 dom node
    // 其实源码中的真正代码应该是 var markup = ReactReconciler.mountComponent( child, ... ),
    // 这里为了简化说明,先不深究 ReactReconciler.mountComponent 还做了点什么
    var markup = child.mountComponent(); 
    // 把组件生命周期函数 componentDidMount 注册到回调函数中,当整个 dom node tree 被添加到容器节点后触发。
    transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
    return markup;
  }
}
// static member
ReactCompositeComponentWrapper._instantiateReactComponent = instantiateReactComponent
  1. 最终的过程是:
<div className="test">
  <span onClick={this.click}>CLICK ME</span>
  <Form/>
</div>

  |
babel and React.createElement
  |
  v

{
  type: 'div',
  props: {
    className: 'xxx',
    children: [ {
      type: 'span',
      props: {
        children:
      },
      ref:
      key:
    }, {
      type: Form,
      props: {
        children:
      },
      ref:
      key:
    } ] | Element
  }
  ref: 'xxx',
  key: 'xxx'
}

  |
var domNode = new ReactDOMComponent( vdom ).mountComponent();
  |
  v

domNode = {
  ReactDOMComponent -> <div/>
  props: {
    children: [
      ReactDOMComponent -> <span/>
      ReactCompositeComponentWrapper.render() -> vdom -> instantiateReactComponent(vdom) -> <form><input/></from>
    ]
  }
}

  |
  |
  v
containerDomNode.appendChild( domNode );

以上是 React 渲染 dom 的一个基本流程,下一篇计划总结下更新 dom 的流程,即 setState 后发生了什么。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.