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