拡張性のあるクイズアプリ⑪

基本情報技術者試験のサンプル問題 科目Aを用いて、アプリを作成し、PWA化してみる。

この回で、拡張性のあるクイズアプリ制作は終了です。

次は、拡張性のあるメモ帳アプリ作成やっていこうと思います。

 

作成中のメモです。

・データベースを使用しないので、選択肢に()が入っているものがある場合に、()使用による問題は発生しない。

拡張性のあるクイズアプリ⑩

選択肢をシャッフルできるようにしました。

App.js

import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View, Button } from "react-native";
import { useEffect, useState } from "react";
import { dataSetArray } from "./setData/dataSet";

export default function App() {
  const [problemText, setProblemText] = useState("");
  const [correctAnswerText, setCorrectAnswerText] = useState("");
  const [correctAnswerAlp, setCorrectAnswerAlp] = useState("");
  const [explanationText, setExplanationText] = useState("");

  const [totalAnswerNumber, setTotalAnswerNumber] = useState(0);
  const [correctAnswerNumber, setCorrectAnswerNumber] = useState(0);

  const [selectionArraySet, setSelectionArraySet] = useState();
  const [selectionSetText, setSelectionSetText] = useState("");

  const [judgementText, setJudgementText] = useState("");

  const [explanationSentence, setExplanationSentence] = useState("");
  const [finishSentence, setFinishSentence] = useState("");

  const createProblemHandler = () => {
    const randomNumber = Math.floor(Math.random() * 1);
    setProblemText(dataSetArray[randomNumber].problem);
    setCorrectAnswerText(dataSetArray[randomNumber].correctAnswer);
    setExplanationText(dataSetArray[randomNumber].explanation);
    setSelectionArraySet(dataSetArray[randomNumber].selectionArray);
    const selectionArrSet = [...selectionArraySet];
    //シャッフル
    const shuffleArray = (arr) => {
      for (let i = arr.length - 1; i > 0; i--) {
        let j = Math.floor(Math.random() * (i + 1));
        let tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
      }
      return arr;
    };
    shuffleArray(selectionArrSet);

    setSelectionSetText(
      `a.${selectionArrSet[0]}\r\nb.${selectionArrSet[1]}\r\nc.${selectionArrSet[2]}\r\nd.${selectionArrSet[3]}`
    );

    const getCorrectAlp = () => {
      const linearSearch = (array, target) => {
        //見つかったらその時点で終了
        for (let i = 0; i < array.length; i++) {
          if (array[i] == target) {
            return i;
          }
        }
        return -1;
      };

      switch (linearSearch(selectionArrSet, correctAnswerText)) {
        case -1:
          console.log("ありませんでした");
        case 0:
          return "a";
        case 1:
          return "b";
        case 2:
          return "c";
        case 3:
          return "d";
      }
    };
    setCorrectAnswerAlp(getCorrectAlp());

    setExplanationSentence("");
    setJudgementText("");
    setFinishSentence("");
  };

  const pressHandler = (buttonTitle) => {
    setTotalAnswerNumber((prevState) => prevState + 1);

    if (buttonTitle == correctAnswerAlp) {
      setJudgementText("Correct");
      setExplanationSentence("");
      setCorrectAnswerNumber((prevState) => prevState + 1);
    } else {
      setJudgementText("InCorrect");
      setExplanationSentence(explanationText);
    }
  };

  const pressHandler_a = () => {
    pressHandler("a");
  };

  const pressHandler_b = () => {
    pressHandler("b");
  };

  const pressHandler_c = () => {
    pressHandler("c");
  };

  const pressHandler_d = () => {
    pressHandler("d");
  };

  const finishHandler = () => {
    setFinishSentence(
      `解いた問題数は${totalAnswerNumber}です。\r\n 正解した問題数は${correctAnswerNumber}です。`
    );
  };

  //1回だけ
  useEffect(() => {
    createProblemHandler();
  }, );

  return (
    <View style={styles.container}>
      <View style={styles.upperButtonContainer}>
        <Button title="次の問題へ" onPress={createProblemHandler}></Button>
        <Button title="終了" onPress={finishHandler}></Button>
      </View>
      <Text style={{ marginTop: 20, marginBottom: 30, fontSize: 20 }}>
        {problemText}
      </Text>
      <Text style={{ fontSize: 20 }}>{selectionSetText}</Text>
      <View style={styles.lowerButtonContainer}>
        <Button title="a" onPress={pressHandler_a}></Button>
        <Button title="b" onPress={pressHandler_b}></Button>
        <Button title="c" onPress={pressHandler_c}></Button>
        <Button title="d" onPress={pressHandler_d}></Button>
      </View>
      <Text style={{ fontSize: 20, marginTop: 30 }}>{judgementText}</Text>
      <Text style={{ fontSize: 20 }}>{explanationSentence}</Text>
      <Text style={{ fontSize: 20 }}>{finishSentence}</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  upperButtonContainer: {
    flexDirection: "row",
    justifyContent: "space-around",
    width: "80%",
  },
  lowerButtonContainer: {
    flexDirection: "row",
    justifyContent: "space-around",
    width: "80%",
  },
});

setData/dataSet.js

export const dataSetArray = [
  {
    id: 1,
    problem: "コンパイラによる最適化の主な目的はどれか。",
    correctAnswer: "プログラムの実行時間を短縮する",
    selectionArray: [
      "プログラムの実行時間を短縮する",
      "プログラムのデバッグを容易にする",
      "プログラムの保守性を改善する",
      "目的プログラムを生成する時間を短縮する",
    ],
    explanation:
      "コンパイラは、高水準言語で書かれたソースプログラムを機械語コンパイル(翻訳)し、プログラムを生成するソフトウェアです。コンパイルの手順は、1.字句解析、2.構文解析、3.意味解析、4.最適化、5.コード生成の順で行われる。このうち、4の最適化では、処理時間や使用するメモリ量が少なくなるようにプログラムを再編成します。",
  },
];

 

注意点としては下記。

getCorrectAnswerAlp を作らず、switch文のところで、各自、setCorrectAnswerAlpを設定してしまうと、レンダリングが複数回されてしまい、変なことになってしまいます。

なお、AlpはAlphabetを略してます。

 

そのあと、データセットのデータを複数にすると、問題文と選択肢が食い違うバグあり。

useEffectのところを下記へ書き換えると、Console Warning出ながらも実行は可能

  useEffect(() => {
    createProblemHandler();
  }, [problemText, selectionSetText]);

原因はおいおい考えることとする。

PWA再入門~React Native(Expo)アプリのPWA化~

下記HPを参考にしました。

Expoで制作したReactNativeアプリをPWA対応Webビルドする必要最小限の手順 #SPA - Qiita

fyi/enabling-web-service-workers.md at master · expo/fyi · GitHub

 

expo customize:web 周辺からやっていきます。

 

ここでは、㋐と違って、㋑に忠実に従ってやってます。

以下、㋑の抜粋です。

Enabling web service workers

 

Start from a template: npx create-react-native-app -t with-workbox・・⓵

Expo's Webpack config has is capable of generating icons, splash screens, manifests, and metadata for your progressive web app based on the app.json and other configuration used for your native app. However, the offline support must be added to your Webpack config manually.

  1. Install the required Workbox dependencies: package.json
  2. Create a local webpack.config.js in your project: expo customize:web
  3. Copy the template webpack.config.js
  4. Create the src/service-worker.js and src/serviceWorkerRegistration.js (the file path is important).
  • Optionally, you can create a noop file for native. ・・②
  1. In your App.js (or other entry file) import the registration and invoke the register method.

①は無視してよいです。下線部のコードからもコピーして取ってこれるからです。

1.では、提供されるpackage.jsonから、コードをコピーし、自分のプロジェクトのpackage.jsonにペーストします。

npx expo install --fixとしておきます。

2. expo customize:webをコマンドプロンプトで実行

3.下線部のwebpack.config.jsをすべてコピーして、自分のプロジェクトのwebpack.config.jsにペースト

4.Expoでは、プロジェクト直下にsrcディレクトリはないので、自分で作成します。その下に、それぞれのファイルをコピー&ペーストで作成します。

②も無視でよいです。

5.下記のようにコードを追記します。③と④。

import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";

import * as serviceWorkerRegistration from "./src/serviceWorkerRegistration";・・⓷

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Hello!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

serviceWorkerRegistration.register();・・・⓸

 

次に、

npx expo install react-native-web react-dom

npx expo install --fix

最近は、コマンドが下記のように変更になってます。

expo build:web ⇒ npx expo export:web

 

ローカルサーバで確認

npx serve web-build --single

 

それでは次に、web-buildディレクトリを、firebase hostingします。

npm install -g firebase-tools

firebase init

firebase.json内の  ”public":"public"を "public":"web-build"に変更

{
  "hosting": {
    "public": "web-build",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
  }
}

firebase deploy

 

Deploy complete!と出れば成功です。

 

https://[firebaseのプロジェクト名].web.appのようなURLが出ます。

.comで終わるURLも使用できます。firebaseの方で確認できます。

