前言
以前做react項目的時候都是使用create-react-app
腳手架初始化項目的,最近想自己從零配置webpack4來實現一個react項目的初始化。
源碼在這裏
配置實現的功能:
- 打包css、js、img等資源
- 支持jsx、es6等語法
- 本地服務器
- eslint
- 按需加載
- 構建分析
- git hooks
最終的目錄結構
- build放置打包文件
- config放置webpack配置文件
- src放置我們的代碼
配置過程
1.創建文件夾sty-react-template
2.初始化git倉庫、生成package.json文件
git init
yarn init
3.創建src目錄、index.html、index.js文件
接下來我們打包試試,雖然現在我們什麼都沒配置,但是webpack4有自己的默認配置。
#安裝依賴
yarn add webpack webpack-cli -D
#執行webpack命令
webpack index.js
此時發現報錯bash: webpack: command not found
因爲我們是本地安裝的webpack,全局的環境變量並沒有webpack命令,所以報錯,我們只能進入對應的目錄下執行命令
node_modules/.bin/webpack index.js
#或者 npx webpack index.js
npx會自動鏈接路徑具體可以參考這裏
此時項目底下多了一個打包文件dist,這是webpack默認配置打包的結果,我們在index.html引入打包的文件就可以了。在瀏覽器中打開index.html可以在控制檯看到打印的hello world。
接下來我們自己創建配置來打包,創建配置之前,我們先忽略node_modelues
文件的改動,在根目錄下創建.gitignore
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
創建config文件夾及配置文件後的目錄結構
生成和開發環境分開配置,通過webpack-merge
合併共用配置
//webpack.common.js
const path = require('path')
// 從根目錄走
function resolve(dir) {
return path.join(__dirname, '..', dir);
}
module.exports = {
context: path.resolve(__dirname, '../'), // 入口起點根目錄
entry: {
app: './src/index.js'
},
output: {
path: resolve('build'),
filename: '[name].js',
}
}
//webpack.dev.js
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common,{
mode:'development'
})
//webpack.prod.js
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'production'
})
webpack4提供了mode模式來優化webpack打包, mode爲production會自動壓縮js代碼。
通過package.json來使用webpack命令
//package.json
...
"scripts": {
"dev":"webpack --config ./config/webpack.dev.js",
"build": "webpack --config ./config/webpack.prod.js"
},
...
–config是告訴webpack通過什麼配置文件打包
執行yarn dev
可以看到js被打包到build目錄下了,然後在html中引入打包的文件
我們可以使用插件來生成html文件,這樣就避免了html每次去手動引入js;使用插件來刪除上次打包的結果
#安裝依賴
yarn add -D html-webpack-plugin clean-webpack-plugin
//修改webpack.common.js文件
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
...
plugins:[
new HtmlWebpackPlugin({template:'./src/index.html'}),
new CleanWebpackPlugin()
]
...
}
現在打包會自動以src下的html爲模板生成新的html,新的html文件會自動引入打包的文件
每次修改完文件都要重新打包才能看到效果,我們可以使用webpack-dev-server
來搭建一個本地服務器來實時更新。
#安裝依賴
yarn add -D webpack-dev-server
//webpack.dev.js
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devServer: {
contentBase: './index.html',
hot: true,
port: 3000
}
})
//修改package.json的dev
"scripts": {
"dev": "webpack-dev-server --open --config ./config/webpack.dev.js",
...
},
執行yarn dev
會就可以直接在http://localhost:3000/實時預覽了
至此webpack的雛形已經完成了,上面一些配置的細節可以參考官方文檔。
接下來我們就去實現jxs語法了。
#安裝依賴,這裏不需要加-D參數
yarn add react react-dom
//修改src下的index.js文件
import React from 'react'
import ReactDOM from 'react-dom'
const App = () => (
<div>
hello react
</div>
)
ReactDOM.render(<App />, document.getElementById('app'))
運行yarn dev
報錯
很明顯不支持jsx語法,我們需要配置loader了
去babel的官網,看看如何配置jsx語法loader
yarn add -D @babel/preset-react babel-loader @babel/core
//webpack.common.js
module.exports = {
...
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react']
}
}
]
},
]
}
}
運行yarn dev
啓動成功
接下來配置css的loader
在src下創建css文件,然後在index中引入並使用
yarn add -D style-loader css-loader
//webpack.common.js
...
module:{
rules:[
...
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
//index.css
...
.red {
color: red;
}
運行yarn dev
可以看到樣式被插進了head中。當項目變大時樣式直接插入head中的方式並不好,我們需要將樣式分離
webpack4的版本中建議使用mini-css-extract-plugin插件(以前是ExtractTextWebpackPlugin
插件)
yarn add -D mini-css-extract-plugin
//webpack.common.js
...
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
rules:[
...
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
// you can specify a publicPath here
// by default it uses publicPath in webpackOptions.output
publicPath: '../',
hmr: process.env.NODE_ENV === 'development',
},
},
'css-loader',
],
}
]
plugins:[
...
new MiniCssExtractPlugin({
filename: 'static/css/[name].css', //打包到static的css目錄下
ignoreOrder: false, // Enable to remove warnings about conflicting order
})
]
可以看到css被分離出去了
接下來配置img、font、mp3等資源的loader。
url-loader和file-loader的區別在於url-loader在文件小於一定大小時可以直接打包成base64的格式嵌入html中,避免一次http請求,所以這裏我們用url-loader
yarn add url-loader -D
//webpack.common.js
module:{
rules:[
...
{
test: /\.(png|jpe?g|svg|gif)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/img/[name].[hash:7].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/font/[name].[hash:7].[ext]'
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/media/[name].[hash:7].[ext]'
}
}
]
}
文件名加上hash是避免緩存的影響,webpack幾種hash值得區別可以參考這裏、
基本上面完成了大部分配置了。
不過有一個問題,將所有的js打包成一個文件導致文件體積很大,我們需要將js拆分。通過import()語法我們可以動態加載js,webpack會自動將js拆分。我們只需要配置拆分文件的name即可,下面我們來創建一個組件動態引入。
修改src目錄下的文件
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'
ReactDOM.render(
<App/>,
document.getElementById('app')
)
// app.js
import React from 'react'
import Test from './components/Test'
function App() {
return (
<div>
<Test />
</div>
)
}
export default App;
// src/components/Test.js
import React, { Component } from 'react';
class Test extends Component {
state = { }
render() {
return (
<div>
test
</div>
);
}
}
export default Test;
啓動項目報錯
現在還不支持類實例屬性簡寫,實例屬性只能寫在constructor中,不過我們可以通過配置babel來支持這種寫法
yarn add -D @babel/plugin-syntax-class-properties
//webpack.common.js
...
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
plugins: ['@babel/plugin-proposal-class-properties']
}
}
]
},
...
]
接下來使用按需加載,按需加載只需要配置chunkFilename即可,然後使用動態import()方法,webpack會自動拆分
我使用react提供的懶加載方法來拆分代碼
// src/app.js
import React from 'react'
// import Test from './components/Test'
const Test = React.lazy(() => import('@/components/Test'))
function App() {
return (
<div>
<React.Suspense fallback={<div>Loading...</div>}>
<Test />
</React.Suspense>
</div>
)
}
export default App;
啓動報錯
不支持異步導入。添加babel插件解決
yarn add -D @babel/plugin-syntax-dynamic-import
// webpack.common.js
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-syntax-dynamic-import']
}
},
'eslint-loader'
]
執行yarn build
不用懶加載打包的情況如下:
通過對比發現多了一個1.js。這裏我們需要配置拆分後的js目錄,通過chunkFilename即可
//webpack.common.js
output: {
...
chunkFilename: 'static/js/[name].[contenthash:8].chunk.js',
},
可以看到打包的入口文件在最外層,拆分的js放到了static的js目錄下了。
接下來是壓縮css代碼。mode爲production只會壓縮js,css壓縮我們通過插件optimize-css-assets-webpack-plugin
yarn add -D optimize-css-assets-webpack-plugin
//webpack.common.js
module.exports = {
...
optimization: {
// splitChunks: {
// chunks: 'all'
// },
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})]
},
}
optimization是webpack提供的優化方式,webpack有默認的壓縮方式,我們可以自己配置minimizer,TerserJSPlugin是webpack4默認的js壓縮方法,這裏我們再寫一遍是防止自定義覆蓋了。
接下來我們進行構建分析,通過webpack-bundle-analyzer
我們可以分析我們打包的代碼,webpack-bundle-analyzer
的使用可以參考官網或這裏
yarn add -D webpack-bundle-analyzer
//webpack.common.js
...
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins:[
...
new BundleAnalyzerPlugin({
analyzerMode: 'disabled', //避免每次啓動都打開分析網站
generateStatsFile: true
})
]
// package.json
"scripts": {
"analyz": "webpack-bundle-analyzer --port 8001 ./build/stats.json"
...
},
每次執行yarn dev
或yarn build
都會在build文件下生成stats.json文件,然後執行yarn analyz
會自動在8001端口打開分析頁面
可以看到項目中node_modules
文件在打包中的體積是最大的,如何優化?
我們可以將這類庫不打包,然後直接通過CDN引入。
// webpack.common.js
module.exports = {
...
externals: {
react: 'React',
'react-dom': 'ReactDOM'
},
}
執行yarn dev
和yarn analyz
可以看到react和react-dom都沒有打包進去,體積減小了很多。但是這裏我們的頁面報錯了,因爲react沒有打包,導致頁面找不到React變量。我們需要手動在html裏引入react的庫
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>第一個模板</title>
<!-- 有很多cdn,選擇一個即可 -->
<script type="text/javascript" src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
關於如何配置externals請參考文檔
爲了保持項目代碼風格我們需要使用eslint進行檢查
yarn add -D eslint
npx eslint --init
這是我的選擇,還有最後一步是安裝依賴,選擇y即可。
命令會自動在項目根目錄生成.eslintrc.js
文件,代碼風格我用的是standard
我們可以擴展或覆蓋代碼風格配置,比如語句必須分號結尾,更多的配置規則查看文檔
module.exports = {
...
rules: {
semi: ["error", "always"], //語句必須用;
}
}
運行npx eslint src
(檢測src目錄代碼風格)
可以看到很多錯誤,我們按照對應的規則修改即可。eslint結合編輯器可以很好的使用,vscode安裝eslint插件後可以直接看到提示
上面就是缺少分號,可以配置保存自動修復。
這裏有個錯誤,沒有使用React變量。但是我們使用了jsx語法就必須引入react。清除警告的方法就是使用"plugin:react/recommended"
// .eslintrc.js
module.exports = {
extends: [
...
"plugin:react/recommended"
],
}
使用eslint-loader可以直接在瀏覽器的控制檯中看到警告信息
yarn add -D eslint-loader
// webpack.common.js
...
module:{
rules:[
...
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-syntax-dynamic-import'],
}
},
'eslint-loader'
]
},
]
}
我們每次在代碼提交時,能不能自動檢測eslint,當有代碼風格錯誤時阻止提交代碼?
可以使用husky
來實現這個功能
# 用yarn的安裝husky@next 用npm的安裝husky
yarn add -D husky@next
//package.json
...
"scripts": {
...
"lint": "eslint src"
},
"husky": {
"hooks": {
"pre-commit": "yarn lint",
"pre-push": "yarn lint"
}
},
在代碼commit或push的時候都會自動執行yarn linit
當檢測不通過時會阻止代碼提交
比如有個文件有語句沒加分號時,我們來提交代碼試試
git add .
git commit -m"test"
可以看到代碼提交失敗,當修改完後再提交就可以了
以上就是這次webpack配置的全部內容了。源碼放在了github上