一步步從零開始用 webpack 搭建一個大型項目

開篇

不少人都或多或少使用過 webpack,可是不多有人可以系統的學習 webpack 配置,遇到錯誤的時候就會一臉懵,不知道從哪查起?性能優化時也不知道能作什麼,網上的優化教程是否是符合本身的項目?等一系列問題!本文從最基礎配置一步步到一個完善的大型項目的過程。讓你對 webpack 不再會畏懼,讓它真正成爲你的得力助手!css

本文從下面幾個課題來實現html

項目地址

github.com/luoxue-vict…前端

我把每一課都切成了不一樣的分支,你們能夠根據課時一步步學習vue

腳手架

npm i -g webpack-box
複製代碼

使用

webpack-box dev   # 開發環境
webpack-box build # 生產環境
webpack-box dll   # 編譯差分包
webpack-box dev index   # 指定頁面編譯(多頁面)
webpack-box build index # 指定頁面編譯(多頁面)
webpack-box build index --report # 開啓打包分析
webpack-box build:ssr  # 編譯ssr
webpack-box ssr:server # 在 server 端運行
複製代碼

在 package.json 中使用node

{
  "scripts": {
    "dev": "webpack-box dev",
    "build": "webpack-box build",
    "dll": "webpack-box dll",
    "build:ssr": "webpack-box build:ssr",
    "ssr:server": "webpack-box ssr:server"
  }
}
複製代碼

使用react

npm run build --report # 開啓打包分析
複製代碼

擴展配置webpack

box.config.jsgit

module.exports = function (config) {
  /** * @param {object} dll 開啓差分包 * @param {object} pages 多頁面配置 經過 box run/build index 來使用 * @param {function} chainWebpack * @param {string} entry 入口 * @param {string} output 出口 * @param {string} publicPath * @param {string} port */
  return {
    entry: 'src/main.js',
    output: 'dist',
    publicPath: '/common/',
    port: 8888,
    dll: {
      venders: ['vue', 'react']
    },
    pages: {
      index: {
        entry: 'src/main.js',
        template: 'public/index.html',
        filename: 'index.html',
      },
      index2: {
        entry: 'src/main.js',
        template: 'public/index2.html',
        filename: 'index2.html',
      }
    },
    chainWebpack(config) {
    }
  }
}
複製代碼

課題 1:初探 webpack?探究 webpack 打包原理

想要學好 webpack,咱們首先要了解 webpack 的機制,咱們先從js加載css開始學習。es6

咱們從下面這個小練習開始走進 webpackgithub

index.js 中引入 index.css

const css = require('./index.css')
console.log(css)
複製代碼

css 文件並不能被 js 識別,webpack 也不例外,上述的寫法不出意外會報錯

咱們如何讓 webpack 識別 css 呢,答案就在 webpack 給咱們提供了 loader 機制,可讓咱們經過loader 將任意的文件轉成 webpack 能夠識別的文件

本章主要講解

  1. webpack 基礎配置
  2. 解析 bundle 如何加載模塊
  3. 動態 import 加載原理
  4. 使用 webpack-chain 重寫配置
  5. 課時 1 小結

webpack 基礎配置

須要的依賴包

package.json

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack", // 開發環境
    "build": "cross-env NODE_ENV=production webpack" // 生產環境
  },
  "dependencies": {
    "cross-env": "^6.0.3", // 兼容各類環境
    "css-loader": "^3.2.0",
    "rimraf": "^3.0.0", // 刪除文件
    "webpack": "^4.41.2"
  },
  "devDependencies": {
    "webpack-cli": "^3.3.10"
  }
}
複製代碼

webpack 基礎配置

webpack.config.js

const path = require('path');
const rimraf = require('rimraf');

// 刪除 dist 目錄
rimraf.sync('dist');

// webpack 配置
module.exports = {
  entry: './src/index',
  mode: process.env.NODE_ENV,
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};
複製代碼

css 引入到 js

src/index.js

const css = require('css-loader!./index.css');
const a = 100;
console.log(a, css);
複製代碼

測試 css

src/index.css

body {
  width: 100%;
  height: 100vh;
  background-color: orange;
}
複製代碼

解析 bundle 如何加載模塊

我刪掉了一些註釋跟一些干擾內容,這樣看起來會更清晰一點

  • bundle 是一個當即執行函數,能夠認爲它是把全部模塊捆綁在一塊兒的一個巨型模塊。
  • webpack 將全部模塊打包成了 bundle 的依賴,經過一個對象注入
  • 0 模塊 就是入口
  • webpack 經過 __webpack_require__ 引入模塊
  • __webpack_require__ 就是咱們使用的 require,被 webpack 封裝了一層

dist/bundle.js

(function(modules) {
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    });

    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );

    module.l = true;

    return module.exports;
  }
  return __webpack_require__((__webpack_require__.s = 0));
})({
  './src/index.js': function(module, exports, __webpack_require__) {
    eval(` const css = __webpack_require__("./src/style/index.css") const a = 100; console.log(a, css) `);
  },

  './src/style/index.css': function(module, exports, __webpack_require__) {
    eval(` exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false); exports.push([module.i, "body { width: 100%; height: 100vh; background-color: orange; }", ""]); `);
  },

  0: function(module, exports, __webpack_require__) {
    module.exports = __webpack_require__('./src/index.js');
  }
});
複製代碼

動態 import 加載原理

若是咱們把 index.js 的 require 改爲 import 會發生什麼?

