tag:blogger.com,1999:blog-970559168775037492024-03-14T10:57:07.941+08:00qop's notesShinder Lin, JavaScript, PHPShinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.comBlogger414125tag:blogger.com,1999:blog-97055916877503749.post-17622020229962237202024-02-04T22:28:00.000+08:002024-02-04T22:28:47.893+08:00Apache HTTPS 服務間歇停止的問題<h1>Apache HTTPS 服務間歇停止的問題</h1>
<p>2024-02-01 開始客戶的網站就發生服務中斷的情況(其實這狀況當時應該發生好幾天了)。</p>
<p>主機架在 DigitalOcean 的 SGP1 上,由於 SGP1 網路設備曾有停擺快 2 天的黑記錄;所以一開始就懷疑是不是網路設備不穩。</p>
<p>以往的經驗,通常停止十分鐘左右就會恢復正常,但這次卻不是想像中單純。</p>
<p>服務大概停擺十分鐘後,就恢復正常,但是每隔一到兩小時就發生一次服務停擺,實著讓人頭疼。</p>
<p>一開始是毫無頭緒到底是什麼問題,也發 ticket 給 DO,DO 回應希望進一步提供資料。</p>
<p>懷疑過:網路設備問題、程式沒寫好、SSL 憑證問題、MySQL 資料庫問題等,大伙搞了兩天也沒離清問題點,只發現 HTTPS 受影響,但 HTTP 似乎沒受影響。可是 HTTPS 中斷服務時, HTTP 也變慢了。</p>
<p>大家的想法都一樣,HTTPS 和 HTTP 都是 Apache 伺服器的服務;沒道理 HTTPS 停擺,而 HTTP 正常。所以焦點又被轉移到 SSL 憑證上。</p>
<p>第三天開始,我終於有比較多的時間,從調 MySQL 開始搞了半天,沒什麼進展。
後來在一堆 Apache error log 中,某個停擺時點找到一條:</p>
<pre class="hljs"><code><div>[Sat Feb 03 23:54:11.819500 2024] [mpm_prefork:error] [pid 8964] AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting
</div></code></pre>
<p>很少在搞設定的我,只好查一下資料 mpm_prefork 是什麼模組,然後網路上也沒查到上面的錯誤會造成什麼問題。
後來在 stackoverflow 找到這篇 <a href="https://stackoverflow.com/questions/36924952/ah00161-server-reached-maxrequestworkers-setting-consider-raising-the-maxreque">AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting</a>。</p>
<p>硬著頭皮依照建議設定 mpm_prefork.conf,心想應該就是這個問題。
一覺起來,果然有改善,再依 log 做些調整。</p>
<pre class="hljs"><code><div># prefork MPM
# StartServers: number of server processes to start
# MinSpareServers: minimum number of server processes which are kept spare
# MaxSpareServers: maximum number of server processes which are kept spare
# MaxRequestWorkers: maximum number of server processes allowed to start
# MaxConnectionsPerChild: maximum number of requests a server process serves
<IfModule mpm_prefork_module>
StartServers 10
MinSpareServers 10
MaxSpareServers 20
ServerLimit 2000
MaxRequestWorkers 1536
MaxConnectionsPerChild 10000
</IfModule>
</div></code></pre>
<p>幾天的心情低落就因為一個設定。</p>
<p>另一個有趣的事情,就是在 stackoverflow 找資料時,看到一篇症狀和我們遇到的差不多狀況。
但傻眼的是,他自己解答自己的問題,答案是「公司找到會設定 Apache 的人才,他弄一下設定就好了」,沒有提到任何的模組或設定...</p>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-15802049492405404712023-11-12T15:45:00.000+08:002023-11-12T15:45:37.334+08:00RemixJS 使用 Jotai 取代 Context<h1 id="remixjs-%E4%BD%BF%E7%94%A8-jotai-%E5%8F%96%E4%BB%A3-context">RemixJS 使用 Jotai 取代 Context</h1>
<p>把之前的專案,用 Jotai 取代 Context 做登入狀態的管理。原本的登入狀態是使用 cookie-session 處理的。</p>
<p>基本上,改用 Jotai 程式碼反而變多了,思路比較徧向以前端的方式解決問題。</p>
<p>以下是遇到的狀況和問題:</p>
<ol>
<li>
<p>要將 loader 取得的資料放到 store,應該要使用 <code>useHydrateAtoms</code> hook。</p>
</li>
<li>
<p>使用 <code>store.get()</code> 或 <code>store.set()</code> 時,似乎不會觸發 re-render;而必須使用 <code>useAtom</code>,應該是訂閱功能做在 <code>useAtom</code> 身上吧。</p>
</li>
<li>
<p>使用 Context 時,有些功能用後端功能處理就可以了;但 Jotai 必須使用前端的功能解決。例如登出時,使用 Context 只要在 logout 頁面的 <code>loader()</code> 處理即可。使用 Jotai 必須登出後,使用 useAtom 去變更 store 狀態。使用 google 登入時,有相同的問題。</p>
</li>
<li>
<p>經常遇到 <a href="https://stackoverflow.com/questions/66374123/warning-text-content-did-not-match-server-im-out-client-im-in-div">Warning: Text content did not match</a> 的問題。解決的方式是在使用 useAtom 的 setter 後,再使用 useNavigate 轉到別的頁面,讓頁面重新 render。</p>
</li>
</ol>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-37633276090413809652023-11-12T00:01:00.001+08:002023-11-12T00:01:00.158+08:00RemixJS, Cloudflare Pages 專案使用 Google Sign-in<h1 id="remixjs-cloudflare-pages-%E5%B0%88%E6%A1%88%E4%BD%BF%E7%94%A8-google-sign-in">RemixJS, Cloudflare Pages 專案使用 Google Sign-in</h1>
<p><a href="https://qops.blogspot.com/2023/11/remixjs-supabase-cloudflare-pages.html">承上篇</a>的環境設定,要使用 Google sign-in 但不使用官方套件 <code>google-auth-library</code>。</p>
<p>可以使用 <a href="https://github.com/subhendukundu/worker-auth-providers">worker-auth-providers</a>,它雖然還在 beta 版,但簡單的 oauth 登入是可行的。</p>
<h2 id="gcp-%E5%9F%BA%E6%9C%AC%E8%A8%AD%E5%AE%9A">GCP 基本設定</h2>
<p>Google Cloud Platform (GCP) 設定並下載憑證資料:</p>
<ol>
<li>
<p>到「API 程式庫」>「公開」>「社交」啟用 Google People API。</p>
</li>
<li>
<p>到「API和服務 > 憑證」,點選「+建立憑證」>「OAuth 用戶 ID」。</p>
</li>
<li>
<p>「應用程式類型」選「網頁應用程式」</p>
</li>
<li>
<p>「已授權的 JavaScript 來源」加入正式環境及測試環境的 URI。</p>
</li>
<li>
<p>「已授權的重新導向 URI」加入接收 Query String 參數的頁面 URI,同樣包含測試及正式環境。</p>
</li>
<li>
<p>建立完成後,「下載 OAuth 用戶端」的 JSON 檔。</p>
</li>
<li>
<p>到「OAuth 同意畫面」,設定應用程式名稱,及測試帳號等。</p>
</li>
</ol>
<h2 id="%E7%99%BB%E5%85%A5%E9%A0%81%E9%9D%A2">登入頁面</h2>
<p>按了「Google 登入」的按鈕轉到 <code>google-signin.tsx</code> 頁面</p>
<pre class="hljs"><code><div><span class="hljs-comment">// app/routes/google-signin.tsx</span>
<span class="hljs-keyword">import</span> { type LoaderFunctionArgs, redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">"@remix-run/cloudflare"</span>;
<span class="hljs-keyword">import</span> { google } <span class="hljs-keyword">from</span> <span class="hljs-string">"worker-auth-providers"</span>;
<span class="hljs-keyword">import</span> { getMySession } <span class="hljs-keyword">from</span> <span class="hljs-string">"~/utils/sessions"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request, context, params }: LoaderFunctionArgs</span>) </span>{
<span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getMySession(request);
<span class="hljs-keyword">const</span> userId = session.get(<span class="hljs-string">"userId"</span>);
<span class="hljs-keyword">if</span> (userId) {
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/"</span>);
}
<span class="hljs-keyword">const</span> env: any = context.env;
<span class="hljs-keyword">const</span> googleLoginUrl = <span class="hljs-keyword">await</span> google.redirect({
<span class="hljs-attr">options</span>: {
<span class="hljs-attr">clientId</span>: env.GOOGLE_CLIENT_ID,
<span class="hljs-attr">redirectTo</span>: env.GOOGLE_REDIRECT_URI,
},
});
<span class="hljs-keyword">return</span> redirect(googleLoginUrl);
}
</div></code></pre>
<h2 id="%E7%94%A8%E6%88%B6%E7%99%BB%E5%85%A5%E5%BE%8C%E8%BD%89%E5%90%91%E7%9A%84%E9%A0%81%E9%9D%A2">用戶登入後轉向的頁面</h2>
<pre class="hljs"><code><div><span class="hljs-comment">//</span>
<span class="hljs-keyword">import</span> { type LoaderFunctionArgs, redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">"@remix-run/cloudflare"</span>;
<span class="hljs-keyword">import</span> { getMySession, getHeadersWithSetCookie } <span class="hljs-keyword">from</span> <span class="hljs-string">"~/utils/sessions"</span>;
<span class="hljs-keyword">import</span> { google } <span class="hljs-keyword">from</span> <span class="hljs-string">"worker-auth-providers"</span>;
<span class="hljs-keyword">import</span> getSupabase <span class="hljs-keyword">from</span> <span class="hljs-string">"~/utils/supabase-client"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request, context, params }: LoaderFunctionArgs</span>) </span>{
<span class="hljs-keyword">const</span> env: any = context.env;
<span class="hljs-keyword">const</span> { user, tokens } = <span class="hljs-keyword">await</span> google.users({
<span class="hljs-attr">options</span>: {
<span class="hljs-attr">clientSecret</span>: env.GOOGLE_CLIENT_SECRET,
<span class="hljs-attr">clientId</span>: env.GOOGLE_CLIENT_ID,
<span class="hljs-attr">redirectUrl</span>: env.GOOGLE_REDIRECT_URI,
},
request,
});
<span class="hljs-comment">// "user": {</span>
<span class="hljs-comment">// "id": "111******",</span>
<span class="hljs-comment">// "email": "shinder.lin@gmail.com",</span>
<span class="hljs-comment">// "verified_email": true,</span>
<span class="hljs-comment">// "name": "Shinder Lin",</span>
<span class="hljs-comment">// "given_name": "Shinder",</span>
<span class="hljs-comment">// "family_name": "Lin",</span>
<span class="hljs-comment">// "picture": "...",</span>
<span class="hljs-comment">// "locale": "zh-TW"</span>
<span class="hljs-comment">// },</span>
<span class="hljs-comment">// console.log({ user, tokens });</span>
<span class="hljs-comment">// tokens: {</span>
<span class="hljs-comment">// access_token: 'ya29...',</span>
<span class="hljs-comment">// expires_in: 3599,</span>
<span class="hljs-comment">// scope: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid',</span>
<span class="hljs-comment">// token_type: 'Bearer',</span>
<span class="hljs-comment">// id_token: 'eyJh...'</span>
<span class="hljs-comment">// }</span>
<span class="hljs-keyword">if</span> (!user?.id) {
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/login"</span>);
}
<span class="hljs-keyword">const</span> supabase = getSupabase(context);
<span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: data1, <span class="hljs-attr">error</span>: error1 } = <span class="hljs-keyword">await</span> supabase
.from(<span class="hljs-string">"g_users"</span>)
.select()
.eq(<span class="hljs-string">"gid"</span>, user.id);
<span class="hljs-comment">// 是否已經登入過</span>
<span class="hljs-keyword">if</span> (data1.length) {
<span class="hljs-keyword">const</span> row: any = data1[<span class="hljs-number">0</span>];
<span class="hljs-keyword">if</span> (row.blocked) {
<span class="hljs-keyword">return</span> { <span class="hljs-attr">msg</span>: <span class="hljs-string">"您的帳號已被停權"</span> };
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">await</span> supabase
.from(<span class="hljs-string">"g_users"</span>)
.update({ <span class="hljs-attr">updated_at</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>() })
.eq(<span class="hljs-string">"gid"</span>, row.gid);
}
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: data2, <span class="hljs-attr">error</span>: error2 } = <span class="hljs-keyword">await</span> supabase
.from(<span class="hljs-string">"g_users"</span>)
.insert({ <span class="hljs-attr">gid</span>: user.id, <span class="hljs-attr">email</span>: user.email, <span class="hljs-attr">name</span>: user.name })
.select();
<span class="hljs-keyword">if</span> (!data2.length) {
<span class="hljs-keyword">return</span> { <span class="hljs-attr">msg</span>: <span class="hljs-string">"DB 無法新增資料"</span> };
}
}
<span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getMySession(request);
session.set(<span class="hljs-string">"gid"</span>, user.id);
session.set(<span class="hljs-string">"userId"</span>, user.email);
session.set(<span class="hljs-string">"nickname"</span>, user.name);
<span class="hljs-keyword">const</span> headers = <span class="hljs-keyword">await</span> getHeadersWithSetCookie(session);
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">'/'</span>, { headers });
}
</div></code></pre>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-8875180263382641522023-11-11T21:13:00.000+08:002023-11-11T21:13:11.315+08:00RemixJS, Supabase 專案發佈到 Cloudflare Pages<h1 id="remixjs-supabase-%E5%B0%88%E6%A1%88%E7%99%BC%E4%BD%88%E5%88%B0-cloudflare-pages">RemixJS, Supabase 專案發佈到 Cloudflare Pages</h1>
<h2 id="%E6%B3%A8%E6%84%8F%E7%9A%84%E4%BA%8B%E9%A0%85">注意的事項</h2>
<p>條列一下,Remix 從專案建立到發佈到 Cloudflare pages 該注意的事項:</p>
<ol>
<li>
<p>建立專案時,直接使用 Cloudflare 的官方工具 <code>npm create cloudflare</code>,以方便發佈,參考<a href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-remix-site/">Deploy a Remix site</a>。</p>
</li>
<li>
<p>原本由 <code>@remix-run/node</code> 匯入的類型 LinksFunction, LoaderFunctionArgs 或函式 json, redirect 改由 <code>@remix-run/cloudflare</code> 匯入。</p>
</li>
<li>
<p>和 NodeJS 相依的套件,儘量不要使用,例如 qs 套件。如果用到了,要在 <code>remox.config.js</code> 裡設定 polyfill。</p>
</li>
<li>
<p>開發時,環境變數設定在 <code>.dev.vars</code> 裡。在正式環境時,須到 Cloudflare pages 的專案裡,設定環境變數。</p>
</li>
<li>
<p>環境變數的取得,不是透過 <code>process.env</code>,是透過 <code>context.env</code>。而 context 物件是由 LoaderFunctionArgs 或 ActionFunctionArgs (<code>{ request, context, params }</code>) 而來。</p>
</li>
</ol>
<p>在測試的專案用到 bcryptjs,而該套件使用到 Node 的 crypto,因此要如下設定:</p>
<pre class="hljs"><code><div><span class="hljs-comment">// remox.config.js</span>
<span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('@remix-run/dev').AppConfig}</span> </span>*/</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-comment">/* 略 */</span>
<span class="hljs-attr">browserNodeBuiltinsPolyfill</span>: { <span class="hljs-attr">modules</span>: { <span class="hljs-attr">crypto</span>: <span class="hljs-literal">true</span> } }, <span class="hljs-comment">// bcryptjs 需要</span>
};
</div></code></pre>
<h2 id="supabase-%E7%9B%B8%E9%97%9C">Supabase 相關</h2>
<h3 id="%E9%80%A3%E7%B7%9A-supabase">連線 supabase</h3>
<pre class="hljs"><code><div><span class="hljs-comment">// app/utils/supabase-client.ts</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/supabase-js"</span>;
<span class="hljs-keyword">let</span> supabase;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSupabase</span>(<span class="hljs-params">context={}</span>)</span>{
<span class="hljs-keyword">if</span>(! supabase) {
supabase = createClient(
context.env.SUPABASE_URL,
context.env.SUPABASE_ANON_KEY
);
}
<span class="hljs-keyword">return</span> supabase;
}
</div></code></pre>
<h3 id="%E6%96%B0%E5%A2%9E">新增</h3>
<pre class="hljs"><code><div> <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> supabase
.from(<span class="hljs-string">"address_book"</span>)
.insert({ name, email, mobile, birthday, address, <span class="hljs-attr">creator_gid</span>: myAuth.gid })
.select();
</div></code></pre>
<h3 id="%E8%AE%80%E5%8F%96">讀取</h3>
<p>算數量</p>
<pre class="hljs"><code><div><span class="hljs-keyword">let</span> totalRows = <span class="hljs-number">0</span>;
<span class="hljs-keyword">const</span> tmpSupabase = supabase
.from(<span class="hljs-string">"address_book"</span>)
.select(<span class="hljs-string">"*"</span>, { <span class="hljs-attr">count</span>: <span class="hljs-string">"exact"</span>, <span class="hljs-attr">head</span>: <span class="hljs-literal">true</span> });
<span class="hljs-keyword">if</span> (searchStr) {
<span class="hljs-keyword">const</span> { count } = <span class="hljs-keyword">await</span> tmpSupabase.or(
<span class="hljs-string">`name.like.%<span class="hljs-subst">${searchStr}</span>%,mobile.like.%<span class="hljs-subst">${searchStr}</span>%`</span>
);
totalRows = count;
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">const</span> { count } = <span class="hljs-keyword">await</span> tmpSupabase;
totalRows = count;
}
</div></code></pre>
<p>條件搜尋</p>
<pre class="hljs"><code><div><span class="hljs-keyword">if</span> (searchStr) {
<span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
.from(<span class="hljs-string">"address_book"</span>)
.select()
.or(<span class="hljs-string">`name.like.%<span class="hljs-subst">${searchStr}</span>%,mobile.like.%<span class="hljs-subst">${searchStr}</span>%`</span>)
.order(<span class="hljs-string">"sid"</span>, { <span class="hljs-attr">ascending</span>: <span class="hljs-literal">false</span> })
.range((page - <span class="hljs-number">1</span>) * perPage, page * perPage - <span class="hljs-number">1</span>);
<span class="hljs-keyword">if</span> (!error) {
rows = data;
}
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
.from(<span class="hljs-string">"address_book"</span>)
.select()
.order(<span class="hljs-string">"sid"</span>, { <span class="hljs-attr">ascending</span>: <span class="hljs-literal">false</span> })
.range((page - <span class="hljs-number">1</span>) * perPage, page * perPage - <span class="hljs-number">1</span>);
<span class="hljs-keyword">if</span> (!error) {
rows = data;
}
}
</div></code></pre>
<h3 id="%E6%9B%B4%E6%96%B0">更新</h3>
<pre class="hljs"><code><div><span class="hljs-keyword">let</span> query = supabase
.from(<span class="hljs-string">"address_book"</span>)
.update({ name, email, mobile, birthday, address })
.eq(<span class="hljs-string">"sid"</span>, sid);
<span class="hljs-keyword">let</span> result;
<span class="hljs-keyword">if</span> (myAuth.gid === context.env.ADMIN_GID) {
result = <span class="hljs-keyword">await</span> query;
} <span class="hljs-keyword">else</span> {
result = <span class="hljs-keyword">await</span> query.eq(<span class="hljs-string">"creator_gid"</span>, myAuth.gid);
}
</div></code></pre>
<h3 id="%E5%88%AA%E9%99%A4">刪除</h3>
<pre class="hljs"><code><div><span class="hljs-keyword">let</span> result;
<span class="hljs-keyword">let</span> query = supabase.from(<span class="hljs-string">"address_book"</span>).delete().eq(<span class="hljs-string">"sid"</span>, sid);
<span class="hljs-keyword">if</span> (myAuth.gid === context.env.ADMIN_GID) {
result = <span class="hljs-keyword">await</span> query;
} <span class="hljs-keyword">else</span> {
result = <span class="hljs-keyword">await</span> query.eq(<span class="hljs-string">"creator_gid"</span>, myAuth.gid);
}
</div></code></pre>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-85996448868544307882023-10-26T00:13:00.000+08:002023-10-26T00:13:11.968+08:00RemixJS 2 保有登入狀態<h1 id="remixjs-2-%E4%BF%9D%E6%9C%89%E7%99%BB%E5%85%A5%E7%8B%80%E6%85%8B">RemixJS 2 保有登入狀態</h1>
<p>呈上篇 <a href="https://qops.blogspot.com/2023/10/remixjs-2-session.html">RemixJS 2 以 session 實作登入機制</a>,session 的資料存放在 cookie 裡,那要如何在頁面內判斷及保有登入的狀態?</p>
<p>在前端可以用原本 react 的處理方式,以 context 來保有狀態。在後端必須每個 request 都要檢查 cookie。</p>
<p><code>get-auth.ts</code> 是將解讀 cookie 的功能寫成模組,用來判斷是否為登入狀態。</p>
<pre class="hljs"><code><div><span class="hljs-comment">// app/modules/get-auth.ts</span>
<span class="hljs-keyword">import</span> { getMySession } <span class="hljs-keyword">from</span> <span class="hljs-string">"~/modules/sessions"</span>;
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getAuth</span>(<span class="hljs-params">request: Request</span>) </span>{
<span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getMySession(request);
<span class="hljs-keyword">const</span> userId = session.get(<span class="hljs-string">"userId"</span>) || <span class="hljs-string">""</span>;
<span class="hljs-keyword">const</span> nickname = session.get(<span class="hljs-string">"nickname"</span>) || <span class="hljs-string">""</span>;
<span class="hljs-keyword">return</span> { userId, nickname, <span class="hljs-attr">auth</span>: !!userId };
<span class="hljs-comment">// auth 屬性用來判斷是否登入</span>
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> getAuth;
</div></code></pre>
<h2 id="authcontextprovider">AuthContextProvider</h2>
<pre class="hljs"><code><div><span class="hljs-comment">// app/contexts/AuthContext.tsx</span>
<span class="hljs-keyword">import</span> React, { createContext } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">export</span> type AuthDataType = {
<span class="hljs-attr">userId</span>: string | <span class="hljs-literal">undefined</span>;
nickname: string | <span class="hljs-literal">undefined</span>;
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> AuthContext = createContext<AuthDataType>({
<span class="hljs-attr">userId</span>: <span class="hljs-string">""</span>,
<span class="hljs-attr">nickname</span>: <span class="hljs-string">""</span>,
});
type PropsType = {
<span class="hljs-attr">userId</span>: string | <span class="hljs-literal">undefined</span>;
nickname: string | <span class="hljs-literal">undefined</span>;
children: React.ReactNode;
};
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AuthContextProvider</span>(<span class="hljs-params">{ userId, nickname, children }: PropsType</span>) </span>{
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">AuthContext.Provider</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">userId</span>, <span class="hljs-attr">nickname</span> }}></span>
{children}
<span class="hljs-tag"></<span class="hljs-name">AuthContext.Provider</span>></span></span>
);
}
</div></code></pre>
<p>將 <code><AuthContextProvider></code> 包住 <code><Outlet /></code>。</p>
<pre class="hljs"><code><div><span class="hljs-comment">// app/root.tsx 部份內容</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request, params }: LoaderFunctionArgs</span>) </span>{
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"App loader"</span>);
<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> getAuth(request);
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> loaderData = useLoaderData<<span class="hljs-keyword">typeof</span> loader>()
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">head</span>></span>
<span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">charSet</span>=<span class="hljs-string">"utf-8"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">Meta</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">Links</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">head</span>></span>
<span class="hljs-tag"><<span class="hljs-name">body</span>></span>
<span class="hljs-tag"><<span class="hljs-name">AuthContextProvider</span> <span class="hljs-attr">userId</span>=<span class="hljs-string">{loaderData.userId}</span> <span class="hljs-attr">nickname</span>=<span class="hljs-string">{loaderData.nickname}</span>></span>
<span class="hljs-tag"><<span class="hljs-name">Outlet</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">AuthContextProvider</span>></span>
<span class="hljs-tag"><<span class="hljs-name">ScrollRestoration</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">Scripts</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">LiveReload</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">body</span>></span>
<span class="hljs-tag"></<span class="hljs-name">html</span>></span></span>
);
}
</div></code></pre>
<h2 id="%E9%98%B2%E6%AD%A2%E6%9C%AA%E6%8E%88%E6%AC%8A%E8%80%8C%E4%BD%BF%E7%94%A8%E5%8A%9F%E8%83%BD">防止未授權而使用功能</h2>
<p>這裡要分兩個部份 loader 和 action。loader 因為有階層關係,可以在 Layout 的 loader 做阻擋的動作。</p>
<pre class="hljs"><code><div><span class="hljs-comment">// app/routes/address-book.tsx 的 loader 部份</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request, params }: LoaderFunctionArgs</span>) </span>{
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"address-book loader"</span>);
<span class="hljs-keyword">const</span> myAuth = <span class="hljs-keyword">await</span> getAuth(request);
<span class="hljs-keyword">if</span> (!myAuth.auth) {
<span class="hljs-comment">// 沒有登入,轉到登入頁面</span>
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">`/login?u=<span class="hljs-subst">${request.url}</span>`</span>);
}
<span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
}
</div></code></pre>
<p>action 就必須在每個頁面處理。</p>
<pre class="hljs"><code><div><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">action</span>(<span class="hljs-params">{ request, params }: ActionFunctionArgs</span>) </span>{
<span class="hljs-keyword">const</span> myAuth = <span class="hljs-keyword">await</span> getAuth(request);
<span class="hljs-keyword">if</span> (!myAuth.auth) {
<span class="hljs-comment">// 沒有登入,轉到登入頁面</span>
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">`/login?u=<span class="hljs-subst">${request.url}</span>`</span>);
}
<span class="hljs-comment">// 略</span>
}
</div></code></pre>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-52441160067856301852023-10-23T23:31:00.000+08:002023-10-23T23:31:49.092+08:00RemixJS 2 以 session 實作登入機制<h1 id="remixjs-2-%E4%BB%A5-session-%E5%AF%A6%E4%BD%9C%E7%99%BB%E5%85%A5%E6%A9%9F%E5%88%B6">RemixJS 2 以 session 實作登入機制</h1>
<p>一般是以 cookie 存放 session id,session 資料存放在後端,可以是記憶體、檔案或資料庫。
但如果考慮到分散式架構,記憶體和檔案都是不可行的。</p>
<p>以識別用戶而言,其實資料很少一個變數值即可,其餘想放的資料頂多三四個變數值。此時使用官方範例的 createCookieSessionStorage 是個不錯的作法,將資料直接放在 cookie 並加密即可,當然除了好的加密密碼,cookie 最好設定為 httpOnly。</p>
<h2 id="session-%E5%B7%A5%E5%85%B7">session 工具</h2>
<p>使用官方的範例,同時包裝兩個方法 <code>getMySession</code> 和 <code>getHeadersWithSetCookie</code> 省點囉唆的動作。</p>
<pre class="hljs"><code><div><span class="hljs-comment">// app/modules/sessions.ts</span>
<span class="hljs-keyword">import</span> { Session, createCookieSessionStorage } <span class="hljs-keyword">from</span> <span class="hljs-string">"@remix-run/node"</span>;
type SessionData = {
<span class="hljs-attr">userId</span>: string;
nickname: string;
};
type SessionFlashData = {
<span class="hljs-attr">error</span>: string;
};
<span class="hljs-keyword">const</span> { getSession, commitSession, destroySession } =
createCookieSessionStorage<SessionData, SessionFlashData>({
<span class="hljs-attr">cookie</span>: {
<span class="hljs-attr">name</span>: <span class="hljs-string">"__session"</span>,
<span class="hljs-comment">// domain: "remix.run",</span>
<span class="hljs-comment">// expires: new Date(Date.now() + 60_000),</span>
<span class="hljs-attr">httpOnly</span>: <span class="hljs-literal">true</span>,
<span class="hljs-comment">// maxAge: 60,</span>
<span class="hljs-attr">path</span>: <span class="hljs-string">"/"</span>,
<span class="hljs-attr">sameSite</span>: <span class="hljs-string">"lax"</span>,
<span class="hljs-attr">secrets</span>: [<span class="hljs-string">"your_secret_key"</span>],
<span class="hljs-comment">// secure: true,</span>
},
});
<span class="hljs-keyword">const</span> getMySession = <span class="hljs-keyword">async</span> (request: Request) => {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> getSession(request.headers.get(<span class="hljs-string">"Cookie"</span>));
};
<span class="hljs-keyword">const</span> getHeadersWithSetCookie = <span class="hljs-keyword">async</span> (session: Session) => {
<span class="hljs-keyword">return</span> {
<span class="hljs-string">"Set-Cookie"</span>: <span class="hljs-keyword">await</span> commitSession(session),
};
};
<span class="hljs-keyword">export</span> {
getSession,
commitSession,
destroySession,
getMySession,
getHeadersWithSetCookie,
};
</div></code></pre>
<h2 id="%E7%99%BB%E5%85%A5%E9%A0%81">登入頁</h2>
<p>關於登入頁幾點請注意:</p>
<ol>
<li>進入登入頁前,先判斷用戶是否已經登入,若是則轉向別的頁面。</li>
<li>表單有兩個欄位 email 和 password,email 為 userId。</li>
<li>帳號和密碼直接寫在程式碼裡是不好的作法,這只是個 demo。</li>
<li>確定登入後,將兩個值寫入 cookie,分別是 userId 和 nickname。</li>
</ol>
<pre class="hljs"><code><div><span class="hljs-comment">// app/routes/login.tsx</span>
<span class="hljs-comment">// 略</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request, params }: LoaderFunctionArgs</span>) </span>{
<span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getMySession(request);
<span class="hljs-keyword">const</span> userId = session.get(<span class="hljs-string">"userId"</span>);
<span class="hljs-keyword">if</span> (userId) {
<span class="hljs-comment">// 如果已經登入</span>
<span class="hljs-keyword">const</span> uStr = getQueryStringObject(request)[<span class="hljs-string">"u"</span>];
<span class="hljs-keyword">if</span> (uStr) {
<span class="hljs-keyword">return</span> redirect(uStr);
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/"</span>);
}
}
<span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LoginPage</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-comment">// 略</span>
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><></span>
<span class="hljs-tag"><<span class="hljs-name">Form</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"post"</span> <span class="hljs-attr">encType</span>=<span class="hljs-string">"multipart/form-data"</span>></span>
{/* 略 */}
<span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"btn btn-primary"</span>></span>
登入
<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
<span class="hljs-tag"></<span class="hljs-name">Form</span>></span>
<span class="hljs-tag"></></span></span>
);
}
type loginUser = { <span class="hljs-attr">email</span>: string; nickname: string; password: string };
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">action</span>(<span class="hljs-params">{ request, params }: ActionFunctionArgs</span>) </span>{
<span class="hljs-keyword">const</span> users: loginUser[] = [
{ <span class="hljs-attr">email</span>: <span class="hljs-string">"lsd0125@gmail.com"</span>, <span class="hljs-attr">password</span>: <span class="hljs-string">"12345678"</span>, <span class="hljs-attr">nickname</span>: <span class="hljs-string">"小嘟"</span> },
{ <span class="hljs-attr">email</span>: <span class="hljs-string">"shin@gmail.com"</span>, <span class="hljs-attr">password</span>: <span class="hljs-string">"345678"</span>, <span class="hljs-attr">nickname</span>: <span class="hljs-string">"肥肥"</span> },
];
<span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getMySession(request);
<span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> getBodyObject(request);
<span class="hljs-keyword">const</span> { email, password } = body;
<span class="hljs-keyword">const</span> theUser = users.find(<span class="hljs-function">(<span class="hljs-params">u</span>) =></span> {
<span class="hljs-keyword">return</span> u.email === email && u.password === password;
});
<span class="hljs-keyword">const</span> output = {
<span class="hljs-attr">success</span>: !!theUser,
<span class="hljs-attr">bodyData</span>: body,
};
<span class="hljs-keyword">if</span> (theUser) {
session.set(<span class="hljs-string">"userId"</span>, email);
session.set(<span class="hljs-string">"nickname"</span>, theUser.nickname);
<span class="hljs-keyword">const</span> headers = <span class="hljs-keyword">await</span> getHeadersWithSetCookie(session);
<span class="hljs-keyword">return</span> json(output, { headers });
}
<span class="hljs-keyword">return</span> output;
}
</div></code></pre>
<h2 id="%E7%99%BB%E5%87%BA%E5%8A%9F%E8%83%BD">登出功能</h2>
<p>登出是直接把兩個值從 cookie 中移除。</p>
<pre class="hljs"><code><div><span class="hljs-comment">// app/routes/logout.tsx</span>
<span class="hljs-keyword">import</span> { type LoaderFunctionArgs, redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">"@remix-run/node"</span>;
<span class="hljs-keyword">import</span> { getMySession, getHeadersWithSetCookie } <span class="hljs-keyword">from</span> <span class="hljs-string">"~/modules/sessions"</span>;
<span class="hljs-keyword">import</span> { getQueryStringObject } <span class="hljs-keyword">from</span> <span class="hljs-string">"~/modules/handle-request-data"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request, params }: LoaderFunctionArgs</span>) </span>{
<span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getMySession(request);
session.unset(<span class="hljs-string">"userId"</span>);
session.unset(<span class="hljs-string">"nickname"</span>);
<span class="hljs-keyword">const</span> headers = <span class="hljs-keyword">await</span> getHeadersWithSetCookie(session);
<span class="hljs-keyword">const</span> uStr = getQueryStringObject(request)[<span class="hljs-string">"u"</span>];
<span class="hljs-keyword">if</span>(uStr){
<span class="hljs-keyword">return</span> redirect(uStr, { headers });
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/"</span>, { headers });
}
}
</div></code></pre>
<p>有 userId 後,其它要存放在後端的資料,就可以存到資料庫裡了,不要把太多資料放在 cookie 裡,通常不要超過 4K。</p>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-79549986177204209102023-10-23T22:24:00.000+08:002023-10-23T22:24:18.030+08:00RemixJS 2 的 .env<h1 id="remixjs-2-%E7%9A%84-env">RemixJS 2 的 .env</h1>
<p>Remix 在 <code>開發環境</code> 是直接支援 .env 的。</p>
<p>但要特別注意,在 <code>production</code> 時是忽略 .env 的。變通的方式很多,可以用 shell script 或 dotenv-cli 工具。</p>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-24692627032193708132023-10-22T23:14:00.002+08:002023-10-22T23:14:13.776+08:00RemixJS 2 action 函式<h1 id="remixjs-2-action-%E5%87%BD%E5%BC%8F">RemixJS 2 action 函式</h1>
<p><code>loader()</code> 的功能是在以 GET 拜訪時,於後端執行,通常是取得頁面要使用的資料。<code>action()</code> 則是用來處理 GET 方法以外的要求。</p>
<h2 id="%E5%AF%A6%E4%BD%9C-api">實作 API</h2>
<p>在沒有頁面的 component function,action() 和 loader() 可以用來建立 RESTful API。以下是個刪除功能的例子。</p>
<pre class="hljs"><code><div><span class="hljs-comment">// app/modules/mysql-connect.ts // 用以建立資料庫連線</span>
<span class="hljs-keyword">import</span> mysql, { PoolOptions } <span class="hljs-keyword">from</span> <span class="hljs-string">"mysql2/promise"</span>;
<span class="hljs-keyword">const</span> options: PoolOptions = {
<span class="hljs-attr">host</span>: process.env.DB_HOST,
<span class="hljs-attr">user</span>: process.env.DB_USER,
<span class="hljs-attr">password</span>: process.env.DB_PASS,
<span class="hljs-attr">database</span>: process.env.DB_NAME,
<span class="hljs-attr">waitForConnections</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">connectionLimit</span>: <span class="hljs-number">10</span>,
<span class="hljs-attr">queueLimit</span>: <span class="hljs-number">0</span>,
<span class="hljs-attr">enableKeepAlive</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">keepAliveInitialDelay</span>: <span class="hljs-number">0</span>,
};
<span class="hljs-keyword">const</span> pool = mysql.createPool(options);
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> pool;
</div></code></pre>
<pre class="hljs"><code><div><span class="hljs-comment">// app/routes/address-book.delete.$sid.tsx</span>
<span class="hljs-keyword">import</span> { type ActionFunctionArgs } <span class="hljs-keyword">from</span> <span class="hljs-string">"@remix-run/node"</span>;
<span class="hljs-keyword">import</span> db <span class="hljs-keyword">from</span> <span class="hljs-string">"./../modules/mysql-connect"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">action</span>(<span class="hljs-params">{ request, params }: ActionFunctionArgs</span>) </span>{
<span class="hljs-keyword">const</span> output = {
<span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>,
<span class="hljs-attr">result</span>: {},
};
<span class="hljs-keyword">const</span> sid = params.sid;
<span class="hljs-keyword">if</span> (request.method === <span class="hljs-string">"DELETE"</span>) {
<span class="hljs-keyword">if</span> (sid && <span class="hljs-built_in">parseInt</span>(sid)) {
<span class="hljs-keyword">const</span> sql = <span class="hljs-string">`DELETE FROM address_book WHERE sid=<span class="hljs-subst">${sid}</span>`</span>;
<span class="hljs-keyword">const</span> [result] = <span class="hljs-keyword">await</span> db.query(sql);
output.result = result;
output.success = <span class="hljs-literal">true</span>;
}
}
<span class="hljs-keyword">return</span> output;
}
</div></code></pre>
<h2 id="%E6%90%AD%E9%85%8D%E9%A0%81%E9%9D%A2%E8%A1%A8%E5%96%AE%E4%BD%BF%E7%94%A8">搭配頁面表單使用</h2>
<p>action() 可以搭配頁面內的表單 (使用 Form 元件) 做回應,在一般情況下是以 AJAX 的方式溝通。以下是一個範例的部份程式碼。</p>
<pre class="hljs"><code><div><span class="hljs-keyword">import</span> type { LoaderFunctionArgs, ActionFunctionArgs } <span class="hljs-keyword">from</span> <span class="hljs-string">"@remix-run/node"</span>;
<span class="hljs-keyword">import</span> {
useLoaderData,
Form,
useActionData,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@remix-run/react"</span>;
<span class="hljs-keyword">import</span> { getBodyObject } <span class="hljs-keyword">from</span> <span class="hljs-string">"~/modules/handle-request-data"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request, params }: LoaderFunctionArgs</span>) </span>{
<span class="hljs-comment">// 載入資料</span>
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AddressBookEdit</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> data = useLoaderData();
<span class="hljs-keyword">const</span> actionData = useActionData<<span class="hljs-keyword">typeof</span> action>();
<span class="hljs-comment">// ...</span>
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"container"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">Form</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"post"</span> <span class="hljs-attr">encType</span>=<span class="hljs-string">"multipart/form-data"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mb-3"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">label</span> <span class="hljs-attr">htmlFor</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"form-label"</span>></span>
email
<span class="hljs-tag"></<span class="hljs-name">label</span>></span>
<span class="hljs-tag"><<span class="hljs-name">input</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
<span class="hljs-attr">className</span>=<span class="hljs-string">"form-control"</span>
<span class="hljs-attr">id</span>=<span class="hljs-string">"email"</span>
<span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span>
<span class="hljs-attr">value</span>=<span class="hljs-string">{form.email}</span>
<span class="hljs-attr">onChange</span>=<span class="hljs-string">{handleFieldChange}</span>
/></span>
<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
<span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"btn btn-primary"</span>></span>
修改
<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
<span class="hljs-tag"></<span class="hljs-name">Form</span>></span>
<span class="hljs-tag"></<span class="hljs-name">div</span>></span></span>
);
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">action</span>(<span class="hljs-params">{ request, params }: ActionFunctionArgs</span>) </span>{
<span class="hljs-keyword">const</span> sid = params.sid;
<span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> getBodyObject(request);
<span class="hljs-keyword">const</span> { name, email, mobile, birthday, address } = body;
<span class="hljs-keyword">const</span> output = {
<span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>,
<span class="hljs-attr">result</span>: {},
<span class="hljs-attr">bodyData</span>: body,
};
<span class="hljs-comment">// 變更資料</span>
<span class="hljs-keyword">return</span> output;
}
</div></code></pre>
<p>注意幾個要點:</p>
<ol>
<li>表單必須使用 <code>@remix-run/react</code> 的 <code>Form</code> 元件建立。</li>
<li>和一般表單一樣可以使用 encType 屬性決定送出的資料編碼方式。</li>
<li>可以使用 action 屬性設定表單傳送的對象 (可以不是該頁面的 <code>action()</code> 函式)。</li>
<li>在頁面內使用 useActionData() 取得表單送出後回傳的訊息或資料。</li>
<li>若要實作 RESTful API 可以依 <code>request.method</code> 是 POST、PUT 或 DELETE 來做不同的處理。</li>
</ol>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-13629052466119439182023-10-22T21:09:00.003+08:002023-10-22T21:09:46.335+08:00RemixJS 2 處理從用戶端來的資料<h1 id="remixjs-2-%E8%99%95%E7%90%86%E5%BE%9E%E7%94%A8%E6%88%B6%E7%AB%AF%E4%BE%86%E7%9A%84%E8%B3%87%E6%96%99">RemixJS 2 處理從用戶端來的資料</h1>
<p><code>handle-request-data.ts</code> 包含了幾個處理要求端來的資料,後來改用 <code>qs</code> 套件比較符合 expressjs 的風格。
其中處理上傳檔案的部份為存成檔案,目前建議存到雲端服務,例如 AWS S3。
這些函式同樣可以使用在 NextJS app router 上。</p>
<pre class="hljs"><code><div><span class="hljs-comment">// *** handle-request-data.ts</span>
<span class="hljs-keyword">import</span> qs <span class="hljs-keyword">from</span> <span class="hljs-string">"qs"</span>;
<span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"node:fs/promises"</span>;
<span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> uuidV4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getQueryStringObject:any = <span class="hljs-function">(<span class="hljs-params">request: Request</span>) =></span> {
<span class="hljs-keyword">const</span> q = request.url.indexOf(<span class="hljs-string">"?"</span>);
<span class="hljs-keyword">if</span> (q < <span class="hljs-number">0</span>) {
<span class="hljs-keyword">return</span> {};
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">return</span> qs.parse(request.url.slice(q+<span class="hljs-number">1</span>));
}
<span class="hljs-comment">// *** 以下為使用 Object 的版本</span>
<span class="hljs-comment">// const url = new URL(request.url);</span>
<span class="hljs-comment">// return Object.fromEntries(url.searchParams);</span>
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getBodyObject = <span class="hljs-keyword">async</span> (request: Request) => {
<span class="hljs-keyword">const</span> contentType = request.headers.get(<span class="hljs-string">"Content-Type"</span>)?.split(<span class="hljs-string">";"</span>)[<span class="hljs-number">0</span>];
<span class="hljs-keyword">switch</span> (contentType) {
<span class="hljs-keyword">case</span> <span class="hljs-string">"multipart/form-data"</span>:
<span class="hljs-keyword">const</span> formData:any = <span class="hljs-keyword">await</span> request.formData();
<span class="hljs-keyword">return</span> qs.parse(<span class="hljs-keyword">new</span> URLSearchParams(formData).toString());
<span class="hljs-comment">// *** 以下為使用 Object 的版本</span>
<span class="hljs-comment">// return Object.fromEntries(formData);</span>
<span class="hljs-keyword">case</span> <span class="hljs-string">"application/x-www-form-urlencoded"</span>:
<span class="hljs-keyword">const</span> txt = <span class="hljs-keyword">await</span> request.text();
<span class="hljs-keyword">return</span> qs.parse(txt);
<span class="hljs-comment">// *** 以下為使用 Object 的版本</span>
<span class="hljs-comment">// const usp = new URLSearchParams(txt);</span>
<span class="hljs-comment">// return Object.fromEntries(usp);</span>
<span class="hljs-keyword">case</span> <span class="hljs-string">"application/json"</span>:
<span class="hljs-keyword">const</span> json = <span class="hljs-keyword">await</span> request.json();
<span class="hljs-keyword">return</span> json;
}
};
type FileDataType = {
<span class="hljs-attr">size</span>: number;
type: string;
lastModified: number;
originalName: string;
filename: string;
path: string;
};
type MultipartDataResultType = {
<span class="hljs-attr">fields</span>: { [index: string]: string | string[] };
files: { [index: string]: FileDataType | FileDataType[] };
error: any;
};
<span class="hljs-comment">// *** 處理檔案上傳</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getMultipartData</span>(<span class="hljs-params">
request: Request,
acceptedMimeTypes = [<span class="hljs-string">"image/jpeg"</span>, <span class="hljs-string">"image/png"</span>], <span class="hljs-regexp">//</span> 篩選類型設定
useUuidFilename = true, <span class="hljs-regexp">//</span> 使用隨機 uuid 為主檔名
uploadDir = <span class="hljs-string">"./tmp"</span> <span class="hljs-regexp">//</span> 上傳的資料夾
</span>) </span>{
<span class="hljs-keyword">let</span> result: MultipartDataResultType = {
<span class="hljs-attr">fields</span>: {},
<span class="hljs-attr">files</span>: {},
<span class="hljs-attr">error</span>: {},
};
<span class="hljs-keyword">const</span> method = request.method.toUpperCase();
<span class="hljs-keyword">let</span> type = request.headers.get(<span class="hljs-string">"Content-Type"</span>);
<span class="hljs-keyword">if</span> (!type) <span class="hljs-keyword">return</span> result;
type = type.split(<span class="hljs-string">";"</span>)[<span class="hljs-number">0</span>]; <span class="hljs-comment">// 取得 mimetype</span>
<span class="hljs-keyword">if</span> (method !== <span class="hljs-string">"GET"</span>) {
<span class="hljs-keyword">if</span> (type === <span class="hljs-string">"multipart/form-data"</span>) {
<span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">await</span> request.formData();
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">await</span> fs.access(uploadDir, fs.constants.F_OK);
} <span class="hljs-keyword">catch</span> (ex) {
<span class="hljs-comment">// 建立上傳的資料夾</span>
<span class="hljs-keyword">await</span> fs.mkdir(uploadDir, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });
}
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> [k, v] <span class="hljs-keyword">of</span> formData.entries()) {
<span class="hljs-built_in">console</span>.log({ k, v });
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> v === <span class="hljs-string">"string"</span>) {
<span class="hljs-comment">// 處理文字欄位</span>
<span class="hljs-keyword">if</span> (!result.fields[k]) {
result.fields[k] = v;
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">if</span> (result.fields[k] <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Array</span>) {
<span class="hljs-keyword">const</span> strArray = result.fields[k] <span class="hljs-keyword">as</span> string[];
strArray.push(v);
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">const</span> val = result.fields[k] <span class="hljs-keyword">as</span> string;
result.fields[k] = [val, v];
}
}
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (v <span class="hljs-keyword">instanceof</span> Blob) {
<span class="hljs-comment">// 處理檔案欄位</span>
result.files = result.files || {};
<span class="hljs-keyword">const</span> { size, type, name, lastModified } = v;
<span class="hljs-keyword">if</span> (acceptedMimeTypes.length) {
<span class="hljs-keyword">if</span> (!acceptedMimeTypes.includes(type)) {
<span class="hljs-keyword">continue</span>;
}
<span class="hljs-keyword">let</span> filename = name;
<span class="hljs-keyword">if</span> (useUuidFilename) {
<span class="hljs-keyword">let</span> tmpName = name.toLowerCase();
<span class="hljs-keyword">let</span> mainName = uuidV4();
<span class="hljs-keyword">let</span> extName = <span class="hljs-string">""</span>;
<span class="hljs-keyword">if</span> (tmpName.indexOf(<span class="hljs-string">"."</span>) !== <span class="hljs-number">-1</span>) {
<span class="hljs-keyword">const</span> frs = tmpName.split(<span class="hljs-string">"."</span>);
extName = <span class="hljs-string">"."</span> + frs[frs.length - <span class="hljs-number">1</span>];
}
filename = mainName + extName;
}
<span class="hljs-keyword">const</span> path = uploadDir + <span class="hljs-string">"/"</span> + filename;
<span class="hljs-keyword">const</span> fileData: FileDataType = {
size,
type,
lastModified,
<span class="hljs-attr">originalName</span>: name,
filename,
path,
};
<span class="hljs-keyword">if</span> (!result.files[k]) {
result.files[k] = fileData;
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">if</span> (result.files[k] <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Array</span>) {
<span class="hljs-keyword">const</span> fdArray = result.files[k] <span class="hljs-keyword">as</span> FileDataType[];
fdArray.push(fileData);
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">const</span> val = result.files[k] <span class="hljs-keyword">as</span> FileDataType;
result.files[k] = [val, fileData];
}
}
<span class="hljs-keyword">await</span> fs.writeFile(path, v.stream() <span class="hljs-keyword">as</span> any);
}
}
}
}
}
<span class="hljs-keyword">return</span> result;
}
</div></code></pre>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-21053099331963747922023-10-22T15:17:00.002+08:002023-10-22T23:47:47.168+08:00RemixJS 2 裡的 link 和 meta 標籤<h1 id="remixjs-2-%E8%A3%A1%E7%9A%84-link-%E5%92%8C-meta-%E6%A8%99%E7%B1%A4">RemixJS 2 裡的 link 和 meta 標籤</h1>
<p>SSR 的特色是可以讓每個頁面擁有各自的 SEO 設定,同時也可以有各自的 link 和 meta 設定。</p>
<h2 id="link-%E6%A8%99%E7%B1%A4">link 標籤</h2>
<p>可以在 root.tsx 使用 <code>links()</code> 設定整個網站都通用的 link 標籤。</p>
<pre class="hljs"><code><div><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> links: LinksFunction = <span class="hljs-function"><span class="hljs-params">()</span> =></span> [
...(cssBundleHref ? [{ <span class="hljs-attr">rel</span>: <span class="hljs-string">"stylesheet"</span>, <span class="hljs-attr">href</span>: cssBundleHref }] : []),
{
<span class="hljs-attr">rel</span>: <span class="hljs-string">"stylesheet"</span>,
<span class="hljs-attr">href</span>: <span class="hljs-string">"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"</span>,
},
{
<span class="hljs-attr">rel</span>: <span class="hljs-string">"stylesheet"</span>,
<span class="hljs-attr">href</span>: <span class="hljs-string">"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"</span>,
},
];
</div></code></pre>
<p>在 Layout 架構中,links() 的功能是堆疊起來的,以下式的架構來說明。</p>
<pre class="hljs"><code><div>app/root.tsx
app/routes/address-book.tsx
app/routes/address-book.add.tsx
</div></code></pre>
<p><code>/address-book/add</code> 的頁面最後取得的 link 標籤會依序是三個檔加起來的設定。</p>
<h2 id="meta-%E6%A8%99%E7%B1%A4">meta 標籤</h2>
<p>meta 標籤的運作則和 link 標籤不同,在 root.tsx 可以直接設定全站的 meta 標籤。
<code><Meta /></code> 是用來載入子元件的 meta 設定。</p>
<pre class="hljs"><code><div><span class="hljs-comment">// root.tsx 片段</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> loaderData = useLoaderData<<span class="hljs-keyword">typeof</span> loader>()
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">head</span>></span>
<span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">charSet</span>=<span class="hljs-string">"utf-8"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">Meta</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">Links</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">head</span>></span>
<span class="hljs-tag"><<span class="hljs-name">body</span>></span>
<span class="hljs-tag"><<span class="hljs-name">Outlet</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">ScrollRestoration</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">Scripts</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">LiveReload</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">body</span>></span>
<span class="hljs-tag"></<span class="hljs-name">html</span>></span></span>
);
}
</div></code></pre>
<p>但在 Layout 的結構中,<code>meta()</code> 並不會疊加之前的設定,而是以最後設定的為主。
另外,在同一個檔案 <code>loader()</code> 會比 <code>meta()</code> 先執行,所以 meta() 可以收到 loader() 回傳的資料。</p>
<pre class="hljs"><code><div><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> meta: MetaFunction = <span class="hljs-function">(<span class="hljs-params">{ data, params, location, matches }</span>) =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"address-book meta"</span>);
<span class="hljs-built_in">console</span>.log(data); <span class="hljs-comment">// { shin: "der" }</span>
<span class="hljs-keyword">return</span> [
{ <span class="hljs-attr">title</span>: <span class="hljs-string">"通訊錄"</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"keyword"</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">"address-book"</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"name"</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">"shin"</span> },
];
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request, params }: LoaderFunctionArgs</span>) </span>{
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"address-book loader"</span>);
<span class="hljs-keyword">return</span> { <span class="hljs-attr">shin</span>: <span class="hljs-string">"der"</span> };
}
</div></code></pre>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-64705967183438345222023-10-22T10:07:00.000+08:002023-10-22T10:07:26.720+08:00RemixJS 2 loader 函式<h1 id="remixjs-2-loader-%E5%87%BD%E5%BC%8F">RemixJS 2 loader 函式</h1>
<p>RemixJS 使用 react ,是架構簡潔的 SSR (Server Side Render) 框架。
它的每個頁面都有可能是 SSR 或 CSR (Client Side Render),所以有特殊的兩個後端執行的函式 <code>loader()</code> 和 <code>action()</code>,頁面內的 components 則無。</p>
<p><code>loader()</code> 的功能是在以 <code>GET</code> 拜訪時,於後端執行,通常是取得頁面要使用的資料。頁面裡 React component 可以使用 useLoaderData() hook 載入 loader() 回傳的資料。</p>
<p>loader() 並非必要的函式。另外,若沒有要回傳資料必須回傳 null。</p>
<pre class="hljs"><code><div><span class="hljs-comment">// app/routes/address-book.edit.$sid.tsx</span>
<span class="hljs-keyword">import</span> { type LoaderFunctionArgs, redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">"@remix-run/node"</span>;
<span class="hljs-keyword">import</span> { useLoaderData } <span class="hljs-keyword">from</span> <span class="hljs-string">"@remix-run/react"</span>;
<span class="hljs-keyword">import</span> dayjs <span class="hljs-keyword">from</span> <span class="hljs-string">"dayjs"</span>;
<span class="hljs-keyword">import</span> db <span class="hljs-keyword">from</span> <span class="hljs-string">"~/modules/mysql-connect"</span>;
<span class="hljs-keyword">import</span> { RowDataPacket } <span class="hljs-keyword">from</span> <span class="hljs-string">"mysql2/promise"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request, params }: LoaderFunctionArgs</span>) </span>{
<span class="hljs-keyword">const</span> sid = params.sid;
<span class="hljs-keyword">const</span> sql = <span class="hljs-string">`SELECT * FROM address_book WHERE sid=?`</span>;
<span class="hljs-keyword">const</span> [rows] = <span class="hljs-keyword">await</span> db.query<RowDataPacket[]>(sql, [sid]);
<span class="hljs-keyword">if</span> (rows.length) {
rows[<span class="hljs-number">0</span>].birthday = dayjs(rows[<span class="hljs-number">0</span>].birthday).format(<span class="hljs-string">"YYYY-MM-DD"</span>);
<span class="hljs-keyword">return</span> rows[<span class="hljs-number">0</span>]; <span class="hljs-comment">// useLoaderData() 可以取得此處回傳的資料</span>
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/address-book"</span>);
}
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AddressBookEdit</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> data = useLoaderData();
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"container"</span>></span>
<span class="hljs-tag"></<span class="hljs-name">div</span>></span></span>
);
}
</div></code></pre>
<h2 id="layout-%E7%9A%84-loader">Layout 的 loader()</h2>
<p>每層的 Layout 都有可能需要個別的資料呈現,所以每層的 loader() 會依序觸發。以檔案 <code>app/routes/address-book.add.tsx</code> 為例。依序會觸發下列檔案的 loader()。</p>
<pre class="hljs"><code><div>app/root.tsx
app/routes/address-book.tsx
app/routes/address-book.add.tsx
</div></code></pre>
<p>然而,依邏輯 Layout 不應該有 action(),就算有也不會依機制呼叫。</p>
<h2 id="api%E5%8A%9F%E8%83%BD-%E6%B2%92%E6%9C%89%E9%A0%81%E9%9D%A2%E5%87%BD%E5%BC%8F%E6%99%82">API功能 (沒有頁面函式時)</h2>
<p>如果該路由不是要呈現 HTML 頁面,而是提供 JSON 格式的資料時,可以只定義 loader() 而不定義頁面 react component 函式。</p>
<pre class="hljs"><code><div><span class="hljs-keyword">import</span> {
type LoaderFunctionArgs,
json,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@remix-run/node"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loader</span>(<span class="hljs-params">{ request, params }: LoaderFunctionArgs</span>) </span>{
<span class="hljs-keyword">return</span> json({ <span class="hljs-attr">name</span>: <span class="hljs-string">"Shinder"</span>, <span class="hljs-attr">method</span>: request.method });
}
</div></code></pre>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-23949411834609022682023-10-21T22:25:00.000+08:002023-10-21T22:25:44.761+08:00RemixJS 2 的路由架構<h1 id="remixjs-2-%E7%9A%84%E8%B7%AF%E7%94%B1%E6%9E%B6%E6%A7%8B">RemixJS 2 的路由架構</h1>
<p>NestJS 13 推出 app router 架構快一年了,把原本 "資料夾" 和 "檔案" 設定並存的方式,大刀闊斧改成以資料夾為主的路由設定方式。</p>
<p>反之,RemixJS 2 則是轉向以 "檔案" 為主的路由設定方式。如果以中小型網站的角度去看,RemixJS 似乎是比較方便的做法。以下是整理官方文件的說明。</p>
<h2 id="%E4%BB%A5%E6%AA%94%E6%A1%88%E7%82%BA%E5%9F%BA%E6%9C%AC%E6%9E%B6%E6%A7%8B">以檔案為基本架構</h2>
<p>檔案資料夾結構</p>
<pre class="hljs"><code><div>app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts._index.tsx
│ ├── concerts.$city.tsx
│ ├── concerts.trending.tsx
│ └── concerts.tsx
└── root.tsx
</div></code></pre>
<table style="border-collapse:collapse;" border="1">
<thead>
<tr>
<th>URL路徑</th>
<th>對應的檔案</th>
<th>Layout (佈局檔)</th>
</tr>
</thead>
<tbody>
<tr>
<td>/</td>
<td>app/routes/_index.tsx</td>
<td>app/root.tsx</td>
</tr>
<tr>
<td>/about</td>
<td>app/routes/about.tsx</td>
<td>app/root.tsx</td>
</tr>
<tr>
<td>/concerts</td>
<td>app/routes/concerts._index.tsx</td>
<td>app/routes/concerts.tsx</td>
</tr>
<tr>
<td>/concerts/trending</td>
<td>app/routes/concerts.trending.tsx</td>
<td>app/routes/concerts.tsx</td>
</tr>
<tr>
<td>/concerts/salt-lake-city</td>
<td>app/routes/concerts.$city.tsx</td>
<td>app/routes/concerts.tsx</td>
</tr>
</tbody>
</table>
<p>歸納幾點規則:</p>
<ol>
<li>根目錄檔案為 <code>_index.tsx</code></li>
<li>檔案名稱中以點「.」做為分隔的符號,表示路徑的分段。 <code>concerts.trending.tsx</code> 表示 <code>/concerts.trending</code> 路徑。</li>
<li>動態路由使用美金符號為分段開頭,例如上表中的 <code>concerts.$city.tsx</code>。</li>
<li>經過的佈局檔,會由 root.tsx 為最開頭。以 <code>app/routes/concerts.trending.tsx</code> 為例,經過的佈局檔會為 <code>root.tsx</code> 再來才是 <code>concerts.tsx</code>。佈局檔和葉路由檔會分別被編譯為個別的 js 檔以方便動態載入。</li>
</ol>
<p>下表為更複雜的狀況:</p>
<table style="border-collapse:collapse;" border="1">
<thead>
<tr>
<th>URL路徑</th>
<th>對應的檔案</th>
</tr>
</thead>
<tbody>
<tr>
<td>/address-book/edit/abc</td>
<td>app/routes/address-book.edit.abc.tsx</td>
</tr>
<tr>
<td>/address-book/edit/123</td>
<td>app/routes/address-book.edit.$sid.tsx</td>
</tr>
<tr>
<td>/address-book/edit/abc/def</td>
<td>app/routes/address-book.edit.abc.def.tsx</td>
</tr>
</tbody>
</table>
<h2 id="%E4%BB%A5%E8%B3%87%E6%96%99%E5%A4%BE%E7%82%BA%E6%9E%B6%E6%A7%8B">以資料夾為架構</h2>
<p>也可以使用資料夾為架構,不過只能在 routes 目錄下的第一層,更內層的資料夾就沒有效果了。
資料夾的命名規則同上述的檔案命名規則,而對應的頁面檔案名稱需為 <code>route.tsx</code>。</p>
<pre class="hljs"><code><div>app/
├── routes/
│ ├── _index/
│ │ ├── signup-form.tsx
│ │ └── route.tsx
│ ├── about/
│ │ ├── header.tsx
│ │ └── route.tsx
│ ├── concerts/
│ │ ├── favorites-cookie.ts
│ │ └── route.tsx
│ ├── concerts.$city/
│ │ └── route.tsx
│ ├── concerts._index/
│ │ ├── featured.tsx
│ │ └── route.tsx
│ └── concerts.trending/
│ ├── card.tsx
│ ├── route.tsx
│ └── sponsored.tsx
└── root.tsx
</div></code></pre>
<h2 id="%E8%87%AA%E8%A8%82%E8%B7%AF%E7%94%B1">自訂路由</h2>
<p>一般使用預設路由規則即可,在某些情況下要自訂路由對應可以設定 <code>remix.config.js</code> 檔,請參考<a href="https://remix.run/docs/en/2.1.0/discussion/routes#manual-route-configuration">官方文件</a>說明。</p>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-83546565204310312972023-09-04T00:43:00.000+08:002023-09-04T00:43:30.717+08:00NextJS app router 實作 body-parser 功能<h1>NextJS app router 實作 body-parser 功能</h1>
<p>承上篇,NextJS 的 app router 所使用的 request 和 response 分別是 NextRequest 和 NextResponse 類型;又分別是 JS 裡 Request 和 Response 類型的延伸。所以解析 http body 要遵循 Request 和 Response 類型的 api 用法。</p>
<p>底下的工具,借用 qs 套件的功能解析 urlencoded 的格式。實作了三個功能:</p>
<ol>
<li>getQueryString(): 解析 query string parameters</li>
<li>getBodyData(): 解析表單資料(包含 multipart 格式)</li>
<li>getMultipartData(): 解析上傳的表單</li>
</ol>
<pre class="hljs"><code><div><span class="hljs-comment">// *** request-parser.js</span>
<span class="hljs-keyword">import</span> qs <span class="hljs-keyword">from</span> <span class="hljs-string">"qs"</span>;
<span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"node:fs/promises"</span>;
<span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> uuidV4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-comment">/** 解析 query string */</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getQueryString</span>(<span class="hljs-params">request</span>) </span>{
<span class="hljs-keyword">return</span> qs.parse(request.url.split(<span class="hljs-string">"?"</span>)[<span class="hljs-number">1</span>]);
}
<span class="hljs-comment">/** 解析 urlencoded, json, multipart */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getBodyData</span>(<span class="hljs-params">request</span>) </span>{
<span class="hljs-keyword">let</span> result = {};
<span class="hljs-keyword">const</span> method = request.method.toUpperCase();
<span class="hljs-keyword">const</span> type = request.headers.get(<span class="hljs-string">"Content-Type"</span>);
<span class="hljs-keyword">if</span> (method !== <span class="hljs-string">"GET"</span>) {
<span class="hljs-keyword">if</span> (type === <span class="hljs-string">"application/x-www-form-urlencoded"</span>) {
<span class="hljs-keyword">const</span> txt = <span class="hljs-keyword">await</span> request.text();
result = txt.length ? qs.parse(txt) : {};
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (type === <span class="hljs-string">"application/json"</span>) {
<span class="hljs-keyword">try</span> {
result = <span class="hljs-keyword">await</span> request.json();
} <span class="hljs-keyword">catch</span> (ex) {}
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">const</span> { fields } = <span class="hljs-keyword">await</span> getMultipartData(request);
<span class="hljs-keyword">if</span> (fields) result = { ...fields };
}
}
<span class="hljs-keyword">return</span> result;
}
<span class="hljs-comment">/** 解析 multipart */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getMultipartData</span>(<span class="hljs-params">
request, <span class="hljs-regexp">//</span> NextRequest 物件
acceptedMimetypes = [], <span class="hljs-regexp">//</span> 篩選類型設定,預設為不篩選
useUuidFilename = true, <span class="hljs-regexp">//</span> 使用隨機 uuid 為主檔名
uploadDir = <span class="hljs-string">"./tmp"</span> <span class="hljs-regexp">//</span> 上傳的資料夾
</span>) </span>{
<span class="hljs-keyword">let</span> result = {
<span class="hljs-attr">fields</span>: <span class="hljs-literal">undefined</span>,
<span class="hljs-attr">files</span>: <span class="hljs-literal">undefined</span>,
<span class="hljs-attr">error</span>: <span class="hljs-literal">undefined</span>,
};
<span class="hljs-keyword">const</span> method = request.method.toUpperCase();
<span class="hljs-keyword">let</span> type = request.headers.get(<span class="hljs-string">"Content-Type"</span>);
<span class="hljs-keyword">if</span> (!type) <span class="hljs-keyword">return</span> result;
type = type.split(<span class="hljs-string">";"</span>)[<span class="hljs-number">0</span>]; <span class="hljs-comment">// 取得 mimetype</span>
<span class="hljs-keyword">if</span> (method !== <span class="hljs-string">"GET"</span>) {
<span class="hljs-keyword">if</span> (type === <span class="hljs-string">"multipart/form-data"</span>) {
<span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">await</span> request.formData();
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">await</span> fs.access(uploadDir, fs.constants.F_OK);
} <span class="hljs-keyword">catch</span> (ex) {
<span class="hljs-comment">// 建立上傳的資料夾</span>
<span class="hljs-keyword">await</span> fs.mkdir(uploadDir, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });
}
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> [k, v] <span class="hljs-keyword">of</span> formData.entries()) {
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> v === <span class="hljs-string">"string"</span>) {
<span class="hljs-comment">// 處理文字欄位</span>
result.fields = result.fields || {};
<span class="hljs-keyword">if</span> (!result.fields[k]) {
result.fields[k] = v;
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">if</span> (result.fields[k] <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Array</span>) {
result.fields[k].push(v);
} <span class="hljs-keyword">else</span> {
result.fields[k] = [result.fields[k], v];
}
}
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (v <span class="hljs-keyword">instanceof</span> Blob) {
<span class="hljs-comment">// 處理檔案欄位</span>
result.files = result.files || {};
<span class="hljs-keyword">const</span> { size, type, name, lastModified } = v;
<span class="hljs-keyword">if</span> (acceptedMimetypes.length) {
<span class="hljs-keyword">if</span> (!acceptedMimetypes.includes(type)) {
<span class="hljs-keyword">continue</span>;
}
<span class="hljs-keyword">let</span> filename = name;
<span class="hljs-keyword">if</span> (useUuidFilename) {
<span class="hljs-keyword">let</span> tmpName = name.toLowerCase();
<span class="hljs-keyword">let</span> mainName = uuidV4();
<span class="hljs-keyword">let</span> extName = <span class="hljs-string">""</span>;
<span class="hljs-keyword">if</span> (tmpName.indexOf(<span class="hljs-string">"."</span>) !== <span class="hljs-number">-1</span>) {
<span class="hljs-keyword">const</span> frs = tmpName.split(<span class="hljs-string">"."</span>);
extName = <span class="hljs-string">"."</span> + frs[frs.length - <span class="hljs-number">1</span>];
}
filename = mainName + extName;
}
<span class="hljs-keyword">const</span> path = uploadDir + <span class="hljs-string">"/"</span> + filename;
<span class="hljs-keyword">const</span> fileData = {
size,
type,
lastModified,
<span class="hljs-attr">originalName</span>: name,
filename,
path,
};
<span class="hljs-keyword">if</span> (!result.files[k]) {
result.files[k] = fileData;
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">if</span> (result.files[k] <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Array</span>) {
result.files[k].push(fileData);
} <span class="hljs-keyword">else</span> {
result.files[k] = [result.files[k], fileData];
}
}
<span class="hljs-keyword">await</span> fs.writeFile(path, v.stream());
}
}
}
}
}
<span class="hljs-keyword">return</span> result;
}
</div></code></pre>
<p>以下是在 route.js 中的用法範例。</p>
<pre class="hljs"><code><div><span class="hljs-keyword">import</span> {
getBodyData,
getQueryString,
getMultipartData,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@/modules/request-parser"</span>;
<span class="hljs-keyword">const</span> responseInit = {
<span class="hljs-attr">status</span>: <span class="hljs-number">200</span>,
<span class="hljs-attr">headers</span>: {
<span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
},
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">request</span>) </span>{
<span class="hljs-keyword">let</span> output = getQueryString(request);
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify(output), responseInit);
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">request</span>) </span>{
<span class="hljs-keyword">let</span> body = <span class="hljs-keyword">await</span> getBodyData(request);
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify(body), responseInit);
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PUT</span>(<span class="hljs-params">request</span>) </span>{
<span class="hljs-keyword">let</span> multi = <span class="hljs-keyword">await</span> getMultipartData(request, [<span class="hljs-string">"image/jpeg"</span>]);
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify(multi), responseInit);
}
</div></code></pre>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-36993830467327540542023-09-03T20:33:00.000+08:002023-09-03T20:33:40.422+08:00NextJS app router 實作 session 功能<h1>NextJS app router 實作 session 功能</h1>
<p>NextJS 的 app router 所使用的 request 和 response 分別是 NextRequest 和 NextResponse 類型;又分別是 JS 裡 Request 和 Response 類型的延伸。而不像 NodeJS 裡的 IncomingMessage 和 ServerResponse,有著彈性的功能。</p>
<p>比較大的問題是,ExpressJS 裡的 request 和 response 是從頭「流」到尾的機制。我們可以很方便在 middlewares 處理完資料,掛載資料到 request 或 response.locals 的屬性上。</p>
<p>然而 NextJS 的 middleware 卻不是這樣運作的,request 物件可能是透過 clone 的方式再往下一個階段傳遞。所以在 middleware 取得的 request 和在 route.js 裡拿到的 request 是不相同(不同參照)的物件。如此就無法藉由 request 物件來傳遞資料。沒有接收 response 物件的情況下,更難使用 response 物件。</p>
<p>後來才找到 <a href="https://github.com/vercel/next.js/discussions/34263">Passing data from middleware to API route</a>,利用 middleware 的 response.rewrite(),將資料放在 cookie 裡,往 end-point 傳遞。</p>
<p>shin-session-obj.js 為實作取得 session 物件的 getSessionObj() 函式。回傳值中若 new_session_id 不為空字串,表示要設定新的 session-id。為了簡明,裡面用 Object 來實作 session 的功能,當然你也可以 DB 或 files 來實作,不過就不在此說明。</p>
<pre class="hljs"><code><div><span class="hljs-comment">// modules/shin-session-obj.js</span>
<span class="hljs-keyword">import</span> { validate, v4 <span class="hljs-keyword">as</span> uuidV4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">let</span> sessions;
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSessionObj</span>(<span class="hljs-params">request</span>) </span>{
<span class="hljs-keyword">let</span> new_session_id = <span class="hljs-string">""</span>;
<span class="hljs-keyword">if</span> (!request) <span class="hljs-keyword">return</span> {}; <span class="hljs-comment">// 沒有 request 時</span>
<span class="hljs-keyword">let</span> sid = request.cookies.get(<span class="hljs-string">"shin-session-id"</span>)?.value;
<span class="hljs-comment">// 沒有 session_id 時, 或 session_id 不是 uuid 時, 設定新的 session-id</span>
<span class="hljs-keyword">if</span> (!sid || !validate(sid)) {
new_session_id = uuidV4();
sid = new_session_id;
<span class="hljs-built_in">console</span>.log({new_session_id})
}
<span class="hljs-comment">// 初始化 sessions</span>
<span class="hljs-keyword">if</span> (!sessions) {
sessions = {};
}
<span class="hljs-comment">// 初始化 session</span>
<span class="hljs-keyword">if</span> (!sessions[sid]) {
sessions[sid] = {};
}
<span class="hljs-keyword">return</span> {
<span class="hljs-attr">session</span>: sessions[sid],
new_session_id,
};
}
</div></code></pre>
<p>在 middleware 會呼叫 getSessionObj() 以測試是否取得 new_session_id。若取得,則在複製的 request 物件裡設定 cookie 內容為我們要傳下傳遞的 session-id;同時,在 response 物件內設定要用戶端儲存的 session-id(儲存於 cookie)。</p>
<pre class="hljs"><code><div><span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { getSessionObj } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/modules/shin-session-obj"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">middleware</span>(<span class="hljs-params">nextRequest, nextFetchEvent</span>) </span>{
<span class="hljs-keyword">const</span> {session, new_session_id} = getSessionObj(nextRequest);
<span class="hljs-keyword">if</span>(new_session_id) {
<span class="hljs-keyword">const</span> clonedRequest = nextRequest.clone();
clonedRequest.headers.append(<span class="hljs-string">"Cookie"</span>, <span class="hljs-string">`shin-session-id=<span class="hljs-subst">${new_session_id}</span>`</span>);
<span class="hljs-keyword">const</span> response = NextResponse.rewrite(nextRequest.url.toString(), {
<span class="hljs-attr">request</span>: clonedRequest,
});
response.cookies.set({
<span class="hljs-attr">name</span>: <span class="hljs-string">"shin-session-id"</span>,
<span class="hljs-attr">value</span>: new_session_id,
<span class="hljs-attr">path</span>: <span class="hljs-string">"/"</span>,
});
<span class="hljs-keyword">return</span> response;
}
}
</div></code></pre>
<p>在任何的 route.js 就可以直接使用 getSessionObj() 取得對應的 session 物件。</p>
<pre class="hljs"><code><div><span class="hljs-keyword">import</span> { getSessionObj } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/modules/shin-session-obj"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">request</span>) </span>{
<span class="hljs-keyword">const</span> {session} = getSessionObj(request);
<span class="hljs-keyword">if</span>(session){
session.count = session.count || <span class="hljs-number">0</span>;
session.count++;
}
<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({session}), {
<span class="hljs-attr">status</span>: <span class="hljs-number">200</span>,
<span class="hljs-attr">headers</span>: {
<span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
},
});
<span class="hljs-keyword">return</span> response;
}
</div></code></pre>
<p>要注意的是此實作,主要是為了說明實作方式,在正式環境使用記憶體存放 session 資料有許多問題。最好還是以 DB 或檔案來存放 session 資料。</p>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-32478285297937680032023-04-08T01:03:00.000+08:002023-04-08T01:03:03.272+08:00建立 fastapi 專案開發環境<h1 id="%E5%BB%BA%E7%AB%8B-fastapi-%E5%B0%88%E6%A1%88%E9%96%8B%E7%99%BC%E7%92%B0%E5%A2%83">建立 fastapi 專案開發環境</h1>
<p>這篇主要是建立基本環境,和執行環境的說明。</p>
<p>安裝所需套件</p>
<pre class="hljs"><code><div>pip install fastapi
pip install uvicorn <span class="hljs-comment"># ASGI server</span>
pip install gunicorn <span class="hljs-comment"># WSGI server</span>
</div></code></pre>
<p>在專案中撰寫測試的程式:</p>
<pre class="hljs"><code><div><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI
app = FastAPI()
<span class="hljs-meta">@app.get('/hello')</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hello</span><span class="hljs-params">()</span>:</span>
<span class="hljs-keyword">return</span> {<span class="hljs-string">"say"</span>: <span class="hljs-string">"hello"</span>}
</div></code></pre>
<p>直接使用 uvicorn 執行:</p>
<pre class="hljs"><code><div>uvicorn main:app --reload --port 8888
</div></code></pre>
<p>執行後可以在 <a href="http://localhost:8888/docs">http://localhost:8888/docs</a> 看到 openAPI document。</p>
<p>gnuicorn 搭配 uvicorn 執行:</p>
<pre class="hljs"><code><div>gunicorn main:app -b 0.0.0.0:8888 -k uvicorn.workers.UvicornWorker
<span class="hljs-comment"># 下式為背景執行</span>
gunicorn main:app -b 0.0.0.0:8888 -k uvicorn.workers.UvicornWorker --daemon
<span class="hljs-comment"># 背景執行時若要停下來,可以使用下式查看 pid 再使用 kill 移除 proccess</span>
ps -ef | grep gunicorn
</div></code></pre>
<p>如果環境中有安裝 PM2 (Nodejs 行程管理工具) 也可以使用 PM2 啟動行程。</p>
<pre class="hljs"><code><div>pm2 --name=gunicorn start <span class="hljs-string">"gunicorn main:app -b 0.0.0.0:8888 -k uvicorn.workers.UvicornWorker"</span>
</div></code></pre>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-2894811699895554202023-04-07T15:52:00.002+08:002023-04-07T15:52:18.298+08:00使用 conda 建立 python 虛擬環境<h1 id="%E4%BD%BF%E7%94%A8-conda-%E5%BB%BA%E7%AB%8B-python-%E8%99%9B%E6%93%AC%E7%92%B0%E5%A2%83">使用 conda 建立 python 虛擬環境</h1>
<p>查看所有的虛擬環境</p>
<pre class="hljs"><code><div>conda env list
</div></code></pre>
<p>使用預設的 python 版本建立環境,下式的 myenv 為虛擬環境的名稱。</p>
<pre class="hljs"><code><div>conda create -n myenv
</div></code></pre>
<p>使用指定的 python 版本建立環境。</p>
<pre class="hljs"><code><div>conda create -n myenv python=3.10
</div></code></pre>
<p>啟用某個環境</p>
<pre class="hljs"><code><div>conda activate myenv
<span class="hljs-comment"># conda deactivate #退出某個環境</span>
pip install --upgrade pip <span class="hljs-comment"># 確保 pip 是最新的版本</span>
</div></code></pre>
<p>安裝套件</p>
<pre class="hljs"><code><div>pip install fastapi
pip install uvicorn
<span class="hljs-comment"># pip install python-multipart # uvicorn 安裝時會自動安裝 python-multipart</span>
</div></code></pre>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-65012516425961446632023-03-26T20:20:00.002+08:002023-04-07T15:49:27.755+08:00將 React app 和 Node.js 寫的 API 放在同一台伺服器<h1 id="%E5%B0%87-react-app-%E5%92%8C-nodejs-%E5%AF%AB%E7%9A%84-api-%E6%94%BE%E5%9C%A8%E5%90%8C%E4%B8%80%E5%8F%B0%E4%BC%BA%E6%9C%8D%E5%99%A8">將 React app 和 Node.js 寫的 API 放在同一台伺服器</h1>
<p>要前後端分離,同時要將 React app 和 API 放在同一台伺服器相同的 port,以避免誇來源的問題。</p>
<p>此時可以用一台 Node express server 解決。</p>
<h2 id="react-app-%E7%99%BC%E4%BD%88%E8%87%B3-express-server-%E9%9D%9C%E6%85%8B%E8%B3%87%E6%96%99%E5%A4%BE">React app 發佈至 Express server 靜態資料夾</h2>
<p>第一種方式是,把發佈後的 React 放在 build 資料夾(別的名稱也可以),並將 build 設定為靜態資料夾。</p>
<p>其餘後端的 API 可以定義在服務靜態 html 檔之前。</p>
<pre class="hljs"><code><div><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"fs/promises"</span>);
<span class="hljs-keyword">const</span> app = express();
app.use(express.static(<span class="hljs-string">"build"</span>));
<span class="hljs-keyword">let</span> html = <span class="hljs-string">""</span>;
fs.readFile(__dirname + <span class="hljs-string">"/build/index.html"</span>).then(<span class="hljs-function">(<span class="hljs-params">txt</span>) =></span> {
html = txt.toString();
});
<span class="hljs-comment">// app.use('/api', YOUR_ROUTER); // 可以把 api 掛在這裡</span>
app.use(<span class="hljs-string">"/api"</span>, (req, res) => {
res.json({ <span class="hljs-attr">name</span>: <span class="hljs-string">"shinder"</span>, <span class="hljs-attr">say</span>: <span class="hljs-string">"hello"</span> });
});
app.use(<span class="hljs-function">(<span class="hljs-params">req, res</span>) =></span> {
res.send(html);
});
<span class="hljs-keyword">const</span> port = <span class="hljs-number">3005</span>;
app.listen(port, () => {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`啟動 <span class="hljs-subst">${port}</span>`</span>);
});
</div></code></pre>
<h2 id="%E4%BD%BF%E7%94%A8%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E4%BC%BA%E6%9C%8D%E5%99%A8">使用反向代理伺服器</h2>
<p>另一種做法是使用反向代理伺服器,可以利用 http-proxy-middleware 套件,建立 proxy。</p>
<pre class="hljs"><code><div><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> { createProxyMiddleware } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"http-proxy-middleware"</span>);
<span class="hljs-keyword">const</span> app = express();
app.use(express.static(<span class="hljs-string">"build"</span>));
<span class="hljs-comment">// app.use('/api', YOUR_ROUTER); // 可以把 api 掛在這裡</span>
app.use(<span class="hljs-string">"/api"</span>, (req, res) => {
res.json({ <span class="hljs-attr">name</span>: <span class="hljs-string">"shinder"</span>, <span class="hljs-attr">say</span>: <span class="hljs-string">"hello"</span> });
});
app.use(
<span class="hljs-string">"/"</span>,
createProxyMiddleware({
<span class="hljs-attr">target</span>: <span class="hljs-string">"http://localhost:3000"</span>,
<span class="hljs-attr">changeOrigin</span>: <span class="hljs-literal">true</span>,
})
);
<span class="hljs-keyword">const</span> port = <span class="hljs-number">3005</span>;
app.listen(port, () => {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`啟動 <span class="hljs-subst">${port}</span>`</span>);
});
</div></code></pre>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-65310522963185778312023-02-10T14:52:00.005+08:002023-02-10T14:52:46.845+08:00MacOS Ventura ssh 問題<div>Mac mini 升級 Ventura 第一個發現的災難,無法使用原先設定好的 auth key 自動登入</div>
<div>解法參考:<a href="https://osxdaily.com/2022/12/22/fix-ssh-not-working-macos-rsa-issue/">https://osxdaily.com/2022/12/22/fix-ssh-not-working-macos-rsa-issue/</a></div>
<br />
<div>基本上是修改設定檔:</div>
<div><span style="background-color: #f3f3f3; color: #303030; font-family: monospace; font-size: 15.6px;">sudo nano /etc/ssh/ssh_config</span></div>
<br />
<div>在設定檔後加入這兩行:</div>
<div><span style="background-color: #f3f3f3; color: #303030; font-family: monospace; font-size: 15.6px;">HostkeyAlgorithms +ssh-rsa</span></div>
<div><span style="background-color: #f3f3f3; color: #303030; font-family: monospace; font-size: 15.6px;">PubkeyAcceptedAlgorithms +ssh-rsa</span></div>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-86031190801891039252022-06-29T23:37:00.003+08:002022-06-29T23:37:42.208+08:00限制 CloudFront CDN 的讀取<h1 id="%E9%99%90%E5%88%B6-cloudfront-cdn-%E7%9A%84%E8%AE%80%E5%8F%96">限制 CloudFront CDN 的讀取</h1>
<h2 id="%E4%BD%BF%E7%94%A8-cloudfront-%E7%9A%84-cdn-%E5%8D%94%E5%8A%A9%E9%9D%9E%E5%85%AC%E9%96%8B-s3-%E8%B3%87%E6%96%99%E7%9A%84%E5%82%B3%E8%BC%B8-%E5%B0%A4%E5%85%B6%E6%98%AF%E5%BD%B1%E7%89%87%E6%AA%94%E5%92%8C%E5%A4%A7%E9%87%8F%E7%9A%84%E5%9C%96%E6%AA%94">使用 CloudFront 的 CDN 協助非公開 S3 資料的傳輸 (尤其是影片檔和大量的圖檔)</h2>
<p>這兩篇介紹的很詳儘:</p>
<p>https://deliciousbrains.com/wp-offload-media/doc/cloudfront-setup/
https://deliciousbrains.com/wp-offload-media/doc/serve-private-media-signed-cloudfront-urls/</p>
<p>A. Create a free HTTPS Certificate</p>
<p>建立免費的 SSL/TLS 認證,建立此認證的用意是讓 CloudFront 可以使用,並指定你自己的網域而不是 CloudFront 提供的預設網域。
注意事項:
1.在 Certificate Manager 建立認證。
2. 要指定區域為 us-east-1 (N. Virginia)
3. Request a public certificate
4. 網域使用包含子網域的設定,例如「.shinder.cc」
5. 使用 DNS Validation 的方式驗證網域</p>
<p>B. Create an Amazon CloudFront Origin Access Identity</p>
<p>建立 OAI 是讓 CloudFront 可以透過 OAI 去讀取 S3 的資料,並同步。這個動作要在設定 CloudFront distribution 前設定,才會是最方便處理的。OAI 的位置在 CloudFront 頁面左側 "Security" > "Origin access identity"。</p>
<p>C. Create an Amazon CloudFront distribution</p>
<p>建立時指定 S3 bucket,並指定使用 OAI,更新 S3 bucket 存取政策。
設定自訂的網域 Alternative domain name (CNAME),並輸入設定第一個動作申請的 SSL 認證。</p>
<p>D. Add a Custom Domain (CNAME) to the distribution</p>
<p>設定 DNS,以自訂的網域指向 CloudFront 預設的網域。</p>
<p>E. Create an Amazon CloudFront Key Pair</p>
<p>在主帳號的下拉選單,點選 Security Credentials 進入 CloudFront Key Pairs 分頁,以建立 key pair。
此組 key pair 用來產生可讀取 CDN 的網域或 cookie。</p>
<p>F. Create a New Amazon CloudFront Behavior for Private Media</p>
<p>在 CloudFront distribution 項目內設定 behavior
設定可讀取的路徑(資料夾前綴), Restrict Viewer Access</p>
<p>G. 使用 aws-cloudfront-sign</p>
<p>最後使用 <a href="https://www.npmjs.com/package/aws-cloudfront-sign">aws-cloudfront-sign</a> 套件,來產生簽證的 URL。</p>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-7490303313521870552022-03-16T20:13:00.004+08:002022-03-16T20:13:52.406+08:00無法使用 FileZilla 透過 sftp 連至遠端 ubuntu<p> 參考<br /><a href="https://www.digitalocean.com/community/questions/filezilla-sftp-error-disconnected-no-supported-authentication-methods-available-server-sent-publickey-2">Filezilla sftp Error: Disconnected: No supported authentication methods available (server sent: publickey)</a><br /><br />解決方式,以 ssh 登入遠端<br />查看檔案<br /><span face="Inter, sans-serif" style="background-color: rgba(242, 201, 76, 0.35); color: #4d5b7c; font-size: 16px;">/etc/ssh/sshd_config</span></p><p>裡面的設定修改為<br /><span face="Inter, sans-serif" style="background-color: rgba(242, 201, 76, 0.35); color: #4d5b7c; font-size: 16px;">PasswordAuthentication yes</span></p><p>重啟伺服器</p>
<p><span face="Inter, sans-serif" style="background-color: rgba(242, 201, 76, 0.35); color: #4d5b7c; font-size: 16px;">service sshd restart</span></p>
<p>或者重啟 ubuntu</p>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-68464141653907614212021-10-29T21:16:00.001+08:002021-10-29T21:16:59.838+08:00SQL 兩點經緯度求距離<h1>SQL 兩點經緯度求距離</h1>
<p>參考資料 <a href="https://stackoverflow.com/questions/1006654/fastest-way-to-find-distance-between-two-lat-long-points">Fastest Way to Find Distance Between Two Lat/Long Points</a>裡的 Binary Worrier 解法。</p>
<p>使用 MariaDB 10,測試的 DB table 為 locations</p>
<pre class="hljs"><code><div><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-string">`locations`</span> (
<span class="hljs-string">`id`</span> <span class="hljs-built_in">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
<span class="hljs-string">`name`</span> <span class="hljs-built_in">varchar</span>(<span class="hljs-number">255</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
<span class="hljs-string">`lat`</span> <span class="hljs-keyword">double</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">COMMENT</span> <span class="hljs-string">'緯度'</span>,
<span class="hljs-string">`lng`</span> <span class="hljs-keyword">double</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">COMMENT</span> <span class="hljs-string">'經度'</span>
) <span class="hljs-keyword">ENGINE</span>=<span class="hljs-keyword">InnoDB</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CHARSET</span>=utf8mb4;
<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> <span class="hljs-string">`locations`</span> (<span class="hljs-string">`id`</span>, <span class="hljs-string">`name`</span>, <span class="hljs-string">`lat`</span>, <span class="hljs-string">`lng`</span>) <span class="hljs-keyword">VALUES</span>
(<span class="hljs-number">1</span>, <span class="hljs-string">'南勢角捷運站'</span>, <span class="hljs-number">24.990508728072932</span>, <span class="hljs-number">121.509155455015</span>),
(<span class="hljs-number">2</span>, <span class="hljs-string">'景安站'</span>, <span class="hljs-number">24.993901994056493</span>, <span class="hljs-number">121.50479966768725</span>),
(<span class="hljs-number">3</span>, <span class="hljs-string">'台大門口'</span>, <span class="hljs-number">25.016772139171792</span>, <span class="hljs-number">121.53351504607843</span>);
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-string">`locations`</span>
<span class="hljs-keyword">ADD</span> PRIMARY <span class="hljs-keyword">KEY</span> (<span class="hljs-string">`id`</span>);
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-string">`locations`</span>
<span class="hljs-keyword">MODIFY</span> <span class="hljs-string">`id`</span> <span class="hljs-built_in">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> AUTO_INCREMENT, AUTO_INCREMENT=<span class="hljs-number">4</span>;
</div></code></pre>
<pre class="hljs"><code><div><span class="hljs-comment">-- 測試用的 SQL</span>
<span class="hljs-comment">-- 以南勢角捷運站為參考位置,求 3 公里以內的景點</span>
<span class="hljs-keyword">SELECT</span> *,
( <span class="hljs-number">6371</span> * <span class="hljs-keyword">ACOS</span>( <span class="hljs-keyword">COS</span>( <span class="hljs-keyword">RADIANS</span>(<span class="hljs-number">24.990508728072932</span>) )
* <span class="hljs-keyword">COS</span>( <span class="hljs-keyword">RADIANS</span>( <span class="hljs-string">`lat`</span> ) )
* <span class="hljs-keyword">COS</span>( <span class="hljs-keyword">RADIANS</span>( <span class="hljs-string">`lng`</span> ) - <span class="hljs-keyword">RADIANS</span>(<span class="hljs-number">121.509155455015</span>) )
+ <span class="hljs-keyword">SIN</span>( <span class="hljs-keyword">RADIANS</span>(<span class="hljs-number">24.990508728072932</span>) )
* <span class="hljs-keyword">SIN</span>( <span class="hljs-keyword">RADIANS</span>( <span class="hljs-string">`lat`</span> ) ) ) ) <span class="hljs-keyword">AS</span> <span class="hljs-string">`distance`</span>
<span class="hljs-keyword">from</span> <span class="hljs-string">`locations`</span>
<span class="hljs-keyword">HAVING</span> <span class="hljs-string">`distance`</span> < <span class="hljs-number">3</span> <span class="hljs-keyword">OR</span> <span class="hljs-string">`distance`</span> <span class="hljs-keyword">IS</span> <span class="hljs-literal">NULL</span>
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> <span class="hljs-string">`distance`</span>;
</div></code></pre>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-45947969483698875482021-10-16T20:34:00.000+08:002021-10-16T20:34:14.532+08:00Nginx 作為均衡負載伺服器 (HTTP load balancer)<h1>Nginx 作為均衡負載伺服器 (HTTP load balancer)</h1>
<p>這裡直接改 <code>/etc/nginx/sites-enabled/default</code> 的設定,其中 upstream 用來定義工作的群組。</p>
<pre class="hljs"><code><div>upstream myapp1 {
ip_hash;
server <span class="hljs-number">10.140</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>:<span class="hljs-number">3000</span> weight=<span class="hljs-number">1</span>;
server <span class="hljs-number">10.140</span><span class="hljs-number">.0</span><span class="hljs-number">.3</span>:<span class="hljs-number">3000</span> weight=<span class="hljs-number">2</span>;
}
server {
listen <span class="hljs-number">80</span>;
location / {
proxy_pass http:<span class="hljs-comment">//myapp1;</span>
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-<span class="hljs-keyword">For</span> $proxy_add_x_forwarded_for;
}
}
</div></code></pre>
<p><code>ip_hash</code> 是決定使用 ip 雜湊運算,讓用戶端的連線固定連到某一台,這樣有利於 session 或 cookie 的運作。</p>
<p><code>weight</code> 的設定為權重,預設為 1。目前的設定,在沒有 ip_hash 的設定時,3 requests 會有 1 個給 10.140.0.2,另外 2 個給 10.140.0.2。有 ip_hash 的設定時,則是以用戶來區分。</p>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-17803848254300266332021-10-16T01:03:00.001+08:002021-10-16T10:52:13.024+08:00Nginx 設定多台 web server<p>Nginx 和 Apache 一樣都可以設定多台 virtual hosts,服務多個站台。在此以 shinder.cc 和 www.shinder.cc 為例說明。系統為 Debian 10,預先安裝 Nginx,PHP 和 certbot 的方式請參考之前的 <a href="https://qops.blogspot.com/2021/06/debian-ningx-php-fpm-certbot.html">在 Debian 上安裝 NginX, PHP-FPM 環境和 Certbot</a>。
以下有三個設定檔,皆位於 <code>/etc/nginx/sites-enabled</code>,其中 default-http 為處理 http 使其轉向到 https,另外兩個則是分別使用不同的後端技術,做為 web server 的服務功能。</p>
<pre class="hljs"><code><div><span class="hljs-comment"># default-http</span>
server {
<span class="hljs-keyword">if</span> ($host = www.shinder.cc) {
<span class="hljs-keyword">return</span> <span class="hljs-number">301</span> https:<span class="hljs-comment">//$host$request_uri;</span>
} <span class="hljs-comment"># managed by Certbot</span>
<span class="hljs-keyword">if</span> ($host = shinder.cc) {
<span class="hljs-keyword">return</span> <span class="hljs-number">301</span> https:<span class="hljs-comment">//$host$request_uri;</span>
} <span class="hljs-comment"># managed by Certbot</span>
listen <span class="hljs-number">80</span> ;
listen [::]:<span class="hljs-number">80</span> ;
server_name www.shinder.cc shinder.cc;
<span class="hljs-keyword">return</span> <span class="hljs-number">404</span>; <span class="hljs-comment"># managed by Certbot</span>
}
</div></code></pre>
<pre class="hljs"><code><div><span class="hljs-comment"># shinder.cc 使用 NodeJS</span>
server {
server_name shinder.cc;
listen <span class="hljs-number">443</span> ssl; <span class="hljs-comment"># managed by Certbot</span>
ssl_certificate /etc/letsencrypt/live/shinder.cc/fullchain.pem; <span class="hljs-comment"># managed by Certbot</span>
ssl_certificate_key /etc/letsencrypt/live/shinder.cc/privkey.pem; <span class="hljs-comment"># managed by Certbot</span>
<span class="hljs-keyword">include</span> /etc/letsencrypt/options-ssl-nginx.conf; <span class="hljs-comment"># managed by Certbot</span>
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; <span class="hljs-comment"># managed by Certbot</span>
location / {
proxy_pass http:<span class="hljs-comment">//127.0.0.1:3000;</span>
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-<span class="hljs-keyword">For</span> $proxy_add_x_forwarded_for;
}
}
</div></code></pre>
<pre class="hljs"><code><div><span class="hljs-comment"># www.shinder.cc 使用 PHP</span>
server {
server_name www.shinder.cc;
listen <span class="hljs-number">443</span> ssl; <span class="hljs-comment"># managed by Certbot</span>
ssl_certificate /etc/letsencrypt/live/shinder.cc/fullchain.pem; <span class="hljs-comment"># managed by Certbot</span>
ssl_certificate_key /etc/letsencrypt/live/shinder.cc/privkey.pem; <span class="hljs-comment"># managed by Certbot</span>
<span class="hljs-keyword">include</span> /etc/letsencrypt/options-ssl-nginx.conf; <span class="hljs-comment"># managed by Certbot</span>
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; <span class="hljs-comment"># managed by Certbot</span>
root /<span class="hljs-keyword">var</span>/www/html;
index index.php index.html index.htm index.nginx-debian.html;
location / {
try_files $uri $uri/ =<span class="hljs-number">404</span>;
}
location ~ \.php$ {
<span class="hljs-keyword">include</span> snippets/fastcgi-php.conf;
fastcgi_pass unix:/<span class="hljs-keyword">var</span>/run/php/php7<span class="hljs-number">.3</span>-fpm.sock;
}
}
</div></code></pre>Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-73475013391067364772021-10-03T10:50:00.001+08:002021-10-03T10:50:41.346+08:00Debian 10 安裝 MySQL 8<h1>Debian 10 安裝 MySQL 8</h1>
<p>參考資料:
<a href="https://computingforgeeks.com/how-to-install-mysql-8-0-on-debian">https://computingforgeeks.com/how-to-install-mysql-8-0-on-debian</a></p>
<p>先更新 repo 資料,並安裝 wget</p>
<pre class="hljs"><code><div>sudo apt update
sudo apt -y install wget
</div></code></pre>
<p>到 <a href="https://repo.mysql.com">https://repo.mysql.com</a> 查看官方 apt repo 資料,例如 mysql-apt-config_0.8.19-1_all.deb,下載並安裝。</p>
<pre class="hljs"><code><div>wget https://repo.mysql.com/mysql-apt-config_0.8.19-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.19-1_all.deb
</div></code></pre>
<p>選擇預設設定即可,選「OK」並按 Enter。接著直接安裝,安裝過程要設定 root 密碼,還有選擇密碼加密的方式,建議使用舊的方式,和 5.X 相容的模式會比較方便:</p>
<pre class="hljs"><code><div>sudo apt update
sudo apt -y install mysql-server
</div></code></pre>
<p>確認安裝的版本,即可登入:</p>
<pre class="hljs"><code><div>apt policy mysql-server
mysql -uroot -p
</div></code></pre>
<p>查看伺服器狀態、關閉、啟動:</p>
<pre class="hljs"><code><div>sudo systemctl status mysql
sudo systemctl stop mysql
sudo systemctl start mysql
</div></code></pre>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0tag:blogger.com,1999:blog-97055916877503749.post-66512116998508933702021-10-02T17:05:00.002+08:002021-10-02T17:05:38.110+08:00GCP 全專案「安全殼層金鑰」設定<h1>GCP 全專案「安全殼層金鑰」設定</h1>
<p>GCP 的全專案「安全殼層金鑰」(ssh public key) 設定,位於「Compute Engine」>「中繼資料(metadata)」>「安全殼層金鑰」,點擊「編輯」用以新增或修改,通常將 <code>~/.ssh/id_rsa.pub</code> 內的資料複製進去即可。該帳號就可以有全專案裡,所有 VM instances 的 sudo 權限。</p>
<p>通常 ssh 用來連線至 VM,所以 ssh public key 設定放在 Compute Engine 的 Metadata 也算是合理的設計。</p>
<p>若要設定特定 VM 的 ssh key,可以到「Compute Engine」>「VM 執行個體」,選擇 VM 後在上方選單點選「編輯」,以進入編輯模式。在「SSH 金鑰」區塊,新增金鑰即可。</p>
Shinderhttp://www.blogger.com/profile/01402644260113429123noreply@blogger.com0