スマホでこのURLにアクセスすると、ウェブブラウザで表示され、右上の縦3ドットを押して、メニュー表示し、ダウンロードを押すとダウンロード、インストールされ、その後はオフラインでも一部使用可なようです。PCなら、Microsoft Edgeで開き、同様に、右上の縦3ドットから、アプリをインストールすることができます。

 

拡張性のあるクイズアプリ⑨

変数名は大事なので改変。

import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View, Button } from "react-native";
import { useEffect, useState } from "react";
import { dataSetArray } from "./setData/dataSet";

export default function App() {
  const [problemText, setProblemText] = useState("");
  const [correctAnswerText, setCorrectAnswerText] = useState("");
  const [explanationText, setExplanationText] = useState("");

  const [totalAnswerNumber, setTotalAnswerNumber] = useState(0);
  const [correctAnswerNumber, setCorrectAnswerNumber] = useState(0);

  const [judgementText, setJudgementText] = useState("");

  const [explanationSentence, setExplanationSentence] = useState("");
  const [finishSentence, setFinishSentence] = useState("");

  const createProblemHandler = () => {
    const randomNumber = Math.floor(Math.random() * 2);
    setProblemText(dataSetArray[randomNumber].problem);
    setCorrectAnswerText(dataSetArray[randomNumber].correctAnswer);
    setExplanationText(dataSetArray[randomNumber].explanation);
    setExplanationSentence("");
    setJudgementText("");
    setFinishSentence("");
  };

  const pressHandler = (buttonTitle) => {
    setTotalAnswerNumber((prevState) => prevState + 1);

    if (buttonTitle == correctAnswerText) {
      setJudgementText("Correct");
      setExplanationSentence("");
      setCorrectAnswerNumber((prevState) => prevState + 1);
    } else {
      setJudgementText("InCorrect");
      setExplanationSentence(explanationText);
    }
  };

  const pressHandler_a = () => {
    pressHandler("a");
  };

  const pressHandler_b = () => {
    pressHandler("b");
  };

  const pressHandler_c = () => {
    pressHandler("c");
  };

  const pressHandler_d = () => {
    pressHandler("d");
  };

  const finishHandler = () => {
    setFinishSentence(
      `解いた問題数は${totalAnswerNumber}です。 正解した問題数は${correctAnswerNumber}です。`
    );
  };

  useEffect(() => {
    createProblemHandler();
  }, []);

  return (
    <View style={styles.container}>
      <View style={styles.upperButtonContainer}>
        <Button title="次の問題へ" onPress={createProblemHandler}></Button>
        <Button title="終了" onPress={finishHandler}></Button>
      </View>
      <Text style={{ marginTop: 20, marginBottom: 30, fontSize: 20 }}>
        {problemText}
      </Text>
      <View style={styles.lowerButtonContainer}>
        <Button title="a" onPress={pressHandler_a}></Button>
        <Button title="b" onPress={pressHandler_b}></Button>
        <Button title="c" onPress={pressHandler_c}></Button>
        <Button title="d" onPress={pressHandler_d}></Button>
      </View>
      <Text style={{ fontSize: 20, marginTop: 30 }}>{judgementText}</Text>
      <Text style={{ fontSize: 20 }}>{explanationSentence}</Text>
      <Text style={{ fontSize: 20 }}>{finishSentence}</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  upperButtonContainer: {
    flexDirection: "row",
    justifyContent: "space-around",
    width: "80%",
  },
  lowerButtonContainer: {
    flexDirection: "row",
    justifyContent: "space-around",
    width: "80%",
  },
});

 

export const dataSetArray = [
  {
    id: 1,
    correctAnswer: "a",
    problem:
      "コンパイラによる最適化の主な目的はどれか。 a. プログラムの実行時間を短縮する b. プログラムのデバッグを容易にする c. プログラムの保守性を改善する d. 目的プログラムを生成する時間を短縮する",
    explanation:
      "コンパイラは、高水準言語で書かれたソースプログラムを機械語コンパイル(翻訳)し、プログラムを生成するソフトウェアです。コンパイルの手順は、1.字句解析、2.構文解析、3.意味解析、4.最適化、5.コード生成の順で行われる。このうち、4の最適化では、処理時間や使用するメモリ量が少なくなるようにプログラムを再編成します。",
  },
  {
    id: 2,
    correctAnswer: "a",
    problem:
      "WAFの説明はどれか a. Webアプリケーションへの攻撃を検知し、阻止する b. Webブラウザの通信内容を改ざんする攻撃をPC内で監視し、検出する c. サーバのOSへの不正なログインを監視する d. ファイルへのマルウェア感染を監視し、検出する",
    explanation:
      "",
  },
  {
    id: 3,
    correctAnswer: "a",
    problem:
      "業務への利用には、会社の情報システム部門の許可が本来は必要なのに、その許可を得ずに勝手に利用されるデバイスクラウドサービス、ソフトウェアを指す用語は? a. シャドーIT b. ソーシャルエンジニアリング c. ダークネット d. バックドア",
    explanation:
      "シャドーITとは、組織の公式な手続きを経ずに、業務に使用されているIT機器や情報システムのことです。",
  },
];
 

