UNDERCOVERj / tech-blog

个人博客☺️
39 stars 1 forks source link

深入浅出webpack摘要 #18

Open UNDERCOVERj opened 5 years ago

UNDERCOVERj commented 5 years ago

[TOC]

核心概念

  1. Entry:入口,webpack执行构建的第一步从Entry开始,可抽象成输入,然后递归解析依赖的文件
  2. Module:模块,一切皆为模块,一个模块对应一个文件,从Entry开始递归找出所有依赖的模块
  3. Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并与分割
  4. Loader:模块转换器,用于将模块的原内容按照需求转换成新内容(浏览器可识别的内容)
  5. Plugin:扩展插件,在构建流程的特定时机中注入扩展逻辑,来改变构建结果
  6. Output:输出结果,经过处理最终想要的代码结果

过程:

  1. 从Entry里配置的Module开始,递归解析Entry依赖的所有Module
  2. 找到一个Module,根据配置的Loader去找出对应的转换规则
  3. Module转换后,再解析出当前Module依赖的Module。
  4. 这些模块以Entry为单位进行分组,一个Entry及其所有依赖的Module被分到一个组就是一个Chunk
  5. 将所有Chunk转换成文件的输出
  6. 流程中在适当的时机执行Plugin里定义的逻辑

Entry

chunk的名称

chunk的名称和Entry的配置有关

  1. 如果entry是一个string或者array,就只会生成一个Chunk,这是chunk的名称是main
  2. 如果entry是一个object,就会出现多个chunk,名称为键值

OutPut

  1. filename:输出文件的名称,webpack会为每个chunk取一个名称

内置变量列表:

变量名 含义
id chunk的唯一表示,从0开始
name chunk的名称
hash chunk的唯一标识的Hash值
chunkhash chunk内容的hash值
contenthash(CSS) 代码内容本身,而不是由一组模块组成的chunk
  1. chunkFilename:配置无入口的Chunk在输出时的文件名称,在运行过程中生成。场景:CommonChunkPlugin、import动态加载。

Module

处理模块的规则

rules

配置模块的读取和解析规则

Resolve

从入口触发找出所有依赖的模块时,配置如何寻找模块对应的文件

DevServer

将webpack构建出的文件保存在内存中,在要访问输出的文件时,必须通过http服务访问

DevServer让webpack构建出的js代码里注入一个代理客户端用于控制网页,网页和DevServer间通过WebSocket协议通信。(刷新)

问题:从entry入口递归解析依赖的文件,只有entry本身和依赖的文件才会进入监听列表,所以html改变,不会触发上述机制,需要重新加载

inline:关闭,通过iframe的方式去运行要开发的网页在location后加/webpack-dev-server

同构

前端渲染的弊端:

  1. 搜索引擎无法收录,展示的数据是在浏览器端异步渲染出来的
  2. 渲染瓶颈,首屏渲染延迟

虚拟DOM的优点

  1. 操作dom耗时长,应减少dom操作,通过将dom抽象成js对象,通过diff算法,最后得出最小的dom操作。
  2. 可以将虚拟dom渲染成字符串(服务端渲染),或者app原生组件(ReactNative)

最终目的

构建出两份js代码,一份在浏览器端运行,一份在node环境运行并输出html

注意:

  1. 不能包含浏览器环境提供的api,例如document
  2. 不能包含css代码,因为会增加额外计算量,影响性能。
  3. 不能将node_modules的第三方模块和node原生模块打包进去,而是通过commonjs引入
  4. 通过commonjs导出一个渲染函数,在http服务器执行这个函数,渲染出html内容返回

devtool

source map:一个信息文件,里面储存着位置信息,转换后的代码的每一个位置,所对应的转换前的位置。

控制是否生成,以及如何生成 source map。方便在浏览器中通过源码调试

优化

缩小文件的搜索范围

  1. 优化loader,使用test、include、exclude
  2. 优化resolve.modules(寻找第三方模块)
resolve: {
    modules: [path.resolve('../node_modules')],
}
  1. 优化resolve.mainFields配置

配置第三方模块使用哪个入口文件,大多数第三方模块都采用main字段去描述入口文件的位置

{
    resolve: {
        mainFields: ['main']
    }
}
  1. 尽量补全后缀
  2. module.noParse

忽略对部分没采用模块化的文件的递归处理,提高构建性能

使用DLLPlugin和DLLReferencePlugin

  1. 将网页依赖的基础模块抽离,打包到一个个单独的动态链接库中,一个动态链接库可以包含多个模块
  2. 当需要导入的模块存在于动态链接库中时,这个模块不能被再次打包,而是去动态链接库中获取
  3. 页面所有动态链接库都需要被加载

用DLLPlugin构建出动态链接库,DLLReferencePlugin告诉webpack使用了哪些动态链接库

使用happyPack

原理:Loader对文件操作很耗时,happypack将任务分解到多个进程中并行处理,从而减少构建时间

代码压缩

mode: production

自动刷新

  1. 文件监听
{
    watch: true,
    watchOptions: {
        ignored: /node_modules/, // 忽略node_modules的监听
        aggregateTimeout: 300, // 节流
        poll: 1000 // 每秒轮询1000次
    }
}

原理:定时获取文件的最后编辑时间(fs.stats),当发生变化则存起来,aggregateTimeout时间后重新构建。

从entr开始递归解析依赖的文件,加入监听列表

  1. 自动刷新浏览器

监听文件发生改变,webpack-dev-server则负责刷新浏览器

刷新原理:

1. 借助浏览器扩展区通过浏览器提供的接口刷新,如:webstorm ide的liveedit功能
2. 向要开发的网页中注入代理客户端代码,通过代理客户端去刷新整个页面
3. 将要开发的网页装进一个iframe钟,通过刷新iframe去看到效果

开启模块热替换

不刷新整个网页,灵敏实时预览

区分环境

new DefinePlugin({
    'process.env': {
        NODE_ENV: 'production'
    }
})

process.env.NODE_ENV === 'production'

接入cdn

{
    output: {
        publicPath: '//js.cdn.com/id/'
    }
}

Tree Shaking

剔除用不上的死代码

提取公共代码

使用splitChunks(webpack4)、commonChunkPlugin

optimization: {
    splitChunks: {
        chunks: 'all',
        cacheGroups: {
            vendor:{ // 抽离第三插件,本来打算搞成多页想想没必要,单页应该就不用抽了
                test: /[\\/]node_modules[\\/]/,
                chunks: 'initial',
                name: 'vendor',
                priority: 10,
                enforce: true
            },
            commons:{
                test: /utils\/|components\//,
                chunks: 'initial',
                name: 'commons',
                priority: 10,
                enforce: true
            }
        }
    }
}
  1. 多用于多页面
  2. 减少网络传输流量,降低服务器成本
  3. 虽然用户第一次打开网站速度得不到优化但之后访问其他页面的速度大大提升

示例

  1. 将lib基础库,打包到单独文件base.js
  2. 再将所有页面依赖的公共代码提取出来放到common.js
  3. 为每个网页都生成一个单独的文件,不包含common.js、base.js

分隔代码以按需加载

  1. 一次性加载所有功能代码,导致页面卡顿
  2. 需要用户想要什么功能就加载这个功能对应的代码

原则:

  1. 将网站划分为小功能
  2. 每一类合并为一个chunk,按需加载对应的chunk
  3. 首屏不按需加载
  4. 分割出去的代码加载需要一定的时机

webpack按需加载


// webpack.config.js

output: {
    chunkFilename: 'js/[name]/[chunkhash:8].chunk.js'
}

// app.js

import (/* webpackChunkName: "xx" */ './xx.js')
    .then(xx => ....)

// react router按需加载

<Route path='/about' component={getAsyncComponent(() => import(/* webpackChunkName */, './about'))}

// getAsyncComponent

function getAsyncComponent(load) {
    return class AsyncComp extends PureComponent {

        componentDidMount() {
            load().then(({default: copmonent}) => this.setState({compoennt}))
        }
        render() {
            let {component} = this.state;
            return component ? createElement(component): null;
        }
    }
}

