本框架模擬webpack打包工具javascript
詳細代碼個步驟請看git地址:https://github.com/jiangzhenfei/easy-webpackcss
{ "name": "simple-webpack", "version": "1.0.0", "description": "", "main": "index.js", "directories": { "lib": "lib" }, "scripts": { "mywebpack": "node ./bin/mwebpack.js" }, "author": "", "license": "ISC", "dependencies": { } }
#! /usr/bin/env node /*標註文件的運行環境*/ const path = require('path'); const fs = require('fs'); //當前工做目錄 const root = process.cwd(); //引入Compiler const Compiler = require('../lib/Compiler'); //配置文件和 Shell 語句中讀取與合併參數,這裏簡化邏輯,沒有處理shell部分 let options = require(path.resolve(__dirname,'../webpack.config.js')); //初始化compiler對象加載全部配置的插件 let compiler = new Compiler(options); // 執行對象的 run 方法開始執行編譯 compiler.run();
在當前目錄下建立/lib/Compiler.jsjava
const path = require('path'); const fs = require('fs'); class Compiler { constructor(options){ this.options = options; } run(){ console.log('---------start---------') } } module.exports = Compiler
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
編譯模塊:從入口文件出發,調用全部配置的Loader對模塊進行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到全部入口依賴的文件都通過了本步驟的處理 完成模塊編譯:在通過第4步使用Loader翻譯完全部模塊後,獲得了每一個模塊被翻譯後的最終內容以及它們之間的依賴關係 輸出資源:根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的Chunk,再把每一個 Chunk 轉換成一個單獨的文件加入到輸出列表,這步是能夠修改輸出內容的最後機會node
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
編譯模塊:從入口文件出發,調用全部配置的Loader對模塊進行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到全部入口依賴的文件都通過了本步驟的處理 代碼轉換成AST,webpack中使用的Acorn,這裏使用babel-types,babel-traverse,babel-generator替代: babylon把源碼轉成AST babel-types生成節點或者判斷節點類型 babel-traverse遍歷AST,捕獲指定的節點 babel-generator將AST從新生成代碼webpack
npm install babylon babel-types babel-generator babel-traverse
查看原生webpack生成的bundle.js,須要將require換成__webpack_require__,而且將路徑修改成相對於根目錄的相對路徑git
把js文件內容解析ast,而且分析require依賴es6
const path = require('path'); const fs = require('fs'); const babylon = require('babylon'); const t = require('babel-types'); //採用es6的寫法,因此要在後面添加.default const 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
輸出資源:根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的 Chunk,再把每一個 Chunk轉換成一個單獨的文件加入到輸出列表,這步是能夠修改輸出內容的最後機會 每次編譯打包後,都會發現webpack打包後的結果很大部分都是同樣的,能夠抽離出一個模板用來構建每次打包的結果: 建立entry.ejs文件github
// 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%>`);}), <% }%> });
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'); //引入ejs 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 = []; 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
上面的webpack已經具有打包js的功能了,可是還不能打包css等文件,原生的webpack是經過各類loader來打包css等其餘文件的,因此再getSource時調用loader,將其餘文件處理成js,而後進行後面的操做web
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'); }
//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; }
完善構建過程shell
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'); //引入ejs 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'); //獲取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(path.join(that.root, loaderName)); //使用loader,能夠看出loader的本質是一個函數 source = loader(source); if (loaderIndex >= 0) { iterateLoader(); } } //遍歷執行loader iterateLoader(); } } 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
建立/src/index.less
@color: #000; body{ color: @color; }
修改/src/index.js
require('index.less')
修改webpack.config.js
module.exports = { entry: './src/index.js', output: { path: './', filename: 'bundle.js' }, module: { rules: [ { test: /\.less$/, use: ['style-loader', 'less-loader'] } ] }, plugins: [ ] }
原生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'); //採用es6的寫法,因此要在後面添加.default const traverse = require('babel-traverse').default; const generator = require('babel-generator').default; const ejs = require('ejs'); //引入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.hooks.emit.call(); //觸發emit this.emitFile(); compiler.hooks.afterEmit.call(); //觸發afterEmit compiler.hooks.done.call(); //觸發done } getSource(modulePath) { let source = fs.readFileSync(modulePath, 'utf8'); let that = this; //獲取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(path.join(that.root, 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 = []; //存儲依賴的模塊路徑 //遍歷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(){ // 讀取模板文件 let entryTemplate = fs.readFileSync(path.join(this.root, 'entry.ejs'), 'utf8'); // 獲取渲染的數據 let { entryId, modules } = this; // 將數據渲染到模板上 let source = ejs.compile(entryTemplate)({ entryId, modules }); //找到目標路徑 let target = path.join(this.root,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(); //引入Compiler const Compiler = require('../lib/Compiler');
//配置文件和 Shell 語句中讀取與合併參數,這裏簡化邏輯,沒有處理shell部分 let options = require(path.resolve(__dirname,'../webpack.config.js')); //初始化compiler對象加載全部配置的插件 let compiler = new Compiler(options); compiler.hooks.entryOption.call(); //觸發entryOptions let {plugins} = options; plugins.forEach(plugin => { plugin.apply(compiler) }); compiler.hooks.afterPlugins.call(), //觸發afterPlugins // 執行對象的 run 方法開始執行編譯 compiler.run();
const { EntryOptionWebpackPlugin, AfterPlugins, RunPlugin, CompileWebpackPlugin, AfterCompileWebpackPlugin, EmitWebpackPlugin, DoneWebpackPlugin } = require('./plugins') module.exports = { entry: './src/index.js', output: { path: './', 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() ] }
執行npm run mywebpack 能夠看到
##結語 webpack的主要工做: 合併option,獲取plugin註冊插件 run得到入口文件,用loader對入口文件進行處理, 將其轉化爲AST進行代碼修改,遞歸分析其依賴的模塊 根據入口文件的依賴項,將其渲染到對應的模板文件,而後寫到出口文件中