前端开发基础之 - Webpack

参考:https://www.valentinog.com/blog/webpack-tutorial

Webpack 4 更新不短时间了,简单做个整理。

本文演示基于仓库:https://github.com/mrzzcn/webpack-playground

1. 完全 0 配置的模块化打包利器

习惯了webpack.config.js的同学可能不知道这个变化,Webpack 4 默认不需要配置文件了,在一个没有配置文件的项目里运行webpack会默认查找./src/index.js,输出到./dist/main.js。内部使用了一个默认配置文件主要内容如下:

1
2
3
4
5
6
7
8
9
10
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve('dist'),
filename: 'main.js'
},
...
};

源码:https://github.com/mrzzcn/webpack-playground/tree/step-0

2. mode: production & development

真实项目中我们经常会有多个配置文件对应多个环境:
development: 本地开发环境,需要处理 webpack-dev-server proxy 等配置
testing:本地测试
stage:beta 环境,替换域名,发布到测试服务器
production:生产环境,替换域名,压缩,增加文件指纹更新缓存,发布到 CDN 等

Webpack 为我们提供 mode 选项用来使用一些默认的配置,有三个值:development | production | none
先看下不添加 mode 时候运行的一个警告:

动态演示:https://asciinema.org/a/240832

可以看到 Webpack 是必须使用 mode 选项的,如果没有提供,将会使用默认值 production,同时会将 DefinePluginprocess.env.NODE_ENV 的值设置为 production
三个值区别如下:

选项 描述
development 会将 DefinePluginprocess.env.NODE_ENV 的值设置为 development。 启用 NamedChunksPluginNamedModulesPlugin
production 会将 DefinePluginprocess.env.NODE_ENV 的值设置为 production。 启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginTerserPlugin
none 会将 DefinePluginprocess.env.NODE_ENV 的值设置为 none。 退出任何默认优化选项

关于 mode 的更多具体信息,请查阅:https://webpack.docschina.org/concepts/mode/
源码:https://github.com/mrzzcn/webpack-playground/tree/step-1

3. 更改默认的入口文件和输出?也可以!

1
2
3
4
5
6
// package.json
"scripts": {
"start": "webpack",
"build:dev": "webpack --mode development ./src/index.js --output ./dist/foo/main.js",
"build": "webpack --mode production ./src/index.js --output ./dist/bar/main.js"
}

源码:https://github.com/mrzzcn/webpack-playground/tree/step-2

4. ES6 with Babel 7

现在写 JS 基本离不开 ES6,使用 ES6 就意味着必须使用编译器。webpack 使用 babel-loader 处理 JS,把 ES6 以上的语法编译到 ES5 适配大多数浏览器。
Babel 7 是 Babel 的最新版本,使用之前需要引用以下依赖:

  1. babel core
  2. babel loader
  3. babel preset env
1
npm i @babel/core babel-loader @babel/preset-env --save-dev

在项目根目录增加文件: .babelrc

1
2
3
4
5
{
"presets": [
"@babel/preset-env"
]
}

下一步,有两种方法启用 babel-loader:

  • 添加配置文件webpack.config.js
  • 使用命令行参数选项--module-bind

webpack4 号称是 0配置 的,这里为啥又出来配置文件呢?其实 webpack4 0配置 的作用是有限的:

  • 入口 (entry point),默认值为:src/index.js
  • 输出 (output),默认值为:dist/main.js
  • mode (production or development)

一般的使用是足够了的,不过如果想要使用自定义的 loader 还是必须创建 config 文件的,源引作者 Sean 的一段回答:

Q: Will loaders in webpack 4 work the same as webpack 3? Is there any plan to provide 0 conf for common loaders like babel-loader?
A: “For the future (after v4, maybe 4.x or 5.0), we have already started the exploration of how a preset or addon system will help define this. What we don’t want: To try and shove a bunch of things into core as defaults What we do want: Allow other to extend”

