1

1.增强CSS的解析功能
2.tree shaking
3.scope hoisting
4.如何用webpack打包组件库或工具库
5.如何用webpack做server render
6.如何在webpack里面做prerender


自动清理构建目录

当前构建时的问题
每次构建的时候不会清理目录,造成构建的输出目录output文件越来越多。

解决方案:
1.通过npm scripts清理构建目录
rm -rf ./dist && webpack
rimref ./dist && webpack
2.自动清理构建目录
避免构建前每次都需要手动删除dist
使用clean-webpack-plugin,默认会删除output指定的输出目录

module.exports = {
  plugis: [new CleanWebpackPluginn()]
}

PostCSS插件autopredfixer自动补充CSS3前缀

css3的属性为什么需要前缀?
Trident(-ms) Geko(-moz-) Webkit(-webkt) Presto(-o)

举个例子

.box{
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
  -o-border-radius: 10px;
  border-radius: 10px;
}

如何在编写Css不需要添加前缀?
使用autoprefixer自动补全css3前缀
使用autoprefixer插件

module.exports = {
  module: {
    rules: [
      {test: /\.less$/, use: [
        'style-loader',
        'css-loader',
        'less-loader',
         {
            loader: 'postcss-loader',
            options: {
              plugins: () => {
                require('autoprefixer')({
                  browsers: ['last 2 version', "> 1%", "IOS 7"]
                })
              }
            }
         }
      ]}
    ]
  }
}

自动转换rem

浏览器的分辨率不同(移动设备流行之后,不同设备分辨率不同,特别是ios),需要前端进行大量的适配。
1.CSS媒体查询实现响应式布局
缺陷:需要写多套适配样式代码

@media screen and (max-width: 980px) {
 .header {
    width: 900px;
  }
}
@media screen and (max-width: 480px) {
 .header {
    width: 400px;
  }
}
@media screen and (max-width: 350px) {
 .header {
    width: 300px;
  }
}

rem是什么
W3C对rem的定义:font-size of the root element
rem和px的对比:
rem是相对单位
px是绝对单位

1.使用px2rem-loader将px转成rem
2.动态计算1rem等于多少px(font-size的值)
可以使用手淘的lib-flexible库

module.exports = {
  module: {
    rules: [
      {test: /\.less$/, use: ['style-loader','css-laoder', 'less-loader', 
      {loader: 'px2rem-loader', 
       options: {
         remUnit: 75, 
         remPrecision: 8
       }
      }
     ]}
    ]
  }
}

资源内联

代码层面:

  1. 页面框架的初始化脚本
    在上面自动转rem的例子中,如果我们想要引入lib-flexible库,需要手动在index.html中引入代码,因为这段代码需要在页面加载之前执行
  2. 上报相关打点
    css初始化和加载完成,js初始化和加载完成等等一些上报点的代码都需要内联到html去,而不能引入到脚本里面去。
  3. css内联避免页面闪动
    将首屏的css内联到页面里面去

请求层面:减少HTTP网络请求数
小图片或字体则内联

HTML和JS内联

raw-loader使用0.5.2的版本

<!DOCTYPE html>
<html lang="en">
  <head>
    <%=require('raw-loder!./meta.html')%>
    <title>Document</title>
    <script>
      <%=require('raw-loder!babel-loader!../node_modules/lib-flexible/flexible.js')%>
    </script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

raw-loader内联html

<script>${require('raw-loader!babel-loader!./meta.html')}</script>

raw-loader内联JS
我们书写的JS脚本里面可能包含es6等高级语法,此时除了raw-loader之外还需要babel-loader,内联之前对他进行转换

<script>${require('raw-loader!babel-loader!../node_modules/lib-flexible')}</script>

CSS内联

方案一:借助style-loader,给他设置一个参数:singleton

module.exports = {
  module: {
    rules: [
      {test: /\.scss$/,
       use: [
         {
           loader: 'style-loader',
           options: {
             insertAt: 'top', // 将样式插入到<head>
             singleton: true, // 将所有的style标签合并成一个
           }
         }
       ]}
    ]
  }
}

方案二:html-inline-css-webpack-plugin
html-webpack-plugin默认使用的是ejs模板引擎,所以可以直接使用花括号的语法(不同版本会有差异)


多页面应用概念

多页面发布上线之后有多个入口,一个页面就是一个业务。
多页面应用的优势,每个页面之间是解耦的,且对SEO更加友好。每一次页面跳转的时候,后台服务器都会返回一个新的html文档,这种类型的网站也就是多页网站,也叫做多页应用。

多页面打包基本思路

每个页面对应一个entry,一个html-webpack-plugin
缺点:每次新增或删除页面需要改webpack配置

module.exports = {
  entry: {
    index: './src/index.js', // 首页
    search: './src/search.js' // 搜索页
  }
}

多页面打包通用方案

动态获取entry和设置html-webpac-plugin数量,但是这里需要一个约定(如果入口都放在src目录下)

module.exports = {
  entry: {
    index: './src/index/index.js',
    search: './src/search/index.js'
  }
}

利用glob.sync
npm i glob -D

entry: glob.sync(path.join(__dirname, './src/*/index.js'))

Source Map

source map是开发利器,在打包的时候会将源代码打包成bundle文件,bundle文件就是经过loader转换和插件处理了的,最后会生成一个大的js文件,这个文件在最后调试的时候是没办法的。
作用:通过source map定位到源代码

开发环境开启,线上环境关闭
线上排查问题的时候可以将sourcemap上传到错误加农系统

source map关键字

module.exports = {
  devtool: 'source-map'
}

eval:使用eval包裹模块代码
source map:产生.map文件
cheap:错误不包含列信息
inline:将.map作为DataURI嵌入,不单独生成.map文件内(map文件内联到js文件中去)
module:包含loader的sourcemap

source map类型

devtool首次构建二次构建是否适合生产环境可以定位的代码
{none}++++++yes最终输出的代码
eval++++++nowebpac生成的代码(一个个的模块)
cheap-eval-source-map+++no经过loader转换后的代码(只能看到行)
cheap-module-eval-source-map+++no源代码(只能看到行)
eval-source-map--+no源代码
cheap-source-map+oyes经过loader转换后的代码(只能看到行)
cheap-module-source-mapo-yes源代码(只能看到行)
inline-cheap-source-map+ono经过loader转换后的代码(只能看到行)
source-map----yes源代码
inline-source-map----no源代码
hidden-source-map----yes源代码

提取页面公共资源

页面之间存在公共模块,且他们使用的基础库是一样的,如果打包的适合把公共模块都单独打一份,基础库也单独打一份,打包的体积会非常大,所以需要进行基础库的分离和公共模块的分离。

基础库分离

思路:将react、react-dom基础包通过cdn引入,不打入bundle中
方法:使用html-webapck-externals-plugin

const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')

plugins: [
  new HtmlWebpackExternalsPlugin({
    externals: [
      {
         module: 'react',
         entry: '//11.url.cn/now/lib/15.1.0/react-with-addons.min.js?_bid=3123',
         global: 'React',
      },
      {
         module: 'react-dom',
         entry: '//11.url.cn/now/lib/15.1.0/react-dom.min.js?_bid=3123',
         global: 'ReactDOM',
      }
    ]
  })
]

以上方法,将把react和react-dom从cdn引入,不会打包进来

利用SplitChunksPlugin进行公共脚本分离

webpack4内置的,替代CommonsChunkPlugin插件
chunks参数说明:

  • async 异步引入的库进行分离(默认)
  • initial 同步引入的库进行分离
  • all 所有引入的库进行分离(推荐)
module.exports = {
  ...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequest: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test://,
          priority: -10
        }
      }
    }
  }
}

test:匹配出需要分离的包

optimization: {
  splitChunks: {
    cacheGroups: {
      commons: {
        test: /(react|react-dom)/,
        name: 'vendors',
        chunks: 'all'
      }
    }
  }
}

此时会把react和react-dom提取出来名字为vendors

利用SplitChunksPlugin分离页面公共文件

minChunks:设置最小引用次数为2次(被引用2次则提取出来)
minuSize:分离的包体积的大小(文件大小超过则提取)

module.exports = {
  optimization: {
    splitChunks:  {
      minSize: 0.
      cacheGroups: {
        commons: {
          name: 'commons',
          chunks:'all',
          minChunks: 2,
        }
      }
    }
  }
}

Three Shaking(摇树优化)

概念:1个模块可能有多个方法,只要其中的方法使用到了,则整个文件都会被打到bundle里面去,tree shaking就是只把用到的方法打入bundle,没用到的方法会在uglify阶段被擦除掉。

使用:webpack默认支持,在.babelrc里设置modules: false即可,production mode的情况下默认开启
要求:必须是ES6的语法(import),CJS的方式不支持

DCE(Elimination)

代码不会被执行,不可到达

if(false){
  console.log('这段代码永远不会执行')
}

代码执行的结果不会被用到
代码只会影响死变量(只写不读)

Three-Shaking原理

利用ES6模块的特点:

  • 只能作为模块顶层的语句出现
  • import的模块名只能是字符串常量
  • import bunding是immutable的
    代码擦除:uglify阶段删除无用代码

