一、模塊化開(kāi)發(fā)
common.js規(guī)范
- 一個(gè)文件就是一個(gè)模塊
- 每個(gè)模塊都有單獨(dú)的作用域
- 通過(guò)module.exports導(dǎo)出成員
- 通過(guò)require函數(shù)載入模塊
commonJS是以同步模式加載模塊
AMD(異步的模塊定義規(guī)范)
Require.js
ES Modules
基本特性
- 自動(dòng)采用嚴(yán)格模式
- 每個(gè)ESM模塊都是單獨(dú)的私有作用域
- ESM是通過(guò)CORS去請(qǐng)求外部JS模塊的
- ESM的script標(biāo)簽會(huì)延遲執(zhí)行腳本
導(dǎo)入和導(dǎo)出
<!--加載模塊不需要提取成員-->
import {} from './modules'
<!--提取模塊的所有成員-->
import * as mod from './modules.js'
<!--動(dòng)態(tài)導(dǎo)入模塊-->
import ('./modules.js').then(modules=>{
console.log(modules)
})
<!--同時(shí)導(dǎo)出默認(rèn)成員和具名成員-->
import title, { name, age } from './modules.js'
直接導(dǎo)出導(dǎo)入成員
1、將多個(gè)模塊統(tǒng)一在一個(gè)文件導(dǎo)出,在從統(tǒng)一入口進(jìn)行引用
2、使用polifill解決瀏覽器不兼容ESmodules的問(wèn)題(只適用于本地測(cè)試開(kāi)發(fā))
<!--在不支持ESModules的文件中使用該polifill文件-->
<script nomodule src="..."><script>
在node環(huán)境下使用ES Modules編寫(xiě)代碼
1、將文件擴(kuò)展名從js改為mjs
<!--index.mjs-->
import { foo, bar} from './modules.mjs'
2、使用node --experimental-module執(zhí)行mjs文件
node --experimental-modules index.mjs
注意事項(xiàng)
1、系統(tǒng)內(nèi)置成員可以通過(guò)ES module的提取成員方式導(dǎo)入,也可以默認(rèn)導(dǎo)入
import fs from 'fs'
import { writeFileSync } from 'fs'
2、第三方模塊不支持直接提取成員,因?yàn)榈谌侥K都是默認(rèn)導(dǎo)出
<!--不支持-->
import { _cameCase } from 'lodash'
<!--僅支持-->
import _ from 'lodash'
console.log(_comCase('ES Module'))
ES modulees 與Common JS模塊交互
- 可以在ES Module中導(dǎo)入commonJS模塊
<!--common.js文件-->
<!--Comonjs模塊始終只會(huì)導(dǎo)出一個(gè)默認(rèn)成員-->
modules.exports = {
foo: 1111
}
====>
exports.foo = 111;
<!--ES Module.mjs文件-->
<!--導(dǎo)入默認(rèn)成員,不能直接提取成員,注意import不是解構(gòu)出對(duì)象-->
import mod from './commonjs.js'
console.log(mod) // { foo :111}
<!--不支持以下寫(xiě)法-->
import { foo } from './commonjs.js'
CommonJs中不能導(dǎo)入ES Module模塊
CommonJs始終只會(huì)導(dǎo)出一個(gè)默認(rèn)成員
注意import不是解構(gòu)導(dǎo)出對(duì)象
在node的最新版本中,在package.json中添加type字段,就表示該工程默認(rèn)使用ES Module編寫(xiě)代碼,這意味著可以不用將js文件改為mjs,不過(guò)此時(shí)如果還需要使用commonJs,需要將CommonJS模塊文件改為cjs后綴名
<!--package.json-->
{
type: "module"
}
<!--運(yùn)行-->
node --exprimental-modules index.js
node --exprimental-modules common.cjs
二、模塊化打包工具
模塊化打包工具的由來(lái)
- 新特性代碼編譯
- 模塊化JavaScript打包
- 執(zhí)行不同類(lèi)型的資源文件
模塊化打包工具概要
打包工具解決的是前端整體的模塊化,并不是單指JavaScript模塊化
webpack
資源文件加載
樣式文件加載
const path = require('path');
module.exports = {
mode:'none',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
}
文件資源加載
const path = require('path');
module.exports = {
mode:'none',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
}
常用加載器分類(lèi)
- 編譯轉(zhuǎn)換類(lèi),轉(zhuǎn)換為JS代碼,如css-loader
- 文件操作類(lèi),將資源文件拷貝到輸出目錄,將文件訪(fǎng)問(wèn)路徑向外導(dǎo)出,如:file-loader
- 代碼檢查器,統(tǒng)一代碼風(fēng)格,提高代碼質(zhì)量,如:eslint-loader
webpack 處理ES2015
因?yàn)槟K打包需要,所以webpack可以處理import和export,除此之外,并不能轉(zhuǎn)換其他的ES6特性。如果想要處理ES6,需要安裝轉(zhuǎn)化ES6的編譯型loader,最常用的就是babel-loader,babel-loader依賴(lài)于babel的核心模塊,@babel/core和@babel/preset-env
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
exclude: /(node_modules)/, // 這個(gè)必須配置
}
注意:Webpack只是打包工具,加載器可以用來(lái)編譯轉(zhuǎn)化代碼
加載資源的方式
遵循ES Modules標(biāo)準(zhǔn)的import聲明
遵循CommonJS標(biāo)準(zhǔn)的require函數(shù)。對(duì)于ES的默認(rèn)導(dǎo)出,要通過(guò)require('./XXX').default的形式獲取
遵循AMD標(biāo)準(zhǔn)的define函數(shù)和require函數(shù)
-
Loader加載的非JavaScript也會(huì)觸發(fā)資源加載
css-loader在處理css代碼時(shí),遇到url函數(shù),會(huì)將這個(gè)資源文件 交給url-loader處理
webpack的核心工作原理
核心工作原理:
- 根據(jù)配置找到打包入口文件
- 順著入口文件代碼里的 import 和 require之類(lèi)的語(yǔ)句
解析推斷文件所依賴(lài)的資源模塊 - 分別去解析每個(gè)資源模塊對(duì)應(yīng)的依賴(lài),最后形成一顆依賴(lài)樹(shù)
- 遞歸依賴(lài)樹(shù),找到每個(gè)節(jié)點(diǎn)對(duì)應(yīng)的資源文件
- 根據(jù)配置文件 rules 屬性,找到資源模塊所對(duì)應(yīng)的加載器,交給對(duì)應(yīng)的加載器加載對(duì)應(yīng)的資源模塊
- 最后將加載以后的結(jié)果放入到bundle.js打包結(jié)果里
實(shí)現(xiàn)整個(gè)項(xiàng)目的打包。
webpack Loader的工作原理
loader機(jī)制是webpack的核心特性之一。每個(gè) Webpack 的 Loader 都需要導(dǎo)出一個(gè)函數(shù),這個(gè)函數(shù)就是我們這個(gè) Loader 對(duì)資源的處理過(guò)程,它的輸入就是加載到的資源文件內(nèi)容,輸出就是我們加工后的結(jié)果。我們通過(guò) source 參數(shù)接收輸入,通過(guò)返回值輸出。
對(duì)于返回的輸出,有兩種思路:
直接在這個(gè) Loader 的最后返回一段 JS 代碼字符串
再找一個(gè)合適的加載器,在后面接著處理我們這里得到的結(jié)果
Webpack 加載資源文件的過(guò)程類(lèi)似于一個(gè)工作管道,你可以在這個(gè)過(guò)程中依次使用多個(gè) Loader,但是最終這個(gè)管道結(jié)束過(guò)后的結(jié)果必須是一段標(biāo)準(zhǔn)的 JS 代碼字符串。
// ./markdown-loader.js
const marked = require('marked')
module.exports = source => {
const html = marked(source)
// const code = `module.exports = ${JSON.stringify(html)}`
const code = `export default ${JSON.stringify(html)}`
return code
}
插件機(jī)制
插件機(jī)制的是webpack的另一個(gè)核心特性,目的是為了增強(qiáng)webpack自動(dòng)化方面的能力。
常見(jiàn)的插件介紹
CleanWebpackPlugin、HtmlWebpackPlugin、CopyWebpackPlugin
插件使用總結(jié)
webpack開(kāi)發(fā)體驗(yàn)問(wèn)題
自動(dòng)進(jìn)行編譯:npx webpack --watch會(huì)監(jiān)視文件的變化自動(dòng)進(jìn)行打包
自動(dòng)打開(kāi)瀏覽器: npx webpack-dev-server --open
source map
Source Map解決了源代碼與運(yùn)行代碼不一致所產(chǎn)生的問(wèn)題.Webpack 支持sourceMap 12種不同的方式,每種方式的效率和效果各不相同。效果最好的速度最慢,速度最快的效果最差.下面是幾種常用方式介紹:
- eval- 是否使用eval執(zhí)行代碼模塊
- cheap- Source map是否包含行信息
- module-是否能夠得到Loader處理之前的源代碼
- inline- SourceMap 不是物理文件,而是以URL形式嵌入到代碼中
- hidden- 看不到SourceMap文件,但確實(shí)是生成了該文件
- nosources- 沒(méi)有源代碼,但是有行列信息。為了在生產(chǎn)模式下保護(hù)源代碼不被暴露
開(kāi)發(fā)模式推薦使用:eval-cheap-module-source-map,原因:
- 代碼每行不會(huì)太長(zhǎng),沒(méi)有列也沒(méi)問(wèn)題
- 代碼經(jīng)過(guò)Loader轉(zhuǎn)換后的差異較大
- 首次打包速度慢無(wú)所謂,重新打包相對(duì)較快
生產(chǎn)模式推薦使用:none,原因:
- Source Map會(huì)暴露源代碼
- 調(diào)試是開(kāi)發(fā)階段的事情
- 對(duì)代碼實(shí)在沒(méi)有信心可以使用nosources-source-map
devtool

