2019-11-30

開篇

很多人都或多或少使用過 webpack,但是很少有人能夠系統(tǒng)的學習 webpack 配置,遇到錯誤的時候就會一臉懵仍翰,不知道從哪查起憔鬼?性能優(yōu)化時也不知道能做什么龟劲,網(wǎng)上的優(yōu)化教程是不是符合自己的項目?等一系列問題轴或!本文從最基礎(chǔ)配置一步步到一個完善的大型項目的過程昌跌。讓你對 webpack 再也不會畏懼,讓它真正成為你的得力助手照雁!

本文從下面幾個課題來實現(xiàn)

  • 課題 1:<a href="#0_1">初探 webpack蚕愤?探究 webpack 打包原理</a>
  • 課題 2:<a href="#0_2">搭建開發(fā)環(huán)境跟生產(chǎn)環(huán)境</a>
  • 課題 3:<a href="#0_3">基礎(chǔ)配置之loader</a>
  • 課時 4:<a href="#0_4">webpack性能優(yōu)化</a>
  • 課時 5:<a href="#0_5">手寫loader實現(xiàn)可選鏈</a>
  • 課時 6:<a href="#0_6">webpack編譯優(yōu)化</a>
  • 課時 7:<a href="#0_7">多頁面配置</a>
  • 課時 8:<a href="#0_8">手寫一個webpack插件</a>
  • 課時 9:<a href="#0_9">構(gòu)建 ssr</a>

項目地址

https://github.com/luoxue-victor/learn_webpack/

我把每一課都切成了不同的分支,大家可以根據(jù)課時一步步學習

image

腳手架

npm i -g webpack-box

使用

webpack-box dev   # 開發(fā)環(huán)境
webpack-box build # 生產(chǎn)環(huán)境
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 中使用

{
  "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"
  }
}

使用

npm run build --report # 開啟打包分析

擴展配置

box.config.js

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) {
    }
  }
}

<a name="0_1">課題 1:初探 webpack饺蚊?探究 webpack 打包原理</a>

想要學好 webpack萍诱,我們首先要了解 webpack 的機制,我們先從js加載css開始學習污呼。

我們從下面這個小練習開始走進 webpack

index.js 中引入 index.css

const css = require('./index.css')
console.log(css)

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

我們?nèi)绾巫?webpack 識別 css 呢曙求,答案就在 webpack 給我們提供了 loader 機制碍庵,可以讓我們通過loader 將任意的文件轉(zhuǎn)成 webpack 可以識別的文件

本章主要講解

  1. <a href="#1_1">webpack 基礎(chǔ)配置</a>
  2. <a href="#1_2">解析 bundle 如何加載模塊</a>
  3. <a href="#1_3">動態(tài) import 加載原理</a>
  4. <a href="#1_4">使用 webpack-chain 重寫配置</a>
  5. <a href="#1_5">課時 1 小結(jié)</a>

<a name="1_1">webpack 基礎(chǔ)配置</a>

需要的依賴包

package.json

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack", // 開發(fā)環(huán)境
    "build": "cross-env NODE_ENV=production webpack" // 生產(chǎn)環(huán)境
  },
  "dependencies": {
    "cross-env": "^6.0.3", // 兼容各種環(huán)境
    "css-loader": "^3.2.0",
    "rimraf": "^3.0.0", // 刪除文件
    "webpack": "^4.41.2"
  },
  "devDependencies": {
    "webpack-cli": "^3.3.10"
  }
}

webpack 基礎(chǔ)配置

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;
}

<a name="1_2">解析 bundle 如何加載模塊</a>

我刪掉了一些注釋跟一些干擾內(nèi)容,這樣看起來會更清晰一點

  • bundle 是一個立即執(zhí)行函數(shù)悟狱,可以認為它是把所有模塊捆綁在一起的一個巨型模塊静浴。
  • 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');
  }
});

<a name="1_3">動態(tài) import 加載原理</a>

如果我們把 index.js 的 require 改成 import 會發(fā)生什么苹享?

我們知道 importrequire 的區(qū)別是,import 是動態(tài)加載只有在用到的時候才會去加載,而 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);

動態(tài)加載打包結(jié)果

除了正常的 bundle 之外焚挠,我們還可以看見一個 0.boundle.js

0.boundle.js 就是我們的動態(tài)加載的 index.css 模塊

|-- bundle.js
|-- 0.boundle.js

動態(tài)模塊

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;
        },"\`]
      `);
    }
  }
]);

動態(tài)模塊加載邏輯

我們再看下 dist/bundle.js

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

原理很簡單漓骚,就是利用的 jsonp 的實現(xiàn)原理加載模塊蝌衔,只是在這里并不是從 server 拿數(shù)據(jù)而是從其他模塊中

  1. 調(diào)用模塊時會在 window 上注冊一個 webpackJsonp 數(shù)組,window['webpackJsonp'] = window['webpackJsonp'] || []
  2. 當我們 import時蝌蹂,webpack 會調(diào)用 __webpack_require__.e(0) 方法噩斟,也就是 requireEnsure
  3. webpack 會動態(tài)創(chuàng)建一個 script 標簽去加載這個模塊,加載成功后會將該模塊注入到 webpackJsonp
  4. webpackJsonp.push 會調(diào)用 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) {
      // 執(zhí)行所有 promise 的 resolve 函數(shù)
      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);
    // ...
    // 動態(tài)加載模塊
    return Promise.all(promises);
  };

  var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || []);
  // 重寫數(shù)組 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");`);
  }
});

<a name="1_4">使用 webpack-chain 重寫配置</a>

我們用 webpack-chain 來寫 webpack 的配置剃允,原因是 webpack-chain 的方式更加靈活

官方解釋

webpack-chain 嘗試通過提供可鏈式或順流式的 API 創(chuàng)建和修改 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();

<a name="1_5">課時 1 小結(jié)</a>

至此課時 1 已經(jīng)結(jié)束了,我們主要做了以下事情

  1. webpack 基礎(chǔ)配置
  2. 將 css 通過 css-loader 打包進 js 中
  3. 解析 bundle 如何加載模塊的
  4. webpack 如何實現(xiàn)的動態(tài)加載模塊

學習一個工具我們不僅要看懂它的配置给郊,還要對它的原理一起了解营袜,只有學到框架的精髓,我們才能應對如今大前端如此迅猛的發(fā)展丑罪。


<a name="0_2">課題 2:搭建開發(fā)環(huán)境跟生產(chǎn)環(huán)境</a>

本章提要:

  • <a href="#2_1">目錄</a>
  • <a href="#2_2">實現(xiàn)可插拔配置</a>
  • <a href="#2_3">構(gòu)建生產(chǎn)環(huán)境</a>
  • <a href="#2_4">構(gòu)建開發(fā)環(huán)境(devServer)</a>
  • <a href="#2_5">提取 css</a>
  • <a href="#2_6">自動生成 html</a>
  • <a href="#2_7">項目測試</a>

<a name="2_1">目錄</a>

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

<a name="2_2">實現(xiàn)可插拔配置</a>

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;
};

<a name="2_3">構(gòu)建生產(chǎn)環(huán)境</a>

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('開始構(gòu)建項目...');
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('構(gòu)建失敗\n'));
    process.exit(1);
  }

  console.log(chalk.cyan('build完成\n'));
});

<a name="2_4">構(gòu)建開發(fā)環(huán)境(devServer)</a>

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 參數(shù)
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);
    });
  });
});
// 監(jiān)聽端口
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));
  });
});

<a name="2_5">提取 css</a>

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);
  };
};

<a name="2_6">自動生成 html</a>

config/HtmlWebpackPlugin.js

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

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

<a name="2_7">項目測試</a>

測試 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);

<a name="0_3">課題 3:基礎(chǔ)配置之loader</a>

本章提要:

  • <a href="#3_1">配置 babel</a>
  • <a href="#3_2">使用 babel 配置 ts</a>
  • <a href="#3_3">ts 靜態(tài)類型檢查</a>
  • <a href="#3_4">友好錯誤提示插件</a>
  • <a href="#3_5">配置樣式荚板,style,css吩屹、less跪另、sass、postcss 等</a>
  • <a href="#3_6">postcss 配置</a>
  • <a href="#3_7">編譯前后 css 對比</a>
  • <a href="#3_8">配置 autoprefixer</a>
  • <a href="#3_9">開啟 source map</a>

目錄

增加以下文件

│──── config                // 配置目錄
│   │── babelLoader.js      // babel-loader 配置
│   │── ForkTsChecker.js    // ts 靜態(tài)檢查
│   │── FriendlyErrorsWebpackPlugin.js // 友好錯誤提示
│   └── style
│──── src                   // 開發(fā)目錄
│   │── 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

<a name="3_1">配置 babel</a>

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 }));
  };
};

<a name="3_2">使用 babel 配置 ts</a>

這里我們使用 babel 插件 @babel/preset-typescriptts 轉(zhuǎn)成 js煤搜,并使用 ForkTsCheckerWebpackPlugin免绿、ForkTsCheckerNotifierWebpackPlugin 插件進行錯誤提示。

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'
    ]
  };
};

<a name="3_3">ts 靜態(tài)類型檢查</a>

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設(shè)為false擦盾,可以阻止Webpack的emit以等待類型檢查器/linter嘲驾,并向Webpack的編譯添加錯誤。
        async: false
      }
    ]);
    // 將TypeScript類型檢查錯誤以彈框提示
    // 如果fork-ts-checker-webpack-plugin的async為false時可以不用
    // 否則建議使用迹卢,以方便發(fā)現(xiàn)錯誤
    config.plugin('ts-notifier').use(ForkTsCheckerNotifierWebpackPlugin, [
      {
        title: 'TypeScript',
        excludeWarnings: true,
        skipSuccessful: true
      }
    ]);
  };
};

<a name="3_4">友好錯誤提示插件</a>

config/FriendlyErrorsWebpackPlugin.js

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

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

<a name="3_5">配置樣式辽故,style,css腐碱、less誊垢、sass、postcss 等</a>

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$/);
  };
};

<a name="3_6">postcss 配置</a>

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
    }
  }
};

<a name="3_7">編譯前后 css 對比</a>

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;
}

<a name="3_8">配置 autoprefixer</a>

自動添加 css 前綴

postcss.config.js

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

轉(zhuǎn)換前

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

轉(zhuǎn)換后

/* 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;
}

<a name="3_9">開啟 source map</a>

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*/

<a name="0_4">課時 4:webpack性能優(yōu)化</a>

本章講解

  1. <a href="#4_1">分離 Manifest</a>
  2. <a href="#4_2">Code Splitting(代碼分割)</a>
  3. <a href="#4_3">Bundle Splitting(打包分割)</a>
  4. <a href="#4_4">Tree Shaking(刪除死代碼)</a>
  5. <a href="#4_5">開啟 gzip</a>

<a href="#4_1">分離 Manifest</a>

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

<a href="#4_2">Code Splitting</a>

  1. 使用動態(tài) import 或者 require.ensure 語法喂走,在第一節(jié)已經(jīng)講解
  2. 使用 babel-plugin-import 插件按需引入一些組件庫

<a href="#4_3">Bundle Splitting</a>

將公共的包提取到 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)
  }
}

<a href="#4_4">Tree Shaking</a>

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 函數(shù)芋肠,并沒有將 square 導出去

當然你可以看見 square 函數(shù)還是在 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;
    }
  }
}

只有當函數(shù)給定輸入后帖池,產(chǎn)生相應的輸出秒咐,且不修改任何外部的東西,才可以安全做shaking的操作

