一个被忽视的 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中讨论。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章