Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

なぜrequire('lodash/zipWith')で関数ひとつだけ読み込めるのか

lodashというユーティリティライブラリがあります。lodashは便利な関数をたくさん提供しています。たとえば配列をシャッフルするshuffle()や、配列の差をとるdifference()などが挙げられます。

lodashは300個以上の関数を提供しています。しかし、実際に使うのはそのうちせいぜい10個です。コードサイズが大きくなってしまうのはJavaScript的に大罪です。そこで、必要な関数だけピックアップする方法が提供されています。それがタイトルにもあるrequire('lodash/zipWith')といった書き方です。

今回はどうしてそれで300を超える関数が読み込めるようになっているのか解説します。

lodashモジュールの様々な読み込み方

lodashは以下のように複数の読み出し方に対応しています。

const {zipWith} = require('lodash'); // lodashのすべてが読み込まれてしまっている
const {zipWith} = require('lodash/array'); // lodashのうち配列操作系だけ読み込む
const zipWith = require('lodash/zipWith'); // 関数をピンポイントで読み込む

ES Modulesを使う書き方もできます(node_modules/配下を参照するので、Node.jsでのみ動作します)。

import * as _ from 'lodash';
import * as lodashArray from 'lodash/array';
import zipWith from 'lodash/zipWith';

では、どうしてこのような読み込み方ができるのでしょうか?実際にインストールしたパッケージを見てみます。

f:id:panda_noir:20190419212255p:plain

なんと631個もJavaScriptファイルが置かれています!そして、それぞれ関数に対応したファイル、arrayに対応したファイルがあります。

つまり、require('lodash/array')ではlodashパッケージ内にあるarray.jsが読み込まれます。同様にrequire('lodash/zipWith')ではzipWith.jsが読み込まれます。

仕様ではどうなっているのか?

Node.jsのドキュメントのModulesページを見てみます。

どうやら、require('example-module/path/to/file')./node_moduels/example-module/path/to/fileと同様のようです(現在のディレクトリのnode_modulesになければ見つかるまで親ディレクトリを辿っていくようです)。

つまり、上のrequire('lodash/array')はドキュメントで保証されている動作らしいです。

これって名前空間なのでは?

このrequire('lodash/array')という書き方、lodashという名前空間のarrayを読み込むと見えませんか?実際の動作も名前空間にみえます。非常に面白いですね。

ただ、実際にコレをやっているパッケージをあまり見ない気がします。そもそもやる必要があるパッケージが少ないというのがありますが。

気がついたらimmerが独自クラスのインスタンスもサポートしていた

immerという、ミュータブルな操作を書く感覚でイミュータブルな操作が行えるライブラリがあります。

import {produce} from 'immer';

// 破壊的に配列をシャッフルする関数
const shuffle=(a,i=a.length,j) => {for(;[a[i],a[j]]=[a[j=0|Math.random()*i],a[--i]],i;);};

const arr = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
const shuffledArray = produce(arr, (draftArray) => {
    shuffle(draftArray);
}); // シャッフルされた配列が得られる

以前のimmerはネイティブの配列とオブジェクトしか扱えませんでした。しかし、久しぶりに見てみたらインスタンスにも適用できるようになっていました(ほとんどのインスタンスは対応していますが、すべてに対応しているわけではないです)。

1. インスタンスに対応

immerのインスタンス対応はMyClass.prototype[immerable] = trueか、myInstance[immerable] = trueとするだけで対応できます。

import {produce, immerable} from 'immer';
class Vector{
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    setZ(z) {
        this.z = z;
    }
}

const vector = new Vector(1, 2);
vector[immerable] = true; // これだけでimmerで使えるようになります

const after = produce(vector, draft => {
    draft.x += 4;
    draft.y += 4;
    draft.setZ(7);
});

console.log(vector, after);

immerableはprototypeに直接設定しても問題ありません。(というかこっちのほうが恐らく主流です)

Vector.prototype[immerable] = true;

クラスがgetterやsetterを使っている場合には使えないようです。

2. draftを外部に出せるcreateDraft()

draftを外に出すと、ネストが浅くなるだけでなく既存のコードをほんの少し変えるだけでimmutable化できるようになります。たとえばshuffle()という破壊的変更を加える関数を考えます。

const shuffle = (arr) => {
    // 破壊的にarrをシャッフルする
    for (let i = arr.length - 1; i > 0; i = 0 | i - 1) {
        const j = 0 | Math.random() * (i+1);
        [arr[i], arr[j]] = [arr[j], arr[i]];
    }
};
const arr = [1, 2, 3, 4, 5];
shuffle(arr);

このコードにcreateDraftを加えてちょっと書き直せば、arrが破壊されなくなります。