また、問題の形式が、4択でもすこし違う問題もある。たとえば、『~についての下記の文章のうち、正しいものはどれか。1つ選べ。』 とか。

今度はそういう場合について考えてみる。

データセットのオブジェクトリテラルのプロパティにselectionを付け加え、

そこに、配列を設定する。trueの文章2つとfalseの文章4つを用意し、trueを1つ、falseを3つ選ぶようにすれば、上記のような問題になる。

selection: [{boolean:true, sentence:"~は・・・である"}, {boolean:false, sentence:"~~は***である"}]

 

 

拡張性のあるクイズアプリの作成⑧

次は、間違えた問題だけ、復習できるようにしたい。(用意されてるデータセットは、オブジェクトの配列だが、それぞらのオブジェクトにisCorrectというプロパティを追加する。

その次は、間違えて復習してわかった問題だけ集めて、もう一回解きたいという要望に答えることとする。

 

他には、画像を見て答える問題にも対応も検討(オブジェクトのidを使って、このidならこの画像を選択肢のあとに表示し、このidなら画像表示なしという関数をつくる。別のファイルに作ってインポートで良い。あるいは、この場合は、オブジェクトに、画像があるものだけ、pictureプロパティを追加するのもありだが、その場合、データベースに移行する場合は、firebaseなどのデータベースを使うことになる。)

 

 

拡張性のあるクイズアプリ簡易版⑦

データセットのオブジェクトの中に、selectionTextを追加します。

 

export const problemArrayData = [
  {
    id: 1,
    correctAnswer: "財務,顧客,内部ビジネスプロセス,学習と成長の四つの視点ごとの課題,施策,目標の間の因果関係を表現したもの",
    problemText:
      "バランススコアカードで使われる戦略マップの説明はどれか",
    selectionText: [
      "切り口となる二つの要素をX軸,Y軸として,市場における自社又は自社製品のポジションを表現したもの",
      "財務,顧客,内部ビジネスプロセス,学習と成長の四つの視点ごとの課題,施策,目標の間の因果関係を表現したもの",
      "市場の魅力度,自社の優位性の二つの軸から成る四つのセルに自社の製品や事業を分類して表現したもの",
      "どのような顧客層に対して,どのような経営資源を使用し,どのような製品・サービスを提供するのかを表現したもの",
    ],
    explanationSentence:
      "戦略マップは、バランススコアカードの4つの視点(財務,顧客,業務プロセス,学習と成長)のそれぞれの戦略目標に、それらの因果関係をつなぐ矢印を書き加えてビジネス戦略を可視化したものです。",
  },
  {
    id: 2,
    correctAnswer: "無線LANのセキュリティ規格",
    problemText:
      "WPA3はどれか",
    selectionText: [
      "HTTP通信の暗号化規格",
      "TCP/IP通信の暗号化規格",
      "Webサーバで使用するデジタル証明書の規格",
      "無線LANのセキュリティ規格",
    ],
    explanationSentence:
      "WPA3(Wi-Fi Protected Access 3)は、無線LANのセキュリティプロトコルWPA2の後継となる次期バージョンです。WPA2には2017年11月に「KRACKs」と呼ばれる4ウェイハンドシェイク時における深刻な脆弱性が見つかっており、翌2018年に発表されたWPA3ではセキュリティのさらなる強化が図られています。具体的には、管理フレーム暗号化を行うPMF機能の使用を必須とするほか、パーソナルモード、エンタープライズモードそれぞれで変更が行われています。WPA3-Personal 楕円曲線暗号を使用してクライアントとアクセスポイント間の鍵共有を行うSAE(Simultaneous Authentication of Equals)を認証時に追加し、辞書攻撃、総当たり攻撃から保護する。→「KRACKs」の無効化 WPA3-Enterprise 暗号化アルゴリズムとして、WPA2のCCMP(AES128ビット)よりも強度の高いCNSA準拠の192ビットブロック暗号を選択可能になった。",
  },
];