More than code
Welcome to the website

基于 webpack4.x的react脚手架搭建

一、前言

文章更新于2018年11月04日 基于webpack4.x

Webpack 是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过 loader 的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、 AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 LESS 等。

伴随着移动互联的大潮,当今越来越多的网站已经从网页模式进化到了 Webapp 模式。它们运行在现代的高级浏览器里,使用 HTML5、 CSS3、 ES6 等更新的技术来开发丰富的功能,网页已经不仅仅是完成浏览的基本需求,并且webapp通常是一个单页面应用,每一个视图通过异步的方式加载,这导致页面初始化和使用过程中会加载越来越多的 JavaScript 代码,这给前端开发的流程和资源组织带来了巨大的挑战。

前端开发和其他开发工作的主要区别,首先是前端是基于多语言、多层次的编码和组织工作,其次前端产品的交付是基于浏览器,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统,这个理想中的模块化系统是前端工程师多年来一直探索的难题。

二、什么是webpack,为什么要使用它

1. 什么是webpack?

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。

2. 为什要使用WebPack?

现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法

  • 模块化,让我们可以把复杂的程序细化为小的文件;
  • 类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版+ 本的JavaScript不能直接使用的特性,并且之后还能转换为JavaScript文件使浏览器可以识别;
  • Scss,less等CSS预处理器

这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack类的工具的出现提供了需求。

3. WebPack和Grunt以及Gulp相比有什么特性

其实Webpack和另外两个并没有太多的可比性,Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具。

Grunt和Gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。

Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。

如果实在要把二者进行比较,Webpack的处理速度更快更直接,能打包更多不同类型的文件。

三、开始使用Webpack

1. 安装

1.1 生成package.json文件

Webpack可以使用npm安装,新建一个空的练习文件夹(此处命名为demo),在demo文件夹中创建一个package.json文件,这是一个标准的npm说明文件,里面蕴含了丰富的信息,包括当前项目的依赖模块,自定义的脚本任务等等。在终端中使用npm init命令可以自动创建这个package.json文件。

npm init

输入这个命令后,终端会问你一系列诸如项目名称,项目描述,作者等信息,不过不用担心,如果你不准备在npm中发布你的模块,这些问题的答案都不重要,回车默认即可。也可以使用npm init -y这个命令来一次生成package.json文件,这样终端不会询问你问题。

1.2 安装webpack

这里把全局webpackwebpack-cli和本地项目webpackwebpack-cli全都先装了,因为后面一些模块会用到。安装webapck时把webpack-cli也装上是因为在webpack4.x版本后webpack模块把一些功能分到了webpack-cli模块,所以两者都需要安装,安装方法如下:

npm install webpack webpack-cli --global    //这是安装全局webpack及webpack-cli模块
npm install webpack webpack-cli --save-dev  //这是安装本地项目模块

上述命令可采用简写,install可简写为i,--global可简写为-g,--save-dev可简写为-D(这个命令是用于把配置添加到package.json的开发环境配置列表中,后面会提到),--save可简写为-S

npm i webpack -g               //这是安装全局webpack命令
npm i webpack webpack-cli -D   //这是安装本地项目模块

2. 使用

2.1 创建项目

在demo文件夹里面创建两个文件夹srcdist文件夹,src文件夹用来存放原始数据和我们将要写的JavaScript模块,dist文件夹用来存放之后供浏览器读取的文件(包括使用webpack打包生成的js文件以及一个index.html文件)。接下来我们再创建三个文件:

  • index.html –放在dist文件夹中;
  • hello.js –放在src文件夹中;
  • index.js –放在src文件夹中;

我们在index.html文件中写入最基础的html代码,它在这里目的在于引入打包后的js文件(这里我们先把之后打包后的js文件命名为bundle.js,之后我们还会详细讲述)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack Project</title>
</head>
<body>
    <div id='root'></div>
    <script src="bundle.js"></script>   <!--这是打包之后的js文件,我们暂时命名为bundle.js-->
</body>
</html>

我们在hello.js中定义一个返回包含问候信息的html元素的函数,并依据CommonJS规范导出这个函数为一个模块:

// hello.js
module.exports = function() {
    let hello = document.createElement('div');
    hello.innerHTML = "hello world";
    return hello;
};

然后在index.js中引入这个模块(hello.js):

//index.js 
const hello = require('./hello.js');
document.querySelector("#root").appendChild(hello());

