Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

一个被忽视的webpack插件 #14

Open
akira-cn opened this issue Aug 11, 2019 · 1 comment
Open

一个被忽视的webpack插件 #14

akira-cn opened this issue Aug 11, 2019 · 1 comment

Comments

@akira-cn
Copy link
Owner

akira-cn commented Aug 11, 2019

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

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

// 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设计得如此不同,我们使用特性检测的方式从不同的环境中获取CanvasRenderingContext2D对象,这段代码写起来比较方便,用起来也很简单,但是我们将这段代码打包之后,会留有额外没用的代码。

此时,如果你希望分别编译到不同平台上时,只保留该平台相关的代码,其实是可以借助打包工具的配置实现,比如在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.platform}.js`,
    },
  },
}

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

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

就可以了。

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

@q269384828
Copy link

以前serverUrl之类的 都是window.XXX暴露出来的,用了Define之后好多了,但是没想到还可以结合alias使用,哈哈哈

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants