天天看点

webpack知识点总结

webpack是什么?

webpack是一种前端资源构建工具,一个静态模块打包器。在webpack看来,前端的所有资源文件都会作为模块处理。它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源。

webpack与gulp的区别是什么?

webpack与gulp最大的区别就是在于打包过程上有所不同

Grunt、Gulp 这类构建工具的打包过程是通过遍历源文件–>匹配规则–>打包,整个过程是基于文件流的打包方式且做不到按需加载。

webpack 是从入口文件开始,把相关模块引入通过加载模块–>解析模块–>打包,整个过程是基于模块化的打包方式且支持按需加载,同时还可以在执行过程中针对性的去做一些优化操作。

1.核心打包原理:

1) 打包的主要流程如下:

  1. 需要读到入口文件里面的内容。
  2. 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树。
  3. 根据AST语法树,生成浏览器能够运行的代码

2. 常见的模块解析包

  1. @babel/parser:将获取到的模块内容 解析成AST(es6的)语法树
  2. @babel/traverse: 遍历AST语法树
  3. @babel/core @babel/preset-env:ES6的AST转化成ES5的AST
  4. style-loader: 将css添加到DOM的内联样式标签style里
  5. css-loader:允许将css文件通过require的方式引入,并返回css代码,(解析css文件内的css代码,将 CSS 转化成 CommonJS 模块,)
  6. less-loader 处理less
  7. sass-loader 处理sass
  8. file-loader:分发文件到output目录并返回相对路径
  9. url-loader:和file-loader类似,但是当文件小于设定的limit时可以返回一个Data Url
  10. html-minify-loader 压缩HTML
  11. babel-loader 用babel来转换ES6文件到ES5
  12. raw-loader:加载文件原始内容(utf-8)
  13. json-loader:加载 JSON 文件(默认包含)

3. 有哪些常见的Plugin?你用过哪些Plugin?

  1. ignore-plugin:忽略部分文件
  2. html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
  3. mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载
  4. clean-webpack-plugin: 目录清理

    5.uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)

  5. terser-webpack-plugin: 支持压缩 ES6 (Webpack4)
  6. webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)
  7. HappyPack:HappyPack 能让 webpack 把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。因此可以很大程度上优化打包速度。
  8. DllPlugin:

4. loader 和 plugin 不同

  1. loader 本质就是一个函数,是使wenbpack拥有加载和解析非js文件的能力,loader支持链式调用,从右至左执行,支持同步或异步函数
  2. plugin :基于事件流框架Tapable,可以扩展webpack的功能,使得webpack更加灵活。可以在构建的过程中通过webpack的api改变输出的结果

5. webpack构建流程

  1. 初始化参数,从配置文件和shell语句中读到的参数合并,得到最后的参数
  2. 开始编译:用合并得到的参数初始化complier对象,加载是所有配置的插件,执行run方法开始编译
  3. 确定入口,通过entry找到入口文件
  4. 编译模块,从入口文件出发,调用所有配置的loader对模块进行解析翻译,在找到该模块依赖的模块进行处理
  5. 完成模块编译,得到每个模块被翻译之后的最终的内容和依赖关系
  6. 输出资源,根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,在把每个chunk转换成一个单独的文件加载到输出列表
  7. 输出完成,确定输出的路径和文件名,把内容写到文件系统中
  8. 在以上过程中,webpack会在特定的时间点广播出特定的事件,插件在监听感兴趣的事件后会执行特定的逻辑,改变webpack的运行结果。

6. source map是什么?生产环境怎么用?

source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。

map文件只要不打开开发者工具,浏览器是不会加载的。

7. webpack 热加载执行原理

HMR(Hot Module Replacement)是webpack一个重要的特性,当代码文件修改并保存之后,webapck通过watch监听到文件发生变化,会对代码文件重新打包生成两个模块补丁文件manifest(js)和一个(或多个)updated chunk(js),将结果存储在内存文件系统中,通过websocket通信机制将重新打包的模块发送到浏览器端,浏览器动态的获取新的模块补丁替换旧的模块,浏览器不需要刷新页面就可以实现应用的更新。

8. 文件监听原理

在发现源码发生变化时,自动重新构建出新的输出文件。

1)Webpack开启监听模式,有两种方式:

启动 webpack 命令时,带上 --watch 参数

在配置 webpack.config.js 中设置 watch:true

缺点:每次需要手动刷新浏览器

2)原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行。

module.export = {
    // 默认false,也就是不开启
    watch: true,
    // 只有开启监听模式时,watchOptions才有意义
    watchOptions: {
        // 默认为空,不监听的文件或者文件夹,支持正则匹配
        ignored: /node_modules/,
        // 监听到变化发生后会等300ms再去执行,默认300ms
        aggregateTimeout:300,
        // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
        poll:1000
    }
}
           

9. 什么是bundle,什么是chunk,什么是module?

10. 文件指纹是什么?怎么用?

文件指纹是打包后输出的文件名的后缀。

Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改
Chunkhash:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhash
Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变

1) JS的文件指纹设置

设置 output 的 filename,用 chunkhash。

2) CSS的文件指纹设置

设置 MiniCssExtractPlugin 的 filename,使用 contenthash。

3) 图片的文件指纹设置

