为了账号安全,请及时绑定邮箱和手机立即绑定

手写实现Webpack

2020.02.29 18:32 4519浏览

webpack是近几年前端比较流行的打包工具,基本上是目前所有前端都必须要掌握的开发利器。不过,光停留在使用工具的阶段上,是难以得到成长的。因此,这篇文章将带大家手把手自己实现webpack的核心功能。

其实,webpack的核心功能是分为如下几个步骤:

  1. 解析文件,并提取出各自的模块依赖
  2. 根据各个模块之间的依赖关系,递归生成依赖图
  3. 最后将所有的依赖文件打包到一个单一的文件中

接下来,我们以一个简单的例子作为开始,作为测试用例。

先建立一个入口main.js文件:

import moduleB from "./moduleB";

console.log(moduleB());

接着,建立moduleA和moduleB两个模块:

export default {
    getName: () => {
        return "scq000";
    }
}

模块B代码如下:

import moduleA from "./moduleA";

console.log(moduleA());

其中模块B引用模块A,这样我们就实现了一个简单的demo例子。

有了这个测试例子,接着我们就可以来实现我们自定义的webpack功能。

先创建一个myWebpack.js的文件,我们可以通过一个createAsset函数读取main.js入口文件,并生成一个最终可以直接在浏览器上运行的代码。

读取入口文件

第一步,是要先读取文件内容,这个实现起来比较简单:

const fs = require('fs');

function createAsset(filename) {
	const content = fs.readFileSync(filename, 'utf8');
    console.log(content);
}

createAsset('./example/main.js');

可以在命令行中输入如下命令进行查看:

node myWebpack.js | js-beautify | highlight

解析依赖

接着,我们就需要根据源码文件,进行解析。这里,我们可以借助AST Explorer来生成代码的AST语法树,然后找ImportDeclaration语句,先打印出来看看效果:

const fs = require('fs');
const babylon = require('baylon');
const traverse = require('babel-traverse').default;

function createAsset(filename) {
	const content = fs.readFileSync(filename, 'utf8');
    
    // 根据源码内容生成语法书
    const ast = babylon.parse(content, {
        sourceType: 'module'
    });
    
    // traverse方法,用来操作语法树
    traverse(ast, {
        ImportDeclaration: ({node}) => {
            console.log(node);
        }
    });
}

image-20200229162940474.png

接着,我们就要根据找到的import语句,将对应的依赖模块放入到dependencies数组中去,并给它赋予id。修改代码如下:

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, 'utf8');
    
    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 result = createAsset('./main.js');
console.log(result);

打印出来,可以看到如下结果:

image-20200229164220420.png

递归生成依赖图

有了上面的基础,我们就能根据入口文件递归生成依赖图了:

function createGraph(entry) {
    const mainAsset = createAsset(entry);
	    
    const queue = [mainAsset];
    
    for(const asset of queue) {
    	const dirname = path.dirname(asset.filename);
        
        asset.mapping = {};
        
        asset.dependencies.forEach(relativePath => {
            const absolutePath = path.join(dirname, relativePath);
            
            const child = createAsset(absolutePath);
            
            asset.mapping[relativePath] = child.id;
            
            queue.push(child);
        });
    }

    console.log(queue);
}

createGraph('./main.js');

根据依赖图,打包生成文件

最后一步,就是需要根据获得的模块依赖信息,合并模块并生成可执行的文件。

为了保证代码可在浏览器上成功运行,这里,我们需要借助babel工具进行代码的转换。

先安装依赖:

npm install babel-core babel-preset-env

接着,在createAsset方法中实现如下代码:

const {code} = babel.transformFromAst(ast, null, {
    presets: ['env']
});

然后再将转换后的代码放入依赖图中,便于后续拼接。

最终依赖图结果如下:

image-20200229180150179.png

所有准备工作完成后,开始实现bundle方法。在bundle方法里,主要是根据依赖图信息,将所有的模块组装到一个字符串中去,最后再输出成文件就可以了。

function bundle(graph) {
    let modules = '';
    // 遍历graph, 生成代码
    graph.forEach(mod => {
        modules += `${mod.id}: [
			function (require, module, exports) { ${mod.code} },
		]`
    })
    
    //组装模块
    const result = `(function() {
	})({${modules}})`;
    
    return result;
}

image-20200229174652978.png

接着我们需要自己去实现require语句:

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

将modules作为参数传给该立即执行函数,然后直接执行第一个模块就可以了:

(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}})

最终,完整代码如下:

function bundle(graph) {
    let modules = '';
    // 遍历graph, 生成代码
    graph.forEach(mod => {
        const modMap = JSON.stringify(mod.mapping);   
        modules += `${mod.id}: [
                function (require, module, exports) { ${mod.code} },
                ${modMap}
            ],`;
    });
    
    //组装模块
    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;
}

至此,我们完成了最基础的打包工作,可以在命令行中执行一下试试效果:

node myWebpack.js > build.js && node build.js

image-20200229181831986.png

参考资料

点击查看更多内容

本文首次发布于慕课网 ,转载请注明出处,谢谢合作

0人点赞

若觉得本文不错,就分享一下吧!

评论

相关文章推荐

正在加载中
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消