工作流程图

主要介绍工作原理,webpack基础不再赘述

webpack

流程大致为,转载自

  • 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数;

  • 开始编译: 用上一步得到的参数初始化Complier对象,加载所有配置的插件,执行对象的run方法开始执行编译;

  • 确定入口: 根据配置中的entry找出所有入口文件;

  • 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤知道所有入口依赖的文件都经过了本步骤的处理;

  • 完成模块编译: 在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及他们之间的依赖关系;

  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;

  • 输出完成: 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

手动实现

分析入口文件

const fs = require('fs')
module.exports = class Webpack{ 
    constructor(options) {
        console.log(options);
        const {entry, output} = options;
        this.entry = entry
        this.output = output
    }
    run() {
        console.log('webpack run');
        this.parse(this.entry)
    }
    parse(entryFile) {
        // 分析入口文件
        const content = fs.readFileSync(entryFile, 'utf-8')
        console.log(content);
    }
}

解析依赖关系

不推荐使用字符串截取,使用@babel/parser来解析内部语法,需要安装npm包

使用parser解析读取到的文件内容

parse(entryFile) {
    // 分析入口文件
    const content = fs.readFileSync(entryFile, 'utf-8')
    console.log(content);
    
    // 分析依赖及以来的路径
    // 使用parser把内容抽象成语法树
    const ast = parser.parse(content, {
        sourceType: 'module'
    })
    console.log(ast);
}

运行结果如下

image-20200908211128939

可以发现里面折叠了3个Node结点,打开来看一下

image-20200908211728577

image-20200908211829226

什么?看懂了!!!大神收下膝盖,不过相信很多人跟我一样第一眼看到时蒙逼的吧,不慌,再好好看看

打开index.js的时候你可能会发现点什么

image-20200908212113842

3行代码,3个结点,别太早下结论,来验证一下

再加点东西

import {add} from './expo.js'
import {minus} from './expo.js'
add(1,2)
minux(3,2)
console.log('hello webpack');

然后再看解析结果(终端放不开,找了个工具盛一下)

image-20200908212502327

这下稳了

image-20200908212647856

ImportDeclaration表示引入依赖,ExpressionStatement表示表达式

然后再来根据分析结果,遍历引入的依赖,使用@babel/traverse,需要安装npm包

const dependencies = []
traverse(ast, {
    ImportDeclaration({node}) { // 可以分析表达式
        const finalName = path.join(path.dirname(entryFile), node.source.value) // 拼接路径
        // console.log(finalName);
        dependencies.push(finalName)
    }
})
console.log(dependencies);

打印一下,可以看到已经获取到了引入的依赖

image-20200908214440480

然后就是将ES6等语法转换为浏览器可以执行的代码,使用@babel/core和@babel/preset-env工具

const {code} = transformFromAst(ast, null, {
    presets: ['@babel/preset-env']
})
console.log(code);

image-20200909143854862

可以看到已经将es6的import转换成了require。到这里依赖的分析已经完成了,将解析到的结果作为一个对象返回出去

return {
    entryFile,
    dependencies,
    code
}

然后我们拿到了这么一个东西

image-20200909145714882

然后还要解析依赖中是否存在依赖。。。

image-20200909151415109

run() {
    const info = this.parse(this.entry)
    // console.log(info);
    
    // 处理其他模块,然后汇总
    this.modules.push(info)
    for(let i = 0; i < this.modules.length; i++) {
        const item = this.modules[i]
        const {dependencies} = item
        if(dependencies) {
            for(let j in dependencies) {
                this.modules.push(this.parse(dependencies[j]))
            }
        }
    }
    console.log(this.modules);
}

如果你看过webpack打包的代码,你会发现,打包出来的代码都是以对象形式存在的,我们还要继续处理

生成文件

解析完成了就要把解析好的代码输出到文件中

file(code) {
    // 生成文件
    const filePath = path.join(this.output.path, this.output.filename)
    const newCode = JSON.parse(code)
    const bundle = `(function() {
        
    })(${newCode})`
    
    fs.writeFileSync(filePath, bundle, 'utf-8')
}

新建一个dist目录,然后运行,会看到已经自动生成了一个main.js

image-20200909171409519

这时其实是有问题的,路径还是原来的引用路径,但是文件到了dist目录下,需要对路径进行处理

image-20200909173510238

因为在对象中有绝对地址,所以直接读取出来

function localRequire(relativePath) {
    return require(graph[module].dependencies[relativePath])
}

然后就来到了webpack的核心require,他模仿了Node中的require操作,是一个立即执行函数,所以文件能直接在浏览器运行

file(code) {
    // 生成文件
    const filePath = path.join(this.output.path, this.output.filename)
    const newCode = JSON.stringify(code)
    const bundle = `(function(graph) {
        function require(module){
            function localRequire(relativePath) {
                return require(graph[module].dependencies[relativePath])
            }
            var exports = {};
            (function(require, exports, code) {
                eval(code)
            })(localRequire, exports, graph[module].code)
            
            return exports
        }
        require('${this.entry}')
    })(${newCode})`
    
    fs.writeFileSync(filePath, bundle, 'utf-8')
}

代码

代码已上传码云


前端小白