背景

随着前端复杂度的不断提升,诞生出很多打包工具,比如最先的gruntgulp。到后来的webpackParcel。但是目前很多脚手架工具,比如vue-cli已经帮我们集成了一些构建工具的使用。有的时候我们可能并不知道其内部的实现原理。其实了解这些工具的工作方式可以帮助我们更好理解和使用这些工具,也方便我们在项目开发中应用。

一些知识点

在我们开始造轮子前,我们需要对一些知识点做一些储备工作。

模块化知识

首先是模块的相关知识,主要的是 es6 modulescommonJS模块化的规范。更详细的介绍可以参考这里 CommonJS、AMD/CMD、ES6 Modules 以及 webpack 原理浅析。现在我们只需要了解:

  1. es6 modules 是一个编译时就会确定模块依赖关系的方式。
  2. CommonJS的模块规范中,Node 在对 JS 文件进行编译的过程中,会对文件中的内容进行头尾包装 ,在头部添加(function (export, require, modules, __filename, __dirname){\n 在尾部添加了\n};。这样我们在单个JS文件内部可以使用这些参数。

AST 基础知识

什么是抽象语法树?

在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

大家可以通过Esprima 这个网站来将代码转化成 ast。首先一段代码转化成的抽象语法树是一个对象,该对象会有一个顶级的type属性Program,第二个属性是body是一个数组。body数组中存放的每一项都是一个对象,里面包含了所有的对于该语句的描述信息:

type:描述该语句的类型 --变量声明语句
kind:变量声明的关键字 -- var
declaration: 声明的内容数组,里面的每一项也是一个对象type: 描述该语句的类型 id: 描述变量名称的对象type:定义name: 是变量的名字init: 初始化变量值得对象type: 类型value: 值 "is tree" 不带引号row: "\"is tree"\" 带引号
复制代码

进入正题

webpack 简易打包

有了上面这些基础的知识,我们先来看一下一个简单的webpack打包的过程,首先我们定义3个文件:

// index.js
import a from './test'console.log(a)// test.js
import b from './message'const a = 'hello' + bexport default a// message.js
const b = 'world'export default b
复制代码

方式很简单,定义了一个index.js引用test.jstest.js内部引用message.js。看一下打包后的代码:

(function (modules) {var installedModules = {};function __webpack_require__(moduleId) {if (installedModules[moduleId]) {return installedModules[moduleId].exports;}var module = installedModules[moduleId] = {i: moduleId,l: false,exports: {}};modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);// Flag the module as loadedmodule.l = true;// Return the exports of the modulereturn module.exports;}// expose the modules object (__webpack_modules__)__webpack_require__.m = modules;// expose the module cache__webpack_require__.c = installedModules;// define getter function for harmony exports__webpack_require__.d = function (exports, name, getter) {if (!__webpack_require__.o(exports, name)) {Object.defineProperty(exports, name, {enumerable: true, get: getter});}};// define __esModule on exports__webpack_require__.r = function (exports) {if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});}Object.defineProperty(exports, '__esModule', {value: true});};// create a fake namespace object// mode & 1: value is a module id, require it// mode & 2: merge all properties of value into the ns// mode & 4: return value when already ns object// mode & 8|1: behave like require__webpack_require__.t = function (value, mode) {/******/if (mode & 1) value = __webpack_require__(value);if (mode & 8) return value;if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;var ns = Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns, 'default', {enumerable: true, value: value});if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) {return value[key];}.bind(null, key));return ns;};// getDefaultExport function for compatibility with non-harmony modules__webpack_require__.n = function (module) {var getter = module && module.__esModule ?function getDefault() {return module['default'];} :function getModuleExports() {return module;};__webpack_require__.d(getter, 'a', getter);return getter;};// Object.prototype.hasOwnProperty.call__webpack_require__.o = function (object, property) {return Object.prototype.hasOwnProperty.call(object, property);};// __webpack_public_path____webpack_require__.p = "";// Load entry module and return exportsreturn __webpack_require__(__webpack_require__.s = "./src/index.js");
})({"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
"use strict";eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ \"./src/test.js\");\n\n\nconsole.log(_test__WEBPACK_IMPORTED_MODULE_0__[\"default\"])\n\n\n//# sourceURL=webpack:///./src/index.js?");}),"./src/message.js": (function (module, __webpack_exports__, __webpack_require__) {// ...}),"./src/test.js": (function (module, __webpack_exports__, __webpack_require__) {// ...})
});
复制代码

