Po ostatnim porównaniu Pythona, Ruby i Golanga dla aplikacji wiersza poleceń, zdecydowałem się użyć tego samego wzorca do porównania budowania prostej usługi internetowej. Do tego porównania wybrałem Flask (Python), Sinatra (Ruby) i Martini (Golang). Tak, istnieje wiele innych opcji bibliotek aplikacji internetowych w każdym języku, ale uważam, że te trzy dobrze nadają się do porównania.
Przeglądy bibliotek
Oto wysokopoziomowe porównanie bibliotek autorstwa Stackshare.
Kolba (Python)
Flask to mikro-framework dla Pythona oparty na Werkzeug, Jinja2 i dobrych intencjach.
W przypadku bardzo prostych aplikacji, takich jak ta pokazana w tym demo, Flask jest doskonałym wyborem. Podstawowa aplikacja Flask to tylko 7 linii kodu (LOC) w jednym pliku źródłowym Pythona. Przewaga Flaska nad innymi bibliotekami internetowymi Pythona (takimi jak Django lub Pyramid) polega na tym, że możesz zacząć od małych rzeczy i w razie potrzeby zbudować bardziej złożoną aplikację.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
Sinatra (Rubin)
Sinatra to DSL do szybkiego tworzenia aplikacji internetowych w Ruby przy minimalnym wysiłku.
Podobnie jak Flask, Sinatra świetnie nadaje się do prostych zastosowań. Podstawowa aplikacja Sinatra to tylko 4 LOC w jednym pliku źródłowym Ruby. Sinatra jest używana zamiast bibliotek, takich jak Ruby on Rails, z tego samego powodu, co Flask - możesz zacząć od małych i rozszerzać aplikację w razie potrzeby.
require 'sinatra'
get '/hi' do
"Hello World!"
end
Martini (Golang)
Martini to potężny pakiet do szybkiego pisania modularnych aplikacji/usług internetowych w Golang.
Martini jest dostarczany z kilkoma bateriami więcej niż Sinatra i Flask, ale nadal jest bardzo lekki na początek - tylko 9 LOC dla podstawowego zastosowania. Martini spotkał się z pewną krytyką społeczności Golang, ale nadal ma jeden z najwyżej ocenianych projektów Github spośród wszystkich frameworków internetowych Golang. Autor Martini odpowiedział tutaj bezpośrednio na krytykę. Niektóre inne frameworki obejmują Revel, Gin, a nawet wbudowaną bibliotekę net/http.
package main
import "github.com/go-martini/martini"
func main() {
m := martini.Classic()
m.Get("/", func() string {
return "Hello world!"
})
m.Run()
}
Pozbywając się podstaw, zbudujmy aplikację!
Opis usługi
Stworzony serwis udostępnia bardzo podstawową aplikację blogową. Powstają następujące trasy:
GET /
:Zwróć bloga (używając szablonu do renderowania).GET /json
:Zwróć zawartość bloga w formacie JSON.POST /new
:Dodaj nowy post (tytuł, streszczenie, treść) do bloga.
Zewnętrzny interfejs do usługi blogów jest dokładnie taki sam dla każdego języka. Dla uproszczenia MongoDB będzie używany jako magazyn danych dla tego przykładu, ponieważ jest najprostszy w konfiguracji i nie musimy się w ogóle martwić o schematy. W normalnej aplikacji podobnej do bloga prawdopodobnie potrzebna byłaby relacyjna baza danych.
Dodaj post
POST /new
$ curl --form title='Test Post 1' \
--form summary='The First Test Post' \
--form content='Lorem ipsum dolor sit amet, consectetur ...' \
http://[IP]:[PORT]/new
Wyświetl HTML
GET /
Wyświetl plik JSON
GET /json
[
{
content:"Lorem ipsum dolor sit amet, consectetur ...",
title:"Test Post 1",
_id:{
$oid:"558329927315660001550970"
},
summary:"The First Test Post"
}
]
Struktura aplikacji
Każdą aplikację można podzielić na następujące komponenty:
Konfiguracja aplikacji
- Zainicjuj aplikację
- Uruchom aplikację
Prośba
- Zdefiniuj trasy, na których użytkownik może żądać danych (GET)
- Zdefiniuj trasy, na których użytkownik może przesyłać dane (POST)
Odpowiedź
- Renderuj JSON (
GET /json
) - Wyrenderuj szablon (
GET /
)
Baza danych
- Zainicjuj połączenie
- Wstaw dane
- Pobierz dane
Wdrażanie aplikacji
- Doker!
Pozostała część tego artykułu porówna każdy z tych składników dla każdej biblioteki. Celem nie jest sugerowanie, że jedna z tych bibliotek jest lepsza od drugiej – ma to na celu zapewnienie konkretnego porównania między trzema narzędziami:
- Płytka (Python)
- Sinatra (Rubin)
- Martini (Golang)
Konfiguracja projektu
Wszystkie projekty są ładowane za pomocą docker i docker-compose. Zanim zagłębimy się w to, jak każda aplikacja jest ładowana pod maską, możemy po prostu użyć dockera, aby uruchomić każdą z nich w dokładnie ten sam sposób - docker-compose up
Poważnie, to wszystko! Teraz dla każdej aplikacji istnieje Dockerfile
i docker-compose.yml
plik, który określa, co się stanie po uruchomieniu powyższego polecenia.
Python (kolba) – Plik Dockera
FROM python:3.4
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
Ten Dockerfile
mówi, że zaczynamy od podstawowego obrazu z zainstalowanym Pythonem 3.4, dodając naszą aplikację do /app
katalogu i za pomocą pip zainstalować nasze wymagania aplikacji określone w requirements.txt
.
Rubin (Sinatra)
FROM ruby:2.2
ADD . /app
WORKDIR /app
RUN bundle install
Ten Dockerfile
mówi, że zaczynamy od podstawowego obrazu z zainstalowanym Ruby 2.2, dodając naszą aplikację do /app
i za pomocą bundlera zainstalować nasze wymagania aplikacji określone w pliku Gemfile
.
Golang (martini)
FROM golang:1.3
ADD . /go/src/github.com/kpurdon/go-blog
WORKDIR /go/src/github.com/kpurdon/go-blog
RUN go get github.com/go-martini/martini && \
go get github.com/martini-contrib/render && \
go get gopkg.in/mgo.v2 && \
go get github.com/martini-contrib/binding
Ten Dockerfile
mówi, że zaczynamy od podstawowego obrazu z zainstalowanym Golangiem 1.3, dodając naszą aplikację do /go/src/github.com/kpurdon/go-blog
katalogu i pobranie wszystkich niezbędnych zależności za pomocą go get
polecenie.
Zainicjuj/uruchom aplikację
Python (kolba) – app.py
# initialize application
from flask import Flask
app = Flask(__name__)
# run application
if __name__ == '__main__':
app.run(host='0.0.0.0')
$ python app.py
Ruby (Sinatra) – app.rb
# initialize application
require 'sinatra'
$ ruby app.rb
Golang (Martini) – app.go
// initialize application
package main
import "github.com/go-martini/martini"
import "github.com/martini-contrib/render"
func main() {
app := martini.Classic()
app.Use(render.Renderer())
// run application
app.Run()
}
$ go run app.go
Zdefiniuj trasę (GET/POST)
Python (kolba)
# get
@app.route('/') # the default is GET only
def blog():
# ...
#post
@app.route('/new', methods=['POST'])
def new():
# ...
Rubin (Sinatra)
# get
get '/' do
# ...
end
# post
post '/new' do
# ...
end
Golang (Martini)
// define data struct
type Post struct {
Title string `form:"title" json:"title"`
Summary string `form:"summary" json:"summary"`
Content string `form:"content" json:"content"`
}
// get
app.Get("/", func(r render.Render) {
// ...
}
// post
import "github.com/martini-contrib/binding"
app.Post("/new", binding.Bind(Post{}), func(r render.Render, post Post) {
// ...
}
Wyrenderuj odpowiedź JSON
Python (kolba)
Flask udostępnia metodę jsonify(), ale ponieważ usługa korzysta z MongoDB, używane jest narzędzie mongodb bson.
from bson.json_util import dumps
return dumps(posts) # posts is a list of dicts [{}, {}]
Rubin (Sinatra)
require 'json'
content_type :json
posts.to_json # posts is an array (from mongodb)
Golang (Martini)
r.JSON(200, posts) // posts is an array of Post{} structs
Wyrenderuj odpowiedź HTML (szablon)
Python (kolba)
return render_template('blog.html', posts=posts)
<!doctype HTML>
<html>
<head>
<title>Python Flask Example</title>
</head>
<body>
{% for post in posts %}
<h1> {{ post.title }} </h1>
<h3> {{ post.summary }} </h3>
<p> {{ post.content }} </p>
<hr>
{% endfor %}
</body>
</html>
Rubin (Sinatra)
erb :blog
<!doctype HTML>
<html>
<head>
<title>Ruby Sinatra Example</title>
</head>
<body>
<% @posts.each do |post| %>
<h1><%= post['title'] %></h1>
<h3><%= post['summary'] %></h3>
<p><%= post['content'] %></p>
<hr>
<% end %>
</body>
</html>
Golang (Martini)
r.HTML(200, "blog", posts)
<!doctype HTML>
<html>
<head>
<title>Golang Martini Example</title>
</head>
<body>
{{range . }}
<h1>{{.Title}}</h1>
<h3>{{.Summary}}</h3>
<p>{{.Content}}</p>
<hr>
{{ end }}
</body>
</html>
Połączenie z bazą danych
Wszystkie aplikacje używają sterownika mongodb specyficznego dla danego języka. Zmienna środowiskowa DB_PORT_27017_TCP_ADDR
to adres IP połączonego kontenera dockera (adres IP bazy danych).
Python (kolba)
from pymongo import MongoClient
client = MongoClient(os.environ['DB_PORT_27017_TCP_ADDR'], 27017)
db = client.blog
Rubin (Sinatra)
require 'mongo'
db_ip = [ENV['DB_PORT_27017_TCP_ADDR']]
client = Mongo::Client.new(db_ip, database: 'blog')
Golang (Martini)
import "gopkg.in/mgo.v2"
session, _ := mgo.Dial(os.Getenv("DB_PORT_27017_TCP_ADDR"))
db := session.DB("blog")
defer session.Close()
Wstaw dane z POST
Python (kolba)
from flask import request
post = {
'title': request.form['title'],
'summary': request.form['summary'],
'content': request.form['content']
}
db.blog.insert_one(post)
Rubin (Sinatra)
client[:posts].insert_one(params) # params is a hash generated by sinatra
Golang (Martini)
db.C("posts").Insert(post) // post is an instance of the Post{} struct
Pobierz dane
Python (kolba)
posts = db.blog.find()
Rubin (Sinatra)
@posts = client[:posts].find.to_a
Golang (Martini)
var posts []Post
db.C("posts").Find(nil).All(&posts)
Wdrażanie aplikacji (Docker!)
Świetnym rozwiązaniem do wdrażania wszystkich tych aplikacji jest użycie docker i docker-compose.
Python (kolba)
Plik Dockera
FROM python:3.4
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
docker-compose.yml
web:
build: .
command: python -u app.py
ports:
- "5000:5000"
volumes:
- .:/app
links:
- db
db:
image: mongo:3.0.4
command: mongod --quiet --logpath=/dev/null
Rubin (Sinatra)
Plik Dockera
FROM ruby:2.2
ADD . /app
WORKDIR /app
RUN bundle install
docker-compose.yml
web:
build: .
command: bundle exec ruby app.rb
ports:
- "4567:4567"
volumes:
- .:/app
links:
- db
db:
image: mongo:3.0.4
command: mongod --quiet --logpath=/dev/null
Golang (Martini)
Plik Dockera
FROM golang:1.3
ADD . /go/src/github.com/kpurdon/go-todo
WORKDIR /go/src/github.com/kpurdon/go-todo
RUN go get github.com/go-martini/martini && go get github.com/martini-contrib/render && go get gopkg.in/mgo.v2 && go get github.com/martini-contrib/binding
docker-compose.yml
web:
build: .
command: go run app.go
ports:
- "3000:3000"
volumes: # look into volumes v. "ADD"
- .:/go/src/github.com/kpurdon/go-todo
links:
- db
db:
image: mongo:3.0.4
command: mongod --quiet --logpath=/dev/null
Wniosek
Na zakończenie przyjrzyjmy się, jak sądzę, kilku kategoriom, w których prezentowane biblioteki oddzielają się od siebie.
Prostota
Chociaż Flask jest bardzo lekki i wyraźnie się czyta, aplikacja Sinatra jest najprostszą z trzech w 23 LOC (w porównaniu z 46 dla Flask i 42 dla Martini). Z tych powodów Sinatra jest zwycięzcą w tej kategorii. Należy jednak zauważyć, że prostota Sinatry wynika z bardziej domyślnej „magii” – np. niejawnej pracy, która dzieje się za kulisami. Dla nowych użytkowników może to często prowadzić do zamieszania.
Oto konkretny przykład „magii” w Sinatrze:
params # the "request.form" logic in python is done "magically" behind the scenes in Sinatra.
I równoważny kod Flask:
from flask import request
params = {
'title': request.form['title'],
'summary': request.form['summary'],
'content': request.form['content']
}
Dla początkujących programistów Flask i Sinatra są z pewnością prostsze, ale dla doświadczonego programisty z czasem spędzonym w innych statycznie typowanych językach Martini zapewnia dość uproszczony interfejs.
Dokumentacja
Dokumentacja Flaska była najłatwiejsza do przeszukania i najbardziej przystępna. Chociaż Sinatra i Martini są dobrze udokumentowane, sama dokumentacja nie była tak przystępna. Z tego powodu Flask jest zwycięzcą w tej kategorii.
Społeczność
Flask jest zwycięzcą w tej kategorii. Społeczność Ruby jest najczęściej dogmatyczna, że Railsy są jedynym dobrym wyborem, jeśli potrzebujesz czegoś więcej niż podstawowej usługi (mimo że Padrino oferuje to oprócz Sinatry). Społeczność Golang wciąż nie jest w pobliżu konsensusu co do jednego (lub nawet kilku) frameworków internetowych, czego można się spodziewać, ponieważ sam język jest tak młody. Python jednak przyjął wiele podejść do tworzenia stron internetowych, w tym Django dla gotowych, w pełni funkcjonalnych aplikacji internetowych oraz Flask, Bottle, CheryPy i Tornado dla podejścia mikro-frameworkowego.
Ostateczne określenie
Zauważ, że celem tego artykułu nie było promowanie jednego narzędzia, a raczej zapewnienie obiektywnego porównania Flask, Sinatry i Martini. Powiedziawszy to, wybrałbym Flask (Python) lub Sinatra (Ruby). Jeśli pochodzisz z języka takiego jak C lub Java, być może statycznie wpisana natura Golanga może ci się spodobać. Jeśli jesteś początkującym, Flask może być najlepszym wyborem, ponieważ jest bardzo łatwy do uruchomienia i ma bardzo mało domyślnej „magii”. Zalecam, abyś był elastyczny w swoich decyzjach przy wyborze biblioteki do swojego projektu.
Pytania? Informacja zwrotna? Proszę o komentarz poniżej. Dziękuję!
Daj nam też znać, jeśli chciałbyś zobaczyć niektóre testy porównawcze.