webpack4.x升級摘要

圖片描述

一:前端工程化的發展

好久之前,互聯網行業有個職位叫作 「軟件開發工程師」 在那個時代,你們可能會蒙,放在現今社會,你們會以爲這個職位太過籠統,因此在此提一下那個時候的互聯網行業狀態。javascript

  • 那個時候的APP還不是ios/安卓 多數是嵌入式應用(和網頁不要緊 使用c++或java開發)後續到了Symbian時代出現了可移植的sis[x]類型應用,曾經一統移動APP市場。
  • 那個時候css百分之90仍是用來作佈局
  • 那個時候js僅僅是爲了相似彈出提示的功能而存在的
  • 那個時候從服務器 - 數據庫 - 業務邏輯 - 頁面 全都由所謂的 「軟件開發工程師」 來完成
  • 因此你們沒必要問軟件開發工程師具體是幹啥的,我只能說 啥都幹

1:我的對前端開發的體驗過程

第一個階段就是開始對着W3C的文檔,拿着txt文本文件一個字母字母的敲着代碼,那個年代,真的單純舒服,上來就是一個項目的文件夾,而後就開始img、js、css三個完美的文件夾,再接上一個index.html,就開始到網上各類下載類庫jquery、underscore.js,而後手動的引入各類類庫,固然過程也伴隨着痛苦。php

  1. 每次來一個項目就開始創建各類繁瑣的文件夾,和拷貝複製類庫
  2. 引入完類庫的時候發現控制檯報錯,$ is not defined,依賴關係出錯,部分類庫須要把jquery做爲依賴
  3. 新寫一個頁面就須要去從新複製其餘頁面的header資源,維護變的困難(徹底沒有組件化的想法)
  4. 部分css3屬性須要本身不斷的手動添加樣式前綴
  5. 最大的問題再維護的時候,不敢輕易的去動一些類庫的引入,不清楚各個庫以前的依賴關係

第二個階段就是在接觸到Vue的時候直接上vue-cli的時候,幾行腳本就能夠啓動本地開發服務和打包線上資源,初次嘗試了webpack這個打包工具,好像對其餘問題不須要作過多的考慮,直接開始業務開發,其餘的事情cli都幫助處理好了。vue init webpack helloWorld, cd helloWorld && cnpm install && npm run dev,真香警告,對工程化只知其一;不知其二,可是好用,方便css

  1. 前端須要一些工具來處理重複的大量繁瑣的事 (資源壓縮 代碼混淆 css前綴 ),之前會用Gulp等Task處理任務
  2. 前端須要一些須要用一些預處理器來處理樣式文件,less之前用第三方工具去編譯 -> less提供的命令行去編譯-> 配置到webpack中自動化實時編譯
  3. 前端須要更加細粒度的配置一下代碼體積的優化,代碼混淆壓縮的操做
  4. 前端部分業務越多,代碼量越多(文件體積越大)須要作文件合併,壓縮以及按需加載等
  5. 開發階段依然在本地開發,但同時保持和線上API的同步,爲此咱們須要本地服務器(能夠是靜態服務器)代理轉發(Nginx webpack-dev-server node-server等方式)

第三個階段就是給公司項目升級webpack過程當中體驗到了更細粒度的控制工程,體驗工程化的過程,實際的體驗到了工程化爲咱們作的事情html

  1. 模塊化開發,不用在擔憂以往開發方式帶來的全局做用域的污染問題,固然以往也能夠經過閉包來實現私有變量的概念
  2. 組件化開發,代碼重用度高,便於維護
  3. 多了構建,編譯過程,能夠在適當的時間去作一些提升工程質量的任務,好比代碼規範的檢測,經常使用的是Eslint (Airbnb, Prettier 規範等)
  4. 提升開發效率,好比css瀏覽器前綴的自動添加,使用postCss甚至能夠提早使用一些好用的東西,好比css變量的概念
  5. 經過chunkHash,contentHash 等實現資源的緩存
  6. 根據工程代碼經過合理的代碼分割提高用戶體驗,作到按需加載,甚至能夠在將來作一些用戶使用的習慣,作一些提早的預加載

二:webpack的基本使用

1:webpack和Grunt / Gulp的區別

這兩類是不一樣的東西,一個能夠理解爲是任務執行器而另一個是模塊打包器,任務執行器是能夠自動化的執行一些之前你須要手動操做的過程,見下面簡單的代碼前端

// coffee 源碼
console.log 'Hello World'
  
//coffee轉 js coffee -c a.coffee
  
(function(){
  console.log('Hello World');
}).call(this);
  
//執行編譯壓縮 uglify -s a.js -o a.min.js
(function(){console.log("Hello World")}).call(this);

coffee須要編譯成瀏覽器支持的js,你須要手動的去執行上面的幾個命令,若是如今須要去修改源碼,在Hello World 後面加上一個!,加完你須要手動的去執行兩條命令重複的去編譯操做。以 gulp 爲例,編寫 gulpfile.js來幫助咱們完成重複的工做vue

gulp   = require('gulp')
coffee = require('gulp-coffee')
uglify = require('gulp-uglify')
rename = require('gulp-rename')
  
file = './src/js/a.coffee'
  
gulp.task('coffee', function(){
    gulp.src(file)
        .pipe(coffee())  // 編譯
        .pipe(uglify())  // 壓縮
        .pipe(rename({
            extname: ".min.js"
        }))
        .pipe(gulp.dest('./build/js'))
  
gulp.task('watch', function(){
    gulp.watch(file, ['coffee'])
})
  
gulp.task('default', ['coffee'])

我只要執行一下 gulp watch 就能夠檢測到coffee文件的變化,而後爲你執行一系列的自動化操做,一樣的操做也發生在less, scss, 這些css的預處理器上。在修改到源文件的狀況下的編譯,壓縮這些重複操做都交由它來完成,在我看來Grunt / Gulp 算是一個可以自動化執行一些繁瑣重複的操做,提升生產效率,算是一個任務執行器。java

這些操做一樣也能夠由webpack完成,接下來咱們看一下官網給出的webpack的定義。node

webpack is a module bundler

官方給出的解釋是,webpack是一個模塊打包器,不是一個任務執行器,它是能夠配合Grunt / Gulp 使用的,相關連接webpack集成 。webpack打包器(bundler)幫助你生成準備用於部署的 JavaScript 和樣式表,將它們轉換爲適合瀏覽器的可用格式。例如,JavaScript的壓縮chunk split懶加載,以提升性能。因此webpack和Gulp/Grunt之間是有必定功能的重疊,可是處理合適,是能夠一塊兒配合工做的,不存在所謂的誰替代誰,只是在某些場景下webpack的可以獨當一面,完成了Grunt/ Gulp的功能。jquery

2:webpack3.x 和 webpack4.x 的對比

rollup以及Parcel的出現,號稱零配置,足以讓一個配置成本比較高的webpack出現了4.0版本,固然也號稱零配置使用,開箱即用,如今來一塊兒看看webpack4.x和3.x比較大的區別,先給出一個Release連接webpack Release v4.0.0下面簡要的介紹一些大的改動webpack

  1. Node環境的升級,不在支持node 4.0的版本,最低支持6.11.5
  2. 配置增長了mode:'production', 'development', 'none' ,所謂的開箱即用的支持點,在不一樣的mode下開啓了一些默認的優化手段
  3. 生產模式開啓了各類優化去生成bundle文件、默認開啓做用域提高、process.env.NODE_ENV設置爲production
  4. 開發模式優化內部rebuild流程,提高開發效率和體驗
  5. 刪除了和添加了一些配置項,NoEmitOnErrorsPlugin、ModuleConcatenationPlugin(default in production mode)、NamedModulesPlugin(default in development mode) ---> 轉到optimization.*的配置項
  6. 原生支持處理JSON文件格式,不須要json-loader
  7. 內置的插件 uglifyjs-webpack-plugin 升級到了 V1, 而V1是能夠支持並行處理壓縮混淆JS的,webpack3以前的內置依賴的版本0.4.6 不支持並行處理。經常使用手段使用webpack-uglify-parallel插件並行處理,利用多核CPU的優點,升級到webapck4能夠不須要了,使用默認也能夠
  8. 一個算是比較大的改動,webpack3.x的ComoonsChunkPlugin廢棄,代替的是optimization.splitChunks 和 optimization.runtimeChunk (會在下文着重介紹)

3:webpack4.x 的一些基本概念

首先先看下圖總體瞭解一下webpack的一些經常使用配置項

圖片描述

接下來簡單的瞭解一下webpack的一些基本概念

  1. mode:三種模式,production、development、none。設置mode,webpack會根據mode作相應的優化
  2. entry:入口,webpack會從入口遞歸尋找全部的依賴,造成依賴關係圖(dependency graph)。目前應用主要分爲單入口和多入口,直觀上表現爲通過webpack處理以後是一個JS文件仍是多個JS文件(在沒有代碼分割以及懶加載的前提下)
  3. output:輸出,主要經過filename來定義生成chunk的命名,能夠經過標識符[name]、[contenthash]、[chunkhash]來實現資源的緩存,chunkFileName針對async chunk
  4. loader:loader用於對模塊的源代碼進行轉換。既然是模塊打包器,那麼就會出現依賴各類各樣的文件和內容,圖片,字體,它們須要通過編譯才能被瀏覽器識別(less、sass,、stylus、ES201五、ts等module) ,都須要經過對應的loader轉換成現代瀏覽器支持的東西。loader支持鏈式傳遞,loader運行在Node.js中,而且可以執行任何可能的操做,好比存在一些不是用來轉換文件的loader,thread-loder(多個node進程處理loader轉碼文件,提升編譯速度)
  5. plugin:插件和loader不一樣的地方在於,loader是針對模塊,好比import以及require的module文件進行轉碼。plugin是在webapck的complier整個生命週期中起做用,在這個編譯階段你能夠在提供的hook中執行你須要的任何操做。好比htmlWebpackPligun插件,能夠在webpack編譯emit文件的鉤子中,生成html去使用這些webpack生成的JS Chunk
  6. module:module的概念能夠理解爲一個個須要加載的文件,Js也好,Css文件也好,都須要通過loader處理,module裏面的rules去就是配置module須要什麼loader去處理
  7. chunks:兩種,init chunks(這些chunks文件是會以script標籤添加到htmlWebpackPlugin生成的html文件中,固然也能夠經過插件內置到html文件中,好比mainifest文件)和async chunks。初始化chunks是從提供給webapck的entries中開始遞歸的尋找依賴的module,生成的一個chunks。異步的chunks能夠理解爲是須要按需加載的,主要能夠分爲如下3個來源:第1、從初始化的chunk中抽出去的代碼,造成的chunk文件(這個就是webapck的splitChunk和runtimeChunk的配置)。第2、能夠經過webpack識別的特定語法require.ensure (vue-router中懶加載的寫法)。第3、ES6的動態導入import(/chunkname/)
  8. optimization:這是webpack4.x出現的一個優化類的配置項,經常使用配置項:splitChunk、runtimeChunk(下文介紹)、minimize、noEmitOnErrors、namedModules、sideEffects(配合tree shaking使用)等等
  9. 圖片描述
    resolve:如上圖,這個配置會增量的告知webpack如何的去尋找依賴,alias(避免一些深層次引用module代碼的別名)、modules(從哪些目錄尋找依賴)、extensions(module的擴展名)、mainFields(第三方類庫存在多個版本的時候,優先使用哪一個版本)。alias能夠手動指定第三方庫的使用,好比當Vue沒有用CDN的時候,若是從node_modules引用,引入的代碼是runtime運行時的代碼,是沒有包含解析單文件.vue的template部分的功能,這個時候須要依賴它的其餘版本,手動指定,常見於用Vue-cli去生成項目架構的時候,發現alias默認有一項,告知webpack引入Vue的時候module的位置是vue/dist/vue.esm.js(包含了解析template的代碼)。mainFields的使用是針對第三方類庫使用各類模塊化寫法以及語法。有ES6的mport、export的,也有CommonJs的模塊導出。有壓縮的min.js也有Ts版本的,這些會在package.json中看到,至於引入第三方模塊的引入那個版本,對於一些成熟的類庫好比Vue,Vue-router等有多個版本,能夠經過設置mainFields告知webpack從package.json中的那個字段導入類庫。ES6的improt、export存在靜態分析,配合tree shaking使用,這也是webpack號稱能提速98%的緣由,可是目前的情況是第三方庫良莠不齊,不少都沒有提供ES6模塊導出的版本,全部目前效果還不是很理想
  10. externals:指定外部擴展。從bundle中排除依賴,好比項目一些基本不會變動版本的第三方類庫,經過引用CDN資源,常見Vue、VueRouter、element-ui、echart等等類庫
  11. devServer:開發模式的配置。在webpack4.x以前的版本經過node的express框架搭建的本地服務器,配合webpack-dev-middleware和webpack-hot-middleware搭建的開發環境。如今能夠經過webapck-dev-server類庫結合devServer配置項去開啓本地開發環境,這個類庫封裝了express的操做,同時內部使用了webpack-dev-middleware。給出配置連接:devServer配置項

三:項目webpack升級流程

先簡短的介紹一下項目,升級的項目爲多入口項目,每一個module模塊代碼一個入口,而後共用不少業務組件

*/build
    webpack.base.conf.js   // dev和prod共用部分
    webpack.dev.conf.js     // dev模式下特有配置
    webapck.prod.conf.js   // prod模式下特有配置
*/common
    components // 多個模塊共用的組件
    assets // 靜態資源    
*/src
    module1  // 模塊1的代碼
        index.js  // 模塊1代碼的入口
    module2  // 模塊2的代碼
    module3  // 模塊3的代碼
*/mock
    mock-xxx.js  // mock api的接口
*/config // 配置文件
    index.js
    dev.js
    prod.js

1:package.json依賴的管理

升級webpack,安裝webpack-cli。全局安裝npm-check-updates,查看package.json中可升級的依賴的版本。

cnpm install -g npm-check-updates  //全局安裝
  
// 在項目根目錄下執行ncu
ncu
  
// 可升級的依賴,列舉部分依賴狀況
axios 0.17.1 --> 0.18.0
webpack  3.5.5 --> 4.16.5
webpack-merge 4.1.0 --> 4.1.4
  
// 升級webpack 安裝webpack-cli
cnpm install webpack webpack-cli --save-dev
  
// 升級相應的loader和plugin
cnpm install url-loader file-loader vue-loader sass-loader css-loader babel-loader html-webpack-plugin --save-dev

2:修改webpack配置

公司使用的vue-cli2.0的腳手架生成的項目,簡單的列舉關於webpack的目錄,針對須要可進行自行調整

*/build
    webpack.base.conf.js   // dev和prod共用部分
    webpack.dev.conf.js     // dev模式下特有配置
    webapck.prod.conf.js   // prod模式下特有配置

// 經過webpack-merge合併配置輸出最後的webpack配置

1:增長mode模式

// webpack.dev.conf.js 開發模式的配置 開啓webpack默認的配置優化
mode: 'development'
  
// webpack.prod.conf.js 生產模式的配置
mode: 'production'

2:升級vue-loader,vue-loaderv.15版本和以前的有所區別,vue-loader不在使用自身的配置,而是解析.vue文件以後使用webpack裏配置的loader,詳細文檔見Vue-loader的使用,補充一點:在.vue 文件中style提供的scoped標記,就是經過vue-loader去實如今template中加入了適當的hash,配合樣式去作到組件內樣式的獨立

// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')  // 插件解析.vue文件把template script style 分別交給webpack配置的loader去處理
 
module.exports = {
  // ...
  plugins: [
    new VueLoaderPlugin()
  ]
}

3:部分webpack的插件已經配置停用。若有的話,按照提示刪除掉,好比module裏的loaders,webpack4再也不支持。而後部分插件轉化爲經過optimazation 進行配置,主要配置點在於code split以及mainifest文件的提取,一樣見下文splitChunks和runtimeChunks的分析

4:開發模式加入devServer。項目是否須要經過手動node搭建本地服務器,取決因而否須要node層面去處理其餘東西,在公司項目中啓動服務前有兩個操做,編譯scss文件(皮膚文件),以及mock文件夾(mock接口),因此仍是保留了手動檔搭建node層面啓動服務,其實徹底能夠有webpck-dev-server的before,after配置項完成

// 下面代碼塊針對devServer的部分配置項作說明,結合相關知識進行分析
devServer: {
   contentBase: path.join(__dirname, '../static'),   // 靜態資源提供,不是webpack生成的bundle,生成的bundle在內存中見註釋1
   host: host || 'localhost',  // 主機名,若是你但願服務器外部可訪問,須要設置爲0.0.0.0,默認localhost
   port: process.env.PORT || port, // 端口號
   historyApiFallback: true,      // handle fallback for HTML5 history API 瞭解一下這個東西見註釋2
   proxy: proxyTable,    // 後端接口轉發見註釋3
   hot: true,   // 熱更新見註釋4
   quiet: true,  // webpack打包信息省略
   publicPath: assetsPublicPath,  // bundle 的位置 outpath: publicPath 相似
   clientLogLevel: "none"  // HRM WDS 在瀏覽器控制檯的輸出
}
  
// 註釋1:寫過express的就知道有一個指定static中間件用來指定資源目錄的
const express = require('express')
const app = express()
  
app.use(staticPath, express.static(xxx))  // xxx/js/vendor.js,就能夠經過localhost:prot/staticPath/js/vendor.js訪問相關靜態資源。因此contentBase就是利用express靜態中間價提供個一種訪問靜態資源文件的能力
  
// 註釋2:vue-router的history模式充分利用 history.pushState API 來完成 URL 跳轉而無須從新加載頁面 example: history.pushState({a:1}, '測試', '/attendance/index') ,而後內部去處理相應的router對象的展現Vue頁面和邏輯,因此這就是你順着程序點路由能夠進去,可是刷新的時候,就顯示404的緣由,由於該路由在服務器上不是真實存在的,而是在index.html中經過JS去解析模擬的,這就須要咱們生產模式下生產的dist文件全部的請求都轉發到index.html。處理方式:在服務器上經過nignx 代理,或者起一個express服務,經過第三方類庫 connect-history-api-fallback,固然也能夠原生Node去寫,此時Node只是一個文件服務器
  
const http = require('http')
const fs = require('fs')
const httpPort = 80
 
http.createServer((req, res) => {
  fs.readFile('index.htm', 'utf-8', (err, content) => {
    if (err) {
      console.log('We cannot open "index.htm" file.')
    }
 
    res.writeHead(200, {
      'Content-Type': 'text/html; charset=utf-8'
    })
 
    res.end(content)
  })
}).listen(httpPort, () => {
  console.log('Server listening on: http://localhost:%s', httpPort)
})
  
// 註釋3:請求代理,經常被問起跨域請求有哪些方案,jsonp、CORS(後端配合)、window iframe等方式,還有一個就是經過node代碼轉發請求,服務端請求不存在跨域的概念,webpack中能夠node本身使用http-proxy-middleware 去進行代理,本次更新使用的wbepack-dev-server模塊同時也依賴了http-proxy-middleware的庫,目前Node不做爲純後端,而是做爲中間代碼,鏈接純後端(java php)和前端。
  
// 註釋4:熱更新,它容許在運行時更新各類模塊,而無需進行徹底刷新,目前的公司項目都是純刷新的方式,熱更新HRM,這個是須要你是用的loader或插件幫助你完成,他們能監聽webpck complier期間得鉤子,而後給出相應源碼更新後須要patch,推送到前端,打補丁,而後實現熱更新,而不是刷新整個頁面去從新加載頁面。好處天然提升開發效率,在修改.vue文件的tempalte和style以及script中不是vue生命週期函數時,是可以保留到當時的vue的各類狀態

5:針對生產模式的優化配置,廢棄生產模式中使用的優化插件,轉爲webpack4的optimization.* 配置

  • NoEmitOnErrorsPlugin // 編譯錯誤跳過編譯階段,不生成文件 (default in production)
  • ModuleConcatenationPlugin // 做用域提高,加快代碼執行 (default in production)
  • NamedModulesPlugin // 默認的module在打包進入chunks的時候都會以module.id 爲標識(這個是隨着依賴遞增的),這會影響到緩存,使用這個插件將會使用文件的路徑做爲標識,詳細見splitChunks and runtimeChunk分析
  • CommonsChunkPlugin // 最爲晦澀難懂的webpack插件,做用代碼分割,被splitChunks && runtimeChunks代替

下面爲升級以前的配置

// 依賴module(require, import 導入的文件)來自node_modules且以.js 結尾的文件將會被打包到名爲vendor的bundle中
new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: function (module, count) {
        // any required modules inside node_modules are extracted to vendor
        return (
            module.resource &&
            /\.js$/.test(module.resource) &&
            module.resource.indexOf(
                path.join(__dirname, '../node_modules')
            ) === 0
        )
    }
}),
 
