原始的<script>标签加载JS的方式的弊端:node
1.容易污染全局变量webpack
2.模块加载的顺序须要事先定义好web
3.模块之间的管理要主观了解清楚json
4.模块加载过多会致使请求过多;所有打包在一块儿影响项目的初始化浏览器
了解完为何须要打包以后,那咱们很容易就能得出咱们打包的目的:处理模块之间的依赖关系,从而更好地加载资源。babel
1.处理输入文件,分析全部依赖项多线程
2.解析依赖项,生成依赖关系图架构
3.根据依赖关系图,生成一个在浏览器中可执行的代码,输出bundle.jsapp
首先来生成一个代码结果框架
- src -- hello.js -- world.js -- entry.js - webpack.js - webpack.config.js - package.json
entry.js
import hello from './hello.js' import {world} from './world.js' hello(world); console.log('hello, webpack');
hello.js
export default function hello(name) { console.log(`hello ${name}!`) }
world.js
export const world = 'world';
webpack.config.js
const path = require("path"); module.exports = { entry: "./src/entry.js", output: { path: path.resolve(__dirname, "./dist"), filename:"bundle.js" } }
咱们主要是使用三个库来实现咱们的打包
@babel/parser: babel解析器,将代码转化为抽象语法树AST@babel/traverse: 配合babel解析器,遍历及更新AST每个子节点
@babel/core: 获取模块中的可执行内容
下面咱们来一步一步实现webpack的主要代码,先看一个基础架构
const fs = require("fs") const parser = require("@babel/parser") const traverse = require("@babel/traverse").default const path = require("path") const { transformFromAst } = require("@babel/core") module.exports = class Webpack { constructor(options) { const { entry, output } = options this.entry = entry; this.output = output; } // 主逻辑 run() {} // 处理逻辑依赖 handleDependence() {} // 生成bundle代码 generateBundle() {} // 输出打包文件 outputBundleFile() {} }
首先来看下handleDependence
// 递归处理每一个文件的逻辑依赖,以及每一个文件的内部的相对地址和绝对地址的对应关系 handleDependence(entry) { // 生成模块依赖图 const dependenceGraph = { [entry]: this.createAsset(entry), } // 递归遍历 const recursionDep = (filename, assert) => { // 维护相对路径和绝对路径的对应关系 assert.mapping = {}; // 返回路径的目录名 const dirname = path.dirname(filename); assert.dependencies.forEach(relativePath => { const absolutePath = path.join(dirname, relativePath); // 设置相对路径和绝对路径的对应关系 assert.mapping[relativePath] = absolutePath; // 若是没有该绝对路径的依赖关系 if (!dependenceGraph[absolutePath]) { const child = this.createAsset(absolutePath); dependenceGraph[absolutePath] = this.createAsset(absolutePath); if (child.dependencies.length > 0) { // 递归 recursionDep(absolutePath, child); } } }) } for (let filename in dependenceGraph) { let assert = dependenceGraph[filename] recursionDep(filename, assert); } return dependenceGraph; }
其中createAsset为
// 获取每一个文件的可执行代码和依赖关系 createAsset(entry) { // 读取文件内容 const entryContent = fs.readFileSync(entry, 'utf-8') // 使用 @babel/parser(JavaScript解析器)解析代码,生成 ast(抽象语法树) const ast = babelParser.parse(entryContent, { sourceType: "module" }) // 从 ast 中获取全部依赖模块(import),并放入 dependencies 中 const dependencies = [] traverse(ast, { // 遍历全部的 import 模块,并将相对路径放入 dependencies ImportDeclaration: ({ node }) => { dependencies.push(node.source.value) } }) // 获取文件内容 const { code } = transformFromAst(ast, null, { presets: ['@babel/preset-env'], }); return { code, dependencies, } }
handleDependence 返回的结果格式以下所示:
{ './src/entry.js': { code: '"use strict";\n\nvar _hello = _interopRequireDefault(require("./hello.js"));\n\nvar _world = require("./world.js");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\n(0, _hello["default"])(_world.world);\nconsole.log(\'hello, webpack\');', dependencies: [ './hello.js', './world.js' ], mapping: { './hello.js': 'src/hello.js', './world.js': 'src/world.js' } }, 'src/hello.js': { code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = hello;\n\nfunction hello(name) {\n console.log("hello ".concat(name, "!"));\n}', dependencies: [] }, 'src/world.js': { code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.world = void 0;\nvar world = \'world\';\nexports.world = world;', dependencies: [] } }
咱们下一步的目的就是要根据这个映射表来生成出来一个可执行的代码。
咱们最终生成的是一个文件,那咱们根据生成的对应结构,拼接成字符串,最终输入到文件中,再执行便可。
为了避免污染全局环境,咱们采用当即执行的方法。
综上,咱们的当即执行方法能够描述为
const result = ` (function() { })() `
同时,咱们再来看下处理后的代码,以./src/entry.js文件为例子
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = message; var _hello = require("./hello.js"); var _name = require("./name.js"); function message() { console.log("".concat(_hello.hello, " ").concat(_name.name, "!")); }
使用的是 CommonJS 规范,而浏览器不支持 commonJS(浏览器没有 module 、exports、require、global);因此这里咱们须要实现它们,并注入到包装器函数内。
let modules = '' for (let filename in Dependence) { let mod = graph[filename] modules += `'${filename}': { code: function(require, module, exports) { ${mod.code} }, mapping: ${JSON.stringify(mod.mapping)}, },` }
咱们再扩展下result的生成,将代码中的相对地址替换成绝对地址。
const result = ` (function(modules) { function require(moduleId) { const {fn, mapping} = modules[moduleId] function localRequire(name) { return require(mapping[name]) } const module = {exports: {}} fn(localRequire, module, module.exports) return module.exports } require('${this.entry}') })({${modules}}) `
entry是入口地址, modules是处理后的dependence;
那generateBundle为
generateBundle(dependenceMap) { let modules = ''; for (let filename in dependenceMap) { let mod = dependenceMap[filename] modules += `'${filename}': { code: function(require, module, exports) { ${mod.code} }, mapping: ${JSON.stringify(mod.mapping)}, },` } const result = ` (function(modules) { function require(moduleId) { const {code, mapping} = modules[moduleId] function localRequire(name) { return require(mapping[name]) } const module = {exports: {}} code(localRequire, module, module.exports) return module.exports } require('${this.entry}') })({${modules}}) `; return result; }
最后的outputBundle方法
// 输出打包文件 outputBundleFile(bundle) { const filePath = path.join(this.output.path, this.output.filename) fs.access(filePath, (err) => { if (!err) { fs.writeFileSync(filePath, bundle, 'utf-8'); } else { console.log(err) fs.mkdir(this.output.path, { recursive: true }, (err) => { if (err) throw err; fs.writeFileSync(filePath, bundle, 'utf-8'); }); } }); }
run() { this.outputBundleFile(this.generateBundle(this.handleDependence(this.entry))); }
(function (modules) { function require(moduleId) { const { code, mapping } = modules[moduleId] function localRequire(name) { return require(mapping[name]) } const module = { exports: {} } code(localRequire, module, module.exports) return module.exports } require('./src/entry.js') })({ './src/entry.js': { code: function (require, module, exports) { "use strict"; var _hello = _interopRequireDefault(require("./hello.js")); var _world = require("./world.js"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } (0, _hello["default"])(_world.world); console.log('hello, webpack'); }, mapping: { "./hello.js": "src/hello.js", "./world.js": "src/world.js" }, }, 'src/hello.js': { code: function (require, module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = hello; function hello(name) { console.log("hello ".concat(name, "!")); } }, mapping: undefined, }, 'src/world.js': { code: function (require, module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.world = void 0; var world = 'world'; exports.world = world; }, mapping: undefined, }, })
放到浏览器中,能够完美执行。
hello world! hello, webpack
const fs = require("fs") const parser = require("@babel/parser"); const traverse = require("@babel/traverse").default const path = require("path") const options = require("./webpack.config.js"); const { transformFromAst } = require("@babel/core") class Webpack { constructor(options) { const { entry, output } = options this.entry = entry this.output = output this.modulesArr = [] } // 主逻辑 run() { this.outputBundleFile(this.generateBundle(this.handleDependence(this.entry))); } // 处理逻辑依赖 handleDependence(entry) { // 生成模块依赖图 const dependenceGraph = { [entry]: this.createAsset(entry), } // 递归遍历 const recursionDep = (filename, assert) => { // 维护相对路径和绝对路径的对应关系 assert.mapping = {}; // 返回路径的目录名 const dirname = path.dirname(filename); assert.dependencies.forEach(relativePath => { const absolutePath = path.join(dirname, relativePath); // 设置相对路径和绝对路径的对应关系 assert.mapping[relativePath] = absolutePath; // 若是没有该绝对路径的依赖关系 if (!dependenceGraph[absolutePath]) { const child = this.createAsset(absolutePath); dependenceGraph[absolutePath] = this.createAsset(absolutePath); if (child.dependencies.length > 0) { // 递归 recursionDep(absolutePath, child); } } }) } for (let filename in dependenceGraph) { let assert = dependenceGraph[filename] recursionDep(filename, assert); } return dependenceGraph; } createAsset(entry) { // 读取文件内容 const entryContent = fs.readFileSync(entry, 'utf-8') // 使用 @babel/parser(JavaScript解析器)解析代码,生成 ast(抽象语法树) const ast = parser.parse(entryContent, { sourceType: "module" }) // 从 ast 中获取全部依赖模块(import),并放入 dependencies 中 const dependencies = [] traverse(ast, { // 遍历全部的 import 模块,并将相对路径放入 dependencies ImportDeclaration: ({ node }) => { dependencies.push(node.source.value) } }) // 获取文件内容 const { code } = transformFromAst(ast, null, { presets: ['@babel/preset-env'], }); return { code, dependencies, } } generateBundle(dependenceMap) { let modules = ''; for (let filename in dependenceMap) { let mod = dependenceMap[filename] modules += `'${filename}': { code: function(require, module, exports) { ${mod.code} }, mapping: ${JSON.stringify(mod.mapping)}, },` } const result = ` (function(modules) { function require(moduleId) { const {code, mapping} = modules[moduleId] function localRequire(name) { return require(mapping[name]) } const module = {exports: {}} code(localRequire, module, module.exports) return module.exports } require('${this.entry}') })({${modules}}) `; return result; } // 输出打包文件 outputBundleFile(bundle) { const filePath = path.join(this.output.path, this.output.filename) fs.access(filePath, (err) => { if (!err) { fs.writeFileSync(filePath, bundle, 'utf-8'); } else { console.log(err) fs.mkdir(this.output.path, { recursive: true }, (err) => { if (err) throw err; fs.writeFileSync(filePath, bundle, 'utf-8'); }); } }); } } (new Webpack(options)).run();
1.支持多文件
2.输出公共bundle
3.多线程打包
4.插件机制设计
等等
1.https://juejin.im/post/5e0d52716fb9a047f0002407?utm_source=gold_browser_extension