咱們知道 importrequire 的區別是,import 是動態加載只有在用到的時候纔會去加載,而 require 只要聲明瞭就會加載,webpack 遇到了 require 就會把它當成一個模塊加載到 bundle 的依賴裏

那麼問題來了,若是咱們使用了 import 去引用一個模塊,它是如何加載的呢?

require 改爲 import()

src/index.js

// const css = require('css-loader!./index.css');
const css = import('css-loader!./index.css');
const a = 100;
console.log(a, css);
複製代碼

動態加載打包結果

除了正常的 bundle 以外,咱們還能夠看見一個 0.boundle.js

0.boundle.js 就是咱們的動態加載的 index.css 模塊

|-- bundle.js
|-- 0.boundle.js
複製代碼

動態模塊

0.boundle.js

這個文件就是把咱們 import 的模塊放進了一個單獨的 js 文件中

(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  [0],
  {
    './node_modules/css-loader/dist/runtime/api.js': function( module, exports, __webpack_require__ ) {
 'use strict';
      eval(` ... `);
    },

    './src/style/index.css': function(module, exports, __webpack_require__) {
      eval(` exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false)); exports.push([module.i, \`body { width: 100%; height: 100vh; background-color: orange; },"\`] `);
    }
  }
]);
複製代碼

動態模塊加載邏輯

咱們再看下 dist/bundle.js

方便理解,我把大部分代碼和註釋都刪掉了

原理很簡單,就是利用的 jsonp 的實現原理加載模塊,只是在這裏並非從 server 拿數據而是從其餘模塊中

  1. 調用模塊時會在 window 上註冊一個 webpackJsonp 數組,window['webpackJsonp'] = window['webpackJsonp'] || []
  2. 當咱們 import時,webpack 會調用 __webpack_require__.e(0) 方法,也就是 requireEnsure
  3. webpack 會動態建立一個 script 標籤去加載這個模塊,加載成功後會將該模塊注入到 webpackJsonp
  4. webpackJsonp.push 會調用 webpackJsonpCallback 拿到模塊
  5. 模塊加載完(then)再使用 __webpack_require__ 獲取模塊
(function(modules) {
  function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];
    var moduleId,
      chunkId,
      i = 0,
      resolves = [];
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if (
        Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
        installedChunks[chunkId]
      ) {
        resolves.push(installedChunks[chunkId][0]);
      }
      // 模塊安裝完
      installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    if (parentJsonpFunction) parentJsonpFunction(data);
    while (resolves.length) {
      // 執行全部 promise 的 resolve 函數
      resolves.shift()();
    }
  }

  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + '' + ({}[chunkId] || chunkId) + '.bundle.js';
  }

  function __webpack_require__(moduleId) {
    // ...
  }

  __webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];
    // ...
    var script = document.createElement('script');
    var onScriptComplete;
    script.charset = 'utf-8';
    script.timeout = 120;
    script.src = jsonpScriptSrc(chunkId);

    onScriptComplete = function(event) {
      // 處理異常,消除反作用
      // ...
    };
    var timeout = setTimeout(function() {
      onScriptComplete({ type: 'timeout', target: script });
    }, 120000);
    script.onerror = script.onload = onScriptComplete;
    document.head.appendChild(script);
    // ...
    // 動態加載模塊
    return Promise.all(promises);
  };

  var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || []);
  // 重寫數組 push 方法
  jsonpArray.push = webpackJsonpCallback;
  jsonpArray = jsonpArray.slice();
  for (var i = 0; i < jsonpArray.length; i++)
    webpackJsonpCallback(jsonpArray[i]);

  return __webpack_require__((__webpack_require__.s = 0));
})({
  './src/index.js': function(module, exports, __webpack_require__) {
    eval(` const css = __webpack_require__.e(0).then(__webpack_require__.t.bind(null, "./src/style/index.css", 7)) const a = 100; console.log(a, css) `);
  },
  0: function(module, exports, __webpack_require__) {
    eval(`module.exports = __webpack_require__("./src/index.js");`);
  }
});
複製代碼

使用 webpack-chain 重寫配置

咱們用 webpack-chain 來寫 webpack 的配置,緣由是 webpack-chain 的方式更加靈活

官方解釋

webpack-chain 嘗試經過提供可鏈式或順流式的 API 建立和修改 webpack 配置。APIKey 部分能夠由用戶指定的名稱引用,這有助於跨項目修改配置方式的標準化。

const path = require('path');
const rimraf = require('rimraf');
const Config = require('webpack-chain');
const config = new Config();
const resolve = src => {
  return path.join(process.cwd(), src);
};

// 刪除 dist 目錄
rimraf.sync('dist');

config
  // 入口
  .entry('src/index')
  .add(resolve('src/index.js'))
  .end()
  // 模式
  // .mode(process.env.NODE_ENV) 等價下面
  .set('mode', process.env.NODE_ENV)
  // 出口
  .output.path(resolve('dist'))
  .filename('[name].bundle.js');

config.module
  .rule('css')
  .test(/\.css$/)
  .use('css')
  .loader('css-loader');

module.exports = config.toConfig();
複製代碼

課時 1 小結

至此課時 1 已經結束了,咱們主要作了如下事情

  1. webpack 基礎配置
  2. 將 css 經過 css-loader 打包進 js 中
  3. 解析 bundle 如何加載模塊的
  4. webpack 如何實現的動態加載模塊

