上一篇博客起了个头,介绍了为什么要用webpack,用webpack可以做什么,以及最简单地配置。这篇博客将结合实际需求,一步一步的引入一些配置,所有内容基于实际需求出发。

entry和output

上一篇博客说到,entry是webpack打包的入口文件,webpack会从这个入口文件开始,寻找该入口文件的依赖模块,以及递归的寻找依赖模块的依赖模块,直到将所有的依赖模块打包成单个js文件输出到output配置的路径。根据不同的使用场景,主要有一对一、多对一、多对多等不同情况。

默认值

如果没有设置entry,那么其默认值是./src/index.js,即webpack会自动寻找根目录下的src文件夹中的index.js当做入口文件

entry为字符串:一对一

通过字符串值可以直接指定入口文件的路径,比如上一篇博客中的例子:

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  }
}

这种情况就是将一个输入文件打包到一个输出文件。

entry为数组:多对一

有时为了业务逻辑更加清晰,我们的入口文件可能不止一个,这时可以将多个入口文件放在一个数组中,指定到entry:

const path = require('path');

module.exports = {
  entry: ['./src/index1.js', './src/index2.js'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  }
}

这时会将./src/index1.js./src/index2.js两个文件打包成一个文件输出。

entry为对象:多对多

这种场景其实更常见,比如我们想做一个SPA,一共有5个page,那么每个page应该都是一个单独的入口文件,最后每个page都应该打包输出一个单独的文件,也就是5个输入、5个输出的情况。这时就要用到对象形式的entry:

const path = require('path');

module.exports = {
  entry: {
    index1: './src/index1.js', 
    index2: './src/index2.js'
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  }
}

在output中的filename中,我们没有直接指定文件名,而是用[name].js,这里的name就是entry中的key值index1和index2,也就是告诉webpack,对entry中的两个入口文件分别打包,最后分别输出到dist下的index1.jsindex2.js

更完美一点的用法

在一个SPA项目中,有可能后面还会增加新的page,按照前面的配置,每次新加一个page,就要把这个page的路径放到entry里面去,告诉webpack把新增的这个page打包。显然这种方式并不完美,可以有更懒的办法。为了便于管理,一般会将所有的页面都放到pages目录下,那么在webpack构建时,只需要读取这个目录下的所有js文件,然后动态生成entry对象就可以了。

const path = require('path');
const glob = require('glob');

function getEntries() {
    const pagePath = path.resolve(__dirname, 'src/pages');
    let entries = {};
    // 读取pages目录下的所有js文件
    let files = glob.sync(pagePath + '/*.js');
    // 根据文件名,生成entry对象
    files.forEach(item => {
        let filename = item.split(path.sep).pop().split('.')[0];
        entries[filename] =  item;
    })
    return entries   
}

module.exports = {
    entry: getEntries(),
    output: {
        path: path.join(__dirname, '/dist'),
        filename: '[name].js'
    }
}

这样,就可以一劳永逸,以后添加的所有页面都可以自动打包了。

输出添加hash

大家都知道浏览器是有缓存机制的,如果是同一个js文件,即使服务器上已经存在更新的版本,浏览器仍有可能从本地缓存读取,从而不能拿到最新的结果。要解决这个问题,就需要让每次打包生成的js文件名不一样,然后让HTML去引用这个新名称的js文件,从而绕过浏览器的缓存。修改文件名最简单的方法就是在文件名后面加上md5戳,在webpack中这很容易实现

hash

modules.exports = {
   entry: getEntries(),
   output: {
        path: path.join(__dirname, '/dist'),
        filename: '[name].[hash].js'
    }
}

只用在filename后面加上[hash],最后打包生成的js文件名就会带上md5戳。比如:

index1.20e52ce145885ab03243.js
index2.20e52ce145885ab03243.js

如果不希望后面的md5戳那么长,也可以指定md5戳的长度:

modules.exports = {
   entry: getEntries(),
   output: {
        path: path.join(__dirname, '/dist'),
        filename: '[name].[hash:5].js'
    }
}

这样最后打包生成的js文件的md5长度只有5位:

index1.20e52.js
index2.20e52.js

hash的计算方法是基于整个项目的,只要整个项目中的任何一个文件发生变化,生成的md5戳就会改变,即使真正的入口js及其依赖并没有发生变化。此外,所有的输出文件都共用这一个md5戳的值。这种方式有些过于粗暴,对浏览器的缓存机制很不友好。我们希望的是,如果文件有改动,文件名后面的hash值改变,如果没有改动,则保持不变,浏览器直接从缓存读取。这样既保证了及时更新,又保证了性能。

chunkhash

chunkhash基于每个入口文件及其依赖进行计算,每个输出文件的md5戳只跟自己的依赖文件有关,所以每个输出文件的md5戳都是不同的,并且只有自己的入口文件或依赖文件发生改变时才会变化。

modules.exports = {
   entry: getEntries(),
   output: {
        path: path.join(__dirname, '/dist'),
        filename: '[name].[chunkhash:5].js'
    }
}

最后输出的结果:

index1.f85c4.js
index2.dd9e3.js

显然,chunkhash的方式更加理想。

动态生成HTML

每次打包生成的js文件名都会不一样,那么index.html中的script标签每次都要去改引入的文件名,这也太麻烦了。同样的,webpack可以帮我们解决这个问题。前面的博客中提到,webpack的强大之处在于强大的生态系统,提供了很多有用的插件。在这里,我们就可以使用插件来帮我们实现。

首先,安装这个插件:

npm i html-webpack-plugin -D

然后,在webpack配置文件中使用这个插件:

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

module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      // options配置
    })
  ]
}

这时候再执行npm run build,就会看到dist文件下会自动生成一个index.html,并且这个Html文件会自动引入文件名变化的index.[md5].js文件。直接将这个文件拖到浏览器中就可以显示。

如果不做任何配置,html-webpack-plugin会创建一个新的空白HTML(官方文档说会默认寻找src/index.ejs,不过我在实测中未实现,有可能是我哪个地方配置不对),只是在这个HTML中引入了打包后的js文件,没有设定任何的DOM。这种情况是合理的,因为当我们用webpack时,很多时候HTML本身就是空白的,所有的DOM都是通过js渲染出来然后挂载上去的,比如常见的vue。可以通过title去设定这个HTML的title标签的内容。当然也可以指定一个模板HTML,html-webpack-plugin会在这个HTML中自动引入打包后的js文件。options的主要配置有:

  • template:一个字符串路径,指定HTML的模板,将在该HTML中引入打包后的js
  • filename:指定输出文件的文件名
  • Chunks:默认情况下,html-webpack-plugin会引入打包后的所有的js文件,可以在chunk中指定引入哪些文件
plugins: [
  new HtmlWebpackPlugin({
    // 指定index.html作为模板,即让index.html自动引用打包后的js文件
    template: 'src/index.html',
    // 输出到dist目录下的index1.html
    filename: 'index1.html',
    // 让index.html只引用dist目录下的page1.[hash].js文件,注意这里只用写文件名的前缀,如果写成page1.js会直接在dist文件夹下找page1.js文件,而不能找到带hash的结果
    chunks: ['page1']
  })
]

如果希望有多个HTML,那么就可以设置多个html-webpack-plugin:

plugins: [
  // 第一个页面,index1.html,引用page1.js打包后的文件
  new HtmlWebpackPlugin({
    template: 'src/index.html',
    filename: 'index1.html',
    chunks: ['page1']
  }),
  // 第二个页面,index2.html,引用page2.js打包后的文件
  new HtmlWebpackPlugin({
    template: 'src/index.html',
    filename: 'index2.html',
    chunks: ['page2']
  })
]

当然,如果页面个数是动态变化的,也可以用多entry类型的形式读取文件夹下的所有文件。更多的配置可以Github

清除dist

