5. webpack-interview

247 阅读14分钟

1. 与webpack类似的工具还有哪些?为什么选择webpack?

  • a.grunt

    • 一句话:自动化。对于需要反复重复的任务,例如压缩/编译/单元测试/linting等,自动化工具 可以减轻劳动,简化工作

    • 最老牌的打包工具

    • 优点:出现早

    • 缺点:配置项多;不同插件可能会有自己的扩展字段; 学习成本高,各种配置规则

    安装: npm i grunt grunt-babel @babel/core @babel/preset-env -D

  • b.gulp

    • 基于nodejs的stream流打包
    • 定位是基于任务流的自动化构建工具
    • gulp是通过 task对整个开发过程进行构建
    • 优点:
      • 流式写法简单直观
      • API简单,代码量少
      • 易于学习和使用
      • 适合多页面应用开发
    • 缺点:
      • 异常处理比较麻烦
      • 工作流程顺序难以精细控制
      • 不太适合单页面或者自定义模块的开发
    npm i gulp-cli -g
    npm i  gulp -D
    npx -p touch nodetouch gulpfile.js
    gulp --help
    
    npm i gulp-cli gulp gulp-babel @babel/core @babel/preset-env -D
    
  • c.webpack

    • 模块化管理工具和打包工具,通过loader的转换,任何形式的资源都可以看成是模块,比如commonjs模块/AMD模块/ES6模块,图片等。可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源
    • 可以将按需加载的模块进行代码分割,等到实际需要的时候再异步加载
    • 定位是模块打包器,而gulp/grunt属于构建工具
    • webpack可以代替gulp/grunt的一些功能,但不是一个职能的工具,可以配合使用
    • 优点:
      • 模块化打包任何资源
      • 适配任何模块系统
      • 适合SPA单页应用开发
    • 缺点:
      • 学习成本高,配置复杂
      • 通过babel编译后的js代码打包后体积过大
  • d.rollup

    • 下一代ES6模块化工具,最大的亮点是利用ES6模块设计,利用tree-shaking生成更简洁的代码
    • 一般而言,对于应用使用webpack,对于类库使用rollup
    • 需要代码拆分,或者很多静态资源需要处理,再或者构建的项目 需要引入很多commonjs模块的依赖时,使用webpack
    • 代码库是基于es6模块,而且希望代码能够被其他人直接使用,使用rollup
    • 优点: 用标准化的格式(es6)来写代码,通过减少死代码尽可能缩小包体积
    • 缺点: 对代码拆分/静态资源/commonjs模块支持不好
  • e.parcel

    • parcel是快速/零配置的web应用程序打包器
    • 目前parcel只能用来构建用于运行在浏览器中的页面,这也是他的出发点和专注点
    • 优点:
      • parcel内置了常用场景的构建方案及其依赖,无需再安装各种依赖
      • parcel能以html为入口,自动检测和打包依赖资源
      • parcel默认支持模块热替换,真正的开箱即用
    • 缺点:
      • 不支持sourcemap
      • 不支持剔除无效代码 treeshaking
      • 配置不灵活
    • 安装
      • yarn global add parcel-bundler
      • npm i -g parcel-bundler parcel -v 零配置 执行: script中: "start": "parcel src/index.html -p 8089" */

// es6 => es5 // Gruntfile.js // 执行: npm run build module.exports = function(grunt) { // 加载babel任务 grunt.loadNpmTasks("grunt-babel"); // 初始化配置 文件 grunt.initConfig({ babel: { options: { sourceMap: true, presets: ["@babel/preset-env"] }, dist: { files: { "dist/app.js": "src/app.js" } } } }); // default指的是入口任务 grunt.registerTask("default", ["babel"]) }

// gulp  gulpfile.js
const gulp = require("gulp");
const babel = require("gulp-babel");
function defaultTask(callback) {
    gulp
    .src("src/app.js") // 读取文件
    .pipe(
        babel({ // 传给babel任务
            presets: ["@babel/preset-env"]
        })
    ).pipe(gulp.dest("dist")); // 写到dist

    callback();
}
exports.default = defaultTask;


// rollup  rollup.config.js
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';

export default {
    input: "src/main.js", // 相当于webpack的entry 
    output: { // 相当于webpack的output
        file: "dist/bundle.js", 
        format: "cjs",
        exports: "default"
    },
    plugins: [
        resolve(),
        babel({
            "presets": ["@babel/preset-env"],
            exclude: "node_modules/**"
        })
    ]
}

