通用、封裝、簡化 webpack 配置

senntyou發表於2019-02-16

通用、封裝、簡化 webpack 配置

現在,基本上前端的專案打包都會用上 webpack,因為 webpack 提供了無與倫比強大的功能和生態。但在建立一個專案的時候,總是免不了要配置 webpack,很是麻煩。

簡化 webpack 配置的一種方式是使用社群封裝好的庫,比如 roadhogroadhog 封裝了 webpack 的一些基礎配置,然後暴露一些額外配置的介面,並附加本地資料模擬功能(mock),詳情可以參考 roadhog 主頁

另一種方式是自己封裝 webpack,這樣做自己能夠更好的掌控專案。

1. 要封裝哪些功能

一般搭建一個專案至少需要兩種功能:本地開發除錯、構建產品程式碼。

其他的諸如測試、部署到伺服器、程式碼檢查、格式優化等功能則不在這篇文章講解範圍,如果有意瞭解,可以檢視我的其他文章。

2. 基礎配置

2.1 目錄結構(示例,配合後面的程式碼講解)

package.json
dev.js                       # 本地開發指令碼
build.js                     # 產品構建指令碼
analyze.js                   # 模組大小分析(可選)

# 單頁面結構
src/                         # 原始碼目錄
  - index.js                 # js 入口檔案
  - index.html               # html 入口檔案
  - ...                      # 其他檔案

# 多頁面結構
src/                         # 原始碼目錄
  - home/                    # home 頁面工作空間
    - index.js               # home 頁面 js 入口檔案
    - index.html             # home 頁面 html 入口檔案
    - ...                    # home 頁面其他檔案
    
  - explore/                 # explore 頁面工作空間
    - index.js               # explore 頁面 js 入口檔案
    - index.html             # explore 頁面 html 入口檔案
    - ...                    # explore 頁面其他檔案 
     
  - about/                   # about 目錄
    - company                # about/company 頁面工作空間
      - index.js             # about/company 頁面 js 入口檔案
      - index.html           # about/company 頁面 html 入口檔案
      - ...                  # about/company 頁面其他檔案
      
    - platform               # about/platform 頁面工作空間
      - index.js             # about/platform 頁面 js 入口檔案
      - index.html           # about/platform 頁面 html 入口檔案
      - ...                  # about/platform 頁面其他檔案    
  
  - ...                      # 更多頁面        

2.2 基礎 npm 包

# package.json

"devDependencies": {
  "@babel/core": "^7.1.2",                  # babel core       
  "@babel/plugin-syntax-dynamic-import": "^7.0.0",         # import() 函式支援
  "@babel/plugin-transform-react-jsx": "^7.0.0",           # react jsx 支援
  "@babel/preset-env": "^7.1.0",            # es6+ 轉 es5
  "@babel/preset-flow": "^7.0.0",           # flow 支援
  "@babel/preset-react": "^7.0.0",          # react 支援
  "autoprefixer": "^9.1.5",                 # css 自動新增廠家字首 -webkit-, -moz-
  "babel-loader": "^8.0.4",                 # webpack 載入 js 的 loader
  "babel-plugin-component": "^1.1.1",       # 如果使用 element ui,需要用到這個
  "babel-plugin-flow-runtime": "^0.17.0",   # flow-runtime 支援 
  "babel-plugin-import": "^1.9.1",          # 如果使用 ant-design,需要用到這個
  "browser-sync": "^2.24.7",                # 瀏覽器例項元件,用於本地開發除錯
  "css-loader": "^1.0.0",                   # webpack 載入 css 的 loader
  "chalk": "^2.4.1",                        # 讓命令列的資訊有顏色
  "file-loader": "^2.0.0",                  # webpack 載入靜態檔案的 loader
  "flow-runtime": "^0.17.0",                # flow-runtime 包
  "html-loader": "^0.5.5",                  # webpack 載入 html 的 loader
  "html-webpack-include-assets-plugin": "^1.0.5",          # 給 html 檔案新增額外靜態檔案連結的外掛
  "html-webpack-plugin": "^3.2.0",          # 更方便操作 html 檔案的外掛
  "less": "^3.8.1",                         # less 轉 css
  "less-loader": "^4.1.0",                  # webpack 載入 less 的 loader
  "mini-css-extract-plugin": "^0.4.3",      # 提取 css 單獨打包
  "minimist": "^1.2.0",                     # process.argv 更便捷處理
  "node-sass": "^4.9.3",                    # scss 轉 css
  "optimize-css-assets-webpack-plugin": "^5.0.1",          # 優化 css 打包,包括壓縮
  "postcss-loader": "^3.0.0",               # 對 css 進行更多操作,比如新增廠家字首
  "sass-loader": "^7.1.0",                  # webpack 載入 scss 的 loader
  "style-loader": "^0.23.0",                # webpack 載入 style 的 loader
  "uglifyjs-webpack-plugin": "^2.0.1",      # 壓縮 js 的外掛
  "url-loader": "^1.1.1",                   # file-loader 的升級版
  "vue-loader": "^15.4.2",                  # webpack 載入 vue 的 loader
  "vue-template-compiler": "^2.5.17",       # 配合 vue-loader 使用的 
  "webpack": "^4.20.2",                     # webpack 模組
  "webpack-bundle-analyzer": "^3.0.2",      # 分析當前打包各個模組的大小,決定哪些需要單獨打包
  "webpack-dev-middleware": "^3.4.0",       # webpack-dev-server 中介軟體
  "webpack-hot-middleware": "^2.24.2"       # 熱更新中介軟體
}

