├── .gitignore ├── .vscode └── setting.json ├── README.md ├── _app.tsx ├── amdxg.config.js ├── config └── rollup.config.js ├── decls.d.ts ├── docs ├── 202005182044-awesome-next-issg.mdx ├── 202005271609-react-app-context.mdx ├── 202005280125-eval-esm-in-iframe.mdx ├── 202006061823-tiny-bundler.mdx ├── 202006091517-todays-react-jsonschema-form.mdx ├── 202006101314-switch-tsconfig-on-webpack.mdx ├── 202006161259-try-netlify-lambda.mdx ├── 202006161500-netlify-lambda-chromium.mdx ├── 202006210107-avoid-ie-first.mdx ├── 202006211925-support-ogp.mdx ├── amdx-0.8.mdx ├── amp-social-share-for-hatena-bookmark.mdx ├── hello-deno-1.mdx ├── mdxx-0.6.mdx ├── mdxx-0.7.mdx ├── mdxx-cli-and-components.mdx ├── mdxx-introduction.mdx ├── next-amp-tailwind-postcss.mdx ├── study-next-amp-by-mdxx-ssg.mdx └── study-recoil.mdx ├── docs_wip ├── 202005200249-engineer-career-pattern.mdx ├── 202005212041-javascript-for-starter.mdx └── age-of-next-js.mdx ├── fragments └── nextjs │ ├── 00.mdx │ ├── 01.mdx │ ├── 02.mdx │ ├── 03.mdx │ ├── 04.mdx │ ├── 05.mdx │ ├── 10.mdx │ └── 11.mdx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── [slug].tsx ├── _document.tsx ├── index.tsx ├── slides │ └── develop-mizchi-dev.tsx ├── tags │ ├── [tag].tsx │ └── index.tsx └── try.tsx ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon.ico ├── icon-16x16.png ├── icon-32x32.png ├── install-sw.html ├── manifest.json ├── ogp │ ├── 202005182044-awesome-next-issg.png │ ├── 202005271609-react-app-context.png │ ├── 202005280125-eval-esm-in-iframe.png │ ├── 202006061823-tiny-bundler.png │ ├── 202006091517-todays-react-jsonschema-form.png │ ├── 202006101314-switch-tsconfig-on-webpack.png │ ├── 202006161259-try-netlify-lambda.png │ ├── 202006161500-netlify-lambda-chromium.png │ ├── 202006210107-avoid-ie-first.png │ ├── amdx-0.8.png │ ├── amp-social-share-for-hatena-bookmark.png │ ├── hello-deno-1.png │ ├── mdxx-0.6.png │ ├── mdxx-0.7.png │ ├── mdxx-cli-and-components.png │ ├── mdxx-introduction.png │ ├── next-amp-tailwind-postcss.png │ ├── study-next-amp-by-mdxx-ssg.png │ └── study-recoil.png └── sw.js ├── script ├── counter.tsx └── generate-ogp.mjs ├── slides └── develop-mizchi-dev.mdx ├── text.png ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/55df35ee63aef4a6f859559af980c9fb87bee1a1/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | out 112 | .netlify 113 | gen 114 | public/amp-script -------------------------------------------------------------------------------- /.vscode/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.wordWrap": "on" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MDXX SSG 2 | 3 | ## Features 4 | 5 | - Build amdx on next ssg 6 | - Support AMP 7 | 8 | ## Create your own blog 9 | 10 | ```bash 11 | # install node and npm 12 | npx degit mizchi/amdx/packages/ssg blog 13 | cd blog 14 | npm install 15 | cp amdxg.config.example amdxg.config 16 | # edit amdxg.config for you 17 | 18 | # create new page 19 | npm run new-page 20 | # edit docs/[current-date].mdx 21 | 22 | # create new page with slug 23 | npm run new-page foo 24 | # edit docs/foo.mdx 25 | 26 | # Preview 27 | npm run dev 28 | 29 | # Deploy 30 | npm run build 31 | npm run deploy 32 | ``` 33 | -------------------------------------------------------------------------------- /_app.tsx: -------------------------------------------------------------------------------- 1 | export function MyApp({ Component, pageProps }) { 2 | return ; 3 | } 4 | -------------------------------------------------------------------------------- /amdxg.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useOgp: true, 3 | host: "https://mizchi.dev", 4 | siteName: "mizdev", 5 | author: "mizchi", 6 | authorLink: "https://twitter.com/mizchi", 7 | lang: "ja", 8 | gtag: "UA-165420141-1", 9 | repository: "mizchi/amdx", 10 | socialShare: { 11 | twitter: true, 12 | hatena: true, 13 | facebook: true, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import nodeResolve from "@rollup/plugin-node-resolve"; 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | import virtual from "@rollup/plugin-virtual"; 6 | import typescript from "rollup-plugin-typescript2"; 7 | import builtins from "rollup-plugin-node-builtins"; 8 | import { amdx } from "rollup-plugin-amdx"; 9 | import { terser } from "rollup-plugin-terser"; 10 | 11 | const plugins = [ 12 | builtins(), 13 | typescript({ 14 | tsconfigOverride: { 15 | compilerOptions: { 16 | jsx: "react", 17 | target: "es2017", 18 | module: "esnext", 19 | jsx: "react", 20 | noEmit: true, 21 | esModuleInterop: true, 22 | moduleResolution: "node", 23 | resolveJsonModule: true, 24 | isolatedModules: true, 25 | }, 26 | }, 27 | }), 28 | nodeResolve(), 29 | commonjs(), 30 | amdx(), 31 | terser({ 32 | module: true, 33 | }), 34 | ]; 35 | 36 | const RUN_TEMPLATE = (entryPath) => ` 37 | import { h, render } from "preact"; 38 | import Entry from "${entryPath}"; 39 | const root = document.querySelector(".root"); 40 | const encoded = root.id; 41 | const props = encoded ? JSON.parse(atob(encoded)) : {}; 42 | render(h(Entry, props), root); 43 | `; 44 | 45 | const SSR_TEMPLATE = (entryPath) => ` 46 | import { h } from "preact"; 47 | import renderToString from "preact-render-to-string"; 48 | import Entry from "${entryPath}"; 49 | export default (props) => renderToString(h(Entry, props)); 50 | `; 51 | 52 | const targetDir = path.join(__dirname, "../script"); 53 | const scriptNames = fs.readdirSync(targetDir); 54 | 55 | const config = scriptNames.map((name) => { 56 | const input = path.join(targetDir, name); 57 | const base = name.replace(".tsx", ""); 58 | return [ 59 | { 60 | input: `_$_${base}_run.js`, 61 | plugins: [ 62 | virtual({ [`_$_${base}_run.js`]: RUN_TEMPLATE(input) }), 63 | ...plugins, 64 | ], 65 | output: { 66 | file: `public/amp-script/${base}/run.js`, 67 | format: "iife", 68 | }, 69 | }, 70 | { 71 | input: [`_$_${base}_ssr.js`], 72 | plugins: [ 73 | virtual({ [`_$_${base}_ssr.js`]: SSR_TEMPLATE(input) }), 74 | ...plugins, 75 | ], 76 | output: { 77 | file: `public/amp-script/${base}/ssr.js`, 78 | format: "esm", 79 | }, 80 | }, 81 | ]; 82 | }); 83 | 84 | export default config.flat(); 85 | -------------------------------------------------------------------------------- /decls.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.mdx"; 2 | declare module "*.css"; 3 | 4 | import "react"; 5 | declare module "react" { 6 | interface StyleHTMLAttributes extends React.HTMLAttributes { 7 | jsx?: boolean; 8 | global?: boolean; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/202005182044-awesome-next-issg.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Incremental Static Regeneration で実現する次世代のサーバーアーキテクチャ 3 | created: 1589802285779 4 | tags: [next, react, ssg] 5 | --- 6 | 7 | next.js 9.4 に Incremental Static Regeneration という実験的な新機能があります。 8 | 9 | [Blog \- Next\.js 9\.4 \| Next\.js](https://nextjs.org/blog/next-9-4) 10 | 11 | パッと見、「段階的な静的サイト生成…?なんのことだろう…」となったのですが、手元で試してみた感じ、これが既存のサーバーの実装アプローチを変える、革命的な機能ではないかと思いました。 12 | 13 | 解説を書きつつ、どのような応用があるか解説します。 14 | 15 | ## next.js の Incremental SSG を試してみる 16 | 17 | リポジトリはここです。 [mizchi/issg\-playground](https://github.com/mizchi/issg-playground) 18 | 19 | 解説にあたっては、必要なのはほぼこのファイルだけで、短いのでそのまま貼ります。 20 | 21 | ```tsx 22 | // pages/[slug].tsx 23 | import { GetStaticProps, GetStaticPaths } from "next"; 24 | 25 | type Props = { 26 | slug: string; 27 | builtAt: number; 28 | }; 29 | 30 | export const getStaticProps: GetStaticProps = async (ctx) => { 31 | return { 32 | props: { 33 | slug: ctx.params.slug as string, 34 | builtAt: Date.now(), 35 | }, 36 | unstable_revalidate: 30, 37 | }; 38 | }; 39 | 40 | export const getStaticPaths: GetStaticPaths = async () => { 41 | return { 42 | paths: ["/foo"], 43 | fallback: true, 44 | }; 45 | }; 46 | 47 | export default (props: Props) => { 48 | return ( 49 | <> 50 | {props.slug}: {props.builtAt} 51 | > 52 | ); 53 | }; 54 | ``` 55 | 56 | - `pages/[slug].tsx` は `/*` をハンドルします。 57 | - `export const getStaticProps` はそのルーティングに来たときのルート要素に渡す props を組み立てます 58 | - `export const getStaticPaths` はそのルーティングに来たときの、パス一覧を返却します。 59 | 60 | これらの機能は元々 `next export` の静的アセットの吐き出しのために、手元のビルド時に一回だけ実行されるものでした。(このブログも、この機能を使って生成されています) 61 | 62 | しかし、 Incremental SSG では、このサーバーは静的に吐き出してデプロイされるのではなく、 server or serverless モードでデプロイすることを想定されています。 63 | 静的アセットの吐き出しサーバーを、動的なサーバーとしてデプロイする、とはどういうことでしょうか。 64 | 65 | ここで、 `unstable_revalidate: 30` と `fallback: true` に注目してください。 66 | 67 | - `getStaticPaths` で `fallback: true` が指定されると、 `paths` で指定されなかったパスも、 `getStaticProps` のロジックに応じて組み立てられます。 68 | - `getStaticProps` で `unstable_revalidate: 30` のような値を返すと、 30 秒間は静的アセットとして返却されます 69 | 70 | ここからが面白くて 71 | 72 | - unstable_revalidate: 30` の秒数が経過後、次のリクエストが発生した際に、一旦はキャッシュを返しつつ、バックグラウンドでもう一度そのページを構築 73 | 74 | この挙動が面白いですね。つまりは stale-while-revalidate の挙動です。 75 | 76 | つまりは、静的サイトジェネレータとしてある程度の運用の容易さを担保しつつ、CDN のスケーラビリティを借りて、かつ、ある程度は動的な振る舞いを取れる、ということです。 77 | 78 | [https://try-issg.now.sh/bar](https://try-issg.now.sh/bar) にデプロイしてあります。この挙動を念頭に起きながら、アクセスしてみてください。 79 | 80 | ## フロントエンドベストプラクティスの実現 81 | 82 | 自分は [光を超えるためのフロントエンドアーキテクチャ \- Speaker Deck](https://speakerdeck.com/mizchi/guang-wochao-erutamefalsehurontoendoakitekutiya) という発表をしたことがあります。要約すると、パフォーマンス最適化のためには、リクエストを全部アプリケーション・サーバーに到達させてはだめで、 CDN Edge に置いた HTML に当てつつ、キャッシュごとにサロゲートキーを当てて、リソースの更新のたびにプログラマブルなインバリデーションを発行する、というものです。 83 | 84 | 当時、これを実現できるのは fastly しかありませんでした。これからは vercel も似たようなことが可能なります。 85 | 86 | まだプログラマブルなインバリデーションはないのですが、RFC のディスカッションを読む限りは、開発者の rauchg と Timer 曰く、もっと多機能なものも考えているらしいので、 それを想定してるように見えます。 87 | 88 | [RFC: Incremental Static Regeneration · Discussion \#11552 · zeit/next\.js](https://github.com/zeit/next.js/discussions/11552) 89 | 90 | ## Vercel / SmartCDN / Next.js のゴールが見えた 91 | 92 | 正直なところ、 next.js が静的 export に対応した当時は、プロダクトとしての軸がぶれているように感じました。SSR のフレームワークでいきたいのか、 SSG になりたいのか、どっちなのかと。そして自前の PaaS を運用しているのは、よくわからないところがありました。 93 | 94 | Vercel (旧名 now.sh) は SmartCDN という機能があります。これはおそらく、この機能を見据えたプログラマブルな CDN として設計されたものだったのでしょう。 95 | 96 | Incremental SSG は、NoCode や Headless CMS のガワとして、next.js を使うことが想定されています。これらの NoCode Backend はお世辞にもスケーラビリティがあるとは言えないものが多く、またレスポンスタイムに難があることが多かったのですが、Incremental SSG モードの Next.js をかぶせることで(初回アクセスをやや犠牲にしつつも) CDN のスケーラビリティの恩恵を受けることができます。 97 | 98 | おそらく Vercel + SmartCDN は、next に最適化された CMS バックエンドとして、オールインワンパッケージを提供するのがゴールなのでしょう。 99 | 100 | ## next.js に投資したいと思った 101 | 102 | とりあえず RFC に fastly の SurrogateKeys 相当のキャッシュタグみたいなものがほしい!とだけ書いておきました。 103 | 104 | [RFC: Incremental Static Regeneration · Discussion \#11552 · zeit/next\.js](https://github.com/zeit/next.js/discussions/11552#discussioncomment-14415) 105 | 106 | また、ZEIT, あらため Vercel は 20 億円の増資を受けたことで、next.js エコシステムの発展は、より加速していくように思います。 107 | 108 | [\(20\) Shu Uesugi さんは Twitter を使っています 「🆕Next\.js の開発元でもある ZEIT はこのたび社名変更し Vercel になりました。 🎉2100 万ドルの資金調達も発表。 👨🏻💻 私はご縁があり 2 月に開発者としてジョインしました。 🤔「○○ は今後どうなるの?」というご質問につきましては、こちらの Notion ドキュメントをご一読ください!→ https://t\.co/eCwc23gIzo」 / Twitter](https://twitter.com/chibicode/status/1252745903540105216) 109 | 110 | ちょっと前まで、next.js は意見が強いフレームワークで、正直 nuxt のほうが使いやすいよなぁ、と思ってたんですが、こういう感じで攻めてくるのは以外で、びっくりしつつも、応援したい気持ちがありますね。 111 | -------------------------------------------------------------------------------- /docs/202005271609-react-app-context.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: React Context を用いた簡易 Store 3 | created: 1590563363484 4 | tags: [react, flux] 5 | --- 6 | 7 | ## 課題 8 | 9 | - redux を引っ張り出すと大仰になる。Context 下に共有ステートを持ってそこに setState できるだけでよい。 10 | - なので、次の 2 つを用意する 11 | - 現在の state を参照する `const appState = useAppState()` 12 | - 現在の state を更新する関数を返す `const setAppState = useSetAppState()` 13 | - `React.useState()` と違って分割している理由は、主にパフォーマンス上の理由 14 | - 大域な参照なので、可能な限りステートを参照したくない 15 | - `setState()` の API は `(prevState: State) => State` も取れるので、状態更新用途に限ってはそもそも `useAppState()` せずに済むことが多い 16 | - でも毎回書いてるけどボイラープレート感強い上に忘れるのでここにメモする 17 | 18 | ## 毎回書いてるボイラープレート 19 | 20 | ```tsx 21 | // src/contexts/AppStateContext.tsx 22 | import React, { Dispatch, SetStateAction, useContext, useState } from "react"; 23 | 24 | export type AppState = { 25 | value: number; 26 | }; 27 | 28 | const initialState: AppState = { 29 | value: 0, 30 | }; 31 | 32 | const AppStateContext = React.createContext(initialState); 33 | const SetAppStateContext = React.createContext< 34 | Dispatch> 35 | >(() => {}); 36 | 37 | export function useAppState() { 38 | return useContext(AppStateContext); 39 | } 40 | export function useSetAppState() { 41 | return useContext(SetAppStateContext); 42 | } 43 | 44 | export function AppStateProvider(props: { 45 | initialState?: AppState; 46 | children: React.ReactNode; 47 | }) { 48 | const [state, setState] = useState( 49 | props.initialState ?? initialState 50 | ); 51 | return ( 52 | 53 | 54 | {props.children} 55 | 56 | 57 | ); 58 | } 59 | ``` 60 | 61 | ## 使い方 62 | 63 | ```tsx 64 | import React from "react"; 65 | import ReactDOM from "react-dom"; 66 | import { 67 | AppStateProvider, 68 | useAppState, 69 | useSetAppState, 70 | } from "./contexts/AppStateContext"; 71 | 72 | function Counter() { 73 | const state = useAppState(); 74 | const setAppState = useSetAppState(); 75 | return ( 76 | 77 | {state.value} 78 | { 80 | setAppState((s) => ({ value: s.value + 1 })); 81 | }} 82 | > 83 | ++ 84 | 85 | 86 | ); 87 | } 88 | 89 | function App() { 90 | return ( 91 | 92 | 93 | 94 | ); 95 | } 96 | 97 | ReactDOM.render(, document.querySelector("#root")); 98 | ``` 99 | -------------------------------------------------------------------------------- /docs/202005280125-eval-esm-in-iframe.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: iframe の中で ES Modules のコードを評価する 3 | created: 1590596703469 4 | tags: [] 5 | --- 6 | 7 | rollup のような ESM を吐くツールのプレビューツールを作っていると、文字列として組み立てたコードを安全に評価したいことがあります。 8 | 9 | ## 方法 10 | 11 | - iframe を createObjectURL 12 | - dynamic import で data uri として文字列を評価 13 | - 得られた関数を実行 14 | 15 | ## コード 16 | 17 | ```ts 18 | // これを評価したい 19 | const code = `export default () => { console.log("xxx") }`; 20 | 21 | const encoded = btoa(code); 22 | const blob = new Blob( 23 | [ 24 | ` 25 | 26 | 27 | 28 | 29 | 32 | 33 | `, 34 | ], 35 | { type: "text/html" } 36 | ); 37 | 38 | // const iframe = document.querySelector("iframe"); 39 | iframe.src = URL.createObjectURL(blob); 40 | ``` 41 | 42 | ## 解説: data uri の import 43 | 44 | ESM のコードを script タグの中で評価しても、実行はできても export されたオブジェクトの参照を手に入れることができません。 45 | 46 | なので、 data uri 化されたコードを `import(...)` を経由することで、export オブジェクトを手に入れています。 47 | 48 | ```ts 49 | import("data:text/javascript;base64,${encoded}").then((mod) => mod.default()); 50 | ``` 51 | 52 | これでモジュールが手に入るので、関数として実行しています。 53 | 54 | ## 解説: iframe 55 | 56 | UI スレッドで何度もコードを実行すると、window に起きる副作用の影響を受けてしまうので、実行コンテキストを隔離する目的で iframe を使います。 57 | 58 | HTML を blob 化して `URL.createObjectURL(blob)` で, 何にも紐付かない iframe を作ります。 59 | 60 | ```ts 61 | const blob = new Blob([``], { 62 | type: "text/html", 63 | }); 64 | iframe.src = URL.createObjectURL(blob); 65 | ``` 66 | 67 | 本当にセキュアにやるなら realm などを使ったほうがいいです。 68 | 69 | [tc39/proposal\-realms: ECMAScript Proposal, specs, and reference implementation for Realms](https://github.com/tc39/proposal-realms) 70 | -------------------------------------------------------------------------------- /docs/202006061823-tiny-bundler.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: module bundler を作った 3 | created: 1591435424224 4 | tags: [] 5 | --- 6 | 7 | このフロントエンドの魔境に生まれたからには一度は俺が考えた最強の module bundler を作りたい。みんなそう思ってると思う。作った。 8 | 9 | [mizchi/bundler: hobby bundler](https://github.com/mizchi/bundler) 10 | 11 | ## tldr 12 | 13 | このコードが 14 | 15 | ```ts 16 | // foo.js 17 | export default 1; 18 | 19 | // index.js 20 | import foo from "./foo.js"; 21 | console.log(foo); 22 | export const index = 1; 23 | ``` 24 | 25 | こうなる 26 | 27 | ```ts 28 | // @mizchi/bundler generate 29 | const _$_exported = {}; 30 | const _$_import = (id) => 31 | _$_exported[id] || _$_modules[id]((_$_exported[id] = {})); 32 | const _$_modules = { 33 | "/foo.js": (_$_exports) => { 34 | _$_exports.default = 1; 35 | return _$_exports; 36 | }, 37 | }; 38 | // -- entry -- 39 | 40 | const { default: foo } = _$_import("/foo.js"); 41 | 42 | console.log(foo); 43 | export const index = 1; 44 | ``` 45 | 46 | ## もうちょっと複雑な例: snowpack 47 | 48 | importmap をサポートしているので、このコードが動く。 49 | 50 | ```ts 51 | import { h } from "preact"; 52 | import render from "preact-render-to-string"; 53 | console.log(render(h("div", {}, "hello"))); 54 | ``` 55 | 56 | (実際は importmap を自分で組み立てたりと色々下準備が必要) 57 | 58 | [bundler/examples/with\-snowpack at master · mizchi/bundler](https://github.com/mizchi/bundler/tree/master/examples/with-snowpack) 59 | 60 | ## もうちょっと複雑な例: dynamic import chunk / worker chunk / publicPath 61 | 62 | ```ts 63 | import { Bundler } from "@mizchi/bundler"; 64 | import { format } from "prettier"; // install yourself 65 | const fileMap = { 66 | "/worker.js": "self.onmessage = (ev) => console.log(ev);", 67 | "/foo.js": "export default 1;", 68 | "/index.js": ` 69 | const p = import("./foo.js"); 70 | const worker = new Worker("./worker.js"); 71 | worker.postMessage({hello: "world"}) 72 | `, 73 | }; 74 | const bundler = new Bundler(fileMap); 75 | (async () => { 76 | const chunks = await bundler.bundleChunks("/index.js", { 77 | publicPath: "/dist/", 78 | }); 79 | // console.log(format(code, { parser: "babel" })); 80 | console.log(chunks); 81 | })(); 82 | ``` 83 | 84 | この出力はこうなる。 85 | 86 | ```json 87 | [ 88 | { 89 | "type": "entry", 90 | "entry": "/index.js", 91 | "builtCode": "// @mizchi/bundler generate\nconst _$_exported = {};\nconst _$_import = (id) => _$_exported[id] || _$_modules[id](_$_exported[id] = {});\nconst _$_modules = {};\n\n;\n\n// -- entry --\n\nconst p = import(\"/dist/_$_foo.js\");\nconst worker = new Worker(\"/dist/_$_worker.js\");\nworker.postMessage({\n hello: \"world\"\n});;\n\n" 92 | }, 93 | { 94 | "type": "chunk", 95 | "entry": "/foo.js", 96 | "chunkName": "/dist/_$_foo.js", 97 | "builtCode": "// @mizchi/bundler generate\nconst _$_exported = {};\nconst _$_import = (id) => _$_exported[id] || _$_modules[id](_$_exported[id] = {});\nconst _$_modules = {};\n\n;\n\n// -- entry --\n\nexport default 1;;\n\n" 98 | }, 99 | { 100 | "type": "chunk", 101 | "entry": "/worker.js", 102 | "chunkName": "/dist/_$_worker.js", 103 | "builtCode": "// @mizchi/bundler generate\nconst _$_exported = {};\nconst _$_import = (id) => _$_exported[id] || _$_modules[id](_$_exported[id] = {});\nconst _$_modules = {};\n\n;\n\n// -- entry --\n\nself.onmessage = ev => console.log(ev);;\n\n" 104 | } 105 | ] 106 | ``` 107 | 108 | これを `dist/*` に配置して、`index.html` から呼べば実行できる。そのための `publicPath` 109 | 110 | ## 実装した機能 111 | 112 | - treeshake による未使用コード削除 113 | - dynamic import 向けの chunk splitting 114 | - webworker 用 chunk splitting (webpack の worker-plugin 相当) 115 | - importmap による書き換え 116 | 117 | ## 何故作ったか 118 | 119 | ### 勉強のため 120 | 121 | hiroppy が書いたやつをふんわり読んで、これなら作れそう、と思って作った。 122 | 123 | [module bundler の作り方\(ECMAScript Modules 編\) \- 技術探し](https://blog.hiroppy.me/entry/create-module-bundler-esm) 124 | 125 | 実際はあんまりちゃんと読んでないけど、babel の parser / generator / traverse を使う、 モジュール用テンプレート、実行用テンプレートという発想だけ持ち帰った感はある。 126 | 127 | ビルド用に id 降ったほうがよい、というのだけ無視していて、仮想 FS 内の絶対パスをそのまま使っている。仮想の FS のルートなら環境依存は無いので。 128 | 129 | ### ESM to Bundled ESM 130 | 131 | 複数の ESM を ESM にバンドルするだけの簡易なコンパイラが欲しかった。webpack も rollup も ESM を入力にできるし、 IE が死のうとしている今、現代のブラウザは ESM は当然のように備えているので、バンドル処理はネットワークをまたがずに RTT を減らすためだけのものでしかない。要は、エントリポイントの ESM はそのまま残して、内部の import export だけ書き換えればよい。 132 | 133 | deno を試した感じ、 importmap がありさえすれば `node_modulse` の名前解決はそこまで困らない印象だったので、import-map をサポートして、 node_modules への名前解決を実装するのをやめた。 134 | 135 | ## ブラウザフレンドリー 136 | 137 | node_modules のサポートをやめた理由でもあるんだけど、主にブラウザをターゲットに動くものなので、ブラウザ内で実行しながらプレビューしたい。 138 | 139 | また、ブラウザ内でバンドラを実行するのは、おそらくそこにエディタがあって高頻度にビルドされることが想定されるので、 AST に変換した中間状態を全部保存して、ビルド処理はグラフをなめるだけで終わるようにした。これでプレビュー速度が高速になった。 140 | 141 | WebWorker のスレッドで実行されることを想定している。worker で並列処理をすることも考えたが、後回し。これ使いたい。[developit/web\-worker: Consistent Web Workers in browser and Node\.](https://github.com/developit/web-worker) 142 | 143 | ## Babel(+ typescript-preset) First 144 | 145 | webpack / rollup は内部で acorn を使ってて、これは 90k と軽量で便利ではあるんだけど、最終的にコードの変形では babel や babel のプラグインを使っている事が多い。 複数の AST 定義を跨ぐのが面倒なので、最初から全部 babel とした。 146 | 147 | このコンパイラを webpack でバンドルしたところ、約 840kb。開発者用ツールなら十分なサイズだと思う。(あとで rollup の es 出力にする) 148 | 149 | ### 可読性のある出力 150 | 151 | sourcemap 対応をしない代わりに、比較的可読性がある出力を目指した。sourcemap を使っていても、結局 debugger などで止めた際に変数が書き換えられていると、 見えてるシンボルと実際のものが別物で、あんまり使い物にならない。async をサポートした ES2017 以上のターゲットなら、そもそもほとんどのコードが変換されないので、生で読んだほうが早い。 152 | 153 | ## 完走した感想 154 | 155 | treeshake は簡易なものを実装してみたが正直しんどい。何を持って副作用があるとするかを、とりあえず whiltelist な AST Node のパターンを作って、それを違反しない限りは副作用がないとしているが、真面目にやるとエッジケースが無限にありそう。 156 | 157 | snowpack や rome と思想が競合してる気がする。 158 | 159 | 完走してない。 160 | 161 | ## 今後 162 | 163 | - rollup で esm バンドルする 164 | - typescript support: 165 | - babel/parser のオプションを有効にしているので、parse はできてる気がするが、たぶん出力にも残ってしまっている 166 | - tsx もサポートする 167 | - エディタ 168 | 169 | ## おまけ: Treeshake 無し版のコード 170 | 171 | treeshake や各種の最適化のためにコードが膨れたので、treeshake 実装前の単純だった時のコードをここに貼る。250 行ほど。 172 | 173 | 皆も作ってみよう! 174 | 175 | [bundler](https://gist.github.com/mizchi/368c75fc1088dabfda7dfed726ef9f85) 176 | 177 | ```tsx 178 | // yarn add @babel/parser @babel/traverse @babel/generator @babel/types memfs 179 | // yarn add @types/babel__core @types/node -D 180 | import path from "path"; 181 | 182 | import { parse } from "@babel/parser"; 183 | import traverse from "@babel/traverse"; 184 | import generate from "@babel/generator"; 185 | import * as t from "@babel/types"; 186 | 187 | import type { IPromisesAPI } from "memfs/lib/promises"; 188 | import createFs from "memfs/lib/promises"; 189 | import { vol } from "memfs"; 190 | 191 | // helper 192 | function createMemoryFs(files: { [k: string]: string }): IPromisesAPI { 193 | vol.fromJSON(files, "/"); 194 | return createFs(vol) as IPromisesAPI; 195 | } 196 | 197 | type Module = { 198 | ast: t.File; 199 | filepath: string; 200 | imports: Import[]; 201 | }; 202 | 203 | type Import = { 204 | filepath: string; 205 | }; 206 | 207 | type Output = { 208 | filepath: string; 209 | code: string; 210 | imports: Import[]; 211 | }; 212 | 213 | class Bundler { 214 | public modulesMap = new Map(); 215 | public outModules: Array = []; 216 | fs: IPromisesAPI; 217 | constructor(public files: { [k: string]: string }) { 218 | this.fs = createMemoryFs(files); 219 | } 220 | public async bundle(entry: string) { 221 | await this.addModule(entry); 222 | await this.transform(entry); 223 | return await this.emit("/index.js"); 224 | } 225 | 226 | async addModule(filepath: string) { 227 | if (this.modulesMap.has(filepath)) { 228 | return; 229 | } 230 | const basepath = path.dirname(filepath); 231 | 232 | const code = (await this.fs.readFile(filepath, { 233 | encoding: "utf-8", 234 | })) as string; 235 | 236 | const ast = parse(code, { 237 | sourceFilename: filepath, 238 | sourceType: "module", 239 | }); 240 | 241 | let imports: Import[] = []; 242 | traverse(ast, { 243 | ImportDeclaration(nodePath) { 244 | const target = nodePath.node.source.value; 245 | const absPath = path.join(basepath, target); 246 | imports.push({ 247 | filepath: absPath, 248 | }); 249 | }, 250 | }); 251 | await Promise.all( 252 | imports.map((imp) => { 253 | return this.addModule(imp.filepath); 254 | }) 255 | ); 256 | this.modulesMap.set(filepath, { 257 | filepath, 258 | ast, 259 | imports, 260 | }); 261 | } 262 | 263 | async transform(filepath: string) { 264 | const mod = this.modulesMap.get(filepath)!; 265 | const alreadyIncluded = this.outModules.find( 266 | (m) => m.filepath === filepath 267 | ); 268 | if (alreadyIncluded) { 269 | return; 270 | } 271 | 272 | const basepath = path.dirname(filepath); 273 | 274 | const newImportStmts: t.Statement[] = []; 275 | 276 | traverse(mod.ast, { 277 | ImportDeclaration(nodePath) { 278 | const target = nodePath.node.source.value; 279 | const absPath = path.join(basepath, target); 280 | const names: [string, string][] = []; 281 | nodePath.node.specifiers.forEach((n) => { 282 | if (n.type === "ImportDefaultSpecifier") { 283 | names.push(["default", n.local.name]); 284 | } 285 | if (n.type === "ImportSpecifier") { 286 | names.push([n.imported.name, n.local.name]); 287 | } 288 | if (n.type === "ImportNamespaceSpecifier") { 289 | newImportStmts.push( 290 | t.variableDeclaration("const", [ 291 | t.variableDeclarator( 292 | t.identifier(n.local.name), 293 | t.callExpression(t.identifier("$$import"), [ 294 | t.stringLiteral(absPath), 295 | ]) 296 | ), 297 | ]) 298 | ); 299 | } 300 | }); 301 | 302 | const newNode = t.variableDeclaration("const", [ 303 | t.variableDeclarator( 304 | t.objectPattern( 305 | names.map(([imported, local]) => { 306 | return t.objectProperty( 307 | t.identifier(imported), 308 | t.identifier(local) 309 | ); 310 | }) 311 | ), 312 | t.callExpression(t.identifier("$$import"), [ 313 | t.stringLiteral(absPath), 314 | ]) 315 | ), 316 | ]); 317 | newImportStmts.push(newNode); 318 | nodePath.replaceWith(t.emptyStatement()); 319 | }, 320 | ExportDefaultDeclaration(nodePath) { 321 | const name = "default"; 322 | const right = nodePath.node.declaration as any; 323 | const newNode = t.expressionStatement( 324 | t.assignmentExpression( 325 | "=", 326 | t.memberExpression( 327 | t.identifier("$$exports"), 328 | t.stringLiteral(name), 329 | true 330 | ), 331 | right 332 | ) 333 | ); 334 | nodePath.replaceWith(newNode); 335 | }, 336 | ExportNamedDeclaration(nodePath) { 337 | // TODO: name mapping 338 | // TODO: Export multiple name 339 | if (nodePath.node.declaration) { 340 | const decl = nodePath.node.declaration.declarations[0]; 341 | const name = decl.id.name; 342 | const right = decl.init; 343 | const newNode = t.expressionStatement( 344 | t.assignmentExpression( 345 | "=", 346 | t.memberExpression( 347 | t.identifier("$$exports"), 348 | t.identifier(name) 349 | // true 350 | ), 351 | right 352 | ) 353 | ); 354 | nodePath.replaceWith(newNode); 355 | } else { 356 | // export { a as b } 357 | const exportNames: Array<{ exported: string; imported: string }> = []; 358 | for (const specifier of nodePath.node.specifiers) { 359 | if (specifier.type == "ExportSpecifier") { 360 | exportNames.push({ 361 | exported: specifier.exported.name, 362 | imported: specifier.local.name, 363 | }); 364 | } 365 | } 366 | nodePath.replaceWith( 367 | t.blockStatement( 368 | exportNames.map((exp) => { 369 | return t.expressionStatement( 370 | t.assignmentExpression( 371 | "=", 372 | t.memberExpression( 373 | t.identifier("$$exports"), 374 | t.identifier(exp.exported) 375 | // true 376 | ), 377 | nodePath.node.source 378 | ? t.memberExpression( 379 | t.callExpression(t.identifier("$$import"), [ 380 | t.stringLiteral( 381 | path.join(basepath, nodePath.node.source.value) 382 | ), 383 | ]), 384 | t.identifier(exp.imported) 385 | ) 386 | : t.identifier(exp.imported) 387 | ) 388 | ); 389 | }) 390 | ) 391 | ); 392 | } 393 | }, 394 | }); 395 | 396 | const out = { 397 | ...mod.ast, 398 | program: { 399 | ...mod.ast.program, 400 | body: [...newImportStmts, ...mod.ast.program.body], 401 | }, 402 | }; 403 | const gen = generate(out); 404 | this.outModules.push({ 405 | imports: mod.imports, 406 | filepath: mod.filepath, 407 | code: gen.code, 408 | }); 409 | 410 | await Promise.all( 411 | mod.imports.map((imp) => { 412 | return this.transform(imp.filepath); 413 | }) 414 | ); 415 | } 416 | 417 | async emit(entry: string) { 418 | const entryMod = this.outModules.find((m) => m.filepath === entry); 419 | 420 | const importCodes = this.outModules 421 | .filter((m) => m.filepath !== entry) 422 | .map((m) => { 423 | return `$$import("${m.filepath}");`; 424 | }) 425 | .join("\n"); 426 | 427 | const mods = this.outModules 428 | .filter((m) => m.filepath !== entry) 429 | .map((m) => { 430 | return `"${m.filepath}": ($$exports) => { 431 | ${m.code} 432 | return $$exports; 433 | } 434 | `; 435 | }) 436 | .join(","); 437 | 438 | return `// minibundle generate 439 | const $$exported = {}; 440 | const $$modules = { ${mods} }; 441 | function $$import(id){ 442 | if ($$exported[id]) { 443 | return $$exported[id]; 444 | } 445 | $$exported[id] = {}; 446 | $$modules[id]($$exported[id]); 447 | return $$exported[id]; 448 | } 449 | // evaluate as static module 450 | ${importCodes}; 451 | 452 | // -- runner -- 453 | const $$exports = {}; // dummy 454 | ${entryMod?.code}; 455 | `; 456 | } 457 | } 458 | 459 | // runtime 460 | const files = { 461 | "/bar.js": ` 462 | import foo from "./foo.js"; 463 | console.log("eval bar once") 464 | export default "bar$" + foo 465 | `, 466 | "/foo.js": ` 467 | console.log("eval foo once") 468 | export default "foo$default"; 469 | export const b = "b"; 470 | export const a = "a"; 471 | `, 472 | "/index.js": ` 473 | import * as t from "./foo.js"; 474 | import foo, {a, b as c} from "./foo.js"; 475 | import bar from "./bar.js"; 476 | export const x = c; 477 | export default 1; 478 | console.log(foo, bar); 479 | `, 480 | }; 481 | 482 | // @ts-ignore 483 | import prettier from "prettier"; 484 | async function main() { 485 | const bundler = new Bundler(files); 486 | const built = await bundler.bundle("/index.js"); 487 | console.log(prettier.format(built)); 488 | console.log("--- eval ----"); 489 | eval(built); 490 | } 491 | main(); 492 | ``` 493 | -------------------------------------------------------------------------------- /docs/202006091517-todays-react-jsonschema-form.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: react-jsonschema-form(@rjsf/core) を使う場合は material-ui を使うしかない 3 | created: 1591683426369 4 | tags: [] 5 | --- 6 | 7 | jsonschema から form を生成してくれる react-jsonschema-form というライブラリがある。デモページをみたところ、かなり良く出来てるように感じたので、使ってみた。 8 | 9 | [react\-jsonschema\-form playground](https://rjsf-team.github.io/react-jsonschema-form/) 10 | 11 | だが、いざ使ってみるとかなり混沌とした状況だった。 12 | 13 | ## tldr 14 | 15 | - `@rjsf/core` の出力は bootstrap@4 で壊れている 16 | - bootstrap@3 を避ける場合 `@rjsf/material-ui` を使うしかない 17 | 18 | ## 経緯 19 | 20 | - まず、多くの紹介記事は古く、`react-jsonschema-form` は `@rjsf/core` に名前が変わっている 21 | - bootstrap@3 は jquery 依存が残っているなど、レガシーな設計なので、可能な限り避けたい 22 | - `@rjsf/core` は bootstrap@3 の CSS を前提にした出力をする 23 | - bootstrap@4 では、アイコンで使われた gryphicon が有料化したためドロップされていおり、これを使った `@rjsf/core` の出力が壊れる(ボタンほぼ全部) 24 | - 公式には font-awesome などを使うように推奨されているが、`@rjsf/core` ではライブラリの出力に組み込まれているので、治すのが難しい 25 | - じゃあ `@rjsf/core` が bootstrap4 対応をしているかと言うと、している気配がない 26 | - その代わりに `@rjsf/material-ui` の開発をしている 27 | 28 | ## `@rjsf/material-ui` を使ったコード 29 | 30 | ```tsx 31 | import { IChangeEvent, UiSchema } from "@rjsf/core"; 32 | import JSONSchemaForm from "@rjsf/material-ui"; 33 | import { JSONSchema7 } from "json-schema"; 34 | import React, { useState } from "react"; 35 | import { utils } from "@rjsf/core"; 36 | 37 | const schema = { 38 | type: "object", 39 | properties: { 40 | message: { 41 | type: "string", 42 | default: "hello", 43 | }, 44 | }, 45 | }; 46 | 47 | const defaultState = utils.getDefaultFormState(schema, {}); 48 | 49 | export function Form() { 50 | return ( 51 | ) => { 55 | console.log(data.formData); 56 | }} 57 | /> 58 | ); 59 | } 60 | ``` 61 | 62 | ## お気持ち 63 | 64 | デザインフレームワークを指定するタイプのライブラリは、はっきりいって辛い。 65 | -------------------------------------------------------------------------------- /docs/202006101314-switch-tsconfig-on-webpack.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: webpack + ts-loader で使う tsconfig.json を動的に切り替える 3 | created: 1591762452334 4 | tags: [webpack] 5 | --- 6 | 7 | webpack を使っていると typescript の module 指定は、ESM を treeshake するために `"module": "esnext"` としたい。 8 | 9 | しかし、`ts-node` を前提としたユーティリティスクリプトを作っていると、`"module": "commonjs"` としないと実行に失敗するようになってしまう。`ts-jest` も同様。 10 | 11 | ts-node に関しては、引数で compilerOptions を書き換える方法があり、次のようになる。 12 | 13 | ```bash 14 | yarn ts-node -O '{"module":"commonjs"}' script/x.ts 15 | ``` 16 | 17 | これだと常にこの引数を付けて実行する必要があり、不便。 18 | 19 | ## ts-loader の configFile 20 | 21 | [TypeStrong/ts\-loader: TypeScript loader for webpack](https://github.com/TypeStrong/ts-loader) をちゃんと読むと、`configFile` という引数がある。これで webpack でコンパイル時の tsconfig を切り替えられる。 22 | 23 | (なお、本記事では開発中の ts-loader では型チェックは不要という立場から `transpileOnly: true` を付けているが、必要な場合は外すように) 24 | 25 | デフォルトは ts-node 用に `"module": "commonjs"` としつつ、webpack に渡す設定だけ `"module": "esnext"` としたい。ただし、 `tsconfig.json` は webpack の補完で使われているので、残しておく必要がある。なので、tsconfig.json は起きつつ、それを extends する webpack.tsconfig.js を追加した。 26 | 27 | tsconfig.json 28 | 29 | ```json 30 | { 31 | "compilerOptions": { 32 | "module": "commonjs", 33 | "target": "es2019", 34 | "allowJs": true, 35 | "skipLibCheck": true, 36 | "strict": true, 37 | "forceConsistentCasingInFileNames": true, 38 | "noEmit": true, 39 | "esModuleInterop": true, 40 | "moduleResolution": "node", 41 | "resolveJsonModule": true, 42 | "isolatedModules": true, 43 | "jsx": "react" 44 | } 45 | } 46 | ``` 47 | 48 | webpack.tsconfig.json 49 | 50 | ```json 51 | { 52 | "extends": "./tsconfig.json", 53 | "compilerOptions": { 54 | "module": "esnext" 55 | } 56 | } 57 | ``` 58 | 59 | webpack.config.js 60 | 61 | ```js 62 | const path = require("path"); 63 | const HTMLPlugin = require("html-webpack-plugin"); 64 | const WorkerPlugin = require("worker-plugin"); 65 | 66 | module.exports = { 67 | module: { 68 | rules: [ 69 | { 70 | test: /\.tsx?$/, 71 | use: { 72 | loader: "ts-loader", 73 | options: { 74 | transpileOnly: true, 75 | configFile: "webpack.tsconfig.json", 76 | }, 77 | }, 78 | }, 79 | ], 80 | }, 81 | resolve: { 82 | extensions: [".js", ".ts", ".tsx"], 83 | }, 84 | }; 85 | ``` 86 | 87 | ## 応用: プロダクションでターゲットを切り替える 88 | 89 | 動的に `tsconfig.json` を選択することで、 開発中は `target: es2015` でデバッグしやすくし、production のみ `target: es5` するという方法もある。 90 | 91 | webpack.tsconfig.prod.json を追加 92 | 93 | ```json 94 | { 95 | "extends": "./tsconfig.json", 96 | "compilerOptions": { 97 | "target": "es5", 98 | "module": "esnext" 99 | } 100 | } 101 | ``` 102 | 103 | ```js 104 | // webpack.config.js 105 | const path = require("path"); 106 | const HTMLPlugin = require("html-webpack-plugin"); 107 | const WorkerPlugin = require("worker-plugin"); 108 | 109 | module.exports = (_, argv) => { 110 | return { 111 | module: { 112 | rules: [ 113 | { 114 | test: /\.tsx?$/, 115 | use: { 116 | loader: "ts-loader", 117 | options: { 118 | transpileOnly: true, 119 | configFile: 120 | argv.mode === "production" 121 | ? "webpack.tsconfig.prod.json" 122 | : "webpack.tsconfig.json", 123 | }, 124 | }, 125 | }, 126 | ], 127 | }, 128 | resolve: { 129 | extensions: [".js", ".ts", ".tsx"], 130 | }, 131 | }; 132 | }; 133 | ``` 134 | 135 | これでプロダクションビルド(`webpack --mode production`) で es5 向けにビルドされるようになった。 136 | -------------------------------------------------------------------------------- /docs/202006161259-try-netlify-lambda.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: ローカル環境で netlify lambda のエミュレータを動かす 3 | created: 1592279996411 4 | tags: [netlify] 5 | --- 6 | 7 | netlify は雑に作り捨ての作例を置いておくのに便利でよく使っている。このブログも netlify にドメインを設定して動かしている。 8 | 9 | そして、あんまり知られていないが、netlify には aws lambda が使えて、これは月当たり 12.5 万回ほどの無料枠がある。で、これをローカルで動かすための `netilfy-lambda` がある。 10 | 11 | これがカジュアルに使えたら便利だと思って、一度素振りしておくことにした。 12 | 13 | ## netlify-lambda の問題 14 | 15 | `netlify-lambda` を使ってみたところ、本当に出来が悪い。ランナーとして、というより、開発用の環境設定の読み取りに謎の処理が多く、勝手にルート要素の `webpack.config.js` を読み取って自分用の `webpack.config.js` を生成して勝手に失敗したり、本番では `/.netlify/functions/` で動くのに(これもドキュメントのすごくわかりにくいところに書いてある)、そもそも `netlify.toml` に functions の指定ディレクトリを書かせるのに、ターゲットディレクトリの指定が必要だったりと、とにかく暗黙のユースケースを察する必要がある。 16 | 17 | 公式ドキュメントを読んでも必要な情報がわからない。まず動くサンプルが見当たらず、特定のユースケースのための設定例ばかり引っかかり、普通に動かすのが難しい。 18 | 19 | ## 結局どうやったか 20 | 21 | 動くリポジトリ [mizchi/try-netlify-lambda](https://github.com/mizchi/try-netlify-lambda) 22 | 23 | - 素朴に `netlify.toml` で `functions = "functions"` を指定 24 | - `netlify-lambda build` を一切使わず、webpack の `target: "node"` で `functions/api.js` を生成 25 | - express router で環境ごとのエンドポイントにマウントして、環境差分を吸収 26 | - デプロイ時は一旦クリーンしてから `netlify deploy --prod` する 27 | 28 | ### netlify.toml 29 | 30 | ``` 31 | [build] 32 | command = "yarn webpack --config webpack/functions.config.js" 33 | publish = "dist" 34 | functions = "functions" 35 | ``` 36 | 37 | ### 構成と npm scripts 38 | 39 | 構成 40 | 41 | ``` 42 | netlify.toml 43 | package.json 44 | tsconfig.js 45 | webpack.config.js # netlify-lamba 用のダミー 46 | src/ 47 | ├── front 48 | │ ├── index.html 49 | │ └── index.ts 50 | ├── functions 51 | │ └── api.ts 52 | └── shared 53 | └── getApiRoot.ts 54 | webpack/ 55 | ├── front.config.js 56 | └── functions.config.js 57 | ``` 58 | 59 | package.json 60 | 61 | ```json 62 | "scripts": { 63 | "build": "rimraf functions && rimraf dist && run-s build:*", 64 | "build:fns": "webpack --config webpack/functions.config.js --mode production", 65 | "build:front": "webpack --config webpack/front.config.js --mode production", 66 | "dev": "run-p dev:*", 67 | "dev:front": "webpack-dev-server --config webpack/front.config.js -w", 68 | "dev:fns": "rimraf functions && run-p dev:fns:*", 69 | "dev:fns:webpack": "webpack --config webpack/functions.config.js -w --mode development", 70 | "dev:fns:serve": "netlify-lambda serve .", 71 | "deploy": "yarn build && netlify deploy --prod" 72 | }, 73 | ``` 74 | 75 | (普通に使うだけだったら `dev` と `build`、そして `deploy` だけ叩けば良い) 76 | 77 | 注意点として、`netlify-lambda serve .` は `functions` を見るが、まずルートの `webpack.config.js` を読んで、 `functions/webpack.config.js` を生成してくる(たぶんオートリロード用?)。このディレクトリをこのままデプロイしようとすると、`webpack.config.js` を functions としてデプロイしようとして、デプロイに失敗する。なので、本番用の `build` では rimraf で `functions` を消している。 78 | 79 | なので、netlify-lambda のためのダミーとして、 `module.exports = {}` という空の `webpack.config.js` を置いて、実際に使う webpack.config は `webpack/` ディレクトリに置いた。 80 | 81 | ここの挙動を察するのに 4 時間が溶けたので、恨みがある。 82 | 83 | ### サーバー 84 | 85 | ↑ の設定で、netlify-lambda の開発用のエミュレータは `localhost:9000/`、本番は `/.netlify/functions/` で起動する。 86 | これを express.Router のマウント位置で辻褄を合わせる。 87 | 88 | AWS Lambda では、 node の http ではなく、lambda 独自の http.Request/Response が来るため、 `serverless-http` を通して express に食わせる。 89 | 90 | ```ts 91 | // src/functions/api.ts 92 | 93 | import express from "express"; 94 | import serverless from "serverless-http"; 95 | 96 | const isProd = process.env.NODE === "production"; 97 | 98 | const app = express(); 99 | const router = express.Router(); 100 | 101 | // 開発時には CORS になるので開けておく 102 | router.use((_req, res, next) => { 103 | if (!isProd) { 104 | res.header("Access-Control-Allow-Origin", "*"); 105 | } 106 | next(); 107 | }); 108 | 109 | router.get<{ id: string }>("/:id", (req, res) => { 110 | res.send("ok " + req.params.id); 111 | }); 112 | 113 | const apiRoot = 114 | process.env.NODE_ENV === "production" ? "/.netlify/functions/api" : "/api"; 115 | app.use(apiRoot, router); 116 | 117 | exports.handler = serverless(app); 118 | ``` 119 | 120 | クライアントからも環境変数を見て辻褄を合わせてリクエストする。 121 | 122 | ```ts 123 | const API_ROOT = 124 | process.env.NODE_ENV === "production" 125 | ? "/.netlify/functions" 126 | : "http://localhost:9000"; 127 | 128 | fetch(API_ROOT + "/api/foo").then(() => {...}); 129 | ``` 130 | 131 | ## 結果 132 | 133 | 動いた 134 | 135 | https://heuristic-keller-8c2758.netlify.app/ 136 | -------------------------------------------------------------------------------- /docs/202006161500-netlify-lambda-chromium.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: netlify-lambda で puppeteer を起動する 3 | created: 1592287218672 4 | tags: [netlify, puppeteer] 5 | --- 6 | 7 | [ローカル環境で netlify lambda のエミュレータを動かす \- mizdev](https://mizchi.dev/202006161259-try-netlify-lambda) を読んでることが前提 8 | 9 | ## tldr 10 | 11 | - netlify-lambda は aws lambda なので、AWS 用にビルドされた chrome が使える 12 | - ローカル開発環境では素の puppeteer にフォールバックする 13 | 14 | ## コード 15 | 16 | ``` 17 | yarn add puppeteer chrome-aws-lambda 18 | ``` 19 | 20 | webpack.config.js の `externals` でビルドしないように指定。 21 | 22 | ```js 23 | // webpack.config.js 24 | externals: { 25 | puppeteer: "puppeteer", 26 | "chrome-aws-lambda": "chrome-aws-lambda", 27 | lambdafs: "lambdafs", 28 | }, 29 | ``` 30 | 31 | (`functions/api.js` にビルドしていたのを `functions/api/index.js` にビルドを変えて、 `functions/api/package.json` に依存を記述した。) 32 | 33 | ```ts 34 | // browser.ts 35 | import puppeteer from "puppeteer"; 36 | 37 | const isProd = process.env.NODE === "production"; 38 | 39 | let _browser: null | puppeteer.Browser = null; 40 | 41 | export async function getBrowser(): Promise { 42 | if (_browser) { 43 | return _browser; 44 | } 45 | if (isProd) { 46 | const chromium = require("chrome-aws-lambda"); 47 | const browser = await chromium.puppeteer.launch({ 48 | args: chromium.args, 49 | defaultViewport: chromium.defaultViewport, 50 | executablePath: await chromium.executablePath, 51 | headless: chromium.headless, 52 | ignoreHTTPSErrors: true, 53 | }); 54 | return (_browser = browser); 55 | } 56 | return (_browser = await puppeteer.launch({ 57 | headless: true, 58 | executablePath: 59 | "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", 60 | })); 61 | } 62 | ``` 63 | 64 | このコードを使う例 65 | 66 | ```ts 67 | router.get("/chrome", async (req, res) => { 68 | let result = null; 69 | let page = null; 70 | try { 71 | const browser = await getBrowser(); 72 | page = await browser.newPage(); 73 | await page.goto("https://google.com"); 74 | result = await page.title(); 75 | } catch (error) { 76 | return res.status(400).send(error.message); 77 | } finally { 78 | if (page) { 79 | page.close(); 80 | } 81 | } 82 | res.send(result); 83 | }); 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/202006210107-avoid-ie-first.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: IE First を避ける 3 | created: 1592669248929 4 | tags: [] 5 | --- 6 | 7 | まず、去年の実績として、IE のシェアが 9% から 5% になっています。 8 | 9 | [Browser Market Share Japan \| StatCounter Global Stats](https://gs.statcounter.com/browser-market-share/all/japan) 10 | 11 | 世界だと 1.4% です。これは途上国などでは Android Chrome が支配的だからです。 https://gs.statcounter.com/browser-market-share/all 12 | 13 | 国内で「IE シェア」などでググって出るサイトは 9% と出ていますが、これはデスクトップのみの数字で、モバイルを含めた数字は 5%前後です。日本はさらに Chrome ベースの新 MS Edge の正式リリースがコロナと確定申告の影響で延期されており、リリースのタイミングでさらに減ることが予想されます。 14 | 15 | 去年の実績値でいえば、来年度には 2% 台、再来年には 1%を切っている可能性があります。これは toC なら十分にサポートを外すことを検討できる数値です。フロントエンドでは、シェア 0.5%を切った段階で完全にサポートしないことを検討していいと言われています。 16 | 17 | 主流なデスクトップブラウザが 2~6 ヶ月毎のローリングアップデートを採用していること、モバイル端末はそもそもの買い替えのライフサイクルが短いことから、今後は IE のような十年単位で残るレガシーは出現しづらい環境になりつつあります。 18 | 19 | ## IE サポートをやめるわけではない 20 | 21 | 自分が主張したいのは、即座に IE のサポートをやめろ、という話ではありません。 22 | 23 | 新規に開発するソフトウェアの機能制約や開発体験で、IE の制約を取り払うことで生まれるメリットを考え、それが勝るならば IE をベースラインにせず、サポートを限定的なものにすることを検討する、という話です。 24 | 25 | 長きにわたる IE8,11 のシェアによって、フロントエンドエンジニアは、無意識に IE が可能なことがウェブの限界だ、と無意識に刷り込まれてしまっています。これだと、PWA や新しい発想を受け入れるのに時間が掛かります。例えば、ちょっと前には最新だともてはやされた ES2015 は、今ではビルドターゲットになりえます。 26 | 27 | IE の範囲内では機能要件をサポートし、非機能要件、Polyfill の使用水準や、アニメーションの簡略化や CSS の装飾を簡素なモノにする、といった選択肢を、それが可能ならば優先的に選択する、という話です。そして、そのために IE が犠牲になることは、プロジェクトマネージャ等に最初に合意を取っておく必要があります。 28 | 29 | 界隈に詳しくない人向けに、なぜ、ここまで IE を問題にするか説明すると、IE11 は 6 年間ほど時が止まっており、そこから Chrome Firefox Safari の競争で激しく進化した他のモダンブラウザと歩調を合わせることが困難になりつつあります。JS はツールの発達でそれほどではありませんが、とくに CSS の開発の工数が嵩みます。それは、場合によってはもう一つ同じものをつくるような工数です。 30 | 31 | ## 発展的な未来のために 32 | 33 | Safari はやや遅いといえど、基本的にモダンブラウザの足並みは揃っている現状です。(Chrome だけ独自機能が増えています) 34 | 35 | webpack などの OSS のエコシステムは IE11 はまだサポートされていますが、徐々に IE をサポートから外す動きがあります。 36 | 37 | また、 [Snowpack](https://www.snowpack.dev/) や [vitejs/vite: Native\-ESM powered web dev build tool\. It's fast\.](https://github.com/vitejs/vite) のような、IE を切ることでモダンブラウザの機能をふんだんに使う方向性のツールが出現し始めています。 38 | 39 | JS 周りでは Service Worker, ESM の dynamic import の活用、CSS では `display: grid` が大きいと思っています。 40 | 41 | というわけで、IE をメインラインに据えず、機能要件だけに落としていくことを提案します。 42 | 43 | ## 追記: 具体的な方針 44 | 45 | まずモダンブラウザ向けに作って、その上で IE 用に別ビルドを提供する、といった方法で、モダンブラウザビルドから負債を切り離していくことを自分は推奨しています。それが実行環境/開発環境のパフォーマンスに大きく寄与します。 46 | 47 | リリースされたものは nomodule や IE だけ polyfill.io 通すといったことが可能でしょう。 48 | 49 | - [ES Modules への橋渡しとしての nomodule 属性 \| blog\.jxck\.io](https://blog.jxck.io/entries/2017-06-21/nomodule-attribute.html) 50 | - [Polyfill\.io](https://polyfill.io/v3/) 51 | 52 | `import/export` の ESM を素で使うのはネットワーク RTT を挟むパフォーマンス上の問題があり、SPA でバンドル処理がなくなることは考えにくいです。依存の深さで 回数遅延 xRTT のレイテンシが追加されるからです。 53 | 54 | とはいえ、今後は webpack 一強を脅かすツールが出現することが予想されます。それを受けて webpack 自体も大きな変化を遂げると予想されます。 55 | 56 | フロントエンドでは採用したツールチェインがエコシステムの本体であり、採用するツールチェインの選択がアプリケーションの品質を大きく左右することがあります。 57 | 58 | ## 追記: toB での MS Edge について 59 | 60 | 今年 1 月にサポートの切れた Windows 7 にも 新 MS Edge は配布されています。これは、古い端末でも(最新アップデートを受け取っている必要はありますが) Chromium ベースの Edge が使えて、バックグラウンドでローリングアップデートされるということです。 61 | 62 | これはつまり、古いマシンでも最新 Chromium が起動することが保証されているので、IE のサポートを打ち切って、代わりに Edge での起動を促す画面を出す、みたいな方法が現実的になります。むしろ toB の方が枠組みさえ作れば IE サポートを打ち切りやすい環境があると思っています。 63 | -------------------------------------------------------------------------------- /docs/202006211925-support-ogp.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: OGP対応をした 3 | created: 1592735118538 4 | tags: [amdxg] 5 | --- 6 | 7 | こんな感じ 8 | 9 |  10 | 11 | ## やり方 12 | 13 | - `node-canvas` でタイトルを元にした画像を `public/ogp/[slug].png` に生成 14 | - `` でその出力画像を指定 15 | - 他、`og:title` や `og:url`, `` を追加 16 | 17 | ## 画像生成スクリプト 18 | 19 | 特に理由がないが node@14 の mjs で書いた。 20 | 21 | ```tsx 22 | // script/generate-ogp.mjs 23 | import canvas from "canvas"; 24 | import fs from "fs/promises"; 25 | import path from "path"; 26 | 27 | const W = 600; 28 | const H = 315; 29 | const LINE_HEIGHT = 30; 30 | 31 | function getRows(ctx, text) { 32 | const words = text.split(" "); 33 | 34 | let rows = []; 35 | let currentRow = []; 36 | let tokens = words.slice(0); 37 | let token; 38 | while ((token = tokens.shift())) { 39 | const mText = [...currentRow, token].join(" "); 40 | const measure = ctx.measureText(mText); 41 | if (measure.width <= W) { 42 | currentRow.push(token); 43 | } else { 44 | rows.push(currentRow.slice()); 45 | currentRow = [token]; 46 | } 47 | } 48 | if (currentRow.length > 0) { 49 | rows.push(currentRow); 50 | } 51 | 52 | return rows; 53 | } 54 | 55 | function renderText(ctx, rows) { 56 | const rowCount = rows.length; 57 | for (let i = 0; i < rowCount; i++) { 58 | const rowText = rows[i].join(" "); 59 | const m = ctx.measureText(rowText); 60 | 61 | const w = (W - m.width) / 2; 62 | // const h = (LINE_HEIGHT + 12) * (i + 1); 63 | const h = 40 + 210 / 2 - (LINE_HEIGHT + 12) * (rowCount - i - 1); 64 | 65 | ctx.fillText(rowText, w, h); 66 | } 67 | } 68 | 69 | async function generateImage(text, outputPath) { 70 | const cvs = canvas.createCanvas(W, H); 71 | const ctx = cvs.getContext("2d"); 72 | ctx.font = `${LINE_HEIGHT}px Impact`; 73 | 74 | const rows = getRows(ctx, text); 75 | 76 | ctx.fillStyle = "white"; 77 | ctx.fillRect(0, 0, W, H); 78 | 79 | ctx.fillStyle = "black"; 80 | renderText(ctx, rows); 81 | 82 | const m = ctx.measureText("mizdev"); 83 | ctx.fillText("mizdev", (W - m.width) / 2, 250); 84 | const buf = cvs.toBuffer(); 85 | await fs.writeFile(outputPath, buf); 86 | } 87 | 88 | async function main() { 89 | // const out = path.join(dirname, "../public/ogp/", p.slug + ".png"); 90 | await generateImage("ここにタイトルを入れる", "ogp-image.png"); 91 | console.log("[gen:ogp]", out); 92 | } 93 | 94 | main(); 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/amdx-0.8.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: amdx-0.8 MDXX => AMDX へ 3 | created: 1589389359440 4 | --- 5 | 6 | ## mdxx => amdx 7 | 8 | MDX eXtended => Accelarated MDX 9 | 10 | - `mdxx-ssg` => `amdxg` 11 | - `mdxx-*` => `amdx-*` 12 | - `mdxx-parser` => `amdx` 13 | - `mdxx-compiler` => `amdx-runner` 14 | 15 | mdxx-parser がほぼ全部の変換を担ってたので、リネームついでに `amdx` というライブラリに変更。 16 | 17 | パッケージ名を変えた理由ですが、 mdxx という名前は、MDMA などの合成ドラッグの総称として使われる言葉であんまりイメージがよくなく、また、AMP のフィーチャーが重くなってきたので、A の文字を頭に付けました。 18 | 19 | [Urban Dictionary: mdxx](https://www.urbandictionary.com/define.php?term=mdxx) 20 | 21 | ## 変更 22 | 23 | - `amdxg-cli`: `amdxg new:page [slug]` でのページ生成に、必ず日時プレフィックスがつくようにしました。記事が増えたときの 24 | 25 | ## バグ修正 26 | 27 | - `amdxg-components`: tailwind の導入と purgecss の導入で本来必要な CSS を削ってしまっていたのをもとに戻しました。 28 | 29 | ## 開発中の機能 30 | 31 | ### next.js 9.4 の SSG Auto generation で now 対応 32 | 33 | 現在は `next export` する前提で作っているが、9.4 の段階的 SSG で, now にデプロイできるようにしたい。 34 | 35 | ### `amdxg new:script` 36 | 37 | `amp-script` を使った Component の雛形を追加します。amp-sciprt は 150kb 制限があり、また動的コンポーネントの追加には、SSR 相当の処理が必要です。なので今回は React ではなく preact を使うことにしました。 38 | 39 | あるいは、 markdown で `tsx:amp-script` のようなコードブロックから自動生成することや、wasm を使った処理を考えています。 40 | 41 | ```tsx:amp-script 42 | /** @jsx h */ 43 | import { h } from "preact"; 44 | import { useState } from "preact/hooks"; 45 | 46 | export default function Counter(props: { initialValue: number }) { 47 | const [state, setState] = useState(props.initialValue); 48 | return ( 49 | 50 | Counter Example 51 | {String(state)} 52 | setState((n) => n + 1)}>+ 53 | setState((n) => n - 1)}>- 54 | 55 | ); 56 | } 57 | ``` 58 | 59 | こういうコードブロックを抽出して、rollup でビルドしたコードを生成できそう 60 | -------------------------------------------------------------------------------- /docs/amp-social-share-for-hatena-bookmark.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: amp-social-share で、はてなブックマークのシェアボタンを設置する 3 | created: 1588784852166 4 | tags: [amp] 5 | --- 6 | 7 | amp-social-share は設定済みプロバイダとして twitter/facebook/line などに対応しているが、はてなブックマークはカスタムプロバイダーとして実装できる。 8 | 9 | [Documentation: \- amp\.dev](https://amp.dev/ja/documentation/components/amp-social-share/) 10 | 11 | ## 実装 12 | 13 | AMP 14 | 15 | ```html 16 | 17 | 18 | 22 | ``` 23 | 24 | [参考にした記事](https://ti-tomo-knowledge.hatenablog.com/entry/2018/07/03/202719) では動的 URL 部分で何やら小難しいことをやっているが、実際には AMP の予約キーワードを使って `CANONICAL_URL` に向けるだけでよい。 25 | 26 | [amphtml/amp\-var\-substitutions\.md at master · ampproject/amphtml](https://github.com/ampproject/amphtml/blob/master/spec/amp-var-substitutions.md) 27 | 28 | しかし、これだけだと何もデザインが当たらないので、ドキュメントに従ってカスタムプロバイダーに CSS を当ててみた。 29 | 30 | ```css 31 | amp-social-share[type="hatena_bookmark"] { 32 | display: inline-block; 33 | position: relative; 34 | width: 60px; 35 | height: 44px; 36 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 500'%3E%3Crect width='500' height='500' rx='101.9' ry='101.9' fill='%2300a4de'/%3E%3Cg fill='%23fff'%3E%3Cpath d='M278.2,258.1q-13.6-15.2-37.8-17c14.4-3.9,24.8-9.6,31.4-17.3s9.8-17.8,9.8-30.7A55,55,0,0,0,275,166a48.8,48.8,0,0,0-19.2-18.6c-7.3-4-16-6.9-26.2-8.6s-28.1-2.4-53.7-2.4H113.6V363.6h64.2q38.7,0,55.8-2.6c11.4-1.8,20.9-4.8,28.6-8.9a52.5,52.5,0,0,0,21.9-21.4c5.1-9.2,7.7-19.9,7.7-32.1C291.8,281.7,287.3,268.2,278.2,258.1Zm-107-71.4h13.3q23.1,0,31,5.2c5.3,3.5,7.9,9.5,7.9,18s-2.9,14-8.5,17.4-16.1,5-31.4,5H171.2V186.7Zm52.8,130.3c-6.1,3.7-16.5,5.5-31.1,5.5H171.2V273h22.6c15,0,25.4,1.9,30.9,5.7s8.4,10.4,8.4,20S230.1,313.4,223.9,317.1Z'/%3E%3Cpath d='M357.6,306.1a28.8,28.8,0,1,0,28.8,28.8A28.8,28.8,0,0,0,357.6,306.1Z'/%3E%3Crect x='332.6' y='136.4' width='50' height='151.52'/%3E%3C/g%3E%3C/svg%3E"); 37 | } 38 | ``` 39 | 40 | こんな感じ。 41 | 42 |  43 | 44 | ロゴの SVG は [素材集 \- 株式会社はてな](https://hatenacorp.jp/press/resource) より 45 | 46 | というわけで設置できた。 47 | -------------------------------------------------------------------------------- /docs/hello-deno-1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello, Deno v1.0.0 3 | created: 1589719008195 4 | --- 5 | 6 | Deno 1.0.0 がリリースされて、ちょっと遊んでみたので、その感想。 7 | 8 | ## 圧倒的ゼロインストール感 9 | 10 | 自分は mac なので `brew install deno` しました。`deno` コマンドが入ります。セットアップはこれで終わり。 11 | 12 | GitHub の trending に上がっていた https://github.com/oakserver/oak という web server を試してみます。 13 | 14 | ```ts 15 | // server.ts 16 | import { Application } from "https://deno.land/x/oak/mod.ts"; 17 | const app = new Application(); 18 | 19 | app.use((ctx) => { 20 | ctx.response.body = "Hello World!"; 21 | }); 22 | 23 | await app.listen({ port: 8000 }); 24 | ``` 25 | 26 | このコードを保存して、実行します 27 | 28 | ``` 29 | deno run --allow-net server.ts 30 | ``` 31 | 32 | これだけ。コマンド実行時に `https://deno.land/x/oak/mod.ts` から依存がダウンロードされて、サーバーが立ちます。 33 | 34 | ここで、インストールコマンドを何も叩いてないのに注目してください。Deno では実行時に URL が静的に解決されて実行されます。パッケージや `package.json` といったものがありません。 35 | 36 | `--allow-net` も特徴的ですね。deno ではデフォルトでは権限が限定されています。ローカルファイルに触るには、 `--allow-read` や `--allow-write` が必要になります。 37 | 38 | ## 開発環境 39 | 40 | ここはまだ難があるように思います。 41 | 42 | Deno ではモジュールシステムやパス解決が純粋な TypeScript と非互換(`.ts` の拡張子省略ができない) ので、 vscode 拡張などで専用の typescript server を起使う、といったソリューションが試みられています。 で、`axetroy/deno` と `justjavac/deno` があるんですが、どちらも中途半端です。どちらも import-map 対応のオプションがあるように見えるんですが、動きません。ちょっと変なことをすると、すぐ動かなくなります。 43 | 44 | deno-ja の slack で聞いた限りでは justjavac の方が公式に近い立ち位置なんですが、 axetroy/deno の fork らしいんですが、まだ axetroy/deno のがちゃんと動いてるみたいです。 45 | 46 | (そもそも deno やってる人たちは vscode を TS の違反の警告を無視するのになれてるらしく、エディタ支援はない前提っぽいで書いてる人が多い印象) 47 | 48 | ## モジュールシステム 49 | 50 | 公式には deno.land/std と、サードパーティ相当の deno.land/x があります 51 | 52 | - https://deno.land/std 53 | - https://deno.land/x 54 | 55 | それとは別に、ESM で node の標準ライブラリに頼らずビルドされたコードは import できます。 npm をバックエンドにした CDN としては、以下のようなものがあります。 56 | 57 | - [jsDelivr \- A free, fast, and reliable Open Source CDN for npm & GitHub](https://www.jsdelivr.com/) のうち、rollup の ESM ビルドが配布されているもの 58 | - [UNPKG](https://unpkg.com/) の `?module` での esm build 59 | - [Pika CDN](https://www.pika.dev/cdn) で ESM 用にビルドされた JS 60 | 61 | ## Next.js クローンを作ろうとしてみた 62 | 63 | [mizchi/toxen](https://github.com/mizchi/toxen) っていうリポジトリで、 next.js っぽいサーバー書いて遊んでました。(まだ実験中です) 64 | 65 | 技術スタック 66 | 67 | - deno 68 | - snowpack 69 | - oak 70 | - preact 71 | - htm 72 | 73 | snowpack は一種のフロントエンド向けのバンドラーなんですが、webpack に頼らずネイティブ ESM で動くモジュールを吐いて、その際に snowpack が生成する `import-map.json` を deno の importmap として使う、といった方法を選んでみました。 74 | 75 | - [Snowpack](https://www.snowpack.dev/) 76 | - [WICG/import\-maps: How to control the behavior of JavaScript imports](https://github.com/WICG/import-maps) 77 | 78 | やってみた感じ、 `import { h } from "preact"` という感じの、比較的 node と似たような書き味で、快適です。 79 | 80 | ただ、事前に snowpack でビルドして、動的に `pages/*.ts` を読んで、 `dist/*.js` に吐き出して…とやっていたら、結局こんな感じの実行スクリプトに 81 | 82 | ``` 83 | snowpack && deno run --unstable --allow-write --allow-read --allow-net --importmap web_modules/import-map.json server.ts 84 | ``` 85 | 86 | 権限を厳密に管理するので、多少辛いですね… 87 | 88 | deno ならではの工夫として、next.js のクローンなので `pages/*.ts` のエンドポイントをターゲットに SSR する必要があるんですが、webpack や rollup のバンドラを使うのではなく `Deno.bundle(Deno.cwd() + "/pages/foo.ts")` みたいなコードで Deno の内部バンドラをそのまま使って、静的な JS を吐いてブラウザに食わせています。 89 | 90 | あと、 amp + amp-script(worker-dom) で動かしているので、 amp ページでありながら動的に動く画面を作れます。 worker-dom は技術的な詳細はここでは解説しませんが、動的なコンポーネントを初期化するには SSR が必須です。 91 | 92 | [Documentation: \- amp\.dev](https://amp.dev/documentation/components/amp-script/) 93 | 94 | React ではなく preact なのは、 amp-script の 150kb 制限をクリアするためです。tsx を使っていなくて htm なのは、preact pragma を使って tsx を書くと React の型がないので、実行時例外になるのをかわすためのワークアラウンドです。 95 | 96 | 今後の TODO として、動的 URL に対応する、Deno ライブラリとして抽象的にする、export できるようにする、といった方向性があると思ってるんですが、これを本気で開発するかはまだ悩んでいて、もうちょっと Deno 自体が安定してほしい気持ちもありますね。 97 | 98 | ただ、このブログのような SSG なら、ランタイムに deno がいないので、比較的安心して採用できる気がしました。 99 | 100 | ## Universal JavaScript を考え直す 101 | 102 | ここまでやってみて、 Deno に対応する、というのは 2 つの水準があると思いました。 103 | 104 | - CLI ツールなど向けに、 deno の標準ライブラリを使って、 `.ts` で deno べったりで書き直す 105 | - Universal な JavaScript として、 rollup でビルドして配布する 106 | 107 | 要は esm に対応してさえいれば読み込みはできるので、Universal な JavaScript としては、rollup でビルドしていると deno 対応している、と言えなくはないです。npm にあげてしまった後は、npm の CDN である jsdelivr を経由して読み込むといいでしょう。 108 | 109 | deno を使っていくかどうかはともかく、 snowpack など esm 前提の実行環境なども増えてるので、この辺を意識しておくと、将来的な可用性が増えるのではないでしょうか。 110 | -------------------------------------------------------------------------------- /docs/mdxx-0.6.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: amdx@0.6 - PWA Support, Lighthouse 100, RSS対応, コンポーネントの Tailwind 化 3 | created: 1589106473785 4 | tags: [amdx] 5 | --- 6 | 7 | 例によってこのブログ自身が実装例です。 8 | 9 |  10 | 11 | Accessbility のスコアがちょっと低いのは、 amp-social-share の生成する要素に `aria-label` がついてないから。どうしろと 12 | 13 | ## Done 14 | 15 | ### amdxg 16 | 17 | - Lighthouse 100 18 | - PWA Support 19 | - RSS 対応 `/rss.xml` 20 | 21 | ### amdxg-cli 22 | 23 | - `amdxg postbuild:rss` - rss 対応 24 | - `amdxg postbuild:sitemap`: WIP - sitemap.xml 生成する(Search Console に認識されない…) 25 | 26 | ### amdxg-components 27 | 28 | - Tailwindcss + postcss + purgecss 29 | - Better CSS markup 30 | 31 | ## 次の TODO 32 | 33 | - タグのサポート: frontmatter で `tags: [a,b,c]` とした場合、タグによるインデックスページを生成 `/tags/hoge` 34 | -------------------------------------------------------------------------------- /docs/mdxx-0.7.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: amdx@0.7 tag 3 | created: 1589127481769 4 | tags: [amdx] 5 | --- 6 | 7 | ## 仕様変更 8 | 9 | `amdxg new:page` が `pages/*.tsx` を生成しないように 10 | 11 | かわりに、 `pages/[slug].tsx` が共通の記事要素になりました。 12 | 13 | ## new feature: tags support 14 | 15 | frontmatter の yaml を使って、タグを宣言できるようになりました 16 | 17 | ```yaml 18 | --- 19 | title: my-title 20 | created: 1589127481769 21 | tags: [a, b, c] 22 | --- 23 | # hello 24 | ``` 25 | 26 | トップページにタグ一覧が表示されます。 27 | 28 | `/tags/[tag]` ページで、そのページに紐付いている記事一覧を出せるようにしました。 29 | 30 | tags 31 | 32 |  33 | 34 | ## v1.0.0 に向けて 35 | 36 | - 画像の拡大が壊れている気がするので、それを修正する 37 | - parser/compiler のリネーム 38 | - そろそろ使えるようになってきた 39 | -------------------------------------------------------------------------------- /docs/mdxx-cli-and-components.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: amdxg@0.5.0 - amdxg-cli & amdx-components 3 | created: 1588952818281 4 | tags: [amdx] 5 | --- 6 | 7 | ## ChangeLog 8 | 9 | - amdx 10 | - `$$ ~ $$` の数式ブロックを amp-mathml に変換 11 | - amdxg-components 12 | - `amdxg` は現状単なるボイラープレートだが、設定が最小で済むように `Layout` `Article` などに抽出 13 | - [amp-social-share で、はてなブックマークに対応](/amp-social-share-for-hatena-bookmark) 14 | - amdxg-cli: CLI ツール 15 | - 記事一覧の生成 (`gen/pages.json`) 16 | - 記事のヒストリーの生成 (`gen/*.history.json`) 17 | 18 | ## amdxg-cli のインストール 19 | 20 | ```bash 21 | npm install -g amdxg-cli 22 | ``` 23 | 24 | ## amdxg new 25 | 26 | WIP 27 | 28 | ## new feature: 新規記事の作成 29 | 30 | ``` 31 | amdxg new:page [foo] 32 | ``` 33 | 34 | ## new feature: インデックスの記事一覧とヒストリーの生成 35 | 36 | ``` 37 | amdxg build:index 38 | ``` 39 | 40 | `docs` ディレクトリの記事一覧から、`gen/pages.json` を生成する。 41 | 42 | ``` 43 | amdxg build:history 44 | ``` 45 | 46 | `docs/*.mdx` の git コミットログから変更ヒストリーを生成する。 47 | 48 | ```tsx 49 | import history from "../gen/pages/foo.history.json"; 50 | import { History } from "amdxg-components"; 51 | 52 | ; 53 | ``` 54 | 55 | amdxg.config で、もし `"repository": "mizchi/amdx"` のように GitHub のリポジトリを指定していると、そのコミットへのリンクになる。 56 | 57 | ## TODO 58 | 59 | - `pages/sitemap.xml`: sitemap 対応 60 | - `pages/feed.rss`: feed 対応 61 | - `amdxg new your-blog`: 最新版から scaffold できるようにする 62 | - `amdxg new:page` 時、記事生成のテンプレート化できるようにする対応 63 | - `amdxg-components`: `Layout` を使わない、カスタマイズ度が高い作例を用意する 64 | - frontmatter のタグ一覧対応 65 | - amdxg を github actions でデプロイできるようにしておく 66 | - amdx preview の vscode extension 67 | -------------------------------------------------------------------------------- /docs/mdxx-introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: MDX eXtended = MDXX | AMP対応 Markdown Compiler と静的サイトジェネレーター 3 | created: 1588688918620 4 | tags: [amdx] 5 | --- 6 | 7 | 最近作っている amdx という markdown コンパイラとそのツール郡を紹介します。 8 | 9 | GitHub はここ [mizchi/amdx](https://github.com/mizchi/amdx) 10 | 11 | このサイト自体も、 [amdxg](https://github.com/mizchi/amdx/tree/master/packages/ssg) というツールで作られています。 12 | 13 | ## ゴールをどこに設定したか 14 | 15 | - パフォーマンスを突き詰めると、ブログは静的サイトジェネレータで AMP 対応するのが一番と考えた 16 | - next.js/SSG export + AMP が便利だったので、next.js 上でコンパイルすることを前提に、静的解析を行う webpack loader を作ることにした 17 | - mdx の parser を借りて、 `.mdx` をロードすると React Component として振る舞いつつ、他の `mdx` を import できると、長い文章を書くときにファイル分割できて便利なのでは 18 | - Markdown プレビュー高速化+ランタイム最小化のために、AST(JSON) 生成する部分と、AST からの実行処理部分を分割した 19 | - 基本は Full AMP で、React Component 動くページを作れる余地も残したい 20 | 21 | ## 何を作ったか/amdx とはなにか 22 | 23 | - remark ベースで React Component を出力する parser / compiler / cli / next.js template 24 | - AMP 対応: img は `amp-img` を吐くなど 25 | - yaml frontmatter 対応 26 | - Code Syntax Highlight 対応: prismjs で静的解析をするので、CSS のみでランタイム不要 27 | - ToC 対応 28 | 29 | 基本は mdx をベースに、高速化+静的解析で最適化+AMP 対応+周辺ツールという感じ 30 | 31 | ## 周辺ツール: npm packages 32 | 33 | - amdx-loader: webpack loader 34 | - rollup-plugin-amdx: rollup 35 | - amdx-cli: コマンドラインで mdx をコンパイルできる CLI ツール 36 | 37 | ## 周辺ツール amdxg 38 | 39 | このブログ自身がサンプルなのですが、自分自身のサイトを作ることもできます。 40 | 41 | こんな感じ。 42 | 43 | ```bash 44 | # install node and npm 45 | npx degit mizchi/amdx/packages/ssg blog 46 | cd blog 47 | npm install 48 | cp amdxg.config.example amdxg.config 49 | # edit amdxg.config for you 50 | 51 | # create new page 52 | npm run new-page 53 | # edit docs/[current-date].mdx 54 | 55 | # create new page with slug 56 | npm run new-page foo 57 | # edit docs/foo.mdx 58 | 59 | # Preview 60 | npm run dev 61 | 62 | # Deploy 63 | npm run build 64 | ``` 65 | 66 | netlify や github pages や firebase hosting を想定しています。 67 | 68 | netlify にデプロイする例。 69 | 70 | ``` 71 | yarn build 72 | netlify deploy -d out --prod 73 | ``` 74 | 75 | まだ色々と手抜きで、かつ頻繁に更新しているので、使いたい人は PR or 自分で CSS 書くなどしてください。 76 | 77 | ## 現在のステータス 78 | 79 | - ドキュメントが足りてない 80 | - SSG といいつつ、ただの愚直な next.js プリセットという感じ。コンポーネント化+ちゃんとしたデザインをする 81 | 82 | ## TODO 83 | 84 | - [ ] カスタムな Remark Plugin を読み込む口を開ける 85 | - [ ] amp-script 対応: AMP 内でバンドルした js を生成して、worker-dom で動かす 86 | - [ ] amp-mathml: 非 AMP 環境向けでは KaTex で対応しているが、amp 時は amp-mathml に切り替える 87 | - [ ] https://mdbuf.netlify.com/ で使うコンパイラを amdx にする 88 | - [ ] 推奨の CSS を配布する 89 | - [ ] commit log から history pages を作成し、かつ GitHub の PR を作れる機能を作りたい(fork せずにできるのか?) 90 | 91 | ## 余談: mdx ではだめだったのか 92 | 93 | https://github.com/mdx-js/mdx/tree/master/packages/mdx を読んだ限りは、拡張しづらく、自分の用途に合いませんでした。 94 | 95 | - remark の parse と complile(stringify) を分離できない 96 | - [mdbuf](https://mdbuf.netlify.com/) を作った経験だと、 React Element は worker で渡せないので、pure json な AST を作る必要がある 97 | - がっつり拡張するなら amdx が良さそうだった 98 | 99 | ## おわり 100 | 101 | 買っておいた秘蔵の `https://mizchi.dev` を netlify から繋いで、今後も自分のブログとしてドッグフーディングしつつ作っていきます。 102 | 103 | 機能要望などあったら、[GitHub の Issue](https://github.com/mizchi/amdx/issues) か [@mizchi](https://twitter.com/mizchi) まで Twitter でお知らせください。 104 | -------------------------------------------------------------------------------- /docs/next-amp-tailwind-postcss.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: next.js の amp モードで tailwind.css を purgecss と合わせて使う 3 | created: 1589132870983 4 | tags: 5 | - next 6 | - amp 7 | - tailwindcss 8 | - purgecss 9 | --- 10 | 11 | [このリポジトリ](https://github.com/mizchi/next-amp-tailwind-purgecss) でやったこと。 12 | 13 | ## やろうとしたこと 14 | 15 | tailwind.css は Utility First と銘打った CSS フレームワークで、コンポーネント化を前提としたモダンフレームワークと相性がいいです。今回は next.js の amp-mode で tailwind を使おうとしてみました。 16 | 17 | [Tailwind CSS \- A Utility\-First CSS Framework for Rapidly Building Custom Designs](https://tailwindcss.com/) 18 | 19 | ## 問題 20 | 21 | 前提として、 amp には inline css の容量制限があり、75kb を超えると AMP と認識されなくなります。 22 | 23 | tailwind はビルドして使うのが前提のフレームワークですが、全部入りの tailwind.min.css は 1.3 M あります。これでは到底、75 kb に収まりません。 24 | 25 | [AMP の CSS サイズ上限が 50000 バイトから 75000 バイトへ 50%増量 ⬆ \| 海外 SEO 情報ブログ](https://www.suzukikenichi.com/blog/amp-increases-css-limit-from-50000-bytes-to-75-bytes/) 26 | 27 | tailwind css は reset.css が重いというより、utility の数が膨大で、それらをすべて読み込むのでビルドサイズがかさんでるという感じでした。 28 | 29 | なので、 purgecss を使って、使っている CSS だけ使えばよい、という発想で CSS のサイズを削ってみました。 30 | 31 | ## purgecss 32 | 33 | purgecss は指定した html,js,tsx, vue のソースコード上に出現する CSS セレクタのみ抽出するライブラリです 34 | 35 | [FullHuman/purgecss: Remove unused CSS](https://github.com/FullHuman/purgecss) 36 | 37 | もちろん、素朴にやるとそれだけでは動かないので、さらに whitelist で許可する CSS を宣言します。今回は postcss を通して使います。 38 | 39 | ## postcss の設定 40 | 41 | ついでに、`postcss-import` を使って、単一ファイルに bundle しつつ、 `cssnano` で圧縮しています。 42 | 43 | 準備 44 | 45 | ``` 46 | yarn add postcss postcss-import taliwindcss @fullhuman/postcss-purgecss postcss-preset-env cssnano postcss-cli -D 47 | ``` 48 | 49 | `postcss.config.js` 50 | 51 | ```js 52 | module.exports = { 53 | plugins: [ 54 | require("postcss-import"), 55 | require("tailwindcss"), 56 | require("@fullhuman/postcss-purgecss")({ 57 | content: [ 58 | "./pages/**/*.{js,jsx,ts,tsx}", 59 | "./components/**/*.{js,jsx,ts,tsx}", 60 | ], 61 | defaultExtractor: (content) => content.match(/[\w-/:]+(? (props) => ); 93 | const initialProps: any = await Document.getInitialProps(ctx); 94 | return { 95 | ...page, 96 | styles: [ 97 | ...initialProps.styles, 98 | , 104 | ], 105 | }; 106 | } 107 | 108 | render() { 109 | return ( 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | ); 118 | } 119 | } 120 | ``` 121 | 122 | (`initialProps.styles` は next.js が生成する AMP のためのデフォルト CSS なので、必須です) 123 | 124 | ## おまけ: amdxg-components に適用してみた 125 | 126 | 動作サンプルはこのブログ自身です。ヘッダやフッタなどが tailwind.css によって書かれています。 127 | 128 | このブログは [amdx/packages/ssg](https://github.com/mizchi/amdx/tree/master/packages/ssg) で書かれているのですが、その内部でさらに分割した `amdxg-components` パッケージに tailwind + purgecss を適用しました。 129 | 130 | `bundle.css` のサイズは最終的に `1.3M` から `6.3K` になりました。これなら AMP 環境でも安心。 131 | -------------------------------------------------------------------------------- /docs/study-next-amp-by-mdxx-ssg.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: next.js の AMP mode を使って静的サイトを作る 3 | created: 1588760362771 4 | tags: [next, amdx] 5 | --- 6 | 7 | この記事は amdxg を作りながら, next.js で AMP に対応したときにやったことです。 8 | 9 | コードはこちらです [amdx/packages/ssg at master · mizchi/amdx](https://github.com/mizchi/amdx/tree/master/packages/ssg) 10 | 11 | ## AMP について 12 | 13 | Google の推奨する HTML のサブセット仕様です。制約付きのインライン CSS のみ + 一切の JS が書けず、代わりに動きがあるものは amp plugin を使って記述します。 14 | 15 | モバイルでは、Google の検索結果画面からは GoogleCDN 上のキャッシュが返却されるので、非常に高速に開くことができます。 16 | 17 |  18 | 19 | (⚡ マークが AMP 対応の印) 20 | 21 | モバイルに限らず、ある種のベストプラクティスの強制なので、PC でも AMP 対応することに意味はあります。 22 | 23 |  24 | 25 | この記事では、実際にこのブログのための SSG を作る過程で、どのように next.js 上で AMP に対応していったかを説明します。 26 | 27 | この記事、このブログ自体が動作サンプルとなっています。 28 | 29 | ## next.js を採用した理由 30 | 31 | まず `pages/*.tsx` を置くだけでパス指定になるという規約が使いやすく、静的サイトの土台としては十分です。場合によっては、サーバーで動的に動かすようにもできます。フロントエンドエンジニアは React の作例なんかを置きたかったりしますよね。 32 | 33 | 静的サイトジェネレータ(SSG)としては、`next build && next export` で、スタンドアロンな静的サイトを生成できる、というのが大きいです。この静的サイト生成で Full AMP として AMP ページを生成しています 34 | 35 | ## 準備 36 | 37 | Chrome 拡張として AMP Validator をインストールしてください。 38 | 39 | [AMP Validator \- Chrome ウェブストア](https://chrome.google.com/webstore/detail/amp-validator/nmoffdblmcmgeicmolmhobpoocbbmknc?hl=ja) 40 | 41 | この Chrome 拡張の ⚡ が緑になっていれば、それは AMP として Valid なウェブサイトです。 42 | 43 |  44 | 45 | 注意点として、 next.js の AMP 有効時は、必ず 1 件はエラーとなります。これは正常です。 46 | 47 |  48 | 49 | なにかエラーが出たときは、 開発モードでこれが 1 件 になるまでエラーを潰す、という感じになります。出力時に 0 件になります。 50 | 51 | ## next.js の AMP mode を使う。 52 | 53 | next.js で AMP を使うだけなら非常にシンプルです。 54 | 55 | ``` 56 | pages/index.tsx # 最新の next は ts にデフォルトで対応している 57 | pages/index.tsx # /foo 58 | 59 | package.json # dependencies に next を含む 60 | ``` 61 | 62 | いつもの next.js のボイラープレートです。これで `yarn next` すると開発用サーバーが立ち上がります。 63 | 64 | ### Full AMP 65 | 66 | 常に AMP で生成するのを Full AMP といいます。このとき、 次のように `amp: true` を指定します。 67 | 68 | ```js 69 | // pages/index.tsx 70 | 71 | export const config = { 72 | amp: true, 73 | }; 74 | 75 | export default () => { 76 | return index; 77 | }; 78 | ``` 79 | 80 | このとき、 AMP は静的ページなので、React は SSR のみ行われます。次のような hooks は実行されません。 81 | 82 | ```js 83 | import React, { useEffect } from "react"; 84 | export const config = { 85 | amp: true, 86 | }; 87 | export default () => { 88 | useEffect(() => { 89 | console.log("ここは AMP mode では実行されない"); 90 | }, []); 91 | return index; 92 | }; 93 | ``` 94 | 95 | 内部的には、 AMP ではオリジナルな記事への canonical の指定を行いますが、 Full AMP では常に自分自身を指します。 96 | 97 | [ページが検出されるようにする \- amp\.dev](https://amp.dev/ja/documentation/guides-and-tutorials/optimize-and-measure/discovery/) 98 | 99 | amdxg での記事のボイラープレートでは、Full AMP を採用していますが、必要に応じて通常ページのレンダリングもできるようにしています。 100 | 101 | ### Hybrid AMP 102 | 103 | amdxg では採用してませんが、 next.js ではモバイルと Google へのインデックスのために AMP を生成しつつ、PC 用には通常の AMP のレンダリングを行うモードがあります。こっちのほうが一般的な AMP かも。 104 | 105 | ```js 106 | // pages/index.tsx 107 | 108 | export const config = { 109 | amp: "hybrid", 110 | }; 111 | 112 | export default () => { 113 | return index; 114 | }; 115 | ``` 116 | 117 | hybrid mode 時、どちらのコンテキストで生成するかで処理を切り替える場合、`useAmp()` で処理を切り替えます。 118 | 119 | ```js 120 | import { useAmp } from "next/amp"; 121 | export const config = { 122 | amp: "hybrid", 123 | }; 124 | 125 | export default () => { 126 | const isAmp = useAmp(); 127 | return index: {isAmp ? "amp" : "normal"}; 128 | }; 129 | ``` 130 | 131 | ## CSS と styled-components 対応 132 | 133 | ベースとなる CSS は [github\-markdown\-css/github\-markdown\.css at gh\-pages · sindresorhus/github\-markdown\-css](https://github.com/sindresorhus/github-markdown-css/blob/gh-pages/github-markdown.css) を採用しているのですが、 AMP 環境では `!import` がエラーになります。なので、そのまま使うのではなく `!import` を除去した CSS を用意しました。 134 | 135 | next.js で SSR の土台となる `pages/_document.tsx` で、webpack の raw-loader で CSS を文字列として読み込んで、この CSS を注入します。 (要: `yarn add raw-loader -D`) 136 | 137 | ついでに styled-components の Hydration も行っています。 138 | 139 | ```tsx 140 | import Document, { Html, Head, Main, NextScript } from "next/document"; 141 | // @ts-ignore 142 | import css from "!!raw-loader!../styles/github-markdown.css"; 143 | // @ts-ignore 144 | import prismCss from "!!raw-loader!../styles/prism.css"; 145 | // @ts-ignore 146 | import custom from "!!raw-loader!../styles/styles.css"; 147 | import { ServerStyleSheet } from "styled-components"; 148 | import ssgConfig from "../amdxg.config"; 149 | 150 | export default class MyDocument extends Document { 151 | static async getInitialProps(ctx: any) { 152 | const sheet = new ServerStyleSheet(); 153 | try { 154 | const page = ctx.renderPage((App) => (props) => 155 | sheet.collectStyles() 156 | ); 157 | const initialProps: any = await Document.getInitialProps(ctx); 158 | const styles = [ 159 | ...initialProps.styles, 160 | , 166 | ...sheet.getStyleElement(), 167 | ]; 168 | return { 169 | ...page, 170 | styles, 171 | }; 172 | } finally { 173 | sheet.seal(); 174 | } 175 | } 176 | 177 | render() { 178 | return ( 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | ); 187 | } 188 | } 189 | ``` 190 | 191 | これらの CSS は、 amp の CSS 制約を満たすように、 `` と一つの CSS タグで展開されます。 192 | 193 | ## Google Analytics 対応 194 | 195 | amp-analytics を使います。 196 | 197 | [AMP ページにアナリティクスを追加する \| AMP ページ向けアナリティクス \| Google Developers](https://developers.google.com/analytics/devguides/collection/amp-analytics?hl=ja) 198 | 199 | Google Analytics 側で新しいサービスを登録し、発行された埋め込みタグの gtag_id だけ抜き出しておきます。 200 | 201 | ``` 202 | // components/ItemLayout.tsx 203 | 204 | function Analytics() { 205 | const json = JSON.stringify({ 206 | vars: { 207 | gtag_id: "[your-gtag-id]", 208 | config: { 209 | "[your-gtag-id]": { groups: "default" }, 210 | }, 211 | }, 212 | }); 213 | return ( 214 | // @ts-ignore 215 | // prettier-ignore 216 | 217 | ); 218 | } 219 | ``` 220 | 221 | ここは色々ややこしいので、 [dev/ItemLayout\.tsx at master · mizchi/dev](https://github.com/mizchi/dev/blob/master/components/ItemLayout.tsx) を見たほうが早いと思います。 222 | 223 | ## AMP Script 対応 224 | 225 | amp では 普通の JS を動かすことはできませんが、 WebWorker 環境に指定要素の DOM の仮想的なオブジェクトを生成して、それを操作することで、DOM に反映させる、という worker-dom を使うことができます。 226 | 227 | [この DOM がすごい 2018: worker\-dom \- mizchi's blog](https://mizchi.hatenablog.com/entry/2018/10/18/155448) 228 | 229 | [Google Developers Japan: amp\-script: AMP ❤️ JS](https://developers-jp.googleblog.com/2019/10/amp-script-amp-js.html) 230 | 231 | [AMP で任意の JS を実行できる amp\-script を試してみた \- Qiita](https://qiita.com/mizchi/items/c7d648eafb03d4c5378a) 232 | 233 | amdxg では、これを next.js+webpack と連携してシームレスに組み込める仕組みを、なにか用意しようと考えています。 234 | 235 | 今はこういう PR を出しています。 236 | 237 | [\[RFC\] Add npm library mode by mizchi · Pull Request \#855 · ampproject/worker\-dom](https://github.com/ampproject/worker-dom/pull/855) 238 | 239 | ## amdx でやったこと 240 | 241 | ### amdx での code syntax highlighter の実装 242 | 243 | amp 制約下では、JS が実行できないので、プログラミング言語のランタイムでの構文解析を実行することはできません。なので、markdown のコードブロックの中身を、事前にトークンに落とすところまで行っています。 244 | 245 | [amdx/highlighter\.ts at master · mizchi/amdx](https://github.com/mizchi/amdx/blob/master/packages/parser/src/highlighter.ts) 246 | 247 | パース後は言語非依存のトークンに変換されているので、あとは CSS を当てるだけです。 248 | 249 | https://github.com/PrismJS/prism-themes/tree/master/themes 250 | 251 | ### amdx-runner での amp-img 対応 252 | 253 | amp ではレイアウト最適化のために img を直接使うのではなく、 `amp-img` を使う必要があります。 254 | 255 | amdxg 用の amdx-runner では、AMP フラグを付けると、 `` の link 構文を、次のようなコードと HTML 要素に変換します。 256 | 257 | こういう alt を使った構文を想定 258 | 259 | ``` 260 | ## doc.mdx 261 | 262 |  263 | ``` 264 | 265 | 展開コード 266 | 267 | ```tsx 268 | import Doc from "./doc.mdx"; // amdx-loader で変換される 269 | ; // AMP フラグを立てると img を amp-img に変換する 270 | ``` 271 | 272 | 変換後の生成コード 273 | 274 | ```tsx 275 | // height はインライン要素で指定する 276 | 277 | 278 | 279 | ``` 280 | 281 | ```css 282 | .amp-img-container { 283 | position: relative; 284 | width: 100%; 285 | display: flex; 286 | } 287 | 288 | .amp-img-container amp-img img { 289 | object-fit: contain; 290 | background: #eee; 291 | } 292 | ``` 293 | 294 | これで指定の高さで width: 100% でレスポンシブに拡大される画像要素とすることができます。 295 | 296 | ## Deploy 297 | 298 | - `docs/*.mdx` の frontmatter を収集した json を生成して `gen/pages.json` を生成 299 | - next build 300 | - next export 301 | - netlify deploy -d out --prod 302 | 303 | このサイトは、 netlify のドメイン紐付け機能で、 google domains で買った mizchi.dev を繋げています。 304 | この辺は別途記事にします。 305 | 306 | ## 感想 307 | 308 | next.js はとても汎用的なフレームワークですが、 amp 対応の静的サイトの土台にも適しています。 309 | 310 | getStaticProps や getStaticPaths を使うとまた難しくなってくるのですが、素朴に使う限りは簡単でした。 311 | 312 | ただし、用意されてるもの以外のツールチェインは無いので、自分で AMP の Issue を調べたり、React や JS で実装しきる基礎力は要求されます。ライブラリも amp plugin 以外は存在しないと思って良いです。 313 | 314 | `docs/*.mdx` にファイルをおいてコマンドを叩いたらデプロイされる、というところまでは作りきったんですが、見えてるものが多すぎて混乱を招くので、静的サイトジェネレータとしてはもうちょっと設計を練りたい感じですね。 315 | -------------------------------------------------------------------------------- /docs/study-recoil.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Recoil について勉強した 3 | created: 1589622364679 4 | tags: [react] 5 | --- 6 | 7 | Fecebook が新しく発表した [Recoil](https://recoiljs.org/) について 8 | 9 | ## 自分の学習手順 10 | 11 | - [Getting Started \| Recoil](https://recoiljs.org/docs/introduction/getting-started/) を写経して動かす 12 | - [Facebook 製の新しいステート管理ライブラリ「Recoil」を最速で理解する \- uhyo/blog](https://blog.uhy.ooo/entry/2020-05-16/recoil-first-impression/) で非同期周りを理解 13 | - 公式ドキュメントの API Reference で理解 [ \| Recoil](https://recoiljs.org/docs/api-reference/core/RecoilRoot) 14 | 15 | これは自分が写経しながら書いた型定義。色々足りてないがチュートリアルで出る範囲は理解できる。 16 | 17 | ```ts 18 | declare module "recoil" { 19 | export type RecoilState = {}; 20 | export const RecoilRoot: React.ComponentType<{ 21 | initializeState?: (options: { 22 | set: (recoilVal: RecoilState, newVal: T) => void; 23 | setUnvalidatedAtomValues: (atomMap: Map) => void; 24 | dangerouslyAllowMutability?: boolean; 25 | }) => void; 26 | children: any; 27 | }>; 28 | export function atom(input: { 29 | key: string; 30 | default: ValueType; 31 | }): RecoilState; 32 | export function selector(input: { 33 | key: string; 34 | get(helpers: { 35 | get(atom: RecoilState): U; 36 | getPromise(atom: RecoilState): Promise; 37 | }): T; 38 | set?( 39 | helpers: { 40 | set(atom: RecoilState, newVal: U): void; 41 | }, 42 | newVal: T 43 | ): void; 44 | }); 45 | export function useRecoilValue(atom: RecoilState): T; 46 | export function useRecoilState( 47 | atom: RecoilState 48 | ): [T, (action: React.SetStateAction) => void]; 49 | export function useSetRecoilState( 50 | atom: RecoilState 51 | ): (action: React.SetStateAction); 52 | } 53 | ``` 54 | 55 | DefinitelyTyped に PR が出てるが、まだマージされてない。 56 | 57 | [Add type definitions for recoil by csantos42 · Pull Request \#44756 · DefinitelyTyped/DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/44756) 58 | 59 | 後述する `waitForAll` などのユーティリティが書きかけ。 60 | 61 | ## 思想的な部分 62 | 63 | - Redux は常に一つでかつすべての状態ありきの思想なので、State とその手続きが宣言される。このせいで、常に使わない State も初期化しないといけない 64 | - Recoil は状態を依存グラフで表現する。atom とそれを参照する selector があり、selector が atom を購読して反映される 65 | 66 | また、 selector への set で atom を同期/非同期に書き換えるというインターフェースになっている。実体は atom を実体とした単方向サブスクリプションだが、コード上は双方向にもなりうる。 67 | 68 | ## selector が mutable 69 | 70 | atom と selector も、 `const [state, setState] = useRecoilState(atom_or_selector)` できる。 selector が setState できる、とはどううことだろうか。 71 | 72 | 単一な状態を持つ atom だけではなく、グラフ中で selector ノードも、まるで Mutable かのような API を持つ。自分自身への更新時、非同期に個別の atom への set を再発行できるファサードになっている。 73 | 74 | 公式サンプルからの引用だが、次のコードは華氏と摂氏の二値が連動して動く。 75 | 76 | ```tsx 77 | import { 78 | RecoilRoot, 79 | atom, 80 | selector, 81 | useRecoilState, 82 | useRecoilValue, 83 | } from "recoil"; 84 | 85 | const tempFahrenheit = atom({ 86 | key: "tempFahrenheit", 87 | default: 32, 88 | }); 89 | 90 | const tempCelcius = selector({ 91 | key: "tempCelcius", 92 | get: ({ get }) => ((get(tempFahrenheit) - 32) * 5) / 9, 93 | set: ({ set }, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32), 94 | }); 95 | 96 | function TempCelcius() { 97 | const [tempF, setTempF] = useRecoilState(tempFahrenheit); 98 | const [tempC, setTempC] = useRecoilState(tempCelcius); 99 | 100 | const addTenCelcius = () => setTempC(tempC + 10); 101 | const addTenFahrenheit = () => setTempF(tempF + 10); 102 | 103 | return ( 104 | 105 | Temp (Celcius): {tempC} 106 | 107 | Temp (Fahrenheit): {tempF} 108 | 109 | Add 10 Celcius 110 | 111 | Add 10 Fahrenheit 112 | 113 | ); 114 | } 115 | ``` 116 | 117 | 元となる状態自体は tempFahrenheit が atom で、その selector としての tempCelcius だが、tempCelcius への set で、 tempFahrenheit を書き換えて、値を反映している。 118 | 119 | これは正直、議論が分かれそうな設計だと思っていて、 vue の computed property などに近いようにみえて、computed property には不可能な副作用も記述できるが、その実装が正しいかどうかは、実装者が責任を持つことになるだろう。 120 | 121 | 単なる set では atom を直接書き換えたほうがきれいだと思うが、ここで面白いのは、set の実装が非同期の Promise を取れるということだ。実質 redux middleware で多段 dispatch するときと同じようなコードになる。 122 | 123 | ## 非同期な state と Suspense 124 | 125 | ここで state / selector は非同期を取れるので、 get / set は async/await のインターフェースをとることができる。 126 | 127 | 1 秒後に値を表示する例 128 | 129 | ```tsx 130 | const lazyState = selector({ 131 | key: "lazyState", 132 | get: async () => { 133 | await new Promise((r) => setTimeout(r, 1000)); 134 | return 1; 135 | }, 136 | }); 137 | 138 | function LazyValue() { 139 | const value = useRecoilValue(lazyState); 140 | return {value}; 141 | } 142 | 143 | function App() { 144 | return ( 145 | 146 | 147 | 148 | 149 | 150 | ); 151 | } 152 | 153 | ReactDOM.render(, document.querySelector("main")); 154 | ``` 155 | 156 | 実装をみると、最初の `useRecoilValue` で `throw new Promise(...)` を発行し、 Suspense にキャッチさせて解決させるやつ。 157 | 158 | これを使うと、ネットワーク越しのリソースを抽象したりすることができそう。 159 | 160 | ## Redux との比較 161 | 162 | - 大域の再計算にならないので、React Component から参照されるときの再計算が、最小限 163 | - 必要なコードだけビルドに含めることができる 164 | - 状態更新の手続きは reducer ではなく、setState の React.SetStateAction 準拠 165 | - 非同期抽象が middleware ではなく、 promise + suspense になる 166 | 167 | ## 自分がまだわかってないところ 168 | 169 | Redux では常にひとつの状態が全部の状態を表すので、SSR で渡したり、 localStorage に状態を書き込んでから、再訪時に状態を復元する、というのが容易だった。Recoil では、RecoilRoot がすべての状態を管理しているはずだが、それを吐き出したり、よみこんだりする方法が(まだ)ない。 170 | 171 | 今ちょうどリロードしたらドキュメントに Core 以外の Utils というのが生えて、この辺の `waitForAll` にその機能がありそうなので、しばらく待ったほうがよさそう。 172 | 173 | https://recoiljs.org/docs/api-reference/utils/waitForAll 174 | 175 | 可能なら React に依存せず、Recoil のリソースの依存グラフだけで実行できると、サーバー上で hydration のために初期実行できて、嬉しい気がする。 176 | 177 | ## で、結局使い物になるの? 178 | 179 | - 自分的にはアリ。ただし、selector への set は、非同期のユースケースを限定したほうが良さそう 180 | - 状態をダンプする系の API は足りてない。 181 | -------------------------------------------------------------------------------- /docs_wip/202005200249-engineer-career-pattern.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: エンジニアになった人のパターン 3 | created: 1589910599048 4 | tags: [] 5 | --- 6 | 7 | ## きっかけ系 8 | 9 | - 大学の授業で興味を持った 10 | - wordpress のカスタマイズで php 11 | - CGI ゲームの改造のために perl 12 | - ブログのカスタマイズで HTML / CSS 13 | - メディアアートをやってみたくて Flash 14 | - メディアアートをやってみたくて Processing 15 | 16 | ## 日常効率系 17 | 18 | - Excel VBA で効率化 19 | - ブラウザ拡張を作りたかった 20 | 21 | ## ゲーム系 22 | 23 | - ゲームを作りたくて Unity 24 | - ゲームを作りたくて HTML5 Canvas 25 | - RPG ツクールでは満足できなくて C# 26 | 27 | ## 性欲駆動系 28 | 29 | - アダルトサイトのクローラを作りたかった 30 | - エロサイトの広告にイライラしてブラウザ拡張で消すために勉強 31 | - エロサイトの広告にイライラして作る側になった 32 | 33 | ## 不可避系 34 | 35 | - ホームページ時代から 36 | - 研究のツールを改造するために覚える羽目になった 37 | - たまたま得意でたまたまコスパよく稼げたので、 38 | 39 | ## アングラ系 40 | 41 | - 大規模 FTP ファイル交換サーバーの運用 42 | - ラグナロクオンラインの BOT ファームの運用 43 | 44 | ## 傾向 45 | 46 | 最近ほど真っ当にアカデミックで勉強した人が多く、昔ほど自学した人が多く感じる。(そもそも情報系の学部が少なかったのでパイがない) 47 | 48 | 自分の世代はちょうど中間ぐらいで、 49 | -------------------------------------------------------------------------------- /docs_wip/202005212041-javascript-for-starter.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: javascript-for-starter 3 | created: 1590061300128 4 | tags: [] 5 | --- 6 | 7 | import Doc0 from "../fragments/nextjs/00.mdx"; 8 | import Doc1 from "../fragments/nextjs/01.mdx"; 9 | import Doc2 from "../fragments/nextjs/02.mdx"; 10 | import Doc3 from "../fragments/nextjs/03.mdx"; 11 | import Doc4 from "../fragments/nextjs/04.mdx"; 12 | import Doc5 from "../fragments/nextjs/05.mdx"; 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs_wip/age-of-next-js.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Next.js の時代が来る 3 | created: 1589463712919 4 | tags: [react, next.js, blitz] 5 | --- 6 | 7 | WIP 8 | 9 | 一昨年まで、フリーランスの仕事の一つとして node の SSR フレームワークを調査してそのベストプラクティスを提案する仕事をやっていた。 10 | 11 | その中で next.js はとても有力な選択肢の一つだったのだけど、どうしても本番投入しかねる懸念点がいくつか残っていた。 12 | 13 | ## 1 年前までの懸念点 14 | 15 | - 静的または query parameter の routing しかなく、SEO 上求められる URL を選択することが不可能なことがあった。 16 | - SSG に注力してるのも、SSR のフレームワークとしては方向性に疑問があった 17 | - カスタマイズしようとすると、単なる node.js サーバーとして拡張することが求められ、ワンストップなフレームワークとは言えなかった 18 | 19 | しかし、これらはこの一年の間に払拭された。 20 | 21 | - Dynamic Routing の採用で、ルーティング設計の懸念が払拭された 22 | - エコシステムが揃ってきて、最低限の設定で拡張箇所が最小限になった。また vercel の存在が大きい 23 | - SSR と SSG の良いとこ取りのフレームワークとして、「段階的な SSG」という選択肢に結実しようとしている 24 | 25 | ## next.js のいいとこどり: Dynamic Routing 26 | 27 | next 9 から追加された File system-Based Dynamic Routing で、 `pages/[id].tsx` や `pages/[foo]/[bar].tsx` というファイルを作ることによって、自由度が高い動的ルーティングができるようになった。 28 | 29 | [Blog \- Next\.js 9 \| Next\.js](https://nextjs.org/blog/next-9) 30 | 31 | このときのアップデートで、無設定で TypeScript が読み込み可能になったり、 `pages/api/*.ts` が自動的に `/api/*` にマップされるなど、細かいところも気が利いている。 32 | 33 | また、 [next.9.3](https://nextjs.org/blog/next-9-3) で追加された `getServerSideProps` や `getStaticProps/getStaticPaths` といった API が本当に便利で、今までは `getInitialProps` で パラメータを決めて、それを動的もしくは 静的な URL を書き出すために `exportPathMap` でパス一覧を指定して、といった感じだったが、静的サイト生成時と、動的サイト生成時で、明示的に区別しつつ、似たような設計が可能になった。 34 | 35 | 余談として、去年東京に来ていた [Joe Haddad](https://github.com/Timer) に直接ルーティング周りについて質問したことがあって、「[umijs](https://umijs.org/) を参考にしたやつを作ってるから待ってて」といっていた。待っていたら期待以上のものが来た。最高。 36 | 37 | ## 段階的静的サイト生成はキラー機能になりうる 38 | 39 | で、ここからが本題なのだけど、 next9.4 で実験的に追加された、[incremental-static-regeneration-beta](https://nextjs.org/blog/next-9-4#incremental-static-regeneration-beta) という機能に、とても未来を感じている。 40 | 41 | これが何なのかと言うと、フロントエンドの従来の常識では、ウェブサイトの最適化には html を CDN の Edge Cache してしまうのが理論上最速だが、SSR や node.js のアプリケーション・サーバーとして処理をする場合、。 42 | 43 | このデモをみるとよい [Static Tweet Next\.js Demo](https://static-tweet.now.sh/) 44 | 45 | 従来なら、静的サイト 46 | 47 | また、 48 | 49 | ## 段階的な SSG 50 | 51 | React Hooks と React.Context が出た去年ぐらいから、 52 | 53 | 去年ぐらい 54 | 55 | next.js 56 | 57 | [Blog \- Next\.js 9\.4 \| Next\.js](https://nextjs.org/blog/next-9-4) 58 | 59 | とくに、段階的な SSG Support というのはキラー機能になりうると思っていて、今までは難しかった 60 | 61 | ## サンプルの充実 62 | 63 | ## 多様な出力モード 64 | 65 | ## Filesystem base routing & Dynamic Routing のサポート 66 | 67 | `pages/[id].tsx` 68 | 69 | ## AMP Support 70 | 71 | ## Blitz 72 | 73 | blitz は next.js をバックエンドにした Rails 志向のフレームワーク 74 | 75 | - View として React 76 | - Routing として next.js 77 | - ORM として prisma 78 | 79 | どういうコードがあるかは、 [このサンプル](https://github.com/blitz-js/blitz/tree/canary/examples/store) をみるといい。見覚えがあるディレクトリ構造だろう。 80 | 81 | ``` 82 | npm i -g blitz 83 | blitz new my-app 84 | cd my-app 85 | blitz start 86 | ``` 87 | 88 | scaffolding が 89 | 90 | https://github.com/blitz-js/blitz/tree/canary/examples/store 91 | 92 | このようなフレームワークが出てきた背景として、 node.js 界隈のエコシステムの進化が、比較的ゆるやかになり、洗練されてきたのがあると思う。 93 | 94 | - ESM 仕様の安定 / Webpack の事実上の標準化 95 | - ベストプラクティスとしての TypeScript の採用 96 | - eslint / prettier 97 | -------------------------------------------------------------------------------- /fragments/nextjs/00.mdx: -------------------------------------------------------------------------------- 1 | このドキュメントは Rails ガイド、 2 | 3 | 昨今、プログラミングスクールで教えられているカリキュラムを見ると、 HTML/CSS/JavaScript の次が Rails という展開が多く、 4 | 5 | - Node.js エコシステムのキャッチアップのし辛さ 6 | - Rails ガイドの完成度 7 | 8 | 個人的には next.js の生産性は眼を見張るものがあると思っているのですが、 9 | 10 | もちろん、 Rails と Next.js はカバー範囲が微妙に違うので比較できる対象ではありません。 ただし、 11 | 12 | --- 13 | 14 | # はじめに 15 | 16 | ## この本を読むと何がわかるようになるか 17 | 18 | 本書のゴールは、 node.js によるフロントエンドツールチェインを理解し、TypeScript を用いて React / Next.js のアプリケーションが開発できるようになることです。また、モダンなフロントエンド / Node.js がどういうものかを紹介するという目的があります。 19 | 20 | 第一部では、 next.js の構成要素を分解しながら、モダンなフロントエンドツールチェインについて学びます。 21 | 22 | 第二部では、第一部で学んだフロントエンド / Node.js の知識を前提に、実践的な next.js。 23 | 24 | Next.js の開発にゴールを設定する意図としては、 Next.js 自体がフロントエンド技術と Node.js の複合的なアプリケーション開発フレームワークです。個別の機能を理解せずに使うことも可能ですが、最小の環境からスタートして、ボトムアップに個別の要素を理解することで、Node.js / フロントエンドへのエコシステムの理解が進むことを期待しています。 25 | 26 | また、 next.js 上で React を TypeScript を使って記述していきます。これは HTML / DOM というものの意味論とコンポーネント概念、そして JavaScript 処理系そのものへの理解と洞察が身につくはずです。 27 | 28 | 本書では、next.js のミニマムな環境からスタートして、TypeScript / Webpack / React / Node.js と段階的に理解を深めながら、 next.js が裏側でやっていることを理解できるようになるのが目的です。なので、この本は next.js を最速で使えるようになる本ではありません。最速で使うだけなら、公式ドキュメントの GETTING STARTED を読んください。 29 | 30 | next.js の内部要素を個別に分解しながら、フロントエンドと node.js というものへの理解を深めることを目的とします。 31 | 32 | ## 本書のターゲット 33 | 34 | - 言語としての JavaScript は知っているが、 node や npm を使い方に詳しくない人 35 | - フロントエンドツールチェインに詳しくなりたい人 36 | - サーバーサイド node.js として、 Web Application Framework というものを触ってみたい人 37 | - next.js に詳しくなりたい人 38 | 39 | ## 前提 40 | 41 | 本書では ES2019 と呼ばれる仕様をターゲットにします。これは tc39(JavaScript の仕様を決める委員会) の定める、2019 年度に勧告された仕様のことです。この仕様は、Safari / Chrome / Firefox などのブラウザで問題なく動作します。Internet Exproler 11 では動作しませんが、この本を読み終わった頃には、ES2019 を ES5 と呼ばれる水準(IE がサポート)に変換することも可能になるはずです。 42 | 43 | ES2019, EcmaScript 2019 Edition, これはつまり、EcmaScript ≒ JavaScript は毎年、文法や機能が追加される言語ということです。毎年変化する言語を学ぶ必要はあるのか?と思う人がいるかも知れません。安心してください。今書いたコードが動かなくことは、セキュリティ問題が発覚したなどの稀な例外()を除いて、とても稀です。「今書いたコードが将来動くこと」を後方互換性といい、JavaScript はこの後方互換性を守ることに、とても注意が払われている言語です。 44 | 45 | ## この本で書かないこと 46 | 47 | - JavaScript の基本的な文法についての解説: [JavaScript Primer \- 迷わないための入門書 \#jsprimer](https://jsprimer.net/) を参照してください 48 | - CSS のコーディング: 本書で解説するのは最小限です。 49 | - データベース: next.js の発展形として、blitz を用いたアプリケーションの解説をしますが、データベースそのものは取り扱いません。 50 | 51 | ## 読み方 52 | 53 | 1 章はハンズオン風に next.js を vercel にデプロイして、動くものを少ない手数で作れる、という体験を重視しています。 54 | 55 | 2 ~ 5 章も同じく、写経で動かしながら、雰囲気を掴むことに注力しています。気になる場合は自分で改造してみてください。 56 | 57 | 6 章は 2 ~ 5 章 を前提に、実践的な next.js アプリケーションを実装することに注力します。 58 | 59 | 7 章は blitz というフレームワークを使いますが、まだ実験的なフレームワークなので、軽く紹介、という感じです。これは、 next.js に Rails を置き換えることを期待している人向けに紹介する、ということを意図しています。blitz 自体の開発の進捗や、同じコンセプトのフレームワークが出現した場合、変更したり削除する可能性もあります。 60 | 61 | 文法上、理解が追いつかないものがある場合、 [JavaScript Primer \- 迷わないための入門書 \#jsprimer](https://jsprimer.net/) を参考にしてください。 62 | -------------------------------------------------------------------------------- /fragments/nextjs/01.mdx: -------------------------------------------------------------------------------- 1 | # 第 1 章: Hello, Next.js! 2 | 3 | ## インストールと環境構築 4 | 5 | node/npm のインストール 6 | 7 | [ダウンロード \| Node\.js](https://nodejs.org/ja/download/) 8 | 9 | vscode のインストール 10 | 11 | [Visual Studio Code – コード エディター \| Microsoft Azure](https://azure.microsoft.com/ja-jp/products/visual-studio-code/) 12 | 13 | ## next.js プロジェクトの作成 14 | 15 | TODO: Windows での環境構築の説明 16 | 17 | ターミナルアプリを起動するか、 vscode 内で `Terminal > New Terminal` でターミナルを起動してください 18 | 19 | ``` 20 | $ mkdir hello-next-app 21 | $ cd hello-next-app 22 | $ npm init -y 23 | 24 | # ライブラリのダウンロード 25 | $ npm install next react react-dom --save 26 | $ code . # vscode でプロジェクトを開く 27 | $ code pages/index.js # エディタで開く 28 | ``` 29 | 30 | `pages/index.js` を次のように編集してください 31 | 32 | ```jsx 33 | export default () => { 34 | return Hello, Next; 35 | }; 36 | ``` 37 | 38 | ここでは、JavaScript の標準仕様に含まれない jsx という文法を使っています。詳しくは後で解説します。一旦は、ただの HTML を JavaScript の中で書くテンプレート記法だと思ってください。 39 | 40 | この時点では、こういうファイル構成になっているはずです。 41 | 42 | ``` 43 | node_modules/ 44 | pages/ 45 | index.js 46 | package-lock.json 47 | package.json 48 | ``` 49 | 50 | ## next サーバーの起動 51 | 52 | ``` 53 | $ npx next 54 | ``` 55 | 56 | ブラウザを開いて、 `http://localhost:3000/` を入力してください 57 | 58 | 次の画面が見えたら成功です。 59 | 60 | TODO: 画像を貼る 61 | 62 | ## 解説 63 | 64 | next.js は `pages/foo.js` というファイルを作成すると、 nqext は `/foo` という URL にそのファイルで `export default` されたコンポーネントを表示します。 65 | 66 | 今回作成した `pages/index.js` というファイル名は少し特別扱いされていて、`/index` もしくは `/` に対応します。 67 | 68 | ついでに `/about` という URL のページを作ってみましょう。 69 | 70 | ```jsx 71 | // pages/about.js 72 | export default () => { 73 | return ( 74 | 75 | About 76 | built by next.js 77 | 78 | ); 79 | }; 80 | ``` 81 | 82 | これで、ブラウザで `http://localhost:3000/about` というページにアクセスできるようになります。 83 | 84 | next.js の基本的な規約として `pages/` 以下に置いたファイルの、「ファイル名から拡張子を省いたもの」が、URL に対応します。 85 | 86 | ## vercel に公開してみよう 87 | 88 | vercel は next.js の開発元が運営しているホスティングサービスです。vercel の提供する CLI ツールを使って、 next.js アプリケーションを vercel に公開してみましょう。 89 | 90 | ``` 91 | $ npm install -g vercel 92 | 93 | $ vercel 94 | # ユーザー登録/メールアドレス が求められるので入力 95 | Vercel CLI 19.0.1 96 | ? Set up and deploy “~/plg/hello-next”? [Y/n] y 97 | ? Which scope do you want to deploy to? mizchi 98 | ? Link to existing project? [y/N] n 99 | ? What’s your project’s name? next-js-tutorial 100 | ? In which directory is your code located? ./ 101 | Auto-detected project settings (Next.js): 102 | - Build Command: `npm run build` or `next build` 103 | - Output Directory: Next.js default 104 | - Development Command: next dev --port $PORT 105 | ? Want to override the settings? [y/N] N 106 | 🔗 Linked to mizchi/next-js-tutorial (created .vercel and added it to .gitignore) 107 | 🔍 Inspect: https://vercel.com/mizchi/next-js-tutorial/2yh1nfshh [3s] 108 | ✅ Production: https://next-js-tutorial-iota.now.sh [copied to clipboard] [31s] 109 | 📝 Deployed to production. Run `now --prod` to overwrite later (https://zeit.ink/2F). 110 | 💡 To change the domain or build command, go to https://zeit.co/mizchi/next-js-tutorial/settings 111 | 112 | $ vercel --prod 113 | Vercel CLI 19.0.1 114 | 🔍 Inspect: https://vercel.com/mizchi/next-js-tutorial/2yh1nfshh [2s] 115 | ✅ Production: https://next-js-tutorial-iota.now.sh [copied to clipboard] [4s] 116 | ``` 117 | 118 | これは自分の環境での実行例です。最後に表示された、 Production の URL を開いてみてください。(ここでは [https://next-js-tutorial-iota.now.sh/](https://next-js-tutorial-iota.now.sh/)) を開いてみてください。 119 | 120 | この URL は誰でもアクセスすることができます。つまり、あなたが作ったウェブサイトが vercel 上で公開されました!おめでとうございます! 121 | 122 | ## 解説しなかったこと 123 | 124 | - npm とはなにか package.json とはなにか 125 | - next.js がどのようにうごいているのか 126 | - React とはなにか 127 | - JSX の文法 128 | - vercel はどのように動いているのか 129 | -------------------------------------------------------------------------------- /fragments/nextjs/02.mdx: -------------------------------------------------------------------------------- 1 | # 第 2 章: TypeScript 2 | 3 | ## TypeScript プロジェクト 4 | 5 | ``` 6 | $ mkdir hello-typescript 7 | $ cd hello-typescript 8 | $ npm install --save-dev typescript 9 | $ npx tsc --init 10 | 11 | $ code . 12 | $ code src/index.ts 13 | ``` 14 | 15 | ```ts 16 | const x: number = 1; 17 | console.log(x); 18 | ``` 19 | 20 | `: number` の部分が TypeScript による文法拡張です。 21 | 22 | これを次のように書き換えてみましょう 23 | 24 | ```ts 25 | const x: number = "hello"; // エラー 26 | console.log(x); 27 | ``` 28 | 29 | TODO: 例外の画像 30 | 31 | この規模感なら嬉しさがわかりづらいですが、大きな規模のプロジェクトでは、 import / export されたモジュールの型の検査が難しくなります。これを TypeScript を導入することで、事前に違反を検知できるようになります。 32 | 33 | 型にはドキュメントとしての役割もあります。プログラミング初心者には手数が増えるだけで、まだ型の必要性はわかりづらいかもしれませんが、現状 node.js / フロントエンドのベストプラクティスとして TypeScript の採用はほぼ必須となっています。最初から慣れていくことをおすすめします。 34 | 35 | 本テキストでは、以下 TypeScript による型を付けながら解説していくこととします。 36 | 37 | ## TypeScript の型システム 38 | 39 | TODO: TBD 40 | 41 | [JavaScript \- TypeScript Deep Dive 日本語版](https://typescript-jp.gitbook.io/deep-dive/recap) 42 | 43 | ## TypeScript の責務 44 | 45 | - TypeScript の拡張構文を除去する 46 | - 型を静的検査をする 47 | - Language Server Protocol を IDE に提供し、補完を行う 48 | 49 | よく誤解されがちな TypeScript の特徴として、 TypeScript の型は実行時の処理に何ら影響を与えません。つまりエラーがおきたときに、型だけ書き換えてそのエラーが治ることはありません。 50 | 51 | また、型エラーがないとしても、実行時の処理を完全に保証するものではありません。例を出します。 52 | 53 | ```ts 54 | const obj: { [key: string]: string } = {}; 55 | obj.x = "foo"; 56 | const bar = obj.bar; 57 | console.log(bar.length); // 実行時エラー 58 | ``` 59 | 60 | これは、 `{ [key: string]: string }` という型は、「 key に string を取り、 value に string を取る Object 型 」 という意味になるのですが(ちなみに JS では `{}` は `new Object()` と同じ意味)、JavaScript の言語仕様では、未定義のフィールドへのアクセスは例外ではなく、 `undefined` を返すという仕様があります。 61 | 62 | メジャーなユースケースのためにこの宣言自体は許容されますが、実行時エラーが発生しないことを保証するものではありません。より厳密に型を付けたければ、次のように宣言する必要があります。 63 | 64 | ```ts 65 | const obj: { [key: string]: string | undefined } = {}; 66 | ``` 67 | 68 | これは、「 key に string を取り、 value に string または undefined を取る Object 型 」 です。 69 | 70 | これは、JavaScript に後付で型制約を取り入れたため、現実的な範囲で型がゆるく宣言できるような余地が残されています。これには賛否両論があります。 71 | 72 | ## Next.js の TypeScript 73 | 74 | next.js は実は typescript compiler を使っていません。代わりに、 babel と `@babel/preset-typescript` を使って型の除去をしています。これには様々な都合があるんですが、 75 | -------------------------------------------------------------------------------- /fragments/nextjs/03.mdx: -------------------------------------------------------------------------------- 1 | # 第 3 章: webpack 2 | 3 | ## 近年の node.js の位置づけ 4 | 5 | node.js は サーバーサイド JavaScript という側面もありますが、フロントエンドツールチェイン(補助ツール)を開発としてのスクリプト言語としての役割もあります。webpack, postcss, typescript などのツールが Node.js によって書かれています。 6 | 7 | ここでは、最も使われている webpack を解説していきます。 8 | 9 | ## webpack を使ってみる 10 | 11 | フロントエンドでは webpack というツールを使ってソースコードを一つのファイルに変換することが一般的です。 12 | 13 | ``` 14 | $ mkdir hello-webpack 15 | $ cd hello-webpack 16 | $ code . 17 | $ npm init -y # package.json のボイラープレートを生成 18 | $ npm install --save-dev webpack webpack-cli 19 | $ code src/index.js 20 | ``` 21 | 22 | ```js 23 | // src/foo.js 24 | export const x = "foo"; 25 | ``` 26 | 27 | ```js 28 | // src/index.js 29 | import { x } from "./foo"; 30 | console.log(foo); // foo を表示 31 | ``` 32 | 33 | ES Modules を使った単純なコード例です。 `pages/index.js` から見て `./foo` という相対パスを解決したファイル(の拡張子省略)の, `x` という名前の export symbol を解決します。 34 | 35 | これをターミナルで実行します。 36 | 37 | ``` 38 | $ npx webpack 39 | Hash: 387d12acbec6e83159a8 40 | Version: webpack 4.43.0 41 | Time: 63ms 42 | Built at: 2020/05/21 23:49:18 43 | Asset Size Chunks Chunk Names 44 | main.js 970 bytes 0 [emitted] main 45 | Entrypoint main = main.js 46 | [0] ./src/index.js + 1 modules 73 bytes {0} [built] 47 | | ./src/index.js 47 bytes [built] 48 | | ./src/foo.js 26 bytes [built] 49 | 50 | WARNING in configuration 51 | The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. 52 | You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/ 53 | ``` 54 | 55 | dist/main.js が生成されました。 56 | 57 | ここで使った npx コマンドが何かというと、 npm からダウンロードしたモジュールは、実行可能な CLI コマンドを提供していることがあります。 58 | ファイル実体は `node_modules/.bin` 以下にあり、 npx はここにパスを通して実行しているようなものだと思ってください。 59 | 60 | これを実行してみましょう。 61 | 62 | ``` 63 | $ node dist/main.js 64 | foo 65 | ``` 66 | 67 | 実行されました。 68 | 69 | webpack をよく知ってる人ほど違和感を持ったかもしれません。`webpack.config.js` を書いていませんね。 70 | 71 | ここでは、webpack の暗黙な既定値を利用しています。無設定だと `src/index.js` をターゲットに、 `dist/main.js` にファイルを出力します。 72 | 73 | ## なぜ webpack で結合するのか? 74 | 75 | 複数の目的があります。 76 | 77 | - 古いブラウザで動くようにするため。 78 | - パフォーマンスのため 79 | - node module を扱うため 80 | - 標準にない loader を使うため 81 | 82 | ### 目的 1: 古いブラウザで動くようにするため 83 | 84 | Internet Exproler のような古いブラウザは、ES Modules をサポートしていません。このフロントエンド開発者を長らく悩ませてきたブラウザは、2020 年現在、世界においては 1.5%、日本において 5% ほどのシェアがあります。 85 | 86 | 参考: [statcounter](https://gs.statcounter.com/browser-market-share/desktop/japan/#monthly-202001-202004) 87 | 88 | 多くのサービス提供者は、まだこれらの数値を重く見ています。開発者が意思決定できる場合を除いて、Internet Exproler をサポートすることを求められることが残念ながらほとんどです。(場合によっては、これは思考停止的な慣習である場合もあります。その場合 ↑ の値を見せてください) 89 | 90 | ユーザーのリテラシーが高く見積もれる開発者用のツールや、モバイルユーザーのみを想定したサービスでは、IE 対応を落とすことができるかもしれません。IE を落とすことで増える技術的な選択肢を重要視するなら、それを説得材料にすることも可能です。 91 | 92 | いずれにせよ、開発者の意思決定の外にある要件なので、一度手軽な環境で試しておき、そのサポートコストがどれぐらいになるか見積もっておくのは大事です。この姿勢は IE 対応に限らず、複数のブラウザを相手にして環境差を埋めなければならないフロントエンド開発では大事なものです。 93 | 94 | ### 目的 2: パフォーマンスのため 95 | 96 | IE のサポートを落とした場合、最近のブラウザは ES Modules をそのまま扱うことができます。だったらそのまま使えばいいんじゃないか?と思うかもしれませんが、使ってる人は少数です。これは先に述べた互換性の問題とは別に、パフォーマンスの問題があります。 97 | 98 | 1. スクリプトをダウンロード 99 | 2. スクリプトを評価 100 | 3. `import` 文を発見したら、またダウンロードして評価 101 | 102 | これを依存の深さの回数だけ繰り返します。着目すべきは、これをネットワーク越しに行っているということです。ネットワークからスクリプトを取得するまで依存がわからず、そのネットワークは、早くても 20 ミリ秒 場合によっては 1 秒ほど必要です。 103 | 104 | TODO: 図 105 | 106 | あなたは `localhost` という特別な環境でコードを書いているということも忘れないでください。 localhost においてはネットワーク遅延なし、ファイルの読み込み速度 = ディスクからの読み込み速度ですが、ユーザーに届く環境(たとえば Vercel) にデプロイした環境では、ネットワーク越しにこの挙動を行います。遅延 x 深さで読み込みの遅延が発生します。 107 | 108 | webpack を使わない、という選択肢ももちろんあります。その場合、依存の深さが浅くなるように、事前に結合済みのスクリプトを呼ぶことになります。つまり、結局いずれかの時点でビルド処理が必要になる、ということです。 109 | 110 | 現時点では、ES Modules という仕様は IE という環境の有無に限らず、あまり使われていません。その結果 ESM をエミュレーションする webpack の重要度がさらに増しています。 111 | 112 | ### 目的 3: node_modules を扱うため 113 | 114 | commonjs と ES Modules は影響はありつつも、お互い無関係に策定されました。 115 | 116 | ファイルの相対パスや絶対パス以外にライブラリの名前が指定された時、それをどう解決するかは、ES Modules に仕様がありません。(importmaps という仕様が策定中ですが、まだ特殊な環境でしか動きません) 117 | 118 | ```js 119 | import React from "react"; 120 | ``` 121 | 122 | これは、 `node_modules/react/package.json` の `main` フィールドが指す `node_modules/react/index.js` という実体として webpack に解釈されます。 123 | この挙動は node.js の commonjs の名前解決ルールに合わせています。 124 | 125 | つまり、これは次のコードと同じです。 126 | 127 | ```js 128 | // src/index.js の場合 129 | import React from "../node_modules/react/index.js"; 130 | ``` 131 | 132 | webpack はこの名前解決を行ってくれます。 133 | 134 | webpack の挙動としては、まず package.json は `module` というフィールドがあれば、ES Modules としてそれを読み込み、`main` というフィールドがあれば、commonjs としてそれを読み込みます。 135 | 136 | commonjs の際は、 `module.exports` された対象を `export default` として扱います。これはなんらかの仕様がある挙動ではなく、 webpack の独自の挙動です。 137 | 138 | typescript でも、この挙動を再現する、 `esMobudelsInterop: true`, `moduleResolution: node` という設定を推奨します。 139 | 140 | 本ドキュメントとしては、 commonjs に深入りしません。現在のベストプラクティスではないからです。ただし、各種設定ファイルを書くのに頻繁に出現するので、とりあえず、 `export default {...}` が `module.exports = {...}` に相当する概念ということだけ覚えてください。 141 | 142 | ## 余談: commonjs と ES Modules 143 | 144 | ここで、 ES Modules ではなく、 `require(...)` と `module.exports` という文法で書かれています。これは文法ではなく、JS の言語仕様の中で module を実現した commonjs という仕様です。 145 | 146 | ES Modules 以前、2008 年から開発が始まった node.js は commonjs というモジュールシステムを採用しました。その後, 2015 年に ES2015 が策定されました。この 2 つは似ているところはありつつも、厳密には、変換規則や評価順に互換性がありません。 147 | 148 | node.js における ES Modules 対応はまだ過渡期で、 node 14 では、 `.mjs` という拡張子で ESM を扱うことができますが、 まだ制約が多く、使いやすいとは言えません。 149 | 150 | このように、 node ツールチェイン では、頻繁に commonjs の仕様で設定を記述することがあります。 151 | 152 | ## webpack.config.js を書く 153 | 154 | ちょっと脱線しましたが、 webpack の解説に戻ります。 155 | 先程は暗黙の既定値を使いましたが、実際には `webpack.config.js` というファイルで webpack の設定を記述していく事が多いです。 156 | 157 | ```js 158 | const path = require("path"); 159 | module.exports = { 160 | entry: { 161 | main: path.join(__dirname, "src/index.js") 162 | }, 163 | output: { 164 | path: path.join(__dirname, "dist") 165 | filename: "[name].js" 166 | } 167 | }; 168 | ``` 169 | 170 | `__dirname` は commonjs の既定値で、そのファイルが置かれているディレクトリの文字列です。 `path.join` による文字列結合で、 `src/index.js` に `main` という名前を与えます。 171 | 172 | `output` の設定では、同じく相対パスの dist に対して、 `[name].js` という名前でファイル名を指定します。これが先の main に当たるので、 `src/index.js` が `dist/main.js` として出力される、という意味になるわけです。 173 | 174 | これは、デフォルトと同じ挙動になります。無設定に等しいです。これを元に拡張していきましょう。 175 | 176 | ## webpack.config.js を拡張する例: ts-loader 177 | 178 | ```js 179 | // webpack.config.js 180 | const path = require("path"); 181 | module.exports = { 182 | resolve: { 183 | extensions: [".js", ".ts", ".tsx"], 184 | }, 185 | module: { 186 | rules: [ 187 | { 188 | test: /\.tsx?$/, 189 | use: "ts-loader", 190 | }, 191 | ], 192 | }, 193 | }; 194 | ``` 195 | 196 | これで webpack は `/\.tsx?$/` という拡張子(例: `.ts`, `.tsx`) のファイルを ts-loader を通して javascript に変形して読み込むようになります。 197 | 198 | 次に、 `resolve.extensions` に注目してください。 typescript は基本的に拡張子を省略することを求めます。これは変換後に `.js` として読み込まれることがあることを意図しています。なので、webpack 環境において省略可能な拡張子のリストに、`.ts`, `.tsx` を加えます。 199 | 200 | 「ts-loader を設定する」「省略可能な拡張子に `.ts(x?)` を追加する」この 2 つの設定は、TypeScript においてほぼ必須です。 201 | 202 | このように、「ES Modules の結合時に loader を通して変形する」というのが webpack の主なユースケースです。 203 | 204 | Webpack が難しい、ということが言われる際に、 プロジェクトごとにここに特殊な loader が大量に増えていき、またプロジェクトごとに異なる設定を要求されがちで、ベストプラクティスが掴みづらいというのがあると思います。 205 | 206 | ## webpack.config.js を拡張する例: style-loader 207 | 208 | 最近では css も npm として配られることが多く、webpack のバンドルの仕組みで、 js と一緒にビルドしてしまうことが多いです。 209 | 210 | 例えば、よく使われる `normalize.css` を読み込む例です。 211 | 212 | ``` 213 | npm install style-loader css-loader --save-dev 214 | npm install normalize.css --save 215 | ``` 216 | 217 | これを使う設定を書いていきましょう。 218 | 219 | ```js 220 | // webpack.config.js 221 | const path = require("path"); 222 | module.exports = { 223 | entry: { 224 | main: path.join(__dirname, "src/index.js") 225 | }, 226 | output: { 227 | path: path.join(__dirname, "dist") 228 | filename: "[name].js" 229 | }, 230 | module: { 231 | rules: [ 232 | { 233 | test: /\.css$/, 234 | use: ['style-loader', 'css-loader'], 235 | } 236 | ] 237 | } 238 | }; 239 | ``` 240 | 241 | `mudule.rules` は、 test の正規表現にマッチしたものを、 指定した loader 順番に使う、という意味です。 242 | 243 | - css-loader: CSS を JavaScript に変換して読み込みます。 (例: module.exports = `body {color: red}`) 244 | - style-loader: css-loader で変換されたオブジェクトを 「HTML の style タグとして head に挿入する」というコードに変換します。 245 | 246 | これを使ってみましょう。 src/index.js に次の行を追加します。 247 | 248 | ```js 249 | // index.js 250 | import "normalize.css/normalize.css"; 251 | ``` 252 | 253 | これによって、 「normalize.css を style タグとして `document.head` に挿入する」という挙動が実現されます。 254 | 255 | ## チャンク分割 256 | 257 | これにより、全体共通チャンク、画面ごとのチャンク、画面遷移時のそれを自動で分割してくれています。webpack のビルドチャンクの分割はとても難しく、熟練のフロントエンドエンジニアでも手こずる作業です。 258 | 259 | ## 筆者の推奨する設定 260 | 261 | ```js 262 | // webpack.config.js 263 | const path = require("path"); 264 | module.exports = { 265 | resolve: { 266 | extensions: [".js", ".ts", ".tsx"], 267 | }, 268 | module: { 269 | rules: [ 270 | { 271 | test: /\.tsx?$/, 272 | use: [ 273 | { 274 | loader: "ts-loader", 275 | options: { 276 | transpileOnly: true, 277 | }, 278 | }, 279 | ], 280 | }, 281 | ], 282 | }, 283 | }; 284 | ``` 285 | 286 | ## next.js とは何なのか 287 | 288 | ここでなぜ webpack を紹介したか、という解説です。 289 | 290 | 実は next.js は webpack と密に連携するサーバーです。なのでデフォルトの設定を持ちます。これはこれで必要な設定が入っていて、先の style-loader 相当のものは実は最初から定義されています。 291 | 292 | next.js は webpack を内蔵しています。 293 | 294 | 基本的には拡張する必要はないのですが、もし必要ならば `next.config.js` というファイルで各種 webpack の設定を拡張することができます。 295 | 296 | next.js 上で webpack の設定を拡張する場合は、次のようにします。 297 | 298 | ```js 299 | // next.config.js 300 | module.exports = { 301 | webpack(config) { 302 | config.module.rules.push({ 303 | // ... 304 | }); 305 | return config; 306 | }, 307 | }; 308 | ``` 309 | -------------------------------------------------------------------------------- /fragments/nextjs/04.mdx: -------------------------------------------------------------------------------- 1 | # 第 4 章: React 2 | 3 | 1 章で 「ただのテンプレート記法だと思ってください」といった React ですが、これは実はいろんな応用があります。 4 | 5 | ## React とはなにか 6 | 7 | React はただの HTML のテンプレート記法ではありません。React が本領を発揮するのは、サーバーサイドではなくクライアントサイドで、動的なページを作る際です。 8 | 9 | Slack や Gmail などを想像してください。最近の Web アプリケーションは、画面遷移なしでもその構成要素が激しく切り替わります。 10 | 11 | 本来、 HTML とは木構造で表される文書データです。HTML は文字列で、それを JavaScript 上で操作する API を DOM (Document Object Model) というのですが、React はこの HTML という木のオブジェクトを、先のテンプレート構文から自動で再計算するライブラリです。 12 | 13 | これだけだと何のことかわからないと思いますが、次の例で解説します。 14 | 15 | 状態 1 16 | 17 | ```html 18 | 19 | X 20 | hello 21 | 22 | ``` 23 | 24 | 状態 2 25 | 26 | ```html 27 | 28 | Y 29 | hello 30 | 31 | ``` 32 | 33 | この 2 つの状態をあるとします。 34 | 35 | - h1 の中身が `X` から `Y` に 36 | - p タグの `class` に hidden を追加 37 | 38 | これを、 DOM API を使って手動で書き換えることもできますが、 React はもっと賢い手段を提供してくれます。 39 | 40 | React では、これらの状態 1 と 状態 2 を都度から全くのゼロ組み立てます。これによって、前の状態を知る必要がありません。 41 | 42 | - 仮想的な状態 1 と 仮想的な状態 2 を構築して、比較 43 | - その差分から、本物の DOM にその操作を差分適用する処理を実行 44 | 45 | 状態 1 から 状態 2 へどのように書き換えるかは、 React が自動で計算してくれます。このとき、JSX という記法は本物の DOM を生成しているわけではなく、「仮想的な状態 1」と「仮想的な状態 2」 を組み立てていて、React は常に最新の状態を受け取り、差分を計算して画面を書き換えている、というわけですね。 46 | 47 | 色々言いましたが、React を使うと状態遷移のための差分を自分で記述する必要ない、というのがミソで、常に「今の画面はこうあるのが正しい」のを、記述するのが、ユーザーの責務となります。 48 | 49 | ## React の環境を作る 50 | 51 | ```bash 52 | mkdir hello-react 53 | cd hello-react 54 | code . 55 | npm install --save react react-dom 56 | npm install --save-dev typescript webpack webpack-cli ts-loader html-webpack-plugin webpack-dev-server @types/react @types/react-dom 57 | ``` 58 | 59 | devDependencies がややこしくなってきましたね! 初出なのは html-webpack-plugin と webpack-dev-server ですが、解説は後回しにして、先にビルドしたい React のコードを書きましょう。 60 | 61 | ```tsx 62 | // src/index.tsx 63 | import React from "react"; 64 | import ReactDOM from "react-dom"; 65 | 66 | // マウント用のElementを生成 67 | const el = document.createElement("div"); 68 | document.body.appendChild(el); 69 | ReactDOM.render(Hello, el); 70 | ``` 71 | 72 | で、このコードを動かす `webpack.config.js` を書いていきます。3 章で使った webpack の設定 + `html-webpack-plugin` というものを使います。 73 | 74 | ```js 75 | // webpack.config.js 76 | const HTMLPlugin = require("html-webpack-plugin"); 77 | module.exports = { 78 | resolve: { 79 | extensions: [".js", ".ts", ".tsx"], 80 | }, 81 | module: { 82 | rules: [ 83 | { 84 | test: /\.tsx?$/, 85 | use: "ts-loader", 86 | }, 87 | ], 88 | }, 89 | plugins: [new HTMLPlugin()], 90 | }; 91 | ``` 92 | 93 | この `html-webpack-plugin` が何かというと、webpack でのビルド時に 「ビルドされた js を script として持つ index.html」を output で指定されたディレクトリに挿入します。 94 | 95 | なので、`webpack` での出力は、次のようになります。 96 | 97 | ``` 98 | dist/ 99 | index.html # 100 | main.js 101 | ``` 102 | 103 | で、 これと組み合わせる `webpack-dev-server` を今回は使います。 webpack によるビルド対象が更新されると自動で再ビルドしつつ、output するディレクトリをルートとする静的アセットのサーバーです。 104 | 105 | 実行します。 106 | 107 | ``` 108 | npx webpack-dev-server 109 | ``` 110 | 111 | 先の dist をルートとして、 `http://localhost:8080` にサーバーが立ち上がりました。多くのサーバーと同じく、 `/` は `index.html` が返却されるため、これで `src/index.tsx` が自動でビルドされて更新される環境が実現できました。 112 | 113 | 試しに、`Hello` を `Hoge` などに書き換えて保存してみてください。画面に反映されるはずです。 114 | 115 | この html-webpack-plugin + webpack-dev-server の組み合わせは、React に限らずフロントエンドのライブラリや新しい機能を実験をする際にとても有利です。是非覚えておきましょう。 116 | 117 | ## Counter の例 118 | 119 | ボタンを押すと、値が 1 増えるという、よくあるカウンタ実装の例を示します 120 | 121 | ```tsx 122 | // src/index.tsx 123 | import React, { useState } from "react"; 124 | import ReactDOM from "react-dom"; 125 | 126 | function Counter() { 127 | const [counter, setCounter] = useState(0); 128 | return ( 129 | 130 | Counter: {counter} 131 | { 133 | setCounter((n) => n + 1); 134 | }} 135 | > 136 | +1 137 | 138 | 139 | ); 140 | } 141 | 142 | // マウント用のElementを生成 143 | const el = document.createElement("div"); 144 | document.body.appendChild(el); 145 | ReactDOM.render(, el); 146 | ``` 147 | 148 | これは Function Component という機能で、`` はその関数の実行結果で展開されます。他に Class Copmonent があるのですが、現在の主流は Function Component なので、今回は解説しません。 149 | 150 | React で状態を扱うには、 hooks という API を使います。 `useState()` は引数として初期値を受け取り、 `[現在の値, 値を更新する関数]` という配列を返します。これを分割代入という記法で、それぞれ受け取っています。 151 | 152 | `onClick={() => {...}}` という構文で、click イベントリスナーに onClick の際に実行される関数を渡しています。つまり、 ボタンをクリックするたびに `setCounter((n) => n + 1)` を実行します。setCounter の引数は 2 種類あって、「次の値」、もしくは「前の値を引数として、次の値を返す関数」です。 153 | 154 | `setCounter` が実行されると、この Counter の関数コンポーネントが、関数として再度実行されます。この時、 `const [counter, setCounter] = useState(0);` の `counter` は 1 を返します。 155 | 156 | この結果、 157 | 158 | ```tsx 159 | 160 | Counter: 1 161 | { 163 | setCounter((n) => n + 1); 164 | }} 165 | > 166 | +1 167 | 168 | 169 | ``` 170 | 171 | という仮想 DOM が生成されます。React の差分適用アルゴリズムにより、目に見える部分としては、 `Counter: 1` の部分が更新されます。目には見えませんが、 button のイベントハンドラの関数も更新されます。 172 | 173 | このように、都度差分オブジェクトをまるごと生成し、React の差分検知で必要な部分だけ書き換えが走る、というのが基本的なライフサイクルとなります。 174 | 175 | ## Component と props と TypeScript 176 | 177 | ```tsx 178 | // src/index.tsx 179 | import React, { useState } from "react"; 180 | import ReactDOM from "react-dom"; 181 | 182 | function Counter(props: { text: string; initialValue: number }) { 183 | const [counter, setCounter] = useState(0); 184 | return ( 185 | 186 | 187 | {props.text}: {props.counter} 188 | 189 | { 191 | setCounter((n) => n + 1); 192 | }} 193 | > 194 | +1 195 | 196 | 197 | ); 198 | } 199 | 200 | function App() { 201 | return ( 202 | <> 203 | 204 | 205 | > 206 | ); 207 | } 208 | 209 | const el = document.createElement("div"); 210 | document.body.appendChild(el); 211 | ReactDOM.render(, el); 212 | ``` 213 | 214 | まず `<>...>` は `Fragment` という仮想のノードで、DOM としての実体をもちません。Component として、複数の要素を返す時に便利な機能です。 215 | 216 | この Fragment の中で、 2 つの Counter を返しています。 ここでコンポーネントへの引数として、 `text` と `initialValue` という値を渡しています。`text="A"` は文字列として渡すときに使える構文で、 `{0}` は 数値型に限らず、JavaScript のオブジェクトを渡す時に使える構文です。なので、 `{"A"}` でも良いです。 217 | 218 | 次に、 Counter の関数コンポーネントの引数を見ていきましょう。ここまで tsx を使いつつ TypeScript の機能を使っていませんでしたが、ここでやっと TypeScript の型の出番です。 219 | 220 | `function Counter(props: { text: string, initialValue: number }) {...}` 221 | 222 | は、第一引数の `props` として `{ text: string, initialValue: number }` のオブジェクトを受け取る、という意味になります。慣習的に、この値は `props` と名付けることになっています。 223 | 224 | このとき、 A と B の2つの `Counter` は独自の初期値と、独自のライフサイクルを持ちます。試しに、2つのカウンターをクリックしてみてください。再描画、再更新の単位は、それぞれ独自となっているはずです。このように、 Component は展開するビューを宣言する関数であるだけでなく、ライフサイクルの単位でもあります。 225 | 226 | ## Context 227 | 228 | props は必ず親から渡す必要があります。親子間で自明ですが、これとは別に、「特定の親に依存した値」という値を参照するために、 context という機能があります。 229 | 230 | ```tsx 231 | // src/index.tsx 232 | import React, { useState, useContext } from "react"; 233 | import ReactDOM from "react-dom"; 234 | 235 | const ThemeContext = React.createContext<{ color: string }>({ 236 | color: "transparent", // 初期値。今回は使わない 237 | }); 238 | 239 | function View() { 240 | const theme = useContext(ThemeContext); 241 | return Text with color; 242 | } 243 | 244 | function App() { 245 | return ( 246 | <> 247 | 248 | 249 | 250 | 251 | 252 | 253 | > 254 | ); 255 | } 256 | 257 | const el = document.createElement("div"); 258 | document.body.appendChild(el); 259 | ReactDOM.render(, el); 260 | ``` 261 | 262 | `React.createContext` で生成された、 `ThemeContext.Provider` という要素の下では、 `useContext` すると、`` の value が取れるようになります。 263 | 264 | ## 簡易 Flux 265 | 266 | ```tsx 267 | // src/index.tsx 268 | import React, { useState, useContext, SetStateAction, Dispatch } from "react"; 269 | import ReactDOM from "react-dom"; 270 | 271 | type State = { 272 | value: number; 273 | }; 274 | 275 | const AppStateContext = React.createContext({ 276 | value: 0, 277 | }); 278 | 279 | const SetAppStateContext = React.createContext>>( 280 | () => {} 281 | ); 282 | 283 | function View() { 284 | const appState = useContext(AppStateContext); 285 | const setAppState = useContext(SetAppStateContext); 286 | 287 | return ( 288 | 289 | {appState.value} 290 | { 292 | setAppState({ value: appState.value + 1 }); 293 | }} 294 | > 295 | 296 | ); 297 | } 298 | 299 | function App() { 300 | const [state, setState] = useState({ value: 0 }); 301 | return ( 302 | <> 303 | 304 | 305 | 306 | 307 | 308 | > 309 | ); 310 | } 311 | 312 | const el = document.createElement("div"); 313 | document.body.appendChild(el); 314 | ReactDOM.render(, el); 315 | ``` 316 | 317 | これは、 View が一つしか無いので必要性がわかりづらいのですが、 大仰な Counter です。 318 | 319 | ## おわり 320 | 321 | React の機能を全部紹介すると大変な分量になるので、ここでは状態管理のためのイディオムの紹介に留めました。 322 | 323 | ## Next.js においての react 324 | 325 | React は効率的な DOM 管理のために生まれました。しかし、その発展の過程で、一つのテンプレートとしても受容されてきたという歴史があります。JSX 記法は React だけの記法ではなく、 vue.js や preact といったフレームワークでも使用することができます。 326 | 327 | next.js ではテンプレートとして React を使いつつ、クライアントサイドでの React の力を 100% 使うことができます。初期状態を初期状態の HTML として返却しつつ、その初期 HTML を引き継いで、シームレスにクライアントで動かすことができます。これは、他の Web Application Framework にない唯一無二の機能です。 328 | 329 | しかし、クライアント・サーバーの状態引き継ぎは実はとても難しいテクニックを必要とします。具体的には Server Side Rendering と、 Client での Hydration という2つを組み合わせます。 330 | 331 | 次の章では、 Server Side Node.js として、クライアントで動くようにするためのテクニックを紹介します。 332 | -------------------------------------------------------------------------------- /fragments/nextjs/05.mdx: -------------------------------------------------------------------------------- 1 | # 第 5 章: Server Side Node.js と React SSR 2 | 3 | ## はじめに 4 | 5 | node.js はフロントエンドツールチェインを扱うものとして扱ってきましたが、この章では Server Side の言語としての側面をやっていきつつ、ここまでやってきた React と協調する SSR という技術を紹介します。 6 | 7 | 先に言っておきますが、この章の内容は難しいですし、next.js というものを扱うにあたって、実際必須ではありません。ただ、知っておかないと next.js のありがたみも半減します。 8 | 9 | next.js がやってくれる SSR を、next.js でやりながら紹介しつつ、その難易度を感じることを目的とします。 10 | 11 | ## express の環境構築 12 | 13 | ``` 14 | npm install express --save 15 | npm install @types/express --save-dev 16 | ``` 17 | 18 | ```ts 19 | import express from "express"; 20 | const app = express(); 21 | 22 | app.get<{ id: string }, { foo: number }>("/foo/:id", (req, res) => { 23 | res.json({ 24 | foo: 1, 25 | }); 26 | }); 27 | 28 | app.listen(3000, () => { 29 | console.log("server strated!"); 30 | }); 31 | ``` 32 | 33 | ## SSR 34 | 35 | ```tsx 36 | // server.tsx 37 | import React from "react"; 38 | import ReactDOMServer from "react-dom/server"; 39 | import express from "express"; 40 | import path from "path"; 41 | 42 | const app = express(); 43 | 44 | type State = { value: 0 }; 45 | 46 | function Html(props: { App: React.ComponentType; initialState: State }) { 47 | const html = ; 48 | return ( 49 | 50 | 51 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | // webpack の出力ディレクトリをマウント 66 | app.use(express.static(path.join(__dirname, "dist"))); 67 | app.get<{ id: string }>("/:id", (req, res) => { 68 | res.send(); 69 | }); 70 | 71 | app.listen(3000, () => { 72 | console.log("server strated!"); 73 | }); 74 | ``` 75 | 76 | ## Client Side Hydration 77 | 78 | ```tsx 79 | // client/index.tsx 80 | import React from "react"; 81 | import ReactDOM from "react-dom"; 82 | import { App } from "./App"; 83 | 84 | ReactDOM.hydrate( 85 | , 86 | document.querySelector("#root") 87 | ); 88 | ``` 89 | 90 | ```tsx 91 | // server.tsx 92 | import React from "react"; 93 | import ReactDOMServer from "react-dom/server"; 94 | import express from "express"; 95 | import path from "path"; 96 | import { App } from "./client/App"; 97 | 98 | const app = express(); 99 | 100 | type State = { value: string }; 101 | 102 | function Html(props: { App: React.ComponentType; initialState: State }) { 103 | const html = ; 104 | return ( 105 | 106 | 107 | 114 | 115 | 116 | 117 | 118 | ); 119 | } 120 | 121 | // webpack の出力ディレクトリをマウント 122 | app.use(express.static(path.join(__dirname, "dist"))); 123 | app.get<{ id: string }>("/:id", (req, res) => { 124 | res.send( 125 | ReactDOMServer.renderToStaticMarkup( 126 | 127 | ) 128 | ); 129 | }); 130 | 131 | app.listen(3000, () => { 132 | console.log("server strated!"); 133 | }); 134 | ``` 135 | 136 | 同じ `client/App` という Component 、同じ `initialState` を使って、クライアントとサーバーで同じものを 2 回 render しています。 `initialState` をクライアントに渡すのが肝で、これによってクライアントでサーバーと同じものを再現しています。 137 | 138 | ここで難しいのが、 `ReactDOM.hydarate(...)` が成功するには、 SSR と CSR の実行が完全に一致する必要がある、ということです。もしレンダリングの過程に乱数や `Date.now()`のような時間データが混じったり、また環境によって異なるグローバル変数のアクセスが混ざったりしていると、Hydration は失敗して、初期状態の SSR の結果を破棄して CSR を再度実行します。 139 | 140 | ## SSR は何のため? 141 | 142 | 1. SEO 最適化 143 | 2. FMP 最適化 144 | 3. AMP 対応 145 | 146 | ### SEO 最適化 147 | 148 | 現状の Google Bot は即座に JavaScript を実行してくれません。SPA を作る際に問題になるのは、 149 | -------------------------------------------------------------------------------- /fragments/nextjs/10.mdx: -------------------------------------------------------------------------------- 1 | # 第 2 部: 実践 next.js 2 | 3 | ここまででやってきたことをフル活用して、 next.js をやっていく章です。この章は 4 | 5 | 1 章で紹介したのは、 next.js の機能の一部だけです。動的・静的なページを 6 | 7 | # はじめに 8 | 9 | ## next.js/examples を眺めてみよう 10 | 11 | next.js の良い点の一つに、公式の `examples` ディレクトリの豊富な作例があります。 12 | 13 | [next\.js/examples at canary · zeit/next\.js](https://github.com/zeit/next.js/tree/canary/examples) 14 | 15 | 例えば、 [next\.js/examples/cms\-contentful](https://github.com/zeit/next.js/tree/canary/examples/cms-contentful) は、バックエンドとして Contentful を使った作例です。 16 | 17 | # 第一章 Server(less) Mode 18 | 19 | ```ts 20 | // pages/api/hello.ts 21 | 22 | export default (req, res) => { 23 | res.json({ text: "hello" }); 24 | }; 25 | ``` 26 | 27 | `pages/api/*` は next.js によって `/api/hello` のルーティングにマッピングされます。 28 | 29 | ```ts 30 | const res = await fetch("/api/hello"); 31 | const data = await res.json(); 32 | console.log(data); // => {text: "hello"} 33 | ``` 34 | 35 | # 第二章 SSG モード 36 | 37 | この機能は、Contentful や Notion のような Headless と組み合わせて使うことを想定しています。 38 | 39 | [next\.js/examples/cms\-contentful at canary · zeit/next\.js](https://github.com/zeit/next.js/tree/canary/examples/cms-contentful) 40 | 41 | TBD 42 | 43 | # 第三章 AMP 44 | 45 | [next\.js/examples/amp\-first at canary · zeit/next\.js](https://github.com/zeit/next.js/tree/canary/examples/amp-first) 46 | 47 | ## ブログを作る 48 | -------------------------------------------------------------------------------- /fragments/nextjs/11.mdx: -------------------------------------------------------------------------------- 1 | # 第 7 章: blitz 2 | 3 | TODO: TBD 4 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack(config) { 3 | config.resolve.extensions.push(".mdx"); 4 | config.module.rules.push({ 5 | test: /\.mdx?/, 6 | use: [ 7 | { 8 | loader: "amdx-loader", 9 | options: { 10 | amp: true, 11 | }, 12 | }, 13 | ], 14 | }); 15 | return config; 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "license": "MIT", 4 | "scripts": { 5 | "build": "amdxg build && next build && next export && amdxg postbuild:rss && node script/generate-ogp.mjs", 6 | "dev": "amdxg build && next" 7 | }, 8 | "dependencies": { 9 | "amdx": "^0.9.0", 10 | "amdx-runner": "^0.9.0", 11 | "amdxg-components": "^0.9.2", 12 | "next": "^9.4.0", 13 | "preact": "^10.4.1", 14 | "preact-render-to-string": "^5.1.8", 15 | "react": "^16.13.1", 16 | "react-dom": "^16.13.1", 17 | "styled-components": "^5.1.0" 18 | }, 19 | "devDependencies": { 20 | "@rollup/plugin-commonjs": "^11.1.0", 21 | "@rollup/plugin-node-resolve": "^7.1.3", 22 | "@rollup/plugin-virtual": "^2.0.1", 23 | "@types/react": "^16.9.34", 24 | "@types/react-dom": "^16.9.7", 25 | "@types/styled-components": "^5.1.0", 26 | "amdx-loader": "^0.9.0", 27 | "amdxg-cli": "^0.9.0", 28 | "raw-loader": "^4.0.1", 29 | "rimraf": "^3.0.2", 30 | "rollup": "^2.9.1", 31 | "rollup-plugin-amdx": "^0.9.0", 32 | "rollup-plugin-node-builtins": "^2.1.2", 33 | "rollup-plugin-terser": "^5.3.0", 34 | "rollup-plugin-typescript2": "^0.27.0", 35 | "typescript": "^3.8.3" 36 | }, 37 | "optionalDependencies": { 38 | "canvas": "^2.6.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pages/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { Article, Layout } from "amdxg-components"; 2 | import { GetStaticProps } from "next"; 3 | import ReactDOMServer from "react-dom/server"; 4 | import pages from "../gen/pages.json"; 5 | import ssgConfig from "../amdxg.config"; 6 | import Head from "next/head"; 7 | 8 | type Props = { 9 | slug: string; 10 | toc: Array; 11 | history: Array; 12 | frontmatter: { 13 | description?: string; 14 | title: string; 15 | created: number; 16 | tags?: string[]; 17 | }; 18 | tags: string[]; 19 | html: string; 20 | }; 21 | 22 | export const config = { amp: true }; 23 | 24 | export function getStaticPaths() { 25 | const paths = pages.map((page) => { 26 | return `/${page.slug}`; 27 | }); 28 | return { 29 | paths, 30 | fallback: false, 31 | }; 32 | } 33 | 34 | export const getStaticProps: GetStaticProps = async (props) => { 35 | const slug = props.params.slug; 36 | const { default: Doc, toc, frontmatter } = await import( 37 | `../docs/${slug}.mdx` 38 | ); 39 | const { default: history } = await import(`../gen/${slug}.history.json`); 40 | return { 41 | props: { 42 | slug, 43 | toc, 44 | history, 45 | tags: frontmatter.tags || [], 46 | frontmatter: frontmatter || { title: slug, created: 0, tags: [] }, 47 | html: ReactDOMServer.renderToStaticMarkup(), 48 | } as Props, 49 | }; 50 | }; 51 | 52 | export default (props: Props) => ( 53 | <> 54 | 55 | 56 | {props.frontmatter.title} - {ssgConfig.siteName} 57 | 58 | 59 | 60 | 64 | 65 | 66 | 70 | 71 | 72 | 80 | 84 | 85 | 86 | > 87 | ); 88 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | // @ts-ignore 3 | import css from "!!raw-loader!amdxg-components/css/bundle.css"; 4 | import { ServerStyleSheet } from "styled-components"; 5 | import ssgConfig from "../amdxg.config"; 6 | 7 | export default class MyDocument extends Document { 8 | static async getInitialProps(ctx: any) { 9 | const sheet = new ServerStyleSheet(); 10 | try { 11 | const page = ctx.renderPage((App) => (props) => 12 | sheet.collectStyles() 13 | ); 14 | const initialProps: any = await Document.getInitialProps(ctx); 15 | return { 16 | ...page, 17 | styles: [ 18 | ...initialProps.styles, 19 | , 25 | ...sheet.getStyleElement(), 26 | ], 27 | }; 28 | } finally { 29 | sheet.seal(); 30 | } 31 | } 32 | 33 | render() { 34 | return ( 35 | 36 | 37 | 43 | {/* */} 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { Layout, PageList, TagList } from "amdxg-components"; 3 | import pages from "../gen/pages.json"; 4 | import ssgConfig from "../amdxg.config"; 5 | import tagmap from "../gen/tagmap.json"; 6 | 7 | export const config = { amp: true }; 8 | 9 | export default () => { 10 | return ( 11 | <> 12 | 13 | {ssgConfig.siteName} 14 | 15 | 16 | Articles 17 | 18 | Tags 19 | 20 | 21 | > 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /pages/slides/develop-mizchi-dev.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import React from "react"; 3 | import { Layout, Slide } from "amdxg-components"; 4 | // @ts-ignore 5 | import rawMdx from "!raw-loader!../../slides/develop-mizchi-dev.mdx"; 6 | import _config from "../../amdxg.config"; 7 | // @ts-ignore 8 | import Doc from "../../slides/develop-mizchi-dev.mdx"; 9 | 10 | export default () => { 11 | return ( 12 | <> 13 | 14 | next.js で自分のブログを作る 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | > 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /pages/tags/[tag].tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { Layout, TagPage } from "amdxg-components"; 3 | import { GetStaticProps } from "next"; 4 | import ssgConfig from "../../amdxg.config"; 5 | import tagmap from "../../gen/tagmap.json"; 6 | 7 | export const config = { amp: true }; 8 | 9 | export function getStaticPaths() { 10 | const paths = Object.keys(tagmap).map((tag) => { 11 | return `/tags/${tag}`; 12 | }); 13 | return { 14 | paths, 15 | fallback: false, 16 | }; 17 | } 18 | 19 | type Props = { 20 | tagName: string; 21 | pages: Array<{ title: string; slug: string }>; 22 | }; 23 | 24 | export const getStaticProps: GetStaticProps = async (props) => { 25 | const tag = props.params.tag; 26 | return { 27 | props: { 28 | tagName: tag, 29 | pages: tagmap[tag as any], 30 | } as Props, 31 | }; 32 | }; 33 | 34 | export default (props: Props) => { 35 | return ( 36 | <> 37 | 38 | 39 | {props.tagName} - {ssgConfig.siteName} 40 | 41 | 42 | 43 | 44 | 45 | > 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /pages/tags/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { Layout, TagList } from "amdxg-components"; 3 | import ssgConfig from "../../amdxg.config"; 4 | import tagmap from "../../gen/tagmap.json"; 5 | 6 | export const config = { amp: true }; 7 | 8 | export default () => { 9 | return ( 10 | <> 11 | 12 | Tags - {ssgConfig.siteName} 13 | 14 | 15 | 16 | 17 | > 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /pages/try.tsx: -------------------------------------------------------------------------------- 1 | import { AmpScript, Layout } from "amdxg-components"; 2 | import ssgConfig from "../amdxg.config"; 3 | import ssrCounter from "../public/amp-script/counter/ssr"; 4 | 5 | export const config = { 6 | amp: true, 7 | }; 8 | 9 | const host = 10 | process.env.NODE_ENV === "production" 11 | ? "https://mizchi.dev/" 12 | : "http://localhost:3000/"; 13 | 14 | function Counter(props: any) { 15 | const encoded = Buffer.from(JSON.stringify(props)).toString("base64"); 16 | return ( 17 | 22 | 27 | 28 | ); 29 | } 30 | 31 | export default () => { 32 | return ( 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/icon-16x16.png -------------------------------------------------------------------------------- /public/icon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/icon-32x32.png -------------------------------------------------------------------------------- /public/install-sw.html: -------------------------------------------------------------------------------- 1 | 2 | installing service worker 3 | 8 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "mz", 3 | "name": "mizchi.dev", 4 | "start_url": "/", 5 | "icons": [ 6 | { 7 | "src": "/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "display": "standalone", 18 | "scope": "/", 19 | "theme_color": "#005af0", 20 | "background_color": "#ffffff" 21 | } 22 | -------------------------------------------------------------------------------- /public/ogp/202005182044-awesome-next-issg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202005182044-awesome-next-issg.png -------------------------------------------------------------------------------- /public/ogp/202005271609-react-app-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202005271609-react-app-context.png -------------------------------------------------------------------------------- /public/ogp/202005280125-eval-esm-in-iframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202005280125-eval-esm-in-iframe.png -------------------------------------------------------------------------------- /public/ogp/202006061823-tiny-bundler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006061823-tiny-bundler.png -------------------------------------------------------------------------------- /public/ogp/202006091517-todays-react-jsonschema-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006091517-todays-react-jsonschema-form.png -------------------------------------------------------------------------------- /public/ogp/202006101314-switch-tsconfig-on-webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006101314-switch-tsconfig-on-webpack.png -------------------------------------------------------------------------------- /public/ogp/202006161259-try-netlify-lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006161259-try-netlify-lambda.png -------------------------------------------------------------------------------- /public/ogp/202006161500-netlify-lambda-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006161500-netlify-lambda-chromium.png -------------------------------------------------------------------------------- /public/ogp/202006210107-avoid-ie-first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006210107-avoid-ie-first.png -------------------------------------------------------------------------------- /public/ogp/amdx-0.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/amdx-0.8.png -------------------------------------------------------------------------------- /public/ogp/amp-social-share-for-hatena-bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/amp-social-share-for-hatena-bookmark.png -------------------------------------------------------------------------------- /public/ogp/hello-deno-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/hello-deno-1.png -------------------------------------------------------------------------------- /public/ogp/mdxx-0.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/mdxx-0.6.png -------------------------------------------------------------------------------- /public/ogp/mdxx-0.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/mdxx-0.7.png -------------------------------------------------------------------------------- /public/ogp/mdxx-cli-and-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/mdxx-cli-and-components.png -------------------------------------------------------------------------------- /public/ogp/mdxx-introduction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/mdxx-introduction.png -------------------------------------------------------------------------------- /public/ogp/next-amp-tailwind-postcss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/next-amp-tailwind-postcss.png -------------------------------------------------------------------------------- /public/ogp/study-next-amp-by-mdxx-ssg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/study-next-amp-by-mdxx-ssg.png -------------------------------------------------------------------------------- /public/ogp/study-recoil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/study-recoil.png -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | importScripts("https://cdn.ampproject.org/sw/amp-sw.js"); 2 | 3 | /* 4 | Checkout https://github.com/ampproject/amp-sw/ to learn more about how to configure 5 | */ 6 | AMP_SW.init({ 7 | assetCachingOptions: [ 8 | { 9 | regexp: /\.(png|jpg|woff2|woff|css|js|html)/, 10 | cachingStrategy: "CACHE_FIRST", // options are NETWORK_FIRST | CACHE_FIRST | STALE_WHILE_REVALIDATE 11 | }, 12 | ], 13 | // offlinePageOptions: { 14 | // url: "/offline", 15 | // assets: [], 16 | // }, 17 | }); 18 | 19 | console.log("start sw"); 20 | -------------------------------------------------------------------------------- /script/counter.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | import { h } from "preact"; 3 | import { useState } from "preact/hooks"; 4 | 5 | export default function Counter(props: { initialValue: number }) { 6 | const [state, setState] = useState(props.initialValue); 7 | return ( 8 | 9 | Counter Example 10 | {String(state)} 11 | setState((n) => n + 1)}>+ 12 | setState((n) => n - 1)}>- 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /script/generate-ogp.mjs: -------------------------------------------------------------------------------- 1 | import canvas from "canvas"; 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | const dirname = path.dirname(new URL(import.meta.url).pathname); 5 | 6 | const W = 600; 7 | const H = 315; 8 | const LINE_HEIGHT = 30; 9 | 10 | function getRows(ctx, text) { 11 | const words = text.split(" "); 12 | 13 | let rows = []; 14 | let currentRow = []; 15 | let tokens = words.slice(0); 16 | let token; 17 | while ((token = tokens.shift())) { 18 | const mText = [...currentRow, token].join(" "); 19 | const measure = ctx.measureText(mText); 20 | if (measure.width <= W) { 21 | currentRow.push(token); 22 | } else { 23 | rows.push(currentRow.slice()); 24 | currentRow = [token]; 25 | } 26 | } 27 | if (currentRow.length > 0) { 28 | rows.push(currentRow); 29 | } 30 | 31 | return rows; 32 | } 33 | 34 | function renderText(ctx, rows) { 35 | const rowCount = rows.length; 36 | for (let i = 0; i < rowCount; i++) { 37 | const rowText = rows[i].join(" "); 38 | const m = ctx.measureText(rowText); 39 | 40 | const w = (W - m.width) / 2; 41 | // const h = (LINE_HEIGHT + 12) * (i + 1); 42 | const h = 40 + 210 / 2 - (LINE_HEIGHT + 12) * (rowCount - i - 1); 43 | 44 | ctx.fillText(rowText, w, h); 45 | } 46 | } 47 | 48 | async function generateImage(text, outputPath) { 49 | const cvs = canvas.createCanvas(W, H); 50 | const ctx = cvs.getContext("2d"); 51 | ctx.font = `${LINE_HEIGHT}px Impact`; 52 | 53 | const rows = getRows(ctx, text); 54 | 55 | ctx.fillStyle = "white"; 56 | ctx.fillRect(0, 0, W, H); 57 | 58 | ctx.fillStyle = "black"; 59 | renderText(ctx, rows); 60 | 61 | const m = ctx.measureText("mizdev"); 62 | ctx.fillText("mizdev", (W - m.width) / 2, 250); 63 | const buf = cvs.toBuffer(); 64 | await fs.writeFile(outputPath, buf); 65 | } 66 | 67 | async function main() { 68 | const pages = JSON.parse( 69 | await fs.readFile(path.join(dirname, "../gen/pages.json")) 70 | ); 71 | await Promise.all( 72 | pages.map(async (p) => { 73 | const out = path.join(dirname, "../public/ogp/", p.slug + ".png"); 74 | await generateImage(p.title, out); 75 | console.log("[gen:ogp]", out); 76 | }) 77 | ); 78 | } 79 | main(); 80 | -------------------------------------------------------------------------------- /slides/develop-mizchi-dev.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: next.js で自分のブログを作る 3 | --- 4 | 5 | # next.js で自分のブログを作る 6 | 7 | 自分のブログとして [mizchi.dev](https://mizchi.dev) を作った話 8 | 9 | at [隅田川\.js \#1\(オンライン\) \- connpass](https://sumidagawajs.connpass.com/event/167632/) 10 | 11 | --- 12 | 13 | ## 何を作ったか 14 | 15 | このブログ自身(mizchi.dev)。スライドツールも突貫で自作 16 | 17 | ソースコード [mizchi/dev](https://github.com/mizchi/dev) 18 | 19 | --- 20 | 21 | ## Lighthouse 22 | 23 |  24 | 25 | --- 26 | 27 | ## Full AMP 28 | 29 |  30 | 31 | --- 32 | 33 | ## GA 対応 34 | 35 |  36 | 37 | --- 38 | 39 | ## Git から編集ヒストリの生成 40 | 41 |  42 | 43 | --- 44 | 45 | ## どんなブログがほしかったか 46 | 47 | - Lighthouse で満点出したい 48 | - 普通の Markdown じゃつまんないから MDX で書きたい 49 | - サーバーの運用をしたくない 50 | - next.js の最適化に乗りたい 51 | 52 | --- 53 | 54 | ## 作った 55 | 56 | - どうせ動かないし CDN 上で静的サイト + Full AMP 57 | - MDX コンパイラを自作 (amdx) 58 | - netlify + 買ったまま忘れてたカスタムドメイン(mizchi.dev) 59 | - `pages/*.tsx` が公開される仕組みを、そのまま採用 60 | 61 | --- 62 | 63 | ## (本音 64 | 65 | - next.js で静的サイト運用の知見を貯めたい 66 | - AMP の知見を貯めたい 67 | - React でガシガシ動く作例を置く場所を、自分のドメインで用意したい 68 | 69 | --- 70 | 71 | ## next.js の SSG + AMP モードの採用 72 | 73 | ```tsx 74 | // pages/foo.tsx 75 | export const config = { amp: true }; 76 | export default function Foo() { 77 | return foo; 78 | } 79 | ``` 80 | 81 | - `amp: true` で常に amp を生成 82 | - AMP canonical は常に自分自身を指す(のを next.js が勝手にやってくれる) 83 | 84 | --- 85 | 86 | ## AMP の plugin を諸々突っ込む 87 | 88 | - `amp-social-share` で twitter / facebook / hatena bookmark のシェアに対応 89 | - `amp-analytics` で GoogleAnalytics 対応 90 | - rollup + preact + amp-script で、AMP 上で動的なコンポーネントが作れる。後で何かに使う 91 | 92 | --- 93 | 94 | # AMP 用 Markdown Compiler を作ろう! 95 | 96 | --- 97 | 98 | ## AMP 環境の Markdown に求められる仕様 99 | 100 | - AMP の仕様を満たす 101 | - `img` => `amp-img` かつ、 amp layout 仕様を満たす固定幅の要素に 102 | - 数式ブロック(`$$ ~ $$`) を amp-mathml に変換 103 | - コードハイライト: **ランタイムで構文解析できない** ので、コードブロックのハイライトを、コンパイル時に済ませておく必要 104 | 105 | --- 106 | 107 | ## MDX について 108 | 109 | Markdown 中で import 構文が使える仕様 110 | 111 | ``` 112 | # Hello, MDX 113 | 114 | import Doc from "./doc"; 115 | 116 | ``` 117 | 118 | Markdown ドキュメントから、別の Markdown ドキュメントや、React.Component 119 | を展開できる。 120 | シンタックスハイライターなどのエコシステムの都合から、`.mdx`拡張子をそのまま採用したい。 121 | 122 | --- 123 | 124 | ## AMDX: Accelarated MDX 125 | 126 | - [mizchi/amdx: Accelarated MDX](https://github.com/mizchi/amdx) 127 | - remark ベースで `@mdx-js/mdx` を元に拡張 (中で使ってる babel plugin はそのままなので、構文は互換) 128 | - refract(prismjs parser) で、コンパイル時にコードブロックをトークン化 129 | - +色々 (toc, frontmatter や WebWorker で動くように等) 130 | 131 | --- 132 | 133 | ## AMDXG: AMDX による静的サイト生成ツールキット 134 | 135 | - amdx-loader: amdx の webpack 用のローダー 136 | - amdxg-components: ブログ用のデフォルトコンポーネント集。使わなくてもいい 137 | - amdxg-cli: ページの雛形や各種メタデータ生成用の CLI 138 | - amdxg-boilerplate: ただのボイラープレート。(注意: まだ安定してない。頻繁に変わる) 139 | 140 | --- 141 | 142 | ## ドキュメントの解析 143 | 144 | `docs/*.mdx` の frontmatter を解析 145 | 146 | ```yaml 147 | --- 148 | title: hello 149 | created: 1589877769475 150 | tags: [react, next, amp] 151 | --- 152 | 153 | ``` 154 | 155 | - タイトルを日付順に並べた `gen/pages.json` を生成 156 | - タグで逆引きする用の `gen/tagmap.json` を生成 157 | - git log のコミットログから `gen/*.history.json` を生成 158 | 159 | --- 160 | 161 | ## Index 162 | 163 | - `gen/pages.json` からページ一覧を表示 164 | - `gen/tagmap.json` からタグ一覧を表示 165 | 166 | --- 167 | 168 | ## ドキュメントの表示 169 | 170 | - `next.config.js` の `webpack` で `.mdx` 拡張子は amdx-loader (自作) を通すようにする 171 | - next.js の danymic routing で 対応する slug の component を動的に返す 172 | 173 | ```tsx 174 | // pages/[slug].tsx 175 | export const getStaticProps: GetStaticProps = async (props) => { 176 | const slug = props.params.slug; 177 | const { default: Doc, toc, frontmatter } = await import( 178 | `../docs/${slug}.mdx` 179 | ); 180 | const { default: history } = await import(`../gen/${slug}.history.json`); 181 | return { 182 | props: { 183 | slug, 184 | toc, 185 | history, 186 | tags: frontmatter.tags || [], 187 | frontmatter: frontmatter || { title: slug, created: 0, tags: [] }, 188 | html: ReactDOMServer.renderToStaticMarkup(), 189 | } as Props, 190 | }; 191 | }; 192 | ``` 193 | 194 | 後は amdxg-components で用意したコンポーネントに突っ込んで表示 195 | 196 | --- 197 | 198 | ## タグページ 199 | 200 | - `gen/tagmap.json` から `/tags/react` のようなページを生成して表示 201 | 202 | ```tsx 203 | // pages/tags/[tag].tsx 204 | export function getStaticPaths() { 205 | const paths = Object.keys(tagmap).map((tag) => { 206 | return `/tags/${tag}`; 207 | }); 208 | return { 209 | paths, 210 | fallback: false, 211 | }; 212 | } 213 | export const getStaticProps: GetStaticProps = async (props) => { 214 | const tag = props.params.tag; 215 | return { 216 | props: { 217 | tagName: tag, 218 | pages: tagmap[tag as any], 219 | } as Props, 220 | }; 221 | }; 222 | ``` 223 | 224 | --- 225 | 226 | ## 自分の環境で遊ぶ 227 | 228 | ```bash 229 | $ npx degit mizchi/mdxx/templates/blog myblog 230 | $ cd myblog 231 | $ git init && git commit -m "Init" # 編集履歴生成に git history を使う 232 | $ edit amdxg.config.js # メタデータを編集 233 | 234 | # 書く 235 | $ npm i -g amdxg-cli 236 | $ amdxg new:page new-article # docs/new-article.mdx に記事の雛形を生成 237 | $ npx run dev # localhost:3000 でプレビューしながら記事を編集 238 | 239 | # build / deploy 240 | 241 | $ npx run build # out/ に静的サイトを生成 242 | 243 | # 要: netlify account 244 | $ npm i -g netlify-cli 245 | $ netlify deploy -d out --prod 246 | ``` 247 | 248 | --- 249 | 250 | ## デモ 251 | 252 | --- 253 | 254 | ## 今後やること 255 | 256 | - CSS が雑 257 | - amxd のプレビュー環境 (mdbuf.netlify.com ベース) 258 | - ドキュメントサイト 259 | - vercel 上で任意のバックエンド(CMS)から Incremental SSG する例を作る 260 | 261 | [Incremental Static Regeneration で実現する次世代のサーバーアーキテクチャ \- mizdev](https://mizchi.dev/202005182044-awesome-next-issg) 262 | 263 | --- 264 | 265 | ## おわり 266 | 267 | Google 検索結果からの遷移が爆速 💪 268 | 269 | (CSS が苦手なので助けて) 270 | -------------------------------------------------------------------------------- /text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/text.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "exclude": ["node_modules"], 18 | "include": [ 19 | "next-env.d.ts", 20 | "**/*.ts", 21 | "**/*.tsx", 22 | "decls.d.ts", 23 | "scripts/create-item.js" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------
built by next.js
hello