こんにちは。ゆうたと申します。
タイトルの通り、プログラミング初心者の私がポートフォリオ用のwebサイトを作ったので紹介させていただきます。
それにあわせて独学の苦悩なども書きたいと思います。
わたしと同じ実務未経験でプログラミングをやっている人は共感できることも多いと思います。
この記事は、
- only text(React/firebaseで制作したWebサイト)の紹介
- 未経験でたった一人で制作することの苦悩
の構成で書いていきます。
この記事を読んでくださっている方のほとんどはわたしより経験や知識が豊富だと思うのでそういった方々のご指摘点があればコメントしてもらえるとうれしいです。
only text(React/firebaseで制作したWebサイト)の紹介
only text
githubはこちらです↓
https://github.com/yuutauh/first
このサイトは文字のみの投稿を重視したソーシャルメディアを目指したWebサイトです。
ホーム画面や投稿の詳細では投稿者にモザイクがしてあり、いいねをしたときにモザイクが解除され投稿者がわかるようになっています。
なんでこんな仕様にしたかというと、インスタやtwitterをはじめとしたソーシャルメディアは画像やショート動画などで溢れかえっています。
投稿内容は似たり寄ったりなのに有名な人だけが評価を独り占めしている印象を受けます。
そのため文字のみで内容を判断して、ホーム画面では誰によって投稿されたかもわからないようにすればそこにソーシャルメディアとして新しさがあるのかなと思い実装しました。
使用した技術
React
Firebase
only textの画面ごとの機能
デザインについて
とても悩んで探しまくった上選ばれたのはneumorphismでした。
パソコンにもスマホにも対応しています。レスポンシブデザインです。
ホーム画面
ホーム画面では投稿されたスレッドを新しい順にソートしています。
ページネーションに関して、twitterのようにスクロールするだけで次のスレッドを表示する無限スクロールをしています。
投稿画面
・文字列のみの投稿となります。250文字以上はバリデーションをかけています。
・タグ付けとタグの作成ができます。やりやすいようにタグの検索
・制作したタグは自動でスレッドに付与され、投稿後はタグリストに保存されてみんなが使えるようになります。
・制作したタグとすでにタグリストにあるタグの名称が重複した場合は自動的にすでにタグリストにあるタグが付与されます(同じ名前のタグが何個もあるのを防ぐため)
・一つの投稿につき5つまでタグ付けできます。
・匿名(初回アクセス時に自動で匿名ログインされます)での投稿も可能です。
タグリストとタグの詳細
・投稿画面で制作されたタグの一覧が表示されています
・ホーム画面と同じく無限スクロール仕様
・タグの詳細画面は各ユーザーがタグをフォローできるようになっていて、フォロワー順、投稿が新しい順、古い順にソートできます
詳細画面
・高評価(いいね)や低評価(いやだね)コメントをすることができます。
・付与したタグが表示されコメントにも高評価、低評価できます。
・hostingとfunctionsを連携してこのページのみSSRして動的にしています。
・Twitterへの共有も可能です。共有内容はシンプルにスレッドの文字列と画像です。↓
プロフィール画面
・ユーザーのサムネはログインしたアカウントに依存します。
・フォロワーとフォローのユーザーリスト
・フォローしたタグの表示
・ユーザーが投稿したスレッドとお気に入りしたスレッドの一覧
・無限スクロール などなど
*DM(ダイレクトメッセージ)機能は追加予定です...
技術面
firebaseの基本的なセットアップは割愛します。
ルーティングは以下の通りです。
react-router-domをつかってルーティングしてます。
//ルーティング以外のコードは省略しています
import { BrowserRouter, Route, Switch } from 'react-router-dom';
const Home = () => {
return(
<BrowserRouter>
<Switch>
<Route path="/body/:body">
//詳細画面へ
<ShowBubble />
</Route>
<Route path="/tags/:tag">
//タグの詳細画面へ
<Tags />
</Route>
<Route exact path="/profile/:profile">
//各ユーザーにプロフィール画面へ
<Profile />
</Route>
<Route path="/search">
//検索画面へ
<Search />
</Route>
<Route path="/tagindex">
//タグの一覧画面へ
<TagIndex />
</Route>
<Route exact path="/login">
//ログイン画面へ
<Login />
</Route>
<Route exact path="/tos">
//利用規約へ
<Tos />
</Route>
<Route exact path="/privacy">
//プライバシーポリシーへ
<Privacy />
</Route>
/*ログインしてない場合はログイン画面に遷移するようにしてますが
匿名ログインできるため不要でした */
<InputRouter path="/input" component={Input} />
<Route path="/">
//Feedsとなっていますがホーム画面です
<Feeds />
</Route>
</Switch>
</BrowserRouter>
)
}
Firebase Authenticationとユーザー周り
- 先ほどのルーティングでユーザー情報取得のために工夫します。
->React.createContext
でAuthContext
を作成
->onAuthStateChanged
で取得したcurrentUser
をAuthContext
のvalue
に設定
->Home
コンポーネントをAuthContext
でラップ
->currnetUser
はAuthContext
下のどの階層でも参照できるようになります
//Auth以外のコードは省略しています
//AuthContextを作成
export const AuthContext = React.createContext();
export const Auth = ({children}) => {
const [loading, setLoading] = useState(true);
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
//ユーザー情報を取得。currentUserステートにuserを更新
const unsubscribe = auth.onAuthStateChanged((user) =>{
//ユーザーがログインしていない場合には匿名ログイン
if(!user) {
fb
.auth()
.signInAnonymously()
.then((user) => {
setCurrentUser(user);
setLoading(false)
})
} else {
setCurrentUser(user);
setLoading(false)
}
})
return unsubscribe
}, []);
return (
//valueにcurrentUserを設定。
<AuthContext.Provider value={{currentUser}}>
//children = home.js
{ !loading && children }
</AuthContext.Provider>
)
}
useContextについてはこちらの公式サイトを参考にしてください↓
https://ja.reactjs.org/docs/context.html
- ログインが成功した場合、投稿やフォロワーなどユーザー情報を保存しなければならないのでfirestoreの
user
コレクションに保存します。
->signInWithPopup(googleProvider or twitterProvider)
からユーザー情報を取得
->firestoreのset
メゾットでユーザー情報をuser
コレクションに保存
->すでにログインしているユーザーを重複して保存しないためにmergeオプションをつけます
//googleログインの関数
const authWithGoogle = () => {
fb.auth()
.signInWithPopup(googleProvider)
.then((user) => {
if (!user) {
return;
}
const o = {
uid: user.user.uid,
displayName: user.user.displayName,
photoURL: user.user.photoURL,
};
//mergeオプション追加
db.collection("users").doc(user.user.uid).set(o, { merge: true });
})
};
//twitterログインの関数
const authWithTwitter = () => {
fb.auth()
.signInWithPopup(twitterProvider)
.then((user) => {
if (!user) {
return;
}
const o = {
uid: user.user.uid,
displayName: user.user.displayName,
photoURL: user.user.photoURL,
};
db.collection("users").doc(user.user.uid).set(o, { merge: true });
})
};
もっと上手な方法があると思います。
Firestore
スレッド及びタグのデータ構造は以下の通りです。
{
"threads": [
{
"userimage": "", //ユーザーのサムネ
"favorites": [], //お気に入りしたユーザーのuidの配列
"body": "", //投稿された文字列
"favoriteCount": 0, //お気に入りされると+1
"tagname": [], //付与されたタグの名前の配列
"badCount": 0, //いやだねされると+1
"username": "", //投稿したユーザーの名前
"uid": "", //投稿したユーザーにuid
"id": "", //uuid()でランダムの文字列
"bads": [], //いやだねしたユーザーのuidの配列
"created": { "seconds": 1665565359, "nanoseconds": 661000000 }
},
//以下略
]
{
"tags": [
{
"id": "", //uuid()でランダムの文字列
"userId": [], //フォローしたユーザーのuid
"userCount": 0, //フォローされたら+1
"created": { "seconds": 1664349563, "nanoseconds": 399000000 },
"name": "", //タグの名前
"threadId": [],
"threadCount": 0 //付与された投稿の個数
},
//以下略
]
}
- inputされた文字列が
body
に格納されます。 - 投稿画面で指定されたタグを
tagname
に格納します。 - 付与されたタグがタグリストにあるタグ or 新しく作成されるタグで挙動が分かれます。
-
タグリストにあるタグの場合、
threadId
が更新されてスレッドのid
が追加されます。 - 新しく作成されるタグの場合、firestoreのtagコレクションに新しくtagが作成されます。
-
firestoreセキュリティールールのコードの公開は避けますが、
threads
とtags
にそれぞれデータスキーマが正しいか ,正しくログインされているか、ユーザーがどの認証プロバイダーを利用しているか などなど設定してあります。
//匿名,google,twitter認証であるか判定する
function isUserValid(auth) {
return auth.token.firebase.sign_in_provider in ['twitter.com', 'google.com','anonymous']
}
Firestore Security Rulesについてこちらを参考にさせていただきました。
- Firebase Authentication 7つの落とし穴 - 脆弱性を生むIDaaSの不適切な利用
- Firestore Security Rules Setup | Advanced Firebase/Firestore
SSR(サーバーサイドレンダリング)
-
firebase hostingとcloud functionsを連携させて
/body/{bodyId}
のパスにリクエストが来た場合、returnHtmlWithOGP
をよびだしています。 -
/body/{bodyId}
のパスは投稿の詳細画面になっています。 - 公式にもやり方があるので参考までに↓
"hosting": {
//省略
"rewrites": [
{
"source": "/body/*",
"function": "returnHtmlWithOGP"
},
{
"source": "**",
"destination": "/index.html"
}
]
},
exports.returnHtmlWithOGP = functions.https.onRequest((req, res) => {
res.set('Cache-Control', 'public, max-age=300, s-maxage=600')
// Access URL '/body/{bodyId}'
const [, , bodyid] = req.path.split('/')
const domain = 'https://onlytext.net'
let indexHTML = fs.readFileSync('./hosting/index.html').toString()
db.collection('threads').doc(bodyid).get()
.then((snapshot) => {
const thread = snapshot.data()
let body = thread.body
indexHTML
.replace(/\<title>.*<\/title>/g, '<title>' + body + '</title>')
.replace(/<\s*meta name="description" content="[^>]*>/g, '<meta name="description" content="' + body + '" />')
.replace(/<\s*meta property="og:title" content="[^>]*>/g, '<meta property="og:title" content="' + body + '" />')
.replace(/<\s*meta property="og:url" content="[^>]*>/g, '<meta property="og:url" content="' + domain + '" />')
.replace(/<\s*meta property="og:description" content="[^>]*>/g, '<meta property="og:description" content="' + body + '" />')
.replace(/<\s*meta name="twitter:card" content="[^>]*>/g, '<meta name="twitter:card" content="summary_large_image" />')
res.status(200).send(indexHTML)
})
.catch(err => {
res.status(404).send(indexHTML)
})
})
SSRについてはこちらの記事を参考にさせていただきました
検索機能
- firebaseで検索するには外部エンジンが必要でお金がかかるらしいのでそれらを避ける形にしました。
- 外部エンジンを使っていないので検索ワードの最初の文字が一致しないとダメな大変厳しい仕様。
const SearchProfile = ({ index, setIndex }) => {
const [search, setSearch] = useState("");
const [result, setResult] = useState([]);
const [error, setError] = useState("");
//省略
const SearchTag = (term) => {
if (typeof term == "string") {
const searchTerm = term.toLowerCase();
const strlength = searchTerm.length;
const strFrontCode = searchTerm.slice(0, strlength - 1);
const strEndCode = searchTerm.slice(strlength - 1, searchTerm.length);
const endCode =
strFrontCode + String.fromCharCode(strEndCode.charCodeAt(0) + 1);
db.collection("users")
.where("displayName", ">=", searchTerm)
.where("displayName", "<", endCode)
.limit(10)
.get()
.then((docs) => {
if (docs.size == 0) {
setError("該当するユーザーがいません...");
setResult([]);
} else {
const items = [];
docs.forEach((doc) => {
items.push(doc.data());
});
setResult(items);
setError("");
}
});
}
};
return(
<>
//省略
<input
placeholder="user を検索"
type="text"
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<button onClick={() => { SearchTag(search) }}>
<i className="uil uil-search"></i>
</button>
</>
)
}
- 正直に言って、なぜこのコードで検索ができるのか細かい説明ができません泣(firestoreの仕様なんでしょうか?)
- こちらのサイトを参考にしました
Google Firestore: Query on substring of a property value (text search)
無限スクロール
- こちらのサイトを参考にしました
React & Infinite Scroll - IntersectionObserver
反省点
- バズる要素がなにもないこと。
- Reactをあまり生かせていない、理解しきれていない(コンポーネントにすべきところを何度も同じ処理をかいてしまっている)
- 変数、関数、CSSの命名が雑でコードの訂正が大変。他人が見たら絶対にわからないです。
- アニメーションがない
などなど
一人で制作することの苦悩
-
決まらないデザイン
欲をかいて一向にすすまない。
figmaやdribbble、pinterest を参考して、いざ自分のサイトに実装してみると、なんかピンと来ない。ピンとくるまで自分でなんとか修正する。
→さらに良さそうなのがあったこれにしよう
→いざ実装、なんかピンと来ない、修正
→さらに良さそう…
の無限ループ
CSSを忘れるのでその度に検索
どこかであきらめるしかないです -
実装したい仕様の検索が大変
例えば、投稿された内容をtwitterに共有したいとします。
そこでtwitter 共有などなど色々検索するわけですが、なかなか欲しい情報が手に入りません。
この場合twitterに限らずSNSに投稿をシェアしたい場合はOGPの設定をすることで実装できOGPと検索することで欲しい情報が手に入るわけですが、
OGPという単語に行き着くまでが独学だととても大変です(調べてから実際にコードを書くのも大変ですが)
私の場合、
firebase関連の言葉(今でも)
ソート
無限スクロール
初回アクセス時のモーダルのポップアップ
アニメーション
などなど
頭の中でなんとなく実装のイメージ出来てるのに、なんと検索していいのかわからない
このもどかしさは初心者には厳しいものがあります -
どうしても限界がある
youtubeで紹介されたとおりにコーディングしてもうまくいかないことがあります。
nodeはライブラリの変化が激しいことが原因らしいです。すでに使えないライブラリで紹介されていたらコーディングしても時間の無駄になりますし、そもそも使えないライブラリであるかの判断も厳しいです。
うまくいかないコードだと判断したら諦めて他を探すことが重要だと感じました。
おわりに
最後まで読んでいただきありがとうこざいます。
冒頭にも書きましたがなにかご指摘などがあればコメントしてください。