Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to implement a template engine based on DOM in javascript

2025-02-28 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

This article focuses on "how to implement a DOM-based template engine in javascript". Interested friends may wish to have a look. The method introduced in this paper is simple, fast and practical. Let's let the editor take you to learn how to implement a DOM-based template engine in javascript.

Preface

Before we begin, let's take a look at the final effect:

Const compiled = Compile (`Hey?, {{greeting}}`, {greeting: `Hello World`,}); compiled.view / / = > `World`, Hello World`

Compile

Implementing a template engine is actually implementing a compiler, like this:

Const compiled = Compile (template: String | Node, data: Object); compiled.view / / = > compiled template

First, let's take a look at how Compile is implemented internally:

/ / compile.js / * template compiler * * @ param {String | Node} template * @ param {Object} data * / function Compile (template, data) {if (! (this instanceof Compile)) return new Compile (template, data); this.options = {}; this.data = data; if (template instanceof Node) {this.options.template = template } else if (typeof template = = 'string') {this.options.template = domify (template);} else {console.error (`"template" only accept DOM node or string template`);} template = this.options.template Walk (template, (node, next) = > {if (node.nodeType = 1) {/ / compile element node this.compile.elementNodes.call (this, node); return next ();} else if (node.nodeType = 3) {/ / compile text node this.compile.textNodes.call (this, node) } next ();}); this.view = template; template = null;} Compile.compile = {}

Walk

From the above code, you can see that the constructor of Compile mainly does one thing-traversing template, and then does different compilation operations by judging different node types. Here, we will not introduce how to traverse template. If you do not understand, you can directly look at the source code of the walk function. Let's focus on how to compile these different types of nodes, taking compiling element nodes with node.nodeType = = 1 as an example:

/ * compile element node * * @ param {Node} node * / Compile.compile.elementNodes = function (node) {const bindSymbol = `:`; let attributes = [] .slice.call (node.attributes), attrName = ``, attrValue =``, directiveName = ``; attributes.map (attribute = > {attrName = attribute.name; attrValue = attribute.value.trim () If (attrName.indexOf (bindSymbol) = = 0 & & attrValue! =') {directiveName = attrName.slice (bindSymbol.length); this.bindDirective ({node, expression: attrValue, name: directiveName,}); node.removeAttribute (attrName) } else {this.bindAttribute (node, attribute);}});}

Oh, forget to mention, here I refer to the instruction syntax of Vue, that is, in the attribute name with the colon: (of course, it can also be any other symbol you like), you can directly write the expression of JavaScript, and then will also provide several special instructions, such as: text,: show and so on to do some different operations on the element.

In fact, the function does only two things:

Traverse all the attributes of the node and do different operations by judging the type of the attribute. The criterion is whether the attribute name is a colon: it begins and the value of the attribute is not empty.

Bind the appropriate instruction to update the property.

Directive

Second, take a look at how Directive is implemented internally:

Import directives from'. / directives'; import {generate} from'. / compile/generate'; export default function Directive (options = {}) {Object.assign (this, options); Object.assign (this, directives [this.name]); this.beforeUpdate & & this.beforeUpdate (); this.update & & this.update (generate (this.expression) (this.compile.options.data));}

Directive did three things:

Registration instruction (Object.assign (this, directives [this.name]))

Evaluate the actual value of the instruction expression (generate (this.expression) (this.compile.options.data))

Update the calculated actual value to DOM (this.update ()).

Before introducing the instruction, take a look at its usage:

Compile.prototype.bindDirective = function (options) {new Directive ({... options, compile: this,});}; Compile.prototype.bindAttribute = function (node, attribute) {if (! hasInterpolation (attribute.value) | | attribute.value.trim () = =') return false This.bindDirective ({node, name: 'attribute', expression: parse.text (attribute.value), attrName: attribute.name,});}

BindDirective makes a very simple encapsulation of Directive, accepting three required attributes:

Node: the node currently compiled, which is used to update the current node in the update method of Directive

Name: the name of the instruction currently bound to distinguish which instruction updater is used to update the view

Expression: the expression of JavaScript after parse.

Updater

Within Directive, we register different instructions through Object.assign (this, directives [this.name]);, so the value of the variable directives might look like this:

/ / directives export default {/ / directive `: show` show: {beforeUpdate () {}, update (show) {this.node.style.display = show? `block`: `none` },}, / / directive `: text` text: {beforeUpdate () {}, update (value) {/ /...},},}

So if the name of an instruction is show, then Object.assign (this, directives [this.name]); is equivalent to:

Object.assign (this, {beforeUpdate () {}, update (show) {this.node.style.display = show? `block`: `none`;},})

Indicates that for the instruction show, the instruction updater will change the display value of the element style to achieve the corresponding function. So you will find that after the whole compiler structure is designed, if we want to expand the function, we can simply write the instruction updater. Here we take the instruction text as an example:

/ / directives export default {/ / directive `: show` / / show: {...}, / / directive`: text` text: {update (value) {this.node.textContent = value;},},}

Have you found that writing an instruction is actually very simple, and then we can use our text instruction like this:

Const compiled = Compile (``, {greeting: `World`,}); compiled.view / / = > `Hey?, Hello World`

Generate

At this point, there is actually a very important point not mentioned, that is, how to render the real data of data to the template, such as Hey?, {{greeting}} how to render into Hey?, Hello World. The real data of the expression can be calculated by the following three steps:

Parse Hey?, {{greeting}} into JavaScript expressions such as' Hey?,'+ greeting

Extract the dependent variables and get the corresponding values in the data

Use new Function () to create an anonymous function to return this expression

* the final calculated data is returned by calling this anonymous function and updated to the view through the update method of the instruction.

Parse text

/ / reference: https://github.com/vuejs/vue/blob/dev/src/compiler/parser/text-parser.js#L15-L41 const tagRE = /\ {(?:.) +)\}\} / g; function parse (text) {if (! tagRE.test (text)) return JSON.stringify (text); const tokens = []; let lastIndex = tagRE.lastIndex = 0; let index, matched While (matched = tagRE.exec (text)) {index = matched.index; if (index > lastIndex) {tokens.push (text.slice (text.slice (lastIndex, index));} tokens.push (matched [1] .trim ()); lastIndex = index + matched [0] .length;} if (lastIndex

< text.length) tokens.push(JSON.stringify(text.slice(lastIndex))); return tokens.join('+'); } 该函数我是直接参考 Vue 的实现,它会把含有双花括号的字符串解析成标准的 JavaScript 表达式,例如: parse(`Hi {{ user.name }}, {{ colon }} is awesome.`); // =>

'Hi' + user.name +','+ colon +'is awesome.'

Extract dependency

We will use the following function to extract the variables that may exist in an expression:

Const dependencyRE = / "[^"] * "|'[^'] *'|\.\ w* [a Methoz Zhuan _]\ w* |\ w* [a-zA-Z$_]\ wspell: | (\ w* [a-zA-Z$_]\ w*) / g Const globals = ['true',' false', 'undefined',' null', 'NaN',' isNaN', 'typeof',' in', 'decodeURI',' decodeURIComponent', 'encodeURI',' encodeURIComponent', 'unescape',' escape', 'eval',' isFinite', 'Number',' String', 'parseFloat',' parseInt',]; function extractDependencies (expression) {const dependencies = [] Expression.replace (dependencyRE, (match, dependency) = > {if (dependency! = = undefined & & dependencies.indexOf (dependency) =-1 & & globals.indexOf (dependency) =-1) {dependencies.push (dependency);}}); return dependencies;}

After matching the possible variable dependencies through the regular expression dependencyRE, there are some comparisons, such as whether it is a global variable, and so on. The effect is as follows:

ExtractDependencies (`typeof String (name) = = 'string' & &' Hello'+ world +'!'+ hello.split (''). Join ('') +'.`); / / = > ["name", "world", "hello"]