Scope Hoisting使用和原理分析

现象:构建后的代码存在大量闭包代码

会导致什么问题:

  • 大量函数闭包包裹代码,导致体积增大(模块越多)
  • 运行代码时创建的函数作用域变多,内存开销变大

模块转换分析:
模块(index.js)

import {helloWorld} from './helloworld'
import '../../common'

document.write(helloWorld())

模块初始化函数(webpack打包后)

/* 0 */
/***/ (function(module,__webpack_exports__, __webpack_requie__) {
  'use strict';
  __webpack_require__.r(__webpack_exports__);
  /* harmony import */ var _common_WEBPACK_IMPORTED_MODULE_0)) = __webpack_require__(1);
/* harmoney */ var _helloworld_WEPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);

document.write(Object(__helloworld_WEBPACK_IMPORTED_MODULE_1__['helloworld'])())
}

结论:

  • 被webpack转换后的模块会带上一层包裹
  • import会被转换成__webpack_require

进一步分析webpack的模块机制

(function(modules) {
  var installModules = {}
  
  function __webpack_require__(moduleId) {
    if (installModules[moduleId]) 
      return installModules[moduleId].exports
    var module = installModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {},
    }
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.1 = true
    return module.exports;
  }
  __webpack_require__(0);
})([
  /* 0 module */
  (function (module, __webpack_exports__, __webpack_require__) {
    ...
  },
  /* 1 module */
  (function (module, __webpack_exports__, __webpack_require__) {
    ...
  },
  /* n module */
  (function (module, __webpack_exports__, __webpack_require__) {
    ...
  },
])

分析:

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

scope hoisting原理

原理:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
对比:通过scope hoisting可以减少函数声明代码和内存开销(将代码内联进来,同一个作用域下面,如果有多个变量,会导致命名冲突,会给这些适当的重命名。这样就减少了内存的开销)

scope hoisting使用

webpack mode为production默认开启
必须是ES6语法,CJS(动态引入模块)不支持

module.exportss = {
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
}

image.png

总结:scope hoisting是把每个模块被webpack处理成的模块初始化函数整理到一个统一的包裹函数里,也就是把多个作用域用一个作用域取代,以减少内存消耗并减少包裹块代码,从每个模块有一个包裹函数变成只有一个包裹函数包裹所有的模块,但是有一个前提就是,当模块的引用次数大于1时,这个效果就会无效


代码分割的意义

对于大的web应用来将,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊的时候才会被使用到。webpack有一个功能就是将你的代码库分割成chunks(语块)当代码运行到需要他们的时候再加载。

适用的场景:

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

懒加载JS脚本的方式
CommonJS:require.ensure(webpack提供的)
ES6:动态import(目前还没有原生支持,需要babel转换)

如何使用动态Import?
安装Babel

npm i @babel/plugin-syntax-dynamic-import --save-dev

ES6:动态import(目前还没有原生支持,需要babel转换)
.babelrc

{
  plugins: ['@babel/plugin-syntax-dynamic-import']
}
loadComponent() {
  import('./text.js').then((txt) => {
    ...
  })
}

懒加载的脚本'text.js'会单独打包成一个js文件image.png


在webpack中使用eslint

行业里优秀的eslint规范实践
Airbnb:eslint-config-airbnb、eslint-config-airbnb-base

制定团队的eslint规范

遵循原则:

  1. 不要重复造轮子,基于eslint:recommend配置并改进
  2. 能够帮助发现代码错误的规则,全部开启
  3. 帮助保持团队的代码风格统一,而不是限制开发体验。

image.png

Eslint如何执行落地?

  1. 和CI/CD系统集成
  2. 和webpack集成

本地开发阶段增加precommit钩子
安装husky
npm i husky --save-dev
增加npm script,通过lint-staged增量检查修改的文件

"scripts": {
  "precommit": "lint-staged"
},
"lint-staged": {
  "linters": {
    "*.{js,scss}": ["eslint --fix" "git add"]
  }
}

webpack与eslint集成
使用eslint-loader,构建时检查js规范

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

新增.eslintrc.js文件

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

webpack打包库和组件

webpack除了可以用来打包应用,也可以用来打包js库

实现一个大整数加法库的打包

  1. 需要打包压缩版和非压缩版
  2. 支持AMD/CJS/ESM模块引入

打包输出的库名称:

  • 未压缩版 large-number.js
  • 压缩版 large-number.min.js
    image.png

支持的使用方式
支持 ES module

import * as largeNumber from 'large-number'
// ...
largeNumber.add('999', '1')

支持CJS

const largeNumbers = require('large-number')
// ...
largeNumbers.add('999', '1')

支持AMD

require(['large-number'], function(large-number){
 // ...
 largeNumber.add('999','1')
})

如何将库暴露出去?
library:指定库的全局变量
librarTarget:支持库引入的方式

module.exports = {
  mode: 'production',
  entry: {
    'large-number': './src/index.js',
    'large-number.min': './src/index.js
  },
  output: {
    filename: '[name].js',
    library: 'largeNumber',
    libraryExport: 'default',
    libraryTarget: 'umd',
  }
}

如何只对.min压缩?
通过include设置只压缩min.js结尾的文件

module.exports = {
  mode: 'none',
  entry: {
    'large-number': './src/index.js',
    'large-number.min': './src/index.js'
  },
  output: {
    filename: '[name].js',
    library: 'largeNumber',
    libraryTarget: 'umd'
  },
  optimization: {
    minimize: true,
    minimizer: [
       new TerserPlugin({
         include: /\.min\.js$/
       })
    ]
  }
}

以上使用TerserPlugin是因为我们的源码使用的是es6的语法,如果使用uglifyPlugin压缩的时候遇到es6的语法会报错

设置package.json的main为'index.js'
然后在index.js中module.exports = require('dist/large-number.min.js')


Webpack实现ssr打包

页面打开过程
当我们点开一个链接,会加载一个新的WebView,加载完新的WebView之后会加载一个新的页面,这个时候页面是一个白屏的状态,请求还没有到达html这一步。然后进行html加载,html加载成功后,会有一个loading态,告诉用户页面正在加载。然后浏览器请求和解析css、js,在js中可能会加载数据,和渲染数据。而ssr就是为了优化白屏时间。

服务端渲染(SSR)是什么?

  • 所有模板等资源都存储在服务端
  • 内网及其拉取数据更快
  • 一个HTML返回所有数据

客户端渲染的时候,他的html、css、js都是串行加载的一个过程。
而服务端渲染的时候所有的模板资源,包括js、css等都是存放在服务端,这时候可以把多个请求数量优化成一个,另外一点就是,客户端渲染的时候依赖于用户本地设备的网络情况,服务端渲染的时候css、js都是在内网的机器,拉取的数据更快。
渲染:HTML+CSS+JS+DATA -> 渲染后的HTML
image.png
非首屏的css和js可以通过异步加载的形式

客户端渲染vs服务端渲染

客户端渲染服务端渲染
请求多个请求(HTML,数据等)1个请求
加载过程HTML&数据串行加载1个请求返回HTML&数据
渲染前端渲染服务端渲染
可交互图片等静态资源加载完成,JS逻辑执行完成可交互图片等静态资源加载完成,JS逻辑执行完成可交互

总结:服务端渲染(SSR)的核心就是减少请求

SSR的优势

  1. 减少白屏时间
  2. 对于SEO友好

SSR代码实现思路
服务端

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

客户端

  1. 打包出针对服务端的组件

webpack ssr打包存在的问题
浏览器的全局变量(Node.js中没有document,window)

  • 组件适配:将不兼容的组件根据打包环境进行配置
  • 请求适配:将fetch或者ajax发送请求的方法改写成isomorphic-fetch或者axios

样式问题(Node.js无法解析css)
方案一:服务端打包通过ignore-loader忽略掉css解析
方案二:将style-loader替换成isomorphic-style-loader

服务端打包如果忽略掉样式,那么如何解决样式不显示的问题?
使用打包出来的浏览器端html为模板
设置占位符,动态插入组件
image.png

首屏数据如何处理?
服务端获取数据
替换占位符
image.png
把数据挂载在window对象上


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

image.png
image.png

  1. 在配置文件中设置stats
module.exports = {
  stats: 'errors-only'
}
  1. 使用friendly-errors-webpack-plugin
    success: 构建成功的日志提示
    warning:构建警告的日志提示
    error:构建报错的日志提示
module.exports = {
  plugins: [
    new FriendlyErrorsWebpacPlugin()
  ]
}

image.png

如何判断构建是否成功?
在CI/CD的pipline或者发布系统需要知道当前构建状态
每次构建完成后输入echo $?获取错误码

构建异常和中断处理

需要捕获报错,然后通过procee.exit将错误码抛出
webpack4之前的版本构建失败不会抛出错误码(error code)
Node.js中的process.exit规范

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

如何主动捕获错误并处理构建错误?
通过webpack里面比较重要的一个对象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.log('build error')
        process.exit(1)
      }
    })
  }
]

fhkd
9 声望0 粉丝