资源处理流程
借用 webpack 官网对 webpack 的描述: webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle
什么是bundle
代码分离是 webpack 的特性之一,使用 entry 配置入口起点,会将代码分成源代码和分发代码
其中源代码是开发编辑的代码,分发代码是经过 webpack 构建,可能经过转义、压缩或优化后的代码,这些代码存放于 bundle 中,可以被浏览器等环境直接运行
什么是dependency graph
看了上面的内容,其实我们还是不清楚 webpack 到底做了哪些事情使浏览器不支持的语法变得可以执行,而去查看源码,会发现源码中代码对我们不是特别友好,经常左跳右跳无法持续跟踪,所以针对 webpack 打包流程,总结了下面的一个简版打包代码,让我们能大体看清 webpack 做了哪些操作,以下只是简版 demo,没有强行靠近 webpack 打包结果,让我们更能清晰的梳理流程。
打包步骤总结
在整理打包内容之前,我们先来看下我们现在项目的结构,项目名称为 webpack_demo,其中包含 webpack.config.js,package.json,dist/main.js,src/index.js,src/hello.js,src/message.js,src/bundler.js,src/complier.js。
webpack.config.js 定义对象,导出项目入口出口文件
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js'
}
}
src/index.js 代码中导入 hello.js方法并执行
import hello from './hello.js'
console.log(hello())
src/hello.js 代码中导入 message.js中的message参数
import { message } from './message.js'
export default function() {
return 'hello ' + message;
}
src/message.js 中定义了一个message变量
export const message = 'world!!!'
上面的代码层层引用下来,可以在控制台输出 ‘hello world!!!’,就代表打包成功了。
上面的环境已经定义完成,接下来让我们按照步骤完成打包操作:
1.在src/complier.js文件中创建complier构造函数,在构造函数中获取webpack.config.js中定义的入口出口参数
module.exports = class Complier {
constructor(options) {
const { entry, output } = options
this.entry = entry
console.log(options)
this.output = output
}
}
2.在 src/bundler.js 文件中引入 webpack.config.js 文件,创建 Complier 的实例化对象并传入 options
const complier = require('./complier')
const options = require('../webpack.config')
new complier(options)
在命令行执行 node src/bundler.js 后,在控制台会打印出 options 内容
3.拿到配置参数后,开始根据入口文件解析文件内容,解析单流程整体为:
根据上面总结内容我们在 src/complier.js 中创建一个 createAsset 方法
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
...
// 开始编译,构建ast语法树 filename: ./src/index.js
createAsset(filename) {
// 1
// content 内容即为index.js中书写的内容
const content = fs.readFileSync(filename, 'utf-8')
// 2
// ast 内容为对象,具体内容可以console.log(ast)查看
// https://astexplorer.net/ 在官网输入index.js内容即可看到对应的树
const ast = parser.parse(content, {
sourceType: 'module'
})
// 创建依赖对象
const dependencies = {}
// 3
// 获取抽象语法树中的依赖文件名
traverse(ast, {
ImportDeclaration: ({node}) => {
// 获取文件的路径名如 './src/index.js' dirname='./src'
const dirname = path.dirname(filename)
const absPath = path.join(dirname, node.source.value)
// node.source.value: .hello.js
// absPath: ./src/index.js
dependencies[node.source.value] = absPath
}
})
// 4
// 将ast转换成可执行代码
// https://www.babeljs.cn/docs/babel-core 将index.js内容直接放在官网即可看到转译后代码
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
// 5
return {
filename,
dependencies,
code
}
}
入口文件的依赖关系已经定义好,接下来根据入口文件的 dependencies ,递归遍历出所有子依赖,在 src/complier.js 文件中定义 run 方法
// 拿到参数、执行、分析入口文件
run() {
// 拿到入口文件的依赖
const mainAsset = this.createAsset(this.entry)
const queue = [mainAsset]
// 6
// 遍历对象
for (const asset of queue) {
// 遍历文件的依赖文件,递归创建依赖图
Object.values(asset.dependencies).forEach(filename => {
const child = this.createAsset(filename)
queue.push(child)
})
}
// 7
return queue
}
命令行执行 node src/bundler.js 看下 queue 的内容如下
4.依赖树已经拿到,接下来在 src/bundler.js 中获取 complier 中返回的 queue
// 获取dependence graph
const graph = new complier(options).run()
5.在 src/bundler.js 中创建函数 bundle,解析 graph 树,定义 require 函数,定义 modules,通过 eval 函数执行依赖树中的 code,在此我们可以知道 webpack 重写了 require 函数,所以 babel 中转换的函数可以正常执行
function bundle(graph){
// 得到依赖文件名的对象
let modules = {};
graph.forEach(item => {
// 将文件名作为key, value为依赖文件,code为文件名对应的函数
modules[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
modules = JSON.stringify(modules)
const result = `(function(graph){
function require(filepath) {
function localRequire(relativePath) {
// 将代码中的require中的路径转换成dependencies存储的带文件夹名的路径
return require(graph[filepath].dependencies[relativePath])
}
var exports = {}
function fn(require, exports, code) {
eval(code)
}
fn(localRequire, exports, graph[filepath].code)
return exports
}
require('${entry}')
})(${modules})`
return result
}
const graph = new complier(options).run()
// 执行bundle函数
const result = bundle(graph)
命令行输出 result 内容,粘贴内容到浏览器控制台并回车执行,发现我们预期的 'hello world!!!' 已经可以正常打印
6.以上我们已经拿到了编译后的代码,最后将它输出到 dist/main.js 中,在 src/bundler.js 中创建方法 createFile(),使用 fs 对象的 writeFileSync 将内容输出,在命令行执行命令后可以看到 src/main.js 中输出了对应内容
function createFile(code) {
fs.writeFileSync(path.join(output.path, output.filename), code)
}
7.下面是 src/complier.js和 src/bundler.js文件全部内容
complier.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')
module.exports = class Complier {
constructor(options) {
const { entry, output } = options
this.entry = entry
this.output = output
}
// 拿到参数、执行、分析入口文件
run() {
const mainAsset = this.createAsset(this.entry)
const queue = [mainAsset]
for (const asset of queue) {
Object.values(asset.dependencies).forEach(filename => {
const child = this.createAsset(filename)
queue.push(child)
})
}
console.log(queue)
return queue
}
// 开始编译,构建ast语法树 filename: ./src/index.js
createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
// 创建依赖
const dependencies = {}
traverse(ast, {
ImportDeclaration: ({node}) => {
// 获取文件的路径名如 './src/index.js' dirname='./src'
const dirname = path.dirname(filename)
const absPath = path.join(dirname, node.source.value)
dependencies[node.source.value] = absPath
}
})
// 将ast转换成代码
// https://www.babeljs.cn/docs/babel-core
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return {
filename,
dependencies,
code
}
}
}
bundler.js
// 引入配置
const fs = require('fs');
const path = require('path')
const options = require('../webpack.config')
const complier = require('./complier')
const { entry, output } = options
function bundle(graph){
// 得到以依赖文件名的对象
let modules = {};
graph.forEach(item => {
modules[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
modules = JSON.stringify(modules)
const result = `(function(graph){
function require(filepath) {
function localRequire(relativePath) {
// 将代码中的require中的路径转换成dependencies存储的带文件夹名的路径
return require(graph[filepath].dependencies[relativePath])
}
var exports = {}
function fn(require, exports, code) {
eval(code)
}
fn(localRequire, exports, graph[filepath].code)
return exports
}
require('${entry}')
})(${modules})`
return result
}
function createFile(code) {
fs.writeFileSync(path.join(output.path, output.filename), code)
}
const graph = new complier(options).run()
const result = bundle(graph)
createFile(result)
总结:
通过上面的 demo,我们已经可以大概了解 webpack 的编译流程,当然 webpack 的源码功能强大且复杂,感兴趣的小伙伴儿可以自行研究。
阅读量:2016
点赞量:0
收藏量:0