使用Webpack实现前端构建工具
webpack简单介绍
webpack 是一个现代 JavaScript 应用程序的静态模块打包器。
核心概念:入口(entry)、输出(output)、loader、插件(plugins)
主要功能点(要解决的问题)
- 日常开发时,代码编译
- 团队协作开发,尽量减少开发时的代码冲突
- 前端资源缓存控制,减少流量花费
一、日常开发时,代码编译
- ES6编译
- vue编译
- Less编译
1.正常配置
初始化项目:
npm init
npm i webpack webpack-cli -D
创建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规范的代码编译为ES5,ES5规范中的一些方法低版本浏览器还没有做兼容,所以需要我们在项目中使用polyfill库来兼容ES5。
项目中安装:
npm i @babel/polyfill -P
项目中引用:
import "@babel/polyfill";
3.Vue编译
1)配置vue-loader
vue-loader主要用作编译*.vue文件
VueLoaderPlugin 用作把你定义的其他规则 也应用到*.vue文件当中
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>标签
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'
}]
}
二、团队协作开发,尽量减少开发时的代码冲突
- 目录结构安排
- Webpack对目录结构的处理
1.目录结构安排
团队开发过程中,要尽量保证各自的功能模块独立。在保持各自开发独立的同时,还要不时提取一些常用到的方法作为工具类,提取一些经常用到的组件作为公共组件,提取一些常用的样式作为公共样式。这样做可以减少代码冲突,并且使我们的开发速度越来越快。
要做到这些,我们需要合理的配置我们的目录结构。
- 根目录
--- dist //编译后文件目录,上传到服务器
src //源文件,正常开发用的目录
package.json //各种包、编译命令配置等
webpack.config.js //前端构建工具
源文件结构如下:
src ---common //用来放工具类、公共组件和公共样式
pages //平时开发业务的目录,跟每个页面相对应
static //用来放一些不需打包的静态资源,如网站的图片、第三方不支持import引用的库
common---css
---js
---fonts //字体图标
---components
---index.js //入口文件
---index.css
pages---index---index.js //入口文件
---index.css
---css
---js
---images
---components //可以用来放vue组件
static---css
---js
---images
编译后的文件结构如下:
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
对应的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',//为项目中的所有资源指定一个基础路径,部分插件会用到
path: __dirname + '/dist'
}
通过入口和输出的处理,能够完成js文件的编译和打包,但是css文件会被打包到js当中,这样不利于日常开发调试,页面访问加载速度也会被减慢,图片、字体文件还没有处理,所以要针对css和静态文件需要做特殊处理。
- 静态文件处理
css文件处理:
npm i mini-css-extract-plugin -D //mini-css-extract-plugin 用作处理css文件,将css打包到css文件当中
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',//为项目中的所有资源指定一个基础路径,部分插件会用到
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',
})
]
};
静态文件处理:
npm i copy-webpack-plugin -D //用来从src目录copy文件到dist目录 处理不需要编译的文件 直接copy过去
npm i file-loader -D //处理被引用的静态文件如background-image: url(./images/logo.png) 将资源copy到指定目录并修改对应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一些静态文件用
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',//为项目中的所有资源指定一个基础路径,部分插件会用到
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]'
}
])
]
};
通过以上配置,可以实现一个基本的前端构建工具。
团队开发过程中,每个人接到不同的需求后,可以在pages目录新建一个文件夹作为本次业务的开发目录,并将相关资源放到此目录,保证了业务的独立性,不会与其他人发生冲突。
因为dist中js与css文件的文件名与pages中的目录名对应,调试js与css也很便捷。
通过不断的扩充common目录,可以不断的提高开发效率。
没有依赖关系的静态文件(html文件中引用的不需要编译的文件)也被妥善处理了。
三、前端资源缓存控制,减少流量花费
浏览器的对静态文件缓存来能提高页面的加载速度,但是这样,我们更新的代码,不会第一时间被获取到,这样会产生一些BUG。
我们需要对资源进行版本控制,发生变化的文件,修改版本后,浏览器会重新进行缓存。
- hash配置
webpack有三种缓存处理策略[hash][chunkhash][contenthash]
区别:
[hash] 当前文件发生变化,所有文件版本都发生变化。
[chunkhash] 当前文件发生变化,引用该文件的文件版本也会发生变化
[contenthash] 当前文件发生变化,只该文件版本发生变化
哈希值配置方式:
output: {
filename: 'js/[name].[contenthash].js',// js/pages/index.js pages/index为入口文件的文件名
publicPath: __dirname + '/dist',//为项目中的所有资源指定一个基础路径,部分插件会用到
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一些静态文件用
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',//为项目中的所有资源指定一个基础路径,部分插件会用到
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值变化记录
我们为文件加了哈希值之后,也需要修改对应页面中资源引用。所以,在文件版本发生变化后,我们需要记录下文件的变化。
处理版本的插件
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一些静态文件用
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',//为项目中的所有资源指定一个基础路径,部分插件会用到
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]'
}
])
]
};
收集到文件版本变化的信息后,我们只需在上线前,遍历所有的页面文件,把相应的文件替换掉即可。