如何使用tree-shaking碘裕?

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

其實在 webpack4 我們根本不需要做這些操作了,因為 webpack 在生產(chǎn)環(huán)境已經(jīng)幫我們默認添加好了不撑,開箱即用文兢!

<a href="#4_5">開啟 gzip</a>

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
      }
    ]);
  };
};

<a name="0_5">課時 5:手寫loader實現(xiàn)可選鏈</a>

本章內(nèi)容

  1. <a href="#5_1">什么是 webpack loader</a>
  2. <a href="#5_2">可選鏈介紹</a>
  3. <a href="#5_3">loader 實現(xiàn)可選鏈</a>

<a name="5_1">什么是 webpack loader</a>

webpack loaderwebpack 為了處理各種類型文件的一個中間層,webpack 本質(zhì)上就是一個 node 模塊焕檬,它不能處理 js 以外的文件姆坚,那么 loader 就幫助 webpack 做了一層轉(zhuǎn)換,將所有文件都轉(zhuǎn)成字符串实愚,你可以對字符串進行任意操作/修改兼呵,然后返回給 webpack 一個包含這個字符串的對象,讓 webpack 進行后面的處理腊敲。如果把 webpack 當成一個垃圾工廠的話击喂,那么 loader 就是這個工廠的垃圾分類!

<a name="5_2">可選鏈介紹</a>

這里并不是純粹意義上的可選鏈碰辅,因為 babelts 都已經(jīng)支持了懂昂,我們也沒有必要去寫一個完整的可選鏈,只是來加深一下對 loader 的理解没宾, loader 在工作當中能幫助我們做什么凌彬?

用途 當我們訪問一個對象屬性時不必擔心這個對象是 undefined 而報錯,導致程序不能繼續(xù)向下執(zhí)行

解釋? 之前的所有訪問鏈路都是合法的循衰,不會產(chǎn)生報錯

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

console.log(obj.foo.bar?.baz) // 2
// 被轉(zhuǎn)成 obj && obj.foo && obj.foo.bar && obj.foo.bar.baz
console.log(obj.foo.err?.baz) // undefined
// 被轉(zhuǎn)成 obj && obj.foo && obj.foo.err && obj.foo.err.baz

<a name="5_3">loader 實現(xiàn)可選鏈</a>

配置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 將整個文件全部轉(zhuǎn)換成字符串会钝,content 就是整個文件的內(nèi)容原探,對 content 進行修改,修改完成后再返回一個新的 content 就完成了一個 loader 轉(zhuǎn)換。是不是很簡單咽弦?

下面的操作意思就是徒蟆,我們匹配 obj.foo.bar?. 并把它轉(zhuǎn)成 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;
  });
};

<a name="0_6">課時 6:webpack編譯優(yōu)化</a>

本章內(nèi)容

  1. <a href="#6_1">cache-loader</a>
  2. <a href="#6_2">DllPlugin</a>
  3. <a href="#6_3">threadLoader</a>

<a name="6_1">cache-loader</a>

cache-loader 主要是將打包好的文件緩存在硬盤的一個目錄里,一般存在 node_modules/.cache 下型型,當你再次 build 的時候如果此文件沒有修改就會從緩存中讀取已經(jīng)編譯過的文件段审,只有有改動的才會被編譯,這樣就大大降低了編譯的時間闹蒜。尤其是項目越大時越明顯寺枉。

此項目使用前后數(shù)據(jù)對比 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')
      })
  }
}

<a name="6_2">DllPlugin</a>

DllPlugin 是將第三方長期不變的包與實際項目隔離開來并分別打包姥闪,當我們 build 時再將已經(jīng)打包好的 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('開始構(gòu)建項目...')
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('構(gòu)建失敗\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'))
      }])
  }
}

<a name="6_3">threadLoader</a>

測試效果變差了 ??,線程數(shù)越小編譯速度越快

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 })
    }
  }
}

<a name="0_7">課時 7:多頁面配置</a>

