webpack:进阶配置及优化

356 阅读2分钟

进阶用法

另一份笔记,基于webpack4留作查询

1.自动清理构建目录

  1. 做法一:在npm scripts中修改命令:rm -rf ./dist && webpackrimraf ./dist && webpack
  2. 做法二:使用clean-webpack-plugin,默认会删除指定的输出目录

2.PostCSS插件autoprefixer自动补齐

适配多内核浏览器,CSS需补齐前缀。

autoprefixer是一个CSS后置处理插件(less、sass是预处理,指在打包前处理,而autoprefixer是在压缩之后,再进行处理)

PostCSS功能较全,除了补齐,还可以支持CSS Modulesstyle Lint等。

3.移动端px rem转换

使用px2rem-loader

。。。。。。。。。。。。没有看,后续做

4.静态资源内联

(1)资源内联的好处

  • 代码层面

    • 页面框架的初始化脚本(计算……)
    • 上报相关打点(page start、css初始化、js初始化、js加载完成)
    • css内联避免页面重新渲染导致页面闪动
  • 请求层面

    • 减少网络请求数量(url-loader将小的图片/字体等内联到网页里面去)

(2)HTML和JS的内联

raw-loader:

  • HTML:<script>${require('raw-loader!babel-loader!./meta.html')}</script>
  • JS:<script>${require('raw-loader!babel-loader!../node_modules/lib-flexible')}</script>

注:以上加babel-loader是为了在内联之前解析ES6语法并翻译成ES5

(3)CSS内联

  • 借助style-loader

    module.exports={
    ...
    module:{
    rules:[
    {
    test:/.scss$/,
    use:[
    {
    loader:'style-loader',
    options:{
    insertAt:'top',//将样式插入到<head>标签中
    singleton:true//是否将所有style标签合并
    },
               'css-loader',
               'sass-loader'
    }
    ]
    }
    ]
    }
     ...
    }
    ​
    
  • 常用:html-inline-css-webpack-plugin

5.多页面打包方案

多页面应用(MPA):每次页面跳转时,web服务器都会返回一个新的html文档。

(1)多页面打包思路

  • 每个页面对应一个entry,一个html-webpack-plugin。缺点:每次新增或删除页面都会需要修改webpack配置(增删entry)。

  • 动态获取entry并设置html-webpack-plugin的数量。➡️利用glob.sync设置通用entry写法

    glob库的设计实现类似于linux的文件通配

    const glob = require('glob');
    ...
    const setMPA = () => {
       const entry = {};
       const HtmlWebpackPlugins = [];
    ​
       const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))
       Object.keys(entryFiles).map((index) => {
           const entryFile = entryFiles[index];
           const match = entryFile.match(/src/(.*)/index.js/);
           const pageName = match && match[1];
           entry[pageName] = entryFile;
           HtmlWebpackPlugins.push(new HtmlWebpackPlugin({
               template: path.join(__dirname, `src/${pageName}/index.html`),
               filename`${pageName}.html`,
               chunks: [pageName],
               injecttrue,
               minify: {
                   html5true,
                   collapseWhitesapcetrue,
                   preserveLineBreaksfalse,
                   minifyCSStrue,
                   minifyJStrue,
                   removeComments:false
              }
          }))
      })
    ​
       console.log('entryFiles',entryFiles);
       return {
           entry,
           HtmlWebpackPlugins
      }
    }
    ​
    const { entry,HtmlWebpackPlugins}=setMPA();
    ​
    module.exports={
     entry: entry,
    ...
    plugins:[
       ...
    ].concat(HtmlWebpackPlugins),
    }
    ​
    

6.代码转换的位置信息source map

阮一峰source map

简单说,Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。

(1)webpack中source map关键字

  • eval:使用eval包裹模块代码,每个代码块后面会有一个source map文件的URL,已确定指定的source map文件
  • source map:产生的.map文件,浏览器开启后可调试
  • cheap:不包含列信息,定位不到行列信息
  • inline:将.map作为DataURI嵌入,不单独生成文件,内联到打包文件内
  • module:包含loader的source map

(2)source map类型

webpack.docschina.org/configurati…

devtool首次构建速度二次构建是否适合生产化境可以定位的代码
(none) 默认++++++最终输出代码
eval++++++webpack生成的代码(一个个模块)
cheap-module-eval-source-mapo++源代码(只能看到行)
cheap-eval-source-map+++经过loader转换后的代码(只能看到行)
cheap-module-source-mapo-源代码(只能看到行)
cheap-source-map+o经过loader转换后的代码(只能看到行)
inline-cheap-module-source-mapo-源代码(只能看到行)
inline-cheap-source-map+o经过loader转换后的代码(只能看到行)
inline-source-map----源代码
hidden-source-map----源代码
source-map----源代码
eval-source-map--+源代码

