webpack原理篇(五十八):实战开发一个简易的webpack
Posted 凯小默
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了webpack原理篇(五十八):实战开发一个简易的webpack相关的知识,希望对你有一定的参考价值。
说明
玩转 webpack 学习笔记
模块化:增强代码可读性和维护性
- 传统的网页开发转变成 Web Apps 开发
- 代码复杂度在逐步增高
- 部署时希望把代码优化成几个 HTTP 请求
- 分离的 JS文件/模块,便于后续代码的维护性
常见的几种模块化方式
ES module:
import * as largeNumber from 'large-number';
// ...
largeNumber.add('999', '1');
CJS:
const largeNumbers = require('large-number');
// ...
largeNumber.add('999', '1');
AMD:
require(['large-number'], function (large-number)
// ...
largeNumber.add('999', '1');
);
AST 基础知识
抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。
在线:demo: https://esprima.org/demo/parse.html
webpack 的模块机制
- 打包出来的是一个 IIFE (匿名闭包)
- modules 是一个数组,每一项是一个模块初始化函数
__webpack_require
用来加载模块,返回module.exports
- 通过
WEBPACK_REQUIRE_METHOD(0)
启动程序
实现一个简易的 webpack
可以将 ES6 语法转换成 ES5 的语法,生成的 JS 文件可以在浏览器中运行
- 通过 babylon 生成AST
- 通过 babel-core 将AST重新生成源码
可以分析模块之间的依赖关系
- 通过 babel-traverse 的 ImportDeclaration 方法获取依赖属性
1、新建初始化项目
新建 mini-webpack 文件夹,执行下面命令,初始化项目
npm init -y
2、安装相关依赖
- babylon:使用 babylon 生成AST
- babel-core:使用 babel-core 将AST重新生成源码
- babel-traverse:使用 babel-traverse 的 ImportDeclaration 方法获取依赖属性
- babel-preset-env:通过根据目标浏览器或运行时环境自动确定所需的 Babel 插件和 polyfill,将 ES2015+ 编译为 ES5 的 Babel 预设。
npm i babylon babel-core babel-traverse
这里需要安装下面这个插件,不安装到时会报错
npm i babel-preset-env
3、添加 minipack.config.js 配置文件
里面模仿 webpack 的配置
const path = require('path');
module.exports =
// 入口
entry: path.join(__dirname, './src/index.js'),
// 输出文件
output:
path: path.join(__dirname, './dist'),
filename: 'kaimo.js'
4、添加 src 入口文件
新建 src,里面添加 index.js
文件,里面依赖 common 文件夹里的 kaimo666.js
里的方法
index.js
文件
import hello from './common/kaimo666.js';
document.write(hello('kaimo666'));
kaimo666.js
文件
export function hello(name)
return `hello $name`;
结构如下:
5、实现 mini-webpack 的核心功能
新建 lib 文件夹,首先添加 index.js 文件,到时执行 node ./lib/index.js
就可以进行编译打包了。
// 编译模块
const Compiler = require('./compiler.js');
// 获取配置
const options = require('../minipack.config.js');
// 实例化 compiler
new Compiler(options).run();
然后实现 compiler.js
功能里面需要结束 config 的配置,以及 run 去执行。
const getAst, getDependencis, transform = require("./parser.js");
const path = require('path');
const fs = require('fs');
module.exports = class Compiler
constructor(options)
const entry, output = options;
this.entry = entry;
this.output = output;
this.modules = [];
run()
// 从入口文件开始构建
const entryModule = this.buildModule(this.entry, true);
this.modules.push(entryModule);
// 遍历模块依赖进行构建
this.modules.map(_module =>
_module.dependencies.map(dependency =>
this.modules.push(this.buildModule(dependency));
)
)
// 构建完成输出文件
this.emitFiles();
/**
* 构建模块:用于获取文件的路径,ast,相关依赖
* @param filename 文件路径
* @param isEntry 是否是入口文件
* */
buildModule(filename, isEntry)
let ast;
if(isEntry)
ast = getAst(filename);
else
// 获取文件的绝对路径:process.cwd()是指当前node命令执行时所在的文件夹目录
let absolutePath = path.join(process.cwd(), './src', filename);
ast = getAst(absolutePath);
return
filename,
dependencies: getDependencis(ast),
transformCode: transform(ast)
// 输出文件
emitFiles()
// 输出的文件路径
const outputPath = path.join(this.output.path, this.output.filename);
// 组装依赖的 modules
let modules = '';
this.modules.map(_module =>
modules += `'$_module.filename': function (require, module, exports) $_module.transformCode ,`
)
// 组装生成的代码 bundle
const bundle = `
(function(modules)
function require(fileName)
const fn = modules[fileName];
const module = exports: ;
fn(require, module, module.exports);
return module.exports;
require('$this.entry');
)($modules)
`;
console.log("emitFiles--->", outputPath, bundle)
// recursive: true 参数,不管创建的目录是否存在
fs.mkdir(this.output.path, recursive: true , function(err)
if (err) throw err;
console.log("目录创建成功");
// 使用 fs.writeFileSync 将数据同步写入文件
fs.writeFileSync(outputPath, bundle, 'utf-8');
console.log("打包完毕");
);
最后实现 parser 里的相关方法
const fs = require('fs');
const babylon = require('babylon');
const default: traverse = require('babel-traverse');
const transformFromAst = require('babel-core');
module.exports =
// 获取文件的 ast
getAst: path =>
// 同步读取文件
console.log("getAst----path>", path)
const content = fs.readFileSync(path, 'utf-8');
console.log("getAst---->", content)
// 分析AST,从中得到 import 的模块信息(路径)
return babylon.parse(content,
sourceType: 'module'
)
,
// 获取文件的依赖
getDependencis: ast =>
const dependencies = [];
traverse(ast,
// ImportDeclaration 方法:当遍历到 import 时的一个回调
ImportDeclaration: ( node ) =>
// 将依赖 push 到 dependencies 中
dependencies.push(node.source.value);
);
return dependencies;
,
transform: ast =>
// es6 转化为 es5
const code = transformFromAst(ast, null,
presets: ['env']
);
return code;
结构如下:
6、添加脚本进行打包
在 package.json 里添加下面脚本
"build": "node ./lib/index.js"
然后我们执行
npm run build
打包完成之后我们可以看到多了一个 dist 的文件夹,里面有打包好的 kaimo.js
文件
7、测试打包好的文件能否正常运行
我们在 dist 文件夹下面添加 index.html
文件,添加下面代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./kaimo.js"></script>
</body>
</html>
浏览器访问 index.html
文件,效果如下
我们改动一下 src 下 index.js
的代码
import hello from './common/kaimo666.js';
document.write(hello('凯小默 kaimo777'));
然后打包,成功之后刷新页面,我们可以看到效果也变了。
以上是关于webpack原理篇(五十八):实战开发一个简易的webpack的主要内容,如果未能解决你的问题,请参考以下文章
webpack原理篇(五十五):webpack流程:准备阶段
webpack原理篇(五十七):webpack流程:文件生成
webpack原理篇(五十九):loader 的链式调用与执行顺序
webpack原理篇(五十四):Tapable是如何和webpack进行关联起来的?