看起来很乱?没关系,我们来屡一下。一眼看过去我们看到的是这样的形式:

(function(modules) {// ...
})({// ...
})
复制代码

这样好理解了吧,就是一个自执行函数,传入了一个modules对象,modules 对象是什么样的格式呢?上面的代码已经给了我们答案:

{"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {// ...}),"./src/message.js": (function (module, __webpack_exports__, __webpack_require__) {// ...}),"./src/test.js": (function (module, __webpack_exports__, __webpack_require__) {// ...})
}
复制代码

是这样的一个 路径 --> 函数 这样的 key,value 键值对。而函数内部是我们定义的文件转移成 ES5 之后的代码:

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ \"./src/test.js\");\n\n\nconsole.log(_test__WEBPACK_IMPORTED_MODULE_0__[\"default\"])\n\n\n//# sourceURL=webpack:///./src/index.js?");
复制代码

到这里基本上结构是分析完了,接着我们看看他的执行,自执行函数一开始执行的代码是:

__webpack_require__(__webpack_require__.s = "./src/index.js");
复制代码

调用了__webpack_require_函数,并传入了一个moduleId参数是"./src/index.js"。再看看函数内部的主要实现:

// 定义 module 格式
var module = installedModules[moduleId] = {i: moduleId, // moduleIdl: false, // 是否已经缓存exports: {} // 导出对象,提供挂载};modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
复制代码

这里调用了我们modules中的函数,并传入了__webpack_require__函数作为函数内部的调用。module.exports参数作为函数内部的导出。因为index.js里面引用了test.js,所以又会通过__webpack_require__来执行对test.js的加载:

var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test.js");
复制代码

test.js内又使用了message.js所以,test.js内部又会执行对message.js的加载。message.js执行完成之后,因为没有依赖项,所以直接返回了结果:

var b = 'world'
__webpack_exports__["default"] = (b)
复制代码

执行完成之后,再一级一级返回到根文件index.js。最终完成整个文件依赖的处理。 整个过程中,我们像是通过一个依赖关系树的形式,不断地向数的内部进入,等返回结果,又开始回溯到根。

开发一个简单的 tinypack

通过上面的这些调研,我们先考虑一下一个基础的打包编译工具可以做什么?

  1. 转换ES6语法成ES5
  2. 处理模块加载依赖
  3. 生成一个可以在浏览器加载执行的 js 文件

第一个问题,转换语法,其实我们可以通过babel来做。核心步骤也就是:

  • 通过babylon生成AST
  • 通过babel-core将AST重新生成源码
/*** 获取文件,解析成ast语法* @param filename // 入口文件* @returns {*}*/
function getAst (filename) {const content = fs.readFileSync(filename, 'utf-8')return babylon.parse(content, {sourceType: 'module',});
}/*** 编译* @param ast* @returns {*}*/
function getTranslateCode(ast) {const {code} = transformFromAst(ast, null, {presets: ['env']});return code
}
复制代码

接着我们需要处理模块依赖的关系,那就需要得到一个依赖关系视图。好在babel-traverse提供了一个可以遍历AST视图并做处理的功能,通过 ImportDeclaration 可以得到依赖属性:

function getDependence (ast) {let dependencies = []traverse(ast, {ImportDeclaration: ({node}) => {dependencies.push(node.source.value);},})return dependencies
}/*** 生成完整的文件依赖关系映射* @param fileName* @param entry* @returns {{fileName: *, dependence, code: *}}*/
function parse(fileName, entry) {let filePath = fileName.indexOf('.js') === -1 ? fileName + '.js' : fileNamelet dirName = entry ? '' : path.dirname(config.entry)let absolutePath = path.join(dirName, filePath)const ast = getAst(absolutePath)return {fileName,dependence: getDependence(ast),code: getTranslateCode(ast),};
}
复制代码

到目前为止,我们也只是得到根文件的依赖关系和编译后的代码,比如我们的index.js依赖了test.js但是我们并不知道test.js还需要依赖message.js,他们的源码也是没有编译过。所以此时我们还需要做深度遍历,得到完成的深度依赖关系:

/*** 获取深度队列依赖关系* @param main* @returns {*[]}*/
function getQueue(main) {let queue = [main]for (let asset of queue) {asset.dependence.forEach(function (dep) {let child = parse(dep)queue.push(child)})}return queue
}
复制代码

那么进行到这一步我们已经完成了所有文件的编译解析。最后一步,就是需要我们按照webpack的思想对源码进行一些包装。第一步,先是要生成一个modules对象:

function bundle(queue) {let modules = ''queue.forEach(function (mod) {modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`})// ...
}
复制代码

得到 modules 对象后,接下来便是对整体文件的外部包装,注册requiremodule.exports

(function(modules) {function require(fileName) {// ...}require('${config.entry}');})({${modules}})
复制代码

而函数内部,也只是循环执行每个依赖文件的 JS 代码而已,完成代码:

function bundle(queue) {let modules = ''queue.forEach(function (mod) {modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`})const result = `(function(modules) {function require(fileName) {const fn = modules[fileName];const module = { exports : {} };fn(require, module, module.exports);return module.exports;}require('${config.entry}');})({${modules}})`;return result;
}
复制代码

到这里基本上也就介绍完了,我们来打包试一下:

(function (modules) {function require(fileName) {const fn = modules[fileName];const module = {exports: {}};fn(require, module, module.exports);return module.exports;}require('./src/index.js');
})({'./src/index.js': function (require, module, exports) {
    "use strict";var _test = require("./test");var _test2 = _interopRequireDefault(_test);function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : {default: obj};}console.log(_test2.default);}, './test': function (require, module, exports) {
    "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _message = require("./message");var _message2 = _interopRequireDefault(_message);function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : {default: obj};}var a = 'hello' + _message2.default;exports.default = a;}, './message': function (require, module, exports) {
    "use strict";Object.defineProperty(exports, "__esModule", {value: true});var b = 'world';exports.default = b;},
})
复制代码

