一个被忽视的 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)’
:
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就能很好地解决这个问题了。
: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中讨论。