手写一个简易版的mini-webpack

在写一个mini-webpack之前,需要先了解一个概念:AST。

AST(abstract syntax tree)抽象语法树

抽象语法树:在计算机科学中,抽象语法树,或者成为语法树,是源代码语法结构中的一种抽象表示。它以树状的形式表现编程语言的语法结构。树上的每个节点都表示源代码中的每个结构。

个人理解就是:AST抽象语法树根据JavaScript代码解析得到,JavaScript代码中的各个部分都可以在AST中表示出来。

AST的形成了反映出各部分代码之间的关系。

手写Webpack的核心思想

我们可以把webpack想象成一个黑匣子,给这个黑匣子一个入口文件(index.js),这个入口文件在这个黑匣子里面经过一系列操作,最后输出bundle.js文件。核心过程可以用下面的图来表示:

手写一个简易版的mini-webpack_第1张图片

大体分为以下几个步骤:

  • 读取入口文件index.js,对index.js中的代码进行解析生成AST抽象语法树。

  • 根据AST抽象语法树,分别生成文件依赖关系图和浏览器可以执行的ES5代码。

  • 实现CMD API,输出bundle文件。

具体实现

首先准备一个样例。

// ./src/index.js

import info from './info.js'
console.log(info)
// ./src/info.js
import { name } from "./consts.js";
export default `my name is ${name}`
// ./src/consts.js
export const name = "yanwuhc"

在src文件夹外面创建一个fc_webpack.js文件,开始mini-webpack的书写。

由于所有文件都需要解析,因此,先实现单个文件解析的方法createAsset

生成AST抽象语法树

首先需要读取文件的内容:

function createAsset(filepath){
    // 读取入口文件内容
    const content = fs.readFileSync(filepath, 'utf-8');
}

接着,使用@babel/parser来生成AST抽象语法树。

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

    // 利用@babel/parser将ES6代码装换成AST生成树
    const ast = parser.parse(content, {
        sourceType: "module" // 需要注明传入的数据是模块化代码,不然会解析不成功
    })
}

解析AST生成依赖关系

这里有个问题:如何根据AST找到一个文件的依赖文件?

实际上,查看AST,可以发现AST中有一个ImportDeclaration属性(表示源代码中的import节点),这个属性上面的source属性的value值指向该文件的依赖文件路径。利用这个,就可以生成文件的依赖关系。

function createAsset(filepath){
    const content = fs.readFileSync(filepath, 'utf-8');
    const ast = parser.parse(content, {
        sourceType: "module"
    })

    // 在AST里面,找到 import 节点的 source属性 的 value属性
    const dependencise = []; // 创建一个数组用来存储依赖路径
    traverse(ast, {
        /*
            原始JavaScript表达式,实际上用到的是path中的node值,
            里面有个source属性,在里面有个value属性,我们利用的是这一部分
      
            ImportDeclaration: (path, state)=>{
                console.log(path)
            }
        */
       ImportDeclaration: ({node})=>{
           dependencise.push(node.source.value);
       }
    })
}

到这里,dependencise中存放的就是当前文件所有依赖文件的相对路径

// index.js 生成的依赖关系如下
[ './info.js' ]

解析AST生成ES5代码

利用AST生成ES5代码,需要借助@babel/core来实现。同时,在使用@babel/core的时候,需要借用@babel/preset-env这个插件来完成。

function createAsset(filepath){
    const content = fs.readFileSync(filepath, 'utf-8');
    const ast = parser.parse(content, {
        sourceType: "module"
    })
    const dependencise = [];
    traverse(ast, {
       ImportDeclaration: ({node})=>{
           dependencise.push(node.source.value);
            
       }
    })
    // 利用AST生成ES5代码
    const { code } = babel.transformFromAstSync(ast, null, {
        presets: ["@babel/preset-env"]
    })
}

经过babel.transformFromAstSync转换之后的内容中,只要提取其中的code属性出来就可以了。获得的code代码如下所示:

