webpack實戰之從roadhog2.x到webpack4.x

 這半周作了一件事,將手上的前端項目從使用過去dva腳手架自帶的roadhog2.x打包工具遷移至使用webpack4.x打包,成功讓本人掉了很多頭髮。javascript

背景

  先說背景,目前主要作的項目其實都是兄弟姐妹系統(是的沒錯,就是前端圈位於鄙視鏈底部的TO B系統),基於早期的JSP多頁應用使用React進行拆分重構;技術選型採用的是react + antd + dva。我從學校回來接入的時候,項目已經開始一段時間了。當時dva腳手架仍是帶的roadhog2.x構建包工具,它是在webpack之上的封裝,大致上就是提供一個開箱即用的傻瓜式構建方案,技術自己是沒有問題的,可是難受就難受在相關文檔不是那麼全,並且擴展性不足(固然若是你是隨便改底層的帶哥,當我沒說...);好比roadhog2.x移除了過去支持的dll配置項,同時sorrycc老哥重心也轉移到umi的開發維護上了...這邊隨着公司項目版本不斷迭代,代碼量的日漸增加以及一些工具、第三方庫的引入致使項目構建愈來愈慢,拖了一萬年的我終於忍不住,開始了將roadhog2.x對應構建方式遷移至webpack4.x的工做。css

webpack4.x

老生常談

源文件、Chunk、Bundle三者的聯繫

  一語蔽之,它們三個就是同一份代碼在不一樣階段的產物或者說別名,源文件是咱們本地coding的代碼,chunk則是源代碼在webpack編譯過程當中的中間產物,最終源代碼打包出來的就是bundle文件。html

約定大於配置

  webpack 4.x要再裝一個webpack-cli依賴配合,能夠經過npm i webpack webpack-cli -D一塊兒安裝。前端

  擼過webpack 4.x的兄弟姐妹確定有見過一個WARNINGThe 'mode' option has not been set, webpack will fallback to 'production' for this value.。如今咱們再進行webpack命令行操做的時候須要指定模式--mode production/development,若是沒有指定會使用默認的production。兩個模式下webpack會自動地進行相應的優化操做,好比指定production會自動進行代碼壓縮等等。java

默認狀況下entry就是src/index.js

  過去咱們還須要指定入口文件好比下面這樣的:node

entry: {
        index: ['babel-polyfill', path.resolve(__dirname, './src/index.js')],
    }   
複製代碼

  如今則根本不須要配置了,由於默認使用的就是這個模塊。react

默認狀況下output被指定爲dist/main.js

  emm,這個通常就不能不設置了,若是每次打包後的資源文件(html,js,css)名相同,因爲強緩存的緣由,咱們部署在服務器(好比Nginx)上的項目並不會更新,雖然這也能夠經過Nginx配置,但其實沒啥必要,咱們只要使每次打出來的文件名不一樣(設置hash),瀏覽器訪問的時候就會從新去請求最新的資源。好比:jquery

output: {
    filename: '[name].[hash:8].js',
    path: path.resolve(__dirname, './dist'),
    publicPath: '/'
  }
複製代碼

development模式下自動會開啓source-map

  做爲開發者,咱們在開發環境下debug每每須要根據控制檯的報錯信息定位具體文件,若是沒有source-map,咱們獲得的將是一段處理過的壓縮代碼,沒法定位到具體文件具體代碼行,這樣很是不利於調試,在webpack4.x前,咱們須要手動配置:webpack

module.exports = {
    devtool: 'source-map'
  }
複製代碼

  而如今在webpack4.x中經過指定模式--mode development將會自動開啓該功能。ios

基本格調

  在開始講遷移的踩坑記錄前,我先簡要講講通常webpack的配置文件由哪些部分組成:

  1. entry,即咱們的總入口文件,咱們要打包總得把從哪裏開始告訴webpack吧?一般這個文件都在src/index.js。舉個例子,你配置完全部的組件之後,確定有一個頂層爹,中間嵌套的用來提供Provider的也好,配置路由的也好,最終都是將這個爹經過選擇器掛載到你的根節點上,相似下面這樣:

ReactDOM.render(<Father />, document.getElementById('root')); 複製代碼

  固然我這邊項目看了下以前貌似直接拿的ant-design-prov1版本的改的(裂開,如今都到v4了)...入口文件dva有本身的封裝,v1版本的大概長下面這樣:

const app = dva({
    history: createHistory(),
  });
  app.use(createLoading());
  app.model(require('./models/global').default);
  app.router(require('./router').default);
  app.start('#root');
  export default app._store;
