在知乎上我們常常會看到有同學發問:BAT 等大型網站的前端工程是如何組織管理的?這的確是一個可以發散的很廣的 Q&A,我想如果要我回答這個問題,不如先從 Webpack 配置說起。
時至今日,Webpack 已經成為前端工程必備的基礎工具之一,不僅被廣泛用於前端工程釋出前的打包,還在開發中擔當本地前端資源伺服器(assets server)、模組熱更新(hot module replacement)、API Proxy 等角色,結合 ESLint 等程式碼檢查工具,還可以實現在對原始碼的嚴格校驗檢查。
正如上文中提到的,前端從開發到部署前都離不開 Webpack 的參與,而 Webpack 的預設配置檔案只有一個,即 webpack.config.js,那麼問題來了,開發期和部署前應該使用同一份 Webpack 配置嗎?答案肯定是否定的,既然 webpack.config.js 是一個 JS 檔案,我們當然可以在檔案裡寫 JavaScript 業務邏輯,通過讀取環境變數 NODE_ENV 來判斷當前是在開發(dev)時還是最終的生產環境(production),然而很多同學習慣把這兩者的配置都混寫在根目錄下的 webpack.config.js,通過很多零散的 if…else 來“臨時”決定某一個 plugin 或者某一個 loader 的配置項,隨著 loaders 和 plugins 的不斷增加,久而久之 webpack.config.js 變得原來越隆長,程式碼的可讀性和可維護性也大大下降。
我想通過本文來介紹一種用 3 個 JS 檔案來配置 Webpack 的方法,這裡借鑑了很多開源專案的配置,同時也結合了我們自己在開發中碰到的種種問題解決方案。
本文中提及的配置基於 Webpack 2 或以上,建議使用 3.0 及以上版本
開發環境與生產環境的區別
開發環境
- NODE_ENV 為 development
- 啟用模組熱更新(hot module replacement)
- 額外的 webpack-dev-server 配置項,API Proxy 配置項
- 輸出 Sourcemap
生產環境
- NODE_ENV 為 production
- 將 React、jQuery 等常用庫設定為 external,直接採用 CDN 線上的版本
- 樣式原始檔(如 css、less、scss 等)需要通過 ExtractTextPlugin 獨立抽取成 css 檔案
- 啟用 post-css
- 啟用 optimize-minimize(如 uglify 等)
- 中大型的商業網站生產環境下,是絕對不能有 console.log() 的,所以要為 babel 配置 Remove console transform
這裡需要說明的是因為開發環境下啟用了 hot module replacement,為了讓樣式原始檔的修改也同樣能被熱替換,不能使用 ExtractTextPlugin,而轉為隨 JS Bundle 一起輸出。
你需要三份配置檔案
1. webpack.base.config.js
在 base 檔案裡,你需要將開發環境和生產環境中通用的配置集中放在這裡:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
const CleanWebpackPlugin = require('clean-webpack-plugin'); const path = require('path'); const webpack = require('webpack'); // 配置常量 // 原始碼的根目錄(本地物理檔案路徑) const SRC_PATH = path.resolve('./src'); // 打包後的資源根目錄(本地物理檔案路徑) const ASSETS_BUILD_PATH = path.resolve('./build'); // 資源根目錄(可以是 CDN 上的絕對路徑,或相對路徑) const ASSETS_PUBLIC_PATH = '/assets/'; module.exports = { context: SRC_PATH, // 設定原始碼的預設根路徑 resolve: { extensions: ['.js', '.jsx'] // 同時支援 js 和 jsx }, entry: { // 注意 entry 中的路徑都是相對於 SRC_PATH 的路徑 vendor: './vendor', a: ['./entry-a'], b: ['./entry-b'], c: ['./entry-c'] }, output: { path: ASSETS_BUILD_PATH, publicPath: ASSETS_PUBLIC_PATH, filename: './[name].js' }, module: { rules: [ { enforce: 'pre', // ESLint 優先順序高於其他 JS 相關的 loader test: /\.jsx?$/, exclude: /node_modules/, loader: 'eslint-loader' }, { test: /\.jsx?$/, exclude: /node_modules/, // 建議把 babel 的執行時配置放在 .babelrc 裡,從而與 eslint-loader 等共享配置 loader: 'babel-loader' }, { test: /\.(png|jpg|gif)$/, use: [ { loader: 'url-loader', options: { limit: 8192, name: 'images/[name].[ext]' } } ] }, { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: [ { loader: 'url-loader', options: { limit: 8192, mimetype: 'application/font-woff', name: 'fonts/[name].[ext]' } } ] }, { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: [ { loader: 'file-loader', options: { limit: 8192, mimetype: 'application/font-woff', name: 'fonts/[name].[ext]' } } ] } ] }, plugins: [ // 每次打包前,先清空原來目錄中的內容 new CleanWebpackPlugin([ASSETS_BUILD_PATH], { verbose: false }), // 啟用 CommonChunkPlugin new webpack.optimize.CommonsChunkPlugin({ names: 'vendor', minChunks: Infinity }) ] }; |
2. webpack.dev.config.js
這是用於開發環境的 Webpack 配置,繼承自 base:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
const webpack = require('webpack'); // 讀取同一目錄下的 base config const config = require('./webpack.base.config'); // 新增 webpack-dev-server 相關的配置項 config.devServer = { contentBase: './', hot: true, publicPath: '/assets/' }; // 有關 Webpack 的 API 本地代理,另請參考 https://webpack.github.io/docs/webpack-dev-server.html#proxy config.module.rules.push( { test: /\.less$/, use: [ 'style-loader', 'css-loader', 'less-loader' ], exclude: /node_modules/ } ); // 真實場景中,React、jQuery 等優先走全站的 CDN,所以要放在 externals 中 config.externals = { react: 'React', 'react-dom': 'ReactDOM' }; // 新增 Sourcemap 支援 config.plugins.push( new webpack.SourceMapDevToolPlugin({ filename: '[file].map', exclude: ['vendor.js'] // vendor 通常不需要 sourcemap }) ); // Hot module replacement Object.keys(config.entry).forEach((key) => { // 這裡有一個私有的約定,如果 entry 是一個陣列,則證明它需要被 hot module replace if (Array.isArray(config.entry[key])) { config.entry[key].unshift( 'webpack-dev-server/client?http://0.0.0.0:8080', 'webpack/hot/only-dev-server' ); } }); config.plugins.push( new webpack.HotModuleReplacementPlugin() ); module.exports = config; |
3. webpack.config.js
這是用於生產環境的 webpack 配置,同樣繼承自 base:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
const webpack = require('webpack'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); // 讀取同一目錄下的 base config const config = require('./webpack.base.config'); config.module.rules.push( { test: /\.less$/, use: ExtractTextPlugin.extract( { use: [ 'css-loader', 'less-loader' ], fallback: 'style-loader' } ), exclude: /node_modules/ } ); config.plugins.push( // 官方文件推薦使用下面的外掛確保 NODE_ENV new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), // 啟動 minify new webpack.LoaderOptionsPlugin({ minimize: true }), // 抽取 CSS 檔案 new ExtractTextPlugin({ filename: '[name].css', allChunks: true, ignoreOrder: true }) ); module.exports = config; |
現在在你的工程資料夾裡應該已經有三個 Webpack 配置檔案,它們分別是:
- webpack.base.config.js
- webpack.dev.config.js
- webpack.config.js
最後,你還需要在 package.json 裡新增相應的配置:
1 2 3 4 5 6 7 8 9 |
{ ... "scripts": { "build": "webpack --optimize-minimize", "dev": "webpack-dev-server --config webpack.dev.config.js", "start": "npm run dev" // 或新增你自己的 start 邏輯 }, ... } |
和很多專案一樣,在開發環境下的時候,你需要使用 npm run dev 來啟動,而在生產環境中,則用 npm run build 來發布。
題外話,在真實場景中,我們不會直接使用 webpack-dev-server,而採用 express + webpack/webpack-dev-middleware,配置方法與上面所述的完全相同。