7.提取页面公共资源

(1)基础库分离

  • 将基础库(例如react、react-dom)通过cdn引入,不打入bundle中。

    使用html-webpack-externals-plugin。

    :该库(3.8.0)与html-webpack-plugin(3.2.0)有冲突,使用--force安装

    cdn引入react、react-dom要在body里引入,否则渲染之前找不到js

  • 利用splitChunksPlugin进行公共脚本分离

    webpack4内置,替代CommonsChunkPlugin插件。

    配置

    module.exports={
    optimization:{
       splitChunks:{
         chunks:'async',
         //设置包的大小
         minSize:30000,
         maxSize:0,
         minChunks:1,//当引用次数大于1时,提取公共
         maxAsyncRequests:5,//最多同时请求的异步资源的数量
         maxInitialRequest:3,
         automaticNameLimiter:'~',
         name:true,
         cacheGroups:{
           vendors:{
             //匹配需要分离出的部分
             test:/[\/]node_modules[\/]/
             priority:-10
          }
        }
      }
    }
    }
    

    chunks选项说明:

    • async:(默认)异步引入的库进行分离
    • Initinal:同步引入的库进行分离
    • all:(推荐)所有引入的库进行分离
  • splitChunksPlugin分离基础包

    module.exports={
    optimization:{
    splitChunks:{
    cacheGroups:{
    commons:{
             //匹配分离出来的库
    test:/(react|react-dom)/,
             //命名为‘vendors’
    name:'vendors',
    chunks:'all'
    }
    }
    }
    }
    }
    
  • 提取页面公共文件

    module.exports={
    optimization:{
    splitChunks:{
    //设置包的大小
    minSize:0,
    cacheGroups:{
    commons:{
             //命名为‘vendors’
    name:'commons',
    chunks:'all',
    //设置最小引用次数为2
    minChunks:2
    }
    }
    }
    }
    }
    

8.tree shaking摇树优化

(1)概念

一个模块有多个方法,当一个方法被使用时,整个文件都会被打包进bundle里去,tree shaking就是把用到的方法打到bundle里,不用的方法会在uglify阶段被擦除掉。

(2)使用

webpack默认支持;modeproduction时默认开启;.babelrc文件中的modulesfalse即可

(3)条件

必须是ES6语法,CJS不支持。

(4)原理

利用ES6模块的特点:

  • import、export只能作为模块的顶层语句出现
  • import的模块名只能是字符常量(避免了动态设置的出现)
  • import binding是immutable的(无法修改)

实际上是对代码的静态分析,不可以有不确定的代码

(5)其他概念

DCE(Dead code elimination

死码消除。

死码消除的范围:

  • 不会执行到,不可到达

    例如:

    if(false){……}
    
  • 代码的结果未被用到

  • 代码只影响了死变量(只读不写)

9.Scope Hoisting

现象

构建后代码存在大量闭包代码

(1)模块转换

模块转换为模块初始化函数。

结论:被webpack转换后的模块会带上一层包裹;import会被转化为__webpack_require

(2)模块机制

示例:

image.png

  • 打包出来是一个IIFE(匿名闭包)
  • modules是一个数组,每一项是一个模块初始化函数
  • __webpack_require用来加载模块,返回module.exports
  • 通过WEBPACK_REQUIRE_METHOD(0)启动程序

(3)原理

将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当重命名一些变量以防止变量名冲突

通过scope hoisting可以减少函数声明代码和内存开销

(4)使用

在webpack mode 为production时默认开启(实际上是开启了一个内置插件:webpack.optimize.ModuleConcatenationPlugin)

10.代码分割&&动态import

(1)场景

大型web项目如果所有文件都放在一个文件里会有太高的消耗,因此最好将代码库分割成chunks(语块),当代码运行到需要他们时再进行加载。

举例:

  • 抽离相同代码到一个共享块
  • js懒加载,使得初始下载的代码更小

(2)js懒加载

1-方式

  1. CommonJS:require.ensure
  2. ES6:动态import(目前还没有原生支持,需要babel转换)

2-使用

  1. 安装babel插件:npm install @babel/plugin-syntax-dynamic-import --save-dev
  2. 配置babel插件:plugins:[ ... @babel/plugin-syntax-dynamic-import, ... ]

11.ESLint

规范实践:

大团队最好制定规则,示例:

截屏2022-03-24 上午9.33.55.png

  • 不重复造轮子,基于eslint:recommend配置并改进
  • 能够帮助发现代码错误的规则并全部开启
  • 在不限制开发体验的前提下,统一代码风格

(1)落地

1-与CI/CD集成

截屏2022-03-24 上午9.52.57.png

2-与webpack集成

使用eslint-loader,构建时检查。

配置:

module.exports={
...
module: {
   rules: [
    {
       test: /.js$/,
       use: [
         "babel-loader",
         "eslint-loader"
      ]
    }
  ]
}
 ...
}

.eslintrc.js(依照个人的习惯设置、修改规则):

module.exports={
   "parser":'babel-eslint',
   "extends":"airbnb",
   "env": {
       "browser"true,
       "node"true
  },
   "rules":{
       "indent": ["error"2]
  }
}

12.打包组件和库

(1)步骤

  1. 实现组件
  2. 设置打包(是否压缩,min版需压缩)
  3. 编写入口文件index.js
  4. 修改package.json(有时需要)

(2)示例

const path=require('path');
const TerserPlugin=require('terser-webpack-plugin')
module.exports={
   mode:'none',
   entry:{
       'myView':'./src/index.js',
       'myView.min':'./src/index.js'
  },
   output:{
       path: path.resolve(__dirname, 'dist/assets'),
       filename:'[name].js',
       library'myView',
       libraryTarget:'umd',
     //简化调用
       libraryExport:'default'
  },
   optimization:{
     //是否压缩
       minimize:true,
     //压缩配置
       minimizer:[
           new TerserPlugin({
               include:/.min.js$/
          })
      ]
  }
}

说明:

  • libraryTarget:'umd'是为了在不同包管理模式下可用
  • 使用TerserPlugin可以保证在ES6下可兼容,注意这个包的版本语法变化
  • 入口文件index.js主要是用于区分环境加载版本
  • 可以设置prepublic等钩子做相应操作

13.SSR打包

SSR的核心就是减少请求数。

渲染:HTML+JS+CSS+Data => 渲染后页面

服务端渲染的优势:

  • 资源都在服务端,可并行加载(相对的客户端只能串行加载)
  • 内网资源,拉数据更快
  • 合并后可减少请求,一个HTML返回所有

优点:

  • 减少白屏时间
  • SEO友好

(1)实现思路

服务端:

  • 使用react-dom/server的renderToString方法将react组件渲染成字符串
  • 服务端路由返回对应模板

客户端:

打包出针对服务端的组件

(2)客户端模板实现

  • 编写对应的webpack.ssr.js:入口文件更改;输出文件更改后缀;libraryTarget需要是umd
  • 源代码的服务端适配(解决问题:ESM规范不可用;nodejs中没有浏览器全局变量document、window;nodejs无法解析css)

(3)问题解决方案

  • 浏览器全局变量:

    • 将不兼容的组件依据打包环境适配
    • 将Fetch、ajax的请求改写为isomorphic-fetch或者axios
  • 样式问题:

    • 将style-loader替换为isomorphic-style-loader(会改变编写习惯,不推荐)
    • 服务端打包通过ignore-loader忽略css的解析
    • 使用原来打包出来的浏览器端html为模板,设置占位符,动态插入组件

(4)首屏数据处理

服务端处理数据,替换占位符。处理方式与模板相同,数据导入需要使用script标签包裹

14.优化构建时命令行的显示日志

(1)统计信息stats

PresetAlternativeDescription
"errors-only"none仅发生错误时输出
"minimal"none只在发生错误或有新的编译时输出
"none"false没有输出
"normal"true标准输出
"verbose"none全部输出

默认全输出

(2)配置

在最外层/devServer添加

stats:'errors-only'

(3)利用插件优化

使用friendly-errors-webpack-pluginplugins添加插件即可(需设置statserrors-only)

  • success:构建成功的日志提示
  • warning:构建警告的日志提示
  • error:构建报错的日志提示

15.构建异常与中断处理

查看构建错误码:构建完成后输入echo $?

(1)构建异常的处理

nodejs的process.exit规范

  • 0表示成功完成,回调函数中,err值为null
  • 非0表示执行失败,回调函数中,err不为null,err.code就是传给exit的数字。

(2)构建异常的捕获

compiler在每次构建结束后会触发done这个hook

process.exit主动处理构建报错

plugins:[
function(){
   //this对象即是compiler
this.hooks.done.tap('done',(stats)=>{
if(stats.compilation.errors&&stats.compilation.errors.length&&process.argv.indexOf('--watch')==-1){
console.error('build error')
       process.exit(1)
}
})
}
]