Here we build a component system together from zero. An environment that a component can access through Markup and JavaScript, as I first learned from my previous articles “Front End Componentalization Basics” and “Building a Component Parser with JSX.”

So our first step is to set up an environment that can use markup. Here we will learn the style of establishing markup using JSX. Here we base our component style on the same JSX as React.


JSX environment setup

JSX is part of React. Instead, Facebook defines JSX as a pure language extension. This JSX can also be used by other component architectures.

We can even use it as a standalone way to quickly create HTML tags.

Establishing the project

So let’s start with the basics. First we need to create a new project directory:

mkdir jsx-component
Copy the code

Initialize the NPM

Create the project folder in your preferred directory, of course. With the folder set up, we can go into the directory and initialize the NPM.

npm init
Copy the code

After the preceding command is executed, some item configuration options are displayed. You can fill in the options if necessary. If you need to open package.json, you can change it by yourself.

Install webpack

Wepack, as many of you have probably already seen, allows you to turn an ordinary JavaScript file into a file that packs together different import and require files.

So we need to install Webpack, of course we can also use NPX directly use Webpack, or global install Webpack – CLI.

So here we use the global installation webpack-CLI:

npm install -g webpack webpack-cli
Copy the code

Once installed, we can check the installed version of WebPack by typing one of the following commands. If there are no errors after execution and a version number comes out, we have successfully installed.

webpack --version
Copy the code

Install Babel

Since JSX is a plugin for Babel, we need to install webPack, babel-Loader, Babel and Babel plugin in sequence.

Another useful thing about using Babel is that it can compile a newer version of JavaScript into an older version of JavaScript, so that we can support running in more older browsers.

To install Babel, we simply need to execute the following command.

npm install --save-dev webpack babel-loader
Copy the code

The important thing to note here is that we need to add –save-dev so that we can add Babel to our development dependency.

After executing, we should see the message in the figure above.

To verify that we have installed it correctly, we can open package.json in our project directory.

{
  "name": "jsx-component"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": ""."license": "ISC"."devDependencies": {
    "babel-loader": "^ 8.1.0"."webpack": "^ 5.4.0"}}Copy the code

Well, under devDependencies, we can see that there are indeed the two packages we just installed. For those of you who are worried, check out package.json again.

Configuration webpack

At this point we need to configure the WebPack configuration. To configure webpack we need to create a webpack.config.js configuration file.

Create a webpack.config.js file at the root of our project.

First, webPack config is a NodeJS module, so we need to write its Settings using module.exports. This is a common configuration method for early NodeJS tools. It uses a JavaScript file to do its configuration, so that it can add some logic to the configuration.

module.exports = {}
Copy the code

One of the most basic things about Webpack is that you need to set up an entry (set its entry file). In this case, we’ll just set main.js.

module.exports = {
  entry: "./main.js"
}
Copy the code

At this point, we can create our main.js file in our root directory. Let’s start with a simple for loop.

// The contents of the main.js file
for (let i of [1.2.3]) {
  console.log(i);
}
Copy the code

Now that we have the basic configuration of webpack configured, let’s execute webPack in the root directory to package our main.js file. We just need to execute the following line:

webpack
Copy the code

After execution, we can see the above prompt in the command line interface.

Those of you who pay attention to details, you must raise your hand and ask! Your command line error! The yellow part does give us a warning, but it doesn’t matter, our next configuration will fix it.

At this point we’ll notice that a new folder dist has been generated in our root directory. This is the default folder generated by webPack packaging, where all of our packed JavaScript and resources will be placed by default.

Here we see that the dist folder contains a packaged main.js file, and this is the version of main.js that we wrote, packaged via Webpack.

Then we open it and see its JavaScript code compiled by Babel. We’ll find a lot of stuff added to our few lines of code that we don’t even have to worry about, the “meow power” of Webpack.

At the very end of the code, you can still see the for loop we wrote. It’s just been modified, but it does the same thing.

Install Babel – loader

Babel-loader is not directly dependent on Babel, so we need to install @babel/core and @babel/preset-env. We just need to execute the following command line to install:

 npm install --save-dev @babel/core @babel/preset-env
Copy the code

The final result, like the one above, proves that the installation was successful. In this case, we need to configure the webpack.config.js to use the babel-loader for packaging.

Add an option called module to the webpack.config.js entry we configured above.