// 在vendor的bundle中把manifest提取出來(mainifest算是webpack實現的在瀏覽器端進行模擬加載模塊和具體加載邏輯的代碼塊,這個最好拆出來,否則無法作緩存,具體見到長效緩存分析)
new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest',
    chunks: ['vendor']
})
  
// 打包以後就會生成一個vendor.js (裏面有着來自node_modules 的第三方類庫)和 mainifest.js (具體加載邏輯)

升級以後,完成原有的配置很簡單,簡單的照着API實現便可,本次升級伴隨着兩個重點,開發模式的rebuild時間,生成環境的打包文件體積和時間,因此還須要作其餘優化

  1. 某些第三方類庫只有某個module才加載,好比只有module1中用mint-ui,這是能夠單獨提出來一個chunk或者直接就加載到module1這個入口的init chunk中 ----> 配置層面
  2. 某些公共組件好比component下公共組件layout在基本每一個系統模塊所有都條用了,能夠考慮拉出來共用,減小重複代碼量 ----> 配置層面
  3. 利用ES的語法去動態的引入模塊import('a.js').then(module => {})去懶加載一個模塊,好比只有在某些vue中才會用到的vue-qrcode-component的類庫,這個類庫不該該出如今vendor中(來自node_modules) ----> 代碼層面
optimization: {
  splitChunks: {
    cacheGroups: {
        // module只要知足下面就會從原來的chunks中抽取出來打包到對應的chunks之中
        // 這個vendors是在至少同時有兩個initial Chunk中引入的來自node_modules的第三方類庫會被打包到chunks-vendors中
        vendors: {
            name: 'chunk-vendors',
            test: /[\\\/]node_modules[\\\/]/,
            priority: -10,
            minChunks: 2,    // 至少同時有幾個chunk知足纔會有可能從這些chunks中提取一些代碼到新的chunk中,也就是至少兩個共用纔會提取,否則就直接打包都所在module的init chunk中
            chunks: 'initial'  // chunk的概念:代碼分割這些操做做用於那些chunk文件,initial是經過提供給webpack入口生成的chunks, async是經過以前提到的import()  路由懶加載造成的chunk, all就是全部的chunk文件
        },
        // 這個common跟上面的區別在於沒有test檢測module來源,只是只要有兩個chunk共用就是提取出來,很顯然就範圍來說,下面的大於上面,這時候到底這個來自node_modules的module進入哪一個chunk,取決於priority(優先級),誰高進入哪一個chunk
        common: {
            name: 'chunk-common',
            minChunks: 2,
            priority: -12,
            chunks: 'initial',
            minSize: 0
        }
    }
  },
  runtimeChunk: {
    name: 'manifest'
  }
}
 
