اذهب إلى المحتوى

كيفية استخدام القوالب Templates في لغة جو Go


هدى جبور

إذا كنا بحاجة إلى عرض البيانات بتنسيقات منظمة مثل التقارير النصية أو صفحات HTML، فإن قوالب لغة جو توفر حلًا فعالًا. تتضمن مكتبة لغة جو القياسية حزمتين تسمحان لأي برنامج مكتوب في هذه اللغة بتقديم البيانات بطريقة منسقة بدقة، وهما text/template و html/template.

يمكننا باستخدام هذه الحزم إنشاء قوالب نصية وتمرير البيانات فيها لتصيير render مستندات مصممة خصيصًا لمتطلباتنا. توفر القوالب المرونة في التكرار على البيانات باستخدام الحلقات وتطبيق المنطق الشرطي لتحديد محتوى ومظهر كل عنصر. نستكشف في هذا المقال كيفية استخدام كلتا حزم القوالب. نستخدم في البداية حزمة text/template لإنشاء تقرير نصي عادي من خلال الاستفادة من الحلقات والعبارات الشرطية والدوال المخصصة. نستخدم بعد ذلك html/template لتصيير مستند HTML مع ضمان الحماية ضد الثغرات الأمنية في إدخال التعليمات البرمجية.

المتطلبات الأولية

1لمتابعة هذا المقال التعليمي، سنحتاج إلى:

الخطوة 1- استيراد حزمة text/template

لنفترض أننا نريد إنشاء تقرير بسيط عن بيانات الكلاب التي لدينا. تنسيق التقرير المطلوب هو كما يلي:

---
Name:  Jujube

Sex:   Female (spayed)

Age:   10 months

Breed: German Shepherd/Pitbull

---
Name:  Zephyr

Sex:   Male (intact)

Age:   13 years, 3 months

Breed: German Shepherd/Border Collie

نحتاج لإنشاء هذا التقرير باستخدام حزمة text/template إلى استيراد الحزمة اللازمة وإعداد المشروع. يتألف التقرير من نص ثابت في القالب (العناصر على اليسار) وبيانات ديناميكية نمررها إلى القالب لتقديمها (على اليمين). يمكن تخزين القوالب مثل متغيرات من النوع string ضمن الشيفرة أو ملفات منفصلة خارج الشيفرة. تحتوي القوالب على نص ثابت معياري متداخل مع عبارات شرطية else و if وعبارات التحكم في التدفق (الحلقات) واستدعاءات الدوال، وكلها محاطة داخل أقواس معقوصة {{. . .}}. أخيرًا يمكننا إنشاء المستند النهائي من خلال توفير البيانات للقالب كما في المثال أعلاه.

ننتقل الآن إلى مساحة العمل الخاصة بنا "go env GOPATH" وننشئ مجلدًا جديدًا لهذا المشروع، ثم ننتقل إليه. يمكن إجراء ذلك من خلال التعليمات التالية بالترتيب:

$ cd `go env GOPATH`
$ mkdir pets
$ cd pets

باستخدام محرر نانو nano أو أي محرر آخر تريده، نفتح ملفًا جديدًا يسمى "pets.go":

$ nano pets.go

ونضع فيه التعليمات التالية:

package main

import (
    "os"
    "text/template"
)

func main() {
}

تنتمي الشيفرة السابقة إلى الحزمة main، وتتضمّن الدالة main التي تسمح بتنفيذ الشيفرة باستخدام الأمر go run. تستورد الشيفرة حزمتين، هما: text/template من مكتبة جو القياسية، والتي نستخدمها لكتابة القالب وعرضه، وحزمة os للتفاعل مع نظام التشغيل من خلال الدوال التي توفرها.

بذلك تكون الأمور جاهزة لبدء كتابة المنطق اللازم لإنشاء التقرير المطلوب باستخدام حزمة text/template.

الخطوة 2- إنشاء بيانات القالب

بدايةً يجب أن يكون لدينا بعض البيانات لتمريرها إلى القالب، لذا سنعرّف بنيةً تسمى Pet تمثل خصائص حيوان أليف. تحتفظ هذه البنية ببيانات كل كلب في التقرير.

. . .
type Pet struct {
    Name   string
    Sex    string
    Intact bool
    Age    string
    Breed  string
}

نُنشئ أيضًا شريحةً من Pet لتخزين معلومات كلبين:

func main() {
    dogs := []Pet{
        {
            Name:   "Jujube",
            Sex:    "Female",
            Intact: false,
            Age:    "10 months",
            Breed:  "German Shepherd/Pitbull",
        },
        {
            Name:   "Zephyr",
            Sex:    "Male",
            Intact: true,
            Age:    "13 years, 3 months",
            Breed:  "German Shepherd/Border Collie",
        },
    }
} // end main

