# webpack4(四) webpack底层原理及脚手架工具分析

# 如何编写一个loader

初始化一个项目

npm init
1

安装webpack webpack-cli

npm install webpack webpack-cli --save-dev
1

创建webpack.config.js文件

var path = require('path');

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

在src文件夹下创建index.js文件

console.log('hello webpack');
1

此时测试已经可以正常打包了

在根目录创建loaders文件夹,创建replaceLoader.js文件

// 将源里的所有 'webpack' 替换为 'loader'
module.exports = function (source) {
    return source.replace('webpack', 'loader');
}
1
2
3
4

然后在webpack.config.js中添加loader依赖

var path = require('path');

module.exports = {
    //....
    module: {
        rules: [
            {
                test: /\.js/,
                use: [
                    path.resolve(__dirname, './loaders/replaceLoader.js')
                ]
            }
        ]
    },
    //...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

打包后发现已经成功使用replaceLoader将 'webpack' 都替换成了 'loader'

module的参数还可以再修改,我们再传递一个option试试看

var path = require('path');

module.exports = {
    //....
    module: {
        rules: [
            {
                test: /\.js/,
                use: [
                    {
                        loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
                        options: {
                            name: 'loaders'
                        }
                    }
                ]
            }
        ]
    },
    //...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

this.query

在loader中,使用this能接收很多东西,options就是通过this.query来接收的

module.exports = function (source) {
    return source.replace('webpack', this.query.name);
}
1
2
3

this.callback

this.callback(
  err: Error | null, //
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
);

// 1. 第一个参数必须是 Error 或者 null
// 2. 第二个参数是一个 string 或者 Buffer。
// 3. 可选的:第三个参数必须是一个可以被这个模块解析的 source map。
// 4. 可选的:第四个选项,会被 webpack 忽略,可以是任何东西(例如一些元数据)。
1
2
3
4
5
6
7
8
9
10
11

this.async

如果我们想在loader里运行异步代码怎么办,直接写是不行的,this里面提供了一个 this.async 方法

this.async 返回一个 this.callback

module.exports = function (source) {
    const options = this.query;
    const callback = this.async();
    setTimeout(() => {
        const result = source.replace('webpack', options.name);
        callback(null, result);
    }, 1000);
}
1
2
3
4
5
6
7
8

现在引用loader的时候用resovle一串很麻烦,我们想要想引入外部loader一样直接使用loader名

在webpack.config.js中

module.exports = {
    //....
    resolveLoader: {
        modules: ['node_modules', './loaders'] // 引入loader时会先去node_modules找,如果找不到再去loaders文件夹下去找
    },
    module: {
        rules: [
            {
                test: /\.js/,
                use: [
                    {
                        loader: 'replaceLoader', // 这样就可以直接写名字了
                        options: {
                            name: 'loaders'
                        }
                    }
                ]
            }
        ]
    },
    //...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 如何编写一个plugin

生成一个项目

在根目录下创建plugins,创建copyright-webpack-plugin.js 文件

class CopyrightWebpackPlugin {
    constructor(options){
        // options会拿到外面传的所有参数
        console.log('插件被使用了')
    }
    
    apply(compiler) {

    }
}

module.exports = CopyrightWebpackPlugin;
1
2
3
4
5
6
7
8
9
10
11
12

在webpack.config.js中添加插件

const path = require('path');
const CopyRightWebpackPlugin = require('./plugins/copyright-webpack-plugin'); 

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    plugins: [
        new CopyRightWebpackPlugin({
            // 这里可以传参数
        })
    ],
    output: {
        path: path.resolve(__dirname, 'dist')
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

此时打包就会打印“插件被使用了 ”

继续完善插件

插件都基于webpack的生命周期钩子来进行

参考官网提供的钩子

compiler参数是webpack的一个实例,里面存储了webpack打包相关的全部东西,compiler.hooks就是钩子。

class CopyrightWebpackPlugin {
    constructor(options){
        console.log('插件被使用了')
    }
    
    apply(compiler) {
        // 同步钩子 用 tap就可以
        compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
            console.log('compilation')
        })
        // 打断点,方便调试
        debugger;
        // webapck生命周期钩子,emit是当把文件放入dist目录前,这是一个异步钩子,所以需要用 tapAsync 和 cb
        compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
            // 将'copyright.txt' 文件在打包文件放入dist之前放在dist中
            compilation.assets['copyright.txt'] = {
                // 文件内容
                source: function () {
                    return 'copyright by zxy'
                },
                // 文件大小
                size: function () {
                    return 16
                }
            }
            // 异步钩子最后要执行一下回调
            cb();
        })
    }
}

module.exports = CopyrightWebpackPlugin;
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

为了方便我们快速调试查看里面的内容,我们可以使用node的一个调试工具

在package中我们先定义一个命令 debug --inspect 参数意思是要开启node调试工具 --inspect-brk 意思是要在第一行添加一个断点

{
    //...
    "scripts": {
        "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
        "build": "webpack"
    },
    //...
}
1
2
3
4
5
6
7
8

打开chrome,在控制台打开node标志的图标进行调试,我们在插件中可以写入 debugger 来打断点,这样在调试的时候就会在断点停止,这时我们就可以看到一些想看到的内容

# Bundler 源码编写

# 模块分析

首先在src下创建几个文件

word.js

export const word = 'hello';

1
2

message.js

import {word} from './word.js';

const message = `say ${word}`;

export default message;
1
2
3
4
5

index.js

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

然后再创建一个bundler.js文件

const fs = require('fs');

const moduleAnalyser = (filename) => {
    const content = fs.readFileSync(filename, 'utf-8');
    console.log(content);
}

moduleAnalyser('./src/index.js');
1
2
3
4
5
6
7
8

用node运行 bundler.js 可以拿到index文件里的内容了

接下来就要拿index.js里的引入的依赖 我们安装一个@babel/parser

npm install @babel/parser --save-dev
1

引用parser 会输出一个 ast 抽象语法树,可以用来分析 再安装 @babel/traverse 来把 import 进来的依赖分出来 最后输出出来的数组便是依赖路径

const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
const path = require('path');

// 分析模块
const moduleAnalyser = (filename) => {
    const content = fs.readFileSync(filename, 'utf-8');
    const ast = parser.parse(content, {
        sourceType: 'module' // 使用module方式解析
    });
    const dependencies = {};// 用来存放所有依赖路径
    traverse(ast, {
        ImportDeclaration({node}) {
            const dirname = path.dirname(filename);
            const newFile = './' + path.join(dirname, node.source.value);
            // 将绝对路径和相对路径都存储,方便后面使用
            dependencies[node.source.value] = newFile;
        }
    })
    //用babel进行编译,将es6编译成es5
    const {code} = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    return {
        filename,
        dependencies,
        code
    }
}
const moduleInfo = moduleAnalyser('./src/index.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

# Dependencies Graph 依赖图谱

通过递归循环,将一层一层的依赖及内容形成依赖图谱,并保存

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])
                );
            }
        }
    }
// 将数组转存为对象,方便使用
    const graph = {};
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    })
    return graph;
}
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 generateCode = (entry) => {
    const graph = JSON.stringify(makeDependenciesGraph(entry));
    return `
        (function(graph){
            function require(module) {
                function localRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath])
                }
                var exports = {};
                (function(require, code) {
                    eval(code)
                })(localRequire, graph[module].code);
                return exports;
            };
            require('${entry}')
        })(${graph})`
}

const code = generateCode('./src/index.js');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
更新时间: 11/8/2019, 4:51:43 PM