前言
之前一段时间工作原因把精力都放在小程序上,趁现在有点空闲时间,刚好官方文档也补充完整了,我准备重温一下 webpack 之路了,因为官方文档已经写得非常详细,我会大量引用原文描述,主要重点放在怎么从零构建 webpack4 代码上,这不是一个系统的教程,而是从零摸索一步步搭建起来的笔记,所以前期可能bug会后续发现继续修复而不是修改文章.
系列文章
webpack4从零开始构建(一)
webpack4+React16项目构建(二)
webpack4功能配置划分细化(三)
webpack4引入Ant Design和Typescript(四)
webpack4代码去重,简化信息和构建优化(五)
webpack4配置Vue版脚手架(六)
基本已经可以使用的完整配置webpack4_demo
PS:
2018/12/12 修改细节布局
2018/12/26上传,代码同步到第四篇文章
2019/03/14上传,补充代码到第二篇文章
引入React
首先安装React环境库
yarn add react react-dom react-router-dom
react和react-dom的区别
- react是核心代码,只包含了定义组件的方法如React.createElement,.createClass,.Component,.children以及其他元素和组件类。
- react-dom是渲染代码,包括ReactDOM.render,.unmountComponentAtNode和.findDOMNode等实现将虚拟DOM渲染到界面
react-router和react-router-dom的区别
- react-router: 实现了路由的核心功能
- react-router-dom: 基于react-router,加入了在浏览器运行环境下的一些功能,例如:Link组件,会渲染一个a标签,Link组件源码a标签行; BrowserRouter和HashRouter组件,前者使用pushState和popState事件构建路由,后者使用window.location.hash和hashchange事件构建路由。
- react-router-native: 基于react-router,类似react-router-dom,加入了react-native运行环境下的一些功能。
开始使用
修改index.html如下
<!doctype html>
<html>
<head>
<title>webpack + React</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
修改index.js,引用React语法写个简单例子
import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(<div>Hello world</div>, document.getElementById("root"));
到此还没结束,我们还需要安装下面babel依赖配置webpack环境才能正常打包运行
yarn add babel-core babel-loader@7 babel-preset-env babel-preset-react
粗略讲解一下各个依赖干嘛的
- babel-core是作为babel的核心,把 javascript 代码分析成 AST (抽象语法树, 是源代码的抽象语法结构的树状表现形式),方便各个插件分析语法进行相应的处理
- babel-loader也是核心插件,允许使用Babel和webpack转换JavaScript文件
- babel-preset-env基于你的实际浏览器及运行环境,自动的确定babel插件及polyfills,转译ES2015及此版本以上的语言
- babel-preset-react编译react代码
注意: 因为babel-core@7+还不稳定,所以默认安装@6+,需要babel-loader@7+才能运行,所以上面指定了版本
根目录新增 .babelrc
配置文件,babel所有的操作基本都会来读取这个配置文件,如果没有这个配置文件,会从package.json
文件的babel属性中读取配置。
{
"presets": [
["env", {
modules: false
}], "react"
]
}
注意: 因为Tree Shaking这个功能是基于ES6 modules 的静态特性检测,来找出未使用的代码,所以如果你使用了 babel 插件的时候,如:babel-preset-env,它默认会将模块打包成commonjs,这样就会让Tree Shaking失效了,所以我们要设置一下关闭 Babel 的模块转换功能,保留原本的 ES6 模块化语法。
我们还要去webpack.common.js
配置loader,完整配置如下
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
module.exports = {
// 入口
entry: "./src/index.js",
// 输出
output: {
// 打包文件名
filename: "[name].bundle.js",
// 输出路径
path: path.resolve(__dirname, "dist"),
// 资源请求路径
publicPath: ""
},
module: {
rules: [
{
test: /\.(js|jsx)$/, // 匹配文件
exclude: /node_modules/, // 过滤文件夹
use: {
loader: "babel-loader"
}
},
{
test: /\.(css|scss)$/, // 匹配文件
use: [
"style-loader", // 使用<style>将css-loader内部样式注入到我们的HTML页面
"css-loader", // 加载.css文件将其转换为JS模块
"sass-loader" // 加载 SASS / SCSS 文件并将其编译为 CSS
]
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/, // 图片处理
use: ["file-loader"]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/, // 字体处理
use: ["file-loader"]
},
{
test: /\.xml$/, // 文件处理
use: ["xml-loader"]
},
{
test: /\.html$/, // 处理html资源如图片
use: ["html-loader"]
}
]
},
plugins: [
// 清除文件
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
// title
title: "test",
// 模板
template: "index.html"
})
]
};
继续使用上一章配置过命令和当前依赖文件package.json, 完整代码如下:
{
"sideEffects": false,
"scripts": {
"dev": "webpack --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
"start": "webpack-dev-server --config webpack.server.js"
},
"dependencies": {
"babel-core": "^6.26.3",
"babel-loader": "7",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"clean-webpack-plugin": "^2.0.0",
"css-loader": "^2.1.1",
"file-loader": "^3.0.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.11.0",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"react-router-dom": "^4.3.1",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"webpack": "^4.29.6",
"webpack-bundle-analyzer": "^3.1.0",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.1",
"webpack-merge": "^4.2.1",
"xml-loader": "^1.2.1"
},
"name": "webpack_demo",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
打开终端执行命令
npm run dev
运行dist目录下的index.html文件可以查看效果
项目拓展
接下来继续展开代码,按照正常项目开发使用ES6方式开发和引用资源处理,首先我们分门别类区分一下资源
index.js修改如下:
import React from "react";
import ReactDOM from "react-dom";
import Main from "./page/main";
ReactDOM.render(<Main />, document.getElementById("root"));
新增main.js
文件代码如下:
import React, { Component, Fragment } from "react";
import ReactDOM from "react-dom";
import "../style/style.scss";
export default class Main extends Component {
constructor(props, context) {
super(props, context);
this.state = {
title: "Hello World!"
};
}
// 挂载前
componentWillMount() {
console.log("componentWillMount");
}
// 挂载后
componentDidMount() {
console.log("componentDidMount");
}
// 接受新props
componentWillReceiveProps(nextProps) {
console.log("componentWillReceiveProps", nextProps);
}
// 是否重新渲染
shouldComponentUpdate(nextProps, nextState) {
console.log("shouldComponentUpdate", nextProps, nextState);
}
// 更新前
componentWillUpdate(nextProps, nextState) {
console.log("componentWillUpdate", nextProps, nextState);
}
// 更新后
componentDidUpdate(prevProps, prevState) {
console.log("componentDidUpdate", nextProps, nextState);
}
// 卸载前
componentWillUnmount() {
console.log("componentWillUnmount");
}
// 捕捉错误
componentDidCatch() {
console.log("componentDidCatch");
}
render() {
return (
<Fragment>
<img className="img1" src={require("../img/1.jpg")} alt="" />
<div className="img2" />
<p>{this.state.title}</p>
</Fragment>
);
}
}
在jsx里相对路径的图片不会被file-loader和url-loader处理,所以我们使用这种写法引入比较方便
<img className="img1" src={require("../img/1.jpg")} alt="" />
style.scss如下:
html {
background-color: #666;
p {
color: red;
}
.img1,
.img2 {
width: 250px;
height: 400px;
}
.img2 {
background: url("../img/2.jpg") no-repeat center center;
background-size: cover;
}
}
修改一下webpack.common.js
图片处理,使用url-loader将50kb内的图片转成base64编码保存进代码减少请求,不符合条件的打包图片放到一个单独文件夹img,因为url-loader内置有file-loader,所以我们不必要再引入
yarn add image-webpack-loader
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
attrs: ["img:src", "img:data-src", "audio:src"],
minimize: true
}
}
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i, // 图片处理
use: [
{
loader: "url-loader",
options: {
name: "[name].[hash:5].[ext]",
limit: 50 * 1024, // size <= 50kb
outputPath: "img"
}
}
]
},
重新执行命令
npm run dev
打开页面会看到1.jpg变成base64代码,一切都在预期内.
使用路由
根目录新增router文件夹,里面创建index.js,代码如下:
import React, { Component, Fragment } from "react";
import Main from "../page/main";
class App extends Component {
render() {
return (
<Fragment>
<Main />
</Fragment>
);
}
}
然后收拾一下main.js页面,把多余生命周期清除掉
import React, { Component, Fragment } from "react";
import { Switch, Route, Redirect, Link } from "react-router-dom";
import View1 from "../component/view1";
import View2 from "../component/view2";
import "../style/style.scss";
export default 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>
);
}
}
分别新增page1.js
和page2.js,main.js
的图片分别迁移进去新目录component
import React, { Fragment } from "react";
export default () => {
return (
<Fragment>
<p>Page1</p>
<img className="img1" src={require("../img/1.jpg")} alt="" />
</Fragment>
);
};
import React, { Fragment } from "react";
export default () => {
return (
<Fragment>
<p>Page2</p>
<div className="img2" />
</Fragment>
);
};
最后src目录下的index.js修改如下:
import React from "react";
import ReactDOM from "react-dom";
import { HashRouter } from "react-router-dom";
import Main from "./page/main";
ReactDOM.render(
<HashRouter>
<Main />
</HashRouter>,
document.getElementById("root")
);
现在整个目录结构如下
执行命令
npm run dev
一个简单的路由切换页面就完成了,界面大概如下
图片压缩
上面我们只是将小于50kb的图片内嵌进代码里,超过50kb的图片我们可以引入插件作处理
yarn add image-webpack-loader
然后我们在修改一下loader配置
{
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
}
}
}
]
},
注意顺序,这种写法会先经过压缩之后再有url-loader作处理,能够让部分原本不符合大小的图片压缩之后就满足转码base64了,为了突出效果限制到20kb内.
以我的测试图为例,压缩率达到
80.4kb -> 45.9kb
至于其他图片配置可根据自己需求修改
解析(resolve)
随着文件越来越多,引用路径越来越复杂,会容易让人混乱,我们可以使用resolve做些依赖处理,这些选项能设置模块如何被解析
在webpack.common.js
新增下面配置代码,设置简化路径
resolve: {
// 创建 import 或 require 的别名,来确保模块引入变得更简单
alias: {
"@": 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")
}
}
然后我们就可以修改对应的文件引入模块写法,例如
import "STYLE/style.scss";
其他可自行修改
打包文件性能可视化
目前基本搭建完了,然后我们就可以利用一款检测打包性能的插件找到可优化空间
yarn add webpack-bundle-analyzer
在webpack.server.js新增依赖
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
plugins里初始化方法
new BundleAnalyzerPlugin()
执行命令会自动打开页面http://127.0.0.1:8888/,这里可以看到性能图,不影响原本的http://localhost:9000/#/查看项目
npm run start
CSS优化
webpack4使用插件和之前版本不一样,我们安装以下依赖
yarn add mini-css-extract-plugin
修改一下webpack.common.js的配置
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
--------------------------省略-----------------------------------
{
test: /\.scss$/, // 匹配文件
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
]
},
---------------------------省略------------------------------------
plugins: [
// 提取样式文件
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"
})
],
执行命令之后就看到有新的样式文件独立出来了
npm run dev
提取公共库
从webpack4开始官方移除了commonchunk插件,改用了optimization属性进行更加灵活的配置,再生产环境下回自动开启,只需要设置
mode: "production"
从文档来看它的默认设置是这样的
New chunk can be shared OR modules are from the node_modules folder
New chunk would be bigger than 30kb (before min+gz)
Maximum number of parallel requests when loading chunks on demand would be lower or equal to 5
Maximum number of parallel requests at initial page load would be lower or equal to 3
新chunk是能够被共享或者来自node_modules文件
新chunk在min+gz压缩之前大于30kb
按需加载的并行请求数小于等于5
首屏渲染的最大并行请求数小于等于3
因为现在demo比较小,没什么好讲解的,一般根据项目情况调整一下拆分机制就好了,假如我想要把node_modules和组件代码拆分出来,可以这么写
module.exports = merge(common, {
optimization: {
splitChunks: {
// 表示显示块的范围,有三个可选值:initial(初始块)、async(按需加载块)、all(全部块)
chunks: "all",
cacheGroups: {
libs: {
// 优先级高于其他就不会被打包进其他chunk,如果想匹配自己定义的拆分规则,则priority需要设置为正数,优先匹配默认拆分规则就设置为负数。
priority: 10,
test: /[\\/]node_modules[\\/]/,
name: "chunk-libs",
chunks: "initial"
},
commons: {
// 优先级高于其他就不会被打包进其他chunk,如果想匹配自己定义的拆分规则,则priority需要设置为正数,优先匹配默认拆分规则就设置为负数。
priority: 15,
test: path.resolve(__dirname, "src/component"),
name: "chunk-commons",
// 最小共用次数
minChunks: 2,
// 如果当前chunk已经被打包进其他chunk的时候就不再打包,而是复用其他chunk
reuseExistingChunk: true
}
}
}
}
});
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。