IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> webpack系统学习(十六)手写一个简单的打包工具 -> 正文阅读

[JavaScript知识库]webpack系统学习(十六)手写一个简单的打包工具

这篇文章分析了如何自己手写一个简单的打包工具。
我们先查看当前项目结构:
在这里插入图片描述
word.js:

//word.js
export const word = 'hello'

message.js:

import {word} from "./word.js";

const message = `say ${word}`

export default message

index.js:

//index.js
import message from "./message.js";
console.log(message)

我们的预期是写一个bundler.js,它将对index.js进行打包,生成能够在浏览器上运行的代码。

1. 入口文件分析

首先,打包工具是在nodejs环境下运行的(和webpack类似)。
我们如果要对入口文件打包,首先要读入入口文件的代码,并对其进行模块分析。

1.1 读取入口文件

这里我们使用node.js 中fs模块的readFileSync同步读取。

//bundler.js
const fs = require('fs')

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename,'utf-8')
  console.log(content)
}

moduleAnalyser('./src/index.js')

使用node bundler.js执行,控制台输出:

import message from "./message.js";

console.log(message)

现在我们已经拿到了index.js的文件内容,但是我们可以看到,index.js的执行还需要message.js模块,所以,我们还需要将模块也提取出来。

1.2 模块提取

我们需要记录下运行index.js所需的依赖,那么,如何将message模块提取出来呢?字符串截取可行吗?稍微思考一下,我们就可以否决掉字符串截取这个方案,对大型的js文件使用字符串截取太过复杂并且容易出错。
我们可以引入@babel/parser@babel/parser可以帮助我们对源码进行分析。

npm i @babel/parser
//bundler.js
const fs = require('fs')
const parser = require('@babel/parser')

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename,'utf-8')
  const ast = parser.parse(content,{
    sourceType: 'module'	//表示content是es6规范的文件
  })
  console.log(ast)
}

moduleAnalyser('./src/index.js')

我们再次执行node bundler.js,控制台输出以下内容:

Node {                                     
  type: 'File',                            
  start: 0,                                
  end: 59,                                 
  loc: SourceLocation {                    
    start: Position { line: 1, column: 0 },
    end: Position { line: 3, column: 20 }
  },
  program: Node {
    type: 'Program',
    start: 0,
    end: 59,
    loc: SourceLocation { start: [Position], end: [Position] },
    sourceType: 'module',
    interpreter: null,
    body: [ [Node], [Node] ],
    directives: []
  },
  comments: []
}

其实这就是抽象语法树,ast中一个program对象,这就是**@babel/parser解析content(也就是index.js)后得到的代码结构,而program.body就是代码的节点结构。
修改
bundler.js**中console.log(ast.program.body),控制台输出:

[
  Node {
    type: 'ImportDeclaration',
    start: 0,
    end: 35,
    loc: SourceLocation { start: [Position], end: [Position] },
    specifiers: [ [Node] ],
    source: Node {
      type: 'StringLiteral',
      start: 20,
      end: 34,
      loc: [SourceLocation],
      extra: [Object],
      value: './message.js'
    }
  },
  Node {
    type: 'ExpressionStatement',
    start: 39,
    end: 59,
    loc: SourceLocation { start: [Position], end: [Position] },
    expression: Node {
      type: 'CallExpression',
      start: 39,
      end: 59,
      loc: [SourceLocation],
      callee: [Node],
      arguments: [Array]
    }
  }
]

控制台输出了一个数组,数组中有两个node对象,第一个node对象就对应了index.js的第一行import message from "./message.js";,该node对象中type表明了这一行是引入声明source.value的值就是引入的模块的值;第二个node对象就对应了index.js的第二行代码console.log(message),该对象的type表明了这一行是js表达式
现在,不管我们在index.js中引入多少个模块,ast.program.body中就会存在与之对应的ImportDeclaration类型的node节点。这时候,我们的模块提取就很容易了,只需要将每一个node遍历之后就能拿到了。
这里,我们使用**@babel/traverse来提取模板。
@babel/traverse 可以用来遍历更新
@babel/parser**生成的AST

  • 对语法树中特定的节点进行操作
  • 进入节点(enter)
  • 退出节点(exit)
    关于**@babel/traverse**的详细用法大家可以在网上查找相关资料,这里笔者将仅做简单叙述。
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename,'utf-8')
  const ast = parser.parse(content,{
    sourceType: 'module'
  })
  const dependencies = {};
  traverse(ast, {
    ImportDeclaration: function (Node){
      const dirname = path.dirname(filename);
      const newDir = '.\\' + path.join(dirname, './message.js');
      dependencies[Node.node.source.value] = newDir
    }
  })
  return {
    filename,
    dependencies
  }
}

moduleAnalyser('./src/index.js')
  1. 在引入const traverse = require('@babel/traverse').default时,由于 @babel/traverse 使用的是ES Module语法,我们如果想通过CommenJS语法引入,需要在后面加上.default
  2. 这里的traverse方法第一个参数就是我们使用 @babel/parser 生成的语法树,第二个参数是一个对象,ImportDeclaration: function (path){ console.log(path.node.source.value) } 这里的意思是,对typeImportDeclaration的node对象执行后面的操作,传入的path就是该节点对象。通过path.node.source.value就可以拿到引入声明的值,即index.js中的 “./message.js”

这里我们使用了 dependencies./message.js 和另一个路径对应起来,因为当我们使用bundler.jsindex.js打包时,需要使用到index.js依赖的模块,所以应该将相对于index.js的路径转换成相对于bundler.js的文件路径,这里将文件路径保存到了dependencies中,便于我们之后的使用。

**moduleAnalyser **最后返回了一个对象,包括了入口文件,以及入口文件的模块依赖。

虽然我们现在可以拿到index.js及其依赖,但是我们不难发现index.js 和依赖模块使用了es module语法,而浏览器是不支持es module 语法的,所以我们还需要使用**@babel/core**对代码进行转换。

npm i @babel/core -s

@babel/core有一个方法transformFromAst可以将抽象语法树转换成浏览器可以运行的js代码,而在使用**@babel/core对Ast进行转换时,我们通常还需要配置@babel/preset-env**,它是一系列插件的集合,包含了我们在babel6中常用的es2015,es2016, es2017等最新的语法转化插件,允许我们使用最新的js语法。

npm i @babel/preset-env -s

transformFromAst会根据参数中的抽象语法树返回一个对象,该返回对象包括一个code字段,修改**moduleAnalyser **:

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename,'utf-8')
  const ast = parser.parse(content,{
    sourceType: 'module'
  })
  const dependencies = {};
  traverse(ast, {
    ImportDeclaration: function (Node){
      const dirname = path.dirname(filename);
      const newDir = '.\\' + path.join(dirname, './message.js');
      dependencies[Node.node.source.value] = newDir
    }
  })
  const {code} = babel.transformFromAst(ast, null, {
    presets:["@babel/preset-env"]
  })
  console.log(code)
  return {
    filename,
    dependencies,
    code
  }
}

执行node bundler.js,控制台输出:

var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj 
}; }
console.log(_message.default);

现在的代码,就是浏览器可以执行的代码了。
但是,如果我们就直接执行这一段代码,肯定是会报错的,因为虽然现在已经把index.js分析完毕,但是 ./message.js还是外部模块,我们还需要对index.js的模块进行分析。

2. 依赖图谱

首先,我们输出moduleAnalyser (’./src/index.js)

{
  filename: './src/index.js',
  dependencies: { './message.js': '.\\src\\message.js' },
  code: '"use strict";\n' +
    '\n' +
    'var _message = _interopRequireDefault(require("./message.js"));\n' +
    '\n' +
    'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default:
 obj }; }\n' +
    '\n' +
    'console.log(_message.default);'
}

moduleAnalyser 中包含了入口文件,入口文件的依赖,转换后的人口文件js代码。
既然已经有了入口文件的依赖,那么我们就应该向分析入口文件一样,逐步分析 【运行
index.js
】所需要的所有依赖(包括依赖的依赖)。
我们新建一个makeDependencisGraph(),它的作用就是得到模块之间的依赖信息(即依赖图谱)。

const makeDependenciesGraph = (entry) => {
  const entryModule = moduleAnalyser(entry)
  const graphArray = [entryModule]
  for(let i = 0; i < graphArray.length; i++) {
    console.log(graphArray.length)
    const item = graphArray[i];
    const { dependencies } = item;
    console.log(dependencies)
    if(dependencies) {
      for(let j in dependencies) {
        graphArray.push(
          moduleAnalyser(dependencies[j])
        );
      }
    }
  }
  console.log(graphArray)
}

makeDependenciesGraph 中,我们使用moduleAnalyser(entry)对入口文件进行初步分析,将分析结果entryModule放入graphArray数组,使用一个循环,在这个循环中,遍历graphArray数组,如果item存在依赖,就再次调用moduleAnalyser分析该依赖,并将分析结果存入graphArray数组。最后,就可以把所有的模块依赖都分析到。
**makeDependenciesGraph(’./src/index.js)**的输出结果:

[
  {
    filename: './src/index.js',
    dependencies: { './message.js': '.\\src\\message.js' },
    code: '"use strict";\n' +
      '\n' +
      'var _message = _interopRequireDefault(require("./message.js"));\n' +
      '\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { defaul
t: obj }; }\n' +
      '\n' +
      'console.log(_message.default);'
  },
  {
    filename: '.\\src\\message.js',
    dependencies: { './word.js': '.\\src\\word.js' },
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.default = void 0;\n' +
      '\n' +
      'var _word = require("./word.js");\n' +
      '\n' +
      'var message = "say ".concat(_word.word);\n' +
      'var _default = message;\n' +
      'exports.default = _default;'
  },
  {
    filename: '.\\src\\word.js',
    dependencies: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.word = void 0;\n' +
      "var word = 'hello';\n" +
      'exports.word = word;'
  }
]

我们是以数组的形式来存储的,这不方便我们以后的使用,所以我们再改变一下我们的数据结构:

const makeDependenciesGraph = () => {
  ...
  const graph = {}
  for(let i in graphArray){
    const item = graphArray[i]
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code
    }
  }
  console.log(graph)
}

输出:

{                                                                          
  './src/index.js': {                                                      
    dependencies: { './message.js': '.\\src\\message.js' },                
    code: '"use strict";\n' +                                              
      '\n' +                                                               
      'var _message = _interopRequireDefault(require("./message.js"));\n' +
      '\n' +                                                               
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n' +
      '\n' +
      'console.log(_message.default);'
  },
  '.\\src\\message.js': {
    dependencies: { './word.js': '.\\src\\word.js' },
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.default = void 0;\n' +
      '\n' +
      'var _word = require("./word.js");\n' +
      '\n' +
      'var message = "say ".concat(_word.word);\n' +
      'var _default = message;\n' +
      'exports.default = _default;'
  },
  '.\\src\\word.js': {
    dependencies: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.word = void 0;\n' +
      "var word = 'hello';\n" +
      'exports.word = word;'
  }
}

现在,我们就可以根据文件路径拿到文件的依赖和可执行代码了。

3. 生成可运行代码

const generateCode = (entry) => {
  const graph = JSON.stringify(makeDependenciesGraph(entry))
  return`
    (function(graph){
      function run(module){
        const code = graph[module].code
        function myRequire(path){
          return run(graph[module].dependencies[path])
        }
        var myExports = {};
        (function(require, exports, code){
          eval(code)
        })(myRequire, myExports , code)
        return exports;
      };
      run('${entry}')
    })(${graph});
  `
}

generateCode的返回值是一个模板字符串,注意,graph需要通过JSON.stringify预处理,否则传入的${graph}实际上是graph.toString(),也就是[object Object]
为了防止模块之间的数据污染,我们使用了闭包。
最外层传入的是graph,graph包含了运行entry的所有代码和依赖信息。
run是运行传入参数module文件的方法,我们在闭包中执行了run(entry),也就是执行index.js
这里希望读者仔细阅读run函数,run函数涉及到了闭包,参数传递,递归等用法,需要读者具有一定的js基础。
我们此时的demo是run('./src/index.js')
首先我们从graph中拿到index.jscode,但是我们可以观察code,其中有_interopRequireDefault(require("./message.js"))这样的代码,也就是说,执行该段code还需要一个require函数,所以我们在一个闭包中执行该段code,同时向其中传入一个函数,闭包使用require对该函数进行接收,而require(module) 的效果和run类似,都是执行该模块,但是,_interopRequireDefault(require("./message.js")),如果要执行**./message.js**,我们需要在graph中获取到对应的code,而graph中并不存在 ./message.js 字段,我们需要graph[module].dependencies[path]来获取其在graph的键值,这里稍作解释:
当我们执行run('./src/index')时,index.js需要**./message.js**,而graph['./src/index.js'].dependencies['./message.js']就是 ‘.\src\message.js’,之后,我们就可以执行run('.\\src\\message.js'),这里有递归操作。
创建一个myExports 空对象的原因和创建myRequire类似,因为有code使用了exports对象,我们将myExports作为exports参数传递给执行code的闭包中,使执行code时能够使用到exports。

这段代码晦涩难懂,希望大家多多思考,如有不懂,欢迎讨论,共同进步。

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-02-26 11:22:00  更:2022-02-26 11:24:42 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 10:58:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码