記一次React項目Webpack4配置過程

引言

最近新項目過多,在新項目中每次使用 webpack 都是拷貝以前的項目的配置文件過來,改改直接使用,不少配置仍是隻知其一;不知其二,一直想用心的從頭配置一次 webpack,加深對 webpack 的理解,因此,有了本文,先獻上如下內容github地址javascript

基本配置 webpack.base.config.js

首先,配置entrycss

const base = {
  entry: ['./src/index']
}
複製代碼

自 webpack4 起,webpack 提供了默認 entry,也就是咱們上面使用的 './src/index',這裏咱們用數組包裹一下,方便動態增刪,往下html

配置 output:java

const base = {
  entry: ['./src/index'],
  output: {
    publicPath: '/', // 項目根目錄
    path: path.resolve(__dirname, './dist'),
    chunkFilename: '[name].[chunkhash].chunk.js'
  }
}
複製代碼

配置 resolve.extensions, require的時候省略文件後綴node

const base = {
  resolve: {
    extensions: [".js", ".json"],
  }
}
複製代碼

配置 devServer,開發環境 webpack-dev-server 配置使用react

const host = 'localhost';
const port = 8080;

const base = {
  devServer: {
    contentBase: [path.join(process.cwd(), './vendor-dev/'), path.join(process.cwd(), './vendor/')], // dllPlugin使用,下文有講
    hot: true,  // 熱加載
    compress: false,
    open: true,  // 
    host: host,
    port: port,
    disableHostCheck: true, // 跳過host檢測
    stats: { colors: true },
    filename: '[name].chunk.js',
    headers: { 'Access-Control-Allow-Origin': '*' }
  }
}
複製代碼

根據不一樣的環境,咱們須要對默認的 entry 進行處理,以下webpack

const CleanWebpackPlugin = require('clean-webpack-plugin');
const isDebug = process.env.NODE_ENV !== 'production';

if (isDebug) {
  base.entry.unshift(`webpack-dev-server/client?http://${host}:${port}`, 'webpack/hot/dev-server'); // 添加devServer入口
  base.plugins.unshift(new webpack.HotModuleReplacementPlugin()); // 添加熱加載
  base.devtool = 'source-map';
} else {
  base.entry.unshift('babel-polyfill');  // 加入 polyfill
  base.plugins.push(new CleanWebpackPlugin(   // 清理目標目錄文件
    "*",
    {
      root: base.output.path,                      //根目錄
      verbose: true,                  //開啓在控制檯輸出信息
      dry: false                  //啓用刪除文件
    }
  ))
}
複製代碼

添加圖片、字體文件處理:git

const base = {
  module: {
    rules: [{
      test: /\.(woff|woff2|ttf|eot|png|jpg|jpeg|gif|svg)(\?v=\d+\.\d+\.\d+)?$/i, // 圖片加載
      loader: 'url-loader',
      query: {
        limit: 10000
      }
    }]
  }
}
複製代碼

production 生成環境對編譯進行 optimizationgithub

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

const base = {
  optimization: {
    minimize: !isDebug, // 是否壓縮
    minimizer: !isDebug ? [new UglifyJsPlugin({
      cache: true,  // 使用緩存
      parallel: true,  // 多線程並行處理
      sourceMap: true,  // 使用sourceMap
      uglifyOptions: {
        comments: false,
        warnings: false,
        compress: {
          unused: true,
          dead_code: true,
          collapse_vars: true,
          reduce_vars: true
        },
        output: {
          comments: false
        }
      }
    })] : [],
    splitChunks: {  // 自行切割全部chunk
      chunks: 'all'
    }
  },
}
複製代碼

splitChunks 配置的 chunks: 'all' 會改變html的引進的腳本,加了chunksHash後每次編譯的結果會不一致,須要結合html-webpack-plugin 使用。web

下面添加 plugins

const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const base = {
  plugins: [
    new ProgressBarPlugin(), // 爲編譯添加進度條
    new webpack.DefinePlugin({  // 爲項目注入環境變量
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      '__DEV__': isDebug
    }),
    new BundleAnalyzerPlugin({  // 生成編譯結果分析報告
      analyzerMode: 'server',
      analyzerHost: '127.0.0.1',
      analyzerPort: 8889,
      reportFilename: 'report.html',
      defaultSizes: 'parsed',
      generateStatsFile: false,
      statsFilename: 'stats.json',
      statsOptions: null,
      logLevel: 'info'
    }),
    new HtmlWebpackPlugin({  // 使用html模板,編譯結束後會根據 entry 注入 script腳本 和 css樣式表
      filename: 'index.html',
      template: path.resolve(__dirname, './index.html')
    })
  ]
}

複製代碼

導出配置

module.exports = base;
複製代碼

React 配置 webpack.react.config.js

webpack 的 react 配置,只要是針對 babel-loader 進行配置,首先聲明一個 bable-loader:

const path = require('path');

const babelLoader = {
  test: /\.jsx?$/,
  loader: 'babel-loader',
  include: [path.resolve(process.cwd(), 'src')],
  query: {
    babelrc: false,  // 禁止使用.babelrc文件
    presets: [  // 配置 presets
      'react',
      'stage-0',
      [
        'env',
        {
          targets: {
            browsers: ["last 2 versions", "safari >= 7", "ie >= 9", 'chrome >= 52']
          },
          useBuiltIns: true,
          debug: false
        }
      ]
    ],
    plugins: [
      'transform-decorators-legacy',
      'transform-class-properties'
    ]
  }
}
複製代碼

首先對preset進行理解,就是bable的一個套餐,裏面包含了各類plugin

  • 使用 babel-preset-react 讓其解析jsx語法
  • 使用 babel-preset-stage-0(stage中最高級的套餐),讓其對ES6的語法進行解析
  • 使用 babel-preset-env,讓其針對配置,對其加入不一樣的 polyfill,這裏使用的是 useBuiltIns,針對咱們在 base 配置中的 babel-polyfill 進行切割,針對咱們在項目中使用到的不兼容的特性進行 polyfill。

另外,添加另外的 plugins

  • babel-plugin-transform-decorators-legacy 解析裝飾器語法,也就是變量前邊的@符號,如antd高階組件中的@Form.create()
  • babel-plugin-transform-class-properties 解析 class 語法

另外,咱們針對開發環境,爲react組件添加熱替換 preset,babel-preset-react-hmre

if (isDebug) {
  babelLoader.query.presets = ['react-hmre'].concat(babelLoader.query.presets)
}
複製代碼

另外,爲了加快編譯速度,咱們使用happypack進行多線程編譯

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); // cpus核數
const happyLoaderId = 'happypack-for-react-babel-loader';

const reactConfig = {
  module: {
    rules: [{
      test: babelLoader.test,
      loader: 'happypack/loader',
      query: {
        id: happyLoaderId
      },
      include: babelLoader.include
    }]
  },
  plugins: [new HappyPack({
    id: happyLoaderId,
    threadPool: happyThreadPool,
    loaders: [babelLoader]
  })]
}
delete babelLoader.test;
delete babelLoader.include;

module.exports = reactConfig;
複製代碼

LESS 配置 webpack.less.config.js

首先,配置 css-loader

const isDebug = process.env.NODE_ENV !== 'production';

const cssLoader = {
  loader: `css-loader`,
  options: {
    sourceMap: isDebug, // 是否添加source-map
    modules: true,  // 是否使用css-module
    localIdentName: '[local]', // 使用class自己名字,不添加任何hash
  }
}
複製代碼

配置 postcss-loader

const postcssLoader = {
  loader: 'postcss-loader',
  options: {
    config: {
      path: __dirname
    }
  }
}
複製代碼

這裏咱們使用配置文件 postcss.config.js 路徑指向當前文件夾,而後新建配置文件 postcss.config.js,以下

module.exports = {
  plugins: () => {
    return [
      require('postcss-nested')(), // 用於解開 @media, @supports, @font-face 和 @document 等css規則
      require('pixrem')(), // 爲 rem 單位添加像素轉化
      require('autoprefixer')({ // 添加內核前綴
        browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 8']
      }),
      require('postcss-flexibility')(), // 添加 flex 佈局 polyfill
      require('postcss-discard-duplicates')() // 去除css中的重複規則
    ]
  }
}
複製代碼

配置 less-loader

const lessLoader = {
  loader: 'less-loader',
  options: {
    sourceMap: isDebug,
    javascriptEnabled: true  // 支持內聯JavaScript
  }
}
複製代碼

接下來,咱們針對不一樣的環境,爲webpack添加不一樣的 module.rules 和 plugins,首先是開發環境,咱們使用 style-loader 將css進行內聯(我的認爲內聯css對熱部署比較友好),另外,同react配置,爲了加快編譯,咱們使用 happypack 對 loader 進行包裹

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const lessHappyLoaderId = 'happypack-for-less-loader';
const cssHappyLoaderId = 'happypack-for-css-loader';

let loaders = [];
let plugins = [];

if (isDebug) {
  loaders = [{
    test: /\.less$/,
    loader: 'happypack/loader',
    query: {id: lessHappyLoaderId}
  }, {
    test: /\.css$/,
    loader: 'happypack/loader',
    query: {id: cssHappyLoaderId}
  }]

  plugins = [new HappyPack({
    id: lessHappyLoaderId,
    threadPool: happyThreadPool,
    loaders: ['style-loader', cssLoader, postcssLoader, lessLoader ]
  }),  new HappyPack({
    id: cssHappyLoaderId,
    threadPool: happyThreadPool,
    loaders: ['style-loader', cssLoader, postcssLoader ]
  })]
}
複製代碼

