使用Webpack實現(xiàn)前端構(gòu)建工具
webpack簡單介紹
webpack 是一個現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器。
核心概念:入口(entry)瓢宦、輸出(output)蟆盹、loader猜年、插件(plugins)
主要功能點(要解決的問題)
- 日常開發(fā)時替饿,代碼編譯
- 團(tuán)隊協(xié)作開發(fā)赞别,盡量減少開發(fā)時的代碼沖突
- 前端資源緩存控制滤馍,減少流量花費
一岛琼、日常開發(fā)時,代碼編譯
- ES6編譯
- vue編譯
- Less編譯
1.正常配置
初始化項目:
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 不需要單獨配置babel
2)項目中代碼需要做ES5的兼容
babel主要是把ES6規(guī)范的代碼編譯為ES5巢株,ES5規(guī)范中的一些方法低版本瀏覽器還沒有做兼容槐瑞,所以需要我們在項目中使用polyfill庫來兼容ES5。
項目中安裝:
npm i @babel/polyfill -P
項目中引用:
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主要是用來處理*.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)隊協(xié)作開發(fā)困檩,盡量減少開發(fā)時的代碼沖突
- 目錄結(jié)構(gòu)安排
- Webpack對目錄結(jié)構(gòu)的處理
1.目錄結(jié)構(gòu)安排
團(tuán)隊開發(fā)過程中祠挫,要盡量保證各自的功能模塊獨立纸泡。在保持各自開發(fā)獨立的同時董朝,還要不時提取一些常用到的方法作為工具類,提取一些經(jīng)常用到的組件作為公共組件翁巍,提取一些常用的樣式作為公共樣式糟趾。這樣做可以減少代碼沖突慌植,并且使我們的開發(fā)速度越來越快。
要做到這些义郑,我們需要合理的配置我們的目錄結(jié)構(gòu)蝶柿。
- 根目錄
--- dist //編譯后文件目錄,上傳到服務(wù)器
src //源文件非驮,正常開發(fā)用的目錄
package.json //各種包交汤、編譯命令配置等
webpack.config.js //前端構(gòu)建工具
源文件結(jié)構(gòu)如下:
src ---common //用來放工具類、公共組件和公共樣式
pages //平時開發(fā)業(yè)務(wù)的目錄院尔,跟每個頁面相對應(yīng)
static //用來放一些不需打包的靜態(tài)資源蜻展,如網(wǎng)站的圖片喉誊、第三方不支持import引用的庫
common---css
---js
---fonts //字體圖標(biāo)
---components
---index.js //入口文件
---index.css
pages---index---index.js //入口文件
---index.css
---css
---js
---images
---components //可以用來放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
對應(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',//為項目中的所有資源指定一個基礎(chǔ)路徑邀摆,部分插件會用到
path: __dirname + '/dist'
}
通過入口和輸出的處理,能夠完成js文件的編譯和打包伍茄,但是css文件會被打包到j(luò)s當(dāng)中栋盹,這樣不利于日常開發(fā)調(diào)試,頁面訪問加載速度也會被減慢敷矫,圖片例获、字體文件還沒有處理,所以要針對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'); //專門處理css,將css打包到一個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',//為項目中的所有資源指定一個基礎(chǔ)路徑,部分插件會用到
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 //用來從src目錄copy文件到dist目錄 處理不需要編譯的文件 直接copy過去
npm i file-loader -D //處理被引用的靜態(tài)文件如background-image: url(./images/logo.png) 將資源copy到指定目錄并修改對應(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'); //專門處理css,將css打包到一個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',//為項目中的所有資源指定一個基礎(chǔ)路徑怎茫,部分插件會用到
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]'
}
])
]
};
通過以上配置收壕,可以實現(xiàn)一個基本的前端構(gòu)建工具。
團(tuán)隊開發(fā)過程中轨蛤,每個人接到不同的需求后蜜宪,可以在pages目錄新建一個文件夾作為本次業(yè)務(wù)的開發(fā)目錄,并將相關(guān)資源放到此目錄祥山,保證了業(yè)務(wù)的獨立性圃验,不會與其他人發(fā)生沖突。
因為dist中js與css文件的文件名與pages中的目錄名對應(yīng)缝呕,調(diào)試js與css也很便捷澳窑。
通過不斷的擴(kuò)充common目錄斧散,可以不斷的提高開發(fā)效率。
沒有依賴關(guān)系的靜態(tài)文件(html文件中引用的不需要編譯的文件)也被妥善處理了摊聋。
三颅湘、前端資源緩存控制,減少流量花費
瀏覽器的對靜態(tài)文件緩存來能提高頁面的加載速度栗精,但是這樣闯参,我們更新的代碼,不會第一時間被獲取到悲立,這樣會產(chǎn)生一些BUG鹿寨。
我們需要對資源進(jìn)行版本控制,發(fā)生變化的文件薪夕,修改版本后脚草,瀏覽器會重新進(jìn)行緩存。
- hash配置
webpack有三種緩存處理策略[hash][chunkhash][contenthash]
區(qū)別:
[hash] 當(dāng)前文件發(fā)生變化原献,所有文件版本都發(fā)生變化馏慨。
[chunkhash] 當(dāng)前文件發(fā)生變化,引用該文件的文件版本也會發(fā)生變化
[contenthash] 當(dāng)前文件發(fā)生變化姑隅,只該文件版本發(fā)生變化
哈希值配置方式:
output: {
filename: 'js/[name].[contenthash].js',// js/pages/index.js pages/index為入口文件的文件名
publicPath: __dirname + '/dist',//為項目中的所有資源指定一個基礎(chǔ)路徑写隶,部分插件會用到
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'); //專門處理css,將css打包到一個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',//為項目中的所有資源指定一個基礎(chǔ)路徑,部分插件會用到
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抵蠼惭觯残枰薷膶?yīng)頁面中資源引用慕趴。所以,在文件版本發(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('寫入成功耙册!');
}
});
}).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'); //專門處理css,將css打包到一個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',//為項目中的所有資源指定一個基礎(chǔ)路徑毫捣,部分插件會用到
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īng)的文件替換掉即可牌柄。