הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

חדש ב node - תמיכה מובנית ב SQLite

25/07/2024

זה לקח המון זמן אבל גירסה 22.5 של node.js הפכה את העבודה עם SQLite להרבה יותר קלה באמצעות שילוב הספריה כמודול מובנה. זה תקציר של ה API:

import { DatabaseSync } from 'node:sqlite';
const database = new DatabaseSync(':memory:');

// Execute SQL statements from strings.
database.exec(`
  CREATE TABLE data(
    key INTEGER PRIMARY KEY,
    value TEXT
  ) STRICT
`);

// Create a prepared statement to insert data into the database.
const insert = database.prepare('INSERT INTO data (key, value) VALUES (?, ?)');
// Execute the prepared statement with bound values.
insert.run(1, 'hello');
insert.run(2, 'world');

// Create a prepared statement to read data from the database.
const query = database.prepare('SELECT * FROM data ORDER BY key');

// Execute the prepared statement and log the result set.
console.log(query.all());

הפקודה exec פשוט מריצה קוד SQL, הפקודה prepare יוצרת פקודת SQL עם משתנים קשורים להרצה ואחרי שיצרתם פקודה מוכנה להרצה תוכלו להשתמש ב run כדי להריץ אותה או ב all כדי להריץ אותה ולקבל חזרה תוצאות.

ספריית SQLite כבר קיימת כמודול מובנה גם ב bun אבל כמובן עם API שונה, ורק deno נשארו מחוץ למשחק פה והשאירו את SQLite בתור ספריה חיצונית שצריך להתקין בנפרד.

כרגע כל ה API סינכרוני והכי חשוב בשביל להפעיל את הקוד יש להוסיף מתג ניסיוני לשורת הפקודה כלומר:

node --experimental-sqlite testdb.mjs

ממש ברגעים אלה הם מוסיפים את ההערה על המתג הניסיוני לעמוד התיעוד, תוכלו לבדוק אם זה כבר עלה לאוויר ולקרוא יותר פרטים על ה API בדף התיעוד כאן: https://nodejs.org/api/sqlite.html#sqlite

הספר שלא סיימתי

24/07/2024

הכלל הבסיסי של לימוד דרך הרגלים - מותר לעזוב ספר, אסור לעזוב את ההרגל. אם התחלת לקרוא ספר על SQL כדי ללמוד את הטכנולוגיה וגילית שהספר משעמם אין שום בעיה לעבור ללמוד דרך קורס, דרך תרגול, דרך צפיה בהרצאות ביוטיוב, דרך קריאת תיעוד או בכל שיטה אחרת. מה שחשוב זה להמשיך ללמוד את הנושא חצי שעה ביום (או כמה שתכננת לקרוא את הספר).

הנה עוד כמה, עם הדגשים הרלוונטיים-

  1. (כשלומדים נושא דרך פיתוח פרויקט) מותר לעזוב פרויקט, אסור לעזוב את הקידוד. והדגש - שימו לב שבפרויקט יש שלבים. אי אפשר להתקדם אם עושים רק התחלות של פרויקטים (או רק סופים).

  2. (כשלומדים נושא דרך קורס) מותר לעזוב תרגיל, אסור לעזוב את הקורס. וגם פה נשים לב שאנחנו לא רוצים לדלג על כל התרגילים שקשורים לפרק מסוים, אבל אין שום בעיה לקחת כמה קורסים ולעשות Mix & Match לתרגילים מקורסים שונים.

  3. (כשמנסים להשתפר בעבודת צוות) מותר לעזוב את הקבוצה, בתנאי שמוצאים קבוצה אחרת במקום. והדבר החשוב כשלומדים עבודת צוות הוא להתאמן על כמה שיותר תפקידים בתוך הקבוצה.

טיפ סקאלה: שרת REST API פשוט עם cask

23/07/2024

הניסיון הראשון שלי לכתוב שרת API בסקאלה היה עם ספרייה בשם Tapir. טפיר מקסימה, יש לה המון תיעוד והרבה יכולות מתקדמות, אבל דווקא בדברים הפשוטים מצאתי פערים בתיעוד וגם בפונקציונאליות. המשך חיפושים הביא אותי לספריה בשם cask עליה אני רוצה לספר היום. אבל קודם הקוד:

package restapi
import cask._
import cask.model.{Request, Response}
import io.circe._
import io.circe.generic.auto._
import io.circe.syntax._
case class Demo(text: String)

object MyServer extends cask.MainRoutes {
  val JsonCorsHeaders: Seq[(String, String)] = Seq(
    "Content-Type" -> "application/json",
    "Access-Control-Allow-Origin" -> "*",
    "Access-Control-Allow-Methods" -> "GET, POST, PUT, DELETE, OPTIONS",
    "Access-Control-Allow-Headers" -> "Origin, X-Requested-With, Content-Type, Accept, Authorization"
  )

  @cask.options("/*")
  def options() = {
    cask.Response(
      "",
      headers = JsonCorsHeaders
    )
  }

  @cask.route("/", methods = Seq("get"))
  def hello() = {
    cask.Response(
      Demo("hello").asJson.toString,
      headers = JsonCorsHeaders
    )
  }


  initialize()
}

הקוד יצר אוביקט שאפשר להריץ אותו (לא צריך להוסיף לזה main או שום דבר), שמפעיל שרת ווב עם נתיב אחד - רק הנתיב הראשי, שמחזיר תמיד אוביקט JSON עם מפתח בשם text והערך hello. הוא מחזיר גם סט של כותרות שהגדרתי עבור CORS, ואם יהיו כמה נתיבים אפשר להשתמש באותו סט כותרות לכולם. בנוסף הקוד מגדיר טיפול גנרי למתודת OPTIONS, שוב בשביל ה CORS ששולחת רק את הכותרות כדי שדפדפנים יוכלו לקבל מידע משרת זה מכל דומיין.

בשביל להתקין את Cask יש להוסיף ל build.sbt את השורה:

libraryDependencies += "com.lihaoyi" %% "cask" % "0.9.1"

וכן אני יודע שהייתם כותבים כזה ב node.js, פייתון או רובי בעשר דקות. בסקאלה זה לקח לי שעתיים, בעיקר בגלל בעיות בתיעוד ויותר מדי ספריות של שרתי ווב. לכל שפה יש גם את החסרונות שלה.

כזה ניסיתי: Webvm.io

22/07/2024

צריכים סביבת לינוקס לתרגולים? webvm.io הוא אחד הפרויקטים המעניינים שראיתי באזור הזה. הם לקחו את Debian ומריצים אותה בדפדפן בתור אפליקציית Client Side בלבד דרך ווב אסמבלי, וגם בנו מנגנון שמאפשר לבנות כל Dockerfile למכונת לינוקס שרצה בדפדפן. מספיק להיכנס ללינק כדי להתרשם והנה עוד כמה דברים שאהבתי בפרויקט:

  1. המכונה ממשיכה לרוץ כל הזמן (טוב, כל עוד הדפדפן דולק). אפילו כשעברתי למצב שינה והפעלתי את המחשב שוב אחרי שעות הכל חזר לאותו מצב. ככה זה כשאין צד שרת.

  2. הרבה דברים עובדים אבל לאט. הפעלה ראשונה של פקודה היא איטית במיוחד ואחרי זה היא רק קצת איטית.

  3. יש תמיכה ברשת דרך שירות שנקרא Tailscale. אני מודה שבינתיים הצלחתי רק לחבר את המכונה לכתובת IP ציבורית, אבל עדיין לא להגיע אליה ב SSH או לצאת ממנה. שני הדברים אמורים להיות אפשריים.

  4. יש תמיכה בעבודה בתור root (נו זה רץ בדפדפן אז אין שום בעיית אבטחה). פשוט מפעילים su וכותבים את הסיסמה password.

  5. יש על המכונה כבר כמה תוכנות כמו python, perl, gcc, vim, ruby. בגלל שלא הצלחתי להפעיל את החיבור לרשת גם לא הצלחתי להתקין תוכנות אחרות.

  6. שינויים ב crontab שניסיתי לעשות לא נכתבו לכן אני מניח שחלק מהקבצים הם לקריאה בלבד. לא היתה בעיה ליצור קבצים בתיקיית הבית, להפעיל תוכנות או כל משימה אחרת של משתמש רגיל.

סך הכל אם אתם מנסים ללמוד לינוקס ובמיוחד עבודה משורת הפקודה, ומחפשים סביבת תרגול שפשוט תעבוד בלחיצת כפתור אתר webvm הוא הפיתרון הטוב והמהיר ביותר שתוכלו למצוא. נסו ושתפו חוויות.

