webpack面试笔记(一)

一.webpack基础

1.模块化

什么是模块化?

模块化是把一个复杂的系统分解到多个模块以方便编码

为什么出现模块化

以前使用命名空间的方式来组织代码,比如jQuery,zepto, 它们有很多缺点:

  • 命名空间冲突,两个库可能会使用同一个名称,例如zepto也被放在window.$下
  • 无法合理管理项目的依赖和版本
  • 无法方便地控制依赖的加载顺序

当项目变大时这种方式将变得难以维护,需要使用模块的思想来组织代码

2.构建的作用及常见功能

概念:

构建就是当源代码无法直接运行时,通过转化将源代码转换成可执行的JavaScript,css,html代码

包括:

  • 代码转换: typescript编译成JavaScript,scss编译成css等
  • 压缩JavaScript,css,html代码,压缩合并图片等
  • 代码分割: 提取多个页面的公共代码,提取首屏不需要执行部分的代码让其异步加载
  • 模块合并: 在采用模块化的项目会有很多个模块和文件,需要构建功能将模块合并成一个文件
  • 自动刷新: 监听本地源代码的变化,自动重新构建,刷新浏览器
  • 代码检验: 在代码被提交到仓库前需要检验代码是否符合标准,但愿测试是否通过
  • 自动发布: 更新完代码后, 自动构建出线上发布代码并传输给发布系统

3.常见的构建工具及其优缺点

1) npm script

npm script是一个任务执行者,npm在安装nodejs时附带的包管理器,npm script是npm内置的一个功能,允许在package.json文件使用scripts字段定义任务:

{
  "scripts": {
    "dev": "node dev.js",
    "pub": "node build.js"
  }
}

上述的scripts字段是一个对象,每个属性对应一段shell脚本

npm scripts的优点:

内置,不必安装其他依赖

npm scripts的缺点:

功能太简单,虽然提供了pre和post两个钩子,但不能方便地管理多个任务之间的依赖

2) grunt

grunt也是一个任务执行者.grunt有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化执行依赖的任务,每个任务的具体执行代码和依赖关系写在配置文件Gruntfile.js中:

module.exports = function(grunt) {
  // 所有插件的配置信息
  grunt.initConfig({
    // uglify 插件的配置信息
    uglify: {
      app_task: {
        files: {
          'build/app.min.js': ['lib/index.js', 'lib/test.js']
        }
      }
    },
    // watch 插件的配置信息
    watch: {
      another: {
          files: ['lib/*.js'],
      }
    }
  });

  // 告诉 grunt 我们将使用这些插件
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-watch');

  // 告诉grunt当我们在终端中启动 grunt 时需要执行哪些任务
  grunt.registerTask('dev', ['uglify','watch']);
};

在项目根目录下执行命令grunt dev就会启动JavaScript文件压缩和自动刷新功能

grunt的优点:
  • 灵活,只负责执行定义的任务
  • 大量可复用的插件封装好了常见的构建任务
grant的缺点:
  • 集成度不高,要写很多配置后才可以使用,无法做到开箱即可使用
  • grunt相当于进化版的npm script,它的诞生其实是为了弥补npm script的不足

3) gulp

gulp是一个基于流的自动化构建工具,除了可以管理和执行任务,还支持监听文件,读写文件.

gulp被设计得非常简单.

常用的5个方法:
  • gulp.task: 注册一个任务

  • gulp.run: 执行任务

  • gulp.watch监听文件变化

  • gulp.src: 读取文件

  • 通过gulp.dest: 写文件

gulp最大的特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递,大致作用如下:

// 引入 Gulp
var gulp = require('gulp'); 
// 引入插件
var jshint = require('gulp-jshint');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');

// 编译 SCSS 任务
gulp.task('sass', function() {
  // 读取文件通过管道喂给插件
  gulp.src('./scss/*.scss')
    // SCSS 插件把 scss 文件编译成 CSS 文件
    .pipe(sass())
    // 输出文件
    .pipe(gulp.dest('./css'));
});

// 合并压缩 JS
gulp.task('scripts', function() {
  gulp.src('./js/*.js')
    .pipe(concat('all.js'))
    .pipe(uglify())
    .pipe(gulp.dest('./dist'));
});

// 监听文件变化
gulp.task('watch', function(){
  // 当 scss 文件被编辑时执行 SCSS 任务
  gulp.watch('./scss/*.scss', ['sass']);
  gulp.watch('./js/*.js', ['scripts']);    
});
gulp的优点:

好用又灵活,既可以单独完成构建也可以和其他工具搭配使用

gulp的缺点:

和grunt类似,集成度不高,要写很多配置后才能使用,无法做到开箱可用

可以将gulp当做grunt的加强版,相对于grunt,gulp增加了监听文件,读写文件,流式处理的功能

4) Fis3

Fis3是一个来自百度的优秀国产构建工具。相对于 Grunt、Gulp 这些只提供基本功能的工具,Fis3 集成了 Web 开发中的常用构建功能,如下所述。

  • 读写文件:通过fis.match读文件,release配置文件输出路径。
  • 资源定位:解析文件之间的依赖关系和文件位置。
  • 文件指纹:通过useHash配置输出文件时给文件 URL 加上 md5 戳来优化浏览器缓存。
  • 文件编译:通过parser配置文件解析器做文件转换,例如把 ES6 编译成 ES5。
  • 压缩资源:通过optimizer配置代码压缩方法。
  • 图片合并:通过spriter配置合并 CSS 里导入的图片到一个文件来减少 HTTP 请求数。

大致使用如下:

// 加 md5
fis.match('*.{js,css,png}', {
  useHash: true
});

// fis3-parser-typescript 插件把 TypeScript 文件转换成 JavaScript 文件
fis.match('*.ts', {
  parser: fis.plugin('typescript')
});

// 对 CSS 进行雪碧图合并
fis.match('*.css', {
  // 给匹配到的文件分配属性 `useSprite`
  useSprite: true
});

// 压缩 JavaScript
fis.match('*.js', {
  optimizer: fis.plugin('uglify-js')
});

// 压缩 CSS
fis.match('*.css', {
  optimizer: fis.plugin('clean-css')
});

// 压缩图片
fis.match('*.png', {
  optimizer: fis.plugin('png-compressor')
});

可以看出Fis3很强大,内置了很多功能,无需做太多配置就能完成大量工作

Fis3的优点:

是集成了各种 Web 开发所需的构建功能,配置简单开箱即用。

Fis3的缺点:

是目前官方已经不再更新和维护,不支持最新版本的 Node.js。

Fis3 是一种专注于 Web 开发的完整解决方案,如果将 Grunt、Gulp 比作汽车的发动机,则可以将Fis3 比作一辆完整的汽车。

5) webpack

webpack是一个打包模块化的工具,在webpack眼中是一个个模块,这样的好处是能够清晰描述出各个模块之间的依赖关系,以方便webpack对模块进行组合和打包,经过webpack的处理,最终会输出浏览器能够使用的静态资源

