“Deno entry to give up” type of article has a lot of big guy to write, but also a more detailed description, so I do not rub this type of article.

I’m going to talk about how to build a Router wheel with Deno from the point of view of building a wheel. (In fact, I haven’t finished writing this set of Web Server, can only take the Router part which is more completed 😅)

Origin of routing

The earliest appearance of routing is originated from the server side. In the early Web system architecture, routing is used to describe the mapping between URL and processing function or resource. For example,.asp.aspx.jsp. SHTML, etc., is through SSR (Serve Side Render) to achieve rendering, and finally directly return to the display page. (It can be understood that some of the front-end concepts are leftover from the early years.)

In the early days of routing, the request URI and the framework’s custom file directory + file class + class function were used to map the route. Suppose the route is/XXX /user/get_user_info

So the file directory structure is:

Application ├── SRC │ ├─ user │ ├─ userController.csCopy the code

So there will be a function like this in userController.cs:

// asp.net MVC example code
// XXX is the project name -> / XXX
namespace xxx.Controllers
{
    // Controller name -> / XXX /user
    public class UserController : Controller // C -> Controller
    {
        // function -> / XXX /user/get_user_info
        public GetUserInfo Index() {
            return View(); // The View points to the View file to be returned. When called, the server will render it}}}Copy the code

It can be seen from this that the early Web application framework is through the project directory structure, file name, class name, function name and other ways of the Path of the unified constraints, and then realize the URI matching. Making a lot of system calls per URI request here is inefficient nowadays, but in the early days it was very advanced because it was probably the fastest way to implement an interface while avoiding defining routes.

Principles of routing

Now that you have learned about the past life of routing, you can learn about the present life of routing: modern routing

Currently, there are three route resolution modes:

  • Folder path
  • Regular match
  • Dictionary tree/prefix tree

Folder path

The folder structure determines the route complexity and clarity, exposes the real project directory, and has limited processing capacity for dynamic routing parameters.

E.g. suppose the route is/XXX /user/info

Then the file structure might look like this:

├─ user │ ├─ info │ ├─ index.htmlCopy the code

In this case, it is very similar to the early routing.

Regular match

URI Path matching based on regular expressions can realize dynamic parameters. It is difficult to maintain complex paths or long string rules, so you need to be familiar with regular expression writing.

E.g. assume that the route is/XXX /user/123456789/info

Then the implementation in the code might be:

app.route('/xxx/user/{\d{9}}/info', getUserInfo);
Copy the code

Any REQUEST/XXX /user/ URI that matches a 9-digit /info will be processed by the getUserInfo function.

Regex matched routing URIs are only suitable for small and well-planned projects. If the project is large and the parameters are multipath deep, the regex is more difficult to maintain and write.

Dictionary tree/prefix tree

Dictionary/prefix trees are implemented by assembling the defined Path into a tree of nodes shred by “/”, then matching the URI through the tree node to find the node information, and then executing the function.

E.g. suppose the route is:

  • /xxx/user/123456789/info
  • /xxx/article/123456

A route is defined as:

  • /xxx/user/{\d{9}}/info
  • /xxx/user/repwd
  • /xxx/article/{\d{6}}

The final tree structure is as follows:

The prefix tree has better performance than the base dictionary tree at matching long strings, so it is more suitable for URI matching scenarios.

The dictionary tree matches all characters, that is to say, the current node needs to be traversed by breadth, while the prefix tree maintains the prefix list of the child nodes in the parent node, and performs index lookup before matching.

Routing implementation

OK, so with that much pushing you can start implementing a simple route, and in this case I’m using a dictionary tree. After continuous improvement and optimization, I will update the way of prefix tree, because from the difference of dictionary tree to prefix tree, the code change is not big.

To implement routes, first of all, you need to define routes, and each route needs a handler. Each route defined requires a URL attribute and a handle attribute. Then, according to the principle of dictionary tree routing, the Path is divided by “/”, and each Path is a node.

Deno natively supports TypeScript, so I’m writing in TypeScript.

Code implementation is as follows:

// Define the route list type
type RouteList = { url: string.handle: () = > void} [];// Divide the Route Path by "/"
function splitRoutePath (routeList: RouteList = []) {
  routeList.forEach((item) = > {
    const splitPath = item.url.split('/');
    const splitPathLen = splitPath.length - 1;

    // If it does not start with/or end with /, empty string entries need to be processed
    if(! splitPath[splitPathLen]) { splitPath.splice(splitPathLen,1); }
    if(! splitPath[0]) { splitPath.splice(0.1); }

    // Call GenerateNodeTree to generate the node tree
    GenerateNodeTree(splitPath, item.handle);
  });
}

// Let's assume that the url is defined, and let's not consider using the regular match Path here
// The route list defined requires a URL attribute and a handle attribute
const rrl = [
  { url: '/user/repwd'.handle: () = > { console.log('/user/repwd'); }}, {url: '/user/:id/info'.handle: () = > { console.log('/user/:id/info'); }}, {url: '/article/:id'.handle: () = > { console.log('/article/:id'); }},];// Call splitRoutePath to split the route path
splitRoutePath(rrl);
Copy the code

SplitRoutePath will give you a list of three Route paths:

  • user -> repwd => handle
  • user -> :id -> info => handle
  • article -> :id => handle

Convert to a dictionary tree structure

Then we need to convert these lists into a tree structure, a dictionary tree of routes. So you need to implement a GenerateNodeTree function to handle it.

// Node type definition
type NodeType = { [key in string]: RouteNode };

// Route node interface definition
export interface RouteNode {
  key: string;
  name: string;
  handler: handlerType | undefined;
  children: NodeType | undefined;
}

// Route parameter extraction - regular expression
const regExpParamKey: RegExp = /^:(\w+)\?? $/;

// Map node tree - dictionary tree
export const mapNodeTree: NodeType = {};

// Generate the number of nodes
function GenerateNodeTree (pathArr: string[] = [], handle: handlerType) {
  const len = pathArr.length; // The length of the path array
  let tempNode: NodeType = mapNodeTree; // 

  pathArr.forEach((item: string, index: number) = > {
    if(! tempNode[item]) {// Matches whether it is a route parameter
      const regResult = regExpParamKey.exec(item);
      // If it is a route parameter, the parameter name is used as the node key; otherwise, the Path is used as the node key
      consttempKey = regResult ! = =null ? regResult[1] : item; 
      // The information needed to assemble the node
      tempNode[item] = {
        key: tempKey,
        name: item,
        handler: index === len - 1 ? handle : undefined.children: index ! == len -1 ? {} : undefined}}if(index ! == len -1) {
      // Process the child node information
      if (!tempNode[item].children) { tempNode[item].children = {}; }
      tempNode = tempNode[item].children || {};
    }
  });
}
Copy the code

After GenerateNodeTree is called, the mapNodeTree object is the final dictionary tree.

The structure of the print tree is as follows:

// Omitted some other field information
mapNodeTree = {
  user: {
    key: "user".children: {
      repwd: {
        key: "repwd".handler: [Function: handle]
      },
      :id: {
        key: "id".children: {
          info: {
            key: "info".handler: [Function: handle]
          }
        } 
      }
    }
  },
  article: {
    key: "article".children: {
      :id: {
        key: "id".handler: [Function: handle]
      }
    }
  }
}
Copy the code

From there, you are done generating the dictionary tree of the route by defining the route path.

Heigh-ho ~ you only have to generate the routing tree to pinch, then I request in how to deal with these routes and call the corresponding function to pinch?

Traverse the matching tree nodes

Don’t worry, the road is step by step, fat is not a bite to eat. Here, of course, we need to implement a function that can traverse the dictionary tree matches.

// Traversal - Matches the dictionary tree node
export const IterateNodeTree = (pathArr: string[] = []) = > {
  const pathParams = new Map(a);const len = pathArr.length;
  let tempIndex = 0;
  let tempNode: NodeType = mapNodeTree;

  // Match node recursive check
  const recursiveCheck = (nodItem: NodeType, nodeKey: string, isEnd: boolean) = > {
    const currentNode = nodItem[nodeKey];
    const nodeChild = currentNode.children;
    const nodeHandler = currentNode.handler;
    if (isEnd) { // If it is the end of the path and there is an execute function, execute, otherwise exit
      // currentNode.key
      nodeHandler && pathParams.size > 0 ? nodeHandler(pathParams) : undefined;
    } else {
      if (nodeChild) {
        tempIndex++;
        tempNode = nodeChild;
        return true; }}return false;
  }

  // Traverse the matching dictionary tree
  while (tempIndex < len) {
    // Get all keynames of the current node entry
    const tempNodeKeys = Object.keys(tempNode);
    // Whether the path ends
    const isEnd = tempIndex === len - 1;
    const isPathParam = tempNodeKeys.find(item= > { return regExpParamKey.exec(item); });
    if(tempNodeKeys.indexOf(pathArr[tempIndex]) ! = = -1) {if(! recursiveCheck(tempNode, pathArr[tempIndex], isEnd)) {return; }}else if (isPathParam) {
      pathParams.set(tempNode[isPathParam].key, pathArr[tempIndex]);
      if(! recursiveCheck(tempNode, isPathParam, isEnd)) {return; }}else { return; }}}Copy the code

With IterateNodeTree implemented, we can simply test the matching results of the dictionary tree.

// Assume the requested URL
const rl: string[] = [
  '/user/repwd'.'/user/123/info'.'/user/456/other/'.'/article/123'.'/article/type'.'/article/type/info',];Copy the code

Let’s run the results:

deno run troute.ts
Copy the code

As you can see from the run results, only URI matches will trigger function execution, while missing URI matches will not trigger function execution.

Hiya ~ you are all local string pinched, I said is a request! Request!!)

Take it one step at a time. If the local string test doesn’t pass, there’s no point in processing the request.

Deno Web Server

Now let’s implement a simple HTTP service

import { serve } from "https://deno.land/std/http/server.ts";

console.log("http://localhost:8000/");

const webServe = serve({ port: 8000 });

for await (const request of webServe) {
  const splitPath = req.url.split('/');
  const splitPathLen = splitPath.length - 1;

  // If the string does not start or end with a slash (/), you need to clear the empty string item
  if(! splitPath[splitPathLen]) { splitPath.splice(splitPathLen,1); }
  if(! splitPath[0]) { splitPath.splice(0.1); }

  console.log('Split URL hierarchy:', splitPath);
  IterateNodeTree(splitPath);
}

Copy the code

Just run, and this simple HTTP is already started. Of course, some of you may not be familiar with it or have just contacted with it, so I will repeat:

By default, Deno is secure. So the Deno module has no access to files, networks, or environments unless you authorize it. You must authorize the Deno process in the command line to access security-sensitive functions.

Since we need access to the network, don’t forget to add the –allow-net flag to the command, and be careful not to write the wrong location after run.

deno run --allow-net troute.ts
Copy the code

(This is a false demonstration)

(Correct password)

At this point we can see that the HTTP service is started and the contents of console.log are printed on the command line.

Initiates a request through fetch

Let’s make a few more requests:

fetch('http://localhost:8000/user/123/info', { method: 'GET' });
fetch('http://localhost:8000/user/repwd', { method: 'POST' });
fetch('http://localhost:8000/user/456/other/', { method: 'POST' });
fetch('http://localhost:8000/article/123', { method: 'GET' });
fetch('http://localhost:8000/article/type', { method: 'POST' });
fetch('http://localhost:8000/article/type/info', { method: 'POST' });

// Remember the route just defined
// The route list defined requires a URL attribute and a handle attribute
const rrl = [
  { url: '/user/repwd'.handle: () = > { console.log('/user/repwd'); }}, {url: '/user/:id/info'.handle: (data: any) = > { console.log('/user/:id/info', data); }}, {url: '/article/:id'.handle: () = > { console.log('/article/:id'); }},];Copy the code

After that we start a command line and run the following command:

deno run --allow-net aa.ts
Copy the code

Of course, don’t forget to add the allow-net permission identifier for access to the network

(I name here more casual diagram convenient, everyone in writing the project time thousands! Wan! No! Want to! Learn from me, this is a bad habit)

From there the matching logic of a routing core is complete, but the implementation of this Router is not capable of achieving the business capability, if anything.

Because here I just describe the origin and concept of routing, as well as the current implementation, but interested in the above code can be modified to achieve simple Web Server function.

Of course, you can try to build it:

deno bundle [OPTIONS] <source_file> [out_file]

deno bundle trouter.ts Router.js
Copy the code

Why did I study Deno

I first mentioned Deno through Ryan Dahl at the JS Conference in Berlin in June 2018. I wasn’t a big fan of Node.js myself at the time but I was looking forward to Deno. Because I want to know what happens when a person thinks he or she failed and tries the same product again. Does he or she learn from it, or does he or she fall into the same hole?

But over time, I’ve seen Deno take a lot of good implementations, and what he blames for failing is not repeated and has more capabilities.

My first experience with Deno was the same as my first experience with Golang. It was a little weird, but very comfortable to write.

If I had to find a reason, it would be this: It is strong and light, just like Golang.

🏆 technology project phase I | talk about Deno some thing…

Copyright notice: the copyright of this article belongs to the author Lin Xiaoshuai, shall not be reproduced without authorization and second modification. Reprint or cooperation please leave a message and contact information below.