月总结

130 阅读19分钟

bjh.fe.oneMonth.summary

开发上线流程

开发

1.创建你的分支 2.点击前往pipe,找到流水线点击BranchPipeLine 3.复制你的分支号,本地拉取你的分支------ > git pull ; git checkout -b 本地分支名 origin/远程分支名 4.coding

代码提交

1.找PM要icafe卡片 2.创建 story 子卡片 3.git add. 4.git commit -m{这里一定要包含一个 icafe-id(story 的)} 5.git push origin HEAD:refs/for/你的分支 6.找人看rc合入

提测

1.agile 2.选择集成流水线 3.表单填写 4.提测--是否免测由qa决定

上线

1.Tower 2.表单填写 3.到预览机让pm走查 4.继续上线直至完成

st=>start: 开始
e=>end: 结束
op1=>operation: 需求评审	
op2=>operation: 技术评审
op3=>operation: 开发
op4=>operation: 联调
op5=>operation: PM,UE走查
op6=>operation: 提测
op7=>operation: 上线
cond=>condition: 通过 或者 不通过?
st->op1->op2->op3->op4->op5->op6->cond
cond(yes)->op7
cond(no)->op3

大文件上传

数据流图

图片 图片

  • beforeFileQueued--文件被加入上传队列之前

  • 抽帧形成封面图片

getCoverPics(target) { // target类型: file:上传本地视频文件,string:视频编辑器,直接传入视频地址
        let url = '';
        if(typeof target === 'string') {
            url = target;
        } else {//把文件类型转化为字符串地址类型,兼容本地播放
            const localFile = target && target.source && target.source.source;
            url = window.URL.createObjectURL(new Blob([localFile], {type: 'video/mp4'}));
        }
        if(url) {
            const canvas = document.createElement('canvas');
            const video =  document.createElement('video');
            video.src = url;自动播放开启
            let gap,coverNum;
            video.autoplay = true;//自动播放开启//
            video.loop = false;
            //onloadedmetadata 事件在指定视频/音频(audio/video)的元数据加载后触发。
            //元数据包含: 时长,尺寸大小(视频),文本轨道。
            //onloadedmetadata 事件在指定视频/音频(audio/video)的元数据加载后触发。
            //元数据包含: 时长,尺寸大小(视频),文本轨道。分
            video.onloadedmetadata = () => {
                gap = Math.floor(video.duration/17) || 1 ;//分帧处理点
                if(video.videoWidth !== 0) { // 兼容处理可播放但只有声音没有画面的视频
                    if (video.videoWidth >= commonData.coverSizeRule.video.width && video.videoHeight >= commonData.coverSizeRule.video.height) {
                        this.needCoverRcmd = true;
                    } else {
                        this.needCoverRcmd = false;
                    }
                    this.props.inspection.needCoverRcmd = this.needCoverRcmd;
                    this.clearCoverImageInterval(); // 兼容处理interval可能调用的问题
                    gap = Math.floor(video.duration/17) || 1 ;
                    coverNum = video.duration >= 17 ? 17 : Math.floor(video.duration);
               };
                video.onplay = () => {
                    video.muted = true;//静音开启//
                    let imgListValue = {value: [], warn: '正在处理,暂时无法从视频中截图', coverSource: 'frontend'};
                    this.canDrawFrame = true;静音开启
                   this.props.updateImageList && this.props.updateImageList(imgListValue);
                    this.createPromise(canvas,video,0,gap, coverNum, []);

                    // 前端抽帧打点
                    window.MP && window.MP.stats && window.MP.stats({ // eslint-disable-line
                        urlkey: 'custom-视频—前端抽帧pv/uv'
                    });
                };
            }
        }

图片 图片 步骤总结 1.vedio的onloadmetedata事件中进行抽帧时间的抓取 2.vedio的onplay事件中进行抽帧 3.通过设置vedio.currentTime事件触发移动到指定抽帧点,触发onseeked事件,此时生成canvas对象, 转成base64地址发送给后端

  • fileQueued--文件被插入到上传队列时

图片

  • upload before-send-file 图片
  • upload before-send 图片
  • upload after-send-file 图片 轮询查码状态 图片 转码完成后status变为transcodeSuccess,使用转码后的视频连接进行播放 图片
  • 回顾大文件上传 1.业务中的大文件切片是用sdk来实现的,我们如何实现一个大文件的切片呢?

在JavaScript中,文件FIle对象是Blob对象的子类,Blob对象包含一个重要的方法slice,通过这个方法,我们就可以对二进制文件进行拆分。

function slice(file, piece = 1024 * 1024 * 5) {
  let totalSize = file.size; // 文件总大小
  let start = 0; // 每次上传的开始字节
  let end = start + piece; // 每次上传的结尾字节
  let chunks = []
  while (start < totalSize) {
    // 根据长度截取每次需要上传的数据
    // File对象继承自Blob对象,因此包含slice方法
    let blob = file.slice(start, end); 
    chunks.push(blob)

    start = end;
    end = start + piece;
  }
  return chunks
}

2.如何将多个切片还原成一个文件?

  • 确认所有切片都已上传,这个可以通过客户端在切片全部上传后调用mkfile接口来通知服务端进行拼接
  • 找到同一个context下的所有切片,确认每个切片的顺序,这个可以在每个切片上标记一个位置索引值
  • 按顺序拼接切片,还原成文件
// 获取context,同一个文件会返回相同的值
function createContext(file) {
 	return file.name + file.length
}

let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH);

