手把手教你撸一个简易的 webpack

背景

随着前端复杂度的不断提升,诞生出很多打包工具,比如最先的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),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

image

大家可以通过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}})`;// We simply return the result, hurray! :)return result;
}

到这里基本上也就介绍完了,接下来就是输出编译好的文件了,这里我们为了可以全局使用tinypack包,我们还需要为其添加到全局命令(这里直接参考我的源码吧,不再赘述了)。我们来测试一下:

npm i tinypack_demo@1.0.7 -gcd examplestinypack

看一下输出的文件:

(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;},
})

再测试一下:
image

恩,基本上已经完成一个建议的 tinypack

参考文章

抽象语法树 Abstract syntax tree

一看就懂的JS抽象语法树

源码

tinypack 所有的源码已经上传 github

转载于:https://www.cnblogs.com/tiedaweishao/p/9155075.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/427627.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

centos7.3下apache搭建django[未成功]

centos7.3下apache搭建django[未成功] 1 apache肯定已经按照完毕了, 如果没有 yum install httpd yum install mod_wsgi 安装完成之后,mod_wsgi.so会在Apache的modules目录中, 2 在httpd.conf文件中添加以下内容  LoadModule wsgi_module modules/mod…

排序算法入门之冒泡排序

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!在开发中,对一组数据进行有序地排…

VMware12.0下安装CentOS-6.9-x86_64-bin-DVD.iso

使用的是vmware workstation 12 pro 创建虚拟机 注意上面的 安装程序光盘镜象文件(iso)(M): 是我之前配置,现在可以不做任何处理 此处使用的是centos的64位 在创建虚拟机存在的地方是D:\vmware\redis,因为我准备在这装redis集群&a…

判断回文字符串

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!《递归入门》所谓回文字符串,就是…

matplotlib 中文显示 的问题

第一种方法 from pylab import mpl import numpy as npmpl.rcParams[font.sans-serif] [SimHei] # 指定默认字体 mpl.rcParams[axes.unicode_minus] False # 解决保存图像是负号 - 显示为方块的问题 %pylab inline t np.arange(-5 * np.pi, 5 * np.pi, 0.01) y np.sin…

排序算法入门之冒泡排序优化

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!这篇文章是对上一篇文章中的冒泡排序进行优化…

递归入门 斐波那契数列

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!《递归入门》斐波那契数列百度百科斐波那契数…

用栈解决四则运算问题

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!本文章的解决方法参考了《大话数据结构》中关…

源码篇:Python 实战案例----银行系统

import time import random import pickle import osclass Card(object):def __init__(self, cardId, cardPasswd, cardMoney):self.cardId cardIdself.cardPasswd cardPasswdself.cardMony cardMoneyself.cardLock False # 后面到了锁卡的时候需要有个卡的状态class User…

排序算法入门之简单选择排序

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!在学了冒泡排序后,会发觉这种算法…

ubuntu12 04下django安装略谈

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!首先你需要肯定你的机子上装了python现在ubu…

React Native 开发环境搭建

1、安装 Python 2,不知道是否已支持 Python 3 2、安装 node,npm。。。 修改 npm 镜像,不建议使用 cnpm,cnpm 安装模块的路径与 npm 有差别 npm config set registry https://registry.npm.taobao.org --global npm config set dis…

递归入门

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!写在前面:对于强大的递归。要想做…

判断一个数是偶数还是奇数

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!《递归入门》交互递归到目前为止&#xff0c…

C语言的fgets 与 gets

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!今天在翻《C语言参考手册》查看文件操作的相…

递归入门 阶乘函数

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!《递归入门》对许多人而言,理解递…

Java-Shiro(三):Shiro与Spring MVC集成

新建Java Daynamic Web项目 导入Spring、SpringMVC依赖包: 导入Spring & Spring MVC包(导入如下所有开发包): Spring AOP依赖扩展包: 配置Spring : 1)修改web.xml导入“#contextLoaderList…

第一个django项目

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!说在前面,这篇文章是为记录下我个…

springcloud20---Config加入eureka

Config server也可以加用户名和密码。Config client通过用户名和密码访问。 Config server也可以做成高可用集群。 Config与eureka配置使用。把Config server注册到eureka。Config client也要注册到eureka。 package com.itmuch.cloud;import org.springframework.boot.SpringA…

字符串右移n位

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!题目:实现字符串右移几位&#x…