background

In today’s world of single-page apps, many people seem to think of simple graphing as something other than a technology. For cutting pages, writing static sites are about to sniff. In fact, it is not so, write static page is the front entry of the basic work, is the embodiment of solid basic skills. And in the work, we also need to develop some static official website. What we need to do is figure out how to better develop static pages.

Crooked horse because of work reason recently, need to be hosted in the content management system of the official website type website for migration. Since to do again, that engineering nature is indispensable, webpack, CSS precompilation and so on. This leads to a better development experience.

Because of the static official website, when using WebPack, you need to specify multiple entries and different templates for each entry. With the help of the HTML-webpack-plugin, you can specify templates for different entries, as follows:

// ...

entrys.map(entryName= > {

htmlWebpackPlugins.push(

new HtmlWebpackPlugin({

template: `${entryName}.html`.

filename: `${entryName}.html`.

chunks: ['vendor'.'common', entryName],

}),

)

})

Copy the code

By iterating through the entry list, we can specify different templates for different entries.

When using frameworks such as Vue/React, we have long been used to extracting and reusing components during development. So in this kind of purely static website development, we must also want to reuse as much as possible within the page of the common part, such as header, footer, copyright and other content.

These are already well established in server-side rendering development mode and can be easily done with template engines such as Nunjucks /pug/ EJS etc.

The template in webpack-html-plugin uses EJS by default. Since ejS is officially used, let’s look for solutions in this direction as well.

After the attempt, it was found that EJS could not achieve the following functions well:

  • Include is supported, but the format of the pass parameter is not elegant. It can be used as follows:

    Index. Ejs:

    <h1><% = require('./header.ejs') ({title:'Page name'}) %></h1>

    Copy the code

    The header. Ejs:

    <title><% = title% ></title>

    Copy the code
  • Image SRC processing within a file is not supported

There’s no way to manipulate images, so there’s no fun to be had. The crooked horse had to find another way, and the final solution was not ideal. On their own to achieve a simple function, convenient and easy to use HTML including loader – webpack-html-include-loader.

Webpack-html-include-loader contains the following core functions:

  • Include HTML files are supported
  • Support for nested include
  • Support for incoming parameter & variable resolution
  • Support for custom syntax tags

This article introduces each of the four core functions in turn and explains their implementation. After reading this article, you will learn how to use this loader and learn a bit about webPack Loader development. Please feel free to comment if you have any questions.

One, the implementation of the basic inclusion function

In order to organize static pages flexibly, one of the most important features is include. Let’s start by looking at how to implement inclusion.

Suppose, by default, we use the following syntax tags for include:

<% - include("./header/main.html") % >

Copy the code

To implement this function, it is actually relatively simple. The loader of Webpack can accept the content of the original module or the result of the previous loader. Here our loader directly processes the content of the original module, that is, the content string.

Therefore, if you want to achieve the include function, you only need to match the include syntax through the re, and then replace it with the corresponding file content globally. The overall code is as follows

// index.js

const path = require('path')

const fs = require('fs')



module.exports = function (content) {

const defaultOptions = {

includeStartTag: '< % -'.

includeEndTag: '% >'.

}



const options = Object.assign({}, defaultOptions)



const {

includeStartTag, includeEndTag

} = options



const pathRelative = this.context



const pathnameREStr = '[-_.a-zA-Z0-9/]+'

// contains the block matching re

const includeRE = new RegExp(

`${includeStartTag}\\s*include\\((['|"])(${pathnameREStr})\\1\\)\\s*${includeEndTag}`.

'g'.

)



return content.replace(includeRE, (match, quotationStart, filePathStr,) => {

const filePath = path.resolve(pathRelative, filePathStr)

const fileContent = fs.readFileSync(filePath, {encoding: 'utf-8'})

// Add files to dependencies to implement hot updates

this.addDependency(filePath)

return fileContent

})

}

Copy the code

Where const pathRelative = this.context is provided by the WebPack Loader API. Context indicates the directory where the current file resides. With this property, we can get the exact path of the included file and then get the contents of the file for replacement.

You may also have noticed that the code calls this.adddependency (filePath), which adds files to dependencies so you can listen for changes to the files.

The rest of the logic is relatively simple. If you are not familiar with the string replace, you are recommended to read the basic document related to regex written by Ruan Yifeng.

Ok, so now we have implemented the basic HTML inclusion functionality. However, we are obviously not satisfied with this, and we should support nested contains anyway, right? Let’s take a look at how to implement nested containment.