借助配置文件webpack.config.js使用 babel-loader

按你熟悉的方式添加文件webpack.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
};

src/index.js 里面添加一些 ES6 代码。重新编译,看看结果是不是和你预想的一样呢?使用这种方式还可以查看 ES6 类的运行机制。

不添加配置文件启用 babel-loader 的方法

的确有一种方法可以不需要配置文件,那就是--module-bind选项,这是 webpack3 引入的。
删除 webpack.config.js
更改 npm scripts,添加参数:

1
2
3
4
5
"scripts": {
"start": "webpack",
"build:dev": "webpack --mode development ./src/index.js --output ./dist/foo/main.js --module-bind js=babel-loader",
"build": "webpack --mode production ./src/index.js --output ./dist/bar/main.js --module-bind js=babel-loader"
}

重新编译,看和上一步的编译结果是否相同。

这种用法会让 编译命令变长从而破坏可读性,了解即可,不推荐在实际项目中使用。

源码:https://github.com/mrzzcn/webpack-playground/tree/step-3

5. React

经常有人会说 React 比较重,学习曲线陡峭,其实,如果你使用 ES6 和 babel,配置 React 相当容易:

1
2
npm i react react-dom --save-dev
npm i @babel/preset-react --save-dev

.babelrc 中配置 React preset

1
2
3
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

完了!
如果你更喜欢.jsx后缀名来区分普通纯 JS 文件和 包含 JSX 语法的 React 页面或组件,需要在webpack.config.js文件中配置 JSX 支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/, // 此处增加 jsx 后缀名识别
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
};

来测试一下,添加文件:./src/App.jsx:

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
return (
<div>
<p>React here!</p>
</div>
);
};
export default App;
ReactDOM.render(<App />, document.getElementById('app'));

在入口文件./src/index.js中添加:

1
import App from './App.jsx';

重新编译,你会发现输出内容变多了不少,因为 React 库被打包进来,不过在文件结尾处,你仍然可以分辨出来我们编写的 React 组件被编译之后的样子。

一旦理解了应用程序的本质:数据结构+算法,你会觉得 React 的设计有多么干净、纯粹,你会觉得自己原来使用模板引擎拼凑代码的方式简直像数字时代的原始人。而一旦写到表单部分,你会突然发现做个原始人挺快乐的。。。😂

HTML webpack plugin

组件需要展示出来,如果想要“看见”,需要把编译的结果呈现在网页上,webpack 使用 html-webpack-pluginhtml-loader 来处理 HTML 文件。

1
npm i html-webpack-plugin html-loader --save-dev

添加src/index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>webpack 4 quickstart</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

编辑webpack.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const HtmlWebPackPlugin = require("html-webpack-plugin");

module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
{
test: /\.html$/,
use: [
{
loader: "html-loader",
options: { minimize: false }
}
]
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/index.html",
filename: "./index.html"
})
]
};

重新编译,看一下dist文件夹,应该可以看到编译产生的 html 文件,使用浏览器打开,即可预览刚才编写的 React 组件。
无需手动添加 JS 引用,webpack 会把编译输出的 资源 自动注入到 Html 文件相应位置。

源码:https://github.com/mrzzcn/webpack-playground/tree/step-4

6. VUE

VUE 对初学者非常友好,有很多API,几乎你能想到的他全都为你做了。而且官网的文档很详细,各种语言版本都有。重要的是,对于旧项目(非MV**框架的项目)而言,使用 VUE 重构非常方便。
在 webpack 项目里使用 VUE也很方便:

1
npm i vue --save-dev

修改 ./src/index.js:

1
2
3
4
5
6
7
8
9
import Vue from 'vue';

const app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
template: `<div>{{message}}</div>`
});

修改webpack.config.js:

1
2
3
4
5
resolve: {
alias: {
vue$: "vue/dist/vue.esm.js"
}
}

关于 VUE 的 不同构建版本的解释 请移步:不同构建版本的解释

