读懂这些题,你就是webpack配置工程师

13,758 阅读22分钟

本篇文章解答了60多道关于webpack的问题,答案有详细有简单,时间关系,有的问题还未做出解答,后续会补上,持续更新。如果答案有不对的地方,请轻拍。

1、全局安装和本地安装,开发依赖和项目依赖有什么区别

1-1、全局安装

全局安装webpack,意味着webpack包被安装到npm的全局包目录里了。
查看npm全局包位置:

npm root -g

附加点npm的知识:

查看npm配置信息:

npm config ls

查看npm下载源:

npm config get registry

查看npm安装目录:

npm config get prefix

这里假设通过命令npm config get prefix获取到的路径为C:\Users\wz\AppData\Roaming\npm ,为了方便起见,用A代替C:\Users\wz\AppData\Roaming
通过这个目录可以获取到以下信息:

  • 1、npm全局配置文件(A\npm\etc)
  • 2、全局安装的包(A\npm\node_modules)
  • 3、全局命令(npx link生成的命令)(A\npm)
  • 4、npm缓存(A\npm-cache)

1-2、本地安装

本地安装webpackwebpack包被安装到了项目根目录下的node_modules里了。

1-3、开发依赖

开发依赖,指的是只有在项目开发、编译、打包、压缩等过程才会用到的包,一旦文件成功产出,就不再需要他们了,比如:less-loaderwebpack等等。

1-4、项目依赖

项目依赖,指的是从开始编写项目到项目上线以后都需要的包,比如:vueaxios等等。

2、使用webpack需要安装哪些模块

需要安装:

  1. webpackwebpack核心包)
  2. webpack-cliwebpack命令行工具,依赖于webpack核心包)

3、webpack默认配置文件(2种)的名字是什么

webpack默认配置文件:
第一种:webpack.config.js
第二种:webpackfile.js

webpack源码中和配置相关的代码,位置:webpack/bin/convert-argv.js

//...
//从这里可以看出webpack的2中配置文件
var defaultConfigFiles = ["webpack.config", "webpackfile"].map(function(filename) {
		return extensions.map(function(ext) {
			return {
				path: path.resolve(filename + ext),
				ext: ext
			};
		});
	}).reduce(function(a, i) {
		return a.concat(i);
	}, []);

//...

4、webpack打包后的结果为什么可以在浏览器中执行

为什么会在浏览器里执行,就要从webpack打包后的文件中去找答案了。
搭建一个简单的项目,只安装webpack这个包,项目目录如下:

index.js如下:

function foo() {
    console.log(window.localStorage)
}
module.exports = { foo }

webpack.config.js如下:

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

在命令行执行webpack命令后,打包后的文件内容,经过删减,总体"框架"代码如下:

    (function (modules) {
        var installedModules = {};
        function __webpack_require__(moduleId) {
            if (installedModules[moduleId]) {
                return installedModules[moduleId].exports;

            }
            var module = installedModules[moduleId] = {
                i: moduleId,
                l: false,
                exports: {}

            };
            modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
            module.l = true;
            return module.exports;

        }
        return __webpack_require__("./src/index.js");
    })({
        "./src/index.js": (function (module, exports) {
            eval(/* ... */)
        })
    })

分析以上代码:
1、总体是一个自执行函数,双括号形式。

(function(modules){/* ... */})({})

一个自执行函数当然可以在浏览器中执行了。

2、自执行函数的函数体内,定义了模块缓存对象installedModules,定义了模块加载方法__webpack_require__,这个__webpack_require__方法就是为浏览器量身打造的,作用相当于node中的require方法。__webpack_require__方法的逻辑也不难理解,首先通过模块名(其实就是一个路径)去模块缓存对象上查找,找不到的话,就新建一个模块module并缓存到installedModules上,然后通过模块名找到对应的模块函数,执行它,并将module等参数传入,最后返回模块导出对象module.exports。这段代码建议仔细看看
3、自执行函数的参数。该参数是一个对象,类似下面这样:

{
    "./src/index.js": (function (module, exports) {eval(/* ... */)})
}

该对象的键是一个路径字符串,其实就是我们调用require方法时传入的模块路径; 值为一个接收moduleexports参数的函数,函数体内是一个包裹着一堆字符串代码的eval函数,这一堆字符串代码就是我们写的代码。可见,webpack为了让我们的代码能够在浏览器里执行,做了多少工作。

参考文章

5、webpack有几种模式,对应模式的作用

webpack有3种模式:

  1. development
  2. production
  3. none

简单来说,不同的模式下,webpack会启用不同的插件,或者不同的优化手段。
development模式下,会启用以下插件:

  • new webpack.NamedModulesPlugin()
  • new webpack.NamedChunksPlugin()

production模式下,会启用以下插件:

  • new UglifyJsPlugin(/* ... */)
  • new webpack.optimize.ModuleConcatenationPlugin()
  • new webpack.NoEmitOnErrorsPlugin()

none模式下,不会启动任何插件。 详情可参考这篇文章

6、配置执行命令的脚本在package.json中如何配置

package.json文件中的script字段里配置,如下,我们配置3条命令,分别为devprostart

{
  "name": "l",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev":"",   //dev命令
    "pro":"",   //pro命令
    "start":""  //start命令
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.35.3"
  }
}

配置完成后,就可以在项目的命令行里执行以下命令了:

npm run dev

npm run pro

npm run start

7、如何指定webpack-dev-server的启动目录

修改和webpack-dev-server有关配置的contentBase选项。

let path = require('path')
module.exports = {
    mode:'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename:'bundle.js'
    },
    devServer: {
        port: 8888,//监听的端口号
        progress: true,//启动打包进度显示
        contentBase: path.join(__dirname, 'dist'),//这里指定启动目录
        compress: true //启动压缩
    }
}

关于webpack-dev-server的更多配置,点击这里
从打包的过程也可以看出webpack-dev-server的启动目录,如下:

8、如何压缩产生的html文件

使用html-webpack-plugin插件,配置如下:

    plugins: [
        new HtmlWebpackPlugin({
            template: './index1.html',
            filename: 'main.html',
            minify: {
                collapseWhitespace: true,//移除空格
                removeAttributeQuotes:true//移除属性的双引号
            }
        })
    ]

官方说如果模式为production的话,minify选项会被默认设置成true,产出的HTML文件会自动压缩,大家可以试试,我尝试的不行。
minify的配置选项有很多,感兴趣可以点这里查看更多。
其实,html-webpack-plugin可以压缩HTML文件,内部是依赖的是这个库html-minifier,这个压缩HTML的库也可以这样使用:

var minify = require('html-minifier').minify;
var result = minify('<p title="blah" id="moo">foo</p>', {
  removeAttributeQuotes: true
});
result; // '<p title=blah id=moo>foo</p>'

关于html-minifier的更多信息,这里

9、如何解决打包后缓存的问题

JavaScript文件解决方法:
第一种:
让文件名称带有hash字符串,这样每次打包js文件时,只有内容有变化,hash字符串就会发生变化,比如下面:

    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[hash:8].bundle.js'
    },

这里打包后的文件名称为main.d3a5dd20.bundle.js
第二种:
html-webpack-plugin的配置项hash置为true,这样打包后的js文件在插入html文件后会以?开始添加hash字符串。如下:

    plugins: [
        new HtmlWebpackPlugin({
            template: './index1.html',
            filename: 'main.html',
            hash:true
        })
    ]

这里打包后的文件名称如下:

<body>
    <h1>This is a title</h1>
<script type="text/javascript" src="main.js?d3a5dd204b4d1b64170c"></script>
</body>

10、webpack中的loader是干什么的

loader的作用是对源代码进行转换,webpack只认识jsjson2种文件,其他类型的文件,比如cssimgless等等,只能依靠loader去解析转换了。
官方解释,这里

11、css-loader和style-loader的作用

css-loader主要处理css文件中@importurl()语法的。官方文档
style-loader主要作用是将css样式以style标签的形式插入到页面中。官方文档

12、请说出loader的特点

  1. 第一个loader要返回js脚本
  2. 每个loader只做一件事情,为了使loader在更多场景下链式调用
  3. 每一个loader都是一个node模块
  4. loader有同步的,也有异步的
  5. loader有两个执行阶段,pitchnormal

官网文档
loader的执行顺序比较讲究, 如下图所示:

13、loader有几种写法

配置文件写法、行内loader写法、命令行写法3种

配置文件写法:
就是将配置信息写到webpack.config.js,写法又有以下几种:

13-1、直接写loader

module.exports={
    module:{
        rules:[
            {
                test: /.js$/,
                loader: 'my-loader',
                exclude: /node_modules/
            },
        ]
    }
}

13-2、使用use,字符串形式

module.exports={
    module:{
        rules[
             {
                test: /.js$/,
                use: 'my-loader',//直接传递字符串
                exclude: /node_modules/
            },
        ]
    }
}

13-3、使用use,对象形式

module.exports={
    module:{
        rules[
             {
                test: /.js$/,
                use: {  //对象形式,可以给loader传递参数
                    loader:'my-loader',
                    options:{}//这里传递参数给loader
                }
                exclude: /node_modules/
            },
        ]
    }
}