عرّفنا البنية Pet بحقول تمثل الخصائص المختلفة للحيوان الأليف. نستخدم هذه البنى للاحتفاظ ببيانات كل كلب في التقرير. تتضمن الحقول: اسم الحيوان الأليف Name وجنس الحيوان الأليف Sex وقيمة منطقية تشير إلى ما إذا كان الحيوان الأليف سليم Intact وعمر الحيوان الأليف Age والسلالة Breed.

أنشأنا داخل الدالة main شريحة Pet باسم dogs وملأناها بنموذجين من كلاب مختلفة. الكلب الأول يُسمّى Jujube والكلب الثاني Zephyr. من المهم ملاحظة أنه في سيناريو العالم الحقيقي يمكن جلب بيانات القالب من قاعدة بيانات أو الحصول عليها من واجهة برمجة تطبيقات خارجية أو توفيرها من خلال إدخال المستخدم، لكن هنا أدخلنا البيانات يدويًا.

يمكننا الآن المتابعة إلى الخطوة التالية لكتابة القالب وعرضه.

الخطوة 3- تنفيذ وعرض بيانات القالب

حان الوقت الآن لاستكشاف كيفية استخدام حزمة text/template لتوليد مستند من قالب، ولكي نتأكد من أن الأمور تعمل بنجاح، سننشئ ملف قالب فارغ ثم نمرّر البيانات إلى القالب لتنفيذه. على الرغم من أن النموذج الأولي لن يعرض سوى النص "Nothing here yet"، إلا أنه سيكون بمثابة نقطة بداية لتوضيح دوال الحزمة text/template.

ننشئ ملف باسم pets.tmpl بالمحتوى التالي:

Nothing here yet.

نحفظ القالب ونخرج من المحرر. في حالة المحرر نانو nano، نضغط على المفتاحين "CTRL + X" ثم المفتاح "Y" و "ENTER" لتأكيد التغييرات. نضيف الآن مقتطف الشفرة التالي داخل main:

. . .
    var tmplFile = pets.tmpl
    tmpl, err := template.New(tmplFile).ParseFiles(tmplFile)
    if err != nil {
        panic(err)
    }
    err = tmpl.Execute(os.Stdout, dogs)
    if err != nil {
        panic(err)
    }
} // main نهاية الدالة 

صرّحنا ضمن الدالة main عن المتغير tmplFile وأسندنا له القيمة “pets.tmpl”، والتي تمثل اسم ملف القالب. استخدمنا بعد ذلك الدالة template.New لإنشاء قالب من template، مع تمرير tmplFile اسمًا للقالب. استدعينا بعد ذلك ParseFiles في القالب الذي أنشأناه حديثًا، مع تمرير tmplFile مثل ملف لتحليله. تربط هذه الخطوة ملف القالب بالقالب.

تحققنا بعد ذلك من أية أخطاء حدثت أثناء تحليل القالب. نلتقط الخطأ في حالة حدوثه، وتنتج لدينا حالة هلع panic في البرنامج.

الآن لتنفيذ القالب نستدعي التابع Execute، ونمرر له وسيط أول os.Stdout ليكون وجهة الخرج ووسيط ثان dogs ليمثّل البيانات الممررة إلى القالب. يمثل os.Stdout (أو أي شيء آخر يحقق الواجهة io.Writer، أي ملف مثلًا) الخرج القياسي الذي سيطبع في هذه الحالة التقرير المُنشأ على الطرفية.

سيؤدي تنفيذ القالب في هذه المرحلة إلى عرض النص المذكور أنفًا، وذلك لأننا لا نستخدم بيانات ديناميكية في القالب.

ستكون الشيفرة كاملة كما يلي:

package main

import (
    "os"
    "text/template"
)

type Pet struct {
    Name   string
    Sex    string
    Intact bool
    Age    string
    Breed  string
}

func main() {
    dogs := []Pet{
        {
            Name:   "Jujube",
            Sex:    "Female",
            Intact: false,
            Age:    "10 months",
            Breed:  "German Shepherd/Pitbull",
        },
        {
            Name:   "Zephyr",
            Sex:    "Male",
            Intact: true,
            Age:    "13 years, 3 months",
            Breed:  "German Shepherd/Border Collie",
        },
    }
    var tmplFile = pets.tmpl
    tmpl, err := template.New(tmplFile).ParseFiles(tmplFile)
    if err != nil {
        panic(err)
    }
    err = tmpl.Execute(os.Stdout, dogs)
    if err != nil {
        panic(err)
    }
} // main نهاية الدالة 

