一、描述

之前写了一篇文章是关于 rax 内置的展示组件 rax-picture 的源码分析,不过因为 rax-picture 的源码太长了,就拆了两部分写,之前的文章主要是关于 picture.weex.js ,即在 weex 容器上, rax-picture 的源码处理逻辑,文章地址:

这篇文章主要是对 picture.web.js 的源码分析以及 webp.js 的源码分析。

二、picture.web.js

1、组件支持的props

相比于 weex 容器的 rax-picture,web 容器环境下(真的用web容器很少,除非是为了降级)支持的 props 非常多。

具体的可以看 rax 的文档中,那些仅在 web 环境下起作用的 props。

picture.web.js 中对于 propsTypes 的声明如下:

 static propTypes = {
    style: PropTypes.object,
    source: PropTypes.object.isRequired,
    resizeMode: PropTypes.oneOf([
      'contain',
      'cover',
      'stretch'
    ]),

    width: PropTypes.string, // width of picture
    height: PropTypes.string, // height of picture
    defaultHeight: PropTypes.string, // default height when the height setting fails

    autoRemoveScheme: PropTypes.bool,
    autoReplaceDomain: PropTypes.bool,
    autoScaling: PropTypes.bool,
    autoWebp: PropTypes.bool,
    autoCompress: PropTypes.bool,
    highQuality: PropTypes.bool,
    compressSuffix: PropTypes.array,

    lazyload: PropTypes.bool,
    placeholder: PropTypes.string,
    autoPixelRatio: PropTypes.bool,
    forceUpdate: PropTypes.bool,
    ignoreGif: PropTypes.bool
  };

3、默认的 props

为了组件的默认渲染,picture.web.js 定义了默认的 props 的值,主要如下:

值得注意的是,默认 lazyload 是 false,而且默认是忽略 gif (ignoreGif:true)的

static defaultProps = {
    placeholder: 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==',
    source: {
      uri: ''
    },
    autoRemoveScheme: true,
    autoReplaceDomain: true,
    autoScaling: true,
    autoWebp: true,
    ignoreGif: true,
    autoCompress: true,
    highQuality: true,
    compressSuffix: ['Q75', 'Q50'],
    defaultHeight: '750rem',

    lazyload: false,
    autoPixelRatio: true
  };

3、componentWillMount

picture.web.js 本身维护了 {visible:true} 的 state,并且有两个静态变量,分别是 urinewStyle

componentWill(nextProps) 中,如果当前 props.source.urinexProps.source.uri 不相等,则将静态变量 uri 置为空,这是因为 rax-picture 的更新策略是,如果没有配置 props.foreUpdate = true,则只有当 props.source.uri 发生变化或者存在 props.children 时才会更新组件。

4、shouldCompoentUpdate

shouldComponentUpdate(nextProps, nextState) 方法就很好理解了,只有存在子组件或者 forceUpdate = true 的时候直接更新组件,如果不存在上述情况,则需要判断当前的 uri 是否发生呢个变化或者是 state.visible 是否发生变化才会进行组件更新。

shouldComponentUpdate(nextProps, nextState) {
  if (this.props.forceUpdate || this.props.children) {
    return true;
  }

  return this.props.source.uri !== nextProps.source.uri ||
    this.state.visible !== nextState.visible;
}

5、render 的过程

本身 picture.web.js 的源码就有 200 多行,还不算其引入的各种本地依赖,因此按照整个 DOM 的 render 过程去分析。

在 render 方法中,首先就是对图片的高度或者是宽度的计算,基于 rax-picture 这种更新策略,因此只有当 this.uri 为空(为空的原因是在 componentWillComponent 中判断如果需要更新组件,则将 this.uri 置空饿了)或者 forceUpdate 的时候才会去重新计算图片你的宽高,而图片的宽高计算则不再重复了,无非就是通过比例去计算。

之后使用 this.newStyle 作为最终需要渲染的样式。

而如果 props.source.uri 存在, 则对一系列其他的属性进行处理:

if (uri) {
  if (autoPixelRatio && window.devicePixelRatio > 1) { // devicePixelRatio >= 2 for web
    if (typeof sWidth === 'string' && sWidth.indexOf('rem') > -1) {
      sWidth = parseInt(sWidth.split('rem')[0]) * 2 + 'rem';
    }
  }

  this.uri = optimizer(uri, {
    ignoreGif: ignoreGif,
    ignorePng: true,
    removeScheme: autoRemoveScheme,
    replaceDomain: autoReplaceDomain,
    scalingWidth: autoScaling ? sWidth : 0,
    webp: autoWebp && (isSupportJPG && isSupportPNG),
    compressSuffix: autoCompress ? getQualitySuffix(highQuality, compressSuffix) : ''
  });
}

上面代码中,高分辨率下的二倍图(autoPixelRatio)等比较直观能够理解,其中 optimizer 是对图片的优化策略,包括图片的压缩等等,在 rax-picture 中,optimizer 又包含了 7 个文件,如果将其也进行分析,则文章太长了,而且时间也很久,所以暂时不对 optimizer 进行更深入的分析,之后单独拿出来。

同样的,web 容器下也支持 resizeMode 的处理。

web容器环境下做到了 lazyload(),使用的是 onAppear() => {} ,在 weex 容器下无法做到是因为 weex 容器下,之后 ListView/WaterFall/RecylerView 这种组件的子组件能够触发 onAppear() 事件,而 web 容器下的组件没有这个限制。

if (lazyload) {
  const { visible } = this.state;
  nativeProps.onAppear = () => this.lazyLoad();
  if (visible) {
    url = this.uri;
  }
} else {
  url = this.uri;
}

因此 web 容器下的 lazyload 实际上就是 onAppear 触发的时候,设置 state.visible 值为 true,这从 layzeload() 方法就可以看出来:

lazyLoad() {
  this.setState({ visible: true });
}

当组件中存在 props.children 或者 props.resizeMode 属性的时候,需要单独的处理,处理的方式是使用 background-size 进行 hack 处理,借助 css 的 background-size 实现各个 resizeMode,因此实际上渲染的是 <View> 而不是 <Image>


if (children || resizeMode) {
  return (
    <View
      {...nativeProps}
      data-once={true}
      style={[
        this.newStyle, {
          backgroundImage: 'url(' + url + ')',
          backgroundSize: resizeMode || 'cover',
          backgroundRepeat: 'no-repeat'
        }, resizeMode === 'cover' || resizeMode === 'contain' ? {
          backgroundPosition: 'center'
        } : null,
        !this.newStyle.height ? {height: defaultHeight} : null
      ]}
    >
      {children}
    </View>
  );
}

如果不需要处理 resizeMode 或者不存在 props.children 的时候,则不需要使用 <View>,直接渲染 <Image> 即可。

return <Image
  {...nativeProps}
  data-once={true}
  source={{
    uri: url
  }}
  style={this.newStyle}
/>;

三、实践

1、代码

import {createElement, Component} from 'rax';
import View from 'rax-view';
import Text from 'rax-text';
import Picture from 'rax-picture';
import styles from './App.css';

const img = 'https://camo.githubusercontent.com/27b9253de7b03a5e69a7c07b0bc1950c4976a5c2/68747470733a2f2f67772e616c6963646e2e636f6d2f4c312f3436312f312f343031333762363461623733613132336537386438323436636438316338333739333538633939395f343030783430302e6a7067';

class App extends Component {
  render() {
    return (
      <View style={styles.app}>
          {[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1].map(()=>{
            return <Picture 
            lazyload={true}
            source={{uri: img+'?' + Date.now()}}
            style={{width:400,height:300}}
            resizeMode="cover" 
          ></Picture>
          })} 
      </View>
    );
  }
}

export default App;

2、效果

G11111IF.gif