תרגילי תכנות בראיונות עבודה

21/07/2024

מייקל שקור מציג טיעון מעניין נגד תרגילי תכנות בראיונות עבודה. הטיעון בגדול הוא:

  1. תרגילי תכנות בראיונות עבודה מפלים לטובה אנשים שיש להם זמן (ולרעה אנשים עם ילדים)

  2. תרגילי תכנות לא משקפים את העבודה שלנו בעולם האמיתי

והמסקנה היא שצריך למצוא דרכים אחרות לבחון מועמדים, לדוגמה שיחה על עבודות קודמות או סשן של Pair Programming עם המראיין סביב בעיה אמיתית. נשים בצד את שתי ההצעות (הן טובות, אבל לא הנקודה שלנו היום), ונחזור להתמקד בטיעון - האם תרגילי תכנות מפלים לרעה אנשים עם משפחות? והאם הם באמת לא משקפים את העבודה שלנו בעולם האמיתי?

במובן מסוים יש היגיון בדברים - לאנשים עם משפחות יש פחות זמן, ולכן הם יכולים להתכונן פחות ולכן יגיעו פחות מוכנים לראיון עבודה ולכן סיכויי הקבלה שלהם יותר נמוכים. אבל לא הייתי קורא לזה אפליה לא הוגנת.

אם נסתכל על "חיפוש עבודה" כמו פרויקט, ברור שחלק מהזמן צריך להיות מושקע בללכת לראיונות, אבל גם חלק מהזמן של חיפוש עבודה צריך להיות מושקע בהתכוננות לקראת אותם ראיונות, וחלק נוסף מהזמן יושקע בבניית קורות חיים ועוד חלק ב Networking וכן הלאה. יותר מזה, שאלות ראיונות עבודה נוטות להתמקד סביב מבני נתונים, אלגוריתמים ולפעמים גם ידע כללי שלא משתמשים בו ביום יום אבל יכול להעשיר ולהוסיף לאיכות הקוד. אלה בדיוק הדברים שמתכנתים טובים לומדים באופן שוטף כדי לא להישאר במקום.

להתכונן לפני זה הדבר המקצועי והנכון לעשות בכל סיטואציה מקצועית: אנחנו מתכוננים לפני שמעבירים הרצאה בכנס, מתכוננים לפני פגישות מקצועיות, מתכוננים לפני האקתון, מתכוננים לפני שמגיע יועץ חיצוני - וכן גם מתכוננים לפני שהולכים לראיון עבודה. זה הופך את הראיון להרבה יותר מעניין לשני הצדדים. ייתרון לאנשים שהתכוננו זה לא אפליה אלא הדבר הנכון לעשות.

מסע דילוגים

20/07/2024

כשצריך ללמוד נושא חדש, איך יודעים באיזה סדר להתקדם, ואפילו מה בכלל צריך ללמוד?

יהיו שיגידו שיש סדר לימוד נכון. שאנשים שכבר לימדו את הנושא הזה הרבה פעמים יודעים מה עובד ומה לא עובד וכדאי להקשיב להם. במיוחד מאחר ולימוד בסדר לא נכון יכול לייצר פערי ידע וטעויות שאחרי זה יהיה קשה לזהות ולתקן, ובשלב שאנחנו רק לומדים אנחנו בטח לא יודעים מספיק בשביל לקבוע בעצמנו. בתי ספר ואוניברסיטאות אוהבים את הגישה הזאת, כי היא נותנת להם סדר ואפשרות ללמד קבוצה שלמה ביחד. גם אני כשאני מלמד קורס אצל לקוח יודע שיש מחיר לכל סטייה מהסילבוס.

אבל אם שמים בצד את הצורך ללמוד בקבוצה הרבה פעמים אנחנו מגלים שלימוד נוקשה לפי הסילבוס הוא לא הדרך הטובה ביותר ללמוד עבור אוטודידקטים. החסרונות של הלימוד לפי הסדר כוללים:

  1. הליכה לפי הסדר מכריחה אותי לעבור גם על נושאים שאני מכיר בחלקם.

  2. הליכה לפי הסדר אומרת שאני אחכה עם הדבר שמעניין אותי, המתנה שעלולה לפגוע במוטיבציה.

  3. הליכה לפי הסדר אומרת שלפעמים אני אתקע על דברים שאולי חשובים ברמה העקרונית אבל כרגע עבורי הם פחות חשובים.