2.如何调试webpack?

// debug.js
const webpack = require("webpack");
const config = require("./webpack.config.js"); // 读取配置文件
debugger;
const compiler = webpack(config);
function compilerCallback(err,stats) {
    const statsString = stats.toString();
    console.log(statsString);
}
debugger;
compiler.run((err,stats) =>  {
    compilerCallback(err, stats)
});

3. loader和plugin的不同?

  • loader直译为加载器,webpack将一切文件视为模块,但是webpack原生只能解析js文件,如果想将其他文件也打包的话,就会用到loader。
  • 所以loader的作用是让webpack拥有了加载和解析非javascript文件的能力
  • plugin直译为插件,plugin可以扩展webpack的能力,让webpack具有更多的灵活性。在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,
  • 在合适的时机通过webpack提供的api改变输出结果

4.webpack的构建流程是什么?

  • 初始化参数:从配置文件和shell语句中读取和合并参数,得到最终的参数
  • 开始编译: 用上一步得到的参数初始化compiler对象 加载所有配置的插件,执行对象的run方法开始执行编译; 确定入口:根据配置中的entry找出所有的入口文件
  • 编译模块 从入口文件出发,调用所有配置的loader对模块进行编译 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过本步骤的处理
  • 完成模块编译 在经过第4步使用loader编译完所有的模块后,得到了每个模块被翻译后的最终内容以及他们之间的依赖关系 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
  • 在以上过程中,webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用webpack提供的api改变webpack的运行结果
  • modules模块 => chunks代码块 => files 文件

5.常见的loader和plugin

loader babel-loader es6或者react转换成es5 css-loader 加载css,支持模块化,压缩,文件导入等特性 eslint-loader 添加eeslint检查js代码 file-loader 把文件输出到指定文件夹,通过相对url引用输出的文件 url-loader 和file-loader类似,文件很小时支持base64 sass-loader postcss-loader autoprefixer 厂商前缀 style-loader plugin case-sensitive-paths-webpack-plugin 如果路径有误直接报错 terser-webpack-plugin 压缩js代码 html-webpack-plugin 自动生成带有入口文件引用的index.html webpack-manifest-plugin 生产资产的显示清单文件 optimize-css-assets-webpack-plugin 优化/压缩css资源 mini-css-extract-plugin 提取css生成单个文件 ModuleScopePlugin 如果引用了src目录外的文件报警插件 cleanWebpackPlugin copyWebpackPlugin: 将指定文件夹拷贝到打包生成的文件夹中 bannerPlugin 内置 版权声明插件

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

  • sourcemap是为了解决开发代码和实际运行代码不一致时帮助debug到原始开发代码的技术 看似配置项很多,其实只是5个关键字:eval/source-map/cheap/module/inline的任意组合

    eval 使用eval包裹模块代码 source-map 产生.map文件 cheap 不包含列信息 也不包含loader的sourcemap module 包含loader的sourcemap inline 将.map作为DataURI嵌入,不单独生成.map文件

如何选择source map的类型 首先在源代码的列信息是没有意义的,只要有行信息就能完整的建立打包前后代码之间的依赖关系。因此,不管是开发还是生产环境都可以忽略模块打包后的列信息关联 不管是生产还是开发环境,都需要debug到最原始的资源位置 需要生成.map文件, 所以得有source-map属性 eval-source-map:内联,每一个文件都生成对应的 source-map,都在 eval 中, 总结: 开发环境使用: cheap-module-eval-sourcee-map 生产环境使用: cheap-module-source-map

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

  • 压缩js terserplugin

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

  • 压缩图片 image-webpack-loader

  • 清除无用的css 单独提取css并且清除用不到的css: Purgecssplugin

  • tree shaking

    • 一个模块可以有多个方法,只要其中某个方法使用到了,整个文件都会被打到bundle里面,treeshaking就是只把用到的方法打到bundle,没用的去掉
    • 原理是利用es6模块的特点,只能作为模块顶层语句出现,import的 模块名只能是字符串常量