This is exactly what we need. Typeof, String, split, and join are not dependent variables in data, so they do not need to be extracted.

Generate

Export function generate (expression) {const dependencies = extractDependencies (expression); let dependenciesCode =''; dependencies.map (dependency = > dependenciesCode + = `var ${dependency} = this.get ("${dependency}"); `); return new Function (`data`,` ${dependenciesCode} return ${expression}; `);}

The purpose of extracting variables is to generate the corresponding string assigned by the variable in the generate function for ease of use in the generate function, for example:

New Function (`data`, `var name = data ["name"]; var world = data ["world"]; var hello = data ["hello"]; return typeof String (name) = = 'string' & &' Hello'+ world +'!'+ hello.split ('). Join (') +'; `); / / will generated: function anonymous (data) {var name = data ["name"]; var world = data ["world"] Var hello = data ["hello"]; return typeof String (name) = = 'string' & &' Hello'+ world +'!'+ hello.split (''). Join (') +';}

In that case, we only need to pass in the corresponding data when calling the anonymous function to get the desired result. Looking back on the previous Directive code, it should be clear at a glance:

Export default class Directive {constructor (options = {}) {/ /. This.beforeUpdate & this.beforeUpdate (); this.update & & this.update (generate (this.expression) (this.compile.data));}}

Generate (this.expression) (this.compile.data) is the value we need after the expression is evaluated by this.compile.data.

Compile text node

We only talked about how to compile the element node of node.nodeType = 1, so how does the text node compile? in fact, after understanding the content mentioned above, the compilation of the text node cannot be simpler:

/ * compile text node * * @ param {Node} node * / Compile.compile.textNodes = function (node) {if (node.textContent.trim ()) = =') return false; this.bindDirective ({node, name: 'text', expression: parse.text (node.textContent),});}

By binding the text instruction and passing in the parsed JavaScript expression, the actual value of the expression is calculated inside the Directive and the update function of text is called to update the view to complete the rendering.

Each instruction

So far, the template engine has only achieved relatively basic functions, but the most common and important list rendering function has not yet been implemented, so we are now going to implement one: each instruction to render a list. It may be noted here that it cannot be implemented in accordance with the ideas of the previous two instructions, but should be considered from a different point of view. List rendering is actually equivalent to a "sub-template". The variables exist in the "local scope" of the data received by the each instruction, which may be abstract and go directly to the code:

/ /: each updater import Compile from 'path/to/compile.js'; export default {beforeUpdate () {this.placeholder = document.createComment (`: Each`); this.node [XSS _ clean] .replaceChild (this.placeholder, this.node);}, update () {if (data & &! Array.isArray (data)) return; const fragment = document.createDocumentFragment () Data.map ((item, index) = > {const compiled = Compile (this.node.cloneNode (true), {item, index,}); fragment.appendChild (compiled.view);}); this.placeholder [XSS _ clean] .replaceChild (fragment, this.placeholder);},}

Before update, we first remove the node of: each from the DOM structure, but we should note that it is not possible to remove it directly, but to insert a node of comment type as a placeholder in the removed position, so that after we render the list data, we can find the original position and insert it into the DOM.

How to compile this so-called "subtemplate"? first, we need to traverse the data of type Array received by the each instruction (currently only this type is supported, of course, you can also add support for the Object type, the principle is the same) Secondly, we compile the template for each item of data in the list and insert the rendered template into the created document fragment. When all the whole list is compiled, we replace the placeholder of the comment type we just created with document fragment to complete the rendering of the list.

At this point, we can use the following: each directive:

Compile (`{{item.content}}`, {comments: [{content: `Just World.`,}, {content: `Just Awesome.`,}, {content: `WOW, Just WOW! `,}],})

Will be rendered as:

Hello World. Just Awesome. WOW, Just WOW!

In fact, if you are careful, you will find that the item and index variables used in the template are actually two key values of the data value in the Compile (template, data) compiler in the each update function. So it's also very simple to customize these two variables:

/ /: each updater import Compile from 'path/to/compile.js'; export default {beforeUpdate () {this.placeholder = document.createComment (`: Each`); this.node [XSS _ clean] .replaceChild (this.placeholder, this.node); / / parse alias this.itemName = `item`; this.indexName = `index`; this.dataName = this.expression If (this.expression.indexOf ('in')! =-1) {const bracketRE = /\ (?:. |\ n) +?) / g; const [item, data] = this.expression.split ('in'); let matched = null If (matched = bracketRE.exec (item)) {const [item, index] = matched [1] .split (','); index? This.indexName = index.trim ():'; this.itemName = item.trim ();} else {this.itemName = item.trim ();} this.dataName = data.trim ();} this.expression = this.dataName }, update () {if (data & &! Array.isArray (data)) return; const fragment = document.createDocumentFragment (); data.map ((item, index) = > {const compiled = Compile (this.node.cloneNode (true), {[this.itemName]: item, [this.indexName]: index,})) Fragment.appendChild (compiled.view);}); this.placeholder [XSS _ clean] .replaceChild (fragment, this.placeholder);},}

In this way, we can customize the item and index variables of the each instruction through (aliasItem, aliasIndex) in items. The principle is to parse the expression of the each instruction in beforeUpdate, extract the relevant variable name, and then the above example can be written like this:

Compile (`{{comment.content}}`, {comments: [{content: `Just World.`,}, {content: `Just Awesome.`,}, {content: `WOW, Just WOW! `,}],})

Conclusion

Here, in fact, a relatively simple template engine is implemented, of course, there are many places can be improved, such as: class,: style,: if or: src and other instructions you can think of, add these functions are very simple.

At this point, I believe you have a deeper understanding of "how to implement a DOM-based template engine in javascript". You might as well do it in practice. Here is the website, more related content can enter the relevant channels to inquire, follow us, continue to learn!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report