Then we can add a rule to the module, which is the rule we use when we build. Rules is an array configuration, where each rule consists of a test and a use.

  • test:
    • testThe value of is a regular expression that matches the files we need to use this rule. Here we need to match all the JavaScript files, so we use/\.js/Can.
  • use:
    • loader:
      • Just join usbabel-loaderThe name of
    • options:
      • presets:
        • Here are the loader options, and here we need to join@babel/preset-env

Our final configuration file will look something like this:

module.exports = {
 entry: './main.js'.module: {
   rules: [{test: /\.js$/,
       use: {
         loader: 'babel-loader'.options: {
           presets: ['@babel/preset-env'],},},},],},};Copy the code

So once we’ve done that, we can run Babel and see what it looks like. As before, we just need to execute Webpack from the command line.

If our configuration file is correctly written, we should see the result in the figure above.

Then we go to the dist folder, open our compiled main.js, and look at the results of our compilation using babel-loader.

As a result, we see that the for of loop is compiled into a regular for loop. This also proves that our Babel-Loader works, correctly programming our new version of JavaScript syntax to be compatible with older browsers’ JavaScript syntax.

At this point we have installed and set up the environment we need for JSX.

Pattern configuration

Finally, we also need to add an environment configuration in webpack.config.js, but this can also be added or not, but for the convenience of normal development.

So we need to add a mode to webpack.config.js, and we use development for the value of this property. This configuration indicates that we are in developer mode.

Generally speaking, the webpack configuration we write in the code repository will have this mode: ‘development’ configuration by default. When we actually release, we’ll change it to mode: ‘Production’.

module.exports = {
  entry: './main.js'.mode: 'development'.module: {
    rules: [{test: /\.js$/,
        use: {
          loader: 'babel-loader'.options: {
            presets: ['@babel/preset-env'],},},},],},};Copy the code

After that, let’s use Webpack to compile and see what the differences are in our main.js.

It is obvious that the compiled code is no longer compressed into a single line. This allows us to debug the code generated by WebPack. Notice here that our code in main.js is converted to a string and put into an eval() function. Our code is put into eval so that we can use it as a single file during debugging.

Introducing the JSX

Now that we’re all set, how do we finally introduce JSX? Before we introduce it, let’s see what happens if we use the JSX syntax in our main.js as it is now. As programmers, we have to be adventurous!

So we added this code to our main.js:

var a = <div/>
Copy the code

Then let’s execute webpack and see!

Boy! Sure enough, the report was wrong. This error tells us that we cannot use the “less than sign” after the =, but in normal JSX syntax, this is actually the “Angle bracket” of the HTML tag. Since there is no COMPILATION of JSX syntax, JavaScript defaults to the “less than sign”.

So how do we make our WebPack compilation process support JSX syntax? The most important package is @babel/plugin-transform-react-jsx. Ok, so let’s execute a command to install the package:

npm install --save-dev @babel/plugin-transform-react-jsx
Copy the code

Once installed, we need to add it to the WebPack configuration. We need to add a plugins [‘@babel/plugin-transform-react-jsx’] to the rules use in the Module.

And then finally our WebPack configuration file looks like this:

module.exports = {
  entry: './main.js'.mode: 'development'.module: {
    rules: [{test: /\.js$/,
        use: {
          loader: 'babel-loader'.options: {
            presets: ['@babel/preset-env'].plugins: ['@babel/plugin-transform-react-jsx'],},},},],},};Copy the code

Once configured, let’s go back to webpack. At this point we realize that there are no more errors. This proves that our code now supports writing using JSX syntax.

Finally, let’s take a look at how it worked out.

We’ll notice that the

we added in Eval is translated into a react. createElement(“div”, null) function call.

So let’s take a look at how we should implement the react. createElement and see if we can change it to our own function name.

JSX basic usage

First, let’s try to understand JSX, which is essentially a shortcut to code syntax. As we saw at the end of the previous section, the JSX syntax is compiled with a call to react.createElement.

JSX fundamentals

Here we will modify the JSX plug-in in WebPack to give it a custom create element function name. We open webpack.config.js, and at the plugins location, we modify it.

module.exports = {
  entry: './main.js'.mode: 'development'.module: {
    rules: [{test: /\.js$/,
        use: {
          loader: 'babel-loader'.options: {
            presets: ['@babel/preset-env'].plugins: [['@babel/plugin-transform-react-jsx',
					{ pragma: 'createElement'}]],},},},],},};Copy the code

