preface
Webpack is an indispensable tool in front-end programming. Its function is to pack multiple modules into one or more bundles. As a front-end novice, I always think Webpack is as mysterious as magic. I found that the principle of Webpack is not complicated, but this article is not to analyze the source code, in fact, before understanding the principle to read the source code is a very inefficient thing, we can try to implement a low version of Webpack
Since this article uses a lot of code, I will present the project code first
Let’s make a simple packer together
Let’s start with Babel
If you’re familiar with the front end, Babel is a Javascript compiler that compiles new js syntax into old browser-executable syntax.
The principle of Babel
Babel compiles code in three parts
- Parse: Converts code into an AST
- Traverse: traverse the AST for modification
- Generate: Convert AST to code2
Know the AST
AST Abstract Syntax Tree, which is an Abstract representation of the syntactic structure of source code. It represents the syntactic structure of a programming language as a tree, with each node in the tree representing a structure in the source code.
Let’s use the Babel tool to manually convert the let syntax to var syntax and observe the structure of the AST
Complete repository connection added
import traverse from '@babel/traverse'
import {parse} from '@babel/parser'
import generate from '@babel/generator'
const code = `let a = 1; let b = 2`
const ast = parse(code, {sourceType: 'module'})
console.log(ast, 'ast');
traverse(ast, {
enter: item= > {
if (item.node.type === 'VariableDeclaration') {
if(item.node.kind === 'let') {
item.node.kind = 'var'}}}})const result = generate(ast, {}, code)
console.log(result.code);
Copy the code
The above code only runs in Node. For debugging purposes, use node -r ts-node/register –inspect-brk let_to_var.ts to debug in the browser console
We can see the structure of the AST by looking at breakpoints
The result of running nodeJS
It is recommended that an online AST analyzer, AstExplorer, be used to explore the structures in the AST
es6 to es5
Es6 -> es5. It seems a bit impractical to use if for every syntax, but Babel /core provides a function to convert
import {parse} from '@babel/parser'
import * as babel from '@babel/core'
const code = `let a = 1; const b = 2`
const ast = parse(code, {sourceType: 'module'})
const result = babel.transformFromAstSync(ast, code, {
presets: ['@babel/preset-env']})console.log(result.code);
Copy the code
Rely on the analysis of
What else can we do with Babel except convert JS syntax? Let’s try analyzing JS dependencies
First we create three JS files
index.js
import a from './a.js'
import b from './b.js'
console.log(a.value + b.value)
Copy the code
a.js
const a = {
value: 1
}
export default a
Copy the code
b.js
const b = {
value: 1
}
export default b
Copy the code
Dependency analysis code
import * as fs from 'fs';
import {parse} from '@babel/parser';
import {relative, resolve, dirname} from 'path';
import traverse from '@babel/traverse';
// Set the root directory
const projectRoot = resolve(__dirname, 'project1');
// Type declaration
type DepRelation = {
[key: string]: { deps: string[], code: string }
}
// Initialize an empty depRelation for collecting data
const depRelation:DepRelation = {};
const collectCodeAndDeps = (filePath: string) = > {
// The project path of the file is index.js
const key = getProjectPath(filePath);
// Get the content of the file and put the content in depRelation
const code = fs.readFileSync(filePath).toString();
depRelation[key] = {deps: [], code};
// Convert the code to bit AST
const ast = parse(code, {sourceType: 'module'});
traverse(ast, {
enter: item= > {
if (item.node.type === 'ImportDeclaration') {
// path.node.source.value is usually a relative directory, such as./a3.js, which needs to be converted to an absolute path first
const depAbsolutePath = resolve(dirname(filePath), item.node.source.value);
// Then turn to the project path
const depProjectPath = getProjectPath(depAbsolutePath)
// Write dependencies in depRelation
depRelation[key].deps.push(depProjectPath)
}
}
});
};
const getProjectPath = (filePath: string) = > {
return relative(projectRoot, filePath);
};
collectCodeAndDeps(resolve(projectRoot, 'index.js'))
console.log(depRelation);
Copy the code
Code thinking
- call
collectCodeAndDeps('index.js')
- The first
depRelation['index.js']
Initialized to{deps: [], code}
- Convert the index.js code into an AST
- Let’s go through the AST and see
import
What dependencies are there, suppose a.js and B.js are relied on - Write the paths of a.js and b.js
depRelation['index.js'].deps
里
Finally printed by depRelation
Return to link
The above code can analyze only one dependency. What if it’s multiple?
Multilayer rely on
Resolving multiple levels of dependency is actually easy to solve by using recursion. Of course, using recursion is risky. If the dependency level is too deep, you risk calling stack overflow
Circular dependencies
What about ring dependence?
For example, import B.js from a.js and import A.js from b.js
If the loop dependencies are handled directly with the above code, the recursion will continue and end up calling stack Overflow
The solution is to add a conditional judgment to determine whether the file has already been recorded in depRelation[‘index.js’].deps before calling the parse function, and if so terminate the recursion
conclusion
The principle of bebel
Graph TD code --> parse --> AST --> traverse --> AST2 --> generate --> code2
Analysis depends on the process of the first is to put the code into the ast, then traverse the ast, whenever found the import statement, we rely on record, for multilayer dependencies, the way we can use recursive processing, if it is a circular dependencies, need to rely on the inspection, if it is already record dependence is not
Webpack core bundler
A bundle is produced by a Bundler. What is a bundle?
Here is the breakdown in the official documentation
It is generated by many different modules and contains the final version of the source file that has been loaded and compiled.
That is, a bundle is a file that contains all the modules and can execute all the modules, and it can be run directly in the browser
So the question we’re going to solve in this video is
- Make the code in the module executable
- Multiple modules are packaged into a single module
Prepare before you start
index.js
import a from './a.js'
import b from './b.js'
console.log(a.getB())
console.log(b.getA())
Copy the code
a.js
import b from './b.js'
const a = {
value: 'a'.getB() {
return b.value + ' from b.js'}}export default a
Copy the code
b.js
import a from './a.js'
const b = {
value: 'b'.getA() {
return a.value + ' from a.js'}}export default b
Copy the code
Obtained using the dependent code analyzed in the previous section
{ 'index.js': { deps: [ 'a.js', 'b.js' ], code: "import a from './a.js'\r\n" + "import b from './b.js'\r\n" + 'console.log(a.getB())\r\n' + 'console.log(b.getA())\r\n' }, 'a.js': { deps: [ 'b.js' ], code: "import b from './b.js'\r\n" + 'const a = {\r\n' + " value: 'a',\r\n" + ' getB() {\r\n' + " return b.value + ' from b.js'\r\n" ' }\r\n' + '}\r\n' + '\r\n' + 'export default a\r\n' }, 'b.js': { deps: [ 'a.js' ], code: "import a from './a.js'\r\n" + 'const b = {\r\n' + " value: 'b',\r\n" + ' getA() {\r\n' + " return a.value + ' from a.js'\r\n" ' }\r\n' + '}\r\n' + '\r\n' + 'export default b\r\n' }}Copy the code
Make the code in the module executable
In the above code, import/export is not run directly by the browser and needs to be converted to a function
We need to use bable/core to convert es6’s import/export syntax into ES5’s require function and exports object
const { code: es5Code } = babel.transform(code, {
presets: ['@babel/preset-env']})Copy the code
The transformed code looks like this
Code,
Let’s take a look at the code of A. js and see what the code looks like after webpack is compiled
Doubt a
Object.defineProperties(exports.'__esModule', {value: true})
Copy the code
What this code does is
- Add one to the current module
__esModule: true
, easy to separate from CommonJS module - and
exports.__esMoudle = true
The same effect, stronger compatibility
Doubt 2
exports["default"] = void 0;
Copy the code
Exports [“default”] = undefined; exports[“default”] = undefined
Details a
// import b from './b.js' becomes
var _b = _interopRequireDefault(require("./b.js"))
// b.value becomes
_b['default'].value
Copy the code
Parse the _interopRequireDefault function
- This function is used to add to the module
defualt
, since commonJS does not export by default, add to ‘default’ for compatibility _
Underscores are used to avoid having the same name as other functions_interop
Most of the prefixed functions are intended to be compatible with older code
Details of the second
var _default = a
exports['default'] = _default
Copy the code
Equivalent to exports. Default = a
summary
By Babel conversion
- will
import
The keyword becomesrequire
function - will
export
The keyword becomesexports
object
Multiple modules are packaged into a single module
To do that, we need to write a Bundler.
The first thing we need to know is what does the packaged code look like
var depRelation = [
{key: 'index.js' , deps: ['a.js' , 'b.js'].code: function. }, {key: 'a.js' , deps: ['b.js'].code: function. }, {key: 'b.js' , deps: ['a.js'].code: function. }]// Why change depRelation from object to array?
// Because the first item of an array is an entry, objects have no concept of a first item
execute(depRelation[0].key) // Execute the entry file
function execute(key){
var item = depRelation.find(i= > i.key === key)
item.code(???)
// The code that executes item, so code better be a function, easy to execute
// What parameters should be passed to code
// The code needs to be perfected
}
Copy the code
There are three problems to be solved
depRelation
It’s an object. It needs to be an arraycode
It’s a string. How do I change it to a functionexecute
The function needs to be perfected
DepRelation into an array
depRelation[key] = {deps: [], code};
Copy the code
Changed to
const item = { key, deps: [].code: es5Code }
depRelation.push(item);
// For other code changes, see the last implementation
Copy the code
Code is a string. How can I change it to a function
steps
- Package the code string in a function
function(require, module, exports)
, includingrequire module exports
Three parameters are specified by the CommonJS 2 specification - Finally, write code into the generated file, and the code quotes will disappear, which can be interpreted as changing from a string to a code
code = ` var b = 1 b += 1 exports.defult = b `
code2 = ` function(require, module, exports) { $(code) } `
Copy the code
Perfecting the Execute function
Principal thinking
const modules = {} // Used to cache all modules
function execute(key) {
if (modules[key]) {return modules[key]} // If the module is cached, return directly
var item = depRelation.find(i= > i.key === key) // Find the module to execute
var require = (path) = > { // Define the require function, which is executed by the require module
return execute(pathToKey(path))
}
modules[key] = { __esModule: true } // Define an __esModule attribute to distinguish it from CommonJS
var module = { exports: module[key] }
// Executing this module will mount the exported content to exports.default, as shown in the compiled Babel code above
item.code(require.module.module.exports)
return module.exports // {default: [exported object], __esModule: true}
}
Copy the code
Simple packer
What does a packed file look like
var depRelation = [ {key: 'index.js' , deps: ['a.js' , 'b.js'], code: function... }, {key: 'a.js' , deps: ['b.js'], code: function... }, {key: 'b.js' , deps: ['a.js'], code: Var modules = {} // modules is used to cache all modules. Execute (depRelation[0].key) function execute(key){var require = . var module = ... item.code(require, module, module.exports) ... }Copy the code
How do I generate this file?
The answer is simple: piece together a string and write it to a file
var dist = ''
dist += content
writeFileSync('dist.js', dist)
Copy the code
The dist file is too long, so I won’t put it here
Run the dist file and get
The packaged file is running successfully
summary
The packaged file is actually a collection of multiple modules and stored through the depRelation array. DeRelation [0] is the inlet file. Each element of the deRelation array is a module. A single element contains the module name key, the module’s dependency on deps, and the module’s executable code function. This function has three arguments: Require Module exports (CommonJS
We’ve seen how WebPack packaging works and made a simple wrapper, but it still has a lot of problems
- The generated code has multiple duplicate __interopXXX functions
- You can only import and run JS files
- You can only understand import, you can’t understand require
- Plug-ins not supported
- Configuration entry files and dist file names are not supported
. So what’s the next step?
Loader
What is loader and why is loader needed
Looking back at the simple packager we made, this packager can only load JS, not CSS, what the hell
No, you need to write a CSS loader to support CSS
CSS – loader homemade version
Three-step logic
- Our Bundler can only load JS
- We want to load CSS
Corollary: If we can turn CSS into JS, then we can load CSS
How to convert to JS, var STR = [CSS code] stored in a variable
To make CSS work, create a new style tag, write the CSS code inside the
// css-loader
const cssLoader = (code) = > { // Accept the code
return `
const str = The ${JSON.stringify(code)}
if (document) {
const style = document.createElement('style')
style.innerHTML = str
document.head.appendChild(style)
}
`
}
module.exports = cssLoader
Copy the code
Loader has been written, how to use it?
When the depRelation object was created, the code was processed using CSS-Loader
The complete code
The single responsibility principle for Webpack
Each loader in webpack does only one thing
Currently our loader does two things
- Convert CSS to JS strings
- Put the JS string inside the style tag
Modify the target to make two consecutive calls to the Loader
failed
Follow the goals above
Css-loader converts CSS to JS string
// css-loader
const cssLoader = (code) = > {
return `
const str = The ${JSON.stringify(code)}
module.exports str
`
}
module.exports = cssLoader
Copy the code
Style-loader inserts JS strings into the style tag
const styleLoader = (code) = > {
return `
if (document) {
const style = document.createElement('style')
style.innerHTML = The ${JSON.stringify(code)}
document.head.appendChild(style)
}
`
}
module.exports = styleLoader
Copy the code
And then pack it again…
The result is this
Yi? Why are there weird strings
In fact, the string added to style-loader is not the code exported by CSS-loader, but the CSS code in the STR variable, so this cannot be implemented
How is webpack style-loader implemented
Webpack style – loader source code
Injectstylesheet into styleTag (content,… code
Webpack’s implementation differs from ours in that webPack can get the required code via request
summary
This time we tried to write the loader ourselves, but ran into a pit for this reason
Because style-loader doesn’t just translate code
- Loaders like Saas-Loader and less-loader translate code from one language to another
- This loader can be called in chain mode
- But style-loader is inserting code, not translating it, so you need to find when and where to insert it
- The timing of style-loader insertion is after csS-loader gets the result
Webpack is implemented as follows:
Injectstylesheet into styleTag (content,… code
The final summary
After many attempts, we have implemented a simple packer and loader. Although it is far from webPack, we are all doing the same thing, that is, analyzing dependencies, generating bundles, and finally exporting dist files. Loader’s function is to convert non-JS modules, Convert to JS modules, because WebPack only recognizes JS
This article is quite long, thank you to see the officer here, if you feel useful, trouble to move your little hands point a thumbs-up, thank you
If you are interested in the source code, you can go to this blog to analyze the Webpack source code