在做vue項目和react項目時,都用到了webpack。webpack幫助我們很好地提高了工作效率,但是一直以來沒有對其原理進行探究,略有遺憾。 因爲使用一個工具,能夠深入瞭解其原理才能更好地使用。 這篇文章將大致分爲三個部分進行解讀:
- webpack打包簡單介紹
- 輸入webpack後發生了什麼,整個運行機制大致是怎樣的?
- 如何理解打包出的bundle.js?
- 如何實現一個簡單的webpack打包工具?
- 打包優化
第一部分: webpack打包簡單介紹
當一個項目使用webpack打包時,webpack會認爲所有的文件都是模塊,並將其打包到一個文件中。 但是webpack只能識別js文件,所以對於其他文件,我們需要使用loader來完成打包。
通過webpack打包,我們能很好地解決前端項目中的依賴問題,這樣可以幫助我們專注於實現項目的代碼邏輯,而非是依賴、命名衝突等。
第二部分: 輸入webpack後發生了什麼, 整個運行機制大致是怎樣的?
一般情況下,我們都會在根目錄下配置一個 webpack.config.js 文件,用於配置webpack打包。 當我們打開控制檯時,輸入webpack, 就會根據配置文件對項目進行打包了。但是,在這個過程中究竟發生了什麼呢?
執行腳本 bin/webpack.js
當在cmd中輸入一個命令執行時,實際上執行的都是一個類似於可執行的二進制文件,比如執行node命令、ping命令時都是這樣的, 在項目的node_modules下的webpack根目錄下找到package.json, 可以看到下面的一個kv:
"bin": { "webpack": "./bin/webpack.js" },
這就說明在執行二進制文件時,會運行 ./bin/webpack.js文件,找到這個文件,我們可以看到主要的代碼如下:
這就說明在執行二進制文件時,會運行 ./bin/webpack.js文件,找到這個文件,我們可以看到主要的代碼如下:
// 引入nodejs的path模塊 var path = require("path"); // 獲取 /bin/webpack.js的絕對路徑 try { var localWebpack = require.resolve(path.join(process.cwd(), "node_modules", "webpack", "bin", "webpack.js")); if(__filename !== localWebpack) { return require(localWebpack); } } catch(e) {} // 引入yargs模塊,用於處理命令行參數 var yargs = require("yargs") .usage("webpack " + require("../package.json").version + "\n" + "Usage: https://webpack.js.org/api/cli/\n" + "Usage without config file: webpack <entry> [<entry>] <output>\n" + "Usage with config file: webpack"); // 使用yargs來初始化命令行對象 require("./config-yargs")(yargs); var DISPLAY_GROUP = "Stats options:"; var BASIC_GROUP = "Basic options:"; // 命令行參數的基本配置 yargs.options({ "json": { type: "boolean", alias: "j", describe: "Prints the result as JSON." }, "progress": { type: "boolean", describe: "Print compilation progress in percentage", group: BASIC_GROUP }, // 省略若干 }); // yargs模塊提供的argv對象,用來讀取命令行參數,alias可以設置某個命令的簡稱,方便輸入。 var argv = yargs.argv; if(argv.verbose) { argv["display"] = "verbose"; } // argv爲讀取命令行的參數,通過conver-argv配置文件將命令行中的參數經過處理保存在options對象中 var options = require("./convert-argv")(yargs, argv); function ifArg(name, fn, init) { if(Array.isArray(argv[name])) { if(init) init(); argv[name].forEach(fn); } else if(typeof argv[name] !== "undefined") { if(init) init(); fn(argv[name], -1); } } // /bin/webpack.js的核心函數 function processOptions(options) { // 支持promise風格的異步回調(promise是承諾的意思) if(typeof options.then === "function") { options.then(processOptions).catch(function(err) { console.error(err.stack || err); process.exit(1); // eslint-disable-line }); return; } // 得到webpack編譯對象時數組情況下的options var firstOptions = [].concat(options)[0]; var statsPresetToOptions = require("../lib/Stats.js").presetToOptions; // 設置輸出option var outputOptions = options.stats; if(typeof outputOptions === "boolean" || typeof outputOptions === "string") { outputOptions = statsPresetToOptions(outputOptions); } else if(!outputOptions) { outputOptions = {}; } // 省略若干。。。。。 // 引入主入口模塊 /lib/webpack.js var webpack = require("../lib/webpack.js"); var compiler; try { // 使用webpack函數開始對獲得的配置對象進行編譯, 返回compiler compiler = webpack(options); } catch(e) { // 省略若干。。。 } function compilerCallback(err, stats) { // 編譯完成之後的回調函數 } // 如果有watch配置,則及時進行編譯。 if(firstOptions.watch || options.watch) { var watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {}; if(watchOptions.stdin) { process.stdin.on("end", function() { process.exit(0); // eslint-disable-line }); process.stdin.resume(); } compiler.watch(watchOptions, compilerCallback); console.log("\nWebpack is watching the files…\n"); } else compiler.run(compilerCallback); } // 處理這些配置選項,即調用上面的函數 processOptions(options);
實際上上面的這段代碼還是比較好理解的,就是使用相關模塊獲取到配置對象,然後從./lib/webpack.js 中獲取到webpack來進行編譯, 然後根據配置選項進行相應的處理。 這裏比較重要的就是webpack.js函數,我們來看看源碼。
./lib/webpack.js解析
// 建立webpack主函數,下面某些代碼被省略了。 function webpack(options, callback) { let compiler; if(Array.isArray(options)) { // 如果webapck是一個數組,則一次執行 compiler = new MultiCompiler(options.map(options => webpack(options))); } else if(typeof options === "object") { // 一般情況下webpack配置應該是一個對象,使用默認的處理配置中的所有選項 new WebpackOptionsDefaulter().process(options);
// 實例化一個 Compiler,Compiler 會繼承一個 Tapable 插件框架
// Compiler 實例化後會繼承到 apply、plugin 等調用和綁定插件的方法
compiler = new Compiler(); compiler.context = options.context; compiler.options = options; new NodeEnvironmentPlugin().apply(compiler); if(options.plugins && Array.isArray(options.plugins)) { // 對於選項中的插件,進行使用、編譯 compiler.apply.apply(compiler, options.plugins); } compiler.applyPlugins("environment"); compiler.applyPlugins("after-environment"); compiler.options = new WebpackOptionsApply().process(options, compiler); } else { throw new Error("Invalid argument: options"); } return compiler; } exports = module.exports = webpack;
注意:
一是 Compiler,實例化它會繼承 Tapable ,這個 Tapable 是一個插件框架,通過繼承它的一系列方法來實現註冊和調用插件,我們可以看到在 webpack 的源碼中,存在大量的 compiler.apply、compiler.applyPlugins、compiler.plugin 等Tapable方法的調用。Webpack 的 plugin 註冊和調用方式,都是源自 Tapable 。Webpack 通過 plugin 的 apply 方法安裝該 plugin,同時傳入一個 webpack 編譯對象(Webpack compiler object)。
二是 WebpackOptionsApply 的實例方法 process (options, compiler),這個方法將會針對我們傳進去的webpack 編譯對象進行逐一編譯,接下來我們再來仔細看看這個模塊。
調用 lib/WebpackOptionsApply.js
模塊的 process
方法來逐一編譯 webpack 編譯對象的各項(這裏的文件纔是比較核心的)
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; // 這裏引入了若干插件(數十個) // 給webpack中的配置對象使用插件 class WebpackOptionsApply extends OptionsApply { constructor() { super(); } // 處理配置獨享主要函數 process(options, compiler) { let ExternalsPlugin; // 根據options來配置options compiler.outputPath = options.output.path; compiler.recordsInputPath = options.recordsInputPath || options.recordsPath; compiler.recordsOutputPath = options.recordsOutputPath || options.recordsPath; compiler.name = options.name; compiler.dependencies = options.dependencies; if(typeof options.target === "string") { let JsonpTemplatePlugin; let NodeSourcePlugin; let NodeTargetPlugin; let NodeTemplatePlugin; switch(options.target) { case "web": // 省略處理代碼 case "webworker": // 省略處理代碼 case "node": case "async-node": // 省略處理代碼 break; case "node-webkit": // 省略處理代碼 break; case "atom": case "electron": case "electron-main": // 省略處理代碼 case "electron-renderer": // 省略處理代碼 default: throw new Error("Unsupported target '" + options.target + "'."); } } else if(options.target !== false) { options.target(compiler); } else { throw new Error("Unsupported target '" + options.target + "'."); } // 根據配置來決定是否生成sourcemap if(options.devtool && (options.devtool.indexOf("sourcemap") >= 0 || options.devtool.indexOf("source-map") >= 0)) { // 省略若干 // sourcemap代碼下通常都會指明源地址 comment = legacy && modern ? "\n/*\n//@ source" + "MappingURL=[url]\n//# source" + "MappingURL=[url]\n*/" : legacy ? "\n/*\n//@ source" + "MappingURL=[url]\n*/" : modern ? "\n//# source" + "MappingURL=[url]" : null; let Plugin = evalWrapped ? EvalSourceMapDevToolPlugin : SourceMapDevToolPlugin; compiler.apply(new Plugin({ filename: inline ? null : options.output.sourceMapFilename, moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate, fallbackModuleFilenameTemplate: options.output.devtoolFallbackModuleFilenameTemplate, append: hidden ? false : comment, module: moduleMaps ? true : cheap ? false : true, columns: cheap ? false : true, lineToLine: options.output.devtoolLineToLine, noSources: noSources, })); } else if(options.devtool && options.devtool.indexOf("eval") >= 0) { legacy = options.devtool.indexOf("@") >= 0; modern = options.devtool.indexOf("#") >= 0; comment = legacy && modern ? "\n//@ sourceURL=[url]\n//# sourceURL=[url]" : legacy ? "\n//@ sourceURL=[url]" : modern ? "\n//# sourceURL=[url]" : null; compiler.apply(new EvalDevToolModulePlugin(comment, options.output.devtoolModuleFilenameTemplate)); } compiler.apply( new CompatibilityPlugin(), // 使用相關插件進行處理 ); return options; } } module.exports = WebpackOptionsApply;
不出意外,這個構造函數被實例化後會返回一個對象。 然後由compiler處理
到這基本上就是大致流程了,我們可以再介紹上一步中的常用的插件:UglifyJsPlugin.js
lib/optimize/UglifyJsPlugin.js
// 引入一些依賴,主要是與壓縮代碼、sourceMap 相關
var SourceMapConsumer = require("webpack-core/lib/source-map").SourceMapConsumer;
var SourceMapSource = require("webpack-core/lib/SourceMapSource");
var RawSource = require("webpack-core/lib/RawSource");
var RequestShortener = require("../RequestShortener");
var ModuleFilenameHelpers = require("../ModuleFilenameHelpers");
var uglify = require("uglify-js");
// 定義構造器函數
function UglifyJsPlugin(options) {
...
}
// 將構造器暴露出去
module.exports = UglifyJsPlugin;
// 按照 Tapable 風格編寫插件
UglifyJsPlugin.prototype.apply = function(compiler) {
...
// 編譯器開始編譯
compiler.plugin("compilation", function(compilation) {
...
// 編譯器開始調用 "optimize-chunk-assets" 插件編譯
compilation.plugin("optimize-chunk-assets", function(chunks, callback) {
var files = [];
...
files.forEach(function(file) {
...
try {
var asset = compilation.assets[file];
if(asset.__UglifyJsPlugin) {
compilation.assets[file] = asset.__UglifyJsPlugin;
return;
}
if(options.sourceMap !== false) {
// 需要 sourceMap 時要做的一些操作...
} else {
// 獲取讀取到的源文件
var input = asset.source();
...
}
// base54 編碼重置
uglify.base54.reset();
// 將源文件生成語法樹
var ast = uglify.parse(input, {
filename: file
});
// 語法樹轉換爲壓縮後的代碼
if(options.compress !== false) {
ast.figure_out_scope();
var compress = uglify.Compressor(options.compress); // eslint-disable-line new-cap
ast = ast.transform(compress);
}
// 處理混淆變量名
if(options.mangle !== false) {
ast.figure_out_scope();
ast.compute_char_frequency(options.mangle || {});
ast.mangle_names(options.mangle || {});
if(options.mangle && options.mangle.props) {
uglify.mangle_properties(ast, options.mangle.props);
}
}
// 定義輸出變量名
var output = {};
// 處理輸出的註釋
output.comments = Object.prototype.hasOwnProperty.call(options, "comments") ? options.comments : /^\**!|@preserve|@license/;
// 處理輸出的美化
output.beautify = options.beautify;
for(var k in options.output) {
output[k] = options.output[k];
}
// 處理輸出的 sourceMap
if(options.sourceMap !== false) {
var map = uglify.SourceMap({ // eslint-disable-line new-cap
file: file,
root: ""
});
output.source_map = map; // eslint-disable-line camelcase
}
// 將壓縮後的數據輸出
var stream = uglify.OutputStream(output); // eslint-disable-line new-cap
ast.print(stream);
if(map) map = map + "";
stream = stream + "";
asset.__UglifyJsPlugin = compilation.assets[file] = (map ?
new SourceMapSource(stream, file, JSON.parse(map), input, inputSourceMap) :
new RawSource(stream));
if(warnings.length > 0) {
compilation.warnings.push(new Error(file + " from UglifyJs\n" + warnings.join("\n")));
}
} catch(err) {
// 處理異常
...
} finally {
...
}
});
// 回調函數
callback();
});
compilation.plugin("normal-module-loader", function(context) {
context.minimize = true;
});
});
};
現在我們回過頭來再看看整體流程,當我們在命令行輸入 webpack 命令,按下回車時都發生了什麼:
- 執行 bin 目錄下的 webpack.js 腳本,解析命令行參數以及開始執行編譯。
- 調用 lib 目錄下的 webpack.js 文件的核心函數 webpack ,實例化一個
Compiler
,繼承 Tapable 插件框架,實現註冊和調用一系列插件。 - 調用 lib 目錄下的
/WebpackOptionsApply.js
模塊的process
方法,使用各種各樣的插件來逐一編譯 webpack 編譯對象的各項。 - 在3中調用的各種插件編譯並輸出新文件。
第三部分:如何理解打包出的bundle.js?
一個入口文件
// webpack.config.js module.exports = { entry: ["./index.js"], //輸入 output: { //輸出 path: __dirname + "/dist", filename: "bundle.js" }, watch: true, module: { loaders: [ { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/, query: { presets: ['es2015', 'react'] } }, { test: /\.css$/, loader: 'style-loader!css-loader' }, { test: /\.less$/, use: [{ loader: "style-loader" // creates style nodes from JS strings }, { loader: "css-loader" // translates CSS into CommonJS }, { loader: "less-loader" // compiles Less to CSS }] }, { test: /\.(jpg|png|svg)$/, loader: 'url-loader' } ] } } // index.js
import React from "react";
import ReactDom from 'react-dom'
import App from './pages/app.jsx'
ReactDom.render(
<App/>,
document.querySelector('#app')
)
// bundle.js /******/ (function(modules) { // webpackBootstrap /******/ // The module cache //已安裝的模塊緩存 /******/ var installedModules = {}; /******/ /******/ // The require function //添加依賴 /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache //如果模塊在緩存中,則執行導出 /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) //如果沒有在緩存中,則加入緩存 /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, //模塊ID /******/ l: false, //是否load /******/ exports: {} //導出 /******/ }; /******/ /******/ // Execute the module function //執行模塊導出 /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded //標註模塊已經載入 /******/ module.l = true; /******/ /******/ // Return the exports of the module //返回模塊導出 /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) //暴露模塊對象 /******/ __webpack_require__.m = modules; //m是modules /******/ /******/ // expose the module cache //暴露模塊緩存 /******/ __webpack_require__.c = installedModules; //c是cache /******/ /******/ // define getter function for harmony exports //定義協調輸出的getter函數 /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __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; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 86); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ //第0個函數 /***/ function(module, exports) { console.log('index'); /***/ },
/* 1 */ //第1個函數
/***/ (function(module, exports, __webpack_require__) {
"use strict";
function reactProdInvariant(code) {
var argCount = arguments.length - 1;
var message = 'Minified React error #' + code + '; visit ' + 'http://facebook.github.io/react/docs/error-decoder.html?invariant=' + code;
for (var argIdx = 0; argIdx < argCount; argIdx++) {
message += '&args[]=' + encodeURIComponent(arguments[argIdx + 1]);
}
message += ' for the full message or use the non-minified dev environment' + ' for full errors and additional helpful warnings.';
var error = new Error(message);
error.name = 'Invariant Violation';
error.framesToPop = 1; // we don't care about reactProdInvariant's own frame
throw error;
}
module.exports = reactProdInvariant;
/***/ }),
// 省略若干。。。。
/******/ ]);
- 可以看到,這個bundle.js是一個自執行函數,前65行都在定義這個自執行函數,最後傳入了一個數組作爲參數,因爲只有一個js文件,這裏的數組長度爲1,並且數組裏的每一個元素都是一個自執行函數,自執行函數中包含着index.js裏的內容。
- 即整個bundle.js文件是一個傳入了 包含若干個模塊的數組 作爲參數,即傳入的modules是一個數組。
- 在這個bundle.js文件中的自執行函數中定義了一個webpack打包的函數 __webpack_require__, 這個函數式一個打包的核心函數, 接收一個moduleId作爲參數,moduleId是一個數字,實際上就是整個自執行函數接收的數組參數的index值。 即整個傳入的module數組,每一個元素都是一個module,我們爲之定義一個特定的moduleId,進入函數,首先判斷要加載的模塊是否已經存在,如果已經存在, 就直接返回installedModules[moduleId].exports,這樣就保證了所有的模塊只會被加載一次,而不會被多次加載。 如果說這個模塊還沒有被加載,那麼我們就創建一個installedModules[moduleId], 他是一個對象,包括i屬性(即moduleId),l屬性(表示這個模塊是否已經被加載, 初始化爲false), exports 屬性它的內容是每個模塊想要導出的內容, 接下來執行 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 函數進行調用,那麼這個函數具體是如何執行的呢? 首先保證在module.exports上進行調用這個函數,然後傳入了module參數,即我們想要調用的這個模塊,傳入module.exports ,那麼在每一個模塊中使用的module和module.exports就都是屬於這個模塊的了, 同時再傳入 __webpack_require__這樣我們就可以在每一個模塊中繼續使用了加載器了,最後,導出這個模塊。 調用完成之後,將l設置爲true,表示已經加載,最後導出module.exports,即導出加載到的模塊。
- 在自執行函數的末尾我們可以看到這個自執行函數最終返回了一個 __webpack_require__ 調用,也就是說返回了一個模塊,因爲__webpck_require__函數本身就會返回一個模塊。 並且這個 __webpack_require__調用接收的參數是一個 moduleId ,且指明瞭其值爲86。 也就是說入口文件的 moduleId 爲86, 我們來看一看模塊 86 的內容是什麼。即在這個bundle.js函數執行之後,實際上得到的第一部分內容是 86 模塊的內容。
/* 86 */ /***/ (function(module, exports, __webpack_require__) { module.exports = __webpack_require__(87); /***/ }),
模塊86非常簡單,就是首先通過 __webpack_require__(87) 引入了 moduleId 爲87的模塊, 然後我們看看87模塊是什麼。
/* 87 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var _react = __webpack_require__(9); var _react2 = _interopRequireDefault(_react); var _reactDom = __webpack_require__(103); var _reactDom2 = _interopRequireDefault(_reactDom); var _app = __webpack_require__(189); var _app2 = _interopRequireDefault(_app); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } _reactDom2.default.render(_react2.default.createElement(_app2.default, null), document.querySelector('#app')); /***/ }),
在這一部分的開頭,我們也看到了index.js的內容,主要任務就是引入了 react 、react-dom、引入了App組件、最後進行渲染。 同樣地,這裏我們可以看到,在這個模塊中,通過 __webpack_reuqire__(9) 引入了_react(這裏的react添加了下劃線,表示這裏的react是沒有對外暴露的), 然後使用_interopRequireDefault這個函數處理 --- 首先判斷引入的是否是一個對象並且同時滿足這個對象是否滿足es6中的module導出,如果滿足,就直接返回這個對象,如果不滿足, 就返回一個值爲obj的對象來進一步處理。 最後一步就是使用引入的各個方法來講 App 模塊掛載到 id爲app爲的元素下。 到這裏,可以看出引入了多個模塊,我們下面分別分析 __webpack_require__(9) 的react模塊以及__webpack_require__(189) 的 app 模塊,即一個是從外部定義的模塊,一個是我們自己寫的模塊。這兩個類型不同的模塊有了區分之後,我們就可以大致理清楚整個 bundle.js 的脈絡了。
__webpack_require__(9)
/* 9 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; module.exports = __webpack_require__(19); /***/ }),
進入了__webpack_require__(9)模塊我們看到,我們需要去尋找 19 模塊。 下面我們看看19模塊。
/* 19 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
// 這裏說明了react是從外部注入的。
/* WEBPACK VAR INJECTION */(function(process) {/**
// 下面的這幾行和我們直接打開react.js代碼的前幾行是一樣的,說明這些代碼確實是直接引入的。
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
var _assign = __webpack_require__(4);
var ReactBaseClasses = __webpack_require__(53);
var ReactChildren = __webpack_require__(88);
var ReactDOMFactories = __webpack_require__(92);
var ReactElement = __webpack_require__(15);
var ReactPropTypes = __webpack_require__(96);
var ReactVersion = __webpack_require__(99);
var createReactClass = __webpack_require__(100);
var onlyChild = __webpack_require__(102);
var createElement = ReactElement.createElement;
var createFactory = ReactElement.createFactory;
var cloneElement = ReactElement.cloneElement;
if (process.env.NODE_ENV !== 'production') {
var lowPriorityWarning = __webpack_require__(36);
var canDefineProperty = __webpack_require__(27);
var ReactElementValidator = __webpack_require__(57);
var didWarnPropTypesDeprecated = false;
createElement = ReactElementValidator.createElement;
createFactory = ReactElementValidator.createFactory;
cloneElement = ReactElementValidator.cloneElement;
}
var __spread = _assign;
var createMixin = function (mixin) {
return mixin;
};
if (process.env.NODE_ENV !== 'production') {
var warnedForSpread = false;
var warnedForCreateMixin = false;
__spread = function () {
lowPriorityWarning(warnedForSpread, 'React.__spread is deprecated and should not be used. Use ' + 'Object.assign directly or another helper function with similar ' + 'semantics. You may be seeing this warning due to your compiler. ' + 'See https://fb.me/react-spread-deprecation for more details.');
warnedForSpread = true;
return _assign.apply(null, arguments);
};
createMixin = function (mixin) {
lowPriorityWarning(warnedForCreateMixin, 'React.createMixin is deprecated and should not be used. ' + 'In React v16.0, it will be removed. ' + 'You can use this mixin directly instead. ' + 'See https://fb.me/createmixin-was-never-implemented for more info.');
warnedForCreateMixin = true;
return mixin;
};
}
var React = {
// Modern
Children: {
map: ReactChildren.map,
forEach: ReactChildren.forEach,
count: ReactChildren.count,
toArray: ReactChildren.toArray,
only: onlyChild
},
Component: ReactBaseClasses.Component,
PureComponent: ReactBaseClasses.PureComponent,
createElement: createElement,
cloneElement: cloneElement,
isValidElement: ReactElement.isValidElement,
// Classic
PropTypes: ReactPropTypes,
createClass: createReactClass,
createFactory: createFactory,
createMixin: createMixin,
// This looks DOM specific but these are actually isomorphic helpers
// since they are just generating DOM strings.
DOM: ReactDOMFactories,
version: ReactVersion,
// Deprecated hook for JSX spread, don't use this for anything.
__spread: __spread
};
if (process.env.NODE_ENV !== 'production') {
var warnedForCreateClass = false;
if (canDefineProperty) {
Object.defineProperty(React, 'PropTypes', {
get: function () {
lowPriorityWarning(didWarnPropTypesDeprecated, 'Accessing PropTypes via the main React package is deprecated,' + ' and will be removed in React v16.0.' + ' Use the latest available v15.* prop-types package from npm instead.' + ' For info on usage, compatibility, migration and more, see ' + 'https://fb.me/prop-types-docs');
didWarnPropTypesDeprecated = true;
return ReactPropTypes;
}
});
Object.defineProperty(React, 'createClass', {
get: function () {
lowPriorityWarning(warnedForCreateClass, 'Accessing createClass via the main React package is deprecated,' + ' and will be removed in React v16.0.' + " Use a plain JavaScript class instead. If you're not yet " + 'ready to migrate, create-react-class v15.* is available ' + 'on npm as a temporary, drop-in replacement. ' + 'For more info see https://fb.me/react-create-class');
warnedForCreateClass = true;
return createReactClass;
}
});
}
// React.DOM factories are deprecated. Wrap these methods so that
// invocations of the React.DOM namespace and alert users to switch
// to the `react-dom-factories` package.
React.DOM = {};
var warnedForFactories = false;
Object.keys(ReactDOMFactories).forEach(function (factory) {
React.DOM[factory] = function () {
if (!warnedForFactories) {
lowPriorityWarning(false, 'Accessing factories like React.DOM.%s has been deprecated ' + 'and will be removed in v16.0+. Use the ' + 'react-dom-factories package instead. ' + ' Version 1.0 provides a drop-in replacement.' + ' For more info, see https://fb.me/react-dom-factories', factory);
warnedForFactories = true;
}
return ReactDOMFactories[factory].apply(ReactDOMFactories, arguments);
};
});
}
module.exports = React;
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(0)))
/***/ }),
這就是react.js的核心代碼,但是爲什麼一共就100行左右的代碼呢? 這裏應該引入了整個 react 文件啊。 我們從內部代碼可以看到,在react模塊中同樣又使用了 __webpack_require__ 來引入了更多的文件, 這時因爲react.js本身就是這麼引入的文件的, https://unpkg.com/[email protected]/dist/react.js, 從源碼上可以看到, 它採用的也是分塊的模式,所以在webpack打包的時候,自然也是使用一個一個模塊的形式進行打包引入了。 這樣做的好處是什麼呢? 因爲這樣可以增加代碼的重用,就19模塊的 var ReactBaseClasses = __webpack_require__(53); 而言, 即react的 ReactBaseClasses 模塊需要使用,另外,在19模塊的createReactClass也是需要的,它先引入了100模塊,然後又引入了 19 模塊。 並且對於大型的框架、庫而言,都是需要按照模塊進行編寫的,不可能直接寫在一個模塊中。 react的19模塊就介紹到這裏。
下面我們再看看189的App模塊。(這個模塊是jsx文件,所以需要通過babel-loader進行轉譯)
/* 189 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _react = __webpack_require__(9); var _react2 = _interopRequireDefault(_react); var _title = __webpack_require__(35); var _title2 = _interopRequireDefault(_title); var _item = __webpack_require__(85); var _item2 = _interopRequireDefault(_item); var _experience = __webpack_require__(193); var _experience2 = _interopRequireDefault(_experience); var _skill = __webpack_require__(199); var _skill2 = _interopRequireDefault(_skill); var _personal = __webpack_require__(202); var _personal2 = _interopRequireDefault(_personal); var _intro = __webpack_require__(203); var _intro2 = _interopRequireDefault(_intro); var _others = __webpack_require__(207); var _others2 = _interopRequireDefault(_others); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } __webpack_require__(214); var App = function (_React$Component) { _inherits(App, _React$Component); function App() { _classCallCheck(this, App); return _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).apply(this, arguments)); } _createClass(App, [{ key: 'render', value: function render() { return _react2.default.createElement( 'div', { className: 'app-wrap' }, _react2.default.createElement( 'div', { className: 'sub' }, _react2.default.createElement( 'div', { className: 'intro' }, _react2.default.createElement(_intro2.default, null) ), _react2.default.createElement( 'div', { className: 'others' }, _react2.default.createElement(_others2.default, null) ) ), _react2.default.createElement( 'div', { className: 'main' }, _react2.default.createElement( 'div', { className: 'experience' }, _react2.default.createElement(_experience2.default, null) ), _react2.default.createElement( 'div', { className: 'skill' }, _react2.default.createElement(_skill2.default, null) ), _react2.default.createElement( 'div', { className: 'personal' }, _react2.default.createElement(_personal2.default, null) ) ) ); } }]); return App; }(_react2.default.Component); exports.default = App; /***/ }),
而下面是app.jsx 的源代碼:
import React from "react"; import Title from '../components/title.jsx' import Item2 from '../components/item2.jsx' import Experience from '../components/experience.jsx' import Skill from '../components/skill.jsx' import Personal from '../components/personal.jsx' import Intro from '../components/intro.jsx' import Others from '../components/others.jsx' require('../css/app.less') class App extends React.Component{ render () { return ( <div className='app-wrap'> <div className="sub"> <div className="intro"> <Intro/> </div> <div className="others"> <Others/> </div> </div> <div className="main"> <div className="experience"> <Experience/> </div> <div className="skill"> <Skill/> </div> <div className="personal"> <Personal/> </div> </div> </div> ) } } export default App;
在模塊的開始,我們就看到這個模塊的 _esModule 就被定義爲了 true,那麼代表這個模塊是符合 es6 的module規範的,這樣我們就可以直接導入導出了。
接下來,我們又看到了 var _react = __webpack_require__(9); 因爲我們在這個文件中引入了 react 模塊,但是在bundle.js最開始定義模塊的時候我們知道,只要加載了一次,這個模塊就會被放在 installedModules 對象中,這樣,我們就可以在第二次及以後使用的過程中,直接返回 installedModules 的這個模塊,而不需要重新加載了。
app模塊下的app.less
接着又引入了一些依賴和更底層的組件(不是隻嵌套組件的組件),比如,在 app.jsx 中我又引入了 app.less 這個less組件, 在模塊189中,我們可以看到確實有一個單獨引入的less組件, __webpack_require__(214); (稍後我們看看這個模塊)
最後開始創建app組件,最後返回這個組件。
模塊 214 (一個less模塊)
/* 214 */ /***/ (function(module, exports, __webpack_require__) { // style-loader: Adds some css to the DOM by adding a <style> tag // load the styles var content = __webpack_require__(215); if(typeof content === 'string') content = [[module.i, content, '']]; // Prepare cssTransformation var transform; var options = {} options.transform = transform // add the styles to the DOM var update = __webpack_require__(18)(content, options); if(content.locals) module.exports = content.locals; // Hot Module Replacement if(false) { // When the styles change, update the <style> tags if(!content.locals) { module.hot.accept("!!../node_modules/css-loader/index.js!../node_modules/less-loader/dist/cjs.js!./app.less", function() { var newContent = require("!!../node_modules/css-loader/index.js!../node_modules/less-loader/dist/cjs.js!./app.less"); if(typeof newContent === 'string') newContent = [[module.id, newContent, '']]; update(newContent); }); } // When the module is disposed, remove the <style> tags module.hot.dispose(function() { update(); }); } /***/ }),
在這個模塊中,我們可以看到這裏首先提到使用 style-loader 將css添加到html中。 接着開始加載 style ,即 215 模塊(css代碼),然後判斷 content 是否是一個字符串,如果是,就創建一個數組,包含這個字符串, 接下來, 使用熱更新機制。 這裏最重要的就是18模塊,將css代碼添加到html中,這個模塊中的的核心函數爲 addStylesToDom , 如下所示:
function addStylesToDom (styles, options) { for (var i = 0; i < styles.length; i++) { var item = styles[i]; var domStyle = stylesInDom[item.id]; if(domStyle) { domStyle.refs++; for(var j = 0; j < domStyle.parts.length; j++) { domStyle.parts[j](item.parts[j]); } for(; j < item.parts.length; j++) { domStyle.parts.push(addStyle(item.parts[j], options)); } } else { var parts = []; for(var j = 0; j < item.parts.length; j++) { parts.push(addStyle(item.parts[j], options)); } stylesInDom[item.id] = {id: item.id, refs: 1, parts: parts}; } } }
即接收兩個參數,第一個就是將要添加的style,第二個就是一些選項, 內部對所有的style進行遍歷, 然後添加進入。
我們可以看到215模塊如下所示:
/* 215 */ /***/ (function(module, exports, __webpack_require__) { exports = module.exports = __webpack_require__(17)(undefined); // imports // module exports.push([module.i, "div.app-wrap {\n width: 80%;\n margin: 0 auto;\n overflow: hidden;\n margin-top: 10px;\n border: thin solid #ccc;\n}\ndiv.app-wrap div.sub {\n box-shadow: 0 0 10px gray;\n float: left;\n width: 35%;\n}\ndiv.app-wrap div.sub div.intro {\n margin-bottom: 63px;\n}\ndiv.app-wrap div.main {\n float: right;\n width: 63%;\n margin-right: 5px;\n}\ndiv.app-wrap div.main div.skill {\n margin-bottom: 10px;\n}\n", ""]); // exports /***/ })
即這裏首先引入了 17 模塊, 17模塊的作用是通過css-loader注入基礎代碼(這個基礎css代碼是一個數組), 接着再push進入我寫的app.less代碼(注意:這裏的css代碼已經被less-loader轉化爲了css代碼), 然後進行注入的,最後是導出的這個css代碼。
app模塊下的introl.jsx模塊(203模塊)
這個模塊的jsx代碼如下:
import React from "react" require('../css/intro.less') import protrait from '../images/portrait.png' class Intro extends React.Component{ render () { return ( <div className='intro-wrap'> <div className="portrait"> <img src={protrait}/> </div> <div className="name">WayneZhu</div> <div className="position"> <span> 前端開發工程師 </span> </div> </div> ) } } export default Intro;
選用這個模塊的目的是因爲這裏有一個導入圖片的步驟,這樣,我們就可以觀察圖片的打包過程了。
下面是bundle.js中的該模塊:
/* 203 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _react = __webpack_require__(9); var _react2 = _interopRequireDefault(_react); var _portrait = __webpack_require__(204); var _portrait2 = _interopRequireDefault(_portrait); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } __webpack_require__(205); var Intro = function (_React$Component) { _inherits(Intro, _React$Component); function Intro() { _classCallCheck(this, Intro); return _possibleConstructorReturn(this, (Intro.__proto__ || Object.getPrototypeOf(Intro)).apply(this, arguments)); } _createClass(Intro, [{ key: 'render', value: function render() { return _react2.default.createElement( 'div', { className: 'intro-wrap' }, _react2.default.createElement( 'div', { className: 'portrait' }, _react2.default.createElement('img', { src: _portrait2.default }) ), _react2.default.createElement( 'div', { className: 'name' }, 'WayneZhu' ), _react2.default.createElement( 'div', { className: 'position' }, _react2.default.createElement( 'span', null, '\u524D\u7AEF\u5F00\u53D1\u5DE5\u7A0B\u5E08' ) ) ); } }]); return Intro; }(_react2.default.Component); exports.default = Intro; /***/ }),
在這個模塊中,我們可以看到webpack將圖片也當做了一個模塊204,然後引入了這個模塊,最後直接在 圖片的src中引用, 所以我們有必要看看 204 模塊的內容。
204模塊(png圖片)
這個模塊很簡單,就是將圖片進行了base64編碼,得到的結果如下所示:
1 2 3 4 5 |
|
這樣,就可以直接將這個編碼當做src,而不會發出請求來當做http請求了。
當然並不是所有的圖片都會被當做 模塊 進行打包, 我們完全可以去請求一個本地資源, 但是對於本地資源,我們需要提前進行設置, 一般,需要在node的服務器文件中添加下面的代碼:
// node你服務器使用的靜態文件 app.use('/', express.static('./www'))
這樣,我們就可以發現,在使用圖片時,可以直接是:
<img src='/images/add.png' className='create' onClick={this.createRoom}/>
即這裏 /images/add.png 默認是在 www 這個文件夾下的,因爲在node中,我們已經設置了靜態文件的位置了。 這樣,webpack 也只是引用,而不會將至轉化爲base64編碼:
_react2.default.createElement( "div", { className: "channel" }, _react2.default.createElement( "span", null, "\u6240\u6709\u623F\u95F4" ), _react2.default.createElement("img", { src: "/images/add.png", className: "create", onClick: this.createRoom }) ),
這樣,我們就可以發現: 這裏直接使用的就是路徑,引用 www 文件夾下的文件。 當然,我們也可以把www下的文件直接以模塊的形式打包進來。 但是,在使用靜態文件時,我們只能使用 www 下這個制定文件夾下的文件,而不能使用其他文件夾下的文件。
可以發現的是,在尋找文件的過程中,採用的是深度優先的遍歷原則。
ok! bundle.js 的內容到這裏大致就比較清楚了。下面,我們嘗試着實現一個簡單的webpack打包工具吧。
第四部分: 如何實現一個簡單的webpack打包工具?
前言:
一個webpack工具是需要很大的時間和精力來創造的,我們不可能實現所有的功能,這裏只是提供一個大體的思路,完成最簡單的功能,如實現使用符合commonjs規範的幾個文件打包爲一個文件。
當然,瀏覽器是沒有辦法執行commonjs規範的js文件的,所以,我們需要寫成自執行函數的形式,就像webpack打包出來的bundle.js一樣。
需求:
我們實現的需求就是一個入口文件example.js依賴於文件a、b、c,其中a和b是和example.js在同一目錄文件下的,而c是在node_modules中的, 我們要將這幾個模塊構建成一個js文件,輸入bundle.js。
- bundle.js 的頭部信息都是一致的,如都是一個自執行函數的定義,其中有一個核心函數 __webpack_require__ ,最終這個自執行函數返回的是入口文件的模塊。 然後依次向下執行。
- 需要分析出各個模塊之間的依賴關係,比如這裏的example.js是依賴於a、b、c的。
- 並且我們使用require('c')的時候,會自動導入node_modules中的相關文件,那麼這一定是有一個詳細的查詢機制的。
- 在生成的bundle.js文件中,每一個模塊都是具有一個唯一的模塊id的,引用時我們只需要引用這個id即可。
分析模塊依賴關係:
CommonJS不同於AMD,是不會在一開始聲明所有依賴的。CommonJS最顯著的特徵就是用到的時候再require
,所以我們得在整個文件的範圍內查找到底有多少個require
。
webpack是使用commonjs的規範來寫腳本的,但是對amd、cmd的書寫方式也支持的很好。 這裏簡單區分一下幾種模塊化的方法。 ADM/CMD是專門爲瀏覽器端的模塊化加載來制定的, 通常使用的方式就是define() 的方式,其中amd要求必須在文件的開頭聲明所有依賴的文件,而cmd則沒有這個要求,而是在使用的時候require即可, 即: amd是提前加載的,而cmd是在使用時再加載的,這是兩者的區別之一。Commonjs是服務器端node的書寫方式,如使用的時候require,而在導出的時候使用module.export,但是如今Commonjs規範已經不僅僅只適用於服務器端了,而是也適用於桌面端,但是隨着其使用越來越廣泛,名字由之前的severjs改爲了common.js。 而es6中的 export 和 import會在babel的編譯下編譯爲瀏覽器可以執行的方式。
怎麼辦呢?
最先蹦入腦海的思路是正則。然而,用正則來匹配require
,有以下兩個缺點:
- 如果
require
是寫在註釋中,也會匹配到。 - 如果後期要支持
require
的參數是表達式的情況,如require('a'+'b')
,正則很難處理。
因此,正則行不通。
一種正確的思路是:使用JS代碼解析工具(如esprima或者acorn),將JS代碼轉換成抽象語法樹(AST),再對AST進行遍歷。這部分的核心代碼是parse.js。
在處理好了require
的匹配之後,還有一個問題需要解決。那就是匹配到require
之後需要幹什麼呢?
舉個例子:
// example.js let a = require('a'); let b = require('b'); let c = require('c');
這裏有三個require
,按照CommonJS的規範,在檢測到第一個require
的時候,根據require即執行
的原則,程序應該立馬去讀取解析模塊a
。如果模塊a
中又require
了其他模塊,那麼繼續解析。也就是說,總體上遵循深度優先遍歷算法。這部分的控制邏輯寫在buildDeps.js中。
尋找模塊:
在完成依賴分析的同時,我們需要解決另外一個問題,那就是如何找到模塊?也就是模塊的尋址問題。
舉個例子:
// example.js let a = require('a'); let b = require('b'); let c = require('c');
在模塊example.js
中,調用模塊a、b、c
的方式都是一樣的。
但是,實際上他們所在的絕對路徑層級並不一致:a和b
跟example
同級,而c
位於與example
同級的node_modules
中。所以,程序需要有一個查找模塊的算法,這部分的邏輯在resolve.js中。
目前實現的查找邏輯是:
- 如果給出的是絕對路徑/相對路徑,只查找一次。找到?返回絕對路徑。找不到?返回false。
- 如果給出的是模塊的名字,先在入口js(example.js)文件所在目錄下尋找同名JS文件(可省略擴展名)。找到?返回絕對路徑。找不到?走第3步。
- 在入口js(example.js)同級的node_modules文件夾(如果存在的話)查找。找到?返回絕對路徑。找不到?返回false。
當然,此處實現的算法還比較簡陋,之後有時間可以再考慮實現逐層往上的查找,就像nodejs默認的模塊查找算法那樣。
拼接 bundle.js :
這是最後一步了。
在解決了模塊依賴
和模塊查找
的問題之後,我們將會得到一個依賴關係對象depTree
,此對象完整地描述了以下信息:都有哪些模塊,各個模塊的內容是什麼,他們之間的依賴關係又是如何等等。具體的結構如下
{ "modules": { "/Users/youngwind/www/fake-webpack/examples/simple/example.js": { "id": 0, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/example.js", "name": "/Users/youngwind/www/fake-webpack/examples/simple/example.js", "requires": [ { "name": "a", "nameRange": [ 16, 19 ], "id": 1 }, { "name": "b", "nameRange": [ 38, 41 ], "id": 2 }, { "name": "c", "nameRange": [ 60, 63 ], "id": 3 } ], "source": "let a = require('a');\nlet b = require('b');\nlet c = require('c');\na();\nb();\nc();\n" }, "/Users/youngwind/www/fake-webpack/examples/simple/a.js": { "id": 1, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/a.js", "name": "a", "requires": [], "source": "// module a\n\nmodule.exports = function () {\n console.log('a')\n};" }, "/Users/youngwind/www/fake-webpack/examples/simple/b.js": { "id": 2, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/b.js", "name": "b", "requires": [], "source": "// module b\n\nmodule.exports = function () {\n console.log('b')\n};" }, "/Users/youngwind/www/fake-webpack/examples/simple/node_modules/c.js": { "id": 3, "filename": "/Users/youngwind/www/fake-webpack/examples/simple/node_modules/c.js", "name": "c", "requires": [], "source": "module.exports = function () {\n console.log('c')\n}" } }, "mapModuleNameToId": { "/Users/youngwind/www/fake-webpack/examples/simple/example.js": 0, "a": 1, "b": 2, "c": 3 } }
打包優化
使用了react全家桶之後,打包出的bundle.js是非常大的, 所以對之進行優化是十分有必要的。
(1)、使用壓縮插件,如下:
在webpack.config.js中進行配置下面的代碼:
plugins: [ new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) ]
這樣打包出來的文件可以從5M減少到1.7左右。
(2)、開發過程中使用 webpack-dev-server.
我們當然可以每次使用打包出來的文件,但是更好的做法是將不把文件打包出來,然後從硬盤中獲取,而是直接打包到內存中(即webapck-dev-server的作用),這樣,我們就可以直接從內存中獲取了,好處就是速度很快。 顯然內存的讀取速度是大於硬盤的。