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留言交流哈!!!