上一期咱们对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一块儿学习的前端框架、技术,能够留言哦,欢迎给意见和交流。
后面会不按期更新,喜欢的同窗能够点下关注的。