從實現一個簡單的webpack中看廣度深度算法

爲何要寫這篇文章

當G和百度的時候,看到一些不錯文章,可是老是給我一種印象,看了就忘記了,可是此次我打算看完以後,從算法的角度寫一篇本身理解的文章,來看看webpack這一類打包工具它們作了什麼?最困難的點在哪裏?node

思路

先看看這個圖webpack

上圖就是webpack的實現流程web

其中值得咱們疑問的地方在哪?

問題: 如何經過遍歷AST收集依賴?其中經過了什麼算法?算法

由於當有不少複雜的依賴的時候,如何解決這些依賴的問題,應該是其中最困難的點npm

實例

看看源文件

第一個文件數組

//word.js
export const word = 'hello'
複製代碼

第二個文件瀏覽器

//message.js
import { word } from './word.js';
const message = `say ${word}`
export default message;
複製代碼

第三個文件babel

//index.js
import message from './message.js'
console.log(message)
複製代碼

看到了 index --> message --> word閉包

轉化問題

看到這三個文件互相依賴的關係;你腦海裏有一些解題思路:函數

  1. 利用babel完成代碼轉換,並生成單個文件的依賴: @bable/parse 能夠生成 AST; @babel/traverse 進行AST遍歷,記錄依賴關係;最後用@babel/core和@babel/preset-env進行代碼的轉換
  2. 生成依賴圖譜
  3. 生成最後打包代碼

這就是咱們的問題,編寫一個函數,將上面的ES6 代碼轉成 ES5 並將這些文件代碼,生成一段瀏覽器能運行起來的代碼

代碼實現

按照上面的思路實現代碼

第一步:
//先安裝好相應的包
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D

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

//處理函數
function one(filename){
    // 讀入文件
    const ast = readFile(filename)
    // 遍歷AST抽象語法🌲
    const dependencies = traverseAST(ast)
    
    //經過@babel/core和@babel/preset-env進行代碼的轉換
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    
    //返回文件名稱,和依賴關係
    return {
        filename,
        dependencies,
        code
    }
}

// 最近多是看了代碼整潔之道以後,就會特別注意命名合理性和代碼塊不在同一層盡力不放在一塊兒,即便這只是一個demo
function readFile(filename){
    const content =  fs.readFileSync(filename, 'utf-8')
    const ast = parser.parse(content, {
        sourceType: 'module'//babel官方規定必須加這個參數,否則沒法識別ES Module
    })
    return ast
}

function traverseAST(ast){
    const dependencies = {}
    traverse(ast, {
        //獲取經過import引入的模塊
        ImportDeclaration({node}){
            const dirname = path.dirname(filename)
            const newFile = './' + path.join(dirname, node.source.value)
            //保存所依賴的模塊
            dependencies[node.source.value] = newFile
        }
    })
    
    return dependencies
}

複製代碼
第二步:生成依賴圖譜。
function two(entry){

    // 先拿到帶有依賴的AST對象
    const entryModule = one(entry)
    
    // 下面就是深度算法運用部分
    const graphArray = getGraphArray(entryModule)
    
    // 接下來就是生成圖譜
    const graph = getGraph(graphArray)
    
    // 返回圖譜
    return graph
}

function getGraphArray(entryModule) {
    const graphArray = [entryModule]
    for(let i = 0; i < graphArray.length; i++){
        const item = graphArray[i];
        const {dependencies} = item;//拿到文件所依賴的模塊集合(鍵值對存儲)
        for(let j in dependencies){
            graphArray.push(
                one(dependencies[j])
            )//敲黑板!關鍵代碼,目的是將入口模塊及其全部相關的模塊放入數組
        }
    }
}

function getGraph(graphArray) {
    const graph = {}
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    })
    return graph
}

// 能夠測試下:console.log(two('./src/index.js'))

複製代碼

看到這裏就應該大概知道代碼是如何處理這種AST樹的了,就和算法中處理二叉樹是同樣,找到規律,不斷的循環

第三步:生成代碼字符串
function three(entry){
    // //要先把對象轉換爲字符串,否則在下面的模板字符串中會默認調取對象的toString方法,參數變成[Object object],顯然不行
    const graph = JSON.stringify(stepTwo(entry))
    
    return ` (function(graph) { //require函數的本質是執行一個模塊的代碼,而後將相應變量掛載到exports對象上 function require(module) { //localRequire的本質是拿到依賴包的exports變量 function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function(require, exports, code) { eval(code); })(localRequire, exports, graph[module].code); return exports;//函數返回指向局部變量,造成閉包,exports變量在函數執行後不會被摧毀 } require('${entry}') })(${graph})`
}

// 能夠測試下:console.log(three('./src/index.js'))
複製代碼

總結:

其實整個下來你會發現,假設你找到了那個最爲複雜的問題,而且經過算法或者其餘的方式解決,那麼你就基本上能夠搞定webpack了;固然真實的webpack還作了不少其餘的事情,這不是咱們此次題目中的重點;閱讀各類經典的工具,你都會發現核心,最爲複雜的仍是算法!

相關文章
相關標籤/搜索