写这篇文章的时候 webpack 5 刚发布不久,相关插件生态还没跟进升级,所以这篇文章使用的版本为 webpack 4。
vue-cli
和create-react-app
的配置源码后书写的,且是自己从零开始一行一行敲出来的,所以内容质量有保证,可放心食用。(本文仅作为webpack学习之用,实际项目中还是更推荐使用成熟的脚手架搭建,然后根据本文教程去定制改造。)
新建一个文件夹作为示例项目,项目根目录运行命令初始化package.json
:
npm init -y
npm i -D [email protected] [email protected]
/config/webpack.base.config.js
写入内容:const path = require('path')
module.exports = {
entry: { // 入口配置
app: './src/index.js'
},
output: { // 出口配置
filename: 'js/[name].[contenthash:8].js',
path: path.resolve(__dirname, '../dist'),
}
}
package.json
里写入script命令:"script": {
"build": "webpack --config ./config/webpack.base.config.js"
}
npm run build
hash
、chunkhash
、contenthash
的区别:hash
是每次打包时重新生成,所有文件共用同一个hash;chunkhash
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。在生产环境中会将一些公共库分离出来,公共库不改变,生成的chunkhash值就不变。contenthash
和文件内容相关,内容不变,生成的哈希值就不变。webpack里css也属于js的一部分,所以chunkhash下css内容改变,对应js的hash也会变,而contenthash下只会改变该css的hash(使用css和js分离后)。使用 html-webpack-plugin 插件来配置html模板文件的关联,这样打包后的js、css等会自动引入到html中,就可以访问html文件查看效果了。
npm i -D [email protected]
/config/webpack.base.config.js
添加plugins,配置html-webpack-plugin:const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: ......,
output: ......,
// 在这里添加
plugins: [
new HtmlWebpackPlugin({
template: 'public/index.html',
inject: 'body',
hash: false
}),
],
}
/public/index.html
写入代码:
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge, chrome=1">
<title>Titletitle>
head>
<body>
<div id="root">div>
body>
html>
console.log('小王子')
npm run build
,打包后会在dist下生成index.html,打开该html查看控制台输出效果。用哪个框架都行,目的都是学习webpack的配置,以react示例。
npm i -S react react-dom
/src/index.js
替换为以下代码:import React from 'react'
import ReactDOM from 'react-dom'
function App () {
return (
<div>
<div className="test">小王子</div>
</div>
)
}
ReactDOM.render(
<App/>,
document.getElementById('root')
)
npm i -D babel-loader @babel/core @babel/preset-env @babel/preset-react
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
}
/config/webpack.base.config.js
添加module,配置babel的rules:const path = require('path')
module.exports = {
entry: ......,
output: ......,
plugins: ......,
// 在这里添加代码
module: {
rules: [
{
test: /\.jsx?$/,
options: {
cacheDirectory: true
},
loader: 'babel-loader'
}
]
}
}
npm run build
,然后打开打包后的index.html查看效果。webpack配置里可以指定mode属性来把运行环境划分为development和production,
使用webpack-merge插件可以针对不同mode环境使用不同的webpack配置,插件帮我们智能合并配置。
npm i -D webpack-merge@4.2.2
/config/webpack.dev.config.js
写入代码:const merge = require('webpack-merge')
const common = require('./webpack.base.config')
module.exports = merge(common, {
mode: 'development',
output: {
filename: 'js/[name].js',
},
})
/config/webpack.prod.config.js
写入代码:const merge = require('webpack-merge')
const common = require('./webpack.base.config')
module.exports = merge(common, {
mode: 'production',
})
"scripts": {
"start": "webpack --config ./config/webpack.dev.config.js",
"build": "webpack --config ./config/webpack.prod.config.js"
},
注意build命令里的webpack.base.config.js换成了webpack.prod.config.js。
这样就分了开发环境和生产环境。
使用clean-webpack-plugin插件可以在build打包之前自动删除上次打包的dist文件夹,防止冗余文件的产生。
npm i -D clean-webpack-plugin
/config/webpack.prod.config.js
里添加plugins配置:const merge = require('webpack-merge')
const common = require('./webpack.base.config')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
// 在这里添加代码
plugins: [
new CleanWebpackPlugin(),
],
})
npm run build
查看dist文件夹还有没有之前遗留的js文件。使用webpack-dev-server插件,在webpack运行时自动启动一个本地服务器运行打包后的html文件,配合热更新,实现代码改动后实时查看效果。
npm i -D webpack-dev-server@3.11.0
/config/webpack.dev.config.js
里添加devServer和plugins配置:const merge = require('webpack-merge')
const common = require('./webpack.base.config')
const webpack = require('webpack')
module.exports = merge(common, {
mode: 'development',
output: {
filename: 'js/[name].[hash:8].js',
},
// 在这里添加代码
devServer: {
open: true,
port: 9000,
compress: true,
hot: true,
inline: true,
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
})
package.json
里修改scripts的start命令:"start": "webpack-dev-server --inline --progress --config ./config/webpack.dev.config.js",
npm start
查看效果。devtool用于配置source map选项,帮助我们调试时追踪原始源代码,有多种source map格式供选择,具体可以参考文档,综合构建速度和使用效果,建议选择 cheap-module-eval-source-map,各方面都比较均衡。
/config/webpack.dev.config.js
里添加devtool配置:module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-module-eval-source-map', // 在这里添加即可
output: ......,
devServer: ......,
plugins: ......,
})
/src/index.js
里添加一条console语句:import React from 'react'
import ReactDOM from 'react-dom'
console.log(123) // 在这里添加即可
function App () {
......
}
......
style-loader 用于创建样式标签引入css代码,不能单独使用;
css-loader 用于解析css文件生成css代码,给style-loader使用;
less-loader 用于将less文件转换为css文件,给css-loader使用;
npm i -D style-loader css-loader less less-loader
index.less
:@color: red;
.test {
color: @color;
}
/src/index.js
引入该less使用:import React from 'react'
import ReactDOM from 'react-dom'
import './index.less'
function App () {
return (
<div>
<div className="test">小王子</div>
</div>
)
}
ReactDOM.render(
<App/>,
document.getElementById('root')
)
/config/webpack.base.config.js
里配置loader:module.exports = {
module: {
rules: [
......
// 接上,追加以下代码
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
]
}
}
rules里的use数组在解析时是按从右往左解析的,需要注意顺序。
postcss 是一个允许使用 JS 插件转换样式的工具集合;
postcss-loader 用于webpack中对css做进一步处理的loader;
autoprefixer 属于postcss的一个插件,配合postcss-loader可以自动给css样式添加浏览器前缀,以兼容低版本浏览器;
browserlist 用于指定项目运行的目标浏览器范围,能被autoprefixer和babel等识别,根据目标浏览器范围做兼容适配。
npm i -D postcss postcss-loader autoprefixer browserlist
/src/index.less
添加样式:@color: red;
.test {
color: @color;
display: flex;
justify-content: center;
}
/config/webpack.base.config.js
里修改css和less的loader配置:module.exports = {
module: {
rules: [
......
// 修改css和less的loader配置
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
},
]
}
}
postcss.config.js
:module.exports = {
plugins: {
autoprefixer: {}
}
}
package.json
里添加browserlist配置:{
"dependencies": ......,
"devDependencies": ......,
---在这里追加---
"browserslist": [
"> 1% in CN",
"last 2 versions"
]
}
npm start
运行,chrome开发者工具查看文字的css样式,看flex相关样式是否自动加上了浏览器前缀。file-loader用于打包静态文件并将引入路径和js关联;
url-loader用于处理图片资源的打包,低于指定大小时会将资源转换为base64格式使用,其他情况处理和file-loader一样。
npm i -D file-loader url-loader
/config/webpack.base.config.js
里添加loader配置:module.exports = {
module: {
rules: [
......
// 接上,追加以下代码
{
test: /\.(jpe?g|png|gif)$/i,
options: {
esModule: false,
limit: 4096, // 配置低于4k的图片会转为base64格式
},
loader: 'url-loader',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i, // 处理字体文件
options: {
esModule: false
},
loader: 'file-loader'
},
]
}
}
terser-webpack-plugin 用于对js做代码压缩及代码混淆等处理,对es6+支持更好,替代以前的uglifyjs-webpack-plugin。
npm i -D terser-webpack-plugin@4.2.3
/config/webpack.prod.config.js
里添加配置:const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
plugins: ......,
// 在这里添加
optimization: {
minimize: true,
minimizer: [
new TerserWebpackPlugin({
terserOptions: {
compress: {
pure_funcs: ['console.log'] // 删除console.log代码
}
}
}),
],
},
})
mini-css-extract-plugin用于将打包后的css单独抽离出来,webpack打包时默认是将css整合进js里通过动态创建style标签实现的,而这个插件将css剥离出来,能减少不必要的js代码及dom操作,提升页面加载性能。
npm i -D mini-css-extract-plugin
/config/webpack.base.config.js
里配置plugins和loader:const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
plugins: [
......,
// 接上,在这里追加
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[id].[contenthash:8].css',
ignoreOrder: true
}),
],
module: {
rules: [
// 修改css和less的loader,替换掉style-loader
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
{
test: /\.less$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
},
]
}
}
npm run build
查看打包目录是否生成单独的css文件。optimize-css-assets-webpack-plugin 插件用于对css文件做压缩处理,默认使用cssnano压缩。
需要配合mini-css-extract-plugin插件,先将css分离后再压缩。
npm i -D optimize-css-assets-webpack-plugin
/config/webpack.prod.config.js
里添加plugins:const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = merge(common, {
plugins: [
......,
// 接上,在这里追加
new OptimizeCssAssetsPlugin(),
]
})
npm run build
查看打包后的css文件是否已被压缩。alias是webpack内置支持的一个属性,用来指定快捷路径标识,配置后就能方便的书写引入路径。
/config/webpack.base.config.js
里配置:module.exports = {
entry: ......,
output: ......,
// 接上,在这里追加
resolve: {
alias: {
'@': path.resolve(__dirname, '../src'),
}
},
}
/src/index.js
修改文件的引入路径:import './index.less' 替换为 import '@/index.less'
npm start
查看是否正常运行splitChunks用于代码分离,有利于性能优化。模块是否分离的判断原则:体积大、稳定不变。
/config/webpack.pord.config.js
添加splitChunks:module.exports = merge(common, {
optimization: {
......,
// 接上,在这里追加
splitChunks: {
chunks: 'all',
maxAsyncRequests: 8,
maxInitialRequests: 6,
minSize: 10000,
cacheGroups: {
react: { // 分离react和react-dom
name: 'chunk-react',
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, // 匹配规则
priority: 20 // 匹配优先级
},
vendors: { // 其他npm依赖(生产环境)
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: { // 组件公共抽离
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
}
},
})
/config/webpack.dev.config.js
里添加chunkFilename:module.exports = merge(common, {
output: {
filename: 'js/[name].js',
// 在这里添加
chunkFilename: 'js/[name].js',
},
})
/config/webpack.base.config.js
里添加chunkFilename:module.exports = {
output: {
filename: 'js/[name].[contenthash:8].js',
path: path.resolve(__dirname, '../dist'),
// 在这里添加
chunkFilename: 'js/[name].[contenthash:8].js',
},
}
npm run build
查看打包后的文件,多打包几次,对比分离出来的chunk文件名是否有变化。DefinePlugin 是webpack内置的一个插件,允许创建一个在编译时可以配置的全局常量,配置后就可以在代码里使用这个常量了。
/config/webpack.base.config.js
里添加plugins:const webpack = require('webpack')
module.exports = {
plugins: [
......,
// 接上,在这里追加
new webpack.DefinePlugin({
VERSION_H5: +new Date() // 这里添加了VERSION_H5
}),
]
}
需要注意,如果给定义的常量赋值为string类型时需要带上原始引号,可以通过单引号包裹双引号的方式或通过JSON.stringify包裹,例如 ‘“abc”’ 或 JSON.stringify(‘abc’)
// 找个合适的地方添加就行
console.log(VERSION_H5)
css modules是一种防止css样式污染的模块化解决方案。
接下来就配置.module.css或.module.less后缀的文件自动以css modules方式处理。
/config/webpack.base.config.js
里配置loader:const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 为了代码简洁,在这里封装了一下
const cssTest = /\.css$/
const lessTest = /\.less$/
const cssModuleTest = /\.module\.css$/
const lessModuleTest = /\.module\.less$/
const baseCssUse = [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
const baseCssModuleUse = [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: "[name]_[local]__[hash:5]"
}
},
},
'postcss-loader'
]
module.exports = {
module: {
rules: [
......,
// 把之前的css和less的配置 替换成以下代码
{
test: cssTest,
exclude: cssModuleTest,
use: baseCssUse
},
{
test: lessTest,
exclude: lessModuleTest,
use: [...baseCssUse, 'less-loader']
},
{
test: cssModuleTest,
use: baseCssModuleUse
},
{
test: lessModuleTest,
use: [...baseCssModuleUse, 'less-loader']
},
......,
]
},
}
.name {
text-decoration: line-through;
}
/src/index.js
添加代码:import style from './index.module.less'
function App () {
return (
<div>
<div className='test'>小王子</div>
<div className={style.name}>Neo</div>
<div>
<img src={icon} alt=""/>
</div>
</div>
)
}
npm start
重启项目,chrome控制台查看元素及样式效果。es6包含新的语法和新的api,新api是用更底层的语言实现的,新语法默认可以被babel降级处理,但新api默认不会处理,例如数组的find、Object.assign、promise等,需要配置polyfill来处理。
从babel v7.4版本开始,官方不再推荐使用@babel/polyfill,更推荐直接使用core-js/stable和regenerator-runtime/runtime。
npm i -S core-js regenerator-runtime
/src/index.js
里最顶部导入:// 必须在入口文件最顶部导入
import "core-js/stable"
import "regenerator-runtime/runtime"
// 然后再导入其他的
......
babel.config.js
里修改@babel/preset-env配置:module.exports = {
presets: [
// 在这里修改 @babel/preset-env 的配置
[
'@babel/preset-env',
{
modules: false,
useBuiltIns: 'entry',
corejs: {
version: '3.8', // 你的core-js版本号前两位
proposals: true,
},
},
],
// 其他的保持不变
......
],
}
实际项目中,本地开发一般都会遇到接口跨域的问题,协议、域名、端口号 这三项任意一项不一致就会跨域,在devServer里配置proxy代理可以解决跨域问题。
/proxy
。/config/webpack.dev.config.js
里配置devServer:module.exports = merge(common, {
devServer: {
contentBase: path.resolve(__dirname, '../dist'),
open: false,
port: 9000,
compress: true,
hot: true,
inline: true,
proxy: {
'/proxy': {
target: 'https://192.111:8800',
ws: true,
changeOrigin: true,
secure: false,
pathRewrite: {
'^/proxy': ''
}
}
}
},
})
target
目标地址,有端口号的需要带上端口号;ws
配置是否支持 web socket;changeOrigin
配置是否支持虚拟主机站点,我也不清楚具体啥意思;secure
是否开启安全验证,目标地址为https
时需设置secure为false;pathRewrite
路径重写,上述是配置了代理后将/proxy替换为空字符串,即实际接口地址不再需要携带/proxy。cross-env是一款运行跨平台设置和使用环境变量的脚本。使用cross-env在scripts脚本命令里配置自定义变量可以实现命令行快捷切换环境配置的功能。
比如配置不同的测试环境使用不同的接口地址,传统方式可能是直接在devServer里修改proxy代理地址的代码,人工修改代码容易出错,在多人开发时也容易出现代码冲突,如果使用cross-env配置的变量进行判断设置对应的代理地址,通过切换scripts命令来切换代理就变的方便多了。
npm i cross-env -D
package.json
里修改scripts:"scripts": {
"start": "npm run start:test1",
"start:test1": "cross-env MY_TYPE=test1 webpack-dev-server --progress --config ./config/webpack.dev.config.js",
"start:test2": "cross-env MY_TYPE=test2 webpack-dev-server --progress --config ./config/webpack.dev.config.js",
"build": "webpack --config ./config/webpack.prod.config.js"
},
MY_TYPE
这个变量,两个命令设置的值分别是test1和test2,运行npm run start:test1
时在webpack配置文件里就可以通过process.env.MY_TYPE
获取到值。cross-env自动把我们设置的变量加在了process.env这个对象上,但是process.env只能在node环境里获取到,而在浏览器环境里获取不到。
不过还记得上面第4条介绍的DefinePlugin吗,利用DefinePlugin我们可以添加个浏览器环境也能用的process.env对象,方式如下:
/config/webpack.base.config.js
里定义DefinePlugin插件配置:new webpack.DefinePlugin({
// VERSION_H5: +new Date(),
'process.env': Object.keys(process.env).reduce(
(env, key) => {
env[key] = JSON.stringify(process.env[key]);
return env;
},
{}
)
}),
/src/index.js
里添加打印语句:console.log(process.env.NODE_ENV)
console.log(process.env.MY_TYPE)
npm run start:test1
和npm run start:test2
查看浏览器打印结果。如果项目使用typescript,需要额外配置。
npm i -D [email protected] [email protected]
tsconfig.json
文件,写入以下内容:{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true
}
}
/config/webpack.base.config.js
里配置 resolve 和 loader:module.exports = {
module: {
resolve: {
......
// 接上,追加以下代码,表示引用文件时如果没带后缀会按照此顺序依次查找
extensions: ['.tsx', '.ts', '.js'],
},
......,
rules: [
......
// 接上,追加以下代码
{
test: /\.tsx?$/,
use: ['ts-loader']
},
]
}
}