目標:使用webpack從零開始配置一個react項目javascript
借用官網的解釋:本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle。css
要理解webpack是什麼,要從兩個詞出發:「模塊」和「打包」。html
以一個html頁面爲例,在頁面中經過script標籤引入了3個JavaScript文件a.js,b.js和c.js,每一個文件中分別定義了一個函數並導出給外部用。而且它們之間有必定的依賴關係,c.js依賴於b.js,b.js依賴於a.js。vue
由於有3個獨立的js文件,因此在加載的時候瀏覽器須要發送三次http請求來獲取這三個文件,而後依次執行其中的代碼,若是其中有一個文件由於網絡問題而延誤了時間,那麼整個頁面的顯示也會被延誤。當咱們的項目逐漸變大,有幾十個到上百個JavaScript文件的時候,那問題會更嚴重,不但有延遲問題,還會遇到很難維護的問題。因此須要儘量的合併文件,減小http請求,這個合併的過程就是打包,一般將幾個分散的有依賴關係的文件打包爲一個文件。java
一般狀況下,爲了提升開發效率,咱們會使用諸如ES6,less等,就須要在運行時作代碼的轉換,使得其能夠在對應的終端上正常執行,這個過程若是每次都手動經過工具作轉換的話很是的費時,理想的狀況是,咱們可使用這些具備新特性的東西,又能夠經過某種工具自動的完成轉換,進而提高開發效率,這個自動轉換的過程也是打包的過程。node
模塊能夠理解爲一個單獨的文件,或者一個方法,每個模塊都是一個單獨的做用域, 也就是說, 在該模塊內部定義的變量, 沒法被其餘模塊讀取, 除非定義爲global(瀏覽器中爲window)對象的屬性。模塊能夠被複用,模塊之間能夠被相互引用,好比一個處理浮點數加法的方法,就是一個模塊,可能在多個地方使用,使用的地方直接導入這個方法便可。react
webpack的核心功能就是打包,下面咱們會針對不一樣的文件類型作處理,充分發揮webpack的打包模塊的做用。webpack
如下內容將從如下幾個方面展開:web
mkdir react-fe
cd react-fe
yarn init // 初始化項目,生成package.json文件
yarn add webpack webpack-cli -D // 安裝webpack
複製代碼
若是對於生成的package.json中有不理解的,能夠查看這篇文章package.json 知多少? package.json中添加腳本配置:express
"scripts": {
"build": "webpack --mode production"
},
複製代碼
在項目目錄下新建src文件,用於存放源文件,新建/src/index.js,內容任意
console.log('hello world');
複製代碼
執行yarn build,會新增一個dist目錄,裏面是打包後的文件
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){console.log("hello world")}]);
複製代碼
webpack 運行時默認讀取項目下的 webpack.config.js 文件做爲配置;這個配置實際上是一個node js的腳本,腳本對外暴露一個配置對象,webpack經過這個對象來讀取相關的一些配置,由於是node js的腳本,因此可使用任何node模塊。一般一個項目會分爲開發模式和生產模式,因此咱們建立一個目錄build,此目錄下的文件均和構建相關。以下:
|- build (打包配置目錄)
|- webpack.config.js -- webpack打包基礎配置文件
|- webpack.dev.config.js -- webpack 開發環境打包配置文件
|- webpack.prod.config.js -- webpack 生產環境 build打包配置文件
複製代碼
webpack.config.js配置以下:
const path = require('path');
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
function src(dir) {
return resolve(path.join('src', dir))
}
module.exports = {
entry: {
main: src('index.js'), // 入口文件
},
output: {
filename: '[name].js', // 輸出文件
},
module: {
rules: []
},
plugins: []
}
複製代碼
配置webpack的mode, 枚舉值有production,development, none,在webpack4中配置了mode,至關於使用DefinePlugin設置了NODE_ENV,這個值用於區分生產模式仍是開發模式,後續會告訴webpack使用哪一種模式開啓內置優化,mode值的不一樣,build時默認的配置也會不一樣,有了默認配置,就不須要手動啓用插件了。
選項 | 描述 |
---|---|
development | 會將 process.env.NODE_ENV 的值設爲 development。啓用 NamedChunksPlugin 和 NamedModulesPlugin。 |
production | 會將 process.env.NODE_ENV 的值設爲 production。啓用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin. |
配置webpack.dev.config.js
var base = require('./webpack.config');
base.mode = "development";
base.devtool = 'cheap-module-eval-source-map';
module.exports = base;
複製代碼
配置webpack.prod.config.js
var base = require('./webpack.config');
base.mode = "production";
base.devtool = 'hidden-source-map';
module.exports = base;
複製代碼
添加npm scripts
"scripts": {
"build": "webpack --config ./build/webpack.prod.config.js --progress --colors",
"dev": "webpack --config ./build/webpack.dev.config.js --progress --colors"
},
複製代碼
cross-env是一個運行跨平臺設置和使用環境變量的腳本,好比咱們想要分析打包後的各個文件大小以及依賴關係,能夠添加"webpack-bundle-analyzer",這個包不是在每一次打包的時候都用到,因此,單獨添加一個條件,只有在須要分析的時候才使用這個包
yarn add cross-env webpack-bundle-analyzer -D
// 添加npm scripts
"scripts": {
"analyze": "cross-env ANALYZE=1 npm run build",
},
// 修改webpack.prod.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
if (process.env.ANALYZE) {
base.plugins.push(new BundleAnalyzerPlugin());
}
複製代碼
只設置 NODE_ENV,則不會自動設置 mode
webpack 中提供一種處理多種文件格式的機制,即是使用 loader。咱們能夠把 loader 理解爲是一個轉換器,負責把某種文件格式的內容轉換成 webpack 能夠支持打包的模塊。 loader自己是一個導出爲function的node模塊; 在沒有任何loader處理的狀況下,webpack默認只能處理js文件,最終輸出js文件,而loader的做用就是把非js文件轉換爲webpack能夠處理的js文件 將內聯圖像轉換爲data URL
負責解析 CSS 代碼,主要是爲了處理 CSS 中的依賴,例如 @import 和 url() 等引用外部文件的聲明;將樣式代碼處理爲js數組,樣式代碼處理爲字符串
yarn add css-loader -D
複製代碼
新建文件,目錄以下:
|- src
|- index.html
|- index.js
|- index.css文件
// index.css
#app {
background-color: #f5f5f5;
color: blue;
}
#app p {
color: gray;
}
// index.js
const a = require('./index.css');
console.log(a);
// index.html
<div id="app">
<h4>hello webpack!</h4>
<p>hello loader!</p>
</div>
<script src="../dist/main.js"></script>
// webpack.config.js,添加rules
{
test: /\.css$/,
use: 'css-loader'
}
複製代碼
執行 yarn build 能夠看到打包後的main.js中,css-loader將樣式代碼處理成了js數組,而且咱們的樣式代碼被處理成了字符串。
function(n, t, o) {
(t = o(2)(!1)).push([
n.i,
"#app {\n background-color: #f5f5f5;\n color: blue;\n }\n #app p {\n color: gray;\n }",
""
]),
(n.exports = t);
},
複製代碼
通過css-loader處理完的文件並無應用到頁面上,若是想要樣式生效,還須要style-loader的處理
yarn add style-loader -D
// webpack.config.js,修改rules
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
}
複製代碼
style-loader將css-loader返回的樣式數組一頓操做插入到html head,而後本身返回了一個空對象。
loader處理的時機:在import或」加載「時預處理文件,相似於其餘構建工具中的task
loader處理順序:從右向左執行,支持鏈式傳遞,鏈中的每一個 loader 會將轉換應用在已處理過的資源上。一組鏈式的 loader 將按照相反的順序執行。鏈中的第一個 loader 將其結果(也就是應用過轉換後的資源)傳遞給下一個 loader,依此類推。最後,鏈中的最後一個 loader,返回 webpack 指望 JavaScript。
一般狀況下,咱們會使用預處理器來編寫css,好比使用less或者sass,這樣能夠大大提升開發效率,下面以less爲例,將原來的css文件修改成less文件,而且內容修改以下:
// index.less
@theme-color: #ff8200;
@dark-gray: #707c93;
@light-gray: #b5c1d2;
#app {
background-color: @light-gray;
color: @theme-color;
p {
color: @dark-gray;
}
}
// 安裝less less-loader,less處理並識別less文件,less-loader能夠將less文件處理爲css文件
yarn add less less-loader -D
// webpack.config.js
module: {
rules: [
{
test: /\.less?$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
}
]
},
// index.js
const a = require('./index.less');
複製代碼
另外經常使用的處理樣式的方式是,使用postcss,它是一種對css編譯的工具,相似babel對js的處理,常見的功能如:
postcss 只是一個工具,自己不會對css一頓操做,它經過插件實現功能。
// 安裝loader和plugin
yarn add postcss-loader postcss-pxtorem autoprefixer -D
// 項目根目錄下建立postcss.config.js
module.exports = {
plugins: {
'autoprefixer': {},
'postcss-pxtorem': {
'rootValue': 108,
'propList': ['*'],
'minPixelValue': 2,
'selectorBlackList': [],
},
},
};
// 修改Webpack配置
module: {
rules: [
{
test: /\.less?$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
},
]
}
複製代碼
開發中還有一個經常使用的功能是,好比對某個依賴的UI庫作單位轉換,也能夠單獨配置plugin,好比咱們項目中設計稿的尺寸是1080,可是依賴的庫是750的,而且是px爲單位的,爲了兼容咱們的項目,就要對依賴庫作單位轉換。
module: {
rules: [
{
test: /\.css?$/,
use: [MiniCssExtractPlugin.loader, {
loader: 'css-loader',
options: {
minimize: true
}
}, {
loader: 'postcss-loader',
options: {
plugins: [
require('postcss-pxtorem')({
rootValue: 37.5,
propWhiteList: ['*'],
'minPixelValue': 2,
})
]
}
}],
include: /node_modules/ant-design/
},
]
}
複製代碼
一般一個成熟的項目,咱們不會使用style-loader的方式將結果插入到head中,也不會本身手動去修改html中引入打包後文件的路徑。下面將配合plugin使用
webpack的plugin比loader強大,經過鉤子能夠涉及整個構建流程,能夠作一些在構建範圍內的事情;理論上能夠干涉 webpack 整個構建流程,能夠在流程的每個步驟中定製本身的構建需求。
plugin:構建流程中處理構建任務,能夠這麼理解,模塊代碼的轉換工做由loader處理,除此以外的任何其餘工做均可以由plugin完成。
經常使用的兩個插件:
yarn add html-webpack-plugin mini-css-extract-plugin -D
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css?$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html', // 配置輸出文件名
template: src('index.html'),
}),
new MiniCssExtractPlugin({
filename: "static/css/[name].css"
}),
]
};
// index.html中去掉script部分,此處會自動在html中添加引用
複製代碼
執行 yarn build的結果
|- dist
|- index.html
|- main.js
|- static
|- css
|- main.css
// index.html中會自動引入css和js
<link href="static/css/main.css" rel="stylesheet">
<body> <div id="app"> <h4>hello webpack!</h4> <p>hello loader!</p> </div> <script type="text/javascript" src="main.js"></script></body>
複製代碼
做爲普通的html開發,你可能會須要從網上下載一些js文件而不是使用cdn的方式引入,好比用於適配移動端的flexiable,這種文件不須要通過babel再次進行處理,因此打包後直接在html中引入便可
yarn add copy-webpack-plugin -D
// webpack.config.js
const CopyPlugin = require('copy-webpack-plugin');
plugins: [
new CopyPlugin([
{ from: src('flexible.js'), to: resolve('dist') },
]),
]
複製代碼
如今的項目中咱們通常會使用ES6開發,因此須要用babel處理,關於babel的使用,能夠參考babel學習
yarn add @babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader -D
yarn add @babel/runtime-corejs3
// 項目根目錄下新建.babelrc文件
{
"presets": [
[
"@babel/preset-env",
{
"modules": "false",
"targets": {
"browsers": [
"> 1%",
"last 2 versions",
"ie >= 10",
"iOS >= 8",
"Android >= 4"
]
}
}
],
],
"plugins": [
[
"@babel/plugin-transform-runtime", {
"helpers": true,
"corejs": 3,
"regenerator": true
}
]
],
// "ignore": ["./src/three.min.js", "./src/panolens.js"]
}
// webpack.config.js,添加對js文件的處理
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
include: resolve(''),
},
]
}
// index.js,修改此文件,測試babel是否生效
const a = require('./index.less');
const obj = {
name: 'apple',
sex:' female',
};
if (obj.hasOwnProperty('sex')) {
const div = document.createElement('div');
div.style.color = 'pink';
const { name } = obj;
div.innerText = name;
document.getElementById('app').appendChild(div);
}
console.log(a);
複製代碼
處理圖片,常使用的兩個loader是url-loader和file-loader,其中 url-loader 是將圖片轉換成一個 DataURL,而後打包到 JavaScript 代碼中,這對小的圖片來講是不錯的處理方式,但是大圖片這種處理方式就不適用了,無疑會增大js的體積,經過使用file-loader將文件處理後輸出到目錄中。
yarn add file-loader url-loader -D
// 新建存放圖片目錄
|- src
|- assets
|- img // 用於放全部的圖片
// webpack.config.js
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192, // 若是超過8192 字節的就使用file-loader處理,並按照下面規則生成文件
name: 'static/images/[name].[hash:8].[ext]'
},
},
],
},
]
}
// 修改index.less
div {
background: url(../src/assets/img/bg.jpg);
}
複製代碼
到此爲止,咱們已經實現瞭如何使用webpack來搭建一個項目了,即便不適用諸如react, vue這樣的框架,也是能夠正常啓動打包項目的。
開發過程當中,咱們但願邊修改就能看到更新後的結果,因此本地開發啓動服務熱更新很重要,否則就得每次去build而後刷新看結果。 webpack爲咱們提供了實現本地熱更新的插件:webpack-dev-server,使用和配置很簡單,可參照官方文檔進行配置。可是本篇咱們不使用這個。經過使用express服務器,能夠進行更多的擴展,結合使用其餘的中間件來響應http請求及其餘的功能,擴展性更好,較爲靈活。 開啓了 hot 功能的 webpack 會往咱們應用的主要代碼中添加 WS 相關的代碼,用於和服務器保持鏈接,等待更新動做。
// webpack.dev.config.js
Object.keys(base.entry).forEach(function (name) {
base.entry[name] = ['webpack-hot-middleware/client'].concat(base.entry[name]);
});
base.plugins.push(
new webpack.HotModuleReplacementPlugin(), // 模塊熱更新
);
// 修改入口文件
if (module.hot) {
module.hot.accept();
}
// devServer.js
var webpack = require('webpack');
var express = require('express');
var path = require('path');
var config = require('./build/webpack.dev.config');
var app = express();
// Webpack developer
var compiler = webpack(config);
var devMiddleWare = require('webpack-dev-middleware')(compiler, {
publicPath: config.output.publicPath,
stats: {
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}
});
app.use(devMiddleWare);
app.use(require('webpack-hot-middleware')(compiler));
var mfs = devMiddleWare.fileSystem;
var file = path.join(config.output.path, 'index.html');
app.get('/middle.html', (req, res) => {
res.end();
})
app.get('*', function (req, res) {
devMiddleWare.waitUntilValid(function () {
var html = mfs.readFileSync(file);
res.end(html);
})
})
var port = 3005;
app.listen(port, function (err, result) {
if (err) {
console.log(err);
}
console.log('Server running on http://localhost:' + port);
});
複製代碼
按照上面的配置,能夠實現,更新本地文件,瀏覽器自動刷新,可是有個問題,好比咱們在頁面輸入了兩個值,自動刷新後,剛剛輸入的值就沒有了,對複雜的頁面操做來講,每次更新文件都須要從新進行一遍操做,是很影響效率的,形成這種現象的緣由是熱更新不能存儲state的狀態,使用react-hot-loader能夠解決這個問題。
下一篇文章介紹react的引入。