vortesnail / blog

:blue_book: 个人技术小文章,旨在对知识的总结,能帮助到别人就更好啦。
551 stars 45 forks source link

从零配置webpack 4+react脚手架(二) #5

Open vortesnail opened 5 years ago

vortesnail commented 5 years ago

前言:

你可能也注意到了,html文件中的关于js的引用是我们手动写的,那假如我们改了输出路径或打包编译之后的文件名,那我们岂不是还要手动去修改html文件中的引用?我们怎么做到,像create-react-app中那样一旦你修改了某个文件内容,页面会自己刷新?我们来一步一步实现它们,当然,这一小节不仅仅只是为了完成这两点。

自动编译html并引入js文件

public的index.html应该自动编译到dist目录,并且所有的js引用是自动添加的。你可以使用html-webpack-plugin插件来处理这个优化。

安装HtmlWebpackPlugin

在控制台执行以下代码:

npm install --save-dev html-webpack-plugin

在webpack.prod.config.js中配置plugins属性

const merge = require('webpack-merge');
const common = require('./webpack.common.config.js');

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      // 这里有小伙伴可能会疑惑为什么不是 '../public/index.html'
      // 我的理解是无论与要用的template是不是在一个目录,都是从根路径开始查找
      template: 'public/index.html',
      inject: 'body',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
      },
    })
  ]
});

更多配置请点击官方README

删除index.html中手动引入的script标签

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>从零配置webpack4+react脚手架</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

现在我们再来打包试试,看看dist中是不是多出了html文件,并且自动引入了script,用浏览器打开它试试看是不是能正确输出内容了!

给打包出的js文件换个不确定名字

这个操作是为了防止因为浏览器缓存带来的业务代码更新,而页面却没变化的问题,你想想看,假如客户端请求js文件的时候发现名字是一样的,那么它很有可能不发新的数据包,而直接用之前缓存的文件,当然,这和缓存策略有关。

那我们怎么给导出文件的安排一个不确定的名字呢?很简单,[hash]或[chunkhash]
修改webpck.prod.config.js

const merge = require('webpack-merge');
const common = require('./webpack.common.config.js');

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = merge(common, {
  mode: 'production',
  output: {
    filename: 'js/[name].[chunkhash:8].bundle.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'public/index.html',
      inject: 'body',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
      },
    })
  ]
});

其中,name就是模块名称,我们在entry中进行过配置,在这里重新设置会代替之前common中的设置,chunkhash是文件内容的hash,webpack默认采用md5的方式对文件进行hash。8是hash的长度,如果不设置,webpack会设置默认值为20。

现在你重新打包,去看看生成的js文件的名字~

打包编译前清理dist目录

在上面的修改后,因为js文件名字不同,你之后再打包,会把之前打包之后的js文件也留下,我们只想要最新打包编译的文件,就需要先清除dist目录,再重新生成。

安装clean-webpack-plugin

npm install --save-dev clean-webpack-plugin

这个插件不被官方文档所收录,可以去github查看它的配置文档

使用clean-webpack-plugin

修改webpck.prod.config.js

const merge = require('webpack-merge');
const common = require('./webpack.common.config.js');

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = merge(common, {
  mode: 'production',
  output: {
    filename: 'js/[name].[chunkhash:8].bundle.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'public/index.html',
      inject: 'body',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
      },
    }),
    new CleanWebpackPlugin()
  ]
});

这里需要注意:之前引入CleanWebpackPlugin的写法是
const CleanWebpackPlugin = require('clean-webpack-plugin'); 
而且在下面new的时候需要传入参数,dist文件路径。但是现在必须这样引入:
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
而且,不用再写路径参数

现在再来执行看看,是不是只有一个js文件了!~

代码分割

我们先看下,我们之前打包编译的时候,控制台的信息:
image.png
我们看到,这个打包之后的bundle.js文件大小为129kb,随着业务代码越来越多,这个包会变得越来越大,你每次修改了代码并发布,用户都需要重新下载这个包,但是想想看,我们修改的代码只是整个代码的一小部分,还有许多其他不变的代码,例如 react 和 react-dom ,那我们把这部分不变的代码单独打包。

修改 webpack.common.config.js ,增加一个入口:

  entry: {
    index: './src/index.js',
    framework: ['react','react-dom'],
  },

重新打包,发现react和react-dom 被编译成framework.js,但是我们的index.bundle.js还是129kb,没有变过。
这是因为我们还没有抽离index.js中的公共代码。

webpack3版本是通过配置CommonsChunkPlugin插件来抽离公共的模块。webpack4版本,官方废弃了CommonsChunkPlugin,而是改用配置optimization.splitChunks的方式,更加方便。

添加代码至 webpack.prod.config.js :

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      cacheGroups: {
        framework: {
          test: "framework",
          name: "framework",
          enforce: true
        },
        vendors: {
          priority: -10,
          test: /node_modules/,
          name: "vendor",
          enforce: true,
        },
      }
    }
  },
  //...
};

