資料分析基本概念
webpack
#可以做的事情
程式碼轉換、檔案優化、程式碼分割、模組合併、自動重新整理、程式碼校驗、自動釋出
#配套視訊
#最終目的
webpack
的基本配置webpack
的高階配置webpack
的優化策略ast
抽象語法樹webpack
的Tapable
- 掌握
webpack
的流程 手寫webpack
- 手寫
webpack
中常見的loader
- 手寫
webpack
中常見的plugin
#1. 安裝webpack
webpack
:提供了內建的東西 express pluginwebpack-cli
: npx webpack- 服務:
webpack-dev-server
:啟動服務 proxy beforeapp- 不會真正的打包檔案, 只會在記憶體中打包 執行命令
npx webpack-dev-server
- 不會真正的打包檔案, 只會在記憶體中打包 執行命令
#2.配置檔案
let path = require("path");
let HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {//webpack 是node中的一個模組 CommonJs
devServer: {//靜態伺服器的配置
port: 3000,
progress: true,//進度提哦啊
contentBase: "./dist",//靜態資源路徑
compress:true//是否壓縮Gzip
},
mode: "production",//環境
entry: "./src/index.js",
output: {
filename: "bundle[hash:8].js",//設定hash之後會解決瀏覽器快取問題
path: path.resolve(__dirname, "dist")//解析 會把相對路徑解析成絕對路徑
},
plugins: [
new HtmlWebpackPlugin({//打包的時候 自動把html打包到dist目錄
template: "./src/index.html",
filename: "index.html",
minify:{
removeAttributeQuotes:true,//去除雙引號
collapseWhitespace:true//單行壓縮
},
hash:true//是否加hash字尾
})
]
};
- 思考1: 如何壓縮
html
檔案 - 思考2: 如何實現命名的hash串
plugins:[
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true
},
hash: true
})
]
#2.1 修改樣式
#2.2.1 loader配置
如果直接插入css
解決: 下載兩個loader
module: {//模組
rules: [//規則
{
test: /\.css$/,
use: [{
loader: 'style-loader',//將css插入到head中
options: {
insert: 'top'//head/top foot
}
}, 'css-loader']
},
{
test: /\.scss$/,
use: ['style-loader','css-loader', 'sass-loader']
}
],
},
#2.1.1 分離css
但是 此時 我們打包後發現css
是插入在js
裡面的
為了解決這個問題 接下來我們引入mini-css-extract-plugin
這個外掛
let MiniCssExtractPlugin require('mini-css-extract-plugin')
rules: [
{
test: /\.css$/,
use: [{
loader: MiniCssExtractPlugin.loader,
}, 'css-loader']//loader順序的規律
},
{
test: /\.(sc|sa)ss$/,
use: [{
loader: MiniCssExtractPlugin.loader,
}, 'css-loader', 'sass-loader']//loader順序的規律
}
]
當我們加入css3
之後 新的問題出現了 沒有字首
#2.1.3 引入字首
此時 我們需要下載一個包autoprefixer
以及一個loader
檔案postcss-loader
{
test: /\.css$/,
use: [{
loader: MiniCssExtractPlugin.loader,
}, 'css-loader','postcss-loader']//loader順序的規律
},
- 建立一個配置檔案
postcss.config.js
module.exports = {
plugins: [require('autoprefixer')]
};
再次打包
需要注意的是 此設定項只能用早生產環境
mode: 'production',
#2.1.4 壓縮css
檔案
如何壓縮檔案呢
其中有個包optimize-css-assets-webpack-plugin
此包主要是用來壓縮css
的 但是 引入這個包後出現了js
沒被壓縮的問題
怎麼解決呢
按照官網配置需要使用TerserJSPlugin
https://www.npmjs.com/package/mini-css-extract-plugin
optimization: {//webpack4.0之後新出的優化項配置
minimizer: [new TerserJSPlugin({}), new OptimizeCssAssetsPlugin({})]
},
TerserJSPlugin
具體引數檢視這個
interface TerserPluginOptions {
test?: string | RegExp | Array<string | RegExp>;
include?: string | RegExp | Array<string | RegExp>;
exclude?: string | RegExp | Array<string | RegExp>;
chunkFilter?: (chunk: webpack.compilation.Chunk) => boolean;
cache?: boolean | string;
cacheKeys?: (defaultCacheKeys: any, file: any) => object;
parallel?: boolean | number;
sourceMap?: boolean;
minify?: (file: any, sourceMap: any) => MinifyResult;
terserOptions?: MinifyOptions;
extractComments?: boolean
| string
| RegExp
| ExtractCommentFn
| ExtractCommentOptions;
warningsFilter?: (warning: any, source: any) => boolean;
}
#2.2 處理js
檔案
#2.2.1 babel核心模組
當我們嘗試對寫了es6
語法的程式碼進行打包時候
並沒有變成es5
接下來執行命令babel
yarn add babel-loader @babel/core @babel/preset-env
babel-loader
:babel
載入器@babel/core
:babel
的核心模組@babel/preset-env
: 將es6
轉換成es5
@babel/plugin-transform-runtime
@babel/runtime
@babel/polyfill
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {//預設
presets: ['@babel/preset-env']
}
}
]
}
接下來 就是見證奇蹟的時刻
#2.2.2 處理箭頭函式
@babel/preset-env
#2.2.3 處理裝飾器
當我們新增裝飾器 會有如下提示
具體可以檢視官網 https://babeljs.io/docs/en/babel-plugin-proposal-decorators
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {//預設
presets: ['@babel/preset-env'],
plugins:[
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose" : true }]
]
}
}
]
},
index.js
@log
class A {
a = 1;//es7 的語法(es6的變種語法) // let a = new A() a.a = 1
}
function log(target) {
console.log(target,'21');
}
#2.2.4 處理es7
語法
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {//預設
presets: ['@babel/preset-env'],
plugins:['@babel/plugin-proposal-class-properties']
}
}
]
}
a.js
class B {
}
function* fnB() {
yield 1;
}
console.log(fnB().next());
module.exports = 'a';
接下來打包發現 每個檔案都會打包一個_classCallCheck
寫了generator執行也會報錯
出現以上問題的原因是
-
在
webpack
執行時不會自動檢測哪些方法重用了- 一些
es6
的高階語法 比如generator和promise不會轉換成es5
- 一些
根據官方文件https://babeljs.io/docs/en/babel-plugin-transform-runtime#docsNav
需要下載兩個包
yarn add @babel/plugin-transform-runtime @babel/runtime -D
執行npx webpack
但是 報了一些警告
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {//預設
presets: ['@babel/preset-env'],
plugins: [
["@babel/plugin-proposal-decorators", {"legacy": true}],
["@babel/plugin-proposal-class-properties", {"loose": true}],
"@babel/plugin-transform-runtime"
]
}
}
],
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/
},
#2.2.5 處理全域性變數的問題
方法一 : 外接loader
require('expose-loader?$!jquery');
1方法二 : 內建loader
在每個模組都注入$
// rules:
{//內建loader
test: require.resolve('jquery'),
use: 'expose-loader?$'
},
// plugins:
//提供者
new webpack.ProvidePlugin({
"$": "jquery"
})
優化:
如果在html
引入cdn
路徑並且在頁面也import $ from jquery
這就壞了, 即使引入cdn也會打包
//排除之外 加入 在cdn引入了這個包 就不會打包這個包
externals: {
'jquery': '$
}
#2.3 處理圖片檔案
#2.3.1 處理js
中的圖片
index.js
import logo from './logo.png';
<img src=logo/>
webpack.config.js:
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader',
options: {
esModule: false,
},
}
}
#2.3.2 處理css
中圖片檔案
因為css-loader
中已經對圖片做loader
處理了 所以 只需要引入相應路徑就行了
#2.3.3 處理html
中的圖片
//1. 下載依賴
yarn add html-withimg-plugin -D
//2. 配置
{
test:/\.html$/,
use:['html-withimg-plugin']
}
#2.4 多入口多出口
#2.5 webpack小外掛
- clean-webpack-plugin
let {CleanWebpackPlugin} = require('clean-webpack-plugin');
//使用:
plugins:[
new CleanWebpackPlugin()
]
- copy-webpack-plugin
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new CopyPlugin([
{ from: 'source', to: 'dest' },
{ from: 'other', to: 'public' },
]),
],
};
#2.6 resolve、分離
#2.6.1 resolve
resolve:{
modules:[path.resolve(__dirname,'node_modules')],//只從當前這個node_modules查詢相應的包
alise:{//別名
"bootstrapcss":"bootstrap/dist/css/bootstrap.css"
},
extensions:['js','jsx','vue','json','css']
}
#2.6.2 分離檔案 dev、 prod、base
let {smart} = require('webpack-merge')
let base = require('./webpack.config.js')
module.exports = smart(base,{
mode:'production'
})
#2.7 分離打包檔案
#2.8 跨域
- 方式一:在devServer中配置
devServer: {
port: 8080,
host: '0.0.0.0',
quiet: true,
proxy: {
// '/api': 'http://127.0.0.1:3000',
'/api': {
target: 'http://127.0.0.1:3000',
pathRewrite:{
'^/api': ''
}
},
},
before(app) {
//app就是express物件
app.get('/list', function (req, res) {
res.send({code: 1, msg: 'hello'});
});
}
},
- 方式二 : 在服務端配置(node/express)
//1: npm i webpack-dev-middleware
let middleDevWebpack = require('webpack-dev-middleware')
let config = require('./webpack.config.js')
app.use(middleDevWebpack(config))
#2.9 懶載入和熱更新實時監聽
-
熱更新
devServer:{ hot:true, quite:true//安靜啟動 }
-
實時監聽
watch:true, wathcOptions:{ poll:1000, aggregateTimeout:500, ignore:/note_modules/ }
#3. webpack優化
打包優化,可以從幾個出發點點
-
打包體積
-
載入速度
-
打包速度
-
webpack自帶優化
- tree-sharking : import 把沒用的程式碼自動刪除掉
- scope-hoisting : 作用域提升
-
優化網路解析時長和執行時長
- 新增DNS預解析
- 延時執行影響頁面渲染的程式碼
-
優化webpack產出
- 優化程式碼重複打包
- 去掉不必要的import
- babel-preset-env 和 autoprefix 配置優化
- webpack runtime檔案inline
- 去除不必要的async語句
- 優化第三方依賴
- lodash按需引入
-
webpack 知識點
- hash、contenthash、chunkhash的區別
- splitChunks詳解
-
必殺技--動態連結庫
-
多程序打包之HappyPack
-
提取公共程式碼
#3.1 webpack自帶優化
tree-sharking
scope-hoisting
#3.2 多執行緒打包
需要用到happypack
實現多執行緒打包
注意: 如果體積較小會使打包時間更長
#第一步:下載
npm install happypack --save-dev
1const HappyPack = require('happypack');
module.exports = {
...
}
#第二步: 將常用的loader
替換為happypack/loader
const HappyPack = require('happypack');
module.exports = {
...
module: {
rules: [
test: /\.js$/,
// use: ['babel-loader?cacheDirectory'] 之前是使用這種方式直接使用 loader
// 現在用下面的方式替換成 happypack/loader,並使用 id 指定建立的 HappyPack 外掛
use: ['happypack/loader?id=babel'],
// 排除 node_modules 目錄下的檔案
exclude: /node_modules/
]
}
}
#三、建立 HappyPack 外掛
module.exports = {
...
module: {
rules: [
test: /\.js$/,
// use: ['babel-loader?cacheDirectory'] 之前是使用這種方式直接使用 loader
// 現在用下面的方式替換成 happypack/loader,並使用 id 指定建立的 HappyPack 外掛
use: ['happypack/loader?id=babel'],
// 排除 node_modules 目錄下的檔案
exclude: /node_modules/
]
},
plugins: [
...,
new HappyPack({
/*
* 必須配置
*/
// id 識別符號,要和 rules 中指定的 id 對應起來
id: 'babel',
// 需要使用的 loader,用法和 rules 中 Loader 配置一樣
// 可以直接是字串,也可以是物件形式
loaders: ['babel-loader?cacheDirectory']
})
]
}
#3.3 關於語言包的打包
有些包自帶語言包,有時候不需要把所有的語言包跟著打包比如moment
,那麼我們就需要把這個包特殊對待,
主要是通過webpack
自導的IgnorePlugin
src下某.js
import moment from 'moment';
import 'moment/locale/zh-cn';
moment.locale('zh-cn');
let r = moment().endOf('day').fromNow();
console.log(r);
webpack.config.js
plugins: [
...
new webpack.IgnorePlugin(/\.\/locale/,/moment/),
]
#3.3 不打包某個檔案
有些檔案我們不希望打包,比如已經在cdn中引入了的檔案,此時要用externals
進行配置
modules:{
noParse:/jquery/,
...
}
plugins: [
...
new webpack.ProvidePlugin({
'$': 'jquery'
}),
]
//忽略打包的檔案
externals:{
'jquery': '$'
}
#3.4 關於css字首的處理
#3.5 關於js新語法的處理
#3.6 關於檔案拆分的處理
#3.7 關於別名和副檔名的處理
#3.8 webpack必殺技 : 動態連結庫
-
什麼是動態連結庫: 用dll連結的方式提取固定的js檔案,並連結這個js檔案
當我們引入一個js檔案的時候,這個js檔案比較大,那我們是否可以單獨打包,釋出到cdn上,直接引用
-
比如 當我們想要把react打包的時候,希望將react和reactdom放到一個js檔案打包的時候 不打包這兩個檔案,而是直接引用js的cdn路徑
新建一個webpack的js配置檔案
webpack.react.js
var path = require('path');
let webpack = require("webpack");
module.exports = {
mode: 'development',
entry: {
react: ['react', 'react-dom']
},
output:{
filename: '_dll_[name].js',
path: path.resolve(__dirname, 'dist'),
library: '_dll_[name]',
// "var" | "assign" | "this" | "window" | "self" | "global" | "commonjs" | "commonjs2" | "commonjs-module" | "amd" | "amd-require" | "umd" | "umd2" | "jsonp" | "system"
// libraryTarget: 'commonjs2'//預設 var
},
plugins: [
new webpack.DllPlugin({
name: '_dll_[name]',
path: path.resolve(__dirname, 'dist', 'manifest.json')
})
]
};
npx webpack --config webpack.react.js
1此時就會生成一個manifest.json檔案
最後 在webpack.prod.config.js線上配置檔案中引入外掛
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dist', 'manifest.json')
})
#3.9 抽離公共程式碼塊
optimization: {//webpack4.0之後出現的優化項
minimizer: [new TerserPlugin({}), new OptimizeCssAssetsWebpackPlugin({})],//壓縮css
//缺陷 可以壓縮css 但是 js壓縮又出現了問題
splitChunks:{//分割程式碼塊
cacheGroups:{//快取組
common:{//公共的邏輯
chunks: 'initial',//從入口檔案開始查詢
minSize: 0,//最小分包體積
minChunks: 2,//
},
vendor:{
priority: 1,
test:/node_modules/,
chunks: 'initial',
minSize: 0,
minChunks: 2
}
}
}
},
#4. webpack打包原理
#webpack 構建流程
Webpack 的執行流程是一個序列的過程,從啟動到結束會依次執行以下流程 :
- 初始化引數:從配置檔案和 Shell 語句中讀取與合併引數,得出最終的引數。
- 開始編譯:用上一步得到的引數初始化 Compiler 物件,載入所有配置的外掛,執行物件的 run 方法開始執行編譯。
- 確定入口:根據配置中的 entry 找出所有的入口檔案。
- 編譯模組:從入口檔案出發,呼叫所有配置的 Loader 對模組進行翻譯,再找出該模組依賴的模組,再遞迴本步驟直到所有入口依賴的檔案都經過了本步驟的處理。
- 完成模組編譯:在經過第 4 步使用 Loader 翻譯完所有模組後,得到了每個模組被翻譯後的最終內容以及它們之間的依賴關係。
- 輸出資源:根據入口和模組之間的依賴關係,組裝成一個個包含多個模組的 Chunk,再把每個 Chunk 轉換成一個單獨的檔案加入到輸出列表,這步是可以修改輸出內容的最後機會。
- 輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和檔名,把檔案內容寫入到檔案系統。
在以上過程中,Webpack 會在特定的時間點廣播出特定的事件,外掛在監聽到感興趣的事件後會執行特定的邏輯,並且外掛可以呼叫 Webpack 提供的 API 改變 Webpack 的執行結果。
#實踐加深理解,擼一個簡易 webpack
#1. 定義 Compiler 類
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {}
// 重寫 require函式,輸出bundle
generate() {}
}
#2. 解析入口檔案,獲取 AST
我們這裡使用@babel/parser,這是 babel7 的工具,來幫助我們分析內部的語法,包括 es6,返回一個 AST 抽象語法樹。
// webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js'
}
}
//
const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')
const Parser = {
getAst: path => {
// 讀取入口檔案
const content = fs.readFileSync(path, 'utf-8')
// 將檔案內容轉為AST抽象語法樹
return parser.parse(content, {
sourceType: 'module'
})
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {
const ast = Parser.getAst(this.entry)
}
// 重寫 require函式,輸出bundle
generate() {}
}
new Compiler(options).run()
#3. 找出所有依賴模組
Babel 提供了@babel/traverse(遍歷)方法維護這 AST 樹的整體狀態,我們這裡使用它來幫我們找出依賴模組。
const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const Parser = {
getAst: path => {
// 讀取入口檔案
const content = fs.readFileSync(path, 'utf-8')
// 將檔案內容轉為AST抽象語法樹
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍歷所有的 import 模組,存入dependecies
traverse(ast, {
// 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 儲存依賴模組路徑,之後生成依賴關係圖需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {
const { getAst, getDependecies } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
}
// 重寫 require函式,輸出bundle
generate() {}
}
new Compiler(options).run()
#4. AST 轉換為 code
將 AST 語法樹轉換為瀏覽器可執行程式碼,我們這裡使用@babel/core 和 @babel/preset-env。
const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
getAst: path => {
// 讀取入口檔案
const content = fs.readFileSync(path, 'utf-8')
// 將檔案內容轉為AST抽象語法樹
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍歷所有的 import 模組,存入dependecies
traverse(ast, {
// 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 儲存依賴模組路徑,之後生成依賴關係圖需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST轉換為code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
const code = getCode(ast)
}
// 重寫 require函式,輸出bundle
generate() {}
}
new Compiler(options).run()
#5. 遞迴解析所有依賴項,生成依賴關係圖
const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
getAst: path => {
// 讀取入口檔案
const content = fs.readFileSync(path, 'utf-8')
// 將檔案內容轉為AST抽象語法樹
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍歷所有的 import 模組,存入dependecies
traverse(ast, {
// 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 儲存依賴模組路徑,之後生成依賴關係圖需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST轉換為code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {
// 解析入口檔案
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判斷有依賴物件,遞迴解析所有依賴項
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依賴關係圖
const dependencyGraph = this.modules.reduce