手把手教你撸一个简易的 webpack
背景
随着前端复杂度的不断提升,诞生出很多打包工具,比如最先的grunt
,gulp
。到后来的webpack
和 Parcel
。但是目前很多脚手架工具,比如vue-cli
已经帮我们集成了一些构建工具的使用。有的时候我们可能并不知道其内部的实现原理。其实了解这些工具的工作方式可以帮助我们更好理解和使用这些工具,也方便我们在项目开发中应用。
一些知识点
在我们开始造轮子前,我们需要对一些知识点做一些储备工作。
模块化知识
首先是模块的相关知识,主要的是 es6 modules
和 commonJS
模块化的规范。更详细的介绍可以参考这里 CommonJS、AMD/CMD、ES6 Modules 以及 webpack 原理浅析。现在我们只需要了解:
es6 modules
是一个编译时就会确定模块依赖关系的方式。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' + b
export default a
// message.js
const b = 'world'
export default b
方式很简单,定义了一个index.js
引用test.js
;test.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 loaded
module.l = true;
// Return the exports of the module
return 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 exports
return __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, // moduleId
l: 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
通过上面的这些调研,我们先考虑一下一个基础的打包编译工具可以做什么?
- 转换ES6语法成ES5
- 处理模块加载依赖
- 生成一个可以在浏览器加载执行的 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' : fileName
let 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
对象后,接下来便是对整体文件的外部包装,注册require
,module.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 -g
cd examples
tinypack
看一下输出的文件:
(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
。
参考文章
源码
tinypack 所有的源码已经上传 github
手把手教你撸一个简易的 webpack的更多相关文章
- 手把手教你撸一个 Webpack Loader
文:小 boy(沪江网校Web前端工程师) 本文原创,转载请注明作者及出处 经常逛 webpack 官网的同学应该会很眼熟上面的图.正如它宣传的一样,webpack 能把左侧各种类型的文件(webpa ...
- 学以致用:手把手教你撸一个工具库并打包发布,顺便解决JS浮点数计算精度问题
本文讲解的是怎么实现一个工具库并打包发布到npm给大家使用.本文实现的工具是一个分数计算器,大家考虑如下情况: \[ \sqrt{(((\frac{1}{3}+3.5)*\frac{2}{9}-\fr ...
- Go语言系列之手把手教你撸一个ORM(一)
项目地址:https://github.com/yoyofxteam/yoyodata 欢迎星星,感谢 前言:最近在学习Go语言,就出于学习目的手撸个小架子,欢迎提出宝贵意见,项目使用Mysql数据库 ...
- C#多线程(16):手把手教你撸一个工作流
目录 前言 节点 Then Parallel Schedule Delay 试用一下 顺序节点 并行任务 编写工作流 接口构建器 工作流构建器 依赖注入 实现工作流解析 前言 前面学习了很多多线程和任 ...
- Vue手把手教你撸一个 beforeEnter 钩子函数
地址 :https://www.jb51.net/article/138821.htm 地址 :https://www.jb51.net/article/108964.htm
- 手把手教你撸个vue2.0弹窗组件
手把手教你撸个vue2.0弹窗组件 在开始之前需要了解一下开发vue插件的前置知识,推荐先看一下vue官网的插件介绍 预览地址 http://haogewudi.me/kiko/inde... 源码地 ...
- 只有20行Javascript代码!手把手教你写一个页面模板引擎
http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ...
- iOS回顾笔记(05) -- 手把手教你封装一个广告轮播图框架
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...
- PWA入门:手把手教你制作一个PWA应用
摘要: PWA图文教程 原文:PWA入门:手把手教你制作一个PWA应用 作者:MudOnTire Fundebug经授权转载,版权归原作者所有. 简介 Web前端的同学是否想过学习app开发,以弥补自 ...
随机推荐
- spring boot 集成freemarker
- HTML 滚动条样式修改
<style> .innerbox{ overflow-y: auto; background-color: #f8f8f8; height: 200px; padding: 10px; ...
- Ubuntu Docker版本的更新与安装
突然发现自己的docker版本特别的低,目前是1.9.1属于古董级别的了,想更新一下最新版本,这样最新的一下命令就可以被支持.研究了半天都没有更新成功,更新后的版本始终都是1.9.1 :查阅了官网资料 ...
- Docker基本命令汇总
Docker的三大核心概念:镜像.容器.仓库 镜像:类似虚拟机的镜像.用俗话说就是安装文件. 容器:类似一个轻量级的沙箱,容器是从镜像创建应用运行实例,可以将其启动.开始.停止.删除.而这些容器都是相 ...
- [Mysql]——通过例子理解事务的4种隔离级别
SQL标准定义了4种隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的. 低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销. 首先,我们使用 test 数据库, ...
- The Mac App Store isn't working. How to fix?
Q. The Mac App Store isn't working. How to fix? First you must have built-in Ethernet at 'en0'. So, ...
- 探秘小程序(10):分享功能+webview
场景: 小程序页面用webview嵌入了h5页面,h5页面需要与小程序进行交互,h5页面内容不同,分享的链接也不一样 分享功能: 小程序的分享功能即用户点击小程序右上角,转发功能页面.可以指定分享卡片 ...
- Asp.Net MVC3 简单入门详解过滤器Filter(转载)
前言 在开发大项目的时候总会有相关的AOP面向切面编程的组件,而MVC(特指:Asp.Net MVC,以下皆同)项目中不想让MVC开发人员去关心和写类似身份验证,日志,异常,行为截取等这部分重复的代码 ...
- Ant + Tomcat + Jenkins 实现自动化部署
前言:博主资历尚浅,很多东西都还在刚起步学习的阶段,这几天开发任务比较轻,就在自己window系统下,模拟部署远程服务器,利用Jenkins + Ant + Tomcat 搭建了一个自动发布部署的环境 ...
- 秒懂AOP
AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如事务管理.安全检查.缓存.对象池管理等.AOP 实现的关键就在于 ...