基於webpack4.x專案實戰2 - 配置一次,多個專案執行
不久前,寫過一篇webpack4的簡單實踐,
今天我們繼續來webpack4.x的實戰第二部分,只需要配置一次,就可以多個專案一起使用。
使用場景:
- 非外包專案,因為外包專案一般只有一個產品
- 我們的專案都使用vue或者react,統一一個框架,本文基於vue
- 我們不想每次開發一個專案都複製貼上一個webpack配置,而且希望只配置一次,每個專案都可以通用
- 我們可以引用公共專案的程式碼,所有專案共享。
- 可以自定義個別專案的webpack配置,靈活配置
一些目錄結構
在這裡,我們有一個約定:
- 單頁面,頁面名稱都為index.html
- 入口檔案都為該專案src下的main.js
- 一些靜態檔案,也就是我們不想打包的檔案,如一些配置等,放在和index.html同級目錄下的static目錄中
- 打包後的檔案放在dist目錄下
我們的目錄結構如下:
--_webpack --lib/cmd.js --dev-server.js --webpack.base.conf.js --webpack.dev.conf.js --webpack.prod.conf.js --common --demo1 --src --main.js --index.html --demo2 複製程式碼
_webpack common demo1/demo2
開發環境:
我們執行npm run dev --dirname=產品名
,
如npm run dev --dirname=demo
來進行開發,如需指定埠,可以為npm run dev --dirname=demo1 --port=8080
生產環境:
我們執行npm run build --dirname=產品名
,
如npm run build --dirname=demo1
來進行打包編譯
程式碼解析
命令指令碼_webpack/lib/cmd.js
'use strict'; const program = require('commander'); const fs = require('fs'); const path = require('path'); let argv; try { // 通過 npm run dev 的方法執行的時候,引數更換獲取方式 argv = JSON.parse(process.env.npm_config_argv).original; }catch (e) { argv = process.argv; } program .version('0.1.0') .option('-d, --dirname <dirname>', '編譯目錄') .option('-p, --port <n>', '埠號') .parse(argv); const dirname = program.dirname; if(!fs.existsSync(path.resolve(__dirname, `../../${dirname}`))) { throw `${dirname}專案不存在` } module.exports = program; 複製程式碼
我們引入了commander
這個庫來接收一些命令,比如產品名--dirname=xxx
(也可以為-d xxx
)、埠號--port=xxx
,這樣,我們就可以通過在命令列接受我們的一些定製的命令了,如果沒有找到你輸入的產品名,直接丟擲異常。
注意點:如果想通過在package.json中設定來接受我們的命令,則下面這一段是必須的
try { // 通過 npm run dev 的方法執行的時候,引數更換獲取方式 argv = JSON.parse(process.env.npm_config_argv).original; }catch (e) { argv = process.argv; } 複製程式碼
這樣,我們就可以通過npm run dev --dirname=xxx --port=xxx
來執行我們的命令了
基礎webpack配置_webpack/webpack.base.conf.js
- 入口檔案
... let entryFilePath = path.resolve(cwd, `${dirname}/src/main.js`); if (!fs.existsSync(entryFilePath)) { entryFilePath = path.resolve(cwd, 'common/src/main.js'); } return { entry: { lib: ['vue', 'vuex'], main: ['webpack-hot-middleware/client?noInfo=true&reload=true', entryFilePath] }, // 入口檔案 output: { filename: 'js/[name].js',// 打包後的檔名稱 path: path.resolve(cwd, `${dirname}/dist`)// 打包後的目錄 } ... } 複製程式碼
我們將vue、vuex這些單獨打包,入口檔案為我們的main.js
,如果專案下沒有這個檔案,則去尋找common
下的main.js
,打包後為lib.js
和main.js
,放在dist目錄下。
- 一些loader
const fs = require('fs'); const path = require('path'); const webpack = require('webpack') const htmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const VueLoaderPlugin = require('vue-loader/lib/plugin'); const devMode = process.env.NODE_ENV == 'development'; // 是否是開發環境 ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.(le|c)ss$/, use: [ devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader' ], }, { test: /\.(png|svg|jpg|gif)$/, // 載入圖片 use: [{ loader: 'url-loader', options: { limit: 8192,// 小於8k的圖片自動轉成base64格式 name: 'images/[name].[ext]?[hash]', // 圖片打包後的目錄 publicPath: '../'// css圖片引用地址 }, }] }, { test: /\.(woff|woff2|eot|ttf|otf)$/, // 載入字型檔案 use: [ 'file-loader' ] }, // 轉義es6 { test: /\.js$/, loader: 'babel-loader', include: /src/,// 只轉化src目錄下的js exclude: /node_modules/, // 忽略掉node_modules下的js } ] }, ... 複製程式碼
-
支援vue
由於專案基於Vue,所以需要
vue-loader
-
支援CSS
-
加入
css-loader
、less-loader
(如果你們專案是用scss,也可以引入scss-loader
) -
支援自動加css3字首
引入了
postcss-loader
,需要和autoprefixer
一起使用。在根目錄下新建./postcss.config.js
檔案,裡面的內容為 -
module.exports = { plugins: [ require('autoprefixer') ] } 複製程式碼
package.json中需要這樣寫
"browserslist": [ "> 1%", // 值越小,支援的瀏覽器返回更廣 "last 2 versions", "not ie <= 8" ], 複製程式碼
-
支援圖片
引入
url-loader
來載入圖片,打包後的圖片放在images資料夾中,在引用圖片時,自動加入hash值 -
支援es6
引入
babel-loader
-
CSS優化
引入
mini-css-extract-plugin
這個外掛對生產環境的CSS進行優化,這裡需要注意的是,webpack4.x建議用mini-css-extract-plugin
替換extract-text-webpack-plugin
- resolve
resolve: { // 建立import別名 alias: { $common: path.resolve(cwd, 'common/src'), $components: path.resolve(cwd, `${dirname}/src/components`), 'vue$': 'vue/dist/vue.esm.js', }, extensions: ['.js', '.json'], // 忽略檔案字尾 modules: [ // 引入模組的話,先從node_modules中查詢,其次是當前產品的src下,最後是common的src下 path.resolve(cwd, 'node_modules'), path.resolve(cwd, `${dirname}/src`), path.resolve(cwd, 'common/src') ] }, 複製程式碼
-
resolve中,我們建立了一些別名。支援Vue必須引入的
vue$
。省略掉js和json的字尾等等 -
modules這裡就比較好玩了
如果我們在程式碼中import dialog from utils/dialog.vue
,它會先去node_modules下查詢,如果沒找到,則去當前專案下的src查詢,如果還是沒有,則去common下的src去查詢,這樣有什麼好處呢?
如果我們的common目錄下有一個dialog.vue檔案,如:common/src/utils/dialog.vue
在我們的專案中,如:專案A,引用這個dialog.vue檔案,是可以直接import dialog from utils/dialog.vue
這樣引入的,即使我們的專案A裡面沒有utils/dialog.vue
這個檔案。
--common --src -- utils/dialog.vue --projectA --src -- utils/dialog.vue --main.js // import dialog from utils/dialog.vue來自於自己目錄下 --projectB --src --main.js // import dialog from utils/dialog.vue 來自於common目錄下 複製程式碼
這樣,當我們的專案A、專案B中存在很多的公用程式碼,可以把公共程式碼放在common中,專案A或B中,只要寫少許程式碼,就可以完成一個專案,如果專案A中的dialog.vue比較特殊,則在專案A中新建同目錄下的dialog.vue檔案,即可覆蓋掉common的檔案,這樣import dialog from utils/dialog.vue
就來自於專案A, 而不是common了。從而達到,即可通用,又可定製的效果。如果專案A中的dialog.vue檔案,只有一點點和common下的不同,則在dialog.vue中,繼承於common即可
- 外掛
... plugins: [ new VueLoaderPlugin(), new htmlWebpackPlugin({ template: path.resolve(cwd, `${dirname}/index.html`), filename: "index.html", inject: true, hash: true, minify: { removeComments: devMode ? false : true, // 刪除html中的註釋程式碼 collapseWhitespace: devMode ? false : true, // 刪除html中的空白符 removeAttributeQuotes: devMode ? false : true // 刪除html元素中屬性的引號 }, chunksSortMode: 'dependency' // 按dependency的順序引入 }), new MiniCssExtractPlugin({ filename: 'css/[name].css', chunkFilename: '[id].css' }), // 優化css new OptimizeCssAssetsPlugin({ ssetNameRegExp: /\.css\.*(?!.*map)/g, cssProcessor: require('cssnano'), // 引入cssnano配置壓縮選項 cssProcessorOptions: { // 用postcss新增字首,這裡關掉 autoprefixer: { disable: true }, discardComments: {// 移除註釋 removeAll: true } }, canPrint: true // 是否將外掛資訊列印到控制檯 }), // 頁面不用每次都引入這些變數 new webpack.ProvidePlugin({ Vue: ['vue', 'default'], Vuex: ['vuex', 'default'] }) ] ... 複製程式碼
比較常用的一些外掛,壓縮css、js、html自動引入css、js等。上面就是我們webpack的基礎配置。
開發環境配置_webpack/webpack.dev.conf.js
'use strict' const fs = require('fs'); const path = require('path'); const merge = require('webpack-merge'); const webpack = require('webpack'); module.exports = (cwd, dirname = null, outputPath = null) => { let baseWebpackConfig = require('./webpack.base.conf')(cwd, dirname, outputPath); return merge(baseWebpackConfig, { mode: 'development', devtool: '#cheap-module-eval-source-map', plugins: [ new webpack.HotModuleReplacementPlugin(), // 用於熱載入 ] }); } 複製程式碼
生產環境配置_webpack/webpack.prod.conf.js
'use strict' const path = require('path'); const webpack = require('webpack'); const merge = require('webpack-merge'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin') const fs = require('fs'); module.exports = (cwd, dirname = null, outputPath = null) => { let baseWebpackConfig = require('./webpack.base.conf')(cwd, dirname, outputPath); return merge(baseWebpackConfig, { devtool: false, mode: 'production', optimization: { minimize: true }, plugins: [ new CleanWebpackPlugin({ default: ['dist'], verbose: true, dry: false }), // 複製靜態資源,將static檔案內的內容複製到指定資料夾 new CopyWebpackPlugin([ { from: path.resolve(cwd, `${dirname}/static`), to: path.resolve(cwd, `${dirname}/dist/static`), ignore: ['.*'] // 忽視.*檔案 } ]), ] }); } 複製程式碼
主要是壓縮js、複製一些靜態檔案等等
開發環境服務
開發環境服務_webpack/dev-server.js
我們用expess來做伺服器,如果你不想用公共的webpack.dev.conf.js
,也可以在你的專案下新建webpack.dev.conf.js
來自定義單獨專案下的配置
'use strict' const path = require('path'); const commander = require('./lib/cmd'); const dirname = commander.dirname; const port = commander.port || 3002; // 埠號 const cwd = path.resolve(__dirname, '../'); let devWebpackConf = require('./webpack.dev.conf.js'); let localWebpackConf = path.resolve(cwd, `${dirname}/webpack.dev.conf.js`); if (fs.existsSync(localWebpackConf)) { // 如果專案下有webpack.dev.conf,則使用該配置,覆蓋掉公有的配置 devWebpackConf = require(localWebpackConf); } const webpackConfig = devWebpackConf(cwd, dirname, null); // webpack的配置 ... // 設定一些靜態資源 const staticPath = path.resolve(cwd, `${dirname}/static`); app.use('/static', express.static(staticPath)); ... 複製程式碼
一些產品名,基本路徑都是從這裡傳入你的webpack配置裡面去。具體內容可以看dev-server.js
編譯服務
生產環境服務_webpack/build.js
'use strict' const ora = require('ora'); // 終端顯示的轉輪loading const rm = require('rimraf'); const path = require('path'); const chalk = require('chalk'); const commander = require('./lib/cmd'); const product = commander.dirname; const cwd = path.resolve(__dirname, '../'); const webpack = require('webpack'); let proWebpackConf = require('./webpack.pro.conf.js'); let localWebpackConf = path.resolve(cwd, `${dirname}/webpack.pro.conf.js`); if (fs.existsSync(localWebpackConf)) { // 如果專案下有webpack.dev.conf,則使用該配置,覆蓋掉公有的配置 proWebpackConf = require(localWebpackConf); } const webpackConfig = proWebpackConf(cwd, dirname, null); // webpack的配置 const spinner = ora('building for production...') spinner.start() // 刪除已編譯檔案 rm(path.resolve(cwd, `${product}/dist`), err => { if (err) throw err // 在刪除完成的回撥函式中開始編譯 webpack(webpackConfig, function (err, stats) { spinner.stop() // 停止loading if (err) throw err process.stdout.write(stats.toString({ colors: true, modules: false, children: false, chunks: false, chunkModules: false }) + '\n\n') if (stats.hasErrors()) { console.log(chalk.red('Build failed with errors.\n')); process.exit(1); } console.log(chalk.cyan('Build complete.\n')); }) }) 複製程式碼
如果不想用公用編譯,同理,也可以在獨自的專案下新建webpack.pro.conf.js
來覆蓋掉公共的編譯配置
結尾
程式碼地址:github.com/xianyulaodi…
程式碼中有兩個demo:
demo1的webpack配置是定製的,它的入口可以為index.js,demo1也舉例了引用common檔案的情況
demo2基於vue,支援開發環境引入mock,打包後mock移除
這樣,我們就完成了我們的webpack配置,只需要配置一次,多個專案公用一套配置,如果common目錄下有的元件,單獨專案下可以直接使用,支援單獨專案覆蓋掉公用元件以及覆蓋掉公用webpack配置,做到既公用,又解耦。尤其適用於專案A、B、C只有少部分功能有差異的情況。