前端工程化(三)

webpack 打包

模塊化開發為我們解決了很多問題,使得代碼組織管理非常的方便,但是又帶來了新的問題,ES Module 存在環境兼容問題,劃分的文件太多,就會導致網絡請求頻繁,不能保證所有資源的模塊化

如果能我們享受模塊化帶來的開發優勢,又能不必擔心生產環境的存在這些問題,于是就有了 webpack, rollup, Parcel 等工具
webpack 模塊化不等于 js ES modele 模塊,相對來講是前端的模塊化處理方案,更加宏觀

  • 快速上手
$ yarn init --yes
$ yarn add webpack webpack-cli -D
$ yarn webpack --version
$ yarn webpack // 默認打包src/index.js // 最終存放到dist/main.js
  • webpack 配置文件

在項目根目錄添加 webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/main.js', // 入口文件
  output: {
    filename: 'bundle.js', // 輸出文件名
    path: path.join(__dirname, 'output'), // 輸出文件路徑(絕對路徑)
  },
};
  • 工作模式

webpack4 新增了工作模式的用法,大大簡化了配置的復雜程度;三種工作模式 mode: production development none

$ webpack --mode none
$ webpack --mode production // 默認模式
$ webpack --mode development

或者采用配置的方式

const path = require('path');

module.exports = {
  // 這個屬性有三種取值,分別是 production、development 和 none。
  // 1. 生產模式下,Webpack 會自動優化打包結果;
  // 2. 開發模式下,Webpack 會自動優化打包速度,添加一些調試過程中的輔助;
  // 3. None 模式下,Webpack 就是運行最原始的打包,不做任何額外處理;
  mode: 'development',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
  },
};
  • 資源模塊加載

    webpack 內部的 loader 只能處理 js 文件,其他文件我們需要配置對應的 loader 才可以完成打包,否則會報錯。

const path = require('path');

module.exports = {
  mode: 'none',
  entry: './src/main.css',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /.css$/,
        // css-loader作用就是將css代碼轉化為js模塊
        // style-loader作用就是將cssloader轉化的結果追加到頁面
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};
  • 導入資源模塊
    入口文件為 js 文件,根據代碼的需要動態導入其他資源,由 javascript 驅動整個前端應用
const path = require('path');

module.exports = {
  mode: 'none',
  entry: './src/main.css',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

// main.js
import './main.css';
  • 文件資源加載器

    • file-loader
      經過 file-loader 處理后,將文件資源放到我們打包目錄的根目錄。返回文件資源的訪問路徑,通過 import 就可以拿到文件資源的路徑。webpack 默認認為文件資源放在網站的根目錄下
      會發起文件請求
const path = require('path');

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/',
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /.png$/,
        use: 'file-loader',
      },
    ],
  },
};
// main.js
import createHeading from './heading.js';
import './main.css';
import iconURL from './icon.png';
// 經過file-loader處理后,將圖片放到我們打包目錄的根目錄。返回圖片的訪問路徑,通過import就可以拿到圖片的路徑。webpack默認認為圖片放在網站的根目錄下
const heading = createHeading();

document.body.append(heading);

const img = new Image();
img.src = iconURL;

document.body.append(img);
  • url-loader
    將文件資源轉化為 Data Url, 最終返回這個 Data Url,不單獨生成資源文件,直接嵌入到 bundle.js 中
    當資源文件過大時,導致 base64 邊長,打包的 bundle.js 體積過大

    • Data URLs
      直接表示文件內容,使用這種 Url 不會發起 Http 請求

      data:[<mediatype>][;base64],<data>
      
      // 協議 + 媒體類型以及編碼+ 文件內容(圖片會被轉化為base64)
      

最佳實踐:小文件使用 Data URLs, 減少請求次數。大文件單獨提取,避免 bundle.js 過大,加載時間過長

module: {
  rules: [
    {
      test: /.css$/,
      use: ['style-loader', 'css-loader'],
    },
    {
      test: /.png$/,
      use: {
        // 必須同時安裝file-loader,當超過limit設置的值,url-loader會自動讓file-loader處理
        loader: 'url-loader',
        options: {
          limit: 10 * 1024, // 10 KB
        },
      },
    },
  ];
}
  • 常用 loader 分類

    • 編譯轉化類型

    • 文件操作類型

    • 代碼質量檢查

  • 處理 ES6+新特性

    webpack 只是打包工具 默認處理代碼中的 export 和 import,但對其他 ES6+新特性不做處理,需要 babel-loader

$ yarn add babel-loader @babel/core @babel/preset-env -D
// babel 只是一個js的轉換平臺。基于平臺通過不同的插件實現轉化