而後,對於生產環境,咱們使用 mini-css-extract-plugin 將 css 文件分離出來,並打包成 chunks,以便減小線上的首屏加載時間。

if (!isDebug) {
  loaders = [{
    test: /\.less$/,
    use: [MiniCssExtractPlugin.loader, {
      loader: 'happypack/loader',
      query: {id: lessHappyLoaderId}
    }]
  }, {
    test: /\.css/,
    use: [MiniCssExtractPlugin.loader, {
      loader: 'happypack/loader',
      query: {id: cssHappyLoaderId}
    }]
  }]

  plugins = [new MiniCssExtractPlugin({
    filename: '[name].css',
    // chunkFilename: "[id].css"
  }), new HappyPack({
    id: lessHappyLoaderId,
    loaders: [
      cssLoader,
      postcssLoader,
      lessLoader
    ],
    threadPool: happyThreadPool
  }), new HappyPack({
    id: cssHappyLoaderId,
    loaders: [
      cssLoader,
      postcssLoader
    ],
    threadPool: happyThreadPool
  })]
}
複製代碼

最後,導出配置

const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

const lessConfig = {
  module: {
    rules: loaders
  },
  plugins,
  optimization: {
    minimizer: [new OptimizeCssAssetsPlugin({ // 使用 OptimizeCssAssetsPlugin 對css進行壓縮
      cssProcessor: require('cssnano'),   // css 壓縮優化器
      cssProcessorOptions: { discardComments: { removeAll: true } } // 去除全部註釋
    })]
  }
};

module.exports = lessConfig;
複製代碼

合併配置 webpack.config.js

最後,咱們將全部配置 merge 在一塊兒

const merge = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
const reactConfig = require('./webpack.react.config');
const lessConfig = require('./webpack.less.config');

const config = merge(baseConfig, reactConfig, lessConfig);

module.exports = config;
複製代碼

而後咱們配置 package.json 的 sctipts,這裏咱們使用better-npm-run導出環境變量

{
  "scripts": {
    "start": "better-npm-run start",
    "build": "better-npm-run build"
  },
  "betterScripts": {
    "start": {
      "command": "webpack-dev-server --config ./build/webpack.config.js",
      "env": {
        "NODE_ENV": "development"
      }
    },
    "build": {
      "command": "webpack --config ./build/webpack.config.js",
      "env": {
        "NODE_ENV": "production"
      }
    }
  },
}
複製代碼

好的,配置到這裏已經完成,咱們能夠肆無忌憚的執行 npm run start了。

額外配置

針對 React 項目,對於開發過程,咱們只關心業務代碼的增量編譯,對於一些第三方 module 咱們不須要對齊進行更改,對於生產環境,這些第三方包也能夠利用緩存將其緩存起來,優化線上用戶體驗,因此咱們可使用DllPlugin或者SplitChunksPlugin對這些第三方包進行分離。

DllPlugin 能夠將指定的module提早編譯好,而後在每次解析到這些指定的module時,webpack可直接使用這些module,而不用從新編譯,這樣能夠大大的增長咱們的編譯速度。

SplitChunksPlugin,可使用test對module進行正則匹配,對指定的模塊打包成chunk,而後每次編譯的時候直接使用這些chunk的緩存,而不用每次解析組裝這些module。固然,使用SplitChunksPlugin生成的chunk在生成環境可能由於咱們指定了chunkHash每次文件名不同,致使咱們不能好好利用瀏覽器緩存這些第三方庫,也會所以影響到咱們html中每次引入的script,必須結合html-webpack-plugin進行使用,但對於一些沒有徹底先後端分離的業務項目來講(如路由由後端來控制,html渲染是後端控制),這很明顯是一個麻煩。

dllPlugin

dllPlugin的原理就是預先編譯模塊,而後在html中最早引進這些打包完的包,這樣 webpack 就能夠從全局變量裏面去找這些預先編譯好的模塊。

下面咱們使用配置使用 dllPlugin,新建配置文件 webpack.dll.config.js,這個文件爲 webpack 須要事先編譯的配置文件

首先聲明輸出 output

const path = require('path');
const isDebug = process.env.NODE_ENV !== 'production';
const output = {
  filename: '[name].js',
  library: '[name]_library',
  path: path.resolve(process.cwd(), isDebug ? './vendor-dev/' : './vendor/') // 編譯打包後的目錄
}
複製代碼

