一个被忽视的 webpack 插件

如今的前端开发,有可能会面对复杂的环境,所以工程化思维几乎是专业前端工程师必备的。让同一套代码,在不同的环境中运行时,如何让它以最优的方式(尽可能小、尽可能快)加载和执行,是我们需要考虑的问题。
假设我们需要在开发环境中输出额外的调试信息,而在线上环境中不输出,我们可以定义环境变量:

// env.js

export
const
isDEV  =
true
;

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

if
(
isDEV )
{

console .
log
(
‘…some information…’
)
;

}

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

:bulb:注意,如果你使用预处理器,比如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”
)
}

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

plugins: [

new

webpack .
DefinePlugin

(
{

isDev :
env .
mode  ===
‘development’

}
)
,

]
,

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

:bulb: 注意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)’
:
>
?
:
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就能很好地解决这个问题了。
:bulb: 注意,再次强调,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中讨论。