学习webpack原理:写一个简单的webpack打包工具

55 篇文章 4 订阅
23 篇文章 0 订阅

  通过写一个简单的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的语法;其实基本过程就是上面提到的三板斧

3.1 @babel/parser

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

3.2 @babel/traverse

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

 3.3  @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包

在根目录下打开命令行,安装如下包,版本没要求,安装最新的即可

  "devDependencies": {
    "@babel/core": "^7.15.0",
    "@babel/parser": "^7.15.3",
    "@babel/preset-env": "^7.15.0",
    "@babel/traverse": "^7.15.0"
  }

4.4 在根目录下建一个配置文件minipack.config.js

执行脚本时,读取该配置文件;跟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树的组织节点长啥样:

 打印查看可能不太直观,我们借助一个网站查看:AST explorer

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

7行源码,分别在body节点中被管理;主要有导入声明、变量声明、函数声明、表达式声明等类型;

打开最后一个输出语句表达式看一下;结构如下

 看到标志符的类型是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('文件已被保存');
})

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨大大28

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值