Database Solutions for Web Apps

I was working on the offline feature of WordsBaking last month. Considering the data needs to be stored offline may exceed the limit of DOM storage (usually 5 MB as I can find on other websites), I have to pick up a database to handle my needs.

Luckily, there are solutions for different platforms:

  • IndexedDB for Internet Explorer (and other browsers built in Android)
  • WebSQL for Safari (and other browsers built in Android)

But as WordsBaking runs on both Windows Phone and iOS, I need a wrap-up that adapts to those databases. Here’s what I found:

Lawnchair is an extremely lite wrapper with varieties of database adapters built-in, and basically it’s just switching adapters for us. It seems that several people have contributed to this project because the source file, though not big, is written in different code styles… And this is one of the reasons that why I didn’t choose it.

The second reason that made me give up Lawnchair is that it has only a global handler that handles failure events, which will make fallbacks much more tricky. However, it turned out that I did little fallbacks on database errors in the current version as the errors simply won’t occur in normal situations.

PouchDB as explained on its official site, is a project inspired by CouchDB, and . As I am new to NoSQL (maybe new to SQL too), I didn’t know CouchDB much before. But as I was considering using PouchDB as my local database solution, I dug a little deeper and found that it’s something (as sharing the same API of CouchDB) based on revision, and every update needs a revision as parameter and it’s the core of CouchDB.

Actually I even considered replacing current MongoDB with CouchDB as PouchDB is created for offline apps and syncs with CouchDB automatically, but later I realized that CouchDB doesn’t fit the needs of WordsBaking as it updates the records data all the time, which is not the strength of CouchDB.

So while the syncing doesn’t work for me because I have MongoDB on the server, and there’s a surplus “revision” parameter that needs me to handle, PouchDB is out of my little game.

And here comes the hero, YDN-DB. It seems to be a personal project but it’s full-featured and well documented, and it also provides customized code downloading as you may need only some features of this lib. BTW, I noticed it has syncing APIs, but I didn’t get much time to give it a try.

After integrated it with WordsBaking, everything seems to work fine. But as the records growing, initializing of WordsBaking became really slow. The main reason is that I need to read all the study records (maybe thousands) at the beginning. So I wrote a little class to handle this specific situation, to merge these records into a single one (TypeScript), and it works like a charm.

// Edit:
// The code below is written when I had just a little TypeScript and Promise experience.
// And it doesn't look very well right now.

class DBStore {
    name: string;
    keyPath: string;
    mergeLimit: number;

    private merging = false;
    private mergeChanged;

    private merges: { (): void; }[] = [];

    constructor(name: string, keyPath: string, mergeLimit: number) {
        this.name = name;
        this.keyPath = keyPath;
        this.mergeLimit = mergeLimit;
    }

    values = (callback: { (err, values?: any[], mergePendings?: string[]): void; }) => {
        db.values(this.name, null, 1000000)
            .then((values: any[]) => {
                db.get(this.name + '-single', 'values')
                    .then((item) => {
                        var mergePendings: string[] = [];
                        var hash: { [term: string]: boolean; } = {};

                        values.forEach((value) => {
                            var key = value[this.keyPath];
                            mergePendings.push(key);
                            hash[key] = true;
                        });

                        if (item) {
                            var svalues: any[] = item.data;

                            svalues.forEach((value) => {
                                var key = value[this.keyPath];
                                if (!hop.call(hash, key)) {
                                    values.push(value);
                                }
                            });
                        }

                        callback(null, values, mergePendings);
                    })
                    .fail((e) => {
                        callback(e);
                    });
            })
            .fail((e) => {
                callback(e);
            });
    };

    putOne = (value, callback: { (err): void; }) => {
        this.put([value], callback);
    };

    put = (values: any[], callback: { (err): void; }) => {
        if (values.length >= this.mergeLimit) {
            this.merge(values, callback);
            return;
        }

        if (this.merging) {
            values.forEach((value) => {
                var key = value[this.keyPath];
                this.mergeChanged[key] = true;
            });
        }

        db.put(this.name, values)
            .then(() => {
                callback(null);

                db.count(this.name)
                    .then((count) => {
                        if (count >= this.mergeLimit) {
                            this.merge();
                        }
                    })
                    .fail((e) => {
                        alert(e.message);
                    });
            }, (e) => {
                callback(e);
            })
            .fail((e) => {
                alert(e.message);
            });
    };

