一步步从零开始用 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"));
}
复制代码

服务端渲染

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 入门到哭泣

哈哈,开玩笑,大概就是这样,半个月差很少一个专题,若是你有好的专题也能够一块儿来讨论

相关文章
相关标签/搜索