从实现一个简单的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还作了不少其余的事情,这不是咱们此次题目中的重点;阅读各类经典的工具,你都会发现核心,最为复杂的仍是算法!

相关文章
相关标签/搜索