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

浏览器未支持模块化时

背景

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

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

八股文不用背-webpack打包的原理_第1张图片
// 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后,控制台会报一下错误:

这是浏览器不识别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\", {\nvalue: true\n});\nexports[\"default\"] = void 0;\n\nvar _default = function _default(a, b) {\nreturn a + b;\n};\n\nexports[\"default\"] = _default;"},"./src\\minus.js": {"deps": {},"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\nvalue: true\n});\nexports[\"default\"] = void 0;\n\nvar _default = function _default(a, b) {\nreturn 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,就能在控制台看到输出

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

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

你可能感兴趣的:(webpack,javascript,前端)