Hope you have read my previous article on testing redis on heroku & App engine .
[wpi_designer_button text=’Download’ link=’https://github.com/arjunsk/go_url_shortener’ style_id=’48’ icon=’github’ target=’_blank’]
OR
[wpi_designer_button text=’Preview’ link=’https://shortify-go.herokuapp.com/’ style_id=’48’ icon=’cloud’ target=’_blank’]
Features Included:
1. Used online redis provided by redislabs
2. Proper folder structure ( I believe so)
3. Shows notifications in go
4. Added support for clipboard.js
I hope you have covered the basics of golang. Infact this youtube channel is highly recommended.
Get Started:
Directory structure: I wanted to in-cooperate MVC architecture. I didn’t use Go Web Frameworks as i wanted to learn the in and outs of the project. I hope this project structure is correct. ( Correct me if I am wrong)
The app has got 3 pages, namely – app, about, 404
MVC can be identified as
Model :- They are kind of data structures that deal with the database. It includes the db functions.
View :- They are those UI elements that are shown on screen.
Controllers :- They are related the functions specific to a page.
Pretty much similar to Ionic right ?
Public :- It is where you place all the public files accessible to viewers.
1. Views
We can template common layout attributes and include them in every page.
header.html
{{define "header"}} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{{.Title}}</title> <!--Note this part --> <link href="/public/css/main.css" type="text/css" rel="stylesheet"> ... </head> <body> {{end}}
footer.html
{{define "footer"}} /public/js/jquery.js /public/js/clipboard.min.js /public/js/notify.min.js /public/js/main.js </body> </html> {{end}}
navigation.html
{{define "navigation"}} <div id="nav"> <ul> <li> <a href="/about">About</a> </li> <li> <a href="/">App</a> </li> </ul> </div> {{end}}
app.html
{{template "header" .}} <!-- By the . we are passing page data to the header.html also --> {{template "navigation" .}} <div class="wrap"> .... <form action="" class="container" method="POST"> <input value="{{.Long_url}}" name="long_url" class="urlTerm" type="text" placeholder="Long URL, eg :- http://www.google.com/... "> .... </form> <div class="container"> <input id="shortenURL" readonly type="text" class="copyTerm" value="{{.Short_url}}" placeholder="Short URL" /> <!--data-clipboard-target="#shortenURL" will be used by clipboard.js --> <button data-clipboard-target="#shortenURL" class="copyButton"> <i class="fa fa-clipboard" aria-hidden="true"></i> </button> </div> </div> {{template "footer" .}}
2. public / js / main.js
var clipboard = new Clipboard('.copyButton'); // initializing Clipboard object //Handles on success of copy function clipboard.on('success', function(e) { showInfoMessage("Copied!"); }); // Notification is handled using jQuery // These functions will be called from go. Similar to echo "<scripts>...</scripts>" in PHP function showSuccessMessage(message){ $.notify(message, "success"); } function showInfoMessage(message){ $.notify(message, "info"); } function showWarningMessage(message){ $.notify(message,"warn"); } function showErrorMessage(message) { $.notify(message, "error"); }
3. Server.go
package main import( "html/template" "net/http" "os" "go_shortify_web_app_heroku/models" "go_shortify_web_app_heroku/controllers" ) // This is passed to every page to set the page details type pageData struct { Title string Short_url string Long_url string } var tpl *template.Template var page_data pageData var host_name string = "https://app_id.herokuapp.com" // used for showing notification popup using js var notify_type int var notify_msg string func init() { tpl = template.Must(template.ParseGlob("views/*.html")) // Parses only html files from view folder models.Redis_db_init() // initialise the db connection } func main(){ // This is core Handler that server all the files in the public folder with appropraite content type. // Else for eg :- png will be loaded as document type, and hinder page load http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("public")))) //Handlers to handle url regex http.HandleFunc("/about",AboutHandler) http.HandleFunc("/404",ErrorHandler) http.HandleFunc("/", HomeHandler) // We are lisening to port that is set in the heroku environment using os.Getenv http.ListenAndServe(":"+os.Getenv("PORT"), nil) } func ErrorHandler(w http.ResponseWriter, r *http.Request) { page_data = pageData{Title:"404"} // setting page title tpl.ExecuteTemplate(w, "error.html",page_data) // opens error.html page } func AboutHandler(w http.ResponseWriter, r *http.Request) { page_data = pageData{Title:"About"} tpl.ExecuteTemplate(w, "about.html",page_data) } func HomeHandler(w http.ResponseWriter, r *http.Request) { // for eg:- app.heroku.com/1234 is the URL //then shortCode = 1234 ( we are excluding the beginging / by using [1:] ) shortCode := r.URL.Path[1:] // The variables are static. So we need to re/initialize it every time page_data = pageData{Title:"Shortify",Short_url:"",Long_url:""} notify_type = 0 // notify-off : read the controllers.ShowNotifications if len(shortCode) != 0 { // meaning we have a shortcode to check in the database if err != nil { redirect_url = host_name + "/404" } controllers.RedirectTo(w,r,redirect_url) // redirect to long url return }else if r.Method == "POST" { // Handles post data. If post url is correct, then save it to db long_url := r.PostFormValue("long_url") //get form data by html form id err := controllers.ValidateURL(long_url)// validate url if err != nil { notify_type,notify_msg = 4, "Invalid URL." // we can set two variables in one line }else{ short_url := host_name + "/" + models.Redis_db_save(long_url) page_data = pageData{Title:"Shortify", Short_url:short_url ,Long_url:long_url} // setting page data notify_type,notify_msg = 1, "URL shortified." } } tpl.ExecuteTemplate(w, "app.html",page_data) // page datas are set in view/.html at appropraite places controllers.ShowNotifications(w,notify_type,notify_msg) // run this after loading the page }
4. models / redis_db.go
//Models are data structures for representing database concepts. package models import ( "github.com/garyburd/redigo/redis" "fmt" "go_shortify_web_app_heroku/controllers" ) var redisPool *redis.Pool // creating redis pool enables us to reuse redigo connections // http://stackoverflow.com/questions/24387350/re-using-redigo-connection-instead-of-recreating-it-every-time func Redis_db_init(){ redisAddr := "redis-XXXXX.c9.us-east-1-2.ec2.cloud.redislabs.com:XXXXX" redisPool = &redis.Pool{ Dial: func() (redis.Conn, error) { conn, err := redis.Dial("tcp", redisAddr) return conn, err }, } } func Redis_db_save(long_url string) (string) { // Reusing redisConn redisConn := redisPool.Get() defer redisConn.Close() new_short_code := controllers.Hash(long_url) // Hash the long url , ie number code redisConn.Do("SET", new_short_code, long_url) // Save the has along with long url into db return fmt.Sprint(new_short_code) // .Sprint => String print converts long to string } func Redis_db_get(shortCode string) (string,error) { redisConn := redisPool.Get() defer redisConn.Close() redirect_url, err := redis.String(redisConn.Do("GET", shortCode)) return redirect_url,err }
5. controllers / app.go
package controllers import ( "hash/fnv" "net/http" "io" "fmt" "net/url" ) // Hash the long url into shortcode func Hash(s string) uint32 { h := fnv.New32a() h.Write([]byte(s)) return h.Sum32() } // Use statusFound. It mainly deal with setting HTTP response data func RedirectTo(w http.ResponseWriter, r *http.Request, urlStr string){ http.Redirect(w, r, urlStr, http.StatusFound) } // Checks if it is a valid url, ie http://google.com func ValidateURL(long_url string) (error){ _,err := url.ParseRequestURI(long_url) return err } // Setting notify_script based on notify_type // no-notify = 0 which is default, that means exit the function . func ShowNotifications(w io.Writer,notify_type int,msg string) { var notify_script string = "" switch notify_type { case 1: notify_script = "<script>showSuccessMessage(\""+msg+"\")</script>" case 2: notify_script = "<script>showInfoMessage(\""+msg+"\")</script>" case 3: notify_script = "<script>showWarningMessage(\""+msg+"\")</script>" case 4: notify_script = "<script>showErrorMessage(\""+msg+"\")</script>" default: return } fmt.Fprint(w,notify_script) // Fprint => File Print ie it writes this into the html files, once loaded. }
I hope i explained something valuable with this tutorial. If i am wrong, do correct me.
To deploy this app on heroku or app engine, follow my previous article.
Happy coding.