生效的条件:
mode:"production"
devtool: false // 不生成sourcemap
.babelrc中配置:
presets:{["@babel/preset-env", {"modules": false}]}

  • scope hoisting

    • scope hoisting可以让webpack打包出来的文件更小,运行更快,又翻译为 作用域提升
    • 原理是将所有的模块按照引用顺序放到一个函数作用域里,然后适当的重命名一些变量以防止命名冲突
    • 这个功能在mode为production时自动开启,开发环境要用webpack.optimize.ConcatenationPlugin插件
  • 代码分割

a.入口点分割
    入口文件设置的时候可以配置
    问题
        如果入口chunks之间包含重复的模块如lodash,那些重复模块都会被引入到各个bundle中
        不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码
b.动态导入和懒加载
    及按需加载
    一般采用以下原则:
        对网站功能进行划分,每一类一个chunk
        对于首页打开页面需要的功能直接加载,尽快展示,某些以来大量代码的功能点可以按需加载
        被分割出去的代码需要一个按需加载的时机
c.preload 预先加载
    preload通常用于本页面要用到的关键资源,如关键js/字体/css文件
    preload会把资源的下载顺序权重提高,使得关键数据提前加载好,优化页面打开速度
    在资源上增加预先加载的注释,指明该模块需要立即被使用
d. prefetch  预先拉取
    高速浏览器未来可能会使用到的某个资源,浏览器会在空闲时加载对应资源,
    如果能预测到用户行为,比如懒加载,点击到其他页面等则相当于提前预加载了需要的资源  
preload和prefetch的区别
    preload是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源 
    prefetch是告诉浏览器页面可能会需要的资源,浏览器不一定会加载这些资源
    建议:对于当前页面很有必要的资源使用preload,对于可能在将来的页面使用的资源使用prefetch
  • 提取公共代码
为什么需要提取
        相同资源被重复加载,造成浪费
        每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验
        如果能把公共代码抽离成单独文件,可减少网络传输流量,降低服务器成本
    如何提取
        基础类库,方便长期缓存
        页面之间公共代码
        各个页面单独生成文件
    splitChunks
        module:就是js的模块化,webpack支持commonjs,es6等模块化规范,简单来说就是通过import语句引入的代码
        chunk:chunk就是webpack根据功能拆分出来的,包括三种:
            项目入口
            import动态引入
            splitChunks拆分出来的代码
        bundle:bundle是webpack打包之后的各个文件,一般就是和chunk一对一关系
        bundle就是对chunk进行编译压缩打包等处理之后的产出

  • CDN
 最影响用户体验的是网页首次打开时的加载等待,导致这个问题的根本是网络传输过程消耗大,CDN的作用就是加速网络传输
    CDN又叫内容分发网络,通过将资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源 ,从而加速资源的获取速度
    第一次访问页面,静态资源做持久化缓存在http头加上cache-control/Expires
    后续访问如果缓存没有过期可以直接使用
    缓存策略:
    html页面是不缓存的
    第三方库是强缓存
    其他的变化了就用新的 不变就用老的
    webpack配置:
        output => filename/chunkFilename => 使用[hash] 每次编译都会产生一个新的hash值
        new MiniCssExtractPlugin({
            filename: "[name].[contenthash].css"
        })


// new PurgecssPlugin({
//     paths:  glob.sync(`${PATHS.src}/**/*`, {nodir:true})
// })

// react中的懒加载:
// const App = React.lazy(() => {
//     import(/* webpacjChunkName: "title"*/"./component/Title")
// });
// this.state.visible && <Suspense fallback={"loading..."}>
//     <App/>
// </Suspense>

// preload
import("util.js"/*webpackPreload:true*/ /*webpackChunName:"utils"*/)
// prefetch
button.addEventListener("click", () => {
    import("util.js"/*webpackPrefetch:true*/ /*webpackChunName:"utils"*/)
    .then(result => {

    })
})

8.webpack中hash/chunkhash/contenthash的区别

  • 文件指纹:打包后输出的文件名和后缀
  • hash一般是结合CDN缓存来使用,通过webpack构建后生成对应的文件名自动带上对应的md5值。
  • 如果文件内容发生变化,对应的hash也会变,对应的html引用的url地追也会变,触发CDN服务器从源服务器上拉去对应数据,进而更新本地缓存
  • hash:每次webpack构建时生成的一个唯一的hash值, 任何文件变化都会导致变化
  • chunkhash:根据chunk生成hash,来源于同一个chunk,hash就一样
  • contenthash:根据内容生成hash,文件内容相同hash就相同

