基於vue2.x的webpack升級與項目搭建指南--進階篇


 

在正式開始之前:

  本文將在上一篇的基礎上進行拓展,漸進演化出一個相對於實際開發/生產環境切實可用的前端構建配置,具體涉及到的工具包括:

  1. clean-webpack-plugin
  2. html-webpack-plugin
  3. babel & babel-loader
  4. postcss
  5. webpack-dev-server
  6. mini-css-extract-plugin
  7. terser-webbpck-plugin & optimize-css-assets-webpack-plugin
  8. thread-loader
  9. cache-loader
  10. eslint & eslint-loader

 

  之前介紹過了,webpack4的mode屬性有"development"和"production"兩種,對應開發環境與生產環境,由於兩個環境下的配置會出現差異,一個webpack.base.conf就不太能適應接下來的應用場景了,所以接下來在追加配置之前,你也許需要分化出對應的配置文件,在不同的場景下加載不同的打包配置,這也是webpack的常規操作了。

  對一般項目來說,在webpack.base.conf的基礎之上,新增兩個配置文件就夠用了,按照慣例,命名為webpack.dev.conf.js和webpack.prod.conf.js,

  然后我要介紹webpack-merge,webpack-merge,一般用來合並兩個配置文件,用法如下:

const merge = require("webpack-merge").merge
const webapckBaseConfig = require("./webpack.base.conf")
module.exports = merge(webapckBaseConfig,{
    ...your configuration
})

  ps:當然你也可以把它當成Object.assign方法,用來合並一個普通的對象,就是有點殺雞焉用牛刀的味道(webpack-merge主要對loader中的rules,尤其是各個具體的loader的option做了追加、替換處理)

 

clean-webpack-plugin

  clean-webpack-plugin用來清空某個目錄,如果不指定目錄,則默認清空output.path中所指定的路徑,雖然實現這個功能有不少途徑,但為了方便,還是選擇使用了這個在我來看是剛需的插件。現在剛剛介紹過的webpack-merge就體現除了作用——開發環境下(如果你使用的是webpack-dev-server)編譯生成的文件是不會寫入硬盤的,所以你應該在webpack.prod.conf.js中添加這個插件而不是webpack.base.conf.js。

  

const merge = require("webpack-merge").merge
const webapckBaseConfig = require("./webpack.base.conf")
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = merge(webapckBaseConfig,{
    mode:"production",
    plugins: [
        new CleanWebpackPlugin(),
    ]
})

 

html-webpack-plugin

  在上一篇文章的最后得到的dist目錄的結構描述成json是這樣的:

"dist":[

  "static": [

    {”img": [...]},

    {"font": [...]},

    {"js":[...]}

  ]
]

 

  這種形式的包還沒有辦法應用到實踐中,原因是缺少一個用於掛載js腳本的和css文件的“實體”,如果沒有一個html文件作為入口用來掛載這些資源,瀏覽器就沒有機會去解析style/script標簽和對應的代碼。沒有進行render流程,所以頁面自然是打不開的。解決這個問題,需要創建一個html文件來引入打包資源,而html-webpack-plugin這個插件幫助我們簡化了這個流程(我的意思是,雖然不推薦,但你確實可以自己在一個html文件中手動把static目錄中的資源引入來使你的前端包變得可用。官網原文:

  This is a webpack plugin that simplifies creation of HTML files to serve your webpack bundles. This is especially useful for webpack bundles that include a hash in the filename which changes every compilation. You can either let the plugin generate an HTML file for you, supply your own template using lodash templates or use your own loader.)

  接下來我在webpack的配置中加上這個插件,下面會挑幾個屬性解釋它們的作用,其余屬性請看: 

  https://github.com/jantimon/html-webpack-plugin

 

const HtmlWebpackPlugin = require("html-webpack-plugin")
...
...
plugins: [
    ...
    new HtmlWebpackPlugin({
     // 選擇一個本地的html文件作為模板而不是讓插件自動生成
      template:"src/module/indexApp/index.html",
      // inject這個屬性可以選擇在html文件中引入打包資源的位置,true是默認值,將script注入到body標簽的最下方,其余可選值為:"head"/"body"/false
      inject: true,
      // 生成的html文件名,"index.html"是默認值
      filename:"index.html",
     // 小圖標
      favicon:"xxxx.png",
     // 破壞緩存,在引入每個資源時以?xxxxxx的形式添加哈希作為請求參數,效果是否與output中的filename中添加[hash]一致這點就有待驗證
    // 因為其他執行過程中會創建新文件的插件比如copy-webpack-plugin和mini-css-extract-plugin都是可以選擇是在生成的文件名后添加hash后綴的
      // hash: "true"
    })
   // ps:這個插件也是免配置的,不想在上面花太多心思的話,可以直接new HtmlWebpackPlugin()
]

 

接下來執行打包命令,打包后dist目錄就成為了這樣:

 

 細節:

 到這里關於這個插件的介紹就結束了,雖然幾乎每個使用webpack的項目都會用這個插件,但實際上我覺得他的重要性並不是那么高,可能是因為我這邊的需求還用不到其他進階的屬性吧。

 

babel-loader & babel

   babel用於將es6以及更前沿的語法轉成es5語法以便於代碼能在低版本環境中運行,使用babel轉譯到低級語法算是絕大多數項目在構建過程中普遍存在的流程,如果你的項目是從零開始構建而不是在老版本的babel配置基礎上重新改的話,你在配置babel的時候也許會比后一種方式少花很多時間(我當初是用后一種方式慢慢摸索解決報錯終於完成babel7.x的升級,這次復盤的時候換了一種思路,把舊版的配置與依賴全部刪除之后直接上新版本以及配置babelrc文件,其過程順利得讓我懷疑之前辛苦爬過的坑像是幻覺一般沒有真實存在過……)。

  話不多說,上代碼。

  公司的項目之前使用的是babel-core@6.0.0以及babel-loader@6.0.0,按照之前的升級理念,舊代碼舊配置僅供參考,我把該備份的備份好后,然后開始重新配置。

  首先需要下載基本的依賴

  

npm install --save-dev @babel/core @babel/cli @babel/preset-env
npm install --save @babel/polyfill
npm install babel-loader -D

 

  babel7.x要求搭配v8.x的babel-loader,寫這篇文章的時候最新的babel-loader版本是8.2.2,所以我就直接安裝了,安裝完成之后開始對webpack追加配置,使用babel-loader對js文件進行處理:

rules: [
      ...
     { test:
/\.js$/, loader: "babel-loader", // 遇到node_modules目錄中的js文件時跳過 exclude: /node_modules/, }, ]

  babel的配置文件命名分兩種類型,一種規定命名為babel.config.xx(拓展名一般是json或者js,你也可以視你的環境和寫法設為cjs和mjs),另一種為.babelrc(json)或者.babelrc.js,其中babelrc的優先級比babel.config更高。在項目的根目錄下創建這個文件,babel-loader便會根據這個配置來處理符合規則的js文件。

{
    // 通過presets配置babel的預設,你可以理解為一系列插件的集合,在預設后可以添加一些這些插件的共同配置
    // 而preset-env又可以說是幾個預設的集合,默認情況下會加載es5到目前最新的preset(es2020),並且內容隨着新標准的推出而更新
    // 但babel這樣全面的支持帶來了不小的的負面效果,比如transform后的代碼體積越來越大(確實)、編譯速度也會受到影響(確實),所以為了回避這個問題,官方並不推薦用戶0配置直接使用babel,用戶提供的配置的粒度越精細,babel的性能表現就會越好(這是我根據官網描述理解的說法,但實際上並沒有感覺出來有啥差別,嘻嘻)
    "presets": [["@babel/preset-env",{
        "targets": {
            "browsers": [
              "> 1%",
              "last 2 versions",
              "not ie <= 10"
            ]
        }
    }
    ]],
    "plugins": [
        // 轉換vue單文件組件script模塊中可能會出現的jsx語法
        "transform-vue-jsx", 
        // 項目使用到的elementui按需導入
        [
            "component",
            {
                "libraryName": "element-ui",
                "styleLibraryName": "theme-chalk"
            }
        ]
    ],
    "comments": false
}
.babrlrc

 

postcss

  postcss是一個用於解析與轉換css代碼的平台類型的工具,我之所以稱之為平台,是因為它通過搭載在平台上的應用了實現對css代碼的各種處理,比如lint機制和客戶端兼容等,這里只展示我這邊構建過程中使用到的客戶端兼容工具autoprefixer的用法,之后可能會用demo水一篇的過程細節出來。

  在webpack中運用postcss-autoprefixer需要配合postcss-loader,所以老樣子,第一步需要安裝依賴;

  npm i postcss postcss-loader autoprefixer -D

  在構建過程中,postcss-loader會嘗試讀取根目錄下名為postcss.config.js的postcss配置,所以接下來需要新建一個postcss.config.js文件:

module.exports = {
    plugins: {
      "autoprefixer": {
      }
    }
}

  autoprefixer需要聲明支持的客戶端名單,如果你不想使用默認配置,可以在根目錄下建一個.browserslistrc文件來配置需要兼容的客戶端的版本范圍,也可以在package.json中添加一個browsersList屬性,比如:

...
  "browserslist": [
  // 全球范圍內超過1%人使用的瀏覽器……數據來源為止,即是說這個數字量越大兼容的客戶端范圍就越小
"> 1%",
  // 瀏覽器廠商最近發布的n個版本
"last 2 versions",
  // 不兼容ie10及以下
"not ie <= 10" ] ...

  可能有時候你並不確定你是否需要使用postcss的功能,這種情況下在config/index.js中設置一個開關是不錯的選擇,添加postcss-loader處理css代碼的流程需要在css-loader處理之前,因為處理樣式的loader之前被封裝到了名為utils的文件里,結合新增的開關,你需要拓展utils.js中的styleLoaders方法:

/**
 * 
 * @param {{usePostcss:Boolean}} options 
 */
exports.cssLoaders = function (options) {

  const vueStyleLoader = {
    loader: "vue-style-loader"
  }
  const cssLoader = {
    loader: "css-loader",
    options: {
      // 如果你使用的是vue-style-loader並且css-loader的版本在v4.0.0及以上,下面這個屬性必須配置為false,具體原因請看https://www.cnblogs.com/byur/p/14194672.html
      esModule: false,
    }
  }
  const postcssLoader = {loader:'postcss-loader'}
  // loader解析順序從右至左
  // const baseLoaders = [vueStyleLoader,cssLoader]
  function generateLoaders (loader) {
    const outputLoaders = [vueStyleLoader,cssLoader]
    if (options.usePostcss){
      outputLoaders.push(postcssLoader)
    }
    if (loader) {
      const targetloader = {loader:loader+"-loader"}
      outputLoaders.push(targetloader)
    }
    return outputLoaders
  }

  return {
    css: generateLoaders(),
    less: generateLoaders("less"),
    sass: generateLoaders("sass"),
    scss: generateLoaders("sass"),
    stylus: generateLoaders("stylus"),
    styl: generateLoaders("stylus")
  }
}
// 追加了參數options
exports.styleLoaders = function (options) {
  var output = []
  // 透傳options
  var loaders = exports.cssLoaders(options)
  
  for (let extension in loaders) {
    var loader = loaders[extension]
    console.log(loader)
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }
  return output
}

  在必要的時候開啟它:

const merge = require("webpack-merge").merge
const webapckBaseConfig = require("./webpack.base.conf")
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const config = require("../config")
const utils = require('./utils')
module.exports = merge(webapckBaseConfig,{
    mode:"production",
    module: {
        // 開啟postcss需要在config.build中將usePostcss設置為true
        rules: utils.styleLoaders({
            usePostcss:config.build.usePostcss
        }),
    },
    plugins: [
        new CleanWebpackPlugin(),
    ]
})

   大功告成。

  postscss這部分內容其實是有一些特殊的,特殊在沒什么存在感(笑),一是因為很多項目實際上只用上了一個autoprefixer,但這個東西實際上想要達到准確理解配置的意義之后再去根據自己的需求進行調整,還是有些麻煩的,所以很多時候就直接復制一份配置到新項目就完事了;而第二點在於autoprefixer這個工具,以開發者(僅代表我自己)的角度,很多時候不太容易察覺到autoprefixer生效與不生效時的區別,你只知道你做的配置當然是在構建過程中生效了,但是不生效的時候客戶端的表現是個什么樣子,這個我平時是很少遇到的,至少在使用主流屬性(比如placeholder、box-shadow等)的時候,所以有時候心里不免懷疑它的必要性。

 

webpack-dev-server

  接下來增加一個開發模式,主要是用來落實“開發體驗”這四個字,webpack的開發模式按官網的說法分為三種(watch mode、webpack-dev-server、webpack-dev-middleware),但實際應用中基本只會使用后兩種(因為第一種不能自動刷新瀏覽器),公司項目原先使用的是中間件,我這次將改用webpack-dev-server來作為開發模式下的平台。

  所以之前新建的webpack.dev.conf.js現在發揮了用武之地,這個文件除了繼承了通用配置之外,還將用來承載開發模式下的一些個性化配置,這其中有一個重要的屬性便是devServer,在webpack默認尋找並且執行的配置文件webpack.config.js中,devServer也是需要在其中配置的,包括在webpack基礎之上封裝的vue-cli,你也可以在它的配置文件中找到devServer的屬性,現在我們把這個屬性配置webpack.dev.conf.js文件中。

const merge = require("webpack-merge").merge
const Webpack = require("webpack")
const webapckBaseConfig = require("./webpack.base.conf")
const config = require("../config")
const utils = require('./utils')
const path = require("path")


module.exports = merge(webapckBaseConfig,{
    mode:"development",
    module: {
        rules: utils.styleLoaders({
            usePostcss:config.dev.usePostcss
        }),
    },
    plugins: [
        new Webpack.HotModuleReplacementPlugin(),
    ],
    devtool:config.dev.devtool,
    devServer: {
        // 在控制台展示構建進度
        progress: true,
        // 內聯模式,發生熱替換時,相關的構建信息將刷新在控制台中,false則展示在瀏覽器中,建議用true。
        inline: true,
        // 日志級別,這個不用解釋
        clientLogLevel: "warning",
        // 可以理解為靜默模式,webpack編譯過程中的錯誤和警告將不會輸出在控制台,構建/熱重載完成后不會有提示,如果沒有其他輔助輸出的工具,不建議設置為false
        quiet: false, 

        historyApiFallback: {
            rewrites: [
                {
                  from: /.*/,
                  to: path.posix.join(config.dev.assetsPublicPath, "index.html"),
                },
            ],
        },
        // 開啟了hot之后,如果插件里沒有添加HotModuleReplacementPlugin(HMR)的話,構建開始時dev-server會自動幫你補上,但還是手動寫吧hhh
        hot: true,
        // 開啟gzip
        compress: true,
        host: config.dev.host,
        port: config.dev.port,
        // 自動打開瀏覽器
        open: config.dev.autoOpenBrowser,
        // 編譯失敗時在瀏覽器全屏展示報錯
        overlay: config.dev.errorOverlay?
            {
                warnings: false,
                errors: true,
            }:false,
        publicPath: config.dev.assetsPublicPath,
        // 代理
        proxy: config.dev.proxyTable,
    },
})  

  附:config.dev

  dev: {
    env: "development",
    port: 8090,
    host: "localhost",
    assetsSubDirectory: "static",
    assetsPublicPath: "/",
   // 你的私人配置 proxyTable: inter.proxy,
autoOpenBrowser: true, errorOverlay: true, // https://www.webpackjs.com/configuration/devtool/#devtool 需要慎重選擇,會影響構建和重載速度 devtool: "#eval-source-map", usePostcss: false, },

   按照常規的寫法npm命令行寫webpack-dev-server build/webpack.dev.conf.js就夠了,但是之前提到過了,這樣有時候分配的內存會不夠用,我摸索出來的的解決辦法使創建一個js腳本,通過node攜帶擴容參數去執行這個腳本並且調動webpack-dev-server的api。

/**
 * 寫法參考:
 * https://github.com/webpack/webpack-dev-server/blob/master/examples/api/simple/server.js  node調起dev-server
 */
const Webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackConfig = require('./webpack.dev.conf');

const compiler = Webpack(webpackConfig);
const devServerOptions = webpackConfig.devServer
// 不同於手動調webpack啟動項目,手動調起WebpackDevServer的時候,會忽略webpackConfig中的devServer,所以需要在第二個參數中補充
const server = new WebpackDevServer(compiler, devServerOptions);

server.listen(devServerOptions.port, devServerOptions.host, () => {
  console.log(`Starting server on http://${devServerOptions.host}:${devServerOptions.port}`);
});

  然后在package.json中添加一條新命令:

……
  "scripts": {
    "build": "node --max_old_space_size=4096 build/build.js",
    "dev": "node --max_old_space_size=4096 build/dev-server.js"
  },
……

    到這里你已經寫出了一個基礎功能還算完備的配置(用來自娛自樂應該足夠了),可以打包發到服務器上部署,也可以本地跑起來開發調試,接下來需要做的是兼顧體驗與性能、對現有配置進行優化,希望我的內容和文筆不會讓你感到乏味。

 

 

 

  現在,一切用數據說話

  之前的一篇文章提到介紹過兩個插件,這里建議你添加一個開關,根據開關狀態用這兩個插件來對現有項目構建流程中的性能表現進行分析。而有一個需要事先說明的事情是,這兩個插件在生效期間也是消耗了部分性能的,所以耗時的計算和webpack計算出來的總耗時會有誤差,一般來說webpack的統計信息中的耗時會比speed-measure-webpack-plugin會少,因為webpack-bundle-analyzer的耗時會被speed-measure-webpack-plugin統計。

  config.build

  build: {
    env: require("./prod.env"),
    index: path.resolve(__dirname, "../dist/index.html"),
    // 輸出靜態文件的目錄
    assetsRoot: path.resolve(__dirname, "../dist"),
    // 除index外其他文件的目錄
    assetsSubDirectory: "static",
    // 資源引用的公共前綴
    assetsPublicPath: "",
    productionSourceMap: false,
    devtool: "#source-map",
    usePostcss: true,
    // 手動開啟,僅供調試分析用,改動該屬性時禁止提交該部分代碼
    analyzeMode: false,
  },

  webpack.prod.conf.js

const merge = require("webpack-merge").merge
const webapckBaseConfig = require("./webpack.base.conf")
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const config = require("../config")
const utils = require('./utils')
const webpackProdConfig = merge(webapckBaseConfig,{
    mode:"production",
    module: {
        // 開啟postcss
        rules: utils.styleLoaders({
            usePostcss:config.build.usePostcss
        }),
    },
    devtool: config.build.productionSourceMap?config.build.devtool:false,
    plugins: [
        new CleanWebpackPlugin(),
    ]
})
// 增加一層判斷
if (config.build.analyzeMode){
    const WebpackBundleAnalyzer = require("webpack-bundle-analyzer").BundleAnalyzerPlugin
    const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
    const spw = new SpeedMeasurePlugin()
    webpackProdConfig.plugins.push(new WebpackBundleAnalyzer())
    module.exports = spw.wrap(webpackProdConfig);
    return;
}
module.exports = webpackProdConfig;

  我跑了五六次,掐頭去尾去了一次最貼近平均值的結果:

    bundle的整體視圖

    附帶的一些統計信息:

  (看到這里stat和parsed的數值對比,可能就有懂哥意識到了這個構建過程中可能重復打包了依賴)

 

    耗時細節的統計信息

  這種幾乎零個性化配置的性能比這篇文章中最后的配置還要好點,讓我感嘆webpack4對相比之前的版本,不光在性能上有所增強,對用戶也友好了太多……(早該管管了!.jpg)

 

  css抽取與代碼壓縮

  為了從loader和plugin上擠出更多時間,接下來我在mode:production的基礎之上,進行樣式代碼提取以及代碼壓縮。

  webpack4之前我這邊做css提取使用的是extract-text-webpack-plugin,webpack4之后extract-text-webpack-plugin不再適用了,官方建議使用mini-css-extract-plugin替代。

  mini-css-extract-plugin需要配合它內置的loader一起使用,所以你需要在之前寫好的utils.js中進行相應的配置,由於development模式下你其實並不需要進行代碼提取/壓縮等操作,所以寫的時候需要增加判斷場景的邏輯:

   utils.js
const path = require('path')
const config = require('../config')
// 引入mini-css-extract-plugin內置的loader
const MiniCssExtractPluginLoader = require("mini-css-extract-plugin").loader
exports.assetsPath = function (_path) {
  var assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory;
  return path.posix.join(assetsSubDirectory, _path)
}
/**
 * 
 * @param {{usePostcss:Boolean}} options 
 */
exports.cssLoaders = function (options) {

  const vueStyleLoader = {
    loader: "vue-style-loader"
  }
  const cssLoader = {
    loader: "css-loader",
    options: {
      // 如果你使用的是vue-style-loader並且css-loader的版本在v4.0.0及以上,下面這個屬性必須配置為false,具體原因請看https://www.cnblogs.com/byur/p/14194672.html
      esModule: false,
    }
  }
  const postcssLoader = {loader:'postcss-loader'}
  // loader解析順序從右至左
  // const baseLoaders = [vueStyleLoader,cssLoader]
  function generateLoaders (loader) {
    const outputLoaders = [vueStyleLoader,cssLoader]
    // 傳入配置中extractCss為true時,插入MiniCssExtractPluginLoader
    if (options.extractCss){
      outputLoaders.splice(1,0,{
        loader:MiniCssExtractPluginLoader,
        options: {
          publicPath: "../../",
        },
      })
    }
    if (options.usePostcss){
      outputLoaders.push(postcssLoader)
    }
    if (loader) {
      const targetloader = {loader:loader+"-loader"}
      outputLoaders.push(targetloader)
    }
    return outputLoaders
  }

  return {
    css: generateLoaders(),
    less: generateLoaders("less"),
    sass: generateLoaders("sass"),
    scss: generateLoaders("sass"),
    stylus: generateLoaders("stylus"),
    styl: generateLoaders("stylus")
  }
}

exports.styleLoaders = function (options) {
  var output = []
  // 透傳options
  var loaders = exports.cssLoaders(options)
  
  for (let extension in loaders) {
    var loader = loaders[extension]
    console.log(loader)
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }
  return output
}
utils.js
   webpack.prod.conf.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
……
    plugins: [
        new MiniCssExtractPlugin({
            filename: "static/styles/[name][contenthash:7].css"
       ignoreOrder: true,
}) ] ……

  至此,在你構建的時候,css代碼會被抽取並以css文件的形式存儲在dist/static/styles路徑中,接下來對css代碼順便加上js代碼進行壓縮,這個過程主要通過配置optimization屬性來完成。對於其中的一些配置,我在注釋里寫了些個人的理解,有不對的地方的地方請多指正。

   webpack.prod.conf.js:
const OptimizeCSSPlugin = require("optimize-css-assets-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin")

    optimization: {
        flagIncludedChunks:true,
        occurrenceOrder:true,
        concatenateModules:true,
        usedExports: true,
        // production模式下以上屬性默認開啟

        // mode:production下minimize屬性默認為true,為true時,若minimizer未配置,則使用terser-webpack-plugin對js代碼進行壓縮優化。
        minimize: true,
        // 在mode:production下js的代碼壓縮是自動開啟的,這里我根據自己的需要,增加了一些額外的配置。為此你需要指定minimizer來分別配置處理css和js代碼的插件。
        minimizer:[
            // 跟官網文檔示例一樣,我使用terser-webpack-plugin作為js代碼壓縮的工具,當然也可以使用第三方的其他插件;需要注意的是terser-webpack-plugin,現在最新版本是v5.x,對應的webpack版本是v5.x,
         在webpack4上使用是會報錯的。
new TerserPlugin({ cache: true, parallel: true, sourceMap: true, // 不單獨提取注釋 extractComments: false, terserOptions: { sourceMap: true, // 從語義上便可理解 compress: { drop_console: true, drop_debugger: true, // pure_funcs接收一個list,指定一些函數,編譯階段去除調用這些函數產生的返回值(如果這些返回值沒有被使用的話),傳console.log時會與drop_console產生相同效果,由於side-effect問題,
                tree-shaking在這時不會生效
// pure_funcs: ["console.log"] }, // 不保留注釋 format: { comments: false, }, } }), // 壓縮css new OptimizeCSSPlugin({ cssProcessorOptions: { // 配置從語義上理解,不解釋了 discardComments: { removeAll: true, }, canPrint: true } }), ], runtimeChunk: true, // splitChunks依托SplitChunksPlugin取代了webpack3及之前版本的CommonsChunkPlugin,配置思路大同小異。 splitChunks: { hidePathInfo: true, cacheGroups: { vendor: { name: "vendor", chunks: "initial", // priority默認是0,以0為基准決定處理bundle的優先級,值越大優先級越高。如果優先級分配不恰當,配置的效果可能不會特別理想。 priority: 0, // 復用在main中已經包含了的模塊。 reuseExistingChunk: true, test: /node_modules\/(.*)\.js/, }, commons: { name: "commons", chunks: "async", priority: -10, reuseExistingChunk: true, }, // 在js分包配置之外,追加樣式緩存組。 styles: { test: /\.css$/, chunks: "all", reuseExistingChunk: true, enforce: true, priority: 10, }, }, maxSize: 1000000, }, }
   看看加上這段配置之后的打包表現:

   統計信息:

  

  css代碼的壓縮效果:

  

  圖片靜態資源壓縮

  這一流程中我使用的是image-webpack-loader,這個loader主要依賴一個叫imagemin的第三方庫,外接了一些庫在node環境下做圖片壓縮,所以下載loader之后會自動下載這幾個庫,有沒有必要使用這個loader我覺得看項目具體需要,對於追求極致的包體積和圖片的加載速度還是有用處的,只是對構建時間影響比較大,image-webpack-loader國內裝依賴比較麻煩,推薦用cnpm裝。稍微有點需要吐槽的是,似乎都是一個團隊出來的工具,為什么每個工具的配置參數還不一樣……

  下面直接上代碼:

   wbepack.base.conf.js
// 從module中抽出處理圖像的loader
const imageLoaders = {
    test: /\.(cur|png|jpe?g|gif|svg)(\?.*)?$/,
    use: [
      {
        loader: 'url-loader',
        query: {
          esModule: false,
          limit: 10,
          name: utils.assetsPath('img/[name].[ext]') 
        }
      },
    ]
};
if (process.env.NODE_ENV === "production" && config.build.imageCompress) {
  imageLoaders.use.push({
    loader: "image-webpack-loader", 
    options: {

      // 處理jpeg
      mozjpeg: {
        quality: 95,
        progressive: true, //官網原文是false creates baseline JPEG file. 不是搞圖像的,不知道baseline意味着什么,就選了默認值true。
      },
      // gif
      gifsicle: {
        interlaced: true, 
      },
      // 將JPG和PNG圖像壓縮為WEBP,我這里的圖基本全是png格式的,所以就沒有對專門處理png圖像的工具做配置,用webp一起處理了。
      webp: {
        quality: 85, // 圖像品質
        method: 5, // 0-6 這個參數控制壓縮速度、壓縮后文件體積,當然也是跟圖像品質掛鈎的,具體細節不是特別清楚
      },
    },
  })
}

……
module: [
    ……
    // 在module中加入imageLoaders
    imageLoaders
]

  看下對比:

  當然這個壓縮的過程其實是比較耗時間的,在本地開發的時候我這邊是不開啟的,因為這樣啟動項目的時候花的時間可能會多一些。

  附:image-webpack-loader可配置的幾個可配置項的配置參數列表:

     https://github.com/imagemin/imagemin-mozjpeg#options

  https://github.com/imagemin/imagemin-opti

  https://github.com/imagemin/imagemin-pngquant

  https://github.com/imagemin/imagemin-svgo

  https://github.com/imagemin/imagemin-gifsicle

 

  多線程(慎用)

  這個操作對我來說一直都是都市傳說,這回有機會親手試一試,主要目的其實就是更充分利用算力,目前據我所知terser-webpack-plugin是默認開啟了多線程的,這點讀者可以在構建的時候打開任務管理器看到,除此之外構建流程中還存在其他耗時過長的工具需要處理,因為happypack已經很久沒更新了,所以這里我在構建流程中選擇加入了thread-loader,嘗試縮減構建耗時。

  這里我截取了一部分thread-loader在webpack官方文檔上的描述,用來湊字數(不是)

Put this loader in front of other loaders. The following loaders run in a worker pool.
// 將thread-loader放置在其他loader之前
Loaders running in a worker pool are limited. Examples:

Loaders cannot emit files.
Loaders cannot use custom loader API (i. e. by plugins).
Loaders cannot access the webpack options.
// 這些loader存在以下限制:
// 這些loader不能生成額外的文件
// 不能使用自定義的loader,比如某個插件配套的loader,比如mini-css-extract-plugin的loader?
// 不能獲取webpack的配置項

  我在反復測試了幾次之后才確定這個loader有點小坑,loader本身效果是存在一定爭議的,這點在國內的各種教程或者帖子沒怎么看到有人提出來,我通過google找到了一篇博客,里邊里邊有提到負優化的問題,使用了loader之后耗時更多了,知乎上的一個老哥也是做過測試的,0配置或者配置不對就負優化,配置對了構建時間也就能提升個一絲半點,切換配置跑了很多遍之后,我算是調整出了一份沒有負優化的配置(當然正面優化也微乎其微,總體花的時間沒什么大的波動),並且不能保證換到另外一個項目也能適用,這里放出來給讀者參考一下:

  多線程處理babel轉譯與圖片壓縮:

  webpack.base.conf.js
const threadLoader = require("thread-loader")
threadLoader.warmup({
  poolTimeout: 1000,
  workerParallelJobs:50,
  poolParallelJobs:500
},["babel-loader"])
threadLoader.warmup({
  poolTimeout: 800,
  workerParallelJobs:50,
  poolParallelJobs:500
  // workers: 6,
},["image-webpack-loader"])
// 從module中抽出處理圖像的loader
const imageLoaders = {
    test: /\.(cur|png|jpe?g|gif|svg)(\?.*)?$/,
    use: [
      {
        loader: 'url-loader',
        query: {
          esModule: false,
          limit: 10,
          name: utils.assetsPath('img/[name].[ext]') 
        }
      },
    ]
};
if (process.env.NODE_ENV === "production" && config.build.imageCompress) {

  imageLoaders.use = imageLoaders.use.concat([
    {
      loader: "thread-loader",
      options: {
        poolTimeout: 1000,
        workerParallelJobs:50,
        poolParallelJobs:500
      }
    },
    {
      loader: "image-webpack-loader", 
      options: {
        // 處理jpeg
        mozjpeg: {
          quality: 90,
          progressive: true, //官網原文是false creates baseline JPEG file. 不是搞圖像的,不知道baseline意味着什么,就選了默認值true。
        },
        // gif
        gifsicle: {
          interlaced: true, 
        },
        // 將JPG和PNG圖像壓縮為WEBP,我這里的圖基本全是png格式的,所以就沒有對專門處理png圖像的工具做配置,用webp一起處理了。
        webp: {
          quality: 85, // 圖像品質
          method: 5, // 0-6 這個參數控制壓縮速度、壓縮后文件體積,當然也是跟圖像品質掛鈎的,具體細節不是特別清楚
        },
      },
    },
  ])
}
const scriptLoaders = {
  test: /\.js$/,
  use: [
    {
      loader: "thread-loader",
      options: {
        poolTimeout: 1000,
        workerParallelJobs:50,
        poolParallelJobs:500
      }
    },
    {
      loader: "babel-loader",
    },
  ],
  include: path.resolve(__dirname, "../src"),
  exclude: /node_modules/,
}

 

  我的項目構建時間因為image-webpack-loader從約110s到約150s(圖片文件數量約為300),加入多線程的配置后構建時間平均在148s,前后的平均數值波動不是特別大,可你要說thread-loader沒生效也不對,可以看到cpu前一分鍾的使用率有一段(大概持續了半分鍾,這段時間是image-webpack-loader在壓縮文件)是明顯提高了不少,但不知道為什么這點提升沒有體現到構建時間上。需要提醒的是,因為多線程的原因,桌面會因為不停啟動image-webpack-loader所依賴的node應用,導致鬼畜地一直彈框,影響正常工作,所以在本地構建的時候建議還是不要開啟多線程使用image-webpack-loader了(或者鎖屏休息一下)

 

 

 

 

  合理使用緩存

  這次介紹cache-loader,用來緩存部分編譯的結果,同樣,你應該只在性能消耗較大的部分使用它。與thread-loader類似,在需要緩存的loader前加上cache-loader即可。

  在css代碼處理過程中使用cache-loader
……
……
……
  function generateLoaders (loader) {
    const outputLoaders = [vueStyleLoader,cssLoader]

    if (options.extractCss){
      outputLoaders.splice(1,0,{
        loader:MiniCssExtractPluginLoader,
        options: {
          publicPath: "../../",
        },
      })
    }
    if (options.usePostcss){
      outputLoaders.push(postcssLoader)
    }
    // 經過測試發現在postcss-loader和mini-css-extract-plugin的loader之前插入cacheloader會報錯,最終選擇在postcss之后插入cache-laoder
    if (options.useCssCache) {
      outputLoaders.push({
        loader:"cache-loader"
      })
    }
    if (loader) {
      const targetloader = {loader:loader+"-loader"}
      outputLoaders.push(targetloader)
    }
    return outputLoaders
  }
……
……
……
utils.js
  在圖像壓縮和babel轉譯過程中使用cache-loader
// 從module中抽出處理圖像的loader
const imageLoaders = {
    test: /\.(cur|png|jpe?g|gif|svg)(\?.*)?$/,
    use: [
      {
        loader: 'url-loader',
        query: {
          esModule: false,
          limit: 10,
          name: utils.assetsPath('img/[name].[ext]') 
        }
      },
    ]
};
if (process.env.NODE_ENV === "production" && config.build.imageCompress) {
  imageLoaders.use = imageLoaders.use.concat([
    {
      loader: "thread-loader",
      options: {
        poolTimeout: 800,
        workerParallelJobs:50,
        poolParallelJobs:500
      }
    },
    // 添加cache-loader
    {
      loader: "cache-loader",
    },
    {
      loader: "image-webpack-loader", 
      options: {
        // 處理jpeg
        mozjpeg: {
          quality: 90,
          progressive: true, //官網原文是false creates baseline JPEG file. 不是搞圖像的,不知道baseline意味着什么,就選了默認值true。
        },
        // gif
        gifsicle: {
          interlaced: true, 
        },
        // 將JPG和PNG圖像壓縮為WEBP,我這里的圖基本全是png格式的,所以就沒有對專門處理png圖像的工具做配置,用webp一起處理了。
        webp: {
          quality: 85, // 圖像品質
          method: 5, // 0-6 這個參數控制壓縮速度、壓縮后文件體積,當然也是跟圖像品質掛鈎的,具體細節不是特別清楚
        },
      },
    },
  ])
}
const scriptLoaders = {
  test: /\.js$/,
  use: [
    {
      loader: "thread-loader",
      options: {
        poolTimeout: 1000,
        workerParallelJobs:50,
        poolParallelJobs:500
      }
    },
    // 添加cache-loader
    {
      loader: "cache-loader",
    },
    {
      loader: "babel-loader",
      options: {
        // 如果只需要對babel-loader的處理結果進行緩存,把cacheDirectory設為true就可以了,這里因為使用了cache-loader,cacheDirectory就沒有做配置,並且本地測試出來使用cache-loader比cacheDirectory=true在構建耗時上的成績會更好
        // cacheDirectory: true,
      },
    },
  ],
  include: path.resolve(__dirname, "../src"),
  exclude: /node_modules/,
}
webpack.base.conf.js

  thread-loader用不用效果都不怎么明顯,還可能會翻車,但cache-loader就真正可以說是立竿見影了,你會看到在對一些性能消耗較大的loader的處理結果進行緩存之后,構建時間有了明顯的縮短(發出了反派一樣邪惡的笑聲):

  使用前

  為image-webpack-loader和babel-loader加入緩存后:

  在樣式解析過程中加入緩存后:

 

  肥大 出 飾拳

  效果拔群

 

  lint機制

  lint是一種針對靜態代碼的檢測機制,用來在編譯前檢測出代碼顯式存在的一些問題,也被用來樹立並且執行一套代碼規范。lint檢測的范圍大概可以分為風格檢查跟質量檢查,打個比方,如果項目里設置了縮進的規則為兩個空格,如果有一行代碼中的縮進是4個空格,那么這個縮進問題就屬於代碼風格上的問題;如果你寫了一個函數,return之后的行里還寫了其他代碼,這種必定不會執行的代碼被lint機制檢查出來時就可以歸類為代碼質量問題。這里我使用eslint主要對代碼質量問題還有小部分代碼風格問題進行了檢查,大部分的代碼風格檢查轉交prettier,用來避免可能會發生的規則沖突(在vue單文件組件中寫代碼時可能會遇到兩種規則沖突導致eslint的警告或者報錯一直存在)。

    eslint(在這之前你需要下載eslint與eslint-loader,在package.json中它們應該歸類到devDependencies):

  我用的編輯器是vscode,所以這一部分我只介紹vscode和webpack上關於eslint的一些配置。

  首先在根目錄下創建一個名為.eslintrc的配置文件(json)用來描述eslint使用到的工具、插件和具體的代碼規則:

  你需要下載配置文件中提到的依賴,否則這些在構建過程中或者構建結束后會產生相應的報錯或警告

module.exports = {
    // 首先你需要確定檢查范圍,root默認值為true,從根目錄開始
    root: true,
    // 這里需要聲明運行環境
    env: {
      node: true,
      es6:true
    },
    // 設置語法選項和解析器
    parserOptions: {
        parser: "babel-eslint",
        "ecmaVersion": 6
    },
    // extends可以理解為繼承某一套配置,recommended集成了eslint的核心規則,這里推薦只要是用eslint就要加上這個
    extends: ["eslint:recommended","plugin:vue/recommended", "@vue/prettier"],
    // 自選規則,根據需要配置,詳細請看https://cn.eslint.org/docs/rules/
    rules: {
        "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
        "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
        // Stylistic Issues
        "no-multi-spaces":["error", { ignoreEOLComments: false }],
        // ECMAScript 6
        "prefer-const": ['warn',{
          "destructuring": "any",
          "ignoreReadBeforeAssign": false
        }],
        "arrow-spacing": ['warn',{ "before": true, "after": true }],
      },
}
  設置一個白名單.eslintignore
dist
src/library
node_modules
static
// ...

  在webpack配置中添加eslint-loader(我這里只在本地開發環境下使用eslint,本地環境下自己寫的代碼語法報錯/警告不解決還敢提代碼那就屬於態度問題了)

   webpack.base.conf.js
const createLintingRule = () => ({
  // 本來檢查范圍內包括JS文件,但因為目錄中有些js文件為第三方庫,在此不做解析,只在保存js文件時使用插件去格式化
  test: /\.(js|vue)$/,
  loader: "eslint-loader",
  enforce: "pre",
  // 可以通過include屬性規定作用范圍
  // include: /src/,
  options: {
    formatter: require("eslint-friendly-formatter"),
    emitWarning: !config.dev.showEslintErrorsInOverlay
  }
});

……
……
……
rules:[
    // config.dev.useEslint = true
    ...(config.dev.useEslint ? [createLintingRule()] : []),
……
……
……
]

 

  prettier(需要下載依賴)

  全部選項請看這里

  .prettierrc(json):

 

{
  "eslintIntegration": true,
  "singleQuote": false,
  "bracketSpacing": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "semi": true,
  "quoteProps": "as-needed"
}

 

  如果你需要編輯器替你提示出來你需要做什么、怎么改、或者設置保存自動修復,你還可以到vscode的拓展中下載eslint插件,eslint會嘗試修復可修復的問題(在https://cn.eslint.org/docs/rules/中會有特殊的圖標標注出來);也可以通過設置npm命令"eslint --fix"來進行批量修復,只是有時候修復的結果會出乎你的預期,這可能會引發其他邏輯上的錯誤,所以如果你是項目開發的中途引入的eslint,要謹慎使用fix命令。

   .vscode/settings.json
{
    // tab長度
    "editor.tabSize": 2,
    // 一行字符數量
    "editor.rulers": [120],
    // 行號
    "files.eol": "\n",
    "editor.lineNumbers": "on",
    // 代碼提示
    "editor.snippetSuggestions": "top",
    // 保存自動修復
    "editor.codeActionsOnSave": {
        "source.fixAll": true,
    },
    // vetur配置
    "vetur.format.options.tabSize": 4,
    "vetur.format.scriptInitialIndent": false,
    "vetur.format.styleInitialIndent": true,
    "vetur.format.defaultFormatter.html": "prettyhtml",
    "vetur.format.defaultFormatter.js": "prettier",
    "vetur.format.defaultFormatterOptions": {
        "js-beautify-html": {
            "wrap_line_length": 120,
            "wrap_attributes": "auto"
        }
    },
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        "vue",
    ],
    "eslint.options": {
        "extensions": [".js",".vue"]
    }
}

  附:eslint在各種平台上的集成的入口:https://eslint.org/docs/user-guide/integrations

 

  本來還想寫些錦上添花的優化比如美化控制台輸出、端口防重(portfinder)的,但想了想其實這些東西本來也沒什么門檻,而且不是每個人都需要這些東西,寫在這里無疑讓這篇本來就夠水的文章更加乏味,所以打算就在這里結束。

  之前計划的時候還有一篇草稿叫~-毅力篇,因為第一次做的時候思路不太清晰,遇到了很多很多很多問題,本來起名為毅力篇是想用來記錄報錯的解決方案的,但寫這篇文章復盤升級操作的時候沒有歷史包袱,從零開始,沒想到過程還是比較順利的,所以毅力篇的草稿估計是沒有放出來的必要了。

  十一月底來南京出差到今天一直在997,空閑時間不是特別穩定(或者可以說特別穩定地少),但這篇博客確實是花了很多時間,主要是懶,還有怕被噴爛所以為了保證數據的真實性翻來覆地改配置然后跑項目,雖然我自己對這種便秘式的更新並不感到羞愧,但無論是寫文章還好,寫代碼還好,我對這段時間自己的狀態確實不滿意,總之希望以后能夠學習和分享更有意義的內容。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM