从今天开始,学习Webpack,减少对脚手架的依赖(下)
問:這篇文章適合哪些人?
答:適合沒接觸過Webpack或者了解不全面的人。
問:這篇文章的目錄怎么安排的?
答:先介紹背景,由背景引入Webpack的概念,進一步介紹Webpack基礎、核心和一些常用配置案例、優化手段,Webpack的plugin和loader確實非常多,短短2w多字還只是覆蓋其中一小部分。
問:這篇文章的出處?
答:此篇文章知識來自付費視頻(鏈接在文章末尾),文章由自己獨立撰寫,已獲得講師授權并首發于掘金
上一篇:從今天開始,學習Webpack,減少對腳手架的依賴(上)
如果你覺得寫的不錯,請給我點一個star,原博客地址:原文地址
PWA配置
PWA全稱Progressive Web Application(漸進式應用框架),它能讓我們主動緩存文件,這樣用戶離線后依然能夠使用我們緩存的文件打開網頁,而不至于讓頁面掛掉,實現這種技術需要安裝workbox-webpack-plugin插件。
如果你的谷歌瀏覽器還沒有開啟支持PWA,請開啟它再進行下面的測試。
安裝插件
$ npm install workbox-webpack-plugin -D 復制代碼webpack.config.js文件配置
// PWA只有在線上環境才有效,所以需要在webpack.prod.js文件中進行配置 const WorkboxWebpackPlugin = require('workbox-webpack-plugin'); const prodConfig = {// 其它配置plugins: [new MiniCssExtractPlugin({}),new WorkboxWebpackPlugin.GenerateSW({clientsClaim: true,skipWaiting: true})] } module.exports = merge(commonConfig, prodConfig); 復制代碼以上配置完畢后,讓我們使用npm run build打包看一看生成了哪些文件,dist目錄的打包結果如下:
|-- dist | |-- index.html | |-- main.f28cbac9bec3756acdbe.js | |-- main.f28cbac9bec3756acdbe.js.map | |-- precache-manifest.ea54096f38009609a46058419fc7009b.js | |-- service-worker.js 復制代碼我們可以代碼塊高亮的部分,多出來了precache-manifest.xxxxx.js文件和service-worker.js,就是這兩個文件能讓我們實現PWA。
改寫index.js
需要判斷瀏覽器是否支持PWA,支持的時候我們才進行注冊,注冊的.js文件為我們打包后的service-worker.js文件。
console.log('hello,world'); if('serviceWorker' in navigator) {navigator.serviceWorker.register('/service-worker.js').then((register) => {console.log('注冊成功');}).catch(error => {console.log('注冊失敗');}) } 復制代碼PWA實際效果
在npm run dev后,我們利用webpack-dev-server啟動了一個小型的服務器,然后我們停掉這個服務器,刷新頁面,PWA的實際結果如下圖所示
WebpackDevServer請求轉發
在這一小節中,我們要學到的技能有:
- 如何進行接口代理配置
- 如何使用接口路徑重寫
- 其他常見配置的介紹
假設我們現在有這樣一個需求:我有一個URL地址(http://www.dell-lee.com/react/api/header.json),我希望我請求的時候,請求的地址是/react/api/header.json,能有一個什么東西能自動幫我把請求轉發到http://www.dell-lee.com域名下,那么這個問題該如何解決呢?可以使用 Webpack 的webpack-dev-server這個插件來解決,其中需要配置proxy屬性。
如何進行接口代理配置
既然我們要做請求,那么安裝axios來發請求再合適不過了,使用如下命令安裝axios:
$ npm install axios --save-dev 復制代碼因為我們的請求代理只能在開發環境下使用,線上的生產環境,需要走其他的代理配置,所以我們需要在webpack.dev.js中進行代理配置
const devConfig = {// 其它配置devServer: {contentBase: './dist',open: false,port: 3000,hot: true,hotOnly: true,proxy: {'/react/api': {target: 'http://www.dell-lee.com'}}} } 復制代碼以上配置完畢后,我們在index.js文件中引入axios模塊,再做請求轉發。
import axios from 'axios';axios.get('/react/api/header.json').then((res) => {let {data,status} = res;console.log(data); }) 復制代碼使用npm run dev后, 我們可以在瀏覽器中看到,我們已經成功請求到了我們的數據。
如何使用接口路徑重寫
現在依然假設有這樣一個場景:http://www.dell-lee.com/react/api/header.json這個后端接口還沒有開發完畢,但后端告訴我們可以先使用http://www.dell-lee.com/react/api/demo.json 這個測試接口,等接口開發完畢后,我們再改回來。解決這個問題最佳辦法是,代碼中的地址不能變動,我們只在proxy代理中處理即可,使用pathRewrite屬性進行配置。
const devConfig = {// 其它配置devServer: {contentBase: './dist',open: false,port: 3000,hot: true,hotOnly: true,proxy: {'/react/api': {target: 'http://www.dell-lee.com',pathRewrite: {'header.json': 'demo.json'}}}} } 復制代碼同樣,我們打包后在瀏覽器中可以看到,我們的測試接口的數據已經成功拿到了。
其他常見配置的含義
轉發到https: 一般情況下,不接受運行在https上,如果要轉發到https上,可以使用如下配置
module.exports = {//其它配置devServer: {proxy: {'/react/api': {target: 'https://www.dell-lee.com',secure: false}}} } 復制代碼跨域: 有時候,在請求的過程中,由于同源策略的影響,存在跨域問題,我們需要處理這種情況,可以如下進行配置。
module.exports = {//其它配置devServer: {proxy: {'/react/api': {target: 'https://www.dell-lee.com',changeOrigin: true,}}} } 復制代碼代理多個路徑到同一個target: 代理多個路徑到同一個target,可以如下進行配置
module.exports = {//其它配置devServer: {proxy: [{context: ['/vue/api', '/react/api'],target: 'http://www.dell-lee.com'}]} } 復制代碼多頁打包
現在流行的前端框架都推行單頁引用(SPA),但有時候我們不得不兼容一些老的項目,他們是多頁的,那么如何進行多頁打包配置呢? 現在我們來思考一個問題:多頁運用,即 多個入口文件+多個對應的html文件 ,那么我們就可以配置 多個入口+配置多個html-webpack-plugin 來進行。
場景:假設現在我們有這樣三個頁面:index.html, list.html, detail.html,我們需要配置三個入口文件,新建三個.js文件。
在webpack.common.js中配置多個entry并使用html-webpack-plugin來生成對應的多個.html頁面。 HtmlWebpackPlugin參數說明:
- template:代表以哪個HTML頁面為模板
- filename:代表生成頁面的文件名
- chunks:代表需要引用打包后的哪些.js文件
在src目錄下新建三個.js文件,名字分別是:index.js,list.js和detail.js,它們的代碼如下:
// index.js代碼 document.getElementById('root').innerHTML = 'this is index page!'// list.js代碼 document.getElementById('root').innerHTML = 'this is list page!'// detail.js代碼 document.getElementById('root').innerHTML = 'this is detail page!' 復制代碼運行npm run build進行打包:
$ npm run build 復制代碼打包后的dist目錄:
|-- dist | |-- detail.dae2986ea47c6eceecd6.js | |-- detail.dae2986ea47c6eceecd6.js.map | |-- detail.html | |-- index.ca8e3d1b5e23e645f832.js | |-- index.ca8e3d1b5e23e645f832.js.map | |-- index.html | |-- list.5f40def0946028db30ed.js | |-- list.5f40def0946028db30ed.js.map | |-- list.html 復制代碼隨機選擇list.html在瀏覽器中運行,結果如下:
思考:現在只有三個頁面,即我們要配置三個入口+三個對應的html,如果我們有十個入口,那么我們也要這樣做重復的勞動嗎?有沒有什么東西能幫助我們自動實現呢?答案當然是有的!
我們首先定義一個makeHtmlPlugins方法,它接受一個 Webpack 配置項的參數configs,返回一個plugins數組
const makeHtmlPlugins = function (configs) {const htmlPlugins = []Object.keys(configs.entry).forEach(key => {htmlPlugins.push(new htmlWebpackPlugin({template: 'src/index.html',filename: `${key}.html`,chunks: [key]}))})return htmlPlugins } 復制代碼通過調用makeHtmlPlugins方法,它返回一個html的plugins數組,把它和原有的plugin進行合并后再復制給configs
configs.plugins = configs.plugins.concat(makeHtmlPlugins(configs)); module.exports = configs; 復制代碼以上配置完畢后,打包結果依然還是一樣的,請自行測試,以下是webpack.commom.js完整的代碼:
const path = require('path'); const webpack = require('webpack'); const htmlWebpackPlugin = require('html-webpack-plugin'); const cleanWebpackPlugin = require('clean-webpack-plugin'); const miniCssExtractPlugin = require('mini-css-extract-plugin'); const optimizaCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin'); const configs = {entry: {index: './src/index.js',list: './src/list.js',detail: './src/detail.js'},module: {rules: [{test: /\.css$/,use: [{ loader: miniCssExtractPlugin.loader,options: {hmr: true,reloadAll: true}},'css-loader']},{ test: /\.js$/, exclude: /node_modules/, loader: [{loader: "babel-loader"},{loader: "imports-loader?this=>window"}] }]},plugins: [new cleanWebpackPlugin(),new miniCssExtractPlugin({filename: '[name].css'}),new webpack.ProvidePlugin({'$': 'jquery','_': 'lodash'})],optimization: {splitChunks: {chunks: 'all'},minimizer: [new optimizaCssAssetsWebpackPlugin()]},output: {filename: '[name].js',path: path.resolve(__dirname,'../dist')} } const makeHtmlPlugins = function (configs) {const htmlPlugins = []Object.keys(configs.entry).forEach(key => {htmlPlugins.push(new htmlWebpackPlugin({template: 'src/index.html',filename: `${key}.html`,chunks: [key]}))})return htmlPlugins } configs.plugins = configs.plugins.concat(makeHtmlPlugins(configs)) module.exports = configs 復制代碼如何打包一個庫文件(Library)
在上面所有的 Webpack 配置中,幾乎都是針對業務代碼的,如果我們要打包發布一個庫,讓別人使用的話,該怎么配置?在下面的幾個小節中,我們將來講一講該怎么樣打包一個庫文件,并讓這個庫文件在多種場景能夠使用。
創建一個全新的項目
步驟:
- 創建library項目
- 使用npm init -y進行配置package.json
- 新建src目錄,創建math.js文件、string.js文件、index.js文件
- 根目錄下創建webpack.config.js文件
- 安裝webpack、webpack-cli:::
按上面的步驟走完后,你的目錄大概看起來是這樣子的:
|-- src | |-- index.js | |-- math.js | |-- string.js |-- webpack.config.js |-- package.json 復制代碼初始化package.json
// 初始化后,改寫package.json {"name": "library","version": "1.0.0","description": "","main": "index.js","scripts": {"build": "webpack"},"keywords": [],"author": "","license": "MIT" }復制代碼創建src目錄,并添加文件
在src目錄下新建math.js,它的代碼是四則混合運算的方法,如下:
export function add(a, b) {return a + b; } export function minus(a, b) {return a - b; } export function multiply(a, b) {return a * b; } export function division(a, b) {return a / b; } 復制代碼在src目錄下新建string.js,它有一個join方法,如下:
export function join(a, b) {return a + '' + b; } 復制代碼在src目錄下新建index.js文件,它引用math.js和string.js并導出,如下:
import * as math from './math'; import * as string from './string';export default { math, string }; 復制代碼添加webpack.config.js
因為我們是要打包一個庫文件,所以mode只配置為生產環境(production)即可。
在以上文件添加完畢后,我們來配置一下webpack.config.js文件,它的代碼非常簡單,如下:
const path = require('path'); module.exports = {mode: 'production',entry: './src/index.js',output: {filename: 'library.js',path: path.resolve(__dirname, 'dist')} } 復制代碼安裝Webpack
因為涉及到 Webpack 打包,所以我們需要使用npm instll進行安裝:
$ npm install webpack webpack-cli -D 復制代碼進行第一次打包
使用npm run build進行第一次打包,在dist目錄下會生成一個叫library.js的文件,我們要測試這個文件的話,需要在dist目錄下新建index.html
$ npm run build $ cd dist $ touch index.html 復制代碼在index.html中引入library.js文件:
<script src="./library.js"></script> 復制代碼至此,我們已經基本把項目目錄搭建完畢,現在我們來考慮一下,可以在哪些情況下使用我們打包的文件:
- 使用ES Module語法引入,例如import library from 'library'
- 使用CommonJS語法引入,例如const library = require('library')
- 使用AMD、CMD語法引入,例如require(['library'], function() {// todo})
- 使用script標簽引入,例如<script src="library.js"></script>
針對以上幾種使用場景,我們可以在output中配置library和libraryTarget屬性(注意:這里的library和libraryTarget和我們的庫名字library.js沒有任何關系,前者是Webpack固有的配置項,后者只是我們隨意取的一個名字)
const path = require('path'); module.exports = {mode: 'production',entry: './src/index.js',output: {filename: '[name].js',path: path.resolve(__dirname, 'dist'),library: 'library',libraryTarget: 'umd'} } 復制代碼配置屬性說明:
- library:這個屬性指,我們庫的全局變量是什么,類似于jquery中的$符號
- libraryTarget: 這個屬性指,我們庫應該支持的模塊引入方案,umd代表支持ES Module、CommomJS、AMD以及CMD
在配置完畢后,我們再使用npm run build進行打包,并在瀏覽器中運行index.html,在console控制臺輸出library這個全局變量,結果如下圖所示:
以上我們所寫的庫非常簡單,在實際的庫開發過程中,往往需要使用到一些第三方庫,如果我們不做其他配置的話,第三方庫會直接打包進我們的庫文件中。
如果用戶在使用我們的庫文件時,也引入了這個第三方庫,就造成了重復引用的問題,那么如何解決這個問題呢?可以在webpack.config.js文件中配置externals屬性。
在string.js文件的join方法中,我們使用第三方庫lodash中的_join()方法來進行字符串的拼接。
import _ from 'lodash'; export function join(a, b) {return _.join([a, b], ' '); } 復制代碼在修改完畢string.js文件后,使用npm run build進行打包,發現lodash直接打包進了我們的庫文件,造成庫文件積極臃腫,有70.8kb。
$ npm run build Built at: 2019-04-05 00:47:25Asset Size Chunks Chunk Names library.js 70.8 KiB 0 [emitted] main 復制代碼針對以上問題,我們可以在webpack.config.js中配置externals屬性,更多externals的用法請點擊externals
const path = require('path'); module.exports = {mode: 'production',entry: './src/index.js',externals: ['lodash'],output: {filename: 'library.js',path: path.resolve(__dirname, 'dist'),library: 'library',libraryTarget: 'umd'} } 復制代碼配置完externals后,我們再進行打包,它的打包結果如下,我們可以看到我們的庫文件又變回原來的大小了,證明我們的配置起作用了。
$ npm run build Built at: 2019-04-05 00:51:22Asset Size Chunks Chunk Names library.js 1.63 KiB 0 [emitted] main 復制代碼如何發布并使用我們的庫文件
在打包完畢后,我們如何發布我們的庫文件呢,以下是發布的步驟:
- 注冊npm賬號
- 修改package.json文件的入口,修改為:"main": "./dist/library.js"
- 運行npm adduser添加賬戶名稱
- 運行npm publish命令進行發布
- 運行npm install xxx來進行安裝
為了維護npm倉庫的干凈,我們并未實際運行npm publish命令,因為我們的庫是無意義的,發布上去屬于垃圾代碼,所以請自行嘗試發布。另外自己包的名字不能和npm倉庫中已有的包名字重復,所以需要在package.json中給name屬性起一個特殊一點的名字才行,例如"name": "why-library-2019"
TypeScript配置
隨著TypeScript的不斷發展,相信未來使用TypeScript來編寫 JS 代碼將變成主流形式,那么如何在 Webpack 中配置支持TypeScript呢?可以安裝ts-loader和typescript來解決這個問題。
新建一個項目webpack-typescript
新創建一個項目,命名為webpack-typescript,并按如下步驟處理:
- 使用npm init -y初始化package.json文件,并在其中添加build Webpack打包命令
- 新建webpack.config.js文件,并做一些簡單配置,例如entry、output等
- 新建src目錄,并在src目錄下新建index.ts文件
- 新建tsconfig.json文件,并做一些配置
- 安裝webpack和webpack-cli
- 安裝ts-loader和typescript
按以上步驟完成后,項目目錄大概如下所示:
|-- src | |-- index.ts |-- tsconfig.json |-- webpack.config.js |-- package.json 復制代碼在package.json中添加好打包命令命令:
"scripts": {"build": "webpack" }, 復制代碼接下來我們需要對webpack.config.js做一下配置:
const path = require('path'); module.exports = {mode: 'production',module: {rules: [{test: /\.(ts|tsx)?$/,use: {loader: 'ts-loader'}}]},entry: {main: './src/index.ts'},output: {filename: '[name].js',path: path.resolve(__dirname, 'dist')} } 復制代碼在tsconfig.json里面進行typescript的相關配置,配置項的說明如下
- module: 表示我們使用ES6模塊
- target: 表示我們轉換成ES5代碼
- allowJs: 允許我們在.ts文件中通過import語法引入其他.js文件
在src/index.ts文件中書寫TypeScript代碼,像下面這樣
class Greeter {greeting: stringconstructor(message: string) {this.greeting = message;}greet() {return 'hello, ' + this.greeting;} }let greeter = new Greeter('why'); console.log(greeter.greet()); 復制代碼打包測試
- 運行npm run build進行打包
- 在生成dist目錄下,新建index.html,并引入打包后的main.js文件
- 在瀏覽器中運行index.html
使用其他模塊的類型定義文件
如果我們要使用lodash庫,必須安裝其對應的類型定義文件,格式為@types/xxx
安裝lodash對應的typescript類型文件:
$ npm install lodash @types/lodash -D 復制代碼安裝完畢后,我們在index.ts中引用lodash,并使用里面的方法:
import * as _ from 'lodash'class Greeter {greeting: stringconstructor(message: string) {this.greeting = message;}greet() {return _.join(['hello', this.greeting], '**');} }let greeter = new Greeter('why'); console.log(greeter.greet()); 復制代碼打包測試
使用npm run build,在瀏覽器中運行index.html,結果如下:
Webpack性能優化
打包分析
在進行 Webpack 性能優化之前,如果我們知道我們每一個打包的文件有多大,打包時間是多少,它對于我們進行性能優化是很有幫助的,這里我們使用webpack-bundle-analyzer來幫助我們解決這個問題。
首先需要使用如下命令去安裝這個插件:
$ npm install webpack-bundle-analyzer --save-dev 復制代碼安裝完畢后,我們需要在webpack.prod.js文件中做一點小小的改動:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const prodConfig = {// 其它配置項mode: 'production',plugins: [new BundleAnalyzerPlugin()] } 復制代碼配置完畢后,我們運行npm run build命令來查看打包分析結果,以下打包結果僅供參考:
縮小文件的搜索范圍
首先我們要弄明白 Webpack 的一個配置參數(Resolve)的作用:它告訴了 Webpack 怎么去搜索文件,它同樣有幾個屬性需要我們去理解:
- extensions:它告訴了 Webpack 當我們在導入模塊,但沒有寫模塊的后綴時應該如何去查找模塊。
- mainFields:它告訴了 Webpack 當我們在導入模塊,但并沒有寫模塊的具體名字時,應該如何去查找這個模塊。
- alias:當我們有一些不得不引用的第三方庫或者模塊的時候,可以通過配置別名,直接引入它的.min.js文件,這樣可以庫內的直接解析
- 其它include、exclude、test來配合loader進行限制文件的搜索范圍
extensions參數
就像上面所說的那樣,extensions它告訴了 Webpack 當我們在導入模塊,但沒有寫模塊的后綴時,應該如何去查找模塊。這種情況在我們開發中是很常見的,一個情形可能如下所示:
// 書寫了模塊后綴 import main from 'main.js'// 沒有書寫模塊后綴 import main from 'main' 復制代碼像上面那樣,我們不寫main.js的.js后綴,是因為 Webpack 會默認幫我們去查找一些文件,我們也可以去配置自己的文件后綴配置:
extensions參數應盡可能只配置主要的文件類型,不可為了圖方便寫很多不必要的,因為每多一個,底層都會走一遍文件查找的工作,會損耗一定的性能。
module.exports = {// 其它配置resolve: {extensions: ['.js', '.json', '.vue']} } 復制代碼如果我們像上面配置后,我們可以在代碼中這樣寫:
// 省略 .vue文件擴展 import BaseHeader from '@/components/base-header';// 省略 .json文件擴展 import CityJson from '@/static/city'; 復制代碼mainFields參數
mainFields參數主要應用場景是,我們可以不寫具體的模塊名稱,由 Webpack 去查找,一個可能的情形如下:
// 省略具體模塊名稱 import BaseHeader from '@components/base-header/';// 以上相當于這一段代碼 import BaseHeader from '@components/base-header/index.vue'; // 或者這一段 import BaseHeader from '@components/base-header/main.vue'; 復制代碼我們也可以去配置自己的mainFields參數:
同extensions參數類似,我們也不建議過多的配置mainFields的值,原因如上。
module.exports = {// 其它配置resolve: {extensions: ['.js', '.json', '.vue'],mainFields: ['main', 'index']} } 復制代碼alias參數
alias參數更像一個別名,如果你有一個目錄很深、文件名很長的模塊,為了方便,配置一個別名這是很有用的;對于一個龐大的第三方庫,直接引入.min.js而不是從node_modules中引入也是一個極好的方案,一個可能得情形如下:
通過別名配置的模塊,會影響Tree Shaking,建議只對整體性比較強的庫使用,像lodash庫不建議通過別名引入,因為lodash使用Tree Shaking更合適。
// 沒有配置別名之前 import main from 'src/a/b/c/main.js'; import React from 'react';// 配置別名之后 import main from 'main.js'; import React from 'react'; 復制代碼// 別名配置 const path = require('path'); module.exports = {// 其它配置resolve: {extensions: ['.js', '.json', '.vue'],mainFields: ['main', 'index'],alias: {main: path.resolve(__dirname, 'src/a/b/c'),react: path.resolve(__dirname, './node_modules/react/dist/react.min.js')}} } 復制代碼Tree Shaking去掉冗余的代碼
Tree Shaking配置我們已經在上面講過,配置Tree Shaking也很簡單。
module.exports = {// 其它配置optimization: {usedExports: true} } 復制代碼如果你對Tree Shaking還不是特別理解,請點擊Tree Shaking。
DllPlugin減少第三方庫的編譯次數
對于有些固定的第三方庫,因為它是固定的,我們每次打包,Webpack 都會對它們的代碼進行分析,然后打包。那么有沒有什么辦法,讓我們只打包一次,后面的打包直接使用第一次的分析結果就行。答案當然是有的,我們可以使用 Webpack 內置的DllPlugin來解決這個問題,解決這個問題可以分如下的步驟進行:
- 把第三方庫單獨打包在一個xxx.dll.js文件中
- 在index.html中使用xxx.dll.js文件
- 生成第三方庫的打包分析結果保存在xxx.manifest.json文件中
- 當npm run build時,引入已經打包好的第三方庫的分析結果
- 優化
單獨打包第三方庫
為了單獨打包第三方庫,我們需要進行如下步驟:
- 根目錄下生成dll文件夾
- 在build目錄下生成一個webpack.dll.js的配置文件,并進行配置。
- 在package.json文件中,配置build:dll命令
- 使用npm run build:dll進行打包
生成dll文件夾:
$ mkdir dll 復制代碼在build文件夾下生成webpack.dll.js:
$ cd build $ touch webpack.dll.js 復制代碼創建完畢后,需要在webpack.dll.js文件中添加如下代碼:
const path = require('path'); module.exports = {mode: 'production',entry: {vendors: ['lodash', 'jquery']},output: {filename: '[name].dll.js',path: path.resolve(__dirname, '../dll'),library: '[name]'} } 復制代碼最后需要在package.json文件中添加新的打包命令:
{// 其它配置"scripts": {"dev": "webpack-dev-server --config ./build/webpack.dev.js","build": "webpack --config ./build/webpack.prod.js","build:dll": "webpack --config ./build/webpack.dll.js"} } 復制代碼使用npm run build:dll打包結果,你的打包結果看起來是下面這樣的:
|-- build | |-- webpack.common.js | |-- webpack.dev.js | |-- webpack.dll.js | |-- webpack.prod.js |-- dll | |-- vendors.dll.js |-- src | |-- index.html | |-- index.js |-- package.json 復制代碼引用xxx.dll.js文件
在上一小節中我們成功拿到了xxx.dll.js文件,那么如何在index.html中引入這個文件呢?答案是需要安裝add-asset-html-webpack-plugin插件:
$ npm install add-asset-html-webpack-plugin -D 復制代碼在webpack.common.js中使用add-asset-html-webpack-plugin插件:
const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin'); const configs = {// 其它配置plugins: [new addAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, '../dll/vendors.dll.js')})] } module.exports = configs; 復制代碼我們將第三方庫全局暴露了一個vendors變量,現引入xxx.dll.js文件結果如下所示:
生成打包分析文件
在webpack.dll.js中使用 Webpack 內置的DllPlugin插件,進行打包分析:
const path = require('path'); const webpack = require('webpack'); module.exports = {mode: 'production',entry: {vendors: ['lodash', 'jquery']},output: {filename: '[name].dll.js',path: path.resolve(__dirname, '../dll'),library: '[name]'},plugins: [new webpack.DllPlugin({name: '[name]',path: path.resolve(__dirname, '../dll/[name].manifest.json')})] } 復制代碼引用打包分析文件
在webpack.common.js中使用 Webpack 內置的DllReferencePlugin插件來引用打包分析文件:
const htmlWebpackPlugin = require('html-webpack-plugin'); const cleanWebpackPlugin = require('clean-webpack-plugin'); const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin'); const webpack = require('webpack'); const path = require('path'); module.exports = {// 其它配置plugins: [new cleanWebpackPlugin(),new htmlWebpackPlugin({template: 'src/index.html'}),new addAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, '../dll/vendors.dll.js')}),new webpack.DllReferencePlugin({manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')})] } 復制代碼優化
現在我們思考一個問題,我們目前是把lodash和jquery全部打包到了vendors文件中,那么如果我們要拆分怎么辦,拆分后又該如何去配置引入?一個可能的拆分結果如下:
const path = require('path'); const webpack = require('webpack'); module.exports = {mode: 'production',entry: {vendors: ['lodash'],jquery: ['jquery']},output: {filename: '[name].dll.js',path: path.resolve(__dirname, '../dll'),library: '[name]'},plugins: [new webpack.DllPlugin({name: '[name]',path: path.resolve(__dirname, '../dll/[name].manifest.json')})] } 復制代碼根據上面的拆分結果,我們需要在webpack.common.js中進行如下的引用配置:
const htmlWebpackPlugin = require('html-webpack-plugin'); const cleanWebpackPlugin = require('clean-webpack-plugin'); const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin'); const path = require('path'); const configs = {// ... 其他配置plugins: [new cleanWebpackPlugin(),new htmlWebpackPlugin({template: 'src/index.html'}),new addAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, '../dll/vendors.dll.js')}),new addAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, '../dll/jquery.dll.js')}),new webpack.DllReferencePlugin({manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')}),new webpack.DllReferencePlugin({manifest: path.resolve(__dirname, '../dll/jquery.manifest.json')})] } module.exports = configs; 復制代碼我們可以發現:隨著我們引入的第三方模塊越來越多,我們不斷的要進行 Webpack 配置文件的修改。對于這個問題,我們可以使用Node的核心模塊fs來分析dll文件夾下的文件,進行動態的引入,根據這個思路我們新建一個makePlugins方法,它返回一個 Webpack 的一個plugins數組:
const makePlugins = function() {const plugins = [new cleanWebpackPlugin(),new htmlWebpackPlugin({template: 'src/index.html'}),];// 動態分析文件const files = fs.readdirSync(path.resolve(__dirname, '../dll'));files.forEach(file => {// 如果是xxx.dll.js文件if(/.*\.dll.js/.test(file)) {plugins.push(new addAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, '../dll', file)}))}// 如果是xxx.manifest.json文件if(/.*\.manifest.json/.test(file)) {plugins.push(new webpack.DllReferencePlugin({manifest: path.resolve(__dirname, '../dll', file)}))}})return plugins; } configs.plugins = makePlugins(configs); module.exports = configs; 復制代碼使用npm run build:dll進行打包第三方庫,再使用npm run build打包,打包結果如下:
本次試驗,第一次打包時間為1100ms+,后面的打包穩定在800ms+,說明我們的 Webpack性能優化已經生效。
|-- build | |-- webpack.common.js | |-- webpack.dev.js | |-- webpack.dll.js | |-- webpack.prod.js |-- dist | |-- index.html | |-- jquery.dll.js | |-- main.1158fa9f961c50aaea21.js | |-- main.1158fa9f961c50aaea21.js.map |-- dll | |-- jquery.dll.js | |-- jquery.manifest.json | |-- vendors.dll.js | |-- vendors.manifest.json |-- src | |-- index.html | |-- index.js |-- package.json |-- postcss.config.js 復制代碼小結:Webpack 性能優化是一個長久的話題,本章也僅僅只是淺嘗輒止,后續會有關于 Webpack 更加深入的解讀博客,敬請期待(立個flag)。
編寫自己的Loader
在我們使用 Webpack 的過程中,我們使用了很多的loader,那么那些loader是哪里來的?我們能不能寫自己的loader然后使用? 答案當然是可以的,Webpack 為我們提供了一些loader的API,通過這些API我們能夠編寫出自己的loader并使用。
如何編寫及使用自己的Loader
場景: 我們需要把.js文件中,所有出現Webpack is good!,改成Webpack is very good!。實際上我們需要編寫自己的loader,所以我們有如下的步驟需要處理:
- 新建webpack-loader項目
- 使用npm init -y命令生成package.json文件
- 創建webpack.config.js文件
- 創建src目錄,并在src目錄下新建index.js
- 創建loaders目錄,并在loader目錄下新建replaceLoader.js
- 安裝webpack、webpack-cli
按上面的步驟新建后的項目目錄如下:
|-- loaders | | -- replaceLoader.js |-- src | | -- index.js |-- webpack.config.js |-- package.json 復制代碼首先需要在webpack.config.js中添加下面的代碼:
const path = require('path'); module.exports = {mode: 'development',entry: './src/index.js',module: {rules: [{test: /\.js$/,use: [path.resolve(__dirname, './loaders/replaceLoader.js')]}]},output: {filename: '[name].js',path: path.resolve(__dirname, 'dist')} } 復制代碼隨后在package.json文件添加build打包命令:
// 其它配置 "scripts": {"build": "webpack" } 復制代碼接下來在src/index.js文件中添加一行代碼:這個文件使用最簡單的例子,只是打印一句話。
console.log('Webpack is good!'); 復制代碼最后就是在loader/replaceLoader.js編寫我們自己loader文件中的代碼:
- 編寫loader時,module.exports是固定寫法,并且它只能是一個普通函數,不能寫箭頭函數(因為需要this指向自身)
- source是打包文件的源文件內容
使用我們的loader: 要使用我們的loader,則需要在modules中寫loader, resolveLoader它告訴了 Webpack 使用loader時,應該去哪些目錄下去找,默認是node_modules,做了此項配置后,我們就不用去顯示的填寫其路徑了,因為它會自動去loaders文件夾下面去找。
const path = require('path'); module.exports = {mode: 'development',entry: './src/index.js',resolveLoader: {modules: ['node_modules', './loaders']},module: {rules: [{test: /\.js$/,use: [{loader: 'replaceLoader',options: {name: 'wanghuayu'}}]}]},output: {filename: '[name].js',path: path.resolve(__dirname, 'dist')} } 復制代碼最后我們運行npm run build,在生成的dist目錄下打開main.js文件,可以看到文件內容已經成功替換了,說明我們的loader已經使用成功了。
/***/ "./src/index.js": /*!**********************!*\!*** ./src/index.js ***!\**********************/ /*! no static exports found */ /***/ (function(module, exports) {eval("console.log('Webpack is very good!');\n\n//# sourceURL=webpack:///./src/index.js?");/***/ })/******/ }); 復制代碼如何向Loader傳參及返回多個值
問題:
- 我們如何返回多個值?
- 我們如何向自己的Loader傳遞參數?
如何返回多個值
Webpack 的 API允許我們使用callback(error, result, sourceMap?, meta?)返回多個值,它有四個參數:
- Error || Null :錯誤類型, 沒有錯誤傳遞null
- result :轉換后的結果
- sourceMap:可選參數,處理分析后的sourceMap
- meta: 可選參數,元信息
返回多個值,可能有如下情況:
// 第三,第四個參數是可選的。 this.callback(null, result); 復制代碼如何傳遞參數
我們知道在使用loader的時候,可以寫成如下的形式:
// options里面可以傳遞一些參數 {test: /\.js$/,use: [{loader: 'replaceLoader',options: {word: 'very good'}}] } 復制代碼再使用options傳遞參數后,我們可以使用官方提供的loader-utils來獲取options參數,可以像下面這樣寫:
const loaderUtils = require('loader-utils'); module.exports = function(source) {var options = loaderUtils.getOptions(this);return source.replace('good', options.word) } 復制代碼如何在Loader中寫異步代碼
在上面的例子中,我們都是使用了同步的代碼,那么如果我們有必須異步的場景,該如何實現呢?我們不妨做這樣的假設,先寫一個setTimeout:
const loaderUtils = require('loader-utils'); module.exports = function(source) {var options = loaderUtils.getOptions(this);setTimeout(() => {var result = source.replace('World', options.name);return this.callback(null, result);}, 0); } 復制代碼如果你運行了npm run build進行打包,那么一定會報錯,解決辦法是:使用this.async()主動標識有異步代碼:
const loaderUtils = require('loader-utils'); module.exports = function(source) {var options = loaderUtils.getOptions(this);var callback = this.async();setTimeout(() => {var result = source.replace('World', options.name);callback(null, result);}, 0); } 復制代碼至此,我們已經掌握了如何編寫、如何引用、如何傳遞參數以及如何寫異步代碼,在下一小節當中我們將學習如何編寫自己的plugin。
編寫自己的Plugin
與loader一樣,我們在使用 Webpack 的過程中,也經常使用plugin,那么我們學習如何編寫自己的plugin是十分有必要的。 場景:編寫我們自己的plugin的場景是在打包后的dist目錄下生成一個copyright.txt文件
plugin基礎
plugin基礎講述了怎么編寫自己的plugin以及如何使用,與創建自己的loader相似,我們需要創建如下的項目目錄結構:
|-- plugins | -- copyWebpackPlugin.js |-- src | -- index.js |-- webpack.config.js |-- package.json 復制代碼copyWebpackPlugins.js中的代碼:使用npm run build進行打包時,我們會看到控制臺會輸出hello, my plugin這段話。
plugin與loader不同,plugin需要我們提供的是一個類,這也就解釋了我們必須在使用插件時,為什么要進行new操作了。
class copyWebpackPlugin {constructor() {console.log('hello, my plugin');}apply(compiler) {} } module.exports = copyWebpackPlugin; 復制代碼webpack.config.js中的代碼:
const path = require('path'); // 引用自己的插件 const copyWebpackPlugin = require('./plugins/copyWebpackPlugin.js'); module.exports = {mode: 'development',entry: './src/index.js',output: {filename: '[name].js',path: path.resolve(__dirname, 'dist')},plugins: [// new自己的插件new copyWebpackPlugin()] } 復制代碼如何傳遞參數
在使用其他plugin插件時,我們經常需要傳遞一些參數進去,那么我們如何在自己的插件中傳遞參數呢?在哪里接受呢?
其實,插件傳參跟其他插件傳參是一樣的,都是在構造函數中傳遞一個對象,插件傳參如下所示:
在plugin的構造函數中調用:使用npm run build進行打包,在控制臺可以打印出我們傳遞的參數值why
class copyWebpackPlugin {constructor(options) {console.log(options.name);}apply(compiler) {} } module.exports = copyWebpackPlugin; 復制代碼如何編寫及使用自己的Plugin
- apply函數是我們插件在調用時,需要執行的函數
- apply的參數,指的是 Webpack 的實例
- compilation.assets打包的文件信息
我們現在有這樣一個需求:使用自己的插件,在打包目錄下生成一個copyright.txt版權文件,那么該如何編寫這樣的插件呢? 首先我們需要知道plugin的鉤子函數,符合我們規則鉤子函數叫:emit,它的用法如下:
class CopyWebpackPlugin {constructor() {}apply(compiler) {compiler.hooks.emit.tapAsync('CopyWebpackPlugin', (compilation, cb) => {var copyrightText = 'copyright by why';compilation.assets['copyright.txt'] = {source: function() {return copyrightText},size: function() {return copyrightText.length;}}cb();})} } module.exports = CopyWebpackPlugin; 復制代碼使用npm run build命名打包后,我們可以看到dist目錄下,確實生成了我們的copyright.txt文件。
|-- dist | |-- copyright.txt | |-- main.js |-- plugins | |-- copyWebpackPlugin.js |-- src | |-- index.js |-- webpack.config.js |-- package.json 復制代碼我們打開copyright.txt文件,它的內容如下:
copyright by why 復制代碼本篇博客由慕課網視頻從基礎到實戰手把手帶你掌握新版Webpack4.0閱讀整理而來,觀看視頻請支持正版。
轉載于:https://juejin.im/post/5cee779ce51d454fd8057b1d
總結
以上是生活随笔為你收集整理的从今天开始,学习Webpack,减少对脚手架的依赖(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据库以及后台开发之写在前面
- 下一篇: 前端 飞升之路