13-4、使用use,数组形式

数组内的每一项可以为字符串,也可以是对象。

module.exports = {
    module: {
        rules: [
            {
                test: /.js$/,
                use: [
                    'my-loader1',//字符串形式
                    { loader: 'my-loader2', options: {} }//对象形式
                ],
                exclude: /node_modules/
            },
        ]
    }
}

行内loader写法:
多个loader之间用!分割。

let something=require('loader2!loader1!./profile.js')

行内loader可添加前缀,代表当前文件是否交由其他loader处理:

  • -! 表示不会让文件再去通过 pre+normal loader处理了
  • ! 表示不会让normal loader处理了
  • !! 该文件只会让行内loader处理
let a = require('inline-loader!./a') // !分割,inline-loader就是行内loader
let a = require('-!inline-loader!./a') // -!表示不会让文件再去通过 pre+normal loader处理了
let a = require('!inline-loader!./a') // !  表示不会让normal loader处理了
let a = require('!!inline-loader!./a') // !! 该文件只会让行内loader处理

命令行写法:

webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'

14、loader的默认顺序是怎样的

从右向左,从下到上

{
    test: /\.js$/,
    use: ['loader3', 'loader2', 'loader1'] 
}

以上loader执行顺序为 loader1---> loader2--->loader3

{
    test: /\.js$/,
    use: {
        loader:'loader3'
    }
},
{
    test: /\.js$/,
    use: {
        loader: 'loader2'
    }
},
{
    test: /\.js$/,
    use: {
        loader: 'loader1'
    }
}

以上loader执行顺序为 loader1--->loader2--->loader3

15、使用loader时如何传递参数

第一种:在配置文件中传递参数

module.exports={
    module:{
        rules[
             {
                test: /.js$/,
                use: {  //对象形式,可以给loader传递参数
                    loader:'my-loader',
                    options:{}//这里传递参数给loader
                }
                exclude: /node_modules/
            },
        ]
    }
}

第二种:行内loader传递参数的方法

let foo = require('expose-loader?$!./a.js')

16、如何控制loader的执行顺序

loader的默认顺序是从右向左,从下到上,不过可以通过enforce字段来打破这种顺序。
enforce有两个取值:pre代表第一个执行。post代表最后一个执行
有如下loader配置:

            {
                test: /.js$/,
                use: 'loader1.js',
                exclude: /node_modules/,
                enforce: 'pre',//代表loader1首先被执行
            },
            {
                test: /.js$/,
                use: 'loader2.js',
                exclude: /node_modules/
            },
            {
                test: /.js$/,
                use: 'loader3.js',
                exclude: /node_modules/
            },
            {
                test: /.js$/,
                use: 'loader4.js',
                exclude: /node_modules/,
                enforce: 'post'//代表loader4最后被执行
            }

如果没有配置enforce字段,执行顺序为:loader4--->loader3--->loader2--->loader1
如果配置了enforce字段,执行顺序为:loader1--->loader3--->loader2--->loader4
注意:没有配置enforce字段的loader默认为normal,按照默认顺序执行.
如果文件在require的时候用到了行内loader的话,执行顺序如下:
pre--->normal--->inline--->post

17、如何指定loader想要处理的js文件

思考中...

18、常见的css预处理器有哪些,对应的loader有哪些

css预处理器:lesssassstylus
对应的loaderless-loadersass-loaderstylus-loader

19、如何实现css样式抽离

安装插件:

npm install mini-css-extract-plugin -D

配置文件:

{
     module: {
        rules: [
            {
                test: /\.css$/,
                use: [{
                    loader: MiniCssExtractPlugin.loader,//使用插件loader
                },
                    'css-loader'
                ]
            },

        ]
    },
    plugins: [
        //添加插件实例
        new MiniCssExtractPlugin({
            filename: 'index.css'
        })
    ]
}

mini-css-extract-plugin更多用法

20、如何实现自动添加浏览器前缀

安装包:

npm install postcss-loader autoprefixer -D

项目根目录下新建文件postcss.config.js,内容如下:

module.exports = {
    plugins: [require('autoprefixer')]
}

webpack.config.js配置:

    module: {
        rules: [
            {
                test: /\.css$/,
                use: [{
                    loader: MiniCssExtractPlugin.loader,
                },
                    'css-loader',
                    'postcss-loader'//这里加入了新的loader
                ],
                include: path.join(__dirname, './src'),
                exclude: /node_modules/
            },

        ]
    }

21、抽离css后如何实现css压缩

安装包:

npm install optimize-css-assets-webpack-plugin -D

webpack.config.js配置:

{
        plugins: [
        new OptimizeCSSAssetsPlugin()
    ]
}

22、压缩js需要使用什么插件

安装包:

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

webpack.config.js配置:

module.exports = {
  optimization: {
    minimizer: [ new UglifyJsPlugin() ],
  },
};

或者:

{
        plugins: [
        new UglifyJsPlugin()
    ]
}

uglifyjs-webpack-plugin官方文档

23、想把es6转化成es5需要哪些模块

npm install @babel/core @babel/preset-env babel-loader -D

24、babel-loader和@babel/core的关系

@babel/core是核心包,提供基础的API服务。babel-loader依赖@babel/core
关于babel的知识,推荐阅读这两篇:
我是第一篇
我是第二篇

25、@babel/plugin-proposal-class-properties和@babel/plugin-proposal-decorators作用是什么

@babel/plugin-proposal-class-properties是用来转换class语法的。比如:
如下语法:

class Bork {
    static a = 'foo';
    static b;

    x = 'bar';
    y;
  }

会被转换为:

var Bork = function Bork() {
  babelHelpers.classCallCheck(this, Bork);
  this.x = 'bar';
  this.y = void 0;
};

Bork.a = 'foo';
Bork.b = void 0;

官方文档
@babel/plugin-proposal-decorators是用来转换修饰符(@)的。
官方文档

26、@babel/plugin-transform-runtime,@babel/runtime,@babel/polyfill区别是什么

@babel/polyfill:
babel 默认只转换 js 语法,而不转换新的 API,比如 IteratorGeneratorSetMapsProxyReflectSymbolPromise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码。
举例来说,es2015Array 对象上新增了 Array.from 方法。babel 就不会转码这个方法。如果想让这个方法运行,必须使用 @babel/polyfill。(内部集成了 core-js 和 regenerator) 使用时,在所有代码运行之前增加 require('@babel/polyfill')。
@babel/plugin-transform-runtime依赖@babel/runtime,也就是说:在使用@babel/plugin-transform-runtime的时候必须把@babel/runtime当做依赖。
推荐阅读24题的两篇文章。

27、如何对js代码进行校验

可以使用eslint包对js代码进行校验。
安装包:

npm i eslint eslint-loader babel-eslint -D

新建eslint配置文件.eslint.js:

module.exports = {
    root: true,
    //指定解析器选项
    parserOptions: {
        sourecType: 'module'
    },
    //指定脚本执行的环境
    env: {
        browser: true
    },
    //启用的规则及其各自的错误级别
    rules: {
        "semi": "error",//语句强制分号结尾
        "indent": ["error",4],//缩进风格
        "quotes":["error","double"]//引号类型
    }
}

webpack.config.js

    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'eslint-loader',
                    options: {
                        fix: true
                    },

                },
                include: [path.resolve(__dirname, 'src')],
                exclude: /node_modules/,
            }
        ]
    }

eslint官网文档

28、请说下暴露变量的三种方式

暴露变量其实就是暴露到全局,也就是挂在到window上。
第一种:
安装包expose-loader

npm install expose-loader -D

在入口文件(webpack.config.js中的entry)中使用expose-loader,这种方式属于内联loader

import $ from 'expose-loader?$!jquery'
console.log(window.$)
  • ! --->将loader和包隔开符号
  • ? --->给expose-loader传递参数
  • $ --->暴露的全局变量命名为$

当然也可以不使用内联loader
第一步:入口文件正常引入:

import $ from 'jquery'
console.log(window.$)

第二步:在webpack.config.js配置文件中配置

rules: [
            {
                test: require.resolve('jquery'),
                use: 'expose-loader?$'
            }
        ]

第二种:
在每个模块中注入$,注意是每个模块都有一个$,但是这个$并不是全局的。
webpack.config.js配置文件中配置

plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery'
        })
    ]

然后模块中可以直接使用$
第三种:
这种方式叫引入不打包。在html文件中引入jquery,这里以jquerycdn为例:

<!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>Document</title>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
    <script src="../dist/bundle.js"></script>
</head>

<body>

</body>

</html>

注意:如果使用了这种方式,就不要在js文件内在引入jquery了,否则会重复打包。如果非要引入,那就修改一下webpack.config.js

module.exports = {
    //...
    externals: {
        jquery: '$'//webpack打包时,会忽略掉jquery
    },
    //...
}

29、ProvidePlugin会将提供的变量挂载在window上吗

不会,官方文档没有明说,但是有句话的潜台词表明了不会。哪句话呢,友情提示:Whenever the identifier is encountered as free variable in a module...
参考官方文档

30、如何在webpack中处理图片模块

先说一下,使用图片的几种方式。

  • js中创建图片来引入
  • 在css中引入
  • 在html文件中用img标签引入

30-1、js中创建图片来引入

使用方式:
安装包file-loader

npm install file-loader -D

js文件内:


import logo from '../logo.png'

//file-loader 默认会在内部生成一张图片到build目录下,被导入的图片在js文件内是一个hash字符串
console.log(logo)//19470b4db4deed52a8ba081c816e8f0d.png

let image = new Image()

image.src = logo

webpack.config.js文件内配上相应的loader

module: {
        rules: [
            {
                test: /\.(png|jpg|gif)$/,
                loader: 'file-loader'
            }
        ]
    },

30-2、在css中引入

使用方式:
安装包style-loadercss-loader,当然还可以安装css预处理包

npm install style-loader css-loader -D

css文件内:

body{
    background: url("../logo.png")
}

webpack.config.js文件内配上相应的loader

module: {
        rules: [
            {
                test: /\.css$/,
                loader: 'style-loader!css-loader'
            }
        ]
    },

30-3、在html文件中用img标签引入

使用方式:
安装包html-withimg-loader

npm install html-withimg-loader -D

html文件内:

<!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>Document</title>
    <script src="../dist/bundle.js"></script>
</head>

<body>
    <img src="./logo.png" alt="">
</body>

</html>

webpack.config.js文件内配上相应的loader

module: {
        rules: [
            {
                test: /\.html$/,
                loader: 'html-withimg-loader'
            },
        ]
    },

31、webpack打包时如何处理html的img

使用html-withimg-loader包来处理html的img。

32、webpack打包时如何把图片变为base64

安装包url-loader.

npm install url-loader -D

webpack.config.js文件内配上相应的loader

module: {
        rules: [
            {
                test: /\.(png|jpg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 200 * 1024
                    }
                }
            },
        ]
    },

当图片小于设定的大小(K)的时候,用base64来转化,否则用file-loader产生真实的图片。

33、file-loader和url-loader的关系

他俩其实没有必然联系,只能说可以搭配起来一起工作。url-loader只能将图片解析成base64,当图片大小超过了限制,url-loader就会把解析图片的工作交给其他工具(默认是file-loader),当然,当图片大小超过了限制,而我们想用其他工具来处理图片,可以通过参数来控制:

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              fallback: 'responsive-loader',//当图片大小超过8K,用responsive-loader处理
              limit: 8*1024,
            },
          },
        ],
      },
    ],
  },
};

可以参考篇幅不长的官方文档

34、publicPath的作用是什么

publicPath指定的路径会被作为前缀添加到所有的url上。这里的url指的是:

  • html文件中的link标签,script标签、img标签
  • css中的带有文件引入的属性等。比如:background:url()

一般当静态资源放在CDN时,publicPath会指定CDN的路径。
官方文档

35、如何配置多页面应用

配置多页面就需要配置webpack的多入口。

module.exports = {
    mode: 'development',
    entry: {
        main: './src/main.js',//入口1,main.js
        index: './src/index.js',//入口2,index.js
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[hash:8].bundle.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './index.html',
            chunks: 'main',//这里的main和entry里的main属性需要保持一致
            filename: 'index.html'
        }),
        new HtmlWebpackPlugin({
            template: './index.html',
            chunks: 'index',//这里的index和entry里的index属性需要保持一致
            filename: 'main.html'
        })
    ]
}

官方文档

36、如何在html文件指定引入某个js文件

思考中...

37、source-mapeval-source-map的区别

这个配置主要是debug用的,配置选项有很多,这里挑选4个说明。

  • source-map 生成map文件,定位到行列
  • eval-source-map 不生成map文件,定位到行列
  • cheap-module-source-map 生成map文件,定位到行
  • cheap-module-eval-source-map 不生成map文件,定位到行

官方文档

38、如何实时编译打包处的文件

在webpack配置文件中新增如下配置信息:

module.exports = {
    mode: 'development',
     //开启实时编译
    watch: true,
     //实时编译的配置选项
    watchOptions: {
        ignored: /node_modules/,
        poll:1000,//每秒询问文件变更的次数
        aggregateTimeout:500//防止重复保存频繁重新编译,500毫秒内重复保存不打包
    }
}

39、aggregateTimeout:500的作用是什么

当检测文件不再发生变化,会先缓存起来,等待一段时间后,再通知监听者,这个等待时间通过aggregateTimeout配置。

40、bannerPlugin的作用

bannerPlugin的作用是在产出的资源文件头部添加信息,比如:添加作者、版本号等信息。

let webpack=require('webpack')
module.exports={
    //...
    plugins: [
        new webpack.BannerPlugin('author:wangZhi')
    ],
    //...
}

产出的文件头部如下所示:
css

/*! author:wangZhi */
body{
    background:red;
}

js

/*! author:wangZhi */
/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {

41、webpack如何配置代理

首先启动一个web服务,配置如下:

var path = require('path');

module.exports = {
  //...
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000
  }
};

然后配置代理:

proxy: {
            '/api': {//以/api开头的请求会被代理到'https://other-server.example.com'
                target: 'https://other-server.example.com',
                pathRewrite: {
                    '^/api': ''//以/api开头的路径会被替换成''
                }
            }
        }

官方文档

42、before函数中的app有什么作用

webpack-dev-server是依靠express启动一个web服务的,配置中的app就是express中的app,有了app,我们可以编写接口,响应接口等,相当于前台自己mock一些数据。建议看看express官网

module.exports = {
  //...
  devServer: {
    before: function(app, server) {
        //编写一个/some/path接口,后续请求会在这里直接处理
      app.get('/some/path', function(req, res) {
        res.json({ custom: 'response' });
      });
    }
  }
};

43、webpack-dev-middleware作用是什么

服务端启动webpack,webpack-dev-middleware其实是一个express的中间件。

let webpack = require('webpack')
let express = require('express')
let config = require('./webpack.config')
let middle = require('webpack-dev-middleware')
let app = express();
//webpack提供的方法,传入webpack配置,得到一个编译对象 
let compiler = webpack(config);
//使用中间件
app.use(middle(compiler))
//监听端口
app.listen(2000)

官方文档

44、resolve属性有哪些配置

列出几个常用的配置:

module.exports = {
  //...
  resolve: {
        //模块查找路径
        modules: [path.resolve('node_modules'),'mydir'],
        //配置别名
        alias: {
            bootstrap:'bootstrap/dist/css/bootstrap.css'
        },
        //查找字段
        mainFields: ['main', 'style'],
        //查找文件名
        mainFiles: ['index.js'],
        //查找文件的后缀名
        extensions:['.js','.css','.vue']
    },
};

官方文档

45、如何定义别名

见上44题。

46、如何省略引入文件的后缀

见上44题。

47、如何定义环境变量

定义环境变量需要用到webpack的一个包--->definePlugin

module.exports = {
    //...
    plugins: [
        new webpack.DefinePlugin({
            DEV: 'dev',
            DEV_str: JSON.stringfiy('dev'),
            FLAG: 'true',
            FLAG_str: "'true'",
            expression: '1+1',
            expression_str: JSON.stringify('1+1')
        })
    ]
}

上述配置定义了6个环境变量,打包编译后的结果为:

  • DEV--->报错,dev is not defined
  • DEV_str---> 'dev'(字符串)
  • FLAG---> true(布尔值)
  • FLAG_str---> 'true'(字符串)
  • expression---> 2(数字型)
  • expression_str---> '1+1'(字符串)

48、如何区分开发环境和生产环境

第一种方式,可以通过设置环境变量来区分,设置环境变量见上一题。

if(DEV==='development'){
    //do something
}else if(DEV==='production'){
    //do something
}

第二种方式,创建多套配置文件。
安装包 webpack-merge

npm install --save-dev webpack-merge

项目目录:

  webpack-demo
  |- package.json
  |- webpack.common.js //开发环境、生产环境公用配置文件
  |- webpack.dev.js //开发环境配置文件 
  |- webpack.prod.js //生产环境配置文件
  |- /dist
  |- /src
    |- index.js
    |- math.js
  |- /node_modules

webpack.common.js文件:

  const path = require('path');
  const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
 
  module.exports = {
    entry: {
      app: './src/index.js'
    },
    plugins: [
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        title: 'Production'
      })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

webpack.dev.js文件:

  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');
 
  module.exports = merge(common, {
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
      contentBase: './dist'
    }
  });

webpack.prod.js文件:

  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');
 
  module.exports = merge(common, {
    mode: 'production',
  });

参考官方文档

49、webpack-merge作用是什么

webpack-merge的作用是合并对象。
最基本的用法:

let merge = require('webpack-merge')
let newObj = merge(obj1,obj2,obj3,...)

还是看文档

50、如何不解析某些依赖库

webpack的配置文件中,通过配置externals字段可以达到不解析某些依赖库的目的。如下:

module.exports = {
    //...
    externals: {
        jquery: '$'//webpack打包时,会忽略掉jquery
    },
    //...
}

官方文档

51、如何设置loader的解析文件夹

1、直接写绝对路径

{
    test: /.js$/,
    use: path.resolve(__dirname,'loader/loader1.js')
}