{
  "test": /.js$/,
  "use": {
    "loader": "babel-loader",
    "options": {
      "presets": ["@babel/preset-env"]
    }
  }
}
  • 模塊加載方式
    webpack 兼容多種標準的模塊加載方式

    • ES Module
    • CommonJs
    • AMD
    • import('XXX.css')
    • @import ()
    • @import url()
    • html 中的 img 的 src 屬性
    • background 屬性的 url
    • a 標簽的 herf 屬性
      ...
  • webpack 核心工作原理

    • 首先設置入口文件,webpack 會根據配置找到入口文件(如果不設置默認 src 下面的 index.js 文件)作為我們的打包入口

    • 根據代碼中出現的 import 或者 require 解析推斷出這個文件所依賴的其他資源模塊

    • 然后分別延伸解析每一個資源模塊對應的依賴,形成一個整個項目當中所有用的資源文件的依賴樹

    • 然后遞歸這個依賴樹,找到每個節點對應的資源文件,根據配置文件的 rules 屬性找到當前模塊的加載器(loader),然后交給加載器加載這個模塊

    • 最終將執行完成的結果放到 output 對應的 bundle.js 中

    • 在整個過程中,會通過 webpack 提供的鉤子函數(生命周期函數)加載對應的任務。這個任務我們也成 plugins

  • webpack 開發一個 loader
    原則: 對同一文件所用到的 loader 執行完成后, 最終必須返回 javascropt 代碼,也就是處理當前資源的最后的 loader 必須是返回 javascript 代碼

const path = require('path');

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/',
  },
  module: {
    rules: [
      {
        test: /.md$/,
        // 將 md 轉化為 html
        use: ['html-loader', './markdown-loader'],
      },
    ],
  },
};
// main.js
import about from './about.md';

console.log(about);
// markdown-loader.js
const marked = require('marked');

module.exports = source => {
  // source為加載進來的資源內容

  const html = marked(source);
  // 如果不交給下個loader處理

  // return `module.exports = "${html}"`
  // return `export default ${JSON.stringify(html)}`
  // 如果交給下個loader處理
  // 返回 html 字符串交給下一個 loader 處理
  return html;
};
  • 常用插件 Plugin

    clean-webpack-plugin
    每次打包前先清除 webpack 輸出目錄

    HtmlWebpackPlugin
    每次打包的文件自動生成 html 文件,自動引入打包結果

plugins: [
  new webpack.ProgressPlugin(),
  new CleanWebpackPlugin(),
  // 不額外添加模板的使用
  new HtmlWebpackPlugin({
    title: 'glh', // 設置標題
    meta: {
      // 設置meta標簽
      viewport: 'width=device-width',
    },
    // ...
  }),
];
// 添加模板,讓HtmlWebpackPlugin根據模板生成
new HtmlWebpackPlugin({
  title: 'glh', // 設置標題
  meta: {
    // 設置meta標簽
    viewport: 'width=device-width',
  },
  template: './public/index.html',
  templateParameters: {
    // 自定義變量
    BASE_URL: './',
  },
  // ...
});
<!--  public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>

  <body>
    <noscript>
      <strong
        >We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
        properly without JavaScript enabled. Please enable it to
        continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
// 用于生成index.html
new HtmlWebpackPlugin({
  template: './public/index.html',
  // ...
});
// 用于生成about.html
new HtmlWebpackPlugin({
  filename: 'about.html',
  // ...
});

copy-webpack-plugin
對一些公共資源文件直接復制到打包目錄中。比如 public/favicon.ico

new CopyWebpackPlugin({
  patterns: [{ from: 'public/favicon.ico', to: '.' }],
});

我們一般在使用插件的時候掌握一些經常用的就可以。后面根據需求再去提煉關鍵詞,搜索自己想用的插件,當然也可以自己寫。插件的約定名稱一般都是 XXX-webpack-plugin,比如我們想要壓縮圖片就可以找 imagemin-webpack-plugin

  • 實現一個自定義 plugin

首先要明白:

  • Plugin 其實就是通過在生命周期的鉤子中掛載函數實現擴展。類似于我們 React 中的聲明周期。
    webpack 在工作的過程中給每一個環節都埋下了鉤子,我們只需要在對應的鉤子下掛載任務就可以輕松的擴展 webpack 的能力

