Fork me on GitHub

webpack16--webpack底层解析

编写一个loader

先初始化一个webpack项目:

1
2
mkdir loader && cd loader
npm init -y

然后安装一波webpack

1
npm install webpack webpack-cli -D

新建一个src目录在下面新增一个index.js的文件

1
console.log('chynb');

然后在package.json中添加上一个打包的命令:

1
2
3
"scripts": {
"build":"webpack"
},

新建一个loaders的文件夹,还有里面放一个replaceLoaders.js的文件,在里面写上loader相关的代码

1
2
3
4
module.exports = function(source){ // 注意这里是不能使用箭头函数的
console.log(this.query); // this.query是能取到options里面的参数的,然后以对象的形式输出
return source.replace('chynb',this.query.name)
}

然后写一个webpack.config.js的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const path = require('path')
module.exports = {
mode:'development',
entry: {
main:'./src/index.js',
},
module:{
rules: [{
loader: path.resolve(__dirname,'./loaders/replaceLoader.js'),
options:{
name:'wdlj'
}
// test: /\.js/,
// use: [path.resolve(__dirname,'./loaders/replaceLoader.js')]
}]
},
output:{
path: path.resolve(__dirname,'dist'),
filename:'[name].js'
}
}

然后运行npm run build命令就会发打包出来的js中chynb就变成了wdlj

上面其实除了query之外也可以使用其他的一些参数.

引入一下webpack的一些官方模块:

1
2
3
4
5
6
7
8
const loaderUtils = require('loader-utils');
module.exports = function(source){ // 这里是不能使用箭头函数的
// console.log(this.query);
const options = loaderUtils.getOptions(this);
// console.log(options);
return source.replace('wd',options.name);
}

同样可以拿到options里面的参数.

如果我们想在loader里面做一些异步的操作,比如说我们将替换那个操作延时1s之后再返回,我们就需要使用webpack官网的一个API去进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
const loaderUtils = require('loader-utils');
module.exports = function(source){ // 这里是不能使用箭头函数的
// 这里的source指的是index.js里面的内容
const options = loaderUtils.getOptions(this);
// console.log(options);
const callback = this.async(); // 声明这里是一个异步的loader
setTimeout(()=>{
const result = source.replace('wd',options.name);
callback(null,result); // callback的参数第一个一般为error第二个为返回的结果
},1000)
// return source.replace('wd',options.name);
}

然后使用命令npm run build,这样loader就可以延时1s来对index.js的内容进行打包.

使用自己的loaders

我们在上面的基础上写两个loaders.

这两个loader分别分同步和异步的loaders,

/loaders/replaceLoader.js

1
2
3
4
5
6
const loaderUtils = require('loader-utils');
module.exports = function(source){ // 这里是不能使用箭头函数的
const options = loaderUtils.getOptions(this);
return source.replace('hello',options.name);
}

/loaders/replaceLoader.js

1
2
3
4
5
6
7
8
9
10
11
const loaderUtils = require('loader-utils');
module.exports = function(source){ // 这里是不能使用箭头函数的
const options = loaderUtils.getOptions(this);
const callback = this.async();
setTimeout(()=>{
const result = source.replace('zoomdong',options.name);
callback(null,result);
},1000)
}

然后配置一波webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const path = require('path')
module.exports = {
mode:'development',
entry: {
main:'./src/index.js',
},
// 这个配置的作用是当你使用loaders的时候去对应的文件夹下面找相应的loader
resolveLoader: {
modules: ['node_modules','./loaders']
},
module:{
rules: [{
test: /\.js/,
use:[
{
loader:'replaceLoader',
options: {
say:'goodbye'
}
},{
loader:'replaceLoaderAsync',
options: {
name:'wd'
}
},
]
}]
},
output:{
path: path.resolve(__dirname,'dist'),
filename:'[name].js'
}
}

index.js写上一些简单的代码:

1
console.log('hello zoomdong');

编写plugin

当我们在源代码中引入文件的时候,我们可以利用loader去处理这些文件,plugin则是帮我们处理一些打包后的事情.

从前面我们就可以看出来了,每次我们使用plugin的时候,都需要去使用一个new操作符,所以一般上而言plugin其实就是一个clas类

