一文带你彻底了解webpack(二)

349 阅读8分钟

前言

话接上篇一文带你彻底了解webpack(一)前言 很多小白朋友应该都听过vite、webpack等,其实每天都在用但是自己并不清 - 掘金

上文详细介绍了webpack是什么并且带你快速上手使用,你已经彻底学会了他的基本使用,至于拓展,接下来就靠天赋异禀的你自行使用和不断摸索了,那么接下来我们进入原理篇,你应该听过一道面试题:vite为什么比webpack快?

那么看完原理篇,你也能成为“面霸”。

你用过webpack吗?

在我们使用webpack进行项目构建时,当你们公司的项目较大,而你恰好需要画画界面写写接口,本来是一件愉快的事情,但是每一次都构建都令你头皮发麻

image.png

那么,webpack为什么这么慢呢?

我们继续沿用上一篇(使用篇)的例子

main.js

import {add } from './tools/add.js'
import './styles/index.css'
console.log(add(1,2));
console.log(add(2,3))
console.log('hello world')

那么当webpack读到这份的代码时,看见了第一行,他会去第一行add.js所在的位置读取add.js,并将这份代码片段放在第一行的位置然后继续往下读,这样保证了代码顺序正确执行,但是当add.js这份文件中又引入了其他的文件,那么就会继续进入下一份文件读取,读取完再回到上一份读取的地方继续往下读,读完后再回到add.js读取的位置然后继续往下读,聪明的小伙伴发现了,这是什么?————递归,这就导致了项目的构建速度慢,当然webpack官方也在积极优化构建速度,包括发展到现在webpack的生态已经非常丰富了,他依然是现在使用最多的工具。至于vite为什么就是比他快,这是因为vite采用了另一种模式,颠覆了webpack这种传统的构建理念,关于vite我会在下一篇文章中讲到。

由于递归的存在,一个js文件引入了几十个js模块,这就存在几十次递归,这就是webpack慢的痛点所在。

打包流程

  1. 读取入口文件中的内容、
  2. 分析入口文件,递归的去读取模块所依赖的其他文件内容,生产AST语法书,举个例子:
{
    'main.js':{
        'code':"import {add} from './tools/add.js'"
        'need':{
            'add.js':{
                'code': "export const add = (a,b) => a + b"
        }
    }
}
}
  1. 根据AST语法树生成浏览器能运行的代码,例如你用的es6,但是浏览器不支持,webpack就要帮你进行降级,生成es5或者其他支持的版本(代码版本降级)

原理实操myWebpack

项目结构:

image.png add.js

export default function add(a, b) {
    return a + b;
}

minus.js

export const minus = (a, b) => a - b

index.js

import add from "./add.js";
import {minus} from "./minus.js";

const sum = add(1, 2)
const division = minus(2, 1)

console.log(sum, division) // 3 1

最后我们将这份index.js引入html,然后运行就发现,报错了,因为浏览器没办法直接读懂模块化语法import,此时构建工具就显得尤为重要,否则要么不用模块化语法写代码,要么自己写一个功能函数让浏览器读懂import语法。

因此webpack就需要有这个能力,将import转换成浏览器能读得懂的模块化语法出来,那么接下来我们创建bundle.js,来模拟实现一下webpack。

首先要读取入口文件,index.js,需要node来读文件,所以说webpack是离不开node的,源码集成了node。 初始化node项目

npm init -y
const fs  = require('fs');

const getModelInfo = (file)=>{
    const body = fs.readFileSync(file, 'utf-8');
    console.log(body)
}

getModelInfo('./src/index.js')

读出来内容如下: image.png

babel/parser分析模块

然后就是递归读取import,看更深层次嵌套的代码。那么事实上这种东西不用自己写直接用第三方的工具来分析模块

 npm i @babel/parser // 分析模块,核心作用:降级

使用一下这个工具,用来分析

const fs  = require('fs');
const parse = require('@babel/parser');
const getModelInfo = (file)=>{
    const body = fs.readFileSync(file, 'utf-8');
    const ast = parse.parse(body,{sourceType:'module'}); // 告诉babel这里用的module语法,毕竟模块化语法有很多 commonjs es6...
    console.log(ast)
}

