This chapter points out some of the useful and fun features of the Dart language and libraries that are used in Dartiverse Search, a client-server app.
As the screenshot shows, Dartiverse Search looks for a user-entered string in GitHub and StackOverflow. The app is asynchronous, adding results as they’re found, so the UI is always responsive.
You can use Dart Editor to get and run Dartiverse Search.
In Dart Editor, go to the Welcome page. (If you don’t see it, choose Tools > Welcome Page.)
In the demo section, click Dartiverse Search to create a copy of the dartiverse_search package.
Use Tools > Pub Build to build the package.
Select bin/server.dart
and click the Run button. You should
see a message that the search server is running at
http://127.0.0.1:9223/.
Enter that URL into any modern browser. The search client UI should appear.
The search server is an HTTP server that provides a WebSocket interface. The search client uses that WebSocket interface as a bi-directional communication channel with the server. The client sends search requests to the server over the WebSocket, and the server replies with any results and then a final, “search done” message.
The server starts things off by binding to localhost, port 9223, and
listening for requests to the WebSocket: ws://127.0.0.1:9223/ws
.
Search clients can connect using that URL.
The real communication between client and server happens when the user enters a search string. As the following diagram shows, the client sends a JSON-encoded search request to the server. The server extracts the search string from the request and sends it to a series of search engines. Each search engine searches a specific site—for example, GitHub—using whatever API that site supports. Whenever a search engine finds a result, the server forwards that result to the client, again using a JSON-encoded message.
The search server implements an HTTP server that both provides content for the client UI and listens for WebSocket requests.
The client code is split between HTML (page structure), CSS (page look), and JavaScript (logic and behavior). That’s typical of web clients.
The twist is that this client’s JavaScript code is produced from Dart code, thanks to the dart2js compiler. Any modern browser can run this JavaScript code.
The client’s UI is simple. It has a search field, implemented as an <input> element named “q”. It displays output in two <div>s named “status” and “results”.
A couple of <script> tags tell the browser to execute the client’s Dart or JavaScript code:
The first line works in browsers that have an embedded Dart VM and so can execute Dart code; currently, only Dartium qualifies.
The second line is important for every other browser. It executes dart.js, which is a standard script that converts all Dart <script> tags to use foo.dart.js instead of foo.dart, with the assumption that foo.dart.js is a JavaScript version of foo.dart. For non-Dartium browsers, dart.js changes the first <script> tag to the following:
You can get dart.js with the browser package from pub. See the dart2js documentation for more information about compiling Dart code into its JavaScript equivalent.
Dart code (web/client.dart
) provides the client’s logic, using the DOM
to interact with UI elements. For example, the client’s Dart code uses
the DOM to find the div where the client displays messages.
The client app uses dart:html’s top-level querySelector()
function to
get the client’s UI elements from the DOM:
The querySelector()
method uses a selector string that identifies an
element in the DOM. See
Finding elements for more about selectors.
The client app uses onChange.listen()
to register a handler that
reacts to user input. Whenever the user presses Enter, the search field
fires a change event, and the handler kicks off a search.
The change event handler gets and sets the text in the search field
using the value
property.
Every time the search client receives a result on the WebSocket, the
client creates a new div (result
) to display it. The client then adds
that new div to the “results” div (resultsElement
).
This code uses method cascades to avoid creating variables to temporarily hold the new HeadingElement and AnchorElement.
The dart:convert library’s global JSON
object lets you encode and
decode JSON-formatted messages. JSON is an easy way to provide string
message data to WebSockets. Using JSON also gives a bit of structure to
the messages and leaves the door open to creating more detailed messages
in the future.
The JSON.encode()
method converts a Dart object to a JSON-encoded
string, and the JSON.decode()
method converts a JSON string back into
a Dart object.
Here’s the code that creates a JSON-encoded search request:
Here’s how the client decodes and processes a JSON response from the server:
For more information, read about dart:convert.
The search client connects to the WebSocket by calling the WebSocket constructor with the argument ‘ws://127.0.0.1:9223/ws’. Then it adds event handlers for open and error events. The open event handler, in turn, registers handlers for message and close events. Here’s the relevant code:
To send a message on the WebSocket connection, the client invokes
WebSocket’s send()
method:
When the client receives a message, it decodes the JSON data (as you saw before) and updates the UI to match:
The main code for the search server is under the bin
directory, in a
file named server.dart
. It’s responsible for serving static files,
managing WebSocket connections, and starting searches.
The code for performing the searches is in a custom library, called
search_engine, that’s implemented in files under the lib
directory.
The search server uses HttpServer (from dart:io), Platform (also from dart:io), and VirtualDirectory (from the http_server package) to implement a basic web server. Here’s the code that initializes the web server and serves static files:
The call to HttpServer.bind()
creates a web server that handles HTTP
requests to the address 127.0.0.1:9223 (also known as localhost:9223).
Once the future returned by bind()
completes, the code creates a
Router (more about that later) and a VirtualDirectory (virDir
).
Because packages used by the client are set up using symbolic links
pointing outside the root directory, ../build
, the jailRoot
property
of virDir
must be false. (By default, symbolic links aren’t allowed
outside the root directory.) The next line sets allowDirectoryListing
to true, allowing the web server to respond to paths that don’t include
a filename. Next, a custom directory handler overrides the default
directory listing code, so that directories display index.html
instead
of a list of files.
Once the VirtualDirectory is all set up, invoking the serve()
method
connects virDir
to a stream of HTTP requests. This stream consists of
every HTTP request that the router doesn’t handle specially—for example,
the stream doesn’t include WebSocket connection requests.
The search server uses the Router class from the route package to serve dynamic content. In this app, the main purpose of the router is to filter out upgrade HTTP requests for /ws and to handle them as WebSocket connections. The code uses dart:io’s WebSocketTransformer class to perform the conversion:
Here’s the custom handleWebSocket()
method, which handles events on
the WebSocket:
The call to webSocket.map()
parses all the messages that the client
sends over the WebSocket, converting each JSON-formatted message into an
object. Then, after checking the message format, the handler initiates
searches on GitHub and StackOverflow.
Here’s the code from the ‘search’ case that starts the searches and handles results as they come in:
Each engine can return up to 3 results, but the WebSocket handler
doesn’t wait around for those results. Instead, a listener handles each
result as it arrives, constructing a JSON-formatted message and using
webSocket.add()
to forward the result to the client. Once both engines
have finished sending any results, the search server sends a
‘searchDone’ message to the client.
The GitHub and StackOverflow searches are implemented in search()
methods that take an input string and return a stream of SearchResult
objects. Here’s an example from the GitHub search()
method:
This method first constructs a search URI, using the Uri.https()
constructor from dart:core’s Uri class. The third argument is a
Map<String, String> that specifies the Uri’s query parameters. For
example, if the input string is ‘polymer’, then the URI is this:
https://api.github.com/search/repositories?q=language%3Adart+polymer
Next, the method creates an instance of StreamController (a class from dart:async) to create and manage the stream of results.
Next comes the search request, using the http package’s get()
function
to send an HTTP GET request to the search URI. The get()
function
returns a Future<Response>. The then()
method registers a handler
for the response, catchError()
registers an error handler, and
whenComplete()
registers a cleanup function. At this point, the
search()
method returns the stream created by StreamController.
Once the response arrives, the response handler decodes it and adds the first three reasonable results to the result stream. If an error occurs, then an error goes in the result stream, causing the search client’s onError handler to execute. After either a successful completion or an error, the stream closes and the search client’s onDone handler executes.
The search server implements a library, named search_engine, that
contains all the code for performing searches. The search_engine
library is declared in search_engine.dart
, with additional
implementation in github_search_engine.dart
and
stack_overflow_search_engine.dart
. Here’s the code that sets up the
library:
The other files in the library don’t have imports. They do, however,
have part of
statements, which let tools and programmers know which
library relies on these files:
The implementation of the search_engine library is split as follows:
Contains two basic classes, SearchResult and SearchEngine. A
SearchResult contains a title and a link. SearchEngine is an
abstract class that defines a common API for all search engines: a
name
property and a search()
method that takes a string argument
and returns a Stream<SearchResult>.
Implements GithubSearchEngine, a SearchEngine subclass that searches GitHub for Dart projects that include the search string.
Implements StackOverflowSearchEngine, a SearchEngine subclass that searches StackOverflow for Dart questions with the search string in the title.
The bulk of the code is in the SearchEngine subclasses.
The search server uses the logging
package to log messages at
varying levels of severity. Here’s the code from bin/server.dart
that
imports API from the logging package and creates a log:
The Logger class has many methods for logging messages at pre-defined levels. Here’s an example of logging an informational message, which you might use for debugging:
Here’s an example of logging a warning:
By default, the logging package doesn’t do anything useful with the log
messages. You must configure the logging level and add a handler for the
log messages. Here’s the code from bin/server.dart
that creates and
configures the Logger object:
Setting the level to Level.ALL
makes all logging messages appear in
the onRecord stream. If you want only warnings to appear, you can set
the level to Level.WARNING
.
For a list of all the levels and what they mean, see the Level API documentation. See the Logger API documentation for a list of methods that log messages.
You’ve seen how the Dartiverse Search sample uses both server-side and client-side Dart code to implement a web app. It makes use of both built-in libraries and libraries from packages published on pub.dartlang.org.
If you’d like to continue exploring Dartiverse Search, consider improving its user interface or adding another search engine. If you’d like to look at other samples, you can find them in Dart Editor and in Dart Code Samples.
Check out the other resources on this website, including code labs, tutorials, and the programmer’s guide.