2.2 webpack打包

在终端中使用如下命令进行打包:

# webpack全局安装的情况下
webpack src/index.js --output=dist/bundle.js  

# webpack非全局安装的情况
node_modules/.bin/webpack src/index.js --output=dist/bundle.js

上述就相当于把src文件夹下的index.js文件打包到dist文件下的bundle.js,这时就生成了bundle.jsindex.html文件引用。

可以看出webpack同时编译了main.jshello.js,在public文件夹下生成了budle.js文件。

现在已经成功的使用Webpack打包了一个文件了。不过在终端中进行复杂的操作,其实是不太方便且容易出错的,接下来看看Webpack的另一种更常见的使用方法。

2.3 通过配置文件来使用webpack

Webpack拥有很多其它的比较高级的功能(比如说本文后面会介绍的loadersplugins),这些功能其实都可以通过命令行模式实现,但是正如前面提到的,这样不太方便且容易出错的,更好的办法是定义一个配置文件,这个配置文件其实也是一个简单的JavaScript模块,我们可以把所有的与打包相关的信息放在里面。

在demo文件夹目录下新建一个名为webpack.config.js的文件,我们在其中写入如下所示的简单配置代码,目前的配置主要涉及到的内容是入口文件路径和打包后文件的存放路径。

// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口文件
    output: {
        path: path.join( __dirname, "/dist"), //打包后的文件存放的地方
        filename: "bundle.js" //打包后输出文件的文件名
    }
}

有了这个配置文件,我们只需在终端中运行webpack命令就可进行打包,这条命令会自动引用webpack.config.js文件中的配置选项.

2.4 更快捷的执行打包任务

在命令行中输入命令需要代码类似于node_modules/.bin/webpack这样的路径其实是比较烦人的,不过值得庆幸的是npm可以引导任务执行,对npm进行配置后可以在命令行中使用简单的npm start命令来替代上面略微繁琐的命令。在package.json中对scripts对象进行相关设置即可,设置方法如下。

{
  "name": "webpack-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack",
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.23.1",
    "webpack-cli": "^3.1.2"
  }
}

:package.json中的script会按照一定顺序寻找命令对应位置,本地的node_modules/.bin路径就在这个寻找清单中,所以无论是全局还是局部安装的Webpack,都不需要写前面那指明详细的路径了。

npm的start命令是一个特殊的脚本名称,其特殊性表现在,在命令行中使用npm start就可以执行其对于的命令,如果对应的此脚本名称不是start,想要在命令行中运行时,需要这样用npm run {script name}npm run build

3、构建本地服务器

3.1 webpack-dev-server配置本地服务器

Webpack提供了一个可选的本地开发服务器,这个本地服务器基于node.js构建,它是一个单独的组件,在webpack中进行配置之前需要单独安装它作为项目依赖:

npm i webpack-dev-server -D

devServer作为webpack配置选项中的一项,以下是它的一些配置选项:

  • contentBase :设置服务器所读取文件的目录,当前我们设置为”./dist”
  • port :设置端口号,如果省略,默认为8080
  • inline :设置为true,当源文件改变时会自动刷新页面
  • historyApiFallback :设置为true,所有的跳转将指向index.html

现在我们把这些配置加到webpack.config.js文件上,如下:

// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口文件
    output: {
        path: path.join( __dirname, "/dist"), //打包后的文件存放的地方
        filename: "bundle.js" //打包后输出文件的文件名
    },
    devServer: {
        contentBase: "./dist", // 本地服务器所加载文件的目录
        port: "8088",   // 设置端口号为8088
        inline: true, // 文件修改后实时刷新
        historyApiFallback: true, //不跳转
    }
}

package.json中的scripts对象中添加如下命令,用以开启本地服务器:

"dev": "webpack-dev-server --open"

在终端中输入npm run dev浏览器自动打开了http://localhost:8080/

devserver作为webpack配置选项中的一项,以下是它的一些配置选项,更多配置可参考这里

3.2 Source Maps调试配置

作为开发,代码调试当然少不了,那么问题来了,经过打包后的文件,你是不容易找到出错的地方的,Source Map就是用来解决这个问题的。

通过如下配置,我们会在打包时生成对应于打包文件的.map文件,使得编译后的代码可读性更高,更易于调试。

在webpack的配置文件中配置source maps,需要配置devtool,关于source maps详细配置参考这里

