webpack配置较为繁杂,大部分教程开篇就从基本术语开始讲起,容易让人望而生畏。so,我已实用为目的写了这篇文章。
- 支持热替换
- 支持scss,自动添加前缀
- 支持eslint
- 支持babel
- 支持图片压缩
- 支持多js入口,多html模板
- 创建 package.json 文件
npm init -y
- 首先,安装 webpack,webpack-cli 和 cross-env
npm install --save-dev webpack webpack-cli cross-env
其中 cross-env 主要是用于比较方便的跨平台设置 process.env.NODE_ENV ,用于区分开发环境和生产环境。
- 新建目录src,目录下创建index.js文件。
- package.json 下 scripts 中加人 build 命令,更方便的运行 webpack
"scripts": {
"build": "cross-env NODE_ENV=production webpack"
},
启动 webpack ,并通过 cross-env 设置当前 process.env.NODE_ENV 为 production ,方便启用相应优化
此时在次目录终端下
npm run build
就可以将src/index.js打包到dist中。
此时的webpack运行的是默认配置,只能打包js文件。 要实现其他功能,需要在根目录下创建 webpack.config.js 文件进行配置。
首先,我们需要配置入口 entry。因为 webpack 是从某一 js 文件开始,打包其中引入的其他文件。为了构建多页面应用程序,我们需要传入一个 js 入口文件组成的对象。为此,我按如下定义 src 目录:
|-- src
|-- public
|-- index.js // 公共模块
|-- pages
|-- home
|-- index.html // home页
|-- index.js // home页js
|-- index.scss // home页js
|-- user
|-- index.html // user页
|-- index.js
|-- index.scss
为了方便读取 src 目录,使用了 glob
npm install --save-dev glob
同时,进行如下配置
const path = require("path");
const glob = require("glob");
const devMode = process.env.NODE_ENV !== 'production'; // 获取 cross-env 设置的 NODE_ENV
const entries = ()=>{
const pagesDir = path.resolve(__dirname, 'src/pages'); // pages目录
const entryFiles = glob.sync(pagesDir + '/**/*.{js,ts}'); // glob寻找js文件路径
const reg = /\/src\/pages\/([^/]+).?\/index\.((js)|(ts))/i;
return entryFiles.reduce((pv,cv)=>Object.assign(pv,reg.test(cv)?{[RegExp.$1]:cv}:null),{}); // 以pages下目录名为key构成对象
}
module.exports = {
mode: devMode ? 'development' : 'production', // mode 是 4.x 新增的配置,可以选择 "development","production","none" 是三种值,可以区分开发环境和生产环境。
entry: {
...entries(), // 解构对象
},
}
有了入口,就要有出口。output配置了关于打包后的js输出。
module.exports = {
// ...
output: {
path: path.resolve(__dirname, 'dist'), // 打包后目录
publicPath: '/', // 静态文件目录
filename: 'static/js/[name].[hash:7].min.js', // 定义输出的目录和文件名
},
}
再次运行build命令,会打包出如下目录结构
|-- dist
|-- static
|-- js
|-- home.xxxxx.js // home/index.js打包,并加入了hsah
|-- user.xxxxx.js
此时 webpack 只打包了js,为了对html进行分别配置,使用 html-webpack-plugin 。此外,还能对 html 进行压缩等优化。
npm install --save-dev html-webpack-plugin
同时增加配置
const HtmlWebpackPlugin = require('html-webpack-plugin');
// ...
const htmlPluginArr = ()=>{
const htmlDir = path.resolve(__dirname, 'src/pages');
const templateFiles = glob.sync(htmlDir + '/**/*.{html,ejs}');
const reg = /\/src\/pages\/([^/]+).?\/index\.((html)|(ejs))/i
return templateFiles.filter((filePath)=>reg.test(filePath)).map(filePath => {
reg.test(filePath)
const filename = RegExp.$1;
const baseOption = {
filename: `${filename}.html`, //目标文件
template: filePath,
chunks: [filename], // 包含的与html同名的chunks代码块
inject: true, // script 插入body底部
minify: { //压缩
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// 更多配置
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency', //按照依赖顺序引入script
}
return new HtmlWebpackPlugin(baseOption);
})
}
// ...
module.exports = {
// ...
plugins: [
...htmlPluginArr()
],
// ...
}
再次运行build命令,会打包出如下目录结构
|-- dist
|-- static
|-- js
|-- home.xxxxx.js // home/index.js打包,并加入了hsah
|-- user.xxxxx.js
|-- home.html // 通过 html-webpack-plugin 自动引入了 home 的 chunk
|-- user.html
进行完如上配置后,再次打包已经能分别打包出不同的 html,并引入相应的 js 了。为了继续优化配置并验证打包的 js ,我们急需启用支持热更新的本地服务。
npm install --save-dev webpack-dev-server
const webpack = require('webpack');
// ...
devServer: {
inline: true, // 使用内联模式
clientLogLevel: 'warning', //控制台消息提示
open: true, //自动打开浏览器
//contentBase: false, // 服务器静态文件目录,禁用,使用CopyWebpackPlugin
compress: true, // gzip 压缩
disableHostCheck: true,
host: "0.0.0.0",
port: 8080,
useLocalIp: true, // 用本地的ip
hot: true, // 启动热更新
overlay: { // 出现错误时在浏览器中显示
warnings: false,
errors: true
},
proxy: { // 代理配置
// https://github.com/chimurai/http-proxy-middleware
"/api": {
target: "http://localhost:3000",
// pathRewrite: {"^/api" : ""}, //改写请求地址,/api/xxx直接写成/xxx
// secure: false, // 支持https
}
}
},
plugins: [
//...
new webpack.HotModuleReplacementPlugin(),
],
增加配置后,我们需要在 package.json 里增加 dev 命令
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --progress",
"build": "cross-env NODE_ENV=production webpack"
},
其中 --progress 代表将运行进度输出到控制台。 配置完成后,执行 npm run dev 命令,就会自动开启本地服务,并打开浏览器。此时,我们就可以进入我们的 html 页面,来实时查看更改。
html 和 js 已经基本配置完成,现在需要对 css 进行配置。 这一部分按照的包会比较多,但是配置都较为简单。
npm install --save-dev node-sass sass-loader css-loader style-loader postcss-loader autoprefixer postcss-import postcss-url mini-css-extract-plugin
注意: mini-css-extract-plugin 用于 css 文件的提取。在 4.x 之前的版本一般使用 extract-text-webpack-plugin ,在4.x后如果要继续使用需要安装最新板 extract-text-webpack-plugin@next
其中每一个包的功能会在文章最后简单描述
const ExtractTextPlugin = require('extract-text-webpack-plugin');
// ...
module.exports = {
// ...
plugins: [
// ...
new MiniCssExtractPlugin({
filename: `static/style/${devMode ? '[name].css' : '[name].[hash].css'}`,
chunkFilename: `static/style/${devMode ? '[name].css' : '[name].[hash].css'}`,
})
],
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
use: [
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader',
],
}
]
},
}
这里有必要提一下 loader。rules 中 使用 use 数组来声明加载满足 test 正则的文件。其中 loader 是从右向左运行的,运行的结果会传给下一个 loader。
所以上面配置的意思就是读取 .sass/scss/css 文件,顺序是 sass-loader -> postcss-loader -> css-loader。
由于 mini-css-extract-plugin 提取 css 文件只能用于生产环境,所以最后依据当前环境来判断是使用 style-loader 还是 MiniCssExtractPlugin.loader.
由于使用了 postcss ,我们必须要对其进行配置,来满足 css 的浏览器兼容性。 postcss 也是可以通过配置文件的形式进行配置的。
在根目录新建 .postcssrc.js 文件
// .postcssrc.js
module.exports = {
// https://github.com/michael-ciniawsky/postcss-load-config
"plugins": {
"postcss-import": {},
"postcss-url": {},
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}
具体配置可以查看 postcss 的文档。
在每个入口 js 文件中引入同一目录下的 scss 文件,再次build
|-- dist
|-- static
|-- js
|-- home.xxxxx.js // home/index.js打包,并加入了hsah
|-- user.xxxxx.js
|-- style
|-- home.xxxxx.css // home/index.scss打包,并加入了hsah
|-- user.xxxxx.css
|-- home.html
|-- user.html
webpack 本身只能打包 js 文件。所以,如上面的 css/scss 文件等其他文件,就需要对应的 loader 去加载使其资源转化。除了 css 文件,我们还需要对字体文件,图片等资源进行加载。并且,为了代码的质量,我们需要使用 eslint 对代码进行规范;为了使用 es6 开发,需要 babel 对代码进行兼容处理。这两个需求都需要用 loader 加载并处理 js 文件。
关于babel配置,官网非常的详细。
在 7.x 版本中所有 stage-x presets 已弃用,所有就不在使用次类规范了。
npm install --save-dev babel-loader @babel/core @babel/preset-env
// 如果需要 polyfill
npm install --save @babel/polyfill
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
}
]
}
关于配置的详细描述的一篇文章 《你真的会用 Babel 吗?》
{
"presets": [
[
"@babel/preset-env",
{
"targets": { // 配支持的环境
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"],
"node": "current"
},
"modules": false, //设置ES6 模块转译的模块格式 默认是 commonjs
"debug": false, // debug,编译的时候 console
"useBuiltIns": "entry", // 是否开启自动支持 polyfill
"include": [], // 总是启用哪些 plugins
"exclude": [] // 强制不启用哪些 plugins,用来防止某些插件被启用
}
],
],
}
以 airbnb 标准为例。
npm install --save-dev eslint eslint-config-airbnb-base eslint-loader eslint-plugin-import eslint-friendly-formatter babel-eslint eslint-plugin-html
module: {
rules: [
// ...
{
enforce: 'pre', // 先使用此 loader 加载 js
test: /\.js$/,
exclude: /node_modules/, // 第三方库中的代码不进行规范检测
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter'), // 在终端上显示错误
}
},
]
},
与 postcss 类似, eslint 也需要单独的配置。
.eslintignore 文件声明了不进行规范检测的部分
/build/
/config/
/dist/
/*.js
.eslintrc.js 文件定义了检测的规范。
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint' // babel 处理后的代码的eslint处理
},
env: {
browser: true,
},
extends: 'airbnb-base', // 所用的规范
globals: { // 可以使用的为在当前 js 内声明的变量
document: true,
navigator: true,
window:true,
node:true
},
plugins: [ // 对 html 文件的规则处理
'html'
],
rules: { // 除了规预设范外的自定义规则
'import/extensions': ['error', 'always', {
js: 'never',
}],
'max-len': ['error', {
code: 9999,
tabWidth: 2
}]
}
};
由于 devServer.overlay 的设置,使得 eslint-friendly-formatter 在终端上显示的 error 级别错误出现在了浏览器上,warning 级别错误出现在了浏览器的控制台里,这样更明显的提示了代码的不规范。
对图片,字体等资源进行处理,包括图片压缩,小图片转 base64
npm install --save-dev url-loader file-loader image-webpack-loader
modules: {
rules: [
// ...
{
test: /\.((woff2?|svg)(\?v=[0-9]\.[0-9]\.[0-9]))|(woff2?|svg|jpe?g|png|gif|ico)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240, // 大于10kb将解析成base64
name: 'static/images/[name].[hash:7].[ext]' //输出目录及文件名
}
},
{
loader: 'image-webpack-loader',
options: {
// https://github.com/tcoopman/image-webpack-loader
mozjpeg: { //压缩jpeg
progressive: true,
quality: 65
},
optipng: {
enabled: false,
},
pngquant: {
quality: '65-90',
speed: 4
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75
}
}
}
]
},
{
test: /\.((ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9]))|(ttf|eot)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/fonts/[name].[hash:7].[ext]'
}
}
]
},
]
}
运行 build ,小图片字体会直接转成 bsae64 ,其他的则分别进入 dist/static 下的 images 或 fonts 下
有时引入其他路径下的文件时,相对路径比较复杂。resolve 配置直接解决了这点,让我们更方便的引入。
module.exports = {
//...
resolve: {
extensions: ['.js', '.json'], // 引入时可以省略相应的扩展名
alias: {
'@': path.resolve(__dirname, './src') // 使用 @ 代替了 src 文件夹
}
},
}
如果使用了 eslint ,此配置会和 eslint 相冲突。为了解决这个问题,我们需要安装 eslint 的一个插件
npm install --save-dev eslint-import-resolver-webpack
然后在 .eslintrc.js 文件中增加 settings
module.exports = {
// ...
plugins: [
'html'
],
settings: {
'import/resolver': {
webpack: { config: 'webpack.config.js' } // 含有resolve的配置文件所在位置
}
},
rules: {
//...
}
};
这样,我们在引入 src 下的文件时,就可以使用 @ 代替一部分相对路径
// old
import '../../public/aaa.js'
// new
import '@/public/aaa'
很多时候我们需要一个静态目录,目录中的内容不经过 webpack 处理。使用 copy-webpack-plugin 将静态文件中的内容完全复制到我们的 dist 下。
npm install --save-dev copy-webpack-plugin
plugins: [
//...
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, './static'),
to: 'static',
ignore: ['.*']
}
]),
new webpack.DefinePlugin({ // 在js中可直接使用的全局变量
'process.env.NODE_ENV': devMode ? 'development' : 'production',
}),
]
配置基本完成,build 和 dev 基本功能有了。
webpack 最重要的功能就是对代码就行优化。4.x 版本新增加了 optimization 配置,以前很多需要插件完成的事现在 webpack 独立就能完成。
每个入口都引入了当前页面的 css 和公用 css (入 reset.css)。如果按照当前配置 build ,会出现多个 css 文件,并且每个 css 中都包含了 reset.css。
所以,我们需要 1.css 文件合并;2.提取公共 css。
需要使用 optimize-css-assets-webpack-plugin 和 cssnano,来完成 压缩,合并,去重。
npm install --save-dev optimize-css-assets-webpack-plugin cssnano
首选,我们通过 optimization 的 splitChunks 提取公共模块。然后使用 optimize-css-assets-webpack-plugin 和 cssnano 对 css 进行优化。
module.exports = {
plugins: [
new OptimizeCSSAssetsPlugin({
// 参考 https://my.oschina.net/itlangz/blog/2986976
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: ['default', {
discardComments: {
removeAll: true,
},
normalizeUnicode: false
}]
},
canPrint: true
}),
]
// ...
optimization: {
splitChunks: { //新版替换webpack.optimize.CommonsChunkPlugin,提取公共模块
cacheGroups: {
styles: {
name: 'styles',
test: /\.scss|css$/,
chunks: 'all', // merge all the css chunk to one file
enforce: true
}
}
},
},
}
最后,我们需要在 html-webpack-plugin 配置的需要引入的 chunk 数组中增加我们生成的 styles 的 chunk,这样打包后的 html 文件才会正常引入 styles。
const htmlPluginArr = ()=>{
const baseOption = {
filename: `${filename}.html`, //目标文件
template: filePath,
chunks: [filename,'styles'],
}
}
再次 build ,会将 css 打包成单个 styles.xxxx.css 文件。
js 代码出去每个入口独有的业务代码之外,还可以大致分为三部分:
- manifest
-
webpack 的 runtime
optimization: {
runtimeChunk: {
name: "manifest"
},
},
将每个打包出来的js文件中的 webpack 相关代码提取成 mainfest 。
- commons
-
公共代码
optimization: {
runtimeChunk: {
name: "manifest"
},
splitChunks: {
cacheGroups: {
commons: {
name: 'commons', // 重复代码打包到commons,和库放在一起
chunks: 'initial',
minChunks: 2,
enforce:true
},
styles: {
name: 'styles',
test: /\.scss|css$/,
chunks: 'all',
enforce: true
}
}
},
},
将重复代码打包为 commons 。
- vendor
-
node_modules 包
optimization: {
runtimeChunk: {
name: "manifest"
},
splitChunks: {
// ...
vendor: {
name: 'vendor',
test: chunk => (
chunk.resource &&
/\.js$/.test(chunk.resource) &&
/node_modules/.test(chunk.resource)
),
chunks: 'initial',
},
},
},
同样,在 html-webpack-plugin 中引入chunk
const htmlPluginArr = ()=>{
const baseOption = {
filename: `${filename}.html`,
template: filePath,
chunks: [filename, 'vendor', 'commons', 'manifest','styles'],
}
}
新建配置目录,将配置文件进行细分,删掉根目录下的 webpack.config.js
|-- rootDir
|-- build
|-- build.js // 打包时删除 dist 目录
|-- webpack.base.conf.js // 公共配置
|-- webpack.dev.conf.js // 开发配置
|-- webpack.prod.conf.js // 生产配置
|-- src
提取公共配置到 base 配置中,使用 webpack-merge 进行合并。
并在 build 开始前使用 rimraf 清空旧 dist 目录。
同时修改 npm 命令
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --progress --config build/webpack.dev.conf.js",
"build": "cross-env NODE_ENV=production node build/build.js"
},
别忘了修改 .eslintrc.js 中配置文件的地址
settings: {
'import/resolver': {
webpack: { config: 'build/webpack.base.conf.js' }
}
},
webpack | |
webpack-cli | 可以通过cli使用webpack |
webpack-dev-server | 用于开启本地服务,代理,热更新 |
webpack-dev-server | 用于合并 webpack 配置 |
file-loader | 加载目录下的文件 |
url-loader | 小图片转成base64 |
image-webpack-loader | 图片压缩 |
copy-webpack-plugin | 复制文件到制定目录 |
html-webpack-plugin | 自动生成html并引入css,js |
html-loader | html片段复用 |
rimraf | 删除文件 |
css-loader | 加载css |
sass-loader | 加载sass |
node-sass | 使用sass-loader的依赖 |
style-loader | 支持把js里引入的css以style标签形式写入html的head |
postcss-loader | 补全css浏览器前缀 |
autoprefixer | 补全css浏览器前缀所需的插件 |
postcss-import | 解决@import引入路径问提 |
postcss-url | 把css中路径改为打包后路径 |
mini-css-extract-plugin | 把css提取成.css文件 |
optimize-css-assets-webpack-plugin | 用于优化或者压缩CSS资源 |
cssnano | 含优化css规则的包 |
eslint | eslint依赖 |
eslint-config-airbnb-base | airbnb规则 |
eslint-loader | 使用eslint加载js |
eslint-plugin-import | 引入相关 |
eslint-friendly-formatter | 终端错误提示 |
eslint-plugin-html | eslint的html相关插件 |
babel-eslint | eslint对babel的支持 |
eslint-import-resolver-webpack | 解决和webpack中resolve的冲突 |
@babel/core | babel依赖 |
babel-loader | 使用babel加载js |
@babel/preset-env | preset是规范的总结 env是es2015,es2016,es2017的集合 |
@babel/polyfill | 垫片,用于js兼容性 |
ts-loader | 加载ts文件 |
typescript | ts依赖 |