如何實現一個簡單的Webpack打包器

爲何須要打包

原始的<script>標籤加載JS的方式的弊端:node

1.容易污染全局變量webpack

2.模塊加載的順序須要事先定義好web

3.模塊之間的管理要主觀瞭解清楚json

4.模塊加載過多會致使請求過多;所有打包在一塊兒影響項目的初始化瀏覽器

瞭解完爲何須要打包以後,那咱們很容易就能得出咱們打包的目的:處理模塊之間的依賴關係,從而更好地加載資源babel

打包器的基本流程

1.處理輸入文件,分析全部依賴項多線程

2.解析依賴項,生成依賴關係圖架構

3.根據依賴關係圖,生成一個在瀏覽器中可執行的代碼,輸出bundle.jsapp

打包器的具體實現

首先來生成一個代碼結果框架

- src
-- hello.js
-- world.js
-- entry.js
- webpack.js
- webpack.config.js
- package.json
entry.js
import hello from './hello.js'
import {world} from './world.js'

hello(world);
console.log('hello, webpack');
hello.js
export default function hello(name) {
  console.log(`hello ${name}!`)
}
world.js
export const world = 'world';
webpack.config.js
const path = require("path");

module.exports = {
    entry: "./src/entry.js",
    output: {
        path: path.resolve(__dirname, "./dist"),
        filename:"bundle.js"
    }
}

咱們主要是使用三個庫來實現咱們的打包

@babel/parser: babel解析器,將代碼轉化爲抽象語法樹AST

@babel/traverse: 配合babel解析器,遍歷及更新AST每個子節點

@babel/core: 獲取模塊中的可執行內容

下面咱們來一步一步實現webpack的主要代碼,先看一個基礎架構

基礎框架

const fs = require("fs")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const path = require("path")
const {
    transformFromAst
} = require("@babel/core")

module.exports = class Webpack {
    constructor(options) {
        const {
            entry,
            output
        } = options
        this.entry = entry;
        this.output = output;
    }
    // 主邏輯
    run() {}
    // 處理邏輯依賴
    handleDependence() {}
    // 生成bundle代碼
      generateBundle() {}
    // 輸出打包文件
    outputBundleFile() {}
}

handleDependence

首先來看下handleDependence

// 遞歸處理每一個文件的邏輯依賴,以及每一個文件的內部的相對地址和絕對地址的對應關係
    handleDependence(entry) {
        // 生成模塊依賴圖
        const dependenceGraph = {
            [entry]: this.createAsset(entry),
        }

        // 遞歸遍歷
        const recursionDep = (filename, assert) => {
            // 維護相對路徑和絕對路徑的對應關係
            assert.mapping = {};
            // 返回路徑的目錄名
            const dirname = path.dirname(filename);
            assert.dependencies.forEach(relativePath => {
                const absolutePath = path.join(dirname, relativePath);
                // 設置相對路徑和絕對路徑的對應關係
                assert.mapping[relativePath] = absolutePath;
                // 若是沒有該絕對路徑的依賴關係
                if (!dependenceGraph[absolutePath]) {
                    const child = this.createAsset(absolutePath);
                    dependenceGraph[absolutePath] = this.createAsset(absolutePath);
                    if (child.dependencies.length > 0) {
                        // 遞歸
                        recursionDep(absolutePath, child);
                    }
                }
            })
        }

        for (let filename in dependenceGraph) {
            let assert = dependenceGraph[filename]
            recursionDep(filename, assert);
        }
        return dependenceGraph;
    }

其中createAsset爲

// 獲取每一個文件的可執行代碼和依賴關係
createAsset(entry) {
        // 讀取文件內容
        const entryContent = fs.readFileSync(entry, 'utf-8')
        // 使用 @babel/parser(JavaScript解析器)解析代碼,生成 ast(抽象語法樹)
        const ast = babelParser.parse(entryContent, {
            sourceType: "module"
        })

        // 從 ast 中獲取全部依賴模塊(import),並放入 dependencies 中
        const dependencies = []
        traverse(ast, {
            // 遍歷全部的 import 模塊,並將相對路徑放入 dependencies
            ImportDeclaration: ({
                node
            }) => {
                dependencies.push(node.source.value)
            }
        })

        // 獲取文件內容
        const {
            code
        } = transformFromAst(ast, null, {
            presets: ['@babel/preset-env'],
        });

        return {
            code,
            dependencies,
        }
}

handleDependence 返回的結果格式以下所示:

{ 
  './src/entry.js': 
   { 
     code: '"use strict";\n\nvar _hello = _interopRequireDefault(require("./hello.js"));\n\nvar _world = require("./world.js");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\n(0, _hello["default"])(_world.world);\nconsole.log(\'hello, webpack\');',
     dependencies: [ './hello.js', './world.js' ],
     mapping: { './hello.js': 'src/hello.js', './world.js': 'src/world.js' } 
   },
  'src/hello.js': 
   { 
     code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = hello;\n\nfunction hello(name) {\n  console.log("hello ".concat(name, "!"));\n}',
     dependencies: [] 
   },
  'src/world.js': 
   { 
     code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.world = void 0;\nvar world = \'world\';\nexports.world = world;',
     dependencies: [] 
   } 
}

咱們下一步的目的就是要根據這個映射表來生成出來一個可執行的代碼。

咱們最終生成的是一個文件,那咱們根據生成的對應結構,拼接成字符串,最終輸入到文件中,再執行便可。

爲了避免污染全局環境,咱們採用當即執行的方法。

綜上,咱們的當即執行方法能夠描述爲

const result = `
  (function() {
  })()
`

同時,咱們再來看下處理後的代碼,以./src/entry.js文件爲例子

"use strict";
                        Object.defineProperty(exports, "__esModule", {
                value: true
            });
            exports["default"] = message;

            var _hello = require("./hello.js");

            var _name = require("./name.js");

            function message() {
                console.log("".concat(_hello.hello, " ").concat(_name.name, "!"));
            }

使用的是 CommonJS 規範,而瀏覽器不支持 commonJS(瀏覽器沒有 module 、exports、require、global);因此這裏咱們須要實現它們,並注入到包裝器函數內。

let modules = ''
for (let filename in Dependence) {
  let mod = graph[filename]
  modules += `'${filename}': {
    code: function(require, module, exports) {
      ${mod.code}
    },
    mapping: ${JSON.stringify(mod.mapping)},
    },`
}

咱們再擴展下result的生成,將代碼中的相對地址替換成絕對地址。

const result = `
    (function(modules) {
      function require(moduleId) {
        const {fn, mapping} = modules[moduleId]
        function localRequire(name) {
          return require(mapping[name])
        }
        const module = {exports: {}}
        fn(localRequire, module, module.exports)
        return module.exports
      }
      require('${this.entry}')
    })({${modules}})
  `

entry是入口地址, modules是處理後的dependence;

generateBundle

那generateBundle爲

generateBundle(dependenceMap) {
        let modules = '';
        for (let filename in dependenceMap) {
            let mod = dependenceMap[filename]
            modules += `'${filename}': {
                code: function(require, module, exports) {
                    ${mod.code}
                },
                mapping:  ${JSON.stringify(mod.mapping)},
            },`
        }
        const result = `
            (function(modules) {
                function require(moduleId) {
                    const {code, mapping} = modules[moduleId]
                    function localRequire(name) {
                        return require(mapping[name])
                    }
                    const module = {exports: {}}
                    code(localRequire, module, module.exports)
                    return module.exports
                }
                require('${this.entry}')
            })({${modules}})
        `;
        return result;
    }

outputBundle

最後的outputBundle方法

// 輸出打包文件
    outputBundleFile(bundle) {
        const filePath = path.join(this.output.path, this.output.filename)
        fs.access(filePath, (err) => {
            if (!err) {
                fs.writeFileSync(filePath, bundle, 'utf-8');
            } else {
                console.log(err)
                fs.mkdir(this.output.path, {
                    recursive: true
                }, (err) => {
                    if (err) throw err;
                    fs.writeFileSync(filePath, bundle, 'utf-8');
                });
            }
        });
    }

run

run() {
    this.outputBundleFile(this.generateBundle(this.handleDependence(this.entry)));
}

bundle.js

(function (modules) {
    function require(moduleId) {
        const {
            code,
            mapping
        } = modules[moduleId]

        function localRequire(name) {
            return require(mapping[name])
        }
        const module = {
            exports: {}
        }
        code(localRequire, module, module.exports)
        return module.exports
    }
    require('./src/entry.js')
})({
    './src/entry.js': {
        code: function (require, module, exports) {
            "use strict";

            var _hello = _interopRequireDefault(require("./hello.js"));

            var _world = require("./world.js");

            function _interopRequireDefault(obj) {
                return obj && obj.__esModule ? obj : {
                    "default": obj
                };
            }

            (0, _hello["default"])(_world.world);
            console.log('hello, webpack');
        },
        mapping: {
            "./hello.js": "src/hello.js",
            "./world.js": "src/world.js"
        },
    },
    'src/hello.js': {
        code: function (require, module, exports) {
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });
            exports["default"] = hello;

            function hello(name) {
                console.log("hello ".concat(name, "!"));
            }
        },
        mapping: undefined,
    },
    'src/world.js': {
        code: function (require, module, exports) {
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });
            exports.world = void 0;
            var world = 'world';
            exports.world = world;
        },
        mapping: undefined,
    },
})