複製代碼

  2. webpack如今有文件解析了,可是咋解析,這個方案須要你告訴webpack。咱們須要在module配置項下的rules內經過正則斷定文件類型而後根據該類型選擇不一樣的loader來進行不一樣編譯,下面以解析jsjsx文件爲例子:

{
    test: /\.(js|jsx)$/,
    use: {
        loader: 'babel-loader',
        options: {
            cacheDirectory: true, // 默認false,開啓後,轉換結果會被緩存,再次編譯優先讀取緩存內容
        }
    },
    exclude: /node_modules/, // include指定包含文件,exclude除去包含文件
  }
複製代碼

  3. 指定了不一樣類型文件的處理方式之後,咱們可能還想要作一些額外的擴展,好比代碼壓縮、生成linkscript標籤、圖片拷貝到存放靜態資源的目錄、編譯過程根據庫依賴關係自動引入依賴等等。這時候就須要配置plugins配置項了,拿生成script標籤引入咱們的bundle爲例:

new HtmlWebpackPlugin({
      template: path.join(__dirname, '/src/index.ejs'), // 參照模板,bundle會在這個模板中經過插入script的方式引入
      filename: 'index.html',
      hash: true, // 防止緩存
  })
複製代碼

  4. 最終咱們獲得的編譯結果須要一個輸出,能夠經過配置項中的output來控制:

output: {
      filename: '[name].[hash:8].js',
      path: path.resolve( __dirname, './dist' ),
      chunkFilename: '[name].[hash:8].async.js', // 按需加載的異步模塊輸出名
      publicPath: '/'
    }
複製代碼

實戰踩坑

mini-css-extract-plugin

  webpack4.x中推薦使用的CSS壓縮提取插件,最終會在咱們提供的模板HTML中插入一個link標籤引入編譯後的樣式文件;過去版本中的webpack使用的是extract-text-webpack-plugin,可是本人最初嘗試使用的時候,報了Tapable.plugin is deprecated. Use new API on .hooks instead問題,去github對應項目下能夠發現以下提示:

loader的支持寫法以及加載順序

  loader支持不少種寫法,具體看實際場景,簡單配置的能夠直接寫在一個字符串內好比loader: 'style-loader!css-loader',匹配順序從右向左。複雜配置的推薦仍是用數組,雖然字符串也能夠經過相似GET請求那種拼接方案來設置配置項,可是可閱讀性太差了。在數組中,具體loader咱們能夠經過對象寫法來配置,看上去就清晰明瞭,例子以下:

module.exports = {
    module: {
      rules: [
        test: /\.css$/,
        use: [
          {
              loader: MiniCssExtractPlugin.loader,
              options: {
                  hmr: true,
              }
          },
          {
              loader: 'css-loader',
          },
        ]
      ]
    }
  }
複製代碼

less處理除了less-loader還須要裝less的開發環境依賴

  emm...這實際上是我當時睿智了,想一想都知道沒有裝less咋處理呢,經過npm i -D less解決。

style-loader與mini-css-extract-plugin存在衝突

  在我本身鼓搗小DEMO的時候,用style-loader都是沒啥問題的,不過在遷移的項目裏,加上就會報錯。這裏就要理清一個問題,style-loader到底負責的內容是什麼,根據webpack官方的文檔說明,它最終會將處理後的CSS以<style></style>的DOM結構寫入HTML。而後思考一下前面的mini-css-extract-plugin功能,它倆最終想要的效果是一致的,會有衝突,因此咱們移除style-loader便可。關聯issue能夠看下這個issue

css和less文件分開解析

  最開始的時候,我對樣式的處理都是經過正則test: /\.(css|less)$/寫在一塊的,可是一直編譯報錯,估計是具體配置項不能共享或者有衝突,分開單獨作處理問題解決。

antd的樣式未加載

  以前roadhog中在webpackrc.js中的處理是:

["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }],
複製代碼

  改用webpack4.x後,在.babelrc文件中一樣寫入以上配置,可是要把style的值設置爲css,修改後,antd樣式成功載入。

@connect裝飾器報錯

  HOC的裝飾器寫法,須要配置babel支持。如今webpack通常都不直接在自身配置文件裏面設置babel了,而是將babel的配置信息抽出來放到.babelrc內以JSON格式維護,在plugins內加入下面這段便可:

["@babel/plugin-proposal-decorators", { "legacy": true }],
複製代碼

babel版本

  在轉webpack4.x的過程當中發現有babel報錯的問題,後查發現是兼容性的坑,因此將有問題的懟到了babel7.x版本配合webpack,7.x版本的babel都帶上了@前綴。

CSS-IN-JS

  由於項目內的樣式是按照css-modules的規範來寫的,因此編譯的時候也須要開啓支持,在css-loaderoptions內設置modules: true便可。

根據文件目錄以及樣式類名生成class

  如今生成class名能夠方便咱們定位調試一些樣式,好比你想在控制檯Element的DOM樹結構裏ctrl + F檢索對應樣式類,而後直接進行調試。這裏就須要接着上面的css-modules配置調整了:

{
      loader: 'css-loader',
      options: {
          importLoaders: 1, // 設置css-loader處理樣式文件前 容許別的loader處理的數量 默認爲0
          modules: {
              localIdentName: '[name]_[local]_[hash:base64:5]', // 修改生成的class的名稱 name就是文件名 local對應類名 [path]支持路徑
          }
      }
  },
  {
      loader: 'less-loader',
      options: {
          javascriptEnabled: true,
      }
  }
複製代碼

  當時改的時候有一個坑,即不能像下面這樣設置class:

  改進後先後對比:

React is not defined

  這是我遷移得差很少的時候忽然發現的,即部分場景出現了React is not defined的報錯,而後定位了代碼發現的確會缺乏依賴,好比我在一個組件中引入了antd的UI組件,即使只是對引入的UI組件進行純函數的操做,但antd自己也有對React的依賴,那爲何以前roadhog處理就沒有問題呢?確定是有額外的插件作了騷操做!最後在stackoverflow上看到一個老哥的回答,又去webpack官方文檔對比了下,靠譜!加入對應插件後解決該問題。

new webpack.ProvidePlugin({ // 根據上下文,在須要依賴React處,自動引入
      "React": "react",
  })
複製代碼

路由跳轉組件未掛載

  不吹不黑,這東西是我遷移過程當中遇到最坑的問題...最先的時候我曾經在webpack輸出的內容裏看到Router的warning,可是後面就消失了,形成當時走了彎路,其實罪魁禍首是這個項目在.webpackrc.js內禁用了import()這種按需動態引入的方式,就直接致使了我編譯出來的文件其實除了根路由的內容,別的內容缺失。找到根源,再定位解決,就容易了,看下roadhog內對應配置項是用什麼處理的便可,最後引入babel-plugin-dynamic-import-node-sync解決:

CommonsChunkPlugin

  webpack4.x中,該用於抽離不一樣入口文件公共部分的插件已被移除,改用optimization配置項下的splitChunks選項使用。

What's more?

progress-bar-webpack-plugin

  用來在命令行可視化webpack編譯進度的插件:

new ProgressBar({
      format: ' build [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)',
      clear: false
  })
複製代碼

chalk

  用來設置輸出顏色的「粉筆」,經過const chalk = require('chalk');引入。

friendly-errors-webpack-plugin

  自定義輸出提示工具:

new FriendlyErrorsWebpackPlugin({
      compilationSuccessInfo: {
          messages: [`You application is running here http://localhost:3000`],
      },
  })
複製代碼

webpack-merge

  這個庫主要是用來進行webpack分包的,針對不一樣環境和功能,咱們徹底能夠將webpack配置文件拆成多個,好比base文件裏就是分包的webpack會共用的配置信息,dev裏就是webpack-dev-serverdevelopment模式下的配置信息,prod放生產部署的壓縮優化配置,dll進行代碼預編譯,提高首次編譯後的代碼編譯效率,通常結構以下:

DllPlugin&DllReferencePlugin

  webpack攜帶的dll預編譯插件,它會將幾乎不改動的庫進行編譯(由你指定),而後生成一個編譯後的js以及負責告知webpack以後編譯過程哪些內容不須要再處理的json

portfinder

  查找可用端口。

Result

  開發環境編譯時長從以前的半分到一分鐘不等到如今的10s左右:

TODO

  進行生產打包部署的替換。畢竟遷移後的打包結果還須要評估依賴缺失的風險,這中間須要通過大量測試及灰度驗證...

附錄:具體配置

.babelrc

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react",
    ],
    "plugins": [
        "dva-hmr",
        [
            "babel-plugin-module-resolver",
            {
                "alias": {
                    "components": "./src/components",
                },
            },
        ],
        "@babel/plugin-proposal-function-bind",
        "dynamic-import-node-sync",
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
        ["@babel/plugin-proposal-class-properties", { "loose" : false }],
        ["@babel/plugin-transform-runtime"],
        ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }],
    ],
}
複製代碼