其中一些值适用于开发环境,一些适用于生产环境。对于开发环境,通常希望更快速的 source map,需要添加到 bundle 中以增加体积为代价,但是对于生产环境,则希望更精准的 source map,需要从 bundle 中分离并独立存在。

对小到中型的项目中,eval-source-map是一个很好的选项,再次强调只应该开发阶段使用它。

4、loaders

loaders是webpack最强大的功能之一,通过不同的loader,webpack有能力调用外部的脚本或工具,实现对不同格式的文件的处理,例如把scss转为css,将ES66、ES7等语法转化为当前浏览器能识别的语法,将JSX转化为js等多项功能。

Loaders需要单独安装并且需要在webpack.config.js中的modules配置项下进行配置,Loaders的配置包括以下几方面:

  • test:一个用以匹配loaders所处理文件的拓展名的正则表达式(必须)
  • loader:loader的名称(必须)
  • include/exclude:手动添加必须处理的文件(文件夹)或屏蔽不需要处理的文件(文件夹)(可选);
  • options:为loaders提供额外的设置选项(可选)

4.1 配置css-loader

如果我们要加载一个css文件,需要安装配置style-loadercss-loader:

npm i style-loader css-loader -D
// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口文件
    output: {
        path: path.join( __dirname, "/dist"), //打包后的文件存放的地方
        filename: "bundle.js" //打包后输出文件的文件名
    },
    devServer: {
        contentBase: "./dist", // 本地服务器所加载文件的目录
        port: "8088",  // 设置端口号为8088
        inline: true, // 文件修改后实时刷新
        historyApiFallback: true, //不跳转
    },
    devtool: 'source-map',  // 会生成对于调试的完整的.map文件,但同时也会减慢打包速度
    module: {
        rules: [
            {
                test: /\.css$/,   // 正则匹配以.css结尾的文件
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是这个顺序,因为调用loader是从右往左编译的
            }
        ]
    }
}

我们在src文件夹下新建css文件夹,该文件夹内新建style.css文件:

/* style.css */
body {
    background: gray;
}

index.js中引用它:

//index.js 
import './css/style.css';  //导入css

const hello = require('./hello.js');
document.querySelector("#root").appendChild(hello());

这时我们运行npm run dev,会发现页面背景变成了灰色。

4.2 配置sass

npm i sass-loader node-sass -D // 因为sass-loader依赖于node-sass,所以还要安装node-sass

增加sass的rules:

// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口文件
    output: {
        path: path.join( __dirname, "/dist"), //打包后的文件存放的地方
        filename: "bundle.js" //打包后输出文件的文件名
    },
    devServer: {
        contentBase: "./dist", // 本地服务器所加载文件的目录
        port: "8088",  // 设置端口号为8088
        inline: true, // 文件修改后实时刷新
        historyApiFallback: true, //不跳转
    },
    devtool: 'source-map',  // 会生成对于调试的完整的.map文件,但同时也会减慢打包速度
    module: {
        rules: [
            {
                test: /\.css$/,   // 正则匹配以.css结尾的文件
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是这个顺序,因为调用loader是从右往左编译的
            },
            {
                test: /\.(scss|sass)$/,   // 正则匹配以.scss和.sass结尾的文件
                use: ['style-loader', 'css-loader', 'sass-loader']  // 需要用的loader,一定是这个顺序,因为调用loader是从右往左编译的
            }
        ]
    }
}

在css文件夹中新建blue.scss文件:

/* blue.scss */
$blue: blue;
body{
    color: $blue;
} 

在index.js中引入blue.scss

//index.js 
import './css/style.css';   // 导入css
import './css/blue.scss';   // 导入scss

const hello = require('./hello.js');
document.querySelector("#root").appendChild(hello());

这时npm run dev重新启动服务器,可以看到文字变成了蓝色。

除此,还有图片、字体等loader,更多配置参考这里

5、Babel

Babel其实是一个编译JavaScript的平台,它的强大之处表现在可以通过编译帮你达到以下目的:

  • 使用下一代的JavaScript代码(ES6,ES7…),即使这些标准目前并未被当前的浏览器完全的支持;
  • 使用基于JavaScript进行了拓展的语言,比如React的JSX;

5.1 Babel的安装与配置

Babel其实是几个模块化的包,其核心功能位于称为babel-core的npm包中,webpack可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析ES6的babel-preset-env包和解析JSX的babel-preset-react包)。