放到瀏覽器中,能夠完美執行。

hello world!
hello, webpack

完整代碼

const fs = require("fs")
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default
const path = require("path")
const options = require("./webpack.config.js");
const {
    transformFromAst
} = require("@babel/core")

class Webpack {
    constructor(options) {
        const {
            entry,
            output
        } = options
        this.entry = entry
        this.output = output
        this.modulesArr = []
    }
    // 主邏輯
    run() {
        this.outputBundleFile(this.generateBundle(this.handleDependence(this.entry)));
    }
    // 處理邏輯依賴
    handleDependence(entry) {
        // 生成模塊依賴圖
        const dependenceGraph = {
            [entry]: this.createAsset(entry),
        }

        // 遞歸遍歷
        const recursionDep = (filename, assert) => {
            // 維護相對路徑和絕對路徑的對應關係
            assert.mapping = {};
            // 返回路徑的目錄名
            const dirname = path.dirname(filename);
            assert.dependencies.forEach(relativePath => {
                const absolutePath = path.join(dirname, relativePath);
                // 設置相對路徑和絕對路徑的對應關係
                assert.mapping[relativePath] = absolutePath;
                // 若是沒有該絕對路徑的依賴關係
                if (!dependenceGraph[absolutePath]) {
                    const child = this.createAsset(absolutePath);
                    dependenceGraph[absolutePath] = this.createAsset(absolutePath);
                    if (child.dependencies.length > 0) {
                        // 遞歸
                        recursionDep(absolutePath, child);
                    }

                }
            })
        }

        for (let filename in dependenceGraph) {
            let assert = dependenceGraph[filename]
            recursionDep(filename, assert);
        }
        return dependenceGraph;
    }
    createAsset(entry) {
        // 讀取文件內容
        const entryContent = fs.readFileSync(entry, 'utf-8')
        // 使用 @babel/parser(JavaScript解析器)解析代碼,生成 ast(抽象語法樹)
        const ast = parser.parse(entryContent, {
            sourceType: "module"
        })

        // 從 ast 中獲取全部依賴模塊(import),並放入 dependencies 中
        const dependencies = []
        traverse(ast, {
            // 遍歷全部的 import 模塊,並將相對路徑放入 dependencies
            ImportDeclaration: ({
                node
            }) => {
                dependencies.push(node.source.value)
            }
        })

        // 獲取文件內容
        const {
            code
        } = transformFromAst(ast, null, {
            presets: ['@babel/preset-env'],
        });

        return {
            code,
            dependencies,
        }
    }
    generateBundle(dependenceMap) {
        let modules = '';
        for (let filename in dependenceMap) {
            let mod = dependenceMap[filename]
            modules += `'${filename}': {
                code: function(require, module, exports) {
                    ${mod.code}
                },
                mapping:  ${JSON.stringify(mod.mapping)},
            },`
        }
        const result = `
            (function(modules) {
                function require(moduleId) {
                    const {code, mapping} = modules[moduleId]
                    function localRequire(name) {
                        return require(mapping[name])
                    }
                    const module = {exports: {}}
                    code(localRequire, module, module.exports)
                    return module.exports
                }
                require('${this.entry}')
            })({${modules}})
        `;
        return result;
    }
    // 輸出打包文件
    outputBundleFile(bundle) {
        const filePath = path.join(this.output.path, this.output.filename)
        fs.access(filePath, (err) => {
            if (!err) {
                fs.writeFileSync(filePath, bundle, 'utf-8');
            } else {
                console.log(err)
                fs.mkdir(this.output.path, {
                    recursive: true
                }, (err) => {
                    if (err) throw err;
                    fs.writeFileSync(filePath, bundle, 'utf-8');
                });
            }
        });
    }

}

(new Webpack(options)).run();

後續擴展

1.支持多文件

2.輸出公共bundle

3.多線程打包

4.插件機制設計

等等

參考資料

1.https://juejin.im/post/5e0d52716fb9a047f0002407?utm_source=gold_browser_extension

2.https://mp.weixin.qq.com/s/uTAJZoqFFDn5cfkwcYr11Q

相關文章
相關標籤/搜索