在作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的做用),这样,咱们就能够直接从内存中获取了,好处就是速度很快。 显然内存的读取速度是大于硬盘的。