學習一個工具咱們不只要看懂它的配置,還要對它的原理一塊兒瞭解,只有學到框架的精髓,咱們才能應對現在大前端如此迅猛的發展。


課題 2:搭建開發環境跟生產環境

本章提要:

目錄

│── build
│   │── base.js                 // 公共部分
│   │── build.js
│   └── dev.js
│── config
│   │── base.js                 // 基礎配置
│   │── css.js                  // css 配置
│   │── HtmlWebpackPlugin.js    // html 配置
│   └── MiniCssExtractPlugin.js // 提取css
│── public                      // 公共資源
│   └── index.html              // html 模版
└── src                         // 開發目錄
    │── style
    │ └── index.css
    └── main.js                // 主入口
複製代碼

實現可插拔配置

package.json

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development node build/dev.js",
    "build": "cross-env NODE_ENV=production node build/build.js"
  },
  "dependencies": {
    "cross-env": "^6.0.3",
    "css-loader": "^3.2.0",
    "cssnano": "^4.1.10",
    "ora": "^4.0.3",
    "rimraf": "^3.0.0",
    "webpack": "^4.41.2"
  },
  "devDependencies": {
    "extract-text-webpack-plugin": "^3.0.2",
    "html-webpack-plugin": "^3.2.0",
    "mini-css-extract-plugin": "^0.8.0",
    "vue-cli-plugin-commitlint": "^1.0.4",
    "webpack-chain": "^6.0.0",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0"
  }
}
複製代碼

build/base.js

const { findSync } = require('../lib');
const Config = require('webpack-chain');
const config = new Config();
const files = findSync('config');
const path = require('path');
const resolve = p => {
  return path.join(process.cwd(), p);
};

module.exports = () => {
  const map = new Map();

  files.map(_ => {
    const name = _.split('/')
      .pop()
      .replace('.js', '');
    return map.set(name, require(_)(config, resolve));
  });

  map.forEach(v => v());

  return config;
};
複製代碼

構建生產環境

build/build.js

const rimraf = require('rimraf');
const ora = require('ora');
const chalk = require('chalk');
const path = require('path');
// 刪除 dist 目錄
rimraf.sync(path.join(process.cwd(), 'dist'));

const config = require('./base')();
const webpack = require('webpack');
const spinner = ora('開始構建項目...');
spinner.start();

webpack(config.toConfig(), function(err, stats) {
  spinner.stop();
  if (err) throw err;
  process.stdout.write(
    stats.toString({
      colors: true,
      modules: false,
      children: false,
      chunks: false,
      chunkModules: false
    }) + '\n\n'
  );

  if (stats.hasErrors()) {
    console.log(chalk.red('構建失敗\n'));
    process.exit(1);
  }

  console.log(chalk.cyan('build完成\n'));
});
複製代碼

構建開發環境(devServer)

build/dev.js

const config = require('./base')();
const webpack = require('webpack');
const chalk = require('chalk');
const WebpackDevServer = require('webpack-dev-server');
const port = 8080;
const publicPath = '/common/';

config.devServer
  .quiet(true)
  .hot(true)
  .https(false)
  .disableHostCheck(true)
  .publicPath(publicPath)
  .clientLogLevel('none');

const compiler = webpack(config.toConfig());
// 拿到 devServer 參數
const chainDevServer = compiler.options.devServer;
const server = new WebpackDevServer(
  compiler,
  Object.assign(chainDevServer, {})
);

['SIGINT', 'SIGTERM'].forEach(signal => {
  process.on(signal, () => {
    server.close(() => {
      process.exit(0);
    });
  });
});
// 監聽端口
server.listen(port);

new Promise(() => {
  compiler.hooks.done.tap('dev', stats => {
    const empty = ' ';
    const common = `App running at: - Local: http://127.0.0.1:${port}${publicPath}\n`;
    console.log(chalk.cyan('\n' + empty + common));
  });
});
複製代碼

提取 css

config/css.js

css 提取 loader 配置

module.exports = (config, resolve) => {
  return (lang, test) => {
    const baseRule = config.module.rule(lang).test(test);
    const normalRule = baseRule.oneOf('normal');
    applyLoaders(normalRule);
    function applyLoaders(rule) {
      rule
        .use('extract-css-loader')
        .loader(require('mini-css-extract-plugin').loader)
        .options({
          publicPath: './'
        });
      rule
        .use('css-loader')
        .loader('css-loader')
        .options({});
    }
  };
};
複製代碼

css 提取插件 MiniCssExtractPlugin

config/MiniCssExtractPlugin.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = (config, resolve) => {
  return () => {
    config
      .oneOf('normal')
      .plugin('mini-css-extract')
      .use(MiniCssExtractPlugin);
  };
};
複製代碼

自動生成 html

config/HtmlWebpackPlugin.js

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = (config, resolve) => {
  return () => {
    config.plugin('html').use(HtmlWebpackPlugin, [
      {
        template: 'public/index.html'
      }
    ]);
  };
};
複製代碼

項目測試

測試 html 模板

public/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>learn_webpack</title>
  <body></body>
</html>
複製代碼

測試 css 模板

src/style/index.css

.test {
  width: 200px;
  height: 200px;
  color: red;
  background-color: orange;
}
複製代碼

程序入口

src/main.js

require('./style/index.css');

const h2 = document.createElement('h2');
h2.className = 'test';
h2.innerText = 'test';
document.body.append(h2);
複製代碼

課題 3:基礎配置之loader

本章提要:

目錄

增長如下文件

│──── config                // 配置目錄
│   │── babelLoader.js      // babel-loader 配置
│   │── ForkTsChecker.js    // ts 靜態檢查
│   │── FriendlyErrorsWebpackPlugin.js // 友好錯誤提示
│   └── style
│──── src                   // 開發目錄
│   │── style
│   │  │── app.css
│   │  │── index.less       // 測試 less
│   │  │── index.scss       // 測試 sass
│   │  └── index.postcss    // 測試 postcss
│   └── ts
│     └── index.ts          // 測試 ts
│── babel.js
│── postcss.config.js       // postcss 配置
│── tsconfig.json           // ts 配置
└──── dist                  // 打包後的目錄
   │── app.bundle.js
   │── app.css
   └── index.html
複製代碼

配置 babel

config/babelLoader.js

module.exports = (config, resolve) => {
  const baseRule = config.module.rule('js').test(/.js│.tsx?$/);
  const babelPath = resolve('babel.js');
  const babelConf = require(babelPath);
  const version = require(resolve('node_modules/@babel/core/package.json'))
    .version;
  return () => {
    baseRule
      .use('babel')
      .loader(require.resolve('babel-loader'))
      .options(babelConf({ version }));
  };
};
複製代碼

使用 babel 配置 ts

這裏咱們使用 babel 插件 @babel/preset-typescriptts 轉成 js,並使用 ForkTsCheckerWebpackPluginForkTsCheckerNotifierWebpackPlugin 插件進行錯誤提示。

babel.js

module.exports = function(api) {
  return {
    presets: [
      [
        '@babel/preset-env',
        {
          targets: {
            chrome: 59,
            edge: 13,
            firefox: 50,
            safari: 8
          }
        }
      ],
      [
        '@babel/preset-typescript',
        {
          allExtensions: true
        }
      ]
    ],
    plugins: [
      '@babel/plugin-transform-typescript',
      'transform-class-properties',
      '@babel/proposal-object-rest-spread'
    ]
  };
};
複製代碼

ts 靜態類型檢查

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin');

module.exports = (config, resolve) => {
  return () => {
    config.plugin('ts-fork').use(ForkTsCheckerWebpackPlugin, [
      {
        // 將async設爲false,能夠阻止Webpack的emit以等待類型檢查器/linter,並向Webpack的編譯添加錯誤。
        async: false
      }
    ]);
    // 將TypeScript類型檢查錯誤以彈框提示
    // 若是fork-ts-checker-webpack-plugin的async爲false時能夠不用
    // 不然建議使用,以方便發現錯誤
    config.plugin('ts-notifier').use(ForkTsCheckerNotifierWebpackPlugin, [
      {
        title: 'TypeScript',
        excludeWarnings: true,
        skipSuccessful: true
      }
    ]);
  };
};
複製代碼

友好錯誤提示插件

config/FriendlyErrorsWebpackPlugin.js

const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');

module.exports = (config, resolve) => {
  return () => {
    config.plugin('error').use(FriendlyErrorsWebpackPlugin);
  };
};
複製代碼

配置樣式,style,css、less、sass、postcss 等

module.exports = (config, resolve) => {
  const createCSSRule = (lang, test, loader, options = {}) => {
    const baseRule = config.module.rule(lang).test(test);
    const normalRule = baseRule.oneOf('normal');
    normalRule
      .use('extract-css-loader')
      .loader(require('mini-css-extract-plugin').loader)
      .options({
        hmr: process.env.NODE_ENV === 'development',
        publicPath: '/'
      });
    normalRule
      .use('css-loader')
      .loader(require.resolve('css-loader'))
      .options({});
    normalRule.use('postcss-loader').loader(require.resolve('postcss-loader'));
    if (loader) {
      const rs = require.resolve(loader);
      normalRule
        .use(loader)
        .loader(rs)
        .options(options);
    }
  };

  return () => {
    createCSSRule('css', /\.css$/, 'css-loader', {});
    createCSSRule('less', /\.less$/, 'less-loader', {});
    createCSSRule('scss', /\.scss$/, 'sass-loader', {});
    createCSSRule('postcss', /\.p(ost)?css$/);
  };
};
複製代碼

postcss 配置

module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      unitToConvert: 'px',
      viewportWidth: 750,
      unitPrecision: 5,
      propList: ['*'],
      viewportUnit: 'vw',
      fontViewportUnit: 'vw',
      selectorBlackList: [],
      minPixelValue: 1,
      mediaQuery: false,
      replace: true,
      exclude: [],
      landscape: false,
      landscapeUnit: 'vw',
      landscapeWidth: 568
    }
  }
};
複製代碼

編譯先後 css 對比

src/style/index.less

/* index.less */
.test {
  width: 300px;
}
複製代碼

dist/app.css

/* index.css */
.test {
  width: 36.66667vw;
  height: 26.66667vw;
  color: red;
  background-color: orange;
}
/* app.css */
.test {
  font-size: 8vw;
}
/* index.less */
.test {
  width: 40vw;
}

/* index.scss */
.test {
  height: 40vw;
}
/* index.postcss */
.test {
  background: green;
  height: 26.66667vw;
}
複製代碼

配置 autoprefixer

自動添加 css 前綴

postcss.config.js

module.exports = {
  plugins: {
    autoprefixer: {
      overrideBrowserslist: [
        '> 1%',
        'last 3 versions',
        'iOS >= 8',
        'Android >= 4',
        'Chrome >= 40'
      ]
    }
  }
};
複製代碼

轉換前

