blog.takurinton.dev

4日間でポートフォリオを作り替えた

2020-11-14

こんにちは

どうもこんにちは。僕です。

最近あったことといえば、Vのインターン落ちて落ち込んでるところに人事の方からのなぐさめのDMがきてさらに泣きそうになったところでRからの内定もらってなんだかメンタルが忙しいことです。

今回は僕がポートフォリオを作り替えた話(需要あるのか?)について話していこうと思います。

なんで作り替えたの?

まずここから

作り替えた理由としては3つあります。

  • Next.jsを試したかった
  • 月曜日にあった勉強会でNextの話出てきて感化されちゃった(元々やりたいとは思ってた)
  • SSR真面目にやりたかった
  • OGPの動的な変更をしたかった(それ用の部分的に使えるライブラリが出てきてメジャーになってもいいのかなと思ってる)
  • ドメインを無駄に増やしたくない
  • 今までサブドメイン使っていろいろサイト公開してきた
  • お金かかるし管理めんどくさいので1つに統合したかった
  • 暇だった
  • 暇でした
  • とまあこんな感じで、思いつきで作った感じになりました。

    テストとか課題とかあったのですが睡眠時間を削ることでなんとか4日間(火曜から金曜)で仕上げることができました。

    環境構築

    まずは環境構築です。特にツールなどは使わずに適当にやりました。(create-next-appとやらをだいぶ開発を進めてから知ったので使ってないだけ)

    また、package.jsonについてはライブラリ管理がめんどくさいので以前のReact製のブログで使用していたものをそのまま流用しました。

    nextはインストールされていなかったので追加し、scriptsの部分はNext用に変更しました。

    // package.json
    
    {
      "name": "portfolio",
      "version": "0.1.0",
      "private": true,
      "dependencies": {
        "@material-ui/core": "^4.11.0",
        "@material-ui/icons": "^4.9.1",
        "@testing-library/jest-dom": "^4.2.4",
        "@testing-library/react": "^9.3.2",
        "@testing-library/user-event": "^7.1.2",
        "@types/highlight.js": "^9.12.4",
        "@types/jest": "^24.0.0",
        "@types/marked": "^1.1.0",
        "@types/react-dom": "^16.9.0",
        "@types/react-router-dom": "^5.1.5",
        "aria-query": "^4.2.2",
        "fontsource-roboto": "^3.0.3",
        "highlight.js": "^10.2.1",
        "marked": "^1.2.0",
        "next": "^10.0.1",
        "now": "^20.1.2",
        "query-string": "^6.13.6",
        "react": "^16.14.0",
        "react-dom": "^16.14.0",
        "react-highlight": "^0.12.0",
        "react-highlight.js": "^1.0.7",
        "react-intersection-observer": "^8.30.1",
        "react-markdown": "^4.3.1",
        "react-scripts": "3.4.3",
        "react-syntax-highlighter": "^15.2.0"
      },
      "scripts": {
        "start": "next start",
        "build": "next build",
        "dev": "next dev",
      },
      "eslintConfig": {
        "extends": "react-app"
      },
      "browserslist": {
        "production": [
          ">0.2%",
          "not dead",
          "not op_mini all"
        ],
        "development": [
          "last 1 chrome version",
          "last 1 firefox version",
          "last 1 safari version"
        ]
      },
      "devDependencies": {
        "@types/node": "^12.19.3",
        "@types/react": "^16.9.56",
        "@zeit/next-sass": "^1.0.1",
        "@zeit/next-typescript": "^1.1.1",
        "babel-plugin-styled-components": "^1.11.1",
        "node-sass": "^5.0.0",
        "typescript": "^3.9.7"
      }
    }
    
    npm i
    npm build
    

    これでおけまるです。

    簡単な設定を行う

    まず、ルーティングについてですが。Nextはルーティングをよしなにしてくれるダイナミックルーティングとかいうやつがあるらしいのでそれに従うことにしました。

    こやつは```pages/```直下にあるものを勝手にやってくれるみたいです。

    また、共通部分に関しては_app.tsxと_document.tsxというものがあり、これを```pages/```の下におくとルーティングとは別にheadやhtml、共通部分の設定をしてくれるみたいです。

    てことでそこから書いていきたいと思います。

    _app.tsxと_document.tsxの定義を行う

    _app.tsxでは共通部分の定義を行っています。先ほどheadと言いましたが、自分は動的に変えたいのでそれは別定義していますのでここにはありませんが、いい感じに外側を定義することができます。

    // _app.tsx
    import React from "react";
    import { Container } from "next/app";
    import { Layout } from '../component/common/layout/Layout'
    
    const App = ({ Component, pageProps }) => {
        React.useEffect(() => {
          const jssStyles = document.querySelector('#jss-server-side');
          if (jssStyles) {
            jssStyles.parentElement.removeChild(jssStyles);
          }
        }, []);
        
        return (
          <>
            
                
                  
                
             
          
          );
    }
    
    export default App
    

    _document.tsxではbodyやhtmlのタグの設定がメインになります。ではlighthouseのaccessibilityが下がってしまうのでここでしっかり定義します。

    ここのMainやHtmlが何を示すのかはまだしっかり理解できていませんが、とりあえず必要らしいので定義します(あとでドキュメント読む)

    また、プチハマりしかけたのですが、この_document.tsxで定義するクラスはNextDocumentを継承しないといけないらしいです。

    // _document.tsx
    import * as React from "react";
    import NextDocument, { DocumentContext, Html, Head, Main, NextScript } from "next/document";
    
    interface Props {}
    
    class Document extends NextDocument {
        render() {
          return (
            
              
              
                
    ) } } export default Document

    Layoutを簡単に定義する

    また、自分はLayoutコンポーネントを作ってそこに共通部分を記述し、_app.tsxで使用しました。

    // Layout.tsx
    import { Header } from '../parts/Header'
    import { HtmlHead } from '../Head'
    
    export const Layout = (props) => (
        <>
            
            
            
    {props.children} );

    Headを定義する

    headはOGPやタイトルの関係もあり動的に変更したいと思っていたのでここで動的に定義することができるようにします。

    // Head.tsx
    import Head from 'next/head';
    import { HeadProps } from '../../props/props'
    
    export const HtmlHead = ({ title, description, image, url }: HeadProps) => {
      return (
        
          {title}
          
          
          
          
          
          
          
          
          
          
          
          
          
          
             
          
      )
    }
    

    propsを定義する

    TypeScriptでは型の定義が必要なので、自分は使うpropsは全部```props/props.ts```に入れてそこから適宜引っ張ってくるみたいな構成にしています。バックエンドは出来上がっているのでそれに合わせたものを先に作成します。

    // props.ts
    
    export interface InternProps {
        id: number, 
        company_name: string, 
        overview: string, 
        period: string 
    }
    
    export interface MadeProps {
        id: number, 
        name: string,
        url: string, 
        explanation: string
    }
    
    export interface SkillProps {
        id: number, 
        name: string
    }
    
    export interface MineProps {
        content: string
    }
    
    export interface PortfolioProps {
        intern: InternProps[]
        skill: SkillProps[], 
        made: MadeProps[], 
        mine: MineProps
    }
    
    export interface Dairyreport {
        id: number, 
        pub_date: string
    }
    
    export interface DairyreportProps {
        next: number | null, 
        prev: number | null, 
        results: Dairyreport[]
    }
    
    export interface DeiryreportPost {
        pub_date: string, 
        contents: string
    }
    export interface DairyreportContent {
        comment: [], 
        post: DeiryreportPost
    }
    
    export interface HeadProps {
        title: string;
        description: string;
        image: string;
        url: string;
    }
    
    export type GetPost = {
        next: string, 
        previous: string, 
        total: number, 
        category: any,
        current: number, 
        results: PostProps[], 
        page_size: string, 
        first: string, 
        last: string
    }
    
    // post
    export type PostProps = {
        id: number,
        title: string, 
        category: string,
        contents: string, 
        contents_image_url: string,
        pub_date: string,
        comment: CommentProps[]
    }
    export const initialPost:PostProps = {
        id: 0,
        title: '', 
        category: '', 
        contents: '',
        contents_image_url: '', 
        pub_date: '', 
        comment: []
    }
    
    // category
    export type CategoryProps = {
        category: string[]
    }
    export const initialCategory:CategoryProps = {
        category: []
    }
    
    
    // comment
    export interface CommentProps {
        name: string, 
        contents: string, 
        pub_date: string
    }
    export const initialCommentState:CommentProps = {
        name: '', 
        contents: '', 
        pub_date: ''
    }
    export const initialComment:CommentProps[] = [
        {
            name: '', 
            contents: '', 
            pub_date: ''
        }
    ]
    
    // search
    export type TypeSearch = {
        content: string, 
    }
    export const initialSearch = {
        content: '', 
    }
    

    ルーティングしてく

    やっと最初にやっておくと幸せになれる定義が終わったので先述したダイナミックルーティングやっていきます!

    ディレクトリ名に依存するみたいです。

    例えば、

    pages/index.tsx  →  https://domain.com 
    pages/blog/index.tsx  →  https://domain.com/blog
    pages/blog/[id].tsx  →  https://domain.com/blog/{id}
    
    のような形になります。もし動的なルーティングを行いたかったら[any].tsxのようなファイルを作成するとそのような形のルーティングになります。よさげ〜〜〜。 # GET Requestしてく NextではgetInitialPropsという関数で情報をフェッチしてSSRしてくれるらしいです。 てことで積極的に利用していこうと思います(共通化とかした方がいいのかな??) まずはルートからやっていきます。 ルートにブログも置いてるので、欲しい情報はブログ関連になります。ブログのエンドポイントを叩いたらこんな感じになりました。 HomeとBlogはそれぞれトップ画面とブログの投稿一覧を表示するためのコンポーネントになっています。 内容についてはGitHubにあるので気になる方は確認してみてください。
    // pages/index.tsx
    import { Home } from '../component/main/Home'
    import { Blog } from '../component/blog/Blog'
    
    const Main = ({ res }) => {
        return (
            
    ) } Main.getInitialProps = async (context) => { const query = context.asPath.split('?').length === 1 ? '' : '?' + context.asPath.split('?')[1] // 汚いので要修正 const res = await fetch(`https://api.takurinton.com/blog/v1/${query}`) const response = await res.json() return { res: response, q: query } } export default Main

    次はブログの記事詳細についてみてみたいと思います。

    ブログの詳細については```domain/post/{id}```で取得したかったので```pages/post/[id].tsx```という場所にファイルをおきました。

    それぞれのコンポーネントの中身については割愛しますが、ここではブログの情報をフェッチしてきて内容をBlogDetailというコンポーネントに渡しています。BlogDetailではマークダウンの処理やコメントコンポーネント(あとで出てくる)を呼び出しています。基本的には可愛くする処理です。

    また、補足ですがgetInitialPropsは引数をとることができ、その引数にはcontextが渡されます。 

    ドキュメントには以下のように書いてあります。

    Context Object
    getInitialProps receives a single argument called context, it's an object with the following properties:
    
    - pathname - Current route. That is the path of the page in /pages
    - query - Query string section of URL parsed as an object
    - asPath - String of the actual path (including the query) shown in the browser
    - req - HTTP request object (server only)
    - res - HTTP response object (server only)
    - err - Error object if any error is encountered during the rendering
    

    それを踏まえた上で[id].tsxを書いていきます。

    // pages/post/[id].tsx
    
    import { BlogDetail } from '../../component/blog/BlogDetail'
    
    import { PostProps } from '../../props/props'
    
    const Post = (props: PostProps) => {
      return (
        
      )
    }
    
    Post.getInitialProps = async (context) => {
        const { id } = context.query
        const res = await fetch(`https://api.takurinton.com/blog/v1/post/${id}`)
        return await res.json()
    }
    
    export default Post
    

    P.S.

    記事書いてる途中に知った。

    getInitialProps以外にもなんちゃらPropsあるみたいだけどなんも調べてないしなんもわからんから全部getInitialPropsで作ってる無能です✌️

    Post Requestしてく

    Get Requestが多すぎてPost Requestを実装する頃には僕の心は死んでいたので脳死で副作用hooksを使用してpostを投げていました(正攻法なのか、Nextでpostするようのメソッドがあるかどうかはググってない、あとでやるあとで)

    今回のpostはコメントを投稿する部分で実装しました。

    (個人ブログでpost投げる時ってそれくらいしかなさそう)

    まずはhooksから定義していきます。

    hooksにrequestを投げる関数まで入れちゃった(よくないけど)

    ここは特に不思議な使い方はしてなくて、postComment関数に必要な情報を入れてpostするみたいな感じになっています。

    hooks/useComment.ts
    import React, { useState } from 'react'
    import { initialCommentState } from '../props/props'
    
    const postComment = async (body:object, id:number) => {
        await fetch(`https://api.takurinton.com/blog/v1/comment/${id}`, {
            method: "POST",
            headers: {
                'Content-Type': 'application/json'
            },
            credentials: "same-origin",
            body: JSON.stringify(body), 
        })
        return
    }
    
    export const useComment = () => {
        const [state, setState] = useState(initialCommentState)
    
        const handleChange = (e: React.ChangeEvent) => {
            setState({...state, [e.target.name]: e.target.value})
        }
        
        const handleSubmit = (body:object, id:number) => {
            postComment(body, id)
            setState(initialCommentState)
        }
    
        return {
            handleChange, 
            handleSubmit, 
            state, 
        }
    }
    

    コメントと入力フォームを表示するためのコンポーネントを作成します。

    ここではhooksを呼び出してCommentFormコンポーネントに渡しています。

    また、CommentFormコンポーネントでは可愛い入力フォームを定義しています。

    CommentContentコンポーネントでは1つ1つのコメントを表示するための処理(これももちろん可愛い)を定義しています。

    // Comment.tsx
    import { CommentContent} from './CommentContent'
    import { CommentProps } from '../../../props/props'
    import { useComment } from '../../../hooks/useComment'
    import { CommentForm } from './CommentForm'
    const css = require('../../../styles/style/input.scss')
    
    export const Comment = (props: { postId: number, comment: CommentProps[] }) => {
        const {
            handleChange, 
            handleSubmit, 
            state
        } = useComment()
    
        const onChange = (e: React.ChangeEvent) => {
            handleChange(e)
        }
    
        const onSubmit = (e: React.FormEvent) => {
            e.preventDefault()
            handleSubmit(state, props.postId)
            props.comment.push(state)
        }
    
        return (
            
    { props.comment.map(c => ( )) }
    ) }

    CommentFormコンポーネントとCommentContentコンポーネントはそれぞれ以下のような形になっています。

    特に変わったことはしていないので説明は割愛します。

    // CommentForm.tsx
    
    import { Submit } from '../../common/atoms/Submit'
    
    export const CommentForm = (value: {state: any, onChange: any, onSubmit: any }) => {
        return (
            

    コメント