So, although I have consulted a lot of blog columns and books in advance, many of the tutorials are several years ago, and many of the plug-ins or loaders have been upgraded and iterated and are no longer used in the past, so the new use methods need to be manually queried one by one. If there is any mistake, please advise. Gratitude!
I thought you should have known this before
1. What is Webpack, and what is its current position on the front end
Needless to say, there is no front-end project today that doesn’t use packaging tools, and webPack is the king of packaging tools, so there is no reason not to understand it.
2. Commonjs and ES6 Module should not be understood
Let’s talk about it briefly
Commonjs is used in Node, require is used for imports and module.exports is used for exports
The ES6 Module uses import and export
- Name export
- By default, export default is exported, and a variable named default is exported. Therefore, you do not need to declare variables like named export, but directly export the value. There can only be one file
Note that when importing a module, CommonJS gets a copy of the exported value; In the ES6 Module, it is a dynamic mapping of values, and the mapping is read-only.
There’s also some AMD and UMD module stuff, so let’s stop there. After all, this is paving the way for WebPack
3. Relationship between entry&Chunk & Bundle
I think this is pretty straightforward
4 Core concepts in WebPack
entry
Used to specify the address (relative address) of the webpack package, such as:
Single entryentry:'./src/index.js'
Or:entry:{
main:'./src/index.js'
} Multiple entryEntry: { main:'./src/index.js'. other:'./src/other.js' } Copy the code
output
Specifies the address and file name of the output file after packaging. The file address is absolute
A single fileoutput:{
filename:'bundle.js'. path:path.join(__dirname,'dist')
}
Multiple filesoutput:{ filename:'[name].js'. path:path.join(__dirname,'dist') } Copy the code
mode
Specifies the current build environment
There are three main options
- production
- development
- none
Setting mode automatically triggers some of webpack’s built-in functions, default to None if not written
loaders
Webpack recognizes.json,.js, modules by default, and other modules that we need loaders to help us put into the dependency graph
It is essentially a function that takes the source file as an argument and returns the converted result
plugins
Plugins can do what we need to do when WebPack is running at a certain stage (WebPack uses tapable to construct a lot of life cycles so that we can use plug-ins to do the right thing for us at the right time). For example, clean-webpack-plugin will delete the original output file under DIST before we pack.
One: basic use
1.1 Processing HTML, CSS, JS
usewebpack-dev-server
We also want our packaged files to be launched on a local server, and Webpack-dev-server is one such thing.
Install: NPM I webpack-dev-server -d
A new configuration of webpack.config.jsdevServer
attribute
As follows:
devServer: {
port: 3000. contentBase: './dist' // The address of the service (i.e. the location of our output file)
open: true // Automatically open the browser
Compress:true / / gzip compression
} Copy the code
Add one to package.json to simplify the commanddev
The command
You can also create a package command, but I prefer NPX
{
"name": "webpack-test02". "version": "1.0.0". "description": "". "main": "index.js". "scripts": { "build":"webpack". "dev": "webpack-dev-server". }, } Copy the code
usehtml-webpack-plugin
As you may have noticed above, there’s not a single HTML file. It doesn’t matter if the server is started. Then we can write an HTML template in the file (just generate the skeleton) and use the HTMl-webpack-plugin to package the template in the dist directory and import the output bundl.js
Install: NPM I html-webpack-plugin-d
Use:
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'.// Template file address
filename: 'index.html'.// Specify the name of the packaged file
hash: true.// It can also generate a hash value }), ]. There are also the following optionsminify: { // Compress the packed HTML file removeAttributeQuotes: true.// Delete attribute double quotes collapseWhitespace: true // Fold an empty line into a single line } Copy the code
With CSS
The basic processing
Two loaders need to be installed, csS-loader (for handling @import syntax in CSS) and style-loader (for inserting CSS into the head tag)
Install: NPM I CSS-loader style-loader -d
Use:
module: {
rules: [
{
test: /\.css$/. use: ['style-loader'.'css-loader']
}, ] } Insert the CSS in front of the head tag each time. To ensure that our own Settings in the template can be overridden later module: { rules: [{ test: /\.css$/. use: [{ loader: 'style-loader'. options: { insert: function insertAtTop(element) { var parent = document.querySelector('head'); // eslint-disable-next-line no-underscore-dangle var lastInsertedElement = window._lastElementInsertedByStyleLoader; if(! lastInsertedElement) { parent.insertBefore(element, parent.firstChild); } else if (lastInsertedElement.nextSibling) { parent.insertBefore(element, lastInsertedElement.nextSibling); } else { parent.appendChild(element); } // eslint-disable-next-line no-underscore-dangle window._lastElementInsertedByStyleLoader = element; }, } }, 'css-loader' ] }, ]} Copy the code
Out of the CSS
Use the mini-CSs-extract-plugin
Install: NPM I mini-CSS-extract-plugin-d
You don’t need to use style-loader anymore
Use:
const MinniCssExtractPlugin = require('mini-css-extract-plugin')
plugins: [
new HtmlWebpackPlugin({
// Specify a template file
template: './src/index.html'. // Specify the name of the output file filename: 'index.html'. // Add hash to resolve cache }), new MinniCssExtractPlugin({ // Specify the name of the output file filename: 'main.css' }) ]. module: { rules: [{ test: /\.css$/. use: [ MinniCssExtractPlugin.loader,// Essentially create a style tag and import the output CSS address 'css-loader'. ] }, ] } Copy the code
Compressing CSS and JS
Compress the CSS file output above
The plugin optimize- CSS -assets-webpack-plugin is used to compress CSS, but uglifyjs-webpack-plugin is used to compress JS
NPM I optimize- CSS-assets -webpack-plugin uglifyjs-webpack-plugin -d
Use :(note that this plugin is no longer used in plugin, it is in the optimization property)
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true. parallel: true. sourceMap: true }), new OptimizeCSSAssetsPlugin({}) ] }, Copy the code
Adding a Vendor Prefix
You need a loader and a style tool: Postcss-loader autoprefixer
So what is postCss
It is a container for compiling plug-ins. It works by receiving source code for processing by the compiled plug-in, and then printing CSS.
Postcss-loader is the connector between PostCSS and Webpack. Postcss-loader can be used with or alone csS-loader. Notice If the postCSS-loader is used alone, it is not recommended to use the @import syntax in the CSS. Otherwise, redundant code will be generated.
PostCss also requires a separate configuration file, postcss.config.js
Also look at generating vendor prefixes using post-loader and Autoprefixer
Installation: NPM I postcss-loader autoprefixer -d
Use: webpack.config.js in the Rules of the Module
{
test: /\.css$/. use: [
MinniCssExtractPlugin.loader,
'css-loader'. 'postcss-loader'. ] }, Copy the code
Postcss. Config. Js
module.exports = {
plugins: [
require("autoprefixer") ({ overrideBrowserslist: ["last 2 versions"."1%" >]
})
] }; Copy the code
With less
Less less loader must be installed because less is used in the less-loader
Installation: NPM I less-loader less-d
Use:
{
test: /\.less$/. use: [
MinniCssExtractPlugin.loader,
'css-loader'. 'postcss-loader'. 'less-loader' ] } Copy the code
Sass similarly
Deal with js
Turn es6 es5
Install: NPM I babel-loader @babel/ core@babel /preset-env -d
Use:
{
test: /\.js$/. use: {
loader: 'babel-loader'. options: {
presets: [ '@babel/preset-env' ]. } } }, Copy the code
Es7 to class syntax
NPM I @babel/plugin-proposal-class-properties -d
Use:
{
test: /\.js$/. use: {
loader: 'babel-loader'. options: {
presets: [ '@babel/preset-env' ]. plugins: [ ["@babel/plugin-proposal-class-properties", { "loose" : true }] ] } } }, Copy the code
More details are available on Babel’s website
1.2 Image Processing
There are three ways to cite a picture
- Js is introduced to create image tags
- CSS is introduced into the url
- Img is introduced in HTML
Using the file – loader
The third case is not available, need to use the extra loader to see below
Install: NPM I file-loader -d
The essence of the reference is that it returns the address of an image, but that image is already in the bundle
Use: js
import './index.css'
import img from '.. /public/img/img01.jpg'
const image = new Image()
image.src = img
document.body.appendChild(image)
Copy the code
CSS
div{
p{
width: 100px;
height: 100px;
transform: rotate(45deg);
background-image: url('./img01.jpg'); } } Copy the code
Webpack. Config. Js
module.export={
module: {
rules: [
{
test: /\.(png|jpg|gif)$/. use: 'file-loader' } ] } } Copy the code
Use url – loader
As an upgraded version of file-loader, we can set the size of an image to base64, or use file-loader to package the original image
Install: NPM I url-loader -d
In the meantime, you can use htML-withimg-loader to process images in HTML, with the esModule property set to false. Otherwise the link is not the same as the address of the image in HTML
Install: NPM I html-withimg-loader -d
Use:
{
test: /\.html$/. use: 'html-withimg-loader'
}, {
test: /\.(png|jpg|gif)$/. use: { loader: 'file-loader'. options: { limit: 50 * 1024. loader: 'file-loader'. esModule: false. outputPath: '/img/'.// Package and output the address // publicPath: "// add the domain name path to the resource } } }, Copy the code
1.3 eslint
1.4 Common plug-ins
cleanWebpackPlugin
Automatically deletes files in the output directory each time you pack
Install: NPM I clean-webpack-plugin -d
Use:
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins:[
new CleanWebpackPlugin(),
]
Copy the code
copyWebpackPlugin
You can have some files that are not dependent on, but need to output inverted dist
Install: NPM I copy-webpack-plugin -d
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins:[
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [{
from: path.join(__dirname, 'public'), to: 'dist' }]. }), ] Copy the code
bannerPlugin
Add copyright to code header
A built-in plug-in for Webpack
Use:
const webpack = require('webpack')
plugins:[
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [{
from: path.join(__dirname, 'public'), to: 'dist' }]. }), new webpack.BannerPlugin('core by gxb'), ] Copy the code
Two: purchase price usage
2.1 Multi-page packaging
That is, multiple entries, each corresponding to a generation dependency tree
Configuration is very simple, with the entrance. The entry format is a chunkNmae: path
entry: {
index: './src/index.js'. other: './src/other.js'
},
output: {
filename: '[name].js'. path: path.join(__dirname, './dist') }, Copy the code
2.2 devtool :'source-map'
The mapping between the source code and the packaged code is the source-map that can be used to locate the source code block when problems occur in the code
It is primarily used in development environments
Configuration is as follows
devtool: 'source-map'
Copy the code
There are many types of devtool attribute values, but the most common ones are summarized below
- Source-map generates a map. Map file, which also locates rows and columns
- Cheap -module-source-map does not generate. Map files, which can be located to rows
- Eval-source-map does not generate. Map files that locate rows and columns
Note that CSS, LESS, AND SCSS need to be configured in the Loader option to locate the source code
Please note that this code has not been tested.
test:\/.css$\,
use: [ 'style-loader'. {
loader:'css-loader'. options: { sourceMap:true } } ] Copy the code
2.3 watch
Watch listener can also be configured in Webpack for constant packaging, that is, we do not need to input commands after modifying the code every time, we can directly save the code by C + S key.
The configuration is as follows:
module.exports = {
watch: true. watchOptions: {
poll: 1000.// How many times per second
aggregateTimeout: 500.// How many milliseconds will it take to trigger again to prevent the repeat button
ignored: /node_modules/ // Ignore constant listening } } Copy the code
2.4 resolve
We all know that webPack starts looking for all dependencies from the entry file, but when looking for third-party packages it always defaults to the main.js file as the entry file, but when looking for bootstrap we sometimes just need to reference it. Wouldn’t it be a bit wasteful to pack it all up
This is where Resole can specify how WebPack will find files for module objects
The configuration is as follows:
resolve:{
modules:[path.join('node_modules')].// Specify the directory to find
mainFields: ['style'.'main'].// Use main instead of style
extensions: ['.js'.'.css'.'.json']// If the file does not have a suffix, then look at the js file first, then look at the CSS...
alias:{
components: './src/components/'// import Button from 'components/ Button './ SRC /components/ Button } } Copy the code
👉Refer to this article for other options
2.5 Environment Splitting
Development and online environments typically require different configurations, so you can use Webpack-Merge to split configuration files into a base common, a development, and an online configuration file.
We will be able to specify the configuration files to be used for development packaging or production packaging.
Install: NPM I webpack-merge -d
It is written as follows:
basis
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
entry: './src/index.js'. plugins: [ new HtmlWebpackPlugin({ template: './src/index.html'. filename: 'index.html' }), new CleanWebpackPlugin(), ]. output: { filename: 'bundle.js'. path: path.join(__dirname, './dist') }, } Copy the code
Development: webpack. Dev. Js
const merge = require('webpack-merge')
const common = require('./webpack.config')
module.exports = merge(base, {
mode: 'development'. devServer: {}, devtool: 'source-map' }) Copy the code
Online: webpack. Prod. Js
const merge = require('webpack-merge');
const common = require('./webpack.config.js');
module.exports = merge(common, {
mode: 'production'. }); Copy the code
2.6 Handling Cross-domains
My previous article summed up the idea of cross-domain, that is, using proxies to handle cross-domain: same-origin policies exist only between browsers, not between servers. So we can first send the data to a proxy server
Chestnut: Send requests like a different domain server
const xhr = new XMLHttpRequest();
xhr.open('get'.'/api/user'.true);
xhr.send();
xhr.onload = function () {
console.log(xhr.response) } Copy the code
Server code:
const express = require('express')
const app = express()
app.get('/test', (req, res) => {
res.json({ msg: 11 })
})
app.listen(3001.function() { console.log('Start service'); }) Copy the code
Webpack.config.js configures the agent
devServer: {
port: 3000. proxy: {
'/api': {
target: 'http://localhost:3001'. pathRewrite: { '^/api': ' ' } } }, progress: true. contentBase: './dist'. open: true }, Copy the code
Three: optimization
3.1 noparse
When we reference some third-party packages, we don’t need to go into those packages to look for dependencies, because they are generally independent.
So there is no need to waste time parsing
The configuration is as follows:
module: {
noParse: /jquery/.// Don't resolve some package dependencies
rules:[]
}
Copy the code
3.2 include
&exclude
We can also specify the area to look for
Such as:
rules: [
{
test: /\.js$/. exclude: '/node_modules/'./ / node_modules ruled out
include: path.resolve('src'), // Look in the SRC file
use: { loader: 'babel-loader'. options: { presets: [ '@babel/preset-env'. ] } } } Copy the code
Use absolute paths instead of exclude
3.3 lgnorePlugin
The plug-in
Some third-party libraries have a lot of things we don’t need, like libraries with language packs. Generally, we only need to use the Chinese package in it, and we don’t need any other language packages.
So this is a plug-in that can be made available in Webpack
const webpack = require('webpack')
plugins: [
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
]
Copy the code
3.4 Multi-Threaded Packaging
Use the happypack plugin
Install NPM I happypack
Use:
const Happypack = require('happypack')
module: { rules: [{
test: /\.css$/. use: 'happypack/loader? id=css' }] } plugins:[ new Happypack({ id: 'css'. use: ['style-loader'.'css-loader'] }) ] Copy the code
3.5 Lazy Loading (On-demand Loading)
Something that we don’t import into our dependency tree right away, we import after we use it (kind of load on demand)
Here we need @babel/plugin-syntax-dynamic-import to parse and recognize the import() dynamic import syntax
Install: NPM i@babel /plugin-syntax-dynamic-import -d
Chestnut:
Index. In js:
const button = document.createElement('button')
button.innerHTML = 'button'
button.addEventListener('click', () = > { console.log('click')
import ('./source.js').then(data= > {
console.log(data.default) }) }) document.body.appendChild(button) Copy the code
source.js
export default 'gxb'
Copy the code
webpack.config.js
{
test: /\.js$/. include: path.resolve('src'),
use: [{
loader: 'babel-loader'. options: { presets: [ '@babel/preset-env'.]. plugins: [ '@babel/plugin-syntax-dynamic-import' ] } }] } Copy the code
Source.js modules add dependencies only when we click the button, otherwise they won’t be packaged immediately
3.6 hot update
Code updates are places where the page only updates that update rather than re-rendering the entire page, i.e. re-refreshing the page
Hot update plug-ins also come with Webpack
The configuration is as follows:
devServer: {
hot: true. port: 3000. contentBase: './dist'. open: true
}, plugins: [ new webpack.HotModuleReplacementPlugin() ] Copy the code
3.7 DllPlugin Dynamic link library
When we use Vue or React, we have to type the files again every time we pack them. But this third-party package doesn’t change at all when we compile it.
So what if we typed them out the first time we packed them, and then linked them into our file
These pre-packaged files are called DLLS and can be understood as a cache
How do you write a cache? The idea might go something like this
- Put things away first
- Get a mapping table, and then you need to look up things in the mapping table, and then you use them directly
It’s a bit of a hassle to configure, but there’s good news. DLL has been scrapped
But since I wrote the title, LET’s do it for understanding
Use the plugin 👉 autodlL-webpack-plugin directly
Install NPM I autoDLL-webpack-plugin
Use:
const path = require('path');
const AutoDllPlugin = require('autodll-webpack-plugin');
module.exports = {
plugins: [
// Configure the file to be packaged as a DLL new AutoDllPlugin({ inject: true.// Set this to true i.e. DLL bundles are inserted into index.html filename: '[name].dll.js'. context: path.resolve(__dirname, '.. / '), // The AutoDllPlugin context must be the same directory as package.json, otherwise the link will fail entry: { react: [ 'react'. 'react-dom' ] } }) ] } Copy the code
The HardSourceWebpackPlugin can be used after the Dll, which is faster and simpler than the Dll
Install: NPM I hard-source-webpack-plugin
use
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
plugins: [
new HardSourceWebpackPlugin()
] } Copy the code
3.8 Extracting common code
Especially in multi-entry files such as index.js that references A.js and other.js that references A.js, it is normal for a.js to be printed twice. B: well.. That’s not necessary
Extract:
module.exports = {
optimization: {
splitChunks: { // Split code blocks for multiple entries
cacheGroups: { / / cache group
common: { // Public module
minSize: 0.// The value is greater than 0k minChunks: 2.// How many times have you been quoted chunks: 'initial' // Where should I start, from the entrance } } } }, } Copy the code
Extract some third-party packages like jquery that are commonly referenced multiple times
optimization: {
splitChunks: { // Split code blocks for multiple entries
cacheGroups: { / / cache group
common: { // Public module
minSize: 0.// More than how much to pull away
minChunks: 2.// How many more times to use the extract extract chunks: 'initial' // Where do we start, at the beginning }, vendor: { priority: 1.// Add weight, (first remove third party) test: /node_modules/.// Select * from this directory minSize: 0.// More than how much to pull away minChunks: 2.// How many more times to use the extract extract chunks: 'initial' // Where do we start, at the beginning } } }, }, Copy the code
3.8 webpack
Some built-in optimizations
Automatically understand, there is nothing in it
Four:tapable
Handwriting knows
Tapable is a library similar to eventEmitter (NodeJS). Its main function is to control the publishing and subscription of various hook functions, and control the plug-in system of WebPack
4.1 the synchronization
4.1.1 SyncHook
The usage and implementation of
One of the most featureless hooks is synchronous serial
const { SyncHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new SyncHook(['name']), } } start() { this.hooks.arch.call('gxb') } tap() { this.hooks.arch.tap('node'.function(name) { console.log('node', name) }) this.hooks.arch.tap('react'.function(name) { console.log('react', name) }) } } const l = new Lesson() l.tap(); l.start() / / implementation class SyncHook { constructor() { this.tasks = [] } tap(name, cb) { let obj = {} obj.name = name obj.cb = cb this.tasks.push(obj) } call(... arg) { this.tasks.forEach(item= > { item.cb(... arg) }) } } const syncHook = new SyncHook() syncHook.tap('node', name => { console.log('node', name) }) syncHook.tap('vue', name => { console.log('vue', name) }) syncHook.call('gxb') Copy the code
4.1.2 SyncBailHook
The usage and implementation of
/ * ** The subscribed handler has a return value other than undefined to stop running down* /
const { SyncBailHook } = require('tapable')
class Lesson { constructor() { this.hooks = { arch: new SyncBailHook(['name']) } } tap() { this.hooks.arch.tap('node', (name) => { console.log('node', name); return 'error' }) this.hooks.arch.tap('vue', (name) => { console.log('vue', name); return undefined }) } start() { this.hooks.arch.call('gxb') } } const l = new Lesson() l.tap() l.start() / * ** Method implementation* / class SyncBailHook { // It is generally possible to take an array parameter, but it is not used below constructor(args) { this.tasks = [] } tap(name, cb) { let obj = {} obj.name = name obj.cb = cb this.tasks.push(obj) } call(... arg) { let ret let index = 0 do { ret = this.tasks[index++].cb(... arg) } while (ret === undefined && index < this.tasks.length); } } const syncBailHook = new SyncBailHook() syncBailHook.tap('node', name => { console.log('node', name); return 'error' }) syncBailHook.tap('vue', name => { console.log('vue', name); }) syncBailHook.call('gxb') Copy the code
4.1.3 SyncWaterfallHook
The usage and implementation of
/ * ** The return value of the previous handler is the input of the next one* /
/ * ** The subscribed handler has a return value other than undefined to stop running down* / const { SyncWaterfallHook } = require('tapable') class Lesson { constructor() { this.hooks = { arch: new SyncWaterfallHook(['name']) } } tap() { this.hooks.arch.tap('node', (name) => { console.log('node', name); return 'node ok' }) this.hooks.arch.tap('vue', (data) => { console.log('vue', data); }) } start() { this.hooks.arch.call('gxb') } } const l = new Lesson() l.tap() l.start() / * ** Method implementation* / class SyncWaterfallHook { // It is generally possible to take an array parameter, but it is not used below constructor(args) { this.tasks = [] } tap(name, cb) { let obj = {} obj.name = name obj.cb = cb this.tasks.push(obj) } call(... arg) { let [first, ...others] = this.tasks letret = first.cb(... arg) others.reduce((pre, next) = > { return next.cb(pre) }, ret) } } const syncWaterfallHook = new SyncWaterfallHook() syncWaterfallHook.tap('node', data => { console.log('node', data); return 'error' }) syncWaterfallHook.tap('vue', data => { console.log('vue', data); }) syncBailHook.call('gxb') Copy the code
4.1.4 SyncLoopHook
The usage and implementation of
/ * ** If a subscribed handler has a return value that is not undefined, it will loop through it* /
const { SyncLoopHook } = require('tapable')
class Lesson { constructor() { this.index = 0 this.hooks = { arch: new SyncLoopHook(['name']) } } tap() { this.hooks.arch.tap('node', (name) => { console.log('node', name); return ++this.index === 3 ? undefined : this.index }) this.hooks.arch.tap('vue', (name) => { console.log('vue', name); return undefined }) } start() { this.hooks.arch.call('gxb') } } const l = new Lesson() l.tap() l.start() / * ** Method implementation* / class SyncLoopHook { // It is generally possible to take an array parameter, but it is not used below constructor(args) { this.tasks = [] } tap(name, cb) { let obj = {} obj.name = name obj.cb = cb this.tasks.push(obj) } call(... arg) { this.tasks.forEach(item= > { let ret; do { ret = item.cb(... arg) } while(ret ! = =undefined); }) } } const syncLoopHook = new SyncLoopHook() let index = 0 syncLoopHook.tap('node', name => { console.log('node', name); return ++index === 3 ? undefined : index }) syncLoopHook.tap('vue', name => { console.log('vue', name); }) syncLoopHook.call('gxb') Copy the code
4.2 the asynchronous
Asynchronous concurrent
2AsyncParallelHook
/ * ** Asynchronous concurrency,Serial versus concurrentThat is, concurrency requires all of the handler to execute before the last callback, whereas serial requires one handler to execute before the second callback* /
const { AsyncParallelHook } = require("tapable") class Lesson { constructor() { this.hooks = { arch: new AsyncParallelHook(['name']) } } tap() { this.hooks.arch.tapAsync('node', (name, cb) => { setTimeout((a)= > { console.log("node", name); cb(); }, 1000); }) this.hooks.arch.tapAsync('vue', (name, cb) => { // Use macro tasks setTimeout((a)= > { console.log('vue', name) cb() }, 1000) }) } start() { this.hooks.arch.callAsync('gxb'.function() { console.log('end') }) } } let l = new Lesson(); l.tap(); l.start(); / * ** implementation* Asynchronous concurrent recall features, callAsync incoming callbacks are executed after all asynchronous tasks have completed* / class SyncParralleHook { constructor() { this.tasks = [] } tapAsync(name, cb) { let obj = {} obj.name = name obj.cb = cb this.tasks.push(obj) } // The async handler should run done to see if it is ready for the last callback callAsync(... arg) { // Take out the callback passed by callAsync const lastCb = arg.pop() // Start executing other asynchronous tasks let index = 0 // the done function is used to determine whether to execute lastCb const done = (a)= > { // Execute lastCb according to the number of asynchronous functions index++ if (index === this.tasks.length) { lastCb() } } this.tasks.forEach(item= >item.cb(... arg, done)) } } const hook = new SyncParralleHook() hook.tapAsync('node', (name, cb) => { setTimeout(function() { console.log('node', name) cb() }, 1000) }) hook.tapAsync('vue', (name, cb) => { setTimeout(function() { console.log('vue', name) cb() }, 1000) }) hook.callAsync('gxb'.function() { console.log('end') }) / * ** The handler uses microtask promis, that is, the handler can return no cb but needs to return a promise* / const { AsyncParallelHook } = require('tapable') class Lesson { constructor() { this.hooks = { arch: new AsyncParallelHook(['name']) } } start() { this.hooks.arch.promise('gxb').then(function() { console.log('end') }) } tap() { this.hooks.arch.tapPromise('node', name => { return new Promise((resove, reject) = > { console.log('node', name) resove() }) }) this.hooks.arch.tapPromise('vue', name => { return new Promise((resove, reject) = > { console.log('vue', name) resove() }) }) } } const l = new Lesson() l.tap() l.start() / * ** implementation * tapPromise * promise * / class AsyncParallelHook { constructor() { this.tasks = [] } tapPromise(name, cb) { let obj = {} obj.name = name obj.cb = cb this.tasks.push(obj) } promise(... arg) { // The handler part of the task returns all promise values, map them into an array. Implement all of them using promise's all const newTasks = this.tasks.map(item= >item.cb(... arg)) return Promise.all(newTasks) } } / / test const hook = new AsyncParallelHook() hook.tapPromise('node', name => { return new Promise((res, rej) = > { console.log('node', name) res() }) }) hook.tapPromise('vue', name => { return new Promise((res, rej) = > { console.log('vue', name) res() }) }) hook.promise('gxb').then(function() { console.log('end') }) Copy the code
4.2.2AsyncParallelBailHook
Same with synchronization
Asynchronous serial port
holdingsAsyncSeriesHook
/ * ** Asynchronous serial* Execute one by one *
* /
const { AsyncSeriesHook } = require('tapable') class Lesson { constructor() { this.hooks = { arch: new AsyncSeriesHook(['name']) } } start() { this.hooks.arch.callAsync('gxb'.function() { console.log('end') }) } tap() { this.hooks.arch.tapAsync('node', (name, cb) => { setTimeout((a)= > { console.log('node', name) cb() }, 1000) }) this.hooks.arch.tapAsync('vue', (name, cb) => { setTimeout((a)= > { console.log('node', name) cb() }, 1000) }) } } const l = new Lesson() l.tap() l.start() / * ** implementation* Features: one function is executed before another is executed* / class AsyncSeriesHook { constructor() { this.tasks = [] } tapAsync(name, cb) { let obj = {} obj.name = name obj.cb = cb this.tasks.push(obj) } callAsync(... arg) { const finalCb = arg.pop() let index = 0 let next = (a)= > { if (this.tasks.length === index) return finalCb() let task = this.tasks[index++].cb task(... arg, next) } next() } } const hook = new AsyncSeriesHook() hook.tapAsync('node', (name, cb) => { setTimeout((a)= > { console.log('node', name) cb() }, 1000) }) hook.tapAsync('vue', (name, cb) => { setTimeout((a)= > { console.log('vue', name) cb() }, 1000) }) hook.callAsync('gxb'.function() { console.log('end') }) Copy the code
4.2.4 AsyncSeriesBailHook
Refer to the synchronous
4.2.5 AsyncSeriesWaterfallHook
Refer to the synchronous
Five: Hand write a simple Webpack
5.1 Building an import File, that is, link to the local PC
To initialize a new project, NPM init-y first generates package.json
Write a bin command
"bin": {
"mypack": "./bin/index.js"
},
Copy the code
./bin/index.js as our entry file
Then run the NPM link, link the NPM module to the corresponding running project, and debug and test the module conveniently
Create a project with the webpack.config.js file (call it the project file to package first — > source project). Run the ‘NPM link mypack’ command
This is to run the command NPX mypack in the project to be packaged, using the one we wrote by hand
5.2 Core construction and compiler class compilation
Go back to the handwritten Webpack project to initialize the Compiler class
Nothing in the entry file
You simply need to get the webpack.config.js file in the source project, and then call the Compiler method to pass in the configuration file’s address
#! /usr/bin/env node
const path = require('path')
// Get the configuration file
const config = require(path.resolve('webpack.config.js')) const Compiler = require('.. /lib/compiler.js') const compiler = new Compiler(config) compiler.run() Copy the code
The basic skeleton of the Compiler class
const path = require('path')
const fs = require('fs')
const tapable = require('tapable')
class Compiler {
constructor(config) {
this.config = config this.entryId = ' ' this.modules = {} this.entry = config.entry this.root = process.cwd() // Get the current project address this.asserts = {} // Store chunkName and output code block this.hooks = { entryInit: new tapable.SyncHook(), beforeCompile: new tapable.SyncHook(), afterCompile: new tapable.SyncHook(), afterPlugins: new tapable.SyncHook(), afteremit: new tapable.SyncHook(), } const plugins = this.config.plugins if (Array.isArray(plugins)) { plugins.forEach(item= > { // Each is an instance, just call a method on the instance and pass in the current Compiler instance item.run(this) }) } } // Building blocks buildMoudle(modulePath, isEntry) {} // Write the output position emitFile() {} run() { this.hooks.entryInit.call() this.buildMoudle(path.resolve(this.root, this.entry), true) this.hooks.afterCompile.call() this.emitFile() } } Copy the code
Building template
The first pass in run is an entry address, and a Boolean that indicates whether the entry is valid. What we need to do is first assign the relative address of the entry to this.entryid
Then get the source code of the file and make it in the form of key-value pairs {file relative address: modified source code}
// Building blocks
buildMoudle(modulePath, isEntry) {
let source = this.getSource(modulePath)
// This. Entry
let moduleName = '/' + path.relative(this.root, modulePath) if (isEntry) { this.entryId = moduleName } // start to modify the source code, mainly for require let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) / / it is. / SRC // Put the modification into the module this.modules[moduleName] = sourceCode // Recursively construct dependent templates dependencies.forEach(item= > { this.buildMoudle(path.resolve(this.root, item), false) }) } Copy the code
// Get the source code
getSource(modulePath) {
// Match the path with the matching rule in module
let content = fs.readFileSync(modulePath, 'utf8')
return content } Copy the code
Transform the source code
There’s a couple of tools here
babylon
Turn the source code into an AST abstract syntax tree@babel/traverse
Used to replace nodes on the AST@babel/generator
The results generated@babel/types
AST node’s Lodash-esque utility library
Install: NPM I Babylon @babel/traverse @babel/types @babel/generator
Change require to __webpack_require__, and modify the internal path parameters of require. Webpack is essentially a manual implementation of require
Note: this time only rewrite require, but still does not support ES6 module
// Change the source code
parse(source, parentPath) {
// Generate the AST syntax tree
let ast = babylon.parse(source)
// For access dependencies let dependencies = [] traverse(ast, { CallExpression(p) { let node = p.node // Rename the require method if (node.callee.name === 'require') { node.callee.name = '__webpack_require__' let moduledName = node.arguments[0].value // get the path to the require method // change the suffix moduledName = moduledName + (path.extname(moduledName) ? ' ' : '.js') // add the parent path./ SRC moduledName = '/' + path.join(parentPath, moduledName) // Add the dependency array dependencies.push(moduledName) // Replace the source code node.arguments = [type.stringLiteral(moduledName)] } } }) let sourceCode = generator(ast).code return { sourceCode, dependencies } } Copy the code
Output to the specified position of output
// Write to the output position
emitFile() {
// Get the output address
let outPath = path.join(this.config.output.path, this.config.output.filename)
/ / template let templateStr = this.getSource(path.join(__dirname, 'main.ejs')) // Populate the template data let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules }) this.asserts[outPath] = code console.log(code); / / write fs.writeFileSync(outPath, this.asserts[outPath]) } Copy the code
(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; } // Load entry module and return exports return __webpack_require__(__webpack_require__.s = "<%-entryId %>"); })({ <% for(let key in modules){ %> "<%- key %>": (function (module, exports,__webpack_require__) { eval(`<%-modules[key] %>`); }), <%} %>}); </div>Copy the code
joinloader
< CSS > < CSS > < CSS > < CSS > < CSS > < CSS
Less-loader: Borrows less files and converts them to CSS files
const less = require('less')
function loader(source) {
let css = ' '
less.render(source, function(err, output) {
css = output.css }) css = css.replace(/\n/g.'\\n') return css } module.exports = loader Copy the code
Style-loader: Create a label and put CSS in it
function loader(source) {
let style = `
let style = document.createElement('style')
style.innerHTML = The ${JSON.stringify(source)}
document.head.appendChild(style) ` return style } module.exports = loader Copy the code
At this point the webpack. Config. Js
module.exports = {
mode: 'development'. entry: './src/index.js'. output: {
filename: 'main.js'. path: path.join(__dirname, './dist') }, module: { rules: [{ test: /\.less$/. use: [path.join(__dirname, './loader/style-loader.js'), path.join(__dirname, './loader/less-loader.js')] }] }, plugins: [ new TestPlugins(), new InitPlugin() ] } Copy the code
At this point, the source file acquisition function is transformed as follows
// Get the source code
getSource(modulePath) {
// Match the path with the matching rule in module
const rules = this.config.module.rules
let content = fs.readFileSync(modulePath, 'utf8')
for (let i = 0; i < rules.length; i++) { let { test, use } = rules[i] let len = use.length // Match to start loader, features from back to front if (test.test(modulePath)) { console.log(111); function normalLoader() { // Take the last one first let loader = require(use[--len]) content = loader(content) if (len > 0) { normalLoader() } } normalLoader() } } return content } Copy the code
And add plug-ins
Write it directly in the source project webpack.config.js
const path = require('path')
class TestPlugins {
run(compiler) {
// Subscribe your methods to hook for use
// Assume it runs after compilation
compiler.hooks.afterCompile.tap('TestPlugins'.function() { console.log(`this is TestPlugins,runtime ->afterCompile `); }) } } class InitPlugin { run(compiler) { // Put the execution period before the start parsing entry compiler.hooks.entryInit.tap('Init'.function() { console.log(`this is InitPlugin,runtime ->entryInit `); }) } } module.exports = { mode: 'development'. entry: './src/index.js'. output: { filename: 'main.js'. path: path.join(__dirname, './dist') }, module: { rules: [{ test: /\.less$/. use: [path.join(__dirname, './loader/style-loader.js'), path.join(__dirname, './loader/less-loader.js')] }] }, plugins: [ new TestPlugins(), new InitPlugin() ] } Copy the code
In fact, handwriting logic is difficult to make clear, rather than directly look at the code, BELOW I have pasted out the whole code of the Compiler class, with the comments written in what I feel is quite detailed
Compiler class in full code
const path = require('path')
const fs = require('fs')
const { assert } = require('console')
// Babylon is the JavaScript parser used in Babel.
// @babel/traverse replaces, removes, and adds nodes to the AST parse traverse syntax tree
// @babel/types Lodash-esque utility library for AST nodes // @babel/generator Result generated const babylon = require('babylon') const traverse = require('@babel/traverse').default; const type = require('@babel/types'); const generator = require('@babel/generator').default const ejs = require('ejs') const tapable = require('tapable') class Compiler { constructor(config) { this.config = config this.entryId = ' ' this.modules = {} this.entry = config.entry this.root = process.cwd() // The current project address this.asserts = {} // Store chunkName and output code block this.hooks = { entryInit: new tapable.SyncHook(), beforeCompile: new tapable.SyncHook(), afterCompile: new tapable.SyncHook(), afterPlugins: new tapable.SyncHook(), afteremit: new tapable.SyncHook(), } const plugins = this.config.plugins if (Array.isArray(plugins)) { plugins.forEach(item= > { // Each is an instance, just call a method on the instance and pass in the current Compiler instance item.run(this) }) } } // Get the source code getSource(modulePath) { // Match the path with the matching rule in module const rules = this.config.module.rules let content = fs.readFileSync(modulePath, 'utf8') for (let i = 0; i < rules.length; i++) { let { test, use } = rules[i] let len = use.length // Match to start loader, features from back to front if (test.test(modulePath)) { console.log(111); function normalLoader() { // Take the last one first let loader = require(use[--len]) content = loader(content) if (len > 0) { normalLoader() } } normalLoader() } } return content } // Change the source code parse(source, parentPath) { // ast syntax tree let ast = babylon.parse(source) // For access dependencies let dependencies = [] traverse(ast, { CallExpression(p) { let node = p.node // Rename the require method if (node.callee.name === 'require') { node.callee.name = '__webpack_require__' let moduledName = node.arguments[0].value // get the path to the require method // change the suffix moduledName = moduledName + (path.extname(moduledName) ? ' ' : '.js') // add the parent path./ SRC moduledName = '/' + path.join(parentPath, moduledName) // Add the dependency array dependencies.push(moduledName) // Replace the source code node.arguments = [type.stringLiteral(moduledName)] } } }) let sourceCode = generator(ast).code return { sourceCode, dependencies } } // Building blocks buildMoudle(modulePath, isEntry) { let source = this.getSource(modulePath) // This. Entry let moduleName = '/' + path.relative(this.root, modulePath) if (isEntry) { this.entryId = moduleName } // start to modify the source code, mainly for require let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) / / it is. / SRC // Put the modification into the module this.modules[moduleName] = sourceCode // Recursively construct dependent templates dependencies.forEach(item= > { this.buildMoudle(path.resolve(this.root, item), false) }) } // Write the output position emitFile() { // Get the output address let outPath = path.join(this.config.output.path, this.config.output.filename) / / template let templateStr = this.getSource(path.join(__dirname, 'main.ejs')) // Populate the template data let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules }) this.asserts[outPath] = code console.log(code); / / write fs.writeFileSync(outPath, this.asserts[outPath]) } run() { this.hooks.entryInit.call() this.buildMoudle(path.resolve(this.root, this.entry), true) this.hooks.afterCompile.call() this.emitFile() } } module.exports = Compiler / * ** At this point a simple Webpack is complete* / Copy the code
At the end of the day, webPack is officially in the door. Save the following for a later summary. I won’t upload the brain map
Reference acknowledgments:
👉The second Tapable in the Webpack series
👉It’s easy to understand devtool in Webpack with two examples: what does ‘source-map’ mean
👉This is the official Tapable Chinese document
👉NPM Link usage summary
👉How to develop webPack Loader
👉How to play the webpack
👉Fix webpack4
👉Webpack: Getting started, advancing, and tuning
This article is formatted using 👉 MDnice