    merge = (newValues?: any[], callback: { (err): void; } = () => { }) => {

        var hop = Object.prototype.hasOwnProperty;

        var invokeCallback = (err) => {
            callback(err);

            this.merging = false;
            this.mergeChanged = undefined;

            var next = this.merges.shift();
            if (next) {
                next();
            }
        };

        var process = () => {
            this.merging = true;
            this.mergeChanged = {};

            this.values((err, values, mergePendings) => {
                if (err) {
                    callback(err);
                    return;
                }

                if (newValues) {
                    var hash: { [key: string]: boolean; } = {};

                    newValues.forEach((value) => {
                        var key = value[this.keyPath];
                        hash[key] = true;
                    });

                    values.forEach((value) => {
                        var key = value[this.keyPath];
                        if (!hop.call(hash, key)) {
                            newValues.push(value);
                        }
                    });

                    values = newValues;
                }

                db.put(this.name + '-single', {
                    id: 'values',
                    data: values
                })
                    .then(() => {
                        var promise;

                        var changed = Object.keys(this.mergeChanged);

                        if (changed.length) {
                            mergePendings = lodash.difference(mergePendings, changed);

                            if (mergePendings.length) {
                                var keys = [];
                                mergePendings.forEach((id) => {
                                    keys.push(new ydn.db.Key(this.name, id));
                                });

                                promise = db.remove(keys);
                            }
                        }
                        else {
                            promise = db.clear(this.name);
                        }

                        promise
                            .then(() => {
                                invokeCallback(null);
                            }, (e) => {
                                invokeCallback(e);
                            })
                            .fail((e) => {
                                alert(e.message);
                            });
                    })
                    .fail((e) => {
                        alert(e.message);
                    });
            });
        };

        if (this.merging) {
            this.merges.push(process);
        }
        else {
            process();
        }

    };

    clear = () => {
        db.clear([this.name, this.name + '-single']);
    };
}

Let's Again Talk About IE (Internet Explorer)

As an amateur but the same time professional front-end engineer, one of the most common things in my life is to press F5 in different browsers.

When this word was not “standardized” as it is now, I was used to pressing that button in several versions of IE, Chrome, Firefox, Safari, and, if I was in a really great mood, Opera. So I hadn’t suffer less than anyone. Especially from IE 6/7.

Now I basically test only in IE 8+ and Chrome. If the project is platform targeted, even better. There has been a lot of scoffs about IE  on compatibility and performance these years, which make me sort of sorry. People would always be fond of the new and tired of the old, even though it is the old one which brings the bright start to all these wonderful things.

IE 6 in my opinion is a great browser. It is out of date but It was well-designed and ignited many features in modern browsers. Actually, tons of newly added features in HTML5 or CSS3 can be imitated in IE 6, a browser which was released 12 years ago. And many earlier IE-only features have become the standards.

The earlier versions of IE is buggy and perform slow, but they perfectly served the days when it was released.

Well I am not trying to ask people not to give it up, but just hoping we can keep the respect, which this browser deserves, to it in its last days.

Microsoft seems to have realized their mistakes on detaining the developing of IE these years and brings us the new IE 9/10/11. Compatibility issues have become minor since version 9 (also it brings us fully GPU acceleration, which is a really important event for web apps), and since version 10, IE finally caught up the steps of modern browsers.

IE now is almost ready for web apps which could be as great as native apps, and I am looking forward to the booming of mobile web apps in the next several years. 🙂

Some Thoughts on Touch-friendly Mobile Web Apps

These days I am working on a project related to English study as my startup (though part-time), due to the limited resources, I chose a native browser based solution, So that we’ll save some time for building only the single main part of the app which will run on multiple devices.

However, the app I want to build has to be user-friendly, and for an HTML based app on a mobile device, which means, relatively poorer performance, to make the user experience acceptable forms a challenge. Also, as the UX we designed, there would be some touch gestures, how to manage these gestures is also a core issue to figure out.

Luckily, both of these two issues are not tricky.

There are some transition animations during page switching, elements removing, etc. I tried to use something like margin (CSS), but it doesn’t work that well, the animation doesn’t look smooth. So I made some searches, an article says CSS 3 transform has even slower performance comparing to CSS properties like margin and the position stuffs. I believed it… But it turned to be wrong. After I randomly changed the page switching from margin to CSS 3 transform + transition, the animation runs like a charm! I can’t say it’s as good as native apps, but it’s far from acceptable.