重新编译,刷新预览,可以看到 VUE 实例已经注入到页面中。

源码:https://github.com/mrzzcn/webpack-playground/tree/step-5

7. CSS

webpack 默认不会吧 CSS 内容提取到单独的 css 文件里。之前我们用 extract-text-webpack-plugin 做这件事,不幸的是 这个插件对 webpack4 的支持并不十分友好。

Michael Ciniawsky: extract-text-webpack-plugin reached a point where maintaining it become too much of a burden and it’s not the first time upgrading a major webpack version was complicated and cumbersome due to issues with it.

不过不用着急,我们有 mini-css-extract-plugin(需要 webpack 4.2.X 以上)。

1
npm i mini-css-extract-plugin css-loader --save-dev

添加 loader 和 提取插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const HtmlWebPackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.html$/,
use: [
{
loader: 'html-loader',
options: { minimize: false }
}
]
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
resolve: {
alias: {
vue$: 'vue/dist/vue.esm.js'
}
},
plugins: [
new HtmlWebPackPlugin({
template: './src/index.html',
filename: './index.html'
}),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].[id].css'
})
]
};

添加 css 文件src/main.css:

1
2
3
4
body {
line-height: 2;
background-color: green;
}

src/index.js中引入 css 文件:import './main.css';
重新编译,刷新预览,你会发现页面文字变得更高,背景也变成更清新的绿色。

源码:https://github.com/mrzzcn/webpack-playground/tree/step-6

8. webpack dev server

每次更改代码都要重新编译,手动刷新,是不是很烦,使用 webpack dev server启动开发服务器,可以大大方便我们的开发调试过程:

1
npm i webpack-dev-server --save-dev

修改 npm scripts:

1
2
3
4
5
"scripts": {
"start": "webpack-dev-server --mode development --open", // 使用 webpack-dev-server 启动开发服务器
"build:dev": "webpack --mode development",
"build": "webpack --mode production"
}

终端启动 npm start
webpack 会启动开发服务器并在默认浏览器中打开预览页面,随意更改源代码,稍等几秒页面即可自动刷新。

源码:https://github.com/mrzzcn/webpack-playground/tree/step-7

9. Proxy

前端开发中,经常会遇到多端同步开始的情况。比如,需求定下来之后,设计、前端、后端都开始工作,前、后端依赖与共同约定的接口开发,最后双方都 Ready 再联调。后端在开发过程中可以使用浏览器、Postman、CURL等工具发起请求进行测试。但是前端在没有后端接口的情况下也要继续工作,这时候就需要模拟后端接口。
修改webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
// 增加以下节点:
devServer: {
proxy: {
'/api': {
target: 'https://yapi-demo.herokuapp.com/mock/16',
changeOrigin: true,
pathRewrite: { '^/api': '' },
toProxy: true
}
}
}

修改 src/index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Vue from 'vue';
import './main.css';

const app = new Vue({
el: '#app',
data: {
message: 'Hello Vue !' + new Date().toTimeString(),
posts: []
},
created() {
fetch('/api/posts')
.then(res => res.json())
.then(posts => {
this.posts = posts;
});
},
template: `<div>
{{message}}
<hr/>
Total {{posts.length}} posts:
<ul>
<li v-for="post in posts" key={{post.id}}>{{post.title}}</li>
</ul>
</div>`
});

保存,重新启动开发服务器。
在打开的浏览器窗口中可以预览利用 mock server 模拟后端接口的应用程序预览。

关于代理服务器 https://yapi-demo.herokuapp.com/mock/16 的搭建,可以参考:使用 Heroku 和 mongoDB Atlas 免费托管 YApi YApi 官网
YApi 是我见过最优秀的 Api 管理软件,同类软件还有 zan-proxy 和 nei 等,不过功能相对较弱。后面我会写文章继续介绍 YApi 的高级用法,包括数据模拟、动态期望、集成对后端Api的自动化测试等。