之前使用面向對象編程,頁面須要引入多個js,形成多個請求,影響加載,須要注意引用的順序,使用時沒法直接從js裏看出文件的層級關係,一旦出錯調試很麻煩css
// /index.html
<div id="root"></div>
<script src="./header.js"></script>
<script src="./index.js"></script>
// /header.js
function Header() {
var root = document.getElementById('root')
var header = document.createElement('div')
header.innerText = 'header'
root.appendChild(header)
}
// /index.js
new Header()
複製代碼
解決傳統編程的弊端html
建立文件寫代碼:vue
// /index.html
<div id="root"></div>
// /header.js
function Header() {
var root = document.getElementById('root')
var header = document.createElement('div')
header.innerText = 'header'
root.appendChild(header)
}
export default Header
// /index.js
import Header from './header.js'
new Header()
複製代碼
npx webpack index.js # 編譯 index.js 文件,生成 ./dist/main.js 文件node
// /index.html 中引入編譯後的文件
<script src="./dist/main.js"></script>
複製代碼
export default Header // 導出
import Header from './header.js' // 引入
複製代碼
module.exports = Header // 導出
var Header = require('./header.js') // 引入
複製代碼
// package.json
{
"private": true, // 表示該項目是私有項目,不會被髮送到 npm 的線上倉庫
"main": "index.js", // 若是項目不被外部引用,則不須要向外部暴露一個 js 文件,可將該行刪除
"scripts": { // 配置 npm 命令, 簡化命令行輸入的命令
"build": "webpack", // 不用加 npx, 會優先從項目目錄中去找 webpack; 配置以後可以使用 npm run build 代替 npx webpack
}
}
複製代碼
// webpack.config.js
const path = require('path') // 引入一個 node 的核心模塊 path
module.exports = {
entry: './index.js', // 打包入口文件
// entry: { // 上面是該種寫法的簡寫
// main: './index.js'
// },
output: {
filename: 'main.js', // 打包後的文件名
path: path.resolve(__dirname, 'dist') // 打包後文件的路徑(要指定一個絕對路徑); 經過 path 的 resolve 方法將當前路徑(__dirname)和指定的文件夾名(dist)作一個拼接
},
mode: 'production', // 配置打包的模式(production/development); 生產模式(會壓縮)/開發模式(不會壓縮)
}
複製代碼
Hash: d8f9a3dacac977cc0968 # 打包對應的惟一 hash 值
Version: webpack 4.40.2 # 打包使用的 webpack 版本
Time: 208ms # 打包消耗的時間
Built at: 2019-09-20 16:38:59 # 打包的當前時間
Asset Size Chunks Chunk Names
# 生成的文件 文件大小 文件對應id 文件對應名字
main.js 930 bytes 0 [emitted] main
Entrypoint main = main.js # 打包的入口文件
[0] ./index.js 36 bytes {0} [built] # 全部被打包的文件
WARNING in configuration # 警告: 未指定打包的模式(默認會以生產模式打包)
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
複製代碼
webpack 默認知道如何打包 js 文件,loader 的做用就是告訴 webpack 如何打包其它不一樣類型的文件react
使用 file-loader 打包一些圖片文件(須要執行命令 npm i file-loader -D 安裝 file-loader)jquery
// webpack.config.js
module.exports = {
module: {
rules: [{
// test: /\.jpg$/,
test: /\.(jpg|png|gif)$/, // 配置容許匹配多個文件類型
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]', // 配置打包後文件的名稱(name:文件原名;hash:哈希值;ext:文件後綴;最終生成:文件原名_哈希值.文件後綴),若不配置,文件會以哈希值命名
outputPath: 'static/img/' // 配置打包後文件放置的路徑位置
}
}
}]
}
}
複製代碼
與 file-loader 相似,還可使用 url-loader 打包一些圖片文件(一樣須要先安裝)webpack
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'static/img/',
limit: 10240 // 與 file-loader 不一樣的是,能夠配置 limit 參數(單位:字節),當文件大於 limit 值時,會生成獨立的文件,小於 limit 值時,直接打包到 js 文件裏
}
}
}]
}
}
複製代碼
注:url-loader 依賴 file-loader,使用 url-loader 同時須要安裝 file-loaderios
在 webpack 的配置裏,loader 是有前後順序的,loader 的執行順序是從下到上,從右到左的git
注:node-sass沒法安裝時,可採用cnpm或查看node-sass沒法安裝時的解決辦法es6
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.css$/, // .css 結尾的文件使用 style-loader 和 css-loader 打包(須要安裝 style-loader 和 css-loader)
use: ['style-loader', 'css-loader'] // css-loader 會幫咱們分析出多個 css 文件之間的關係,將多個 css 合併成一個 css;style-loader 將 css-loader 處理好的 css 掛載到頁面的 head 部分
}, {
test: /\.scss$/, // .scss 結尾的文件使用 style-loader 和 css-loader 和 sass-loader 打包(須要安裝 style-loader 和 css-loader 和 sass-loader 和 node-sass)
use: ['style-loader', 'css-loader', 'sass-loader'] // 這裏先執行 sass-loader 將 sass 代碼翻譯成 css 代碼;而後再由 css-loader 處理;都處理好了再由 style-loader 將代碼掛載到頁面上
}]
}
}
// index.js
// 配置好以後在 js 中引入 css 便可
import './index.css'
複製代碼
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.css/,
use: ['postcss-loader'] // 須要執行 npm i postcss-loader -D 安裝 postcss-loader
}]
}
}
// postcss.config.js // 在根目錄下建立該文件
module.exports = {
plugins: [
require('autoprefixer') // 須要執行 npm i -D autoprefixer 安裝 autoprefixer
]
}
複製代碼
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.css/,
use: ['style-loader', {
loader: 'css-loader',
options: {
importLoaders: 1 // 有時候會在一個樣式文件裏 import 另外一個樣式文件,這就須要配置 importLoaders 字段,是指在當前 loader 以後指定 n 個數量的 loader 來處理 import 進來的資源(這裏是指在 css-loader 以後使用 sass-loader 來處理 import 進來的資源)
}
}, 'sass-loader']
}]
}
}
複製代碼
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.css$/,
use: ['style-loader', {
loader: 'css-loader',
options: {
modules: true // 開啓 css 的模塊化打包
}
}]
}]
}
}
// index.css(若使用 sass,增長對應 loader 便可)
.avatar {
width: 100px;
height: 100px;
}
// index.js
import style from './index.css'
var img = new Image()
img.src = ''
img.classList.add(style.avatar)
複製代碼
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.(eot|ttf|svg)$/,
use: ['file-loader']
}]
}
}
複製代碼
plugin 能夠在 webpack 運行到某個時刻的時候幫你作一些事情(相似 vue 的生命週期函數同樣)
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [new HtmlWebpackPlugin({
template: 'index.html' // 指定生成 html 的模版文件(若是不指定,則會生成一個默認的不附帶其它內容的 html 文件)
})]
}
複製代碼
// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, 'dist')] // 若不配置,默認刪除 output 下 path 指定的目錄
})]
}
複製代碼
// webpack.config.js
const path = require('path')
module.exports = {
// entry: './src/index.js', // 簡寫方式
entry: {
main: './src/index.js'
},
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
}
}
複製代碼
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index1: './src/a.js',
index2: './src/b.js'
},
output: {
publicPath: 'http://cdn.com.cn', // 會在自動生成的 html 文件中,引入文件路徑的前面加上此路徑
filename: '[name].[hash].js', // name 即指 entry 中配置的須要打包文件的 key (也即 index1 和 index2, 最終會生成 index1.js 和 index2.js)
path: path.resolve(__dirname, 'dist')
},
plugins: [new HtmlWebpackPlugin()]
}
複製代碼
// webpack.config.js
module.exports = {
devtool: 'source-map'
// devtool: 'cheap-module-eval-source-map' // 經常使用於開發環境
// devtool: 'cheap-module-source-map' // 經常使用於生產環境
}
複製代碼
devtool | 解釋 |
---|---|
none | 不生成 sourceMap |
source-map | 生成 .map 文件 |
inline-source-map | 不生成 .map 文件,sourceMap 會被合併到打包生成的文件裏 |
cheap-source-map | 只告訴出錯的行,不告訴出錯的列 |
cheap-module-source-map | 除了業務代碼裏的錯誤,還要提示一些 loader 裏面的錯誤 |
eval | 不生成 .map 文件,使用 eval 在打包後文件裏生成對應關係 |
在 webpack 命令後面加 --watch,webpack 會監聽打包的文件,只要文件發生變化,就會自動從新打包
// package.json
{
"scripts": {
"watch": "webpack --watch"
}
}
複製代碼
// webpack.config.js
module.exports = {
devServer: {}
}
// package.json
{
"scripts": {
"wdserver": "webpack-dev-server"
}
}
複製代碼
open: true // 啓動服務的時候自動在瀏覽器中打開當前項目(默認 false)
port: 8888 // 自定義啓動服務的端口號(默認 8080)
contentBase: './static' // 指定資源的請求路徑(默認 當前路徑)
例如:
/static 文件夾下存在一張圖片 /static/img.png
devServer 裏配置 contentBase: './static'
/index.html 中使用 <img src="img.png" />
這樣它就會去 /static 文件夾下去找 img.png 而不是從根目錄下去找 img.png
複製代碼
藉助 express 和 webpack-dev-middleware 本身手動搭建服務
// server.js(在 node 中使用 webpack)
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackConfig = require('./webpack.config.js')
const complier = webpack(webpackConfig)
const app = express()
app.use(webpackDevMiddleware(complier, {}))
app.listen(3000, () => {
console.log('server is running at port 3000')
})
複製代碼
// package.json
{
"scripts": {
"nodeserver": "node server.js"
}
}
複製代碼
webpack index.js -o main.js # 編譯 index.js 輸出 main.js
HotModuleReplacementPlugin 是 webpack 自帶的一個插件,不須要單獨安裝
// webpack.config.js
const webpack = require('webpack')
module.exports = {
devServer: {
hot: true, // 讓 webpack-dev-server 開啓 hot module replacement 這樣的一個功能
hotOnly: true // 即使是 hot module replacement 的功能沒有生效,也不讓瀏覽器自動刷新
},
plugins: [new webpack.HotModuleReplacementPlugin()]
}
複製代碼
更改樣式文件,頁面就不會整個從新加載,而是隻更新樣式
// /index.html
<!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>html 模板</title>
</head>
<body></body>
</html>
複製代碼
// /src/index.css
div {
width: 100px;
height: 100px;
}
div:nth-of-type(odd) {
background-color: rgb(255, 0, 0);
}
複製代碼
// /src/index.js
import './index.css'
var btn = document.createElement('button')
btn.innerText = 'button'
document.body.appendChild(btn)
btn.onclick = () => {
var item = document.createElement('div')
item.innerText = 'item'
document.body.appendChild(item)
}
複製代碼
// /package.json
{
"name": "webpack-test",
"version": "1.0.0",
"description": "",
"private": false,
"scripts": {
"wdserver": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"css-loader": "^3.2.0",
"html-webpack-plugin": "^3.2.0",
"style-loader": "^1.0.0",
"webpack": "^4.41.1",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.2"
}
}
複製代碼
// /webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
module.exports = {
entry: {
main: './src/index.js'
},
output: {
publicPath: '/',
filename: '[name].[hash].js',
path: path.resolve(__dirname, 'dist')
},
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html'
}),
new webpack.HotModuleReplacementPlugin()
],
devServer: {
hot: true,
hotOnly: true
}
}
複製代碼
更改 number.js 文件中的代碼,只會從頁面上移除 id 爲 number 的元素,而後從新執行一遍 number() 方法,不會對頁面上的其它部分產生影響,也不會致使整個頁面的重載
// /index.html
<!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>html 模板</title>
</head>
<body></body>
</html>
複製代碼
// /src/counter.js
function counter() {
var div = document.createElement('div')
div.setAttribute('id', 'counter')
div.innerText = 1
div.onclick = function() {
div.innerText = parseInt(div.innerText, 10) + 1
}
document.body.appendChild(div)
}
export default counter
複製代碼
// /src/number.js
function number() {
var div = document.createElement('div')
div.setAttribute('id', 'number')
div.innerText = 20
document.body.appendChild(div)
}
export default number
複製代碼
// /src/index.js
import counter from './counter'
import number from './number'
counter()
number()
// 相比 css 須要本身書寫重載的代碼,那是由於 css-loader 內部已經幫 css 寫好了這部分代碼
if (module.hot) {
module.hot.accept('./number', () => {
// 監測到代碼發生變化,就會執行下面的代碼
document.body.removeChild(document.getElementById('number'))
number()
})
}
複製代碼
// /package.json
{
"name": "webpack-test",
"version": "1.0.0",
"description": "",
"private": false,
"scripts": {
"wdserver": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^3.2.0",
"webpack": "^4.41.1",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.2"
}
}
複製代碼
// /webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
module.exports = {
entry: {
main: './src/index.js'
},
output: {
publicPath: '/',
filename: '[name].[hash].js',
path: path.resolve(__dirname, 'dist')
},
mode: 'production',
plugins: [
new HtmlWebpackPlugin({
template: 'index.html'
}),
new webpack.HotModuleReplacementPlugin()
],
devServer: {
hot: true,
hotOnly: true
}
}
複製代碼
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/, // 不去匹配 node_modules 文件夾下的 js
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}]
}
}
複製代碼
上面的步驟,只是作了語法上的翻譯(如: let/const/箭頭函數/... 都會被轉換),但一些新的變量和方法並無被翻譯(如: promise/.map()/...),這時就要使用 @babel/polyfill 來處理
像上面配置好以後,會發現打包後的文件特別大,由於一些沒用到的 ES6 語法也被打包了進去,所以須要作以下操做
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
corejs: 3,
useBuiltIns: 'usage',
targets: { // 經過 targets 指定項目運行的環境,打包時會自動判斷是否須要去解析轉化代碼
chrome: '67'
}
}
]
]
}
}]
}
}
複製代碼
若是寫的是業務代碼,可採用上面方法使用 polyfill 去打包;若是是開發組件或者庫的話,可以使用 plugin-transform-runtime polyfill 會污染全局環境,plugin-transform-runtime 會以閉包的形式幫助組件去引入相關內容 @babel/plugin-transform-runtime 官方文檔
// /index.html
<!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>html 模板</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
複製代碼
// /src/index.js
import React, { Component } from 'react'
import ReactDom from 'react-dom'
class App extends Component {
render() {
return <div>Hello World</div>
}
}
ReactDom.render(<App />, document.getElementById('app'))
複製代碼
// /.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"corejs": 3,
"useBuiltIns": "usage",
"targets": {
"chrome": 67
}
}
],
"@babel/preset-react"
]
}
複製代碼
// /package.json
{
"name": "webpack-test",
"version": "1.0.0",
"description": "",
"private": false,
"scripts": {
"wdserver": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.6.4",
"@babel/polyfill": "^7.6.0",
"@babel/preset-env": "^7.6.3",
"@babel/preset-react": "^7.6.3",
"@babel/runtime-corejs3": "^7.6.3",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^3.0.0",
"core-js": "^3.3.2",
"html-webpack-plugin": "^3.2.0",
"react": "^16.10.2",
"react-dom": "^16.10.2",
"webpack": "^4.41.1",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.2"
}
}
複製代碼
// /webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const webpack = require('webpack')
module.exports = {
entry: {
main: './src/index.js'
},
output: {
publicPath: './',
filename: '[name].[hash].js',
path: path.resolve(__dirname, 'dist')
},
mode: 'development',
devtool: 'cheap-module-eval-source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html'
}),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, 'dist')]
}),
new webpack.HotModuleReplacementPlugin()
],
devServer: {
hot: true,
hotOnly: true
}
}
複製代碼
module.exports = {
optimization: {
usedExports: true
}
}
複製代碼
npm i -D webpack-merge # 安裝 webpack-merge 模塊,做用是將公共的 webpack 配置代碼與開發 / 生產環境中的 webpack 配置代碼進行合併
/build/webpack.common.js # 存放公共的 webpack 配置代碼
// 示例僅展現部分代碼(下同)
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
entry: {
main: './src/index.js'
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html'
}),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [path.resolve(process.cwd(), 'dist')] // __dirname => process.cwd()
})
],
output: {
filename: '[name].[hash].js',
path: path.resolve(__dirname, '../dist') // dist => ../dist
}
}
複製代碼
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const devConfig = {
mode: 'development'
}
module.exports = merge(commonConfig, devConfig)
複製代碼
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const prodConfig = {
mode: 'production'
}
module.exports = merge(commonConfig, prodConfig)
複製代碼
{
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js"
}
}
複製代碼
// /src/lodash.js
import _ from 'lodash'
window._ = _
複製代碼
// /src/index.js
console.log(_.join(['a', 'b', 'c'])) // 輸出a,b,c
console.log(_.join(['a', 'b', 'c'], '***')) // 輸出a***b***c
複製代碼
// /build/webpack.common.conf.js
module.exports = {
entry: {
lodash: './src/lodash.js',
main: './src/index.js'
}
}
複製代碼
webpack 中的代碼分割底層使用的是 SplitChunksPlugin 這個插件
// /src/index.js
import _ from 'lodash'
console.log(_.join(['a', 'b', 'c'])) // 輸出a,b,c
console.log(_.join(['a', 'b', 'c'], '***')) // 輸出a***b***c
複製代碼
// /build/webpack.common.conf.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
複製代碼
// /src/index.js
function getComponent() {
return import('lodash').then(({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['hello', 'world'], '-')
return element
})
}
getComponent().then(element => {
document.body.appendChild(element)
})
複製代碼
經過 import('lodash') 引入,分割打包後的文件名稱是 [id].[hash].js,打包後文件的辨識度不高; 使用 import(/* webpackChunkName: "lodash" */ 'lodash') 來爲打包後的文件起別名,提高辨識度(最終生成文件名稱爲:vendors~lodash.[hash].js,意思是符合 vendors 組的規則,入口是main),詳情可搜索查看 SplitChunksPlugin 的配置 這種方式被稱爲魔法註釋,詳情可查看魔法註釋 Magic Comments 官網地址
注意: 若是報錯「Support for the experimental syntax 'dynamicImport' isn't currently enabled」,可安裝 @babel/plugin-syntax-dynamic-import 進行解決
@babel/plugin-syntax-dynamic-import 官網地址
// npm i -D @babel/plugin-syntax-dynamic-import # 安裝模塊包
// /.babelrc # 配置
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
複製代碼
// /build/webpack.common.conf.js
module.exports = {
output: {
filename: '[name].[hash].js', // 入口文件根據 filename 命名
chunkFilename: '[name].chunk.js', // 非入口文件根據 chunkFilename 命名
path: path.resolve(__dirname, '../dist')
}
}
複製代碼
// SplitChunksPlugin 的默認配置
// webpack.common.conf.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'async', // async:作代碼分割時,只對異步代碼生效;all:對同步和異步代碼都生效;initial:只對同步代碼生效
minSize: 30000, // 單位:字節;當打包的庫大於 minSize 時才作代碼分割,小於則不作代碼分割
maxSize: 0, // 當打包的庫大於 maxSize 時,嘗試對其進行二次分割,通常不作配置
minChunks: 1, // 當一個模塊被用了至少 minChunks 次時,纔對其進行代碼分割
maxAsyncRequests: 5, // 同時加載的模塊數最可能是 maxAsyncRequests 個,若是超過 maxAsyncRequests 個,只對前 maxAsyncRequests 個類庫進行代碼分割,後面的就不作代碼分割
maxInitialRequests: 3, // 整個網站首頁(入口文件)加載的時候,入口文件引入的庫進行代碼分割時,最多隻能分割 maxInitialRequests 個js文件
automaticNameDelimiter: '~', // 打包生成文件名稱之間的鏈接符
name: true, // 打包起名時,讓 cacheGroups 裏的名字有效
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, // 若是是從 node_modules 裏引入的模塊,就打包到 vendors 組裏
priority: -10 // 指定該組的優先級,若一個類庫符合多個組的規則,就打包到優先級最高的組裏
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 若是一個模塊已經被打包過了(一個模塊被多個文件引用),那麼再打包的時候就會忽略這個模塊,直接使用以前被打包過的那個模塊
}
}
}
}
}
複製代碼
注:SplitChunksPlugin 上面的一些配置須要配合 cacheGroups 裏的配置一塊兒使用才能生效(如 chunks 的配置)
// webpack.common.conf.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
filename: 'vendors.js' // 配置 filename 以後,打包會以 filename 的值爲文件名,生成的文件是 vendors.js
},
default: false
}
}
}
}
複製代碼
LazyLoading:懶加載並非 webpack 裏面的概念,而是 ES 裏面的概念;何時執行,何時纔會去加載對應的模塊
// /src/index.js
import _ from 'lodash'
document.addEventListener('click', () => {
var element = document.createElement('div')
element.innerHTML = _.join(['hello', 'world'], '-')
document.body.appendChild(element)
})
複製代碼
// /src/index.js
function getComponent() {
return import(/* webpackChunkName: "lodash" */ 'lodash').then(
({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['hello', 'world'], '-')
return element
}
)
}
document.addEventListener('click', () => {
getComponent().then(element => {
document.body.appendChild(element)
})
})
複製代碼
使用 ES7 的 async 和 await 後,上面代碼能夠改寫成下面這種寫法,效果等同
// /src/index.js
async function getComponent() {
const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash')
const element = document.createElement('div')
element.innerHTML = _.join(['hello', 'world'], '-')
return element
}
document.addEventListener('click', () => {
getComponent().then(element => {
document.body.appendChild(element)
})
})
複製代碼
打包後生成的每個 js 文件,都是一個 chunk
// /package.json
{
"scripts": {
"build": "webpack --profile --json > stats.json"
}
}
複製代碼
附錄:
除了 webpack 官方提供的分析工具,還有不少其它的分析工具,可查看GUIDES/Code Splitting/Bundle Analysis:
改寫前:
// /src/index.js
document.addEventListener('click', () => {
var element = document.createElement('div')
element.innerHTML = 'hello world'
document.body.appendChild(element)
})
複製代碼
改寫後:
// /src/handleClick.js
function handleClick() {
const element = document.createElement('div')
element.innerHTML = 'hello world'
document.body.appendChild(element)
}
export default handleClick
// /src/index.js
document.addEventListener('click', () => {
import('./handleClick.js').then(({ default: func }) => {
func()
})
})
複製代碼
// /build/webpack.common.conf.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'async' // 默認(async)只對異步代碼作分割
}
}
}
複製代碼
Prefetching/Preloading modules 官網地址
/* webpackPrefetch: true */
/* webpackPreload: true */
複製代碼
// /src/handleClick.js
function handleClick() {
const element = document.createElement('div')
element.innerHTML = 'hello world'
document.body.appendChild(element)
}
export default handleClick
// /src/index.js
document.addEventListener('click', () => {
import(/* webpackPrefetch: true */ './handleClick.js').then(({ default: func }) => {
func()
})
})
複製代碼
npm install --save-dev mini-css-extract-plugin
// /build/webpack.common.conf.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
plugins: [
new MiniCssExtractPlugin({})
],
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 使用了 MiniCssExtractPlugin.loader 就不須要 style-loader 了
'css-loader'
]
}
]
}
}
複製代碼
注意: 若是使用了 TreeShaking (排除未使用的代碼)還需配置
// /package.json
{
"sideEffects": ["*.css"]
}
複製代碼
// /package.json
{
"scripts": {
"build": "webpack --config ./build/webpack.prod.conf.js"
}
}
複製代碼
npm run build
// /build/webpack.common.conf.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css', // 打包後的 css 若是被頁面直接引用,就以 filename 的規則命名
chunkFilename: '[name].chunk.css' // 打包後的 css 若是是間接引用的,就以 chunkFilename 的規則命名
})
]
}
複製代碼
// /build/webpack.prod.conf.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})]
}
}
複製代碼
// /src/index.js
import './index1.css'
import './index2.css'
複製代碼
// /build/webpack.prod.conf.js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles', // 打包後的文件名
test: /\.css$/, // 匹配全部 .css 結尾的文件,將其放到該組進行打包
chunks: 'all', // 無論是同步仍是異步加載的,都打包到該組
enforce: true // 忽略默認的一些參數(好比minSize/maxSize/...)
}
}
}
}
}
複製代碼
根據入口文件的不一樣,將 css 文件打包到不一樣的文件裏 參考Extracting CSS based on entry 官網地址
// /build/webpack.prod.conf.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}
}
複製代碼
注意:
對於老版本的 webpack,即使沒有對源代碼作任何的變動,有可能兩次打包的 contenthash 值也不同,這是由於打包生成的文件之間存在關聯,這些關聯代碼叫作 manifest,存在於各個文件中,可經過額外的配置,將關聯代碼提取出來
// /build/webpack.common.conf.js
module.exports = {
optimization: {
runtimeChunk: {
name: 'runtime' // 打包後會多出一個 runtime.js 用於存儲文件之間的關聯代碼
}
}
}
複製代碼
// /build/webpack.common.conf.js
module.exports = {
performance: false
}
複製代碼
// /src/jquery.ui.js
export function ui() {
$('body').css('background-color', _.join(['green'], ''))
}
複製代碼
// /src/index.js
import { ui } from './jquery.ui'
ui()
複製代碼
// /build/webpack.common.conf.js
const webpack = require('webpack')
module.exports = {
plugins: [new webpack.ProvidePlugin({
$: 'jquery',
_: 'lodash',
_join: ['lodash', 'join'] // 若是想直接使用 _join 替代 lodash 的 join 方法,能夠這樣配置
})]
}
複製代碼
// /build/webpack.common.conf.js
module.exports = {
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader'
}, {
loader: 'imports-loader?this=>window'
}]
}]
}
}
複製代碼
以上這些更改 webpack 打包的一些默認行爲,或者說實現一些 webpack 原始打包實現不了的效果,的行爲都叫作 Shimming (墊片的行爲)
// /build/webpack.prod.conf.js
const prodConfig = {
// ...
}
module.exports = prodConfig
複製代碼
// /build/webpack.dev.conf.js
const devConfig = {
// ...
}
module.exports = devConfig
複製代碼
// /build/webpack.common.conf.js
const merge = require('webpack-merge')
const devConfig = require('./webpack.dev.conf.js')
const prodConfig = require('./webpack.prod.conf.js')
const commonConfig = {
// ...
}
module.exports = (env) => {
if (env && env.production) {
return merge(commonConfig, prodConfig)
} else {
return merge(commonConfig, devConfig)
}
}
複製代碼
// package.json
{
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.common.conf.js",
"build": "webpack --env.production --config ./build/webpack.common.conf.js"
}
}
複製代碼
// /src/index.js
export function add(a, b) {
return a + b
}
export function minus(a, b) {
return a - b
}
export function multiply(a, b) {
return a * b
}
export function division(a, b) {
return a / b
}
複製代碼
// /webpack.config.js
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js-math.js',
library: 'jsMath',
libraryTarget: 'umd'
}
}
複製代碼
// /dist/index.html
<script src="./js-math.js"></script>
<script>
console.log(jsMath.add(2, 4)) // 6
</script>
複製代碼
// ES2015 module import:
import jsMath from 'js-math'
jsMath.add(2, 3)
// CommonJS module require:
const jsMath = require('js-math')
jsMath.add(2, 3)
// AMD module require:
require(['js-math'], function(jsMath) {
jsMath.add(2, 3)
})
複製代碼
libraryTarget: 'var' // 讓 library 的值做爲全局變量使用
libraryTarget: 'this' // 將 library 的值掛載到 this 對象上使用
libraryTarget: 'window' // 將 library 的值掛載到 window 對象上使用
libraryTarget: 'umd' // 使其支持在 ES2015/CommonJS/AMD 中使用
複製代碼
import _ from 'lodash'
// /webpack.config.js
module.exports = {
// externals: ['lodash'] // 表示咱們的庫在打包時不把 lodash 打包進去,而是讓業務代碼去加載 lodash
externals: { // 詳細配置
lodash: {
root: '_', // 表示若是 lodash 是經過 script 標籤引入的,必須在頁面上注入一個名爲 _ 的全局變量,這樣才能正確執行
commonjs: 'lodash' // 表示經過 CommonJS 這種寫法去加載時,名稱必須起爲 lodash,如:const lodash = require('lodash')
}
}
}
複製代碼
// /package.json
{
"main": "./dist/js-math.js"
}
複製代碼
去npm 官網註冊 npm 賬號
運行命令 npm adduser # 添加用戶信息 npm publish # 將庫上傳到 npm
npm install js-math # 安裝使用
npm i -D workbox-webpack-plugin # 安裝模塊包
配置:
// /build/webpack.prod.conf.js
const WorkboxPlugin = require('workbox-webpack-plugin')
module.exports = {
plugins: [
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})
]
}
複製代碼
// /src/index.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then(registration => {
console.log('service-worker registed')
})
.catch(error => {
console.log('service-worker register error')
})
})
}
複製代碼
npm run build # 打包項目 打包後會多出兩個文件:precache-manifest.js 和 service-worker.js
啓動一個服務,訪問打包後的項目 斷開服務,刷新瀏覽器,項目仍能正常訪問
npm i -D http-server # 安裝模塊包
配置命令,在 dist 目錄下啓動一個服務
// /package.json
{
"scripts": {
"httpServer": "http-server dist"
}
}
複製代碼
http://127.0.0.1:8080/index.html
注:要在訪問地址後加 /index.html,不然可能會出現報錯npm i -D typescript ts-loader # 安裝模塊包
編寫代碼及配置
// /src/index.tsx
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message
}
greet() {
return 'Hello, ' + this.greeting
}
}
let greeter = new Greeter('world')
// let greeter = new Greeter(123) // 因爲 Greeter 中限定了數據類型爲 string,這裏若是傳非 string 的數據,就會在代碼中報錯
alert(greeter.greet())
複製代碼
// /webpack.config.js
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
}
}
複製代碼
// /tsconfig.json
{
"compilerOptions": {
"outDir": "./dist", // 用 ts-loader 作 TypeScript 代碼打包時,將打包生成的文件放到 ./dist 目錄下(不寫也行,由於 webpack.config.js 中已經配置了)
"module": "es6", // 指的是用 ES Module 模塊的引用方式(即:若是在 index.tsx 文件裏引入其它模塊的話,須要經過 import ... 這種方式去引入)
"target": "es5", // 指的是打包 TypeScript 語法時,要將最終的語法轉換成什麼形式
"allowJs": true // 容許在 TypeScript 語法裏引入 js 模塊
}
}
複製代碼
雖然在寫 TypeScript 代碼時,會有很好的錯誤提示,但有時在 TypeScript 代碼中引入一些其它的庫,調用其它庫的方法時,並無錯誤提示,須要執行如下步驟:
若是不肯定是否有對應庫的類型文件的支持,能夠在GitHub上搜索 DefinitelyTyped,打開後下面有個 TypeSearch 的連接,去 TypeSearch 頁面裏搜索,若是搜索到了,就說明它有對應庫的類型文件的支持,而後安裝便可
// /src/index.js // 使用 axios 模擬請求
import axios from 'axios'
axios.get('/api/data.json').then(res => {
console.log(res)
})
複製代碼
// /webpack.config.js
module.exports = {
devServer: {
proxy: {
// '/api': 'http://...' // 簡寫,若是請求是以 /api 開頭的,就將其代理到 http://... 進行請求
'/api': {
target: 'http://...', // 若請求地址以 /api 開頭,將其代理到 http://... 進行請求
secure: false, // 若是請求地址是 https 的話,須要配置此項
pathRewrite: { // 對一些請求路徑的重寫
'data.json': 'data-test.json'
},
changeOrigin: true, // 能夠幫助咱們改變請求裏的 origin,跳過一些服務端的 origin 驗證
headers: { // 請求轉發時改變請求頭,模擬一些數據
host: 'www...',
cookie: '123...'
}
}
}
}
}
複製代碼
// /src/home.js
import React, { Component } from 'react'
class Home extends Component {
render() {
return <div>HomePage</div>
}
}
export default Home
複製代碼
// /src/list.js
import React, { Component } from 'react'
class List extends Component {
render() {
return <div>ListPage</div>
}
}
export default List
複製代碼
// /src/index.js
import React, { Component } from 'react' // 須要安裝 react 庫
import { BrowserRouter, Route } from 'react-router-dom' // 須要安裝 react-router-dom 庫
import ReactDom from 'react-dom' // 須要安裝 react-dom 庫
import Home from './home.js'
import List from './list.js'
class App extends Component {
render() {
return (
<BrowserRouter>
<div>
<Route path="/" exact component={Home} />
<Route path="/list" component={List} />
</div>
</BrowserRouter>
)
}
}
ReactDom.render(<App />, document.getElementById('root')) // 須要在 html 中寫一個 id 爲 root 的容器
複製代碼
// /webpack.config.js
module.exports = {
devServer: {
historyApiFallback: true // 只需配置該參數,便可經過不一樣的路由加載不一樣的 js
// 注意:這種方法只適用於開發環境中,上線使用須要後端作路由映射處理
}
}
複製代碼
// /.eslintrc.js
module.exports = {
env: { // 指定代碼的運行環境。不一樣的運行環境,全局變量不同,指明運行環境這樣 ESLint 就能識別特定的全局變量。同時也會開啓對應環境的語法支持
browser: true,
es6: true,
},
extends: [
'plugin:vue/essential',
'airbnb-base',
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: [
'vue',
],
rules: { // 這裏能夠對規則進行細緻的定義,覆蓋 extends 中的規則
},
};
複製代碼
// /webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader', {
loader: 'eslint-loader', // 在 babel-loader 處理以前先用 eslint-loader 檢測一下
options: {
fix: true, // 做用同 npx eslint --fix ...
cache: true // 能夠下降 ESLint 對打包過程性能的損耗
// force: 'pre' // 無論 eslint-loader 放在什麼位置,強制它最早執行
}
}]
}
]
},
devServer: {
overlay: true // 配置此項後,【開發環境】在瀏覽器打開項目時,eslint 檢查的一些報錯信息就會以浮層的形式在瀏覽器中展現
}
}
複製代碼
// /webpack.config.js
const path = require('path')
module.exports = {
module: {
rules: [{
exclude: /node_modules/ // 排除應用規則的目錄
// include: path.resolve(__dirname, './src') // 限定應用規則的目錄
}]
}
}
複製代碼
// /webpack.config.js
module.exports = {
resolve: {
extensions: ['.js', '.jsx'], // 當咱們引入一個組件,未指定後綴時(如:import Child from './child/child'),它會自動先去找 ./child/child.js,若是沒有,再去找 ./child/child.jsx,合理的配置可減小查找匹配的次數,下降性能損耗
mainFiles: ['index', 'child'], // 配置該項後,當咱們引入一個文件夾路徑時(如:import Child from './child/'),它就會自動先去找該文件夾下的 index,若是沒有,再去找 child。同上,該配置不易過多,不然影響性能
alias: { // 配置別名
child: path.resolve(__dirname, './src/child') // 使用時就能夠這樣寫:import Child from 'child'
}
}
}
複製代碼
// /build/webpack.dll.js
const path = require('path')
module.exports = {
mode: 'production',
entry: {
vendors: ['lodash', 'jquery']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]' // 打包生成一個庫,並暴露在全局變量 [name](即:vendors)中
}
}
複製代碼
// /package.json
{
"scripts": {
"build:dll": "webpack --config ./build/webpack.dll.js"
},
"dependencies": {
"jquery": "^3.4.1",
"lodash": "^4.17.15"
}
}
複製代碼
// /webpack.config.js
const path = require('path')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
module.exports = {
plugins: [
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, './dll/vendors.dll.js') // 指的是要往 HtmlWebpackPlugin 生成的 index.html 里加的內容
})
]
}
複製代碼
// /package.json
{
"scripts": {
"build": "webpack"
}
}
複製代碼
至此,第三方模塊只打包一次,並引入生產打包代碼中的目標已經實現了 可是 /src/index.js 中 import _ from 'lodash'
使用的仍是 node_modules 裏面的庫 接下來須要實現的是:引入第三方模塊的時候,讓它從 dll 文件裏引入,而不是從 node_modules 裏引入
// /build/webpack.dll.js
// 經過該配置文件打包會生成相似於庫的打包結果
const path = require('path')
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.DllPlugin({
// 使用 webpack 自帶的插件對打包產生的庫文件進行分析
// 把庫裏面一些第三方模塊的映射關係放到 path 對應的文件裏
name: '[name]', // 暴露出的 DLL 函數的名稱
path: path.resolve(__dirname, '../dll/[name].manifest.json') // 分析結果文件輸出的絕對路徑
})
]
}
複製代碼
// /webpack.config.js
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, './dll/vendors.manifest.json')
})
]
}
複製代碼
// /build/webpack.dll.js
module.exports = {
entry: {
vendors: ['lodash', 'jquery'],
react: ['react', 'react-dom']
}
}
複製代碼
結合 5.3 的配置,此時打包輸出的文件有: /dll/vendors.dll.js
/dll/vendors.manifest.json
/dll/react.dll.js
/dll/react.manifest.json
而後配置 /webpack.config.js
// /webpack.config.js
module.exports = {
plugins: [
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, './dll/vendors.dll.js')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, './dll/vendors.manifest.json')
}),
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, './dll/react.dll.js')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, './dll/react.manifest.json')
})
]
}
複製代碼
注:若是打包生成的 dll 文件有不少,就須要在 /webpack.config.js 中添加不少的 plugin,爲了簡化代碼,能夠藉助 node 去分析 dll 文件夾下的文件,循環處理,代碼以下:
// /webpack.config.js
const fs = require('fs') // 藉助 node 中的 fs 模塊去讀取 dll 文件夾
const plugins = [ // 初始存入一些基礎的 plugin
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
const files = fs.readdirSync(path.resolve(__dirname, './dll'))
files.forEach(file => {
if (/.*\.dll.js/.test(file)) {
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, './dll', file)
}))
}
if (/.*\.manifest.json/.test(file)) {
plugins.push(new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, './dll', file)
}))
}
})
module.exports = {
plugins
}
複製代碼
// /src/index.js
console.log('home page')
// /src/list.js
console.log('list page')
複製代碼
// /build/webpack.common.conf.js
module.exports = {
entry: { // 配置多個入口文件
main: './src/index.js',
list: './src/list.js'
},
plugins: [
// 配置多個打包輸出頁面
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: 'index.html',
chunks: ['runtime', 'vendors', 'main'] // 不一樣的頁面引入不一樣的入口文件(如有 runtime 或者 vendors 就引入,沒有就不寫)
}),
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: 'list.html',
chunks: ['runtime', 'vendors', 'list']
})
]
}
複製代碼
如上,若是每增長一個頁面,就手動增長代碼的話,就會致使大量重複代碼,下面開始對打包配置代碼進行優化:
// /build/webpack.common.conf.js
const fs = require('fs')
const makePlugins = configs => { // 自定義方法 makePlugins,用於動態生成 plugins
const plugins = [
// 初始能夠存一些基本的 plugin,如:CleanWebpackPlugin
]
// 根據不一樣的入口文件,生成不一樣的 html
// Object.keys() 方法會返回一個由給定對象枚舉屬性組成的數組
Object.keys(configs.entry).forEach(item => {
plugins.push(new HtmlWebpackPlugin({
template: 'src/index.html',
filename: `${item}.html`,
chunks: [item]
}))
})
// 動態添加並使用打包生成的一些第三方 dll 庫
const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
files.forEach(file => {
if (/.*\.dll.js/.test(file)) {
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll', file)
}))
}
if (/.*\.manifest.json/.test(file)) {
plugins.push(new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file)
}))
}
})
return plugins
}
const configs = {
// 將 module.exports 導出的一堆配置放到變量 configs 裏
entry: {
index: './src/index.js',
list: './src/list.js'
}
// ...
// 這裏不寫 plugins,經過一個方法去生成 plugins
}
configs.plugins = makePlugins(configs) // 調用 makePlugins 自定義的方法,生成 plugins
module.exports = configs // 導出重組好的 configs
複製代碼
// /src/index.js
console.log('hello world !')
複製代碼
// /loaders/replaceLoader.js
module.exports = function(source) {
return source.replace('hello', '你好') // 對源代碼執行一個替換
}
複製代碼
// /package.json
{
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1"
}
}
複製代碼
// /webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
module: {
rules: [{
test: /\.js$/,
use: [
path.resolve(__dirname, './loaders/replaceLoader.js')
]
}]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
複製代碼
在 /loaders/replaceLoader.js 中,除了經過 return 返回處理後的源代碼以外,還可使用 this.callback 作返回處理
// /loaders/replaceLoader.js
module.exports = {
const result = source.replace('hello', '你好')
this.callback(null, result)
}
複製代碼
// /loaders/replaceLoader.js
module.exports = function(source) {
// 可經過 this.query 獲取使用 loader 時 options 裏面傳遞的配置
console.log(this.query) // { name: 'xiaoli' }
return source.replace('hello', '你好')
}
複製代碼
// /webpack.config.js
const path = require('path')
module.exports = {
module: {
rules: [{
test: /\.js$/,
use: [{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
options: {
name: 'xiaoli'
}
}]
}]
}
}
複製代碼
// /loaders/replaceLoader.js
const loaderUtils = require('loader-utils')
module.exports = {
const options = loaderUtils.getOptions(this)
console.log(options) // { name: 'xiaoli' }
}
複製代碼
若是 loader 裏調用一些異步的操做(好比延遲 return),打包就會報錯,說 loader 沒有返回內容,須要使用 this.async()
// /loaders/replaceLoaderAsync.js
module.exports = function(source) {
const callback = this.async()
setTimeout(() => {
const result = source.replace('hello', '你好')
callback(null, result) // 這樣調用的 callback 實際上就是 this.callback()
}, 1000)
}
複製代碼
// /loaders/replaceLoaderAsync.js
module.exports = function(source) {
const callback = this.async()
setTimeout(() => {
const result = source.replace('hello', '你好')
callback(null, result)
}, 1000)
}
複製代碼
// /loaders/replaceLoader.js
module.exports = function(source) {
const result = source.replace('world', '世界')
this.callback(null, result)
}
複製代碼
// /webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.js$/,
use: [{
loader: path.resolve(__dirname, './loaders/replaceLoader.js')
}, {
loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js')
}]
}]
}
}
複製代碼
// /webpack.config.js
module.exports = {
resolveLoader: {
// 當你引入一個 loader 的時候,它會先到 node_modules 裏面去找,若是找不到,再去 loaders 目錄下去找
modules: ['node_modules', './loaders']
},
module: {
rules: [{
test: /\.js$/,
use: [{
loader: 'replaceLoader'
}, {
loader: 'replaceLoaderAsync'
}]
}]
}
}
複製代碼
// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
// 構造函數
constructor() {
console.log('插件被使用了')
}
// 當調用插件時,會執行 apply 方法,該方法接收一個參數 compiler,能夠理解爲 webpack 的實例
apply(compiler) {}
}
module.exports = CopyrightWebpackPlugin
複製代碼
// /src/index.js
console.log('hello world !')
複製代碼
// /package.json
{
"name": "plugin",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10"
}
}
複製代碼
// /webpack.config.js
const path = require('path')
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
plugins: [new CopyrightWebpackPlugin()],
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
複製代碼
// /webpack.config.js
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js')
module.exports = {
plugins: [new CopyrightWebpackPlugin({
name: 'li' // 這裏傳遞參數
})]
}
複製代碼
// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
constructor(options) {
// 經過 options 接收參數
console.log(options)
}
apply(compiler) {}
}
module.exports = CopyrightWebpackPlugin
複製代碼
// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
apply(compiler) {
// emit 是指當你把打包的資源放到目標文件夾的時刻
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, callback) => {
console.log('插件執行了')
callback()
})
}
}
module.exports = CopyrightWebpackPlugin
複製代碼
// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, callback) => {
// 打包生成的全部內容是存放在 compilation.assets 裏面的
// 在 emit 時刻的時候,向打包生成的內容裏增長一個 copyright.txt 文件
compilation.assets['copyright.txt'] = {
// 文件裏的內容
source: function() {
return 'copyright text ...'
},
// 文件的大小
size: function() {
return 18
}
}
callback()
})
}
}
複製代碼
// /package.json
{
"scripts": {
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js"
}
}
複製代碼
done (異步時刻)表示打包完成
compile (同步時刻)
同步時刻是 tap 且沒有 callback
異步時刻是 tapAsync 有 callback
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
console.log('compile 時刻執行')
})
複製代碼
// /src/word.js
export const word = 'hello'
複製代碼
// /src/message.js
import { word } from './word.js'
// 須要寫 .js 後綴,由於沒有使用 webpack
const message = `say ${word}`
export default message
複製代碼
// /src/index.js
import message from './message.js'
console.log(message)
複製代碼
npm i @babel/parser # 做用是分析代碼,產生抽象語法樹
npm i @babel/traverse # 做用是幫助咱們快速找到 import 節點
// /bundler.js
// 此文件就是咱們要作的打包工具
// 打包工具是用 nodeJs 來編寫的
// node 的一個用於讀取文件的模塊
const fs = require('fs')
const path = require('path')
// 使用 babelParser 分析代碼,產生抽象語法樹
const parser = require('@babel/parser')
// 默認導出的內容是 ESModule 的導出,若是想用 export default 導出內容,須要在後面加個 .default
const traverse = require('@babel/traverse').default
const moduleAnalyser = filename => {
// 以 utf-8 編碼讀取入口文件的內容
const content = fs.readFileSync(filename, 'utf-8')
// console.log(content)
// 分析文件內容,輸出抽象語法樹
const ast = parser.parse(content, {
sourceType: 'module'
})
// ast.program.body 便是文件內容中的節點
// console.log(ast.program.body)
const dependencies = {}
// 對抽象語法樹進行遍歷,找出 Node.type === 'ImportDeclaration' 的元素,並作處理
traverse(ast, {
ImportDeclaration({ node }) {
// console.log(node)
const dirname = path.dirname(filename)
const newFile = './' + path.join(dirname, node.source.value)
dependencies[node.source.value] = newFile
}
})
// console.log(dependencies)
// 將入口文件和對應依賴返回出去
return {
filename, // 入口文件
dependencies // 入口文件裏的依賴
}
}
// 傳入口文件,調用方法
moduleAnalyser('./src/index.js')
複製代碼
// /bundler.js
const babel = require('@babel/core')
const moduleAnalyser = filename => {
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
// code 就是瀏覽器能夠運行的代碼
return {
code
}
}
// 分析轉化以後的結果
const moduleInfo = moduleAnalyser('./src/index.js')
console.log(moduleInfo)
複製代碼
npm i cli-highlight -g // 安裝 cli-highlight node bundler.js | highlight // 運行時在後面加上 | highlight
// /bundler.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencies = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
const newFile = './' + path.join(dirname, node.source.value)
dependencies[node.source.value] = newFile
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return {
filename,
dependencies,
code
}
}
const makeDependenciesGraph = entry => {
const entryModule = moduleAnalyser(entry)
const graphArray = [ entryModule ]
// 循環遍歷依賴中的依賴
for(let i = 0; i < graphArray.length; i++) {
const item = graphArray[i]
const { dependencies } = item
if (dependencies) {
for(let j in dependencies) {
graphArray.push(
moduleAnalyser(dependencies[j])
)
}
}
}
// 將遍歷後的依賴數組轉化成對象的形式
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph
}
// 最終分析生成的代碼和依賴信息
const graphInfo = makeDependenciesGraph('./src/index.js')
console.log(graphInfo)
複製代碼
// /bundler.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencies = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
const newFile = './' + path.join(dirname, node.source.value)
dependencies[node.source.value] = newFile
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return {
filename,
dependencies,
code
}
}
const makeDependenciesGraph = entry => {
const entryModule = moduleAnalyser(entry)
const graphArray = [ entryModule ]
for(let i = 0; i < graphArray.length; i++) {
const item = graphArray[i]
const { dependencies } = item
if (dependencies) {
for(let j in dependencies) {
graphArray.push(
moduleAnalyser(dependencies[j])
)
}
}
}
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph
}
const generateCode = entry => {
const graph = JSON.stringify(makeDependenciesGraph(entry))
return `
(function(graph) {
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath])
}
var exports = {}
(function(require, exports, code) {
eval(code)
})(localRequire, exports, graph[module].code)
return exports
}
require('${entry}')
})(${graph})
`
}
const code = generateCode('./src/index.js')
console.log(code)
複製代碼
(function(graph) {
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
};
var exports = {};
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;
};
require('./src/index.js');
})({"./src/index.js":{"dependencies":{"message.js":"./src\\message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src\\message.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\nvar word = 'hello';\nexports.word = word;"}});
複製代碼