// index.js 解析生成的ES5-code
"use strict";
var _info = _interopRequireDefault(require("./info.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
console.log(_info["default"]);

最后,需要将上面转换的结果导出去,供后面的函数使用。

let ID = 0;

function createAsset(filepath){
    const content = fs.readFileSync(filepath, 'utf-8');
    const ast = parser.parse(content, {
        sourceType: "module"
    })
    const dependencise = [];
    traverse(ast, {
       ImportDeclaration: ({node})=>{
           dependencise.push(node.source.value);
            
       }
    })
    const { code } = babel.transformFromAstSync(ast, null, {
        presets: ["@babel/preset-env"]
    })

    let id = ID++;
    // 将转换的所有结果返回出去
    return {
        id,
        filepath,
        code,
        dependencise
    }
}

注意:导出的内容中新增了一个id属性,这是作为解析当前文件的唯一标识。导出的结果如下:

{
  id: 0,
  filepath: './src/index.js',
  code: '"use strict";\n' +
    '\n' +
    'var _info = _interopRequireDefault(require("./info.js"));\n' +
    '\n' +
    'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
    '\n' +
    'console.log(_info["default"]);',
  dependencies: [ './info.js' ]
}

循环解析多个文件,创建文件依赖关系图

createAsset方法完成了单个文件的解析:传入一个文件的路径,会返回解析该文件生成的一个对象(包含idfilepathcodedependencise属性)。利用这个函数,可以解析多个文件得到一个结果数组。

创建文件依赖关系图,实际上就是利用createAsset这个方法来解析所有文件。

重点是如何做到所有文件都能被解析到?这里使用队列循环的思想进行文件解析,直到解析完成为止。

function createGraph(entry){
    const mainAsset = createAsset(entry);
    
    // 使用队列循环来创建依赖关系图
    let queue = [mainAsset];

    // 循环处理-形成依赖
    for(const asset of queue){
        const dirname = path.dirname(asset.filepath);

        asset.mapping = {}; // 形成相对路径和模块id的依赖关系 

        asset.dependencise.forEach(relativePath=>{
            const absolutePath = path.join(dirname, relativePath);

            const child = createAsset(absolutePath);

            asset.mapping[relativePath] = child.id;  // 形成相对路径和模块id的映射关系
            
            queue.push(child)
        })
    }
    return queue;
}

这里添加了一个mapping属性,用于建立相对路径和模块id之间的关系(在后面打包生成bundle会用到)。

解析得到的文件依赖关系图如下,包含所有的文件解析结果,以及一个mapping对应关系表。

[
  {
    id: 0,
    filepath: './src/index.js',
    code: '"use strict";\n' +
      '\n' +
      'var _info = _interopRequireDefault(require("./info.js"));\n' +
      '\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
      '\n' +
      'console.log(_info["default"]);',
    dependencies: [ './info.js' ],
    mapping: { './info.js': 1 }
  },
  {
    id: 1,
    filepath: 'src\\info.js',
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = void 0;\n' +
      '\n' +
      'var _consts = require("./consts.js");\n' +
      '\n' +
      'var _default = "my name is ".concat(_consts.name);\n' +
      '\n' +
      'exports["default"] = _default;',
    dependencies: [ './consts.js' ],
    mapping: { './consts.js': 2 }
  },
  {
    id: 2,
    filepath: 'src\\consts.js',
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.name = void 0;\n' +
      'var name = "yanwuhc";\n' +
      'exports.name = name;',
    dependencies: [],
    mapping: {}
  }
]

实现CMD API,生成bundle文件的内容

上一步中,生成了graph文件依赖关系图。这一步,我们要利用生成的graph来生成最终的bundle.js

首先,需要对graph数据进行改造,生成清晰的module-map表。

function bundle(graph){
    // 对生成的graph进行改造生成清晰的module-map表
    let modules = '';
    graph.forEach(mod=>{
        modules += `
            ${mod.id}:[
                function(require, module, exports){
                    ${mod.code}
                },
                ${JSON.stringify(mod.mapping)}
            ],
        `
    })
}

得到的结果如下:每个解析生成文件的id对应一个数组,数组存放了一个function和一个mapping对应关系表。其中,functionrequiremoduleexports需要在后面实现。

0:[
    function(require, module, exports){
        "use strict";

        var _info = _interopRequireDefault(require("./info.js"));

        function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

        console.log(_info["default"]);
    },
    {"./info.js":1}
],

    1:[
        function(require, module, exports){
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });
            exports["default"] = void 0;

            var _consts = require("./consts.js");

            var _default = "my name is ".concat(_consts.name);

            exports["default"] = _default;
        },
        {"./consts.js":2}
    ],

        2:[
            function(require, module, exports){
                "use strict";

                Object.defineProperty(exports, "__esModule", {
                    value: true
                });
                exports.name = void 0;
                var name = "yanwuhc";
                exports.name = name;
            },
            {}
        ],

上面得到的modules将作为参数传入一个立即执行函数中,在立即执行函数中,必须实现requiremoduleexports这三个api。

实际上,该立即执行函数只需要执行第一个解析生成的代码即可(由index.js生成)。

const result = `
        (function(modules){
            // 实现 require
            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}})
    `

fn传入参数使用localRequire的原因是,在生成的代码中,require传入的参数是一个相对路径。因此,在这里需要将id转换为相对路径,mapping属性就起到作用了。

最后生成输出的代码为:

(function(modules){
    // 实现require API
    function require(id){
        const [fn, mapping] = modules[id];

        const localRequire = relativePath=>{
            return require(mapping[relativePath])
        }

        const module = {
            exports: {}
        }

        fn(localRequire, module, module.exports);

        return module.exports
    }

    // 打包后的文件,只需要执行入口文件就行
    require(0)
})({
    0:[
        function(require, module, exports){
            "use strict";

            var _info = _interopRequireDefault(require("./info.js"));

            function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

            console.log(_info["default"]);
        },
        {"./info.js":1}
    ],

    1:[
        function(require, module, exports){
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });
            exports["default"] = void 0;

            var _consts = require("./consts.js");

            var _default = "my name is ".concat(_consts.name);

            exports["default"] = _default;
        },
        {"./consts.js":2}
    ],

    2:[
        function(require, module, exports){
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });
            exports.name = void 0;
            var name = "yanwuhc";
            exports.name = name;
        },
        {}
    ],
})

复制粘贴到浏览器中,看是否能输出:

手写一个简易版的mini-webpack_第2张图片

输出bundle.js文件

const graph = createGraph("./src/index.js")
const bundleContent = bundle(graph);

fs.writeFileSync('bundle.js', bundleContent);

源码

https://gitee.com/yanwuhc/mini-webpack

欢迎大家一起交流学习!

你可能感兴趣的:(webpack,webpack,javascript)