ToCode
Открыть в Telegram
1 419
Подписчики
-224 часа
-17 дней
-130 день
Архив постов
1 419
הפונקציות toReversed ו toSorted ב JavaScript
אני תמיד אוהב כששפת תכנות לוקחת עוד צעד בכיוון הפונקציונאלי, וזה יהיה הסיפור היום עם שתי פונקציות חדשות ב JavaScript - הפונקציות toReversed ו toSorted, הגירסאות הפונקציונאליות של reverse ו sort. בעוד שהפונקציות המקוריות שינו את מבנה הנתונים, הגירסאות החדשות שלהן מחזירות לנו עותק של מבנה הנתונים ממוין או הפוך.
הפונקציה reverse של JavaScript מופעלת על מערך והופכת את האיברים בו:
> const x = [1, 2, 3, 4];
undefined
> x.reverse()
[ 4, 3, 2, 1 ]
הגירסה החדשה שלה toReversed מחזירה עותק חדש של המערך עם האיברים הפוכים:
> const x = [1, 2, 3, 4]
undefined
> x.toReversed()
[ 4, 3, 2, 1 ]
> x
[ 1, 2, 3, 4 ]
נשים לב שכשהמערך מקונן האלמנטים הפנימיים לא מועתקים, לכן:
> const x = [{a: 10}, {a: 10}, {a: 20}, {a: 30}]
undefined
> x.toReversed()
[ { a: 30 }, { a: 20 }, { a: 10 }, { a: 10 } ]
> x.toReversed()[0].a = 40
40
> x
[ { a: 10 }, { a: 10 }, { a: 20 }, { a: 40 } ]
אם במקום היפוך אנחנו רוצים למיין נוכל לקרוא ל toSorted, שהיא הגירסה הפונקציונאלית של sort. כמו sort גם toSorted יכולה לקבל פונקציית מיון או למיין לפי סדר מילוני:
> const values = [1, 10, 21, 2];
undefined
> values.toSorted()
[ 1, 10, 2, 21 ]
> values.toSorted((a, b) => a - b)
[ 1, 2, 10, 21 ]
אפשר לחשוב על שתיהן בתור גירסה מהירה וברורה יותר של array.slice().reverse() ו array.slice().sort:
> x.slice().reverse()
[ 9, 1, 2, 10 ]
> x
[ 10, 2, 1, 9 ]
> x.slice().sort()
[ 1, 10, 2, 9 ]
> x
[ 10, 2, 1, 9 ]1 419
חמישה טיפים ל Pair Programmning יעיל יותר
תכנות בצוות יכול לעזור לנו לכתוב קוד טוב יותר ומהר יותר, אבל גם יכול בקלות להשתבש. המפתח להצלחה, אם כי קשה מאוד ליישום הוא בחירה טובה של השותפים לפרויקט. הנה כמה טיפים שעוזרים לי לזהות אנשים שכדאי לכתוב איתם קוד, ולחלופין אנשים שעדיף לכל הצדדים שנעבוד בנפרד:
1. בחרו חבר או חברה עם רקע שונה משלכם. המטרה של Pair Programming היא לא לכתוב קוד כמה שיותר מהר אלא לדבר על הדברים המשמעותיים בתהליך הפיתוח. ככל שיש פער ברקע שלכם אפשר יהיה לקבל נקודת מבט נוספת ומעניינת על הארכיטקטורה.
2. בחרו טכנולוגיות אותן לפחות אחד הצדדים מבין או מבינה היטב. המטרה של Pair Programming היא הדיון, אז אנחנו רוצים לצמצם זמני Debug וזמני חיפוש באינטרנט.
3. עשו שיעורי בית - לא צריך לכתוב יחד את כל הקוד. נסו להחליט יחד על הארכיטקטורה, לממש יחד את החלקים הקשים והחשובים של הפרויקט, ואז להתקדם בנפרד בחלקים שלא דורשים מחשבה.
4. הקשיבו לצד השני גם (ובמיוחד) כשנשמע שהם אומרים שטויות - זוכרים שהתחלנו עם חבר או חברה עם רקע שונה משלנו? זה אומר שחלק מהדברים שנראים לנו מובנים מאליהם יהיו עבורם חדשים ומפתיעים, וחלק מהדברים שנראים להם מובנים מאליהם יהיו לנו חדשים ומפתיעים. אין לכם כרגע את הכלים להבדיל בין השטויות לדברי החוכמה של הצד השני כי הגעתם מעולמות שונים, ולכן היו מוכנים להקשיב ולנסות רעיונות.
5. גיט סטאש הוא חבר - חלק מהמשחק של לעבוד ביחד זה להיחשף לרעיונות חדשים שלא הייתם מנסים אם הייתם לבד. חלק מהרעיונות האלה טובים, חלק גרועים וחלק לא מספיק מלוטשים. אני משתדל לקבור מהר עם git restore את הרעיונות הגרועים, אבל את הלא מלוטשים להשאיר בצד עם git stash. יום אחד אולי יהיה זמן לחקור אותם שוב. או שלא. וזה בסדר.
יש לכם טיפים נוספים שעבדו? אשמח לשמוע בתגובות פה או בטלגרם.
1 419
עדכון אימג'ים אוטומטי עם Watchtower
נכון, אנחנו לא אמורים להריץ מערכת פרודקשן עם Docker Compose. נכון, הוא לא כזה טוב בהוספת עוד קונטיינרים, בניהול הקונטיינרים שרצים ובכל הדברים שקוברנטס ודומיו יודעים לעשות. אבל אי אפשר להתווכח עם הפשטות שלו ולפעמים כשהפרויקט קטן דוקר קומפוז כן יכול לתת פיתרון סביר, גם אם זמני.
ואם אנחנו כבר מריצים מערכת עם Docker Compose, מדי פעם אנחנו צריכים גם להעלות גירסה חדשה של אחד האימג'ים. בדיוק בשביל זה נוצר Watchtower.
ל Watchtower יש תפקיד אחד פשוט ושם הקסם שלו - פעם ב X זמן (כמה שתחליטו) הוא מתחבר ל Registry, מחפש גירסאות חדשות של האימג'ים ואם הוא מוצא הוא מוריד את הגירסאות החדשות ומעלה מחדש קונטיינרים עם אותן גירסאות חדשות. זה אומר שמספיק לדחוף אימג' ל Registry בשביל לעדכן מכונה רצה וזה כבר טוב.
איך זה עובד? יחסית פשוט. Watchtower הוא סרביס שאנחנו מוסיפים ל docker-compose.yml, אפשר בנוסף לסרביס שלנו או בקומפוז אחר לגמרי:
version: "3"
services:
cavo:
image: ynonp/myapp:latest
ports:
- "443:3443"
- "80:3080"
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 30
כן צריך לשים לב ל-3 נקודות שאפשר ליפול בהן:
1. אם האימג' שמור ברגיסטרי פרטי אז צריך להעביר פנימה לתוך הקונטיינר של watchtower את פרטי ההתחברות, בדרך כלל בצורת מיפוי הקובץ config.json של דוקר פנימה לתוך הקונטיינר.
2. האינטרוול קובע כל כמה זמן צריך לבדוק אם קיימת גירסה חדשה של האימג'. ברירת המחדל היא פעם ביום אז בשביל מנגנון של דיפלוימנט ב CI/CD כדאי להוריד את זה. המספר 30 בדוגמה מציין 30 שניות.
3. אם לא כתבתם אחרת ה watchtower יעדכן את כל האימג'ים בכל הקונטיינרים שרצים על המכונה. אפשר לסנן קונטיינרים אם נעביר אחרי האינטרוול את שם הקונטיינר שאנחנו רוצים לעדכן (שימו לב שזה שם הקונטיינר לא שם הסרביס בקומפוז), או שיטה יותר פשוטה לדעתי היא להגדיר label על כל קונטיינר שצריך עדכון. נשים לב שאם יש לנו מספר פרויקטים שונים עם קבצי docker-compose שונים אז עדיין watchtower של אחד עשוי לשדרג קונטיינרים של הפרויקט השני.
סך הכל שילוב שלושת הסעיפים מביא אותנו ל docker-compose.yml שנראה בערך כך:
services:
db:
image: postgres:16.3
volumes:
- ./tmp/db:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${DBPASSWORD}
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/ynon/.docker/config.json:/config.json
command: --interval 30 --label-enable
web:
image: my.private.registry.com/webapp:latest
labels:
com.centurylinklabs.watchtower.enable: true
environment:
DATABASE_PASSWORD: ${DBPASSWORD}
DATABASE_HOST: db
ports:
- "3000:3000"
depends_on:
- db1 419
עכשיו הפעילו מחדש את השרת בשני חלונות - בחלון אחד נפעיל
./bin/vite dev ובחלון שני ./bin/rails s. אם הכל הלך כמו שצריך תוכלו להיכנס ל localhost:3000 ולראות את הקומפוננטה ובנוסף תוכלו לשנות את קוד הקומפוננטה, לשמור ולראות את העמוד בדפדפן מתעדכן.
בשביל לרנדר קומפוננטה אחרת לפי העמוד אני מעדיף לכתוב קצת קוד JavaScript שיסתכל בעמוד, יטען קומפוננטות בצורה דינמית לפי data attribute של אלמנטים ויאתחל קומפוננטות אלה.
אני משנה את שם הקובץ הראשי מ application.js ל application.jsx וכותב בו את התוכן הבא:
// file: app/frontend/entrypoints/application.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
async function renderAll() {
const reactEntrypoints = document.querySelectorAll('.react');
for (const el of reactEntrypoints) {
const componentName = el.dataset.component;
const Component = (await import(\../components/${componentName}.tsx\)).default;
const root = createRoot(el);
const dataprops = el.dataset.props;
const props = dataprops ? JSON.parse(dataprops) : {};
root.render(<Component {...props} />)
}
}
renderAll();
גם את קוד הקומפוננטה אני מעדכן כי אפשר להוריד משם את ה render (הוא קורה בקוד הראשי) ולכן הקובץ Home.tsx מכיל עכשיו רק את זה:
import React from 'react';
export default function Home({text}: {
text: string
}) {
return <div>
<h1>{text}</h1>
<a href="/about">About Page</a>
</div>
}
ולסיום אני מעדכן את ה View בקובץ app/views/home/index.html.erb כדי ליצור את הקומפוננטה:
<div
class="react"
data-component="Home"
data-props='<%= {text: "hello"}.to_json %>'
></div>
שימו לב שה Properties לקומפוננטה כתובים כאן ב View, זה אומר שנוכל בקלות להעביר אותם מה Controller.
רק בשביל המשחק אני מוסיף דף נוסף לאתר. בקובץ app/controllers./home_controller.rb אני מוסיף עוד פונקציה:
class HomeController < ApplicationController
def index
end
def about
end
end
בתיקיית frontend/components אני יוצר קומפוננטה נוספת בשם About.tsx:
// file: app/frontend/components/About.tsx
import React from 'react';
export default function About() {
return <p>About Us</p>
}
ובקובץ app/views/home/about.html.erb אני כותב את פרטי הקומפוננטה:
<div
class="react"
data-component="About"
></div>
ולסיום בקובץ config/routes.rb אני מוסיף את הנתיב:
Rails.application.routes.draw do
root to: 'home#index'
get '/about', to: 'home#about'
end
עכשיו אפשר לעבור בין שני הדפים ולראות שכל כניסה לדף מציגה את הקומפוננטה שמתאימה לו.
עוד ספריות מומלצות
יש עוד מספר ספריות שלפעמים אני משלב שהופכות את החיבור בין ריאקט לריילס לאפילו יותר מוצלח:
1. הספריה js-from-routes מחברת בין ה Routes שמוגדרים בריילס בתור פונקציות ל JavaScript.
2. הספריה typesfromserializers יודעת לייצר ממשקי טייפסקריפט ישירות מתוך Serializers. סריאקלייזרים למי שלא מכיר זה מנגנון ריילסי שקובע איך מודל יהפוך ל JSON. עם הספריה הזאת יש לנו אוטומטית טיפוסי טייפסקריפט שמתאימים לכל ה JSON-ים שאנחנו שולחים מריילס (בין אם בתור props לקומפוננטות או דרך ה API).1 419
מבנה פרויקט Rails, React ו TypeScript
אנחנו ב 2024, ריילס עדיין שווה את המאמץ וריאקט הפכה לספריית ה UI הדיפולטית של מפתחים רבים, כמובן בשילוב טייפסקריפט. יש המון אפשרויות לשילוב שלושת הטכנולוגיות האלה יחד ואני רוצה להראות כאן שיטה אחת שעבדה בשבילי די טוב.
ריילס
אני מתחיל ביצירת פרויקט ריילס 7 חדש עם:
$ rails new --skip-javascript .
למרות שיש בריילס מנגנון לעבודה עם JavaScript (אפילו שניים), אני מעדיף את vite ולכן אני מתקין את הג'ם vite-ruby לפי הוראות ההתקנה שלהם. מתוך תיקיית הפרויקט אני כותב:
bundle add 'vite_rails'
bundle exec vite install
ואז אפשר לנסות להפעיל את שרת הפיתוח:
$ ./bin/vite dev
ואצלי מופיע הפלט הבא:
The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
VITE v5.2.13 ready in 242 ms
➜ Local: http://localhost:3036/vite-dev/
➜ press h + enter to show help
^C
אני עוצר אותו עם Ctrl+C כדי לתקן כמה קונפיגורציות. מריצים:
$ yarn add vite-plugin-rails typescript react react-dom @vitejs/plugin-react @types/react @types/react-dom
ואז משנים את הקובץ vite.config.ts לתוכן הבא:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import ViteRails from 'vite-plugin-rails'
export default defineConfig({
plugins: [
react(),
ViteRails(),
],
})
ממשיכים בשורת הפקודה ויוצרים את הקונטרולר הראשון:
$ ./bin/rails g controller HomeController index
ואז בקובץ config/routes.rb אני מוסיף הפניה אליו כדי שנוכל לראות אותו בדפדפן:
Rails.application.routes.draw do
root to: 'home#index'
end
מפעילים את השרת עם ./bin/rails s, נכנסים ל localhost:3000 ורואים את הדף שיצרנו.
ריאקט
קובץ ה JavaScript שנטען כשהיישום שלנו עולה הוא app/javascript/entrypoints/application.js. אני יוצר תיקייה חדשה בשם app/frontend/components ובתוכה קובץ חדש בשם Home.tsx. תוכן הקובץ הוא:
import React from 'react';
import { createRoot } from 'react-dom/client';
function Home({text}: {
text: string
}) {
return <h1>{text}</h1>
}
const main = document.querySelector('main')!;
const root = createRoot(main);
root.render(<Home text="Hello World" />);
הקובץ מגדיר את הקומפוננטה הראשונה שלנו וגם מרנדר אותה למסך. זה עדיין לא מספיק טוב כי זה אומר שכל עמוד שניכנס אליו יציג את אותה קומפוננטה. תכף נתקן את זה. אבל קודם בואו נראה שרואים את הקומפוננטה על המסך.
לפני שנוכל להתקדם יש לעדכן את הקובץ app/views/layouts/application.html כך שיוכל לרענן דפי vite וגם יכיל אלמנט main אליו נרנדר את הקומפוננטה שיצרנו. עדכנו את תוכן הקובץ לקוד הבא:
<!DOCTYPE html>
<html>
<head>
<title>RailsReactTypescriptDemo</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<% if Rails.env.development? %>
<script type="module">
import RefreshRuntime from 'http://localhost:3036/vite-dev/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<% end %>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application" %>
<%= vite_client_tag %>
<%= vite_javascript_tag 'application' %>
<!--
If using a TypeScript entrypoint file:
vite_typescript_tag 'application'
If using a .jsx or .tsx entrypoint, add the extension:
vite_javascript_tag 'application.jsx'
Visit the guide for more information: https://vite-ruby.netlify.app/guide/rails
-->
</head>
<body>
<main />
<%= yield %>
</body>
</html>1 419
זמן לימוד ארוך
בבית ספר לימדו אותי שילדים חכמים לומדים דברים מהר יותר. שעדיף ללמוד את החומר בפחות זמן ואם שני ילדים מקבלים את אותו ציון אז זה שלמד פחות הוא היותר חכם.
החיים האמיתיים לימדו אותי את ההיפך. היום אני מבין שמה שאנחנו צריכים זה את היכולת ללמוד משהו לאורך זמן ארוך. את היכולת להתרכז, להעמיק, לשאול עוד שאלות. בהינתן מסלול מספיק ארוך התמדה מנצחת מהירות בכל פעם.
1 419
הכל בסדר טייפסקריפט?
הקוד הבא בטייפסקריפט הצליח לבלבל אותי לרגע-
type MyItem = {type: 'a', url: string, id: number}
function go(item: MyItem) {}
const f = {type: 'a', url: 'url', id: 0}
go(f)
הוא לא מתקמפל.
הפונקציה לא מוכנה לקבל את f כי היא טוענת שהוא מטיפוס לא מתאים. הכל בסדר טייפסקריפט? תסתכל בטיפוס MyItem, זה בדיוק מתאים.
האמת שטייפסקריפט צודק (לפחות חלקית). הדרך בה טייפסקריפט עובד כשיש לי השמה של אוביקט למשתנה היא לנסות להסיק אוטומטית מה הטיפוס הכי הגיוני של אותו משתנה, ולפי הטיפוס הזה הוא מגביל את הפעולות הבאות שאפשר לעשות עם אותו משתנה. במקרה של f הטיפוס שטייפסקריפט הסיק הוא:
const f: {
type: string;
url: string;
id: number;
}
כלומר ה type הוא מסוג string ולא מהסוג הספציפי 'a'. מבחינת טייפסקריפט מותר לשנות את ה type לערך אחר, כלומר הקוד הבא כן מתקמפל:
type MyItem = {type: 'a', url: string, id: number}
function go(item: MyItem) {}
const f = {type: 'a', url: 'url', id: 0}
f.type = 'b';
כשטייפסקריפט רואה את הקריאה:
go(f)
הוא מסתכל רק על הטיפוס של f ועל הטיפוס שהפונקציה צריכה לקבל ומבין שאין התאמה. טייפסקריפט לא יכול להתחייב שאף אחד לא שינה את הערך של type כי מבחינתו מותר לשנות את הערך שם. לכן הוא גם לא יכול להתחייב שמה שנמצא כרגע במשתנה f באמת מתאים לחתימה של הפונקציה.
איך פותרים את זה? אז מסתבר שיש כמה דרכים - קודם כל אפשר להגיד לטייפסקריפט בצורה מפורשת מה הטיפוס של f:
type MyItem = {type: 'a', url: string, id: number}
function go(item: MyItem) {}
const f: MyItem = {type: 'a', url: 'url', id: 0}
עכשיו אפשר יהיה להעביר אותו לפונקציה go ואי אפשר יהיה לשנות את הערך של f.type.
עוד אופציה היא להגדיר ש type הוא קבוע בתוך הגדרת האוביקט:
const f = {type: 'a' as const, url: 'url', id: 0}
ודרך שלישית לקבל את אותו טיפוס היא להגדיר את type ממש להיות a:
const f = {type: 'a' as 'a', url: 'url', id: 0}
מכירים רעיונות נוספים? שתפו בתגובות אשמח לשמוע.1 419
מה map מחזירה
עוד דרך להסתכל על ההבדל בין תכנות מונחה עצמים לתכנות פונקציונאלי היא להסתכל על פונקציות וערכי ההחזר שלהן. ניקח את הפונקציה map לדוגמה ונשווה -
סקאלה, שהיא שפה מונחית עצמים, תגדיר לכל Collection פונקציית map שונה שמחזירה Collection חדש מאותו הסוג. בגלל זה כשאני מריץ map על קבוצה אני מקבל חזרה קבוצה:
scala> Set(1, 2, 3).map(_ * 2)
val res1: Set[Int] = Set(2, 4, 6)
scala> List(1, 2, 3).map(_ * 2)
val res4: List[Int] = List(2, 4, 6)
קלוז'ר לעומתה שמה את הדגש על הפונקציה ולכן הפעלה של map על כל דבר מחזירה תמיד LazySeq:
user=> (type (map (partial * 2) #{1 2 3}))
clojure.lang.LazySeq
user=> (type (map (partial * 2) [1 2 3]))
clojure.lang.LazySeq
ו JavaScript מסכימה להגדיר map רק על רשימה ולכן:
new Set([1, 2, 3]).map(i => i * 2)
TypeError: new Set([1, 2, 3]).map is not a function. (In 'new Set([1, 2, 3]).map(i => i * 2)', 'new Set([1, 2, 3]).map' is undefined)
מה עדיף? לא נעים להגיד אבל אני מצליח לטעות בכל השפות. הדבר החשוב הוא תמיד לזכור באיזה שפה אנחנו ומה המאפיינים של אותה שפה. אפילו (ואולי במיוחד) כשלפונקציות יש את אותו שם.
נ.ב. ומה מחזיר הקוד הבא ברובי? נסו לבדוק ותראו אם הבנתם למה:
3.1.1 :002 > [1, 2, 3].map { _ * 2 }1 419
אופטימיזציית זנב הרקורסיה ב JavaScript ב 2024
לפני 6 שנים כתבתי על אופטימיזציית זנב הרקורסיה בדפדפנים:
https://www.tocode.co.il/blog/2018-09-javascript-tco
מאז הרבה קרה בדפדפן אבל דווקא הפיצ'ר הזה נשאר מאחור. ספארי, כרום וגם פיירפוקס לא מצליחים לחשב רקורסיבית מספרים גדולים.
אבל נקודת אור אחת כן הצטרפה בעולם של JavaScript בצד שרת מהכיוון של bun (נוד ודינו, כמו הדפדפנים עדיין לא תומכים באופטימיזציה זו).
הנה התוכנית:
function factors_of(number, i=2) {
if (number < i) {
return [];
}
if (number % i === 0) {
return [i, ...factors_of(number / i, i)];
}
return factors_of(number, i+1);
}
console.log(factors_of(909090909090909090909090));
והרצה בצד שרת ב node:
node a.js
/Users/ynonp/tmp/a.js:1
function factors_of(number, i=2) {
^
RangeError: Maximum call stack size exceeded
at factors_of (/Users/ynonp/tmp/a.js:1:20)
at factors_of (/Users/ynonp/tmp/a.js:10:12)
at factors_of (/Users/ynonp/tmp/a.js:10:12)
at factors_of (/Users/ynonp/tmp/a.js:10:12)
at factors_of (/Users/ynonp/tmp/a.js:10:12)
at factors_of (/Users/ynonp/tmp/a.js:10:12)
at factors_of (/Users/ynonp/tmp/a.js:10:12)
at factors_of (/Users/ynonp/tmp/a.js:10:12)
at factors_of (/Users/ynonp/tmp/a.js:10:12)
at factors_of (/Users/ynonp/tmp/a.js:10:12)
Node.js v21.7.1
ב Deno:
deno run a.js
error: Uncaught (in promise) RangeError: Maximum call stack size exceeded
function factors_of(number, i=2) {
^
at factors_of (file:///Users/ynonp/tmp/a.js:1:20)
at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
ורק bun מפתיע לטובה:
$ bun a.js
[
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
71, 101, 251, 401, 9384251
]1 419
למה בנית את זה ככה?
נתונה קומפוננטת ריאקט:
function ItemData() {
const { id } = useParams();
const { data, error, isLoading } = useSWR('/api/user', fetcher);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return <ItemView item={data} />
}
הקומפוננטה לוקחת את הפרמטר id מהגדרות הנתיב, פונה לשרת כדי למשוך את המידע על הפריט ואז מציגה את המידע ב ItemView. התצוגה בקומפוננטה נפרדת כדי שאפשר יהיה לבדוק אותה בלי קשר למשיכת המידע וגם זה יותר נוח לתחזק ככה את הקוד, אבל למה לפנות לשרת רק אחרי שהקומפוננטה מתרנדרת כדי למשוך את המידע?
תשובה 1 - כי זה הכי מהיר
פניה שניה לשרת? מה פתאום, פניתי לשרת רק פעם אחת. ה HTML וה JavaScript שמגיעים ללקוח נשלחים מ CDN ולא מהשרת שלי. רק אחרי טעינת העמוד המערכת פונה לשרת שלי כדי למשוך את כל המידע הרלוונטי ולהציג את העמוד.
מה אפשר לעשות אחרת? האמת שטוב ששאלת. אם רשימת הפריטים ידועה מראש אפשר היה לבנות ב Offline עמודי HTML שונים עבור כל id ולשמור את הכל ב CDN, וכך הדפדפן היה יכול להציג את המידע הכי מהר - מיד איך שהוא מקבל את העמוד אפילו בלי להריץ קוד ריאקט. תכל'ס נקסט יודע לעשות את זה אוטומטית.
אם הרשימה דינמית אפשר להשתמש ב Server Components כדי למשוך את כל המידע מבסיס הנתונים לפני ששולחים את העמוד ללקוח וכך לצמצם קריאות. באצת יהיה מעניין לבדוק מבחינת ביצועים איזה גישה נותנת תוצאות טובות יותר.
תשובה 2 - כי זה לא היה מספיק חשוב
פניה שניה לשרת? נו בסדר יש לי עוד 5 מאיפה שזאת באה. למי אכפת כמה פניות לשרת אתה שולח בשביל להציג את התוכן? אני שומר ב DB סטטיסטיקה שמראה של 95% מהלקוחות העמוד נטען תוך פחות משניה וזה כל מה שחשוב. כל עוד אנחנו לא חורגים מתקציב הביצועים שלנו עדיף להתמקד בפיצ'רים ולא באופטימיזציות.
תשובה 3 - כי זה מה שהיה כתוב בתיעוד
כל מי שכותב ריאקט יודע ש react-query ו swr הן הדרך הכי טובה למשוך מידע מהשרת. מה רצית שאכתוב useEffect שם? חוץ מזה הכל עובד אז לא נוגעים.
תשובה טובה ל"למה בנית את זה ככה" צריכה לפתוח דיון. צריכה לחשוף עוד אופציות ואת מערך השיקולים שהוביל לבחירה בדרך מסוימת. אם אין לכם עדיין תשובה טובה זה אולי זמן טוב להתחיל לחפש.1 419
שלושה יתרונות של ניהול הרשאות מבוסס Policy
ניהול הרשאות במערכת זה המנגנון שמאפשר לנו מרגע שמשתמש התחבר להבין מה מותר לאותו משתמש לעשות. באנגלית זה נקרא Authorization ובדרך כלל אנחנו מפרידים את מנגנון ניהול ההרשאות ממנגנון ההזדהות (Authentication). ההזדהות מאפשרת לי להגיד שמי שנכנס הרגע הוא משתמש X וניהול ההרשאות זה מה שמאפשר לי להגיד שלמשתמש X מותר להיכנס לדף מסוים.
במנגנון ניהול ההרשאות אנחנו מוצאים שתי גישות מרכזיות (עם ספריות קוד תואמות). הגישה הראשונה היא ניהול הרשאות מבוסס משתמש. פה יש לנו ב Rails את cancancan וב JavaScript/Typescript את casl. בגישה זאת אנחנו מתחילים את התוכנית בלקחת אוביקט משתמש ו"להדביק" לו הרשאות:
import { defineAbility } from '@casl/ability';
export default (user) => defineAbility((can) => {
can('read', 'Article');
if (user.isLoggedIn) {
can('update', 'Article', { authorId: user.id });
can('create', 'Comment');
can('update', 'Comment', { authorId: user.id });
}
});
הרבה פעמים אני מחלק את קטע הקוד הזה לפונקציות ותהיה לי פונקציה אחת שמטפלת במשתמש מסוג "גולש רגיל" פונקציה אחרת ל"גולש מחובר" פונקציה שלישית ל"מנוי" ורביעית ל"מנהל האתר". בכל פונקציה אני מדביק למשתמש את ההרשאות המתאימות.
פה באתר למשל אני משתמש בספריה כזאת בקוד ריילס ויש לי הרשאות כמו:
def guest_user(user)
can :read, BlogPost do |post|
post.published_at <= Time.now
end
can :read, Lesson, free: true
end
גישה כזאת עובדת מאוד טוב בתחילת הפרויקט או בפרויקטים קטנים, כל עוד כל ההרשאות נכנסות בקובץ אחד.
גישה שנייה לניהול הרשאות היא ניהול מבוסס מדיניות. בגישה זו אני מגדיר Policy לגישה ל"דבר" מסוים ובתוך המדיניות אני מגדיר מי רשאי לעשות מה עם אותו דבר. זאת הגישה שאנחנו פוגשים בספריות ריילס כמו pundit ו action_policy, פאנדיט גם זמינה ב JavaScript. הנה קטע מתוך התיעוד של action_policy כדי שנראה את ההבדל:
class PostPolicy < ApplicationPolicy
# everyone can see any post
def show?
true
end
def update?
# \user\ is a performing subject,
# \record\ is a target object (post we want to update)
user.admin? || (user.id == record.user_id)
end
end
או ב JavaScript מתוך התיעוד של pundit:
import { Policy } from 'pundit'
export default class PostPolicy extends Policy {
constructor(user, record) {
super(user, record)
this.setup.apply(this)
}
edit() {
return this.user.id === this.record.userId
}
destroy() {
return this.user.isAdmin
}
}
שלושה יתרונות מהירים של מדיניות הרשאות מבוססת מודלים הם:
1. כל ההרשאות של מודל מסוים מרוכזות במקום אחד.
2. אפשר לשתף Policy בין כמה מודלים.
3. ניהול שינויים - אם אני רוצה לקבוע ממחר שתוכן מסוג מסוים כבר לא חופשי בגישה הראשונה אני צריך לשנות בשני מקומות (גם בפונקציה שמגדירה הרשאות לאורחים וגם בפונקציה שמגדירה הרשאות למשתמשים רשומים). בגישה השנייה מספיק לעדכן את פונקציית הצפייה ב Policy שמתאים לדבר אותו אני מזיז.