/* index.css */
.test {
  width: 200px;
  height: 200px;
  color: red;
  display: flex;
  background-color: orange;
}
複製代碼

轉換後

/* index.css */
.test {
  width: 26.66667vw;
  height: 26.66667vw;
  color: red;
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  background-color: orange;
}
複製代碼

開啓 source map

config.devtool('cheap-source-map');
複製代碼
└── dist
  │── app.bundle.js
  │── app.bundle.js.map
  │── app.css
  │── app.css.map
  └── index.html
複製代碼

在源文件下會有一行註釋,證實開啓了 sourcemap

/*# sourceMappingURL=app.css.map*/
複製代碼

課時 4:webpack性能優化

本章講解

  1. 分離 Manifest
  2. Code Splitting(代碼分割)
  3. Bundle Splitting(打包分割)
  4. Tree Shaking(刪除死代碼)
  5. 開啓 gzip

分離 Manifest

module.exports = (config, resolve) => {
  return () => {
    config
      .optimization
      .runtimeChunk({
        name: "manifest"
      })
  }
}
複製代碼

Code Splitting

  1. 使用動態 import 或者 require.ensure 語法,在第一節已經講解
  2. 使用 babel-plugin-import 插件按需引入一些組件庫

Bundle Splitting

將公共的包提取到 chunk-vendors 裏面,好比你require('vue'),webpack 會將 vue 打包進 chunk-vendors.bundle.js

module.exports = (config, resolve) => {
  return () => {
    config
      .optimization.splitChunks({
        chunks: 'async',
        minSize: 30000,
        minChunks: 1,
        maxAsyncRequests: 3,
        maxInitialRequests: 3,
        cacheGroups: {
          vendors: {
            name: `chunk-vendors`,
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
            chunks: 'initial'
          },
          common: {
            name: `chunk-common`,
            minChunks: 2,
            priority: -20,
            chunks: 'initial',
            reuseExistingChunk: true
          }
        }
      })
    config.optimization.usedExports(true)
  }
}
複製代碼

Tree Shaking

config/optimization.js

config.optimization.usedExports(true);
複製代碼

src/treeShaking.js

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}
複製代碼

在 main.js 中只引用了 cube

import { cube } from './treeShaking';

console.log(cube(2));
複製代碼

未使用 Tree Shaking

{
  "./src/treeShaking.js": function( module, __webpack_exports__, __webpack_require__ ) {
 "use strict";
    __webpack_require__.r(__webpack_exports__);
    __webpack_require__.d(__webpack_exports__, "square", function() {
      return square;
    });
    __webpack_require__.d(__webpack_exports__, "cube", function() {
      return cube;
    });
    function square(x) {
      return x * x;
    }
    function cube(x) {
      return x * x * x;
    }
  }
}
複製代碼

使用了 Tree Shaking

這裏只導出了 cube 函數,並無將 square 導出去

固然你能夠看見 square 函數仍是在 bundle 裏面,可是在壓縮的時候就會被幹掉了,由於它並無被引用

{
  "./src/treeShaking.js": function( module, __webpack_exports__, __webpack_require__ ) {
 "use strict";
    __webpack_require__.d(__webpack_exports__, "a", function() {
      return cube;
    });
    function square(x) {
      return x * x;
    }
    function cube(x) {
      return x * x * x;
    }
  }
}
複製代碼

只有當函數給定輸入後,產生相應的輸出,且不修改任何外部的東西,才能夠安全作shaking的操做

如何使用tree-shaking?

  1. 確保代碼是es6格式,即 export,import
  2. package.json中,設置 sideEffects
  3. 確保 tree-shaking 的函數沒有反作用
  4. babelrc中設置presets [["@babel/preset-env", { "modules": false }]] 禁止轉換模塊,交由webpack進行模塊化處理
  5. 結合uglifyjs-webpack-plugin

其實在 webpack4 咱們根本不須要作這些操做了,由於 webpack 在生產環境已經幫咱們默認添加好了,開箱即用!

開啓 gzip

CompressionWebpackPlugin.js

const CompressionWebpackPlugin = require('compression-webpack-plugin');

module.exports = (config, resolve) => {
  return () => {
    config.plugin('CompressionWebpackPlugin').use(CompressionWebpackPlugin, [
      {
        algorithm: 'gzip',
        test: /\.js(\?.*)?$/i,
        threshold: 10240,
        minRatio: 0.8
      }
    ]);
  };
};
複製代碼

課時 5:手寫loader實現可選鏈

本章內容

  1. 什麼是 webpack loader
  2. 可選鏈介紹
  3. loader 實現可選鏈

什麼是 webpack loader

webpack loaderwebpack 爲了處理各類類型文件的一箇中間層,webpack 本質上就是一個 node 模塊,它不能處理 js 之外的文件,那麼 loader 就幫助 webpack 作了一層轉換,將全部文件都轉成字符串,你能夠對字符串進行任意操做/修改,而後返回給 webpack 一個包含這個字符串的對象,讓 webpack 進行後面的處理。若是把 webpack 當成一個垃圾工廠的話,那麼 loader 就是這個工廠的垃圾分類!

可選鏈介紹

這裏並非純粹意義上的可選鏈,由於 babelts 都已經支持了,咱們也沒有必要去寫一個完整的可選鏈,只是來加深一下對 loader 的理解, loader 在工做當中能幫助咱們作什麼?

用途 當咱們訪問一個對象屬性時沒必要擔憂這個對象是 undefined 而報錯,致使程序不能繼續向下執行

