Angular の electron application をそのまま build する

はじめに

Angular の electron application をそのまま build する方法をまとめます。
Angular の electron アプリは maximegris/angular-electron のようなボイラープレートをベースに開発する記事が多いですが、今回は利用せず electron-userland/electron-builder をそのまま利用していきます。

Angular の electron application をそのまま build する

まずは Angular CLI でプロジェクトを開始します。
ng serve でいつもの画面が出ることを確認します。

$ ng new angular-electron
$ cd angular-electron
$ ng serve

f:id:nananao_dev:20200818080637p:plain

Angular と electron の開発環境を作る

ng serve で十分ではありますが electron の window で確認したくなることがあるかもしれません。
大した手間ではないので electron を devDependencies にインストールします。

$ npm install --save-dev electron@latest

package.jsonmainscripts に以下を追加します。

  "main": "main.js",
  "scripts": {
    ...
    "start:electron": "ng build --base-href ./ && tsc -p tsconfig.electron.json && electron .", // 追加
    ...
  }

package.json と同じ階層の project root dir に main.ts を配置します。
webPreferenceswebSecurity: false を指定すると CORS を無視できるようです。

const { app, BrowserWindow } = require('electron');
const url = require('url');
const path = require('path');

let mainWindow;

function createWindow(): void {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      webSecurity: false,
    },
  });

  mainWindow.loadURL(
    url.format({
      pathname: path.join(__dirname, `/dist/index.html`),
      protocol: 'file:',
      slashes: true,
    })
  );
  // Open the DevTools.
  mainWindow.webContents.openDevTools();

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow();
  }
});

今回 main.ts と typescript ファイルとしたので tsconfig.electron.json として tsconfig をproject root に配置します。
"target": "es2015" でもそのまま動くようです。

{
  "compilerOptions": {
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowJs": true,
    "target": "es2015",
    "types": ["node"],
    "lib": ["es2017", "es2016", "es2015", "dom"]
  },
  "files": ["main.ts"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

最後に angular.jsonoutputPathdist に変更します。
json はコメントアウトできないので削除してください。

...
"options": {
  ...
  // "outputPath": "dist/angular-electron", <- 削除
  "outputPath": "dist",
  ...
}

それでは electron の開発環境を start してみましょう。
main.jsmainWindow.webContents.openDevTools(); としているのでデバッグコンソールごと開きます。

$ npm run start:electron

f:id:nananao_dev:20200818083131p:plain

Angular の electron application を build する

mac や windows 向けに electron を build していきます。
electron-builder を devDependencies にインストールします。

$ npm install --save-dev electron-builder

electron-builder.json を project root に配置します。
このファイルの files 配下がビルドに含めるファイルとなります。
"output": "release/" としているので angular の outputPath とは別に release というフォルダに実行ファイルが吐かれます。

{
  "productName": "angular-electron",
  "directories": {
    "output": "release/"
  },
  "files": [
    "**/*",
    "!**/*.ts",
    "!*.code-workspace",
    "!LICENSE.md",
    "!package.json",
    "!package-lock.json",
    "!src/",
    "!e2e/",
    "!hooks/",
    "!angular.json",
    "!_config.yml",
    "!karma.conf.js",
    "!tsconfig.json",
    "!tslint.json"
  ],
  "win": {
    "icon": "dist/assets/icons",
    "target": ["portable"]
  },
  "mac": {
    "icon": "dist/assets/icons",
    "target": ["dmg"]
  },
  "linux": {
    "icon": "dist/assets/icons",
    "target": ["AppImage"]
  }
}

package.jsonscripts に以下を追加します。

  "scripts": {
    ...
    "electron:build:mac": "npm run build:electron && node_modules/.bin/electron-builder build --mac --x64",
    "electron:build:win": "npm run build:electron && node_modules/.bin/electron-builder build --win --x64",
    "build:electron": "npm run electron:tsc && ng build --base-href ./",
    "electron:tsc": "tsc -p tsconfig.electron.json"
  },

あとはターミナルから build するだけ!

$ # mac 向けの build
$ npm run electron:build:mac
$ # win 向けの build
$ npm run electron:build:win

すると release フォルダに dmg/exe ができているので実行するだけ!
macであれば application フォルダに入れちゃいなよって出ます。

f:id:nananao_dev:20200818091222p:plain

まとめ

Angular の electron application をそのまま build する方法をまとめました。
github レポジトリに置いておいたので参考にしてください。

github.com

参考

qiita.com

material icons を css でカスタマイズしてログインアイコン作る

はじめに

material icons を css でカスタマイズしてよく見るログインアイコンを作る流れをまとめます。
早い話このアイコンです。

login icon

material icons を css でカスタマイズする流れ

まずはパクり参考にしたいiconのカラーコードを取得したいところです。
私は以下のサイトにパクり参考にしたい画像を放り込んでカラーコードを取得することがあります。

lab.syncer.jp

あとはhtml/cssを書くだけですね。

html

<button mat-icon-button >
  <mat-icon class="login-icon">person</mat-icon>
</button>

css

.login-icon {
  background: #a4c4fc;
  color: #4374e3;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}

material icons は background 指定できるんですね。
と言う気付きでした。

まとめ

material icons を css でカスタマイズしてよく見るログインアイコンを作る流れをまとめます。

stackblitz.com

Angular Material で Dark Mode への切替をつくる

はじめに

Angular Material で Dark Mode への切替を作ります。

f:id:nananao_dev:20200721231743p:plain
dark mode

Angular Material

Angular Material の mat-light-thememat-dark-theme という予め準備されているものを利用します。
.light-theme という class がつけばライトモードで .dark-theme という class がつけばダークモードとし ngClass で切り替えます。

$app-light-primary: mat-palette($mat-orange, 600);
$app-light-accent: mat-palette($mat-blue, 800);
$app-light-warn: mat-palette($mat-red, 500);


$app-dark-primary: mat-palette($mat-lime, A200);
$app-dark-accent: mat-palette($mat-cyan, A400);
$app-dark-warn: mat-palette($mat-red, A400);

$app-light-theme: mat-light-theme($app-light-primary, $app-light-accent, $app-light-warn);
$app-dark-theme: mat-dark-theme($app-dark-primary, $app-dark-accent, $app-dark-warn);

.light-theme {
  @include angular-material-theme($app-light-theme);
}

.dark-theme {
  @include angular-material-theme($app-dark-theme);
}

hasToggledTheme という bool値を toggleさせます。
ドキュメントにある通り mat-app-background という class で変化させたい対象を括ります。

<div [ngClass]="hasToggledTheme ? 'dark-theme' : 'light-theme'">
  <div class="mat-app-background">

    <mat-toolbar>
      <span>Theme Toggle</span>
      <span class="toolbar-spacer"></span>
      <mat-icon>brightness_5</mat-icon>
      <mat-slide-toggle [checked]="hasToggledTheme" (change)="toggledTheme()"></mat-slide-toggle>
      <mat-icon>brightness_2</mat-icon>
    </mat-toolbar>

    <!-- ここにボタン等をいれる -->

  </div>
</div>

あとは typescript 側で toggle します。

  toggledTheme() {
    this.hasToggledTheme = !this.hasToggledTheme;
  }

stackblitz 貼っておきます。

stackblitz.com

まとめ

Angular Material で Dark Mode への切替を作りました。
今回まとめた方法以外にも service から theme を持ってくる方法もあるようです。
ユーザーにもう少し theme をいじらせたい時はこういう感じにすると良いかもしれません。

angular-material-theme-switch-3a6bpw - StackBlitz

go の middleware で status code を取得する

はじめに

go の net/http の middleware 内で status code を取得する方法をまとめます。
go の ResponseWriter という interface を status code 断面で深掘りするお話となります。

golang.org

go の middleware で status code を取得する

go の net/http の middleware として rs/zerolog を使う で書いた通り logger などは status code に応じて処理を変えたいことがあります。

net/http の middleware は以下のような型を持っている必要があるので http.ResponseWriter を Wrap して status code を取得してみます。

func Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        next.ServeHTTP(w, r)
    })
}

