Webpack已經出來很久了,相關的文章也有很多,然而比較完整的例子卻不是很多,讓很多新手不知如何下腳,下腳了又遍地坑
說實話,官方文檔是蠻亂的,而且有些還是錯的錯的。。很多配置問題隻有爬過坑才知道
本文首先介紹Webpack(3)的一些基礎知識,然後以一個已經完成的小Demo,逐一介紹如何在項目中進行配置
該Demo主要包含編譯Sass/ES6,提取(多個)CSS檔案,提取公共檔案,子產品熱更新替換,開發與線上環境區分,使用jQuery插件的方式、頁面資源引入路徑自動生成(可指定生成位置),熱更新編譯模版檔案自動生成webpack伺服器中的資源路徑,編寫一個簡單的插件,異步加載子產品 等基礎功能
應該能幫助大家更好地在項目中使用Webpack3來管理前端資源
本文比較啰嗦,可以直接看第四部分Webpack3配置在Demo中的應用,或者直接去Fork這個Demo邊看邊玩
Webpack已更新為v4版本,優化之後性能提升好幾倍,請移步這個 webpack4項目配置Demo,以及 這篇更新優化點
首先,學習Webpack,還是推薦去看官方文檔,還是挺全面的,包括中文的和英文的,以及GitHub上關于webpack的項目issues,還有就是一些完整了例子,最後就是得自己練手配置,才能在過程中掌握好這枯燥的配置。
- 1. 為什麼要用Webpack
- 2. 什麼是Webpack
- 3. Webpack的基礎配置
- 1. webpack的配置方式主要有三種
- 1. 通過cli指令行傳入參數
- 2. 通過在一個配置檔案設定相應配置,導出使用
- 3. 通過使用NodeJS的API配置
- 2. 常見的幾個配置屬性
- 1. context 絕對路徑
- 2. entry 子產品入口檔案設定
- 3. resolve 處理資源的查找引用方式
- 4. output 設定檔案的輸出
- 5. devtool指定sourceMap的配置
- 6. module指定子產品如何被加載
- 7. plugins設定webpack配置過程中所用到的插件
- 1. webpack的配置方式主要有三種
- 4. Webpack3配置在Demo中的應用
- 1. 搭建個伺服器
- 2. 設定基礎項目目錄
- 3. 開發和生産環境的Webpack配置檔案區分
- 4. 設定公共子產品
- 5. 編譯ES6成ES5
- 6. 編譯Sass成CSS,嵌入到頁面<style>标簽中,或将其提取出(多個)CSS檔案來用<link>引入
- 7. jQuery插件的引入方式
- 8. HtmlWebpackPlugin将頁面模闆編譯成最終的頁面檔案,包含JS及CSS資源的引用
- 9. 使用url-loader和file-loader和html-loader來處理圖檔、字型等檔案的資源引入路徑問題
- 10. 子產品熱更新替換的正确姿勢
- 11. 壓縮子產品代碼
- 12. 異步加載子產品
- 13. 其他配置
- 14. 自定義HtmlWebpackPlugin插件編譯模版檔案生成的JS/CSS插入位置
- 15. 熱更新編譯模版檔案自動生成webpack伺服器中的資源路徑
- 16. 其他常見問題整理
一 、為什麼要用Webpack
首先,得知道為什麼要用webpack
前端本可以直接HTML、CSS、Javascript就上了,不過如果要處理檔案依賴、檔案合并壓縮、資源管理、使用新技術改善生活的時候,就得利用工具來輔助了。
以往有常見的子產品化工具RequireJS,SeaJS等,建構工具Grunt、Gulp等,新的技術Sass、React、ES6、Vue等,要在項目中使用這些東西,不用工具的話就略麻煩了。
其實簡單地說要聚焦兩點:子產品化以及自動建構。
子產品化可以使用RequireJS來處理依賴,使用Gulp來進行建構;也可以使用ES6新特性來處理子產品化依賴,使用webpack來建構
兩種方式都狠不錯,但潮流所驅,後者變得愈來愈強大,當然也不是說後者就替代了前者,隻是大部分情況下,後者更好
二、什麼是Webpack
如其名,Web+Pack 即web的打包,主要用于web項目中打包資源進行自動建構。
Webpack将所有資源視為JS的子產品來進行建構,是以對于CSS,Image等非JS類型的檔案,Webpack會使用相應的加載器來加載成其可識别的JS子產品資源
通過配置一些資訊,就能将資源進行打包建構,更好地實作前端的工程化
三、Webpack的基礎配置
可以認為Webpack的配置是4+n模式,四個基本的 entry(入口設定)、output(輸出設定)、loader(加載器設定)、plugin(插件設定),然後加上一些特殊功能的配置。
使用Webpack首先需要安裝好NodeJS
node -v
npm -v
確定已經可以使用node,使用NPM包管理工具來安裝相應依賴包(網絡環境差可以使用淘寶鏡像CNPM來安裝)
npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm -v
全局安裝好webpack包
npm i -g webpack
webpack -v
1. 通過cli指令行傳入參數
webpack ./src.js -o ./dest.js --watch --color
// ./webpack.config.js檔案
module.exports = {
context: ...
entry: {
},
output: {
}
};
// 指令行調用(不指定檔案時預設查找webpack.config.js)
webpack [--config webpack.config.js]
這個和第二點有點類似,差別主要是第二種基本都是使用{key: value}的形式配置的,API則主要是一些調用
另外,某些插件的在這兩種方式的配置上也有一些差別
最常用的是第二種,其次第三種,第一種不太建議單獨使用(因為相對麻煩,功能相對簡單)
一般當做入口檔案(包括但不限于JS、HTML模闆等檔案)的上下文位置,
預設使用目前目錄,不過建議還是填上一個
// 上下文位置
context: path.resolve(__dirname, 'static')
可以接受字元串表示一個入口檔案,不過一般來說是多頁應用多,就設定成每頁一個入口檔案得了
比如home對應于一個./src/js/home子產品,這裡的key會被設定成webpack的一個chunk,即最終webpack會又三個chunkname:home | detail | common
也可以對應于多個子產品,用數組形式指定,比如這裡把jquery設定在common的chunk中
也可以設定成匿名函數,用于動态添加的子產品
// 檔案入口配置
entry: {
home: './src/js/home',
detail: './src/js/detail',
// 提取jquery入公共檔案
common: ['jquery']
},
如上方其實是省略了後JS綴,又比如想在項目中引入util.js 可以省略字尾
import {showMsg} from './components/util';
// 處理相關檔案的檢索及引用方式
resolve: {
extensions: ['.js', '.jsx', '.json'],
modules: ['node_modules'],
alias: {
}
},
最基礎的就是這三個了
path指定輸出目錄,要注意的是這個目錄影響範圍是比較大,與該chunk相關的資源生成路徑是會基于這個路徑的
filename指定生成的檔案名,可以使用[name] [id]來指定相應chunk的名稱,如上的home和detail,用[hash]來指定本次webpack編譯的标記來防緩存,不過建議是使用[chunkhash]來依據每個chunk單獨來設定,這樣不改變的chunk就不會變了
hash放在?号之後的好處是,不會生成新的檔案(隻是檔案内容被更改了),同時hash會附在引用該資源的URL後(如script标簽中的引用)
publicPath指定所引用資源的目錄,如在html中的引用方式,建議設定一個
// 檔案輸出配置
output: {
// 輸出所在目錄
path: path.resolve(__dirname, 'static/dist/js'),
filename: '[name].js?[chunkhash:8]'// 設定檔案引用主路徑
publicPath: '/public/static/dist/js/'
}
5.devtool指定sourceMap的配置
如果開啟了,就可以在浏覽器開發者工具檢視源檔案
// 啟用sourceMap
devtool: 'cheap-module-source-map',

