手寫(xiě)webpack

webpack是近幾年前端比較流行的打包工具,基本上是目前所有前端都必須要掌握的開(kāi)發(fā)利器。不過(guò),光停留在使用工具的階段上,是難以得到成長(zhǎng)的。因此,這篇文章將帶大家手把手自己實(shí)現(xiàn)webpack的核心功能。

其實(shí),webpack的核心功能是分為如下幾個(gè)步驟:

  1. 解析文件,并提取出各自的模塊依賴
  2. 根據(jù)各個(gè)模塊之間的依賴關(guān)系,遞歸生成依賴圖
  3. 最后將所有的依賴文件打包到一個(gè)單一的文件中

接下來(lái),我們以一個(gè)簡(jiǎn)單的例子作為開(kāi)始,作為測(cè)試用例。

先建立一個(gè)入口main.js文件:

import moduleB from "./moduleB";

console.log(moduleB());

接著,建立moduleA和moduleB兩個(gè)模塊:

export default {
    getName: () => {
        return "scq000";
    }
}

模塊B代碼如下:

import moduleA from "./moduleA";

console.log(moduleA());

其中模塊B引用模塊A,這樣我們就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的demo例子。

有了這個(gè)測(cè)試?yán)?,接著我們就可以?lái)實(shí)現(xiàn)我們自定義的webpack功能。

先創(chuàng)建一個(gè)myWebpack.js的文件,我們可以通過(guò)一個(gè)createAsset函數(shù)讀取main.js入口文件,并生成一個(gè)最終可以直接在瀏覽器上運(yùn)行的代碼。

讀取入口文件

第一步,是要先讀取文件內(nèi)容,這個(gè)實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單:

const fs = require('fs');

function createAsset(filename) {
    const content = fs.readFileSync(filename, 'utf8');
    console.log(content);
}

createAsset('./example/main.js');

可以在命令行中輸入如下命令進(jìn)行查看:

node myWebpack.js | js-beautify | highlight

解析依賴

接著,我們就需要根據(jù)源碼文件,進(jìn)行解析。這里,我們可以借助AST Explorer來(lái)生成代碼的AST語(yǔ)法樹(shù),然后找ImportDeclaration語(yǔ)句,先打印出來(lái)看看效果:

const fs = require('fs');
const babylon = require('baylon');
const traverse = require('babel-traverse').default;

function createAsset(filename) {
    const content = fs.readFileSync(filename, 'utf8');
    
    // 根據(jù)源碼內(nèi)容生成語(yǔ)法書(shū)
    const ast = babylon.parse(content, {
        sourceType: 'module'
    });
    
    // traverse方法,用來(lái)操作語(yǔ)法樹(shù)
    traverse(ast, {
        ImportDeclaration: ({node}) => {
            console.log(node);
        }
    });
}
image-20200229162940474.png

接著,我們就要根據(jù)找到的import語(yǔ)句,將對(duì)應(yīng)的依賴模塊放入到dependencies數(shù)組中去,并給它賦予id。修改代碼如下:

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;

let ID = 0;

function createAsset(filename) {
    const content = fs.readFileSync(filename, 'utf8');
    
    const ast = babylon.parse(content, {
        sourceType: 'module'
    });
    
    const dependencies = [];

    traverse(ast, {
        ImportDeclaration: ({node}) => {
            dependencies.push(node.source.value);
        }
    });

    const id = ID++;
    return {
        id,
        filename,
        dependencies
    }
};

const result = createAsset('./main.js');
console.log(result);

打印出來(lái),可以看到如下結(jié)果:

image-20200229164220420.png

遞歸生成依賴圖

有了上面的基礎(chǔ),我們就能根據(jù)入口文件遞歸生成依賴圖了:

function createGraph(entry) {
    const mainAsset = createAsset(entry);
        
    const queue = [mainAsset];
    
    for(const asset of queue) {
        const dirname = path.dirname(asset.filename);
        
        asset.mapping = {};
        
        asset.dependencies.forEach(relativePath => {
            const absolutePath = path.join(dirname, relativePath);
            
            const child = createAsset(absolutePath);
            
            asset.mapping[relativePath] = child.id;
            
            queue.push(child);
        });
    }

    console.log(queue);
}

createGraph('./main.js');

根據(jù)依賴圖,打包生成文件

最后一步,就是需要根據(jù)獲得的模塊依賴信息,合并模塊并生成可執(zhí)行的文件。

為了保證代碼可在瀏覽器上成功運(yùn)行,這里,我們需要借助babel工具進(jìn)行代碼的轉(zhuǎn)換。

先安裝依賴:

npm install babel-core babel-preset-env

接著,在createAsset方法中實(shí)現(xiàn)如下代碼:

const {code} = babel.transformFromAst(ast, null, {
    presets: ['env']
});

然后再將轉(zhuǎn)換后的代碼放入依賴圖中,便于后續(xù)拼接。

最終依賴圖結(jié)果如下:

image-20200229180150179.png

所有準(zhǔn)備工作完成后,開(kāi)始實(shí)現(xiàn)bundle方法。在bundle方法里,主要是根據(jù)依賴圖信息,將所有的模塊組裝到一個(gè)字符串中去,最后再輸出成文件就可以了。

function bundle(graph) {
    let modules = '';
    // 遍歷graph, 生成代碼
    graph.forEach(mod => {
        modules += `${mod.id}: [
            function (require, module, exports) { ${mod.code} },
        ]`
    })
    
    //組裝模塊
    const result = `(function() {
    })({${modules}})`;
    
    return result;
}
image-20200229174652978.png

接著我們需要自己去實(shí)現(xiàn)require語(yǔ)句:

    const modMap = JSON.stringify(mod.mapping);   
    modules += `${mod.id}: [
            function (require, module, exports) { ${mod.code} },
            ${modMap}
        ]`;

將modules作為參數(shù)傳給該立即執(zhí)行函數(shù),然后直接執(zhí)行第一個(gè)模塊就可以了:

(function(modules) {
    function require(id) {
        const [fn, mapping] = modules[id];
        
        function localRequire(relativePath) {
            return require(mapping[relativePath]);
        }
        
        const module = { exports: {} };
        
        fn(localRequire, module, module.exports);
        
        return module.exports;
    }
    
    require(0);
    
})({${modules}})

最終,完整代碼如下:

function bundle(graph) {
    let modules = '';
    // 遍歷graph, 生成代碼
    graph.forEach(mod => {
        const modMap = JSON.stringify(mod.mapping);   
        modules += `${mod.id}: [
                function (require, module, exports) { ${mod.code} },
                ${modMap}
            ],`;
    });
    
    //組裝模塊
    const result = `(function(modules) {
        function require(id) {
            const [fn, mapping] = modules[id];
            
            function localRequire(relativePath) {
                return require(mapping[relativePath]);
            }
            
            const module = { exports: {} };
            
            fn(localRequire, module, module.exports);
            
            return module.exports;
        }
    
        require(0);
    
    })({${modules}})`;
    
    return result;
}

至此,我們完成了最基礎(chǔ)的打包工作,可以在命令行中執(zhí)行一下試試效果:

node myWebpack.js > build.js && node build.js
image-20200229181831986.png

參考資料

https://www.youtube.com/watch?v=Gc9-7PBqOC8

——--轉(zhuǎn)載請(qǐng)注明出處--———

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容