雖然使用webpack有一陣日子了,但是對其內部的相關打包過程,還不是很瞭解,今天翻閱了相關資料後,借鑑寫了一個webpack簡化版本,僅是其核心的部分,mypack
瞭解一下requireJS的大致運行過程
這裏需要node環境(用到了文件讀寫相關,webpack也是在node上運行的)
// a.js
let fs = require('fs')
function myRequire (moduleName) {
// content指示文件的內容
let content = fs.readFileSync(moduleName, 'UTF8')
let fn = new Function('exports', 'module', 'require', '__dirname',
'__filename', content + '\n return module.exports')
let module = {
exports: {}
}
return fn(module.exports, module, myRequire, __dirname, __filename)
}
let str = myRequire('./b.js')
console.log(str)
// b.js
module.exports = "Hello RequireJS!"
模擬一下AMD的打包方式,雖然不常用,但是對理解起來是有幫助的
可以直接在瀏覽器中運行
// myAMD.js
let factories = {}
function myDefine (moduleName, dependencies, factory) {
factory.dependencies = dependencies // 掛在該方法上的依賴關係
factories[moduleName] = factory // 創建對應關係
}
function myRequire (mods, callback){
let result = mods.map(mod => {
let factory = factories[mod] // 獲取該方法
let dependencies = factory.dependencies // 獲取該依賴
let exports
myRequire(dependencies, function (){
exports = factory.apply(null, arguments)
})
return exports
})
return callback.apply(null, result)
}
// 申明依賴
myDefine('name', [], function () {
return 'AMD'
})
myDefine('age', [], function () {
return 666
})
myDefine('person', ['name', 'age'], function (name, age){
let person = 'person ' + name + ' is ' + age
return person
})
// 引入依賴 第一種方式
myRequire(['name', 'age'], function (name, age){
console.log(name, age)
})
myRequire(['person'], function (person){
console.log(person)
})
其中的核心思想就是
- 申明瞭一個全局的factories,並且通過其moduleName作爲key,其factory(也就是callback)作爲value,在申明的過程中就已經關聯到了全局的factories,後續直接從中取出即可。
- 在掛載其依賴的時候,require內部分別將其依賴轉換爲依賴的結果(map方法),這裏如果再有依賴可以利用遞歸的方式解決。
mypack的實現
- 配置mypack的環境
- 首先在建立mypack,(並進入mypack文件夾中)並且將在bin中創建出mypack.js,並且初始化npm環境
npm init -y
|-mypack
| |-bin
| |-mypack.js
| |-package.json
|
// 注意修改package.json中的路徑問題 bin: bin/mypack.js
-
然後在mypack.js中文件頭部
#! /usr/bin/env node
不然會報沒有適當的js解析器 -
之後我們進行鏈接
npm link
這樣我們就全局安裝,退回其他位置都可以執行命令>mypack
- 首先確定入口文件,出口文件的位置,也即有執行的開始位置,將解析入口文件中的內容,遞歸的讀入,把require替換成本規則可識別的函數名(本例myRequire),並且以key(文件路徑),value(內容)的形式放到總的modules中。也即所有用到的依賴已經到內存中。
- 立即執行函數,並且第一個require的就是入口文件,依次執行,也就是碰見myRequire(已經在第二步全部正則替換)時,解析出內容返回。
- 遞歸的進行myRequire的解析,其參數就爲之前的modules的key,通過映射關係取出內容執行。
// mypack.js mypack代碼
#! /usr/bin/env node
let entry = './src/index.js' // 入口文件
let output = './dist/index.js' // 出口文件
const fs = require('fs')
const path = require('path')
let modules = []
let script = fs.readFileSync(entry, 'UTF8')
// 匹配對應關係
let replaceReg = (script) => {
return script.replace(/require\(['"](.+?)["']\)/g, function () {
let name = path.join('./src', arguments[1])
let content = fs.readFileSync(name, 'UTF8')
// 對於每拿到的content都進行一次替換
content = replaceReg(content)
modules.push({name, content})
return `mypack_require('${name}')`
})
}
script = replaceReg(script)
const ejs = require('ejs')
let template = `
(function(modules) {
// 引入
function mypack_require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, mypack_require);
return module.exports;
}
return mypack_require("<%-entry%>");
})
({
"<%-entry%>": (function(module, exports, mypack_require) {
eval(\`<%-script%>\`);
}) // index.js的文件
<%for(let i = 0; i < modules.length; i++){
let module = modules[i]%>
,
"<%-module.name%>": (function(module, exports, mypack_require) {
eval(\`<%-module.content%>\`);
})
<%}%>
});
`
// 最終result爲打包後的結果
let result = ejs.render(template, {
entry,
script,
modules
})
fs.writeFileSync(output, result)
console.log('mypack success')
// 目錄結構
|-src
| |a.js
| |c.js
| |index.js
// index.js打包前的內容
require('./a.js')
console.log('hello mypack')
// a.js
require('./c.js')
console.log('hello mypack666')
// c.js
console.log('=====c.js======')
// index.js 打包後的內容
(function(modules) {
// 引入
function mypack_require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, mypack_require);
return module.exports;
}
return mypack_require("./src/index.js");
})
({
"./src/index.js": (function(module, exports, mypack_require) {
eval(`mypack_require('src\a.js')
console.log('hello mypack')`);
}) // index.js的文件
,
"src\a.js": (function(module, exports, mypack_require) {
eval(`console.log('hello mypack666')`);
})
});
- 添加一個styleLoader,這裏以函數的形式進行放入,也可以單獨寫一個模塊引入
- loader 其實就是在解析所有的代碼時(遞歸讀入代碼,即replaceReg之中,對模塊內容在進行一些處理)
- loader 解析不同的文件是根據正則表達式的後綴進行區分的。
- 這裏針對於styleLoader中的css代碼,第一是進行了壓縮成一行,並且替換掉了/r/n解析時的標誌,因爲innerText接收的是字符串,然而字符串是無法換行的,所以只能壓縮成一行,在將原來空行形成的/r/n替換掉。(文末提出一種不壓縮的方案)
// mypack.js mypack代碼加入了styleLoader
#! /usr/bin/env node
let entry = './src/index.js' // 入口文件
let output = './dist/index.js' // 出口文件
const fs = require('fs')
const path = require('path')
let modules = []
let script = fs.readFileSync(entry, 'UTF8')
//======================styleLoader申明===================
let styleLoader = function (source) { //模擬styleLoader
return `
let style = document.createElement('style')
style.innerText = ${JSON.stringify(source).replace(/\\r\\n/g, '')}
document.head.appendChild(style)
`
}
//=====================styleLoader申明結束===================
// 匹配對應關係
let replaceReg = (script) => {
return script.replace(/require\(['"](.+?)["']\)/g, function () {
let name = path.join('./src', arguments[1])
let content = fs.readFileSync(name, 'UTF8')
// 對於每拿到的content都進行一次替換
content = replaceReg(content)
//======================應用規則=====================
if(/\.css$/.test(name)){
content = styleLoader(content) // 處理styleLoader
}
//=====================應用規則結束===================
modules.push({name, content})
return `mypack_require('${name}')`
})
}
script = replaceReg(script)
const ejs = require('ejs')
let template = `
(function(modules) {
// 引入
function mypack_require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, mypack_require);
return module.exports;
}
return mypack_require("<%-entry%>");
})
({
"<%-entry%>": (function(module, exports, mypack_require) {
eval(\`<%-script%>\`);
}) // index.js的文件
<%for(let i = 0; i < modules.length; i++){
let module = modules[i]%>
,
"<%-module.name%>": (function(module, exports, mypack_require) {
eval(\`<%-module.content%>\`);
})
<%}%>
});
`
// 最終result爲打包後的結果
let result = ejs.render(template, {
entry,
script,
modules
})
fs.writeFileSync(output, result)
console.log('mypack success')
// 目錄結構
|-src
| |a.js
| |c.js
| |b.css
| |index.js
// index.js打包前的內容
require('./a.js')
require('./b.css')
console.log('hello mypack')
// a.js
require('./c.js')
console.log('hello mypack666')
// c.js
console.log('=====c.js======')
//b.css
body{
background: hotpink;
}
// index.js 打包後的內容
(function(modules) {
// 引入
function mypack_require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, mypack_require);
return module.exports;
}
return mypack_require("./src/index.js");
})
({
"./src/index.js": (function(module, exports, mypack_require) {
eval(`mypack_require('src\a.js')
mypack_require('src\b.css')
console.log('hello mypack')`);
}) // index.js的文件
,
"src\c.js": (function(module, exports, mypack_require) {
eval(`console.log('=====c.js======')`);
})
,
"src\a.js": (function(module, exports, mypack_require) {
eval(`mypack_require('src\c.js')
console.log('hello mypack666')`);
})
,
"src\b.css": (function(module, exports, mypack_require) {
eval(`
let style = document.createElement('style')
style.innerText = "body{ background: hotpink;}"
document.head.appendChild(style)
`);
})
});
- 不壓縮css的一種寫法
style.innerText = \\\` ${source} \\\`
// 代碼在構建的時候 輸出到index.js變成了 evel(` ...\` backgrounde... \` ... `)
// 代碼再被執行的時候,執行evel的時候,就以模板字符串` background ...` 呈現出來
// index.js 打包後變爲
style.innerText = \` body{
background: hotpink;
} \`