webpack由浅入深——(webapck简易版)

webpack系列文章

  1. webpack由浅入深——(webpack基础配置)
  2. webpack由浅入深——(webpack优化配置)
  3. webpack由浅入深——(tapable)
  4. webpack由浅入深——(webapck简易版)
  5. webpack由浅入深——(ast、loader和plugin)

Webpack流程概括

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,Webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

原生webpack产出

在当前目录下创建usewebpack文件夹,然后在给目录下执行以下操作:

  1. 安装依赖的模块
$ npm init -y
$ yarn add webpack webpack-cli html-webpack-plugin
复制代码
  1. 编写webpack配置文件
const path = require('path');
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {},
    plugins: []
}
复制代码
  1. 源文件
  • src/index.js
let a=require('./a');
console.log(a);
复制代码
  • src/a.js
let b=require('./base/b');
module.exports='a'+b;
复制代码
  • src/base/b.js
module.exports='b';
复制代码
  1. 产出bundle.js
(function(modules) {// 启动函数
    // 模块的缓存
    var installedModules = {}; 
    // webpack实现的require方法
    function __webpack_require__(moduleId) {
      // 检查缓存中是否存在此模块ID
      if (installedModules[moduleId]) {
        return installedModules[moduleId].exports;
      }
      // 缓存中没有此模块ID,创建一个模块并且放置到缓存中
      var module = (installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
      });
      // 执行模块函数为module.export赋值
      modules[moduleId].call(
        module.exports,
        module,
        module.exports,
        __webpack_require__
      );
      // 标志模块已经加载
      module.l = true;
      // 返回模块的export属性
      return module.exports;
    }
    // 加载入口模块并且返回export
    return __webpack_require__((__webpack_require__.s = "./src/index.js"));
  })({
    "./src/a.js": function(module, exports, __webpack_require__) {
      eval(
        "let b=__webpack_require__(\"./src/base/b.js\");\r\nmodule.exports='a'+b;\n\n"
      );
    },
    "./src/base/b.js": function(module, exports) {
      eval("module.exports='b';\n\n");
    },
    "./src/index.js": function(module, exports, __webpack_require__) {
      eval(
        'let a=__webpack_require__("./src/a.js");\r\nconsole.log(a);\r\n\n\n'
      );
    }
  }); 
复制代码

编写mwebpack

在当前目录下创建mwebpack文件夹,并且在文件下创建mwebpack,然后执行以下操作:

1. 创建项目package.json

{
  "name": "mwebpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  // 添加了bin选项,使用命令行来运行./bin/mwebpack.js
  "bin": {
    "mwebpack": "./bin/mwebpack.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
复制代码

2. 创建/bin/mwebpack.js

  • 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数
#! /usr/bin/env node        /*标注文件的运行环境*/
const path = require('path');
const fs = require('fs');
//当前工作目录
const root = process.cwd();
//配置文件和 Shell 语句中读取与合并参数,这里简化逻辑,没有处理shell部分
let options = require(path.resolve('webpack.config.js'));
复制代码
  • 开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
#! /usr/bin/env node    
const path = require('path');
const fs = require('fs');
const root = process.cwd();
//引入Compiler
const Compiler = require('../lib/Compiler'); 

let options = require(path.resolve('webpack.config.js'));

//初始化compiler对象加载所有配置的插件
let compiler = new Compiler(options); 
// 执行对象的 run 方法开始执行编译
compiler.run();
复制代码

3. 初始化Compiler

在当mwebpack目录下创建/bin/Compiler.js

const path = require('path');
const fs = require('fs');
class Compiler {
    constructor(options){
        this.options = options;
    }
    run(){
        console.log('---------start---------')
    }
}
module.exports = Compiler
复制代码

4. 连接usewebpack和mwebpack

  • 将nmwebpack/bin/mwebpack.js链接到全局D:/dev/node.js/mwebpack(node安装在D盘)
  1. 用命令行切换到mwebpack目录,
  2. 然后执行npm link,那么nmwebpack就和npm和npx一样成为nodejs的命令了
  • 在usewebpack中使用mwebpack编译文件
  1. 用命令行切换到usewebpack目录,
  2. 然后执行npx mwebpack命令,可以看到usewabpack下node_modules中的.bin目录下出现了mwebpack.cmd和mwebpack的包。
  3. mwebpack.cmd会调用全局的mwebpack命令,然后再调用mwebpack/bin/mwebpack.js.

5. 完善run函数

  • 确定入口:根据配置中的entry找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
const path = require('path');
const fs = require('fs');
class Compiler {
    constructor(options){
        this.options = options;
    }
    run(){
        let that = this;
        let {entry} = this.options; // 获取webpck.config.js中的entry
        this.root = process.cwd();      
        this.entryId = null;        //记录入口的id,这里采用单入口简化
        this.modules = {};          //缓存入口的依赖,这里采用单入口简化
        
        // 找出该模块依赖的模块
        //再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
        this.buildModule(path.resolve(this.root, entry), true);
        
        // 输出资源
        this.emitFile();
    }
}
module.exports = Compiler
复制代码

6. 编写buildModule

  • 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
const path = require('path');
const fs = require('fs');
class Compiler {
    constructor(options){
        this.options = options;
    }
    run(){
        let that = this;
        let {entry} = this.options; 
        this.root = process.cwd();      
        this.entryId = null;        
        this.modules = {};          
        this.buildModule(path.resolve(this.root, entry), true);
        this.emitFile();
    }
     getSource(modulePath) {
        let source = fs.readFileSync(modulePath, 'utf8');
        
        //TODO:loader的处理逻辑写在这里,后面会提到
        
        return source; 
        
    }
    buildModule(modulePath,isEntry){
        let that = this; 
        let source = this.getSource(modulePath);//获取源代码
        
        //生成相对于工作根目录的模块ID,相对路径exp:'./sec/index'
        let moduleId = './' + path.relative(this.root, modulePath);
        
        //如果是入口的话把id赋给compiler对象的入口
        if (isEntry) {
            this.entryId = moduleId;
        }
    
        //获取AST的编译结果,获取依赖的模块,并且将代码进行转换
        let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId));
        this.modules[moduleId] = sourcecode;
        
        //递归解析依赖的模块
        dependencies.forEach(dependency => that.buildModule(path.join(that.root, dependency)));
    }
    emitFile(){
        
    }
}
module.exports = Compiler
复制代码

7. 编写parse函数

  • 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理

代码转换成AST,webpack中使用的Acorn,这里使用babel-types,babel-traverse,babel-generator替代:

  • babylon把源码转成AST
  • babel-types生成节点或者判断节点类型
  • babel-traverse遍历AST,捕获指定的节点
  • babel-generator将AST重新生成代码
npm install babylon babel-types babel-generator babel-traverse
复制代码

查看原生webpack生成的bundle.js,需要将require换成__webpack_require__,并且将路径修改为相对于根目录的相对路径

{
    "./src/a.js": function(module, exports, __webpack_require__) {
      eval(
        "let b=__webpack_require__(\"./src/base/b.js\");\r\nmodule.exports='a'+b;\n\n"
      );
    },
    "./src/base/b.js": function(module, exports) {
      eval("module.exports='b';\n\n");
    },
    "./src/index.js": function(module, exports, __webpack_require__) {
      eval(
        'let a=__webpack_require__("./src/a.js");\r\nconsole.log(a);\r\n\n\n'
      );
    }
  }
复制代码

利用https://astexplorer.net/可以看到require转换成AST:

const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const t = require('babel-types');
//采用es6的写法,所以要在后面添加.default
const traverse = require('babel-traverse').default;
const generator = require('babel-generator').default;
class Compiler {
    constructor(options){
        this.options = options;
    }
    run(){
        let that = this;
        let {entry} = this.options; 
        this.root = process.cwd();      
        this.entryId = null;        
        this.modules = {};          
        this.buildModule(path.resolve(this.root, entry), true);
        this.emitFile();
    }
    getSource(modulePath) {
        let source = fs.readFileSync(modulePath, 'utf8');
        //TODO:loader的处理逻辑写在这里,后面会提到
        return source; 
    }
    buildModule(modulePath,isEntry){
        let that = this;
        let source = this.getSource(modulePath);
        let moduleId = './' + path.relative(this.root, modulePath);
        if (isEntry) {
            this.entryId = moduleId;
        }
        let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId));
        this.modules[moduleId] = sourcecode;
        dependencies.forEach(dependency => that.buildModule(path.join(that.root, dependency)));
    }
    parse(source, parentPath) {
        let that = this;
        let ast = babylon.parse(source);    //源码转语法树
        let dependencies = [];      //存储依赖的模块路径
        //遍历AST找到对应的节点进行修改
        traverse(ast, {
            CallExpression(p) {//p当前路径
                if (p.node.callee.name == 'require') {
                    let node = p.node;
                    //修改方法名
                    node.callee.name = '__webpack_require__';
                    // 得到模块名exp:'./a'
                    let moduleName = node.arguments[0].value;
                    //如果需要的话,添加.js后缀 
                    moduleName += (moduleName.lastIndexOf('.') > 0 ? '' : '.js');
                    //得到依赖模块的id,exp:'./src/a'
                    let moduleId = './' + path.relative(that.root, path.join(parentPath, moduleName));
                    //相对于根目录的相对路径
                    node.arguments = [t.stringLiteral(moduleId)];
                    //把模块id放置到当前模块的依赖列表里
                    dependencies.push(moduleId);
                }
            }
        });
        //将修改的AST重新生成代码
        let sourcecode = generator(ast).code;
        return { sourcecode, dependencies };
    }
    emitFile(){
        
    }
}
module.exports = Compiler
复制代码

8. 编写emitFile函数

  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会

每次编译打包后,都会发现webpack打包后的结果很大部分都是一样的,可以抽离出一个模板用来构建每次打包的结果:

// MainTemplate这里采用ejs模板简化
(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 = "<%-entryId%>"));
  })({
   <%for (let moduleId in modules) {let source = modules[moduleId];%>
        "<%-moduleId%>":(function(module,exports,__webpack_require__){eval(`<%-source%>`);}),
    <% }%>
  }); 
复制代码

完善emitFile函数

const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const t = require('babel-types');
const traverse = require('babel-traverse').default;
const generator = require('babel-generator').default;
const ejs = require('ejs');     //引入ejs
class Compiler {
    constructor(options){
        this.options = options;
    }
    run(){
        let that = this;
        let {entry} = this.options; 
        this.root = process.cwd();      
        this.entryId = null;        
        this.modules = {};          
        this.buildModule(path.resolve(this.root, entry), true);
        this.emitFile();
    }
    getSource(modulePath) {
        let source = fs.readFileSync(modulePath, 'utf8');
        //TODO:loader的处理逻辑写在这里,后面会提到
        return source; 
    }
    buildModule(modulePath,isEntry){
        let that = this;
        let source = this.getSource(modulePath);
        let moduleId = './' + path.relative(this.root, modulePath);
        if (isEntry) {
            this.entryId = moduleId;
        }
        let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId));
        this.modules[moduleId] = sourcecode;
        dependencies.forEach(dependency => that.buildModule(path.join(that.root, dependency)));
    }
    parse(source, parentPath) {
        let that = this;
        let ast = babylon.parse(source);    
        let dependencies = [];    
        traverse(ast, {
            CallExpression(p) {
                if (p.node.callee.name == 'require') {
                    let node = p.node;
                    node.callee.name = '__webpack_require__';
                    let moduleName = node.arguments[0].value;
                    moduleName += (moduleName.lastIndexOf('.') > 0 ? '' : '.js');
                    let moduleId = './' + path.relative(that.root, path.join(parentPath, moduleName));
                    node.arguments = [t.stringLiteral(moduleId)];
                    dependencies.push(moduleId);
                }
            }
        });
        let sourcecode = generator(ast).code;
        return { sourcecode, dependencies };
    }
    emitFile(){
        // 读取模板文件
        let entryTemplate = fs.readFileSync(path.join(__dirname, 'entry.ejs'), 'utf8');
        // 获取渲染的数据
        let { entryId, modules } = this;
        // 将数据渲染到模板上
        let source = ejs.compile(entryTemplate)({
            entryId,
            modules
        });
        //找到目标路径
        let target = path.join(this.options.output.path, this.options.output.filename);
        //将渲染后的模板目标文件
        fs.writeFileSync(target, source);
    }
}
module.exports = Compiler
复制代码