webpack具有很大的灵活性,能够配置如何处理文件,比如:

module.exports = {
  // 所有模块的入口,Webpack 从入口开始递归解析出所有依赖的模块
  entry: './app.js',
  output: {
    // 把入口所依赖的所有模块打包成一个文件 bundle.js 输出 
    filename: 'bundle.js'
  }
}
webpack的优点:
  • 专注处理模块化的项目,能够做到开箱即用一步到位
  • 提供plugin扩展,完整好用不失灵活
  • 使用场景不仅限于web开发
  • 社区庞大活跃,经常引入紧跟时代发展的新特性,能够为大多数场景找到已有的开源扩展
  • 良好的开发体验
webpack的缺点:
  • 只能用于模块化开发的项目

6) Rollup

Rollup是一个和 Webpack 很类似但专注于 ES6 的模块打包工具。 Rollup 的亮点在于能针对 ES6 源码进行 Tree Shaking 以去除那些已被定义但没被使用的代码,以及 Scope Hoisting 以减小输出文件大小提升运行性能。 然而 Rollup 的这些亮点随后就被 Webpack 模仿和实现。 由于 Rollup 的使用和 Webpack 差不多,这里就不详细介绍如何使用了

rollup和webpack的差别:
  • Rollup 是在 Webpack 流行后出现的替代品;
  • Rollup 生态链还不完善,体验不如 Webpack;
  • Rollup 功能不如 Webpack 完善,但其配置和使用更加简单;
  • Rollup 不支持 Code Spliting,但好处是打包出来的代码中没有 Webpack 那段模块的加载、执行和缓存的代码。

Rollup 在用于打包 JavaScript 库时比 Webpack 更加有优势,因为其打包出来的代码更小更快。 但功能不够完善,很多场景都找不到现成的解决方案。

4.webpack

webpack是一个打包模块化JavaScript的工具,它会从main.js出发,识别出源码中的模块化导入语句,递归地查找入口文件的所有依赖,将入口和其他所有依赖打包到一个单独的文件中.从webpack2版本开始,webpack就已经内置了对es6,commonjs,amd模块化语句的支持

5.loader

1) loader机制的作用是什么

loader可以看作具有文件转换功能的翻译员,配置里面的module.rules数组配置了一组规则,告诉webpack在遇到哪些文件时使用哪些loader去加载和转换

2) css-loader与style-loader的作用

  • css-loader读取css文件
  • style-loader把css内容注入到JavaScript中

3) 配置loader时需要注意的地方

use属性的值需要的是一个由loader名称组成的数组,loader的执行顺序是由后往前的

每一个loader都可以通过url querystring的方式传入参数,例如css-loader?minimize中的minimize告诉css-loader需要开启css压缩

6.plugin

1) plugin作用是什么?

  • Plugin是用来扩展webpack功能的,通过在构建流程中注入钩子实现,它给webpack带来了很大的灵活性
  • webpack是通过Plugin属性来配置需要使用的插件列表的.plugins属性是一个数组,里面的每一项都是插件的一个实例,在实例化一个组件时可以通过构造函数传入这个组件支持的配置属性

2) ExtractTextPlugin插件的作用?

ExtractTextPlugin插件的作用是提取出JavaScript代码里的css到一个单独的文件

可以通过插件的filename属性,告诉插件输出的css名称是通过[name]_[content_hash:8].css字符串模板生成的,里面的[name]代表文件名称,[content_hash:8]代表根据文件内容算出的8位hash值,还有很多配置选项可以在ExtractTextPlugin的主页可以查到

7.DevServer

1) DevServer开发工具

DevServer会启动一个http服务器用于服务网页请求,同时会帮助启动webpack,并接webpack发出的文件变更信号,通过websocket协议自动刷新网页做到实时预览

安装DevServer:
npm i -D webpack-dev-server

2) 实时预览

  • webpack在启动时可以开启监听模式,开启监听模式后webpack会监听本地文件系统的变化,发生变化时重新构建出新的结果.webpack默认是关闭监听模式的,你可以在启动webpack时通过webpack --watch来开启监听模式
  • 通过DevServer启动的webpack会开启监听模式,当发生变化时重新执行完构建后通知DevServer. DevServer会让webpack在构建出的JavaScript代码中注入一个代理客户端用于控制网页,网页和DevServer之间通过websocket协议通信,以方便DevServer主动向客户端发送命令. DevServer在收到来自"webpack文件变化"通知时通过注入客户端控制网页刷新

3) 什么是模块热替换

  • 模块热替换能够做到在不重新加载整个网页的情况下,通过将被更新过的模块替换老的模块,再重新执行一次来实现实时预览
  • 模块热替换相对于默认的刷新机制能提供更快的响应和更好的开发体验.模块热替换默认是关闭的,要开启模块热替换,只需要在启动DevServer时带上--hot参数,重启DevServer后再去更新文件就能体验到模块热替换的神奇了

4) 什么是Source Map及其使用

Source Map能够提供将压缩文件恢复到源文件原始位置的映射代码的方式.这意味着你可以在优化压缩代码后轻松的进行调试.在编译器输出的代码上进行断点调试是一件辛苦和不优雅的事情,调试工具可以通过Source Map映射代码,让你在源代码上断点调试

Source Map使用:

webpack支持生成Source Map,只需要在启动时带上--devtool source-map参数,加上参数重启DevServer后刷新页面,再打开chrome浏览器的开发者工具,就可以在source栏中看到可调试的源代码了

8.webpack的核心概念

1) webpack的几个核心概念:

  • Entry: 入口,webpack执行构建的第一步将从Entry开始,可抽象成输入
  • Module: 模块,在webpack中一切都是模块,一个模块对应着一个文件.webpack会从配置的Entry开始递归找出所有依赖的模块
  • Chunk: 代码块, 一个Chunk由多个模块组合而成, 用于代码合并和分割
  • Loader: 模块转换器,用于把模块内容按照需求转换成新内容
  • Plugin: 扩展插件,在webpack构建流程中的特定时机注入扩展逻辑来改变构建结构或做你想要的事情
  • Output: 输出结果, 在webpack经过一系列处理并得出最终想要的代码后输出结果

2) webpack简单工作原理

  • webpack启动后会从Entry里配置的Module开始递归解析Entry依赖所有的Module;
  • 每找到一个Module,就会根据配置的loader去找出对应的转换规则,对Module进行转换后,再解析出当前Module依赖的Module;
  • 这些模块会以Entry为单位进行分组,一个Entry和其所有依赖的Module所有被分到一个组,就是Chunk;
  • 最后webpack会把Chunk转换成文件输出;
  • 在整个流程中webpack会在恰当的时机执行Plugin里定义的逻辑.

二.webpack配置

1.Entry

