20210105のJavaScriptに関する記事は15件です。

業界未経験の私がポートフォリオを作って転職をしたい件

自分で作ったポートフォリオサイト・作品集(作成途中)

https://github.com/yapiro/

githubのurlに今まで作ったアプリが入っているので、ぜひご覧ください。

「topページ」
スクリーンショット 2021-01-05 21.12.34.png
ropページは、誰もが操作しやすく簡単に理解できる見た目にしてみました。構成は4つ。①自分のプロフィール②スキル/制作実績③学習の積み上げ報告(twitter)④Qiita
この4つを意識することが転職活動を成功に導くと仮定して選びました。

「作品集(サイトを見ればもう一つあります)」
スクリーンショット 2021-01-05 21.18.17.png
作品集はスキル/制作実績ページにあります。画像向かって左側には自分の趣味であるウイスキーや旅、筋トレをいろんな人が主体的に投稿して楽しめたらいいなという思いで作りました(未完成)。

作ってみたきっかけ

きっかけは単刀直入にエンジニアになることでした。
東日本大震災で被災した友人から当時の話を聞いたことをきっかけに、人のために働きたいという想いが根底にあり、今一番感動させられるのはIT技術だと思ったからです。今やスマホがないと生きていけない人がほとんどな世の中で、自分自身がサービス開発に携わり、サービス利用者を満足させたいと思いました。

使用言語

フロントエンド言語であるjavascriptを選びました。その中でもSPA(シングルページアプリケーション)を実装したかったので、reactとvueで作ってました。
前述したポートフォリオサイトはreactを使用しました。

サイト概要

https://yapiro.github.io/pf-site/
・このサイトは主に自分を知ってもらうために作成
・自己紹介をはじめ、作品、学習記録をアピールすることが目的

アプリ概要

https://yapiro.github.io/portfolio/
reactで作りました。
・ブログ簡易掲示板
・投稿機能
・写真投稿機能

使い方

上半分はブログのため、見たい記事があればクリック
下半分は3つの項目(ウイスキー・旅・筋トレ)をみんなと共有したいときにクリック

ローカル環境へのインストール方法

git clone https://yapiro.github.io/portfolio/
npm install
npm start

現在

現在もアプリは作成途中です。
作成した際はhttps://github.com/yapiroに順次掲載していきます。
https://twitter.com/1m8pseqcODr2idm
twitterでもリンクを貼っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[ JavaScript 入門 ] 演算を行うメソッドをクロージャーを用いて作成してみる!

演算を行うメソッドをクロージャーを用いて作成してみる!

// 演算を行うメソッドを持ったオブジェクトを
// クロージャーを用いて作成してみる。 

const calc = calcFactory(10); // 初期値を10として設定
calc.plus(5);      // 出力結果 "10 + 5 = 15"

まずcalcFactory実行して初期値を設定できるようにする:point_up:
そして、オブジェクトを返す為、returnを記述。

function calcFactory(val){
  return {
  }
}

その中にメソッドとしてplusを格納する必要がある:raised_hand:

function calcFactory(val){
 return {
 plus: function(target) {
            const newVal = val + target;
            console.log(`${val} + ${target} = ${newVal}`);  
        },      
    };
}

plus(5); このメソッド自体引数を持っているので、function(target)としてあげる!
const newVal = val + target; のところで渡ってきたvaltargetを足してあげる必要があるのでこの記述。:v:   

このようすることで、出力したい出力結果 "10 + 5 = 15"というのは、 plus:のメソッド内では全て参照可能になるので、valには10がわたり、targetにはplus(5)の5が渡る:runner_tone2:   

この出力結果に合うようにコンソールに出力する:point_up_2:

function calcFactory(val){
 return {
 plus: function(target) {
            const newVal = val + target;
            console.log(`${val} + ${target} = ${newVal}`);  
        },      
    };
}
const calc = calcFactory(10);
calc.plus(5); 

このようにすることによって、コンソールに10 + 5 = 15と出力することができる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript (ES6) 基礎編 switch文とは?

こちらではJavaScript学習の備忘録となります。
プログラミング初心者や他の言語にも興味、関心をお持ちの方の参考になれば幸いです。


switch文とは?

if文以外の条件分岐の方法。
ある値によって処理を分岐する場合に、switch文を用いる。

switch文の書き方とは?

switch ( 変数や定数などの条件の値 ) { 処理 }

また、switch文の中に「 case 」を追加することで処理を分けることができる。

script.js
// switch{}の中にcaseを追加して処理を分ける
switch (条件の値) {
  case 値1:
    // 「条件の値」 が 「値1」 と等しいときの処理
    break;
}

switch文では、分岐の数だけcaseを追加していく。

script.js
// 分岐の数だけcaseを追加する
switch (条件の値) {
  case 値1:
    // 「条件の値」 が 「値1」 と等しいときの処理
    break;
  case 値2:
    // 「条件の値」 が 「値2」 と等しいときの処理
    break;
  case 値3:
    // 「条件の値」 が 「値3」 と等しいときの処理
    break;
}

上記「break」について

switch文では、breakが非常に重要。
breakとは、switch文を終了する命令である。
breakがないと、合致したcaseの処理を行った後、その次のcaseの処理も実行してしまう。
そのため、switch文を使うときはbreakを忘れないように気をつける。

defaultとは?

switchの条件の値がcaseの値と一致したとき、その部分の処理が実行されるが、caseのどれにも一致しなかったとき、defaultの処理が実行される。
defaultは、if文のelseに似たようなものである。

script.js
switch (条件の値) {
  case 値1:
    処理
    break;
  case 値2:
    処理
    break;
  case 値3:
    処理
    break;
  default:
    処理 // 「条件の値」が値1、値2、値3のどれとも一致しなかった場合に実行
    break;
}

おわりに

値の後ろのコロン「 : 」をセミコロン「 ; 」で記述しないように気をつける。
はじめてのswitch文だったので備忘録として記事にしました。
何かありましたらご指摘願います。
宜しくお願いいたします!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 初級者から中級者へ

ブラウザとJSエンジン

ブラウザの機能の一つにJavaScript Engineがあり、chromeではV8エンジンが使われている
node.jsなどのソフトウェア上でも動く
Universal JavaScriptはソフトウェアやブラウザなどいろいろな環境でJavaScriptが使えるようにしようという設計論
ブラウザごとに様々なエンジンが使われている

JSエンジンの中身

JSエンジンの中身とは
・ECMAScript
・Web APIs
である

ECMAScriptとはー

JavaScriptとECMAScript(初心者向け)
https://qiita.com/YS114411/items/d69db9b4a0801f548864

Web APIs とは
Web上の API
API = Application Programming Interface
つまり、アプリケーションとプログラムをつなぐもの
ソフトウェアやサービス間で機能を共有したりする
ここにJSエンジンがある

コードが実装されるまで

JavaScriptコード実行前
JSエンジン
・コード
・グローバルオブジェクト
(window(Web APIs))
・this

グローバルオブジェクトとは

どこからでもアクセスできるオブジェクト
JavaScriptコード実行前にはこれとthisが準備される
ブラウザの環境ではWindowオブジェクトとなる

実行コンテキスト

コードを実行する際の文脈・状況

3つの種類
・グローバルコンテキスト
・関数コンテキスト
・evalコンテキスト

グローバルコンテキスト
ー実行中のコンテキスト内の変数・関数
ーグローバルオブジェクト
ーthis

関数コンテキスト
ー実行中のコンテキスト内の変数・関数
ーarguments
(super)
ーthis
ー外部変数

グローバルコンテキスト

let a = 0;
//直下に書かれたコードが実行される環境
function b () {}

console.log(a);
b();

関数コンテキスト

let a = 0;  //外部変数
//{}に書かれたコードが実行される環境
function b () {
    console.log(this, arguments, a)
}

//console.log(a);
b();
thisの値

Window {window: Window, self: Window, document: document, name: "", location: Location, …}


コールスタック

実行されたコードがたどってきたコンテキストの積み重ね

function a() {    //ここから処理 ↓ 実行が完了すると消滅
}
function b() {
    a();
}
function c() {
    b();
}
c();              // ↑ ここから積む

//後入れ先出し Last in, First out

chrome
ブラウザでF12 > Sources
Step in 処理を積む
Step out 処理を実行する

ホイスティング

コンテキスト内で宣言した変数や関数の定義をコード実行前にメモリーに配置する

varの場合

a();

function a() {
    console.log('a is called');
}


var b = 0;

console.log(b);

a is called
undefined

//undefinedの処理
.
.

var b;

console.log(b);

b = 0;

let constの場合

//error
main.js:19 Uncaught ReferenceError: Cannot access 'b' before initialization

関数式

a();

const a = function () {
    let c = 1;

    console.log(c);

    d();
    function d() {
        console.log('d is called');

    }
    console.log('a is called');
}


const b = 0;

console.log(b);


//error
main.js:1 Uncaught ReferenceError: Cannot access 'a' before initialization
    at main.js:1

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 初級者から中級者へ(実行環境編)

ブラウザとJSエンジン

ブラウザの機能の一つにJavaScript Engineがあり、chromeではV8エンジンが使われている
node.jsなどのソフトウェア上でも動く
Universal JavaScriptはソフトウェアやブラウザなどいろいろな環境でJavaScriptが使えるようにしようという設計論
ブラウザごとに様々なエンジンが使われている

JSエンジンの中身

JSエンジンの中身とは
・ECMAScript
・Web APIs
である

ECMAScriptとはー

JavaScriptとECMAScript(初心者向け)
https://qiita.com/YS114411/items/d69db9b4a0801f548864

Web APIs とは
Web上の API
API = Application Programming Interface
つまり、アプリケーションとプログラムをつなぐもの
ソフトウェアやサービス間で機能を共有したりする
ここにJSエンジンがある

コードが実装されるまで

JavaScriptコード実行前
JSエンジン
・コード
・グローバルオブジェクト
(window(Web APIs))
・this

グローバルオブジェクトとは

どこからでもアクセスできるオブジェクト
JavaScriptコード実行前にはこれとthisが準備される
ブラウザの環境ではWindowオブジェクトとなる

実行コンテキスト

コードを実行する際の文脈・状況

3つの種類
・グローバルコンテキスト
・関数コンテキスト
・evalコンテキスト

グローバルコンテキスト
ー実行中のコンテキスト内の変数・関数
ーグローバルオブジェクト
ーthis

関数コンテキスト
ー実行中のコンテキスト内の変数・関数
ーarguments
(super)
ーthis
ー外部変数

グローバルコンテキスト

let a = 0;
//直下に書かれたコードが実行される環境
function b () {}

console.log(a);
b();

関数コンテキスト

let a = 0;  //外部変数
//{}に書かれたコードが実行される環境
function b () {
    console.log(this, arguments, a)
}

//console.log(a);
b();
thisの値

Window {window: Window, self: Window, document: document, name: "", location: Location, …}


コールスタック

実行されたコードがたどってきたコンテキストの積み重ね

function a() {    //ここから処理 ↓ 実行が完了すると消滅
}
function b() {
    a();
}
function c() {
    b();
}
c();              // ↑ ここから積む

//後入れ先出し Last in, First out

chrome
ブラウザでF12 > Sources
Step in 処理を積む
Step out 処理を実行する

ホイスティング

コンテキスト内で宣言した変数や関数の定義をコード実行前にメモリーに配置する

varの場合

a();

function a() {
    console.log('a is called');
}


var b = 0;

console.log(b);

a is called
undefined

//undefinedの処理
.
.

var b;

console.log(b);

b = 0;

let constの場合

//error
main.js:19 Uncaught ReferenceError: Cannot access 'b' before initialization

関数式

a();

const a = function () {
    let c = 1;

    console.log(c);

    d();
    function d() {
        console.log('d is called');

    }
    console.log('a is called');
}


const b = 0;

console.log(b);


//error
main.js:1 Uncaught ReferenceError: Cannot access 'a' before initialization
    at main.js:1

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[ JavaScript 入門 ]ブロックスコープの概念を問題を解いたら少し理解できた件

JavaScriptのブロックスコープに関する問題を解いてみた:raised_hand:  

問題:writing_hand:

* 問題:
 * 以下のコードではエラーが発生します。
 * コンソールで"fn called"と表示されるように
 * fn内のコードを変更してください。
 * 
 * ※if文は削除してはいけません。
 */
function fn() {
    if(true) {
        let a = 'fn called';
    }
    return a; // ReferenceError: a is not defined
}

const result = fn();
console.log(result);

解答:v:
ifの中でletを使って変数定義している為、ブロックスコープが有効になり、ブロックスコープの外で、return a; のようにaを呼び出そうとしてもReferenceError: a is not definedのようなエラーが出てしまう!:frowning2:  

よって、ブロックスコープの外に変数宣言を持っていくことでエラーを解消することができる!!

function fn() {
    let a;
    if(true) {
         a = 'fn called';
    }
    return a; 
}

const result = fn();
console.log(result);

このようにしてあげることによって、return a; とlet a;が同じスコープ内にあることからa; を呼び出すことができる!

ちなみに、let a = 'fn called'; を var a = 'fn called';にすることで、varはブロックスコープを無視することより、aを取得することはできるが、varは非推奨とのことだったので、上記のような方法でa;を取得しました!:ok_hand:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Writing Cypress tests in TypeScript

Typescript で Cypress testを書く

はじめに

Typescript で Webアプリケーションを書いている際に、テストプログラムが必要になる。
比較検討した結果Cypressが何かと使い勝手が良いようなので使うことにした。

環境要件

  • OS: WSL2上で動作するUbuntsu 20.04LTS
  • 使用エディタ VScode
  • パッケージ管理には yarn を使用

この記事の目標

  • Cypress のインストール完了
  • Cypress の起動
  • Typescriptによるテストコードの作成・実行

前提条件

  • 適当なtypescript projectが存在し起動する

作業内容

WSL2上でXが使えるようにする

X server インストール

VcXsrvをインストールする。
インストール自体は exeなので困ることは無い。

初期設定

