前言
從上次更完webpack從什么都不懂到入門之后,好久沒有更新過文章了,可能是因為自己懶了吧。今天看了下自己的索引量少了一半o(╥﹏╥)o,發現事態嚴重,趕緊更新一篇23333
也是因為最近踩了一些多頁面的坑,搭了一個簡單的多頁面架構,對於單頁面我想大家看了上篇文章基本入門了吧,接下來我們就開始玩玩多頁面了。
那么閑話就不多說了,開始進入正題吧
本文的webpack基於webpack4.0,具體是4.12.0版本
模塊化
對於我們上一個項目,我們沒有把我們的webpack的模塊化,而對於我們之前寫的單頁面也越來越大,管理起來也頗為麻煩,而自從es6出來以后,模塊化也逐漸融入了前端的思想當中。
接下來,我們簡單了解一下前端的模塊化規范和如何實現我們的webpack模塊化吧
什么是模塊化
在了解前端模塊化規范前,還是需要先來簡單地了解下什么是模塊化,模塊化開發?
模塊化是指解決一個復雜問題時自頂向下逐層把系統划分成若干模塊的過程,有多種屬性,分別反映其內部特性。
接力借助大佬的一句話,
很多人覺得模塊化開發的工程意義是復用,我不太認可這種看法,在我看來,模塊化開發的最大價值應該是分治,是分治,分治!(重說三)。不管你將來是否要復用某段代碼,你都有充分的理由將其分治為一個模塊。
首先,既然是模塊化設計,那么作為一個模塊化系統所必須的能力:
- 定義封裝的模塊。
- 定義新模塊對其他模塊的依賴。
- 可對其他模塊的引入支持。
好了,思想有了,那么總要有點什么來建立一個模塊化的規范制度吧,不然各式各樣的模塊加載方式只會將局攪得更為混亂。那么我們接下來就講講JavaScript的模塊化規范。
CommonJS
在es6沒出來之前,js是沒有模塊的功能,所以CommonJS應運而生
CommonJS定義的模塊分為:
- 模塊引用(require)
- 模塊定義(exports)
- 模塊標識(module)
require()用來引入外部模塊;exports對象用於導出當前模塊的方法或變量,唯一的導出口;module對象就代表模塊本身。
AMD
由於一個重大的局限,使得CommonJS規范不適用於瀏覽器環境。因為我們請求的模塊都放在服務器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於"假死"狀態。
因此,瀏覽器端的模塊,不能采用"同步加載"(synchronous),只能采用"異步加載"(asynchronous)。這就是AMD規范誕生的背景。
於是乎,AMD(異步模塊定義)出現了,它就主要為前端JS的表現制定規范。而AMD就只有一個接口:define(id?,dependencies?,factory);
AMD 是 RequireJS 在推廣過程中對模塊定義的規范化產出
CMD
CMD 與 AMD 挺相近,二者區別如下:
-
對於依賴的模塊AMD是提前執行,CMD是延遲執行。(RequireJS 從 2.0 開始,也改成可以延遲執行)
-
CMD推崇依賴就近,按需加載;AMD推崇依賴前置。
-
AMD 的 api 默認是一個當多個用,CMD 嚴格的區分推崇職責單一,其每個 API 都簡單純粹。
CMD 是 SeaJS 在推廣過程中對模塊定義的規范化產出
UMD
UMD是AMD與CommonJS模塊方式的糅合。
AMD 瀏覽器第一的原則發展 異步加載模塊。
CommonJS 模塊以服務器第一原則發展,選擇同步加載,它的模塊無需包裝(unwrapped modules)。
這迫使人們又想出另一個更通用的模式UMD (Universal Module Definition)。希望解決跨平台的解決方案。
他的缺點是:代碼量太大。兼容需要額外的代碼,而且是每個文件都要寫這么一大段代碼。
實現webpack模塊化
當然,webpack模塊化是基於CommonJs的,接下來我們一步一步的修改我們之前那個單頁面的webpack-demo,先試着把webpakc-demo的模塊化。
在根目錄下新建 config 的文件夾,用於存放我們各個模塊的配置文件
entry
在config目錄下新建 entry.js 文件
代碼如下:
module.exports = {
index: './src/js/index.js'
}
output
在config目錄下新建 output.js 文件
代碼如下:
const path = require('path');
module.exports = {
// 必須是絕對路勁
path: path.resolve(__dirname, '../dist'),
filename: 'js/[name].js'
}
module
在config目錄下新建 modules.js 文件
代碼如下:
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
rules: [{
test: /\.js$/,
use: ['babel-loader'],
// 除node_modules目錄外,其他都babel編譯
exclude: /node_modules/
}, {
test: /\.css$/,
// 從右向左(從下向上)開始執行
// 提取css
use: ExtractTextPlugin.extract({
// 只在開發環境使用
// use style-loader in development
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {
minimize: true //css壓縮
}
}, {
loader: 'postcss-loader'
}],
// 解決css打包背景圖的路徑問題
publicPath: '../'
})
}, {
test: /\.less$/,
// use:['style-loader','css-loader','less-loader']
// 如果想分離less
use: ExtractTextPlugin.extract({
// 只在開發環境使用
// use style-loader in development
fallback: 'style-loader',
use: ['css-loader', 'postcss-loader', 'less-loader'],
// 解決css打包背景圖的路徑問題
publicPath: '../'
})
}, {
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
// 小於10kb轉成base64
limit: 10240,
// 打包后的文件夾
outputPath: 'img'
}
}, {
loader: 'image-webpack-loader',
options: {
// 設置對jpg格式的圖片壓縮的程度設置
mozjpeg: {
progressive: true,
quality: 65
}
}
}]
}]
}
plugins
在config目錄下新建 plugins.js 文件
代碼如下:
const path = require('path')
// 生成html頁面
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 刪除某些東西
const CleanWebpackPlugin = require('clean-webpack-plugin')
// 處理靜態資源,靜態資源輸出
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 引入webpack
const webpack = require('webpack')
// 提取css
const ExtractTextPlugin = require('extract-text-webpack-plugin')
// 刪除冗余css代碼
const PurifyCssWebpack = require('purifycss-webpack')
// 用於獲取指定文件夾下的文件
const glob = require('glob')
module.exports = [
// 生成多個頁面需要調用多次new HtmlWebpackPlugin() 多頁面的配置
new HtmlWebpackPlugin({
// 生成多個頁面,filename作為標識
filename: 'index.html',
// 多頁面引入自己的js
// chunks:['index'],
// 自定義模板標題
// 模板頁title定義為 <%= htmlWebpackPlugin.options.title %>
// 必須這么寫htmlWebpackPlugin
title: 'hello world',
// 模板
template: './src/index.html',
// 生成的文件消除緩存
hash: true,
// 壓縮輸出
minify: {
// 刪除空白字符(折疊空白區域)
// collapseWhitespace:true,
// 刪除屬性的雙引號
// removeAttributeQuotes:true
}
}),
new CleanWebpackPlugin(['dist']),
// 靜態資源輸出
new CopyWebpackPlugin([{
// 初始文件夾
from: path.resolve(__dirname, '../src/asset'),
// 目標文件夾
to: './public'
}]),
// 使用熱更新
new webpack.HotModuleReplacementPlugin(),
// 提取css
new ExtractTextPlugin({
filename: 'css/[name].css',
// 根據不同環境走不同的配置(在開發環境下不使用)
disable: process.env.NODE_ENV === "development"
}),
// 刪除冗余css代碼
new PurifyCssWebpack({
// purifycss會根據配置的路勁遍歷你的HTML文件,查找你使用的CSS
paths: glob.sync(path.join(__dirname, 'src/*.html')),
minimize: true
}),
// 向全局暴露第三方庫
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
})
]
devServer
在config目錄下新建 devServer.js 文件
代碼如下:
const path = require('path')
module.exports = {
// 設置服務器訪問的基本目錄
contentBase: path.resolve(__dirname, '../dist'),
// 設置開發服務器的地址
host: 'localhost',
// 設置開發服務器的端口
port: 8080,
// 自動打開瀏覽器
open: true,
// 配置熱更新
hot: true
}
optimization
在config目錄下新建 optimization.js 文件
代碼如下:
module.exports = {
splitChunks: {
cacheGroups: {
// 這會創建一個vendor代碼塊,這個代碼塊包含所有被其他入口(entrypoints)共享的代碼。
vendor: {
test: /node_modules/,
chunks: 'initial',
name: 'vendor',
priority: 10,
enforce: true,
}
}
},
// 為每一個入口默認添加一個只包含 runtime 的 chunk
runtimeChunk: {
name: 'runtime'
}
}
webpack.config.js
再看看我們的webpack.config.js 文件
const entry = require('./config/entry')
const output = require('./config/output')
const modules = require('./config/modules')
const plugins = require('./config/plugins')
const devServer = require('./config/devServer')
const optimization = require('./config/optimization')
module.exports = {
// 入口配置
entry,
// 出口文件
output,
module: modules,
plugins,
// 開發服務器
devServer,
optimization
}
npm run dev 測試一下,發現和以前的沒有區別就成功啦
前面講了這么多,然后發現和以前一點區別都沒有,是不是很像拍死我了,虧我看了大半個鍾,然而啥也沒學到。
別着急嘛,你在看看我們模塊化之后,主文件是不是簡單了許多,而且排錯也更加的方便直觀了
多頁面構建(正題)
既然已經實現了模塊化,我們接下來就說說多頁面吧,也是時候進入正題了
上次我們說到,多次new HtmlWebpackPlugin()就可以生成多個頁面了,但是如果我們有20來個頁面,new個20多次,實屬不是很優雅,所以我們想看有沒有一個方法,根據我們傳進去的路徑就可以生成我們制定的頁面呢?當然是可以的。不過既然是多頁面那么肯定就是多入口的,因為webpack是以js為入口的,所以我們需要在entry那里定義我們的入口即可
打開entry.js,並修改entry的代碼
const glob = require('glob')
// 獲取所有入口文件
const getEntry = function (globPath) {
let entries = {};
glob.sync(globPath).forEach(function (entry) {
var pathname = entry.split('/').splice(-1).join('/').split('.')[0];
entries[pathname] = [entry];
});
return entries;
};
const entries = getEntry('./src/static/js/*.js')
module.exports = entries
打開plugins.js,並修改plugins的代碼
......
plugins = [
--w HtmlWebpackPlugin({
-- filename: 'index.html',
-- title: 'hello world',
-- template: './src/index.html',
-- hash: true,
--,
......
]
const entries = require('./entry')
let chunks = Object.keys(entries);
const metaArr = [{
charset: 'UTF-8'
}, {
name: 'viewport',
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'
}, {
'http-equiv': 'X-UA-Compatible',
content: 'ie=edge'
}]
// 生成HTML文件
chunks.forEach(function (pathname) {
if (pathname == 'vendor') {
return;
}
var conf = {
// 輸出的文件名
filename: pathname + '.html',
// 引用的模板文件
template: path.resolve(__dirname, '../src/index.html'),
// 是否清除緩存
hash: true,
// 頁面的js
chunks: [pathname, 'runtime', 'vendor'],
// 將腳本放在head元素中,默認為body中
// inject: 'head',
meta: metaArr,
// 壓縮html
minify: {
// 刪除空白字符(折疊空白區域)
collapseWhitespace: true,
// 刪除屬性的雙引號
removeAttributeQuotes: true
},
// 站點圖標
favicon: path.resolve(__dirname, '../src/favicon.ico')
};
plugins.push(new HtmlWebpackPlugin(conf));
});
module.exports = plugins
在/src/js目錄下新建個about.js來測試下我們寫的多頁面配置,代碼如下:
console.log('我是about頁');
修改/src/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><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<ul>
<li><a href="./index.html">1</a></li>
<li><a href="./about.html">2</a></li>
</ul>
</body>
</html>
運行 npm run dev , 點擊2,你會發現跳轉到了about頁,而且控制台也輸出了‘我是about頁’,這樣我們的多頁面配置就算是初步完成了
模板引擎篇
雖然完成了我們的多頁面配置,但是其實你會發現也有很多的不方便啦,像我這么懶的人,肯定是越方便越好
先說說我們的問題,想一個正常的頁面,肯定會有很多公共的頭部和底部,比如導航欄就是公共的,這時,我們上面的多頁面方面,要每次把公共部分寫進去,這樣就會很麻煩了
如果用過gulp的同學知道有個 gulp-file-include 可以引入公共文件,但是有個缺點,就是導航欄高亮要通過js控制,不能通過傳值
我們這里就用 ejs 來解決我們的問題(但是ejs並不能解決我們引入公共頭的問題,因為他不支持inclued那種語法),下面會詳細說明解決方法
npm install ejs-loader -S
首先,因為 html-webpack-plugin 不關心你用的是什么模板引起,只要你export出來的是一份完整的HTML字符串,他就會自動處理了。所以,我們這個這邊通過js文件來export一份完整的HTML代碼。下面我用index.js和about.js作為例子:
因為我們安裝了ejs,配置下ejs-loader,修改下config/modules.js
module.exports = {
rules:[{
test: /\.ejs$/,
use: ['ejs-loader']
}]
}
layout模板篇
然后修改下我們src目錄,既然我們用了ejs,為了方便我們更好的處理,我們需要修改下我們的目錄結構了
把src所有目錄刪除掉,新建 layout、pages、static 目錄,從命名來看,layout是放着我們的模板文件,pages放着是我們的頁面文件,static放着我們的資源文件
在layout目錄下新建layout.ejs文件,內容如下:
<%= header %>
<%= content %>
<%= footer %>
從代碼來看就知道,header、content、footer都是作為一個變量,然后我們新建header.ejs、footer.ejs作為公共頭部和尾部
header.ejs內容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<title><% if (pageTitle) { %><%= pageTitle %><% } %></title>
</head>
<body>
<header>
<ul>
<% navList.forEach(\function(item){ %><li class="<%=item.url==navName?'active':'' %>"><%=item.title %> </li><% }) %>
</ul>
</header>
header頁面的\都去掉,因為Jekyll編譯失敗,無奈加上
footer.ejs內容如下:
<footer>我是公共底部</footer>
</body>
</html>
然后到我們的核心,新建 index.js:
const layout = require('./layout.ejs');
const header = require('./header.ejs');
const footer = require('./footer.ejs');
// 這里作為我們需要的參數
const pf = {
pageTitle: '',
navName: ''
};
const moduleExports = {
init({
pageTitle,
navName
}) {
pf.pageTitle = pageTitle;
pf.navName = navName;
return this;
},
run(content) {
const componentRenderData = Object.assign({}, {
navList: [{
title: '首頁',
url: 'index'
}, {
title: '關於我們',
url: 'about'
}]
}, pf);
const renderData = {
header: header(componentRenderData),
footer: footer(componentRenderData),
content,
};
return layout(renderData);
},
};
module.exports = moduleExports;
index.js的主要功能就是接收各個頁面獨有的參數(比如說頁面名稱),並將這些參數傳入生成各自HTML字符串,然后根據layout本身的模板文件將各組件的HTML以及頁面實際內容的HTML拼接在一起,最終形成一個完整的HTML頁面文檔。
pages頁面篇
在我們之前新建的pages目錄下新建我們的頁面目錄,這里我們新建index和about目錄,為什么新建是因為我們需要用他們各自的js到處完整的HTML字符串(上面也說了),接下啦就是開始干正事了
在index目錄下新建 index.ejs 和 index.js 文件
index.ejs主要負責layout模板中的content內容,主要是頁面內容:
<ul>
<li>1</li>
<li>2</li>
</ul>
<h1 class="h1">我是首頁</h1>
<img src="${require('../../static/img/1.jpg')}">
index.js 主要是負責導出頁面的字符串
const content = require('./index.ejs');
const layout = require('../../layout');
module.exports = layout.init({
pageTitle: '首頁',
navName: 'index'
}).run(content());
做了這些處理之后,會發現我們的入口文件發生了改變,這樣子就需要修改我們的入口配置文件了,打開config/entry.js文件
--const entries = getEntry('./src/js/*.js')
const entries = getEntry('./src/static/js/*.js')
也是因為我們使用js為入口,修改下config下的modules.js和plugins.js文件
modules.js
const path = require('path')
module.exports = {
rules : [{
--exclude: /node_modules/
include: [
path.resolve(__dirname, '../src/static/js')
]
}]
}
plugins.js
// 修改template
template: path.resolve(__dirname, '../src/pages/' + pathname + '/' + pathname + '.js'),
然后運行npm run dev 發現和之前沒有變化 O(∩_∩)O 這樣就基本完成多頁面的配置了
掉坑篇
css-loader的坑
因為css-loader升級到1.0.0版本之后,壓縮就失效了,css-loader把cssnano移除掉了,導致無法壓縮,這里作者推薦的其中一個解決方案是@intervolga/optimize-cssnano-plugin
這里我們安裝一下
npm i @intervolga/optimize-cssnano-plugin -S
然后在plugins.js添加下配置,修改如下:
// 解決css壓縮
const OptimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');
plugins = [
......
new OptimizeCssnanoPlugin({
// sourceMap: nextSourceMap,
cssnanoOptions: {
preset: ['default', {
discardComments: {
removeAll: true,
},
}],
}
})
]
字體文件的坑
因為我們很經常用bootstrap這種UI庫,所以我們引入這種庫報個需要一個loader處理這種字體文件,所以我們修改下modules.js文件
module.exports = {
rules:[{
test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
use: [{
loader: 'url-loader',
options: {
// 小於10kb轉成base64
limit: 10240,
// 打包后的文件夾
outputPath: 'img'
}
}, {
loader: 'image-webpack-loader',
options: {
// 設置對jpg格式的圖片壓縮的程度設置
mozjpeg: {
progressive: true,
quality: 65
}
}
}]
}]
}
優化篇
babel優化
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-object-assign", "transform-runtime"]
}
雖然我們之前那個對於const、let這些常用es6可以轉成es5,但對於Object.assign這些還是不行,我們這里優化一下我們之前的babel,安裝下面依賴
npm install babel-plugin-transform-object-assign babel-preset-stage-2 babel-plugin-transform-runtime -S
DllPlugin
DllPlugin 是什么?從官方的說法就是,DLLPlugin 和 DLLReferencePlugin 用某種方法實現了拆分 bundles,同時還大大提升了構建的速度。簡單來說,就是webpack的動態鏈接庫,這里的代碼不會執行,只是提供給我們引入。
實際就是,我們引入了多個相對較大的庫(比如,bootstrap,loadsh,jquery),但是我們又不怎么需要修改他們,就可以用 DLLPlugin 打包了。
新建 webpack-dll.config.js 文件,代碼如下
const path = require('path')
const webpack = require('webpack')
// 文件處理器
const modules = require('./config/modules')
const ExtractTextPlugin = require('extract-text-webpack-plugin');
// 解決css壓縮
const OptimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');
module.exports = {
// 入口配置
entry: {
comment: ['jquery', './vendor/css/comment.css'],
},
// 出口文件
output: {
path: path.join(__dirname, 'dist'),
filename: 'js/[name].js',
library: '[name]'
},
// module.rules
// loaders
module: modules,
// 插件
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, './manifest.json'),
name: '[name]',
context: __dirname,
}),
// 向全局暴露第三方庫
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.$': 'jquery',
'window.jQuery': 'jquery'
}),
new ExtractTextPlugin('css/[name].css'),
new OptimizeCssnanoPlugin({
// sourceMap: nextSourceMap,
cssnanoOptions: {
preset: ['default', {
discardComments: {
removeAll: true,
},
}],
},
})
],
}
這里是不是發現模塊化的好處了,我們可以調用之前寫好的模塊配置,既節省了代碼,也方便修改
再 package.json 配置一下我們的啟動命令,
{
...
"srcipt": {
...
"dll": "webpack --progress --colors --config ./webpack-dll.config.js --mode production",
...
}
...
}
運行 npm run dll 這樣就會生成 manifest.json 文件了,也就是我們webpack動態依賴庫
配置了我們的動態依賴庫之后,我們也需要在我們webpack.config.js中引入我們的依賴庫,這里我們在config/plugins.js中修改即可
const plugins = [
new webpack.DllReferencePlugin({
context: staticRootDir,
manifest: require('../manifest.json'),
name: 'comment',
})
]
webpack-dll.config.js存在的問題
雖然說 DllPlugin 的東西是不常修改的,但是如果我們就是要修改呢,那就需要刪除我們的動態庫,重新生成一份
手動刪除是在不符合我這種懶人,這時候我們就需要使用出我們強大的node啦,新建一個 delete.js 文件
const fs = require('fs')
// 通過child_process的exex執行命令行命令
const { exec } = require('child_process')
fs.unlink('./manifest.json', (err) => {
console.log('成功刪除 manifest.json');
// 刪除后重新npm run dll
exec(`npm run dll`, (error, stdout, stderr) => {
console.log(`stdout: ${stdout}`);
})
});
這樣就實現了我們刪除 manifest.json 后,自動執行dll了
總結
學到這里,我想已經大概已經怎么使用webpack多頁面了,當然,其實還有好多依然講的很基礎,但是我相信,一步一步來,遇到坑填坑,總會學習到更多東西的。
本篇webpack多頁面入門文章到此結束 O(∩_∩)O~
最后,感謝各位觀眾老爺觀看