webpack每执行一次,就会在dist目录下生成一些文件。如果这次生成的文件和上次是同名的,则会直接覆盖,问题不大。但如果是不同名的,则会在dist文件中累加,最后导致dist文件夹中存在很多不必要的文件。例如上面我们加上md5戳之后,每次打包生成的输出文件名并不相同,这就会造成dist文件夹的累积。即使不是用md5戳,也会有类似场景。比如开始我在pages下有一个page3.js,打包后在dist目录下生成了page3.js,后来我不需要这个页面了,把pages下的page3.js删除了,执行webpack,但此时dist目录下的page3.js依然存在。显然这些不是我们希望的。一种解决方法是每次执行webpack前手动清空dist,不过这也太麻烦了,这种麻烦的事情当然需要webpack来替我们做。

首先,需要安装这个插件

npm i clean-webpack-plugin

然后,在配置文件中使用这个插件

const {CleanWebpackPlugin}  = require('clean-webpack-plugin');
modules.exports = {
  ...,
  plugins: [
    new CleanWebpackPlugin()
  ]
}

这样,每次执行webpack时就会自动清除dist目录下的所有文件。

watch模式

到目前为止,每次修改js文件后,都需要手动的执行npm run build来启动webpack进行编译。同样的,这种机械性的重复工作很烦,应该交给webpack来做才对。只需要在执行webpack时加上--watch启动监听模式就可以了,直接在package.json中修改:

"scripts": {
  "build": "webpack --watch"
}

这样只需要执行一遍npm run build,后面只要我们修改了入口文件或其依赖文件中的任意一个,都会自动重新执行webpack打包。注意,如果是修改了webpack的配置文件webpack.config.js,并不会自动重新执行,还是要手动执行以下。

webpack-dev-server自动刷新

虽然修改文件后可以自动打包了,但还是要手动刷新一下浏览器才能看到效果,这步操作也还是有点烦,同样的交给webpack来做吧。

单个输出

我们这里先考虑只有一个输出的情况,即html-webpack-plugin为默认配置:

plugins: [new HtmlWebpackPlugin()]
  • 安装一下webpack-dev-sever插件
npm i webpack-dev-server -D
  • 在配置文件webpack.config.js中启用:
module.exports = {
  devServer: {
    // 指定dist文件夹作为服务器的目录
    contentBase: './dist'
  }
}
  • 这时候,在package.json中直接通过webpack-dev-server启动
"scripts": {
  "build": "webpack --watch",
  "start": "webpack-dev-server --opem"
}

然后执行npm run start,浏览器窗口就会自动打开,显示index.html引用所有js文件的结果。这是只要改变index.html或者任意一个引用的js文件,浏览器都会自动刷新以显示最新的结果。

多个输出

单个输出的情况比较简单,dist目录下只有一个index.html文件,浏览器一打开就会显示这个文件。但是,如果是上面“动态生成HTML”一节描述的多个html文件,DevSever肯定无法知道显示哪一个了,这时有以下2种办法:

  1. Webpack-dev-server一般默认打开的是localhost:8080,在contentBase中我们配置的就是dist文件夹,所以这个地址等同于dist文件夹,如果输入localhost:8080/index1.html就会显示index1.html,输入localhost:8080/index2.html就会显示index2.html
  2. 直接配置openPage,告诉webpack-dev-server默认打开哪个:

    devServer: {
      contentBase: "./dist",
      // 告诉webpack-dev-server默认打开index2.html
      openPage: './index2.html'
    }

热更新

在已经使用了webpack-dev-server的情况下,修改文件已经不需要手动刷新了,但是,浏览器自动刷新也会有一些问题:比如在调试一个表单验证问题,已经填写了一些信息,如果浏览器自动刷新,会直接将之前填写的表单内容清空。而webpack的热更新(HMR, Hot Module Replacement),就可以让浏览器在不刷新的情况下直接更新浏览器页面。

  1. 使用HotModuleReplacementPlugin,它包含在webpack中。在配置文件中添加:

    const webpack = require('webpack');
    
    module.exports = {
      plugins: [new webpack.HotModuleReplacementPlugin()]
    }
  2. 在之前devServer中设置hot为true

    devSever: {
      contentBase: './dist',
      hot: true
    }
  3. 入口文件index.js底部添加对子模块的监听

    if (module.hot) {
      module.hot.accept();
    }

这样,如果再修改子模块a.js中的文件时,在不刷新浏览器的情况下就可以更新。

Devtool调试

到目前为止,都是介绍如何用webpack打包从而进行开发,却忽视了一个问题于webpack将多个文件打包成了单个文件的。最后在浏览器中,我们能看到的也就是打包后的这个文件。如果是打断点调试,我们当然希望是能在打包之前原始模块中,这样定位问题和修改也比较方便。为了实现这个功能,webpack中提供了devtool配置,只需要设置如下:

module.exports = {
  devtool: 'source-map'
}

这样,在浏览器的控制台中就可以看到打包之前的原始模块文件。

clipboard.png

mode

通常,js代码都要分开发模式和生产模式。比如,在开发模式中,不用太纠结代码的性能,更加重视代码的可读性,而在生产模式中,代码一般需要进行优化和压缩等。在webpack4中,可以通过mode来配置development模式和production模式,主要有两种配置方法:

  1. 直接在配置文件中指定mode

    module.exports = {
      mode: 'development'
    }
  2. 在运行webpack时增加--mode,可以在package.json中添加

    "scripts": {
      "dev": "webpack --mode development",
      "prod": "webpack --mode production"
    }

    这时,运行npm run dev就是开发模式,npm run prod就是生产模式

分别看一下dist目录下输出的文件,可以发现production模式下的代码体积更小,dev模式下的代码可读性更强。

多配置文件

在webpack 4中,已经可以通过mode来指定不同生产环境下代码的编译模式了,但这还是不够的。比如,在开发环境下,我们希望开启devtool调试,而在生产模式下就不需要。像这样的区分配置还有很多,如果每次都是去修改webpack.config.js,显然非常麻烦。一种简单的方法是,我们建一个build文件夹,在这个文件下下放三个配置文件。为了整合,先安装一个叫webpack-merge的插件,其主要作用是合并两个webpack配置文件:

npm i webpack-merge -D

然后分别写三个配置文件:

  • webpack.base.config.js:这个文件用来存放公共的配置,也就是在开发环境和生产环境都要用到的

    module.exports = {
      // 公共配置,如entry/output
    }
  • webpack.dev.config.js:开发环境下的独有配置

    const merge = require('webpack-merge');
    const baseConfig = require('./webpack.base.config')
    module.exports = merge(baseConfig, {
        mode: 'development',
        devtool: 'source-map',
        devServer: {
            contentBase: './dist',
            // 在开发模式下,默认打开index1 
            openPage: './index1.html',
            hot: true
        },
    })
  • webpack.prod.config.js:生产环境下的独有配置

    const merge = require('webpack-merge');
    const baseConfig = require('./webpack.base.config');
    
    module.exports = merge(baseConfig, {
        mode: 'production',
        devServer: {
            contentBase: './dist',
            // 在生产模式下,默认打开index2
            openPage: './index2.html',
            hot: true
        },
    })

可以看到,base只是一个公共文件,真正传到webpack使用的是dev和prod。那么如何传到webpack呢?在webpack或者webpack-dev-server运行时,可以通过--config指定配置文件。所以,我们直接修改package.json

"scripts": {
  "dev": "webpack-dev-server --open --config ./build/webpack.dev.config.js",
  "prod": "webpack-dev-server --open --config ./build/webpack.prod.config.js"
}

此时,执行npm run dev,就会按照webpack.dev.config.js的配置,打开index1.html,并且开启debug;执行npm run prod,就会按照webpack.prod.config.js的配置,打开index2.html


程序员不止程序猿
177 声望7 粉丝