以下の所だけ追加して後はデフォルトで良い

  • Additional parameters for VcXsrv
    • -ac

動作確認

# sudo apt install x11-apps
# export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2; exit;}'):0
# xeyes

正しく設定されていれば目玉が出る。

Cypress のインストール

依存パッケージのインストール

# sudo apt-get update
# sudo apt-get install libgtk2.0-0 libgtk-3-0 libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb

本体のインストール

yarnで入れていく関係上、package.json等を更新するため、予めproject フォルダで実行する

# cd your_project_path
# yarn add -D cypress

typescript 向け設定

以下を見ながら実行する
https://docs.cypress.io/guides/tooling/typescript-support.html#Install-TypeScript

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactコンポーネントの状態で初心者がしてしまう3つの間違い

本記事は、Tyler Hawkins氏による「3 Mistakes Junior Developers Make With a React Component's State」(2020年6月26日公開)の和訳を、著者の許可を得て掲載しているものです。

Reactコンポーネントの状態で初心者がしてしまう3つの間違い

そして、それをやめる方法

Image for post

Photo by Jamie Street on Unsplash.

私がウェブ開発で好きなことの1つは、常に新しいことを学ぶことができるということです。さまざまなプログラミング言語、ライブラリ、フレームワークを習得するのに一生を費やしても、すべてを理解することはできません。

私達は皆学び続けているので、それは間違いをおかしやすいということも意味します。でもそれでいいのです。目標は、より良くなること、より良くあることです。間違ってもそこから学べば、あなたは素晴らしいことをしているということです!しかし、新しいことを学ばず、同じ間違いを繰り返しているとしたら...あなたのキャリアが停滞しているような気がします。

そのような考えで、コードレビューでよく目にする間違いを3つご紹介します。Reactコンポーネントの状態の処理で、初心者がよくする間違いです。それぞれをよく観察し、修正する方法について説明します。

1. 状態を直接変更する

コンポーネントの状態を変更する時は、現在の状態を直接変更するのではなく、変更を加えた状態の新しいコピーを返すことが重要です。コンポーネントの状態を誤って変更すると、Reactの差分アルゴリズムが変更を取得できず、コンポーネントが適切に更新されません。

例を見てみましょう。次のような状態があるとします。

this.state = {
  colors: ['red', 'green', 'blue']
}

次に、この配列にyellowを追加します。こうしたくなるかもしれません。

this.state.colors.push('yellow')

またはこのように。

this.state.colors = [...this.state.colors, 'yellow']

しかし、どちらの方法も正しくありません!クラスコンポーネントの状態を更新する時は、常にsetStateメソッドを使用し、オブジェクトを変化させないように注意する必要があります。

配列に要素を追加する正しい方法は、次の通りです。

this.setState(prevState => ({ colors: [...prevState.colors, 'yellow'] }))

これが2番目の間違いにも関係してきます。次を見てみましょう。

2. 関数を使用せずに、前の状態に依存する状態を設定する

setStateメソッドを使用する方法は2つあります。1つ目は、引数としてオブジェクトを指定する方法です。2つ目は、引数として関数を指定する方法です。では、どのような場合にどちらを使用すべきなのでしょうか?

例えば、有効または無効にできるボタンがある場合、ブール値を保持するisDisabledという状態の一部があるかもしれません。ボタンを有効から無効に切り替えたい場合は、引数としてオブジェクトを指定して、こう書きたくなるかもしれません。

this.setState({ isDisabled: !this.state.isDisabled })

これの何が問題なのでしょうか?問題は、Reactの状態の更新はバッチ処理が可能であるという事実にあります。つまり、単一の更新サイクルで複数の状態更新が発生する可能性があります。更新がバッチ処理され、有効・無効の状態に複数の更新があった場合、最終的な結果は期待したものではない可能性があります。

状態を更新するより良い方法は、引数として前の状態の関数を指定することです。

this.setState(prevState => ({ isDisabled: !prevState.isDisabled }))

これで、状態の更新がバッチ処理され、有効・無効の状態に複数の更新が同時に行われた場合でも、それぞれの更新は正しい前の状態に依存するため、常に期待通りの結果が得られます。

カウンタのインクリメントのような場合にも、も同じことが言えます。

こうしないで

this.setState({ counterValue: this.state.counterValue + 1 })

こうしてください。

this.setState(prevState => ({ counterValue: prevState.counterValue + 1}))

ここで重要なのは、新しい状態が古い状態の値に依存している場合は、常に関数を引数として指定する必要があるということです。古い状態の値に依存しない値を設定する場合は、引数としてオブジェクトを指定できます。

3. setStateが非同期であることを忘れる

最後に、setStateが非同期メソッドであるということを覚えておくことが重要です。例として、次のような状態のコンポーネントがあるとしましょう。

this.state = { name: 'John' }

そして、状態を更新してコンソールに状態を記録するメソッドがあるとします。

this.setState({ name: 'Matt' })
console.log(this.state.name)

これで「Matt」が記録されると思うかもしれませんが、違います。「John」が記録されます!

これは、setStateが非同期だからです。つまり、setStateを呼び出す行に到達すると状態の更新が開始されますが、非同期コードは非ブロッキングであるため、その下のコードは続けて実行されます。

状態が更新されたに実行する必要のあるコードがある場合、Reactには、更新が完了すると実行されるコールバック関数があります。

更新後に現在の状態を記録する正しい方法は、次の通りです。

this.setState({ name: 'Matt' }, () => console.log(this.state.name))

ずっと良くなりました!これで、期待通りに「Matt」が正しく記録されます。

まとめ

これがよくある3つの間違いとその修正方法です!覚えておいてください。間違っても大丈夫。あなたは学び続けています。私もです。私達は皆学び続けています。一緒に学び続け、より良くなりましょう。

更新:同じ概念を、関数コンポーネントとフックを使用して学びたいですか?私のフォローアップ記事を読んでください!

React関数コンポーネントの状態で初心者がしてしまう3つの間違い

Zack Shapiroに謝意を表します。

翻訳協力

Original Author: Tyler Hawkins
Original Article: 3 Mistakes Junior Developers Make With a React Component's State
Thank you for letting us share your knowledge!

この記事は以下の方々のご協力により公開する事ができました。改めて感謝致します。
選定担当: @gracen
翻訳担当: @gracen
監査担当: -
公開担当: @gracen

ご意見・ご感想をお待ちしております

今回の記事はいかがでしたか?
・こういう記事が読みたい
・こういうところが良かった
・こうした方が良いのではないか
などなど、率直なご意見を募集しております。
頂いたお声は、今後の記事の質向上に役立たせて頂きますので、お気軽に
コメント欄にてご投稿ください。Twitterでもご意見を受け付けております。
皆様のメッセージをお待ちしております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript超入門

JavaScript超入門

JavaScript(以下JS)の入門用の記事です
めちゃくちゃ基礎の内容のみです

目次

Hello World

ここがポイント!
console.logでコンソールと言うところに表示できる

console.log("Hello World");

