Unicode substring

September 6, 2018

最近遇到一个问题:在做字符串截取操作时,如果字符串中包含了 emoji 字符(一个表情占多个 unicode 字符),而碰巧又把它截断了,程序会出错。在 ReactNative App 下的具体表现就是崩溃。由于以前做的是网页比较多,基本没有输入表情字符的案例,而在手机上就不一样了,因此这个问题还是第一次发现。

比如说:

'😋Emoji😋'.substring(0, 2) // 😋

因此,如果对这个字符串做 substring(0, 1) 操作,就会截取到一个未知字符。

中间的探索过程就不谈了,Google 了一下解决方案,以及咨询同事们以后,发现最简单的办法是通过 lodash 自带的 toArray 方法,先将它转为数组,然后将整个逻辑改为数据的截取操作,最后再转回字符串。

export function safeSubStr (str, start, end) {
  const charArr = _.toArray(str);
  return _.slice(charArr, start, end).join('');
}

实际上解决问题的是 _.toArray,它帮我们把表情字符正确地截了出来:

_.toArray('😋Emoji😋') // ["😋", "E", "m", "o", "j", "i", "😋"]

其实我也比较好奇它是怎么做的,通过观察源码,发现了真正的解决方案:

// lodash/_unicodeToArray.js
/** Used to compose unicode character classes. */
var rsAstralRange = '\\ud800-\\udfff',
    rsComboMarksRange = '\\u0300-\\u036f',
    reComboHalfMarksRange = '\\ufe20-\\ufe2f',
    rsComboSymbolsRange = '\\u20d0-\\u20ff',
    rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange,
    rsVarRange = '\\ufe0e\\ufe0f';

/** Used to compose unicode capture groups. */
var rsAstral = '[' + rsAstralRange + ']',
    rsCombo = '[' + rsComboRange + ']',
    rsFitz = '\\ud83c[\\udffb-\\udfff]',
    rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')',
    rsNonAstral = '[^' + rsAstralRange + ']',
    rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}',
    rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]',
    rsZWJ = '\\u200d';

/** Used to compose unicode regexes. */
var reOptMod = rsModifier + '?',
    rsOptVar = '[' + rsVarRange + ']?',
    rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*',
    rsSeq = rsOptVar + reOptMod + rsOptJoin,
    rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')';

/** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */
var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g');

/**
 * Converts a Unicode `string` to an array.
 *
 * @private
 * @param {string} string The string to convert.
 * @returns {Array} Returns the converted array.
 */
function unicodeToArray(string) {
  return string.match(reUnicode) || [];
}

module.exports = unicodeToArray;

一大堆正则就不谈了,也不知道它是从哪里找来的这些值,最后组装了一个 reUnicode 正则来实现 unicode 转数组。话又说回来,这么做会不会有性能问题呢?我表示比较担忧。好在项目里面需要这么做的场景不多,字符串也不长,可以如此暴力解决。如果换个场景,还真不好说。也许又需要一种更高效的解决方案了。

失望

August 8, 2018

今年的 TI 本子到目前为止已经充了 ¥850 左右,770 级。

  • 不朽 1 没有开到极其珍稀(PA),其它齐全
  • 不朽 2 齐全,一件极其珍稀(黑鸟)
  • 不朽 3 没有开到极其珍稀(巫医),其它齐全
  • 宝瓶 1 一轮,一件稀有额外(术士)
  • 宝瓶 2 一轮,一件稀有额外(大屁股)

战绩可以说非常不尽人意。虽然中途 V 社承认自己失误(被迫?)发了一次补偿,但依然没我。

现在每周就肝肝幽穴风云,肝肝代币,箱子开了马上又是一次轮回,感觉除了中看不中用的等级以外什么都没留下。想要的东西永远开不到,除了失望以外说不出别的感受来。

今天中午又开了一个箱子,依然是熟悉的啥都没有,突然就觉得好累,有点不想肝了。人生啊。

Gitlab CI Setup

July 19, 2018