npm i babel-loader @babel/core @babel/preset-env @babel/preset-react -D

注意这里的版本:
webpack 4.x | babel-loader 8.x | babel 7.x

npm i babel-loader @babel/core @babel/preset-env @babel/preset-react -D

webpack 4.x | babel-loader 7.x | babel 6.x

npm i babel-loader@7 babel-core babel-preset-env -D

我们采用的是最新的版本安装版本,即安装的最新版本的babel

// webpack.config.js
const path = require('path');
module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口文件
    output: {
        path: path.join( __dirname, "/dist"), //打包后的文件存放的地方
        filename: "bundle.js" //打包后输出文件的文件名
    },
    devServer: {
        contentBase: "./dist", // 本地服务器所加载文件的目录
        port: "8088",  // 设置端口号为8088
        inline: true, // 文件修改后实时刷新
        historyApiFallback: true, //不跳转
    },
    devtool: 'source-map',  // 会生成对于调试的完整的.map文件,但同时也会减慢打包速度
    module: {
        rules: [
            {
                test: /\.css$/,   // 正则匹配以.css结尾的文件
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是这个顺序,因为调用loader是从右往左编译的
            },
            {
                test: /\.(scss|sass)$/,   // 正则匹配以.scss和.sass结尾的文件
                use: ['style-loader', 'css-loader', 'sass-loader']  // 需要用的loader,一定是这个顺序,因为调用loader是从右往左编译的
            },
            {                             // jsx配置
                test: /(\.jsx|\.js)$/,   
                use: {                    // 注意use选择如果有多项配置,可写成这种对象形式
                    loader: "babel-loader",
                    options: {
                        presets: [
                           "@babel/preset-env", "@babel/preset-react"
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
}

现在我们已经支持ES6及JSX的语法了,我们用react来试试。
安装reactreact-dom.

npm i react react-dom -S

接下来,修改hello.js文件:

// hello.js
import React, {Component} from 'react' // 这两个模块必须引入

let name = lishuaishuai

export default class Hello extends Component{
    render() {
        return (
            <div>
                {name}
            </div>
        );
    }
}

修改index.js文件:

//index.js 
import './css/style.css';  // 导入css
import './css/blue.scss';  // 导入scss

import React from 'react';
import {render} from 'react-dom';
import Hello from './hello'; // 可省略.js后缀名

render(<Hello />, document.getElementById('root'));

运行npm run dev即可启动项目。

5.2 优化babel配置

Babel其实可以完全在webpack.config.js 中进行配置,但是考虑到babel具有非常多的配置选项,在单一的webpack.config.js文件中进行配置往往使得这个文件显得太复杂,因此一些开发者支持把babel的配置选项放在一个单独的名为 .babelrc 的配置文件中。webpack会自动调用.babelrc里的babel配置选项。

提取webpack.config.js文件中的babel配置项,在根目录下新建文件.babelrc

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

现在babel的基本配置完成。

6、插件(Plugins)

插件(Plugins)是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。
Loaders和Plugins常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程其作用。

6.1 插件如何使用

使用某个插件,需要通过npm进行安装,然后在webpack.config.js配置文件的plugins(是一个数组)配置项中添加该插件的实例,下面我们先来使用一个简单的版权声明插件。

// webpack.config.js
const webpack = require('webpack');  // 这个插件不需要安装,是基于webpack的,需要引入webpack模块

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,   // 正则匹配以.css结尾的文件
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是这个顺序,因为调用loader是从右往左编译的
            },
            {
                test: /\.(scss|sass)$/,   // 正则匹配以.scss和.sass结尾的文件
                use: ['style-loader', 'css-loader', 'sass-loader']  // 需要用的loader,一定是这个顺序,因为调用loader是从右往左编译的
            },
            {                             // jsx配置
                test: /(\.jsx|\.js)$/,   
                use: {                    // 注意use选择如果有多项配置,可写成这种对象形式
                    loader: "babel-loader"
                },
                exclude: /node_modules/   // 排除匹配node_modules模块
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究')  // new一个插件的实例 
    ]
}

运行npm run build打包后我们看到bundle.js文件显示如下:

6.2 自动生成html文件(HtmlWebpackPlugin)

到目前为止我们都是使用一开始建好的index.html文件,而且也是手动引入bundle.js,要是以后我们引入不止一个js文件,而且更改js文件名的话,也得手动更改index.html中的js文件名,所以能不能自动生成index.html且自动引用打包后的js呢?HtmlWebpackPlugin插件就是用来解决这个问题的:

首先安装插件:

npm i html-webpack-plugin -D

然后我们对项目结构进行一些更改:

  • dist整个文件夹删除;
  • src文件夹下新建一个index.template.html(名称自定义)文件模板(当然这个是可选的,因为就算不设置模板,HtmlWebpackPlugin插件也会生成默认html文件,这里我们设置模块会让我们的开发更加灵活),如下:
<!-- index.template.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Here is Template</title>
  </head>
  <body>
    <div id='root'>
    </div>
  </body>
</html>

webpack.config.js中我们引入了HtmlWebpackPlugin插件,并配置了引用了我们设置的模板,如下:

// webpack.config.js

module.exports = {
    ... ...
    plugins: [
        ... ... 
        new HtmlWebpackPlugin({
            template: path.join(__dirname, "/src/index.template.html")// new一个这个插件的实例,并传入相关的参数
        })
    ]
}

然后我们使用npm run build进行打包,你会发现,dist文件夹和html文件都会自动生成。

为什么会自动生成dist文件夹呢?因为我们在output出口配置项中定义了出口文件所在的位置为dist文件夹,且出口文件名为bundle.js,所以HtmlWebpackPlugin会自动帮你在index.html中引用名为bundle.js文件,如果你在webpack.config.js文件中更改了出口文件名,index.html中也会自动更改该文件名,这样修改起来很方便。

6.3 清理dist文件夹(CleanWebpackPlugin)

你可能已经注意到,在我们删掉/dist文件夹之前,由于前面的代码示例遗留,导致我们的/dist文件夹比较杂乱。

通常,在每次构建前清理/dist文件夹,是比较推荐的做法,因此只会生成用到的文件,这时候就用到CleanWebpackPlugin插件了。

npm i clean-webpack-plugin -D
// webpack.config.js
...
const CleanWebpackPlugin = require('clean-webpack-plugin'); // 引入CleanWebpackPlugin插件

module.exports = {
    ...
    plugins: [
        ... ...
        new CleanWebpackPlugin(['dist']),  // 所要清理的文件夹名称
    ]
}

插件的使用方法都是一样的,首先引入,然后new一个实例,实例可传入参数。

现在我们运行npm run build后就会发现,webpack会先将/dist文件夹删除,然后再生产新的/dist文件夹。

6.4 热替换(HotModuleReplacementPlugin)

热替换模块(Hot Module Replacement),也被称为 HMR。在更改代码后,会时时更新修改的部分。

用法:

  • devServer配置项中添加hot: true参数。
  • 因为HotModuleReplacementPlugin是webpack模块自带的,所以引入webpack后,在plugins配置项中直接使用即可。
// webpack.config.js
...
const webpack = require('webpack');  // 这个插件不需要安装,是基于webpack的,需要引入webpack模块

module.exports = {
    ...
    devServer: {
        ... ...
        hot: true // 热更新
    },
    ...
    plugins: [
        ... ...
        new webpack.HotModuleReplacementPlugin() // 热更新插件 
    ]
}

此时我们重新启动项目npm run dev后,修改hello.js的内容,会发现浏览器预览效果会自动刷新(也许反应会比较慢,因为我们使用了source-map和其他配置的影响,后面代码分离的时候我们再处理)。

7、项目优化及拓展

7.1 代码分离

在前面,只配置了webpack.config.js配置文件,但在打包和开发环境需要区分不同的配置文件。
1. 在根目录下创建build文件夹,在build文件夹下创建webpack.base.jswebpack.dev.jswebpack.prod.js分别代表公共配置文件、开发环境配置文件、生产环境配置文件。

  1. 安装一个合并模块插件:
npm i webpack-merge -D
  1. webpack.config.js的代码拆分到上述新建的三个文件中,然后把webpack.config.js文件删除,具体如下:
// webpack.base.js
const path = require('path');  // 路径处理模块
const webpack = require('webpack');  // 这个插件不需要安装,是基于webpack的,需要引入webpack模块
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 引入HtmlWebpackPlugin插件

module.exports = {
    entry: path.join(__dirname, "/src/index.js"), // 入口文件
    output: {
        path: path.join( __dirname, "/dist"), //打包后的文件存放的地方
        filename: "bundle.js" //打包后输出文件的文件名
    },
    module: {
        rules: [
            {
                test: /\.css$/,   // 正则匹配以.css结尾的文件
                use: ['style-loader', 'css-loader']  // 需要用的loader,一定是这个顺序,因为调用loader是从右往左编译的
            },
            {
                test: /\.(scss|sass)$/,   // 正则匹配以.scss和.sass结尾的文件
                use: ['style-loader', 'css-loader', 'sass-loader']  // 需要用的loader,一定是这个顺序,因为调用loader是从右往左编译的
            },
            {                             // jsx配置
                test: /(\.jsx|\.js)$/,   
                use: {                    // 注意use选择如果有多项配置,可写成这种对象形式
                    loader: "babel-loader"
                },
                exclude: /node_modules/   // 排除匹配node_modules模块
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),  // new一个插件的实例 
        new HtmlWebpackPlugin({
            template: path.join(__dirname, "/src/index.template.html")// new一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin()
    ]
}
// webpack.dev.js
const merge = require('webpack-merge');  // 引入webpack-merge功能模块
const base = require('./webpack.base.js'); // 引入webpack.common.js

module.exports = merge(base, {   // 将webpack.common.js合并到当前文件
    devServer: {
        contentBase: "./dist",   // 本地服务器所加载文件的目录
        port: "8088",  // 设置端口号为8088
        inline: true,  // 文件修改后实时刷新
        historyApiFallback: true, //不跳转
        hot: true     //热加载
    },
    devtool: 'source-map'
})
// webpack.prod.js
const merge = require('webpack-merge');
const base = require('./webpack.base.js');
const CleanWebpackPlugin = require('clean-webpack-plugin'); // 引入CleanWebpackPlugin插件

module.exports = merge(base, { // 将webpack.common.js合并到当前文件
    plugins: [
        new CleanWebpackPlugin(['dist']),  // 所要清理的文件夹名称
    ]
})
  1. 设置package.json的scripts命令:
{
  ... ...
  "scripts": {
    "build": "webpack --config build/webpack.prod.js",
    "dev": "webpack-dev-server --open --config build/webpack.dev.js"
  },
  ... ...
}

7.2 多入口多出口

到目前为止我们都是一个入口文件和一个出口文件,在很多情况下,我们需要多入口和多出口。
webpack.common.js中的entry入口有三种写法,分别为字符串、数组和对象。首先我们在src文件夹下新建two.js文件在entryoutput配置如下:

// webpack.base.js
...
module.exports = {
    entry: {
        index: path.join(__dirname, "/src/index.js"),
        two: path.join(__dirname, "/src/two.js")
    }, 
    output: {
        path: path.join( __dirname, "/dist"), //打包后的文件存放的地方
        filename: "[name].js" //打包后输出文件的文件名
    },
    ...
}
// two.js
function two() {
    let element = document.createElement('div');
    element.innerHTML = '我是第二个入口文件';
    return element;
}

document.getElementById('root').appendChild(two());

然后我们运行npm run build打包后发现/dist文件夹下会多出two.js文件,同时index.html也会自动将two.js引入。

7.3 增加css前缀、分离css、消除冗余css、分离图片

7.3.1 增加css前缀

平时我们写css时,一些属性需要手动加上前缀,比如-webkit-border-radius: 10px;,但是大量的css工作量很大,所以我们配置webpack来自动添加前缀:

npm i postcss-loader autoprefixer -D

安装好这两个模块后,在项目根目录下新建postcss.config.js文件:

// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')  // 引用autoprefixer模块
    ]
}

webpack.base.js文件中的增加postcss-loader配置:

...
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,   // 正则匹配以.css结尾的文件
                use: [            
                    {loader: 'style-loader'}, // 这里采用的是对象配置loader的写法
                    {loader: 'css-loader'},
                    {loader: 'postcss-loader'} // 使用postcss-loader
                ]  
            },
            ...
        ]
    },
    ...
}

