上一期咱們對webpack的構建進行了改進,添加了babelrc和postcss編譯器,還有把項目的構建能力適應了多頁面開發。可是你們發現這個工程還不能算得上是一個腳手架,尤爲是添加了多頁面能力以後,每次添加頁面都要手動添加插件配置,因此咱們要進行一些簡單的封裝,達到經過簡單配置進行統一設置配置的效果。css
本期重點:對目前的項目進行簡單的封裝,簡化項目配置難度html
首先,沒有webpack配置基礎或者配置修改經驗的同窗請移步第一期(基礎配置)和第二期(插件與提取)前端
GitHub : github.com/wwwjason199…vue
往期連接:
從搭建vue-腳手架到掌握webpack配置(一.基礎配置)
從搭建vue-腳手架到掌握webpack配置(二.插件與提取)
從搭建vue-腳手架到掌握webpack配置(三.多頁面構建)node
首先咱們要構思一下具體那些配置項是咱們會常常用到的,並且會須要常常修改的。
新建一個config目錄,在該目錄下新建index.js文件
index.js的內容以下:jquery
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;
複製代碼
注意:配置項中的路徑都相對於跟目錄webpack
page:就如webpack.config裏面的entry,進行了改良,若是屬性是數組的話,第二個參數是html的標題(title)git
defaultTitle:是全部頁面默認的title程序員
externals:如註釋es6
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值就能夠區分版本生成目錄
"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文件把這些方法的而後值引用到對應的配置項上。留意註釋~~~~我在這裏~~~~
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關鍵字本身對號入座。
一樣在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。
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
這些和開發服務器相關的配置項,之前引入到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
值是dist目錄下的靜態資源地址,如值是static
的話,build以後,~\static
目錄下的文件就會被複制到dist/static
目錄下
而實現文件複製的插件是 CopyWebpackPlugin
,使用前記得install
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, './static'),
to: config.assetsSubDirectory,
// ignore: ['.*']
}
])
複製代碼
該項是地址前綴,若是用到了第三方配置資源的地址,那麼這裏就能夠填寫對應的域名。
開發環境中它在
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相信有意去入門webpack的同窗看完了這一系列的文章確定對webpack有了更多的瞭解。
Jason後面會複習一下es6+ 和 想去深刻學習一下node,還會寫一些vue的項目。後面要寫什麼文章可能就看哪方面積累和了解的更深刻,還有哪些內容跟適合總結成文章了。
你們有什麼想和Jason一塊兒學習的前端框架、技術,能夠留言哦,歡迎給意見和交流。
後面會不按期更新,喜歡的同窗能夠點下關注的。