Gitlab 有一套内置的 CI 系统,相比集成 Jenkins 来说更加方便一些,用法也稍为简单。以下是搭建过程。

前置准备:须要准备一台用来跑 CI 任务的机器(可以是 Mac / Linux / Windows 之一)。

Read more »

中国药神

July 9, 2018

《我不是药神》是一部好电影。

影片最打动我的一段,是小吕请勇哥去他家吃饭的那几分钟。这些小人物倾家荡产,拼了命地活着,到底只不过就是为了一些「小事」而已。不然何苦呢?得了绝症的小吕幸福吗?从某个角度看,他非常幸福:有一个至死都不离不弃的爱人,还有一个至少到现在为止都健健康康的孩子。但生活就是这样残酷。

吃不起特效药的人,去抗议药厂卖天价药,对于不幸的患者来说,我命都快没了,管你是对是错呢?影片故意刻画了一个近乎反派立场的药厂,是不得已而为之,但我们要记住:真正对人类社会的发展做出贡献的是药厂。它卖天价药,卖任何价格,都没有问题,你永远不知道药厂为了第一片药付出了多少。至于吃不起,那是你的问题。就像影片说的一样:穷病,没法治。

影片从「病」这个角度,揭露出了绝大部分人生活在这个社会上的一些无奈。除非你有钱到刘强东这种程度,否则这个世界上总有你付不起的代价,这一刻是公平的。

这部电影好就好在,它选取了一个能够引发共鸣,但又值得深思的角度,同时把故事给讲好了。其实真的不难,真心希望它能够赚一笔大的,让大家以后都有样学样,多拍点有营养的东西。

PS. 毕导可以出来点评一下了,我猜这绝对又是境外势力的阴谋?

Simplest Wechat Client on Linux

June 11, 2018

微信没有为 Linux 提供桌面客户端,可用的替代方式有:

  1. 使用网页版微信
  2. 使用第三方客户端,如 electronic-wechat
  3. 自己动手,将网页版微信封装为桌面应用程序

但是每种方式都有不尽人意的地方。网页版总是嵌入在浏览器中,用起来不太方便;第三方客户端安全性无法保证;自己做一个客户端又太麻烦。

然而,实际上还有一种更简单的方式:通过 Chrome 将网页直接转化为桌面应用。

步骤:

  1. 使用 Chrome 打开网页版微信
  2. 右上角设置,More tools -> Create shortcut...
  3. 然后就可以在 Chrome Apps 中找到微信了

通过此方式创建的 Apps 同时拥有桌面应用的表现以及网页版的功能,并且可以将它固定到 Dock 栏,以及独立于浏览器运行,只能用「完美」两个字形容。

除微信外,其它缺少 Linux 客户端但有网页客户端的应用亦可如法炮制,如有道云笔记等。

React Native Text Inline Image

May 30, 2018

原文地址(需科学上网):medium.com

RN 版本:0.49

图文混排(在文字中插入图片,并保持正确换行)是客户端普遍的需求,但在 RN 中它有一点问题,具体表现在 Android 平台下图片显得异常的小,并且相同系统不同设备之间的表现也不尽一样,而 ios 则表现正常。

Read more »

Thoughts of ReactNative

April 27, 2018

使用 ReactNative 开发半年有余,本文是作为一些简单的感想。

官网简介:

Build native mobile apps using JavaScript and React.

简约,不简单。看着很牛逼,但实际用起来总是差了点意思。

总而言之:帮你节省时间的同时,隐藏着无处不在的坑。

Read more »

Auto-height Webview of ReactNative

April 24, 2018

自动高的 Webview 实现方式其实跟 iframe 无二,无非是计算其内容高度后再赋值给容器样式。但是普通的办法实际上用起来差强人意,其问题主要体现在页面加载过慢,需要整个页面(包括图片)加载完成后才能计算出高度。而实际想要的效果往往是跟普通“网页”的表现一致,即:先加载文字,图片等内容异步加载、显示。在尝试了多款开源解决方案后,问题均没有得到解决,因此有了自己动手的想法。

