webpack4.X 基礎知識筆記

目錄

1. WebPack 基礎知識

首先需要明白webpack爲什麼會出現,作用是什麼,總之他就是一個模塊打包工具;
commonJS的模塊導出方式爲:

function SiderBar() {
    var dom = document.getElementById('root');
    var siderbar = document.createElement('div');
    siderbar.innerHTML = 'siderbar';
    dom.append(siderbar);
}

module.exports = SiderBar

commonJS的模塊導入(引入)方式爲:

var SiderBar = require('./siderbar.js')

webpack是一個打包工具,,他能識別任何一個模塊引入的語法,包括ES Module(也就是 import export)、CommonJS 、CMD 、AMD都可以識別。webpack都能夠進行很好的翻譯。

2. Webpack 安裝、使用配置文件、淺析打包輸出的內容、Loader、使用 Loader 打包靜態資源(圖片篇)、使用 Loader 打包靜態資源(樣式篇)

2.1 安裝有兩種方式:全局安裝與局部安裝
  • 全局安裝:npm install webpack webpack-cli -g 不推薦使用,卸載npm uninstall webpack webpack-cli -g;
  • 局部安裝:npm install webpack webpack-cli --save-dev 或者npm install webpack webpack-cli -D是等價的。局部安裝的直接輸入webpack -v是不能運行這個命令的,可以通過 npx webpack -v去運行命令。npx會從項目的node_modul去找wenpack包,所以可以通過npx去查找我們在項目中的webpack包;可以在不同的項目中使用不同版本的webpack
    輸入npm info webpack可以查看包的信息,包括版本等信息。
  • 安裝指定版本的webpack:npm install webapck@版本號 webpack-cli -D
2.2 使用配置文件

webpack在進行打包的時候,首先會查找項目中是否存在配置文件webpack.config.js,如果沒有,他會使用默認的配置文件進行打包。
在項目的目錄下新建webpack.config.js文件,裏面輸入內容:

// 引入node核心模塊path
const path = require('path')

module.exports = {
    // 入口文件
    entry: './src/index.js',
    // 打包出的文件配置
    output: {
      // 文件名
      filename: 'bundle.js',
      //  打包後的文件放在哪個文件夾,是一個絕對路徑 
      //  __dirname就是webpack.config.js所在的當前目錄的路徑,改成bundle就是說,打包後的文件放在bundle文件夾中
      path: path.resolve(__dirname,'bundle')
    }
}

代碼的註釋很詳細,就不在重複;上面代碼就是說將我們項目中的/src/index.js文件進行打包到bundle文件夾下的bundle.js中。
一般情況,我們將我們的源代碼放在src目錄下。

需要注意的是: webpack中默認的配置文件是webpack.config.js,我們可以輸入命令npx webpack --config webpackconfig.js將默認的配置文件指向webpackconfig.js這個文件。

我們每次進行打包的時候,都需要輸入命令npm webpack,我們可以在webpack.config.js這個文件中進行配置我們允許的命令;如下面代碼:

  "scripts": {
    "bundle": "webpack"
  },

上面的配置就是當我們輸入npm run build的時候相當於輸入 npm webpack,進行打包我們的文件。這裏運行 npm webpack是不會進行全局檢測是否安裝了 webpack,而是從項目的node_module去查找。

補充:查看官網的guides中的Getting started裏面的內容

2.3 淺析打包輸出的內容

我們輸入命令npm run bundle或者npx webpack之後,控制檯會輸出下面的內容:

Hash: 7d4f5a28d798a39e7e58
Version: webpack 4.31.0
Time: 293ms
Built at: 2019-05-16 08:56:10
    Asset      Size  Chunks             Chunk Names
bundle.js  1.36 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js 870 bytes {0} [built]
[1] ./src/header.js 249 bytes {0} [built]
[2] ./src/siderbar.js 246 bytes {0} [built]
[3] ./src/content.js 240 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

Hash:是對應打包成功後文件的Hash值是唯一值;
bundle.js:我們打包出的文件;
Chunks:我們打包後的文件,每一個文件都會有一個自己的id值,這裏面存放該文件的id值跟與其他打包後的文件有關係的對應文件的id值;
Chunk Names:存放Chunks中每一個id值對應的名字。這裏的main,這裏的入口文件,對應的名字就是main;我們前面說到配置打包的入口文件:

    // 入口文件
    entry: './src/index.js',

其實是下面的簡寫形式:

    // 入口文件
    entry: {
      main: './src/index.js',
    },

WARNING in configuration:這裏的警告說,我們在進行打包的時候,沒有配置打包的模式與打包的環境;其實我們在沒有配置模式的情況下默認是production,如下面的代碼:

// 引入node核心模塊path
const path = require('path')
module.exports = {
    // 配置打包模式
    mode: 'production',
    // 入口文件
    entry: {
      main: './src/index.js',
    },
    // 打包出的文件配置
    output: {
      // 文件名
      filename: 'bundle.js',
      //  打包後的文件放在哪個文件夾,是一個絕對路徑 
      //  __dirname就是webpack.config.js所在的當前目錄的路徑,當前模塊的目錄名,改成bundle就是說,打包後的文件放在dist文件夾中
      path: path.resolve(__dirname,'dist')
    }
}

添加之後,再次進行打包就不會出現警告了:

Hash: 81601a0beb210cf86adc
Version: webpack 4.31.0
Time: 632ms
Built at: 2019-05-16 15:20:51
    Asset      Size  Chunks             Chunk Names
bundle.js  1.36 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js 870 bytes {0} [built]
[1] ./src/header.js 249 bytes {0} [built]
[2] ./src/siderbar.js 246 bytes {0} [built]
[3] ./src/content.js 240 bytes {0} [built]

這個production模式打包後的文件,會對文件進行壓縮,上面打包後的文件內容如下:打包爲一行;

!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)}([function(e,n,t){var r=t(1),o=t(2),u=t(3);new r,new o,new u},function(e,n){e.exports=function(){var e=document.getElementById("root"),n=document.createElement("div");n.innerHTML="header",e.append(n)}},function(e,n){e.exports=function(){var e=document.getElementById("root"),n=document.createElement("div");n.innerHTML="SiderBar",e.append(n)}},function(e,n){e.exports=function(){var e=document.getElementById("root"),n=document.createElement("div");n.innerHTML="content",e.append(n)}}]);

我們將打包模式配置成mode: 'development';打包後的內容就不會進行壓縮,內容如下:

/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
2.4 Loader解析

webpack本身只是識別js文件的打包,當我們需要進行打包比如圖片等文件時,需要在配置文件裏面進行配置;webpack在進行打包的時候,首先是直接打包js文件,然後其他的文件會通過配置文件裏面的module裏面進行查找對應文件的打包規則;如果我們需要對圖片文件進行打包,配置如下:

    module: {
      rules: [{
        test: /\.(png|jpe?g|gif)$/,
        use: {
          loader: 'file-loader'
        }
      }]
    },

然後記得進行安裝對應的loader;輸入命令:npm install file-loader --save-dev然後運行打包命令就可以了;
loader就是一個打包方案,只要不是.js文件,就需要進行配置打包的規則。

2.5 使用 Loader 打包靜態資源(圖片篇)、url-loader的使用

如果我們在打包靜態資源的時候,比如圖片,默認打包後的結果是會生成一個字符串爲圖片命名,我們現在希望我們打包之後他的文件名以及擴展名,也就是文件類型都是不會改變,可以進行如下配置:[name].[ext]分別代表的是之前文件的文件名以及之前文件的後綴,他其實就是一個webpack的一個佔位符。可以在官網的file-loader裏面可以看到很多佔位符的用法。

    module: {
        rules: [{
            test: /\.(png|jpe?g|gif)$/,
            use: {
                loader: 'file-loader',
                options: {
                  // 佔位符 Placeholders
                  name: '[name].[ext]'
                }
            }
        }]
    },

同時我們可以設置打包後輸出的的文件夾的位置:下面是將圖片文件打包在images文件夾中。可以查看官網的文檔裏面file-loader的相關配置。

    module: {
        rules: [{
            test: /\.(png|jpe?g|gif)$/,
            use: {
                loader: 'file-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'images/'
                }
            }
        }]
    },
url-loader 的使用:

url-loader可以進行上面的打包,但是唯一不同的是,url-loader是將圖片直接打包成一個base:64的字符串,進行顯示在src中。
在這裏插入圖片描述
這個會有一些問題,是減少了發送http請求,但是如果文件很大,加載的時間也是很長,頁面會顯示空白。如果圖片小,可以使用這種方式進行打包。如果圖片很大,需要將圖片通過file-loader打包到一個文件夾,這樣更適合項目。其實在url-loader中已經考慮到了,我們可以進行添加一個配置進行限制:limit: 2048意思就是如果圖片超過2048個字節,也就是2kb就將圖片打包到imagees文件夾中,如果小於,就直接生成一個base:64的字符串進行顯示。

    module: {
        rules: [{
            test: /\.(png|jpe?g|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'images/',
                    limit: 2048
                }
            }
        }]
    },

可以查看官網API進行深入的研究這兩個loader

2.6 使用 Loader 打包靜態資源(樣式篇)

首先需要進行配置打包css文件的打包配置,需要使用style-loader、css-loader兩個loader進行打包。其中css-loader他是可以解析我們css文件之間的依賴關係,比如我們的index.css中引用了avatar.css文件,如下:@import './avatar.css';他就會進行解析,之間的依賴關係,最終將這些文件打包到一個文件中。style-loader的作用是,當得到css-loader打包後的內容之後,會將這些掛載到頁面的header部分,也就是放在了style標籤中。配置如下:

    module: {
        rules: [{
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
        }]
    },

如果我們需要打包.sass、scss這些其他css文件,我們需要引入sass-loader進行打包:npm install sass-loader node-sass webpack --save-dev如下的配置:

    module: {
        rules: [ {
            test: /\.scss$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader'
            ]
        }]
    },

其實loader的執行是有先後順序的:從上到下、從右到左;比如上面的配置,首先是通過sass-loader進行處理,然後通過css-loader進行處理文件關係,最後通過style-loader進行掛載到頁面。

2.6.1 對於 css 打包進行添加廠商前綴,使用postcss-loader自動添加廠商前綴:

首先進行安裝npm i -D postcss-loader;然後新建postcss.config.js文件,然後進行安裝一個autoprefixer插件,輸入命令:npm install autoprefixer -Dpostcss.config.js文件配置如下:

module.exports = {
    plugins: [
      require('autoprefixer')
    ]
}

最後需要記得在打包配置裏面進行添加postcss-loader配置項:

    module: {
        rules: [{
            test: /\.scss$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader',
                'postcss-loader'
            ]
        }]
    },

其實我們的廠商前綴是由postcss-loader裏面我們添加的autoprefixer插件給我們添加的廠商前綴。

2.6.2 css-loader 中一些常用的配置項:

我們在進行配置loader的時候,如果裏面需要配置參數,那麼將loader寫成一個對象,而不是數組裏面的字符串,我們給css-loader添加一個選項:importLoaders: 2,如下面代碼:importLoaders: 2參數的意思是:如果我們在js文件裏面通過import語句進行引入scss文件是可以進行打包的,按照我們前面說的,從下往上執行loader,但是如果我們的在js文件中引入的scss文件中還通過import引入了其他scss文件,那麼這裏的scss文件在進行打包的時候不會進行類似在js文件中scss打包的過程從下往上執行loader,所以我們需要添加配置項importLoaders: 2,意思就是我們在打包scss文件的時候(或者css)需要執行前面兩個loader,這樣就保證了不管scss文件(或者css)中通過import引入多少其他的scss文件(或者css),都能夠正常的打包,都會依次從上到下依次執行loader進行打包。

    module: {
        rules: [{
            test: /\.scss$/,
            use: [
                'style-loader',
                {
                  loader: 'css-loader',
                  options: {
                    importLoaders: 2
                  }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }]
    },
2.6.3 css 文件模塊化打包:只在一個模塊內有效

我們在寫css、scss的時候會直接將他們在代碼中進行引入,如下:這會導致如果頁面創建元素,所有的元素的樣式都會受到影響,

import  './index.scss';
import avater from './avater.png';

import createAvatar from './createAvatar'

createAvatar();
// 打包圖片文件--avater爲打包後的文件名
var img = new Image();
img.src = avater;
img.classList.add(avater)
var dom = document.getElementById('root');
dom.append(img);

createAvatar.js文件中也是創建一個img標籤:

import avater from './avater.png'
function createAvatar () {
    var img = new Image();
    img.src = avater;
    img.classList.add('avater')
    var dom = document.getElementById('root');
    dom.append(img);
}
export default createAvatar

index.scss中,代碼如下:


body {
    .avater {
        height: 150px;
        width: 150px;
        transform: translate(100px, 100px);
    }
}

這樣寫會導致頁面中兩個圖片顯示都一樣,都使用了index.scss中的樣式;解決這個問題,就是我們可以在打包css這些文件的時候,實現模塊化打包,如下面代碼:
在打包的時候,進行配置打包爲模塊;modules: true意思就是開啓模塊化打包。

module.exports = {
    // 配置打包模式
    mode: 'development',
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    module: {
        rules: [{
            test: /\.scss$/,
            use: [
                'style-loader',
                {
                  loader: 'css-loader',
                  options: {
                    importLoaders: 2,
                    modules: true
                  }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }]
    },

然後在代碼中通過style.avater這種方法,進行添加樣式,這樣就不影響全部文件了。

import style from './index.scss';
import avater from './avater.png';

import createAvatar from './createAvatar'

createAvatar();
// 打包圖片文件--avater爲打包後的文件名
var img = new Image();
img.src = avater;
img.classList.add(style.avater)
var dom = document.getElementById('root');
dom.append(img);
2.6.4 打包字體文件:

我們項目裏面也經常會用到圖片庫中的小圖片,比如iconfont裏面的小圖片,我們查看iconfont.css發現,裏面引入了eot|ttf|svg|woff這些文件,如下面代碼:

@font-face {
    font-family: "iconfont";
    src: url('./font/iconfont.eot?t=1558162704401'); /* IE9 */
    src: url('./font/iconfont.eot?t=1558162704401#iefix') format('embedded-opentype'), /* IE6-IE8 */
    url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAASIAAsAAAAACQgAAAQ7AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCCfgqFVIRuATYCJAMMCwgABCAFhG0HWxvRBxHVoy2Q/SxwzuYE2hXq6ImKdq351ClK774P+U+55ftJYCczKxZYca2qhqqHlK24nUQMelK9FgLpdvvtgwTDKPAeOIriDMIYEisANjdD9/zrCMWSnOf9tMy0dKYX0l//czm9awvOD5TLXMuOegGGvYACGmPbAgq0QBL0luFVB7kbx4EAXIwsNBq16ABGoY8jAN1DB/cFS+goFcWEIdaCpQV60LG2WLoDYKX3ffpREAyQWBL61DaDGg5Anff0XpJ1r+50EPD7swBgVwEyIAugAB2FtjaQg1gWGZdtiqTqSgXBe8nrdXDEm+0fHkAkoAm6zBcT0+ISvCeLv2t8jAN/31cBVgAD/KeVEEN3tJC34tqkxgXGTXfu3rlOc+Hr8MTcgtOSZ+3EUtPhtPGde0E3oi52evhU0NLOz59jd2fHqMcOJzH9aVu+ZJ1uCLHUbjdt5HAiDzx61pbXhwd6tFHySNO01ZCj8/OxS00ix/7dJ0PLqOZyCt95MQyuS8liz5VIqjb2mDW2dlPIXA/rQrkT1MMuDA2uc6mtTW+mbvTjrn1LDzj2L9k7nXc2x/GEdvDtNJpV1zp2gypowHGWmss91q3j/kucZHGY9gDD0AZ06pO7BjY2dOwwyZt01y77UnvnPY7KoDOqqHeEt4hBq+jG0Q2HDWvo3qU1Wiwqad9EuDX31jEYFDiWezSPWuu6WdkBXdvGt4yfOiXOrmmLLgk3fnYYtWktF5e2b/3+HjE28ELhIt/x3G1z8/Kr68rbo2ub+FZxU6bGtUxo2zWhbvgrVVl9RW0Z2zqmcVRDu79wi7pB4of00ieV0apXehC7x4L0V6uIUZ1JeW/lSTsn1Ss7KmKZ/kpX76nOnVhdS//Izv3GW+tyT3G/NHRKGJQwMKEjSpfDB3r3Ghz6PXTSnDnclEwKxJNTvXY8GOzXzW/Qg3cPJnmPmsy2+BxQ37ELrJt6fsw7fT/z45fmG22bHfdtLL5pblGya8Ku8ZWrLf/OW/gez1jlt7tslshAHwCA91KekTNJ4nqek8v/ld8g8P38sHdA1T8mFADAS8u9QOzxXirAHAC7m3aCP4MPrFMUoa6XS+JZPC0pO4x+77XgihNe6K5gnvYKDVwIEEaLQOIjGmRMEqoQs8BCKAIrpjZwmRqvFkKMg4xKBZBhPgIS6AhI/F0BWaC7qEJ0g4XNN7AKDBm4ThSypZASuFU74yK4w/oDu1HSplGUX3xDU8LCcaFHfSGfNghd3aaTV0zIfcw436YX0aCZIozoNAyBIDM53KQ+RPLQNLroRfVGsVLqMy6CO1h/wG6UtNNcVObzNzQlLFyBKbO+kE/bOejUWgB6lSUQ5lx6n2/TE9HUKM0UwYgtGIwYgVw8yOEmtaNFJA8a0khDZfX4inh922CmT81okVHCriWZ/1FMit0iU0QrfosS7CXX8isp8K5UVQA=') format('woff2'),
    url('./font/iconfont.woff?t=1558162704401') format('woff'),
    url('./font/iconfont.ttf?t=1558162704401') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
    url('./font/iconfont.svg?t=1558162704401#iconfont') format('svg'); /* iOS 4.1- */
  }

我們如果直接使用webpack打包會提示報錯,無法打包那些後綴的文件,我們可以通過file-loader進行打包這些字體文件,在webpack.config.js裏面進行配置,如下:使得將字體文件打包到dist的相對路徑裏面,我們直接使用。

    module: {
        rules: [ {
            test: /\.(eot|ttf|svg|woff)$/,
            use: {
                loader: 'file-loader'
            }
        }]
    },

然後我們可以繼續深入,查看官網的document中的GUIDS裏面的Asset Management裏面的內容,還有loader裏面的sass-loader、css-loader、postcss-loader進行深入瞭解。

3. 使用Plugins讓打包更便捷

3. 1 html-webpack-plugin 插件的使用——將html進行打包,並根據設置的html模板進行打包

我們在打包的時候,每次打包之後,打包後的文件,並沒有index.html就是我們在src文件夾裏面寫的網頁,都需要我們進行復制到dist目錄裏面,很是麻煩,我們可以使用webpack插件,來幫助我們解決這個問題。
首先需要安裝這個插件:cnpm install --save-dev html-webpack-plugin,然後在webpack.config.js裏面進行配置,代碼如下:

const HtmlWebpakcPlugin = require('html-webpack-plugin')

module.exports = {
    // 配置打包模式
    mode: 'development',
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    plugins: [new HtmlWebpakcPlugin()],
    // 打包出的文件配置
    output: {
        // 文件名
        filename: 'bundle.js',
        //  打包後的文件放在哪個文件夾,是一個絕對路徑 
        //  __dirname就是webpack.config.js所在的當前目錄的路徑,當前模塊的目錄名,改成bundle就是說,打包後的文件放在dist文件夾中
        path: path.resolve(__dirname, 'dist')
    }
}

這樣,打包之後,會將我們的html文件也會進行打包,html-webpack-plugin這個插件在打包結束後,自動生成一個html文件,並把打包生成的js自動引入到這個html中。我們打開打包後的html文件,會發現,我們在裏面的html結構沒有顯示,比如我們在裏面寫的<div></div>沒有被打包顯示,這個時候,我們需要對html-webpack-plugin這個插件進行配置一個template選項,讓他根據模板進行打包我們的html文件,模板文件也就是我們需要打包的文件,也就是以模板文件進行打包。配置如下:

const HtmlWebpakcPlugin = require('html-webpack-plugin')

module.exports = {
    // 配置打包模式
    mode: 'development',
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    plugins: [new HtmlWebpakcPlugin({
      template: './src/index.html'
    })],
    // 打包出的文件配置
    output: {
        // 文件名
        filename: 'bundle.js',
        //  打包後的文件放在哪個文件夾,是一個絕對路徑 
        //  __dirname就是webpack.config.js所在的當前目錄的路徑,當前模塊的目錄名,改成bundle就是說,打包後的文件放在dist文件夾中
        path: path.resolve(__dirname, 'dist')
    }
}

Plugin可以在webpack運行到某一時刻的時候,幫你做一些事情,很類似vue的聲明週期函數;

3.2 clean-webpack-plugin 插件的使用——清除打包地址的文件

我們在查看我們打包後的內容,我們如果將output配置裏面的輸出文件名修改爲其他的,我們重新進行打包,會發現,打包是成功了,但是我們修改之前打包的js文件還是存在在dist文件夾中,我們需要做的是,每次打包之前,需要將dist中之前打包的文件先進行刪除,然後再次進行打包js文件。這個時候,我們需要引入一個插件clean-webpack-plugin,
首先輸入命令進行安裝:npm install clean-webpack-plugin -D
然後在webpack.config.js裏面進行配置,如下:new CleanWebpackPlugin()意思是每次打包之前刪除dist裏面所有的內容。

// 引入node核心模塊path
const path = require('path')
// 將我們寫的html文件,進行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
    // 配置打包模式
    mode: 'development',
    // 入口文件
    entry: {
        main: './src/index.js',
    },
    plugins: [new HtmlWebpakcPlugin({
      template: './src/index.html'
    }),new CleanWebpackPlugin()],
    // 打包出的文件配置
    output: {
        // 文件名
        filename: 'bundle.js',
        //  打包後的文件放在哪個文件夾,是一個絕對路徑 
        //  __dirname就是webpack.config.js所在的當前目錄的路徑,當前模塊的目錄名,改成bundle就是說,打包後的文件放在dist文件夾中
        path: path.resolve(__dirname, 'dist')
    }
}

這裏需要注意的是HtmlWebpakcPlugin是在打包之後運行的插件,CleanWebpackPlugin是在打包之前運行的插件。還有一個需要注意的是new CleanWebpackPlugin(['dist'])這裏的dist是不需要添加的,直接new CleanWebpackPlugin()就好了

4. Entry 與 Output 的基礎配置

4.1 多個文件的打包

我們在打包文件的時候,經常會遇到打包多個文件,而不是一個js文件,這時候我們需要修改我們的配置;比如,下面代碼,我們打包mainsub的文件:

    // 入口文件
    entry: {
        main: './src/index.js',
        sub: './src/index.js'
    },

如果我們還是用之前的打包輸出的配置;就會報錯,提示多個文件打包到一個文件的錯誤:

    // 打包出的文件配置
    output: {
        // 文件名
        filename: 'main.js',
        //  打包後的文件放在哪個文件夾,是一個絕對路徑 
        //  __dirname就是webpack.config.js所在的當前目錄的路徑,當前模塊的目錄名,改成bundle就是說,打包後的文件放在dist文件夾中
        path: path.resolve(__dirname, 'dist')
    }

這個時候我們需要配置輸出的文件,讓他根據入口配置的文件,也就是那個對象的鍵,根據鍵來進行打包輸出文件,配置如下:

    // 打包出的文件配置
    output: {
        // 文件名
        filename: '[name].js',
        //  打包後的文件放在哪個文件夾,是一個絕對路徑 
        //  __dirname就是webpack.config.js所在的當前目錄的路徑,當前模塊的目錄名,改成bundle就是說,打包後的文件放在dist文件夾中
        path: path.resolve(__dirname, 'dist')
    }

這樣,打包後輸出的文件分別爲main.jssub.js;這樣就完成了多個文件的打包。

4.2 將打包後的js文件在html中,通過配置cdn地址進行引入

我們經常會遇到,我們在打包我們的js代碼之後,會將js文件放在cdn服務器上,通過cdn服務器的地址來在頁面進行引入腳本。比如,我們希望在html引入js的時候地址前面加上cdn域名:http://cdn.com/main.js這樣。我們需要在output裏面進行配置一個publicPath的選項:

    // 打包出的文件配置
    output: {
        publicPath: 'http://cdn.com.cn',
        // 文件名
        filename: '[name].js',
        //  打包後的文件放在哪個文件夾,是一個絕對路徑 
        //  __dirname就是webpack.config.js所在的當前目錄的路徑,當前模塊的目錄名,改成bundle就是說,打包後的文件放在dist文件夾中
        path: path.resolve(__dirname, 'dist')
    }

打包後html的引入如下:

<script type="text/javascript" src="http://cdn.com.cn/main.js"></script>
<script type="text/javascript" src="http://cdn.com.cn/sub.js"></script>

進一步學習:查看官網的document裏的configuration中的output的配置參數。然後進行查看guides裏面的output management裏的內容。查看plugins裏面的htmlwebpackplugin插件,查看該插件對應的官網。

5. SourceMap 的配置

我們在前面配置了我們的打包模式 爲開發者模式:mode: 'development',在這個模式,默認SourceMap已經被配置進去了,我們可以關掉認SourceMap,配置如下:

    // 配置打包模式
    mode: 'development',
    devtool: 'none',

在這裏插入圖片描述
如果我們關閉了SourceMap,我們如果在寫代碼的時候,不小心製造一個小bug,比如說我們把console寫成了conssole這樣,在打包時,是不會出現問題的,但是在頁面的控制檯會提示,該方法不存在,我們點擊之後,提示打包後的文件的第153行報錯,其實我們真正 想知道的不是打包後文件哪裏出錯,而是我們在源代碼裏哪個位置出現錯誤;而SourceMap他是一個映射關係,他知道dist目錄下的main.js也就是我們打包生成的文件153行實際上對應是src目錄下index.js文件也就是我們的源代碼文件中的第58行。我們可以通過SourceMap獲取到index.js中第一行代碼出錯了;所以我們需要進行配置SourceMap,來根據SourceMap的映射關係,找到我們源代碼出錯位置:我們查看打包後的文件,會多出一個main.js.map的文件,就是我們的源文件,存放着映射關係。

    // 配置打包模式
    mode: 'development',
    devtool: 'source-map',

在這裏插入圖片描述
我們可以查看官網有關SourceMap的配置,也就是devtool的配置,其中如果將devtool配置成inline-cheap-source-map意思他會將打包後的SourceMap直接通過地址放在在打包的文件代碼的底部:

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vc3JjL2luZGV4LmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7O0FBR0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLGtEQUEwQyxnQ0FBZ0M7QUFDMUU7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxnRUFBd0Qsa0JBQWtCO0FBQzFFO0FBQ0EseURBQWlELGNBQWM7QUFDL0Q7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQ

這裏,需要說明的是,前面我們進行配置成source-map,點擊報錯,他會提示代碼報錯位置的行與列,其實我們在查看問題的時候,並不需要精確到哪一列代碼出現問題,只需要知道在哪一行;這時候我們可以將devtool配置成cheap-source-map,他只會提示代碼在哪一行報錯。
這裏的cheap-source-map他就會映射我們寫的業務代碼,比如打包的文件index.js而不會將我們在loder裏面引入的第三方模塊代碼的映射,如果需要映射loder裏面的的代碼需要將配置修改爲cheap-module-inline-source-map
還有一種打包方式就是eval,他是打包速度最快的,但是如果代碼過多,也會影響打包速度;

這裏建議我們配置sourceMap的打包方式,如果是在development環境進行打包的時候,將devtool配置成cheap-module-eval-source-map,提示的錯誤比較全,打包速度也是比較快。如果我們代碼已經上線,也就是在production模式下,我們可以將devtool配置成cheap-module-source-map,這樣提示效果會好一些。

6. 使用 WebpackDevServer 提升開發效率

我們現在每次進行打包的時候還要需要輸入打包命令,打包之後在將html打開,查看打包結果;這樣操作是比較麻煩的,我們需要的是可以監聽到我們改動的文件,然後自動進行重新打包,我們有三種方法進行配置;

6.1 第一種:我們可以在package.json進行配置,監聽文件變化
  "scripts": {
    "watch": "webpack --watch"
  },

他的意思是,會進行監聽我們打包的文件,只要打包文件發生變化,就會重新打包。

6.2 第二種:監聽打包文件變化,並自動啓動服務器,加載html文件

我們的第二種打包方式是,我們不僅需要監聽打包文件的變化並重新打包,而且需要在打包完成之後,直接打開我們的html文件,類似在服務器中。前提是首先安裝webpack-dev-server,輸入命令:npm install webpack-dev-server -D
我們可以在webpack.config.js添加一個devServer配置:

module.exports = {
    devServer: {
      // 服務器啓動的根路徑
      contentBase: './dist'
    }
}

然後在package.json裏面添加一個運行命令,來運行我們的服務器:

  "scripts": {
    "watch": "webpack --watch",
    "start": "webpack-dev-server"
  },

這時候會啓動一個服務器,webpack-dev-server比我們之前配置的webpack --watch這種方式的好處是,他不僅會監聽我們打包文件變化,並進行更新打包,而且也會重新刷新我們的網頁。我們還可以在webpack.config.js中的devServer配置添加一個open: true選項,他會在打開服務器後,自動打開瀏覽器,並自動訪問啓動的服務器地址,打開網頁,不用我們手動進行將服務器啓動後的地址輸入到瀏覽器進行打開:

    devServer: {
      // 服務器啓動的根路徑
      contentBase: './dist',
      open: true
    },

還有一個就是我們在使用vue-cli腳手架的時候,我們會在devServer裏面配置一個proxy選項,用來模擬後臺API請求地址或者轉發我們的接口API地址,解決跨域代理;如下面的配置:他的意思是當我們訪問localhost:8080/api地址的時候,他會自動將地址轉發到localhost:3000端口。

    devServer: {
      // 服務器啓動的根路徑
      contentBase: './dist',
      open: true,
      proxy: {
        '/api': 'http://localhost:3000'
      }
    },
6.3 第三種:自己手寫一個服務器,實現類似Webpack-dev-server的效果

首先在package.json裏面進行配置命令,使得運行命令之後,就運行我們的腳本文件:

  "scripts": {
    "middleware": "node server.js"
  },

我們可以藉助node的框架,去搭建服務器,這裏使用express框架來實現,首先進行安裝該框架:npm install express webpack-dev-middleware -D,我們這裏同時安裝了一個webapckwebpack-dev-middleware中間件,用來監聽我們打包文件的變化,來自動進行打包。
然後我們修改webpack.config.js裏面的output配置,添加一個publicPath: '/'將文件打包成功後的引用地址前面都加一個根路徑,確保打包生成文件的引用路徑不會出現錯誤。

    // 打包出的文件配置
    output: {
        // 文件引入的cnd地址
        // publicPath: 'http://cdn.com.cn',
        publicPath: '/',
        // 文件名
        filename: '[name].js',
        //  打包後的文件放在哪個文件夾,是一個絕對路徑 
        //  __dirname就是webpack.config.js所在的當前目錄的路徑,當前模塊的目錄名,改成bundle就是說,打包後的文件放在dist文件夾中
        path: path.resolve(__dirname, 'dist')
    }

然後我們的server.js代碼如下:

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
// 進行webpack編譯--執行編譯器,進行打包一次代碼,
// 在node中直接使用webpack
const complier = webpack(config);

const app = express();
// 只要打包的文件內容發生變化,就會重新打包,通過編譯器;將打包的文件放在跟webpack.config.js配置的一致
app.use(webpackDevMiddleware(complier, {
  publicPath: config.output.publicPath
}));

app.listen(3000, () => {
  console.log('server is run')
})

上面的我們是直接通過調用webpack配置在node裏面使用webpack,我們也可以直接在命令行裏去執行webpack,可以在官網的API裏面的Command Line Interface查找到很多的命令:比如webpack index.js -o main.jsindex.js打包成main.js輸出。如果需要查找在node裏面運行webpack,可以查看node.js APi裏面去查找。

深入學習:打開官網的documentation裏面的Guides中的development裏面的內容閱讀,然後查看configuration裏面有關devtool、devServer裏面的內容。

7. Hot Module Replacement 熱模塊更新

我們使用webpack-dev-server幫助我們進行打包,會發現不會產生一個dist,因爲他會生成一個打包文件,不過沒有放在文件裏面而是放在了計算機的內存中,可以有效提升打包的速度。
我們在寫代碼的時候,經常會碰到,我們在修改了我們的css文件後,只是想每次在更新頁面的時候,只是將樣式文件重新加載,進行渲染,並不想讓整個頁面進行重新加載,破壞了我們頁面的狀態;我們想做的就是每次改完樣式文件之後,只是重新加載樣式文件,不破壞頁面的狀態;我們可以使用熱模塊更新來解決這個問題:我在webpack.config.js裏面的devServer配置項中添加一個hot: true(讓webpack-dev-server開啓熱模塊更新的功能)選項跟hotOnly:true(即便是HTML的功能沒有生效,也不讓瀏覽器自動重新刷新,)

    devServer: {
      // 服務器啓動的根路徑
      contentBase: './dist',
      open: true,
      proxy: {
        '/api': 'http://localhost:3000'
      },
      hot: true,
      hotOnly: true
    },

然後我們在webpack.config.js中引入webpack,在plugin裏面進行添加一個webpack的插件webpack.HotModuleReplacementPlugin();代碼如下:

// 引入node核心模塊path
const path = require('path')
// 將我們寫的html文件,進行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
const webpack = require('webpack');
module.exports = {
    //....
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpakcPlugin({
          template: './src/index.html'
        }),
        new webpack.HotModuleReplacementPlugin()
    ]
 }

這樣,如果只改了css樣式,頁面不會從新加載,只會重新加載css文件。
如果對於js文件,也想通過熱模塊更新,在webpack.config.js裏面的配置跟上面一樣,但是我們在代碼裏面需要進行判斷,加一些代碼邏輯,用來判斷是否開啓熱模塊更新,然後進行處理我們的邏輯:需要進行監控我們需要修改的文件

// 如果支持熱模塊加載
if(module.hot) {
    // 監控number文件,如果發生改變,就會執行裏面的代碼。
    // 第一個參數爲依賴的文件的名字,
  module.hot.accept('./number', ()=> {
    document.body.removeChild(document.getElementById('number'))
    number();
  })
}

查看官方文檔guides裏面的Hot Module Replacement中的內容深入瞭解,然後查看api查看Hot Module Replacement配置選項,然後進行查看concepts裏面的Hot Module Replacement內容,進行更深入的瞭解。

8. 使用 Babel 處理 ES6 語法

8.1 使用 Babel 按需打包js文件

首先進行安裝,輸入命令npm install --save-dev babel-loader @babel/core,然後在webpack.config.js增加一個規則,如下:代碼意思就是除了node_modules裏面的js內容不檢測,其他的js文件文件都通過babel-loader進行加載轉換。

module: {
  rules: [
    { 
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader" 
    }
  ]
}

其實babel-loader只是Babelwebpack之間的一個通訊工具,babel-loader並不會將es6語法翻譯爲es5語法,而babel/preset-env纔是真正轉換的工具。首選進行安裝,輸入命令:npm install @babel/preset-env --save-dev,然後進行配置在webpack.config.js

module: {
  rules: [
    { 
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader",
      options: {
         presets: ["@babel/preset-env"]
      }
    }
  ]
}

也可以新建一個.babelrc的文件,然後裏面增加配置,啓用一些插件。

{
  "presets": ["@babel/preset-env"]
}

這種轉換隻是一部分,低版本的有些函數還是不存在的,比如promise等函數,我們還需要藉助@babel/polyfill,進行對低版本瀏覽器對es6中這些函數的支持,輸入命令進行安裝:npm install --save @babel/polyfill,然後全局引入,放在業務代碼的最頂部,

import "@babel/polyfill";

這樣全局引入,會將所有es6的新增的函數打包到js文件中,這樣會使打包的文件體積增大。我們需要的是在該js文件中使用的es6函數的實現,在webpack.config.js進行配置;

module: {
  rules: [
    { 
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader",
      options: {
       presets: [["@babel/preset-env",{
         useBuiltIns: 'usage'
        }]]
      }  
    }
  ]
}

就會實現按需加載,如果js中有用到es6新增函數,會將對應的實現代碼打包到js中,而不是將全部es6新增的函數實現方式打包進去。

8.2 打包轉換中的其他參數(設置打包支持瀏覽器的版本、編寫類庫文件babel轉換配置)

我們在打包的選項中,還可以進行配置其他選項,比如可以進行配置打包支持瀏覽器的版本,babel可以根據打包後支持的瀏覽器,去進行判斷需要打包哪一些es6語法的實現函數,代碼如下:這句代碼是設置了打包後的爲高於谷歌67版本的瀏覽器。

module: {
  rules: [
    { 
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader",
      options: {
      presets: [["@babel/preset-env",{
        targets: {
           chrome: "67",
         },
         useBuiltIns: 'usage'
        }]]
      }  
    }
  ]
}

我們如果寫業務代碼需要使用babel轉換的時候,使用上面的配置就可以了,我們如果編寫一個類庫或者一個UI組件,需要進行不同的配置,配置步驟如下:
安裝plugin-transform-runtime,輸入命令:npm install --save-dev @babel/plugin-transform-runtime,然後進行安裝babel/runtime,輸入命令:npm install --save @babel/runtime,然後進行安裝babel/runtime-corejs2 ,輸入命令:npm install --save @babel/runtime-corejs2,然後在webpack.config.js進行配置;這樣配置的好處是,我們在使用babel轉換的時候,藉助polyfill在全局進行注入,這樣會污染全局,使用下面的配置,他會以閉包的形式,進行注入支持es6語法的函數。不存在全局污染。

    module: {
        rules: [{ 
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "babel-loader",
            options: {
            "plugins": [["@babel/plugin-transform-runtime",{
                "corejs": 2,
                "helpers": true,
                "regenerator": true,
                "useESModules": false
              }]]
            } 
        }]
    },

如果我們配置的options會很多的時候,可以在項目裏面新建一個.babelrc的文件,將options裏面的內容寫在這裏。然後去掉webpack.config.js中我們配置的babel-loaderoptions裏面的內容。

9. Webpack 實現對React框架代碼的打包

react首選需要安裝他,輸入命令npm install react react-dom --save,然後在我們的js文件寫react代碼,如下:

import "@babel/polyfill";

import React, {Component} from 'react'

import ReactDom from 'react-dom'

class App extends Component {
  return () {
    return <div>Hello World</div>
  }
}
ReactDom.render(<App />, document.getElementById('root'))

安裝babel/preset-react,進行轉換;輸入命令npm install --save-dev @babel/preset-react,然後在.babelrc文件中增加配置:

{
    presets: [
        [
            "@babel/preset-env", {
                targets: {
                    chrome: "67",
                },
                useBuiltIns: 'usage'
            }
        ],
        "@babel/preset-react"
    ]
}

這裏的插件執行是從下往上執行的,就是先進行打包react代碼,然後進行es6代碼的轉換。

10. Webpack 打包的一些坑

是遇到很多坑,比如使用了html-webpack-plugin插件,然後我們允許webpack-dev-server命令的時候,我們配置了打包入口的html模板,但是啓動服務之後,沒有頁面顯示,查看打包信息出現Entrypoint undefined = index.htmlwebpack.config.js配置如下:

// 引入node核心模塊path
const path = require('path')
// 將我們寫的html文件,進行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
const webpack = require('webpack');

module.exports = {
    // 配置打包模式
    mode: 'development',
    devtool: 'cheap-module-eval-source-map',
    // 入口文件
    entry: {
        main: './src/index.js',
        // sub: './src/index.js'
    },
    devServer: {
      // 服務器啓動的根路徑
      contentBase: './dist',
      open: true,
      proxy: {
        '/api': 'http://localhost:3000'
      },
      hot: true,
      hotOnly: true
    },
    module: {
        rules: [{ 
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "babel-loader"
        },
            {
            test: /\.(png|jpe?g|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'images/',
                    limit: 204800
                }
            }
        }, {
            test: /\.vue$/,
            use: {
                loader: 'vue-loader'
            }
        }, {
            test: /\.scss$/,
            use: [
                'style-loader',
                {
                  loader: 'css-loader',
                  options: {
                    importLoaders: 2
                  }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }, {
            test: /\.(eot|ttf|svg|woff)$/,
            use: {
                loader: 'file-loader'
            }
        }, {
            test: /\.css$/,
            use: [
                'style-loader',
                'css-loader',
                'postcss-loader'
            ]
        }]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new webpack.HotModuleReplacementPlugin(),
        new HtmlWebpakcPlugin({
            template: './src/index.html'
        }),
        
    ],
    optimization: {
      usedExports: true
    },
    // 打包出的文件配置
    output: {
        // 文件引入的cnd地址
        // publicPath: 'http://cdn.com.cn',
        publicPath: './',
        // 文件名
        filename: '[name].js',
        //  打包後的文件放在哪個文件夾,是一個絕對路徑 
        //  __dirname就是webpack.config.js所在的當前目錄的路徑,當前模塊的目錄名,改成bundle就是說,打包後的文件放在dist文件夾中
        path: path.resolve(__dirname, 'dist')
    }
}

後來經過測試,將輸出配置成如下:

    // 打包出的文件配置
    output: {
        path: path.resolve(__dirname, 'dist')
    }

這樣就可以了,不要配置,打包輸出的文件名以及地址就可以,雖然是解決了允許服務器沒有顯示頁面的問題,但是這個解決辦法還不是最佳。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章