自定義的 Plugin 其實就是一個函數,或者包含 apply 的方法的對象
apply 方法接受一個 compiler 對象參數,這個參數包含我們整個構建過程中的所有配置信息,通過這個對象我們可以注冊鉤子函數,通過 tap 方法注冊任務
tap 方法又接受兩個參數,一個是插件名稱,一個是當前次打包執行的上下文

  • 和 loader 區別:loader 是專注實現資源模塊加載轉化
    Plugin 是解決處理資源加載轉化之外的的一些自動化工作
    相比于 Loader,Plugin 的能力范圍更寬
    因為 Loader 只是在加載模塊的范圍工作,而插件的工作范圍可以觸及到 webpack 的每一個環節
class MyPlugin {
  apply(compiler) {
    console.log('MyPlugin 啟動');
    // 這里要做的事情就是在emit鉤子上掛載一個任務,這個任務幫我們去除打包后沒有必要的注釋(mode=none情況下)其他鉤子可參考官網
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation => 可以理解為此次打包的上下文
      for (const name in compilation.assets) {
        // console.log(name)
        // console.log(compilation.assets[name].source())
        if (name.endsWith('.js')) {
          const contents = compilation.assets[name].source();
          const withoutComments = contents.replace(/\/\*\*+\*\//g, '');
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length,
          };
        }
      }
    });
  }
}
plugins: [new MyPlugin()];
  • 增強 webpack 的開發體驗
// 不使用Webpack Dev Server情況下,自動監聽打包文件的變化
$ yarn webpack --watch
$ http-server -c-1 dist //or $ browser-sync dist --file  "**/*"

以上方式效率太低,文件不斷的被讀寫操作,有待優化

  • Webpack Dev Server

    編寫源代碼=> webpack 打包=> 運行應用=> 刷新瀏覽器
    我們可以借助 Webpack Dev Server 來提升開發體驗,更接近生產環境的運行狀態,同時也可以設置 proxy,對于錯誤我們可以使用 souceMap 來快速定位源代碼問題

$ yarn add webpack-dev-server -D
$ yarm webpack-dev-server --open

webpack-dev-server 并不會將打包結果放到磁盤中,暫時存放到內存中,從臨時內存中讀取內容發送給瀏覽器,從而大大提高了效率

  • webpackDevServer 的靜態資源訪問
devServer: {
  contentBase: './public', //也可以指定數組標識多個目錄
}
  • 代理 proxy
    代理方式適用于后端沒有配置 cors 的情況
    如果我們的項目最終上線前后端代碼符合同源策略,也就沒必要設置 cors 了,這個時候可以通過本地服務器配置代理的方式實現跨域請求
  devServer: {
    proxy: {
      '/api': {
        // http://localhost:8080/api/users -> https://api.github.com/api/users
        target: 'https://api.github.com',
        // http://localhost:8080/api/users -> https://api.github.com/users
        pathRewrite: {
          '^/api': '' // 根據后端接口文件路勁因情況而定,這里只是用github舉例說明
        },
        // 不能使用 localhost:8080 作為請求 GitHub 的主機名
        changeOrigin: true
      }
    }
  }
// main.js;
// 跨域請求,雖然 GitHub 支持 CORS,但是不是每個服務端都應該支持。
// fetch('https://api.github.com/users')
fetch('/api/users') // http://localhost:8080/api/users
  .then(res => res.json())
  .then(data => {
    data.forEach(item => {
      const li = document.createElement('li');
      li.textContent = item.login;
      ul.append(li);
    });
  });
  • sourceMap

    由于編寫的代碼和運行的代碼不一致,sourceMap 幫我們定位源代碼錯誤
    webpack 提供了 12 中 sourceMap 方式。每種方式的效果和效率不同,效果最好的,效率最差,效果最差的,效率最高,因此我們只需要實際開發中符合需求的最佳實踐
    cheap: 定位到行,不定位列
    eval: 定位到文件
    module: 定位 loader 處理前的源代碼
    inline: 把 sourcemap 嵌入到打包文件中,不額外生成對應的.map 文件
    hidden: 會有錯誤信息,但是不是源文件。開發第三方包的時候可以用
    nosources: 看不到源代碼,但是會有行列信息,保護在生產環境中源代碼不被暴露

devtool: // 開發環境  'cheap-module-eval-source-map',
  // 生產環境 'none',
  // 如果對自己上線代碼沒有信心 'nosources-source-map'
const HtmlWebpackPlugin = require('html-webpack-plugin');

const allModes = [
  'eval',
  'cheap-eval-source-map',
  'cheap-module-eval-source-map',
  'eval-source-map',
  'cheap-source-map',
  'cheap-module-source-map',
  'inline-cheap-source-map',
  'inline-cheap-module-source-map',
  'source-map',
  'inline-source-map',
  'hidden-source-map',
  'nosources-source-map',
];

