Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

ドメイン移行戦記

f:id:fa11enprince:20200515235243j:plain
独自ドメインに移行しました。
旧ドメイン:https://fa11enprince.hatenablog.com/
新ドメイン:https://gup.monster/

前は独自ドメインはサブドメインでしかできなかったそうなのですが、
いつの間にかネイキッドドメインがOKになってるそうなので、wwwなしのネイキッドドメインで運用します。
サブドメインでは新規分はGoogle AdSenseが申請を受け付けてくれません。
自動で元のURLからリダイレクトしてくれるので素晴らしいです。

ここの記事に従って作業すれば終わりです。 以下、作業の記録です。1か所ハマりポイントがあります(謎のリダイレクト問題)。

ドメイン移行作業

Aレコードを作成

Aレコードで

13.230.115.161
13.115.18.61

を設定 サブドメインではないのでCNAMEの設定は不要です。

はてなブログの独自ドメインの設定

「独自ドメインの設定」をします。
入力後…10分くらい待たないとエラーと表示されます。
とりあえずは待てばOKなのです。数分すると「有効」となりエラーが消えます。
あと、時間があまりたってないと新URLのhttps証明書がエラーになってたりします(ちょっと待てば大丈夫)。

謎のリダイレクト問題1

新URLhttps://gup.monsterにアクセスすると
元のURLhttps://fa11enprince.hatenablog.comに飛ばされる…。

なんじゃこりゃ…と思ってググるとここが見つかる。
いったん「独自ドメインを設定」を空欄にして保存、再度ドメインを入力すると、OKになる。

URLの修正

JavaScriptでrssフィードを利用しているところがあって
rssフィードのURLの修正が必要でした。
あとは上部の自作メニューで

https://[旧URL]/archive/category/Programming

としていたところを

/archive/category/Programming

としました。

Google Analytics

以下2点を変更すればOK
1. プロパティの設定、デフォルトURL 2. ビューの設定、ウェブサイトのURL

ドメイン移行後の作業

ここからが長い戦いです。アクセス回復を見守りつつ、AdSenseを再度申請してみました。

Google AdSense

サイト追加で申請しなおします。
審査完了後のドメイン変更 - AdSense Community

旧アナリティクスコードをいったん消します。 承認が終わると一斉に貼りかえます。
と思ったのですが、その必要がなかった…戻したはいいもののエラーが…
これもしばらく待ってみます。 data-ad-clientが当然同じユーザなので一緒。サイト毎にコードが違うってわけでないです。
これは単に審査が通ってないから表示されていないのです。

Google AdSense 1回目の審査

2020/05/18 予想通り審査に落ちる。
広告が表示されないのはGoogleに承認されていないからです。403エラーが広告で発生すると思います。
ちなみにですが、このブログは、独自ドメインにする前は「はてなブログPro」で「はてなのサブドメイン」で運用していました。
Google AdSenseの審査に通っていました。その当時は2016年くらいで、
審査の基準もゆるくかつまだhttpしか対応していなかったのでかなり簡単でした。

サイトの停止または利用不可

お客様のサイトで複数のポリシー違反が確認されたため、サイトに広告を表示できない状態です。

理由としては「サイトの停止または利用不可」とのこと。

ということでむりやり検索エンジンに存在を知らせる
https://www.paradisia.jp/entry/2019/04/21/183223

「サイトの停止または利用不可」とのこと。他のブログを読むと、Googleのサーチボットにサイトが認識されていないことに気がつく。Google アナリティクスと、Googleサーチ・コンソールにサイトを登録し

と書いてあるので、サイトをSearch Consoleに登録してみる
ついでにsitemap.xmlを登録する
って…
https://support.google.com/webmasters/thread/4498538?hl=ja
https://support.google.com/webmasters/thread/3717131?hl=ja
むむむ…登録できない

これ最近の話だ。
https://support.google.com/webmasters/thread/44339439?hl=ja
ひとまず放置

サイトは広告を表示できない状態です

サイト上にコードが見つかりませんでした。これは、コードが不足しているか不完全である、またはサイトの URL が正しくないことが原因です。

えええ。そんなことはない。そもそもクローラーからみてサイトが見つかってないのが原因と思われる。

Google AdSense 2回目の審査