// 获取对于同一个文件,获取其的context
let context = createContext(file);

let tasks = [];
chunks.forEach((chunk, index) => {
  let fd = new FormData();
  fd.append("file", chunk);
  // 传递context
  fd.append("context", context);
  // 传递切片索引值
  fd.append("chunk", index + 1);
	
  tasks.push(post("/mkblk.php", fd));
});
// 所有切片上传完毕后,调用mkfile接口
Promise.all(tasks).then(res => {
  let fd = new FormData();
  fd.append("context", context);
  fd.append("chunks", chunks.length);
  post("/mkfile.php", fd).then(res => {
    console.log(res);
  });
});

3.断点续传

  • 在切片上传成功后,保存已上传的切片信息
  • 当下次传输相同文件时,遍历切片列表,只选择未上传的切片进行上传
  • 所有切片上传完毕后,再调用mkfile接口通知服务端进行文件合并问题就落在了如何保存已上传切片的信息了,保存一般有两种策略:
  1. 可以通过locaStorage等方式保存在前端浏览器中,这种方式不依赖于服务端,实现起来也比较方便,缺点在于如果用户清除了本地文件,会导致上传记录丢失
  2. 服务端本身知道哪些切片已经上传,因此可以由服务端额外提供一个根据文件context查询已上传切片的接口,在上传文件前调用该文件的历史上传记录(项目中用到的就是这个方式) 下面我写了一个本地存储的
 // 获取已上传切片记录
function getUploadSliceRecord(context){
  let record = localStorage.getItem(context)
  if(!record){
    return []
  }else {
    try{
      return JSON.parse(record)
    }catch(e){}
  }
}
// 保存已上传切片
function saveUploadSliceRecord(context, sliceIndex){
  let list = getUploadSliceRecord(context)
  list.push(sliceIndex)
  localStorage.setItem(context, JSON.stringify(list))
}
let context = createContext(file);
// 获取上传记录
let record = getUploadSliceRecord(context);
let tasks = [];
chunks.forEach((chunk, index) => {
  // 已上传的切片则不再重新上传
  if(record.includes(index)){
    return
  }
	
  let fd = new FormData();
  fd.append("file", chunk);
  fd.append("context", context);
  fd.append("chunk", index + 1);

  let task = post("/mkblk.php", fd).then(res=>{
    // 上传成功后保存已上传切片记录
    saveUploadSliceRecord(context, index)
    record.push(index)
  })
  tasks.push(task);
});

海报业务

流程图

图片

两套组件一个用于展示,一个用于同步生成海报

图片

点击生成海报时,先拿到海报id,并获得底部回流链接

图片

生成海报

图片

海报保存

图片

思考--我们如何不用api自己去来实现一个dom元素转成图片呢?

图片

前端打点

图片

canvas放大镜关键点记录

image.png

模糊搜索亮高显示的思考

 <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>auto complete</title>
  <style>
    bdi {
      color: rgb(0, 136, 255);
    }

    li {
      list-style: none;
    }
  </style>
</head>
<body>
  <input class="inp" type="text">
  <section>
    <ul class="container"></ul>
  </section>
</body>
<script>

  function debounce(fn, timeout = 300) {//防抖
    let t; //设置一个接收标记,接受定时器返回值
    return (...args) => {//argsfn的插入参 ,即最下面的输入事件参数e,以数组的形式返回
      if (t) {
        clearTimeout(t);
      }
      t = setTimeout(() => {
        fn.apply(fn, args);//执行(e) => {} 改变指针传入参数e
      }, timeout);
    }
  }

  function memorize(fn) {//缓存
    const cache = new Map();//创建一个Map对象进行缓存
    return (name) => {//name为fn插入参  即value 输入值为空,清空container
      if (!name) {
        container.innerHTML = '';
        return;
      }
      if (cache.get(name)) {//如果map对象中有输入的valu 直接插入container
        container.innerHTML = cache.get(name);
        return;
      }
      //有缓存不再调用,请求,匹配,亮高;
      const res = fn.call(fn, name).join('');//以上两种情况都不是的话 fn执行后返回handleInput的Search 并进行渲染 join方法把数组转成了字符串并清除了","
      cache.set(name, res);
      console.log(name)
      console.log(cache)
      console.log(res)
      container.innerHTML = res;
    }
  }

  function handleInput(value) {
    const reg = new RegExp(value);//三个标志:全局模式g,不区分大小写模式i,多行模式m 即第二个参数  这里只匹配第一个结果 
    const search = data.reduce((res, cur) => {
      if (reg.test(cur)) {
        res.push(`<li>${cur.replace(reg, '<bdi>$&</bdi>')}</li>`);//亮高关键字显示,如果再Vue项目里 结合v-html
      }
      return res;
    }, []);//第二个参数[] 默认第一开始遍历时res为数组
    return search;
  }

  const data = ["上海野生动物园", "上饶野生动物园", "北京巷子", "上海中心", "上海黄埔江", "迪士尼上海", "陆家嘴上海中心"]
  const container = document.querySelector('.container');
  const memorizeInput = memorize(handleInput);
  document.querySelector('.inp').addEventListener('input', debounce(e => {
    memorizeInput(e.target.value);
  }))
</script>
</html>

Redux-immutable

什么是 immutable 数据?它有什么优势?

immutable 数据一种利用结构共享形成的持久化数据结构,一旦有部分被修改,那么将会返回一个全新的对象,并且原来相同的节点会直接共享。

具体点来说,immutable 对象数据内部采用是多叉树的结构,凡是有节点被改变,那么它和与它相关的所有上级节点都更新。

用一张动图来模拟一下这个过程: 图片 p1-jj.byteimg.com/tos-cn-i-t2…

因此,采用 immutable 既能够最大效率地更新数据结构,又能够和现有的 PureComponent (memo) 顺利对接,感知到状态的变化,是提高 React 渲染性能的极佳方案。

项目中涉及的 immutable 方法

1.fromJs:它的功能是将 JS 对象转换为 immutable 对象。

import {fromJS} from 'immutable';
const immutableState = fromJS ({
    count: 0
});

redux 的 reducer 文件中看到这个 api, 是 immutable 库当中导出的方法。

2.toJS 和 fromJS 功能刚好相反,用来将 immutable 对象转换为 JS 对象。但是值得注意的是,这个方法并没有在 immutable 库中直接导出,而是需要让 immutable 对象调用。比如:

const jsObj = immutableState.toJS ();

3.get/getIn 用来获取 immutable 对象属性。通过与 JS 对象的对比来体会一下:

//JS 对象
let jsObj = {a: 1};
let res = jsObj.a;
//immutable 对象
let immutableObj = fromJS (jsObj);
let res = immutableObj.get ('a');

//JS 对象
let jsObj = {a: {b: 1}};
let res = jsObj.a.b;
//immutable 对象
let immutableObj = fromJS (jsObj);
let res = immutableObj.getIn (['a', 'b']);// 注意传入的是一个数组

4.set:用来对 immutable 对象的属性赋值。

let immutableObj = fromJS ({a: 1});
immutableObj.set ('a', 2);

5.merge:新数据与旧数据对比,旧数据中不存在的属性直接添加,旧数据中已存在的属性用新数据中的覆盖。

let immutableObj = fromJS ({a: 1});
immutableObj.merge ({
    a: 2,
    b: 3
});// 修改了 a 属性,增加了 b 属性

Webpack

dllPlugin与DllReferencePlugin

  1. 什么是dll webpack 这块儿,就是事先把常用但又构建时间长的代码提前打包好(例如 react、react-dom),取个名字叫 dll。后面再打包的时候就跳过原来的未打包代码,直接用dll。这样一来,构建时间就会缩短,提高 webpack 打包速度。 第一开始觉得dll和缓存有点区分不开,来个对比: |DLL | 缓存 | |---|---| |1.把公共代码打包为 DLL 文件存到硬盘里 | 1.把常用文件存到硬盘/内存里
    | |2.第二次打包时动态链接 DLL 文件,不重新打包 | 2.第二次加载时直接读取缓存,不重新请求 | |3.打包时间缩短 | 3.加载时间缩短 |
  • 如何配置 1.定义webpack.dll.config.js 首先写一个创建 dll 文件的打包脚本,目的是把 react,react-dom打包成 dll 文件:

// webpack.dll.config.js
module.exports = {
    entry: {
        // 定义程序中打包公共文件的入口文件vendor.js
        vender: resource.vender,
        lib: resource.lib
    },
    
    plugins: [
        new webpack.DllPlugin({
            // manifest缓存文件的请求上下文(默认为webpack执行环境上下文)
            context: process.cwd(),
            
            // manifest.json文件的输出位置
            path: path.join(src, 'js', 'dll', '[name]-manifest.json'),
            
            // 定义打包的公共vendor文件对外暴露的函数名
            name: '[name]_[hash]'
        })
    ]
}

打包脚本写好了,我们总得运行吧?所以我们写个运行脚本放在package.jsonscripts 标签里,这样我们运行 npm run build:dll 就可以打包 dll 文件了:

{
  "scripts": {
    "build:dll": "webpack --config configs/webpack.dll.js",
  },
}

2.链接 dll 文件,也就是告诉 webpack 可以命中的 dll 文件,配置也是一大坨:

// 文件目录:configs/webpack.config.js
// 代码太长可以不看

const path = require('path');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); // 顾名思义,把资源加到 html 里,那这个插件把 dll 加入到 index.html 里
const webpack = require('webpack');
module.exports = {
  // ......
  plugins: [
    new webpack.DllReferencePlugin({
      // 注意: DllReferencePlugin 的 context 必须和 package.json 的同级目录,要不然会链接失败
      context: path.resolve(__dirname, '../'),
      manifest: path.resolve(__dirname, '../dll/react.manifest.json'),
    }),
    new AddAssetHtmlPlugin({
      filepath: path.resolve(__dirname, '../dll/_dll_react.js'),
    }),
  ]
}

路径解析

当我们写一个 import 语句来引用一个模块时,webpack 是如何获取到对应模块的文件路径的呢?

  • 解析相对路径 1.查找相对当前模块的路径下是否有对应文件或文件夹 2.是文件则直接加载 3.是文件夹则继续查找文件夹下的 package.json 文件 4.有 package.json 文件则按照文件中 main 字段的文件名来查找文件 5.无 package.json 或者无 main 字段则查找 index.js 文件

  • 解析模块名 查找当前文件目录下,父级目录及以上目录下的 node_modules 文件夹,看是否有对应名称的模块

  • 解析绝对路径 直接查找对应路径的文件

    resolve配置
  • resolve.alias 假设我们有个 utils 模块极其常用,经常编写相对路径很麻烦,希望可以直接import 'utils'来引用,那么我们可以配置某个模块的别名,如:

    alias: {
		utils: path.resolve(__dirname, 'src/utils') 
		// 这里使用 path.resolve 和 __dirname 来获取绝对路径
	}

上述的配置是模糊匹配,意味着只要模块路径中携带了 utils 就可以被替换掉,如:

import 'utils/query.js' // 等同于 import '[项目绝对路径]/src/utils/query.js'
  • resolve.extensions 我们在引用模块时,其实可以直接这样:
import * as common from './src/utils/common'

webpack 会自行补全文件后缀,而这个补全的行为,也是可以配置的。

extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx'],
// 这里的顺序代表匹配后缀的优先级,例如对于 index.js 和 index.jsx,会优先选择 index.js

webpack 会尝试给你依赖的路径添加上 extensions 字段所配置的后缀,然后进行依赖路径查找,所以可以命中 src/utils/common.js 文件。

但如果你是引用 src/styles 目录下的 common.css 文件时,如 import './src/styles/common',webpack 构建时则会报无法解析模块的错误。 解决方法:

//在 extensions 添加一个 .css 的配置:
extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx', '.css'],
  • resolve.modules 对于直接声明依赖名的模块(如 react ),webpack 会类似 Node.js 一样进行路径搜索,搜索 node_modules 目录,这个目录就是使用 resolve.modules 字段进行配置的,默认就是:
resolve: {
  modules: ['node_modules'],
},

通常情况下,我们不会调整这个配置,但是如果可以确定项目内所有的第三方依赖模块都是在项目根目录下的 node_modules 中的话,那么可以在 node_modules 之前配置一个确定的绝对路径:

resolve: {
  modules: [
    path.resolve(__dirname, 'node_modules'), // 指定当前目录下的 node_modules 优先查找
    'node_modules', // 如果有一些类库是放在一些奇怪的地方的,你可以添加自定义的路径或者目录
  ],
},

这样配置在某种程度上可以简化模块的查找,提升构建速度。

  • resolve.mainFields 有 package.json 文件则按照文件中 main 字段的文件名来查找文件,其实确切的情况并不是这样的,webpack 的 resolve.mainFields 配置可以进行调整。当引用的是一个模块或者一个目录时,会使用 package.json 文件的哪一个字段下指定的文件,默认的配置是这样的:
resolve: {
  // 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
  mainFields: ['browser', 'module', 'main'],

  // target 的值为其他时,mainFields 默认值为:
  mainFields: ["module", "main"],
},
  • resolve.mainFiles

当目录下没有 package.json 文件时,我们说会默认使用目录下的index.js这个文件,其实这个也是可以配置的,是的,使用 resolve.mainFiles 字段,默认配置是:

resolve: {
  mainFiles: ['index'], // 你可以添加其他默认使用的文件名
},

entry

  • 单一入口的配置:
module.exports = {
  entry: './src/index.js' 
}

// 上述配置等同于
module.exports = {
  entry: {
    main: './src/index.js' // 你可以使用其他名称来替代 main,例如 index、app 等
  }
}

如果我们有多个页面,需要有多个 js 作为构建入口,可以是这样配置:

// 多个入口生成不同文件
module.exports = {
  entry: {
    // 按需取名,通常是业务名称
    foo: './src/foo.js',
    bar: './src/bar.js',
  },
}

还有一种场景比较少用到,即是多个文件作为一个入口来配置,webpack 会解析多个文件的依赖然后打包到一起:

// 使用数组来对多个文件进行打包
module.exports = {
  entry: {
    main: [
      './src/foo.js',
      './src/bar.js'
    ]
  }
}
  • 动态 entry 我们选择在项目的「src/」目录下创建一个新的文件夹名为「pages」,然后在「src/pages」下创建新的文件夹来作为入口存放的路径,例如「src/pages/foo/index.js」为一个新的页面入口,然后我们在配置里边可以这样来创建入口:
const path = require('path');
const fs = require('fs');

// src/pages 目录为页面入口的根目录
const pagesRoot = path.resolve(__dirname, './src/pages');
// fs 读取 pages 下的所有文件夹来作为入口,使用 entries 对象记录下来
const entries = fs.readdirSync(pagesRoot).reduce((entries, page) => {
  // 文件夹名称作为入口名称,值为对应的路径,可以省略 `index.js`,webpack 默认会寻找目录下的 index.js 文件
  entries[page] = path.resolve(pagesRoot, page);
  return entries;
}, {});

module.exports = {
  // 将 entries 对象作为入口配置
  entry: entries,

  // ...
};
  • moudle的规则条件匹配设置

大多数情况下,配置 loader 的匹配条件时,只要使用test字段就好了,很多时候都只需要匹配文件后缀名来决定使用什么 loader,但也不排除在某些特殊场景下,我们需要配置比较复杂的匹配条件。webpack 的规则提供了多种配置形式:

1.{ test: ... } 匹配特定条件 2.{ include: ... } 匹配特定路径 3.{ exclude: ... } 排除特定路径 4.{ and: [...] }必须匹配数组中所有条件 5.{ or: [...] } 匹配数组中任意一个条件 6.{ not: [...] } 排除匹配数组中所有条件

上述的所谓条件的值可以是:

1.字符串:必须以提供的字符串开始,所以是字符串的话,这里我们需要提供绝对路径 2.正则表达式:调用正则的 test 方法来判断匹配 3.函数:(path) => boolean,返回 true 表示匹配 4.数组:至少包含一个条件的数组 5.对象:匹配所有属性值的条件

通过例子来帮助理解:

rules: [
  {
    test: /\.jsx?/, // 正则
    include: [
      path.resolve(__dirname, 'src'), // 字符串,注意是绝对路径
    ], // 数组
    // ...
  },
  {
    resource: {
      test: {
        js: /\.js/,
        jsx: /\.jsx/,
      }, // 对象,不建议使用
      not: [
        (value) => { /* ... */ return true; }, // 函数,通常需要高度自定义时才会使用
      ],
    }
  },
],
  • loader实行顺序 从后到前的顺序是在同一个rule中进行的,那如果多个 rule 匹配了同一个模块文件,loader 的应用顺序又是怎样的呢?看一份这样的配置: eslint-loader 要检查的是人工编写的代码,如果在 babel-loader 之后使用,那么检查的是 Babel 转换后的代码,所以必须在babel-loader处理之前使用。
rules: [
  {
    test: /\.js$/,
    exclude: /node_modules/,
    loader: "eslint-loader",
  },
  {
    test: /\.js$/,
    exclude: /node_modules/,
    loader: "babel-loader",
  },
],

这样无法法保证 eslint-loaderbabel-loader 应用前执行。webpack 在 rules 中提供了一个 enforce 的字段来配置当前 rule 的 loader 类型,没配置的话是普通类型,我们可以配置 pre post,分别对应前置类型或后置类型的 loader。

还有一种行内 loader,即我们在应用代码中引用依赖时直接声明使用的 loader,如 const json = require('json-loader!./file.json') 这种。

所有的 loader 按照前置 -> 行内 -> 普通 -> 后置的顺序执行。所以当我们要确保 eslint-loader 在 babel-loader 之前执行时,可以如下添加 enforce 配置:

rules: [
  {
    enforce: 'pre', // 指定为前置类型
    test: /\.js$/,
    exclude: /node_modules/,
    loader: "eslint-loader",
  },
]
  • noParse module.noParse 字段,可以用于配置哪些模块文件的内容不需要进行解析。对于一些不需要解析依赖(即无依赖) 的第三方大型类库等,可以通过这个字段来配置,以提高整体的构建速度。

使用 noParse 进行忽略的模块文件中不能使用 import、require、define 等导入机制。

module.exports = {
  // ...
  module: {
    noParse: /jquery|lodash/, // 正则表达式

    // 或者使用 function
    noParse(content) {
      return /jquery|lodash/.test(content)
    },
  }
}
  • definePlugin

在不同的 mode 中,会使用 DefinePlugin 来设置运行时的 process.env.NODE_ENV 常量。DefinePlugin 用于创建一些在编译时可以配置值,在运行时可以使用的常量

module.exports = {
  // ...
  plugins: [
    new webpack.DefinePlugin({
      PRODUCTION: JSON.stringify(true), // const PRODUCTION = true
      VERSION: JSON.stringify('5fa3b9'), // const VERSION = '5fa3b9'
      BROWSER_SUPPORTS_HTML5: true, // const BROWSER_SUPPORTS_HTML5 = 'true'
      TWO: '1+1', // const TWO = 1 + 1,
      CONSTANTS: {
        APP_VERSION: JSON.stringify('1.1.2') // const CONSTANTS = { APP_VERSION: '1.1.2' }
      }
    }),
  ],
}

有了上面的配置,就可以在应用代码文件中,访问配置好的常量了,如:

console.log("Running App version " + VERSION);

if(!BROWSER_SUPPORTS_HTML5) require("html5shiv");
  • TerserPlugin

webpack mode 为 production 时会启用 TerserPlugin 来压缩 JS 代码,我们看一下如何使用的:

module.exports = {
  // ...
  // TerserPlugin 的使用比较特别,需要配置在 optimization 字段中,属于构建代码优化的一部分
  optimization: {
    minimize: true, // 启用代码压缩
    minimizer: [new TerserPlugin({
      test: /\.js(\?.*)?$/i, // 只处理 .js 文件
      cache: true, // 启用缓存,可以加速压缩处理
    })], // 配置代码压缩工具
  },
}
  • IgnorePlugin IgnorePlugin 和 DefinePlugin 一样,也是一个 webpack 内置的插件,可以直接使用 webpack.IgnorePlugin 来获取。 这个插件用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去。例如我们使用 moment.js,直接引用后,里边有大量的 i18n 的代码,导致最后打包出来的文件比较大,而实际场景并不需要这些 i18n 的代码,这时我们可以使用 IgnorePlugin 来忽略掉这些代码文件,配置如下:
module.exports = {
  // ...
  plugins: [
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
  ]
}
//IgnorePlugin 配置的参数有两个,第一个是匹配引入模块路径的正则表达式,第二个是匹配模块的对应上下文,即所在目录名。
  • Css压缩

我们需要压缩 CSS 代码的话,得使用 postcss-loader,在它的基础上来使用 cssnano

  module: {
    rules: {
      // ...
      {
        test: /\.css/,
        include: [
          path.resolve(__dirname, 'src'),
        ],
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: () => [ // 返回 postcss 的插件列表
                require('cssnano')(), // 使用 cssnano
              ],
            },
          },
       ],
      },
    },
  }
}
  • Tree shaking

移除 JavaScript 上下文中的未引用代码,删掉用不着的代码,能够有效减少 JS 代码文件的大小。

// src/math.js
export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

// src/index.js
import { cube } from './math.js' // 在这里只是引用了 cube 这个方法

console.log(cube(3))

如果整个项目代码只是上述两个文件,那么很明显,square 这个方法是未被引用的代码,是可以删掉的。只需要在production 的 mode下,webpack 便会移除 square 方法的这一部分代码,来减少构建出来的代码整体体积。

而用 development mode,那么我们需要在配置文件中新增:

module.exports = {
  mode: 'development',
  //...
  optimization: { 
    useExports: true, // 模块内未使用的部分不进行导出
  },
}

然后执行构建后查看构建出来的代码内容,可以发现:

/*! exports provided: square, cube */
/*! exports used: cube */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* unused harmony export square */
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return cube; });
function square(x) {
  return x * x;
}
function cube(x) {
  return x * x * x;
}
/***/ })

最外层的是模块闭包,webpack 会把一个模块用一个 function 包起来,避免多个模块的内部变量冲突,使用了 useExports: true 之后可以看到注释说明 square 未使用,对外暴露的方法只有 cube。这里已经给模块中是否被使用到的方法打了标识,当你使用TerserPlugin后,Terser 会移除那些没有对外暴露且没有额外副作用的方法,来减小构建出来的代码体积。

  • optimization.sideEffects

lodash ,它是一个工具库,提供了大量的对字符串、数组、对象等常见数据类型的处理函数,但是有的时候我们只是使用了其中的几个函数,全部函数的实现都打包到我们的应用代码中,其实很浪费。

现在 lodash 的 ES 版本 的 package.json 文件中已经有 sideEffects: false 这个声明了,当某个模块的package.json 文件中有了这个声明之后,webpack 会认为这个模块没有任何副作用,只是单纯用来对外暴露模块使用,一旦你开启了 optimization.sideEffects 的话,那么在打包的时候就会做一些额外的处理。

例如你这么使用 lodash:

import { forEach, includes } from 'lodash-es'

forEach([1, 2], (item) => {
  console.log(item)
})

console.log(includes([1, 2, 3], 1))

由于lodash-es这个模块的「package.json」文件有 sideEffects: false 的声明,最终的结果类似于 webpack 将上述的代码转换为以下的代码去处理:

import { default as forEach } from 'lodash-es/forEach'
import { default as includes } from 'lodash-es/includes'

webpack 不会把 lodash-es 所有的代码内容打包进来,只是打包了你用到的那两个模块相关的代码,这便是 sideEffects 的作用。

「package.json」下的 sideEffects 可以是匹配文件路径的数组,表示这些模块文件是有副作用的,不能被移除:

{
  sideEffects: [
    "*.css"
  ]
}

CSS 代码文件是最典型的有副作用的模块,主要import了就不能移除,因为你需要它的样式代码,所以使用 sideEffects 来优化你项目代码时切记,要声明 CSS 文件是有副作用的。

  • 分离公共部分 webpack 来拆分公共的 JS 代码,一个最简单的例子:
module.exports = {
  // ... webpack 配置

  optimization: {
    splitChunks: {
      chunks: "all", // 所有的 chunks 代码公共的部分分离出来成为一个单独的文件
      name: 'common', // 给分离出来的 chunk 起个名字
    },
  },
}

我们需要在 HTML 中引用两个构建出来的 JS 文件,并且 commons.bundle.js 需要在入口代码之前。下面是个简单的例子:

<script src="commons.bundle.js" charset="utf-8"></script>
<script src="index.bundle.js" charset="utf-8"></script>

如果你使用了 html-webpack-plugin,那么对应需要的 JS 文件都会在 HTML 文件中正确引用,不用担心。如果你会根据页面区分不同的 JS 入口的话,那么在使用 html-webpack-plugin 时需要指定对应的 chunk

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'page.html',
      chunks: ['common', 'page'], // 这里要包括 common 部分和页面业务部分
    }),
  ]
}

1.splitChunks 配置项 chunks 表示从哪些模块中抽取代码,可以设置 all/async/initial 三个值其中一个,分别表示 所有模块/异步加载的模块/同步加载的模块,或者也可以设置一个 function,用于过滤掉不需要抽取代码的模块,例如:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks (chunk) {
        // 排除 `my-excluded-chunk`
        return chunk.name !== 'my-excluded-chunk';
      }
    }
  }
};

cacheGroups 是最关键的配置,表示抽离公共部分的配置,一个 key-value 的配置对应一个生成的代码文件,通常我们都会在这里下功夫,先看下简单的例子:

  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        }
      },
    }
  }
}
//priority 权重配置,如果一个模块满足多个 cacheGroup 的匹配条件,那么就由权重来确定抽离到哪个 cacheGroup
// reuseExistingChunk 设置为 true 表示如果一个模块已经被抽离出去了,那么则复用它,不会重新生成。