module.exports = allModes.map(item => {
  return {
    devtool: item,
    mode: 'none',
    entry: './src/main.js',
    output: {
      filename: `js/${item}.js`,
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        },
      ],
    },
    plugins: [
      new HtmlWebpackPlugin({
        filename: `${item}.html`,
      }),
    ],
  };
});
  • 熱更新(HMR)代替自動刷新
    自動刷新導致頁面狀態丟失
    熱更新就是在頁面不跟新的情況下,只將修改的模塊實時替換到應用中
$ yarn webpack-dev-server --hot
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/main.js',
  output: {
    filename: 'js/bundle.js',
  },
  devtool: 'source-map',
  devServer: {
    hot: true,
    // hotOnly: true // 只使用 HMR,不會 fallback 到 live reloading
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    // ...
  ],
};

默認的 HMR 開啟后還需要我們手動去處理熱更新的邏輯。當然在 css 文件中由于 cssloader 中已經幫我們處理了,所以我們可以看到修改 css 可以出發熱跟新
編寫的 js 模塊由于代碼太過靈活,如果沒有框架的約束,wabpack 很難實現通用的熱更新

  • HMR API
import createEditor from './editor';
import background from './better.png';
import './global.css';

const editor = createEditor();
document.body.appendChild(editor);

const img = new Image();
img.src = background;
document.body.appendChild(img);

// ============ 以下用于處理 HMR,與業務代碼無關 ============

// console.log(createEditor)

if (module.hot) {
  let lastEditor = editor;
  // 處理js模塊的熱更新
  module.hot.accept('./editor', () => {
    // console.log('editor 模塊更新了,需要這里手動處理熱替換邏輯')
    // console.log(createEditor)

    const value = lastEditor.innerHTML;
    document.body.removeChild(lastEditor);
    const newEditor = createEditor();
    // 解決文本框狀態丟失
    newEditor.innerHTML = value;
    document.body.appendChild(newEditor);
    lastEditor = newEditor;
  });
  // 處理img熱更新
  module.hot.accept('./better.png', () => {
    img.src = background;
    console.log(background);
  });
}

以上例子 只是說明 webpack 沒辦法提供通用方案。實現一個熱更新原理就是利用 module.hot,HotModuleReplacementPluginApi 提供的這個。大部分框架中都集成了 HMR。

  • 不同環境的配置文件
// 函數方式配置
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = (env, argv) => {
  const config = {
    // ...
  };

  if (env === 'production') {
    config.mode = 'production';
    config.devtool = false;
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public']),
    ];
  }

  return config;
};

文件劃分的配置

// webpack.common.js

module.exports = {};

// webpack.dev.js
const common = require('./webpack.common');
const merge = require('webpack-merge'); // 安裝webpack-merge合并配置
module.exports = merge(common, {
  mode: 'development',
  devtool: 'cheap-eval-module-source-map',
  devServer: {
    hot: true,
    contentBase: 'public',
  },
  plugins: [new webpack.HotModuleReplacementPlugin()],
});

// webpack.prod.js
const merge = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const common = require('./webpack.common');

module.exports = merge(common, {
  mode: 'production',
  plugins: [new CleanWebpackPlugin(), new CopyWebpackPlugin(['public'])],
});
$ yarn webpack --config webpack.prod.js
$ yarn webpack-dev-server --config webpack.dev.js
  • DefinePlugin
    為代碼注入全局成員
    默認注入 process.evn.NODE_ENV 常量
plugins: [
  new webpack.DefinePlugin({
    // 值要求的是一個代碼片段
    API_BASE_URL: JSON.stringify('https://api.example.com'),
  }),
];
  • Tree-shaking
    將未引用代碼去除掉 生產環境下自動開啟
    在其他模式下開啟需要:
 optimization: {
    // 模塊只導出被使用的成員
    usedExports: true,
    // 盡可能合并每一個模塊到一個函數中
    concatenateModules: true, // scope Hoisting
    // 壓縮輸出結果
    minimize: true
  }
  • Tree-shaking && babel
    由于 Tree-shaking 是基于 ESModule 實現的,但是 舊版本 babel 中如果用到 preset-env 的插件集合的時候會默認開啟轉化 ESModule 的導入導出語法為 Commonjs 的規范。所以導致 Tree-shaking 失效,新版本已默認關閉

  • sideEffects 新特新
    標識代碼是否有副作用,為 Tree-shaking 提供更大的壓縮空間

// webpack.config.js
optimization: {
  sideEffects: true; // 開啟sideEffects功能
}
// package.json
"siedEffects": false // 標識代碼是否有副作用

副作用需要我們手動添加并且謹慎使用,一般用在開發第三方包中,當我們的代碼有副作用,但是卻配置了以上兩個屬性,就會導致程序報錯。