2020/05/21に2回目の申請をしてみました。
しかしSearch Consoleの様子を見てると、微妙な感じです。全然新URLがインデックスされていない…。
2020/05/26に不合格の通知が来ました。やっぱりか。

Google AdSense 3回目の審査

2020/05/27 3回目の審査をしてみました。
Search Consoleでクロール指示をしたり悪あがき再びです。
なんでか「アドレス変更」は使えないので、地道にインデックスされるようにします。
2020/06/03 3回目の不合格を受け取る。

不合格の理由に「サイトの停止または利用不可」はなくなり、 「価値の低い広告枠: コンテンツが存在しない」だけになり、その中の理由が「価値の低い広告枠: コンテンツが存在しない」に変わりました。
しばらくたってGoogleのクローラーが新ドメインを認識し始めたのだと思われます。
新ドメイン側がほぼインデックスされてない状況で、
おそらくまだ一部の記事が旧ドメイン側のでインデックスされているのだと思われますので、
Search Consoleで粘るか、さらにまだ待つ必要がありそうです。
タイトルで記事を検索した時に旧ドメインが出たり、 site:https://gup.monster/ で探したときに、全然でないか、Search Consoleのカバレッジを見たときに全然サイトのURLが集まってない感じなのが原因でしょうか。

記事を削除してから審査というのがあるのですが、それは本末転倒な感じがするのでしません。
ここまでくれば、あとは時間の問題な気がしています。

この記事が本質的な対策をしていました。
文字数とかでなくて、オリジナリティとかでもなくて、単に、
そもそもちゃんとした記事のページがGoogleインデックスされてないって話だと思います。

なぜこんなに苦労するかというと、一つにはなかなかサイトマップを認識できないからなのかなとも思ってます。

アドセンスについては、アクセス状況が改善するまで、
気長に1年がかりくらいで申請しようと思います。 …と思ったらドメインを移行して1か月半にして合格しました。

Google AdSense 4回目の審査

ある程度クローラーが認識してくれたと思うので、
修正したよってのにチェックをつけて再度審査を依頼してみました。
2020/06/26 Search Consoleで移行作業的な頑張りが功を奏したのか合格しました。
それより問題なのはアクセス戻ってない。さらに新ドメインでサイトマップ登録できてない…。

Search Console使って頑張る編

Search Consoleを使って対策します。

クローラー対策

2020/06/06 アクセスが3割くらい減ってきたので悪あがき。 過去のURLでサーチコンソールに登録して、カバレッジで有効になっているURLを個別にみます。
このとき旧URLのままになっているはずなので、「公開テスト」します。
そうするとクロールしてくれて、リダイレクトされているので新URLに変わるので、ここで
「インデックス登録をリクエスト」をひたすら1日の上限まで繰り返すと効率がいいのかも。

サイトマップ登録チャレンジ1回目

サーチコンソールで旧URLを登録してみる。
とりあえずこっちも頑張って新ドメインと並行で、「URLを検査」し、
インデックス登録をリクエスト

アドレス変更をやってみる

URLプレフィックスより旧URL登録 設定 > アドレス変更で最新のURLを入力 としたところ、この現象に遭遇… 確認エラー
「1件以上の必須テストに失敗しました。リクエストを続行できません」

ホームページからの 301 リダイレクト ページを取得できませんでした

うーむ、SEOは強いのに、ことごとくGoogleと相性悪いような(;´Д`)。
https://support.google.com/webmasters/thread/16572782?hl=ja

サイトマップ登録チャレンジ2回目

サイトマップの場所はクローラーが必ず参照する/robots.txtに書かれています。 ここのサイトの場合であれば、
https://gup.monster/robots.txt
です。ここにsitemap_index.xmlが記載されているのですが、
ふと何気にこれを旧ドメインのサーチコンソール
(つまりはてなのドメインだったほう)に投入したところ、なぜか成功しました。しかし0のまま。
ところで、ここの構造は何度か変わっているそうですが、今だと

sitemap_common.xml
sitemap_periodical.xml?year=2020&month=6
sitemap_periodical.xml?year=2020&month=5
...
sitemap_periodical.xml?year=2012&month=1

のように並んでいます。
このsitemap_periodical.xmlに個別の記事の個別、たとえば、
https://gup.monster/entry/2020/06/05/033013
のようなURLが並んでいる構造です。

これ個別に登録すればいいんじゃ?と思い至り、 先頭の1つ目の2020年6月分、
sitemap_periodical.xml?year=2020&month=6 を登録…。すると成功し、件数も出てきました。

どうもGoogle側では旧ドメインのURLでないとうまくいかず、 新ドメインのほうでやるとなぜか確実に失敗するようです。 リダイレクトしてるし、そもそもsitemap内に書かれているURLは新URLなので全部突っ込んであげようと。 ちょっとコピペが面倒なので、このサイトのスクリプトを使い省力化 すると、全部成功するではありませんか(登録して1秒程度は失敗のままですがすぐ成功します)。
f:id:fa11enprince:20200606014420p:plain
厳密にいうと、/aboutとか固定ページ等があるsitemap_common.xmlだけ失敗しますが…。←30分後に成功していました。

ダメもとでこの状態でアドレス変更を試みる

さんざん失敗した「アドレス変更」がこのおかげなのか成功しました。
f:id:fa11enprince:20200606021321p:plain
この機能は少なくとも半年間持続するとのこと。

Search Console を使用してアドレス変更リクエストを送信すると、元のサイトをクロールすることよりも、新しいサイトをクロールしてインデックス登録することを重視するよう、Google に通知されます。また、元のサイトから新しいサイトにさまざまなシグナルが転送され、正規ページを判断する際に、元のサイトよりも新しいサイトが優先されます。このようなアクションは、Search Console で移行を開始してから 180 日間続きます。 https://support.google.com/webmasters/answer/9370220?hl=ja
あとで気づいたのですが、「アドレス変更」ができたら1週間後あたりには旧ドメインからサイトマップを消し去ったほうがスムーズに新URLに移行できるのではないかとおもっています。ただ、地道に正しい新URLを教えてあげる必要はありそう。

サイトマップ登録チャレンジ3回目

旧ドメインのほうのサイトマップはもう大丈夫なのですが、
新ドメインに変更して5週目にして
新ドメインのほうは一つもサイトマップが登録できず。

旧ドメインのURLのサイトマップ登録を消す

2020/06/22 なかなか旧ドメインのURLでのアクセスが減らないのでサイトマップの登録を外す。 外してから2日後くらいから旧ドメインのアクセスがほぼ0になる。

独自ドメイン移行後の経過

独自ドメインに移行してからGoogleからの検索流入の状況についてザックリ記載したいと思います。ちなみにBingのほうはほぼ変化がありません。

1~9週目のアクセス状況

見事に下がって停滞しています。
5週目の時点で旧ドメインのサイトマップを削除しました。
それ以降一気にアクセスが下がり、すぐに旧ドメインへのアクセスがなくなりました。
そしていまだに新ドメインのサイトマップがなぜか全く登録できません。
元の10分の1にまで低下しています。
f:id:fa11enprince:20200717190021p:plain

JavaScriptで静的変数

f:id:fa11enprince:20200513231943j:plain 例えば次のような処理を考えてみます。
名前と身長と体重を与えると、その人のBMIがメソッドにより出力されるというものです。
さらに、食料foodNumがあって、食べると1つ減って、
代わりに1kg増えるものとします。食料はあらかじめ、貯蔵量に限界があって、減る一方とします。
一人の人のを都度処理するなら下記のような感じで書くと思います。
Vueとか使ってるとよく使うオブジェクトリテラルっす。

// 小数第N位で四捨五入
function roundFloat(number, n) {
    var _pow = Math.pow(10 , n);
    return Math.round(number * _pow) / _pow;
}
(function() {
    // Object Literal
    var person = {
        name: 'Bob',
        height: 180,
        weight: 65,
        foodNum: 10,  // classにしたときにこれを静的変数にしたい…
        bmi: function() {
            if (!this.height || !this.weight) {
                return 0;
            }
            return roundFloat(this.weight / Math.pow(this.height / 100, 2), 2);
        },
        show: function() {
            console.log('name: ' + this.name + ", bmi: " + this.bmi() + ", foodNum: " + this.foodNum);
        },
        eat: function() {
            if (this.foodNum != 0) {
                this.foodNum--;
                this.weight += 1;
            }
        }
    };
    person.show();
    person.eat();
    person.show();
})();

出力結果はこんな感じです

name: Bob, bmi: 20.06, foodNum: 10
name: Bob, bmi: 20.37, foodNum: 9

ただ、複数人のを一度に処理したいときにclass化してかつ、foodNumは共有資源で誰かが食べると、
みんなの分が減るものとします(なんか怖いですが)。
そうした時に、global変数以外では静的変数にしたくなります。
しかし、JavaScriptには静的変数がありません…。というのでこうやってしまおう!というもの。

ES5版

function roundFloat(number, n) {
    var _pow = Math.pow(10 , n);
    return Math.round(number * _pow) / _pow;
}
(function() {
    // Class
    var Person = function(name, height, weight) {
        if (!(this instanceof Person)) {
            return new Person(name, height, weight);
        }
        this.name = name;
        this.height = height;
        this.weight = weight;
    };
    Person.prototype.bmi = function() {
        if (!this.height || !this.weight) {
            return 0;
        }
        return roundFloat(this.weight / Math.pow(this.height / 100, 2), 2);
    };
    Person.prototype.show = function() {
        console.log('name: ' + this.name + ", bmi: " + this.bmi() + ", foodNum: " + Person.foodNum);
    };
    Person.prototype.eat = function() {
        if (Person.foodNum && Person.foodNum != 0) {
            Person.foodNum--;
            this.weight += 1;
        }
    }
    Person.foodNum = 10;  // static variable

    var person1 = new Person('Bob', 180, 65);
    person1.show();
    person1.eat();
    person1.show();
    var person2 = new Person('Alice', 170, 50);
    person2.show();
    person2.eat();
    person2.show();
})();

出力結果

name: Bob, bmi : 20.06, foodNum: 10
name: Bob, bmi : 20.37, foodNum: 9
name: Alice, bmi : 17.3, foodNum: 9
name: Alice, bmi : 17.65, foodNum: 8

Person.foodNumと単にオブジェクトにプロパティを設定してあげているのがポイントです。 JavaScriptっぽいですね。JavaScriptに慣れてない人はなんだこの変態文法と思うはずです。
そんな人はES2015版を見てもらえばいいかもしれません。ただし、実運用で使う場合はBabelを忘れずに…。

ES2015版

const roundFloat = (number, n) => {
    const _pow = Math.pow( 10 , n );
    return Math.round( number * _pow ) / _pow;
}
(() => {
    // ES2015 Class
    class Person {
        constructor(name, height, weight) {
            this.name = name;
            this.height = height;
            this.weight = weight;
        }
        static foodNum = 10;  // static variable
        bmi() {
            if (!this.height || !this.weight) {
                return 0;
            }
            return roundFloat(this.weight / Math.pow(this.height / 100, 2), 2);
        }
        show() {
            console.log(`name: ${this.name}, bmi : ${this.bmi()}, foodNum: ${Person.foodNum}`);
        }
        eat() {
            if (Person.foodNum != 0) {
                Person.foodNum--;
                this.weight += 1;
            }
        }
    }
    const person1 = new Person('Bob', 180, 65);
    person1.show();
    person1.eat();
    person1.show();
    const person2 = new Person('Alice', 170, 50);
    person2.show();
    person2.eat();
    person2.show();
})();

さっきと同じです。ES5版のSyntax Sugarっすね。Java/C#っぽくなりました。TypeScriptだとなお近くなります。

ぼやき

JavaScriptとNode力が足りないのでたまにES5で書くとthis.hoge = function() {}と書かねばならないところを、
var foo = function() {}とやって自爆しまう雑魚でした。
ECMAScript 6 compatibility table を見ると、もう最近はIEを捨てると、ちょっとしたJSを書くときではBabeらずにES2015で書いてしまってもいいかもしれない…と思った。 JSの辛いところは古くからあるライブラリはやはりES5以前の知識が必須なところですかね。

参考リンク

Static variables in JavaScript - Stack Overflow

上限付き境界ワイルドカードについて

f:id:fa11enprince:20200511185745j:plain Javaを使っていると、Clazz<Foo extends Bar>とかClazz<? extends T>というような
あまり見た目にやさしくない呪文を見かけると思います。
これはなんだ?って話です。
これは上限付きワイルドカード(upper bounded wildcard)といいます。

そもそもなんでこれが必要なのか

メソッドやクラスを使う場合を考えてみると
どんな型でも受け入れたい場合どうすればよいか。
その場合はObjectを使います。。。もしくは?(unbounded wildcard)を使います。。。
ってのは冗談で、そうすると使いたいメソッドがObjectにないことが多くて
警告でcastしてないよ!!ってのがしょっちゅう出てきます。 そこで出てくるのがジェネリクス(Generics)です。
List<E>とかがその典型ですね。
List<Integer>とすればInteger型でListが扱えます。
List<String>とすればString型でListが扱えます。

List<T>だけで十分ではないの?って思うと思います。 たいていはそれで充分なのですが、なんかたまに
? extends T
ってのを見かける。これJava特有だし。(C#にはwhereなるものがあるが…) これライブラリでよく見ると思いますが、自分での実装で使う機会はそう多くないため、
いつ使うんだ?ってなりがちです。

具体例

例えばですが、配列を使っていてソートをするのに
挿入ソートのクラスでどんな型でも受け入れたいってことにしたい場合、
こいつが必要になってきます。
すごい単純なクラスを作ってみます。
単にInsertionSorter<T>となっていないところがポイントです

import java.util.Arrays;

public class InsertionSorter<T extends Comparable<T>> {
    T[] data;

    public void sort(T[] data) {
        this.data = data;
        isort(data);
    }

    private void isort(T[] data) {
        for (int left = 1; left < data.length; left++) {
            for (int right = left; right > 0
                 && data[right - 1].compareTo(data[right]) > 0; right--) {
                swap(data, right - 1, right);
            }
        }
    }

    public void swap(T[] arr, int pos1, int pos2) {
        T temp = arr[pos1];
        arr[pos1] = arr[pos2];
        arr[pos2] = temp;
    }
    
    public static void main(String[] args) {
        Integer[] data = { 5, 4, 3, 2, 6, 7, 8, 1, 9 };
        InsertionSorter<Integer> sorter = new InsertionSorter<>();
        sorter.sort(data);
        Arrays.stream(data).forEach(i -> System.out.print(i + ","));
    }
}

オンラインで実行したい方はこちら: http://tpcg.io/bTn8mbKH

sortをinterfaceのメソッドにしてなくてStrategyパターンになれないから微妙ですが…例なのでシンプルに。

<T extends Comparable<T>>
Comparableを継承するTって意味です(extends継承だけでなくimplements継承も含まれます)。
要はComparableを継承するものに制限しています。
上記のクラスではcompareTo(T o)を使っています。
compareTo(T o)を使えるのはComparableな型でなくてはなりません。
ところが単にTとしてしまうとcompareTo(T o)なんてメソッドありませんとエラーになってしまいます。
ピンとこない方は、上記のURLでInsertionSorterのGenericsの部分を単にTに変えてみてください。 実行しようとするとエラーになると思います。 それを解決するためにこのT extends Comparable<T>というものがあります。
こうすると、「ほうほう、このTはComparableなんだな」とコンパイラがわかってくれてエラーにしなくなります。

参考リンク

https://www.ibm.com/developerworks/jp/java/library/j-jtp07018.html

Express 4 + EJS + Socket.IOでWebSocketをやってみる

f:id:fa11enprince:20200416000912j:plain WebSocketをNode.js(Express 4 + EJS + Socket.IO)で復習がてら実装しました。 ハマりポイントはbin/wwwを修正しないと動いてくれないところ。
Expressってシンプルで綺麗な設計のフレームワークなんですが、
結構はまりやすいポイントが多いと思います。

Nodeおよびライブラリのバージョン

Node.js   12.14.1
Express    4.16.1
socket.io  2.3.0
EJS        2.6.0

最終成果物

次のと同じものを作る手順を記載します。

ひな形を作成

まずexpress-generatorをグローバルインストールをします。

$ npm install express-generator -g

次にひな形生成をします。いわゆるスキャフォールディングです。 プロジェクトのフォルダはexpress-ws-ejsとします。 テンプレートはEJSを選択します

$ cd express-ws-ejs
$ express --ejs

ソースコードを修正

まずsocket.ioを使えるようにします。

$ npm i socket.io --save
$ npm i

/models/chat.js

WebSocketのサーバ側です。 新規作成します。modelsフォルダを作り、chat.jsを作成します。

const socketio = require('socket.io');

function chat(server) {
    const sio = socketio.listen(server);
    sio.on('connection', function(socket) {
        socket.on('chat-message', function(msg) {
            console.log('Send message to client');
            sio.emit('chat-message', msg + '💛');
        });
        socket.on("disconnect", function() {
        });
    });
};

module.exports = chat;

■補足
いくつかあるemitの違い

const sio = socketio.listen(server);
sio.on('connection', function(socket) {
    socket.emit('chat-message', 'message'); // 送信元クライアントだけに送信
    socket.broadcast.emit('chat-message', 'message'); // 送信元を除く全クライアントに送信
    sio.emit('chat-message', 'message'); // 接続されている全クライアントに送信
}

他にも似たようなのがありますが、これだけ押さえておけばよいでしょう。
javascript - Send response to all clients except sender - Stack Overflow

/bin/www

ここでsocket.ioのlistenをしている/models/chat.jsを呼ばないといけません。
chat.jsはのちほど作成します。 app.jsでこれを呼ぶことはできません。というのもcreateServerをしているのが
/bin/wwwだからだと思われます。 ちなみにですが、varじゃなくてconstがいいのですがexpress-generatorvarで作るので、
varのままにしていたりします。

@@ -5,7 +5,8 @@
  */
 
 var app = require('../app');
-var debug = require('debug')('express-ws-ejs-o:server');
+var chat = require('../models/chat');
+var debug = require('debug')('express-ws-ejs:server');
 var http = require('http');
 
 /**
@@ -28,6 +29,7 @@
 server.listen(port);
 server.on('error', onError);
 server.on('listening', onListening);
+chat(server);
 

/app.js

usersのrouterはいらないので消します

@@ -5,7 +5,6 @@
 var logger = require('morgan');
 
 var indexRouter = require('./routes/index');
-var usersRouter = require('./routes/users');
 
 var app = express();
 
 @@ -20,7 +19,6 @@
 app.use(express.static(path.join(__dirname, 'public')));
 
 app.use('/', indexRouter);
-app.use('/users', usersRouter);
 
 // catch 404 and forward to error handler
 app.use(function(req, res, next) {

/controllers/index.js

これは新規作成します。controllersフォルダを作り、index.jsを作成します。

exports.index = function(req, res) {
    res.render('index', { title: 'Express' });
}

/routes/index.js

var express = require('express');
var router = express.Router();
var index_controller = require('../controllers/index');

router.get('/', index_controller.index);

module.exports = router;

その他リソースおよびview

主にクライアント側です。
Node使ってるとクライアント側なのかサーバ側なのかたまに混乱します。
/public/javascripts/index.js

$(function() {
    const socket = io('http://localhost:3000');
    $('form').submit(function() {
        console.log($('#m').val());
        socket.emit('chat-message', $('#m').val());
        $('#m').val('');
        return false;
    });
    socket.on('chat-message', function(msg) {
        $('#messages').append($('<li>').text(msg));
        window.scrollTo(0, document.body.scrollHeight);
    });
});

/public/stylesheets/index.css

* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
#messages { margin-bottom: 40px }

/views/index.js

@@ -2,10 +2,18 @@
 <html>
   <head>
     <title><%= title %></title>
-    <link rel='stylesheet' href='/stylesheets/style.css' />
+    <link rel='stylesheet' href='/stylesheets/index.css' />
   </head>
   <body>
-    <h1><%= title %></h1>
-    <p>Welcome to <%= title %></p>
+    <ul id="messages"></ul>
+    <form>
+        <input id="m" autocomplete="off">
+        <button type="submit">Send</button>
+    </form>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
+    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
+        integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8="
+        crossorigin="anonymous"></script>
+    <script src="javascripts/index.js"></script>
   </body>
 </html>

Expressの起動

最後にExpressを起動すれば終わりです。

$ npm start

http://localhost:3000 にアクセスし、ブラウザのウィンドウを2つ以上立ち上げると動作確認ができます。 f:id:fa11enprince:20200416002210p:plain

参考

https://socket.io/get-started/chat/
https://liginc.co.jp/web/programming/node-js/132081
https://developer.mozilla.org/ja/docs/Learn/Server-side/Express_Nodejs
https://www.gitignore.io/api/node