比如這裡就是對應的一個source Map,建議在開發環境下開啟,幫助調試每個子產品的代碼
這個配置的選項是滿多的,而且還可以各種組合,按照自己的選擇來吧
通過設定一些規則,使用相應的loader來加載
主要就是配置module的rules規則組,通過use字段指定loader,如果隻有一個loader,可以直接用字元串,loader要設定options的就換成數組的方式吧
或者使用多個loader的時候,也用數組的形式,規則不要用{ }留白,在windows下雖然正常,但在Mac下會報錯提示找不到loader
多個loader遵循從右到左的pipe 的方式,如下 eslint-loader是先于babel-loader執行的
通過exclude或include等屬性再确定規則的比對位置
// 子產品的處理配置,比對規則對應檔案,使用相應loader配置成可識别的子產品
module: {
rules: [{
test: /\.css$/,
use: 'css-loader'
}, {
test: /\.jsx?$/,
// 編譯js或jsx檔案,使用babel-loader轉換es6為es5
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
}
}, {
loader: 'eslint-loader'
}]
}
比如下方為使用webpack自帶的提取公共JS子產品的插件
// 插件配置
plugins: [
// 提取公共子產品檔案
new webpack.optimize.CommonsChunkPlugin({
chunks: ['home', 'detail'],
filename: '[name].js',
name: 'common'
}),
new ...
]
這就是webpack最基礎的東西了,看起來内容很少,當然還有其他很多,但複雜的地方在于如何真正去使用這些配置
四、Webpack配置在Demo中的應用
下面以一個相對完整的基礎Demo着手,介紹一下幾個基本功能該如何配置
Demo項目位址 建議拿來練練
既然是Demo,至少就得有一個伺服器,用node來搭建一個簡單的伺服器,處理各種資源的請求傳回
建立一個伺服器檔案server.js,以及頁面檔案目錄views,其他資源檔案目錄public
伺服器檔案很簡單,請求什麼就傳回什麼,外加了一個gzip的功能
let http = require('http'),
fs = require('fs'),
path = require('path'),
url = require('url'),
zlib = require('zlib');
http.createServer((req, res) => {
let {pathname} = url.parse(req.url),
acceptEncoding = req.headers['accept-encoding'] || '',
referer = req.headers['Referer'] || '',
raw;
console.log('Request: ', req.url);
try {
raw = fs.createReadStream(path.resolve(__dirname, pathname.replace(/^\//, '')));
raw.on('error', (err) => {
console.log(err);
if (err.code === 'ENOENT') {
res.writeHeader(404, {'content-type': 'text/html;charset="utf-8"'});
res.write('<h1>404錯誤</h1><p>你要找的頁面不存在</p>');
res.end();
}
});
if (acceptEncoding.match(/\bgzip\b/)) {
res.writeHead(200, { 'Content-Encoding': 'gzip' });
raw.pipe(zlib.createGzip()).pipe(res);
} else if (acceptEncoding.match(/\bdeflate\b/)) {
res.writeHead(200, { 'Content-Encoding': 'deflate' });
raw.pipe(zlib.createDeflate()).pipe(res);
} else {
res.writeHead(200, {});
raw.pipe(res);
}
} catch (e) {
console.log(e);
}
}).listen(8088);
console.log('伺服器開啟成功', 'localhost:8088/');
頁面檔案假設采用每一類一個目錄,目錄下的tpl為源檔案,另外一個為生成的目标頁面檔案
/public目錄下,基本配置檔案就放在根目錄下,JS,CSS,Image等資源檔案就放在/public/static目錄下
我們要利用package.json檔案來管理編譯建構的包依賴,以及設定快捷的腳本啟動方式,是以,先在/public目錄下執行 npm init 吧
public/static/dist目錄用來放置編譯後的檔案目錄,最終頁面引用的将是這裡的資源
public/static/imgs目錄用來放置圖檔源檔案,有些圖檔會生成到dist中
public/static/libs目錄主要用來放置第三方檔案,也包括那些很少改動的檔案
public/static/src 用來放置js和css的源檔案,相應根目錄下暴露一個檔案出來,公共檔案放到相應子目錄下(如js/components和scss/util)
最後檔案結構看起來是這樣的,那就可以開幹了
首先在項目目錄下安裝webpack吧
npm i webpack --save-dev
用Webpack來建構,在開發環境和生産環境的配置還是有一些差別的,建構是耗時的,比如在開發環境下就不需要壓縮檔案、計算檔案hash、提取css檔案、清理檔案目錄這些輔助功能了,而可以引入熱更新替換來加快開發時的子產品更新效率。
是以建議區分一下兩個環境,同時将兩者的共同部分提取出來便于維護
NODE_ENV是nodejs在執行時的環境變量,webpack在運作建構期間也可以通路這個變量,是以我們可以在dev和prod下配置相應的環境變量
這個配置寫在package.json裡的scripts字段就好了,比如
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build:dev": "export NODE_ENV=development && webpack-dev-server --config webpack.config.dev.js",
"build:prod": "export NODE_ENV=production && webpack --config webpack.config.prod.js --watch "
},
這樣一來,我們就可以直接用 npm run build:prod來執行生産環境的配置指令(設定了production的環境變量,使用prod.js)
直接用npm run build:dev來執行開發環境的配置指令(設定了development的環境變量,使用dev.js,這裡還使用了devServer,後面說)
注意這裡是Unix系統配置環境變量的寫法,在windows下,記得改成 SET NODE_ENV=development&& webpack-dev-server.......(&&前不要空格)
然後就可以在common.js配置檔案中擷取環境變量
// 是否生産環境
isProduction = process.env.NODE_ENV === 'production',
然後可以在plugins中定義一個變量提供個編譯中的子產品檔案使用
// 插件配置
plugins: [
// 定義變量,此處定義NODE_ENV環境變量,提供給生成的子產品内部使用
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
}
}),
這樣一來,我們可以在home.js中判斷是否為開發環境來引入一些檔案
// 開發環境時,引入頁面檔案,友善改變頁面檔案後及時子產品熱更新
if (process.env.NODE_ENV === 'development') {
require('../../../../views/home/home.html');
}
然後我們使用webpack-merge工具來合并公共配置檔案和開發|生産配置檔案
npm i webpack-merge --save-dev
merge = require('webpack-merge')
commonConfig = require('./webpack.config.common.js')
/**
* 生産環境Webpack打包配置,整合公共部分
* @type {[type]}
*/
module.exports = merge(commonConfig, {
// 生産環境不開啟sourceMap
devtool: false,
// 檔案輸出配置
output: {
// 設定檔案引用主路徑
publicPath: '/public/static/dist/js/'
},
// 子產品的處理配置
公共子產品其實可以分為JS和CSS兩部分(如果有提取CSS檔案的話)
在公共檔案的plugin中加入
// 提取公共子產品檔案
new webpack.optimize.CommonsChunkPlugin({
chunks: ['home', 'detail'],
// 開發環境下需要使用熱更新替換,而此時common用chunkhash會出錯,可以直接不用hash
filename: '[name].js' + (isProduction ? '?[chunkhash:8]' : ''),
name: 'common'
}),
設定公共檔案的提取源子產品chunks,以及最終的公共檔案子產品名
公共子產品的檔案的提取規則是chunks中的子產品公共部分,如果沒有公共的就不會提取,是以最好是在entry中就指定common子產品初始包含的第三方子產品,如jquery,react等
// 檔案入口配置
entry: {
home: './src/js/home',
detail: './src/js/detail',
// 提取jquery入公共檔案
common: ['jquery']
},
要講ES6轉換為ES5,當然首用babel了,先安裝loader及相關的包
npm i babel-core babel-loader babel-preset-env babel-polyfill babel-plugin-transform-runtime --save-dev
-env包主要用來配置文法支援度
-polyfill用來支援一些ES6拓展的但babel轉換不了的方法(Array.from Generator等)
-runtime用來防止重複的ES6編譯檔案所需生成(可以減小檔案大小)
然後在/public根目錄下建立 .babelrc檔案,寫入配置
{
"presets": [
"env"
],
"plugins": ["transform-runtime"]
}
然後在common.js的配置檔案中新增一條loader配置就行了,注意使用exclude排除掉不需要轉換的目錄,否則可能會出錯哦
{
test: /\.jsx?$/,
// 編譯js或jsx檔案,使用babel-loader轉換es6為es5
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
}
}]
}
6. 編譯Sass成CSS,嵌入到頁面<style>标簽中,或将其提取出(多個)CSS檔案來用<link>引入
sass的編譯node-sass需要python2.7的環境,先确定已經安裝并設定了環境變量
npm i sass-loader node-sass style-loader css-loader --save-dev
類似的,設定一下loader規則
不過這裡要設定成使用提取CSS檔案的插件設定了,因為它的disable屬性可以快速切換是否提取CSS(這裡設定成生産環境才提取)
好好看這個栗子,其實分三步:設定(new)兩個執行個體,loader比對css和sass兩種檔案規則,在插件中引入這兩個執行個體
提取多個CSS檔案其實是比較麻煩的,但也不是不可以,方法就是設定多個執行個體和對應的幾個loader規則
這裡把引入的sass當做是自己寫的檔案,提取成一個檔案[name].css,把引入的css當做是第三方的檔案,提取成一個[name]_vendor.css,既做到了合并,也做到了拆分,目前還沒想到更好的方案
上面提到過,output的path設定成了/public/static/dist/js ,是以這裡的filename 生成是基于上面的路徑,可以用../來更換生成的css目錄
[contenthash]是css檔案内容的hash,在引用它的地方有展現
fallback表示不可提取時的代替方案,即上述所說的使用style-loader嵌入到<style>标簽中
npm i extract-text-webpack-plugin --save-dev
ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
/ 對import 引入css(如第三方css)的提取
cssExtractor = new ExtractTextWebpackPlugin({
// 開發環境下不需要提取,禁用
disable: !isProduction,
filename: '../css/[name]_vendor.css?[contenthash:8]',
allChunks: true
})
// 對import 引入sass(如自己寫的sass)的提取
sassExtractor = new ExtractTextWebpackPlugin({
// 開發環境下不需要提取,禁用
disable: !isProduction,
filename: '../css/[name].css?[contenthash:8]',
allChunks: true
});
// 插件配置
plugins: [
// 從子產品中提取CSS檔案的配置
cssExtractor,
sassExtractor
]
module: {
rules: [{
test: /\.css$/,
// 提取CSS檔案
use: cssExtractor.extract({
// 如果配置成不提取,則此類檔案使用style-loader插入到<head>标簽中
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {
// url: false,
minimize: true
}
},
// 'postcss-loader'
]
})
}, {
test: /\.scss$/,
// 編譯Sass檔案 提取CSS檔案
use: sassExtractor.extract({
// 如果配置成不提取,則此類檔案使用style-loader插入到<head>标簽中
fallback: 'style-loader',
use: [
'css-loader',
// 'postcss-loader',
{
loader: 'sass-loader',
options: {
sourceMap: true,
outputStyle: 'compressed'
}
}
]
})
}
這樣一來,如果在不同檔案中引入不同的檔案,生成的css可能長這樣
// ./home.js
import '../../libs/bootstrap-datepicker/datepicker3.css';
import '../../libs/chosen/chosen.1.0.0.css';
import '../../libs/layer/skin/layer.css';
import '../../libs/font-awesome/css/font-awesome.min.css';
import '../scss/detail.scss';
// ./detail.js
import '../../libs/bootstrap-datepicker/datepicker3.css';
import '../../libs/chosen/chosen.1.0.0.css';
import '../../libs/layer/skin/layer.css';
import '../scss/detail.scss';
// ./home.html
<link href="/public/static/dist/js/../css/common_vendor.css?66cb1f48" rel="stylesheet">
<link href="/public/static/dist/js/../css/common.css?618d2a04" rel="stylesheet">
<link href="/public/static/dist/js/../css/home_vendor.css?12a314c8" rel="stylesheet">
<link href="/public/static/dist/js/../css/home.css?c196fc33" rel="stylesheet">
// ./detail.html
<link href="/public/static/dist/js/../css/common_vendor.css?66cb1f48" rel="stylesheet">
<link href="/public/static/dist/js/../css/common.css?618d2a04" rel="stylesheet">
可以看到,公共檔案也被提取出來了,利用HtmlWebpackPlugin就能将其置入了
另外,可以看到這裡的絕對路徑,其實就是因為在output中設定了publicPath為/public/static/dist/js/
當然了,也不是說一定得在js中引入這些css資源檔案,你可以直接在頁面中手動<link>引入第三方CSS
我這裡主要是基于子產品化檔案依賴,以及多CSS檔案的合并壓縮的考慮才用這種引入方式的
目前來說,jQuery及其插件在項目中還是很常用到的,那麼就要考慮如何在Webpack中使用它
第一種方法,就是直接頁面中<script>标簽引入了,但這種方式不受子產品化的管理,好像有些不妥
第二種方法,就是直接在子產品中引入所需要的jQuery插件,而jQuery本身由Webpack插件提供,通過ProvidePlugin提供子產品可使用的變量$|jQuery|window.jQuery
不過這種方法好像也有不妥,把所有第三方JS都引入了,可能會降低編譯效率,生成的檔案也可能比較臃腫
npm i jquery --save
//
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery'
}),
]
// ./home.js
import '../../libs/bootstrap-datepicker/bootstrap-datepicker.js';
console.log('.header__img length', jQuery('.header__img').length);
第三種辦法,可以在子產品内部直接引入jQuery插件,也可以直接在頁面通過<script>标簽引入jQuery插件,而jQuery本身由Webpack的loader導出為全局可用
上述ProvidePlugin定義的變量隻能在子產品内部使用,我們可以使用expose-loader将jQuery設定為全局可見
npm i expose-loader --save
// 添加一條規則
{
test: require.resolve('jquery'),
// 将jQuery插件變量導出至全局,提供外部引用jQuery插件使用
use: [{
loader: 'expose-loader',
options: '$'
}, {
loader: 'expose-loader',
options: 'jQuery'
}]
}
要注意在Webpack3中不能使用webpack.NamedModulesPlugin()來擷取子產品名字,它會導緻expose 出錯失效(bug)
不過現在問題又來了,這個應該是屬于HtmlWebpackPlugin的不夠機智的問題,先說說它怎麼用吧
第一個重要的功能就是生成對資源的引入了,第二個就是幫助我們填入資源的chunkhash值防止浏覽器緩存
這個在生産環境使用就行了,開發環境是不需要的
npm i html-webpack-plugin --save-dev
HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
// 設定編譯檔案頁面檔案資源子產品的引入
new HtmlWebpackPlugin({
// 模版源檔案
template: '../../views/home/home_tpl.html',
// 編譯後的目标檔案
filename: '../../../../views/home/home.html',
// 要處理的子產品檔案
chunks: ['common', 'home'],
// 插入到<body>标簽底部
inject: true
}),
new HtmlWebpackPlugin({
template: '../../views/detail/detail_tpl.html',
filename: '../../../../views/detail/detail.html',
chunks: ['common', 'detail'],
inject: true
}),
]
使用方式是配置成插件的形式,想對多少個模闆進行操作就設定多少個執行個體
注意template是基于context配置中的上下文的,filename是基于output中的path路徑的
// ./home_tpl.html
<script src="/public/static/libs/magicsearch/jquery.magicsearch2.js"></script>
</body>
// ./home.html
<script src=/public/static/libs/magicsearch/jquery.magicsearch2.js></script>
<script type="text/javascript" src="/public/static/dist/js/common.js?cc867232"></script>
<script type="text/javascript" src="/public/static/dist/js/home.js?5d4a7836"></script>
</body>
它會編譯成這樣,然而,然而,要注意到這裡是有問題的
這裡有個jQuery插件,而Webpack使用expose是将jQuery導出到了全局中,我們通過entry設定把jQuery提取到了公共檔案common中
是以正确的做法是common.js檔案先于jQuery插件加載
而這個插件隻能做到在<head> 或<body>标簽尾部插入,我們隻好手動挪動一下<script>的位置
不過,我們還可以基于這個插件,再寫一個插件來實作自動提升公共檔案 <script>标簽到最開始
HtmlWebpackPlugin運作時有一些事件
html-webpack-plugin-before-html-generation
html-webpack-plugin-before-html-processing
html-webpack-plugin-alter-asset-tags
html-webpack-plugin-after-html-processing
html-webpack-plugin-after-emit
html-webpack-plugin-alter-chunks
在編譯完成時,正則比對到<script>标簽,找到所設定的公共子產品(可能設定了多個公共子產品),按實際順序提升這些公共子產品即可
完整代碼如下:
1 // ./webpack.myPlugin.js
2
3
4 let extend = require('util')._extend;
5
6
7 // HtmlWebpackPlugin 運作後調整公共script檔案在html中的位置,主要用于jQuery插件的引入
8 function HtmlOrderCommonScriptPlugin(options) {
9 this.options = extend({
10 commonName: 'common'
11 }, options);
12 }
13
14 HtmlOrderCommonScriptPlugin.prototype.apply = function(compiler) {
15 compiler.plugin('compilation', compilation => {
16 compilation.plugin('html-webpack-plugin-after-html-processing', (htmlPluginData, callback) => {
17 // console.log(htmlPluginData.assets);
18
19 // 組裝數組,反轉保證順序
20 this.options.commonName = [].concat(this.options.commonName).reverse();
21
22 let str = htmlPluginData.html,
23 scripts = [],
24 commonScript,
25 commonIndex,
26 commonJS;
27
28 //擷取編譯後html的腳本标簽,同時在原html中清除
29 str = str.replace(/(<script[^>]*>(\s|\S)*?<\/script>)/gi, ($, $1) => {
30 scripts.push($1);
31 return '';
32 });
33
34 this.options.commonName.forEach(common => {
35 if (htmlPluginData.assets.chunks[common]) {
36 // 找到公共JS标簽位置
37 commonIndex = scripts.findIndex(item => {
38 return item.includes(htmlPluginData.assets.chunks[common].entry);
39 });
40
41 // 提升該公共JS标簽至頂部
42 if (commonIndex !== -1) {
43 commonScript = scripts[commonIndex];
44 scripts.splice(commonIndex, 1);
45 scripts.unshift(commonScript);
46 }
47 }
48 });
49
50 // 重新插入html中
51 htmlPluginData.html = str.replace('</body>', scripts.join('\r\n') + '\r\n</body>');
52
53 callback(null, htmlPluginData);
54 });
55 });
56 };
57
58
59 module.exports = {
60 HtmlOrderCommonScriptPlugin,
61 };
然後,就可以在配置中通過插件引入了
{HtmlOrderCommonScriptPlugin} = require('./webpack.myPlugin.js');
// HtmlWebpackPlugin 運作後調整公共script檔案在html中的位置,主要用于jQuery插件的引入
new HtmlOrderCommonScriptPlugin({
// commonName: 'vendor'
})
親測還是蠻好用的,可以應對簡單的需求了
這個配置開發環境和生産環境是不同的,先看看生産環境的,主要的特點是有目錄結構的設定,設定了一些生成的路徑以及名字資訊
開發環境因為是使用了devServer,不需要控制目錄結構
npm i url-loader [email protected] html-loader --save-dev
這裡要注意的是file-loader就不要用0.10版本以上的了,會出現奇怪的bug,主要是下面設定的outputPath和publicPath和[path]會不按套路出牌
導緻生成的頁面引用資源變成奇怪的相對路徑
rules: [{
test: /\.(png|gif|jpg)$/,
use: {
loader: 'url-loader',
// 處理圖檔,當大小在範圍之内時,圖檔轉換成Base64編碼,否則将使用file-loader引入
options: {
limit: 8192,
// 設定生成圖檔的路徑名字資訊 [path]相對context,outputPath輸出的路徑,publicPath相應引用的路徑
name: '[path][name].[ext]?[hash:8]',
outputPath: '../',
publicPath: '/public/static/dist/',
}
}
}, {
test: /\.(eot|svg|ttf|otf|woff|woff2)\w*/,
use: [{
loader: 'file-loader',
options: {
// 設定生成字型檔案的路徑名字資訊 [path]相對context,outputPath輸出的路徑,publicPath相應引用的主路徑
name: '[path][name].[ext]?[hash:8]',
outputPath: '../',
publicPath: '/public/static/dist/',
// 使用檔案的相對路徑,這裡先不用這種方式
// useRelativePath: isProduction
}
}],
}, {
test: /\.html$/,
// 處理html源檔案,包括html中圖檔路徑加載、監聽html檔案改變重新編譯等
use: [{
loader: 'html-loader',
options: {
minimize: true,
removeComments: false,
collapseWhitespace: false
}
}]
}]
比較生澀難懂,看個栗子吧
scrat.png是大于8192的,最終頁面引入會被替換成絕對路徑,并且帶有hash防止緩存,而輸出的圖檔所在位置也是用着相應的目錄,便于管理
// ./home_tpl.html
<img class="header__img" src="../../public/static/imgs/kl/scrat.png" width="200" height="200">
// ./home.html
<img class=header__img src=/public/static/dist/imgs/kl/scrat.png?8ad54ef5 width=200 height=200>
如果換個小圖,就會替換成base64編碼了,在css中的引入也一樣
<img class=header__img src=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAA6CAYAAABrnUYFAAAaVElEQVR4Xu1df3wdVZX/npnkNUlbWn6sFhAELSgibSYtCIhagXXB0vdSsOwu4CKiXbAglk8zAVwloELzwoqIUsFFRXQVaiEvRcAfLD9EfqxtXtqKS239AQoFRUlb+tK+5N2znzNvJp1M34+ZeZOElLn/QPPuPffcc2e+c+75dQlxiyUQSyCWwChJgEaJbkw2lkAsgVg
再來看看開發環境的
rules: [{
test: /\.(png|gif|jpg)$/,
// 處理圖檔,當大小在範圍之内時,圖檔轉換成Base64編碼,否則将使用file-loader引入
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}, {
test: /\.(eot|svg|ttf|otf|woff|woff2)\w*/,
// 引入檔案
use: 'file-loader'
}]
10. 子產品熱更新替換的正确姿勢
在開發環境下,如果做到子產品的熱更新替換,效果肯定是棒棒的。生成環境就先不用了
在最初的時候,隻是做到了熱更新,并沒有做到熱替換,其實都是坑在作祟
熱更新,需要一個配置伺服器,Webpack內建了devServer的nodejs伺服器,配置一下它
// 開發環境設定本地伺服器,實作熱更新
devServer: {
contentBase: path.resolve(__dirname, 'static'),
// 提供給外部通路
host: '0.0.0.0',
port: 8188,
// 設定頁面引入
inline: true
},
正常的話,啟動服務應該就可以了吧
webpack-dev-server --config webpack.config.dev.js
要記住,devServer編譯的子產品是輸出在伺服器上的(預設根目錄),不會影響到本地檔案,是以要在頁面上手動設定一下引用的資源
<script src="http://localhost:8188/common.js"></script>
<script src="http://localhost:8188/home.js"></script>
浏覽器通路,改動一下home.js檔案,這時應該可以看到頁面自動重新整理,這就是熱更新了😁
當然了,熱更新還不夠,得做到熱替換,即頁面不重新整理替換子產品
可以呀,多配置一下
// 開發環境設定本地伺服器,實作熱更新
devServer: {
...
// 設定熱替換
hot: true,
...
},
// 插件配置
plugins: [
// 熱更新替換
new webpack.HotModuleReplacementPlugin(),
]
再去浏覽器試試,改個檔案,正常的話應該也能看到
但就是一直停留在App hot update...不動了,驚不驚喜,意不意外
原因是還沒在目前項目中安裝webpack-dev-server,HMR的消息接收不到,指令沒報錯隻是因為在全局安裝了webpack有那指令
npm i webpack-dev-server --save-dev
再試試,然而你發現,才剛開始編譯,就不停地重複編譯了
你得設定一下publicPath 比如
output: {
publicPath: '/dist/js/',
},
再試試,更改子產品,你又會發現頁面還是重新重新整理了
要善于用Preserve log來看看重新整理之前發生了什麼
已經有進展了,這時HMR在擷取JSON檔案時404了,而且通路的域名端口是localhost:8088是我們自己node伺服器的端口
devServer的端口是8188的,看起來這JSON檔案時devServer生成的,可能是路徑被識别成相對路徑了
那就設定成絕對路徑吧
output: {
// 設定路徑,防止通路本地伺服器相關資源時,被開發伺服器認為時相對其的路徑
publicPath: 'http://localhost:8188/dist/js/',
},
再來,恭喜 又錯了,跨域通路
那就在devServer再配置一下header讓8088可以通路,可以暴力一點設定*
devServer: {
...
// 允許開發伺服器通路本地伺服器的包JSON檔案,防止跨域
headers: {
'Access-Control-Allow-Origin': '*'
},
...
},
再來,額😆呵呵,又重新重新整理了
指明了子產品沒有被設定成accepted,那它就不知道要熱替換哪個子產品了,隻好整個重新整理。
需要在子產品中設定一下,機智是冒泡型的,是以在主入口設定就行了,比如這裡的子產品入口home.js
// 設定允許子產品熱替換
if (module.hot) {
module.hot.accept();
}
這就成功了,這裡建議的NamedModulesPlugin是用不了了,因為和espose-loader沖突了
是不是很啰嗦呢,總結一下
1. 在本項目總安裝webpack-dev-server
2. devServer配置中設定hot: true
3. plugins配置中設定new webpack.HotModuleReplacementPlugin()
4. output配置中設定publicPath: 'http://localhost:8188/dist/js/'
5. devServer配置中設定header允許跨域通路
6. 子產品中設定接受熱替換module.hot.accept()
7. 不要在指令行加參數 --hot 和 new webpack.HotModuleReplacementPlugin() 同時使用,會棧溢出錯誤,隻用配置檔案的就行了
另外,預設是隻能子產品熱替換,如果也想監聽頁面檔案改變來實作HTML頁面的熱替換,該怎麼做呢
把HTML也當做子產品引入就行了(開發環境下),在之前已經使用了html-loader能處理html字尾資源的情況下
// ./home.js
// 開發環境時,引入頁面檔案,友善改變頁面檔案後及時子產品熱更新
if (process.env.NODE_ENV === 'development') {
require('../../../../views/home/home_tpl.html');
}
記得import不能放在if語句塊裡面,是以這裡用require來代替
有點奇怪,在最開始的時候,這樣是能實作熱替換的,但這段時間卻一直不行了,顯示已更新,但内容卻沒更新
隻好暫時用第二步熱更新來替換,接收到改變時頁面自動重新整理
// ./home.js
// 開發環境時,引入頁面檔案,友善改變頁面檔案後及時子產品熱更新
if (process.env.NODE_ENV === 'development') {
require('../../../../views/home/home_tpl.html');
}
// 設定允許子產品熱替換
if (module.hot) {
module.hot.accept();
// 頁面檔案更新 自動重新整理頁面
module.hot.accept('../../../../views/home/home_tpl.html', () => {
location.reload();
});
}
壓縮JS代碼就用自帶的插件就行了
壓縮CSS代碼用相應的loader options
// 壓縮代碼
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: {
warnings: false
}
}),
12. 異步加載子產品 require.ensure
異步加載子產品,在很多時候是需要的。比如在首頁的時候,不應該要求使用者就下載下傳了其他不需要的資源。
而webpack中異步加載子產品是比較友善的,主要是require.ensure這個方法
require.ensure(dependencies: String[], callback: function(require), chunkName: String)
比如,在home.html頁面中,我想點選某個元素之後,再異步加載某個子產品來執行
// 添加一個子產品 ./async.js
// 這個子產品用于檢測異步加載
function log() {
console.log('log from async.js');
}
export {
log
};
// 在 ./home.js子產品中設定點選之後異步引入
$('.bg-input').click(() => {
console.log('clicked, loading async.js');
require.ensure([], require => {
require('./components/async').log();
console.log('loading async.js done');
});
});
可以看到,點選之後,異步請求了這個子產品
webpack 在編譯的時候分析在require.ensure中定義的依賴子產品,将其生成到一個新的chunk中(不在home.js裡),之後按需拉取下來
另外,要注意的是,如果子產品已經被引入了,那它是不會單獨被打包出去的
// require('./components/async2').log();
$('.bg-input').click(() => {
console.log('clicked, loading async.js')
require.ensure([], require => {
require('./components/async2').log();
require('./components/async1').log();
console.log('loading async.js done');
});
});
兩個依賴都會放到一起,如果把注釋去掉的話,那異步的子產品就隻有async-1.js了
require.ensure的第一個參數是依賴,這裡的依賴加載完成後,才會執行回調函數(在裡頭我們可以再次設定依賴)
是以,如果隻是想加載一個子產品,我們可以直接這麼寫。但是,這隻是下載下傳了,它是執行不了的
$('.bg-input').click(() => {
console.log('clicked, loading async.js')
require(['./components/async1']);
});
是以一般來說,第一個參數更多是用做回調裡子產品的依賴,一般執行的操作都是放到回調裡
第三個參數是定義這個chunk的名字,要同時在output中設定chunkFilename
// 檔案輸出配置
output: {
// 異步加載子產品名
chunkFilename: '[name].js'
},
require.ensure([], require => {
...
}, 'async_chunk');
再來稍微配一下react的環境
npm i react react-dom babel-preset-react --save-dev
在home.js檔案中加入
let React = require('react');
let ReactDOM = require('react-dom')
class Info extends React.Component {
constructor(props) {
super(props);
this.state = {
name: this.props.name || 'myName'
};
}
showYear(e) {
console.log(this);
let elem = ReactDOM.findDOMNode(e.target);
console.log('year ' + elem.getAttribute('data-year'));
}
render() {
return <p onClick={this.showYear} data-year={this.props.year}>{this.state.name}</p>
}
}
Info.defaultProps = {
year: new Date().getFullYear()
};
ReactDOM.render(<Info />, document.querySelector('#box'));
修改.bablerc檔案
{
"presets": [
"env",
"react"
],
"plugins": ["transform-runtime"]
}
其他配置,比如eslint代碼檢查、postcss支援等就不在這說了,用到了就用類似的方式添加進去吧
14. 自定義HtmlWebpackPlugin插件編譯模版檔案生成的JS/CSS插入位置
HtmlWebpackPlugin主要用來編譯模版檔案,生成新的頁面檔案
new HtmlWebpackPlugin({
template: '../../parent/parent_index_src.html',
filename: '../../../../parent/parent_index.tpl',
chunks: ['common', 'parent'],
inject: true
}),
一般來說會這樣用,可以同時将JS資源與CSS資源插入到頁面中(可自動配hash值),非常友善
但是修改inject屬性隻會不插入或插入到</head>或</body>标簽之前,自定義不了插入位置
上述第八點提到了利用插件來調整生成<script>标簽,其實還有更便捷的方法可以實作:使用其支援的模版引擎
假設現在是smarty頁面,有個公共父模版檔案,很多子頁面套用這個檔案,那麼它可以長成這個樣子
<!-- 父頁面 -->
<!DOCTYPE html>
<html>
<head>
<title>某個系統</title>
<meta charset="utf-8">
<meta lang="zh-CN">
<% for(var key in htmlWebpackPlugin.files.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[key] %>">
<% } %>
<{block name="page_css"}><{/block}>
</head>
<body>
<section class="container">
父頁面
</section>
<{block name="page_content"}><{/block}>
<script src="/public/static/js/jquery.min.js"></script>
<% for(var key in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[key] %>"></script>
<% } %>
<{block name="page_js"}><{/block}>
<script src="http://localhost:8188/dist/js/common.js"></script>
<script src="http://localhost:8188/dist/js/parent.js"></script>
</body>
</html>
<!-- 子頁面 -->
<{extends file="../parent/parent_index.tpl"}>
<{block name="page_css"}>
<% for(var key in htmlWebpackPlugin.files.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[key] %>">
<% } %>
<{/block}>
<{block name="page_content"}>
<h1>子頁面</h1>
<div>
</div>
<{/block}>
<{block name="page_js"}>
<% for(var key in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[key] %>"></script>
<% } %>
<{/block}>
這裡,為了實作子頁面插入到父頁面之後,還能保持CSS與JS資源放在正确的位置,需要指定一個編譯後的生成位置
使用到了Webpack内置支援的ejs模版,并使用到了其htmlWebpackPlugin變量,裡面攜帶了本次編譯的一些資訊,我們可以直接輸出來插入資源,然後再設定 inject: false就行了
下面是一個例子的輸出,更多的就去看文檔吧
"htmlWebpackPlugin": {
"files": {
publicPath : "",
"css": [],
"js": [ "js/main.ae8647e767cd76e54693.bundle.js"],
"chunks": {
"main": {
"size":23,
"entry": "js/main.ae8647e767cd76e54693.bundle.js",
"css": [],
"hash":"ae8647e767cd76e54693",
}
},
manifest : ""
},
"options":{
template : "C:\\dev\\webpack-demo\\node_modules\\.2.28.0@html-webpack-plugin\\lib\\loader.js!c:\\dev\\webpack-demo\\index.html",
filename : "index.html",
hash : false,
inject : false,
compile : true,
favicon : false,
minify : false,
cache : true,
showErrors : true,
chunks : ["main"],
excludeChunks : [],
title : "I am title",
xhtml : false
}
}
15. 熱更新編譯模版檔案自動生成webpack伺服器中的資源路徑
熱更新時,webpack的devServer預設隻會将子產品編譯到記憶體中,編譯到我們設定的伺服器裡,不會編譯生成到本地開發目錄中
這并不算什麼問題,問題是我們需要在頁面中手動引入伺服器的子產品,比如
<script src="http://localhost:8188/dist/js/common.js"></script>
<script src="http://localhost:8188/dist/js/parent.js"></script>
使用熱更新時手動添加,不使用時手動删掉才上傳代碼,這還好,但是,我們有模版檔案
假設模版檔案為a_src.html ,需要編譯成a.html,我們實際項目中要通路的檔案是編譯後的a.html檔案,而我們隻能在源檔案a_src.html中做改動
使用熱更新的時候,并不能将源檔案編譯寫到新檔案上,我們隻能換着法子通路源檔案或者直接改動新檔案并複制一份到源檔案中,而且還得手動添加熱更新的伺服器子產品路徑
太麻煩了,那就在熱更新的時候也編譯模版檔案吧,使用HtmlWebpackHarddiskPlugin 插件自動生成資源引用路徑,同時在源檔案的更改可以自動編譯寫到新檔案中
// 安裝
npm install --save-dev html-webpack-harddisk-plugin
var HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
// 配合htmlWebpackPlugin使用,加上參數alwaysWriteToDisk
new HtmlWebpackPlugin({
template: '../../main/protected/views/flow/a_src.html',
filename: '../../../../main/protected/views/flow/a.htmll',
chunks: ['a'],
inject: false,
alwaysWriteToDisk: true
}),
new HtmlWebpackPlugin({
template: '../../main/protected/views/parent/parent_src.html',
filename: '../../../../main/protected/views/parent/parent.html',
chunks: ['common', 'parent'],
inject: false,
alwaysWriteToDisk: true
}),
// 調用
new HtmlWebpackHarddiskPlugin()
然後在源模版檔案裡,配合上一點的ejs模版生成出來就行了,可以自動檢測是生成環境的路徑還是開發環境的熱更新路徑 解放了勞動力
源模版檔案:
<!-- 編譯後腳本 -->
<% for(var key in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[key] %>"></script>
<% } %>
development:
// 檔案輸出配置
output: {
publicPath: 'http://localhost:8188/dist/js/',
},
<!-- 編譯後腳本 -->
<script src="http://localhost:8188/dist/js/common.js"></script>
<script src="http://localhost:8188/dist/js/parent.js"></script>
production:
// 檔案輸出配置
output: {
publicPath: '/public/assets/dist/js/'
},
<!-- 編譯後腳本 -->
<script src="/public/assets/dist/js/common.js?784109bb"></script>
<script src="/public/assets/dist/js/parent.js?997487cf"></script>
一個項目有多個webpack沖突的解決
如果一個項目中用多個webpack來編譯,并引入了多個檔案,就會産生沖突了,這會導緻webpack隻會識别第一個引入的變量
這時候,需要配置output的jsonpFunction參數
// 檔案輸出配置
output: {
// 輸出所在目錄
path: path.resolve(__dirname, 'assets/dist/js'),
// 開發環境使用熱更新,友善編譯,可以直接不用hash
filename: '[name].js',
jsonpFunction: 'abcJSONP'
},
Only one instance of babel-polyfill is allowed
引入多個polyfill導緻沖突,不能重複引入
import 'babel-polyfill';
解決辦法是:引入的時候判斷一下(沒辦法,它自己沒判斷)
if (!global._babelPolyfill) {
require('babel-polyfill');
}
轉載請注明
[-_-]眼睛累了吧,注意勞逸結合呀[-_-]