基於webpack4.x項目實戰2 - 配置一次,多個項目運行

基於webpack4.x項目實戰1-簡單使用javascript

基於webpack4.x項目實戰2 - 配置一次,多個項目運行css

基於webpack4.x項目實戰3 - 手寫一個clihtml



不久前,寫過一篇webpack4的簡單實踐,vue

今天咱們繼續來webpack4.x的實戰第二部分,只須要配置一次,就能夠多個項目一塊兒使用。java

使用場景:

  1. 非外包項目,由於外包項目通常只有一個產品
  2. 咱們的項目都使用vue或者react,統一一個框架,本文基於vue
  3. 咱們不想每次開發一個項目都複製粘貼一個webpack配置,並且但願只配置一次,每一個項目均可以通用
  4. 咱們能夠引用公共項目的代碼,全部項目共享。
  5. 能夠自定義個別項目的webpack配置,靈活配置

一些目錄結構

在這裏,咱們有一個約定:node

  1. 單頁面,頁面名稱都爲index.html
  2. 入口文件都爲該項目src下的main.js
  3. 一些靜態文件,也就是咱們不想打包的文件,如一些配置等,放在和index.html同級目錄下的static目錄中
  4. 打包後的文件放在dist目錄下

咱們的目錄結構以下:react

--_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
複製代碼
  1. _webpack:用來存放webpack、node的一些配置文件,主要用來打包編譯
  2. common:用來存放咱們的一些公共文件,好比一些經常使用的工具腳本,經常使用組件等等
  3. demo1/demo2:就是咱們開發項目的目錄

開發環境:webpack

咱們執行npm run dev --dirname=產品名css3

npm run dev --dirname=demo來進行開發,如需指定端口,能夠爲npm run dev --dirname=demo1 --port=8080git

生產環境:

咱們執行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.jsmain.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
        }
    ]
},
...
複製代碼
  1. 支持vue

    因爲項目基於Vue,因此須要vue-loader

  2. 支持CSS

    1. 加入css-loaderless-loader(若是大家項目是用scss,也能夠引入scss-loader)

    2. 支持自動加css3前綴

    引入了postcss-loader,須要和autoprefixer一塊兒使用。在根目錄下新建./postcss.config.js文件,裏面的內容爲

module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}
複製代碼

package.json中須要這樣寫

"browserslist": [
    "> 1%", // 值越小,支持的瀏覽器返回更廣
    "last 2 versions",
    "not ie <= 8"
],
複製代碼
  1. 支持圖片

    引入url-loader來加載圖片,打包後的圖片放在images文件夾中,在引用圖片時,自動加入hash值

  2. 支持es6

    引入babel-loader

  3. 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只有少部分功能有差別的狀況。

相關文章
相關標籤/搜索