设置file-loader的name,使用hash。

module.exports = {
    entry: {
        app: './scr/app.js',
        search: './src/search.js'
    },
    output: {
        filename: '[name][chunkhash:8].js',
        path:__dirname + '/dist'
    },
    plugins:[
        new MiniCssExtractPlugin({
            filename: `[name][contenthash:8].css`
        })
    ],
    module:{
        rules:[{
            test:/\.(png|svg|jpg|gif)$/,
            use:[{
                loader:'file-loader',
                options:{
                    name:'img/[name][hash:8].[ext]'
                }
            }]
        }]
    }
}
           

11.如何利用webpack来优化前端性能?

12. 如何提高webpack的构建速度?

  1. ** 使用最新的webpack和node版本**,较新的版本能够建立更高效的模块树以及提高解析速度。
  2. 多进程/多实例构建:HappyPack(不维护了)、thread-loader
  3. 多进程并行压缩

webpack-paralle-uglify-plugin(不再维护)

uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)

terser-webpack-plugin 开启 parallel 参数(支持ES6)

  1. DLL:

使用 DllPlugin 进行对第三方库分包提前打包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,通过 json 文件告诉webpack这些库提前打包好了,避免反复编译浪费时间。

HashedModuleIdsPlugin 可以解决模块数字id问题

  1. 充分利用缓存提升二次构建速度:

babel-loader 开启缓存

terser-webpack-plugin 开启缓存

使用 cache-loader 或者 hard-source-webpack-plugin

  1. 缩小构建目标/减少文件搜索范围:
  1. exclude(不需要被解析的模块)/include(需要被解析的模块)
  2. resolve.modules 告诉 webpack 解析模块时搜索的目录,指明第三方模块的绝对路径
  3. resolve.mainFields 限定模块入口文件名,只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
  4. resolve.alias 当从 npm 包中导入模块时(例如,import * as React from ‘react’),此选项将决定在 package.json 中使用哪个字段导入模块。根据 webpack 配置中指定的 target 不同,默认值也会有所不同
  5. resolve.extensions 尽可能减少后缀尝试的可能性
  6. noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)

    IgnorePlugin (完全排除模块)

  1. 动态Polyfill
通过 Polyfill Service识别 User Agent,下发不同的 Polyfill,做到按需加载,社区维护。(部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)
  1. Scope hoisting (「作用域提升」)

构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 把引入的 js 文件“提升到”它的引入者顶部,其实现原理为:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。

必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法和 Scope Hoisting 要分析模块之间的依赖关系,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法

  1. 提取页面公共资源:
  1. 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
  2. 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件

    基础包分离

  1. Tree shaking
  1. purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)

    打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率

  2. 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
  3. 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码

13. 是否写过Loader?简单描述一下编写loader的思路?

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

编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用**this.callback()**方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去

const loaderUtils = require('loader-utils');
const fs = require('fs');
const path = require('path');

module.exports = function(source) {
    const { name } = loaderUtils.getOptions(this);
    const url = loaderUtils.interpolateName(this, "[name].[ext]", {
        source,
    });

    console.log(url);
    this.emitFile(path.join(__dirname, url), source);
    const json = JSON.stringify(source)
        .replace('foo', '')
        .replace(/\u2028/g, '\\u2028')
        .replace(/\u2029/g, '\\u2029');

    return `export default ${json}`;

}
           

14. 是否写过Plugin?简单描述一下编写Plugin的思路?

Plugin的编写核心是在plugin类中一定要实现apply(compiler)这个方法,绑定compiler对象中的hooks,进行监听,webpack会在特定的时间点广播出特定的事件,插件在监听感兴趣的事件后会执行特定的逻辑,改变webpack的运行结果。

module.exports = class ZipPlugin {
    constructor(options) {
        this.options = options;
    }

    apply(compiler) {
        compiler.hooks.emit.tapAsync('ZipPlugin', (compilation, callback) => {
            const folder = zip.folder(this.options.filename);

            for (let filename in compilation.assets) {
                const source = compilation.assets[filename].source();
                folder.file(filename, source);
            }

            zip.generateAsync({
                type: 'nodebuffer'
            }).then((content) => {
                const outputPath = path.join(
                    compilation.options.output.path, 
                    this.options.filename + '.zip'
                );

                const outputRelativePath = path.relative(
                    compilation.options.output.path,
                    outputPath
                );
                compilation.assets[outputRelativePath] = new RawSource(content);

                callback();
            });
        });
    }
}
           

15. 聊一聊Babel原理吧

大多数JavaScript Parser遵循 estree 规范,Babel 最初基于 acorn 项目(轻量级现代 JavaScript 解析器),Babel 是对浏览器识别不了的代码进行转换兼容的库,Babel大概分为三大部分:

  1. Parser 解析:将代码转换成抽象语法树 (AbstractSyntaxTree,简称 AST)

    词法分析:将字符串形式的代码分割为令牌(token)流,即语法单元成的数组

    语法分析:将token流转换成 AST

  2. Transformer 转换:根据配置好的 plugins/presets 把 Parser 生成的 AST 转变为新的 AST

    Taro就是利用 babel 完成的小程序语法转换

  3. Generator 生成:把新的 AST 生成代码