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はルーティングをよしなにしてくれるダイナミックルーティングとかいうやつがあるらしいのでそれに従うことにしました。

` `

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

_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を定義する

`

// 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

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

`

それぞれのコンポーネントの中身については割愛しますが、ここではブログの情報をフェッチしてきて内容を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 (
        

コメント