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
- Replace all require with __webpack__require,
- All dependencies in the module are read, spliced into an object, passed into the self-calling function.
Build the basic framework of the project
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- Register custom events
- Unbind events when appropriate
- 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…