1) 什么是Entry

  • entry是配置模块的入口,可抽象成输入,webpack执行构建的第一步将从入口开始搜寻及递归解析出所有入口依赖的模块
  • entry配置是必填的,若不填则将导致webpack报错退出

2) 什么是context

webpack在寻找相对路默认路径的文件会以context为根目录, context默认为执行启动webpack时所在的当前工作目录.如果想改变context的默认配置,则可以在配置文件里这样设置它:

module.exports={
context: path.resolve(__dirname, 'app')
}

注意:

  • context必须是一个相对路径的字符串.除此之外,还可以通过在启动webpack时带上参数: webpack --context来设置context
  • 之所以先介绍context,因为entry的路径和其依赖的模块的路径可能采用相对于context得路径来描述,context会影响到这些相对路径所指向的真实文件

3) Entry改变类型有哪些

类型 示例 含义
string
'./app/entry'
入口模块的文件路径,可以是相对路径
array
['./app/entry1', './app/entry2']
入口模块的文件路径,可以是相对路径
object
{ a: './app/entry-a', b: ['./app/entry-b1', './app/entry-b2']}
配置多个入口,每个入口生成一个Chunk
注意: 如果是array类型,在搭配output.library配置项使用时,只在数组里的最后一个入口文件的模块会被导出

4) Chunk名称

webpack会为每个生成的Chunk取一个名称,Chunk的名称和Entry的配置有关:

  • 如果entry是一个string或array,就只会生成一个Chunk,这时Chunk的名称是main
  • 如果entry是一个object,就可能出现多个Chunk,这时Chunk的名称是object键值对里键的名称

5) 如何配置动态Entry

假如项目有多个页面需要为每个页面的入口配置一个entry,但这些页面的数量可能会不断增长,则这时entry的配置会受到其他因素的影响导致不能写成静态的值.其解决方法是把Entry设置成一个函数动态返回上面所说的配置,例如:

// 同步函数
entry: () => {
  return {
    a:'./pages/a',
    b:'./pages/b',
  }
};
// 异步函数
entry: () => {
  return new Promise((resolve)=>{
    resolve({
       a:'./pages/a',
       b:'./pages/b',
    });
  });
};

2.Output

output配置如何输出最终的代码.output是一个object,里面包含一系列配置项

3.Module

概念:

module配置如何处理模块

1) loader

rules配置模块的读取和解析规则,通常用来配置loader.其类型是一个数组,数组中的每一项都描述了如何去处理部分文件.配置一项rules大致可以通过以下方式:

  • 条件匹配: 通过test,include,exclude三个配置来命中loader要应用规则的文件
  • 应用规则: 对选中后的文件通过use配置项来应用loader,可以只应用一个loader或者安装
  • 重置规则: 一组loader的执行顺序默认是从右到左边执行,通过enforce选项可以让其中一个loader的执行顺序放到前面或者最后

eg:

module: {
  rules: [
    {
      // 命中 JavaScript 文件
      test: /\.js$/,
      // 用 babel-loader 转换 JavaScript 文件
      // ?cacheDirectory 表示传给 babel-loader 的参数,用于缓存 babel 编译结果加快重新编译速度
      use: ['babel-loader?cacheDirectory'],
      // 只命中src目录里的js文件,加快 Webpack 搜索速度
      include: path.resolve(__dirname, 'src')
    },
    {
      // 命中 SCSS 文件
      test: /\.scss$/,
      // 使用一组 Loader 去处理 SCSS 文件。
      // 处理顺序为从后到前,即先交给 sass-loader 处理,再把结果交给 css-loader 最后再给 style-loader。
      use: ['style-loader', 'css-loader', 'sass-loader'],
      // 排除 node_modules 目录下的文件
      exclude: path.resolve(__dirname, 'node_modules'),
    },
    {
      // 对非文本文件采用 file-loader 加载
      test: /\.(gif|png|jpe?g|eot|woff|ttf|svg|pdf)$/,
      use: ['file-loader'],
    },
  ]
}

在loader需要传入很多参数时,你还可以通过一个Object来描述,例如在上面的babel-loader配置中右如下代码:

use: [
  {
    loader:'babel-loader',
    options:{
      cacheDirectory:true,
    },
    // enforce:'post' 的含义是把该 Loader 的执行顺序放到最后
    // enforce 的值还可以是 pre,代表把 Loader 的执行顺序放到最前面
    enforce:'post'
  },
  // 省略其它 Loader
]

上述例子中test include exclude这三个命中文件的配置只传入一个字符串或正则,其实它们还都支持数组类型,使用如下:

{
  test:[
    /\.jsx?$/,
    /\.tsx?$/
  ],
  include:[
    path.resolve(__dirname, 'src'),
    path.resolve(__dirname, 'tests'),
  ],
  exclude:[
    path.resolve(__dirname, 'node_modules'),
    path.resolve(__dirname, 'bower_modules'),
  ]
}

数组中每项之间都是或的关系,即文件路径符合数组中的任何一个条件就会被命中

2) noParse配置

noParse配置可以让webpack忽略对部分没采用模块化的文件的递归解析和处理,这样做的好处是能够提高构建性能

原因是一些库,比如jquery,让webpack去解析这些文件耗时又没有意义

noparse是可选配置,类型需要是RegExp, [RegExp], function其中一个

例如想要忽略jquery,chartjs,可以:

// 使用正则表达式
noParse: /jquery|chartjs/

// 使用函数,从 Webpack 3.0.0 开始支持
noParse: (content)=> {
  // content 代表一个模块的文件路径
  // 返回 true or false
  return /jquery|chartjs/.test(content);
}

注意被忽略掉的文件里不应该包含import, require, define等模块语句, 不然会导致构建出的代码中包含无法在浏览器环境下执行的模块化语句.

3) parser配置

因为webpack是以模块化的JavaScript文件为入口,所以内置了对模块化JavaScript的解析功能,支持AMD,CommonJS,SystemJS,ES6.parser属性可以更细粒度的配置哪些模块语法要那些解析哪些不解析,和noParse配置的区别在于parser可精确到语法层面,而noParse只能控制哪些文件不被解析

eg:

module: {
  rules: [
    {
      test: /\.js$/,
      use: ['babel-loader'],
      parser: {
      amd: false, // 禁用 AMD
      commonjs: false, // 禁用 CommonJS
      system: false, // 禁用 SystemJS
      harmony: false, // 禁用 ES6 import/export
      requireInclude: false, // 禁用 require.include
      requireEnsure: false, // 禁用 require.ensure
      requireContext: false, // 禁用 require.context
      browserify: false, // 禁用 browserify
      requireJs: false, // 禁用 requirejs
      }
    },
  ]
}

4.Resolve

1) alias(路径映射)

resolve.alias配置项通过别名来把原导入路径映射成一个新的导入路径。例如使用以下配置:

// Webpack alias 配置
resolve:{
  alias:{
    components: './src/components/'
  }
}


当你通过import Button from 'components/button'导入时,实际上被alias等价替换成了import Button from './src/components/button'

以上 alias 配置的含义是把导入语句里的components关键字替换成./src/components/

这样做可能会命中太多的导入语句,alias 还支持$符号来缩小范围到只命中以关键字结尾的导入语句:

resolve:{
  alias:{
    'react$': '/path/to/react.min.js'
  }
}


react$只会命中以react结尾的导入语句,即只会把import 'react'关键字替换成import '/path/to/react.min.js'

2) mainFields(优先采用哪份代码)

有一些第三方模块会针对不同环境提供几份代码,例如分别提供采用es5和es6的2份代码,这2份代码的位置写在package.json文件里,如下:

{
  "jsnext:main": "es/index.js",// 采用 ES6 语法的代码入口文件
  "main": "lib/index.js" // 采用 ES5 语法的代码入口文件
}

webpack会根据mainFields的配置去决定优先采用哪一份代码:

mainFields: ['browser', 'main']

webpack会按照数组里的顺序去package.json文件中寻找,只会使用找到的第一个

如果你想优先采用es6的那份代码,可以:

mainFields: ['jsnext:main', 'browser', 'main']

3) extensions(后缀列表)

在导入语句没带语句后缀时,webpack会自动带上后缀去尝试访问文件是否存在.resolve.extensions用于配置在尝试过程中用到的后缀列表,默认是:

extensions: ['.js', '.json']

也就是说当遇到require('./data')这样的导入语句时,Webpack 会先去寻找./data.js文件,如果该文件不存在就去寻找./data.json文件, 如果还是找不到就报错。

假如你想让 Webpack 优先使用目录下的 TypeScript 文件,可以这样配置:

extensions: ['.ts', '.js', '.json']

4) modules

resolve.modules配置webpack去哪些目录下去寻找第三方模块,默认去node_modules寻找.

如果项目里会有一些模块大量被其他模块依赖和导入,由于其他模块的位置分布不定,针对不同的文件要去计算被导入模块文件的相对路径,这个路径有时候会很长,比如import '../../../components/button',可以利用modules配置优化

eg:

modules:['./src/components','node_modules']

配置后, 你可以简单通过import 'button'导入

5) descriptionFields

resolve.descriptionFields配置描述第三方模块的文件名称,也就是package.json

eg:

descriptionFiles: ['package.json']

6) enforceExtension

resolve.enforceExtension如果配置为true所有导入语句都必须要带文件后缀,例如开启前import './foo'能够正常工作,开启后就必须写成import './foo.js'

7) enforceModuleExtension

  • enforceModuleExtensionenforceExtension作用类似
  • enforceModuleExtension只对node_modules下的模块生效。
  • enforceModuleExtension通常搭配enforceExtension使用,在enforceExtension:true时,因为安装的第三方模块中大多数导入语句没带文件后缀, 所以这时通过配置enforceModuleExtension:false来兼容第三方模块。

5.Plugin

1) 什么是Plugin:

Plugin用于扩展webpack功能,各种各样的Plugin几乎让webpack可以做任何构建相关的事情

2) Plugin配置:

Plugin的配置很简单,Plugins配置项接受一个数组,数组每一项都是一个要使用的Plugin 实例,Plugin需要的参数通过构造函数传入

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

module.exports = {
  plugins: [
    // 所有页面都会用到的公共代码提取到 common 代码块中
    new CommonsChunkPlugin({
      name: 'common',
      chunks: ['a', 'b']
    }),
  ]
};

6.DevServer

概念:

  • 它提供了一些配置项可以改变 DevServer 的默认行为。
  • 要配置 DevServer ,除了在配置文件里通过devServer传入参数外,还可以通过命令行参数传入。
  • 注意只有在通过 DevServer 去启动 Webpack 时配置文件里devServer才会生效,因为这些参数所对应的功能都是 DevServer 提供的,Webpack 本身并不认识devServer配置项。

1) hot(热替换)

devServer.hot配置是否启用使用DevServer中提到的模块热替换功能。 DevServer 默认的行为是在发现源代码被更新后会通过自动刷新整个页面来做到实时预览,开启模块热替换功能后将在不刷新整个页面的情况下通过用新模块替换老模块来做到实时预览。

2) inline(是否通过代理客户端控制网页刷新)

DevServer 的实时预览功能依赖一个注入到页面里的代理客户端去接受来自 DevServer 的命令和负责刷新网页的工作。devServer.inline用于配置是否自动注入这个代理客户端到将运行在页面里的 Chunk 里去,默认是会自动注入。 DevServer 会根据你是否开启inline来调整它的自动刷新策略:

  • 如果开启inline,DevServer 会在构建完变化后的代码时通过代理客户端控制网页刷新。
  • 如果关闭inline,DevServer 将无法直接控制要开发的网页。这时它会通过 iframe 的方式去运行要开发的网页,当构建完变化后的代码时通过刷新 iframe 来实现实时预览。 但这时你需要去http://localhost:8080/webpack-dev-server/实时预览你的网页了。

如果你想使用 DevServer 去自动刷新网页实现实时预览,最方便的方法是直接开启inline

3) historyApiFallback(针对命中路由返回html文件)

devServer.historyApiFallback用于方便的开发使用了HTML5 History API的单页应用。 这类单页应用要求服务器在针对任何命中的路由时都返回一个对应的 HTML 文件,例如在访问http://localhost/userhttp://localhost/home时都返回index.html文件, 浏览器端的 JavaScript 代码会从 URL 里解析出当前页面的状态,显示出对应的界面。

配置historyApiFallback最简单的做法是:

historyApiFallback: true


这会导致任何请求都会返回index.html文件,这只能用于只有一个 HTML 文件的应用。

如果你的应用由多个单页应用组成,这就需要 DevServer 根据不同的请求来返回不同的 HTML 文件,配置如下:

historyApiFallback: {
  // 使用正则匹配命中路由
  rewrites: [
    // /user 开头的都返回 user.html
    { from: /^\/user/, to: '/user.html' },
    { from: /^\/game/, to: '/game.html' },
    // 其它的都返回 index.html
    { from: /./, to: '/index.html' },
  ]
}

4) contentBase(配置DevServer http服务器的文件根目录)

devServer.contentBase配置 DevServer HTTP 服务器的文件根目录。 默认情况下为当前执行目录,通常是项目根目录,所有一般情况下你不必设置它,除非你有额外的文件需要被 DevServer 服务。 例如你想把项目根目录下的public目录设置成 DevServer 服务器的文件根目录,你可以这样配置:

devServer:{
  contentBase: path.join(__dirname, 'public')
}

这里需要指出可能会让你疑惑的地方,DevServer 服务器通过 HTTP 服务暴露出的文件分为两类:

  • 暴露本地文件。
  • 暴露 Webpack 构建出的结果,由于构建出的结果交给了 DevServer,所以你在使用了 DevServer 时在本地找不到构建出的文件。

contentBase只能用来配置暴露本地文件的规则,你可以通过contentBase:false来关闭暴露本地文件。

5) headers(响应头)

devServer.headers配置项可以在 HTTP 响应中注入一些 HTTP 响应头,使用如下:

devServer:{
  headers: {
    'X-foo':'bar'
  }
}

6) host(配置DevServer的服务监听地址)

devServer.host配置项用于配置 DevServer 服务监听的地址。 例如你想要局域网中的其它设备访问你本地的服务,可以在启动 DevServer 时带上--host 0.0.0.0host的默认值是127.0.0.1即只有本地可以访问 DevServer 的 HTTP 服务。

7) port(配置DevServer的服务监听的端口)

devServer.port配置项用于配置 DevServer 服务监听的端口,默认使用 8080 端口。 如果 8080 端口已经被其它程序占有就使用 8081,如果 8081 还是被占用就使用 8082,以此类推。

8) allowHosts(http请求的host白名单列表)

allowedHosts: [
  // 匹配单个域名
  'host.com',
  'sub.host.com',
  // host2.com 和所有的子域名 *.host2.com 都将匹配
  '.host2.com'
]

9) disableHostCheck(是否关闭请求的host检查)

devServer.disableHostCheck配置项用于配置是否关闭用于 DNS 重绑定的 HTTP 请求的 HOST 检查。 DevServer 默认只接受来自本地的请求,关闭后可以接受来自任何 HOST 的请求。 它通常用于搭配--host 0.0.0.0使用,因为你想要其它设备访问你本地的服务,但访问时是直接通过 IP 地址访问而不是 HOST 访问,所以需要关闭 HOST 检查。

10) https(是否使用https服务)

DevServer 默认使用 HTTP 协议服务,它也能通过 HTTPS 协议服务。 有些情况下你必须使用 HTTPS,例如 HTTP2 和 Service Worker 就必须运行在 HTTPS 之上。 要切换成 HTTPS 服务,最简单的方式是:

devServer:{
  https: true
}


DevServer 会自动的为你生成一份 HTTPS 证书。

如果你想用自己的证书可以这样配置:

devServer:{
  https: {
    key: fs.readFileSync('path/to/server.key'),
    cert: fs.readFileSync('path/to/server.crt'),
    ca: fs.readFileSync('path/to/ca.pem')
  }
}

11) clientLogLevel(客户端日志等级)

devServer.clientLogLevel配置在客户端的日志等级,这会影响到你在浏览器开发者工具控制台里看到的日志内容。clientLogLevel是枚举类型,可取如下之一的值none | error | warning | info。 默认为info级别,即输出所有类型的日志,设置成none可以不输出任何日志。

12) compress(是否启用gzip压缩)

devServer.compress配置是否启用 gzip 压缩。boolean为类型,默认为false

13) open( DevServer 启动且第一次构建完时自动去打开web开发的页面)

devServer.open用于在 DevServer 启动且第一次构建完时自动用你系统上默认的浏览器去打开要开发的网页。 同时还提供devServer.openPage配置项用于打开指定 URL 的网页。 

14) 代理配置WebSocket

devServer: {
        host: '0.0.0.0',
        port: 8112,
        hot: true,
        // sockHost: process.env.WDS_SOCKET_HOST, // WebSocket 主机地址
        // sockPort: process.env.WDS_SOCKET_PORT, // WebSocket 端口号
        sockHost: '192.168.137.12', // WebSocket 主机地址
        sockPort: '9002', // WebSocket 端口号
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩
        // 设置代理
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': {
                target: 'http://127.0.0.1:8000',
                changeOrigin: true,
                ws: true,
                headers: {'X-Real-IP': '1.1.1.1'},
                pathRewrite: {
                  '^/api': ''
                }
            },
        }
    }

7.其他配置

Target:

avaScript 的应用场景越来越多,从浏览器到 Node.js,这些运行在不同环境的 JavaScript 代码存在一些差异。target配置项可以让 Webpack 构建出针对不同运行环境的代码。target可以是以下之一:

target值 描述
web 针对浏览器**(默认)**,所有代码都集中在一个文件里
node 针对 Node.js,使用require语句加载 Chunk 代码
async-node 针对 Node.js,异步加载 Chunk 代码
webworker 针对 WebWorker
electron-main 针对Electron主线程
electron-renderer 针对 Electron 渲染线程

例如当你设置target:'node'时,源代码中导入 Node.js 原生模块的语句require('fs')将会被保留,fs模块的内容不会打包进 Chunk 里。

Devtools:

devtool配置 Webpack 如何生成 Source Map,默认值是false即不生成 Source Map,想为构建出的代码生成 Source Map 以方便调试,可以这样配置:

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

watch和watchOptions:

前面介绍过 Webpack 的监听模式,它支持监听文件更新,在文件发生变化时重新编译。在使用 Webpack 时监听模式默认是关闭的,想打开需要如下配置:

module.export = {
  watch: true
}


在使用 DevServer 时,监听模式默认是开启的。

除此之外,Webpack 还提供了watchOptions配置项去更灵活的控制监听模式,使用如下:

module.export = {
  // 只有在开启监听模式时,watchOptions 才有意义
  // 默认为 false,也就是不开启
  watch: true,
  // 监听模式运行时的参数
  // 在开启监听模式时,才有意义
  watchOptions: {
    // 不监听的文件或文件夹,支持正则匹配
    // 默认为空
    ignored: /node_modules/,
    // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
    // 默认为 300ms  
    aggregateTimeout: 300,
    // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
    // 默认每隔1000毫秒询问一次
    poll: 1000
  }
}

externals:

Externals 用来告诉 Webpack 要构建的代码中使用了哪些不用被打包的模块,也就是说这些模版是外部环境提供的,Webpack 在打包时可以忽略它们。

有些 JavaScript 运行环境可能内置了一些全局变量或者模块,例如在你的 HTML HEAD 标签里通过以下代码:

<script src="path/to/jquery.js"></script>


引入 jQuery 后,全局变量jQuery就会被注入到网页的 JavaScript 运行环境里。

如果想在使用模块化的源代码里导入和使用 jQuery,可能需要这样:

import $ from 'jquery';
$('.my-element');


构建后你会发现输出的 Chunk 里包含的 jQuery 库的内容,这导致 jQuery 库出现了2次,浪费加载流量,最好是 Chunk 里不会包含 jQuery 库的内容。

Externals 配置项就是为了解决这个问题。

通过externals可以告诉 Webpack JavaScript 运行环境已经内置了那些全局变量,针对这些全局变量不用打包进代码中而是直接使用全局变量。 要解决以上问题,可以这样配置externals

module.export = {
  externals: {
    // 把导入语句里的 jquery 替换成运行环境里的全局变量 jQuery
    jquery: 'jQuery'
  }
}

resolveLoader:

ResolveLoader 用来告诉 Webpack 如何去寻找 Loader,因为在使用 Loader 时是通过其包名称去引用的, Webpack 需要根据配置的 Loader 包名去找到 Loader 的实际代码,以调用 Loader 去处理源文件。

ResolveLoader 的默认配置如下:

module.exports = {
  resolveLoader:{
    // 去哪个目录下寻找 Loader
    modules: ['node_modules'],
    // 入口文件的后缀
    extensions: ['.js', '.json'],
    // 指明入口文件位置的字段
    mainFields: ['loader', 'main']
  }
}

该配置项常用于加载本地的 Loader。

8.如何整体配置结构

const path = require('path');

module.exports = {
  // entry 表示 入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  // 类型可以是 string | object | array   
  entry: './app/entry', // 只有1个入口,入口只有1个文件
  entry: ['./app/entry1', './app/entry2'], // 只有1个入口,入口有2个文件
  entry: { // 有2个入口
    a: './app/entry-a',
    b: ['./app/entry-b1', './app/entry-b2']
  },

  // 如何输出结果:在 Webpack 经过一系列处理后,如何输出最终想要的代码。
  output: {
    // 输出文件存放的目录,必须是 string 类型的绝对路径。
    path: path.resolve(__dirname, 'dist'),

    // 输出文件的名称
    filename: 'bundle.js', // 完整的名称
    filename: '[name].js', // 当配置了多个 entry 时,通过名称模版为不同的 entry 生成不同的文件名称
    filename: '[chunkhash].js', // 根据文件内容 hash 值生成文件名称,用于浏览器长时间缓存文件

    // 发布到线上的所有资源的 URL 前缀,string 类型
    publicPath: '/assets/', // 放到指定目录下
    publicPath: '', // 放到根目录下
    publicPath: 'https://cdn.example.com/', // 放到 CDN 上去

    // 导出库的名称,string 类型
    // 不填它时,默认输出格式是匿名的立即执行函数
    library: 'MyLibrary',

    // 导出库的类型,枚举类型,默认是 var
    // 可以是 umd | umd2 | commonjs2 | commonjs | amd | this | var | assign | window | global | jsonp ,
    libraryTarget: 'umd', 

    // 是否包含有用的文件路径信息到生成的代码里去,boolean 类型
    pathinfo: true, 

    // 附加 Chunk 的文件名称
    chunkFilename: '[id].js',
    chunkFilename: '[chunkhash].js',

    // JSONP 异步加载资源时的回调函数名称,需要和服务端搭配使用
    jsonpFunction: 'myWebpackJsonp',

    // 生成的 Source Map 文件名称
    sourceMapFilename: '[file].map',

    // 浏览器开发者工具里显示的源码模块名称
    devtoolModuleFilenameTemplate: 'webpack:///[resource-path]',

    // 异步加载跨域的资源时使用的方式
    crossOriginLoading: 'use-credentials',
    crossOriginLoading: 'anonymous',
    crossOriginLoading: false,
  },

  // 配置模块相关
  module: {
    rules: [ // 配置 Loader
      {  
        test: /\.jsx?$/, // 正则匹配命中要使用 Loader 的文件
        include: [ // 只会命中这里面的文件
          path.resolve(__dirname, 'app')
        ],
        exclude: [ // 忽略这里面的文件
          path.resolve(__dirname, 'app/demo-files')
        ],
        use: [ // 使用那些 Loader,有先后次序,从后往前执行
          'style-loader', // 直接使用 Loader 的名称
          {
            loader: 'css-loader',      
            options: { // 给 html-loader 传一些参数
            }
          }
        ]
      },
    ],
    noParse: [ // 不用解析和处理的模块
      /special-library\.js$/  // 用正则匹配
    ],
  },

  // 配置插件
  plugins: [
  ],

  // 配置寻找模块的规则
  resolve: { 
    modules: [ // 寻找模块的根目录,array 类型,默认以 node_modules 为根目录
      'node_modules',
      path.resolve(__dirname, 'app')
    ],
    extensions: ['.js', '.json', '.jsx', '.css'], // 模块的后缀名
    alias: { // 模块别名配置,用于映射模块
       // 把 'module' 映射 'new-module',同样的 'module/path/file' 也会被映射成 'new-module/path/file'
      'module': 'new-module',
      // 使用结尾符号 $ 后,把 'only-module' 映射成 'new-module',
      // 但是不像上面的,'module/path/file' 不会被映射成 'new-module/path/file'
      'only-module$': 'new-module', 
    },
    alias: [ // alias 还支持使用数组来更详细的配置
      {
        name: 'module', // 老的模块
        alias: 'new-module', // 新的模块
        // 是否是只映射模块,如果是 true 只有 'module' 会被映射,如果是 false 'module/inner/path' 也会被映射
        onlyModule: true, 
      }
    ],
    symlinks: true, // 是否跟随文件软链接去搜寻模块的路径
    descriptionFiles: ['package.json'], // 模块的描述文件
    mainFields: ['main'], // 模块的描述文件里的描述入口的文件的字段名称
    enforceExtension: false, // 是否强制导入语句必须要写明文件后缀
  },

  // 输出文件性能检查配置
  performance: { 
    hints: 'warning', // 有性能问题时输出警告
    hints: 'error', // 有性能问题时输出错误
    hints: false, // 关闭性能检查
    maxAssetSize: 200000, // 最大文件大小 (单位 bytes)
    maxEntrypointSize: 400000, // 最大入口文件大小 (单位 bytes)
    assetFilter: function(assetFilename) { // 过滤要检查的文件
      return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
    }
  },

  devtool: 'source-map', // 配置 source-map 类型

  context: __dirname, // Webpack 使用的根目录,string 类型必须是绝对路径

  // 配置输出代码的运行环境
  target: 'web', // 浏览器,默认
  target: 'webworker', // WebWorker
  target: 'node', // Node.js,使用 `require` 语句加载 Chunk 代码
  target: 'async-node', // Node.js,异步加载 Chunk 代码
  target: 'node-webkit', // nw.js
  target: 'electron-main', // electron, 主线程
  target: 'electron-renderer', // electron, 渲染线程

  externals: { // 使用来自 JavaScript 运行环境提供的全局变量
    jquery: 'jQuery'
  },

  stats: { // 控制台输出日志控制
    assets: true,
    colors: true,
    errors: true,
    errorDetails: true,
    hash: true,
  },

  devServer: { // DevServer 相关的配置
    proxy: { // 代理到后端服务接口
      '/api': 'http://localhost:3000'
    },
    contentBase: path.join(__dirname, 'public'), // 配置 DevServer HTTP 服务器的文件根目录
    compress: true, // 是否开启 gzip 压缩
    historyApiFallback: true, // 是否开发 HTML5 History API 网页
    hot: true, // 是否开启模块热替换功能
    https: false, // 是否开启 HTTPS 模式
    },

    profile: true, // 是否捕捉 Webpack 构建的性能信息,用于分析什么原因导致构建性能不佳

    cache: false, // 是否启用缓存提升构建速度

    watch: true, // 是否开始
    watchOptions: { // 监听模式选项
    // 不监听的文件或文件夹,支持正则匹配。默认为空
    ignored: /node_modules/,
    // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
    // 默认为300ms 
    aggregateTimeout: 300,
    // 判断文件是否发生变化是不停的去询问系统指定文件有没有变化,默认每隔1000毫秒询问一次
    poll: 1000
  },
}