2、配置别名

    resolveLoader: {
        alias: {
            loader1: path.resolve(__dirname, 'loader', 'loader1')
        }
    }

3、配置modules

    resolveLoader: {
        modules: ['node_modules', path.resolve(__dirname, 'loader')]
    }

我是官方文档

52、使用了moment后默认会引入locale文件夹

思考中...

53、dllPlugin如何使用

打包一个dll文件:

let path = require('path');
//引入插件,webpack内置插件
let DllPlugin = require('webpack/lib/DllPlugin')
module.exports = {
    mode: 'development',
    entry: {
        //将react、react-dom库打包成动态链接库
        react:['react','react-dom']
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].dll.js',
        //动态链接库导出的全局变量
        library: '_dll_[name]'
    },
    plugins: [
        new DllPlugin({
            //name需和output.library一致。
            name: '_dll_[name]',
            //生成的json文件存放目录
            path: path.join(__dirname, 'dist', '[name].manifest.json')
        })
    ]
}

按照如上配置,我们最终得到了一个react.dll.js文件,该文件内容经过删减替换整理后,如下:

var _dll_react = (function (modules) {
    var installedModules = {};
    function __webpack_require__(moduleId) {
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}

        };
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        return module.exports;

    }
    return __webpack_require__(0);
})({
    "module1": (function (module, exports, __webpack_require__) { /* ... */}),
    "module2": (function (module, exports, __webpack_require__) { /* ... */}),
    "module3": (function (module, exports, __webpack_require__) { /* ... */}),
    "module4": (function (module, exports, __webpack_require__) { /* ... */}),
    0: (function (module, exports, __webpack_require__) {
        eval("module.exports = __webpack_require__")
    })
})

为了直观,我将源码中的类似./node_modules/object-assign/index.js这样的属性名替换成了module1,module2等等。
分析上述代码,我们可以得知,_dll_react变量其实就是__webpack_require__方法。该方法接受一个模块id,返回该模块的内容。
再来看一下,生成的react.manifest.json文件,内容经过删减整理如下:

{
    "name": "_dll_react",
    "content": {
        "./node_modules/react-dom/index.js": {
            "id": "./node_modules/react-dom/index.js",
            "buildMeta": {
                "providedExports": true
            }
        }
        //...
    }
}

content对象里的键名./node_modules/react-dom/index.js就是模块请求路径,也就是说,当webpack遇到了如下语句require('./node_modules/react-dom/index.js')时,webpack会拿着调用require方法传入的路径去react.manifest.json文件内的content对象中找到键为该路径的属性,然后webpack就获取到了该路径对应的模块内容。
如果我们不使用动态链接库,当webpack遇到了如下语句require('./node_modules/react-dom/index.js')时,webpack会拿着调用require方法传入的路径去获取文件内容,然后拼接头部信息(就是function (module, exports, __webpack_require__) {}),然后递归解析文件内容的require语句,然后将依赖写入依赖列表。
综上所述,使用动态链接库确实在打包速度上得到了一定的提升。
使用一个dll文件相对来说就比较简单了,按照格式写就可以了:

let path = require('path');
//引入插件,webpack内置插件
let DllReferencePlugin = require('webpack/lib/DllReferencePlugin')
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].bundle.js',
    },
    plugins: [
        //使用动态链接库
        new DllReferencePlugin({
           manifest:require('./dist/react.manifest.json')
        })
    ]
}

官方文档

54、如何实现多线程打包,优化前5个

安装包happypack

npm install --save-dev happypack

webpack.config.js配置文件如下:

module.exports = {
    rules: [
        {
            test: /\.js$/,
            use: 'happypack/loader?id=jsx'
        },

        {
            test: /\.less$/,
            use: 'happypack/loader?id=styles'
        },
    ],
    plugins: [
        new HappyPack({
            id: 'jsx',
            threads: 4,
            loaders: ['babel-loader']
        }),

        new HappyPack({
            id: 'styles',
            threads: 2,
            loaders: ['style-loader', 'css-loader', 'less-loader']
        })
    ]
}

happypack如何工作的呢,一图胜千言:

官方文档

55、tree-shaking是否支持require语法和有什么作用

不支持require语法,依赖于ES2015模块的静态结构,比如:import export。 作用就是能够去除未用到的代码。
官方文档

56、scope hosting的作用是什么

Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 "作用域提升",是在 Webpack3 中新推出的功能。

  • 代码体积更小,因为函数声明语句会产生大量代码
  • 代码在运行时因为创建的函数作用域更少了,内存开销也随之变小

在配置文件中添加一个新的插件,就可以实现scope hosting功能。

module.exports = {
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
}

参考文章

57、请简述webpack整个工作流程

