webpack学习笔记

383 阅读15分钟

webpack中的关键概念

entry

指定webpack打包的入口文件的位置,多页面可以有多入口

output

指定输出文件的路径

loader

  • webpack 原生只支持js和json,所以需要各种loader来处理各式各样的文件.
  • loader 本身是一个函数,接受源文件,返回转换的结果.
  • 一个规则中的多个loader从右向左链式调用.
  • 通过option

常用loader

plugins

  • 对webpack功能的增强,所有loader无法实现功能,都可以用插件来实现.
  • 作用于整个构建过程.

常用插件

mode

指定当前的构建环境.development/production/none,开启不同的模式webpack的默认行为不同.

不同模式下的默认行为

module.exports = {
    entry: "./src/inde.xhs,
    output: {
        filename: 'bundle.js,
        path:path,
    }
    modules: {
        rules: [
            {
                test:/.css$/,
                use: [
                    loader:"css-loader",
                    option:"",
                ]
                
            }
        ]
    }
    pulgins: []
}

理解module/chunk/bundle

  • module即模块,和js模块化中的模块应当是一样的,例如组件或者一组公用的方法可以称为模块.webpack中,模块还可以是css/资源文件.webpack的配置文件中,module字段就表明如何去处理每个类型的模块.
  • chunk,是module到bundle的中间过程概念,chunk是对module的按照某种规则的再封装或者组织.从体积上说,module < chunk < bundle.
    • chunk的概念是为了配合浏览器的缓存机制,使得一次代码更新尽可能少的让浏览器重新拉取数据.
  • bundle,是最终打包出来的一个个文件,是由chunk构建成的.很多情况下chunk和bundle是一对一的.

生成chunk的几种方式

  • entry.对于传递对象的多入口可以生成多个chunk.
  • 打包分块.
  • 异步模块.
module.exports = {
  entry: {
    main: __dirname + "/app/main.js",
    other: __dirname + "/app/two.js",
  },
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "[name].js", //打包后输出文件的文件名
    chunkFilename: '[name].js',// 用于异步模块
  },

  optimization: {
    // webpack自身的运行时代码,是否需要分离出去,该属性的值可以是string(single/multiple),object,boolean
    //single 表示所有chunk共享一个运行时
    //multiple/true 表示不共享,每个chunk都会有运行时chunk
    runtimeChunk: "single",
    splitChunks: {
      //此属性中,包含的是一个个如何组织chunk到bundle的规则
      cacheGroups: {
        commons: {
          chunks: "initial",
          minChunks: 2,
          // webpack默认会限制同时最多只能发送3个请求
          maxInitialRequests: 5,
          // 小于minSize的chunk不会单独分离,默认是30kb.
          minSize: 0 
        },
        vendor: {
          test: /node_modules/,
          chunks: "initial",
          name: "vendor",
          // chunk匹配中多个规则时,根据优先级来确定使用哪个
          priority: 10,
          enforce: true
        }

      },
    }
  }
}

一些基础配置

支持es6

  • 需要@babel/core @babel/present-env babel-loader
  • 配置.babelrc.两个重要的概念:
    • plugins.一个plugin对应一个功能.
    • preset.一组plugin的集合.需要支持es6的话,需要引入@babel/preset-env.
  • 引入@babel/preset-react来支持react的jsx语法.

解析css

  • css-loader 加载css文件,转化为commonjs对象.
  • style-loader将样式通过<style>标签插入到head中.
  • less-loader sass-loader可以对less和sass进行解析.

解析图片和字体

  • file-loader 对文件解析,可以处理图片和字体文件
  • url-loader 内部也使用了file-loader,比其多的一个功能是可以自动将小资源转为base64.

文件监听

在文件发生变化时自动打包更新,不需要一直去build.

  • webpack --watch或者添加watch:true的配置来启动监听,但是得手动刷新浏览器.
  • 文件监听是通过webpack轮训文件的修改时间来实现的,配置如下:
module.export = {
    watch: true,//默认为false
    watchOptions: {
        ignored: /node_modules/,//支持正则
        aggregateTimeout: 300,//表示发现文件改变后不立即更新,再等待300ms,和其他修改的文件一起更新
        poll: 1000,//表示每秒检查100次
    }
}

