なめこ備忘録

プログラミングに関する備忘録や経験したこと,考えたことなど好き勝手書きます.

[tex: ]

PC以外でもJSがしたい

こんにちは,なめこです.

今回はちょっとした思いつきに従ってReact Nativeで遊んでみます.

React NativeはiOSAndroid端末で動くアプリをJavaScriptで作れるフレームワークです.

以下の記事に構成に関する説明がされてます.

qiita.com

さて,まあ構成に関しては軽く流してもいいのですが,今回注目したのは以下の文

React Nativeで楽に作るスマホアプリ開発入門(基本編) - Qiita React NativeはWebkitスマホブラウザ)のJavaScriptランタイムで動く(つまり、スマホブラウザ上で動くようなコードを書いているイメージ)

要するに端末上でJSが動いてるわけですね.
それならアプリ上で任意のJSプログラムを動かすことも可能なのでは? と思い立ってしまったので挑戦してみることにしました.
上手くいけばタブレットなどでもNodeのプログラム実行ができるはずです.

今回はひとまずターミナルでのNodeの対話環境に近いものを目指します.

import周りとかstyle sheetに関する部分は本筋ではないので省略します. まとめたプログラムは最後に置いておきます.

前準備

ターミナル上で必要となる表示をstateとして用意しておきます. 入力と出力を履歴含めて保持できれば十分なのでstateの中身は少ないです.

this.state = {
  // 入力された文字列
  inputValue: '',
  // 入力履歴と出力のリスト
  list: [],
  // 入力履歴のリスト
  historyList: [],
};

JSが動くようにする

デフォルトで生成されるAppクラスの内部に関数_onPressを定義します. これは入力決定時(現時点ではENTERボタンを押した時)に実行されます.

そしてこの中で悪魔の関数を呼び出します. 文字列をJSのプログラムとして評価し,実行するevalさんです.
ある意味で便利ではありますが,危険度の高い関数でもあるのでご利用は計画的に.

evalの詳細は以下

developer.mozilla.org

_onPress = () => {
    // 現在のstateを取得
    const {inputValue, list, historyList} = this.state;

    // 入力文字を実行し,実行結果を取得
    const result = eval(inputValue);
    
    // 実行コードと結果を追加
    const _list = list.concat();
    const _historyList = historyList.concat();
    _list.push(inputValue+'');
    _list.push(result+'');
    _historyList.push(inputValue+'');

    // stateを更新
    this.setState({
      inputValue:'',
      list:_list,
      historyList:_historyList
    });
  };

全体を含む実行画面はこんな感じです.
ひとまずevalは機能しているようです.

f:id:NAMEKO:20190220233244p:plain

変数が保持されるようにする

先ほどの入力状態から以下のような入力をするとエラーが出ました.

f:id:NAMEKO:20190220233251p:plain

f:id:NAMEKO:20190220233254p:plain

_onPress内のevalで変数aにアクセスしたことでエラーが出てます.
原因は割とお察しですが一応varなしでの挙動も確認しておきましょう.

f:id:NAMEKO:20190220233257p:plain

案の定普通に動きました. varなどの宣言子を用いて変数宣言を行った場合,関数処理後にそのスコープを抜け,その場で宣言されたローカル変数は消えます. しかし何もついていない場合,グローバル変数として作られるので変数が残ります.

今回はvarで宣言した変数aが消えているので,ないものにアクセスしようとしてエラーになってるわけです.

varの他constやletも同様にエラーとなるので,とりあえず気にせずできるように今はこれらの文字列を消しておきます.

文字列の変換関数_encodeを定義してevalの実行前に呼び出すようにします.

// '(var|const|let) 'を削除
_encode = (str) => str.replace(/(var|const|let) /g, '');
// 入力文字を実行し,実行結果を取得
const result = eval(this._encode(inputValue));

f:id:NAMEKO:20190220233301p:plain

これで無理やりではありますがとりあえず動くようになりました.

文字列が使えるようにする

これで変数周りは一通り完了...だと良かったのですがまだ問題がありました.

f:id:NAMEKO:20190220233304p:plain

文字列を扱おうとこんなプログラムを書いてみると以下のようなエラーが出ました.

f:id:NAMEKO:20190220233308p:plain

\u2018????
と一瞬なりましたがUNICODEの「' (シングルクォーテーション)」ですね.
どうやら入力はUNICODEのようです. 明示的に変換してあげましょう.

_encode = (str) => str.replace(/(var|const|let) /g, '').replace(/(\u2018|\u2019|\u201c|\u201d)/g, "'");

ついでに「" (ダブルクォーテーション)」の方も変換してあります.

f:id:NAMEKO:20190220233312p:plain

これで文字列も使えるようになりました.

もちろんオブジェクトや配列も利用できます.

f:id:NAMEKO:20190220233317p:plain

変数宣言以外での宣言子の文字列を残す

文字列を扱えるようになってから気づきましたが,現状だと「var 」がどこにあっても削除されてしまいます

f:id:NAMEKO:20190221000909p:plain

正規表現をうまく使って文頭にある場合にのみ削除するようにします. 文頭指定だけだと「 var 」みたいな形になると反応しなくなるので,文頭から空白が続いた場合も対象になるようにします.

_encode = (str) => str.replace(/^ *(var|const|let) /g, '').replace(/(\u2018|\u2019|\u201c|\u201d)/g, "'");

f:id:NAMEKO:20190221001353p:plain

これでひとまずおかしな挙動は消えたと思います.

まとめ

今回は対話形式でのプログラミングができるようにしてみました.

変数宣言周りは無理やりなのでもう少しいい方法がないか検討してみますが,しばらくはこのままになりそうです.

少しずつ体裁とか色々整えていって色々できるようにしていきたいです.

プログラム

import React, {Component} from 'react';
import {
  Platform, 
  StyleSheet, 
  Alert,
  Text, 
  TextInput, 
  Button, 
  ImagePicker,
  Permissions,
  View,
  TouchableOpacity,
  FlatList
} from 'react-native';

type Props = {};
export default class App extends Component<Props> {
  constructor(props){
    super(props);
    this.state = {
      inputValue: '',
      // 入力履歴と出力のリスト
      list: [],
      // 入力履歴のリスト
      historyList: [],
    };
  }

  // テキスト入力の反映
  _onChangeText = inputValue => this.setState({inputValue});
  _onPress = () => {
    const {inputValue, list, historyList} = this.state;

    // 入力文字をエンコードした上で実行し,実行結果を取得
    const result = eval(this._encode(inputValue));
    // if(typeof(result) === 'string' || result instanceof String){
    //   result = `'${result}'`;
    // }
    // Alert.alert(result);
    // const result = this._data(this._encode(inputValue));
    // const result = eval(inputValue);
    
    // 実行コードと結果を追加
    const _list = list.concat();
    const _historyList = historyList.concat();
    _list.push(inputValue+'');
    _list.push(result+'');
    _historyList.push(inputValue+'');

    this.setState({
      inputValue:'',
      list:_list,
      historyList:_historyList
    });
  };

  // '(var|const|let) 'を削除,''や""を使用できるようにする
  _encode = (str) => str.replace(/^ *(var|const|let) /g, '').replace(/(\u2018|\u2019|\u201c|\u201d)/g, "'");
  
  render() {
    const {
      inputValue,
      list,
      historyList
    } = this.state;
    
    return (
      <View style={styles.container}>
        <FlatList style={styles.list} data={list} renderItem={({item}) => <Text style={[styles.item, styles.color]}>{item}</Text>} />
        
        <View style={styles.separator}/>
        <View style={styles.input}>
          <Text style={styles.color}>{'> '}</Text><TextInput style={[styles.inputArea, styles.color]} value={this.state.inputValue} onChangeText={this._onChangeText}/>
          <TouchableOpacity onPress={this._onPress}>
            <Text style={styles.color}> ENTER </Text>
          </TouchableOpacity>
        </View>

      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#1D1D1D',
  },
  input: {
    flexDirection:'row',
    height:20,
    margin:10,
  },
  list: {
    flex:1,
    margin:10,
    marginTop: 20,
  },
  item: {
    fontSize: 15,
    textAlign: 'left',
  },
  inputArea: {
    fontSize: 15,
    flex: 1,
  },
  color: {
    color: '#FEFEFE',
  },
  separator: {
    height: 1,
    backgroundColor: '#FEFEFE',
  },
});