而後聲明整體配置

const dllConfig = {
  entry: {
    vendor: ['react', 'react-dom']  // 咱們須要事先編譯的模塊,用entry表示
  },
  output: output,
  plugins: [
    new webpack.DllPlugin({  // 使用dllPlugin
      path: path.join(output.path, `${output.filename}.json`),
      name: output.library // 全局變量名, 也就是 window 下 的 [output.library]
    }),
    new ProgressBarPlugin(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      __DEV__: isDebug
    })
  ],
  optimization: {}
}

複製代碼

而後,咱們根據不一樣的環境,添加配置

if (!isDebug) {
  dllConfig.mode = 'production';
  dllConfig.optimization.minimize = true;
  dllConfig.optimization.minimizer = [new UglifyJsPlugin({
    cache: true,
    parallel: true,
    sourceMap: true,
    uglifyOptions: {
      comments: false,
      warnings: false,
      compress: {
        unused: true,
        dead_code: true,
        collapse_vars: true,
        reduce_vars: true
      },
      output: {
        comments: false
      }
    }
  })];
} else {
  dllConfig.mode = 'development';
}

module.exports = dllConfig;
複製代碼

須要注意的是,當咱們使用dllPlugin對react進行編譯時,咱們須要使用isDebug對react進行生產環境和開發環境的區分,由於當咱們在生成環境使用開發環境的react的時候,react會報錯,因此,咱們這裏須要對不一樣環境的庫進行打包。

編譯打包,最後生成了一個 vendor.js 和 vendor.js.json,而後,咱們能夠在咱們編譯的配置中使用 dllReferencePlugin 引進這個json

下面咱們新建配置文件 webpack.dll.reference.config.js

const path = require('path');
const dllConfig = require('./webpack.dll.config');
const baseConfig = require('./webpack.base.config');
const webpack = require('webpack');
const isDebug = process.env.NODE_ENV !== 'production';
const CopyWebpackPlugin = require('copy-webpack-plugin');

const dllPath = dllConfig.output.path;
const dllEntry = dllConfig.entry;

const plugins = [
  new CopyWebpackPlugin([{ from: path.join(process.cwd(), isDebug ? './vendor-dev/' : './vendor/'), to: baseConfig.output.path, ignore: ['*.json']}]) // 將dll文件拷貝到編譯目錄
];

Object.keys(dllEntry).forEach((key) => {
  const manifest = path.join(dllPath, `${key}.js.json`);
  plugins.push(new webpack.DllReferencePlugin({
    manifest: require(manifest), // 引進dllPlugin編譯的json文件
    name: `${key}_library` // 全局變量名,與dllPlugin聲明的一直
  }))
})

module.exports = {
  plugins
}
複製代碼

最後,咱們把這個配置在 webpack.config.js 裏 merge 進來

const merge = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
const reactConfig = require('./webpack.react.config');
const lessConfig = require('./webpack.less.config');
const dllReferenceConfig = require('./webpack.dll.reference.config');

const config = merge(baseConfig, reactConfig, lessConfig, dllReferenceConfig);

module.exports = config;
複製代碼

而後在package.json添加預編譯腳本

{
  "scripts": {
    "start:dll": "better-npm-run start:dll",
    "build:dll": "better-npm-run build:dll"
  },
  "betterScripts": {
    "start:dll": {
      "command": "webpack --config ./build/webpack.dll.config.js",
      "env": {
        "NODE_ENV": "development"
      }
    },
    "build:dll": {
      "command": "webpack --config ./build/webpack.dll.config.js",
      "env": {
        "NODE_ENV": "production"
      }
    }
  }
}
複製代碼

打完收工,最後,在npm run start以前,咱們得先執行npm run start:dll,並在html中引進這個vendor.js,否則會報錯,找不到library,html以下

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>app</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="/vendor.js"></script> <!-- 根據根目錄設置 -->
</head>
<body>
</body>
</html>
複製代碼

SplitChunksPlugin

針對 SplitChunksPlugin,其實就是打包 chunks,如咱們把node_modules下的全部模塊打到一個chunk中

const splitChunkConfig = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          name: 'vendor',
          chunks: 'initial',
          priority: -10,
          reuseExistingChunk: false,
          test: /node_modules\/(.*)\.js/
        }
      }
    }
  }
}
複製代碼

使用 test 匹配 node_modules,最後會生成一個 vendor.chunk.js,若是設置有 chunkHash,文件名會帶hash,而後在html中引進便可。

最後

以上,基本搞了一套 webpack 相對編譯較快的配置,嗯呢~,該沉澱一下,獻上以上github地址,以上配置,已整理成cli,項目根目錄一鍵生成,詳情見 README

相關文章
相關標籤/搜索