Two, improve the flexibility of inclusion: nested inclusion

Now that we’ve implemented the basic inclusion functionality, it’s easy to implement nested containment. Just do it recursively. Because it is called recursively, we extract the replacement logic for the include syntax tag into a function replaceIncludeRecursive.

Here’s the code:

const path = require('path')

const fs = require('fs')



+ // Replace include recursively

+ function replaceIncludeRecursive({

+ apiContext, content, includeRE, pathRelative, maxIncludes,

+ {})

+ return content.replace(includeRE, (match, quotationStart, filePathStr) => {

+ const filePath = path.resolve(pathRelative, filePathStr)

+ const fileContent = fs.readFileSync(filePath, {encoding: 'utf-8'})

+

+ apiContext.addDependency(filePath)

+

+ if(--maxIncludes > 0 && includeRE.test(fileContent)) {

+ return replaceIncludeRecursive({

+ apiContext, content: fileContent, includeRE, pathRelative: path.dirname(filePath), maxIncludes,

+})

+}

+ return fileContent

+})

+}



module.exports = function (content) {

const defaultOptions = {

includeStartTag: '<%-',

includeEndTag: '%>',

+ maxIncludes: 5,

}



const options = Object.assign({}, defaultOptions)



const {

includeStartTag, includeEndTag, maxIncludes

} = options



const pathRelative = this.context



const pathnameREStr = '[-_.a-zA-Z0-9/]+'

const includeRE = new RegExp(

`${includeStartTag}\\s*include\\((['|"])(${pathnameREStr})\\1\\)\\s*${includeEndTag}`,

'g',

)



- return content.replace(includeRE, (match, quotationStart, filePathStr,) => {

- const filePath = path.resolve(pathRelative, filePathStr)

- const fileContent = fs.readFileSync(filePath, {encoding: 'utf-8'})

- // Add files to dependencies to implement hot updates

- this.addDependency(filePath)

- return fileContent

-})

+ const source = replaceIncludeRecursive({

+ apiContext: this, content, includeRE, pathRelative, maxIncludes,

+})

+ return source

}

Copy the code

The logic is very simple, put the original replacement logic into replaceIncludeRecursive function, call the method in the main logic. In addition, webpack-html-include-loader sets the maximum number of nesting layers to 5 by default.

So far, we have implemented the flexible include function. If you remember, ejS includes originally supported passing in parameters to replace some of the content in the include template. We can call them variables.

Pass in parameters & variable parsing

Again, set a default syntax mark for the passed arguments as follows: <% -include (“./header/main.html”, {“title”: “home “}) %>.

When the file is included, the parameters are passed in the format of the JSON serialization string.

JSON serialized string is a string that is processed by loader. We need to convert string parameters into parameter objects, which need to be parsed by json. parse method.

Variable inserts are then performed in the included file using <%= title %>.

So to implement variable resolution, we need to implement the resolution of the passed parameter, and then replace the corresponding variable tag.

The code is as follows:

const path = require('path')

const fs = require('fs')



// Replace include recursively

