ESLint の設定 (Nuxt TypeScript)
Nuxt で開発する時の ESLint の設定ファイルです。Nuxt のバージョンは 2.10 です。
前提
構文チェックに ESLint, コードフォーマットには Prettier を利用します。eslint-plugin-prettier
を利用して ESLint 経由で Prettier を動かします。
参考:
Integrating with Linters · Prettier
設定ファイル
npm i -D @nuxtjs/eslint-config-typescript eslint eslint-config-prettier eslint-plugin-nuxt eslint-plugin-prettier prettier
// .eslintrc.js module.exports = { root: true, extends: [ 'eslint:recommended', '@nuxtjs/typescript', 'plugin:nuxt/recommended', 'plugin:vue/strongly-recommended', 'plugin:prettier/recommended', 'prettier/vue' ], rules: { // Add rules } }
ESLint の設定ファイルを読む
ざっくりです。
基本知識
rules
に設定されたルールがチェックされます。
module.exports = { rules: { semi: ['error'] } }
ESLint のルール以外の構文チェックを設定したい場合 plugins
を追加します。プラグインの名前から eslint-plugin-
を省略することができます。eslint-plugin-vue
なら vue
で設定可能です。
module.exports = { plugins; ['vue'], rules: { 'vue/html-self-closing': ['error'] } }
参考:
https://eslint.org/docs/user-guide/configuring#configuring-rules
https://eslint.org/docs/user-guide/configuring#configuring-plugins
実際に適用されるルールを確認
ESLint には --print-config path::String
というコマンドが用意されています。これを利用することで、最終的にどのような設定が適用されるのか確認ができます。
node_modules/.bin/eslint --print-config pages/index.vue
{ "env": { "browser": true, "node": true, "es6": true, "jest/globals": true }, "globals": { "document": false, "navigator": false, "window": false }, "parser": "<PATH_TO_PROJECT>/node_modules/vue-eslint-parser/index.js", "parserOptions": { "ecmaVersion": 2018, "sourceType": "module", "ecmaFeatures": { "jsx": true }, "parser": "@typescript-eslint/parser" }, "plugins": [ "standard", "promise", "node", "import", "unicorn", "jest", "@typescript-eslint", "nuxt", "vue", "prettier" ], "rules": { "vue/html-self-closing": [ 0 ], "vue/array-bracket-spacing": [ "off" ], // ... (数が多いので省略) }, "settings": { "import/resolver": { "node": { "extensions": [ ".js", ".mjs" ] } } } }
.eslintrc.js
にはなかった plugins
や rules
の設定が追加されているのが確認できると思います。
設定ファイルを拡張する extends
extends
を利用すると既存の設定ファイルを再利用することができます。npm から設定ファイルをインストールして利用することも可能です。
参考:
https://eslint.org/docs/2.0.0/user-guide/configuring#extending-configuration-files
命名規則
plugins
と同様に extends
も名前を省略して設定することができます。eslint-config-
が省略可能です。
プラグイン内の設定ファイルを利用する場合は plugin:
を先頭につけます。名前の先頭の eslint-plugin-
はここでも省略可能です
最初の設定ファイルを省略なしで記述すると以下のようになります。
extends: [ 'eslint:recommended', '@nuxtjs/eslint-config-typescript', 'plugin:eslint-plugin-nuxt/recommended', 'plugin:eslint-plugin-vue/strongly-recommended', 'plugin:eslint-plugin-prettier/recommended', 'eslint-config-prettier/vue' ],
命名規則がわかると利用されているパッケージ名がわかるので確認しやすくなると思います。 (この記事で言いたいことはこれくらいかもしれない)
@nuxtjs/eslint-config-typescript
試しに1つ、今回利用している設定ファイルを確認してみます。
eslint-config/packages/eslint-config-typescript at master · nuxt/eslint-config · GitHub
module.exports = { extends: [ '@nuxtjs' ], plugins: ['@typescript-eslint'], parserOptions: { parser: '@typescript-eslint/parser' }, rules: { '@typescript-eslint/no-unused-vars': ['error', { args: 'all', argsIgnorePattern: '^_' }] } }
内部で @nuxtjs/eslint-config
, @typescript-eslint/eslint-plugin
, @typescript-eslint/parser
を利用しているのがわかります。
いつ ESLint を走らせるのか
(ここの説明は雑です)
手動で
eslint --ext .ts,.vue, .
eslint --ext .ts,.vue --fix .
コミット時
husky
の pre-commit
hook に lint-staged
を設定することで、変更したファイルのみを ESLint の対象にする。
ファイル保存時
VSCode を利用している場合
{ "eslint.validate": ["javascript", "typescript", "vue"], "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "editor.formatOnSave": false }
保存時に ESLint を実行させます。必要に応じて "editor.formatOnSave": false,
のように既存処理が邪魔をしない設定も追加してください。
Date と Timezone
ちょっとしたメモです。でてくるサンプルコードは JavaScript です。
- ISO 8601とは
- 日付と時刻の表記に関するISOの国際規格
2010-10-10T00:00:00.000+09:00
みたいなやつ- ISO 形式 (ISO 8601) の文字列判定 - ryotah’s blog
- 1888年以前は東京のオフセットは +09:18:59 なんだそうです
new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long', timeStyle: 'long' }).format(new Date(-2587712400000)) // => "1888年1月1日 0:00:00 GMT+9" new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long', timeStyle: 'long' }).format(new Date(-2587712400001)) // => "1888年1月1日 0:18:58 GMT+9:18:59"
- Date オブジェクトは関係演算子 (
==
とか>
とか) で比較可能- 比較演算子 - JavaScript | MDN
moment
やluxon
などの Date 系ライブラリで生成されたオブジェクトが関係演算子で比較できるのも、valueOf
を持っているから- https://moment.github.io/luxon/docs/manual/math.html#comparing-datetimes
DateTime implements #valueOf to return the epoch timestamp, so you can compare DateTimes with <, >, <=, and >=.
- 厳密等価演算子 (
===
) は型変換なしで比較をするので注意 (同時刻だとしてもtrue
にはならない)
- https://moment.github.io/luxon/docs/manual/math.html#comparing-datetimes
- IANA timezone name とは
America/Los_Angeles
,Asia/Tokyo
のようなタイムゾーンを表すことができる名前- https://www.iana.org/time-zones
The Time Zone Database (often called tz or zoneinfo) contains code and data that represent the history of local time for many representative locations around the globe
- https://ja.wikipedia.org/wiki/Tz_database
- IANA timezone と IE11
- IE 11 でも
Intl.DateTimeFormat
は利用できるが IANA timezone name には対応していない - http://kangax.github.io/compat-table/esintl/#test-DateTimeFormat_accepts_IANA_timezone_names
- (DateTimeFormat の詳細を開けて確認してください)
- IE 11 でも
- いつタイムゾーンを気にすべきか
import { DateTime, Settings } from "luxon"; Settings.defaultZoneName = 'Asia/Tokyo'; /** * 生成 */ console.log(DateTime.fromISO("2019-10-05T14:00:00").valueOf()); // => 1570251600000 console.log(DateTime.fromISO("2019-10-05T14:00:00", { zone: "America/Los_Angeles" }).valueOf()); // => 1570309200000 const tokyo = DateTime.fromISO("2019-10-05T14:00:00"); /** * 取得, 変更 */ console.log(tokyo.hour); // => 14 console.log(tokyo.setZone("America/Los_Angeles").hour); // =>22 console.log(tokyo.startOf('day')); // `startOf("day")` で「日」の開始に変更 console.log(tokyo.startOf('day').valueOf()); // => 2019-10-05T00:00:00.000+09:00 // => 1570201200000 console.log(tokyo.setZone("America/Los_Angeles").startOf('day')); console.log(tokyo.setZone("America/Los_Angeles").startOf('day').valueOf()); // => 2019-10-04T00:00:00.000-07:00 // => 1570172400000 /** * 表示 */ console.log(tokyo.toLocaleString(DateTime.DATETIME_SHORT)); // => 10/5/2019, 2:00 PM console.log(tokyo.setZone("America/Los_Angeles").toLocaleString(DateTime.DATETIME_SHORT)); // => 10/4/2019, 10:00 PM
Nuxt のモジュールをテストする
以下のような構成のモジュールをつくったとして、テストをどのように書くことができるか整理します。
. ├── README.md ├── __tests__ │ ├── fixtures │ │ ├── nuxt.config.js │ │ └── pages │ │ └── index.vue │ └── index.js ├── jest.config.js ├── lib │ ├── module.js │ └── plugin.template.js ├── package-lock.json └── package.json
モジュールは「Google Analytics を設定するプラグインを提供するモジュール」として話を進めます。
モジュール本体
実際の機能を提供しているファイルは lib/module.js
, lib/plugin.template.js
です。
// module.js const path = require('path'); module.exports = async function Module(moduleOptions) { const options = { // Default values id: undefined, ...this.options['ga'], // options via nuxt.config.js ...moduleOptions, }; this.addPlugin({ src: path.resolve(__dirname, 'plugin.template.js'), fileName: 'ga.js', options, ssr: false, }); };
// plugin.template.js export default ({ app }) => { const moduleOptions = <%= serialize(options) %>; if (!id) { return; } // Load analytics.js (function(i, s, o, g, r, a, m) { // ... })( window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga' ); // Create a tracker ga('create', id, 'auto'); app.router.afterEach(to => { // Update the tracker ga('set', 'page', to.fullPath); // Send a pageview hit ga('send', 'pageview'); }); };
詳細な説明は省きますが、モジュールオプションか nuxt.config.js
のトップレベルのオプションを利用して GA のトラッキング ID を設定することが可能です。トラッキング ID が設定されない場合は GA のスクリプトがロードされないようになっています。
テストの方針
Nuxt のモジュールは Nuxt がビルドされる時に呼び出されます。そのため、モジュール関連のテストをするには Nuxt をプログラムから利用する必要があります(*)。
今回はテスト用に nuxt.config.js
と pages/index.vue
を用意し、テストコード内では Nuxt インスタンスを作成 → ビルド → サーバ起動 を行い、実行結果が期待したものになるか確認をします。
(*) すべてのケースでこのように Nuxt をプログラムから利用する必要はないと思っています。自分が作成したロジックだけをテストするのであれば、別の方法も有効かもしれません。今回のテストも、プラグインのテストだけなら plugin.template.js
を lodash template でコンパイルするという選択もあります。Nuxt をビルドするテストは時間がかかるという点も忘れない方がいいです。
(WIP) テストファイル
(記事は途中ですが、以下コードは問題なく動作します。現段階だと説明がわかりにくいだけです。)
// __tests__/index.js const { Nuxt, Builder } = require('nuxt'); process.env.PORT = process.env.PORT || 3000; const config = require('./fixtures/nuxt.config'); const url = path => `http://localhost:${process.env.PORT}${path}`; // Nuxt のビルドには時間がかかるので jest のデフォルトタイムを変更 // https://jestjs.io/docs/ja/jest-object#jestsettimeouttimeout jest.setTimeout(60000); describe('ga module', () => { let nuxt; let addTemplate; /** * テスト用に用意した `nuxt.config` を利用して Nuxt インスタンスを生成 * ビルドが完了したらサーバを起動 */ const initNuxt = async config => { if (nuxt) { await clearNuxt(); } // https://ja.nuxtjs.org/api/nuxt#nuxt-%E3%81%AE%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF nuxt = new Nuxt(config); addTemplate = nuxt.moduleContainer.addTemplate = jest.fn( nuxt.moduleContainer.addTemplate ); await new Builder(nuxt).build(); // https://github.com/nuxt/nuxt.js/blob/8786ff731767425bb0c1c72af6d8a8895155acf5/packages/server/src/server.js#L225 await nuxt.server.listen(process.env.PORT); }; /** * Nuxt サーバーをクローズする */ const clearNuxt = async () => { await nuxt.close(); }; /** * addTemplate (jest のモック関数) を利用してテンプレートオプションを取得 */ const getTemplateOptions = () => { const call = addTemplate.mock.calls.find(args => args[0].src.includes('plugin.template.js') ); return call && call[0] && call[0].options; }; afterEach(async () => { await clearNuxt(); }); test('installs ga script and sets trackingId with Nuxt options', async () => { // トップレベルオプションに `ga` 設定を追加 await initNuxt({ ...config, ga: { id: 'WITH_NUXT_OPTIONS', }, }); // https://ja.nuxtjs.org/api/nuxt-render-and-get-window // > Nuxt.js アプリケーションの URL を渡して window を取得します。 // // `__tests__/fixtures/pages/index.vue` がレンダーされます const window = await nuxt.renderAndGetWindow(url('/')); expect(window.ga).toBeDefined(); expect(getTemplateOptions().id).toBe('WITH_NUXT_OPTIONS'); }); });
// __tests__/fixtures/nuxt.config.js const { resolve } = require('path'); module.exports = { srcDir: __dirname, // https://ja.nuxtjs.org/api/configuration-dev // // https://github.com/nuxt/nuxt.js/blob/8786ff731767425bb0c1c72af6d8a8895155acf5/packages/builder/src/builder.js#L57 // dev が true だと監視状態になります。無効にしておいたほうがよさそうです。 dev: false, // @@ はエイリアスで `rootDir` を指しています。 // https://ja.nuxtjs.org/guide/directory-structure/#%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B9-%E5%88%A5%E5%90%8D- modules: ['@@'], };
参考
- https://github.com/nuxt-community/awesome-nuxt#modules
- オフィシャルのモジュールを参考に
- https://ja.nuxtjs.org/api/nuxt
- Nuxt.js をプログラムで使う
nuxt.renderRoute(route, context = {})
nuxt.renderAndGetWindow(url, options = {})
コンポーネント単体テストについてつらつらと
書きます。
何をテストするのか
基本的に、考え方は関数のテストと同じだと思います。何かが入力されると結果が出力される。テストでは、その結果が正しいかを確認します。「入力」と「出力」にフォーカスすることが大切です。
一般的なヒント | Vue Test Utils では以下のように説明しています。
コンポーネントのパブリックインターフェイスを検証するテストを作成し、内部をブラックボックスとして扱うことをお勧めします。単一のテストケースでは、コンポーネントに提供された入力(ユーザーのやり取りやプロパティの変更)によって、期待される出力(結果の描画またはカスタムイベントの出力)が行われることが示されます。
このように考えれば、テストの方針も立てやすくなります。結果、何をテストしているのか理解しやすくなりますし、コードもシンプルになります。内部実装をリファクタしたとしても、テストが壊れることはないです。
なぜ書くのか
なぜテストをするのですか | Vue Test Utils から引用。
単体テストの利点:
(ここに「フレームワークへの理解が深まる」も追加できると思います。)
単体テストのすべきこと:
- 実行が早いこと
- 理解しやすいこと
- 一つの作業だけをテストすること
テストが書きにくいな思ったら、もしかしたら、コンポーネントの設計に問題があるかもしれません。例えば、あるコンポーネントに複数の役割を持たせていたり、必要以上に大きく依存関係が複雑になってたり。
テストを書くことで既存コードに改良の余地があることに気づく、そういったケースもあるかと思います。
入力と出力とは何か
具体的に「入力」と「出力」にはどのようなものがあるのでしょうか。一般的な SPA の Web アプリケーションを例にして考えてみます。
入力:
- プロパティ(親コンポーネントから渡される)
- Vue なら
props
, Angular なら@Input
に相当するもの
- Vue なら
- DOM
- ユーザーによる UI 操作により発生
- クリックアクションやフォームの入力など
- Route
- URL が変更することにより発生
- Vue なら
this.$route
, Angular ならActivatedRoute
- Store
- Flux, Redux のような状態管理を導入している場合、Store の変更もコンポーネントへの入力となる
- Vue (Vuex) なら
this.$store.state
(あるいはthis.$store.getters
), Angular (ngrx) ならthis.store.pipe(select(...)
など
出力:
- イベント(親コンポーネントへ渡す)
- Vue なら
emit
, Angular なら@Output
に相当するもの
- Vue なら
- DOM
- 表示されたテキスト, 要素の On/Off, スタイル, 属性 など
- Route
- Router へのアクション
- Store
- Vue (Vuex) なら
this.$store.{dispatch|commit}
, Angular (ngrx) ならthis.store.dispatch
など
- Vue (Vuex) なら
これ以外にもあるとは思いますが、大体このようなものではないでしょうか。
テストを書く前に調べること
初めてさわるフレームワークでコンポーネントの単体テストを書く場合、上にあげたような「入力」をシュミレートする方法と「出力」を検証する方法を知っておくと、順調にテストが書けると思います。
あとは非同期の扱いや、テストダブル(スタブ・モック・スパイなど)の扱いについて知っておくといいかと。(これらに関しては、利用するテストフレームワーク側の機能を利用することも多いです。)
一般的なコンポーネントのテストシナリオ
Angular ドキュメントには Component Test Scenarios というセクションがあり、一般的なテストシナリオの実装方法について解説しています。「一般的なテストシナリオにはどんなものがあるのか」について知るという点で、Angular ユーザー以外にも参考になると思います。
以下は抜粋です。
- Component binding
- DOM の操作 / 評価
- Component with a dependency
- 依存関係のあるコンポーネント
- Component with async service
- 非同期処理を扱うサービスを利用しているコンポーネント
- Component with inputs and outputs
- Routing component
- Router を扱うコンポーネント
- Routed components
- ルート設定されているコンポーネント
- Nested component tests
- Components with RouterLink
- Vue の場合
<nuxt-link :to="...">
,<router-link :to="...">
を使うコンポーネント
- Vue の場合
- Use a page object
- Page オブジェクトを利用して要素操作に関するロジックを分離する
Vue 単体テストのサンプル
テストシナリオを踏まえて、Vue の単体テストで必要そうなケースをいくつかピックアップしたのがこちらです。(若干中途半端ではありますが)
props
と emit
のテスト
describe("Button", () => { it(`should emit a 'click' event`, () => { const wrapper = shallowMount(Button, { propsData: { text: "text" } }); wrapper.find("button").trigger("click"); expect(wrapper.emitted().click).toBeTruthy(); }); });
nuxt-link
(router-link
) を利用しているコンポーネントのテスト
NuxtLink
(RouterLink
) をスタブします。
describe("Breadcrumbs", () => { it("should have nuxt-link correspond to props", () => { const breadcrumbs = [{ label: "Foo", path: "foo" }]; const wrapper = shallowMount(Breadcrumbs, { propsData: { breadcrumbs }, stubs: { NuxtLink: RouterLinkStub } }); expect(wrapper.find(RouterLinkStub).props().to).toBe(breadcrumbs[0].path); }); });
Router を利用しているコンポーネントのテスト
createLocalVue
を利用してグローバルの Vue クラスを汚染しないローカルの Vue クラスで VueRouter
を利用します。
import { createLocalVue, shallowMount } from "@vue/test-utils"; import VueRouter from "vue-router"; describe("Menu", () => { it(`should have '-current' class if the path is same as the current route`, () => { const localVue = createLocalVue(); localVue.use(VueRouter); const router = new VueRouter(); router.push("/foo"); const wrapper = shallowMount(Menu, { localVue, router, propsData: { menus: [ { label: "foo", value: "/foo" }, { label: "bar", value: "/bar" } ] }, stubs: { NuxtLink: RouterLinkStub } }); const fooLink = wrapper.findAll(RouterLinkStub).at(0); const barLink = wrapper.findAll(RouterLinkStub).at(1); expect(fooLink.classes()).toContain("-current"); expect(barLink.classes()).not.toContain("-current"); }); });
内部で他のコンポーネントを利用しているケース
shallowMount() を利用すると、子コンポーネントはスタブされたコンポーネントになります。
メモ:
Nuxt のページコンポーネントのテスト
Nuxt の機能と、テスト対象のコンポーネントの機能をわけて考えることが大切です。
以下のテストでは「fetch
時に適切なアクションがディスパッチされているか」と「適切な middleware
が登録されているか」についてだけテストをしています。初回アクセス時に fetch
が呼ばれているか、パラメータ変更時にも fetch
が呼ばれるか、許可されたユーザーのみアクセスできるか、などのテストはしていません。(それは Nuxt のテストや登録されているミドルウェア自体のテストになるから)
describe("FooPage", () => { it("dispatches FetchFoo before rendering the page", () => { const wrapper = shallowMount(FooPage); const store = { dispatch: jest.fn() }; const fooId = "asdf"; const route = fooRoute(fooId); const context: any = { store, route }; // simulate the nuxt fetch hook wrapper.vm.$options.fetch(context); expect(context.store.dispatch).toHaveBeenCalledWith(FetchFoo({ fooId })); }); it(`needs 'requireAuth' middleware`, () => { const wrapper = shallowMount(FooPage); expect(wrapper.vm.$options.middleware).toContain("requireAuth"); }); }); const fooRoute = fooId => ({ params: { fooId } });
ミューテーション単体のテスト
// from https://vuex.vuejs.org/ja/guide/testing.html describe("mutations", () => { it("INCREMENT", () => { // ステートのモック const state = { count: 0 }; // ミューテーションを適用する increment(state); // 結果を検証する expect(state.count).to.equal(1); }); });
アクション単体のテスト
アクションは外部の API を呼び出す場合が多いです。そのため、モック関数を作成したり、非同期処理をうまく扱う必要があります。
import { FetchFoo, ReceiveFoo, ResetShopStaffs } from "./types"; import { actions } from "../actions"; describe("actions", () => { let store; beforeEach(() => { // inject plugins store = { $api: { request: jest.fn() } }; }); it("FetchFoo request Foo and then commit ReceiveFoo", async () => { // fake data const fakeRequest = "request"; const fakeResponse = "response"; // mock functions const commit = jest.fn(); store.$api.request.mockReturnValue(fakeResponse); // dispach const action = FetchFoo(fakeRequest); actions[action.type].bind(store)({ commit }, action); // request expect(store.$api.request).toHaveBeenCalledWith(fakeRequest); await resolvePromises(); expect(commit).toHaveBeenCalledWith(ReceiveFoo(fakeResponse)); }); }); export const resolvePromises = () => new Promise(resolve => setImmediate(resolve));
ストアのテスト(アクション、ミューテーション など全てを含む)
// from https://vue-test-utils.vuejs.org/ja/guides/using-with-vuex.html import { createLocalVue } from "@vue/test-utils"; import Vuex from "vuex"; import storeConfig from "./store-config"; import { cloneDeep } from "lodash"; test("increments count value when increment is commited", () => { const localVue = createLocalVue(); localVue.use(Vuex); const store = new Vuex.Store(cloneDeep(storeConfig)); expect(store.state.count).toBe(0); store.commit("increment"); expect(store.state.count).toBe(1); }); test("updates evenOrOdd getter when increment is commited", () => { const localVue = createLocalVue(); localVue.use(Vuex); const store = new Vuex.Store(cloneDeep(storeConfig)); expect(store.getters.evenOrOdd).toBe("even"); store.commit("increment"); expect(store.getters.evenOrOdd).toBe("odd"); });
補足
グローバルプロパティをスタブする:
const $route = { path: "http://www.example-path.com" }; const wrapper = shallowMount(Foo, { mocks: { $route } });
https://vue-test-utils.vuejs.org/ja/api/options.html#mocks
まとまらない何か
テストに関係する Tips とかメモとか。
flush-promises
(setImmediate
) を利用して非同期を解決させる
Vue でグローバル登録されてるフィルタが存在する場合
import Vue from "vue"; import * as filters from "./util/filters"; // We would extract this to a function that would be reused by both app.js and jest-setup but, // we didn't want to change original production code Object.keys(filters).forEach(key => { Vue.filter(key, filters[key]); });
How do we test these globally registered filters? https://github.com/agualis/vue-hackernews-2.0/blob/master/src/jest-setup.js
同年、同月、同日にテストすることを願わん
momentjs - How do I set a mock date in Jest? - Stack Overflow
Date.now = jest.fn(() => 1487076708000); //14.02.2017`
Jest のカスタムマッチャ
const myMockFn = jest.fn()...; // The mock function was called at least once expect(mockFunc).toBeCalled();
上記のようなカスタムマッチャは .mock
プロパティを検査する方法の糖衣構文
https://jestjs.io/docs/en/mock-functions
jest.spyOn(object, methodName)
jest.fn
と同様の関数を作成する- 引数に与えられた
object[methodName]
へのコールも実装
https://jestjs.io/docs/ja/jest-object#jestspyonobject-methodname
参考 URLs
- Vue
- Jest
- Angular
- その他
- テストダブル - Wikipedia
- テストスタブ, テストスパイ, モックオブジェクト, フェイクオブジェクト, ダミーオブジェクト
- テストダブル - Wikipedia
Vue SSR 環境構築メモ
Vue SSR ガイド, vuejs/vue-hackernews-2.0 を参考に環境構築を試してみた時のメモです。
- サーバサイドで Vue をコンパイルする
- クライアントとサーバのコードをわける (Webpack)
- ルーティング
- プリフェッチを実現
- サーバとクライアント、それぞれをビルドするために個別の Webpack 設定ファイルを用意
- バンドルレンダラ
- クライアントマニフェストを生成
- ビルド設定をもうちょい
サーバサイドで Vue をコンパイルする
https://github.com/ryotah/vue-ssr/compare/step-0...step-1-render-to-string
やっていること:
コード抜粋:
server.js
const Vue = require("vue"); const server = require("express")(); // ステップ 1: レンダラを作成 const renderer = require("vue-server-renderer").createRenderer({ template: require("fs").readFileSync("./index.template.html", "utf-8") }); server.get("*", (req, res) => { // ステップ 2: Vue インスタンスを作成 const app = new Vue({ data: { url: req.url }, template: `<div>The visited URL is: {{ url }}</div>` }); // ステップ 3: Vue インスタンスを HTML に描画 renderer .renderToString(app) .then(html => res.end(html)) .catch(err => { res.status(500).end("Internal Server Error"); }); }); server.listen(8080);
参考:
クライアントとサーバのコードをわける (Webpack)
https://github.com/ryotah/vue-ssr/compare/step-1-render-to-string...step-2-webpack
やっていること:
- クライアントとサーバー両方をバンドルするために webpack を導入
entry-client.js
,entry-server.js
,app.js
を用意entry-*.js
がapp.js
(アプリケーションのユニバーサルエントリー) を読み込む
コード抜粋:
entry-client.js
import { createApp } from "./app"; export default context => { const { app } = createApp(context); return app; };
server.js
// ... const createApp = require("./dist/entry").default; server.get("*", (req, res) => { // ステップ 2: Vue インスタンスを作成 const app = createApp({ url: req.url }); // ステップ 3: Vue インスタンスを HTML に描画 // ... }); server.listen(8080);
webpack.config.js
module.exports = { entry: "./entry-server.js", output: { filename: "entry.js", path: path.resolve(__dirname, "dist"), libraryTarget: "commonjs2" } // ... };
参考:
ルーティング
https://github.com/ryotah/vue-ssr/compare/step-2-webpack...step-3-router
やっていること:
vue-router
を導入entry-server.js
にルーティングロジックを実装
コード抜粋:
entry-server.js
import { createApp } from "./app"; export default context => { return new Promise((resolve, reject) => { const { app, router } = createApp(); // set server-side router's location router.push(context.url); // wait until router has resolved possible async components and hooks router.onReady(() => { const matchedComponents = router.getMatchedComponents(); if (!matchedComponents.length) { reject({ code: 404 }); } resolve(app); }, reject); }); };
参考:
プリフェッチを実現
https://github.com/ryotah/vue-ssr/compare/step-3-router...step-4-store
やっていること:
- サーバサイドとクライアントサイドで同じデータを利用可能にするために
Vuex
を導入 - ルートコンポーネントにプリフェッチ用メソッド
asyncData
を実装 entry-client.js
にサーバからのデータを受け取るstore.replaceState(window.__INITIAL_STATE__)
を追加
コード抜粋:
entry-server.js
// ... router.onReady(() => { const matchedComponents = router.getMatchedComponents(); // ... // call `asyncData()` on all matched route components Promise.all( matchedComponents.map( ({ asyncData }) => asyncData && asyncData({ store, route: router.currentRoute }) ) ) .then(() => { // すべてのプリフェッチのフックが解決されると、ストアには、 // アプリケーションを描画するために必要とされる状態が入っています。 // 状態を context に付随させ、`template` オプションがレンダラに利用されると、 // 状態は自動的にシリアライズされ、HTML 内に `window.__INITIAL_STATE__` として埋め込まれます context.state = store.state; resolve(app); }) .catch(reject); }, reject); // ...
参考:
サーバとクライアント、それぞれをビルドするために個別の Webpack 設定ファイルを用意
https://github.com/ryotah/vue-ssr/compare/step-4-store...step-5-hydration
バンドルレンダラ
https://github.com/ryotah/vue-ssr/compare/step-5-hydration...step-6-bundle-renderer
やっていること:
- バンドルレンダラを利用
- JSON 形式でサーバサイドのコードをバンドル
参考:
クライアントマニフェストを生成
https://github.com/ryotah/vue-ssr/compare/step-6-bundle-renderer...step-7-client-manifest
参考:
ビルド設定をもうちょい
https://github.com/ryotah/vue-ssr/compare/step-7-client-manifest...step-8-build
やっていること:
webpack-dev-middleware
を導入- コードに変更があったら、サーバとクライアント両方をビルドするように設定
コード抜粋:
server.js
const path = require('path'); const express = require('express'); const MemoryFS = require('memory-fs'); const app = express(); const webpack = require('webpack'); let clientManifest; let serverBundle; let renderer; function update() { if (!clientManifest || !serverBundle) { return; } // create a renderer const { createBundleRenderer } = require('vue-server-renderer'); const template = require('fs').readFileSync('./index.template.html', 'utf-8'); renderer = createBundleRenderer(serverBundle, { runInNewContext: false, // recommended template, clientManifest, }); } // dev middleware const clientConfig = require('./build/webpack.client.config.js'); const clientCompiler = webpack({ ...clientConfig, // TODO: mode: 'development', }); const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { publicPath: clientConfig.output.publicPath, }); app.use(devMiddleware); clientCompiler.plugin('done', stats => { try { clientManifest = JSON.parse( devMiddleware.fileSystem.readFileSync( path.join(clientConfig.output.path, 'vue-ssr-client-manifest.json'), 'utf-8' ) ); } catch (e) { console.log(e); } update(); }); // watch and update server renderer const serverConfig = require('./build/webpack.server.config.js'); const serverCompiler = webpack({ ...serverConfig, // TODO: mode: 'development', }); const mfs = new MemoryFS(); serverCompiler.outputFileSystem = mfs; serverCompiler.watch({}, (err, stats) => { try { serverBundle = JSON.parse( mfs.readFileSync( path.join(clientConfig.output.path, 'vue-ssr-server-bundle.json'), 'utf-8' ) ); } catch (e) { console.log(e); } update(); }); // ...
10月メモ・リンク集
10月に調べたことのメモです。
Vue, Nuxt 関連
Migrate to Nuxt 2.0
- ts-loader バージョンアップ(Webpack4 に対応)
lang="postcss"
を削除- https://github.com/nuxt/nuxt.js/issues/3231#issuecomment-381885334
because nuxt has enabled postcss-loader in css by default
参考:
- Release Notes - Nuxt.js
- migrate to nuxt 2.0 by Ataww · Pull Request #55 · nuxt-community/typescript-template · GitHub
Nuxt の options.ignore
を利用して pages
内にコンテナやストアを配置
// nuxt.config.js ignore: [ // ... '**/pages/**/{containers,components}/*' ],
メリット:
- 関係のあるコードを近くに配置する
- pages, containers, store の関係をはっきりさせる
- a page has a store and container(s)
変更前のディレクトリ構成:
. ├── containers │ └── foo │ ├── index.vue (<= コンテナコンポーネント) │ ├── components │ ├── store │ └── utils └── pages └── foo └── index.vue (<= ページコンポーネント)
変更後のディレクトリ構成:
. └── pages └── foo ├── index.vue (<= ページコンポーネント) ├── components ├── containers │ └── index.vue (<= コンテナコンポーネント) ├── store └── utils
https://nuxtjs.org/api/configuration-ignore#the-ignore-property
ページ遷移時に権限確認
- Middleware arguments · Issue #1687 · nuxt/nuxt.js · GitHub
- vue-router の
meta
を利用すると綺麗に実装できる
- vue-router の
// middleware // ... // Get authorizations for matched routes (with children routes too) const authorizationLevels = route.meta.map((meta) => { if (meta.auth && typeof meta.auth.authority !== 'undefined') return meta.auth.authority return 0 }) // Get highest authorization level const highestAuthority = Math.max.apply(null, authorizationLevels) // ...
不要なビルド処理を削減
- API: build プロパティ - Nuxt.js
extend メソッドは一度はサーバーのバンドルのため、一度はクライアントのバンドルのため、つまり二度呼び出されます。
webpack.config.js (実装例):
module.exports = (config, options) => { // Add plugins config.plugins.push( // ... ); // Do not run type checking twice. (This config is called twice, // one time for the server bundle, and one time for the client bundle.) if (options.isServer) { config.plugins.push(new ForkTSCheckerPlugin({ vue: true })); } // ... return config; };
文字コード・正規表現
Unicode Property Escapes
- GitHub - tc39/proposal-regexp-unicode-property-escapes: Proposal to add Unicode property escapes
\p{…}
and\P{…}
to regular expressions in ECMAScript. - JavaScriptのUnicode Property Escapesについての補説
- ECMAScript 2018よりRegExpで使えるようになったUnicodeプロパティーエスケープ (\p{...}, \P{...}) についての補説
なおHiraganaとKatakanaはあるのにKanjiがないと思われるかもしれませんが、Han (Hani) というのが漢字のことです(「漢字」の中国語読みをローマ字化したHanziに由来)。
補足:
文字コードの変換
- Unicode Lookup: convert special characters
- 文字を入力すると Hex (16進数) , HTML コードに変換してくれる
latin
,kana
なども
- マルチバイト文字, fromCharCode, charCodeAt, Unicode - ryotah’s blog
'>'.charCodeAt(0).toString(16) => '3e'
,String.fromCharCode(0x3e) => '>'
- JavaScriptでのサロゲートペア文字列のメモ - Qiita
['𩸽'.charCodeAt(0), '𩸽'.charCodeAt(1)].map(num => num.toString(16)); // => ["d867", "de3d"]
パスワード向け正規表現
例: 数字・アルファベット・記号を許容する
- 記号とは
- アスキー文字ならば可能、と定義した場合
サンプル:
// 数字・小文字・大文字・記号を最低1文字含む, 8文字以上64文字以下 /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!-\/:-@[-`{-~])[!-~]{8,64}$/.test(value);
その他参考:
肯定的先読み
- 言語別:パスワード向けの正規表現 - Qiita
SVG 関連
SVG をアイコンフォントの代替にする
参考記事:
実装例 (Font Awesome):
<svg> <use xlink:href="fa-brands.svg#facebook"></use> </svg>
<?xml version="1.0" encoding="UTF-8"?> <!-- Font Awesome Free 5.5.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --> <svg xmlns="http://www.w3.org/2000/svg" style="display: none;"> // ... <symbol id="facebook" viewBox="0 0 448 512"> <title id="facebook-title">Facebook</title> <path d="..."></path> </symbol> // ... </svg>
https://fontawesome.com/how-to-use/on-the-web/advanced/svg-sprites
書き出し (WIP):
svgo
で不要タグなどを整理. 色情報消去(fill を空にする)svgo *.svg --disable=removeTitle --config='{ "plugins": [ { "removeAttrs": { "attrs": "fill" } } ], "floatPrecision": xx }'
<symbol id="xxx" viewBox="0 0 xx xx">
を追加 (単体ファイルにする? or SVG Sprite にする?)- svgo で対応する or 別ツールを利用する
アウトプット例:
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><title>title</title><symbol id="root" viewBox="0 0 24 24"><path d="..." fill-rule="evenodd"/></symbol></svg>
関連情報
SVG 基礎知識:
- SVG 要素リファレンス - SVG: Scalable Vector Graphics | MDN
- 構造的要素
<defs>, <g>, <svg>, <symbol>, <use>
- 構造的要素
- svg 要素の基本的な使い方まとめ
defs要素は各種定義情報(テンプレートとなる図形やグラデーション等)を格納する.プログラムにおけるサブルーチンを記述する場所に相当する.
use要素はxlink:href属性に設定した要素の内容を元に新しい図形を描画することを示す.
g要素が図形の論理的なひとまとまりを表すのに対し,symbol要素はviewBoxを定義し新たな図形を定義する点が異なる.
- SVGのviewBoxをわかりやすく紐解く | Tech Blog | 株式会社INDETAIL - インディテール
ツール・書き出し:
- GitHub - svg/svgo: Node.js tool for optimizing SVG files
- GitHub - svgstore/svgstore: Combines multiple svg files into one.
- GitHub - jkphl/svg-sprite: SVG sprites & stacks galore — A low-level Node.js module that takes a bunch of SVG files, optimizes them and bakes them into SVG sprites of several types along with suitable stylesheet resources (e.g. CSS, Sass, LESS, Stylus, etc.)
- Workflow for creating SVG sprites with NPM scripts – Oleg Varaksin – Medium
- Preparing and Exporting SVG Icons in Sketch – Design + Sketch – Medium
- Sketchでアイコンを作成 / 書き出しするときの Tips
その他:
JIRA 芸
JIRA 使い始めました。
Form Events
- input - Event reference | MDN
- change - Event reference | MDN
For input elements with type=checkbox or type=radio, the input event should fire when a user toggles the control (via touch, mouse or keyboard) per the HTML5 specification, but historically, this has not been the case.
- html-form-event - StackBlitz
- サンプル