1. 多入口配置
首先我们来看下webpack入口怎么配置:
module.exports = {
entry: {
'index' : '../src/index.js'
}
}
多入口只需要配置多个key:value对即可,key就是你要发布的包名,主要为了后面的outpu准备,value就是你入口文件所在的位置
那么问题来了,是不是我每次加一个入口就要手动去添加呢?这样也太不智能了吧!
别急,不就是个读取目录吗,glob可以办到呀,glob是一个node插件,可以帮助我们按照一定规则读取文件的路径,我们就可以根据一定的规则构建出entry了。
var glob = requir('glob')
var getEntry = function() {
var entry = {}
var files = glob.sync(path.resolve(__dirname, '../src/*/index.js'))//你的入口文件相对于当前的路径
files.forEach(file => {
var key = file.split('/').splice(-2, 1)[0]
entry[key] = file
})
return entry
}
module.exports = {
entry: getEntry()
}
这样就解决了我们的多入口问题了
2. 多出口配置
对于多出口webpack还是比较容易实现的,毕竟我们在entry下功夫拿到了key
{
output: {
filename: '[name]/js/vender.[hash].js',
path: path.resolve(__dirname, '../dist')
}
}
是不是很简单,此时你已经可以使用这套配置再加上常规的module配置开发多入口多出的js项目了,但是距离真正的spa项目还差的远,因为你没有HTML呀,也不能自动把打包好的js编译到HTML文件中呀,而且你要想本地调试,你得有个服务器吧,watch太麻烦得刷新页面,你得有个HMR支持吧…是不是说到这已经懵了,别急别急后面还有更多要配置的-。-哈
3. 添加一个HTML模板
webpack有一个插件叫做html-webpack-plugin,这个插件可以帮助我们构建出你想要的模板。配置如下
{
plugins: [
new HtmlWebpackPlugin({
title: 'your title',
filename: 'index.html',// 模板要往哪发布
template: path.resolve(__dirname, '../dist/src/project1/index.html'), // 模板的存放位置
chunks: [name], // chunks主要用于多入口文件,也就是你引用哪些打包的文件
hash: true,
inject: true, // 默认值,决定打包的js在html中的位置,默认在body底部
})
]
}
这样我们就配置完了一个单入口的html-webpack-plugin。
那怎么实现多入口配置呢?答案是有几个入口就new几个HtmlWebpackPlugin,这个是无限制的,但是我们手动去写是不是太麻烦了,程序员嘛,一定要学会使用工具呀!上面我们构建entry的时候就得到了入口文件,那我们是不是也可以仿照上面的方法也构建一个HtmlWebpackPlugin的实例数组呢?为了不浪费性能,我们决定就在一个循环里搞定好了!嗯说干就干,上代码!!
var getEntry = function() {
var entry = {}
var htmlPlugins = []
var files = glob.sync(path.resolve(__dirname, '../src/*/index.js'))//你的入口文件相对于当前的路径
files.forEach(file => {
var key = file.split('/').splice(-2, 1)[0]
entry[key] = file
htmlPlugins.push(new HtmlWebpackPlugin({
title: key,
filename: path.resolve(__dirname, '../dist/' + key + '/index.html'),
template: path.resolve(__dirname, '../src/' + key + '/index.html'),
chunk: [key],
hash: true,
inject: true
}))
})
return {entry, htmlPlugins}
}
var {htmlPlugins} = getEntry()
module.exports = {
plugins: [
...htmlPlugins
]
}
此时你激动地去运行了一下 webpack 确实包如期打好了,但是我们发现里面的js文件貌似路径不太对,恭喜,你又学到了一个新东西,就是publicPath,它是用来为你的资源指定正确的存放位置的。
{
output: {
....
publicPath: '../', // 因为我们的html和js打包的路径都是基于页面应用project1或者project2路径打包的,所以需要跳出一级,引用路径才能正常访问到。
}
}
现在HTML,JS都具备了,我们就差一个本地服务就能玩起来了,别急webpack早就帮你安排好了server
4. webpack-dev-server配置
webpack-dev-server可以帮助我们在本地建立一个静态资源服务器,并且为我们提供了HMR热重载技术,使得我们不需要刷新就可以进行组件更新和调试,像极了你在devtools中直接修改代码的样子!是不是很爽
devServer : {
contentBase: path.resolve(__dirname, '../dist'),// 指定了服务器根目录
compress: true, // 启用gzip压缩
hot: true, // 启用HMR
port: 8080,
publicPath: '/', // 如何访问资源总是以'/'开头,需要根据我们的output决定如何配置
}
此时运行 webpack serve,就可以在http://localhost:8080/project1/index.html访问我们打包好的HTML应用了。
5. 基础module配置
此时我们具备了开发多入口多出口的工程化能力了,那我们就可以开发react或者vue了,只要具有相应的loader就好了,在webpack中,每一个文件就是module,而loader其实就是一个函数,可以流式地帮助我们处理文件。
module: {
rules: [
{
test: '/.(js|jsx|ts|tsx)$/',
exclude: '/node_modules/',
loader: 'babel-loader'
},
{
test: '/.vue$/',
loader: 'vue-loader'
},
{
test: '/.(css | scss)$/',
use: [
'style-loader',
'css-loader',
'sass-loader'
]
},
{
test: '/.(jpg|png|jpeg|gif|eot|svg|ttf|woff|woff2)$/',
loader: 'url-loader'
}
]
},
plugins: [
new VueLoaderPlugin(),
]
好了,现在你真的可以去开发项目了!!但是配置还有很大的优化空间
6. 打包优化
-
抽离公共依赖
我们知道,我们一个页面有时候可能会引用多个vue实例化root组件,但是基于现在的打包情况,我们只能把vue或者react打包到应用程序中,整个包体积就会很大,那可不可以多个包公用一个依赖呢,比如我把依赖用CDN的方式引入可以吗,当然,webpack在入口文件递归每一个module的时候,就会组成一个构建树,构建树就叫做chunk,chunk给我们组织好了依赖关系,所以我们也可以通知webpack,树上的哪些东西我们不需要,webpack就会为我们进行tree-shaking把这个依赖从树上摇下来。
通过配置externals就可以满足需求
{ externals: { 'vue' : 'Vue', 'vue-router': 'VueRouter', // key就是你import进来的包的位置,value就是暴露的包名 } }
然后我们在html模板中引入对应的js即可,值得注意的是引用时也可以找到性能最佳的引用,例如vue.runtime.min.js,要比我们的开发时构建时esm体积小,性能更好,因为去掉了模板编译的代码,体积小了30%左右,前提是你的vue文件已经被vue-loader编译为了render函数,才可以使用这个运行时。
-
抽离CSS
现在我们的CSS是被style-loader直接打包到了项目js文件中,style-loader主要是帮助我们把css对象转化成字符串并且通过DOM的api追加到head中的,大概原理是这样,细节其实还是有很多考量的。
那我们为什么要抽离CSS,单纯就是想把CSS拿出来?在JS中放着不也能正常显示麽,费这劲干嘛。这就要说到浏览器的解析机制了,浏览器在得到HTML文档的时候,从上到下HTML解析为DOM,然后生成DOM树,同时解析CSS的时候生成CSSOM,生成CSSOM树,DOM树和CSSOM树结合构建出了render树,render树确定好以后会根据dom和定位构建出布局layout,最后进行paint绘制,当遇到内联的JS时,就会阻塞DOM的解析,而且当上面的CSS没有加载完毕的时候,JavaScript 执行将暂停,直至 CSSOM 就绪(因为JS能查询修改CSSOM和DOM)。
重新追加的内嵌的CSS同理会让CSSOM不得不重排或重绘,
所以我们找到了问题,我们的JS打包时设置了output.inject = true,所以就会把代码放到body后面,此时DOM已经解析完了,但是我们在JS代码执行的时候,又加了一大段CSS到head中,那浏览器不得不重新走一遍构建流程,不仅如此,用户在没有CSS的时候,看到的就是一块几乎没有意义的首屏,CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。所以我们要把CSS放在head中,并且让他尽快下载并加装到浏览器参与构建CSSOM与渲染,而不是等到最后。
说了这么多题外话,我们看看如何抽离,只需要在loader中新增一个loader处理,并且一定要把style-loader移除,因为你抽离了CSS为单独文件,就没有document对象了,style-loader不仅没有意义而且还会报错。
{
test: /.(sc|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader',
]
},
.......
plugins: [
new MiniCssExtractPlugin({filename: '[name]/css/[name].css'}) // path就是默认的output.path
]
- 清除dist的冗余包
plugins: [
new CleanWebpackPlugin() // 默认清除output.path
]
-
我们发现,开发的久了,项目里的入口文件越来越多,打包也变得非常慢,有时候我们只想打包某一个指定目录下的入口文件,并不想全部打包,有办法操作吗?
当然,我们可以在node中读取环境变量,从而根据命令行下发的指令去构建entry,这样就可以定点打包了。
具体流程大概是这样:
命令行输入打包目录 => 通过环境变量取到目录名=>根据目录名在遍历时过滤不需要的entry=>构建出需要打包的entry=>正常打包即可
代码实现也比较容易
1.命令行增加后缀: npm run dev --run:project1 2.process.env.npm_config_argv['original'] 获取到命令值数组 3.截取得到project1,具体怎么截取很简单这里就不罗列了 4.写入到环境变量,process.env.BUILD_DIR = 'project1' 5.let buildFile = process.env.BUILD_DIR files = files.filter(file => file.indexOf(buildFile)!==-1) 过滤一个新的文件集合 6.进行构建entry 7.正常打包
-
定义一些环境变量方便webpack打包时候注入环境变量到我们的js
plugins: [
new DefineWebpackPlugins({
'process.env.ENV_NAME' : """ + process.env.ENV_NAME + """,
'process.env.BASE_URL' : """ + process.env.BASE_URL + """"
})
]
- 把错误信息从控制台或者命令行输出到页面
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
devServer: {
...
quiet: true,
overlay: {
errors: true
},
...
}
plugins: [
new FriendlyErrorsWebpackPlugin(),
]
- 写一个webpack插件
- 一个 JavaScript 函数或 JavaScript 类,用于承接这个插件模块的所有逻辑;
- 在它原型上定义的
apply
方法,会在安装插件时被调用,并被 webpack compiler 调用一次; - 指定一个触及到 webpack 本身的事件钩子,即下文会提及的 hooks,用于特定时机处理额外的逻辑;
- 对 webpack 实例内部做一些操作处理;
- 在功能流程完成后可以调用 webpack 提供的回调函数;
插件就是一个函数,函数的原型上要有一个apply方法,webpack会调用该方法,并且把webpack实例传入apply中,webpack执行会有一些事件钩子供我们处理,并且我们可以对实例内部进行操作,还可以执行回调。
根据插件所能触及到的 event hook(事件钩子),对其进行分类。每个 event hook 都被预先定义为 synchronous hook(同步), asynchronous hook(异步), waterfall hook(瀑布), parallel hook(并行),而在 webpack 内部会使用 call/callAsync 方法调用这些 hook。 —— webpack 中文文档
如果你不进一步追究,那么按照如下所示的方式对不同钩子进行 tap 处理即可,其中 tap 方法用于同步处理,异步方式则可以调用 tapAsync 方法或 tapPromise 方法。
class HelloWorldPlugin {
apply(compiler) {
console.log(compiler, 'Hello World before!')
compiler.hooks.done.tap(
'HelloAsyncPlugin',
compilation => {
console.log("webpack build done.");
}
);
}
}
module.exports = HelloWorldPlugin
最后
webpack为我们提供了十分便利的模块化开发,推动了前端的组件化工程化,依靠流程式和丰富的拓展插件配置的打包模式,在目前来说仍是被各大cli主流使用的打包工具,但是随着ES MODULE的兴起与落地,基于snowpack的vite已经面世,并且效率更高,速度更快,且更为易用,有空我会专门出一期vite的新手配置教程供大家参考,最后放上webpack的全部配置与本地的架构目录。
架构
build
plugins
env_config
index.js
webpack.base.conf.js
src
project1
index.js
index.html
project2
index.js
index.html
npm脚本命令
"scripts": {
"lint": "eslint --ext .js,.vue src",
"start:dev": "cross-env NODE_ENV=development webpack serve --config build/webpack.base.conf.js",
"watch": "npm run dev --m dev",
"test": "echo "Error: no test specified" && exit 1",
"dev": "cross-env NODE_ENV=development webpack --config build/webpack.base.conf.js --progress"
},
环境变量配置
var env = {
dev: {
env_name: 'dev',
base_url: '//dev.webpack.com/'
},
test: {
env_name: 'test',
base_url: '//test.webpack.com/'
},
build: {
env_name: 'build',
base_url: '//build.webpack.com/'
}
}
let type = JSON.parse(process.env.npm_config_argv)['original'].pop()
let buildDir = ''
if(type.includes('--run:')) {
buildDir = type.replace('--run:', '')
}
if('dev_test_build'.includes(type)) {
type = type.split(':').pop()
} else {
type = 'dev'
}
console.log(type, 'type')
console.log(process.env.npm_config_argv, 'process.env.npm_config_argv')
process.env.ENV_NAME = env[type || 'dev'].env_name
process.env.BASE_URL = env[type || 'dev'].base_url
process.env.BUILD_DIR = buildDir
module.exports.env = env
webpack.base.conf.js
const path = require('path')
const glob = require('glob')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const {
VueLoaderPlugin
} = require('vue-loader')
const HelloWorldPlugin = require('./plugins/hell-world-plugin')
const vueLoaderConfig = require('./vue_config/vue-loader.conf')
require('./env_config/index.js')
function getEntry() {
let entrys = {}
let htmlPlugins = []
let files = glob.sync(path.resolve(__dirname, '../src/*/index.js'))
let buildFile = process.env.BUILD_DIR
files = files.filter(file => file.indexOf(buildFile)!==-1)
files.forEach(item => {
const name = item.split('/').splice(-2, 1)[0]
entrys[name] = item
htmlPlugins.push(new HtmlWebpackPlugin({
title: name,
filename: path.resolve(__dirname, '../dist/' + name + '/index.html'),
chunks: [name],
hash: true,
template: path.resolve(__dirname, '../src/' + name + '/index.html'),
inject: true
}))
})
return {entrys, htmlPlugins}
}
const {entrys, htmlPlugins} = getEntry()
module.exports = (env, option) => {
return {
entry: entrys,
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@' : path.resolve(__dirname, '../src/')
},
extensions: ['.js', '.jsx', '.vue']
},
output: {
filename: '[name]/js/vendor.[hash].js',
path: path.resolve(__dirname, '../dist'),
publicPath: '../'
},
devServer: {
contentBase: path.resolve(__dirname, '../dist'),
compress: true,
port: 8080,
hot: true,
quiet: true,
overlay: {
errors: true
},
publicPath: '/',
after() {
console.log(`服务已启动---运行在 http://localhost:8080`)
}
},
module: {
rules: [
{
test: /.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
// presets: ['@babel/preset-env', '@babel/preset-react', '@vue/babel-preset-jsx']
}
},
{
test: /.vue$/,
loader: 'vue-loader',
// options: vueLoaderConfig
},
{
test: /.(sc|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader',
]
},
{
test: /.(jpg|png|jpeg|gif|eot|svg|ttf|woff|woff2)$/,
loader: "url-loader"
},
]
},
plugins: [
new CleanWebpackPlugin(),
new FriendlyErrorsWebpackPlugin(),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env.ENV_NAME' : """ + process.env.ENV_NAME + """,
'process.env.BASE_URL' : """ + process.env.BASE_URL + """
}),
new HelloWorldPlugin({options: true}),
new MiniCssExtractPlugin({filename: '[name]/css/[name].css'}),
...htmlPlugins
],
externals: {
'vue': 'Vue',
'vue-router': "VueRouter"
}
}
}