getModelInfo('./src/index.js')

分析模块生成了一个抽象语法树

image.png

事实上他讲我们的代码分析好了,代码部分放在了ast.program.body,我们不妨打印出来看看。

image.png 他帮我们分析成了一个又一个的模块,包括import声明引入部分在哪里...

image.png

收集依赖 @babel/traverse

遍历ast整理一整条模块引入关系。

刚刚能够看到他帮我们分析成了一个数组,数组中有很多个模块,引入部分、变量部分、输出部分等等,起始位置在哪里最终位置在哪里,路径在哪里,都分析的明明白白

那么如何做遍历呢?第三方工具:@babel/traverse

npm i @babel/traverse
const fs  = require('fs');
const parse = require('@babel/parser');
const path = require('path');
const traverse = require('@babel/traverse').default;
const getModelInfo = (file)=>{
    const body = fs.readFileSync(file, 'utf-8');
    const ast = parse.parse(body,{sourceType:'module'}); // 告诉babel这里用的module语法,毕竟模块化语法有很多 commonjs es6...
    // console.log(ast.program.body)

    const deps = {}; // 各个模块的路径
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file);
            const absPath = './' + path.join(dirname,node.source.value);
            deps[node.source.value] = absPath;
        }
    })
    console.log(deps)
}

getModelInfo('./src/index.js')

然后我们就能得到这些依赖文件的路径

{ './add.js': './src\\add.js', './minus.js': './src\\minus.js' }

接下来要做的就是将各个模块加载进来。

ES6 > ES5 @babel/core @babel/preset-env

 npm i @babel/core @babel/preset-env
const fs  = require('fs');
const parse = require('@babel/parser');
const path = require('path');
const traverse = require('@babel/traverse').default;
const bable = require('@babel/core');
const getModelInfo = (file)=>{
    const body = fs.readFileSync(file, 'utf-8');
    const ast = parse.parse(body,{sourceType:'module'}); // 告诉babel这里用的module语法,毕竟模块化语法有很多 commonjs es6...
    // console.log(ast.program.body)

    const deps = {}; // 各个模块的路径
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file);
            const absPath = './' + path.join(dirname,node.source.value);
            deps[node.source.value] = absPath;
        }
    })
    console.log(deps)
    // 代码转换
    const {code} = bable.transformFromAst(ast,null,{
        presets:['@babel/preset-env']
    })
    console.log(code)
}

getModelInfo('./src/index.js')

然后我们就得到了降级后的代码


var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum, division); // 3 1

至此入口文件代码降级成功了,入口文件所需要依赖的模块地址也梳理好了,最后就是顺着模块依赖的地址把其他模块也降级最后混到一块

递归所有依赖

const fs  = require('fs');
const parse = require('@babel/parser');
const path = require('path');
const traverse = require('@babel/traverse').default;
const bable = require('@babel/core');
const getModelInfo = (file)=>{
    const body = fs.readFileSync(file, 'utf-8');
    const ast = parse.parse(body,{sourceType:'module'}); // 告诉babel这里用的module语法,毕竟模块化语法有很多 commonjs es6...
    // console.log(ast.program.body)

    const deps = {}; // 各个模块的路径
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file);
            const absPath = './' + path.join(dirname,node.source.value);
            deps[node.source.value] = absPath;
        }
    })
    console.log(deps)
    // 代码转换
    const {code} = bable.transformFromAst(ast,null,{
        presets:['@babel/preset-env']
    })
    console.log(code)
    return {file,deps,code};
}

const parseModules = (file)=>{
    const entry = getModelInfo(file);
    const temp = [entry];
    for (let i=0;i<temp.length;i++){
        const deps = temp[i].deps;
        if (deps){
            for (let j in deps){ // './add.js'
                if (deps.hasOwnProperty(j)){
                    temp.push(getModelInfo(deps[j]))
                }
            }
        }
    }
    console.log(temp)
}


