使用Webpack實(shí)現(xiàn)前端構(gòu)建工具
webpack簡(jiǎn)單介紹
webpack 是一個(gè)現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器。
核心概念:入口(entry)、輸出(output)、loader、插件(plugins)
主要功能點(diǎn)(要解決的問(wèn)題)
- 日常開(kāi)發(fā)時(shí),代碼編譯
- 團(tuán)隊(duì)協(xié)作開(kāi)發(fā),盡量減少開(kāi)發(fā)時(shí)的代碼沖突
- 前端資源緩存控制,減少流量花費(fèi)
一、日常開(kāi)發(fā)時(shí),代碼編譯
- ES6編譯
- vue編譯
- Less編譯
1.正常配置
初始化項(xiàng)目:
npm init
npm i webpack webpack-cli -D
創(chuàng)建webpack.config.js文件:
const path = require('path');
module.export = {
entry: './app.js',
output: {
path: './dist',
publicPath: '/dist',
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.css$/,
use: [{
loader: 'css-loader'
}]
}]
},
plugins: []
};
2.ES6編譯
1)webpack 已支持ES6 不需要單獨(dú)配置babel
2)項(xiàng)目中代碼需要做ES5的兼容
babel主要是把ES6規(guī)范的代碼編譯為ES5,ES5規(guī)范中的一些方法低版本瀏覽器還沒(méi)有做兼容,所以需要我們?cè)陧?xiàng)目中使用polyfill庫(kù)來(lái)兼容ES5。
項(xiàng)目中安裝:
npm i @babel/polyfill -P
項(xiàng)目中引用:
import "@babel/polyfill";
3.Vue編譯
1)配置vue-loader
vue-loader主要用作編譯*.vue文件
VueLoaderPlugin 用作把你定義的其他規(guī)則 也應(yīng)用到*.vue文件當(dāng)中
vue-loader 安裝:
npm i vue vue-loader -D
npm i vue-template-compile -D
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.export = {
entry: './app.js',
output: {
path: './dist',
publicPath: '/dist',
filename: 'bundle.js'
},
module: {
rules: [{ //編譯vue文件
test: /\.vue$/,
loader: 'vue-loader'
},{
test: /\.css$/,
use: [{
loader: 'css-loader'
}]
}]
},
plugins: [
new VueLoaderPlugin()
]
};
2)配置vue-style-loader
vue-style-loader主要是用來(lái)處理*.vue文件中<style>標(biāo)簽
vue-style-loader 安裝:
npm i vue-style-loader -D
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.export = {
entry: './app.js',
output: {
path: './dist',
publicPath: '/dist',
filename: 'bundle.js'
},
module: {
rules: [{ //編譯vue文件
test: /\.vue$/,
loader: 'vue-loader'
},{
test: /\.css$/,
use: [{
loader: 'vue-style-loader'
}, {
loader: 'css-loader'
}]
}]
},
plugins: [
new VueLoaderPlugin()
]
};
4.Less編譯
Less 安裝:
npm i less-loader less -D
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.export = {
entry: './app.js',
output: {
path: './dist',
publicPath: '/dist',
filename: 'bundle.js'
},
module: {
rules: [{ //編譯vue文件
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.css$/,
use: [{
loader: 'vue-style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'less-loader'
}]
}, {
test: /\.less$/,
use: [{
loader: 'css-loader'
}, {
loader: 'less-loader'
}]
}]
},
plugins: [
new VueLoaderPlugin()
]
};
可以合并一下 css與less的處理
{
test: /\.(c|le)ss$/,
use: [{
loader: 'vue-style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'less-loader'
}]
}
二、團(tuán)隊(duì)協(xié)作開(kāi)發(fā),盡量減少開(kāi)發(fā)時(shí)的代碼沖突
- 目錄結(jié)構(gòu)安排
- Webpack對(duì)目錄結(jié)構(gòu)的處理
1.目錄結(jié)構(gòu)安排
團(tuán)隊(duì)開(kāi)發(fā)過(guò)程中,要盡量保證各自的功能模塊獨(dú)立。在保持各自開(kāi)發(fā)獨(dú)立的同時(shí),還要不時(shí)提取一些常用到的方法作為工具類(lèi),提取一些經(jīng)常用到的組件作為公共組件,提取一些常用的樣式作為公共樣式。這樣做可以減少代碼沖突,并且使我們的開(kāi)發(fā)速度越來(lái)越快。
要做到這些,我們需要合理的配置我們的目錄結(jié)構(gòu)。
- 根目錄
--- dist //編譯后文件目錄,上傳到服務(wù)器
src //源文件,正常開(kāi)發(fā)用的目錄
package.json //各種包、編譯命令配置等
webpack.config.js //前端構(gòu)建工具
源文件結(jié)構(gòu)如下:
src ---common //用來(lái)放工具類(lèi)、公共組件和公共樣式
pages //平時(shí)開(kāi)發(fā)業(yè)務(wù)的目錄,跟每個(gè)頁(yè)面相對(duì)應(yīng)
static //用來(lái)放一些不需打包的靜態(tài)資源,如網(wǎng)站的圖片、第三方不支持import引用的庫(kù)
common---css
---js
---fonts //字體圖標(biāo)
---components
---index.js //入口文件
---index.css
pages---index---index.js //入口文件
---index.css
---css
---js
---images
---components //可以用來(lái)放vue組件
static---css
---js
---images
編譯后的文件結(jié)構(gòu)如下:
dist ---css---pages---somePage.css
| ---index.css
|-common.css
fonts---materialIcon.woff2
images---pages---somePage---logo.png
js---pages---somePage.js
| ---index.js
|-common.js
static---css---echarts.css
---js---echarts.js
---images---favicon.ico
對(duì)應(yīng)的webpack.config.js修改:
- 多入口處理
定義好入口文件 -> 遍歷所有文件 -> 找出入口文件路徑
定義好入口文件:
./src/common/index.js
./src/pages/*/index.js
遍歷所有文件:
npm i glob -D //glob遍歷文件工具
const glob = require('glob'); //遍歷文件
let entryFile = {};
const files = [
...glob.sync(path.join(__dirname, './src/common/index.js')), // common入口
...glob.sync(path.join(__dirname, './src/pages/*/index.js')), // pages 入口
];
找出入口文件路徑:
files.forEach(val => {
let filePath = val.split('/src/')[1];
let folder = filePath.split('/index.js')[0];
entryFile[folder] = val;
});
/*
entryFile
{
'common': '/work/project/src/common/index.js',
'pages/index': '/work/project/src/pages/index/index.js',
}
*/
- 輸出處理
{
filename: 'js/[name].js',// js/pages/index.js pages/index為入口文件的文件名
publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
path: __dirname + '/dist'
}
通過(guò)入口和輸出的處理,能夠完成js文件的編譯和打包,但是css文件會(huì)被打包到j(luò)s當(dāng)中,這樣不利于日常開(kāi)發(fā)調(diào)試,頁(yè)面訪問(wèn)加載速度也會(huì)被減慢,圖片、字體文件還沒(méi)有處理,所以要針對(duì)css和靜態(tài)文件需要做特殊處理。
- 靜態(tài)文件處理
css文件處理:
npm i mini-css-extract-plugin -D //mini-css-extract-plugin 用作處理css文件,將css打包到css文件當(dāng)中
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const glob = require('glob'); //遍歷文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //專(zhuān)門(mén)處理css,將css打包到一個(gè)css文件中
let entryFile = {};
const files = [
...glob.sync(path.join(__dirname, './src/common/index.js')), // common入口
...glob.sync(path.join(__dirname, './src/pages/*/index.js')), // pages 入口
];
files.forEach(val => {
let filePath = val.split('/src/')[1];
let folder = filePath.split('/index.js')[0];
entryFile[folder] = val;
});
module.export = {
entry: entryFile,
output: {
filename: 'js/[name].js',// js/pages/index.js pages/index為入口文件的文件名
publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
path: __dirname + '/dist'
},
module: {
rules: [{ //編譯vue文件
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.(c|le)ss$/,
use: [{
loader: 'vue-style-loader'
}, {
loader: MiniCssExtractPlugin.loader,
}, {
loader: 'css-loader'
}, {
loader: 'less-loader'
}]
}]
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename:'css/[name].css',
chunkFilename: 'css/[id].css',
})
]
};
靜態(tài)文件處理:
npm i copy-webpack-plugin -D //用來(lái)從src目錄copy文件到dist目錄 處理不需要編譯的文件 直接copy過(guò)去
npm i file-loader -D //處理被引用的靜態(tài)文件如background-image: url(./images/logo.png) 將資源copy到指定目錄并修改對(duì)應(yīng)url()中的地址
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const glob = require('glob'); //遍歷文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //專(zhuān)門(mén)處理css,將css打包到一個(gè)css文件中
const CopyWebpackPlugin = require('copy-webpack-plugin'); //copy一些靜態(tài)文件用
let entryFile = {};
const files = [
...glob.sync(path.join(__dirname, './src/common/index.js')), // common入口
...glob.sync(path.join(__dirname, './src/pages/*/index.js')), // pages 入口
];
files.forEach(val => {
let filePath = val.split('/src/')[1];
let folder = filePath.split('/index.js')[0];
entryFile[folder] = val;
});
module.export = {
entry: entryFile,
output: {
filename: 'js/[name].js',// js/pages/index.js pages/index為入口文件的文件名
publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
path: __dirname + '/dist'
},
module: {
rules: [{ //編譯vue文件
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.(c|le)ss$/,
use: [{
loader: 'vue-style-loader'
}, {
loader: MiniCssExtractPlugin.loader,
}, {
loader: 'css-loader'
}, {
loader: 'less-loader'
}]
}, {
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader',
options: {
name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/pages/index/images/logo.png
return path.replace('/images', '').split('/src/')[1].split('.')[0] + '.[ext]';
},
outputPath: 'images/',
publicPath: '/v1/dist/images/'
}
}]
}, {
test: /\.(woff2)$/,
use: [{
loader: 'file-loader',
options: {
name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/common/fonts/xxx.woff2
return path.replace('/common/fonts', '').split('/src/')[1].split('.')[0] + '.[ext]';
},
outputPath: 'fonts/',
publicPath: '/v1/dist/fonts/'
}
}]
}]
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename:'css/[name].css',
chunkFilename: 'css/[id].css',
}),
new CopyWebpackPlugin([
{
from: path.join(__dirname, './src/static'),
to: path.join(__dirname, './dist/static') + '/[name].[ext]'
}
])
]
};
通過(guò)以上配置,可以實(shí)現(xiàn)一個(gè)基本的前端構(gòu)建工具。
團(tuán)隊(duì)開(kāi)發(fā)過(guò)程中,每個(gè)人接到不同的需求后,可以在pages目錄新建一個(gè)文件夾作為本次業(yè)務(wù)的開(kāi)發(fā)目錄,并將相關(guān)資源放到此目錄,保證了業(yè)務(wù)的獨(dú)立性,不會(huì)與其他人發(fā)生沖突。
因?yàn)閐ist中js與css文件的文件名與pages中的目錄名對(duì)應(yīng),調(diào)試js與css也很便捷。
通過(guò)不斷的擴(kuò)充common目錄,可以不斷的提高開(kāi)發(fā)效率。
沒(méi)有依賴關(guān)系的靜態(tài)文件(html文件中引用的不需要編譯的文件)也被妥善處理了。
三、前端資源緩存控制,減少流量花費(fèi)
瀏覽器的對(duì)靜態(tài)文件緩存來(lái)能提高頁(yè)面的加載速度,但是這樣,我們更新的代碼,不會(huì)第一時(shí)間被獲取到,這樣會(huì)產(chǎn)生一些BUG。
我們需要對(duì)資源進(jìn)行版本控制,發(fā)生變化的文件,修改版本后,瀏覽器會(huì)重新進(jìn)行緩存。
- hash配置
webpack有三種緩存處理策略[hash][chunkhash][contenthash]
區(qū)別:
[hash] 當(dāng)前文件發(fā)生變化,所有文件版本都發(fā)生變化。
[chunkhash] 當(dāng)前文件發(fā)生變化,引用該文件的文件版本也會(huì)發(fā)生變化
[contenthash] 當(dāng)前文件發(fā)生變化,只該文件版本發(fā)生變化
哈希值配置方式:
output: {
filename: 'js/[name].[contenthash].js',// js/pages/index.js pages/index為入口文件的文件名
publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
path: __dirname + '/dist'
}
完整配置
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const glob = require('glob'); //遍歷文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //專(zhuān)門(mén)處理css,將css打包到一個(gè)css文件中
const CopyWebpackPlugin = require('copy-webpack-plugin'); //copy一些靜態(tài)文件用
let entryFile = {};
const files = [
...glob.sync(path.join(__dirname, './src/common/index.js')), // common入口
...glob.sync(path.join(__dirname, './src/pages/*/index.js')), // pages 入口
];
files.forEach(val => {
let filePath = val.split('/src/')[1];
let folder = filePath.split('/index.js')[0];
entryFile[folder] = val;
});
module.export = {
entry: entryFile,
output: {
filename: 'js/[name].[contenthash].js',// js/pages/index.js pages/index為入口文件的文件名
publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
path: __dirname + '/dist'
},
module: {
rules: [{ //編譯vue文件
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.(c|le)ss$/,
use: [{
loader: 'vue-style-loader'
}, {
loader: MiniCssExtractPlugin.loader,
}, {
loader: 'css-loader'
}, {
loader: 'less-loader'
}]
}, {
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader',
options: {
name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/pages/index/images/logo.png
return path.replace('/images', '').split('/src/')[1].split('.')[0] + '.[hash:8].[ext]';
},
outputPath: 'images/',
publicPath: '/v1/dist/images/'
}
}]
}, {
test: /\.(woff2)$/,
use: [{
loader: 'file-loader',
options: {
name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/common/fonts/xxx.woff2
return path.replace('/common/fonts', '').split('/src/')[1].split('.')[0] + '.[hash:8].[ext]';
},
outputPath: 'fonts/',
publicPath: '/v1/dist/fonts/'
}
}]
}]
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename:'css/[name].[contenthash].css',
chunkFilename: 'css/[id].[contenthash].css',
}),
new CopyWebpackPlugin([
{
from: path.join(__dirname, './src/static'),
to: path.join(__dirname, './dist/static') + '/[name].[hash:8].[ext]'
}
])
]
};
- hash值變化記錄
我們?yōu)槲募恿斯V抵螅残枰薷膶?duì)應(yīng)頁(yè)面中資源引用。所以,在文件版本發(fā)生變化后,我們需要記錄下文件的變化。
處理版本的插件
const fs = require('fs');
function readFile(filePath) {
return new Promise((resolve, reject) => {
if(fs.existsSync(filePath)) {
fs.readFile(filePath, 'utf8', function(err, data) {
if(err) {
reject(err);
}else {
resolve(data);
}
});
}else {
resolve();
}
});
}
class HandleHashPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.done.tap('HandleHashPlugin', (arg) => {
let compiledFile = arg.toJson().assets;
let fileData = {};
/*
{
'images/pages/index/logo.png': {
last: '408685cc',
cur: '91d265fb'
}
}
*/
const filePath = path.join(__dirname, './filePath.json');
readFile(filePath).then((fileContent) => {
fileContent = fileContent ? JSON.parse(fileContent) : {};
compiledFile.forEach((val) => {
let filename = val.name;
let hash = filename.match(/\.[0-9|a-z]*\.[0-9|a-z]*$/)[0].split('.')[1];
filename = filename.replace(`.${hash}`, '');
fileData[filename] = {
last: fileContent[filename] ? fileContent[filename]['cur'] || '' : '',
cur: hash
}
});
if(fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
fs.writeFile(filePath, JSON.stringify(fileData), {
flag: 'a'
}, function(err) {
if(err) {
console.error(err);
}else {
console.log('寫(xiě)入成功!');
}
});
}).catch((err) => {
console.log(err);
});
})
}
}
插件使用:
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const glob = require('glob'); //遍歷文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //專(zhuān)門(mén)處理css,將css打包到一個(gè)css文件中
const CopyWebpackPlugin = require('copy-webpack-plugin'); //copy一些靜態(tài)文件用
let entryFile = {};
const files = [
...glob.sync(path.join(__dirname, './src/common/index.js')), // common入口
...glob.sync(path.join(__dirname, './src/pages/*/index.js')), // pages 入口
];
files.forEach(val => {
let filePath = val.split('/src/')[1];
let folder = filePath.split('/index.js')[0];
entryFile[folder] = val;
});
module.export = {
entry: entryFile,
output: {
filename: 'js/[name].[contenthash].js',// js/pages/index.js pages/index為入口文件的文件名
publicPath: __dirname + '/dist',//為項(xiàng)目中的所有資源指定一個(gè)基礎(chǔ)路徑,部分插件會(huì)用到
path: __dirname + '/dist'
},
module: {
rules: [{ //編譯vue文件
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.(c|le)ss$/,
use: [{
loader: 'vue-style-loader'
}, {
loader: MiniCssExtractPlugin.loader,
}, {
loader: 'css-loader'
}, {
loader: 'less-loader'
}]
}, {
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader',
options: {
name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/pages/index/images/logo.png
return path.replace('/images', '').split('/src/')[1].split('.')[0] + '.[hash:8].[ext]';
},
outputPath: 'images/',
publicPath: '/v1/dist/images/'
}
}]
}, {
test: /\.(woff2)$/,
use: [{
loader: 'file-loader',
options: {
name: (path) => {///work/Asoxer/asoxer-server/static/v1/src/common/fonts/xxx.woff2
return path.replace('/common/fonts', '').split('/src/')[1].split('.')[0] + '.[hash:8].[ext]';
},
outputPath: 'fonts/',
publicPath: '/v1/dist/fonts/'
}
}]
}]
},
plugins: [
new HandleHashPlugin(),
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename:'css/[name].[contenthash].css',
chunkFilename: 'css/[id].[contenthash].css',
}),
new CopyWebpackPlugin([
{
from: path.join(__dirname, './src/static'),
to: path.join(__dirname, './dist/static') + '/[name].[hash:8].[ext]'
}
])
]
};
收集到文件版本變化的信息后,我們只需在上線前,遍歷所有的頁(yè)面文件,把相應(yīng)的文件替換掉即可。