package.json

{
  "name": "your-app-name",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "dll": "cross-env ESLINT=none webpack --progress --colors --config webpack.config.dll.js --mode production",
    "dev": "cross-env ESLINT=none webpack-dev-server --open --config webpack.config.dev.js --mode development",
  },
  "dependencies": {
    "@babel/polyfill": "^7.0.0-beta.36",
    "antd": "3.7.2",
    "aphrodite": "^1.2.1",
    "axios": "^0.18.0",
    "classnames": "^2.2.5",
    "dva": "^2.1.0",
    "dva-loading": "^1.0.4",
    "enquire-js": "^0.1.1",
    "jquery": "^3.2.1",
    "lodash": "^4.17.4",
    "lodash-decorators": "^4.4.1",
    "moment": "^2.19.1",
    "omit.js": "^1.0.0",
    "path-to-regexp": "^2.1.0",
    "prop-types": "^15.5.10",
    "qs": "^6.5.0",
    "rc-drawer-menu": "^0.5.0",
    "react": "^16.7.0-alpha.0",
    "react-addons-css-transition-group": "^15.6.2",
    "react-container-query": "^0.9.1",
    "react-document-title": "^2.0.3",
    "react-dom": "^16.7.0-alpha.0",
    "react-fittext": "^1.0.0",
    "react-image-lightbox-rotate": "^1.2.0",
    "react-lazyload": "^2.3.0",
    "react-pdf-js": "^4.2.3",
    "react-swf": "^1.0.7",
    "rollbar": "^2.3.4",
    "url-polyfill": "^1.0.10"
  },
  "devDependencies": {
    "@babel/core": "^7.5.5",
    "@babel/plugin-proposal-class-properties": "^7.5.5",
    "@babel/plugin-proposal-decorators": "^7.3.0",
    "@babel/plugin-proposal-function-bind": "^7.2.0",
    "@babel/plugin-transform-runtime": "^7.5.5",
    "@babel/preset-env": "^7.5.5",
    "@babel/preset-react": "^7.0.0",
    "babel-eslint": "^8.1.2",
    "babel-loader": "^8.0.6",
    "babel-plugin-dva-hmr": "^0.4.2",
    "babel-plugin-dynamic-import-node-sync": "^2.0.1",
    "babel-plugin-import": "^1.6.7",
    "babel-plugin-module-resolver": "^3.1.1",
    "babel-plugin-transform-decorators-legacy": "^1.3.5",
    "babel-polyfill": "^6.26.0",
    "chalk": "^2.4.2",
    "clean-webpack-plugin": "^3.0.0",
    "copy-webpack-plugin": "^5.0.4",
    "cross-env": "^5.2.0",
    "cross-port-killer": "^1.0.1",
    "css-loader": "^3.1.0",
    "eslint": "^4.14.0",
    "eslint-config-airbnb": "^16.0.0",
    "eslint-config-prettier": "^2.9.0",
    "eslint-plugin-babel": "^4.0.0",
    "eslint-plugin-compat": "^2.1.0",
    "eslint-plugin-import": "^2.8.0",
    "eslint-plugin-jsx-a11y": "^6.0.3",
    "eslint-plugin-markdown": "^1.0.0-beta.6",
    "eslint-plugin-react": "^7.7.0",
    "file-loader": "^4.1.0",
    "friendly-errors-webpack-plugin": "^1.7.0",
    "glob": "^7.1.4",
    "html-webpack-plugin": "^3.2.0",
    "less": "^2.7.3",
    "less-loader": "^5.0.0",
    "mini-css-extract-plugin": "^0.8.0",
    "mockjs": "^1.0.1-beta3",
    "portfinder": "^1.0.13",
    "postcss-loader": "^3.0.0",
    "progress-bar-webpack-plugin": "^1.12.1",
    "purify-css": "^1.2.5",
    "purifycss-webpack": "^0.7.0",
    "redbox-react": "^1.6.0",
    "regenerator-runtime": "^0.11.1",
    "style-loader": "^0.23.1",
    "url-loader": "^2.1.0",
    "webpack": "^4.39.1",
    "webpack-bundle-analyzer": "^2.11.2",
    "webpack-cli": "^3.3.6",
    "webpack-dev-server": "^3.7.2",
    "webpack-merge": "^4.2.1"
  },
  "engines": {
    "node": ">=8.0.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 10"
  ]
}
複製代碼

webpack.config.base.js

const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const theme = require('./src/theme');
const Mode = process.env.NODE_ENV !== 'production';

module.exports = {
    output: {
        filename: '[name].[hash:8].js',
        path: path.resolve( __dirname, './dist' ),
        chunkFilename: '[name].[hash:8].async.js',
        publicPath: '/'
    },
    module: {
        rules: [
            {
                test: /\.css$/, 
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            hmr: Mode,
                        }
                    },
                    {
                        loader: 'css-loader',
                    },
                ]
            },
            {
                test: /\.less$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            hmr: Mode,
                        }
                    },
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1,
                            modules: {
                                localIdentName: '[name]_[local]_[hash:base64:5]',
                            }
                        }
                    },
                    {
                        loader: 'less-loader',
                        options: {
                            javascriptEnabled: true,
                            modifyVars: theme,
                        }
                    }
                ]
            },
            {
                test: /(\.js|\.jsx)$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        cacheDirectory: true,
                    }
                },
                exclude: /node_modules/,
            },
            {
                test: /\.(jpg|jpeg|png|svg|git|swf)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 1024,
                            outputPath: 'images'
                        }
                    }
                ]   
            }
        ],
    },

    plugins: [
        new MiniCssExtractPlugin({
            filename: Mode ? '[name].css' : '[name].[hash:8].css',
            chunkFilename: Mode ? '[id].css' : '[id].[hash:8].css',
            ignoreOrder: false,
        }),
        new CopyWebpackPlugin(
            [
                {
                    from: path.resolve(__dirname, './public'),
                }
            ]
        ),
        new webpack.IgnorePlugin(/\.\/locale/, /moment/),
        new webpack.ProvidePlugin({
            "React": "react",
        }),
    ],

    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    }
}
複製代碼

webpack.config.dev.js

const merge = require('webpack-merge');
const webpack = require('webpack');
const baseConfig = require('./webpack.config.base');
const ProgressBar = require('progress-bar-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const portfinder = require('portfinder');
const chalk = require('chalk');
const path = require('path');

let DEFAULT_PORT = 8000;

let checkAndGetPort = () => {
    portfinder.basePort = DEFAULT_PORT;
    portfinder.getPort((err, port) => {
        if (!err) {
            DEFAULT_PORT = port;
        }
    })
}

let mergeConfig = async () => {
    await checkAndGetPort();
    return merge(
        baseConfig, {
            devServer: {
                contentBase: './dist',
                port: DEFAULT_PORT,
                inline: true,
                historyApiFallback: true,
                hot: true,
                quiet: true,
                proxy: {}                      
            },
            plugins: [
                new webpack.HotModuleReplacementPlugin(),
                new HtmlWebpackPlugin({
                    template: path.join(__dirname, '/src/index.ejs'),
                    filename: 'index.html',
                    hash: true,
                    isDev: true,
                }),
                new ProgressBar({
                    format: ' build [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)',
                    clear: false
                }),
                new webpack.DllReferencePlugin({
                    context: __dirname,
                    manifest: require('./dist/vendor-manifest.json')
                }),
                new FriendlyErrorsWebpackPlugin({
                    compilationSuccessInfo: {
                        messages: [`You application is running here http://localhost:${DEFAULT_PORT}`],
                    },
                })
            ]
        }
    )
}

module.exports = mergeConfig();
複製代碼

webpack.config.dll.js

const path = require('path');
const webpack = require('webpack');
module.exports = {
  resolve: {
    extensions: [ '.js', '.jsx' ]
  },
  entry: {
    vendor: [
      'antd', 'aphrodite', 'axios', 'classnames',
      'dva', 'dva-loading', 'enquire-js',
      'react', 'react-dom', 'react-image-lightbox-rotate', 'moment',
      'qs', 'prop-types', 'path-to-regexp', 'react-pdf-js', 'react-swf',
      'lodash', 'jquery', 'rc-drawer-menu'
    ]
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, './dist'),
    library: 'vendor_lib_[hash:8]',
  },
  plugins: [
    new webpack.DllPlugin({
      context: __dirname,
      path: path.resolve(__dirname, './dist/vendor-manifest.json'),
      name: 'vendor_lib_[hash:8]',
    })
  ],
};


複製代碼

index.ejs

<%= htmlWebpackPlugin.options.isDev ? '<script src='./vendor.dll.js'></script>' : '' %>
複製代碼
相關文章
相關標籤/搜索