likes
comments
collection
share

八股文不用背-webpack打包的原理

作者站长头像
站长
· 阅读数 49

浏览器未支持模块化时

背景

浏览器解析html时,是不识别import关键字的,也就是说浏览器是不支持模块引入的,而我们开发项目需要模块化开发,怎么办呢?

比如我们现在创建了index.js、add.js、minus.js三个文件 文件目录是:

八股文不用背-webpack打包的原理

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

当我在html中引入index.js,然你在浏览器打开html后,控制台会报一下错误:

八股文不用背-webpack打包的原理

这是浏览器不识别import关键词

如果我能把这三个文件合并到一个文件bundle.js,那么就完全不需要import啦~~~ 。开干!

  1. 如果bundle.js中有一个depsGraph(就是一个map),存着这三个文件的代码,我们就能通过eval()执行对应文件的代码。
  2. 但是import关键词我们没什么办法,这是es6的的语法,我们先将它转换成es5的语法,import add from './add.js' => require(\"./add.js\"),当我们eval(index.js的代码),浏览器无法识别require函数,那也简单,我们自己写一个require函数,入参是文件名,出参是代码执行完的exports对象
  3. 同样,exports关键词,我们也没什么办法,我们将其转化成es5语法,exports default => exports[\"default\"],那感情好啊,我直接定义一个对象exports

第一步:构造depsGraph

这是我们想要的depsGraph

{
    // 文件的绝对路径
    "./src/index.js": {
        // 该文件代码里import的所有依赖文件的绝对路径
        "deps": {
            "./add.js": "./src\\add.js",
            "./minus.js": "./src\\minus.js"
        },
        // 该文件的代码
        "code": "\"use strict\";\n\nvar _add = _interopRequireDefault(require(\"./add.js\"));\n\nvar _minus = _interopRequireDefault(require(\"./minus.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar sum = (0, _add[\"default\"])(1, 2);\nvar division = (0, _minus[\"default\"])(2, 1);\nconsole.log(sum);\nconsole.log(division);"
    },
    "./src\\add.js": {
        "deps": {},
        "code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _default = function _default(a, b) {\n  return a + b;\n};\n\nexports[\"default\"] = _default;"
    },
    "./src\\minus.js": {
        "deps": {},
        "code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _default = function _default(a, b) {\n  return a - b;\n};\n\nexports[\"default\"] = _default;"
    }
}

为了得到上图的depsGraph,我们需要定义一个函数getModuleInfo,入参是入口文件(index.js)的绝对路径,功能是将入口文件及其所有依赖文件代码做对应关系

// 文件读取模块
const fs = require('fs')
// 文件路径模块
const path = require('path')
// babel的parser插件
const parser = require('@babel/parser')
// babel的traverse插件能遍历parser解析出来的抽象语法树
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

let depsGraph = {}
// ./src/index.js是项目的入口文
getModuleInfo('./src/index.js')
function getModuleInfo(file){
    // 先读取文件内容
    const body = fs.readFileSync(file, 'utf-8')
    // 通过parser.parse将文件内容解析成一个抽象语法树
    const ast = parser.parse(body, {
        sourceType: 'module' //表示我们要解析的是ES模块
    });
    const deps = {}
    // 通过traverse插件遍历抽象语法树
    traverse(ast, {
        // 对树节点是importDeclaration的模块进行处理
        // 因为这种节点就是import语句,比如import add from './add.js'就会被解析成这种节点
        ImportDeclaration({
            node
        }) {
            // 这个node就是当前文件的依赖文件,比如index.js的代码中有import add from './add.js',那么这个node就是依赖的文件add.js
            // 获取依赖文件的绝对路径
            const dirname = path.dirname(file)
            const abspath = "./" + path.join(dirname, node.source.value)
            // 将依赖的 相对路径 和其 绝对路径 形成对应关系
            // 比如 './add.js' : './src/add.js'
            deps[node.source.value] = abspath
        }
    })
    // 这个大家都懂,将代码转化成es5语法
    const {
        code
    } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    // 形成 文件路径-{依赖文件的绝对路径,文件的代码} 对应关系
    depsGraph[file] = {deps,
        code}
    // 遍历依赖文件,然后重复递归调用
    for (const key in deps) {
        if (Object.hasOwnProperty.call(deps, key)) {
            const el = deps[key];
            getModuleInfo(el)
        }
    }
}

第二步:构造require函数

定义一个require函数,入参是文件的路径,然后从depsGraph中获取文件路径对应的代码,然后执行。

  1. 入口文件./src/index.js,执行require('./src/index.js')

depsGraph中index.js是这样的:

"./src/index.js": {
    // 该文件代码里import的所有依赖文件的绝对路径
    "deps": {
        "./add.js": "./src\\add.js",
        "./minus.js": "./src\\minus.js"
    },
    // 该文件的代码
    "code": "\"use strict\";\n\nvar _add = _interopRequireDefault(require(\"./add.js\"));\n\nvar _minus = _interopRequireDefault(require(\"./minus.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar sum = (0, _add[\"default\"])(1, 2);\nvar division = (0, _minus[\"default\"])(2, 1);\nconsole.log(sum);\nconsole.log(division);"
    },

定义require函数

function require(path){
    let {code} = depsGraph[path]
    eval(code)
}
require('./src/index.js')

如果是上图这样的话,index.js代码中的require函数是require(\"./add.js\")require(\"./minus.js\"),./add.js和./minus.js都是相对路径,而depsGraph中存的是key是文件的绝对路径,明显取不到。 然而我们在depsGraph中保存着index.js的所有依赖的绝对路径

"deps": {
        "./add.js": "./src\\add.js",
        "./minus.js": "./src\\minus.js"
    },

显然,对于index.js文件里的require函数,他们的入参是相对路径,而我们写的require必须传入的是绝对路径,那我们得重写一个require函数->absRequire函数,入参是相对路径,通过deps找到绝对路径,然后作为require函数的入参去执行

function require(path){
    let {code, deps} = depsGraph[path]
    function absRequire(relPath){
        return require(deps[replPath])
    }
    (function(require,code){
        eval(code)
    })(absRequire,code)
    return 
}
require('./src/index.js')

构造export

node环境下,每个js文件默认都有一个export导出对象。 上图的代码中,我们没有显性的返回东西,而我们对require的功能定义:入参是文件绝对路径,出参是代码的执行后的exports对象,那我们只要定义一个exports对象,然后eval(code),然后代码执行的结果就会被赋值在exports对象上,然后我们就可以返回它。

function require(path){
    let {code, deps} = depsGraph[path]
    function absRequire(relPath){
        return require(deps[replPath])
    }
    let exports = {}
    ;(function(require,code,exports){
        eval(code)
    })(absRequire,code,exports)
    return exports
}
require('./src/index.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')
let depsGraph = {}
const getModuleInfo = (file) => {
    const body = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(body, {
        sourceType: 'module' //表示我们要解析的是ES模块
    });
    const deps = {}
    traverse(ast, {
        ImportDeclaration({
            node
        }) {
            const dirname = path.dirname(file)
            const abspath = "./" + path.join(dirname, node.source.value)
            deps[node.source.value] = abspath
        }
    })
    const {
        code
    } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    depsGraph[file] = {deps,
        code}
    for (const key in deps) {
        if (Object.hasOwnProperty.call(deps, key)) {
            const el = deps[key];
            getModuleInfo(el)
        }
    }
}
// 新增代码
const bundle = (file) => {
    getModuleInfo(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}')
    })(${JSON.stringify(depsGraph)})`
}
fs.writeFileSync('./dist/bundle.js', bundle('./src/index.js'))

然后运行node bundle.js,就会在dist文件夹下生成合并和后的bundle.js,然后在index.html中 引入./dist/bundle.js,用浏览器打开html,就能在控制台看到输出

看到这里的看官,麻烦点个赞赞吧!

转载自:https://juejin.cn/post/7137122886971555847
评论
请登录