webpack mini demo原理分析

341 阅读2分钟

场景引入:

存在以下三个js文件,index.js与其他两个文件存在依赖关系

//minus.js
export const minus = (a,b)=>{
    return a-b
}
//add.js
export default (a,b)=>{
    return 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);
console.log(division);

如果我们要执行index.js获取打印值,需要怎么做呢?

(1)想必你会说在node环境运行index.js,但是问题是node环境是不支持import语法的

image.png

(2)有或者有的同学会说,将js文件引入html,并启动Live server执行

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="./src/index.js" type="module"></script>
</body>
</html>

确实非常完美

image.png

但是同时也很遗憾,在Chrome 61之前的版本浏览器是不支持ES module的

那么在浏览器不支持ES6的时期,我们是怎么解决的呢?

那么就要回归我们文章的主角身上Webpack!

wepack mini demo的原理可总结为以下几步:

  • 第1步:获取主入口文件
  • 第2步:@babel/parser 解析AST语法树
  • 第3步:@babel/parser收集依赖(遍历AST,寻找import语句)
  • 第4步:@babel/core @babel/preset-env定义ES6的AST语法树转化成ES5方法
  • 第5步:利用定义的转化方法,递归解析所有依赖
  • 第6步:定义require与exports,通过eval执行代码,并且导出bundle文件

第1步:获取主入口文件

const fs = require('fs')
const getModuleInfo = (file)=>{
   const body = fs.readFileSync(file,'utf-8')
}

第2步:npm install @babel/parser 解析AST语法树

const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示我们要解析的是ES模块
    });

image.png

第3步:npm install @babel/parser收集依赖(遍历AST,寻找import语句)

const path = require('path')
const traverse = require('@babel/traverse').default
const deps = {} //存储依赖信息
traverse(ast, {
  ImportDeclaration({ node }) {
    //ImportDeclaration方法代表的是
    对type类型为ImportDeclaration的节点的处理
    const dirname = path.dirname(file)
    const abspath = './' + path.join(dirname, node.source.value) 
    //value指的是什么意思呢?其实就是import的值
    deps[node.source.value] = abspath
  },
})

image.png

第4步:npm install @babel/core @babel/preset-env将ES6的AST语法树转化成ES5

const babel = require('@babel/core')
const {code} = babel.transformFromAst(ast,null,{
      presets:["@babel/preset-env"]
 })
官方提供的预设:

@babel/preset-env for compiling ES2015+ syntax
@babel/preset-typescript for TypeScript
@babel/preset-react for React
@babel/preset-flow for Flow

image.png

第5步:递归解析所有依赖

const parseModules = (file) =>{
        const entry = getModuleInfo(file)
        const temp = [entry]
        for (let i=0; i<temp.length; i++){
            const deps = temp[i].deps
            if(deps){
                for(const key in deps){
                    if(deps.hasOwnProperty(key)){
                    temp.push(getModuleInfo(deps[key]))
                    }
                }
            }
        }
        const depsGraph = {}
        temp.forEach(moduleInfo=>{
            depsGraph[moduleInfo.file] = {
                deps:moduleInfo.deps,
                code:moduleInfo.code
            }
        })
        return depsGraph
    }

image.png

第6步:定义require与exports,导出bundle.js

const bundle = (file) =>{
        const depsGraph = JSON.stringify(parseModules(file))
        return `(function (graph) {
            function require(file) {
                function absRequire(relPath) {
                    return require(graph[file].deps[relPath])
                }
                var exports = {};
                (function (require,exports,code) {
                    eval(code)
                })(absRequire,exports,graph[file].code)
                return exports
            }
            require('${file}')
        })(${depsGraph})`
    }
    const content = bundle('./src/index.js')
    fs.mkdirSync('./dist');//创建导出文件夹
    fs.writeFileSync('./dist/bundle.js',content)//导出代码

执行结果

image.png

总结

Webpack通过分析模块之间的依赖,最终将所有模块打包成一份或者多份代码包 (bundle),供 HTML 直接引用。浏览器不支持ES 模块时期,解决ES6语法兼容问题的优秀方案