// package.json 配置有副作用的文件,這樣webpack在打包的過程中就不會忽略這些
"siedEffects" :[
  "./src/extend.js",
  "*/css"
]
  • Code Splitting
    打包成一個文件導致體積過大,加載時間過長。
    應用啟動的首屏并不是所有模塊都工作的
    所以我們需要分包,按需加載

    • 多入口打包
      適用于多頁面應用
      entry: {
        index: './src/index.js',
        album: './src/album.js'
      },
      output: {
        filename: '[name].bundle.js'
      },
      optimization: {
        splitChunks: {
        // 自動提取所有公共模塊到單獨 bundle
          chunks: 'all'
        }
      },
      plugins: [
        new HtmlWebpackPlugin({
          title: '首頁',
          template: './src/index.html',
          filename: 'index.html',
          chunks: ['index'] //  對不同的頁面指定不同的打包js文件
        }),
        new HtmlWebpackPlugin({
          title: '其他頁面',
          template: './src/album.html',
          filename: 'album.html',
          chunks: ['album']
        })
      ]
    
    • 動態導入 import()
      適用于單頁面應用
      在 react 或者 vue 中一般都是通過路由映射組件實現動態加載
      webpack 會根據 import()把對應的模塊拆分到不同的輸出文件,根據加載的需要執行不同的 js 文件
    // import posts from './posts/posts'
    // import album from './album/album'
    const render = () => {
    const hash = window.location.hash || '#posts'
    
    const mainElement = document.querySelector('.main')
    
    mainElement.innerHTML = ''
    
    if (hash === '#posts') {
        // mainElement.appendChild(posts())
        import(/_ webpackChunkName: 'components' _/'./posts/posts').then(({ default: posts }) => {
        mainElement.appendChild(posts())
      })
    } else if (hash === '#album') {
        // mainElement.appendChild(album())
        import(/_ webpackChunkName: 'components' _/'./album/album').then(({ default: album }) => {
        mainElement.appendChild(album())
        })
      }
    }
    
    render()
    
    window.addEventListener('hashchange', render)
    
  • 魔法注釋
    通過在 import 語句中加注釋的方式為 webpack 提供打包后的名稱如果設置一樣則打包到一個文件

if (hash === '#posts') {
  // mainElement.appendChild(posts())
  import(/* webpackChunkName: 'components' */ './posts/posts').then(
    ({ default: posts }) => {
      mainElement.appendChild(posts());
    }
  );
} else if (hash === '#album') {
  // mainElement.appendChild(album())
  import(/* webpackChunkName: 'components' */ './album/album').then(
    ({ default: album }) => {
      mainElement.appendChild(album());
    }
  );
}
  • MiniCssExtractPlugin
    提取 css 到單個文件
    需要考慮 css 大小,如果很少寫 css 那么還是采用 stypeLoader 注入到頁面的 style 標簽中
 module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 將樣式通過 style 標簽注入
          MiniCssExtractPlugin.loader, // 將樣式通過Link標簽方式注入
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()
  ]
  • webpack 內部提供的生產環境的壓縮只是針對 JS 代碼的。如果想要壓縮其他形式資源,需要單獨安裝對應的插件
optimization: {
  minimize: [
  // 要使用其他壓縮,這里要把默認的js壓縮的插件也安裝進來,是因為webpack會覆蓋了 optimization原有的默認配置
  // 這里配置的壓縮都只會在生產環境起作用,符合我們的預期,不用再去放到webpack.prod.js或者根據環境變量判斷
  new TreserWebpackPlugin(),
  // 這里以壓縮css為例,其他的參見官網
  new OptimizeCssAssetsWebpackPlugin()]
}
  • 輸出文件名稱 Hash
    一般我們部署前端資源文件的時候,都會開啟靜態資源緩存,避免每次都請求資源,整體應用的響應速度就會大幅度提升,不過也會有問題,當我們緩存時間設置過長,我們的應用更新過后,瀏覽器并不會及時更新。這就需要我們在生產環境中需要給文件添加 Hash 值,全新的文件名就是全新的請求,也就避免了上述問題
    hash: 只要內容修改,所有文件 hash 都會跟新
    contenthash: 文件級別的 hash,當前修改的文件以及被引用的文件 hash 會被動跟新
    chunk: 當文件內容改變,只修改當前同類的 hash 值
  output: {
    filename: '[name]-[contenthash:8].bundle.js'
  },

還有一些其他的配置項比如 preformance target externals resolve other option 我們只需要查閱官方文檔即可,另外還需要多理解 manifest 和 runtime 這樣的 webpack 概念

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372