输出的bundle.js文件:

(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 a = __webpack_require__("./src\\a.js");
console.log(a);`);}),
    
    "./src\a.js":(function(module,exports,__webpack_require__){eval(`let b = __webpack_require__("./src\\base\\b.js");
module.exports = 'a' + b;`);}),
    
    "./src\base\b.js":(function(module,exports,__webpack_require__){eval(`module.exports = 'b';`);}),
    
  }); 
复制代码

9. 实现loader功能

上面的webpack已经具备打包js的功能了,但是还不能打包css等文件,原生的webpack是通过各种loader来打包css等其他文件的,所以再getSource时调用loader,将其他文件处理成js,然后进行后面的操作

const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const t = require('babel-types');
const traverse = require('babel-traverse').default;
const generator = require('babel-generator').default;
const ejs = require('ejs');     //引入ejs
class Compiler {
    constructor(options){
        this.options = options;
    }
    run(){
        let that = this;
        let {entry} = this.options; 
        this.root = process.cwd();      
        this.entryId = null;        
        this.modules = {};          
        this.buildModule(path.resolve(this.root, entry), true);
        this.emitFile();
    }
    getSource(modulePath) {
        let source = fs.readFileSync(modulePath, 'utf8');
        
        //获取webpack.config.js中的rules
        let rules = that.options.module.rules;

        //遍历rules调用loader
        for (let i = 0; i < rules.length; i++) {
            let rule = rules[i];
            // 用rule的test中正则匹配文件的类型是否需要使用laoder
            if (rule.test.test(modulePath)) {
                //获取rule中的loaders,例如['style-laoder','css-loader']
                let loaders = rule.use;
                let length = loaders.length;    //loader的数量 
                let loaderIndex = length - 1;   // 往右向左执行
                
                // loader遍历器
                function iterateLoader() {
                    let loaderName = loaders[loaderIndex--];
                    //loader只是一个包名,需要用require引入
                    let loader = require(join(that.root, 'node_modules', loaderName));
                    //使用loader,可以看出loader的本质是一个函数
                    source = loader(source);
                    if (loaderIndex >= 0) {
                        iterateLoader();
                    }
                }
                
                //遍历执行loader
                iterateLoader();
                break;
            }
        }
        return source; 
    }
    buildModule(modulePath,isEntry){
        let that = this;
        let source = this.getSource(modulePath);
        let moduleId = './' + path.relative(this.root, modulePath);
        if (isEntry) {
            this.entryId = moduleId;
        }
        let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId));
        this.modules[moduleId] = sourcecode;
        dependencies.forEach(dependency => that.buildModule(path.join(that.root, dependency)));
    }
    parse(source, parentPath) {
        let that = this;
        let ast = babylon.parse(source);    
        let dependencies = [];    
        traverse(ast, {
            CallExpression(p) {
                if (p.node.callee.name == 'require') {
                    let node = p.node;
                    node.callee.name = '__webpack_require__';
                    let moduleName = node.arguments[0].value;
                    moduleName += (moduleName.lastIndexOf('.') > 0 ? '' : '.js');
                    let moduleId = './' + path.relative(that.root, path.join(parentPath, moduleName));
                    node.arguments = [t.stringLiteral(moduleId)];
                    dependencies.push(moduleId);
                }
            }
        });
        let sourcecode = generator(ast).code;
        return { sourcecode, dependencies };
    }
    emitFile(){
        let entryTemplate = fs.readFileSync(path.join(__dirname, 'entry.ejs'), 'utf8');
        let { entryId, modules } = this;
        let source = ejs.compile(entryTemplate)({
            entryId,
            modules
        });
        let target = path.join(this.options.output.path, this.options.output.filename);
        fs.writeFileSync(target, source);
    }
}
module.exports = Compiler
复制代码

在usewebpack创建mode_modules/less-loader.js(为了说明loader的原理不使用的第三方的loader)

//less-loader的作用将less文件转化为css文件
var less = require('less');
module.exports = function (source) {
    let css;
    less.render(source, (err, output) => {
        css = output.css;
    });
    return css.replace(/\n/g, '\\n', 'g');
}
复制代码

在usewebpack创建mode_modules/less-loader.js

//style-loader的功能就是将加载的css文件放在style标签中插入到页面
module.exports = function (source) {
    let str = `
      let style = document.createElement('style');
      style.innerHTML = ${JSON.stringify(source)};
      document.head.appendChild(style);
    `;
    return str;
}
复制代码

在usewebpack创建/src/index.less,

@color:red;
body{
    color:@color;
}
复制代码

修改usewebpack中/src/index.js,

require('index.less')
复制代码

修改usewebpack中package.json

const path = require('path');
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.less$/,
                use: ['style-loader', 'less-loader']
            }
        ]
    },
    plugins: []
}
复制代码

创建一个页面引用打包后的js,在浏览器中运行:

10. 实现plugin功能

原生webpack支持很多种插件,在webpack编译的过程中的各个阶段使用,常见的一些钩子:

  • entryOption 读取配置文件
  • afterPlugins 加载所有的插件
  • run 开始执行编译流程
  • compile 开始编译
  • afterCompile 编译完成
  • emit 写入文件
  • done 完成整体流程
  • 修改bin/mwebpack.js

注册规则阶段的钩子,供用户订阅来执行插件。

const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const t = require('babel-types');
const traverse = require('babel-traverse').default;
const generator = require('babel-generator').default;
const ejs = require('ejs');
//使用tapable来创建发布者,利用call等来触发
const { SyncHook } = require('tapable');
class Compiler {
    constructor(options){
        this.options = options;
         this.hooks = {
            entryOption: new SyncHook(),
            afterPlugins: new SyncHook(),
            run: new SyncHook(),
            beforeCompile: new SyncHook(),
            afterCompile: new SyncHook(),
            emit: new SyncHook(),
            afterEmit: new SyncHook(),
            done: new SyncHook(),
        }
    }
    run(){
        let compiler = this;
        compiler.hooks.run.call();              //触发run
        let {entry} = this.options; 
        this.root = process.cwd();      
        this.entryId = null;        
        this.modules
        compiler.hooks.beforeCompile.call();    //触发beforeCompile
        this.buildModule(path.resolve(this.root, entry), true);
        compiler.hooks.afterCompile.call();     //afterCompile
        this.emitFile();
        compiler.hooks.afterEmit.call();        //触发afterEmit
        compiler.hooks.done.call();             //触发done
    }
    getSource(modulePath) {
        let source = fs.readFileSync(modulePath, 'utf8');
        let rules = that.options.module.rules;
        for (let i = 0; i < rules.length; i++) {
            let rule = rules[i];
            if (rule.test.test(modulePath)) {
                let loaders = rule.use;
                let length = loaders.length;    
                let loaderIndex = length - 1; 
                function iterateLoader() {
                    let loaderName = loaders[loaderIndex--];
                    let loader = require(join(that.root, 'node_modules', loaderName));
                    source = loader(source);
                    if (loaderIndex >= 0) {
                        iterateLoader();
                    }
                }
                iterateLoader();
                break;
            }
        }
        return source; 
    }
    buildModule(modulePath,isEntry){
        let that = this;
        let source = this.getSource(modulePath);
        let moduleId = './' + path.relative(this.root, modulePath);
        if (isEntry) {
            this.entryId = moduleId;
        }
        let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId));
        this.modules[moduleId] = sourcecode;
        dependencies.forEach(dependency => that.buildModule(path.join(that.root, dependency)));
    }
    parse(source, parentPath) {
        let that = this;
        let ast = babylon.parse(source);    
        let dependencies = [];    
        traverse(ast, {
            CallExpression(p) {
                if (p.node.callee.name == 'require') {
                    let node = p.node;
                    node.callee.name = '__webpack_require__';
                    let moduleName = node.arguments[0].value;
                    moduleName += (moduleName.lastIndexOf('.') > 0 ? '' : '.js');
                    let moduleId = './' + path.relative(that.root, path.join(parentPath, moduleName));
                    node.arguments = [t.stringLiteral(moduleId)];
                    dependencies.push(moduleId);
                }
            }
        });
        let sourcecode = generator(ast).code;
        return { sourcecode, dependencies };
    }
    emitFile(){
        this.hooks.emit.call();             //触发emit
        let entryTemplate = fs.readFileSync(path.join(__dirname, 'entry.ejs'), 'utf8');
        let { entryId, modules } = this;
        let source = ejs.compile(entryTemplate)({
            entryId,
            modules
        });
        let target = path.join(this.options.output.path, this.options.output.filename);
        fs.writeFileSync(target, source);
    }
}
module.exports = Compiler
复制代码
#! /usr/bin/env node    
const path = require('path');
const fs = require('fs');
const root = process.cwd();
const Compiler = require('../lib/Compiler');
let options = require(path.resolve('webpack.config.js'));
let compiler = new Compiler(options); 
compiler.hooks.entryOption.call();     //触发entryOptions
let {plugins} = options;        //获取webpack.config.js中的plugns进行注册
plugins.forEach(plugin => {
    plugin.apply(compiler)
});
compiler.hooks.afterPlugins.call(),     //触发afterPlugins
compiler.run();
复制代码

修改usewebpack中的webpack.config.js

const path = require('path');
//为了简要说明webpack插件的原理,不采用require第三方的插件
class EntryOptionWebpackPlugin {
    apply(compiler) {
        compiler.hooks.entryOption.tap('Plugin', (option) => {
            console.log('EntryOptionWebpackPlugin');
        });
    }
}
class AfterPlugins {
    apply(compiler) {
        compiler.hooks.afterPlugins.tap('Plugin', (option) => {
            console.log('AfterPlugins');
        });
    }
}
class RunPlugin {
    apply(compiler) {
        compiler.hooks.run.tap('Plugin', (option) => {
            console.log('RunPlugin');
        });
    }
}
class CompileWebpackPlugin {
    apply(compiler) {
        compiler.hooks.compile.tap('Plugin', (option) => {
            console.log('CompileWebpackPlugin');
        });
    }
}
class AfterCompileWebpackPlugin {
    apply(compiler) {
        compiler.hooks.afterCompile.tap('Plugin', (option) => {
            console.log('AfterCompileWebpackPlugin');
        });
    }
}
class EmitWebpackPlugin {
    apply(compiler) {
        compiler.hooks.emit.tap('Plugin', () => {
            console.log('EmitWebpackPlugin');
        });
    }
}
class DoneWebpackPlugin {
    apply(compiler) {
        compiler.hooks.done.tap('Plugin', (option) => {
            console.log('DoneWebpackPlugin');
        });
    }
}
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.less$/,
                use: ['style-loader', 'less-loader']
            }
        ]
    },
    plugins: [
        new EntryOptionWebpackPlugin(),
        new AfterPlugins(),
        new RunPlugin(),
        new CompileWebpackPlugin(),
        new AfterCompileWebpackPlugin(),
        new EmitWebpackPlugin(),
        new DoneWebpackPlugin()
    ]
}
复制代码

执行npx mwebpack 可以看到

##结语 webpack的主要工作:

  1. 合并option,获取plugin注册插件
  2. run获得入口文件,用loader对入口文件进行处理,
  3. 将其转化为AST进行代码修改,递归分析其依赖的模块
  4. 根据入口文件的依赖项,将其渲染到对应的模板文件,然后写到出口文件中
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值