原理

流程:

  1. 初始化参数:从配置文件和shell中读参数
  2. 开始编译:用初始化compiler对象,加载所有配置的插件通过执行对象的run 方法开始编译
  3. 确定入口
  4. 编译模块(递归、Loader)
  5. 完成模块编译(得到依赖关系和最终翻译后的内容)
  6. 输出资源:组装Chunk
  7. 输出完成

三个阶段:

  1. 初始化:启动构建,读取与合并配置参数,加载Plugin,实例化Compiler
  2. 编译:从Entry触发,每个module串行调用loader,递归编译
  3. 输出:将编译后的module组成Chunk,将Chunk转换成文件

Loader编写

  1. 获取options
import loaderUtils from 'loader-utils';

module.exports = function(source) {
    const options = loaderUtils.getOptions(this) || {};

    ...

    return source;// 将原内容返回,等于没有处理
}
  1. 向loader注入API
this.callback(
    err: Error | null,
    content: string | Buffer // 原内容转换后的内容
    sourceMap?: SourceMap
    abstractSynctaxTree?: AST // 如果本次转换为原内容生成了AST语法书,则将这个AST返回,避免重复生成AST,方便之后使用
)

return;
  1. 同步和异步
module.exports = function(source) {
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) [
        callback(err, result, sourceMaps, ast);
    })
}
  1. 处理二进制数据
module.exports = function (source) {
    source instanceof Buffer === true;
    return source;
}
module.exports.raw = true; // 告诉webpack该loader是否需要二进制数据
  1. 缓存加速
this.cacheable(false) // 关闭该loader的缓存功能
  1. 使用本地Loader
    1. 本地npm模块npm link
    2. 在项目根目录npm link loader-name,将2中的全局npm模块链接到node_modules下

Loader Api

this.context // 当前处理的文件所在目录,处理/src/main.js,则this.context等于/src
this.resource // 当前处理的文件的完整请求路径,/src/main.js?name=1
this.resourcePath // 当前处理的文件的路径,/src/main.js
this.resourceQuery // 当前处理的文件的queryString
this.target // target配置
this.loadModule // 获取依赖的文件的处理结果
this.resolve // 文件完整路径
this.addDependency // 为当前处理的文件添加依赖文件
this.addContextDependency // 将整个目录加入当前处理的文件依赖
this.clearDependencies // 清除当前处理的文件所有依赖
this.emitFile(name, content[,...]) // 输出一个文件  

Plugin编写

webpack生命周期会广播许多事件,plugin监听这些事件。

class BasicPlugin {
    constructor(options) {}
    apply(compiler) {
        compiler.plugin('compilation', function(comilation) {

        })
    }
}

module.exports = BasicPlugin;
  1. 读取配置时,执行 new BasicPlugin(options),得到实例
  2. 初始化 compiler 后,调用 basicPlugin.apply(compiler)
  3. 插件实例获取到compiler后,就可以通过compiler.plugin(事件名称,回调函数)监听到webpack广播的事件
  4. 通过 compiler 去操作 webpack

compiler(代表从启动到关闭的生命周期)

  1. 包含webpack的所有配置信息

compilation(一次新的编译)

  1. 包含当前的模块资源、编译生成原理、变化的文件等
  2. 当一个文件发生变化,便又一次新的compilation被创建
  3. compilation提供很多事件回调提供插件的扩展

Tapable

观察者模式

compiler和compilation都继承自Tapable

API

  1. 读取输出资源、代码块、模块及其依赖

emit事件发生时,代表源文件的转换和组装已完成,可读取到最终将输出的资源、代码块、模块及其依赖,也可以修改输出资源的内容

  1. 监听文件的变化

当入口模块及依赖模块发生变化时会触发一次新的compilation

不会监听html,可手动将html添加到依赖列表

compilaer.plugin('after-compile', (compilation) => {
    compilation.fileDependencies.push(filePath);
})
emit
watch-run // 依赖文件发生变化
after-compile
  1. 修改输出资源

监听emit资源

输出资源存放在compilation.assets中,键为文件名称,值为文件对应的内容