前端的發(fā)展,大致的發(fā)展路線可以看黃玄的JavaScript 模塊化七日談。從最初的全局污染式的注入到ES6模塊化,打包工具的不斷迭代替換。主要的原因都是因為前端發(fā)展越來越復(fù)雜龐大所導(dǎo)致。
本篇文章主要是來談?wù)?webpack 在我們平時的開發(fā)工作中起到什么作用,以及我們該如何靈活的應(yīng)用它來成為我們的利器。大多數(shù)情況下我不會說明怎么使用,因為這樣會導(dǎo)致篇幅太多不容易閱覽,所以具體的配置還是得自己閱覽官方文檔。
背景
如今的前端百花齊放,不再像以前那樣直接操作 DOM 然后壓縮扔到服務(wù)器上去??此茮]啥問題,但是不斷的重復(fù)勞動力導(dǎo)致開發(fā)效率低下。
React、Vue、Angular2。Typescript、Flow、CoffeeScript、ES6。SASS、LESS。分別為前端框架、JS超集/JS新標準、CSS預(yù)處理器。以上的這些無法直接的在瀏覽器上跑,都需要轉(zhuǎn)換為 ES5/CSS 才可以。(注:ES6 可以在支持 ES6 語法的瀏覽器上運行,如Chrome)
構(gòu)建工具
無論什么構(gòu)建工具,它們做的內(nèi)容都是大同小異:代碼轉(zhuǎn)換、文件優(yōu)化、代碼分割、模塊合并、自動刷新、代碼校驗、自動分布。歷史上的構(gòu)建工具都是基于Node.js開發(fā)的。有Grunt、Gulp、Fis3、Rollup、Browserify 等等。更具體可以參考前端構(gòu)建:3類13種熱門工具的選型參考
至于它們之間優(yōu)劣性以及為什么選擇webpack在網(wǎng)上有很多相關(guān)的資料可以參考,在這里就不再贅述了。
開始
基礎(chǔ)配置
/// webpack.config.js
const path = require('path');
module.exports = {
// js 執(zhí)行入口文件
entry: './main.js',
output: {
// 將所有依賴的模塊合并輸出到一個 bundle.js 文件
filename: 'bundle.js',
// 將輸出文件都放到 dist 目錄下
path: path.resolve(__dirname, './dist'),
}
};
執(zhí)行 webpack --config webpack.config.js,則會在dist文件夾生成bundle.js文件,這就是最基本的 webpack 配置。更多配置查看官網(wǎng)webpack
Loader
Loader 主要是用于將模塊代碼轉(zhuǎn)換為可在瀏覽器運行的代碼??梢岳斫鉃榉g機。如將 Less 轉(zhuǎn)換為 CSS,Typescript 轉(zhuǎn)換為 Javascript 等。
Plugin
Plugin 主要是擴展 webpack 的功能,增強 webpack 的靈活性。如extract-text-webpack-plugin,可以將包中的文本提取到單獨的文件中,從bundle.js提取 css 到單獨的文件出來等。
DevServer
webpack-dev-server,可以幫我們解決上面沒提到但是在開發(fā)中遇到的痛點。
- 提供 HTTP 服務(wù)而不是使用本地文件預(yù)覽;
- 監(jiān)聽文件的變化并自動刷新網(wǎng)頁,做到實時預(yù)覽:
- 支持 Source Map,以方便調(diào)試。
見招拆招
交待完 webpack 的基礎(chǔ)也是重要的功能之后,我們從工作中開始,見招拆招,也就是說我們平時需要做什么,webpack 能幫我們做什么。
見招 - ES6
ES6的出現(xiàn)引入了新的語法,提高了開發(fā)效率。但是目前仍有很多瀏覽器對其標準支持不全。所以我們需要將其轉(zhuǎn)換為 ES5 以及對新 API 打 polyfill。才能正常的使用。
拆招 - Babel
Babel 是 JS 編譯器,主要功能就是將 ES6 轉(zhuǎn)為 ES5,詳看 What is Babel? · Babel。在項目根目錄創(chuàng)建.babelrc。
{
// plugins 告訴 Babel 要使用哪些插件,這些插件可以控制如何轉(zhuǎn)換代碼 。
"plugins": [
[
"transform-runtime",
{
"polyfill": false
}
],
],
// presets屬性告訴 Babel要轉(zhuǎn)換的源碼使用了哪些新的語法特性,一個 Presets對一組新語法的特性提供了支持,多個 Presets 可以疊加。
"presets": [
[
// 除此之外,還有往上的標準如 ES2016等 以及 Env,其中 Env 包含ES 標準的最新特性
"es2015",
{
"modules": false
}
],
// 社區(qū)提出卻還未入標準的新特性,有stage0 - stage4,被納入的可能性依次增加
"stage-2",
// 特定應(yīng)用場景語法特性
"react"
]
}
在了解 Babel 后,下一步就是配置 Webpack。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
}
]
}
}
見招 - Typescript
Typescript 是 Javascript 類型的超集,它可以編譯成純 Javascript。TypeScript—JavaScript的超集
拆招 - Typescript
Typescript 官方提供了能將 Typescript 轉(zhuǎn)換成 JavaScript 的編譯器。執(zhí)行安裝npm i -g typescript,然后在根目錄新建配置編譯選項tsconfig.json。
{
"compilerOptions": {
"module": "commonjs", // 編譯出的代碼采用的模塊規(guī)范
"target": "es5", // 編譯出的代碼采用 ES 的哪個版本
"sourceMap": true // 輸出 Source Map 以方便調(diào)試
},
"exclude": [
"node_modules"
]
}
配置完tsconfig.json,我們就可以配置 Webpack。
module.exports = {
...
resolve: {
extensions: ['.ts']
},
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader'
}
]
},
devtool: 'source-map',
}
見招 - SASS/LESS
SASS 和 LESS 都是 CSS 的預(yù)處理器,它們都是可以方便的管理代碼,抽離樣式公共部分,通過邏輯來書寫更加靈活的樣式代碼,從而提高效率。關(guān)于他們更多的信息可以Sass: Syntactically Awesome Style Sheets和Getting started | Less.js去查看。
拆招 - SASS-LOADER / LESS-LOADER
安裝完sass-loader或less-loader之后,直接配置Webpack。
module.exports = {
module: {
rules: [
{
test: /\.scss/, 或 /\.less/
use: ['style-loader', 'css-loader', 'sass-loader'] 或 ['style-loader', 'css-loader', 'less-loader']
}
]
}
}
其處理流程如下:
1、通過 loader 將 sass/less 文件轉(zhuǎn)換為 css 代碼,再將其交給 css-loader 處理;
2、css-loader 會找出 css 代碼中導(dǎo)入語句如@import或url(),同時支持 css modules、壓縮 css 等功能,然后交給 style-loader 處理;
3、style-loader 會講 css 轉(zhuǎn)換為字符串注入 js 代碼中。
見招 - React
React 中主要是因為其代碼中使用了 JSX 和 Class 特性,因此我們需要將其轉(zhuǎn)換為瀏覽器能識別的 JavaScript 代碼。
拆招 - Babel
我們需要依賴 babel-preset-react來完成語法上的轉(zhuǎn)換。所以我們還需要配置.babelrc,加入 React Preset。
"presets": [
"react"
]
其實這樣就可以了。但是我們有時候會使用 React + Typescript 組合來提高我們開發(fā)效率。在上面我們提到 Typescript 的開發(fā),我們這次來修改其配置文件tsconfig.json。
{
"compilerOptions": {
"jsx": "react" // 開啟 JSX,支持 React
}
}
至于 Webpack 的配置,其實不用太多的改動,只需要支持下/\.tsx/后綴文件就行。
見招 - Vue
Vue 沒有 React 那樣會內(nèi)置專屬語法,但它和 React 一樣,都推崇組件化和由數(shù)據(jù)驅(qū)動的思想。話不多說,直接拆招。
拆招 - vue-loader
解析 vue 主要需要 vue-loader 和 vue-template-compiler。vue-loader 主要事用來解析和轉(zhuǎn)換.vue文件,提取出其中的邏輯代碼、樣式代碼以及 html 模板 template,再分別將它們交給對應(yīng)的 Loader 去處理,如 template 則就是由 vue-template-compiler 去處理的。
/// webpack
module: {
rules: [
{
test: /\.vue$/,
use: ['vue-loader'],
},
]
}
同樣,假如我們需要 Vue + Typescript 組合呢?從 Vue 2.5 開始,就提供了對TS 的支持。配置 tsconfig.json。
{
"compilerOptions": {
"target": "es5",
"module": "es2015", // 用于使 Tree Shaking 優(yōu)化生效
"moduleResolution": "node",
}
}
除此之外還需要在聲明文件 vue-shims.d.ts 定義 vue 類型:
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}
修改 webpack 配置文件。
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/],
}
}
]
}
到這里為止,我們就可以通過 webpack 來進行我們的開發(fā)工作了。但是實際項目中有很多的痛點,例如代碼檢查,熱更新,CDN發(fā)布等。我們不可能每次都手動的來配置,這樣太繁瑣太浪費時間了。接下來我們通過 webpack 來優(yōu)化我們的開發(fā)體驗。
見招 - 監(jiān)聽更新
當(dāng)我們在開發(fā)階段,肯定會在期間不斷地修改源碼。但是我們不可能每一次修改就手動編譯然后刷新頁面,這明顯浪費我們的時間跟精力。于是就有了自動化監(jiān)聽更新,原理就是監(jiān)聽本地源碼包括樣式,一旦發(fā)生變化時,就會自動構(gòu)建然后刷新瀏覽器。
拆招 - webpack
通過 webpack 開啟監(jiān)聽模式,一般有兩種方式:
- 配置
webpack.config.js設(shè)置watch: true; - 執(zhí)行 webpack 時,可以帶上參數(shù),如
webpack --watch
它的工作原理就是通過 aggregateTimeout 設(shè)置等待時間,到該時間時就會去檢查編輯后的文件的最后編輯時間從而達到監(jiān)聽的目的。
見招 - 自動刷新瀏覽器
在上面我們提到了監(jiān)聽更新,但是更新完后瀏覽器應(yīng)該有所表現(xiàn),不然手動刷新瀏覽器的行為也是蠻愚蠢的。所以當(dāng)我們監(jiān)聽到的文件一旦發(fā)生了修改,瀏覽器就要主動去刷新瀏覽器。
拆招 - webpack-dev-server
我們使用 webpack-dev-server 模塊啟動 webpack 模塊時,webpack 模塊的監(jiān)聽模式默認會被開啟。webpack 模塊會在文件發(fā)生變化時通知 webpack-dev-server 模塊。
通過 webpack-dev-server 啟動時,有以下兩種方式可以實現(xiàn)自動刷新:
-
webpack-dev-server(默認):向要開發(fā)的網(wǎng)頁注入代理客戶端代碼,通過代理客戶端去刷新整個頁面; -
webpack-dev-server --inline false:將要開發(fā)的網(wǎng)頁裝進一個 iframe 中,通過刷新 iframe 去看到最新效果。
見招 - 模塊熱替換
在上面提到的更新后刷新是會刷新整個頁面,這樣的體驗不好。所以 webpack-dev-server 還支持模塊熱替換,就是在不刷新整個頁面的情況下只替換修改的文件,這樣不但快捷,而且數(shù)據(jù)也不會丟失。
拆招 - webpack-dev-server
實現(xiàn)模塊熱替換也有兩種方式:
- webpack-dev-server-hot
- HotModuleReplacementPlugin(推薦)
見招 - 檢查代碼
當(dāng)我們的項目越來越龐大時,特別是多人協(xié)作開發(fā),會導(dǎo)致一個問題就是代碼會有多種風(fēng)格導(dǎo)致可讀性下降。因此我們需要在提交之前執(zhí)行自動化檢查,讓項目成員強制遵守統(tǒng)一的代碼風(fēng)格,同時也可以分析出潛在的問題。
拆招 - **lint 及 husky
**lint 這里指的是針對不同的語言使用不同的 lint 檢查工具。
-
eslint:用來檢查 JavaScript,配置.eslintrc來添加規(guī)則,再結(jié)合eslint-loader就可以通過 webpack 來執(zhí)行代碼檢查; -
tslint:用來檢查 TypeScript,配置tslint.json來添加規(guī)則,再結(jié)合tslint-loader就可以通過 webpack 來執(zhí)行代碼檢查; -
stylelint:用來檢查樣式文件,如 SCSS、Less等,配置.stylelintrc來添加規(guī)則,再結(jié)合stylelint-webpack-plugin就可以通過 webpack 來執(zhí)行代碼檢查;
上面通過整合到 webpack 存在個問題,就是在開發(fā)過程中構(gòu)建速度會變慢很多。所以我們建議在提交的時候通過 Git Hook 來執(zhí)行我們的代碼檢查,如husky,husky 會通過 Npm Script Hook 自動配置好 Git Hook,然后我們只需要在 package.json 添加 script 腳本,其中 precommit 和 prepush 只需要其中一個就好了,配置如下:
{
"scripts": {
// 在執(zhí)行 git commit 前會執(zhí)行的腳本
"precommit": "npm run lint",
//在執(zhí)行 git push 前會執(zhí)行的腳本
"prepush": "lint",
// 調(diào)用 eslint、stylelint 等工具檢查代碼
"lint": "eslint && stylelint"
}
}
其他
除了上面這些,我們可能還需要需要以下的配置:
加載圖片
- file-loader:將 JavaScript 和 CSS 中導(dǎo)入圖片的路徑替換成正確的路徑,并同時將其輸出到對應(yīng)位置;
- url-loader:將文件的內(nèi)容經(jīng)過 base 64 編碼后注入JavaScript 或 CSS 中。
加載SVG
- raw-loader:可以將文本文件內(nèi)容讀取出來,注入到 JavaScript 或 CSS 中。
- svg-inline-loader:跟 raw-loader 一樣,但是增加了對 svg 壓縮的功能。
優(yōu)化
區(qū)分環(huán)境
區(qū)分環(huán)境的好處我就不多解釋了,這里主要是用到了 webpack自帶(當(dāng)代碼出現(xiàn)process時,webpack會將其模塊打包進去)的 process 模塊。使用方法也很簡單 process.env.NODE_ENV 就行了。
壓縮代碼
上線后我們除了GZIP對其文件進行壓縮,我們還需要對文件本身進行壓縮進而減少網(wǎng)絡(luò)傳輸流量和提高網(wǎng)頁加載速度。這里的文件壓縮就是用到了UglifyJsPlugin 插件。詳情配置可以查看官方,需要注意的是,記得區(qū)分環(huán)境如 source-map 等。
壓縮 CSS
壓縮 CSS,用一款基于 PostCSS 的壓縮工具 cssnano。css-loader已經(jīng)內(nèi)置其模塊了,只需要開啟 css-loader 的 minimize 選項即可。
CDN加速
這里不是說要通過前端來做 CDN 加速的事,而是當(dāng)我們上傳靜態(tài)資源時,靜態(tài)資源需要通過 CDN 服務(wù)提供的 URL 地址去訪問,而我們要做的,就是在生成頁面時,將我們的靜態(tài)資源替換為CDN的地址。
我們所說的靜態(tài)資源主要分為兩種,入口 HTML 文件以及 JS、CSS、圖片等靜態(tài)資源。前者的處理方法是存在服務(wù)器而非CDN,并且服務(wù)器不對其做緩存處理,這樣就可以保證每次請求的入口文件都是最新的;后者則會上傳 CDN 服務(wù)上,做緩存處理。
簡單的來說就是入口 HTML 文件是在每一次請求都是最新的,那么其請求的 靜態(tài)資源的 Hash 值也有可能會更新,那么只要發(fā)生變換,則去請求新的靜態(tài)資源就行了。
那么問題來了,怎么做才能每次打包新的 HTML 文件時,其請求的靜態(tài)資源的也會跟隨變化呢?webpack 及其插件提供了其功能,分別為:
-
output.publicPath中設(shè)置 Javascript 的地址; -
css-loader.publicPath中設(shè)置被 CSS 導(dǎo)入的資源的地址; -
Webplugin.stylePublicPath中設(shè)置 CSS 文件的地址。
提取公共代碼
webpack 有個專門用于提取多個 Chunk 中公共部分的插件 CommonsChunkPlugin,用法如下:
const ComrnonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
new CommonsChunkPlugin({
// 從 a、b chunk 提取共同的代碼模塊
chunks: ['a', 'b'],
// 將其封裝到 common 新 chunk
name: 'common',
})
按需加載
在這里只針對 Vue、React 來說。目前比較流行的做法就是在路由上做處理。
Vue
vue-router 通過 vue 的動態(tài)組件 & 異步組件 — Vue.js,就可以實現(xiàn)按需加載了,如:
resolve => require(['./Test'], resolve)
React
react-router 還可以配合 react-loadable,實現(xiàn)路由按需加載,如:
function asyncLoad (loader) {
return Loadable({ loader });
}
asyncLoad(() => import('./Test'));
分析報告
webpack 自帶分析功能webpack --profile --json > stats.json,也可以安裝可視化分析工具webpack-bundle-analyzer更加直觀的觀察項目的情況。
最后
本篇的大多內(nèi)容是閱覽完《深入淺出 webpack》后的總結(jié)。之所以想總結(jié),是因為 webpack 的配置給人的感覺就是配置麻煩很瑣碎。因此就有了這個想法,對知識點的查漏補缺,同時也是一次對知識點的梳理。這篇文章目前主要梳理常用的一些配置、插件以及優(yōu)化。當(dāng)然這也只是冰山一角,更多的還需要自己去查閱官方文檔,不同版本也會有不同的差異性。之后遇到問題,我也會持續(xù)記錄下來。