Implement webPack packaging function

A brief analysis of the results of webpack packaging

Start by creating three files index.js

let news = require('./news.js')
console.log(news.content)
Copy the code

message.js

module.exports = {
    content: It's going to rain today
}
Copy the code

news.js

let message = require('./message.js')
module.exports = {
  content: 'Today there is a big news, breaking news !!!! The content is${message.content}`
}
Copy the code

Then we use Webpack for packaging to analyze the results of this packaging

Perform a simplified analysis of the packaged results. The core code extracted is as follows

 (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/index.js": (function(module.exports, __webpack_require__) {eval("let news = __webpack_require__(/*! ./news.js */ \"./src/news.js\")\nconsole.log(news.content)\n\n//# sourceURL=webpack:///./src/index.js?"); }),"./src/message.js": (function(module.exports) {eval("\ nmodule exports = {\ n content: ` today rain ` \ n} \ n \ n # sourceURL = webpack: / / / / /. / SRC/message. Js?")}),
    "./src/news.js": (function(module.exports, __webpack_require__) {eval("let message = __webpack_require__(/*! ./message.js */ \"./ SRC /message.js\")\ nModule.exports = {\n Content: 'Big news today, breaking news !!!! Content is ${message. The content} ` \ n \ n \ n} / / # sourceURL = webpack: / / /. / SRC/news. Js?");})
})  

// Parse this code
// The first is a self-calling function modules: it is an object obj
// Execute this self-calling function, self-calling function. An internal __webpack_require__() parameter is returned as the value in the entry
// Call __webpack_require__() to pass in the entry value
// Start executing the function
// Internal module = {
// i: modulesId,
// l: false
// export: {}
// }
// Proceed, calling the outermost parameter, changing this to null {}, and finally a recursive call to __webpack_require__
Copy the code

So we’re actually going to implement a Webpack. There are two main tasks

  1. Replace all require with __webpack__require,
  2. All dependencies in the module are read, spliced into an object, passed into the self-calling function.

Build the basic framework of the project

  1. Create a bin directory and create the yJ-pack file in the bin directory. The main purpose of this file is to read the webpack.config.js configuration directory (webPack-like custom configuration names are not currently supported), pass the read configuration into the Compiler module, and let the compiler module process it accordingly. This module does nothing else
#! /usr/bin/env node 
const path = require('path') 
// 1. Read the configuration file of the project to be packaged
let config = require(path.resolve('webpack.config.js'))
// let config = require(process.cwd(),'webpack.config.js')
const Compiler = require('.. /lib/compiler')
let a = new Compiler(config)
a.start()
Copy the code
  1. Create our Compiler module
class Compiler {
    constructor(config) {
       this.config = config   // Initialize the configuration
       this.entry = config.entry   // The relative path of the entry
       this.root = process.cwd()   // The current directory to execute the command
    }
    start() {
      // Perform corresponding file operations}}module.exports = Compiler

Copy the code

Read entry file

  1. First of all, let’s think about, what do we need to do?

Our main goal is to read the entry file, analyze the dependencies between modules, and replace require with __webpack_require__

class Compiler {
    constructor(config) {
       this.config = config   // Initialize the configuration
       this.entry = config.entry   // The relative path of the entry
       this.root = process.cwd()   // The current directory to execute the command
       this.analyseObj = {}   // This is the last file object we need
    }
	// The utility function is used to concatenate paths
    getOriginPath(path1,path2) {
        return path.resolve(path1,path2)
    }
    // The utility function reads the file
    readFile(modulePath) {
		 return fs.readFileSync(modulePath,'utf-8')}// the entry function
    start() {
      // Perform corresponding file operations
      // Get the path of the entry file and analyze it
      let originPath = this.getOriginPath(this.root,this.entry)
      this.depAnalyse(originPath)
      }
     // The core function
    depAnalyse(modulePath){
    // The content is the entry file in webpack.config.js
    let content =  this.readFile(modulePath)
   }
}
Copy the code

Replace require with AST syntax tree

After this step reads the file, replace require with __webpack_require__. Mainly using the Babel plugin

const fs = require('fs')
const path = require('path')
const traverse = require('@babel/traverse').default;
const parser = require('@babel/parser');
const generate = require('@babel/generator').default
class Compiler {
    constructor(config) {
       this.config = config   // Initialize the configuration
       this.entry = config.entry   // The relative path of the entry
       this.root = process.cwd()   // The current directory to execute the command
       this.analyseObj = {}   // This is the last file object we need
    }
    // The utility function is used to concatenate paths
    getOriginPath(path1,path2) {
      return path.resolve(path1,path2)
    }
    // The utility function reads the file
    readFile(modulePath) {
      return fs.readFileSync(modulePath,'utf-8')}// the entry function
    start() {
      // Perform corresponding file operations
      // Get the path of the entry file and analyze it
      let originPath = this.getOriginPath(this.root,this.entry)
      this.depAnalyse(originPath)
    }
    // The core function
    depAnalyse(modulePath){
      // The content is the entry file in webpack.config.js
      let content =  this.readFile(modulePath)
      // Convert code to ast syntax tree
      const ast = parser.parse(content) 
      // traverse is to replace the contents of AST
      traverse(ast, {
          CallExpression(p) {
            if(p.node.callee.name === 'require') {
                p.node.callee.name = '__webpack_require__'}}})// Finally convert the AST syntax tree to code
      letSourceCode = generate(ast).code}} So we have completed the first step, read the current entry file, and then add the contentrequireStudent: The substitutionCopy the code

Recursive implementation of module dependency analysis

In fact, there are certain problems with the above steps. What if there are multiple module dependencies in index.js? Similar index. Js

let a = require('./news.js)
let b = require('./news1.js)
Copy the code

Because we need to store each module’s dependencies in an array. Each module is then recursively traversed. So let’s move on to improving the depAnalyse function

depAnalyse(modulePath){
  // The content is the entry file in webpack.config.js
  let content =  this.readFile(modulePath)
  // Convert code to ast syntax tree
  const ast = parser.parse(content) 
  // To access all dependencies of the current module. Easy to traverse later
  let dependencies = []
  // traverse is to replace the contents of AST
   traverse(ast, {
       CallExpression(p) {
         if(p.node.callee.name === 'require') {
             p.node.callee.name = '__webpack_require__'
		     // The path is handled because the file path \ is under window. Lunix has a /. So let's do it all together
             let oldValue = p.node.arguments[0].value
             p.node.arguments[0].value = '/'+ path.join('src',oldValue).replace(/\\+/g.'/')
		     // Push the file path that the current module depends on into the array
             dependencies.push(p.node.arguments[0].value)
          }
        }
    })
    // Finally convert the AST syntax tree to code
    let sourceCode =  generate(ast).code
    // Push the current dependencies and file contents into the object.
    let relavitePath = '/'+ path.relative(this.root,modulePath).replace(/\\+/g.'/')
    this.analyseObj[relavitePath] = sourceCode
    // Each module may have other dependencies, so we need to iterate recursively.
    dependencies.forEach(dep= >{
      // Do some recursion
      this.depAnalyse(this.getOriginPath(this.root,dep))
    })
}
Copy the code

So you print this.analyseobj and find that you have the object we want. Next we think about how to generate the WebPack template.

Generate the WebPack template file

  1. First we find the original simplified WebPack package file. We use EJS for the corresponding transformation, create a template folder, build output.ejs template. The template as follows
 (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 = "<%-entry%>"); < % ({})for (let k in modules) { %>	
		"<%-k%>": 
		(function(module.exports, __webpack_require__) {eval(`<%-modules[k]%>`)}), % > < %}})Copy the code

Let Webpack combine with EJS to output files

  1. This step is mainly after we have analyzed the file. Combine the template with our analysis of this.analyseobj
start() {
  let originPath = this.getOriginPath(this.root,this.entry)
  this.depAnalyse(originPath)
  // Compile complete
  this.emitFile()
}
emitFile() {
  let template= this.readFile(path.join(__dirname,'.. /template/output.ejs'))
  let result =  ejs.render(template,{
    entry: this.entry,
    modules: this.analyseObj
  })
  let outputPath = path.join(this.config.output.path,this.config.output.filename)
  fs.writeFileSync(outputPath,result)
  // console.log(result)
}
Copy the code

This will output the file to the specified directory. It is then executed using Node or in a browser. You can read it. This gives us a simple WebPack package

Loader function of Webpack

Loader is essentially a function

How to develop your own loader in Webpack

Start by defining your own loader in webpack.config.js

const path = require('path')
module.exports = {
    entry: './src/index.js'.output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    mode: 'development'.module: {rules: [
            / / {
            // test: /\.js$/,
            // use: ['./src/loaders/loader1.js','./src/loaders/loader2.js','./src/loaders/loader3.js' ]
            // }
            / / {
            // test: /\.js$/,
            // use: './src/loaders/loader2.js'
            // },
            {
            test: /\.js$/,
            use: {
              loader: './src/loaders/loader1.js'.options: {
                  name: 'today 111111111'}}}]}}Copy the code

Then create your own loader

module.exports = function(source){
    console.log(this.query)
    return source.replace(/ / g today.this.query.name)
}
Copy the code

So our basic loader has been implemented. The loader function is to replace all js’ today ‘with’ today 111111111 ‘. Now we know how to write a simple loader, so we will look at how to make our own webpck support loader function

How to write webpack support loader functionality

  1. Simple analysis by the above loader. We mainly read modules for webpack.config.js. Then match the corresponding file, and finally process the file accordingly.

Let’s think about when loader is executed. DpAnalyse should be executed after we read the file, so we continue to modify dpAnalyse. Note: Loader executes from bottom to top, right to left

depAnalyse(modulePath){
  // The content is the entry file in webpack.config.js
  let content =  this.readFile(modulePath)
  // loader will load the module.rules file in webpack.config.js. First check whether the configuration file module.rules in JS exists, and then flashback through the loader in it
  // Start processing loader
    for(var i = this.rules.length-1; i>=0; i--){// this.rules[i]  
        let {test,use} = this.rules[i]
        // Whether the match matches the rule
        if(test.test(modulePath)){
            if(Array.isArray(use)){
            // We need to judge the number, object, and character
            // There is no encapsulation here
            for(var j=use.length-1; j>=0; j--){let loader =  require(path.join(this.root,use[j])) 
                content = loader(content)
              }
            }else if(typeof use === 'string') {
                let loader =  require(path.join(this.root,use)) 
                content = loader(content)
            }else if(use instanceof Object) {// console.log(use.options)
                // console.log(" now use is the first item ")
                let loader =  require(path.join(this.root,use.loader)) 
                content = loader.call({query:use.options},content)
            }
           
        } 
    }
  // Convert code to ast syntax tree
  const ast = parser.parse(content) 
  // To access all dependencies of the current module. Easy to traverse later
  let dependencies = []
  // traverse is to replace the contents of AST
   traverse(ast, {
       CallExpression(p) {
         if(p.node.callee.name === 'require') {
             p.node.callee.name = '__webpack_require__'
		     // The path is handled because the file path \ is under window. Lunix has a /. So let's do it all together
             let oldValue = p.node.arguments[0].value
             p.node.arguments[0].value = '/'+ path.join('src',oldValue).replace(/\\+/g.'/')
		     // Push the file path that the current module depends on into the array
             dependencies.push(p.node.arguments[0].value)
          }
        }
    })
    // Finally convert the AST syntax tree to code
    let sourceCode =  generate(ast).code
    // Push the current dependencies and file contents into the object.
    let relavitePath = '/'+ path.relative(this.root,modulePath).replace(/\\+/g.'/')
    this.analyseObj[relavitePath] = sourceCode
    // Each module may have other dependencies, so we need to iterate recursively.
    dependencies.forEach(dep= >{
      // Do some recursion
      this.depAnalyse(this.getOriginPath(this.root,dep))
    })
}
Copy the code

In this way, we simply implement the functions of loader

Implement plugins for Webpack

How to develop a custom plugins

Plugins are essentially custom classes. But WebPack specifies that we must implement the Apply method in this class. The idea is that webpack implements its own set of life cycles internally, and then you just need to invoke the life cycle provided by WebPack in your Apply method

  1. Now let’s implement our own simple HelloWorld plugins
class HelloWorldPlugin{
    apply(Compiler){
       // Execute after the file is packaged
        Compiler.hooks.done.tap('HelloWorldPlugin'.(compilation) = > {
            console.log("The whole Webpack pack is finished.")})// execute when webpack outputs the file
        Compiler.hooks.emit.tap('HelloWorldPlugin'.(compilation) = > {
            console.log("File initiation.")})// console.log('hello world')}}module.exports = HelloWorldPlugin
Copy the code

Finally, we just need to import the appropriate plugins in webpack.config.js.

Implement the lifecycle with Tapable

You can see above that WebPack implements its own life cycle. So how did he do it? The core is actually a publishing subscriber model, webPack is actually mainly using a core library Tapable so we build a simple class, to achieve the corresponding life cycle. There are three main steps

  1. Register custom events
  2. Unbind events when appropriate
  3. Invoke events when appropriate
/ / study front end
const {SyncHook}  = require('tapable')
class Frontend{
    constructor() {
        this.hooks = {
           beforeStudy: new SyncHook(),
           afterHtml: new SyncHook(),
           afterCss: new SyncHook(),
           afterJs: new SyncHook(),
           afterReact: new SyncHook() 
        }
    }
    study() {
        console.log('Get ready to learn')
        this.hooks.beforeStudy.call()
        console.log('Get ready to learn HTML')
        this.hooks.afterHtml.call()
        console.log('Get ready to learn CSS')
        this.hooks.afterCss.call()
        console.log('Get ready to learn JS')
        this.hooks.afterJs.call()
        console.log('Get ready to learn React')
        this.hooks.afterReact.call()
    }
}
let f = new Frontend()
f.hooks.afterHtml.tap('afterHtml'.() = >{
    console.log("I want to build Taobao after LEARNING HTML")
})

f.study()
Copy the code

Thus we have implemented our own lifecycle function

Let’s write our webpack to support plugins

We implemented the compiler lifecycle by defining the webpack lifecycle as soon as the compiler function was initialized and calling it in start

class Compiler {
    constructor(config) {
       this.config = config 
       this.entry = config.entry
       this.root = process.cwd()
       this.analyseObj = {}
       this.rules = config.module.rules
       this.hooks = {
        // Lifecycle definition
        compile: new SyncHook(),
        afterCompile: new SyncHook(),
        emit: new SyncHook(),
        afterEmit: new SyncHook(),
        done: new SyncHook()
       }
       // All plug-in objects in the plugins array call the Apply method, which is equivalent to registering events
       if(Array.isArray(this.config.plugins)){
        this.config.plugins.forEach(plugin= > {
            plugin.apply(this)}}}start() {
        // Start compiling
        this.hooks.compile.call()
        // Start packing
        // Dependency analysis
        let originPath = this.getOriginPath(this.root,this.entry)
        this.depAnalyse(originPath)
        // The compilation is complete
        this.hooks.afterCompile.call()
        // Start sending file
        this.hooks.emit.call()
        this.emitFile()
        this.hooks.afterEmit.call()
        this.hooks.done.call()
    }
  }
Copy the code

Write an HTMLPlugin for your webpack

So next, we write an HTMLPlugin for our own Webpack to implement our own htmlPlugin class. And perform the corresponding functions within the corresponding Webpack cycle using the Cheerio library, basically dom manipulation of HTML strings

const fs = require('fs')
const cheerio = require('cheerio')

class HTMLPlugin{
    constructor(options){
        this.options = options
    }
    apply(Compiler){
        // Register the afterEmit event
        Compiler.hooks.afterEmit.tap('HTMLPlugin'.(compilation) = > {
            // console.log(" webpack complete ")
           let result = fs.readFileSync(this.opions.template,'utf-8')
            // console.log(this.options)
            // console.log(result)
            let $ = cheerio.load(result)
            Object.keys(compilation.assets).forEach(item= >{$(`<script src="${item}"></script>`).appendTo('body')})// Generate HTML. The output
            // $.html()
            fs.writeFileSync('./dist/' + this.options.filename,$.html())
        })
        // console.log('hello world')}}module.exports = HTMLPlugin
Copy the code

The source address

And then we’re done. Github: github.com/yujun96/yj-… Github.com/yujun96/web…