不过本方案目前也只适用于自己拼接的 HTML,不适用于直接打开链接的 Webview,应用场景主要是在 ReactNative 应用内打开由 CMS 编辑的类新闻页面。

主要思路为:通过 Webview 提供的 postMessage 交互方式,不断地从 HTML 页面把自己计算好的高度抛送给 APP 端。但是这里其实有个问题,ReactNative Webview 的 postMessage 必须在页面加载完成以后才会注入,因此可以先加载一个空白页,待 postMessage 注入完成以后,再将实际文章内容插入到 body 中。

但是这么做有一个问题就是,页面将无法知道真正的内容“是否已加载完”,因为 window.onload 事件在加载开始之前就已经结束了。因此它只能不停地抛送高度信息,直到页面被销毁。

核心代码(HTML):

<html>
<head>
    <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
    <script>
      var inserted = false;
      var interval = setInterval(function () {
        var body = document.body, html = document.documentElement;
        var height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
        if (window.postMessage) {
          if (!inserted) {
            document.body.innerHTML = '${valueParsed}';
            inserted = true;
          }
          window.postMessage(height + '');
        }
        if (document.readyState === 'complete') {
          //clearInterval(interval)
        }
      }, 200);
    </script>
</head>
<body></body>
</html>

核心代码(App):

export default class AutoHeightWebview extends PureComponent {
  constructor (props) {
    super(props);
    this.state = {
      webviewHeight: 0
    };
  }

  assembleHTML = (value) => {
    // 组装HTML,略
  };

  onMessage = (event) => {
    const webviewHeight = parseFloat(event.nativeEvent.data);
    if (!isNaN(webviewHeight) && this.state.webviewHeight !== webviewHeight) {
      this.setState({webviewHeight});
    }
  };

  render () {
    const HTML = this.assembleHTML(this.props.html);
    const onLoadEnd = this.props.onLoadEnd || function () {};
    // 防止 postMessage 与页面原有方法冲突
    const patchPostMessageFunction = function () {
      var originalPostMessage = window.postMessage;
      var patchedPostMessage = function (message, targetOrigin, transfer) {
        originalPostMessage(message, targetOrigin, transfer);
      };
      patchedPostMessage.toString = function () {
        return String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage');
      };
      window.postMessage = patchedPostMessage;
    };

    const patchPostMessageJsCode = '(' + String(patchPostMessageFunction) + ')();';

    return (
      <WebView
        injectedJavaScript={patchPostMessageJsCode}
        source={{html: HTML, baseUrl: 'http:'}}
        scalesPageToFit={Platform.OS !== 'ios'}
        bounces={false}
        scrollEnabled={false}
        startInLoadingState={false}
        automaticallyAdjustContentInsets={true}
        onMessage={this.onMessage}
        onLoadEnd={onLoadEnd}
      />
    );
  }
}

关于 Moment.js 的一些思考

January 24, 2018

Moment.js 是一个流行的基于 JavaScript 的时间处理工具库。应该是一个从 2011 年开始启动的项目,至今它的 Github repo 也有了 3w+ 的星星,可以说在前端界人尽皆知了。反正我自从用了它基本上就没再接触过其它的相关库。

但最近我却对它的看法却产生了些许改变。原因是,它的 API 设计给使用者埋下了巨大无比的坑,简单来说:“名不副实”。

Read more »

Linux Setup for Work

January 9, 2018

因为各种烦人的原因,公司搬家后到新办公室第一件事先把老电脑格了。犹豫了一下,最终还是放弃了重装 Windows,支持我做出选择的原因有几:

  • 不需要进行(纯)MS 系开发
  • 没有必须使用的 Windows 软件
  • Windows 上跑 Android emulator 卡得头疼
  • NVIDIA 已有支持 Linux 的官方显卡驱动
  • Linux 开发效率更高
  • Linux 学习价值更高

本文是办公室适用(对我来说)的安装记录。

Read more »