背景:这世纪20年代,互联网前端内卷严重,业内再也不需要cli工程师(指的是仅仅会用vue-cli、create-react-app,不会配置webpack)。

这篇文章是整体的综述,可以快速的过一下webpack相关配置和需要注意点,面试之前最是好用。
不过,文中涉及的点比较多却比较概括,不够完整和丰富,另外,也基本上是v4时代,东西或有落伍。
所以今年(22年),在文档库./目录下,计划对相关要点做以丰富和补充。

工程化:问题抛出?

随便抛出几个面试常见的问题

  • 前端为什么要构建和打包?开放式问题,文章最后给出要点
  • moudle、chunk、bundle?
  • loader、plugin?
  • 怎么懒加载?
  • 常见的性能优化?
  • babel-runtime?babel-polyfill?

基本配置和配置思路

  • 拆分配置和merge
    这个其实应该是实践中第一步要做的。
    拆分配置就是说,定义一个公共的配置文件,然后面向dev环境的面向生产环境的文件都引入下;
    就比如:
    webpack.common.js —— 定义公共的配置
    webpack.dev.js —— 开发的配置中,合并了公共配置
    webpack.prod.js —— 线上的配置中,合并了公共配置
    但是最终,查分的东西需要合并的。合并的操作需要依赖smart from webpack-merge:
  1. const { smart } = require('webpack-merge');

因为开发环境和生产环境定义的配置是不同的,比如开发环境需要定义dev-server,但是生产环境不用;再比如,开发环境直接引用图片url,生产环境可能要考虑base-64。

dev环境下本地服务dev-server

dev-server:webpack-dev-server
这里注意,开发的时候跑项目用的是webpack-dev-server。打包的时候需要用webpack。

处理es6

这个应该写在common配置里面。
用babal-loader这个loader。
用到babel的话,babel相关的配置写在.babelrc文件中。比如.babelrc, preset-env就包括常用的es6、es7….and so on:

  1. // in config.module.rules
  2. moudule:{
  3. rules:[
  4. {
  5. test: /.js$/,
  6. loader: ['babel-loader'],
  7. include: srcPath,
  8. exclude: /node_moudles/
  9. },
  10. ]
  11. }
  12. // in babelrc
  13. {
  14. "presets": ["@babel/preset-env"],
  15. "plugins": []
  16. }

样式处理

这个应该写在common配置里面。

  1. basic. 处理css
  • css-loader: 将模块话的css文件解析出来,比如我们的项目中可能这样写引入css:
    1. import 'main.css';

(webpack是万物皆模块,本来是分不清css、js等文件的)

  • style-loader:是将我们解析出的css,以’style’标签插入到页面中。

这里涉及的过程是要有顺序的(这里有个考点,需要的顺序和配置中书写的顺序相反),所以,最终的配置可能是:

  1. {
  2. test: /.css$/,
  3. loader: ['style-loader', 'css-loader']
  4. }
  1. advance. postcss兼容性处理
    我们需要借助postcss-loader来搞点浏览器前缀之类的来处理下兼容性的问题。比如transform,可能需要变成-webkit-transform。
    这个步骤应该先于css-loader,所以应该在后面loader增加一个postcss-loader:
    1. {
    2. test: /.css$/,
    3. loader: ['style-loader', 'css-loader', 'postcss-loader']
    4. }

另外,postcss-loader需要一个配置来引入具体某一个插件, 新建一个postcss.config.js:

  1. module.exports = {
  2. plugins: [require('autoprefixer')] // autoprefixer增加前缀的一个插件
  3. }
  1. more. 处理less、sass
    less为例,这个过程参考上面的,就是先解析less语法,在解析css,最后插入为style标签:
    1. {
    2. test: /.less$/,
    3. loader: ['style-loader', 'css-loader', 'less-loader']
    4. }

处理图片

这里的策略应该跟据开发和上线的需求作出区分。
dev开发的时候,直接引入图片的url就行,那么在webpack.dev.js中配置:

  1. rules: [
  2. {
  3. test: / \.(png|jpg|jpeg|gif)$/,
  4. use: 'file-loader'
  5. }
  6. ]

但在生产打包的时候,需要的不是file-loader,而是url-loader,可能需要将小于5kb的图片用base的url返回,然后写在html中,这样减少了一次http请求图片。(url-loader:Loads files as base64 encoded URL, url-loader

支持React和Vue

  • 支持React只要在.babelrc中配置下
  1. {
  2. presets: ['@babel/presets-react']
  3. }
  • 支持Vue用vue-loader,在rules中
    1. {
    2. test: /.vue$/,
    3. loader: ['vue-loader'],
    4. include: srcPath
    5. }

高级配置

多入口或多页应用

多入口,这是针对线上build的。至少要配置三件事:1. 配置多个入口(entry) 2. 将js输出成多个文件;3. 配置html对应多个;
下面例子是将index.html变成index.html、other.html两个入口:

  1. // webpack.common.js
  2. // 这两个是我们代码中的入口
  3. {
  4. entry: {
  5. index: path.join(srcPath, 'index.js'),
  6. other: path.join(srcPath, 'other.js')
  7. }
  8. }
  9. // webpack.prod.js
  10. // 输出文件
  11. // [name].[contentHash:8].js name就是和entry的key相对应的
  12. output: {
  13. filename: '[name].[contentHash:8].js',
  14. path:distPath,
  15. }
  16. // webpack.common.js
  17. // 生成多个html文件
  18. plugins: [
  19. // 多入口,生成 index.html
  20. new HtmlWebpackPlugin({
  21. template: path.join(srcPath, 'index.html'),
  22. filename: 'index.html',
  23. chunks: ['index']
  24. }),
  25. // 多入口,生成 other.html
  26. new HtmlWebpackPlugin({
  27. template: path.join(srcPath, 'other.html'),
  28. filename: 'other.html',
  29. chunks: ['other']
  30. }),
  31. ]

这里chunks这个概念非常重要,chunks就是当前的这个html文件中要script那些js,比如针对上面的例子,我们可选的chunks有: index.js和other.js。不错,都来自我们entry的配置。另外,如果这里不写chunks这个配置,那么默认就是所有的js文件。

抽离CSS文件

上面的基本配置’style-loader’, ‘css-loader’这种,其实最后做了这样的事情:把css也放在js文件中,然后由js生成style标签然后再插入到html中。但这样的话,style标签的生成依赖js的执行过程,另外,css也需要压缩,或者去除注释。
这样就是为啥要做所谓抽离css这一步。
那么具体的做法大致为:
(可以让dev(webpack.dev.js)的配置保持不变,将上文中提到的抽离的过程放在prod(webpack.prod.js)里面,这样需要将css处理过程从common(webpack.common.js)中拆分出来)

  1. // 1. 借助插件MiniCssExtraPlugin提过的loader代替之前的style-loader
  2. {
  3. test: /.less$/,
  4. loader: [MiniCssExtraPlugin.loader, 'css-loader', 'less-loader', 'postcss-loader']
  5. }
  6. // ... ...
  7. // 2. 增加MiniCssExtraPlugin
  8. // 在plugins里面
  9. plugins: {
  10. // 添加
  11. new MiniCssExtraPlugin({
  12. filename: 'css/main.[contentHash8].css' // 这就是打包后被输出的路径以及文件名
  13. }),
  14. }

压缩需要另外两个插件:TerserJSPlugin、OptimizeCSSAssertsPlugin

  1. // 这个要在optimization中配置
  2. optimization: {
  3. minimizer: [
  4. new TerserJSPlugin({}), new OptimizeCSSAssertsPlugin()
  5. ]
  6. }

开启css module

css module是css-loader的功能,这样开启:

  1. {
  2. loader: "css-loader",
  3. options: {
  4. modules: true, // 开启css module
  5. },
  6. },

参考:

公共代码抽离

这个很重要!
公共代码包括,1. 程序在多个入口的分包中引用的共同文件;2. 第三方依赖库;
因为我们的文件通常是有hash值的,没有变动的文件(尤其第三方库更不会变动了)的hash是不变的。这样就能保证对于访问者来讲,每次需要更新的内容最小。不变的东西命中缓存不香吗?
下面是我们要实现代码抽离的第一步,在run build对应的打包配置下(webpack.prod.js),添加optimization.splitChunks:

  1. optimization: {
  2. // minimizer: .......... ,
  3. splitChunks: {
  4. chunks: 'all',
  5. // 分组
  6. cachaGroups: {
  7. // 第三方
  8. vender: {
  9. name: 'vender',
  10. priority: 1,
  11. test: /node_moudules/,
  12. minSize: 0,
  13. minChunks: 1,
  14. },
  15. // 公共模块
  16. common: {
  17. name: 'common',
  18. priority: 0,
  19. minSize: 0,
  20. minChunks: 2,
  21. },
  22. }
  23. }
  24. }

简单解释下:
上面的optimization.splitChunks.chunks的可选项有:

  • initial: 入口chunks,对于异步导入文件不处理
  • async: 异步chunks,对于非异步导入文件不处理
  • all: 全部处理
    对于第三方的引用,一般我们都是从node_moudles里面拉过来的代码,所以optimization.splitChunks.cachaGroups.vender / optimization.splitChunks.cachaGroups.common中(这其实是两个chunk):
  • 命中的规则就是test: /node_moudules/;
  • priority: 1,表示优先级较高,比较重要;
  • minSize: 0, 这个配置是说,被引用文件的大小低于这个值就不用抽取了,很明显,0就是,都抽取;有的包可能很小,但是没有限制的话,反而不好;
  • minChunks: 1, 这个配置的意思是,若是引用出现至少一次,就需要抽取;这个配置对于第三方模块来讲确实是的,只要被引用一次就应该抽取出来,但是对于开发书写的代码来讲,这里应该配置2,表示某一个公共模块被引用大于等于两次的时候,应该抽出来,这个抽出部分代码的逻辑也符合所谓公共模块的概念。
  • name: 是build出来的js文件的名字;
    这个name最终用到:
  1. output: {
  2. filename: '[name].[contentHash:8].js',
  3. path:distPath,
  4. }

这些chunks,最终需要根据具体项目情况和入口情况,配置到HtmlWebpackPlugin插件的chunks中:

  1. plugins: [
  2. // 多入口,生成 index.html
  3. new HtmlWebpackPlugin({
  4. template: path.join(srcPath, 'index.html'),
  5. filename: 'index.html',
  6. chunks: ['index', 'common', 'vender'] // 根据实际需要,配置chunks
  7. }),
  8. // 多入口,生成 other.html
  9. new HtmlWebpackPlugin({
  10. template: path.join(srcPath, 'other.html'),
  11. filename: 'other.html',
  12. chunks: ['other', 'common']
  13. }),
  14. ]

懒加载

webpack有一个机制其实是支持懒加载的。
就是它认为当import作为函数调用的时候,webpack就会在import语句返回一个promise,文件的内容自然就是当加载完成后被传进then了。
而且,这样加载的文件会被作为chunk,单独会被打包成js单独文件,它被build产生的东西可能是:3.jf7cjen5.js。
比如:

  1. // 文件lazyLoad.js
  2. export default {value: 'lazyLoad'};
  3. // 文件B中加载
  4. setTimeout(() => {
  5. import('./lazyLoad').then(res => console.log(res.default));
  6. }, 5000);

B文件5秒后才会去加载lazyLoad.js,lazyLoad.js build的时候就会被单独打包成文件。

所以说,懒加载是webpack自带的功能。
不过说到懒加载,有必要其实多讨论下React中的懒加载(异步)组件。
知乎: 为什么react加载异步组件的方法要这么写?原理是什么?
待完成: 懒加载

概念回看

moudle、chunk、bundle

  • moudle: 通常我们会听到说,webpack中,万物皆为moudle,那么这个万物是什么?首先,moudle这个概念的时间点是我们打包之前,这个‘万物’说的是,从我们指定的src进去所遇到的所有对资源的引用,比如,其他js依赖文件,比如css,比如引入image;
  • chunk: chunk这个概念适用的时间点是,马上打包完成时,那么chunk是啥?chunk相当于对moudle文件的重新组织之后,生成的目标代码块,chunk在内存中,还没有持久化到文件系统中。在webpack中,有三种东西可以产出chunk:
  1. entry定义的单(多)入口;
  2. import作为函数调用来异步加载文件,这个被加载的文件在内存中形成chunk;
  3. splitChunks(上文里提到)中我们主动配置的分包规则下,产出的chunk;

性能优化

  • 打包方面的优化
    (babel-loader、IgnorePlugin、noParse、happyPack、ParallelUglifyPlugin、自动刷新、热更新、DllPlugin)
  • 产出代码的方面,产品本身

    构建优化

    构建优化:babel-loader

    对于label-loader有两点:
  1. 缓存

    1. {
    2. test: /.js$/,
    3. loader: ['babel-loader?cachaDirectory'],
    4. include: srcPath,
    5. // exclude: /node_moudles/ // 其实include、exclude只要写一个就行
    6. }
  2. include & exclude确实范围

构建优化:IgnorePlugin

避免引入无用模块:
这里涉及一个问题,咋样就能知道是无用的?
比如又一个第三方库,支持n个语言,但是我现在不做国际化,只需要中文。但是如果在线上环境下如果这些不要的东西都打包,势必体积会增大。(以moment为例子)
这时候需要借助IgnorePlugin了。

  1. 启用插件:在webpack.prod.js中

    1. plugins: [
    2. // ...
    3. // IgnorePlugin是webpack自带的
    4. // 忽略moment库里面的local
    5. new webpack.IgnorePlugin(/\.\/locale/, /moment/),
    6. ]
  2. 在页面手动引入

    1. import moment from 'moment';
    2. import 'moment/locale/zh-cn'; // 手动引用中文语言包

构建优化:noParse

避免重复打包。
这是因为我们总会遇到这种xxx.min.js,比如react.min.js这种是已经被工程化打包过的文件,那我们就没必要再搞一次了,不是吗?
这种问题要交给noParse处理:

  1. // 我们在module里面加上:
  2. module:{
  3. noParse: [/react\.min\.js$/] // 检测到这个文件名就不重复打包
  4. }

这个注意一点就是,noParse是不做重复打包了,直接引入。但是IgnorePlugin是不引入。

构建优化:happyPack

happyPack是打包开启多进程,进程,不是线程。
要开启多进程打包,首先配一个happyPack插件,当然最开始需要安装下并引入:

  1. // 引入
  2. const HappyPack = require('happypack')
  3. // ...
  4. // 在plugins里面
  5. plugins: [
  6. new HappyPack({
  7. id: 'babel', // 这个id可以表示当前的 happy pack 的作用
  8. loaders: ['babel-loader?cacheDirectory']
  9. })
  10. ]

然后修改下我们之前在module.rules中写的use:

  1. {
  2. // ['babel-loader?cacheDirectory'] 这是以前
  3. // 引入happypack之后就这么写,id要和上面的匹配
  4. use: [happypack/loader?id=babel]
  5. }

这里有一点,多进程的打包,不一定一定比单进程快,因为开启进程也有开销。

构建优化:ParallelUglifyPlugin

多进程压缩JS,配置如下(配置的含义在下面代码中注了):

  1. // 引入
  2. const ParallelUglifyPlugin = require('ParallelUglifyPlugin')
  3. // ...
  4. // 在plugins里面
  5. plugins: [
  6. new ParallelUglifyPlugin({
  7. output: {
  8. beautify: false, // 不要格式,压成一行
  9. comments: false // 不要注释
  10. },
  11. compress: {
  12. drop_console: true,// 不要console
  13. collapse_vars: true, // 合并变量
  14. reduce_vars: true, // 提取成变量
  15. }
  16. })
  17. ]

什么叫合并变量?

  1. var a = 10;
  2. var b = 10;
  3. var c = a + b;
  4. // 可能合并成
  5. var c = 20;
  6. // 或者
  7. var c = 10 + 10;

什么是提取成变量?

  1. x = 'Hello'; y = 'Hello'
  2. // 转化成
  3. var a = 'Hello'; x = a; y = b

刚才提到一点:这里有一点,多进程的打包,不一定一定比单进程快,因为开启进程也有开销。
当程序规模比较小,本来打包就不算慢,这种情况下,可能还会降低打包速度,这就是因为多进程开销。这种多进程打包策略应用于,现在我们的打包速度遇到瓶颈了,可以试试这些技术看能不能优化。

构建优化:自动刷新&热更新

自动刷新可以借由devServer实现,这个肯定是开发环境用。
热更新和自动刷新是不一样的,区别在于:

  • 自动刷新相当于点击了刷新,状态会丢失;
  • 热更新状态不丢失;
    热更新需要使用的插件是:HotModuleReplacementPlugin
  1. const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
  2. // 1. entry中
  3. entry: {
  4. index: [
  5. 'webpack-dev-server/client?http://localhost:8080/',
  6. 'webpack/hot/dev-server',
  7. path.join(srcPath, 'index.js')
  8. ]
  9. }
  10. // 2. Plugins用起来
  11. plugins: [
  12. // ...,
  13. new HotModuleReplacementPlugin()
  14. ]
  15. // 3. devServer中hot: true
  16. devServer: {
  17. port: 8080,
  18. progress: true, // 显示打包进度
  19. contentBase: distPath, // 跟目录
  20. open: true, // 自动开启浏览器
  21. compress: true, // gzip压缩
  22. hot: true,
  23. proxy: {
  24. // ...
  25. }
  26. }

热更新需要设置一个监听范围和模块,若在模块之外的变化,不触发热更新?
热更新是替换网页代码的执行,不会改变状态。热更新原理,面试题。

构建优化:DllPlugin

我理解这个Dll没太大用途。就是把一些不常变化的东西打成dll,真正需要使用的时候把dll再拿过来使用。
DllPlugin负责打包过程,而DllReferencePlugin负责使用这些dll。

代码产出优化

目的是要达到:

  • 体积更小
  • 合理分包,不重复加载
  • 速度更快,内存使用少

小图片Base64

  1. rules: [
  2. {
  3. test: / \.(png|jpg|jpeg|gif)$/,
  4. use: 'file-loader'
  5. }
  6. ]

生产打包的时候,需要的不是file-loader,而是url-loader,可能需要将小于5kb的图片用base的url返回,然后写在html中,这样减少了一次http请求图片。

bundle加hash

xxx.[contentHash:8].js会根据内容算出hash值

懒加载

前文讲过

公共代码打包

见上文,公共代码抽离

IngorePlugin

见上文

mode:production

自带压缩代码
自动删除调试代码,比如一些warnning
自动开启Tree-Shaking

概念:Tree Shaking

Tree Shaking其实名字起的很形象。
就像你在大雪天去踹一棵树一样,原本不属于树而且没用的东西就会掉下来。

这里有一点需要注意,CommonJS不能生效,必须是ES Modeule的模块下Tree Shaking才会生效(ES Modeule静态引用,而Tree Shaking是静态分析,CommonJS执行的时候才引入,没法分析)。
mode:production,自动会开启Tree shaking。
// TODO:Tree Shaking 原理

概念:Scope Hosting

默认打包结果中,一个函数的个数没有优化,但是引擎里面作用域过多。
这个Scope Hosting的作用是把,可以合成而不影响功能的多个函数变成一个函数,这样作用域少,体积小。对js的执行和内存消耗方面都有所优化。

配置方式:

  1. const moudleConcatenationPulgin
  2. = require('webpack/lib/optimize/xxxfff')
  3. // plugins
  4. plugins: [
  5. new MoudleConcatenationPulgin()
  6. ]

另外,这个也是基于ES6 Moudlue,CommonJS会失效。有些第三方包,有js Es Moudle模块化语法的打包,也提供了不是js模块化所以,为了我们scope hosting尽量的做多一点优化,我们需要告诉webpack,打包的时候如果可以,去拿那些用了es6模块化的打包。
下面配置是告诉Webpack,如果第三方打包中存在es6的打包,就优先使用:

  1. resovle: {
  2. // jsnext:main ---- es6 模块化语法
  3. mainFields: ['jsnext:main', 'browser', 'main']
  4. }

这个顺序就是在第三方库的node_module里打包某个库的时候,不同类型的文件明,优先级被的问题

babel

巴别塔,babel,这个名字倒是很有点意思。
那是很早很早的时候,人类在经历了大洪水后存活了下来,变得不可一世,他们要在两河流域要建造一座塔。
这个塔计划非常宏伟,一旦建成了人们就能走到天上去。那时候人们的语言是相同的,所以建造的很快。
但是,上帝不想这样的事情发,认为自己的权威收到了威胁。上帝就让人们说了不同的语言,于是,人们因此彼此无法沟通,合作不下去了,最终这塔也就没建造起来。

所以,我们因为这个故事,现在有了一个语法转换器,babel。(官网:https://www.babeljs.cn/docs/
babel是EcmaScript语法之间的转换器。比如,把ES6代码翻译成ES5代码。
babel本身是一个空壳,它定义了流程,而具体转换的方法,都是通过各个plugin来实现的。

  • .babelrc
    这是babel的配置文件。
  • presets
    presets,预设,是常用的plugin的集合套件。代替了很多需要自己再写的plugin。最常用的就是:presets-env、presets-react。
  • polyfill
    补丁,就是提供某些环境下可能不存在的API。补丁是API层面的,并不是语法层面,presets可是语法层面的。
    比如,我想用Array.prototype.yxn这个方法,那么可能polyfill(babal-yxn-polyfill)可能这样写:
  1. // code in babal-yxn-polyfill
  2. if(!Array.prototype.yxn){
  3. Array.prototype.yxn = function () {
  4. // .....
  5. }
  6. }
  7. // 使用起来
  8. const x = [];
  9. x.yxn();
  • babel-polyfill
    babel-polyfill是core.js + regenerator.js(处理generator)的集合。 babel-polyfill已经被7.4弃用,直接推荐core.js和regenerator.js。
    另外,polyfill的转换,实际上,只是引入一下,并没有做工程化,也就是说,代码中只出现:
  1. require('@babel/polyfill')

当然,babel也可以按需引入:

  1. // .babelrc
  2. {
  3. "presets": [
  4. "@babel/preset-env",
  5. // 下面这段就是按需引入的配置
  6. {
  7. "useBuiltIns": "usage",
  8. "corejs": 3
  9. }
  10. ],
  11. plugins: []
  12. }

使用polyfill存在的一个比较重要的问题是,polyfill污染了全局环境。
当然,大多数情况下,这个点不是人们最关系的,污染了也没事,但是如果能不污染还是最好。
babel-plugin-transform-runtime插件就是来解决这个问题的。解决的办法是,用下划线定义这些补丁,具体可以参考官方文档:
https://www.babeljs.cn/docs/babel-plugin-transform-runtime

前端为什么要工程化问题要点?

要点:

  1. 代码相关的方向
  • 体积小,加载快
  • 编译高级语言和语法
  • 兼容性,错误检查等等
  1. 研发流程方面 (这点比较进阶)
  • 统一高效的开发环境
  • 统一的构建流程和产出标准
  • 集成公司的规范