9.判断如何配置webpack

  • 让源文件加入到构建流程中去被webpack控制,配置entry
  • 自定义输出文件的位置和名称,配置output
  • 自定义寻找依赖模块的策略,配置resolve
  • 自定义解析和转换文件的策略,配置module,通常配置module.rules中的loader
  • 其他大部分需求可能需要通过Plugin去实现,配置plugin

三.webpack实战

1.如何接入es6

1) 为什么es6要转换为es5(兼容性)

虽然目前部分浏览器和 Node.js 已经支持 ES6,但由于它们对 ES6 所有的标准支持不全,这导致在开发中不敢全面地使用 ES6。

2) babel有什么作用(js编译器)

Babel 是一个 JavaScript 编译器,能将 ES6 代码转为 ES5 代码,让你使用最新的语言特性而不用担心兼容性问题,并且可以通过插件机制根据需求灵活的扩展。

3) babel有什么属性和作用

  • plugins属性告诉babel要使用哪些插件,插件可以控制如何转换代码
  • presets属性告诉babel要转换的源码都使用了哪些新语法特性,一个presets对一组新语法通过支持,多个presets可以叠加

4) 如何接入babel

babel做的事情是转换代码,通过loader去接入babel:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
      },
    ]
  },
  // 输出 source-map 方便直接调试 ES6 源码
  devtool: 'source-map'
};

配置命中了项目目录下所有的JavaScript文件,通过babel-loader调用babel完成转换工作.在重新执行构建前,需要先安装新引入的依赖:

# Webpack 接入 Babel 必须依赖的模块
npm i -D babel-core babel-loader 
# 根据你的需求选择不同的 Plugins 或 Presets
npm i -D babel-preset-env

2.如何接入scss

SCSS的优点?

方便地管理代码,抽离公共的部分,通过逻辑写出更灵活的代码

如何接入SCSS?

最适合的方式是使用 Loader,Webpack 官方提供了对应的sass-loader

Webpack 接入 sass-loader 相关配置如下:

module.exports = {
  module: {
    rules: [
      {
        // 增加对 SCSS 文件的支持
        test: /\.scss$/,
        // SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
    ]
  },
};

webpack处理SCSS流程?

  • 通过 sass-loader 把 SCSS 源码转换为 CSS 代码,再把 CSS 代码交给 css-loader 去处理。
  • css-loader 会找出 CSS 代码中的@importurl()这样的导入语句,告诉 Webpack 依赖这些资源。同时还支持 CSS Modules、压缩 CSS 等功能。处理完后再把结果交给 style-loader 去处理。
  • style-loader 会把 CSS 代码转换成字符串后,注入到 JavaScript 代码中去,通过 JavaScript 去给 DOM 增加样式。如果你想把 CSS 代码提取到一个单独的文件而不是和 JavaScript 混在一起,可以使用ExtractTextPlugin。

3.React的使用

要在使用 Babel 的项目中接入 React 框架是很简单的,只需要加入 React 所依赖的 Presets babel-preset-react。
通过以下命令:

# 安装 React 基础依赖
npm i -D react react-dom
# 安装 babel 完成语法转换所需依赖
npm i -D babel-preset-react


安装新的依赖后,再修改.babelrc配置文件加入 React Presets

"presets": [
    "react"
],


就完成了一切准备工作。

再修改main.js文件如下:

import * as React from 'react';
import { Component } from 'react';
import { render } from 'react-dom';

class Button extends Component {
  render() {
    return <h1>Hello,Webpack</h1>
  }
}

render(<Button/>, window.document.getElementById('app'));


重新执行构建打开网页你将会发现由 React 渲染出来的Hello,Webpack

4.vue的使用

如何接入 Vue 框架?

目前最成熟和流行的开发 Vue 项目的方式是采用 ES6 加 Babel 转换Vue 官方提供了对应的vue-loader可以非常方便的完成单文件组件的转换。

修改 Webpack 相关配置如下:

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


安装新引入的依赖:

# Vue 框架运行需要的库
npm i -S vue
# 构建所需的依赖
npm i -D vue-loader css-loader vue-template-compiler

所装依赖的作用?

  • vue-loader:解析和转换.vue文件,提取出其中的逻辑代码script、样式代码style、以及 HTML 模版template,再分别把它们交给对应的 Loader 去处理。
  • css-loader:加载由vue-loader提取出的 CSS 代码。
  • vue-template-compiler:把vue-loader提取出的 HTML 模版编译成对应的可执行的 JavaScript 代码,这和 React 中的 JSX 语法被编译成 JavaScript 代码类似。预先编译好 HTML 模版相对于在浏览器中再去编译 HTML 模版的好处在于性能更好。

5.使用angular2框架

由于 Angular2 项目中采用了注解的语法,而且@angular/platform-browser源码中有许多 DOM 操作,配置需要修改为如下:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "sourceMap": true,
    // 开启对 注解 的支持
    "experimentalDecorators": true,
    // Angular2 依赖新的 JavaScript API 和 DOM 操作
    "lib": [
      "es2015",
      "dom"
    ]
  },
  "exclude": [
    "node_modules/*"
  ]
}

6.为单页面应用生成html

1) 单页面常见问题?

一个页面往往有很多模块需要加载:

  • 项目采用 ES6 语言加 React 框架。
  • 给页面加入Google Analytics,这部分代码需要内嵌进 HEAD 标签里去。
  • 给页面加入组件内容,这部分代码需要异步加载以提升首屏加载速度。
  • 压缩和分离 JavaScript 和 CSS 代码,提升加载速度。

一般情况下部分代码被内嵌进了 HTML 的 HEAD 标签中,部分文件的文件名称被打上根据文件内容算出的 Hash 值,并且加载这些文件的 URL 地址也被正常的注入到了 HTML 中。 如果你还采用手写index.html文件去完成以上要求,这就会使工作变得复杂、易错,项目难以维护。

2) 如何解决上述问题?

使用插件: web-webpack-plugin

webpack配置:

const path = require('path');
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const { WebPlugin } = require('web-webpack-plugin');

