webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)
什么是webpack?
它是一个模块打包器,也可以引用官网的一幅图解释,我们可以看到webpack,可以分析各个模块的依赖关系,最终打包成我们常见的静态文件,.js 、 .css 、 .jpg 、.png。今天我们先不弄那么复杂,我们就介绍webpack是怎么分析ES6的模块依赖,怎么把ES6的代码转成ES5的。
动手前的思考
为了专注于打包器编写,就不自己设计
API
了,使用webpack的API
就好。第一版
打包器
只实现简单js
打包功能,使代码能在浏览器端运行。只实现单一入口的打包器
webpack打包js步骤
根据设置的
入口文件
,找到对应文件,并分析依赖。解析[抽象语法树(AST)。
获取源码,并做适当修改,使代码能在浏览器端运行。
将入口文件以及依赖文件,通过模板打包到一个文件中。
分析webpack打包出来的文件
先创建一个简单的项目,执行打包命令npx webpack
,得到dist目录下的文件bundle.js如下。
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
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":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no static exports found */
/***/ (function(module, exports) {
eval("console.log('index.js')\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
});
可以看到上面的代码主要分两部分:
- webpack_require 是自定义的webpack加载函数
- 资源文件列表,键是文件路径, 值是一个函数对象。
然后把上面的代码可整理成一个EJS文件模板,具体可以参考下面的打包文件模板
, 用于将打包后的资源列表渲染成具体的打包文件。
创建命令目录
先创建一个命令目录,用于编写自己的webpack打包命令。
目录如下:
安装依赖包
由于ES6转ES5中需要用到babel,所以要用到一下插件
npm install --save-dev
@babel/generator
@babel/parser
@babel/traverse
@babel/types
babylon
ejs
tapable
相关插件说明:
- @babel/parser 将源代码解析成 AST。
- @babel/generator 将AST解码生 js代码。
- @babel/core 包括了整个babel工作流,也就是说在@babel/core里面我们会使用到@babel/parser、transformer[s]、以及@babel/generator。
- @babel/runtime 也是工具类,但是是为了babel编译时提供一些基础工具库。作用于transformer[s]阶段,当然这是一个工具库,如果要使用这个工具库,还需要引入@babel/plugin-transform-runtime,它才是transformer[s]阶段里面的主角。
- @babel/template 也是工具类,主要用途是为parser提供模板引擎,更加快速的转化成AST
- @babel/traverse 也是工具类,主要用途是来便利AST树,也就是在@babel/generator过程中生效。
- @babel/types 也是工具类,主要用途是在创建AST的过程中判断各种语法的类型。
软链接
创建软链接,使用npm link
将命令目录jd-pack映射到全局目录下,这样就可以使用全局命令打包了。
编写命令引导脚本
通过执行jd-pack命令执行的脚本,如下:
#! /usr/bin/env node
// 拿到webpack.config.js
let path = require('path');
let config = require(path.resolve('webpack.config.js'));
// 编译器
let Compiler = require("../lib/Compiler.js");
let compiler = new Compiler(config);
compiler.run();
编写编译器Compiler结构
编译器才是整个webpack的核心,它负责获取资源文件,解析资源文件(AST),最后生成打包的文件。
这里暂时不包括loader和plugin
首先编写Compiler文件结构,如下:
let fs = require('fs');
let path = require('path')
let babylon = require('babylon')
let t = require('@babel/types');
let traverse = require('@babel/traverse').default;
let generator = require('@babel/generator').default;
let ejs = require('ejs')
let {SyncHook} = require('tapable'); //发布订阅插件
//babylon 主要是把源码转换成ast抽象语法树
//@babel/traverse 遍历抽象语法树的工具,它会访问树中的所有节点,在进入每个节点时触发 enter 钩子函数,退出每个节点时触发 exit 钩子函数
//@babel/types 在创建AST的过程中判断各种语法的类型
//@babel/generator 将AST解码生 js代码
//@babel/parser 将源代码解析成 AST
class Compiler{
constructor(config){
//entry output
this.config = config;
// 文件入口
this.entryId; // ./src/index.js
// 所有模块依赖
this.modules = {}
this.entry = config.entry;
// 工作路径
this.root = process.cwd();
}
buildModule(modulePath, isEntry){
}
emitFile(){// 发射文件
}
run(){
//执行
this.buildModule(path.resolve(this.root, this.entry), true);
//发射一个文件, 打包后的文件
this.emitFile();
}
}
module.exports = Compiler;
文件模块递归加载
接着实现文件模块加载方法buildModule,如下:
getSource(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8');
return content;
}
buildModule(modulePath, isEntry){
let source = this.getSource(modulePath);
let moduleName = './' + path.relative(this.root, modulePath);
if(isEntry) {
this.entryId = moduleName; //保存入口名字
}
//改造source源码
let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName));
// console.log(sourceCode, dependencies)
// 把相对路径和模块中内容对应起来
this.modules[moduleName] = sourceCode;
// dependencies.forEach(dep => {
// this.buildModule(path.join(this.root, dep), false);
// })
}
文件加载模块方法使用递归的方式加载所有的文件。
文件内容解析AST
ast语法树可以参考官网https://astexplorer.net/, 通过将代码解析成AST语法树,这里最主要的是将代码中的require方法替换为webpack自定义的webpack_require方法,如下:
parse(source, parentPath) { //AST解析语法树
// console.log(source, parentPath)
let ast = babylon.parse(source); //ast在线解析,如写require('./name.js')会解析成ast语法树 https://astexplorer.net/
let dependencies = [];//依赖数组
traverse(ast, {
CallExpression(p){//调用表达式,如执行函数fun(),加载模块require()等
let node = p.node;
if (node.callee.name === 'require'){
node.callee.name = '__webpack_require__';
let moduleName = node.arguments[0].value;
moduleName = moduleName + (path.extname(moduleName)?'':'.js');
moduleName = './'+path.join(parentPath,moduleName); //'src/name.js'
dependencies.push(moduleName);
node.arguments = [t.stringLiteral(moduleName)];
}
}
});
let sourceCode = generator(ast).code;//将转换后的ast转为源码
return {sourceCode, dependencies}
}
打包文件模板
首先要拿到webpack默认打包生成的文件模板,然后把生所内容渲染在模板上即可,将webpack打包的文件内容精减后如下:
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})
/************************************************************************/
({
<%for(let key in modules){%>
"<%-key%>":
(function(module, exports, __webpack_require__) {
eval(`<%-modules[key]%>`);
}),
<%}%>
});
发射打包的文件
执行到这里表示文件已经打包完毕,即可以生成文件到dist目录了,代码如下:
emitFile(){// 发射文件
//用数据渲染
let main = path.join(this.config.output.path, this.config.output.filename);
let templateStr = this.getSource(path.join(__dirname,'bundle.ejs'));
let result = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules})
this.assets = {}
this.assets[main] = result;
fs.writeFileSync(main, this.assets[main]);
}
完整源码如下
let fs = require('fs');
let path = require('path')
let babylon = require('babylon')
let t = require('@babel/types');
let traverse = require('@babel/traverse').default;
let generator = require('@babel/generator').default;
let ejs = require('ejs')
let {SyncHook} = require('tapable'); //发布订阅插件
class Compiler{
constructor(config){
//entry output
this.config = config;
// 文件入口
this.entryId; // ./src/index.js
// 所有模块依赖
this.modules = {}
this.entry = config.entry;
// 工作路径
this.root = process.cwd();
}
getSource(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8');
return content;
}
parse(source, parentPath) { //AST解析语法树
// console.log(source, parentPath)
let ast = babylon.parse(source); //ast在线解析,如写require('./name.js')会解析成ast语法树 https://astexplorer.net/
let dependencies = [];//依赖数组
traverse(ast, {
CallExpression(p){//调用表达式,如执行函数fun(),加载模块require()等
let node = p.node;
if (node.callee.name === 'require'){
node.callee.name = '__webpack_require__';
let moduleName = node.arguments[0].value;
moduleName = moduleName + (path.extname(moduleName)?'':'.js');
moduleName = './'+path.join(parentPath,moduleName); //'src/name.js'
dependencies.push(moduleName);
node.arguments = [t.stringLiteral(moduleName)];
}
}
});
let sourceCode = generator(ast).code;//将转换后的ast转为源码
return {sourceCode, dependencies}
}
buildModule(modulePath, isEntry){
console.log(modulePath+"cccc");
let source = this.getSource(modulePath);
let moduleName = './' + path.relative(this.root, modulePath);
console.log(source, modulePath+"cccc");
if(isEntry) {
this.entryId = moduleName; //保存入口名字
}
//改造source源码
let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName));
// console.log(sourceCode, dependencies)
// 把相对路径和模块中内容对应起来
this.modules[moduleName] = sourceCode;
//递归解析更深的依赖
dependencies.forEach(dep => {
this.buildModule(path.join(this.root, dep), false);
})
}
emitFile(){// 发射文件
//用数据渲染
let main = path.join(this.config.output.path, this.config.output.filename);
let templateStr = this.getSource(path.join(__dirname,'bundle.ejs'));
let result = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules})
this.assets = {}
this.assets[main] = result;
fs.writeFileSync(main, this.assets[main]);
}
run(){
//执行
this.buildModule(path.resolve(this.root, this.entry), true);
//发射一个文件, 打包后的文件
this.emitFile();
}
}
module.exports = Compiler;
❤️感谢大家
关注公众号「ITLove」即可加我好友,大家一起共同交流和进步。