同样也可对scss文件配置postcss-loader

7.3.2 分离css
npm i extract-text-webpack-plugin@next -D  // 加上@next是为了安装最新的,否则会出错

安装完以上插件后在webpack.base.js文件中引入并使用该插件:

// webpack.common.js
...
const ExtractTextPlugin = require('extract-text-webpack-plugin') //引入分离插件

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,   // 正则匹配以.css结尾的文件
                use: ExtractTextPlugin.extract({  // 这里我们需要调用分离插件内的extract方法
                    fallback: 'style-loader',  // 相当于回滚,经postcss-loader和css-loader处理过的css最终再经过style-loader处理
                    use: ['css-loader', 'postcss-loader']
                })
            },
            ...
        ]
    },
    plugins: [
        ...
        new ExtractTextPlugin('css/index.css') // 将css分离到/dist文件夹下的css文件夹中的index.css
    ]
}

运行npm run build后会发现/dist文件夹内多出了/css文件夹及index.css文件。

上面的extract-text-webpack-plugin虽然可以使用,但是现在webpack4.x版本分离css方法推荐采用mini-css-extract-plugin插件,具体使用方式可参考这里

7.3.3 消除冗余css

有时候我们css写得多了,可能会不自觉的写重复了一些样式,这就造成了多余的代码,上线前又忘了检查,对于这方面,我们应该尽量去优化它,webpack就有这个功能。

