likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 32

明确webpack实现的功能

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

我们的代码主要实现以下四步

graph TD
    A(1.找到入口文件) --> B(2.解析入口文件并提取出依赖)
    B --> C{3.递归创造出依赖图}
    C --> D(4.把所有文件打包成一个文件)

开始开发

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是什么?【敲黑板】手把手带你写一个简易版webpack!内附超详细分解

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.遍历所有文件,形成最终的依赖图🌟🌟🌟🌟🌟核心的一步,就一行代码,完成所有模块的遍历。

  • 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;
            //⬇️关键一步,向数组中推进子依赖
            allAsset.push(childAsset);
        });
    }

    return allAsset;
}

获取到了文件依赖图之后,我们需要把所有文件打包成一个文件然后输出。

12.新增一个方法bundle最后一个函数了!胜利在望~bundle实现的是通过graph来创造一个立即执行函数,立即执行函数的参数有全部模块的代码体。

  • mywebpack.js - bundle
function bundle(graph) {

    //自执行函数的参数
    let modules = '';
    
    //遍历所有模块,拼接modules字符串:每个id对应一个value,value里要包括该module的可执行代码
    graph.forEach(module => {
        modules += `${module.id}:[

        ],`;
    })
    //返回的结果
    const result = `
        (function() {
            
        })({${modules}})
    `;
}

到现在我们会发现,graph里只记录了每个模块对应的依赖关系,并没有包括可执行的代码体。所以下一步,我们需要获取每个模块的代码体。

修改createAsset函数,获取到代码体且编译。

首先咱们会用到babel-core,方便各个插件分析语法进行相应的处理。

安装他!npm i babel-core

还会用到 babel-preset-env,它可以根据开发者的配置,按需加载插件,可以作为预设来编译代码。

安装他! npm i babel-preset-env

  • mywebpack.js - createAsset
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++;

    const {
        code
    } = babel.transformFromAst(ast, null, {
        // 第三个参数, 告诉babel以什么方式编译我们的代码. 这里我们就用官方提供的preset-env, 编译es2015+的js代码.
        // 当然还有其他的各种预设, 可以编译ts, react等等代码.
        presets: ['env']
    })

    return {
        id,
        filename,
        dependencies,
        code
    }
}

13.把获取到的code放到result中根据commonJs的规范,每个模块的代码函数需要接收三个参数:require,module,exports。

  • module变量代表当前的模块,它是一个变量,其中exports属性就是对外的模块。加载某个模块,其实就是加载该模块的module.exports属性。
  • mywebpack.js - bundle
function bundle(graph) {
    let modules = '';

    graph.forEach(module => {
        modules += `${module.id}:[
            function(require, module, exports) {
                ${module.code}
            },
        ],`;
    })

    const result = `
        (function() {
            
        })({${modules}})
    `;

    return result;
}

接下来开始实现require函数,首先需要把mapping传到对应modules的value里面。


function bundle(graph) {
    let modules = '';

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

    const result = `
        (function() {
            
        })({${modules}})
    `;

    return result;
}

requier要接收一个参数,来表示引入了哪些代码,这个时候就可以用mapping的映射关系,使用id找到引入的代码。

function bundle(graph) {
    let modules = '';

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

    // 取出的fn是上面定义的modules里的function,mapping是${JSON.stringify(module.mapping)}
    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;
}

当当当!到这里就基本完成了我们的简易版webpack,这个时候可以node运行下mywebpack.js,把输出的结果代码复制到浏览器里看看能不能正常运行。浏览器打印出:Yolanda is a girl 就意味着你的webpack已经正常运转起来了!给自己鼓个掌吧~

最后可以优化一下流程,每次都复制到浏览器看输出结果有点费劲,可以配置下package.json用命令行操作。npm init

  • package.json
"scripts": {
    "build": "rm -rf dist.js && node mywebpack.js > dist.js"
},

最后运行

npm run build.

大功告成!

贴上mywebpack.js完整代码:

// 步骤:
// 1.找到一个入口文件;
// 2.解析这个入口文件,提起入口文件的依赖;
// 3.解析入口文件中依赖的依赖,递归的创造一个依赖图,可以描述文件间的依赖关系;
// 4.将所有的文件打包成一个文件。

//node里面的fs模块可以读取文件
const fs = require('fs')
//node的基础api 可以帮助拿到绝对路径
const path = require('path')
//babel(语法转换)的解析工具
const babylon = require('babylon')
//babel-traverse :可以像遍历对象一样遍历ast中的节点
const tranverse = require('babel-traverse').default;
//babel-core:把js文件解析为ast
const babel = require('babel-core')

//记录ID
let ID = 0;
//拿到该file的id,filename 以及 依赖的数组
function createAsset(filename){
    //同步的读取文件
    const content =fs.readFileSync(filename,'utf-8')
    //转化为ast找到相应节点
    const ast = babylon.parse(content,{
        sourceType:'module'
    })
    //定义一个数组用于储存依赖
    const dependencies = []
    //定义一个id 每调用一次creatAsset函数id++
    let id = ID++

    //遍历拿到ImportDeclaration中的source中的value
    tranverse(ast,{
        ImportDeclaration:({node})=>{
            //拿到node中的source的value push到储存依赖的数组里
            dependencies.push(node.source.value)
        }
    })

    const{ code } = babel.transformFromAst(ast , null,{
        presets:['env']
    })
    //输出一个对象,其中包括id,filename以及它所依赖的dependencies
    return {
        id,
        filename,
        dependencies,
        code
    }
}

//创建依赖图 核心!!
function createGraph(entry){
    const mainAsset = createAsset(entry)
    const allAsset = [mainAsset]
    for( let asset of allAsset){
        //拿到当前的dirname
        const dirname= path.dirname(asset.filename)
        //给asset新增一个mapping对象,方便后续查找
        asset.mapping = {}
        //将dependencies中的相对路径转化为绝对路径
        asset.dependencies.forEach(relativePath=>{
            const absolutePath = path.join(dirname,relativePath)
            //拿到绝对路径下的子元素的asset内容
            const childAsset = createAsset(absolutePath)
            //存储对应关系
            asset.mapping[relativePath]=childAsset.id
            //递归的核心一行代码!!🌟🌟🌟🌟🌟
            allAsset.push(childAsset)
        })
    }
    return allAsset
}
//创建整体结果代码块(需要立即执行)接收module作为参数
function bundle (graph){
    let modules = ''
    graph.forEach(module=>{
        modules+=`
        ${module.id}:[
            function(require,module,exports){
                ${module.code}
            },
            ${JSON.stringify(module.mapping)}
        ],
        `
    })
    //输出的结果为字符串
    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
}

//传入相对路径
const graph = createGraph('./source/entry.js')
const res = bundle(graph)
console.log(res)
转载自:https://segmentfault.com/a/1190000040235554
评论
请登录