In this chapter, we will talk about Dart's ability to store data locally on a client, break the storage limit, and prevent security issues in our web applications. We will also take a look at the good old cookies, show you how to use Web Storage, and elaborate on the more powerful and useful IndexedDB to store large amount of data in the user's web browser. The following topics will be covered in this chapter:
The concept of cookies was introduced for the first time in 1994 in the Mosaic Netscape web browser. A year later, this concept was introduced in Internet Explorer. From that time, the idea of tracking contents of shopping cart baskets across browser sessions remains relevant until today. So, what problem do cookies solve?
An HTTP protocol is stateless and doesn't allow the server to remember the request later. Because it was not designed to be stateful, each request is distinct and individual. This simplifies contracts and minimizes the amount of data transferred between the client and the server. In order to have stateful communication between web browsers, you need to provide an area in their subdirectories where the state information can be stored and accessed. The area and the information stored in this area is called a cookie. Cookies contain the following data:
Cookies are handled transparently by the web browser. They are added to the header of each HTTP request if its URL matches the domain and the path is specified in the cookie. Cookies are accessible via the document.cookie
property. Bear in mind that when you create a cookie, you specify the name, value, expiry date, and optionally the path and domain of cookies in a special text format. You can find a cookie by its name and fetch the value only, as all the other information that is specified while creating a cookie is used by the web browser and is not available to you. We created the cookies.dart
file to keep small library wrappers of the original API of the cookie to avoid boilerplate and code duplication, which allow you to easily set, get, and remove cookies from any application. The cookies use the date format derived from the RFC 1123 International format, that is, Wdy, DD Mon YYYY HH:MM:SS GMT. Let's look at them in more detail:
You can see the date in this example: Thu, 09 Oct 2014 17:16:29 GMT.
Let's go through the following code and see how cookies can be reached by Dart. We import the dart:html
package especially to make document.cookies
available in our code. The intl.dart
package is imported because of DateFormat
usage in the toUTCString
function to calculate the expiry date in the UTC format of the cookie based on the value in the days
attribute and transforms it into a string. If the value of days
is less than one, then the toUTCString
function returns an empty string. To create cookies with the setCookie
function, we need to specify the name of the cookie, value, and the number of days to expire. We can provide the optional path and domain information as well. In practice, you cannot use non-ASCII characters in cookies at all. To use Unicode, control codes, or other arbitrary byte sequences, you must use the most popular UTF-8-inside-URL-encoding that is produced by different encoding methods of the Uri
class. To return cookies via getCookie
, we only need the name of the cookie. At the end, you will find the removeCookie
function.
library cookie; import 'dart:html'; import 'package:intl/intl.dart'; // Number milliseconds in one day var theDay = 24*60*60*1000; DateFormat get cookieFormat => new DateFormat("EEE, dd MMM yyyy HH:mm:ss"); String toUTCString(int days) { if (days > 0) { var date = new DateTime.now(); date = new DateTime.fromMillisecondsSinceEpoch( date.millisecondsSinceEpoch + days*24*60*60*1000); return " expires=${cookieFormat.format(date)} GMT"; } else { return " "; } } void setCookie(String name, String value, int days, {String path:'/', String domain:null}) { StringBuffer sb = new StringBuffer( "${Uri.encodeQueryComponent(name)}=" + "${Uri.encodeQueryComponent(value)};"); sb.write(toUTCString(days)); sb.write("path=${Uri.encodeFull(path)}; "); if (domain != null) { sb.write("domain=${Uri.encodeFull(domain)}; "); } document.cookie = sb.toString(); } String getCookie(String name) { var cName = name + "="; document.cookie.split("; ").forEach((String cookie) { if (cookie.startsWith(cName)) { return cookie.substring(cName.length); } }); return null; } void removeCookie(String name) { setCookie(name, '', -1); }
The domain of the cookie tells the browser the domain where the cookie should be sent, and in its absence, the domain of the cookie becomes the domain of the page. It allows cookies to cross subdomains, but it does not allow the cookies to cross domains. The path of the domain is the directory present in the root of the domain where the cookie is active.
For a better understanding of the use of cookies in the real world, you can look at the shopping_cart_cookie
project. This project is very big, so I will show you small code snippets and point you in the right direction. This project contains the following main classes:
Product
: This class describes the items in a cart with the ID, description, and price. Users can specify the quantity of items they want to buy, so the amount can be calculated by multiplying the quantity with the price.ShoppingModel
: This class helps you to fetch products from the product.json
file and returns the Future
instance with a list of products.ShoppingController
: This class renders the grid of products and updates the amount per product and the total amount. We can send a reference in the body of the table. This reference on the element keeps the total amount and the instance of the ShoppingModel
and StorageManager
class via a constructor injection.We will call getProducts
from the model in the constructor especially to return data from the server and when the data is ready, we will call the _init
method to initialize our application. The readQuantity
method is called for the first time to check and return the quantity of the product saved in the cookie. Later in the code, we will call calculateAmount
of the product based on the quantity and price, as shown in the following code:
ShoppingController(this.tBody, this.totalAmount, this.service, this.storage) { model.getProducts().then((List<Product> products) { _products = products; _init(); }); } _init() { update().then((value) { draw(); drawTotal(); }); } Future update() { return Future.forEach(_products, (Product product) { // Read quantity of product from cookie return readQuantity(product); }); } Future readQuantity(Product product) { return storage.getItem(Product.toCookieName(product)) .then((String quantity) { if (quantity != null && quantity.length > 0) { product.quantity = int.parse(quantity); } else { product.quantity = 0; } }); } //…
The Product.toCookieName
method creates a unique string identifier with a combination of Product.NAME
and product.id
. Now launch the application and open the index.html
file on the Dartium web browser to see a list of products.
We can check the existence of cookies in the web browser. In Dartium, open Developer tools from the Tools menu item and choose the Resources tab, as shown in the following screenshot:
Select Cookies and choose localhost to ensure that no cookies are associated with our web server.
If the user changes the quantity of any product via the text input field, then the number of products and the total number of selected products will be recalculated. At the same time, our code calls the saveQuantity
method of the ShoppingController
class, as follows:
saveQuantity(Product product) { if (product.quantity == 0) { storage.removeItem(Product.toCookieName(product)); } else { storage.setItem(Product.toCookieName(product), product.quantity.toString()); } }
The product is removed from the cookie if the number in the Quantity field equals zero. In other cases, we will create or update the quantity value in cookies.
Let's check the preceding information. Return to the Resources tab and navigate to the localhost tree item. Click on the Refresh icon at the bottom of the window to see the list of cookies associated with our server.
Now, close the index.html
page and open it again. Information about the selected products along with their specified quantity and number will be available here. Click on the Check Out button to invoke a cart.checkOut
method to show a message about the paid items and remove the cookies from them. The code is as follows:
checkOut.onClick.listen((Event event){ cart.checkOut(); });
The following screenshot shows the resulting message:
Cookies are a universal mechanism that help to persist user-specific data in the web browser. They can be very useful while essaying the following roles:
You can combine different roles to the achieve specific requirements of your business logic. The following are the benefits of having cookies:
The following are the disadvantages of cookies:
So, the main purpose of cookies is to make the client-to-server communication stateful. If you need to only save data on a client or work offline, you can use other techniques and one of them is Web Storage.
18.118.24.30