注意

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

本章內(nèi)容

  1. <a href="#7_1">使用</a>
  2. <a href="#7_2">改造為腳手架</a>
  3. <a href="#7_3">多頁面配置</a>

<a name="7_1">使用</a>

box build # 不加參數(shù)則會編譯所有頁面函喉,并清空 dist
box dev   # 默認編譯 index 頁面

參數(shù)

# index2 是指定編譯的頁面避归。不會清空 dist
# report 開啟打包分析
box build index2 --report 
box dev index2 --report 

<a name="7_2">改造為腳手架</a>

分成三個命令,進行不同操作

  • 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(`構(gòu)建開發(fā)環(huán)境`)
  .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(`構(gòu)建生產(chǎn)環(huán)境`)
  .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()
}

<a name="7_3">多頁面配置</a>

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) {
    }
  }
}

<a name="0_8">課時 8:手寫一個webpack插件</a>

如果把 webpack 當成一個垃圾工廠管呵,loader 就是垃圾分類梳毙,將所有垃圾整理好交給 webpack。plugin 就是如何去處理這些垃圾捐下。

webpack 插件寫起來很簡單账锹,就是你要知道各種各樣的鉤子在什么時候觸發(fā),然后你的邏輯寫在鉤子里面就ok了

  • apply 函數(shù)是 webpack 在調(diào)用 plugin 的時候執(zhí)行的坷襟,你可以認為它是入口
  • compiler 暴露了和 webpack 整個生命周期相關(guān)的鉤子
  • Compilation 暴露了與模塊和依賴有關(guān)的粒度更小的事件鉤子

本節(jié)概要

  • <a href="#8_1">實現(xiàn)一個 CopyPlugin</a>
  • <a href="#8_2">使用</a>

<a name="8_1">實現(xiàn)一個 CopyPlugin</a>

我們今天寫一個 copy 的插件牌废,在webpack構(gòu)建完成之后,將目標目錄下的文件 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

<a name="8_2">使用</a>

將打包出來的 dist 目錄下的內(nèi)容 copy 到 dist2 目錄下

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

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

<a name="0_9">課時 9:構(gòu)建 ssr</a>

ssr 就是服務(wù)端渲染啤握,做 ssr 的好處就是為了處理 spa 的不足鸟缕,比如 seo 優(yōu)化,服務(wù)端緩存等問題排抬。

今天主要用 react 的 ssr 來做一個簡單的實例懂从,讓大家更清晰的入門

本章概要

  • <a href="#8_1">創(chuàng)建 box build:ssr</a>
  • <a href="#8_1">編譯 ssr</a>
  • <a href="#8_1">編譯 jsx 語法</a>
  • <a href="#8_1">入口區(qū)分服務(wù)端/客戶端</a>
  • <a href="#8_1">服務(wù)端渲染</a>
  • <a href="#8_1">小結(jié)</a>

<a name="8_1">創(chuàng)建 box build:ssr</a>

老規(guī)矩,先來一個 box build:ssr 命令讓程序可以執(zhí)行

執(zhí)行 box build:ssr 會調(diào)用 build/ssr 執(zhí)行編譯

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

<a name="8_1">編譯 ssr</a>

與其他的編譯沒有什么區(qū)別蹲蒲,值得住的是

  • 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('開始構(gòu)建項目...');
  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('構(gòu)建失敗\n'));
      process.exit(1);
    }
    console.log(chalk.cyan('build完成\n'));
  });
};

<a name="8_1">編譯 jsx 語法</a>

因為我們是用 react 寫的番甩,避免不了會用到 jsx 語法,所以我們需要在 babel-loader 中使用 @babel/preset-react

npm i @babel/preset-react -D

config/babelLoader.js

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

<a name="8_1">入口區(qū)分服務(wù)端/客戶端</a>

區(qū)分服務(wù)端跟客戶端分別渲染

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

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

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

<a name="8_1">服務(wù)端渲染</a>

  • 將打包出來的 static 文件夾作為一個服務(wù)
  • 訪問 http://127.0.0.1:8080届搁,進入服務(wù)端渲染的頁面
  • 再執(zhí)行一遍 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>`;
  }
}

<a name="8_1">小結(jié)</a>

至此 ssr 已經(jīng)結(jié)束了缘薛,其實所有看起來很高大上的技術(shù)都是從一點一滴積累起來的窍育,只要我們明白原理,你也能做出更優(yōu)秀的框架


完結(jié)

這個可能大概寫了兩個多星期宴胧,每天寫一點點積少成多漱抓,自我感覺提升了很大,如果有興趣跟我一起學習的同學可以來加我進群恕齐,我在群里會每天組織不同的課題來學習乞娄。

接下來的課題大概是:

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

哈哈,開玩笑显歧,大概就是這樣仪或,半個月差不多一個專題,如果你有好的專題也可以一起來討論


最后兩件小事

  1. 有想入群的學習前端進階的加我微信 luoxue2479 回復加群即可
  2. 喜歡這篇文章的話士骤,點個 在看范删,讓更多的人看到
  3. 有寫錯的地方和更好的建議可以在下面 留言,一起討論
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拷肌,一起剝皮案震驚了整個濱河市到旦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌廓块,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件契沫,死亡現(xiàn)場離奇詭異带猴,居然都是意外死亡,警方通過查閱死者的電腦和手機懈万,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門拴清,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人会通,你說我怎么就攤上這事口予。” “怎么了涕侈?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵沪停,是天一觀的道長。 經(jīng)常有香客問我裳涛,道長木张,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任端三,我火速辦了婚禮舷礼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘郊闯。我一直安慰自己妻献,他們只是感情好蛛株,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著育拨,像睡著了一般谨履。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上至朗,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天屉符,我揣著相機與錄音,去河邊找鬼锹引。 笑死矗钟,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的嫌变。 我是一名探鬼主播吨艇,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼腾啥!你這毒婦竟也來了东涡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤倘待,失蹤者是張志新(化名)和其女友劉穎疮跑,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凸舵,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡祖娘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了啊奄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渐苏。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖菇夸,靈堂內(nèi)的尸體忽然破棺而出琼富,到底是詐尸還是另有隱情,我是刑警寧澤庄新,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布鞠眉,位于F島的核電站,受9級特大地震影響择诈,放射性物質(zhì)發(fā)生泄漏凡蚜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一吭从、第九天 我趴在偏房一處隱蔽的房頂上張望朝蜘。 院中可真熱鬧,春花似錦涩金、人聲如沸谱醇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽副渴。三九已至奈附,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間煮剧,已是汗流浹背斥滤。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留勉盅,地道東北人伴逸。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓锅铅,卻偏偏與公主長得像淮悼,于是被迫代替她去往敵國和親猴凹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 目錄第1章 webpack簡介 11.1 webpack是什么宰闰? 11.2 官網(wǎng)地址 21.3 為什么使用 web...
    lemonzoey閱讀 1,737評論 0 1
  • 寫在開頭 先說說為什么要寫這篇文章, 最初的原因是組里的小朋友們看了webpack文檔后, 表情都是這樣的: (摘...
    Lefter閱讀 5,292評論 4 31
  • 作者:小 boy (滬江前端開發(fā)工程師)本文原創(chuàng)移袍,轉(zhuǎn)載請注明作者及出處解藻。原文地址:https://www.smas...
    iKcamp閱讀 2,759評論 0 18
  • 1. 新建一個文件夾,命名為 webpack-cli , webpack-cli 就是你的項目名,項目名建議使用小...
    魯大師666閱讀 1,476評論 1 3
  • 看見你在做一套英語語法 猛一看還以為看到蔡健雅 一顆子彈用光速打在心中 整個身體被愛神踩在腳下 空氣因流經(jīng)你而變得...
    燉只小雞兒閱讀 433評論 4 7