热更新

热更新配置实现过程:

//webpack.config.js
moudle.export = {
    //....
    devServer: {
        contentBase: "./dist",//指定本地启动的服务器的根目录
        hot:"true" //设置为true会指定引入HotModuleRepalcementPlugin插件
    },
    //plugins: [
    //    new webpack.HotModuleRepalcementPlugin(),
    //]
}
  • webpack-dev-server --open 即可启动热跟新.
  • 热跟新指的是开发过程中代码发生改变后浏览器自动同步修改,不需要刷新,并且也没有刷新过程.
  • 热更新不会在文件系统上输出而是将打包结果保存在内存中.
  • 需要借助webpack-dev-server和webpack内置的插件HotModuleReplacementPlugin实现.
  • 也可以使用更加灵活的webpack-dev-middleware.wds其实是对wdm的封装.
  • 不设置hot:true的话,hmr不会启动,会使用live reload的方式了更新浏览.
    • 即源代码发生变化后,浏览器自动刷新,获取最新的代码.
    • 但是相较于hmr这样的缺陷是,页面重新load失去的当前的状态,而hmr不刷新,状态会保存,一些打开的窗口或者设置的字段不会重置.
  • hmr是增量更新.

热更新执行过程:

  1. 使用webpack-compiler将代码打包成bundle.
  2. 借助HotModuleReplacementPlugin插件,将HMR(Hot Module Replacement)代码打包在bundle中.
    • 此目的是为了让浏览器具有和本地服务器通信的能力.
    • 在浏览器中被打包进去的hmr称为HMR Runtime.
  3. wds会启动一个bundle server和一个hmr server.bundle-server使得浏览器可以通过http访问打包好的bundle. 首次启动时浏览器时,走的路径是上图中的1->2->A->B.
    • 这个过程bundle是在内存中,为什么http可以直接访问内存中的文件?
  4. 当有文件更新时走的上图中的1->2->3->4. 有文件更新时,编译器会重新打包编译,然后hmr sever利用websocket通知浏览器,浏览器在通过ws去获取变化的信息,进而判断是否需要热更新.

文件指纹策略

什么是文件指纹?

webpack打包出来的文件名带有的hash值,称为文件指纹.起到的作用是文件的版本号.

为什么需要文件指纹且有多种?

没有文件版本号的话,若想更新一个文件,为文件名不变化,由于浏览器有缓存,是不会拿到最新的文件.所以之前的做法是在URL中加入文件版本号的参数.现在有了webpack,可以直接在构建结果中给文件加上版本号.便于是浏览器得到最新文件.

多种文件指纹的目的是使得文件的更新能够精细化控制,如果所有文件使用一个版本号策略,那么只有一个文件的变动,会使得所有文件的版本号都改变,即使这些文件没改变,浏览器也需要花额外的资源去重新获取.所以针对不同的文件类型,使用不同的版本号策略

webpack中文件指纹的类型

  • hash,每次build都会生成,是和build相关的.
  • chunkhash,依据chunk的内容生成.用于js.似乎可以和contenthash互换使用.
  • contenthash,依据文件(所提取的部分)的内容生成.一般用在css(MiniCssExtractPlugin内)/资源文件中.
module.exports = {
  entry: {
    module_a: "./src/module_a.js",
    module_b: "./src/module_b.js"
  },
  output: {
    filename: "[name].[chunkhash:8].js"
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].[contenthash:8].css"
    })
  ]
};

代码压缩

  • js.使用webpack内置uglifyjs-webpack-plugin,在production模式下, 会自动压缩.
  • css使用optimize-css-assets-webpack-plugin+`cssnano(css预处理器).
  • html使用html-webpack-plugin
// 压缩html和构造html
new HtmlWebpackPlugin({
            // html模板位置
            template: path.join(__dirname, 'src/index.html'),
            // 打包后的名字
            filename: 'index.html',
            // 该html中要加载的chunk的名字
            chunks: ['index'],
            inject: true,
            // 压缩html的参数
            minify: {
                html5: true,
                collapseWhitespace: true,
                preserveLineBreaks: false,
                minifyCSS: true,
                minifyJS: true,
                removeComments: false
            }
        }),
        
        
//css压缩方式
new OptimizeCSSAssetsPlugin({
            assetNameRegExp: /\.css$/g,
            cssProcessor: require('cssnano')
        }),

webpack 进阶用法

自动清理构建目录

  • 使用clean-webpack-plugin.每次构建之前会清理构建目录下的文件,避免文件数量一致增加.
  • 使用时只需要将CleanWebpackPlugin的实例添加进插件数组中即可.

CSS前缀补齐

  • 需要添加loaderpostcss-loader,此loader中继续依赖插件autoprefixer.
rules = [
    {
        test: /.css$/,
        use: [
            "css-loader",
            {
                loader: "postcss-loader",
                options: {
                    plugins: () => [
                        require("autoprefixer")({
                            browsers: ["last 2 version", ">1%", "ios 7"],
                        });
                    ]
                }
            }
        ]
    }
]

px转rem

理解一些概念

  • 物理像素,实实在在存在的像素,是有rgb三通道组成的led小亮点.也称设备像素.平常说的分辨率就是物理像素.

  • 逻辑像素(CSS像素/渲染像素/DIP设备无关像素),是软件层面的设计,目的是为了让不同设备上的像素单位看起来是差不多的.

    • 在屏幕尺寸一定时,分辨率越高,那单个像素的实际物理大小就越小,如果不抽象一个逻辑像素,那么同样的像素数的元素,在不同ppi的屏幕上大小是不一样的.
    • 典型的例子就是同样的应用在pc的高分屏下,会变得特别小.
  • devicePixelRatio. 设备物理像素和逻辑像素的比例.表示的含义是一个逻辑像素对应几个设备像素.

    • 改变系统或者浏览器的缩放,会改变这个值.例如,在windows中可以设置缩放参数.
    • 因此在不同的设备上只要设置合适的dpr,就能保持元素大小基本一致.
  • meta中的viewport,这个元数据,最初是苹果在ios设备上使用的,目的就是为了适配PC端的页面在移动端的显示问题.其中的关键属性:

    • width,指的是浏览器的窗口的宽度是多少css像素.一般指定为device-width.也就是和设备屏幕的css像素数一致,这个属性的值是设备暴露出来的.
    • initial-scale指的是页面初次加载时的缩放比例,这个缩放是针对device-width的.取值为0-10.和width属性谁的值大,取谁的值作为浏览器窗口的宽度.
    • maximum-scale minimum-scale设置最大最小的缩放.
    • user-scalable 取值yes/no,是否可以让用户缩放.
  • rem是相对单位,相对于文档根元素的fontSize.

替换方案

  • 使用px2rem-loader来处理css.设置option中的remUnit为当前设计稿的1/10.
  • 在html中引入lib-flexible, 来在html元素上设置fontSize.fontSize的大小为屏幕宽度的1/10.
  • 通过以上两步,相当于是将页面按照宽度,等比例的缩放了.

为什么需要px转rem?

为了去适配移动设备,如果pc端的页面直接在移动端显示,会出横向现滚动条或者排版错乱.其实替换rem解决的问题也仅仅于此,因为它只是对页面进行了等比例缩放.没有解决任何移动端的适配问题.所以这是一种偷懒的适配.

内联资源

  • 内联资源指的是将css/js/html直接插入到html模板中.
  • 图片字体文件的内联可以通过之前介绍的url-loader实现.

css内联

  • 使用style-loader或者html-inline-css-webpack-plugin
rules: {
    test: /.css$/,
    use: {
        laoder: "style-loader",
        options: {
            insertAt: "top",//插入到html的head
            singleton: true,//将style标签合并到一个
        }
    }
}

js和html内联

  • 使用raw-loader.该loader的作用就是将一个文件按照字符串读入.
<html>
    <head>
        <!-引入meta html->
        ${ require('raw-loader!./meta.html')}
        <!--插入js代码-->
        <script>${ require('raw-loader!./test.js') }</script>
    </head>
</html>

多页面打包

  • 多页面打包时,需要指定多个entry以及相应的html-webpack-plugin.
  • 其中的痛点是,每次添加一个页面都需要去添加entry和html-webpack-plugin.
  • 可以约定页面入口文件的路径和名称, 使用glob库来读取指定格式的文件名,然后使用代码动态设置entry,以及添加html-webpack-plugin的实例.

对于有配置差异的页面,这样写似乎也是比较麻烦,因为是针对不同的页面设置不同的参数.

使用source map

source map是方便调试的一个工具,可以建立源码和打包后文件之间的映射关系.

webpack开启source map只需要配置devtool: source-map即可.

实际上此处source-map的类型有很多例如,并且这些类型可以各种组合使用.

  • inline-source-map直接将source map以dataURL形式打包在js文件中.不推荐使用,但是对一些三方库可能比较有用.
  • cheap-source-map不生成列信息.可以用来加快打包速度
  • source-map单独生成一个map文件.信息完整,但是生成速度慢.
  • module-source-map会包含loader的source map.
  • eval使用eval和//@sourceMap执行每个模块的源码.有点在于速度最快,但是损失了一些原始信息.
  • eval-source-map类似eval,但是source map以dataURL放在eval中,速度慢,但是重新构建速度会比较快,并且信息完整.是在开发模式下最推荐使用的类型.

提取公共资源

提取公共资源的目的是:

  • 加快打包速度.如果将基础包分离,那么就不需要每次构建都去打包基础包.
  • 提高浏览器缓存效率.同之前分析文件指纹的原因一致.
  • 页面间共享公共chunk,减小js文件大小.

两种提取方式

  • 使用html-webpack-externals-plugin,将基础包,例如一些第三库,通过在html中使用cdn连接去加载,而不再打包在bundle中.
    • webpack自身也提供externals属性,可以配置哪些模块不需要打包进去,但是需要手动在html中引入这些基础包的url.
new HtmlWebpackExternalsPlugin({
  externals: [
    {
      module: 'jquery',//告诉webpack将jQuery模块不要打包在bundle中
      entry: 'https://unpkg.com/jquery@3.2.1/dist/jquery.min.js',//在html中插入cdnurl
      global: 'jQuery',//告诉webpack使用全局的jQuery
    },
  ],
})
  • 使用optimization中的splitChunks,webpack使用了split-chunk-webpack-plugin插件(在webpack3中使用的是common-chunk-webpack-plugin).
//此为webpack在splitChunks中所使用的默认值
module.exports = {
  //...
  optimization: {
    splitChunks: {
      //可选值有3个 async/initial/all 也可以是一个函数
      //async:表示将异步加载的模块抽分打包
      //initial:表示同步加载模块分离
      //all:所有公共模块都进行分离
      chunks: 'async',
      //单位为字节,表示模块压缩前大小大于此值,才进行分离
      minSize: 30000,
      // 单位字节,表示模块压缩前大小小于此值,才进行分离
      maxSize: 0,
      // 
      minRemainingSize: 0,
      // 表示 chunks 被公用的最小次数,大于此值则分离
      minChunks: 1,
      // 按需加载时并行请求的最大数量 ??
      maxAsyncRequests: 6,
      maxInitialRequests: 4,
      automaticNameDelimiter: '~',
      automaticNameMaxLength: 30,
      // 缓存组, 里边包含了一个个分 chunk 的规则,不写任何分 chunk 的规则的话,会有个默认的规则,如下.
      cacheGroups: {
      // splitChunks 下的所有属性都会被继承到缓存组里边
        defaultVendors: {
         // 匹配要 分chunk 的模块
          test: /[\\/]node_modules[\\/]/,
          // 指定优先级,如果 模块被匹配到多个规则,会先使用优先级高的
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

tree-shaking

tree-shaking 是一种能够在静态分析阶段,删除无效代码的打包策略.在 webpack 中在 production 模式下默认是开启的.

使用 tree-shaking 必须配合 es6 的模块化语法.而 commonJS 是不可以的.

什么是无效代码:

  • 已定义但是没有使用.
  • 逻辑永远不会到达的代码
  • 只写不读,或这行结果不会被用到

Scope Hoisting

要解决的问题是:webpack 将代码打包后,会为每个模块都创建一个闭包,当模块数量非常大的时候,内存占用是非常多的.scope hoisting就是为了解决闭包过多的问题.

webpack 开启 production 模式后,会默认使用ModuleConcatenationPlugin,来实现scope hoisting,原理是将引用的模块按照引用的顺序放在同一个作用域中,消除闭包过多的问题.引入同一个闭包还会出现命名冲突的问题,所以还要适当的重命名.

当然不是所有情况都可以解决,使用scope hoisting的条件是:

  • 必须使用 es6,不能是 commonJS
  • 模块的引用次数不能大于 1 次

动态 import

为什么需要动态 import?

为了实现按需加载,以减少首次 js 的加载大小, 让首屏显示更快.

懒加载脚本的两种方式:

  • 对于 commonJS, 使用 required.ensure.
  • 对于 es6,没有原生支持动态导入,需要借助babel插件@babel/plugin-syntax-dynamic-import.
    • 语法: import('./path/to/module.js')
    • 此插件会将动态引入的部分分离到单独的bundle中,在引用的地方使用jsonp去加载js代码.

使用eslint

最常用的 eslint 规范: eslint-config-airbnb,大而全的js校验规则,同时也包含了react的相关规则.如果不使用 react 可以用eslint-config-airbnb-base.

如果自己需要定制规则,最好在eslint:recommend的基础上定制.

结合 webpack 使用可以借助eslint-loader 来处理 js 文件.

打包基础库

webpack 提供了打包基础的能力,支持各种引入方式,ES,AMD,CJS,或者使用 script 标签直接引入.

打包一个名为 large-number 的库,同时需要是支持压缩和非压缩版本. 配置代码:

module.exports = {
    // 为了自己控制是否压缩,所以指定mode为none,
    mode: "none",
    // 配置两个入口会打包出两个bundle,对应压缩和非压缩
    entry: {
        "large-number": "./src/index.js",
        "large-number.min": "./src/index.js".
    },
    output: {
        filename: "[name].js",
        // 以下两个字段需要配合使用.
        // libraryTarget指定模块暴露的方式,其取值有很多,主要可以分为3类:
        // 1. 通过一个变量暴露. 这个变量是由library指定. var/assign
        // 2. 通过对象属性. 这个属性是由library指定的. this/window/global/...
        // 3. 通过模块导入. 这种情况下library不起作用. amd/umd...
        library: "largeNumber",
        // 指定为umd后会支持所有的导出方式
        libraryTarget: "umd",
    },
    optimization: {
        minimize: true,
        minimizer: [
            // 使用TerserPlugin来压缩文件, 指定只压缩min.js.
            new TerserPlugin({
                include: /\.min\.js$/
            })
        ]
    }
}

实现SSR打包

SSR(server side render) 服务端渲染.

为了解决的问题是,目前采用前端框架开发 web 页面,所造成的问题,主要是首次加载白屏时间长不利于SEO.

解决思路是:

  1. 因为在使用了web框架后,实际的html里基本是一个空的文件,具体的元素内容是由VUE/React这写框架去创建渲染(此处的渲染和浏览器的渲染应当不是一个意思,此处只是框架创建生成了具体的dom元素)的.
  2. 而ssr就是指将这个框架的创建生成元素的过程放在服务器端进行而不是在浏览器端.
  3. 这样的话,html不在只是一个模板,而是有实际内容的.这样浏览器拿到html后会很快呈现出内容,而不再需要等待框架去生成.

SSR也存在问题:

  • 造成服务端的性能问题.因为需要服务器去渲生成html.需要配合缓存策略和负载均衡来优化.
  • TTFB(Time To First Byte),就是第一次浏览器接收到数据的时间会变长,因为多个服务器生成html的过程.
  • 即使能较快看到页面被呈现出来,但是仍然是不可交互的,还是得等到框架将虚拟dom构建好,事件绑定完成才可以交互.
  • 构建和部署变得复杂.服务器端需要额外提供nodejs的环境.构建也需要针对服务器端单独实现.
  • 开发变得复杂.一些只有在浏览器中有的变量或者方法都需要特殊处理.并且所用的一些第三方库也需要支持SSR.

预渲染的一些区别联系?

如何实现

  1. 首先需要实现在服务端的node环境中将页面中所有组件渲染为html的逻辑.
    • 一般框架都会 提供一个类似renderToString的函数,用于将组件将渲染为字符串.
    • 得到组件的字符串后,需要将其插入到 html 模板中.
    • 最后配合express,启动一个 server 将组合后的 html 返回给响应的请求.
  2. 改造源代码,以适配在 node 环境中的执行.例如:
    • 组件的引入方式需要使用 required.
    • 数据的请求方式不能再使用fetch,而需要使用支持 node 的axois.
    • 需要增加针对 ssr 的入口.
  3. 需要单独编写一个针对 ssr 的 webpack 配置.
    • 需要更改 entry,使用针对 ssr 的入口文件.
    • 需要设置 output 中的 libraryTarget 为 umd,因为第一步中需要在 node 中使用 required 引入打包出来的模块.

梳理一下整个执行过程:

使用 ssr 的 webpack 配置打包生成要渲染的模块 => 在 node 环境中使用框架提供的renderToString方法将模块渲染为字符串 => 将字符串插入到打包出来的 html 模板字符串中(因为使用打包出来的 html 会将样式带着) => 通过 express 起的服务返回

配置构建信息输出

webpack在构建过程中有许多输出,作为业务开发者可能并不关心,为了控制输入内容,可以通过设置stats属性.其可选值为

也可以是对象类型的值,实现更精细的控制.

可以配合插件friendly-errors-webpack-plugins让成功/错误/警告灯输出带有颜色.

编写可维护的 webpack 构建配置

构建配置包设计

构建配置管理的可选方案

  • 将所有环境的配置放在一个文件中,通过指定--env参数, 在文件内部通过分支结构,针对不同的环境,指定配置参数.
  • 针对不同环境或者目的的构建, 编写不同的 webpack 配置文件,配合 webpack --config使用.
    • 使用webpack-merge, 合并配置.
    • 配合方案三,打包为 npm 库. 方便管理和使用.
  • 将构建设计成一个库.例如: hjs-webpack Neutrino等.就是将配置打包为 npm 库,团队使用时直接安装,在 webpack 配置中引入即可.
  • 抽成一个工具进行管理.例如: create-react-app nwb等.此可针对大型团队,用于创建项目/配置环境等一系列规范.
此课程针对第二和第三点讲解.

功能模块设计和目录设计

一个模块设计的例子:

一个构建包项目的目录结构:

构建包项目中使用的工具

  • eslint. 约束代码规范

  • 测试框架mocha和断言库chai. 用于编写冒烟测试单元测试.

  • istanbul用于计算测试覆盖率.

  • 持续集成.可以使用travis jenkins等工具.travis 目前使用比较多.

  • 发布到 npm. 版本号遵循semver规范.

  • git commit message 遵循angular js规范

  • 使用validate commit message工具来校验,提交信息是否符合规范

  • 使用conventional-changelog来自动生成每次版本变化之间的修改信息.

分析和优化打包速度和体积大小

  • 使用 webpack 输出的 stats 信息,来分析打包的体积

  • 使用speed-measure-webpack-plugin,可以输出每个插件和 loader 的执行速度

  • 使用webpack-bundle-analyzer, 可以可视化的分析每一个bundle 的体积构成

  • 使用thread-loader,多线程打包,加速打包过程,在 webpack3 中使用HappyPack. 其主要是加速 loader 过程.

  • 使用terser-webpack-plugin设置parallel参数为 true 开启并行压缩,该插件是基于uglifyjs.

  • 使用DLLPluginDLLReferencePlugin来分离基础库.虽然使用spitChunk也能达到类似的效果,但是 dll 的优势在于,一次将基础库打包好后就不需要在每次构建时去分析和提取.但是在webpack4中dll的提升并不明显,所以vue-clicreate-react-app都不再使用.

  • 使用缓存来加快打包速度。

    • babel-loader 开启缓存. 会将语法转换过程进行缓存.
    • terser-webpack-plugin开启代码压缩缓存.
    • hard-source-webpack-plugin其作用的官方描述是提供模块打包的中间步骤缓存.
    • 以上三个配置的缓存都会放在node-modules/.cache下.
  • 使用resolve缩小构建目标.意思是取消一些没有必要的查找和构建过程.例如:

    • 指定模块的目录,减少模块的搜索层级.
    • 指定后缀扩展
  • 使用purgecss-webpack-plugin配合mini-css-extract-plugin.将无用的css在打包结果中去除.

  • 使用image-webpack-loader,可以压缩图片.

  • 使用polyfill service来动态引入所需要的polyfill.

    • 原理是使用浏览器的UA,后端服务识别当前浏览器所需要的polyfill

webpack 打包原理

输入webpack命令后大致的执行过程

  • 执行webpack包中bin字段下配置的js文件.
  • 执行过程中,会判断当前有没有安装命令行工具.
    • 支持webpack-cliwebpack-command两个工具.
    • 必须只能安装其中一个.如果没有安装会使用runCommand方法去安装.
  • 最后将命令行工具引入.
  • 使用yargs解析命令中的参数信息.
  • cli判断命令是否是NON_COMPLATION_CMD.
    • 如果是,则不需要webpack实例去编译.例如: init remove add migrate等都是不需要编译的命令.对于这些命令会传递参数后,直接执行对应的包.包不存在则会提示安装.
  • 使用解析后的参数生成options outputOptions
    • 输入的参数不代表最终webpack所使用的配置项,所以需要生成最终webpack的配置项
    • 这一步也会合并配置文件和命令行中的参数.
  • 使用生成的配置项,生成一个webpack的实例compiler
  • 执行配置文件中设置的插件的apply方法.
  • 通过WebpackOptionsApply来处理内部插件.
  • 运行compiler,不同的参数决定了compiler的运行方式,例如: run 或者 watch.
webpack中的两个核心对象compiler complation均继承自Tapable.使得webpack可以定义各种各样的钩子,在不同的时机被调用,这是webpack插件化开发的基础.

插件的调用也是通过钩子实现的,每一个插件必须提供一个`apply`方法,接受`compiler`作为参数,apply方法会在compiler对象被实例化后,通过options上的plugin参数,被webpack调用.在apply方法中会注册一些钩子,当这些钩子被调用时,插件的功能代码就被真正执行了.

webpack 中的关键执行步骤

entry-option

run

make

before-resolve

build-module

normal-module-loader

program

seal

emit

一个简易webpack的示例

https://github.com/geektime-geekbang/geektime-webpack-course.git

  1. 输入代码字符串,使用babylon生成AST.
  2. 输入AST,使用babel-traverse提取依赖的文件路径.
  3. 将AST再使用babel-core转为js代码.
  4. 从entry开始,遍历所有依赖,生成如下的modules数组.
{
    filename: "index.js",
    dependencies: ["greet.js"],
    source: "...", //js代码
}
  1. 生成最终bundle.
    • 将每个module使用函数包住,传入require/module/exports参数.其目的是在require函数内执行每个module的代码.
    • 使用立即执行函数, 将modules作为参数传入.
    • require(entry).从entry开始执行代码.
// 类似webpack中的__webpack_require__函数,是代码运行的核心.
function require(fileName) {
    const fn = modules[fileName];
    const module = { exports : {} };
    fn(require, module, module.exports); // 遇到模块内的依赖, 会不断的递归调用require.
    return module.exports;
}

loader

  • 从代码上来说,loader是一个导出为函数的模块.同一个规则下的loader的执行顺序是从右往左.
  • loader-runner用于在不安装webpack, 开发和调试loader的包.
  • loader-utils包提供了编写loader时的工具方法.
  • this.async(),返会一个callback, 用于异步操作时,返回执行结果.
  • cacheable默认是true,即缓存打开状态,但是缓存的生效条件是:
    • loader没有依赖
    • 相同的输入有相同的输出

plugin

  • 从代码上看,插件是一个类,必须有一个apply方法,且接受compiler对象.
  • 异常处理
    • 在参数校验阶段,可以直接使用throw抛出异常
    • 进入到hooks,可以使用compiler.errors.push或者compilers.warnings.push,传递异常.
  • 插件的编写需要熟悉webpack的hooks.compilercomplation对象.