前言
之前一段时间工作原因把精力都放在小程序上, 趁现在有点空闲时间, 刚好官方文档也补充完整了, 我准备重温一下 webpack 之路了, 因为官方文档已经写得非常详细, 我会大量引用原文描述, 主要重点放在怎么从零构建 webpack4 代码上, 这不是一个系统的教程, 而是从零摸索一步步搭建起来的笔记, 所以前期可能 bug 会后续发现继续修复而不是修改文章.
基本已经可以使用的完整配置 webpack4_demo,
继续上回分解, 我们之前已经实现了资源处理, 配置环境分开, 引入 React 库和 babel 库, 图片优化和打包可视化, 这一章我们就将零散的文件进一步规格化配置
2018/12/26 上传, 代码同步到第四篇文章
2019/03/14 上传, 补充代码到第三篇文章
配置文件
我们在根目录单独新建文件夹config
, 将所有 webpack 配置文件放进去, 然后改一下相对路径的引入, 接下来抽取出些配置文件单独一个模块管理.
alias.js
路径简化单独一个配置文件方便查找
const path = require('path');
// 创建 import 或 require 的别名,来确保模块引入变得更简单
module.exports = {"@": path.resolve(__dirname, "../src/"),
IMG: path.resolve(__dirname, "../src/img"),
STYLE: path.resolve(__dirname, "../src/style"),
JS: path.resolve(__dirname, "../src/js"),
ROUTER: path.resolve(__dirname, "../src/router"),
PAGE: path.resolve(__dirname, "../src/page"),
CMT: path.resolve(__dirname, "../src/component")
};
rules.js
规则处理单独一个模块, 实在太多了
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = [
{test: /\.(js|jsx)$/, // 匹配文件
exclude: /node_modules/, // 过滤文件夹
use: {loader: "babel-loader"}
},
{
test: /\.s?css$/, // 匹配文件
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
// you can specify a publicPath here
// by default it use publicPath in webpackOptions.output
publicPath: "../"
}
},
// "style-loader", // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面
"css-loader", // 加载.css 文件将其转换为 JS 模块
"sass-loader" // 加载 SASS / SCSS 文件并将其编译为 CSS
]
},
{test: /\.(html)$/,
use: {
loader: "html-loader",
options: {attrs: ["img:src", "img:data-src", "audio:src"],
minimize: true
}
}
},
{test: /\.(png|svg|jpe?g|gif)$/i, // 图片处理
use: [
{
loader: "url-loader",
options: {name: "[name].[hash:5].[ext]",
limit: 20 * 1024, // size <= 50kb
outputPath: "img"
}
},
{
loader: "image-webpack-loader",
options: {
// Compress JPEG images
mozjpeg: {
progressive: true,
quality: 65
},
// Compress PNG images
optipng: {enabled: false},
// Compress PNG images
pngquant: {
quality: "65-90",
speed: 4
},
// Compress GIF images
gifsicle: {interlaced: false},
// Compress JPG & PNG images into WEBP
webp: {quality: 75}
}
}
]
},
{test: /\.(woff|woff2|eot|ttf|otf)$/, // 字体处理
use: ["file-loader"]
},
{
test: /\.xml$/, // 文件处理
use: ["xml-loader"]
}
];
webpack.common.js
现在改改路径和引入, 瞬间清爽很多, 有个地方需要注意的是现在配置文件和 dist 文件不在同一个层级, 默认是不允许删除层级之上, 我们需要开放权限
因为最近更新版本不支持以前写法, 所以替换一下
// 清除文件
new CleanWebpackPlugin({
dangerouslyAllowCleanPatternsOutsideProject: true,
cleanOnceBeforeBuildPatterns: ["../dist"],
dry: true
}),
const path = require("path"),
HtmlWebpackPlugin = require("html-webpack-plugin"),
CleanWebpackPlugin = require("clean-webpack-plugin"),
MiniCssExtractPlugin = require("mini-css-extract-plugin"),
alias = require("./alias"),
rules = require("./rules");
module.exports = {
// 入口
entry: "./src/index.js",
// 输出
output: {
// 打包文件名
filename: "[name].bundle.js",
// 输出路径
path: path.resolve(__dirname, "../dist"),
// 资源请求路径
publicPath: ""
},
module: {rules},
plugins: [
// 清除文件
new CleanWebpackPlugin({
dangerouslyAllowCleanPatternsOutsideProject: true,
cleanOnceBeforeBuildPatterns: ["../dist/**"],
dry: false
}),
// 提取样式文件
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: "style/[name].[chunkhash:8].css",
chunkFilename: "style/[id].css"
}),
new HtmlWebpackPlugin({
// title
title: "test",
// 模板
template: "index.html"
})
],
resolve: {
// 创建 import 或 require 的别名,来确保模块引入变得更简单
alias
}
};
package.json
也稍微改一下执行路径, 换个更加合适的命令名
"scripts": {
"dev": "webpack --config ./config/webpack.dev.js",
"prod": "webpack --config ./config/webpack.prod.js",
"server": "webpack-dev-server --config ./config/webpack.server.js"
},
为了实现配置效果我们需要安装一个插件 cross-env
yarn add cross-env
这是一个可以跨平台系统设置环境变量的库, 简单来说就是在命令行设置变量
"scripts": {
"dev": "cross-env NODE_ENV=DEV webpack --config ./config/webpack.dev.js",
"prod": "cross-env NODE_ENV=PROD webpack --config ./config/webpack.prod.js",
"server": "cross-env NODE_ENV=SERVER webpack-dev-server --config ./config/webpack.server.js"
},
然后我们就能在 js 里获取 process.env.NODE_ENV
字段拿到我们自定义的字段了. 接下来我们修改一下配置文件
图片配置
我们目前的图片配置分别使用了 url-loader
转码和 image-webpack-loader
做压缩, 实际开发中我们不需要压缩, 所以将后者抽离.
{test: /\.(png|svg|jpe?g|gif)$/i, // 图片处理
use:
process.env.NODE_ENV === "PROD"
? [
{
loader: "url-loader",
options: {name: "[name].[hash:5].[ext]",
limit: 20 * 1024, // size <= 50kb
outputPath: "img"
}
},
{
loader: "image-webpack-loader",
options: {
// Compress JPEG images
mozjpeg: {
progressive: true,
quality: 65
},
// Compress PNG images
optipng: {enabled: false},
// Compress PNG images
pngquant: {
quality: "65-90",
speed: 4
},
// Compress GIF images
gifsicle: {interlaced: false}
}
}
]
: [
{
loader: "url-loader",
options: {name: "[name].[hash:5].[ext]",
limit: 20 * 1024, // size <= 50kb
outputPath: "img"
}
}
]
},
REACT 热更新
引入 react 之后会发现现在修改 js 代码会刷新, 但是浏览器需要手动刷新才看到效果, 控制台提示
Ignored an update to unaccepted module,The following modules couldn’t be hot updated: (They would need a full reload!)
这个问题我们可以通过引入 react-hot-loader 解决
yarn add react-hot-loader
先在 .babelrc
添加配置
{
"presets": [
["env", {modules: false}], "react"
],
"plugins": ["react-hot-loader/babel"]
}
然后把根组件包裹在里面输出, 修改 \src\page\main.jsx
文件
import React, {Component, Fragment} from "react";
import {Switch, Route, Redirect, Link} from "react-router-dom";
import {hot} from "react-hot-loader";
import View1 from "CMT/view1";
import View2 from "CMT/view2";
import "STYLE/style.scss";
class Main extends Component {constructor(props, context) {super(props, context);
this.state = {title: "Hello World!"};
}
render() {
return (
<Fragment>
<p>{this.state.title}</p>
<Link to="/view1/">View1</Link>
<Link to="/view2/">View2</Link>
<Switch>
<Route exact path="/" component={View1} />
<Route path="/view1/" component={View1} />
<Route path="/view2/" component={View2} />
<Redirect to="/" />
</Switch>
</Fragment>
);
}
}
export default hot(module)(Main);
然后重新执行命令测试即可
HTMl&CSS 热更新
如果足够认真的话你们会发现现在如果修改样式之后代码会更新, 但是浏览器不会自动刷新了.
那是因为热更新的代码是输出在内存中, 而我们之前引入了 mini-css-extract-plugin
插件提取 css 单独合并一个模块之后, 尽管代码也会更新, 但是 html 引用的 link
没有改变所以沿用的还是更新前的样式,html 更新暂时没找到方法, 但是不影响 React 开发, 而 css 更新我们可以用过环境配置, 不提取样式解决.
当然, 如果真的需要实现 html 更新的话, 可以简单粗暴的换回全局刷新即可
hot: true,
// hotOnly: true
rules.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = [{test: /\.(js|jsx)$/, // 匹配文件
exclude: /node_modules/, // 过滤文件夹
use: {loader: "babel-loader"}
}, {
test: /\.s?css$/, // 匹配文件
use: [process.env.NODE_ENV !== "SERVER" ? {
loader: MiniCssExtractPlugin.loader,
options: {
// you can specify a publicPath here
// by default it use publicPath in webpackOptions.output
publicPath: '../'
}
} : 'style-loader', // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面,
'css-loader', // 加载.css 文件将其转换为 JS 模块
'sass-loader' // 加载 SASS / SCSS 文件并将其编译为 CSS
]
},
{test: /\.(html)$/,
use: {
loader: "html-loader",
options: {attrs: ["img:src", "img:data-src", "audio:src"],
minimize: true
}
}
},
{test: /\.(png|svg|jpe?g|gif)$/i, // 图片处理
use:
process.env.NODE_ENV === "PROD"
? [
{
loader: "url-loader",
options: {name: "[name].[hash:5].[ext]",
limit: 20 * 1024, // size <= 50kb
outputPath: "img"
}
},
{
loader: "image-webpack-loader",
options: {
// Compress JPEG images
mozjpeg: {
progressive: true,
quality: 65
},
// Compress PNG images
optipng: {enabled: false},
// Compress PNG images
pngquant: {
quality: "65-90",
speed: 4
},
// Compress GIF images
gifsicle: {interlaced: false}
}
}
]
: [
{
loader: "url-loader",
options: {name: "[name].[hash:5].[ext]",
limit: 20 * 1024, // size <= 50kb
outputPath: "img"
}
}
]
},
{test: /\.(woff|woff2|eot|ttf|otf)$/, // 字体处理
use: ["file-loader"]
},
{
test: /\.xml$/, // 文件处理
use: ["xml-loader"]
}
]
webpack.common.js
const path = require("path"),
HtmlWebpackPlugin = require("html-webpack-plugin"),
CleanWebpackPlugin = require("clean-webpack-plugin"),
MiniCssExtractPlugin = require("mini-css-extract-plugin"),
alias = require("./alias"),
rules = require("./rules");
module.exports = {
// 入口
entry: "./src/index.js",
// 输出
output: {
// 打包文件名
filename: "[name].bundle.js",
// 输出路径
path: path.resolve(__dirname, "../dist"),
// 资源请求路径
publicPath: ""
},
module: {rules},
plugins: [
// 清除文件
new CleanWebpackPlugin({
dangerouslyAllowCleanPatternsOutsideProject: true,
cleanOnceBeforeBuildPatterns: ["../dist/**"],
dry: false
}),
// 提取样式文件
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename:
process.env.NODE_ENV !== "PROD"
? "[name].css"
: "style/[name].[contenthash].css",
chunkFilename:
process.env.NODE_ENV !== "PROD"
? "[id].css"
: "style/[id].[contenthash].css"
}),
new HtmlWebpackPlugin({
// title
title: "test",
// 模板
template: "index.html"
})
],
resolve: {
// 创建 import 或 require 的别名,来确保模块引入变得更简单
alias
}
};
输出名字那里也换了一下, 官方推荐
For long term caching use
filename: "[contenthash].css"
. Optionally add[name]
.
默认配置如下:
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: devMode ? '[name].css' : '[name].[hash].css',
chunkFilename: devMode ? '[id].css' : '[id].[hash].css',
})
但是这样子即使没改动也会导致 hash 改变而重新打包, 这里简单说一下几种常用变量的区别
- hash: 和整个项目构建相关并且全部文件公用相同 hash 值, 即没有缓存效果只适用于开发阶段
- chunkhash: 根据入口依赖文件解析构建对应的 chunk 生成对应的 hash 值, 可以保证正常业务修改不影响公共代码, 因为公共代码属于一个单独模块, 但是样式被打包进业务模块所以两者公用同一个 chunkhash.
- contenthash: 样式模块根据自身内容而生成, 做到不被业务代码改变而影响
因为对应打包路径换了一下, 所以 loader 也需要判断一些路径
{
test: /\.s?css$/, // 匹配文件
use: [
process.env.NODE_ENV !== "SERVER"
? {
loader: MiniCssExtractPlugin.loader,
options: {
// you can specify a publicPath here
// by default it use publicPath in webpackOptions.output
publicPath: process.env.NODE_ENV === "DEV" ? "./" : "../"
}
}
: "style-loader", // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面,
"css-loader", // 加载.css 文件将其转换为 JS 模块
{
loader: "postcss-loader",
options: {
config: {path: "./" // 写到目录即可,文件名强制要求是 postcss.config.js}
}
},
"sass-loader" // 加载 SASS / SCSS 文件并将其编译为 CSS
]
},
生产警告!!!!
你们以为这样就算完了? 不, 你打包生产环境看看
npm run prod
然后你会发现居然没有打包 css!!??
到处查找资料发现有两种办法解决
修改引入方法
import "STYLE/style.scss"; -> require("STYLE/style.scss");
修改package.json
"sideEffects": ["*.scss", "*.css"]
具体原因可以看
CSS 压缩
mini-css-extract-plugin
没有实现压缩功能, 我们自己重新引用一个完成库optimize-css-assets-webpack-plugin
yarn add optimize-css-assets-webpack-plugin
它会在构建期间搜索 css 资源并且优化压缩处理.
然后生产配置文件修改webpack.prod.js
const merge = require("webpack-merge"),
common = require("./webpack.common.js"),
OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
// 原始源代码
devtool: 'source-map',
plugins: [new OptimizeCssAssetsPlugin()
]
});
CSS 增强
postcss-loader可以同通过配置增强 CSS 的功能, 在这里我们先简单使用自动补全前缀的功能, 首先
yarn add postcss-loader autoprefixer
在根目录新建 postcss.config.js
作为配置文件
const autoprefixer = require('autoprefixer');
module.exports = {
plugins: [
autoprefixer({browsers: ['iOS >= 6', 'Android >= 4', 'IE >= 9']
})
]
};
只是加载插件设定兼容的系统版本, 同时也要在 rules.js
修改, 需要设定寻找配置的路径
{
test: /\.s?css$/, // 匹配文件
use: [process.env.NODE_ENV !== "SERVER" ? {
loader: MiniCssExtractPlugin.loader,
options: {
// you can specify a publicPath here
// by default it use publicPath in webpackOptions.output
publicPath: '../'
}
} : 'style-loader', // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面,
'css-loader', // 加载.css 文件将其转换为 JS 模块
{
loader: 'postcss-loader',
options: {
config: {path: './' // 写到目录即可,文件名强制要求是 postcss.config.js}
}
},
'sass-loader' // 加载 SASS / SCSS 文件并将其编译为 CSS
]
}
注意引入位置
Use it after
css-loader
andstyle-loader
, but before other preprocessor loaders like e.gsass|less|stylus-loader
, if you use any.
代理
启动服务器开发有时候需要访问外部域名请求, 但是后台又没帮你解决跨域问题的话, 我们可以再配置增加一个跨域配置, 如下
devServer: {
// 打开模式, Iframe mode 和 Inline mode 最后达到的效果都是一样的,都是监听文件的变化,然后再将编译后的文件推送到前端,完成页面的 reload 的
inline: true,
// 指定了服务器资源的根目录
contentBase: path.join(__dirname, '../dist'),
// 是否开启 gzip 压缩
compress: false,
port: 9000,
// 是否开启热替换功能
// hot: true,
// 是否自动打开页面, 可以传入指定浏览器名字打开
open: false,
// 是否开启部分热替换功能
hotOnly: true,
proxy: {
'/api': {
// 代理地址
target: 'http://alpha.xiaohuxi.cn',
changeOrigin: true,
// 默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。如果你想要接受
secure: true,
// 重写路径
pathRewrite: {'^/api': ''},
}
}
},
当下所有 /api
的请求都会被转发到 http://www.test.cn
地址去, 更多用法参考文档 http-proxy-middleware