2.3 基本命令

# package.json

"scripts": {
  "dev": "node dev.js",
  "build": "node build.js",
  "analyze": "node analyze.js",
}
npm run dev                                 # 開發
npm run build                               # 構建
npm run analyze                             # 模組分析

如果需要支援多入口構建,在命令後面新增引數:

npm run dev -- home                         # 開發 home 頁面 
npm run analyze -- explore                  # 模組分析 explore 頁面 

# 構建多個頁面 
npm run build -- home explore about/* about/all --env test/prod  
  • home, explore 確定構建的頁面;about/*, about/allabout 目錄下所有的頁面;all, * 整個專案所有的頁面
  • 有時候可能還會針對不同的伺服器環境(比如測試機、正式機)做出不同的構建,可以在後面加引數
  • -- 用來分割 npm 本身的引數與指令碼引數,參考 npm - run-script 瞭解詳情

2.4 dev.js 配置

開發一般用需要用到下面的元件:

const minimist = require('minimist');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const devMiddleWare = require('webpack-dev-middleware');
const hotMiddleWare = require('webpack-hot-middleware');
const browserSync = require('browser-sync');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

const { HotModuleReplacementPlugin } = webpack;

const argv = minimist(process.argv.slice(2));

const page = argv._[0];

 // 單頁面
const entryFile = `${__dirname}/src/index.js`;
// 多頁面 
const entryFile = `${__dirname}/src/${page}/index.js`; 

// 編譯器物件
const compiler = webpack({
  entry: [
    'webpack-hot-middleware/client?reload=true',      // 熱過載需要
    entryFile,
  ],
  output: {
    path: `${__dirname}/dev/`,                        // 打包到 dev 目錄
    filename: 'index.js',
    publicPath: '/dev/',
  },
  plugins: [
    new HotModuleReplacementPlugin(),                 // 熱過載外掛
    new HtmlWebpackPlugin({                           // 處理 html
      // 單頁面
      template: `${__dirname}/src/index.html`,
      // 多頁面
      template: `${__dirname}/src/${page}/index.html`,
    }),
    new VueLoaderPlugin(),                            // vue-loader 所需
  ],
  module: {
    rules: [
      {                                               // js 檔案載入器
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-env', '@babel/preset-react'],
          plugins: [
            '@babel/plugin-transform-react-jsx',
            '@babel/plugin-syntax-dynamic-import',
          ],
        },
        test: /\.(js|jsx)$/,
      },
      {                                               // css 檔案載入器
        loader: 'style-loader!css-loader',
        test: /\.css$/,
      },
      {                                               // less 檔案載入器
        loader: 'style-loader!css-loader!less-loader',
        test: /\.less$/,
      },
      {                                               // scss 檔案載入器
        loader: 'style-loader!css-loader!sass-loader',
        test: /\.(scss|sass)$/,
      },
      {                                               // 靜態檔案載入器
        loader: 'url-loader',
        test: /\.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,
        options: {
          limit: 1,
        },
      },
      {                                               // html 檔案載入器
        loader: 'html-loader',
        test: /\.html$/,
        options: {
          attrs: ['img:src', 'link:href'],
          interpolate: 'require',
        },
      },
      {                                               // vue 檔案載入器
        loader: 'vue-loader',
        test: /\.vue$/,
      },
    ],
  },
  resolve: {
    alias: {},                                        // js 配置別名   
    modules: [`${__dirname}/src`, 'node_modules'],    // 模組定址基路徑
    extensions: ['.js', '.jsx', '.vue', '.json'],     // 模組定址副檔名
  },
  devtool: 'eval-source-map',                         // sourcemap 
  mode: 'development',                                // 指定 webpack 為開發模式
});

// browser-sync 配置
const browserSyncConfig = {
  server: {
    baseDir: `${__dirname}/`,                         // 靜態伺服器基路徑,可以訪問專案所有檔案 
  },
  startPath: '/dev/index.html',                       // 開啟伺服器視窗時的預設地址
};

// 新增中介軟體
browserSyncConfig.middleware = [
  devMiddleWare(compiler, {
    stats: 'errors-only',
    publicPath: '/dev/',
  }),
  hotMiddleWare(compiler),
];

browserSync.init(browserSyncConfig);                  // 初始化瀏覽器例項,開始除錯開發

2.5 build.js 配置

構建過程中,一般會有這些過程:

  1. 提取樣式檔案,單獨打包、壓縮、新增瀏覽器廠家字首
  2. js 在產品模式下進行打包,並生成 sourcemap 檔案
  3. html-webpack-plugin 自動把打包好的樣式檔案與指令碼檔案引用到 html 檔案中,並壓縮
  4. 對所有資源進行 hash 化處理(可選)
const minimist = require('minimist');
const webpack = require('webpack');
const chalk = require('chalk');
const autoprefixer = require('autoprefixer');
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 {yellow, red} = chalk;

const argv = minimist(process.argv.slice(2));

const pages = argv._; // ['home', 'explore', 'about/*', 'about/all']
const allPages = getAllPages(pages); // 根據 page 中的 `*, all` 等關鍵字,獲取所有真正的 pages

// 單頁面,只有一個入口,所以只有一個配置檔案
const config = { ... }; 

// 多頁面,多個入口,所有有多個配置檔案
const configs = allPages.map(page => ({
  // 單頁面
  entry: `${__dirname}/src/index.js`,                 // js 入口檔案
  // 多頁面
  entry: `${__dirname}/src/${page}/index.js`,         // js 入口檔案
  output: {
    path: `${__dirname}/dist/`,                       // 輸出路徑
    filename: '[chunkhash].js',                       // 輸出檔名,這裡完全取 hash 值來命名
    hashDigestLength: 32,                             // hash 值長度
    publicPath: '/dist/',
  },
  plugins: [
    new MiniCssExtractPlugin({                        // 提取所有的樣式檔案,單獨打包
      filename: '[chunkhash].css',                    // 輸出檔名,這裡完全取 hash 值來命名
    }),
    new HtmlWebpackPlugin({
      // 單頁面
      template: `${__dirname}/src/index.html`,        // html 入口檔案
      // 多頁面
      template: `${__dirname}/src/${page}/index.html`,// html 入口檔案
      minify: {                                       // 指定如果壓縮 html 檔案
        removeComments: !0,
        collapseWhitespace: !0,
        collapseBooleanAttributes: !0,
        removeEmptyAttributes: !0,
        removeScriptTypeAttributes: !0,
        removeStyleLinkTypeAttributes: !0,
        minifyJS: !0,
        minifyCSS: !0,
      },
    }),
    new VueLoaderPlugin(),                            // vue-loader 所需
    new OptimizeCssAssetsPlugin({                     // 壓縮 css
      cssProcessorPluginOptions: {
        preset: ['default', { discardComments: { removeAll: true } }],
      },
    }),
    
    // webpack 打包的 js 檔案是預設壓縮的,所以這裡不需要再額外新增 uglifyjs-webpack-plugin
  ],
  module: {
    rules: [
      {                                               // js 檔案載入器,與 dev 一致
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-env', '@babel/preset-react'],
          plugins: [
            '@babel/plugin-transform-react-jsx',
            '@babel/plugin-syntax-dynamic-import',
          ],
        },
        test: /\.(js|jsx)$/,
      },
      {                                               // css 檔案載入器,新增了瀏覽器廠家字首
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: [
                autoprefixer({
                  browsers: [
                    '> 1%',
                    'last 2 versions',
                    'Android >= 3.2',
                    'Firefox >= 20',
                    'iOS 7',
                  ],
                }),
              ],
            },
          },
        ],
        test: /\.css$/,
      },
      {                                               // less 檔案載入器,新增了瀏覽器廠家字首
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: [
                autoprefixer({
                  browsers: [
                    '> 1%',
                    'last 2 versions',
                    'Android >= 3.2',
                    'Firefox >= 20',
                    'iOS 7',
                  ],
                }),
              ],
            },
          },
          'less-loader',
        ],
        test: /\.less$/,
      },
      {                                               // scss 檔案載入器,新增了瀏覽器廠家字首
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: [
                autoprefixer({
                  browsers: [
                    '> 1%',
                    'last 2 versions',
                    'Android >= 3.2',
                    'Firefox >= 20',
                    'iOS 7',
                  ],
                }),
              ],
            },
          },
          'sass-loader',
        ],
        test: /\.(scss|sass)$/,
      },
      {                                               // 靜態檔案載入器,與 dev 一致
        loader: 'url-loader',
        test: /\.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,
        options: {
          limit: 1,
        },
      },
      {                                               // html 檔案載入器,與 dev 一致
        loader: 'html-loader',
        test: /\.html$/,
        options: {
          attrs: ['img:src', 'link:href'],
          interpolate: 'require',
        },
      },
      {                                               // vue 檔案載入器,與 dev 一致
        loader: 'vue-loader',
        test: /\.vue$/,
      },
    ],
  },
  resolve: {
    alias: {},                                        // js 配置別名   
    modules: [`${__dirname}/src`, 'node_modules'],    // 模組定址基路徑
    extensions: ['.js', '.jsx', '.vue', '.json'],     // 模組定址副檔名
  },
  devtool: 'source-map',                              // sourcemap 
  mode: 'production',                                 // 指定 webpack 為產品模式
}));

// 執行一次 webpack 構建
const run = (config, cb) => {
  webpack(config, (err, stats) => {
    if (err) {
      console.error(red(err.stack || err));
      if (err.details) {
        console.error(red(err.details));
      }
      process.exit(1);
    }
  
    const info = stats.toJson();
  
    if (stats.hasErrors()) {
      info.errors.forEach(error => {
        console.error(red(error));
      });
      process.exit(1);
    }
  
    if (stats.hasWarnings()) {
      info.warnings.forEach(warning => {
        console.warn(yellow(warning));
      });
    }
    
    // 如果是多頁面,需要把 index.html => `${page}.html`
    // 因為每個頁面匯出的 html 檔案都是 index.html 如果不重新命名,會被覆蓋掉
    
    if(cb) cb();
  });
};

// 單頁面
run(config);

// 多頁面
let index = 0;
// go on
const goon = () => {
  run(configs[index], () => {
    index += 1;

    if (index < configs.length) goon();
  });
};

goon();

2.6 analyze.js 配置

圖片描述

const minimist = require('minimist');
const chalk = require('chalk');
const webpack = require('webpack');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

const {yellow, red} = chalk;

const argv = minimist(process.argv.slice(2));

const page = argv._[0];

 // 單頁面
const entryFile = `${__dirname}/src/index.js`;
// 多頁面 
const entryFile = `${__dirname}/src/${page}/index.js`; 

const config = {
  entry: entryFile,
  output: {
    path: `${__dirname}/analyze/`,                    // 打包到 analyze 目錄
    filename: 'index.js',
  },
  plugins: [
    new VueLoaderPlugin(),                            // vue-loader 所需
    new BundleAnalyzerPlugin(),                       // 新增外掛 
  ],
  module: {
    rules: [
      {                                               // js 檔案載入器
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-env', '@babel/preset-react'],
          plugins: [
            '@babel/plugin-transform-react-jsx',
            '@babel/plugin-syntax-dynamic-import',
          ],
        },
        test: /\.(js|jsx)$/,
      },
      {                                               // css 檔案載入器
        loader: 'style-loader!css-loader',
        test: /\.css$/,
      },
      {                                               // less 檔案載入器
        loader: 'style-loader!css-loader!less-loader',
        test: /\.less$/,
      },
      {                                               // scss 檔案載入器
        loader: 'style-loader!css-loader!sass-loader',
        test: /\.(scss|sass)$/,
      },
      {                                               // 靜態檔案載入器
        loader: 'url-loader',
        test: /\.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,
        options: {
          limit: 1,
        },
      },
      {                                               // html 檔案載入器
        loader: 'html-loader',
        test: /\.html$/,
        options: {
          attrs: ['img:src', 'link:href'],
          interpolate: 'require',
        },
      },
      {                                               // vue 檔案載入器
        loader: 'vue-loader',
        test: /\.vue$/,
      },
    ],
  },
  resolve: {
    alias: {},                                        // js 配置別名   
    modules: [`${__dirname}/src`, 'node_modules'],    // 模組定址基路徑
    extensions: ['.js', '.jsx', '.vue', '.json'],     // 模組定址副檔名
  },
  mode: 'production',                                 // 指定 webpack 為產品模式
};

webpack(config, (err, stats) => {
  if (err) {
    console.error(red(err.stack || err));
    if (err.details) {
      console.error(red(err.details));
    }
    process.exit(1);
  }

  const info = stats.toJson();

  if (stats.hasErrors()) {
    info.errors.forEach(error => {
      console.error(red(error));
    });
    process.exit(1);
  }

  if (stats.hasWarnings()) {
    info.warnings.forEach(warning => {
      console.warn(yellow(warning));
    });
  }
});

2.7 擴充套件配置

你可以根據需要擴充套件配置,比如新增外掛、載入器等,比如:

3. 封裝

上面的程式碼可以封裝成一個全域性命令,比如 lila,執行上面的命令就可以更簡潔:

lila dev home                               # 開發 home 頁面 
lila analyze explore                        # 模組分析 explore 頁面 

# 構建多個頁面 
lila build home explore about/* about/all --env test/prod

後續

更多部落格,檢視 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證

相關文章