0%

本文为听 尤雨溪 的知乎 Live 不吹不黑聊聊前端框架 后的部分记录。


组件的分类

  • 接入型 container
  • 展示型
  • 交互型 比如各类加强版的表单组件,通常强调复用
  • 功能型 比如 <router-view><transition>,作为一种扩展、抽象机制存在

JSX 与模板区别

JSX 更自由,适合功能型组件;模板适合展示型组件。


路由

将 url 映射到组件树


web 应用、原生应用路由区别:

web 应用从一个 url 调到另一个 url,上一个 url 的状态被清空;原生应用一层一层的叠加。


主流的 CSS 方案

  • 跟 JS 完全解耦,靠预处理器和比如 BEM 这样的规范来保持可维护性,偏传统
  • CSS Modules,依然是 CSS,但是通过编译来避免 CSS 类名的全局冲突
  • 各类 CSS-in-JS 方案,React 社区为代表,比较激进
  • Vue 的单文件组件 CSS,或是 Angular 的组件 CSS(写在装饰器里面),一种比较折中的方案

传统 css 的一些问题:

  1. 作用域
  2. Critical CSS
  3. Atomic CSS
  4. 分发复用
  5. 跨平台复用

这些天有在 iOS 和 macOS 上录制屏幕,输出的文件都是 mov 格式的,于是期望转换成 mp4 格式,找到了一下方法:

$ brew install ffmpeg
$ ffmpeg -i /path/to/input.mov /path/to/output.mp4

缘起

昨天看到了 Android 上 Telegram 消息列表的动效,觉得比较有趣,便打算试着用 ReactNative 模仿一下。

PS:视频是从 macOS 端录制的,但是效果是一致的。

可以看到列表可视区域内最下面一条消息的用户的头像会随着列表的滚动而上下移动位置。

阅读全文 »

Functional Component 的引入为 React 的开发带来了极大的便利,然而它并不像 Class Component 那样可以设置 shouldComponentUpdate

测试代码如下

import React from "react";

const ItemX = id => {
  return props => {
    console.log(`Item${id} render`);
    const { title } = props;
    let timer = null;

    return <div>{title}</div>;
  };
};

const Item1 = ItemX(1);

class App extends React.Component {
  state = {
    title1: 1,
    x: 3
  };

  _handleClickIncX = () => {
    const { x } = this.state;
    this.setState({
      x: x + 1
    });
  };

  _handleClickIncTitle1 = () => {
    const { title1 } = this.state;
    this.setState({
      title1: title1 + 1
    });
  };

  render() {
    const { title1, title2, title3 } = this.state;
    return (
      <div>
        <Item1 title={title1} />
        <button onClick={this._handleClickIncX}>incX</button>
        <button onClick={this._handleClickIncTitle1}>incTitle1</button>
      </div>
    );
  }
}

可以发现点击 incX 也会导致 Item1 的执行,这就很尴尬了,那么如何让 Functional Component 也具有 shouldComponentUpdate 的功能呢?

我们可以用 PureComponentFunctional Component 包裹起来。

function pure1(component) {
  return class extends React.PureComponent {
    render() {
      const props = this.props;
      return component(props);
    }
  };
}

const Item2 = pure1(ItemX(2));

class App extends React.Component {
  state = {
    title1: 1,
    title2: 2,
    x: 3
  };

  _handleClickIncTitle2 = () => {
    const { title2 } = this.state;
    this.setState({
      title2: title2 + 1
    });
  };

  render() {
    const { title1, title2 } = this.state;
    return (
      <div>
        <Item1 title={title1} />
        <Item2 title={title2} />
        <button onClick={this._handleClickIncX}>incX</button>
        <button onClick={this._handleClickIncTitle1}>incTitle1</button>
        <button onClick={this._handleClickIncTitle2}>incTitle2</button>
      </div>
    );
  }
}

点击 incX 和 incTitle1 并不会导致 Item2 被执行。

项目开发中可以使用 recompose 中的 pure 函数来包裹 Functional Component


然而 pure1 本质上是用一个 Class Component 包裹 Functional Component,这样似乎并不那么 Functional,那么换种方法呢?

const hasOwnProperty = Object.prototype.hasOwnProperty;

function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y;
  } else {
    return x !== x && y !== y;
  }
}

function shallowEqual(objA, objB) {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== "object" ||
    objA === null ||
    typeof objB !== "object" ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}

function pure2(component) {
  let isInitialized = false;
  let oldProps = null;
  let oldValue = null;

  return function(newProps) {
    if (!isInitialized) {
      isInitialized = true;
      oldProps = newProps;
      oldValue = component(newProps);
      return oldValue;
    }

    if (shallowEqual(oldProps, newProps)) {
      return oldValue;
    } else {
      oldProps = newProps;
      oldValue = component(newProps);
      return oldValue;
    }
  };
}

const Item3 = pure2(ItemX(3));

class App extends React.Component {
  state = {
    title1: 1,
    title2: 2,
    title3: 3,
    x: 3
  };

  _handleClickIncTitle3 = () => {
    const { title3 } = this.state;
    this.setState({
      title3: title3 + 1
    });
  };

  render() {
    const { title1, title2, title3 } = this.state;
    return (
      <div>
        <Item1 title={title1} />
        <Item2 title={title2} />
        <Item3 title={title3} />
        <button onClick={this._handleClickIncX}>incX</button>
        <button onClick={this._handleClickIncTitle1}>incTitle1</button>
        <button onClick={this._handleClickIncTitle2}>incTitle2</button>
        <button onClick={this._handleClickIncTitle3}>incTitle3</button>
      </div>
    );
  }
}

shallowEqual 这个函数是从 React 源码中 PureComponent 那部分提取出来的。

Nice Work!

与 css 中的表现不同,在 React Native 中,flex 元素的子元素默认会在主轴方向填满父元素。需要子元素大小包裹内容时,可以在父元素样式中添加 flexWrap: 'wrap'


TouchableWithoutFeedbackTouchableHighlight 的差异。可以在 TouchableHighlight 元素可以设置 style 属性,而 TouchableWithoutFeedback 元素上不能设置 style 属性。


ios android 平台中 borderRadius 有所差异:

const style = StyleSheet.create({
  avatar: {
    width: 50,
    height: 50,
    backgroundColor: 'transparent',
    overflow: 'hidden',
    ...Platform.select({
      ios: {
        borderRadius: 50 / 2,
      },
      android: {
        borderRadius: 50,
      }
    }),
  }
});

日前在 ReactNative 开发中有遇到如何写出符合多种 viewport 宽度的样式的问题。在常规的移动端开发中,可以借助 remvw 等单位,那么在使用 ReactNative 时有没有类似的方案呢?最近进行小程序开发时注意到它们有 rpx 这种单位:

rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。

受其启发,决定在代码中实现类似的单位,当需要自适应的时候写字符串 '20rpx',不需要的时候正常写数字 20

// style-sheet.js
import { StyleSheet, PixelRatio } from 'react-native';

const REG = /[\d\.]rpx/;

function pre(obj) {
  Object.keys(obj).forEach(key => {
    const value = obj[key];
    if (typeof value === 'object') {
      obj[key] = pre(value);
    } else if (typeof value === 'string' && REG.test(value)) {
      obj[key] = PixelRatio.getPixelSizeForLayoutSize(parseFloat(value) / 4);
    }
  });

  return obj;
}

function create(obj) {
  obj = pre(obj);
  return StyleSheet.create(obj);
}

const BORDER_COLOR = '#e6e6e6';

