WHAT is Webpack
Webpack is a static module packaging tool that combines each module in our project into one or more bundles.
Some core concepts related to this (configured in the webpack.config.js configuration file) :
- Entry: The entry to the package that tells Webpack
Where to start
- Output: Tells Webpack
Where to print it
The bundle it creates, andHow to name
These documents. The default value for the main output file is./dist/main.js, and the other generated files are placed in the./dist folder by default - Loader: Webpack can only understand JavaScript and JSON files, which are available in webPack out of the box. But we don’t just have these two types of files in our project, there are other types, what do we do with them? Loader is webpack capable
Handle other types of files
Can process and convert other types of files into valid modules, such as SASS to CSS, or ES6 to ES5. Specifically,Loader is a function
Is responsible for converting the input source text to a specific text output. They can be either asynchronous or synchronous. When configured, loader has two attributes: the test attribute (to identify which files will be converted) and the use attribute (to define which loader will be used for conversion).
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js',},// Loader configuration rules are defined in module.rules
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader'}].// Use raw-loader conversion before packaging when encountering "path parsed to '.txt' in require()/import statement"}};Copy the code
- Plugin: Plugins are used for
Change the behavior of the build process
Such as automatically uploading static resources to the cloud, removing duplicate files from the output, injecting environment variables, etc. Specifically, a plug-in is an instance of a class that can be hooked up to the lower-level API of WebPack. To use a plug-in, simply require it and add it to the Plugins array. Most plug-ins can be customized with options, or the same plug-in can be used multiple times in a configuration file for different purposes. In this case, you need to create an instance of the plug-in using the new operator.
If you need to convert Vue/React code, SASS, or some other translation language, use loader. If you need to tweak JavaScript, or work with files in a certain way, use the plugin.
const HtmlWebpackPlugin = require('html-webpack-plugin'); // Install via NPM
const webpack = require('webpack'); // Used to access built-in plug-ins
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader'}],},plugins: [new HtmlWebpackPlugin({ template: './src/index.html'})].// html-webpack-plugin can generate an HTML file and automatically inject all generated bundles into this file
};
Copy the code
WHY: Webpack
To do a good job, he must sharpen his tools.
When we write large complex projects where we decouples business logic through modularity, can there be a way that not only lets us write modules, but also supports any module format (at least prior to ESM) and can handle various resources at the same time?
That’s why we use WebPack, which packages JavaScript applications (ESM and CommonJS support) that can be extended to support many different static resources.
HOW does Webpack work
After knowing WHAT & WHY, we are most curious about HOW? How exactly does Webpack work? Let’s start with a simple example. What does WebPack compile into our code? Since the output of webpack compilation and packaging is essentially the same for complex projects as it is for simple lines of code, we can start with the simplest case and explore the secrets of packaging output.
1. A simple example
Create index.js from SRC:
const sayHello = require('./hello.js')
console.log(sayHello('hui ho'))
Copy the code
hello.js
:
module.exports = function (name) {
return 'hello ' + name
}
Copy the code
Importing packaged files in index.html:
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script type="text/javascript" src=".. /dist/bundle.js" charset="utf-8"></script>
</body>
</html>
Copy the code
After executing the package command, open dist/bundle.js(the output file name specified) :
/ * * * * * * / (() = > { // webpackBootstrap
/ * * * * * * / var __webpack_modules__ = ({
/ * * * / "./src/hello.js":
/ *! * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/hello.js ***! \ * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((module) = > {
eval("module.exports = function (name) {\n return 'hello ' + name\n}\n\n//# sourceURL=webpack://webpack/./src/hello.js?");
/ * * * / }),
/ * * * / "./src/index.js":
/ *! * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/index.js ***! \ * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) = > {
eval("const sayHello = __webpack_require__(/*! . / hello * / \ ". / SRC/hello. Js \ ") \ nconsole log (sayHello (' hui ho)) \ n \ n / / # sourceURL = webpack: / / webpack /. / SRC/index. Js?");
/ * * * / })
/ * * * * * * / });
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * * * * / // The module cache
/ * * * * * * / var __webpack_module_cache__ = {};
/ * * * * * * /
/ * * * * * * / // The require function
/ * * * * * * / function __webpack_require__(moduleId) {
/ * * * * * * / // Check if module is in cache
/ * * * * * * / var cachedModule = __webpack_module_cache__[moduleId];
/ * * * * * * / if(cachedModule ! = =undefined) {
/ * * * * * * / return cachedModule.exports;
/ * * * * * * / }
/ * * * * * * / // Create a new module (and put it into the cache)
/ * * * * * * / var module = __webpack_module_cache__[moduleId] = {
/ * * * * * * / // no module.id needed
/ * * * * * * / // no module.loaded needed
/ * * * * * * / exports: {}
/ * * * * * * / };
/ * * * * * * /
/ * * * * * * / // Execute the module function
/ * * * * * * / __webpack_modules__[moduleId](module.module.exports, __webpack_require__);
/ * * * * * * /
/ * * * * * * / // Return the exports of the module
/ * * * * * * / return module.exports;
/ * * * * * * / }
/ * * * * * * /
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * * * * /
/ * * * * * * / // startup
/ * * * * * * / // Load entry module and return exports
/ * * * * * * / // This entry module can't be inlined because the eval devtool is used.
/ * * * * * * / var __webpack_exports__ = __webpack_require__("./src/index.js");
/ * * * * * * /
/ * * * * * * / })()
;
Copy the code
There are a lot of comments, so let’s extract the core:
// The outermost layer is an immediate execute function IIFE
(() = > { // webpackBootstrap
// Defines a __webpack_modules__ object with a file name attribute whose value is the corresponding file content
var __webpack_modules__ = ({
"./src/hello.js": ((module) = > {
eval("module.exports = function (name) {\n return 'hello ' + name\n}\n\n//# sourceURL=webpack://webpack/./src/hello.js?");
}),
"./src/index.js":
eval("const sayHello = __webpack_require__(/*! . / hello * / \ ". / SRC/hello. Js \ ") \ nconsole log (sayHello (' hui ho)) \ n \ n / / # sourceURL = webpack: / / webpack /. / SRC/index. Js?");
})
// The module cache
// Cache module
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if(cachedModule ! = =undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}};// Execute the module function
__webpack_modules__[moduleId](module.module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
var __webpack_exports__ = __webpack_require__("./src/index.js"); }) ();Copy the code
We can draw the conclusion that:
- The webpack result is one
IIFE
, commonly called webpackBootstrap; - In the package result, a module loading function webpack_require is defined;
- First use the webpack_require loading function to load the entry module./ SRC /index.js.
- The loading function webpack_require uses the closure variable webpack_module_cache to cache the loaded module results.
2. Working principle
It can be summarized as the following figure [from network] :
- First, WebPack reads the ones defined by the developer in the project
webpack.config.js
Configuration file, or obtain the necessary parameters from shell statements to complete the configuration reading. - Next, the required WebPack plug-in is instantiated and the plug-in hooks are mounted on the WebPack event stream so that the plug-in has the ability to alter the output during the appropriate build process.
- Also, based on the entry file defined by the configuration,
Import file
(There can be more than one)To start with dependency collection
: Compiles all dependent files. This compilation process depends on Loader. Different types of files are parsed according to different Loaders defined by the developer. Compiled content parsing generates AST static syntax trees, analyzes file dependencies, and implements modularized implementation with WebPack’s own loader. - After the above process is complete, the results are produced and packaged into the appropriate directory according to the developer’s configuration.
Some core concepts:
- AST: Our old friend, abstract syntax trees, are JS objects that help us with code analysis.
- Compiler: Instances of the Compiler object contain
Complete WebPack configuration
There is only one compiler instance globally. When the plug-in is instantiated, it receives a Compiler object through which it passesYou can access the internal environment of Webpack
. - Compilation object: When WebPack is running in development mode, a new Compilation object is created whenever a file change is detected. This object contains information about the current module resources, build resources, changed files, and so on. In other words,
All build data generated during the build process is stored on this object
It controls every step of the build process and provides many event callbacks for plug-ins to extend.
4. How about implementing a mini-Webpack yourself
First we’ll install a few packages to use:
- Babel /parser: Used to parse input code into abstract syntax trees (AST)
- Babel /traverse: Used to traverse abstract syntax trees (AST) of input
- @babel/core: the core module of Babel, which performs code conversion
- @babel/ PRESET -env: ES6 + code can be automatically converted to ES5 based on the target browser or runtime environment configured
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D
Copy the code
1. Core code
First we need a configuration file:
// mini-webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js'.mode: 'development'.output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'}}Copy the code
Then create a new bundle.js that implements the core package:
const options = require("./mini-webpack.config");
const fs = require("fs");
const path = require('path');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
// Create a MiniWebpack class
class MiniWebpack {
constructor(options) {
/ / configuration items
this.options = options;
}
// Parse the entry file
parse(filename) {
// Use Node's core module fs to read files
const fileBuffer = fs.readFileSync(filename, "utf-8");
// Get AST from @babel/parser
const ast = parser.parse(fileBuffer, { sourceType: "module" });
const deps = {}; // To collect dependencies
/ / traverse the AST
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
const absPath = ". /"+ path.join(dirname, node.source.value); deps[node.source.value] = absPath; }});// Code conversion
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]});const moduleInfo = { filename, deps, code };
return moduleInfo;
}
// Collect the module dependency graph
analyse(file) {
// Define the dependency graph
const depsGraph = {};
// Get the entry information first
const entry = this.parse(file);
const temp = [entry];
for (let i = 0; i < temp.length; i++) {
const item = temp[i];
const deps = item.deps;
if (deps) {
// Iterate over module dependencies to get module information recursively
for (const key in deps) {
if (deps.hasOwnProperty(key)) {
temp.push(this.parse(deps[key]));
}
}
}
}
temp.forEach((moduleInfo) = > {
depsGraph[moduleInfo.filename] = {
deps: moduleInfo.deps,
code: moduleInfo.code,
};
});
return depsGraph;
}
// Generate the code for final execution
generate(graph, entry) {
// is an immediate function
return `(function(graph){
function require(file) {
var exports = {};
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports
}
require('${entry}')
})(${graph}) `;
}
// Specify the directory for the packaged files
outputFile(output, code) {
const {path: dirPath, filename} = output;
const outputPath = path.join(dirPath, filename);
if(! fs.existsSync(dirPath)){ fs.mkdirSync(dirPath) } fs.writeFileSync(outputPath, code,'utf-8')}// String all the packaging logic together
bundle(){
const {entry, output} = this.options
const graph = this.analyse(entry)
const graphStr = JSON.stringify(graph)
const code = this.generate(graphStr, entry)
this.outputFile(output, code)
}
}
// Instantiate a MiniWebpack
const miniWebpack = new MiniWebpack(options)
// Run the packaging logic
miniWebpack.bundle()
Copy the code
2. Here’s an example
The entire directory is shown in the figure below:
Create a few new files in the SRC directory:
index.html
:
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script type="text/javascript" src=".. /dist/bundle.js" charset="utf-8"></script>
</body>
</html>
Copy the code
Index. Js:
import minus from './minus.js'
import add from './add.js'
console.log('3-1 = > > > > > >', minus(3.1))
console.log('3 + 1 = > > > > > >', add(3.1))
Copy the code
minus.js
:
// minus.js
export default (a, b) => { return a - b }
Copy the code
add.js
:
// add.js
export default (a, b) => { return a + b }
Copy the code
Run the bundle logic with the node bundle.js command.
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"."build": "node bundle.js"
},
Copy the code
Execute NPM run build:
OK! We have successfully packaged the file: dist/bundle.js. We have introduced this file in index. HTML.
Success! 🎉
Implement a simple loader
A loader has a single responsibility to complete the smallest unit of file conversion. A source file may need to go through multi-step conversion before it can be used normally. For example, the Sass file is output to CSS through Sas-Loader, and then the content is sent to CSS-Loader for processing. Even the output content of CSS-Loader is also sent to style-loader for processing. Convert to JavaScript code loaded through a script. Use as follows:
module.exports = {
// ...
module: {
rules: [{test: /\.less$/,
use: [
{
loader: "style-loader".// Create a style node from the JS string
},
{
loader: "css-loader".// Compile CSS to comply with the CommonJS specification
},
{
loader: "less-loader".// Compile less to CSS},],},],},};Copy the code
When we call multiple Loaders in series to convert a file, each loader is executed in chained order. In Webpack, when there are multiple matching Loaders for the same file, follow the following principles:
- The loader execution sequence is the same as the configuration sequence
On the contrary
That is, the last loader configured is executed first and the first loader is executed last. - The first loader that executes receives the contents of the source file as a parameter, and the other Loaders receive the return value of the previous loader that executes as a parameter. The last loader executed returns the final result.
When configuring loader, you can add some configurations, such as:
module: {
rules: [{test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader".options: {
plugins: ["dynamic-import-webpack",}},]; }Copy the code
How to get the options passed in here? Using the loader-utils module:
const loaderUtils = require("loader-utils");
module.exports = function (source) {
// Get developer configuration options
const options = loaderUtils.getOptions(this);
// ...
return content;
};
Copy the code
Loader is essentially a function, we only care about its input and output.
For example, if we want to implement a replace-loader, this loader can be customized to replace the string we configured to replace, using and configuring as follows:
const path = require("path");
module.exports = {
// ...
module: {
rules: [{test: /\.js$/,
use: {
loader: "replaceLoader".options: {
str: "let".replaceStr: "const".// Replace let with const},},},],},};Copy the code
In replaceLoader. In js:
const loaderUtils = require("loader-utils");
module.exports = function (source) {
/ / get the options
const options = loaderUtils.getOptions(this);
return source.replace(options.str, options.replaceStr);
};
Copy the code
Back to the previous example:
let sayHello = require('./hello.js')
console.log(sayHello('hui ho'))
Copy the code
After packing:
The loader takes effect
Implement a simple plugin
Webpack has a stream of events, and many events are triggered during the life cycle of a WebPack build. At this point, various plug-ins registered under development can listen for events related to themselves as needed. After the event is captured, the compiled output can be changed through the API provided by WebPack when appropriate.
So the difference between loader and plugin is obvious:
- Loader is one
converter
To perform a simple file conversion operation. - The plugin is a
extender
, it enriches webpack itself. After the loader process ends, during the whole process of Webpack packaging, Weback Plugin does not operate files directly, but works based on the event mechanism, listens to some events in the process of Webpack packaging, and modifies the packaging results.
The Webpack plug-in is a JavaScript object with the Apply method. The Apply method is called by the Webpack Compiler and the Compiler object is accessible throughout the compile life cycle.
Since plug-ins can carry parameters/options, you must pass a new instance to the plugins property in the WebPack configuration.
So the plugin we implement should be a class or constructor.
Let’s simply implement an HtmlWebpackPlugin, generate an HTML file after packaging, and introduce the JS file generated by packaging in this file.
// my-html-webpack-plugin.js
const pluginName = "MyHtmlWebpackPlugin";
class MyHtmlWebpackPlugin {
apply(compiler) {
const filename = compiler.options.output.filename;
compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) = > {
const content = ` <! DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Webpack</title> <script defer src="./${filename}"></script>
</head>
<body>
</body>
</html>
`;
// Insert this file as a new file resource into the WebPack build:
compilation.assets["index.html"] = {
source: function () {
return content;
},
size: function () {
returncontent.length; }}; callback(); }); }}module.exports = MyHtmlWebpackPlugin;
Copy the code
Configure in webpack.config.js:
const path = require("path");
const MyHtmlWebpackPlugin = require('./my-html-webpack-plugin');
module.exports = {
mode: "development".entry: {
main: "./src/index.js",},output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",},// ...
plugins: [new MyHtmlWebpackPlugin()],
};
Copy the code
Execute the package command:
Success!
👉 : 🐶 compiler – hooks