webpack

目录

Babel:JS编译器(es6->es5,jsx->js)

loader:编译->js

less-loader:less->css

css-loader:css->js

style-loader:

创建style标签,将js中的样式资源插入标签内,并将标签添加到head中生效

ts-loader:

打包编译Typescript文件

执行顺序:出栈(从后往前)

plugin:发布订阅、广播、监听

优、删、简:压缩代码和图片

对AST抽象语法树进行优化

Tree Shaking:未使用的代码移除

压缩算法:简化表达式,字符串压缩

分类

html-webpack-plugin :

处理html资源,默认会创建一个空的HTML,自动引入打包输出的所有资源(js/css)

mini-css-extract-plugin:

打包过后的css在js文件里,该插件可以把css单独抽出来

clean-webpack-plugin :

每次打包时候,CleanWebpackPlugin 插件就会自动把上一次打的包删除

loader和plugin的区别:loader运行在编译阶段,plugins 在整个周期都起作用

热更新加载原理

代码变动->重新编译->局部更新->无需刷新页面

websocket:本地、浏览器的双向通信、hash编译标识、生成时间判断变化

与vite区别:webpack更新整个module、vite更新单个文件

懒加载

代码分割:拆分成块,需要时候再加载

commonJS:import动态导入

减小初始加载时间: 只加载必需的代码块

并行加载: 浏览器支持并行加载多个资源

缓存优化: 只有发生更改的代码块需要重新下载

原理/手写

Loader

Node 模块导出函数(source源码){return 处理后的source}

同步:return/this.callback

异步:promise+

Node.js 7.6.0+

配置

链式调用:上一个loader返回作为下一个loader参数

Plugin:带有apply方法的class

Tapable:订阅发布

compiler:编译过程

配置:webpack.config.js 里 require 并实例化

webpack

未涉及 loader 和 plugin


Plugin API | webpack 中文文档 | webpack中文文档 | webpack中文网

Webpack基于common JS规范,它将根据模块的依赖关系进行静态分析,然后将这些模块( js、css、less )按照指定的规则生成对应的静态资源减少了页面的请求

Babel:JS编译器(es6->es5,jsx->js)

将es6、es7、es8等语法转换成浏览器可识别的es5或es3语法,即浏览器兼容的语法,比如将箭头函数转换为普通函数

将jsx转换成浏览器认的js

阶段: parsing (js解析为ast)、transforming (ast转换优化)、generating (ast生成js)

1)通过babylon将 js 转化成 ast (抽象语法树)

2)通过babel-traverse是一个对 ast 进行遍历,使用 babel 插件转化成新的 ast

3)通过babel-generator将 ast 生成新的 js 代码

loader:编译->js

webpack只认识JS和JSON,所以Loader相当于翻译官,将其他类型资源进行预处理,最终变为js代码。

less-loader:less->css

开发中,会使用less预处理器编写css样式,使开发效率提高)

css-loader:css->js

将css文件变成commonjs模块(模块化的规范)加载到js中,模块内容是样式字符串

style-loader:

创建style标签,将js中的样式资源插入标签内,并将标签添加到head中生效

ts-loader:

打包编译Typescript文件

执行顺序:出栈(从后往前)

  • style-loader 将css插入到页面的style标签
  • css-loader 是将@import 和 url() 转换成 import/require()的
  • less-loader 是将less文件编译成css
  • postcss是目前css兼容性的解决方案,会自动帮我们加入前缀,以使css样式在不同的浏览器能兼容

postcss-loader要写在最后(其实只要放在css-loader之后就可以)

{ test: /\.(less|css)?$/, loader: ["style-loader", "css-loader", "less-loader", "postcss-loader"]}

webpack会按照从右到左的顺序执行loader,我们新解析less,之后进行css的打包编译。如果你不适用less等预处理语言,安装css-loader和style-loader即可。

plugin:发布订阅、广播、监听

优、删、简:压缩代码和图片

