先創建三個文件 index.jsjavascript
let news = require('./news.js')
console.log(news.content)
複製代碼
message.jscss
module.exports = {
content: `今天要下雨啦`
}
複製代碼
news.jshtml
let message = require('./message.js')
module.exports = {
content: `今天有個大新聞,爆炸新聞!!!!內容是${message.content}`
}
複製代碼
而後咱們利用webpack進行打包,來分析這個打包後的結果前端
對這個打包後的結果進行簡化分析。抽離出來的核心代碼以下java
(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 news = __webpack_require__(/*! ./news.js */ \"./src/news.js\")\nconsole.log(news.content)\n\n//# sourceURL=webpack:///./src/index.js?");}),
"./src/message.js":(function(module, exports) {eval("\nmodule.exports = {\n content: `今天要下雨啦`\n}\n\n//# sourceURL=webpack:///./src/message.js?")}),
"./src/news.js":(function(module, exports, __webpack_require__) {eval("let message = __webpack_require__(/*! ./message.js */ \"./src/message.js\")\nmodule.exports = {\n content: `今天有個大新聞,爆炸新聞!!!!內容是${message.content}`\n}\n\n//# sourceURL=webpack:///./src/news.js?");})
})
// 對這段代碼進行分析
// 首先是一個自調用函數 modules: 是一個對象 obj
// 執行了這個自調用函數,自調用函數。內部返回了一個__webpack_require__() 傳參爲entry 裏面的值
// 執行__webpack_require__() 傳入entry的值
// 開始執行函數
// 函數內部 module = {
// i: modulesId,
// l: false
// export: {}
// }
// 繼續執行,調用了最外層傳入的參數,改變this指向爲空{}, 最後就是一個__webpack_require__的遞歸調用
複製代碼
因此其實咱們要實現一個webpack.主要任務有兩個node
#! /usr/bin/env node
const path = require('path')
// 1. 讀取須要打包項目的配置文件
let config = require(path.resolve('webpack.config.js'))
// let config = require(process.cwd(),'webpack.config.js')
const Compiler = require('../lib/compiler')
let a = new Compiler(config)
a.start()
複製代碼
class Compiler {
constructor(config) {
this.config = config // 將配置初始化
this.entry = config.entry // 入口的相對路徑
this.root = process.cwd() // 執行命令的當前目錄
}
start() {
// 進行相應的文件操做
}
}
module.exports = Compiler
複製代碼
咱們主要目的是讀取入口文件,分析各個模塊之間的依賴關係,將require替換成__webpack_require__react
class Compiler {
constructor(config) {
this.config = config // 將配置初始化
this.entry = config.entry // 入口的相對路徑
this.root = process.cwd() // 執行命令的當前目錄
this.analyseObj = {} // 這個就是咱們最後須要的文件對象
}
// 工具函數--- 用來拼接路徑
getOriginPath(path1,path2) {
return path.resolve(path1,path2)
}
// 工具函數--- 讀取文件
readFile(modulePath) {
return fs.readFileSync(modulePath,'utf-8')
}
// 入口函數
start() {
// 進行相應的文件操做
// 拿到入口文件的路徑,進行分析
let originPath = this.getOriginPath(this.root,this.entry)
this.depAnalyse(originPath)
}
//核心函數
depAnalyse(modulePath){
// 這樣content,就是咱們就能夠讀取webpack.config.js裏面的入口文件
let content = this.readFile(modulePath)
}
}
複製代碼
這一步讀取文件以後,將require替換成__webpack_require__ .主要是利用來babel插件webpack
const fs = require('fs')
const path = require('path')
const traverse = require('@babel/traverse').default;
const parser = require('@babel/parser');
const generate = require('@babel/generator').default
class Compiler {
constructor(config) {
this.config = config // 將配置初始化
this.entry = config.entry // 入口的相對路徑
this.root = process.cwd() // 執行命令的當前目錄
this.analyseObj = {} // 這個就是咱們最後須要的文件對象
}
// 工具函數--- 用來拼接路徑
getOriginPath(path1,path2) {
return path.resolve(path1,path2)
}
// 工具函數--- 讀取文件
readFile(modulePath) {
return fs.readFileSync(modulePath,'utf-8')
}
// 入口函數
start() {
// 進行相應的文件操做
// 拿到入口文件的路徑,進行分析
let originPath = this.getOriginPath(this.root,this.entry)
this.depAnalyse(originPath)
}
// 核心函數
depAnalyse(modulePath){
//這樣content,就是咱們就能夠讀取webpack.config.js裏面的入口文件
let content = this.readFile(modulePath)
// 將代碼轉化爲ast語法樹
const ast = parser.parse(content)
// traverse是將AST裏面的內容進行替換
traverse(ast, {
CallExpression(p) {
if(p.node.callee.name === 'require') {
p.node.callee.name = '__webpack_require__'
}
}
})
// 最後將ast語法樹轉化爲代碼
let sourceCode = generate(ast).code
}
}
這樣咱們就完成了第一步,讀取了當前入口文件,而後將內容的require進行了替換
複製代碼
其實上述步驟還有必定的問題。若是index.js裏面有多個模塊依賴怎麼辦?相似 index.jsgit
let a = require('./news.js) let b = require('./news1.js)
複製代碼
由於咱們須要將每一個模塊的依賴放在一個數組中進行保存。而後在對每一個模塊進行遞歸遍歷。 那麼咱們繼續改進depAnalyse函數github
depAnalyse(modulePath){
// 這樣content,就是咱們就能夠讀取webpack.config.js裏面的入口文件
let content = this.readFile(modulePath)
// 將代碼轉化爲ast語法樹
const ast = parser.parse(content)
// 用於存取當前模塊的全部依賴。便於後面遍歷
let dependencies = []
// traverse是將AST裏面的內容進行替換
traverse(ast, {
CallExpression(p) {
if(p.node.callee.name === 'require') {
p.node.callee.name = '__webpack_require__'
// 這裏是對路徑進行處理,由於在window下面文件路徑\.而lunix下面是/ 。因此咱們進行統一處理一下
let oldValue = p.node.arguments[0].value
p.node.arguments[0].value = './'+ path.join('src',oldValue).replace(/\\+/g,'/')
// 將當前模塊依賴的文件路徑推送到數組裏面,遍歷咱們後續進行遞歸遍歷
dependencies.push(p.node.arguments[0].value)
}
}
})
// 最後將ast語法樹轉化爲代碼
let sourceCode = generate(ast).code
// 把當前的依賴,和文件內容推到對象裏面去。
let relavitePath = './'+ path.relative(this.root,modulePath).replace(/\\+/g,'/')
this.analyseObj[relavitePath] = sourceCode
// 每一個模塊可能還有其餘的依賴,因此咱們須要遞歸遍歷一下。
dependencies.forEach(dep=>{
//遞歸一下
this.depAnalyse(this.getOriginPath(this.root,dep))
})
}
複製代碼
這樣你打印this.analyseObj就發現已經獲得了咱們想要的對象。接下來咱們思考怎麼生成webpack模版。
(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 = "<%-entry%>");
})
({
<% for (let k in modules) { %>
"<%-k%>":
(function(module, exports, __webpack_require__) {eval(`<%-modules[k]%>`)
}),
<%}%>
})
複製代碼
start() {
let originPath = this.getOriginPath(this.root,this.entry)
this.depAnalyse(originPath)
// 編譯完成
this.emitFile()
}
emitFile() {
let template= this.readFile(path.join(__dirname,'../template/output.ejs'))
let result = ejs.render(template,{
entry: this.entry,
modules: this.analyseObj
})
let outputPath = path.join(this.config.output.path,this.config.output.filename)
fs.writeFileSync(outputPath,result)
// console.log(result)
}
複製代碼
這樣咱們就將文件輸出到指定目錄了。而後利用node執行,或者放在瀏覽器執行。就能夠讀取了。這樣咱們就實現了簡單的webpack打包功能
loader其實本質上就是一個函數
首先在webpack.config.js中定義本身的loader
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
mode: 'development',
module:{
rules: [
// {
// test: /\.js$/,
// use: ['./src/loaders/loader1.js','./src/loaders/loader2.js','./src/loaders/loader3.js' ]
// }
// {
// test: /\.js$/,
// use: './src/loaders/loader2.js'
// },
{
test: /\.js$/,
use: {
loader: './src/loaders/loader1.js',
options: {
name: '今天111111111'
}
}
}
]
}
}
複製代碼
而後在本身新建本身的loader
module.exports = function(source){
console.log(this.query)
return source.replace(/今天/g,this.query.name)
}
複製代碼
這樣咱們最基礎的loader就已經實現了。這個loader的功能就是將js的全部 ‘今天’替換成‘'今天111111111’,如今咱們知道怎麼寫一個簡單的loader,那麼接下來咱們看一下怎麼讓咱們本身寫的webpck支持loader功能
咱們思考一下loader在何時執行呢?,應該在咱們讀取文件以後就應該執行,因此咱們繼續對dpAnalyse進行改造 注意: loader的執行順序是從下到上,從右到左
depAnalyse(modulePath){
// 這樣content,就是咱們就能夠讀取webpack.config.js裏面的入口文件
let content = this.readFile(modulePath)
// loader的處理主要是首先讀取webpack.config.js裏面的配置文件module.rules。 首先判斷js裏面的配置文件module.rules 存在與否,而後在對裏面的loader進行倒敘遍歷
// 開始處理loader
for(var i = this.rules.length-1;i>=0;i--){
// this.rules[i]
let {test,use} = this.rules[i]
// 匹配是否符合規則
if(test.test(modulePath)){
if(Array.isArray(use)){
// 這裏要判斷數字,對象,字符
// 這兒能夠封裝一下,這裏面沒有封裝
for(var j=use.length-1;j>=0;j--){
let loader = require(path.join(this.root,use[j]))
content = loader(content)
}
}else if(typeof use === 'string') {
let loader = require(path.join(this.root,use))
content = loader(content)
}else if(use instanceof Object){
// console.log(use.options)
// console.log("如今use是第一項")
let loader = require(path.join(this.root,use.loader))
content = loader.call({query:use.options},content)
}
}
}
// 將代碼轉化爲ast語法樹
const ast = parser.parse(content)
// 用於存取當前模塊的全部依賴。便於後面遍歷
let dependencies = []
// traverse是將AST裏面的內容進行替換
traverse(ast, {
CallExpression(p) {
if(p.node.callee.name === 'require') {
p.node.callee.name = '__webpack_require__'
// 這裏是對路徑進行處理,由於在window下面文件路徑\.而lunix下面是/ 。因此咱們進行統一處理一下
let oldValue = p.node.arguments[0].value
p.node.arguments[0].value = './'+ path.join('src',oldValue).replace(/\\+/g,'/')
// 將當前模塊依賴的文件路徑推送到數組裏面,遍歷咱們後續進行遞歸遍歷
dependencies.push(p.node.arguments[0].value)
}
}
})
// 最後將ast語法樹轉化爲代碼
let sourceCode = generate(ast).code
// 把當前的依賴,和文件內容推到對象裏面去。
let relavitePath = './'+ path.relative(this.root,modulePath).replace(/\\+/g,'/')
this.analyseObj[relavitePath] = sourceCode
// 每一個模塊可能還有其餘的依賴,因此咱們須要遞歸遍歷一下。
dependencies.forEach(dep=>{
//遞歸一下
this.depAnalyse(this.getOriginPath(this.root,dep))
})
}
複製代碼
這樣咱們就簡單實現了loader的功能
plugins 其實就是一個自定義的類。但webpack中規定了咱們這個類裏面必需要實現apply方法。 其實原理就是webpack中內部實現了本身的一套生命週期,而後你只須要在大家apply方法裏面去調用webpack裏面提供的生命週期就行
class HelloWorldPlugin{
apply(Compiler){
// 在文件打包結束後執行
Compiler.hooks.done.tap('HelloWorldPlugin',(compilation)=> {
console.log("整個webpack打包結束")
})
// 在webpack輸出文件的時候執行
Compiler.hooks.emit.tap('HelloWorldPlugin',(compilation)=> {
console.log("文件開始發射")
})
// console.log('hello world')
}
}
module.exports = HelloWorldPlugin
複製代碼
最後咱們只須要在webpack.config.js引入相應的plugins就能夠了。
上面能夠看到webpack實現了本身的生命週期。那麼他是怎麼實現的呢?核心其實就是一個發佈訂閱者模式,webpack其實主要是利用了一個核心庫tapable 那麼咱們本身來構建一個簡單的類,來實現相應的生命週期。主要分爲三步
// study 前端
const {SyncHook} = require('tapable')
class Frontend{
constructor() {
this.hooks = {
beforeStudy: new SyncHook(),
afterHtml: new SyncHook(),
afterCss: new SyncHook(),
afterJs: new SyncHook(),
afterReact: new SyncHook()
}
}
study() {
console.log('開始準備學習')
this.hooks.beforeStudy.call()
console.log('開始準備學習html')
this.hooks.afterHtml.call()
console.log('開始準備學習css')
this.hooks.afterCss.call()
console.log('開始準備學習js')
this.hooks.afterJs.call()
console.log('開始準備學習react')
this.hooks.afterReact.call()
}
}
let f = new Frontend()
f.hooks.afterHtml.tap('afterHtml',()=>{
console.log("學完html後我想造淘寶")
})
f.study()
複製代碼
這樣咱們就實現了本身的生命週期函數
在compiler函數一初始化的時候就定義本身的webpack的生命週期,而且在start中間進行相應的調用,這樣咱們就實現了本身的生命週期
class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry
this.root = process.cwd()
this.analyseObj = {}
this.rules = config.module.rules
this.hooks = {
// 生命週期的定義
compile: new SyncHook(),
afterCompile: new SyncHook(),
emit: new SyncHook(),
afterEmit: new SyncHook(),
done: new SyncHook()
}
// plugins數組中全部插件對象,調用apply方法,至關於註冊事件
if(Array.isArray(this.config.plugins)){
this.config.plugins.forEach(plugin => {
plugin.apply(this)
})
}
}
start() {
// 開始編譯了
this.hooks.compile.call()
// 開始打包
// 依賴分析
let originPath = this.getOriginPath(this.root,this.entry)
this.depAnalyse(originPath)
// 編譯完成了
this.hooks.afterCompile.call()
// 開始發射文件了
this.hooks.emit.call()
this.emitFile()
this.hooks.afterEmit.call()
this.hooks.done.call()
}
}
複製代碼
那麼接下來,咱們爲本身的webpack寫一個htmlplugin插件 實現本身的htmlplugin類。而且在相應的webpack週期內執行相應的函數 這裏使用了cheerio庫,主要是能夠對html字符串進行dom操做
const fs = require('fs')
const cheerio = require('cheerio')
class HTMLPlugin{
constructor(options){
this.options = options
}
apply(Compiler){
// 註冊afterEmit 事件
Compiler.hooks.afterEmit.tap('HTMLPlugin',(compilation)=> {
// console.log("整個webpack打包結束")
let result = fs.readFileSync(this.opions.template,'utf-8')
// console.log(this.options)
// console.log(result)
let $ = cheerio.load(result)
Object.keys(compilation.assets).forEach(item=>{
$(`<script src="${item}"></script>`).appendTo('body')
})
// 生成html。輸出
// $.html()
fs.writeFileSync('./dist/' + this.options.filename,$.html())
})
// console.log('hello world')
}
}
module.exports = HTMLPlugin
複製代碼
最後咱們就大功告成。詳細代碼(github):
github.com/yujun96/yj-…
github.com/yujun96/web…