【前端冷知识】一个被忽视的webpack插件

220 阅读6分钟
原文链接: mp.weixin.qq.com

如今的前端开发,有可能会面对复杂的环境,所以工程化思维几乎是专业前端工程师必备的。让同一套代码,在不同的环境中运行时,如何让它以最优的方式(尽可能小、尽可能快)加载和执行,是我们需要考虑的问题。

假设我们需要在开发环境中输出额外的调试信息,而在线上环境中不输出,我们可以定义环境变量:

// env.js

export const isDEV = true;

import {isDEV} from './env.js';

if(isDEV) {

  console.log('...some information...');

}

在发布上线的时候,我们将isDEV的值设置为false。

💡注意,如果你使用预处理器,比如webpack等打包器,或者代码压缩工具,当isDev === false时,console.log代码并不会被输出到线上,因为现在的预编译工具一般都是会做这种基础的优化的,当if分支条件肯定为false的时候,直接 从代码里将整个分支移除。所以如果isDev的值为false,在线上代码里,整个if语句块都不会被输出。

如果我们采用的是特性检测的方式让代码执行在不同的环境中,并且这些环境肯定是不相容的时候,我们希望将它分开编译成两套代码,不借助工具的配置的话,会比较困难。

比如:

function createContext2D() {

  if(typeof document !== 'undefined' && typeof document.createElement === 'function') {

    // 如果是浏览器环境

    const canvas = document.createElement('canvas');

    return canvas.getContext('2d');

  }

  if(typeof wx !== 'undefined' && typeof wx.createCanvas === 'function') {

    // 如果是微信小游戏环境

    const canvas = wx.createCanvas();

    return canvas.getContext('2d');

  }

  if(typeof wx !== 'undefined' && typeof wx.createCanvasContext === 'function') {

    // 如果是微信小程序环境

    return wx.createCanvasContext('canvas');

  }

  return null;

}

在这里,我们不吐槽为什么微信小程序和微信小游戏的canvas API设计得如此不同,我们使用特性检测的方式从不同的环境中获取CanvasRenderingContext2DD对象,这段代码写起来比较方便,用起来也很简单,但是我们将这段代码打包之后,会留有额外没用的代码。

此时,如果你希望分别编译到不同平台上时,只保留该平台相关的代码,其实是可以借助打包工具的配置实现,比如在webpack中,可以配置webpack的DefinePlugin插件:

// webpack.config.js

plugins: [

  new webpack.DefinePlugin({

    'typeof document': env.platform === 'browser' ? '"object"' : '"undefined"',

    'typeof document.createElement': env.platform === 'browser' ? '"function"' : '"undefined"',

    'typeof wx': env.platform !== 'browser' ? '"object"' : '"undefined"',

    'typeof wx.createCanvas': env.platform === 'minigame' ? '"function"' : '"undefined"',

    'typeof wx.createCanvasContext': env.platform === 'miniprogram' ? '"function"' : '"undefined"',

  }),

  ...

],

当我们这么定义了之后,可以分别编译三个平台上的代码:

// package.json

"scripts": {

  "compile:browser": "webpack --env.platform=browser --env.mode=production",

  "compile:minigame": "webpack --env.platform=minigame --env.mode=production",

  "compile:miniprogram": "webpack --env.platform=miniprogram --env.mode=production",

}

这样我们在三个平台上分别输出的createContext2D方法如下:

// browser

function t(){return document.createElement("canvas").getContext("2d")}

// mimigame

function t(){return wx.createCanvas().getContext("2d")}

// miniprogram

function t(){return wx.createCanvasContext("canvas")}

👉🏻 webpack的DefinePlugin插件是一个经常被开发者忽略的极有用的一个插件,它可以用来实现类似于宏替换的功能。

比如:

plugins: [

  new webpack.DefinePlugin({

    isDev: env.mode === 'development'

  }),

  ...

],

可以实现上面我们那个在开发环境下输出log的需求,不需要再额外写一个env.js。

💡 注意DefinePlugin插件并不是定义了一个叫做isDev的变量,而是将代码中的isDev用编译时env.mode === 'development'表达式的值替换。所以,在打包的代码中:

if(isDev) {

  console.log('...some information...');

}

直接被替换成

// env.mode === development

if(true) {

  console.log('...some information...');

}

// env.mode === production

if(false) {

  console.log('...some information...');

}

然后再进一步优化成

// env.mode === development

if(true) {

console.log('...some information...');

// env.mode === production

// 被从源代码中除去

所以其实这个插件叫DefinePlugin有点不合适,可能叫MacroPlugin或者其他什么的名称更好。

我们可以做其他的宏替换,比如:

plugins: [

  new webpack.DefinePlugin({

    'Math.PI': Math.PI,

    ...

  }),

  ...

],

如果这么配置,下面的代码:

console.log(Math.PI, Math.PI * 2, Math.PI / 2);

会被编译成:

console.log(3.141592653589793,6.283185307179586,1.5707963267948966);

这个意义不是很大,这种优化JS引擎本身也会做,不过确实可以快一点点。

还有:

plugins: [

  new webpack.DefinePlugin({

    'Math.max(a, b)': a > b ? a : b,

    ...

  }),

  ...

],

这个局限性就更大了,意义很小。

我们可以用这个插件来定义一些预置的宏,提供模块的信息,比如将package.json中的版本号导入到模块中:

// webpack.config.js

const version = require('./package.json').version;

...

plugins: [

  new webpack.DefinePlugin({

    '__VERSION__': version,

    ...

  }),

  ...

],

在模块代码中:

const version = __VERSION__;

export {version};

当然我们可以将package.json直接import进来然后将version属性导出,但是这么做会把整个package.json中的内容全都打包进模块,如果我们只是使用其中的version属性,那么打包一整个package.json文件也没必要,所以采用DefinePlugin就能很好地解决这个问题了。

💡 注意,再次强调,DefinePlugin做的是代码中的宏替换,不要把它当做定义变量来使用。

如果在模块中,有与宏名相同的变量,那么这个宏就并不会被替换:

// 定义了同名变量

const __VERSION__ = myVersion;

// 此时__VERSION__就不会被替换成webpack插件中定义的宏

const version = __VERSION__;

export {version};

我们也要管理好在webpack的DefinePlugin中定义的宏,没有必要,就不要定义太多宏,如果定义了,必须要在使用到的代码中以注释标注:

const version = __VERSION__; // from webpack DefinePlugin

export {version};

否则的话,将来可能会给维护代码的同学带来困扰,毕竟在代码中看到一个标识符不知道这个标识符从哪儿来的,是一件很恼火的事情。

扩展

前面的条件编译问题,如果我们提供针对不同平台的模块级别的代码,那么也可以使用webpack的另一个特性:alias。

比如我们将之前createContext2D的代码重构一下,写成3个模块:

// platform/create-context-2d.browser.js

export function createContext2D() {

  const canvas = document.createElement('canvas');

  return canvas.getContext('2d');

}

// platform/create-context-2d.minigame.js

export function createContext2D() {

  const canvas = wx.createCanvas();

  return canvas.getContext('2d');

}

// platform/create-context-2d.miniprogram.js

export function createContext2D() {

  return wx.createCanvasContext('canvas');

}

然后通过配置webpack.config的alias:

...

return {

  ...

  resolve: {

    alias: {

      'create-context-2d': `./src/platform/create-context-2d.${env.mod}.js`,

    },

  },

}

这样我们在代码中直接使用:

import {createContext2D} from 'create-context-2d';

就可以了。

好了,关于条件编译和DefinePlugin插件的问题就讨论到这里,关于这两块,大家还有什么想法,欢迎在issue中讨论。