再测试一下:

恩,基本上已经完成一个简易的 tinypack

参考文章

抽象语法树 Abstract syntax tree

一看就懂的JS抽象语法树

源码

tinypack 所有的源码已经上传 github

手把手教你撸一个简易的 webpack相关推荐

  1. 小程序工程化实践(上篇)-- 手把手教你撸一个小程序 webpack 插件,一个例子带你熟悉 webpack 工作流程...

    本文基于 webpack 4 和 babel 7,Mac OS,VS Code 小程序开发现状: 小程序开发者工具不好用,官方对 npm 的支持有限,缺少对 webpack, babel 等前端常用工 ...

  2. 手把手教你撸一个Web汇率计算器

    手把手教你撸一个Web汇率计算器 前言 前段时间刚接触到前端网页开发,但是对于刚入门的小白而言,像flask.Django等这类稍大型的框架确实不太适合,今天这个Dash是集众家之长于一体的轻量化We ...

  3. c# 手把手教你撸一个吃鸡游戏跑圈机制

    最近迷上吃鸡游戏,慢慢对他的跑圈机制产生了兴趣,于是就试着写了个吃鸡游戏跑圈机制出来~~~ 一.话不多少,先上跑圈效果图: 二.知识提要: 1.C# winform程序中比较简单的绘图控件就是 Pic ...

  4. 手把手教你撸一个能生成抖音风格动图的gif制作平台

    前言 又到了一周一次的周总结, 笔者基于之前的开源项目 blink , 开发了一款能在线配置故障艺术, 并一键生成gif动图的平台, 这里暂时取名为QT. 接下来笔者将复盘一下该可视化平台的实现步骤以 ...

  5. 哈哈,手把手教你撸一个在线网盘(附源码)!

    最近,有不少同学问,"小编,有什么开源的网盘项目吗?我想学习一下?" 肯定要满足大家啊! 说实话,网盘基本上都是大家的标配了,刚需产品,方便便利:比如想随时随地查看和修改文件,那么 ...

  6. 手把手教你撸一个泡妞神奇

    哈哈哈哈!!!当我说在写这边文章的时候,妹子已经追到了,哈哈哈哈哈!!! 其实东西是一年前写的,妹子早就追到手了,当时就是用这个东西来表白的咯,二话不说,先看效果(点击屏幕可显示下一句) 当时我是在 ...

  7. 手把手教你撸一个Loading

    点击上面蓝色字体关注 "IT大飞说" 置顶公众号(**ID:ITBigFly**)第一时间收到推送 作为 Android 开发者,无奈经常会碰到各种各样的奇葩需求,现在大多公司 U ...

  8. 手把手教你撸一个小程序带太阳码的海报分享

    1.前言 我们都知道,微信小程序目前还不支持转发朋友圈,可能现在Android是支持了,iOS还不支持,但总的来说还不能支持普遍机型.这样假如我们需要推荐某个心仪的商品到朋友圈就没法分享出去,于是就可 ...

  9. 自定义view学习-手把手教你制作一个可扩展日历控件

    来看看效果图先,手把手教你实现一个简易,但高扩展度的日历控件,可自由扩展成签到,单选,多选日期. 首先我们来分析实现思路.对于上图的效果,很明显是一个6x7的表格. 我们可以两个for循环控制绘制每个 ...

最新文章

  1. buffer IO和direct IO
  2. 红书《题目与解读》第一章 数学 题解《ACM国际大学生程序设计竞赛题目与解读》
  3. php webserver documentroot,PHP $_SERVER['DOCUMENT_ROOT'] 问题
  4. 重磅!教育部:不得将发论文和物质奖励挂钩,防止高额奖励论文
  5. 计算机应用基础模4答案,2012年自考“计算机应用基础”练习题及答案四
  6. 总结一些通用的处理方法
  7. 【前端攻略】最全面的水平垂直居中方案与flexbox布局
  8. 中南大学在线考试答案计算机基础,中南大学《计算机基础》在线考试题库(267题)(有答案).doc...
  9. otn系统中常用的电层_自动化系统中常用的液位计
  10. matlab调用手机摄像头,MATLAB下使用摄像头
  11. 《Linux编程》课堂测验 ·002【Shell编程】
  12. ScheduledExecutorService 延迟 / 周期执行线程池
  13. PLSQL中的RECORD小例子
  14. java集合框架中迭代器的作用_Java中的集合框架之迭代器
  15. Python requests练习:爬取猫眼电影排行
  16. 分享一些常用软件序列号及注册码
  17. UNI-APP前后端实战课《悦读》
  18. JAVA餐厅线上点菜系统计算机毕业设计Mybatis+系统+数据库+调试部署
  19. PRN(20200908):Frosting Weights for Better Continual Training
  20. xhtml转xsl-fo

热门文章

  1. java几点钟_实现 Java 本地缓存,该从这几点开始
  2. 打开程序时固定位置_FANUC机器人:参考位置功能介绍与设定方法
  3. 多线程之继承Thread类及多线程内存分析
  4. java断言assert
  5. MySql DATE_FORMAT函数用法
  6. elasticsearch index 之merge
  7. [error]Cannot create __weak reference in file using manual refer XCode7.3
  8. BZOJ1566 [NOI2009]管道取珠
  9. flex textinput 输入限制(数字、字母)
  10. android 什么是9.png