function replaceIncludeRecursive({

- apiContext, content, includeRE, pathRelative, maxIncludes,

+ apiContext, content, includeRE, variableRE, pathRelative, maxIncludes,

{})

- return content.replace(includeRE, (match, quotationStart, filePathStr) => {

+ return content.replace(includeRE, (match, quotationStart, filePathStr, argsStr) => {

+ // Parses the parameters passed in

+ let args = {}

+ try {

+ if(argsStr) {

+ args = JSON.parse(argsStr)

+}

+ } catch (e) {

+ apicontext.emiterror (new Error(' format Error passed in, JSON cannot be parsed into '))

+}



const filePath = path.resolve(pathRelative, filePathStr)

const fileContent = fs.readFileSync(filePath, {encoding: 'utf-8'})



apiContext.addDependency(filePath)



+ // Replace the variables in the current file first

+ const fileContentReplacedVars = fileContent.replace(variableRE, (matchedVar, variable) => {

+ return args[variable] || ''

+})



- if(--maxIncludes > 0 && includeRE.test(fileContent)) {

+ if(--maxIncludes > 0 && includeRE.test(fileContentReplacedVars)) {

return replaceIncludeRecursive({

- apiContext, content: fileContent, includeRE, pathRelative: path.dirname(filePath), maxIncludes,

+ apiContext, content: fileContentReplacedVars, includeRE, pathRelative: path.dirname(filePath), maxIncludes,

})

}

- return fileContentReplacedVars

+ return fileContentReplacedVars

})

}



module.exports = function (content) {

const defaultOptions = {

includeStartTag: '<%-',

includeEndTag: '%>',

+ variableStartTag: '<%=',

+ variableEndTag: '%>',

maxIncludes: 5,

}



const options = Object.assign({}, defaultOptions)



const {

- includeStartTag, includeEndTag, maxIncludes

+ includeStartTag, includeEndTag, maxIncludes, variableStartTag, variableEndTag,

} = options



const pathRelative = this.context



const pathnameREStr = '[-_.a-zA-Z0-9/]+'

+ const argsREStr = '{(\\S+? \\s*:\\s*\\S+?) (,\\s*(\\S+? \\s*:\\s*\\S+?) +? *} '

const includeRE = new RegExp(

- `${includeStartTag}\\s*include\\((['|"])(${pathnameREStr})\\1\\)\\s*${includeEndTag}`,

+ `${includeStartTag}\\s*include\\((['|"])(${pathnameREStr})\\1\\s*(?:,\\s*(${argsREStr}))?\\s*\\)\\s*${includeEndTag}`,

'g',

)



+ const variableNameRE = '\\S+'

+ const variableRE = new RegExp(

+ `${variableStartTag}\\s*(${variableNameRE})\\s*${variableEndTag}`,

+ 'g',

+)



const source = replaceIncludeRecursive({

- apiContext: this, content, includeRE, pathRelative, maxIncludes,

+ apiContext: this, content, includeRE, variableRE, pathRelative, maxIncludes,

})



return source

}

Copy the code

When an error occurs during loader processing, oader API emitError can be used to output error information.

At this point, we have implemented all the major features that webpack-html-include-loader should have. To make it more user-friendly for users, let’s extend the implementation of custom syntax tags.

4. Custom syntax tags

We can pass in custom options by specifying loader options, or by embedding query. This article starts with the Webpack-html-plugin as an example. We changed the webpack-html-plugin code to include <#- :

entrys.map(entryName => {

htmlWebpackPlugins.push(

new HtmlWebpackPlugin({

- template: `${entryName}.html`,

+ template: `html-loader! webpack-html-include-loader? includeStartTag=<#-! ${entryName}.html`,

filename: `${entryName}.html`,

chunks: ['vendor', 'common', entryName],

}),

)

})

Copy the code

Among them, webpack-html-include-loader solves the problem of file inclusion, and htML-loader solves the processing of images and other resources. If you also have similar needs, can make reference.

Implementing custom syntactic tags is as simple as passing custom tags into the re dynamically. The only thing you need to be careful about is escaping the passed value.

In regular expressions, need a backslash escapes, a total of 12 characters: ^,., [, $, (,), |, *, +,?, {and \ \. If the RegExp method is used to generate the regular object, escape the need to use two forward slashes, because internal will escape a string.

The code logic is as follows:

module.exports = function (content) {

const defaultOptions = {

includeStartTag: '<%-',

includeEndTag: '%>',

variableStartTag: '<%=',

variableEndTag: '%>',

maxIncludes: 5,

}

+ const customOptions = getOptions(this)



+ if(! isEmpty(customOptions)) {

+ // Escape the custom options that require regular escape

+ Object.keys(customOptions).filter(key => key.endsWith('Tag')).forEach((tagKey) => {

+ customOptions[tagKey] = escapeForRegExp(customOptions[tagKey])

+})

+}



- const options = Object.assign({}, defaultOptions)

+ const options = Object.assign({}, defaultOptions, customOptions)

// ...

}

Copy the code

The escapeForRegExp logic is as follows, where $& is the string matching the regular:

Escape special characters in the re

function escapeForRegExp(str) {

return str.replace(/[.*+?^${}()|[\]\\]/g.'\ \ $&')

}

Copy the code

The getOptions method is provided by loader-utils. It also provides many additional tools, which are very useful in loader development.

5. Some other logic

In addition to the core functions above, there is more detailed logic, such as validation of custom options with schema-utils. Some general functions of custom are not covered here. Interested students can look at the source code. The link is as follows: github.com/verymuch/we… Welcome criticism + star.

conclusion

This article introduces the main functions and development ideas of Webpack-html-include-loader. I hope you can gain something after reading this article, and have a simple understanding of the development of Webpack Loader.


If you like, please scan the code to follow my public account. I will accompany you to read regularly and share some other front-end knowledge.