JSON¶
در بخش قبل، برنامهای دیدیم که از HTTP برای دریافت محتوای یک کتاب استفاده میکرد. این عالی است، اما تعداد زیادی از سرورها، داده را در یک فرمت خاص به نام JSON
برمیگردانند.
برنامه بعدی نشان میدهد که چگونه میتوانیم داده JSON را دریافت کنیم و به ما اجازه میدهد که با فشردن یک دکمه، نقل قولهای تصادفی مجموعهای از کتابها را نمایش دهیم. روی دکمه "ویرایش" کلیک و کمی در برنامه جستجو کنید. شاید برخی از این کتابها را خوانده باشید! اکنون روی دکمه ویرایش کلیک کنید!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
|
این برنامه بطور کلی مشابه نمونه قبلی است:
- تابع
init
برنامه را در حالتLoading
، با یک دستور برای دریافت یک نقل قول تصادفی، شروع میکند. - تابع
update
پیامGotQuote
را برای هر بار که یک نقل قول جدید در دسترس است، مدیریت میکند. هر چه در آنجا اتفاق بیفتد، هیچ دستور اضافی نداریم. همچنین پیامMorePlease
را زمانی که کسی دکمه را فشار میدهد، مدیریت میکند و دستوری برای دریافت نقل قولهای تصادفی بیشتر صادر میکند. - تابع
view
به شما نقل قولها را نشان میدهد!
تفاوت اصلی در تعریف تابع getRandomQuote
است. بجای Http.expectString
از Http.expectJson
استفاده شده است. این چه معنایی دارد؟
JSON¶
زمانی که از نشانی /api/random-quotes
برای یک نقل قول تصادفی درخواست میکنید، سرور یک رشته JSON مانند این تولید میکند:
{
"author": "Seneca",
"quote": "December used to be a month but it is now a year",
"source": "Letters from a Stoic",
"year": 54
}
هیچ تضمینی درباره اطلاعات موجود در اینجا وجود ندارد. سرور میتواند نام فیلدها را تغییر دهد و فیلدها ممکن است در شرایط مختلف انواع متفاوتی داشته باشند. اصلا حساب و کتاب ندارد!
در جاوااسکریپت، رویکرد متداول این است که با تبدیل JSON به ساختار داده موجود، امیدوار باشیم که هیچ اشتباهی پیش نیاید. اما اگر یک اشتباه تایپی یا داده غیر منتظرهای وجود داشته باشد، در جایی از کد خود یک خطای زمان اجرا دریافت میکنید. آیا کد اشتباه بود؟ آیا داده اشتباه بود؟ وقت آن است که شروع به جستجو کنیم تا بفهمیم!
در Elm، رویکرد متداول این است که JSON را قبل از ورود به برنامه اعتبارسنجی کنیم. بنابراین اگر داده، ساختار غیرمنتظرهای داشته باشد، بلافاصله از آن مطلع میشویم. هیچ راهی برای عبور داده بد و ایجاد یک خطای زمان اجرا در برنامه وجود ندارد. این کار با استفاده از دیکودِرهای JSON انجام میشود.
دیکودِرهای JSON¶
فرض کنید یک فایل JSON داریم:
باید آن را از طریق یک Decoder
اجرا تا به اطلاعات خاصی دسترسی پیدا کنیم. بنابراین اگر بخواهیم "age"
را دریافت کنیم، باید JSON را از طریق یک Decoder Int
که دقیقا نحوه دسترسی به آن اطلاعات را توصیف میکند، اجرا کنیم:
اگر همه چیز خوب پیش برود، یک مقدار Int
در طرف دیگر خواهیم داشت! و اگر بخواهیم "name"
را دریافت کنیم، باید JSON را از طریق یک Decoder String
که دقیقا نحوه دسترسی به آن را توصیف میکند، اجرا کنیم:
اگر همه چیز خوب پیش برود، یک مقدار String
در طرف دیگر خواهیم داشت!
اما چگونه میتوانیم دیکودِرهایی مانند این ایجاد کنیم؟
بلوکهای سازنده¶
بسته elm/json
ماژول Json.Decode
را در اختیار ما میگذارد. این ماژول پر از دیکودِرهای کوچک است که میتوانیم آنها را به هم متصل کنیم. بنابراین، برای دریافت "age"
از { "name": "Tom", "age": 42 }
دیکودِر زیر را ایجاد میکنیم:
import Json.Decode exposing (Decoder, field, int)
ageDecoder : Decoder Int
ageDecoder =
field "age" int
-- int : Decoder Int
-- field : String -> Decoder a -> Decoder a
تابع field
دو آرگومان میگیرد:
String
— نام یک فیلد. یک شی با فیلد"age"
را درخواست میکنیم.Decoder a
— یک دیکودِر برای مرحله بعدی. اگر فیلد"age"
وجود داشته باشد، این دیکودِر را روی مقدار آن امتحان خواهیم کرد.
بنابراین، تابع field "age" int
درخواست یک فیلد "age"
را میکند و اگر وجود داشته باشد، دیکودِر Int
را اجرا میکند تا سعی کند یک عدد صحیح استخراج کند.
تقریبا همین کار را برای استخراج فیلد "name"
انجام میدهیم:
import Json.Decode exposing (Decoder, field, string)
nameDecoder : Decoder String
nameDecoder =
field "name" string
-- string : Decoder String
-- field : String -> Decoder a -> Decoder a
در این مورد، یک شی با فیلد "name"
را درخواست میکنیم و اگر وجود داشته باشد، میخواهیم مقدار آن یک String
باشد.
ترکیب دیکودِرها¶
اگر بخواهیم دو فیلد را دیکود کنیم چطور؟ دیکودِرها را با استفاده از تابع map2
به هم متصل میکنیم:
این تابع دو دیکودِر میگیرد، آنها را امتحان و نتایج را ترکیب میکند. اکنون میتوانیم دو دیکودِر مختلف را با هم ترکیب کنیم:
import Json.Decode exposing (Decoder, map2, field, string, int)
type alias Person =
{ name : String
, age : Int
}
personDecoder : Decoder Person
personDecoder =
map2 Person
(field "name" string)
(field "age" int)
بنابراین، اگر از personDecoder
روی { "name": "Tom", "age": 42 }
استفاده کنیم، یک رکورد مانند Person "Tom" 42
دریافت خواهیم کرد.
اگر واقعا بخواهیم پیادهسازی کاملی داشته باشیم، باید personDecoder
را به صورت map2 Person nameDecoder ageDecoder
تعریف و از دیکودِرهای کوچکتر استفاده کنیم. پیشنهاد میکنم همیشه دیکودِرهای خود را از بلوکهای سازنده کوچکتر بسازید!
دیکودِرهای تو در تو¶
بسیاری از دادههای JSON ساده و صاف نیستند. تصور کنید که نشانی /api/random-quotes/v2
با اطلاعات بیشتری درباره نویسندگان بروزرسانی شده است:
{
"author":
{
"name": "Seneca",
"age": 68,
"origin": "Cordoba"
},
"quote": "December used to be a month but it is now a year",
"source": "Letters from a Stoic",
"year": 54
}
میتوانیم این سناریوی جدید را با دیکودِرهای تو در تو مدیریت کنیم:
import Json.Decode exposing (Decoder, map2, map4, field, int, string)
type alias Quote =
{ author : Person
, quote : String
, source : String
, year : Int
}
type alias Person =
{ name : String
, age : Int
}
quoteDecoder : Decoder Quote
quoteDecoder =
map4 Quote
(field "author" personDecoder)
(field "quote" string)
(field "source" string)
(field "year" int)
personDecoder : Decoder Person
personDecoder =
map2 Person
(field "name" string)
(field "age" int)
توجه داشته باشید که به دیکود کردن فیلد "origin"
نویسنده نمیپردازیم. دیکودِرها میتوانند فیلدها را نادیده بگیرند، این کار میتواند در استخراج مقدار کوچکی از داده بزرگ JSON بسیار مفید باشد.
مراحل بعدی¶
در ماژول Json.Decode
تعدادی تابع مهم وجود دارد که در اینجا به آنها نپرداختیم:
bool
:Decoder Bool
list
:Decoder a -> Decoder (List a)
dict
:Decoder a -> Decoder (Dict String a)
oneOf
:List (Decoder a) -> Decoder a
بنابراین، روشهای مختلفی برای استخراج ساختار دادههای متفاوت وجود دارد. تابع oneOf
برای JSON با ساختار نامنظم بسیار مفید است. (برای نمونه، ممکن است گاهی یک Int
دریافت کنید و گاهی یک String
که شامل اعداد است. این واقعا آزاردهنده است!)
توابع map2
و map4
را برای مدیریت اشیا با فیلدهای متعدد دیدیم. اما وقتی شروع به کار با اشیای بزرگتر JSON میکنید، پیشنهاد میکنم نگاهی به بسته NoRedInk/elm-json-decode-pipeline
بیندازید. این بسته، نوع داده پیچیدهتری دارد، اما برخی توسعهدهندگان کار با آن را ترجیح میدهند.
نمونه واقعی
داستانهای زیادی شنیدهام که هنگام تغییر از جاوااسکریپت به Elm، باگهایی در کد سرور پیدا شده است. دیکودِرهایی که توسعهدهندگان مینویسند به عنوان یک مرحله اعتبارسنجی عمل کرده و موارد عجیب در مقادیر JSON را شناسایی میکنند. بنابراین، وقتی پروژه NoRedInk از React به Elm تغییر کرد، چند باگ در کد Ruby آنها را آشکار ساخت!
یادداشت مترجم
کار با دیکودِرهای JSON در ابتدا ممکن است کمی دشوار باشد. به همین علت، جامعه کاربری ابزارهای مختلفی برای تبدیل ساختار داده JSON به کد Elm طراحی و پیادهسازی کرده است. عملکرد این ابزارها به گونهای است که با دریافت ورودی JSON خروجی Elm را به صورت استاندارد یا پایپلاین به شما نمایش میدهند که از آن میتوانید در کد خود استفاده کنید. برای کسب اطلاعات بیشتر، به منابع زیر مراجعه کنید: