前言
webpack和gulp最大的區別在于它是一個打包工具,它串聯起了整個前端工程化
的每一項內容。我非常慶幸的是經歷過webpack1到2的升級,也經歷了2到3的升級,打包的相關內容也越來越多。如今webpack從3變成4,很多人抱怨webpack的配置太過于復雜,在webpack4以后它的配置會變得越來越簡單,對于開發者來說,entry,output,loader,plugin四大板塊是必須要清楚的。
- entry 輸入
- output 輸出
- loader 打包規則
- plugin 插件生態
當你了解了上面的這些內容,還不夠。你需要了解各個版本間的差異性,這樣你才能充分利用它所有的功能。Webpack1到2最大的升級是tree-shaking,其次是配置文件的對象化,再其次包括插件的寫法優化。Webpack2到3的最大升級是scope-hoisting。3到4簡化了整個打包配置操作。
code-spliting
code-spliting(代碼分割)應該是所有前端人都知道的優化點。當你單頁面做越做越大的時候,非首屏的頁面就會考慮到不優先加載。但是怎么去劃分懶加載的包,最高效的方法就是路由懶加載。
舉個栗子,在你使用vue路由的時候,你可能會考慮到除了第一頁的內容,不會預先加載,會延時加載后面幾頁的功能。這七個頁面會從 app.js 中拆分成為7個js包。這樣的代碼分割,在大型的單頁面應用中,我們必須使用到因為后面的頁面我們不需要提前加載。
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const routes = [
{
path: '/',
component: resolve => require(['../views/Map'], resolve),
},
{
path: '/setting',
component: resolve => require(['../views/Setting'], resolve),
},
{
path: '/cities',
component: resolve => require(['../views/Cities'], resolve),
},
{
path: '/discovery',
component: resolve => require(['../views/Discovery'], resolve),
},
{
path: '/about',
component: resolve => require(['../views/About'], resolve),
},
{
path: '/more',
component: resolve => require(['../views/More'], resolve),
},
{
path: '/weather',
component: resolve => require(['../views/Weather'], resolve),
},
];
const router = new VueRouter({ mode: 'history', base: '/app/', routes });
隨著webpack2語法的進化,上面的代碼也可以被寫成這樣子。
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const routes = [
{
path: '/',
component:() => import('../views/Map'),
},
{
path: '/setting',
component: () => import('../views/Setting'),
},
{
path: '/cities',
component: () => import('../views/Cities'),
},
{
path: '/discovery',
component: () => import('../views/Discovery'),
},
{
path: '/about',
component: () => import('../views/About'),
},
{
path: '/more',
component: () =>import('../views/More'),
},
{
path: '/weather',
component: () => import('../views/Weather'),
},
];
const router = new VueRouter({ mode: 'history', base: '/app/', routes });
每個懶加載的背后都附送一個鉤子。使用了code-splitting,webpack會根據你可以將一些首屏不顯示的內容額外打包成為一個獨立的js。webpack2中懶加載打包會連同樣式以內聯的形式一起打入JS中,這樣的好處在于公共樣式也被細化抽離,但是可能會造成樣式冗余。webpack3則提供了ExtractTextPlugin中提供了抽取公共樣式的方法,公共樣式可以額外抽離。
tree-shaking
tree-shaking是rollup提出的一款技術,反哺到了webpack2的升級版本中。這可以說一個非常難以理解的概念,就像lodash這樣的公共方法,在項目編寫會積累的越來越多,但是我們不希望將這些方法全部打包入一個js文件當中。常見的方法有:
- 項目解耦,將一個大型項目拆分成幾個小型項目
- 使用tree-shaking,它只打包有用的方法,沒有用的方法則不會進行打包
tree-shaking默認是不會觸發的。在webpack3,你需要配置babel,uglifyjs-webpack-plugin等才能觸發。在webpack4,production模式默認觸發。首先,如果在編寫代碼過程中必須使用得當,純函數對于tree項目打包有相當大的優勢,也就是你的變量盡量要保持函數間的干凈,不要相互污染。
情景一:最簡單的例子
在index.js
引入另一個js中的兩個方法,webpack4的打包結果是只會存在console.log(1)
。而console.log(2)
已不會進入打包的范圍當中。
//core.js
export function test1() {
console.log(1)
}
export function test2() {
console.log(2)
}
//index.js
import {test1} from './core'
test1()
情景二:存在一個常量或者變量
當core.js
有個全局變量a=2
,這個變量可能會在別的函數中改變,webpack會檢查該函數是否在打包范圍內。不會在該范圍內的,如test1
,則不會被打包。
//core.js
let a = 2;
export function test1() {
a = 1;
console.log(a);
}
export function test2() {
console.log(a);
}
//index.js
import {test2} from './core'
test2()
情景三:存在一個對象
如果你輸出的是一個對象,你只需要其中的一個方法,此時你同樣結構只需要一個方法test2,別的方法是不會被打包進去。
//core.js
let a = 2;
function test1() {
a = 1;
console.log(a);
}
function test2() {
console.log(a);
}
function test3() {
console.log(3)
}
function test4() {
console.log(4)
}
export {
test1,
test2,
test3,
test4
}
情景四:存在prototype或者class
其實結果很明顯,由于別的方法會在實例化的時候聲明,由于被實例化的新的對象相互間是有聯系的,這也注定了它會被打包進去。
scope-hoisting
它的中文名就是作用域提升,這個名字非常熟悉。說到底,javascript的模塊化就是通過閉包來實現作用域的隔離,但是當我們模塊化程度達到一定程度之后,過多閉包會讓某些變量沒法銷毀,造成性能劣勢。作用域提升即是把兩個閉包合成一個閉包。
通過Scope Hoisting優化Webpack輸出里面講了最最基礎的一個作用域提升的例子,我在這里借用這個例子。首先寫兩個js,它們相互之間是引用關系。這里采用webpack4版本舉例,在development mode時沒有采用scope hoisting,而production mode時默認開啟了該優化。
// core.js
export const a = 'Hello,Webpack';
// index.js
import { a } from './base.js';
console.log(a);
如果你在項目中使用webpack3,你需要開啟webpack.optimize.ModuleConcatenationPlugin
來滿足作用域提升的功能。如果你在項目中使用webpack4,那么你在開發模式即是關閉作用域提升,在生產模式開啟該功能。