【敲黑板】手把手带你写一个简易版webpack!内附超详细分解

业界 作者:SegmentFault 2021-06-25 13:16:12

作者:炸鸡排我们走

来源:SegmentFault 思否社区

明确webpack实现的功能

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
我们的代码主要实现以下四步

开始开发

1.目录下新建三个js源文件
  • entry.js
import message from './message.js'
console.log(message)
  • message.js
import name from "./name.js"
export default `${name} is a girl`
  • name.js
export const name = 'Yolanda'
梳理下依赖关系,我们的入口文件是entry.js,entry依赖message,message依赖name。
2.新建一个mywebpack.js文件,首先读取一下entry入口文件中的内容。
读取文件需要用到node的一个基础api-fs(文件系统),fs.readFileSync可以同步拿到js文件中的代码内容。
  • mywebpack.js
const fs = require('fs')
function creatAsset (filename){
    const content = fs.readFileSync(filename,'utf-8')
    console.log(content)
}
creatAsset('./source/entry.js')
在当前目录下运行一下命令, 看一下输出
node mywebpack.js
输出内容为entry.js中的代码:
import message from './message.js';
console.log(message);
3. 分析ast,思考如何解析出entry.js文件中的依赖
可以使用ast工具 https://astexplorer.net/
看一下entry.js文件的ast是什么?

3.1 可以看到最上级是一个File, File中包含换一个program, 就是我们的程序
3.2 在program的body属性里, 就是我们各种语法的描述
3.3 可以看到第一个就是 ImportDeclaration, 也就是引入的声明.
3.4 ImportDeclaration里有一个source属性, 它的value就是引入的文件地址 './message.js'
4.生成entry.js的ast
首先安装一个Babylon(基于Babel的js解析工具)
npm i babylon
  • mywebpack.js
const fs = require('fs');
const babylon = require('babylon');

function createAsset(filename) {
    const content = fs.readFileSync(filename, 'utf-8');

    const ast = babylon.parse(content, {
        sourceType: 'module'
    });

    console.log(ast);
}

createAsset('./source/entry.js');
可以看到输出了一个Object, 这就是咱们entry.js的AST.
5.基于这个ast,找到entry.js的ImportDeclaration中的source.value
首先,需要遍历出ImportDeclaration节点,那就需要一个工具:babel-traverse(可以像遍历对象一样遍历ast中的节点)
npm i babel-traverse
然后利用它遍历并获取到对应节点,提供一个函数来操作此节点。
  • mywebpack.js
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;

function createAsset(filename) {
    const content = fs.readFileSync(filename, 'utf-8');

    const ast = babylon.parse(content, {
        sourceType: 'module'
    });

    traverse(ast, {
        ImportDeclaration: ({
            node
        }) => {
            console.log(node)
        }
    })
}

createAsset('./source/entry.js');
6.获取entry.js的依赖
可能会出现多个依赖,这里需要建一个数组存储。

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

function createAsset(filename) {
    const content = fs.readFileSync(filename, 'utf-8');

    const ast = babylon.parse(content, {
        sourceType: 'module'
    });

    const dependencies = [];

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

    console.log(dependencies);
}

createAsset('./source/entry.js');
可以自行打印一下dependencies数组看看.这里输出的是一个包括ImportDeclaration.source.value的数组。
7.优化creatAsset函数,增加id用于区分文件
  • mywebpack.js
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, 'utf-8');

    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 mainAsset = createAsset('./source/entry.js');
console.log(mainAsset);
运行一下看看, 是不是返回了正确的结果.
8.现在我们有了单个文件的依赖了,接下来尝试建立模块间的依赖关系
新建一个createGraph函数,在这个函数里引用createAsset。
同时entry这个参数是动态的,所以createGraph接收entry这个参数。
  • mywebpack.js - createGraph
function createGraph(entry) {
    const mainAsset = createAsset(entry);
}

const graph = createGraph('./source/entry.js');
console.log(graph);
声明一个数组:allAsset用于存储全部的asset
  • mywebpack.js - createGraph
function createGraph(entry) {
    const mainAsset = createAsset(entry);
}

const graph = createGraph('./source/entry.js');
console.log(graph);
9.把相对路径转换为绝对路径
我们在dependencies中存储的都是相对路径,但是我们需要绝对路径才能拿到模块的Asset,这个时候要想办法拿到每个模块的绝对路径。

这里用到了node的另一个基础API:path,其中用到了它的两个方法:
1.path.dirname(获取当前文件的路径名)
2.path.join(拼接路径)
  • mywebpack.js - createGraph
function createGraph(entry) {
    const mainAsset = createAsset(entry);
    const allAsset = [mainAsset];

    for (let asset of allAsset) {
        const dirname = path.dirname(asset.filename);
        asset.dependencies.forEach(relativePath => {
            const absoultePath = path.join(dirname, relativePath);
            const childAsset = createAsset(absoultePath);
        });
    }
}
10.我们还需要一个map,用于记录dependencies和childAsset之间的关系
map可以存储模块间的依赖关系,方便后续的查找。

  • mywebpack.js - createGraph
function createGraph(entry) {
    const mainAsset = createAsset(entry);
    const allAsset = [mainAsset];

    for (let asset of allAsset) {
        const dirname = path.dirname(asset.filename);

        asset.mapping = {};

        asset.dependencies.forEach(relativePath => {
            const absoultePath = path.join(dirname, relativePath);

            const childAsset = createAsset(absoultePath);

            asset.mapping[relativePath] = childAsset.id;
        });
    }
}
11.遍历所有文件,形成最终的依赖图

关注公众号:拾黑(shiheibook)了解更多

赞助链接:

关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接