9.如何对bundle体积进行监控和分析

webpack-bundle-analyzer

10.如何提高webpack的构建速度

  • 费时分析
const SpeedMeasureWebpackPlugin = require("speed-measure-webpack-plugin");
const smw = new SpeedMeasureWebpackPlugin();
module.exports = smw.wrap({

})
  • 缩小范围
extensions 指定extensions之后可以不用再require或者import时候加上文件扩展名,回一次尝试添加扩展名进行匹配
    resolve:{
        extensions: [".js",".json"]
    }
alias
    匹配别名可以加快webpack查找模块的速度
    如每当引入bootstrap模块的时候,会直接引入bootstrap,而不用从node_modules文件夹中按照查找规则进行查找
    const bootstrap = require(__dirname, "node_modules/_bootstrap@3.3.7@bootstrap/dist/css/bootstrap.css")
    resolve: {
        alias: {
            "bootstrap" : bootstrap
        }
    }
modules
    对于直接声明依赖名的模块如react,webpack会类似nodejs一样进行路径搜索,一直向外查找node_modules目录
    这个目录就是使用resolve.modules字段进行配置
    默认配置是: resolve:{modules:["node_modules"]}
    如果可以确定项目内所有的第三方依赖模块都在项目根目录下的node_modules中查找:
    resolve: {
        modules: [path.resolve(__dirname, "node_modules")]
    }
mainFields
    默认情况下package.json文件按照文件中main字段的文件名来查找文件
    resolve: {
        mainFields: ["browser", "module", "main"]
    }
mainFiles
    当目录下没有package.json文件时,会默认使用目录下的index.js文件,也可以配置 
    resolve: {
        mainFiles: ["index"]
    }
resolveLoader
    里面的配置项和resolve是一样的,用于找loader

  • noParse 用于配置那些模块文件的内容不需要进行解析
不需要解析依赖(即无依赖)的第三方大型类库等,可以配置这个字段提高构建速度
module: {
    noParse: /jquery|lodash/, // 正则匹配
    // 或者使用函数
    noParse(content){
        return /jquery|lodash/.test(content)
    }
}
使用noParse的文件中不能使用import/require/define等导入机制
  • IgnorePlugin
用于忽略某些特定模块,让webpack不把这些指定模块打包进去
IgnorePlugin 在打包时忽略本地化内容,如引入了一个插件,只用到了中文语言包,打包的时候把非中文语言包排除掉
plugins: [
  // 忽略解析三方包里插件
  new webpack.IgnorePlugin(/\.\/locale/, /moment/)
]
//
import moment from 'moment'
// 引入中文
import 'moment/locale/zh-cn'
// 设置中文
moment.locale('zh-cn');
let momentStr = moment().subtract(10, 'days').calendar();
console.log('现在时间:', momentStr);

>friendly-errors-webpack-plugin
stats: "verbose" // 全部输出 
plugins: [
    new FirendlyErrorsWebpackPlugin()
]
  • DLL

    • .dll为后缀的文件成为动态链接库,在一个动态链接库中可以包含给其他模块调用的函数和数据

    • 把基础模块独立出来打包到单独的动态链接库里,当需要倒入的模块在动态链接库里的时候,模块不能再次被打包,而是去动态链接库里获取

    • DllPlugin:用于打包出一个个动态链接库

    • DllReferencePlugin: 在配置文件中引入DllPlugin插件打包好的动态链接库

  • 利用缓存

webpack中利用缓存一般有以下几种思路:
    babel-loader开启缓存
    使用cache-loader
    使用hard-source-webpack-plugin
babel-loader:转译js文件过程消耗性能较高
{
    test: /\.js$/,
    exclude: /node_modules/,
    use: [{
        loader: "babel-loader",
        options: { cacheDirectory: true }
    }]
}

cache-loader:
在一些性能开销较大的loader之前添加这个loader,将结果缓存到磁盘
存和读取这些缓存文件会有一些时间开销,所以只对性能开销较大的loader使用

