说明
玩转 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'));
然后打包,成功之后刷新页面,我们可以看到效果也变了。