wepack搭建react项目(一)

1,674 阅读11分钟

目标:使用webpack从零开始配置一个react项目

概念

借用官网的解释:本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

要理解webpack是什么,要从两个词出发:“模块”和“打包”。

为什么要打包?

以一个html页面为例,在页面中通过script标签引入了3个JavaScript文件a.js,b.js和c.js,每个文件中分别定义了一个函数并导出给外部用。并且它们之间有一定的依赖关系,c.js依赖于b.js,b.js依赖于a.js。

因为有3个独立的js文件,所以在加载的时候浏览器需要发送三次http请求来获取这三个文件,然后依次执行其中的代码,如果其中有一个文件因为网络问题而延误了时间,那么整个页面的显示也会被延误。当我们的项目逐渐变大,有几十个到上百个JavaScript文件的时候,那问题会更严重,不但有延迟问题,还会遇到很难维护的问题。所以需要尽可能的合并文件,减少http请求,这个合并的过程就是打包,通常将几个分散的有依赖关系的文件打包为一个文件。

通常情况下,为了提高开发效率,我们会使用诸如ES6,less等,就需要在运行时做代码的转换,使得其可以在对应的终端上正常执行,这个过程如果每次都手动通过工具做转换的话非常的费时,理想的情况是,我们可以使用这些具有新特性的东西,又可以通过某种工具自动的完成转换,进而提升开发效率,这个自动转换的过程也是打包的过程。

什么是模块

模块可以理解为一个单独的文件,或者一个方法,每一个模块都是一个单独的作用域, 也就是说, 在该模块内部定义的变量, 无法被其他模块读取, 除非定义为global(浏览器中为window)对象的属性。模块可以被复用,模块之间可以被相互引用,比如一个处理浮点数加法的方法,就是一个模块,可能在多个地方使用,使用的地方直接导入这个方法即可。

webpack的核心功能就是打包,下面我们会针对不同的文件类型做处理,充分发挥webpack的打包模块的作用。

配置

以下内容将从以下几个方面展开:

  1. 初始化webpack配置
  2. 配置css,使用预处理器
  3. 配置html,自动引入打包后的文件
  4. 使用 Babel 来支持 ES 新特性
  5. 处理图片
  6. 本地搭建服务器

初始化项目

mkdir react-fe
cd react-fe
yarn init // 初始化项目,生成package.json文件
yarn add webpack webpack-cli -D  // 安装webpack

如果对于生成的package.json中有不理解的,可以查看这篇文章package.json 知多少? package.json中添加脚本配置:

"scripts": {
    "build": "webpack --mode production"
},

在项目目录下新建src文件,用于存放源文件,新建/src/index.js,内容任意

console.log('hello world');

执行yarn build,会新增一个dist目录,里面是打包后的文件

!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){console.log("hello world")}]);

webpack 运行时默认读取项目下的 webpack.config.js 文件作为配置;这个配置其实是一个node js的脚本,脚本对外暴露一个配置对象,webpack通过这个对象来读取相关的一些配置,因为是node js的脚本,所以可以使用任何node模块。通常一个项目会分为开发模式和生产模式,所以我们创建一个目录build,此目录下的文件均和构建相关。如下:

|- build (打包配置目录)
    |- webpack.config.js -- webpack打包基础配置文件
    |- webpack.dev.config.js -- webpack 开发环境打包配置文件
    |- webpack.prod.config.js -- webpack 生产环境 build打包配置文件

webpack.config.js配置如下:

const path = require('path');

function resolve(dir) {
    return path.join(__dirname, '..', dir)
}

function src(dir) {
    return resolve(path.join('src', dir))
}

module.exports = {
    entry: {
        main: src('index.js'), // 入口文件
    },
    output: {
        filename: '[name].js', // 输出文件
    },
    module: {
        rules: []
    },
    plugins: []
}

配置webpack的mode, 枚举值有production,development, none,在webpack4中配置了mode,相当于使用DefinePlugin设置了NODE_ENV,这个值用于区分生产模式还是开发模式,后续会告诉webpack使用哪种模式开启内置优化,mode值的不同,build时默认的配置也会不同,有了默认配置,就不需要手动启用插件了。

选项 描述
development 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin。
production 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin.

配置webpack.dev.config.js

var base = require('./webpack.config');