// getModelInfo('./src/index.js')
parseModules('./src/index.js')  // 给一个入口文件,不仅分析入口文件,还要分析入口文件依赖的模块,再分析依赖的模块,依次递归,直到没有依赖为止

然后我们就拿到了整个依赖

{ './add.js': './src\\add.js', './minus.js': './src\\minus.js' }
"use strict";

var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum, division); // 3 1
{}
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = add;
function add(a, b) {
  return a + b;
}
{}
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.minus = void 0;
var minus = exports.minus = function minus(a, b) {
  return a - b;
};
[
  {
    file: './src/index.js',
    deps: { './add.js': './src\\add.js', './minus.js': './src\\minus.js' },
    code: '"use strict";\n' +
      '\n' +
      'var _add = _interopRequireDefault(require("./add.js"));\n' +
      'var _minus = require("./minus.js");\n' +
      'function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }\n' +
      'var sum = (0, _add["default"])(1, 2);\n' +
      'var division = (0, _minus.minus)(2, 1);\n' +
      'console.log(sum, division); // 3 1'
  },
  {
    file: './src\\add.js',
    deps: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = add;\n' +
      'function add(a, b) {\n' +
      '  return a + b;\n' +
      '}'
  },
  {
    file: './src\\minus.js',
    deps: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.minus = void 0;\n' +
      'var minus = exports.minus = function minus(a, b) {\n' +
      '  return a - b;\n' +
      '};'
  }
]

处理require

可以看见import都已经被处理成了require但是在windows环境下是没有require目前还是在node环境下

const fs  = require('fs');
const parse = require('@babel/parser');
const path = require('path');
const traverse = require('@babel/traverse').default;
const bable = require('@babel/core');
const getModelInfo = (file)=>{
    const body = fs.readFileSync(file, 'utf-8');
    const ast = parse.parse(body,{sourceType:'module'}); // 告诉babel这里用的module语法,毕竟模块化语法有很多 commonjs es6...
    // console.log(ast.program.body)

    const deps = {}; // 各个模块的路径
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file);
            const absPath = './' + path.join(dirname,node.source.value);
            deps[node.source.value] = absPath;
        }
    })
    console.log(deps)
    // 代码转换
    const {code} = bable.transformFromAst(ast,null,{
        presets:['@babel/preset-env']
    })
    console.log(code)
    return {file,deps,code};
}

const parseModules = (file)=>{
    const entry = getModelInfo(file);
    const temp = [entry];
    for (let i=0;i<temp.length;i++){
        const deps = temp[i].deps;
        if (deps){
            for (let j in deps){ // './add.js'
                if (deps.hasOwnProperty(j)){
                    temp.push(getModelInfo(deps[j]))
                }
            }
        }
    }
    // 处理一下数据结构
    const depsGraph = {};
    temp.forEach(moduleInfo=>{
        depsGraph[moduleInfo.file] = {
            code: moduleInfo.code,
            deps: moduleInfo.deps
        }
    })


    // console.log(depsGraph)
    return depsGraph
}

const bundle = (file)=>{
    const depsGraph = JSON.stringify(parseModules(file));
    return `
    (function(graph){
        function require(file){
            (function (code){eval(code)})(graph[file].code)
        }
        require('${file}')
    })(${depsGraph})
    `

}

// getModelInfo('./src/index.js')
// parseModules('./src/index.js')  // 给一个入口文件,不仅分析入口文件,还要分析入口文件依赖的模块,再分析依赖的模块,依次递归,直到没有依赖为止
const content = bundle('./src/index.js')
console.log(content)

fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js',content)

那么至此,我们已经处理了require并且把所有的代码都整合到了一起并打包成一个js文件,最终最终就可以直接把这份js引入给html,就不会出现最开始的报错问题了。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<script src="./dist/bundle.js"></script>
</body>
</html>

image.png

结语

那么通过这么一个实际例子,大家也能看见webpack的真正工作流程,看上去是套了个壳子,用的是第三方的工具来做。

最后,创作不易,如有帮助,一键三连!