webpack

模块打包器:开发一个项目,业务逻辑会很多,开发会按照功能逻辑拆分成一个个的模块,这样开发的时候更加有条理,维护起来也会更加方便。

但这样就会涉及到一个问题,模块之间会有复杂的依赖关系,在处理这些模块依赖的时候,后端的开发有着得天独厚的条件,模块化是天生支持的。

模块打包器会先分析项目依赖,然后按照复杂的规则把它们打包在一起,当然这些规则是隐藏起来的,不需要知道是怎么打包的,只需要知道这个打包器专门会把你的模块依赖的代码都打包到一起,输出一个新文件,你会得到新的js文件。

它不仅能帮你打包js文件,还有其他资源文件,都会视为模块,都会打包。

但如果只是打包,那就太小瞧webpack了,它有着很强大的生态,有各种loader,来帮你处理文件的内容,比如编译语法,处理路径,还有插件辅助你开发和项目构建,从而加快你的开发效率,如果你在开发一个大型单页应用,它的代码分割功能对页面的性能是意义非凡的,从项目的起始到项目上线,它参与了整个的项目周期。

更新部分:
NamedModulesPlugin =》 optimization.namedModules;CommonsChunkPlugin =》 optimization.splitChunks, optimization.runtimeChunk

名词:

  • chunk:代码块
  • bundle : 被打包过的
  • module : 模块

核心概念:

  • entry:
    • 代码的入口
    • 打包的入口(依次查找依赖)
    • 单个或多个
module.exports = {
    entry : 'index.js'
    ====
    entry:['index.js','vendors.js'] //创建多个入口
    ====
    // 推荐,扩展性好,直接新增即可
    entry : {
        index : 'index.js',// index对应的是index.js
        vendor : 'verdor.js'
    }
}
  • output:
    • 打包生成的文件(bundle)
    • 一个或多个
    • 自定义规则
    • 配个cdn
module.exports = {
    entry : {
        index : 'index.js',// index对应的是index.js
        vendor : 'verdor.js'
    }
    output : {
        filename : 'index.min.js'
        ==
        //多个:自定义规则
        // [name]对应入口文件,hash是打包过程后的版本号
        filename:'[name].min.[hash:5].js'
        
    }
}
  • loaders:
    loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
    loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

主要负责:

  • 处理文件
  • 转化为模块

属性:

  • test:用于标识出应该被对应的 loader 进行转换的某个或某些文件。
  • use:表示进行转换时,应该使用哪个 loader。
const config = {
  output: {
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
//以txt结尾的文件,用raw-loade转换
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }
};
  • plugins:
    • 参与打包整个过程
    • 打包优化和压缩
    • 配置编译时的变量
    • 及其灵活
  plugins: [
    new webpack.optimize.UglifyJsPlugin(),
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]

webpack使用

  • webpack-cli
    安装:npm install webpack-cli -g
    • 交互式的初始化一个项目
    • 迁移项目!v1-v2

打包

一、打包JS

命令

webpack entry<entry> output
===
webpack --config wenpack.conf.js

示例1:es6模块化打包

//app.js,入口文件
import sum from './sum'
console.log('sum(23,24) = ', sum(23,24))

终端输入命令:

webpack app.js bundle.js

这时候就打包出了一个bundle.js,在index中引入这个js文件即可。


屏幕快照 2018-08-09 下午4.44.19.png

示例2:common.js打包

//minus.js
module.exports = function(a,b){
    return a - b
}
//app.js
let minus = require('./minus')
console.log('minus(24,17) = ', minus(24,17))

执行命令:webpack app.js bundle.js,可以看到成功的被打包了。


屏幕快照 2018-08-09 下午4.57.26.png

示例2:AMD打包

//muti.js
define(function(require, factory) {
    'use strict';
    return function(a,b){
        return a*b
    }
});
//app.js
require(['muti'],function(muti){
    console.log('muti(2,3)=',muti(2,3))
})

打包成功!!!这时候发现打包出了两个文件:0.bundle.js和bundle.js

  • webpack.config.js :配置文件,使用commonJS语法
//webpack.config.js
module.exports = {
    entry : {
        app : './app.js'
    },
    output:{
        filename : '[name].[hash:5]'
    }
}

这时候打包了两个文件:


屏幕快照 2018-08-09 下午6.21.45.png

二、编译 ES6

babel

npm install babel-loader@8.0.0-beta.0 @babel/core

新建index.html,webpack.config.js

    module:{
        rules : [
            {
                test:/\.js$/,
                use:'babel-loader',
                // 排除在规则之外的不编译
                exclude:'/node_module/'
            }
        ]

    }

  • babel presets
    参数
    • targets : 目标,告诉babel,当编译的时候根据指定的目标选择哪些语法进行编译,哪些不编译
    • targets.browsers :浏览器环境
    • targets.vrowers : 'last 2 versions'
    • target.browers :">1%" : 大于市场份额1%的浏览器
    • browerslist : 指定支持的浏览器
    • can I use
npm install @babel/preset-env -save-dev

为babel指定presets编译es5,es5....

rules : [
            {
                test:/\.js$/,
                use:{
                    loader:'babel-loader',
                    options:{
                        // 给loader指定presets
                        presets:['@babel/preset-env',{
                            targets:{
                                browsers:['>1%','last 2 versions']
                            }
                        }]
                    }                    
                },
                exclude:'/node_module/'
            }

打包,看结果:


屏幕快照 2018-08-10 上午12.43.43.png

可以看出,es6语法被转成了es5。

babel的两个插件: babel plyfill , babel runtime transform

babel-preset-es2015 是一个babel的插件,用于将部分ES6 语法转换为ES5 语法。但是babel-preset并不会转换promise、generator等函数,我们还要引入babel-polify库。
项目中现在一般直接使用babel-preset-env,她整合了babel-preset-es2015, babel-preset-es2016, and babel-preset-es2017,而且可以配置需要支持的浏览器/运行环境,仅转化需要支持的语法,使文件更轻量

插件作用:解决实现低版本函数和方法不支持。
- Generator
- set
- Map
- Arry.from
- Array.protptype.includes

  • babel plyfill :
    全局垫片,为应用准备。打包后可以在文件中看到:它会判断如果浏览器不支持这个方法(如includes),就会在生成的打包文件加上这个方法的实现。

    npm install babel-polyfill --save //--save,真实项目中的依赖
    //使用
    import "babel-polyfill" ```
    
    
  • babel runtime transform:
    局部垫片,不会污染全局,为框架准备

    npm install @babel/runtime --save
    npm install @babel/plgin-transform-runtime --save-dev
    

2、使用

  • 安装这两个插件
  • 入口文件引入:import "babel-polyfill/runtime,即可使用新的语法。
  • 根目录新建 : .babelrc,把babel都写在这个文件,之前写在webpack.config.js中的options拿过来,如下:
{
    "presets" : [
        ["@babel/preset-env",{
            "targets":{
                "browsers":["last 2 versions"]
            }
        }]
    ],
    "plugins": ["@babel/transform-runtime"]
}

三、编译 Typescript:js

Typescript:js的超集,来自微软,需要使用对应的loader。

  • typescript-loader

    • 安装:
    npm i typescript ts-loader --save-dev //官方
    npm i typescript awesome-typescript-loader --save-dev //第三方开发
    
    • 配置
      • tsconfig.json,建在根目录
        • 配置选项:详见官网
        • 常用选项:
          • compilerOptions
          • include
          • exclude
      • webpack.config.js
        使用:
  • 下载 : npm install webpack typescript ts-loader awesome-typescript-loader --save-dev

  • 新建tsconfig.json,在根目录

{
    "compilerOptions": {
        "module": "commonjs",
        // 指定编译后的文件的运行环境
        "target":"es5",
        "allowJs": true
    },
    // 路径
    "include": [
        "./src/*"
    ],
    // 指定不编译的部分
    "exclude": [
        "./node_modules"
    ]
}
//webpack.config.js
module.exports = {
    entry : {
        'app' : './src/app.ts'
    },
    output : {
        filename:'[name].bundel.js'
    },
    module : {
        rules : [
            {
                test : /\.tsx?$/,
                user : {
                    loader : 'ts-loader'
                }
            }
        ]
    }
}
// app.ts
const NUM = 45

interface Cat {
    name : String,
    gender : String
}

function touchCat (cat:Cat){
    console.log('miao~',cat.name)
}

touchCat({
    name : 'tom',
    gender : 'male'
})

打包公共代码 : SplitChunksPlugin

现在都是模块化开发,这样就会有模块互相依赖的情况,公共模块就是公共代码。
SplitChunksPlugin的登场就是为了抹平之前CommonsChunkPlugin的痛的,它能够抽出懒加载模块之间的公共模块,并且不会抽到父级,而是会与首次用到的懒加载模块并行加载,这样我们就可以放心的使用懒加载模块了.
SplitChunksPlugin的好,好在解决了入口文件过大的问题还能有效自动化的解决懒加载模块之间的代码重复问题
目的

  • 减少代码冗余
  • 提高加载速度
    屏幕快照 2018-08-10 上午3.46.36.png

    插件:
  • SplitChunksPlugin(之前的版本是CommonsChunkPlugin)
    • 参数 :

      • chunks: 表示显示块的范围,有三个可选值:initial(初始块)、async(按需加载块)、 all(全部块),默认为all;
      • minSize: 表示在压缩前的最小模块大小,默认为0;
      • minChunks: 表示被引用次数,默认为1;
      • maxAsyncRequests: 最大的按需(异步)加载次数,默认为1;
      • maxInitialRequests: 最大的初始化加载次数,默认为1;
      • name: 拆分出来块的名字(Chunk Names),默认由块名和hash值自动生成;
      • cacheGroups: 缓存组。
    • 使用场景:

      • 单页应用
      • 单页应用 + 第三方依赖
      • 多页应用 + 第三方依赖 + webpack生成代码
    • 使用步骤:

      • 安装局部webpack : npm install webpack --save-dev,因为这个插件是webpack自带的,所以需要安装在本地。(全局webpack理解为是工具,局部就是本文件夹处理依赖的)
      • 新建subpageA.js(a),subpageB.js(b),module.js(c),a、b都引用c,这时c就是公共模块。在配置文件里写 :
!!!!CommonsChunkPlugin已被弃用
        // 代码合并
        new webpack.optimize.CommonsChunkPlugin({
            // 生成的文件
            name : 'common',
            minChunks : 2
        })
!!!现在用SplitChunksPlugin
        new webpack.optimize.SplitChunksPlugin({
            name : 'common',
            minChunks: 2,
        })
!!!或者写成以下(和plugins同级)
optimization: { 
        splitChunks: { 
            cacheGroups: { 
                commons: {
                     name: "common", 
                     chunks: "initial", 
                     minChunks: 2 
                } 
            } 
        } 
    }

这时候打包发现并没有把公共部分提取出来。
附详细配置

new webpack.optimize.SplitChunksPlugin({
                    chunks: "initial", // 必须三选一: "initial" | "all"(默认就是all) | "async"
                    minSize: 0, // 最小尺寸,默认0
                    minChunks: 1, // 最小 chunk ,默认1
                    maxAsyncRequests: 1, // 最大异步请求数, 默认1
                    maxInitialRequests: 1, // 最大初始化请求书,默认1
                    name: function () {
                    }, // 名称,此选项可接收 function
                    cacheGroups: { // 这里开始设置缓存的 chunks
                        priority: 0, // 缓存组优先级
                        vendor: { // key 为entry中定义的 入口名称
                            chunks: "initial", // 必须三选一: "initial" | "all" | "async"(默认就是异步)
                            name: "vendor", // 要缓存的 分隔出来的 chunk 名称
                            minSize: 0,
                            minChunks: 1,
                            enforce: true,
                            maxAsyncRequests: 1, // 最大异步请求数, 默认1
                            maxInitialRequests: 1, // 最大初始化请求书,默认1
                            reuseExistingChunk: true // 可设置是否重用该chunk(查看源码没有发现默认值)
                        }
                    }
                })

代码分割和懒加载

在前端的优化过程中,一个很常见的手段就是对代码切分,让用户在浏览的时候加载更少的代码,通过代码分割和懒加载,可以节省加载时间,如果用户只浏览一个页面的时候却下载了所有的代码,那么就会对用户的带宽造成浪费,影响浏览时间。
代码分割和懒加载是一回事,wenpack会自动把代码分割之后再把需要加载的代码加载过来。
这两个虽然是webpack的功能,但是并不在webpack的配置中。
实现方式

  • webpack methods
  • require.ensure:动态加载模块,对promise有依赖
    参数 :
    • [] : dependencies
    • callback : 执行
    • errorCallback: 可以省略
    • chunkName
require.ensure(['lodash'],function () {
    // 此处require是异步,不是commonjs
    var _ = require('lodash')
},'vendor') 
  • require.include

当两个模块都依赖了第三方模块的时候,
可以提前把第三方模块放到父模块里,这样动态加载两个模块的时候,由于父模块已经有了这个第三方模块,所以不会重复加载。比如a,b依赖c,将c在引用ab的父级模块中用此方法先引入,就可以把c模块提取在这个父级模块中。

  • ES 2015 Loader spec
    - import():返回的是promise,传入动态加载的模块名后,就可以像使用promise一样去使用 : import().then()
    import与require.ensure最大的区别:他在引入的时候会直接执行,而不需要require了
import('./subPageA').then(function(){
})

业务场景

一、代码分割
- 分离业务代码和第三方依赖
- 分离业务代码业务公共代码和第三方依赖
- 分离首次加载和访问后加载的代码(提高首屏加载速度)

代码演示:

  • 单entry:把第三方依赖单独出来
import './subpageA'
import './subpageB'
// vendor指定chunk名称
// 外层ensure只是将lodash加载到了页面中,并不执行
// 在回调中require才是真正的执行
// ensure中可以省略不写参数ensure(['']
// 打包结果 : lodash被提取到了vendor,实现了第三方依赖了业务代码的分离
require.ensure(['lodash'],function () {
    // 此处require是异步,不是commonjs
    var _ = require('lodash')
    _.join(['1','2'],'3')
},'vendor') 
export default 'pageA'
  • 如果两个文件(a,b)依赖同一个文件(c),希望把c单独出来,如果是同步的引用方式,是无法实现的,在入口文件引用a,b的时候,就可以动态引入:
//import './subpageA'
//import './subpageB'
require.ensure(['./subpageA'],function (params) {
    let subpageA  = require('./subpageA')
},'subpageA')

require.ensure(['./subpageB'],function (params) {
    let subpageA  = require('./subpageB')
},'subpageB')


require.ensure(['lodash'],function () {
    // 此处require是异步,不是commonjs
    var _ = require('lodash')
    _.join(['1','2'],'3')
},'vendor') 
export default 'pageA'

屏幕快照 2018-08-12 上午3.39.24.png

这时候发现,c没有被单独打包出来.
把c模块先引进,这样就把c模块提取到了引用ab模块的父级模块上。
概括一下 : a,b依赖c,将c在引用ab的父级模块中用此方法先引入,就可以把c模块提取在这个父级模块中。

require.include('./moduleA')
.......(省略代码和上面的代码一致)

前端面试之webpack面试常见问题

概念问题一:什么是webpack和grunt和gulp有什么不同

Webpack是一个模块打包器,他可以递归的打包项目中的所有模块,最终生成几个打包后的文件。他和其他的工具最大的不同在于他支持code-splitting、模块化(AMD,ESM,CommonJs)、全局分析。

问题二:什么是bundle,什么是chunk,什么是module?

答案:bundle是由webpack打包出来的文件,chunk是指webpack在进行模块的依赖分析的时候,代码分割出来的代码块。module是开发中的单个模块。

问题三:什么是Loader?什么是Plugin?

答案:
1)Loaders是用来告诉webpack如何转化处理某一类型的文件,并且引入到打包出的文件中
2)Plugin是用来自定义webpack打包过程的方式,一个插件是含有apply方法的一个对象,通过这个方法可以参与到整个webpack打包的各个流程(生命周期)。

问题:如何可以自动生成webpack配置?

答案: webpack-cli /vue-cli /etc ...脚手架工具

问题一:webpack-dev-server和http服务器如nginx有什么区别?

答案:webpack-dev-server使用内存来存储webpack开发环境下的打包文件,并且可以使用模块热更新,他比传统的http服务对开发更加简单高效。

问题二:什么 是模块热更新?

答案:模块热更新是webpack的一个功能,他可以使得代码修改过后不用刷新浏览器就可以更新,是高级版的自动刷新浏览器。

化问题一:什么是长缓存?在webpack中如何做到长缓存优化?

答案:浏览器在用户访问页面的时候,为了加快加载速度,会对用户访问的静态资源进行存储,但是每一次代码升级或是更新,都需要浏览器去下载新的代码,最方便和简单的更新方式就是引入新的文件名称。在webpack中可以在output纵输出的文件指定chunkhash,并且分离经常更新的代码和框架代码。通过NameModulesPlugin或是HashedModuleIdsPlugin使再次打包文件名不变。

化问题二:什么是Tree-shaking?CSS可以Tree-shaking吗

答案:Tree-shaking是指在打包中去除那些引入了,但是在代码中没有被用到的那些死代码。在webpack中Tree-shaking是通过uglifySPlugin来Tree-shaking
JS。Css需要使用Purify-CSS。

推荐阅读更多精彩内容