ToCode
Ir al canal en Telegram
1 419
Suscriptores
-124 horas
Sin datos7 días
-230 días
Archivo de publicaciones
1 419
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
אני מוצא בקובץ את השורה:
// "outDir": "./", /* Specify an output folder for all emitted files. */
מוציא אותה מהערה ומעדכן אותה כדי שהקבצים שטייפסקריפט מייצר יישמרו לתיקיה נפרדת:
"outDir": "./dist",
עכשיו אפשר לעדכן את package.json ולהוסיף את פקודת הבניה:
"scripts": {
"build": "tsc",
"dev": "ts-node main.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
ולהפעיל:
$ npm run build
הפקודה תיצור לנו תיקיה בשם dist ובתוכה שני קבצים: main.js ו const.js. אלה קבצי JavaScript שעברו קומפילציה מהטייפסקריפט. עכשיו אפשר להריץ את הפרויקט המקומפל עם:
$ node dist/main.js1 419
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */1 419
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */1 419
# עבודה עם TypeScript בפרויקט Node.JS
בעבודה על פרויקט Node.JS לבחירה בטייפסקריפט יש גם יתרונות וגם חסרונות עליהם נדבר בשיעור זה.
## איך עובדים עם TypeScript ב Node.JS
בניגוד לפרויקטי צד לקוח בהם אנחנו רגילים "לקמפל" את הקוד שלנו עם וובפאק או בייבל, בעבודה עם Node.JS אנחנו רגילים לכתוב קוד בקובץ עם סיומת
.js ופשוט להריץ אותו. המעבר לטייפסקריפט מחייב אותנו ללמוד שיטת עבודה חדשה ולהוסיף עוד כלי בדרך:
1. אנחנו נכתוב קוד TypeScript בקבצים עם סיומת ts.
2. הקומפיילר של טייפסקריפט יהפוך אותם לקבצי JavaScript, ובדרך יבדוק את תקינות הטיפוסים.
3. אנחנו נפעיל node.js כדי להריץ את קבצי ה JavaScript שהקומפיילר יצר.
זה תהליך יותר מסובך וכן יש יותר מקום לטעויות. אני מקווה להראות בשיעור זה שזה לא יותר מדי מסובך, ושאפשר לקבל החזר טוב על ההשקעה.
## יצירת פרויקט Node.JS ו TypeScript
הכלי הראשון שנראה נקרא ts-node והוא מתאים לעבודה במצב פיתוח. כלי זה הוא סוג של תוסף ל node.js שיודע להריץ TypeScript. במקום לדבר בואו נראה את זה בקוד.
בתיקיה חדשה אני כותב את הקובץ main.ts (שימו לב לסיומת ts שמציינת TypeScript), ובו התוכן הבא:
function printTimes(text: string, times: number = 5) {
for (let i=0; i < times; i++) {
console.log(text);
}
}
printTimes("Hello World!", 2);
אפשר לראות שמדובר בקובץ TypeScript גם לפי הסיומת וגם לפי התוכן - ליד כל פרמטר לפונקציה הוספתי את הטיפוס שלו, מתוך טיפוסים שכבר ראינו בשיעורים הקודמים.
באותה תיקיה אני יוצר גם קובץ package.json עם:
$ npm init -y
מוסיף את ts-node:
$ npm install --save-dev ts-node
ומעדכן את הקובץ package.json כדי להוסיף סקריפט הרצה למצב פיתוח:
{
"name": "hello-node-world",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "ts-node main.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"ts-node": "10.9.1"
}
}
עכשיו אפשר להפעיל:
$ npm run dev
והסקריפט שלי רץ עם ts-node שבאופן אוטומטי הופך אותו מ TypeScript ל JavaScript לפני שמפעיל את node עליו. גם אם היה לנו יותר מקובץ אחד אפשר היה לטעון קבצי ts אחרים מהפרויקט והכל היה עובד. לדוגמה אני יוצר קובץ בשם const.ts עם התוכן הבא:
export const foo = "bar";
ומעדכן את main.ts כדי שישתמש בקובץ החדש:
import { foo } from './const';
function printTimes(text: string, times: number = 5) {
for (let i=0; i < times; i++) {
console.log(text);
}
console.log(foo);
}
printTimes("Hello World!", 2);
ושוב הכל עובד ואני מקבל את ההדפסות הנכונות.
## מעבר לפרודקשן וקומפילציה מסודרת של הפרויקט
למרות שמאוד נוח לעבוד עם ts-node כלי זה אינו מתאים לסביבת פרודקשן כיוון שהוא מקמפל כל קובץ כשטוענים אותו. נכון, אפשר לעשות אופטימיזציות, אבל אני מעדיף בפרודקשן לקמפל הכל מראש ולתת ל node רק להריץ. בשביל זה נשתמש בכלי tsc.
תחילה אני מוסיף את tsc לפרויקט עם:
$ npm install --save-dev typescript
באותה תיקיית פרויקט ניצור קובץ הגדרות TypeScript סטנדרטי עם הפקודה:
$ npx tsc --init
וקיבלנו קובץ בשם tsconfig.json ברירת מחדל עם התוכן הבא:
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */1 419
# איך לא רואים אותך בענן?
לא מזמן DHH סיפר שבייסקמפ ו hey מתכננים להגר החוצה מהענן. אני מסכים עם כל מילה שהוא כתב וממליץ לכם לקרוא את הפוסט בקישור, ובכל מקרה הנה עוד כמה סיבות למה גם אני ממליץ ללקוחות קטנים להתרחק מעננים:
1. המעבר מפיתוח לייצור הרבה יותר מסובך ממה שמדמיינים - זה נכון תמיד, אבל בענן הגדרות האבטחה נועדו לאפשר לחברות גדולות להיות מאוד יצירתיות ועדיין מאובטחות. בתור עסק קטן שלא צריך את כל הגמישות הזאת, רק ללמוד איך הכלים עובדים ואיזה הגדרות חשובות יכול להיות מסובך כמו לבנות גירסה ראשונה של המערכת.
2. החיסכון לא גדול כמו שמדמיינים - את הדברים הגדולים הענן לא חוסך. אם יש לי פוסטגרס שרץ בענן אני עדיין אצטרך לשדרג את המידע כשתצא גירסה חדשה, רק שהפעם במקום להיעזר במדריך פוסטגרס הרשמי ולקבל גישה ישירה לבסיס הנתונים אני צריך לעבוד דרך הממשק של חברת הענן. אם אני צריך לגבות את אותו בסיס נתונים אני עדיין צריך להחליט לכמה זמן לשמור את הגיבוי ומתי לשמור גיבוי אינקרמנטלי ומתי גיבוי מלא, רק בלי גישה ישירה לבסיס הנתונים ועם המגבלות של אותה חברת ענן. עכשיו אפשר לטעון שהם חסכו לי עבודה דרך ממשק שורת הפקודה או עריכה של קבצי הגדרות, אבל אני לא חושב שזה חיסכון ששווה לשלם עליו.
3. אפשר לקבל תוצאות טובות ובזול עם VPS או שרתים פיזיים - לינוד עדיין משכירים שרת VPS זול ב 5 דולר בחודש, ומכונות טובות ב 40$ בחודש. כן, נצטרך לעבוד קשה בשביל לקבל High Availability במובן שאם Region שלם נופל המערכת אוטומטית תתחיל לעבוד מ Region אחר או מ VPS אחר, אבל גם זה אפשרי ובכל מקרה לעסקים קטנים לא בטוח כמה זה הכרחי.
4. בעזרת Kubernetes ו Docker (לא מכירים אותם עדיין? יש קורס) אפשר לקבל חוויה מאוד דומה ל Deployment האוטומטי של חברות הענן, אבל בשליטה שלכם ועם תמיכה טובה במצב פיתוח, ואפשרות לעבור מהר בין ספקי תשתית.
5. נכון, האפשרות להקים עוד מאות שרתים בלחיצת כפתור כשהעומס על התשתיות גדל היא מלהיבה. אבל אם אתם עסק קטן ולא סטארט-אפ שחולם על צמיחה, אתם כנראה לא תצטרכו אותה. ברוב המקרים מספיק להוסיף שרת או שניים, מה שאפשר לעשות בקלות עם אנסיבל.
בשורה התחתונה עבודה בקלאוד כמו AWS דורשת לימוד של כלים ושיטות עבודה מאוד ספציפיים לאותה חברת ענן. אנחנו לא חוסכים את עבודת ה IT אנחנו רק מזיזים אותה או דוחים אותה. במקום ללמוד את כל הכלים מראש כדי לקבל מוצר בסיסי בסביבת פיתוח, אנחנו מקבלים "במתנה" מוצר בענן בשלוש לחיצות, אבל משלמים את המחיר (גם בכסף וגם בזמן לימוד) ככל שהמוצר גדל.
1 419
# פורטל לתוך קומפוננטה אחרת
לא מזמן נתקלתי בטריק החמוד הזה בריאקט שרציתי לשתף, לא בשביל שתשתמשו בו בפרודקשן אלא יותר כ Proof Of Concept, כדי להבין עוד דבר או שניים על פורטלים ו ref.
המשחק הוא פשוט - יש לי קומפוננטה שמנהלת סטייט, נניח רשימת משימות, ואני רוצה לכתוב במקום אחר (לא בתוך תת העץ שלה) כמה משימות פתוחות יש במערכת. ביום רגיל היינו לוקחים את הסטייט, מעלים אותו לקומפוננטה הקרובה ביותר בעץ שמשותפת לשתי הקומפוננטות ומעבירים לכל קומפוננטה את החלק שהיא צריכה. אבל היום לא יום רגיל.
## נקודת התחלה
אני מתחיל עם הקומפוננטה הבאה:
function TodoList() {
const [items, setItems] = useState([
{ id: 1, text: "one", done: false },
{ id: 2, text: "two", done: false },
{ id: 3, text: "three", done: false },
{ id: 4, text: "four", done: false }
]);
function toggle(item) {
setItems((oldItems) =>
oldItems.map((it) => (it.id === item.id ? { ...it, done: !it.done } : it))
);
}
return (
<>
<p>Open Tasks: {items.filter((it) => !it.done).length}</p>
<ul>
{items.map((i) => (
<li key={i.id}>
<label>
<input
type="checkbox"
checked={i.done}
onChange={(e) => toggle(i)}
/>
{i.text}
</label>
</li>
))}
</ul>
</>
);
}
אפשר לראות אותה בקודסנדבוקס כאן:
https://codesandbox.io/s/summer-monad-1ezijz?file=/src/App.js:42-837
הקומפוננטה מציגה רשימת פריטים ולחיצה על כל פריט משנה את שדה done שלו. היא גם מציגה טקסט שאומר כמה משימות פתוחות יש.
המשימה היא להעביר את הטקסט לקומפוננטה אחרת, בלי להזיז את הסטייט.
## הפיתרון: פורטל
פיתרון לא שגרתי לבעיה יהיה לבנות פורטל. פורטל זו דרך של קומפוננטה לרנדר חלק מעצמה בתוך "מקום" אחר, כשבדרך כלל המקום האחר הוא אלמנט חדש שהיא יוצרת ב body בשביל להציג דיאלוג מודאלי, אבל אותו מקום אחר יכול להיות גם ref שמגיע מקומפוננטה אחרת.
הטריק היחיד כאן הוא להשתמש ב Callback Ref כדי להעביר את הערך המעודכן של הפורטל לקומפוננטה אחרי כל render.
בתוך קומפוננטת המשימות אני מחליף את פיסקת הטקסט ב:
{portal &&
createPortal(
<p>Open Tasks: {items.filter((it) => !it.done).length}</p>,
portal
)}
כש portal זה property שעובר מבחוץ, ובקומפוננטה החיצונית אני מייצר את האלמנט portal באופן הבא:
export default function App() {
const [portal, setPortal] = useState(null);
return (
<div className="App">
<div ref={(node) => setPortal(node)} />
<TodoList portal={portal} />
</div>
);
}
הדוגמה המלאה בקודסנדבוקס בקישור:
https://codesandbox.io/s/zealous-architecture-nrskck1 419
# מה זה ולמה צריך Callback Ref
אתם כבר יודעים להשתמש ב useEffect כדי להריץ קוד כשמשהו משתנה, ולהשתמש ב useRef כדי לתפוס אלמנט ב DOM אחרי render, אבל בשילוב בין השניים יש טעות שמאוד קל לעשות. בואו נראה קצת קוד.
## הטעות שהכי קל לעשות עם useRef
אחד השימושים של useRef הוא בשילוב בין ספרייה חיצונית לקוד ריאקט. דמיינו למשל ספריה של נגן וידאו שמקבלת אלמנט DOM והופכת אותו לנגן, ואנחנו רוצים לעטוף אותה בקומפוננטת ריאקט שגם תיצור את אותו אלמנט DOM.
כל עוד קומפוננטת ריאקט שלכם משאירה על המסך תמיד את אלמנט ה DOM שהספריה החיצונית מקבלת אפשר להסתדר עם קוד כזה:
function MyPlayer() {
const el = useRef(null);
const player = useRef(null);
useEffect(() => {
if (el.current) {
player.current = new VideoPlayer(el.current);
}
}, []);
return (<div ref={el} /);
}
## למה זה בעיה
הצרות יתחילו כשנצטרך להפעיל מחדש את האפקט אם האלמנט משתנה. קחו לדוגמה את הקומפוננטה הבאה:
function BuggyMeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useRef(null);
useEffect(() => {
const node = measuredRef.current;
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
} else {
setHeight(-1);
}
}, [measuredRef.current]);
return (
<>
<Child ref={measuredRef} />
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
הקומפוננטה מודדת גובה של אלמנט לפי ref שהיא יוצרת, אבל לא היא זאת שיצרה את האלמנט אלא קומפוננטת-ילד. הדרישה היא שכשהקומפוננטת ילד קיימת ומשתמשת ב ref אז יוצג הגובה האמיתי של האלמנט, אבל אם מאיזושהי סיבה הקומפוננטת ילד לא משתמשת ב ref יוצג המספר -1.
קומפוננטת Child היא:
const Child = React.forwardRef(function Child(props, ref) {
const [visible, setVisible] = useState(true);
function toggle() {
setVisible((v) => !v);
}
return (
<>
{visible && <h1 ref={ref}>Hello world</h1>}
<button onClick={toggle}>Toggle</button>
</>
);
});
ואתם יכולים לראות בקודסנדבוקס הבא שהקוד לא עובד:
https://codesandbox.io/s/brave-shtern-b10unz?file=/src/index.js
פשוט לוחצים על כפתור Toggle ורואים שהכותרת נעלמה אבל הגובה הוא עדיין 38 פיקסלים.
הסיבה היא ששינוי בסטייט פנימי של Child לא גרם ל render מחדש של BuggyMeasureExample, ולכן האפקט לא רץ מחדש ואז לא משנה מה כתוב או לא כתוב ברשימת התלויות, כי ריאקט אפילו לא מגיע להשוות את התלויות.
## מה עושים במקום
במקרה הכללי כלל אצבע טוב הוא לא לכתוב אף פעם ref בתוך מערך התלויות של אפקט (ואם כבר כתבתם אף פעם לא לתת את ה ref הזה למישהו אחר).
אם אתם כן צריכים קוד שירוץ כל פעם שאלמנט ב DOM מתעדכן, וכן צריכים לתת את ה ref לאלמנט הזה לקומפוננטה אחרת, תצטרכו להשתמש בטריק שנקרא Callback Ref. זה נראה ככה:
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback((node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
} else {
setHeight(-1);
}
}, []);
return (
<>
<Child ref={measuredRef} />
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
עכשיו useCallback מחזירה פונקציה ולכן measureRef הוא פונקציה. אם ref הוא פונקציה אז באופן אוטומטי ריאקט מפעיל אותה כל פעם אחרי שמתבצע render ומעביר לה את ה node המעודכן. אנחנו לא צריכים כאן מערך תלויות כי זה לא אפקט שגורם להרצה מחדש בכל render אלא מנגנון של ref עם פונקציה. מערך התלויות הריק אומר לריאקט להשתמש באותה פונקציה בין render-ים, וזה אפשרי כי לפונקצייה הפנימית אין תלויות חיצוניות, מלבד setHeight שריאקט מבטיח לנו שיישאר קבוע.1 419
# 3 פיצ'רים עדכניים של Node שאהבתי במיוחד
מצד אחד העובדה שכל שבועיים יוצאת גירסה חדשה של נוד היא קצת מעייפת, ובמיוחד כשנזכרים שברוב הגירסאות יש יותר תיקוני אבטחה מפיצ'רים מלהיבים, אבל בכל זאת פעם בכמה גירסאות יש שינוי שממש כיף לשלב בקוד חדש שכותבים. הנה 3 שינויים כאלה מהתקופה האחרונה שאני משלב בקוד באופן קבוע:
## מעבר ל import
שנים היתה הפרדה בין node לדפדפן כשדפדפנים משתמשים ב import ו node ב require. גם היום בגירסאות עדכניות של נוד, ניסיון להריץ קוד עם import יציג הודעת שגיאה מוזרה:
SyntaxError: Cannot use import statement outside a module
אבל הפעם יש פה יותר מגרעין של אופטימיות - אי אפשר להשתמש ב import מחוץ למודול, אומר שאפשר להשתמש ב import בתוך מודול.
יש שתי דרכים לשכנע את נוד שאנחנו בעולם המודולים, הראשונה היא להוסיף לקובץ package.json את המפתח type עם הערך module, לדוגמה קובץ כזה יעבוד:
{
"name": "games",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "hello.js",
"keywords": [],
"author": "",
"license": "ISC"
}
אפשרות שניה היא לשנות את הסיומת של הקובץ ל mjs. בשני המקרים אחרי השינוי תוכלו לכתוב קוד כמו:
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
const rl = readline.createInterface({ input, output });
const question = "Who are you? "
while(true) {
const answer = await rl.question(question);
console.log(`Welcome, ${answer}`);
}
באותו הקשר עוד שתי פנינים מהקוד הזה:
1. שימו לב לתחילית node: לפני שם המודול. זה אומר שאנחנו טוענים מודול מובנה ב node. לא חייבים לכתוב ככה אבל מומלץ, כי אם תהיה לכם שגיאת כתיב נוד לא יתבלבל וינסה לטעון מודול מספריית node_modules בטעות.
2. ממשק ה Promises ו async/await כבר עובד ממש טוב ב Node, כולל במודולים מובנים ואפילו בלולאה הראשית (לא צריך לכתוב פונקציית main אסינכרונית).
## מצב Watch
זוכרים את nodemon? שנים השתמשנו בו כדי להפעיל מחדש תוכניות נוד כשהקוד משתנה. תשמחו לשמוע שכבר אין בו צורך כי ל node יש מצב ריענון אוטומטי. אם קראתי לתוכנית hello.mjs אני יכול להפעיל משורת הפקודה:
$ node --watch hello.mjs
וכל פעם שהקוד ישתנה בצורה אוטומטית נוד יטען את הקוד העדכני ויפעיל מחדש את התוכנית.
## טעינת קבצי JSON עם import
לא ברור למה אבל לכתוב import לקובץ json זה פשוט הרבה יותר נוח מלקרוא את הקובץ ולפענח את ה JSON. בהנחה שיש לכם קובץ JSON בתיקיה בשם data.json עם התוכן הבא:
{
"text": "Who goes there?"
}
תוכלו לבקש מ Node שיטען אותו וידפיס את הטקסט מתוכו עם הקוד הבא:
import data from './data.json' assert { type: "json" };
console.log(data.text);
וכן ה assert שם בסוף שורת ה import קצת מוזר אבל בשביל פיענוח אוטומטי של JSON-ים אני מוכן לשלם את המחיר.1 419
4. המפתח
endpoints מתאר את נקודות הקצה שיש ב API. הוא מקבל פונקציה שמקבלת אוביקט builder ובאמצעותו בונה את אוביקט נקודות הקצה שהיא מחזירה. מה שחשוב לראות כאן זה את ההפעלה של builder.query לכל נקודת קצה לקריאה - כלומר כזו שקוראת את רשימת הפתקים או כזו שקוראת פתק מסוים. פונקציות אחרות על builder יאפשרו פעולות נוספות כמו mutation שיוצרת נקודת קצה לעדכון.
באופן אוטומטי כל Endpoint שאני מגדיר הופך ל Custom Hook אותו אוכל להפעיל מקומפוננטת ריאקט. לכן בקוד ריאקט עוד מעט אוכל לכתוב:
const { data, error, isLoading } = useGetNotesQuery();
ואני לא צריך להעביר כאן את ה URL או שום דבר, כי הכל כבר כתוב בתוך הסרביס.
בקוד סנדבוקס הזה תוכלו למצוא את התוכנית המלאה שמשתמשת בסרביס שכתבתי ומציגה על המסך רשימה של פתקים:
https://codesandbox.io/s/rtk-demo-fgs64q
מה שמעניין כאן הוא שהתוכנית מגדירה שתי קומפוננטות וכל קומפוננטה מפעילה את useGetNotesQuery. אם תסתכלו ב Network Tab בכלי הפיתוח תוכלו לראות שיש רק פניה אחת ל API כדי לקבל את רשימת הפתקים, ו RTK Query משתמש באותה תוצאה לכל הקומפוננטות.
<iframe src="https://codesandbox.io/embed/rtk-demo-fgs64q?fontsize=14&hidenavigation=1&theme=dark"
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
title="rtk demo"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
## דפדוף בתוצאות
דוגמה אחת ללוגיקה שאפשר להפעיל על התוצאות היא דפדוף: אנחנו רוצים לקבל תמיד רק 20 תוצאות בכל עמוד, ולתת לכל קומפוננטה אפשרות לבקש עמוד ספציפי או מספר אחר של תוצאות בעמוד.
קוד צד השרת כבר תומך בדפדוף באמצעות הפרמטרים page ו limit, ולכן מספיק לי לעדכן בקוד ה service את הפונקציה getNotes כדי שתוסיף פרמטרים אלה עם ערכי ברירת מחדל:
endpoints: (builder) => ({
getNotes: builder.query({
query: ({ page = 1, limit = 20 }) => `/notes?page=${page}&limit=${limit}`
}),
וזה לדעתי הכח של RTK Query - היכולת לשנות במקום אחד ולקבל עדכון אוטומטי של כל הקומפוננטות ביישום שניגשות לנתיב מסוים. שימו לב שהפונקציה ב query חייבת לקבל רק פרמטר אחד, כי היא משתמשת בפרמטר הזה בתור Cache Key, לכן אני מעביר שם אוביקט עם שני השדות page ו limit.
בקוד עצמו אני יכול להחליט להעביר ערכים לפרמטרים אלה, או להישאר עם ערכי ברירות המחדל. אני הוספתי תיבה כדי לבחור את העמוד בקומפוננטה שמציגה את הפתקים, ובקומפוננטה שמציגה את מספר הפתקים העברתי ערך -1 למשתנה limit כדי לבטל את הגבלת התוצאות. הקוד לגירסה המעודכנת בקישור:
https://codesandbox.io/s/rtk-demo-part-2-yxleqj
## לאן ממשיכים
ל RTK Query יש עוד המון יכולות מעבר למה שהראיתי כאן בפוסט, ואני ממליץ לחפש בתיעוד לפי הנושאים הבאים כדי ללמוד עליה יותר:
1. מוטציות - מוטציות מאפשרות לעדכן פריט בשרת באמצעות בקשות POST, PUT או DELETE. אחרי העדכון נרצה לעדכן גם את העותק השמור שלנו של הפריט הזה, וגם למשוך מחדש (re-sync) את העותק המעודכן מהשרת כדי לראות שהעדכון הצליח.
2. שימוש בספריות fetch אחרות - לדוגמה משיכת מידע משרת GraphQL
3. מחיקת פריט מה Cache ושליפתו מחדש
4. שינוי אוטומטי של פורמט התשובה, כשהשרת מחזיר לנו תוצאה בפורמט אחד אבל אנחנו מעדיפים יותר מידע או שדות עם שמות אחרים.
אחד הדברים שאני מאוד אהבתי ב RTK Query הוא התחושה שהם "חשבו על הכל", ושכל פעם שאני צריך להוסיף איזה מנגנון יש דרך פשוטה לקבל אותו. העבודה עם RTK Query דורשת יותר השקעה מראש כדי להבין ולקודד את כל נקודות הקצה ב API, אבל באפליקציות גדולות ההשקעה משתלמת.1 419
# היכרות עם RTK Query
ספריית RTK Query היא ספריית הרחבה ל Redux Toolkit שמאפשרת לנו לכתוב ביישומי צד-לקוח את קוד התקשורת שלנו במקום אחד כדי שיהיה קל לתחזק ולעדכן את הקוד. בפוסט זה אציג אותה דרך כמה דוגמאות ונבין מה הצדדים הטובים והפחות טובים של העבודה איתה.
## מי צריך את RTK Query
ספריית RTK Query מאפשרת לכתוב את כל קוד התקשורת של היישום בנפרד מקוד הקומפוננטות בתוך משהו שנקרא Service. כל סרביס אחראי על תקשורת עם API מסוים ומכיל את כל המנגנונים לחיבור לאותו API ואת כל הלוגיקה בחיבור הזה.
"לוגיקה בחיבור הזה" אתם שואלים? מה זאת אומרת "לוגיקה בחיבור ל API", הרי כל מה שצריך בשביל להתחבר ל API זה לשלוח בקשת HTTP, למשל עם שורה כמו:
const response = await (await fetch(url)).json();
זאת היתה שורה אחת. ואם אנחנו כבר ביישום ריאקט אז יש ספריות כמו swr ו react-query שנותנות לנו אפילו קיצור דרך לשורה הזאת:
const { data, error } = useSWR(url);
וזה נכון בדוגמאות צעצוע, אבל בעולם האמיתי יש כל מיני לוגיקות שאנחנו רוצים להוסיף לקוד התקשורת שלנו למשל:
1. קונפיגורציה של ה endpoint לפי סביבת עבודה.
2. זיהוי משתמש, וחיבור מחדש בצורה אוטומטית אם הטוקן פג תוקף.
3. מנגנון דפדוף כשיש הרבה תוצאות.
4. שינוי מבנה התשובה אם השרת לא מחזיר את המידע בדיוק בצורה שאני צריך אותו.
5. שילוב בין HTTP ל Web Sockets - כלומר שליפת המידע בפעם הראשונה דרך HTTP, ואז פתיחת Web Socket כדי לקבל עדכונים על אותו שדה מידע.
כשאני כותב את קוד התקשורת במקום אחד יש לי רק מקום אחד לכתוב בו את כל הלוגיקה סביב התקשורת. ו RTK Query מספקת בדיוק את המקום הזה.
העבודה עם RTK Query תתן לנו את אותן יכולות של swr או react-query, כלומר הספריה תאפשר לנו לתשאל את ה API מכמה קומפוננטות ובאופן אוטומטי תשמור את התשובות כדי לא לשלוח יותר מדי בקשות לשרת וכדי לשפר ביצועים, ובנוסף היא תאפשר לכתוב את כל הלוגיקה שלנו שקשורה לבקשות במקום אחד.
בגלל שספריית RTK Query תומכת גם ב GraphQL וגם ב Rest, יהיה לנו קל להחליף את שכבת התקשורת או הפרוטוקול בלי לשנות את הקומפוננטות, מה שמשאיר לנו גמישות יותר גדולה בפיתוח היישום.
בצד השלילי צריך להגיד שבגלל שכל קוד התקשורת נכתב במקום אחד, יותר קשה לי להוסיף קומפוננטה שניגשת רק ל Endpoint מסוים (בהשוואה ל swr או react-query). אני גם לא יכול לשחק עם קומפוננטות כך שקומפוננטה מסוימת ניגשת ל API בצורה אחת וקומפוננטה אחרת בצורה אחרת. הוצאת קוד התקשורת מהקומפוננטות אומרת שאני צריך להתייחס לקוד התקשורת שלי כמו עוד רכיב במערכת, עם כל המשמעויות לטוב ולרע.
## בניית ממשק לקריאת פתקים
בעבודה עם RTK Query רכיב התקשורת הבסיסי נקרא Service. הוא הולך לקבל סלייס ב Store והוא ייצור בצורה אוטומטית Custom Hooks אותם נוכל להפעיל מתוך הקומפוננטות.
בשביל המשחק בניתי API לעבודה עם פתקים על mockapi ואנחנו נכתוב תוכנית ראשונה שמציגה פתקים מתוך אותו API. ה Service המפורסם יראה כך:
// Need to use the React-specific entry point to import createApi
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
// Define a service using a base URL and expected endpoints
export const notesApi = createApi({
reducerPath: "notesApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://634fa864df22c2af7b5647a4.mockapi.io/api/v1/"
}),
endpoints: (builder) => ({
getNotes: builder.query({
query: () => `/notes`
}),
getNote: builder.query({
query: (noteId) => `/notes/${noteId}`
})
})
});
export default notesApi;
בואו נראה מה הוא כולל:
1. הפונקציה הראשית createApi מקבלת אוביקט פרמטרים ויוצרת סרביס.
2. באוביקט הפרמטרים המפתח reducerPath צריך להכיל שם ייחודי לאותו סרביס, וזה יהיה הנתיב שלו ב Reducer.
3. המפתח baseQuery מקבל אוביקט שאחראי על יצירת השאילתות. ברירת המחדל היא האוביקט fetchBaseQuery שהוא מעטפת פשוטה סביב Fetch API, אבל קל לכתוב אוביקטים כאלה חדשים (למשל עבור GraphQL), ויש גם המון מוכנים ב npm.1 419
# עולם חדש מופלא
בנסיעה לאילת, לא משנה כמה תנסה לסבך את הדרך ולגלות איזה קיצור חדש ולמרות המרחק, מעטים ירגישו תחושת הרפתקאה או מתח לאורך המסלול. אנשים הגיעו לאילת לפנינו, אנחנו יודעים שהיא שם ומה שלא יהיה בסוף אפשר להדליק וויז ולהגיע. תחושת הביטחון הזאת מאפשרת לחקור את המסלול בלי לדאוג יותר מדי.
ובגלל שכך המצב ברוב הטיולים בעולם קשה לדמיין שפעם מסע לגלות ארצות חדשות היה מסוכן, מלהיב ושונה. לא ידעת מה אתה הולך למצוא או אם בכלל יש ארץ בסוף הים. המתח היה חלק מהעיסקה וחלק מהסיבה לצאת למסע. תחושת הביטחון של מגלי ארצות לא יכלה היתה להיות מבוססת על העובדה שיש ארץ בסוף המסלול, אלא על ביטחון עצמי ביכולת שלהם להתמודד עם אתגרי המסלול כשיגיעו.
היום במקום לצאת למסעות אנחנו יכולים לבנות מוצרים חדשים, ותחושת הביטחון מקבלת יחס דומה:
אם אני בונה משהו שאנשים כבר בנו לפניי או שאני כבר בניתי בעבר, אני יכול "לשחק" עם הדרך, לבנות את זה קצת אחרת, להיכנס לפינות שקודם דילגתי עליהן, כי יש לי את הביטחון שהכל יהיה בסדר בסוף. אילת נמצאת שם בסוף המסלול.
אבל אם אני בונה משהו חדש לגמרי או לפחות שאני לא בניתי לפני, תחושת הביטחון לא יכולה להיות מבוססת על הידיעה שזה יצליח, ולא על חיזוקים חיצוניים שאני מקבל לאורך המסלול. מי שמנסה להתקבל לעבודה ראשונה בהייטק היום לא יודע אם הוא מסוג האנשים "שמתקבלים להייטק", או שהוא לא "מבוגר מדי למצוא עבודה" וצריך להתמודד עם הקושי הזה לאורך כל תהליך הלימוד וההכנה לראיונות. "אני יודע שאני הולך במסלול שמתאים לי" זה משפט שנותן ביטחון מסוג אחר. אני לא יודע מה אמצא בסוף המסלול הזה, אבל יודע שהוא הדרך הטובה ביותר עבורי, וההזדמנות והמתח שווים את המסע.
1 419
# טיפ JavaScript: עדכון פרמטרים של שורת כתובת
כתובת אינטרנט יכולה להכיל פרמטרים אחרי סימן שאלה שמגיעים בפורמט של שם הפרמטר ואז סימן שווה ואז הערך שלו - למשל בכתובת הבאה יש שני פרמטרים בשמות foo ו bar עם הערכים 10 ו 20:
http://www.tocode.co.il?foo=10&bar=20
אם אתם עדיין משתמשים בביטויים רגולאריים או פונקציות לעריכת טקסט כדי לקבל ולעדכן את הערכים של אותם פרמטרים, תשמחו לשמוע שמזמן יש פיתרון חדש שעובד ברוב הדפדפנים ונקרא URLSearchParams. בשביל להשתמש בו אני יוצר אוביקט URL מהכתובת, ואז קורא לפונקציה searchParams של אותו אוביקט. כשיש לי ביד searchParams אני יכול להפעיל את הפונקציות שלו כדי לקרוא ולעדכן את משתני החיפוש. הפונקציות המעניינות הן:
1. פונקציית get שמקבלת שם של פרמטר ומחזירה את הערך הראשון שמקושר אליו
2. פונקציית append שמוסיפה ערך חדש לפרמטר (יכולים להיות כמה מופעים של אותו פרמטר, אז שימו לב לקרוא לה כמה פעמים רק אם אתם באמת צריכים - למשל בשביל לייצג מערך).
3. פונקציית set שמעדכנת ערך של פרמטר ומשאירה רק מופע אחד שלו.
את שאר הפונקציות אפשר למצוא בתיעוד כאן:
https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
בעזרת אותן פונקציות אני יכול לשלוף ולהדפיס את הערכים של foo ו bar מהכתובת הקודמת:
const url = new URL("http://www.tocode.co.il?foo=10&bar=20");
> url.searchParams.get('foo')
'10'
> url.searchParams.get('bar')
'20'
או לעדכן את אחד הערכים, להוסיף פרמטר חדש ולקבל את הכתובת המעודכנת:
> url.searchParams.set('foo', 'new value')
> url.searchParams.set('newKey', 12)
> url.toString()
'http://www.tocode.co.il/?foo=new+value&bar=20&newKey=12'
¡Ya disponible! Investigación de Telegram 2025 — los principales insights del año 