لنُشغّل ملف البرنامج "pets.go" من خلال الأمر go run:

$ go run pets.go

سيكون الخرج على النحو التالي:

Nothing here yet.

لا يطبع البرنامج البيانات حتى الآن، ولكن على الأقل يعمل بطريقة صحيحة. لنكتب الآن قالبًا.

الخطوة 3- كتابة قالب

القالب هو أكثر من مجرد نص عادي بترميز UTF-8، إذ يحتوي القالب على نص ثابت إضافةً إلى الإجراءات التي توجّه محرك القالب حول كيفية معالجة البيانات وإنشاء المخرجات. تُغلّف الإجراءات بأقواس معقوصة {{ <action> }}، وتعمل على البيانات باستخدام تدوين النقطة (.).

من الشائع استخدام بُنى البيانات القابلة للتكرار عند تمرير البيانات إلى قالب، مثل الشرائح أو المصفوفات أو الروابط maps. سنستكشف في هذه الخطوة كيفية التكرار على شريحة في القالب باستخدام range.

التكرار على شريحة

يمكننا استخدام الكلمة المفتاحية range في لغة جو داخل حلقة for للتكرار على شريحة، وكذلك هو الحال في القوالب؛ إذ يمكننا استخدام الإجراء range لتحقيق نفس النتيجة، ولكن بصيغة مختلفة قليلًا؛ فبدلًا من استخدام كلمة مفتاحية for، يمكن ببساطة استخدام range متبوعًا بالبيانات القابلة للتكرار، وتغلق الحلقة بالتعليمة {{ end }}.

لنعدّل ملف "pets.tmpl" عن طريق استبدال محتوياته بما يلي:

{{ range . }}
---
(Pet will appear here...)
{{ end }}

يتخذ الإجراء range النقطة (.) وسيطًا له، والذي يمثل كامل شريحة dogs، ثم نُغلق الحلقة باستخدام {{ end }}. نضع ضمن الحلقة نصًا ثابتًا سيُعرض لكل حيوان أليف. في هذه المرحلة عمومًا، لن تُعرض أية معلومات عن الكلاب في الخرج.

احفظ الملف "pets.tmpl" وشغّل ملف البرنامج "pets.go" من خلال الأمر go run:

$ go run pets.go

سيكون الخرج على النحو التالي:

---
(Pet will appear here...)

---
(Pet will appear here...)

يُطبع النص الثابت مرتين نظرًا لوجود كلبين في الشريحة. دعونا الآن نستبدل هذا ببعض النصوص الثابتة المفيدة، إلى جانب بيانات الكلاب.

عرض حقل

عند استخدام range مع النقطة . في القالب السابق، إذ تشير النقطة إلى العنصر الحالي في الشريحة أثناء كل تكرار للحلقة وعندما يكون هناك عنصر واحد في الشريحة فهو يشير إلى كامل الشريحة. يتيح ذلك الوصول إلى الحقول المُصدّرة لكل حيوان أليف مباشرةً باستخدام تدوين النقطة دون الحاجة إلى الإشارة إلى فهارس الشريحة. بالتالي لكي نعرض حقل، يمكن ببساطة تغليفه بأقواس معقوصة وإسباقه بنقطة. لنحّدث ملف "pets.tmpl" بالشيفرة التالية:

{{ range . }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }}

Age:   {{ .Age }}

Breed: {{ .Breed }}
{{ end }}

بذلك سيتضمن الخرج أربعة حقول لكل كلب: الاسم والجنس والعمر والسلالة. تمثل النقطة . الحيوان الأليف الحالي الذي يجري تكراره في الحلقة. إذًا يمكننا الوصول إلى الحقل المقابل لكل حيوان أليف وعرض قيمته جنبًا إلى جنب مع التسميات المناسبة باستخدام تدوين النقطة.

لنُشغّل ملف البرنامج "pets.go" من خلال الأمر go run:

$ go run pets.go

سيكون الخرج على النحو التالي:

---
Name:  Jujube

Sex:   Female

Age:   10 months

Breed: German Shepherd/Pitbull

---
Name:  Zephyr

Sex:   Male

Age:   13 years, 3 months

Breed: German Shepherd/Border Collie

يبدو الأمر جيدًا. الآن دعونا نرى كيفية استخدام المنطق الشرطي لعرض الحقل الخامس.

استخدام الشروط

تجاهلنا الحقل Intact في القالب السابق؛ لإبقاء التقرير أكثر سهولة للقراءة، فبدلًا من عرض القيمة المنطقية مباشرةً true أو false، يمكننا استخدام إجراء if-else لتخصيص الخرج بناءً على قيمة الحقل، وتقديم معلومات أوضح وأكثر سهولة للفهم من مجرد وضع true أو false.

