webpack系列文章
Webpack流程概括
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
- 开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
- 确定入口:根据配置中的 entry 找出所有的入口文件
- 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译:在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
原生webpack产出
在当前目录下创建usewebpack文件夹,然后在给目录下执行以下操作:
- 安装依赖的模块
$ npm init -y$ yarn add webpack webpack-cli html-webpack-plugin复制代码
- 编写webpack配置文件
const path = require('path');module.exports = { mode: 'development', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: {}, plugins: []}复制代码
- 源文件
- src/index.js
let a=require('./a');console.log(a);复制代码
- src/a.js
let b=require('./base/b');module.exports='a'+b;复制代码
- src/base/b.js
module.exports='b';复制代码
- 产出bundle.js
(function(modules) {// 启动函数 // 模块的缓存 var installedModules = {}; // webpack实现的require方法 function __webpack_require__(moduleId) { // 检查缓存中是否存在此模块ID if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // 缓存中没有此模块ID,创建一个模块并且放置到缓存中 var module = (installedModules[moduleId] = { i: moduleId, l: false, exports: {} }); // 执行模块函数为module.export赋值 modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ); // 标志模块已经加载 module.l = true; // 返回模块的export属性 return module.exports; } // 加载入口模块并且返回export return __webpack_require__((__webpack_require__.s = "./src/index.js")); })({ "./src/a.js": function(module, exports, __webpack_require__) { eval( "let b=__webpack_require__(\"./src/base/b.js\");\r\nmodule.exports='a'+b;\n\n" ); }, "./src/base/b.js": function(module, exports) { eval("module.exports='b';\n\n"); }, "./src/index.js": function(module, exports, __webpack_require__) { eval( 'let a=__webpack_require__("./src/a.js");\r\nconsole.log(a);\r\n\n\n' ); } }); 复制代码
编写mwebpack
在当前目录下创建mwebpack文件夹,并且在文件下创建mwebpack,然后执行以下操作:
1. 创建项目package.json
{ "name": "mwebpack", "version": "1.0.0", "description": "", "main": "index.js", // 添加了bin选项,使用命令行来运行./bin/mwebpack.js "bin": { "mwebpack": "./bin/mwebpack.js" }, "keywords": [], "author": "", "license": "ISC"}复制代码
2. 创建/bin/mwebpack.js
- 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数
#! /usr/bin/env node /*标注文件的运行环境*/const path = require('path');const fs = require('fs');//当前工作目录const root = process.cwd();//配置文件和 Shell 语句中读取与合并参数,这里简化逻辑,没有处理shell部分let options = require(path.resolve('webpack.config.js'));复制代码
- 开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
#! /usr/bin/env node const path = require('path');const fs = require('fs');const root = process.cwd();//引入Compilerconst Compiler = require('../lib/Compiler'); let options = require(path.resolve('webpack.config.js'));//初始化compiler对象加载所有配置的插件let compiler = new Compiler(options); // 执行对象的 run 方法开始执行编译compiler.run();复制代码
3. 初始化Compiler
在当mwebpack目录下创建/bin/Compiler.js
const path = require('path');const fs = require('fs');class Compiler { constructor(options){ this.options = options; } run(){ console.log('---------start---------') }}module.exports = Compiler复制代码
4. 连接usewebpack和mwebpack
- 将nmwebpack/bin/mwebpack.js链接到全局D:/dev/node.js/mwebpack(node安装在D盘)
- 用命令行切换到mwebpack目录,
- 然后执行npm link,那么nmwebpack就和npm和npx一样成为nodejs的命令了
- 在usewebpack中使用mwebpack编译文件
- 用命令行切换到usewebpack目录,
- 然后执行npx mwebpack命令,可以看到usewabpack下node_modules中的.bin目录下出现了mwebpack.cmd和mwebpack的包。
- mwebpack.cmd会调用全局的mwebpack命令,然后再调用mwebpack/bin/mwebpack.js.
5. 完善run函数
- 确定入口:根据配置中的entry找出所有的入口文件
- 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
const path = require('path');const fs = require('fs');class Compiler { constructor(options){ this.options = options; } run(){ let that = this; let {entry} = this.options; // 获取webpck.config.js中的entry this.root = process.cwd(); this.entryId = null; //记录入口的id,这里采用单入口简化 this.modules = {}; //缓存入口的依赖,这里采用单入口简化 // 找出该模块依赖的模块 //再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理 this.buildModule(path.resolve(this.root, entry), true); // 输出资源 this.emitFile(); }}module.exports = Compiler复制代码
6. 编写buildModule
- 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译:在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
const path = require('path');const fs = require('fs');class Compiler { constructor(options){ this.options = options; } run(){ let that = this; let {entry} = this.options; this.root = process.cwd(); this.entryId = null; this.modules = {}; this.buildModule(path.resolve(this.root, entry), true); this.emitFile(); } getSource(modulePath) { let source = fs.readFileSync(modulePath, 'utf8'); //TODO:loader的处理逻辑写在这里,后面会提到 return source; } buildModule(modulePath,isEntry){ let that = this; let source = this.getSource(modulePath);//获取源代码 //生成相对于工作根目录的模块ID,相对路径exp:'./sec/index' let moduleId = './' + path.relative(this.root, modulePath); //如果是入口的话把id赋给compiler对象的入口 if (isEntry) { this.entryId = moduleId; } //获取AST的编译结果,获取依赖的模块,并且将代码进行转换 let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId)); this.modules[moduleId] = sourcecode; //递归解析依赖的模块 dependencies.forEach(dependency => that.buildModule(path.join(that.root, dependency))); } emitFile(){ }}module.exports = Compiler复制代码
7. 编写parse函数
- 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
代码转换成AST,webpack中使用的Acorn,这里使用babel-types,babel-traverse,babel-generator替代:
- babylon把源码转成AST
- babel-types生成节点或者判断节点类型
- babel-traverse遍历AST,捕获指定的节点
- babel-generator将AST重新生成代码
npm install babylon babel-types babel-generator babel-traverse复制代码
查看原生webpack生成的bundle.js,需要将require换成__webpack_require__,并且将路径修改为相对于根目录的相对路径
{ "./src/a.js": function(module, exports, __webpack_require__) { eval( "let b=__webpack_require__(\"./src/base/b.js\");\r\nmodule.exports='a'+b;\n\n" ); }, "./src/base/b.js": function(module, exports) { eval("module.exports='b';\n\n"); }, "./src/index.js": function(module, exports, __webpack_require__) { eval( 'let a=__webpack_require__("./src/a.js");\r\nconsole.log(a);\r\n\n\n' ); } }复制代码
利用https://astexplorer.net/可以看到require转换成AST:
const path = require('path');const fs = require('fs');const babylon = require('babylon');const t = require('babel-types');//采用es6的写法,所以要在后面添加.defaultconst traverse = require('babel-traverse').default;const generator = require('babel-generator').default;class Compiler { constructor(options){ this.options = options; } run(){ let that = this; let {entry} = this.options; this.root = process.cwd(); this.entryId = null; this.modules = {}; this.buildModule(path.resolve(this.root, entry), true); this.emitFile(); } getSource(modulePath) { let source = fs.readFileSync(modulePath, 'utf8'); //TODO:loader的处理逻辑写在这里,后面会提到 return source; } buildModule(modulePath,isEntry){ let that = this; let source = this.getSource(modulePath); let moduleId = './' + path.relative(this.root, modulePath); if (isEntry) { this.entryId = moduleId; } let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId)); this.modules[moduleId] = sourcecode; dependencies.forEach(dependency => that.buildModule(path.join(that.root, dependency))); } parse(source, parentPath) { let that = this; let ast = babylon.parse(source); //源码转语法树 let dependencies = []; //存储依赖的模块路径 //遍历AST找到对应的节点进行修改 traverse(ast, { CallExpression(p) {//p当前路径 if (p.node.callee.name == 'require') { let node = p.node; //修改方法名 node.callee.name = '__webpack_require__'; // 得到模块名exp:'./a' let moduleName = node.arguments[0].value; //如果需要的话,添加.js后缀 moduleName += (moduleName.lastIndexOf('.') > 0 ? '' : '.js'); //得到依赖模块的id,exp:'./src/a' let moduleId = './' + path.relative(that.root, path.join(parentPath, moduleName)); //相对于根目录的相对路径 node.arguments = [t.stringLiteral(moduleId)]; //把模块id放置到当前模块的依赖列表里 dependencies.push(moduleId); } } }); //将修改的AST重新生成代码 let sourcecode = generator(ast).code; return { sourcecode, dependencies }; } emitFile(){ }}module.exports = Compiler复制代码
8. 编写emitFile函数
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
每次编译打包后,都会发现webpack打包后的结果很大部分都是一样的,可以抽离出一个模板用来构建每次打包的结果:
// MainTemplate这里采用ejs模板简化(function(modules) { var installedModules = {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } 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; } return __webpack_require__((__webpack_require__.s = "<%-entryId%>")); })({ <%for (let moduleId in modules) { let source = modules[moduleId];%> "<%-moduleId%>":(function(module,exports,__webpack_require__){ eval(`<%-source%>`);}), <% }%> }); 复制代码
完善emitFile函数
const path = require('path');const fs = require('fs');const babylon = require('babylon');const t = require('babel-types');const traverse = require('babel-traverse').default;const generator = require('babel-generator').default;const ejs = require('ejs'); //引入ejsclass Compiler { constructor(options){ this.options = options; } run(){ let that = this; let {entry} = this.options; this.root = process.cwd(); this.entryId = null; this.modules = {}; this.buildModule(path.resolve(this.root, entry), true); this.emitFile(); } getSource(modulePath) { let source = fs.readFileSync(modulePath, 'utf8'); //TODO:loader的处理逻辑写在这里,后面会提到 return source; } buildModule(modulePath,isEntry){ let that = this; let source = this.getSource(modulePath); let moduleId = './' + path.relative(this.root, modulePath); if (isEntry) { this.entryId = moduleId; } let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId)); this.modules[moduleId] = sourcecode; dependencies.forEach(dependency => that.buildModule(path.join(that.root, dependency))); } parse(source, parentPath) { let that = this; let ast = babylon.parse(source); let dependencies = []; traverse(ast, { CallExpression(p) { if (p.node.callee.name == 'require') { let node = p.node; node.callee.name = '__webpack_require__'; let moduleName = node.arguments[0].value; moduleName += (moduleName.lastIndexOf('.') > 0 ? '' : '.js'); let moduleId = './' + path.relative(that.root, path.join(parentPath, moduleName)); node.arguments = [t.stringLiteral(moduleId)]; dependencies.push(moduleId); } } }); let sourcecode = generator(ast).code; return { sourcecode, dependencies }; } emitFile(){ // 读取模板文件 let entryTemplate = fs.readFileSync(path.join(__dirname, 'entry.ejs'), 'utf8'); // 获取渲染的数据 let { entryId, modules } = this; // 将数据渲染到模板上 let source = ejs.compile(entryTemplate)({ entryId, modules }); //找到目标路径 let target = path.join(this.options.output.path, this.options.output.filename); //将渲染后的模板目标文件 fs.writeFileSync(target, source); }}module.exports = Compiler复制代码
输出的bundle.js文件:
(function(modules) { var installedModules = {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } 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; } return __webpack_require__((__webpack_require__.s = "./src\index.js")); })({ "./src\index.js":(function(module,exports,__webpack_require__){ eval(`let a = __webpack_require__("./src\\a.js");console.log(a);`);}), "./src\a.js":(function(module,exports,__webpack_require__){ eval(`let b = __webpack_require__("./src\\base\\b.js");module.exports = 'a' + b;`);}), "./src\base\b.js":(function(module,exports,__webpack_require__){ eval(`module.exports = 'b';`);}), }); 复制代码
9. 实现loader功能
上面的webpack已经具备打包js的功能了,但是还不能打包css等文件,原生的webpack是通过各种loader来打包css等其他文件的,所以再getSource时调用loader,将其他文件处理成js,然后进行后面的操作
const path = require('path');const fs = require('fs');const babylon = require('babylon');const t = require('babel-types');const traverse = require('babel-traverse').default;const generator = require('babel-generator').default;const ejs = require('ejs'); //引入ejsclass Compiler { constructor(options){ this.options = options; } run(){ let that = this; let {entry} = this.options; this.root = process.cwd(); this.entryId = null; this.modules = {}; this.buildModule(path.resolve(this.root, entry), true); this.emitFile(); } getSource(modulePath) { let source = fs.readFileSync(modulePath, 'utf8'); //获取webpack.config.js中的rules let rules = that.options.module.rules; //遍历rules调用loader for (let i = 0; i < rules.length; i++) { let rule = rules[i]; // 用rule的test中正则匹配文件的类型是否需要使用laoder if (rule.test.test(modulePath)) { //获取rule中的loaders,例如['style-laoder','css-loader'] let loaders = rule.use; let length = loaders.length; //loader的数量 let loaderIndex = length - 1; // 往右向左执行 // loader遍历器 function iterateLoader() { let loaderName = loaders[loaderIndex--]; //loader只是一个包名,需要用require引入 let loader = require(join(that.root, 'node_modules', loaderName)); //使用loader,可以看出loader的本质是一个函数 source = loader(source); if (loaderIndex >= 0) { iterateLoader(); } } //遍历执行loader iterateLoader(); break; } } return source; } buildModule(modulePath,isEntry){ let that = this; let source = this.getSource(modulePath); let moduleId = './' + path.relative(this.root, modulePath); if (isEntry) { this.entryId = moduleId; } let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId)); this.modules[moduleId] = sourcecode; dependencies.forEach(dependency => that.buildModule(path.join(that.root, dependency))); } parse(source, parentPath) { let that = this; let ast = babylon.parse(source); let dependencies = []; traverse(ast, { CallExpression(p) { if (p.node.callee.name == 'require') { let node = p.node; node.callee.name = '__webpack_require__'; let moduleName = node.arguments[0].value; moduleName += (moduleName.lastIndexOf('.') > 0 ? '' : '.js'); let moduleId = './' + path.relative(that.root, path.join(parentPath, moduleName)); node.arguments = [t.stringLiteral(moduleId)]; dependencies.push(moduleId); } } }); let sourcecode = generator(ast).code; return { sourcecode, dependencies }; } emitFile(){ let entryTemplate = fs.readFileSync(path.join(__dirname, 'entry.ejs'), 'utf8'); let { entryId, modules } = this; let source = ejs.compile(entryTemplate)({ entryId, modules }); let target = path.join(this.options.output.path, this.options.output.filename); fs.writeFileSync(target, source); }}module.exports = Compiler复制代码
在usewebpack创建mode_modules/less-loader.js(为了说明loader的原理不使用的第三方的loader)
//less-loader的作用将less文件转化为css文件var less = require('less');module.exports = function (source) { let css; less.render(source, (err, output) => { css = output.css; }); return css.replace(/\n/g, '\\n', 'g');}复制代码
在usewebpack创建mode_modules/less-loader.js
//style-loader的功能就是将加载的css文件放在style标签中插入到页面module.exports = function (source) { let str = ` let style = document.createElement('style'); style.innerHTML = ${JSON.stringify(source)}; document.head.appendChild(style); `; return str;}复制代码
在usewebpack创建/src/index.less,
@color:red;body{ color:@color;}复制代码
修改usewebpack中/src/index.js,
require('index.less')复制代码
修改usewebpack中package.json
const path = require('path');module.exports = { mode: 'development', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /\.less$/, use: ['style-loader', 'less-loader'] } ] }, plugins: []}复制代码
创建一个页面引用打包后的js,在浏览器中运行:
10. 实现plugin功能
原生webpack支持很多种插件,在webpack编译的过程中的各个阶段使用,常见的一些钩子:
- entryOption 读取配置文件
- afterPlugins 加载所有的插件
- run 开始执行编译流程
- compile 开始编译
- afterCompile 编译完成
- emit 写入文件
- done 完成整体流程
- 修改bin/mwebpack.js
注册规则阶段的钩子,供用户订阅来执行插件。
const path = require('path');const fs = require('fs');const babylon = require('babylon');const t = require('babel-types');const traverse = require('babel-traverse').default;const generator = require('babel-generator').default;const ejs = require('ejs');//使用tapable来创建发布者,利用call等来触发const { SyncHook } = require('tapable');class Compiler { constructor(options){ this.options = options; this.hooks = { entryOption: new SyncHook(), afterPlugins: new SyncHook(), run: new SyncHook(), beforeCompile: new SyncHook(), afterCompile: new SyncHook(), emit: new SyncHook(), afterEmit: new SyncHook(), done: new SyncHook(), } } run(){ let compiler = this; compiler.hooks.run.call(); //触发run let {entry} = this.options; this.root = process.cwd(); this.entryId = null; this.modules compiler.hooks.beforeCompile.call(); //触发beforeCompile this.buildModule(path.resolve(this.root, entry), true); compiler.hooks.afterCompile.call(); //afterCompile this.emitFile(); compiler.hooks.afterEmit.call(); //触发afterEmit compiler.hooks.done.call(); //触发done } getSource(modulePath) { let source = fs.readFileSync(modulePath, 'utf8'); let rules = that.options.module.rules; for (let i = 0; i < rules.length; i++) { let rule = rules[i]; if (rule.test.test(modulePath)) { let loaders = rule.use; let length = loaders.length; let loaderIndex = length - 1; function iterateLoader() { let loaderName = loaders[loaderIndex--]; let loader = require(join(that.root, 'node_modules', loaderName)); source = loader(source); if (loaderIndex >= 0) { iterateLoader(); } } iterateLoader(); break; } } return source; } buildModule(modulePath,isEntry){ let that = this; let source = this.getSource(modulePath); let moduleId = './' + path.relative(this.root, modulePath); if (isEntry) { this.entryId = moduleId; } let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId)); this.modules[moduleId] = sourcecode; dependencies.forEach(dependency => that.buildModule(path.join(that.root, dependency))); } parse(source, parentPath) { let that = this; let ast = babylon.parse(source); let dependencies = []; traverse(ast, { CallExpression(p) { if (p.node.callee.name == 'require') { let node = p.node; node.callee.name = '__webpack_require__'; let moduleName = node.arguments[0].value; moduleName += (moduleName.lastIndexOf('.') > 0 ? '' : '.js'); let moduleId = './' + path.relative(that.root, path.join(parentPath, moduleName)); node.arguments = [t.stringLiteral(moduleId)]; dependencies.push(moduleId); } } }); let sourcecode = generator(ast).code; return { sourcecode, dependencies }; } emitFile(){ this.hooks.emit.call(); //触发emit let entryTemplate = fs.readFileSync(path.join(__dirname, 'entry.ejs'), 'utf8'); let { entryId, modules } = this; let source = ejs.compile(entryTemplate)({ entryId, modules }); let target = path.join(this.options.output.path, this.options.output.filename); fs.writeFileSync(target, source); }}module.exports = Compiler复制代码
#! /usr/bin/env node const path = require('path');const fs = require('fs');const root = process.cwd();const Compiler = require('../lib/Compiler');let options = require(path.resolve('webpack.config.js'));let compiler = new Compiler(options); compiler.hooks.entryOption.call(); //触发entryOptionslet {plugins} = options; //获取webpack.config.js中的plugns进行注册plugins.forEach(plugin => { plugin.apply(compiler)});compiler.hooks.afterPlugins.call(), //触发afterPluginscompiler.run();复制代码
修改usewebpack中的webpack.config.js
const path = require('path');//为了简要说明webpack插件的原理,不采用require第三方的插件class EntryOptionWebpackPlugin { apply(compiler) { compiler.hooks.entryOption.tap('Plugin', (option) => { console.log('EntryOptionWebpackPlugin'); }); }}class AfterPlugins { apply(compiler) { compiler.hooks.afterPlugins.tap('Plugin', (option) => { console.log('AfterPlugins'); }); }}class RunPlugin { apply(compiler) { compiler.hooks.run.tap('Plugin', (option) => { console.log('RunPlugin'); }); }}class CompileWebpackPlugin { apply(compiler) { compiler.hooks.compile.tap('Plugin', (option) => { console.log('CompileWebpackPlugin'); }); }}class AfterCompileWebpackPlugin { apply(compiler) { compiler.hooks.afterCompile.tap('Plugin', (option) => { console.log('AfterCompileWebpackPlugin'); }); }}class EmitWebpackPlugin { apply(compiler) { compiler.hooks.emit.tap('Plugin', () => { console.log('EmitWebpackPlugin'); }); }}class DoneWebpackPlugin { apply(compiler) { compiler.hooks.done.tap('Plugin', (option) => { console.log('DoneWebpackPlugin'); }); }}module.exports = { mode: 'development', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /\.less$/, use: ['style-loader', 'less-loader'] } ] }, plugins: [ new EntryOptionWebpackPlugin(), new AfterPlugins(), new RunPlugin(), new CompileWebpackPlugin(), new AfterCompileWebpackPlugin(), new EmitWebpackPlugin(), new DoneWebpackPlugin() ]}复制代码
执行npx mwebpack 可以看到
##结语 webpack的主要工作:- 合并option,获取plugin注册插件
- run获得入口文件,用loader对入口文件进行处理,
- 将其转化为AST进行代码修改,递归分析其依赖的模块
- 根据入口文件的依赖项,将其渲染到对应的模板文件,然后写到出口文件中