【webpack打包原理】写一个简单的webpack打包工具

已被阅读 909 次 | 文章分类:javascript | 2022-03-04 20:52

通过写一个简单的webpack工具,理解打包过程中的技术以及基础理论

1 AST概念

抽象语法树( Abstract Syntax Tree,AST ),或简称语法树( Syntax tree ),是源代码语法结构的一种抽象表示。 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

2 AST应用

在前端,可以通过 Javascript 解析器将我们程序的源代码映射成为一棵语法树,而树的每个节点对应着代码里的一种结构;比如表达式,声明语句,赋值语句等都会被映射为语法树上的一个节点,进而我们就可以通过操作语法树上的节点来控制我们的源代码;总结下来就是AST三板斧:

(1) 将源代码映射成AST

(2) 操作AST进行遍历更新

(3) 将更新后的AST再转换为源

说到以上三点,作为一个前端开发者,必然想到babel,下面就以babel来解释一下AST在编译中的应用

3 babel与AST的关系

Babel解析器是一个javascript解析器;最常见的功能就是将我们的高版本js语法解析为浏览器识别的es5语法;为了使用高版本js带来的便利,工作中会经常用es6或者es7来开发,但是某些浏览器并不能完全兼容该语法,所以需要babel将他们编译为浏览器可识别的es5的语法

@babel/parser

负责将源码解析为AST;该api负责将javascript源码根据表达式,函数声明,变量定义,导入声明等类型,解析为树节点,用树的方式将代码组织起来;方便后续遍历更新

@babel/traverse

                                            
<strong>负责遍历操作AST节点;</strong>通过该api可以方便的获取所有节点,比如获取所有变量类型节点,或者获取所有的console.log()表达式,获取所有debugger等等;在打包时设置的去除所有打印语句或者debugger关键词等;就是在这一步实现,我们根据传入的规则,将一些不需要的节点遍历删除或者更改;
                                            
                                        

@babel/core

在遍历更新完ast后,将更新后的AST重新编译为浏览器可兼容的低版本源码;

4 代码演示

4.1 新建一个目录

在根目录下新建src文件夹,然后在src文件夹新建如下三个测试文件

                                            
// hello.js
export const hello="hello"
                                            
                                        
                                            
// name.js
export const name="name"
                                            
                                        
                                            
// message.js 

import {hello} from './hello.js'
import {name} from './name.js'
 
export default function message() {
  console.log(`${hello} ${name}!`)
                                            
                                        

4.2 新建入口文件

在src目录下新建entry.js

                                            
// 这是一个入口文件
 
// 导入
import message from './message.js'
import {name} from './name.js'
 
// 变量
const value="xiaobai";
// 函数
function getName(){
    return value
}
// 表达式 
message()
let res=value==="xiaobai"?true:false
console.log('----name-----: ', name)
                                            
                                        

到这里准备工作完成了;下面剧通过AST三板斧对入口文件关联的所有代码进行打包

4.3 安装babel包

执行脚本时,读取该配置文件;跟vue cli中vue.config.js是一个道理

                                            
const path = require('path')
module.exports = {
    entry: 'src/entry.js',
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, './dist'),
    }
}
                                            
                                        

4.5 代码编译脚本文件(打包核心部分)

根目录下新建一个index.js;待会我们直接执行node index.js 即可打包我们的代码;跟vue工程中 npm run build是一个道理;

(1)第一步:获取所有源码内容

根据三板斧流程,获取更新后的源码

                                            
const fs = require('fs');
const path= require('path')
// 获取配置文件
const config = require('./minipack.config');
// 获取入口文件路径
const entry = config.entry;
// 获取入口文件内容
const mainAssert = createAsset(entry)
                                            
                                        
                                            
// 获取入口文件内容
function createAsset(){
    // 1 获取AST
    const content = fs.readFileSync(entry, 'utf-8');
    const babelParser = require('@babel/parser')
    const ast = babelParser.parse(content, {
      sourceType: "module"
    })
    const dependencies = []
    
    // 2 遍历AST;获取入口文件的所有依赖
    const traverse = require('@babel/traverse').default
    traverse(ast, {
      // 遍历所有的 import 模块,并将相对路径放入 dependencies
      ImportDeclaration: ({node}) => {
        dependencies.push(node.source.value)
      }
    })
    // 3 AST编译为源码
    const {transformFromAst} = require('@babel/core');
    const {code} = transformFromAst(ast, null, {
        presets: ['@babel/preset-env'],   // 代码解析规则
      })
      // 返回结果
      return {
        dependencies,
        code,
    }
}
                                            
                                        

我们打印ast变量,看一下ast树的组织节点长啥样:

/net/upload/image/20220304/9ef6c6c25f8d4d978a852ed5a72d15db.png

打印查看可能不太直观,我们借助一个网站查看:https://astexplorer.net/

将entry.js文件内容复制到左侧,右侧自动展示出AST树,可以直观的看到源码是如何被以节点的方式组织;如下

/net/upload/image/20220304/5c55a41b1a3943a09ef830fc47e0effb.png

7行源码,分别在body节点中被管理;主要有导入声明、变量声明、函数声明、表达式声明等类型;打开最后一个输出语句表达式看一下;结构如下

/net/upload/image/20220304/7117824ce7bc4eb19632b9aa77191470.png

看到标志符的类型是console;其实我们再vue工程中打包的时候,会加如一些规则插件,比如去掉console,debugger等;在编译过程中,就是在利用Travser遍历AST后,然后根据节点名称去做删除操作的;将删除后的AST再转换成新的源码。

(2)递归解析所有依赖,形成依赖关系图

我们根据解析后的入口文件AST,将所有以依赖的文件用数组管理起来;代码如下

                                            
// entry: 入口文件绝对地址
const queue = {
  [entry]: mainAssert
}
// 递归解析所有的依赖项,生成一个依赖关系图
// 遍历 queue,获取每一个 asset 及其所以依赖模块并将其加入到队列中,直至所有依赖模块遍历完成
for (let filename in queue) {
  let assert = queue[filename]
  recursionDep(filename, assert)
  console.log("queue",queue);
}
 
 
 
 
 
/**
 * 递归遍历,获取所有的依赖
 * @param {*} assert 入口文件
*/
function recursionDep(filename, assert) {
  // 跟踪所有依赖文件(模块唯一标识符)
  assert.mapping = {}
  // 由于所有依赖模块的 import 路径为相对路径,所以获取当前绝对路径
  const dirname = path.dirname(filename)
  assert.dependencies.forEach(relativePath => {
    // 获取绝对路径,以便于 createAsset 读取文件
    const absolutePath = path.join(dirname, relativePath)
    // 与当前 assert 关联
    assert.mapping[relativePath] = absolutePath
    // 依赖文件没有加入到依赖图中,才让其加入,避免模块重复打包
    if (!queue[absolutePath]) {
      // 获取依赖模块内容
      const child = createAsset(absolutePath)
      // 将依赖放入 queue,以便于继续调用 recursionDep 解析依赖资源的依赖,
      // 直到所有依赖解析完成,这就构成了一个从入口文件开始的依赖图
      queue[absolutePath] = child
      if(child.dependencies.length > 0) {
        // 继续递归
        recursionDep(absolutePath, child)
      }
    }
  })
  }
                                            
                                        

(3) 根据依赖关系数组,返回浏览器可执行的js文件

这一步就是将入口文件所依赖的所有js文件代码,写入到匿名IIFE函数中;

                                            
// 使用依赖图,返回一个可以在浏览器运行的 JavaScript 文件
let modules = ''
for (let filename in queue) {
  let mod = queue[filename]
  modules += `'${filename}': [
    function(require, module, exports) {
      ${mod.code}
    },
    ${JSON.stringify(mod.mapping)},
  ],`
}
 
 
 
const result = `
  (function(modules) {
    function require(moduleId) {
      const [fn, mapping] = modules[moduleId]
      function localRequire(name) {
        return require(mapping[name])
      }
      const module = {exports: {}}
      fn(localRequire, module, module.exports)
      return module.exports
    }
    require('${entry}')
  })({${modules}})
`
                                            
                                        

(4)将可执行js代码,写入到文件;利用nodejs fs的writeFile将文件写入到js文件;

                                            
// 写入 ./dist/bundle.js
fs.writeFile(`./dist/bundle.js`, result, (err) => {
  if (err) throw err;
  console.log('文件已被保存');
})
                                            
                                        

QQ:3410192267 | 技术支持 微信:popstarqqsmall

Copyright ©2017 xiaobaigis.com . 版权所有 鲁ICP备17027716号