在作vue項目和react項目時,都用到了webpack。webpack幫助咱們很好地提升了工做效率,可是一直以來沒有對其原理進行探究,略有遺憾。 由於使用一個工具,可以深刻了解其原理才能更好地使用。 這篇文章將大體分爲三個部分進行解讀:php
當一個項目使用webpack打包時,webpack會認爲全部的文件都是模塊,並將其打包到一個文件中。 可是webpack只能識別js文件,因此對於其餘文件,咱們須要使用loader來完成打包。 css
經過webpack打包,咱們能很好地解決前端項目中的依賴問題,這樣能夠幫助咱們專一於實現項目的代碼邏輯,而非是依賴、命名衝突等。html
通常狀況下,咱們都會在根目錄下配置一個 webpack.config.js 文件,用於配置webpack打包。 當咱們打開控制檯時,輸入webpack, 就會根據配置文件對項目進行打包了。可是,在這個過程當中究竟發生了什麼呢? 前端
當在cmd中輸入一個命令執行時,實際上執行的都是一個相似於可執行的二進制文件,好比執行node命令、ping命令時都是這樣的, 在項目的node_modules下的webpack根目錄下找到package.json, 能夠看到下面的一個kv:vue
"bin": { "webpack": "./bin/webpack.js" },
這就說明在執行二進制文件時,會運行 ./bin/webpack.js文件,找到這個文件,咱們能夠看到主要的代碼以下:node
// 引入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函數,咱們來看看源碼。 react
// 創建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 等調用和綁定插件的方法jquery
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;
注意: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 編譯對象進行逐一編譯,接下來咱們再來仔細看看這個模塊。git
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 命令,按下回車時都發生了什麼:
Compiler
,繼承 Tapable 插件框架,實現註冊和調用一系列插件。/WebpackOptionsApply.js
模塊的 process
方法,使用各類各樣的插件來逐一編譯 webpack 編譯對象的各項。
// 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;
/***/ }),
// 省略若干。。。。
/******/ ]);
/* 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 */ /***/ (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.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代碼。
這個模塊的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工具是須要很大的時間和精力來創造的,咱們不可能實現全部的功能,這裏只是提供一個大致的思路,完成最簡單的功能,如實現使用符合commonjs規範的幾個文件打包爲一個文件。
固然,瀏覽器是沒有辦法執行commonjs規範的js文件的,因此,咱們須要寫成自執行函數的形式,就像webpack打包出來的bundle.js同樣。
需求:
咱們實現的需求就是一個入口文件example.js依賴於文件a、b、c,其中a和b是和example.js在同一目錄文件下的,而c是在node_modules中的, 咱們要將這幾個模塊構建成一個js文件,輸入bundle.js。
分析模塊依賴關係:
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中。
目前實現的查找邏輯是:
固然,此處實現的算法還比較簡陋,以後有時間能夠再考慮實現逐層往上的查找,就像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的做用),這樣,咱們就能夠直接從內存中獲取了,好處就是速度很快。 顯然內存的讀取速度是大於硬盤的。