import {createDraft, finishDraft} from 'immer';
const shuffle = (arr) => {
    // 破壊的にarrをシャッフルする
    for (let i = arr.length - 1; i > 0; i = 0 | i - 1) {
        const j = 0 | Math.random() * (i+1);
        [arr[i], arr[j]] = [arr[j], arr[i]];
    }
};
const arr = [1, 2, 3, 4, 5];
const draft = createDraft(arr); // 追加
shuffle(draft); // draftを渡すように変更する
const shuffledArr = finishDraft(draft); // 追加

ただ、個人的にはネストされたほうが「このエリアはimmerのエリアだな」と分かりやすくなるのでcreateDraftよりproduceが好きです。

Nginxのincludeディレクティブはいちいちフルパスを書かなくてもいい

ずっとNginxのincludeディレクティブは絶対パスをちゃんと書かないといけないと思っていましたが、どうやら--prefixで指定したディレクトリからの相対パスでも書けるようです。

$ nginx -V
nginx version: nginx/1.15.10
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx

configure arguments内の--prefix=/etc/nginxから見た相対パスを書くことが出来ます。たとえば以下の2つは同じです。

include /etc/nginx/conf.d/default.conf;
include conf.d/default.conf;

というか、すでにPHPを使っている場合はこの記法を使っているはずです。

include fastcgi_params;
include /etc/nginx/fastcgi_params; # こう書いてもいい

超!体系的に学ぶWebアプリケーション開発

この記事はWebサーバーを建てるところからWebアプリケーションを開発できるようになることを目標としています。

目次

  • 目次
  • この記事の目標
  • [超!基本]
    • Webページが表示されるまで
    • HTMLとは?
    • JavaScriptとは?
  • [超!Webサーバー]
    • Webサーバーとは?
    • ファイアウォールによるコントロール
  • [超!Webアプリケーション]
    • React+TypeScriptを始めてみる
      • 1. Node.jsをインストールする
      • 2. 作業用ディレクトリを作る
      • 3. npmのセットアップを行う
      • 4. ReactやTypeScript、Webpackをインストールする
    • React+TypeScriptでHello World
  • [超!通信]
    • HTTPSとHTTPの違い
    • HTTP通信の流れ
    • HTTP/2とは?
    • WebSocketとは?

この記事の目標

この記事では、Webフレームワークに頼らずにWebサーバーを建て、Webアプリケーション開発の環境構築まで行って、Web開発の基礎を固めることを目的とします。

  • Webサーバーの動作を理解する
  • Webアプリケーションの開発の始め方を理解する
  • LaravelやRuby on RailsなどWebアプリケーションフレームワークに通じる基礎を理解する
  • WebSocketやHTTP/2など最新技術がどこで動くのか理解する
続きを読む

バンドラなんていらない時代になりつつあるぞ

Webpackだとかbabelだとか、ダサくないですか?設定してnpm run buildして…そんなことしないで済む時代が来ているんですよ!

バンドラがなぜ必要だったのか

バンドラはたくさんあるファイルを1つにまとめ、依存関係を解消することが目的でした。

しかし、ES Modulesの普及に伴い、<script type="modules">が現実的に使えるようになりつつあります。

また、HTTP/2においてサーバープッシュという、リクエストされていないファイルを送信できる機能が登場しました。これを使うと、importするファイルをあらかじめ送ることができます。

@pika/web + サーバープッシュという選択肢

@pika/webという依存パッケージをES Modulesで読み込めるように変換するツールがあります。変換したパッケージはimport {createElement, render} from './web_modules/preact.js';のようにして読み込むことができます。

@pika/webなら$ npx @pika/webを実行するだけで、ES Modulesを使った開発ができるようになります。

依存ファイルをあらかじめ送信する

ES Modulesでは依存ファイルを以下のようにしてダウンロードします。

  1. JSファイルを読み込む
  2. 解析する
  3. ファイル内にあるimportを見つける
  4. 依存ファイルをリクエストする
  5. ダウンロード完了してから処理を再開する

サーバープッシュを使えば、依存ファイルはすでにダウンロード済みなので4の段階をスキップできます。

Nginxでサーバープッシュする

Nginxなら、つぎの設定でサーバープッシュを使用できます(参考: NginxがHTTP2サーバプッシュに対応したので試す - ASnoKaze blog)

server {
    listen 443 http2;
    # HTTPSの設定は省略します。
    server_name example.com;

    http2_push_preload on;
    location / {
        root   html;
        add_header "Link" "</main.js>;rel=preload;as=module, </web_modules/preact.js>;rel=preload;as=module";
        index  index.html;
        try_files $uri $uri.html $uri/index.html = 404;
    }
}

実際に読み込んでみる

デモを用意しました。

f:id:panda_noir:20190311155423p:plain
サーバープッシュあり

f:id:panda_noir:20190311155427p:plain
サーバープッシュなし

f:id:panda_noir:20190311162407p:plain
バンドルあり

サーバープッシュありのほうが若干速くなっています。ただし、バンドルありにはまだ勝てません。