1. 程式人生 > >基於vue2.x的webpack升級與專案搭建指南--基礎篇

基於vue2.x的webpack升級與專案搭建指南--基礎篇

first thing fitrst 博主宣告:絕對不當標題黨
有人看最好不過的背景:

  十月初對公司產品的前端構建做了一些優化,但還遺留了不少問題(可瞭解我的前一篇博文:一次webpack小規模優化經歷 https://www.cnblogs.com/byur/p/13977657.html),這裡姑且列了一個表出來記錄當前這個版本的不足:

  1.熱過載過慢:單檔案改動,熱過載的十次平均響應時間約為17s,嚴重影響開發體驗;

  2.某些bundle體積過大,導致單個資源請求耗時過多,瀏覽器載入速度收到影響;

  3.沒有liint機制去控制編碼過程中的語法規範,也沒做程式碼儲存的自動格式化,程式碼質量低,組員編碼風格迥異、交接成本高。

  4.打包體積與打包速度仍有優化空間。

  綜合考慮以上問題後,個人判斷webpack1的效能不足以為前端專案的構建流程提供更好的支援,遂決定把webpack升級到更高版本,用新特性與更強的效能,改善構建體驗,造福運維與測試同事hhhh。

 

這次基本沒圖的正文:

  本系列博文將演示如何將webpackv1.x(1.13.2)升級到v4.x(4.44.2),選擇這個小版本的原因是因為它是webpack4的最新一個小版本(2020.11),webpack4從釋出測試版本到現在為止已經有兩年多了,兩年裡的迭代和bug修復,足夠讓這個大版本的功能變得完善和穩定到讓人信任的程度。至於升級的手法,我認為在原來配置的基礎上做修改逐步升級,極有可能會被原來的寫法誤導,導致浪費時間,所以這次升級過程中我換了一種思路,具體的做法是做備份之後刪除原來的配置檔案,從零開始進行升級,因此本文興許也可以當作一個用webpack構建專案的入門教程。

  package.json裡有個devDependencies,記錄了專案在開發環境下需要的依賴,在做好檔案備份後,我將node_modules刪除,將devDependencies的列表清空;然後npm i。

  然後我開始實現一個最簡化的版本,我裝上了4.x版本最新的webpack:

npm i [email protected] -D

   webpack4.x版本需要命令列工具才能執行,所以我們還需要去下載webpack-cli,我就隨便選了一個不算新也不算舊的版本:

npm i [email protected] -D

  然後開始寫配置檔案,首先寫一個基本版的測試一下新版本webpack的可行性:

  先建立一個簡單的入口檔案test.js供打包用:

1 import {cloneDeep} from "lodash"
2 const obj = {color:'red'}
3 const copy = cloneDeep(obj)

  在專案根目錄下建立webpack.config.js檔案:

1 const path = require('path');
2 module.exports = {
3     entry: "./src/test.js",
4     output: {
5         path: path.resolve(__dirname,"dist"),
6         filename: 'testbundle.js',
7     }
8 }

  webpack啟動時,如果未指定執行的檔案,就會自動讀取根目錄下的webpack.config.js中的配置,現在修改package.json的scripts中的build命令:

  "scripts": {
    "build": "webpack"
  },

  執行npm run build,webpack便會按配置進行打包,然後你會看到你的dist目錄中多出一個名為testbundle.js的檔案。

  同時,按照我這個配置,會在控制檯到看到一個警告:

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

  這個警告表示,當前傳給webpack的配置中沒有設定“mode”屬性,對此webpack將視作mode: "production"來處理,這裡需要提到webpack4配置檔案中的mode屬性,webpack4內建了一些比較通用的外掛配置,省去了開發者為配置webpack而消耗的時間精力,使用mode屬性就可以快捷配置這兩套外掛,mode:"development"跟mode:"production"分別就對開發環境與生產環境兩種場景做了優化,比如持久化快取、程式碼壓縮等,如果你不想使用這兩種預設的任意一種,可以將mode的值設為"none"。至於其他細節,感興趣的朋友可以從文件獲取更多資訊:https://www.webpackjs.com/concepts/mode/ 

  剛才演示的配置檔案,靈活性與效能都遠遠達不到真實工作場景的需求,只能稱作玩具,所以接下來你將接觸更具體也更接近實際場景的配置。

 

  現在,我們把配置檔案改一改,常規的思路是將開發環境的配置跟產品環境的配置分離成兩個檔案,一般命名為webpack.dev.conf.js和webpack.prod.conf.js,因為這兩個場景下的配置都有部分共同之處,所以又可以抽出一個公共的配置檔案webpack.base.conf.js,目前我們先不去考慮生產環境與開發環境下的差異,先建立一個基本配置webpack.base.js,讓webpack能夠正確地解析一個vue檔案,順利完成打包。

  首先解析.vue檔案,需要安裝vue-loader以及與vue同版本號的vue-template-loader,這裡需要注意的是vue-loader版本如果在15及以上,需要額外從vue-loader的目錄裡引入VueLoaderPlugin,VueLoaderPlugin將使用你在rules中定義的其他規則來檢查和處理.vue檔案中符合規則的語句塊

const VueLoaderPlugin = require("vue-loader/lib/plugin");
...
...
...
module: {
  rules: [
    {
      test: "/\.vue$/"
      loader: "vue-loader"
    }
  ]
}, plugins: [ new VueLoaderPlugin() ]

  

  如果這時候你已經看到本文的更下面並且寫好了build檔案,或者是在webpack.config.js的基礎之上改寫配置檔案,此時執行打包命令你將會發現控制檯輸出了很多錯誤,例如:

  

  滿屏的紅字有些嚇人,但仔細看看就會發現其實並不是什麼大不了的問題,截圖上有一段樣式程式碼,並且報錯提示你可能需要其他loader去處理vue-loader的解析結果,所以為了解決截圖上的問題能,使webpack能夠順利對樣式程式碼進行處理,你需要新增相應的loader,新增什麼由你的專案具體使用情況決定:

npm i css-loader style-loader url-loader file-loader less less-loader sass node-sass stylus stylus-loader -D

  css-loader用於解析css程式碼,style-loader則生成style標籤將css掛載在到頁面結構中,file-loader讀取靜態資源的引用路徑,在輸出目錄中生成符合規則的檔案,供編譯後的程式碼使用,url-loader在file-loader的基礎之上,將體積小於指定數值的檔案轉碼成base64字串,可通過這種方式減少資源請求數。其他檔案其他loader以及相關依賴不再贅述。

  

  css相關loader的載入我沿用了專案之前的寫法(反正也是從別的地方抄來的),稍微加了些改動:

 

exports.cssLoaders = function () {

  // style-loader改為使用vue-style-loader,除了具備與style-loader一樣的功能之外,還實現了不需要頁面重新整理的樣式層面的熱過載(來自vue-laoder官網描述)

  const vueStyleLoader = {
    loader: "vue-style-loader"
  }
const cssLoader = { loader: "css-loader",
   // 如果你使用的是vue-style-loader並且css-loader的版本在v4.0.0及以上,這個屬性需要加上,具體原因請看https://www.cnblogs.com/byur/p/14194672.html
   
   options: {
   esModule: false
   }
  }
  // 當在一條規則中應用多個loader時,loader的執行順序從右至左,所以預處理語言相關的loader擺右邊 
 // 如果generateLoaders沒有接收到引數,將以返回基礎的loader配置:使用css-loader與vue-style-loader
  function generateLoaders (loader) {
    const outputLoaders = [vueStyleLoader,cssLoader]
    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 () { var output = [] var loaders = exports.cssLoaders() for (let extension in loaders) { var loader = loaders[extension] console.log(loader) output.push({ test: new RegExp('\\.' + extension + '$'), use: loader }) } return output }

   

  這個丐版的styleLoaders輸出了一個儲存了處理樣式檔案規則的陣列,你可以使用拓展運算子將這些規則掛載到rules中。所以接下來我們建立一個build.js,用這個檔案調起webpack的api進行打包。我比較傾向於這種寫法,用命令列呼叫node執行一個build檔案,在這個檔案中執行webpack,這樣寫在處理不同打包配置的場景時要稍微方便一些,比如有的公司就分sit、uat、prod(生產)等好幾套環境,會對應不同的全域性配置(如介面的的baseurl、請求加解密、區域性打包等等),這種情況下可以通過process.argv來獲取命令列引數,細化配置;對於我來說另外一個好處是方便加old_space引數給記憶體擴容,這樣能避免一些稍大的專案執行過程中出現記憶體不夠導致編譯失敗的問題(64位windows給node分配的記憶體大概是1.4G)

process.env.NODE_ENV = 'production'

var webpack = require('webpack')
var webpackConfig = require('./webpack.prod.conf')


webpack(webpackConfig, function (err, stats) {
  // spinner.stop()
  if (err) throw err
  process.stdout.write(stats.toString({
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false
  }) + '\n')
})
丐版build.js

  

  把build命令改寫為:

  

"build": "node --max_old_space_size=2077 build/build.js"

 

  現在執行npm run build,看看會發生什麼:

  

  過程沒報錯,一個基礎的打包流程,到現在其實就走完了,這個包實際上也不能用,但我認為基礎篇的意義在於引導讀者順利完成第一步,在這個前提之上進行功能的豐富,這樣的話無論是操作失誤回退程式碼或者是對優化方向的梳理都有一定的積極意義。

  基礎篇到這裡就該結束了,在進階篇,我將展示一個完成度更高的版本。

  

  附:
var path = require('path')
var config = require('../config')
var utils = require('./utils')
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  mode: "none",
  entry: "./src/module/indexApp/index.js",
  output: {
    path: config.build.assetsRoot,
    publicPath: config.build.assetsPublicPath,
    filename: utils.assetsPath('js/[name][hash].js'),
    chunkFilename: utils.assetsPath('js/[id][chunkhash].js')
  },
  resolve: {
    extensions: ["*",'.js', '.vue'],
    alias: {
      'vue$': 'vue/dist/vue',
      'src': path.resolve(__dirname, '../src'),
      'common': path.resolve(__dirname, '../src/common'),
      'components': path.resolve(__dirname, '../src/components'),
      'components2': path.resolve(__dirname, '../src/components2'),
      'module': path.resolve(__dirname, '../src/module'),
      'config': path.resolve(__dirname, '../src/config'),
      'library': path.resolve(__dirname, '../src/library'),
      'jsplumb': path.resolve(__dirname, '../src/library/jsplumb.js'),
      'echarts-wordcloud': path.resolve(__dirname, '../src/library/echarts-wordcloud')
    }
  },
  module: {
    rules: [
      ...utils.styleLoaders(),
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.(cur|png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        exclude: [
          path.resolve(__dirname, '../src/components/icon'),
        ],
        query: {
          limit: 10,
          name: utils.assetsPath('img/[name].[ext]') 
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        query: {
          limit: 10000,
          name: utils.assetsPath('fonts/[name].[ext]')
        }
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]
}
webpack.base.conf.js
var path = require('path')
var config = require('../config')

exports.assetsPath = function (_path) {
  var assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory;
  return path.posix.join(assetsSubDirectory, _path)
}

exports.cssLoaders = function () {
  const vueStyleLoader = {
    loader: "vue-style-loader"
  }
  const cssLoader = {
    loader: "css-loader",
  }
  // loader解析順序從右至左
  // const baseLoaders = [vueStyleLoader,cssLoader]
  function generateLoaders (loader) {
    const outputLoaders = [vueStyleLoader,cssLoader]
    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 () {
  var output = []
  var loaders = exports.cssLoaders()
  
  for (let extension in loaders) {
    var loader = loaders[extension]
    console.log(loader)
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }
  return output
}
utils.js
process.env.NODE_ENV = 'production'

var webpack = require('webpack')
var webpackConfig = require('./webpack.base.conf')


webpack(webpackConfig, function (err, stats) {
  // spinner.stop()
  if (err) throw err
  process.stdout.write(stats.toString({
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false
  }) + '\n')
})
build.js