webpack的使用及优化

# 简介

webpack是一个模块打包工具,可以使用它管理项目中的模块依赖,并编译输出模块所需的静态文件。它可以很好地管理、打包开发中所用到的HTML,CSS,JavaScript和静态文件(图片,字体)等,打包成一个文件,减少服务器压力和下载宽带,让开发更高效。对于不同类型的依赖,webpack有对应的模块加载器,而且会分析模块间的依赖关系,最后合并生成优化的静态资源

Webpack 最主要的目的就是为了将所有小文件打包成一个或多个大文件,官网的图片很好的诠释了这个事情,除此之外,Webpack 也是一个能让你使用各种前端新技术的工具。webpack是把项目当作一个整体,通过一个给定的的主文件,webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包成一个或多个浏览器可识别的js文件

# webpack与gulp、grunt的区别

Grunt:大量现成的插件封装了常见的任务(Task),也能管理任务之间的依赖关系,自动化执行依赖的任务 Gulp:是一个基于流(Stream)的自动化构建工具。 除了可以管理和执行任务,还支持监听文件、读写文件。 Gulp 的最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递。Gulp 的优点是好用又不失灵活,既可以单独完成构建也可以和其它工具搭配使用。Gulp 被设计得非常简单,只通过下面5个方法就可以胜任几乎所有构建场景:【WRTSD】

  • 通过 gulp.task 注册一个任务;
  • 通过 gulp.run 执行任务;
  • 通过 gulp.watch 监听文件变化;
  • 通过 gulp.src 读取文件;
  • 通过 gulp.dest 写文件。

Webpack:webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能

总括

  • 从构建思路来说 gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系; webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工;
  • 对于知识背景来说 gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路

# 与其他类似工具比较

从应用场景上来看:

  • webpack适用于大型复杂的前端站点构建;

  • rollup适用于基础库的打包,如vue、react;

  • parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果;

    由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel

# 基本功能(构建作用)

构建工具就是将源代码转换成可执行的 JavaScript、CSS、HTML 代码,包括以下内容:

  • 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等等;
  • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等;
  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载;
  • 模块合并:在采用模块化的项目有很多模块和文件,需要构建功能把模块分类合并成一个文件;
  • 自动刷新:监听本地源代码的变化,自动构建,刷新浏览器;
  • 代码校验:在代码被提交到仓库前需要检测代码是否符合规范,以及单元测试是否通过;
  • 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统;

# webpack原理

# 4个核心概念【EOLP】

  • 入口(entry): 告诉webpack要使用哪个模块作为构建项目的起点,可抽象成输入, 默认为./src/index.js;

  • 输出(output):告诉webpack在哪里输出它打包好的代码以及如何命名,默认为./dist;

  • loader:告诉webpack如何转换某一类型的文件,并且引入到打包出的文件中;

    有两个属性:

    • test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
    • use 属性,表示进行转换时,应该使用哪个 loader。
  • 插件(plugins):plugin 从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务;

# 4个专用名词【RMCB】

  • Resolve:配置寻找模块的规则;
  • Module(模块):是开发中的单个模块;在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk(代码块):是webpack在进行模块的依赖分析的时候,代码分割出来的代码块。一个 Chunk 由多个模块组合而成,用于代码合并与分割; coding split的产物,我们可以对一些代码打包成一个单独的chunk,比如某些公共模块,去重,更好的利用缓存。或者按需加载某些功能模块,优化加载时间。在webpack3及以前我们都利用CommonsChunkPlugin将一些公共代码分割成一个chunk,实现单独加载。在webpack4 中CommonsChunkPlugin被废弃,使用SplitChunksPlugin
  • bundle:是webpack打包出来的文件;

# 规范

webpack默认遵循commonjs规范 module.exports

默认情况下webpack的配置文件叫webpack.config.js,可以通过--config指定webpack的配置文件名

使用webpack进行打包时有两种模式:

  • 开发模式:主要是用于测试,代码调试等;
  • 生产模式:要考虑性能问题,要压缩 如果没有插件 就不会压缩;

# 打包(构建)过程

# 简述

1、初始化:启动构建,读取和合并参数,加载plugin,实例化complier;

2、编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理;

3、输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统;

# 完整过程

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,通过执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中;

在以上过程中,Webpack 会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

# 工作原理总结

基于订阅/发布模型建立的Webpack打包工具把一个个繁杂耦合的前端源代码处理工作拆分成了很多个细小的任务。通过Tapable.plugin来注册一个个订阅器就可以在webpack工作中的某个具体步骤插入你的处理逻辑。这种插片式的计方便我们低耦合的对前端打包流程进行自定义。webpack的订阅/发布实现是基于Tapable.js

# 打包原理

将所有依赖打包成一个bundle.js,通过代码分割成单元片段按需加载

# 热更新实现原理【要点】

webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块原理在一个源码发生变化时,只需重新编译发生变化的模块,再用新输出的模块替换掉浏览器中对应的老模块

  1. Webpack编译期,为需要热更新的 entry 注入热更新代码(EventSource通信);
  2. 页面首次打开后,服务端与客户端通过 EventSource 建立通信渠道,把下一次的 hash 返回前端;
  3. 客户端获取到hash,这个hash将作为下一次请求服务端 hot-update.jshot-update.json的hash;
  4. 修改页面代码后,Webpack 监听到文件修改后,开始编译,编译完成后,发送 build 消息给客户端;
  5. 客户端获取到hash,成功后客户端构造hot-update.js script链接,然后插入主文档;
  6. hot-update.js 插入成功后,执行hotAPI 的 createRecord 和 reload方法,获取到 Vue 组件的 render方法,重新 render 组件, 继而实现 UI 无刷新更新;

HMR 作为一个 Webpack 内置的功能,可以通过 --hot 或者 HotModuleReplacementPlugin 开启

//webpack.dev.conf.js
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
  baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
new webpack.HotModuleReplacementPlugin(),
    
