webpack打包原理解析

1、找到二维码生成插件的官方编译库 all.min .js,以这个文件为参考,找到项目中所有的JS中,有二维码的方法,统一放到这个文件中,再进行替换?
2、创建一个示例demo.html文件,把找到的二维码相关的js文件放到这个html文件中引入调用?
百分百会报错,然后根据这个报错去找对应的方法?
这个方法的问题在于,如何初始化引入?以前没做过。
3、了解webapck编译文件的编译思路,找到当前方法的对应的原方法,将找到的方法,全部干掉,再引入all.min.js文件,能跑得通,不会有bug就OK了。可能造成的问题,现有文件中,有可能有没有被调用的方法遗留。

分析webapck编译文件,了解编译思路

1、找到二维码生成插件的官方编译库 all.min .js,以这个文件为参考,找到项目中所有的JS中,有二维码的方法,统一放到这个文件中,再进行替换?
2、创建一个示例demo.html文件,把找到的二维码相关的js文件放到这个html文件中引入调用?
百分百会报错,然后根据这个报错去找对应的方法?
这个方法的问题在于,如何初始化引入?以前没做过。
3、了解webapck编译文件的编译思路,找到当前方法的对应的原方法,将找到的方法,全部干掉,再引入all.min.js文件,能跑得通,不会有bug就OK了。可能造成的问题,现有文件中,有可能有没有被调用的方法遗留。

分析webapck编译文件,了解编译思路
!function (modules) {
    
    
    /**
     * 函数说明
     * 整个函数声明了一个变量installedMoudules,
     * 函数webpack_require,给函数添加了属性
     * m 保存的是传入的模块数组modules
     * c installedModules变量
     * d
     * r
     * t
     * n
     * o
     * p一个空字符串
     * 
    */
   // modules就是一个数组,参数就是一个个我们声明的模块
    var installedModules = [];
    function __webpack_require__(moduleId) {
    
    
        // moduleId 顾名思义,我们声明的模块ID
        if(installedMOdules[moduleId])
            return installedMOdules[moduleId].exports;
        var module = installedModules[moduleId] = {
    
    
            i: moduleId,
            l: false,
            export: []
        };

        //调模块的函数,modules
        modules[moduleId].call(module.export, module, module.exports, __webpack_require__);
        module.l = true;

        //返回对应模块
        return module.exports;
    }

    __webpack_require__.m = modules;
    __webpack_require__.c = installedModules;
    __webpack_require__.d = function (exports, name, getter) {
    
    
        if (!__webpack_require__.o(exports, name, getter)) {
    
    
            if (!__webpack_require__.o(exports, name)) {
    
    
                Object.defineProperty(exports, name, {
    
    
                    configurable: false,
                    enumerable: true,
                    get: getter
                });
            }
        }
    };

}

个人理解

webpack是是npm的工具模块,是一个JS应用打包器, 它将应用中的各个模块打包成一个或者多个bundle文件。

借助loaders和plugins,它可以改变、压缩和优化各种各样的文件。

输入不同资源,比如:html、css、js、img、font文件等,然后将它们输出浏览器可以正常解析的文件。

打包思路
1、利用babel完成代码转换及解析,并生成单个文件的依赖模块Map

2、从入口开始递归分析,并生成整个项目的依赖图谱

3、将各个引用模块打包为一个立即执行函数

4、将最终的bundle文件写入bundle.js中

单个文件的依赖模块Map

我们使用这几个包:

@babel/parser:负责将代码解析为抽象语法树

@babel/traverse:遍历抽象语法树的工具,我们可以在语法树中解析特定的节点,然后做一些操作,如ImportDeclaration获取通过import引入的模块,FunctionDeclaration获取函数

@babel/core:代码转换,如ES6的代码转为ES5的模式

由这几个模块的作用,其实已经可以推断出应该怎样获取单个文件的依赖模块了,转为Ast->遍历Ast->调用ImportDeclaration。代码如下:

//exportDependencies.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const exportDependencies = (filename) => {
    
    
    const content = fs.readFileSync(filename, 'utf-8')
    //转为Ast
    const ast = parser.parse(content, {
    
    
        sourceType: 'module'//这个参数用来识别ES Module
    })

    const dependencies = {
    
    }
    //遍历AST抽象语法树
    traverse(ast, {
    
    
        //调用ImportDeclaration获取通过import引入的模块
        ImportDeclaration({
    
    node}) {
    
    
            const dirname = path.dirname(filename)
            const newFile = './' + path.join(dirname, node.source.value)
            //保存依赖模块
            dependencies[node.source.value] = newFile
        }
    })
    //使用@babel/core和@babel/preset-env包转化代码
    const {
    
    code} = babel.transformFromAst(ast, null, {
    
    
        presets: ["@babel/preset-env"]
    })
    return{
    
    
        fulename,//这个文件名
        dependencies,//该文件所依赖的模块组成的Map
        code //转换完成代码
    }
}
module.exports = exportDependencies
//forexample
//info.js
const a = 1
export a
//index.js
import info from "./info.js"
console.log(info)

//testExport.js
const exportDependencies = require('./exportDependencies')
console.log(exportDependencies('./src/index.js'))

单个文件的依赖模块Map

有了获取单个文件依赖的基础,我们就可以在这基础上,进一步得出整个项目的模块依赖图谱了。首先,从入口开始计算,得到entryMap,然后遍历entryMap.dependencies,取出其value(即依赖的模块的路径),然后再获取这个依赖模块的依赖图谱,以此类推递归下去即可,代码如下:

const exportDependencies = require('./exportDependencies')

//entry为入口文件
const exportGraph = (entry) => {
    
    
    const entryModule = exportDependencies(entry)
    const graphArray = [entryModule]
    for(let i = 0; i < graphArray.length; i++) {
    
    
        const item = graphArray[i];
        //拿到文件所依赖的模块,dependencies的值参考exportDependencies
        const {
    
     dependencies } = item;
        for(let j in dependencies) {
    
    
            graphArray.push(
                exportDependencies(dependencies[j])
            )//关键代码,目的是将入口模块机器所有相关的模块放入数组中
        }
    }
    //接下来生成图谱
    const graph = {
    
    }
    graphArray.forEach(item => {
    
    
        graph[item.filename] = {
    
    
            dependencies: item.dependencies,
            code: item.code
        }
    })
    //可以看出graph其实是 文件路径名: 文件内容的集合
    return graph
}

module.export = exportGraph

输出立即执行函数

首先,我们的代码被加载到页面中的时候,是需要立即执行的。所以输出的bundle.js实质上要是一个立即执行函数。我们主要注意以下几点:

我们写模块的时候,用的是 import/export.经转换后,变成了require/exports

我们要让require/exports能正常运行,那么我们得定义这两个东西,并加到bundle.js里

在依赖图谱里,代码都成了字符串。要执行,可以使用eval

因此,我们要做这些工作:

1、定义一个require函数,require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上

2、获取整个项目的依赖图谱,从入口开始,调用require方法。完整代码如下:

const exportGraph = require('./exportGraph')
//写入文件,可以用fs.writeFileSync等方法,写入到output.path中
const exportBundle = require('./exportBundle')

const exportCode = (entry) => {
    
    
    //要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object Object]
    const graph = JSON.stringify(exportGraph(entry))
    exportBundle(`
        (function(graph) {
    
    
            //require函数的本质是执行一个模块的代码,然后将相应的变量挂载到exports对象上
            function require(module) {
    
    
                //localRequire的本质是拿到依赖包的exports变量
                function localRequire(relativePath) {
    
    
                    return require(graph(graph[module].dependencies[relativePath]);)
                }
                var exports = {
    
    };
                (function(require, exports, code) {
    
    
                    eval(code);
                })(localRequire, exports, graph[module].code);
                return exports;
                //函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
            }
            require('${entry')
        })(${
    
    graph})
    )
    module.exports = exportCode
}

至此,简单打包完成。我贴一下我跑的demo的结果。bundle.js的文件内容为:

(function(graph) {
    
    
    //require函数的本质是执行一个模块的代码,然后将相应额变量挂载到exports对象上
    function require(module) {
    
    
        //localRequire的本质是拿到依赖包的exports变量
        function localRequire(relativePath) {
    
    
            returnrequire(graph[module].dependencies[relativePath]);
        }
        var exports = {
    
    };
        (function(require, exports, code) {
    
    
            eval(code);
        })(localRequire, exports, graph[module].code);
        return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
        require('./src/index.js')
    }
})({
    
    "./src/index.js":{
    
    "dependencies":{
    
    "./info.js":"./src/info.js"},"code":"\"use}})

猜你喜欢

转载自blog.csdn.net/weixin_43706224/article/details/128919212