学习webpack之二:基础配置

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

entry和output

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

默认值

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

entry为字符串:一对一

经过字符串值能够直接指定入口文件的路径,好比上一篇博客中的例子:java

const path = require('path');

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

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

entry为数组:多对一

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

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两个文件打包成一个文件输出。github

entry为对象:多对多

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

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.jsshell

更完美一点的用法

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

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'
}
复制代码

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

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

相关文章
相关标签/搜索