通用、封裝、簡化 webpack 配置
現在,基本上前端的專案打包都會用上 webpack
,因為 webpack
提供了無與倫比強大的功能和生態。但在建立一個專案的時候,總是免不了要配置 webpack
,很是麻煩。
簡化 webpack
配置的一種方式是使用社群封裝好的庫,比如 roadhog。roadhog
封裝了 webpack
的一些基礎配置,然後暴露一些額外配置的介面,並附加本地資料模擬功能(mock
),詳情可以參考 roadhog 主頁。
另一種方式是自己封裝 webpack
,這樣做自己能夠更好的掌控專案。
1. 要封裝哪些功能
一般搭建一個專案至少需要兩種功能:本地開發除錯、構建產品程式碼。
其他的諸如測試、部署到伺服器、程式碼檢查、格式優化等功能則不在這篇文章講解範圍,如果有意瞭解,可以檢視我的其他文章。
2. 基礎配置
2.1 目錄結構(示例,配合後面的程式碼講解)
package.json
dev.js # 本地開發指令碼
build.js # 產品構建指令碼
analyze.js # 模組大小分析(可選)
# 單頁面結構
src/ # 原始碼目錄
- index.js # js 入口檔案
- index.html # html 入口檔案
- ... # 其他檔案
# 多頁面結構
src/ # 原始碼目錄
- home/ # home 頁面工作空間
- index.js # home 頁面 js 入口檔案
- index.html # home 頁面 html 入口檔案
- ... # home 頁面其他檔案
- explore/ # explore 頁面工作空間
- index.js # explore 頁面 js 入口檔案
- index.html # explore 頁面 html 入口檔案
- ... # explore 頁面其他檔案
- about/ # about 目錄
- company # about/company 頁面工作空間
- index.js # about/company 頁面 js 入口檔案
- index.html # about/company 頁面 html 入口檔案
- ... # about/company 頁面其他檔案
- platform # about/platform 頁面工作空間
- index.js # about/platform 頁面 js 入口檔案
- index.html # about/platform 頁面 html 入口檔案
- ... # about/platform 頁面其他檔案
- ... # 更多頁面
2.2 基礎 npm 包
# package.json
"devDependencies": {
"@babel/core": "^7.1.2", # babel core
"@babel/plugin-syntax-dynamic-import": "^7.0.0", # import() 函式支援
"@babel/plugin-transform-react-jsx": "^7.0.0", # react jsx 支援
"@babel/preset-env": "^7.1.0", # es6+ 轉 es5
"@babel/preset-flow": "^7.0.0", # flow 支援
"@babel/preset-react": "^7.0.0", # react 支援
"autoprefixer": "^9.1.5", # css 自動新增廠家字首 -webkit-, -moz-
"babel-loader": "^8.0.4", # webpack 載入 js 的 loader
"babel-plugin-component": "^1.1.1", # 如果使用 element ui,需要用到這個
"babel-plugin-flow-runtime": "^0.17.0", # flow-runtime 支援
"babel-plugin-import": "^1.9.1", # 如果使用 ant-design,需要用到這個
"browser-sync": "^2.24.7", # 瀏覽器例項元件,用於本地開發除錯
"css-loader": "^1.0.0", # webpack 載入 css 的 loader
"chalk": "^2.4.1", # 讓命令列的資訊有顏色
"file-loader": "^2.0.0", # webpack 載入靜態檔案的 loader
"flow-runtime": "^0.17.0", # flow-runtime 包
"html-loader": "^0.5.5", # webpack 載入 html 的 loader
"html-webpack-include-assets-plugin": "^1.0.5", # 給 html 檔案新增額外靜態檔案連結的外掛
"html-webpack-plugin": "^3.2.0", # 更方便操作 html 檔案的外掛
"less": "^3.8.1", # less 轉 css
"less-loader": "^4.1.0", # webpack 載入 less 的 loader
"mini-css-extract-plugin": "^0.4.3", # 提取 css 單獨打包
"minimist": "^1.2.0", # process.argv 更便捷處理
"node-sass": "^4.9.3", # scss 轉 css
"optimize-css-assets-webpack-plugin": "^5.0.1", # 優化 css 打包,包括壓縮
"postcss-loader": "^3.0.0", # 對 css 進行更多操作,比如新增廠家字首
"sass-loader": "^7.1.0", # webpack 載入 scss 的 loader
"style-loader": "^0.23.0", # webpack 載入 style 的 loader
"uglifyjs-webpack-plugin": "^2.0.1", # 壓縮 js 的外掛
"url-loader": "^1.1.1", # file-loader 的升級版
"vue-loader": "^15.4.2", # webpack 載入 vue 的 loader
"vue-template-compiler": "^2.5.17", # 配合 vue-loader 使用的
"webpack": "^4.20.2", # webpack 模組
"webpack-bundle-analyzer": "^3.0.2", # 分析當前打包各個模組的大小,決定哪些需要單獨打包
"webpack-dev-middleware": "^3.4.0", # webpack-dev-server 中介軟體
"webpack-hot-middleware": "^2.24.2" # 熱更新中介軟體
}
2.3 基本命令
# package.json
"scripts": {
"dev": "node dev.js",
"build": "node build.js",
"analyze": "node analyze.js",
}
npm run dev # 開發
npm run build # 構建
npm run analyze # 模組分析
如果需要支援多入口構建,在命令後面新增引數:
npm run dev -- home # 開發 home 頁面
npm run analyze -- explore # 模組分析 explore 頁面
# 構建多個頁面
npm run build -- home explore about/* about/all --env test/prod
-
home, explore
確定構建的頁面;about/*, about/all
指about
目錄下所有的頁面;all, *
整個專案所有的頁面 - 有時候可能還會針對不同的伺服器環境(比如測試機、正式機)做出不同的構建,可以在後面加引數
-
--
用來分割npm
本身的引數與指令碼引數,參考 npm – run-script 瞭解詳情
2.4 dev.js 配置
開發一般用需要用到下面的元件:
- webpack
- webpack-dev-server 或 webpack-dev-middleware
- webpack-hot-middleware
- HotModuleReplacementPlugin
- browser-sync
const minimist = require(`minimist`);
const webpack = require(`webpack`);
const HtmlWebpackPlugin = require(`html-webpack-plugin`);
const devMiddleWare = require(`webpack-dev-middleware`);
const hotMiddleWare = require(`webpack-hot-middleware`);
const browserSync = require(`browser-sync`);
const VueLoaderPlugin = require(`vue-loader/lib/plugin`);
const { HotModuleReplacementPlugin } = webpack;
const argv = minimist(process.argv.slice(2));
const page = argv._[0];
// 單頁面
const entryFile = `${__dirname}/src/index.js`;
// 多頁面
const entryFile = `${__dirname}/src/${page}/index.js`;
// 編譯器物件
const compiler = webpack({
entry: [
`webpack-hot-middleware/client?reload=true`, // 熱過載需要
entryFile,
],
output: {
path: `${__dirname}/dev/`, // 打包到 dev 目錄
filename: `index.js`,
publicPath: `/dev/`,
},
plugins: [
new HotModuleReplacementPlugin(), // 熱過載外掛
new HtmlWebpackPlugin({ // 處理 html
// 單頁面
template: `${__dirname}/src/index.html`,
// 多頁面
template: `${__dirname}/src/${page}/index.html`,
}),
new VueLoaderPlugin(), // vue-loader 所需
],
module: {
rules: [
{ // js 檔案載入器
loader: `babel-loader`,
exclude: /node_modules/,
options: {
presets: [`@babel/preset-env`, `@babel/preset-react`],
plugins: [
`@babel/plugin-transform-react-jsx`,
`@babel/plugin-syntax-dynamic-import`,
],
},
test: /.(js|jsx)$/,
},
{ // css 檔案載入器
loader: `style-loader!css-loader`,
test: /.css$/,
},
{ // less 檔案載入器
loader: `style-loader!css-loader!less-loader`,
test: /.less$/,
},
{ // scss 檔案載入器
loader: `style-loader!css-loader!sass-loader`,
test: /.(scss|sass)$/,
},
{ // 靜態檔案載入器
loader: `url-loader`,
test: /.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,
options: {
limit: 1,
},
},
{ // html 檔案載入器
loader: `html-loader`,
test: /.html$/,
options: {
attrs: [`img:src`, `link:href`],
interpolate: `require`,
},
},
{ // vue 檔案載入器
loader: `vue-loader`,
test: /.vue$/,
},
],
},
resolve: {
alias: {}, // js 配置別名
modules: [`${__dirname}/src`, `node_modules`], // 模組定址基路徑
extensions: [`.js`, `.jsx`, `.vue`, `.json`], // 模組定址副檔名
},
devtool: `eval-source-map`, // sourcemap
mode: `development`, // 指定 webpack 為開發模式
});
// browser-sync 配置
const browserSyncConfig = {
server: {
baseDir: `${__dirname}/`, // 靜態伺服器基路徑,可以訪問專案所有檔案
},
startPath: `/dev/index.html`, // 開啟伺服器視窗時的預設地址
};
// 新增中介軟體
browserSyncConfig.middleware = [
devMiddleWare(compiler, {
stats: `errors-only`,
publicPath: `/dev/`,
}),
hotMiddleWare(compiler),
];
browserSync.init(browserSyncConfig); // 初始化瀏覽器例項,開始除錯開發
2.5 build.js 配置
構建過程中,一般會有這些過程:
- 提取樣式檔案,單獨打包、壓縮、新增瀏覽器廠家字首
- 對
js
在產品模式下進行打包,並生成sourcemap
檔案 -
html-webpack-plugin
自動把打包好的樣式檔案與指令碼檔案引用到html
檔案中,並壓縮 - 對所有資源進行 hash 化處理(可選)
const minimist = require(`minimist`);
const webpack = require(`webpack`);
const chalk = require(`chalk`);
const autoprefixer = require(`autoprefixer`);
const HtmlWebpackPlugin = require(`html-webpack-plugin`);
const MiniCssExtractPlugin = require(`mini-css-extract-plugin`);
const OptimizeCssAssetsPlugin = require(`optimize-css-assets-webpack-plugin`);
const VueLoaderPlugin = require(`vue-loader/lib/plugin`);
const {yellow, red} = chalk;
const argv = minimist(process.argv.slice(2));
const pages = argv._; // [`home`, `explore`, `about/*`, `about/all`]
const allPages = getAllPages(pages); // 根據 page 中的 `*, all` 等關鍵字,獲取所有真正的 pages
// 單頁面,只有一個入口,所以只有一個配置檔案
const config = { ... };
// 多頁面,多個入口,所有有多個配置檔案
const configs = allPages.map(page => ({
// 單頁面
entry: `${__dirname}/src/index.js`, // js 入口檔案
// 多頁面
entry: `${__dirname}/src/${page}/index.js`, // js 入口檔案
output: {
path: `${__dirname}/dist/`, // 輸出路徑
filename: `[chunkhash].js`, // 輸出檔名,這裡完全取 hash 值來命名
hashDigestLength: 32, // hash 值長度
publicPath: `/dist/`,
},
plugins: [
new MiniCssExtractPlugin({ // 提取所有的樣式檔案,單獨打包
filename: `[chunkhash].css`, // 輸出檔名,這裡完全取 hash 值來命名
}),
new HtmlWebpackPlugin({
// 單頁面
template: `${__dirname}/src/index.html`, // html 入口檔案
// 多頁面
template: `${__dirname}/src/${page}/index.html`,// html 入口檔案
minify: { // 指定如果壓縮 html 檔案
removeComments: !0,
collapseWhitespace: !0,
collapseBooleanAttributes: !0,
removeEmptyAttributes: !0,
removeScriptTypeAttributes: !0,
removeStyleLinkTypeAttributes: !0,
minifyJS: !0,
minifyCSS: !0,
},
}),
new VueLoaderPlugin(), // vue-loader 所需
new OptimizeCssAssetsPlugin({ // 壓縮 css
cssProcessorPluginOptions: {
preset: [`default`, { discardComments: { removeAll: true } }],
},
}),
// webpack 打包的 js 檔案是預設壓縮的,所以這裡不需要再額外新增 uglifyjs-webpack-plugin
],
module: {
rules: [
{ // js 檔案載入器,與 dev 一致
loader: `babel-loader`,
exclude: /node_modules/,
options: {
presets: [`@babel/preset-env`, `@babel/preset-react`],
plugins: [
`@babel/plugin-transform-react-jsx`,
`@babel/plugin-syntax-dynamic-import`,
],
},
test: /.(js|jsx)$/,
},
{ // css 檔案載入器,新增了瀏覽器廠家字首
use: [
MiniCssExtractPlugin.loader,
`css-loader`,
{
loader: `postcss-loader`,
options: {
plugins: [
autoprefixer({
browsers: [
`> 1%`,
`last 2 versions`,
`Android >= 3.2`,
`Firefox >= 20`,
`iOS 7`,
],
}),
],
},
},
],
test: /.css$/,
},
{ // less 檔案載入器,新增了瀏覽器廠家字首
use: [
MiniCssExtractPlugin.loader,
`css-loader`,
{
loader: `postcss-loader`,
options: {
plugins: [
autoprefixer({
browsers: [
`> 1%`,
`last 2 versions`,
`Android >= 3.2`,
`Firefox >= 20`,
`iOS 7`,
],
}),
],
},
},
`less-loader`,
],
test: /.less$/,
},
{ // scss 檔案載入器,新增了瀏覽器廠家字首
use: [
MiniCssExtractPlugin.loader,
`css-loader`,
{
loader: `postcss-loader`,
options: {
plugins: [
autoprefixer({
browsers: [
`> 1%`,
`last 2 versions`,
`Android >= 3.2`,
`Firefox >= 20`,
`iOS 7`,
],
}),
],
},
},
`sass-loader`,
],
test: /.(scss|sass)$/,
},
{ // 靜態檔案載入器,與 dev 一致
loader: `url-loader`,
test: /.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,
options: {
limit: 1,
},
},
{ // html 檔案載入器,與 dev 一致
loader: `html-loader`,
test: /.html$/,
options: {
attrs: [`img:src`, `link:href`],
interpolate: `require`,
},
},
{ // vue 檔案載入器,與 dev 一致
loader: `vue-loader`,
test: /.vue$/,
},
],
},
resolve: {
alias: {}, // js 配置別名
modules: [`${__dirname}/src`, `node_modules`], // 模組定址基路徑
extensions: [`.js`, `.jsx`, `.vue`, `.json`], // 模組定址副檔名
},
devtool: `source-map`, // sourcemap
mode: `production`, // 指定 webpack 為產品模式
}));
// 執行一次 webpack 構建
const run = (config, cb) => {
webpack(config, (err, stats) => {
if (err) {
console.error(red(err.stack || err));
if (err.details) {
console.error(red(err.details));
}
process.exit(1);
}
const info = stats.toJson();
if (stats.hasErrors()) {
info.errors.forEach(error => {
console.error(red(error));
});
process.exit(1);
}
if (stats.hasWarnings()) {
info.warnings.forEach(warning => {
console.warn(yellow(warning));
});
}
// 如果是多頁面,需要把 index.html => `${page}.html`
// 因為每個頁面匯出的 html 檔案都是 index.html 如果不重新命名,會被覆蓋掉
if(cb) cb();
});
};
// 單頁面
run(config);
// 多頁面
let index = 0;
// go on
const goon = () => {
run(configs[index], () => {
index += 1;
if (index < configs.length) goon();
});
};
goon();
2.6 analyze.js 配置
const minimist = require(`minimist`);
const chalk = require(`chalk`);
const webpack = require(`webpack`);
const { BundleAnalyzerPlugin } = require(`webpack-bundle-analyzer`);
const VueLoaderPlugin = require(`vue-loader/lib/plugin`);
const {yellow, red} = chalk;
const argv = minimist(process.argv.slice(2));
const page = argv._[0];
// 單頁面
const entryFile = `${__dirname}/src/index.js`;
// 多頁面
const entryFile = `${__dirname}/src/${page}/index.js`;
const config = {
entry: entryFile,
output: {
path: `${__dirname}/analyze/`, // 打包到 analyze 目錄
filename: `index.js`,
},
plugins: [
new VueLoaderPlugin(), // vue-loader 所需
new BundleAnalyzerPlugin(), // 新增外掛
],
module: {
rules: [
{ // js 檔案載入器
loader: `babel-loader`,
exclude: /node_modules/,
options: {
presets: [`@babel/preset-env`, `@babel/preset-react`],
plugins: [
`@babel/plugin-transform-react-jsx`,
`@babel/plugin-syntax-dynamic-import`,
],
},
test: /.(js|jsx)$/,
},
{ // css 檔案載入器
loader: `style-loader!css-loader`,
test: /.css$/,
},
{ // less 檔案載入器
loader: `style-loader!css-loader!less-loader`,
test: /.less$/,
},
{ // scss 檔案載入器
loader: `style-loader!css-loader!sass-loader`,
test: /.(scss|sass)$/,
},
{ // 靜態檔案載入器
loader: `url-loader`,
test: /.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/,
options: {
limit: 1,
},
},
{ // html 檔案載入器
loader: `html-loader`,
test: /.html$/,
options: {
attrs: [`img:src`, `link:href`],
interpolate: `require`,
},
},
{ // vue 檔案載入器
loader: `vue-loader`,
test: /.vue$/,
},
],
},
resolve: {
alias: {}, // js 配置別名
modules: [`${__dirname}/src`, `node_modules`], // 模組定址基路徑
extensions: [`.js`, `.jsx`, `.vue`, `.json`], // 模組定址副檔名
},
mode: `production`, // 指定 webpack 為產品模式
};
webpack(config, (err, stats) => {
if (err) {
console.error(red(err.stack || err));
if (err.details) {
console.error(red(err.details));
}
process.exit(1);
}
const info = stats.toJson();
if (stats.hasErrors()) {
info.errors.forEach(error => {
console.error(red(error));
});
process.exit(1);
}
if (stats.hasWarnings()) {
info.warnings.forEach(warning => {
console.warn(yellow(warning));
});
}
});
2.7 擴充套件配置
你可以根據需要擴充套件配置,比如新增外掛、載入器等,比如:
-
provide-plugin 可以提供一些全域性模組的匯出,比如
jquery
- define-plugin 可以動態定義一些全域性變數
- css-loader 可以配置成 css-modules
- 如果某個頁面匯出
js bundle
很大,想分割成多個檔案,可以使用 dll-plugin、split-chunks-plugin - 如果想在命令列顯示構建的進度,可以使用 progress-plugin
3. 封裝
上面的程式碼可以封裝成一個全域性命令,比如 lila,執行上面的命令就可以更簡潔:
lila dev home # 開發 home 頁面
lila analyze explore # 模組分析 explore 頁面
# 構建多個頁面
lila build home explore about/* about/all --env test/prod
後續
更多部落格,檢視 https://github.com/senntyou/blogs
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證)