Monthly Archives: November 2015

Email Render For different clients

In fact, there is no difference between web front-end development and email development. But if we want to compatible with all email clients, the thing becomes a trouble. Here I list some notes to make the thing a little easier.

  • Tool: litmus
    1. Now more and more developers use Mac as the main laptop to work. If we don’t have all devices, how can we know the final render result before we send out? Here is an online website: https://litmus.com You can use it free feature to get render result on Outlook, iphone6, and Gmail. For me, it is enough. Because most of the problems are coming from Outlook. If I can fix Outlook issue, others are normally ok.
  •  position
    • we’d better use a table to design the whole layout. Note: here the width doesn’t have px .
    1. <table border="0" cellpadding="0" cellspacing="0" width="580" style="background-color: #0290ba;">
  • background image
    • For normal case, we can use the background to do.
    • <td width="100" height="100" style="background: url('someurl');">text</td>
      
      <td background="@bgImage" style="background-size: cover;background-position:50% 50%" bgcolor="#7bceeb" valign="middle">
    • For Outlook, we’d use a different one. This method is good, but if we want to achieve “background-size: cover” effect, we need to add “aspect” to control its resize ratio. (Please note: here the size we use the width’s same size. Normally we should select the larger one between  width and height. ) If not, it will resize the whole original image, it is like “background-size: contain”. Depends on which is your requirement, you need to know exactly which “background-size” you need.
    • <!--[if gte mso 9]>
      <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:620px;height:190px;">
      <v:fill type="tile" src="@bgImage" color="#7bceeb" aspect="atmost" size="620pt 620pt" /> 
      <v:textbox inset="0,0,0,0"> 
      <![endif]-->
    • The method above will create a background, but its size is not scalable. If we want a scalable background, we’d better use following method. And your content is between the two codes. So please note: The first method we mentioned above doesn’t need to put content between codes, you just need to put above code before your td or table and then the background will be applied to your next content. But the second method here we talk needs to put your content between the two codes snip.
    • <!--[if gte mso 9]>
      <img src="@bgImage" height="190" width="620" border="0" style="display: block;" />
      <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="false" stroke="false" style="position:relative;top:0;left:0;width:620px; height: 190px;">
      <v:textbox inset="0,0,0,0">
      <![endif]-->
    • <!--[if gte mso 9]>
      </v:textbox>
      </v:rect>
      <![endif]-->
    • Until now, the background is ok. But if we want to put overlay above the background, we need to a little adjust the second method. (This case is very important for user experience, because sometimes, if we just put content purely over the background, the background color will impact the content’s final effect. We’d better put a dark overlay over the background. In fact, the dark overlay is between the background and the content)
    • <!--[if gte mso 9]>
      <img src="@bgImage" alt="replace" height="190" width="620" border="0" style="display: block;" />
      <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" fillcolor="black" stroke="false" style="position:relative;top:0;left:0;width:620px; height: 190px;">
      <v:fill opacity="0.5" />
      <v:textbox inset="0,0,0,0">
      <![endif]-->
    • <!--[if gte mso 9]>
      </v:fill>
      </v:textbox>
      </v:rect>
      <![endif]-->
    • Above method use img to solve  background problem, but its scale image is for the entire image. If you want background-position and background-size applying on the image, you need to use the following method.
    • <!--[if gte mso 9]>
      <v:rect xmlns:v="urn:schemas-microsoft-com:vml" stroke="false" style="width:620px;height:190px; background-repeat:no-repeat; background-position:top center;">
      <v:fill type="frame" color="#181907" opacity="0.49" src="@bgImage" />
      <v:textbox inset="0,0,0,0">
      <![endif]-->
    • <!--[if gte mso 9]>
      </v:textbox></v:fill>
      </v:rect>
      <![endif]-->
  • Margin or padding
    • For Hotmail, it still supports padding-left, padding-right, and padding-bottom. But for Outlook, it doesn’t support any. Here I use a trick to solve it. As I say, it is a trick, so it can’t solve it totally. I add empty space to be as a padding or margin.
    • <!--[if gte mso 9]>
      <tr><td width="620" height="50" align="center" valign="middle"></td></tr>
      <![endif]-->
      
    • Using a negative margin-top on a div inside a td will not have any effect unless the div has any other preceding element neighbors. If you had, for example, an h1 before your div inside the td, you could use margin-top: -10px; on the div to move the div closer to the h1 visually.
  • Float
    • For normal case, we can use this to float text.
    • style="float: right"
    • But for Outlook, we’d better use align to do it.
    • <td width="620" align="center">
      <span style="font-size: 40px; font-weight: bold; text-transform: uppercase; color: #fff;"> @bodyTopic </span>
      </td>
  • display
    • ‘display:none’ is not supported in Outlook. Here is the fixed solution as below.
    • style="display: none; max-height: 0px; font-size: 0px; overflow: hidden; mso-hide: all"
  • border-radius
    • Outlook doesn’t support rounded button, what you can do is to use image as background to implement this. I know it is totally trick but there is no another way to solve it.

 

Play Framework (1) – WebSocket

Even though Play uses async method to communicate with front-end and back-end. But sometimes, we still need to call third-party restful api to do something else. Especially, third-party restful api takes too long time to return data. In this case, it is wasteful to wait there and always ask whether it is ok or not. Good way to handle it is to make use of third-party callback and web socket to deal with this case.

So it is very clear that we have three sides: one is third-party api, the other is our back-end, another is our front-end.

What we need to do is that front-end builds a WebSocket with back-end. And then back-end requests data from third-party api. When third-party api returns data,  back-end sends data back to front-end.

  1. Build a web socket between front-end and back-end.
  2. front-end sends data to back-end
  3. back-end calls third-party api
  4. third-party api returns data back to back-end
  5. back-end returns data back to front-end by web socket

So, there are two routes, one is for web socket, another is for callback. The connection between the two is to store actorRef to keep the bridge. You also needs to ask callback to append special token back in order to figure out this actor.

  • Javascript code to send web socket to build connection between front-end and back-end.
  • var myWebSocket = new WebSocket("ws://<your_domain_name>/<your_ws_route>/");
    myWebSocket.onopen = function() { myWebSocket.send(<your_data>); }; 
    myWebSocket.onmessage = function(event) { alert(event.data); // to render data }; 
    myWebSocket.onclose = function() { console.log("wsconnect close."); };
  • Nginx configuration to build connection. Please note: Here if you don’t configure “

    proxy_read_timeout” attribute, your websocket default connection time is 60 seconds and then it is closed automatically. So here normally I set it as 1d.

  • location /daoApp/websocket/create/ {
      proxy_pass  http://<your_domain_name>;
      proxy_http_version 1.1;
      proxy_set_header Host $host;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_read_timeout 1d;
    }
  • Play routes to accept web socket request and another one is to accept callback request
  • POST    /<your_callback_route>/ controllers.WebSocketApp.apiCallback
    GET     /<your_ws_route>/       controllers.WebSocketApp.create
  • And next I have one global variable to store actorRef for each connect between front-end and back-end. For each web socket request, I will store unique key combining the actorRef and trigger call to third-party api. For each callback request, I will to find its mapping actorRef to send data back to front-end.
  • object WebSocketApp extends Controller {
      // remember to build a map to store your actorRef
      // here we use ConcurrentHashMap which is to avoid memory leak in multiple cores device
      var channels = new ConcurrentHashMap[String, ChannelMsg]().asScala
      def apiCallback = Action {
        request =>
          val requestBody = request.body.asFormUrlEncoded
          requestBody match {
            case Some(s) =>
              if (s.contains("parentID")) {
                val parentID = s("parentID").head
                val channelInfo = channels.get(parentID)
                channelInfo match {
                  case Some(info) =>
                    val yourParsedData = <deal_with_data_from_api>
                    actorInfo match {
                      case Some(actor) =>
                        actor ! <your_parse_data>
                        channels.remove(parentID)
                      case None =>
                    }
                  case None =>
                }
              }
            case None =>
          }
          Ok
      }
    
      def create = WebSocket.acceptWithActor[String, String] { request => out =>
        Props(new DAOWSCreateActor(out, request))
      }
    }
    class DAOWSCreateActor(out: ActorRef, request: RequestHeader) extends Actor {
      override def receive = {
        case topicName: String =>
          val queryTokenInfo = <your_send_request_to_third_party>
          // remember to involve your actorRef to channelMsg which will be used to parse actor 
          valchannelMsg = <construct_your_channel_msg>
          // remember to involve your key to callback on post which will be used to find actor back to send data back to front-end
          WebSocketApp.channels.put(<your_key>, channelMsg)
        case _ =>
      }
    }

Play Framework (10) – Cookie by Scala

Last time, we use cookie/session to help us to enhance web application’s security issue. This time, we talk about how to design normal cookie and use it to improve user experience. Our target is to avoid user’s trouble to login many times. User only needs to login once and come back in future without login. Cookie is also used to recognize user and obtain user information without boring user.

  1. Cookie Design

    1. Cookie is constructed by user name, ip, expired time and salt. Here I use constant string to combine them.
      Cookie = Encode(userName + IP + ExpiredTime + Salt)
    2. import scala.util.Random
      def createCookieString(userName: String, request: Request[Any]): String = {
       val expireTime = createExpireTime(<your_cookie_maxAge>)
       val ip = request.remoteAddress
       val salt = Random.alphanumeric.take(5).mkString
      
       val valueString = Seq(userName, ip, expireTime, salt).mkString(<your_constant_string>)
       val valueEncodedString = DAOBase64.encode(valueString)
       valueEncodedString
      }
    3. Create expired time
    4. def createExpireTime(maxAge: Int): String = {
        val now = new DateTime()
        val expireTime = now.plusDays(maxAge)
        expireTime.toString()
      }
    5. Cookie must be encoded, here is encode code
    6. import org.apache.commons.codec.binary.{ Base64 => ApacheBase64 }
      object DAOBase64 {
       def decode(encoded: String) = new String(ApacheBase64.decodeBase64(encoded.getBytes))
       def encode(decoded: String) = new String(ApacheBase64.encodeBase64(decoded.getBytes))
      }
    7. Parse your cookie
    8. def parseCookieString(input: String): CookieMessage = {
        val decodeCookie = DAOBase64.decode(input)
      
        val cookieStrings= decodeCookie.split(<your_constant_string>)
        val userName = cookieStrings(0)
        val ip = cookieStrings(1)
        val expireTime = cookieStrings(2)
      
        CookieMessage(input, userName, ip, expireTime)
      }
    9. case class CookieMessage(cookie: String, userName: String, ip: String, expireTime: String)
  2.  Return Cookie

    1. def buildCookie(cookieString: String): Cookie = {
        Cookie(Shared.cookieName, cookieString, maxAge = Some(<your_cookie_maxSec>))
      }
      Ok(userInfoTemp)
        .withSession("WWW-Authenticate" -> "user")
        .withCookies(buildCookie(cookieString))
  3. Clean up Cookie

    1. Ok.withNewSession
        .discardingCookies(DiscardingCookie(Shared.cookieName))