在做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文件,找到這個文件,我們可以看到主要的代碼如下:
// 引入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風格的異步回調 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, /******/ l: false, /******/ 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; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __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 */ /***/ function(module, exports) { console.log('index'); /***/ },
/* 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/react@15.6.1/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編碼,得到的結果如下所示:
/* 204 */
/***/ (function(module, exports) {
// 下面的編碼內容省略了大部分
module.exports = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYUAAAFRCAYAAACbjLFxAAACC"
/***/ }),
這樣,就可以直接將這個編碼當做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的作用),這樣,我們就可以直接從內存中獲取了,好處就是速度很快。 顯然內存的讀取速度是大於硬盤的。