نفتح ملف "pets.tmpl" مجددًا ونعدّل القالب على النحو التالي:

{{ range . }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }} ({{ if .Intact }}intact{{ else }}fixed{{ end }})

Age:   {{ .Age }}

Breed: {{ .Breed }}
{{ end }}

يشتمل القالب الآن على عبارة if-else للتحقق من قيمة الحقل Intact. إذا كان الحقل true، فإنه يطبع (intact)، وإلا فإنه يطبع (fixed). يمكننا أيضًا تحسين الخرج أكثر؛ من خلال عرض المصطلحات الخاصة بالجنس لكلب حالته fixed، مثل spayed أو neutered، بدلًا من استخدام المصطلح العام fixed. لتحقيق ذلك يمكننا إضافة عبارة if متداخلة داخل كتلةelse:

{{ range . }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }} ({{ if .Intact }}intact{{ else }}{{ if (eq .Sex "Female") }}spayed{{ else }}neutered{{ end }}{{ end }})

Age:   {{ .Age }}

Breed: {{ .Breed }}
{{ end }}

مع هذا التعديل؛ يتحقق القالب أولًا مما إذا كان الحيوان الأليف سليمًا. إذا لم يكن الأمر كذلك، فإنه يتحقق أيضًا مما إذا كان الحيوان الأليف أنثى Female. يسمح هذا بمعلومات أكثر دقة في التقرير.

احفظ ملف القالب وشغّل ملف البرنامج "pets.go" من خلال الأمر go run:

$ go run pets.go

سيكون الخرج:

---
Name:  Jujube

Sex:   Female (spayed)

Age:   10 months

Breed: German Shepherd/Pitbull

---
Name:  Zephyr

Sex:   Male (intact)

Age:   13 years, 3 months

Breed: German Shepherd/Border Collie

لدينا كلبان وثلاث حالات محتملة لعرض Intact. دعونا نضيف كلبًا آخر إلى الشريحة في pets.go لتغطية الحالات الثلاث:

. . .
func main() {
    dogs := []Pet{
        {
            Name:   "Jujube",
            Sex:    "Female",
            Intact: false,
            Age:    "10 months",
            Breed:  "German Shepherd/Pitbull",
        },
        {
            Name:   "Zephyr",
            Sex:    "Male",
            Intact: true,
            Age:    "13 years, 3 months",
            Breed:  "German Shepherd/Border Collie",
        },
        {
            Name:   "Bruce Wayne",
            Sex:    "Male",
            Intact: false,
            Age:    "3 years, 8 months",
            Breed:  "Chihuahua",
        },
    }
. . .

لنُشغّل ملف البرنامج "pets.go" من خلال الأمر go run:

$ go run pets.go

ليكون الخرج:

---
Name:  Jujube

Sex:   Female (spayed)

Age:   10 months

Breed: German Shepherd/Pitbull

---
Name:  Zephyr

Sex:   Male (intact)

Age:   13 years, 3 months

Breed: German Shepherd/Border Collie

---
Name:  Bruce Wayne

Sex:   Male (neutered)

Age:   3 years, 8 months

Breed: Chihuahua

رائع، يبدو كما هو متوقع.

الآن دعونا نناقش دوال القالب، مثل الدالة eq التي استخدمناها للتو.

استخدام دوال القالب

توفر الحزمة text/template -إضافةً إلى الدالة eq التي استخدمناها سابقًا- العديد من الدوال الأخرى لمقارنة قيم الحقول وإرجاع النتائج المنطقية، مثل gt (أكبر من) و ne (عدم تساوي) و le (أقل من أو يساوي) والمزيد. يمكن استدعاء هذه الدوال بطريقتين مختلفتين:

  1. كتابة اسم الدالة متبوعة بمعامل واحد أو أكثر ومفصولة بمسافات. هذه هي الطريقة التي استخدمنا بها الدالة eq في هذا المقال:"eq .Sex "Female.
  2. كتابة معامل واحد متبوع برمز الأنبوب |، ثم اسم الدالة والمعلمات الإضافية إذا لزم الأمر. يسمح هذا بربط استدعاءات عدة دوال معًا، مع جعل خرج كل دالة مدخلًا للتالية. هذا مشابه لكيفية عمل أنابيب الأوامر في سطر أوامر Unix.

مثلًا يمكن كتابة عملية المقارنة السابقة باستخدام الدالة eq في القالب بالشكل: "Sex | eq "Female.، وهذا يُكافئ التعبير "eq .Sex "Female.

دعونا الآن نستخدم الدالة len لعرض عدد الكلاب في الجزء العلوي من التقرير. نفتح ملف "pets.tmpl" ونضيف الشيفرة التالية في البداية:

Number of dogs: {{ . | len -}}

{{ range . }}
. . .

يمكنك أيضًا كتابتها بالشكل {{ - . len }}. تحسب هذه الدالة طول البيانات المُمررة في .، وهي في هذه الحالة شريحة الكلاب. بالتالي سنتمكن من عرض عدد الكلاب في أعلى التقرير من خلال تضمين هذه الدالة في القالب.

لاحظ الشَرطة - بجانب الأقواس المزدوجة المعقوصة، وتمنع هذه الشرطة طباعة الأسطر الجديدة n\ بعد الإجراء. يمكن أيضًا استخدامها لمنع طباعة السطر الجديد قبل الإجراء من خلال وضعها قبل الإجراء، أي في البداية {{ - . len - }}.

لنُشغّل ملف البرنامج "pets.go" من خلال الأمر go run:

$ go run pets.go

سيكون الخرج على النحو التالي:

Number of dogs: 3
---
Name:  Jujube

Sex:   Female (spayed)

Age:   10 months

Breed: German Shepherd & Pitbull

---
Name:  Zephyr

Sex:   Male (intact)

Age:   13 years, 3 months

Breed: German Shepherd & Border Collie

---
Name:  Bruce Wayne

Sex:   Male (neutered)

Age:   3 years, 8 months

Breed: Chihuahua

باستخدام الشرطة في {{- len | .}}، لا توجد أسطر فارغة بين جملة Number of dogs وتفاصيل الكلب الأول.

تجدر الإشارة إلى أن الدوال المضمنة في حزمة text/template محدودة، ومع ذلك يمكن استخدام أي دالة في لغة جو مع القوالب طالما أنها تُرجع قيمة واحدة أو قيمتين، إذ تكون الثانية قيمةً من نوع خطأ error. يسمح هذا بتوسيع دوال القوالب التي يمكن استخدامها من خلال دمج دوال خاصة.

استخدام دوال لغة جو مع القوالب

لنفترض أننا نريد كتابة قالب يأخذ شريحة من الكلاب ويعرض فقط الكلب الأخير. يمكننا في قوالب لغة جو استخراج مجموعة فرعية من شريحة باستخدام الدالة المبنية مسبقًا slice، والتي تعمل بطريقة تشبه [mySlice [x:y في لغة جو. إذا كنا نريد مثلًا استرداد العنصر الأخير من شريحة مكونة من ثلاثة عناصر، فيمكن استخدام {{ slice . 2 }}. من المهم ملاحظة أن slice تُرجع شريحةً أخرى، وليس عنصرًا فرديًا. لذا، {{slice. 2}} تكافئ [:slice [2، وليس [slice [2. يمكن أيضًا أن تقبل الدالة slice عدة فهارس، مثل{{ slice. 0 2 }}لاسترداد الشريحة [slice [0: 2، لكننا لن نستخدم ذلك في هذا السيناريو.

يظهر التحدي عندما نريد الإشارة إلى الفهرس الأخير للشريحة داخل القالب الخاص بنا. على الرغم من أن الدالة len متاحة، إلا أن العنصر الأخير في الشريحة موجود في الفهرس len - 1، وللأسف، لا تدعم القوالب العمليات الحسابية. هنا يمكننا إنشاء دالة مخصصة للتغلب على هذا القيد، وذلك من خلال كتابة دالة تُنقص قيمة عدد صحيح مُمرر لها.

بدايةً ننشئ ملف قالب جديد. نفتح ملفًا جديدًا يسمى "lastPet.tmpl" ونضع المحتوى التالي:

{{- range (len . | dec | slice . ) }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }} ({{ if .Intact }}intact{{ else }}{{ if ("Female" | eq .Sex) }}spayed{{ else }}neutered{{ end }}{{ end }})

Age:   {{ .Age }}

Breed: {{ .Breed }}
{{ end -}}

يستخدم هذا القالب إجراء range مع الشريحة المُعدّلة للتكرار على آخر كلب في الشريحة المحددة. نُطبّق الدالة المخصصة dec الموجودة في السطر الأول لتقليل طول الشريحة، مما يسمح لنا بالوصول إلى الفهرس الأخير. يعرض القالب بعد ذلك المعلومات ذات الصلة بالكلب الأخير، بما في ذلك الاسم والجنس والعمر والسلالة.

لتعريف الدالة dec المخصصة وتمريرها إلى القالب، نُجري التغييرات التالية داخل الدالة main في الملف "pets.go" -بعد شريحة الكلاب وقبل استدعاء ()tmpl.Execute- كما هو موضح أدناه:

. . .
    funcMap := template.FuncMap{
        "dec": func(i int) int { return i - 1 },
    }
    var tmplFile = lastPet.tmpl
    tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile)
    if err != nil {
        panic(err)
    }
    . . .

صرّحنا عن FuncMap على أنها رابط map للدوال، إذ تمثِّل أزواج (المفتاح، القيمة) أسماء الدوال والتطبيق المقابلة لها. نُعرّف في هذه الحالة الدالة dec على أنها دالة مجهولة تطرح 1 من عدد صحيح وتعيد النتيجة.

نُغيّر بعد ذلك اسم ملف القالب إلى "lastPet.tmpl". أخيرًا نستدعي التابع Funcs من القالب، قبل استدعاء ParseFiles، ونمرر له funcMap لإتاحة الدالة dec داخل القالب. من المهم ملاحظة أنه يجب استدعاء Funcs قبل ParseFiles لتسجيل الدالة المخصصة بطريقة صحيحة مع القالب.

دعونا نفهم بدايةً ما يحدث في الإجراء range:

{{- range (len . | dec | slice . ) }}

يجري في هذا السطر الحصول على طول شريحة الكلاب باستخدام . len، ثم تمرير النتيجة إلى الدالة dec المخصصة لطرح قيمة 1 من المتغير المُمرر لها len . | dec، ثم تمرير النتيجة مثل معاملٍ ثانٍ إلى الدالة slice. لذلك، بعبارات أبسط، بالنسبة لشريحة مكونة من ثلاثة كلاب، فإن range تعادل:

{{- range (slice . 2) }}

لنُشغّل ملف البرنامج "pets.go" بعد حفظه من خلال الأمر go run:

$ go run pets.go

سيكون الخرج على النحو التالي:

---
Name:  Bruce Wayne

Sex:   Male (neutered)

Age:   3 years, 8 months

Breed: Chihuahua

يبدو هذا جيدًا. ماذا لو أردنا إظهار آخر كلبين بدلًا من آخر كلب فقط؟ نُحرّر الملف "lastPet.tmpl" ونضيف استدعاءً آخرًا للدالة dec:

{{- range (len . | dec | dec | slice . ) }}
. . .

لنُشغّل ملف البرنامج "pets.go" بعد حفظه من خلال الأمر go run:

$ go run pets.go

ليكون الخرج على النحو التالي:

---
Name:  Zephyr

Sex:   Male (intact)

Age:   13 years, 3 months

Breed: German Shepherd/Border Collie

---
Name:  Bruce Wayne

Sex:   Male (neutered)

Age:   3 years, 8 months

Breed: Chihuahua

يمكن تحسين الدالة dec من خلال جعلها تأخذ معاملًا واحدًا ونغيّر اسمها بحيث نكتب minus 2 بدلًا من dec | dec.

لنفرض أننا أردنا عرض الكلاب الهجينة مثل "Zephyr" بطريقة مختلفة، وذلك باستبدال الشرطة المائلة بعلامة العطف &. لحسن الحظ لن نضطر لكتابة دالة خاصة لذلك، إذ يمكننا الاستفادة من دالة موجودة في الحزمة strings، لكن نحتاج إلى إجراء بعض التغييرات على ملف "pets.go" قبل ذلك. نستورد أولًا الحزمة strings مع الحزم الأخرى في أعلى الملف. نُحدِّث بعد ذلك المتغير funcMap داخل الدالة main لتضمين دالة ReplaceAll من حزمة strings:

package main

import (
    "os"
    "strings"
    "text/template"
)
. . .
func main() {
    . . .
    funcMap := template.FuncMap{
        "dec":     func(i int) int { return i - 1 },
        "replace": strings.ReplaceAll,
    }
    . . .
} // main نهاية الدالة

من خلال إضافة strings.ReplaceAll إلى funcMap، نكون قد جعلناها متاحةً في القالب تحت الاسم replace. نفتح ملف "lastPet.tmpl" ونعدّله لاستخدام replace:

{{- range (len . | dec | dec | slice . ) }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }} ({{ if .Intact }}intact{{ else }}{{ if ("Female" | eq .Sex) }}spayed{{ else }}neutered{{ end }}{{ end }})

Age:   {{ .Age }}

Breed: {{ replace .Breed “/”  &  }}
{{ end -}}

لنُشغّل ملف البرنامج "pets.go" بعد حفظه من خلال الأمر go run:

$ go run pets.go

سيكون الخرج على النحو التالي:

---
Name:  Zephyr

Sex:   Male (intact)

Age:   13 years, 3 months

Breed: German Shepherd & Border Collie

---
Name:  Bruce Wayne

Sex:   Male (neutered)

Age:   3 years, 8 months

Breed: Chihuahua

تحتوي سلالة Zephyr الآن على علامة عطف بدلًا من شرطة مائلة. أجرينا هذا التعديل على حقل Breed داخل القالب بدلًا من تعديل البيانات في pets.go. يتبع هذا المبدأ القائل بأن عرض البيانات هو مسؤولية القوالب وليس الشيفرة.

تجدر الإشارة إلى أن بعض بيانات الكلاب، مثل حقل Breed، تحتوي على بعض المظاهر التي قد لا تكون طريقة عرض المعلومات فيها مثالية في هندسة البرمجيات عمومًا، إذ يمكن أن يؤدي التنسيق الحالي لتخزين سلالات متعددة في سلسلة واحدة مفصولة بشرطة مائلة / إلى اختلافات في عملية إدخال البيانات، مما يؤدي إلى تنسيقات غير متسقة في قاعدة البيانات (على سبيل المثال، Labrador/Poodle و Labrador & Poodleو Labrador, Poodleو Labrador-Poodle mix، إلخ).

لمعالجة هذه المشكلة وتحسين المرونة في البحث حسب السلالة وتقديم البيانات، قد يكون من الأفضل تخزين حقل Breed مثل شريحة من السلاسل (string[]) بدلًا من سلسلة واحدة. سيؤدي هذا التغيير إلى إزالة الغموض في التنسيق ويسمح بمعالجة أسهل في القوالب. يمكن بعد ذلك استخدام دالة strings.Join ضمن القالب لربط جميع السلالات، جنبًا إلى جنب مع ملاحظة إضافية من خلال الحقل Breed. بحيث تشير إلى ما إذا كان الكلب سلالة أصيلة (purebred) أو سلالة هجينة (mixed breed).

دعونا في الختام نعرض نفس البيانات في مستند HTML ونرى لماذا يجب علينا دائمًا استخدام حزمة html/template عندما يكون ناتج القالب الخاصة بنا بتنسيق HTML.

الخطوة 5- كتابة قالب HTML

في حين أن الحزمة text/template مناسبة لطباعة الخرج بدقة (سواءً من سطر الأوامر أو مكان آخر) وإنشاء ملفات منظّمة من البرامج الدفعية Batch program (برامج تعالج سلسلة من المهام أو الأوامر دفعة واحدة أو بطريقة غير تفاعلية)، إلا أنه من الشائع استخدام قوالب لغة جو لتصيير صفحات HTML في تطبيقات الويب. على سبيل المثال، يعتمد مُنشئ الموقع الثابت (أداة تساعد في إنشاء ملفات HTML ثابتة بناءً على القوالب والمحتوى. يبسط عملية إنشاء مواقع الويب وإدارتها عن طريق تحويل القوالب والمحتوى والموارد الأخرى إلى موقع ويب ثابت جاهز للنشر) هوغو Hugo على كل من text/template و html/template مثل أساس لنظام القوالب الخاص به. تتيح هذه الحزم للمستخدمين تحديد القوالب ذات معاملات النوع وإدراج البيانات ديناميكيًا فيها، مما يتيح إنشاء صفحات HTML لمواقع الويب.

تقدم لغة HTML ميزات فريدة لا نراها مع النص العادي، إذ تستخدم أقواس الزاوية لتغليف العناصر (<td>) وعلامات العطف لتمييز الكيانات (;nbsp&) وعلامات الاقتباس لتغليف قيم أو سمات الوسوم (<"/a href="https://www.digitalocean.com>). عند إدخال البيانات التي تحتوي على هذه الأحرف باستخدام حزمة text/template، يمكن أن ينتج عن ذلك HTML تالف أو حتى حقن شيفرة Code injection (ثغرة أمنية يتمكن منها المهاجم من إدخال التعليمات البرمجية الضارة وتنفيذها داخل تطبيق أو نظام).

تعالج حزمة html/template هذه التحديات، بحيث تهرب تلقائيًا من المحارف التي قد تخلق إشكالية، وتستبدلها بكيانات HTML الآمنة. تُصبح علامة العطف في البيانات (;amp&) وقوس الزاوية اليسرى (;It&) وهكذا.

دعونا نواصل استخدام نفس بيانات الكلاب، لإثبات خطورة استخدام text/template مع HTML. نفتح "pets.go" ونعدّل حقل Name على النحو التالي:

. . .
    dogs := []Pet{
        {
            Name:   "<script>alert(\"Gotcha!\");</script>Jujube",
            Sex:    "Female",
            Intact: false,
            Age:    "10 months",
            Breed:  "German Shepherd/Pit Bull",
        },
        {
            Name:   "Zephyr",
            Sex:    "Male",
            Intact: true,
            Age:    "13 years, 3 months",
            Breed:  "German Shepherd/Border Collie",
        },
        {
            Name:   "Bruce Wayne",
            Sex:    "Male",
            Intact: false,
            Age:    "3 years, 8 months",
            Breed:  "Chihuahua",
        },
    }
        . . .

نُنشئ الآن قالب HTML في ملف جديد يسمى "petsHtml.tmpl":

<p><strong>Pets:</strong> {{ . | len }}</p>
{{ range . }}
<hr />
<dl>
    <dt>Name</dt>
    <dd>{{ .Name }}</dd>
    <dt>Sex</dt>
    <dd>{{ .Sex }} ({{ if .Intact }}intact{{ else }}{{ if (eq .Sex "Female") }}spayed{{ else }}neutered{{ end }}{{ end }})</dd>
    <dt>Age</dt>
    <dd>{{ .Age }}</dd>
    <dt>Breed</dt>
    <dd>{{ replace .Breed “/” “ & ” }}</dd>
</dl>
{{ end }}

نحفظ قالب HTML. نحتاج إلى تعديل المتغير tmpFile قبل تشغيل "pets.go"، ولكن دعونا أيضًا نُعدّل البرنامج لإخراج القالب إلى ملف بدلًا من الطرفية. نفتح الملف "pets.go" ونضيف الشيفرة التالية داخل الدالة main:

. . .
    funcMap := template.FuncMap{
        "dec":     func(i int) int { return i - 1 },
        "replace": strings.ReplaceAll,
    }
    var tmplFile = "petsHtml.tmpl"
    tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile)
    if err != nil {
        panic(err)
    }
    var f *os.File
    f, err = os.Create("pets.html")
    if err != nil {
        panic(err)
    }
    err = tmpl.Execute(f, dogs)
    if err != nil {
        panic(err)
    }
    err = f.Close()
    if err != nil {
        panic(err)
    }
} // end main

نفتح ملف File جديد يسمى "pets.html" ونمرّره (بدلًا من os.Stdout) إلى tmpl.Execute، ثم نغلق الملف عند الانتهاء.

لنُشغّل ملف البرنامج "pets.go" من خلال الأمر go run لإنشاء ملف HTML. نفتح بعد ذلك صفحة الويب المحلية هذه في المتصفح:

$ go run pets.go

img1

شغّل المتصفح البرنامج النصي المحقون، وهذا هو السبب في أنه لا يجب أبدًا استخدام حزمة text/template لإنشاء HTML، خاصةً عندما لا يمكن الوثوق تمامًا بمصدر بيانات القالب.

بصرف النظر عن محارف الهروب في HTML، تعمل حزمة html /template تمامًا مثل text/template ولها نفس الاسم الأساسي ("قالب" template)، مما يعني أن كل ما علينا فعله لجعل القالب آمنًا هو استبدال استيراد text/template مع html /template. لنعدّل ملف "pets.go" وفقًا لذلك الآن:

package main

import (
    "os"
    "strings"
    "html/template"
)
. . .

نحفظ الملف لتعديل بيانات "pets.html" ونشغّله مرةً أخيرة. ثم نعيد تحميل ملف HTML في المتصفح:

img2

صيّرت حزمة html/template النص المُدخل على أنه نص فقط في صفحة الويب. نفتح الملف "pets.html" في محرر النصوص (أو نعرض مصدر الصفحة في المتصفح) وننظر إلى أول كلب Jujube:

. . .
<dl>
        <dt>Name</dt>
        <dd>&lt;script&gt;alert(&#34;Gotcha!&#34;);&lt;/script&gt;Jujube</dd>
        <dt>Sex</dt>
        <dd>Female (spayed)</dd>
        <dt>Age</dt>
        <dd>10 months</dd>
        <dt>Breed</dt>
        <dd>German Shepherd &amp; Pit Bull</dd>
</dl>
. . .

استبدلت حزمة html أقواس الزاوية ومحارف الاقتباس في اسم Jujube، وكذلك علامة العطف في السلالة.

الخاتمة

توفر قوالب لغة جو حلًا متعدد القدرات لدمج البيانات في تنسيقات نصية متنوعة. يمكن استخدام القوالب في أدوات سطر الأوامر لتنسيق الخرج، وكذلك في تطبيقات الويب لإنشاء صفحات HTML. تعلمنا خلال هذا المقال كيفية الاستفادة من حزم القوالب المضمنة في لغة جو لإنتاج نص جيد التنظيم وعرض HTML باستخدام نفس البيانات.

ترجمة -وبتصرف- للمقال How To Use Templates in Go لصاحبه Kristin Davidson.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...