新建一个叫做plugins的文件夹,里面放入一个copyright-webpack-plugin.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CopyrightWebpackPlugin{
apply(compiler){
compiler.hooks.emit.tap('CopyrightWebpackPlugin',(compilation)=>{
console.log('compiler');
})
// 异步的时候
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin',(compilation,cb) => {
// console.log(compilation.assets);//可以查看打包生成的内容
// console.log(23333);
compilation.assets['copyright.txt'] = {
source: function(){
return 'Copyright by zoomdong'
},
size: function(){
return '21'
}
}
cb();
})
}
}
module.exports = CopyrightWebpackPlugin;

每次在我们打包的时候,都会生成一个用于声明版权的文件

然后在src下面随便建一个js文件

/src/index.js

1
console.log('hello world');

然后配置一波webpack.config.js

里面写上一些关于打包的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const path = require('path');
const CopyRightWebpackPlugin = require('./plugins/copyright-webpack-plugin')
module.exports = {
mode:'development',
entry: {
main:'./src/index.js',
},
plugins: [
new CopyRightWebpackPlugin({
name:'wd'
})
],
output:{
path: path.resolve(__dirname,'dist'),
filename:'[name].js'
}
}

bundler源码编写

在这里一章节中我们不使用webpack,而是我们自己手写一个类似于webpack的打包工具从而来学习一下webpack的底层打包原理

模块分析

使用high-light来对nodejs跑出来的代码进行高亮化的设置

1
2
3
4
5
npm install cli-highlight -g
npm install @babel/parser --save // 这个方法用来分析js的语法
npm install @babel/traverse --save // 这是一个用于分析AST的模块
npm install @babel/core --save // babel的核心模块
npm install @babel/preset-env --save

使用的时候

1
node xxx.js | highnight

建立一个叫做bundler的文件夹,然后在里面新建一个叫做src目录
里面有index.js,message.js,word.js

word.js

1
export const word = 'hello'

message.js

1
2
3
4
5
import { word } from './word.js';
const message = `say ${word}`
export default message

index.js

1
2
3
import message from './message.js'
console.log(message);

然后在根目录下面创建一个bundler.js的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
// @babel/traverse使用的是import导入
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
const moduleAnalyser = (filename) =>{
const content = fs.readFileSync(filename,'utf-8');
const ast = (parser.parse(content,{
sourceType:'module',
}))
const dependencies = {};
traverse(ast,{
ImportDeclaration({ node }){
const dirname = path.dirname(filename)
const newFile = './' + path.join(dirname,node.source.value) // 这个是我们打包的时候要使用的路径
// console.log(newFile);
// console.log(node);
dependencies[node.source.value]= newFile;
// console.log(dependencies); //对入口文件的依赖分析
}
});
// 把抽象语法树转换为一个对象,这个时候的code和之前见到的就不同了.(成了一端翻译好的代码了)
const { code } = babel.transformFromAst(ast,null,{
presets: ["@babel/preset-env"]
});
// console.log(code);
return {
filename,
dependencies,
code
}
// console.log(ast.program.body);
}
const moduleInfo = moduleAnalyser('./src/index.js');
console.log(moduleInfo);

找到所有的依赖函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const makeDependenciesGraph = (entry) => {
// 拿到对入口文件做的依赖的方式
const entryModule = moduleAnalyser(entry);
const graphArray = [entryModule];
for (let i = 0; i < graphArray.length; i++) {
const item = graphArray[i];
const { dependencies } = item;
if(dependencies) {
for(let j in dependencies) {
graphArray.push(moduleAnalyser(dependencies[j]));
// moduleAnalyser(dependencies[j]) // 达到一个递归的作用,拿到每一个依赖
}
}
}
const graph = {};
graphArray.forEach((item)=>{
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
// console.log(graph);
return graph;
}

代码生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 一个生成代码的函数,把src目录下不能运行的内容打包成可以使用的代码
const generator = (entry) => {
const graph = JSON.stringify(makeDependenciesGraph(entry));
return `
(function(){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire , exports, graph[module].code);
return exports;
};
require('${entry}')
})(${graph})
`;
}

源代码地址

github地址

-------------本文结束感谢您的阅读-------------