=Browser Incompatibilities: the Most Common Problem=
The most common problem for Web Site developers is the fact that every browser treats HTML Tags, CSS and Javascript in it's own way.
This Recipe tries to address one of the problem I faced the most: having a slightly different CSS for every Browser.
=The Usual Solution=
The usual solution is to load every time a different CSS depending on the Browser. But this solution has some side effects:
- It's difficult to Maintain: very hard because every update to the style needs to be applied to multiple files
- It's Boring: for the above reason
- It's Not Cool: again, for the same above reason
=But, we have Templates on AppEngine=
The title should already suggest what's my idea.
Use the power of the Django Template engine to render "slightly different" Stylesheets based on the Browser (or, better, on it's "User-Agent").
=My Solution=
The solution is composed of:
1) A utility function that parses the "User-Agent" and return the Browser Name and the Version
...
def getBrowserDetails(userAgent):
result = {}
# Regexp for most used "User-Agent"s built using "http://www.useragentstring.com/"
browsersRegexps = [
['Internet Explorer', 'MSIE (\S+)'],
['Opera', 'Opera[ /](\S+)'],
['Konqueror', 'KHTML/(\S+)'],
['Firefox', 'Gecko/\S+ Firefox/(\S+)'],
['Chrome','AppleWebKit/\S+ (KHTML, like Gecko) Chrome/(\S+) Safari/\S+'],
['Safari', 'Version/\S+ Safari/(\S+)'],
['Mozilla', 'Gecko/(\S+)'],
['WebKit', 'AppleWebKit/(\S+)']
]
# Search for the Browser that matches, if any
for browser in browsersRegexps:
compiledRegexp = re.compile( browser[1] )
searchResult = compiledRegexp.search( userAgent )
if (searchResult):
result['browser_name'] = browser[0]
result['browser_version'] = searchResult.group(1)
return result
# If nothing matches, return "unknown"
result['browser_name'] = "unknown"
result['browser_version'] = "unknown"
return result
...
2) A Python Script that contains the right URL-Handler mapping and RequestHandler to do the job. Mine is named "styler.py":
import wsgiref.handlers
import os
import re
from google.appengine.ext.webapp import template
from google.appengine.ext import webapp
from google.appengine.api import memcache
import utils
class CssStyleHandler( webapp.RequestHandler ):
def get( self, requestedCssFilename ):
# Retrieving 'browser_name' and 'browser_version'
# First, check into the MemCache
browserDetailsDic = memcache.get(self.request.headers['User-Agent'])
if browserDetailsDic is None:
# If it's not found in MemCache, calculate it...
browserDetailsDic = utils.getBrowserDetails(self.request.headers['User-Agent'])
# ... and Store it in MemCache for the Future
memcache.add(self.request.headers['User-Agent'], browserDetailsDic)
# TODO: A good idea would be to cache the Stylesheet Rendering too
# Loading the Template
templatePath = os.path.join( os.path.dirname( __file__ ), 'style/' + requestedCssFilename )
if ( os.path.exists(templatePath) ):
# Setting Content-Type as "text/css"
self.response.headers['Content-Type'] = 'text/css'
# Rendering the Result
self.response.out.write( template.render( templatePath, browserDetailsDic ) )
def main():
# Creating a WSGI Application Handler
application = webapp.WSGIApplication([
( '/style/([\w/]+\.css)$', CssStyleHandler )
], debug=True )
# Executing the WSGI Application
wsgiref.handlers.CGIHandler().run( application )
if __name__ == "__main__":
main()
3) Last but not least important, the right URL-Script mapping for the "app.yaml" file:
...
handlers:
- url: /style/.*
script: styler.py
...
=Usage of MemCache=
One of the great things of AppEngine is the fact that the developer has access to the MemCache engine. Here I use it to cache the results of the "User-Agent" parsing, so it will be done only "once per browser kind". This means that the result will be shared across both User Sessions and Different Users.
All, with just a couple of lines more. Cool, isn't it? ;-)
=Usage Example=
A very simple example of where this can be useful? Mmm, here is a simple one. It's about the CSS command "opacity":
.opaque {
{% ifequal browser_name "Internet Explorer" %}
filter: alpha(opacity=70);
{% else %}
opacity: 0.7;
{% endifequal %}
}
I know: as usual, the most of the time, is Internet Explorer the problem. But, so far in this recipe, I tried to be Politically Correct :-).
=Possible Evolutions/Enhancements=
Of course, this "dish" is great, but there is always space for "improvements" in this field, isn't it?
A cool idea could be to MemCache the rendered Stylesheets, using the CSS filename and the Browser Informations as Key.
Another one, to add a CSS Compressor after the Stylesheet is rendered, lowering the load time.
Please, Help Yourself!
That allows you to target JS, CSS, or whatever else you like at IE in a version specific manner. They're fully standards-compliant and validating (all other browsers just see them as comments).
End result is that you write one stylesheet aimed at the standard, then add a second one for IE lte 7, or IE lt 7 or IE gt 8 or whatever it is you want to override. I usually end up with about 5 lines in an IE-7 specific sheet (assuming semmantic markup - it's often hundreds of lines if I have to deal with bad HTML) and everything's good.
You can use the same for JS. For example, use this to point IE7.js at only IE lt 7. It upgrades IE from 5.5 to 6 to operate almost exactly like 7, meaning that your 5 line IE7 override CSS takes care of it all.