今天就是2019的最後一天,提早祝你們元旦快樂,css
這幾年一路走來略有心得,從了編程,也不能荒廢了愛寫做的手藝,因此平時有空會寫點文章,關於本身的職場、人生經驗之談。前端
今天發表下本身對手寫webpack的看法(若有不對,歡迎評論交流)
java
若不是生活所迫,誰會把本身弄的一身才華。[ 手動滑稽 ] node
webpack是一個工具,是一個致力於作前端構建的工具。簡單的理解:webpack就是一個模塊打包機器,它能夠將前端的js代碼(無論ES6/ES7)、引用的css資源、圖片資源、字體資源等各類資源進行打包整合,最後按照預設規則輸出到一個或多個js模塊文件中,而且能夠作到兼容瀏覽器運行。webpack
下載、安裝、建立配置文件(webpack.config.js)、輸入配置項、搞定!web
//webpack.config.js
let path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename:'main.js',
path: path.resolve(__dirname, 'dist')
}
}複製代碼
這是一個最簡單的配置,只包含了模式,入口文件以及出口文件,接下來咱們僅先討論webpack對js文件的打包編程
首先咱們建立一個空項目webpack-test,該項目下有三個js文件數組
//index.js 入口文件
let result = require('./a.js');
console.log(result);
// a.js 引用不b.js文件
let b = require('./b.js');
module.exports = 'a' + b;
// b.js
module.exports = 'b';複製代碼
好了,三個js文件建立好了,咱們指望webpack將這三個文件打包成一個文件,而且能正常打印'ab'。瀏覽器
命令行執行 npx webpack 能夠看到生成main.js文件緩存
// main.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/a.js":
(function(module, exports, __webpack_require__) {
eval("let b = __webpack_require__(/*! ./b.js */ \"./src/b.js\");\r\n\r\nmodule.exports = 'a' + b;\n\n//# sourceURL=webpack:///./src/a.js?");
}),
"./src/b.js":
(function(module, exports) {
eval("module.exports = 'b';\n\n//# sourceURL=webpack:///./src/b.js?");
}),
"./src/index.js":
(function(module, exports, __webpack_require__) {
eval("let result = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\r\nconsole.log(result);\n\n//# sourceURL=webpack:///./src/index.js?");
})
});複製代碼
徹底符合預期,三個 js文件打包成一個,並正常打印出'ab'。
咱們剛纔打包出的main.js文件,就是webpack最後生成的文件,那麼咱們來分析下man.js,首先將裏面內容清空
// main.js
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
// ...
}
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})();複製代碼
整個文件只含一個當即執行函數(IIFE)咱們一般叫它webpackBootstrap ,函數內部最後執行__webpack_require__()函數,這個函數咱們暫且不去理會,咱們先來看傳入參數(modules)是什麼?
{
"./src/a.js":
(function(module, exports, __webpack_require__) {
eval("let b = __webpack_require__(/*! ./b.js */ \"./src/b.js\");\r\n\r\nmodule.exports = 'a' + b;\n\n//# sourceURL=webpack:///./src/a.js?");
}),
"./src/b.js":
(function(module, exports) {
eval("module.exports = 'b';\n\n//# sourceURL=webpack:///./src/b.js?");
}),
"./src/index.js":
(function(module, exports, __webpack_require__) {
eval("let result = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\r\nconsole.log(result);\n\n//# sourceURL=webpack:///./src/index.js?");
})
}複製代碼
很明顯,參數是一個對象,key對應各代碼塊相對路徑,value則是代碼塊自己。 接下來咱們看函數內部都作了什麼事?
// main.js
(function(modules) {
// 定義installedModules用來緩存_webpack_require_函數加載過的模塊
var installedModules = {};
// 定義模塊加載函數 __webpack_require__ 且該函數只接收一個參數moduleId
function __webpack_require__(moduleId) {
// ...
}
// 執行模塊加載函數 傳入參數爲 './src/index.js' 即入口文件相對路徑
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})();複製代碼
能夠看到其實主要作了兩件事:
一、定義一個模塊加載函數 webpack_require。
二、使用加載函數加載入口模塊 「./src/index.js」。
接下來咱們分析__webpack_require__函數內部邏輯
// webpack 模塊加載函數 __webpack_require__
function __webpack_require__(moduleId) {
// 重複加載則利用緩存
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 第一次被加載的模塊 初始化模塊對象 並緩存到installedModules對象裏
var module = installedModules[moduleId] = {
i: moduleId, // module 對象i 屬性值爲傳入參數moduleId 即 模塊相對路徑值
l: false, // l 屬性值爲false 標識未加載
exports: {} // 模塊導出對象
}
//
/**
* module.exports 模塊導出對象引用 其實就是改變了模塊包裹函數內部的 this 指向
* module 當前模塊對象引用
* module.exports 模塊導出對象引用
* __webpack_require__ 用於在模塊中加載其餘模塊
*/
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 模塊加載標識爲已加載
module.l = true;
// 返回當前模塊的導出對象引用
return module.exports;
}複製代碼
首先,加載函數使用了閉包變量 installedModules,用來將已加載過的模塊保存在內存中。 接着是初始化模塊對象,並把它掛載到緩存裏。而後是模塊的執行過程,加載入口文件時 modules[moduleId] 其實就是 ./src/index.js 對應的模塊函數。執行模塊函數前傳入了跟模塊相關的幾個實參,讓模塊能夠導出內容,以及加載其餘模塊的導出。最後標識該模塊加載完成,返回模塊的導出內容。
根據 webpack_require 的緩存和導出邏輯,咱們得知在整個 IIFE 運行過程當中,加載已緩存的模塊時,都會直接返回installedModules[moduleId].exports,換句話說,相同的模塊只有在第一次引用的時候纔會執行模塊自己。
模塊都經過modules[moduleId].call(module.exports, module, module.exports, webpack_require);這個函數加載進來 下面咱們就進入到 modules[moduleId]代碼塊內部。 首先加載的確定是入口文件'./src/index.js'
// "./src/index.js":
(function(module, exports, __webpack_require__) {
let result = __webpack_require__("./src/a.js");
console.log(result);
})複製代碼
能夠看到當加載index.js的時候 先經過__webpack_require__函數先去加載a.js
// "./src/a.js":
(function(module, exports, __webpack_require__) {
let b = __webpack_require__("./src/b.js");
module.exports = 'a' + b;
})複製代碼
當去加載a.js時候其實又先去加載b.js
(function(module, exports) {
module.exports = 'b';
})複製代碼
經過代碼咱們很直觀能夠看出模塊加載流程,只需肯定入口文件,就能夠將全部模塊按順序加載進來,而且經過moduleId參數可確保同一模塊只需加載一次
經過以上分析,咱們須要作如下工做:
一、拿到入口文件路徑(簡單 配置信息裏就有) 二、拿到各模塊相對路徑以及源碼(須要本身實現) 三、實現模塊加載函數 (參照_webpack_require__)
第一步配置咱們本身的打包命令
"scripts": {
"test-webpack": "node test-webpack.js"
}複製代碼
第二步建立執行文件
// test-webpack.js
let path = require('path');
let Compiler = require('./lib/Compiler.js');
// 拿到webpack.config.js
let config = require(path.resolve('webpack.config.js'));
// 編譯類
let compiler = new Compiler(config);
// 運行代碼
compiler.run();複製代碼
這個文件裏幹了兩件事,第一拿到打包配置信息(webpack.config.js),第二執行編譯函數(Compiler()),咱們的核心代碼其實也就在Compiler.js這個文件裏
// Compiler.js
let fs = require('fs');
let path = require('path');
class Compiler{
constructor(config){
// 接收傳入config 將config掛載到實例config上
this.config = config;
// 入口文件路徑
this.entryPath = config.entry ;
// 工做路徑
this.root = process.cwd();
// 解析全部的模塊依賴
this.modules = {};
}
// run 方法開始編譯
run(){
// 第一步須要建立模塊依賴關係
// buildModule 函數接收兩個參數,一、文件絕對路徑,二、是不是主模塊
this.buildModule(path.resolve(this.root,this.entryPath),true);
// 將打包後的文件發射出去
this.emitFile();
}
// 建立模塊依賴關係
buildModule(modulePath,isEntry){
}
emitFile(){
}
}
module.exports = Compiler;複製代碼
以前咱們說過咱們須要作的就是生成實參, buildModule函數須要幫咱們拿到key值 value值
// 建立模塊依賴關係
buildModule(modulePath,isEntry){
// 拿到模塊id 相對路徑 即key值 根據絕對路徑和工做路徑就可獲取相對路徑
let moduleId = './' + path.relative(this.root,modulePath);
// 是不是主入口
if( isEntry ){
this.entryPath = moduleId;
}
// 根據路徑拿到模塊源碼
let sourceCode = fs.readFileSync(modulePath,'utf8');
// 解析源碼 改造源碼 拿到value值
let { resultCode,dependencies } = this.parse(sourceCode,path.dirname(moduleId));
// 把相對路徑和模塊中的內容對應起來
this.modules[moduleId] = resultCode;
// 遞歸執行 加載每個依賴模塊
dependencies.forEach(dep=>{
this.buildModule(path.resolve(this.root,dep),false);
})
}複製代碼
key 值是肯定的,value值須要咱們解析改造,咱們先來看源碼是什麼
let result = require('./a.js');
console.log(result);複製代碼
再來看目標代碼是什麼
{"./src/index.js":
(function(module, exports, __webpack_require__) {
eval("let result = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\r\nconsole.log(result);\n\n//# sourceURL=webpack:///./src/index.js?");})
}複製代碼
對比源碼 和 目標代碼
一、將require轉爲__webpack_require__
二、參數'./a.js' 轉爲 './src/a.js'
這塊須要引入一個概念抽象語法樹(AST),解析源碼用的,這塊不對它作過多介紹。
而後須要引入幾個輔助包
一、babylon //其做用是把源碼轉換爲ast
二、@babel/traverse //遍歷節點
三、@babel/types //替換遍歷節點
四、@babel/generator //生成替換的節點
// 解析源碼
parse(source,parentPath){
// 源碼解析成ast
let ast = babylon.parse(source);
// 存儲每一個模塊所需依賴的模塊路徑
let dependencies = [];
// 遍歷解析後的源碼
traverse(ast,{
CallExpression(p){
let node = p.node; // 對應每一個節點
if( node.callee.name === 'require' ){
// 若是節點是require 將 require 改形成 __webpack_require__
node.callee.name = '__webpack_require__' ;
// 改造require裏面的參數 './a.js' > './src/a.js'
let resultPath = node.arguments[0].value; //拿到模塊引用名字
resultPath = resultPath + (path.extname(resultPath)?'':'.js') // 判斷是否寫後綴名
resultPath = './' + path.join(parentPath,resultPath); //拼接上父路徑
dependencies.push(resultPath);
node.arguments = [t.stringLiteral(resultPath)] //源碼名字改掉
}
}
});
// 生成轉換後的代碼
let resultCode = generator(ast).code;
// 輸出轉化後的源碼和依賴
return {resultCode,dependencies }
}複製代碼
咱們的核心代碼就完成了,打印一下轉化後的源碼
{ './src\\index.js':
'//index.js 入口文件 \nlet result = __webpack_require__("./src\\\\a.js");\n\nconsole.log(result);',
'./src\\a.js':
'// a.js 引用不b.js文件 \nlet b = __webpack_require__("./src\\\\b.js");\n\nmodule.exports = \'a\' + b;',
'./src\\b.js': '// b.js \nmodule.exports = \'b\';' }複製代碼
(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;
}
__webpack_require__.m = modules;
__webpack_require__.c = installedModules;
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
__webpack_require__.p = "";
return __webpack_require__(__webpack_require__.s = "<%-entryPath%>");
})({
<%for(let in key modules){%>
"<%-key%>":
(function(module, exports, __webpack_require__) {
eval(`<%-modules[key]%>`),
})
<%}%>
})複製代碼
模板文件用ejs去作,將webpackBootstrap源碼拿過來稍微改造下,將入口文件路徑和參數傳入,接下來咱們須要寫emitFile函數
emitFile(){
// 輸出到那個目錄下
let main = path.join(this.config.output.path,this.config.output.filename);
// 將代碼都出來
let templateStr = fs.readFileSync(path.join(__dirname,'template.ejs'),'utf8');
// 用ejs渲染 獲得目標代碼塊
let code = ejs.render(templateStr,{entryPath:this.entryPath,modules:this.modules});
// 將路徑和代碼塊對應起來
this.assets = {};
this.assets[main] = code;
// 將文件寫入
fs.writeFileSync(main,this.assets[main]);
}複製代碼
好了 咱們已經實現了webpack打包,接下來咱們看webpack中的loader與插件
什麼是loader? webpack只能處理javaScript的模塊,若是須要處理其餘類型的文件,就須要使用loader進行轉化。說的更直白些就是說webpack打包的時候只能識別.js文件,那麼它遇到其餘類型的文件就不知道該怎麼辦了,這個時候須要一個函數將其餘類型的文件包起來轉換成js代碼,而後webpack就能夠執行打包,而這個函數就是loader。常見的loder有file-loader、url-loader、style-loader、css-loader、less-loader等。下面咱們手寫下less-loader和css-loader。
// less-loader.js
// 引入less模塊
let less = require('less');
// loader 就是個函數,拿到源碼轉換成目標代碼返回 less-loader 乾的事就是把Less代碼轉換成css
function loader( code ){
let css = '';
less.render(code,function(err,c){
css = c.css;
})
css = css.replace(/\n/g, '\\n');
return css;
}
module.exports = loader;複製代碼
less-loader 乾的事就是把Less代碼轉換成css
// style-loader.js
function loader( code ){
let style = `
document.createElement('style');
style.innerHTML = ${JSON.stringify(code)}
document.head.appendChild(style);
`
return style;
}
module.exports = loader;複製代碼
style-loader就更簡單了,拿到css源碼,而後建立style標籤,將源碼賦值給style標籤,最後將style插入頭部
好了,咱們知道loader其實就是函數,目的是爲了加載非js文件,那麼webpack如何執行這些函數呢?
// webpack.config.js
let path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename:'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.less/,
use: [
path.resolve(__dirname,'loader','style-loader'),
path.resolve(__dirname,'loader','less-loader'),
]
}
]
}
}複製代碼
固然須要在webpack.config.js 文件中配置信息告訴webpack什麼狀況下用什麼loader,上面這段配置信息表面,當加載.less文件時,先執行less-loader函數,再執行 style-loader函數。 而後咱們看在加載函數中拿到這些信息後怎麼作?
getSource(modulePath){
// 拿到loader配置信息,是個數組
let rules = this.config.module.rules;
let content = fs.readFileSync(modulePath,'utf8');
// 遍歷每一個規則來處理
for ( let i = 0; i < rules.length; i++) {
let rule = rules[i];
let { test, use } = rule;
// 從最後一個規則開始執行
let len = use.length - 1 ;
if( test.test(modulePath) ){ // 這個模塊須要用loader轉換
// 獲取對應的loader函數
function normalLoader(){
let loader = require(use[len--]);
content = loader(content)
// 遞歸調用
if( len >= 0 ){
normalLoader();
}
}
normalLoader();
}
}
return content;
}複製代碼
當咱們加載模塊的時候,須要拿到loader配置信息,而後匹配什麼文件用什麼對應的loader函數,最後將處理後的代碼返回。
webpack插件是一個具備apply方法的js對象,apply方法會被webpack的compiler(編譯器)對象調用,而且compiler對象可在整個編譯生命週期內訪問。
實現插件功能咱們的compiler裏面必須定義生命週期鉤子函數,藉助tapable(能夠實現發佈訂閱) 實現咱們的鉤子函數。
let { SyncHook } = require('tapable');
class Compiler{
constructor(config){
// 接收傳入config 將config掛載到實例config上
this.config = config;
// 入口文件路徑
this.entryPath = config.entry ;
// 工做路徑
this.root = process.cwd();
// 解析全部的模塊依賴
this.modules = {};
// 建立鉤子函數
this.hooks = {
entryOption: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
run: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook(),
}
// 若是配置了plugins 參數 拿到每一個插件 並執行其apply方法。
let plugins = this.config.plugins;
if(Array.isArray(plugins)){
plugins.forEach(plugin=>{
plugin.apply(this);
});
}
}
}複製代碼
而後將鉤子函數放入對應的生命週期內
// run 方法開始編譯
run(){
// 執行開始編譯鉤子函數
this.hooks.compile.call();
// 第一步須要建立模塊依賴關係
// buildModule 函數接收兩個參數,一、文件絕對路徑,二、是不是主模塊
this.buildModule(path.resolve(this.root,this.entryPath),true);
// 執行編譯完鉤子函數
this.hooks.afterCompile.call();
// 將打包後的文件發射出去
this.emitFile();
// 執行發射完鉤子函數
this.hooks.emit.call();
// 最終完成鉤子函數
this.hooks.emit.call();
}複製代碼
下面咱們就編寫一個插件, webpack插件的組成:
一個JavaScript函數或者class(ES6語法)。 在它的原型上定義一個apply方法。 指定掛載的webpack事件鉤子。 處理webpack內部實例的特定數據。 功能完成後調用webpack提供的回調。
class TestPlugin{
apply(compiler){
compiler.hooks.compile.tap('compile',function(){
console.log('開始編譯階段,執行須要的插件')
})
}
}複製代碼
咱們定義了TestPlugin這麼一個插件, 它有一個apply方法,該方法內部監聽開始編譯鉤子函數,最後咱們將插件配置上
plugins: [
new TestPlugin(),
]複製代碼
運行
$ yarn run test-webpack
yarn run v1.21.0
$ node test-webpack.js
開始編譯階段,執行須要的插件
Done in 2.82s.複製代碼
能夠看到咱們的插件已經實現了。
這樣咱們一個簡易版的webpack就已經實現了。
在2019年的最後一天,送朋友們一句話:「永遠年輕,永遠熱淚盈眶。」
來源於個人我的公衆號「碼農小劉」。
不爲流量,只爲能結交更多喜歡互聯網、熱愛前端的朋友。