hard-source-webpack-plugin
HardSourceWebpackPlugin为模块提供了中间缓存,缓存默认的存放路径是node_modules/.cache/hard-source
配置HardSourceWebpackPlugin,首次构建时间不会有太大变化,但是从第二次开始,构建时间大约可以减少80%左右
webpack5中 内置了HardSourceWebpackPlugin
使用: new HardSourceWebpackPlugin()
  • oneOf

    • 每个文件对于rules中的所有规则都会遍历一边,如果使用oneOf就可以解决这个问题,只要能匹配一个即刻推出
    • 注意:在oneOf中不能两个配置处理同一种类型文件
  • 多进程处理 thread-loader

把这个loader放置在其他loader之前,放置在这个loader之后的loader就会在一个单独的worker池中运行

thread-loader会对其后面的loader(这里是babel-loader)开启多进程打包。 
进程启动大概为600ms,进程通信也有开销。(启动的开销比较昂贵,不要滥用)
只有工作消耗时间比较长,才需要多进程打包
thread-loader必须最后执行,再次说明loader是从下往上,从右往左的执行顺序,所以想要使用thread-loader优化某项的打包速度,必须放在其后执行
use: [
    {
        loader: "thread-loader",
        options: {workers:3} // 进程数量
    },
    {
        loader: "babel-loader",
        options: {
            presets: ["@babel/preset-env", "@babel/preset-react"]
        }
    }
]

11.loader的种类以及执行顺序是怎样的?

种类:inline内联 / pre前置 / post后置 / normal正常 loader
loader的执行时机:loader是在拿到入口文件开始递归编译模块的时候执行的
loader的叠加顺序:post  + inline + normal + pre
特殊配置:
-! 不要前置和普通loader
! 不要普通loader
!! 不要前后置和普通loader,只要内联loader

pitch
比如a!b!c!module 正常调用顺序应该是c,b,a,但是真正的调用顺序是:a picth -> b pitch -> c pitch  ->  c   ->  b -> a,
如果其中任意一个pitching loader有返回值就相当于在他以及他右边的loader已经执行完毕
比如b pitch 返回了字符串“result b” 接下来只有a会被修通执行,而且a的loader收到的参数是result b
loader根据返回值可以分为两种:
    一种是返回js代码(一个module的代码,含有类似module.export语句)的loader
    还有不能作为最左边loader的其他loader
有时候想把两个第一种loader chain起来,比如style-loader!css-loader!问题是css-loader的返回值是一串js代码,
如果按照正常方式写style-loader,参数就是一串代码字符串
为了解决这种问题,需要在style-loader里 执行require(css-loader!resource)

12.是否写过loader?描述一下编写loader的思路 13.是否写过plugin?描述一下编写plugin的思路

14.webpack打包的原理是什么?聊一聊babel和抽象语法树 看zf-pack手写webpack

15. tree-shaking的原理

import {flatten, concat} from "lodash";
会将lodash打包进bundle
优化:
使用babel插件将上面的import语句改成:
import flatten from "lodash/flatten";
import concat from "lodash/concat";

实现按需加载:
npm i babel-plugin-import -D
babel-loader中:
options: {
    plugins:[["import", {library: "lodash"}]]
}


// babel插件 babel-plugin-import
// babel插件的基本格式 是一个函数,返回一个对象,对象里面有一个visitor叫访问器
// 访问器是一个对象,有很多属性,每个属性对应ast语法树上的一个节点类型,执行里面定义分方法
let babel = require("@babel/core");
let types =  require("babel-types");
const visitor = {
    ImportDeclaration: {
        enter(path, state={ opts }) {
            const specifiers =  path.node.specifiers; // [{flatten, concat }]
            const source =  path.node.source; // 模块名字 lodash
            if(
                // plugins:[["import", {library: "lodash"}]]
                state.opts.library == source.value && 
                !types.isImportDefaultSpecifier(specifiers[0])
            ){
                const declarations = specifiers.map((specifier,  index) => {
                    return types.ImportDeclaration(
                        [types.importDefaultSpecifier(specifier.local)],
                        types.stringLiteral(`${source.value}/${specifier.local.name}`)
                    )
                })
                path.replaceWithMultiple(declarations);
            }
        }
    }
}
module.exports = function(babel) { 
    return {
        visitor
    }
}

16.webpack的热更新是如何做到的?说明其原理

什么是HMR: Hot Module Replacement是指当对代码修改并保存后,webpack会对代码进行重新打包, 并将新的模块发送到浏览器,浏览器用新的模块替换掉旧的模块,以实现在不刷新浏览器的前提下更新页面