webpack HRM
HMR(Hot Module Replacement) 模塊熱替換,應(yīng)用運(yùn)行過(guò)程中,實(shí)時(shí)替換某個(gè)模塊,應(yīng)用運(yùn)行狀態(tài)不受影響。
webpack-dev-server自動(dòng)刷新導(dǎo)致的頁(yè)面狀態(tài)丟失。我們希望在頁(yè)面不刷新的前提下,模塊也可以即使更新。熱替換只將修改的模塊實(shí)時(shí)替換至應(yīng)用中。
HMR是webpack中最強(qiáng)大的功能之一,極大程度的提高了開(kāi)發(fā)者的工作效率。
HMR已經(jīng)集成在了webpack-dev-server中,運(yùn)行webpack-dev-server --hot,也可以通過(guò)配置文件開(kāi)啟.
Webpack中的HMR并不是對(duì)所有文件開(kāi)箱即用,樣式文件支持熱更新,腳本文件需要手動(dòng)處理模塊熱替換邏輯。而通過(guò)腳手架創(chuàng)建的項(xiàng)目?jī)?nèi)部都集成了HMR方案。
HMR注意事項(xiàng):
- 處理HMR的代碼報(bào)錯(cuò)會(huì)導(dǎo)致自動(dòng)刷新
- 沒(méi)啟動(dòng)HMR的情況下,HMR API報(bào)錯(cuò)
- 代碼中多了很多與業(yè)務(wù)無(wú)關(guān)的代碼
生產(chǎn)環(huán)境優(yōu)化
我們?cè)谏a(chǎn)環(huán)境中,更注重開(kāi)發(fā)效率,而在生產(chǎn)環(huán)境中,更注重開(kāi)發(fā)效率。
模式(mode)
webpack建議我們?yōu)椴煌沫h(huán)境創(chuàng)建不同的配置,兩種方案:
- 配置文件根據(jù)環(huán)境不同導(dǎo)出不同配置
const path = require('path')
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: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
module: {
rules: [
{
test: /.md$/,
use: ['html-loader', './markdown-loader.js']
}
]
},
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'
}),
// 開(kāi)發(fā)過(guò)程最好不要使用這個(gè)插件
// new CopyWebpackPlugin({
// patterns: ['public']
// }),
// new MyPlugin(),
new webpack.HotModuleReplacementPlugin()
],
devServer: {
contentBase: './public',
proxy: {
'/api': {// 以/api開(kāi)頭的地址都會(huì)被代理到接口當(dāng)中
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用localhost:8080作為請(qǐng)求GitHub的主機(jī)名
changeOrigin: true, // 以實(shí)際代理的主機(jī)名去請(qǐng)求
}
},
// hot: true
hotOnly: true, // 如果熱替換代碼報(bào)錯(cuò)了,則不刷新
},
devtool: 'eval-cheap-module-source-map'
}
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: ['public']
})
]
}
return config
}
- 一個(gè)環(huán)境對(duì)應(yīng)一個(gè)配置文件
Webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: `bundle.js`
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: `index.html`
})
]
}
Webpack.dev.js
const common = require('./webpack.common')
const merge = require('webpack-merge')
module.export = merge(common, {
mode: 'development',
})
Webpack.prod.js
const common = require('./webpack.common')
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: ['public']
})
]
})
Package.json
"scripts": {
"server": "npx webpack serve --config webpack.dev.js --open",
"build": "webpack --config webpack.prod.js"
},
webpack的優(yōu)化配置
- definePlugin
DefinePlugin 為代碼注入全局成員,這個(gè)內(nèi)置插件默認(rèn)就會(huì)啟動(dòng),往每個(gè)代碼中注入一個(gè)全局變量process.env.NODE_ENV
const webpack = require('webpack')
plugins: [
new HtmlWebpackPlugin({
filename: `index.html`
}),
new webpack.DefinePlugin({
API_BASE_URL: JSON.stringify('http://api.example.com')
})
]`
- Tree-Shaking 搖掉代碼中未引用到的代碼(dead-code),這個(gè)功能在生產(chǎn)模式下自動(dòng)被開(kāi)啟。Tree-Shaking并不是webpack中的某一個(gè)配置選項(xiàng),而是一組功能搭配使用后的效果。因?yàn)門(mén)ree-Shaking前提是ES Modules,由Webpack打包的代碼必須使用ESM,為了轉(zhuǎn)化ES中的新特性,會(huì)使用babel處理新特性,就有可能將ESM轉(zhuǎn)化CommonJS,而我們使用的@babel/preset-env這個(gè)插件集合就會(huì)轉(zhuǎn)化ESM為CommonJS,所以Tree-Shaking會(huì)不生效。但是在最新版babel-loader關(guān)閉了轉(zhuǎn)換ESM的插件,所以使用babel-loader不會(huì)導(dǎo)致Tree-Shaking失效
optimization: {
usedExports: true,
minimize: true
}
- 合并模塊函數(shù) concatenateModules, 又被成為Scope Hoisting,作用域提升
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true
}
- sideEffects 副作用,指的是模塊執(zhí)行時(shí)除了導(dǎo)出成員之外所做的事情,sideEffects一般用于npm包標(biāo)記是否有副作用。如果沒(méi)有副作用,則沒(méi)有用到的模塊則不會(huì)被打包。
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true,
sideEffects: true
}
在package.json里面增加一個(gè)屬性sideEffects,值為false,表示沒(méi)有副作用,沒(méi)有用到的代碼則不進(jìn)行打包。確保你的代碼真的沒(méi)有副作用,否則在webpack打包時(shí)就會(huì)誤刪掉有副作用的代碼,比如說(shuō)在原型上添加方法,則是副作用代碼;還有CSS代碼也屬于副作用代碼。
代碼分割
webpack的一個(gè)弊端:所有的代碼都會(huì)被打包到一起,如果應(yīng)用復(fù)雜,bundle會(huì)非常大。而并不是每個(gè)模塊在啟動(dòng)時(shí)都是必要的,所以需要分包、按需加載。物極必反,資源太大了不行,太碎了也不行。太大了會(huì)影響加載速度;太碎了會(huì)導(dǎo)致請(qǐng)求次數(shù)過(guò)多,因?yàn)樵谀壳爸髁鞯腍TTP1.1有很多缺陷,如同域并行請(qǐng)求限制、每次請(qǐng)求都會(huì)有一定的延遲,請(qǐng)求的Header浪費(fèi)帶寬流量。所以模塊打包時(shí)有必要的。
目前的webpack分包方式有兩種:
- 多入口打包:適用于多頁(yè)應(yīng)用程序,一個(gè)頁(yè)面對(duì)應(yīng)一個(gè)打包入口,公共部分單獨(dú)抽取。
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js'
},
// 每個(gè)打包入口形成一個(gè)獨(dú)立的chunk
plugins: [
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Nulti Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
],
// 不同的打包入口肯定會(huì)有公共模塊,我們需要提取公共模塊:
optimization: {
splitChunks: {
chunks: 'all'
}
}
- 動(dòng)態(tài)導(dǎo)入:需要用到某個(gè)模塊時(shí),再加載這個(gè)模塊,動(dòng)態(tài)導(dǎo)入的模塊會(huì)被自動(dòng)分包。通過(guò)動(dòng)態(tài)導(dǎo)入生成的文件只是一個(gè)序號(hào),可以使用魔法注釋指定分包產(chǎn)生bundle的名稱(chēng)。相同的chunk名會(huì)被打包到一起。
import(/* webpackChunkName: 'posts' */'./post/posts').then({default: posts}) => {
mainElement.appendChild(posts())
}
MiniCssExtractPlugin可以提取CSS到單個(gè)文件
當(dāng)css代碼超過(guò)150kb左右才建議使用。
const MiniCssExtracPlugin = require('mini-css-extract-plugin')
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader',
MiniCssExtracPlugin.loader,
'css-loader'
]
}
]
},
OptimizeCssAssetsWebpackPlugin 壓縮輸出的CSS文件
webpack僅支持對(duì)js的壓縮,其他文件的壓縮需要使用插件。
可以使用 optimize-css-assets-webpack-plugin壓縮CSS代碼。放到minimizer中,在生產(chǎn)模式下就會(huì)自動(dòng)壓縮
optimization: {
minimizer: [
new TerseWebpackPlugin(), // 指定了minimizer說(shuō)明要自定義壓縮器,所以要把JS的壓縮器指指明,否則無(wú)法壓縮
new OptimizeCssAssetWebpackPlugin()
]
}
輸出文件名hash
生產(chǎn)模式下,文件名使用Hash
項(xiàng)目級(jí)別的hash
output: {
filename: '[name]-[hash].bundle.js'
},
chunk級(jí)別的hash
output: {
filename: '[name]-[chunkhash].bundle.js'
},
文件級(jí)別的hash,:8是指定hash長(zhǎng)度 (推薦)
output: {
filename: '[name]-[contenthash:8].bundle.js'
},