// 好比某些第三方類庫只在某個Vue文件中使用,經過動態引入import('vue-qrcode-component').then() 一個類庫只在一個地方用,徹底沒有必要打包到vendor中(由於vue-qrcode-component 來自node_module)
// import 就是新建一個chunk  這個chunk是沒有名字的,須要經過/* webpackChunkName: "loadsh"*/ 生成這個loadsh的chunk.name
import(/* webpackChunkName: "loadsh"*/ 'loadsh').then(m => {
    const _ = m.default
    console.log(_.join(['hello', 'world']))
})

總結:通常通過這幾步驟就能完成一個webapck項目的升級,對於本身項目的複雜的地方須要額外的處理,寫着寫着發現篇幅愈來愈長了,把上文一直出現的splitChunk和runtimeChunk留到下個篇幅着重介紹一下。在公司作升級以前,給的指標不只僅是升級框架,還須要在dev模式開發的rebuild的速度更快(修改一個地方,rebuild的時間12s左右才能看到效果,痛苦,能夠經過lessModule來解決),在prod模式下打包的項目文件體積減少已經打包所須要的時間更短(一次測試環境發佈須要5,6分鐘)。如今vue-cli3.0已經出現了,找時間把vue-cli3.0源碼給你們分析一下,簡單的包裝了一下操做

相關文章
相關標籤/搜索