webpack的打包过程比较复杂,这里用一张图简述一下,权当抛砖引玉了。

58、请简述webpack中的HotModuleReplacement原理。

1、当文件发生变化后,webpack会重新打包,打包完成后,发布done事件。
2、done回调函数执行,通过服务端与客户端建立的长连接发送hash值到客户端。
3、客户端收到hash值之后,确认是否要更新。如果更新,则会通过Ajax去请求manifest.json文件,该文件记录了所有发生变动的模块。
4、通过manifest.json文件,客户端使用jsonp方式去拉取每一个变动模块的最新代码。
5、客户端更新模块,加入了3个属性:parentschildrenhot
6、通过模块id找到父模块中所有依赖该模块的回调函数并执行。
7、页面自动更新,热替换完成。
如下图所示:

59、webpack代码分割方式有哪些,其中import()方法的原理是什么。

第一种:多入口

module.exports = {
    entry: {
        page1: './src/page1.js',
        page2: './src/page2.js',
        page3: './src/page3.js'
    }
}

第二种:webpack内部配置项。

module.exports = {
    optimization:{
        splitChunks:{
            cacheGroups:{
                vendors:{
                    chunks:'initial',//指定分割的类型,默认3种选项 all async initial
                    name:'vendors',//给分割出去的代码块起一个名字叫vendors
                    test:/node_modules/,//如果模块ID匹配这个正则的话,就会添加一vendors代码块
                    priority:-10 //优先级
                },
                commons:{
                    chunks:'initial',
                    name:'commons',
                    minSize:0,//如果模块的大小大于多少的话才需要提取
                    minChunks:2,//最少最几个chunk引用才需要提取
                    priority:-20
                }
            }
        }
    }
}

第三种:调用webpack提供的import方法。
假设,项目的入口文件是index.js,内容如下:

    let button = document.createElement('button');
    button.innerHTML = '点我点我';
    button.addEventListener('click',event=>{
        debugger;
        import('./hello.js').then(result=>{
            alert(result.default);
        });
    });
    document.body.appendChild(button);

入口文件index.js被编译后的代码如下:

let button = document.createElement('button');
button.innerHTML = '点我点我';
button.addEventListener('click', event => {
    __webpack_require__.e("src_hello_js.js").then(__webpack_require__.t.bind(__webpack_require__, "./src/hello.js")).then(result => {
        alert(result.default);
    });
});
document.body.appendChild(button);

看重点代码import('./hello.js')被编译成了__webpack_require__.e("src_hello_js.js").then(__webpack_require__.t.bind(__webpack_require__, "./src/hello.js")),./hello.js会被当作为一个入口文件,然后打包成一个独立的文件,和项目入口文件index.js打包出来的文件放在一起。也就是说,我们这里的项目本来就一个入口文件,结果打包出来两个文件,原因就是使用了import方法将代码进行了分割。
__webpack_require__.e方法代码如下:

__webpack_require__.e = function (chunkId) {
    return new Promise((resovle, reject) => {
        installedChunks[chunkId] = resovle;
        let script = document.createElement('script');
        script.src = chunkId;
        document.body.appendChild(script);
    }).catch(error => {
        alert('异步加载失败');
    });
}

可以看到该方法主要功能就是根据传入的chunkId使用jsonp去拉取对应的模块代码。这里它返回了一个promise,并将resolve放到了全局installedChunks对象上。因为这里不能确定jsonp什么时候成功,所以无法调用resolve,只能将它挂载到全局变量中。那什么时候能确定jsonp成功了呢,答案是在jsonp的回调函数里可以确定,也就是下面的webpackJsonp方法里。
先来看看jsonp拉取回来的代码长什么样子吧,如下:

window.webpackJsonp("src_hello_js.js", {
    "./src/hello.js": (function (module, exports, __webpack_require__) {
        module.exports = 'hello';
    }),
});

jsonp标准格式,返回一个方法调用,参数就是响应的数据。
window.webpackJsonp方法代码如下:

window.webpackJsonp = (chunkId, moreModules) => {
    for (moduleId in moreModules) {
        modules[moduleId] = moreModules[moduleId];
    }
    installedChunks[chunkId]();//resolve()
    installedChunks[chunkId] = 0;
}

webpackJsonp主要工作就是将拉取回来的模块一一挂载到全局的modules对象中。并且改变对应的promise状态,也即是执行事先放在全局变量中的resolve方法,这样就会执行之后的then方法了。
__webpack_require__.t方法代码如下:

__webpack_require__.t = function (value) {
    value = __webpack_require__(value);
    return {
        default:
            value
    };
}

该方法就是简单的去加载模块,然后返回加载后的结果。
一图胜千言: