webpack5 从零开始搭建 vue 项目
- 基础用法
- 进阶用法
- 构建速度和体积优化
一、基础用法
- 初始化项目
mkdir build-webpack && cd build-webpack
npm init -y
npm i -D webpack webpack-cli@3.3.12
注意:由于 webpack-cli4版本也 webpack-dev-server 最新版不兼容,固这里安装的是webpack-cli 底版本,不然热刷新报错。
新建 src/index.js
'use strict'
function hello() {
return 'hello webpack!'
}
document.write(hello());
根目录新建 webpack.config.js
'use strict'
const path = require('path');;
module.exports = {
mode: 'production', // 打包模式 development、production
// 入口
entry: {
index: './src/index.js'
},
// 输出
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
}
}
package.json scripts 新增打包命令
"build": "webpack",
这样一个 webpack 基础的打包工程就完成了,当然这个工程也仅公只能支持 js、json 文件打包。下面我们来通过 loader、plugin 扩展这个基础工程,使他成为名副其实的前端通用工程。
- 支持ES6(Babel)
src/index.js
'use strict'
const users = ['zhangsan', 'lishi'];
users.forEach(item => {
document.write(item)
})
比如上面这段带有ES6新特性代码,在某些浏览器下是不能去接运行的,这就需要进行 babel 转换
npm i -D @babel/core @babel/preset-env babel-loader
根目录新建 .babelrc
{
"presets": [
"@babel/preset-env"
]
}
webpack.config.js
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
},
- 支持 vue
由于 vue 模板里里边有 style css,还需要安装对应loader。vue-style-loader css-loader
npm i -S vue vue-template-compiler
npm i -D vue-loader vue-style-loader css-loader
webpack.config.js 新增对应 loader plugin
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
}
]
},
plugins: [
new VueLoaderPlugin()
]
webpack 的配置算是完成,下面我们来写个基础组件
index.js
'use strict'
import Vue from 'vue';
import Hello from './components/hello.vue';
new Vue({
render: (h) => h(Hello)
}).$mount('#app')
hello.vue
<template>
<div class="hello">
<p>{{text}}</p>
</div>
</template>
<script>
export default {
data() {
return {
text: 'hello webpack!'
}
}
}
</script>
<style lang="css" scoped>
.hello {
font-size: 20px;
color: red;
}
</style>
- html-webpack-plugin
每次 build 根据模块文件生成对就的 html 文件
npm i -D html-webpack-plugin
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
})
]
- clean-webpack-plugin
每次打包前清空上次 build 生成的 dist 目录
npm i -D clean-webpack-plugin
webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [
new CleanWebpackPlugin()
]
- less
支持 css 预处理
npm i -D less less-loader
module: {
rules: [
{
test: /\.less$/,
use: [
'vue-style-loader',
'css-loader',
'less-loader'
]
}
]
}
- 图片处理
npm i -D file-loader url-loader
module: {
rules: [
{
test: /\.(jpg|jpeg|png|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240
}
}
]
}
]
}
- 字体处理
module: {
rules: [
{
test: /\.(ttf|eot|svg|woff|woff2)$/,
use: 'file-loader'
}
]
}
- text 处理
npm i -D raw-loader
module: {
rules: [
{
test: /\.txt$/,
use: 'raw-loader'
}
]
}
- 资源文件 webpack5 asset 处理
webpack5使用四种新增的资源模块(Asset Modules)替代了这些loader的功能。
asset/resource 将资源分割为单独的文件,并导出url,就是之前的 file-loader的功能.
asset/inline 将资源导出为dataURL(url(data:))的形式,之前的 url-loader的功能.
asset/source 将资源导出为源码(source code). 之前的 raw-loader 功能.
asset 自动选择导出为单独文件或者 dataURL形式(默认为8KB). 之前有url-loader设置asset size limit 限制实现。
module: {
rules: [
{
test: /\.(jpg|jpeg|png|gif|svg)$/i,
type: 'asset',
generator: {
filename: 'images/[hash][ext][query]'
}
},
{
test: /\.(ttf|eot|woff|woff2|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[hash][ext][query]'
}
},
{
test: /\.txt$/,
type: 'asset',
generator: {
filename: 'files/[hash][ext][query]'
},
parser: {
dataUrlCondition: {
maxSize: 4 * 1024 // 4kb 指定大小
}
}
}
]
}
- 文件指纹策略:chunkhash、contenthash和hash
文件指纹用作版本管理及浏览器缓存
- Hash:整个项目的构建相关,即只要项目有文件修改, hash 值就会更改
- Chunkhash:不同的 entry 会生成不同的 chunkhash 值
- Contenthash:根据文件内容定义 hash
一般 entry 里边采用 chunkhash,css,img 资源采用 contenthash
output: {
path: path.join(__dirname, 'dist'),
filename: '[name][chunkhash:8].js'
},
module: {
rules: [
{
test: /\.(jpg|jpeg|png|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
name: '[name]_[hash:8].[ext]'
}
}
]
},
{
test: /\.(ttf|eot|svg|woff|woff2)$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}
}
]
},
css 文件指纹由于前面是设置 vue-style-loader 内联在 html head 里并没有打包成单独的 css 文件,需要借助一个插件,打包在独立的 css 文件,才能看到 文件指纹效果
- mini-css-extract-plugin
npm i -D mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
})
]
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
]
}
]
}
- PostCSS插件autoprefixer自动补齐CSS3前缀
npm i -D postcss-loader autoprefixer@8.0.0
注意这里安装的postcss-loader postcss8版本,最新版会报错
Error: PostCSS plugin autoprefixer requires PostCSS 8. Update PostCSS or downgrade this plugin.
package.json
"browserslist": [
"defaults",
"not ie < 8",
"last 2 versions",
"> 1%",
"iOS 7",
"last 3 iOS versions"
]
根目录新建 postcss 配置文件 postcss.config.js
module.exports = {
plugins:[
require('autoprefixer')
]
}
module.rules 改造,注意 less-loader 与 postcss-loader 不能调换,不然 vue template 里的 style 样式不能被补全,只能补全通过 import ‘***.less|.css’ 的样式文件
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'less-loader'
]
}
]
},
- CssNext
npm i -D postcss-cssnext
'use strict'
const postcssCssnext = require('postcss-cssnext');
module.exports = {
plugins: [
postcssCssnext({
browsers: [
'> 1%',
'last 2 versions'
]
})
]
}
:root {
--heighlightColor: hwb(190, 35%, 20%);
}
body {
color: var(--heighlightColor);
}
打包输出
body {
color: rgb(89, 185, 204);
}
该插件可以转换一些 css 新语法特性
具体新语法参考:http://caibaojian.com/scb/cssnext.html
- 设置别名
resolve: {
// 设置别名
alias: {
'@': path.join(__dirname, 'src') // 这样配置后 @ 可以指向 src 目录
}
},
- eslint 配置
npm i -D eslint eslint-loader eslint-friendly-formatter
npm i -D babel-eslint eslint-config-standard eslint-config-vue eslint-plugin-html eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard eslint-plugin-vue
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
es6: true
},
extends: [
'plugin:vue/essential',
'standard'
],
parser: 'vue-eslint-parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
plugins: [
'vue',
'html'
]
}
// loader
{
test: /^((?!bmap).)*\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [path.resolve(process.cwd(), 'src')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
// package.json
"scripts": {
"lint": "eslint --ext .js,.vue src",
"lint-fix": "eslint --fix --ext .js,.vue src"
},
- stylelint 配置
npm i stylelint stylelint-webpack-plugin stylelint-config-standard -D
// .stylelintrc.js
'use strict'
module.exports = {
"extends": "stylelint-config-standard",
"rules": {
"rule-empty-line-before": "never",
"selector-list-comma-newline-after": "never-multi-line",
"string-quotes": "single",
"indentation": 2,
"selector-pseudo-element-colon-notation": "single",
"no-descending-specificity": null
}
}
// webpack.config.js
const StyleLintPlugin = require('stylelint-webpack-plugin')
module.exports = {
plugins: [
new StyleLintPlugin({
files: ['src/**/*.vue', 'src/**/*.(le|c)ss']
})
]
}
- webpack-dev-server 热刷新
npm i -D webpack-dev-server
target: 'web',
devServer: {
contentBase: './dist',
host: 'localhost', // hostname
port: '8888', // 端口
open: true, // 打开应用
hot: true, // 热刷新
inline: true // 刷新模式
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
package.json scripts 新增命令
"serve": "webpack-dev-server"
二、进阶用法
- 打包配置文件归总
scripts 命令修改
npm i -D cross-env webpack-merge
"build": "cross-env NODE_ENV=production webpack",
"serve": "cross-env NODE_ENV=development webpack serve"
webapck.config.js
'use strict'
const baseConfig = require('./lib/webpack.base.js');
const devConfig = require('./lib/webpack.dev.js');
const prodConfig = require('./lib/webpack.prod.js');
const { merge } = require('webpack-merge');
const NODE_ENV = process.env.NODE_ENV;
module.exports = () => {
switch (NODE_ENV) {
case 'development':
return merge(baseConfig, devConfig);
case 'production':
return merge(baseConfig, prodConfig);
default:
return new Error('no mode');
}
}
lib/webpack.base.js
'use strict'
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
target: 'web',
entry: {
main: './src/main.js'
},
output: {
path: path.join(process.cwd(), 'dist'),
filename: '[name]_[chunkhash:8].js'
},
resolve: {
// 设置别名
alias: {
'@': path.join(process.cwd(), 'src') // 这样配置后 @ 可以指向 src 目录
}
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.(jpg|jpeg|png|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
name: '[name]_[hash:8].[ext]'
}
}
]
},
{
test: /\.(ttf|eot|svg|woff|woff2)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}
]
},
{
test: /\.txt$/,
use: 'raw-loader'
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html'
}),
new CleanWebpackPlugin()
]
};
lib/webpack.dev.js
'use strict'
const webpack = require('webpack');
module.exports = {
mode: 'development',
devServer: {
contentBase: './dist',
host: 'localhost', // hostname
port: '8888', // 端口
open: true, // 打开应用
hot: true, // 热刷新
inline: true // 刷新模式
},
module: {
rules: [
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader',
'postcss-loader'
]
},
{
test: /\.less$/,
use: [
'vue-style-loader',
'css-loader',
'postcss-loader',
'less-loader'
]
}
]
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};
lib/webpack.prod.js
'use strict'
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'less-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
})
]
};
- 环境变量
通常项目需要分为生产环境和本地环境添加不同的环境变量,webpack 中可以使用 DefinePlugin 进行设置
'use strict'
const webpack = require('webpack');
module.exports = {
mode: 'production',
plugins: [
new webpack.DefinePlugin({
ENV: JSON.stringify('production'),
HOST: JSON.stringify('prod.ifrontend.net'),
IS_PRODUCTION: true,
ENV_ID: 20210829,
CONSTANTS: JSON.stringify({
TYPES: ['mobile', 'qq', 'email']
})
})
]
}
// app.js
console.log(ENV) // production
console.log(HOST) // prod.ifrontend.net
console.log(IS_PRODUCTION) // true
console.log(ENV_ID) // 20210829
console.log(CONSTANTS) // {TYPES: ['mobile', 'qq', 'email']}
注意这里值必须加上 JSON.stringify,是因为 DefinePlugin 在替换环境变量时对字符串类型的值进行的是完全替换。如果不加 JSON.stringify 的话,在替换后就会成为变量名,而非字符串值。
- source map 配置
webpack 配置非常简单,只需要在 webpack.config.js 中添加 devtool 即可。
具体参考:https://webpack.docschina.org/configuration/devtool/#root
source map 主要是帮助开发者调试源码,跟 Chrome 的开发者工具配合,在 “Sources” 选项卡下面的 “webpack://” 目录中可以找到解析后的工程源码
开发环境:由于生成 source map 是会延长整体的构建时间、打包速度。所以一般不会配置 devtool: ‘source-map’,会选择一个简化版的 source map 。cheap-source-map、eval-source-map、eval-cheap-module-source-map
生产环境:为了防止暴露源码,提高安全性,一般会选择 hidden-source-map、nosources-source-map 两种策略。
- 代码分割及动态 import
npm i -D @babel/plugin-syntax-dynamic-import
.babelrc 增加 plugins
{
"plugins": [
"@babel/plugin-syntax-dynamic-import"
]
}
<button @click="dynamicImportFn">动态 import 组件</button>
<component :is="dynamicComponent"></component>
export default {
data() {
return {
dynamicComponent: null
}
},
methods: {
dynamicImportFn() {
console.log('动态组件')
import(/* webpackChunkName: "dynamic" */ './Dynamic.vue').then(component => {
this.dynamicComponent = component.default
})
}
}
}
最好使用 @babel/plugin-transform-runtime 插件,它包括 es6 的新特性,比如动态 import 、async/await 等等
- css 质量检测
npm i -D stylelint
'use strict'
const stylelint = require('stylelint');
module.exports = {
plugins: [
stylelint({
config: {
rules: {
'declaration-no-important': true
}
}
})
]
}
三、构建速度和体积优化
- 速度分析插件
npm i – D speed-measure-webpack-plugin
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({...config配置})
- 体积分析
npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
- 多进程/多实例
多进程构建的方案比较知名的有以下三个:
- thread-loader (推荐使用这个)
- parallel-webpack
- HappyPack
npm i -D thread-loader
'use strict'
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'thread-loader',
options: {
workers: 2
}
},
{
loader: 'babel-loader'
}
]
}
]
}
}
- 代码压缩
webpack4+ 设置 mode: ‘production’ ,就会默认包括 tree shaking、scope hosting、js 代码压缩等等
css 并没有做处理,下面我们来使用 css-minimizer-webpack-plugin 进行压缩,
注意:压缩之前首先做 css 代码分离, mini-css-extract-plugin 具体配置,上面已经有了,这里不引入了
npm i -D css-minimizer-webpack-plugin
'use strict'
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
plugins: [
new CssMinimizerPlugin()
]
}
- image 压缩
npm i -D image-webpack-loader
rules: [{
test: /\.(gif|png|jpe?g|svg)$/i,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false,
},
// the webp option will enable WEBP
webp: {
quality: 75
}
}
},
],
}]
- 清除冗余 css 代码
npm i -D @fullhuman/postcss-purgecss
这里笔者选择使用 purgecss 的 postcss 插件方式,是因为前面已经用过 postcss。
这里当然也可以选择用 purgecss-webpack-plugin ,webpack 插件方式使用
注意:
min-css-extract-plugin purgcess-webpack-plugin 配合
extract-text-webpack-plugin purifycss-webpack 配合
min-css-extract-plugin 可以理解成 extract-text-webpack-plugin 升级版本,它拥有更丰富的特性和更好的性能。
postcss.config.js
'use strict'
const purgecss = require('@fullhuman/postcss-purgecss')
module.exports = {
plugins: [
require('autoprefixer'),
purgecss({
content: ['./public/**/*.html', './src/**/*.vue'],
defaultExtractor(content) {
const contentWithoutStyleBlocks = content.replace(/<style[^]+?<\/style>/gi, '')
return contentWithoutStyleBlocks.match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || []
},
safelist: [/-(leave|enter|appear)(|-(to|from|active))$/, /^(?!(|.*?:)cursor-move).+-move$/,
/^router-link(|-exact)-active$/, /data-v-.*/
],
})
]
}
- 多进程并行压缩代码
js、css 多线程压缩代码
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true
}),
new CssMinimizerPlugin({
parallel: true
}),
]
},
}
- 预编译资源模块
webpack.dll.js
'use strict'
const path = require('path');
const webpack = require('webpack');
const dllAssetPath = path.resolve(process.cwd(), 'dll');
const dllLibraryName = 'dllExample';
module.exports = {
mode: 'production',
entry: ['vue'],
output: {
path: dllAssetPath,
filename: 'vendor.js',
library: dllLibraryName
},
plugins: [
new webpack.DllPlugin({
name: dllLibraryName,
path: path.resolve(dllAssetPath, 'manifest.json')
})
]
}
// package.json
{
...
"scripts": {
"dll": "webpack --config webpack.dll.js"
}
}
npm run dll 会生成一个 dll目录,里面会有两个文件 vendor.js 和 manifest.json,前者是包含库的代码,后者则是资源清单。
链接到业务代码
// webpack.config.js
module.exports = {
plugins; [
new webpack.DllReferencePlugin({
manifest: require(path.resolve(process.cwd(), 'dll/manifest.json')),
})
]
}
- 利用缓存提升二次构建速度
'use strict'
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 设置 babel-loader 缓存
}
}
]
}
]
}
}
参考网址:https://webpack.docschina.org/configuration/cache/
缓存生成的 webpack 模块和 chunk,来改善构建速度。cache
会在开发
模式被设置成 type: 'memory'
而且在 生产
模式 中被禁用。 cache: true
与 cache: { type: 'memory' }
配置作用一致。 传入 false
会禁用缓存:
module.exports = {
cache: {
type: 'filesystem', // 将缓存类型设置为文件系统
buildDependencies: {
config: [__filename],
},
version: '1.0'
}
}