cacheGroups对象,定义了需要被抽离的模块,其中test属性是比较关键的一个值,他可以是一个字符串,也可以是正则表达式,还可以是函数。如果定义的是字符串,会匹配入口模块名称,会从其他模块中把包含这个模块的抽离出来。name是抽离后生成的名字,和入口文件模块名称相同,这样抽离出来的新生成的framework模块会覆盖被抽离的framework模块,虽然他们都叫framework。
vendors这个缓存组,它的test设置为 /node_modules/ 表示只筛选从node_modules文件夹下引入的模块,所以所有第三方模块才会被拆分出来。

重新打包,我们发现index.bundle.js文件大小只有:1.69kb
image.png
我们随意修改一下app.js中的内容,比如

import React from 'react';

function App() {
  return (
    <div className="App">
      <h1>I am changed</h1>
    </div>
  );
}

export default App;

再打包一次,你会发现index.bundle.js(不被缓存)的hash值变了,但是freamework.bundle.js(能被缓存)的hash值没变,成了成了!!

压缩JS文件

我们需要把打包生成的js文件尽可能压缩,以便减少文件体积,更快地被用户加载。
我们需要一个插件: uglifyjs-webpack-plugin 来做这份工作

安装uglifyjs-webpack-plugin

在控制台执行以下代码:

npm install uglifyjs-webpack-plugin --save-dev

引入uglifyjs-webpack-plugin

增加如下代码至 webpack.prod.config.js :

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

optimization内配置minimizer参数

minimizer: [
  new UglifyJsPlugin(),
    //...
],

现在optimization参数应该是现在这样:

  optimization: {
    minimizer: [new UglifyJsPlugin()],
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      cacheGroups: {
        framework: {
          priority: 100,
          test: "framework",
          name: "framework",
          enforce: true
        },
        vendors: {
          priority: -10,
          test: /node_modules/,
          name: "vendor",
          enforce: true,
        },
      }
    }
  },

重新打包编译看看~我们的index.bundle.js减少了0.1kb,当然,随着业务代码越来越多,这部分差距会渐渐变大。

自动编译打包

我们每次修改代码,查看结果都要经历以此 npm run build ,大大降低了开发效率,这难以忍受!
webpack给我们提供了devServer开发环境,支持热更新,相当舒服。

安装webpack-dev-server

在控制台执行以下代码:

npm install webpack-dev-server --save-dev

增加代码至 webpack.dev.config.js :

是不是都快忘记这个之前创建的配置文件了?没关系,反正也没代码,它是专门用来配置我们开发环境的

const path = require('path');
const merge = require('webpack-merge');
const common = require('./webpack.common.config.js');

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = merge(common, {
  mode: 'development',
  output: {
    filename: 'js/[name].[hash:8].bundle.js',
  },
  devServer: {
    contentBase: path.resolve(__dirname, '../dist'),
    open: true,
    port: 9000,
    compress: true,
    hot: true
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      inject: 'body',
      hash: false
    }),
    new webpack.HotModuleReplacementPlugin()
  ]
});

HotModuleReplacementPlugin是webpack热更新的插件,设置devServer.hot为true,并且在plugins中引入HotModuleReplacementPlugin插件即可。
还需要注意的是我们开启了hot,那么导出不能使用chunkhash,需要替换为hash。

修改我们的package.json

像之前build的时候,我们是通过配置package.json做到的,现在我们同样加入以下代码来模拟:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config ./config/webpack.prod.config.js",
+   "start": "webpack-dev-server --inline --config ./config/webpack.dev.config.js"
  },

接下来,在控制台执行

npm run start

是不是自动开了一个端口为9000的网页,上面是我们写的页面内容,这和我们的配置都是一一对应的。
现在你随意修改app.js中的代码,再回到页面看下是不是也跟着变了,那我们就整合webpack-dev-server成功!

下面一小节我们会配置css相关的属性,加油!

funny741643 commented 4 years ago

star是你的了

vortesnail commented 4 years ago

star是你的了

哈哈,谢谢,这个系列有点太基础了,之后会写一篇究极长文,欢迎关注。

fsa2000 commented 4 years ago

谢谢!!就是需要基础的,从入门到放弃:)

Yzmx1995 commented 4 years ago

补充两点: 1.mode设置成production后会自动压缩文件,不用引入额外插件 2.热更新需要在入口文件下方添加if (module.hot) { module.hot.accept(() => {}); },否则不生效,修改内容还是刷新页面

vortesnail commented 4 years ago

补充两点: 1.mode设置成production后会自动压缩文件,不用引入额外插件 2.热更新需要在入口文件下方添加if (module.hot) { module.hot.accept(() => {}); },否则不生效,修改内容还是刷新页面

是的,你说的没错,但是针对第一个问题,有时候你需要额外的自定义设置,还是需要引入额外插件; 这篇文章已经有点老了,有的内容的确写的有问题,建议看最新的文章:我是这样搭建Typescript+React项目环境的!(2.7w字详解)

Yzmx1995 commented 4 years ago

补充两点: 1.mode设置成production后会自动压缩文件,不用引入额外插件 2.热更新需要在入口文件下方添加if (module.hot) { module.hot.accept(() => {}); },否则不生效,修改内容还是刷新页面

是的,你说的没错,但是针对第一个问题,有时候你需要额外的自定义设置,还是需要引入额外插件; 这篇文章已经有点老了,有的内容的确写的有问题,建议看最新的文章:我是这样搭建Typescript+React项目环境的!(2.7w字详解)

👌