[‘@babel/plugin-transform-react-jsx’] [[‘@babel/plugin-transform-react-jsx’]] [[‘@babel/plugin-transform-react-jsx’, {pragma: ‘createElement method’}]]. By adding the pragma parameter, we can customize the function names of the elements we create.

With this change, our JSX has no connection to the React framework. If we execute the webpack to see the resulting effect, the react. createElement inside will become createElement.

Let’s try adding an HTML file to execute our main.js. First create a main.html file in the root directory and type the following code:

<script src="./main.js"></script>
Copy the code

Then we perform the HTML file opening in the browser.

At this point our console will throw us an error that our createElement is undefined. It’s true that we haven’t defined this function in main.js yet, so it won’t be found.

So we need to write our own createElement function. We’ll open main.js directly from the root directory and delete the for loop, then add the following code:

function createElement() {
  return;
}

let a = <div />;
Copy the code

We’ll just return null, and let the function be called first. Let’s recompile with Webpack and refresh our main.html page. At this point, we will find that the error is not reported and can run normally.

Implement the createElement function

In our compiled code, we can see that the JSX element calls createElement with two parameters. The first argument is div and the second is a null.

Why is the second argument null? The second parameter is actually used to pass the property list. If we add an id=”a” to the div inside main.js, let’s see what happens when we compile it.

We see that the second parameter becomes a JavaScript object stored as key-value. If we think about it for a moment, JSX is not that mysterious. It just compiles and rewrites HTML into JavaScript objects, which we can think of as “syntactic sugar”.

But JSX affects the structure of the code, so we wouldn’t exactly call it syntactic sugar.

Now let’s write some more complicated JSX, and we’ll add some children elements to our original div.

function createElement() {
  return;
}

let a = (
  <div id="a">
    <span></span>
    <span></span>
    <span></span>
  </div>
);
Copy the code

Finally, let’s perform webpack packaging to see what happens.

In the console, we can see the result of the final compilation, which is a recursive call to createElement. You’ve actually formed a tree here.

The parent is the first div element, and the child is later when the argument is passed to the first createElement function. And since our spans are unattribute, the second argument to createElement after all of them is null.

Based on the compilation results we see here, we can now figure out what the parameters of our createElement function should be.

  • The first parameter typeThis is the type of tag
  • Second parameter attribute— All attributes and values within the tag
  • The rest of the arguments are subattributes . childrenHere we are using a relatively new syntax in JavaScript. childrenGive the children variable an array of all the following arguments

The createElement function could be written like this:

function createElement(type, attributes, ... children) {
  return;
}
Copy the code

We have the function, but what does the function do? This function can be used to do just about anything, and since it looks like a DOM API, we could have made it a solid DOM unrelated to React.

For example, we could return the element of type Type in this function. Here we add all the attributes passed in to the element, and we can attach its children to the element.

To create an element we use createElement(type), to add an attribute we use setAttribute(), and finally to put a child element we use appendChild().

function createElement(type, attributes, ... children) {
  // Create the element
  let element = document.createElement(type);
  // Hang the attribute
  for (let attribute in attributes) {
    element.setAttribute(attribute);
  }
  // Hang all child elements
  for (let child of children) {
    element.appendChild(child);
  }
  // Finally, our element is a node
  // So we can return directly
  return element;
}
Copy the code

Here we implement the logic of the createElement function. Finally, we need to mount our DOM node on the page. So we can mount it directly on top of the body.

// Add this code at the end of main.js
let a = (
 <div id="a">
   <span></span>
   <span></span>
   <span></span>
 </div>
);

document.body.appendChild(a);
Copy the code

Another thing to note here is that we don’t have the body tag in main.html, without which we wouldn’t be able to mount it. So here we need to add the body tag to main.html.

<body></body>

<script src="dist/main.js"></script>
Copy the code

Ok, at this time we can webpack to see the effect.

Wonderful! We have successfully generated and mounted the node on the body. But if we add text to our div, a text node will be passed into our createElement function. Needless to say, our createElement function cannot handle text nodes with its current logic.

Next we add the logic to handle the text nodes, but first we remove the SPAN tag from the div and replace it with the text “Hello World”.