I am using Lumia 1020 with IE 10 Mobile myself, and get a Xiaomi 2S with its built-in browser and Chrome Mobile. These browsers all perform well, especially IE 10 and Chrome Mobile. Also, I tested it on an iPhone 5 of my friend, Safari Mobile does a great job, too. That’s actually not a surprise, I wrote some web games in 2010 runs on my iPod touch 3 32G using CSS 3 animations, even with some 3D stuffs, and the game performed well.

As for gestures management, I wrote two classes respectively called GestureIdentifier and GestureDelegate and so far they works well. The idea is really simple but it really makes things much easier, hoping telling this will help some starters learn about the power of ideas which is far above code itself.

And also some quick notes:

About “meta viewport”, seems that some phones have a minimum valid width of 360 instead of 320, so any value below 360 will not work… T-T gotta make some changes to fit this new value…

About click and “tap”. Seems that if these is a click event (especially on an “a” tag), these will be a semi-transparent rectangle when you “tap”. But sometimes, it doesn’t look well… Luckily “tap” based on touch event would be a solution… As I got the two gesture classes, I can simply define a new GestureIdentifier called “tap” and write a simple rule like the path of touch has only one point. Solved.

Wishing this tool will soon be available to everyone!

[Update]

The touch event trick for preventing tap highlight works most of the time, but on IE Mobile there are still some conditions on which the semi-transparent rectangle (tap highlight) will show up, so I continued searching for solutions. Luckily, there are way better and more official ones:

For IE, there’s a meta item named “msapplication-tap-highlight”, and simply setting the content to “no” solves the problem.

<meta name="msapplication-tap-highlight" content="no" />

For Webkit (Chrome and Safari), there’s a style named “-webkit-tap-highlight-color”, and “transparent” is the answer.

html { -webkit-tap-highlight-color: transparent; }

CSS3+PNG解决GIF动画无半透明的问题

因为GIF不支持半透明, 在使用时, 如果背景固定还好说, 如果背景是变化的, 那么透明的GIF图片边缘会有很明显的粗糙锯齿.

最近做JS游戏, 因为只针对设备开发的, iPhone和iPod里的Mobile Safari支持部分CSS3, 所以就想出了这个解决办法. 虽然比较小众, 但是对于我来说, 还是有价值的.

当然, 因为PNG图一般都偏大, 所以这个方案不适用于帧数很多的动画.

思路是很简单的, 通过改变背景的位置(背景定位)来实现.

首先我们需要把动画中出现的每一帧连成一个长条, 就像电影一样, 然后通过CSS3, 使它们逐帧显示出来. 例子如下(以Webkit为例).

@-webkit-keyframes ‘somename’ {
    0% { background-position: 0px; }
    49.9999% { background-position: 0px; }
    50% { background-position: -100px; }
    100% { background-position: -100px; }
}

#someid {
    background-image: url(somepic.png);
    -webkit-animation-name: somename;
    -webkit-animation-duration: 0.2s;
    -webkit-animation-iteration-count: infinite;
}

属性的意思我就不解释了, 英文, 很明了的. 因为CSS3动画本身貌似不是为逐帧的动画而生, 所以, 请注意49.9999%和50%这个百分比, 因为相隔很近, 可以让背景一次性从一个位置, 跳动到另一个位置, 这样以来, 就模拟了帧的变化.

如果想了解更多的CSS3动画技术, 请Google之. 其实我也不是很懂. 所以说不定会有更加直接的办法呢?

离线Web应用-HTML5 Cache Manifest试验

原文地址 http://www.bennadel.com/blog/1944-Experimenting-With-HTML5-s-Cache-Manifest-For-Offline-Web-Applications.htm
译者 Vilic Vane (www.vilic.info)

(译者注:第一次翻译东西,限于英文水平,不足之处请见谅.另外,原文作者使用的是ColdFusion,代码中有些内容并非浏览器最终获得的内容,请注意区别.然后空了会争取翻一下文件内的注释.)

之前我试过通过Safari的SQLite支持来创建客户端的数据库,觉得还应该试试通过HTML5的”Cache Manifest(缓存清单)”来创建离线的Web应用程序.Cache Manifest是一个列出了应用程序所需的所有需要被缓存的资源的文本文件,以便在离线的时候程序一样可以使用.在Cache Manifest中列出的文件会被储存在”Application Cache(应用程序缓存)”里.Application Cache就像浏览器普通缓存,但是却更加强大.只要资源被储存在了Application Cache里,除非相应缓存被清除或者Cache Manifest作废,浏览器永远不会再次从网络请求这个资源.

