关于webpack:敲黑板手把手带你写一个简易版webpack内附超详细分解

33次阅读

共计 9411 个字符,预计需要花费 24 分钟才能阅读完成。

明确 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 是什么?

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)

正文完
 0