מה עושים? מדלגים. דילוג לא אומר שאנחנו פחות טובים, פשוט שאנחנו לומדים אחרת. דילוג לחלקים הכי מעניינים של הקורס כבר בהתחלה יכול לתת מוטיבציה. דילוג מהיר על חלקים שאני מכיר יעזור לי להישאר מרוכז ועם מוטיבציה גבוהה, ודילוג על חלקים קשים בקורס יכול לתת למוח שלי את הזמן שהוא צריך כדי לעכל את מה שלמדתי. ממילא תמיד אפשר לדלג חזרה.

למה וויתרתי על סוליד ב 2024

19/07/2024

לפני כשנתיים עשיתי פרויקט קצר ב SolidJS. מאוד התלהבתי ממנו וגם כתבתי עליו קצת פה ואפילו הקלטתי וידאו קצר. חזרתי אליו לא מזמן כדי לראות מה השתנה ואני חייב להודות שהרבה מההתלהבות ירדה. אלה הדברים המרכזיים שהפריעו לי ב SolidJS מודל 2024:

  1. הגודל - לקחתי פרויקט ריאקט קטן (פחות מ-10 קבצים, חצי מגה של JavaScript כולל הכל) ותרגמתי לסוליד. התוצאה לקחה 400K. נכון זה 20% פחות וזה מרשים והכל, אבל 20% מחצי מגה זה עדיין מעט. יכול להיות שאם היה לי פרויקט ריאקט של 10 מגה הייתי רואה הבדל משמעותי, אבל גם יכול להיות שבפרויקט של 10 מגה החיסכון היה קטן בהרבה מ 20%. בכל מקרה עבור פרויקט קטן החיסכון בגודל לא מורגש.

  2. סיבוכיות סביב props - לפני שנתיים עוד נראה לי הגיוני לכתוב props בכותרת של כל פונקציית קומפוננטה ולעשות את ה Destructuring בתוך גוף הפונקציה עם פונקציה שלהם. היום זה נראה הרבה יותר מסורבל. זאת הדוגמה שלהם מהתיעוד להגדרת ערך ברירת מחדל לפרופ:

function MyComponent(props) {
  // Using mergeProps to set default values for props
  const finalProps = mergeProps({ defaultName: "Ryan Carniato" }, props);

  return <div>Hello {finalProps.defaultName}</div>;
}

וזו אותה קומפוננטה בריאקט:

function MyComponent({defaultName = "Ryan Carniato"}) {
    return <div>Hello {defaultName}</div>;
}
  1. שימוש ב class במקום className. כן אני יודע כולם כועסים על ריאקט וה className שלהם, אבל תכל'ס אני שמח לכתוב קומפוננטה שמקבלת className בתור פרופ:
function ReactComponent({className}) {
    return (<div className={className}>yay</div>)
}

הכתיב הזה לא עובד אם שם הפרמטר היה class, כי class זו מילה שמורה ב JavaScript.

  1. קבלת ערך מסיגנל דורשת קריאה לפונקציה. עוד משהו שלפני שנתיים לא הפריע לי אבל היום נראה מוזר זה השימוש בסיגנלים דרך קריאה לפונקציה:
const [count, setCount] = createSignal(0);
console.log(count()); 

גם בהשוואה ל vue וגם בהשוואה ל preact הכתיב הזה מסורבל. זו דוגמה מתוך התיעוד של preact לסיגנלים שלהם שעובדים בצורה הרבה יותר טבעית:

import { signal } from "@preact/signals";

const count = signal(0);

// Read a signal’s value by accessing .value:
console.log(count.value);   // 0
  1. אקוסיסטם ופופולריות - לפני שנתיים האקוסיסטם של Solid היתה קטנה אבל היתה איזה תחושה שאולי בעתיד זה יתפוס והיא תגדל. היום כבר ברור שסוליד לא יהיה הדבר הגדול הבא והאקוסיסטם לא יגדל.

כן יש אלטרנטיבות לריאקט אבל אני חושש שלפחות עבורי Solid כבר לא אחת מהם. אם צריך לגוון סבלט כנראה תעבוד טוב וגם Vue.

יותר מדי מהונדס

18/07/2024

אחד המושגים המבלבלים בתוכנה הוא Over Engineering. ההגדרה בויקיפדיה של המושג היא פיתרון בעיה בצורה יותר מדי מסובכת כאשר המורכבות של הפיתרון לא מוסיפה שום ערך, וכך אם השקענו אקסטרה עבודה אבל לא קיבלנו ערך עבורה זה אומר שסך הכל פתרנו את הבעיה בצורה פחות טובה.

ספציפית בתוכנה כשאנחנו אומרים על מוצר שהוא מהונדס יתר על המידה אנחנו מתכוונים:

  1. קשה לעדכן את הקוד, כיוון שכל שינוי שובר הרבה מנגנונים אחרים.

  2. הקוד נשבר לבד עם הזמן (כי ספריות מתיישנות) ולכן דורש הרבה עבודת תחזוקה רק כדי להמשיך לעבוד.

  3. למפתחים חדשים לוקח הרבה זמן עד שמתחילים להיות פרודוקטיביים כי יש הרבה מנגנונים ללמוד.

מה שמיוחד בהנדסת יתר זה שמישהו באמת ישב וחשב על כל הדברים שיכולים לקרות ולהשתנות ועבד מאוד קשה כדי לבנות את הקוד כמו שהוא כתוב, אבל מרוב קוד ותכנון קיבלנו מוצר שמתנהג גרוע בדיוק כמו מוצרים שלא בוצעה עליהם כל עבודת החשיבה הזאת, או הרבה פעמים אפילו יותר גרוע.

בעבודה על קוד חדש האתגר הראשון שלנו הוא לזהות שעברנו את הקו, לזהות שכתבנו משהו יותר מדי מסובך ושבטח אפשר לכתוב את זה בצורה פשוטה יותר. אתגר שני ולפעמים קשור הוא למצוא את שיטת הקידוד הפשוטה יותר שפותרת את הבעיה. הרבה פעמים יש לנו את הראשון אבל לא את השני, ואז שווה לארגן את הקוד כדי "לתחום" את הסיבוכיות עד שנבין איך לפרק אותה.

לדוגמה, נניח שאני צריך באפליקציית ווב ליצור מזהים ייחודיים לכל מיני דברים במערכת. אני יודע שהספריה uuid יכולה ליצור מזהה ייחודי למשהו ואני מחליט להשתמש בה כדי ליצור את המזהים. אני מבין גם שלהכניס ספריה חיצונית מוסיף סיבוכיות למערכת, כי עכשיו אצטרך להתקין אותה כל פעם שאני מתקין את התוכנה שלי ולשים לב לעדכוני גירסאות - אבל מצד שני אני לא מכיר (עדיין) דרך יותר טובה לחשב מזהים ייחודיים. במצב כזה אוכל לכתוב פונקציה שלי שעוטפת את הספריה החיצונית וקוראת לה, ובכל המערכת לקרוא רק לפונקציה שלי במקום לספריה החיצונית. יום אחד כשארגיש מספיק בטוח לגבי עבודה עם דפדפנים חדשים אוכל להחליף את הספריה החיצונית במימוש המובנה בדפדפן וכך יחסית בקלות להוריד סיבוכיות.

השאלה "מה אם ארצה לשנות את דעתי" היא תמיד דרך טובה להסתכל על הנדסת תוכנה, ובמיוחד כשאנחנו מסתכלים על מנגנונים שמוסיפים סיבוכיות למערכת.

היום למדתי: שינוי שם שם מפתח במפה בקלוז'ר

17/07/2024

הנה פעולה שלא חשבתי שאי פעם אצטרך אבל מסתבר שיש לא מעט איזכורים שלה ברשת בכל מיני שפות, ובינתיים מהשפות שאני מכיר מצאתי שרק קלוז'ר מציעה דרך סטנדרטית להחליף שם של מפתח. אבל לפני הקוד בואו נבין על מה אנחנו מדברים - בשפות תכנות מודרניות יש מבנה נתונים שנקרא "מפה" או "מילון", ובדרך כלל יש המון דברים שאפשר לעשות עם ערכים של מילון אבל מעט דברים לעשות עם המפתחות. הרבה שפות אפילו יצרו מנגנוני הגנה כדי שלא נוכל להשתמש בכל טיפוס נתונים בתור מפתח. לדוגמה בפייתון רשימה לא יכולה להיות מפתח:

>>> d = {}
>>> d[[1, 2, 3]] = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

או ב JavaScript כל מפתח שנבחר יהפוך ל String אוטומטית:

> o = {}
{}
> o[2] = 10
10
> o['2']
10

אם כבר יצרתי מילון עם מפתחות ואני צריך לשנות שם של מפתח ברוב השפות אני אצטרך להוציא את המפתח הישן מהמילון ולהכניס ערך חדש עם מפתח בשם החדש. רובי עושה את זה הכי אלגנטי אבל באמת שאותו טריק עובד בכל מקום:

3.1.1 :001 > h = { name: "ynon" }
 => {:name=>"ynon"}
3.1.1 :002 > h[:nickname] = h.delete :name
 => "ynon"
3.1.1 :003 > h
 => {:nickname=>"ynon"}

היה לי מילון עם מפתח בשם name ובשביל להחליף אותו ל nickname היה צריך להוציא את המפתח הישן מהמילון ולשים חדש עם אותו ערך.

וקלוז'ר? דווקא היא הפתיעה לטובה עם פונקציית rename-keys שמקבלת מפה ומפה של שינויים ומשנה את השמות של המפתחות לפי מפת השינויים. כמובן שבקלוז'ר אנחנו מדברים על מבני נתונים Immutable ולכן אנחנו לא באמת משנים מילון אלא מייצרים מילון חדש עם המפתחות בשמות החדשים:

user=> (clojure.set/rename-keys {:name "ynon"} {:name :nickname})
{:nickname "ynon"}

מכירים פונקציה כזאת בעוד שפות? אל תתביישו וספרו בתגובות.

טיפ CSS - אפקט שורות מתחת לטקסט

16/07/2024

רוב הזמן כשאני צריך לשים קו ב CSS מתחת לשורת טקסט אני משתמש ב border או ב box shadow והכל עובד ופשוט, כל עוד מדובר בשורה אחת, לדוגמה (עם טיילווינד):

<div class="border-b border-gray-400">
  Hello World
</div>

אפשר אפילו להוסיף כמה שורות אם אנחנו יודעים איזה טקסט כתוב בדיוק בכל שורה:

<div class="border-b border-gray-400 mb-2">
  Line 1
</div>
<div class="border-b border-gray-400 mb-2">
  Line 2
</div>
<div class="border-b border-gray-400 mb-2">
  Line 3
</div>

וכך מקבלים אפקט כמו של מחברת שמראה טקסט עם קו של שורה מתחת לכל שורת טקסט.

הסיפור הזה מסתבך אם אני לא יודע בדיוק איפה לחתוך את המילים לשורה חדשה. כן אפשר להשתמש ב JavaScript לחשב, אבל טריק יותר קל הוא להשתמש בגרדייאנט לינארי בתור תמונת רקע ואיתו לייצר את האפקט של השורות. זה הקוד:

<div class="p-4 w-full max-w-md mx-auto">
  <div class="relative">
    <div class="absolute inset-0 bg-[size:100%_2rem] bg-[linear-gradient(#999_1px,transparent_1px)] bg-[position:0_-1px]"></div>
    <p class="relative leading-8 pt-[1px]">
      This text now starts from the first line. It appears as if it's written on the notebook lines, aligning perfectly with each line to create a realistic effect.
    </p>
  </div>
</div>

במקום לצייר קו מתחת לכל שורה אני מגדיר אלמנט עם גרדיינט לינארי ברקע. הגרדיינט הוא שאחראי על ציור השורות וה line-height (שנקבע על ידי המאפיין leading של טיילווינד) גורם לטקסט להתאים בגובה לגובה של השורות בתמונת הרקע. כשיש יותר טקסט אין שום בעיה ובאופן אוטומטי הטקסט יחולק בין השורות.

אם אנחנו צריכים להציג את השורות גם כשאין טקסט פשוט נגדיר גובה לפיסקה והוא יקבע כמה שורות יצוירו, לדוגמה:

<div class="p-4 w-full mx-auto">
  <div class="relative">
    <div class="absolute inset-0 bg-[size:100%_2rem] bg-[linear-gradient(#999_1px,transparent_1px)] bg-[position:0_-1px]"></div>
    <p class="relative leading-8 pt-[1px] w-full h-32 ">
    </p>
  </div>
</div>

נ.ב. בשביל לשחק עם הדוגמאות הכי קל לבקר ב https://play.tailwindcss.com/ ולהדביק שם את קוד הטיילווינד כדי לראות את התוצאה בצד ימין של המסך.