实现目标
将一个入口文件及其依赖js打包到一个出口文件。
注意:只是简单实现webpack的部分功能,目的是帮助理解webpack的打包流程。
webpack打包流程
- 初始化Compiler: new Webpack(config)得到Compiler对象;
- 开始编译:调用Compiler对象run方法开始执行编译;
- 确定入口:根据配置中的entry找到所有入口文件;
- 编译模块:从入口文件出发,调用所有配置的Loader对模块进行编译,再找出该模块依赖的模块,递归直到所有的模块都被加载;
- 完成模块编译:使用Loader编译完所有模块后,得到了每个模块被编译后的最终内容以及模块的依赖关系图;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
具体实现
整体结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| |-- ToysWebpack |-- config |-- index.js 执行这个文件,开始打包 |-- webpack.prod.config.js 生产环境webpack打包配置 |-- webpack.test.config.js 测试环境webpack打包配置 |-- dist |-- index.bundle.js 生产环境包 |-- index.test.js 测试环境包 |-- node_modules |-- src 待打包的原始文件夹,出了两个环境的打包入口文件,其他都是依赖文件 |-- add.js |-- error.js |-- index.js 生产环境打包的入口文件 |-- log.js |-- minus.js |-- test.js 测试环境打包的入口文件 |-- webpack |-- Compiler.js 打包主文件 |-- index.js webpack打包的开始执行文件 |-- parse.js 编译工具 |-- index.html 供打包后测试使用 |-- package-lock.json |-- package.json 打包脚本写里面
|
package.json
debug - 调试脚本, test - 打测试环境包, prod - 打生产环境包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| { "name": "toys_webpack", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "debug": "cross-env NODE_ENV=prod node --inspect-brk ./config/index.js ", "test": "cross-env NODE_ENV=test node ./config/index.js ", "prod": "cross-env NODE_ENV=prod node ./config/index.js " }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.15.0", "@babel/parser": "^7.15.3", "@babel/preset-env": "^7.15.0", "@babel/traverse": "^7.15.0", "cross-env": "^7.0.3", "mkdirp": "^1.0.4" } }
|
config文件夹
index.js:执行package.json里的脚本后会到这个文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const webpack = require('../webpack/index.js') const env = process.env.NODE_ENV
const optionsMap = { prod: () => require('./webpack.prod.config.js'), test: () => require('./webpack.test.config.js') } const options = optionsMap[env]()
const compiler = webpack(options)
compiler.run()
|
webpack.prod.config.js:生产环境打包配置
1 2 3 4 5 6 7 8 9
| const path = require('path')
module.exports = { entry: '../src/index.js', output: { path: path.resolve(__dirname, '../dist'), filename: 'index.bundle.js', } }
|
webpack.test.config.js: 测试环境打包配置
1 2 3 4 5 6 7 8 9
| const path = require('path')
module.exports = { entry: '../src/test.js', output: { path: path.resolve(__dirname, '../dist'), filename: 'index.test.js', } }
|
src文件夹
生产环境相关文件
index.js: 入口文件
1 2 3 4
| import { add } from './add.js' import { log } from './log.js'
log(add(1,2))
|
add.js
1
| export const add = (a, b) => a + b
|
log.js
1
| export const log = console.log
|
测试环境相关代码
test.js: 入口文件
1 2 3 4
| import { minus } from './minus.js' import { error } from './error.js'
error(minus(1,2))
|
minus.js
1
| export const minus = (a, b) => a - b
|
error.js
1
| export const error = console.error
|
webpack文件夹
index.js
webpack打包的开始执行文件
1 2 3 4 5 6 7 8 9 10
| const Compiler = require('./Compiler.js')
const webpack = (options) => { return new Compiler(options) }
module.exports = webpack
|
Compiler.js
打包主文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| const path = require('path') const fs = require('fs') const mkdirp = require('mkdirp') const { getAst, getDeps, getCode } = require('./parse.js') const { entry } = require('../config/webpack.prod.config.js')
class Compiler { constructor (options) { this.options = options this.modules = [] this.entryFilePath = path.resolve('config', options.entry) }
run () { const fileInfo = this.build(this.entryFilePath) this.modules.push(fileInfo) this.modules.forEach(item => { const deps = item.deps for (const dep in deps) {
const depFileInfo = this.build(deps[dep]) this.modules.push(depFileInfo) } }) const depsGraph = this.modules.reduce((graph, module) => { return { ...graph, [module.filePath]: { deps: module.deps, code: module.code } } }, {}) const generate = (depsGraph) => {
const bundle = ` (function(depsGraph){ // require目的:加载入口文件 function require(module){ // 函数内部的require其实执行的是localRequire function localRequire(relativePath){ // 找到要引入模块的绝对路径,通过require加载 return require(depsGraph[module].deps[relativePath]) } // 定义暴露的对象,将来模块要暴露的内容都放在这里 let exports = {}; (function(require, exports, code){ eval(code) })(localRequire, exports, depsGraph[module].code); // 作为require的返回值返回出去 return exports } require('${this.entryFilePath}') })(${JSON.stringify(depsGraph)}) ` const { path: outputPath, filename } = this.options.output const filePath = path.resolve(outputPath, filename) const made = mkdirp.sync(path.dirname(filePath)) fs.writeFileSync(filePath, bundle) } generate(depsGraph) }
build (filePath) { const ast = getAst(filePath) const deps = getDeps(filePath, ast) const code = getCode(ast) return { filePath, deps, code } } }
module.exports = Compiler
|
parse.js
编译工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| const fs = require('fs') const path = require('path') const { parse } = require('@babel/parser') const traverse = require('@babel/traverse').default const { transformFromAstSync } = require('@babel/core')
const getAst = (filePath) => { const file = fs.readFileSync(filePath, 'utf-8')
const ast = parse(file, { sourceType: "module" }) return ast }
const getDeps = (filePath, ast) => { const deps = {} const dirname = path.dirname(filePath) traverse(ast, {
ImportDeclaration: (NodePath) => { const relativePath = NodePath.node.source.value const absolutePath = path.resolve(dirname, relativePath) deps[relativePath] = absolutePath } }) return deps }
const getCode = (ast) => { const { code } = transformFromAstSync(ast, null, { presets: ["@babel/preset-env"] }) return code }
module.exports = { getAst, getDeps, getCode }
|
index.html
供打包后测试使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!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="./dist/index.bundle.js"></script> <script src="./dist/index.test.js"></script> </body> </html>
|