chi/middleware の NewWrapResponseWriter()

chi/middleware には NewWrapResponseWriter() という関数があり ResponseWriter を引数にとって status code を含む WrapResponseWriter という構造体を返してくれます。

この関数を使って status code を取得する流れを見ていきます。

ww := middleware.NewWrapResponseWriter(http.ResponseWriter, *http.Request.ProtoMajor)
fmt.Println("status code: ", ww.Status())

ResponseWriter

そもそも http.ResponseWriter には status code というプロパティを持ってません。
ResponseWriter は以下のような interface になってます。

type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

以下のような ResponseWriter を Wrap して取り出しやすい位置に status code を置いた構造体 basicWriter があります。

type basicWriter struct {
    http.ResponseWriter
    wroteHeader bool
    code        int
    bytes       int
    tee         io.Writer
}

この構造体に以下のように WriteHeader() を実装してあげると Wrap した構造体側にも status code を格納することができます。

func (b *basicWriter) WriteHeader(code int) {
    if !b.wroteHeader {
        b.code = code
        b.wroteHeader = true
        b.ResponseWriter.WriteHeader(code)
    }
}

通常は WriteHeader は以下のように response に対して生えているメソッドになっていて w.status = code として response へ status code を格納するような形で使われているようです。

func (w *response) WriteHeader(code int) {
    if w.conn.hijacked() {
        caller := relevantCaller()
        w.conn.server.logf("http: response.WriteHeader on hijacked connection from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
        return
    }
    if w.wroteHeader {
        caller := relevantCaller()
        w.conn.server.logf("http: superfluous response.WriteHeader call from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
        return
    }
    checkWriteHeaderCode(code)
    w.wroteHeader = true
    w.status = code

    if w.calledHeader && w.cw.header == nil {
        w.cw.header = w.handlerHeader.Clone()
    }

    if cl := w.handlerHeader.get("Content-Length"); cl != "" {
        v, err := strconv.ParseInt(cl, 10, 64)
        if err == nil && v >= 0 {
            w.contentLength = v
        } else {
            w.conn.server.logf("http: invalid Content-Length of %q", cl)
            w.handlerHeader.Del("Content-Length")
        }
    }
}

chi/middlewarebasicWriter では Status() というメソッドが生えているので、先ほど格納した status code が簡単に取得できるという流れでした。

func (b *basicWriter) Status() int {
    return b.code
}

まとめ

go の middleware で status code を取得する流れについてまとめました。
net/http は何度読んでも勉強になります。