整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          webpack5入門到實戰 (6-處理HTML資源)

          webpack5入門到實戰 (6-處理HTML資源)

          一)處理 Html 資源

          1. 下載包

          npm i html-webpack-plugin -D

          2. 配置

          ? Webpack.config.js

          const path=require('path')
          const ESLintWebpackPlugin=require('eslint-webpack-plugin')
          const HtmlWebpackPlugin=require('html-webpack-plugin')
          
          module.exports={
          //入口:相對路徑和絕對路徑都行
          entry: './src/main.js',
          //輸出
          output: {
          /**
          * path:文件輸出目錄,必須是絕對路徑
          * path.resolve方法返回一個絕對路徑
          * __dirname當前文件的文件夾絕對路徑
          */
          path: path.resolve(__dirname,'dist'),
          //文件輸出的名字 將 js 文件輸出到 static/js 目錄中
          filename: 'static/js/main.js',
          // 自動將上次打包目錄資源清空
          clean:true,
          },
          //加載器
          module: {
          rules: [
          ......
          ]
          },
          //插件
          plugins: [
          new ESLintWebpackPlugin({
          // 指定檢查文件的根目錄
          context: path.resolve(__dirname,'src')
          }),
          new HtmlWebpackPlugin({
          // 以 public/index.html 為模板創建文件
          // 新的html文件有兩個特點:1. 內容和源文件一致 2. 自動引入打包生成的js等資源
          template: path.resolve(__dirname,'public/index.html')
          })
          ],
          //模式
          mode:'development', //開發模式
          };

          ? public/index.html

          <!DOCTYPE html>
          <html lang="en">
          <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Document</title>
          </head>
          <body>
          <h1>Hello Webpack5</h1>
          <div class="box1"></div>
          <div class="box2"></div>
          <div class="box3"></div>
          <div class="box4"></div>
          <div class="box5"></div>
          </body>
          </html>

          編譯后dist 目錄就會輸出一個 index.html 文件

          (二)開發服務器&自動化

          每次寫完代碼都需要手動輸入指令才能編譯代碼,太麻煩了,我們希望一切自動化

          1. 下載包

          npm i webpack-dev-server -D

          2. 配置

          ? webpack.config.js

          const path=require('path')
          const ESLintWebpackPlugin=require('eslint-webpack-plugin')
          const HtmlWebpackPlugin=require('html-webpack-plugin')
          
          module.exports={
          //入口:相對路徑和絕對路徑都行
          entry: './src/main.js',
          //輸出
          output: {
          /**
          * path:文件輸出目錄,必須是絕對路徑
          * path.resolve方法返回一個絕對路徑
          * __dirname當前文件的文件夾絕對路徑
          */
          path: path.resolve(__dirname,'dist'),
          //文件輸出的名字 將 js 文件輸出到 static/js 目錄中
          filename: 'static/js/main.js',
          // 自動將上次打包目錄資源清空
          clean:true,
          },
          //加載器
          module: {
          rules: [
          ......
          ]
          },
          //插件
          plugins: [
          new ESLintWebpackPlugin({
          // 指定檢查文件的根目錄
          context: path.resolve(__dirname,'src')
          }),
          new HtmlWebpackPlugin({
          // 以 public/index.html 為模板創建文件
          // 新的html文件有兩個特點:1. 內容和源文件一致 2. 自動引入打包生成的js等資源
          template: path.resolve(__dirname,'public/index.html')
          })
          ],
          //模式
          mode:'development', //開發模式
          //開發服務器
          devServer:{
          host:'localhost', // 服務器地址
          port: '3000', //端口
          open: true, //是否自動開啟瀏覽器
          }
          };

          3. 啟動

          npx webpack serve

          注意:使用開發服務器時,所有代碼都會在內存中編譯打包,并不會輸出到 dist 目錄下

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

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

          • 課題 1:初探 webpack?探究 webpack 打包原理
          • 課題 2:搭建開發環境跟生產環境
          • 課題 3:基礎配置之loader
          • 課時 4:webpack性能優化
          • 課時 5:手寫loader實現可選鏈
          • 課時 6:webpack編譯優化
          • 課時 7:多頁面配置
          • 課時 8:手寫一個webpack插件
          • 課時 9:構建 ssr

          項目地址

          github.com/luoxue-vict…

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



          腳手架

          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 中使用

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

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

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

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

          在 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 會發生什么?

          我們知道 import 跟 require 的區別是,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 配置。API 的 Key 部分可以由用戶指定的名稱引用,這有助于跨項目修改配置方式的標準化。

          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:搭建開發環境跟生產環境

          本章提要:

          • 目錄
          • 實現可插拔配置
          • 構建生產環境
          • 構建開發環境(devServer)
          • 提取 css
          • 自動生成 html
          • 項目測試

          目錄

          │── 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

          本章提要:

          • 配置 babel
          • 使用 babel 配置 ts
          • ts 靜態類型檢查
          • 友好錯誤提示插件
          • 配置樣式,style,css、less、sass、postcss 等
          • postcss 配置
          • 編譯前后 css 對比
          • 配置 autoprefixer
          • 開啟 source map

          目錄

          增加以下文件

          │──── 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-typescript 將 ts 轉成 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'
           ]
           };
          };

          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 loader 是 webpack 為了處理各種類型文件的一個中間層,webpack 本質上就是一個 node 模塊,它不能處理 js 以外的文件,那么 loader 就幫助 webpack 做了一層轉換,將所有文件都轉成字符串,你可以對字符串進行任意操作/修改,然后返回給 webpack 一個包含這個字符串的對象,讓 webpack 進行后面的處理。如果把 webpack 當成一個垃圾工廠的話,那么 loader就是這個工廠的垃圾分類!

          可選鏈介紹

          這里并不是純粹意義上的可選鏈,因為 babel 跟 ts 都已經支持了,我們也沒有必要去寫一個完整的可選鏈,只是來加深一下對 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
          • 使用

          實現一個 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
          • 編譯 ssr
          • 編譯 jsx 語法
          • 入口區分服務端/客戶端
          • 服務端渲染
          • 小結

          創建 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 已經結束了,其實所有看起來很高大上的技術都是從一點一滴積累起來的,只要我們明白原理,你也能做出更優秀的框架


          本文作者:前端技匠

          原文鏈接:https://juejin.im/post/5de06aa851882572d672c1ad

          ebpack,作為前端構建的基石,其代碼分割功能為我們提供了優化網站性能的強大武器。今天,我們就來深入剖析 Webpack 代碼分割的奧秘,從入門到實戰,助你打造閃電般加載體驗!

          為什么需要代碼分割?

          想象一下,當你的網站代碼量越來越龐大,打包后的 JavaScript 文件也會越來越大。這會導致:

          • 頁面加載緩慢: 用戶需要等待很長時間才能看到頁面內容。
          • 網絡請求壓力大: 龐大的 JavaScript 文件會占用大量帶寬。
          • 資源浪費: 用戶可能只需要使用部分功能,但卻要加載所有代碼。

          Webpack 代碼分割 正是為了解決這些問題而生的!它可以將代碼分割成多個 chunk(代碼塊),按需加載,從而:

          • 加快首屏加載速度: 只加載首屏需要的代碼,其他代碼異步加載。
          • 減少網絡請求壓力: 只加載當前需要的代碼塊。
          • 優化緩存利用率: 每個代碼塊獨立緩存,更新代碼時只需加載更新的代碼塊。

          Webpack 代碼分割實戰:兩種常用方式


          1.entry配置:多入口,多頁面

          entry 配置是最基本的代碼分割方式,適用于多頁面應用。通過配置多個入口文件,Webpack 會自動將每個入口文件及其依賴打包成獨立的 chunk。

          示例代碼 (Webpack 配置):

          module.exports={
            // ...
            entry: {
              index: './src/index.js',
              about: './src/about.js',
            },
            output: {
              // ...
              filename: '[name].bundle.js', //  每個入口文件生成一個獨立的 bundle
            },
          };
          

          2.SplitChunksPlugin:提取公共代碼,優化緩存

          SplitChunksPlugin 是 Webpack 內置的代碼分割插件,可以自動分析代碼,提取多個 chunk 中的公共代碼。

          示例代碼 (Webpack 配置):

          module.exports={
            // ...
            optimization: {
              splitChunks: {
                chunks: 'all', //  所有類型的 chunk 都參與分割
                name: 'common', //  公共代碼塊命名為 'common'
              },
            },
          };
          

          進階技巧:動態導入,按需加載

          Webpack 支持使用 import() 語法進行動態導入,實現按需加載。

          示例代碼:

          //  點擊按鈕時才加載 lodash 庫
          button.addEventListener('click', ()=> {
            import('lodash').then((_)=> {
              console.log(_.join(['Hello', 'Webpack'], ' '));
            });
          });
          

          源碼解析:Webpack 代碼分割原理

          Webpack 會根據配置,將代碼分割成多個 chunk,每個 chunk 都有一個唯一的 ID。Webpack 會在生成的 HTML 文件中插入 <script> 標簽,異步加載其他 chunk。當瀏覽器執行到 import() 語句時,會根據 chunk ID 加載對應的代碼塊。

          總結

          Webpack 代碼分割是優化網站性能的利器,掌握它,你就能像魔法師一樣,將代碼分割成多個部分,按需加載,為用戶帶來閃電般加載體驗!

          #頭條創作挑戰賽#


          主站蜘蛛池模板: 亚洲福利一区二区| 无码精品人妻一区| 一区二区三区在线播放视频| 亚洲免费一区二区| 中文字幕日韩精品一区二区三区| 亚洲AV无码一区二区大桥未久| 国产一区二区三区免费看| 中文字幕亚洲综合精品一区| 四虎永久在线精品免费一区二区 | 国产精品无码一区二区三区不卡 | 亚洲日韩精品国产一区二区三区| 国产成人久久精品麻豆一区| 无码人妻一区二区三区兔费| 无码免费一区二区三区免费播放 | 亚洲av一综合av一区| 日本高清一区二区三区| 91精品国产一区| 91精品国产一区| 无码人妻精品一区二区三区久久 | 亚洲乱码一区二区三区在线观看 | 亚洲AV成人精品一区二区三区 | 影音先锋中文无码一区| 国产激情一区二区三区小说| 一区二区三区视频在线观看| 国产一区视频在线| 国产一区二区内射最近更新| 色视频综合无码一区二区三区| 国产精品区一区二区三| 中文字幕人妻AV一区二区| 国产91久久精品一区二区| 国产成人无码精品一区二区三区| 精品一区精品二区| 久久国产午夜精品一区二区三区| 视频在线一区二区| 国产精品被窝福利一区| 国产内射999视频一区| 久久一区二区三区精品| 亚洲无圣光一区二区 | 一区二区和激情视频| 国产香蕉一区二区三区在线视频| 国产成人综合一区精品|