解釋? 以前的全部訪問鏈路都是合法的,不會產生報錯

const obj = {
  foo: {
    bar: {
      baz: 2
    }
  }
}

console.log(obj.foo.bar?.baz) // 2
// 被轉成 obj && obj.foo && obj.foo.bar && obj.foo.bar.baz
console.log(obj.foo.err?.baz) // undefined
// 被轉成 obj && obj.foo && obj.foo.err && obj.foo.err.baz
複製代碼

loader 實現可選鏈

配置loader,options-chain-loader

config/OptionsChainLoader.js

module.exports = (config, resolve) => {
  const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
  const normalRule = baseRule.oneOf('normal');
  return () => {
    normalRule
      .use('options-chain')
      .loader(resolve('options-chain-loader'))
  }
}
複製代碼

其實就是正則替換,loader 將整個文件所有轉換成字符串,content 就是整個文件的內容,對 content 進行修改,修改完成後再返回一個新的 content 就完成了一個 loader 轉換。是否是很簡單?

下面的操做意思就是,咱們匹配 obj.foo.bar?. 並把它轉成 obj && obj.foo && obj.foo.bar && obj.foo.bar.

options-chain-loader.js

module.exports = function(content) {
  return content.replace(new RegExp(/([\$_\w\.]+\?\.)/,'g'),function(res) {
    let str  = res.replace(/\?\./,'');
    let arrs = str.split('.');
    let strArr = [];
    for(let i = 1; i <= arrs.length; i++) {
      strArr.push(arrs.slice(0,i).join('.')); 
    }
    let compile = strArr.join('&&');
    const done = compile + '&&' + str + '.'
    return  done;
  });
};
複製代碼

課時 6:webpack編譯優化

本章內容

  1. cache-loader
  2. DllPlugin
  3. threadLoader

cache-loader

cache-loader 主要是將打包好的文件緩存在硬盤的一個目錄裏,通常存在 node_modules/.cache 下,當你再次 build 的時候若是此文件沒有修改就會從緩存中讀取已經編譯過的文件,只有有改動的纔會被編譯,這樣就大大下降了編譯的時間。尤爲是項目越大時越明顯。

此項目使用先後數據對比 3342ms --> 2432ms 效果仍是比較明顯

這裏只對 babel 加入了 cache-loader,由於咱們的 ts/js 都是由 babel 進行編譯的,不須要對 ts-loader 緩存(咱們也沒有用到)

config/cacheLoader.js

module.exports = (config, resolve) => {
  const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
  const babelPath = resolve('babel.js')
  const babelConf = require(babelPath);
  const version = require(resolve('node_modules/@babel/core/package.json')).version
  return () => {
    baseRule
      .exclude
      .add(filepath => {
        // 不緩存 node_modules 下的文件
        return /node_modules/.test(filepath)
      })
      .end()
      .use('cache-loader')
      .loader('cache-loader')
      .options({
        // 緩存位置
        cacheDirectory: resolve('node_modules/.cache/babel')
      })
  }
}
複製代碼

DllPlugin

DllPlugin 是將第三方長期不變的包與實際項目隔離開來並分別打包,當咱們 build 時再將已經打包好的 dll 包引進來就 ok 了

我提取了兩個包 vue、react,速度差很少提高了 200ms,從 2698ms 到 2377ms

打包 dll

build/dll.js

const path = require("path");
const dllPath = path.join(process.cwd(), 'dll');
const Config = require('webpack-chain');
const config = new Config();
const webpack = require('webpack')
const rimraf = require('rimraf');
const ora = require('ora')
const chalk = require('chalk')
const BundleAnalyzerPlugin = require('../config/BundleAnalyzerPlugin')(config)

BundleAnalyzerPlugin()
config
  .entry('dll')
  .add('vue')
  .add('react')
  .end()
  .set('mode', "production")
  .output
  .path(dllPath)
  .filename('[name].js')
  .library("[name]")
  .end()
  .plugin('DllPlugin')
  .use(webpack.DllPlugin, [{
    name: "[name]",
    path: path.join(process.cwd(), 'dll', 'manifest.json'),
  }])
  .end()

rimraf.sync(path.join(process.cwd(), 'dll'))
const spinner = ora('開始構建項目...')
spinner.start()

webpack(config.toConfig(), function (err, stats) {
  spinner.stop()
  if (err) throw err
  process.stdout.write(stats.toString({
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false
  }) + '\n\n')

  if (stats.hasErrors()) {
    console.log(chalk.red('構建失敗\n'))
    process.exit(1)
  }
  console.log(chalk.cyan('build完成\n'))
})
複製代碼

將 dll 包合併

const webpack = require('webpack')

module.exports = (config, resolve) => {
  return () => {
    config.plugin('DllPlugin')
      .use(webpack.DllReferencePlugin, [{
        context: process.cwd(),
        manifest: require(resolve('dll/manifest.json'))
      }])
  }
}
複製代碼

threadLoader

測試效果變差了 😅,線程數越小編譯速度越快

config/threadLoader.js

module.exports = (config, resolve) => {
  const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
  return () => {
    const useThreads = true;
    if (useThreads) {
      const threadLoaderConfig = baseRule
        .use('thread-loader')
        .loader('thread-loader');
      threadLoaderConfig.options({ workers: 3 })
    }
  }
}
複製代碼

課時 7:多頁面配置

注意

  • 棄用 npm run build & npm run dev & npm run dll
  • 改爲 box build & box dev & box dll
  • link npm link 將 box 命令連接到全局