//dev-client.js
require('eventsource-polyfill')
//webpack-dev-server/client   webpack/hot/dev-server
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
hotClient.subscribe(function (event) {
  if (event.action === 'reload') {
    window.location.reload()
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 编译钩子顺序【要点】

按照下面的钩子调用顺序执行。

  • before-run 清除缓存
  • run 注册缓存数据钩子
  • before-compile
  • compile 开始编译
  • make 从入口分析依赖以及间接依赖模块,创建模块对象
  • build-module 模块构建
  • seal 构建结果封装, 不可再更改
  • after-compile 完成构建,缓存数据
  • emit 输出到dist目录

# 工作流程【要点】

webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现的核心就是 tapable

webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。在Tapable1.0之前,也就是webpack3及其以前使用的Tapable,提供了包括

  • plugin(name:string, handler:function)注册插件到Tapable对象中
  • apply(…pluginInstances: (AnyPlugin|function)[])调用插件的定义,将事件监听器注册到Tapable实例注册表中
  • applyPlugins(name:string, …)多种策略细致地控制事件的触发,包括applyPluginsAsyncapplyPluginsParallel等方法实现对事件触发的控制,实现

(1)多个事件连续顺序执行

(2)并行执行

(3)异步执行

(4)一个接一个地执行插件,前面的输出是后一个插件的输入的瀑布流执行顺序

(5)在允许时停止执行插件,即某个插件返回了一个undefined的值,即退出执行 我们可以看到,Tapable就像nodejs中EventEmitter,提供对事件的注册on和触发emit,理解它很重要

示例:

// sum.js
// 这个模块化写法是 node 环境独有的,浏览器原生不支持使用
module.exports = function(a, b) {
    return a + b
}
// index.js
var sum = require('./sum')
console.log(sum(1, 2))

//ES6版本
// sum.js
export default (a, b) => {
    return a + b
}
// index.js
import sum from './sum'
console.log(sum(1, 2))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./build/bundle.js"></script>
</body>
</html>
//webpack.config.js
const path = require('path')
module.exports = {
    entry:  './app/index.js', // 入口文件
    output: {
      path: path.resolve(__dirname, 'build'), // 必须使用绝对地址,输出文件夹
      filename: "bundle.js" // 打包后输出文件的文件名
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

发现原本两个 JS 文件只有100B,但是打包后却增长到 2.66KB,去 bundle.js 文件中看看。把代码简化以后:

var array = [(function () {
        var sum = array[1]
        console.log(sum(1, 2))
    }),
    (function (a,b) {
        return a + b
    })
]
array[0]() // -> 3

"scripts": {
   "start": "webpack"
 },
1
2
3
4
5
6
7
8
9
10
11
12
13

因为 module.export 浏览器是不支持的,所以 webpack 将代码改成浏览器能识别的样子。现在将 index.html 文件在浏览器中打开,应该也可以看到正确的 log。

配置实例:

const path = require('path');
module.exports = {
  // Webpack打包的入口
  entry: "./app/entry", // string | object | array 
  output: {  // 定义webpack如何输出的选项
    path: path.resolve(__dirname, "dist"), // string
    // 所有输出文件的目标路径
    filename: "[chunkhash].js", // string
    // 「入口(entry chunk)」文件命名模版
    publicPath: "/assets/", // string
    // 构建文件的输出目录
    /* 其它高级配置 */
  },
  module: {  // 模块相关配置
    rules: [ // 配置模块loaders,解析规则
      {
        test: /\.jsx?$/,  // RegExp | string
        include: [ // 和test一样,必须匹配选项
          path.resolve(__dirname, "app")
        ],
        exclude: [ // 必不匹配选项(优先级高于test和include)
          path.resolve(__dirname, "app/demo-files")
        ],
        loader: "babel-loader", // 模块上下文解析
        options: { // loader的可选项
          presets: ["es2015"]
        },
      },
  },
  resolve: { //  解析模块的可选项
    modules: [ // 模块的查找目录
      "node_modules",
      path.resolve(__dirname, "app")
    ],
    extensions: [".js", ".json", ".jsx", ".css"], // 用到的文件的扩展
    alias: { // 模块别名列表
      "module": "new-module"
	  },
  },
  devtool: "source-map", // enum
  // 为浏览器开发者工具添加元数据增强调试
  plugins: [ // 附加插件列表
    // ...
  ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 配置

# 项目初始化

# V3

    "webpack": "3.10.0",
    "webpack-bundle-analyzer": "2.9.1",
    "webpack-dev-server": "2.9.7",
    "webpack-merge": "4.1.1"
1
2
3
4

# V4

在webpack3中,webpack本身和它的cli以前都是在同一个包中,但在第4版中,他们已经将两者分开来更好地管理它们。安装Webpack 脚手架npm i -D webpack-cli

npm install webpack webpack-cli -g # 或者  yarn global add webpack webpack-cli
npm init -y  //-y 默认所有的配置
yarn add webpack webpack-cli -D
1
2
3
  "scripts": {
    "build": "webpack --mode production" //我们在这里配置
  },
  "devDependencies": {
    "webpack": "^4.16.0",
    "webpack-cli": "^3.0.8"
  }
1
2
3
4
5
6
7

# entry/output

var path = require('path')
var baseConfig = {
    //entry: './src/index.js'
    entry: {
        main: './src/index.js'
    },
    output: {
       // filename: 'main.js',//单个
        filename: '[name].js',//多个
        path: path.resolve('./build')
    }
}
module.exports = baseConfig
1
2
3
4
5
6
7
8
9
10
11
12
13

# resolve的使用

在webpack中如何配置我们的别名呢?在vue-cli中我们经常@一个文件夹,其意思就是在src目录下,现在我们去一探究竟。在module下,注意跟rules同级;

resolve: { //解析模块的可选项  
    // modules: [ ]//模块的查找目录 配置其他的css等文件
    extensions: [".js", ".json", ".jsx",".less", ".css"],  //用到文件的扩展名
        alias: { //模快别名列表
            utils: path.resolve(__dirname,'src/utils')
        }
}

const format = require('utils/format')  // utils ?  没有相对路径  回想@  => 别名
//在src新建相应的文件。在format.js里接受一个参数并把它转成大写
module.exports = function format(chars) {
    return chars.toUpperCase()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
  resolve: {
    extensions: ['.js', '.css', '.json'],// 文件扩展名,写明以后就不需要每个文件写后缀
//config.resolve.alias.set('@$', resolve('src')) //vue.config.js的使用方式;
    alias: {// 路径别名,比如这里可以使用 css 指向 static/css 路径
      '@': resolve('src'),
      'css': resolve('static/css')
    }
  },
  devtool: '#cheap-module-eval-source-map',// 生成 source-map,用于打断点,这里有好几个选项
}
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      '@': resolve('src')
    }
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 使用 DevServer

webpack-dev-server

webpack是我们需要的模块打包机,webpack-dev-server用来创建本地服务器,监听你的代码修改,并自动刷新修改后的结果。内部封装了一个express

# 配置参数【要点】
  • contentBase, // 为文件提供本地服务器
  • port, // 监听端口,默认8080
  • inline, // 设置为true,源文件发生改变自动刷新页面
  • historyApiFallback // 依赖HTML5 history API, 如果设置为true,所有的页面跳转指向index.html
devServer:{
    contentBase: './src' // 本地服务器所加载的页面所在的目录
    historyApiFallback: true, // 不跳转 //try_files $uri $uri/ /index.html;
    inline: true // 实时刷新
}
1
2
3
4
5

然后我们在根目录下创建一个webpack.config.js,在package.json添加两个命令用于本地开发和生产发布

 "scripts": {
    "build": "webpack --config webpack.config.js",
    "dev": "webpack-dev-server",
    "start": "webpack-dev-server --watch", //实时预览
     "dev2": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
  },
1
2
3
4
5
6

npm run dev 运行命令后,就可以启动 HTTP 服务; 启动结果如下所示,我们可以通过 http://localhost:8080/ 访问我们的 index.html 的demo

实时预览

在 index.html 中需要将 js 的路径修改为:

<script src="bundle.js"></script>  
1

而不能是之前的(因为这个是编译生成的,并不是通过 devServer 生成放在内存的

<script src="./dist/bundle.js"></script> 
1

模块热替换

可以通过配置 -- hot 进行模块热替换

对应vue.config.js中的配置

  devServer: {
    port: process.env.VUE_APP_PORT,
    proxy: {
      [process.env.VUE_APP_BASE_API]: {
        target: process.env.VUE_APP_API_TARGET_URL,
        changeOrigin: true
        // pathRewrite: {
        //   ['^' + process.env.VUE_APP_BASE_API]: ''
        // }
      }
    }
  },
1
2
3
4
5
6
7
8
9
10
11
12

# loader配置

# 作用

1、实现对不同格式的文件的处理,比如说将scss转换为css,或者typescript转化为js; 2、转换这些文件,从而使其能够被添加到依赖图中;

# 配置参数

  • test: //匹配所处理文件的扩展名的正则表达式(必须)
  • loader: //loader的名称(必须)
  • include/exclude: //手动添加处理的文件,屏蔽不需要处理的文件(可选); // exclude:[], 不匹配选项(优先级高于test和include
  • query: //为loaders提供额外的设置选项

其他参数

resource 和 issuer: 两者都是用于更加精确地确定模块规则的作用范围。使用频率不高。两者关系如下:

比如组件 import './index.css',可以这么理解:被加载模块是 resource,加载方就是 issuer。通常 css 配置的加载方是全局的,现在我们要限定配置。

enforce:用来指定一个 loader 的种类,其默认值为 normal,可选值为

  • pre,在 use 配置的所有 loader 之前执行,比如下面就是保证检测的代码不是其他 loader 更改过来的;
  • post,在 use 配置的所有 loader 之后执行
  • inline,官方不推荐使用;
module.exports = {
  module: {
    rules: [
      {
        use: ['style-loader', 'css-loader'],
        resource: {
          test: /\.css$/,
          exclude: /node_modules/
        },
        issuer: {
          test: /\.js$/,
          exclude: '/node_modules/',
          include: '/src/pages/'
        }
      },
      {
        test: /\.js$/,
        enforce: 'pre',
        use: 'eslint-loader'
      }
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var baseConfig = {
    // ...
    module: {
        rules: [{
            test: /*匹配文件后缀名的正则*/,
            use: [
            loader: /*loader名字*/,
            query: /*额外配置*/
            ]
    }]
}
}
module: {
    rules: [
        {
            test: /\.less$/,
            use: [
                {loader: 'style-loader'},
                {loader: 'css-loader'},
                {loader: 'less-loader'}
            ],
            exclude: /node_modules/
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# Loader 结构解读

module.exports = function loader(content, map, meta) {
  var callback = this.async();
  var result = handler(content, map, meta);
  callback(
    null, // err
    result.content, // 转换后的内容
    result.map, // 转换后的 source-map
    result.meta // 转换后的 AST
  );
};
1
2
3
4
5
6
7
8
9
10

从上面可看出,本质是个函数,功能是将收到后的内容进行转换,然后返回转换后的结果,可能有 source-map 和 AST 对象

每个 loader 本质都是一个函数,output = loader(input)。在 webpack4 之前,input 和 output 都必须为字符串,而 webpack4 之后,也支持**抽象语法树(AST)**的传递,那 loader 就可以是链式的了,即 output = loaderA(loaderB(input))

  • input,可能是工程源文件字符串,可能是上一个loader 的转化结果(字符串、source map 或 AST 对象);
  • output,同 input 一样,如果是最后一个 loader 就将结果给 webpack 后续处理;

注意,第一个 loader 的输入时源文件,之后所有 loader 的输入是上一个 loader 的输出,最后一个 loader 输出给 webpack。

# Loader 要点总结【要点】

Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子

  • Loader 为模块转换器,用于将模块的原内容按照需求转换成新内容

  • Loader 的职责是单一的,只需要完成一种转换,遵守单一职责原则

  • Webpack 为 Loader 提供了一系列 API 供 Loader 调用,例如:

    • loader-utils.getOptions( this ) 获取用户传入的 options,
    • this.callback( ) 自定义返回结果,
    • this.async( ) 支持异步操作;
    • this.context 当前文件所在的目录;
    • this.resource 当前处理文件的完整请求路径;
    • 其它等等
  • 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。 此外webpack还为开发者准备了开发loader的工具函数集——loader-utils

    const { getOptions } = require("loader-utils");
    // 获取webpack配置的options,写loader的固定套路第一步
    module.exports = function(content, map, meta) {
      const opts = getOptions(this) || {};
      const code = JSON.stringify(content);
      const isESM = typeof opts.esModule !== "undefined" ? options.esModule : true;
    // 直接返回原文件内容
      return `${isESM ? "export default" : "module.exports ="} ${code}`;
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    module.exports = function(content, sourceMap) {
      const res = "samy"
      //加入缓存; 如果文件和依赖包都没有更改,那 loader 就直接使用缓存,而不是重复转换
      if (this.cacheable) {
        console.log("缓存");
        this.cacheable();
      }
      // 支持 source-map
      const options = loaderUtils.getOptions(this) || {};
      //source-map 便于开发者在浏览器查看源代码。如果没有对 source-map 处理,最终也生成不了 map 文件,那在浏览器 devtool 中可能会看到错误的源码。
      if (options.sourceMap && sourceMap) {
        //参数中新增 sourceMap 对象,它是由 webpack 或上一个 loader 传递下来的,只有它存在 loader 才能继续向下处理;
        const currentRequest = loaderUtils.getCurrentRequest(this);
        //通过依赖包 source-map 对 map 进行操作,接收和消费之前的文件内容和 source-map,对内容进行修改,然后产生新的 source-map;
        const node = SourceNode.fromStringWithSourceMap( content, new SourceMapConsumer(sourceMap));
        node.prepend(useStrict);
        const result = node.toStringWithSourceMap({
          file: currentRequest
        });
        const callback = this.async();
        callback(null, result.code, result.map.toJSON());//使用 this.async 获取 callback 函数,callback 参数分别是抛出错误、处理后的源码和新的 source-map;
      }
      return res;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

# 编写 Loader

一个loader是一个导出为函数的 js 模块,这个函数有三个参数:content, map, meta

  • content: 表示源文件字符串或者buffer
  • map: 表示sourcemap对象
  • meta: 表示元数据,辅助对象

实现要点:【3步骤】

  • 获取loader配置;const { getOptions } = require("loader-utils")
  • 默认三个参数 ; module.exports = function(content, map, meta) {}
  • 处理content返回及判断即可;

示例:手写一个 loader 源码,其功能是将 /hello/gi 转换成 HELLO

第一步:源码编写;在原有的项目底下,新建目录 custom-loader 作为我们编写 loader 的名称,执行 npm init 命令,新建一个模块化项目,然后新建 index.js 文件;

function convert(source){
  return source && source.replace(/hello/gi,'HELLO');
}
module.exports = function(content){
  return convert(content);
}
1
2
3
4
5
6

第二步:Npm link 模块注册;在 custom-loader 目录底下,运行以下命令,将本地模块注册到全局:npm link;然后在项目根目录执行以下命令,将注册到全局的本地 Npm 模块链接到项目的 node_modules 下:npm link custom-loader

第三步:Webpack中配置编写的 loader

  module:{
    rules:[
      {
        test:/\.js/,
        use:['custom-loader'],
        include:path.resolve(__dirname,'show')
      }
    ]
  }
1
2
3
4
5
6
7
8
9

# 分类

  • babel-loader:把 ES6 转换成 ES5; 默认情况下不支持js的高级语法,所以需要使用babel;

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件

  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去; 推荐用这个,不用 file-loader库了;

  • image-webpack-loader 处理压缩图片,解决上面 url-loader超出配置不能压缩的情况, 得配合 url-loader使用;

  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性

  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS; 跟 css-loader同时使用;

  • postcss-loader 提供了一种方式用 JavaScript 代码来处理 CSS。它负责把 CSS 代码解析成抽象语法树结构(Abstract Syntax Tree,AST),再交由插件来进行处理

  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试

  • eslint-loader:通过 ESLint 检查 JavaScript 代码

  • html-loader :将 HTML 文件转化为字符串并进行格式化,然后将 HTML 片段通过 JS 加载进来。

  • ts-loader注意,typescript 的配置不是在 ts-loader 中,而是在工程目录的 tsconfig.json 中

    {
      "compilerOptions": {
        "target": "es5",
        "sourceMap": true
      }
    }
    
    1
    2
    3
    4
    5
    6

# 详细介绍

# babel

Babel 可以让你使用 ES2015/16/17 写代码而不用顾忌浏览器的问题,Babel 可以帮你转换代码。

  • babel-loader 用于让 webpack 知道如何运行 babel; 转译 ES6+;
  • babel-core 可以看做编译器,这个库知道如何解析代码
  • babel-preset-env 这个库可以根据环境的不同转换代码
  • @babel/core:babel 编译器核心模块
  • @babel/preset-env:预置器,根据用户配置的目标环境自动添加需要的插件和补丁来编译 ES6+;

参数及要点:

  • cacheDirectory,缓存机制,这里设为 true,在重复打包未改变的模块时防止二次编译,提高打包速度,指向 node_modules/.cache/babel-loader

  • presets 的 modules 设置为 false,意思是禁止让 @babel/preset-env 将模块语句转换,让 ES6 Module 语法给 webpack 处理,若是为 true,会将 ES6 Module 模块转化为 CommonJS 形式,这将会导致 tree-shaking 特性失效,所以要设置为false,便于优化;

package.json

yarn add babel-loader babel-core  babel-preset-env -D  #babel基本的三个文件
1
npm install babel-loader @babel/core @babel/preset-env -D #v4新版本
1
    "babel-core": "6.26.0",
    "babel-eslint": "8.0.3",
    "babel-loader": "7.1.2",
    "babel-plugin-transform-runtime": "6.23.0",
    "babel-preset-env": "1.6.1",
    "babel-preset-stage-2": "6.24.1",
1
2
3
4
5
6

webpack.config.js

module.exports = {
// ......
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
                include: [resolve('src'), resolve('test')],
                exclude: /node_modules/ // 不包括路径,
                //options: { // loader的可选项
                // cacheDirectory: true,
                //  presets: ["es2015"]
                 //presets: [
             	   //[ 'env', { modules: false } ]
           		 //]
                //},
            }
        ]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

.babelrc 配置 Babel 有很多方式,以上示例及下面配置,这里推荐使用 .babelrc 文件管理

注意,babel-loader 支持从 .babelrc 文件读取 babel 配置,可将 presets 和 plugins 从配置中提取出来。

module.exports = {
    // if you wish to apply custom babel options instead of using vue-loader's default:
    babel: {
        presets: ['es2015', 'stage-0'],
        plugins: ['transform-runtime']
    }
}
1
2
3
4
5
6
7
{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  "plugins": ["transform-runtime"]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
# 处理文件资源及图片
  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件; 打包文件类型文件,并返回到 output.publicPath 中;

    注意,配置中 file-loader 的 options.publicPath 会覆盖 output.publicPath,优先级高些

module.exports = {
  entry: './app.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: './assets/'
  },
  rules: [{
      test: /\.(png|jpg|jpeg|webp|gif)$/,
      use: {
        loader: 'file-loader',
        options: {
          name: '[name].[ext]',
          // publicPath: './new-assets/'
        }
      }
    }
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去;
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: { // 配置 url-loader 的可选项
          limit: 10000,// 限制 图片大小 10000B,小于限制会将图片转换为 base64格式
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
          //name: 'images/[name].[hash].[ext]'// 超出限制,创建的文件格式
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('media/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
        }
      }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • image-webpack-loader:在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,可以用 image-webpack-loader来压缩图片;
rules: [{
  test: /\.(gif|png|jpe?g|svg)$/i,
  use: [
    'file-loader',{
      loader: 'image-webpack-loader',
      options: {
        bypassOnDebug: true, // webpack@1.x
        disable: true, // webpack@2.x and newer
      },
    },
  ],
}]
1
2
3
4
5
6
7
8
9
10
11
12
# 处理 CSS 文件

Webpack 不原生支持解析 CSS 文件。要支持非 JavaScript 类型的文件,需要使用 Webpack 的 Loader 机制;

  • css-loader 可以让 CSS 文件也支持 import,并且会解析 CSS 文件

  • style-loader 可以将解析出来的 CSS 通过标签的形式插入到 HTML 中,所以后面依赖css-loader

  • postcss-loader 提供了一种方式用 JavaScript 代码来处理 CSS。它负责把 CSS 代码解析成抽象语法树结构(Abstract Syntax Tree,AST),再交由插件来进行处理-webkit-transform: rotate(45deg); transform: rotate(45deg);

  • extract-text-webpack-plugin 插件将 CSS 文件打包为一个单独文件; 这里有一个坑,extract-text-webpack-plugin在4.0并不支持这样的安装。于是我们选择换一种方式,选择4.00-beta.0版本的; yarn add extract-text-webpack-plugin@last -D

  • optimize-css-assets-webpack-plugin 压缩提取的CSS。我们正在使用此插件,以便可以删除来自不同组件的可能重复的CSS

  • mini-css-extract-plugin 把css专门打包成一个css文件,在index.html文件中引入css;

module.exports = {
// ...
    module: {
      rules: [
        {
            test: /\.css$/,
            use: ['style-loader',
                {
                   loader: 'css-loader',
                    options: {
                        modules: true
                     }
                }
            ]
        },
      ]
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

但是将 CSS 代码整合进 JS 文件也是有弊端的,大量的 CSS 代码会造成 JS 文件的大小变大,操作 DOM 也会造成性能上的问题,所以接下来我们将使用 extract-text-webpack-plugin 插件将 CSS 文件打包为一个单独文件;

生成对应的 css 文件;存在的坑点:

  • 我们需要手动将生成的 css 文件引入到 index.html 中;
  • 修改 css 文件后,会生成新的 css 文件,原先的不会删除;后面的插件部分会帮处理;
const ExtractTextPlugin = require("extract-text-webpack-plugin")
module.exports = {
    module: {
      rules: [
        {
          test: /\.css$/,
          loader: ExtractTextPlugin.extract({// 写法和之前基本一致
                fallback: 'style-loader',// 必须这样写,否则会报错
                use: [{
                    loader: 'css-loader',
                    options: { 
                        modules: true
                    }
                }]
            })
        ]
        }
      ]
    },
    plugins: [// 插件列表
      new ExtractTextPlugin("css/[name].[hash].css")// 输出的文件路径
       //filename:`[name]_[hash:8].css`// 从 .js 文件中提取出来的 .css 文件的名称
    ]
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

css压缩

var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
// extract css into its own file
new ExtractTextPlugin({
    filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// Compress extracted CSS. We are using this plugin so that possible duplicated CSS from different components can be deduped.
//压缩提取的CSS。我们正在使用此插件,以便可以删除来自不同组件的可能重复的CSS
new OptimizeCSSPlugin({
    cssProcessorOptions: {
        safe: true
    }
}),
1
2
3
4
5
6
7
8
9
10
11
12
13

# plugin配置

# plugin与loader的区别

  • loader是使webpack拥有加载和解析非js文件的能力。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader
  • plugin可以扩展webpack的功能,使得webpack更加灵活。可以在构建的过程中通过webpack的api改变输出的结果; 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
  • loaders负责的是处理源文件的如css、jsx,一次处理一个文件。而plugins并不是直接操作单个文件

# Plugin 要点总结【要点】

相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果

  • Webpack 在编译过程中,会广播很多事件,例如 run、compile、done、fail 等等,可以查看官网;
  • Webpack 的事件流机制应用了观察者模式,编写的插件可以监听 Webpack 事件来触发对应的处理逻辑;
  • 插件中可以使用很多 Webpack 提供的 API,例如读取输出资源、代码块、模块及依赖等;

事件:

  • compiler是webpack的'编译器'引用
  • compile('编译器'对'开始编译'这个事件的监听)
  • compilation('编译器'对'编译ing'这个事件的监听)
  • emit('编译器'对'生成最终资源'这个事件的监听)
  • done, failed, after-emit;

按照下面的钩子调用顺序执行。

  • before-run 清除缓存
  • run 注册缓存数据钩子
  • before-compile
  • compile 开始编译
  • make 从入口分析依赖以及间接依赖模块,创建模块对象
  • build-module 模块构建
  • seal 构建结果封装, 不可再更改
  • after-compile 完成构建,缓存数据
  • emit 输出到dist目录

# 编写 Plugin

实现要点:【3步骤】

  • 是否构造传递初始化函数;
  • 调用插件 apply 函数传入 compiler 对象
  • 通过 compiler 对象监听事件

示例:手写一个 plugin 源码,其功能是在 Webpack 编译成功或者失败时输出提示;

第一步:源码编写;在原有的项目底下,新建目录 custom-plugin 作为我们编写 loader 的名称,执行 npm init 命令,新建一个模块化项目,然后新建 index.js 文件;

class CustomPlugin{
  constructor(doneCallback, failCallback){// 保存在创建插件实例时传入的回调函数
     this.doneCallback = doneCallback;
     this.failCallback = failCallback;
  }
  apply(compiler){
    compiler.plugin('done',(stats)=>{// 成功完成一次完整的编译和输出流程时,会触发 done 事件
      this.doneCallback(stats);
    })
    compiler.plugin('failed',(err)=>{// 在编译和输出的流程中遇到异常时,会触发 failed 事件
      this.failCallback(err);
    })
  }
}
module.exports = CustomPlugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第二步:Npm link 模块注册;跟 Loader 注册一样;npm link, npm link custom-plugin

第三步:Webpack中配置编写的 loader

  plugins:[
    new CustomPlugin(
     stats => {console.info('编译成功!')},
     err => {console.error('编译失败!')}
   ),
  ],
1
2
3
4
5
6

比如你想实现一个编译结束退出命令的插件

class BuildEndPlugin {
  apply (compiler) {
    const afterEmit = (compilation, cb) => {
      cb()
      setTimeout(function () {
        process.exit(0)
      }, 1000)
    }
    compiler.plugin('after-emit', afterEmit)
  }
}
module.exports = BuildEndPlugin
1
2
3
4
5
6
7
8
9
10
11
12

# 分类

  • extract-text-webpack-plugin 插件将 CSS 文件打包为一个单独文件; 详见上面示例的使用;

    deprecate extract-text-webpack-plugin@* Deprecated. Please use https://github.com/webpack-contrib/mini-css-extract-plugin

  • optimize-css-assets-webpack-plugin 压缩css

  • html-webpack-plugin 执行 build 操作会发现同时生成了 HTML 文件,并且已经自动引入了 JS 文件

  • clean-webpack-plugin 去掉没有用到的模块

  • copy-webpack-plugin 文件直接复制

  • DefinePlugindefine-plugin:自带的;定义环境变量

  • UglifyJsPlugin, uglifyjs-webpack-plugin:自带的;通过UglifyES压缩ES6代码

  • CommonsChunkPlugincommons-chunk-plugin 自带的,webpack内置插件 抽取公共部分

  • OccurenceOrderPlugin 自带的;为组件分配ID,通过这个插件webpack可以分析和优先考虑使用最多 的模块,然后为他们分配最小的ID

  • HotModuleReplacementPlugin: 自带的;自动刷新实时预览修改后的结果;

  • ProvidePlugin 引用框架 jquery lodash工具库是很多组件会复用的,省去了import。使用webpack.ProvidePlugin插件

# 详细介绍

# 基本插件
  • ExtractTextWebpackPlugin: 它会将入口引用css文件,都打包都独立的css文件中,而不是内嵌在js打包文件中
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var lessRules = {
    use: [
        {loader: 'css-loader'},
        {loader: 'less-loader'}
    ]
}
var baseConfig = {
    // ... 
    module: {
        rules: [
            // ...
            {test: /\.less$/, use: ExtractTextPlugin.extract(lessRules)}
        ]
    },
    plugins: [
        new ExtractTextPlugin('main.css')
    ]
}
new ExtractTextPlugin({
    filename: utils.assetsPath('css/[name].[contenthash].css')
}),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • HtmlWebpackPlugin: 依据一个简单的index.html模版,生成一个自动引用你打包后的js文件的新index.html
var HTMLWebpackPlugin = require('html-webpack-plugin')
var baseConfig = {
    // ...
    plugins: [
        new HTMLWebpackPlugin()
    ]
}
1
2
3
4
5
6
7
  • HotModuleReplacementPlugin: 自带的; 它允许你在修改组件代码时,**自动刷新实时预览修改后的结果 **注意永远不要在生产环境中使用HMR。
devServer: {
  hot: true,
},
plugins: [
  new webpack.HotModuleReplacementPlugin(),
  new webpack.NamedModulesPlugin(), // HMR shows correct file names显示被替换模块的名称
]
1
2
3
4
5
6
7
 plugins: [
    new webpack.DefinePlugin({
      'process.env': config.dev.env
    }),
    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    new FriendlyErrorsPlugin()
  ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • DefinePlugindefine-plugin:自带的;定义环境变量
var ENV = process.env.NODE_ENV
var baseConfig = {
    // ... 
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(ENV)
        })
    ]
}
1
2
3
4
5
6
7
8
9
"scripts": {
    "start": "NODE_ENV=development webpack-dev-server",
    "build": "NODE_ENV=production webpack"
}
1
2
3
4
  • ProvidePlugin 引用框架 jquery lodash工具库是很多组件会复用的,省去了import。使用webpack.ProvidePlugin插件
  • copy-webpack-plugin src下其他的文件直接复制到dist目录下,并不是每个文件都需要打包处理的,很多资源可能直接就可以复制过去。
new CopyWebpackPlugin([  //src下其他的文件直接复制到dist目录下
    { from:'src/assets/favicon.ico',to: 'favicon.ico' }
])
new webpack.ProvidePlugin({//引用框架jquery,lodash工具库是很多组件会复用的,省去了import
    '_': 'lodash'  //引用webpack
})

new CopyWebpackPlugin([
    {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
    }
])
1
2
3
4
5
6
7
8
9
10
11
12
13
14

优化插件

  • OccurenceOrderPlugin: 为组件分配ID,通过这个插件webpack可以分析和优先考虑使用最多 的模块,然后为他们分配最小的ID CommonsChunkPlugin 自带的,压缩代码
  • UglifyJsPlugin: 自带的,压缩代码
# **tree-shaking(**摇树)优化

webpack 2 的到来带来的最棒的新特性之一就是tree-shaking 。tree-shaking源自于rollup.js; webpack 里的tree-shaking的到来不得不归功于es6规范的模块; presets 的 modules 设置为 false意思是禁止让 @babel/preset-env 将模块语句转换,让 ES6 Module 语法给 webpack 处理,若是为 true,会将 ES6 Module 模块转化为 CommonJS 形式,这将会导致 tree-shaking 特性失效;

其实–optimize-minimize的底层实现是一个插件UglifyJsPlugin,因此,我们可以直接在webpack.config.js里配置它;从 webpack 4 开始,也可以通过 "mode" 配置选项轻松切换到压缩输出,只需设置为 "production"

# 原理

Tree-shaking的本质用于消除项目一些不必要的代码。早在编译原理中就有提到DCE(dead code eliminnation),作用是消除不可能执行的代码,它的工作是使用编辑器判断出某些代码是不可能执行的,然后清除。但是和DCE又有略不相同。可以说是DCE的一种实现,它的主要工作是应用于模块间,在打包过程中抽出有用的部分,用于完成DCE。

Tree-shaking是依赖ES6模块静态分析的,ES6 module的特点如下:

  1. 只能作为模块顶层的语句出现
  2. import 的模块名只能是字符串常量
  3. import binding 是 immutable的
# 使用三步骤

参考文档 (opens new window)

  • 使用 ES2015 模块语法(即 importexport)。
  • 在项目 package.json 文件中,添加一个 "sideEffects" 入口。
  • 引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如 UglifyJSPlugin)。
{// 所有文件都有副作用,全都不可 tree-shaking
 "sideEffects": true
}
{// 没有文件有副作用,全都可以 tree-shaking
 "sideEffects": false
}
{// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}
new webpack.optimize.UglifyJsPlugin()
1
2
3
4
5
6
7
8
9
10
11
12
13
# Code Splitting

Code Splitting 一般需要做这些事情:

  • 为 Vendor 单独打包(Vendor 指第三方的库或者公共的基础组件,因为 Vendor 的变化比较少,单独打包利于缓存); vendor.22736000e5e6774b73a3.js
  • 为 Manifest (Webpack 的 Runtime 代码)单独打包; manifest.5afe50c3e4d9f9d9d5a5.js
  • 为异步加载的代码打一个公共的包; app.5c5fb7409929639ae5e7.js
  • 为不同入口的公共业务代码打包(同理,也是为了缓存和加载速度); 1.c15af7b230c804ebb02b.js

Code Splitting 一般是通过配置 CommonsChunkPlugin 来完成的。一个典型的配置如下,分别为 vendormanifestvendor-async 配置了 CommonsChunkPlugin。详见下面示例,上半截部分;

# Long-term caching

策略: 给静态文件一个很长的缓存过期时间,比如一年。然后在给文件名里加上一个 hash,每次构建时,当文件内容改变时,文件名中的 hash 也会改变。浏览器在根据文件名作为文件的标识,所以当 hash 改变时,浏览器就会重新加载这个文件。

核心:Webpack 内部维护了一个自增的 id,每个 chunk 都有一个 id。所以当增加 entry 或者其他类型 chunk 的时候,id 就会变化,导致内容没有变化的 chunk 的 id 也发生了变化。

new webpack.optimize.OccurenceOrderPlugin()
new webpack.optimize.UglifyJsPlugin()
   new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),

output: {
  path: config.build.assetsRoot,
  filename: utils.assetsPath('js/[name].[chunkhash].js'),
  chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},

    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      },
      sourceMap: true
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css')
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: {
        safe: true
      }
    }),
     // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function (module, count) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor']
    }),
      // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

# 项目中使用

ps: 自己编写webpack.config.js文件要考虑到对应的loader及plugin的版本对应问题,所以还是推荐用cli方式处理好点,不用再去考虑兼容性问题; 配置好我们webpack的运行环境时,联想下vue-cli。平时使用vue-cli会自动帮我们配置并生成项目。

# 分离代码

可以考虑将依赖的库和自己的代码分割开来,这样用户在下一次使用应用时就可以尽量避免重复下载没有变更的代码,那么既然要将依赖代码提取出来,我们需要变更下入口和出口的部分代码。

const VENOR = [// 这是 packet.json 中 dependencies 下的
  "lodash",
  "vue"
]
module.exports = {
// 之前我们都是使用了单文件入口; entry 同时也支持多文件入口,现在我们有两个入口
// 一个是我们自己的代码,一个是依赖库的代码
  entry: {// bundle 和 vendor 都是自己随便取名的,会映射到 [name] 中
    bundle: './src/index.js',
    vendor: VENOR
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  // ...
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

为什么 bundle 文件大小压根没变。这是因为 bundle 中也引入了依赖库的代码,刚才的步骤并没有抽取 bundle 中引入的代码,接下来让我们学习如何将共同的代码抽取出来。

# 抽取共同代码

使用 webpack 自带的插件 CommonsChunkPlugin

module.exports = {
//...
  output: {
    path: path.join(__dirname, 'dist'),
    // 既然我们希望缓存生效,就应该每次在更改代码以后修改文件名
    filename: '[name].[chunkhash].js' // [chunkhash]会自动根据文件是否更改而更换哈希
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
    // vendor 的意义和之前相同
    // manifest文件是将每次打包都会更改的东西单独提取出来,保证没有更改的代码无需重新打包,可以加快打包速度
      names: ['vendor', 'manifest'],
      minChunks: Infinity // 配合 manifest 文件使用
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当我们重新 build 以后,会发现 bundle 文件很明显的减小了体积; 但是我们使用哈希来保证缓存的同时会发现每次 build 都会生成不一样的文件,这时候我们引入另一个插件来帮助我们删除不需要的文件。

clean-webpack-plugin 然后 build 的时候会发现以上文件被删除了。

module.exports = {
 //...
  plugins: [
   // 只删除 dist 文件夹下的 bundle 和 manifest 文件
    new CleanWebpackPlugin(['dist/bundle.*.js','dist/manifest.*.js'], {
      verbose: true,// 打印 log
      dry: false// 删除文件
    }),
  ]
};
1
2
3
4
5
6
7
8
9
10

因为我们现在将文件已经打包成三个 JS 了,以后也许会更多,每次新增 JS 文件我们都需要手动在 HTML 中新增标签,现在我们可以通过一个插件来自动完成这个功能。

html-webpack-plugin 执行 build 操作会发现同时生成了 HTML 文件,并且已经自动引入了 JS 文件; 根据模块生成一个html文件 此时不会在dist文件夹下面新建index文件了

需要在public新建 index文件 根据这个模板文件 在内存中生成 index.html 然后自动引入bundle.js

module.exports = {
//...
  plugins: [
  // 我们这里将之前的 HTML 文件当做模板
  // 注意在之前 HTML 文件中请务必删除之前引入的 JS 文件
    new HtmlWebpackPlugin({
      template: 'index.html'
    })
  ]
};
1
2
3
4
5
6
7
8
9
10

# 按需加载代码

现在我们的 bundle 文件包含了我们全部的自己代码。但是当用户访问我们的首页时,其实我们根本无需让用户加载除了首页以外的代码,这个优化我们可以通过路由的异步加载来完成

通过 route/index.js 加载组件用import引入;

const login = () => import(/* webpackChunkName: "login" */ '../pages/login')
1

执行 build 命令,可以发现我们的 bundle 文件又瘦身了,并且新增了几个文件; 将 HTML 文件在浏览器中打开,当点击路由跳转时,可以在开发者工具中的 Network 一栏中看到加载了一个 JS 文件。

# 自动刷新

每次更新代码都需要执行依次 build,并且还要等上一会很麻烦,使用自动刷新的功能 webpack-dev-serve

"scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server --open"
  },
1
2
3
4

现在直接执行 npm run dev 可以发现浏览器自动打开了一个空的页面,并且在命令行中也多了新的输出

等待编译完成以后,修改 JS 或者 CSS 文件,可以发现 webpack 自动帮我们完成了编译,并且只更新了需要更新的代码;但是每次重新刷新页面对于 debug 来说很不友好,这时候就需要用到模块热替换了。 Vue 或者其他框架都有自己的一套 hot-loader webpack-hot-middleware

# 完整代码

# v3【实践要点】

看到我们在经历了这么多步以后,将 bundle 缩小到了只有 27.1KB,像 vendor 这种常用的库我们一般可以使用 CDN 的方式外链进来

"scripts": {
    "build": "NODE_ENV=production webpack -p",
    "dev": "webpack-dev-server --open"
  }
1
2
3
4
var webpack = require('webpack');
var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin')
var CleanWebpackPlugin = require('clean-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

const VENOR = ["faker",
  "lodash",
  "vue",
  "vue-router"
]

module.exports = {
  entry: {
    bundle: './src/index.js',
    vendor: VENOR
  },
  devServer: {// 如果想修改 webpack-dev-server 配置,在这个对象里面修改
    port: 8081
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].[chunkhash].js'
  },
  module: {
    rules: [{
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: [{
            loader: 'url-loader',
            options: {
                limit: 10000,
                name: 'images/[name].[hash:7].[ext]'
            }
        }]
    },
    {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: [{// 这边其实还可以使用 postcss 先处理下 CSS 代码
                loader: 'css-loader'
            }]
        })
    },
    ]
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: ['vendor', 'manifest'],
      minChunks: Infinity
    }),
    new CleanWebpackPlugin(['dist/*.js'], {
      verbose: true,
      dry: false
    }),
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    new webpack.DefinePlugin({// 生成全局变量
      "process.env.NODE_ENV": JSON.stringify("process.env.NODE_ENV")
    }),
    new ExtractTextPlugin("css/[name].[contenthash].css"),// 分离 CSS 代码
    new OptimizeCSSPlugin({// 压缩提取出的 CSS,并解决ExtractTextPlugin分离出的 JS 重复问题
      cssProcessorOptions: {
        safe: true
      }
    }),
    new webpack.optimize.UglifyJsPlugin({// 压缩 JS 代码
      compress: {
        warnings: false
      }
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# v4【实践要点】
const path = require('path');  //引入node的path模块
const webpack = require('webpack'); //引入的webpack,使用lodash
const HtmlWebpackPlugin = require('html-webpack-plugin')  //将html打包
const ExtractTextPlugin = require('extract-text-webpack-plugin')//css拆分,将一部分抽离出来  
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
    entry: './src/index.js', //入口文件  在vue-cli main.js
    output: {//webpack如何输出
        path: path.resolve(__dirname, 'dist'), //定位,输出文件的目标路径
        filename: '[name].js'
    },
    module: { //模块的相关配置
        rules: [ //根据文件的后缀提供一个loader,解析规则
            {
                test: /\.js$/,  //es6 => es5 
                include: [
                    path.resolve(__dirname, 'src')
                ],
                // exclude:[], 不匹配选项(优先级高于test和include)
                use: 'babel-loader'
            },
            {
                test: /\.less$/,
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: [
                        'css-loader',
                        'less-loader'
                    ]
                })
            },
            { //图片loader
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader' //根据文件地址加载文件
                    }
                ]
            }
        ]                  
    },
    resolve: { //解析模块的可选项  
        // modules: [ ]//模块的查找目录 配置其他的css等文件
        extensions: [".js", ".json", ".jsx",".less", ".css"],  //用到文件的扩展名
        alias: { //模快别名列表
            utils: path.resolve(__dirname,'src/utils')
        }
    },
    plugins: [  //插进的引用, 压缩,分离美化
        new ExtractTextPlugin('[name].css'),  //[name] 默认  也可以自定义name  声明使用
        new HtmlWebpackPlugin({//将模板的头部和尾部添加css和js模板,dist目录发布到服务器上,项目包。可以直接上线
            file: 'index.html', //打造单页面运用 最后运行的不是这个
            template: 'src/index.html'  //vue-cli放在跟目录下
        }),
        new CopyWebpackPlugin([  //src下其他的文件直接复制到dist目录下
            { from:'src/assets/favicon.ico',to: 'favicon.ico' }
        ]),
        new webpack.ProvidePlugin({//引用框架jquery,lodash工具库是很多组件会复用的,省去了import
            '_': 'lodash'  //引用webpack
        })
    ],
    devServer: {  //服务于webpack-dev-server  内部封装了一个express 
        port: '8080',
        before(app) {
            app.get('/api/test.json', (req, res) => {
                res.json({
                    code: 200,
                    message: 'Hello World'
                })
            })
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# v3与v4的比较

Webpack 4 的改变主要是对社区中最佳实践的吸收。Webpack 4 通过新的 API 大大提升了 Code Splitting 的体验。但 Long-term caching 中 Vendor hash 的问题还是没有解决,需要手动配置。

区别总括:

  • mode

    webpack增加了一个mode配置,只有两种development | production。对不同的环境会启用不同的配置。

  • 代码分割

    使用动态import,而不是用system.import或者require.ensure

  • CommonsChunkPlugin

    CommonChunksPlugin已经从webpack4中移除。 可使用optimization.splitChunks进行模块划分(提取公用代码)。 但是需要注意一个问题,默认配置只会对异步请求的模块进行提取拆分,如果要对entry进行拆分 需要设置optimization.splitChunks.chunks = 'all'。

  • UglifyJsPlugin

    v4只需要使用optimization.minimize为true就行,production mode下面自动为true optimization.minimizer可以配置你自己的压缩程序

  • ExtractText

    webpack4使用MiniCssExtractPlugin取代ExtractTextWebpackPlugin。

  • vue-loader

    使用vue-loader插件为.vue文件中的各部分使用相对应的loader,比如css-loader等

# 开发和生产环境的区分

在 webpack 4+ 中,你可以使用 mode 选项: Webpack 4 下如果需要一个 test 环境,那 test 环境的 mode 也是 development。因为 mode 只有开发和生产两种,测试环境应该是属于开发阶段。

设置了 mode 之后会把 process.env.NODE_ENV 也设置为 development 或者 production。然后在 production 模式下,会默认开启 UglifyJsPlugin 等等一堆插件。

module.exports = {
  mode: 'production'
}
//mode: 'development',//production(默认)
1
2
3
4

但是在 webpack 3 及其更低版本中,你需要使用 DefinePlugin (opens new window)

var webpack = require('webpack')

module.exports = {
  // ...
  plugins: [
    // ...
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    })
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 第三方库 build 的选择

在 Webpack 3 时代,我们需要在生产环境的的 Webpack 配置里给第三方库设置 alias,把这个库的路径设置为 production build 文件的路径。以此来引入生产版本的依赖

在 Webpack 4 引入了 mode 之后,对于部分依赖,我们可以不用配置 alias,比如 React。React 的入口文件是这样的:

但大部分的第三库并没有做这个入口的环境判断。所以这种情况下我们还是需要手动配置 alias。

resolve: {
  extensions: [".js", ".vue", ".json"],
  alias: {
    vue$: "vue/dist/vue.runtime.min.js"
  }
},

'use strict';
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# Code Splitting

Webpack 4 下还有一个大改动,就是废弃了 CommonsChunkPlugin,引入了 optimization.splitChunks 这个选项。

optimization.splitChunks 默认是不用设置的。如果 mode 是 production,那 Webpack 4 就会开启 Code Splitting。

默认 Webpack 4 只会对按需加载的代码做分割。如果我们需要配置初始加载的代码也加入到代码分割中,可以设置 splitChunks.chunks'all'

内置的代码切分的规则

  • 新 bundle 被两个及以上模块引用,或者来自 node_modules
  • 新 bundle 大于 30kb (压缩之前)
  • 异步加载并发加载的 bundle 数不能大于 5 个
  • 初始加载的 bundle 数不能大于 3 个;

简单的说,Webpack 会把代码中的公共模块自动抽出来,变成一个包,前提是这个包大于 30kb,不然 Webpack 是不会抽出公共代码的,因为增加一次请求的成本是不能忽视的。

# Long-term caching

Long-term caching 这里,基本的操作和 Webpack 3 是一样的。不过 Webpack 3 的 Long-term caching 在操作的时候,有个小问题,这个问题是关于 chunk 内容和 hash 变化不一致的:

在公共代码 Vendor 内容不变的情况下,添加 entry,或者 external 依赖,或者异步模块的时候,Vendor 的 hash 会改变

webpack.NamedChunksPlugin 只能对普通的 Webpack 模块起作用,异步模块,external 模块是不会起作用的。

异步模块可以在 import 的时候加上 chunkName 的注释,比如这样:import(/* webpackChunkName: "lodash" */ 'lodash').then() 这样就有 Name 了

# vue项目使用

# vue-loader

Vue Loader 是一个 webpack (opens new window) 的 loader,它允许你以一种名为单文件组件 (SFCs) (opens new window)的格式撰写 Vue 组件; 简而言之,webpack 和 Vue Loader 的结合为你提供了一个现代、灵活且极其强大的前端工作流,来帮助撰写 Vue.js 应用。【vue-loader 是 webpack 的一个 loader,用于处理 .vue 文件

# 特性
  • 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug;
  • 允许在一个 .vue 文件中使用自定义块,并对其运用自定义的 loader 链;
  • 使用 webpack loader 将 <style><template> 中引用的资源当作模块依赖来处理;
  • 为每个组件模拟出 scoped CSS
  • 在开发过程中使用热重载来保持状态。
# 用途

js可以写es6、style样式可以scss或less、template可以加jade等根据官网的定义,

# 示例

vue 组件包含模板、js 和样式。vue-loader 用来将模板、JS 和样式拆分。所以还得额外安装另外的预加载器

  • vue-template-compiler 来编译模板;
  • css-loader 处理样式;
npm install vue vue-loader vue-template-compiler css-loader -D
1
 "css-loader": "^0.28.0",
 "vue-loader": "^12.1.0",
 "vue-style-loader": "^3.0.1",
 "vue-template-compiler": "^2.3.3",
1
2
3
4
module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      }
    ]
  }
};
var vueLoaderConfig = require('./vue-loader.conf')
{
    test: /\.vue$/,
     loader: 'vue-loader',
     options: vueLoaderConfig
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 样式相关

# Scoped CSS

<style> 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素。可以使组件的样式不相互污染。如果一个项目的所有style标签都加上了scoped属性,相当于实现了样式的模块化。

scoped属性的实现原理是给每一个dom元素添加了一个独一无二的动态属性,给css选择器额外添加一个对应的属性选择器,来选择组件中的dom。

这类似于 Shadow DOM 中的样式封装。它有一些注意事项,但不需要任何 polyfill。它通过使用 PostCSS 来实现以下转换:

<style scoped>
.example {
  color: red;
}
</style>
<template>
  <div class="example">hi</div>
</template>
//转换结果
<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>
<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 子组件的根元素

**使用 scoped 后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。**这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。

# scoped样式穿透

scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性。有以下两种方式实现:

  • 使用/deep/
//Parent
<template>
<div class="wrap">
    <Child />
</div>
</template>

<style lang="scss" scoped>
.wrap /deep/ .box{
    background: red;
}
</style>

//Child
<template>
    <div class="box"></div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 使用两个style标签
//Parent
<template>
<div class="wrap">
    <Child />
</div>
</template>

<style lang="scss" scoped>
//其他样式
</style>
<style lang="scss">
.wrap .box{
    background: red;
}
</style>

//Child
<template>
    <div class="box"></div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 官方webpack模板(v3)

https://github.com/vuejs-templates/webpack

# vue-cli

vue-cli3+webpack4 ( vue-cli 的 webpack 模板已经预先配置好)

# 优化

# 实现按需加载

# 第三方模块按需导入

不过很多组件库已经提供了现成的解决方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安装以上插件后,.babelrc配置中或babel-loader的参数中进行设置,即可实现组件按需加载了

后面还有路由按需加载处理;

原理:node_modules/babel-plugin-component/lib/core.js

//95-98行左右
if (!cachePath[libraryName]) {
    var themeName = styleLibraryName.replace(/^./_image/06.webpack-use-perf/, '');
    cachePath[libraryName] = styleLibraryName.indexOf('~') === 0 ? resolve(process.cwd(), themeName) : "".concat(libraryName, "/").concat("packages", "/").concat(themeName);
}
1
2
3
4
5

实践:.babelrc

{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",{
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

npm install babel-plugin-import --save-dev
// .babelrc
{
  "plugins": [["import", {
    "libraryName": "iview",
    "libraryDirectory": "src/components"
  }]]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from 'vue';
import {
    Dialog,
    Autocomplete,
    Dropdown,
    ...
} from 'element-ui';
Vue.use(Pagination);
Vue.use(Dialog);
Vue.use(Autocomplete);

//import { Button } from 'iview'
//Vue.component('Table', Table)

...
//这里提个醒
//MessageBox,Message,Notification这三个组件只能挂载Vue原型上调用,
//不能使用Vue.use();否则项目运行会默认执行一次,即使没有使用它们

//样式和字体引入
/*icon字体路径变量*/
$--font-path: "~element-ui/lib/theme-chalk/fonts";
/*按需引入用到的组件的scss文件和基础scss文件*/
@import "~element-ui/packages/theme-chalk/src/base.scss";
@import "~element-ui/packages/theme-chalk/src/button.scss";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

单页应用的按需加载 现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。

通过import(*)语句来控制加载时机,webpack内置了对于import(*)的解析,会将import(*)中引入的模块作为一个新的入口在生成一个chunk。 当代码执行到import(*)语句时,会去加载Chunk对应生成的文件。import()会返回一个Promise对象,所以为了让浏览器支持,需要事先注入Promise polyfill

# 路由懒加载

# vue按需加载配合webpack设置

webpack中提供了require.ensure()来实现按需加载。以前引入路由是通过import 这样的方式引入,改为const定义的方式进行引入。

不进行页面按需加载引入方式

import home from ‘…//common/home.vue’
1

进行页面按需加载的引入方式:

const home = r => require.ensure( [], () => r (require(’…//common/home.vue’)))
//或者;
const home = () => import(/* webpackChunkName: "home" */ '../pages/home/index')

//简写方式;
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  }
1
2
3
4
5
6
7
8
9
10

# 利用webpack来优化前端性能(提高性能和体验)

用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效;

  • 对于 Webpack4,打包项目使用 production 模式,这样会自动开启代码压缩;
  • 优化图片,对于小图可以使用 base64 的方式写入文件中;
  • 按照路由拆分代码,实现按需加载;
  • 给打包出来的文件名添加哈希,实现浏览器缓存文件;
  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的UglifyJsPluginParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩css;
  • 利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于output参数和各loader的publicPath参数来修改资源路径;
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现;使用 ES6 模块来开启 tree shaking,这个技术可以移除没有使用的代码;
  • 提取公共代码;CommonsChunkPlugin;

# 提高构建(优化打包)速度【要点】

  • 使用Tree-shakingScope Hoisting来剔除多余代码;

  • 多入口情况下,使用CommonsChunkPlugin来提取公共代码;通过externals配置来提取常用库

  • 减少文件搜索范围**

    • 比如通过别名
    • loader 的 test,include & exclude
  • Webpack4 默认压缩并行

  • babel 也可以缓存编译;通过cacheDirectory选项开启loader: 'babel-loader?cacheDirectory'

  • 利用DllPluginDllReferencePlugin预编译资源模块 通过DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin将预编译的模块加载进来。

  • 使用Happypack 实现多线程加速编译

  • 使用webpack-uglify-parallel提升uglifyPlugin的压缩速度。 原理上webpack-uglify-parallel采用了多核并行压缩来提升压缩速度

  • 修改source-map配置;

    productionSourceMap: false,
    config.when(!isProd, config => config.devtool('cheap-source-map'))
    
    1
    2

# vue项目编译优化详细

缩小文件的搜索范围

  • 优化Loader配置

    由于Loader对文件的转换操作很耗时,所以需要尽可能少的文件被Loader处理。我们可以通过以下3方面优化Loader配置:

    (1)优化正则匹配

    (2)通过cacheDirectory选项开启缓存

    (3)通过include、exclude来减少被处理的文件

    /项目原配置:
    {
      test: /\.js$/,
      loader: 'babel-loader',
      include: [resolve('src'), resolve('test')]
    },
    //优化后配置:
    {
      // 1、如果项目源码中只有js文件,就不要写成/\.jsx?$/,以提升正则表达式的性能
      test: /\.js$/,
      // 2、babel-loader支持缓存转换出的结果,通过cacheDirectory选项开启
      loader: 'babel-loader?cacheDirectory',
      // 3、只对项目根目录下的src 目录中的文件采用 babel-loader
      include: [resolve('src')]
    },
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 优化resolve.modules配置

    resolve.modules 用于配置Webpack去哪些目录下寻找第三方模块。和Node.js的模块寻找机制很相似。

    resolve: {// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
      modules: [path.resolve(__dirname,'node_modules')]
    },
    
    1
    2
    3
  • 优化resolve.alias配置

    alias: {
      '@': resolve('src'),
    },
    import common from '@/common.js';//通过配置,引用src底下的common.js文件,就可以直接这么写
    
    1
    2
    3
    4
  • 优化resolve.extensions配置

    在导入语句没带文件后缀时,Webpack 会在自动带上后缀后去尝试询问文件是否存在。默认是:extensions :[‘. js ‘,’. json ’] 。如果这个列表越长,或者正确的后缀越往后,就会造成尝试的次数越多,所以 resolve .extensions 的配置也会影响到构建的性能

    优化措施

    • 后缀尝试列表要尽可能小,不要将项目中不可能存在的情况写到后缀尝试列表中。

    • 频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程。

    • 在源码中写导入语句时,要尽可能带上后缀,从而可以避免寻找过程。例如在确定的情况下将 require(’. /data ’)写成require(’. /data.json ’),可以结合enforceExtension 和 enforceModuleExtension开启使用来强制开发者遵守这条优化

  • 优化resolve.noParse配置

    **noParse配置项可以让Webpack忽略对部分没采用模块化的文件的递归解析和处理,这 样做的好处是能提高构建性能。**原因是一些库如jQuery、ChartJS 庞大又没有采用模块化标准,让Webpack去解析这些文件既耗时又没有意义。 noParse是可选的配置项,类型需要是RegExp 、[RegExp]、function中的一种。例如,若想要忽略jQuery 、ChartJS ,则优化配置如下:

    // 使用正则表达式 
    noParse: /jquerylchartjs/ 
    // 使用函数,从 Webpack3.0.0开始支持 
    noParse: (content)=> { 
      return /jquery|chartjs/.test(content); // 返回true或false
    }
    
    1
    2
    3
    4
    5
    6

减少冗余代码

babel-plugin-transform-runtime 是Babel官方提供的一个插件,作用是减少冗余的代码

Babel在将ES6代码转换成ES5代码时,通常需要一些由ES5编写的辅助函数来完成新语法的实现,例如在转换 class extent 语法时会在转换后的 ES5 代码里注入 extent 辅助函数用于实现继承。

babel-plugin-transform-runtime会将相关辅助函数进行替换成导入语句,从而减小babel编译出来的代码的文件大小。 .babelrc

"plugins": [
    "transform-runtime"
]
1
2
3

使用HappyPack多进程解析和处理文件

由于有大量文件需要解析和处理,所以构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack构建慢的问题会显得更为严重。运行在 Node.之上的Webpack是单线程模型的,也就是说Webpack需要一个一个地处理任务,不能同时处理多个任务。Happy Pack (opens new window) 就能让Webpack做到这一点,它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程

webpack.base.conf.js 文件对module.rules进行配置;

module: {
    rules: [
        {
            test: /\.js$/,// 将对.js 文件的处理转交给 id 为 babel 的HappyPack实例
            use:['happypack/loader?id=babel'],
            include: [resolve('src'), resolve('test'),   
                      resolve('node_modules/webpack-dev-server/client')],
            exclude:path.resolve(__dirname,'node_modules'),// 排除第三方插件
        },
        {
            test: /\.vue$/,
            use: ['happypack/loader?id=vue'],
        },
    ]
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

webpack.prod.conf.js 文件进行配置

const HappyPack = require('happypack');
const HappyPackThreadPool = HappyPack.ThreadPool({size:5});//共享进程池,包含5个子进程
plugins: [
    new HappyPack({
        id:'vue',// 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
        loaders:[{
            loader:'vue-loader',
            options: vueLoaderConfig
        }
                ],
        threadPool: HappyPackThreadPool,
    }),
    new HappyPack({
        id:'babel',// 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
        loaders:['babel-loader?cacheDirectory'],// 如何处理.js文件,用法和Loader配置中一样
        threadPool: HappyPackThreadPool,
    }),
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

使用ParallelUglifyPlugin多进程压缩代码文件

由于压缩JavaScript 代码时,需要先将代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理AST ,所以导致这个过程的计算量巨大,耗时非常多。原本会使用UglifyJS去一个一个压缩再输出,但是ParallelUglifyPlugin会开启多个子进程

const ParallelUglifyPlugin =require('webpack-parallel-uglify-plugin');
plugins: [
    new ParallelUglifyPlugin({
        cacheDir: '.cache/',
        uglifyJs:{
            compress: {
                warnings: false
            },
            sourceMap: true
        }
    }),
]
1
2
3
4
5
6
7
8
9
10
11
12

使用自动刷新

相关优化措施:

(1)配置忽略一些不监听的一些文件,如:node_modules。

(2)watchOptions.aggregateTirneout 的值越大性能越好,因为这能降低重新构建的频率。

(3) watchOptions.poll 的值越小越好,因为这能降低检查的频率。

devServer: {
  watchOptions: {
    ignored: /node_modules/,// 不监听的文件或文件夹,支持正则匹配
    aggregateTimeout: 300,// 监听到变化后等300ms再去执行动作
    poll: 1000// 默认每秒询问1000次
  }
}
1
2
3
4
5
6
7

开启模块热替换

DevServer 还支持一种叫做模块热替换( Hot Module Replacement )的技术可在不刷新整个网页的情况下做到超灵敏实时预览。原理在一个源码发生变化时,只需重新编译发生变化的模块,再用新输出的模块替换掉浏览器中对应的老模块 。模块热替换技术在很大程度上提升了开发效率和体验 。

devServer: {
  hot: true,
},
plugins: [
  new webpack.HotModuleReplacementPlugin(),
  new webpack.NamedModulesPlugin(), // HMR shows correct file names显示被替换模块的名称
]
1
2
3
4
5
6
7

提取公共代码

如果每个页面的代码都将这些公共的部分包含进去,则会造成以下问题 :

• 相同的资源被重复加载,浪费用户的流量和服务器的成本。

• 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。

如果将多个页面的公共代码抽离成单独的文件,就能优化以上问题 。Webpack内置了专门用于提取多个Chunk中的公共部分的插件CommonsChunkPlugin。 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中

new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: function(module, count) {
        return (
            module.resource &&
            /\.js$/.test(module.resource) &&
            module.resource.indexOf(
                path.join(__dirname, '../node_modules')
            ) === 0
        );
    }
}),
new webpack.optimize.CommonsChunkPlugin({// 抽取出代码模块的映射关系
    name: 'manifest',
    chunks: ['vendor']
}),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

提取公共css

当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。

var ExtractTextPlugin = require("extract-text-webpack-plugin")
module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          extractCSS: true
        }
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin("style.css")
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

按需(懒)加载代码

通过vue写的单页应用时,可能会有很多的路由引入。当打包构建的时候,javascript包会变得非常大,影响加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。

const Foo = () => import('./Foo.vue')
const router = new VueRouter({
  routes: [
    { path: '/foo', component: Foo }
  ]
})
1
2
3
4
5
6

优化SourceMap

我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩,去掉多余的空格,且babel编译化后,最终会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发不好调式,因此sourceMap出现了,它就是为了解决不好调式代码问题的。

开发环境推荐: cheap-module-eval-source-map

生产环境推荐: cheap-module-source-map

模式设置分析:

  • cheap: 源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息
  • module :不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个 Vue 文件报错了,我们希望能定位到具体的 Vue 文件,因此我们也需要 module 配置;
  • eval-source-map:eval 打包代码的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。【开发环境】
  • soure-map :source-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加source-map 属性;【生产环境】

示例:

productionSourceMap: true,
module.exports = merge(baseWebpackConfig, {//webpack.dev.conf.js
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: '#cheap-module-eval-source-map'
}
var webpackConfig = merge(baseWebpackConfig, {//webpack.prod.conf.js
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true
    })
  },
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

构建结果输出分析

为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来; webpack-bundle-analyzer

if (config.build.bundleAnalyzerReport) {
    var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
//执行 $ npm run build --report 后生成分析报告如下:
1
2
3
4
5

模板预编译

当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。

预编译模板最简单的方式就是使用单文件组件 (opens new window)——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。

如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader (opens new window),它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数

启用DllPlugin和DllReferencePlugin预编译库文件

这是最复杂也是提升效果最明显的一步,原理是将第三方库文件单独编译打包一次,以后的构建都不需要再编译打包第三方库

  • 增加build/webpack.dll.config.js文件,并在其中配置需要单独DLL化的模块
const path = require("path")
const webpack = require("webpack")

module.exports = {
  // 你想要打包的模块的数组
  entry: {
    vendor: ['vue/dist/vue.esm.js', 'axios', 'vue-router', 'iview']
  },
  output: {
    path: path.join(__dirname, '../static/js'), // 打包后文件输出的位置
    filename: '[name].dll.js',
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, '.', '[name]-manifest.json'),
      name: '[name]_library',
      context: __dirname
    }),
    // 压缩打包的文件
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    })
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • build/webpack.dev.conf.jsbuild/webpack.prod.conf.js 增加如下插件
new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require('./vendor-manifest.json')
})
1
2
3
4
  • /package.json增加命令
"dll": "webpack --config ./build/webpack.dll.config.js"
1
  • /index.html增加DLL化JS引入(必须首先引入)
<script src="/static/js/vendor.dll.js"></script>
1
  • 执行构建
npm run dll(这一步会生成build/vendor-manifest.json和static/js/vendor.dll.js)
npm run dev 或 npm run build
1
2

# 相关问题

# Babel 原理

本质就是编译器,当代码转为字符串生成 AST,对 AST 进行转变最后再生成新的代码;(分为三步

  1. 词法分析生成 Token,
  2. 语法分析生成 AST,
  3. 遍历 AST,根据插件变换相应的节点,最后把 AST 转换为代码

ES6是如何实现编译成ES5的?

# css-loader的原理

# 配置单页应用及多页应用

单页应用可以理解为webpack的标准模式,直接在entry中指定单页应用的入口即可,这里不再赘述

多页应用的话,可以使用webpack的 AutoWebPlugin来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:

  • 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表
  • 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

# 参照链接

https://www.webpackjs.com/

https://webpack.js.org/

上次更新: 2022/04/15, 05:41:27
×