npm i html-webpack-plugin -D
? 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 文件
每次寫完代碼都需要手動輸入指令才能編譯代碼,太麻煩了,我們希望一切自動化
npm i webpack-dev-server -D
? 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, //是否自動開啟瀏覽器
}
};
npx webpack serve
注意:使用開發服務器時,所有代碼都會在內存中編譯打包,并不會輸出到 dist 目錄下
篇
很多人都或多或少使用過 webpack,但是很少有人能夠系統的學習 webpack 配置,遇到錯誤的時候就會一臉懵,不知道從哪查起?性能優化時也不知道能做什么,網上的優化教程是不是符合自己的項目?等一系列問題!本文從最基礎配置一步步到一個完善的大型項目的過程。讓你對 webpack 再也不會畏懼,讓它真正成為你的得力助手!
本文從下面幾個課題來實現
項目地址
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 可以識別的文件
本章主要講解
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 如何加載模塊
我刪掉了一些注釋跟一些干擾內容,這樣看起來會更清晰一點
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 拿數據而是從其他模塊中
(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 已經結束了,我們主要做了以下事情
學習一個工具我們不僅要看懂它的配置,還要對它的原理一起了解,只有學到框架的精髓,我們才能應對如今大前端如此迅猛的發展。
課題 2:搭建開發環境跟生產環境
本章提要:
目錄
│── build │ │── base.js // 公共部分 │ │── build.js │ └── dev.js │── config │ │── base.js // 基礎配置 │ │── css.js // css 配置 │ │── HtmlWebpackPlugin.js // html 配置 │ └── MiniCssExtractPlugin.js // 提取css │── public // 公共資源 │ └── index.html // html 模版 └── src // 開發目錄 │── style │ └── index.css └── main.js // 主入口
實現可插拔配置
package.json
{ "scripts": { "dev": "cross-env NODE_ENV=development node build/dev.js", "build": "cross-env NODE_ENV=production node build/build.js" }, "dependencies": { "cross-env": "^6.0.3", "css-loader": "^3.2.0", "cssnano": "^4.1.10", "ora": "^4.0.3", "rimraf": "^3.0.0", "webpack": "^4.41.2" }, "devDependencies": { "extract-text-webpack-plugin": "^3.0.2", "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^0.8.0", "vue-cli-plugin-commitlint": "^1.0.4", "webpack-chain": "^6.0.0", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.9.0" } }
build/base.js
const { findSync }=require('../lib'); const Config=require('webpack-chain'); const config=new Config(); const files=findSync('config'); const path=require('path'); const resolve=p=> { return path.join(process.cwd(), p); }; module.exports=()=> { const map=new Map(); files.map(_=> { const name=_.split('/') .pop() .replace('.js', ''); return map.set(name, require(_)(config, resolve)); }); map.forEach(v=> v()); return config; };
構建生產環境
build/build.js
const rimraf=require('rimraf'); const ora=require('ora'); const chalk=require('chalk'); const path=require('path'); // 刪除 dist 目錄 rimraf.sync(path.join(process.cwd(), 'dist')); const config=require('./base')(); const webpack=require('webpack'); const spinner=ora('開始構建項目...'); spinner.start(); webpack(config.toConfig(), function(err, stats) { spinner.stop(); if (err) throw err; process.stdout.write( stats.toString({ colors: true, modules: false, children: false, chunks: false, chunkModules: false }) + '\n\n' ); if (stats.hasErrors()) { console.log(chalk.red('構建失敗\n')); process.exit(1); } console.log(chalk.cyan('build完成\n')); });
構建開發環境(devServer)
build/dev.js
const config=require('./base')(); const webpack=require('webpack'); const chalk=require('chalk'); const WebpackDevServer=require('webpack-dev-server'); const port=8080; const publicPath='/common/'; config.devServer .quiet(true) .hot(true) .https(false) .disableHostCheck(true) .publicPath(publicPath) .clientLogLevel('none'); const compiler=webpack(config.toConfig()); // 拿到 devServer 參數 const chainDevServer=compiler.options.devServer; const server=new WebpackDevServer( compiler, Object.assign(chainDevServer, {}) ); ['SIGINT', 'SIGTERM'].forEach(signal=> { process.on(signal, ()=> { server.close(()=> { process.exit(0); }); }); }); // 監聽端口 server.listen(port); new Promise(()=> { compiler.hooks.done.tap('dev', stats=> { const empty=' '; const common=`App running at: - Local: http://127.0.0.1:${port}${publicPath}\n`; console.log(chalk.cyan('\n' + empty + common)); }); });
提取 css
config/css.js
css 提取 loader 配置
module.exports=(config, resolve)=> { return (lang, test)=> { const baseRule=config.module.rule(lang).test(test); const normalRule=baseRule.oneOf('normal'); applyLoaders(normalRule); function applyLoaders(rule) { rule .use('extract-css-loader') .loader(require('mini-css-extract-plugin').loader) .options({ publicPath: './' }); rule .use('css-loader') .loader('css-loader') .options({}); } }; };
css 提取插件 MiniCssExtractPlugin
config/MiniCssExtractPlugin.js
const MiniCssExtractPlugin=require('mini-css-extract-plugin'); module.exports=(config, resolve)=> { return ()=> { config .oneOf('normal') .plugin('mini-css-extract') .use(MiniCssExtractPlugin); }; };
自動生成 html
config/HtmlWebpackPlugin.js
const HtmlWebpackPlugin=require('html-webpack-plugin'); module.exports=(config, resolve)=> { return ()=> { config.plugin('html').use(HtmlWebpackPlugin, [ { template: 'public/index.html' } ]); }; };
項目測試
測試 html 模板
public/index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>learn_webpack</title> <body></body> </html>
測試 css 模板
src/style/index.css
.test { width: 200px; height: 200px; color: red; background-color: orange; }
程序入口
src/main.js
require('./style/index.css'); const h2=document.createElement('h2'); h2.className='test'; h2.innerText='test'; document.body.append(h2);
課題 3:基礎配置之loader
本章提要:
目錄
增加以下文件
│──── config // 配置目錄 │ │── babelLoader.js // babel-loader 配置 │ │── ForkTsChecker.js // ts 靜態檢查 │ │── FriendlyErrorsWebpackPlugin.js // 友好錯誤提示 │ └── style │──── src // 開發目錄 │ │── style │ │ │── app.css │ │ │── index.less // 測試 less │ │ │── index.scss // 測試 sass │ │ └── index.postcss // 測試 postcss │ └── ts │ └── index.ts // 測試 ts │── babel.js │── postcss.config.js // postcss 配置 │── tsconfig.json // ts 配置 └──── dist // 打包后的目錄 │── app.bundle.js │── app.css └── index.html
配置 babel
config/babelLoader.js
module.exports=(config, resolve)=> { const baseRule=config.module.rule('js').test(/.js│.tsx?$/); const babelPath=resolve('babel.js'); const babelConf=require(babelPath); const version=require(resolve('node_modules/@babel/core/package.json')) .version; return ()=> { baseRule .use('babel') .loader(require.resolve('babel-loader')) .options(babelConf({ version })); }; };
使用 babel 配置 ts
這里我們使用 babel 插件 @babel/preset-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性能優化
本章講解
分離 Manifest
module.exports=(config, resolve)=> { return ()=> { config .optimization .runtimeChunk({ name: "manifest" }) } }
Code Splitting
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?
其實在 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實現可選鏈
本章內容
什么是 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編譯優化
本章內容
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:多頁面配置
注意
本章內容
使用
box build # 不加參數則會編譯所有頁面,并清空 dist box dev # 默認編譯 index 頁面
參數
# index2 是指定編譯的頁面。不會清空 dist # report 開啟打包分析 box build index2 --report box dev index2 --report
改造為腳手架
分成三個命令,進行不同操作
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了
本節概要
實現一個 CopyPlugin
我們今天寫一個 copy 的插件,在webpack構建完成之后,將目標目錄下的文件 copy 到另一個目錄下
const fs=require('fs-extra') const globby=require('globby') class CopyDirWebpackPlugin { constructor(options) { this.options=options; } apply(compiler) { const opt=this.options compiler.plugin('done', (stats)=> { if (process.env.NODE_ENV==='production') { (async ()=>{ const toFilesPath=await globby([`${opt.to}/**`, '!.git/**']) toFilesPath.forEach(filePath=> fs.removeSync(filePath)) const fromFilesPath=await globby([`${opt.from}/**`]) fromFilesPath.forEach(fromPath=> { const cachePath=fromPath fromPath=fromPath.replace('dist', opt.to) const dirpaths=fromPath.substring(0, fromPath.lastIndexOf('/')) fs.mkdirpSync(dirpaths) fs.copySync(cachePath, fromPath) }) console.log(` 完成copy ${opt.from} to ${opt.to}`) })() } }); } } module.exports=CopyDirWebpackPlugin
使用
將打包出來的 dist 目錄下的內容 copy 到 dist2 目錄下
const CopyPlugin=require('../webapck-plugin-copy'); module.exports=({ config })=> { return ()=> { config.plugin('copy-dist') .use(CopyPlugin, [{ from: 'dist', to: 'dist2' }]) } }
課時 9:構建 ssr
ssr 就是服務端渲染,做 ssr 的好處就是為了處理 spa 的不足,比如 seo 優化,服務端緩存等問題。
今天主要用 react 的 ssr 來做一個簡單的實例,讓大家更清晰的入門
本章概要
創建 box build:ssr
老規矩,先來一個 box build:ssr 命令讓程序可以執行
執行 box build:ssr 會調用 build/ssr 執行編譯
program .usage('<command> [options]') .version(packageConfig.version) .command('build:ssr [app-page]') .description(`服務端渲染`) .action(async (name, cmd)=> { const options=cleanArgs(cmd); const args=Object.assign(options, { name }, boxConf); if (lock) return; lock=true; require('../build/ssr')(args); });
編譯 ssr
與其他的編譯沒有什么區別,值得住的是
.libraryTarget('umd') .globalObject('this')
build/ssr.js
module.exports=function(options) { const path=require('path'); const Config=require('webpack-chain'); const config=new Config(); const webpack=require('webpack'); const rimraf=require('rimraf'); const ora=require('ora'); const chalk=require('chalk'); const PATHS={ build: path.join(process.cwd(), 'static'), ssrDemo: path.join(process.cwd(), 'src', 'ssr.jsx') }; require('../config/babelLoader')({ config, tsx: true })(); require('../config/HtmlWebpackPlugin')({ config, options: { publicPath: '/', filename: 'client.ssr.html' } })(); config .entry('ssr') .add(PATHS.ssrDemo) .end() .set('mode', 'development') // production .output.path(PATHS.build) .filename('[name].js') .libraryTarget('umd') .globalObject('this') .library('[name]') .end(); rimraf.sync(path.join(process.cwd(), PATHS.build)); const spinner=ora('開始構建項目...'); spinner.start(); webpack(config.toConfig(), function(err, stats) { spinner.stop(); if (err) throw err; process.stdout.write( stats.toString({ colors: true, modules: false, children: false, chunks: false, chunkModules: false }) + '\n\n' ); if (stats.hasErrors()) { console.log(chalk.red('構建失敗\n')); process.exit(1); } console.log(chalk.cyan('build完成\n')); }); };
編譯 jsx 語法
因為我們是用 react 寫的,避免不了會用到 jsx 語法,所以我們需要在 babel-loader 中使用 @babel/preset-react
npm i @babel/preset-react -D
config/babelLoader.js
if (tsx) { babelConf.presets.push('@babel/preset-react'); }
入口區分服務端/客戶端
區分服務端跟客戶端分別渲染
const React=require("react"); const ReactDOM=require("react-dom"); const SSR=<div onClick={()=> alert("hello")}>Hello world</div>; if (typeof document==="undefined") { console.log('在服務端渲染') module.exports=SSR; } else { console.log('在客戶端渲染') const renderMethod=!module.hot ? ReactDOM.render : ReactDOM.hydrate; renderMethod(SSR, document.getElementById("app")); }
服務端渲染
module.exports=function (options) { const express=require("express"); const { renderToString }=require("react-dom/server"); const chalk=require('chalk') const SSR=require("../static/ssr"); const port=process.env.PORT || 8080; server(port); function server(port) { const app=express(); app.use(express.static("static")); app.get("/", (req, res)=> res.status(200).send(renderMarkup(renderToString(SSR))) ); const empty=' ' const common=`App running at: - Local: http://127.0.0.1:${port}\n` console.log(chalk.cyan('\n' + empty + common)) app.listen(port, ()=> process.send && process.send("online")); } function renderMarkup(html) { return `<!DOCTYPE html> <html> <head> <title>Webpack SSR Demo</title> <meta charset="utf-8" /> </head> <body> <div id="app">${html}</div> <script src="./ssr.js"></script> </body> </html>`; } }
小結
至此 ssr 已經結束了,其實所有看起來很高大上的技術都是從一點一滴積累起來的,只要我們明白原理,你也能做出更優秀的框架
本文作者:前端技匠
原文鏈接:https://juejin.im/post/5de06aa851882572d672c1ad
ebpack,作為前端構建的基石,其代碼分割功能為我們提供了優化網站性能的強大武器。今天,我們就來深入剖析 Webpack 代碼分割的奧秘,從入門到實戰,助你打造閃電般加載體驗!
想象一下,當你的網站代碼量越來越龐大,打包后的 JavaScript 文件也會越來越大。這會導致:
Webpack 代碼分割 正是為了解決這些問題而生的!它可以將代碼分割成多個 chunk(代碼塊),按需加載,從而:
entry 配置是最基本的代碼分割方式,適用于多頁面應用。通過配置多個入口文件,Webpack 會自動將每個入口文件及其依賴打包成獨立的 chunk。
module.exports={
// ...
entry: {
index: './src/index.js',
about: './src/about.js',
},
output: {
// ...
filename: '[name].bundle.js', // 每個入口文件生成一個獨立的 bundle
},
};
SplitChunksPlugin 是 Webpack 內置的代碼分割插件,可以自動分析代碼,提取多個 chunk 中的公共代碼。
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 會根據配置,將代碼分割成多個 chunk,每個 chunk 都有一個唯一的 ID。Webpack 會在生成的 HTML 文件中插入 <script> 標簽,異步加載其他 chunk。當瀏覽器執行到 import() 語句時,會根據 chunk ID 加載對應的代碼塊。
Webpack 代碼分割是優化網站性能的利器,掌握它,你就能像魔法師一樣,將代碼分割成多個部分,按需加載,為用戶帶來閃電般加載體驗!
#頭條創作挑戰賽#
*請認真填寫需求信息,我們會在24小時內與您取得聯系。