const SS = {
  hairlineWidth: StyleSheet.hairlineWidth,
  create,
  pre,
  absoluteFillObject: StyleSheet.absoluteFillObject,
  common: {
    backgroundColor: '#F2F3F8',
    center: {
      justifyContent: 'center',
      alignItems: 'center',
      display: 'flex'
    },
    centerH: {
      alignItems: 'center',
      display: 'flex',
      flexDirection: 'row'
    },
    centerV: {
      alignItems: 'center',
      display: 'flex',
      flexDirection: 'column'
    },
    primaryGreen: '#75CA2A',
    primaryDarkGreen: '#62A427',
    borderColor: BORDER_COLOR,
  }
}

export default SS;

goBack 时回传参数

// A.js
class A extends React.Component {
  _handlePressLocationSelect = () => {
    const { navigation } = this.props;
    navigation.navigate(
      'B',
      {
        onSelect: this._onSelect
      }
    );
  }

  _onSelect = ({}) => {
    this.setState({
    });
  }
}
// B.js
class B extends React.Component {
  _onPressBack = ({}) => {
    const { navigation } = this.props;
    navigation.goBack();
    navigation.state.params.onSelect({});
  }
}

阅读全文 »

setData: invokeWebviewMethod 数据传输长度为 *** 已经超过最大长度 1048576

在小程序中执行 setData 时,如果数据内容过大的话,会出现数据传输长度超过最大长度的问题。其原因在于 setData 实际上是在 webview 上执行了 stringByEvaluatingJavaScriptFromString 这类方法,如果传入数据过大的话,就会使 webview 的内存开销过大,JavaScript 代码执行失败。

开发中,在存在大量数据的情况下,可以对数据执行 map 之类的操作,仅留下需要展示的数据以及对应的 key,这样每次 setData 的数据就会小很多。


rich-text 数据来源

由于小程序不支持 iframe 这样的组件,所以在需要展示 html 页面的时候,只能使用 rich-text

欲显示的 html 内容最好在服务器端转换成数组的形式,有以下几个原因:

  1. 根据小程序开发文档所言,nodes 属性推荐使用 Array 类型,由于组件会将 String 类型转换为 Array 类型,因而性能会有所下降
  2. rich-text 仅支持有限的 html 标签。对于不支持的标签,一部分可以替换成 span 或者 tag 这类它支持的标签。剩下的也可以根据实际情况直接去除或者替换成提示文字。
  3. 小程序中的 wxss 样式仅能根据 class 生效,而原 html 文档的 css 文件中会有使用类型选择器,为了能使用原有的这些样式,为每一个元素增加类似 __tag__span 这样的类名,同时对原 css 执行正则表达式将其中的 span {...} 替换成 .__tag__span {...} 的形式
  4. 剔除 rich-text 不支持的 attr

以上这些操作适合在服务器端执行,代码如下:

const parse5 = require('parse5');
const entities = require('entities');

const AVAILABLE_ATTRS = [
  'class',
  'style',
  'span',
  'width',
  'alt',
  'src',
  'height',
  'start',
  'type',
  'colspan',
  'rowspan',
];

const AVAILABLE_TAGS = [
  'a', 'abbr', 'b', 'blockquote',
  'br', 'code', 'col', 'colgroup',
  'dd', 'del', 'div', 'dl', 'dt',
  'em', 'fieldset', 'h1', 'h2', 'h3',
  'h4', 'h5', 'h6', 'hr', 'i', 'img',
  'ins', 'label', 'legend', 'li',
  'ol', 'p', 'q', 'span', 'strong',
  'sub', 'sup', 'table', 'tbody',
  'td', 'tfoot', 'th', 'thead',
  'tr', 'ul',
];

const REPLACEABLE_TAGS = [
  'pre', 'small', 'var', 'button', 'font', 'details',
  'summary', 'caption', 'figure', 'figcaption', 'dfn',
  'string', 's',
];

const REPLACE_MAP = {
  pre: 'div',
  small: 'span',
  var: 'span',
  button: 'span',
  font: 'span',
  details: 'div',
  summary: 'div',
  caption: 'div',
  figure: 'div',
  figcaption: 'div',
  dfn: 'span',
  string: 'span',
  s: 'span',
};

const IGNOREABLE_TAGS = [
  'meta',
];

function parseNode(ctx, node) {
  const { childNodes, tagName, attrs: _attrs } = node;
  let needOverrideTagName = false;

  if (node.nodeName === '#text') {
    return {
      type: 'text',
      text: entities.decodeHTML(node.value),
    };
  }

  if (AVAILABLE_TAGS.indexOf(tagName) === -1) {
    if (IGNOREABLE_TAGS.indexOf(tagName) > -1) {
      return null;
    } else if (REPLACEABLE_TAGS.indexOf(tagName) > -1) {
      needOverrideTagName = true;
    } else {
      if (tagName !== 'iframe') {
        ctx.logger.info(`UNSUPPORT TAG: ${tagName}`);
      }

      return {
        name: 'div',
        attrs: {
          class: 'content-unavailable',
        },
        children: [
          {
            type: 'text',
            text: '此处内容无法在当前环境中显示',
          },
        ],
      };
    }
  }

  let children;
  if (childNodes && childNodes.length > 0) {
    children = childNodes.map(node => parseNode(ctx, node)).filter(child => !!child);
  }

  const attrs = _attrs
    .filter(attr => AVAILABLE_ATTRS.indexOf(attr.name) > -1)
    .reduce((obj, attr) => {
      obj[attr.name] = attr.value;
      return obj;
    }, {});

  const classNames = attrs.class ? attrs.class.split(' ') : [];
  classNames.push(`__tag_${tagName}`, '__univ');
  attrs.class = classNames.join(' ');

  const nodeObj = {
    name: needOverrideTagName ? REPLACE_MAP[tagName] : tagName,
    attrs,
    children,
  };

  return nodeObj;
}

function parseHtml(ctx, html) {
  const documentFragment = parse5.parseFragment(html);
  const nodes = documentFragment.childNodes.map(node => parseNode(ctx, node));
  return nodes;
}

rich-text setData

在使用 setDatarich-textnodes 属性赋值时,如果元素过多,则可能出现页面卡顿或者数据传输长度超过最大长度的问题。此时,可以将 nodes 数组分成适量片段,分段 setData

<rich-text wx:for="{{ nodesList }}" wx:for-index="index" wx:for-item="nodes" nodes="{{nodes}}"></rich-text>
const nodes = ***;
const nodesList = [];
const lastNodes = nodes.reduce((arr, cur) => {
  const curArr = [cur];
  const newArr = arr.concat(curArr);
  if (JSON.stringify(newArr).length > (1048576 - 1024 * 1000)) {
    nodesList.push(arr);
    return curArr;
  } else {
    return newArr;
  }
}, []);
nodesList.push(lastNodes);

this.setData({
  'nodesList': []
});
nodesList.forEach((nodes, idx) => {
  if (idx === 0) {
    this.setData({
      [`nodesList[${idx}]`]: nodes,
      loading: false
    });
  } else {
    this.setData({
      [`nodesList[${idx}]`]: nodes
    });
  }
});

在项目中有需要导出 CSV 文件,用 Node.js 生成的 CSV 在 Excel 中打开中文会乱码。查阅资料后得知需要给导出的文件添加 BOM。

const filename = 'export.csv';
ctx.body = `\ufeff${csv}`; // add utf-8 bom to string
ctx.set('content-disposition', `attachment; filename=${filename}`);
ctx.set('content-type', 'text/csv');

特殊字符 (JavaScript) - MSDN

在对移动端的项目进行 E2E 测试时,需要用到 ChromeDriver 的 Mobile Emulation 来模拟移动设备。

在 Javascript 中启用 Mobile Emulation 需要如下配置。

chrome: {
  desiredCapabilities: {
    browserName: 'chrome',
    javascriptEnabled: true,
    acceptSslCerts: true,
    chromeOptions: {
      args: ['--headless'],
      mobileEmulation: {
        deviceName: 'iPhone 6'
      }
    }
  }
}

deviceName 可以在 Chrome 浏览器的 开发者工具 - Settings - Devices 中找到。