npm i purifycss-webpack purify-css glob -D

安装完上述三个模块后,在webpack.prod.js文件中进行配置,引入purifycss-webpackglob插件并使用它们。

// webpack.prod.js
... ...
const PurifyCssWebpack = require('purifycss-webpack'); // 引入PurifyCssWebpack插件
const glob = require('glob');  // 引入glob模块,用于扫描全部html文件中所引用的css

module.exports = merge(base, {   // 将webpack.common.js合并到当前文件
    plugins: [
        new PurifyCssWebpack({
            paths: glob.sync(path.join(__dirname, 'src/*.html')) // 同步扫描所有html文件中所引用的css
        })
    ]
})

这样,我们在css文件里如果存在了多余的css样式,就会被清理掉。

7.3.4 处理图片

处理css中的图片资源时,我们常用的两种loader是file-loader或者url-loader,两者的主要差异在于。url-loader可以设置图片大小限制,当图片超过限制时,其表现行为等同于file-loader,而当图片不超过限制时,则会将图片以base64的形式打包进css文件,以减少请求次数。

npm i url-loader file-loader -D 

然后在webpack.base.js中配置url-loader

// webpack.common.js
...
module.exports = {
    ...
    module: {
        rules: [
            ... ...
            {
                test: /\.(png|jpg|svg|gif)$/,  // 正则匹配图片格式名
                use: [
                    {
                        loader: 'url-loader'  // 使用url-loader
                    }
                ]
            },
            ...
        ]
    },
    ...
}