当我刚开始使用Cache Manifest的时候,我对”离线”方面的兴趣比不上对beasty(译者注:抱歉…没查到这个单词)缓存机制方面的兴趣.只要浏览器发现了Cache Manifest文件,并且得到了你对于它创建离线缓存的许可,浏览器接下来便会在后台开始下载所有需要缓存的资源.如此一来,你只需要点击主页(只是举个例子,不必真的这么做)就可以最终让整个应用程序被缓存下来.并且,一旦应用程序的资源被缓存,对于这个程序的操作,更多的便成了本地的操作,而不是远程的.

为了进行这个HTML5的Cache Manifest试验,我创建了一个非常简单的照片列表页面,并在其中列出了一些链接,以便于查看一些较高分辨率的图片.每一个”查看”页面都是会通过照片的ID动态加载相应图片的ColdFusion页面.列表页面的HTML内容非常简单:

<!---
Use an HTML5 doc-type. This is required for some browsers to
work properly with the offline-cache.
--->
<!DOCTYPE HTML>

<!---
Add a file to the cache manifest. NOTE: Any file that calls
a cache manifest file will be automatically cached, even if
it is in the WHITELIST portion of the cache manifest.
--->
<html manifest="./cache_manifest.cfm">
<head>
<title>Cache Manifest For Offline Pages</title>

<!---
These are the two graphics (logo and startup screen) use
for "app mode" on the iPhone (when the web page is put on
the main screen as its own logo).

NOTE: This part has nothing to do with the cache manifest
stuff - this is strictly related to runnin a website in
"App Mode" on an iPhone. Feel free to remove this entirely
from the example.
--->
<link rel="apple-touch-icon" href="icon.png"/>
<link rel="apple-touch-startup-image" href="home.png" />
</head>
<body>

<h1>
Cache Manifest For Offline Pages
</h1>

<h3>
Pictures of Alisha Morrow
</h3>

<ul>
<cfoutput>

<!---
Notice that we are dynamically generating this
page. It will only be generated on the first
request. After that, this page will be coming out
of the cache.
--->
<cfloop
index="pictureIndex"
from="1"
to="5"
step="1">

<li>
<a href="view.cfm?id=#pictureIndex#"
>View Image #pictureIndex#</a>
</li>

</cfloop>

</cfoutput>
</ul>

<p>
Now:
<cfoutput>
#timeFormat( now() , "hh:mm:ss TT" )#
</cfoutput>
</p>

</body>
</html>

当我们使用Cache Manifest的时候,有两个地方非常重要,首先,因为Cache Manifest是HTML5的一部分,我们需要使用HTML5 doctype:

<!DOCTYPE HTML>

虽然不是所有浏览器都要求这样做(我猜想的话),但为了跨浏览器的兼容性,这样做更好.然后,或许更加重要的东西是我们需要添加一个链接来指向Cache Manifest文件.这条链接是定义在HTML标签里的:

<html manifest="./cache_manifest.cfm">

cache_manifest.cfm文件就是列出了应用程序中所有需要缓存的资源的文件:

Cache Manifest (cache_manifest.cfm)

<!---
Define the Cache Manifest content. I'm doing it this way since
the "CACHE MANIFEST" line needs to be the first line in the file
and storing it in a buffer allows us to TRIM later without having
ugly line breaks.
--->
<cfsavecontent variable="cacheManifest">

<!---
NOTE: Cache Manifest must be the very first thing in this
manifest file.
--->
CACHE MANIFEST

<!---
When a cache manifest is reviewed by the browser, it uses a
complete byte-wise comparison. As such, we can use COMMENTS
to defunk a previously used cache manifest. In this way, we
can use a version-comment to indicate change even when the
file list has not changed.

NOTE: If ANY part of this file is different from the previous
cache manifest, ALL of the files are re-downloaded.
--->
# Cache Manifest Version: 1.3

<!---
Let's list the file that get cached. The URLs to these files
are relative to the cache manifest file (or absolute).
--->
# Core files.
./index.cfm

# iPhone App files.
./icon.png
./home.png