base.mode = "development";
base.devtool = 'cheap-module-eval-source-map';

module.exports = base;

配置webpack.prod.config.js

var base = require('./webpack.config');

base.mode = "production";
base.devtool = 'hidden-source-map';

module.exports = base;

添加npm scripts

"scripts": {
    "build": "webpack --config ./build/webpack.prod.config.js --progress --colors",
    "dev": "webpack --config ./build/webpack.dev.config.js --progress --colors"
},

配置环境变量

cross-env是一个运行跨平台设置和使用环境变量的脚本,比如我们想要分析打包后的各个文件大小以及依赖关系,可以添加"webpack-bundle-analyzer",这个包不是在每一次打包的时候都用到,所以,单独添加一个条件,只有在需要分析的时候才使用这个包

yarn add cross-env webpack-bundle-analyzer -D
// 添加npm scripts
"scripts": {
    "analyze": "cross-env ANALYZE=1 npm run build",
},
// 修改webpack.prod.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
if (process.env.ANALYZE) {
    base.plugins.push(new BundleAnalyzerPlugin());
}

只设置 NODE_ENV,则不会自动设置 mode

配置css,使用预处理器

webpack 中提供一种处理多种文件格式的机制,便是使用 loader。我们可以把 loader 理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 可以支持打包的模块。 loader本身是一个导出为function的node模块; 在没有任何loader处理的情况下,webpack默认只能处理js文件,最终输出js文件,而loader的作用就是把非js文件转换为webpack可以处理的js文件 将内联图像转换为data URL

css-loader

负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,例如 @import 和 url() 等引用外部文件的声明;将样式代码处理为js数组,样式代码处理为字符串

yarn add css-loader -D

新建文件,目录如下:

|- src 
    |- index.html
    |- index.js
	|- index.css文件

// index.css

#app {
  background-color: #f5f5f5;
  color: blue;
}
#app p {
  color: gray;
}

// index.js
const a = require('./index.css');
console.log(a);

// index.html
<div id="app">
    <h4>hello webpack!</h4>
    <p>hello loader!</p>
</div>
<script src="../dist/main.js"></script>

// webpack.config.js,添加rules
{
    test: /\.css$/,
    use: 'css-loader'
}

执行 yarn build 可以看到打包后的main.js中,css-loader将样式代码处理成了js数组,并且我们的样式代码被处理成了字符串。

 function(n, t, o) {
    (t = o(2)(!1)).push([
      n.i,
      "#app {\n    background-color: #f5f5f5;\n    color: blue;\n  }\n  #app p {\n    color: gray;\n  }",
      ""
    ]),
      (n.exports = t);
  },

经过css-loader处理完的文件并没有应用到页面上,如果想要样式生效,还需要style-loader的处理

yarn add style-loader -D
// webpack.config.js,修改rules
 {
    test: /\.css$/,
    use: [ 'style-loader', 'css-loader' ]
}

style-loader将css-loader返回的样式数组一顿操作插入到html head,然后自己返回了一个空对象。

loader处理的时机:在import或”加载“时预处理文件,类似于其他构建工具中的task

loader处理顺序:从右向左执行,支持链式传递,链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 期望 JavaScript。

less-loader

通常情况下,我们会使用预处理器来编写css,比如使用less或者sass,这样可以大大提高开发效率,下面以less为例,将原来的css文件修改为less文件,并且内容修改如下:

// index.less
@theme-color: #ff8200;
@dark-gray: #707c93;
@light-gray: #b5c1d2;

#app {
    background-color: @light-gray;
    color: @theme-color;

    p {
        color: @dark-gray;
    }
}
// 安装less less-loader,less处理并识别less文件,less-loader可以将less文件处理为css文件
yarn add less less-loader -D
// webpack.config.js
    module: {
        rules: [
            {
                test: /\.less?$/,
				use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']	
			}
		]
	},
// index.js
const a = require('./index.less');

postcss-loader

另外常用的处理样式的方式是,使用postcss,它是一种对css编译的工具,类似babel对js的处理,常见的功能如:

  1. 使用下一代css语法
  2. 自动补全浏览器前缀: autoprefixer
  3. 自动把px代为转换成rem: postcss-pxtorem
  4. css 代码压缩等等

postcss 只是一个工具,本身不会对css一顿操作,它通过插件实现功能。