let a = <div id="a">hello world</div>;
Copy the code

Before we add the logic of the text node, let’s do a webpack to see what errors are reported before we hang up the child node.

First we see that at the createElement call our text is passed in as a string, and then that argument receives the child node, and in our logic we use appendChild, which receives the DOM node. Obviously, our text string is not a node, so we’ll get an error.

By debugging this way we can immediately figure out where we need to add logic to implement our functionality. This way can also be considered a shortcut.

So let’s go back to main.js and determine the type of child before we hang it. If it’s a String, create a text node using createTextNode() and then mount it to the parent element. So we’re done with the character nodes.

function createElement(type, attributes, ... children) {
  // Create the element
  let element = document.createElement(type);
  // Hang the attribute
  for (let name in attributes) {
    element.setAttribute(name, attributes[name]);
  }
  // Hang all child elements
  for (let child of children) {
    if (typeof child === 'string') 
		child = document.createTextNode(child);
    element.appendChild(child);
  }
  // Finally, our element is a node
  // So we can return directly
  return element;
}

let a = <div id="a">hello world</div>;

document.body.appendChild(a);
Copy the code

Once we’ve packed with this new code, Webpack, we can see our text displayed on the browser.

The createElement we wrote here is a useful thing, and we can use it to do some DOM manipulation. It can even take the place of writing document. CreateElement all the time.

We can verify this by reappending our previous three spans within the div and adding text to each span. 11

let a = (
  <div id="a">Hello world:<span>a</span>
    <span>b</span>
    <span>c</span>
  </div>
);
Copy the code

Then when we repack the Webpack, we can see that we can actually complete the DOM manipulation.

The code is now capable of performing some basic componentization capabilities.

Implement custom tags

We’ve been using some of the tags that come with HTML. What if we now change the D in div to uppercase D?

let a = (
  <Div id="a">Hello world:<span>a</span>
    <span>b</span>
    <span>c</span>
  </Div>
);
Copy the code

Sure enough, errors will be reported. But that’s the key to getting to the root of the problem. Here we see that when we change div to div, the div passed to our createElement changes from the string ‘div’ to a div class.

Of course, Div class is not defined in JavaScript, so we will report Div undefined. Knowing what the problem is, we can solve it. First we need to solve the undefined problem, so let’s create a Div class.

// Add it after createElment
class Div {}
Copy the code

Then we need to do a type check in createElement. If the type we encounter is a character type, we’ll do it the same way. If we encounter anything else, we instantiate the type passed in.

function createElement(type, attributes, ... children) {
  // Create the element
  let element;
  if (typeof type === 'string') {
    element = document.createElement(type);
  } else {
    element = new type();
  }

  // Hang the attribute
  for (let name in attributes) {
    element.setAttribute(name, attributes[name]);
  }
  // Hang all child elements
  for (let child of children) {
    if (typeof child === 'string') child = document.createTextNode(child);
    element.appendChild(child);
  }
  // Finally, our element is a node
  // So we can return directly
  return element;
}
Copy the code

Here we have another question: how can we make custom tags behave like normal HTML tags? In the latest version of the DOM standard, we can do this by registering the names and types of our custom tags.

However, in our current version of secure browsing, this is not recommended. So when using our custom Element, it is recommended that we write our own interface.

The first thing we need to do is create a tag class that will allow any tag to be mounted on our DOM tree just like the elements of our normal HTML tags.

It will contain the following methods:

  • mountTo()Create an element node to mount to laterparentOn the parent node
  • setAttribute()Attach all attributes to the element
  • appendChild()Attach all its children to the element

First we simply implement the mountTo method in our Div class. We also need to add setAttribute and appendChild methods, because in our createElement we have the logic to mount the child element of the attribute. If these two methods are not present, an error is reported. However, we will not implement the logic of these two methods at this time, and leave the method content blank.

class Div {
  setAttribute() {}
  appendChild() {}
  mountTo(parent) {
    this.root = document.createElement('div');
    parent.appendChild(this.root); }}Copy the code

It’s easy to create a div element node for the root attribute of the class, and then mount the node to the parent of the element. The parent is passed in as an argument.

We can then change our original body.appendChild code to mount our custom element class using the mountTo method.

// document.body.appendChild(a);
a.mountTo(document.body);
Copy the code

With the current code, we webpack to see the effect:

We can see that our Div custom element is properly mounted on the body. But the SPAN tag in the Div is not mounted. If we want it to work like a normal div, we need to implement our setAttribute and appendChild logic.

Let’s try the rest of the implementation logic together. We need to add a constructor to our Div class before we start writing setAttribute and appendChild. Here we can create the element and delegate it to root.

constructor() {
  this.root = document.createElement('div');
}
Copy the code

The setAttribute method is very simple, just use this.root and call setAttribute in the DOM API. AppendChild is the same. Our final code looks like this:

class Div {
  // constructor
  // Create a DOM node
  constructor() {
    this.root = document.createElement('div');
  }
  // Mount the attributes of the element
  setAttribute(name, attribute) {
    this.root.setAttribute(name, attribute);
  }
  // Mount the element's child elements
  appendChild(child) {
    this.root.appendChild(child);
  }
  // Mount the current element
  mountTo(parent) {
    parent.appendChild(this.root); }}Copy the code

Let’s webpack to see the effect:

We can see that both div and SPAN were successfully mounted on the body. It also proves that our homemade DIV works.

There is also a problem here because we end up calling a.mountto (), and if our variable a is not a custom element, but our normal HTML element, they won’t have mountTo.

So here we also need to add a Wrapper class to our normal elements to keep them in the standard format of our element classes. Also known as the standard interface.

Let’s start by writing an ElementWrapper class, whose contents are essentially the same as our Div. There are only two differences

  1. When creating a DOM node, you can pass in the current element nametypeGo to our constructor and use this type to create our DOM node
  2. AppendChild cannot be used directlythis.root.appendChildAppendChild’s logic needs to be changed because all normal tags are changed to our custom classchild.mountTo(this.root)
class ElementWrapper {
  // constructor
  // Create a DOM node
  constructor(type) {
    this.root = document.createElement(type);
  }
  // Mount the attributes of the element
  setAttribute(name, attribute) {
    this.root.setAttribute(name, attribute);
  }
  // Mount the element's child elements
  appendChild(child) {
    child.mountTo(this.root);
  }
  // Mount the current element
  mountTo(parent) {
    parent.appendChild(this.root); }}class Div {
  // constructor
  // Create a DOM node
  constructor() {
    this.root = document.createElement('div');
  }
  // Mount the attributes of the element
  setAttribute(name, attribute) {
    this.root.setAttribute(name, attribute);
  }
  // Mount the element's child elements
  appendChild(child) {
    child.mountTo(this.root);
  }
  // Mount the current element
  mountTo(parent) {
    parent.appendChild(this.root); }}Copy the code

Another problem here is that text nodes are not converted to our custom class when encountered. So we also need to write a text node called TextWrapper.

class TextWrapper {
  // constructor
  // Create a DOM node
  constructor(content) {
    this.root = document.createTextNode(content);
  }
  // Mount the attributes of the element
  setAttribute(name, attribute) {
    this.root.setAttribute(name, attribute);
  }
  // Mount the element's child elements
  appendChild(child) {
    child.mountTo(this.root);
  }
  // Mount the current element
  mountTo(parent) {
    parent.appendChild(this.root); }}Copy the code

With these element class interfaces in place, we can override our createElement logic. Replace our original document.createElement and Document.CreateTextNode with instantiations of New ElementWrapper(type) and new TextWrapper(content).

function createElement(type, attributes, ... children) {
  // Create the element
  let element;
  if (typeof type === 'string') {
    element = new ElementWrapper(type);
  } else {
    element = new type();
  }

  // Hang the attribute
  for (let name in attributes) {
    element.setAttribute(name, attributes[name]);
  }
  // Hang all child elements
  for (let child of children) {
    if (typeof child === 'string') 
		child = new TextWrapper(child);
    element.appendChild(child);
  }
  // Finally, our element is a node
  // So we can return directly
  return element;
}
Copy the code

Then we webpack and have a look.

Without incident, our entire element is normally mounted on top of the body. The same thing works if we change our Div back to Div.

Of course, we wouldn’t normally write a Div element that doesn’t make any sense. Here we’ll write the name of our component, for example, Carousel, a component of the wheel map.

Complete code – useful to you, give me a ⭐️ bar, thank you!


I am Sanzuo from the public account “Technology Galaxy”, a technologist who is reshaping knowledge. See you next time.