在以上過程當中,Webpack會在特定的時間點廣播出特定的事件,插件在監聽到感興趣的事件後會執行特定的邏輯,而且插件能夠調用 Webpack 提供的 API 改變 Webpack 的運行結果。 css
在當前目錄下建立usewebpack文件夾,而後在給目錄下執行如下操做:html
$ npm init -y
$ yarn add webpack webpack-cli html-webpack-plugin
複製代碼
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {},
plugins: []
}
複製代碼
let a=require('./a');
console.log(a);
複製代碼
let b=require('./base/b');
module.exports='a'+b;
複製代碼
module.exports='b';
複製代碼
(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,而後執行如下操做:node
{
"name": "mwebpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
// 添加了bin選項,使用命令行來運行./bin/mwebpack.js
"bin": {
"mwebpack": "./bin/mwebpack.js"
},
"keywords": [],
"author": "",
"license": "ISC"
}
複製代碼
#! /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'));
複製代碼
#! /usr/bin/env node
const path = require('path');
const fs = require('fs');
const root = process.cwd();
//引入Compiler
const Compiler = require('../lib/Compiler');
let options = require(path.resolve('webpack.config.js'));
//初始化compiler對象加載全部配置的插件
let compiler = new Compiler(options);
// 執行對象的 run 方法開始執行編譯
compiler.run();
複製代碼
在當mwebpack目錄下建立/bin/Compiler.jswebpack
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
複製代碼
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
複製代碼
代碼轉換成AST,webpack中使用的Acorn,這裏使用babel-types,babel-traverse,babel-generator替代:es6
npm install babylon babel-types babel-generator babel-traverse
複製代碼
查看原生webpack生成的bundle.js,須要將require換成__webpack_require__,而且將路徑修改成相對於根目錄的相對路徑web
{
"./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: shell
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
複製代碼
每次編譯打包後,都會發現webpack打包後的結果很大部分都是同樣的,能夠抽離出一個模板用來構建每次打包的結果:npm
// 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函數json
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
複製代碼
輸出的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';`);}),
});
複製代碼
上面的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'); //引入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(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,在瀏覽器中運行:
原生webpack支持不少種插件,在webpack編譯的過程當中的各個階段使用,常見的一些鉤子:
註冊規則階段的鉤子,供用戶訂閱來執行插件。
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(); //觸發entryOptions
let {plugins} = options; //獲取webpack.config.js中的plugns進行註冊
plugins.forEach(plugin => {
plugin.apply(compiler)
});
compiler.hooks.afterPlugins.call(), //觸發afterPlugins
compiler.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的主要工做: