instructions

This article is suitable for those who still stay in the simple use of Webpack, complicated words may go to the Internet to find some information or configuration partners, take you to re-understand the “true face” of Webpack, the basic part here will not be described, mainly to complete the usual use of you may be more confused. Of course, when writing this article, I am also studying Webpack together with everyone, so any omissions are welcome to correct.

Webpack overview

This article is based on the webpack5 document to bring you a new understanding of Webpack, said here have to mention, friends when looking at the document, can see English documents or try to see English documents, not to see, learning English also want to see, not for other reasons, is looking at English documents when it is very handsome, is it?

I’m just kidding. In fact, all of you should know that English documents are updated quickly. If you use the latest Webpack, there may be some difficult problems if you use it as in Chinese documents.

Webpack, in my opinion, is a factory, can we use the various files, js, CSS, img, and so on resources together, and it also helps us to simplify the development process, improve the performance of application, and other advantages, so in the face of such benefits, our front end must also can’t let go, so we must be serious to treat him.

Concepts

The following concepts should be understood first. If not, you can open the webpack document above to get a general idea of what these concepts mean so that you can move on.

  • entry
  • output
  • loaders
  • plugins
  • mode

Configuration

After a preliminary understanding of the above concept, I believe that you can easily, but still hope that you follow me to complete the following exercise, first let’s create a project, project directory structure is as follows:

|-- webpack-learning
    |-- index.html
    |-- package.json
    |-- webpack.config.js
    |-- src
        |-- a.js
        |-- index.js
Copy the code

We have written some JS files and basic webpack configuration in webpack:

Okay, that’s about it, so let’s talk about the configuration options in Webpack.config.js.

entry

Entry is the entry file that specifies the application, but what type of value can the entry file be? When we open the document, we find that the value of entry can be string, String array, object, or even a function that returns object, String, or string array. Therefore, we have a clear idea of the type to be used to configure entry objects.

output & output.library & output.libraryExport & output.libraryTarget

It should be noted that output is a pair with our entry, but entry can be configured with various types, while our output can only be configured with one type, namely object, and the following error will be reported for other types:

Output. library is a value that is exposed by our entry file and is used in conjunction with output.libraryTarget, usually used in libraries or UI framework libraries. When we specify this option:

module.exports = {
    ...
    library: 'MyLibrary'
}
Copy the code

Remember that the bundle we packaged is a self-executing function that will put the code from our module in this self-executing function. Here we add this configuration. After we packaged the bundle, let’s look at the bundle.

<script>
  console.log(MyLibrary)
</script>
Copy the code

Then take a look at the console output:

const log = require('./a')
module.exports = log
Copy the code

Then print MyLibrary in an HTML file and there you have it:

Return to output.libraryExport, which defines a key for the exported variable as an object property:

module.exports = {
    ...
    library: 'MyLibrary'.libraryExport: 'default'
}
Copy the code

In this case, we need to export variables in the entry file like this:

. exports.default = logCopy the code

So when we refer to the entry file, we can get the log method directly, otherwise if you use this method, you need to say:

MyLibrary.default()
Copy the code

Output. libraryTarget is used with output.library. By default, this configuration is var, which means that the entry file exports a variable. Indicates that variables imported from the entry file can be mounted to these global objects, so the imported variables can be used as follows:

this[MyLibrary] = log
window[MyLibrary] = log
global[MyLibrary] = log
Copy the code

Another configuration case is how modularized a variable exported from this entry should be used. There are several configurations:

  • amd
  • umd
  • commonjs
  • commonjs2

Library is ignored when commonjs2 is set to module. Exports of module. Exports by default. The exported module looks like this:

exports['MyLibrary'] = log
Copy the code

The reason for mentioning these export modules is that when I configured the externals option of webpack to exclude the packaging of the library, I could not exclude the packaging of the element-UI component library until I saw the source code of Element. The variable name of the imported library was called Element. I wonder if any of you have ever had the same problem as me.

module.rules

  1. Loaders can be configured in module.rules, but it is necessary to know that there are three loaders configuration methods, loaders is only one of the configuration files, the other two. One way is to import a file and then specify the loaders resolution file that is required:
import Styles from 'style-loader! css-loader? modules! ./styles.css'
Copy the code

There is also a loaders command that specifies loaders to be loaded when executing the package command:

webpack --module-bind jade-loader --module-bind 'css=style-loader! css-loader'
Copy the code

optimization

Since webpack4.0, there is an optimization option that overrides the default optimization option by default depending on the mode you choose.

devtool

Devtool development tool configuration, this option is a time when we say that the development of debugging, before I have been useless to figure out what exactly is what, is to find information to understand, is actually when we enable webpack, all of the code will be processed, compression, etc., so if there is any error message, We can’t find the exact location of the error code, so WebPack gives us the option to configure how to debug the packaged code. There are several configuration options:

  • Generate a.map mapping file to locate faulty rows and columns (source-map)
  • Does not generate.map mapping files to locate faulty rows and columns (eval-source-map)
  • Generate a.map mapping file to locate faulty lines (cheap-module-source-map)
  • Does not generate a.map map file to locate faulty lines (cheap-module-eval-source-map)

Each of these cases has an impact on the speed of the package build, as well as the size of the package file, so the configuration is up to us.

stats & performance

Stats and Performance are options that you probably don’t know much about. The former is the information that pops up in the command line during packaging or Webpack execution. If you don’t need this information, use the configuration set to None to disable all information. The latter is the prompt for the command line interface when the package file is too large or other conditions affect the performance. If you do not need it, you can set Hints: False to turn off the performance reminder message.

watch & watchOptions

The watch option means that Webpack will listen for the file you want to pack. If the file changes, WebPack will automatically compile the package. Note that this option is for packaging, if you use webpack-dev-server it is hot update mechanism. WatchOptions provides detailed configuration of the watch options, such as anti-shake, polling time, and the configuration of folders to ignore listening, etc.

If the above points are clearly understood, I think you may already be familiar with Webpack. The reason why I put forward these points is that I did not understand these before systematic learning, so I bring them out and tell you.

loaders

Webpack can only parse JS files, so we need these loaders to handle non-JS files.

css-loader & style-loader

CSS – loader and style – loader. These two loaders are commonly used to process styles. Generally, we will put them together to process CSS files. The difference between them is that the former generates the link tag and inserts it into the document header, while the latter generates the style tag and inserts it into the document. From right to left, so it can be configured as follows:

{
    test: /\.css$/,
    loaders: ['style-loader'.'css-loader']}Copy the code

This represents the sequence in which we use CSS-loader first, then style-loader again.

url-loader & file-loader

These two loaders are used to process image file paths and are often used. The former means that some image resources will be converted to Base64 encoding format, so that in the case of small image volume, there is no need to request server to reduce the number of requests. Meanwhile, urL-loader can configure an options to control the size of resources to be converted to Base64. If the size is larger than this, Use file-loader to request resources normally:

cache-loader & thread-loader

The two loaders listed here are related to webpack optimization, one is cache, the other is thread, their function is obvious, use cache-loader to cache the converted content, so that the next time if the file is not modified, I’m going to stick with what’s in the cache. Thread-loader enables multi-threading to execute packaged builds, speeding up the build speed. It should be noted that these two loaders should be used in loaders with time-consuming operations, such as babel-loader, which can significantly optimize the construction speed of Webpack. If loader conversion is not time-consuming, these two loaders are not needed. After all, more loaders will naturally increase the running time.

Plugins

html-webpack-plugin

How to configure HTML files with webpack-dev-server? Webpack-dev-server: import import files from webpack-dev-server: import import files from webpack-dev-server

So when we package, we’ll actually find that both entry files are imported from the packaged dist directory

// index.html
<script type="text/javascript" src="index.js"></script><script type="text/javascript" src="main.js"></script></body>
// main.html
<script type="text/javascript" src="index.js"></script><script type="text/javascript" src="main.js"></script></body>
Copy the code

This is obviously not what we want, so we need to use the chunks option in the HTML-webpack-plugin:

So let’s look at the HTML file after we pack it up:

// main.html
<script type="text/javascript" src="main.js"></script>
// index.html
<script type="text/javascript" src="index.js">
Copy the code

If you want an HTML file to import two chunks at the same time, you can add a string of chunks to the chunks array to add the chunks to the HTML file.

ignorePlugin

This plug-in for webpack bring a plug-in, mainly applied to ignore certain third-party library package, for example, we may often be used in a time plugin my moment, this bag is bigger, because it contains a lot of language packs, and we may only need one or several languages, but they all introduced, This is definitely not what we want, so we can use this ignorePlugin to ignore the language package in the moment:

Here we capture the volume comparison before and after the ignorePlugin configuration:

We can see that the volume is nearly 500KB smaller, which has a very obvious effect on our optimization.

DllPlugin & DllReferencePlugin

These two plug-ins are also webpack plug-ins, from the official documentation we can also find these two plug-ins, their function is used to separate third-party libraries, DLL is dynamic-link library, namely the meaning of Dynamic link library.

To use the plugin, first install the vue dependencies and then modify our index.js file:

import Vue from 'vue'

new Vue({
  render: h= > h('h1'.'Dynamic link Library')
}).$mount('#root')
Copy the code

After packaging, you can see our package file size:

Then add our plugin to the configuration file, here we will use the configuration library mentioned above, if you are not clear about it, you can review it.

Add a webpack configuration file, webpack.library.js:

This configuration file is specifically used to package our vue as a link library. Add our packaged commands to package.json:

"dist": "webpack --config webpack.library.js"
Copy the code

When we run our dist command, we can find two additional files in the dist directory, one is the vue.js packaged with our vue, and the other is our module directory manifest.json file, so that our dynamic link library is packaged. The next step is how to get Webpack to look for dependencies in the dynamic link library instead of node_modules when we introduce Vue.

This brings us to the DllReferencePlugin, which we added to the original WebPack configuration file. Note that the two plugins are in two separate configuration files.

module.exports = {
    ...
    new webpack.DllReferencePlugin({
        // Specify the path to our directory file
        manifest: path.resolve(__dirname, 'dist'.'manifest.json')})}Copy the code

After this configuration, when we import vUE, WebPack will go to the json file configuration path to find our dynamic link library. Here we have one final step, which is to manually import our dynamic link library in our packaged HTML file:

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

Note that the script tag needs to be placed on top of the js file we introduced, and when we pack it we can see again that the packaged file has eliminated the vue packaging and has been reduced in size:

The optimization of webpack

That’s more or less how webPack is optimized, but there are a lot of details that we might not notice

Here can refer to the big guy’s article, said very detailed, so no longer repeat.

Webpack has a deep understanding

Hand write a simple Webpack

Let’s start by creating a new project in the following directory

|-- webpack-implement
    |-- package-lock.json
    |-- package.json
    |-- bin
    |   |-- main.js
    |-- lib
        |-- Compiler.js
        |-- main.ejs
Copy the code

Here in the bin directory main.js is our entry file, if you have written their own scaffolding, should be more clear. We configure our bin command in package.json

"bin": {
    "qpack": "./bin/main.js"
  }
Copy the code

This means we can use qpack as a command to launch our entry file. We add the following to the entry file

The code is actually very simple, just get our configuration file, then load a Compiler class, all the processing methods in this class, before writing this class, let’s first look at the webpack file looks like. After we pack the webpack-learning project we learned earlier, let’s look at the bundle.js package:

(function (modules) {
  var installedModules = {};
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false.exports: {}}; modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);
    module.l = true;
    return module.exports;
  }
  return __webpack_require__(__webpack_require__.s = "./src/index.js"); ({})"./src/a.js":
      (function (module, exports) {
        eval("module.exports = function (str) {\r\n console.log(str)\r\n}\n\n//# sourceURL=webpack:///./src/a.js?");
      }),
    "./src/index.js":
      (function (module, exports, __webpack_require__) {
        eval("const log = __webpack_require__(/*! ./a.js */ \"./src/a.js\")\r\n\r\nlog('aaaaaaaaaaaaaaaaa')\r\n\n\n//# sourceURL=webpack:///./src/index.js?"); })});Copy the code

This is the part we cleaned up after removing comments and some code that we won’t use this time. So you can see that this is a self-executing function, and we’re passing in an object, and that object is our dependency, and key is the file path that we’re relying on, and value is a self-executing function, and we’re wrapping our module code in eval, We’ll follow this code to implement our own WebPack packaging.

Let’s start with our Compiler implementation class:

module.exports = class Compiler {
  constructor (config) {
    // Initialize the configuration
    this.config = config
    // Import file name
    this.entryId = null
    // Modules depend on objects
    this.modules = {}
    // Form the final package file to pass in the parameters of the immediately executed function
    this.assets = {}
    // Get the entry in the configuration file
    this.entry = config.entry
    // The current working directory
    this.root = process.cwd()
  }
}
Copy the code

We also need to have a run method for our main.js to run:

module.exports = class Compiler {
    / / a little
  run () {
    // Build module dependencies and generate code
    this.buildModule(path.resolve(this.root, this.entry), true)
    // Puts the built code generation in the specified input directory
    this.emitFile()
  }
}
Copy the code

The key is how we build our module dependencies, recursively going through each module from our entry file:

/* ** @param modulePath {string} The path name of the module passed in ** @param isEntry {Boolean} Whether the current path is an entry file */
buildModule (modulePath, isEntry) {
  // Get the contents of the module, i.e. the code
  const source = this.getSource(modulePath)
  // According to the source code we analyzed above, we need to obtain the relative path of the module
  const moduleName = `. /${path.relative(this.root, modulePath)}`
  if (isEntry) {
    this.entryId = moduleName
  }
  // We need to use the contents of the module and the module path for ast parsing to generate the source code we want and module dependencies
  const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName))
  this.modules[moduleName] = sourceCode
  // If there are dependencies in the module, recursively traverse the module
  dependencies.forEach(dep= > {
    this.buildModule(path.join(this.root, dep), false)})}Copy the code

Our buildModule method starts at the entry file and iterates through the module recursively to generate the source code we saw earlier, along with objects like {‘./ SRC /a.js’: (function(){})}. Now look at how the parse method builds:

// Babylon has been replaced by @babel/parser to generate AST
const babelParser = require('@babel/parser')
/ / traverse the AST
const traverse = require('@babel/traverse').default
// To determine the type or generate the specified node type
const t = require('@babel/types')
// The AST generates code
const generator = require('@babel/generator').default

parse (source, parentPath) {
  / / generated AST
  let ast = babelParser.parse(source)
  // Records depend on arrays
  let dependencies = []
  / / traverse the AST
  traverse(ast, {
    CallExpression (p) {
      const node = p.node
      // find the method name called require
      if (node.callee.name === 'require') {
        // Change the variable name we specified
        node.callee.name = '__webpack_require__'
        // Get the require method argument
        let moduleName = node.arguments[0].value
        // Use the path to form the desired form, such as./ SRC /index.js
        moduleName = moduleName + (path.extname(moduleName) ? ' ' : '.js')
        moduleName = `. /${path.join(parentPath, moduleName)}`
        // Add require's module to the dependency array
        dependencies.push(moduleName)
        Based on the new moduleName we generated, change argments in the AST to moduleName
        node.arguments = [t.stringLiteral(moduleName)]
      }
    }
  })
  // Generate the source code
  const sourceCode = generator(ast).code
  return { sourceCode, dependencies }
}
Copy the code

The parse method returns the modified code we need, along with the dependency array. The AST conversion code is also used, and in fact Babel uses the above steps to convert a higher version of the syntax to a lower version. Finally, we use parsed code and dependencies to generate the packaged file we want. For simplicity, instead of concatenating our bundle files with strings, we use EJS to render, creating our main.ejs file first:

generateFile () {
  // Get the path of our package file according to the configuration file
  const main = path.join(this.config.output.path, this.config.output.filename)
  // Get the render template
  const templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
  // Return the source of our bundle after rendering
  const code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules })
  // When packaging multiple files, store the files in assets
  this.assets[main] = code
  // Write the source code to a file
  fs.writeFileSync(main, code)
}
Copy the code

Now that we have our own webpack basic packing functionality done, let’s delete the previous dist directory and execute the packing command we wrote, qpack, and take a look at the result:

As you can see, the generated file is basically the same as the one we saw before. Let’s create a new index.html file in the dist directory to test whether the packaged file works. I won’t show you the results here, but you can follow the above steps to try it out for yourself.

The realization of the loader

Now that we’re done with packing, let’s look at how to add a Loader in our own Webpack. First of all, we get the contents of the module in the getSource method of the Compiler class, so our loader must be added in this method, and determine the file type according to the re. If it matches, the corresponding Loader method will be called:

getSource (path) {
  // Read the contents of the file
  let content = fs.readFileSync(path, 'utf8')
  // Get the configuration item in Rules and iterate through the Loader in the configuration item
  const rules = this.config.module.rules
  rules.forEach(rule= > {
    const { test, use } = rule
    // The loader execution sequence is bottom-up, so we get the last loader
    let len = use.length - 1
    if (test.test(path)) {
      const normalLoader = (a)= > {
        // Import the last loader, if there is any loader after, the length is reduced, recursive traversal rule
        const loader = require(use[len--])
        content = loader(content)
        if (len >= 0) {
          normalLoader()
        }
      }
      normalLoader()
    }
  })
  return content
}
Copy the code

Add loader method we have written, next is to write our own loader method implementation. Loader is a method, the parameter is the content of the imported file, and then convert the content of the file to return, according to this idea, we first create a folder loaders in our webpack-learning, Create two of our loader methods under the folder, style-loader and less-loader. Then add the two loaders to the WebPack configuration:

module.exports = {
  // ...
  module: {
    rules: [{test: /\.less$/.use: [
          path.resolve(__dirname,'loaders'.'style-loader.js'),
          path.resolve(__dirname,'loaders'.'less-loader.js']}]},}Copy the code

< loader > < loader > < loader > < loader >

const less = require('less')

module.exports = function loader (code) {
  let css
  // Convert less files to CSS files using the render method provided by less
  less.render(code, function(err, code) {
    css = code.css
  })
  // The converted content \n will be translated, so it needs to be replaced with \\n
  css = css.replace(/\n/g.'\\n')
  return css
}
Copy the code

This returns a CSS content that will be used as a style-loader argument to execute the style-loader method:

module.exports = function loader (code) {
  const styleStr = `
    const style = document.createElement('style')
    style.innerHTML = The ${JSON.stringify(code)}
    document.head.appendChild(style)
  `
  return styleStr
}
Copy the code

< span style = “box-sizing: border-box! Important; word-wrap: break-word! Important;”

body {background: red; }Copy the code

Then execute our command to open the bundle.js file and see:

There is a test.less dependency, which is the content we just converted using loader, so that our bundle execution will add a STlye tag to the HTML to make the style work.

Understand plugins implementations

Before we understand plugin implementation, we need to understand a library called Tapable, which is the base class of various hook implementations in WebPack. Tapable contains many types of hooks, including synchronous hooks, asynchronous hooks, asynchronous serial hooks and so on. We don’t want to focus on this library, but we need to understand how plugin is implemented, so we will first consider tapable as a publish-subscribe dependency, and we will need to insert these hooks into it during webPack packaging. Lifecycle hooks packaged as Webpack.

Here we simply use the SyncHook hook, and first modify the Compiler constructor:

const {
  SyncHook
} = require('tapable')

class Compiler {
    constructor (config) {
    / / to omit...
    // These are some of the lifecycle hooks we defined
    this.hooks = {
      entryOptions: new SyncHook(),
      compile: new SyncHook(),
      afterCompile: new SyncHook(),
      afterPlugins: new SyncHook(),
      run: new SyncHook(),
      emit: new SyncHook(),
      done: new SyncHook()
    }

    const plugins = this.config.plugins
    if (Array.isArray(plugins)) {
      plugins.forEach(plugin= > {
        // The plugin needs to provide an apply method, which we pass in as a parameter
        plugin.apply(this)})}// After the plug-in completes execution, call the lifecycle methods we defined above
    this.hooks.afterPlugins.call()
  }
}
Copy the code

We also put these defined hooks into the Compiler execution process:

run () {
    this.hooks.run.call()
    this.hooks.compile.call()
    this.buildModule(path.resolve(this.root, this.entry), true)
    this.hooks.afterCompile.call()
    this.emitFile()
    this.hooks.emit.call()
    this.hooks.done.call()
}
Copy the code

Create a plugins folder and create a new DonePlugin JS file. In webpack, plugins need to be instantiated with new. And there will be an apply method, so our plug-in will look something like this:

module.exports = class DonePlugin {
  apply (compiler) {
    // Execute a callback to penalize the hook after the file is generated
    compiler.hooks.emit.tap('DonePlugin', () = > {console.log('Compile complete ~~~~~')}}}Copy the code

So when we run the qpack command, we see the console output

Ok, our plug-in is called successfully! Of course, this is just a simple call to a synchronous hook SyncHook, the actual Webpack contains a lot of complex hooks, interested in which you can study the source code, you can learn more things.

At the end

This is the end of the study of Webpack, and it took nearly two weeks to write. If any students need these source codes, please contact me. I will sort them out and put them on my Github. This article is also to open your eyes to learn webpack, more usage can refer to the link below and community leaders share!

Refer to the link

  • Webpack4 in 10 days
  • Webpack official documentation