本章描述的主要功能
- postcss-loader
- nodemon
- 使用less/scss/sass
- 封装资源处理,样式处理,并加入全局config(仿照vue)
- 生产开发分离
- 开发环境配置压缩打包
- chunk接受
- 多页面开发原理和配置介绍
代码接着上一章webpack学习笔记(一)从零开始构建基础webpack项目
postcss-loader
postcss-loader介绍 使用postcss-loader
来为css样式自动加入浏览器前缀,postcss-loader
的使用需要配合相对应的postcss.config.js
文件,以及其插件,这里我只使用了autoprefixer
插件
npm i -D postcss-loader autoprefixer
加入postcss-loader
,创建postcss.config.js
文件并写代码
/* webpack.js */
...
{
// 对css文件使用loader
test: /\.css$/,
// 使用插件提取样式
use: ExtractTextPlugin.extract({
// 样式loader运行顺序 加入postcss-loader
use: ['css-loader', 'postcss-loader'],
// 若上述处理进行顺利,执行style-loader并导出文件
fallback: 'style-loader',
// 样式覆盖路径 处理背景图之类
publicPath: '../'
})
}
...
/* postcss.config.js */
module.exports = {
plugins: [
require('autoprefixer')({
browsers: ['last 5 versions']
})
]
};
写一下样式并看效果,这里直接打包生成文件看效果npm run build
已经加入相关了浏览器前缀
nodemon
先附上nodemon的GitHub介绍,我们用它来监控进程,这样我们就不需要每次修改之后还需要重启node服务,下载安装包,并修改package.json
npm i -D nodemon
"scripts": {
"start": "nodemon --watch build/ --exec \"webpack-dev-server --config build/webpack.js\"",
"build": "webpack --config build/webpack.js"
}
我们使用nodemon
监控build
这个文件夹,并且开辟一个子进程运行webpack-dev-server
,然后我们可以先运行npm start
,之后在运行本地服务的时候修改build
文件夹下面的文件就不再需要重启服务
使用less/scss/sass
首先下载loader
,并且修改.css
的loader
,添加对.less
.scss
.sass
文件的处理,.scss
和.sass
公用一个loader
,并且执行sass-loader
需要node-sass
来配合使用
下载安装包
npm i -D less-loader sass-loader node-sass
增加对应的处理loader
,这里就先不分离css了,直接在浏览器上看
/* webpack.js */
...
{
// 对less文件使用loader
test: /\.less$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
},
{
// 对sass文件使用loader
test: /\.sass$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
},
{
// 对scss文件使用loader
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
}
...
创建.less
.scss
.sass
文件,随便写点什么,然后由main.js
引入
源码如下
运行结果如下
封装资源处理,样式处理,并加入全局config(仿照vue)
考虑到之后要的分离,不可能每一个环境配置里面都写一遍loader,而且样式的loader基本相似,顶多预处理的loader不一样而已,用代码来生成这些吧
封装处理资源类(img, media)
build
目录下创建utils.js
,webpack.js
引入utils.js
,并修改图片和媒体文件的loader
处理
/* utils.js */
const path = require('path');
// 静态资源文件+第三方资源存放文件夹名称
const staticDir = 'static';
// 资源路径 背景图片 bgm等,传入路径以及文件名 ex img/xxx.png
exports.assetsPath = _path => path.posix.join(staticDir, _path);
/* webpack.js */
const utils = require('./utils');
...
{
// 对下列资源文件使用loader
test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
loader: 'url-loader',
options: {
// 小于10kb将会转换成base64
limit: 10240,
// 大于10kb的资源输出地[name]是名字[ext]后缀
name: utils.assetsPath('img/[name].[hash:6].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10240,
name: utils.assetsPath('media/[name].[hash:6].[ext]')
}
}
...
这里也就不运行看效果了,只是改了一下执行方法而已~
封装处理样式类
无论怎样封装,最终输出的都必须是对象或类数组对象,类似下面的形式
// 分离样式
{
// xxx.ext
test: /\.xxx$/,
// 使用插件提取样式
use: ExtractTextPlugin.extract({
// 样式loader运行顺序
use: [xx, xx, xx],
fallback: 'style-loader',
// 样式覆盖路径 处理背景图之类
publicPath: '../../'
})
}
// 不分离样式
{
// xxx.ext
test: /\.xxx$/,
use: [xx, xx, xx]
}
第一步,在build
下创建一个全局配置的config.js
文件,之后多页面开发也会用到
/* config.js */
module.exports = {
// 开发
dev: {
// .map文件是否生成
sourceMap: true,
// 是否分离样式
extract: false
},
// 生产
prod: {
sourceMap: false,
extract: true
}
};
重头戏来了,utils.js
加入样式处理函数,具体代码和功能附上
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
// 静态资源文件+第三方资源存放文件夹名称
const staticDir = 'static';
// 资源路径 背景图片 bgm等,传入路径以及文件名 ex img/xxx.png
exports.assetsPath = _path => path.posix.join(staticDir, _path);
/**
* 样式处理 生成样式loader对象,并放入数组里面
* @param { Object } options 对应config.js里面的对象属性
* @return { Array }
*/
exports.styleLoader = (options) => {
options = options || {};
// 两个固定的loader
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
};
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
};
/**
* 生成{test: xxx, use: xxx}中use的值
* @param { String } loader loader名 less, scss, sass, 可为空,空默认css
*/
function generateLoaders(loader) {
// 加入两个固定loader
const loaders = [cssLoader, postcssLoader];
if (loader) {
// 通过名字加入loader,然后从末尾压入数组,感谢loader的执行顺序是从后往前
loaders.push({
loader: `${loader}-loader`,
options: {
// 是否生成.map
sourceMap: options.sourceMap
}
});
}
// 是否需要提取样式
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'style-loader',
publicPath: '../../'
});
}
return ['style-loader'].concat(loaders);
}
// 通过上述函数生成对应的值
const loaders = {
css: generateLoaders(),
less: generateLoaders('less'),
scss: generateLoaders('sass'),
sass: generateLoaders('sass')
};
// console.log(loaders);
// 打印结果如下
// {
// css:
// ['style-loader',
// { loader: 'css-loader', options: [Object] },
// { loader: 'postcss-loader', options: [Object] }],
// less:
// ['style-loader',
// { loader: 'css-loader', options: [Object] },
// { loader: 'postcss-loader', options: [Object] },
// { loader: 'less-loader', options: [Object] }],
// scss:
// ['style-loader',
// { loader: 'css-loader', options: [Object] },
// { loader: 'postcss-loader', options: [Object] },
// { loader: 'sass-loader', options: [Object] }],
// sass:
// ['style-loader',
// { loader: 'css-loader', options: [Object] },
// { loader: 'postcss-loader', options: [Object] },
// { loader: 'sass-loader', options: [Object] }]
// }
const output = [];
// 遍历并加入正则,得到最终的loader
for (const key in loaders) {
const loader = loaders[key];
// console.log(new RegExp(`\\.${key}$`));
output.push({
test: new RegExp(`\\.${key}$`),
use: loader
});
}
return output;
// console.log(JSON.stringify(output));
// 运行结果如下 test其实是有值的,上述打印可见
// [
// {
// "test": {},
// "use": ["style-loader", { "loader": "css-loader", "options": {} }, { "loader": "postcss-loader", "options": {} }]
// },
// {
// "test": {},
// "use": ["style-loader", { "loader": "css-loader", "options": {} }, { "loader": "postcss-loader", "options": {} }, { "loader": "less-loader", "options": {} }]
// },
// {
// "test": {},
// "use": ["style-loader", { "loader": "css-loader", "options": {} }, { "loader": "postcss-loader", "options": {} }, { "loader": "sass-loader", "options": {} }]
// },
// {
// "test": {},
// "use": ["style-loader", { "loader": "css-loader", "options": {} }, { "loader": "postcss-loader", "options": {} }, { "loader": "sass-loader", "options": {} }]
// }
// ]
};
webpack.js
中使用这个方法,删除原有的样式loader
处理方法,并在loaders
数组中通过...utils.styleLoader()
,附上目前webpack.js
代码
/* webpack.js */
// 路径解析
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const rm = require('rimraf');
const utils = require('./utils');
// 删除
rm(path.join(__dirname, '../dist'), (err) => {
if (err) throw err;
});
module.exports = {
// js文件入口
entry: path.join(__dirname, '../src/script/main.js'),
// 输出到dist目录
output: {
path: path.join(__dirname, '../dist'),
filename: 'static/js/[name].[hash].js'
},
// devServer: {
// host: '192.168.0.101',
// port: '2018'
// },
module: {
loaders: [
...utils.styleLoader(),
{
// 对js文件使用loader
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.html$/,
loader: 'html-loader',
options: {
// 标签+属性
attrs: ['img:src', 'audio:src', 'video:src']
}
},
{
// 对下列资源文件使用loader
test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
loader: 'url-loader',
options: {
// 小于10kb将会转换成base64
limit: 10240,
// 大于10kb的资源输出地[name]是名字[ext]后缀
name: utils.assetsPath('img/[name].[hash:6].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10240,
name: utils.assetsPath('media/[name].[hash:6].[ext]')
}
}
]
},
plugins: [
new ExtractTextPlugin('static/css/[name].[hash].css'),
new HtmlWebpackPlugin({
// 生成的html文件名,该文件将被放置在输出目录
filename: 'index.html',
// 源html文件路径
template: path.join(__dirname, '../src/page/index.html')
}),
new CopyWebpackPlugin([{
// 源文件目录
from: path.join(__dirname, '../static'),
// 目标目录 dist目录下
to: 'static',
// 筛选过滤,这里复制所有文件,连同文件夹
ignore: ['.*']
}])
]
};
环境分离
一般工作中都会建立两个环境,开发环境,生成环境,话不多说,先在build
下面创建两个环境文件再说,开发webpack.dev.js
,生产webpack.prod.js
,然后修改package.json
中的运行命令
/* package.json */
"scripts": {
"start": "nodemon --watch build/ --exec \"webpack-dev-server --config build/webpack.dev.js\"",
"build": "webpack --config build/webpack.prod.js"
}
基础配置
在之前的webpack.js
中有一些配置不需要分离出去,如.js
.html
,图片和媒体资源的处理,html-webpack-plugin
的配置,copy-webpack-plugin
等这些基础的配置,开发和生产环境可以共用,其它配置需要分离出去,删除配置之后的代码如下
/* webpack.js */
// 路径解析
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const utils = require('./utils');
module.exports = {
// js文件入口
entry: path.join(__dirname, '../src/script/main.js'),
// 输出到dist目录
output: {
path: path.join(__dirname, '../dist'),
filename: 'static/js/[name].[hash].js'
},
// devServer: {
// host: '192.168.0.101',
// port: '2018'
// },
module: {
loaders: [
{
// 对js文件使用loader
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.html$/,
loader: 'html-loader',
options: {
// 标签+属性
attrs: ['img:src', 'audio:src', 'video:src']
}
},
{
// 对下列资源文件使用loader
test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
loader: 'url-loader',
options: {
// 小于10kb将会转换成base64
limit: 10240,
// 大于10kb的资源输出地[name]是名字[ext]后缀
name: utils.assetsPath('img/[name].[hash:6].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10240,
name: utils.assetsPath('media/[name].[hash:6].[ext]')
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
// 生成的html文件名,该文件将被放置在输出目录
filename: 'index.html',
// 源html文件路径
template: path.join(__dirname, '../src/page/index.html')
}),
new CopyWebpackPlugin([{
// 源文件目录
from: path.join(__dirname, '../static'),
// 目标目录 dist目录下
to: 'static',
// 筛选过滤,这里复制所有文件,连同文件夹
ignore: ['.*']
}])
]
};
webpack-merge
先附上介绍webpack-merge 使用这个包,我们可以合并对象或数组,以及函数,这样我们就可以在开发和生产环境的代码中引入基础配置,用这个包来合并两者,达到环境的构建
npm i -D webpack-merge
暂停一下
写这篇博客的时候,发现自己漏了sourceMap的生成,按照上一篇的配置,即便在config.js
里面设置sourceMap: true
也不会生成.map
文件,原因是我忘记在webpack配置里面加入devtool: 'inline-source-map'
(开发),devtool: 'source-map'
(生产)
开发环境
在开发环境中,我们并需要压缩代码,也不需要分离样式(不过部分移动端手机浏览器不分离样式就没法加载样式),只需开启本地服务,进行项目调试,引入webpack.js
通过webpack-merge
进行添砖加瓦
const baseWebpack = require('./webpack');
const merge = require('webpack-merge');
const config = require('./config');
const utils = require('./utils');
// 部分移动端浏览器不提取样式无法被加载
// const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = merge(baseWebpack, {
devtool: 'inline-source-map',
module: {
loaders: utils.styleLoader(config.dev)
}
// 先保留一下
// plugins: [
// new ExtractTextPlugin('static/css/style.css')
// ]
});
生产环境
生产环境中,我们需要删除dist
文件夹,需要压缩css和js代码,先下载需要两个包uglifyjs-webpack-plugin
js压缩,压缩css我们只需要改动utils.js
(ps:写这片博客前我用的是optimize-css-assets-webpack-plugin
来压缩的,按照loader
的执行顺序,只需要在css-loader
加入minimize
属性即可,然后整理这篇文章的时候发现这样也可以压缩)
npm install -D uglifyjs-webpack-plugin
/* webpack.prod.js */
const path = require('path');
const baseWebpack = require('./webpack');
const merge = require('webpack-merge');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const config = require('./config');
const rm = require('rimraf');
const utils = require('./utils');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
// 删除
rm(path.join(__dirname, '../dist'), (err) => {
if (err) throw err;
});
module.exports = merge(baseWebpack, {
devtool: 'source-map',
module: {
loaders: utils.styleLoader(config.prod)
},
plugins: [
new ExtractTextPlugin('static/css/[name].[hash].css'),
new UglifyJsPlugin({
// 是否生成.map
sourceMap: config.prod.sourceMap
})
]
});
打包然后浏览器打开dist
下面的index.html
。js和css都已被压缩,另外我添加了一个name.js
浏览器打开结果(sourceMap生效了)
多页面开发
前提条件chunk
如果不了解chunk,即便是我们能生成多个html
文件,但其所引用的js文件始终是同一个,这样就达不到多页面开发的要求,毕竟a页面不需要b页面的某些东西。。。
这里简单说一下chunk,我们现在在创建entry
的时候只提供了一个入口文件main.js
,如果我们添加多个入口文件,在src/script/
创建demo1.js
demo2.js
然后我们修改entry
的写法,这些入口文件就是chunk,其chuankName
为entry
的键名,也就是之前一直使用[name]
的值
/* webpack.js */
entry: {
index: path.join(__dirname, '../src/script/main.js'),
demo1: path.join(__dirname, '../src/script/demo1.js'),
demo2: path.join(__dirname, '../src/script/demo2.js')
}
直接打包生成文件看一下npm run build
可以看到生成了index, demo1, demo2这些js文件,不再是之前的main名字的形式,说明我们的设置的chuankName生效了,但是我们的index.html
只想要index.js这个文件,怎么弄呢,别着急,html-webpack-plugin
插件早已提供的方法,其提供了一个chunks
属性,值为一个数组,里面放入需要提取的入口文件的chuankName即可
/* webpack.js */
new HtmlWebpackPlugin({
// 生成的html文件名,该文件将被放置在输出目录
filename: 'index.html',
// 只提取chuankName为index的入口文件打包生成的js
chunks: ['index'],
// 源html文件路径
template: path.join(__dirname, '../src/page/index.html')
})
继续打包一下npm run build
,只引入了一个js
加入一个新的html文件,我们创建demo1.html
,并且使用html-webpack-plugin
来处理它,让它所对应的入口文件为demo1.js
/* webpack.js */
new HtmlWebpackPlugin({
// 生成的html文件名,该文件将被放置在输出目录
filename: 'demo1.html',
chunks: ['demo1'],
// 源html文件路径
template: path.join(__dirname, '../src/page/demo1.html')
})
继续打包
了解了上述原理,多页面开发的配置就一目了然,我们需要创建入口文件entry
对象,还需要配置html文件的HtmlWebpackPlugin插件的数组,并且为了统一配置,我们需要保持entry
对象的键名和html文件的文件名相同,直白一点就是我们创建一个html的时候,需要同时创建一个同名的js文件,并且在entry
对像中注册登记,键名就是其文件名,举个栗子如下
/* 栗子 */
module.exports = {
/* 四个入口文件 */
entry: {
/* 键名 文件名保持一致 */
index: path.join(__dirname, '../src/script/index.js'),
demo1: path.join(__dirname, '../src/script/demo1.js'),
demo2: path.join(__dirname, '../src/script/demo2.js'),
demo3: path.join(__dirname, '../src/script/demo3.js')
},
....
plugins: [
/* 所对应的html文件 */
new HtmlWebpackPlugin({
filename: 'index.html',
chunks: ['index'],
template: path.join(__dirname, '../src/page/index.html')
}),
new HtmlWebpackPlugin({
filename: 'demo1.html',
chunks: ['demo1'],
template: path.join(__dirname, '../src/page/demo1.html')
}),
new HtmlWebpackPlugin({
filename: 'demo2.html',
chunks: ['demo2'],
template: path.join(__dirname, '../src/page/demo2.html')
}),
new HtmlWebpackPlugin({
filename: 'demo3.html',
chunks: ['demo3'],
template: path.join(__dirname, '../src/page/demo3.html')
})
]
};
看完上面的栗子,所谓的多页面开发无非就是通过简单的代码生成上述栗子中的entry
对象 HtmlWebpackPlugin
数组,而且我们要求名称一致,这就便利了我们来通过代码生成这些配置
撸起袖子加油干
通过代码生成对象和数组
/* webpack.js */
// 所有多页面的文件名
const PageName = ['index', 'demo1', 'demo2'];
// entry对象
const Entries = {};
// 插件数组
const HtmlPlugins = [];
// 生成
PageName.forEach((page) => {
const htmlPlugin = new HtmlWebpackPlugin({
filename: `${page}.html`,
template: path.join(__dirname, `../src/page/${page}.html`),
chunks: [page]
});
HtmlPlugins.push(htmlPlugin);
Entries[page] = path.join(__dirname, `../src/script/${page}.js`);
});
/* 配置代码 省略了很多0.0 */
module.exports = {
// js文件入口
entry: Entries,
plugins: [
...HtmlPlugins,
]
};
到这里时候我们需要改一下入口文件的名字,之前我们一直使用的是main.js
,现在我们需要将其改为index.js
,运行一下
html,js一一对应的被生成了,我们只需要手动输入html文件名就能进行多页面,不过为了规范,我们之前创建了一个全局对象config.js
,我们可以将这PageName的值写到config.js
里面,然后在webpack.js
里面调用
/* config.js */
module.exports = {
// 多页面配置
pageNames: [
'index',
'demo1',
'demo2'
],
// 开发
dev: {
sourceMap: true,
extract: false
},
// 生产
prod: {
sourceMap: true,
extract: true
}
};
/* webpack.js */
const config = require('./config');
config.pageNames.forEach((page) => {
....
});
如果进行多页面开发,就自行创建对应文件并在config.js
内注册,如果不使用nodemon
就需要重启node
基础的多页面开发的原理以及配置就说到这里了,下一章的笔记将会描述如何创建模版,提取公共模块~
依旧附上本章GitHub源码