# View Pages. Notice that these can be dynamic pages as long
# as they are valid URLs.
./view.cfm?id=1
./view.cfm?id=2
./view.cfm?id=3
./view.cfm?id=4
./view.cfm?id=5

# Images that get viewed.
./images/1.jpg
./images/2.jpg
./images/3.jpg
./images/4.jpg
./images/5.jpg

</cfsavecontent>


<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->


<!---
Let's reset the output and set the appropriate content type.
It is critical that the manifest file be served up as a type
"text/cache-manifest" mime-type.

NOTE: We need to be careful about the whitespace here since
the very first line of the file must contain the phrase,
"CACHE MANIFEST". As such, we must TRIM() the content.
--->
<cfcontent
type="text/cache-manifest"
variable="#toBinary( toBase64( trim( cacheManifest ) ) )#"
/>

在使用Cache Manifest的时候,有一些要点需要明白.第一,”CACHE MANIFEST”这行文字,必须出现在Cache Manifest文件的开头.第二,文件的类型必须被定义为MIME类型,”text/cache-manifest”.第三,整个Cache Manifest文件是通过每个字节来区分版本的.这第三点的意思就是说,Cache Manifest文件中任何位置的变动,包括注释内容(#),都会使原来的Cache Manifest作废.此时,整个列表中需要被缓存的资源都会被重新下载.

就像在CSS文件中一样,Cache Manifest中列出的资源路径既可以是完整的,也可以是相对这个Cache Manifest文件的.因为我们缓存的是动态页面,这些动态页面按说应该独立地列在Cache Manifest文件里(译者注:可能意思是,举个例子,有时动态页面需要更新缓存,而图片不变,这是如果动态页面的Cache Manifest文件和图片的是一个文件,便会使整个列表,也即包括图片被重新下载).还有其他的一些办法来做到这点,但是仅对于我的试验来说,这是最简单的做法.

列表中的”查看”页面除了动态显示与获得的ID相应的图片以外,什么也没做.

view.cfm

<!--- Param the image ID that we are going to view. --->
<cfparam name="url.id" type="numeric" />


<!---
Use an HTML5 doc-type. This is required for some browsers to
work properly with the offline-cache.
--->
<!DOCTYPE HTML>

<!---
Add a file to the cache manifest. NOTE: Any file that calls
a cache manifest file will be automatically cached, even if
it is in the WHITELIST portion of the cache manifest.
--->
<html manifest="./cache_manifest.cfm">
<head>
<title>View Image</title>
</head>
<body>

<h1>
View Image
</h1>

<p>
&laquo; <a href="./index.cfm">Back to List</a>
</p>

<p>
<cfoutput>
<img src="./images/#url.id#.jpg" width="300" />
</cfoutput>
</p>

</body>
</html>

我在查看页面中也加入了Cache Manifest的链接;虽然我相信它并没有被浏览器请求.只要用户首先打开的是列表页面,Cache Manifest文件便会被下载,并且查看页面应该被自动缓存了.我在这里也加上这个,只是单纯地为了保险(译者注:原文为for good measure,不知道能不能我这样翻译).

在列表页面中,你可能已经注意到了一些奇怪的LINK标签,这些LINK标签是为苹果的iPhone设计的Web应用在”App Mode(应用程序模式)”用来定义其图标和启动界面图形的.”App Mode”是当页面URL被添加到了iPhone的Home Screen(主页面)时实现的全屏的模式.这些Link标签与Cache Manifest没有关系;但是,它们能说明我进行HTML5在这一块的探索的原因-为iPhone创建可以在本地运行(译者注:原文为native-esque,没查到)的Web应用程序.

Cache Manifest似乎是HTML5的一个非常酷特征,这些例子能够在Firefox,Safari和我的iPhone上运行(没有尝试过其他浏览器).我并没有在桌面Safari的研究上下很多的功夫,但是或许它能在Firefox的离线模式和iPhone的飞行模式下使用已经非常惊人了.就像我之前所说的,我最终想要进行这个研究,是为了让我的Dig Deep Fitness web application在iPhone上像本地应用程序那样运行.

注意:当我刚开始尝试使用Cache Manifest时,为了让离线缓存被识别,费了很大的力气.两天的时间我一直在检查我的拼写和无效链接.后来我重新启动了Firefox,终于得到了一点安慰(译者注:原文blam,没查到,Google了下,貌似有类似的意思)-它开始工作了.所以,如果第一次没有作用,你或许需要重新启动一下浏览器.