本章內容

  1. 使用
  2. 改造爲腳手架
  3. 多頁面配置

使用

box build # 不加參數則會編譯全部頁面,並清空 dist
box dev   # 默認編譯 index 頁面
複製代碼

參數

# index2 是指定編譯的頁面。不會清空 dist
# report 開啓打包分析
box build index2 --report 
box dev index2 --report 
複製代碼

改造爲腳手架

分紅三個命令,進行不一樣操做

  • build
  • dev
  • dll

bin/box.js

#!/usr/bin/env node

const chalk = require('chalk')
const program = require('commander')
const packageConfig = require('../package.json');
const { cleanArgs } = require('../lib')
const path = require('path')
const __name__ = `build,dev,dll`

let boxConf = {}
let lock = false

try {
  boxConf = require(path.join(process.cwd(), 'box.config.js'))()
} catch (error) { }

program
  .usage('<command> [options]')
  .version(packageConfig.version)
  .command('build [app-page]')
  .description(`構建開發環境`)
  .option('-r, --report', '打包分析報告')
  .option('-d, --dll', '合併差分包')
  .action(async (name, cmd) => {
    const options = cleanArgs(cmd)
    const args = Object.assign(options, { name }, boxConf)
    if (lock) return
    lock = true;
    if (boxConf.pages) {
      Object.keys(boxConf.pages).forEach(page => {
        args.name = page;
        require('../build/build')(args)
      })
    } else {
      require('../build/build')(args)
    }
  })

program
  .usage('<command> [options]')
  .version(packageConfig.version)
  .command('dev [app-page]')
  .description(`構建生產環境`)
  .option('-d, --dll', '合併差分包')
  .action(async (name, cmd) => {
    const options = cleanArgs(cmd)
    const args = Object.assign(options, { name }, boxConf)
    if (lock) return
    lock = true;
    require('../build/dev')(args)
  })

program
  .usage('<command> [options]')
  .version(packageConfig.version)
  .command('dll [app-page]')
  .description(`編譯差分包`)
  .action(async (name, cmd) => {
    const options = cleanArgs(cmd)
    const args = Object.assign(options, { name }, boxConf)
    if (lock) return
    lock = true;
    require('../build/dll')(args)
  })

program.parse(process.argv).args && program.parse(process.argv).args[0];
program.commands.forEach(c => c.on('--help', () => console.log()))

if (process.argv[2] && !__name__.includes(process.argv[2])) {
  console.log()
  console.log(chalk.red(` 沒有找到 ${process.argv[2]} 命令`))
  console.log()
  program.help()
}

if (!process.argv[2]) {
  program.help()
}
複製代碼

多頁面配置

box.config.js

module.exports = function (config) {
  return {
    entry: 'src/main.js', // 默認入口
    dist: 'dist', // 默認打包目錄
    publicPath: '/',
    port: 8888,
    pages: {
      index: {
        entry: 'src/main.js',
        template: 'public/index.html',
        filename: 'index.html',
      },
      index2: {
        entry: 'src/main.js',
        template: 'public/index2.html',
        filename: 'index2.html',
      }
    },
    chainWebpack(config) {
    }
  }
}
複製代碼

課時 8:手寫一個webpack插件

若是把 webpack 當成一個垃圾工廠,loader 就是垃圾分類,將全部垃圾整理好交給 webpack。plugin 就是如何去處理這些垃圾。

webpack 插件寫起來很簡單,就是你要知道各類各樣的鉤子在何時觸發,而後你的邏輯寫在鉤子裏面就ok了

  • apply 函數是 webpack 在調用 plugin 的時候執行的,你能夠認爲它是入口
  • compiler 暴露了和 webpack 整個生命週期相關的鉤子
  • Compilation 暴露了與模塊和依賴有關的粒度更小的事件鉤子

本節概要

實現一個 CopyPlugin

咱們今天寫一個 copy 的插件,在webpack構建完成以後,將目標目錄下的文件 copy 到另外一個目錄下

const fs = require('fs-extra')
const globby = require('globby')

class CopyDirWebpackPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    const opt = this.options
    compiler.plugin('done', (stats) => {
      if (process.env.NODE_ENV === 'production') {
        (async ()=>{
          const toFilesPath = await globby([`${opt.to}/**`, '!.git/**'])
          toFilesPath.forEach(filePath => fs.removeSync(filePath))
          const fromFilesPath = await globby([`${opt.from}/**`])
          fromFilesPath.forEach(fromPath => {
            const cachePath = fromPath
            fromPath = fromPath.replace('dist', opt.to)
            const dirpaths = fromPath.substring(0, fromPath.lastIndexOf('/'))
            fs.mkdirpSync(dirpaths)
            fs.copySync(cachePath, fromPath)
          })
          console.log(` 完成copy ${opt.from} to ${opt.to}`)
        })()
      }
    });
  }
}

module.exports = CopyDirWebpackPlugin
複製代碼

使用

將打包出來的 dist 目錄下的內容 copy 到 dist2 目錄下

const CopyPlugin = require('../webapck-plugin-copy');

module.exports = ({ config }) => {
  return () => {
    config.plugin('copy-dist')
      .use(CopyPlugin, [{
        from: 'dist',
        to: 'dist2'
      }])
  }
}
複製代碼

課時 9:構建 ssr

ssr 就是服務端渲染,作 ssr 的好處就是爲了處理 spa 的不足,好比 seo 優化,服務端緩存等問題。

今天主要用 react 的 ssr 來作一個簡單的實例,讓你們更清晰的入門

本章概要

建立 box build:ssr

老規矩,先來一個 box build:ssr 命令讓程序能夠執行

執行 box build:ssr 會調用 build/ssr 執行編譯

program
  .usage('<command> [options]')
  .version(packageConfig.version)
  .command('build:ssr [app-page]')
  .description(`服務端渲染`)
  .action(async (name, cmd) => {
    const options = cleanArgs(cmd);
    const args = Object.assign(options, { name }, boxConf);
    if (lock) return;
    lock = true;
    require('../build/ssr')(args);
  });
複製代碼

編譯 ssr

與其餘的編譯沒有什麼區別,值得住的是

  • target 指定爲 umd 模式
  • globalObject 爲 this
  • 入口改成 ssr.jsx
.libraryTarget('umd')
.globalObject('this')
複製代碼

build/ssr.js

module.exports = function(options) {
  const path = require('path');
  const Config = require('webpack-chain');
  const config = new Config();
  const webpack = require('webpack');
  const rimraf = require('rimraf');
  const ora = require('ora');
  const chalk = require('chalk');
  const PATHS = {
    build: path.join(process.cwd(), 'static'),
    ssrDemo: path.join(process.cwd(), 'src', 'ssr.jsx')
  };

  require('../config/babelLoader')({ config, tsx: true })();
  require('../config/HtmlWebpackPlugin')({
    config,
    options: {
      publicPath: '/',
      filename: 'client.ssr.html'
    }
  })();

  config
    .entry('ssr')
    .add(PATHS.ssrDemo)
    .end()
    .set('mode', 'development') // production
    .output.path(PATHS.build)
    .filename('[name].js')
    .libraryTarget('umd')
    .globalObject('this')
    .library('[name]')
    .end();

  rimraf.sync(path.join(process.cwd(), PATHS.build));
  const spinner = ora('開始構建項目...');
  spinner.start();

  webpack(config.toConfig(), function(err, stats) {
    spinner.stop();
    if (err) throw err;
    process.stdout.write(
      stats.toString({
        colors: true,
        modules: false,
        children: false,
        chunks: false,
        chunkModules: false
      }) + '\n\n'
    );

    if (stats.hasErrors()) {
      console.log(chalk.red('構建失敗\n'));
      process.exit(1);
    }
    console.log(chalk.cyan('build完成\n'));
  });
};
複製代碼

編譯 jsx 語法

由於咱們是用 react 寫的,避免不了會用到 jsx 語法,因此咱們須要在 babel-loader 中使用 @babel/preset-react

npm i @babel/preset-react -D
複製代碼

config/babelLoader.js

if (tsx) {
  babelConf.presets.push('@babel/preset-react');
}
複製代碼

入口區分服務端/客戶端

區分服務端跟客戶端分別渲染

const React = require("react");
const ReactDOM = require("react-dom");

const SSR = <div onClick={() => alert("hello")}>Hello world</div>;

if (typeof document === "undefined") {
  console.log('在服務端渲染')
  module.exports = SSR;
} else {
  console.log('在客戶端渲染')
  const renderMethod = !module.hot ? ReactDOM.render : ReactDOM.hydrate;
  renderMethod(SSR, document.getElementById("app"));
}
複製代碼

服務端渲染

  • 將打包出來的 static 文件夾做爲一個服務
  • 訪問 http://127.0.0.1:8080,進入服務端渲染的頁面
  • 再執行一遍 ssr.js 進行事件綁定
module.exports = function (options) {
  const express = require("express");
  const { renderToString } = require("react-dom/server");
  const chalk = require('chalk')
  
  const SSR = require("../static/ssr");
  const port = process.env.PORT || 8080;

  server(port);
  
  function server(port) {
    const app = express();
    app.use(express.static("static"));
    app.get("/", (req, res) =>
      res.status(200).send(renderMarkup(renderToString(SSR)))
    );

    const empty = ' '
    const common = `App running at: - Local: http://127.0.0.1:${port}\n`
      console.log(chalk.cyan('\n' + empty + common))
    
    app.listen(port, () => process.send && process.send("online"));
  }
  
  function renderMarkup(html) {
    return `<!DOCTYPE html> <html> <head> <title>Webpack SSR Demo</title> <meta charset="utf-8" /> </head> <body> <div id="app">${html}</div> <script src="./ssr.js"></script> </body> </html>`;
  }
}
複製代碼

小結

至此 ssr 已經結束了,其實全部看起來很高大上的技術都是從一點一滴積累起來的,只要咱們明白原理,你也能作出更優秀的框架


完結

這個可能大概寫了兩個多星期,天天寫一點點聚沙成塔,自我感受提高了很大,若是有興趣跟我一塊兒學習的同窗能夠來加我進羣,我在羣裏會天天組織不一樣的課題來學習。

接下來的課題大概是:

  • 手寫 vue-next 源碼
  • ts 從入門到放棄
  • node 入門到哭泣

哈哈,開玩笑,大概就是這樣,半個月差很少一個專題,若是你有好的專題也能夠一塊兒來討論


最後兩件小事

  1. 有想入羣的學習前端進階的加我微信 luoxue2479 回覆加羣便可
  2. 有寫錯的地方和更好的建議能夠在下面 留言,一塊兒討論
  3. 想看更多幹貨能夠關注個人公衆號【前端技匠】

相關文章
相關標籤/搜索