JSの基本ルール

  • 基本半角
  • コメントは//または/**/で書く
  • 大文字と小文字は区別する
  • 文の終わりに;は付けても付けなくてもいい(例外あり)
  • 文字列は""または''または``で囲む
  • 足し算は+を使う
  • 引き算は-を使う
  • 掛け算は*を使う
  • 割り算は/を使う
  • 割り算の余りは%を使う
  • *,/,%は+,-より先に計算される

文字と数字の計算は思わぬ結果に

/*
+,-,*,/,%の挙動の違い
*/
console.log('9' + 2)//92
console.log('9' - 2)//7
console.log('9' * 2)//18
console.log('9' / 2)//4.5
console.log('9' % 2)//1

変数と定数

変数

letで宣言する
何でも入る
型はない
使う時は宣言した名前を書く
変数は後から値を変えられる

let a
a = 0
//let a = 0でもいい
console.log(a)//0
a = 9
console.log(a)//9
a = a + 1
console.log(a)//10

定数

constで宣言する
何でも入る
型はない
使う時は宣言した名前を書く
後から値を変えられない

const a
a = 0
//const a = 0でもいい
console.log(a)//0
a = 9 //error
console.log(a)

JSの便利な小技

1足すだけ、1引くだけは短縮できる

let a = 9
a++
console.log(a)//10
a--
console.log(a)//9

スマートに文字を連結

``で囲んだ文字列は${}で変数を埋め込める

let Hello = "Hello"
let World = "World"
console.log(Hello + World)//HelloWorld
console.log(`${Hello}${World}`)//HelloWorld

変数の更新は省略できる

let a = 10
a += 1//a = a + 1
a -= 1//a = a -1
a *= 1//a = a * 1
a /= 1//a = a / 1
a %= 1//a = a % 1

条件分岐(if)

条件が成り立つ時(trueの時)に実行する処理をかけます

if

宣言の仕方

if (条件) {
    処理
}
//処理が一行の時は{}を省略可能
if (条件) 処理

条件の書き方

演算子 条件 結果
=== 厳密に等しい  '1' === 1 false
== 等しい  '1' == 1 true
!== 厳密に等しくない  '1' !== 1 true
!= 等しくない  '1' != 1 false
> ~より大きい  1 > 1 false
>= ~以上  1 >= 1 true
< ~より小さい  1 < 1 false
<= ~以下  1 <= 1 true

===と==の違い

===は同じ型である必要がある
文字の1と数字の1は違う
==は値が同じならいい
文字の1も数字の1も値は1だから正しい

条件が成り立たない場合(else)

宣言の仕方

if (条件) {
    処理
}else{
    処理
}

条件が成り立たない場合の応用(else if)

宣言の仕方

if (条件) {
    処理
}else if(条件){
    処理
}

小技

!を使うことでtrueとfalseを反転できる

console.log(!1==1)//false

関数

宣言の仕方

function 名前 (引数/*省略可*/) {
    処理
}

呼び出し方

function 名前 (引数/*省略可*/) {
    処理
}
名前()

function hello() {
    console.log("Hello");
}
hello()//Hello

引数

()の中に宣言することで呼び出した時に値を渡せる

function hello(word) {
    console.log(word);
}
hello("Hello")//Hello

戻り値

returnで値を返すことができる

function sum(num1,num2) {
    return num1 + num2
}
let num = sum(1,2)
console.log(num)//3

作成中

DOM操作

繰り返し処理(for)(while)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ExcelとかWordでクリップボードにコピーしたデータからAsciiDocの表に変換するツールをつくってみた(結合セル対応)

AsciiDocで結合セルを作るのがややこしい(といってもHTMLと同様だが)ので、AsciiDocに不慣れでも表のひな形を作成できるように、Excelの表からAsciiDocに変換するツールを作ってみた。
Excel用に作ってたらWordも小変更で対応できた。(セルの中身の処理はテキトウにつくったので、過度な期待は禁物。)

オンライン版(CodePen)

スクリーンショット

Excel

環境はExcel2019
image.png

実行結果:
image.png

上記の出力結果のコピー
|===
.2+|a |b |c |d 2.2+|e
.2+|f |g |h
|i .2+|j |k |l |m
2+|n |o |p |q
|===

asciidoctorで変換した結果:
image.png

Word

image.png

実行結果:
image.png

上記の出力結果のコピー
|===
|   A    |   B    |   C   
2+|   D    .2+|   E   
|   F    |   G   
|===

asciidoctorで変換した結果:
image.png

C#ソースコード

  • 処理対象は表1個だけです。
  • ネストした表(テーブル内にテーブルがある)には対応していません。
  • 書式は無視します。
ClipboardedHtmlToAsciiDocTable.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

class SampleForm : Form
{
    static readonly int ExpectedHeaderMaxLines = 20;
    static readonly Regex rxTableBeginTag = new Regex(@"<table(?:\s)?[^>]*>", RegexOptions.Multiline | RegexOptions.IgnoreCase);
    //                                         1                   2             3                             *?は最短マッチ   o:pはMS office(word)対策
    static readonly Regex rxTag = new Regex(@"<([a-z][a-z0-9]*|o:p)(|\s[^>]*)>|</([a-z][a-z0-9]*|o:p)>|<!--(?:.*?)-->", RegexOptions.Multiline | RegexOptions.IgnoreCase);
    //static readonly Regex rxTag = new Regex(@"<([a-z][a-z0-9]*)(|\s[^>]*)>|</([a-z][a-z0-9]*)>|<!--(?:.*?)-->", RegexOptions.Multiline | RegexOptions.IgnoreCase);


    TextBox txtAdoc;

    SampleForm()
    {
        Text = "HTML table(Clipborad) to AsciiDoc";
        ClientSize = new Size(700, 430);

        var btn = new Button(){
            Size = new Size(280, 25),
            Text = "Get AsciiDoc from Clipborad",
        };
        btn.Click += (s,e)=>{ParseFromHtmlClipboard();};
        Controls.Add(btn);

        var btnDbg = new Button(){
            Location = new Point(300, 0),
            Size = new Size(220, 25),
            Text = "Get HTML from Clipborad(開発者用)",
        };
        btnDbg.Click += (s,e)=>{DumpHtmlClipboard();};
        Controls.Add(btnDbg);

        txtAdoc = new TextBox(){
            Location = new Point(0,30),
            Size = new Size(700, 400),
            Text = "",
            Multiline = true,
            WordWrap = false, // 折り返し表示をしない
            ScrollBars = ScrollBars.Both,
        };
        Controls.Add(txtAdoc);
        txtAdoc.KeyDown += (s,e)=>{ if (e.Control && e.KeyCode == Keys.A) { ((TextBox)s).SelectAll(); } };

        Resize    += (s,e)=>{MyResize();};
        ResizeEnd += (s,e)=>{MyResize();};
    }

    void MyResize()
    {
        int h = ClientSize.Height - txtAdoc.Top;
        if(h<50){h=50;}
        txtAdoc.Size = new Size(ClientSize.Width, h);
    }

    void ParseFromHtmlClipboard()
    {
        MemoryStream ms = GetHtmlClipboard();
        if ( ms != null ) {
            string tmp = Parse(ms);
            if ( tmp != null ) {
                txtAdoc.Text = tmp;
                txtAdoc.Focus();
                txtAdoc.SelectAll();
            }else {
                txtAdoc.Text = "Parse Failed";
            }
        }
        else {
            txtAdoc.Text = "Clipboard Load failed";
        }
    }

    void DumpHtmlClipboard()
    {
        MemoryStream ms = GetHtmlClipboard();
        if ( ms != null ) {
            string tmp = GetHtmlText(ms);
            if ( tmp != null ) {
                txtAdoc.Text = tmp;
                //txtAdoc.Focus();
                //txtAdoc.SelectAll();
            }else {
                txtAdoc.Text = "Parse Failed";
            }
        }
        else {
            txtAdoc.Text = "Clipboard Load failed";
        }
    }

    static MemoryStream GetHtmlClipboard()
    {
        return Clipboard.GetData("Html Format") as MemoryStream;
    }

    static string GetHtmlText(MemoryStream ms)
    {
        int startHtml = -1;
        int endHtml = -1;

        // ヘッダ情報(StartHTML, EndHTML)を取得
        //  StartHTML:nnnnnnnnnn
        //  EndHTML:nnnnnnnnnn
        //public StreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize, bool leaveOpen)
        //  leaveOpen=trueで開かないと、msが閉じてしまう。
        using ( var sr = new StreamReader(ms, Encoding.UTF8, true, 1024, true) ) {
            Regex rx = new Regex(@"^(StartHTML:|EndHTML:)([0-9]+)");
            int lineCount = 0;
            string s;
            while ( (s = sr.ReadLine()) != null )
            {
                lineCount++;
                Match m = rx.Match(s);
                if ( m.Success ) {
                    int n = Convert.ToInt32(m.Groups[2].Value, 10); // 10進
                    if ( m.Groups[1].Value == "StartHTML:" ) {
                        startHtml = n;
                    }
                    else {
                        endHtml = n;
                    }
                    if ( startHtml >= 0 && endHtml > startHtml ) {
                        break;
                    }
                }
                if ( lineCount >= ExpectedHeaderMaxLines ) {
                    break;
                }
            }
        }

        // HTML部分を取得(EndHTMLは無視)
        ms.Position = startHtml;
        using ( var sr = new StreamReader(ms, Encoding.UTF8, false) )
        {
            return sr.ReadToEnd();
        }
    }

    // stypeタグの中身を返す
    static string GetStyleText(string htmlText, out int endPos)
    {
        endPos = 0;
        int styleStartTagPos = htmlText.IndexOf("<style>");
        if ( styleStartTagPos < 0 ) {
            return null;
        }
        styleStartTagPos += "<style>".Length;

        int styleEndTagPos = htmlText.IndexOf("</style>", styleStartTagPos);
        if ( styleEndTagPos < 0 ) {
            return null;
        }
        endPos = styleEndTagPos + "</style>".Length;

        int commentStartTagPos = htmlText.IndexOf("<!--", styleStartTagPos);
        if ( commentStartTagPos >= 0 ) {
            commentStartTagPos += "<!--".Length;
        }
        int commentEndTagPos   = (commentStartTagPos<0)?-1:htmlText.IndexOf("-->", commentStartTagPos);
        if (commentStartTagPos >= 0 && commentEndTagPos > commentStartTagPos && commentEndTagPos < styleEndTagPos ) {
            // コメントタグがある場合、コメントタグを除去(コメントタグ内のみを返す)
            return htmlText.Substring(commentStartTagPos, commentEndTagPos - commentStartTagPos);
        }
        else {
            // コメントタグがない場合
            return htmlText.Substring(styleStartTagPos, styleEndTagPos - styleStartTagPos);
        }
    }

    static int IndexOfUsingRegex(string src, int startPos, Regex rTarget, out int length)
    {
        Match m = rTarget.Match(src, startPos);
        if ( !m.Success ) {
            length = 0;
            return -1;
        }
        length = m.Groups[0].Length;
        return m.Groups[0].Index;
    }

    // tableタグ込みで返す
    // ネストは許容しない(検出してnullを返す)
    static string GetFirstTableText(string htmlText, int pos)
    {
        int len;
        int tableStartTagPos = IndexOfUsingRegex(htmlText, pos, rxTableBeginTag, out len);
        if ( tableStartTagPos < 0 ) {
            return null;
        }

        int tableEndTagPos = htmlText.IndexOf("</table>", tableStartTagPos+len);
        if ( tableEndTagPos < 0 ) {
            return null;
        }
        int dummy;
        int tmpPos = IndexOfUsingRegex(htmlText, tableStartTagPos+len, rxTableBeginTag, out dummy);

        if ( tmpPos>=0 && tmpPos<tableEndTagPos ) {
            // ネストしている(閉じタグよりも手前の位置に2つ目の開始タグを検出した)
            return null;
        }

        tableEndTagPos += "</table>".Length;
        return htmlText.Substring(tableStartTagPos, tableEndTagPos - tableStartTagPos);
    }


    //static readonly Regex rxCss = new Regex(@":;", RegexOptions.Multiline | RegexOptions.IgnoreCase);

    //static Dictionary<string,Dictionary<string,string>> ParseCssPart(string styleText)
    //{
    //}

    // https://momdo.github.io/html/syntax.html#attributes-2
    //  属性名は、制御文字、U+0020 SPACE、U+0022(")、U+0027(')、U+003E(>)、U+002F(/)、U+003D(=)、および非文字以外の1つ以上の文字で構成されなければならない。HTML構文において、外来要素に対するものでさえ、属性名は、ASCII小文字およびASCII大文字の任意の組み合わせで書かれてもよい。
    //  属性値は、テキストが曖昧なアンパサンドを含めることができない追加の制限をもつ場合を除き、テキストおよび文字参照の混合物である。
    // 引用符で囲まれない属性値構文
    //  ASCII空白文字 U+0022 QUOTATION MARK文字(")、
    //                U+0027 APOSTROPHE文字(')、U+003D EQUALS SIGN文字(=)、
    //                U+003C LESS-THAN SIGN文字(<)、U+003E GREATER-THAN SIGN文字(>)、
    //             またはU+0060 GRAVE ACCENT文字(`)文字を含んではならず、かつ空文字列であってはならない。

    //                                           1                                            =      2                                   3            4 
    //                                           <------------------------------------->             <-------------------------------->  <----->      <------->
    //                                                                                            <---------------------------------------------------------------->
    static readonly Regex rxAttr = new Regex(@"\b([^\x00-\x1F\x20\x22\x27\x2F\x3D\x3E]+)\s*(?:=\s*(?:([^\x20\x22\x27\x3C\x3D\x3E\x60]+)|'([^']*)'|\x22([^x22]*)\x22))?", RegexOptions.Multiline | RegexOptions.IgnoreCase);

    static Dictionary<string,string> ParseAttrs(string attrsStr)
    {
        var dict = new Dictionary<string,string>();

        Match mAttr = rxAttr.Match(attrsStr);
        while ( mAttr.Success ) {
            string key = mAttr.Groups[1].Value.ToLower();

            string value = "";
            if ( mAttr.Groups[2].Length>0 ) {
                value = mAttr.Groups[2].Value;
            }
            else if(mAttr.Groups[3].Length>0) {
                value = mAttr.Groups[3].Value;
            }
            else if(mAttr.Groups[4].Length>0) {
                value = mAttr.Groups[4].Value;
            }
            else { // without "="
                // do nothing
            }

            if ( !dict.ContainsKey(key) ) {
                dict.Add(key, value);
            }

            mAttr = mAttr.NextMatch();
        }
        return dict;
    }

    static string EscapeContentForAdocTableCell(string s)
    {
        // Replace (string input, string replacement);
        s = rxTag.Replace(s, ""); // HTML全般のタグを消去
        s = s.Replace("\r\n", " ")
             .Replace("\n", " ")
             .Replace("\r", " ")
             .Replace("\t", " ")
             .Replace("&nbsp;", " ")
             .Replace("&lt;", "<")
             .Replace("&gt;", ">")
             .Replace("&amp;", "&")
             .Replace("|", "{VBar}"); // ADoc用
        return s;
    }

    static string ParseTableToAdoc(string tableText)
    {
        var sb = new StringBuilder();



        Match m = rxTag.Match(tableText);
        string lastStartTag = null;
        int lastPos = -1;
        int lastTdPos = -1;
        int currentTdCount = 0;

        sb.AppendLine("|===");

        while (m.Success) {
            if ( m.Groups[1].Length > 0 ) {
                string tag = m.Groups[1].Value;
                string attrsStr = m.Groups[2].Value;
                lastPos = m.Groups[0].Index + m.Groups[0].Length;

                if ( tag == "tr" ) {
                    currentTdCount = 0;
                }
                else if ( tag == "td" ) {
                    lastTdPos = lastPos;
                    if ( lastStartTag != "tr" ) {
                        sb.Append(" ");
                    }
                    var attrs = ParseAttrs(attrsStr);
                    if (attrs.ContainsKey("colspan") || attrs.ContainsKey("rowspan")){
                        if ( attrs.ContainsKey("colspan") ) {
                            sb.Append(attrs["colspan"]);
                        }
                        if ( attrs.ContainsKey("rowspan") ) {
                            sb.Append(".");
                            sb.Append(attrs["rowspan"]);
                        }
                        sb.Append("+");
                    }
                    sb.Append("|");
                    currentTdCount++;
                }
                lastStartTag = tag;
            }
            else if ( m.Groups[3].Length > 0 ) {
                string tag = m.Groups[3].Value;
                int tagStartPos = m.Groups[0].Index;
                //Console.WriteLine("</" + m.Groups[3].Value +">");
                if ( tag == "tr" ) {
                    sb.AppendLine("");
                }
                else if ( tag == "td" ) {
                    string s = tableText.Substring(lastTdPos, tagStartPos - lastTdPos);
                    sb.Append(EscapeContentForAdocTableCell(s));
                    lastTdPos = -1;
                }
                lastPos = -1;
                lastStartTag = null;
            }

            m = m.NextMatch();
        }

        sb.AppendLine("|===");

        Console.WriteLine(sb.ToString());

        return sb.ToString();
    }

    static string Parse(MemoryStream ms)
    {
        // debug code {
        //string htmlText = File.ReadAllText("testdata_html_excel.txt");

        // } end of debug code

        string htmlText = GetHtmlText(ms);
        int pos;
        string styleText = GetStyleText(htmlText, out pos)??"";
        string tableText = GetFirstTableText(htmlText, pos);
        return ParseTableToAdoc(tableText);
    }


    [STAThread]
    static void Main(string[] args)
    {
        Application.Run(new SampleForm());
    }
}

JavaScriptに移植してWeb上に公開してみた

開発時メモ: クリップボード形式は MIMEタイプとしてtext/htmlを指定すると、オフセット情報とかのヘッダ情報なしの html が得られる。

ネストチェックはしていない。

See the Pen TableOfHtml2AsciiDoc by kob58im (@kob58im) on CodePen.

TableOfHtml2AsciiDoc - CodePen

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

XMLHttpRequestオブジェクトとは

XMLHttpRequestオブジェクトとは

スクリプト言語でサーバーとのHTTP通信を行うための組み込みオブジェクト。

<よく使うメソッド>
◎open
 (使える場面)
 ・新しく作成されたリクエストを初期化したいとき
 ・既存のリクエストを再初期化したいとき

 (例)

index.js
var ajax = new XMLHttpRequest();
ajax.open('GET', 'URL');

◎responseType
 列挙型の文字列地
 (使える場面)
 ・レスポンスに含まれているデータの型を示したいとき
 ・レスポンスの型を変更したいとき

 (例)

index.js
var ajax = new XMLHttpRequest();
ajax.responseType = 'json';

◎send
 (使える場面)
 ・リクエストをサーバーに送信したいとき

 (例)

index.js
var ajax = new XMLHttpRequest();
ajax.send(null);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのオブジェクトとJSON型の違い【今更ですが・・・】

今更ならが、JavaScriptのオブジェクトとJSONの違いについてメモっときます。

(「そういえばどこが違うんだっけ・・・」となってしまったので。笑)

JavaScriptのオブジェクトとJSONの違い

JSONはJavaScriptの記法を元に生まれたので書き方が似ているけど、オブジェクトとJSONは全然別物。

オブジェクト・・・プロパティの集まりの名称
JSON・・・テキストベースのデータフォーマット

【補足】
データフォーマット・・・特定の形式 (フォーマット) に当てはめられたデータの集まり

ちょっと理解しづらいですが、まぁ何となく理解できればOKかと。
ちなみにデータフォーマットはJSON意外だと、XMLCSVなどがありまっする。

JavaScriptのオブジェクトとJSONの書き方の違い

凄く単純で、キー名をダブルクォーテーション("")で囲んでるかどうかで判断すればOK。

オブジェクトの書き方

オブジェクトの書き方は以下の通り。
・キー名はダブルクォーテーション("")またはシングルクォーテーション('')囲む or 囲まないどっちでもOK。
・値はダブルクォーテーション("")またはシングルクォーテーション('')囲む。
最後のカンマはあってもなくてもOK

下記のようなパターンがある。
でも「キー名は囲まずに、値の文字列はダブルクウォーテーションで囲む」のが一般的。

{ 
  id: 0,
  test: "テスト",
}

{ 
  id: 0,
  test: 'テスト',
}

{ 
  "id": 0,
  "test": 'テスト',
}

{ 
  'id': 0,
  'test': 'テスト',
}

JSONの書き方

JSONの書き方は以下の通り。

・キーをダブルクオーテーション("")で囲む
・値の文字列をダブルクォーテーション("")で囲む。
最後のカンマはつけられない。

{ 
  "id": 0, //文字列は囲まない
  "test": "テスト" //最後はカンマなし
}

こんな感じ。

JSON.stringify()とJSON.parse()の違い

JSON.stringify()はJavaScriptのオブジェクトをJSON文字列に変換する関数。
JSON.parse()JSON文字列をJavaScriptオブジェクトに変換する関数。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript~文字列カウント~

はじめに

あけましておめでとうございます前回記事から日が空いてしまいました。
ここやTwitterでの活動のやる気が認められて案件をもらいそれに取り組んでいました。
今まで全然たいした内容は投稿出来ていなかったですがそれを評価していただいて嬉しかったです。
ただ案件に時間がかかってしまいなかなか記事を書けず仕事をしながら投稿を続ける難しさを実感しました。
自分のように今勉強中で転職活動している人は今回のような例もあるので投稿してみるのをオススメします。

今回の投稿内容は文字列の長さをカウントするというコードです。
これは入力フォームの文字カウントに使えるのでぜひ参考にしてもらえたらと思います。

文字数カウント

文字をカウントする構文は次のようになります。

構文
文字列.length

これを実際に使うと

'テスト'.length //3
'test'.length //4

このように文字の後ろに「.length」をつけるだけでカウントしてくれます。

入力フォームカウント

これを元に入力フォームの文字数をカウントするスクリプトを紹介します。

入力フォームHTML
<textarea class="textarea"></textarea>
<p>現在<span class="string_num">0</span>文字入力中です。</p>

<!-- classに関しては好きな名前で大丈夫です。分かりやすいものを使用してください -- >

続いてJavaScriptです

入力フォームjs
const textarea = document.querySelector('.textarea');
//「document.querySelector()」を使うことによって任意のHTML要素を簡単に取得できる。
//今回は「document.querySelector('.textarea')」なのでテキストエリアの要素を取得しています

const string_num = document.querySelector('.string_num');
//上と同様にspanの要素を取得しています

textarea.addEventListener('keyup',onKeyUp);
//「addEventListener()」使ってイベント処理を実現しています。
//「オブジェクト.addEventListener(イベントのタイプ, 関数, オプション);」のように記載します。
//今回の場合はオブジェクトに「textarea」,イベントのタイプが「keyup」でこれは「キーボードのキーを離した際に発生するイベント」で
//文字を入力したり、消したりした後キーボードから指を離すと発生します。
//この関数の名前が「onKeyUp」になります。

function onKeyUp(){
const inputText = textarea.value;
//「textarea.value」によってテキストエリアに入力されたテキストを取得しています。

string_num.innerText = inputText.length;
//「innerText」は要素の中身を変更するもので他には「innerHTML」,「textContent」があります。今回は「string_num」の中身を変更します。
//先ほど取得したテキストを「.length」でカウントしてその結果に変更します。

終わりに

今回は新しい要素がいっぱい出てきてなかなか消化しにくい投稿になってしまいました。
その分今までに比べてポートフォリオ等でも使えるような内容だと思います。
自分としては改めてまとめることで知識の整理にもなってよかったです。
今後も案件があるので毎日投稿は難しいですが自分の知識や技術の向上に繋がるので頑張って投稿しようと思ってます。
最後まで読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[ Java Script 入門 ] グローバルスコープとスクリプトスコープは同じものなのか?

グローバルスコープとスクリプトスコープ 

let a = 0;
var b = 0;
function c() {}
debugger;

これらを定義し、ブラウザ上で検証をぶちかます。
debuggerはブラウザを開いていると、この時点に達すると処理が止まる。)
検証のSources内、Command + R で更新。

Script
a: 0  


Global                Window
PERSISTENT: 1
TEMPORARY: 0
addEventListener: ƒ addEventListener()
alert: ƒ alert()
atob: ƒ atob()
b: 0
blur: ƒ blur()
btoa: ƒ btoa()
c: ƒ c()

Scope内にこのように表示された。  
これがdebuggerから見える変数ということになる!

グローバルスコープとは?

Sources内の結果からわかるように、VarやFunctionで定義した際は、Globalの Windowオプションにプロパティーとして保持される。

スクリプトスコープとは?

letやconstにて宣言をとるとスクリプトの方に保持される

windowオブジェクト自体がグローバルスコープ?

console.log(window.b);
を
console.log(b);と省略しても取得する値は変わらない。

ってことは、windowオブジェクト自体がグローバルスコープなんだ:hand_splayed:

なので

window.d = 1;
console.log(d);

すると、d = 1はグローバルスコープに追加されているので、コンソール上には1が表示される:hand_splayed:

使い勝手ではグローバルスコープもスクリプトスコープも変わらない

ただ、

window.d = 1;
let d = 2;
console.log(d);

とすると2が表示される。これはスコープチェーンによるもの!

まとめ

一般的には、スクリプトスコープもグローバルスコープと呼ばれるとのこと。
ただ、スコープチェーンによる挙動の変化があるので、別のスコープであるという考え方は大切になる!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoogleAPIをNode.jsから叩いてみた

今更ながら、GoogleAPIをNode.jsから触ってみます。
Google Drive、Gmail、Google Calendar、Googleフォトなど、皆さんgoogleサービスにお世話になっているのではないでしょうか。
ちゃんと、APIも提供されていて、npmモジュールもあるので、今後もいろいろ使えるかもしれません。
本人認証して簡単なリスト表示をするところまでですが、後はリファレンスを見れば拡張できるかと思います。

GitHubに上げておきます。

poruruba/GoogleApiSample
 https://github.com/poruruba/GoogleApiSample

参考情報

googleapis/google-api-nodes-client
 https://github.com/googleapis/google-api-nodejs-client

Google Cloud Platform Console
 https://console.cloud.google.com/

Google CalendarのQuickStart
 https://developers.google.com/calendar/quickstart/nodejs

Google DriveのQuickStart
 https://developers.google.com/drive/api/v3/quickstart/nodejs

Google PhotoのQuickStart
 https://developers.google.com/photos/library/guides/get-started

GmailのQuickStart
 https://developers.google.com/gmail/api/quickstart/nodejs

Google SheetのQuickStart
 https://developers.google.com/sheets/api/quickstart/nodejs

GCPのプロジェクトの作成

まずは、Google Cloud Platformのプロジェクトを作成します。すでに作成済みの場合はそれを使ってもよいです。

 https://console.cloud.google.com/projectcreate

image.png

プロジェクト名は適当に「SampleProject」としておきます。
Googleアカウント認証は、GoogleのサーバとOAuth2プロトコルで認証しますので、OAuth2のクライアントIDを作成します。
左上のメニューのAPIとサービスの認証情報を選択します。

image.png

次に、上の方にある「+認証情報を作成」をクリックし、OAuthクライアントIDを選択します。
アプリケーションの種類には、「ウェブアプリケーション」を選択します。
名前は適当に「GoogleApiSample」とでもしておきます。

image.png

そして、承認済みのJavascript生成もとには、これから立ち上げるサーバのURLを指定します。
例えば、https://hogehoge:10443 という感じです。
承認済みのリダイレクトURIには、これから立ち上げるWebページのURLを指定します。
こんな感じで作る予定です。https://hogehoge:10443/googleapisample/signin.html

作成できたら、クライアントIDとクライアントシークレットを覚えておきます。

利用するGoogleAPIを有効化

