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 的運行結果。 css

圖解webpack

原生webpack產出

在當前目錄下建立usewebpack文件夾,而後在給目錄下執行如下操做:html

  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,而後執行如下操做:node

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.jswebpack

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的命令了
    npm link
    node bin
  • 在usewebpack中使用mwebpack編譯文件
  1. 用命令行切換到usewebpack目錄,
  2. 而後執行npx mwebpack命令,能夠看到usewabpack下node_modules中的.bin目錄下出現了mwebpack.cmd和mwebpack的包。
  3. mwebpack.cmd會調用全局的mwebpack命令,而後再調用mwebpack/bin/mwebpack.js.
    npx mwebpack

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替代:es6

  • 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__,而且將路徑修改成相對於根目錄的相對路徑web

{
    "./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: shell

requireAST

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打包後的結果很大部分都是同樣的,能夠抽離出一個模板用來構建每次打包的結果:npm

// 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函數json

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,在瀏覽器中運行:

loader

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 能夠看到

plugin
##結語 webpack的主要工做:

  1. 合併option,獲取plugin註冊插件
  2. run得到入口文件,用loader對入口文件進行處理,
  3. 將其轉化爲AST進行代碼修改,遞歸分析其依賴的模塊
  4. 根據入口文件的依賴項,將其渲染到對應的模板文件,而後寫到出口文件中
相關文章
相關標籤/搜索