webpack
是當下前端界中最著名的一個模塊加載工具,react
和vue
也都是用其做爲項目的開發工具之一。小組最近在二次開發一個開源項目,前端主要使用的技術棧試react+redux+es6
。構建工具則採用的是webpack
。起初整個項目的2707 modules
打包花費時長大概有112s
,通過對一番折騰,使整個打包編譯時間降到40s
左右。javascript
下面是整個項目的webpack.config.js
文件,能夠參考這個文件進行下面的閱讀。css
require("babel-register"); require("babel-polyfill"); var webpack = require('webpack'); var webpackPostcssTools = require('webpack-postcss-tools'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); var UnusedFilesWebpackPlugin = require("unused-files-webpack-plugin").default; var BannerWebpackPlugin = require('banner-webpack-plugin'); var AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); var HappyPack = require('happypack'); var ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin'); var _ = require('underscore'); var glob = require('glob'); var fs = require('fs'); var chevrotain = require("chevrotain"); var allTokens = require("./frontend/src/metabase/lib/expressions/tokens").allTokens; function hasArg(arg) { var regex = new RegExp("^" + ((arg.length === 2) ? ("-\\w*"+arg[1]+"\\w*") : (arg)) + "$"); return process.argv.filter(regex.test.bind(regex)).length > 0; } var SRC_PATH = __dirname + '/frontend/src/metabase'; var BUILD_PATH = __dirname + '/resources/frontend_client'; // default NODE_ENV to development var NODE_ENV = process.env["NODE_ENV"] || "development"; var IS_WATCHING = hasArg("-w") || hasArg("--watch"); if (IS_WATCHING) { process.stderr.write("Warning: in webpack watch mode you must restart webpack if you change any CSS variables or custom media queries\n"); } // Babel: var BABEL_CONFIG = { cacheDirectory: ".babel_cache" }; // Build mapping of CSS variables var CSS_SRC = glob.sync(SRC_PATH + '/css/**/*.css'); var CSS_MAPS = { vars: {}, media: {}, selector: {} }; CSS_SRC.map(webpackPostcssTools.makeVarMap).forEach(function(map) { for (var name in CSS_MAPS) _.extend(CSS_MAPS[name], map[name]); }); // CSS Next: var CSSNEXT_CONFIG = { features: { // pass in the variables and custom media we scanned for before customProperties: { variables: CSS_MAPS.vars }, customMedia: { extensions: CSS_MAPS.media } }, import: { path: ['resources/frontend_client/app/css'] }, compress: false }; var CSS_CONFIG = { localIdentName: NODE_ENV !== "production" ? "[name]__[local]___[hash:base64:5]" : "[hash:base64:5]", restructuring: false, compatibility: true, url: false, // disabled because we need to use relative url() importLoaders: 1 } // happypack.config var happyPackConfig = { plugins:[ new HappyPack({ id: 'happyBabel', threads: 4, cache: true, loaders:[ { path: 'babel', query: BABEL_CONFIG } ] }), new HappyPack({ id: 'happyEslint', threads: 4, cache: true, loaders: ['eslint'] }) ] } var config = module.exports = { context: SRC_PATH, entry: { "app-main": './app-main.js', "app-public": './app-public.js', "app-embed": './app-embed.js', styles: './css/index.css', }, // output to "dist" output: { path: BUILD_PATH + '/app/dist', filename: '[name].bundle.js?[hash]', publicPath: 'app/dist/' }, module: { loaders: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'HappyPack/loader?id=happyBabel' }, { test: /\.(js|jsx)$/, exclude: /node_modules|\.spec\.js/, loader: 'HappyPack/loader?id=happyEslint' }, { test: /\.(eot|woff2?|ttf|svg|png)$/, loader: "file-loader" }, { test: /\.json$/, loader: "json-loader" }, { test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader?" + JSON.stringify(CSS_CONFIG) + "!postcss-loader") } ] }, resolve: { extensions: ["", ".webpack.js", ".web.js", ".js", ".jsx", ".css"], alias: { 'metabase': SRC_PATH, 'style': SRC_PATH + '/css/core/index.css', 'ace': __dirname + '/node_modules/ace-builds/src-min-noconflict', } }, plugins: [ new UnusedFilesWebpackPlugin({ globOptions: { ignore: [ "**/types/*.js", "**/*.spec.*", "**/__support__/*.js" ] } }), new webpack.DllReferencePlugin({ context: __dirname, manifest: require('./manifest.json'), name:"vendors_dll" }), // Extracts initial CSS into a standard stylesheet that can be loaded in parallel with JavaScript // NOTE: the filename on disk won't include "?[chunkhash]" but the URL in index.html generated by HtmlWebpackPlugin will: new ExtractTextPlugin('[name].bundle.css?[contenthash]'), new HtmlWebpackPlugin({ filename: '../../index.html', chunks: ["app-main", "styles"], template: __dirname + '/resources/frontend_client/index_template.html', inject: 'head', alwaysWriteToDisk: true, }), new HtmlWebpackPlugin({ filename: '../../public.html', chunks: ["app-public", "styles"], template: __dirname + '/resources/frontend_client/index_template.html', inject: 'head', alwaysWriteToDisk: true, }), new HtmlWebpackPlugin({ filename: '../../embed.html', chunks: ["app-embed", "styles"], template: __dirname + '/resources/frontend_client/index_template.html', inject: 'head', alwaysWriteToDisk: true, }), new AddAssetHtmlPlugin({ filepath: BUILD_PATH + '/app/dist/*.dll.js', includeSourcemap: false }), new HtmlWebpackHarddiskPlugin({ outputPath: __dirname + '/resources/frontend_client/app/dist' }), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify(NODE_ENV) } }), new BannerWebpackPlugin({ chunks: { 'app-main': { beforeContent: "/*\n* This file is subject to the terms and conditions defined in\n * file 'LICENSE.txt', which is part of this source code package.\n */\n", }, 'app-public': { beforeContent: "/*\n* This file is subject to the terms and conditions defined in\n * file 'LICENSE.txt', which is part of this source code package.\n */\n", }, 'app-embed': { beforeContent: "/*\n* This file is subject to the terms and conditions defined in\n * file 'LICENSE-EMBEDDING.txt', which is part of this source code package.\n */\n", }, } }), ].concat(happyPackConfig.plugins), postcss: function (webpack) { return [ require("postcss-import")(), require("postcss-url")(), require("postcss-cssnext")(CSSNEXT_CONFIG) ] } }; if (NODE_ENV === "hot") { // suffixing with ".hot" allows us to run both `yarn run build-hot` and `yarn run test` or `yarn run test-watch` simultaneously config.output.filename = "[name].hot.bundle.js?[hash]"; // point the publicPath (inlined in index.html by HtmlWebpackPlugin) to the hot-reloading server config.output.publicPath = "http://localhost:8080/" + config.output.publicPath; config.module.loaders.unshift({ test: /\.jsx$/, exclude: /node_modules/, loaders: ['react-hot', 'babel?'+JSON.stringify(BABEL_CONFIG)] }); // disable ExtractTextPlugin config.module.loaders[config.module.loaders.length - 1].loader = "style-loader!css-loader?" + JSON.stringify(CSS_CONFIG) + "!postcss-loader" config.devServer = { hot: true, inline: true, contentBase: "frontend" }; config.plugins.unshift( new webpack.NoErrorsPlugin(), new webpack.HotModuleReplacementPlugin() ); } if (NODE_ENV !== "production") { // replace minified files with un-minified versions for (var name in config.resolve.alias) { var minified = config.resolve.alias[name]; var unminified = minified.replace(/[.-\/]min\b/g, ''); if (minified !== unminified && fs.existsSync(unminified)) { config.resolve.alias[name] = unminified; } } // enable "cheap" source maps in hot or watch mode since re-build speed overhead is < 1 second config.devtool = "cheap-module-source-map"; config.output.devtoolModuleFilenameTemplate = '[absolute-resource-path]'; config.output.pathinfo = true; } else { config.plugins.push(new ParallelUglifyPlugin({ uglifyJs:{ compress: { warnings: false, }, output: { comments: false, }, mangle: { except: allTokens.map(function(currTok) { return chevrotain.tokenName(currTok); }) } }, cacheDir: '.js-cache' })) config.devtool = "source-map"; }
webpack
編譯緩慢一直是現代化前端開發的一個痛點。社區中不少優秀的開發者都貢獻出很是多的插件來視圖解決這個問題。下面就將本文中用到的插件拋出,在下面這幾個插件的配合下,編譯速度會獲得顯著的提高。html
happypack
: 讓loader
以多進程去處理文件,藉助緩存機制,能夠在rebuild
的時候更快webpack.DllPlugin
: 優先構建npm
的第三方包webpack.DllReferencePlugin
: 只負責用來引用由webpack.DllPlugin
生成的第三方依賴項webpack-parallel-uglify-plugin
: 並行壓縮javascript
文件(生產環境中使用,能夠顯著的提高構建速度)下面就對這些插件以及我踩下的坑進行一個簡單的介紹。前端
happypack
https://github.com/amireh/happypack
happypack
容許webpack
並行編譯多個文件來提高構建速度。可是在某些狀況下,其提高的效果並非十分明顯,這個時候就須要看一下本身電腦的cpu
佔用率,以及進程的運行狀況。vue
happypack
做爲webpack
的一個插件,因此在使用以前應該先安裝。java
yarn add happywebpack -D
配置過程很簡單,只須要在plugins
選項中建立其實例,能夠建立一個或多個,而後在loader
中引用便可。只須要注意一點,當建立多個happypack
的實例的時候,給每一個實例傳遞一個id
參數。基本的變更以下:node
原配置文件react
// 省略了部分的配置文件 var config = module.exports = { //................ module: { loaders: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel', query: BABEL_CONFIG }, { test: /\.(js|jsx)$/, exclude: /node_modules|\.spec\.js/, loader: 'eslint' }, { test: /\.(eot|woff2?|ttf|svg|png)$/, loader: "file-loader" }, { test: /\.json$/, loader: "json-loader" }, { test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader?" + JSON.stringify(CSS_CONFIG) + "!postcss-loader") } ] } //............... }
改動以下webpack
// happypack.config:更多的配置能夠參考文檔,按需索取。 var happyPackConfig = { plugins:[ new HappyPack({ id: 'happyBabel', threads: 4, cache: true, loaders:[ { path: 'babel', query: BABEL_CONFIG } ] }), new HappyPack({ id: 'happyEslint', threads: 4, cache: true, loaders: ['eslint'] }) ] } var config = module.exports = { //................ module: { loaders: [ // 變更這兩個 { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'HappyPack/loader?id=happyBabel' }, { test: /\.(js|jsx)$/, exclude: /node_modules|\.spec\.js/, loader: 'HappyPack/loader?id=happyEslint' }, // 其它的並未改動 { test: /\.(eot|woff2?|ttf|svg|png)$/, loader: "file-loader" }, { test: /\.json$/, loader: "json-loader" }, { test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader?" + JSON.stringify(CSS_CONFIG) + "!postcss-loader") } ] } //............... } // 在module.loader中引用
而後,當咱們運行:git
yarn run build
就會看到以下輸出:
大概意思就是,happupack
的版本是3.1.0
,對babel-loader
開啓了四個線程並從緩存中加載了627
個模塊。
webpack.DllPlugin
和webpack.DllReferencePlugin
這兩個插件在使用的時候,仍是有幾個小坑的,下面就會爲你們講述幾個。
先說一下基本的用法,官方推薦在使用的時候,咱們須要寫兩個webpack
配置文件。其中一個配置文件主要用於webpack.DllPlugin
插件進行第三方的預打包,另外一個則是主webpack
配置文件,在其中使用webpack.DllReferencePlugin
插件引用第三方生成的依賴模塊。
因此,咱們其中一個配置文件能夠命名以下:ddl.config.js
const webpack = require('webpack') const vendors = Object.keys(require('package.json')['dependencies']) const SRC_PATH = __dirname + '/frontend/src/metabase' const BUILD_PATH = __dirname + '/resources/frontend_client' module.exports = { output: { path: BUILD_PATH + '/app/dist', filename: '[name].dll.js', library: '[name]_dll', }, entry: { // 第三方依賴設置爲打包的入口 vendors: vendors, }, plugins: [ new webpack.DllPlugin({ path: 'manifest.json', name: '[name]_dll', context: __dirname, }), ], }
接下來,在咱們進行webpack
的正式打包以前能夠先來一個預打包,運行以下命令:
webpack --config ddl.donfig.js
命令結束以後,咱們能夠在BUILD_PATH
下面生成了一個vendors.dll.js
(具體的名稱根據你的配置而來)以及根目錄下面的manifset.json
文件。打開這個文件,能夠看到webpack.DllPlugin
插件爲每一個第三方包都生成了一個惟一的全局id。
上面的這個插件的配置有幾個須要注意的地方,output.library
屬性是必須的,同時webpack.DllPlugin
參數對象的name
屬性和其保持一致。更詳細的配置能夠參考文檔。
預打包以後,咱們須要對咱們的主webpack.config.js
文件作以下改動。
//.......................... plugins:[ // ........ new webpack.DllReferencePlugin({ context: __dirname, manifest: require('./manifest.json'), // 上述生成的文件的名稱 name:"vendors_dll" }), //......... ] //..........................
配置很簡單,詳細的配置小夥伴能夠參考文檔按需索取。這裏有幾個須要注意的地方給你們說明一下。
vendors.dll.js
文件必定要在引入咱們的html
文件中,並且在引入模塊文件以前引入,不然你會看到這個錯誤。
(騷年,有沒有以爲菊花一緊)
html-webpack-plugin
來動態建立咱們的html
模板,這個時候咱們怎麼把生成的vendors.dll.js
引入到咱們的頁面中呢?路徑能夠寫死,可是你試試,反正我遇到了這個錯誤。若是你的能夠,歡迎在github
上留言交流。
。這個插件的主要做用就是將咱們本身的靜態文件插入到模版生成的html
文件中。因此須要對webpack.config.js
做出以下的改動。
var AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); //.......................... plugins:[ // ........ new AddAssetHtmlPlugin({ filepath: BUILD_PATH + '/app/dist/*.dll.js', includeSourcemap: false }), //......... ] //..........................
includeSourcemap
選項若是不配置的話,可能會遇到vendors.dll.js.map cannot found
的錯誤
而後,運行,bingo。至此,打包時間已經從100s
左右降到了35s
左右。恭喜恭喜。
webpack-parallel-uglify-plugin
https://github.com/gdborton/webpack-parallel-uglify-plugin
這個插件的用處十分的強大,並行壓縮javascript
,配置也十分簡單,參考官方文檔就能知道怎麼使用,如咱們的配置文件就作了以下的變更。
原js文件
config.plugins.push(new webpack.optimize.UglifyJsPlugin({ // suppress uglify warnings in production // output from these warnings was causing Heroku builds to fail (#5410) compress: { warnings: false, }, output: { comments: false, }, mangle: { // this is required to ensure we don't minify Chevrotain token identifiers // https://github.com/SAP/chevrotain/tree/master/examples/parser/minification except: allTokens.map(function(currTok) { return chevrotain.tokenName(currTok); }) } }))
變更後
config.plugins.push(new ParallelUglifyPlugin({ uglifyJs:{ compress: { warnings: false, }, output: { comments: false, }, mangle: { // this is required to ensure we don't minify Chevrotain token identifiers // https://github.com/SAP/chevrotain/tree/master/examples/parser/minification except: allTokens.map(function(currTok) { return chevrotain.tokenName(currTok); }) } }, cacheDir: '.js-cache' }))
至此,咱們大部分的優化的內容已經完成,下面是咱們打包時間的一個對比。
優化前打包時間
優化後打包時間
除了上述的幾個能夠優化的地方,還有不少一些小點能夠進行優化,好比:
若是你有好的優化點,歡迎在個人github留言交流哈!!!