これから、先ほど作ったプロジェクトで、Google Drive、Gmail、Google Calendar、Google Photoを使えるように有効化します。
左上のメニューから、APIとサービス→ライブラリ を選択します。

image.png

検索入力のところに、driveと入力します。

image.png

そうすると、Google Drive APIが見つかりますので選択し、「有効にする」ボタンを押下します。

image.png

同様に、Gmail、Google Calendar、Google Photoも有効にします。

サーバを立ち上げる

とりあえず、GitHubにもろもろを上げておいたのでそれを展開します。

https://github.com/poruruba/GoogleApiSample

$ unzip GoogleApiSample-main.zip
$ cd GoogleApiSample-main
$ mkdir cert
$ npm install

HTTPSで立ち上げる必要があり、certフォルダにSSL証明書のファイルを配置します。
Let’s Encryptの場合は、cert.pem、chain.pem、privkey.pemです。

起動の前に、各環境に合わせて変更する必要があります。

「public/googleapisample/js/start.js」
以下の2か所を修正します。

/public/googleapisample/js/start.js
const GOOGLE_REDIRECT_URL = 'https://【サーバのホスト名】:10443/googleapisample/signin.html';
const AUTHORISE_URL = 'https://【サーバのホスト名】:10443/gapi/authorize';

「api/controllers/gapi/index.js」
以下の3か所です。

/api/controllers/gapi/index.js
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '【クライアントID】';
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '【クライアントシークレット】';
const GOOGLE_REDIRECT_URL = process.env.GOOGLE_REDIRECT_URL || 'https://【サーバのホスト名】:10443/googleapisample/signin.html';

起動は、以下です。

$ npm app.js

何も指定しなければ、HTTPSが10443ポートで立ち上がるかと思います。

さっそく動かしてみる

以下のURLをブラウザから開きます。

 https://【サーバのホスト名】:10443/googleapisample/index.html

サーバのホスト名は、GCPプロジェクトのOAuth2クライアントを作成したときのサーバのホスト名と同じである必要があります。

image.png

Authorizeボタンを押下します。

image.png

Googleアカウントのログイン選択画面が表示されます。
アカウントを選択すると以下の画面が表示されます。この画面は、アプリがテスト用であるために表示されています。(本番にしたい場合はhttps://support.google.com/cloud/answer/7454865)

image.png

詳細リンクをクリックし、【サーバのホスト名】(安全ではないページ)に移動 のリンクをクリックします。

image.png

これから触るGoogle Drive、Gmail、Google Calendar、Google Photoへのアクセス権を許可するかの確認が表示されます。許可ボタンを押下します。

そうすると、GoogleアカウントのもろもろのGoogle Drive、Gmail、Google Calendar、Google Photoの情報を取得したのち以下のように各テキストエリアに表示されて終了です。

image.png

流れを見てみる

まず、ログインから。Authorizeボタン押下から始まりました。
以下の部分が呼ばれます。子ウィンドウを作成し、/public/googleapisample/signin.htmlを開いています。Googleアカウントのログイン処理(の前半部分)は、signin.htmlに実装しています。

/public/googleapisample/js/start.js
        do_authorize: function(){
            this.new_win = open(GOOGLE_REDIRECT_URL, null, 'width=500,height=750');
        },

一方、signin.htmlでは、ページが表示されてすぐに、以下を呼び出しています。

/public/googleapisample/signin.html
    window.location = window.opener.vue.make_authorize_url();

親ページのmake_authorize_url()を呼び出して戻り値のURLにページ遷移しています。

/public/googleapisample/js/start.js
        make_authorize_url: function(){
            return AUTHORISE_URL + '?state=abcd&prompt=true';
        }

Googleアカウントログイン用のURLを生成しています。URLはサーバ側で生成しています。

以下の部分です。
GOOGLE_SCOPEには、取得したい対象サービスをリストアップしています。(OAuth 2.0 Scopes for Google APIs)

/api/controllers/gapi/index.js
    if( event.path == '/api/authorize' ){
        var params = {
            scope: GOOGLE_SCOPE,
            access_type: 'offline'
        };
        if( event.queryStringParameters.state )
            params.state = event.queryStringParameters.state;
        if( event.queryStringParameters.prompt )
            params.prompt = 'consent';
        const auth = new google.auth.OAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URL);
        var url = auth.generateAuthUrl(params);
        return new Redirect(url);
    }else

実は、URLは固定なので毎回取得する必要はないのですが、今回は手順に沿って生成しています。
見ての通り、リダイレクトを返していまして、やっとGoogleアカウント認証ページに遷移します。

ログインが完了すると、またこのページ(/public/googleapisample/signin.html)に戻ってきます。その時に、codeがパラメータとして戻ってきますのでそれを処理します。

/public/googleapisample/siginin.html
        if( searchs.code ){
            try{
                if( window.opener )
                  window.opener.vue.callback_authorization_code(null, searchs.code, searchs.state);
            }finally{
                window.close();
            }

取得されたcodeは認可コードと呼ばれており、以降で利用するアクセストークンの生成に必要なパラメータです。

この認可コードを親ページにcallback_authorization_code()関数を介して戻しています。

/public/googleapisample/js/start.js
        callback_authorization_code: async function(err, code, state){
            if( err ){
                alert(err);
                return;
            }

            this.progress_open();
            try{
                var params = {
                    code: code
                };
                var json = await do_post("/gapi/token", params);
                console.log(json);

認可コードの処理は実はサーバ側で実施しますので、サーバ側に転送しています。

/api/controllers/gapi/index.js
    if( event.path == '/gapi/token'){
        try{
            const auth = new google.auth.OAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URL);
            var token = await auth.getToken(body.code);
            console.log(token);
            auth.setCredentials(token.tokens);

これで、token.tokensにもろもろのトークンが取得できました。

後はこれを使って、Google Drive、Gmail、Google Calendar、Google Photoの情報を取得する処理をして、クライアントに返しています。

/api/controllers/gapi/index.js
    const drive = google.drive({version: 'v3', auth: auth});
    var drive_list = await drive.files.list({
        pageSize: 10,
        fields: 'nextPageToken, files(id, name)',
    });
    var image_list = await do_get_image_list({ pageSize: 100 }, token.tokens.access_token);
    const calendar = google.calendar({version: 'v3', auth});
    var calendar_list = await calendar.events.list({
        calendarId: 'primary',
        maxResults: 10,
        singleEvents: true,
        orderBy: 'updated'
    });
    const gmail = google.gmail({version: 'v1', auth});
    var mail_list = await gmail.users.labels.list({
        userId: 'me',
    });

    return new Response({ drive_list, image_list, calendar_list, mail_list });

クライアント側では受け取ったレスポンスをテキストエリアに表示しています。

/public/googleapisample/js/start.js
            this.drive_list = JSON.stringify(json.drive_list.data.files, null , "\t");
            this.image_list = JSON.stringify(json.image_list.mediaItems, null , "\t");
            this.mail_list = JSON.stringify(json.mail_list.data.labels, null , "\t");
            this.calendar_list = JSON.stringify(json.calendar_list.data.items, null , "\t");
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む