底层是利用发布订阅模式,webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,在特定的时机对资源做处理

对AST抽象语法树进行优化

在代码压缩过程中,Terser 会将 JavaScript 代码解析成 AST(Abstract Syntax Tree,抽象语法树)结构,然后对 AST 进行操作和优化。AST 是一种将代码抽象化的数据结构,它可以更方便地进行代码分析、转换和优化。

Tree Shaking:未使用的代码移除

Terser 通过 Tree Shaking 技术,可以将未使用的代码从打包后的文件中移除,从而减小文件体积。Tree Shaking 是一种静态分析技术,它可以分析代码中哪些部分是可达的,哪些部分是不可达的。

  • 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
  • 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
  • 先进行 tree shaking,然后再使用 Babel 进行转译

常通过设置 mode 为 'production' 来开启。在生产模式下,Webpack 会自动开启一些优化,包括 Tree Shaking。

// webpack.config.js
module.exports = {
  mode: 'production',
  // other configurations...
};
压缩算法:简化表达式,字符串压缩

Terser 采用了一些压缩算法,例如变量重命名、死代码移除、字符串压缩、简化表达式等,来进一步压缩 JavaScript 代码。

分类

html-webpack-plugin :

处理html资源,默认会创建一个的HTML,自动引入打包输出的所有资源(js/css)

mini-css-extract-plugin:

打包过后的css在js文件里,该插件可以把css单独抽出来

clean-webpack-plugin :

每次打包时候,CleanWebpackPlugin 插件就会自动把上一次打的包删除

loader和plugin的区别:loader运行在编译阶段,plugins 在整个周期都起作用

前端性能优化——包体积压缩82%、打包速度提升65% - 掘金

热更新加载原理

代码变动->重新编译->局部更新->无需刷新页面

代码变动,webpack 重新编译,编译后浏览器替换修改的模块,局部更新,无需刷新整个页面

热加载是通过内置的 HotModuleReplacementPlugin 实现的

websocket:本地、浏览器的双向通信、hash编译标识、生成时间判断变化

1) 通过webpack-dev-server开启server服务,本地 server 启动之后,再去启动 websocket 服务,建立本地服务和浏览器的双向通信

2) webpack 每次编译后,会生成一个Hash值Hash 代表每一次编译的标识。本次输出的 Hash 值会编译新生成的文件标识,被作为下次热更新的标识

3)webpack监听文件变化(主要是通过文件的生成时间判断是否有变化),当文件变化后,重新编译

4)编译结束后,本地服务器通知浏览器请求变化的资源,同时将新生成的 hash 值传给浏览器,用于下次热更新使用

5)浏览器通过 jsonp 拉取更新的模块后,用新模块替换掉旧的模块,从而实现了局部刷新

与vite区别:webpack更新整个module、vite更新单个文件

jsonp 回调触发模块热替换逻辑。Vite把需要在启动过程中完成的工作,转移到响应浏览器请求的过程中

之后reload页面时,首屏的性能会好很多(缓存

懒加载

动态加载的文件,需要做 resolve、load、transform、parse 操作,并且还有大量的 http请求

代码分割:拆分成块,需要时候再加载

commonJS:import动态导入

源代码直接上线:虽然过程可控,但是http请求多,性能开销大。

打包成唯一脚本:服务器压力小,但是页面空白期长,用户体验不好。

本质其实就是在源代码直接上线打包成唯一脚本main.bundle.js这两种极端方案之间的一种更适合实际场景的中间状态。

减小初始加载时间: 只加载必需的代码块

并行加载: 浏览器支持并行加载多个资源

缓存优化: 只有发生更改的代码块需要重新下载

原理/手写

Loader

Node 模块导出函数(source源码){return 处理后的source}

同步:return/this.callback

const loaderUtils = require("loader-utils");

// 定义一个导出函数,这个函数将被 Webpack loader 调用
module.exports = function(source) {
    // source源码
    const content = doSomeThing2JsString(source);
    
     // 获取用户配置的options
    const options = loaderUtils.getOptions(this);
    
    // 输出当前 loader 执行时的上下文路径,用于解析其他模块路径
    console.log('this.context');
    
    /*
     * this.callback 参数:
     * error:Error | null,当 loader 出错时向外抛出一个 error
     * content:String | Buffer,经过 loader 编译后需要导出的内容
     * sourceMap:为方便调试生成的编译后内容的 source map
     * ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,
     * 进而省去重复生成 AST 的过程
     */
    // 通过 this.callback 向 Webpack 返回处理后的结果,这是 loader 的标准写法
    this.callback(null, content);
    // 或者直接使用 return content; 也是可以的,与 this.callback 实现相同的效果
}

异步:promise+

如果计算量很小,同步也可以,但尽可能的异步化 Loader,

尤其是在执行的操作可能会耗费较长时间的情况下。这是因为 Webpack 在构建过程中是一个基于异步操作的系统,而异步化的 loader 有助于提高整体构建性能和并行性

this.async 返回一个类似于 Node.js 中的回调函数,它接受两个参数,第一个是错误对象(如果有错误的话),第二个是处理后的内容

module.exports = function(content){
    function timeout(delay) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve("{};" + content)
            }, delay)
        })
    }
    const callback = this.async()
    timeout(1000).then(data => {
        callback(null, data)
    })
}
Node.js 7.6.0+
module.exports = async function(content){
    function timeout(delay) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve("{};" + content)
            }, delay)
        })
    }
    const data = await timeout(1000)
    return data
}

配置

// webpack.config.js
module.exports = {

  module: {
    rules: [
      {
        //匹配文件后缀名.css等
        test: /^your-regExp$/,
        use: [
          {
             loader: 'loader-name-A',
             // options:自定义配置
             options: {
              // 自定义配置给 loader-name-A
              // 可以根据 loader 的文档提供适当的选项
              // 例如,以下是 loader-name-A 可能使用的某些自定义选项
              optionA: true,
              optionB: 'value',
              },
          }, 
          {
             loader: 'loader-name-B',
          }
        ]
      },
    ]
  }
}

链式调用:上一个loader返回作为下一个loader参数

loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的参数

Plugin:带有apply方法的class

Tapable订阅发布

class SyncHook{
    constructor(){
        this.hooks = [];
    }

    // 订阅事件
    tap(name, fn){
        this.hooks.push(fn);
    }

    // 发布
    call(){
        this.hooks.forEach(hook => hook(...arguments));
    }
}

compiler:编译过程

compiler 是 Webpack 的主要编译实例,代表了整个编译过程。

Webpack 在运行时会创建一个 compiler 对象,

它包含了完整的 Webpack 配置信息,生命周期的各个阶段的钩子(hooks)。

//@file: plugins/myplugin.js
class myPlugin {
    constructor(options){
        //用户自定义配置
        this.options = options
        console.log(this.options)
    }
    apply(compiler) {
        console.log("This is my first plugin.")
    }
}

module.exports = myPlugin

配置:webpack.config.js 里 require 并实例化

const MyPlugin = require('./plugins/myplugin-4.js')

module.exports = {
    ......,
    plugins: [
        new MyPlugin("Plugin is instancing.")
    ]
}

webpack

1)webpack 从项目的entry入口文件开始递归分析,调用所有配置的 loader对模块进行编译

2)babel将 js ->ast抽象语法树

3)babel-traverse对 ast 进行遍历,找到文件的import引用节点

4)每个模块生成一个唯一的 id,并将解析过的模块缓存起来,根据依赖关系生成依赖图谱

5)递归遍历所有依赖图谱的模块,组装成一个个包含多个模块的 Chunk(块)

把所有依赖打包成一个 或多个bundle.js文件(捆bundle)浏览器可识别的JavaScript文件。

6)最后将生成的文件输出到 output 的目录中

未涉及 loader 和 plugin

  1. Webpack通过一个给定的主/入口文件(如:index.js)开始找到项目的所有依赖文件,

  2. 解析js->ast语法树->json数据结构

  3. 将es6 es7 等高级的语法->es5

  4. 递归遍历引入的其他 js,生成最终的依赖关系图谱

  5. 最终生成一个可以在浏览器加载执行的 js 文件

const fs = require('fs');
const path = require('path');
// babylon解析js语法,生产AST 语法树
// ast将js代码转化为一种JSON数据结构
const babylon = require('babylon');
// babel-traverse是一个对ast进行遍历的工具, 对ast进行替换
const traverse = require('babel-traverse').default;
// 将es6 es7 等高级的语法转化为es5的语法
const { transformFromAst } = require('babel-core');

// 每一个js文件,对应一个id
let ID = 0;

// filename参数为文件路径, 读取内容并提取它的依赖关系
function createAsset(filename) {
  const content = fs.readFileSync(filename, 'utf-8');

  // 获取该文件对应的ast 抽象语法树
  const ast = babylon.parse(content, {
    sourceType: 'module'
  });

  // dependencies保存所依赖的模块的相对路径
  const dependencies = [];

  // 通过查找import节点,找到该文件的依赖关系
  // 因为项目中我们都是通过 import 引入其他文件的,找到了import节点,就找到这个文件引用了哪些文件
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      // 查找import节点
      dependencies.push(node.source.value);
    }
  });

  // 通过递增计数器,为此模块分配唯一标识符, 用于缓存已解析过的文件
  const id = ID++;
  // 该`presets`选项是一组规则,告诉`babel`如何传输我们的代码.
  // 用`babel-preset-env`将代码转换为浏览器可以运行的东西.
  const { code } = transformFromAst(ast, null, {
    presets: ['env']
  });

  // 返回此模块的相关信息
  return {
    id, // 文件id(唯一)
    filename, // 文件路径
    dependencies, // 文件的依赖关系
    code // 文件的代码
  };
}

// 我们将提取它的每一个依赖文件的依赖关系,循环下去:找到对应这个项目的`依赖图`
function createGraph(entry) {
  // 得到入口文件的依赖关系
  const mainAsset = createAsset(entry);
  const queue = [mainAsset];
  for (const asset of queue) {
    asset.mapping = {};
    // 获取这个模块所在的目录
    const dirname = path.dirname(asset.filename);
    asset.dependencies.forEach((relativePath) => {
      // 通过将相对路径与父资源目录的路径连接,将相对路径转变为绝对路径
      // 每个文件的绝对路径是固定、唯一的
      const absolutePath = path.join(dirname, relativePath);
      // 递归解析其中所引入的其他资源
      const child = createAsset(absolutePath);
      asset.mapping[relativePath] = child.id;
      // 将`child`推入队列, 通过递归实现了这样它的依赖关系解析
      queue.push(child);
    });
  }

  // queue这就是最终的依赖关系图谱
  return queue;
}

// 自定义实现了require 方法,找到导出变量的引用逻辑
function bundle(graph) {
  let modules = '';
  graph.forEach((mod) => {
    modules += `${mod.id}: [
      function (require, module, exports) { ${mod.code} },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });
  const result = `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];
        function localRequire(name) {
          return require(mapping[name]);
        }
        const module = { exports : {} };
        fn(localRequire, module, module.exports);
        return module.exports;
      }
      require(0);
    })({${modules}})
  `;
  return result;
}

// ❤️ 项目的入口文件
const graph = createGraph('./example/entry.js');
const result = bundle(graph);

// ⬅️ 创建dist目录,将打包的内容写入main.js中
fs.mkdir('dist', (err) => {
  if (!err)
    fs.writeFile('dist/main.js', result, (err1) => {
      if (!err1) console.log('打包成功');
    });
});

Webpack揭秘——走向高阶前端的必经之路 - 掘金

你可能感兴趣的:(前端面试,webpack,前端,node.js)