// 安装loader和plugin
yarn add postcss-loader postcss-pxtorem autoprefixer -D
// 项目根目录下创建postcss.config.js
module.exports = {
    plugins: {
        'autoprefixer': {},
        'postcss-pxtorem': {
            'rootValue': 108,
            'propList': ['*'],
            'minPixelValue': 2,
            'selectorBlackList': [],
        },
    },
};
// 修改Webpack配置
 module: {
        rules: [
            {
                test: /\.less?$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
            },
        ]
 }

开发中还有一个常用的功能是,比如对某个依赖的UI库做单位转换,也可以单独配置plugin,比如我们项目中设计稿的尺寸是1080,但是依赖的库是750的,并且是px为单位的,为了兼容我们的项目,就要对依赖库做单位转换。

 module: {
        rules: [
            {
                test: /\.css?$/,
                use: [MiniCssExtractPlugin.loader, {
                    loader: 'css-loader',
                    options: {
                        minimize: true
                    }
                }, {
                    loader: 'postcss-loader', 
                    options: {
                        plugins: [
                            require('postcss-pxtorem')({
                                rootValue: 37.5,
                                propWhiteList: ['*'],
                                'minPixelValue': 2,
                            })
                        ]
                    }
                }],
                include: /node_modules/ant-design/
            },
        ]
 }

通常一个成熟的项目,我们不会使用style-loader的方式将结果插入到head中,也不会自己手动去修改html中引入打包后文件的路径。下面将配合plugin使用

添加plugin,分离html,css,js

webpack的plugin比loader强大,通过钩子可以涉及整个构建流程,可以做一些在构建范围内的事情;理论上可以干涉 webpack 整个构建流程,可以在流程的每一个步骤中定制自己的构建需求。

plugin:构建流程中处理构建任务,可以这么理解,模块代码的转换工作由loader处理,除此之外的任何其他工作都可以由plugin完成。

常用的两个插件:

  1. html-webpack-plugin可以根据模板自动生成html代码,并自动引用css和js文件,这对生成的文件使用了hash的情况非常有用
  2. mini-css-extract-plugin 将js文件中引用的样式单独抽离成css文件,需要结合css-loader一起使用,另外,这个插件使用后就不需要style-loader
yarn add html-webpack-plugin mini-css-extract-plugin -D

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /\.css?$/,
				use: [MiniCssExtractPlugin.loader, 'css-loader']	
			}
		]
	},
	plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html', // 配置输出文件名
            template: src('index.html'),
        }),
        new MiniCssExtractPlugin({
            filename: "static/css/[name].css"
        }),
	]
};
// index.html中去掉script部分,此处会自动在html中添加引用

执行 yarn build的结果

|- dist
  |- index.html
  |- main.js
  |- static
	|- css
	  |- main.css
// index.html中会自动引入css和js
<link href="static/css/main.css" rel="stylesheet">
<body>
    <div id="app">
        <h4>hello webpack!</h4>
        <p>hello loader!</p>
    </div>
<script type="text/javascript" src="main.js"></script></body>

作为普通的html开发,你可能会需要从网上下载一些js文件而不是使用cdn的方式引入,比如用于适配移动端的flexiable,这种文件不需要经过babel再次进行处理,所以打包后直接在html中引入即可

yarn add copy-webpack-plugin -D
// webpack.config.js
const CopyPlugin = require('copy-webpack-plugin');
 plugins: [
        new CopyPlugin([
            { from: src('flexible.js'), to: resolve('dist') },
		]),
 ]

使用 Babel 来支持 ES 新特性

现在的项目中我们一般会使用ES6开发,所以需要用babel处理,关于babel的使用,可以参考babel学习

yarn add @babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader -D
yarn add @babel/runtime-corejs3
// 项目根目录下新建.babelrc文件
{
    "presets": [
      [
        "@babel/preset-env",
        {
          "modules": "false",
          "targets": {
            "browsers": [
              "> 1%",
              "last 2 versions",
              "ie >= 10",
              "iOS >= 8",
              "Android >= 4"
            ]
          }
        }
    ],
  ],
    "plugins": [
        [
            "@babel/plugin-transform-runtime", {
              "helpers": true,
              "corejs": 3,
              "regenerator": true
            }
        ]
    ],
    // "ignore": ["./src/three.min.js", "./src/panolens.js"]
  }
// webpack.config.js,添加对js文件的处理
 module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                loader: 'babel-loader',
                exclude: /node_modules/,
                include: resolve(''),
			},
		]
 }
//  index.js,修改此文件,测试babel是否生效
const a = require('./index.less');
const obj = {
    name: 'apple',
    sex:' female',
};

if (obj.hasOwnProperty('sex')) {
    const div = document.createElement('div');
    div.style.color = 'pink';
    const { name } = obj;
    div.innerText = name;
    document.getElementById('app').appendChild(div);
}
console.log(a);

处理图片,压缩文件

处理图片,常使用的两个loader是url-loader和file-loader,其中 url-loader 是将图片转换成一个 DataURL,然后打包到 JavaScript 代码中,这对小的图片来说是不错的处理方式,可是大图片这种处理方式就不适用了,无疑会增大js的体积,通过使用file-loader将文件处理后输出到目录中。

yarn add file-loader url-loader -D
// 新建存放图片目录
|- src 
    |- assets
	|- img  // 用于放所有的图片
// webpack.config.js
module: {
        rules: [
            {
                test: /\.(png|jpg|gif)$/i,
                use: [
                  {
                    loader: 'url-loader',
                    options: {
                      limit: 8192, // 如果超过8192 字节的就使用file-loader处理,并按照下面规则生成文件
                      name: 'static/images/[name].[hash:8].[ext]'
                    },
                  },
                ],
			},
		]
}
// 修改index.less
div {
        background: url(../src/assets/img/bg.jpg);
    }

到此为止,我们已经实现了如何使用webpack来搭建一个项目了,即使不适用诸如react, vue这样的框架,也是可以正常启动打包项目的。

webpack搭建本地服务器(express + webpack-dev-middleware)

开发过程中,我们希望边修改就能看到更新后的结果,所以本地开发启动服务热更新很重要,不然就得每次去build然后刷新看结果。 webpack为我们提供了实现本地热更新的插件:webpack-dev-server,使用和配置很简单,可参照官方文档进行配置。但是本篇我们不使用这个。通过使用express服务器,可以进行更多的扩展,结合使用其他的中间件来响应http请求及其他的功能,扩展性更好,较为灵活。 开启了 hot 功能的 webpack 会往我们应用的主要代码中添加 WS 相关的代码,用于和服务器保持连接,等待更新动作。

// webpack.dev.config.js
Object.keys(base.entry).forEach(function (name) {
    base.entry[name] = ['webpack-hot-middleware/client'].concat(base.entry[name]);
});

base.plugins.push(
    new webpack.HotModuleReplacementPlugin(), // 模块热更新
);
// 修改入口文件
if (module.hot) {
    module.hot.accept();
  }
// devServer.js
var webpack = require('webpack');
var express = require('express');
var path = require('path');
var config = require('./build/webpack.dev.config');

var app = express();
// Webpack developer
var compiler = webpack(config);
var devMiddleWare = require('webpack-dev-middleware')(compiler, {
      publicPath: config.output.publicPath,
      stats: {
          colors: true,
          modules: false,
          children: false,
          chunks: false,
          chunkModules: false
      }
});
app.use(devMiddleWare);
app.use(require('webpack-hot-middleware')(compiler));
var mfs = devMiddleWare.fileSystem;
var file = path.join(config.output.path, 'index.html');
app.get('/middle.html', (req, res) => {
    res.end();
})
app.get('*', function (req, res) {
    devMiddleWare.waitUntilValid(function () {
        var html = mfs.readFileSync(file);
        res.end(html);
    })
})

var port =  3005;

app.listen(port, function (err, result) {
    if (err) {
        console.log(err);
    }
    console.log('Server running on http://localhost:' + port);
});

按照上面的配置,可以实现,更新本地文件,浏览器自动刷新,但是有个问题,比如我们在页面输入了两个值,自动刷新后,刚刚输入的值就没有了,对复杂的页面操作来说,每次更新文件都需要重新进行一遍操作,是很影响效率的,造成这种现象的原因是热更新不能存储state的状态,使用react-hot-loader可以解决这个问题。

下一篇文章介绍react的引入。

参考文章

  1. Webpack Loader简析(一):基本概念
  2. package.json 知多少?
  3. npm install 原理分析
  4. 强化:构建易用易扩展的工作流