Web Framework สำหรับ clojurescript บนโหนด

ในประเพณีของ Django, ขวดและราง ออกแบบมาสำหรับนักพัฒนาอินดี้ที่จัดส่งอย่างรวดเร็ว การต่อสู้ทดสอบในเว็บไซต์จริง
ปรัชญา เริ่มต้นอย่างรวดเร็ว | เอกสาร API | ตัวอย่าง ชุมชน
( ns webserver
( :require
[promesa.core :as p]
[sitefox.html :refer [render]]
[sitefox.web :as web]))
( defn root [_req res]
( ->> ( render [ :h1 " Hello world! " ])
( .send res)))
( p/let [[app host port] ( web/start )]
( .get app " / " root)
( print " Serving on " ( str " http:// " host " : " port)))PORT - กำหนดค่าพอร์ตเว็บเซิร์ฟเวอร์เซิร์ฟเวอร์เชื่อมโยงไปที่BIND_ADDRESS - กำหนดค่าที่อยู่ IP SiteFox Web Server เชื่อมโยงไปที่SMTP_SERVER - กำหนดค่าเซิร์ฟเวอร์ SMTP ขาออกเช่น SMTP_SERVER=smtps://username:[email protected]/?pool=trueDATABASE_URL - กำหนดค่าฐานข้อมูลเพื่อเชื่อมต่อ ค่าเริ่มต้นเป็น sqlite://./database.sqlite วิธีที่เร็วที่สุดในการเริ่มต้นคือการใช้หนึ่งในสคริปต์ create ซึ่งจะตั้งค่าโครงการตัวอย่างสำหรับคุณด้วยคำสั่งเดียว หากคุณกำลังสร้างเว็บไซต์ที่เรียบง่ายโดยไม่มีการโต้ตอบส่วนหน้ามากเกินกว่าการส่งแบบฟอร์ม NBB สร้างสคริปต์เป็นวิธี:
npm init sitefox-nbb mywebsite
สิ่งนี้จะสร้างโฟลเดอร์ที่เรียกว่า mywebsite ที่มีโครงการใหม่ของคุณ หมายเหตุคุณสามารถใช้ Scittle เพื่อเรียกใช้ CLJS ฝั่งไคลเอ็นต์
หากคุณกำลังสร้างแอปพลิเคชัน clojurescript แบบเต็มรูปแบบ Shadow-Cljs สร้างสคริปต์เป็นวิธี:
npm init sitefox-shadow-fullstack myapp
ที่จะสร้างโฟลเดอร์ที่เรียกว่า myapp ที่มีโครงการใหม่ของคุณ
เพิ่ม sitefox ในโครงการของคุณเป็นการพึ่งพา:
{:deps
{io.github.chr15m/sitefox {:git/tag "v0.0.26" :git/sha "e6ea2027b5d4277917732d43d550083c8e105da9"}}}
หากคุณใช้ npm คุณสามารถติดตั้ง SiteFox เป็นแบบพึ่งพาได้ หากคุณทำเช่นนั้นคุณจะต้องเพิ่ม node_modules/sitefox/src ใน classpath ของคุณอย่างใด
npm i sitefox
หมายเหตุ : ผู้ใช้ M1 Mac อาจจำเป็นต้องตั้งค่าเวอร์ชัน Python ใน NPM เช่นนี้:
npm config set python python3
นี่เป็นเพราะการสร้าง node-sqlite3 บางครั้งล้มเหลวโดยไม่ต้องตั้งค่า ดูปัญหานี้สำหรับรายละเอียดเพิ่มเติม
ตัวอย่างเซิร์ฟเวอร์ที่มีสองเส้นทางซึ่งหนึ่งในนั้นเขียนค่าไปยังฐานข้อมูลคีย์-ค่า
( ns my.server
( :require
[promesa.core :as p]
[sitefox.web :as web]
[sitefox.db :refer [kv]]))
( defn home-page [req res]
; send a basic hello world response
( .send res " Hello world! " ))
( defn hello [req res]
; write a value to the key-value database
( p/let [table ( kv " sometable " )
r ( .write table " key " 42 )]
( .json res true )))
( defn setup-routes [app]
; flush all routes from express
( web/reset-routes app)
; set up an express route for "/"
( .get app " / " home-page)
; set up an express route for "/hello"
( .post app " /hello " hello)
; statically serve files from the "public" dir on "/"
; (or from "build" dir in PROD mode)
( web/static-folder app " / " " public " ))
( defn main! []
; create an express server and start serving
; BIND_ADDRESS & PORT env vars set host & port.
( p/let [[app _host _port] ( web/start )]
; set up the routes for the first time
( setup-routes app)))ตัวอย่างเพิ่มเติม sitefox ที่นี่
หากคุณต้องการการสนับสนุนกับ sitefox คุณสามารถ:
SiteFox ใช้เว็บเซิร์ฟเวอร์ด่วนที่มีค่าเริ่มต้นที่เหมาะสมสำหรับเซสชันและการบันทึก ดูเอกสารการกำหนดเส้นทางด่วนสำหรับรายละเอียด
สร้างเซิร์ฟเวอร์ใหม่ด้วย web/start และตั้งค่าเส้นทางที่ตอบสนองด้วย "Hello World!" ดังนี้:
( -> ( web/start )
( .then ( fn [app host port]
( .get app " /myroute "
( fn [req res]
( .send res " Hello world! " )))) SiteFox มาพร้อมกับระบบเสริมเพื่อโหลดเส้นทางใหม่เมื่อเซิร์ฟเวอร์เปลี่ยน เส้นทางด่วนของคุณจะถูกโหลดซ้ำทุกครั้งที่รหัสเซิร์ฟเวอร์ของคุณได้รับการรีเฟรช (เช่นโดย build Shadow-Cljs) ในตัวอย่างนี้ setup-routes จะถูกเรียกเมื่อมีการสร้างใหม่เกิดขึ้น
( defn setup-routes [app]
; flush all routes from express
( web/reset-routes app)
; ask express to handle the route "/"
( .get app " / " ( fn [req res] ( .send res " Hello world! " ))))
; during the server setup hook up the reloader
( reloader ( partial #'setup-routes app)) ฉันแนะนำห้องสมุด Promesa สำหรับการจัดการโฟลว์การควบคุมสัญญา ตัวอย่างนี้จำเป็นต้องมี [promesa.core :as p] :
( p/let [[app host port] ( web/start )]
; now use express `app` to set up routes and middleware
)ดูตัวอย่างเหล่านี้:
แทนที่จะเป็นเทมเพลต SiteFox เสนอทางลัดสำหรับการเรนเดอร์รีเอเจนต์ด้านเซิร์ฟเวอร์, เอกสาร HTML ที่ผสานรวมกัน
[sitefox.html :refer [render-into]]คุณสามารถโหลดเอกสาร HTML และแสดงผลรีเอเจนต์ลงในองค์ประกอบที่เลือก:
( def index-html ( fs/readFileSync " index.html " ))
( defn component-main []
[ :div
[ :h1 " Hello world! " ]
[ :p " This is my content. " ]])
; this returns a new HTML string that can be returned
; e.g. with (.send res)
( render-into index-html " main " [component-main])SiteFox ใช้ Node-HTML-PARSER และเสนอทางลัดสำหรับการทำงานกับ HTML & Reagent:
html/parse เป็นชวเลขสำหรับ node-html-parser/parsehtml/render เป็นชวเลขสำหรับ render-to-static-markuphtml/$ เป็นชวเลขสำหรับ querySelector ของตัวแยกวิเคราะห์html/$$ เป็นชวเลขสำหรับตัวแยก querySelectorAllดูโครงการตัวอย่างเทมเพลตด้วย
SiteFox ทำให้ง่ายต่อการเริ่มจัดเก็บข้อมูลคีย์ค่าโดยไม่มีการกำหนดค่า คุณสามารถเปลี่ยนเป็นข้อมูลที่มีโครงสร้างมากขึ้นในภายหลังหากคุณต้องการ มันรวม KeyV ซึ่งเป็นฐานข้อมูลที่ได้รับการสนับสนุนคีย์-ค่า คุณสามารถเข้าถึงร้านค้าคีย์-ค่าผ่าน db/kv และฐานข้อมูลพื้นฐานผ่าน db/client
ดูเอกสารฉบับเต็มสำหรับโมดูล DB
โดยค่าเริ่มต้นจะใช้ฐานข้อมูล SQLite ในเครื่องและคุณสามารถเริ่มข้อมูลคงที่บนเซิร์ฟเวอร์ได้ทันทีโดยไม่ต้องกำหนดค่าใด ๆ เมื่อคุณย้ายไปที่การผลิตคุณสามารถกำหนดค่าฐานข้อมูลอื่นโดยใช้ Database Variable DATABASE_URL ตัวอย่างเช่นในการใช้ฐานข้อมูล Postgres ที่เรียกว่า "DBNAME" คุณสามารถเข้าถึงได้ดังนี้ (ขึ้นอยู่กับการตั้งค่าเครือข่าย/ท้องถิ่นของคุณ):
DATABASE_URL="postgres://%2Fvar%2Frun%2Fpostgresql/DBNAME"
DATABASE_URL=postgres://someuser:somepassword@somehost:5432/DBNAME
DATABASE_URL=postgres:///somedatabase
โปรดทราบว่าคุณจะต้อง npm install @keyv/postgres หากคุณต้องการใช้แบ็กเอนด์ Postgres
ในการใช้ฐานข้อมูลและอินเทอร์เฟซคีย์-ค่าก่อนต้องใช้โมดูลฐานข้อมูล:
[sitefox.db :as db] ตอนนี้คุณสามารถใช้ db/kv เพื่อเขียนค่าคีย์ไปยังตาราง "
( let [table ( db/kv " sometable " )]
( .set table " key " " 42 " ))ดึงค่าอีกครั้ง:
( -> ( .get table " key " )
( .then ( fn [val] ( print val)))) คุณสามารถใช้ db/client เพื่อเข้าถึงไคลเอนต์ฐานข้อมูลพื้นฐาน ตัวอย่างเช่นการทำแบบสอบถามกับฐานข้อมูลที่กำหนดค่า:
( let [c ( db/client )]
( -> ( .query c " select * from sometable WHERE x = 1 " )
( .then ( fn [rows] ( print rows)))))แนะนำให้ใช้ Promesa อีกครั้งสำหรับการจัดการโฟลว์ควบคุมระหว่างการดำเนินการฐานข้อมูล
ในการสำรวจข้อมูลค่าคีย์จากบรรทัดคำสั่งให้ใช้ SQLite และ JQ เพื่อกรองข้อมูลเช่นนี้:
sqlite3 database.sqlite "select * from keyv where key like 'SOMEPREFIX%';" | cut -f 2 -d "|" | jq '.'
โดยค่าเริ่มต้นโมดูล node-sqlite3 จะไม่ให้ร่องรอยสแต็กเต็มรูปแบบพร้อมหมายเลขบรรทัด ฯลฯ เมื่อเกิดข้อผิดพลาดของฐานข้อมูล เป็นไปได้ที่จะเปิดการร่องรอยสแต็ก Verbose ด้วยการลงโทษประสิทธิภาพเล็กน้อยดังนี้:
( ns yourapp
( :require
[ " sqlite3 " :as sqlite3]))
( .verbose sqlite3) หากคุณต้องการเรียกใช้ SQLITE3 ในการผลิตคุณอาจพบข้อผิดพลาด SQLITE_BUSY: database is locked เมื่อทำการดำเนินการฐานข้อมูลพร้อมกันจากไคลเอนต์ที่แตกต่างกัน เป็นไปได้ที่จะแก้ไขปัญหาการเกิดขึ้นพร้อมกันและการล็อคเหล่านี้โดยการเปิดใช้งานโหมดบันทึกการเขียนล่วงหน้าใน SQLite3 ดังนี้:
(ns yourapp
(:require
[sitefox.db :refer [client]]))
(p/let [c (client)
wal-mode-enabled (.query c "PRAGMA journal_mode=WAL;")]
(js/console.log wal-mode-enabled))
รหัสนี้สามารถวางได้อย่างปลอดภัยในฟังก์ชั่นหลักของรหัสเซิร์ฟเวอร์ของคุณ
เซสชันจะเปิดใช้งานโดยค่าเริ่มต้นและผู้เข้าชมแต่ละคนไปยังเซิร์ฟเวอร์ของคุณจะมีเซสชันของตัวเอง ข้อมูลเซสชันนั้นยังคงมีอยู่ด้านเซิร์ฟเวอร์ทั่วทั้งหน้าโหลดเพื่อให้คุณสามารถใช้เพื่อจัดเก็บสถานะการรับรองความถูกต้องตัวอย่างเช่น เซสชั่นจะถูกสำรองไว้ในตาราง kv แบบไม่เหมาะสม (ดูส่วนฐานข้อมูลด้านบน) คุณสามารถอ่านและเขียนโครงสร้างข้อมูล JS โดยพลการไปยังเซสชันโดยใช้ req.session
ในการเขียนค่าไปยังที่เก็บเซสชัน (ภายในฟังก์ชั่นตัวจัดการเส้นทาง):
( let [session ( aget req " session " )]
( aset session " myvalue " 42 ))เพื่ออ่านค่าจากร้านค้าเซสชัน:
( aget req " session " " myvalue " )SiteFox ห่อไลบรารีหนังสือเดินทางเพื่อใช้การรับรองความถูกต้อง คุณสามารถเพิ่มการรับรองความถูกต้องตามอีเมลและรหัสผ่านง่าย ๆ ในแอปของคุณด้วยการโทรสามฟังก์ชัน:
( defn setup-routes [app]
( let [template ( fs/readFileSync " index.html " )]
( web/reset-routes app)
; three calls to set up email based authentication
( auth/setup-auth app)
( auth/setup-email-based-auth app template " main " )
( auth/setup-reset-password app template " main " )
; ... add your additional routes here ... ;
)) สตริง template ที่ส่งผ่านเป็นเอกสาร HTML และ "main" คือตัวเลือกที่ระบุว่าจะติดตั้ง UI Auth ได้ที่ไหน สิ่งนี้จะตั้งค่าเส้นทางต่อไปนี้โดยค่าเริ่มต้นซึ่งคุณสามารถส่งผู้ใช้ไปลงทะเบียนลงชื่อเข้าใช้และรีเซ็ตรหัสผ่าน:
/auth/sign-in/auth/sign-up/auth/reset-passwordนอกจากนี้ยังเป็นไปได้ที่จะแทนที่แบบฟอร์มรีเอเจนต์ Auth Auth เริ่มต้นและ URL เปลี่ยนเส้นทางเพื่อปรับแต่งด้วยเวอร์ชันของคุณเอง ดูเอกสารรับรองความถูกต้องสำหรับรายละเอียดเกี่ยวกับวิธีการจัดหาแบบฟอร์มรีเอเจนต์ของคุณเอง ดูซอร์สโค้ดสำหรับฟอร์ม Auth Auth ที่เป็นค่าเริ่มต้นหากคุณต้องการทำเอง
เมื่อผู้ใช้ลงทะเบียนข้อมูลของพวกเขาจะยังคงอยู่ในฐานข้อมูล KeyV เริ่มต้นที่ใช้โดย SiteFox คุณสามารถเรียกคืนโครงสร้างข้อมูลของผู้ใช้ที่ผ่านการรับรองความถูกต้องในวัตถุคำขอ:
( let [user ( aget req " user " )] ...) จากนั้นคุณสามารถอัปเดตข้อมูลของผู้ใช้และบันทึกข้อมูลกลับไปยังฐานข้อมูล ห้องสมุด applied-science.js-interop สะดวกสำหรับสิ่งนี้ (จำเป็นที่นี่เป็น j ):
( p/let [user ( aget req " user " )]
( j/assoc! user :somekey 42 )
( auth/save-user user)) หากคุณต้องการสร้างตารางใหม่จะมีประโยชน์ในการคีย์บน UUID ของผู้ใช้ซึ่งคุณสามารถรับได้ด้วย (aget user "id")
ดูตัวอย่างการรับรองความถูกต้องสำหรับรายละเอียดเพิ่มเติม
หากต้องการเพิ่มรูปแบบการรับรองความถูกต้องใหม่เช่นชื่อผู้ใช้หรือ OAuth บุคคลที่สามให้ปรึกษาเอกสารหนังสือเดินทางและ Auth.cljs ดึงคำขอต้อนรับมากที่สุด!
SiteFox Bundles Nodemailer สำหรับการส่งอีเมล กำหนดค่าเซิร์ฟเวอร์ SMTP ขาออกของคุณ:
SMTP_SERVER=smtps://username:[email protected]/?pool=true
จากนั้นคุณสามารถใช้ฟังก์ชั่น send-email ดังนี้:
( -> ( mail/send-email
" [email protected] "
" [email protected] "
" This is my test email. "
:text " Hello, This is my first email from **Sitefox**. Thank you. " )
( .then js/console.log)) โดยอีเมลที่ส่งเริ่มต้นจะถูกบันทึกไปที่ ./logs/mail.log ในรูปแบบ JSON-LINES
หากคุณไม่ได้ระบุเซิร์ฟเวอร์ SMTP โมดูลอีเมลจะอยู่ในโหมดดีบัก จะไม่มีการส่งอีเมลอีเมลขาออกจะถูกเขียนไปยัง /tmp เพื่อตรวจสอบและผลลัพธ์ send-email จะถูกบันทึกไปยังคอนโซล
หากคุณตั้งค่า SMTP_SERVER=ethereal บริการ eThereal.email จะถูกใช้ หลังจากเรียกใช้ Email send-email คุณสามารถพิมพ์คุณสมบัติ url ของผลลัพธ์ คุณสามารถใช้ลิงก์ที่พิมพ์เพื่อทดสอบอีเมลของคุณในโหมด Dev
ดูโครงการ Send-Email ตัวอย่าง
ดูตัวอย่างการตรวจสอบแบบฟอร์มซึ่งใช้ Node-Input-Validator และตรวจสอบปัญหา CSRF
เพื่อให้แน่ใจว่าคุณสามารถ POST โดยไม่ต้องเตือน CSRF คุณควรสร้างองค์ประกอบที่ซ่อนอยู่เช่นนี้ (ไวยากรณ์ไวยากรณ์):
[ :input { :name " _csrf " :type " hidden " :default-value ( .csrfToken req)}] หากคุณกำลังทำคำขอ POST AJAX จากฝั่งไคลเอ็นต์คุณควรส่งโทเค็น CSRF เป็นส่วนหัว โทเค็นที่ถูกต้องมีให้เป็นสตริงที่ JSON Endpoint /_csrf-token และคุณสามารถดึงข้อมูลได้โดยใช้ fetch-csrf-token และเพิ่มลงในส่วนหัวของคำขอดึงข้อมูลดังนี้:
( ns n ( :require [sitefox.ui :refer [fetch-csrf-token]]))
( -> ( fetch-csrf-token )
( .then ( fn [token]
( js/fetch " /api/endpoint "
#js { :method " POST "
:headers #js { :Content-Type " application/json "
:X-XSRF-TOKEN token} ; <- use token here
:body ( js/JSON.stringify ( clj->js some-data))})))) หมายเหตุ : คุณสามารถดึงโทเค็น CSRF จากคุกกี้ฝั่งไคลเอ็นต์แทนหากคุณตั้งค่าตัวแปรสภาพแวดล้อม SEND_CSRF_TOKEN นี่เป็นค่าเริ่มต้นในเวอร์ชัน sitefox ก่อนหน้า เมื่อตั้งค่า SiteFox จะส่งโทเค็นในทุกคำขอ GET ในคุกกี้ XSRF-TOKEN และสามารถเรียกคืนได้ด้วยฟังก์ชั่น ui/csrf-token นี่เป็นรูปแบบการป้องกัน CSRF ที่ถูกต้อง แต่ปลอดภัยน้อยกว่า
ในบางสถานการณ์ที่หายากคุณอาจต้องการปิดการตรวจสอบ CSRF (เช่นการโพสต์ไปยัง API จากอุปกรณ์ที่ไม่ใช่เบราว์เซอร์) หากคุณรู้ว่าคุณกำลังทำอะไรอยู่คุณสามารถใช้ pre-csrf-router เพื่อเพิ่มเส้นทางที่ข้ามการตรวจสอบ CSRF:
( defn setup-routes [app]
; flush all routes from express
( web/reset-routes app)
; set up an API route bypassing CSRF checks
( .post ( j/get app " pre-csrf-router " ) " /api/endpoint " endpoint-unprotected-by-csrf)
; set up an express route for "/hello" which is protected as normal
( .post app " /hello " hello)) โดยค่าเริ่มต้นเว็บเซิร์ฟเวอร์จะเขียนลงในบันทึกไฟล์ในโฟลเดอร์ ./logs ไฟล์เหล่านี้จะถูกหมุนโดยอัตโนมัติโดยเซิร์ฟเวอร์ บันทึกมีสองประเภท:
logs/access.log ซึ่งเป็นบันทึกการเข้าถึงเว็บมาตรฐานในรูปแบบ "รวม"logs/error.log ที่เขียน tracebacks โดยใช้ tracebacks/install-traceback-handlerเพื่อส่งข้อยกเว้นที่ไม่ได้รับการยกเว้นไปยังบันทึกข้อผิดพลาด:
(def admin-email (env-required "ADMIN_EMAIL"))
(def build-id (try (fs/readFileSync "build-id.txt") (catch :default _e "dev")))
(install-traceback-handler admin-email build-id)
สร้าง build-id.txt ตาม GIT ปัจจุบันที่กระทำดังต่อไปนี้:
git rev-parse HEAD | cut -b -8 > build-id.txt
หากคุณต้องการได้รับหมายเลขบรรทัด clojurescript ที่ถูกต้องใน tracebacks จำเป็นต้องมี ["source-maps-support" :as sourcemaps] และจากนั้น::
(.install sourcemaps)
คุณสามารถใช้ฟังก์ชั่น web/setup-error-handler เพื่อให้บริการหน้าสำหรับข้อผิดพลาดเหล่านั้นตามองค์ประกอบรีเอเจนต์ที่คุณกำหนด:
( defn component-error-page [_req error-code error]
[ :section.error
[ :h2 error-code " Error " ]
( case error-code
404 [ :p " We couldn't find the page you're looking for. " ]
500 [ :<> [ :p " An error occurred: " ] [ :p ( .toString error)]]
[ :div " An unknown error occurred. " ])])
( web/setup-error-handler app my-html-template " main " component-error-page)คุณสามารถรวมสิ่งเหล่านี้เพื่อจับทั้งข้อผิดพลาดของเซิร์ฟเวอร์ภายใน 500 และข้อยกเว้นที่ไม่ได้รับการยกเว้นดังนี้:
(let [traceback-handler (install-traceback-handler admin-email build-id)]
(web/setup-error-handler app template-app "main" component-error-page traceback-handler))
การโหลดสดใหม่ได้รับการสนับสนุนโดยใช้ทั้ง nbb และ shadow-cljs มันถูกเปิดใช้งานโดยค่าเริ่มต้นเมื่อใช้ NPM สร้างสคริปต์ ตัวอย่างมีรายละเอียดเพิ่มเติม
SiteFox ผลิตโดย Chris McCormick (@MCCRMX บน Twitter และ@[email protected] บน Mastodon) ฉันวนซ้ำในขณะที่สร้างเว็บไซต์สำหรับตัวเองและสำหรับลูกค้า