前端知识体系4.前端工程化1.Webpack专题

本文目录:

  • 1.webpack的定义及基础核心概念
  • 2.webpack构建原理
  • 3.webpack运行的基本流程
  • 4.webpack 动态加载的实现原理及使用方法
  • 5.loader的原理及手写loader的思路
  • 6.plugin的原理及手写plugin的思路
  • 7.loader和plugin的区别
  • 8.tree sharking是什么
  • 9.什么是webpack热更新
  • 10.介绍下webpack5的新特性
  • 11.Webpack性能优化
  • 12.在前端工程化涌现出众多工具, 试说明webpack与grunt、gulp的不同?
  • 13.npm打包时需要注意哪些?如何利用webpack来更好的构建?

1.webpack的定义及基础核心概念

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
webpack有四个核心概念:
入口(entry),输出(output),loader,插件(plugins)
webpack的入口文件模板结构:

module.exports = {
    //入口配置
    entry: '',
    //出口配置
    output: '',
    //模块配置
    module: {
        rules: [
            {
                test: /\.css/,
                use: ["style-loader", "css-loader"]
            }
         ]
    },
    //插件配置
    plugins: {},
    //模式配置,开发模式还是生产模式
    mode:'',
    //开发服务器配置
    devServer: {},
    //解析配置
    resolve: {}
}

2.webpack构建原理

webpack.config.js导出一个Object对象(或者导出一个Function,或者导出一个Promise函数,还可以导出一个数组包含多份配置)。Webpack从入口文件开始,识别出源码中的模块化导入语句,递归地找出所有依赖,然后把入口文件和所有依赖打包到一个单独的文件中(即一个chunk),这就是所谓的模块打包。

3.webpack运行的基本流程

webpack运行的基本流程分为初始化、编译、输出三个阶段.
初始化:
从配置文件和shell文件读取、合并参数;
加载plugin
实例化compiler
编译:
从entry发出,针对每个module串行调用对应loader编译文件内容
找到module依赖的module,递归进行编译处理
输出:
把编译后module组合成chunk
把chunk转换成文件,输出到文件系统

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

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

4.webpack 动态加载的实现原理及使用方法

在代码中所有被import()的模块,都将打成一个单独的包,放在chunk存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。
ES6的import语法告诉我们,模块只能做静态加载。
所谓静态加载,就是你不能写成如下形式:

let filename  = 'module.js';
import {mod} from './' + filename. 
//也不能写成如下形式:
if(condition) {
  import {mod} from './path1'
} else {
  import {mod} from './path2'
}

先webpack4以后的版本的支持下,import可以进行动态加载,大致用法如下:import()接收一个路径参数,然后通过then的方式引入模块

let filename = 'module.js'; 

import('./' + filename). then(module =>{
    console(module);
}).catch(err => {
    console(err.message); 
});

//如果你知道 export的函数名
import('./' + filename). then(({fnName}) =>{
    console(fnName);
}).catch(err => {
    console(err.message); 
});

这里有一点要注意的是:
import的加载是加载的模块的引用。而import()加载的是模块的拷贝,就是类似于require(),怎么来说明?看下面的例子:
module.js 文件:

export let counter = 3;
export function incCounter() {
  counter++;
}

main.js 文件:

let filename = 'module.js'; 
 import('./' + filename).then(({counter, incCounter})=>{
 console.log(counter); //3
 incCounter(); 
 console.log(counter); //3
 }); 

原本的import写法:

import {counter, incCounter} from './module.js';
console.log(counter); //3
incCounter();
console.log(counter); //4

5.loader的原理及手写loader的思路

loader是 webpack 用于在编译过程中解析各类文件格式,并输出;
loader(加载器)是一个代码转换器,它由 webpack 的 loader runner 执行调用,接收原始资源数据作为参数(当多个加载器联合使用时,上一个loader的结果会传入下一个loader),最终输出 javascript 代码(和可选的 source map)给 webpack 做进一步编译。
手写loader的思路:
loader本质上就是一个 node 模块,通过写一个函数来完成自动化的过程。
这里通过写一个最简单的loader来理解手写loader的思路。
当只有一个 loader 应用于资源文件时,它接收源码作为参数,输出转换后的 js 代码。文件路径:loaders/simple-loader.js

module.exports = function loader (source) {
    console.log('simple-loader is working');
    return source;
}

这就是一个最简单的 loader 了,这个 loader 啥也没干,就是接收源码,然后原样返回,为了证明这个loader被调用了,我在里面打印了一句话‘simple-loader is working’。
测试这个 loader:
若是使用 npm 安装的第三方 loader,直接写 loader 的名字就可以了。但是现在用的是自己开发的本地 loader,需要我们手动配置路径,告诉 webpack 这些 loader 在哪里。

// webpack.config.js
const path = require('path');
module.exports = {
  entry: {...},
  output: {...},
  module: {
    rules: [
      {
        test: /\.js$/,
        // 直接指明 loader 的绝对路径
        use: path.resolve(__dirname, 'loaders/simple-loader')
      }
    ]
  }
}

执行webpack编译,可以看到,控制台输出 ‘simple-loader is working’。说明 loader 成功被调用。

6.plugin的原理及手写plugin的思路

wenpack根据自己的工作机制提供了许多hooks,类似于Vue的生命周期。
例如:run(开始编译阶段),make( 从 entry 开始递归分析依赖,准备对每个模块进行 build),done(完成所有的编译过程)
plugin必须是一个函数,或者是一个包含apply的对象。一般来说我们都会定义一个类型,然后在这个类型中定义apply方法,最后再通过这个类型来创建一个实例对象去使用这个插件。
例如下面这段代码

const pluginName = 'myplugin'
module.exports =  class myplugin {
    apply(){}
}

这个apply方法接收一个叫compiler的参数对象,这个对象是webpack工作中最核心的对象,包含了此次打包构建的所有配置信息,我们就可以通过这个对象去注册钩子函数。

const pluginName = 'myplugin'
module.exports =  class myplugin {
    apply(compiler){
        compiler.hooks.run.tap(pluginName, () =>{
            {
                console.log('开始编译');
            }
        })
    }
}

我们想在run阶段输出‘开始编译’这句话,在webpack.config.js中引入并配置

const myplugin = require('./myplugin')
...
plugins:[
  new myplugin()
]
...

进行webpack编译,在控制台可以看到在开始阶段输出了内容,说明plugin生效了。

7.loader和plugin的区别

对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程。
plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。

不同的作用

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

不同的用法

  • Loader在module.rules中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
  • Plugin在plugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

8.tree sharking是什么

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术。
我们在项目中创建一个utils.js文件:

export function add(a, b) {
    console.log('add');
    return a + b;
}
export function minus(a, b) {
    console.log('minus');
    return a - b;
}
export function multiply(a, b) {
    console.log('multiply');
    return a * b;
}
export function divide(a, b) {
    console.log('divide');
    return a / b;
}

index.js文件中导入utils.js的add方法并调用:

import { add } from './utils';
add(10, 2);

运行npm run build后查看dist/bundle.js文件,可以发现utils.js中所有的代码都打包了,并没有像我们预期的那样只打包add()函数。
CommonJS的动态特性模块意味着tree shaking不适用。因为它是不可能确定哪些模块实际运行之前是需要的或者是不需要的。在ES6中,进入了完全静态的导入语法:import。ES6的import语法完美可以使用tree shaking,因为可以在代码不运行的情况下就能分析出不需要的代码。
webpack4以后的版本,只需要将mode设置为production即可开启tree shaking。

9.什么是webpack热更新

模块热替换(HMR - Hot Module Replacement)允许在运行时替换,添加,删除各种模块,而无需进行完全刷新重新加载整个页面。
一个带有热替换功能的webpack.config.js 文件的配置如下,做了这么几件事

  • 引入了webpack库
  • 使用了new webpack.HotModuleReplacementPlugin()
  • 设置devServer选项中的hot字段为true

10.介绍下webpack5的新特性

1.通过
嵌套tree-shaking的实现
移除Node.js polyfills 自动加载功能
有效减少打包后的文件体积。
2.生成的代码不再仅仅是ES5,也会生成 ES6 的代码
3.optimization配置中优化了minSize&maxSize的配置方式,对js和css有了区分,单位是kb

optimization: {
  runtimeChunks: {},
  splitChunks: {},
  // 在文件大小为0-30kb的情况下进行文件分割
  minSize: {
    javaScript: 0,
    style: 0
  },
  maxSize: {
    javaScript: 30,
    style: 30
  }
}

4.在配置文件中使用cache: {type: "filesystem"}配置实现持久化缓存,提高构建速度

11.Webpack性能优化

优化可以从两个方面考虑,一个是优化开发体验,一个是优化输出质量。

优化开发体验

①缩小文件搜索范围
resolve字段告诉webpack怎么去搜索文件,所以首先要重视resolve字段的配置:
由于loader对文件转换操作很耗时,应该尽量减少loader处理的文件,可以使用include命中需要处理的文件,缩小命中范围。
②DllPlugin可以将特定的类库提前打包然后引入
DllPlugin是webpack的内置插件,这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案
③HappyPack
因为Node是单线程运行的,所以Webpack在打包的过程中也是单线程的,特别是在执行Loader的时候,这样会导致等待的情况,HappyPack可以将Loader的同步执行转换为并行的,HappyPack插件需要另外安装。
④使用source-map优化代码调试
在webpack.config.js中加入devtool:'source-map'可以让构建后代码出错,会通过映射关系追踪源代码错误。
实际开发中我们往往只需要在开发环境中开启source-map

const isProd = process.env.NODE_ENV === 'production';
module.exports = {
    devtool: isProd
        ? false
        : '#cheap-module-source-map',
}

⑤热更新HMR
利用webpack内置插件HotModuleReplacementPlugin,无需在每次更改内容时都重新加载整个页面。

优化输出质量

优化输出质量最大的好处就是可以减少首屏的加载时间
①按需加载路由
如果我们把十几个页面甚至更多的路由页面,把这些页面全部打包进一个JS文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给客户,这时候我们就可以使用按需加载,将每个路由页面单独打包为一个文件
②使用Tree Shaking,删除项目中未被引用的代码。
③开启Scope Hoisting
Scope Hoisting直译就是作用域提升,Scope Hoisting会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中,让Webpack打包出来的代码更小、运行更快。
Scope Hoisting 是webpack内置的功能,只要配置一个插件即可

module.exports = {
  plugins: [
    // 开启 Scope Hoisting 功能
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
}

④区分环境--减小生产环境代码体积
代码运行环境分为开发环境和生产环境,代码需要根据不同环境做不同的操作,许多第三方库中也有大量的根据开发环境判断的if else代码,构建也需要根据不同环境输出不同的代码,所以需要一套机制可以在源码中区分环境,区分环境之后可以使输出的生产环境的代码体积减小。Webpack中使用内置DefinePlugin插件来定义配置文件适用的环境。

plugins:[
    new webpack.DefinePlugin({
        'process.env': {
            NODE_ENV: JSON.stringify('production')
        }
    })
]

注意,JSON.stringify('production') 的原因是,环境变量值需要一个双引号包裹的字符串,而stringify后的值是'"production"'
然后就可以在源码中使用定义的环境:

if(process.env.NODE_ENV === 'production'){
    console.log('你在生产环境')
    doSth();
}else{
    console.log('你在开发环境')
    doSthElse();
}

⑤使用terser-webpack-plugin插件压缩JS代码
如果使用的是 webpack v5 或以上版本,你不需要安装这个插件。webpack v5 自带最新的 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

npm install terser-webpack-plugin --save-dev

然后将插件添加到你的 webpack 配置文件中

const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },
};

⑥压缩图片资源
对于某些网站,图像占据了页面很大部分,虽然它们不会阻塞页面渲染,但是它们仍然占用了很大一部分带宽,在webpack中可以使用url-loader来优化。
url-loader 可以将小型静态文件内联到应用程序中。如果不进行配置,它将把接受一个传递的文件,将其放在已编译的包旁边,并返回该文件的url。但是,如果指定 limit 选项,它将把小于这个限制的文件编码为Base64 数据的 url 并返回这个url,这会将图像内联到 JavaScript 代码中,从而可以减少一个HTTP请求。

module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
        options: {
          // Inline files smaller than 10 kB (10240 bytes)
          limit: 10 * 1024,
        },
      },
    ],
  }
};

12.在前端工程化涌现出众多工具, 试说明webpack与grunt、gulp的不同?

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。
grunt和gulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。
webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。
所以总结一下:
从构建思路来说
gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系 webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工复制代码
对于知识背景来说
gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路

13.npm打包时需要注意哪些?如何利用webpack来更好的构建?

Npm是目前最大的 JavaScript 模块仓库,里面有来自全世界开发者上传的可复用模块。你可能只是JS模块的使用者,但是有些情况你也会去选择上传自己开发的模块。 关于NPM模块上传的方法可以去官网上进行学习,这里只讲解如何利用webpack来构建。

NPM模块需要注意以下问题:

  1. 要支持CommonJS模块化规范,所以要求打包后的最后结果也遵守该规则。
  2. Npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传。
  3. Npm包大小应该是尽量小(有些仓库会限制包大小)
  4. 发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。
  5. UI组件类的模块应该将依赖的其它资源文件,例如.css文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于webpack配置做以下扩展和优化:

1.CommonJS模块化规范的解决方案: 设置output.libraryTarget='commonjs2'使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用
输出ES5代码的解决方案:使用babel-loader把 ES6 代码转换成 2.ES5 的代码。再通过开启devtool: 'source-map'输出SourceMap以发布调试。
3.Npm包大小尽量小的解决方案:Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件
4.不能将依赖模块打包到NPM模块中的解决方案:使用externals配置项来告诉webpack哪些模块不需要打包。
5.对于依赖的资源文件打包的解决方案:通过css-loader和extract-text-webpack-plugin来实现,配置如下:

const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
  module: {
    rules: [
      {
        // 增加对 CSS 文件的支持
        test: /\.css/,
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader']
        }),
      },
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      // 输出的 CSS 文件名称
      filename: 'index.css',
    }),
  ],
};

你可能感兴趣的:(前端知识体系4.前端工程化1.Webpack专题)