前言
上一期我們對webpack的構建進行了改進,新增了babelrc和postcss編譯器,還有把專案的構建能力適應了多頁面開發。但是大家發現這個工程還不能算得上是一個腳手架,尤其是新增了多頁面能力之後,每次新增頁面都要手動新增外掛配置,所以我們要進行一些簡單的封裝,達到通過簡單配置進行統一設定配置的效果。
本期重點:對目前的專案進行簡單的封裝,簡化專案配置難度
首先,沒有webpack配置基礎或者配置修改經驗的同學請移步第一期(基礎配置)和第二期(外掛與提取)
GitHub : github.com/wwwjason199…
往期連結:
從搭建vue-腳手架到掌握webpack配置(一.基礎配置)
從搭建vue-腳手架到掌握webpack配置(二.外掛與提取)
從搭建vue-腳手架到掌握webpack配置(三.多頁面構建)
構思配置檔案
首先我們要構思一下具體那些配置項是我們會經常用到的,而且會需要經常修改的。
新建一個config目錄,在該目錄下新建index.js檔案
index.js的內容如下:
const config = {
page:{
index:'./src/main.js',
home: ['./src/home.js','home page']
},
defaultTitle:"this is all title",//頁面的預設title
externals : {//大三方外部引入庫宣告
'jquery':'window.jQuery'
},
cssLoader : 'less',//記得預先安裝對應loader
// cssLoader : 'less!sass',//可以用!號新增多個css預載入器
usePostCSS : true, //需要提前安裝postcss-loader
toExtractCss : true,
assetsPublicPath: '/',//資源字首、可以寫cdn地址
assetsSubDirectory: 'static',//建立的的靜態資源目錄地址
host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,//除錯開啟時是否自動開啟瀏覽器
uglifyJs : true,//是否醜化js
sourceMap : true,//是否開啟資源對映
plugins:[]//額外外掛
}
module.exports = config;
複製程式碼
注意:配置項中的路徑都相對於跟目錄
配置項說明
page:就如webpack.config裡面的entry,進行了改良,如果屬性是陣列的話,第二個引數是html的標題(title)
defaultTitle:是所有頁面預設的title
externals:如註釋
cssLoader:要使用的css預載入器,可以用!
分割設定多個載入器,使用的同時記住npm install less-loader
安裝對應的loader
usePostCss:是否使用postcss,也是要預先安裝post-loader
toExtractCss:是否抽取css檔案
assetsPublicPath:資源的公共地址字首,頁面的所有資源引入會指向該地址,可以是一個cdn的域名。
assetsSubDirectory:要在根目錄下建立一個static目錄存放不被webpack編譯的檔案(靜態檔案),而assetsSubDirectory
值是dist目錄下的靜態資源地址,如值是static
的話,build之後,~\static
目錄下的檔案就會被複制到dist/static
目錄下
host \port \autoOpenBrower:如註釋
uglifyJs \sourceMap :如註釋
plugins:可以new一些外掛進去
整理專案配置
在config目錄下放封裝的配置邏輯指令碼檔案,新建一個static目錄放靜態資源,多出的幾個檔案後面會慢慢道來
按版本生成程式碼
以免多次build程式碼的時候都會覆蓋上次的生成記錄,我們可以做一個小優化,用package.json裡的version值作為目錄名在dist下生成如dist/1.0.0/
的目錄
只要改一下output的值就能實現這一需求
output:{
path:path.resolve(__dirname,'./dist/'+ process.env.npm_package_version),
filename:"js/[name].js"
},
複製程式碼
process.env.npm_package_version
能得到package.json裡的version值,具體參考這裡
在每次build之前按需要修改package.json裡的version值就可以區分版本生成目錄
修改npm script
"scripts": {
"clean": "node config/build.js",
"build": "webpack --progress --hide-modules --config config/webpack.prod.conf.js",
"dev": "cross-env NODE_ENV=development webpack-dev-server --hot",
"c-b": "npm run clean && npm run build"
},
複製程式碼
你會發現多了一個clean和c-b,而且build指向了一個新的檔案 config/webpack.prod.conf.js
,clean也執行了一個新的檔案config/build.js
。從名字能看出來clean是用來清理目錄的,c-b是clean和build一起執行的
所以改完了npm script之後我們在config目錄建立這兩個檔案吧。
config/webpack.prod.conf.js
檔案用於獨立生成環境是用到的webpack配置項
config/build.js
是清理邏輯。
先清理目錄
config/build.js
內容如下:
'use strict'
process.env.NODE_ENV = 'production'
const rm = require('rimraf')
const path = require('path')
// const webpack = require('webpack')
// const webpackConfig = require('./webpack.prod.conf')
rm(path.resolve(__dirname,'../dist/'+ process.env.npm_package_version), err => {
if (err) throw err
// webpack(webpackConfig, (err, stats) => {
// if (err) throw err
// })
})
複製程式碼
就是簡單地用node的rimraf元件刪除當前版本的目錄
為什麼有一些註釋的部分呢?
其實這些程式碼是從官方的vue-cli裡面貼上過來的,原本vue-cli預設是刪除和webpack執行一起執行的,但是我發現這樣做 一來沒有了webpack --progress
載入進度顯示,二來要引入很多node外掛來書寫載入提示,三來clean和build一起執行太過絕對了。所以我把執行webpack的邏輯註釋掉了,然後用npm script裡的build進行代替。
獨立生產環境配置
在上幾期我們簡單的用if (process.env.NODE_ENV === 'production')
作為生產環境的判斷,在webpack.config.js檔案裡面一起編寫配置項。為了規範化和獨立性,把if裡的內容抽離到一個新的檔案(config/webpack.prod.conf.js
)裡面,如下
process.env.NODE_ENV = 'production'
const path = require('path')
const config = require('../config')
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require("../webpack.config.js")
const webpackConfig = merge(baseWebpackConfig, {
devtool : '#source-map',
output:{
path:path.resolve(__dirname,'../dist/'+ process.env.npm_package_version),
filename:"js/[name].[chunkhash].js",
chunkFilename:"js/[id].[chunkhash].js",
publicPath:config.assetsPublicPath || '/'
},
plugins :[
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.LoaderOptionsPlugin({
minimize: true
}),
//提取多入庫的公共模組
Object.keys(config.page).length >= 2 ? new webpack.optimize.CommonsChunkPlugin({
name: 'common',
minChunks:2
}):()=>{},
//抽取從node_modules引入的模組,如vue
new webpack.optimize.CommonsChunkPlugin({
name: 'vender',
minChunks:function(module,count){
var sPath = module.resource;
// console.log(sPath,count);
return sPath &&
/\.js$/.test(sPath) &&
sPath.indexOf(
path.join(__dirname, '../node_modules')
) === 0
}
}),
//將webpack runtime 和一些複用部分抽取出來
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks:Infinity
}),
//將import()非同步載入複用的公用模組再進行提取
new webpack.optimize.CommonsChunkPlugin({
// name: ['app','home'],
async: 'vendor-async',
children: true,
deepChildren:true,
minChunks:2
}),
]
})
if(config.uglifyJs){
module.exports.plugins = (module.exports.plugins || []).concat([
new webpack.optimize.UglifyJsPlugin({
sourceMap: config.sourceMap,
compress: {
warnings: false
}
}),
])
}
if(config.sourceMap){
module.exports.devtool = false
}
module.exports = webpackConfig
複製程式碼
process.env.NODE_ENV = 'production'
首先宣告當前是生成環境
const config = require('../config')
引入了上面構思的配置項的配置檔案
你會發現用到了webpack-merge
外掛(記得npm install --save-dev webpack-merge
),顧名思義這是合併兩個物件裡面的webpack配置項的。
其他的就是生產程式碼時用到的公共塊抽取外掛、醜化js等外掛,不清楚這些外掛的可以翻回去看(二)和(三)期
還有重新配置了一下output的配置,主要是多了publicPath
項,這是設定支援公用地址字首的配置項。
重點來了!
用配置項進行自動配置專案的魅力就在於,可以通過通俗易懂的配置規則得到配置複雜的webpack構建邏輯的效果。這也正正是考驗一個程式設計師程式設計能力的地方,半自動或全自動的配置背後可是執行著你封裝的邏輯指令碼。
自動配置多入口
我們之前構思的配置項第一個page
就是一個宣告多入口,他類似wepack裡的entry
,每一項對應一個入口,也對應一個頁面,如下就對應兩個頁面
page:{
index:'./src/main.js',
home: ['./src/home.js','home page']
},
複製程式碼
這可以說是半自動的配置方法,有的人會傾向於全自動的方法,就是通過查詢給定的檔案目錄下包含的入口js檔案自動生成入口配置,而不用像我這樣手動宣告用到的入口。感興趣的可以參考這裡:link
Jason做過不少的小程式開發,比較習慣明確的列出所包含的頁面,所以更青睞這種半自動的配置方式。列明頁面入口不僅方面深入新增自己想要的規則,而且可以通過該配置項知道本專案包含哪些頁面。
開始封裝
在config/index.js
底下開始封裝我們要的邏輯,當然你可以獨立出一個新的檔案。這裡寫到一起是因為,一方面考慮到入門教程的複雜性,另一方面我們可以在同一個檔案下一遍對照配置項一遍封裝邏輯。
const config = {
page:{
index:'./src/main.js',
home: ['./src/home.js','home page']
},
//...
}
module.exports = config;
/**
* some auto-create-function
*/
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const isProduction = process.env.NODE_ENV === 'production'
//自動生成HTML模板
const createHTMLTamplate = function(obj){
let htmlList = [];
let pageList = obj;
for(let key in pageList){
htmlList.push(
new HtmlWebpackPlugin({
filename: key + '.html',
title:Array.isArray(pageList[key])&&pageList[key][1]
?pageList[key][1].toString()
:config.defaultTitle,
template:path.resolve(__dirname,'../index.html'),
chunks:[key,'vender','manifest','common'],
chunksSortMode: 'dependency'
})
)
}
return htmlList
}
//設定多入口
const setEntry = function(obj){
let entry = {};
let pageList = obj;
for(let key in pageList){
if(Array.isArray(pageList[key]) && pageList[key][0]){
entry[key] = path.resolve(__dirname,'../'+pageList[key][0].toString());
}else{
entry[key] = path.resolve(__dirname,'../'+pageList[key].toString());
}
}
return entry
}
module.exports.plugins = (module.exports.plugins || []).concat(
createHTMLTamplate(config.page)
);
module.exports.entry = setEntry(config.page);
複製程式碼
有點程式設計能力的同學不難看懂這些邏輯。只是遍歷page
值裡面的每一項,返回entry和html模板外掛陣列。注意一點就是這裡用了Array.isArray(pageList[key]
判斷當前是否陣列,作簡單的值相容。
還是那句,不懂HtmlWebpackPlugin看前兩期 或者 看這裡
createHTMLTamplate
返回對應配置項的HtmlWebpackPlugin外掛列表
setEntry
返回入口chunk物件entry
回到webpack.config.js
然後我們返回到webpack.config.js檔案把這些方法的然後值引用到對應的配置項上。留意註釋~~~~我在這裡~~~~
const path = require('path')
const config = require('./config')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const webpack = require('webpack')
const merge = require('webpack-merge')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
baseWebpackConfig = {
entry:config.entry, //~~~~~~~~~我在這裡~~~~~~~~
output:{
path:path.resolve(__dirname,'./dist/'+ process.env.npm_package_version),
filename:"js/[name].js"
},
module:{
rules:[
//...
]
},
plugins:[
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, './static'),
to: config.assetsSubDirectory,
}
])
].concat(config.plugins),//~~~~~~~我在這裡~~~~~~~
resolve:{
extensions: ['.js', '.vue', '.json'],
alias:{
'vue$':'vue/dist/vue.esm.js',// 'vue/dist/vue.common.js' for webpack 1
'@': path.resolve(__dirname,'./src'),
}
},
externals:config.externals,
}
module.exports = baseWebpackConfig;
複製程式碼
當然還用到了很多其他的配置項,檢查一下config關鍵字自己對號入座。
自動新增css前處理器
同樣在config/index.js
下面新增自動配置css前處理器的邏輯,下面貼出程式碼有點長,但是請一定細心看一下,有註釋幫助理解,認真看下其實重點邏輯也就中間一部分
const config = {
//...
cssLoader : 'less',//記得預先安裝對應loader
// cssLoader : 'less!sass',//可以用!號新增多個css預載入器
usePostCSS : true, //需要提前安裝postcss-loader
toExtractCss : true,
//...
}
module.exports = config;
/**
* some auto-create-function
*/
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const ExtractTextPlugin = require("extract-text-webpack-plugin")
const ExtractRootCss = new ExtractTextPlugin({filename:'styles/[name].root.[hash].css',allChunks:false});
const ExtractVueCss = new ExtractTextPlugin({filename:'styles/[name].[chunkhash].css',allChunks:true});
const isProduction = process.env.NODE_ENV === 'production'
//自動生成HTML模板
const createHTMLTamplate = function(obj){
//...
}
//設定多入口
const setEntry = function(obj){
//...
}
//設定樣式前處理器
const cssRules = {
less: {name:'less'},
sass: {name:'sass', options:{indentedSyntax: true}},
scss: {name:'sass'},
stylus: {name:'stylus'},
styl: {name:'stylus'}
}
//vue內嵌樣式用到的配置規則
const cssLoaders = function(options){
options = options || {}
let loaders = {};
const loaderList = options.loaders
//判斷樣式是來自檔案還是.vue檔案內嵌,然後用對應的外掛例項
const ExtractCss = options.isRootCss ? ExtractRootCss : ExtractVueCss;
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}//css-loader
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
//判斷是否使用postcss
const frontLoader = options.usePostCSS ? [cssLoader,postcssLoader]:[cssLoader]
//出了less等預載入的loader之外,還一定要有一般css的編譯
if(loaderList.indexOf('css') === -1)loaderList.unshift("css")
//遍歷陣列生成loader佇列
loaderList.forEach(element => {
const loaderOptions = cssRules[element]&&cssRules[element].options;
const loaderName = cssRules[element]&&cssRules[element].name;
let arr = element==="css" ? [] : [{
loader: loaderName+"-loader",
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
}]
//是否提取到css檔案
if(options.Extract){
loaders[element] = ExtractCss.extract({
use: frontLoader.concat(arr),
fallback: 'vue-style-loader'
})
}else{
loaders[element] = ['vue-style-loader'].concat(frontLoader,arr)
}
});
//是否提取到css檔案
if(options.Extract){
module.exports.plugins = (module.exports.plugins || []).concat([ExtractRootCss,ExtractVueCss]);
}
return loaders
}
//樣式檔案用到的配置規則
const styleLoaders = function(options){
options.isRootCss = true;
let output = [];
const loaders = cssLoaders(options);
for (const extension in loaders) {
let loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
module.exports.plugins = (module.exports.plugins || []).concat(
createHTMLTamplate(config.page)
);
module.exports.entry = setEntry(config.page);
module.exports.styleLoaders = styleLoaders({
loaders: config.cssLoader.split('!'),
sourceMap : config.sourceMap,
usePostCSS : config.usePostCSS,
Extract : isProduction&&config.toExtractCss,//生成環境才判斷是否進行提取
});
module.exports.cssLoaders = cssLoaders({
loaders: config.cssLoader.split('!'),
sourceMap : config.sourceMap,
//vue-loader內部自動開啟postcss所以開發環境下會有警告,所以也是生成環境才進行進一步判斷
usePostCSS : isProduction&&config.usePostCSS,
Extract : isProduction&&config.toExtractCss,
});
複製程式碼
有看過vue-cli內部封裝的程式碼(沒看過也沒關係)的同學可能會發現以上的程式碼有點像vue-cli裡面的邏輯。Jason確實借鑑了一點vue-cli的封裝思想。
cssRules
:該物件是預先設定好不同預載入的名稱很options配置項。我們會發現同一種css處理器也會有不一樣的規則和字尾名(就如sass和scss),構思的配置項裡面很難一一列出,那麼我們就要藉助這裡物件進行區分。後期有什麼需要新增的options配置也可以在cssRules
的第二個引數中新增。
cssLoaders
:生成cssloader佇列的方法,同時可以直接賦值到vue-loader內的規則裡面。返回值如下
{
css:[vue-style-loader,css-loader],
less:[vue-style-loader,css-loader,less-loader]
}
複製程式碼
styleLoaders
:該方法則是匹配對應css處理器字尾名檔案的配置規則。
細心的同學會留意到cssLoaders和styleLoaders,在對是否使用postcss時候多出 isProduction
判斷。因為 vue-loader內部自動開啟postcss所以開發環境下會有警告,所以也是生成環境才進行進一步判斷是否開啟postcss。
再回到webpack.config.js
baseWebpackConfig = {
//...
module:{
rules:[
{
test:/\.js$/,
loader:"babel-loader",
exclude:/node_modules/
},
{
test:/\.(png|jpe?j|gif|svg)(\?.*)?$/,
loader:'url-loader',
options:{
limit:10000,
name:'img/[name].[ext]?[hash]'
}
},
{
test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader:"url-loader",
options:{
limit:10000,
name:'fonts/[name].[ext]?[hash]'
}
},
{
test:/\.vue$/,
loader:'vue-loader',
options:{
loaders: config.cssLoaders //~~~~~~~我在這裡~~~~~~~
}
},
].concat(config.styleLoaders) //~~~~~~~我在這裡~~~~~~~
},
//...
}
module.exports = baseWebpackConfig;
複製程式碼
好了到現在半自動化的封裝邏輯都寫好了,下面選取一些需要注意的配置進行介紹。
其他配置項
host \port \autoOpenBrower
host \port \autoOpenBrower
這些和開發伺服器相關的配置項,以前引入到webpack.config.js裡面。
在webpack.config.js
下面新增程式碼(你也可以想vue-cli一樣再獨立一個檔案webpack.dev.conf.js),如下
const path = require('path')
const config = require('./config')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const webpack = require('webpack')
const merge = require('webpack-merge')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
baseWebpackConfig = {
entry:config.entry,
//...
}
if (process.env.NODE_ENV === 'development') {
console.log(process.env.NODE_ENV);
baseWebpackConfig = merge(baseWebpackConfig,{
devtool : '#eval-source-map',
devServer : {
clientLogLevel: 'warning',
historyApiFallback: true,
hot: true,
compress: true,
host: HOST || config.host,
port: PORT || config.port,
open: config.autoOpenBrowser,
publicPath:config.assetsPublicPath || '/'
},
plugins : [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"development"'
}
}),
new webpack.HotModuleReplacementPlugin()
]
})
}
module.exports = baseWebpackConfig;
複製程式碼
顯然,host和port都可以被process.env.PORT\HOST 覆蓋。其他devServer配置項可以參考 官方文件
有一點要注意,HotModuleReplacementPlugin外掛一定要在開啟webpack-dev-server的時候才呼叫,所以要獨立在該開發環境判斷中。
assetsSubDirectory
一開始構思配置項的時候對該屬性有介紹
assetsSubDirectory
值是dist目錄下的靜態資源地址,如值是static
的話,build之後,~\static
目錄下的檔案就會被複制到dist/static
目錄下
而實現檔案複製的外掛是 CopyWebpackPlugin
,使用前記得install
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, './static'),
to: config.assetsSubDirectory,
// ignore: ['.*']
}
])
複製程式碼
assetsPublicPath
該項是地址字首,如果用到了第三方配置資源的地址,那麼這裡就可以填寫對應的域名。
開發環境中它在
devServer : {
clientLogLevel: 'warning',
historyApiFallback: true,
hot: true,
compress: true,
host: HOST || config.host,
port: PORT || config.port,
open: config.autoOpenBrowser,
publicPath:config.assetsPublicPath || '/'
},
複製程式碼
生成環境中它在
output:{
path:path.resolve(__dirname,'../dist/'+ process.env.npm_package_version),
filename:"js/[name].[chunkhash].js",
chunkFilename:"js/[id].[chunkhash].js",
publicPath:config.assetsPublicPath || '/'
}
複製程式碼
其他配置項都淺而易懂,不多解釋,當然你可以發揮自己的創造力新增更多自己需要的配置項。
執行一下
執行清理並構建
npm run c-b
複製程式碼
得到的如下結果
完整的專案、webapck.config.js、config/index.js等檔案可以下載或者克隆本專案的github
GitHub : github.com/wwwjason199…
總結
整個系列學習編寫vue腳手架的過程到這裡算是得到了一個比較完整的入門,從一開始入門webpack的配置項、到引入常用外掛實現檔案抽離、再到適配多頁面多入口、最終對專案進行自動化的封裝。
不知不覺差不多實現vue官方給出的vue-cli裡面的大部分能力,是不是發現自己不再是webpack的小白,還挺有成就感呢。
我們在整個學習的過程中有很多借鑑vue-cli的思想和規範。可能有人會說自己寫這麼麻煩幹嘛,直接用vue-cli不就行了嗎?此言差矣,這不僅是一個學習webpack的過程,更是學會因地制宜按專案的實際情況構建工程的過程。而且能讓我們深入體會工程化和自動化的思想。
Jason的一些話
前段時間工作有點忙,而且廣州寒氣逼人下班都懶得動了,停更了差不多有一個月,雖然等更新的人不多,但是真的要跟有關注的同學說一聲對不起。開始放假而且天氣暖和了才把這一期碼完,請大家原諒,也希望大家不要學我這個重度拖延症患者一樣懶。
後面該碼什麼文章呢?
Jason寫的這些文章文筆不怎麼好,但都是以和大家一起學習一門技術為初衷在寫,Jason相信有意去入門webpack的同學看完了這一系列的文章肯定對webpack有了更多的瞭解。
Jason後面會複習一下es6+ 和 想去深入學習一下node,還會寫一些vue的專案。後面要寫什麼文章可能就看哪方面積累和了解的更深入,還有哪些內容跟適合總結成文章了。
大家有什麼想和Jason一起學習的前端框架、技術,可以留言哦,歡迎給意見和交流。
後面會不定期更新,喜歡的同學可以點下關注的。