同时,我们也可以配置options中的limit,来决定图片多大的时候转化为base64。

7.3.5 压缩代码

在webpack4.x版本中当你打包时会自动把js压缩了,--mode production表示打包时是生产环境,会自己将js进行压缩,而--mode development表示当前是开发环境,不需要进行压缩。

{
  ...
  "scripts": {
    "build": "webpack --config webpack.prod.js --mode production",
    "dev": "webpack-dev-server --config webpack.dev.js --mode development"
  },
  ...
  }
}

四、其它

1. webpack中hash、chunkhash、contenthash区别

hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从源服务器上拉取对应数据,进而更新本地缓存。但是在实际使用的时候,这几种hash计算还是有一定区别。

1.1 hash

hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值

1.2 chunkhash

chunkhash和hash不一样,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。我们在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。

1.3 contenthash

chunkhash方式,由于index.css被index.js引用了,所以共用相同的chunkhash值。但是这样子有个问题,如果index.js更改了代码,css文件就算内容没有任何改变,由于是该模块发生了改变,导致css文件会重复构建。

在上面提到的extract-text-webpack-plugin或者mini-css-extract-plugincontenthash,顾名思义,contenthash代表的是文本文件内容的hash值。保证即使css文件所处的模块里就算其他文件内容改变,只要css文件内容不变,那么不会重复构建。

五、总结

这篇文章很长,但是对于学习webpack和react的webpack环境配置很详细。
关于react的webpack环境配置,会在github上有详细配置,后续更新。

webpack 3.x GitHub

webpack 4.x GitHub

React CLI GitHub

赞(7) 打赏
未经允许不得转载:李帅帅空间 » 基于 webpack4.x的react脚手架搭建

评论 1

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #1

    之前一直用3.x来着,对4.x的配置使用是一头雾水,很详细,很到位的文章,先mark了!

    aiyayao4个月前 (11-07)回复

React CLI - 快速创建react项目脚手架工具

官 网GitHub

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