一、什么是webpack
当我们在使用webpack打包一个文件的时候,只要配置好文件的entry、output、plugins等就可以打包出一个dist文件夹出来。我们打包压缩的文件就在dist文件夹下。那么webpack是如何将那么多的文件内容压缩打包到dist文件夹下呢?我们通过一个例子来说明。
webpack 核心原理
在此之前我们要先了解webpack的核心原理是什么?
- 打包的主要流程
- 需要读取到入口文件
- 递归去获取模块依赖的文件内容,生成AST语法树
- 根据AST语法树,生成浏览器能够运行的代码
- 获取模板内容
- 模块分析
- 收集依赖
- ES6转ES5
二、 实践一个简单的例子
1. 创建一个小demo
- 创建一个文件,并初始化
- mkdir my-webpack && cd my-webpack
- npm init -y
- 在根目录下创建src文件夹,在src文件夹下创建index.js、add.js、minus.js。 代码也很简单:创建加和减两个函数,在index.js中引入。
//index.js
import add from './add.js'
import {minus} from './minus.js' //ES模块---
const sum = add(1,2)
const division =minus(2,1)
console.log('sum:',sum);
console.log('division:',division);
// add.js ES6规范
export default (a,b) =>{
return a + b
}
// minus.js
export const minus = (a,b) =>{
return a - b
}export const minus = (a,b) =>{
return a - b
}
- 在根目录下创建一个index.html,并引入src下面的index.js文件。我们打开这个index.html的控制台。会看到一个经典报错:
因为浏览器还无法识别import等ES6的语法。
- 接下来在根目录下创建一个bundle.js,现在的目录结构为:
2. 在bundle.js里面开始打包的流程
第一步:获取模板内容
const fs = require('fs') //用来读取文件
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, 'utf-8')
console.log(body);
})
getModuleInfo('./src/index.js')
我们可以运行输出一下,可以看到,我们index.js 的代码就被获取到了。
第二步:模块分析 这里我们要借助 babel 来解析模块。
npm i @babel/parser -D
@babel/parser用来做模块分析,并生成抽象语法树。
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模块
})
console.log(ast);
})
getModuleInfo('./src/index.js')
我们运行打印一下,可以看到AST语法树就生成了,其中progarm.body我们所收集到的模块的node结点。
第三步:收集依赖 这里我们借用 babel 的 @babel/traverse 来进行收集依赖。
npm i @babel/traverse -D @babel/traverse 我们可以将它与 babel 解析器一起使用来遍历和更新节点,用来遍历结点收集依赖 代码更新为:
const fs = require('fs') //用来读取文件
const parser = require('@babel/parser')
const tarverse = require('@babel/traverse').default
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, 'utf-8')
//解析模块依赖
const ast = parser.parse(body, {
sourceType: 'module', //要解析的是代码中的ES模块
})
//遍历收集依赖
const deps = {} //用来保存依赖
tarverse(ast, {
//指明收集啥依赖
ImportDeclaration({ node }) {
const dirname = path.dirname(file) //读取当前文件所处文件夹的文件名
const abspath = './' + path.join(dirname, node.source.value)
let abs = abspath.replace(/\\/g, '/') // window 会有\\ ,需要转成 /
deps[node.source.value] = abs // abspath = ./src/sum.js
}
})
console.log(deps)
})
getModuleInfo('./src/index.js')
我们打印输出deps:可以看到输出一个对象
第四步:将ES6代码转成ES5 借助 babel 的 @babel/core @babel/preset-env,将ES6 降级为ES5
npm i @babel/core @babel/preset-env -D
//ES6 转ES5
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env'] // 这个插件会生成我们想要的代码
})
console.log(code); //降级后的代码
我们打印输出code:
最后在getModuleInfo 函数中,将路径、收集依赖、降级的代码整和成对象返回出去:
getModuleInfo完整代码:
const fs = require('fs') //用来读取文件
const parser = require('@babel/parser')
const tarverse = require('@babel/traverse').default
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, 'utf-8')
//解析模块依赖
const ast = parser.parse(body, {
sourceType: 'module', //要解析的是代码中的ES模块
})
//遍历收集依赖
const deps = {} //用来保存依赖
tarverse(ast, {
//指明收集啥依赖
ImportDeclaration({ node }) {
const dirname = path.dirname(file) //读取当前文件所处文件夹的文件名
const abspath = './' + path.join(dirname, node.source.value)
let abs = abspath.replace(/\\/g, '/') // window 会有\\ ,需要转成 /
deps[node.source.value] = abs // abspath = ./src/sum.js
}
})
//ES6 转ES5
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
const moduleInfo = { file, deps, code } // 路径 、依赖 、降级代码
return moduleInfo
})
getModuleInfo('./src/index.js')
第五步: 递归获取文件依赖
创建一个parseModules 用来递归收集依赖,
//递归获取文件依赖
const parseModules = (file) => {
const entry = getModuleInfo(file) //调用getModuleInfo得到一个包含路径 、依赖 、降级代码的对象
const temp = [entry] // [{url,deps,es5code}]
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])) // getModuleInfo(./src/add.js) ...
}
}
}
}
console.log(temp);
}
parseModules('./src/index.js')
我们打印一下temp,可以看到 idnex add minus 的依赖都被存入。
我们修改一下目录结构,让它看起来更简洁,方便之后的操作:
//修改数据结构,使 file为key, code 为value
const depsCraph = {}
temp.forEach(moduleInfo => {
depsCraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
就变成:
parseModules 的完整代码:
//递归获取文件依赖
const parseModules = (file) => {
const entry = getModuleInfo(file) //调用getModuleInfo得到一个包含路径 、依赖 、降级代码的对象
const temp = [entry] // [{url,deps,es5code}]
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])) // getModuleInfo(./src/add.js) ...
}
}
}
}
const depsCraph = {}
temp.forEach(moduleInfo => {
depsCraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
return depsCraph
}
parseModules('./src/index.js')
这样我们就拿到了文件里面的所有依赖,并将其转换成了ES5的语法
第六步:解决关键字冲突
从code的打印我们可以看到,虽然被降级成了ES5的语法,但是依然存在require和exports两个关键字,浏览默也识别不了,所以我们就需要自己定义一个require函数和exports对象。
- 创建一个bundle函数用来创建关键字
//解决关键字require和exports
const bundle = (file) => {
const depsCraph = JSON.stringify(parseModules(file)) // 拿到所有的依赖
//自执行函数,返回出去的函数不能让它执行,所以用云括号包装成一个字符串返回
return `(function (graph) {
function require(file) { // 自定义require函数
//返回处一个自执行函数
var exports = {}; // 定义exports关键字对象
(function (require,exports,code) { //这里的形参不能是别的名,要和code的打印的名字一致
eval(code)
})(absRequire,exports,graph[file].code)
//拿到文件的绝对路径,将函数传给自执行函数
function absRequire(relPath){
return require(graph[file].deps[relPath])//将code里面require接收的路径转化为绝对路径
}
}
require('${file}')
})(${depsCraph})`
}
const content = bundle('./src/index.js')
我们打印一下content,拿到了所有打包好的数据。
最后,我们只需将我们打包好的数据写入文件即可:
fs.mkdirSync('./dist') // 生成文件夹
fs.writeFileSync('./dist/bundle.js',content) //写入文件
我们就可以看到目录结构下生成的dist文件夹了,里面的bundle.js就是我们打包好的数据了。
bundle.js
返回了一个自执行函数,只要我们引入这份js文件,就会自动执行。
测试
我们重新在index.html引入这份我们打包好的bundle.js,打开控制台我们可以看到:
输出成功了,我们把代码解析成浏览器能够识别的样子了。
这就是webpack的打包的核心原理,希望对你有所帮助。