模塊化
- 演變過程
階段一. 按文件劃分,每個文件就是一個獨立模塊,代碼中調(diào)用全局成員
問題:污染全局 命名沖突 完全依靠約定
階段二: 命名空間 每個模塊只暴露一個全局對象,模塊成員掛到對象下面
問題: 沒有私有空間,模塊成員仍然可以被修改,模塊依賴關(guān)系問題
階段三: 立即執(zhí)行函數(shù)內(nèi)部掛載對象,對象上掛載對外成員,私有成員在函數(shù)內(nèi)部
commonjs標(biāo)準(zhǔn)
- 一個文件都是一個模塊
- 每個模塊都有單獨作用域
- module.exports導(dǎo)出成員
- require載入模塊
- 同步模式加載模塊
AMD
ESModule
- 自動采用嚴(yán)格模式
- 每個模塊都要單獨作用域
- 通過cors去請求外部js
- script標(biāo)簽延遲執(zhí)行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ES Module - 模塊的特性</title>
</head>
<body>
<!-- 通過給 script 添加 type = module 的屬性,就可以以 ES Module 的標(biāo)準(zhǔn)執(zhí)行其中的 JS 代碼了 -->
<script type="module">
console.log('this is es module')
</script>
<!-- 1. ESM 自動采用嚴(yán)格模式,忽略 'use strict' -->
<script type="module">
console.log(this)
</script>
<!-- 2. 每個 ES Module 都是運行在單獨的私有作用域中 -->
<script type="module">
var foo = 100
console.log(foo)
</script>
<script type="module">
console.log(foo)
</script>
<!-- 3. ESM 是通過 CORS 的方式請求外部 JS 模塊的 -->
<!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->
<!-- 4. ESM 的 script 標(biāo)簽會延遲執(zhí)行腳本 -->
<script defer src="demo.js"></script>
<p>需要顯示的內(nèi)容</p>
</body>
</html>
export import
- export { }是固定寫法,不是導(dǎo)出對象的意思
- export default 后面可以接不同類型變量
- import { } 對應(yīng)export
- import name 對應(yīng) export default
- 對外暴露的成員暴露的是引用關(guān)系
- 導(dǎo)入的成員是只讀成員
- import 引入文件名路徑要完成不能省略,相對路徑不能省略,省略會認(rèn)為在加載第三方模塊,可以是絕對路徑 url
- import {} from './module' 或者 import './module' 執(zhí)行模塊, 而不提取具體成員
- import * as obj from './module'導(dǎo)入所有模塊,所有模塊存儲在對象中
- import from不能動態(tài)導(dǎo)入 ,可以使用import()函數(shù)動態(tài)導(dǎo)入,返回一個promise
import('./module').then(module=>{
console.log(module)
})
默認(rèn)導(dǎo)出與export導(dǎo)出同時存在
import {a,b,c as default} from './modules'
簡寫 import c, {a,b} from './modules'導(dǎo)入模塊作為導(dǎo)出成員 export {a,b} from './module'
script type="nomodule" //不支持 ES Module的瀏覽器才會執(zhí)行
es中可以導(dǎo)入commonjs模塊,作為默認(rèn)導(dǎo)出,反之不可以
webpack
- npm init初始化package.json文件
- 使用 cnpm i webpack webpack-cli -D
- npx webpack 默認(rèn)按照 src/index.js打包
- 自定義配置 項目下新建 webpack.config.js
- webpack --mode development/production/none 開發(fā)模式/生產(chǎn)模式打包/原始狀態(tài)打包 也可以在配置文件中設(shè)置 mode屬性
const path = require('path')
module.exports = {
// 這個屬性有三種取值,分別是 production、development 和 none。
// 1. 生產(chǎn)模式下,Webpack 會自動優(yōu)化打包結(jié)果;
// 2. 開發(fā)模式下,Webpack 會自動優(yōu)化打包速度,添加一些調(diào)試過程中的輔助;
// 3. None 模式下,Webpack 就是運行最原始的打包,不做任何額外處理;
mode: 'development',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}
資源加載
- 加載其他資源通過指定對應(yīng)loader來解析對應(yīng)文件
- module屬性 配置 rules來指定對其他資源加載規(guī)則
- rules中可以指定多個loader,從后往前執(zhí)行 css-loader把css轉(zhuǎn)換為js模塊,style-loader把對應(yīng)模塊以style標(biāo)簽形式插入html中
var path = require("path")
module.exports = {
mode:'development',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, "./dist"),
filename: '.js/bundle.[name].[hash].js'
},
module:{
rules:[
{
test:/.css$/,
use:['style-loader','css-loader']
}
]
}
}
文件資源 loader
- 加載圖片 cnpm i file-loader -D
- base64加載資源 cnpm i url-loader -D
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit:10*1024,//10k一下url-loader處理 以上自動使用file-loader
esModule: false
}
}
],
常用loader
- 編譯轉(zhuǎn)換類loader ex. css-loader => css轉(zhuǎn)換為js模塊
- 文件操作類型loader 把文件輸出到對應(yīng)目錄,同時導(dǎo)出文件的訪問路徑 ex.file-loader
- 代碼檢查loader 統(tǒng)一代碼風(fēng)格 eslint-loader
webpack 加載資源的方式
- 兼容 Es module CommonJs AMD
- 加載的非js也會觸發(fā)資源加載對應(yīng)的loader ex. css中 @import url img的src
- html-loader a標(biāo)簽href默認(rèn)不會觸發(fā)資源加載,可以在options只配置
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
webpack工作原理
- 入口js中通過import/require找到對應(yīng)的文件依賴,分別取解析每一個資源模塊對應(yīng)的依賴,形成一個依賴樹,遞歸資源樹,找到對應(yīng)對應(yīng)資源配置的加載器去加載對應(yīng)模塊,最后把加載結(jié)果放到bundle.js
es6轉(zhuǎn)換
- cnpm i babel-loader @babel/core @babel/preset-env -D
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
自定義loader
- 項目根目錄新建markdown-loader.js
- loader返回結(jié)果必須是一段js代碼,方便交給下一個loader處理
- 負責(zé)輸入到輸出的轉(zhuǎn)換 類型管道,同一個資源可以使用多個loader
const marked = require('marked')
module.exports = source => {
//source接受輸入
const html = marked(source)
// 返回 html 字符串 交給下一個 html-oader 處理
return html
}
webpack.config.js配置
rules: [
{
test: /.md$/,
use: [
'html-loader',
'./markdown-loader' //從后往前執(zhí)行
]
}
]
plugin
- 解決除了資源加載之外的自動化工作 ex:清除目錄 壓縮代碼 拷貝靜態(tài)資源到輸出目錄
- 常用插件 clean-webpack-plugin(清除目錄) html-webpack-plugin(生成html)
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
}),
new CopyWebpackPlugin([
// 'public/**'
'public'
])
]
}
自定義插件 鉤子機制
- 通過在聲明周期鉤子中掛載函數(shù)來擴展
class MyPlugin {
apply (compiler) {
console.log('MyPlugin 啟動')
//webpack即將往輸出目錄輸出文件執(zhí)行
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解為此次打包的上下文
for (const name in compilation.assets) {
// console.log(name)
// console.log(compilation.assets[name].source())
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
監(jiān)視模式運行 webpack --watch
- 編譯自動刷新 cnpm i webpack-dev-server
- npx webpack-dev-server --open 自動運行webpack進行打包并啟動開發(fā)服務(wù)器
此時會監(jiān)聽文件變化自動刷新瀏覽器
并不會生成dist目錄,打包結(jié)果暫存在內(nèi)存中,http-server從內(nèi)存中讀取資源發(fā)送給瀏覽器解析 - devServer配置靜態(tài)資源訪問路徑 一般是開發(fā)階段配置靜態(tài)資源訪問路徑,生成上線打包public目錄
devServer:{
contentBase:'./public'
},
代理
devServer:{
proxy:{
'/api':{
target:'https://xxxxx',
//路徑重寫
pathRewrite:{
'^api':''
},
//使用實際請求的地址作為主機名
changeOrigin:true
}
}
},
sourceMap
- 解決編寫代碼與運行代碼不一致產(chǎn)生的調(diào)試問題
- jquery末尾 中 添加 //# sourceMappingURL=jquery-3.4.1.min.map生成源碼
- webpack 配置 devtool:'source-map'
sourceMap 類型
- eval模式 找問題代碼是打包過后的模塊代碼,每個模塊轉(zhuǎn)換過后的代碼放在eval函數(shù)中執(zhí)行 執(zhí)行最后通過 //# sourceURL = xx指定源碼地址 定位錯誤出現(xiàn)的文件,不會生成sourceMap文件,構(gòu)建速度最快,但無法直接找到錯誤具體行信息
- eval-source-map 想必eval生成了sourceMap ,可以查看錯誤信息行列信息
- cheap-eval-source-map 比eval-source-map輕量 只能看到錯誤行信息,速度更多
- cheap-module-eval-source-map 于cheap-eval-source-map 想比顯示的是沒有經(jīng)過es6編輯的代碼
- inline-source-map 把sourceMap以base64嵌入源代碼中
- hidden-source-map 生成了sourceMap文件,代碼中并沒有引入對應(yīng)map文件
- nosource-source-map 能看到錯誤的行列信息,但看不到源代碼
- webpack.config.js可以導(dǎo)出一個數(shù)組,產(chǎn)生多少打包結(jié)果
選擇souceMap
- 開發(fā)環(huán)境 cheap-module-eval-source-map
- 生成環(huán)境 none/nosource-source-map
//相同入口可以生成 a.js b.js
module.exports = [
{
entry: './src/main.js',
output: {
filename: 'a.js'
}
},
{
entry: './src/main.js',
output: {
filename: 'b.js'
}
}
]
HMR
- 模塊熱更新 頁面不刷新情況下,更新模塊
- 集成在webpack-dev-server中 webpack-dev-sercer --hot開啟或者在配置文件中配置
- webpack-dev-server hot選項設(shè)置為true plugin中
new webpack.HotModuleReplacementPlugin() - js文件熱更新需要自定義處理模塊替換邏輯,默認(rèn)會自動刷新,樣式文件經(jīng)過loader處理,loader中處理過了熱更新,不需要自定義處理
//注冊對應(yīng)模塊發(fā)生變化的處理函數(shù)
module.hot.accept('./edit',function(){
處理邏輯
})
hot改為hotOnly 出現(xiàn)錯誤,不會自動刷新
不同環(huán)境下配置
1.配置文件中添加判斷
配置文件導(dǎo)出一個函數(shù) 函數(shù)中返回配置項
接受兩個參數(shù),一個是cli 命名傳遞的參數(shù) 一個是運行傳遞的所有參數(shù)
-- npx webpack --env production 傳遞參數(shù)打包生成環(huán)境
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = (env, argv) => {
const config = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
}
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
2 多配置文件方式
- webpack.dev.js webpack.common.js webpack.prod.js
- webpack-merge追加新的配置
- npx webpack --config 配置文件名 進行打包
common
const path = require("path")
const webpack = require('webpack')
const merge = require('webpack-merge')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
devServer:{
contentBase:'./public'
},
entry: './src/main.js',
output: {
path: path.resolve(__dirname, "./dist"),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.vue$/,
loader: ['vue-loader'],
exclude: /node_modules/,
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
],
exclude: /node_modules/,
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10 * 1024,//10k一下url-loader處理 以上自動使用file-loader
esModule: false
}
}
],
exclude: /node_modules/,
}
]
},
plugins: [
new CleanWebpackPlugin(),
new VueLoaderPlugin(),
new htmlWebpackPlugin({
title: 'spa單頁應(yīng)用',
url: '',
filename: 'index.html',
template: './index.html'
})
],
}
dev
// 開發(fā)環(huán)境配置
const merge = require('webpack-merge') // webpack 合并配置插件 詳細了解==>(https://github.com/survivejs/webpack-merge)
const common = require('./webpack.common.js') // 引入公共模塊配置
const webpack = require('webpack') // 引入webpack
module.exports = merge(common, {
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist'
},
mode: 'development',
})
prod
// 配置生產(chǎn)環(huán)境
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
devtool:'none',
plugins: [
new UglifyJSPlugin()
new CopyWebpackPlugin(
{
patterns: [
{
from: path.resolve(__dirname, './public'), //定義要拷貝的源目錄,必填項
to: 'public' //定義要拷貝到的目標(biāo)目錄,非必填,不填寫則拷貝到打包的output輸出地址中
}
]
}
),
],
})
DefinePlugin
- 向全局注入全局變量
const webpack = require('webpack')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
// 值要求的是一個代碼片段
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
tree-shaking
- 去掉未引用代碼
- babel-loader使用失效,tree-shaking處理的必須是esModule
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
// 如果 Babel 加載模塊時已經(jīng)轉(zhuǎn)換了 ESM,則會導(dǎo)致 Tree Shaking 失效
// ['@babel/preset-env', { modules: 'commonjs' }]
// ['@babel/preset-env', { modules: false }]
// 也可以使用默認(rèn)配置,也就是 auto,這樣 babel-loader 會自動關(guān)閉 ESM 轉(zhuǎn)換
['@babel/preset-env', { modules: 'auto' }]
]
}
}
}
]
optimization:{
usedExports:true,
// 盡可能合并每一個模塊到一個函數(shù)中
concatenateModules: true,
minimize:true //開啟壓縮
},
副作用 sideEffects
- 模塊執(zhí)行時除了導(dǎo)出成員之外所做的事情
- 一般用于npm包標(biāo)記是否有副作用
- package.json配置 "sideEffects":"false" 標(biāo)識項目代碼沒有副作用
- 一個組件a.js文件引入所有組件,另一個b.js引入了一個a.js中的一個組件,但由于a.js引入了所有組件,打包就會打包所有組件,為了解決這個問題,采用副作用處理,沒有用到的組件模塊就不會打包
- 要確定項目的副作用,否則使用該功能會誤刪有作用的代碼
- package.json 可以設(shè)置哪些文件有副作用
"sideEffects": [
"./src/extend.js",
"*.css"
]
optimization: {
sideEffects: true, //開啟副作用功能
// 模塊只導(dǎo)出被使用的成員
// usedExports: true,
// 盡可能合并每一個模塊到一個函數(shù)中
// concatenateModules: true,
// 壓縮輸出結(jié)果
// minimize: true,
}
代碼分割
- 應(yīng)用比較大時,所有模塊打包到一起,會造成體積很大影響加載速度
- 分包 按需加載 采用動態(tài)導(dǎo)入或者多入口打包來解決
- 多入口打包
- entry采用對象寫法配置多入口
- output filename: '[name].bundle.js' //動態(tài)輸出文件名 name最后會被替換為入口文件的名稱
- HtmlWebpackPlugin 指定chunk來指定輸出html的對應(yīng)打包js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' //動態(tài)輸出文件名 name最后會被替換為入口文件的名稱
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
提取公共模塊
optimization: {
splitChunks: {
// 自動提取所有公共模塊到單獨 bundle
chunks: 'all'
}
},
按需加載
- 需要某個模塊才去加載對應(yīng)模塊
- 采用動態(tài)導(dǎo)入,會被自動分包 import('xx').then(module=>{ })
- /* webpackChunkName: 'components' */ 給分包起名,默認(rèn)是數(shù)字,如果名字起一樣,就會被打包到一個文件中
if (hash === '#posts') {
// mainElement.appendChild(posts())
import(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import(/* webpackChunkName: 'album' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
MiniCssExtractPlugin
- 提取css到單獨文件
- css文件超過一定體積如150kb,才需要考慮提取文件
- webpack默認(rèn)壓縮不會壓縮css需要單獨插件optimize-css-assets-webpack-plugin
- terser-webpack-plugin防止css壓縮覆蓋js壓縮
- minimizer生成環(huán)境默認(rèn)開啟,下面對應(yīng)插件才會運行
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 將樣式通過 style 標(biāo)簽注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin()
]
}
文件hash
- 生產(chǎn)環(huán)境資源文件配置hash防止文件修改緩存
- output與plugin filename 一般支持hash
- [name]-[hash].bound.css 項目級別hash 只要項目任何地方改動,當(dāng)前hash就會發(fā)生改變
- [name]-[chunkhash].bound.css 同一路的文件發(fā)生改變,hash才會改變
- [name]-[contenthash].bound.css 文件hash 文件發(fā)生改變才會觸發(fā)hash改變
- [name]-[contenthash:8].bound.css :數(shù)字指定hash長度