入門
先簡單地提下模塊化的思想。
模塊化
簡單來說就是將複雜的系統分解成各個簡單的子模塊,便於開發和維護。
一般 JavaScript 模塊化規範有 CommonJS,AMD 和 ES6 中的模塊化。
CommonJS
其核心思想就是利用 require
來同步加載依賴的模塊,通過 module.exports
來暴露模塊的接口。
優點在於
- 代碼可在 node.js 環境下運行
- NPM 包中大部分模塊都支持 CommonJS
缺點在於:無法在瀏覽器環境下運行,要想運行必須通過工具轉換。
AMD
異步模塊定義(Asynchronous Module Definition) 是以異步的方式加載模塊,主要用於解決瀏覽器的模塊化加載,比如像 require.js.
優點
- 可無需轉換即可在瀏覽器環境中運行
- 可異步加載模塊
- 可並行加載多個模塊
- 可在node和瀏覽器環境中運行
缺點:瀏覽器沒有原生支持 AMD,若要使用需要導入相應的包。
ES6 的模塊化
它是 ECMA 提出的 JavaScript 模塊化規範,是在語言層面上實現的。他將逐漸取代 CommonJS 和 AMD 規範。但是目前仍然無法運行在大部分 JavaScript 運行環境中,所以需要轉換工具轉成 ES5 後才能運行。
tip: CSS 樣式文件中也開始支持模塊化,比如 SASS, SCSS, less
等
不僅這些模塊化規範需要轉換工具才能運行,還有目前流行 React 中 JSX 語法和 vue 中的 template 也需要進行轉換,還有當前流行 es6,typescript 也需要轉換。所以隨着項目越來越複雜和技術越來越新,不可避免的需要出現構建工具來統一的管理這些轉換工具。
常見的構建工具
構建工具主要的工作如下:
- 代碼轉換
- 文件優化:壓縮靜態資源文件
- 分割代碼,將公共代碼分離出來
- 模塊合併
- 實現熱更新
- 自動校驗
- 實現自動發佈
主要的構建工具有(誕生的時間排序):
- Npm Script:屬於 NPM 內置的功能,在 package.json 文件中的 script 對象中配置,其中每個屬性對應一個 shell 命令。
- Grunt:通過執行任務的方式進行構建。
- Gulp:一種基於流的自動化構建工具,除了可以管理和執行任務,還能監聽文件,讀寫文件。
- Webpack:專注於構建模塊化項目,在 webpack 裏,一切文件都被視爲模塊。通過 loader 轉換文件,通過 Plugin 注入鉤子,最後輸出有多個模塊組合而成的文件。
- Rollup:和 webpack 類似,他的亮點就是可以 Tree shaking,以減小輸出文件大小和提高運行性能。後期 webpack 也是實現了 Tree Shaking。
選擇 Webpack 的原因
- 可以爲新項目提供一站式的解決方案
- 有良好的生態和維護團隊
- 被大量使用,經受住了大家的考驗。
安裝
# 初始化 npm 包
npm init
# 以下三選一
# 穩定版
npm install webpack -D
# 指定版
npm install webpack@<version> -D
# 最新的體驗版
npm install webpack@beta -D
請看以下 webpack 使用實例(文件都在根目錄)
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app"></div>
<!-- 導入webpack打包輸出的文件 -->
<script src="./dist/bundle.js"></script>
</body>
</html>
show.js
function show(content){
document.getElementById("app").innerText = "hello, " + content;
}
// 通過 CommonJS 規範導出show函數
module.exports=show;
main.js
// 通過 CommonJS 規範導入show函數
const show = require('./show.js')
show("webpack")
wepack.config.js
const path = require('path')
module.exports = {
// javascript 執行入口文件
entry: './main.js',
output: {
// 將所有依賴的模塊合併輸出到bundle.js文件中
filename: "bundle.js",
// 將輸出文件都放到此目錄下
path: path.resolve(__dirname, "./dist")
}
}
然後在根目錄執行打包命令 webpack
會看到生成 dist 目錄,裏面會有打包好的文件bundle.js。
打開 index.html 文件便可看到構建效果。
從 Webpack 2版本開始,就已經內置了轉換 CommonJS、ES6、AMD模塊化的功能
簡單的 Loader 使用示例
假如我們需要給頁面添加樣式,新建了 main.css
文件:
#app{
color:red;
}
爲了讓樣式生效,我們還需要在 main.js
中導入:
// 通過 CommonJS 規範導入 main.css
require('main.css')
// 通過 CommonJS 規範導入show函數
const show = require('./show.js')
show("webpack")
因爲 webpack 原生是不支持非 JavaScript 文件轉換的,所以直接通過webpack
打包命令可能會報錯:
ERROR in ./main.css 1:0
Module parse failed: Unexpected character '#' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> #app{
| color: red;
| }
@ ./main.js 2:0-21
由此需要引出 Loader 概念,它就是用來處理文件轉換的。如果需要支持 CSS 模塊的轉換,首先需要先安裝相關 Loader:
npm install -D style-loader css-loader
之後在 webpack.config.js
文件中配置:
const path = require('path')
module.exports = {
// javascript 執行入口文件
entry: './main.js',
output: {
// 將所有依賴的模塊合併輸出到bundle.js文件中
filename: "bundle.js",
// 將輸出文件都放到此目錄下
path: path.resolve(__dirname, "./dest")
},
// 配置 Loader
module: {
rules: [
{
test:/\.css$/,
// css-loader負責讀取css文件,再通過 style-loader 注入到js中
use:['style-loader', 'css-loader?minimize']
}
]
}
}
通過在 module.rules
數組中配置一系列規則,以便告知 webpack 根據哪些 Loader 去轉換指定的文件。其中 use
屬性需要注意以下幾點:
-
Loader 的執行順序是由後向前的。
-
每個 Loader 傳參方式有兩種:
-
通過 URL querystring 的方式,比如
css-loader?minimize
就是告知 Loader 要開啓 CSS 壓縮。 -
通過options 方式:
const path = require('path') module.exports = { // javascript 執行入口文件 entry: './main.js', output: { // 將所有依賴的模塊合併輸出到bundle.js文件中 filename: "bundle.js", // 將輸出文件都放到此目錄下 path: path.resolve(__dirname, "./dest") }, module: { rules: [ { test:/\.css$/, use:['style-loader', { loader:'css-loader', options: { minimize:true } }] } ] } }
-
注:webpack3.0 和 css-loader1.0 以上就不支持 minimize 了,爲了講解傳參方式,還是用了它。
簡單的 Plugin 使用示例
Plugin 是用來擴展 webpack 功能的,通過在構建流程中注入鉤子實現。比如我們需要將 CSS 從 bundle.js 文件中分離出來,可以用 extract-text-webpack-plugin
,首先還是先安裝 Plugin:
npm install extract-text-webpack-plugin -D
然後在 webpack.config.js 文件中配置:
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
let plugins = []
plugins.push(new ExtractTextPlugin({
// 提取出來的 css 文件名
filename: `[name]_[contenthash:8].css`
}))
module.exports = {
// javascript 執行入口文件
entry: './main.js',
output: {
// 將所有依賴的模塊合併輸出到bundle.js文件中
filename: "bundle.js",
// 將輸出文件都放到此目錄下
path: path.resolve(__dirname, "./dist")
},
module: {
rules: [
{
test:/\.css$/,
use:ExtractTextPlugin.extract({
fallback:'style-loader',
use:['css-loader']
})
}
]
},
plugins
}
**注:webpack4.0 以上就不再支持 extract-text-webpack-plugin **,可以通過安裝新的版本解決:npm install --D extract-text-webpack-plugin@next
使用 DevServer
一般我們開發網頁調試有以下幾個需求:
- 提供 http 服務而不是不是本地文件預覽
- 監聽文件內容變化,實時更新頁面
- 提供 sourcemap 功能,以便調試頁面
後兩個功能 webpack 原生支持。第一個需求可使用 webpack-dev-server
,它可以啓動 http 服務用於網絡請求,並同時執行 webpack 命令,將構建後的文件存儲在內存中。所以執行 webpack-dev-server
命令是不會創建 dist
目錄,所以在 .index.html
中靜態資源路徑需要將以前的./dist/bundle.js
修改爲 ./bundle.js
。
第二個功能可以通過給 webpack-dev-server
命令加 --hot
參數實現:
也就是常說的熱替換,在不重新刷新頁面的情況下通過替換舊模塊來更新頁面。
webpack-dev-server --hot
第三個功能可以通過給 webpack-dev-server
命令加 --devtool source-map
參數實現:
webpack-dev-server --devtool source-map
核心概念
以下幾個核心概念需要清楚:
- Entry:入口
- Module:模塊
- Chunk:代碼塊
- Loader:模塊轉換器
- Plugin:擴展插件
- Output:輸出結果
webpack 的流程:
webpack 啓動時會先找到 Entry 配置裏 Module,去解析 Entry 依賴的所有 Module。每找到一個 Module 就會根據 Loader 裏的相應規則進行轉換,轉換後會再次解析當前 Module 依賴的所有 Module,這些模塊會以 Entry 爲單位進行分組,一個 Entry及其依賴的所有 Module 形成一個組,也就是一個 chunk,webpack 會將所有的 chunk 轉換成輸出文件。在構建中,webpack會在恰當的時機執行 plugin 中的邏輯。
配置
webpack 配置總的來說有兩種方式,一種通過配置文件webpack.config.js
,另一種則是通過命令行的方式,如webpack --devtool map-resource
本章主要講解下以下配置概念:
- Entry:配置模塊的入口文件
- Output:配置構建輸出文件信息
- Module:配置解析模塊的規則
- Resolve:配置尋找模塊的規則
- Plugins:配置擴展插件
- DevServer:配置 DevServer
- 配置總結
Entry
context
webpack 在處理相對路徑時會以 context
爲根目錄,context
默認爲當前配置文件目錄,淡然也可以通過以下兩種方式修改:
- 修改配置文件webpack.config.js
module.exports={
context:path.resolve(__dirname, 'app')
}
- 命令行式改變
webpack --context path
Entry 類型
entry可以是三種類型:
- String:
'./app/entry'
,只輸出一個Chunk,Chunk的名稱是main - Array:
['./app/entry1', './app/entry2']
,只輸出最後一個元素的 Chunk,Chunk的名稱是main - Object:
{a:'./app/entry-a',b:'./app/entry-b'}
,輸出多個Chunk,Chunk的名稱是對象對應的鍵值
動態配置 Entry
當入口文件是動態時,就需要給 Entry 傳入函數,如下配置:
// 同步函數
entry:()=>{
return {
a:'./app/entry-a',
b:'./app/entry-b'
}
}
// 或者異步函數
entry:()=>{
return new Pormise((resolve)=>{
resolve({
a:'./app/entry-a',
b:'./app/entry-b'})
})
}
Output
常見的屬性有:
filename
:配置輸出文件的名稱path
:配置輸出文件的目錄publicPath
:用於處理靜態資源引用地址
filename 和 path
比如:
output:{
filename:'bundle.js',
path:path.resolve(__dirname, './dist')
}
filename
除了可以配置靜態名稱也可以配置動態名稱:
filename: [id].js
:chunk的唯一標識符,從0開始filename: [name].js
:chunk的名稱filename: [hash].js
:chunk 唯一標識hash,可以取hash前幾位,比如取前8位:filename: [hash:8].js
filename: [chunkhash].js
:chunk 內容的 hash,可以取hash前幾位,比如取前8位:filename: [hash:8].js
其他屬性
chunkFilename
publicPath
crossOriginLoading
libraryTarget
和 librarylibraryExport
- 等等…
Module
主要用來配置模塊處理功能,裏面主要有 rules
屬性,Array 類型,用於加載和解析模塊文件,用於配置 Loader 規則。
rules
元素有以下幾個常用的屬性:
- test
- use
- noParse
- parser
配置 Loader
通過以下的示例來說明 Loader 配置。
module: {
rules: [
{
// 命中 js 文件
// test 也可以接收數組類型,其中每個元素條件之間是“或”的關係
test:/\.js$/,
// 用 babel-loader 轉換 JavaScript 文件
// ?cacheDirectory 表示傳給 babel-loader 的參數,用於緩存 babel 的便宜結果,加快編譯速度
// use 數組裏的loader執行順序爲從右向左,也可以通過enforce強制設置執行位置。
use:['babel-loader?cacheDirectory'],
// 只命中 src 目錄裏的 JavaScript 文件,加快 Webpack 的搜索速度
include:path.resolve(__dirname,'src'),
// 通過 exclude 排除 node_modules 目錄下的文件,
// 一般來講,include和exclude不必要同時配置,在此爲了說明兩者用法,故同時配置
exclude: path.resolve(__dirname,'node_modules'),
// 配置哪些內置模塊語法被解析
parser: {
amd:false, // 禁用 AMD
commonjs:false, // 禁用 CommonJS
system:false, // 禁用 SystemJS
harmony:false, // 禁用ES6 import/export
// 等等...
},
}
],
// 忽略符合以下規則模塊的解析, noparse 可以是 RegExp,[RegExp],function中的一種
noParse: /jquery|chartjs/
}
Resolve
用來告知 webpack 按照何種規則來尋找模塊。
可由以下示例來講解:
resolve: {
// 配置別名
alias: {
'@':'./src/components',
},
// 決定優先是哪個入口文件代碼,查找順序從左至右
mainFields: ['browser','main'],
// 配置擴展名,當導入文件的擴展名省略時,從以下數組中依次匹配查找,查找順序從左至右
extensions: ['.ts','.js','.json'],
// './src/components' 和 'node_modules' 路徑下的模塊導入的相對路徑可以其作爲根目錄
// 比如 './src/components' 的button模塊,直接可以通過 import 'button' 導入
modules:['./src/components', 'node_modules']
// 配置第三方模塊文件名稱描述,也就是 package.json
descriptionFiles: ['package.json'],
// 模塊導入時是否必須帶擴展名
enforceExtension: true,
// 模塊導入時是否必須帶擴展名,只針對 node_modules 下的模塊生效
enforceModuleExtension: false
},
Plugin
Plugin 用於擴展 webpack,配置很簡單,接收數組類型,每個元素都是 Plugin的示例,Plugin的參數由構造函數傳入,比如:
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
plugins:[new ExtractTextPlugin({
// 提取出來的 css 文件名
filename: `style.css`
})]
}
難點在於各自 Plugin 自身的參數配置。
DevServer
DevServer 除了在命令行配置參數,也可以在配置文件webpack.config.js中配置
module.exports={
devServer: {
host:'127.0.0.1',
port:8080
}
}
此配置項僅針對 devServer
命令有效,webpack
命令會忽略此配置項。
之前也講了它的簡單用法,以下講下其常用的配置:
hot
:用於熱替換host
:用設置web服務器地址port
:用於設置端口號https
:用於設置https服務,默認是http服務。open
:在第一次構建完是否自動打開網頁
其他配置項
devtool
:用於設置 source-map,便於調試watch
和watchOptions
:用於監聽文件改動,webpack-dev-server
命令自動開啓externals
:告知以下文件無需打包。- 等等…
實戰
具體配置可查看書籍或相關Loader介紹。
優化
優化方向有兩個:
- 優化開發效率(針對開發人員)
- 提高構建速度
- 優化開發體驗
- 優化輸出質量(針對用戶)
- 減少首屏加載時間
- 提升流暢度
縮小文件的搜索範圍
- 優化 Loader 配置:針對
module
配置- 可以添加 noParse 屬性,排除一些非模塊化文件,像
noparse:/jquery|chart\.js/
。確保這些文件中沒有模塊化操作。 - 在 rules 屬性中給其 Loader 儘可能添加 include 或 exclude 屬性以縮小解析範圍。
- 可以添加 noParse 屬性,排除一些非模塊化文件,像
resolve
配置:extensions
的數組長度儘可能的短,頻率高的後綴名放前面。- 儘可能的在
alias
中配置依賴模塊的具體位置。 - 等等…
使用 HappyPack
使用 HappyPack 插件進行多進程構建。它會接管某些 Loader 任務,並行執行這些任務。
使用 parallelUglifyPlugin
插件
webpack 內置了 UglifyPlugin
用於壓縮代碼,parallelUglifyPlugin
插件可以多進程並行壓縮代碼。
使用自動刷新優化開發體驗
主要通過配置 watch
實現。
監聽文件是否發生改變,如果發生改變則在一定時間內重新構建輸出文件,並通過向網頁中注入代理客戶端代碼的方式來實現自動刷新功能。
開啓模塊熱替換優化開發體驗
原理:只需編譯發生變化了的代碼,再將新的輸出模塊替換瀏覽器中舊的模塊。
優勢:
- 構建速度快。
- 可以在保存瀏覽器狀態的情況下更新頁面。
區分環境
通過 DefinePlugin
插件來定義 process.env.NODE_ENV
的值。
壓縮代碼
JavaScript 壓縮原理:插件通過分析 JavaScript 語法分析樹,按照一定的規則將代碼中的輸出日誌、註釋等一些無用代碼去除。
CSS 壓縮原理:理解 CSS 含義,比如會將 color:#ff0000
壓縮成 color:red
,壓縮率可達到 60%。
- 壓縮 ES5
- 可用之前說的
UglifyPlugin
和ParallelUglifyPlugin
插件進行配置,也可以啓動webpack
時帶上--optimize-minimize
參數,它會自動配置UglifyPlugin
插件的默認參數。
- 可用之前說的
- 壓縮 ES6
- 可用
uglifyESPlugin
插件進行壓縮,需要注意的是壓縮 ES6 代碼需要運行環境支持ES6語法纔有意義。
- 可用
- 壓縮 CSS
- 通過給
css-loader
Loader添加minimize
參數進行壓縮:use:['css-loader?minimize']
- 通過給
CDN 加速
將不同的靜態資源放在不同域名的CDN下,可以利用webpack
進行配置
使用 Tree Shaking
Tree Shaking 插件是爲了剔除沒有用到的死代碼。
使用 Tree shaking 的前提是模塊化必須是 ES6 語法,因爲其導入導出的路徑必須是靜態字符串,不能出現在代碼塊中。像 CommonJS 就不一樣:require(x+y)
提取公共代碼
爲什麼要提取?
因爲如果每個頁面都包含大量公共代碼的話,會導致:
- 相同的資源被重複加載,浪費用戶流量和服務器成本
- 每個頁面需要加載的資源過大,影響頁面渲染
如何提取?
先將所有頁面中用到的基礎依賴庫(如react,react-dom)提取到單獨的一個文件 base.js 中。然後再從所有頁面中提取出不包含base.js中代碼的公共代碼到common.js文件中,然後將每個網頁剩餘的代碼單獨包裝成一個文件。
如圖:
提取公共代碼可利用 webpack 內置的 commonsChunkPlugin 插件
分割代碼以按需加載
像現在流行的單頁應用,將所有的功能整合到了一個 HTML 文件中。其實每次只是運用到其中的一部分功能,爲了使用一部分功能而加載所有資源而使得網頁加載相對緩慢。爲了解決此問題,則可以將整個頁面分成一個個小的功能,而根據功能之間的相關程度進行分類,把每一類合併成一個chunk,然後再按需加載每一個Chunk。
可以使用 import(*)
語句實現按需加載。
使用 Prepack
通過深入分析JavaScript代碼邏輯,預先執行代碼邏輯,然後將執行結果輸出,此技術還未成熟。
比如:
// 轉換前
function hello(){
console.log("hello,world")
}
hello();
// 轉換後
console.log("hello,world")
開啓 Scop Hoisting
可以進一步的減少打包體積,通過將多個函數進行合併,但是前提是運用ES6語法的模塊。
可利用 webpack 內置的功能進行開啓
輸出分析
如果需要分析webpack 的詳細輸出信息,可在命令行添加以下參數
--profile
:記錄構建過程中的耗時信息--json
:以 json 格式記錄輸出文件信息
如:webpack --profile --json > state.json
>
爲 LINUX 中的管道命令,就是將輸出結果存到 state.json
文件中
可以通過以下兩種方式來可視化分析 state.json
文件信息
- Webpack Analyse:一個web應用,登錄其官網上傳 json 文件即可分析
- webpack-bundle-analyzer:一個全局插件,在
state.json
所在目錄下輸入webpack-bundle-analyzer
即可分析state.json
。
原理
工作原理概括
核心概念
- Entry:構建入口。
- Ouput:構建輸出信息。
- Module:模塊,在 webpack 中每個文件都當作是一個模塊。
- Chunk:代碼塊,有多個模塊組成,用於代碼合併與分割。
- Loader:模塊轉換器,Loader 根據需求將原有文件轉成新的內容。
- Plugin:Webpack 擴展插件,webpack 在構建的過程中會廣播一些事件,而 Plugin 則通過監聽相應的事件來擴展某些功能。
構建流程
主要分三大階段:
- 初始化:啓動構建,讀取合併 webpack 配置參數,加載插件,初始化 compiler 對象。
- 編譯:讀取 Entry 配置中的模塊,根據匹配規則調用相應的 Loader 對其進行轉換,並找到其依賴的模塊進行遞歸解析轉換,並確定各個模塊之間的依賴關係。
- 輸出:將有聯繫的多個Module合併成一個chunk,再將每個chunk輸出成一個文件並保存在系統中,保存信息由 Output 配置決定。
編寫 Loader
簡單來說輸出一個轉換函數。
Npm Link 可用於調試開發本地的 Npm 模塊。
編寫 Plugin
以下有些常用的屬性和function總結如下:
compilation.chunk
:數組類型,存放所有的chunk
chunk.forEachModule(module=>{})
:遍歷每個module
module.fileDependencies
:數組類型,表示當前模塊所依賴的文件路徑chunk.files
:數組類型,表示chunk
輸出的文件,有可能不只一個文件。compilation.assets[filename].source()
:表示filename
輸出文件的內容compilation.fileDependencies
:數組類型,表示文件依賴列表compiler.options.plugins
:當前配置使用的所有插件列表