阅读 1749

webpack原理解析(一)实现一个简单的webpack

背景

Webpack 是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。webpack如此强大,其内部机制到底是怎样的,今天我们来一探究竟。

准备工作

新建一个目录,执行npm init -y 初始化一个package.json文件

新建src下index.js,作为入口文件,export.js作为依赖的模块

新建webpack.config.js,这里就按照webpack4的基础配置写一下

新建bundle.js作为入口进行webpack构建 创建lib下webpack.js

根据配置读取入口文件

bundle.js:

//读取到webpackd 的配置文件
const options = require("./webpack.config.js")
const Webpack = require("./lib/webpack.js")
new Webpack(options).run()
复制代码

webpack.js 这里写了一个Webpack类,bundle.js实例化这个类,将参数(引入的基础配置的内容)传进来并执行run方法

module.exports = class Webpack {
    constructor(options){
        console.log(options)
    }
    run(){
        console.log("hello")
    }
}
复制代码

此时执行node bundle.js :

image
已经可以根据基础配置读取到入口模块了

分析出依赖模块

我这里写的index.js是这样:

import { sayHi } from './export.js'
sayHi("hfj")
console.log("hello webpack index")
复制代码

此时需要借助node的核心模块-- filesystem来读到内容

 const conts = fs.readFileSync(entryFile,'utf-8')
复制代码

接下来借助@babel/parser(记得自己安装和引入。。)把内容抽象成语法树

const ast = parser.parse(conts, {
            sourceType: "module"
          });
复制代码

打印下ast.program.body

image
看type,哪里是依赖模块,哪里是表达式,已经很明显了,接下来通过@babel/traverse来提取依赖模块:

const dependencies = {}
          traverse(ast,{
            ImportDeclaration({node}){
                const newPath = "./" + path.join(
                    path.dirname(entryFile),
                    node.source.value
                    )
                dependencies[node.source.value] = newPath
                console.log(dependencies)
            }
        })
复制代码

注意下,这里做了一个处理,将依赖模块以对象的形式放到了dependencies里。

编译内容

接下来,通过@babel/core将ast处理成内容

const {code} = transformFromAst(ast,null,{
            presets: ["@babel/preset-env"]
        })
        console.log(code)
复制代码

打印的code如下:

image
内容成功拿到了!

遍历依赖模块

以上是对入口文件进行的编译,接下来还要看入口模块的依赖模块有哪些,分别按照刚才的方法进行内容分析,最后做一个汇总(根据dependence来判断的是否还有依赖模块):

 run(){
       const info = this.analysis(this.entry)
        this.modulesArr.push(info)
        for(let i=0;i<this.modulesArr.length;i++) {
            const item = this.modulesArr[i]
            const { dependencies } = item;
            if(dependencies) {
                for(let j in dependencies){
                    this.modulesArr.push(this.analysis(dependencies[j]))
                }
            }
        }
        // console.log(this.modules)
        //数组结构转换
        const obj = {}
        this.modulesArr.forEach((item) => {
            obj[item.entryFile] = {
                dependencies:item.dependencies,
                code:item.code
            }
        })
    }
复制代码

最终是将modulesArr转化成了对象,方便后续处理

输出浏览器可执行的js代码

此处划重点! webpack最终是将模块化的js打包成一个chunk,打包后的js到dist(webpack基础配置里设置的output)

首先用了fs.writeFileSync将生成的bundle写入filePath(filePath之前有在this.output保存) 接下来就是处理obj(上一步骤中的汇总后的模块)了:

核心是用eval()来执行obj中的code,同时需要处理requrie和export require和export通过形参传入,之后执行自己写的reqire和export require处理了模块路径,exports是一个对象,看源码--

const bundle = `(function(graph){
            function require(moduleId){
                function localRequire(relativePath){
                   return require(graph[moduleId].dependencies[relativePath]) 
                }
                var exports = {};
                (function(require,exports,code){
                    eval(code)
                })(localRequire,exports,graph[moduleId].code)
                return exports;
            }
            require('${this.entry}')
        })(${newCode})`
复制代码

执行node bundle.js,在dist文件夹下输出如下js:

(function(graph){
            function require(moduleId){
                function localRequire(relativePath){
                   return require(graph[moduleId].dependencies[relativePath]) 
                }
                var exports = {};
                (function(require,exports,code){
                    eval(code)
                })(localRequire,exports,graph[moduleId].code)
                return exports;
            }
            require('./src/index.js')
        })({"./src/index.js":{"dependencies":{"./export.js":"./src\\export.js"},"code":"\"use strict\";\n\nvar _export = require(\"./export.js\");\n\n(0, _export.sayHi)(\"hfj\");\nconsole.log(\"hello webpack index\");"},"./src\\export.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.sayHi = void 0;\n\nvar sayHi = function sayHi(params) {\n  console.log(\"hello ~\" + params);\n};\n\nexports.sayHi = sayHi;"}})
复制代码

在浏览器执行:输出:

image

完成~

总结

webpack通过入口文件逐层遍历到模块依赖,进行代码分析、代码转换,最终生成可在浏览器运行的打包后的代码

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

以上实现了一个简易版本的webpack

下一篇:webpack原理解析(二)loader和plugin机制

如有兴趣,欢迎进一步交流探讨~

附github地址:github.com/manli-tongh…

参考:Webpack官网

关注我们