module.exports = {
  entry: {
    app: './main.js'// app 的 JavaScript 执行入口文件
  },
  output: {
    filename: '[name]_[chunkhash:8].js',// 给输出的文件名称加上 Hash 值
    path: path.resolve(__dirname, './dist'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
        // 排除 node_modules 目录下的文件,
        // 该目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
        exclude: path.resolve(__dirname, 'node_modules'),
      },
      {
        test: /\.css$/,// 增加对 CSS 文件的支持
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader?minimize'] // 压缩 CSS 代码
        }),
      },
    ]
  },
  plugins: [
    // 使用本文的主角 WebPlugin,一个 WebPlugin 对应一个 HTML 文件
    new WebPlugin({
      template: './template.html', // HTML 模版文件所在的文件路径
      filename: 'index.html' // 输出的 HTML 的文件名称
    }),
    new ExtractTextPlugin({
      filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 Hash 值
    }),
    new DefinePlugin({
      // 定义 NODE_ENV 环境变量为 production,以去除源码中只有开发时才需要的部分
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
    // 压缩输出的 JavaScript 代码
    new UglifyJsPlugin({
      // 最紧凑的输出
      beautify: false,
      // 删除所有的注释
      comments: false,
      compress: {
        // 在UglifyJs删除没有用到的代码时不输出警告
        warnings: false,
        // 删除所有的 `console` 语句,可以兼容ie浏览器
        drop_console: true,
        // 内嵌定义了但是只用到一次的变量
        collapse_vars: true,
        // 提取出出现多次但是没有定义成变量去引用的静态值
        reduce_vars: true,
      }
    }),
  ],
};

以上配置:

  • 增加对css文件的支持,提取出Chunk中的css代码到单独的文件中,压缩css文件
  • 定义NODE_ENV环境变量为production,以去除源码中只有开发时才需要的部分
  • 给输出文件名称加上hash值
  • 压缩输出的JavaScript代码

最核心的部分在于plugins里面:

7.离线应用

问题一:什么是离线应用?

离线应用是指通过离线缓存技术,让资源在第一次被加载后缓存在本地,下次访问它时就直接返回本地的文件,就算没有网络连接。

问题二:离线应用优点?

  • 在没有网络的情况下也能打开网页。
  • 由于部分被缓存的资源直接从本地加载,对用户来说可以加速网页加载速度,对网站运营者来说可以减少服务器压力以及传输流量费用。

问题三:离线应用核心是什么?

离线应用的核心是离线缓存技术,历史上曾先后出现2种离线离线缓存技术,它们分别是:

  • AppCache又叫 Application Cache,目前已经从 Web 标准中删除,请尽量不要使用它。
  • Service Workers是目前最新的离线缓存技术,是Web Worker的一部分。 它通过拦截网络请求实现离线缓存,比 AppCache 更加灵活。它也是构建PWA应用的关键技术之一。

问题四:什么是 Service Workers?

Service Workers是一个在浏览器后台运行的脚本,它生命周期完全独立于网页。它无法直接访问 DOM,但可以通过 postMessage 接口发送消息来和 UI 进程通信。 拦截网络请求是 Service Workers 的一个重要功能,通过它能完成离线缓存、编辑响应、过滤响应等功能。

问题五:Service Workers兼容性?

目前 Chrome、Firefox、Opera 都已经全面支持 Service Workers,但对于移动端浏览器就不太乐观了,只有高版本的 Android 支持。 由于 Service Workers 无法通过注入 polyfill 去实现兼容,所以在你打算使用它前请先调查清楚你的网页的运行场景。

判断浏览器是否支持 Service Workers 的最简单的方法是通过以下代码:

// 如果 navigator 对象上存在 serviceWorker 对象,就表示支持
if (navigator.serviceWorker) {
  // 通过 navigator.serviceWorker 使用
}

问题六:如何注册Service Workers?

要给网页接入 Service Workers,需要在网页加载后注册一个描述 Service Workers 逻辑的脚本。 代码如下:

if (navigator.serviceWorker) {
  window.addEventListener('DOMContentLoaded',function() {
    // 调用 serviceWorker.register 注册,参数 /sw.js 为脚本文件所在的 URL 路径
      navigator.serviceWorker.register('/sw.js');
  });
}


一旦这个脚本文件被加载,Service Workers 的安装就开始了。这个脚本被安装到浏览器中后,就算用户关闭了当前网页,它仍会存在。 也就是说第一次打开该网页时 Service Workers 的逻辑不会生效,因为脚本还没有被加载和注册,但是以后再次打开该网页时脚本里的逻辑将会生效。

在 Chrome 中可以通过打开网址chrome://inspect/#service-workers来查看当前浏览器中所有注册了的 Service Workers。

8.检查代码

问题一:代码检查具体是做什么?

检查代码主要检查以下几项:

  • 代码风格:让项目成员强制遵守统一的代码风格,例如如何缩进、如何写注释等,保障代码可读性,不把时间浪费在争论如何写代码更好看上;
  • 潜在问题:分析出代码在运行过程中可能出现的潜在 Bug。

问题二:怎么做代码检查?

在做代码风格检查时需要按照不同的文件类型来检查,下面来分别介绍。

检查 JavaScript

目前最常用的 JavaScript 检查工具是ESlint,它不仅内置了大量常用的检查规则,还可以通过插件机制做到灵活扩展。

结合Webpack

eslint-loader可以方便的把 ESLint 整合到 Webpack 中,使用方法如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // node_modules 目录的下的代码不用检查
        exclude: /node_modules/,
        loader: 'eslint-loader',
        // 把 eslint-loader 的执行顺序放到最前面,防止其它 Loader 把处理后的代码交给 eslint-loader 去检查
        enforce: 'pre',
      },
    ],
  },
}


接入 eslint-loader 后就能在控制台中看到 ESLint 输出的错误日志了。

检查 TypeScript

TSLint是一个和 ESlint 相似的 TypeScript 代码检查工具,区别在于 TSLint 只专注于检查 TypeScript 代码

结合Webpack

tslint-loader是一个和 eslint-loader 相似的 Webpack Loader, 能方便的把 TSLint 整合到 Webpack,其使用方法如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.ts$/,
        // node_modules 目录的下的代码不用检查
        exclude: /node_modules/,
        loader: 'tslint-loader',
        // 把 tslint-loader 的执行顺序放到最前面,防止其它 Loader 把处理后的代码交给 tslint-loader 去检查
        enforce: 'pre',
      },
    ],
  },
}

检查 CSS

stylelint是目前最成熟的 CSS 检查工具,内置了大量检查规则的同时也提供插件机制让用户自定义扩展。 stylelint 基于 PostCSS,能检查任何 PostCSS 能解析的代码,诸如 SCSS、Less 等。

结合Webpack

StyleLintPlugin能把 stylelint 整合到 Webpack,其使用方法很简单,如下:

const StyleLintPlugin = require('stylelint-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    new StyleLintPlugin(),
  ],
}

问题三:代码检查功能整合到 Webpack 中导致的问题和解决方法?

把代码检查功能整合到 Webpack 中会导致以下问题:

  • 由于执行检查步骤计算量大,整合到 Webpack 中会导致构建变慢;
  • 在整合代码检查到 Webpack 后,输出的错误信息
上一篇:Android 中的串口开发


下一篇:nginx中的HTTP 负载均衡